From f97edad1eaa3867da0915f8d79bae0b25512ba8e Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 4 Mar 2026 18:42:44 +0900 Subject: [PATCH] feat: Enhance screen group deletion functionality with optional numbering rules deletion - Added a new query parameter `deleteNumberingRules` to the `deleteScreenGroup` function, allowing users to specify if numbering rules should be deleted when a root screen group is removed. - Updated the `deleteScreenGroup` controller to handle the deletion of numbering rules conditionally based on the new parameter. - Enhanced the frontend `ScreenGroupTreeView` component to include a checkbox for users to confirm the deletion of numbering rules when deleting a root group, improving user control and clarity during deletion operations. - Implemented appropriate warnings and messages to inform users about the implications of deleting numbering rules, ensuring better user experience and data integrity awareness. --- .../src/controllers/screenGroupController.ts | 47 ++++++++------- .../src/services/tableManagementService.ts | 54 ++++++++++++----- .../admin/systemMng/tableMngList/page.tsx | 25 +++++++- .../components/screen/ScreenGroupTreeView.tsx | 58 +++++++++++++++++-- frontend/lib/api/screenGroup.ts | 10 +++- .../SplitPanelLayout2Component.tsx | 20 ++++--- .../TableSectionRenderer.tsx | 2 +- 7 files changed, 158 insertions(+), 58 deletions(-) diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index 0e97e2e2..51d903af 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -308,6 +308,7 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response const client = await pool.connect(); try { const { id } = req.params; + const deleteNumberingRules = req.query.deleteNumberingRules === "true"; const companyCode = req.user?.companyCode || "*"; await client.query('BEGIN'); @@ -380,31 +381,29 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response }); } - // 2-4. 해당 회사의 채번 규칙 삭제 (최상위 그룹 삭제 시) - // 삭제되는 그룹이 최상위인지 확인 - const isRootGroup = await client.query( - `SELECT 1 FROM screen_groups WHERE id = $1 AND parent_group_id IS NULL`, - [id] - ); - - if (isRootGroup.rows.length > 0) { - // 최상위 그룹 삭제 시 해당 회사의 채번 규칙도 삭제 - // 먼저 파트 삭제 - await client.query( - `DELETE FROM numbering_rule_parts - WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`, - [targetCompanyCode] + // 2-4. 해당 회사의 채번 규칙 삭제 (최상위 그룹 삭제 + 사용자가 명시적으로 요청한 경우에만) + if (deleteNumberingRules) { + const isRootGroup = await client.query( + `SELECT 1 FROM screen_groups WHERE id = $1 AND parent_group_id IS NULL`, + [id] ); - // 규칙 삭제 - const deletedRules = await client.query( - `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`, - [targetCompanyCode] - ); - if (deletedRules.rowCount && deletedRules.rowCount > 0) { - logger.info("그룹 삭제 시 채번 규칙 삭제", { - companyCode: targetCompanyCode, - deletedCount: deletedRules.rowCount - }); + + if (isRootGroup.rows.length > 0) { + await client.query( + `DELETE FROM numbering_rule_parts + WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`, + [targetCompanyCode] + ); + const deletedRules = await client.query( + `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`, + [targetCompanyCode] + ); + if (deletedRules.rowCount && deletedRules.rowCount > 0) { + logger.warn("최상위 그룹 삭제 시 채번 규칙 삭제 (사용자 명시 요청)", { + companyCode: targetCompanyCode, + deletedCount: deletedRules.rowCount + }); + } } } } diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 791940ec..9dea4037 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -513,6 +513,15 @@ export class TableManagementService { detailSettingsStr = JSON.stringify(settings.detailSettings); } + // 입력타입에 해당하지 않는 설정값은 NULL로 강제 초기화 + const inputType = settings.inputType; + const referenceTable = inputType === "entity" ? (settings.referenceTable || null) : null; + const referenceColumn = inputType === "entity" ? (settings.referenceColumn || null) : null; + const displayColumn = inputType === "entity" ? (settings.displayColumn || null) : null; + const codeCategory = inputType === "code" ? (settings.codeCategory || null) : null; + const codeValue = inputType === "code" ? (settings.codeValue || null) : null; + const categoryRef = inputType === "category" ? (settings.categoryRef || null) : null; + await query( `INSERT INTO table_type_columns ( table_name, column_name, column_label, input_type, detail_settings, @@ -525,11 +534,11 @@ export class TableManagementService { column_label = COALESCE(EXCLUDED.column_label, table_type_columns.column_label), input_type = COALESCE(EXCLUDED.input_type, table_type_columns.input_type), detail_settings = COALESCE(EXCLUDED.detail_settings, table_type_columns.detail_settings), - code_category = COALESCE(EXCLUDED.code_category, table_type_columns.code_category), - code_value = COALESCE(EXCLUDED.code_value, table_type_columns.code_value), - reference_table = COALESCE(EXCLUDED.reference_table, table_type_columns.reference_table), - reference_column = COALESCE(EXCLUDED.reference_column, table_type_columns.reference_column), - display_column = COALESCE(EXCLUDED.display_column, table_type_columns.display_column), + code_category = EXCLUDED.code_category, + code_value = EXCLUDED.code_value, + reference_table = EXCLUDED.reference_table, + reference_column = EXCLUDED.reference_column, + display_column = EXCLUDED.display_column, display_order = COALESCE(EXCLUDED.display_order, table_type_columns.display_order), is_visible = COALESCE(EXCLUDED.is_visible, table_type_columns.is_visible), category_ref = EXCLUDED.category_ref, @@ -538,17 +547,17 @@ export class TableManagementService { tableName, columnName, settings.columnLabel, - settings.inputType, + inputType, detailSettingsStr, - settings.codeCategory, - settings.codeValue, - settings.referenceTable, - settings.referenceColumn, - settings.displayColumn, + codeCategory, + codeValue, + referenceTable, + referenceColumn, + displayColumn, settings.displayOrder || 0, settings.isVisible !== undefined ? settings.isVisible : true, companyCode, - settings.categoryRef || null, + categoryRef, ] ); @@ -849,16 +858,26 @@ export class TableManagementService { ...detailSettings, }; - // table_type_columns 테이블에서 업데이트 (company_code 추가) + // 입력타입 변경 시 이전 타입의 설정값 초기화 + const clearEntity = finalInputType !== "entity"; + const clearCode = finalInputType !== "code"; + const clearCategory = finalInputType !== "category"; + await query( `INSERT INTO table_type_columns ( - table_name, column_name, input_type, detail_settings, + table_name, column_name, input_type, detail_settings, is_nullable, display_order, company_code, created_date, updated_date ) VALUES ($1, $2, $3, $4, 'Y', 0, $5, now(), now()) - ON CONFLICT (table_name, column_name, company_code) - DO UPDATE SET + ON CONFLICT (table_name, column_name, company_code) + DO UPDATE SET input_type = EXCLUDED.input_type, detail_settings = EXCLUDED.detail_settings, + reference_table = CASE WHEN $6 THEN NULL ELSE table_type_columns.reference_table END, + reference_column = CASE WHEN $6 THEN NULL ELSE table_type_columns.reference_column END, + display_column = CASE WHEN $6 THEN NULL ELSE table_type_columns.display_column END, + code_category = CASE WHEN $7 THEN NULL ELSE table_type_columns.code_category END, + code_value = CASE WHEN $7 THEN NULL ELSE table_type_columns.code_value END, + category_ref = CASE WHEN $8 THEN NULL ELSE table_type_columns.category_ref END, updated_date = now()`, [ tableName, @@ -866,6 +885,9 @@ export class TableManagementService { finalInputType, JSON.stringify(finalDetailSettings), companyCode, + clearEntity, + clearCode, + clearCategory, ] ); diff --git a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx index e2911ed8..a8d58662 100644 --- a/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/tableMngList/page.tsx @@ -453,18 +453,39 @@ export default function TableManagementPage() { [loadColumnTypes, loadConstraints, pageSize, tables], ); - // 입력 타입 변경 + // 입력 타입 변경 - 이전 타입의 설정값 초기화 포함 const handleInputTypeChange = useCallback( (columnName: string, newInputType: string) => { setColumns((prev) => prev.map((col) => { if (col.columnName === columnName) { const inputTypeOption = memoizedInputTypeOptions.find((option) => option.value === newInputType); - return { + const updated: typeof col = { ...col, inputType: newInputType, detailSettings: inputTypeOption?.description || col.detailSettings, }; + + // 엔티티가 아닌 타입으로 변경 시 참조 설정 초기화 + if (newInputType !== "entity") { + updated.referenceTable = undefined; + updated.referenceColumn = undefined; + updated.displayColumn = undefined; + } + + // 코드가 아닌 타입으로 변경 시 코드 설정 초기화 + if (newInputType !== "code") { + updated.codeCategory = undefined; + updated.codeValue = undefined; + updated.hierarchyRole = undefined; + } + + // 카테고리가 아닌 타입으로 변경 시 카테고리 참조 초기화 + if (newInputType !== "category") { + updated.categoryRef = undefined; + } + + return updated; } return col; }), diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index e8b56b36..b3ee38d7 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -135,6 +135,7 @@ export function ScreenGroupTreeView({ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [deletingGroup, setDeletingGroup] = useState(null); const [deleteScreensWithGroup, setDeleteScreensWithGroup] = useState(false); // 화면도 함께 삭제 체크박스 + const [deleteNumberingRules, setDeleteNumberingRules] = useState(false); // 채번 규칙도 함께 삭제 체크박스 const [isDeleting, setIsDeleting] = useState(false); // 삭제 진행 중 상태 const [deleteProgress, setDeleteProgress] = useState({ current: 0, total: 0, message: "" }); // 삭제 진행 상태 @@ -439,7 +440,8 @@ export function ScreenGroupTreeView({ const handleDeleteGroup = (group: ScreenGroup, e?: React.MouseEvent) => { e?.stopPropagation(); setDeletingGroup(group); - setDeleteScreensWithGroup(false); // 기본값: 화면 삭제 안함 + setDeleteScreensWithGroup(false); + setDeleteNumberingRules(false); setIsDeleteDialogOpen(true); }; @@ -572,11 +574,17 @@ export function ScreenGroupTreeView({ // 최종적으로 대상 그룹 삭제 currentStep++; setDeleteProgress({ current: currentStep, total: totalSteps, message: "그룹 삭제 완료 중..." }); - const response = await deleteScreenGroup(deletingGroup.id); + const isRootGroup = !deletingGroup.parent_group_id; + const response = await deleteScreenGroup(deletingGroup.id, { + deleteNumberingRules: isRootGroup && deleteNumberingRules, + }); if (response.success) { + const messages = []; + if (deleteScreensWithGroup) messages.push(`화면 ${totalScreensToDelete}개`); + if (isRootGroup && deleteNumberingRules) messages.push("채번 규칙"); toast.success( - deleteScreensWithGroup - ? `그룹과 화면 ${totalScreensToDelete}개가 삭제되었습니다` + messages.length > 0 + ? `그룹과 ${messages.join(", ")}이(가) 삭제되었습니다` : "그룹이 삭제되었습니다" ); await loadGroupsData(); @@ -593,6 +601,7 @@ export function ScreenGroupTreeView({ setIsDeleteDialogOpen(false); setDeletingGroup(null); setDeleteScreensWithGroup(false); + setDeleteNumberingRules(false); } }; @@ -1479,7 +1488,7 @@ export function ScreenGroupTreeView({

{deleteScreensWithGroup - ? "⚠️ 그룹에 속한 모든 화면, 플로우, 관련 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다." + ? "그룹에 속한 모든 화면, 플로우, 관련 데이터가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다." : "그룹에 속한 화면들은 미분류로 이동됩니다." }

@@ -1520,6 +1529,43 @@ export function ScreenGroupTreeView({ )} + + {/* 최상위 그룹일 때 채번 삭제 경고 */} + {deletingGroup && !deletingGroup.parent_group_id && ( +
+
+
+ +
+

+ 최상위 그룹 삭제 - 채번 규칙 경고 +

+

+ 이 그룹은 최상위 그룹입니다. + 아래 체크박스를 선택하면 해당 회사의 모든 채번 규칙과 채번 파트가 영구적으로 삭제됩니다. + 삭제된 채번 데이터는 복구할 수 없으며, 채번이 필요한 모든 기능이 중단됩니다. +

+
+
+
+ +
+ setDeleteNumberingRules(e.target.checked)} + className="h-4 w-4 rounded border-destructive text-destructive focus:ring-destructive" + /> + +
+
+ )} {/* 로딩 오버레이 */} {isDeleting && ( @@ -1551,7 +1597,7 @@ export function ScreenGroupTreeView({ { - e.preventDefault(); // 자동 닫힘 방지 + e.preventDefault(); confirmDeleteGroup(); }} disabled={isDeleting} diff --git a/frontend/lib/api/screenGroup.ts b/frontend/lib/api/screenGroup.ts index f3883240..7dd91390 100644 --- a/frontend/lib/api/screenGroup.ts +++ b/frontend/lib/api/screenGroup.ts @@ -156,9 +156,15 @@ export async function updateScreenGroup(id: number, data: Partial): } } -export async function deleteScreenGroup(id: number): Promise> { +export async function deleteScreenGroup(id: number, options?: { deleteNumberingRules?: boolean }): Promise> { try { - const response = await apiClient.delete(`/screen-groups/groups/${id}`); + const params = new URLSearchParams(); + if (options?.deleteNumberingRules) { + params.set("deleteNumberingRules", "true"); + } + const queryString = params.toString(); + const url = `/screen-groups/groups/${id}${queryString ? `?${queryString}` : ""}`; + const response = await apiClient.delete(url); return response.data; } catch (error: any) { return { success: false, error: error.message }; diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx index 6c631d83..b53a3def 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx @@ -649,11 +649,12 @@ export const SplitPanelLayout2Component: React.FC { if (selectedLeftItem) { loadRightData(selectedLeftItem); } + loadLeftData(); }, }, }); @@ -664,6 +665,7 @@ export const SplitPanelLayout2Component: React.FC { if (selectedLeftItem) { loadRightData(selectedLeftItem); } + loadLeftData(); }, }, }); window.dispatchEvent(event); console.log("[SplitPanelLayout2] 우측 수정 모달 열기:", editData); }, - [config.rightPanel?.editModalScreenId, config.rightPanel?.addModalScreenId, config.rightPanel?.mainTableForEdit, selectedLeftItem, loadRightData], + [config.rightPanel?.editModalScreenId, config.rightPanel?.addModalScreenId, config.rightPanel?.mainTableForEdit, selectedLeftItem, loadRightData, loadLeftData], ); // 좌측 패널 수정 버튼 클릭 @@ -835,10 +838,11 @@ export const SplitPanelLayout2Component: React.FC d[pkColumn] === selectedId); if (item) { - // 액션 버튼에 모달 화면이 설정되어 있으면 해당 화면 사용 const modalScreenId = btn.modalScreenId || config.rightPanel?.editModalScreenId || config.rightPanel?.addModalScreenId; if (!modalScreenId) { @@ -936,6 +940,7 @@ export const SplitPanelLayout2Component: React.FC o.value === modalCondition)?.label || modalCondition} - ${modalTitle}`} - alreadySelected={conditionalTableData[modalCondition] || []} + alreadySelected={Object.values(conditionalTableData).flat()} uniqueField={tableConfig.saveConfig?.uniqueField} onSelect={handleConditionalAddItems} columnLabels={columnLabels}