[as] feat: 결재선 추가 로직 수정

This commit is contained in:
2026-05-19 17:39:00 +09:00
parent 3c0a16ea67
commit ab3c4e77ee
6 changed files with 39 additions and 61 deletions
@@ -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`)
@@ -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 }));
}
@@ -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,
}));
@@ -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 "결재구분을 선택해 주세요.";
}
@@ -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<ElaprAplnRow>) => {
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) => (
<SortableAplnRow
key={row.rowKey}
row={row}
displaySeqNo={index + 1}
isEditing={editingRowKey === row.rowKey}
isLocked={isEditingLocked}
dragDisabled={isEditingLocked || !row.atrzSeComCd}
@@ -5,12 +5,13 @@ import { CSS } from "@dnd-kit/utilities";
import { Equal } from "lucide-react";
import { CodeSelect, findCodeLabel, useCodeList } from "@/entities/code";
import { Button, Input, TableCell, TableRow } from "@common/components";
import { Button, TableCell, TableRow } from "@common/components";
import type { ElaprAplnRow } from "../model/apln-row-types";
interface SortableAplnRowProps {
row: ElaprAplnRow;
displaySeqNo: number;
isEditing: boolean;
isLocked: boolean;
dragDisabled: boolean;
@@ -24,6 +25,7 @@ interface SortableAplnRowProps {
function SortableAplnRow({
row,
displaySeqNo,
isEditing,
isLocked,
dragDisabled,
@@ -66,21 +68,7 @@ function SortableAplnRow({
</Button>
</TableCell>
<TableCell align="center">{row.isNew ? "-" : (row.aplnId ?? "-")}</TableCell>
<TableCell align="center">
{isEditing ? (
<Input
type="number"
className="w-20"
min={1}
value={row.atrzSeqNo ?? ""}
onChange={(e) =>
onChange({ atrzSeqNo: Number.parseInt(e.target.value, 10) || undefined })
}
/>
) : (
(row.atrzSeqNo ?? "-")
)}
</TableCell>
<TableCell align="center">{displaySeqNo}</TableCell>
<TableCell align="center">
{isEditing ? (
<CodeSelect