From de7fa7a71bdd21b7392d7a6a07841aad267d0653 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 15 Jan 2026 14:36:00 +0900 Subject: [PATCH 01/57] =?UTF-8?q?fix:=20=EB=B0=9C=EC=A3=BC/=EC=9E=85?= =?UTF-8?q?=EA=B3=A0=EA=B4=80=EB=A6=AC=20=EA=B7=B8=EB=A3=B9=20=ED=8E=B8?= =?UTF-8?q?=EC=A7=91=20=EC=8B=9C=20=EB=8B=A8=EA=B1=B4=EB=A7=8C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=90=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20EditModal.tsx:=20conditional-container=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=20=EC=8B=9C=20onSave=20=EB=AF=B8=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20ModalRepeaterTableCom?= =?UTF-8?q?ponent.tsx:=20groupedData=20prop=20=EC=9A=B0=EC=84=A0=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/EditModal.tsx | 36 ++++++------------- .../ModalRepeaterTableComponent.tsx | 16 +++++++-- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index b3c94ade..7c722ad6 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -309,17 +309,10 @@ export const EditModal: React.FC = ({ className }) => { // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ•จ์ˆ˜ const loadGroupData = async () => { if (!modalState.tableName || !modalState.groupByColumns || modalState.groupByColumns.length === 0) { - // console.warn("ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ๊ทธ๋ฃนํ•‘ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค."); return; } try { - // console.log("๐Ÿ” ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘:", { - // tableName: modalState.tableName, - // groupByColumns: modalState.groupByColumns, - // editData: modalState.editData, - // }); - // ๊ทธ๋ฃนํ•‘ ์ปฌ๋Ÿผ ๊ฐ’ ์ถ”์ถœ (์˜ˆ: order_no = "ORD-20251124-001") const groupValues: Record = {}; modalState.groupByColumns.forEach((column) => { @@ -329,15 +322,9 @@ export const EditModal: React.FC = ({ className }) => { }); if (Object.keys(groupValues).length === 0) { - // console.warn("๊ทธ๋ฃนํ•‘ ์ปฌ๋Ÿผ ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค:", modalState.groupByColumns); return; } - // console.log("๐Ÿ” ๊ทธ๋ฃน ์กฐํšŒ ์š”์ฒญ:", { - // tableName: modalState.tableName, - // groupValues, - // }); - // ๊ฐ™์€ ๊ทธ๋ฃน์˜ ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ ์กฐํšŒ (entityJoinApi ์‚ฌ์šฉ) const { entityJoinApi } = await import("@/lib/api/entityJoin"); const response = await entityJoinApi.getTableDataWithJoins(modalState.tableName, { @@ -347,23 +334,19 @@ export const EditModal: React.FC = ({ className }) => { enableEntityJoin: true, }); - // console.log("๐Ÿ” ๊ทธ๋ฃน ์กฐํšŒ ์‘๋‹ต:", response); - // entityJoinApi๋Š” ๋ฐฐ์—ด ๋˜๋Š” { data: [] } ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ const dataArray = Array.isArray(response) ? response : response?.data || []; if (dataArray.length > 0) { - // console.log("โœ… ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ฑ๊ณต:", dataArray.length, "๊ฑด"); setGroupData(dataArray); setOriginalGroupData(JSON.parse(JSON.stringify(dataArray))); // Deep copy toast.info(`${dataArray.length}๊ฐœ์˜ ๊ด€๋ จ ํ’ˆ๋ชฉ์„ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค.`); } else { - console.warn("๊ทธ๋ฃน ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค:", response); setGroupData([modalState.editData]); // ๊ธฐ๋ณธ๊ฐ’: ์„ ํƒ๋œ ํ–‰๋งŒ setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]); } } catch (error: any) { - console.error("โŒ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์กฐํšŒ ์˜ค๋ฅ˜:", error); + console.error("๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์กฐํšŒ ์˜ค๋ฅ˜:", error); toast.error("๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); setGroupData([modalState.editData]); // ๊ธฐ๋ณธ๊ฐ’: ์„ ํƒ๋œ ํ–‰๋งŒ setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]); @@ -1043,17 +1026,18 @@ export const EditModal: React.FC = ({ className }) => { const groupedDataProp = groupData.length > 0 ? groupData : undefined; // ๐Ÿ†• UniversalFormModal์ด ์žˆ๋Š”์ง€ ํ™•์ธ (์ž์ฒด ์ €์žฅ ๋กœ์ง ์‚ฌ์šฉ) - // ์ตœ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ ๋˜๋Š” ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€ ํ™”๋ฉด์— universal-form-modal์ด ์žˆ๋Š”์ง€ ํ™•์ธ + // ์ตœ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์— universal-form-modal์ด ์žˆ๋Š”์ง€ ํ™•์ธ + // โš ๏ธ ์ˆ˜์ •: conditional-container๋Š” ์ œ์™ธ (groupData๊ฐ€ ์žˆ์œผ๋ฉด EditModal.handleSave ์‚ฌ์šฉ) const hasUniversalFormModal = screenData.components.some( (c) => { - // ์ตœ์ƒ์œ„์— universal-form-modal์ด ์žˆ๋Š” ๊ฒฝ์šฐ + // ์ตœ์ƒ์œ„์— universal-form-modal์ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ ์ž์ฒด ์ €์žฅ ๋กœ์ง ์‚ฌ์šฉ if (c.componentType === "universal-form-modal") return true; - // ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋‚ด๋ถ€์— universal-form-modal์ด ์žˆ๋Š” ๊ฒฝ์šฐ - // (์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ์œผ๋ฉด ๋‚ด๋ถ€ ํ™”๋ฉด์—์„œ universal-form-modal์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ€์ •) - if (c.componentType === "conditional-container") return true; return false; } ); + + // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด EditModal.handleSave ์‚ฌ์šฉ (์ผ๊ด„ ์ €์žฅ) + const shouldUseEditModalSave = groupData.length > 0 || !hasUniversalFormModal; // ๐Ÿ”‘ ์ฒจ๋ถ€ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ–‰(๋ ˆ์ฝ”๋“œ) ๋‹จ์œ„๋กœ ํŒŒ์ผ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก tableName ์ถ”๊ฐ€ const enrichedFormData = { @@ -1095,9 +1079,9 @@ export const EditModal: React.FC = ({ className }) => { id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} - // ๐Ÿ†• UniversalFormModal์ด ์žˆ์œผ๋ฉด onSave ์ „๋‹ฌ ์•ˆ ํ•จ (์ž์ฒด ์ €์žฅ ๋กœ์ง ์‚ฌ์šฉ) - // ModalRepeaterTable๋งŒ ์žˆ์œผ๋ฉด ๊ธฐ์กด๋Œ€๋กœ onSave ์ „๋‹ฌ (ํ˜ธํ™˜์„ฑ ์œ ์ง€) - onSave={hasUniversalFormModal ? undefined : handleSave} + // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๊ฑฐ๋‚˜ UniversalFormModal์ด ์—†์œผ๋ฉด EditModal.handleSave ์‚ฌ์šฉ + // groupData๊ฐ€ ์žˆ์œผ๋ฉด ์ผ๊ด„ ์ €์žฅ์„ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ EditModal.handleSave ์‚ฌ์šฉ + onSave={shouldUseEditModalSave ? handleSave : undefined} isInModal={true} // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ๋ฅผ ModalRepeaterTable์— ์ „๋‹ฌ groupedData={groupedDataProp} diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 2caf1332..153cebdf 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -180,8 +180,11 @@ export function ModalRepeaterTableComponent({ filterCondition: propFilterCondition, companyCode: propCompanyCode, + // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (EditModal์—์„œ ์ „๋‹ฌ, ๊ฐ™์€ ๊ทธ๋ฃน์˜ ์—ฌ๋Ÿฌ ํ’ˆ๋ชฉ) + groupedData, + ...props -}: ModalRepeaterTableComponentProps) { +}: ModalRepeaterTableComponentProps & { groupedData?: Record[] }) { // โœ… config ๋˜๋Š” component.config ๋˜๋Š” ๊ฐœ๋ณ„ prop ์šฐ์„ ์ˆœ์œ„๋กœ ๋ณ‘ํ•ฉ const componentConfig = { ...config, @@ -208,9 +211,16 @@ export function ModalRepeaterTableComponent({ // ๋ชจ๋‹ฌ ํ•„ํ„ฐ ์„ค์ • const modalFilters = componentConfig?.modalFilters || []; - // โœ… value๋Š” formData[columnName] ์šฐ์„ , ์—†์œผ๋ฉด prop ์‚ฌ์šฉ + // โœ… value๋Š” groupedData ์šฐ์„ , ์—†์œผ๋ฉด formData[columnName], ์—†์œผ๋ฉด prop ์‚ฌ์šฉ const columnName = component?.columnName; - const externalValue = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; + + // ๐Ÿ†• groupedData๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด (EditModal์—์„œ ๊ทธ๋ฃน ์กฐํšŒ ๊ฒฐ๊ณผ) ์šฐ์„  ์‚ฌ์šฉ + const externalValue = (() => { + if (groupedData && groupedData.length > 0) { + return groupedData; + } + return (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; + })(); // ๋นˆ ๊ฐ์ฒด ํŒ๋‹จ ํ•จ์ˆ˜ (์ˆ˜์ • ๋ชจ๋‹ฌ์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋Š” ์œ ์ง€) const isEmptyRow = (item: any): boolean => { From d4b5bdd835244c6e9b43101611ed62ef38787477 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 19 Jan 2026 13:18:17 +0900 Subject: [PATCH 02/57] =?UTF-8?q?feat:=20RepeaterInput=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (columnOrder) - ์กฐํšŒ ์ปฌ๋Ÿผ -> ์ €์žฅ ์ปฌ๋Ÿผ ๋งคํ•‘ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (fieldMappings) - ์ปฌ๋Ÿผ๋ณ„ ๋ผ๋ฒจ, ์ˆœ์„œ, ์ €์žฅ ์—ฌ๋ถ€ ํ†ตํ•ฉ ์„ค์ • UI ๊ตฌํ˜„ - ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ (fieldMappings ์—†์œผ๋ฉด ๊ธฐ์กด ๋กœ์ง ์‚ฌ์šฉ) --- .../components/webtypes/RepeaterInput.tsx | 32 ++- .../webtypes/config/RepeaterConfigPanel.tsx | 196 +++++++++++++++++- .../SubDataLookupPanel.tsx | 16 +- .../repeater-field-group/useSubDataLookup.ts | 12 +- frontend/types/repeater.ts | 10 + 5 files changed, 252 insertions(+), 14 deletions(-) diff --git a/frontend/components/webtypes/RepeaterInput.tsx b/frontend/components/webtypes/RepeaterInput.tsx index 7cd4b279..49751699 100644 --- a/frontend/components/webtypes/RepeaterInput.tsx +++ b/frontend/components/webtypes/RepeaterInput.tsx @@ -309,18 +309,32 @@ export const RepeaterInput: React.FC = ({ _subDataMaxValue: maxValue, }; - // ์„ ํƒ๋œ ํ•˜์œ„ ๋ฐ์ดํ„ฐ์˜ ํ•„๋“œ ๊ฐ’์„ ์ƒ์œ„ item์— ๋ณต์‚ฌ (์„ค์ •๋œ ๊ฒฝ์šฐ) - // ์˜ˆ: warehouse_code, location_code ๋“ฑ - if (subDataLookup.lookup.displayColumns) { - subDataLookup.lookup.displayColumns.forEach((col) => { - if (selectedItem[col] !== undefined) { - // ํ•„๋“œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด ๋ณต์‚ฌ - const fieldDef = fields.find((f) => f.name === col); - if (fieldDef || col.includes("_code") || col.includes("_id")) { - newItems[itemIndex][col] = selectedItem[col]; + // fieldMappings๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ๋งคํ•‘์— ๋”ฐ๋ผ ๊ฐ’ ๋ณต์‚ฌ + if (subDataLookup.lookup.fieldMappings && subDataLookup.lookup.fieldMappings.length > 0) { + subDataLookup.lookup.fieldMappings.forEach((mapping) => { + if (mapping.targetField && mapping.targetField !== "") { + // ๋งคํ•‘๋œ ํƒ€๊ฒŸ ํ•„๋“œ์— ์†Œ์Šค ์ปฌ๋Ÿผ ๊ฐ’ ๋ณต์‚ฌ + const sourceValue = selectedItem[mapping.sourceColumn]; + if (sourceValue !== undefined) { + newItems[itemIndex][mapping.targetField] = sourceValue; } } }); + } else { + // fieldMappings๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋กœ์ง (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) + // ์„ ํƒ๋œ ํ•˜์œ„ ๋ฐ์ดํ„ฐ์˜ ํ•„๋“œ ๊ฐ’์„ ์ƒ์œ„ item์— ๋ณต์‚ฌ (์„ค์ •๋œ ๊ฒฝ์šฐ) + // ์˜ˆ: warehouse_code, location_code ๋“ฑ + if (subDataLookup.lookup.displayColumns) { + subDataLookup.lookup.displayColumns.forEach((col) => { + if (selectedItem[col] !== undefined) { + // ํ•„๋“œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด ๋ณต์‚ฌ + const fieldDef = fields.find((f) => f.name === col); + if (fieldDef || col.includes("_code") || col.includes("_id")) { + newItems[itemIndex][col] = selectedItem[col]; + } + } + }); + } } setItems(newItems); diff --git a/frontend/components/webtypes/config/RepeaterConfigPanel.tsx b/frontend/components/webtypes/config/RepeaterConfigPanel.tsx index 97e20574..857ece17 100644 --- a/frontend/components/webtypes/config/RepeaterConfigPanel.tsx +++ b/frontend/components/webtypes/config/RepeaterConfigPanel.tsx @@ -319,6 +319,103 @@ export const RepeaterConfigPanel: React.FC = ({ }); }; + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๊ฐ€์ ธ์˜ค๊ธฐ (columnOrder๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด displayColumns ์ˆœ์„œ) + const getOrderedDisplayColumns = (): string[] => { + const displayColumns = config.subDataLookup?.lookup?.displayColumns || []; + const columnOrder = config.subDataLookup?.lookup?.columnOrder; + + if (columnOrder && columnOrder.length > 0) { + // columnOrder์— ์žˆ๋Š” ์ปฌ๋Ÿผ๋งŒ, ์ˆœ์„œ๋Œ€๋กœ ๋ฐ˜ํ™˜ (displayColumns์— ์žˆ๋Š” ๊ฒƒ๋งŒ) + const orderedCols = columnOrder.filter(col => displayColumns.includes(col)); + // columnOrder์— ์—†์ง€๋งŒ displayColumns์— ์žˆ๋Š” ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + const remainingCols = displayColumns.filter(col => !columnOrder.includes(col)); + return [...orderedCols, ...remainingCols]; + } + return displayColumns; + }; + + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์œ„๋กœ) + const handleDisplayColumnMoveUp = (columnName: string) => { + const orderedColumns = getOrderedDisplayColumns(); + const index = orderedColumns.indexOf(columnName); + if (index <= 0) return; + + const newOrder = [...orderedColumns]; + [newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]]; + handleSubDataLookupChange("lookup.columnOrder", newOrder); + }; + + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์•„๋ž˜๋กœ) + const handleDisplayColumnMoveDown = (columnName: string) => { + const orderedColumns = getOrderedDisplayColumns(); + const index = orderedColumns.indexOf(columnName); + if (index < 0 || index >= orderedColumns.length - 1) return; + + const newOrder = [...orderedColumns]; + [newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]]; + handleSubDataLookupChange("lookup.columnOrder", newOrder); + }; + + // ํ‘œ์‹œ ์ปฌ๋Ÿผ ํ† ๊ธ€ ์‹œ columnOrder๋„ ์—…๋ฐ์ดํŠธ + const handleDisplayColumnToggleWithOrder = (columnName: string, checked: boolean) => { + const currentColumns = config.subDataLookup?.lookup?.displayColumns || []; + const currentOrder = config.subDataLookup?.lookup?.columnOrder || []; + const currentMappings = config.subDataLookup?.lookup?.fieldMappings || []; + + let newColumns: string[]; + let newOrder: string[]; + let newMappings: { sourceColumn: string; targetField: string }[]; + + if (checked) { + newColumns = [...currentColumns, columnName]; + newOrder = [...currentOrder, columnName]; + // ๊ธฐ๋ณธ ๋งคํ•‘ ์ถ”๊ฐ€: ๋™์ผํ•œ ์ปฌ๋Ÿผ๋ช…์ด targetTable์— ์žˆ์œผ๋ฉด ์ž๋™ ๋งคํ•‘, ์—†์œผ๋ฉด ๋นˆ ๋ฌธ์ž์—ด + const targetColumn = tableColumns.find((c) => c.columnName === columnName); + newMappings = [...currentMappings, { sourceColumn: columnName, targetField: targetColumn ? columnName : "" }]; + } else { + newColumns = currentColumns.filter((c) => c !== columnName); + newOrder = currentOrder.filter((c) => c !== columnName); + newMappings = currentMappings.filter((m) => m.sourceColumn !== columnName); + } + + // displayColumns, columnOrder, fieldMappings ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ + const newConfig = { ...config.subDataLookup } as SubDataLookupConfig; + if (!newConfig.lookup) { + newConfig.lookup = { tableName: "", linkColumn: "", displayColumns: [] }; + } + newConfig.lookup.displayColumns = newColumns; + newConfig.lookup.columnOrder = newOrder; + newConfig.lookup.fieldMappings = newMappings; + + onChange({ + ...config, + subDataLookup: newConfig, + }); + }; + + // ํ•„๋“œ ๋งคํ•‘ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleFieldMappingChange = (sourceColumn: string, targetField: string) => { + const currentMappings = config.subDataLookup?.lookup?.fieldMappings || []; + const existingIndex = currentMappings.findIndex((m) => m.sourceColumn === sourceColumn); + + let newMappings: { sourceColumn: string; targetField: string }[]; + if (existingIndex >= 0) { + newMappings = [...currentMappings]; + newMappings[existingIndex] = { sourceColumn, targetField }; + } else { + newMappings = [...currentMappings, { sourceColumn, targetField }]; + } + + handleSubDataLookupChange("lookup.fieldMappings", newMappings); + }; + + // ํŠน์ • ์ปฌ๋Ÿผ์˜ ํ˜„์žฌ ๋งคํ•‘๋œ ํƒ€๊ฒŸ ํ•„๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ + const getFieldMapping = (sourceColumn: string): string => { + const mappings = config.subDataLookup?.lookup?.fieldMappings || []; + const mapping = mappings.find((m) => m.sourceColumn === sourceColumn); + return mapping?.targetField || ""; + }; + return (
{/* ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ ํƒ */} @@ -588,7 +685,7 @@ export const RepeaterConfigPanel: React.FC = ({ handleDisplayColumnToggle(col.columnName, checked as boolean)} + onCheckedChange={(checked) => handleDisplayColumnToggleWithOrder(col.columnName, checked as boolean)} />
)} + {/* ์ปฌ๋Ÿผ ์„ค์ • (์ˆœ์„œ + ๋ผ๋ฒจ + ์ €์žฅ ์ปฌ๋Ÿผ) */} + {(config.subDataLookup?.lookup?.displayColumns?.length || 0) > 0 && ( +
+ +

์ˆœ์„œ, ๋ผ๋ฒจ, ์ €์žฅ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•˜์„ธ์š”

+
+ {getOrderedDisplayColumns().map((colName, index) => { + const col = subDataTableColumns.find((c) => c.columnName === colName); + const currentLabel = config.subDataLookup?.lookup?.columnLabels?.[colName] || ""; + const currentMapping = getFieldMapping(colName); + const orderedColumns = getOrderedDisplayColumns(); + const isFirst = index === 0; + const isLast = index === orderedColumns.length - 1; + + return ( +
+ {/* ์ƒ๋‹จ: ์ˆœ์„œ ๋ฒ„ํŠผ + ๋ฒˆํ˜ธ + ์ปฌ๋Ÿผ๋ช… */} +
+ {/* ์ˆœ์„œ ๋ณ€๊ฒฝ ๋ฒ„ํŠผ */} +
+ + +
+ + {/* ์ˆœ์„œ ๋ฒˆํ˜ธ */} + {index + 1} + + {/* ์ปฌ๋Ÿผ๋ช… */} +
+ {col?.columnLabel || colName} + ({colName}) +
+
+ + {/* ์ค‘๋‹จ: ๋ผ๋ฒจ ์ž…๋ ฅ */} +
+ ํ‘œ์‹œ ๋ผ๋ฒจ: + handleColumnLabelChange(colName, e.target.value)} + placeholder={col?.columnLabel || colName} + className="h-6 flex-1 text-xs" + /> +
+ + {/* ํ•˜๋‹จ: ์ €์žฅ ์ปฌ๋Ÿผ ์„ ํƒ */} +
+ ์ €์žฅ ์ปฌ๋Ÿผ: + +
+
+ ); + })} +
+ {config.targetTable && ( +

+ * ์ €์žฅ ๋Œ€์ƒ: {config.targetTable} +

+ )} +
+ )} + {/* ์„ ํƒ ์„ค์ • */} {(config.subDataLookup?.lookup?.displayColumns?.length || 0) > 0 && (
diff --git a/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx b/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx index 5baf0fe0..51be5a64 100644 --- a/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx +++ b/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx @@ -78,8 +78,20 @@ export const SubDataLookupPanel: React.FC = ({ return config.lookup.columnLabels?.[columnName] || columnName; }; - // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๋ชฉ๋ก - const displayColumns = config.lookup.displayColumns || []; + // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๋ชฉ๋ก (columnOrder๊ฐ€ ์žˆ์œผ๋ฉด ์ˆœ์„œ ์ ์šฉ) + const displayColumns = useMemo(() => { + const columns = config.lookup.displayColumns || []; + const columnOrder = config.lookup.columnOrder; + + if (columnOrder && columnOrder.length > 0) { + // columnOrder ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ (displayColumns์— ์žˆ๋Š” ๊ฒƒ๋งŒ) + const orderedCols = columnOrder.filter(col => columns.includes(col)); + // columnOrder์— ์—†์ง€๋งŒ displayColumns์— ์žˆ๋Š” ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + const remainingCols = columns.filter(col => !columnOrder.includes(col)); + return [...orderedCols, ...remainingCols]; + } + return columns; + }, [config.lookup.displayColumns, config.lookup.columnOrder]); // ์š”์•ฝ ์ •๋ณด ํ‘œ์‹œ์šฉ ์„ ํƒ ์ƒํƒœ const summaryText = useMemo(() => { diff --git a/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts b/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts index b2c44e3d..2753dd16 100644 --- a/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts +++ b/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts @@ -197,10 +197,18 @@ export function useSubDataLookup(props: UseSubDataLookupProps): UseSubDataLookup return "์„ ํƒ ์•ˆ๋จ"; } - const { displayColumns, columnLabels } = config.lookup; + const { displayColumns, columnLabels, columnOrder } = config.lookup; const parts: string[] = []; - displayColumns.forEach((col) => { + // columnOrder๊ฐ€ ์žˆ์œผ๋ฉด ์ˆœ์„œ ์ ์šฉ, ์—†์œผ๋ฉด displayColumns ์ˆœ์„œ + let orderedColumns = displayColumns; + if (columnOrder && columnOrder.length > 0) { + const orderedCols = columnOrder.filter(col => displayColumns.includes(col)); + const remainingCols = displayColumns.filter(col => !columnOrder.includes(col)); + orderedColumns = [...orderedCols, ...remainingCols]; + } + + orderedColumns.forEach((col) => { const value = selectedItem[col]; if (value !== undefined && value !== null && value !== "") { const label = columnLabels?.[col] || col; diff --git a/frontend/types/repeater.ts b/frontend/types/repeater.ts index 2362210b..bbdc8727 100644 --- a/frontend/types/repeater.ts +++ b/frontend/types/repeater.ts @@ -113,6 +113,14 @@ export type RepeaterData = RepeaterItemData[]; // ํ’ˆ๋ชฉ ์„ ํƒ ์‹œ ์žฌ๊ณ /๋‹จ๊ฐ€ ๋“ฑ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  ์„ ํƒํ•˜๋Š” ๊ธฐ๋Šฅ // ============================================================ +/** + * ์„ ํƒ ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋งคํ•‘ ์„ค์ • + */ +export interface SubDataFieldMapping { + sourceColumn: string; // ์กฐํšŒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (์˜ˆ: lot_number) + targetField: string; // ์ €์žฅ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (์˜ˆ: lot_number) ๋˜๋Š” "" (์„ ํƒ์•ˆํ•จ) +} + /** * ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ…Œ์ด๋ธ” ์„ค์ • */ @@ -121,6 +129,8 @@ export interface SubDataLookupSettings { linkColumn: string; // ์ƒ์œ„ ๋ฐ์ดํ„ฐ์™€ ์—ฐ๊ฒฐํ•  ์ปฌ๋Ÿผ (์˜ˆ: item_code) displayColumns: string[]; // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ๋“ค (์˜ˆ: ["warehouse_code", "location_code", "quantity"]) columnLabels?: Record; // ์ปฌ๋Ÿผ ๋ผ๋ฒจ (์˜ˆ: { warehouse_code: "์ฐฝ๊ณ " }) + columnOrder?: string[]; // ์ปฌ๋Ÿผ ํ‘œ์‹œ ์ˆœ์„œ (์—†์œผ๋ฉด displayColumns ์ˆœ์„œ ์‚ฌ์šฉ) + fieldMappings?: SubDataFieldMapping[]; // ์„ ํƒ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋งคํ•‘ (์กฐํšŒ ์ปฌ๋Ÿผ โ†’ ์ €์žฅ ์ปฌ๋Ÿผ) additionalFilters?: Record; // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด } From b62a0b7e3b1572eb8485425968ace76afd7b3648 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 19 Jan 2026 18:48:18 +0900 Subject: [PATCH 03/57] =?UTF-8?q?fix:=20=EB=B6=84=ED=95=A0=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=ED=99=94=EB=A9=B4=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SplitPanelLayoutComponent.tsx | 470 ++++++++++++++++-- 1 file changed, 439 insertions(+), 31 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 50f7c41b..869d2c3c 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -33,6 +33,7 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; @@ -171,6 +172,12 @@ export const SplitPanelLayoutComponent: React.FC const [rightSearchQuery, setRightSearchQuery] = useState(""); const [isLoadingLeft, setIsLoadingLeft] = useState(false); const [isLoadingRight, setIsLoadingRight] = useState(false); + + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๊ด€๋ จ ์ƒํƒœ + const [activeTabIndex, setActiveTabIndex] = useState(0); // 0 = ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„), 1+ = ์ถ”๊ฐ€ ํƒญ + const [tabsData, setTabsData] = useState>({}); // ํƒญ๋ณ„ ๋ฐ์ดํ„ฐ ์บ์‹œ + const [tabsLoading, setTabsLoading] = useState>({}); // ํƒญ๋ณ„ ๋กœ๋”ฉ ์ƒํƒœ + const [rightTableColumns, setRightTableColumns] = useState([]); // ์šฐ์ธก ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด const [expandedItems, setExpandedItems] = useState>(new Set()); // ํŽผ์ณ์ง„ ํ•ญ๋ชฉ๋“ค const [leftColumnLabels, setLeftColumnLabels] = useState>({}); // ์ขŒ์ธก ์ปฌ๋Ÿผ ๋ผ๋ฒจ @@ -1001,12 +1008,137 @@ export const SplitPanelLayoutComponent: React.FC ], ); + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ํ•จ์ˆ˜ + const loadTabData = useCallback( + async (tabIndex: number, leftItem: any) => { + const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1]; + if (!tabConfig || !leftItem || isDesignMode) return; + + const tabTableName = tabConfig.tableName; + if (!tabTableName) return; + + setTabsLoading((prev) => ({ ...prev, [tabIndex]: true })); + try { + // ์กฐ์ธ ํ‚ค ํ™•์ธ + const keys = tabConfig.relation?.keys; + const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn; + const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn; + + let resultData: any[] = []; + + if (leftColumn && rightColumn) { + // ์กฐ์ธ ์กฐ๊ฑด์ด ์žˆ๋Š” ๊ฒฝ์šฐ + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const searchConditions: Record = {}; + + if (keys && keys.length > 0) { + // ๋ณตํ•ฉํ‚ค + keys.forEach((key) => { + if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { + searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + } + }); + } else { + // ๋‹จ์ผํ‚ค + const leftValue = leftItem[leftColumn]; + if (leftValue !== undefined) { + searchConditions[rightColumn] = leftValue; + } + } + + console.log(`๐Ÿ”— [์ถ”๊ฐ€ํƒญ ${tabIndex}] ์กฐํšŒ ์กฐ๊ฑด:`, searchConditions); + + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + search: searchConditions, + enableEntityJoin: true, + size: 1000, + }); + + resultData = result.data || []; + } else { + // ์กฐ์ธ ์กฐ๊ฑด์ด ์—†๋Š” ๊ฒฝ์šฐ: ์ „์ฒด ๋ฐ์ดํ„ฐ ์กฐํšŒ (๋…๋ฆฝ ํƒญ) + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + enableEntityJoin: true, + size: 1000, + }); + resultData = result.data || []; + } + + // ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ ์šฉ + const dataFilter = tabConfig.dataFilter; + if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + resultData = resultData.filter((item: any) => { + return dataFilter.conditions.every((cond: any) => { + const value = item[cond.column]; + const condValue = cond.value; + switch (cond.operator) { + case "equals": + return value === condValue; + case "notEquals": + return value !== condValue; + case "contains": + return String(value).includes(String(condValue)); + default: + return true; + } + }); + }); + } + + // ์ค‘๋ณต ์ œ๊ฑฐ ์ ์šฉ + const deduplication = tabConfig.deduplication; + if (deduplication?.enabled && deduplication.groupByColumn) { + const groupedMap = new Map(); + resultData.forEach((item) => { + const key = String(item[deduplication.groupByColumn] || ""); + const existing = groupedMap.get(key); + if (!existing) { + groupedMap.set(key, item); + } else { + // keepStrategy์— ๋”ฐ๋ผ ์œ ์ง€ํ•  ํ•ญ๋ชฉ ๊ฒฐ์ • + const sortCol = deduplication.sortColumn || "start_date"; + const existingVal = existing[sortCol]; + const newVal = item[sortCol]; + if (deduplication.keepStrategy === "latest" && newVal > existingVal) { + groupedMap.set(key, item); + } else if (deduplication.keepStrategy === "earliest" && newVal < existingVal) { + groupedMap.set(key, item); + } + } + }); + resultData = Array.from(groupedMap.values()); + } + + console.log(`๐Ÿ”— [์ถ”๊ฐ€ํƒญ ${tabIndex}] ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ:`, resultData.length); + setTabsData((prev) => ({ ...prev, [tabIndex]: resultData })); + } catch (error) { + console.error(`์ถ”๊ฐ€ํƒญ ${tabIndex} ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:`, error); + toast({ + title: "๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ", + description: `ํƒญ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + variant: "destructive", + }); + } finally { + setTabsLoading((prev) => ({ ...prev, [tabIndex]: false })); + } + }, + [componentConfig.rightPanel?.additionalTabs, isDesignMode, toast], + ); + // ์ขŒ์ธก ํ•ญ๋ชฉ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ const handleLeftItemSelect = useCallback( (item: any) => { setSelectedLeftItem(item); setExpandedRightItems(new Set()); // ์ขŒ์ธก ํ•ญ๋ชฉ ๋ณ€๊ฒฝ ์‹œ ์šฐ์ธก ํ™•์žฅ ์ดˆ๊ธฐํ™” - loadRightData(item); + setTabsData({}); // ๐Ÿ†• ๋ชจ๋“  ํƒญ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (activeTabIndex === 0) { + loadRightData(item); + } else { + loadTabData(activeTabIndex, item); + } // ๐Ÿ†• modalDataStore์— ์„ ํƒ๋œ ์ขŒ์ธก ํ•ญ๋ชฉ ์ €์žฅ (๋‹จ์ผ ์„ ํƒ) const leftTableName = componentConfig.leftPanel?.tableName; @@ -1017,7 +1149,30 @@ export const SplitPanelLayoutComponent: React.FC }); } }, - [loadRightData, componentConfig.leftPanel?.tableName, isDesignMode], + [loadRightData, loadTabData, activeTabIndex, componentConfig.leftPanel?.tableName, isDesignMode], + ); + + // ๐Ÿ†• ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleTabChange = useCallback( + (newTabIndex: number) => { + setActiveTabIndex(newTabIndex); + + // ์„ ํƒ๋œ ์ขŒ์ธก ํ•ญ๋ชฉ์ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ํƒญ์˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (selectedLeftItem) { + if (newTabIndex === 0) { + // ๊ธฐ๋ณธ ํƒญ: ์šฐ์ธก ํŒจ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋กœ๋“œ + if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) { + loadRightData(selectedLeftItem); + } + } else { + // ์ถ”๊ฐ€ ํƒญ: ํ•ด๋‹น ํƒญ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋กœ๋“œ + if (!tabsData[newTabIndex]) { + loadTabData(newTabIndex, selectedLeftItem); + } + } + } + }, + [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData], ); // ์šฐ์ธก ํ•ญ๋ชฉ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ @@ -2534,6 +2689,34 @@ export const SplitPanelLayoutComponent: React.FC className="flex flex-shrink-0 flex-col" > + {/* ๐Ÿ†• ํƒญ ๋ฐ” (์ถ”๊ฐ€ ํƒญ์ด ์žˆ์„ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 && ( +
+ handleTabChange(Number(value))} + className="w-full" + > + + + {componentConfig.rightPanel?.title || "๊ธฐ๋ณธ"} + + {componentConfig.rightPanel?.additionalTabs?.map((tab, index) => ( + + {tab.label || `ํƒญ ${index + 1}`} + + ))} + + +
+ )} >
- {componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„"} + {activeTabIndex === 0 + ? componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„" + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.title || + componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.label || + "์šฐ์ธก ํŒจ๋„"} {!isDesignMode && (
- {componentConfig.rightPanel?.showAdd && ( - - )} + {/* ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + {activeTabIndex === 0 + ? componentConfig.rightPanel?.showAdd && ( + + ) + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.showAdd && ( + + )} {/* ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •/์‚ญ์ œ๋Š” ๊ฐ ์นด๋“œ์—์„œ ์ฒ˜๋ฆฌ */}
)} @@ -2575,20 +2770,231 @@ export const SplitPanelLayoutComponent: React.FC
)} - {/* ์šฐ์ธก ๋ฐ์ดํ„ฐ */} - {isLoadingRight ? ( - // ๋กœ๋”ฉ ์ค‘ -
-
- -

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

-
-
- ) : rightData ? ( - // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ - Array.isArray(rightData) ? ( - // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ - (() => { + {/* ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง */} + {activeTabIndex > 0 ? ( + // ์ถ”๊ฐ€ ํƒญ ์ปจํ…์ธ  + (() => { + const currentTabConfig = componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]; + const currentTabData = tabsData[activeTabIndex] || []; + const isTabLoading = tabsLoading[activeTabIndex]; + + if (isTabLoading) { + return ( +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ); + } + + if (!selectedLeftItem) { + return ( +
+

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

+
+ ); + } + + if (currentTabData.length === 0) { + return ( +
+

๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+
+ ); + } + + // ํƒญ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง (๋ชฉ๋ก/ํ…Œ์ด๋ธ” ๋ชจ๋“œ) + const isTableMode = currentTabConfig?.displayMode === "table"; + + if (isTableMode) { + // ํ…Œ์ด๋ธ” ๋ชจ๋“œ + const displayColumns = currentTabConfig?.columns || []; + const columnsToShow = + displayColumns.length > 0 + ? displayColumns.map((col) => ({ + ...col, + label: col.label || col.name, + })) + : Object.keys(currentTabData[0] || {}) + .filter(shouldShowField) + .slice(0, 8) + .map((key) => ({ name: key, label: key })); + + return ( +
+ + + + {columnsToShow.map((col: any) => ( + + ))} + {(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && ( + + )} + + + + {currentTabData.map((item: any, idx: number) => ( + + {columnsToShow.map((col: any) => ( + + ))} + {(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && ( + + )} + + ))} + +
+ {col.label} + ์ž‘์—…
+ {formatCellValue(col.name, item[col.name], {}, col.format)} + +
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} +
+
+
+ ); + } else { + // ๋ชฉ๋ก (์นด๋“œ) ๋ชจ๋“œ + const displayColumns = currentTabConfig?.columns || []; + const summaryCount = currentTabConfig?.summaryColumnCount ?? 3; + const showLabel = currentTabConfig?.summaryShowLabel ?? true; + + return ( +
+ {currentTabData.map((item: any, idx: number) => { + const itemId = item.id || idx; + const isExpanded = expandedRightItems.has(itemId); + + // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๊ฒฐ์ • + const columnsToShow = + displayColumns.length > 0 + ? displayColumns + : Object.keys(item) + .filter(shouldShowField) + .slice(0, 8) + .map((key) => ({ name: key, label: key })); + + const summaryColumns = columnsToShow.slice(0, summaryCount); + const detailColumns = columnsToShow.slice(summaryCount); + + return ( +
+
toggleRightItemExpansion(itemId)} + > +
+
+ {summaryColumns.map((col: any) => ( +
+ {showLabel && ( + {col.label}: + )} + + {formatCellValue(col.name, item[col.name], {}, col.format)} + +
+ ))} +
+
+
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} + {detailColumns.length > 0 && + (isExpanded ? ( + + ) : ( + + ))} +
+
+ {isExpanded && detailColumns.length > 0 && ( +
+
+ {detailColumns.map((col: any) => ( +
+ {col.label}: + {formatCellValue(col.name, item[col.name], {}, col.format)} +
+ ))} +
+
+ )} +
+ ); + })} +
+ ); + } + })() + ) : ( + /* ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„) ๋ฐ์ดํ„ฐ */ + <> + {isLoadingRight ? ( + // ๋กœ๋”ฉ ์ค‘ +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ) : rightData ? ( + // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ + Array.isArray(rightData) ? ( + // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ + (() => { // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง const filteredData = rightSearchQuery ? rightData.filter((item) => { @@ -3018,14 +3424,16 @@ export const SplitPanelLayoutComponent: React.FC
- ) : ( - // ์„ ํƒ ์—†์Œ -
-
-

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

-

์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

-
-
+ ) : ( + // ์„ ํƒ ์—†์Œ +
+
+

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

+

์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

+
+
+ )} + )} From 585febfb52a310031725444c74beafb0cee5f25f Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 19 Jan 2026 18:58:23 +0900 Subject: [PATCH 04/57] =?UTF-8?q?make:=20RepeaterFieldGroup=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20-=20=ED=95=98=EC=9C=84=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20-=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=A0=95=EC=9D=98=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20subDataSource=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=ED=95=84=EB=93=9C=EB=B3=84=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80(isHidden)=20=EC=98=B5=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EA=B8=B0=EC=A1=B4=20fieldMappings=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=20=EC=A0=9C=EA=B1=B0,=20=ED=95=84=EB=93=9C=EB=B3=84?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=5FrepeaterFieldsConfig=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EC=84=A4=EC=A0=95=20=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?:=20"=EC=9D=B4=20=ED=95=84=EB=93=9C=EB=93=A4=EC=9D=98=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EC=A1=B0=ED=9A=8C=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B0=92=20=EA=B0=80=EC=A0=B8=EC=99=80?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=B4=EC=A4=98"=EB=9D=BC=EB=8A=94=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=97=AD=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/webtypes/RepeaterInput.tsx | 18 +- .../webtypes/config/RepeaterConfigPanel.tsx | 185 ++++++++++-------- .../RepeaterFieldGroupRenderer.tsx | 18 ++ frontend/lib/utils/buttonActions.ts | 38 +++- frontend/types/repeater.ts | 14 +- 5 files changed, 182 insertions(+), 91 deletions(-) diff --git a/frontend/components/webtypes/RepeaterInput.tsx b/frontend/components/webtypes/RepeaterInput.tsx index 49751699..050b386b 100644 --- a/frontend/components/webtypes/RepeaterInput.tsx +++ b/frontend/components/webtypes/RepeaterInput.tsx @@ -907,6 +907,10 @@ export const RepeaterInput: React.FC = ({ const renderGridLayout = () => { // ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ค์ •์ด ์žˆ์œผ๋ฉด ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ const linkColumn = subDataLookup?.lookup?.linkColumn; + + // hidden์ด ์•„๋‹Œ ํ•„๋“œ๋งŒ ํ‘œ์‹œ + // isHidden์ด true์ด๊ฑฐ๋‚˜ displayMode๊ฐ€ hidden์ธ ํ•„๋“œ๋Š” ์ œ์™ธ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) + const visibleFields = fields.filter((f) => !f.isHidden && f.displayMode !== "hidden"); return (
@@ -919,7 +923,7 @@ export const RepeaterInput: React.FC = ({ {allowReorder && ( )} - {fields.map((field) => ( + {visibleFields.map((field) => ( {field.label} {field.required && *} @@ -958,8 +962,8 @@ export const RepeaterInput: React.FC = ({ )} - {/* ํ•„๋“œ๋“ค */} - {fields.map((field) => ( + {/* ํ•„๋“œ๋“ค (hidden ์ œ์™ธ) */} + {visibleFields.map((field) => ( {renderField(field, itemIndex, item[field.name])} @@ -987,7 +991,7 @@ export const RepeaterInput: React.FC = ({ @@ -1016,6 +1020,10 @@ export const RepeaterInput: React.FC = ({ const renderCardLayout = () => { // ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์„ค์ •์ด ์žˆ์œผ๋ฉด ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ const linkColumn = subDataLookup?.lookup?.linkColumn; + + // hidden์ด ์•„๋‹Œ ํ•„๋“œ๋งŒ ํ‘œ์‹œ + // isHidden์ด true์ด๊ฑฐ๋‚˜ displayMode๊ฐ€ hidden์ธ ํ•„๋“œ๋Š” ์ œ์™ธ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€) + const visibleFields = fields.filter((f) => !f.isHidden && f.displayMode !== "hidden"); return ( <> @@ -1084,7 +1092,7 @@ export const RepeaterInput: React.FC = ({ {!isCollapsed && (
- {fields.map((field) => ( + {visibleFields.map((field) => (
); })}
- {config.targetTable && ( -

- * ์ €์žฅ ๋Œ€์ƒ: {config.targetTable} -

- )} +

+ * ์ €์žฅ ์„ค์ •์€ ํ•„๋“œ ์ •์˜์—์„œ "ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ"๋กœ ์„ค์ •ํ•˜์„ธ์š” +

)} @@ -1545,35 +1491,106 @@ export const RepeaterConfigPanel: React.FC = ({ {/* ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ด ์•„๋‹ ๋•Œ๋งŒ ํ‘œ์‹œ ๋ชจ๋“œ ์„ ํƒ */} {field.type !== "category" && ( -
-
- - -
+
+
+
+ + +
-
-
- updateField(index, { required: checked as boolean })} - /> - +
+
+ updateField(index, { required: checked as boolean })} + /> + +
+ + {/* ์ˆจ๊น€ ์ฒดํฌ๋ฐ•์Šค */} +
+ updateField(index, { isHidden: checked as boolean })} + /> + +
+ + {/* ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ */} + {config.subDataLookup?.enabled && ( +
+
+ { + updateField(index, { + subDataSource: { + enabled: checked as boolean, + sourceColumn: field.subDataSource?.sourceColumn || "", + }, + }); + }} + /> + +
+ + {field.subDataSource?.enabled && ( +
+ + +

+ ์žฌ๊ณ  ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ์ด ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค +

+
+ )} +
+ )}
)} diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index 2aefb047..63e1cbb9 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -287,12 +287,18 @@ const RepeaterFieldGroupComponent: React.FC = (props) => if (onChange && items.length > 0) { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // ๐Ÿ†• subDataSource ์„ค์ •์ด ์žˆ๋Š” ํ•„๋“œ ๋ชฉ๋ก (ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์—ฐ๋™) + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: targetTable, _originalItemIds: itemIds, // ๐Ÿ†• ์›๋ณธ ID ๋ชฉ๋ก๋„ ํ•จ๊ป˜ ์ „๋‹ฌ _existingRecord: !!item.id, // ๐Ÿ†• ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ ํ”Œ๋ž˜๊ทธ (id๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ) _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก + _repeaterFieldsConfig: fieldsConfig, // ๐Ÿ†• ํ•„๋“œ ์„ค์ • (subDataSource ๋“ฑ) })); onChange(dataWithMeta); } @@ -393,11 +399,17 @@ const RepeaterFieldGroupComponent: React.FC = (props) => if (items.length > 0) { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // ๐Ÿ†• subDataSource ์„ค์ •์ด ์žˆ๋Š” ํ•„๋“œ ๋ชฉ๋ก + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: effectiveTargetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก + _repeaterFieldsConfig: fieldsConfig, // ๐Ÿ†• ํ•„๋“œ ์„ค์ • (subDataSource ๋“ฑ) })); onChange(dataWithMeta); } else { @@ -681,6 +693,11 @@ const RepeaterFieldGroupComponent: React.FC = (props) => (newValue: any[]) => { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // ๐Ÿ†• subDataSource ์„ค์ •์ด ์žˆ๋Š” ํ•„๋“œ ๋ชฉ๋ก + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); // ๐Ÿ†• ๋ชจ๋“  ํ•ญ๋ชฉ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ let valueWithMeta = newValue.map((item: any) => ({ @@ -688,6 +705,7 @@ const RepeaterFieldGroupComponent: React.FC = (props) => _targetTable: effectiveTargetTable || targetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก + _repeaterFieldsConfig: fieldsConfig, // ๐Ÿ†• ํ•„๋“œ ์„ค์ • (subDataSource ๋“ฑ) })); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์ธ ๊ฒฝ์šฐ, FK ๊ฐ’ ์ถ”๊ฐ€ diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index debf58b2..b0ac2ba9 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -803,7 +803,7 @@ export class ButtonActionExecutor { for (const item of parsedData) { // ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ๊ฑฐ - const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, ...itemData } = item; + const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, _subDataSelection, _subDataMaxValue, ...itemData } = item; // ๐Ÿ”ง ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ๋งŒ ์ถ”์ถœ (RepeaterFieldGroup ์„ค์ • ๊ธฐ๋ฐ˜) const itemOnlyData: Record = {}; @@ -812,6 +812,42 @@ export class ButtonActionExecutor { itemOnlyData[field] = itemData[field]; } }); + + // ๐Ÿ†• ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์„ ํƒ์—์„œ ๊ฐ’ ์ถ”์ถœ (subDataSource ์„ค์ • ๊ธฐ๋ฐ˜) + // ํ•„๋“œ ์ •์˜์—์„œ subDataSource.enabled๊ฐ€ true์ด๊ณ  sourceColumn์ด ์„ค์ •๋œ ํ•„๋“œ๋งŒ ์ฒ˜๋ฆฌ + if (_subDataSelection && typeof _subDataSelection === 'object') { + // _repeaterFieldsConfig์—์„œ subDataSource ์„ค์ • ํ™•์ธ + const fieldsConfig = item._repeaterFieldsConfig as Array<{ + name: string; + subDataSource?: { enabled: boolean; sourceColumn: string }; + }> | undefined; + + if (fieldsConfig && Array.isArray(fieldsConfig)) { + fieldsConfig.forEach((fieldConfig) => { + if (fieldConfig.subDataSource?.enabled && fieldConfig.subDataSource?.sourceColumn) { + const targetField = fieldConfig.name; // ํ•„๋“œ๋ช… = ์ €์žฅํ•  ์ปฌ๋Ÿผ๋ช… + const sourceColumn = fieldConfig.subDataSource.sourceColumn; + const sourceValue = _subDataSelection[sourceColumn]; + + if (sourceValue !== undefined && sourceValue !== null) { + itemOnlyData[targetField] = sourceValue; + console.log(`๐Ÿ“‹ [handleSave] ํ•˜์œ„ ๋ฐ์ดํ„ฐ ๊ฐ’ ๋งคํ•‘: ${sourceColumn} โ†’ ${targetField} = ${sourceValue}`); + } + } + }); + } else { + // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: fieldsConfig๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ + Object.keys(_subDataSelection).forEach((subDataKey) => { + if (itemOnlyData[subDataKey] === undefined || itemOnlyData[subDataKey] === null || itemOnlyData[subDataKey] === '') { + const subDataValue = _subDataSelection[subDataKey]; + if (subDataValue !== undefined && subDataValue !== null) { + itemOnlyData[subDataKey] = subDataValue; + console.log(`๐Ÿ“‹ [handleSave] ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์„ ํƒ ๊ฐ’ ์ถ”๊ฐ€ (๋ ˆ๊ฑฐ์‹œ): ${subDataKey} = ${subDataValue}`); + } + } + }); + } + } // ๐Ÿ”ง ๋งˆ์Šคํ„ฐ ์ •๋ณด + ํ’ˆ๋ชฉ ๊ณ ์œ  ์ •๋ณด ๋ณ‘ํ•ฉ // masterFields: ์ƒ๋‹จ ํผ์—์„œ ์ˆ˜์ •ํ•œ ์ตœ์‹  ๋งˆ์Šคํ„ฐ ์ •๋ณด diff --git a/frontend/types/repeater.ts b/frontend/types/repeater.ts index bbdc8727..c7f0fa98 100644 --- a/frontend/types/repeater.ts +++ b/frontend/types/repeater.ts @@ -43,9 +43,19 @@ export interface CalculationFormula { * ํ•„๋“œ ํ‘œ์‹œ ๋ชจ๋“œ * - input: ์ž…๋ ฅ ํ•„๋“œ๋กœ ํ‘œ์‹œ (ํŽธ์ง‘ ๊ฐ€๋Šฅ) * - readonly: ์ฝ๊ธฐ ์ „์šฉ ํ…์ŠคํŠธ๋กœ ํ‘œ์‹œ + * - hidden: ์ˆจ๊น€ (UI์— ํ‘œ์‹œ๋˜์ง€ ์•Š์ง€๋งŒ ๋ฐ์ดํ„ฐ์— ํฌํ•จ๋จ) * - (์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์€ ์ž๋™์œผ๋กœ ๋ฐฐ์ง€๋กœ ํ‘œ์‹œ๋จ) */ -export type RepeaterFieldDisplayMode = "input" | "readonly"; +export type RepeaterFieldDisplayMode = "input" | "readonly" | "hidden"; + +/** + * ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์†Œ์Šค ์„ค์ • + * ํ•„๋“œ ๊ฐ’์„ ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์‚ฌ์šฉ + */ +export interface SubDataSourceConfig { + enabled: boolean; // ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + sourceColumn: string; // ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ…Œ์ด๋ธ”์˜ ์†Œ์Šค ์ปฌ๋Ÿผ (์˜ˆ: lot_number) +} /** * ๋ฐ˜๋ณต ๊ทธ๋ฃน ๋‚ด ๊ฐœ๋ณ„ ํ•„๋“œ ์ •์˜ @@ -60,6 +70,8 @@ export interface RepeaterFieldDefinition { options?: Array<{ label: string; value: string }>; // select์šฉ width?: string; // ํ•„๋“œ ๋„ˆ๋น„ (์˜ˆ: "200px", "50%") displayMode?: RepeaterFieldDisplayMode; // ํ‘œ์‹œ ๋ชจ๋“œ: input(์ž…๋ ฅ), readonly(์ฝ๊ธฐ์ „์šฉ) + isHidden?: boolean; // ์ˆจ๊น€ ์—ฌ๋ถ€ (true๋ฉด ํ…Œ์ด๋ธ”์— ํ‘œ์‹œ ์•ˆ ํ•จ, ๋ฐ์ดํ„ฐ๋Š” ์ €์žฅ) + subDataSource?: SubDataSourceConfig; // ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ ์„ค์ • categoryCode?: string; // category ํƒ€์ž…์ผ ๋•Œ ์‚ฌ์šฉํ•  ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ formula?: CalculationFormula; // ๊ณ„์‚ฐ์‹ (type์ด "calculated"์ผ ๋•Œ ์‚ฌ์šฉ) numberFormat?: { From 447bf937de54a181e8b0ce67d0a1ee40639c4b94 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 20 Jan 2026 14:13:09 +0900 Subject: [PATCH 05/57] =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=ED=95=B8=EB=93=A4=EB=9F=AC=20Primary=20Key=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SplitPanelLayoutComponent.tsx | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 0113a9a8..33997fc7 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1418,7 +1418,7 @@ export const SplitPanelLayoutComponent: React.FC // ์ˆ˜์ • ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ const handleEditClick = useCallback( - (panel: "left" | "right", item: any) => { + async (panel: "left" | "right", item: any) => { // ๐Ÿ†• ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ • ๋ฒ„ํŠผ ์„ค์ • ํ™•์ธ if (panel === "right" && componentConfig.rightPanel?.editButton?.mode === "modal") { const modalScreenId = componentConfig.rightPanel?.editButton?.modalScreenId; @@ -1427,18 +1427,40 @@ export const SplitPanelLayoutComponent: React.FC // ์ปค์Šคํ…€ ๋ชจ๋‹ฌ ํ™”๋ฉด ์—ด๊ธฐ const rightTableName = componentConfig.rightPanel?.tableName || ""; - // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) + // Primary Key ์ฐพ๊ธฐ: ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์‹ค์ œ PK ์ปฌ๋Ÿผ ์กฐํšŒ let primaryKeyName = "id"; let primaryKeyValue: any; - if (item.id !== undefined && item.id !== null) { + // 1. ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์‹ค์ œ PK ์ฐพ๊ธฐ + let pkColumn = rightTableColumns.find( + (col) => col.isPrimaryKey === true || col.is_primary_key === true || col.is_primary_key === "YES" + ); + + // 2. rightTableColumns๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด API๋กœ ์ง์ ‘ ์กฐํšŒ + if (!pkColumn && rightTableColumns.length === 0 && rightTableName) { + try { + const columnsResponse = await tableTypeApi.getColumns(rightTableName); + pkColumn = columnsResponse?.find( + (col: any) => col.isPrimaryKey === true || col.is_primary_key === true || col.is_primary_key === "YES" + ); + } catch (error) { + console.error("PK ์ปฌ๋Ÿผ ์กฐํšŒ ์‹คํŒจ:", error); + } + } + + if (pkColumn) { + const pkName = pkColumn.columnName || pkColumn.column_name; + primaryKeyName = pkName; + primaryKeyValue = item[pkName]; + } else if (item.id !== undefined && item.id !== null) { + // 3. ํด๋ฐฑ: id ์ปฌ๋Ÿผ primaryKeyName = "id"; primaryKeyValue = item.id; } else if (item.ID !== undefined && item.ID !== null) { primaryKeyName = "ID"; primaryKeyValue = item.ID; } else { - // ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ + // 4. ์ตœํ›„์˜ ํด๋ฐฑ: ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ const firstKey = Object.keys(item)[0]; primaryKeyName = firstKey; primaryKeyValue = item[firstKey]; From c31b0540aa2bfcb9e170c2f0512b9d6ffe5b7102 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 20 Jan 2026 16:08:38 +0900 Subject: [PATCH 06/57] =?UTF-8?q?fix:=20=EB=B6=84=ED=95=A0=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=97=B0=EA=B2=B0=20=ED=95=84=ED=84=B0=EC=97=90=20?= =?UTF-8?q?operator=20equals=20=EB=88=84=EB=9D=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20:=20=EC=9A=B0=EC=B8=A1=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=EC=97=90=20=EC=97=B0=EA=B4=80=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0(=EB=B6=80=EC=84=9C=EC=9D=B8=EC=9B=90)=EA=B0=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95=20-=20=EC=9E=AC?= =?UTF-8?q?=EA=B3=A0=EC=9D=B4=EB=A0=A5=EC=97=90=EC=84=9C=20=ED=92=88?= =?UTF-8?q?=EB=B2=88=EC=9C=BC=EB=A1=9C=20=EC=B6=9C=EB=A0=A5=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?:=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A1=B0=EC=9D=B8=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=ED=95=9C=20=EC=BB=AC=EB=9F=BC=20=EB=B3=84=EC=B9=AD?= =?UTF-8?q?=EC=9D=B4=20=EC=A4=91=EB=B3=B5=20=EC=83=9D=EC=84=B1=EB=90=98?= =?UTF-8?q?=EC=96=B4=20SQL=20=EC=97=90=EB=9F=AC=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/entityJoinService.ts | 36 +++++++++++++++---- .../SplitPanelLayoutComponent.tsx | 19 ++++++++-- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 25d96927..86762b64 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -334,6 +334,10 @@ export class EntityJoinService { ); }); + // ๐Ÿ”ง _label ๋ณ„์นญ ์ค‘๋ณต ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ Set + // ๊ฐ™์€ sourceColumn์—์„œ ์—ฌ๋Ÿฌ ์กฐ์ธ ์„ค์ •์ด ์žˆ์„ ๋•Œ _label์€ ์ฒซ ๋ฒˆ์งธ๋งŒ ์ƒ์„ฑ + const generatedLabelAliases = new Set(); + const joinColumns = joinConfigs .map((config) => { const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; @@ -368,16 +372,26 @@ export class EntityJoinService { // _label ํ•„๋“œ๋„ ํ•จ๊ป˜ SELECT (ํ”„๋ก ํŠธ์—”๋“œ getColumnUniqueValues์šฉ) // sourceColumn_label ํ˜•์‹์œผ๋กœ ์ถ”๊ฐ€ - resultColumns.push( - `COALESCE(${alias}.${col}::TEXT, '') AS ${config.sourceColumn}_label` - ); + // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€: ๊ฐ™์€ sourceColumn์—์„œ _label์€ ์ฒซ ๋ฒˆ์งธ๋งŒ ์ƒ์„ฑ + const labelAlias = `${config.sourceColumn}_label`; + if (!generatedLabelAliases.has(labelAlias)) { + resultColumns.push( + `COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}` + ); + generatedLabelAliases.add(labelAlias); + } // ๐Ÿ†• referenceColumn (PK)๋„ ํ•ญ์ƒ SELECT (parentDataMapping์šฉ) // ์˜ˆ: customer_code, item_number ๋“ฑ // col๊ณผ ๋™์ผํ•ด๋„ ๋ณ„๋„์˜ alias๋กœ ์ถ”๊ฐ€ (customer_code as customer_code) - resultColumns.push( - `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}` - ); + // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€: referenceColumn๋„ ํ•œ ๋ฒˆ๋งŒ ์ถ”๊ฐ€ + const refColAlias = config.referenceColumn; + if (!generatedLabelAliases.has(refColAlias)) { + resultColumns.push( + `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${refColAlias}` + ); + generatedLabelAliases.add(refColAlias); + } } else { resultColumns.push( `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}` @@ -392,6 +406,11 @@ export class EntityJoinService { const individualAlias = `${config.sourceColumn}_${col}`; + // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€: ๊ฐ™์€ alias๊ฐ€ ์ด๋ฏธ ์ƒ์„ฑ๋˜์—ˆ์œผ๋ฉด ์Šคํ‚ต + if (generatedLabelAliases.has(individualAlias)) { + return; + } + if (isJoinTableColumn) { // ์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์€ ์กฐ์ธ ๋ณ„์นญ ์‚ฌ์šฉ resultColumns.push( @@ -403,6 +422,7 @@ export class EntityJoinService { `COALESCE(main.${col}::TEXT, '') AS ${individualAlias}` ); } + generatedLabelAliases.add(individualAlias); }); // ๐Ÿ†• referenceColumn (PK)๋„ ํ•จ๊ป˜ SELECT (parentDataMapping์šฉ) @@ -410,11 +430,13 @@ export class EntityJoinService { config.referenceTable && config.referenceTable !== tableName; if ( isJoinTableColumn && - !displayColumns.includes(config.referenceColumn) + !displayColumns.includes(config.referenceColumn) && + !generatedLabelAliases.has(config.referenceColumn) // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€ ) { resultColumns.push( `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}` ); + generatedLabelAliases.add(config.referenceColumn); } } diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 869d2c3c..ab387348 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -924,10 +924,15 @@ export const SplitPanelLayoutComponent: React.FC const { entityJoinApi } = await import("@/lib/api/entityJoin"); // ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด ์ƒ์„ฑ + // ๐Ÿ”ง entity ํƒ€์ž… ์ปฌ๋Ÿผ์€ ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•˜๋ฏ€๋กœ operator: 'equals' ์‚ฌ์šฉ const searchConditions: Record = {}; keys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { - searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๋Š” ์ •ํ™•ํ•œ ๊ฐ’ ๋งค์นญ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ equals ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ + searchConditions[key.rightColumn] = { + value: leftItem[key.leftColumn], + operator: "equals", + }; } }); @@ -1033,16 +1038,24 @@ export const SplitPanelLayoutComponent: React.FC if (keys && keys.length > 0) { // ๋ณตํ•ฉํ‚ค + // ๐Ÿ”ง entity ํƒ€์ž… ์ปฌ๋Ÿผ์€ ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•˜๋ฏ€๋กœ operator: 'equals' ์‚ฌ์šฉ keys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { - searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + searchConditions[key.rightColumn] = { + value: leftItem[key.leftColumn], + operator: "equals", + }; } }); } else { // ๋‹จ์ผํ‚ค + // ๐Ÿ”ง entity ํƒ€์ž… ์ปฌ๋Ÿผ์€ ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•˜๋ฏ€๋กœ operator: 'equals' ์‚ฌ์šฉ const leftValue = leftItem[leftColumn]; if (leftValue !== undefined) { - searchConditions[rightColumn] = leftValue; + searchConditions[rightColumn] = { + value: leftValue, + operator: "equals", + }; } } From 0907d318ebd096b658b5a56ab407f7d8ff7ddcb5 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 20 Jan 2026 17:05:36 +0900 Subject: [PATCH 07/57] =?UTF-8?q?fix:=20=EC=88=98=EC=A0=95=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EC=B1=84=EB=B2=88=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=AC=ED=95=A0=EB=8B=B9=20=EB=B0=A9=EC=A7=80=20?= =?UTF-8?q?handleSave():=20formData.id=20=EC=B2=B4=ED=81=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=93=9C=20=ED=8C=90=EB=B3=84,?= =?UTF-8?q?=20=EA=B8=B0=EC=A1=B4=20=EB=B2=88=ED=98=B8=20=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=20handleUniversalFormModalTableSectionSave():=20formData.id=20?= =?UTF-8?q?=EB=B0=8F=20originalGroupedData=20=EC=B2=B4=ED=81=AC=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=93=9C=20=ED=8C=90=EB=B3=84?= =?UTF-8?q?=20=EC=8B=A0=EA=B7=9C=20=EB=93=B1=EB=A1=9D=20=EC=8B=9C=EC=97=90?= =?UTF-8?q?=EB=A7=8C=20allocateCode=20=ED=98=B8=EC=B6=9C=ED=95=98=EC=97=AC?= =?UTF-8?q?=20=EC=B1=84=EB=B2=88=20=EC=BD=94=EB=93=9C=20=ED=95=A0=EB=8B=B9?= =?UTF-8?q?=20=EC=9E=85=EA=B3=A0=EA=B4=80=EB=A6=AC=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EC=9E=85?= =?UTF-8?q?=EA=B3=A0=EB=B2=88=ED=98=B8=20=EC=A6=9D=EA=B0=80=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/utils/buttonActions.ts | 64 +++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index b0ac2ba9..af342a1f 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -707,12 +707,19 @@ export class ButtonActionExecutor { if (repeaterJsonKeys.length > 0) { console.log("๐Ÿ”„ [handleSave] RepeaterFieldGroup JSON ๋ฌธ์ž์—ด ๊ฐ์ง€:", repeaterJsonKeys); - + // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (RepeaterFieldGroup ์ €์žฅ ์ „์— ์‹คํ–‰) - console.log("๐Ÿ” [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); - + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ ์ฒดํฌ: formData.id๊ฐ€ ์กด์žฌํ•˜๋ฉด UPDATE ๋ชจ๋“œ์ด๋ฏ€๋กœ ์ฑ„๋ฒˆ ์ฝ”๋“œ ์žฌํ• ๋‹น ๊ธˆ์ง€ + const isEditModeRepeater = + context.formData.id !== undefined && context.formData.id !== null && context.formData.id !== ""; + + console.log("๐Ÿ” [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘", { + isEditMode: isEditModeRepeater, + formDataId: context.formData.id, + }); + const fieldsWithNumberingRepeater: Record = {}; - + // formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ ์ฐพ๊ธฐ for (const [key, value] of Object.entries(context.formData)) { if (key.endsWith("_numberingRuleId") && value) { @@ -721,22 +728,27 @@ export class ButtonActionExecutor { console.log(`๐ŸŽฏ [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ํ•„๋“œ ๋ฐœ๊ฒฌ: ${fieldName} โ†’ ๊ทœ์น™ ${value}`); } } - + console.log("๐Ÿ“‹ [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ:", fieldsWithNumberingRepeater); - - // ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์žˆ๋Š” ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ - if (Object.keys(fieldsWithNumberingRepeater).length > 0) { - console.log("๐ŸŽฏ [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)"); + + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ์—์„œ๋Š” ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋›ฐ๊ธฐ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€) + // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ์—์„œ๋งŒ allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ๋ฒˆํ˜ธ ํ• ๋‹น + if (Object.keys(fieldsWithNumberingRepeater).length > 0 && !isEditModeRepeater) { + console.log( + "๐ŸŽฏ [handleSave-RepeaterFieldGroup] ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ - ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)", + ); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); - + for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) { try { console.log(`๐Ÿ”„ [handleSave-RepeaterFieldGroup] ${fieldName} ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ: ${ruleId}`); const allocateResult = await allocateNumberingCode(ruleId); - + if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; - console.log(`โœ… [handleSave-RepeaterFieldGroup] ${fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${context.formData[fieldName]} โ†’ ${newCode}`); + console.log( + `โœ… [handleSave-RepeaterFieldGroup] ${fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${context.formData[fieldName]} โ†’ ${newCode}`, + ); context.formData[fieldName] = newCode; } else { console.warn(`โš ๏ธ [handleSave-RepeaterFieldGroup] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ:`, allocateResult.error); @@ -745,9 +757,11 @@ export class ButtonActionExecutor { console.error(`โŒ [handleSave-RepeaterFieldGroup] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); } } + } else if (isEditModeRepeater) { + console.log("โญ๏ธ [handleSave-RepeaterFieldGroup] ์ˆ˜์ • ๋ชจ๋“œ - ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋œ€ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€)"); } - - console.log("โœ… [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); + + console.log("โœ… [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ ์™„๋ฃŒ"); // ๐Ÿ†• ์ƒ๋‹จ ํผ ๋ฐ์ดํ„ฐ(๋งˆ์Šคํ„ฐ ์ •๋ณด) ์ถ”์ถœ // RepeaterFieldGroup JSON๊ณผ ์ปดํฌ๋„ŒํŠธ ํ‚ค๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€๊ฐ€ ๋งˆ์Šคํ„ฐ ์ •๋ณด @@ -1951,7 +1965,16 @@ export class ButtonActionExecutor { } // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) - console.log("๐Ÿ” [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ ์ฒดํฌ: formData.id ๋˜๋Š” originalGroupedData๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ๋ชจ๋“œ + const isEditModeUniversal = + (formData.id !== undefined && formData.id !== null && formData.id !== "") || + originalGroupedData.length > 0; + + console.log("๐Ÿ” [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘", { + isEditMode: isEditModeUniversal, + formDataId: formData.id, + originalGroupedDataCount: originalGroupedData.length, + }); const fieldsWithNumbering: Record = {}; @@ -1977,9 +2000,12 @@ export class ButtonActionExecutor { console.log("๐Ÿ“‹ [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ:", fieldsWithNumbering); - // ๐Ÿ”ฅ ์ €์žฅ ์‹œ์ ์— allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€ - if (Object.keys(fieldsWithNumbering).length > 0) { - console.log("๐ŸŽฏ [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)"); + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ์—์„œ๋Š” ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋›ฐ๊ธฐ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€) + // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ์—์„œ๋งŒ allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ๋ฒˆํ˜ธ ํ• ๋‹น + if (Object.keys(fieldsWithNumbering).length > 0 && !isEditModeUniversal) { + console.log( + "๐ŸŽฏ [handleUniversalFormModalTableSectionSave] ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ - ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)", + ); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { @@ -2006,6 +2032,8 @@ export class ButtonActionExecutor { // ์˜ค๋ฅ˜ ์‹œ ๊ธฐ์กด ๊ฐ’ ์œ ์ง€ } } + } else if (isEditModeUniversal) { + console.log("โญ๏ธ [handleUniversalFormModalTableSectionSave] ์ˆ˜์ • ๋ชจ๋“œ - ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋œ€ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€)"); } console.log("โœ… [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); From a55115ac486b5dba1b2cf873ad03ea6a958059b8 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 09:25:21 +0900 Subject: [PATCH 08/57] =?UTF-8?q?=EC=97=91=EC=85=80=20=EC=97=B4=EC=A7=A4?= =?UTF-8?q?=EB=A0=A4=EC=84=9C=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=90=98?= =?UTF-8?q?=EB=8A=94=EA=B1=B0=EB=9E=91=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EC=8B=9C=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=91=90=EA=B0=9C=EC=9D=B4=EC=83=81=EC=9D=B4=EB=A9=B4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=A1=9C=20=EB=82=98=EC=98=A4=EB=8D=98?= =?UTF-8?q?=EA=B1=B0=20=EC=88=98=EC=A0=95=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 40 ++++++++++++++----- frontend/lib/utils/buttonActions.ts | 30 ++++++++++++-- frontend/lib/utils/excelExport.ts | 6 ++- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 366aa05b..9793acd8 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2688,19 +2688,41 @@ export const TableListComponent: React.FC = ({ const value = row[mappedColumnName]; // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘๋œ ๊ฐ’ ์ฒ˜๋ฆฌ - if (categoryMappings[col.columnName] && value !== null && value !== undefined) { - const mapping = categoryMappings[col.columnName][String(value)]; - if (mapping) { - return mapping.label; + if (value !== null && value !== undefined) { + const valueStr = String(value); + + // ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ธ ๊ฒฝ์šฐ๋งŒ) + if (valueStr.startsWith("CATEGORY_")) { + console.log("๐Ÿ” [์—‘์…€๋‹ค์šด๋กœ๋“œ] ์นดํ…Œ๊ณ ๋ฆฌ ๋ณ€ํ™˜ ์‹œ๋„:", { + columnName: col.columnName, + value: valueStr, + hasMappings: !!categoryMappings[col.columnName], + mappingsKeys: categoryMappings[col.columnName] ? Object.keys(categoryMappings[col.columnName]).slice(0, 5) : [], + }); } + + if (categoryMappings[col.columnName]) { + // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ค‘๋ณต ๊ฐ’ ์ฒ˜๋ฆฌ + if (valueStr.includes(",")) { + const values = valueStr.split(",").map((v) => v.trim()).filter((v) => v); + const labels = values.map((v) => { + const mapping = categoryMappings[col.columnName][v]; + return mapping ? mapping.label : v; + }); + return labels.join(", "); + } + // ๋‹จ์ผ ๊ฐ’ ์ฒ˜๋ฆฌ + const mapping = categoryMappings[col.columnName][valueStr]; + if (mapping) { + return mapping.label; + } + } + + return value; } // null/undefined ์ฒ˜๋ฆฌ - if (value === null || value === undefined) { - return ""; - } - - return value; + return ""; }); }); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index debf58b2..6c69ad10 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4737,7 +4737,24 @@ export class ButtonActionExecutor { const filteredRow: Record = {}; visibleColumns!.forEach((columnName: string) => { const label = columnLabels?.[columnName] || columnName; - filteredRow[label] = row[columnName]; + let value = row[columnName]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋ฅผ ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (CATEGORY_๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฐ’) + if (value && typeof value === "string" && value.includes("CATEGORY_")) { + // ๋จผ์ € _label ํ•„๋“œ ํ™•์ธ (API์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ) + const labelFieldName = `${columnName}_label`; + if (row[labelFieldName]) { + value = row[labelFieldName]; + } else { + // _value_label ํ•„๋“œ ํ™•์ธ + const valueLabelFieldName = `${columnName}_value_label`; + if (row[valueLabelFieldName]) { + value = row[valueLabelFieldName]; + } + } + } + + filteredRow[label] = value; }); return filteredRow; }); @@ -5010,8 +5027,15 @@ export class ButtonActionExecutor { value = row[`${columnName}_name`]; } // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ํ•„๋“œ๋Š” ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์ปฌ๋Ÿผ๋งŒ) - else if (categoryMap[columnName] && typeof value === "string" && categoryMap[columnName][value]) { - value = categoryMap[columnName][value]; + else if (categoryMap[columnName] && typeof value === "string") { + // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ + if (value.includes(",")) { + const values = value.split(",").map((v) => v.trim()).filter((v) => v); + const labels = values.map((v) => categoryMap[columnName][v] || v); + value = labels.join(", "); + } else if (categoryMap[columnName][value]) { + value = categoryMap[columnName][value]; + } } filteredRow[label] = value; diff --git a/frontend/lib/utils/excelExport.ts b/frontend/lib/utils/excelExport.ts index 52c22f5a..6bd97624 100644 --- a/frontend/lib/utils/excelExport.ts +++ b/frontend/lib/utils/excelExport.ts @@ -116,8 +116,10 @@ export async function importFromExcel( return; } - // JSON์œผ๋กœ ๋ณ€ํ™˜ - const jsonData = XLSX.utils.sheet_to_json(worksheet); + // JSON์œผ๋กœ ๋ณ€ํ™˜ (๋นˆ ์…€๋„ ํฌํ•จํ•˜์—ฌ ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‚ค ์œ ์ง€) + const jsonData = XLSX.utils.sheet_to_json(worksheet, { + defval: "", // ๋นˆ ์…€์— ๋นˆ ๋ฌธ์ž์—ด ํ• ๋‹น + }); console.log("โœ… ์—‘์…€ ๊ฐ€์ ธ์˜ค๊ธฐ ์™„๋ฃŒ:", { sheetName: targetSheetName, From 6a0aa87d3b7c6eeb356ae63453ecdbaf0c00c1a1 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 09:25:51 +0900 Subject: [PATCH 09/57] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj From 4781a17b71a614175dc053591316fd44610c6d3e Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 21 Jan 2026 09:33:44 +0900 Subject: [PATCH 10/57] =?UTF-8?q?=EB=A6=AC=EC=82=AC=EC=9D=B4=EC=A6=88=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0:=20Realtime?= =?UTF-8?q?PreviewDynamic=20=EB=B0=8F=20TabsWidget=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EC=A6=88=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EA=B3=A0,=20=EB=A6=AC=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EC=A6=88=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=98=EC=97=AC=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B2=BD=ED=97=98=EC=9D=84=20=ED=96=A5=EC=83=81=EC=8B=9C?= =?UTF-8?q?=EC=BC=B0=EC=8A=B5=EB=8B=88=EB=8B=A4.=20=EC=9D=B4=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95=20=EC=8B=9C=20=EB=8D=94?= =?UTF-8?q?=20=EB=82=98=EC=9D=80=20=EB=B0=98=EC=9D=91=EC=84=B1=EA=B3=BC=20?= =?UTF-8?q?=EC=A0=95=ED=99=95=EC=84=B1=EC=9D=84=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=90=98=EC=97=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/RealtimePreviewDynamic.tsx | 150 +++++++++- frontend/components/screen/ScreenDesigner.tsx | 271 +++++++++++------- .../v2-tabs-widget/tabs-component.tsx | 178 +++++++++++- 3 files changed, 482 insertions(+), 117 deletions(-) diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index 61c7e77c..75eec128 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -39,6 +39,7 @@ interface RealtimePreviewProps { onUpdateComponent?: (updatedComponent: any) => void; // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ onSelectTabComponent?: (tabId: string, compId: string, comp: any) => void; // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ selectedTabComponentId?: string; // ๐Ÿ†• ์„ ํƒ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + onResize?: (componentId: string, newSize: { width: number; height: number }) => void; // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฝœ๋ฐฑ // ๋ฒ„ํŠผ ์•ก์…˜์„ ์œ„ํ•œ props screenId?: number; @@ -139,6 +140,7 @@ const RealtimePreviewDynamicComponent: React.FC = ({ onUpdateComponent, // ๐Ÿ†• ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ ์ฝœ๋ฐฑ onSelectTabComponent, // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์ฝœ๋ฐฑ selectedTabComponentId, // ๐Ÿ†• ์„ ํƒ๋œ ํƒญ ์ปดํฌ๋„ŒํŠธ ID + onResize, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ฝœ๋ฐฑ }) => { // ๐Ÿ†• ํ™”๋ฉด ๋‹ค๊ตญ์–ด ์ปจํ…์ŠคํŠธ const { getTranslatedText } = useScreenMultiLang(); @@ -146,6 +148,102 @@ const RealtimePreviewDynamicComponent: React.FC = ({ const [actualHeight, setActualHeight] = React.useState(null); const contentRef = React.useRef(null); const lastUpdatedHeight = React.useRef(null); + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ + const [isResizing, setIsResizing] = React.useState(false); + const [resizeSize, setResizeSize] = React.useState<{ width: number; height: number } | null>(null); + const rafRef = React.useRef(null); + + // ๐Ÿ†• size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด resizeSize ์ดˆ๊ธฐํ™” (๋ ˆ์ด์•„์›ƒ ์ƒํƒœ๊ฐ€ props์— ๋ฐ˜์˜๋˜์—ˆ์Œ) + React.useEffect(() => { + if (resizeSize && !isResizing) { + // component.size๊ฐ€ resizeSize์™€ ๊ฐ™์•„์ง€๋ฉด resizeSize ์ดˆ๊ธฐํ™” + if (component.size?.width === resizeSize.width && component.size?.height === resizeSize.height) { + setResizeSize(null); + } + } + }, [component.size?.width, component.size?.height, resizeSize, isResizing]); + + // 10px ๋‹จ์œ„ ์Šค๋ƒ… ํ•จ์ˆ˜ + const snapTo10 = (value: number) => Math.round(value / 10) * 10; + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋Ÿฌ + const handleResizeStart = React.useCallback( + (e: React.MouseEvent, direction: "e" | "s" | "se") => { + e.stopPropagation(); + e.preventDefault(); + + const startMouseX = e.clientX; + const startMouseY = e.clientY; + const startWidth = component.size?.width || 200; + const startHeight = component.size?.height || 100; + + setIsResizing(true); + setResizeSize({ width: startWidth, height: startHeight }); + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + rafRef.current = requestAnimationFrame(() => { + const deltaX = moveEvent.clientX - startMouseX; + const deltaY = moveEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + setResizeSize({ width: newWidth, height: newHeight }); + }); + }; + + const handleMouseUp = (upEvent: MouseEvent) => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + const deltaX = upEvent.clientX - startMouseX; + const deltaY = upEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ๋Š” ์œ ์ง€ํ•œ ์ฑ„๋กœ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + // resizeSize๋Š” null๋กœ ์„ค์ •ํ•˜์ง€ ์•Š๊ณ  ๋งˆ์ง€๋ง‰ ํฌ๊ธฐ ์œ ์ง€ + // (component.size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์˜ฌ๋ฐ”๋ฅธ ํฌ๊ธฐ ํ‘œ์‹œ) + + // ๐Ÿ†• ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ ํ˜ธ์ถœํ•˜์—ฌ ๋ ˆ์ด์•„์›ƒ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (onResize) { + onResize(component.id, { width: newWidth, height: newHeight }); + } + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ํ”Œ๋ž˜๊ทธ๋งŒ ํ•ด์ œ (resizeSize๋Š” ๋งˆ์ง€๋ง‰ ํฌ๊ธฐ ์œ ์ง€) + setIsResizing(false); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [component.id, component.size, onResize] + ); // ํ”Œ๋กœ์šฐ ์œ„์ ฏ์˜ ์‹ค์ œ ๋†’์ด ์ธก์ • React.useEffect(() => { @@ -249,18 +347,27 @@ const RealtimePreviewDynamicComponent: React.FC = ({ return `${actualHeight}px`; } - // 1์ˆœ์œ„: style.height๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋˜๋Š” ์ˆซ์ž+px) + // ๐Ÿ†• 1์ˆœ์œ„: size.height๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ (๋ ˆ์ด์•„์›ƒ์—์„œ ๊ด€๋ฆฌ๋˜๋Š” ์‹ค์ œ ํฌ๊ธฐ) + // size๋Š” ๋ ˆ์ด์•„์›ƒ ์ƒํƒœ์—์„œ ์ง์ ‘ ๊ด€๋ฆฌ๋˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ฆˆ๋กœ ๋ณ€๊ฒฝ๋จ + if (size?.height && size.height > 0) { + if (component.componentConfig?.type === "table-list") { + return `${Math.max(size.height, 200)}px`; + } + return `${size.height}px`; + } + + // 2์ˆœ์œ„: componentStyle.height (์ปดํฌ๋„ŒํŠธ ์ •์˜์—์„œ ์˜จ ๊ธฐ๋ณธ ์Šคํƒ€์ผ) if (componentStyle?.height) { return typeof componentStyle.height === "number" ? `${componentStyle.height}px` : componentStyle.height; } - // 2์ˆœ์œ„: size.height (ํ”ฝ์…€) + // 3์ˆœ์œ„: ๊ธฐ๋ณธ๊ฐ’ if (component.componentConfig?.type === "table-list") { - return `${Math.max(size?.height || 200, 200)}px`; + return "200px"; } - // size.height๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์ตœ์†Œ 10px - return `${size?.height || 10}px`; + // ๊ธฐ๋ณธ ๋†’์ด + return "10px"; }; // layout ํƒ€์ž… ์ปดํฌ๋„ŒํŠธ์ธ์ง€ ํ™•์ธ @@ -405,16 +512,22 @@ const RealtimePreviewDynamicComponent: React.FC = ({ const { adjustedPositionX, isOnSplitPanel, isDraggingSplitPanel } = calculateButtonPosition(); + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ํฌ๊ธฐ๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + // (size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด ์œ„ useEffect์—์„œ resizeSize๋ฅผ null๋กœ ์„ค์ •) + const displayWidth = resizeSize ? `${resizeSize.width}px` : getWidth(); + const displayHeight = resizeSize ? `${resizeSize.height}px` : getHeight(); + const baseStyle = { left: `${adjustedPositionX}px`, // ๐Ÿ†• ์กฐ์ •๋œ X ์ขŒํ‘œ ์‚ฌ์šฉ top: `${position.y}px`, ...componentStyle, // componentStyle ์ „์ฒด ์ ์šฉ (DynamicComponentRenderer์—์„œ ์ด๋ฏธ size๊ฐ€ ๋ณ€ํ™˜๋จ) - width: getWidth(), // getWidth() ์šฐ์„  (table-list ๋“ฑ ํŠน์ˆ˜ ์ผ€์ด์Šค) - height: getHeight(), // getHeight() ์šฐ์„  (flow-widget ๋“ฑ ํŠน์ˆ˜ ์ผ€์ด์Šค) + width: displayWidth, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘์ด๋ฉด resizeSize ์‚ฌ์šฉ + height: displayHeight, // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘์ด๋ฉด resizeSize ์‚ฌ์šฉ zIndex: component.type === "layout" ? 1 : position.z || 2, right: undefined, - // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ํŠธ๋žœ์ง€์…˜ ์—†์ด ์ฆ‰์‹œ ์ด๋™ + // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„ ๋“œ๋ž˜๊ทธ ์ค‘์—๋Š” ํŠธ๋žœ์ง€์…˜ ์—†์ด ์ฆ‰์‹œ ์ด๋™, ๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘์—๋„ ํŠธ๋žœ์ง€์…˜ ์—†์Œ transition: + isResizing ? "none" : isOnSplitPanel && isButtonComponent ? (isDraggingSplitPanel ? "none" : "left 0.1s ease-out") : undefined, }; @@ -546,6 +659,27 @@ const RealtimePreviewDynamicComponent: React.FC = ({ )}
)} + + {/* ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€์žฅ์ž๋ฆฌ ์˜์—ญ - ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ + ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋งŒ ํ‘œ์‹œ */} + {isSelected && isDesignMode && onResize && ( + <> + {/* ์˜ค๋ฅธ์ชฝ ๊ฐ€์žฅ์ž๋ฆฌ (๋„ˆ๋น„ ์กฐ์ ˆ) */} +
handleResizeStart(e, "e")} + /> + {/* ์•„๋ž˜ ๊ฐ€์žฅ์ž๋ฆฌ (๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, "s")} + /> + {/* ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ๋ชจ์„œ๋ฆฌ (๋„ˆ๋น„+๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, "se")} + /> + + )}
); }; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index e8561387..410ed46e 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -4905,115 +4905,155 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD - {/* ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์„ ํƒ๋œ ๊ฒฝ์šฐ ๋ณ„๋„ ํŒจ๋„ ํ‘œ์‹œ */} + {/* ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ์‹œ์—๋„ UnifiedPropertiesPanel ์‚ฌ์šฉ */} {selectedTabComponentInfo ? ( -
-
-
-

ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ค์ •

-

- {selectedTabComponentInfo.component.label || selectedTabComponentInfo.component.componentType} -

-
- -
- {/* DynamicComponentConfigPanel ๋ Œ๋”๋ง */} -
- {(() => { - const DynamicConfigPanel = require("@/lib/utils/getComponentConfigPanel").DynamicComponentConfigPanel; - const tabComp = selectedTabComponentInfo.component; - - // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ - const componentForConfig = { - id: tabComp.id, - type: "component", - componentType: tabComp.componentType, - label: tabComp.label, - position: tabComp.position, - size: tabComp.size, - componentConfig: tabComp.componentConfig || {}, - style: tabComp.style || {}, - inputType: tabComp.inputType || tabComp.componentConfig?.inputType, // ๐Ÿ†• inputType ์ถ”๊ฐ€ - widgetType: tabComp.widgetType || tabComp.componentConfig?.widgetType, // ๐Ÿ†• widgetType ์ถ”๊ฐ€ + (() => { + const tabComp = selectedTabComponentInfo.component; + + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ComponentData ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ + const tabComponentAsComponentData: ComponentData = { + id: tabComp.id, + type: "component", + componentType: tabComp.componentType, + label: tabComp.label, + position: tabComp.position || { x: 0, y: 0 }, + size: tabComp.size || { width: 200, height: 100 }, + componentConfig: tabComp.componentConfig || {}, + style: tabComp.style || {}, + } as ComponentData; + + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์šฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ ํ•ธ๋“ค๋Ÿฌ + const updateTabComponentProperty = (componentId: string, path: string, value: any) => { + const { tabsComponentId, tabId } = selectedTabComponentInfo; + + setLayout((prevLayout) => { + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; + + const currentConfig = (tabsComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; + + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === tabId) { + return { + ...tab, + components: (tab.components || []).map((comp: any) => { + if (comp.id !== componentId) return comp; + + // path๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ ์ค‘์ฒฉ ์†์„ฑ ์—…๋ฐ์ดํŠธ + const pathParts = path.split("."); + const newComp = { ...comp }; + let current: any = newComp; + + for (let i = 0; i < pathParts.length - 1; i++) { + const part = pathParts[i]; + if (!current[part]) { + current[part] = {}; + } else { + current[part] = { ...current[part] }; + } + current = current[part]; + } + current[pathParts[pathParts.length - 1]] = value; + + return newComp; + }), + }; + } + return tab; + }); + + const updatedComponent = { + ...tabsComponent, + componentConfig: { ...currentConfig, tabs: updatedTabs }, }; - return ( - 0 ? tables[0].columns : []} - menuObjid={selectedScreen?.menuObjid} - currentComponent={componentForConfig} - onChange={(newConfig: any) => { - // componentConfig ์ „์ฒด ์—…๋ฐ์ดํŠธ - ํ•จ์ˆ˜ํ˜• ์—…๋ฐ์ดํŠธ๋กœ ํด๋กœ์ € ๋ฌธ์ œ ํ•ด๊ฒฐ - const { tabsComponentId, tabId, componentId } = selectedTabComponentInfo; - - setLayout((prevLayout) => { - const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); - if (!tabsComponent) return prevLayout; + // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์—…๋ฐ์ดํŠธ + const updatedComp = updatedTabs + .find((t: any) => t.id === tabId) + ?.components?.find((c: any) => c.id === componentId); + if (updatedComp) { + setSelectedTabComponentInfo((prev) => + prev ? { ...prev, component: updatedComp } : null + ); + } - const currentConfig = (tabsComponent as any).componentConfig || {}; - const tabs = currentConfig.tabs || []; + return { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedComponent : c + ), + }; + }); + }; - const updatedTabs = tabs.map((tab: any) => { - if (tab.id === tabId) { - return { - ...tab, - components: (tab.components || []).map((comp: any) => - comp.id === componentId - ? { ...comp, componentConfig: newConfig } - : comp - ), - }; - } - return tab; - }); + // ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ + const deleteTabComponent = (componentId: string) => { + const { tabsComponentId, tabId } = selectedTabComponentInfo; + + setLayout((prevLayout) => { + const tabsComponent = prevLayout.components.find((c) => c.id === tabsComponentId); + if (!tabsComponent) return prevLayout; - const updatedComponent = { - ...tabsComponent, - componentConfig: { - ...currentConfig, - tabs: updatedTabs, - }, - }; + const currentConfig = (tabsComponent as any).componentConfig || {}; + const tabs = currentConfig.tabs || []; - const newLayout = { - ...prevLayout, - components: prevLayout.components.map((c) => - c.id === tabsComponentId ? updatedComponent : c - ), - }; + const updatedTabs = tabs.map((tab: any) => { + if (tab.id === tabId) { + return { + ...tab, + components: (tab.components || []).filter((c: any) => c.id !== componentId), + }; + } + return tab; + }); - // ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ ์ •๋ณด ์—…๋ฐ์ดํŠธ - const updatedComp = updatedTabs - .find((t: any) => t.id === tabId) - ?.components?.find((c: any) => c.id === componentId); - if (updatedComp) { - setSelectedTabComponentInfo((prev) => - prev ? { ...prev, component: updatedComp } : null - ); - } + const updatedComponent = { + ...tabsComponent, + componentConfig: { ...currentConfig, tabs: updatedTabs }, + }; - return newLayout; - }); - }} - screenTableName={selectedScreen?.tableName} - tableColumns={tables.length > 0 ? tables[0].columns : []} + setSelectedTabComponentInfo(null); + + return { + ...prevLayout, + components: prevLayout.components.map((c) => + c.id === tabsComponentId ? updatedComponent : c + ), + }; + }); + }; + + return ( +
+
+ ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ + +
+
+ 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + currentScreenCompanyCode={selectedScreen?.companyCode} + onStyleChange={(style) => { + updateTabComponentProperty(tabComp.id, "style", style); + }} allComponents={layout.components} - menuObjid={selectedScreen?.menuObjid} + menuObjid={menuObjid} /> - ); - })()} -
-
+
+
+ ); + })() ) : ( { + setLayout((prevLayout) => { + const updatedComponents = prevLayout.components.map((comp) => + comp.id === componentId + ? { ...comp, size: newSize } + : comp + ); + + const newLayout = { + ...prevLayout, + components: updatedComponents, + }; + + // saveToHistory๋Š” ๋ณ„๋„๋กœ ํ˜ธ์ถœ (prevLayout ๊ธฐ๋ฐ˜) + setTimeout(() => saveToHistory(newLayout), 0); + return newLayout; + }); + }} // ๐Ÿ†• ํƒญ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ onSelectTabComponent={(tabId, compId, comp) => handleSelectTabComponent(component.id, tabId, compId, comp) @@ -5476,6 +5535,24 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD // console.log("๐Ÿ“ค ์ž์‹ ์ปดํฌ๋„ŒํŠธ ์„ค์ • ๋ณ€๊ฒฝ์„ ์ƒ์„ธ์„ค์ •์— ์•Œ๋ฆผ:", config); // TODO: ์‹ค์ œ ๊ตฌํ˜„์€ DetailSettingsPanel๊ณผ์˜ ์—ฐ๋™ ํ•„์š” }} + // ๐Ÿ†• ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค๋Ÿฌ + onResize={(componentId, newSize) => { + setLayout((prevLayout) => { + const updatedComponents = prevLayout.components.map((comp) => + comp.id === componentId + ? { ...comp, size: newSize } + : comp + ); + + const newLayout = { + ...prevLayout, + components: updatedComponents, + }; + + setTimeout(() => saveToHistory(newLayout), 0); + return newLayout; + }); + }} /> ); })} diff --git a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx index 6aa29cb9..91965aa8 100644 --- a/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx +++ b/frontend/lib/registry/components/v2-tabs-widget/tabs-component.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useRef, useCallback } from "react"; +import React, { useState, useRef, useCallback, useEffect } from "react"; import { ComponentRegistry } from "../../ComponentRegistry"; import { ComponentCategory } from "@/types/component"; import { Folder, Plus, Move, Settings, Trash2 } from "lucide-react"; @@ -21,8 +21,26 @@ const TabsDesignEditor: React.FC<{ const [dragPosition, setDragPosition] = useState<{ x: number; y: number } | null>(null); const containerRef = useRef(null); const rafRef = useRef(null); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ + const [resizingCompId, setResizingCompId] = useState(null); + const [resizeSize, setResizeSize] = useState<{ width: number; height: number } | null>(null); + const [lastResizedCompId, setLastResizedCompId] = useState(null); const activeTab = tabs.find((t) => t.id === activeTabId); + + // ๐Ÿ†• ํƒญ ์ปดํฌ๋„ŒํŠธ size๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋ฉด resizeSize ์ดˆ๊ธฐํ™” + useEffect(() => { + if (resizeSize && lastResizedCompId && !resizingCompId) { + const targetComp = activeTab?.components?.find(c => c.id === lastResizedCompId); + if (targetComp && + targetComp.size?.width === resizeSize.width && + targetComp.size?.height === resizeSize.height) { + setResizeSize(null); + setLastResizedCompId(null); + } + } + }, [tabs, activeTab, resizeSize, lastResizedCompId, resizingCompId]); const getTabStyle = (tab: TabItem) => { const isActive = tab.id === activeTabId; @@ -157,6 +175,110 @@ const TabsDesignEditor: React.FC<{ [activeTabId, component, onUpdateComponent, tabs] ); + // 10px ๋‹จ์œ„ ์Šค๋ƒ… ํ•จ์ˆ˜ + const snapTo10 = useCallback((value: number) => Math.round(value / 10) * 10, []); + + // ๋ฆฌ์‚ฌ์ด์ฆˆ ์‹œ์ž‘ ํ•ธ๋“ค๋Ÿฌ + const handleResizeStart = useCallback( + (e: React.MouseEvent, comp: TabInlineComponent, direction: "e" | "s" | "se") => { + e.stopPropagation(); + e.preventDefault(); + + const startMouseX = e.clientX; + const startMouseY = e.clientY; + const startWidth = comp.size?.width || 200; + const startHeight = comp.size?.height || 100; + + setResizingCompId(comp.id); + setResizeSize({ width: startWidth, height: startHeight }); + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + rafRef.current = requestAnimationFrame(() => { + const deltaX = moveEvent.clientX - startMouseX; + const deltaY = moveEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + setResizeSize({ width: newWidth, height: newHeight }); + }); + }; + + const handleMouseUp = (upEvent: MouseEvent) => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + const deltaX = upEvent.clientX - startMouseX; + const deltaY = upEvent.clientY - startMouseY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (direction === "e" || direction === "se") { + newWidth = snapTo10(Math.max(50, startWidth + deltaX)); + } + if (direction === "s" || direction === "se") { + newHeight = snapTo10(Math.max(20, startHeight + deltaY)); + } + + // ๐Ÿ†• ํƒญ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ ์—…๋ฐ์ดํŠธ ๋จผ์ € ์‹คํ–‰ + if (onUpdateComponent) { + const updatedTabs = tabs.map((tab) => { + if (tab.id === activeTabId) { + return { + ...tab, + components: (tab.components || []).map((c) => + c.id === comp.id + ? { + ...c, + size: { + width: newWidth, + height: newHeight, + }, + } + : c + ), + }; + } + return tab; + }); + + onUpdateComponent({ + ...component, + componentConfig: { + ...component.componentConfig, + tabs: updatedTabs, + }, + }); + } + + // ๐Ÿ†• ๋ฆฌ์‚ฌ์ด์ฆˆ ์ƒํƒœ ํ•ด์ œ (resizeSize๋Š” ๋งˆ์ง€๋ง‰ ํฌ๊ธฐ ์œ ์ง€, lastResizedCompId ์„ค์ •) + setLastResizedCompId(comp.id); + setResizingCompId(null); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }, + [activeTabId, component, onUpdateComponent, tabs] + ); + return (
{/* ํƒญ ํ—ค๋” */} @@ -205,6 +327,15 @@ const TabsDesignEditor: React.FC<{ {activeTab.components.map((comp: TabInlineComponent) => { const isSelected = selectedTabComponentId === comp.id; const isDragging = draggingCompId === comp.id; + const isResizing = resizingCompId === comp.id; + + // ๋“œ๋ž˜๊ทธ/๋ฆฌ์‚ฌ์ด์ฆˆ ์ค‘ ํ‘œ์‹œํ•  ํฌ๊ธฐ + // resizeSize๊ฐ€ ์žˆ๊ณ  ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์ด๋ฉด resizeSize ์šฐ์„  ์‚ฌ์šฉ (๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ ๋ฐ˜์˜ ์ „๊นŒ์ง€) + const compWidth = comp.size?.width || 200; + const compHeight = comp.size?.height || 100; + const isResizingThis = (resizingCompId === comp.id || lastResizedCompId === comp.id) && resizeSize; + const displayWidth = isResizingThis ? resizeSize!.width : compWidth; + const displayHeight = isResizingThis ? resizeSize!.height : compHeight; // ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฅผ DynamicComponentRenderer ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ const componentData = { @@ -213,7 +344,7 @@ const TabsDesignEditor: React.FC<{ componentType: comp.componentType, label: comp.label, position: comp.position || { x: 0, y: 0 }, - size: comp.size || { width: 200, height: 100 }, + size: { width: displayWidth, height: displayHeight }, componentConfig: comp.componentConfig || {}, style: comp.style || {}, }; @@ -279,23 +410,46 @@ const TabsDesignEditor: React.FC<{ {/* ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง - ํ•ธ๋“ค ์•„๋ž˜์— ๋ณ„๋„ ์˜์—ญ */}
- +
+ +
+ + {/* ๋ฆฌ์‚ฌ์ด์ฆˆ ๊ฐ€์žฅ์ž๋ฆฌ ์˜์—ญ - ์„ ํƒ๋œ ์ปดํฌ๋„ŒํŠธ์—๋งŒ ํ‘œ์‹œ */} + {isSelected && ( + <> + {/* ์˜ค๋ฅธ์ชฝ ๊ฐ€์žฅ์ž๋ฆฌ (๋„ˆ๋น„ ์กฐ์ ˆ) */} +
handleResizeStart(e, comp, "e")} + /> + {/* ์•„๋ž˜ ๊ฐ€์žฅ์ž๋ฆฌ (๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, comp, "s")} + /> + {/* ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ๋ชจ์„œ๋ฆฌ (๋„ˆ๋น„+๋†’์ด ์กฐ์ ˆ) */} +
handleResizeStart(e, comp, "se")} + /> + + )}
); From e8fc6643520dbd733fb20e18634169f964fb345f Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 10:32:37 +0900 Subject: [PATCH 11/57] =?UTF-8?q?fix:=20=EB=B6=84=ED=95=A0=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=88=98=EC=A0=95=20=EB=B2=84=ED=8A=BC=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Primary Key ์ปฌ๋Ÿผ๋ช…์„ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐฑ์—”๋“œ๋กœ ์ „๋‹ฌํ•˜๋„๋ก ๊ฐœ์„  - ๋ฐฑ์—”๋“œ ์ž๋™ ๊ฐ์ง€ ์‹คํŒจ ์‹œ์—๋„ ํด๋ผ์ด์–ธํŠธ ์ œ๊ณต ๊ฐ’ ์šฐ์„  ์‚ฌ์šฉ - Primary Key ์ฐพ๊ธฐ ๋กœ์ง ๊ฐœ์„  (์„ค์ •๊ฐ’ > id > ID > non-null ํ•„๋“œ) --- backend-node/src/routes/dataRoutes.ts | 11 ++++-- backend-node/src/services/dataService.ts | 35 ++++++++++++------- frontend/components/common/ScreenModal.tsx | 8 ++++- .../SplitPanelLayoutComponent.tsx | 35 +++++++++++++++---- 4 files changed, 66 insertions(+), 23 deletions(-) diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index 574f1cf8..a7757397 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -606,7 +606,7 @@ router.get( }); } - const { enableEntityJoin, groupByColumns } = req.query; + const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query; const enableEntityJoinFlag = enableEntityJoin === "true" || (typeof enableEntityJoin === "boolean" && enableEntityJoin); @@ -626,17 +626,22 @@ router.get( } } + // ๐Ÿ†• primaryKeyColumn ํŒŒ์‹ฑ + const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined; + console.log(`๐Ÿ” ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ: ${tableName}/${id}`, { enableEntityJoin: enableEntityJoinFlag, groupByColumns: groupByColumnsArray, + primaryKeyColumn: primaryKeyColumnStr, }); - // ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ (Entity Join ์˜ต์…˜ + ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ ํฌํ•จ) + // ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ (Entity Join ์˜ต์…˜ + ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ + Primary Key ์ปฌ๋Ÿผ ํฌํ•จ) const result = await dataService.getRecordDetail( tableName, id, enableEntityJoinFlag, - groupByColumnsArray + groupByColumnsArray, + primaryKeyColumnStr // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ ); if (!result.success) { diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 8c6e63f0..60de20db 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -490,7 +490,8 @@ class DataService { tableName: string, id: string | number, enableEntityJoin: boolean = false, - groupByColumns: string[] = [] + groupByColumns: string[] = [], + primaryKeyColumn?: string // ๐Ÿ†• ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ Primary Key ์ปฌ๋Ÿผ๋ช… ): Promise> { try { // ํ…Œ์ด๋ธ” ์ ‘๊ทผ ๊ฒ€์ฆ @@ -499,20 +500,30 @@ class DataService { return validation.error!; } - // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ - const pkResult = await query<{ attname: string }>( - `SELECT a.attname - FROM pg_index i - JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - WHERE i.indrelid = $1::regclass AND i.indisprimary`, - [tableName] - ); + // ๐Ÿ†• ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ Primary Key ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + let pkColumn = primaryKeyColumn || ""; + + // Primary Key ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์ž๋™ ๊ฐ์ง€ + if (!pkColumn) { + const pkResult = await query<{ attname: string }>( + `SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = $1::regclass AND i.indisprimary`, + [tableName] + ); - let pkColumn = "id"; // ๊ธฐ๋ณธ๊ฐ’ - if (pkResult.length > 0) { - pkColumn = pkResult[0].attname; + pkColumn = "id"; // ๊ธฐ๋ณธ๊ฐ’ + if (pkResult.length > 0) { + pkColumn = pkResult[0].attname; + } + console.log(`๐Ÿ”‘ [getRecordDetail] ์ž๋™ ๊ฐ์ง€๋œ Primary Key:`, pkResult); + } else { + console.log(`๐Ÿ”‘ [getRecordDetail] ํด๋ผ์ด์–ธํŠธ ์ œ๊ณต Primary Key: ${pkColumn}`); } + console.log(`๐Ÿ”‘ [getRecordDetail] ํ…Œ์ด๋ธ”: ${tableName}, Primary Key ์ปฌ๋Ÿผ: ${pkColumn}, ์กฐํšŒ ID: ${id}`); + // ๐Ÿ†• Entity Join์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ if (enableEntityJoin) { const { EntityJoinService } = await import("./entityJoinService"); diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 44685dc0..fdd104df 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -374,8 +374,9 @@ export const ScreenModal: React.FC = ({ className }) => { const editId = urlParams.get("editId"); const tableName = urlParams.get("tableName") || screenInfo.tableName; const groupByColumnsParam = urlParams.get("groupByColumns"); + const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… - console.log("๐Ÿ“‹ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ:", { mode, editId, tableName, groupByColumnsParam }); + console.log("๐Ÿ“‹ URL ํŒŒ๋ผ๋ฏธํ„ฐ ํ™•์ธ:", { mode, editId, tableName, groupByColumnsParam, primaryKeyColumn }); // ์ˆ˜์ • ๋ชจ๋“œ์ด๊ณ  editId๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ ์กฐํšŒ if (mode === "edit" && editId && tableName) { @@ -414,6 +415,11 @@ export const ScreenModal: React.FC = ({ className }) => { params.groupByColumns = JSON.stringify(groupByColumns); console.log("โœ… [ScreenModal] groupByColumns๋ฅผ params์— ์ถ”๊ฐ€:", params.groupByColumns); } + // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ (๋ฐฑ์—”๋“œ ์ž๋™ ๊ฐ์ง€ ์‹คํŒจ ์‹œ ์‚ฌ์šฉ) + if (primaryKeyColumn) { + params.primaryKeyColumn = primaryKeyColumn; + console.log("โœ… [ScreenModal] primaryKeyColumn์„ params์— ์ถ”๊ฐ€:", primaryKeyColumn); + } console.log("๐Ÿ“ก [ScreenModal] ์‹ค์ œ API ์š”์ฒญ:", { url: `/data/${tableName}/${editId}`, diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ab387348..9b8e7cf0 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1590,21 +1590,40 @@ export const SplitPanelLayoutComponent: React.FC // ์ปค์Šคํ…€ ๋ชจ๋‹ฌ ํ™”๋ฉด ์—ด๊ธฐ const rightTableName = componentConfig.rightPanel?.tableName || ""; - // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) + // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: ์„ค์ •๊ฐ’ > id > ID > non-null ํ•„๋“œ) + // ๐Ÿ”ง ์„ค์ •์—์„œ primaryKeyColumn ์ง€์ • ๊ฐ€๋Šฅ + const configuredPrimaryKey = componentConfig.rightPanel?.editButton?.primaryKeyColumn; + let primaryKeyName = "id"; let primaryKeyValue: any; - if (item.id !== undefined && item.id !== null) { + if (configuredPrimaryKey && item[configuredPrimaryKey] !== undefined && item[configuredPrimaryKey] !== null) { + // ์„ค์ •๋œ Primary Key ์‚ฌ์šฉ + primaryKeyName = configuredPrimaryKey; + primaryKeyValue = item[configuredPrimaryKey]; + } else if (item.id !== undefined && item.id !== null) { primaryKeyName = "id"; primaryKeyValue = item.id; } else if (item.ID !== undefined && item.ID !== null) { primaryKeyName = "ID"; primaryKeyValue = item.ID; } else { - // ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ - const firstKey = Object.keys(item)[0]; - primaryKeyName = firstKey; - primaryKeyValue = item[firstKey]; + // ๐Ÿ”ง ์ฒซ ๋ฒˆ์งธ non-null ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ + const keys = Object.keys(item); + let found = false; + for (const key of keys) { + if (item[key] !== undefined && item[key] !== null) { + primaryKeyName = key; + primaryKeyValue = item[key]; + found = true; + break; + } + } + // ๋ชจ๋“  ํ•„๋“œ๊ฐ€ null์ด๋ฉด ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ ์‚ฌ์šฉ + if (!found && keys.length > 0) { + primaryKeyName = keys[0]; + primaryKeyValue = item[keys[0]]; + } } console.log("โœ… ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { @@ -1629,7 +1648,7 @@ export const SplitPanelLayoutComponent: React.FC hasGroupByColumns: groupByColumns.length > 0, }); - // ScreenModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ID + groupByColumns ์ „๋‹ฌ) + // ScreenModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ID + groupByColumns + primaryKeyColumn ์ „๋‹ฌ) window.dispatchEvent( new CustomEvent("openScreenModal", { detail: { @@ -1638,6 +1657,7 @@ export const SplitPanelLayoutComponent: React.FC mode: "edit", editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ ...(groupByColumns.length > 0 && { groupByColumns: JSON.stringify(groupByColumns), }), @@ -1650,6 +1670,7 @@ export const SplitPanelLayoutComponent: React.FC screenId: modalScreenId, editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "์—†์Œ", }); From 29a4ab7b9df0bfaf7ea03bdfa9ef3d3adc1dc1f2 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 11:40:47 +0900 Subject: [PATCH 12/57] =?UTF-8?q?entity-search-iniput=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EntitySearchInputComponent.tsx | 37 +++++- .../EntitySearchInputConfigPanel.tsx | 122 ++++++++++++++++-- .../EntitySearchInputWrapper.tsx | 5 + .../entity-search-input/EntitySearchModal.tsx | 11 +- .../components/entity-search-input/config.ts | 9 ++ 5 files changed, 166 insertions(+), 18 deletions(-) diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 5045a43b..f1604337 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -11,6 +11,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { dynamicFormApi } from "@/lib/api/dynamicForm"; import { cascadingRelationApi } from "@/lib/api/cascadingRelation"; +import { AutoFillMapping } from "./config"; export function EntitySearchInputComponent({ tableName, @@ -37,6 +38,8 @@ export function EntitySearchInputComponent({ formData, // ๋‹ค์ค‘์„ ํƒ props multiple: multipleProp, + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ props + autoFillMappings: autoFillMappingsProp, // ์ถ”๊ฐ€ props component, isInteractive, @@ -47,6 +50,7 @@ export function EntitySearchInputComponent({ isInteractive?: boolean; onFormDataChange?: (fieldName: string, value: any) => void; webTypeConfig?: any; // ์›นํƒ€์ž… ์„ค์ • (์—ฐ์‡„๊ด€๊ณ„ ๋“ฑ) + autoFillMappings?: AutoFillMapping[]; // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ }) { // uiMode๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด modeProp ์‚ฌ์šฉ, ๊ธฐ๋ณธ๊ฐ’ "combo" const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; @@ -54,6 +58,18 @@ export function EntitySearchInputComponent({ // ๋‹ค์ค‘์„ ํƒ ๋ฐ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • (props > webTypeConfig > componentConfig ์ˆœ์„œ) const config = component?.componentConfig || component?.webTypeConfig || {}; const isMultiple = multipleProp ?? config.multiple ?? false; + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • (props > config) + const autoFillMappings: AutoFillMapping[] = autoFillMappingsProp ?? config.autoFillMappings ?? []; + + // ๋””๋ฒ„๊ทธ: ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • ํ™•์ธ + console.log("๐Ÿ”ง [EntitySearchInput] ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ •:", { + autoFillMappingsProp, + configAutoFillMappings: config.autoFillMappings, + effectiveAutoFillMappings: autoFillMappings, + isInteractive, + hasOnFormDataChange: !!onFormDataChange, + }); // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ์ถ”์ถœ const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; @@ -309,6 +325,23 @@ export function EntitySearchInputComponent({ console.log("๐Ÿ“ค EntitySearchInput -> onFormDataChange:", component.columnName, newValue); } } + + // ๐Ÿ†• ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์ ์šฉ + if (autoFillMappings.length > 0 && isInteractive && onFormDataChange && fullData) { + console.log("๐Ÿ”„ ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์ ์šฉ:", { mappings: autoFillMappings, fullData }); + + for (const mapping of autoFillMappings) { + if (mapping.sourceField && mapping.targetField) { + const sourceValue = fullData[mapping.sourceField]; + if (sourceValue !== undefined) { + onFormDataChange(mapping.targetField, sourceValue); + console.log(` โœ… ${mapping.sourceField} โ†’ ${mapping.targetField}:`, sourceValue); + } else { + console.log(` โš ๏ธ ${mapping.sourceField} ๊ฐ’์ด ์—†์Œ`); + } + } + } + } }; // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ์—์„œ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ œ๊ฑฐ @@ -436,7 +469,7 @@ export function EntitySearchInputComponent({ const isSelected = selectedValues.includes(String(option[valueField])); return ( handleSelectOption(option)} className="text-xs sm:text-sm" @@ -509,7 +542,7 @@ export function EntitySearchInputComponent({ {effectiveOptions.map((option, index) => ( handleSelectOption(option)} className="text-xs sm:text-sm" diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx index fb75daa4..22a52aab 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx @@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react"; // allComponents๋Š” ํ˜„์žฌ ์‚ฌ์šฉ๋˜์ง€ ์•Š์ง€๋งŒ ํ–ฅํ›„ ํ™•์žฅ์„ ์œ„ํ•ด props์— ์œ ์ง€ -import { EntitySearchInputConfig } from "./config"; +import { EntitySearchInputConfig, AutoFillMapping } from "./config"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { tableTypeApi } from "@/lib/api/screen"; import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation"; @@ -236,6 +236,7 @@ export function EntitySearchInputConfigPanel({ const newConfig = { ...localConfig, ...updates }; setLocalConfig(newConfig); onConfigChange(newConfig); + console.log("๐Ÿ“ [EntitySearchInput] ์„ค์ • ์—…๋ฐ์ดํŠธ:", { updates, newConfig }); }; // ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™” @@ -636,9 +637,9 @@ export function EntitySearchInputConfigPanel({ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tableColumns.map((column) => ( + {tableColumns.map((column, idx) => ( { updateConfig({ displayField: column.columnName }); @@ -690,9 +691,9 @@ export function EntitySearchInputConfigPanel({ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tableColumns.map((column) => ( + {tableColumns.map((column, idx) => ( { updateConfig({ valueField: column.columnName }); @@ -812,8 +813,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -860,8 +861,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -919,8 +920,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -939,6 +940,105 @@ export function EntitySearchInputConfigPanel({
)} + + {/* ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • */} +
+
+
+ +

์ž๋™ ์ฑ„์›€ ๋งคํ•‘

+
+ +
+

+ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•˜๋ฉด ์†Œ์Šค ํ•„๋“œ์˜ ๊ฐ’์ด ๋Œ€์ƒ ํ•„๋“œ์— ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. +

+ + {(localConfig.autoFillMappings || []).length > 0 && ( +
+ {(localConfig.autoFillMappings || []).map((mapping, index) => ( +
+ {/* ์†Œ์Šค ํ•„๋“œ (์„ ํƒ๋œ ์—”ํ‹ฐํ‹ฐ) */} +
+ + +
+ + {/* ํ™”์‚ดํ‘œ */} +
+ โ†’ +
+ + {/* ๋Œ€์ƒ ํ•„๋“œ (ํผ) */} +
+ + { + const mappings = [...(localConfig.autoFillMappings || [])]; + mappings[index] = { ...mappings[index], targetField: e.target.value }; + updateConfig({ autoFillMappings: mappings }); + }} + placeholder="ํผ ํ•„๋“œ๋ช…" + className="h-8 text-xs" + /> +
+ + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+ ))} +
+ )} + + {(localConfig.autoFillMappings || []).length === 0 && ( +
+ ๋งคํ•‘์ด ์—†์Šต๋‹ˆ๋‹ค. + ์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +
+ )} +
); } diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx index dd6ed5c4..f8a3a22e 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx @@ -37,6 +37,9 @@ export const EntitySearchInputWrapper: React.FC = ({ // placeholder const placeholder = config.placeholder || widget?.placeholder || "ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”"; + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • + const autoFillMappings = config.autoFillMappings || []; console.log("๐Ÿข EntitySearchInputWrapper ๋ Œ๋”๋ง:", { tableName, @@ -44,6 +47,7 @@ export const EntitySearchInputWrapper: React.FC = ({ valueField, uiMode, multiple, + autoFillMappings, value, config, }); @@ -68,6 +72,7 @@ export const EntitySearchInputWrapper: React.FC = ({ value={value} onChange={onChange} multiple={multiple} + autoFillMappings={autoFillMappings} component={component} isInteractive={props.isInteractive} onFormDataChange={props.onFormDataChange} diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx index 555efe9b..422dfbfa 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx @@ -148,9 +148,9 @@ export function EntitySearchModal({ ์„ ํƒ )} - {displayColumns.map((col) => ( + {displayColumns.map((col, colIdx) => ( {col} @@ -179,7 +179,8 @@ export function EntitySearchModal({ ) : ( results.map((item, index) => { - const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`; + // null๊ณผ undefined ๋ชจ๋‘ ์ฒดํฌํ•˜์—ฌ ์œ ๋‹ˆํฌ ํ‚ค ์ƒ์„ฑ + const uniqueKey = item[valueField] != null ? `${item[valueField]}` : `row-${index}`; const isSelected = isItemSelected(item); return ( )} - {displayColumns.map((col) => ( - + {displayColumns.map((col, colIdx) => ( + {item[col] || "-"} ))} diff --git a/frontend/lib/registry/components/entity-search-input/config.ts b/frontend/lib/registry/components/entity-search-input/config.ts index fab81c9f..3dae8779 100644 --- a/frontend/lib/registry/components/entity-search-input/config.ts +++ b/frontend/lib/registry/components/entity-search-input/config.ts @@ -1,3 +1,9 @@ +// ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ํƒ€์ž… +export interface AutoFillMapping { + sourceField: string; // ์„ ํƒ๋œ ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ (์˜ˆ: customer_name) + targetField: string; // ํผ์˜ ํ•„๋“œ (์˜ˆ: partner_name) +} + export interface EntitySearchInputConfig { tableName: string; displayField: string; @@ -18,5 +24,8 @@ export interface EntitySearchInputConfig { cascadingRelationCode?: string; // ์—ฐ์‡„๊ด€๊ณ„ ์ฝ”๋“œ (WAREHOUSE_LOCATION ๋“ฑ) cascadingRole?: "parent" | "child"; // ์—ญํ•  (๋ถ€๋ชจ/์ž์‹) cascadingParentField?: string; // ๋ถ€๋ชจ ํ•„๋“œ์˜ ์ปฌ๋Ÿผ๋ช… (์ž์‹ ์—ญํ• ์ผ ๋•Œ๋งŒ ์‚ฌ์šฉ) + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • + autoFillMappings?: AutoFillMapping[]; // ์—”ํ‹ฐํ‹ฐ ์„ ํƒ ์‹œ ๋‹ค๋ฅธ ํ•„๋“œ์— ์ž๋™์œผ๋กœ ๊ฐ’ ์ฑ„์šฐ๊ธฐ } From ad8b1791bc6f9ed5dc99dfbdf9d12ccad19da3e8 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Wed, 21 Jan 2026 11:53:51 +0900 Subject: [PATCH 13/57] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EB=A9=94=EB=89=B4=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํ™”๋ฉด ๊ทธ๋ฃน ์ปจํŠธ๋กค๋Ÿฌ ๊ธฐ๋Šฅ ํ™•์žฅ - ๋ฉ”๋‰ด ๋ณต์‚ฌ ์„œ๋น„์Šค ๊ฐœ์„  - ๋ฉ”๋‰ด-ํ™”๋ฉด ๋™๊ธฐํ™” ์„œ๋น„์Šค ์ถ”๊ฐ€ - ๋ฒˆํ˜ธ ๊ทœ์น™ ์„œ๋น„์Šค ๊ฐœ์„  - ํ™”๋ฉด ๊ด€๋ฆฌ ์„œ๋น„์Šค ํ™•์žฅ - CopyScreenModal ๊ธฐ๋Šฅ ๊ฐœ์„  - DataFlowPanel, FieldJoinPanel ์ˆ˜์ • --- .../controllers/numberingRuleController.ts | 37 +- .../src/controllers/screenGroupController.ts | 518 ++++++-- .../controllers/screenManagementController.ts | 261 ++++ .../src/routes/screenManagementRoutes.ts | 24 + backend-node/src/services/menuCopyService.ts | 228 +++- .../src/services/menuScreenSyncService.ts | 1021 +++++++++++++++ .../src/services/numberingRuleService.ts | 205 ++- .../src/services/screenManagementService.ts | 1162 ++++++++++++++++- .../admin/screenMng/screenMngList/page.tsx | 8 +- .../numbering-rule/NumberingRuleDesigner.tsx | 9 +- .../components/screen/CopyScreenModal.tsx | 377 +++++- .../screen/panels/DataFlowPanel.tsx | 2 + .../screen/panels/FieldJoinPanel.tsx | 2 + frontend/lib/api/screen.ts | 13 + frontend/lib/api/screenGroup.ts | 164 +++ 15 files changed, 3895 insertions(+), 136 deletions(-) create mode 100644 backend-node/src/services/menuScreenSyncService.ts diff --git a/backend-node/src/controllers/numberingRuleController.ts b/backend-node/src/controllers/numberingRuleController.ts index ab7114a5..d94cd25a 100644 --- a/backend-node/src/controllers/numberingRuleController.ts +++ b/backend-node/src/controllers/numberingRuleController.ts @@ -169,14 +169,22 @@ router.put("/:ruleId", authenticateToken, async (req: AuthenticatedRequest, res: const { ruleId } = req.params; const updates = req.body; + logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์š”์ฒญ", { ruleId, companyCode, updates }); + try { const updatedRule = await numberingRuleService.updateRule(ruleId, updates, companyCode); + logger.info("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์„ฑ๊ณต", { ruleId, companyCode }); return res.json({ success: true, data: updatedRule }); } catch (error: any) { + logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { + ruleId, + companyCode, + error: error.message, + stack: error.stack + }); if (error.message.includes("์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜")) { return res.status(404).json({ success: false, error: error.message }); } - logger.error("๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { error: error.message }); return res.status(500).json({ success: false, error: error.message }); } }); @@ -257,4 +265,31 @@ router.post("/:ruleId/reset", authenticateToken, async (req: AuthenticatedReques } }); +// ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ (ํ™”๋ฉด ๋ณต์ œ ํ›„ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์™„๋ฃŒ ์ƒํƒœ์—์„œ ํ˜ธ์ถœ) +router.post("/copy-for-company", authenticateToken, async (req: AuthenticatedRequest, res: Response) => { + const userCompanyCode = req.user!.companyCode; + const { sourceCompanyCode, targetCompanyCode } = req.body; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํšŒ์‚ฌ๊ฐ„ ๋ณต์ œ ๊ฐ€๋Šฅ + if (userCompanyCode !== "*") { + return res.status(403).json({ success: false, error: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ํšŒ์‚ฌ๊ฐ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค." }); + } + + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ success: false, error: "์›๋ณธ ํšŒ์‚ฌ ์ฝ”๋“œ์™€ ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค." }); + } + + try { + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹œ์ž‘", { sourceCompanyCode, targetCompanyCode }); + + const result = await numberingRuleService.copyRulesForCompany(sourceCompanyCode, targetCompanyCode); + + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { sourceCompanyCode, targetCompanyCode, result }); + return res.json({ success: true, data: result }); + } catch (error: any) { + logger.error("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ", { error: error.message, sourceCompanyCode, targetCompanyCode }); + return res.status(500).json({ success: false, error: error.message }); + } +}); + export default router; diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index 2d7bc0e1..ffe34a66 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -1,15 +1,17 @@ import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; -import { MultiLangService } from "../services/multilangService"; import { AuthenticatedRequest } from "../types/auth"; +import { + syncScreenGroupsToMenu, + syncMenuToScreenGroups, + getSyncStatus, + syncAllCompanies, +} from "../services/menuScreenSyncService"; // pool ์ธ์Šคํ„ด์Šค ๊ฐ€์ ธ์˜ค๊ธฐ const pool = getPool(); -// ๋‹ค๊ตญ์–ด ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค -const multiLangService = new MultiLangService(); - // ============================================================ // ํ™”๋ฉด ๊ทธ๋ฃน (screen_groups) CRUD // ============================================================ @@ -17,7 +19,7 @@ const multiLangService = new MultiLangService(); // ํ™”๋ฉด ๊ทธ๋ฃน ๋ชฉ๋ก ์กฐํšŒ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { page = 1, size = 20, searchTerm } = req.query; const offset = (parseInt(page as string) - 1) * parseInt(size as string); @@ -92,7 +94,7 @@ export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; let query = ` SELECT sg.*, @@ -137,8 +139,8 @@ export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) = // ํ™”๋ฉด ๊ทธ๋ฃน ์ƒ์„ฑ export const createScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = req.user!.companyCode; - const userId = req.user!.userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body; if (!group_name || !group_code) { @@ -196,47 +198,6 @@ export const createScreenGroup = async (req: AuthenticatedRequest, res: Response // ์—…๋ฐ์ดํŠธ๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ const updatedResult = await pool.query(`SELECT * FROM screen_groups WHERE id = $1`, [newGroupId]); - // ๋‹ค๊ตญ์–ด ์นดํ…Œ๊ณ ๋ฆฌ ์ž๋™ ์ƒ์„ฑ (๊ทธ๋ฃน ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜) - try { - // ๊ทธ๋ฃน ๊ฒฝ๋กœ ์กฐํšŒ (์ƒ์œ„ ๊ทธ๋ฃน โ†’ ํ˜„์žฌ ๊ทธ๋ฃน) - const groupPathResult = await pool.query( - `WITH RECURSIVE group_path AS ( - SELECT id, parent_group_id, group_name, group_level, 1 as depth - FROM screen_groups - WHERE id = $1 - UNION ALL - SELECT g.id, g.parent_group_id, g.group_name, g.group_level, gp.depth + 1 - FROM screen_groups g - INNER JOIN group_path gp ON g.id = gp.parent_group_id - WHERE g.parent_group_id IS NOT NULL - ) - SELECT group_name FROM group_path - ORDER BY depth DESC`, - [newGroupId] - ); - - const groupPath = groupPathResult.rows.map((r: any) => r.group_name); - - // ํšŒ์‚ฌ ์ด๋ฆ„ ์กฐํšŒ - let companyName = "๊ณตํ†ต"; - if (finalCompanyCode !== "*") { - const companyResult = await pool.query( - `SELECT company_name FROM company_mng WHERE company_code = $1`, - [finalCompanyCode] - ); - if (companyResult.rows.length > 0) { - companyName = companyResult.rows[0].company_name; - } - } - - // ๋‹ค๊ตญ์–ด ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ - await multiLangService.ensureScreenGroupCategory(finalCompanyCode, companyName, groupPath); - logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ๋‹ค๊ตญ์–ด ์นดํ…Œ๊ณ ๋ฆฌ ์ž๋™ ์ƒ์„ฑ ์™„๋ฃŒ", { groupPath, companyCode: finalCompanyCode }); - } catch (multilangError: any) { - // ๋‹ค๊ตญ์–ด ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ ์‹คํŒจํ•ด๋„ ๊ทธ๋ฃน ์ƒ์„ฑ์€ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ - logger.warn("ํ™”๋ฉด ๊ทธ๋ฃน ๋‹ค๊ตญ์–ด ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ์„ฑ ์‹คํŒจ (๋ฌด์‹œํ•˜๊ณ  ๊ณ„์†):", multilangError.message); - } - logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์ƒ์„ฑ", { userCompanyCode, finalCompanyCode, groupId: newGroupId, groupName: group_name, parentGroupId: parent_group_id }); res.json({ success: true, data: updatedResult.rows[0], message: "ํ™”๋ฉด ๊ทธ๋ฃน์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); @@ -253,7 +214,7 @@ export const createScreenGroup = async (req: AuthenticatedRequest, res: Response export const updateScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const userCompanyCode = req.user!.companyCode; + const userCompanyCode = req.user?.companyCode || "*"; const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body; // ํšŒ์‚ฌ ์ฝ”๋“œ ๊ฒฐ์ •: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ, ์•„๋‹ˆ๋ฉด ํ˜„์žฌ ๊ทธ๋ฃน์˜ ํšŒ์‚ฌ ์œ ์ง€ @@ -340,10 +301,35 @@ export const updateScreenGroup = async (req: AuthenticatedRequest, res: Response // ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response) => { + const client = await pool.connect(); try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; + await client.query('BEGIN'); + + // 1. ์‚ญ์ œํ•  ๊ทธ๋ฃน๊ณผ ํ•˜์œ„ ๊ทธ๋ฃน ID ์ˆ˜์ง‘ (CASCADE ์‚ญ์ œ ๋Œ€์ƒ) + const childGroupsResult = await client.query(` + WITH RECURSIVE child_groups AS ( + SELECT id FROM screen_groups WHERE id = $1 + UNION ALL + SELECT sg.id FROM screen_groups sg + JOIN child_groups cg ON sg.parent_group_id = cg.id + ) + SELECT id FROM child_groups + `, [id]); + const groupIdsToDelete = childGroupsResult.rows.map((r: any) => r.id); + + // 2. menu_info์—์„œ ์‚ญ์ œ๋  screen_group ์ฐธ์กฐ๋ฅผ NULL๋กœ ์ •๋ฆฌ + if (groupIdsToDelete.length > 0) { + await client.query(` + UPDATE menu_info + SET screen_group_id = NULL + WHERE screen_group_id = ANY($1::int[]) + `, [groupIdsToDelete]); + } + + // 3. screen_groups ์‚ญ์ œ let query = `DELETE FROM screen_groups WHERE id = $1`; const params: any[] = [id]; @@ -354,18 +340,24 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response query += " RETURNING id"; - const result = await pool.query(query, params); + const result = await client.query(query, params); if (result.rows.length === 0) { + await client.query('ROLLBACK'); return res.status(404).json({ success: false, message: "ํ™”๋ฉด ๊ทธ๋ฃน์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค." }); } - logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ", { companyCode, groupId: id }); + await client.query('COMMIT'); + + logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ", { companyCode, groupId: id, cleanedRefs: groupIdsToDelete.length }); res.json({ success: true, message: "ํ™”๋ฉด ๊ทธ๋ฃน์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); } catch (error: any) { + await client.query('ROLLBACK'); logger.error("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ ์‹คํŒจ:", error); res.status(500).json({ success: false, message: "ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", error: error.message }); + } finally { + client.release(); } }; @@ -377,14 +369,19 @@ export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response // ๊ทธ๋ฃน์— ํ™”๋ฉด ์ถ”๊ฐ€ export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; - const { group_id, screen_id, screen_role, display_order, is_default } = req.body; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; + const { group_id, screen_id, screen_role, display_order, is_default, target_company_code } = req.body; if (!group_id || !screen_id) { return res.status(400).json({ success: false, message: "๊ทธ๋ฃน ID์™€ ํ™”๋ฉด ID๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค." }); } + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•  ๋•Œ target_company_code ์‚ฌ์šฉ + const effectiveCompanyCode = (userCompanyCode === "*" && target_company_code) + ? target_company_code + : userCompanyCode; + const query = ` INSERT INTO screen_group_screens (group_id, screen_id, screen_role, display_order, is_default, company_code, writer) VALUES ($1, $2, $3, $4, $5, $6, $7) @@ -396,13 +393,13 @@ export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) screen_role || 'main', display_order || 0, is_default || 'N', - companyCode === "*" ? "*" : companyCode, + effectiveCompanyCode, userId ]; const result = await pool.query(query, params); - logger.info("ํ™”๋ฉด-๊ทธ๋ฃน ์—ฐ๊ฒฐ ์ถ”๊ฐ€", { companyCode, groupId: group_id, screenId: screen_id }); + logger.info("ํ™”๋ฉด-๊ทธ๋ฃน ์—ฐ๊ฒฐ ์ถ”๊ฐ€", { companyCode: effectiveCompanyCode, groupId: group_id, screenId: screen_id }); res.json({ success: true, data: result.rows[0], message: "ํ™”๋ฉด์ด ๊ทธ๋ฃน์— ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); } catch (error: any) { @@ -418,7 +415,7 @@ export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) export const removeScreenFromGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_group_screens WHERE id = $1`; const params: any[] = [id]; @@ -449,7 +446,7 @@ export const removeScreenFromGroup = async (req: AuthenticatedRequest, res: Resp export const updateScreenInGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_role, display_order, is_default } = req.body; let query = ` @@ -487,7 +484,7 @@ export const updateScreenInGroup = async (req: AuthenticatedRequest, res: Respon // ํ™”๋ฉด ํ•„๋“œ ์กฐ์ธ ๋ชฉ๋ก ์กฐํšŒ export const getFieldJoins = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_id } = req.query; let query = ` @@ -528,8 +525,8 @@ export const getFieldJoins = async (req: AuthenticatedRequest, res: Response) => // ํ™”๋ฉด ํ•„๋“œ ์กฐ์ธ ์ƒ์„ฑ export const createFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { screen_id, layout_id, component_id, field_name, save_table, save_column, join_table, join_column, display_column, @@ -570,7 +567,7 @@ export const createFieldJoin = async (req: AuthenticatedRequest, res: Response) export const updateFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { layout_id, component_id, field_name, save_table, save_column, join_table, join_column, display_column, @@ -615,7 +612,7 @@ export const updateFieldJoin = async (req: AuthenticatedRequest, res: Response) export const deleteFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_field_joins WHERE id = $1`; const params: any[] = [id]; @@ -648,7 +645,7 @@ export const deleteFieldJoin = async (req: AuthenticatedRequest, res: Response) // ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋ชฉ๋ก ์กฐํšŒ export const getDataFlows = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, source_screen_id } = req.query; let query = ` @@ -698,8 +695,8 @@ export const getDataFlows = async (req: AuthenticatedRequest, res: Response) => // ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ƒ์„ฑ export const createDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, source_screen_id, source_action, target_screen_id, target_action, data_mapping, flow_type, flow_label, condition_expression, is_active @@ -738,7 +735,7 @@ export const createDataFlow = async (req: AuthenticatedRequest, res: Response) = export const updateDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, source_screen_id, source_action, target_screen_id, target_action, data_mapping, flow_type, flow_label, condition_expression, is_active @@ -781,7 +778,7 @@ export const updateDataFlow = async (req: AuthenticatedRequest, res: Response) = export const deleteDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_data_flows WHERE id = $1`; const params: any[] = [id]; @@ -814,7 +811,7 @@ export const deleteDataFlow = async (req: AuthenticatedRequest, res: Response) = // ํ™”๋ฉด-ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ export const getTableRelations = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_id, group_id } = req.query; let query = ` @@ -863,8 +860,8 @@ export const getTableRelations = async (req: AuthenticatedRequest, res: Response // ํ™”๋ฉด-ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ์ƒ์„ฑ export const createTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = req.user!.companyCode; - const userId = req.user!.userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, screen_id, table_name, relation_type, crud_operations, description, is_active } = req.body; if (!screen_id || !table_name) { @@ -897,7 +894,7 @@ export const createTableRelation = async (req: AuthenticatedRequest, res: Respon export const updateTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, table_name, relation_type, crud_operations, description, is_active } = req.body; let query = ` @@ -932,7 +929,7 @@ export const updateTableRelation = async (req: AuthenticatedRequest, res: Respon export const deleteTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = req.user!.companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_table_relations WHERE id = $1`; const params: any[] = [id]; @@ -962,7 +959,7 @@ export const deleteTableRelation = async (req: AuthenticatedRequest, res: Respon // ============================================================ // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์š”์•ฝ ์กฐํšŒ (์œ„์ ฏ ํƒ€์ž…๋ณ„ ๊ฐœ์ˆ˜, ๋ผ๋ฒจ ๋ชฉ๋ก) -export const getScreenLayoutSummary = async (req: Request, res: Response) => { +export const getScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => { try { const { screenId } = req.params; @@ -1030,7 +1027,7 @@ export const getScreenLayoutSummary = async (req: Request, res: Response) => { }; // ์—ฌ๋Ÿฌ ํ™”๋ฉด์˜ ๋ ˆ์ด์•„์›ƒ ์š”์•ฝ ์ผ๊ด„ ์กฐํšŒ (๋ฏธ๋‹ˆ์–ด์ฒ˜ ๋ Œ๋”๋ง์šฉ ์ขŒํ‘œ ํฌํ•จ) -export const getMultipleScreenLayoutSummary = async (req: Request, res: Response) => { +export const getMultipleScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => { try { const { screenIds } = req.body; @@ -1230,7 +1227,7 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response // ============================================================ // ์—ฌ๋Ÿฌ ํ™”๋ฉด์˜ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ (๋ฉ”์ธ ํ…Œ์ด๋ธ” โ†’ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๊ด€๊ณ„) -export const getScreenSubTables = async (req: Request, res: Response) => { +export const getScreenSubTables = async (req: AuthenticatedRequest, res: Response) => { try { const { screenIds } = req.body; @@ -2060,3 +2057,368 @@ export const getScreenSubTables = async (req: Request, res: Response) => { } }; + +// ============================================================ +// ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” API +// ============================================================ + +/** + * ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” + * screen_groups๋ฅผ menu_info๋กœ ๋™๊ธฐํ™” + */ +export const syncScreenGroupsToMenuController = async (req: AuthenticatedRequest, res: Response) => { + try { + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; + const { targetCompanyCode } = req.body; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ + let companyCode = userCompanyCode; + if (userCompanyCode === "*" && targetCompanyCode) { + companyCode = targetCompanyCode; + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)๋Š” ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ + if (companyCode === "*") { + return res.status(400).json({ + success: false, + message: "๋™๊ธฐํ™”ํ•  ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + }); + } + + logger.info("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์š”์ฒญ", { companyCode, userId }); + + const result = await syncScreenGroupsToMenu(companyCode, userId); + + if (!result.success) { + return res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + errors: result.errors, + }); + } + + res.json({ + success: true, + message: `๋™๊ธฐํ™” ์™„๋ฃŒ: ์ƒ์„ฑ ${result.created}๊ฐœ, ์—ฐ๊ฒฐ ${result.linked}๊ฐœ, ์Šคํ‚ต ${result.skipped}๊ฐœ`, + data: result, + }); + } catch (error: any) { + logger.error("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” + * menu_info๋ฅผ screen_groups๋กœ ๋™๊ธฐํ™” + */ +export const syncMenuToScreenGroupsController = async (req: AuthenticatedRequest, res: Response) => { + try { + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; + const { targetCompanyCode } = req.body; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ + let companyCode = userCompanyCode; + if (userCompanyCode === "*" && targetCompanyCode) { + companyCode = targetCompanyCode; + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)๋Š” ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ + if (companyCode === "*") { + return res.status(400).json({ + success: false, + message: "๋™๊ธฐํ™”ํ•  ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + }); + } + + logger.info("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์š”์ฒญ", { companyCode, userId }); + + const result = await syncMenuToScreenGroups(companyCode, userId); + + if (!result.success) { + return res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + errors: result.errors, + }); + } + + res.json({ + success: true, + message: `๋™๊ธฐํ™” ์™„๋ฃŒ: ์ƒ์„ฑ ${result.created}๊ฐœ, ์—ฐ๊ฒฐ ${result.linked}๊ฐœ, ์Šคํ‚ต ${result.skipped}๊ฐœ`, + data: result, + }); + } catch (error: any) { + logger.error("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ + */ +export const getSyncStatusController = async (req: AuthenticatedRequest, res: Response) => { + try { + const userCompanyCode = req.user?.companyCode || "*"; + const { targetCompanyCode } = req.query; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ + let companyCode = userCompanyCode; + if (userCompanyCode === "*" && targetCompanyCode) { + companyCode = targetCompanyCode as string; + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)๋Š” ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ + if (companyCode === "*") { + return res.status(400).json({ + success: false, + message: "์กฐํšŒํ•  ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + }); + } + + const status = await getSyncStatus(companyCode); + + res.json({ + success: true, + data: status, + }); + } catch (error: any) { + logger.error("๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” + * ๋ชจ๋“  ํšŒ์‚ฌ์— ๋Œ€ํ•ด ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) + */ +export const syncAllCompaniesController = async (req: AuthenticatedRequest, res: Response) => { + try { + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ „์ฒด ๋™๊ธฐํ™” ๊ฐ€๋Šฅ + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ „์ฒด ๋™๊ธฐํ™”๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + logger.info("์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ์š”์ฒญ", { userId }); + + const result = await syncAllCompanies(userId); + + if (!result.success) { + return res.status(500).json({ + success: false, + message: "์ „์ฒด ๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + // ๊ฒฐ๊ณผ ์š”์•ฝ + const totalCreated = result.results.reduce((sum, r) => sum + r.created, 0); + const totalLinked = result.results.reduce((sum, r) => sum + r.linked, 0); + + res.json({ + success: true, + message: `์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ: ${result.totalCompanies}๊ฐœ ํšŒ์‚ฌ ์ค‘ ${result.successCount}๊ฐœ ์„ฑ๊ณต`, + data: { + totalCompanies: result.totalCompanies, + successCount: result.successCount, + failedCount: result.failedCount, + totalCreated, + totalLinked, + details: result.results, + }, + }); + } catch (error: any) { + logger.error("์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "์ „์ฒด ๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * [PoC] screen_groups ๊ธฐ๋ฐ˜ ๋ฉ”๋‰ด ํŠธ๋ฆฌ ์กฐํšŒ + * + * ๊ธฐ์กด menu_info ๋Œ€์‹  screen_groups๋ฅผ ์‚ฌ์ด๋“œ๋ฐ” ๋ฉ”๋‰ด๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ…Œ์ŠคํŠธ API + * - screen_groups๋ฅผ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ๋ฐ˜ํ™˜ + * - ๊ฐ ๊ทธ๋ฃน์— ์—ฐ๊ฒฐ๋œ ๊ธฐ๋ณธ ํ™”๋ฉด์˜ URL ํฌํ•จ + * - menu_objid๋ฅผ ํ†ตํ•ด ๊ถŒํ•œ ์ฒดํฌ ๊ฐ€๋Šฅ + * + * DB ๋ณ€๊ฒฝ ์—†์ด ๋กœ์ง๋งŒ ์ถ”๊ฐ€ + */ +export const getMenuTreeFromScreenGroups = async (req: AuthenticatedRequest, res: Response) => { + try { + const userCompanyCode = req.user?.companyCode || "*"; + const { targetCompanyCode } = req.query; + + // ์กฐํšŒํ•  ํšŒ์‚ฌ ์ฝ”๋“œ ๊ฒฐ์ • + const companyCode = userCompanyCode === "*" && targetCompanyCode + ? String(targetCompanyCode) + : userCompanyCode; + + logger.info("[PoC] screen_groups ๊ธฐ๋ฐ˜ ๋ฉ”๋‰ด ํŠธ๋ฆฌ ์กฐํšŒ", { + userCompanyCode, + targetCompanyCode: companyCode + }); + + // 1. screen_groups ์กฐํšŒ (๊ณ„์ธต ๊ตฌ์กฐ ํฌํ•จ) + const groupsQuery = ` + SELECT + sg.id, + sg.group_name, + sg.group_code, + sg.parent_group_id, + sg.group_level, + sg.display_order, + sg.icon, + sg.is_active, + sg.menu_objid, + sg.company_code, + -- ๊ธฐ๋ณธ ํ™”๋ฉด ์ •๋ณด (URL ์ƒ์„ฑ์šฉ) + ( + SELECT json_build_object( + 'screen_id', sd.screen_id, + 'screen_name', sd.screen_name, + 'screen_code', sd.screen_code + ) + FROM screen_group_screens sgs + JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id + WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code + ORDER BY + CASE WHEN sgs.is_default = 'Y' THEN 0 ELSE 1 END, + sgs.display_order ASC + LIMIT 1 + ) as default_screen, + -- ํ•˜์œ„ ํ™”๋ฉด ๊ฐœ์ˆ˜ + ( + SELECT COUNT(*) + FROM screen_group_screens sgs + WHERE sgs.group_id = sg.id AND sgs.company_code = sg.company_code + ) as screen_count + FROM screen_groups sg + WHERE sg.company_code = $1 + AND (sg.is_active = 'Y' OR sg.is_active IS NULL) + ORDER BY sg.group_level ASC, sg.display_order ASC, sg.group_name ASC + `; + + const groupsResult = await pool.query(groupsQuery, [companyCode]); + + // 2. ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ๋ณ€ํ™˜ + const groups = groupsResult.rows; + const groupMap = new Map(); + const rootGroups: any[] = []; + + // ๋จผ์ € ๋ชจ๋“  ๊ทธ๋ฃน์„ Map์— ์ €์žฅ + for (const group of groups) { + const menuItem = { + id: group.id, + objid: group.menu_objid || group.id, // ๊ถŒํ•œ ์ฒดํฌ์šฉ (menu_objid ์šฐ์„ ) + name: group.group_name, + name_kor: group.group_name, + icon: group.icon, + url: group.default_screen + ? `/screens/${group.default_screen.screen_id}` + : null, + screen_id: group.default_screen?.screen_id || null, + screen_code: group.default_screen?.screen_code || null, + screen_count: parseInt(group.screen_count) || 0, + parent_id: group.parent_group_id, + level: group.group_level || 0, + display_order: group.display_order || 0, + is_active: group.is_active === 'Y', + menu_objid: group.menu_objid, // ๊ธฐ์กด ๊ถŒํ•œ ์‹œ์Šคํ…œ ์—ฐ๊ฒฐ์šฉ + children: [], + // menu_info ํ˜ธํ™˜ ํ•„๋“œ + menu_name_kor: group.group_name, + menu_url: group.default_screen + ? `/screens/${group.default_screen.screen_id}` + : null, + parent_obj_id: null, // ๋‚˜์ค‘์— ์„ค์ • + seq: group.display_order || 0, + status: group.is_active === 'Y' ? 'active' : 'inactive', + }; + + groupMap.set(group.id, menuItem); + } + + // ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„ ์„ค์ • + for (const group of groups) { + const menuItem = groupMap.get(group.id); + + if (group.parent_group_id && groupMap.has(group.parent_group_id)) { + const parent = groupMap.get(group.parent_group_id); + parent.children.push(menuItem); + menuItem.parent_obj_id = parent.objid; + } else { + // ์ตœ์ƒ์œ„ ๊ทธ๋ฃน + rootGroups.push(menuItem); + menuItem.parent_obj_id = "0"; + } + } + + // 3. ํ†ต๊ณ„ ์ •๋ณด + const stats = { + totalGroups: groups.length, + groupsWithScreens: groups.filter(g => g.default_screen).length, + groupsWithMenuObjid: groups.filter(g => g.menu_objid).length, + rootGroups: rootGroups.length, + }; + + logger.info("[PoC] screen_groups ๋ฉ”๋‰ด ํŠธ๋ฆฌ ์ƒ์„ฑ ์™„๋ฃŒ", stats); + + res.json({ + success: true, + message: "[PoC] screen_groups ๊ธฐ๋ฐ˜ ๋ฉ”๋‰ด ํŠธ๋ฆฌ", + data: rootGroups, + stats, + // ํ”Œ๋žซ ๋ฆฌ์ŠคํŠธ๋„ ์ œ๊ณต (๊ธฐ์กด menu_info ํ˜•์‹ ํ˜ธํ™˜) + flatList: Array.from(groupMap.values()).map(item => ({ + objid: String(item.objid), + OBJID: String(item.objid), + menu_name_kor: item.name, + MENU_NAME_KOR: item.name, + menu_url: item.url, + MENU_URL: item.url, + parent_obj_id: String(item.parent_obj_id || "0"), + PARENT_OBJ_ID: String(item.parent_obj_id || "0"), + seq: item.seq, + SEQ: item.seq, + status: item.status, + STATUS: item.status, + menu_type: 1, // ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด + MENU_TYPE: 1, + screen_group_id: item.id, + menu_objid: item.menu_objid, + })), + }); + + } catch (error: any) { + logger.error("[PoC] screen_groups ๋ฉ”๋‰ด ํŠธ๋ฆฌ ์กฐํšŒ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋ฉ”๋‰ด ํŠธ๋ฆฌ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + diff --git a/backend-node/src/controllers/screenManagementController.ts b/backend-node/src/controllers/screenManagementController.ts index 5605031e..4eae31a4 100644 --- a/backend-node/src/controllers/screenManagementController.ts +++ b/backend-node/src/controllers/screenManagementController.ts @@ -834,3 +834,264 @@ export const cleanupDeletedScreenMenuAssignments = async ( }); } }; + +// ๊ทธ๋ฃน ๋ณต์ œ ์™„๋ฃŒ ํ›„ ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId ์ฐธ์กฐ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ +export const updateTabScreenReferences = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { targetScreenIds, screenIdMap } = req.body; + + if (!targetScreenIds || !Array.isArray(targetScreenIds)) { + return res.status(400).json({ + success: false, + message: "targetScreenIds ๋ฐฐ์—ด์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + if (!screenIdMap || typeof screenIdMap !== "object") { + return res.status(400).json({ + success: false, + message: "screenIdMap ๊ฐ์ฒด๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + const result = await screenManagementService.updateTabScreenReferences( + targetScreenIds, + screenIdMap + ); + + return res.json({ + success: true, + message: `${result.updated}๊ฐœ ๋ ˆ์ด์•„์›ƒ์˜ ํƒญ ์ฐธ์กฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`, + updated: result.updated, + details: result.details, + }); + } catch (error) { + console.error("ํƒญ screenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", error); + return res.status(500).json({ + success: false, + message: "ํƒญ screenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; + +// ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ (๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ๋ณต์ œ ์‹œ) +export const copyScreenMenuAssignments = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { sourceCompanyCode, targetCompanyCode, screenIdMap } = req.body; + const userCompanyCode = req.user?.companyCode; + + // ๊ถŒํ•œ ์ฒดํฌ: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ๊ฐ€๋Šฅ + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ + success: false, + message: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + if (!screenIdMap || typeof screenIdMap !== "object") { + return res.status(400).json({ + success: false, + message: "screenIdMap ๊ฐ์ฒด๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + const result = await screenManagementService.copyScreenMenuAssignments( + sourceCompanyCode, + targetCompanyCode, + screenIdMap + ); + + return res.json({ + success: true, + message: `ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ${result.copiedCount}๊ฐœ ๋ณต์ œ ์™„๋ฃŒ`, + data: result, + }); + } catch (error) { + console.error("ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์‹คํŒจ:", error); + return res.status(500).json({ + success: false, + message: "ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; + +// ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์ œ +export const copyCodeCategoryAndCodes = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { sourceCompanyCode, targetCompanyCode } = req.body; + const userCompanyCode = req.user?.companyCode; + + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ + success: false, + message: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + const result = await screenManagementService.copyCodeCategoryAndCodes( + sourceCompanyCode, + targetCompanyCode + ); + + return res.json({ + success: true, + message: `์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ${result.copiedCategories}๊ฐœ, ์ฝ”๋“œ ${result.copiedCodes}๊ฐœ ๋ณต์ œ ์™„๋ฃŒ`, + data: result, + }); + } catch (error) { + console.error("์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์‹คํŒจ:", error); + return res.status(500).json({ + success: false, + message: "์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; + +// ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์ œ +export const copyCategoryMapping = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { sourceCompanyCode, targetCompanyCode } = req.body; + const userCompanyCode = req.user?.companyCode; + + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ + success: false, + message: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + const result = await screenManagementService.copyCategoryMapping( + sourceCompanyCode, + targetCompanyCode + ); + + return res.json({ + success: true, + message: `์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ${result.copiedMappings}๊ฐœ, ๊ฐ’ ${result.copiedValues}๊ฐœ ๋ณต์ œ ์™„๋ฃŒ`, + data: result, + }); + } catch (error) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์‹คํŒจ:", error); + return res.status(500).json({ + success: false, + message: "์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; + +// ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ ์ž…๋ ฅํƒ€์ž… ์„ค์ • ๋ณต์ œ +export const copyTableTypeColumns = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { sourceCompanyCode, targetCompanyCode } = req.body; + const userCompanyCode = req.user?.companyCode; + + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ + success: false, + message: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + const result = await screenManagementService.copyTableTypeColumns( + sourceCompanyCode, + targetCompanyCode + ); + + return res.json({ + success: true, + message: `ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ${result.copiedCount}๊ฐœ ๋ณต์ œ ์™„๋ฃŒ`, + data: result, + }); + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์‹คํŒจ:", error); + return res.status(500).json({ + success: false, + message: "ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; + +// ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ +export const copyCascadingRelation = async ( + req: AuthenticatedRequest, + res: Response +) => { + try { + const { sourceCompanyCode, targetCompanyCode } = req.body; + const userCompanyCode = req.user?.companyCode; + + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + if (!sourceCompanyCode || !targetCompanyCode) { + return res.status(400).json({ + success: false, + message: "sourceCompanyCode์™€ targetCompanyCode๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }); + } + + const result = await screenManagementService.copyCascadingRelation( + sourceCompanyCode, + targetCompanyCode + ); + + return res.json({ + success: true, + message: `์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ${result.copiedCount}๊ฐœ ๋ณต์ œ ์™„๋ฃŒ`, + data: result, + }); + } catch (error) { + console.error("์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์‹คํŒจ:", error); + return res.status(500).json({ + success: false, + message: "์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } +}; diff --git a/backend-node/src/routes/screenManagementRoutes.ts b/backend-node/src/routes/screenManagementRoutes.ts index 67263277..e8dc402a 100644 --- a/backend-node/src/routes/screenManagementRoutes.ts +++ b/backend-node/src/routes/screenManagementRoutes.ts @@ -29,6 +29,12 @@ import { getScreensByMenu, unassignScreenFromMenu, cleanupDeletedScreenMenuAssignments, + updateTabScreenReferences, + copyScreenMenuAssignments, + copyCodeCategoryAndCodes, + copyCategoryMapping, + copyTableTypeColumns, + copyCascadingRelation, } from "../controllers/screenManagementController"; const router = express.Router(); @@ -83,4 +89,22 @@ router.post( cleanupDeletedScreenMenuAssignments ); +// ๊ทธ๋ฃน ๋ณต์ œ ์™„๋ฃŒ ํ›„ ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId ์ฐธ์กฐ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ +router.post("/screens/update-tab-references", updateTabScreenReferences); + +// ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ (๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ๋ณต์ œ ์‹œ) +router.post("/copy-menu-assignments", copyScreenMenuAssignments); + +// ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์ œ +router.post("/copy-code-category", copyCodeCategoryAndCodes); + +// ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์ œ +router.post("/copy-category-mapping", copyCategoryMapping); + +// ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ +router.post("/copy-table-type-columns", copyTableTypeColumns); + +// ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ +router.post("/copy-cascading-relation", copyCascadingRelation); + export default router; diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index a163f30c..1e65cddd 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -16,6 +16,8 @@ export interface MenuCopyResult { copiedCategoryMappings: number; copiedTableTypeColumns: number; // ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ ์ž…๋ ฅํƒ€์ž… ์„ค์ • copiedCascadingRelations: number; // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • + copiedNodeFlows: number; // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ (์ œ์–ด๊ด€๋ฆฌ) + copiedDataflowDiagrams: number; // ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ (๋ฒ„ํŠผ ์ œ์–ด) menuIdMap: Record; screenIdMap: Record; flowIdMap: Record; @@ -983,6 +985,14 @@ export class MenuCopyService { client ); + // === 2.1๋‹จ๊ณ„: ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ๋Š” ํ™”๋ฉด ๋ณต์‚ฌ์—์„œ ์ฒ˜๋ฆฌ === + // (screenManagementService.ts์˜ copyScreen์—์„œ ์ฒ˜๋ฆฌ) + const copiedNodeFlows = 0; + + // === 2.2๋‹จ๊ณ„: ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ๋ณต์‚ฌ๋Š” ํ™”๋ฉด ๋ณต์‚ฌ์—์„œ ์ฒ˜๋ฆฌ === + // (screenManagementService.ts์˜ copyScreen์—์„œ ์ฒ˜๋ฆฌ) + const copiedDataflowDiagrams = 0; + // ๋ณ€์ˆ˜ ์ดˆ๊ธฐํ™” let copiedCodeCategories = 0; let copiedCodes = 0; @@ -1132,6 +1142,8 @@ export class MenuCopyService { copiedCategoryMappings, copiedTableTypeColumns, copiedCascadingRelations, + copiedNodeFlows, + copiedDataflowDiagrams, menuIdMap: Object.fromEntries(menuIdMap), screenIdMap: Object.fromEntries(screenIdMap), flowIdMap: Object.fromEntries(flowIdMap), @@ -1144,6 +1156,8 @@ export class MenuCopyService { - ๋ฉ”๋‰ด: ${result.copiedMenus}๊ฐœ - ํ™”๋ฉด: ${result.copiedScreens}๊ฐœ - ํ”Œ๋กœ์šฐ: ${result.copiedFlows}๊ฐœ + - ๋…ธ๋“œ ํ”Œ๋กœ์šฐ(์ œ์–ด๊ด€๋ฆฌ): ${copiedNodeFlows}๊ฐœ + - ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ(๋ฒ„ํŠผ ์ œ์–ด): ${copiedDataflowDiagrams}๊ฐœ - ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ: ${copiedCodeCategories}๊ฐœ - ์ฝ”๋“œ: ${copiedCodes}๊ฐœ - ์ฑ„๋ฒˆ๊ทœ์น™: ${copiedNumberingRules}๊ฐœ @@ -2556,33 +2570,34 @@ export class MenuCopyService { } // 4. ๋ฐฐ์น˜ INSERT๋กœ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ - if (rulesToCopy.length > 0) { - const ruleValues = rulesToCopy + // menu ์Šค์ฝ”ํ”„์ธ๋ฐ menu_objid ๋งคํ•‘์ด ์—†๋Š” ๊ทœ์น™์€ ์ œ์™ธ (์—ฐ๊ฒฐ ์—†์ด ๋ณต์ œํ•˜์ง€ ์•Š์Œ) + const validRulesToCopy = rulesToCopy.filter((r) => { + if (r.scope_type === "menu") { + const newMenuObjid = menuIdMap.get(r.menu_objid); + if (newMenuObjid === undefined) { + logger.info(` โญ๏ธ ์ฑ„๋ฒˆ๊ทœ์น™ "${r.rule_name}" ๊ฑด๋„ˆ๋œ€: ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ์—†์Œ (์›๋ณธ menu_objid: ${r.menu_objid})`); + // ruleIdMap์—์„œ๋„ ์ œ๊ฑฐ + ruleIdMap.delete(r.rule_id); + return false; // ๋ณต์ œ ๋Œ€์ƒ์—์„œ ์ œ์™ธ + } + } + return true; + }); + + if (validRulesToCopy.length > 0) { + const ruleValues = validRulesToCopy .map( (_, i) => `($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})` ) .join(", "); - const ruleParams = rulesToCopy.flatMap((r) => { + const ruleParams = validRulesToCopy.flatMap((r) => { const newMenuObjid = menuIdMap.get(r.menu_objid); - // scope_type = 'menu'์ธ ๊ฒฝ์šฐ menu_objid๊ฐ€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•จ (check ์ œ์•ฝ์กฐ๊ฑด) - // menuIdMap์— ์—†์œผ๋ฉด ์›๋ณธ menu_objid๊ฐ€ ๋ณต์‚ฌ๋œ ๋ฉ”๋‰ด ๋ฒ”์œ„ ๋ฐ–์ด๋ฏ€๋กœ - // scope_type์„ 'table'๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, ๋งคํ•‘์ด ์—†์œผ๋ฉด null ์ฒ˜๋ฆฌ + // menu ์Šค์ฝ”ํ”„์ธ ๊ฒฝ์šฐ ๋ฐ˜๋“œ์‹œ menu_objid๊ฐ€ ์žˆ์Œ (์œ„์—์„œ ํ•„ํ„ฐ๋ง๋จ) const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null; - // scope_type ๊ฒฐ์ • ๋กœ์ง: - // 1. menu ์Šค์ฝ”ํ”„์ธ๋ฐ menu_objid ๋งคํ•‘์ด ์—†๋Š” ๊ฒฝ์šฐ - // - table_name์ด ์žˆ์œผ๋ฉด 'table' ์Šค์ฝ”ํ”„๋กœ ๋ณ€๊ฒฝ - // - table_name์ด ์—†์œผ๋ฉด 'global' ์Šค์ฝ”ํ”„๋กœ ๋ณ€๊ฒฝ - // 2. ๊ทธ ์™ธ์—๋Š” ์›๋ณธ scope_type ์œ ์ง€ - let finalScopeType = r.scope_type; - if (r.scope_type === "menu" && finalMenuObjid === null) { - if (r.table_name) { - finalScopeType = "table"; // table_name์ด ์žˆ์œผ๋ฉด table ์Šค์ฝ”ํ”„ - } else { - finalScopeType = "global"; // table_name๋„ ์—†์œผ๋ฉด global ์Šค์ฝ”ํ”„ - } - } + // scope_type์€ ์›๋ณธ ์œ ์ง€ (menu ์Šค์ฝ”ํ”„๋Š” ๋ฐ˜๋“œ์‹œ menu_objid๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ) + const finalScopeType = r.scope_type; return [ r.newRuleId, @@ -2610,8 +2625,8 @@ export class MenuCopyService { ruleParams ); - copiedCount = rulesToCopy.length; - logger.info(` โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ${copiedCount}๊ฐœ ๋ณต์‚ฌ`); + copiedCount = validRulesToCopy.length; + logger.info(` โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ${copiedCount}๊ฐœ ๋ณต์‚ฌ (${rulesToCopy.length - validRulesToCopy.length}๊ฐœ ๊ฑด๋„ˆ๋œ€)`); } // 4-1. ๊ธฐ์กด ์ฑ„๋ฒˆ ๊ทœ์น™์˜ menu_objid ์—…๋ฐ์ดํŠธ (์ƒˆ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ) - ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ @@ -3324,4 +3339,175 @@ export class MenuCopyService { logger.info(`โœ… ์—ฐ์‡„๊ด€๊ณ„ ๋ณต์‚ฌ ์™„๋ฃŒ: ${copiedCount}๊ฐœ`); return copiedCount; } + + /** + * ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ (node_flows ํ…Œ์ด๋ธ” - ์ œ์–ด๊ด€๋ฆฌ์—์„œ ์‚ฌ์šฉ) + * - ์›๋ณธ ํšŒ์‚ฌ์˜ ๋ชจ๋“  node_flows๋ฅผ ๋Œ€์ƒ ํšŒ์‚ฌ๋กœ ๋ณต์‚ฌ + * - ๋Œ€์ƒ ํšŒ์‚ฌ์— ๊ฐ™์€ ์ด๋ฆ„์˜ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ๊ฐ€ ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ + * - ์—†์œผ๋ฉด ์ƒˆ๋กœ ๋ณต์‚ฌ (flow_data ํฌํ•จ) + * - ์›๋ณธ ID โ†’ ์ƒˆ ID ๋งคํ•‘ ๋ฐ˜ํ™˜ (๋ฒ„ํŠผ์˜ flowId, selectedDiagramId ๋งคํ•‘์šฉ) + */ + private async copyNodeFlows( + sourceCompanyCode: string, + targetCompanyCode: string, + client: PoolClient + ): Promise<{ copiedCount: number; nodeFlowIdMap: Map }> { + logger.info(`๐Ÿ“‹ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ(์ œ์–ด๊ด€๋ฆฌ) ๋ณต์‚ฌ ์‹œ์ž‘`); + const nodeFlowIdMap = new Map(); + let copiedCount = 0; + + // 1. ์›๋ณธ ํšŒ์‚ฌ์˜ ๋ชจ๋“  node_flows ์กฐํšŒ + const sourceFlowsResult = await client.query( + `SELECT * FROM node_flows WHERE company_code = $1`, + [sourceCompanyCode] + ); + + if (sourceFlowsResult.rows.length === 0) { + logger.info(` ๐Ÿ“ญ ์›๋ณธ ํšŒ์‚ฌ์— ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์—†์Œ`); + return { copiedCount: 0, nodeFlowIdMap }; + } + + logger.info(` ๐Ÿ“‹ ์›๋ณธ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ: ${sourceFlowsResult.rows.length}๊ฐœ`); + + // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๊ธฐ์กด ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์กฐํšŒ (์ด๋ฆ„ ๊ธฐ์ค€) + const existingFlowsResult = await client.query( + `SELECT flow_id, flow_name FROM node_flows WHERE company_code = $1`, + [targetCompanyCode] + ); + const existingFlowsByName = new Map( + existingFlowsResult.rows.map((f) => [f.flow_name, f.flow_id]) + ); + + // 3. ๋ณต์‚ฌํ•  ํ”Œ๋กœ์šฐ ํ•„ํ„ฐ๋ง + ๊ธฐ์กด ํ”Œ๋กœ์šฐ ๋งคํ•‘ + const flowsToCopy: any[] = []; + for (const flow of sourceFlowsResult.rows) { + const existingId = existingFlowsByName.get(flow.flow_name); + if (existingId) { + // ๊ธฐ์กด ํ”Œ๋กœ์šฐ ์žฌ์‚ฌ์šฉ - ID ๋งคํ•‘ ์ถ”๊ฐ€ + nodeFlowIdMap.set(flow.flow_id, existingId); + logger.info(` โ™ป๏ธ ๊ธฐ์กด ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์žฌ์‚ฌ์šฉ: ${flow.flow_name} (${flow.flow_id} โ†’ ${existingId})`); + } else { + flowsToCopy.push(flow); + } + } + + if (flowsToCopy.length === 0) { + logger.info(` ๐Ÿ“ญ ๋ชจ๋“  ๋…ธ๋“œ ํ”Œ๋กœ์šฐ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•จ (๋งคํ•‘ ${nodeFlowIdMap.size}๊ฐœ)`); + return { copiedCount: 0, nodeFlowIdMap }; + } + + logger.info(` ๐Ÿ”„ ๋ณต์‚ฌํ•  ๋…ธ๋“œ ํ”Œ๋กœ์šฐ: ${flowsToCopy.length}๊ฐœ`); + + // 4. ๊ฐœ๋ณ„ INSERT (RETURNING์œผ๋กœ ์ƒˆ ID ํš๋“) + for (const flow of flowsToCopy) { + const insertResult = await client.query( + `INSERT INTO node_flows (flow_name, flow_description, flow_data, company_code) + VALUES ($1, $2, $3, $4) + RETURNING flow_id`, + [ + flow.flow_name, + flow.flow_description, + JSON.stringify(flow.flow_data), + targetCompanyCode, + ] + ); + + const newFlowId = insertResult.rows[0].flow_id; + nodeFlowIdMap.set(flow.flow_id, newFlowId); + logger.info(` โž• ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ: ${flow.flow_name} (${flow.flow_id} โ†’ ${newFlowId})`); + copiedCount++; + } + + logger.info(` โœ… ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ์™„๋ฃŒ: ${copiedCount}๊ฐœ, ๋งคํ•‘ ${nodeFlowIdMap.size}๊ฐœ`); + + return { copiedCount, nodeFlowIdMap }; + } + + /** + * ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ๋ณต์‚ฌ (dataflow_diagrams ํ…Œ์ด๋ธ” - ๋ฒ„ํŠผ ์ œ์–ด ์„ค์ •์—์„œ ์‚ฌ์šฉ) + * - ์›๋ณธ ํšŒ์‚ฌ์˜ ๋ชจ๋“  dataflow_diagrams๋ฅผ ๋Œ€์ƒ ํšŒ์‚ฌ๋กœ ๋ณต์‚ฌ + * - ๋Œ€์ƒ ํšŒ์‚ฌ์— ๊ฐ™์€ ์ด๋ฆ„์˜ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ + * - ์—†์œผ๋ฉด ์ƒˆ๋กœ ๋ณต์‚ฌ (relationships, node_positions, control, plan, category ํฌํ•จ) + * - ์›๋ณธ ID โ†’ ์ƒˆ ID ๋งคํ•‘ ๋ฐ˜ํ™˜ + */ + private async copyDataflowDiagrams( + sourceCompanyCode: string, + targetCompanyCode: string, + userId: string, + client: PoolClient + ): Promise<{ copiedCount: number; diagramIdMap: Map }> { + logger.info(`๐Ÿ“‹ ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ(๋ฒ„ํŠผ ์ œ์–ด) ๋ณต์‚ฌ ์‹œ์ž‘`); + const diagramIdMap = new Map(); + let copiedCount = 0; + + // 1. ์›๋ณธ ํšŒ์‚ฌ์˜ ๋ชจ๋“  dataflow_diagrams ์กฐํšŒ + const sourceDiagramsResult = await client.query( + `SELECT * FROM dataflow_diagrams WHERE company_code = $1`, + [sourceCompanyCode] + ); + + if (sourceDiagramsResult.rows.length === 0) { + logger.info(` ๐Ÿ“ญ ์›๋ณธ ํšŒ์‚ฌ์— ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์—†์Œ`); + return { copiedCount: 0, diagramIdMap }; + } + + logger.info(` ๐Ÿ“‹ ์›๋ณธ ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ: ${sourceDiagramsResult.rows.length}๊ฐœ`); + + // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ ์กฐํšŒ (์ด๋ฆ„ ๊ธฐ์ค€) + const existingDiagramsResult = await client.query( + `SELECT diagram_id, diagram_name FROM dataflow_diagrams WHERE company_code = $1`, + [targetCompanyCode] + ); + const existingDiagramsByName = new Map( + existingDiagramsResult.rows.map((d) => [d.diagram_name, d.diagram_id]) + ); + + // 3. ๋ณต์‚ฌํ•  ๋‹ค์ด์–ด๊ทธ๋žจ ํ•„ํ„ฐ๋ง + ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ ๋งคํ•‘ + const diagramsToCopy: any[] = []; + for (const diagram of sourceDiagramsResult.rows) { + const existingId = existingDiagramsByName.get(diagram.diagram_name); + if (existingId) { + // ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ ์žฌ์‚ฌ์šฉ - ID ๋งคํ•‘ ์ถ”๊ฐ€ + diagramIdMap.set(diagram.diagram_id, existingId); + logger.info(` โ™ป๏ธ ๊ธฐ์กด ๋‹ค์ด์–ด๊ทธ๋žจ ์žฌ์‚ฌ์šฉ: ${diagram.diagram_name} (${diagram.diagram_id} โ†’ ${existingId})`); + } else { + diagramsToCopy.push(diagram); + } + } + + if (diagramsToCopy.length === 0) { + logger.info(` ๐Ÿ“ญ ๋ชจ๋“  ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์ด๋ฏธ ์กด์žฌํ•จ (๋งคํ•‘ ${diagramIdMap.size}๊ฐœ)`); + return { copiedCount: 0, diagramIdMap }; + } + + logger.info(` ๐Ÿ”„ ๋ณต์‚ฌํ•  ๋‹ค์ด์–ด๊ทธ๋žจ: ${diagramsToCopy.length}๊ฐœ`); + + // 4. ๊ฐœ๋ณ„ INSERT (RETURNING์œผ๋กœ ์ƒˆ ID ํš๋“) + for (const diagram of diagramsToCopy) { + const insertResult = await client.query( + `INSERT INTO dataflow_diagrams (diagram_name, relationships, company_code, created_by, node_positions, control, plan, category) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING diagram_id`, + [ + diagram.diagram_name, + JSON.stringify(diagram.relationships), + targetCompanyCode, + userId, + diagram.node_positions ? JSON.stringify(diagram.node_positions) : null, + diagram.control ? JSON.stringify(diagram.control) : null, + diagram.plan ? JSON.stringify(diagram.plan) : null, + diagram.category ? JSON.stringify(diagram.category) : null, + ] + ); + + const newDiagramId = insertResult.rows[0].diagram_id; + diagramIdMap.set(diagram.diagram_id, newDiagramId); + logger.info(` โž• ๋‹ค์ด์–ด๊ทธ๋žจ ๋ณต์‚ฌ: ${diagram.diagram_name} (${diagram.diagram_id} โ†’ ${newDiagramId})`); + copiedCount++; + } + + logger.info(` โœ… ๋ฐ์ดํ„ฐํ”Œ๋กœ์šฐ ๋‹ค์ด์–ด๊ทธ๋žจ ๋ณต์‚ฌ ์™„๋ฃŒ: ${copiedCount}๊ฐœ, ๋งคํ•‘ ${diagramIdMap.size}๊ฐœ`); + + return { copiedCount, diagramIdMap }; + } } diff --git a/backend-node/src/services/menuScreenSyncService.ts b/backend-node/src/services/menuScreenSyncService.ts new file mode 100644 index 00000000..3d581a4a --- /dev/null +++ b/backend-node/src/services/menuScreenSyncService.ts @@ -0,0 +1,1021 @@ +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; + +const pool = getPool(); + +/** + * ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” ์„œ๋น„์Šค + * + * ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™”: + * 1. screen_groups โ†’ menu_info: ํ™”๋ฉด๊ด€๋ฆฌ ํด๋” ๊ตฌ์กฐ๋ฅผ ๋ฉ”๋‰ด๋กœ ๋™๊ธฐํ™” + * 2. menu_info โ†’ screen_groups: ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด๋ฅผ ํ™”๋ฉด๊ด€๋ฆฌ ํด๋”๋กœ ๋™๊ธฐํ™” + */ + +// ============================================================ +// ํƒ€์ž… ์ •์˜ +// ============================================================ + +interface SyncResult { + success: boolean; + created: number; + linked: number; + skipped: number; + errors: string[]; + details: SyncDetail[]; +} + +interface SyncDetail { + action: 'created' | 'linked' | 'skipped' | 'error'; + sourceName: string; + sourceId: number | string; + targetId?: number | string; + reason?: string; +} + +// ============================================================ +// ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” +// ============================================================ + +/** + * screen_groups๋ฅผ menu_info๋กœ ๋™๊ธฐํ™” + * + * ๋กœ์ง: + * 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ screen_groups ์กฐํšŒ (ํด๋” ๊ตฌ์กฐ) + * 2. ์ด๋ฏธ menu_objid๊ฐ€ ์—ฐ๊ฒฐ๋œ ๊ฒƒ์€ ์ œ์™ธ + * 3. ์ด๋ฆ„์œผ๋กœ ๊ธฐ์กด menu_info ๋งค์นญ ์‹œ๋„ + * - ๋งค์นญ๋˜๋ฉด: ์–‘์ชฝ์— ์—ฐ๊ฒฐ ID ์—…๋ฐ์ดํŠธ + * - ๋งค์นญ ์•ˆ๋˜๋ฉด: menu_info์— ์ƒˆ๋กœ ์ƒ์„ฑ + * 4. ๊ณ„์ธต ๊ตฌ์กฐ(parent) ์œ ์ง€ + */ +export async function syncScreenGroupsToMenu( + companyCode: string, + userId: string +): Promise { + const result: SyncResult = { + success: true, + created: 0, + linked: 0, + skipped: 0, + errors: [], + details: [], + }; + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + logger.info("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹œ์ž‘", { companyCode, userId }); + + // 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ screen_groups ์กฐํšŒ (์•„์ง menu_objid๊ฐ€ ์—†๋Š” ๊ฒƒ) + const screenGroupsQuery = ` + SELECT + sg.id, + sg.group_name, + sg.group_code, + sg.parent_group_id, + sg.group_level, + sg.display_order, + sg.description, + sg.icon, + sg.menu_objid, + -- ๋ถ€๋ชจ ๊ทธ๋ฃน์˜ menu_objid๋„ ์กฐํšŒ (๊ณ„์ธต ์—ฐ๊ฒฐ์šฉ) + parent.menu_objid as parent_menu_objid + FROM screen_groups sg + LEFT JOIN screen_groups parent ON sg.parent_group_id = parent.id + WHERE sg.company_code = $1 + ORDER BY sg.group_level ASC, sg.display_order ASC + `; + const screenGroupsResult = await client.query(screenGroupsQuery, [companyCode]); + + // 2. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ๊ธฐ์กด menu_info ์กฐํšŒ (์‚ฌ์šฉ์ž ๋ฉ”๋‰ด, menu_type=1) + // ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์„ ์œ„ํ•ด ๋ถ€๋ชจ ์ด๋ฆ„๋„ ์กฐํšŒ + const existingMenusQuery = ` + SELECT + m.objid, + m.menu_name_kor, + m.parent_obj_id, + m.screen_group_id, + p.menu_name_kor as parent_name + FROM menu_info m + LEFT JOIN menu_info p ON m.parent_obj_id = p.objid + WHERE m.company_code = $1 AND m.menu_type = 1 + `; + const existingMenusResult = await client.query(existingMenusQuery, [companyCode]); + + // ๊ฒฝ๋กœ(๋ถ€๋ชจ์ด๋ฆ„ > ์ด๋ฆ„) โ†’ ๋ฉ”๋‰ด ๋งคํ•‘ (screen_group_id๊ฐ€ ์—†๋Š” ๊ฒƒ๋งŒ) + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งค์นญ๋„ ์œ ์ง€ (ํ•˜์œ„ ํ˜ธํ™˜) + const menuByPath: Map = new Map(); + const menuByName: Map = new Map(); + existingMenusResult.rows.forEach((menu: any) => { + if (!menu.screen_group_id) { + const menuName = menu.menu_name_kor?.trim().toLowerCase() || ''; + const parentName = menu.parent_name?.trim().toLowerCase() || ''; + const pathKey = parentName ? `${parentName}>${menuName}` : menuName; + + menuByPath.set(pathKey, menu); + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งคํ•‘์€ ์ฒซ ๋ฒˆ์งธ ๊ฒƒ๋งŒ (์ค‘๋ณต ๋ฐฉ์ง€) + if (!menuByName.has(menuName)) { + menuByName.set(menuName, menu); + } + } + }); + + // ๋ชจ๋“  ๋ฉ”๋‰ด์˜ objid ์ง‘ํ•ฉ (์‚ญ์ œ ํ™•์ธ์šฉ) + const existingMenuObjids = new Set(existingMenusResult.rows.map((m: any) => Number(m.objid))); + + // 3. ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด์˜ ๋ฃจํŠธ ์ฐพ๊ธฐ (parent_obj_id = 0์ธ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด) + // ์—†์œผ๋ฉด ์ƒ์„ฑ + let userMenuRootObjid: number | null = null; + const rootMenuQuery = ` + SELECT objid FROM menu_info + WHERE company_code = $1 AND menu_type = 1 AND parent_obj_id = 0 + ORDER BY seq ASC + LIMIT 1 + `; + const rootMenuResult = await client.query(rootMenuQuery, [companyCode]); + + if (rootMenuResult.rows.length > 0) { + userMenuRootObjid = Number(rootMenuResult.rows[0].objid); + } else { + // ๋ฃจํŠธ ๋ฉ”๋‰ด๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ + const newObjid = Date.now(); + const createRootQuery = ` + INSERT INTO menu_info (objid, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_type, company_code, writer, regdate, status) + VALUES ($1, 0, '์‚ฌ์šฉ์ž', 'User', 1, 1, $2, $3, NOW(), 'active') + RETURNING objid + `; + const createRootResult = await client.query(createRootQuery, [newObjid, companyCode, userId]); + userMenuRootObjid = Number(createRootResult.rows[0].objid); + logger.info("์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ๋ฃจํŠธ ์ƒ์„ฑ", { companyCode, objid: userMenuRootObjid }); + } + + // 4. screen_groups ID โ†’ menu_objid ๋งคํ•‘ (์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด) + const groupToMenuMap: Map = new Map(); + + // screen_groups์˜ ๋ถ€๋ชจ ์ด๋ฆ„ ์กฐํšŒ๋ฅผ ์œ„ํ•œ ๋งคํ•‘ + const groupIdToName: Map = new Map(); + screenGroupsResult.rows.forEach((g: any) => { + groupIdToName.set(g.id, g.group_name?.trim().toLowerCase() || ''); + }); + + // 5. ์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋” ID ์ฐพ๊ธฐ (level 0, parent_group_id IS NULL) + // ์ด ํด๋”๋Š” ๋ฉ”๋‰ด๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ , ํ•˜์œ„ ํด๋”๋“ค์„ ์‚ฌ์šฉ์ž ๋ฃจํŠธ ๋ฐ”๋กœ ์•„๋ž˜์— ๋ฐฐ์น˜ + const topLevelCompanyFolderIds = new Set(); + for (const group of screenGroupsResult.rows) { + if (group.group_level === 0 && group.parent_group_id === null) { + topLevelCompanyFolderIds.add(group.id); + // ์ตœ์ƒ์œ„ ํด๋” โ†’ ์‚ฌ์šฉ์ž ๋ฃจํŠธ์— ๋งคํ•‘ (ํ•˜์œ„ ํด๋”์˜ ๋ถ€๋ชจ๋กœ ์‚ฌ์šฉ) + groupToMenuMap.set(group.id, userMenuRootObjid!); + logger.info("์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋” ์Šคํ‚ต", { groupId: group.id, groupName: group.group_name }); + } + } + + // 6. ๊ฐ screen_group ์ฒ˜๋ฆฌ + for (const group of screenGroupsResult.rows) { + const groupId = group.id; + const groupName = group.group_name?.trim(); + const groupNameLower = groupName?.toLowerCase() || ''; + + // ์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋”๋Š” ๋ฉ”๋‰ด๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ์Šคํ‚ต + if (topLevelCompanyFolderIds.has(groupId)) { + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: groupName, + sourceId: groupId, + reason: '์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋” (๋ฉ”๋‰ด ์ƒ์„ฑ ์Šคํ‚ต)', + }); + continue; + } + + // ์ด๋ฏธ ์—ฐ๊ฒฐ๋œ ๊ฒฝ์šฐ - ์‹ค์ œ๋กœ ๋ฉ”๋‰ด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + if (group.menu_objid) { + const menuExists = existingMenuObjids.has(Number(group.menu_objid)); + + if (menuExists) { + // ๋ฉ”๋‰ด๊ฐ€ ์กด์žฌํ•˜๋ฉด ์Šคํ‚ต + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: groupName, + sourceId: groupId, + targetId: group.menu_objid, + reason: '์ด๋ฏธ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ๋จ', + }); + groupToMenuMap.set(groupId, Number(group.menu_objid)); + continue; + } else { + // ๋ฉ”๋‰ด๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์œผ๋ฉด ์—ฐ๊ฒฐ ํ•ด์ œํ•˜๊ณ  ์žฌ์ƒ์„ฑ + logger.info("์‚ญ์ œ๋œ ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ํ•ด์ œ", { groupId, deletedMenuObjid: group.menu_objid }); + await client.query( + `UPDATE screen_groups SET menu_objid = NULL, updated_date = NOW() WHERE id = $1`, + [groupId] + ); + // ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ์žฌ์ƒ์„ฑ ๋˜๋Š” ์žฌ์—ฐ๊ฒฐ + } + } + + // ๋ถ€๋ชจ ๊ทธ๋ฃน ์ด๋ฆ„ ์กฐํšŒ (๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์šฉ) + const parentGroupName = group.parent_group_id ? groupIdToName.get(group.parent_group_id) : ''; + const pathKey = parentGroupName ? `${parentGroupName}>${groupNameLower}` : groupNameLower; + + // ๊ฒฝ๋กœ๋กœ ๊ธฐ์กด ๋ฉ”๋‰ด ๋งค์นญ ์‹œ๋„ (์šฐ์„ ์ˆœ์œ„: ๊ฒฝ๋กœ ๋งค์นญ > ์ด๋ฆ„ ๋งค์นญ) + let matchedMenu = menuByPath.get(pathKey); + if (!matchedMenu) { + // ๊ฒฝ๋กœ ๋งค์นญ ์‹คํŒจ์‹œ ์ด๋ฆ„์œผ๋กœ ์‹œ๋„ (ํ•˜์œ„ ํ˜ธํ™˜) + matchedMenu = menuByName.get(groupNameLower); + } + + if (matchedMenu) { + // ๋งค์นญ๋œ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ + const menuObjid = Number(matchedMenu.objid); + + // screen_groups์— menu_objid ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET menu_objid = $1, updated_date = NOW() WHERE id = $2`, + [menuObjid, groupId] + ); + + // menu_info์— screen_group_id ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE menu_info SET screen_group_id = $1 WHERE objid = $2`, + [groupId, menuObjid] + ); + + // ํ•ด๋‹น ๊ทธ๋ฃน์— ์—ฐ๊ฒฐ๋œ ๊ธฐ๋ณธ ํ™”๋ฉด์œผ๋กœ URL ํ•ญ์ƒ ์—…๋ฐ์ดํŠธ (ํ™”๋ฉด ์žฌ์ƒ์„ฑ ์‹œ์—๋„ ๋ฐ˜์˜) + const defaultScreenQuery = ` + SELECT sd.screen_id, sd.screen_code, sd.screen_name + FROM screen_group_screens sgs + JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id + WHERE sgs.group_id = $1 AND sgs.company_code = $2 + ORDER BY + CASE WHEN sgs.is_default = 'Y' THEN 0 ELSE 1 END, + sgs.display_order ASC + LIMIT 1 + `; + const defaultScreenResult = await client.query(defaultScreenQuery, [groupId, companyCode]); + if (defaultScreenResult.rows.length > 0) { + const defaultScreen = defaultScreenResult.rows[0]; + const newMenuUrl = `/screens/${defaultScreen.screen_id}`; + await client.query( + `UPDATE menu_info SET menu_url = $1, screen_code = $2 WHERE objid = $3`, + [newMenuUrl, defaultScreen.screen_code, menuObjid] + ); + logger.info("๋ฉ”๋‰ด URL ์—…๋ฐ์ดํŠธ", { groupName, screenId: defaultScreen.screen_id, menuUrl: newMenuUrl }); + } + + groupToMenuMap.set(groupId, menuObjid); + result.linked++; + result.details.push({ + action: 'linked', + sourceName: groupName, + sourceId: groupId, + targetId: menuObjid, + }); + + // ๋งค์นญ๋œ ๋ฉ”๋‰ด๋Š” Map์—์„œ ์ œ๊ฑฐ (์ค‘๋ณต ๋งค์นญ ๋ฐฉ์ง€) + menuByPath.delete(pathKey); + menuByName.delete(groupNameLower); + + } else { + // ์ƒˆ ๋ฉ”๋‰ด ์ƒ์„ฑ + const newObjid = Date.now() + groupId; // ๊ณ ์œ  ID ๋ณด์žฅ + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด objid ๊ฒฐ์ • + // ์šฐ์„ ์ˆœ์œ„: groupToMenuMap > parent_menu_objid (์กด์žฌ ํ™•์ธ ํ•„์ˆ˜) + let parentMenuObjid = userMenuRootObjid; + if (group.parent_group_id && groupToMenuMap.has(group.parent_group_id)) { + // ํ˜„์žฌ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒ์„ฑ๋œ ๋ถ€๋ชจ ๋ฉ”๋‰ด ์‚ฌ์šฉ + parentMenuObjid = groupToMenuMap.get(group.parent_group_id)!; + } else if (group.parent_group_id && group.parent_menu_objid) { + // ๊ธฐ์กด parent_menu_objid๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + const parentMenuExists = existingMenuObjids.has(Number(group.parent_menu_objid)); + if (parentMenuExists) { + parentMenuObjid = Number(group.parent_menu_objid); + } + } + + // ๊ฐ™์€ ๋ถ€๋ชจ ์•„๋ž˜์—์„œ ๊ฐ€์žฅ ๋†’์€ seq ์กฐํšŒ ํ›„ +1 + let nextSeq = 1; + const maxSeqQuery = ` + SELECT COALESCE(MAX(seq), 0) + 1 as next_seq + FROM menu_info + WHERE parent_obj_id = $1 AND company_code = $2 AND menu_type = 1 + `; + const maxSeqResult = await client.query(maxSeqQuery, [parentMenuObjid, companyCode]); + if (maxSeqResult.rows.length > 0) { + nextSeq = parseInt(maxSeqResult.rows[0].next_seq) || 1; + } + + // ํ•ด๋‹น ๊ทธ๋ฃน์— ์—ฐ๊ฒฐ๋œ ๊ธฐ๋ณธ ํ™”๋ฉด ์กฐํšŒ (is_default = 'Y' ์šฐ์„ , ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ํ™”๋ฉด) + let menuUrl: string | null = null; + let screenCode: string | null = null; + const defaultScreenQuery = ` + SELECT sd.screen_id, sd.screen_code, sd.screen_name + FROM screen_group_screens sgs + JOIN screen_definitions sd ON sgs.screen_id = sd.screen_id + WHERE sgs.group_id = $1 AND sgs.company_code = $2 + ORDER BY + CASE WHEN sgs.is_default = 'Y' THEN 0 ELSE 1 END, + sgs.display_order ASC + LIMIT 1 + `; + const defaultScreenResult = await client.query(defaultScreenQuery, [groupId, companyCode]); + if (defaultScreenResult.rows.length > 0) { + const defaultScreen = defaultScreenResult.rows[0]; + screenCode = defaultScreen.screen_code; + menuUrl = `/screens/${defaultScreen.screen_id}`; + logger.info("๊ธฐ๋ณธ ํ™”๋ฉด URL ์„ค์ •", { groupName, screenId: defaultScreen.screen_id, menuUrl }); + } + + // menu_info์— ์‚ฝ์ž… + const insertMenuQuery = ` + INSERT INTO menu_info ( + objid, parent_obj_id, menu_name_kor, menu_name_eng, + seq, menu_type, company_code, writer, regdate, status, screen_group_id, menu_desc, + menu_url, screen_code + ) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'active', $8, $9, $10, $11) + RETURNING objid + `; + await client.query(insertMenuQuery, [ + newObjid, + parentMenuObjid, + groupName, + group.group_code || groupName, + nextSeq, + companyCode, + userId, + groupId, + group.description || null, + menuUrl, + screenCode, + ]); + + // screen_groups์— menu_objid ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET menu_objid = $1, updated_date = NOW() WHERE id = $2`, + [newObjid, groupId] + ); + + groupToMenuMap.set(groupId, newObjid); + result.created++; + result.details.push({ + action: 'created', + sourceName: groupName, + sourceId: groupId, + targetId: newObjid, + }); + } + } + + await client.query('COMMIT'); + + logger.info("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์™„๋ฃŒ", { + companyCode, + created: result.created, + linked: result.linked, + skipped: result.skipped + }); + + return result; + + } catch (error: any) { + await client.query('ROLLBACK'); + logger.error("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹คํŒจ", { + companyCode, + error: error.message, + stack: error.stack, + code: error.code, + detail: error.detail, + }); + result.success = false; + result.errors.push(error.message); + return result; + } finally { + client.release(); + } +} + + +// ============================================================ +// ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” +// ============================================================ + +/** + * menu_info๋ฅผ screen_groups๋กœ ๋™๊ธฐํ™” + * + * ๋กœ์ง: + * 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด(menu_type=1) ์กฐํšŒ + * 2. ์ด๋ฏธ screen_group_id๊ฐ€ ์—ฐ๊ฒฐ๋œ ๊ฒƒ์€ ์ œ์™ธ + * 3. ์ด๋ฆ„์œผ๋กœ ๊ธฐ์กด screen_groups ๋งค์นญ ์‹œ๋„ + * - ๋งค์นญ๋˜๋ฉด: ์–‘์ชฝ์— ์—ฐ๊ฒฐ ID ์—…๋ฐ์ดํŠธ + * - ๋งค์นญ ์•ˆ๋˜๋ฉด: screen_groups์— ์ƒˆ๋กœ ์ƒ์„ฑ (ํด๋”๋กœ) + * 4. ๊ณ„์ธต ๊ตฌ์กฐ(parent) ์œ ์ง€ + */ +export async function syncMenuToScreenGroups( + companyCode: string, + userId: string +): Promise { + const result: SyncResult = { + success: true, + created: 0, + linked: 0, + skipped: 0, + errors: [], + details: [], + }; + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + logger.info("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์‹œ์ž‘", { companyCode, userId }); + + // 0. ํšŒ์‚ฌ ์ด๋ฆ„ ์กฐํšŒ (ํšŒ์‚ฌ ํด๋” ์ฐพ๊ธฐ/์ƒ์„ฑ์šฉ) + const companyNameQuery = `SELECT company_name FROM company_mng WHERE company_code = $1`; + const companyNameResult = await client.query(companyNameQuery, [companyCode]); + const companyName = companyNameResult.rows[0]?.company_name || companyCode; + + // 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ์กฐํšŒ (menu_type=1) + const menusQuery = ` + SELECT + m.objid, + m.menu_name_kor, + m.menu_name_eng, + m.parent_obj_id, + m.seq, + m.menu_url, + m.menu_desc, + m.screen_group_id, + -- ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ screen_group_id๋„ ์กฐํšŒ (๊ณ„์ธต ์—ฐ๊ฒฐ์šฉ) + parent.screen_group_id as parent_screen_group_id + FROM menu_info m + LEFT JOIN menu_info parent ON m.parent_obj_id = parent.objid + WHERE m.company_code = $1 AND m.menu_type = 1 + ORDER BY + CASE WHEN m.parent_obj_id = 0 THEN 0 ELSE 1 END, + m.parent_obj_id, + m.seq + `; + const menusResult = await client.query(menusQuery, [companyCode]); + + // 2. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ๊ธฐ์กด screen_groups ์กฐํšŒ (๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์„ ์œ„ํ•ด ๋ถ€๋ชจ ์ด๋ฆ„๋„ ์กฐํšŒ) + const existingGroupsQuery = ` + SELECT + g.id, + g.group_name, + g.menu_objid, + g.parent_group_id, + p.group_name as parent_name + FROM screen_groups g + LEFT JOIN screen_groups p ON g.parent_group_id = p.id + WHERE g.company_code = $1 + `; + const existingGroupsResult = await client.query(existingGroupsQuery, [companyCode]); + + // ๊ฒฝ๋กœ(๋ถ€๋ชจ์ด๋ฆ„ > ์ด๋ฆ„) โ†’ ๊ทธ๋ฃน ๋งคํ•‘ (menu_objid๊ฐ€ ์—†๋Š” ๊ฒƒ๋งŒ) + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งค์นญ๋„ ์œ ์ง€ (ํ•˜์œ„ ํ˜ธํ™˜) + const groupByPath: Map = new Map(); + const groupByName: Map = new Map(); + existingGroupsResult.rows.forEach((group: any) => { + if (!group.menu_objid) { + const groupName = group.group_name?.trim().toLowerCase() || ''; + const parentName = group.parent_name?.trim().toLowerCase() || ''; + const pathKey = parentName ? `${parentName}>${groupName}` : groupName; + + groupByPath.set(pathKey, group); + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งคํ•‘์€ ์ฒซ ๋ฒˆ์งธ ๊ฒƒ๋งŒ (์ค‘๋ณต ๋ฐฉ์ง€) + if (!groupByName.has(groupName)) { + groupByName.set(groupName, group); + } + } + }); + + // ๋ชจ๋“  ๊ทธ๋ฃน์˜ id ์ง‘ํ•ฉ (์‚ญ์ œ ํ™•์ธ์šฉ) + const existingGroupIds = new Set(existingGroupsResult.rows.map((g: any) => Number(g.id))); + + // 3. ํšŒ์‚ฌ ํด๋” ์ฐพ๊ธฐ ๋˜๋Š” ์ƒ์„ฑ (๋ฃจํŠธ ๋ ˆ๋ฒจ์— ํšŒ์‚ฌ๋ช…์œผ๋กœ ๋œ ํด๋”) + let companyFolderId: number | null = null; + const companyFolderQuery = ` + SELECT id FROM screen_groups + WHERE company_code = $1 AND parent_group_id IS NULL AND group_level = 0 + ORDER BY id ASC + LIMIT 1 + `; + const companyFolderResult = await client.query(companyFolderQuery, [companyCode]); + + if (companyFolderResult.rows.length > 0) { + companyFolderId = companyFolderResult.rows[0].id; + logger.info("ํšŒ์‚ฌ ํด๋” ๋ฐœ๊ฒฌ", { companyCode, companyFolderId, companyName }); + } else { + // ํšŒ์‚ฌ ํด๋”๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ + // ๋ฃจํŠธ ๋ ˆ๋ฒจ์—์„œ ๊ฐ€์žฅ ๋†’์€ display_order ์กฐํšŒ ํ›„ +1 + let nextRootOrder = 1; + const maxRootOrderQuery = ` + SELECT COALESCE(MAX(display_order), 0) + 1 as next_order + FROM screen_groups + WHERE parent_group_id IS NULL + `; + const maxRootOrderResult = await client.query(maxRootOrderQuery); + if (maxRootOrderResult.rows.length > 0) { + nextRootOrder = parseInt(maxRootOrderResult.rows[0].next_order) || 1; + } + + const createFolderQuery = ` + INSERT INTO screen_groups ( + group_name, group_code, parent_group_id, group_level, + display_order, company_code, writer, hierarchy_path + ) VALUES ($1, $2, NULL, 0, $3, $4, $5, '/') + RETURNING id + `; + const createFolderResult = await client.query(createFolderQuery, [ + companyName, + companyCode.toLowerCase(), + nextRootOrder, + companyCode, + userId, + ]); + companyFolderId = createFolderResult.rows[0].id; + + // hierarchy_path ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET hierarchy_path = $1 WHERE id = $2`, + [`/${companyFolderId}/`, companyFolderId] + ); + + logger.info("ํšŒ์‚ฌ ํด๋” ์ƒ์„ฑ", { companyCode, companyFolderId, companyName }); + } + + // 4. menu_objid โ†’ screen_group_id ๋งคํ•‘ (์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด) + const menuToGroupMap: Map = new Map(); + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ์ค‘ ์ด๋ฏธ screen_group_id๊ฐ€ ์žˆ๋Š” ๊ฒƒ ๋“ฑ๋ก + menusResult.rows.forEach((menu: any) => { + if (menu.screen_group_id) { + menuToGroupMap.set(Number(menu.objid), Number(menu.screen_group_id)); + } + }); + + // ๋ฃจํŠธ ๋ฉ”๋‰ด(parent_obj_id = 0)์˜ objid ์ฐพ๊ธฐ โ†’ ํšŒ์‚ฌ ํด๋”์™€ ๋งคํ•‘ + let rootMenuObjid: number | null = null; + for (const menu of menusResult.rows) { + if (Number(menu.parent_obj_id) === 0) { + rootMenuObjid = Number(menu.objid); + // ๋ฃจํŠธ ๋ฉ”๋‰ด๋Š” ํšŒ์‚ฌ ํด๋”์™€ ์—ฐ๊ฒฐ + if (companyFolderId) { + menuToGroupMap.set(rootMenuObjid, companyFolderId); + } + break; + } + } + + // 5. ๊ฐ ๋ฉ”๋‰ด ์ฒ˜๋ฆฌ + for (const menu of menusResult.rows) { + const menuObjid = Number(menu.objid); + const menuName = menu.menu_name_kor?.trim(); + + // ๋ฃจํŠธ ๋ฉ”๋‰ด(parent_obj_id = 0)๋Š” ์Šคํ‚ต (์ด๋ฏธ ํšŒ์‚ฌ ํด๋”์™€ ๋งคํ•‘๋จ) + if (Number(menu.parent_obj_id) === 0) { + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: menuName, + sourceId: menuObjid, + targetId: companyFolderId || undefined, + reason: '๋ฃจํŠธ ๋ฉ”๋‰ด โ†’ ํšŒ์‚ฌ ํด๋”์™€ ๋งคํ•‘๋จ', + }); + continue; + } + + // ์ด๋ฏธ ์—ฐ๊ฒฐ๋œ ๊ฒฝ์šฐ - ์‹ค์ œ๋กœ ๊ทธ๋ฃน์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + if (menu.screen_group_id) { + const groupExists = existingGroupIds.has(Number(menu.screen_group_id)); + + if (groupExists) { + // ๊ทธ๋ฃน์ด ์กด์žฌํ•˜๋ฉด ์Šคํ‚ต + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: menuName, + sourceId: menuObjid, + targetId: menu.screen_group_id, + reason: '์ด๋ฏธ ํ™”๋ฉด๊ทธ๋ฃน๊ณผ ์—ฐ๊ฒฐ๋จ', + }); + menuToGroupMap.set(menuObjid, Number(menu.screen_group_id)); + continue; + } else { + // ๊ทธ๋ฃน์ด ์‚ญ์ œ๋˜์—ˆ์œผ๋ฉด ์—ฐ๊ฒฐ ํ•ด์ œํ•˜๊ณ  ์žฌ์ƒ์„ฑ + logger.info("์‚ญ์ œ๋œ ๊ทธ๋ฃน ์—ฐ๊ฒฐ ํ•ด์ œ", { menuObjid, deletedGroupId: menu.screen_group_id }); + await client.query( + `UPDATE menu_info SET screen_group_id = NULL WHERE objid = $1`, + [menuObjid] + ); + // ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ์žฌ์ƒ์„ฑ ๋˜๋Š” ์žฌ์—ฐ๊ฒฐ + } + } + + const menuNameLower = menuName?.toLowerCase() || ''; + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ์ด๋ฆ„ ์กฐํšŒ (๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์šฉ) + const parentMenu = menusResult.rows.find((m: any) => Number(m.objid) === Number(menu.parent_obj_id)); + const parentMenuName = parentMenu?.menu_name_kor?.trim().toLowerCase() || ''; + const pathKey = parentMenuName ? `${parentMenuName}>${menuNameLower}` : menuNameLower; + + // ๊ฒฝ๋กœ๋กœ ๊ธฐ์กด ๊ทธ๋ฃน ๋งค์นญ ์‹œ๋„ (์šฐ์„ ์ˆœ์œ„: ๊ฒฝ๋กœ ๋งค์นญ > ์ด๋ฆ„ ๋งค์นญ) + let matchedGroup = groupByPath.get(pathKey); + if (!matchedGroup) { + // ๊ฒฝ๋กœ ๋งค์นญ ์‹คํŒจ์‹œ ์ด๋ฆ„์œผ๋กœ ์‹œ๋„ (ํ•˜์œ„ ํ˜ธํ™˜) + matchedGroup = groupByName.get(menuNameLower); + } + + if (matchedGroup) { + // ๋งค์นญ๋œ ๊ทธ๋ฃน๊ณผ ์—ฐ๊ฒฐ + const groupId = Number(matchedGroup.id); + + try { + // menu_info์— screen_group_id ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE menu_info SET screen_group_id = $1 WHERE objid = $2`, + [groupId, menuObjid] + ); + + // screen_groups์— menu_objid ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET menu_objid = $1, updated_date = NOW() WHERE id = $2`, + [menuObjid, groupId] + ); + + menuToGroupMap.set(menuObjid, groupId); + result.linked++; + result.details.push({ + action: 'linked', + sourceName: menuName, + sourceId: menuObjid, + targetId: groupId, + }); + + // ๋งค์นญ๋œ ๊ทธ๋ฃน์€ Map์—์„œ ์ œ๊ฑฐ (์ค‘๋ณต ๋งค์นญ ๋ฐฉ์ง€) + groupByPath.delete(pathKey); + groupByName.delete(menuNameLower); + } catch (linkError: any) { + logger.error("๊ทธ๋ฃน ์—ฐ๊ฒฐ ์ค‘ ์—๋Ÿฌ", { menuName, menuObjid, groupId, error: linkError.message, stack: linkError.stack }); + throw linkError; + } + + } else { + // ์ƒˆ screen_group ์ƒ์„ฑ + // ๋ถ€๋ชจ ๊ทธ๋ฃน ID ๊ฒฐ์ • + let parentGroupId: number | null = null; + let groupLevel = 1; // ๊ธฐ๋ณธ๊ฐ’์€ 1 (ํšŒ์‚ฌ ํด๋” ์•„๋ž˜) + + // ์šฐ์„ ์ˆœ์œ„ 1: menuToGroupMap์—์„œ ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ ์ƒˆ ๊ทธ๋ฃน ID ์กฐํšŒ (๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒ์„ฑ๋œ ๊ฒƒ) + if (menuToGroupMap.has(Number(menu.parent_obj_id))) { + parentGroupId = menuToGroupMap.get(Number(menu.parent_obj_id))!; + } + // ์šฐ์„ ์ˆœ์œ„ 2: ๋ถ€๋ชจ ๋ฉ”๋‰ด๊ฐ€ ๋ฃจํŠธ ๋ฉ”๋‰ด๋ฉด ํšŒ์‚ฌ ํด๋” ์‚ฌ์šฉ + else if (Number(menu.parent_obj_id) === rootMenuObjid) { + parentGroupId = companyFolderId; + } + // ์šฐ์„ ์ˆœ์œ„ 3: ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ screen_group_id๊ฐ€ ์žˆ๊ณ , ํ•ด๋‹น ๊ทธ๋ฃน์ด ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋ฉด ์‚ฌ์šฉ + else if (menu.parent_screen_group_id && existingGroupIds.has(Number(menu.parent_screen_group_id))) { + parentGroupId = Number(menu.parent_screen_group_id); + } + + // ๋ถ€๋ชจ ๊ทธ๋ฃน์˜ ๋ ˆ๋ฒจ ์กฐํšŒ + if (parentGroupId) { + const parentLevelQuery = `SELECT group_level FROM screen_groups WHERE id = $1`; + const parentLevelResult = await client.query(parentLevelQuery, [parentGroupId]); + if (parentLevelResult.rows.length > 0) { + groupLevel = (parentLevelResult.rows[0].group_level || 0) + 1; + } + } + + // ๊ฐ™์€ ๋ถ€๋ชจ ์•„๋ž˜์—์„œ ๊ฐ€์žฅ ๋†’์€ display_order ์กฐํšŒ ํ›„ +1 + let nextDisplayOrder = 1; + const maxOrderQuery = parentGroupId + ? `SELECT COALESCE(MAX(display_order), 0) + 1 as next_order FROM screen_groups WHERE parent_group_id = $1 AND company_code = $2` + : `SELECT COALESCE(MAX(display_order), 0) + 1 as next_order FROM screen_groups WHERE parent_group_id IS NULL AND company_code = $1`; + const maxOrderParams = parentGroupId ? [parentGroupId, companyCode] : [companyCode]; + const maxOrderResult = await client.query(maxOrderQuery, maxOrderParams); + if (maxOrderResult.rows.length > 0) { + nextDisplayOrder = parseInt(maxOrderResult.rows[0].next_order) || 1; + } + + // group_code ์ƒ์„ฑ (์˜๋ฌธ๋ช… ๋˜๋Š” ์ด๋ฆ„ ๊ธฐ๋ฐ˜) + const groupCode = (menu.menu_name_eng || menuName || 'group') + .replace(/\s+/g, '_') + .toLowerCase() + .substring(0, 50); + + // screen_groups์— ์‚ฝ์ž… + const insertGroupQuery = ` + INSERT INTO screen_groups ( + group_name, group_code, parent_group_id, group_level, + display_order, company_code, writer, menu_objid, description + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id + `; + + let newGroupId: number; + try { + logger.info("์ƒˆ ๊ทธ๋ฃน ์ƒ์„ฑ ์‹œ๋„", { + menuName, + menuObjid, + groupCode: groupCode + '_' + menuObjid, + parentGroupId, + groupLevel, + nextDisplayOrder, + companyCode, + }); + + const insertResult = await client.query(insertGroupQuery, [ + menuName, + groupCode + '_' + menuObjid, // ๊ณ ์œ ์„ฑ ๋ณด์žฅ + parentGroupId, + groupLevel, + nextDisplayOrder, + companyCode, + userId, + menuObjid, + menu.menu_desc || null, + ]); + + newGroupId = insertResult.rows[0].id; + } catch (insertError: any) { + logger.error("๊ทธ๋ฃน ์ƒ์„ฑ ์ค‘ ์—๋Ÿฌ", { + menuName, + menuObjid, + parentGroupId, + groupLevel, + error: insertError.message, + stack: insertError.stack, + code: insertError.code, + detail: insertError.detail, + }); + throw insertError; + } + + // hierarchy_path ์—…๋ฐ์ดํŠธ + let hierarchyPath = `/${newGroupId}/`; + if (parentGroupId) { + const parentPathQuery = `SELECT hierarchy_path FROM screen_groups WHERE id = $1`; + const parentPathResult = await client.query(parentPathQuery, [parentGroupId]); + if (parentPathResult.rows.length > 0 && parentPathResult.rows[0].hierarchy_path) { + hierarchyPath = `${parentPathResult.rows[0].hierarchy_path}${newGroupId}/`.replace('//', '/'); + } + } + await client.query( + `UPDATE screen_groups SET hierarchy_path = $1 WHERE id = $2`, + [hierarchyPath, newGroupId] + ); + + // menu_info์— screen_group_id ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE menu_info SET screen_group_id = $1 WHERE objid = $2`, + [newGroupId, menuObjid] + ); + + menuToGroupMap.set(menuObjid, newGroupId); + result.created++; + result.details.push({ + action: 'created', + sourceName: menuName, + sourceId: menuObjid, + targetId: newGroupId, + }); + } + } + + await client.query('COMMIT'); + + logger.info("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์™„๋ฃŒ", { + companyCode, + created: result.created, + linked: result.linked, + skipped: result.skipped + }); + + return result; + + } catch (error: any) { + await client.query('ROLLBACK'); + logger.error("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์‹คํŒจ", { + companyCode, + error: error.message, + stack: error.stack, + code: error.code, + detail: error.detail, + }); + result.success = false; + result.errors.push(error.message); + return result; + } finally { + client.release(); + } +} + + +// ============================================================ +// ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ +// ============================================================ + +/** + * ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ + * + * - ์—ฐ๊ฒฐ๋œ ํ•ญ๋ชฉ ์ˆ˜ + * - ์—ฐ๊ฒฐ ์•ˆ ๋œ ํ•ญ๋ชฉ ์ˆ˜ + * - ์–‘๋ฐฉํ–ฅ ๋น„๊ต + */ +export async function getSyncStatus(companyCode: string): Promise<{ + screenGroups: { total: number; linked: number; unlinked: number }; + menuItems: { total: number; linked: number; unlinked: number }; + potentialMatches: Array<{ menuName: string; groupName: string; similarity: string }>; +}> { + // screen_groups ์ƒํƒœ + const sgQuery = ` + SELECT + COUNT(*) as total, + COUNT(menu_objid) as linked + FROM screen_groups + WHERE company_code = $1 + `; + const sgResult = await pool.query(sgQuery, [companyCode]); + + // menu_info ์ƒํƒœ (์‚ฌ์šฉ์ž ๋ฉ”๋‰ด๋งŒ, ๋ฃจํŠธ ์ œ์™ธ) + const menuQuery = ` + SELECT + COUNT(*) as total, + COUNT(screen_group_id) as linked + FROM menu_info + WHERE company_code = $1 AND menu_type = 1 AND parent_obj_id != 0 + `; + const menuResult = await pool.query(menuQuery, [companyCode]); + + // ์ด๋ฆ„์ด ๊ฐ™์€ ์ž ์žฌ์  ๋งค์นญ ํ›„๋ณด ์กฐํšŒ + const matchQuery = ` + SELECT + m.menu_name_kor as menu_name, + sg.group_name + FROM menu_info m + JOIN screen_groups sg ON LOWER(TRIM(m.menu_name_kor)) = LOWER(TRIM(sg.group_name)) + WHERE m.company_code = $1 + AND sg.company_code = $1 + AND m.menu_type = 1 + AND m.screen_group_id IS NULL + AND sg.menu_objid IS NULL + LIMIT 10 + `; + const matchResult = await pool.query(matchQuery, [companyCode]); + + const sgTotal = parseInt(sgResult.rows[0].total); + const sgLinked = parseInt(sgResult.rows[0].linked); + const menuTotal = parseInt(menuResult.rows[0].total); + const menuLinked = parseInt(menuResult.rows[0].linked); + + return { + screenGroups: { + total: sgTotal, + linked: sgLinked, + unlinked: sgTotal - sgLinked, + }, + menuItems: { + total: menuTotal, + linked: menuLinked, + unlinked: menuTotal - menuLinked, + }, + potentialMatches: matchResult.rows.map((row: any) => ({ + menuName: row.menu_name, + groupName: row.group_name, + similarity: 'exact', + })), + }; +} + + +// ============================================================ +// ์ „์ฒด ๋™๊ธฐํ™” (๋ชจ๋“  ํšŒ์‚ฌ) +// ============================================================ + +interface AllCompaniesSyncResult { + success: boolean; + totalCompanies: number; + successCount: number; + failedCount: number; + results: Array<{ + companyCode: string; + companyName: string; + direction: 'screens-to-menus' | 'menus-to-screens'; + created: number; + linked: number; + skipped: number; + success: boolean; + error?: string; + }>; +} + +/** + * ๋ชจ๋“  ํšŒ์‚ฌ์— ๋Œ€ํ•ด ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ + * + * ๋กœ์ง: + * 1. ๋ชจ๋“  ํšŒ์‚ฌ ์กฐํšŒ + * 2. ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ + * - ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” + * - ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” + * 3. ๊ฒฐ๊ณผ ์ง‘๊ณ„ + */ +export async function syncAllCompanies( + userId: string +): Promise { + const result: AllCompaniesSyncResult = { + success: true, + totalCompanies: 0, + successCount: 0, + failedCount: 0, + results: [], + }; + + try { + logger.info("์ „์ฒด ๋™๊ธฐํ™” ์‹œ์ž‘", { userId }); + + // ๋ชจ๋“  ํšŒ์‚ฌ ์กฐํšŒ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ํšŒ์‚ฌ ์ œ์™ธ) + const companiesQuery = ` + SELECT company_code, company_name + FROM company_mng + WHERE company_code != '*' + ORDER BY company_name + `; + const companiesResult = await pool.query(companiesQuery); + + result.totalCompanies = companiesResult.rows.length; + + // ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” + for (const company of companiesResult.rows) { + const companyCode = company.company_code; + const companyName = company.company_name; + + try { + // 1. ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” + const screensToMenusResult = await syncScreenGroupsToMenu(companyCode, userId); + result.results.push({ + companyCode, + companyName, + direction: 'screens-to-menus', + created: screensToMenusResult.created, + linked: screensToMenusResult.linked, + skipped: screensToMenusResult.skipped, + success: screensToMenusResult.success, + error: screensToMenusResult.errors.length > 0 ? screensToMenusResult.errors.join(', ') : undefined, + }); + + // 2. ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” + const menusToScreensResult = await syncMenuToScreenGroups(companyCode, userId); + result.results.push({ + companyCode, + companyName, + direction: 'menus-to-screens', + created: menusToScreensResult.created, + linked: menusToScreensResult.linked, + skipped: menusToScreensResult.skipped, + success: menusToScreensResult.success, + error: menusToScreensResult.errors.length > 0 ? menusToScreensResult.errors.join(', ') : undefined, + }); + + if (screensToMenusResult.success && menusToScreensResult.success) { + result.successCount++; + } else { + result.failedCount++; + } + + } catch (error: any) { + logger.error("ํšŒ์‚ฌ ๋™๊ธฐํ™” ์‹คํŒจ", { companyCode, companyName, error: error.message }); + result.results.push({ + companyCode, + companyName, + direction: 'screens-to-menus', + created: 0, + linked: 0, + skipped: 0, + success: false, + error: error.message, + }); + result.failedCount++; + } + } + + logger.info("์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ", { + totalCompanies: result.totalCompanies, + successCount: result.successCount, + failedCount: result.failedCount, + }); + + return result; + + } catch (error: any) { + logger.error("์ „์ฒด ๋™๊ธฐํ™” ์‹คํŒจ", { error: error.message }); + result.success = false; + return result; + } +} + diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 8208ecc5..381fc907 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -854,7 +854,13 @@ class NumberingRuleService { return { ...ruleResult.rows[0], parts }; } catch (error: any) { await client.query("ROLLBACK"); - logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { error: error.message }); + logger.error("์ฑ„๋ฒˆ ๊ทœ์น™ ์ˆ˜์ • ์‹คํŒจ", { + ruleId, + companyCode, + error: error.message, + stack: error.stack, + updates + }); throw error; } finally { client.release(); @@ -1062,6 +1068,203 @@ class NumberingRuleService { ); logger.info("์‹œํ€€์Šค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ", { ruleId, companyCode }); } + + /** + * ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ (๋ฉ”๋‰ด ๋™๊ธฐํ™” ์™„๋ฃŒ ํ›„ ํ˜ธ์ถœ) + * ๋ฉ”๋‰ด ์ด๋ฆ„์„ ๊ธฐ์ค€์œผ๋กœ ์ฑ„๋ฒˆ๊ทœ์น™์„ ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๋ฉ”๋‰ด์— ์—ฐ๊ฒฐ + * ๋ณต์ œ ํ›„ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์˜ numberingRuleId ์ฐธ์กฐ๋„ ์—…๋ฐ์ดํŠธ + */ + async copyRulesForCompany( + sourceCompanyCode: string, + targetCompanyCode: string + ): Promise<{ copiedCount: number; skippedCount: number; details: string[]; ruleIdMap: Record }> { + const pool = getPool(); + const client = await pool.connect(); + + const result = { copiedCount: 0, skippedCount: 0, details: [] as string[], ruleIdMap: {} as Record }; + + try { + await client.query("BEGIN"); + + // 1. ์›๋ณธ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ (menu + table ์Šค์ฝ”ํ”„ ๋ชจ๋‘) + const sourceRulesResult = await client.query( + `SELECT nr.*, mi.menu_name_kor as source_menu_name + FROM numbering_rules nr + LEFT JOIN menu_info mi ON nr.menu_objid = mi.objid + WHERE nr.company_code = $1 AND nr.scope_type IN ('menu', 'table')`, + [sourceCompanyCode] + ); + + logger.info("์›๋ณธ ์ฑ„๋ฒˆ๊ทœ์น™ ์กฐํšŒ", { + sourceCompanyCode, + count: sourceRulesResult.rowCount + }); + + // 2. ๊ฐ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ + for (const rule of sourceRulesResult.rows) { + // ์ƒˆ rule_id ์ƒ์„ฑ + const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // ์ด๋ฏธ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ (์ด๋ฆ„ ๊ธฐ๋ฐ˜) + const existsCheck = await client.query( + `SELECT rule_id FROM numbering_rules + WHERE company_code = $1 AND rule_name = $2`, + [targetCompanyCode, rule.rule_name] + ); + + if (existsCheck.rows.length > 0) { + // ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ๋งคํ•‘๋งŒ ์ถ”๊ฐ€ + result.ruleIdMap[rule.rule_id] = existsCheck.rows[0].rule_id; + result.skippedCount++; + result.details.push(`๊ฑด๋„ˆ๋œ€ (์ด๋ฏธ ์กด์žฌ): ${rule.rule_name}`); + continue; + } + + let targetMenuObjid = null; + + // menu ์Šค์ฝ”ํ”„์ธ ๊ฒฝ์šฐ ๋Œ€์ƒ ๋ฉ”๋‰ด ์ฐพ๊ธฐ + if (rule.scope_type === 'menu' && rule.source_menu_name) { + const targetMenuResult = await client.query( + `SELECT objid FROM menu_info + WHERE company_code = $1 AND menu_name_kor = $2 + LIMIT 1`, + [targetCompanyCode, rule.source_menu_name] + ); + + if (targetMenuResult.rows.length === 0) { + result.skippedCount++; + result.details.push(`๊ฑด๋„ˆ๋œ€ (๋ฉ”๋‰ด ์—†์Œ): ${rule.rule_name} - ๋ฉ”๋‰ด: ${rule.source_menu_name}`); + continue; + } + + targetMenuObjid = targetMenuResult.rows[0].objid; + } + + // ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ + await client.query( + `INSERT INTO numbering_rules ( + rule_id, rule_name, description, separator, reset_period, + current_sequence, table_name, column_name, company_code, + created_at, updated_at, created_by, scope_type, menu_objid + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), $10, $11, $12)`, + [ + newRuleId, + rule.rule_name, + rule.description, + rule.separator, + rule.reset_period, + 0, // ์‹œํ€€์Šค ์ดˆ๊ธฐํ™” + rule.table_name, + rule.column_name, + targetCompanyCode, + rule.created_by, + rule.scope_type, + targetMenuObjid, + ] + ); + + // ์ฑ„๋ฒˆ๊ทœ์น™ ํŒŒํŠธ ๋ณต์ œ + const partsResult = await client.query( + `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`, + [rule.rule_id] + ); + + for (const part of partsResult.rows) { + await client.query( + `INSERT INTO numbering_rule_parts ( + rule_id, part_order, part_type, generation_method, + auto_config, manual_config, company_code, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`, + [ + newRuleId, + part.part_order, + part.part_type, + part.generation_method, + part.auto_config ? JSON.stringify(part.auto_config) : null, + part.manual_config ? JSON.stringify(part.manual_config) : null, + targetCompanyCode, + ] + ); + } + + // ๋งคํ•‘ ์ถ”๊ฐ€ + result.ruleIdMap[rule.rule_id] = newRuleId; + result.copiedCount++; + result.details.push(`๋ณต์ œ ์™„๋ฃŒ: ${rule.rule_name} (${rule.scope_type})`); + logger.info("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { + ruleName: rule.rule_name, + oldRuleId: rule.rule_id, + newRuleId, + targetMenuObjid + }); + } + + // 3. ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์˜ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + if (Object.keys(result.ruleIdMap).length > 0) { + logger.info("ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘", { + targetCompanyCode, + mappingCount: Object.keys(result.ruleIdMap).length + }); + + // ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๋ชจ๋“  ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ + const layoutsResult = await client.query( + `SELECT sl.layout_id, sl.properties + FROM screen_layouts sl + JOIN screen_definitions sd ON sl.screen_id = sd.screen_id + WHERE sd.company_code = $1 + AND sl.properties::text LIKE '%numberingRuleId%'`, + [targetCompanyCode] + ); + + let updatedLayouts = 0; + + for (const layout of layoutsResult.rows) { + let propsStr = JSON.stringify(layout.properties); + let updated = false; + + // ๊ฐ ๋งคํ•‘์— ๋Œ€ํ•ด ์น˜ํ™˜ + for (const [oldRuleId, newRuleId] of Object.entries(result.ruleIdMap)) { + if (propsStr.includes(`"${oldRuleId}"`)) { + propsStr = propsStr.split(`"${oldRuleId}"`).join(`"${newRuleId}"`); + updated = true; + } + } + + if (updated) { + await client.query( + `UPDATE screen_layouts SET properties = $1::jsonb WHERE layout_id = $2`, + [propsStr, layout.layout_id] + ); + updatedLayouts++; + } + } + + logger.info("ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ numberingRuleId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ", { + targetCompanyCode, + updatedLayouts + }); + result.details.push(`ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ${updatedLayouts}๊ฐœ์˜ ์ฑ„๋ฒˆ๊ทœ์น™ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ`); + } + + await client.query("COMMIT"); + + logger.info("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ", { + sourceCompanyCode, + targetCompanyCode, + copiedCount: result.copiedCount, + skippedCount: result.skippedCount, + ruleIdMapCount: Object.keys(result.ruleIdMap).length + }); + + return result; + } catch (error) { + await client.query("ROLLBACK"); + logger.error("ํšŒ์‚ฌ๋ณ„ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ", { error, sourceCompanyCode, targetCompanyCode }); + throw error; + } finally { + client.release(); + } + } } export const numberingRuleService = new NumberingRuleService(); diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 783e83c0..15c0e1f5 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -2549,6 +2549,730 @@ export class ScreenManagementService { })); } + /** + * ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” numberingRuleId ์ˆ˜์ง‘ + * - componentConfig.autoGeneration.options.numberingRuleId (text-input) + * - componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal) + * - componentConfig.action.excelNumberingRuleId (์—‘์…€ ์—…๋กœ๋“œ) + */ + private collectNumberingRuleIdsFromLayouts(layouts: any[]): Set { + const ruleIds = new Set(); + + for (const layout of layouts) { + const props = layout.properties; + if (!props) continue; + + // 1. componentConfig.autoGeneration.options.numberingRuleId (text-input ์ปดํฌ๋„ŒํŠธ) + const autoGenRuleId = props?.componentConfig?.autoGeneration?.options?.numberingRuleId; + if (autoGenRuleId && typeof autoGenRuleId === 'string' && autoGenRuleId.startsWith('rule-')) { + ruleIds.add(autoGenRuleId); + } + + // 2. componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal) + const sections = props?.componentConfig?.sections; + if (Array.isArray(sections)) { + for (const section of sections) { + const fields = section?.fields; + if (Array.isArray(fields)) { + for (const field of fields) { + const ruleId = field?.numberingRule?.ruleId; + if (ruleId && typeof ruleId === 'string' && ruleId.startsWith('rule-')) { + ruleIds.add(ruleId); + } + } + } + // optionalFieldGroups ๋‚ด๋ถ€์˜ ํ•„๋“œ๋“ค๋„ ํ™•์ธ + const optGroups = section?.optionalFieldGroups; + if (Array.isArray(optGroups)) { + for (const optGroup of optGroups) { + const optFields = optGroup?.fields; + if (Array.isArray(optFields)) { + for (const field of optFields) { + const ruleId = field?.numberingRule?.ruleId; + if (ruleId && typeof ruleId === 'string' && ruleId.startsWith('rule-')) { + ruleIds.add(ruleId); + } + } + } + } + } + } + } + + // 3. componentConfig.action.excelNumberingRuleId (์—‘์…€ ์—…๋กœ๋“œ) + const excelRuleId = props?.componentConfig?.action?.excelNumberingRuleId; + if (excelRuleId && typeof excelRuleId === 'string' && excelRuleId.startsWith('rule-')) { + ruleIds.add(excelRuleId); + } + + // 4. componentConfig.action.numberingRuleId (๋ฒ„ํŠผ ์•ก์…˜) + const actionRuleId = props?.componentConfig?.action?.numberingRuleId; + if (actionRuleId && typeof actionRuleId === 'string' && actionRuleId.startsWith('rule-')) { + ruleIds.add(actionRuleId); + } + } + + return ruleIds; + } + + /** + * ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ ๋ฐ ID ๋งคํ•‘ ๋ฐ˜ํ™˜ + * - ์›๋ณธ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ๋Œ€์ƒ ํšŒ์‚ฌ๋กœ ๋ณต์‚ฌ + * - ์ด๋ฆ„์ด ๊ฐ™์€ ๊ทœ์น™์ด ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ + * - current_sequence๋Š” 0์œผ๋กœ ์ดˆ๊ธฐํ™” + */ + private async copyNumberingRulesForScreen( + ruleIds: Set, + sourceCompanyCode: string, + targetCompanyCode: string, + client: any + ): Promise> { + const ruleIdMap = new Map(); + + if (ruleIds.size === 0) { + return ruleIdMap; + } + + console.log(`๐Ÿ”„ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ ์‹œ์ž‘: ${ruleIds.size}๊ฐœ ๊ทœ์น™`); + + // 1. ์›๋ณธ ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (ํšŒ์‚ฌ ์ฝ”๋“œ ์ œํ•œ ์—†์ด rule_id๋กœ ์กฐํšŒ) + // ํ™”๋ฉด์ด ๋‹ค๋ฅธ ํšŒ์‚ฌ์˜ ์ฑ„๋ฒˆ ๊ทœ์น™์„ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํšŒ์‚ฌ ํ•„ํ„ฐ ์ œ๊ฑฐ + const ruleIdArray = Array.from(ruleIds); + const sourceRulesResult = await client.query( + `SELECT * FROM numbering_rules WHERE rule_id = ANY($1)`, + [ruleIdArray] + ); + + if (sourceRulesResult.rows.length === 0) { + console.log(` ๐Ÿ“ญ ๋ณต์‚ฌํ•  ์ฑ„๋ฒˆ ๊ทœ์น™ ์—†์Œ (ํ•ด๋‹น rule_id ์—†์Œ)`); + return ruleIdMap; + } + + console.log(` ๐Ÿ“‹ ์›๋ณธ ์ฑ„๋ฒˆ ๊ทœ์น™: ${sourceRulesResult.rows.length}๊ฐœ`); + + // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๊ธฐ์กด ์ฑ„๋ฒˆ ๊ทœ์น™ ์กฐํšŒ (์ด๋ฆ„ ๊ธฐ์ค€) + const existingRulesResult = await client.query( + `SELECT rule_id, rule_name FROM numbering_rules WHERE company_code = $1`, + [targetCompanyCode] + ); + const existingRulesByName = new Map( + existingRulesResult.rows.map((r: any) => [r.rule_name, r.rule_id]) + ); + + // 3. ๊ฐ ๊ทœ์น™ ๋ณต์‚ฌ ๋˜๋Š” ์žฌ์‚ฌ์šฉ + for (const rule of sourceRulesResult.rows) { + const existingId = existingRulesByName.get(rule.rule_name); + + if (existingId) { + // ๊ธฐ์กด ๊ทœ์น™ ์žฌ์‚ฌ์šฉ + ruleIdMap.set(rule.rule_id, existingId); + console.log(` โ™ป๏ธ ๊ธฐ์กด ์ฑ„๋ฒˆ ๊ทœ์น™ ์žฌ์‚ฌ์šฉ: ${rule.rule_name} (${rule.rule_id} โ†’ ${existingId})`); + } else { + // ์ƒˆ๋กœ ๋ณต์‚ฌ - ์ƒˆ rule_id ์ƒ์„ฑ + const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + // scope_type์ด 'menu'์ธ ๊ฒฝ์šฐ ๋Œ€์ƒ ํšŒ์‚ฌ์—์„œ ๊ฐ™์€ ์ด๋ฆ„์˜ ๋ฉ”๋‰ด ์ฐพ๊ธฐ + let newScopeType = rule.scope_type; + let newMenuObjid: string | null = null; + + if (rule.scope_type === 'menu' && rule.menu_objid) { + // ์›๋ณธ menu_objid๋กœ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ๋œ screen_group ์กฐํšŒ + const sourceMenuResult = await client.query( + `SELECT mi.menu_name_kor, sg.group_name + FROM menu_info mi + LEFT JOIN screen_groups sg ON sg.id = mi.screen_group_id + WHERE mi.objid = $1`, + [rule.menu_objid] + ); + + if (sourceMenuResult.rows.length > 0) { + const { menu_name_kor: menuName, group_name: groupName } = sourceMenuResult.rows[0]; + + // ๋ฐฉ๋ฒ• 1: ๊ทธ๋ฃน ์ด๋ฆ„์œผ๋กœ ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๋ฉ”๋‰ด ์ฐพ๊ธฐ (๋” ์ •ํ™•) + let targetMenuResult; + if (groupName) { + targetMenuResult = await client.query( + `SELECT mi.objid, mi.menu_name_kor + FROM menu_info mi + JOIN screen_groups sg ON sg.id = mi.screen_group_id + WHERE mi.company_code = $1 AND sg.group_name = $2 + LIMIT 1`, + [targetCompanyCode, groupName] + ); + } + + // ๋ฐฉ๋ฒ• 2: ๊ทธ๋ฃน์œผ๋กœ ๋ชป ์ฐพ์œผ๋ฉด ๋ฉ”๋‰ด ์ด๋ฆ„์œผ๋กœ ์ฐพ๊ธฐ + if (!targetMenuResult || targetMenuResult.rows.length === 0) { + targetMenuResult = await client.query( + `SELECT objid, menu_name_kor FROM menu_info + WHERE company_code = $1 AND menu_name_kor = $2 + LIMIT 1`, + [targetCompanyCode, menuName] + ); + } + + if (targetMenuResult.rows.length > 0) { + // ๋Œ€์ƒ ํšŒ์‚ฌ์— ๋งค์นญ๋˜๋Š” ๋ฉ”๋‰ด๊ฐ€ ์žˆ์œผ๋ฉด ์—ฐ๊ฒฐ + newMenuObjid = targetMenuResult.rows[0].objid; + console.log(` ๐Ÿ”— ๋ฉ”๋‰ด ์—ฐ๊ฒฐ: "${menuName}" โ†’ "${targetMenuResult.rows[0].menu_name_kor}" (objid: ${newMenuObjid})`); + } else { + // ๋Œ€์ƒ ํšŒ์‚ฌ์— ๋ฉ”๋‰ด๊ฐ€ ์—†์œผ๋ฉด ๋ณต์ œํ•˜์ง€ ์•Š์Œ (๋ฉ”๋‰ด ๋™๊ธฐํ™” ํ›„ ๋‹ค์‹œ ์‹œ๋„ ํ•„์š”) + console.log(` โญ๏ธ ์ฑ„๋ฒˆ๊ทœ์น™ "${rule.rule_name}" ๊ฑด๋„ˆ๋œ€: ๋Œ€์ƒ ํšŒ์‚ฌ์— "${menuName}" ๋ฉ”๋‰ด ์—†์Œ`); + continue; // ์ด ์ฑ„๋ฒˆ๊ทœ์น™์€ ๋ณต์ œํ•˜์ง€ ์•Š์Œ + } + } else { + // ์›๋ณธ ๋ฉ”๋‰ด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์œผ๋ฉด ๋ณต์ œํ•˜์ง€ ์•Š์Œ + console.log(` โญ๏ธ ์ฑ„๋ฒˆ๊ทœ์น™ "${rule.rule_name}" ๊ฑด๋„ˆ๋œ€: ์›๋ณธ ๋ฉ”๋‰ด(${rule.menu_objid})๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ`); + continue; // ์ด ์ฑ„๋ฒˆ๊ทœ์น™์€ ๋ณต์ œํ•˜์ง€ ์•Š์Œ + } + } + + // numbering_rules ๋ณต์‚ฌ (current_sequence = 0์œผ๋กœ ์ดˆ๊ธฐํ™”) + await client.query( + `INSERT INTO numbering_rules ( + rule_id, rule_name, description, separator, reset_period, + current_sequence, table_name, column_name, company_code, + created_at, updated_at, created_by, scope_type, last_generated_date, menu_objid + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`, + [ + newRuleId, + rule.rule_name, + rule.description, + rule.separator, + rule.reset_period, + 0, // current_sequence ์ดˆ๊ธฐํ™” + rule.table_name, + rule.column_name, + targetCompanyCode, + new Date(), + new Date(), + rule.created_by, + newScopeType, + null, // last_generated_date ์ดˆ๊ธฐํ™” + newMenuObjid, // ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๋ฉ”๋‰ด objid (์—†์œผ๋ฉด null) + ] + ); + + // numbering_rule_parts ๋ณต์‚ฌ + const partsResult = await client.query( + `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`, + [rule.rule_id] + ); + + for (const part of partsResult.rows) { + await client.query( + `INSERT INTO numbering_rule_parts ( + rule_id, part_order, part_type, generation_method, + auto_config, manual_config, company_code, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, + [ + newRuleId, + part.part_order, + part.part_type, + part.generation_method, + part.auto_config ? JSON.stringify(part.auto_config) : null, + part.manual_config ? JSON.stringify(part.manual_config) : null, + targetCompanyCode, + new Date(), + ] + ); + } + + ruleIdMap.set(rule.rule_id, newRuleId); + console.log(` โž• ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ: ${rule.rule_name} (${rule.rule_id} โ†’ ${newRuleId}), scope: ${newScopeType}, menu_objid: ${newMenuObjid || 'NULL'}, ํŒŒํŠธ ${partsResult.rows.length}๊ฐœ`); + } + } + + console.log(` โœ… ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ ์™„๋ฃŒ: ๋งคํ•‘ ${ruleIdMap.size}๊ฐœ`); + return ruleIdMap; + } + + /** + * properties ๋‚ด์˜ numberingRuleId ๋งคํ•‘ + * - componentConfig.autoGeneration.options.numberingRuleId (text-input) + * - componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal) + * - componentConfig.action.excelNumberingRuleId (์—‘์…€ ์—…๋กœ๋“œ) + */ + private updateNumberingRuleIdsInProperties(properties: any, ruleIdMap: Map): any { + if (!properties || ruleIdMap.size === 0) return properties; + + const updated = JSON.parse(JSON.stringify(properties)); + + // 1. componentConfig.autoGeneration.options.numberingRuleId (text-input) + if (updated?.componentConfig?.autoGeneration?.options?.numberingRuleId) { + const oldId = updated.componentConfig.autoGeneration.options.numberingRuleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + updated.componentConfig.autoGeneration.options.numberingRuleId = newId; + console.log(` ๐Ÿ”— autoGeneration.numberingRuleId: ${oldId} โ†’ ${newId}`); + } + } + + // 2. componentConfig.sections[].fields[].numberingRule.ruleId (universal-form-modal) + if (Array.isArray(updated?.componentConfig?.sections)) { + for (const section of updated.componentConfig.sections) { + // ์ผ๋ฐ˜ ํ•„๋“œ + if (Array.isArray(section?.fields)) { + for (const field of section.fields) { + if (field?.numberingRule?.ruleId) { + const oldId = field.numberingRule.ruleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + field.numberingRule.ruleId = newId; + console.log(` ๐Ÿ”— field.numberingRule.ruleId: ${oldId} โ†’ ${newId}`); + } + } + } + } + // optionalFieldGroups ๋‚ด๋ถ€์˜ ํ•„๋“œ๋“ค + if (Array.isArray(section?.optionalFieldGroups)) { + for (const optGroup of section.optionalFieldGroups) { + if (Array.isArray(optGroup?.fields)) { + for (const field of optGroup.fields) { + if (field?.numberingRule?.ruleId) { + const oldId = field.numberingRule.ruleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + field.numberingRule.ruleId = newId; + console.log(` ๐Ÿ”— optField.numberingRule.ruleId: ${oldId} โ†’ ${newId}`); + } + } + } + } + } + } + } + } + + // 3. componentConfig.action.excelNumberingRuleId + if (updated?.componentConfig?.action?.excelNumberingRuleId) { + const oldId = updated.componentConfig.action.excelNumberingRuleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + updated.componentConfig.action.excelNumberingRuleId = newId; + console.log(` ๐Ÿ”— excelNumberingRuleId: ${oldId} โ†’ ${newId}`); + } + } + + // 4. componentConfig.action.numberingRuleId (๋ฒ„ํŠผ ์•ก์…˜) + if (updated?.componentConfig?.action?.numberingRuleId) { + const oldId = updated.componentConfig.action.numberingRuleId; + const newId = ruleIdMap.get(oldId); + if (newId) { + updated.componentConfig.action.numberingRuleId = newId; + console.log(` ๐Ÿ”— action.numberingRuleId: ${oldId} โ†’ ${newId}`); + } + } + + return updated; + } + + /** + * properties ๋‚ด์˜ ํƒญ ์ปดํฌ๋„ŒํŠธ screenId ๋งคํ•‘ + * - componentConfig.tabs[].screenId (tabs-widget) + */ + private updateTabScreenIdsInProperties(properties: any, screenIdMap: Map): any { + if (!properties || screenIdMap.size === 0) return properties; + + const updated = JSON.parse(JSON.stringify(properties)); + + // componentConfig.tabs[].screenId (tabs-widget) + if (Array.isArray(updated?.componentConfig?.tabs)) { + for (const tab of updated.componentConfig.tabs) { + if (tab?.screenId) { + const oldId = Number(tab.screenId); + const newId = screenIdMap.get(oldId); + if (newId) { + tab.screenId = newId; + console.log(` ๐Ÿ”— tab.screenId: ${oldId} โ†’ ${newId}`); + } + } + } + } + + return updated; + } + + /** + * ๊ทธ๋ฃน ๋ณต์ œ ์™„๋ฃŒ ํ›„ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์˜ screenId/modalScreenId ์ฐธ์กฐ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ + * - tabs ์ปดํฌ๋„ŒํŠธ์˜ screenId + * - conditional-container์˜ screenId + * - ๋ฒ„ํŠผ/์•ก์…˜์˜ modalScreenId + * @param targetScreenIds ๋ณต์ œ๋œ ๋Œ€์ƒ ํ™”๋ฉด ID ๋ชฉ๋ก + * @param screenIdMap ์›๋ณธ ํ™”๋ฉด ID -> ์ƒˆ ํ™”๋ฉด ID ๋งคํ•‘ + */ + async updateTabScreenReferences( + targetScreenIds: number[], + screenIdMap: { [key: number]: number } + ): Promise<{ updated: number; details: string[] }> { + const result = { updated: 0, details: [] as string[] }; + + if (targetScreenIds.length === 0 || Object.keys(screenIdMap).length === 0) { + console.log(`โš ๏ธ updateTabScreenReferences ์Šคํ‚ต: targetScreenIds=${targetScreenIds.length}, screenIdMap keys=${Object.keys(screenIdMap).length}`); + return result; + } + + console.log(`๐Ÿ”„ updateTabScreenReferences ์‹œ์ž‘:`); + console.log(` - targetScreenIds: ${targetScreenIds.length}๊ฐœ`); + console.log(` - screenIdMap: ${JSON.stringify(screenIdMap)}`); + + const screenMap = new Map( + Object.entries(screenIdMap).map(([k, v]) => [Number(k), v]) + ); + + await transaction(async (client) => { + // ๋Œ€์ƒ ํ™”๋ฉด๋“ค์˜ ๋ชจ๋“  ๋ ˆ์ด์•„์›ƒ ์กฐํšŒ (screenId ๋˜๋Š” modalScreenId ์ฐธ์กฐ๊ฐ€ ์žˆ๋Š” ๊ฒƒ) + const placeholders = targetScreenIds.map((_, i) => `$${i + 1}`).join(', '); + const layoutsResult = await client.query( + `SELECT layout_id, screen_id, properties + FROM screen_layouts + WHERE screen_id IN (${placeholders}) + AND ( + properties::text LIKE '%"screenId"%' + OR properties::text LIKE '%"modalScreenId"%' + )`, + targetScreenIds + ); + + console.log(`๐Ÿ” ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ๋ ˆ์ด์•„์›ƒ: ${layoutsResult.rows.length}๊ฐœ`); + + for (const layout of layoutsResult.rows) { + let properties = layout.properties; + if (typeof properties === 'string') { + try { + properties = JSON.parse(properties); + } catch (e) { + continue; + } + } + + let hasChanges = false; + + // ์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  screenId/modalScreenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ + const updateReferences = async (obj: any, path: string = ''): Promise => { + if (!obj || typeof obj !== 'object') return; + + for (const key of Object.keys(obj)) { + const value = obj[key]; + const currentPath = path ? `${path}.${key}` : key; + + // screenId ์—…๋ฐ์ดํŠธ + if (key === 'screenId' && typeof value === 'number') { + const newId = screenMap.get(value); + if (newId) { + obj[key] = newId; + hasChanges = true; + result.details.push(`layout_id=${layout.layout_id}: ${currentPath} ${value} โ†’ ${newId}`); + console.log(`๐Ÿ”— screenId ๋งคํ•‘: ${value} โ†’ ${newId} (${currentPath})`); + + // screenName๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ (์žˆ๋Š” ๊ฒฝ์šฐ) + if (obj.screenName !== undefined) { + const newScreenResult = await client.query( + `SELECT screen_name FROM screen_definitions WHERE screen_id = $1`, + [newId] + ); + if (newScreenResult.rows.length > 0) { + obj.screenName = newScreenResult.rows[0].screen_name; + } + } + } else { + console.log(`โš ๏ธ screenId ${value} ๋งคํ•‘ ์—†์Œ (${currentPath}) - screenMap์— ํ•ด๋‹น ํ‚ค ์—†์Œ`); + } + } + + // modalScreenId ์—…๋ฐ์ดํŠธ + if (key === 'modalScreenId' && typeof value === 'number') { + const newId = screenMap.get(value); + if (newId) { + obj[key] = newId; + hasChanges = true; + result.details.push(`layout_id=${layout.layout_id}: ${currentPath} ${value} โ†’ ${newId}`); + console.log(`๐Ÿ”— modalScreenId ๋งคํ•‘: ${value} โ†’ ${newId} (${currentPath})`); + } else { + console.log(`โš ๏ธ modalScreenId ${value} ๋งคํ•‘ ์—†์Œ (${currentPath}) - screenMap์— ํ•ด๋‹น ํ‚ค ์—†์Œ`); + } + } + + // ๋ฐฐ์—ด ์ฒ˜๋ฆฌ + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + await updateReferences(value[i], `${currentPath}[${i}]`); + } + } + // ๊ฐ์ฒด ์žฌ๊ท€ + else if (typeof value === 'object' && value !== null) { + await updateReferences(value, currentPath); + } + } + }; + + await updateReferences(properties); + + if (hasChanges) { + await client.query( + `UPDATE screen_layouts SET properties = $1 WHERE layout_id = $2`, + [JSON.stringify(properties), layout.layout_id] + ); + result.updated++; + } + } + + console.log(`โœ… screenId/modalScreenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${result.updated}๊ฐœ ๋ ˆ์ด์•„์›ƒ`); + }); + + return result; + } + + /** + * ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId๋ฅผ ๋Œ€์ƒ ํšŒ์‚ฌ์—์„œ ๊ฐ™์€ ์ด๋ฆ„์˜ ํ™”๋ฉด์œผ๋กœ ์ž๋™ ๋งคํ•‘ + * @param properties ๋ ˆ์ด์•„์›ƒ properties + * @param targetCompanyCode ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ + * @param client PostgreSQL ํด๋ผ์ด์–ธํŠธ + * @returns ์—…๋ฐ์ดํŠธ๋œ properties + */ + private async autoMapTabScreenIds( + properties: any, + targetCompanyCode: string, + client: any + ): Promise { + if (!Array.isArray(properties?.componentConfig?.tabs)) { + return properties; + } + + const tabs = properties.componentConfig.tabs; + let hasChanges = false; + + for (const tab of tabs) { + if (!tab?.screenId) continue; + + const oldScreenId = Number(tab.screenId); + const oldScreenName = tab.screenName; + + // 1. ์›๋ณธ ํ™”๋ฉด ์ด๋ฆ„ ์กฐํšŒ (screenName์ด ์—†๋Š” ๊ฒฝ์šฐ) + let screenNameToFind = oldScreenName; + if (!screenNameToFind) { + const sourceResult = await client.query( + `SELECT screen_name FROM screen_definitions WHERE screen_id = $1 LIMIT 1`, + [oldScreenId] + ); + if (sourceResult.rows.length > 0) { + screenNameToFind = sourceResult.rows[0].screen_name; + } + } + + if (!screenNameToFind) continue; + + // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์—์„œ ์œ ์‚ฌํ•œ ์ด๋ฆ„์˜ ํ™”๋ฉด ์ฐพ๊ธฐ + // ์›๋ณธ ํ™”๋ฉด ์ด๋ฆ„์—์„œ ํšŒ์‚ฌ ์ ‘๋‘์–ด๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ํ•ต์‹ฌ ์ด๋ฆ„์œผ๋กœ ๊ฒ€์ƒ‰ + // ์˜ˆ: "ํƒ‘์”ฐ ํ’ˆ๋ชฉ ์นดํ…Œ๊ณ ๋ฆฌ์„ค์ •" โ†’ "์นดํ…Œ๊ณ ๋ฆฌ์„ค์ •"์œผ๋กœ ๊ฒ€์ƒ‰ + const nameParts = screenNameToFind.split(' '); + const coreNamePart = nameParts.length > 1 ? nameParts.slice(-1)[0] : screenNameToFind; + + const targetResult = await client.query( + `SELECT screen_id, screen_name + FROM screen_definitions + WHERE company_code = $1 + AND deleted_date IS NULL + AND is_active = 'Y' + AND screen_name LIKE $2 + ORDER BY screen_id DESC + LIMIT 1`, + [targetCompanyCode, `%${coreNamePart}`] + ); + + if (targetResult.rows.length > 0) { + const newScreen = targetResult.rows[0]; + tab.screenId = newScreen.screen_id; + tab.screenName = newScreen.screen_name; + hasChanges = true; + console.log(`๐Ÿ”— ํƒญ screenId ์ž๋™ ๋งคํ•‘: ${oldScreenId} (${oldScreenName}) โ†’ ${newScreen.screen_id} (${newScreen.screen_name})`); + } + } + + return properties; + } + + /** + * ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” flowId ์ˆ˜์ง‘ + */ + private collectFlowIdsFromLayouts(layouts: any[]): Set { + const flowIds = new Set(); + + for (const layout of layouts) { + const props = layout.properties; + if (!props) continue; + + // webTypeConfig.dataflowConfig.flowConfig.flowId + const flowId = props?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId; + if (flowId && !isNaN(parseInt(flowId))) { + flowIds.add(parseInt(flowId)); + } + + // webTypeConfig.dataflowConfig.selectedDiagramId + const diagramId = props?.webTypeConfig?.dataflowConfig?.selectedDiagramId; + if (diagramId && !isNaN(parseInt(diagramId))) { + flowIds.add(parseInt(diagramId)); + } + + // webTypeConfig.dataflowConfig.flowControls[].flowId + const flowControls = props?.webTypeConfig?.dataflowConfig?.flowControls; + if (Array.isArray(flowControls)) { + for (const control of flowControls) { + if (control?.flowId && !isNaN(parseInt(control.flowId))) { + flowIds.add(parseInt(control.flowId)); + } + } + } + + // componentConfig.action.excelAfterUploadFlows[].flowId + const excelFlows = props?.componentConfig?.action?.excelAfterUploadFlows; + if (Array.isArray(excelFlows)) { + for (const flow of excelFlows) { + if (flow?.flowId && !isNaN(parseInt(flow.flowId))) { + flowIds.add(parseInt(flow.flowId)); + } + } + } + } + + return flowIds; + } + + /** + * ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ๋ฐ ID ๋งคํ•‘ ๋ฐ˜ํ™˜ + * - ์›๋ณธ ํšŒ์‚ฌ์˜ ํ”Œ๋กœ์šฐ๋ฅผ ๋Œ€์ƒ ํšŒ์‚ฌ๋กœ ๋ณต์‚ฌ + * - ์ด๋ฆ„์ด ๊ฐ™์€ ํ”Œ๋กœ์šฐ๊ฐ€ ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ + */ + private async copyNodeFlowsForScreen( + flowIds: Set, + sourceCompanyCode: string, + targetCompanyCode: string, + client: any + ): Promise> { + const flowIdMap = new Map(); + + if (flowIds.size === 0) { + return flowIdMap; + } + + console.log(`๐Ÿ”„ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ์‹œ์ž‘: ${flowIds.size}๊ฐœ flowId`); + + // 1. ์›๋ณธ ํ”Œ๋กœ์šฐ ์กฐํšŒ (company_code = "*" ์ „์—ญ ํ”Œ๋กœ์šฐ๋Š” ๋ณต์‚ฌํ•˜์ง€ ์•Š์Œ) + const flowIdArray = Array.from(flowIds); + const sourceFlowsResult = await client.query( + `SELECT * FROM node_flows + WHERE flow_id = ANY($1) + AND company_code = $2`, + [flowIdArray, sourceCompanyCode] + ); + + if (sourceFlowsResult.rows.length === 0) { + console.log(` ๐Ÿ“ญ ๋ณต์‚ฌํ•  ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์—†์Œ (์›๋ณธ ํšŒ์‚ฌ ์†Œ์† ํ”Œ๋กœ์šฐ ์—†์Œ)`); + return flowIdMap; + } + + console.log(` ๐Ÿ“‹ ์›๋ณธ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ: ${sourceFlowsResult.rows.length}๊ฐœ`); + + // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ ๊ธฐ์กด ํ”Œ๋กœ์šฐ ์กฐํšŒ (์ด๋ฆ„ ๊ธฐ์ค€) + const existingFlowsResult = await client.query( + `SELECT flow_id, flow_name FROM node_flows WHERE company_code = $1`, + [targetCompanyCode] + ); + const existingFlowsByName = new Map( + existingFlowsResult.rows.map((f: any) => [f.flow_name, f.flow_id]) + ); + + // 3. ๊ฐ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ๋˜๋Š” ์žฌ์‚ฌ์šฉ + for (const flow of sourceFlowsResult.rows) { + const existingId = existingFlowsByName.get(flow.flow_name); + + if (existingId) { + // ๊ธฐ์กด ํ”Œ๋กœ์šฐ ์žฌ์‚ฌ์šฉ + flowIdMap.set(flow.flow_id, existingId); + console.log(` โ™ป๏ธ ๊ธฐ์กด ํ”Œ๋กœ์šฐ ์žฌ์‚ฌ์šฉ: ${flow.flow_name} (${flow.flow_id} โ†’ ${existingId})`); + } else { + // ์ƒˆ๋กœ ๋ณต์‚ฌ + const insertResult = await client.query( + `INSERT INTO node_flows (flow_name, flow_description, flow_data, company_code) + VALUES ($1, $2, $3, $4) + RETURNING flow_id`, + [ + flow.flow_name, + flow.flow_description, + JSON.stringify(flow.flow_data), + targetCompanyCode, + ] + ); + + const newFlowId = insertResult.rows[0].flow_id; + flowIdMap.set(flow.flow_id, newFlowId); + console.log(` โž• ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ: ${flow.flow_name} (${flow.flow_id} โ†’ ${newFlowId})`); + } + } + + console.log(` โœ… ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ์™„๋ฃŒ: ๋งคํ•‘ ${flowIdMap.size}๊ฐœ`); + return flowIdMap; + } + + /** + * properties ๋‚ด์˜ flowId, selectedDiagramId ๋“ฑ์„ ๋งคํ•‘ + */ + private updateFlowIdsInProperties(properties: any, flowIdMap: Map): any { + if (!properties || flowIdMap.size === 0) return properties; + + const updated = JSON.parse(JSON.stringify(properties)); + + // webTypeConfig.dataflowConfig.flowConfig.flowId + if (updated?.webTypeConfig?.dataflowConfig?.flowConfig?.flowId) { + const oldId = parseInt(updated.webTypeConfig.dataflowConfig.flowConfig.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + updated.webTypeConfig.dataflowConfig.flowConfig.flowId = newId; + console.log(` ๐Ÿ”— flowConfig.flowId: ${oldId} โ†’ ${newId}`); + } + } + + // webTypeConfig.dataflowConfig.selectedDiagramId + if (updated?.webTypeConfig?.dataflowConfig?.selectedDiagramId) { + const oldId = parseInt(updated.webTypeConfig.dataflowConfig.selectedDiagramId); + const newId = flowIdMap.get(oldId); + if (newId) { + updated.webTypeConfig.dataflowConfig.selectedDiagramId = newId; + console.log(` ๐Ÿ”— selectedDiagramId: ${oldId} โ†’ ${newId}`); + } + } + + // webTypeConfig.dataflowConfig.flowControls[].flowId + if (Array.isArray(updated?.webTypeConfig?.dataflowConfig?.flowControls)) { + for (const control of updated.webTypeConfig.dataflowConfig.flowControls) { + if (control?.flowId) { + const oldId = parseInt(control.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + control.flowId = newId; + console.log(` ๐Ÿ”— flowControls.flowId: ${oldId} โ†’ ${newId}`); + } + } + } + } + + // componentConfig.action.excelAfterUploadFlows[].flowId + if (Array.isArray(updated?.componentConfig?.action?.excelAfterUploadFlows)) { + for (const flow of updated.componentConfig.action.excelAfterUploadFlows) { + if (flow?.flowId) { + const oldId = parseInt(flow.flowId); + const newId = flowIdMap.get(oldId); + if (newId) { + flow.flowId = String(newId); + console.log(` ๐Ÿ”— excelAfterUploadFlows.flowId: ${oldId} โ†’ ${newId}`); + } + } + } + } + + return updated; + } + /** * ํ™”๋ฉด ๋ณต์‚ฌ (ํ™”๋ฉด ์ •๋ณด + ๋ ˆ์ด์•„์›ƒ ๋ชจ๋‘ ๋ณต์‚ฌ) (โœ… Raw Query ์ „ํ™˜ ์™„๋ฃŒ) */ @@ -2610,6 +3334,9 @@ export class ScreenManagementService { } // 4. ์ƒˆ ํ™”๋ฉด ์ƒ์„ฑ (๋Œ€์ƒ ํšŒ์‚ฌ์— ์ƒ์„ฑ) + // ์‚ญ์ œ๋œ ํ™”๋ฉด(is_active = 'D')์„ ๋ณต์‚ฌํ•  ๊ฒฝ์šฐ ํ™œ์„ฑ ์ƒํƒœ('Y')๋กœ ๋ณ€๊ฒฝ + const newIsActive = sourceScreen.is_active === 'D' ? 'Y' : sourceScreen.is_active; + const newScreenResult = await client.query( `INSERT INTO screen_definitions ( screen_code, screen_name, description, company_code, table_name, @@ -2622,7 +3349,7 @@ export class ScreenManagementService { copyData.description || sourceScreen.description, targetCompanyCode, // ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์‚ฌ์šฉ sourceScreen.table_name, - sourceScreen.is_active, + newIsActive, // ์‚ญ์ œ๋œ ํ™”๋ฉด์€ ํ™œ์„ฑ ์ƒํƒœ๋กœ ๋ณต์‚ฌ copyData.createdBy, new Date(), copyData.createdBy, @@ -2642,7 +3369,45 @@ export class ScreenManagementService { const sourceLayouts = sourceLayoutsResult.rows; - // 5. ๋ ˆ์ด์•„์›ƒ์ด ์žˆ๋‹ค๋ฉด ๋ณต์‚ฌ + // 5. ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) + let flowIdMap = new Map(); + if (sourceLayouts.length > 0 && sourceScreen.company_code !== targetCompanyCode) { + // ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” flowId ์ˆ˜์ง‘ + const flowIds = this.collectFlowIdsFromLayouts(sourceLayouts); + + if (flowIds.size > 0) { + console.log(`๐Ÿ” ํ™”๋ฉด ๋ณต์‚ฌ - flowId ์ˆ˜์ง‘: ${flowIds.size}๊ฐœ`); + + // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ๋ณต์‚ฌ ๋ฐ ๋งคํ•‘ ์ƒ์„ฑ + flowIdMap = await this.copyNodeFlowsForScreen( + flowIds, + sourceScreen.company_code, + targetCompanyCode, + client + ); + } + } + + // 5.1. ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) + let ruleIdMap = new Map(); + if (sourceLayouts.length > 0 && sourceScreen.company_code !== targetCompanyCode) { + // ๋ ˆ์ด์•„์›ƒ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ฑ„๋ฒˆ ๊ทœ์น™ ID ์ˆ˜์ง‘ + const ruleIds = this.collectNumberingRuleIdsFromLayouts(sourceLayouts); + + if (ruleIds.size > 0) { + console.log(`๐Ÿ” ํ™”๋ฉด ๋ณต์‚ฌ - ์ฑ„๋ฒˆ ๊ทœ์น™ ID ์ˆ˜์ง‘: ${ruleIds.size}๊ฐœ`); + + // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ณต์‚ฌ ๋ฐ ๋งคํ•‘ ์ƒ์„ฑ + ruleIdMap = await this.copyNumberingRulesForScreen( + ruleIds, + sourceScreen.company_code, + targetCompanyCode, + client + ); + } + } + + // 6. ๋ ˆ์ด์•„์›ƒ์ด ์žˆ๋‹ค๋ฉด ๋ณต์‚ฌ if (sourceLayouts.length > 0) { try { // ID ๋งคํ•‘ ๋งต ์ƒ์„ฑ @@ -2660,6 +3425,29 @@ export class ScreenManagementService { ? idMapping[sourceLayout.parent_id] : null; + // properties ํŒŒ์‹ฑ + let properties = sourceLayout.properties; + if (typeof properties === "string") { + try { + properties = JSON.parse(properties); + } catch (e) { + // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + } + } + + // flowId ๋งคํ•‘ ์ ์šฉ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) + if (flowIdMap.size > 0) { + properties = this.updateFlowIdsInProperties(properties, flowIdMap); + } + + // ์ฑ„๋ฒˆ ๊ทœ์น™ ID ๋งคํ•‘ ์ ์šฉ (ํšŒ์‚ฌ๊ฐ€ ๋‹ค๋ฅธ ๊ฒฝ์šฐ) + if (ruleIdMap.size > 0) { + properties = this.updateNumberingRuleIdsInProperties(properties, ruleIdMap); + } + + // ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId๋Š” ๊ฐœ๋ณ„ ๋ณต์ œ ์‹œ์ ์— ์—…๋ฐ์ดํŠธํ•˜์ง€ ์•Š์Œ + // ๋ชจ๋“  ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ ํ›„ updateTabScreenReferences์—์„œ screenIdMap ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ + await client.query( `INSERT INTO screen_layouts ( screen_id, component_type, component_id, parent_id, @@ -2675,9 +3463,7 @@ export class ScreenManagementService { Math.round(sourceLayout.position_y), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ Math.round(sourceLayout.width), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ Math.round(sourceLayout.height), // ์ •์ˆ˜๋กœ ๋ฐ˜์˜ฌ๋ฆผ - typeof sourceLayout.properties === "string" - ? sourceLayout.properties - : JSON.stringify(sourceLayout.properties), + JSON.stringify(properties), sourceLayout.display_order, new Date(), ] @@ -2896,6 +3682,372 @@ export class ScreenManagementService { console.log(`โœ… ์ด ${updateCount}๊ฐœ ๋ ˆ์ด์•„์›ƒ์˜ ์—ฐ๊ฒฐ๋œ ํ™”๋ฉด ID ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ (๋ฒ„ํŠผ + ์กฐ๊ฑด๋ถ€์ปจํ…Œ์ด๋„ˆ)`); return updateCount; } + + /** + * ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ (screen_menu_assignments) + * + * @param sourceCompanyCode ์›๋ณธ ํšŒ์‚ฌ ์ฝ”๋“œ + * @param targetCompanyCode ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ + * @param screenIdMap ์›๋ณธ ํ™”๋ฉด ID -> ์ƒˆ ํ™”๋ฉด ID ๋งคํ•‘ + * @returns ๋ณต์ œ ๊ฒฐ๊ณผ + */ + async copyScreenMenuAssignments( + sourceCompanyCode: string, + targetCompanyCode: string, + screenIdMap: Record + ): Promise<{ copiedCount: number; skippedCount: number; details: string[] }> { + const result = { + copiedCount: 0, + skippedCount: 0, + details: [] as string[], + }; + + return await transaction(async (client) => { + logger.info("๐Ÿ”— ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์‹œ์ž‘", { sourceCompanyCode, targetCompanyCode }); + + // 1. ์›๋ณธ ํšŒ์‚ฌ์˜ screen_groups (menu_objid ํฌํ•จ) ์กฐํšŒ + const sourceGroupsResult = await client.query<{ + id: number; + group_name: string; + menu_objid: string | null; + }>( + `SELECT id, group_name, menu_objid + FROM screen_groups + WHERE company_code = $1 AND menu_objid IS NOT NULL`, + [sourceCompanyCode] + ); + + // 2. ๋Œ€์ƒ ํšŒ์‚ฌ์˜ screen_groups (menu_objid ํฌํ•จ) ์กฐํšŒ + const targetGroupsResult = await client.query<{ + id: number; + group_name: string; + menu_objid: string | null; + }>( + `SELECT id, group_name, menu_objid + FROM screen_groups + WHERE company_code = $1 AND menu_objid IS NOT NULL`, + [targetCompanyCode] + ); + + // 3. ๊ทธ๋ฃน ์ด๋ฆ„ ๊ธฐ๋ฐ˜์œผ๋กœ menu_objid ๋งคํ•‘ ์ƒ์„ฑ + const menuObjidMap = new Map(); // ์›๋ณธ menu_objid -> ์ƒˆ menu_objid + for (const sourceGroup of sourceGroupsResult.rows) { + if (!sourceGroup.menu_objid) continue; + + const matchingTarget = targetGroupsResult.rows.find( + (t) => t.group_name === sourceGroup.group_name + ); + + if (matchingTarget?.menu_objid) { + menuObjidMap.set(sourceGroup.menu_objid, matchingTarget.menu_objid); + logger.debug(`๋ฉ”๋‰ด ๋งคํ•‘: ${sourceGroup.group_name} | ${sourceGroup.menu_objid} โ†’ ${matchingTarget.menu_objid}`); + } + } + + logger.info(`๐Ÿ“‹ ๋ฉ”๋‰ด ๋งคํ•‘ ์ƒ์„ฑ ์™„๋ฃŒ: ${menuObjidMap.size}๊ฐœ`); + + // 4. ์›๋ณธ screen_menu_assignments ์กฐํšŒ + const assignmentsResult = await client.query<{ + screen_id: number; + menu_objid: string; + display_order: number; + is_active: string; + }>( + `SELECT screen_id, menu_objid::text, display_order, is_active + FROM screen_menu_assignments + WHERE company_code = $1`, + [sourceCompanyCode] + ); + + logger.info(`๐Ÿ“Œ ์›๋ณธ ํ• ๋‹น: ${assignmentsResult.rowCount}๊ฐœ`); + + // 5. ์ƒˆ ํ• ๋‹น ์ƒ์„ฑ + for (const assignment of assignmentsResult.rows) { + const newScreenId = screenIdMap[assignment.screen_id]; + const newMenuObjid = menuObjidMap.get(assignment.menu_objid); + + if (!newScreenId) { + logger.warn(`โš ๏ธ ํ™”๋ฉด ID ๋งคํ•‘ ์—†์Œ: ${assignment.screen_id}`); + result.skippedCount++; + result.details.push(`ํ™”๋ฉด ${assignment.screen_id}: ๋งคํ•‘ ์—†์Œ`); + continue; + } + + if (!newMenuObjid) { + logger.warn(`โš ๏ธ ๋ฉ”๋‰ด objid ๋งคํ•‘ ์—†์Œ: ${assignment.menu_objid}`); + result.skippedCount++; + result.details.push(`๋ฉ”๋‰ด ${assignment.menu_objid}: ๋งคํ•‘ ์—†์Œ`); + continue; + } + + try { + await client.query( + `INSERT INTO screen_menu_assignments + (screen_id, menu_objid, company_code, display_order, is_active, created_by) + VALUES ($1, $2, $3, $4, $5, 'system') + ON CONFLICT (screen_id, menu_objid, company_code) DO NOTHING`, + [ + newScreenId, + newMenuObjid, + targetCompanyCode, + assignment.display_order, + assignment.is_active, + ] + ); + result.copiedCount++; + logger.debug(`โœ… ํ• ๋‹น ๋ณต์ œ: screen ${newScreenId} โ†’ menu ${newMenuObjid}`); + } catch (error: any) { + logger.error(`โŒ ํ• ๋‹น ๋ณต์ œ ์‹คํŒจ: ${error.message}`); + result.skippedCount++; + result.details.push(`ํ• ๋‹น ์‹คํŒจ: ${error.message}`); + } + } + + logger.info(`โœ… ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์™„๋ฃŒ: ${result.copiedCount}๊ฐœ ๋ณต์ œ, ${result.skippedCount}๊ฐœ ์Šคํ‚ต`); + return result; + }); + } + + /** + * ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์ œ + */ + async copyCodeCategoryAndCodes( + sourceCompanyCode: string, + targetCompanyCode: string, + menuObjidMap?: Map + ): Promise<{ copiedCategories: number; copiedCodes: number; details: string[] }> { + const result = { + copiedCategories: 0, + copiedCodes: 0, + details: [] as string[], + }; + + return transaction(async (client) => { + logger.info(`๐Ÿ“ฆ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`); + + // 1. ๊ธฐ์กด ๋Œ€์ƒ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ + await client.query(`DELETE FROM code_info WHERE company_code = $1`, [targetCompanyCode]); + await client.query(`DELETE FROM code_category WHERE company_code = $1`, [targetCompanyCode]); + + // 2. menuObjidMap ์ƒ์„ฑ (์—†๋Š” ๊ฒฝ์šฐ) + if (!menuObjidMap || menuObjidMap.size === 0) { + menuObjidMap = new Map(); + const groupPairs = await client.query<{ source_objid: string; target_objid: string }>( + `SELECT DISTINCT + sg1.menu_objid::text as source_objid, + sg2.menu_objid::text as target_objid + FROM screen_groups sg1 + JOIN screen_groups sg2 ON sg1.group_name = sg2.group_name + WHERE sg1.company_code = $1 AND sg2.company_code = $2 + AND sg1.menu_objid IS NOT NULL AND sg2.menu_objid IS NOT NULL`, + [sourceCompanyCode, targetCompanyCode] + ); + groupPairs.rows.forEach(p => menuObjidMap!.set(p.source_objid, p.target_objid)); + } + + // 3. ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ๋ณต์ œ + const categories = await client.query( + `SELECT * FROM code_category WHERE company_code = $1`, + [sourceCompanyCode] + ); + + for (const cat of categories.rows) { + const newMenuObjid = cat.menu_objid ? menuObjidMap.get(cat.menu_objid.toString()) || cat.menu_objid : null; + + await client.query( + `INSERT INTO code_category + (category_code, category_name, category_name_eng, description, sort_order, is_active, company_code, menu_objid, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'system')`, + [cat.category_code, cat.category_name, cat.category_name_eng, cat.description, cat.sort_order, cat.is_active, targetCompanyCode, newMenuObjid] + ); + result.copiedCategories++; + } + + // 4. ์ฝ”๋“œ ์ •๋ณด ๋ณต์ œ + const codes = await client.query( + `SELECT * FROM code_info WHERE company_code = $1`, + [sourceCompanyCode] + ); + + for (const code of codes.rows) { + const newMenuObjid = code.menu_objid ? menuObjidMap.get(code.menu_objid.toString()) || code.menu_objid : null; + + await client.query( + `INSERT INTO code_info + (code_category, code_value, code_name, code_name_eng, description, sort_order, is_active, company_code, menu_objid, parent_code_value, depth, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, 'system')`, + [code.code_category, code.code_value, code.code_name, code.code_name_eng, code.description, code.sort_order, code.is_active, targetCompanyCode, newMenuObjid, code.parent_code_value, code.depth] + ); + result.copiedCodes++; + } + + logger.info(`โœ… ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์™„๋ฃŒ: ์นดํ…Œ๊ณ ๋ฆฌ ${result.copiedCategories}๊ฐœ, ์ฝ”๋“œ ${result.copiedCodes}๊ฐœ`); + return result; + }); + } + + /** + * ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์ œ + */ + async copyCategoryMapping( + sourceCompanyCode: string, + targetCompanyCode: string, + menuObjidMap?: Map + ): Promise<{ copiedMappings: number; copiedValues: number; details: string[] }> { + const result = { + copiedMappings: 0, + copiedValues: 0, + details: [] as string[], + }; + + return transaction(async (client) => { + logger.info(`๐Ÿ“ฆ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`); + + // 1. ๊ธฐ์กด ๋Œ€์ƒ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ + await client.query(`DELETE FROM table_column_category_values WHERE company_code = $1`, [targetCompanyCode]); + await client.query(`DELETE FROM category_column_mapping WHERE company_code = $1`, [targetCompanyCode]); + + // 2. menuObjidMap ์ƒ์„ฑ (์—†๋Š” ๊ฒฝ์šฐ) + if (!menuObjidMap || menuObjidMap.size === 0) { + menuObjidMap = new Map(); + const groupPairs = await client.query<{ source_objid: string; target_objid: string }>( + `SELECT DISTINCT + sg1.menu_objid::text as source_objid, + sg2.menu_objid::text as target_objid + FROM screen_groups sg1 + JOIN screen_groups sg2 ON sg1.group_name = sg2.group_name + WHERE sg1.company_code = $1 AND sg2.company_code = $2 + AND sg1.menu_objid IS NOT NULL AND sg2.menu_objid IS NOT NULL`, + [sourceCompanyCode, targetCompanyCode] + ); + groupPairs.rows.forEach(p => menuObjidMap!.set(p.source_objid, p.target_objid)); + } + + // 3. category_column_mapping ๋ณต์ œ + const mappings = await client.query( + `SELECT * FROM category_column_mapping WHERE company_code = $1`, + [sourceCompanyCode] + ); + + for (const m of mappings.rows) { + const newMenuObjid = m.menu_objid ? menuObjidMap.get(m.menu_objid.toString()) || m.menu_objid : null; + + await client.query( + `INSERT INTO category_column_mapping + (table_name, logical_column_name, physical_column_name, menu_objid, company_code, description, created_by) + VALUES ($1, $2, $3, $4, $5, $6, 'system')`, + [m.table_name, m.logical_column_name, m.physical_column_name, newMenuObjid, targetCompanyCode, m.description] + ); + result.copiedMappings++; + } + + // 4. table_column_category_values ๋ณต์ œ + const values = await client.query( + `SELECT * FROM table_column_category_values WHERE company_code = $1`, + [sourceCompanyCode] + ); + + for (const v of values.rows) { + const newMenuObjid = v.menu_objid ? menuObjidMap.get(v.menu_objid.toString()) || v.menu_objid : null; + + await client.query( + `INSERT INTO table_column_category_values + (table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, description, color, icon, is_active, is_default, company_code, menu_objid, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, 'system')`, + [v.table_name, v.column_name, v.value_code, v.value_label, v.value_order, v.parent_value_id, v.depth, v.description, v.color, v.icon, v.is_active, v.is_default, targetCompanyCode, newMenuObjid] + ); + result.copiedValues++; + } + + logger.info(`โœ… ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์™„๋ฃŒ: ๋งคํ•‘ ${result.copiedMappings}๊ฐœ, ๊ฐ’ ${result.copiedValues}๊ฐœ`); + return result; + }); + } + + /** + * ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ ์ž…๋ ฅํƒ€์ž… ์„ค์ • ๋ณต์ œ + */ + async copyTableTypeColumns( + sourceCompanyCode: string, + targetCompanyCode: string + ): Promise<{ copiedCount: number; details: string[] }> { + const result = { + copiedCount: 0, + details: [] as string[], + }; + + return transaction(async (client) => { + logger.info(`๐Ÿ“ฆ ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`); + + // 1. ๊ธฐ์กด ๋Œ€์ƒ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ + await client.query(`DELETE FROM table_type_columns WHERE company_code = $1`, [targetCompanyCode]); + + // 2. ๋ณต์ œ + const columns = await client.query( + `SELECT * FROM table_type_columns WHERE company_code = $1`, + [sourceCompanyCode] + ); + + for (const col of columns.rows) { + await client.query( + `INSERT INTO table_type_columns + (table_name, column_name, input_type, detail_settings, is_nullable, display_order, company_code) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [col.table_name, col.column_name, col.input_type, col.detail_settings, col.is_nullable, col.display_order, targetCompanyCode] + ); + result.copiedCount++; + } + + logger.info(`โœ… ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์™„๋ฃŒ: ${result.copiedCount}๊ฐœ`); + return result; + }); + } + + /** + * ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ + */ + async copyCascadingRelation( + sourceCompanyCode: string, + targetCompanyCode: string + ): Promise<{ copiedCount: number; details: string[] }> { + const result = { + copiedCount: 0, + details: [] as string[], + }; + + return transaction(async (client) => { + logger.info(`๐Ÿ“ฆ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ: ${sourceCompanyCode} โ†’ ${targetCompanyCode}`); + + // 1. ๊ธฐ์กด ๋Œ€์ƒ ํšŒ์‚ฌ ๋ฐ์ดํ„ฐ ์‚ญ์ œ + await client.query(`DELETE FROM cascading_relation WHERE company_code = $1`, [targetCompanyCode]); + + // 2. ๋ณต์ œ + const relations = await client.query( + `SELECT * FROM cascading_relation WHERE company_code = $1`, + [sourceCompanyCode] + ); + + for (const rel of relations.rows) { + // ์ƒˆ๋กœ์šด relation_code ์ƒ์„ฑ + const newRelationCode = `${rel.relation_code}_${targetCompanyCode}`; + + await client.query( + `INSERT INTO cascading_relation + (relation_code, relation_name, description, parent_table, parent_value_column, parent_label_column, + child_table, child_filter_column, child_value_column, child_label_column, child_order_column, child_order_direction, + empty_parent_message, no_options_message, loading_message, clear_on_parent_change, company_code, is_active, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, 'system')`, + [newRelationCode, rel.relation_name, rel.description, rel.parent_table, rel.parent_value_column, rel.parent_label_column, + rel.child_table, rel.child_filter_column, rel.child_value_column, rel.child_label_column, rel.child_order_column, rel.child_order_direction, + rel.empty_parent_message, rel.no_options_message, rel.loading_message, rel.clear_on_parent_change, targetCompanyCode, rel.is_active] + ); + result.copiedCount++; + } + + logger.info(`โœ… ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์™„๋ฃŒ: ${result.copiedCount}๊ฐœ`); + return result; + }); + } } // ์„œ๋น„์Šค ์ธ์Šคํ„ด์Šค export diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 4e2878eb..d3a11572 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -111,7 +111,12 @@ export default function ScreenManagementPage() { }; // ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง๋œ ํ™”๋ฉด - const filteredScreens = screens.filter((screen) => + // ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—ฌ๋Ÿฌ ํ‚ค์›Œ๋“œ(ํด๋” ๊ณ„์ธต ๊ฒ€์ƒ‰)์ด๋ฉด ํ™”๋ฉด ํ•„ํ„ฐ๋ง ์—†์ด ๋ชจ๋“  ํ™”๋ฉด ํ‘œ์‹œ + // ๋‹จ์ผ ํ‚ค์›Œ๋“œ๋ฉด ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋กœ ํ™”๋ฉด ํ•„ํ„ฐ๋ง + const searchKeywords = searchTerm.toLowerCase().trim().split(/\s+/).filter(Boolean); + const filteredScreens = searchKeywords.length > 1 + ? screens // ํด๋” ๊ณ„์ธต ๊ฒ€์ƒ‰ ์‹œ์—๋Š” ํ™”๋ฉด ํ•„ํ„ฐ๋ง ์—†์Œ (ํด๋”์—์„œ ์ด๋ฏธ ํ•„ํ„ฐ๋ง๋จ) + : screens.filter((screen) => screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) || screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -183,6 +188,7 @@ export default function ScreenManagementPage() { selectedScreen={selectedScreen} onScreenSelect={handleScreenSelect} onScreenDesign={handleDesignScreen} + searchTerm={searchTerm} onGroupSelect={(group) => { setSelectedGroup(group); setSelectedScreen(null); // ํ™”๋ฉด ์„ ํƒ ํ•ด์ œ diff --git a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx index 3a20b883..1192922d 100644 --- a/frontend/components/numbering-rule/NumberingRuleDesigner.tsx +++ b/frontend/components/numbering-rule/NumberingRuleDesigner.tsx @@ -217,13 +217,16 @@ export const NumberingRuleDesigner: React.FC = ({ }); // ์ €์žฅ ์ „์— ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช…๊ณผ menuObjid ์ž๋™ ์„ค์ • - // ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜์œผ๋กœ ์ฑ„๋ฒˆ๊ทœ์น™ ๊ด€๋ฆฌ (menuObjid๋กœ ํ•„ํ„ฐ๋ง) + // menuObjid๊ฐ€ ์žˆ์œผ๋ฉด menu ์Šค์ฝ”ํ”„, ์—†์œผ๋ฉด ๊ธฐ์กด scopeType ์œ ์ง€ + const effectiveMenuObjid = menuObjid || currentRule.menuObjid || null; + const effectiveScopeType = effectiveMenuObjid ? "menu" : (currentRule.scopeType || "global"); + const ruleToSave = { ...currentRule, parts: partsWithDefaults, - scopeType: "menu" as const, // ๋ฉ”๋‰ด ๊ธฐ๋ฐ˜ ์ฑ„๋ฒˆ๊ทœ์น™ + scopeType: effectiveScopeType as "menu" | "global", // menuObjid ์œ ๋ฌด์— ๋”ฐ๋ผ ๊ฒฐ์ • tableName: currentTableName || currentRule.tableName || null, // ํ˜„์žฌ ํ…Œ์ด๋ธ”๋ช… (์ฐธ๊ณ ์šฉ) - menuObjid: menuObjid || currentRule.menuObjid || null, // ๋ฉ”๋‰ด OBJID (ํ•„ํ„ฐ๋ง ๊ธฐ์ค€) + menuObjid: effectiveMenuObjid, // ๋ฉ”๋‰ด OBJID (ํ•„ํ„ฐ๋ง ๊ธฐ์ค€) }; console.log("๐Ÿ’พ ์ฑ„๋ฒˆ ๊ทœ์น™ ์ €์žฅ:", { diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index 5590cef4..6f429362 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -33,9 +33,10 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle, AlertTriangle, Check, ChevronsUpDown, FolderTree } from "lucide-react"; +import { Loader2, Copy, Link as LinkIcon, Trash2, AlertCircle, AlertTriangle, Check, ChevronsUpDown, FolderTree, Hash, Code, Table, Settings, Database } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; import { ScreenDefinition } from "@/types/screen"; -import { screenApi } from "@/lib/api/screen"; +import { screenApi, updateTabScreenReferences } from "@/lib/api/screen"; import { ScreenGroup, addScreenToGroup, createScreenGroup } from "@/lib/api/screenGroup"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; @@ -135,6 +136,15 @@ export default function CopyScreenModal({ // ๊ทธ๋ฃน ๋ณต์ œ ๋ชจ๋“œ: "all" (์ „์ฒด), "folder_only" (ํด๋”๋งŒ), "screen_only" (ํ™”๋ฉด๋งŒ) const [groupCopyMode, setGroupCopyMode] = useState<"all" | "folder_only" | "screen_only">("all"); + // ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์˜ต์…˜ (์ฒดํฌ ์‹œ: ๋ณต์ œ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” โ†’ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์ˆœ์„œ๋กœ ์‹คํ–‰) + const [copyNumberingRules, setCopyNumberingRules] = useState(false); + + // ์ถ”๊ฐ€ ๋ณต์‚ฌ ์˜ต์…˜๋“ค + const [copyCodeCategory, setCopyCodeCategory] = useState(false); // ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์‚ฌ + const [copyCategoryMapping, setCopyCategoryMapping] = useState(false); // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์‚ฌ + const [copyTableTypeColumns, setCopyTableTypeColumns] = useState(false); // ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ ์ž…๋ ฅํƒ€์ž… ์„ค์ • ๋ณต์‚ฌ + const [copyCascadingRelation, setCopyCascadingRelation] = useState(false); // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์‚ฌ + // ๋ณต์‚ฌ ์ค‘ ์ƒํƒœ const [isCopying, setIsCopying] = useState(false); const [copyProgress, setCopyProgress] = useState({ current: 0, total: 0, message: "" }); @@ -584,6 +594,7 @@ export default function CopyScreenModal({ screen_id: result.mainScreen.screenId, screen_role: "MAIN", display_order: 1, + target_company_code: finalCompanyCode, // ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ }); console.log(`โœ… ๋ณต์ œ๋œ ํ™”๋ฉด์„ ๊ทธ๋ฃน(${selectedTargetGroupId})์— ์ถ”๊ฐ€ ์™„๋ฃŒ`); } catch (groupError) { @@ -609,7 +620,7 @@ export default function CopyScreenModal({ }; // ์ด๋ฆ„ ๋ณ€ํ™˜ ํ—ฌํผ ํ•จ์ˆ˜ (์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ) - const transformName = (originalName: string, isRootGroup: boolean = false): string => { + const transformName = (originalName: string, isRootGroup: boolean = false, sourceCompanyCode?: string): string => { // ๋ฃจํŠธ ๊ทธ๋ฃน์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ž…๋ ฅํ•œ ์ด๋ฆ„ ์‚ฌ์šฉ if (isRootGroup) { return newGroupName.trim(); @@ -621,7 +632,12 @@ export default function CopyScreenModal({ return originalName.replace(new RegExp(groupFindText, "g"), groupReplaceText); } - // ๊ธฐ๋ณธ: "(๋ณต์ œ)" ๋ถ™์ด๊ธฐ + // ๋‹ค๋ฅธ ํšŒ์‚ฌ๋กœ ๋ณต์ œํ•˜๋Š” ๊ฒฝ์šฐ: ์›๋ณธ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ (์ค‘๋ณต๋  ์ผ ์—†์Œ) + if (sourceCompanyCode && sourceCompanyCode !== targetCompanyCode) { + return originalName; + } + + // ๊ฐ™์€ ํšŒ์‚ฌ ๋‚ด ๋ณต์ œ: "(๋ณต์ œ)" ๋ถ™์ด๊ธฐ (์ค‘๋ณต ๋ฐฉ์ง€) return `${originalName} (๋ณต์ œ)`; }; @@ -633,17 +649,19 @@ export default function CopyScreenModal({ screenCodes: string[], // ๋ฏธ๋ฆฌ ์ƒ์„ฑ๋œ ํ™”๋ฉด ์ฝ”๋“œ ๋ฐฐ์—ด codeIndex: { current: number }, // ํ˜„์žฌ ์‚ฌ์šฉํ•  ์ฝ”๋“œ ์ธ๋ฑ์Šค (์ฐธ์กฐ๋กœ ์ „๋‹ฌ) stats: { groups: number; screens: number }, - totalScreenCount: number // ์ „์ฒด ํ™”๋ฉด ์ˆ˜ (์ง„ํ–‰๋ฅ  ํ‘œ์‹œ์šฉ) + totalScreenCount: number, // ์ „์ฒด ํ™”๋ฉด ์ˆ˜ (์ง„ํ–‰๋ฅ  ํ‘œ์‹œ์šฉ) + screenIdMap: { [key: number]: number } // ์›๋ณธ ํ™”๋ฉด ID -> ์ƒˆ ํ™”๋ฉด ID ๋งคํ•‘ ): Promise => { // 1. ํ˜„์žฌ ๊ทธ๋ฃน ์ƒ์„ฑ (์›๋ณธ display_order ์œ ์ง€) const timestamp = Date.now(); const randomSuffix = Math.floor(Math.random() * 1000); const newGroupCode = `${targetCompany}_GROUP_${timestamp}_${randomSuffix}`; - console.log(`๐Ÿ“ ๊ทธ๋ฃน ์ƒ์„ฑ: ${sourceGroupData.group_name} (๋ณต์ œ)`); + const transformedGroupName = transformName(sourceGroupData.group_name, false, sourceGroupData.company_code); + console.log(`๐Ÿ“ ๊ทธ๋ฃน ์ƒ์„ฑ: ${transformedGroupName}`); const newGroupResponse = await createScreenGroup({ - group_name: transformName(sourceGroupData.group_name), // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ + group_name: transformedGroupName, // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ group_code: newGroupCode, parent_group_id: parentGroupId, target_company_code: targetCompany, @@ -663,13 +681,29 @@ export default function CopyScreenModal({ const sourceScreensInfo = sourceGroupData.screens || []; // ํ™”๋ฉด ์ •๋ณด์™€ display_order๋ฅผ ํ•จ๊ป˜ ๋งคํ•‘ + // allScreens์—์„œ ๋ชป ์ฐพ์œผ๋ฉด ๊ทธ๋ฃน์˜ screens ์ •๋ณด๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ (๋‹ค๋ฅธ ํšŒ์‚ฌ ํด๋” ๋ณต์‚ฌ ์‹œ) const screensWithOrder = sourceScreensInfo.map((s: any) => { const screenId = typeof s === 'object' ? s.screen_id : s; const displayOrder = typeof s === 'object' ? s.display_order : 0; const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN'; - const screenData = allScreens.find((sc) => sc.screenId === screenId); + const screenName = typeof s === 'object' ? s.screen_name : ''; + const tableName = typeof s === 'object' ? s.table_name : ''; + + // allScreens์—์„œ ๋จผ์ € ์ฐพ๊ณ , ์—†์œผ๋ฉด ๊ทธ๋ฃน์˜ screens ์ •๋ณด๋กœ ๋Œ€์ฒด + let screenData = allScreens.find((sc) => sc.screenId === screenId); + if (!screenData && screenId && screenName) { + // allScreens์— ์—†๋Š” ๊ฒฝ์šฐ (๋‹ค๋ฅธ ํšŒ์‚ฌ ํ™”๋ฉด) - ๊ทธ๋ฃน์˜ screens ์ •๋ณด๋กœ ์ตœ์†Œํ•œ์˜ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + screenData = { + screenId: screenId, + screenName: screenName, + screenCode: `SCREEN_${screenId}`, // ์ž„์‹œ ์ฝ”๋“œ (์‹ค์ œ ๋ณต์‚ฌ ์‹œ ์ƒˆ ์ฝ”๋“œ ์ƒ์„ฑ) + tableName: tableName || '', + description: '', + companyCode: sourceGroupData.company_code || '', + } as any; + } return { screenId, displayOrder, screenRole, screenData }; - }).filter(item => item.screenData); // ํ™”๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒƒ๋งŒ + }).filter(item => item.screenData && item.screenId); // screenId๊ฐ€ ์œ ํšจํ•œ ๊ฒƒ๋งŒ // display_order ์ˆœ์œผ๋กœ ์ •๋ ฌ screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)); @@ -687,12 +721,13 @@ export default function CopyScreenModal({ message: `ํ™”๋ฉด ๋ณต์ œ ์ค‘: ${screen.screenName}` }); - console.log(` ๐Ÿ“„ ํ™”๋ฉด ๋ณต์ œ: ${screen.screenName} โ†’ ${newScreenCode}`); + const transformedScreenName = transformName(screen.screenName, false, sourceGroupData.company_code); + console.log(` ๐Ÿ“„ ํ™”๋ฉด ๋ณต์ œ: ${screen.screenName} โ†’ ${transformedScreenName}`); const result = await screenApi.copyScreenWithModals(screen.screenId, { targetCompanyCode: targetCompany, mainScreen: { - screenName: transformName(screen.screenName), // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ + screenName: transformedScreenName, // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ screenCode: newScreenCode, description: screen.description || "", }, @@ -700,14 +735,18 @@ export default function CopyScreenModal({ }); if (result.mainScreen?.screenId) { + // ์›๋ณธ ํ™”๋ฉด ID -> ์ƒˆ ํ™”๋ฉด ID ๋งคํ•‘ ๊ธฐ๋ก + screenIdMap[screen.screenId] = result.mainScreen.screenId; + await addScreenToGroup({ group_id: newGroup.id, screen_id: result.mainScreen.screenId, screen_role: screenRole || "MAIN", display_order: displayOrder, // ์›๋ณธ ์ •๋ ฌ์ˆœ์„œ ์œ ์ง€ + target_company_code: targetCompany, // ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ }); stats.screens++; - console.log(` โœ… ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ: ${result.mainScreen.screenName}`); + console.log(` โœ… ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ: ${result.mainScreen.screenName} (${screen.screenId} โ†’ ${result.mainScreen.screenId})`); } } catch (screenError) { console.error(` โŒ ํ™”๋ฉด ๋ณต์ œ ์‹คํŒจ (${screen.screenCode}):`, screenError); @@ -730,7 +769,8 @@ export default function CopyScreenModal({ screenCodes, codeIndex, stats, - totalScreenCount + totalScreenCount, + screenIdMap // screenIdMap ์ „๋‹ฌ ); } } @@ -769,6 +809,7 @@ export default function CopyScreenModal({ const finalCompanyCode = targetCompanyCode || sourceGroup.company_code; const stats = { groups: 0, screens: 0 }; + const screenIdMap: { [key: number]: number } = {}; // ์›๋ณธ ํ™”๋ฉด ID -> ์ƒˆ ํ™”๋ฉด ID ๋งคํ•‘ console.log("๐Ÿ”„ ๊ทธ๋ฃน ๋ณต์ œ ์‹œ์ž‘ (์žฌ๊ท€์ ):", { sourceGroup: sourceGroup.group_name, @@ -795,7 +836,7 @@ export default function CopyScreenModal({ // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ์›๋ณธ ์ด๋ฆ„์— ๋ณ€ํ™˜ ์ ์šฉ const rootGroupName = useGroupBulkRename && groupFindText - ? transformName(sourceGroup.group_name) + ? transformName(sourceGroup.group_name, false, sourceGroup.company_code) : newGroupName.trim(); const newGroupResponse = await createScreenGroup({ @@ -818,14 +859,41 @@ export default function CopyScreenModal({ if (groupCopyMode !== "folder_only") { const sourceScreensInfo = sourceGroup.screens || []; - // ํ™”๋ฉด ์ •๋ณด์™€ display_order๋ฅผ ํ•จ๊ป˜ ๋งคํ•‘ - const screensWithOrder = sourceScreensInfo.map((s: any) => { - const screenId = typeof s === 'object' ? s.screen_id : s; - const displayOrder = typeof s === 'object' ? s.display_order : 0; - const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN'; - const screenData = allScreens.find((sc) => sc.screenId === screenId); - return { screenId, displayOrder, screenRole, screenData }; - }).filter(item => item.screenData); + // ํ™”๋ฉด ์ •๋ณด์™€ display_order๋ฅผ ํ•จ๊ป˜ ๋งคํ•‘ + // allScreens์—์„œ ๋ชป ์ฐพ์œผ๋ฉด ๊ทธ๋ฃน์˜ screens ์ •๋ณด๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ (๋‹ค๋ฅธ ํšŒ์‚ฌ ํด๋” ๋ณต์‚ฌ ์‹œ) + console.log(`๐Ÿ” ๋ฃจํŠธ ๊ทธ๋ฃน ํ™”๋ฉด ๋งคํ•‘ ์‹œ์ž‘: ${sourceScreensInfo.length}๊ฐœ ํ™”๋ฉด, allScreens: ${allScreens.length}๊ฐœ`); + const screensWithOrder = sourceScreensInfo.map((s: any) => { + const screenId = typeof s === 'object' ? s.screen_id : s; + const displayOrder = typeof s === 'object' ? s.display_order : 0; + const screenRole = typeof s === 'object' ? s.screen_role : 'MAIN'; + const screenName = typeof s === 'object' ? s.screen_name : ''; + const tableName = typeof s === 'object' ? s.table_name : ''; + + // allScreens์—์„œ ๋จผ์ € ์ฐพ๊ณ , ์—†์œผ๋ฉด ๊ทธ๋ฃน์˜ screens ์ •๋ณด๋กœ ๋Œ€์ฒด + let screenData = allScreens.find((sc) => sc.screenId === screenId); + const foundInAllScreens = !!screenData; + + if (!screenData && screenId && screenName) { + // allScreens์— ์—†๋Š” ๊ฒฝ์šฐ (๋‹ค๋ฅธ ํšŒ์‚ฌ ํ™”๋ฉด) - ๊ทธ๋ฃน์˜ screens ์ •๋ณด๋กœ ์ตœ์†Œํ•œ์˜ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + console.log(` โš ๏ธ allScreens์—์„œ ๋ชป ์ฐพ์Œ, ๊ทธ๋ฃน ์ •๋ณด ์‚ฌ์šฉ: ${screenId} - ${screenName}`); + screenData = { + screenId: screenId, + screenName: screenName, + screenCode: `SCREEN_${screenId}`, // ์ž„์‹œ ์ฝ”๋“œ (์‹ค์ œ ๋ณต์‚ฌ ์‹œ ์ƒˆ ์ฝ”๋“œ ์ƒ์„ฑ) + tableName: tableName || '', + description: '', + companyCode: sourceGroup.company_code || '', + } as any; + } else if (screenData) { + console.log(` โœ… allScreens์—์„œ ์ฐพ์Œ: ${screenId} - ${screenData.screenName}`); + } else { + console.log(` โŒ ํ™”๋ฉด ์ •๋ณด ์—†์Œ: screenId=${screenId}, screenName=${screenName}`); + } + return { screenId, displayOrder, screenRole, screenData }; + }).filter(item => item.screenData && item.screenId); // screenId๊ฐ€ ์œ ํšจํ•œ ๊ฒƒ๋งŒ + + console.log(`๐Ÿ” ๋งคํ•‘ ์™„๋ฃŒ: ${screensWithOrder.length}๊ฐœ ํ™”๋ฉด ๋ณต์‚ฌ ์˜ˆ์ •`); + screensWithOrder.forEach(item => console.log(` - ${item.screenId}: ${item.screenData?.screenName}`)); // display_order ์ˆœ์œผ๋กœ ์ •๋ ฌ screensWithOrder.sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0)); @@ -843,12 +911,13 @@ export default function CopyScreenModal({ message: `ํ™”๋ฉด ๋ณต์ œ ์ค‘: ${screen.screenName}` }); - console.log(`๐Ÿ“„ ํ™”๋ฉด ๋ณต์ œ: ${screen.screenName} โ†’ ${newScreenCode}`); + const transformedScreenName = transformName(screen.screenName, false, sourceGroup.company_code); + console.log(`๐Ÿ“„ ํ™”๋ฉด ๋ณต์ œ: ${screen.screenName} โ†’ ${transformedScreenName}`); const result = await screenApi.copyScreenWithModals(screen.screenId, { targetCompanyCode: finalCompanyCode, mainScreen: { - screenName: transformName(screen.screenName), // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ + screenName: transformedScreenName, // ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ์ ์šฉ screenCode: newScreenCode, description: screen.description || "", }, @@ -856,14 +925,18 @@ export default function CopyScreenModal({ }); if (result.mainScreen?.screenId) { + // ์›๋ณธ ํ™”๋ฉด ID -> ์ƒˆ ํ™”๋ฉด ID ๋งคํ•‘ ๊ธฐ๋ก + screenIdMap[screen.screenId] = result.mainScreen.screenId; + await addScreenToGroup({ group_id: newRootGroup.id, screen_id: result.mainScreen.screenId, screen_role: screenRole || "MAIN", display_order: displayOrder, // ์›๋ณธ ์ •๋ ฌ์ˆœ์„œ ์œ ์ง€ + target_company_code: finalCompanyCode, // ๋Œ€์ƒ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ }); stats.screens++; - console.log(`โœ… ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ: ${result.mainScreen.screenName}`); + console.log(`โœ… ํ™”๋ฉด ๋ณต์ œ ์™„๋ฃŒ: ${result.mainScreen.screenName} (${screen.screenId} โ†’ ${result.mainScreen.screenId})`); } } catch (screenError) { console.error(`ํ™”๋ฉด ๋ณต์ œ ์‹คํŒจ (${screen.screenCode}):`, screenError); @@ -886,11 +959,180 @@ export default function CopyScreenModal({ screenCodes, codeIndex, stats, - totalScreenCount + totalScreenCount, + screenIdMap // screenIdMap ์ „๋‹ฌ ); } } + // 6. ํƒญ ์ปดํฌ๋„ŒํŠธ์˜ screenId ์ฐธ์กฐ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ + console.log("๐Ÿ” screenIdMap ์ƒํƒœ:", screenIdMap, "ํ‚ค ๊ฐœ์ˆ˜:", Object.keys(screenIdMap).length); + if (Object.keys(screenIdMap).length > 0) { + console.log("๐Ÿ”— ํƒญ screenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์ค‘...", screenIdMap); + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "ํƒญ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์ค‘..." }); + + const targetScreenIds = Object.values(screenIdMap); + try { + const updateResult = await updateTabScreenReferences(targetScreenIds, screenIdMap); + console.log(`โœ… ํƒญ screenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${updateResult.updated}๊ฐœ ๋ ˆ์ด์•„์›ƒ`); + } catch (tabUpdateError) { + console.warn("ํƒญ screenId ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ ์‹คํŒจ (๋ฌด์‹œ):", tabUpdateError); + } + } + + // 7. ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์˜ต์…˜์ด ์„ ํƒ๋œ ๊ฒฝ์šฐ (๋ณต์ œ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” โ†’ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ) + if (copyNumberingRules) { + try { + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "๋ฉ”๋‰ด ๋™๊ธฐํ™” ์ค‘..." }); + console.log("๐Ÿ“‹ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹œ์ž‘ (์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์ค€๋น„)..."); + + // 7-1. ๋ฉ”๋‰ด ๋™๊ธฐํ™” (ํ™”๋ฉด ๊ทธ๋ฃน โ†’ ๋ฉ”๋‰ด) + const syncResponse = await apiClient.post("/screen-groups/sync/screen-to-menu", { + targetCompanyCode: finalCompanyCode, + }); + + if (syncResponse.data?.success) { + console.log("โœ… ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์™„๋ฃŒ:", syncResponse.data.data); + + // 7-2. ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์ค‘..." }); + console.log("๐Ÿ“‹ ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹œ์ž‘..."); + + const numberingResponse = await apiClient.post("/numbering-rules/copy-for-company", { + sourceCompanyCode: sourceGroup.company_code, + targetCompanyCode: finalCompanyCode, + }); + + if (numberingResponse.data?.success) { + console.log("โœ… ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์™„๋ฃŒ:", numberingResponse.data.data); + toast.success(`์ฑ„๋ฒˆ๊ทœ์น™ ${numberingResponse.data.data?.copiedCount || 0}๊ฐœ๊ฐ€ ๋ณต์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + } else { + console.warn("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์‹คํŒจ:", numberingResponse.data?.error); + toast.warning("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋™์œผ๋กœ ๋ณต์ œํ•ด์ฃผ์„ธ์š”."); + } + + // 7-3. ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ (screen_menu_assignments) + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์ค‘..." }); + console.log("๐Ÿ“‹ ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์‹œ์ž‘..."); + + const menuAssignResponse = await apiClient.post("/screen-management/copy-menu-assignments", { + sourceCompanyCode: sourceGroup.company_code, + targetCompanyCode: finalCompanyCode, + screenIdMap, + }); + + if (menuAssignResponse.data?.success) { + console.log("โœ… ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์™„๋ฃŒ:", menuAssignResponse.data.data); + toast.success(`ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ${menuAssignResponse.data.data?.copiedCount || 0}๊ฐœ๊ฐ€ ๋ณต์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + } else { + console.warn("ํ™”๋ฉด-๋ฉ”๋‰ด ํ• ๋‹น ๋ณต์ œ ์‹คํŒจ:", menuAssignResponse.data?.error); + } + } else { + console.warn("๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹คํŒจ:", syncResponse.data?.error); + toast.warning("๋ฉ”๋‰ด ๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฑ„๋ฒˆ๊ทœ์น™์ด ๋ณต์ œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."); + } + } catch (numberingError) { + console.error("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜:", numberingError); + toast.warning("์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + + // 8. ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์ œ + if (copyCodeCategory) { + try { + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์ค‘..." }); + console.log("๐Ÿ“‹ ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์‹œ์ž‘..."); + + const response = await apiClient.post("/screen-management/copy-code-category", { + sourceCompanyCode: sourceGroup.company_code, + targetCompanyCode: finalCompanyCode, + }); + + if (response.data?.success) { + console.log("โœ… ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์™„๋ฃŒ:", response.data.data); + toast.success(`์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ ${response.data.data?.copiedCategories || 0}๊ฐœ, ์ฝ”๋“œ ${response.data.data?.copiedCodes || 0}๊ฐœ๊ฐ€ ๋ณต์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + } else { + console.warn("์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์‹คํŒจ:", response.data?.error); + toast.warning("์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } catch (error) { + console.error("์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜:", error); + toast.warning("์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ/์ฝ”๋“œ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + + // 9. ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์ œ + if (copyCategoryMapping) { + try { + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์ค‘..." }); + console.log("๐Ÿ“‹ ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์‹œ์ž‘..."); + + const response = await apiClient.post("/screen-management/copy-category-mapping", { + sourceCompanyCode: sourceGroup.company_code, + targetCompanyCode: finalCompanyCode, + }); + + if (response.data?.success) { + console.log("โœ… ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์™„๋ฃŒ:", response.data.data); + toast.success(`์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ ${response.data.data?.copiedMappings || 0}๊ฐœ, ๊ฐ’ ${response.data.data?.copiedValues || 0}๊ฐœ๊ฐ€ ๋ณต์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + } else { + console.warn("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์‹คํŒจ:", response.data?.error); + toast.warning("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } catch (error) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜:", error); + toast.warning("์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘/๊ฐ’ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + + // 10. ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ ์ž…๋ ฅํƒ€์ž… ์„ค์ • ๋ณต์ œ + if (copyTableTypeColumns) { + try { + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์ค‘..." }); + console.log("๐Ÿ“‹ ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์‹œ์ž‘..."); + + const response = await apiClient.post("/screen-management/copy-table-type-columns", { + sourceCompanyCode: sourceGroup.company_code, + targetCompanyCode: finalCompanyCode, + }); + + if (response.data?.success) { + console.log("โœ… ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์™„๋ฃŒ:", response.data.data); + toast.success(`ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ${response.data.data?.copiedCount || 0}๊ฐœ๊ฐ€ ๋ณต์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + } else { + console.warn("ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์‹คํŒจ:", response.data?.error); + toast.warning("ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜:", error); + toast.warning("ํ…Œ์ด๋ธ” ํƒ€์ž… ์ปฌ๋Ÿผ ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + + // 11. ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ + if (copyCascadingRelation) { + try { + setCopyProgress({ current: stats.screens, total: totalScreenCount, message: "์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์ค‘..." }); + console.log("๐Ÿ“‹ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์‹œ์ž‘..."); + + const response = await apiClient.post("/screen-management/copy-cascading-relation", { + sourceCompanyCode: sourceGroup.company_code, + targetCompanyCode: finalCompanyCode, + }); + + if (response.data?.success) { + console.log("โœ… ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์™„๋ฃŒ:", response.data.data); + toast.success(`์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ${response.data.data?.copiedCount || 0}๊ฐœ๊ฐ€ ๋ณต์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + } else { + console.warn("์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์‹คํŒจ:", response.data?.error); + toast.warning("์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } catch (error) { + console.error("์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜:", error); + toast.warning("์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ๋ณต์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + } + toast.success( `๊ทธ๋ฃน ๋ณต์ œ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! (๊ทธ๋ฃน ${stats.groups}๊ฐœ + ํ™”๋ฉด ${stats.screens}๊ฐœ)` ); @@ -1045,6 +1287,89 @@ export default function CopyScreenModal({

+ {/* ์ถ”๊ฐ€ ๋ณต์‚ฌ ์˜ต์…˜ (์„ ํƒ์‚ฌํ•ญ) */} +
+ + + {/* ์ฝ”๋“œ ์นดํ…Œ๊ณ ๋ฆฌ + ์ฝ”๋“œ ๋ณต์‚ฌ */} +
+ setCopyCodeCategory(checked === true)} + /> + +
+ + {/* ์ฑ„๋ฒˆ๊ทœ์น™ ๋ณต์ œ */} +
+ setCopyNumberingRules(checked === true)} + /> + +
+ + {/* ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘ + ๊ฐ’ ๋ณต์‚ฌ */} +
+ setCopyCategoryMapping(checked === true)} + /> +