From de7fa7a71bdd21b7392d7a6a07841aad267d0653 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 15 Jan 2026 14:36:00 +0900 Subject: [PATCH 01/22] =?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 f2ab4f11bda249fe76d4f0a4394eb5ec2d36202d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 10:14:20 +0900 Subject: [PATCH 02/22] =?UTF-8?q?=EC=A7=84=EC=A7=9C=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=ED=96=88=EC=9D=8C=20=EC=A7=84=EC=A7=9C=EC=A7=84=EC=A7=9C?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../registry/components/pivot-grid/PivotGridComponent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 57bc2e8a..53ad204d 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -1674,7 +1674,11 @@ export const PivotGridComponent: React.FC = ({ className="flex-1 overflow-auto focus:outline-none" style={{ maxHeight: enableVirtualScroll && containerHeight > 0 ? containerHeight : undefined, - minHeight: 100 // ์ตœ์†Œ ๋†’์ด ๋ณด์žฅ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + // ์ตœ์†Œ 200px ๋ณด์žฅ + ๋ฐ์ดํ„ฐ์— ๋งž๊ฒŒ ์กฐ์ • (์ตœ๋Œ€ 400px) + minHeight: Math.max( + 200, // ์ ˆ๋Œ€ ์ตœ์†Œ๊ฐ’ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + Math.min(400, (sortedFlatRows.length + 3) * ROW_HEIGHT + 50) + ) }} tabIndex={0} onKeyDown={handleKeyDown} From c282d5c611bfa0bac9e43a7f478dbffa1ccdd1a3 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 10:14:48 +0900 Subject: [PATCH 03/22] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj From d4b5bdd835244c6e9b43101611ed62ef38787477 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 19 Jan 2026 13:18:17 +0900 Subject: [PATCH 04/22] =?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 d09a6977f7083b394d0837aa248d699fa00c8799 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 17:25:12 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=EA=B2=80=EC=83=89=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 78abf111..366aa05b 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -41,7 +41,7 @@ import { Lock, } from "lucide-react"; import * as XLSX from "xlsx"; -import { FileText, ChevronRightIcon } from "lucide-react"; +import { FileText, ChevronRightIcon, Search } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -455,6 +455,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์ปฌ๋Ÿผ ํ—ค๋” ํ•„ํ„ฐ ์ƒํƒœ (์ƒ๋‹จ์—์„œ ์„ ์–ธ) const [headerFilters, setHeaderFilters] = useState>>({}); + const [headerLikeFilters, setHeaderLikeFilters] = useState>({}); // LIKE ๊ฒ€์ƒ‰์šฉ const [openFilterColumn, setOpenFilterColumn] = useState(null); // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ - filteredData๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ @@ -488,6 +489,22 @@ export const TableListComponent: React.FC = ({ }); } + // 2-1. ๐Ÿ†• LIKE ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ ์šฉ + if (Object.keys(headerLikeFilters).length > 0) { + result = result.filter((row) => { + return Object.entries(headerLikeFilters).every(([columnName, searchText]) => { + if (!searchText || searchText.trim() === "") return true; + + // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ + const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; + const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue).toLowerCase() : ""; + + // LIKE ๊ฒ€์ƒ‰ (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ) + return cellStr.includes(searchText.toLowerCase()); + }); + }); + } + // 3. ๐Ÿ†• Filter Builder ์ ์šฉ if (filterGroups.length > 0) { result = result.filter((row) => { @@ -541,7 +558,7 @@ export const TableListComponent: React.FC = ({ } return result; - }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, headerLikeFilters, filterGroups]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -2935,6 +2952,7 @@ export const TableListComponent: React.FC = ({ headerFilters: Object.fromEntries( Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set)]), ), + headerLikeFilters, // LIKE ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ €์žฅ pageSize: localPageSize, timestamp: Date.now(), }; @@ -2955,6 +2973,7 @@ export const TableListComponent: React.FC = ({ frozenColumnCount, showGridLines, headerFilters, + headerLikeFilters, localPageSize, ]); @@ -2991,6 +3010,9 @@ export const TableListComponent: React.FC = ({ }); setHeaderFilters(filters); } + if (state.headerLikeFilters) { + setHeaderLikeFilters(state.headerLikeFilters); + } } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ๋ณต์› ์‹คํŒจ:", error); } @@ -5737,7 +5759,7 @@ export const TableListComponent: React.FC = ({ }} className={cn( "hover:bg-primary/20 ml-1 rounded p-0.5 transition-colors", - headerFilters[column.columnName]?.size > 0 && "text-primary bg-primary/10", + (headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && "text-primary bg-primary/10", )} title="ํ•„ํ„ฐ" > @@ -5745,7 +5767,7 @@ export const TableListComponent: React.FC = ({ e.stopPropagation()} > @@ -5754,16 +5776,42 @@ export const TableListComponent: React.FC = ({ ํ•„ํ„ฐ: {columnLabels[column.columnName] || column.displayName} - {headerFilters[column.columnName]?.size > 0 && ( + {(headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && ( )}
-
+ {/* LIKE ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ */} +
+ + { + setHeaderLikeFilters((prev) => ({ + ...prev, + [column.columnName]: e.target.value, + })); + }} + className="border-input bg-background placeholder:text-muted-foreground h-7 w-full rounded-md border pl-7 pr-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" + onClick={(e) => e.stopPropagation()} + /> +
+ {/* ๊ตฌ๋ถ„์„  */} +
๋˜๋Š” ๊ฐ’ ์„ ํƒ:
+
{columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { const isSelected = headerFilters[column.columnName]?.has(val); return ( From b62a0b7e3b1572eb8485425968ace76afd7b3648 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 19 Jan 2026 18:48:18 +0900 Subject: [PATCH 06/22] =?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 07/22] =?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 08/22] =?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 09/22] =?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 10/22] =?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 11/22] =?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 12/22] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj From e8fc6643520dbd733fb20e18634169f964fb345f Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 10:32:37 +0900 Subject: [PATCH 13/22] =?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 14/22] =?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 cb8b434434a957da3ec78f889712aed7671091c4 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 16:01:58 +0900 Subject: [PATCH 15/22] =?UTF-8?q?=ED=94=BC=EB=B2=97=EC=97=90=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=95=88=EB=90=98=EB=8D=98=EA=B1=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/registry/components/pivot-grid/PivotGridComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 53ad204d..4b4465e1 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -500,9 +500,9 @@ export const PivotGridComponent: React.FC = ({ const filteredData = useMemo(() => { if (!data || data.length === 0) return data; - // ํ•„ํ„ฐ ์˜์—ญ์˜ ํ•„๋“œ๋“ค๋กœ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง + // ๋ชจ๋“  ์˜์—ญ(ํ–‰/์—ด/ํ•„ํ„ฐ)์˜ ํ•„ํ„ฐ ๊ฐ’์ด ์žˆ๋Š” ํ•„๋“œ๋กœ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง const activeFilters = fields.filter( - (f) => f.area === "filter" && f.filterValues && f.filterValues.length > 0 + (f) => f.filterValues && f.filterValues.length > 0 ); if (activeFilters.length === 0) return data; From e6bb366ec7c04d7a88b44cb8fc856459bbbd5fa1 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 16:15:20 +0900 Subject: [PATCH 16/22] =?UTF-8?q?=ED=94=BC=EB=B2=97=EC=97=90=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=EC=AA=BD=EC=97=90=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=EB=B2=84=ED=8A=BC=20=EB=84=A3=EC=97=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 4 + .../pivot-grid/components/FieldPanel.tsx | 162 +++++++++++++++++- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 4b4465e1..b815ce06 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -299,6 +299,8 @@ export const PivotGridComponent: React.FC = ({ // ==================== ์ƒํƒœ ==================== const [fields, setFields] = useState(initialFields); + // ์ดˆ๊ธฐ ํ•„๋“œ ์„ค์ • ์ €์žฅ (์ดˆ๊ธฐํ™”์šฉ) + const initialFieldsRef = useRef(initialFields); const [pivotState, setPivotState] = useState({ expandedRowPaths: [], expandedColumnPaths: [], @@ -1129,6 +1131,7 @@ export const PivotGridComponent: React.FC = ({ onFieldsChange={handleFieldsChange} collapsed={!showFieldPanel} onToggleCollapse={() => setShowFieldPanel(!showFieldPanel)} + initialFields={initialFieldsRef.current} /> {/* ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ */} @@ -1405,6 +1408,7 @@ export const PivotGridComponent: React.FC = ({ onFieldsChange={handleFieldsChange} collapsed={!showFieldPanel} onToggleCollapse={() => setShowFieldPanel(!showFieldPanel)} + initialFields={initialFieldsRef.current} /> {/* ํ—ค๋” ํˆด๋ฐ” */} diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx index 967afd08..2ef1227e 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx @@ -37,6 +37,10 @@ import { BarChart3, GripVertical, ChevronDown, + RotateCcw, + FilterX, + LayoutGrid, + Trash2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -56,6 +60,8 @@ interface FieldPanelProps { onFieldSettingsChange?: (field: PivotFieldConfig) => void; collapsed?: boolean; onToggleCollapse?: () => void; + /** ์ดˆ๊ธฐ ํ•„๋“œ ์„ค์ • (ํ•„๋“œ ๋ฐฐ์น˜ ์ดˆ๊ธฐํ™”์šฉ) */ + initialFields?: PivotFieldConfig[]; } interface FieldChipProps { @@ -123,15 +129,23 @@ const SortableFieldChip: React.FC = ({ transition, }; + // ํ•„ํ„ฐ ์ ์šฉ ์—ฌ๋ถ€ ํ™•์ธ + const hasFilter = field.filterValues && field.filterValues.length > 0; + const filterCount = field.filterValues?.length || 0; + return (
{/* ๋“œ๋ž˜๊ทธ ํ•ธ๋“ค */} @@ -143,11 +157,24 @@ const SortableFieldChip: React.FC = ({ + {/* ํ•„ํ„ฐ ์•„์ด์ฝ˜ (ํ•„ํ„ฐ ์ ์šฉ ์‹œ) */} + {hasFilter && ( + + )} + {/* ํ•„๋“œ ๋ผ๋ฒจ */}
- {/* ์ ‘๊ธฐ ๋ฒ„ํŠผ */} - {onToggleCollapse && ( -
+ {/* ํ•˜๋‹จ ๋ฒ„ํŠผ ์˜์—ญ */} +
+ {/* ์ดˆ๊ธฐํ™” ๋“œ๋กญ๋‹ค์šด */} + + + + + + + + ํ•„ํ„ฐ๋งŒ ์ดˆ๊ธฐํ™” + {filteredFieldCount > 0 && ( + + ({filteredFieldCount}๊ฐœ) + + )} + + + + ํ•„๋“œ ๋ฐฐ์น˜ ์ดˆ๊ธฐํ™” + + + + + ์ „์ฒด ์ดˆ๊ธฐํ™” + + + + + {/* ์ ‘๊ธฐ ๋ฒ„ํŠผ */} + {onToggleCollapse && ( -
- )} + )} +
{/* ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„๋ ˆ์ด */} From 62a82b3bcfb2020db793fd08b40e47fdd49cef3a Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 16:40:37 +0900 Subject: [PATCH 17/22] =?UTF-8?q?=EB=B0=91=EA=BA=BD=EC=87=A0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=96=88=EC=9D=8C!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridConfigPanel.tsx | 26 ++++++- .../pivot-grid/components/FieldPanel.tsx | 71 +++++++++++++++++++ .../registry/components/pivot-grid/types.ts | 1 + .../pivot-grid/utils/pivotEngine.ts | 4 +- 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx index 37f0862b..448c92a5 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx @@ -16,6 +16,7 @@ import { PivotAreaType, AggregationType, FieldDataType, + DateGroupInterval, } from "./types"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -202,6 +203,28 @@ const AreaDropZone: React.FC = ({ )} + {/* ํ–‰/์—ด ์˜์—ญ์—์„œ ๋‚ ์งœ ํƒ€์ž…์ผ ๋•Œ ๊ทธ๋ฃนํ™” ์˜ต์…˜ */} + {(area === "row" || area === "column") && field.dataType === "date" && ( + + )} + )} + + {/* ์ƒํƒœ ์œ ์ง€ ์ฒดํฌ๋ฐ•์Šค */} +
+ setPersistState(checked === true)} + className="h-3.5 w-3.5" + /> + +
{/* ์ฐจํŠธ ํ† ๊ธ€ */} {chartConfig && ( @@ -1689,137 +1763,224 @@ export const PivotGridComponent: React.FC = ({ > - {/* ์—ด ํ—ค๋” */} - - {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (ํ–‰ ํ•„๋“œ ๋ผ๋ฒจ + ํ•„ํ„ฐ) */} - + {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (์ฒซ ๋ฒˆ์งธ ๋ ˆ๋ฒจ์—๋งŒ ํ‘œ์‹œ) */} + {levelIdx === 0 && ( + + )} + + {/* ์—ด ํ—ค๋” ์…€ - ํ•ด๋‹น ๋ ˆ๋ฒจ */} + {levelCells.map((cell, cellIdx) => ( + ))} - {rowFields.length === 0 && ํ•ญ๋ชฉ} - - - {/* ์—ด ํ—ค๋” ์…€ */} - {flatColumns.map((col, idx) => ( - )} - colSpan={dataFields.length || 1} - style={{ width: columnWidths[idx] || "auto", minWidth: 50 }} - onClick={dataFields.length === 1 ? () => handleSort(dataFields[0].field) : undefined} - > -
- {col.caption || "(์ „์ฒด)"} - {dataFields.length === 1 && } -
- {/* ์—ด ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค */} -
handleResizeStart(idx, e)} - /> - - ))} - - {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} - {totals?.showRowGrandTotals && ( -
)} - colSpan={dataFields.length || 1} - rowSpan={dataFields.length > 1 ? 2 : 1} - > - ์ด๊ณ„ - - )} - - {/* ์—ด ํ•„๋“œ ํ•„ํ„ฐ (ํ—ค๋” ์˜ค๋ฅธ์ชฝ ๋์— ํ‘œ์‹œ) */} - {columnFields.length > 0 && ( + + )) + ) : ( + // ์—ด ํ•„๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ: ๋‹จ์ผ ํ–‰ + - )} - + + {/* ์—ด ํ—ค๋” ์…€ (์—ด ํ•„๋“œ ์—†์„ ๋•Œ) */} + {flatColumns.map((col, idx) => ( + + ))} + + {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} + {totals?.showRowGrandTotals && ( + + )} + + )} {/* ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋ผ๋ฒจ (๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ํ•„๋“œ์ธ ๊ฒฝ์šฐ) */} {dataFields.length > 1 && ( diff --git a/frontend/lib/registry/components/pivot-grid/types.ts b/frontend/lib/registry/components/pivot-grid/types.ts index d55d6982..d4d8b1e5 100644 --- a/frontend/lib/registry/components/pivot-grid/types.ts +++ b/frontend/lib/registry/components/pivot-grid/types.ts @@ -331,8 +331,11 @@ export interface PivotResult { // ํ”Œ๋žซ ํ–‰ ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) flatRows: PivotFlatRow[]; - // ํ”Œ๋žซ ์—ด ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) + // ํ”Œ๋žซ ์—ด ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) - ๋ฆฌํ”„ ๋…ธ๋“œ๋งŒ flatColumns: PivotFlatColumn[]; + + // ์—ด ํ—ค๋” ๋ ˆ๋ฒจ๋ณ„ (๋‹ค์ค‘ ํ–‰ ํ—ค๋”์šฉ) + columnHeaderLevels: PivotColumnHeaderCell[][]; // ์ดํ•ฉ๊ณ„ grandTotals: { @@ -361,6 +364,14 @@ export interface PivotFlatColumn { isTotal?: boolean; } +// ์—ด ํ—ค๋” ์…€ (๋‹ค์ค‘ ํ–‰ ํ—ค๋”์šฉ) +export interface PivotColumnHeaderCell { + caption: string; // ํ‘œ์‹œ ํ…์ŠคํŠธ + colSpan: number; // ๋ณ‘ํ•ฉํ•  ์—ด ์ˆ˜ + path: string[]; // ์ „์ฒด ๊ฒฝ๋กœ + level: number; // ๋ ˆ๋ฒจ (0๋ถ€ํ„ฐ ์‹œ์ž‘) +} + // ==================== ์ƒํƒœ ๊ด€๋ฆฌ ==================== export interface PivotGridState { diff --git a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts index 9c3b5bc1..35893dea 100644 --- a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts +++ b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts @@ -10,6 +10,7 @@ import { PivotFlatRow, PivotFlatColumn, PivotCellValue, + PivotColumnHeaderCell, DateGroupInterval, AggregationType, SummaryDisplayMode, @@ -76,6 +77,31 @@ export function pathToKey(path: string[]): string { return path.join("||"); } +/** + * ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ (์—ด ์ „์ฒด ํ™•์žฅ์šฉ) + */ +function generateAllPaths( + data: Record[], + fields: PivotFieldConfig[] +): string[] { + const allPaths: string[] = []; + + // ๊ฐ ๋ ˆ๋ฒจ๊นŒ์ง€์˜ ๊ณ ์œ  ๊ฒฝ๋กœ ์ˆ˜์ง‘ + for (let depth = 1; depth <= fields.length; depth++) { + const fieldsAtDepth = fields.slice(0, depth); + const pathSet = new Set(); + + data.forEach((row) => { + const path = fieldsAtDepth.map((f) => getFieldValue(row, f)); + pathSet.add(pathToKey(path)); + }); + + pathSet.forEach((pathKey) => allPaths.push(pathKey)); + } + + return allPaths; +} + /** * ํ‚ค๋ฅผ ๊ฒฝ๋กœ๋กœ ๋ณ€ํ™˜ */ @@ -326,6 +352,66 @@ function getMaxColumnLevel( return Math.min(maxLevel, totalFields - 1); } +/** + * ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” ์ƒ์„ฑ + * ๊ฐ ๋ ˆ๋ฒจ๋ณ„๋กœ ์…€๊ณผ colSpan ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ + */ +function buildColumnHeaderLevels( + nodes: PivotHeaderNode[], + totalLevels: number +): PivotColumnHeaderCell[][] { + if (totalLevels === 0 || nodes.length === 0) { + return []; + } + + const levels: PivotColumnHeaderCell[][] = Array.from( + { length: totalLevels }, + () => [] + ); + + // ๋ฆฌํ”„ ๋…ธ๋“œ ์ˆ˜ ๊ณ„์‚ฐ (colSpan ๊ณ„์‚ฐ์šฉ) + function countLeaves(node: PivotHeaderNode): number { + if (!node.children || node.children.length === 0 || !node.isExpanded) { + return 1; + } + return node.children.reduce((sum, child) => sum + countLeaves(child), 0); + } + + // ํŠธ๋ฆฌ ์ˆœํšŒํ•˜๋ฉฐ ๊ฐ ๋ ˆ๋ฒจ์— ์…€ ์ถ”๊ฐ€ + function traverse(node: PivotHeaderNode, level: number) { + const colSpan = countLeaves(node); + + levels[level].push({ + caption: node.caption, + colSpan, + path: node.path, + level, + }); + + if (node.children && node.isExpanded) { + for (const child of node.children) { + traverse(child, level + 1); + } + } else if (level < totalLevels - 1) { + // ํ™•์žฅ๋˜์ง€ ์•Š์€ ๋…ธ๋“œ๋Š” ๋‹ค์Œ ๋ ˆ๋ฒจ๋“ค์— ๋นˆ ์…€๋กœ ์ฑ„์›€ + for (let i = level + 1; i < totalLevels; i++) { + levels[i].push({ + caption: "", + colSpan, + path: node.path, + level: i, + }); + } + } + } + + for (const node of nodes) { + traverse(node, 0); + } + + return levels; +} + // ==================== ๋ฐ์ดํ„ฐ ๋งคํŠธ๋ฆญ์Šค ์ƒ์„ฑ ==================== /** @@ -735,12 +821,11 @@ export function processPivotData( uniqueValues.forEach((val) => expandedRowSet.add(val)); } - if (expandedColumnPaths.length === 0 && columnFields.length > 0) { - const firstField = columnFields[0]; - const uniqueValues = new Set( - filteredData.map((row) => getFieldValue(row, firstField)) - ); - uniqueValues.forEach((val) => expandedColSet.add(val)); + // ์—ด์€ ํ•ญ์ƒ ์ „์ฒด ํ™•์žฅ (์—ด ํ—ค๋”๋Š” ํ™•์žฅ/์ถ•์†Œ UI๊ฐ€ ์—†์Œ) + // ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์—ด ๊ฒฝ๋กœ๋ฅผ ํ™•์žฅ ์ƒํƒœ๋กœ ์„ค์ • + if (columnFields.length > 0) { + const allColumnPaths = generateAllPaths(filteredData, columnFields); + allColumnPaths.forEach((pathKey) => expandedColSet.add(pathKey)); } // ํ—ค๋” ํŠธ๋ฆฌ ์ƒ์„ฑ @@ -788,6 +873,12 @@ export function processPivotData( grandTotals.grand ); + // ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” ์ƒ์„ฑ + const columnHeaderLevels = buildColumnHeaderLevels( + columnHeaders, + columnFields.length + ); + return { rowHeaders, columnHeaders, @@ -799,6 +890,7 @@ export function processPivotData( caption: path[path.length - 1] || "", span: 1, })), + columnHeaderLevels, grandTotals, }; } From 2327d6e97c0479a656ec30710f69c90743e8a845 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 21 Jan 2026 17:14:07 +0900 Subject: [PATCH 21/22] =?UTF-8?q?=EC=83=81=ED=83=9C=EC=9C=A0=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 8db463c4..13cb1a68 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -447,8 +447,8 @@ export const PivotGridComponent: React.FC = ({ useEffect(() => { if (!isStateRestored) return; // ๋ณต์› ์™„๋ฃŒ ์ „์—๋Š” ๋ฌด์‹œ - // persistState๊ฐ€ ์ผœ์ ธ์žˆ๊ณ  ์ €์žฅ๋œ ์ƒํƒœ๊ฐ€ ์žˆ์œผ๋ฉด initialFields๋กœ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ - if (persistState && typeof window !== "undefined") { + // ์ €์žฅ๋œ ์ƒํƒœ๊ฐ€ ์žˆ์œผ๋ฉด initialFields๋กœ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Œ + if (typeof window !== "undefined") { const savedState = localStorage.getItem(stateStorageKey); if (savedState) return; // ์ด๋ฏธ ์ €์žฅ๋œ ์ƒํƒœ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฌด์‹œ } @@ -456,13 +456,32 @@ export const PivotGridComponent: React.FC = ({ if (initialFields.length > 0) { setFields(initialFields); } - }, [initialFields, isStateRestored, persistState, stateStorageKey]); + // persistState๋Š” ์˜์กด์„ฑ์—์„œ ์ œ์™ธ - ์ฒดํฌ๋ฐ•์Šค ๋ณ€๊ฒฝ ์‹œ ํ˜„์žฌ ์ƒํƒœ ์œ ์ง€ + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialFields, isStateRestored, stateStorageKey]); - // ์ƒํƒœ ์œ ์ง€ ์„ค์ • ์ €์žฅ + // ์ƒํƒœ ์œ ์ง€ ์„ค์ • ์ €์žฅ + ์ผœ์งˆ ๋•Œ ํ˜„์žฌ ์ƒํƒœ ์ฆ‰์‹œ ์ €์žฅ useEffect(() => { if (typeof window === "undefined") return; localStorage.setItem(persistSettingKey, String(persistState)); - }, [persistState, persistSettingKey]); + + // ์ƒํƒœ ์œ ์ง€๋ฅผ ์ผœ๋ฉด ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ฆ‰์‹œ ์ €์žฅ + if (persistState && isStateRestored && fields.length > 0) { + const stateToSave = { + version: PIVOT_STATE_VERSION, + fields, + pivotState, + sortConfig, + columnWidths, + }; + localStorage.setItem(stateStorageKey, JSON.stringify(stateToSave)); + } + + // ์ƒํƒœ ์œ ์ง€๋ฅผ ๋„๋ฉด ์ €์žฅ๋œ ์ƒํƒœ ์‚ญ์ œ + if (!persistState) { + localStorage.removeItem(stateStorageKey); + } + }, [persistState, persistSettingKey, isStateRestored, fields, pivotState, sortConfig, columnWidths, stateStorageKey]); // ์ƒํƒœ ์ €์žฅ (localStorage) const saveStateToStorage = useCallback(() => { From e19a28fa52511448d7095638038c03688f8589e0 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Thu, 22 Jan 2026 09:59:20 +0900 Subject: [PATCH 22/22] =?UTF-8?q?=EC=97=91=EC=85=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=20=EA=B3=B5=EA=B8=89=EC=97=85?= =?UTF-8?q?=EC=B2=B4=20=EB=AA=85=EC=9D=B4=20=EC=95=88=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EA=B0=80=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/ExcelUploadModal.tsx | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 64fe38b8..cddbb73f 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -928,12 +928,39 @@ export const ExcelUploadModal: React.FC = ({ {field.inputType === "entity" ? (
0 ? 2 : 1} - > -
- {rowFields.map((f, idx) => ( -
- {f.caption} - { - const newFields = fields.map((fld) => - fld.field === field.field && fld.area === "row" - ? { ...fld, filterValues: values, filterType: type } - : fld - ); - handleFieldsChange(newFields); - }} - trigger={ - - } - /> - {idx < rowFields.length - 1 && /} -
+ {/* ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” */} + {columnHeaderLevels.length > 0 ? ( + // ์—ด ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ: ๊ฐ ๋ ˆ๋ฒจ๋ณ„๋กœ ํ–‰ ์ƒ์„ฑ + columnHeaderLevels.map((levelCells, levelIdx) => ( +
1 ? 1 : 0)} + > +
+ {rowFields.map((f, idx) => ( +
+ {f.caption} + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "row" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + {idx < rowFields.length - 1 && /} +
+ ))} + {rowFields.length === 0 && ํ•ญ๋ชฉ} +
+
+
+ {cell.caption || "(์ „์ฒด)"} + {levelIdx === columnHeaderLevels.length - 1 && dataFields.length === 1 && ( + + )} +
+
1 ? 1 : 0)} + > + ์ด๊ณ„ + 0 && ( + 1 ? 1 : 0)} + > +
+ {columnFields.map((f) => ( + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "column" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + ))} +
+
1 ? 2 : 1} > -
- {columnFields.map((f) => ( - { - const newFields = fields.map((fld) => - fld.field === field.field && fld.area === "column" - ? { ...fld, filterValues: values, filterType: type } - : fld - ); - handleFieldsChange(newFields); - }} - trigger={ - - } - /> +
+ {rowFields.map((f, idx) => ( +
+ {f.caption} + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "row" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + {idx < rowFields.length - 1 && /} +
))} + {rowFields.length === 0 && ํ•ญ๋ชฉ}
handleSort(dataFields[0].field) : undefined} + > +
+ {col.caption || "(์ „์ฒด)"} + {dataFields.length === 1 && } +
+
handleResizeStart(idx, e)} + /> +
1 ? 2 : 1} + > + ์ด๊ณ„ +