From 84013993c9528cb6ae58cd72cb9586817da176ad Mon Sep 17 00:00:00 2001
From: bicochan
Date: Fri, 5 Jun 2026 17:18:14 +0900
Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EA=B3=B5=EC=A2=85=EC=BD=94?=
=?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=ED=9A=8C=EA=B3=84=EC=BD=94=EB=93=9C=20?=
=?UTF-8?q?=ED=95=98=EC=9C=84=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?=
=?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/as/next-env.d.ts | 6 --
.../lib/acntg-se-grid.ts | 31 ++++++--
.../lib/use-acntg-se-rows.ts | 78 +++++++++----------
.../ui/acntg-code-management-page.tsx | 66 ++++++++--------
.../acntg-code-management/ui/acntg-se-row.tsx | 2 +-
.../lib/cnstp-se-grid.ts | 31 ++++++--
.../lib/use-cnstp-se-rows.ts | 68 ++++++++--------
.../ui/cnstp-code-management-page.tsx | 66 ++++++++--------
.../cnstp-code-management/ui/cnstp-se-row.tsx | 2 +-
9 files changed, 193 insertions(+), 157 deletions(-)
delete mode 100644 apps/as/next-env.d.ts
diff --git a/apps/as/next-env.d.ts b/apps/as/next-env.d.ts
deleted file mode 100644
index 3cd7048ed..000000000
--- a/apps/as/next-env.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-///
-///
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts b/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts
index 1b7eaa6bf..c0ca7abc7 100644
--- a/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts
+++ b/apps/as/src/pages/acntg-code-management/lib/acntg-se-grid.ts
@@ -145,6 +145,15 @@ export function collectDescendantRowKeys(
return result;
}
+export function insertRootSlot(
+ structure: AcntgDisplaySlot[],
+ slot: AcntgDisplaySlot,
+): AcntgDisplaySlot[] {
+ const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null);
+ if (firstRootIndex === -1) return [slot, ...structure];
+ return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)];
+}
+
export function insertChildSlot(
structure: AcntgDisplaySlot[],
parentRowKey: string,
@@ -153,12 +162,7 @@ export function insertChildSlot(
const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey);
if (parentIndex === -1) return [...structure, slot];
- const parentDepth = structure[parentIndex].depth;
- let insertAt = parentIndex + 1;
- while (insertAt < structure.length && structure[insertAt].depth > parentDepth) {
- insertAt += 1;
- }
-
+ const insertAt = parentIndex + 1;
return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)];
}
@@ -214,6 +218,21 @@ export function markAcntgRowsDeleted(
return { ...state, deletedKeys };
}
+export function validateDraftParentAcntgSeCd(
+ state: AcntgSeGridState,
+ rowKey: string,
+): string | null {
+ const draft = state.drafts[rowKey];
+ if (!draft?.parentRowKey) return null;
+
+ const parent = mergeAcntgRow(state, draft.parentRowKey);
+ if (!parent?.acntgSeCd?.trim()) {
+ return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요.";
+ }
+
+ return null;
+}
+
export function removeDraftFromGrid(state: AcntgSeGridState, rowKey: string): AcntgSeGridState {
const { [rowKey]: _removed, ...restDrafts } = state.drafts;
const keysToRemove = collectDescendantRowKeys(state.structure, rowKey);
diff --git a/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts b/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts
index a8c9ebc72..5fd9f85c5 100644
--- a/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts
+++ b/apps/as/src/pages/acntg-code-management/lib/use-acntg-se-rows.ts
@@ -13,11 +13,13 @@ import {
hasAcntgGridPendingChanges,
hydrateGridFromServer,
insertChildSlot,
+ insertRootSlot,
markAcntgRowsDeleted,
mergeAcntgRow,
mergeAllAcntgRows,
patchAcntgGridRow,
removeDraftFromGrid,
+ validateDraftParentAcntgSeCd,
} from "./acntg-se-grid";
import { buildSaveMultiPayload } from "./build-save-multi-payload";
@@ -81,51 +83,43 @@ export function useAcntgSeRows({ serverContent, onAlert, onSave }: UseAcntgSeRow
useYn: "Y",
},
},
- structure: [...prev.structure, { rowKey, depth: 0, parentRowKey: null }],
+ structure: insertRootSlot(prev.structure, { rowKey, depth: 0, parentRowKey: null }),
}));
}, []);
- const addChildRow = useCallback(
- (parent: AcntgSeRow) => {
- if (parent.isNew) {
- onAlert("상위 행을 먼저 저장한 후 하위 항목을 추가할 수 있습니다.");
- return;
- }
+ const addChildRow = useCallback((parent: AcntgSeRow) => {
+ const rowKey = createDraftRowKey();
+ const slot = {
+ rowKey,
+ depth: 0,
+ parentRowKey: parent.rowKey,
+ };
- const rowKey = createDraftRowKey();
- const slot = {
- rowKey,
- depth: 0,
- parentRowKey: parent.rowKey,
- };
+ setGrid((prev) => {
+ const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey);
+ const depth = (parentSlot?.depth ?? 0) + 1;
- setGrid((prev) => {
- const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey);
- const depth = (parentSlot?.depth ?? 0) + 1;
-
- return {
- ...prev,
- drafts: {
- ...prev.drafts,
- [rowKey]: {
- rowKey,
- parentRowKey: parent.rowKey,
- cdLvNo: (parent.cdLvNo ?? 1) + 1,
- acntgSeCd: "",
- acntgSeCdNm: "",
- expln: "",
- useYn: "Y",
- },
+ return {
+ ...prev,
+ drafts: {
+ ...prev.drafts,
+ [rowKey]: {
+ rowKey,
+ parentRowKey: parent.rowKey,
+ cdLvNo: (parent.cdLvNo ?? 1) + 1,
+ acntgSeCd: "",
+ acntgSeCdNm: "",
+ expln: "",
+ useYn: "Y",
},
- structure: insertChildSlot(prev.structure, parent.rowKey, {
- ...slot,
- depth,
- }),
- };
- });
- },
- [onAlert],
- );
+ },
+ structure: insertChildSlot(prev.structure, parent.rowKey, {
+ ...slot,
+ depth,
+ }),
+ };
+ });
+ }, []);
const removeDraft = useCallback((rowKey: string) => {
setGrid((prev) => removeDraftFromGrid(prev, rowKey));
@@ -146,6 +140,12 @@ export function useAcntgSeRows({ serverContent, onAlert, onSave }: UseAcntgSeRow
for (const row of mergedRows) {
if (!row.isDeleted && (row.isNew || row.isDirty)) {
+ const parentCodeError = validateDraftParentAcntgSeCd(grid, row.rowKey);
+ if (parentCodeError) {
+ onAlert(parentCodeError);
+ return false;
+ }
+
const fieldError = validateAcntgSeRow(row);
if (fieldError) {
onAlert(fieldError);
diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx
index 07594e1c9..d480ced59 100644
--- a/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx
+++ b/apps/as/src/pages/acntg-code-management/ui/acntg-code-management-page.tsx
@@ -105,38 +105,40 @@ function AcntgCodeManagementPage() {
등록된 코드가 없습니다. [코드 추가]로 1단계 코드를 등록하세요.
) : (
-
-
-
-
-
-
-
-
-
-
-
- 코드
- 코드명
- 설명
- 사용 여부
- 구분
- 관리
-
-
-
- {displayRows.map(({ row, depth }) => (
- updateRow(row.rowKey, patch)}
- onAddChild={() => addChildRow(row)}
- onDelete={() => handleDelete(row)}
- />
- ))}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ 코드
+ 코드명
+ 설명
+ 사용 여부
+ 구분
+ 관리
+
+
+
+ {displayRows.map(({ row, depth }) => (
+ updateRow(row.rowKey, patch)}
+ onAddChild={() => addChildRow(row)}
+ onDelete={() => handleDelete(row)}
+ />
+ ))}
+
+
+
)}
diff --git a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx
index a8011e6b8..69b53ec60 100644
--- a/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx
+++ b/apps/as/src/pages/acntg-code-management/ui/acntg-se-row.tsx
@@ -85,7 +85,7 @@ function AcntgSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Acn
type="button"
size="sm"
color="white"
- disabled={row.isNew || !canAddChildRow()}
+ disabled={!canAddChildRow()}
onClick={onAddChild}
>
하위추가
diff --git a/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts b/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts
index 63d9bedbd..6b1bd99f9 100644
--- a/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts
+++ b/apps/as/src/pages/cnstp-code-management/lib/cnstp-se-grid.ts
@@ -145,6 +145,15 @@ export function collectDescendantRowKeys(
return result;
}
+export function insertRootSlot(
+ structure: CnstpDisplaySlot[],
+ slot: CnstpDisplaySlot,
+): CnstpDisplaySlot[] {
+ const firstRootIndex = structure.findIndex((item) => item.parentRowKey === null);
+ if (firstRootIndex === -1) return [slot, ...structure];
+ return [...structure.slice(0, firstRootIndex), slot, ...structure.slice(firstRootIndex)];
+}
+
export function insertChildSlot(
structure: CnstpDisplaySlot[],
parentRowKey: string,
@@ -153,12 +162,7 @@ export function insertChildSlot(
const parentIndex = structure.findIndex((item) => item.rowKey === parentRowKey);
if (parentIndex === -1) return [...structure, slot];
- const parentDepth = structure[parentIndex].depth;
- let insertAt = parentIndex + 1;
- while (insertAt < structure.length && structure[insertAt].depth > parentDepth) {
- insertAt += 1;
- }
-
+ const insertAt = parentIndex + 1;
return [...structure.slice(0, insertAt), slot, ...structure.slice(insertAt)];
}
@@ -214,6 +218,21 @@ export function markCnstpRowsDeleted(
return { ...state, deletedKeys };
}
+export function validateDraftParentCnstpSeCd(
+ state: CnstpSeGridState,
+ rowKey: string,
+): string | null {
+ const draft = state.drafts[rowKey];
+ if (!draft?.parentRowKey) return null;
+
+ const parent = mergeCnstpRow(state, draft.parentRowKey);
+ if (!parent?.cnstpSeCd?.trim()) {
+ return "하위 항목을 추가하려면 상위 코드를 먼저 입력하세요.";
+ }
+
+ return null;
+}
+
export function removeDraftFromGrid(state: CnstpSeGridState, rowKey: string): CnstpSeGridState {
const { [rowKey]: _removed, ...restDrafts } = state.drafts;
const keysToRemove = collectDescendantRowKeys(state.structure, rowKey);
diff --git a/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts b/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts
index fe1976e13..7e4320c8f 100644
--- a/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts
+++ b/apps/as/src/pages/cnstp-code-management/lib/use-cnstp-se-rows.ts
@@ -14,11 +14,13 @@ import {
hasCnstpGridPendingChanges,
hydrateGridFromServer,
insertChildSlot,
+ insertRootSlot,
markCnstpRowsDeleted,
mergeAllCnstpRows,
mergeCnstpRow,
patchCnstpGridRow,
removeDraftFromGrid,
+ validateDraftParentCnstpSeCd,
} from "./cnstp-se-grid";
function validateCnstpSeRow(row: CnstpSeRow): string | null {
@@ -80,47 +82,39 @@ export function useCnstpSeRows({ serverContent, onAlert, onSave }: UseCnstpSeRow
useYn: "Y",
},
},
- structure: [...prev.structure, { rowKey, depth: 0, parentRowKey: null }],
+ structure: insertRootSlot(prev.structure, { rowKey, depth: 0, parentRowKey: null }),
}));
}, []);
- const addChildRow = useCallback(
- (parent: CnstpSeRow) => {
- if (parent.isNew) {
- onAlert("상위 행을 먼저 저장한 후 하위 항목을 추가할 수 있습니다.");
- return;
- }
+ const addChildRow = useCallback((parent: CnstpSeRow) => {
+ const rowKey = createDraftRowKey();
- const rowKey = createDraftRowKey();
+ setGrid((prev) => {
+ const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey);
+ const depth = (parentSlot?.depth ?? 0) + 1;
- setGrid((prev) => {
- const parentSlot = prev.structure.find((item) => item.rowKey === parent.rowKey);
- const depth = (parentSlot?.depth ?? 0) + 1;
-
- return {
- ...prev,
- drafts: {
- ...prev.drafts,
- [rowKey]: {
- rowKey,
- parentRowKey: parent.rowKey,
- cdLvNo: (parent.cdLvNo ?? 1) + 1,
- cnstpSeCd: "",
- cnstpSeCdNm: "",
- expln: "",
- useYn: "Y",
- },
- },
- structure: insertChildSlot(prev.structure, parent.rowKey, {
+ return {
+ ...prev,
+ drafts: {
+ ...prev.drafts,
+ [rowKey]: {
rowKey,
- depth,
parentRowKey: parent.rowKey,
- }),
- };
- });
- },
- [onAlert],
- );
+ cdLvNo: (parent.cdLvNo ?? 1) + 1,
+ cnstpSeCd: "",
+ cnstpSeCdNm: "",
+ expln: "",
+ useYn: "Y",
+ },
+ },
+ structure: insertChildSlot(prev.structure, parent.rowKey, {
+ rowKey,
+ depth,
+ parentRowKey: parent.rowKey,
+ }),
+ };
+ });
+ }, []);
const removeDraft = useCallback((rowKey: string) => {
setGrid((prev) => removeDraftFromGrid(prev, rowKey));
@@ -141,6 +135,12 @@ export function useCnstpSeRows({ serverContent, onAlert, onSave }: UseCnstpSeRow
for (const row of mergedRows) {
if (!row.isDeleted && (row.isNew || row.isDirty)) {
+ const parentCodeError = validateDraftParentCnstpSeCd(grid, row.rowKey);
+ if (parentCodeError) {
+ onAlert(parentCodeError);
+ return false;
+ }
+
const fieldError = validateCnstpSeRow(row);
if (fieldError) {
onAlert(fieldError);
diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx
index 68b0bf8a3..25ad711cb 100644
--- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx
+++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-code-management-page.tsx
@@ -105,38 +105,40 @@ function CnstpCodeManagementPage() {
등록된 코드가 없습니다. [코드 추가]로 1단계 코드를 등록하세요.
) : (
-
-
-
-
-
-
-
-
-
-
-
- 코드
- 코드명
- 설명
- 사용 여부
- 구분
- 관리
-
-
-
- {displayRows.map(({ row, depth }) => (
- updateRow(row.rowKey, patch)}
- onAddChild={() => addChildRow(row)}
- onDelete={() => handleDelete(row)}
- />
- ))}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ 코드
+ 코드명
+ 설명
+ 사용 여부
+ 구분
+ 관리
+
+
+
+ {displayRows.map(({ row, depth }) => (
+ updateRow(row.rowKey, patch)}
+ onAddChild={() => addChildRow(row)}
+ onDelete={() => handleDelete(row)}
+ />
+ ))}
+
+
+
)}
diff --git a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx
index 1588c12c5..090e7d2ba 100644
--- a/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx
+++ b/apps/as/src/pages/cnstp-code-management/ui/cnstp-se-row.tsx
@@ -85,7 +85,7 @@ function CnstpSeRowComponent({ row, depth, onChange, onAddChild, onDelete }: Cns
type="button"
size="sm"
color="white"
- disabled={row.isNew || !canAddChildRow()}
+ disabled={!canAddChildRow()}
onClick={onAddChild}
>
하위추가