번호 자동 부여 대응 및 API 공통 처리 보강

This commit is contained in:
JiWoong Sul
2025-10-23 14:02:31 +09:00
parent 09c31b2503
commit 7e933a2dda
55 changed files with 948 additions and 586 deletions

View File

@@ -72,8 +72,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
query: {if (includeParts.isNotEmpty) 'include': includeParts.join(',')},
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalDto.fromJson(data).toEntity();
return ApprovalDto.fromJson(_api.unwrapAsMap(response)).toEntity();
}
/// 활성화된 결재 행위 목록을 조회한다.
@@ -133,8 +132,9 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
'$_basePath/$id/can-proceed',
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalProceedStatusDto.fromJson(data).toEntity();
return ApprovalProceedStatusDto.fromJson(
_api.unwrapAsMap(response),
).toEntity();
}
/// 새로운 결재를 생성한다.
@@ -145,8 +145,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
data: input.toPayload(),
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalDto.fromJson(data).toEntity();
return ApprovalDto.fromJson(_api.unwrapAsMap(response)).toEntity();
}
/// 결재 기본 정보를 수정한다.
@@ -157,8 +156,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
data: input.toPayload(),
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalDto.fromJson(data).toEntity();
return ApprovalDto.fromJson(_api.unwrapAsMap(response)).toEntity();
}
/// 결재를 삭제(비활성화)한다.
@@ -174,8 +172,7 @@ class ApprovalRepositoryRemote implements ApprovalRepository {
'$_basePath/$id/restore',
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalDto.fromJson(data).toEntity();
return ApprovalDto.fromJson(_api.unwrapAsMap(response)).toEntity();
}
/// 결재 단계/행위 응답에서 결재 객체 JSON을 추출한다.

View File

@@ -51,9 +51,8 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
query: {if (includeSteps) 'include': 'steps'},
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalTemplateDto.fromJson(
data,
_api.unwrapAsMap(response),
).toEntity(includeSteps: includeSteps);
}
@@ -68,9 +67,8 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
data: input.toCreatePayload(),
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
final created = ApprovalTemplateDto.fromJson(
data,
_api.unwrapAsMap(response),
).toEntity(includeSteps: false);
if (steps.isNotEmpty) {
await _postSteps(created.id, steps);
@@ -109,8 +107,9 @@ class ApprovalTemplateRepositoryRemote implements ApprovalTemplateRepository {
'$_basePath/$id/restore',
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? {};
return ApprovalTemplateDto.fromJson(data).toEntity(includeSteps: false);
return ApprovalTemplateDto.fromJson(
_api.unwrapAsMap(response),
).toEntity(includeSteps: false);
}
/// 템플릿 단계 전체를 신규로 등록한다.

View File

@@ -204,14 +204,12 @@ extension ApprovalStepActionTypeX on ApprovalStepActionType {
class ApprovalCreateInput {
ApprovalCreateInput({
required this.transactionId,
required this.approvalNo,
required this.approvalStatusId,
required this.requestedById,
this.note,
});
final int transactionId;
final String approvalNo;
final int approvalStatusId;
final int requestedById;
final String? note;
@@ -220,7 +218,6 @@ class ApprovalCreateInput {
final trimmedNote = note?.trim();
return {
'transaction_id': transactionId,
'approval_no': approvalNo,
'approval_status_id': approvalStatusId,
'requested_by_id': requestedById,
if (trimmedNote != null && trimmedNote.isNotEmpty) 'note': trimmedNote,

View File

@@ -116,7 +116,7 @@ class _ApprovalHistoryEnabledPageState
final currentPage = result?.page ?? 1;
final totalPages = result == null || result.pageSize == 0
? 1
: (result.total / result.pageSize).ceil().clamp(1, 9999) as int;
: (result.total / result.pageSize).ceil().clamp(1, 9999);
final sortedHistories = _applySorting(histories);
return AppLayout(

View File

@@ -135,7 +135,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
final currentPage = result?.page ?? 1;
final totalPages = result == null || result.pageSize == 0
? 1
: (result.total / result.pageSize).ceil().clamp(1, 9999) as int;
: (result.total / result.pageSize).ceil().clamp(1, 9999);
final hasNext = result == null
? false
: (result.page * result.pageSize) < result.total;
@@ -378,14 +378,13 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
/// 신규 결재 등록 다이얼로그를 열어 UI 단계에서 필요한 필드와 안내를 제공한다.
Future<void> _openCreateApprovalDialog() async {
final approvalNoController = TextEditingController();
final transactionController = TextEditingController();
final requesterController = TextEditingController();
final noteController = TextEditingController();
String? createdApprovalNo;
InventoryEmployeeSuggestion? requesterSelection;
int? statusId = _controller.defaultApprovalStatusId;
String? transactionError;
String? approvalNoError;
String? statusError;
String? requesterError;
@@ -422,8 +421,6 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
onPressed: isSubmitting
? null
: () async {
final approvalNo = approvalNoController.text
.trim();
final transactionText = transactionController.text
.trim();
final transactionId = int.tryParse(
@@ -433,9 +430,6 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
final hasStatuses = statusOptions.isNotEmpty;
setState(() {
approvalNoError = approvalNo.isEmpty
? '결재번호를 입력하세요.'
: null;
transactionError = transactionText.isEmpty
? '트랜잭션 ID를 입력하세요.'
: (transactionId == null
@@ -449,8 +443,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
: null;
});
if (approvalNoError != null ||
transactionError != null ||
if (transactionError != null ||
statusError != null ||
requesterError != null) {
return;
@@ -458,7 +451,6 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
final input = ApprovalCreateInput(
transactionId: transactionId!,
approvalNo: approvalNo,
approvalStatusId: statusId!,
requestedById: requesterSelection!.id,
note: note.isEmpty ? null : note,
@@ -470,6 +462,7 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
return;
}
if (result != null) {
createdApprovalNo = result.approvalNo;
Navigator.of(
context,
rootNavigator: true,
@@ -489,28 +482,18 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text('결재번호', style: shadTheme.textTheme.small),
const SizedBox(height: 8),
ShadInput(
controller: approvalNoController,
enabled: !isSubmitting,
placeholder: const Text('예: APP-2025-0001'),
onChanged: (_) {
if (approvalNoError != null) {
setState(() => approvalNoError = null);
}
},
),
if (approvalNoError != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
approvalNoError!,
style: shadTheme.textTheme.small.copyWith(
color: materialTheme.colorScheme.error,
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: shadTheme.colorScheme.mutedForeground
.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'결재번호는 저장 시 자동으로 생성됩니다.',
style: shadTheme.textTheme.muted,
),
),
const SizedBox(height: 16),
Text('트랜잭션 ID', style: shadTheme.textTheme.small),
const SizedBox(height: 8),
@@ -689,13 +672,16 @@ class _ApprovalEnabledPageState extends State<_ApprovalEnabledPage> {
},
);
approvalNoController.dispose();
transactionController.dispose();
requesterController.dispose();
noteController.dispose();
if (created == true && mounted) {
SuperportToast.success(context, '결재를 생성했습니다.');
final number = createdApprovalNo ?? '-';
SuperportToast.success(
context,
'결재를 생성했습니다. ($number)',
);
}
}

View File

@@ -33,13 +33,10 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
'page': page,
'page_size': pageSize,
if (query != null && query.isNotEmpty) 'q': query,
if (statusId != null) ...{
'status_id': statusId,
'step_status_id': statusId,
},
if (statusId != null) ...{'status_id': statusId},
if (approverId != null) 'approver_id': approverId,
if (approvalId != null) 'approval_id': approvalId,
'include': 'approval,approver,step_status',
'include': 'approval,approver,status',
},
options: Options(responseType: ResponseType.json),
);
@@ -54,8 +51,9 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
'$_basePath/$id',
options: Options(responseType: ResponseType.json),
);
final data = (response.data?['data'] as Map<String, dynamic>?) ?? const {};
return ApprovalStepRecordDto.fromJson(data).toEntity();
return ApprovalStepRecordDto.fromJson(
_api.unwrapAsMap(response),
).toEntity();
}
/// 결재 단계를 생성한다.
@@ -66,11 +64,9 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
data: input.toPayload(),
options: Options(responseType: ResponseType.json),
);
final raw = response.data;
final data =
(raw?['data'] as Map<String, dynamic>?) ??
(raw is Map<String, dynamic> ? raw : const <String, dynamic>{});
return ApprovalStepRecordDto.fromJson(data).toEntity();
return ApprovalStepRecordDto.fromJson(
_api.unwrapAsMap(response),
).toEntity();
}
/// 결재 단계를 수정한다.
@@ -81,11 +77,9 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
data: input.toPayload(),
options: Options(responseType: ResponseType.json),
);
final raw = response.data;
final data =
(raw?['data'] as Map<String, dynamic>?) ??
(raw is Map<String, dynamic> ? raw : const <String, dynamic>{});
return ApprovalStepRecordDto.fromJson(data).toEntity();
return ApprovalStepRecordDto.fromJson(
_api.unwrapAsMap(response),
).toEntity();
}
/// 결재 단계를 비활성화한다.
@@ -101,10 +95,8 @@ class ApprovalStepRepositoryRemote implements ApprovalStepRepository {
'$_basePath/$id/restore',
options: Options(responseType: ResponseType.json),
);
final raw = response.data;
final data =
(raw?['data'] as Map<String, dynamic>?) ??
(raw is Map<String, dynamic> ? raw : const <String, dynamic>{});
return ApprovalStepRecordDto.fromJson(data).toEntity();
return ApprovalStepRecordDto.fromJson(
_api.unwrapAsMap(response),
).toEntity();
}
}

View File

@@ -118,7 +118,7 @@ class _ApprovalStepEnabledPageState extends State<_ApprovalStepEnabledPage> {
final pageSize = result?.pageSize ?? records.length;
final totalPages = pageSize == 0
? 1
: (totalCount / pageSize).ceil().clamp(1, 9999) as int;
: (totalCount / pageSize).ceil().clamp(1, 9999);
final hasNext = result == null
? false
: (result.page * result.pageSize) < result.total;

View File

@@ -116,7 +116,7 @@ class _ApprovalTemplateEnabledPageState
final currentPage = result?.page ?? 1;
final totalPages = result == null || result.pageSize == 0
? 1
: (result.total / result.pageSize).ceil().clamp(1, 9999) as int;
: (result.total / result.pageSize).ceil().clamp(1, 9999);
final showReset =
_searchController.text.trim().isNotEmpty ||
_controller.statusFilter != ApprovalTemplateStatusFilter.all;