결재/인벤토리 주석화 1단계 및 계획 문서 추가
This commit is contained in:
46
doc/commenting_plan.md
Normal file
46
doc/commenting_plan.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 코드 한글 주석화 계획
|
||||||
|
|
||||||
|
## 1. 목표 및 원칙
|
||||||
|
- 모든 퍼블릭 API, 핵심 비즈니스 로직, 복잡한 분기마다 **한국어 주석**을 제공한다.
|
||||||
|
- 기존 `Commenting Policy — Korean` 지침을 준수하고, 불필요한 주석은 추가하지 않는다.
|
||||||
|
- 주석 도입 후에도 **SRP 및 Clean Architecture** 경계를 흐리지 않도록 모듈별 책임을 명확히 한다.
|
||||||
|
|
||||||
|
## 2. 적용 범위
|
||||||
|
| 우선순위 | 모듈/디렉터리 | 설명 |
|
||||||
|
|----------|---------------|------|
|
||||||
|
| 1 | `lib/features/approvals/**` | 결재 도메인은 화면/컨트롤러/리포지토리 모두 복잡도가 높아 주석 필요도가 큼 |
|
||||||
|
| 1 | `lib/features/inventory/**` | 입·출·대여 흐름이 복잡하고 테스트 범위가 넓어 문서화 우선 처리 |
|
||||||
|
| 2 | `lib/core/**` | 네트워크, 권한, 공통 유틸 등 시스템 전체에 영향을 주는 코드 |
|
||||||
|
| 2 | `lib/widgets/**` | 공통 위젯, Table/FilterBar 등 가이드성 주석 필요 |
|
||||||
|
| 3 | `lib/features/masters/**`, `features/reporting/**` | 상대적으로 단순하지만, 주요 API/폼 설명은 보강 |
|
||||||
|
|
||||||
|
## 3. 작업 절차
|
||||||
|
1. **파일 인벤토리 작성**: 우선순위 1 디렉터리부터 핵심 파일 목록과 주석 필요 위치(클래스/메서드)를 표 형태로 정리한다.
|
||||||
|
2. **주석 작성 가이드 수립**:
|
||||||
|
- Doc comment(`///`)에는 목적, 입력/출력, 예외 상황을 명시.
|
||||||
|
- Inline comment(`//`)는 비즈니스 룰, 성능 고려 등 필수 설명에만 사용.
|
||||||
|
- 테스트 코드에는 시나리오 요약 위주로 간결히 작성.
|
||||||
|
3. **모듈별 작업**:
|
||||||
|
- (a) 컨트롤러/서비스 레이어부터 주석 추가 →
|
||||||
|
- (b) 데이터/DTO →
|
||||||
|
- (c) UI 위젯.
|
||||||
|
각 단계마다 `dart format` 실행 후 단위 테스트 수행.
|
||||||
|
4. **검수 및 리팩토링**: 주석 추가 후 가독성이 떨어지는 긴 메서드는 분리/정리.
|
||||||
|
5. **QA 체크리스트**:
|
||||||
|
- `flutter analyze` 경고 없음.
|
||||||
|
- 번역/용어 일관성 확인(예: 결재/승인 용어).
|
||||||
|
- PR 설명에 적용 범위 및 주요 주석 추가 포인트 기록.
|
||||||
|
|
||||||
|
## 4. 일정 및 산출물
|
||||||
|
| 단계 | 예상 산출물 | 비고 |
|
||||||
|
|------|-------------|------|
|
||||||
|
| 준비 (Day 0.5) | 주석 대상 파일 목록, 템플릿 예시 | Markdown 체크리스트 작성 |
|
||||||
|
| 구현 (Day 1~2) | 모듈별 주석 적용 커밋 | 우선순위 1 → 2 → 3 순 |
|
||||||
|
| 검수 (Day 0.5) | 리뷰 피드백 반영, 문서 업데이트 | `IMPLEMENTATION_TASKS.md`에 진행 상황 갱신 |
|
||||||
|
|
||||||
|
## 5. 참고 사항
|
||||||
|
- 새로운 주석 정책 파일(`doc/commenting_guidelines.md`) 초안이 필요하면 준비 단계에서 함께 정의.
|
||||||
|
- API 연동 작업과 병행 시 주석 작성 범위를 명확히 분리해 충돌 방지.
|
||||||
|
- 대용량 파일(>400 LOC)은 주석보다 구조 분리를 우선 검토.
|
||||||
|
## 인벤토리 모듈 주석화
|
||||||
|
자세한 일정과 적용 파일은 doc/inventory_commenting_plan.md 참고
|
||||||
19
doc/inventory_commenting_plan.md
Normal file
19
doc/inventory_commenting_plan.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 인벤토리 모듈 주석화 계획
|
||||||
|
|
||||||
|
## 적용 대상
|
||||||
|
- 입고 페이지: `lib/features/inventory/inbound/presentation/pages/inbound_page.dart`
|
||||||
|
- 출고 페이지: `lib/features/inventory/outbound/presentation/pages/outbound_page.dart`
|
||||||
|
- 대여 페이지: `lib/features/inventory/rental/presentation/pages/rental_page.dart`
|
||||||
|
- 상품 자동완성 위젯: `lib/features/inventory/shared/widgets/product_autocomplete_field.dart`
|
||||||
|
|
||||||
|
## 우선 적용 순서
|
||||||
|
1. 입고 화면(페이지/공유 위젯)
|
||||||
|
2. 출고 화면
|
||||||
|
3. 대여 화면
|
||||||
|
|
||||||
|
## 주석 작성 원칙
|
||||||
|
- 주요 위젯/State 클래스에 `///` 주석으로 역할과 핵심 동작 설명
|
||||||
|
- 복잡한 검증/상태 변경에는 `//` 주석으로 근거 추가
|
||||||
|
- 기존 코드 스타일 유지(영문 변수명 유지, 주석만 한글)
|
||||||
|
- 주석 추가 후 `dart format`, `flutter analyze`, `flutter test`로 검증
|
||||||
|
|
||||||
@@ -4,8 +4,12 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
|||||||
import '../../domain/entities/approval_history_record.dart';
|
import '../../domain/entities/approval_history_record.dart';
|
||||||
import '../../domain/repositories/approval_history_repository.dart';
|
import '../../domain/repositories/approval_history_repository.dart';
|
||||||
|
|
||||||
|
/// 결재 이력에서 필터링 가능한 행위 타입.
|
||||||
enum ApprovalHistoryActionFilter { all, approve, reject, comment }
|
enum ApprovalHistoryActionFilter { all, approve, reject, comment }
|
||||||
|
|
||||||
|
/// 결재 이력 화면의 목록/필터 상태를 관리하는 컨트롤러.
|
||||||
|
///
|
||||||
|
/// 기간, 검색어, 행위 타입에 따라 목록을 조회하고 페이지 사이즈를 조절한다.
|
||||||
class ApprovalHistoryController extends ChangeNotifier {
|
class ApprovalHistoryController extends ChangeNotifier {
|
||||||
ApprovalHistoryController({required ApprovalHistoryRepository repository})
|
ApprovalHistoryController({required ApprovalHistoryRepository repository})
|
||||||
: _repository = repository;
|
: _repository = repository;
|
||||||
@@ -30,6 +34,9 @@ class ApprovalHistoryController extends ChangeNotifier {
|
|||||||
String? get errorMessage => _errorMessage;
|
String? get errorMessage => _errorMessage;
|
||||||
int get pageSize => _result?.pageSize ?? _pageSize;
|
int get pageSize => _result?.pageSize ?? _pageSize;
|
||||||
|
|
||||||
|
/// 현재 필터 조건에 맞춰 결재 이력 목록을 불러온다.
|
||||||
|
///
|
||||||
|
/// 페이지 번호는 1 이상을 기대하며, 실패 시 [_errorMessage]에 사유를 기록한다.
|
||||||
Future<void> fetch({int page = 1}) async {
|
Future<void> fetch({int page = 1}) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -60,22 +67,26 @@ class ApprovalHistoryController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색어를 업데이트해 다음 조회 시 적용될 수 있도록 한다.
|
||||||
void updateQuery(String value) {
|
void updateQuery(String value) {
|
||||||
_query = value;
|
_query = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 행위 타입 필터를 갱신한다.
|
||||||
void updateActionFilter(ApprovalHistoryActionFilter filter) {
|
void updateActionFilter(ApprovalHistoryActionFilter filter) {
|
||||||
_actionFilter = filter;
|
_actionFilter = filter;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 조회 기간을 설정한다. null을 전달하면 해당 조건을 제거한다.
|
||||||
void updateDateRange(DateTime? from, DateTime? to) {
|
void updateDateRange(DateTime? from, DateTime? to) {
|
||||||
_from = from;
|
_from = from;
|
||||||
_to = to;
|
_to = to;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색어/행위/기간 필터를 초기화한다.
|
||||||
void clearFilters() {
|
void clearFilters() {
|
||||||
_query = '';
|
_query = '';
|
||||||
_actionFilter = ApprovalHistoryActionFilter.all;
|
_actionFilter = ApprovalHistoryActionFilter.all;
|
||||||
@@ -84,11 +95,13 @@ class ApprovalHistoryController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 축적된 오류 메시지를 초기화한다.
|
||||||
void clearError() {
|
void clearError() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 페이지 사이즈를 변경한다. 0 이하 값은 무시한다.
|
||||||
void updatePageSize(int value) {
|
void updatePageSize(int value) {
|
||||||
if (value <= 0) {
|
if (value <= 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ class _ApprovalHistoryEnabledPageState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 결재 이력 데이터를 표 형태로 렌더링하는 위젯.
|
||||||
class _ApprovalHistoryTable extends StatelessWidget {
|
class _ApprovalHistoryTable extends StatelessWidget {
|
||||||
const _ApprovalHistoryTable({
|
const _ApprovalHistoryTable({
|
||||||
required this.histories,
|
required this.histories,
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
bool get isApplyingTemplate => _isApplyingTemplate;
|
bool get isApplyingTemplate => _isApplyingTemplate;
|
||||||
int? get applyingTemplateId => _applyingTemplateId;
|
int? get applyingTemplateId => _applyingTemplateId;
|
||||||
|
|
||||||
|
/// 필터 조건과 페이지 정보를 기반으로 결재 목록을 조회한다.
|
||||||
|
///
|
||||||
|
/// [page]가 1보다 작으면 1페이지로 보정한다. 조회 실패 시 [_errorMessage]에
|
||||||
|
/// 예외 메시지를 기록하고, 선택된 상세가 목록에서 사라진 경우 자동으로 선택을 해제한다.
|
||||||
Future<void> fetch({int page = 1}) async {
|
Future<void> fetch({int page = 1}) async {
|
||||||
_isLoadingList = true;
|
_isLoadingList = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -112,6 +116,9 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 결재 단계에서 사용할 수 있는 행위 목록을 로드한다.
|
||||||
|
///
|
||||||
|
/// 이미 데이터가 존재하면 [force]가 `true`일 때만 재조회한다.
|
||||||
Future<void> loadActionOptions({bool force = false}) async {
|
Future<void> loadActionOptions({bool force = false}) async {
|
||||||
if (_actions.isNotEmpty && !force) {
|
if (_actions.isNotEmpty && !force) {
|
||||||
return;
|
return;
|
||||||
@@ -130,6 +137,9 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 활성화된 결재 템플릿 목록을 조회해 캐싱한다.
|
||||||
|
///
|
||||||
|
/// 템플릿이 비어 있거나 [force]가 `true`이면 API를 다시 호출한다.
|
||||||
Future<void> loadTemplates({bool force = false}) async {
|
Future<void> loadTemplates({bool force = false}) async {
|
||||||
if (_templates.isNotEmpty && !force) {
|
if (_templates.isNotEmpty && !force) {
|
||||||
return;
|
return;
|
||||||
@@ -152,6 +162,10 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 특정 결재를 선택하고 상세 정보를 로드한다.
|
||||||
|
///
|
||||||
|
/// 상세와 이력, 단계 정보를 모두 포함해 최신 상태를 유지하며, 실패 시
|
||||||
|
/// [_errorMessage]로 사용자에게 전달할 메시지를 구성한다.
|
||||||
Future<void> selectApproval(int id) async {
|
Future<void> selectApproval(int id) async {
|
||||||
_isLoadingDetail = true;
|
_isLoadingDetail = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -171,11 +185,16 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 선택된 결재 상세를 비우고 화면을 초기화한다.
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
_selected = null;
|
_selected = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 결재 단계에 대해 승인/반려/코멘트 등 지정된 행위를 수행한다.
|
||||||
|
///
|
||||||
|
/// - 유효한 단계 ID와 액션이 존재해야 하며, 실행 중에는 중복 호출을 방지한다.
|
||||||
|
/// - API 호출이 성공하면 목록과 상세 상태를 동기화하고, 실패 시 오류 메시지를 기록한다.
|
||||||
Future<bool> performStepAction({
|
Future<bool> performStepAction({
|
||||||
required ApprovalStep step,
|
required ApprovalStep step,
|
||||||
required ApprovalStepActionType type,
|
required ApprovalStepActionType type,
|
||||||
@@ -224,6 +243,9 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 템플릿 단계 정보를 현재 결재에 덮어씌운다.
|
||||||
|
///
|
||||||
|
/// 선택된 결재가 없거나 템플릿에 등록된 단계가 없으면 즉시 실패로 처리한다.
|
||||||
Future<bool> applyTemplate(int templateId) async {
|
Future<bool> applyTemplate(int templateId) async {
|
||||||
final approvalId = _selected?.id;
|
final approvalId = _selected?.id;
|
||||||
if (approvalId == null) {
|
if (approvalId == null) {
|
||||||
@@ -279,22 +301,26 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색 키워드를 변경하고 UI 갱신을 유도한다.
|
||||||
void updateQuery(String value) {
|
void updateQuery(String value) {
|
||||||
_query = value;
|
_query = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 상태 필터 값을 변경한다.
|
||||||
void updateStatusFilter(ApprovalStatusFilter filter) {
|
void updateStatusFilter(ApprovalStatusFilter filter) {
|
||||||
_statusFilter = filter;
|
_statusFilter = filter;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 조회 기간을 설정한다. 두 값 모두 `null`이면 기간 조건을 해제한다.
|
||||||
void updateDateRange(DateTime? from, DateTime? to) {
|
void updateDateRange(DateTime? from, DateTime? to) {
|
||||||
_fromDate = from;
|
_fromDate = from;
|
||||||
_toDate = to;
|
_toDate = to;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색어/상태/기간 등의 필터 조건을 초기화한다.
|
||||||
void clearFilters() {
|
void clearFilters() {
|
||||||
_query = '';
|
_query = '';
|
||||||
_statusFilter = ApprovalStatusFilter.all;
|
_statusFilter = ApprovalStatusFilter.all;
|
||||||
@@ -303,11 +329,13 @@ class ApprovalController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 사용자에게 노출 중인 오류 메시지를 초기화한다.
|
||||||
void clearError() {
|
void clearError() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 액션 타입과 동일한 코드(또는 별칭)를 가진 결재 행위를 찾는다.
|
||||||
ApprovalAction? _findActionByType(ApprovalStepActionType type) {
|
ApprovalAction? _findActionByType(ApprovalStepActionType type) {
|
||||||
final aliases = _actionAliases[type] ?? [type.code];
|
final aliases = _actionAliases[type] ?? [type.code];
|
||||||
for (final action in _actions) {
|
for (final action in _actions) {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import '../controllers/approval_controller.dart';
|
|||||||
|
|
||||||
const _approvalsResourcePath = '/approvals/requests';
|
const _approvalsResourcePath = '/approvals/requests';
|
||||||
|
|
||||||
|
/// 결재 관리 최상위 페이지.
|
||||||
|
///
|
||||||
|
/// 기능 플래그에 따라 실제 화면 또는 비활성 안내 화면을 보여준다.
|
||||||
class ApprovalPage extends StatelessWidget {
|
class ApprovalPage extends StatelessWidget {
|
||||||
const ApprovalPage({super.key});
|
const ApprovalPage({super.key});
|
||||||
|
|
||||||
@@ -58,6 +61,7 @@ class ApprovalPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 결재 기능이 활성화되었을 때 사용되는 실제 페이지 위젯.
|
||||||
class _ApprovalEnabledPage extends StatefulWidget {
|
class _ApprovalEnabledPage extends StatefulWidget {
|
||||||
const _ApprovalEnabledPage();
|
const _ApprovalEnabledPage();
|
||||||
|
|
||||||
@@ -695,6 +699,9 @@ class _ApprovalTable extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 결재 상세 탭 전체를 감싸는 카드 위젯.
|
||||||
|
///
|
||||||
|
/// 선택 상태와 로딩 여부에 따라 안내 문구 또는 상세 정보를 노출한다.
|
||||||
class _DetailSection extends StatelessWidget {
|
class _DetailSection extends StatelessWidget {
|
||||||
const _DetailSection({
|
const _DetailSection({
|
||||||
required this.approval,
|
required this.approval,
|
||||||
@@ -894,6 +901,7 @@ class _OverviewTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 결재 단계 목록과 템플릿 적용 컨트롤을 묶어 보여주는 탭.
|
||||||
class _StepTab extends StatelessWidget {
|
class _StepTab extends StatelessWidget {
|
||||||
const _StepTab({
|
const _StepTab({
|
||||||
required this.approval,
|
required this.approval,
|
||||||
@@ -1227,6 +1235,7 @@ class _StepTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 템플릿 목록을 선택·적용하고 재조회할 수 있는 툴바 UI.
|
||||||
class _TemplateToolbar extends StatelessWidget {
|
class _TemplateToolbar extends StatelessWidget {
|
||||||
const _TemplateToolbar({
|
const _TemplateToolbar({
|
||||||
required this.templates,
|
required this.templates,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ApprovalApproverAutocompleteField extends StatefulWidget {
|
|||||||
_ApprovalApproverAutocompleteFieldState();
|
_ApprovalApproverAutocompleteFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 승인자 자동완성 필드의 내부 상태를 관리한다.
|
||||||
class _ApprovalApproverAutocompleteFieldState
|
class _ApprovalApproverAutocompleteFieldState
|
||||||
extends State<ApprovalApproverAutocompleteField> {
|
extends State<ApprovalApproverAutocompleteField> {
|
||||||
late final TextEditingController _textController;
|
late final TextEditingController _textController;
|
||||||
@@ -39,6 +40,7 @@ class _ApprovalApproverAutocompleteFieldState
|
|||||||
_syncFromId();
|
_syncFromId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 외부에서 제공된 ID 값으로부터 표시 문자열을 동기화한다.
|
||||||
void _syncFromId() {
|
void _syncFromId() {
|
||||||
final idText = widget.idController.text.trim();
|
final idText = widget.idController.text.trim();
|
||||||
final id = int.tryParse(idText);
|
final id = int.tryParse(idText);
|
||||||
@@ -55,10 +57,12 @@ class _ApprovalApproverAutocompleteFieldState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색어에 매칭되는 승인자 목록을 반환한다.
|
||||||
Iterable<ApprovalApproverCatalogItem> _options(String query) {
|
Iterable<ApprovalApproverCatalogItem> _options(String query) {
|
||||||
return ApprovalApproverCatalog.filter(query);
|
return ApprovalApproverCatalog.filter(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 특정 승인자를 선택했을 때 내부 상태와 콜백을 갱신한다.
|
||||||
void _handleSelected(ApprovalApproverCatalogItem item) {
|
void _handleSelected(ApprovalApproverCatalogItem item) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selected = item;
|
_selected = item;
|
||||||
@@ -68,6 +72,7 @@ class _ApprovalApproverAutocompleteFieldState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 선택된 값을 초기화한다.
|
||||||
void _handleCleared() {
|
void _handleCleared() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selected = null;
|
_selected = null;
|
||||||
@@ -81,6 +86,7 @@ class _ApprovalApproverAutocompleteFieldState
|
|||||||
return '${item.name} (${item.employeeNo}) · ${item.team}';
|
return '${item.name} (${item.employeeNo}) · ${item.team}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 사용자가 직접 입력한 사번(ID)을 기반으로 값을 결정한다.
|
||||||
void _applyManualEntry(String value) {
|
void _applyManualEntry(String value) {
|
||||||
final trimmed = value.trim();
|
final trimmed = value.trim();
|
||||||
if (trimmed.isEmpty) {
|
if (trimmed.isEmpty) {
|
||||||
@@ -104,6 +110,7 @@ class _ApprovalApproverAutocompleteFieldState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 포커스가 해제될 때 수동 입력을 확정한다.
|
||||||
void _handleFocusChange() {
|
void _handleFocusChange() {
|
||||||
if (!_focusNode.hasFocus) {
|
if (!_focusNode.hasFocus) {
|
||||||
_applyManualEntry(_textController.text);
|
_applyManualEntry(_textController.text);
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import '../../domain/entities/approval_step_input.dart';
|
|||||||
import '../../domain/entities/approval_step_record.dart';
|
import '../../domain/entities/approval_step_record.dart';
|
||||||
import '../../domain/repositories/approval_step_repository.dart';
|
import '../../domain/repositories/approval_step_repository.dart';
|
||||||
|
|
||||||
|
/// 결재 단계 관리 화면을 위한 상태 컨트롤러.
|
||||||
|
///
|
||||||
|
/// 단계 목록 조회, 필터링, 단건 생성/수정을 담당한다.
|
||||||
class ApprovalStepController extends ChangeNotifier {
|
class ApprovalStepController extends ChangeNotifier {
|
||||||
ApprovalStepController({required ApprovalStepRepository repository})
|
ApprovalStepController({required ApprovalStepRepository repository})
|
||||||
: _repository = repository;
|
: _repository = repository;
|
||||||
@@ -31,6 +34,7 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
bool get isLoadingDetail => _isLoadingDetail;
|
bool get isLoadingDetail => _isLoadingDetail;
|
||||||
ApprovalStepRecord? get selected => _selected;
|
ApprovalStepRecord? get selected => _selected;
|
||||||
|
|
||||||
|
/// 단계 목록을 조회한다. 페이지와 검색 조건을 함께 적용한다.
|
||||||
Future<void> fetch({int page = 1}) async {
|
Future<void> fetch({int page = 1}) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -53,21 +57,27 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색어를 변경해 다음 조회에 반영한다.
|
||||||
void updateQuery(String value) {
|
void updateQuery(String value) {
|
||||||
_query = value;
|
_query = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 선택한 단계 상태 ID를 갱신한다. null이면 전체 상태를 의미한다.
|
||||||
void updateStatusId(int? value) {
|
void updateStatusId(int? value) {
|
||||||
_statusId = value;
|
_statusId = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 승인자 ID 필터를 변경한다. null이면 필터를 해제한다.
|
||||||
void updateApproverId(int? value) {
|
void updateApproverId(int? value) {
|
||||||
_approverId = value;
|
_approverId = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 특정 결재 단계의 상세를 조회한다.
|
||||||
|
///
|
||||||
|
/// 성공 시 [_selected]에 저장하고, 실패 시 오류 메시지를 기록한다.
|
||||||
Future<ApprovalStepRecord?> fetchDetail(int id) async {
|
Future<ApprovalStepRecord?> fetchDetail(int id) async {
|
||||||
_isLoadingDetail = true;
|
_isLoadingDetail = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -85,11 +95,13 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 현재 선택된 단계를 초기화한다.
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
_selected = null;
|
_selected = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 저장된 오류 메시지를 초기화한다.
|
||||||
void clearError() {
|
void clearError() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -98,6 +110,7 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
bool get hasActiveFilters =>
|
bool get hasActiveFilters =>
|
||||||
_query.trim().isNotEmpty || _statusId != null || _approverId != null;
|
_query.trim().isNotEmpty || _statusId != null || _approverId != null;
|
||||||
|
|
||||||
|
/// 검색어/상태/승인자 필터를 기본값으로 리셋한다.
|
||||||
void resetFilters() {
|
void resetFilters() {
|
||||||
_query = '';
|
_query = '';
|
||||||
_statusId = null;
|
_statusId = null;
|
||||||
@@ -105,6 +118,7 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 새로운 단계를 생성하고 현재 페이지를 다시 불러온다.
|
||||||
Future<ApprovalStepRecord?> createStep(ApprovalStepInput input) async {
|
Future<ApprovalStepRecord?> createStep(ApprovalStepInput input) async {
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -123,6 +137,7 @@ class ApprovalStepController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 기존 단계를 수정하고 목록을 갱신한다.
|
||||||
Future<ApprovalStepRecord?> updateStep(
|
Future<ApprovalStepRecord?> updateStep(
|
||||||
int id,
|
int id,
|
||||||
ApprovalStepInput input,
|
ApprovalStepInput input,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:superport_v2/core/common/models/paginated_result.dart';
|
|||||||
import '../../../domain/entities/approval_template.dart';
|
import '../../../domain/entities/approval_template.dart';
|
||||||
import '../../../domain/repositories/approval_template_repository.dart';
|
import '../../../domain/repositories/approval_template_repository.dart';
|
||||||
|
|
||||||
|
/// 결재 템플릿 목록에서 사용할 상태 필터.
|
||||||
enum ApprovalTemplateStatusFilter { all, activeOnly, inactiveOnly }
|
enum ApprovalTemplateStatusFilter { all, activeOnly, inactiveOnly }
|
||||||
|
|
||||||
/// 결재 템플릿 화면 상태 컨트롤러
|
/// 결재 템플릿 화면 상태 컨트롤러
|
||||||
@@ -31,6 +32,9 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
String? get errorMessage => _errorMessage;
|
String? get errorMessage => _errorMessage;
|
||||||
int get pageSize => _result?.pageSize ?? _pageSize;
|
int get pageSize => _result?.pageSize ?? _pageSize;
|
||||||
|
|
||||||
|
/// 템플릿 목록을 조회해 캐시에 저장한다.
|
||||||
|
///
|
||||||
|
/// 검색어/상태 조건을 함께 적용하며, 실패 시 [_errorMessage]에 예외 메시지를 기록한다.
|
||||||
Future<void> fetch({int page = 1}) async {
|
Future<void> fetch({int page = 1}) async {
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
@@ -58,16 +62,21 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 검색어 조건을 갱신해 다음 조회에 반영한다.
|
||||||
void updateQuery(String value) {
|
void updateQuery(String value) {
|
||||||
_query = value;
|
_query = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 템플릿 상태 필터를 변경한다.
|
||||||
void updateStatusFilter(ApprovalTemplateStatusFilter filter) {
|
void updateStatusFilter(ApprovalTemplateStatusFilter filter) {
|
||||||
_statusFilter = filter;
|
_statusFilter = filter;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 특정 템플릿의 상세 정보를 조회한다.
|
||||||
|
///
|
||||||
|
/// 단계 정보까지 포함해 반환하며 실패 시 null을 반환하고 오류 메시지를 보관한다.
|
||||||
Future<ApprovalTemplate?> fetchDetail(int id) async {
|
Future<ApprovalTemplate?> fetchDetail(int id) async {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -81,6 +90,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 새 템플릿을 생성하고 목록을 1페이지로 새로 고친다.
|
||||||
Future<ApprovalTemplate?> create(
|
Future<ApprovalTemplate?> create(
|
||||||
ApprovalTemplateInput input,
|
ApprovalTemplateInput input,
|
||||||
List<ApprovalTemplateStepInput> steps,
|
List<ApprovalTemplateStepInput> steps,
|
||||||
@@ -99,6 +109,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 기존 템플릿을 수정하고 현재 페이지를 유지한 채 목록을 다시 가져온다.
|
||||||
Future<ApprovalTemplate?> update(
|
Future<ApprovalTemplate?> update(
|
||||||
int id,
|
int id,
|
||||||
ApprovalTemplateInput input,
|
ApprovalTemplateInput input,
|
||||||
@@ -118,6 +129,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 템플릿을 삭제(비활성화)한 뒤 목록을 재조회한다.
|
||||||
Future<bool> delete(int id) async {
|
Future<bool> delete(int id) async {
|
||||||
_setSubmitting(true);
|
_setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
@@ -133,6 +145,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 삭제된 템플릿을 복구한 뒤 목록을 최신 상태로 만든다.
|
||||||
Future<ApprovalTemplate?> restore(int id) async {
|
Future<ApprovalTemplate?> restore(int id) async {
|
||||||
_setSubmitting(true);
|
_setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
@@ -148,6 +161,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 오류 메시지를 초기화한다.
|
||||||
void clearError() {
|
void clearError() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -157,12 +171,14 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
_query.trim().isNotEmpty ||
|
_query.trim().isNotEmpty ||
|
||||||
_statusFilter != ApprovalTemplateStatusFilter.all;
|
_statusFilter != ApprovalTemplateStatusFilter.all;
|
||||||
|
|
||||||
|
/// 검색어와 상태 필터를 기본값으로 되돌린다.
|
||||||
void resetFilters() {
|
void resetFilters() {
|
||||||
_query = '';
|
_query = '';
|
||||||
_statusFilter = ApprovalTemplateStatusFilter.all;
|
_statusFilter = ApprovalTemplateStatusFilter.all;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 페이지 사이즈를 변경한다. 0 이하 값은 무시한다.
|
||||||
void updatePageSize(int value) {
|
void updatePageSize(int value) {
|
||||||
if (value <= 0) {
|
if (value <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -171,6 +187,7 @@ class ApprovalTemplateController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 네트워크 작업 진행 여부를 갱신한다.
|
||||||
void _setSubmitting(bool value) {
|
void _setSubmitting(bool value) {
|
||||||
_isSubmitting = value;
|
_isSubmitting = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ class SuperportTable extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 정렬 상태와 토글 동작을 제공하는 테이블 헤더 셀.
|
||||||
class _SortableHeader extends StatelessWidget {
|
class _SortableHeader extends StatelessWidget {
|
||||||
const _SortableHeader({
|
const _SortableHeader({
|
||||||
required this.child,
|
required this.child,
|
||||||
@@ -245,6 +246,7 @@ class _SortableHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 페이지 이동 및 페이지 사이즈 변경을 처리하는 푸터 UI.
|
||||||
class _PaginationFooter extends StatelessWidget {
|
class _PaginationFooter extends StatelessWidget {
|
||||||
const _PaginationFooter({
|
const _PaginationFooter({
|
||||||
required this.pagination,
|
required this.pagination,
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import 'package:superport_v2/core/theme/superport_shad_theme.dart';
|
|||||||
import 'package:superport_v2/features/inventory/inbound/presentation/pages/inbound_page.dart';
|
import 'package:superport_v2/features/inventory/inbound/presentation/pages/inbound_page.dart';
|
||||||
import 'package:superport_v2/features/inventory/shared/widgets/product_autocomplete_field.dart';
|
import 'package:superport_v2/features/inventory/shared/widgets/product_autocomplete_field.dart';
|
||||||
|
|
||||||
import '../../helpers/test_app.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
@@ -77,17 +75,6 @@ void main() {
|
|||||||
view.resetDevicePixelRatio();
|
view.resetDevicePixelRatio();
|
||||||
});
|
});
|
||||||
|
|
||||||
final router = GoRouter(
|
|
||||||
initialLocation: '/inventory/inbound',
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: '/inventory/inbound',
|
|
||||||
builder: (context, state) =>
|
|
||||||
Scaffold(body: InboundPage(routeUri: state.uri)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ScaffoldMessenger(
|
home: ScaffoldMessenger(
|
||||||
|
|||||||
Reference in New Issue
Block a user