From ab3c4e77ee9e89f89e72f2d30348c1178333e9e4 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 19 May 2026 17:39:00 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EA=B2=B0=EC=9E=AC=EC=84=A0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/docs/approval-line-management/SPEC.md | 8 +-- .../lib/assign-atrz-seq.ts | 6 ++ .../lib/use-apln-rows.ts | 8 +-- .../lib/validators.ts | 3 - .../ui/elapr-apln-grid.tsx | 55 ++++++++----------- .../ui/sortable-apln-row.tsx | 20 ++----- 6 files changed, 39 insertions(+), 61 deletions(-) create mode 100644 apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts diff --git a/apps/as/docs/approval-line-management/SPEC.md b/apps/as/docs/approval-line-management/SPEC.md index edfe4c957..1ae5e65ae 100644 --- a/apps/as/docs/approval-line-management/SPEC.md +++ b/apps/as/docs/approval-line-management/SPEC.md @@ -231,7 +231,7 @@ END | ------------- | ------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------- | | (드래그 핸들) | — | — | `useSortable` 연동 | | 결재선아이디 | `aplnId` | 읽기 전용 | 신규 행: `—`, 저장 후 서버 `aplnId` 표시 | -| 결재순서번호 | `atrzSeqNo` | 숫자 Input | 드래그 시 자동 재부여 가능 | +| 결재순서번호 | `atrzSeqNo` | 읽기 전용 (그리드 행 순서 1부터 표시) | 저장 시 행 순서 기준 자동 채번 | | 결재구분 | `atrzSeComCd` | Select (`ATRZSECD` 공통코드) | 표시명 `atrzSeComNm` (조회 행) | | 결재자 | `aprvrUuid` | 읽기 전용 이름 + **[선택]** 버튼 | 응답에 성명 없음 — 선택 시 로컬 `aprvrNm` 유지 | | 등록일시 | `regDt` | 읽기 전용 | 신규 행은 `—` | @@ -246,10 +246,10 @@ END | 편집 해제 | 행별 **[저장]** 성공 또는 **[취소]** 시 `editingRowId` 초기화 후 전체 조회 모드 복귀 | | 조회 행 | 편집 중이 아닐 때 읽기 전용. **[수정]** 클릭 시 해당 행만 편집 모드 진입 | | 신규 행 | `[행 추가]` 시 맨 아래 편집 행 1건 생성 (`editingRowId` = 임시 키) | -| 필수값 | `atrzSeComCd`, `aprvrUuid`, `atrzSeqNo` | -| 순서 중복 | 동일 서식 내 `atrzSeqNo` 중복 시 저장 전 검증·안내 | +| 필수값 | `atrzSeComCd`, `aprvrUuid` | +| 순서 중복 | 저장 시 행 순서로 자동 채번하므로 중복 없음 | | 결재구분 코드 | `ATRZSECD` — 예: `ATRZ01` 검토자, `ATRZ02` 승인자, `ATRZ03` 최종승인자 | -| **순서 제약** | `ATRZ01`은 최상단 블록, `ATRZ03`은 최하단 블록. 드래그·결재구분 변경·저장 시 `validateAtrzOrder`로 검증, 위반 시 적용/저장 불가 | +| **순서 제약** | `ATRZ01`은 최상단 블록, `ATRZ03`은 최하단 블록. 드래그 종료·저장 시 `validateAtrzOrder`로 검증, 위반 시 적용/저장 불가 (결재구분 변경 시에는 미검증) | | 저장 단위 | 행별 **[저장]** (단건 POST/PUT) 또는 툴바 **[결재선 저장]** (일괄) | #### 행 드래그 정렬 (`@dnd-kit` + `useSortable`) diff --git a/apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts b/apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts new file mode 100644 index 000000000..6b5b63bd4 --- /dev/null +++ b/apps/as/src/pages/approval-line-management/lib/assign-atrz-seq.ts @@ -0,0 +1,6 @@ +import type { ElaprAplnRow } from "../model/apln-row-types"; + +/** 그리드 행 순서(1-based)로 결재순서번호 일괄 채번 */ +export function assignAtrzSeqNumbers(rows: ElaprAplnRow[]): ElaprAplnRow[] { + return rows.map((row, index) => ({ ...row, atrzSeqNo: index + 1 })); +} diff --git a/apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts b/apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts index 4697a7341..26dda19be 100644 --- a/apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts +++ b/apps/as/src/pages/approval-line-management/lib/use-apln-rows.ts @@ -8,6 +8,7 @@ import type { ElaprApln } from "@/entities/elapr"; import type { ElaprAplnRow } from "../model/apln-row-types"; +import { assignAtrzSeqNumbers } from "./assign-atrz-seq"; import { validateAtrzOrder } from "./validators"; function toRow(item: ElaprApln): ElaprAplnRow { @@ -43,14 +44,12 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { if (!elaprTmpltId || isEditingLocked) return; const rowKey = createDraftRowKey(); - const nextSeq = rows.length + 1; setRows((prev) => [ ...prev, { rowKey, elaprTmpltId, - atrzSeqNo: nextSeq, atrzSeComCd: "", aprvrUuid: "", aprvrNm: "", @@ -59,7 +58,7 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { }, ]); setEditingRowKey(rowKey); - }, [elaprTmpltId, isEditingLocked, rows.length]); + }, [elaprTmpltId, isEditingLocked]); const startEdit = useCallback( (rowKey: string) => { @@ -126,9 +125,8 @@ export function useAplnRows({ elaprTmpltId, serverRows }: UseAplnRowsOptions) { const newIndex = prev.findIndex((r) => r.rowKey === overKey); if (oldIndex < 0 || newIndex < 0) return prev; - const moved = arrayMove(prev, oldIndex, newIndex).map((row, idx) => ({ + const moved = assignAtrzSeqNumbers(arrayMove(prev, oldIndex, newIndex)).map((row) => ({ ...row, - atrzSeqNo: idx + 1, isDirty: true, })); diff --git a/apps/as/src/pages/approval-line-management/lib/validators.ts b/apps/as/src/pages/approval-line-management/lib/validators.ts index 96ae63f12..a61de75b8 100644 --- a/apps/as/src/pages/approval-line-management/lib/validators.ts +++ b/apps/as/src/pages/approval-line-management/lib/validators.ts @@ -39,9 +39,6 @@ export function validateAtrzOrder(rows: ElaprAplnRow[]): string | null { } export function validateAplnRow(row: ElaprAplnRow): string | null { - if (row.atrzSeqNo == null || Number.isNaN(row.atrzSeqNo)) { - return "결재순서번호를 입력해 주세요."; - } if (!row.atrzSeComCd?.trim()) { return "결재구분을 선택해 주세요."; } diff --git a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx index e5ccd70f4..e3647d441 100644 --- a/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx +++ b/apps/as/src/pages/approval-line-management/ui/elapr-apln-grid.tsx @@ -22,9 +22,10 @@ import { TableRow, } from "@common/components"; +import { assignAtrzSeqNumbers } from "../lib/assign-atrz-seq"; import { useAplnRows } from "../lib/use-apln-rows"; import { useDndSensors } from "../lib/use-dnd-sensors"; -import { validateAplnRow, validateAtrzOrder, validateUniqueSeqNo } from "../lib/validators"; +import { validateAplnRow, validateAtrzOrder } from "../lib/validators"; import type { ElaprAplnRow } from "../model/apln-row-types"; import type { ReviewerOption } from "../model/mock-reviewers"; @@ -82,14 +83,6 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { }; const handleRowChange = (row: ElaprAplnRow, patch: Partial) => { - const nextRows = rows.map((r) => - r.rowKey === row.rowKey ? { ...r, ...patch, isDirty: true } : r, - ); - const orderError = validateAtrzOrder(nextRows); - if (orderError) { - alert(orderError); - return; - } updateRow(row.rowKey, patch); }; @@ -99,13 +92,12 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { alert(fieldError); return; } - const nextRows = rows.map((r) => (r.rowKey === row.rowKey ? row : r)); - const dupError = validateUniqueSeqNo(nextRows); - if (dupError) { - alert(dupError); - return; - } - const orderError = validateAtrzOrder(nextRows); + + const rowsWithSeq = assignAtrzSeqNumbers(rows.map((r) => (r.rowKey === row.rowKey ? row : r))); + const savedRow = rowsWithSeq.find((r) => r.rowKey === row.rowKey); + if (!savedRow) return; + + const orderError = validateAtrzOrder(rowsWithSeq); if (orderError) { alert(orderError); return; @@ -113,21 +105,21 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { if (!elaprTmpltId) return; - if (row.isNew) { + if (savedRow.isNew) { await createMutation.mutateAsync({ elaprTmpltId, - aprvrUuid: row.aprvrUuid, - atrzSeComCd: row.atrzSeComCd, - atrzSeqNo: row.atrzSeqNo, + aprvrUuid: savedRow.aprvrUuid, + atrzSeComCd: savedRow.atrzSeComCd, + atrzSeqNo: savedRow.atrzSeqNo, }); - } else if (row.aplnId) { + } else if (savedRow.aplnId) { await updateMutation.mutateAsync({ - aplnId: row.aplnId, + aplnId: savedRow.aplnId, body: { elaprTmpltId, - aprvrUuid: row.aprvrUuid, - atrzSeComCd: row.atrzSeComCd, - atrzSeqNo: row.atrzSeqNo, + aprvrUuid: savedRow.aprvrUuid, + atrzSeComCd: savedRow.atrzSeComCd, + atrzSeqNo: savedRow.atrzSeqNo, }, }); } @@ -153,18 +145,14 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { const handleBulkSave = async () => { if (!elaprTmpltId || isEditingLocked) return; - const dupError = validateUniqueSeqNo(rows); - if (dupError) { - alert(dupError); - return; - } - const orderError = validateAtrzOrder(rows); + const rowsWithSeq = assignAtrzSeqNumbers(rows); + const orderError = validateAtrzOrder(rowsWithSeq); if (orderError) { alert(orderError); return; } - const dirtyRows = rows.filter((r) => r.isDirty || r.isNew); + const dirtyRows = rowsWithSeq.filter((r) => r.isDirty || r.isNew); for (const row of dirtyRows) { const fieldError = validateAplnRow(row); @@ -257,10 +245,11 @@ function ElaprAplnGrid({ elaprTmpltId }: ElaprAplnGridProps) { items={rows.map((r) => r.rowKey)} strategy={verticalListSortingStrategy} > - {rows.map((row) => ( + {rows.map((row, index) => ( {row.isNew ? "-" : (row.aplnId ?? "-")} - - {isEditing ? ( - - onChange({ atrzSeqNo: Number.parseInt(e.target.value, 10) || undefined }) - } - /> - ) : ( - (row.atrzSeqNo ?? "-") - )} - + {displaySeqNo} {isEditing ? (