From 47c87dc11850f7dd3c6bce2a661d845d84bade5b Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 29 Sep 2025 19:39:35 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A3=BC=EC=84=9D=ED=99=94=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=EC=83=81=ED=99=A9=20=EC=A0=95=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=ED=95=B5=EC=8B=AC=20=EB=AA=A8=EB=93=88=EC=97=90=20?= =?UTF-8?q?=ED=95=9C=EA=B8=80=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/commenting_plan.md | 10 +++++++ doc/core_commenting_plan.md | 22 ++++++++++++++ lib/core/config/environment.dart | 5 ++++ .../interceptors/auth_interceptor.dart | 19 ++++++++++-- lib/core/permissions/permission_manager.dart | 3 ++ lib/core/routing/app_router.dart | 4 +++ lib/core/services/token_storage.dart | 5 ++++ lib/core/services/token_storage_native.dart | 4 +++ lib/core/services/token_storage_stub.dart | 2 ++ lib/core/services/token_storage_web.dart | 4 +++ lib/core/theme/superport_shad_theme.dart | 4 +++ .../approvals/data/dtos/approval_dto.dart | 20 +++++++++++++ .../data/dtos/approval_template_dto.dart | 11 +++++++ .../approval_repository_remote.dart | 14 +++++++++ .../approval_template_repository_remote.dart | 12 ++++++++ .../repositories/approval_repository.dart | 9 ++++++ .../approval_template_repository.dart | 7 +++++ .../dtos/approval_history_record_dto.dart | 5 ++++ .../approval_history_repository_remote.dart | 2 ++ .../approval_history_repository.dart | 2 ++ .../pages/approval_request_page.dart | 1 + .../data/dtos/approval_step_record_dto.dart | 4 +++ .../approval_step_repository_remote.dart | 5 ++++ .../approval_step_repository.dart | 3 ++ .../pages/approval_step_page.dart | 3 ++ .../pages/approval_template_page.dart | 3 ++ .../presentation/pages/dashboard_page.dart | 1 + .../login/presentation/pages/login_page.dart | 2 ++ .../customer/data/dtos/customer_dto.dart | 11 +++++++ .../customer_repository_remote.dart | 6 ++++ .../customer/domain/entities/customer.dart | 5 ++++ .../repositories/customer_repository.dart | 6 ++++ .../controllers/customer_controller.dart | 13 +++++++++ .../presentation/pages/customer_page.dart | 7 +++++ .../masters/group/data/dtos/group_dto.dart | 5 ++++ .../repositories/group_repository_remote.dart | 6 ++++ .../controllers/group_controller.dart | 12 ++++++++ .../group/presentation/pages/group_page.dart | 3 ++ .../data/dtos/group_permission_dto.dart | 11 +++++++ .../group_permission_repository_remote.dart | 6 ++++ .../group_permission_repository.dart | 6 ++++ .../group_permission_controller.dart | 14 +++++++++ .../pages/group_permission_page.dart | 3 ++ .../masters/menu/data/dtos/menu_dto.dart | 8 +++++ .../repositories/menu_repository_remote.dart | 6 ++++ .../controllers/menu_controller.dart | 13 +++++++++ .../menu/presentation/pages/menu_page.dart | 3 ++ .../product/data/dtos/product_dto.dart | 16 ++++++++++ .../product_repository_remote.dart | 6 ++++ .../product/domain/entities/product.dart | 6 ++++ .../repositories/product_repository.dart | 6 ++++ .../controllers/product_controller.dart | 15 ++++++++++ .../presentation/pages/product_page.dart | 3 ++ .../masters/uom/data/dtos/uom_dto.dart | 5 ++++ .../repositories/uom_repository_remote.dart | 2 ++ .../masters/uom/domain/entities/uom.dart | 2 ++ .../masters/user/data/dtos/user_dto.dart | 8 +++++ .../repositories/user_repository_remote.dart | 6 ++++ .../masters/user/domain/entities/user.dart | 5 ++++ .../domain/repositories/user_repository.dart | 6 ++++ .../controllers/user_controller.dart | 13 +++++++++ .../user/presentation/pages/user_page.dart | 3 ++ .../presentation/pages/vendor_page.dart | 3 ++ .../warehouse/data/dtos/warehouse_dto.dart | 11 +++++++ .../warehouse_repository_remote.dart | 6 ++++ .../warehouse/domain/entities/warehouse.dart | 5 ++++ .../repositories/warehouse_repository.dart | 6 ++++ .../controllers/warehouse_controller.dart | 12 ++++++++ .../presentation/pages/warehouse_page.dart | 3 ++ .../presentation/pages/reporting_page.dart | 4 +++ .../pages/postal_search_page.dart | 2 ++ lib/main.dart | 3 ++ lib/widgets/app_shell.dart | 20 +++++++++++-- lib/widgets/components/feedback.dart | 9 ++++++ lib/widgets/components/filter_bar.dart | 5 ++++ lib/widgets/components/form_field.dart | 20 +++++++++++++ .../components/keyboard_shortcuts.dart | 1 + lib/widgets/components/responsive.dart | 29 +++++++++++++++++++ lib/widgets/components/superport_dialog.dart | 3 +- lib/widgets/components/superport_table.dart | 13 +++++++++ lib/widgets/spec_page.dart | 4 +++ .../controllers/approval_controller_test.dart | 10 +++++++ 82 files changed, 596 insertions(+), 5 deletions(-) diff --git a/doc/commenting_plan.md b/doc/commenting_plan.md index 354f3ce..0248f8f 100644 --- a/doc/commenting_plan.md +++ b/doc/commenting_plan.md @@ -49,3 +49,13 @@ 세부 계획은 doc/test_commenting_plan.md 참고 ## Core/Widgets 주석화 세부 계획은 doc/core_commenting_plan.md 참고 + +### 진행 현황 메모 +- [x] `responsive.dart` 단말 분기/가시성 헬퍼 주석 추가 +- [x] `feedback.dart` 토스트·스켈레톤 메서드 용도 설명 보강 +- [x] `form_field.dart` 필수/에러/캡션 속성 주석화 +- [x] `lib/features/approvals` 컨트롤러/페이지/공유 위젯 주석 보강 +- [x] `lib/features/approvals/data/**` 원격 저장소·DTO 변환 로직 주석화 +- [x] `lib/features/inventory` 공유 카탈로그/자동완성 위젯 주석 보강 +- [x] `lib/features/reporting` 보고서 페이지 필터/상태 주석 보강 +- [x] `lib/features/masters/**` DTO/Repository/Controller/Page 주석 보강 diff --git a/doc/core_commenting_plan.md b/doc/core_commenting_plan.md index c4c3487..d0d67ad 100644 --- a/doc/core_commenting_plan.md +++ b/doc/core_commenting_plan.md @@ -5,12 +5,34 @@ |----------|------|----------------| | 1 | `lib/core/network/api_client.dart` | API 호출, 에러 매핑, 인터셉터 연결 | | 1 | `lib/core/network/api_error.dart` | 에러 모델/매퍼 설명 | +| 1 | `lib/core/network/interceptors/auth_interceptor.dart` | 토큰 갱신 흐름, 재시도 로직 | | 1 | `lib/core/permissions/permission_manager.dart` | 권한 검사/Scope 동작 | | 2 | `lib/core/config/environment.dart` | env 초기화 흐름, 추가 설명 필요시 | | 2 | `lib/core/theme/superport_shad_theme.dart` | 테마 팩토리 설명 | | 2 | `lib/core/routing/app_router.dart` | 라우터 설정 요약 | | 3 | `lib/widgets/components/**` (이미 일부 주석 있음) | 추가 주석 대상 확인 | +## 진행 현황 +- [x] `lib/core/network/interceptors/auth_interceptor.dart` 토큰 갱신/재시도 흐름 문서화 +- [x] `lib/core/services/token_storage.dart` 토큰 저장/초기화 Doc 주석 보강 +- [x] `lib/core/permissions/permission_manager.dart` 오버라이드 처리/Scope 조회 주석 추가 +- [x] `lib/core/config/environment.dart` 권한 맵 로딩/조회 인라인 설명 보강 +- [x] `lib/core/theme/superport_shad_theme.dart` 라이트/다크 컬러 스킴 및 상태 배지 설명 추가 +- [x] `lib/core/routing/app_router.dart` ShellRoute 구조와 라우트 명세 주석화 +- [x] `lib/widgets/components/superport_table.dart` 정렬/페이지네이션 모델 설명 및 fromCells 생성자 주석 추가 +- [x] `lib/widgets/components/superport_dialog.dart` show 헬퍼 주석, 편의 함수 한글 설명 갱신 +- [x] `lib/widgets/components/filter_bar.dart` 버튼 활성 상태 게터 주석 보강 +- [x] `lib/widgets/components/keyboard_shortcuts.dart` 다이얼로그 키보드 단축키 역할 설명 추가 +- [x] `lib/widgets/components/responsive.dart` 분기 헬퍼/레이아웃 위젯 주석 추가 +- [x] `lib/widgets/components/feedback.dart` 토스트·스켈레톤 메서드 용도 설명 +- [x] `lib/widgets/components/form_field.dart` 필드 속성/멀티라인 설정 주석화 + +## 남은 작업 (Tasks) +- [x] `lib/widgets/components/responsive.dart` 전역 함수/클래스에 환경별 역할 Doc 주석 추가 +- [x] `lib/widgets/components/feedback.dart` 토스트/스켈레톤 정적 메서드 용도 요약 (필요 시 예시 포함) +- [x] `lib/widgets/components/form_field.dart` 필수/유효성 문구 속성 설명 재검토(다단 필드 지원 여부 언급) +- [x] 주석화 완료 파일 재검토 후 동일 패턴으로 `doc/commenting_plan.md`에 반영 여부 확인 + ## 진행 순서 1. `core/network` → `core/permissions` 2. `core/theme`, `core/routing`, `core/config` diff --git a/lib/core/config/environment.dart b/lib/core/config/environment.dart index 0d4e073..0017fae 100644 --- a/lib/core/config/environment.dart +++ b/lib/core/config/environment.dart @@ -18,6 +18,7 @@ class Environment { /// 프로덕션 여부 static late final bool isProduction; + /// 환경 변수에서 파싱한 리소스별 권한 집합. static final Map> _permissions = {}; /// 환경 초기화 @@ -74,6 +75,7 @@ class Environment { } } + /// `.env` 파일에서 `PERMISSION__*` 키를 파싱해 권한 맵을 구성한다. static void _loadPermissions() { _permissions.clear(); for (final entry in dotenv.env.entries) { @@ -82,6 +84,7 @@ class Environment { continue; } final resource = entry.key.substring(prefix.length).toLowerCase(); + // 콤마 구분 문자열을 소문자/trim 처리해 비교를 일관되게 맞춘다. final values = entry.value .split(',') .map((token) => token.trim().toLowerCase()) @@ -91,12 +94,14 @@ class Environment { } } + /// 환경에 설정된 권한이 있는 경우 해당 액션 허용 여부를 반환한다. static bool hasPermission(String resource, String action) { final actions = _permissions[resource.toLowerCase()]; if (actions == null || actions.isEmpty) { return true; } if (actions.contains('all')) { + // all 키워드는 모든 액션 허용을 의미한다. return true; } return actions.contains(action.toLowerCase()); diff --git a/lib/core/network/interceptors/auth_interceptor.dart b/lib/core/network/interceptors/auth_interceptor.dart index f8abdcb..fc4ea2e 100644 --- a/lib/core/network/interceptors/auth_interceptor.dart +++ b/lib/core/network/interceptors/auth_interceptor.dart @@ -1,17 +1,21 @@ -// ignore_for_file: public_member_api_docs - import 'dart:async'; import 'package:dio/dio.dart'; import '../../services/token_storage.dart'; +/// 갱신 토큰을 기반으로 새로운 토큰 쌍을 받아오는 비동기 콜백 시그니처. typedef RefreshTokenCallback = Future Function(); +/// 액세스/리프레시 토큰 값을 함께 보관하기 위한 불변 모델. class TokenPair { + /// [accessToken], [refreshToken]을 모두 전달받아 초기화한다. const TokenPair({required this.accessToken, required this.refreshToken}); + /// 인증 헤더에 사용되는 액세스 토큰 문자열. final String accessToken; + + /// 토큰 재발급 요청에 전달되는 리프레시 토큰 문자열. final String refreshToken; } @@ -28,11 +32,17 @@ class AuthInterceptor extends Interceptor { final TokenStorage _tokenStorage; final Dio _dio; + + /// 서버에 토큰 재발급을 요청하는 콜백. null이면 갱신을 시도하지 않는다. final RefreshTokenCallback? onRefresh; + /// 동시에 들어온 요청을 순차적으로 처리하기 위한 대기열. final List> _refreshQueue = []; + + /// 현재 토큰 갱신 중인지 여부. bool _isRefreshing = false; + /// 요청 직전에 저장된 액세스 토큰을 Authorization 헤더에 주입한다. @override Future onRequest( RequestOptions options, @@ -45,6 +55,7 @@ class AuthInterceptor extends Interceptor { handler.next(options); } + /// 401 응답을 감지하면 토큰을 갱신하고 동일 요청을 한 번 재시도한다. @override Future onError( DioException err, @@ -69,12 +80,14 @@ class AuthInterceptor extends Interceptor { } } + /// 토큰 갱신을 시도해야 하는 상황인지 판별한다. bool _shouldAttemptRefresh(DioException err) { return onRefresh != null && err.response?.statusCode == 401 && err.requestOptions.extra['__retry'] != true; } + /// 토큰을 갱신하고 대기 중인 요청을 깨운다. Future _refreshToken() async { if (_isRefreshing) { final completer = Completer(); @@ -107,6 +120,7 @@ class AuthInterceptor extends Interceptor { } } + /// 최신 토큰을 헤더에 주입해 한 번만 동일 요청을 재시도한다. Future> _retry(RequestOptions requestOptions) async { final token = await _tokenStorage.readAccessToken(); if (token != null && token.isNotEmpty) { @@ -119,6 +133,7 @@ class AuthInterceptor extends Interceptor { } } +/// 토큰 재발급 실패 시 재시도 루프를 중단하기 위해 사용하는 내부 예외. class _RefreshFailedException implements Exception { const _RefreshFailedException(); } diff --git a/lib/core/permissions/permission_manager.dart b/lib/core/permissions/permission_manager.dart index 3ec0dba..cb861b9 100644 --- a/lib/core/permissions/permission_manager.dart +++ b/lib/core/permissions/permission_manager.dart @@ -13,12 +13,14 @@ class PermissionManager extends ChangeNotifier { } } + /// 리소스별 임시 권한 집합을 보관한다. final Map> _overrides = {}; /// 지정한 리소스/행동이 허용되는지 여부를 반환한다. bool can(String resource, PermissionAction action) { final override = _overrides[resource]; if (override != null) { + // View 권한은 최소 접근을 허용하기 위해 별도로 처리한다. if (override.contains(PermissionAction.view) && action == PermissionAction.view) { return true; @@ -45,6 +47,7 @@ class PermissionScope extends InheritedNotifier { required super.child, }) : super(notifier: manager); + /// 현재 빌드 컨텍스트에서 [PermissionManager]를 조회한다. static PermissionManager of(BuildContext context) { final scope = context.dependOnInheritedWidgetOfExactType(); assert( diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index 3c074c4..ac3af58 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -27,6 +27,10 @@ import '../constants/app_sections.dart'; final _rootNavigatorKey = GlobalKey(debugLabel: 'root'); /// 애플리케이션 전체 라우팅 구성을 담당하는 GoRouter 인스턴스. +/// +/// - 로그인 전용 라우트는 루트(primary) 네비게이터에서 처리한다. +/// - 로그인 이후 화면은 [ShellRoute] 하위에서 `AppShell` 레이아웃을 공유한다. +/// - 각 기능 모듈은 고유 name/path를 가지며 `GoRouter` 딥링크와 연결된다. final appRouter = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: loginRoutePath, diff --git a/lib/core/services/token_storage.dart b/lib/core/services/token_storage.dart index 076f959..76000aa 100644 --- a/lib/core/services/token_storage.dart +++ b/lib/core/services/token_storage.dart @@ -4,14 +4,19 @@ import 'token_storage_stub.dart' /// 액세스/리프레시 토큰을 안전하게 보관하는 스토리지 인터페이스. abstract class TokenStorage { + /// 액세스 토큰을 저장한다. null을 전달하면 기존 값을 제거한다. Future writeAccessToken(String? token); + /// 저장된 액세스 토큰을 읽어온다. 없으면 null을 반환한다. Future readAccessToken(); + /// 리프레시 토큰을 저장한다. null이면 값을 삭제한다. Future writeRefreshToken(String? token); + /// 저장된 리프레시 토큰을 읽어온다. 없으면 null을 반환한다. Future readRefreshToken(); + /// 저장 중인 모든 토큰 정보를 초기화한다. Future clear(); } diff --git a/lib/core/services/token_storage_native.dart b/lib/core/services/token_storage_native.dart index 50da20e..2899ffe 100644 --- a/lib/core/services/token_storage_native.dart +++ b/lib/core/services/token_storage_native.dart @@ -2,9 +2,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'token_storage.dart'; +/// 안전한 스토리지에 저장할 액세스 토큰 키. const _kAccessTokenKey = 'access_token'; +/// 안전한 스토리지에 저장할 리프레시 토큰 키. const _kRefreshTokenKey = 'refresh_token'; +/// 모바일/데스크톱에서 [FlutterSecureStorage]를 사용하는 토큰 스토리지를 생성한다. TokenStorage buildTokenStorage() { const storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), @@ -16,6 +19,7 @@ TokenStorage buildTokenStorage() { return _SecureTokenStorage(storage); } +/// [FlutterSecureStorage] 기반 토큰 스토리지 구현체. class _SecureTokenStorage implements TokenStorage { const _SecureTokenStorage(this._storage); diff --git a/lib/core/services/token_storage_stub.dart b/lib/core/services/token_storage_stub.dart index 8a942d7..f456a93 100644 --- a/lib/core/services/token_storage_stub.dart +++ b/lib/core/services/token_storage_stub.dart @@ -1,7 +1,9 @@ import 'token_storage.dart'; +/// 현재 플랫폼에서 지원되는 토큰 스토리지가 없을 때 사용하는 기본 구현을 생성한다. TokenStorage buildTokenStorage() => _UnsupportedTokenStorage(); +/// 토큰 저장 기능이 제공되지 않는 환경에서 호출될 때 예외를 던지는 스텁. class _UnsupportedTokenStorage implements TokenStorage { Never _unsupported() { throw UnsupportedError('TokenStorage is not supported on this platform.'); diff --git a/lib/core/services/token_storage_web.dart b/lib/core/services/token_storage_web.dart index a4aea39..fd599dc 100644 --- a/lib/core/services/token_storage_web.dart +++ b/lib/core/services/token_storage_web.dart @@ -3,11 +3,15 @@ import 'dart:html' as html; import 'token_storage.dart'; +/// 웹 로컬스토리지에 저장할 액세스 토큰 키. const _kAccessTokenKey = 'access_token'; +/// 웹 로컬스토리지에 저장할 리프레시 토큰 키. const _kRefreshTokenKey = 'refresh_token'; +/// 웹 환경에서 로컬스토리지를 활용하는 토큰 스토리지를 생성한다. TokenStorage buildTokenStorage() => _WebTokenStorage(html.window.localStorage); +/// 브라우저 로컬스토리지에 토큰을 저장하는 구현체. class _WebTokenStorage implements TokenStorage { const _WebTokenStorage(this._storage); diff --git a/lib/core/theme/superport_shad_theme.dart b/lib/core/theme/superport_shad_theme.dart index 5cca74d..cfc00f0 100644 --- a/lib/core/theme/superport_shad_theme.dart +++ b/lib/core/theme/superport_shad_theme.dart @@ -5,6 +5,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; class SuperportShadTheme { const SuperportShadTheme._(); + /// 브랜드 색상을 기준으로 정의한 주 색상 값. static const Color primaryColor = Color(0xFF1B4F87); static const Color successColor = Color(0xFF2E8B57); static const Color warningColor = Color(0xFFFFC107); @@ -16,6 +17,7 @@ class SuperportShadTheme { return ShadThemeData( brightness: Brightness.light, colorScheme: ShadColorScheme( + // 라이트 모드 기본 배경/텍스트 대비값 설정. background: Color(0xFFFFFFFF), foreground: Color(0xFF09090B), card: Color(0xFFFFFFFF), @@ -94,6 +96,7 @@ class SuperportShadTheme { return ShadThemeData( brightness: Brightness.dark, colorScheme: ShadColorScheme( + // 다크 모드 대비를 위해 어두운 배경/밝은 텍스트 조합을 사용한다. background: Color(0xFF09090B), foreground: Color(0xFFFAFAFA), card: Color(0xFF09090B), @@ -168,6 +171,7 @@ class SuperportShadTheme { } /// 상태 텍스트 배경을 위한 데코레이션을 반환한다. + /// 상태 문자열을 기반으로 배경/테두리 색을 선택해 상태 배지를 표현한다. static BoxDecoration statusDecoration(String status) { Color backgroundColor; Color borderColor; diff --git a/lib/features/approvals/data/dtos/approval_dto.dart b/lib/features/approvals/data/dtos/approval_dto.dart index fc5c9b4..2cd28a2 100644 --- a/lib/features/approvals/data/dtos/approval_dto.dart +++ b/lib/features/approvals/data/dtos/approval_dto.dart @@ -3,6 +3,10 @@ import 'package:superport_v2/core/common/utils/json_utils.dart'; import '../../domain/entities/approval.dart'; +/// 결재 API 응답을 표현하는 DTO. +/// +/// - 원본 JSON 형식을 유지하면서 도메인 엔티티 변환을 제공한다. +/// - 일부 필드는 누락 가능성을 고려하여 기본값을 지정한다. class ApprovalDto { ApprovalDto({ this.id, @@ -38,6 +42,7 @@ class ApprovalDto { final DateTime? createdAt; final DateTime? updatedAt; + /// API 응답 JSON을 [ApprovalDto]로 변환한다. factory ApprovalDto.fromJson(Map json) { return ApprovalDto( id: json['id'] as int?, @@ -74,6 +79,7 @@ class ApprovalDto { ); } + /// DTO를 도메인 [Approval] 엔티티로 변환한다. Approval toEntity() => Approval( id: id, approvalNo: approvalNo, @@ -92,6 +98,7 @@ class ApprovalDto { updatedAt: updatedAt, ); + /// 페이징 응답을 파싱해 [PaginatedResult]로 변환한다. static PaginatedResult parsePaginated(Map? json) { final rawItems = JsonUtils.extractList(json, keys: const ['items']); final items = rawItems @@ -107,6 +114,7 @@ class ApprovalDto { } } +/// 결재 상태(Status) DTO. class ApprovalStatusDto { ApprovalStatusDto({required this.id, required this.name, this.color}); @@ -122,9 +130,11 @@ class ApprovalStatusDto { ); } + /// DTO를 [ApprovalStatus]로 변환한다. ApprovalStatus toEntity() => ApprovalStatus(id: id, name: name, color: color); } +/// 결재 요청자 DTO. class ApprovalRequesterDto { ApprovalRequesterDto({ required this.id, @@ -144,10 +154,12 @@ class ApprovalRequesterDto { ); } + /// DTO를 [ApprovalRequester]로 변환한다. ApprovalRequester toEntity() => ApprovalRequester(id: id, employeeNo: employeeNo, name: name); } +/// 결재 승인자 DTO. class ApprovalApproverDto { ApprovalApproverDto({ required this.id, @@ -167,10 +179,12 @@ class ApprovalApproverDto { ); } + /// DTO를 [ApprovalApprover]로 변환한다. ApprovalApprover toEntity() => ApprovalApprover(id: id, employeeNo: employeeNo, name: name); } +/// 결재 단계 DTO. class ApprovalStepDto { ApprovalStepDto({ this.id, @@ -206,6 +220,7 @@ class ApprovalStepDto { ); } + /// DTO를 [ApprovalStep]으로 변환한다. ApprovalStep toEntity() => ApprovalStep( id: id, stepOrder: stepOrder, @@ -217,6 +232,7 @@ class ApprovalStepDto { ); } +/// 결재 이력 DTO. class ApprovalHistoryDto { ApprovalHistoryDto({ this.id, @@ -258,6 +274,7 @@ class ApprovalHistoryDto { ); } + /// DTO를 [ApprovalHistory]로 변환한다. ApprovalHistory toEntity() => ApprovalHistory( id: id, action: action.toEntity(), @@ -269,6 +286,7 @@ class ApprovalHistoryDto { ); } +/// 결재 행위(Action) DTO. class ApprovalActionDto { ApprovalActionDto({required this.id, required this.name}); @@ -282,9 +300,11 @@ class ApprovalActionDto { ); } + /// DTO를 [ApprovalAction]으로 변환한다. ApprovalAction toEntity() => ApprovalAction(id: id, name: name); } +/// 문자열/DateTime 입력을 DateTime으로 변환한다. DateTime? _parseDate(Object? value) { if (value == null) return null; if (value is DateTime) return value; diff --git a/lib/features/approvals/data/dtos/approval_template_dto.dart b/lib/features/approvals/data/dtos/approval_template_dto.dart index 17981b7..11962b1 100644 --- a/lib/features/approvals/data/dtos/approval_template_dto.dart +++ b/lib/features/approvals/data/dtos/approval_template_dto.dart @@ -3,6 +3,7 @@ import 'package:superport_v2/core/common/utils/json_utils.dart'; import '../../domain/entities/approval_template.dart'; +/// 결재 템플릿 API 응답을 표현하는 DTO. class ApprovalTemplateDto { ApprovalTemplateDto({ required this.id, @@ -28,6 +29,7 @@ class ApprovalTemplateDto { final DateTime? updatedAt; final List steps; + /// JSON을 [ApprovalTemplateDto]로 파싱한다. factory ApprovalTemplateDto.fromJson(Map json) { return ApprovalTemplateDto( id: json['id'] as int? ?? 0, @@ -50,6 +52,7 @@ class ApprovalTemplateDto { ); } + /// DTO를 [ApprovalTemplate]으로 변환한다. ApprovalTemplate toEntity({bool includeSteps = true}) { return ApprovalTemplate( id: id, @@ -65,6 +68,7 @@ class ApprovalTemplateDto { ); } + /// 페이징 응답을 파싱해 [PaginatedResult]를 반환한다. static PaginatedResult parsePaginated( Map? json, { bool includeSteps = false, @@ -83,6 +87,7 @@ class ApprovalTemplateDto { } } +/// 템플릿 작성자 DTO. class ApprovalTemplateAuthorDto { ApprovalTemplateAuthorDto({ required this.id, @@ -102,11 +107,13 @@ class ApprovalTemplateAuthorDto { ); } + /// DTO를 [ApprovalTemplateAuthor]로 변환한다. ApprovalTemplateAuthor toEntity() { return ApprovalTemplateAuthor(id: id, employeeNo: employeeNo, name: name); } } +/// 템플릿 단계 DTO. class ApprovalTemplateStepDto { ApprovalTemplateStepDto({ this.id, @@ -131,6 +138,7 @@ class ApprovalTemplateStepDto { ); } + /// DTO를 [ApprovalTemplateStep]으로 변환한다. ApprovalTemplateStep toEntity() { return ApprovalTemplateStep( id: id, @@ -141,6 +149,7 @@ class ApprovalTemplateStepDto { } } +/// 템플릿 승인자 DTO. class ApprovalTemplateApproverDto { ApprovalTemplateApproverDto({ required this.id, @@ -160,11 +169,13 @@ class ApprovalTemplateApproverDto { ); } + /// DTO를 [ApprovalTemplateApprover]로 변환한다. ApprovalTemplateApprover toEntity() { return ApprovalTemplateApprover(id: id, employeeNo: employeeNo, name: name); } } +/// 문자열/DateTime을 파싱해 [DateTime]으로 반환한다. DateTime? _parseDate(Object? value) { if (value == null) return null; if (value is DateTime) return value; diff --git a/lib/features/approvals/data/repositories/approval_repository_remote.dart b/lib/features/approvals/data/repositories/approval_repository_remote.dart index 94f69ad..8148fe2 100644 --- a/lib/features/approvals/data/repositories/approval_repository_remote.dart +++ b/lib/features/approvals/data/repositories/approval_repository_remote.dart @@ -6,6 +6,10 @@ import '../../domain/entities/approval.dart'; import '../../domain/repositories/approval_repository.dart'; import '../dtos/approval_dto.dart'; +/// 결재 API 엔드포인트를 호출하는 원격 저장소 구현체. +/// +/// - 모든 요청은 [ApiClient]를 통해 인증/에러 매핑을 공유한다. +/// - 엔티티 변환은 DTO 계층에 위임한다. class ApprovalRepositoryRemote implements ApprovalRepository { ApprovalRepositoryRemote({required ApiClient apiClient}) : _api = apiClient; @@ -13,6 +17,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { static const _basePath = '/approvals'; + /// 결재 목록을 조회한다. 필터 조건이 없으면 최신순 페이지를 반환한다. @override Future> list({ int page = 1, @@ -41,6 +46,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.parsePaginated(response.data ?? const {}); } + /// 결재 상세를 조회한다. 단계/이력 포함 여부를 쿼리 파라미터로 제어한다. @override Future fetchDetail( int id, { @@ -59,6 +65,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.fromJson(data).toEntity(); } + /// 활성화된 결재 행위 목록을 조회한다. @override Future> listActions({bool activeOnly = true}) async { final response = await _api.get>( @@ -74,6 +81,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return items; } + /// 결재 단계 행위를 수행하고 업데이트된 결재 정보를 반환한다. @override Future performStepAction(ApprovalStepActionInput input) async { final response = await _api.post>( @@ -90,6 +98,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.fromJson(approvalJson).toEntity(); } + /// 결재 단계들을 일괄로 생성하거나 재배치한다. @override Future assignSteps(ApprovalStepAssignmentInput input) async { final response = await _api.post>( @@ -106,6 +115,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.fromJson(approvalJson).toEntity(); } + /// 새로운 결재를 생성한다. @override Future create(ApprovalInput input) async { final response = await _api.post>( @@ -117,6 +127,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.fromJson(data).toEntity(); } + /// 결재 기본 정보를 수정한다. @override Future update(int id, ApprovalInput input) async { final response = await _api.patch>( @@ -128,11 +139,13 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.fromJson(data).toEntity(); } + /// 결재를 삭제(비활성화)한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 결재를 복구한다. @override Future restore(int id) async { final response = await _api.post>( @@ -143,6 +156,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository { return ApprovalDto.fromJson(data).toEntity(); } + /// 결재 단계/행위 응답에서 결재 객체 JSON을 추출한다. Map? _extractApprovalFromActionResponse( Map body, ) { diff --git a/lib/features/approvals/data/repositories/approval_template_repository_remote.dart b/lib/features/approvals/data/repositories/approval_template_repository_remote.dart index abf126e..9dbee3d 100644 --- a/lib/features/approvals/data/repositories/approval_template_repository_remote.dart +++ b/lib/features/approvals/data/repositories/approval_template_repository_remote.dart @@ -6,6 +6,10 @@ import '../../domain/entities/approval_template.dart'; import '../../domain/repositories/approval_template_repository.dart'; import '../dtos/approval_template_dto.dart'; +/// 결재 템플릿 관련 API를 호출하는 원격 저장소. +/// +/// - 템플릿/단계 CRUD와 복구 API를 캡슐화한다. +/// - 단계 등록은 별도 엔드포인트로 처리한다. class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { ApprovalTemplateRepositoryRemote({required ApiClient apiClient}) : _api = apiClient; @@ -14,6 +18,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { static const _basePath = '/approval-templates'; + /// 결재 템플릿 목록을 조회한다. 검색/활성 여부 필터를 지원한다. @override Future> list({ int page = 1, @@ -34,6 +39,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { return ApprovalTemplateDto.parsePaginated(response.data); } + /// 템플릿 상세 정보를 조회한다. 필요 시 단계 포함 여부를 지정한다. @override Future fetchDetail( int id, { @@ -50,6 +56,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { ).toEntity(includeSteps: includeSteps); } + /// 템플릿을 생성하고 필요하면 단계까지 함께 등록한다. @override Future create( ApprovalTemplateInput input, { @@ -70,6 +77,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { return fetchDetail(created.id, includeSteps: true); } + /// 템플릿 기본 정보와 단계 구성을 수정한다. @override Future update( int id, @@ -87,11 +95,13 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { return fetchDetail(id, includeSteps: true); } + /// 템플릿을 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 템플릿을 복구한다. @override Future restore(int id) async { final response = await _api.post>( @@ -102,6 +112,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { return ApprovalTemplateDto.fromJson(data).toEntity(includeSteps: false); } + /// 템플릿 단계 전체를 신규로 등록한다. Future _postSteps( int templateId, List steps, @@ -117,6 +128,7 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository { ); } + /// 템플릿 단계 정보를 부분 수정한다. Future _patchSteps( int templateId, List steps, diff --git a/lib/features/approvals/domain/repositories/approval_repository.dart b/lib/features/approvals/domain/repositories/approval_repository.dart index 8f7b51d..884bab8 100644 --- a/lib/features/approvals/domain/repositories/approval_repository.dart +++ b/lib/features/approvals/domain/repositories/approval_repository.dart @@ -2,7 +2,11 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/approval.dart'; +/// 결재 도메인에서 사용하는 저장소 인터페이스. +/// +/// - presentation 레이어는 이 인터페이스만 의존하며, 실제 구현은 data 레이어가 담당한다. abstract class ApprovalRepository { + /// 결재 목록을 조회한다. 필터/페이지 조건을 지원한다. Future> list({ int page = 1, int pageSize = 20, @@ -14,6 +18,7 @@ abstract class ApprovalRepository { bool includeSteps = false, }); + /// 결재 상세 정보를 조회한다. Future fetchDetail( int id, { bool includeSteps = true, @@ -29,11 +34,15 @@ abstract class ApprovalRepository { /// 결재 단계 일괄 생성/재배치 Future assignSteps(ApprovalStepAssignmentInput input); + /// 결재를 생성한다. Future create(ApprovalInput input); + /// 결재를 수정한다. Future update(int id, ApprovalInput input); + /// 결재를 삭제한다. Future delete(int id); + /// 삭제된 결재를 복구한다. Future restore(int id); } diff --git a/lib/features/approvals/domain/repositories/approval_template_repository.dart b/lib/features/approvals/domain/repositories/approval_template_repository.dart index cfade0a..2ba1311 100644 --- a/lib/features/approvals/domain/repositories/approval_template_repository.dart +++ b/lib/features/approvals/domain/repositories/approval_template_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/approval_template.dart'; +/// 결재 템플릿 도메인 저장소 인터페이스. abstract class ApprovalTemplateRepository { + /// 템플릿 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -10,20 +12,25 @@ abstract class ApprovalTemplateRepository { bool? isActive, }); + /// 템플릿 상세를 조회한다. Future fetchDetail(int id, {bool includeSteps = true}); + /// 템플릿을 생성한다. 단계 입력은 옵션이다. Future create( ApprovalTemplateInput input, { List steps = const [], }); + /// 템플릿 기본 정보와 단계 구성을 수정한다. Future update( int id, ApprovalTemplateInput input, { List? steps, }); + /// 템플릿을 삭제한다. Future delete(int id); + /// 삭제된 템플릿을 복구한다. Future restore(int id); } diff --git a/lib/features/approvals/history/data/dtos/approval_history_record_dto.dart b/lib/features/approvals/history/data/dtos/approval_history_record_dto.dart index e79b4a8..b3dd221 100644 --- a/lib/features/approvals/history/data/dtos/approval_history_record_dto.dart +++ b/lib/features/approvals/history/data/dtos/approval_history_record_dto.dart @@ -5,6 +5,7 @@ import 'package:superport_v2/features/approvals/domain/entities/approval.dart'; import '../../domain/entities/approval_history_record.dart'; +/// 결재 이력(History) API 응답을 표현하는 DTO. class ApprovalHistoryRecordDto { ApprovalHistoryRecordDto({ required this.id, @@ -30,6 +31,7 @@ class ApprovalHistoryRecordDto { final DateTime actionAt; final String? note; + /// 원본 JSON에서 필드를 파싱해 DTO를 생성한다. factory ApprovalHistoryRecordDto.fromJson(Map json) { final approvalData = json['approval'] as Map?; final id = json['id'] as int? ?? 0; @@ -74,6 +76,7 @@ class ApprovalHistoryRecordDto { ); } + /// DTO를 도메인 [ApprovalHistoryRecord] 엔티티로 변환한다. ApprovalHistoryRecord toEntity() { return ApprovalHistoryRecord( id: id, @@ -89,6 +92,7 @@ class ApprovalHistoryRecordDto { ); } + /// 페이징 응답을 읽어 [PaginatedResult] 형태로 변환한다. static PaginatedResult parsePaginated( Map? json, ) { @@ -107,6 +111,7 @@ class ApprovalHistoryRecordDto { } } +/// 다양한 형식으로 전달될 수 있는 날짜 값을 파싱한다. DateTime? _parseDate(Object? value) { if (value == null) return null; if (value is DateTime) return value; diff --git a/lib/features/approvals/history/data/repositories/approval_history_repository_remote.dart b/lib/features/approvals/history/data/repositories/approval_history_repository_remote.dart index fdc9ba9..11e83e9 100644 --- a/lib/features/approvals/history/data/repositories/approval_history_repository_remote.dart +++ b/lib/features/approvals/history/data/repositories/approval_history_repository_remote.dart @@ -6,6 +6,7 @@ import '../../domain/entities/approval_history_record.dart'; import '../../domain/repositories/approval_history_repository.dart'; import '../dtos/approval_history_record_dto.dart'; +/// 결재 이력 API를 호출하는 원격 저장소 구현체. class ApprovalHistoryRepositoryRemote implements ApprovalHistoryRepository { ApprovalHistoryRepositoryRemote({required ApiClient apiClient}) : _api = apiClient; @@ -14,6 +15,7 @@ class ApprovalHistoryRepositoryRemote implements ApprovalHistoryRepository { static const _basePath = '/approval-histories'; + /// 결재 이력 목록을 조회한다. @override Future> list({ int page = 1, diff --git a/lib/features/approvals/history/domain/repositories/approval_history_repository.dart b/lib/features/approvals/history/domain/repositories/approval_history_repository.dart index 1b206f3..7c9dda6 100644 --- a/lib/features/approvals/history/domain/repositories/approval_history_repository.dart +++ b/lib/features/approvals/history/domain/repositories/approval_history_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/approval_history_record.dart'; +/// 결재 이력 데이터를 조회하는 도메인 저장소 인터페이스. abstract class ApprovalHistoryRepository { + /// 이력 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, diff --git a/lib/features/approvals/request/presentation/pages/approval_request_page.dart b/lib/features/approvals/request/presentation/pages/approval_request_page.dart index 049ee38..44b7533 100644 --- a/lib/features/approvals/request/presentation/pages/approval_request_page.dart +++ b/lib/features/approvals/request/presentation/pages/approval_request_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import '../../../presentation/pages/approval_page.dart'; +/// 결재 요청 탭에서 사용하는 래퍼 페이지. 실 구현은 [ApprovalPage]를 재사용한다. class ApprovalRequestPage extends StatelessWidget { const ApprovalRequestPage({super.key}); diff --git a/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart b/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart index b31af94..d0484fb 100644 --- a/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart +++ b/lib/features/approvals/step/data/dtos/approval_step_record_dto.dart @@ -5,6 +5,7 @@ import 'package:superport_v2/features/approvals/domain/entities/approval.dart'; import '../../domain/entities/approval_step_record.dart'; +/// 결재 단계 기록 API 응답을 표현하는 DTO. class ApprovalStepRecordDto { ApprovalStepRecordDto({ required this.approvalId, @@ -20,6 +21,7 @@ class ApprovalStepRecordDto { final String? templateName; final ApprovalStep step; + /// JSON에서 필요한 필드를 추출해 DTO를 생성한다. factory ApprovalStepRecordDto.fromJson(Map json) { final approvalData = json['approval'] as Map?; final approvalId = @@ -49,6 +51,7 @@ class ApprovalStepRecordDto { ); } + /// DTO를 [ApprovalStepRecord] 엔티티로 변환한다. ApprovalStepRecord toEntity() { return ApprovalStepRecord( approvalId: approvalId, @@ -59,6 +62,7 @@ class ApprovalStepRecordDto { ); } + /// 페이징 응답을 [PaginatedResult] 형태로 반환한다. static PaginatedResult parsePaginated( Map? json, ) { diff --git a/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart b/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart index 6f10f26..3a6c3f4 100644 --- a/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart +++ b/lib/features/approvals/step/data/repositories/approval_step_repository_remote.dart @@ -7,6 +7,7 @@ import 'package:superport_v2/features/approvals/step/domain/repositories/approva import '../dtos/approval_step_record_dto.dart'; import '../../domain/entities/approval_step_input.dart'; +/// 결재 단계 API를 호출하는 원격 저장소 구현체. class ApprovalStepRepositoryRemote implements ApprovalStepRepository { ApprovalStepRepositoryRemote({required ApiClient apiClient}) : _api = apiClient; @@ -15,6 +16,7 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository { static const _basePath = '/approval-steps'; + /// 결재 단계 목록을 조회한다. @override Future> list({ int page = 1, @@ -40,6 +42,7 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository { return ApprovalStepRecordDto.parsePaginated(response.data); } + /// 단일 결재 단계 상세를 조회한다. @override Future fetchDetail(int id) async { final response = await _api.get>( @@ -50,6 +53,7 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository { return ApprovalStepRecordDto.fromJson(data).toEntity(); } + /// 결재 단계를 생성한다. @override Future create(ApprovalStepInput input) async { final response = await _api.post>( @@ -64,6 +68,7 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository { return ApprovalStepRecordDto.fromJson(data).toEntity(); } + /// 결재 단계를 수정한다. @override Future update(int id, ApprovalStepInput input) async { final response = await _api.patch>( diff --git a/lib/features/approvals/step/domain/repositories/approval_step_repository.dart b/lib/features/approvals/step/domain/repositories/approval_step_repository.dart index a08052a..845fc32 100644 --- a/lib/features/approvals/step/domain/repositories/approval_step_repository.dart +++ b/lib/features/approvals/step/domain/repositories/approval_step_repository.dart @@ -3,7 +3,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/approval_step_input.dart'; import '../entities/approval_step_record.dart'; +/// 결재 단계 목록/상세를 다루는 도메인 저장소 인터페이스. abstract class ApprovalStepRepository { + /// 결재 단계 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -13,6 +15,7 @@ abstract class ApprovalStepRepository { int? approvalId, }); + /// 결재 단계 상세를 조회한다. Future fetchDetail(int id); /// 결재 단계를 생성한다. diff --git a/lib/features/approvals/step/presentation/pages/approval_step_page.dart b/lib/features/approvals/step/presentation/pages/approval_step_page.dart index 374b2ee..06072ae 100644 --- a/lib/features/approvals/step/presentation/pages/approval_step_page.dart +++ b/lib/features/approvals/step/presentation/pages/approval_step_page.dart @@ -14,6 +14,7 @@ import '../../domain/entities/approval_step_input.dart'; import '../../domain/entities/approval_step_record.dart'; import '../../domain/repositories/approval_step_repository.dart'; +/// 결재 단계 관리 진입 페이지. 기능 플래그에 따라 실제 화면 또는 준비중 화면을 노출한다. class ApprovalStepPage extends StatelessWidget { const ApprovalStepPage({super.key}); @@ -50,6 +51,7 @@ class ApprovalStepPage extends StatelessWidget { } } +/// 결재 단계 기능이 활성화된 경우 사용하는 실제 화면 위젯. class _ApprovalStepEnabledPage extends StatefulWidget { const _ApprovalStepEnabledPage(); @@ -58,6 +60,7 @@ class _ApprovalStepEnabledPage extends StatefulWidget { _ApprovalStepEnabledPageState(); } +/// 결재 단계 목록과 필터 상태를 관리하는 상태 클래스. class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> { late final ApprovalStepController _controller; final TextEditingController _searchController = TextEditingController(); diff --git a/lib/features/approvals/template/presentation/pages/approval_template_page.dart b/lib/features/approvals/template/presentation/pages/approval_template_page.dart index 29b65f5..fae3afe 100644 --- a/lib/features/approvals/template/presentation/pages/approval_template_page.dart +++ b/lib/features/approvals/template/presentation/pages/approval_template_page.dart @@ -15,6 +15,7 @@ import '../../../domain/entities/approval_template.dart'; import '../../../domain/repositories/approval_template_repository.dart'; import '../controllers/approval_template_controller.dart'; +/// 결재 템플릿 관리 페이지. 기능 플래그에 따라 준비중 화면을 노출한다. class ApprovalTemplatePage extends StatelessWidget { const ApprovalTemplatePage({super.key}); @@ -51,6 +52,7 @@ class ApprovalTemplatePage extends StatelessWidget { } } +/// 결재 템플릿 기능이 활성화된 경우 사용하는 실제 화면 위젯. class _ApprovalTemplateEnabledPage extends StatefulWidget { const _ApprovalTemplateEnabledPage(); @@ -59,6 +61,7 @@ class _ApprovalTemplateEnabledPage extends StatefulWidget { _ApprovalTemplateEnabledPageState(); } +/// 템플릿 목록/필터/폼 상태를 관리하는 상태 클래스. class _ApprovalTemplateEnabledPageState extends State<_ApprovalTemplateEnabledPage> { late final ApprovalTemplateController _controller; diff --git a/lib/features/dashboard/presentation/pages/dashboard_page.dart b/lib/features/dashboard/presentation/pages/dashboard_page.dart index e613249..da03c57 100644 --- a/lib/features/dashboard/presentation/pages/dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/dashboard_page.dart @@ -5,6 +5,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:superport_v2/widgets/app_layout.dart'; import 'package:superport_v2/widgets/components/empty_state.dart'; +/// Superport 메인 대시보드 화면. class DashboardPage extends StatelessWidget { const DashboardPage({super.key}); diff --git a/lib/features/login/presentation/pages/login_page.dart b/lib/features/login/presentation/pages/login_page.dart index 470a69a..5d3ccaf 100644 --- a/lib/features/login/presentation/pages/login_page.dart +++ b/lib/features/login/presentation/pages/login_page.dart @@ -4,6 +4,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../../core/constants/app_sections.dart'; +/// Superport 로그인 화면. 간단한 유효성 검증 후 대시보드로 이동한다. class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -11,6 +12,7 @@ class LoginPage extends StatefulWidget { State createState() => _LoginPageState(); } +/// 로그인 폼의 상태를 관리한다. class _LoginPageState extends State { final idController = TextEditingController(); final passwordController = TextEditingController(); diff --git a/lib/features/masters/customer/data/dtos/customer_dto.dart b/lib/features/masters/customer/data/dtos/customer_dto.dart index 9a9ff68..bf01aae 100644 --- a/lib/features/masters/customer/data/dtos/customer_dto.dart +++ b/lib/features/masters/customer/data/dtos/customer_dto.dart @@ -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 json) { return CustomerDto( id: json['id'] as int?, @@ -57,6 +59,7 @@ class CustomerDto { ); } + /// DTO를 JSON 맵으로 변환한다. Map 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 parsePaginated(Map? 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 json) { return CustomerZipcodeDto( zipcode: json['zipcode'] as String, @@ -130,6 +137,7 @@ class CustomerZipcodeDto { ); } + /// DTO를 JSON 맵으로 직렬화한다. Map 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 customerInputToJson(CustomerInput input) { final map = input.toPayload(); map.removeWhere((key, value) => value == null); diff --git a/lib/features/masters/customer/data/repositories/customer_repository_remote.dart b/lib/features/masters/customer/data/repositories/customer_repository_remote.dart index 2b8743f..40a0794 100644 --- a/lib/features/masters/customer/data/repositories/customer_repository_remote.dart +++ b/lib/features/masters/customer/data/repositories/customer_repository_remote.dart @@ -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> list({ int page = 1, @@ -37,6 +39,7 @@ class CustomerRepositoryRemote implements CustomerRepository { return CustomerDto.parsePaginated(response.data ?? const {}); } + /// 고객을 생성한다. @override Future create(CustomerInput input) async { final response = await _api.post>( @@ -48,6 +51,7 @@ class CustomerRepositoryRemote implements CustomerRepository { return CustomerDto.fromJson(data).toEntity(); } + /// 고객 정보를 수정한다. @override Future update(int id, CustomerInput input) async { final response = await _api.patch>( @@ -59,11 +63,13 @@ class CustomerRepositoryRemote implements CustomerRepository { return CustomerDto.fromJson(data).toEntity(); } + /// 고객을 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 고객을 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/customer/domain/entities/customer.dart b/lib/features/masters/customer/domain/entities/customer.dart index 98ae15a..11e425b 100644 --- a/lib/features/masters/customer/domain/entities/customer.dart +++ b/lib/features/masters/customer/domain/entities/customer.dart @@ -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 toPayload() { return { 'customer_code': customerCode, diff --git a/lib/features/masters/customer/domain/repositories/customer_repository.dart b/lib/features/masters/customer/domain/repositories/customer_repository.dart index a4bbc7c..6cb6a54 100644 --- a/lib/features/masters/customer/domain/repositories/customer_repository.dart +++ b/lib/features/masters/customer/domain/repositories/customer_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/customer.dart'; +/// 고객 데이터를 다루는 도메인 저장소 인터페이스. abstract class CustomerRepository { + /// 고객 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -12,11 +14,15 @@ abstract class CustomerRepository { bool? isActive, }); + /// 고객을 생성한다. Future create(CustomerInput input); + /// 고객을 수정한다. Future update(int id, CustomerInput input); + /// 고객을 삭제한다. Future delete(int id); + /// 삭제된 고객을 복구한다. Future restore(int id); } diff --git a/lib/features/masters/customer/presentation/controllers/customer_controller.dart b/lib/features/masters/customer/presentation/controllers/customer_controller.dart index 76a2501..3fecc94 100644 --- a/lib/features/masters/customer/presentation/controllers/customer_controller.dart +++ b/lib/features/masters/customer/presentation/controllers/customer_controller.dart @@ -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 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 create(CustomerInput input) async { _setSubmitting(true); try { @@ -129,6 +138,7 @@ class CustomerController extends ChangeNotifier { } } + /// 기존 고객을 수정한다. Future update(int id, CustomerInput input) async { _setSubmitting(true); try { @@ -144,6 +154,7 @@ class CustomerController extends ChangeNotifier { } } + /// 고객을 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -159,6 +170,7 @@ class CustomerController extends ChangeNotifier { } } + /// 삭제된 고객을 복구한다. Future restore(int id) async { _setSubmitting(true); try { @@ -174,6 +186,7 @@ class CustomerController extends ChangeNotifier { } } + /// 에러 메시지를 초기화한다. void clearError() { _errorMessage = null; notifyListeners(); diff --git a/lib/features/masters/customer/presentation/pages/customer_page.dart b/lib/features/masters/customer/presentation/pages/customer_page.dart index 7d1e322..d37f1ab 100644 --- a/lib/features/masters/customer/presentation/pages/customer_page.dart +++ b/lib/features/masters/customer/presentation/pages/customer_page.dart @@ -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: diff --git a/lib/features/masters/group/data/dtos/group_dto.dart b/lib/features/masters/group/data/dtos/group_dto.dart index 3dfd2a4..0fb4852 100644 --- a/lib/features/masters/group/data/dtos/group_dto.dart +++ b/lib/features/masters/group/data/dtos/group_dto.dart @@ -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 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 parsePaginated(Map? 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; diff --git a/lib/features/masters/group/data/repositories/group_repository_remote.dart b/lib/features/masters/group/data/repositories/group_repository_remote.dart index 2469034..73220b7 100644 --- a/lib/features/masters/group/data/repositories/group_repository_remote.dart +++ b/lib/features/masters/group/data/repositories/group_repository_remote.dart @@ -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> list({ int page = 1, @@ -35,6 +37,7 @@ class GroupRepositoryRemote implements GroupRepository { return GroupDto.parsePaginated(response.data ?? const {}); } + /// 새 그룹을 생성한다. @override Future create(GroupInput input) async { final response = await _api.post>( @@ -46,6 +49,7 @@ class GroupRepositoryRemote implements GroupRepository { return GroupDto.fromJson(data).toEntity(); } + /// 그룹 정보를 수정한다. @override Future update(int id, GroupInput input) async { final response = await _api.patch>( @@ -57,11 +61,13 @@ class GroupRepositoryRemote implements GroupRepository { return GroupDto.fromJson(data).toEntity(); } + /// 그룹을 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 그룹을 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/group/presentation/controllers/group_controller.dart b/lib/features/masters/group/presentation/controllers/group_controller.dart index 72d6d5e..b1e9add 100644 --- a/lib/features/masters/group/presentation/controllers/group_controller.dart +++ b/lib/features/masters/group/presentation/controllers/group_controller.dart @@ -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 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 create(GroupInput input) async { _setSubmitting(true); try { @@ -95,6 +102,7 @@ class GroupController extends ChangeNotifier { } } + /// 그룹 정보를 수정한다. Future update(int id, GroupInput input) async { _setSubmitting(true); try { @@ -110,6 +118,7 @@ class GroupController extends ChangeNotifier { } } + /// 그룹을 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -125,6 +134,7 @@ class GroupController extends ChangeNotifier { } } + /// 삭제된 그룹을 복구한다. Future 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(); diff --git a/lib/features/masters/group/presentation/pages/group_page.dart b/lib/features/masters/group/presentation/pages/group_page.dart index a559826..51f13de 100644 --- a/lib/features/masters/group/presentation/pages/group_page.dart +++ b/lib/features/masters/group/presentation/pages/group_page.dart @@ -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(); diff --git a/lib/features/masters/group_permission/data/dtos/group_permission_dto.dart b/lib/features/masters/group_permission/data/dtos/group_permission_dto.dart index 360948f..edffef9 100644 --- a/lib/features/masters/group_permission/data/dtos/group_permission_dto.dart +++ b/lib/features/masters/group_permission/data/dtos/group_permission_dto.dart @@ -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 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 parsePaginated( Map? 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 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 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; diff --git a/lib/features/masters/group_permission/data/repositories/group_permission_repository_remote.dart b/lib/features/masters/group_permission/data/repositories/group_permission_repository_remote.dart index 3430d74..117ec90 100644 --- a/lib/features/masters/group_permission/data/repositories/group_permission_repository_remote.dart +++ b/lib/features/masters/group_permission/data/repositories/group_permission_repository_remote.dart @@ -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> list({ int page = 1, @@ -39,6 +41,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository { return GroupPermissionDto.parsePaginated(response.data ?? const {}); } + /// 그룹 권한을 생성한다. @override Future create(GroupPermissionInput input) async { final response = await _api.post>( @@ -50,6 +53,7 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository { return GroupPermissionDto.fromJson(data).toEntity(); } + /// 그룹 권한을 수정한다. @override Future update(int id, GroupPermissionInput input) async { final response = await _api.patch>( @@ -61,11 +65,13 @@ class GroupPermissionRepositoryRemote implements GroupPermissionRepository { return GroupPermissionDto.fromJson(data).toEntity(); } + /// 그룹 권한을 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 그룹 권한을 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/group_permission/domain/repositories/group_permission_repository.dart b/lib/features/masters/group_permission/domain/repositories/group_permission_repository.dart index 8672e01..d044f4b 100644 --- a/lib/features/masters/group_permission/domain/repositories/group_permission_repository.dart +++ b/lib/features/masters/group_permission/domain/repositories/group_permission_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/group_permission.dart'; +/// 그룹-메뉴 권한을 다루는 도메인 저장소 인터페이스. abstract class GroupPermissionRepository { + /// 권한 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -12,11 +14,15 @@ abstract class GroupPermissionRepository { bool includeDeleted = false, }); + /// 그룹 권한을 생성한다. Future create(GroupPermissionInput input); + /// 그룹 권한을 수정한다. Future update(int id, GroupPermissionInput input); + /// 그룹 권한을 삭제한다. Future delete(int id); + /// 삭제된 그룹 권한을 복구한다. Future restore(int id); } diff --git a/lib/features/masters/group_permission/presentation/controllers/group_permission_controller.dart b/lib/features/masters/group_permission/presentation/controllers/group_permission_controller.dart index 44683e5..1fd8138 100644 --- a/lib/features/masters/group_permission/presentation/controllers/group_permission_controller.dart +++ b/lib/features/masters/group_permission/presentation/controllers/group_permission_controller.dart @@ -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 get groups => List.unmodifiable(_groups); List get menus => List.unmodifiable(_menus); + /// 그룹 목록을 로드해 권한 연결 시 선택할 수 있도록 준비한다. Future loadGroups() async { _isLoadingGroups = true; notifyListeners(); @@ -69,6 +71,7 @@ class GroupPermissionController extends ChangeNotifier { } } + /// 메뉴 목록을 로드해 권한 연결 시 선택할 수 있도록 준비한다. Future loadMenus() async { _isLoadingMenus = true; notifyListeners(); @@ -89,6 +92,7 @@ class GroupPermissionController extends ChangeNotifier { } } + /// 그룹 권한 목록을 조회한다. Future 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 create(GroupPermissionInput input) async { _setSubmitting(true); try { @@ -151,6 +160,7 @@ class GroupPermissionController extends ChangeNotifier { } } + /// 그룹 권한을 수정한다. Future update(int id, GroupPermissionInput input) async { _setSubmitting(true); try { @@ -166,6 +176,7 @@ class GroupPermissionController extends ChangeNotifier { } } + /// 그룹 권한을 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -181,6 +192,7 @@ class GroupPermissionController extends ChangeNotifier { } } + /// 삭제된 그룹 권한을 복구한다. Future 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(); diff --git a/lib/features/masters/group_permission/presentation/pages/group_permission_page.dart b/lib/features/masters/group_permission/presentation/pages/group_permission_page.dart index 070b8bf..50ab9e4 100644 --- a/lib/features/masters/group_permission/presentation/pages/group_permission_page.dart +++ b/lib/features/masters/group_permission/presentation/pages/group_permission_page.dart @@ -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; diff --git a/lib/features/masters/menu/data/dtos/menu_dto.dart b/lib/features/masters/menu/data/dtos/menu_dto.dart index bdcee62..258848c 100644 --- a/lib/features/masters/menu/data/dtos/menu_dto.dart +++ b/lib/features/masters/menu/data/dtos/menu_dto.dart @@ -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 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 parsePaginated(Map? 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 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; diff --git a/lib/features/masters/menu/data/repositories/menu_repository_remote.dart b/lib/features/masters/menu/data/repositories/menu_repository_remote.dart index 5f1c372..5bcdeab 100644 --- a/lib/features/masters/menu/data/repositories/menu_repository_remote.dart +++ b/lib/features/masters/menu/data/repositories/menu_repository_remote.dart @@ -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> list({ int page = 1, @@ -38,6 +40,7 @@ class MenuRepositoryRemote implements MenuRepository { return MenuDto.parsePaginated(response.data ?? const {}); } + /// 새 메뉴를 생성한다. @override Future create(MenuInput input) async { final response = await _api.post>( @@ -49,6 +52,7 @@ class MenuRepositoryRemote implements MenuRepository { return MenuDto.fromJson(data).toEntity(); } + /// 메뉴 정보를 수정한다. @override Future update(int id, MenuInput input) async { final response = await _api.patch>( @@ -60,11 +64,13 @@ class MenuRepositoryRemote implements MenuRepository { return MenuDto.fromJson(data).toEntity(); } + /// 메뉴를 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 메뉴를 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/menu/presentation/controllers/menu_controller.dart b/lib/features/masters/menu/presentation/controllers/menu_controller.dart index 611e28c..8ddaa7f 100644 --- a/lib/features/masters/menu/presentation/controllers/menu_controller.dart +++ b/lib/features/masters/menu/presentation/controllers/menu_controller.dart @@ -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 get parents => _parents; + /// 상위 메뉴 목록을 로드해 드롭다운에 표시한다. Future loadParents() async { _isLoadingParents = true; notifyListeners(); @@ -56,6 +58,7 @@ class MenuController extends ChangeNotifier { } } + /// 메뉴 목록을 조회한다. Future 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 create(MenuInput input) async { _setSubmitting(true); try { @@ -119,6 +127,7 @@ class MenuController extends ChangeNotifier { } } + /// 메뉴 정보를 수정한다. Future update(int id, MenuInput input) async { _setSubmitting(true); try { @@ -135,6 +144,7 @@ class MenuController extends ChangeNotifier { } } + /// 메뉴를 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -151,6 +161,7 @@ class MenuController extends ChangeNotifier { } } + /// 삭제된 메뉴를 복구한다. Future 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(); diff --git a/lib/features/masters/menu/presentation/pages/menu_page.dart b/lib/features/masters/menu/presentation/pages/menu_page.dart index a3676dd..a162738 100644 --- a/lib/features/masters/menu/presentation/pages/menu_page.dart +++ b/lib/features/masters/menu/presentation/pages/menu_page.dart @@ -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(); diff --git a/lib/features/masters/product/data/dtos/product_dto.dart b/lib/features/masters/product/data/dtos/product_dto.dart index 9bfdb22..0ae49b9 100644 --- a/lib/features/masters/product/data/dtos/product_dto.dart +++ b/lib/features/masters/product/data/dtos/product_dto.dart @@ -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 json) { return ProductDto( id: json['id'] as int?, @@ -47,6 +49,7 @@ class ProductDto { ); } + /// DTO를 JSON 맵으로 직렬화한다. Map 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 parsePaginated(Map? 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 json) { return ProductVendorDto( id: json['id'] as int, @@ -130,20 +138,24 @@ class ProductVendorDto { ); } + /// DTO를 JSON 맵으로 직렬화한다. Map 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 json) { return ProductUomDto( id: json['id'] as int, @@ -151,13 +163,16 @@ class ProductUomDto { ); } + /// DTO를 JSON 맵으로 직렬화한다. Map 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 productInputToJson(ProductInput input) { final map = input.toPayload(); map.removeWhere((key, value) => value == null); diff --git a/lib/features/masters/product/data/repositories/product_repository_remote.dart b/lib/features/masters/product/data/repositories/product_repository_remote.dart index aef0568..a4b59d8 100644 --- a/lib/features/masters/product/data/repositories/product_repository_remote.dart +++ b/lib/features/masters/product/data/repositories/product_repository_remote.dart @@ -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> list({ int page = 1, @@ -38,6 +40,7 @@ class ProductRepositoryRemote implements ProductRepository { return ProductDto.parsePaginated(response.data ?? const {}); } + /// 제품을 생성한다. @override Future create(ProductInput input) async { final response = await _api.post>( @@ -49,6 +52,7 @@ class ProductRepositoryRemote implements ProductRepository { return ProductDto.fromJson(data).toEntity(); } + /// 제품 정보를 수정한다. @override Future update(int id, ProductInput input) async { final response = await _api.patch>( @@ -60,11 +64,13 @@ class ProductRepositoryRemote implements ProductRepository { return ProductDto.fromJson(data).toEntity(); } + /// 제품을 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 제품을 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/product/domain/entities/product.dart b/lib/features/masters/product/domain/entities/product.dart index 64fa6fb..16011ef 100644 --- a/lib/features/masters/product/domain/entities/product.dart +++ b/lib/features/masters/product/domain/entities/product.dart @@ -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 toPayload() { return { 'product_code': productCode, diff --git a/lib/features/masters/product/domain/repositories/product_repository.dart b/lib/features/masters/product/domain/repositories/product_repository.dart index 1945ea0..c0a9599 100644 --- a/lib/features/masters/product/domain/repositories/product_repository.dart +++ b/lib/features/masters/product/domain/repositories/product_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/product.dart'; +/// 제품 데이터를 다루는 도메인 저장소 인터페이스. abstract class ProductRepository { + /// 제품 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -12,11 +14,15 @@ abstract class ProductRepository { bool? isActive, }); + /// 제품을 생성한다. Future create(ProductInput input); + /// 제품을 수정한다. Future update(int id, ProductInput input); + /// 제품을 삭제한다. Future delete(int id); + /// 삭제된 제품을 복구한다. Future restore(int id); } diff --git a/lib/features/masters/product/presentation/controllers/product_controller.dart b/lib/features/masters/product/presentation/controllers/product_controller.dart index 964cb13..5500b42 100644 --- a/lib/features/masters/product/presentation/controllers/product_controller.dart +++ b/lib/features/masters/product/presentation/controllers/product_controller.dart @@ -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 get vendorOptions => _vendorOptions; List get uomOptions => _uomOptions; + /// 제품 목록을 조회한다. Future fetch({int page = 1}) async { _isLoading = true; _errorMessage = null; @@ -82,6 +85,7 @@ class ProductController extends ChangeNotifier { } } + /// 필터/폼에서 사용할 공급업체와 단위 목록을 로드한다. Future 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 create(ProductInput input) async { _setSubmitting(true); try { @@ -153,6 +163,7 @@ class ProductController extends ChangeNotifier { } } + /// 제품 정보를 수정한다. Future update(int id, ProductInput input) async { _setSubmitting(true); try { @@ -168,6 +179,7 @@ class ProductController extends ChangeNotifier { } } + /// 제품을 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -183,6 +195,7 @@ class ProductController extends ChangeNotifier { } } + /// 삭제된 제품을 복구한다. Future 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(); diff --git a/lib/features/masters/product/presentation/pages/product_page.dart b/lib/features/masters/product/presentation/pages/product_page.dart index 8537b5b..3443539 100644 --- a/lib/features/masters/product/presentation/pages/product_page.dart +++ b/lib/features/masters/product/presentation/pages/product_page.dart @@ -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(); diff --git a/lib/features/masters/uom/data/dtos/uom_dto.dart b/lib/features/masters/uom/data/dtos/uom_dto.dart index 4ed4984..6a7ce16 100644 --- a/lib/features/masters/uom/data/dtos/uom_dto.dart +++ b/lib/features/masters/uom/data/dtos/uom_dto.dart @@ -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 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 parsePaginated(Map? 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; diff --git a/lib/features/masters/uom/data/repositories/uom_repository_remote.dart b/lib/features/masters/uom/data/repositories/uom_repository_remote.dart index 55a3752..1c72060 100644 --- a/lib/features/masters/uom/data/repositories/uom_repository_remote.dart +++ b/lib/features/masters/uom/data/repositories/uom_repository_remote.dart @@ -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> list({ int page = 1, diff --git a/lib/features/masters/uom/domain/entities/uom.dart b/lib/features/masters/uom/domain/entities/uom.dart index 84a6ece..6615990 100644 --- a/lib/features/masters/uom/domain/entities/uom.dart +++ b/lib/features/masters/uom/domain/entities/uom.dart @@ -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, diff --git a/lib/features/masters/user/data/dtos/user_dto.dart b/lib/features/masters/user/data/dtos/user_dto.dart index 74c1815..19bebfc 100644 --- a/lib/features/masters/user/data/dtos/user_dto.dart +++ b/lib/features/masters/user/data/dtos/user_dto.dart @@ -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 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 parsePaginated( Map? 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 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; diff --git a/lib/features/masters/user/data/repositories/user_repository_remote.dart b/lib/features/masters/user/data/repositories/user_repository_remote.dart index 490a667..207e19e 100644 --- a/lib/features/masters/user/data/repositories/user_repository_remote.dart +++ b/lib/features/masters/user/data/repositories/user_repository_remote.dart @@ -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> list({ int page = 1, @@ -36,6 +38,7 @@ class UserRepositoryRemote implements UserRepository { return UserDto.parsePaginated(response.data ?? const {}); } + /// 사용자를 생성한다. @override Future create(UserInput input) async { final response = await _api.post>( @@ -47,6 +50,7 @@ class UserRepositoryRemote implements UserRepository { return UserDto.fromJson(data).toEntity(); } + /// 사용자 정보를 수정한다. @override Future update(int id, UserInput input) async { final response = await _api.patch>( @@ -58,11 +62,13 @@ class UserRepositoryRemote implements UserRepository { return UserDto.fromJson(data).toEntity(); } + /// 사용자를 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 사용자를 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/user/domain/entities/user.dart b/lib/features/masters/user/domain/entities/user.dart index 2968406..47dc54b 100644 --- a/lib/features/masters/user/domain/entities/user.dart +++ b/lib/features/masters/user/domain/entities/user.dart @@ -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 toPayload() { return { 'employee_no': employeeNo, diff --git a/lib/features/masters/user/domain/repositories/user_repository.dart b/lib/features/masters/user/domain/repositories/user_repository.dart index 28eae7a..48a46d6 100644 --- a/lib/features/masters/user/domain/repositories/user_repository.dart +++ b/lib/features/masters/user/domain/repositories/user_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/user.dart'; +/// 사용자 데이터를 다루는 도메인 저장소 인터페이스. abstract class UserRepository { + /// 사용자 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -11,11 +13,15 @@ abstract class UserRepository { bool? isActive, }); + /// 사용자를 생성한다. Future create(UserInput input); + /// 사용자 정보를 수정한다. Future update(int id, UserInput input); + /// 사용자를 삭제한다. Future delete(int id); + /// 삭제된 사용자를 복구한다. Future restore(int id); } diff --git a/lib/features/masters/user/presentation/controllers/user_controller.dart b/lib/features/masters/user/presentation/controllers/user_controller.dart index 09ea085..7ddf163 100644 --- a/lib/features/masters/user/presentation/controllers/user_controller.dart +++ b/lib/features/masters/user/presentation/controllers/user_controller.dart @@ -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 get groups => _groups; + /// 권한 그룹 목록을 로드한다. Future loadGroups() async { _isLoadingGroups = true; notifyListeners(); @@ -52,6 +55,7 @@ class UserController extends ChangeNotifier { } } + /// 사용자 목록을 조회한다. Future 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 create(UserInput input) async { _setSubmitting(true); try { @@ -108,6 +116,7 @@ class UserController extends ChangeNotifier { } } + /// 사용자 정보를 수정한다. Future update(int id, UserInput input) async { _setSubmitting(true); try { @@ -123,6 +132,7 @@ class UserController extends ChangeNotifier { } } + /// 사용자를 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -138,6 +148,7 @@ class UserController extends ChangeNotifier { } } + /// 삭제된 사용자를 복구한다. Future 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(); diff --git a/lib/features/masters/user/presentation/pages/user_page.dart b/lib/features/masters/user/presentation/pages/user_page.dart index 2618fa3..4166ce8 100644 --- a/lib/features/masters/user/presentation/pages/user_page.dart +++ b/lib/features/masters/user/presentation/pages/user_page.dart @@ -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(); diff --git a/lib/features/masters/vendor/presentation/pages/vendor_page.dart b/lib/features/masters/vendor/presentation/pages/vendor_page.dart index 0291388..720f649 100644 --- a/lib/features/masters/vendor/presentation/pages/vendor_page.dart +++ b/lib/features/masters/vendor/presentation/pages/vendor_page.dart @@ -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(); diff --git a/lib/features/masters/warehouse/data/dtos/warehouse_dto.dart b/lib/features/masters/warehouse/data/dtos/warehouse_dto.dart index 859d306..68876f1 100644 --- a/lib/features/masters/warehouse/data/dtos/warehouse_dto.dart +++ b/lib/features/masters/warehouse/data/dtos/warehouse_dto.dart @@ -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 json) { return WarehouseDto( id: json['id'] as int?, @@ -47,6 +49,7 @@ class WarehouseDto { ); } + /// DTO를 JSON 맵으로 직렬화한다. Map 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 parsePaginated(Map? 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 json) { return WarehouseZipcodeDto( zipcode: json['zipcode'] as String, @@ -112,6 +119,7 @@ class WarehouseZipcodeDto { ); } + /// DTO를 JSON 맵으로 직렬화한다. Map 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 warehouseInputToJson(WarehouseInput input) { final map = input.toPayload(); map.removeWhere((key, value) => value == null); diff --git a/lib/features/masters/warehouse/data/repositories/warehouse_repository_remote.dart b/lib/features/masters/warehouse/data/repositories/warehouse_repository_remote.dart index cb73df4..334671e 100644 --- a/lib/features/masters/warehouse/data/repositories/warehouse_repository_remote.dart +++ b/lib/features/masters/warehouse/data/repositories/warehouse_repository_remote.dart @@ -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> list({ int page = 1, @@ -33,6 +35,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository { return WarehouseDto.parsePaginated(response.data ?? const {}); } + /// 창고를 생성한다. @override Future create(WarehouseInput input) async { final response = await _api.post>( @@ -44,6 +47,7 @@ class WarehouseRepositoryRemote implements WarehouseRepository { return WarehouseDto.fromJson(data).toEntity(); } + /// 창고 정보를 수정한다. @override Future update(int id, WarehouseInput input) async { final response = await _api.patch>( @@ -55,11 +59,13 @@ class WarehouseRepositoryRemote implements WarehouseRepository { return WarehouseDto.fromJson(data).toEntity(); } + /// 창고를 삭제한다. @override Future delete(int id) async { await _api.delete('$_basePath/$id'); } + /// 삭제된 창고를 복구한다. @override Future restore(int id) async { final response = await _api.post>( diff --git a/lib/features/masters/warehouse/domain/entities/warehouse.dart b/lib/features/masters/warehouse/domain/entities/warehouse.dart index f571aa5..9e5278a 100644 --- a/lib/features/masters/warehouse/domain/entities/warehouse.dart +++ b/lib/features/masters/warehouse/domain/entities/warehouse.dart @@ -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 toPayload() { return { 'warehouse_code': warehouseCode, diff --git a/lib/features/masters/warehouse/domain/repositories/warehouse_repository.dart b/lib/features/masters/warehouse/domain/repositories/warehouse_repository.dart index 47ef9ab..2b42e9e 100644 --- a/lib/features/masters/warehouse/domain/repositories/warehouse_repository.dart +++ b/lib/features/masters/warehouse/domain/repositories/warehouse_repository.dart @@ -2,7 +2,9 @@ import 'package:superport_v2/core/common/models/paginated_result.dart'; import '../entities/warehouse.dart'; +/// 창고 데이터를 다루는 도메인 저장소 인터페이스. abstract class WarehouseRepository { + /// 창고 목록을 조회한다. Future> list({ int page = 1, int pageSize = 20, @@ -10,11 +12,15 @@ abstract class WarehouseRepository { bool? isActive, }); + /// 창고를 생성한다. Future create(WarehouseInput input); + /// 창고 정보를 수정한다. Future update(int id, WarehouseInput input); + /// 창고를 삭제한다. Future delete(int id); + /// 삭제된 창고를 복구한다. Future restore(int id); } diff --git a/lib/features/masters/warehouse/presentation/controllers/warehouse_controller.dart b/lib/features/masters/warehouse/presentation/controllers/warehouse_controller.dart index 506ba0f..4138069 100644 --- a/lib/features/masters/warehouse/presentation/controllers/warehouse_controller.dart +++ b/lib/features/masters/warehouse/presentation/controllers/warehouse_controller.dart @@ -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 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 create(WarehouseInput input) async { _setSubmitting(true); try { @@ -97,6 +104,7 @@ class WarehouseController extends ChangeNotifier { } } + /// 창고 정보를 수정한다. Future update(int id, WarehouseInput input) async { _setSubmitting(true); try { @@ -112,6 +120,7 @@ class WarehouseController extends ChangeNotifier { } } + /// 창고를 삭제한다. Future delete(int id) async { _setSubmitting(true); try { @@ -127,6 +136,7 @@ class WarehouseController extends ChangeNotifier { } } + /// 삭제된 창고를 복구한다. Future 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(); diff --git a/lib/features/masters/warehouse/presentation/pages/warehouse_page.dart b/lib/features/masters/warehouse/presentation/pages/warehouse_page.dart index 10231ea..297b59d 100644 --- a/lib/features/masters/warehouse/presentation/pages/warehouse_page.dart +++ b/lib/features/masters/warehouse/presentation/pages/warehouse_page.dart @@ -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(); diff --git a/lib/features/reporting/presentation/pages/reporting_page.dart b/lib/features/reporting/presentation/pages/reporting_page.dart index d7db326..22e0ca4 100644 --- a/lib/features/reporting/presentation/pages/reporting_page.dart +++ b/lib/features/reporting/presentation/pages/reporting_page.dart @@ -13,6 +13,7 @@ import 'package:superport_v2/widgets/components/feedback.dart'; import 'package:superport_v2/widgets/components/filter_bar.dart'; import 'package:superport_v2/widgets/components/superport_date_picker.dart'; +/// 보고서 다운로드 화면 루트 위젯. class ReportingPage extends StatefulWidget { const ReportingPage({super.key}); @@ -20,6 +21,7 @@ class ReportingPage extends StatefulWidget { State createState() => _ReportingPageState(); } +/// 보고서 페이지 UI 상태와 필터 조건을 관리하는 상태 클래스. class _ReportingPageState extends State { late final WarehouseRepository _warehouseRepository; final intl.DateFormat _dateFormat = intl.DateFormat('yyyy.MM.dd'); @@ -99,6 +101,7 @@ class _ReportingPageState extends State { } } + /// 적용된 필터를 초기 상태로 되돌린다. void _resetFilters() { setState(() { _appliedDateRange = null; @@ -112,6 +115,7 @@ class _ReportingPageState extends State { }); } + /// 대기 중인 필터 값을 실제 적용 상태로 확정한다. void _applyFilters() { setState(() { _appliedDateRange = _pendingDateRange; diff --git a/lib/features/util/postal_search/presentation/pages/postal_search_page.dart b/lib/features/util/postal_search/presentation/pages/postal_search_page.dart index 8820335..bac059b 100644 --- a/lib/features/util/postal_search/presentation/pages/postal_search_page.dart +++ b/lib/features/util/postal_search/presentation/pages/postal_search_page.dart @@ -6,6 +6,7 @@ import '../../../../../widgets/app_layout.dart'; import '../models/postal_search_result.dart'; import '../widgets/postal_search_dialog.dart'; +/// 우편번호 검색 모달을 미리보기하는 데모 페이지. class PostalSearchPage extends StatefulWidget { const PostalSearchPage({super.key}); @@ -13,6 +14,7 @@ class PostalSearchPage extends StatefulWidget { State createState() => _PostalSearchPageState(); } +/// 우편번호 검색 결과와 모달 상호작용을 관리한다. class _PostalSearchPageState extends State { PostalSearchResult? _lastSelection; diff --git a/lib/main.dart b/lib/main.dart index 99d8307..5102956 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'core/theme/theme_controller.dart'; import 'injection_container.dart'; import 'core/permissions/permission_manager.dart'; +/// Superport 애플리케이션 진입점. 환경 초기화 후 앱 위젯을 실행한다. Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Environment.initialize(); @@ -16,6 +17,7 @@ Future main() async { runApp(const SuperportApp()); } +/// 전체 앱을 구성하는 루트 위젯. class SuperportApp extends StatefulWidget { const SuperportApp({super.key}); @@ -23,6 +25,7 @@ class SuperportApp extends StatefulWidget { State createState() => _SuperportAppState(); } +/// 테마/권한 스코프를 초기화하고 라우터를 구성한다. class _SuperportAppState extends State { late final ThemeController _themeController; late final PermissionManager _permissionManager; diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 08bd61e..db9d36d 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -6,6 +6,7 @@ import '../core/constants/app_sections.dart'; import '../core/theme/theme_controller.dart'; import '../core/permissions/permission_manager.dart'; +/// 앱 기본 레이아웃을 제공하는 셸 위젯. 사이드 네비게이션과 AppBar를 구성한다. class AppShell extends StatelessWidget { const AppShell({ super.key, @@ -248,10 +249,17 @@ class _ThemeMenuButton extends StatelessWidget { (value) => PopupMenuItem( value: value, child: Row( + mainAxisSize: MainAxisSize.min, children: [ Icon(_icon(value), size: 18), const SizedBox(width: 8), - Text(_label(value)), + Flexible( + child: Text( + _label(value), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), @@ -265,10 +273,18 @@ class _ThemeMenuButton extends StatelessWidget { ), child: Row( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18), const SizedBox(width: 8), - Text('테마 · $label', style: theme.textTheme.labelSmall), + Flexible( + child: Text( + '테마 · $label', + style: theme.textTheme.labelSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), diff --git a/lib/widgets/components/feedback.dart b/lib/widgets/components/feedback.dart index 5b71a06..08e3a1e 100644 --- a/lib/widgets/components/feedback.dart +++ b/lib/widgets/components/feedback.dart @@ -5,22 +5,27 @@ import 'package:shadcn_ui/shadcn_ui.dart'; class SuperportToast { SuperportToast._(); + /// 성공 처리 완료를 사용자에게 안내한다. static void success(BuildContext context, String message) { _show(context, message, _ToastVariant.success); } + /// 정보성 피드백을 노출한다. static void info(BuildContext context, String message) { _show(context, message, _ToastVariant.info); } + /// 주의가 필요한 상황을 경고한다. static void warning(BuildContext context, String message) { _show(context, message, _ToastVariant.warning); } + /// 오류 발생 시 스낵바를 표시한다. static void error(BuildContext context, String message) { _show(context, message, _ToastVariant.error); } + /// 공통 스낵바 렌더링 로직. static void _show( BuildContext context, String message, @@ -106,9 +111,13 @@ class SuperportSkeletonList extends StatelessWidget { this.padding = const EdgeInsets.all(16), }); + /// 렌더링할 스켈레톤 행 개수. final int itemCount; + /// 각 항목 높이. final double height; + /// 행 사이 간격. final double gap; + /// 전체 패딩. final EdgeInsetsGeometry padding; @override diff --git a/lib/widgets/components/filter_bar.dart b/lib/widgets/components/filter_bar.dart index 7487a2e..41c194c 100644 --- a/lib/widgets/components/filter_bar.dart +++ b/lib/widgets/components/filter_bar.dart @@ -157,8 +157,13 @@ class FilterBarActionConfig { final Key? applyKey; final Key? resetKey; + /// 즉시 적용 가능한지 여부. bool get canApply => applyEnabled ?? hasPendingChanges; + + /// Reset 버튼을 노출할지 여부. bool get shouldShowReset => showReset ?? (hasActiveFilters || hasPendingChanges); + + /// Reset 버튼이 활성화 가능한지 여부. bool get canReset => resetEnabled ?? shouldShowReset; } diff --git a/lib/widgets/components/form_field.dart b/lib/widgets/components/form_field.dart index 97bf976..433ede8 100644 --- a/lib/widgets/components/form_field.dart +++ b/lib/widgets/components/form_field.dart @@ -17,12 +17,19 @@ class SuperportFormField extends StatelessWidget { this.spacing = _kFieldSpacing, }); + /// 폼 필드 라벨 텍스트. final String label; + /// 입력 영역으로 렌더링할 위젯. final Widget child; + /// 필수 여부. true면 라벨 옆에 `*` 표시를 추가한다. final bool required; + /// 보조 설명 문구. 에러가 없을 때만 출력된다. final String? caption; + /// 에러 메시지. 존재하면 캡션 대신 우선적으로 노출된다. final String? errorText; + /// 라벨 우측에 배치할 추가 위젯(예: 도움말 버튼). final Widget? trailing; + /// 라벨과 본문 사이 간격. final double spacing; @override @@ -81,14 +88,23 @@ class SuperportTextInput extends StatelessWidget { }); final TextEditingController? controller; + /// 입력 없을 때 보여줄 플레이스홀더 위젯. final Widget? placeholder; + /// 입력 변경 콜백. final ValueChanged? onChanged; + /// 제출(Enter) 시 호출되는 콜백. final ValueChanged? onSubmitted; + /// 키보드 타입. 숫자/이메일 등으로 지정 가능. final TextInputType? keyboardType; + /// 입력 활성 여부. final bool enabled; + /// 읽기 전용 여부. true면 수정 불가. final bool readOnly; + /// 최대 줄 수. 1보다 크면 멀티라인 입력을 지원한다. final int maxLines; + /// 앞에 붙일 위젯 (아이콘 등). final Widget? leading; + /// 뒤에 붙일 위젯 (버튼 등). final Widget? trailing; @override @@ -118,9 +134,13 @@ class SuperportSwitchField extends StatelessWidget { this.caption, }); + /// 스위치 현재 상태. final bool value; + /// 상태 변경 시 호출되는 콜백. final ValueChanged onChanged; + /// 스위치 상단에 표시할 제목. final String? label; + /// 보조 설명 문구. final String? caption; @override diff --git a/lib/widgets/components/keyboard_shortcuts.dart b/lib/widgets/components/keyboard_shortcuts.dart index 4b04d8d..3800283 100644 --- a/lib/widgets/components/keyboard_shortcuts.dart +++ b/lib/widgets/components/keyboard_shortcuts.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +/// 다이얼로그에서 ESC/Enter 키를 처리하고 포커스를 트랩하는 래퍼 위젯. class DialogKeyboardShortcuts extends StatefulWidget { const DialogKeyboardShortcuts({ super.key, diff --git a/lib/widgets/components/responsive.dart b/lib/widgets/components/responsive.dart index 1885dcc..2b4a8a6 100644 --- a/lib/widgets/components/responsive.dart +++ b/lib/widgets/components/responsive.dart @@ -1,10 +1,14 @@ import 'package:flutter/widgets.dart'; +/// 데스크톱 레이아웃으로 간주할 최소 너비(px). const double desktopBreakpoint = 1200; +/// 태블릿 레이아웃을 구분하는 최소 너비(px). const double tabletBreakpoint = 960; +/// 뷰포트 크기별 분기값. enum DeviceBreakpoint { mobile, tablet, desktop } +/// 현재 화면 너비에 맞는 [DeviceBreakpoint]를 계산한다. DeviceBreakpoint breakpointForWidth(double width) { if (width >= desktopBreakpoint) { return DeviceBreakpoint.desktop; @@ -15,34 +19,52 @@ DeviceBreakpoint breakpointForWidth(double width) { return DeviceBreakpoint.mobile; } +/// 주어진 너비가 데스크톱 분기에 해당하는지 여부. bool isDesktop(double width) => width >= desktopBreakpoint; + +/// 주어진 너비가 태블릿 분기에 해당하는지 여부. bool isTablet(double width) => width >= tabletBreakpoint && width < desktopBreakpoint; + +/// 주어진 너비가 모바일 분기에 해당하는지 여부. bool isMobile(double width) => width < tabletBreakpoint; +/// 컨텍스트 기반으로 데스크톱 범위인지 확인한다. bool isDesktopContext(BuildContext context) => isDesktop(MediaQuery.of(context).size.width); + +/// 컨텍스트 기반으로 태블릿 범위인지 확인한다. bool isTabletContext(BuildContext context) => isTablet(MediaQuery.of(context).size.width); + +/// 컨텍스트 기반으로 모바일 범위인지 확인한다. bool isMobileContext(BuildContext context) => isMobile(MediaQuery.of(context).size.width); +/// 반응형 분기 정보를 담는 값 객체. class ResponsiveBreakpoints { ResponsiveBreakpoints._(this.width) : breakpoint = breakpointForWidth(width); + /// 현재 뷰 가로 너비. final double width; + /// 너비에서 계산된 분기값. final DeviceBreakpoint breakpoint; + /// 모바일 범위인지 여부. bool get isMobile => breakpoint == DeviceBreakpoint.mobile; + /// 태블릿 범위인지 여부. bool get isTablet => breakpoint == DeviceBreakpoint.tablet; + /// 데스크톱 범위인지 여부. bool get isDesktop => breakpoint == DeviceBreakpoint.desktop; + /// 현재 컨텍스트에서 [ResponsiveBreakpoints]를 생성한다. static ResponsiveBreakpoints of(BuildContext context) { final size = MediaQuery.of(context).size; return ResponsiveBreakpoints._(size.width); } } +/// 기기 타입에 따라 위젯 빌더를 분기 실행하는 레이아웃 헬퍼. class ResponsiveLayoutBuilder extends StatelessWidget { const ResponsiveLayoutBuilder({ super.key, @@ -51,8 +73,11 @@ class ResponsiveLayoutBuilder extends StatelessWidget { required this.desktop, }); + /// 모바일 뷰에서 사용할 빌더. final WidgetBuilder mobile; + /// 태블릿 뷰에서 사용할 빌더. 제공되지 않으면 데스크톱 빌더를 재사용한다. final WidgetBuilder? tablet; + /// 데스크톱 뷰에서 사용할 빌더. final WidgetBuilder desktop; @override @@ -74,6 +99,7 @@ class ResponsiveLayoutBuilder extends StatelessWidget { } } +/// 특정 분기에서만 child를 표시하는 헬퍼 위젯. class ResponsiveVisibility extends StatelessWidget { const ResponsiveVisibility({ super.key, @@ -86,8 +112,11 @@ class ResponsiveVisibility extends StatelessWidget { }, }); + /// 조건을 만족할 때 보여줄 실제 위젯. final Widget child; + /// 조건을 만족하지 않을 때 대체로 렌더링할 위젯. final Widget replacement; + /// 어떤 분기에서 child를 노출할지 정의한 집합. final Set visibleOn; @override diff --git a/lib/widgets/components/superport_dialog.dart b/lib/widgets/components/superport_dialog.dart index d3eaadb..a3bf52f 100644 --- a/lib/widgets/components/superport_dialog.dart +++ b/lib/widgets/components/superport_dialog.dart @@ -62,6 +62,7 @@ class SuperportDialog extends StatelessWidget { final FutureOr Function()? onSubmit; final bool enableFocusTrap; + /// 공통 다이얼로그를 노출하는 헬퍼. `showDialog`와 동일하게 동작한다. static Future show({ required BuildContext context, required SuperportDialog dialog, @@ -285,7 +286,7 @@ class _SuperportDialogHeader extends StatelessWidget { } } -/// Convenience wrapper around [SuperportDialog.show] to reduce boilerplate in pages. +/// 페이지에서 반복되는 호출 패턴을 줄이기 위한 편의 함수. Future showSuperportDialog({ required BuildContext context, required String title, diff --git a/lib/widgets/components/superport_table.dart b/lib/widgets/components/superport_table.dart index 3b81f8d..49a4d20 100644 --- a/lib/widgets/components/superport_table.dart +++ b/lib/widgets/components/superport_table.dart @@ -11,7 +11,10 @@ class SuperportTableSortState { required this.ascending, }); + /// 정렬 대상이 되는 컬럼 인덱스. final int columnIndex; + + /// 오름차순 여부. `false`면 내림차순이다. final bool ascending; } @@ -25,10 +28,19 @@ class SuperportTablePagination { this.pageSizeOptions = const [10, 20, 50], }); + /// 현재 페이지 번호(1-base). final int currentPage; + + /// 전체 페이지 수. final int totalPages; + + /// 전체 데이터 건수. final int totalItems; + + /// 현재 페이지네이션에서 선택된 페이지 크기. final int pageSize; + + /// 사용자에게 노출할 페이지 크기 옵션 목록. final List pageSizeOptions; } @@ -55,6 +67,7 @@ class SuperportTable extends StatelessWidget { _headerCells = null, _rowCells = null; + /// 헤더와 행을 [ShadTableCell] 단위로 직접 전달할 때 사용하는 생성자. const SuperportTable.fromCells({ super.key, required List header, diff --git a/lib/widgets/spec_page.dart b/lib/widgets/spec_page.dart index 44c50e9..306d916 100644 --- a/lib/widgets/spec_page.dart +++ b/lib/widgets/spec_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; +/// 기능 플래그 비활성화 시 사양 정보를 보여주기 위한 테이블 정의. class SpecTable { const SpecTable({ required this.columns, @@ -13,6 +14,7 @@ class SpecTable { final double? columnWidth; } +/// 사양 페이지의 섹션 정보를 표현한다. class SpecSection { const SpecSection({ required this.title, @@ -27,6 +29,7 @@ class SpecSection { final SpecTable? table; } +/// 기능 비활성 시 화면 대신 노출하는 사양 설명 페이지. class SpecPage extends StatelessWidget { const SpecPage({ super.key, @@ -139,6 +142,7 @@ class SpecPage extends StatelessWidget { } } +/// 사양 테이블을 렌더링하는 내부 위젯. class _SpecTableView extends StatelessWidget { const _SpecTableView({required this.table}); diff --git a/test/features/approvals/presentation/controllers/approval_controller_test.dart b/test/features/approvals/presentation/controllers/approval_controller_test.dart index fb87570..a6e0a3e 100644 --- a/test/features/approvals/presentation/controllers/approval_controller_test.dart +++ b/test/features/approvals/presentation/controllers/approval_controller_test.dart @@ -8,15 +8,20 @@ import 'package:superport_v2/features/approvals/domain/repositories/approval_rep import 'package:superport_v2/features/approvals/domain/repositories/approval_template_repository.dart'; import 'package:superport_v2/features/approvals/presentation/controllers/approval_controller.dart'; +/// ApprovalRepository 모킹 클래스. class _MockApprovalRepository extends Mock implements ApprovalRepository {} +/// Approval 생성 요청을 대체하기 위한 가짜 입력. class _FakeApprovalInput extends Fake implements ApprovalInput {} +/// 단계 행위 요청을 대체하기 위한 가짜 입력. class _FakeStepActionInput extends Fake implements ApprovalStepActionInput {} +/// ApprovalTemplateRepository 모킹 클래스. class _MockApprovalTemplateRepository extends Mock implements ApprovalTemplateRepository {} +/// 템플릿 단계 할당 요청을 대체하기 위한 가짜 입력. class _FakeStepAssignmentInput extends Fake implements ApprovalStepAssignmentInput {} @@ -46,6 +51,7 @@ void main() { histories: const [], ); + /// 테스트용 페이징 응답을 생성하는 헬퍼. PaginatedResult createResult(List items) { return PaginatedResult( items: items, @@ -70,6 +76,7 @@ void main() { ); }); + // fetch 메서드 관련 시나리오 group('fetch', () { setUp(() { when( @@ -86,6 +93,7 @@ void main() { ).thenAnswer((_) async => createResult([sampleApproval])); }); + // 정상적으로 결재 목록을 조회한다. test('목록을 조회한다', () async { await controller.fetch(); @@ -93,6 +101,7 @@ void main() { expect(controller.errorMessage, isNull); }); + // 검색어/상태/기간 필터가 Repository 호출에 반영되는지 확인한다. test('필터 전달을 검증한다', () async { controller.updateQuery('TRX'); controller.updateStatusFilter(ApprovalStatusFilter.approved); @@ -116,6 +125,7 @@ void main() { ).called(1); }); + // Repository 오류 발생 시 errorMessage가 설정된다. test('에러 발생 시 errorMessage 설정', () async { when( () => repository.list(