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} > 하위추가