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