From 35b6f4ae54235b4b15820ea292d0c88605bbdd38 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 8 Jul 2026 19:22:40 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EA=B2=B0=EC=9E=AC=EC=84=A0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../elapr/api/elapr-prgrs-stts-service.ts | 6 +- apps/as/src/entities/elapr/api/queries.ts | 3 +- apps/as/src/entities/elapr/index.ts | 16 ++- .../elapr/lib/prgrs-stts-save-utils.ts | 127 ++++++++++++++++++ .../elapr/model/elapr-prgrs-stts-interface.ts | 20 ++- 5 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts diff --git a/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts b/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts index 25991ab97..f1931d556 100644 --- a/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts +++ b/apps/as/src/entities/elapr/api/elapr-prgrs-stts-service.ts @@ -26,8 +26,10 @@ async function findById(elaprPrgrsId: string): Promise { - const response = await client.post(BASE, body); +async function save( + requestDtos: ElaprPrgrsSttsSaveRequestDto[], +): Promise { + const response = await client.post(BASE, requestDtos); return response.data; } diff --git a/apps/as/src/entities/elapr/api/queries.ts b/apps/as/src/entities/elapr/api/queries.ts index 8100a4d3f..e93c1540e 100644 --- a/apps/as/src/entities/elapr/api/queries.ts +++ b/apps/as/src/entities/elapr/api/queries.ts @@ -272,7 +272,8 @@ export function useSaveElaprPrgrsStts() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (body: ElaprPrgrsSttsSaveRequestDto) => elaprPrgrsSttsService.save(body), + mutationFn: (requestDtos: ElaprPrgrsSttsSaveRequestDto[]) => + elaprPrgrsSttsService.save(requestDtos), onSuccess: () => { invalidateElaprPrgrsSttsQueries(queryClient); }, diff --git a/apps/as/src/entities/elapr/index.ts b/apps/as/src/entities/elapr/index.ts index 42673c996..2b09c533d 100644 --- a/apps/as/src/entities/elapr/index.ts +++ b/apps/as/src/entities/elapr/index.ts @@ -43,14 +43,20 @@ export { validateAtrzOrder, validateUniqueSeqNo, } from "./lib/apln-validators"; -export { - buildElaprPrgrsSttsListFromApln, - buildElaprPrgrsSttsListFromAplnRows, -} from "./lib/dmnd-save-utils"; export { findCurrentUserPendingApprovalRequest, hasCurrentUserPendingApprovalRequest, } from "./lib/approval-request-utils"; +export { + buildElaprPrgrsSttsListFromApln, + buildElaprPrgrsSttsListFromAplnRows, +} from "./lib/dmnd-save-utils"; +export type { ElaprPrgrsSttsRow } from "./lib/prgrs-stts-save-utils"; +export { + assignPrgrsSttsAtrzSeqNumbers, + buildPrgrsSttsSavePayload, + hasUnsavedPrgrsSttsChanges, +} from "./lib/prgrs-stts-save-utils"; export { createDraftRowKey, useAplnRows } from "./lib/use-apln-rows"; export { useDndSensors } from "./lib/use-dnd-sensors"; export { @@ -77,11 +83,13 @@ export type { ElaprApproverInsertRequestDto, ElaprPrgrsSttsMyDocSearchParams, ElaprPrgrsSttsResponseDto, + ElaprPrgrsSttsSaveMode, ElaprPrgrsSttsSaveRequestDto, ElaprPrgrsSttsSearchDto, ElaprPrgrsSttsService, ElaprProcessRequestDto, } from "./model/elapr-prgrs-stts-interface"; +export { ELAPR_PRGRS_STTS_SAVE_MODE } from "./model/elapr-prgrs-stts-interface"; export type { ElaprTmplt, ElaprTmpltSaveRequest, diff --git a/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts b/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts new file mode 100644 index 000000000..5b9a1a352 --- /dev/null +++ b/apps/as/src/entities/elapr/lib/prgrs-stts-save-utils.ts @@ -0,0 +1,127 @@ +import { + ELAPR_PRGRS_STTS_SAVE_MODE, + type ElaprPrgrsSttsResponseDto, + type ElaprPrgrsSttsSaveRequestDto, +} from "../model/elapr-prgrs-stts-interface"; + +/** 결재선 그리드 로컬 행 — 진행상태 API 응답 + UI 보조 필드 */ +export interface ElaprPrgrsSttsRow { + rowKey: string; + elaprPrgrsId?: string; + elaprDmndId?: string; + aprvrUuid?: string; + atrzSeComCd?: string; + atrzSeqNo?: number; + atrzPrgrsSttsComCd?: string; + isNew?: boolean; + isDirty?: boolean; + /** 소프트 삭제 — 최종 저장 시 mode:D */ + isDeleted?: boolean; + /** 수정 취소용 스냅샷 */ + snapshot?: ElaprPrgrsSttsRow; +} + +function getVisibleRows(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] { + return rows.filter((row) => !row.isDeleted); +} + +/** 그리드 행 순서(1-based)로 결재순서번호 일괄 채번 */ +export function assignPrgrsSttsAtrzSeqNumbers(rows: ElaprPrgrsSttsRow[]): ElaprPrgrsSttsRow[] { + return rows.map((row, index) => ({ ...row, atrzSeqNo: index + 1 })); +} + +function rowDiffersFromServer( + row: ElaprPrgrsSttsRow, + serverRow: ElaprPrgrsSttsResponseDto, +): boolean { + return ( + row.aprvrUuid !== serverRow.aprvrUuid || + row.atrzSeComCd !== serverRow.atrzSeComCd || + row.atrzSeqNo !== serverRow.atrzSeqNo + ); +} + +function shouldIncludeUpdate( + row: ElaprPrgrsSttsRow, + serverRows: ElaprPrgrsSttsResponseDto[], +): boolean { + if (row.isDirty) return true; + if (!row.elaprPrgrsId) return false; + + const serverRow = serverRows.find((s) => s.elaprPrgrsId === row.elaprPrgrsId); + if (!serverRow) return false; + + return rowDiffersFromServer(row, serverRow); +} + +export function hasUnsavedPrgrsSttsChanges( + rows: ElaprPrgrsSttsRow[], + serverRows: ElaprPrgrsSttsResponseDto[], +): boolean { + if (rows.some((row) => row.isDeleted && row.elaprPrgrsId)) return true; + if (rows.some((row) => row.isNew && !row.isDeleted)) return true; + + const visibleWithSeq = assignPrgrsSttsAtrzSeqNumbers(getVisibleRows(rows)); + + for (const row of visibleWithSeq) { + if (row.isNew) continue; + if (!row.elaprPrgrsId) continue; + if (row.isDirty) return true; + + const serverRow = serverRows.find((s) => s.elaprPrgrsId === row.elaprPrgrsId); + if (!serverRow) continue; + if (rowDiffersFromServer(row, serverRow)) return true; + } + + return false; +} + +export function buildPrgrsSttsSavePayload( + rows: ElaprPrgrsSttsRow[], + elaprDmndId: string, + serverRows: ElaprPrgrsSttsResponseDto[], +): ElaprPrgrsSttsSaveRequestDto[] { + const payload: ElaprPrgrsSttsSaveRequestDto[] = []; + + for (const row of rows) { + if (row.isDeleted && row.elaprPrgrsId) { + payload.push({ + mode: ELAPR_PRGRS_STTS_SAVE_MODE.DELETE, + elaprPrgrsId: row.elaprPrgrsId, + elaprDmndId, + }); + } + } + + const visibleWithSeq = assignPrgrsSttsAtrzSeqNumbers(getVisibleRows(rows)); + + for (const row of visibleWithSeq) { + if (row.isNew) { + payload.push({ + mode: ELAPR_PRGRS_STTS_SAVE_MODE.CREATE, + elaprDmndId, + aprvrUuid: row.aprvrUuid, + atrzSeComCd: row.atrzSeComCd, + atrzSeqNo: row.atrzSeqNo, + atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd, + }); + continue; + } + + if (!row.elaprPrgrsId) continue; + + if (shouldIncludeUpdate(row, serverRows)) { + payload.push({ + mode: ELAPR_PRGRS_STTS_SAVE_MODE.UPDATE, + elaprPrgrsId: row.elaprPrgrsId, + elaprDmndId, + aprvrUuid: row.aprvrUuid, + atrzSeComCd: row.atrzSeComCd, + atrzSeqNo: row.atrzSeqNo, + atrzPrgrsSttsComCd: row.atrzPrgrsSttsComCd, + }); + } + } + + return payload; +} diff --git a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts index 2ab1c5e14..46dad56ec 100644 --- a/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts +++ b/apps/as/src/entities/elapr/model/elapr-prgrs-stts-interface.ts @@ -1,5 +1,15 @@ import type { PageableParams, PageResponse } from "@/shared/model"; +export const ELAPR_PRGRS_STTS_SAVE_MODE = { + CREATE: "C", + READ: "R", + UPDATE: "U", + DELETE: "D", +} as const; + +export type ElaprPrgrsSttsSaveMode = + (typeof ELAPR_PRGRS_STTS_SAVE_MODE)[keyof typeof ELAPR_PRGRS_STTS_SAVE_MODE]; + /** 백엔드 ElaprPrgrsSttsResponseDto */ export interface ElaprPrgrsSttsResponseDto { elaprPrgrsId: string; @@ -16,10 +26,12 @@ export interface ElaprPrgrsSttsResponseDto { /** 백엔드 ElaprPrgrsSttsSaveRequestDto */ export interface ElaprPrgrsSttsSaveRequestDto { + mode?: ElaprPrgrsSttsSaveMode; + elaprPrgrsId?: string; elaprDmndId?: string; - aprvrUuid: string; - atrzSeComCd: string; - atrzSeqNo: number; + aprvrUuid?: string; + atrzSeComCd?: string; + atrzSeqNo?: number; atrzPrgrsSttsComCd?: string; } @@ -56,7 +68,7 @@ export interface ElaprApproverInsertRequestDto { export interface ElaprPrgrsSttsService { findAllByDmndId(elaprDmndId?: string): Promise; findById(elaprPrgrsId: string): Promise; - save(body: ElaprPrgrsSttsSaveRequestDto): Promise; + save(requestDtos: ElaprPrgrsSttsSaveRequestDto[]): Promise; process(elaprPrgrsId: string, body: ElaprProcessRequestDto): Promise; processByDocNoAndAprvrUuid( elaprDocNo: string,