주석화 진행상황 정리하고 핵심 모듈에 한글 주석 추가

This commit is contained in:
JiWoong Sul
2025-09-29 19:39:35 +09:00
parent 9467b8c87f
commit 47c87dc118
82 changed files with 596 additions and 5 deletions

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/customer.dart';
/// 고객(Customer) API 응답을 다루는 DTO.
class CustomerDto {
CustomerDto({
this.id,
@@ -36,6 +37,7 @@ class CustomerDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// 원본 JSON으로부터 DTO를 생성한다.
factory CustomerDto.fromJson(Map<String, dynamic> json) {
return CustomerDto(
id: json['id'] as int?,
@@ -57,6 +59,7 @@ class CustomerDto {
);
}
/// DTO를 JSON 맵으로 변환한다.
Map<String, dynamic> toJson() {
return {
if (id != null) 'id': id,
@@ -76,6 +79,7 @@ class CustomerDto {
};
}
/// DTO를 도메인 [Customer] 엔티티로 변환한다.
Customer toEntity() => Customer(
id: id,
customerCode: customerCode,
@@ -93,6 +97,7 @@ class CustomerDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 파싱해 [PaginatedResult] 형식으로 반환한다.
static PaginatedResult<Customer> parsePaginated(Map<String, dynamic>? json) {
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
final items = rawItems
@@ -108,6 +113,7 @@ class CustomerDto {
}
}
/// 고객 주소의 우편번호 정보를 담는 DTO.
class CustomerZipcodeDto {
CustomerZipcodeDto({
required this.zipcode,
@@ -121,6 +127,7 @@ class CustomerZipcodeDto {
final String? sigungu;
final String? roadName;
/// JSON에서 우편번호 정보를 파싱한다.
factory CustomerZipcodeDto.fromJson(Map<String, dynamic> json) {
return CustomerZipcodeDto(
zipcode: json['zipcode'] as String,
@@ -130,6 +137,7 @@ class CustomerZipcodeDto {
);
}
/// DTO를 JSON 맵으로 직렬화한다.
Map<String, dynamic> toJson() {
return {
'zipcode': zipcode,
@@ -139,6 +147,7 @@ class CustomerZipcodeDto {
};
}
/// DTO를 [CustomerZipcode] 엔티티로 변환한다.
CustomerZipcode toEntity() => CustomerZipcode(
zipcode: zipcode,
sido: sido,
@@ -147,6 +156,7 @@ class CustomerZipcodeDto {
);
}
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;
@@ -154,6 +164,7 @@ DateTime? _parseDate(Object? value) {
return null;
}
/// 고객 입력 모델을 API 요청 바디로 변환한다.
Map<String, dynamic> customerInputToJson(CustomerInput input) {
final map = input.toPayload();
map.removeWhere((key, value) => value == null);

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/customer.dart';
import '../../domain/repositories/customer_repository.dart';
import '../dtos/customer_dto.dart';
/// 고객(API) CRUD를 호출하는 원격 저장소 구현체.
class CustomerRepositoryRemote implements CustomerRepository {
CustomerRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
static const _basePath = '/customers';
/// 고객 목록을 조회한다.
@override
Future<PaginatedResult<Customer>> list({
int page = 1,
@@ -37,6 +39,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
return CustomerDto.parsePaginated(response.data ?? const {});
}
/// 고객을 생성한다.
@override
Future<Customer> create(CustomerInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -48,6 +51,7 @@ class CustomerRepositoryRemote implements CustomerRepository {
return CustomerDto.fromJson(data).toEntity();
}
/// 고객 정보를 수정한다.
@override
Future<Customer> update(int id, CustomerInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -59,11 +63,13 @@ class CustomerRepositoryRemote implements CustomerRepository {
return CustomerDto.fromJson(data).toEntity();
}
/// 고객을 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 고객을 복구한다.
@override
Future<Customer> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -1,3 +1,4 @@
/// 고객(Customer) 도메인 엔티티.
class Customer {
Customer({
this.id,
@@ -31,6 +32,7 @@ class Customer {
final DateTime? createdAt;
final DateTime? updatedAt;
/// 선택한 속성만 변경한 새 인스턴스를 반환한다.
Customer copyWith({
int? id,
String? customerCode,
@@ -66,6 +68,7 @@ class Customer {
}
}
/// 고객 주소의 우편번호/행정구역 정보를 표현한다.
class CustomerZipcode {
CustomerZipcode({
required this.zipcode,
@@ -80,6 +83,7 @@ class CustomerZipcode {
final String? roadName;
}
/// 고객 생성/수정 시 사용하는 입력 모델.
class CustomerInput {
CustomerInput({
required this.customerCode,
@@ -105,6 +109,7 @@ class CustomerInput {
final bool isActive;
final String? note;
/// API 요청 바디에 사용하기 위한 맵으로 직렬화한다.
Map<String, dynamic> toPayload() {
return {
'customer_code': customerCode,

View File

@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../entities/customer.dart';
/// 고객 데이터를 다루는 도메인 저장소 인터페이스.
abstract class CustomerRepository {
/// 고객 목록을 조회한다.
Future<PaginatedResult<Customer>> list({
int page = 1,
int pageSize = 20,
@@ -12,11 +14,15 @@ abstract class CustomerRepository {
bool? isActive,
});
/// 고객을 생성한다.
Future<Customer> create(CustomerInput input);
/// 고객을 수정한다.
Future<Customer> update(int id, CustomerInput input);
/// 고객을 삭제한다.
Future<void> delete(int id);
/// 삭제된 고객을 복구한다.
Future<Customer> restore(int id);
}

View File

@@ -4,10 +4,13 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../domain/entities/customer.dart';
import '../../domain/repositories/customer_repository.dart';
/// 고객 유형 필터 옵션.
enum CustomerTypeFilter { all, partner, general }
/// 고객 활성 상태 필터 옵션.
enum CustomerStatusFilter { all, activeOnly, inactiveOnly }
/// 고객 목록 조회/등록/수정을 담당하는 프레젠테이션 컨트롤러.
class CustomerController extends ChangeNotifier {
static const int defaultPageSize = 20;
@@ -34,6 +37,7 @@ class CustomerController extends ChangeNotifier {
int get pageSize => _pageSize;
String? get errorMessage => _errorMessage;
/// 고객 목록을 조회한다. 필터/페이지 상태는 내부에서 유지된다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -82,6 +86,7 @@ class CustomerController extends ChangeNotifier {
}
}
/// 검색어를 변경한다.
void updateQuery(String value) {
if (_query == value) {
return;
@@ -90,6 +95,7 @@ class CustomerController extends ChangeNotifier {
notifyListeners();
}
/// 고객 유형 필터를 변경한다.
void updateTypeFilter(CustomerTypeFilter filter) {
if (_typeFilter == filter) {
return;
@@ -98,6 +104,7 @@ class CustomerController extends ChangeNotifier {
notifyListeners();
}
/// 고객 활성 상태 필터를 변경한다.
void updateStatusFilter(CustomerStatusFilter filter) {
if (_statusFilter == filter) {
return;
@@ -106,6 +113,7 @@ class CustomerController extends ChangeNotifier {
notifyListeners();
}
/// 페이지 크기를 변경한다.
void updatePageSize(int size) {
if (size <= 0 || _pageSize == size) {
return;
@@ -114,6 +122,7 @@ class CustomerController extends ChangeNotifier {
notifyListeners();
}
/// 신규 고객을 생성한다.
Future<Customer?> create(CustomerInput input) async {
_setSubmitting(true);
try {
@@ -129,6 +138,7 @@ class CustomerController extends ChangeNotifier {
}
}
/// 기존 고객을 수정한다.
Future<Customer?> update(int id, CustomerInput input) async {
_setSubmitting(true);
try {
@@ -144,6 +154,7 @@ class CustomerController extends ChangeNotifier {
}
}
/// 고객을 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -159,6 +170,7 @@ class CustomerController extends ChangeNotifier {
}
}
/// 삭제된 고객을 복구한다.
Future<Customer?> restore(int id) async {
_setSubmitting(true);
try {
@@ -174,6 +186,7 @@ class CustomerController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();

View File

@@ -16,6 +16,7 @@ import '../../domain/entities/customer.dart';
import '../../domain/repositories/customer_repository.dart';
import '../controllers/customer_controller.dart';
/// 고객 관리 화면. 기능 플래그에 따라 사양 페이지를 보여주거나 실제 목록을 노출한다.
class CustomerPage extends StatelessWidget {
const CustomerPage({super.key, required this.routeUri});
@@ -86,6 +87,7 @@ class CustomerPage extends StatelessWidget {
}
}
/// 고객 관리 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _CustomerEnabledPage extends StatefulWidget {
const _CustomerEnabledPage({required this.routeUri});
@@ -95,6 +97,7 @@ class _CustomerEnabledPage extends StatefulWidget {
State<_CustomerEnabledPage> createState() => _CustomerEnabledPageState();
}
/// 고객 목록 UI와 라우트 파라미터 싱크를 담당하는 상태 클래스.
class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
late final CustomerController _controller;
final TextEditingController _searchController = TextEditingController();
@@ -403,6 +406,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
GoRouter.of(context).go(newLocation);
}
/// URL 파라미터에서 고객 유형 필터 값을 파싱한다.
CustomerTypeFilter _typeFromParam(String? value) {
switch (value) {
case 'partner':
@@ -414,6 +418,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
}
}
/// 고객 유형 필터를 URL 파라미터 문자열로 변환한다.
String? _encodeType(CustomerTypeFilter filter) {
switch (filter) {
case CustomerTypeFilter.all:
@@ -425,6 +430,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
}
}
/// URL 파라미터에서 고객 활성 상태를 파싱한다.
CustomerStatusFilter _statusFromParam(String? value) {
switch (value) {
case 'active':
@@ -436,6 +442,7 @@ class _CustomerEnabledPageState extends State<_CustomerEnabledPage> {
}
}
/// 고객 상태 필터를 URL 파라미터 문자열로 변환한다.
String? _encodeStatus(CustomerStatusFilter filter) {
switch (filter) {
case CustomerStatusFilter.all:

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/group.dart';
/// 권한 그룹(Group) API 응답을 표현하는 DTO.
class GroupDto {
GroupDto({
this.id,
@@ -26,6 +27,7 @@ class GroupDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 그룹 정보를 파싱한다.
factory GroupDto.fromJson(Map<String, dynamic> json) {
return GroupDto(
id: json['id'] as int?,
@@ -40,6 +42,7 @@ class GroupDto {
);
}
/// DTO를 도메인 [Group] 엔티티로 변환한다.
Group toEntity() => Group(
id: id,
groupName: groupName,
@@ -52,6 +55,7 @@ class GroupDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<Group> parsePaginated(Map<String, dynamic>? json) {
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
final items = rawItems
@@ -67,6 +71,7 @@ class GroupDto {
}
}
/// 문자열/DateTime을 파싱해 [DateTime]으로 반환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/group.dart';
import '../../domain/repositories/group_repository.dart';
import '../dtos/group_dto.dart';
/// 권한 그룹 API를 호출하는 원격 저장소 구현체.
class GroupRepositoryRemote implements GroupRepository {
GroupRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class GroupRepositoryRemote implements GroupRepository {
static const _basePath = '/groups';
/// 그룹 목록을 조회한다.
@override
Future<PaginatedResult<Group>> list({
int page = 1,
@@ -35,6 +37,7 @@ class GroupRepositoryRemote implements GroupRepository {
return GroupDto.parsePaginated(response.data ?? const {});
}
/// 새 그룹을 생성한다.
@override
Future<Group> create(GroupInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -46,6 +49,7 @@ class GroupRepositoryRemote implements GroupRepository {
return GroupDto.fromJson(data).toEntity();
}
/// 그룹 정보를 수정한다.
@override
Future<Group> update(int id, GroupInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -57,11 +61,13 @@ class GroupRepositoryRemote implements GroupRepository {
return GroupDto.fromJson(data).toEntity();
}
/// 그룹을 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 그룹을 복구한다.
@override
Future<Group> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -4,8 +4,10 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../domain/entities/group.dart';
import '../../domain/repositories/group_repository.dart';
/// 기본 그룹 여부 필터.
enum GroupDefaultFilter { all, defaultOnly, nonDefault }
/// 그룹 사용 상태 필터.
enum GroupStatusFilter { all, activeOnly, inactiveOnly }
/// 그룹 마스터 화면 상태 컨트롤러
@@ -34,6 +36,7 @@ class GroupController extends ChangeNotifier {
GroupStatusFilter get statusFilter => _statusFilter;
String? get errorMessage => _errorMessage;
/// 그룹 목록을 조회한다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -65,21 +68,25 @@ class GroupController extends ChangeNotifier {
}
}
/// 검색어를 변경한다.
void updateQuery(String value) {
_query = value;
notifyListeners();
}
/// 기본 그룹 여부 필터를 변경한다.
void updateDefaultFilter(GroupDefaultFilter filter) {
_defaultFilter = filter;
notifyListeners();
}
/// 사용 여부 필터를 변경한다.
void updateStatusFilter(GroupStatusFilter filter) {
_statusFilter = filter;
notifyListeners();
}
/// 새 그룹을 생성한다.
Future<Group?> create(GroupInput input) async {
_setSubmitting(true);
try {
@@ -95,6 +102,7 @@ class GroupController extends ChangeNotifier {
}
}
/// 그룹 정보를 수정한다.
Future<Group?> update(int id, GroupInput input) async {
_setSubmitting(true);
try {
@@ -110,6 +118,7 @@ class GroupController extends ChangeNotifier {
}
}
/// 그룹을 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -125,6 +134,7 @@ class GroupController extends ChangeNotifier {
}
}
/// 삭제된 그룹을 복구한다.
Future<Group?> restore(int id) async {
_setSubmitting(true);
try {
@@ -140,11 +150,13 @@ class GroupController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();
}
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
void _setSubmitting(bool value) {
_isSubmitting = value;
notifyListeners();

View File

@@ -13,6 +13,7 @@ import '../../domain/entities/group.dart';
import '../../domain/repositories/group_repository.dart';
import '../controllers/group_controller.dart';
/// 권한 그룹 관리 페이지. 기능 플래그에 따라 사양 화면 또는 실제 목록을 보여준다.
class GroupPage extends StatelessWidget {
const GroupPage({super.key});
@@ -69,6 +70,7 @@ class GroupPage extends StatelessWidget {
}
}
/// 그룹 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _GroupEnabledPage extends StatefulWidget {
const _GroupEnabledPage();
@@ -76,6 +78,7 @@ class _GroupEnabledPage extends StatefulWidget {
State<_GroupEnabledPage> createState() => _GroupEnabledPageState();
}
/// 그룹 목록과 필터/폼 상태를 관리하는 상태 클래스.
class _GroupEnabledPageState extends State<_GroupEnabledPage> {
late final GroupController _controller;
final TextEditingController _searchController = TextEditingController();

View File

@@ -2,6 +2,7 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../domain/entities/group_permission.dart';
/// 그룹별 메뉴 권한을 표현하는 DTO.
class GroupPermissionDto {
GroupPermissionDto({
this.id,
@@ -31,6 +32,7 @@ class GroupPermissionDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 권한 정보를 파싱한다.
factory GroupPermissionDto.fromJson(Map<String, dynamic> json) {
return GroupPermissionDto(
id: json['id'] as int?,
@@ -52,6 +54,7 @@ class GroupPermissionDto {
);
}
/// DTO를 도메인 [GroupPermission] 엔티티로 변환한다.
GroupPermission toEntity() => GroupPermission(
id: id,
group: group.toEntity(),
@@ -67,6 +70,7 @@ class GroupPermissionDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<GroupPermission> parsePaginated(
Map<String, dynamic>? json,
) {
@@ -84,12 +88,14 @@ class GroupPermissionDto {
}
}
/// 권한 설정에 포함된 그룹 정보를 담는 DTO.
class GroupPermissionGroupDto {
GroupPermissionGroupDto({required this.id, required this.groupName});
final int id;
final String groupName;
/// JSON에서 그룹 정보를 파싱한다.
factory GroupPermissionGroupDto.fromJson(Map<String, dynamic> json) {
return GroupPermissionGroupDto(
id: json['id'] as int? ?? json['group_id'] as int,
@@ -98,16 +104,19 @@ class GroupPermissionGroupDto {
);
}
/// DTO를 [GroupPermissionGroup] 엔티티로 변환한다.
GroupPermissionGroup toEntity() =>
GroupPermissionGroup(id: id, groupName: groupName);
}
/// 권한 대상 메뉴 정보를 담는 DTO.
class GroupPermissionMenuDto {
GroupPermissionMenuDto({required this.id, required this.menuName});
final int id;
final String menuName;
/// JSON에서 메뉴 정보를 파싱한다.
factory GroupPermissionMenuDto.fromJson(Map<String, dynamic> json) {
return GroupPermissionMenuDto(
id: json['id'] as int? ?? json['menu_id'] as int,
@@ -115,10 +124,12 @@ class GroupPermissionMenuDto {
);
}
/// DTO를 [GroupPermissionMenu] 엔티티로 변환한다.
GroupPermissionMenu toEntity() =>
GroupPermissionMenu(id: id, menuName: menuName);
}
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/group_permission.dart';
import '../../domain/repositories/group_permission_repository.dart';
import '../dtos/group_permission_dto.dart';
/// 그룹-메뉴 권한 API를 호출하는 원격 저장소.
class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
GroupPermissionRepositoryRemote({required ApiClient apiClient})
: _api = apiClient;
@@ -14,6 +15,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
static const _basePath = '/group-menu-permissions';
/// 그룹 권한 목록을 조회한다.
@override
Future<PaginatedResult<GroupPermission>> list({
int page = 1,
@@ -39,6 +41,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
return GroupPermissionDto.parsePaginated(response.data ?? const {});
}
/// 그룹 권한을 생성한다.
@override
Future<GroupPermission> create(GroupPermissionInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -50,6 +53,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
return GroupPermissionDto.fromJson(data).toEntity();
}
/// 그룹 권한을 수정한다.
@override
Future<GroupPermission> update(int id, GroupPermissionInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -61,11 +65,13 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository {
return GroupPermissionDto.fromJson(data).toEntity();
}
/// 그룹 권한을 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 그룹 권한을 복구한다.
@override
Future<GroupPermission> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../entities/group_permission.dart';
/// 그룹-메뉴 권한을 다루는 도메인 저장소 인터페이스.
abstract class GroupPermissionRepository {
/// 권한 목록을 조회한다.
Future<PaginatedResult<GroupPermission>> list({
int page = 1,
int pageSize = 20,
@@ -12,11 +14,15 @@ abstract class GroupPermissionRepository {
bool includeDeleted = false,
});
/// 그룹 권한을 생성한다.
Future<GroupPermission> create(GroupPermissionInput input);
/// 그룹 권한을 수정한다.
Future<GroupPermission> update(int id, GroupPermissionInput input);
/// 그룹 권한을 삭제한다.
Future<void> delete(int id);
/// 삭제된 그룹 권한을 복구한다.
Future<GroupPermission> restore(int id);
}

View File

@@ -8,6 +8,7 @@ import '../../../menu/domain/repositories/menu_repository.dart';
import '../../domain/entities/group_permission.dart';
import '../../domain/repositories/group_permission_repository.dart';
/// 그룹 권한 활성 여부 필터.
enum GroupPermissionStatusFilter { all, activeOnly, inactiveOnly }
/// 그룹-메뉴 권한 화면용 컨트롤러
@@ -53,6 +54,7 @@ class GroupPermissionController extends ChangeNotifier {
List<Group> get groups => List.unmodifiable(_groups);
List<MenuItem> get menus => List.unmodifiable(_menus);
/// 그룹 목록을 로드해 권한 연결 시 선택할 수 있도록 준비한다.
Future<void> loadGroups() async {
_isLoadingGroups = true;
notifyListeners();
@@ -69,6 +71,7 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 메뉴 목록을 로드해 권한 연결 시 선택할 수 있도록 준비한다.
Future<void> loadMenus() async {
_isLoadingMenus = true;
notifyListeners();
@@ -89,6 +92,7 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 그룹 권한 목록을 조회한다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -116,26 +120,31 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 그룹 필터를 변경한다.
void updateGroupFilter(int? groupId) {
_groupFilter = groupId;
notifyListeners();
}
/// 메뉴 필터를 변경한다.
void updateMenuFilter(int? menuId) {
_menuFilter = menuId;
notifyListeners();
}
/// 권한 활성 상태 필터를 변경한다.
void updateStatusFilter(GroupPermissionStatusFilter filter) {
_statusFilter = filter;
notifyListeners();
}
/// 삭제 포함 여부를 변경한다.
void updateIncludeDeleted(bool value) {
_includeDeleted = value;
notifyListeners();
}
/// 그룹 권한을 생성한다.
Future<GroupPermission?> create(GroupPermissionInput input) async {
_setSubmitting(true);
try {
@@ -151,6 +160,7 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 그룹 권한을 수정한다.
Future<GroupPermission?> update(int id, GroupPermissionInput input) async {
_setSubmitting(true);
try {
@@ -166,6 +176,7 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 그룹 권한을 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -181,6 +192,7 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 삭제된 그룹 권한을 복구한다.
Future<GroupPermission?> restore(int id) async {
_setSubmitting(true);
try {
@@ -196,11 +208,13 @@ class GroupPermissionController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();
}
/// 제출 상태 플래그를 갱신하고 리스너에게 알린다.
void _setSubmitting(bool value) {
_isSubmitting = value;
notifyListeners();

View File

@@ -18,6 +18,7 @@ import '../../domain/entities/group_permission.dart';
import '../../domain/repositories/group_permission_repository.dart';
import '../controllers/group_permission_controller.dart';
/// 그룹-메뉴 권한 설정 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
class GroupPermissionPage extends StatelessWidget {
const GroupPermissionPage({super.key});
@@ -99,6 +100,7 @@ class GroupPermissionPage extends StatelessWidget {
}
}
/// 그룹 권한 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _GroupPermissionEnabledPage extends StatefulWidget {
const _GroupPermissionEnabledPage();
@@ -107,6 +109,7 @@ class _GroupPermissionEnabledPage extends StatefulWidget {
_GroupPermissionEnabledPageState();
}
/// 그룹 권한 목록/필터/폼 상태를 관리하는 상태 클래스.
class _GroupPermissionEnabledPageState
extends State<_GroupPermissionEnabledPage> {
late final GroupPermissionController _controller;

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/menu.dart';
/// 메뉴(Menu) API 응답을 표현하는 DTO.
class MenuDto {
MenuDto({
this.id,
@@ -30,6 +31,7 @@ class MenuDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 메뉴 정보를 파싱한다.
factory MenuDto.fromJson(Map<String, dynamic> json) {
return MenuDto(
id: json['id'] as int?,
@@ -50,6 +52,7 @@ class MenuDto {
);
}
/// DTO를 도메인 [MenuItem]으로 변환한다.
MenuItem toEntity() => MenuItem(
id: id,
menuCode: menuCode,
@@ -64,6 +67,7 @@ class MenuDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<MenuItem> parsePaginated(Map<String, dynamic>? json) {
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
final items = rawItems
@@ -79,12 +83,14 @@ class MenuDto {
}
}
/// 하위 메뉴 요약 정보를 담는 DTO.
class MenuSummaryDto {
MenuSummaryDto({required this.id, required this.menuName});
final int id;
final String menuName;
/// JSON에서 요약 정보를 파싱한다.
factory MenuSummaryDto.fromJson(Map<String, dynamic> json) {
return MenuSummaryDto(
id: json['id'] as int,
@@ -92,9 +98,11 @@ class MenuSummaryDto {
);
}
/// DTO를 [MenuSummary] 엔티티로 변환한다.
MenuSummary toEntity() => MenuSummary(id: id, menuName: menuName);
}
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/menu.dart';
import '../../domain/repositories/menu_repository.dart';
import '../dtos/menu_dto.dart';
/// 메뉴 마스터 API를 호출하는 원격 저장소.
class MenuRepositoryRemote implements MenuRepository {
MenuRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class MenuRepositoryRemote implements MenuRepository {
static const _basePath = '/menus';
/// 메뉴 목록을 조회한다.
@override
Future<PaginatedResult<MenuItem>> list({
int page = 1,
@@ -38,6 +40,7 @@ class MenuRepositoryRemote implements MenuRepository {
return MenuDto.parsePaginated(response.data ?? const {});
}
/// 새 메뉴를 생성한다.
@override
Future<MenuItem> create(MenuInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -49,6 +52,7 @@ class MenuRepositoryRemote implements MenuRepository {
return MenuDto.fromJson(data).toEntity();
}
/// 메뉴 정보를 수정한다.
@override
Future<MenuItem> update(int id, MenuInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -60,11 +64,13 @@ class MenuRepositoryRemote implements MenuRepository {
return MenuDto.fromJson(data).toEntity();
}
/// 메뉴를 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 메뉴를 복구한다.
@override
Future<MenuItem> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -4,6 +4,7 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../domain/entities/menu.dart';
import '../../domain/repositories/menu_repository.dart';
/// 메뉴 사용 여부 필터.
enum MenuStatusFilter { all, activeOnly, inactiveOnly }
/// 메뉴 마스터 상태 컨트롤러
@@ -38,6 +39,7 @@ class MenuController extends ChangeNotifier {
String? get errorMessage => _errorMessage;
List<MenuItem> get parents => _parents;
/// 상위 메뉴 목록을 로드해 드롭다운에 표시한다.
Future<void> loadParents() async {
_isLoadingParents = true;
notifyListeners();
@@ -56,6 +58,7 @@ class MenuController extends ChangeNotifier {
}
}
/// 메뉴 목록을 조회한다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -83,26 +86,31 @@ class MenuController extends ChangeNotifier {
}
}
/// 검색어를 변경한다.
void updateQuery(String value) {
_query = value;
notifyListeners();
}
/// 상위 메뉴 필터를 변경한다.
void updateParentFilter(int? parentId) {
_parentFilter = parentId;
notifyListeners();
}
/// 메뉴 사용 여부 필터를 변경한다.
void updateStatusFilter(MenuStatusFilter filter) {
_statusFilter = filter;
notifyListeners();
}
/// 삭제 포함 여부를 변경한다.
void updateIncludeDeleted(bool value) {
_includeDeleted = value;
notifyListeners();
}
/// 메뉴를 생성한다.
Future<MenuItem?> create(MenuInput input) async {
_setSubmitting(true);
try {
@@ -119,6 +127,7 @@ class MenuController extends ChangeNotifier {
}
}
/// 메뉴 정보를 수정한다.
Future<MenuItem?> update(int id, MenuInput input) async {
_setSubmitting(true);
try {
@@ -135,6 +144,7 @@ class MenuController extends ChangeNotifier {
}
}
/// 메뉴를 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -151,6 +161,7 @@ class MenuController extends ChangeNotifier {
}
}
/// 삭제된 메뉴를 복구한다.
Future<MenuItem?> restore(int id) async {
_setSubmitting(true);
try {
@@ -167,11 +178,13 @@ class MenuController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();
}
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
void _setSubmitting(bool value) {
_isSubmitting = value;
notifyListeners();

View File

@@ -13,6 +13,7 @@ import '../../domain/entities/menu.dart';
import '../../domain/repositories/menu_repository.dart';
import '../controllers/menu_controller.dart' as menu;
/// 메뉴 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
class MenuPage extends StatelessWidget {
const MenuPage({super.key});
@@ -89,6 +90,7 @@ class MenuPage extends StatelessWidget {
}
}
/// 메뉴 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _MenuEnabledPage extends StatefulWidget {
const _MenuEnabledPage();
@@ -96,6 +98,7 @@ class _MenuEnabledPage extends StatefulWidget {
State<_MenuEnabledPage> createState() => _MenuEnabledPageState();
}
/// 메뉴 목록과 필터를 관리하는 상태 클래스.
class _MenuEnabledPageState extends State<_MenuEnabledPage> {
late final menu.MenuController _controller;
final TextEditingController _searchController = TextEditingController();

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/product.dart';
/// 제품(Product) API 응답을 표현하는 DTO.
class ProductDto {
ProductDto({
this.id,
@@ -28,6 +29,7 @@ class ProductDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 제품 정보를 파싱한다.
factory ProductDto.fromJson(Map<String, dynamic> json) {
return ProductDto(
id: json['id'] as int?,
@@ -47,6 +49,7 @@ class ProductDto {
);
}
/// DTO를 JSON 맵으로 직렬화한다.
Map<String, dynamic> toJson() {
return {
if (id != null) 'id': id,
@@ -62,6 +65,7 @@ class ProductDto {
};
}
/// DTO를 도메인 [Product] 엔티티로 변환한다.
Product toEntity() => Product(
id: id,
productCode: productCode,
@@ -75,6 +79,7 @@ class ProductDto {
updatedAt: updatedAt,
);
/// 엔티티 값을 DTO로 역변환한다.
static ProductDto fromEntity(Product entity) => ProductDto(
id: entity.id,
productCode: entity.productCode,
@@ -96,6 +101,7 @@ class ProductDto {
updatedAt: entity.updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<Product> parsePaginated(Map<String, dynamic>? json) {
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
final items = rawItems
@@ -111,6 +117,7 @@ class ProductDto {
}
}
/// 제품에 연결된 공급업체 정보를 담는 DTO.
class ProductVendorDto {
ProductVendorDto({
required this.id,
@@ -122,6 +129,7 @@ class ProductVendorDto {
final String vendorCode;
final String vendorName;
/// JSON에서 공급업체 정보를 파싱한다.
factory ProductVendorDto.fromJson(Map<String, dynamic> json) {
return ProductVendorDto(
id: json['id'] as int,
@@ -130,20 +138,24 @@ class ProductVendorDto {
);
}
/// DTO를 JSON 맵으로 직렬화한다.
Map<String, dynamic> toJson() {
return {'id': id, 'vendor_code': vendorCode, 'vendor_name': vendorName};
}
/// DTO를 [ProductVendor] 엔티티로 변환한다.
ProductVendor toEntity() =>
ProductVendor(id: id, vendorCode: vendorCode, vendorName: vendorName);
}
/// 제품의 단위(UOM) 정보를 담는 DTO.
class ProductUomDto {
ProductUomDto({required this.id, required this.uomName});
final int id;
final String uomName;
/// JSON에서 단위 정보를 파싱한다.
factory ProductUomDto.fromJson(Map<String, dynamic> json) {
return ProductUomDto(
id: json['id'] as int,
@@ -151,13 +163,16 @@ class ProductUomDto {
);
}
/// DTO를 JSON 맵으로 직렬화한다.
Map<String, dynamic> toJson() {
return {'id': id, 'uom_name': uomName};
}
/// DTO를 [ProductUom] 엔티티로 변환한다.
ProductUom toEntity() => ProductUom(id: id, uomName: uomName);
}
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;
@@ -165,6 +180,7 @@ DateTime? _parseDate(Object? value) {
return null;
}
/// 제품 입력 모델을 API 요청 바디로 변환한다.
Map<String, dynamic> productInputToJson(ProductInput input) {
final map = input.toPayload();
map.removeWhere((key, value) => value == null);

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../dtos/product_dto.dart';
/// 제품 마스터 API를 호출하는 원격 저장소.
class ProductRepositoryRemote implements ProductRepository {
ProductRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class ProductRepositoryRemote implements ProductRepository {
static const _basePath = '/products';
/// 제품 목록을 조회한다.
@override
Future<PaginatedResult<Product>> list({
int page = 1,
@@ -38,6 +40,7 @@ class ProductRepositoryRemote implements ProductRepository {
return ProductDto.parsePaginated(response.data ?? const {});
}
/// 제품을 생성한다.
@override
Future<Product> create(ProductInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -49,6 +52,7 @@ class ProductRepositoryRemote implements ProductRepository {
return ProductDto.fromJson(data).toEntity();
}
/// 제품 정보를 수정한다.
@override
Future<Product> update(int id, ProductInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -60,11 +64,13 @@ class ProductRepositoryRemote implements ProductRepository {
return ProductDto.fromJson(data).toEntity();
}
/// 제품을 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 제품을 복구한다.
@override
Future<Product> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -1,3 +1,4 @@
/// 제품(Product) 도메인 엔티티.
class Product {
Product({
this.id,
@@ -23,6 +24,7 @@ class Product {
final DateTime? createdAt;
final DateTime? updatedAt;
/// 일부 속성을 변경한 새 인스턴스를 반환한다.
Product copyWith({
int? id,
String? productCode,
@@ -50,6 +52,7 @@ class Product {
}
}
/// 제품에 연결된 공급업체 정보.
class ProductVendor {
ProductVendor({
required this.id,
@@ -62,6 +65,7 @@ class ProductVendor {
final String vendorName;
}
/// 제품의 단위(UOM) 정보.
class ProductUom {
ProductUom({required this.id, required this.uomName});
@@ -69,6 +73,7 @@ class ProductUom {
final String uomName;
}
/// 제품 생성/수정에 사용하는 입력 모델.
class ProductInput {
ProductInput({
required this.productCode,
@@ -86,6 +91,7 @@ class ProductInput {
final bool isActive;
final String? note;
/// API 요청 바디로 직렬화한다.
Map<String, dynamic> toPayload() {
return {
'product_code': productCode,

View File

@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../entities/product.dart';
/// 제품 데이터를 다루는 도메인 저장소 인터페이스.
abstract class ProductRepository {
/// 제품 목록을 조회한다.
Future<PaginatedResult<Product>> list({
int page = 1,
int pageSize = 20,
@@ -12,11 +14,15 @@ abstract class ProductRepository {
bool? isActive,
});
/// 제품을 생성한다.
Future<Product> create(ProductInput input);
/// 제품을 수정한다.
Future<Product> update(int id, ProductInput input);
/// 제품을 삭제한다.
Future<void> delete(int id);
/// 삭제된 제품을 복구한다.
Future<Product> restore(int id);
}

View File

@@ -8,8 +8,10 @@ import '../../../uom/domain/repositories/uom_repository.dart';
import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
/// 제품 사용 여부 필터.
enum ProductStatusFilter { all, activeOnly, inactiveOnly }
/// 제품 마스터 화면 상태를 관리하는 컨트롤러.
class ProductController extends ChangeNotifier {
static const int defaultPageSize = 20;
@@ -52,6 +54,7 @@ class ProductController extends ChangeNotifier {
List<Vendor> get vendorOptions => _vendorOptions;
List<Uom> get uomOptions => _uomOptions;
/// 제품 목록을 조회한다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -82,6 +85,7 @@ class ProductController extends ChangeNotifier {
}
}
/// 필터/폼에서 사용할 공급업체와 단위 목록을 로드한다.
Future<void> loadLookups() async {
_isLoadingLookups = true;
notifyListeners();
@@ -98,6 +102,7 @@ class ProductController extends ChangeNotifier {
}
}
/// 검색어를 변경한다.
void updateQuery(String value) {
if (_query == value) {
return;
@@ -106,6 +111,7 @@ class ProductController extends ChangeNotifier {
notifyListeners();
}
/// 공급업체 필터를 변경한다.
void updateVendorFilter(int? vendorId) {
if (_vendorFilter == vendorId) {
return;
@@ -114,6 +120,7 @@ class ProductController extends ChangeNotifier {
notifyListeners();
}
/// 단위(UOM) 필터를 변경한다.
void updateUomFilter(int? uomId) {
if (_uomFilter == uomId) {
return;
@@ -122,6 +129,7 @@ class ProductController extends ChangeNotifier {
notifyListeners();
}
/// 사용 여부 필터를 변경한다.
void updateStatusFilter(ProductStatusFilter filter) {
if (_statusFilter == filter) {
return;
@@ -130,6 +138,7 @@ class ProductController extends ChangeNotifier {
notifyListeners();
}
/// 페이지 크기를 변경한다.
void updatePageSize(int size) {
if (size <= 0 || _pageSize == size) {
return;
@@ -138,6 +147,7 @@ class ProductController extends ChangeNotifier {
notifyListeners();
}
/// 제품을 생성한다.
Future<Product?> create(ProductInput input) async {
_setSubmitting(true);
try {
@@ -153,6 +163,7 @@ class ProductController extends ChangeNotifier {
}
}
/// 제품 정보를 수정한다.
Future<Product?> update(int id, ProductInput input) async {
_setSubmitting(true);
try {
@@ -168,6 +179,7 @@ class ProductController extends ChangeNotifier {
}
}
/// 제품을 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -183,6 +195,7 @@ class ProductController extends ChangeNotifier {
}
}
/// 삭제된 제품을 복구한다.
Future<Product?> restore(int id) async {
_setSubmitting(true);
try {
@@ -198,11 +211,13 @@ class ProductController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();
}
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
void _setSubmitting(bool value) {
_isSubmitting = value;
notifyListeners();

View File

@@ -19,6 +19,7 @@ import '../../domain/entities/product.dart';
import '../../domain/repositories/product_repository.dart';
import '../controllers/product_controller.dart';
/// 제품 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
class ProductPage extends StatelessWidget {
const ProductPage({super.key, required this.routeUri});
@@ -74,6 +75,7 @@ class ProductPage extends StatelessWidget {
}
}
/// 제품 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _ProductEnabledPage extends StatefulWidget {
const _ProductEnabledPage({required this.routeUri});
@@ -83,6 +85,7 @@ class _ProductEnabledPage extends StatefulWidget {
State<_ProductEnabledPage> createState() => _ProductEnabledPageState();
}
/// 제품 목록과 필터/폼 상태를 관리하는 상태 클래스.
class _ProductEnabledPageState extends State<_ProductEnabledPage> {
late final ProductController _controller;
final TextEditingController _searchController = TextEditingController();

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/uom.dart';
/// 단위(UOM) API 응답을 표현하는 DTO.
class UomDto {
UomDto({
this.id,
@@ -24,6 +25,7 @@ class UomDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 단위 정보를 파싱한다.
factory UomDto.fromJson(Map<String, dynamic> json) {
return UomDto(
id: json['id'] as int?,
@@ -37,6 +39,7 @@ class UomDto {
);
}
/// DTO를 도메인 [Uom] 엔티티로 변환한다.
Uom toEntity() => Uom(
id: id,
uomName: uomName,
@@ -48,6 +51,7 @@ class UomDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<Uom> parsePaginated(Map<String, dynamic>? json) {
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
final items = rawItems
@@ -63,6 +67,7 @@ class UomDto {
}
}
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/uom.dart';
import '../../domain/repositories/uom_repository.dart';
import '../dtos/uom_dto.dart';
/// 단위(UOM) 마스터 API를 호출하는 원격 저장소.
class UomRepositoryRemote implements UomRepository {
UomRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class UomRepositoryRemote implements UomRepository {
static const _basePath = '/uoms';
/// UOM 목록을 조회한다.
@override
Future<PaginatedResult<Uom>> list({
int page = 1,

View File

@@ -1,3 +1,4 @@
/// 단위(UOM) 도메인 엔티티.
class Uom {
Uom({
this.id,
@@ -19,6 +20,7 @@ class Uom {
final DateTime? createdAt;
final DateTime? updatedAt;
/// 선택한 속성만 변경한 새 인스턴스를 반환한다.
Uom copyWith({
int? id,
String? uomName,

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/user.dart';
/// 사용자(User) API 응답을 표현하는 DTO.
class UserDto {
UserDto({
this.id,
@@ -30,6 +31,7 @@ class UserDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 사용자 정보를 파싱한다.
factory UserDto.fromJson(Map<String, dynamic> json) {
return UserDto(
id: json['id'] as int?,
@@ -48,6 +50,7 @@ class UserDto {
);
}
/// DTO를 도메인 [UserAccount] 엔티티로 변환한다.
UserAccount toEntity() => UserAccount(
id: id,
employeeNo: employeeNo,
@@ -62,6 +65,7 @@ class UserDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<UserAccount> parsePaginated(
Map<String, dynamic>? json,
) {
@@ -79,12 +83,14 @@ class UserDto {
}
}
/// 사용자에 연결된 그룹 정보를 담는 DTO.
class UserGroupDto {
UserGroupDto({required this.id, required this.groupName});
final int id;
final String groupName;
/// JSON에서 그룹 정보를 파싱한다.
factory UserGroupDto.fromJson(Map<String, dynamic> json) {
return UserGroupDto(
id: json['id'] as int,
@@ -92,9 +98,11 @@ class UserGroupDto {
);
}
/// DTO를 [UserGroup] 엔티티로 변환한다.
UserGroup toEntity() => UserGroup(id: id, groupName: groupName);
}
/// 문자열/DateTime을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../dtos/user_dto.dart';
/// 사용자 마스터 API를 호출하는 원격 저장소.
class UserRepositoryRemote implements UserRepository {
UserRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class UserRepositoryRemote implements UserRepository {
static const _basePath = '/employees';
/// 사용자 목록을 조회한다.
@override
Future<PaginatedResult<UserAccount>> list({
int page = 1,
@@ -36,6 +38,7 @@ class UserRepositoryRemote implements UserRepository {
return UserDto.parsePaginated(response.data ?? const {});
}
/// 사용자를 생성한다.
@override
Future<UserAccount> create(UserInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -47,6 +50,7 @@ class UserRepositoryRemote implements UserRepository {
return UserDto.fromJson(data).toEntity();
}
/// 사용자 정보를 수정한다.
@override
Future<UserAccount> update(int id, UserInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -58,11 +62,13 @@ class UserRepositoryRemote implements UserRepository {
return UserDto.fromJson(data).toEntity();
}
/// 사용자를 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 사용자를 복구한다.
@override
Future<UserAccount> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -1,3 +1,4 @@
/// 사용자(User) 도메인 엔티티.
class UserAccount {
UserAccount({
this.id,
@@ -25,6 +26,7 @@ class UserAccount {
final DateTime? createdAt;
final DateTime? updatedAt;
/// 선택된 속성만 변경한 새 인스턴스를 반환한다.
UserAccount copyWith({
int? id,
String? employeeNo,
@@ -54,6 +56,7 @@ class UserAccount {
}
}
/// 사용자에 연결된 그룹 정보.
class UserGroup {
UserGroup({required this.id, required this.groupName});
@@ -61,6 +64,7 @@ class UserGroup {
final String groupName;
}
/// 사용자 생성/수정 입력 모델.
class UserInput {
UserInput({
required this.employeeNo,
@@ -80,6 +84,7 @@ class UserInput {
final bool isActive;
final String? note;
/// API 요청 바디로 직렬화한다.
Map<String, dynamic> toPayload() {
return {
'employee_no': employeeNo,

View File

@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../entities/user.dart';
/// 사용자 데이터를 다루는 도메인 저장소 인터페이스.
abstract class UserRepository {
/// 사용자 목록을 조회한다.
Future<PaginatedResult<UserAccount>> list({
int page = 1,
int pageSize = 20,
@@ -11,11 +13,15 @@ abstract class UserRepository {
bool? isActive,
});
/// 사용자를 생성한다.
Future<UserAccount> create(UserInput input);
/// 사용자 정보를 수정한다.
Future<UserAccount> update(int id, UserInput input);
/// 사용자를 삭제한다.
Future<void> delete(int id);
/// 삭제된 사용자를 복구한다.
Future<UserAccount> restore(int id);
}

View File

@@ -6,8 +6,10 @@ import '../../../group/domain/repositories/group_repository.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
/// 사용자 활성 여부 필터.
enum UserStatusFilter { all, activeOnly, inactiveOnly }
/// 사용자 마스터 화면 상태를 관리하는 컨트롤러.
class UserController extends ChangeNotifier {
UserController({
required UserRepository userRepository,
@@ -38,6 +40,7 @@ class UserController extends ChangeNotifier {
String? get errorMessage => _errorMessage;
List<Group> get groups => _groups;
/// 권한 그룹 목록을 로드한다.
Future<void> loadGroups() async {
_isLoadingGroups = true;
notifyListeners();
@@ -52,6 +55,7 @@ class UserController extends ChangeNotifier {
}
}
/// 사용자 목록을 조회한다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -78,21 +82,25 @@ class UserController extends ChangeNotifier {
}
}
/// 검색어를 변경한다.
void updateQuery(String value) {
_query = value;
notifyListeners();
}
/// 그룹 필터를 변경한다.
void updateGroupFilter(int? groupId) {
_groupFilter = groupId;
notifyListeners();
}
/// 사용자 상태 필터를 변경한다.
void updateStatusFilter(UserStatusFilter filter) {
_statusFilter = filter;
notifyListeners();
}
/// 사용자를 생성한다.
Future<UserAccount?> create(UserInput input) async {
_setSubmitting(true);
try {
@@ -108,6 +116,7 @@ class UserController extends ChangeNotifier {
}
}
/// 사용자 정보를 수정한다.
Future<UserAccount?> update(int id, UserInput input) async {
_setSubmitting(true);
try {
@@ -123,6 +132,7 @@ class UserController extends ChangeNotifier {
}
}
/// 사용자를 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -138,6 +148,7 @@ class UserController extends ChangeNotifier {
}
}
/// 삭제된 사용자를 복구한다.
Future<UserAccount?> restore(int id) async {
_setSubmitting(true);
try {
@@ -153,11 +164,13 @@ class UserController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();
}
/// 제출 상태 플래그를 갱신하고 리스너에게 알린다.
void _setSubmitting(bool value) {
_isSubmitting = value;
notifyListeners();

View File

@@ -15,6 +15,7 @@ import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repository.dart';
import '../controllers/user_controller.dart';
/// 사용자 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 보여준다.
class UserPage extends StatelessWidget {
const UserPage({super.key});
@@ -80,6 +81,7 @@ class UserPage extends StatelessWidget {
}
}
/// 사용자 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _UserEnabledPage extends StatefulWidget {
const _UserEnabledPage();
@@ -87,6 +89,7 @@ class _UserEnabledPage extends StatefulWidget {
State<_UserEnabledPage> createState() => _UserEnabledPageState();
}
/// 사용자 목록과 필터 상태를 관리하는 상태 클래스.
class _UserEnabledPageState extends State<_UserEnabledPage> {
late final UserController _controller;
final TextEditingController _searchController = TextEditingController();

View File

@@ -15,6 +15,7 @@ import '../../../vendor/domain/entities/vendor.dart';
import '../../../vendor/domain/repositories/vendor_repository.dart';
import '../controllers/vendor_controller.dart';
/// 벤더 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
class VendorPage extends StatelessWidget {
const VendorPage({super.key, required this.routeUri});
@@ -67,6 +68,7 @@ class VendorPage extends StatelessWidget {
}
}
/// 벤더 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _VendorEnabledPage extends StatefulWidget {
const _VendorEnabledPage({required this.routeUri});
@@ -76,6 +78,7 @@ class _VendorEnabledPage extends StatefulWidget {
State<_VendorEnabledPage> createState() => _VendorEnabledPageState();
}
/// 벤더 목록과 필터/폼 상태를 관리하는 상태 클래스.
class _VendorEnabledPageState extends State<_VendorEnabledPage> {
late final VendorController _controller;
final TextEditingController _searchController = TextEditingController();

View File

@@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart';
import '../../domain/entities/warehouse.dart';
/// 창고(Warehouse) API 응답을 표현하는 DTO.
class WarehouseDto {
WarehouseDto({
this.id,
@@ -28,6 +29,7 @@ class WarehouseDto {
final DateTime? createdAt;
final DateTime? updatedAt;
/// JSON에서 창고 정보를 파싱한다.
factory WarehouseDto.fromJson(Map<String, dynamic> json) {
return WarehouseDto(
id: json['id'] as int?,
@@ -47,6 +49,7 @@ class WarehouseDto {
);
}
/// DTO를 JSON 맵으로 직렬화한다.
Map<String, dynamic> toJson() {
return {
if (id != null) 'id': id,
@@ -62,6 +65,7 @@ class WarehouseDto {
};
}
/// DTO를 도메인 [Warehouse] 엔티티로 변환한다.
Warehouse toEntity() => Warehouse(
id: id,
warehouseCode: warehouseCode,
@@ -75,6 +79,7 @@ class WarehouseDto {
updatedAt: updatedAt,
);
/// 페이징 응답을 [PaginatedResult]로 변환한다.
static PaginatedResult<Warehouse> parsePaginated(Map<String, dynamic>? json) {
final rawItems = JsonUtils.extractList(json, keys: const ['items']);
final items = rawItems
@@ -90,6 +95,7 @@ class WarehouseDto {
}
}
/// 창고 주소에 대한 우편번호 정보를 담는 DTO.
class WarehouseZipcodeDto {
WarehouseZipcodeDto({
required this.zipcode,
@@ -103,6 +109,7 @@ class WarehouseZipcodeDto {
final String? sigungu;
final String? roadName;
/// JSON에서 우편번호 정보를 파싱한다.
factory WarehouseZipcodeDto.fromJson(Map<String, dynamic> json) {
return WarehouseZipcodeDto(
zipcode: json['zipcode'] as String,
@@ -112,6 +119,7 @@ class WarehouseZipcodeDto {
);
}
/// DTO를 JSON 맵으로 직렬화한다.
Map<String, dynamic> toJson() {
return {
'zipcode': zipcode,
@@ -121,6 +129,7 @@ class WarehouseZipcodeDto {
};
}
/// DTO를 [WarehouseZipcode] 엔티티로 변환한다.
WarehouseZipcode toEntity() => WarehouseZipcode(
zipcode: zipcode,
sido: sido,
@@ -129,6 +138,7 @@ class WarehouseZipcodeDto {
);
}
/// 문자열/DateTime 값을 파싱해 [DateTime]으로 변환한다.
DateTime? _parseDate(Object? value) {
if (value == null) return null;
if (value is DateTime) return value;
@@ -136,6 +146,7 @@ DateTime? _parseDate(Object? value) {
return null;
}
/// 창고 입력 모델을 API 요청 바디로 변환한다.
Map<String, dynamic> warehouseInputToJson(WarehouseInput input) {
final map = input.toPayload();
map.removeWhere((key, value) => value == null);

View File

@@ -6,6 +6,7 @@ import '../../domain/entities/warehouse.dart';
import '../../domain/repositories/warehouse_repository.dart';
import '../dtos/warehouse_dto.dart';
/// 창고(Warehouse) 마스터 API를 호출하는 원격 저장소.
class WarehouseRepositoryRemote implements WarehouseRepository {
WarehouseRepositoryRemote({required ApiClient apiClient}) : _api = apiClient;
@@ -13,6 +14,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
static const _basePath = '/warehouses';
/// 창고 목록을 조회한다.
@override
Future<PaginatedResult<Warehouse>> list({
int page = 1,
@@ -33,6 +35,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
return WarehouseDto.parsePaginated(response.data ?? const {});
}
/// 창고를 생성한다.
@override
Future<Warehouse> create(WarehouseInput input) async {
final response = await _api.post<Map<String, dynamic>>(
@@ -44,6 +47,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
return WarehouseDto.fromJson(data).toEntity();
}
/// 창고 정보를 수정한다.
@override
Future<Warehouse> update(int id, WarehouseInput input) async {
final response = await _api.patch<Map<String, dynamic>>(
@@ -55,11 +59,13 @@ class WarehouseRepositoryRemote implements WarehouseRepository {
return WarehouseDto.fromJson(data).toEntity();
}
/// 창고를 삭제한다.
@override
Future<void> delete(int id) async {
await _api.delete<void>('$_basePath/$id');
}
/// 삭제된 창고를 복구한다.
@override
Future<Warehouse> restore(int id) async {
final response = await _api.post<Map<String, dynamic>>(

View File

@@ -1,3 +1,4 @@
/// 창고(Warehouse) 도메인 엔티티.
class Warehouse {
Warehouse({
this.id,
@@ -23,6 +24,7 @@ class Warehouse {
final DateTime? createdAt;
final DateTime? updatedAt;
/// 일부 속성만 변경한 새 인스턴스를 반환한다.
Warehouse copyWith({
int? id,
String? warehouseCode,
@@ -50,6 +52,7 @@ class Warehouse {
}
}
/// 창고 주소에 대한 우편번호/행정 정보.
class WarehouseZipcode {
WarehouseZipcode({
required this.zipcode,
@@ -64,6 +67,7 @@ class WarehouseZipcode {
final String? roadName;
}
/// 창고 생성/수정에 사용하는 입력 모델.
class WarehouseInput {
WarehouseInput({
required this.warehouseCode,
@@ -81,6 +85,7 @@ class WarehouseInput {
final bool isActive;
final String? note;
/// API 요청 바디로 직렬화한다.
Map<String, dynamic> toPayload() {
return {
'warehouse_code': warehouseCode,

View File

@@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../entities/warehouse.dart';
/// 창고 데이터를 다루는 도메인 저장소 인터페이스.
abstract class WarehouseRepository {
/// 창고 목록을 조회한다.
Future<PaginatedResult<Warehouse>> list({
int page = 1,
int pageSize = 20,
@@ -10,11 +12,15 @@ abstract class WarehouseRepository {
bool? isActive,
});
/// 창고를 생성한다.
Future<Warehouse> create(WarehouseInput input);
/// 창고 정보를 수정한다.
Future<Warehouse> update(int id, WarehouseInput input);
/// 창고를 삭제한다.
Future<void> delete(int id);
/// 삭제된 창고를 복구한다.
Future<Warehouse> restore(int id);
}

View File

@@ -4,8 +4,10 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
import '../../domain/entities/warehouse.dart';
import '../../domain/repositories/warehouse_repository.dart';
/// 창고 사용 여부 필터.
enum WarehouseStatusFilter { all, activeOnly, inactiveOnly }
/// 창고 마스터 화면 상태를 관리하는 컨트롤러.
class WarehouseController extends ChangeNotifier {
static const int defaultPageSize = 20;
@@ -30,6 +32,7 @@ class WarehouseController extends ChangeNotifier {
int get pageSize => _pageSize;
String? get errorMessage => _errorMessage;
/// 창고 목록을 조회한다.
Future<void> fetch({int page = 1}) async {
_isLoading = true;
_errorMessage = null;
@@ -58,6 +61,7 @@ class WarehouseController extends ChangeNotifier {
}
}
/// 검색어를 변경한다.
void updateQuery(String value) {
if (_query == value) {
return;
@@ -66,6 +70,7 @@ class WarehouseController extends ChangeNotifier {
notifyListeners();
}
/// 사용 여부 필터를 변경한다.
void updateStatusFilter(WarehouseStatusFilter filter) {
if (_statusFilter == filter) {
return;
@@ -74,6 +79,7 @@ class WarehouseController extends ChangeNotifier {
notifyListeners();
}
/// 페이지 크기를 변경한다.
void updatePageSize(int size) {
if (size <= 0 || _pageSize == size) {
return;
@@ -82,6 +88,7 @@ class WarehouseController extends ChangeNotifier {
notifyListeners();
}
/// 창고를 생성한다.
Future<Warehouse?> create(WarehouseInput input) async {
_setSubmitting(true);
try {
@@ -97,6 +104,7 @@ class WarehouseController extends ChangeNotifier {
}
}
/// 창고 정보를 수정한다.
Future<Warehouse?> update(int id, WarehouseInput input) async {
_setSubmitting(true);
try {
@@ -112,6 +120,7 @@ class WarehouseController extends ChangeNotifier {
}
}
/// 창고를 삭제한다.
Future<bool> delete(int id) async {
_setSubmitting(true);
try {
@@ -127,6 +136,7 @@ class WarehouseController extends ChangeNotifier {
}
}
/// 삭제된 창고를 복구한다.
Future<Warehouse?> restore(int id) async {
_setSubmitting(true);
try {
@@ -142,11 +152,13 @@ class WarehouseController extends ChangeNotifier {
}
}
/// 에러 메시지를 초기화한다.
void clearError() {
_errorMessage = null;
notifyListeners();
}
/// 제출 상태 플래그를 갱신하고 리스너에 알린다.
void _setSubmitting(bool value) {
_isSubmitting = value;
notifyListeners();

View File

@@ -17,6 +17,7 @@ import '../../domain/entities/warehouse.dart';
import '../../domain/repositories/warehouse_repository.dart';
import '../controllers/warehouse_controller.dart';
/// 창고 관리 페이지. 기능 플래그에 따라 사양/실제 화면을 전환한다.
class WarehousePage extends StatelessWidget {
const WarehousePage({super.key, required this.routeUri});
@@ -81,6 +82,7 @@ class WarehousePage extends StatelessWidget {
}
}
/// 창고 기능이 활성화된 경우 사용하는 실제 화면 위젯.
class _WarehouseEnabledPage extends StatefulWidget {
const _WarehouseEnabledPage({required this.routeUri});
@@ -90,6 +92,7 @@ class _WarehouseEnabledPage extends StatefulWidget {
State<_WarehouseEnabledPage> createState() => _WarehouseEnabledPageState();
}
/// 창고 목록과 필터 상태를 관리하는 상태 클래스.
class _WarehouseEnabledPageState extends State<_WarehouseEnabledPage> {
late final WarehouseController _controller;
final TextEditingController _searchController = TextEditingController();