From 2645d627daf608fb204acde791ff6143fb6b0d5e Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 8 Jan 2026 12:25:35 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EC=88=98=EC=A3=BC=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=AA=A8=EB=8B=AC=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20-=20UPDATE=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B3=91=ED=95=A9=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20id=20=ED=83=80=EC=9E=85=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/lib/api/dynamicForm.ts | 7 ++ .../UniversalFormModalComponent.tsx | 14 ++-- frontend/lib/utils/buttonActions.ts | 68 +++++++++++-------- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/frontend/lib/api/dynamicForm.ts b/frontend/lib/api/dynamicForm.ts index a5a3b2eb..d2433c48 100644 --- a/frontend/lib/api/dynamicForm.ts +++ b/frontend/lib/api/dynamicForm.ts @@ -93,10 +93,15 @@ export class DynamicFormApi { ): Promise> { try { console.log("๐Ÿ”„ ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์š”์ฒญ:", { id, formData }); + console.log("๐ŸŒ API URL:", `/dynamic-form/${id}`); + console.log("๐Ÿ“ฆ ์š”์ฒญ ๋ณธ๋ฌธ:", JSON.stringify(formData, null, 2)); const response = await apiClient.put(`/dynamic-form/${id}`, formData); console.log("โœ… ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต:", response.data); + console.log("๐Ÿ“Š ์‘๋‹ต ์ƒํƒœ:", response.status); + console.log("๐Ÿ“‹ ์‘๋‹ต ํ—ค๋”:", response.headers); + return { success: true, data: response.data, @@ -104,6 +109,8 @@ export class DynamicFormApi { }; } catch (error: any) { console.error("โŒ ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", error); + console.error("๐Ÿ“Š ์—๋Ÿฌ ์‘๋‹ต:", error.response?.data); + console.error("๐Ÿ“Š ์—๋Ÿฌ ์ƒํƒœ:", error.response?.status); const errorMessage = error.response?.data?.message || error.message || "๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 4ee024a4..3e043331 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -333,6 +333,14 @@ export function UniversalFormModalComponent({ } } + // ๐Ÿ†• ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (ํ’ˆ๋ชฉ ๋ฆฌ์ŠคํŠธ ๋“ฑ) + for (const [key, value] of Object.entries(formData)) { + if (key.startsWith("_tableSection_") && Array.isArray(value)) { + event.detail.formData[key] = value; + console.log(`[UniversalFormModal] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ณ‘ํ•ฉ: ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); + } + } + // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (UPDATE/DELETE ์ถ”์ ์šฉ) if (originalGroupedData.length > 0) { event.detail.formData._originalGroupedData = originalGroupedData; @@ -355,15 +363,9 @@ export function UniversalFormModalComponent({ // ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜ ์ฐพ๊ธฐ const tableSection = config.sections.find((s) => s.type === "table"); if (!tableSection) { - // console.log("[UniversalFormModal] ํ…Œ์ด๋ธ” ์„น์…˜ ์—†์Œ - _groupedData ๋ฌด์‹œ"); return; } - // console.log("[UniversalFormModal] ์ˆ˜์ • ๋ชจ๋“œ - ํ…Œ์ด๋ธ” ์„น์…˜ ์ดˆ๊ธฐํ™”:", { - // sectionId: tableSection.id, - // itemCount: _groupedData.length, - // }); - // ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ˆ˜์ •/์‚ญ์ œ ์ถ”์ ์šฉ) setOriginalGroupedData(JSON.parse(JSON.stringify(_groupedData))); diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4c9d638b..7512d6c0 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -724,11 +724,16 @@ export class ButtonActionExecutor { // originalData๋Š” ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ editData๋กœ ์ „๋‹ฌ๋˜์–ด context.originalData๋กœ ์„ค์ •๋จ // ๋นˆ ๊ฐ์ฒด {}๋„ truthy์ด๋ฏ€๋กœ Object.keys๋กœ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์œ ๋ฌด ํ™•์ธ const hasRealOriginalData = originalData && Object.keys(originalData).length > 0; - const isUpdate = hasRealOriginalData && !!primaryKeyValue; + + // ๐Ÿ†• ํด๋ฐฑ ๋กœ์ง: originalData๊ฐ€ ์—†์–ด๋„ formData์— id๊ฐ€ ์žˆ์œผ๋ฉด UPDATE๋กœ ํŒ๋‹จ + // ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ๋“ฑ์—์„œ originalData ์ „๋‹ฌ์ด ๋ˆ„๋ฝ๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌ + const hasIdInFormData = formData.id !== undefined && formData.id !== null && formData.id !== ""; + const isUpdate = (hasRealOriginalData || hasIdInFormData) && !!primaryKeyValue; console.log("๐Ÿ” [handleSave] INSERT/UPDATE ํŒ๋‹จ:", { hasOriginalData: !!originalData, hasRealOriginalData, + hasIdInFormData, originalDataKeys: originalData ? Object.keys(originalData) : [], primaryKeyValue, isUpdate, @@ -741,18 +746,18 @@ export class ButtonActionExecutor { // UPDATE ์ฒ˜๋ฆฌ - ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ์‚ฌ์šฉ (์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) console.log("๐Ÿ”„ UPDATE ๋ชจ๋“œ๋กœ ์ €์žฅ:", { primaryKeyValue, - formData, - originalData, hasOriginalData: !!originalData, + hasIdInFormData, + updateReason: hasRealOriginalData ? "originalData ์กด์žฌ" : "formData.id ์กด์žฌ (ํด๋ฐฑ)", }); - if (originalData) { + if (hasRealOriginalData) { // ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ: ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ ์—…๋ฐ์ดํŠธ console.log("๐Ÿ“ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ์‹คํ–‰ (๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ)"); saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName); } else { - // ์ „์ฒด ์—…๋ฐ์ดํŠธ (๊ธฐ์กด ๋ฐฉ์‹) - console.log("๐Ÿ“ ์ „์ฒด ์—…๋ฐ์ดํŠธ ์‹คํ–‰ (๋ชจ๋“  ํ•„๋“œ)"); + // ์ „์ฒด ์—…๋ฐ์ดํŠธ (originalData ์—†์ด id๋กœ UPDATE ํŒ๋‹จ๋œ ๊ฒฝ์šฐ) + console.log("๐Ÿ“ ์ „์ฒด ์—…๋ฐ์ดํŠธ ์‹คํ–‰ (originalData ์—†์Œ - ํด๋ฐฑ ๋ชจ๋“œ)"); saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, { tableName, data: formData, @@ -1862,37 +1867,45 @@ export class ButtonActionExecutor { const originalItem = originalGroupedData.find((orig) => orig.id === item.id); if (!originalItem) { - console.warn(`โš ๏ธ [UPDATE] ์›๋ณธ ๋ฐ์ดํ„ฐ ์—†์Œ - INSERT๋กœ ์ฒ˜๋ฆฌ: id=${item.id}`); - // ์›๋ณธ์ด ์—†์œผ๋ฉด ์‹ ๊ทœ๋กœ ์ฒ˜๋ฆฌ - const rowToSave = { ...commonFieldsData, ...item, ...userInfo }; - Object.keys(rowToSave).forEach((key) => { + // ๐Ÿ†• ํด๋ฐฑ ๋กœ์ง: ์›๋ณธ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด๋„ id๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ์‹œ๋„ + // originalGroupedData ์ „๋‹ฌ์ด ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌ + console.warn(`โš ๏ธ [UPDATE] ์›๋ณธ ๋ฐ์ดํ„ฐ ์—†์Œ - id๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ UPDATE ์‹œ๋„ (ํด๋ฐฑ): id=${item.id}`); + + // โš ๏ธ ์ค‘์š”: commonFieldsData๊ฐ€ item๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์•„์•ผ ํ•จ + // item์— ์žˆ๋Š” ๊ธฐ์กด ๊ฐ’(์˜ˆ: manager_id=123)์ด commonFieldsData์˜ ์ƒˆ ๊ฐ’(manager_id=234)์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š๋„๋ก + // ์ˆœ์„œ: item(๊ธฐ์กด) โ†’ commonFieldsData(์ƒˆ๋กœ ์ž…๋ ฅ) โ†’ userInfo(๋ฉ”ํƒ€๋ฐ์ดํ„ฐ) + const rowToUpdate = { ...item, ...commonFieldsData, ...userInfo }; + Object.keys(rowToUpdate).forEach((key) => { if (key.startsWith("_")) { - delete rowToSave[key]; + delete rowToUpdate[key]; } }); - delete rowToSave.id; // id ์ œ๊ฑฐํ•˜์—ฌ INSERT - // ๐Ÿ†• ๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ ID ์—ฐ๊ฒฐ (๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ) - if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) { - rowToSave[saveConfig.primaryKeyColumn] = mainRecordId; - } - - const saveResult = await DynamicFormApi.saveFormData({ - screenId: screenId!, + console.log("๐Ÿ“ [UPDATE ํด๋ฐฑ] ์ €์žฅํ•  ๋ฐ์ดํ„ฐ:", { + id: item.id, tableName: saveTableName, - data: rowToSave, + commonFieldsData, + itemFields: Object.keys(item).filter(k => !k.startsWith("_")), + rowToUpdate, }); - if (!saveResult.success) { - throw new Error(saveResult.message || "ํ’ˆ๋ชฉ ์ €์žฅ ์‹คํŒจ"); + // id๋ฅผ ์œ ์ง€ํ•˜๊ณ  UPDATE ์‹คํ–‰ + const updateResult = await DynamicFormApi.updateFormData(item.id, { + tableName: saveTableName, + data: rowToUpdate, + }); + + if (!updateResult.success) { + throw new Error(updateResult.message || "ํ’ˆ๋ชฉ ์ˆ˜์ • ์‹คํŒจ"); } - insertedCount++; + updatedCount++; continue; } // ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ™•์ธ (๊ณตํ†ต ํ•„๋“œ ํฌํ•จ) - const currentDataWithCommon = { ...commonFieldsData, ...item }; + // โš ๏ธ ์ค‘์š”: commonFieldsData๊ฐ€ item๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์•„์•ผ ํ•จ (์ƒˆ๋กœ ์ž…๋ ฅํ•œ ๊ฐ’์ด ๊ธฐ์กด ๊ฐ’์„ ๋ฎ์–ด์”€) + const currentDataWithCommon = { ...item, ...commonFieldsData }; const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon); if (hasChanges) { @@ -1917,13 +1930,14 @@ export class ButtonActionExecutor { } // 3๏ธโƒฃ ์‚ญ์ œ๋œ ํ’ˆ๋ชฉ DELETE (์›๋ณธ์—๋Š” ์žˆ์ง€๋งŒ ํ˜„์žฌ์—๋Š” ์—†๋Š” ํ•ญ๋ชฉ) - const currentIds = new Set(currentItems.map((item) => item.id).filter(Boolean)); - const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id)); + // โš ๏ธ id ํƒ€์ž… ํ†ต์ผ: ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋น„๊ต (์ˆซ์ž vs ๋ฌธ์ž์—ด ๋ถˆ์ผ์น˜ ๋ฐฉ์ง€) + const currentIds = new Set(currentItems.map((item) => String(item.id)).filter(Boolean)); + const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(String(orig.id))); for (const deletedItem of deletedItems) { console.log(`๐Ÿ—‘๏ธ [DELETE] ํ’ˆ๋ชฉ ์‚ญ์ œ: id=${deletedItem.id}, tableName=${saveTableName}`); - const deleteResult = await DynamicFormApi.deleteFormDataFromTable(saveTableName, deletedItem.id); + const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deletedItem.id, saveTableName); if (!deleteResult.success) { throw new Error(deleteResult.message || "ํ’ˆ๋ชฉ ์‚ญ์ œ ์‹คํŒจ"); From 3e9bf29bcf97fa0d311386319d449f4612d2a833 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Thu, 8 Jan 2026 14:13:19 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20SplitPanelLayout=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=20=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20groupByColumns=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EB=A9=80?= =?UTF-8?q?=ED=8B=B0=ED=85=8C=EB=84=8C=EC=8B=9C=20=EB=B3=B4=ED=98=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(=EC=98=81=EC=97=85=EA=B4=80=EB=A6=AC=5F?= =?UTF-8?q?=EA=B1=B0=EB=9E=98=EC=B2=98=EB=B3=84=20=ED=92=88=EB=AA=A9=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=93=B1=EC=97=90=EC=84=9C,,)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/src/routes/dataRoutes.ts | 6 +- backend-node/src/services/dataService.ts | 46 +++++++- .../SplitPanelLayoutComponent.tsx | 102 ++++++++++++------ 3 files changed, 119 insertions(+), 35 deletions(-) diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index f87aa5d6..f9d88d92 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -698,6 +698,7 @@ router.post( try { const { tableName } = req.params; const filterConditions = req.body; + const userCompany = req.user?.companyCode; if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ @@ -706,11 +707,12 @@ router.post( }); } - console.log(`๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ:`, { tableName, filterConditions }); + console.log(`๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ:`, { tableName, filterConditions, userCompany }); const result = await dataService.deleteGroupRecords( tableName, - filterConditions + filterConditions, + userCompany // ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); if (!result.success) { diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index a1a494f2..75c57673 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -1189,6 +1189,13 @@ class DataService { [tableName] ); + console.log(`๐Ÿ” ํ…Œ์ด๋ธ” ${tableName}์˜ Primary Key ์กฐํšŒ ๊ฒฐ๊ณผ:`, { + pkColumns: pkResult.map((r) => r.attname), + pkCount: pkResult.length, + inputId: typeof id === "object" ? JSON.stringify(id).substring(0, 200) + "..." : id, + inputIdType: typeof id, + }); + let whereClauses: string[] = []; let params: any[] = []; @@ -1216,17 +1223,31 @@ class DataService { params.push(typeof id === "object" ? id[pkColumn] : id); } - const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(" AND ")}`; + const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(" AND ")} RETURNING *`; console.log(`๐Ÿ—‘๏ธ ์‚ญ์ œ ์ฟผ๋ฆฌ:`, queryText, params); const result = await query(queryText, params); + // ์‚ญ์ œ๋œ ํ–‰์ด ์—†์œผ๋ฉด ์‹คํŒจ ์ฒ˜๋ฆฌ + if (result.length === 0) { + console.warn( + `โš ๏ธ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์‹คํŒจ: ${tableName}, ํ•ด๋‹น ์กฐ๊ฑด์— ๋งž๋Š” ๋ ˆ์ฝ”๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`, + { whereClauses, params } + ); + return { + success: false, + message: "์‚ญ์ œํ•  ๋ ˆ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ์‚ญ์ œ๋˜์—ˆ๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.", + error: "RECORD_NOT_FOUND", + }; + } + console.log( `โœ… ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์™„๋ฃŒ: ${tableName}, ์˜ํ–ฅ๋ฐ›์€ ํ–‰: ${result.length}` ); return { success: true, + data: result[0], // ์‚ญ์ œ๋œ ๋ ˆ์ฝ”๋“œ ์ •๋ณด ๋ฐ˜ํ™˜ }; } catch (error) { console.error(`๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์˜ค๋ฅ˜ (${tableName}):`, error); @@ -1240,10 +1261,14 @@ class DataService { /** * ์กฐ๊ฑด์— ๋งž๋Š” ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ (๊ทธ๋ฃน ์‚ญ์ œ) + * @param tableName ํ…Œ์ด๋ธ”๋ช… + * @param filterConditions ์‚ญ์ œ ์กฐ๊ฑด + * @param userCompany ์‚ฌ์šฉ์ž ํšŒ์‚ฌ ์ฝ”๋“œ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง) */ async deleteGroupRecords( tableName: string, - filterConditions: Record + filterConditions: Record, + userCompany?: string ): Promise> { try { const validation = await this.validateTableAccess(tableName); @@ -1255,6 +1280,7 @@ class DataService { const whereValues: any[] = []; let paramIndex = 1; + // ์‚ฌ์šฉ์ž ํ•„ํ„ฐ ์กฐ๊ฑด ์ถ”๊ฐ€ for (const [key, value] of Object.entries(filterConditions)) { whereConditions.push(`"${key}" = $${paramIndex}`); whereValues.push(value); @@ -1269,10 +1295,24 @@ class DataService { }; } + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: company_code ํ•„ํ„ฐ๋ง (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ œ์™ธ) + const hasCompanyCode = await this.checkColumnExists(tableName, "company_code"); + if (hasCompanyCode && userCompany && userCompany !== "*") { + whereConditions.push(`"company_code" = $${paramIndex}`); + whereValues.push(userCompany); + paramIndex++; + console.log(`๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ ์ ์šฉ: company_code = ${userCompany}`); + } + const whereClause = whereConditions.join(" AND "); const deleteQuery = `DELETE FROM "${tableName}" WHERE ${whereClause} RETURNING *`; - console.log(`๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ:`, { tableName, conditions: filterConditions }); + console.log(`๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ:`, { + tableName, + conditions: filterConditions, + userCompany, + whereClause, + }); const result = await pool.query(deleteQuery, whereValues); diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ad7f5302..afc5c13e 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1613,47 +1613,89 @@ export const SplitPanelLayoutComponent: React.FC try { console.log("๐Ÿ—‘๏ธ ๋ฐ์ดํ„ฐ ์‚ญ์ œ:", { tableName, primaryKey }); - // ๐Ÿ” ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ๋””๋ฒ„๊น… - console.log("๐Ÿ” ์ค‘๋ณต ์ œ๊ฑฐ ๋””๋ฒ„๊น…:", { + // ๐Ÿ” ๊ทธ๋ฃน ์‚ญ์ œ ์„ค์ • ํ™•์ธ (editButton.groupByColumns ๋˜๋Š” deduplication) + const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || []; + const deduplication = componentConfig.rightPanel?.dataFilter?.deduplication; + + console.log("๐Ÿ” ์‚ญ์ œ ์„ค์ • ๋””๋ฒ„๊น…:", { panel: deleteModalPanel, - dataFilter: componentConfig.rightPanel?.dataFilter, - deduplication: componentConfig.rightPanel?.dataFilter?.deduplication, - enabled: componentConfig.rightPanel?.dataFilter?.deduplication?.enabled, + groupByColumns, + deduplication, + deduplicationEnabled: deduplication?.enabled, }); let result; - // ๐Ÿ”ง ์ค‘๋ณต ์ œ๊ฑฐ๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, groupByColumn ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ๊ด€๋ จ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ - if (deleteModalPanel === "right" && componentConfig.rightPanel?.dataFilter?.deduplication?.enabled) { - const deduplication = componentConfig.rightPanel.dataFilter.deduplication; - const groupByColumn = deduplication.groupByColumn; - - if (groupByColumn && deleteModalItem[groupByColumn]) { - const groupValue = deleteModalItem[groupByColumn]; - console.log(`๐Ÿ”— ์ค‘๋ณต ์ œ๊ฑฐ ํ™œ์„ฑํ™”: ${groupByColumn} = ${groupValue} ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ`); - - // groupByColumn ๊ฐ’์œผ๋กœ ํ•„ํ„ฐ๋งํ•˜์—ฌ ์‚ญ์ œ - const filterConditions: Record = { - [groupByColumn]: groupValue, - }; - - // ์ขŒ์ธก ํŒจ๋„์˜ ์„ ํƒ๋œ ํ•ญ๋ชฉ ์ •๋ณด๋„ ํฌํ•จ (customer_id ๋“ฑ) - if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") { - const leftColumn = componentConfig.rightPanel.join.leftColumn; - const rightColumn = componentConfig.rightPanel.join.rightColumn; - filterConditions[rightColumn] = selectedLeftItem[leftColumn]; + // ๐Ÿ”ง ์šฐ์ธก ํŒจ๋„ ์‚ญ์ œ ์‹œ ๊ทธ๋ฃน ์‚ญ์ œ ์กฐ๊ฑด ํ™•์ธ + if (deleteModalPanel === "right") { + // 1. groupByColumns๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ (ํŒจ๋„ ์„ค์ •์—์„œ ์„ ํƒ๋œ ์ปฌ๋Ÿผ๋“ค) + if (groupByColumns.length > 0) { + const filterConditions: Record = {}; + + // ์„ ํƒ๋œ ์ปฌ๋Ÿผ๋“ค์˜ ๊ฐ’์„ ํ•„ํ„ฐ ์กฐ๊ฑด์œผ๋กœ ์ถ”๊ฐ€ + for (const col of groupByColumns) { + if (deleteModalItem[col] !== undefined && deleteModalItem[col] !== null) { + filterConditions[col] = deleteModalItem[col]; + } } - console.log("๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ ์กฐ๊ฑด:", filterConditions); + // ๐Ÿ”’ ์•ˆ์ „์žฅ์น˜: ์กฐ์ธ ๋ชจ๋“œ์—์„œ ์ขŒ์ธก ํŒจ๋„์˜ ํ‚ค ๊ฐ’๋„ ํ•„ํ„ฐ ์กฐ๊ฑด์— ํฌํ•จ + // (๋‹ค๋ฅธ ๊ฑฐ๋ž˜์ฒ˜์˜ ๊ฐ™์€ ํ’ˆ๋ชฉ์ด ์‚ญ์ œ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€) + if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") { + const leftColumn = componentConfig.rightPanel.join?.leftColumn; + const rightColumn = componentConfig.rightPanel.join?.rightColumn; + if (leftColumn && rightColumn && selectedLeftItem[leftColumn]) { + // rightColumn์ด filterConditions์— ์—†์œผ๋ฉด ์ถ”๊ฐ€ + if (!filterConditions[rightColumn]) { + filterConditions[rightColumn] = selectedLeftItem[leftColumn]; + console.log(`๐Ÿ”’ ์•ˆ์ „์žฅ์น˜: ${rightColumn} = ${selectedLeftItem[leftColumn]} ์ถ”๊ฐ€`); + } + } + } - // ๊ทธ๋ฃน ์‚ญ์ œ API ํ˜ธ์ถœ - result = await dataApi.deleteGroupRecords(tableName, filterConditions); - } else { - // ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ + // ํ•„ํ„ฐ ์กฐ๊ฑด์ด ์žˆ์œผ๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ + if (Object.keys(filterConditions).length > 0) { + console.log(`๐Ÿ”— ๊ทธ๋ฃน ์‚ญ์ œ (groupByColumns): ${groupByColumns.join(", ")} ๊ธฐ์ค€`); + console.log("๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ ์กฐ๊ฑด:", filterConditions); + + result = await dataApi.deleteGroupRecords(tableName, filterConditions); + } else { + // ํ•„ํ„ฐ ์กฐ๊ฑด์ด ์—†์œผ๋ฉด ๋‹จ์ผ ์‚ญ์ œ + console.log("โš ๏ธ groupByColumns ๊ฐ’์ด ์—†์–ด ๋‹จ์ผ ์‚ญ์ œ๋กœ ์ „ํ™˜"); + result = await dataApi.deleteRecord(tableName, primaryKey); + } + } + // 2. ์ค‘๋ณต ์ œ๊ฑฐ(deduplication)๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ + else if (deduplication?.enabled && deduplication?.groupByColumn) { + const groupByColumn = deduplication.groupByColumn; + const groupValue = deleteModalItem[groupByColumn]; + + if (groupValue) { + console.log(`๐Ÿ”— ์ค‘๋ณต ์ œ๊ฑฐ ํ™œ์„ฑํ™”: ${groupByColumn} = ${groupValue} ๊ธฐ์ค€์œผ๋กœ ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ`); + + const filterConditions: Record = { + [groupByColumn]: groupValue, + }; + + // ์ขŒ์ธก ํŒจ๋„์˜ ์„ ํƒ๋œ ํ•ญ๋ชฉ ์ •๋ณด๋„ ํฌํ•จ (customer_id ๋“ฑ) + if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") { + const leftColumn = componentConfig.rightPanel.join.leftColumn; + const rightColumn = componentConfig.rightPanel.join.rightColumn; + filterConditions[rightColumn] = selectedLeftItem[leftColumn]; + } + + console.log("๐Ÿ—‘๏ธ ๊ทธ๋ฃน ์‚ญ์ œ ์กฐ๊ฑด:", filterConditions); + result = await dataApi.deleteGroupRecords(tableName, filterConditions); + } else { + result = await dataApi.deleteRecord(tableName, primaryKey); + } + } + // 3. ๊ทธ ์™ธ: ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ + else { result = await dataApi.deleteRecord(tableName, primaryKey); } } else { - // ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ + // ์ขŒ์ธก ํŒจ๋„: ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ result = await dataApi.deleteRecord(tableName, primaryKey); } From 3f81c449ad218a0f34372f39d2d238f5a31c4f19 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 8 Jan 2026 14:24:07 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=B3=91=ED=95=A9?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/codeMergeController.ts | 172 ++++++++++++++++++ backend-node/src/routes/codeMergeRoutes.ts | 20 +- frontend/lib/utils/buttonActions.ts | 36 +++- 3 files changed, 216 insertions(+), 12 deletions(-) diff --git a/backend-node/src/controllers/codeMergeController.ts b/backend-node/src/controllers/codeMergeController.ts index 29abfa8e..74d9e893 100644 --- a/backend-node/src/controllers/codeMergeController.ts +++ b/backend-node/src/controllers/codeMergeController.ts @@ -282,3 +282,175 @@ export async function previewCodeMerge( } } +/** + * ๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ - ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์—์„œ ํ•ด๋‹น ๊ฐ’์„ ์ฐพ์•„ ๋ณ€๊ฒฝ + * ์ปฌ๋Ÿผ๋ช…์— ์ƒ๊ด€์—†์ด oldValue๋ฅผ ๊ฐ€์ง„ ๋ชจ๋“  ๊ณณ์„ newValue๋กœ ๋ณ€๊ฒฝ + */ +export async function mergeCodeByValue( + req: AuthenticatedRequest, + res: Response +): Promise { + const { oldValue, newValue } = req.body; + const companyCode = req.user?.companyCode; + + try { + // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ + if (!oldValue || !newValue) { + res.status(400).json({ + success: false, + message: "ํ•„์ˆ˜ ํ•„๋“œ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (oldValue, newValue)", + }); + return; + } + + if (!companyCode) { + res.status(401).json({ + success: false, + message: "์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", + }); + return; + } + + // ๊ฐ™์€ ๊ฐ’์œผ๋กœ ๋ณ‘ํ•ฉ ์‹œ๋„ ๋ฐฉ์ง€ + if (oldValue === newValue) { + res.status(400).json({ + success: false, + message: "๊ธฐ์กด ๊ฐ’๊ณผ ์ƒˆ ๊ฐ’์ด ๋™์ผํ•ฉ๋‹ˆ๋‹ค.", + }); + return; + } + + logger.info("๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์‹œ์ž‘", { + oldValue, + newValue, + companyCode, + userId: req.user?.userId, + }); + + // PostgreSQL ํ•จ์ˆ˜ ํ˜ธ์ถœ + const result = await pool.query( + "SELECT * FROM merge_code_by_value($1, $2, $3)", + [oldValue, newValue, companyCode] + ); + + // ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ + const affectedData = Array.isArray(result) ? result : ((result as any).rows || []); + const totalRows = affectedData.reduce( + (sum: number, row: any) => sum + parseInt(row.out_rows_updated || 0), + 0 + ); + + logger.info("๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ", { + oldValue, + newValue, + affectedTablesCount: affectedData.length, + totalRowsUpdated: totalRows, + }); + + res.json({ + success: true, + message: `์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ: ${oldValue} โ†’ ${newValue}`, + data: { + oldValue, + newValue, + affectedData: affectedData.map((row: any) => ({ + tableName: row.out_table_name, + columnName: row.out_column_name, + rowsUpdated: parseInt(row.out_rows_updated), + })), + totalRowsUpdated: totalRows, + }, + }); + } catch (error: any) { + logger.error("๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์‹คํŒจ:", { + error: error.message, + stack: error.stack, + oldValue, + newValue, + }); + + res.status(500).json({ + success: false, + message: "์ฝ”๋“œ ๋ณ‘ํ•ฉ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: { + code: "CODE_MERGE_BY_VALUE_ERROR", + details: error.message, + }, + }); + } +} + +/** + * ๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + * ์ปฌ๋Ÿผ๋ช…์— ์ƒ๊ด€์—†์ด ํ•ด๋‹น ๊ฐ’์„ ๊ฐ€์ง„ ๋ชจ๋“  ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์กฐํšŒ + */ +export async function previewMergeCodeByValue( + req: AuthenticatedRequest, + res: Response +): Promise { + const { oldValue } = req.body; + const companyCode = req.user?.companyCode; + + try { + if (!oldValue) { + res.status(400).json({ + success: false, + message: "ํ•„์ˆ˜ ํ•„๋“œ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (oldValue)", + }); + return; + } + + if (!companyCode) { + res.status(401).json({ + success: false, + message: "์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", + }); + return; + } + + logger.info("๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", { oldValue, companyCode }); + + // PostgreSQL ํ•จ์ˆ˜ ํ˜ธ์ถœ + const result = await pool.query( + "SELECT * FROM preview_merge_code_by_value($1, $2)", + [oldValue, companyCode] + ); + + const preview = Array.isArray(result) ? result : ((result as any).rows || []); + const totalRows = preview.reduce( + (sum: number, row: any) => sum + parseInt(row.out_affected_rows || 0), + 0 + ); + + logger.info("๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์™„๋ฃŒ", { + tablesCount: preview.length, + totalRows, + }); + + res.json({ + success: true, + message: "์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์™„๋ฃŒ", + data: { + oldValue, + preview: preview.map((row: any) => ({ + tableName: row.out_table_name, + columnName: row.out_column_name, + affectedRows: parseInt(row.out_affected_rows), + })), + totalAffectedRows: totalRows, + }, + }); + } catch (error: any) { + logger.error("๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ:", error); + + res.status(500).json({ + success: false, + message: "์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: { + code: "PREVIEW_BY_VALUE_ERROR", + details: error.message, + }, + }); + } +} + diff --git a/backend-node/src/routes/codeMergeRoutes.ts b/backend-node/src/routes/codeMergeRoutes.ts index 78cbd3e1..2cb41923 100644 --- a/backend-node/src/routes/codeMergeRoutes.ts +++ b/backend-node/src/routes/codeMergeRoutes.ts @@ -3,6 +3,8 @@ import { mergeCodeAllTables, getTablesWithColumn, previewCodeMerge, + mergeCodeByValue, + previewMergeCodeByValue, } from "../controllers/codeMergeController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -13,7 +15,7 @@ router.use(authenticateToken); /** * POST /api/code-merge/merge-all-tables - * ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์‹คํ–‰ (๋ชจ๋“  ๊ด€๋ จ ํ…Œ์ด๋ธ”์— ์ ์šฉ) + * ์ฝ”๋“œ ๋ณ‘ํ•ฉ ์‹คํ–‰ (๋ชจ๋“  ๊ด€๋ จ ํ…Œ์ด๋ธ”์— ์ ์šฉ - ๊ฐ™์€ ์ปฌ๋Ÿผ๋ช…๋งŒ) * Body: { columnName, oldValue, newValue } */ router.post("/merge-all-tables", mergeCodeAllTables); @@ -26,10 +28,24 @@ router.get("/tables-with-column/:columnName", getTablesWithColumn); /** * POST /api/code-merge/preview - * ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์‹ค์ œ ์‹คํ–‰ ์—†์ด ์˜ํ–ฅ๋ฐ›์„ ๋ฐ์ดํ„ฐ ํ™•์ธ) + * ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (๊ฐ™์€ ์ปฌ๋Ÿผ๋ช… ๊ธฐ์ค€) * Body: { columnName, oldValue } */ router.post("/preview", previewCodeMerge); +/** + * POST /api/code-merge/merge-by-value + * ๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ (๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์—์„œ ํ•ด๋‹น ๊ฐ’์„ ์ฐพ์•„ ๋ณ€๊ฒฝ) + * Body: { oldValue, newValue } + */ +router.post("/merge-by-value", mergeCodeByValue); + +/** + * POST /api/code-merge/preview-by-value + * ๊ฐ’ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ปฌ๋Ÿผ๋ช… ์ƒ๊ด€์—†์ด ๊ฐ’์œผ๋กœ ๊ฒ€์ƒ‰) + * Body: { oldValue } + */ +router.post("/preview-by-value", previewMergeCodeByValue); + export default router; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4c9d638b..fdde7398 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4920,26 +4920,35 @@ export class ButtonActionExecutor { const { oldValue, newValue } = confirmed; - // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ (์˜ต์…˜) + // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ‘œ์‹œ (๊ฐ’ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰ - ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ์—์„œ ๊ฒ€์ƒ‰) if (config.mergeShowPreview !== false) { const { apiClient } = await import("@/lib/api/client"); - const previewResponse = await apiClient.post("/code-merge/preview", { - columnName, + toast.loading("์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰ ์ค‘...", { duration: Infinity }); + + const previewResponse = await apiClient.post("/code-merge/preview-by-value", { oldValue, }); + toast.dismiss(); + if (previewResponse.data.success) { const preview = previewResponse.data.data; const totalRows = preview.totalAffectedRows; + // ์ƒ์„ธ ์ •๋ณด ์ƒ์„ฑ + const detailList = preview.preview + .map((p: any) => ` - ${p.tableName}.${p.columnName}: ${p.affectedRows}๊ฑด`) + .join("\n"); + const confirmMerge = confirm( - "โš ๏ธ ์ฝ”๋“œ ๋ณ‘ํ•ฉ ํ™•์ธ\n\n" + + "์ฝ”๋“œ ๋ณ‘ํ•ฉ ํ™•์ธ\n\n" + `${oldValue} โ†’ ${newValue}\n\n` + "์˜ํ–ฅ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ:\n" + - `- ํ…Œ์ด๋ธ” ์ˆ˜: ${preview.preview.length}๊ฐœ\n` + + `- ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ˆ˜: ${preview.preview.length}๊ฐœ\n` + `- ์ด ํ–‰ ์ˆ˜: ${totalRows}๊ฐœ\n\n` + - `๋ฐ์ดํ„ฐ๋Š” ์‚ญ์ œ๋˜์ง€ ์•Š๊ณ , "${columnName}" ์ปฌ๋Ÿผ ๊ฐ’๋งŒ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.\n\n` + + (preview.preview.length <= 10 ? `์ƒ์„ธ:\n${detailList}\n\n` : "") + + "๋ชจ๋“  ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ๊ฐ’์ด ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.\n\n" + "๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", ); @@ -4949,13 +4958,12 @@ export class ButtonActionExecutor { } } - // ๋ณ‘ํ•ฉ ์‹คํ–‰ + // ๋ณ‘ํ•ฉ ์‹คํ–‰ (๊ฐ’ ๊ธฐ๋ฐ˜ - ๋ชจ๋“  ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ปฌ๋Ÿผ) toast.loading("์ฝ”๋“œ ๋ณ‘ํ•ฉ ์ค‘...", { duration: Infinity }); const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.post("/code-merge/merge-all-tables", { - columnName, + const response = await apiClient.post("/code-merge/merge-by-value", { oldValue, newValue, }); @@ -4964,9 +4972,17 @@ export class ButtonActionExecutor { if (response.data.success) { const data = response.data.data; + + // ๋ณ€๊ฒฝ๋œ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ๋ชฉ๋ก ์ƒ์„ฑ + const changedList = data.affectedData + .map((d: any) => `${d.tableName}.${d.columnName}: ${d.rowsUpdated}๊ฑด`) + .join(", "); + toast.success( - "์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ!\n" + `${data.affectedTables.length}๊ฐœ ํ…Œ์ด๋ธ”, ${data.totalRowsUpdated}๊ฐœ ํ–‰ ์—…๋ฐ์ดํŠธ`, + `์ฝ”๋“œ ๋ณ‘ํ•ฉ ์™„๋ฃŒ! ${data.affectedData.length}๊ฐœ ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ, ${data.totalRowsUpdated}๊ฐœ ํ–‰ ์—…๋ฐ์ดํŠธ`, ); + + console.log("์ฝ”๋“œ ๋ณ‘ํ•ฉ ๊ฒฐ๊ณผ:", data.affectedData); // ํ™”๋ฉด ์ƒˆ๋กœ๊ณ ์นจ context.onRefresh?.(); From 11e25694b9f536f867f325aca37c64472d44c847 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 8 Jan 2026 14:49:24 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=9E=85=EB=A0=A5=20=EC=85=80=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=20=EB=8B=A4=EC=A4=91=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/InteractiveScreenViewer.tsx | 69 ++-- .../webtype-configs/EntityTypeConfigPanel.tsx | 57 ++++ .../EntitySearchInputComponent.tsx | 298 +++++++++++++++++- .../EntitySearchInputConfigPanel.tsx | 17 + .../EntitySearchInputWrapper.tsx | 83 +++++ .../entity-search-input/EntitySearchModal.tsx | 109 +++++-- .../components/entity-search-input/config.ts | 3 + .../components/entity-search-input/index.ts | 1 + .../components/entity-search-input/types.ts | 3 + frontend/lib/registry/init.ts | 4 +- frontend/types/screen-management.ts | 2 + 11 files changed, 547 insertions(+), 99 deletions(-) create mode 100644 frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 376f9953..f786d1d1 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -1369,58 +1369,25 @@ export const InteractiveScreenViewer: React.FC = ( } case "entity": { + // DynamicWebTypeRenderer๋กœ ์œ„์ž„ํ•˜์—ฌ EntitySearchInputWrapper ์‚ฌ์šฉ const widget = comp as WidgetComponent; - const config = widget.webTypeConfig as EntityTypeConfig | undefined; - - console.log("๐Ÿข InteractiveScreenViewer - Entity ์œ„์ ฏ:", { - componentId: widget.id, - widgetType: widget.widgetType, - config, - appliedSettings: { - entityName: config?.entityName, - displayField: config?.displayField, - valueField: config?.valueField, - multiple: config?.multiple, - defaultValue: config?.defaultValue, - }, - }); - - const finalPlaceholder = config?.placeholder || "์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•˜์„ธ์š”..."; - const defaultOptions = [ - { label: "์‚ฌ์šฉ์ž", value: "user" }, - { label: "์ œํ’ˆ", value: "product" }, - { label: "์ฃผ๋ฌธ", value: "order" }, - { label: "์นดํ…Œ๊ณ ๋ฆฌ", value: "category" }, - ]; - - return ( - , + return applyStyles( + updateFormData(fieldName, value), + onFormDataChange: updateFormData, + formData: formData, + readonly: readonly, + required: required, + placeholder: widget.placeholder || "์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•˜์„ธ์š”", + isInteractive: true, + className: "w-full h-full", + }} + />, ); } diff --git a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx index 05171d01..1cae7dce 100644 --- a/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx +++ b/frontend/components/screen/panels/webtype-configs/EntityTypeConfigPanel.tsx @@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; +import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Search, Database, Link, X, Plus } from "lucide-react"; import { EntityTypeConfig } from "@/types/screen"; @@ -26,6 +27,8 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder: "", displayFormat: "simple", separator: " - ", + multiple: false, // ๋‹ค์ค‘ ์„ ํƒ + uiMode: "select", // UI ๋ชจ๋“œ: select, combo, modal, autocomplete ...config, }; @@ -38,6 +41,8 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, displayFormat: safeConfig.displayFormat, separator: safeConfig.separator, + multiple: safeConfig.multiple, + uiMode: safeConfig.uiMode, }); const [newFilter, setNewFilter] = useState({ field: "", operator: "=", value: "" }); @@ -74,6 +79,8 @@ export const EntityTypeConfigPanel: React.FC = ({ co placeholder: safeConfig.placeholder, displayFormat: safeConfig.displayFormat, separator: safeConfig.separator, + multiple: safeConfig.multiple, + uiMode: safeConfig.uiMode, }); }, [ safeConfig.referenceTable, @@ -83,8 +90,18 @@ export const EntityTypeConfigPanel: React.FC = ({ co safeConfig.placeholder, safeConfig.displayFormat, safeConfig.separator, + safeConfig.multiple, + safeConfig.uiMode, ]); + // UI ๋ชจ๋“œ ์˜ต์…˜ + const uiModes = [ + { value: "select", label: "๋“œ๋กญ๋‹ค์šด ์„ ํƒ" }, + { value: "combo", label: "์ž…๋ ฅ + ๋ชจ๋‹ฌ ๋ฒ„ํŠผ" }, + { value: "modal", label: "๋ชจ๋‹ฌ ํŒ์—…" }, + { value: "autocomplete", label: "์ž๋™์™„์„ฑ" }, + ]; + const updateConfig = (key: keyof EntityTypeConfig, value: any) => { // ๋กœ์ปฌ ์ƒํƒœ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ setLocalValues((prev) => ({ ...prev, [key]: value })); @@ -260,6 +277,46 @@ export const EntityTypeConfigPanel: React.FC = ({ co /> + {/* UI ๋ชจ๋“œ */} +
+ + +

+ {localValues.uiMode === "select" && "๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ๋“œ๋กญ๋‹ค์šด ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค."} + {localValues.uiMode === "combo" && "์ž…๋ ฅ ํ•„๋“œ์™€ ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ์ด ํ•จ๊ป˜ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค."} + {localValues.uiMode === "modal" && "๋ชจ๋‹ฌ ํŒ์—…์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ์„ ํƒํ•ฉ๋‹ˆ๋‹ค."} + {localValues.uiMode === "autocomplete" && "์ž…๋ ฅํ•˜๋ฉด์„œ ์ž๋™์™„์„ฑ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค."} +

+
+ + {/* ๋‹ค์ค‘ ์„ ํƒ */} +
+
+ +

์—ฌ๋Ÿฌ ํ•ญ๋ชฉ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

+
+ updateConfig("multiple", checked)} + /> +
+ {/* ํ•„ํ„ฐ ๊ด€๋ฆฌ */}
diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 70785171..5045a43b 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -35,7 +35,9 @@ export function EntitySearchInputComponent({ parentValue: parentValueProp, parentFieldId, formData, - // ๐Ÿ†• ์ถ”๊ฐ€ props + // ๋‹ค์ค‘์„ ํƒ props + multiple: multipleProp, + // ์ถ”๊ฐ€ props component, isInteractive, onFormDataChange, @@ -49,8 +51,11 @@ export function EntitySearchInputComponent({ // uiMode๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด modeProp ์‚ฌ์šฉ, ๊ธฐ๋ณธ๊ฐ’ "combo" const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; - // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ์ถ”์ถœ (webTypeConfig ๋˜๋Š” component.componentConfig์—์„œ) - const config = component?.componentConfig || {}; + // ๋‹ค์ค‘์„ ํƒ ๋ฐ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • (props > webTypeConfig > componentConfig ์ˆœ์„œ) + const config = component?.componentConfig || component?.webTypeConfig || {}; + const isMultiple = multipleProp ?? config.multiple ?? false; + + // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ์ถ”์ถœ const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; // cascadingParentField: ConfigPanel์—์„œ ์ €์žฅ๋˜๋Š” ํ•„๋“œ๋ช… const effectiveParentFieldId = parentFieldId || config.cascadingParentField || config.parentFieldId; @@ -68,11 +73,27 @@ export function EntitySearchInputComponent({ const [isLoadingOptions, setIsLoadingOptions] = useState(false); const [optionsLoaded, setOptionsLoaded] = useState(false); + // ๋‹ค์ค‘์„ ํƒ ์ƒํƒœ (์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’๋“ค) + const [selectedValues, setSelectedValues] = useState([]); + const [selectedDataList, setSelectedDataList] = useState([]); + // ์—ฐ์‡„๊ด€๊ณ„ ์ƒํƒœ const [cascadingOptions, setCascadingOptions] = useState([]); const [isCascadingLoading, setIsCascadingLoading] = useState(false); const previousParentValue = useRef(null); + // ๋‹ค์ค‘์„ ํƒ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ • + useEffect(() => { + if (isMultiple && value) { + const vals = + typeof value === "string" ? value.split(",").filter(Boolean) : Array.isArray(value) ? value : [value]; + setSelectedValues(vals.map(String)); + } else if (isMultiple && !value) { + setSelectedValues([]); + setSelectedDataList([]); + } + }, [isMultiple, value]); + // ๋ถ€๋ชจ ํ•„๋“œ ๊ฐ’ ๊ฒฐ์ • (์ง์ ‘ ์ „๋‹ฌ ๋˜๋Š” formData์—์„œ ์ถ”์ถœ) - ์ž์‹ ์—ญํ• ์ผ ๋•Œ๋งŒ ํ•„์š” const parentValue = isChildRole ? (parentValueProp ?? (effectiveParentFieldId && formData ? formData[effectiveParentFieldId] : undefined)) @@ -249,23 +270,75 @@ export function EntitySearchInputComponent({ }, [value, displayField, effectiveOptions, mode, valueField, tableName, selectedData]); const handleSelect = (newValue: any, fullData: EntitySearchResult) => { - setSelectedData(fullData); - setDisplayValue(fullData[displayField] || ""); - onChange?.(newValue, fullData); + if (isMultiple) { + // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ + const valueStr = String(newValue); + const isAlreadySelected = selectedValues.includes(valueStr); + + let newSelectedValues: string[]; + let newSelectedDataList: EntitySearchResult[]; + + if (isAlreadySelected) { + // ์ด๋ฏธ ์„ ํƒ๋œ ํ•ญ๋ชฉ์ด๋ฉด ์ œ๊ฑฐ + newSelectedValues = selectedValues.filter((v) => v !== valueStr); + newSelectedDataList = selectedDataList.filter((d) => String(d[valueField]) !== valueStr); + } else { + // ์„ ํƒ๋˜์ง€ ์•Š์€ ํ•ญ๋ชฉ์ด๋ฉด ์ถ”๊ฐ€ + newSelectedValues = [...selectedValues, valueStr]; + newSelectedDataList = [...selectedDataList, fullData]; + } + + setSelectedValues(newSelectedValues); + setSelectedDataList(newSelectedDataList); + + const joinedValue = newSelectedValues.join(","); + onChange?.(joinedValue, newSelectedDataList); + + if (isInteractive && onFormDataChange && component?.columnName) { + onFormDataChange(component.columnName, joinedValue); + console.log("๐Ÿ“ค EntitySearchInput (multiple) -> onFormDataChange:", component.columnName, joinedValue); + } + } else { + // ๋‹จ์ผ์„ ํƒ ๋ชจ๋“œ + setSelectedData(fullData); + setDisplayValue(fullData[displayField] || ""); + onChange?.(newValue, fullData); + + if (isInteractive && onFormDataChange && component?.columnName) { + onFormDataChange(component.columnName, newValue); + console.log("๐Ÿ“ค EntitySearchInput -> onFormDataChange:", component.columnName, newValue); + } + } + }; + + // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ์—์„œ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ œ๊ฑฐ + const handleRemoveValue = (valueToRemove: string) => { + const newSelectedValues = selectedValues.filter((v) => v !== valueToRemove); + const newSelectedDataList = selectedDataList.filter((d) => String(d[valueField]) !== valueToRemove); + + setSelectedValues(newSelectedValues); + setSelectedDataList(newSelectedDataList); + + const joinedValue = newSelectedValues.join(","); + onChange?.(joinedValue || null, newSelectedDataList); - // ๐Ÿ†• onFormDataChange ํ˜ธ์ถœ (formData์— ๊ฐ’ ์ €์žฅ) if (isInteractive && onFormDataChange && component?.columnName) { - onFormDataChange(component.columnName, newValue); - console.log("๐Ÿ“ค EntitySearchInput -> onFormDataChange:", component.columnName, newValue); + onFormDataChange(component.columnName, joinedValue || null); + console.log("๐Ÿ“ค EntitySearchInput (remove) -> onFormDataChange:", component.columnName, joinedValue); } }; const handleClear = () => { - setDisplayValue(""); - setSelectedData(null); - onChange?.(null, null); + if (isMultiple) { + setSelectedValues([]); + setSelectedDataList([]); + onChange?.(null, []); + } else { + setDisplayValue(""); + setSelectedData(null); + onChange?.(null, null); + } - // ๐Ÿ†• onFormDataChange ํ˜ธ์ถœ (formData์—์„œ ๊ฐ’ ์ œ๊ฑฐ) if (isInteractive && onFormDataChange && component?.columnName) { onFormDataChange(component.columnName, null); console.log("๐Ÿ“ค EntitySearchInput -> onFormDataChange (clear):", component.columnName, null); @@ -280,7 +353,10 @@ export function EntitySearchInputComponent({ const handleSelectOption = (option: EntitySearchResult) => { handleSelect(option[valueField], option); - setSelectOpen(false); + // ๋‹ค์ค‘์„ ํƒ์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ ๋“œ๋กญ๋‹ค์šด ๋‹ซ๊ธฐ + if (!isMultiple) { + setSelectOpen(false); + } }; // ๋†’์ด ๊ณ„์‚ฐ (style์—์„œ height๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’) @@ -289,6 +365,111 @@ export function EntitySearchInputComponent({ // select ๋ชจ๋“œ: ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ๋“œ๋กญ๋‹ค์šด if (mode === "select") { + // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ + if (isMultiple) { + return ( +
+ {/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} + {component?.label && component?.style?.labelDisplay !== false && ( + + )} + + {/* ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค ํ‘œ์‹œ (ํƒœ๊ทธ ํ˜•์‹) */} +
!disabled && !isLoading && setSelectOpen(true)} + style={{ cursor: disabled ? "not-allowed" : "pointer" }} + > + {selectedValues.length > 0 ? ( + selectedValues.map((val) => { + const opt = effectiveOptions.find((o) => String(o[valueField]) === val); + const label = opt?.[displayField] || val; + return ( + + {label} + {!disabled && ( + + )} + + ); + }) + ) : ( + + {isLoading + ? "๋กœ๋”ฉ ์ค‘..." + : shouldApplyCascading && !parentValue + ? "์ƒ์œ„ ํ•ญ๋ชฉ์„ ๋จผ์ € ์„ ํƒํ•˜์„ธ์š”" + : placeholder} + + )} + +
+ + {/* ์˜ต์…˜ ๋“œ๋กญ๋‹ค์šด */} + {selectOpen && !disabled && ( +
+ + + + ํ•ญ๋ชฉ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {effectiveOptions.map((option, index) => { + const isSelected = selectedValues.includes(String(option[valueField])); + return ( + handleSelectOption(option)} + className="text-xs sm:text-sm" + > + +
+ {option[displayField]} + {valueField !== displayField && ( + {option[valueField]} + )} +
+
+ ); + })} +
+
+
+ {/* ๋‹ซ๊ธฐ ๋ฒ„ํŠผ */} +
+ +
+
+ )} + + {/* ์™ธ๋ถ€ ํด๋ฆญ ์‹œ ๋‹ซ๊ธฐ */} + {selectOpen &&
setSelectOpen(false)} />} +
+ ); + } + + // ๋‹จ์ผ์„ ํƒ ๋ชจ๋“œ (๊ธฐ์กด ๋กœ์ง) return (
{/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} @@ -366,6 +547,95 @@ export function EntitySearchInputComponent({ } // modal, combo, autocomplete ๋ชจ๋“œ + // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ + if (isMultiple) { + return ( +
+ {/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} + {component?.label && component?.style?.labelDisplay !== false && ( + + )} + + {/* ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค ํ‘œ์‹œ (ํƒœ๊ทธ ํ˜•์‹) + ๊ฒ€์ƒ‰ ๋ฒ„ํŠผ */} +
+
+ {selectedValues.length > 0 ? ( + selectedValues.map((val) => { + // selectedDataList์—์„œ ๋จผ์ € ์ฐพ๊ณ , ์—†์œผ๋ฉด effectiveOptions์—์„œ ์ฐพ๊ธฐ + const dataFromList = selectedDataList.find((d) => String(d[valueField]) === val); + const opt = dataFromList || effectiveOptions.find((o) => String(o[valueField]) === val); + const label = opt?.[displayField] || val; + return ( + + {label} + {!disabled && ( + + )} + + ); + }) + ) : ( + {placeholder} + )} +
+ + {/* ๋ชจ๋‹ฌ ๋ฒ„ํŠผ: modal ๋˜๋Š” combo ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ํ‘œ์‹œ */} + {(mode === "modal" || mode === "combo") && ( + + )} +
+ + {/* ๊ฒ€์ƒ‰ ๋ชจ๋‹ฌ: modal ๋˜๋Š” combo ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ๋ Œ๋”๋ง */} + {(mode === "modal" || mode === "combo") && ( + + )} +
+ ); + } + + // ๋‹จ์ผ์„ ํƒ ๋ชจ๋“œ (๊ธฐ์กด ๋กœ์ง) return (
{/* ๋ผ๋ฒจ ๋ Œ๋”๋ง */} diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx index 3cd4d35a..fb75daa4 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx @@ -747,6 +747,23 @@ export function EntitySearchInputConfigPanel({

+
+
+ + + updateConfig({ multiple: checked }) + } + /> +
+

+ {localConfig.multiple + ? "์—ฌ๋Ÿฌ ํ•ญ๋ชฉ์„ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ’์€ ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค." + : "ํ•˜๋‚˜์˜ ํ•ญ๋ชฉ๋งŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."} +

+
+
= ({ + component, + value, + onChange, + readonly = false, + ...props +}) => { + // component์—์„œ ํ•„์š”ํ•œ ์„ค์ • ์ถ”์ถœ + const widget = component as any; + const webTypeConfig = widget?.webTypeConfig || {}; + const componentConfig = widget?.componentConfig || {}; + + // ์„ค์ • ์šฐ์„ ์ˆœ์œ„: webTypeConfig > componentConfig > component ์ง์ ‘ ์†์„ฑ + const config = { ...componentConfig, ...webTypeConfig }; + + // ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์„ค์ •๋œ ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์ •๋ณด ์‚ฌ์šฉ + const tableName = config.referenceTable || widget?.referenceTable || ""; + const displayField = config.labelField || config.displayColumn || config.displayField || "name"; + const valueField = config.valueField || config.referenceColumn || "id"; + + // UI ๋ชจ๋“œ: uiMode > mode ์ˆœ์„œ + const uiMode = config.uiMode || config.mode || "select"; + + // ๋‹ค์ค‘์„ ํƒ ์„ค์ • + const multiple = config.multiple ?? false; + + // placeholder + const placeholder = config.placeholder || widget?.placeholder || "ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”"; + + console.log("๐Ÿข EntitySearchInputWrapper ๋ Œ๋”๋ง:", { + tableName, + displayField, + valueField, + uiMode, + multiple, + value, + config, + }); + + // ํ…Œ์ด๋ธ” ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + if (!tableName) { + return ( +
+ ํ…Œ์ด๋ธ” ํƒ€์ž… ๊ด€๋ฆฌ์—์„œ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š” +
+ ); + } + + return ( + + ); +}; + +EntitySearchInputWrapper.displayName = "EntitySearchInputWrapper"; + diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx index 7f841ec3..555efe9b 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx @@ -11,7 +11,9 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Search, Loader2 } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Search, Loader2, Check } from "lucide-react"; +import { cn } from "@/lib/utils"; import { useEntitySearch } from "./useEntitySearch"; import { EntitySearchResult } from "./types"; @@ -26,6 +28,9 @@ interface EntitySearchModalProps { modalTitle?: string; modalColumns?: string[]; onSelect: (value: any, fullData: EntitySearchResult) => void; + // ๋‹ค์ค‘์„ ํƒ ๊ด€๋ จ + multiple?: boolean; + selectedValues?: string[]; // ์ด๋ฏธ ์„ ํƒ๋œ ๊ฐ’๋“ค } export function EntitySearchModal({ @@ -39,6 +44,8 @@ export function EntitySearchModal({ modalTitle = "๊ฒ€์ƒ‰", modalColumns = [], onSelect, + multiple = false, + selectedValues = [], }: EntitySearchModalProps) { const [localSearchText, setLocalSearchText] = useState(""); const { @@ -71,7 +78,15 @@ export function EntitySearchModal({ const handleSelect = (item: EntitySearchResult) => { onSelect(item[valueField], item); - onOpenChange(false); + // ๋‹ค์ค‘์„ ํƒ์ด ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + if (!multiple) { + onOpenChange(false); + } + }; + + // ํ•ญ๋ชฉ์ด ์„ ํƒ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + const isItemSelected = (item: EntitySearchResult): boolean => { + return selectedValues.includes(String(item[valueField])); }; // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๊ฒฐ์ • @@ -123,10 +138,16 @@ export function EntitySearchModal({ {/* ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ…Œ์ด๋ธ” */}
-
+
- + + {/* ๋‹ค์ค‘์„ ํƒ ์‹œ ์ฒดํฌ๋ฐ•์Šค ์ปฌ๋Ÿผ */} + {multiple && ( + + )} {displayColumns.map((col) => ( + {!multiple && ( + + )} {loading && results.length === 0 ? ( - ) : results.length === 0 ? ( - ) : ( results.map((item, index) => { const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`; + const isSelected = isItemSelected(item); return ( - handleSelect(item)} - > + className={cn( + "border-t cursor-pointer transition-colors", + isSelected ? "bg-blue-50 hover:bg-blue-100" : "hover:bg-accent" + )} + onClick={() => handleSelect(item)} + > + {/* ๋‹ค์ค‘์„ ํƒ ์‹œ ์ฒดํฌ๋ฐ•์Šค */} + {multiple && ( + + )} {displayColumns.map((col) => ( - ))} - - - ); + {item[col] || "-"} + + ))} + {!multiple && ( + + )} + + ); }) )} @@ -211,12 +250,18 @@ export function EntitySearchModal({ )} + {/* ๋‹ค์ค‘์„ ํƒ ์‹œ ์„ ํƒ๋œ ํ•ญ๋ชฉ ์ˆ˜ ํ‘œ์‹œ */} + {multiple && selectedValues.length > 0 && ( +
+ {selectedValues.length}๊ฐœ ํ•ญ๋ชฉ ์„ ํƒ๋จ +
+ )}
diff --git a/frontend/lib/registry/components/entity-search-input/config.ts b/frontend/lib/registry/components/entity-search-input/config.ts index e147736f..fab81c9f 100644 --- a/frontend/lib/registry/components/entity-search-input/config.ts +++ b/frontend/lib/registry/components/entity-search-input/config.ts @@ -11,6 +11,9 @@ export interface EntitySearchInputConfig { showAdditionalInfo?: boolean; additionalFields?: string[]; + // ๋‹ค์ค‘ ์„ ํƒ ์„ค์ • + multiple?: boolean; // ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ ์„ ํƒ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: false) + // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • (cascading_relation ํ…Œ์ด๋ธ”๊ณผ ์—ฐ๋™) cascadingRelationCode?: string; // ์—ฐ์‡„๊ด€๊ณ„ ์ฝ”๋“œ (WAREHOUSE_LOCATION ๋“ฑ) cascadingRole?: "parent" | "child"; // ์—ญํ•  (๋ถ€๋ชจ/์ž์‹) diff --git a/frontend/lib/registry/components/entity-search-input/index.ts b/frontend/lib/registry/components/entity-search-input/index.ts index 3f70d0fe..5d1bf7d4 100644 --- a/frontend/lib/registry/components/entity-search-input/index.ts +++ b/frontend/lib/registry/components/entity-search-input/index.ts @@ -42,6 +42,7 @@ export type { EntitySearchInputConfig } from "./config"; // ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ณด๋‚ด๊ธฐ export { EntitySearchInputComponent } from "./EntitySearchInputComponent"; +export { EntitySearchInputWrapper } from "./EntitySearchInputWrapper"; export { EntitySearchInputRenderer } from "./EntitySearchInputRenderer"; export { EntitySearchModal } from "./EntitySearchModal"; export { useEntitySearch } from "./useEntitySearch"; diff --git a/frontend/lib/registry/components/entity-search-input/types.ts b/frontend/lib/registry/components/entity-search-input/types.ts index d29268b6..73abd9dd 100644 --- a/frontend/lib/registry/components/entity-search-input/types.ts +++ b/frontend/lib/registry/components/entity-search-input/types.ts @@ -19,6 +19,9 @@ export interface EntitySearchInputProps { placeholder?: string; disabled?: boolean; + // ๋‹ค์ค‘์„ ํƒ + multiple?: boolean; // ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ ์„ ํƒ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: false) + // ํ•„ํ„ฐ๋ง filterCondition?: Record; // ์ถ”๊ฐ€ WHERE ์กฐ๊ฑด companyCode?: string; // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ diff --git a/frontend/lib/registry/init.ts b/frontend/lib/registry/init.ts index c0082c2b..46f562b1 100644 --- a/frontend/lib/registry/init.ts +++ b/frontend/lib/registry/init.ts @@ -12,7 +12,7 @@ import { CheckboxWidget } from "@/components/screen/widgets/types/CheckboxWidget import { RadioWidget } from "@/components/screen/widgets/types/RadioWidget"; import { FileWidget } from "@/components/screen/widgets/types/FileWidget"; import { CodeWidget } from "@/components/screen/widgets/types/CodeWidget"; -import { EntityWidget } from "@/components/screen/widgets/types/EntityWidget"; +import { EntitySearchInputWrapper } from "@/lib/registry/components/entity-search-input/EntitySearchInputWrapper"; import { ButtonWidget } from "@/components/screen/widgets/types/ButtonWidget"; // ๊ฐœ๋ณ„์ ์œผ๋กœ ์„ค์ • ํŒจ๋„๋“ค์„ import @@ -352,7 +352,7 @@ export function initializeWebTypeRegistry() { name: "์—”ํ‹ฐํ‹ฐ ์„ ํƒ", category: "input", description: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—”ํ‹ฐํ‹ฐ ์„ ํƒ ํ•„๋“œ", - component: EntityWidget, + component: EntitySearchInputWrapper, configPanel: EntityConfigPanel, defaultConfig: { entityType: "", diff --git a/frontend/types/screen-management.ts b/frontend/types/screen-management.ts index 646632f5..89127c1a 100644 --- a/frontend/types/screen-management.ts +++ b/frontend/types/screen-management.ts @@ -365,6 +365,8 @@ export interface EntityTypeConfig { separator?: string; // ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ ํ‘œ์‹œ ์‹œ ๊ตฌ๋ถ„์ž (๊ธฐ๋ณธ: ' - ') // UI ๋ชจ๋“œ uiMode?: "select" | "modal" | "combo" | "autocomplete"; // ๊ธฐ๋ณธ: "combo" + // ๋‹ค์ค‘ ์„ ํƒ + multiple?: boolean; // ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ ์„ ํƒ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ: false) } /** From 4dfa82d3ddb15a92fd28e908e76737fb3f65630b Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 8 Jan 2026 15:56:06 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EB=B6=84=ED=95=A0=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/tableManagementController.ts | 64 ++++ .../src/routes/tableManagementRoutes.ts | 10 + .../src/services/tableManagementService.ts | 132 +++++++ frontend/lib/api/tableManagement.ts | 34 ++ .../SplitPanelLayoutComponent.tsx | 56 ++- .../SplitPanelLayoutConfigPanel.tsx | 325 ++++++------------ 6 files changed, 392 insertions(+), 229 deletions(-) diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 9b3d81a2..65cd5f4c 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -2185,3 +2185,67 @@ export async function multiTableSave( } } +/** + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ + * GET /api/table-management/tables/entity-relations?leftTable=xxx&rightTable=yyy + * + * column_labels์—์„œ ์ •์˜๋œ ์—”ํ‹ฐํ‹ฐ/์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + */ +export async function getTableEntityRelations( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { leftTable, rightTable } = req.query; + + logger.info(`=== ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ ์‹œ์ž‘: ${leftTable} <-> ${rightTable} ===`); + + if (!leftTable || !rightTable) { + const response: ApiResponse = { + success: false, + message: "leftTable๊ณผ rightTable ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + error: { + code: "MISSING_PARAMETERS", + details: "leftTable๊ณผ rightTable ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + }, + }; + res.status(400).json(response); + return; + } + + const tableManagementService = new TableManagementService(); + const relations = await tableManagementService.detectTableEntityRelations( + String(leftTable), + String(rightTable) + ); + + logger.info(`ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ ์™„๋ฃŒ: ${relations.length}๊ฐœ ๋ฐœ๊ฒฌ`); + + const response: ApiResponse = { + success: true, + message: `${relations.length}๊ฐœ์˜ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.`, + data: { + leftTable: String(leftTable), + rightTable: String(rightTable), + relations, + }, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:", error); + + const response: ApiResponse = { + success: false, + message: "ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: { + code: "ENTITY_RELATIONS_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} + diff --git a/backend-node/src/routes/tableManagementRoutes.ts b/backend-node/src/routes/tableManagementRoutes.ts index d0716d59..fa7832ee 100644 --- a/backend-node/src/routes/tableManagementRoutes.ts +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -25,6 +25,7 @@ import { toggleLogTable, getCategoryColumnsByMenu, // ๐Ÿ†• ๋ฉ”๋‰ด๋ณ„ ์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ ์กฐํšŒ multiTableSave, // ๐Ÿ†• ๋ฒ”์šฉ ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ + getTableEntityRelations, // ๐Ÿ†• ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ } from "../controllers/tableManagementController"; const router = express.Router(); @@ -38,6 +39,15 @@ router.use(authenticateToken); */ router.get("/tables", getTableList); +/** + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ + * GET /api/table-management/tables/entity-relations?leftTable=xxx&rightTable=yyy + * + * column_labels์—์„œ ์ •์˜๋œ ์—”ํ‹ฐํ‹ฐ/์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + */ +router.get("/tables/entity-relations", getTableEntityRelations); + /** * ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ * GET /api/table-management/tables/:tableName/columns diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 98db1eee..7df10fdb 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1306,6 +1306,41 @@ export class TableManagementService { paramCount: number; } | null> { try { + // ๐Ÿ†• ๋ฐฐ์—ด ๊ฐ’ ์ฒ˜๋ฆฌ (๋‹ค์ค‘ ๊ฐ’ ๊ฒ€์ƒ‰ - ๋ถ„ํ• ํŒจ๋„ ์—”ํ‹ฐํ‹ฐ ํƒ€์ž…์—์„œ "2,3" ํ˜•ํƒœ ์ง€์›) + // ์ขŒ์ธก์—์„œ "2"๋ฅผ ์„ ํƒํ•ด๋„, ์šฐ์ธก์—์„œ "2,3"์„ ๊ฐ€์ง„ ํ–‰์ด ํ‘œ์‹œ๋˜๋„๋ก ํ•จ + if (Array.isArray(value) && value.length > 0) { + // ๋ฐฐ์—ด์˜ ๊ฐ ๊ฐ’์— ๋Œ€ํ•ด OR ์กฐ๊ฑด์œผ๋กœ ๊ฒ€์ƒ‰ + // ์šฐ์ธก ์ปฌ๋Ÿผ์— "2,3" ๊ฐ™์€ ๋‹ค์ค‘ ๊ฐ’์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ + // ๊ฐ ๊ฐ’์„ LIKE ๋˜๋Š” = ์กฐ๊ฑด์œผ๋กœ ์ฒ˜๋ฆฌ + const conditions: string[] = []; + const values: any[] = []; + + value.forEach((v: any, idx: number) => { + const safeValue = String(v).trim(); + // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๊ฑฐ๋‚˜, ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’ ์ค‘ ํ•˜๋‚˜๋กœ ํฌํ•จ + // ์˜ˆ: "2,3" ์ปฌ๋Ÿผ์—์„œ "2"๋ฅผ ์ฐพ์œผ๋ ค๋ฉด: + // - ์ •ํ™•ํžˆ "2" + // - "2," ๋กœ ์‹œ์ž‘ + // - ",2" ๋กœ ๋๋‚จ + // - ",2," ์ค‘๊ฐ„์— ํฌํ•จ + const paramBase = paramIndex + (idx * 4); + conditions.push(`( + ${columnName}::text = $${paramBase} OR + ${columnName}::text LIKE $${paramBase + 1} OR + ${columnName}::text LIKE $${paramBase + 2} OR + ${columnName}::text LIKE $${paramBase + 3} + )`); + values.push(safeValue, `${safeValue},%`, `%,${safeValue}`, `%,${safeValue},%`); + }); + + logger.info(`๐Ÿ” ๋‹ค์ค‘ ๊ฐ’ ๋ฐฐ์—ด ๊ฒ€์ƒ‰: ${columnName} IN [${value.join(", ")}]`); + return { + whereClause: `(${conditions.join(" OR ")})`, + values, + paramCount: values.length, + }; + } + // ๐Ÿ”ง ํŒŒ์ดํ”„๋กœ ๊ตฌ๋ถ„๋œ ๋ฌธ์ž์—ด ์ฒ˜๋ฆฌ (๋‹ค์ค‘์„ ํƒ ๋˜๋Š” ๋‚ ์งœ ๋ฒ”์œ„) if (typeof value === "string" && value.includes("|")) { const columnInfo = await this.getColumnWebTypeInfo( @@ -4630,4 +4665,101 @@ export class TableManagementService { return false; } } + + /** + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ + * column_labels์—์„œ ์—”ํ‹ฐํ‹ฐ ํƒ€์ž… ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ…Œ์ด๋ธ” ๊ฐ„ ๊ด€๊ณ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param leftTable ์ขŒ์ธก ํ…Œ์ด๋ธ”๋ช… + * @param rightTable ์šฐ์ธก ํ…Œ์ด๋ธ”๋ช… + * @returns ๊ฐ์ง€๋œ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๋ฐฐ์—ด + */ + async detectTableEntityRelations( + leftTable: string, + rightTable: string + ): Promise> { + try { + logger.info(`๋‘ ํ…Œ์ด๋ธ” ๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹œ์ž‘: ${leftTable} <-> ${rightTable}`); + + const relations: Array<{ + leftColumn: string; + rightColumn: string; + direction: "left_to_right" | "right_to_left"; + inputType: string; + displayColumn?: string; + }> = []; + + // 1. ์šฐ์ธก ํ…Œ์ด๋ธ”์—์„œ ์ขŒ์ธก ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + // ์˜ˆ: right_table์˜ customer_id -> left_table(customer_mng)์˜ customer_code + const rightToLeftRels = await query<{ + column_name: string; + reference_column: string; + input_type: string; + display_column: string | null; + }>( + `SELECT column_name, reference_column, input_type, display_column + FROM column_labels + WHERE table_name = $1 + AND input_type IN ('entity', 'category') + AND reference_table = $2 + AND reference_column IS NOT NULL + AND reference_column != ''`, + [rightTable, leftTable] + ); + + for (const rel of rightToLeftRels) { + relations.push({ + leftColumn: rel.reference_column, + rightColumn: rel.column_name, + direction: "right_to_left", + inputType: rel.input_type, + displayColumn: rel.display_column || undefined, + }); + } + + // 2. ์ขŒ์ธก ํ…Œ์ด๋ธ”์—์„œ ์šฐ์ธก ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + // ์˜ˆ: left_table์˜ item_id -> right_table(item_info)์˜ item_number + const leftToRightRels = await query<{ + column_name: string; + reference_column: string; + input_type: string; + display_column: string | null; + }>( + `SELECT column_name, reference_column, input_type, display_column + FROM column_labels + WHERE table_name = $1 + AND input_type IN ('entity', 'category') + AND reference_table = $2 + AND reference_column IS NOT NULL + AND reference_column != ''`, + [leftTable, rightTable] + ); + + for (const rel of leftToRightRels) { + relations.push({ + leftColumn: rel.column_name, + rightColumn: rel.reference_column, + direction: "left_to_right", + inputType: rel.input_type, + displayColumn: rel.display_column || undefined, + }); + } + + logger.info(`์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์™„๋ฃŒ: ${relations.length}๊ฐœ ๋ฐœ๊ฒฌ`); + relations.forEach((rel, idx) => { + logger.info(` ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})`); + }); + + return relations; + } catch (error) { + logger.error(`์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹คํŒจ: ${leftTable} <-> ${rightTable}`, error); + return []; + } + } } diff --git a/frontend/lib/api/tableManagement.ts b/frontend/lib/api/tableManagement.ts index d03d83bf..5953fd82 100644 --- a/frontend/lib/api/tableManagement.ts +++ b/frontend/lib/api/tableManagement.ts @@ -328,6 +328,40 @@ class TableManagementApi { }; } } + + /** + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ + * column_labels์—์„œ ์ •์˜๋œ ์—”ํ‹ฐํ‹ฐ/์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์„ค์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ + * ๋‘ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ์™ธ๋ž˜ํ‚ค ๊ด€๊ณ„๋ฅผ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + */ + async getTableEntityRelations( + leftTable: string, + rightTable: string + ): Promise; + }>> { + try { + const response = await apiClient.get( + `${this.basePath}/tables/entity-relations?leftTable=${encodeURIComponent(leftTable)}&rightTable=${encodeURIComponent(rightTable)}` + ); + return response.data; + } catch (error: any) { + console.error(`โŒ ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์กฐํšŒ ์‹คํŒจ: ${leftTable} <-> ${rightTable}`, error); + return { + success: false, + message: error.response?.data?.message || error.message || "ํ…Œ์ด๋ธ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", + errorCode: error.response?.data?.errorCode, + }; + } + } } // ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index afc5c13e..e674e1a9 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -830,7 +830,8 @@ export const SplitPanelLayoutComponent: React.FC size: 1, }); - const detail = result.items && result.items.length > 0 ? result.items[0] : null; + // result.data๊ฐ€ EntityJoinResponse์˜ ์‹ค์ œ ๋ฐฐ์—ด ํ•„๋“œ + const detail = result.data && result.data.length > 0 ? result.data[0] : null; setRightData(detail); } else if (relationshipType === "join") { // ์กฐ์ธ ๋ชจ๋“œ: ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์˜ ๊ด€๋ จ ๋ฐ์ดํ„ฐ (์—ฌ๋Ÿฌ ๊ฐœ) @@ -899,16 +900,54 @@ export const SplitPanelLayoutComponent: React.FC return; } - // ๐Ÿ†• ๋ณตํ•ฉํ‚ค ์ง€์› - if (keys && keys.length > 0 && leftTable) { + // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ ๋กœ์ง ๊ฐœ์„  + // 1. ์„ค์ •๋œ keys๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ + // 2. ์—†์œผ๋ฉด ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ์—์„œ ์ •์˜๋œ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„๋ฅผ ์ž๋™์œผ๋กœ ์กฐํšŒ + let effectiveKeys = keys || []; + + if (effectiveKeys.length === 0 && leftTable && rightTableName) { + // ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ + console.log("๐Ÿ” [๋ถ„ํ• ํŒจ๋„] ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ ์‹œ์ž‘:", leftTable, "->", rightTableName); + const { tableManagementApi } = await import("@/lib/api/tableManagement"); + const relResponse = await tableManagementApi.getTableEntityRelations(leftTable, rightTableName); + + if (relResponse.success && relResponse.data?.relations && relResponse.data.relations.length > 0) { + effectiveKeys = relResponse.data.relations.map((rel) => ({ + leftColumn: rel.leftColumn, + rightColumn: rel.rightColumn, + })); + console.log("โœ… [๋ถ„ํ• ํŒจ๋„] ์ž๋™ ๊ฐ์ง€๋œ ๊ด€๊ณ„:", effectiveKeys); + } + } + + if (effectiveKeys.length > 0 && leftTable) { // ๋ณตํ•ฉํ‚ค: ์—ฌ๋Ÿฌ ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ๋ง const { entityJoinApi } = await import("@/lib/api/entityJoin"); - // ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด ์ƒ์„ฑ + // ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด ์ƒ์„ฑ (๋‹ค์ค‘ ๊ฐ’ ์ง€์›) + // ๐Ÿ†• ํ•ญ์ƒ ๋ฐฐ์—ด๋กœ ์ „๋‹ฌํ•˜์—ฌ ๋ฐฑ์—”๋“œ์—์„œ ๋‹ค์ค‘ ๊ฐ’ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰์„ ์ง€์›ํ•˜๋„๋ก ํ•จ + // ์˜ˆ: ์ขŒ์ธก์—์„œ "2"๋ฅผ ์„ ํƒํ•ด๋„, ์šฐ์ธก์—์„œ "2,3"์„ ๊ฐ€์ง„ ํ–‰์ด ํ‘œ์‹œ๋˜๋„๋ก const searchConditions: Record = {}; - keys.forEach((key) => { + effectiveKeys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { - searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + const leftValue = leftItem[key.leftColumn]; + // ๋‹ค์ค‘ ๊ฐ’ ์ง€์›: ๋ชจ๋“  ๊ฐ’์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋‹ค์ค‘ ๊ฐ’ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰ ํ™œ์„ฑํ™” + if (typeof leftValue === "string") { + if (leftValue.includes(",")) { + // "2,3" ํ˜•ํƒœ๋ฉด ๋ถ„๋ฆฌํ•ด์„œ ๋ฐฐ์—ด๋กœ + const values = leftValue.split(",").map((v: string) => v.trim()).filter((v: string) => v); + searchConditions[key.rightColumn] = values; + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋‹ค์ค‘ ๊ฐ’ ๊ฒ€์ƒ‰ (๋ถ„๋ฆฌ):", key.rightColumn, "=", values); + } else { + // ๋‹จ์ผ ๊ฐ’๋„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ (์šฐ์ธก์— "2,3" ๊ฐ™์€ ๋‹ค์ค‘ ๊ฐ’์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ) + searchConditions[key.rightColumn] = [leftValue.trim()]; + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋‹ค์ค‘ ๊ฐ’ ๊ฒ€์ƒ‰ (๋‹จ์ผ):", key.rightColumn, "=", [leftValue.trim()]); + } + } else { + // ์ˆซ์ž๋‚˜ ๋‹ค๋ฅธ ํƒ€์ž…์€ ๋ฐฐ์—ด๋กœ ๊ฐ์‹ธ๊ธฐ + searchConditions[key.rightColumn] = [leftValue]; + console.log("๐Ÿ”— [๋ถ„ํ• ํŒจ๋„] ๋‹ค์ค‘ ๊ฐ’ ๊ฒ€์ƒ‰ (๊ธฐํƒ€):", key.rightColumn, "=", [leftValue]); + } } }); @@ -947,7 +986,7 @@ export const SplitPanelLayoutComponent: React.FC setRightData(filteredData); } else { - // ๋‹จ์ผํ‚ค (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) + // ๋‹จ์ผํ‚ค (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) ๋˜๋Š” ๊ด€๊ณ„๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; const rightColumn = componentConfig.rightPanel?.relation?.foreignKey; @@ -965,6 +1004,9 @@ export const SplitPanelLayoutComponent: React.FC componentConfig.rightPanel?.deduplication, // ๐Ÿ†• ์ค‘๋ณต ์ œ๊ฑฐ ์„ค์ • ์ „๋‹ฌ ); setRightData(joinedData || []); // ๋ชจ๋“  ๊ด€๋ จ ๋ ˆ์ฝ”๋“œ (๋ฐฐ์—ด) + } else { + console.warn("โš ๏ธ [๋ถ„ํ• ํŒจ๋„] ํ…Œ์ด๋ธ” ๊ด€๊ณ„๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:", leftTable, "->", rightTableName); + setRightData([]); } } } diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index 5ca50ffb..832107c4 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -429,6 +429,71 @@ export const SplitPanelLayoutConfigPanel: React.FC + >([]); + const [isDetectingRelations, setIsDetectingRelations] = useState(false); + + useEffect(() => { + const detectRelations = async () => { + const leftTable = config.leftPanel?.tableName || screenTableName; + const rightTable = config.rightPanel?.tableName; + + // ์กฐ์ธ ๋ชจ๋“œ์ด๊ณ  ์–‘์ชฝ ํ…Œ์ด๋ธ”์ด ๋ชจ๋‘ ์žˆ์„ ๋•Œ๋งŒ ๊ฐ์ง€ + if (relationshipType !== "join" || !leftTable || !rightTable) { + setAutoDetectedRelations([]); + return; + } + + setIsDetectingRelations(true); + try { + const { tableManagementApi } = await import("@/lib/api/tableManagement"); + const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable); + + if (response.success && response.data?.relations) { + console.log("๐Ÿ” ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€:", response.data.relations); + setAutoDetectedRelations(response.data.relations); + + // ๊ฐ์ง€๋œ ๊ด€๊ณ„๊ฐ€ ์žˆ๊ณ , ํ˜„์žฌ ์„ค์ •๋œ ํ‚ค๊ฐ€ ์—†์œผ๋ฉด ์ž๋™์œผ๋กœ ์ฒซ ๋ฒˆ์งธ ๊ด€๊ณ„๋ฅผ ์„ค์ • + const currentKeys = config.rightPanel?.relation?.keys || []; + if (response.data.relations.length > 0 && currentKeys.length === 0) { + // ์ฒซ ๋ฒˆ์งธ ๊ด€๊ณ„๋งŒ ์ž๋™ ์„ค์ • (์‚ฌ์šฉ์ž๊ฐ€ ์ถ”๊ฐ€๋กœ ์„ค์ • ๊ฐ€๋Šฅ) + const firstRel = response.data.relations[0]; + console.log("โœ… ์ฒซ ๋ฒˆ์งธ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ์„ค์ •:", firstRel); + updateRightPanel({ + relation: { + ...config.rightPanel?.relation, + type: "join", + useMultipleKeys: true, + keys: [ + { + leftColumn: firstRel.leftColumn, + rightColumn: firstRel.rightColumn, + }, + ], + }, + }); + } + } + } catch (error) { + console.error("โŒ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹คํŒจ:", error); + setAutoDetectedRelations([]); + } finally { + setIsDetectingRelations(false); + } + }; + + detectRelations(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.leftPanel?.tableName, config.rightPanel?.tableName, screenTableName, relationshipType]); + console.log("๐Ÿ”ง SplitPanelLayoutConfigPanel ๋ Œ๋”๋ง"); console.log(" - config:", config); console.log(" - tables:", tables); @@ -1633,234 +1698,50 @@ export const SplitPanelLayoutConfigPanel: React.FC )} - {/* ์ปฌ๋Ÿผ ๋งคํ•‘ - ์กฐ์ธ ๋ชจ๋“œ์—์„œ๋งŒ ํ‘œ์‹œ */} + {/* ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ์ž๋™ ๊ฐ์ง€ (์ฝ๊ธฐ ์ „์šฉ) - ์กฐ์ธ ๋ชจ๋“œ์—์„œ๋งŒ ํ‘œ์‹œ */} {relationshipType !== "detail" && ( -
-
-
- -

์ขŒ์ธก ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์„ ์šฐ์ธก ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ๊ณผ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค

-
- +
+
+ +

ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ์—์„œ ์ •์˜๋œ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„์ž…๋‹ˆ๋‹ค

-

๋ณตํ•ฉํ‚ค: ์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ์œผ๋กœ ์กฐ์ธ (์˜ˆ: item_code + lot_number)

- - {/* ๋ณตํ•ฉํ‚ค๊ฐ€ ์„ค์ •๋œ ๊ฒฝ์šฐ */} - {(config.rightPanel?.relation?.keys || []).length > 0 ? ( - <> - {(config.rightPanel?.relation?.keys || []).map((key, index) => ( -
-
- ์กฐ์ธ ํ‚ค {index + 1} - -
-
-
- - -
-
- - -
-
+ {isDetectingRelations ? ( +
+
+ ๊ด€๊ณ„ ๊ฐ์ง€ ์ค‘... +
+ ) : autoDetectedRelations.length > 0 ? ( +
+ {autoDetectedRelations.map((rel, index) => ( +
+ + {leftTableName}.{rel.leftColumn} + + + + {rightTableName}.{rel.rightColumn} + + + {rel.inputType === "entity" ? "์—”ํ‹ฐํ‹ฐ" : "์นดํ…Œ๊ณ ๋ฆฌ"} +
))} - +

+ ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ์—์„œ ์—”ํ‹ฐํ‹ฐ/์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ •์„ ๋ณ€๊ฒฝํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค +

+
+ ) : config.rightPanel?.tableName ? ( +
+

๊ฐ์ง€๋œ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+

+ ํ…Œ์ด๋ธ” ํƒ€์ž…๊ด€๋ฆฌ์—์„œ ์—”ํ‹ฐํ‹ฐ ํƒ€์ž…๊ณผ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์„ ์„ค์ •ํ•˜์„ธ์š” +

+
) : ( - /* ๋‹จ์ผํ‚ค (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) */ - <> -
- - - - - - - - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - {leftTableColumns.map((column) => ( - { - updateRightPanel({ - relation: { ...config.rightPanel?.relation, leftColumn: value }, - }); - setLeftColumnOpen(false); - }} - > - - {column.columnName} - ({column.columnLabel || ""}) - - ))} - - - - -
- -
- -
- -
- - - - - - - - - ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - {rightTableColumns.map((column) => ( - { - updateRightPanel({ - relation: { ...config.rightPanel?.relation, foreignKey: value }, - }); - setRightColumnOpen(false); - }} - > - - {column.columnName} - ({column.columnLabel || ""}) - - ))} - - - - -
- +
+

์šฐ์ธก ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜๋ฉด ๊ด€๊ณ„๋ฅผ ์ž๋™ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค

+
)}
)}
+ ์„ ํƒ + ))} - - ์„ ํƒ - + ์„ ํƒ +
+

๊ฒ€์ƒ‰ ์ค‘...

+ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค
+ handleSelect(item)} + onClick={(e) => e.stopPropagation()} + /> + - {item[col] || "-"} - - -
+ +