From a466e523d9f773a1901396357e2e72aac3b50602 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 13 Feb 2026 11:51:59 +0900 Subject: [PATCH 1/7] refactor: Remove password masking functionality from data services - Deleted the `maskPasswordColumns` function from `dataService.ts` and its usage in data responses, simplifying the data handling process. - Removed password handling logic from `DynamicFormService`, ensuring that password management is streamlined and centralized. - Updated related components to reflect the removal of password masking, improving code clarity and maintainability. --- backend-node/src/services/dataService.ts | 45 +----- .../src/services/dynamicFormService.ts | 28 ---- .../screen/InteractiveScreenViewer.tsx | 21 ++- .../SelectedItemsDetailInputComponent.tsx | 65 +++++---- .../SplitPanelLayoutComponent.tsx | 70 +++++---- .../SplitPanelLayoutConfigPanel.tsx | 137 +++++++----------- 6 files changed, 147 insertions(+), 219 deletions(-) diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 241bc9e3..2150a4af 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -18,45 +18,6 @@ import { pool } from "../database/db"; // ๐Ÿ†• Entity ์กฐ์ธ์„ ์œ„ํ•œ pool impo import { buildDataFilterWhereClause } from "../utils/dataFilterUtil"; // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์œ ํ‹ธ import { v4 as uuidv4 } from "uuid"; // ๐Ÿ†• UUID ์ƒ์„ฑ -/** - * ๋น„๋ฐ€๋ฒˆํ˜ธ(password) ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋งˆ์Šคํ‚น - * - table_type_columns์—์„œ input_type = 'password'์ธ ์ปฌ๋Ÿผ์„ ์กฐํšŒ - * - ๋ฐ์ดํ„ฐ ์‘๋‹ต์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’์„ ๋น„์›Œ์„œ ํ•ด์‹œ๊ฐ’ ๋…ธ์ถœ ๋ฐฉ์ง€ - */ -async function maskPasswordColumns(tableName: string, data: any): Promise { - try { - const passwordCols = await query<{ column_name: string }>( - `SELECT DISTINCT column_name FROM table_type_columns - WHERE table_name = $1 AND input_type = 'password'`, - [tableName] - ); - if (passwordCols.length === 0) return data; - - const passwordColumnNames = new Set(passwordCols.map(c => c.column_name)); - - // ๋‹จ์ผ ๊ฐ์ฒด ์ฒ˜๋ฆฌ - const maskRow = (row: any) => { - if (!row || typeof row !== "object") return row; - const masked = { ...row }; - for (const col of passwordColumnNames) { - if (col in masked) { - masked[col] = ""; // ํ•ด์‹œ๊ฐ’ ๋Œ€์‹  ๋นˆ ๋ฌธ์ž์—ด - } - } - return masked; - }; - - if (Array.isArray(data)) { - return data.map(maskRow); - } - return maskRow(data); - } catch (error) { - // ๋งˆ์Šคํ‚น ์‹คํŒจํ•ด๋„ ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ (์„œ๋น„์Šค ์ค‘๋‹จ ๋ฐฉ์ง€) - console.warn("โš ๏ธ password ์ปฌ๋Ÿผ ๋งˆ์Šคํ‚น ์‹คํŒจ:", error); - return data; - } -} - interface GetTableDataParams { tableName: string; limit?: number; @@ -661,14 +622,14 @@ class DataService { return { success: true, - data: await maskPasswordColumns(tableName, normalizedGroupRows), // ๐Ÿ”ง ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜! + password ๋งˆ์Šคํ‚น + data: normalizedGroupRows, // ๐Ÿ”ง ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜! }; } } return { success: true, - data: await maskPasswordColumns(tableName, normalizedRows[0]), // ๊ทธ๋ฃนํ•‘ ์—†์œผ๋ฉด ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ + password ๋งˆ์Šคํ‚น + data: normalizedRows[0], // ๊ทธ๋ฃนํ•‘ ์—†์œผ๋ฉด ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ }; } } @@ -687,7 +648,7 @@ class DataService { return { success: true, - data: await maskPasswordColumns(tableName, result[0]), // password ๋งˆ์Šคํ‚น + data: result[0], }; } catch (error) { console.error(`๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ ์˜ค๋ฅ˜ (${tableName}/${id}):`, error); diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 28ea6cc9..9e0915ee 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -2,7 +2,6 @@ import { query, queryOne, transaction, getPool } from "../database/db"; import { EventTriggerService } from "./eventTriggerService"; import { DataflowControlService } from "./dataflowControlService"; import tableCategoryValueService from "./tableCategoryValueService"; -import { PasswordUtils } from "../utils/passwordUtils"; export interface FormDataResult { id: number; @@ -860,33 +859,6 @@ export class DynamicFormService { } } - // ๋น„๋ฐ€๋ฒˆํ˜ธ(password) ํƒ€์ž… ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ - // - ๋นˆ ๊ฐ’์ด๋ฉด ๋ณ€๊ฒฝ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ (๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ์ง€) - // - ๊ฐ’์ด ์žˆ์œผ๋ฉด ์•”ํ˜ธํ™” ํ›„ ์ €์žฅ - try { - const passwordCols = await query<{ column_name: string }>( - `SELECT DISTINCT column_name FROM table_type_columns - WHERE table_name = $1 AND input_type = 'password'`, - [tableName] - ); - for (const { column_name } of passwordCols) { - if (column_name in changedFields) { - const pwValue = changedFields[column_name]; - if (!pwValue || pwValue === "") { - // ๋นˆ ๊ฐ’ โ†’ ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ์ง€ (๋ณ€๊ฒฝ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ) - delete changedFields[column_name]; - console.log(`๐Ÿ” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ ${column_name}: ๋นˆ ๊ฐ’์ด๋ฏ€๋กœ ์—…๋ฐ์ดํŠธ ์Šคํ‚ต (๊ธฐ์กด ์œ ์ง€)`); - } else { - // ๊ฐ’ ์žˆ์Œ โ†’ ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ €์žฅ - changedFields[column_name] = PasswordUtils.encrypt(pwValue); - console.log(`๐Ÿ” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ ${column_name}: ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ์™„๋ฃŒ`); - } - } - } - } catch (pwError) { - console.warn("โš ๏ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", pwError); - } - // ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด ์—…๋ฐ์ดํŠธ ๊ฑด๋„ˆ๋›ฐ๊ธฐ if (Object.keys(changedFields).length === 0) { console.log("๐Ÿ“‹ ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์—…๋ฐ์ดํŠธ๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค."); diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 05d8bdc9..c4b2ad0a 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -2231,11 +2231,20 @@ export const InteractiveScreenViewer: React.FC = ( } : component; - // ๐Ÿ†• ๋ชจ๋“  ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ†ตํ•ฉ (์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ๋‚ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ธฐ๋ณธ ๋ ˆ์ด์–ด formData ์ฐธ์กฐ ๊ฐ€๋Šฅํ•˜๋„๋ก) + // ๋ชจ๋“  ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ํ†ตํ•ฉ (์กฐ๊ฑด ํ‰๊ฐ€์šฉ - ํŠธ๋ฆฌ๊ฑฐ ์ปดํฌ๋„ŒํŠธ ๊ฒ€์ƒ‰์— ํ•„์š”) const allLayerComponents = useMemo(() => { return layers.flatMap((layer) => layer.components); }, [layers]); + // ๐Ÿ”ง ํ™œ์„ฑ ๋ ˆ์ด์–ด ์ปดํฌ๋„ŒํŠธ๋งŒ ํ†ตํ•ฉ (์ €์žฅ/๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์šฉ) + // ๊ธฐ๋ณธ ๋ ˆ์ด์–ด(base) + ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด๋งŒ ํฌํ•จ + // ๋น„ํ™œ์„ฑ ๋ ˆ์ด์–ด์˜ ์ค‘๋ณต columnName ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ €์žฅ ๋ฐ์ดํ„ฐ๋ฅผ ์˜ค์—ผ์‹œํ‚ค๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ + const visibleLayerComponents = useMemo(() => { + return layers + .filter((layer) => layer.type === "base" || activeLayerIds.includes(layer.id)) + .flatMap((layer) => layer.components); + }, [layers, activeLayerIds]); + // ๐Ÿ†• ๋ ˆ์ด์–ด๋ณ„ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ํ•จ์ˆ˜ const renderLayerComponents = useCallback((layer: LayerDefinition) => { // ํ™œ์„ฑํ™”๋˜์ง€ ์•Š์€ ๋ ˆ์ด์–ด๋Š” ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ @@ -2272,7 +2281,7 @@ export const InteractiveScreenViewer: React.FC = ( > = ( > = ( > = ( > = ( })} ); - }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, allLayerComponents, layers]); + }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, visibleLayerComponents, layers]); return ( diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index 6af5a88f..c2bb436d 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -67,6 +67,10 @@ export const SelectedItemsDetailInputComponent: React.FC ์ปดํฌ๋„ŒํŠธ ์„ค์ • > component.id const dataSourceId = useMemo( () => urlDataSourceId || componentConfig.dataSourceId || component.id || "default", @@ -228,7 +232,21 @@ export const SelectedItemsDetailInputComponent: React.FC[]> = {}; - if (firstRecord.customer_id && firstRecord.item_id) { + // ๋™์  ํ•„ํ„ฐ ๊ตฌ์„ฑ: parentDataMapping์˜ targetField + sourceKeyField + const editFilters: Record = {}; + const parentMappings = componentConfig.parentDataMapping || []; + parentMappings.forEach((mapping: any) => { + if (mapping.targetField && firstRecord[mapping.targetField]) { + editFilters[mapping.targetField] = firstRecord[mapping.targetField]; + } + }); + if (firstRecord[sourceKeyField]) { + editFilters[sourceKeyField] = firstRecord[sourceKeyField]; + } + + const hasRequiredKeys = Object.keys(editFilters).length >= 2; + + if (hasRequiredKeys) { try { const { dataApi } = await import("@/lib/api/data"); // ๋ชจ๋“  sourceTable์˜ ๋ฐ์ดํ„ฐ๋ฅผ API๋กœ ์ „์ฒด ๋กœ๋“œ (์ค‘๋ณต ํ…Œ์ด๋ธ” ์ œ๊ฑฐ) @@ -238,10 +256,7 @@ export const SelectedItemsDetailInputComponent: React.FC { const groupFields = additionalFields.filter((f) => f.groupId === group.id); groupFields.forEach((field) => { - if (field.name === "item_id" && field.autoFillFrom && item.originalData) { - itemId = item.originalData[field.autoFillFrom] || null; + if (field.name === sourceKeyField && field.autoFillFrom && item.originalData) { + sourceKeyValue = item.originalData[field.autoFillFrom] || null; } }); }); } // 3์ˆœ์œ„: fallback (์ตœํ›„์˜ ์ˆ˜๋‹จ) - if (!itemId && item.originalData) { - itemId = item.originalData.id || null; + if (!sourceKeyValue && item.originalData) { + sourceKeyValue = item.originalData.id || null; } - if (!itemId) { - console.error("โŒ [2๋‹จ๊ณ„ ์ €์žฅ] item_id๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ:", item); + if (!sourceKeyValue) { + console.error(`โŒ [2๋‹จ๊ณ„ ์ €์žฅ] ${sourceKeyField}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ:`, item); continue; } - // upsert ๊ณตํ†ต parentKeys: customer_id + item_id (์ •ํ™•ํ•œ ๋งค์นญ) - const itemParentKeys = { ...parentKeys, item_id: itemId }; + // upsert ๊ณตํ†ต parentKeys: parentMapping ํ‚ค + sourceKeyField (์ •ํ™•ํ•œ ๋งค์นญ) + const itemParentKeys = { ...parentKeys, [sourceKeyField]: sourceKeyValue }; // === Step 1: ๋ฉ”์ธ ํ…Œ์ด๋ธ”(customer_item_mapping) ์ €์žฅ === // ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋งคํ•‘ ๋ ˆ์ฝ”๋“œ ์ง€์› (๊ฑฐ๋ž˜์ฒ˜ ํ’ˆ๋ฒˆ/ํ’ˆ๋ช…์ด ๋‹ค์ค‘์ผ ์ˆ˜ ์žˆ์Œ) @@ -688,11 +703,11 @@ export const SelectedItemsDetailInputComponent: React.FC { - if (field.name !== "item_id" && field.autoFillFrom && item.originalData) { + if (field.name !== sourceKeyField && field.autoFillFrom && item.originalData) { const value = item.originalData[field.autoFillFrom]; if (value !== undefined && value !== null && !record[field.name]) { record[field.name] = value; @@ -1700,7 +1715,7 @@ export const SelectedItemsDetailInputComponent: React.FC f.name !== "item_id" && f.width !== "0px"); + const sampleFields = (componentConfig.additionalFields || []).filter(f => f.name !== sourceKeyField && f.width !== "0px"); const sampleGroups = componentConfig.fieldGroups || [{ id: "default", title: "์ž…๋ ฅ ์ •๋ณด", order: 0 }]; const gridCols = sampleGroups.length === 1 ? "grid-cols-1" : "grid-cols-2"; diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index bb9306c8..ebecedb3 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1183,31 +1183,15 @@ export const SplitPanelLayoutComponent: React.FC } // leftItem์ด null์ด๋ฉด join ๋ชจ๋“œ ์ด์™ธ์—๋Š” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ๋ถˆ๊ฐ€ + // detail ๋ชจ๋“œ: ์„ ํƒ ์•ˆ ํ•˜๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋œธ, ์„ ํƒํ•˜๋ฉด ํ•„ํ„ฐ๋ง + // join ๋ชจ๋“œ: ์„ ํƒ ์•ˆ ํ•˜๋ฉด ์ „์ฒด, ์„ ํƒํ•˜๋ฉด ํ•„ํ„ฐ๋ง if (!leftItem) return; setIsLoadingRight(true); try { - if (relationshipType === "detail") { - // ์ƒ์„ธ ๋ชจ๋“œ: ๋™์ผ ํ…Œ์ด๋ธ”์˜ ์ƒ์„ธ ์ •๋ณด (์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ™œ์„ฑํ™”) - const primaryKey = leftItem.id || leftItem.ID || Object.values(leftItem)[0]; - - // ๐Ÿ†• ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ API ์‚ฌ์šฉ - const { entityJoinApi } = await import("@/lib/api/entityJoin"); - const rightDetailJoinColumns = extractAdditionalJoinColumns( - componentConfig.rightPanel?.columns, - rightTableName, - ); - const result = await entityJoinApi.getTableDataWithJoins(rightTableName, { - search: { id: primaryKey }, - enableEntityJoin: true, - size: 1, - companyCodeOverride: companyCode, - additionalJoinColumns: rightDetailJoinColumns, // ๐Ÿ†• Entity ์กฐ์ธ ์ปฌ๋Ÿผ ์ „๋‹ฌ - }); - - const detail = result.items && result.items.length > 0 ? result.items[0] : null; - setRightData(detail); - } else if (relationshipType === "join") { + // detail / join ๋ชจ๋‘ ๋™์ผํ•œ ํ•„ํ„ฐ๋ง ๋กœ์ง ์‚ฌ์šฉ + // (์ฐจ์ด์ : ์ดˆ๊ธฐ ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๋‹ค๋ฆ„ - detail์€ ์ดˆ๊ธฐ ๋กœ๋“œ ์•ˆ ํ•จ) + { // ์กฐ์ธ ๋ชจ๋“œ: ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์˜ ๊ด€๋ จ ๋ฐ์ดํ„ฐ (์—ฌ๋Ÿฌ ๊ฐœ) const keys = componentConfig.rightPanel?.relation?.keys; const leftTable = componentConfig.leftPanel?.tableName; @@ -1443,16 +1427,24 @@ export const SplitPanelLayoutComponent: React.FC // ํƒญ์˜ dataFilter (API ์ „๋‹ฌ์šฉ) const tabDataFilterForApi = (tabConfig as any).dataFilter; + // ํƒญ์˜ relation type ํ™•์ธ (detail์ด๋ฉด ์ดˆ๊ธฐ ์ „์ฒด ๋กœ๋“œ ์•ˆ ํ•จ) + const tabRelationType = tabConfig.relation?.type || "join"; + if (!leftItem) { - // ์ขŒ์ธก ๋ฏธ์„ ํƒ: ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ (dataFilter๋Š” API์— ์ „๋‹ฌ) - const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { - enableEntityJoin: true, - size: 1000, - companyCodeOverride: companyCode, - additionalJoinColumns: tabJoinColumns, - dataFilter: tabDataFilterForApi, - }); - resultData = result.data || []; + if (tabRelationType === "detail") { + // detail ๋ชจ๋“œ: ์„ ํƒ ์•ˆ ํ•˜๋ฉด ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ๋œธ + resultData = []; + } else { + // join ๋ชจ๋“œ: ์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ (dataFilter๋Š” API์— ์ „๋‹ฌ) + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + enableEntityJoin: true, + size: 1000, + companyCodeOverride: companyCode, + additionalJoinColumns: tabJoinColumns, + dataFilter: tabDataFilterForApi, + }); + resultData = result.data || []; + } } else if (leftColumn && rightColumn) { const searchConditions: Record = {}; @@ -2781,16 +2773,20 @@ export const SplitPanelLayoutComponent: React.FC if (!isDesignMode && componentConfig.autoLoad !== false) { loadLeftData(); // ์ขŒ์ธก ๋ฏธ์„ ํƒ ์ƒํƒœ์—์„œ ์šฐ์ธก ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ธฐ๋ณธ ๋กœ๋“œ + // join ๋ชจ๋“œ: ์ดˆ๊ธฐ ์ „์ฒด ๋กœ๋“œ / detail ๋ชจ๋“œ: ์ดˆ๊ธฐ ๋กœ๋“œ ์•ˆ ํ•จ const relationshipType = componentConfig.rightPanel?.relation?.type || "detail"; if (relationshipType === "join") { loadRightData(null); - // ์ถ”๊ฐ€ ํƒญ๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ - const tabs = componentConfig.rightPanel?.additionalTabs; - if (tabs && tabs.length > 0) { - tabs.forEach((_: any, idx: number) => { + } + // ์ถ”๊ฐ€ ํƒญ: ๊ฐ ํƒญ์˜ relation.type์— ๋”ฐ๋ผ ์ดˆ๊ธฐ ๋กœ๋“œ ๊ฒฐ์ • + const tabs = componentConfig.rightPanel?.additionalTabs; + if (tabs && tabs.length > 0) { + tabs.forEach((tab: any, idx: number) => { + const tabRelType = tab.relation?.type || "join"; + if (tabRelType === "join") { loadTabData(idx + 1, null); - }); - } + } + }); } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -4645,7 +4641,7 @@ export const SplitPanelLayoutComponent: React.FC ) : ( <>

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

-

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

+

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

)} diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx index 52fd30a1..d77cb88d 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -1542,13 +1542,10 @@ export const SplitPanelLayoutConfigPanel: React.FC { - if (relationshipType === "detail") { - return leftTableName; // ์ƒ์„ธ ๋ชจ๋“œ์—์„œ๋Š” ์ขŒ์ธก๊ณผ ๋™์ผ - } return config.rightPanel?.tableName || ""; - }, [relationshipType, leftTableName, config.rightPanel?.tableName]); + }, [config.rightPanel?.tableName]); // ์šฐ์ธก ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (๋กœ๋“œ๋œ ์ปฌ๋Ÿผ ์‚ฌ์šฉ) const rightTableColumns = useMemo(() => { @@ -1567,8 +1564,8 @@ export const SplitPanelLayoutConfigPanel: React.FC { - // ์ƒ์„ธ ๋ชจ๋“œ๋กœ ๋ณ€๊ฒฝ ์‹œ ์šฐ์ธก ํ…Œ์ด๋ธ”์„ ํ˜„์žฌ ํ™”๋ฉด ํ…Œ์ด๋ธ”๋กœ ์„ค์ • - if (value === "detail" && screenTableName) { - updateRightPanel({ - relation: { ...config.rightPanel?.relation, type: value }, - tableName: screenTableName, - }); - } else { - updateRightPanel({ - relation: { ...config.rightPanel?.relation, type: value }, - }); - } + updateRightPanel({ + relation: { ...config.rightPanel?.relation, type: value }, + }); }} > - {relationshipType === "detail" ? "1๊ฑด ์ƒ์„ธ๋ณด๊ธฐ" : "์—ฐ๊ด€ ๋ชฉ๋ก"} + {relationshipType === "detail" ? "์„ ํƒ ์‹œ ํ‘œ์‹œ" : "์—ฐ๊ด€ ๋ชฉ๋ก"}
- 1๊ฑด ์ƒ์„ธ๋ณด๊ธฐ - ์ขŒ์ธก ํด๋ฆญ ์‹œ ํ•ด๋‹น ํ•ญ๋ชฉ์˜ ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ (๊ฐ™์€ ํ…Œ์ด๋ธ”) + ์„ ํƒ ์‹œ ํ‘œ์‹œ + ์ขŒ์ธก ์„ ํƒ ์‹œ์—๋งŒ ์šฐ์ธก ๋ฐ์ดํ„ฐ ํ‘œ์‹œ / ๋ฏธ์„ ํƒ ์‹œ ๋นˆ ํ™”๋ฉด
์—ฐ๊ด€ ๋ชฉ๋ก - ์ขŒ์ธก ํด๋ฆญ ์‹œ ์—ฐ๊ด€๋œ ๋ฐ์ดํ„ฐ ๋ชฉ๋ก ํ‘œ์‹œ / ๋ฏธ์„ ํƒ ์‹œ ์ „์ฒด ํ‘œ์‹œ + ๋ฏธ์„ ํƒ ์‹œ ์ „์ฒด ํ‘œ์‹œ / ์ขŒ์ธก ์„ ํƒ ์‹œ ํ•„ํ„ฐ๋ง
@@ -2305,7 +2294,7 @@ export const SplitPanelLayoutConfigPanel: React.FC {/* ์šฐ์ธก ํŒจ๋„ ์„ค์ • */}
-

์šฐ์ธก ํŒจ๋„ ์„ค์ • ({relationshipType === "detail" ? "1๊ฑด ์ƒ์„ธ๋ณด๊ธฐ" : "์—ฐ๊ด€ ๋ชฉ๋ก"})

+

์šฐ์ธก ํŒจ๋„ ์„ค์ • ({relationshipType === "detail" ? "์„ ํƒ ์‹œ ํ‘œ์‹œ" : "์—ฐ๊ด€ ๋ชฉ๋ก"})

@@ -2338,63 +2327,49 @@ export const SplitPanelLayoutConfigPanel: React.FC */} {/* ๊ด€๊ณ„ ํƒ€์ž…์— ๋”ฐ๋ผ ํ…Œ์ด๋ธ” ์„ ํƒ UI ๋ณ€๊ฒฝ */} - {relationshipType === "detail" ? ( - // ์ƒ์„ธ ๋ชจ๋“œ: ์ขŒ์ธก๊ณผ ๋™์ผํ•œ ํ…Œ์ด๋ธ” (์ž๋™ ์„ค์ •) -
- -
-

- {config.leftPanel?.tableName || screenTableName || "ํ…Œ์ด๋ธ”์ด ์ง€์ •๋˜์ง€ ์•Š์Œ"} -

-

์ƒ์„ธ ๋ชจ๋“œ์—์„œ๋Š” ์ขŒ์ธก๊ณผ ๋™์ผํ•œ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค

-
-
- ) : ( - // ์กฐ๊ฑด ํ•„ํ„ฐ ๋ชจ๋“œ: ์ „์ฒด ํ…Œ์ด๋ธ”์—์„œ ์„ ํƒ ๊ฐ€๋Šฅ -
- - - - - - - - - ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - - {availableRightTables.map((table) => ( - { - updateRightPanel({ tableName: table.tableName }); - setRightTableOpen(false); - }} - > - - {table.displayName || table.tableName} - {table.displayName && ({table.tableName})} - - ))} - - - - -
- )} +
+ + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {availableRightTables.map((table) => ( + { + updateRightPanel({ tableName: table.tableName }); + setRightTableOpen(false); + }} + > + + {table.displayName || table.tableName} + {table.displayName && ({table.tableName})} + + ))} + + + + +
From 5eab4669f0a0ddeae018f500db0b93dda106e8a9 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 13 Feb 2026 14:25:12 +0900 Subject: [PATCH 2/7] feat: Update screen management service and UI components for main table handling - Enhanced the `ScreenManagementService` to update the main table name in the database when saving layout data, improving data integrity and tracking. - Modified the `ScreenDesigner` component to include the main table name in the save request, ensuring the correct table is referenced. - Updated the `TablesPanel` to generate unique keys for join tables based on source columns, preventing key collisions and improving rendering performance. - Refactored the `TabsWidget` to streamline screen information loading and removed redundant screen info loading logic, enhancing efficiency and user experience. --- .../src/services/screenManagementService.ts | 12 ++- frontend/components/screen/ScreenDesigner.tsx | 1 + .../components/screen/panels/TablesPanel.tsx | 14 +++- .../components/screen/widgets/TabsWidget.tsx | 61 +++++--------- frontend/components/v2/V2Select.tsx | 80 ++++++++++++++----- .../components/v2-select/V2SelectRenderer.tsx | 10 +-- 6 files changed, 108 insertions(+), 70 deletions(-) diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 87e2ece6..2c25f7e0 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -5177,8 +5177,18 @@ export class ScreenManagementService { throw new Error("์ด ํ™”๋ฉด์˜ ๋ ˆ์ด์•„์›ƒ์„ ์ €์žฅํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); } + // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ (ํ…Œ์ด๋ธ”์ด ์„ ํƒ๋œ ๊ฒฝ์šฐ) + const mainTableName = layoutData.mainTableName; + if (mainTableName) { + await query( + `UPDATE screen_definitions SET table_name = $1, updated_date = NOW() WHERE screen_id = $2`, + [mainTableName, screenId], + ); + console.log(`โœ… [saveLayoutV2] ํ™”๋ฉด ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ: ${mainTableName}`); + } + // ์ €์žฅํ•  layout_data์—์„œ ๋ ˆ์ด์–ด ๋ฉ”ํƒ€ ์ •๋ณด ์ œ๊ฑฐ (์ˆœ์ˆ˜ ๋ ˆ์ด์•„์›ƒ๋งŒ ์ €์žฅ) - const { layerId: _lid, layerName: _ln, conditionConfig: _cc, ...pureLayoutData } = layoutData; + const { layerId: _lid, layerName: _ln, conditionConfig: _cc, mainTableName: _mtn, ...pureLayoutData } = layoutData; const dataToSave = { version: "2.0", ...pureLayoutData, diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 76bd8973..75937daa 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -2062,6 +2062,7 @@ export default function ScreenDesigner({ await screenApi.saveLayoutV2(selectedScreen.screenId, { ...v2Layout, layerId: currentLayerId, + mainTableName: currentMainTableName, // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” (DB ์—…๋ฐ์ดํŠธ์šฉ) }); } else { await screenApi.saveLayout(selectedScreen.screenId, layoutWithResolution); diff --git a/frontend/components/screen/panels/TablesPanel.tsx b/frontend/components/screen/panels/TablesPanel.tsx index 12dcc19a..3cbae41e 100644 --- a/frontend/components/screen/panels/TablesPanel.tsx +++ b/frontend/components/screen/panels/TablesPanel.tsx @@ -44,6 +44,11 @@ interface EntityJoinTable { tableName: string; currentDisplayColumn: string; availableColumns: EntityJoinColumn[]; + // ๊ฐ™์€ ํ…Œ์ด๋ธ”์ด ์—ฌ๋Ÿฌ FK๋กœ ์กฐ์ธ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์†Œ์Šค ์ปฌ๋Ÿผ์œผ๋กœ ๊ตฌ๋ถ„ + joinConfig?: { + sourceColumn: string; + [key: string]: unknown; + }; } interface TablesPanelProps { @@ -414,7 +419,11 @@ export const TablesPanel: React.FC = ({
- {entityJoinTables.map((joinTable) => { + {entityJoinTables.map((joinTable, idx) => { + // ๊ฐ™์€ ํ…Œ์ด๋ธ”์ด ์—ฌ๋Ÿฌ FK๋กœ ์กฐ์ธ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ sourceColumn์œผ๋กœ ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ + const uniqueKey = joinTable.joinConfig?.sourceColumn + ? `entity-join-${joinTable.tableName}-${joinTable.joinConfig.sourceColumn}` + : `entity-join-${joinTable.tableName}-${idx}`; const isExpanded = expandedJoinTables.has(joinTable.tableName); // ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง const filteredColumns = searchTerm @@ -431,8 +440,7 @@ export const TablesPanel: React.FC = ({ } return ( - // ์—”ํ‹ฐํ‹ฐ ์กฐ์ธ ํ…Œ์ด๋ธ”์— ๊ณ ์œ  ์ ‘๋‘์‚ฌ ์ถ”๊ฐ€ (๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ํ‚ค ์ค‘๋ณต ๋ฐฉ์ง€) -
+
{/* ์กฐ์ธ ํ…Œ์ด๋ธ” ํ—ค๋” */}
>({}); const [screenLoadingStates, setScreenLoadingStates] = useState>({}); const [screenErrors, setScreenErrors] = useState>({}); - // ํƒญ๋ณ„ ํ™”๋ฉด ์ •๋ณด (screenId, tableName) ์ €์žฅ - const [screenInfoMap, setScreenInfoMap] = useState>({}); + // ํƒญ๋ณ„ ํ™”๋ฉด ์ •๋ณด (screenId, tableName) - ์ธ๋ผ์ธ ์ปดํฌ๋„ŒํŠธ์˜ ํ…Œ์ด๋ธ” ์„ค์ •์—์„œ ์ถ”์ถœ + const screenInfoMap = React.useMemo(() => { + const map: Record = {}; + for (const tab of tabs as ExtendedTabItem[]) { + const inlineComponents = tab.components || []; + if (inlineComponents.length > 0) { + // ์ธ๋ผ์ธ ์ปดํฌ๋„ŒํŠธ์—์„œ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ์˜ selectedTable ์ถ”์ถœ + const tableComp = inlineComponents.find( + (c) => c.componentType === "v2-table-list" || c.componentType === "table-list", + ); + const selectedTable = tableComp?.componentConfig?.selectedTable; + if (selectedTable || tab.screenId) { + map[tab.id] = { + id: tab.screenId, + tableName: selectedTable, + }; + } + } + } + return map; + }, [tabs]); // ์ปดํฌ๋„ŒํŠธ ํƒญ ๋ชฉ๋ก ๋ณ€๊ฒฝ ์‹œ ๋™๊ธฐํ™” useEffect(() => { @@ -157,21 +176,10 @@ export function TabsWidget({ ) { setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true })); try { - // ๋ ˆ์ด์•„์›ƒ๊ณผ ํ™”๋ฉด ์ •๋ณด๋ฅผ ๋ณ‘๋ ฌ๋กœ ๋กœ๋“œ - const [layoutData, screenDef] = await Promise.all([ - screenApi.getLayout(extTab.screenId), - screenApi.getScreen(extTab.screenId), - ]); + const layoutData = await screenApi.getLayout(extTab.screenId); if (layoutData && layoutData.components) { setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components })); } - // ํƒญ์˜ ํ™”๋ฉด ์ •๋ณด ์ €์žฅ (tableName ํฌํ•จ) - if (screenDef) { - setScreenInfoMap((prev) => ({ - ...prev, - [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName }, - })); - } } catch (error) { console.error(`ํƒญ "${tab.label}" ํ™”๋ฉด ๋กœ๋“œ ์‹คํŒจ:`, error); setScreenErrors((prev) => ({ ...prev, [tab.id]: "ํ™”๋ฉด์„ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." })); @@ -185,31 +193,6 @@ export function TabsWidget({ loadScreenLayouts(); }, [visibleTabs, screenLayouts, screenLoadingStates]); - // screenInfoMap์ด ์—†๋Š” ํƒญ์˜ ํ™”๋ฉด ์ •๋ณด ๋ณด์ถฉ ๋กœ๋“œ - // screenId๊ฐ€ ์žˆ์ง€๋งŒ screenInfoMap์— ์•„์ง ์—†๋Š” ํƒญ์˜ ํ™”๋ฉด ์ •๋ณด๋ฅผ ๋กœ๋“œ - useEffect(() => { - const loadMissingScreenInfo = async () => { - for (const tab of visibleTabs) { - const extTab = tab as ExtendedTabItem; - // screenId๊ฐ€ ์žˆ๊ณ  screenInfoMap์— ์•„์ง ์—†๋Š” ๊ฒฝ์šฐ ๋กœ๋“œ - if (extTab.screenId && !screenInfoMap[tab.id]) { - try { - const screenDef = await screenApi.getScreen(extTab.screenId); - if (screenDef) { - setScreenInfoMap((prev) => ({ - ...prev, - [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName }, - })); - } - } catch (error) { - console.error(`ํƒญ "${tab.label}" ํ™”๋ฉด ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:`, error); - } - } - } - }; - loadMissingScreenInfo(); - }, [visibleTabs, screenInfoMap]); - // ์„ ํƒ๋œ ํƒญ ๋ณ€๊ฒฝ ์‹œ localStorage์— ์ €์žฅ + ActiveTab Context ์—…๋ฐ์ดํŠธ useEffect(() => { if (persistSelection && typeof window !== "undefined") { diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index c7ea8c94..2029f473 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -58,28 +58,56 @@ const DropdownSelect = forwardRef { const [open, setOpen] = useState(false); + // ํ˜„์žฌ ์„ ํƒ๋œ ๊ฐ’ ์กด์žฌ ์—ฌ๋ถ€ + const hasValue = useMemo(() => { + if (!value) return false; + if (Array.isArray(value)) return value.length > 0; + return value !== ""; + }, [value]); + // ๋‹จ์ผ ์„ ํƒ + ๊ฒ€์ƒ‰ ๋ถˆ๊ฐ€๋Šฅ โ†’ ๊ธฐ๋ณธ Select ์‚ฌ์šฉ if (!searchable && !multiple) { return ( - +
+ + {/* ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ (๊ฐ’์ด ์žˆ์„ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {allowClear && hasValue && !disabled && ( + { + e.stopPropagation(); + e.preventDefault(); + onChange?.(""); + }} + onPointerDown={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + > + + + )} +
); } @@ -142,10 +170,18 @@ const DropdownSelect = forwardRef
{allowClear && selectedValues.length > 0 && ( - + onPointerDown={(e) => { + // Radix Popover๊ฐ€ onPointerDown์œผ๋กœ ํŒ์˜ค๋ฒ„๋ฅผ ์—ฌ๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ + e.stopPropagation(); + e.preventDefault(); + }} + > + + )}
diff --git a/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx b/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx index 5ab010f2..18898198 100644 --- a/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx +++ b/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx @@ -70,18 +70,18 @@ export class V2SelectRenderer extends AutoRegisteringComponentRenderer { } // ๐Ÿ†• formData์— ๊ฐ’์ด ์—†๊ณ  ๊ธฐ๋ณธ๊ฐ’์ด ์„ค์ •๋œ ๊ฒฝ์šฐ, ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ + // ๋‹จ, formData์— ํ•ด๋‹น ํ‚ค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด(์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•œ ๊ฒฝ์šฐ) ๊ธฐ๋ณธ๊ฐ’์„ ์žฌ์ ์šฉํ•˜์ง€ ์•Š์Œ + const hasKeyInFormData = formData !== undefined && formData !== null && columnName in (formData || {}); if ( (currentValue === "" || currentValue === undefined || currentValue === null) && defaultValue && isInteractive && onFormDataChange && - columnName + columnName && + !hasKeyInFormData // formData์— ํ‚ค ์ž์ฒด๊ฐ€ ์—†์„ ๋•Œ๋งŒ ๊ธฐ๋ณธ๊ฐ’ ์ ์šฉ (์ดˆ๊ธฐ ๋ Œ๋”๋ง) ) { - // ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์‹œ ๊ธฐ๋ณธ๊ฐ’์„ formData์— ์„ค์ • setTimeout(() => { - if (!formData?.[columnName]) { - onFormDataChange(columnName, defaultValue); - } + onFormDataChange(columnName, defaultValue); }, 0); currentValue = defaultValue; } From bfd90792f8f4fcd3ec89d59d578415340288a03c Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 13 Feb 2026 15:12:54 +0900 Subject: [PATCH 3/7] feat: Enhance ScreenModal and InteractiveScreenViewer with improved resolution handling - Added detailed console logging in ScreenModal for debugging screen resolution, including final resolution and dimensions applied. - Updated getModalStyle in ScreenModal to handle null screen dimensions gracefully, ensuring default styles are applied when necessary. - Modified InteractiveScreenViewer's DialogContent to dynamically adjust width based on popupScreenResolution, improving responsiveness and user experience. - Ensured maximum width constraints are respected in both components, enhancing layout consistency across different screen sizes. --- frontend/components/common/ScreenModal.tsx | 22 ++++++++++++++++++- .../screen/InteractiveScreenViewer.tsx | 8 ++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 138f560c..b6660709 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -554,6 +554,16 @@ export const ScreenModal: React.FC = ({ className }) => { // ํ™”๋ฉด ๊ด€๋ฆฌ์—์„œ ์„ค์ •ํ•œ ํ•ด์ƒ๋„ ์‚ฌ์šฉ (์šฐ์„ ์ˆœ์œ„) const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution; + console.log("๐Ÿ” [ScreenModal] ํ•ด์ƒ๋„ ๋””๋ฒ„๊ทธ:", { + screenId, + v2ScreenResolution: v2LayoutData?.screenResolution, + layoutScreenResolution: (layoutData as any).screenResolution, + screenInfoResolution: (screenInfo as any).screenResolution, + finalScreenResolution: screenResolution, + hasWidth: screenResolution?.width, + hasHeight: screenResolution?.height, + }); + let dimensions; if (screenResolution && screenResolution.width && screenResolution.height) { // ํ™”๋ฉด ๊ด€๋ฆฌ์—์„œ ์„ค์ •ํ•œ ํ•ด์ƒ๋„ ์‚ฌ์šฉ @@ -563,9 +573,11 @@ export const ScreenModal: React.FC = ({ className }) => { offsetX: 0, offsetY: 0, }; + console.log("โœ… [ScreenModal] ํ™”๋ฉด๊ด€๋ฆฌ ํ•ด์ƒ๋„ ์ ์šฉ:", dimensions); } else { // ํ•ด์ƒ๋„ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ์ž๋™ ๊ณ„์‚ฐ dimensions = calculateScreenDimensions(components); + console.log("โš ๏ธ [ScreenModal] ํ•ด์ƒ๋„ ์—†์Œ - ์ž๋™ ๊ณ„์‚ฐ:", dimensions); } setScreenDimensions(dimensions); @@ -869,16 +881,24 @@ export const ScreenModal: React.FC = ({ className }) => { // ๋ชจ๋‹ฌ ํฌ๊ธฐ ์„ค์ • - ํ™”๋ฉด๊ด€๋ฆฌ ์„ค์ • ํฌ๊ธฐ + ํ—ค๋”/ํ‘ธํ„ฐ const getModalStyle = () => { if (!screenDimensions) { + console.log("โš ๏ธ [ScreenModal] getModalStyle: screenDimensions๊ฐ€ null - ๊ธฐ๋ณธ ์Šคํƒ€์ผ ์‚ฌ์šฉ"); return { className: "w-fit min-w-[400px] max-w-4xl overflow-hidden", style: { padding: 0, gap: 0, maxHeight: "calc(100dvh - 8px)" }, }; } + const finalWidth = Math.min(screenDimensions.width, window.innerWidth * 0.98); + console.log("โœ… [ScreenModal] getModalStyle: ํ•ด์ƒ๋„ ์ ์šฉ๋จ", { + screenDimensions, + finalWidth: `${finalWidth}px`, + viewportWidth: window.innerWidth, + }); + return { className: "overflow-hidden", style: { - width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`, + width: `${finalWidth}px`, // CSS๊ฐ€ ์•Œ์•„์„œ ์ฒ˜๋ฆฌ: ๋ทฐํฌํŠธ ์•ˆ์— ๋“ค์–ด๊ฐ€๋ฉด auto-height, ๋„˜์น˜๋ฉด max-height๋กœ ์ œํ•œ maxHeight: "calc(100dvh - 8px)", maxWidth: "98vw", diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index c4b2ad0a..252f5c2b 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -2494,7 +2494,13 @@ export const InteractiveScreenViewer: React.FC = ( setPopupScreen(null); setPopupFormData({}); // ํŒ์—… ๋‹ซ์„ ๋•Œ formData๋„ ์ดˆ๊ธฐํ™” }}> - + {popupScreen?.title || "์ƒ์„ธ ์ •๋ณด"} From 9e9aa01b03f23e1d0ab8793f3e6ff2bc5f7e7577 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Sun, 22 Feb 2026 20:54:34 +0900 Subject: [PATCH 4/7] Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into gbpark-node From 9614ce39735a920ba48867ef13180ed277fc7bc2 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Mon, 23 Feb 2026 09:16:44 +0900 Subject: [PATCH 5/7] feat: Enhance EditModal and V2Repeater functionality - Implemented zone offset adjustments for conditional components in EditModal to ensure correct rendering positions. - Added repeaterSave event dispatching in EditModal after saving data, improving integration with V2Repeater. - Updated V2Repeater to handle existing detail data loading based on foreign key relationships, enhancing data management. - Improved calculation rules handling in V2RepeaterConfigPanel, allowing for dynamic updates and better user experience. - Enhanced SplitPanelLayoutComponent to conditionally load data based on selected items and tab changes, improving performance and usability. --- frontend/components/screen/EditModal.tsx | 47 +++- frontend/components/screen/ScreenDesigner.tsx | 10 +- frontend/components/v2/V2Repeater.tsx | 215 ++++++++++++++---- frontend/components/v2/V2Select.tsx | 12 +- .../config-panels/V2RepeaterConfigPanel.tsx | 176 ++++++++++---- .../v2-repeater/V2RepeaterRenderer.tsx | 3 + .../SplitPanelLayoutComponent.tsx | 82 ++++--- frontend/types/v2-repeater.ts | 2 + 8 files changed, 410 insertions(+), 137 deletions(-) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index d8ce8e7a..0fd0cfec 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -565,12 +565,32 @@ export const EditModal: React.FC = ({ className }) => { return newActiveIds; }, [formData, groupData, conditionalLayers, screenData?.components]); - // ๐Ÿ†• ํ™œ์„ฑํ™”๋œ ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ + // ํ™œ์„ฑํ™”๋œ ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด์˜ ์ปดํฌ๋„ŒํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ (Zone ์˜คํ”„์…‹ ์ ์šฉ) const activeConditionalComponents = useMemo(() => { return conditionalLayers .filter((layer) => activeConditionalLayerIds.includes(layer.id)) - .flatMap((layer) => (layer as LayerDefinition & { components: ComponentData[] }).components || []); - }, [conditionalLayers, activeConditionalLayerIds]); + .flatMap((layer) => { + const layerWithComps = layer as LayerDefinition & { components: ComponentData[] }; + const comps = layerWithComps.components || []; + + // Zone ์˜คํ”„์…‹ ์ ์šฉ: ์กฐ๊ฑด๋ถ€ ๋ ˆ์ด์–ด ์ปดํฌ๋„ŒํŠธ๋Š” Zone ๋‚ด๋ถ€ ์ƒ๋Œ€ ์ขŒํ‘œ๋กœ ์ €์žฅ๋˜๋ฏ€๋กœ + // Zone์˜ ์ ˆ๋Œ€ ์ขŒํ‘œ๋ฅผ ๋”ํ•ด์ค˜์•ผ EditModal์—์„œ ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜์— ๋ Œ๋”๋ง๋จ + const associatedZone = zones.find((z) => z.zone_id === (layer as any).zoneId); + if (!associatedZone) return comps; + + const zoneOffsetX = associatedZone.x || 0; + const zoneOffsetY = associatedZone.y || 0; + + return comps.map((comp) => ({ + ...comp, + position: { + ...comp.position, + x: parseFloat(comp.position?.x?.toString() || "0") + zoneOffsetX, + y: parseFloat(comp.position?.y?.toString() || "0") + zoneOffsetY, + }, + })); + }); + }, [conditionalLayers, activeConditionalLayerIds, zones]); const handleClose = () => { setModalState({ @@ -881,14 +901,31 @@ export const EditModal: React.FC = ({ className }) => { } } + // V2Repeater ์ €์žฅ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (๋””ํ…Œ์ผ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ) + const hasRepeaterInstances = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0; + if (hasRepeaterInstances) { + const masterRecordId = groupData[0]?.id || formData.id; + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: masterRecordId, + masterRecordId, + mainFormData: formData, + tableName: screenData.screenInfo.tableName, + }, + }), + ); + console.log("๐Ÿ“‹ [EditModal] ๊ทธ๋ฃน ์ €์žฅ ํ›„ repeaterSave ์ด๋ฒคํŠธ ๋ฐœ์ƒ:", { masterRecordId }); + } + // ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ const messages: string[] = []; if (insertedCount > 0) messages.push(`${insertedCount}๊ฐœ ์ถ”๊ฐ€`); if (updatedCount > 0) messages.push(`${updatedCount}๊ฐœ ์ˆ˜์ •`); if (deletedCount > 0) messages.push(`${deletedCount}๊ฐœ ์‚ญ์ œ`); - if (messages.length > 0) { - toast.success(`ํ’ˆ๋ชฉ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค (${messages.join(", ")})`); + if (messages.length > 0 || hasRepeaterInstances) { + toast.success(messages.length > 0 ? `ํ’ˆ๋ชฉ์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค (${messages.join(", ")})` : "์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ onSave ์ฝœ๋ฐฑ ์‹คํ–‰ (ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ) if (modalState.onSave) { diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 76bd8973..af4fc96b 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -5555,8 +5555,12 @@ export default function ScreenDesigner({ return false; } - // 6. ์‚ญ์ œ (๋‹จ์ผ/๋‹ค์ค‘ ์„ ํƒ ์ง€์›) - if (e.key === "Delete" && (selectedComponent || groupState.selectedComponents.length > 0)) { + // 6. ์‚ญ์ œ (๋‹จ์ผ/๋‹ค์ค‘ ์„ ํƒ ์ง€์›) - Delete ๋˜๋Š” Backspace(Mac) + const isInputFocused = document.activeElement instanceof HTMLInputElement || + document.activeElement instanceof HTMLTextAreaElement || + document.activeElement instanceof HTMLSelectElement || + (document.activeElement as HTMLElement)?.isContentEditable; + if ((e.key === "Delete" || (e.key === "Backspace" && !isInputFocused)) && (selectedComponent || groupState.selectedComponents.length > 0)) { // console.log("๐Ÿ—‘๏ธ ์ปดํฌ๋„ŒํŠธ ์‚ญ์ œ (๋‹จ์ถ•ํ‚ค)"); e.preventDefault(); e.stopPropagation(); @@ -7418,7 +7422,7 @@ export default function ScreenDesigner({

ํŽธ์ง‘: Ctrl+C(๋ณต์‚ฌ), Ctrl+V(๋ถ™์—ฌ๋„ฃ๊ธฐ), Ctrl+S(์ €์žฅ), - Ctrl+Z(์‹คํ–‰์ทจ์†Œ), Delete(์‚ญ์ œ) + Ctrl+Z(์‹คํ–‰์ทจ์†Œ), Delete/Backspace(์‚ญ์ œ)

โš ๏ธ diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index 0f16cd31..734032f3 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -43,6 +43,7 @@ export const V2Repeater: React.FC = ({ onDataChange, onRowClick, className, + formData: parentFormData, }) => { // ์„ค์ • ๋ณ‘ํ•ฉ const config: V2RepeaterConfig = useMemo( @@ -153,21 +154,15 @@ export const V2Repeater: React.FC = ({ // ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ (์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ ์‹œ์—๋Š” ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ ์•ˆํ•จ) let mergedData: Record; if (config.useCustomTable && config.mainTableName) { - // ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”: ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅ mergedData = { ...cleanRow }; - // ๐Ÿ†• FK ์ž๋™ ์—ฐ๊ฒฐ - foreignKeySourceColumn์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์‚ฌ์šฉ if (config.foreignKeyColumn) { - // foreignKeySourceColumn์ด ์žˆ์œผ๋ฉด mainFormData์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’ ์‚ฌ์šฉ - // ์—†์œผ๋ฉด ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID ์‚ฌ์šฉ (๊ธฐ์กด ๋™์ž‘) const sourceColumn = config.foreignKeySourceColumn; let fkValue: any; if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) { - // mainFormData์—์„œ ์ฐธ์กฐ ์ปฌ๋Ÿผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ fkValue = mainFormData[sourceColumn]; } else { - // ๊ธฐ๋ณธ: ๋งˆ์Šคํ„ฐ ๋ ˆ์ฝ”๋“œ ID ์‚ฌ์šฉ fkValue = masterRecordId; } @@ -176,7 +171,6 @@ export const V2Repeater: React.FC = ({ } } } else { - // ๊ธฐ์กด ๋ฐฉ์‹: ๋ฉ”์ธ ํผ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; mergedData = { ...mainFormDataWithoutId, @@ -192,7 +186,19 @@ export const V2Repeater: React.FC = ({ } } - await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); + // ๊ธฐ์กด ํ–‰(id ์กด์žฌ)์€ UPDATE, ์ƒˆ ํ–‰์€ INSERT + const rowId = row.id; + if (rowId && typeof rowId === "string" && rowId.includes("-")) { + // UUID ํ˜•ํƒœ์˜ id๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋ฐ์ดํ„ฐ โ†’ UPDATE + const { id: _, created_date: _cd, updated_date: _ud, ...updateFields } = filteredData; + await apiClient.put(`/table-management/tables/${tableName}/edit`, { + originalData: { id: rowId }, + updatedData: updateFields, + }); + } else { + // ์ƒˆ ํ–‰ โ†’ INSERT + await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); + } } } catch (error) { console.error("โŒ V2Repeater ์ €์žฅ ์‹คํŒจ:", error); @@ -228,6 +234,108 @@ export const V2Repeater: React.FC = ({ parentId, ]); + // ์ˆ˜์ • ๋ชจ๋“œ: useCustomTable + FK ๊ธฐ๋ฐ˜์œผ๋กœ ๊ธฐ์กด ๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ ์ž๋™ ๋กœ๋“œ + const dataLoadedRef = useRef(false); + useEffect(() => { + if (dataLoadedRef.current) return; + if (!config.useCustomTable || !config.mainTableName || !config.foreignKeyColumn) return; + if (!parentFormData) return; + + const fkSourceColumn = config.foreignKeySourceColumn || config.foreignKeyColumn; + const fkValue = parentFormData[fkSourceColumn]; + if (!fkValue) return; + + // ์ด๋ฏธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (data.length > 0) return; + + const loadExistingData = async () => { + try { + console.log("๐Ÿ“ฅ [V2Repeater] ์ˆ˜์ • ๋ชจ๋“œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ:", { + tableName: config.mainTableName, + fkColumn: config.foreignKeyColumn, + fkValue, + }); + + const response = await apiClient.post( + `/table-management/tables/${config.mainTableName}/data`, + { + page: 1, + size: 1000, + search: { [config.foreignKeyColumn]: fkValue }, + autoFilter: true, + } + ); + + const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || []; + if (Array.isArray(rows) && rows.length > 0) { + console.log(`โœ… [V2Repeater] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ${rows.length}๊ฑด ๋กœ๋“œ ์™„๋ฃŒ`); + + // isSourceDisplay ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณด๊ฐ• + const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay); + const sourceTable = config.dataSource?.sourceTable; + const fkColumn = config.dataSource?.foreignKey; + const refKey = config.dataSource?.referenceKey || "id"; + + if (sourceDisplayColumns.length > 0 && sourceTable && fkColumn) { + try { + const fkValues = rows.map((row) => row[fkColumn]).filter(Boolean); + const uniqueValues = [...new Set(fkValues)]; + + if (uniqueValues.length > 0) { + // FK ๊ฐ’ ๊ธฐ๋ฐ˜์œผ๋กœ ์†Œ์Šค ํ…Œ์ด๋ธ”์—์„œ ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ๋งŒ ์กฐํšŒ + const sourcePromises = uniqueValues.map((val) => + apiClient.post(`/table-management/tables/${sourceTable}/data`, { + page: 1, size: 1, + search: { [refKey]: val }, + autoFilter: true, + }).then((r) => r.data?.data?.data || r.data?.data?.rows || []) + .catch(() => []) + ); + const sourceResults = await Promise.all(sourcePromises); + const sourceMap = new Map(); + sourceResults.flat().forEach((sr: any) => { + if (sr[refKey]) sourceMap.set(String(sr[refKey]), sr); + }); + + // ๊ฐ ํ–‰์— ์†Œ์Šค ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ + // RepeaterTable์€ isSourceDisplay ์ปฌ๋Ÿผ์„ `_display_${col.key}` ํ•„๋“œ๋กœ ๋ Œ๋”๋งํ•จ + rows.forEach((row: any) => { + const sourceRecord = sourceMap.get(String(row[fkColumn])); + if (sourceRecord) { + sourceDisplayColumns.forEach((col) => { + const displayValue = sourceRecord[col.key] ?? null; + row[col.key] = displayValue; + row[`_display_${col.key}`] = displayValue; + }); + } + }); + console.log("โœ… [V2Repeater] ์†Œ์Šค ํ…Œ์ด๋ธ” ํ‘œ์‹œ ๋ฐ์ดํ„ฐ ๋ณด๊ฐ• ์™„๋ฃŒ"); + } + } catch (sourceError) { + console.warn("โš ๏ธ [V2Repeater] ์†Œ์Šค ํ…Œ์ด๋ธ” ์กฐํšŒ ์‹คํŒจ (ํ‘œ์‹œ๋งŒ ์˜ํ–ฅ):", sourceError); + } + } + + setData(rows); + dataLoadedRef.current = true; + if (onDataChange) onDataChange(rows); + } + } catch (error) { + console.error("โŒ [V2Repeater] ๊ธฐ์กด ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + + loadExistingData(); + }, [ + config.useCustomTable, + config.mainTableName, + config.foreignKeyColumn, + config.foreignKeySourceColumn, + parentFormData, + data.length, + onDataChange, + ]); + // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด ๋กœ๋“œ useEffect(() => { const loadCurrentTableColumnInfo = async () => { @@ -451,58 +559,71 @@ export const V2Repeater: React.FC = ({ loadCategoryLabels(); }, [data, sourceCategoryColumns]); + // ๊ณ„์‚ฐ ๊ทœ์น™ ์ ์šฉ (์†Œ์Šค ํ…Œ์ด๋ธ”์˜ _display_* ํ•„๋“œ๋„ ์ฐธ์กฐ ๊ฐ€๋Šฅ) + const applyCalculationRules = useCallback( + (row: any): any => { + const rules = config.calculationRules; + if (!rules || rules.length === 0) return row; + + const updatedRow = { ...row }; + for (const rule of rules) { + if (!rule.targetColumn || !rule.formula) continue; + try { + let formula = rule.formula; + const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || []; + for (const field of fieldMatches) { + if (field === rule.targetColumn) continue; + // ์ง์ ‘ ํ•„๋“œ โ†’ _display_* ํ•„๋“œ ์ˆœ์œผ๋กœ ๊ฐ’ ํƒ์ƒ‰ + const raw = updatedRow[field] ?? updatedRow[`_display_${field}`]; + const value = parseFloat(raw) || 0; + formula = formula.replace(new RegExp(`\\b${field}\\b`, "g"), value.toString()); + } + updatedRow[rule.targetColumn] = new Function(`return ${formula}`)(); + } catch { + updatedRow[rule.targetColumn] = 0; + } + } + return updatedRow; + }, + [config.calculationRules], + ); + + // _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ onDataChange ํ˜ธ์ถœ + const notifyDataChange = useCallback( + (newData: any[]) => { + if (!onDataChange) return; + const targetTable = + config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + if (targetTable) { + onDataChange(newData.map((row) => ({ ...row, _targetTable: targetTable }))); + } else { + onDataChange(newData); + } + }, + [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], + ); + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleDataChange = useCallback( (newData: any[]) => { - setData(newData); - - // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ ์ „๋‹ฌ (๋ฐฑ์—”๋“œ์—์„œ ํ…Œ์ด๋ธ” ๋ถ„๋ฆฌ์šฉ) - if (onDataChange) { - const targetTable = - config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; - - if (targetTable) { - // ๊ฐ ํ–‰์— _targetTable ์ถ”๊ฐ€ - const dataWithTarget = newData.map((row) => ({ - ...row, - _targetTable: targetTable, - })); - onDataChange(dataWithTarget); - } else { - onDataChange(newData); - } - } - - // ๐Ÿ†• ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ ์ปฌ๋Ÿผ ๋„ˆ๋น„ ์กฐ์ • + const calculated = newData.map(applyCalculationRules); + setData(calculated); + notifyDataChange(calculated); setAutoWidthTrigger((prev) => prev + 1); }, - [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], + [applyCalculationRules, notifyDataChange], ); // ํ–‰ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleRowChange = useCallback( (index: number, newRow: any) => { + const calculated = applyCalculationRules(newRow); const newData = [...data]; - newData[index] = newRow; + newData[index] = calculated; setData(newData); - - // ๐Ÿ†• _targetTable ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ - if (onDataChange) { - const targetTable = - config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; - - if (targetTable) { - const dataWithTarget = newData.map((row) => ({ - ...row, - _targetTable: targetTable, - })); - onDataChange(dataWithTarget); - } else { - onDataChange(newData); - } - } + notifyDataChange(newData); }, - [data, onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName], + [data, applyCalculationRules, notifyDataChange], ); // ํ–‰ ์‚ญ์ œ ํ•ธ๋“ค๋Ÿฌ diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index c7ea8c94..b13d450e 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -153,13 +153,11 @@ const DropdownSelect = forwardRef { - // value๋Š” CommandItem์˜ value (๋ผ๋ฒจ) - // search๋Š” ๊ฒ€์ƒ‰์–ด + filter={(itemValue, search) => { if (!search) return 1; - const normalizedValue = value.toLowerCase(); - const normalizedSearch = search.toLowerCase(); - if (normalizedValue.includes(normalizedSearch)) return 1; + const option = options.find((o) => o.value === itemValue); + const label = (option?.label || option?.value || "").toLowerCase(); + if (label.includes(search.toLowerCase())) return 1; return 0; }} > @@ -172,7 +170,7 @@ const DropdownSelect = forwardRef handleSelect(option.value)} > = ({ const [currentTableColumns, setCurrentTableColumns] = useState([]); // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ const [entityColumns, setEntityColumns] = useState([]); // ์—”ํ‹ฐํ‹ฐ ํƒ€์ž… ์ปฌ๋Ÿผ const [sourceTableColumns, setSourceTableColumns] = useState([]); // ์†Œ์Šค(์—”ํ‹ฐํ‹ฐ) ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ - const [calculationRules, setCalculationRules] = useState([]); + const [calculationRules, setCalculationRules] = useState( + config.calculationRules || [] + ); const [loadingColumns, setLoadingColumns] = useState(false); const [loadingSourceColumns, setLoadingSourceColumns] = useState(false); @@ -553,26 +555,56 @@ export const V2RepeaterConfigPanel: React.FC = ({ updateConfig({ columns: newColumns }); }; + // ๊ณ„์‚ฐ ๊ทœ์น™์„ config์— ๋ฐ˜์˜ํ•˜๋Š” ํ—ฌํผ + const syncCalculationRules = (rules: CalculationRule[]) => { + setCalculationRules(rules); + updateConfig({ calculationRules: rules }); + }; + // ๊ณ„์‚ฐ ๊ทœ์น™ ์ถ”๊ฐ€ const addCalculationRule = () => { - setCalculationRules(prev => [ - ...prev, + const newRules = [ + ...calculationRules, { id: `calc_${Date.now()}`, targetColumn: "", formula: "" } - ]); + ]; + syncCalculationRules(newRules); }; // ๊ณ„์‚ฐ ๊ทœ์น™ ์‚ญ์ œ const removeCalculationRule = (id: string) => { - setCalculationRules(prev => prev.filter(r => r.id !== id)); + syncCalculationRules(calculationRules.filter(r => r.id !== id)); }; // ๊ณ„์‚ฐ ๊ทœ์น™ ์—…๋ฐ์ดํŠธ const updateCalculationRule = (id: string, field: keyof CalculationRule, value: string) => { - setCalculationRules(prev => - prev.map(r => r.id === id ? { ...r, [field]: value } : r) + syncCalculationRules( + calculationRules.map(r => r.id === id ? { ...r, [field]: value } : r) ); }; + // ์ˆ˜์‹ ์ž…๋ ฅ ํ•„๋“œ์— ์ปฌ๋Ÿผ๋ช… ์‚ฝ์ž… + const insertColumnToFormula = (ruleId: string, columnKey: string) => { + const rule = calculationRules.find(r => r.id === ruleId); + if (!rule) return; + const newFormula = rule.formula ? `${rule.formula} ${columnKey}` : columnKey; + updateCalculationRule(ruleId, "formula", newFormula); + }; + + // ์ˆ˜์‹์˜ ์˜์–ด ์ปฌ๋Ÿผ๋ช…์„ ํ•œ๊ธ€ ์ œ๋ชฉ์œผ๋กœ ๋ณ€ํ™˜ + const formulaToKorean = (formula: string): string => { + if (!formula) return ""; + let result = formula; + const allCols = config.columns || []; + // ๊ธด ์ปฌ๋Ÿผ๋ช…๋ถ€ํ„ฐ ์น˜ํ™˜ (๋ถ€๋ถ„ ๋งค์นญ ๋ฐฉ์ง€) + const sorted = [...allCols].sort((a, b) => b.key.length - a.key.length); + for (const col of sorted) { + if (col.title && col.key) { + result = result.replace(new RegExp(`\\b${col.key}\\b`, "g"), col.title); + } + } + return result; + }; + // ์—”ํ‹ฐํ‹ฐ ์ปฌ๋Ÿผ ์„ ํƒ ์‹œ ์†Œ์Šค ํ…Œ์ด๋ธ” ์ž๋™ ์„ค์ • const handleEntityColumnSelect = (columnName: string) => { const selectedEntity = entityColumns.find(c => c.columnName === columnName); @@ -1374,7 +1406,7 @@ export const V2RepeaterConfigPanel: React.FC = ({ {(isModalMode || isInlineMode) && config.columns.length > 0 && ( <> -

+
-

- ์˜ˆ: ๊ธˆ์•ก = ์ˆ˜๋Ÿ‰ * ๋‹จ๊ฐ€ -

-
+
{calculationRules.map((rule) => ( -
- - - = - - updateCalculationRule(rule.id, "formula", e.target.value)} - placeholder="quantity * unit_price" - className="h-7 flex-1 text-xs" - /> - - +
+
+ + = + updateCalculationRule(rule.id, "formula", e.target.value)} + placeholder="์ปฌ๋Ÿผ ํด๋ฆญ ๋˜๋Š” ์ง์ ‘ ์ž…๋ ฅ" + className="h-6 flex-1 font-mono text-[10px]" + /> + +
+ + {/* ํ•œ๊ธ€ ์ˆ˜์‹ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {rule.formula && ( +

+ {config.columns.find(c => c.key === rule.targetColumn)?.title || rule.targetColumn || "๊ฒฐ๊ณผ"} = {formulaToKorean(rule.formula)} +

+ )} + + {/* ์ปฌ๋Ÿผ ์นฉ: ๋””ํ…Œ์ผ ์ปฌ๋Ÿผ + ์†Œ์Šค(ํ’ˆ๋ชฉ) ์ปฌ๋Ÿผ + ์—ฐ์‚ฐ์ž */} +
+ {config.columns + .filter(col => col.key !== rule.targetColumn && !col.isSourceDisplay) + .map((col) => ( + + ))} + {config.columns + .filter(col => col.isSourceDisplay) + .map((col) => ( + + ))} + {["+", "-", "*", "/", "(", ")"].map((op) => ( + + ))} +
))} {calculationRules.length === 0 && ( -

+

๊ณ„์‚ฐ ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค

)} diff --git a/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx b/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx index 908bc4f1..e531b655 100644 --- a/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx +++ b/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx @@ -20,6 +20,7 @@ interface V2RepeaterRendererProps { onRowClick?: (row: any) => void; onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void; parentId?: string | number; + formData?: Record; } const V2RepeaterRenderer: React.FC = ({ @@ -31,6 +32,7 @@ const V2RepeaterRenderer: React.FC = ({ onRowClick, onButtonClick, parentId, + formData, }) => { // component.componentConfig ๋˜๋Š” component.config์—์„œ V2RepeaterConfig ์ถ”์ถœ const config: V2RepeaterConfig = React.useMemo(() => { @@ -101,6 +103,7 @@ const V2RepeaterRenderer: React.FC = ({ onRowClick={onRowClick} onButtonClick={onButtonClick} className={component?.className} + formData={formData} /> ); }; diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx index ebecedb3..f56b0fb3 100644 --- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1526,22 +1526,30 @@ export const SplitPanelLayoutComponent: React.FC [componentConfig.rightPanel?.additionalTabs, isDesignMode, toast], ); - // ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ์—๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ) + // ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ const handleTabChange = useCallback( (newTabIndex: number) => { setActiveTabIndex(newTabIndex); + // ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ ๋ฐ์ดํ„ฐ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail"; + const requireSelection = mainRelationType === "detail"; + if (newTabIndex === 0) { if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) { - loadRightData(selectedLeftItem); + if (!requireSelection || selectedLeftItem) { + loadRightData(selectedLeftItem); + } } } else { if (!tabsData[newTabIndex]) { - loadTabData(newTabIndex, selectedLeftItem); + if (!requireSelection || selectedLeftItem) { + loadTabData(newTabIndex, selectedLeftItem); + } } } }, - [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData], + [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData, componentConfig.rightPanel?.relation?.type], ); // ์ขŒ์ธก ํ•ญ๋ชฉ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ (๋™์ผ ํ•ญ๋ชฉ ์žฌํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ โ†’ ์ „์ฒด ๋ฐ์ดํ„ฐ ํ‘œ์‹œ) @@ -1554,24 +1562,31 @@ export const SplitPanelLayoutComponent: React.FC selectedLeftItem[leftPk] === item[leftPk]; if (isSameItem) { - // ์„ ํƒ ํ•ด์ œ โ†’ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + // ์„ ํƒ ํ•ด์ œ setSelectedLeftItem(null); - setCustomLeftSelectedData({}); // ์ปค์Šคํ…€ ๋ชจ๋“œ ์šฐ์ธก ํผ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + setCustomLeftSelectedData({}); setExpandedRightItems(new Set()); setTabsData({}); - if (activeTabIndex === 0) { - loadRightData(null); + + const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail"; + if (mainRelationType === "detail") { + // "์„ ํƒ ์‹œ ํ‘œ์‹œ" ๋ชจ๋“œ: ์„ ํƒ ํ•ด์ œ ์‹œ ๋ฐ์ดํ„ฐ ๋น„์›€ + setRightData(null); } else { - loadTabData(activeTabIndex, null); - } - // ์ถ”๊ฐ€ ํƒญ๋“ค๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ - const tabs = componentConfig.rightPanel?.additionalTabs; - if (tabs && tabs.length > 0) { - tabs.forEach((_: any, idx: number) => { - if (idx + 1 !== activeTabIndex) { - loadTabData(idx + 1, null); - } - }); + // "์—ฐ๊ด€ ๋ชฉ๋ก" ๋ชจ๋“œ: ์„ ํƒ ํ•ด์ œ ์‹œ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (activeTabIndex === 0) { + loadRightData(null); + } else { + loadTabData(activeTabIndex, null); + } + const tabs = componentConfig.rightPanel?.additionalTabs; + if (tabs && tabs.length > 0) { + tabs.forEach((_: any, idx: number) => { + if (idx + 1 !== activeTabIndex) { + loadTabData(idx + 1, null); + } + }); + } } return; } @@ -2778,15 +2793,17 @@ export const SplitPanelLayoutComponent: React.FC if (relationshipType === "join") { loadRightData(null); } - // ์ถ”๊ฐ€ ํƒญ: ๊ฐ ํƒญ์˜ relation.type์— ๋”ฐ๋ผ ์ดˆ๊ธฐ ๋กœ๋“œ ๊ฒฐ์ • - const tabs = componentConfig.rightPanel?.additionalTabs; - if (tabs && tabs.length > 0) { - tabs.forEach((tab: any, idx: number) => { - const tabRelType = tab.relation?.type || "join"; - if (tabRelType === "join") { - loadTabData(idx + 1, null); - } - }); + // ์ถ”๊ฐ€ ํƒญ: ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ถ”๊ฐ€ ํƒญ๋„ ์ดˆ๊ธฐ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (relationshipType !== "detail") { + const tabs = componentConfig.rightPanel?.additionalTabs; + if (tabs && tabs.length > 0) { + tabs.forEach((tab: any, idx: number) => { + const tabRelType = tab.relation?.type || "join"; + if (tabRelType === "join") { + loadTabData(idx + 1, null); + } + }); + } } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -3734,6 +3751,17 @@ export const SplitPanelLayoutComponent: React.FC const currentTabData = tabsData[activeTabIndex] || []; const isTabLoading = tabsLoading[activeTabIndex]; + // ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ขŒ์ธก ๋ฏธ์„ ํƒ ์‹œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ + const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail"; + if (mainRelationType === "detail" && !selectedLeftItem && !isDesignMode) { + return ( +
+

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

+

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

+
+ ); + } + if (isTabLoading) { return (
diff --git a/frontend/types/v2-repeater.ts b/frontend/types/v2-repeater.ts index d09ac9e9..fab7a523 100644 --- a/frontend/types/v2-repeater.ts +++ b/frontend/types/v2-repeater.ts @@ -180,7 +180,9 @@ export interface V2RepeaterProps { data?: any[]; // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ (์—†์œผ๋ฉด API๋กœ ๋กœ๋“œ) onDataChange?: (data: any[]) => void; onRowClick?: (row: any) => void; + onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void; className?: string; + formData?: Record; // ์ˆ˜์ • ๋ชจ๋“œ์—์„œ FK ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ์šฉ } // ๊ธฐ๋ณธ ์„ค์ •๊ฐ’ From bfdf061ead6f9a1afd5f70d4d75e3c07c62bfff1 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 23 Feb 2026 10:53:10 +0900 Subject: [PATCH 6/7] refactor: Clean up and enhance component structure in V2Media and ComponentsPanel - Removed redundant comments and improved clarity in the `ComponentsPanel` for better maintainability. - Refactored the `V2Media` component to streamline the file handling logic and ensure consistent state management. - Merged default configurations in `UniversalFormModalConfigPanel` to enhance safety and prevent potential issues with incomplete configurations. - Updated file upload handling in `FileManagerModal` to improve user experience and maintain consistent styling across components. --- .../screen/panels/ComponentsPanel.tsx | 3 +- frontend/components/v2/V2Media.tsx | 1399 +++++++++-------- .../UniversalFormModalConfigPanel.tsx | 28 +- .../v2-file-upload/FileManagerModal.tsx | 292 ++-- 4 files changed, 880 insertions(+), 842 deletions(-) diff --git a/frontend/components/screen/panels/ComponentsPanel.tsx b/frontend/components/screen/panels/ComponentsPanel.tsx index a076b867..aa7b894d 100644 --- a/frontend/components/screen/panels/ComponentsPanel.tsx +++ b/frontend/components/screen/panels/ComponentsPanel.tsx @@ -114,8 +114,7 @@ export function ComponentsPanel({ "image-display", // โ†’ v2-media (image) // ๊ณตํ†ต์ฝ”๋“œ๊ด€๋ฆฌ๋กœ ํ†ตํ•ฉ ์˜ˆ์ • "category-manager", // โ†’ ๊ณตํ†ต์ฝ”๋“œ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์œผ๋กœ ํ†ตํ•ฉ ์˜ˆ์ • - // ๋ถ„ํ•  ํŒจ๋„ ์ •๋ฆฌ (split-panel-layout v1 ์œ ์ง€) - "split-panel-layout2", // โ†’ split-panel-layout๋กœ ํ†ตํ•ฉ + // ๋ถ„ํ•  ํŒจ๋„ ์ •๋ฆฌ "screen-split-panel", // ํ™”๋ฉด ์ž„๋ฒ ๋”ฉ ๋ฐฉ์‹์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ // ๋ฏธ์™„์„ฑ/๋ฏธ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ (๊ธฐ์กด ํ™”๋ฉด ํ˜ธํ™˜์„ฑ ์œ ์ง€, ์ƒˆ ์ถ”๊ฐ€๋งŒ ๋ง‰์Œ) "accordion-basic", // ์•„์ฝ”๋””์–ธ ์ปดํฌ๋„ŒํŠธ diff --git a/frontend/components/v2/V2Media.tsx b/frontend/components/v2/V2Media.tsx index 733d6657..0a4faaae 100644 --- a/frontend/components/v2/V2Media.tsx +++ b/frontend/components/v2/V2Media.tsx @@ -2,13 +2,13 @@ /** * V2Media - * + * * ํ†ตํ•ฉ ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ (๋ ˆ๊ฑฐ์‹œ FileUploadComponent ๊ธฐ๋Šฅ ํ†ตํ•ฉ) * - file: ํŒŒ์ผ ์—…๋กœ๋“œ * - image: ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ/ํ‘œ์‹œ * - video: ๋น„๋””์˜ค * - audio: ์˜ค๋””์˜ค - * + * * ํ•ต์‹ฌ ๊ธฐ๋Šฅ: * - FileViewerModal / FileManagerModal (์ž์„ธํžˆ๋ณด๊ธฐ) * - ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์„ค์ • @@ -24,9 +24,23 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { V2MediaProps } from "@/types/v2-components"; -import { - Upload, X, File, Image as ImageIcon, Video, Music, Eye, Download, Trash2, Plus, - FileText, Archive, Presentation, FileImage, FileVideo, FileAudio +import { + Upload, + X, + File, + Image as ImageIcon, + Video, + Music, + Eye, + Download, + Trash2, + Plus, + FileText, + Archive, + Presentation, + FileImage, + FileVideo, + FileAudio, } from "lucide-react"; import { apiClient } from "@/lib/api/client"; import { toast } from "sonner"; @@ -77,115 +91,276 @@ const getFileIcon = (extension: string) => { /** * V2 ๋ฏธ๋””์–ด ์ปดํฌ๋„ŒํŠธ (๋ ˆ๊ฑฐ์‹œ ๊ธฐ๋Šฅ ํ†ตํ•ฉ) */ -export const V2Media = forwardRef( - (props, ref) => { - const { - id, - label, - required, - readonly, - disabled, - style, - size, - config: configProp, - value, - onChange, - formData, - columnName, - tableName, - onFormDataChange, - isDesignMode = false, - isInteractive = true, - onUpdate, - ...restProps - } = props; +export const V2Media = forwardRef((props, ref) => { + const { + id, + label, + required, + readonly, + disabled, + style, + size, + config: configProp, + value, + onChange, + formData, + columnName, + tableName, + onFormDataChange, + isDesignMode = false, + isInteractive = true, + onUpdate, + ...restProps + } = props; - // ์ธ์ฆ ์ •๋ณด - const { user } = useAuth(); + // ์ธ์ฆ ์ •๋ณด + const { user } = useAuth(); - // config ๊ธฐ๋ณธ๊ฐ’ - const config = configProp || { type: "file" as const }; - const mediaType = config.type || "file"; + // config ๊ธฐ๋ณธ๊ฐ’ + const config = configProp || { type: "file" as const }; + const mediaType = config.type || "file"; - // ํŒŒ์ผ ์ƒํƒœ - const [uploadedFiles, setUploadedFiles] = useState([]); - const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle"); - const [dragOver, setDragOver] = useState(false); - const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); - - // ๋ชจ๋‹ฌ ์ƒํƒœ - const [viewerFile, setViewerFile] = useState(null); - const [isViewerOpen, setIsViewerOpen] = useState(false); - const [isFileManagerOpen, setIsFileManagerOpen] = useState(false); - - const fileInputRef = useRef(null); + // ํŒŒ์ผ ์ƒํƒœ + const [uploadedFiles, setUploadedFiles] = useState([]); + const [uploadStatus, setUploadStatus] = useState<"idle" | "uploading" | "success" | "error">("idle"); + const [dragOver, setDragOver] = useState(false); + const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); - // ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํŒ๋‹จ - const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); - const recordTableName = formData?.tableName || tableName; - const recordId = formData?.id; - // ๐Ÿ”‘ columnName ์šฐ์„  ์‚ฌ์šฉ (์‹ค์ œ DB ์ปฌ๋Ÿผ๋ช…), ์—†์œผ๋ฉด id, ์ตœํ›„์— attachments - const effectiveColumnName = columnName || id || 'attachments'; + // ๋ชจ๋‹ฌ ์ƒํƒœ + const [viewerFile, setViewerFile] = useState(null); + const [isViewerOpen, setIsViewerOpen] = useState(false); + const [isFileManagerOpen, setIsFileManagerOpen] = useState(false); - // ๋ ˆ์ฝ”๋“œ์šฉ targetObjid ์ƒ์„ฑ - const getRecordTargetObjid = useCallback(() => { - if (isRecordMode && recordTableName && recordId) { - return `${recordTableName}:${recordId}:${effectiveColumnName}`; + const fileInputRef = useRef(null); + + // ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํŒ๋‹จ + const isRecordMode = !!(formData?.id && !String(formData.id).startsWith("temp_")); + const recordTableName = formData?.tableName || tableName; + const recordId = formData?.id; + // ๐Ÿ”‘ columnName ์šฐ์„  ์‚ฌ์šฉ (์‹ค์ œ DB ์ปฌ๋Ÿผ๋ช…), ์—†์œผ๋ฉด id, ์ตœํ›„์— attachments + const effectiveColumnName = columnName || id || "attachments"; + + // ๋ ˆ์ฝ”๋“œ์šฉ targetObjid ์ƒ์„ฑ + const getRecordTargetObjid = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `${recordTableName}:${recordId}:${effectiveColumnName}`; + } + return null; + }, [isRecordMode, recordTableName, recordId, effectiveColumnName]); + + // ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ + const getUniqueKey = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `v2media_${recordTableName}_${recordId}_${id}`; + } + return `v2media_${id}`; + }, [isRecordMode, recordTableName, recordId, id]); + + // ๋ ˆ์ฝ”๋“œ ID ๋ณ€๊ฒฝ ์‹œ ํŒŒ์ผ ๋ชฉ๋ก ์ดˆ๊ธฐํ™” + const prevRecordIdRef = useRef(null); + useEffect(() => { + if (prevRecordIdRef.current !== recordId) { + prevRecordIdRef.current = recordId; + if (isRecordMode) { + setUploadedFiles([]); } - return null; - }, [isRecordMode, recordTableName, recordId, effectiveColumnName]); + } + }, [recordId, isRecordMode]); - // ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ - const getUniqueKey = useCallback(() => { - if (isRecordMode && recordTableName && recordId) { - return `v2media_${recordTableName}_${recordId}_${id}`; - } - return `v2media_${id}`; - }, [isRecordMode, recordTableName, recordId, id]); + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ localStorage์—์„œ ํŒŒ์ผ ๋ณต์› + useEffect(() => { + if (!id) return; - // ๋ ˆ์ฝ”๋“œ ID ๋ณ€๊ฒฝ ์‹œ ํŒŒ์ผ ๋ชฉ๋ก ์ดˆ๊ธฐํ™” - const prevRecordIdRef = useRef(null); - useEffect(() => { - if (prevRecordIdRef.current !== recordId) { - prevRecordIdRef.current = recordId; - if (isRecordMode) { - setUploadedFiles([]); - } - } - }, [recordId, isRecordMode]); + try { + const backupKey = getUniqueKey(); + const backupFiles = localStorage.getItem(backupKey); + if (backupFiles) { + const parsedFiles = JSON.parse(backupFiles); + if (parsedFiles.length > 0) { + setUploadedFiles(parsedFiles); - // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ localStorage์—์„œ ํŒŒ์ผ ๋ณต์› - useEffect(() => { - if (!id) return; - - try { - const backupKey = getUniqueKey(); - const backupFiles = localStorage.getItem(backupKey); - if (backupFiles) { - const parsedFiles = JSON.parse(backupFiles); - if (parsedFiles.length > 0) { - setUploadedFiles(parsedFiles); - - if (typeof window !== "undefined") { - (window as any).globalFileState = { - ...(window as any).globalFileState, - [backupKey]: parsedFiles, - }; - } + if (typeof window !== "undefined") { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [backupKey]: parsedFiles, + }; } } - } catch (e) { - console.warn("ํŒŒ์ผ ๋ณต์› ์‹คํŒจ:", e); } - }, [id, getUniqueKey, recordId]); + } catch (e) { + console.warn("ํŒŒ์ผ ๋ณต์› ์‹คํŒจ:", e); + } + }, [id, getUniqueKey, recordId]); - // DB์—์„œ ํŒŒ์ผ ๋ชฉ๋ก ๋กœ๋“œ - const loadComponentFiles = useCallback(async () => { - if (!id) return false; + // DB์—์„œ ํŒŒ์ผ ๋ชฉ๋ก ๋กœ๋“œ + const loadComponentFiles = useCallback(async () => { + if (!id) return false; + + try { + let screenId = formData?.screenId; + + if (!screenId && typeof window !== "undefined") { + const pathname = window.location.pathname; + const screenMatch = pathname.match(/\/screens\/(\d+)/); + if (screenMatch) { + screenId = parseInt(screenMatch[1]); + } + } + + if (!screenId && isDesignMode) { + screenId = 999999; + } + + if (!screenId) { + screenId = 0; + } + + const params = { + screenId, + componentId: id, + tableName: recordTableName || formData?.tableName || tableName, + recordId: recordId || formData?.id, + columnName: effectiveColumnName, + }; + + const response = await getComponentFiles(params); + + if (response.success) { + const formattedFiles = response.totalFiles.map((file: any) => ({ + objid: file.objid || file.id, + savedFileName: file.savedFileName || file.saved_file_name, + realFileName: file.realFileName || file.real_file_name, + fileSize: file.fileSize || file.file_size, + fileExt: file.fileExt || file.file_ext, + regdate: file.regdate, + status: file.status || "ACTIVE", + uploadedAt: file.uploadedAt || new Date().toISOString(), + targetObjid: file.targetObjid || file.target_objid, + filePath: file.filePath || file.file_path, + ...file, + })); + + // localStorage์™€ ๋ณ‘ํ•ฉ + let finalFiles = formattedFiles; + const uniqueKey = getUniqueKey(); + try { + const backupFiles = localStorage.getItem(uniqueKey); + if (backupFiles) { + const parsedBackupFiles = JSON.parse(backupFiles); + const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid)); + const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); + finalFiles = [...formattedFiles, ...additionalFiles]; + } + } catch (e) { + console.warn("ํŒŒ์ผ ๋ณ‘ํ•ฉ ์˜ค๋ฅ˜:", e); + } + + setUploadedFiles(finalFiles); + + if (typeof window !== "undefined") { + (window as any).globalFileState = { + ...(window as any).globalFileState, + [uniqueKey]: finalFiles, + }; + + GlobalFileManager.registerFiles(finalFiles, { + uploadPage: window.location.pathname, + componentId: id, + screenId: formData?.screenId, + recordId: recordId, + }); + + try { + localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + } + return true; + } + } catch (error) { + console.error("ํŒŒ์ผ ์กฐํšŒ ์˜ค๋ฅ˜:", error); + } + return false; + }, [ + id, + tableName, + columnName, + formData?.screenId, + formData?.tableName, + formData?.id, + getUniqueKey, + recordId, + isRecordMode, + recordTableName, + effectiveColumnName, + isDesignMode, + ]); + + // ํŒŒ์ผ ๋™๊ธฐํ™” + useEffect(() => { + loadComponentFiles(); + }, [loadComponentFiles]); + + // ์ „์—ญ ์ƒํƒœ ๋ณ€๊ฒฝ ๊ฐ์ง€ + useEffect(() => { + const handleGlobalFileStateChange = (event: CustomEvent) => { + const { componentId, files, isRestore } = event.detail; + + if (componentId === id) { + setUploadedFiles(files); + + try { + const backupKey = getUniqueKey(); + localStorage.setItem(backupKey, JSON.stringify(files)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + } + }; + + if (typeof window !== "undefined") { + window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); + return () => { + window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); + }; + } + }, [id, getUniqueKey]); + + // ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ + const handleFileUpload = useCallback( + async (files: File[]) => { + if (!files.length) return; + + // ์ค‘๋ณต ์ฒดํฌ + const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase()); + const duplicates: string[] = []; + const uniqueFiles: File[] = []; + + files.forEach((file) => { + const fileName = file.name.toLowerCase(); + if (existingFileNames.includes(fileName)) { + duplicates.push(file.name); + } else { + uniqueFiles.push(file); + } + }); + + if (duplicates.length > 0) { + toast.error(`์ค‘๋ณต๋œ ํŒŒ์ผ: ${duplicates.join(", ")}`); + if (uniqueFiles.length === 0) return; + toast.info(`${uniqueFiles.length}๊ฐœ์˜ ์ƒˆ๋กœ์šด ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.`); + } + + const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files; + setUploadStatus("uploading"); + toast.loading("ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘...", { id: "file-upload" }); try { + const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table"; + const effectiveRecordId = recordId || formData?.id; + let screenId = formData?.screenId; - if (!screenId && typeof window !== "undefined") { const pathname = window.location.pathname; const screenMatch = pathname.match(/\/screens\/(\d+)/); @@ -194,368 +369,73 @@ export const V2Media = forwardRef( } } - if (!screenId && isDesignMode) { - screenId = 999999; + let targetObjid; + const effectiveIsRecordMode = + isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith("temp_")); + + if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { + targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; + } else if (screenId) { + targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`; + } else { + targetObjid = `temp_${id}`; } - if (!screenId) { - screenId = 0; - } + const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; - const params = { - screenId, - componentId: id, - tableName: recordTableName || formData?.tableName || tableName, - recordId: recordId || formData?.id, + const finalLinkedTable = effectiveIsRecordMode + ? effectiveTableName + : formData?.linkedTable || effectiveTableName; + + const uploadData = { + autoLink: formData?.autoLink || true, + linkedTable: finalLinkedTable, + recordId: effectiveRecordId || `temp_${id}`, columnName: effectiveColumnName, + isVirtualFileColumn: formData?.isVirtualFileColumn || true, + docType: config?.docType || "DOCUMENT", + docTypeName: config?.docTypeName || "์ผ๋ฐ˜ ๋ฌธ์„œ", + companyCode: userCompanyCode, + tableName: effectiveTableName, + fieldName: effectiveColumnName, + targetObjid: targetObjid, + isRecordMode: effectiveIsRecordMode, }; - const response = await getComponentFiles(params); + const response = await uploadFiles({ + files: filesToUpload, + ...uploadData, + }); if (response.success) { - const formattedFiles = response.totalFiles.map((file: any) => ({ + const fileData = response.files || (response as any).data || []; + + if (fileData.length === 0) { + throw new Error("์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); + } + + const newFiles = fileData.map((file: any) => ({ objid: file.objid || file.id, - savedFileName: file.savedFileName || file.saved_file_name, - realFileName: file.realFileName || file.real_file_name, - fileSize: file.fileSize || file.file_size, - fileExt: file.fileExt || file.file_ext, + savedFileName: file.saved_file_name || file.savedFileName, + realFileName: file.real_file_name || file.realFileName || file.name, + fileSize: file.file_size || file.fileSize || file.size, + fileExt: file.file_ext || file.fileExt || file.extension, + filePath: file.file_path || file.filePath || file.path, + docType: file.doc_type || file.docType, + docTypeName: file.doc_type_name || file.docTypeName, + targetObjid: file.target_objid || file.targetObjid, + parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, + companyCode: file.company_code || file.companyCode, + writer: file.writer, regdate: file.regdate, status: file.status || "ACTIVE", - uploadedAt: file.uploadedAt || new Date().toISOString(), - targetObjid: file.targetObjid || file.target_objid, - filePath: file.filePath || file.file_path, + uploadedAt: new Date().toISOString(), ...file, })); - // localStorage์™€ ๋ณ‘ํ•ฉ - let finalFiles = formattedFiles; - const uniqueKey = getUniqueKey(); - try { - const backupFiles = localStorage.getItem(uniqueKey); - if (backupFiles) { - const parsedBackupFiles = JSON.parse(backupFiles); - const serverObjIds = new Set(formattedFiles.map((f: any) => f.objid)); - const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); - finalFiles = [...formattedFiles, ...additionalFiles]; - } - } catch (e) { - console.warn("ํŒŒ์ผ ๋ณ‘ํ•ฉ ์˜ค๋ฅ˜:", e); - } - - setUploadedFiles(finalFiles); - - if (typeof window !== "undefined") { - (window as any).globalFileState = { - ...(window as any).globalFileState, - [uniqueKey]: finalFiles, - }; - - GlobalFileManager.registerFiles(finalFiles, { - uploadPage: window.location.pathname, - componentId: id, - screenId: formData?.screenId, - recordId: recordId, - }); - - try { - localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); - } catch (e) { - console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); - } - } - return true; - } - } catch (error) { - console.error("ํŒŒ์ผ ์กฐํšŒ ์˜ค๋ฅ˜:", error); - } - return false; - }, [id, tableName, columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, isDesignMode]); - - // ํŒŒ์ผ ๋™๊ธฐํ™” - useEffect(() => { - loadComponentFiles(); - }, [loadComponentFiles]); - - // ์ „์—ญ ์ƒํƒœ ๋ณ€๊ฒฝ ๊ฐ์ง€ - useEffect(() => { - const handleGlobalFileStateChange = (event: CustomEvent) => { - const { componentId, files, isRestore } = event.detail; - - if (componentId === id) { - setUploadedFiles(files); - - try { - const backupKey = getUniqueKey(); - localStorage.setItem(backupKey, JSON.stringify(files)); - } catch (e) { - console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); - } - } - }; - - if (typeof window !== "undefined") { - window.addEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); - return () => { - window.removeEventListener("globalFileStateChanged", handleGlobalFileStateChange as EventListener); - }; - } - }, [id, getUniqueKey]); - - // ํŒŒ์ผ ์—…๋กœ๋“œ ์ฒ˜๋ฆฌ - const handleFileUpload = useCallback( - async (files: File[]) => { - if (!files.length) return; - - // ์ค‘๋ณต ์ฒดํฌ - const existingFileNames = uploadedFiles.map((f) => f.realFileName.toLowerCase()); - const duplicates: string[] = []; - const uniqueFiles: File[] = []; - - files.forEach((file) => { - const fileName = file.name.toLowerCase(); - if (existingFileNames.includes(fileName)) { - duplicates.push(file.name); - } else { - uniqueFiles.push(file); - } - }); - - if (duplicates.length > 0) { - toast.error(`์ค‘๋ณต๋œ ํŒŒ์ผ: ${duplicates.join(", ")}`); - if (uniqueFiles.length === 0) return; - toast.info(`${uniqueFiles.length}๊ฐœ์˜ ์ƒˆ๋กœ์šด ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.`); - } - - const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files; - setUploadStatus("uploading"); - toast.loading("ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘...", { id: "file-upload" }); - - try { - const effectiveTableName = recordTableName || formData?.tableName || tableName || "default_table"; - const effectiveRecordId = recordId || formData?.id; - - let screenId = formData?.screenId; - if (!screenId && typeof window !== "undefined") { - const pathname = window.location.pathname; - const screenMatch = pathname.match(/\/screens\/(\d+)/); - if (screenMatch) { - screenId = parseInt(screenMatch[1]); - } - } - - let targetObjid; - const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_')); - - if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { - targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; - } else if (screenId) { - targetObjid = `screen_files:${screenId}:${id}:${effectiveColumnName}`; - } else { - targetObjid = `temp_${id}`; - } - - const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; - - const finalLinkedTable = effectiveIsRecordMode - ? effectiveTableName - : (formData?.linkedTable || effectiveTableName); - - const uploadData = { - autoLink: formData?.autoLink || true, - linkedTable: finalLinkedTable, - recordId: effectiveRecordId || `temp_${id}`, - columnName: effectiveColumnName, - isVirtualFileColumn: formData?.isVirtualFileColumn || true, - docType: config?.docType || "DOCUMENT", - docTypeName: config?.docTypeName || "์ผ๋ฐ˜ ๋ฌธ์„œ", - companyCode: userCompanyCode, - tableName: effectiveTableName, - fieldName: effectiveColumnName, - targetObjid: targetObjid, - isRecordMode: effectiveIsRecordMode, - }; - - const response = await uploadFiles({ - files: filesToUpload, - ...uploadData, - }); - - if (response.success) { - const fileData = response.files || (response as any).data || []; - - if (fileData.length === 0) { - throw new Error("์—…๋กœ๋“œ๋œ ํŒŒ์ผ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); - } - - const newFiles = fileData.map((file: any) => ({ - objid: file.objid || file.id, - savedFileName: file.saved_file_name || file.savedFileName, - realFileName: file.real_file_name || file.realFileName || file.name, - fileSize: file.file_size || file.fileSize || file.size, - fileExt: file.file_ext || file.fileExt || file.extension, - filePath: file.file_path || file.filePath || file.path, - docType: file.doc_type || file.docType, - docTypeName: file.doc_type_name || file.docTypeName, - targetObjid: file.target_objid || file.targetObjid, - parentTargetObjid: file.parent_target_objid || file.parentTargetObjid, - companyCode: file.company_code || file.companyCode, - writer: file.writer, - regdate: file.regdate, - status: file.status || "ACTIVE", - uploadedAt: new Date().toISOString(), - ...file, - })); - - const updatedFiles = [...uploadedFiles, ...newFiles]; - setUploadedFiles(updatedFiles); - setUploadStatus("success"); - - // localStorage ๋ฐฑ์—… - try { - const backupKey = getUniqueKey(); - localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); - } catch (e) { - console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); - } - - // ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ - if (typeof window !== "undefined") { - const globalFileState = (window as any).globalFileState || {}; - const uniqueKey = getUniqueKey(); - globalFileState[uniqueKey] = updatedFiles; - (window as any).globalFileState = globalFileState; - - GlobalFileManager.registerFiles(newFiles, { - uploadPage: window.location.pathname, - componentId: id, - screenId: formData?.screenId, - recordId: recordId, - }); - - const syncEvent = new CustomEvent("globalFileStateChanged", { - detail: { - componentId: id, - uniqueKey: uniqueKey, - recordId: recordId, - files: updatedFiles, - fileCount: updatedFiles.length, - timestamp: Date.now(), - }, - }); - window.dispatchEvent(syncEvent); - } - - // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ - if (onUpdate) { - onUpdate({ - uploadedFiles: updatedFiles, - lastFileUpdate: Date.now(), - }); - } - - // onChange ์ฝœ๋ฐฑ (objid ๋ฐฐ์—ด ๋˜๋Š” ๋‹จ์ผ ๊ฐ’) - const fileIds = updatedFiles.map((f) => f.objid); - const finalValue = config.multiple ? fileIds : fileIds[0] || ""; - const targetColumn = columnName || effectiveColumnName; - - console.log("๐Ÿ“ค [V2Media] ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ - ๊ฐ’ ์ „๋‹ฌ:", { - columnName: targetColumn, - fileIds, - finalValue, - hasOnChange: !!onChange, - hasOnFormDataChange: !!onFormDataChange, - }); - - if (onChange) { - onChange(finalValue); - } - - // ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ๋งž๊ฒŒ (fieldName, value) ํ˜•์‹ - if (onFormDataChange && targetColumn) { - // ๐Ÿ”‘ ๋‹จ์ผ ํŒŒ์ผ: ์ฒซ ๋ฒˆ์งธ objid๋งŒ ์ „๋‹ฌ (DB ์ปฌ๋Ÿผ์— ์ €์žฅ๋  ๊ฐ’) - // ๋ณต์ˆ˜ ํŒŒ์ผ: ์ฝค๋งˆ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ์ „๋‹ฌ - const formValue = config.multiple - ? fileIds.join(',') - : (fileIds[0] || ''); - - console.log("๐Ÿ“ [V2Media] formData ์—…๋ฐ์ดํŠธ:", { - columnName: targetColumn, - fileIds, - formValue, - isMultiple: config.multiple, - isRecordMode: effectiveIsRecordMode, - }); - // (fieldName: string, value: any) ํ˜•์‹์œผ๋กœ ํ˜ธ์ถœ - onFormDataChange(targetColumn, formValue); - } - - // ๊ทธ๋ฆฌ๋“œ ํŒŒ์ผ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ - if (typeof window !== "undefined") { - const refreshEvent = new CustomEvent("refreshFileStatus", { - detail: { - tableName: effectiveTableName, - recordId: effectiveRecordId, - columnName: targetColumn, - targetObjid: targetObjid, - fileCount: updatedFiles.length, - }, - }); - window.dispatchEvent(refreshEvent); - } - - toast.dismiss("file-upload"); - toast.success(`${newFiles.length}๊ฐœ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ`); - } else { - throw new Error(response.message || (response as any).error || "ํŒŒ์ผ ์—…๋กœ๋“œ ์‹คํŒจ"); - } - } catch (error) { - console.error("ํŒŒ์ผ ์—…๋กœ๋“œ ์˜ค๋ฅ˜:", error); - setUploadStatus("error"); - toast.dismiss("file-upload"); - toast.error(`์—…๋กœ๋“œ ์˜ค๋ฅ˜: ${error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); - } - }, - [config, uploadedFiles, onChange, id, getUniqueKey, recordId, isRecordMode, recordTableName, effectiveColumnName, tableName, onUpdate, onFormDataChange, user, columnName], - ); - - // ํŒŒ์ผ ๋ทฐ์–ด ์—ด๊ธฐ/๋‹ซ๊ธฐ - const handleFileView = useCallback((file: FileInfo) => { - setViewerFile(file); - setIsViewerOpen(true); - }, []); - - const handleViewerClose = useCallback(() => { - setIsViewerOpen(false); - setViewerFile(null); - }, []); - - // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ - const handleFileDownload = useCallback(async (file: FileInfo) => { - try { - await downloadFile({ - fileId: file.objid, - serverFilename: file.savedFileName, - originalName: file.realFileName, - }); - toast.success(`${file.realFileName} ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ`); - } catch (error) { - console.error("ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์˜ค๋ฅ˜:", error); - toast.error("ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ"); - } - }, []); - - // ํŒŒ์ผ ์‚ญ์ œ - const handleFileDelete = useCallback( - async (file: FileInfo | string) => { - try { - const fileId = typeof file === "string" ? file : file.objid; - const fileName = typeof file === "string" ? "ํŒŒ์ผ" : file.realFileName; - const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName; - - await deleteFile(fileId, serverFilename); - - const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); + const updatedFiles = [...uploadedFiles, ...newFiles]; setUploadedFiles(updatedFiles); + setUploadStatus("success"); // localStorage ๋ฐฑ์—… try { @@ -572,6 +452,13 @@ export const V2Media = forwardRef( globalFileState[uniqueKey] = updatedFiles; (window as any).globalFileState = globalFileState; + GlobalFileManager.registerFiles(newFiles, { + uploadPage: window.location.pathname, + componentId: id, + screenId: formData?.screenId, + recordId: recordId, + }); + const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: id, @@ -580,12 +467,12 @@ export const V2Media = forwardRef( files: updatedFiles, fileCount: updatedFiles.length, timestamp: Date.now(), - action: "delete", }, }); window.dispatchEvent(syncEvent); } + // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ if (onUpdate) { onUpdate({ uploadedFiles: updatedFiles, @@ -593,15 +480,17 @@ export const V2Media = forwardRef( }); } - // onChange ์ฝœ๋ฐฑ + // onChange ์ฝœ๋ฐฑ (objid ๋ฐฐ์—ด ๋˜๋Š” ๋‹จ์ผ ๊ฐ’) const fileIds = updatedFiles.map((f) => f.objid); const finalValue = config.multiple ? fileIds : fileIds[0] || ""; const targetColumn = columnName || effectiveColumnName; - console.log("๐Ÿ—‘๏ธ [V2Media] ํŒŒ์ผ ์‚ญ์ œ ์™„๋ฃŒ - ๊ฐ’ ์ „๋‹ฌ:", { + console.log("๐Ÿ“ค [V2Media] ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ - ๊ฐ’ ์ „๋‹ฌ:", { columnName: targetColumn, fileIds, finalValue, + hasOnChange: !!onChange, + hasOnFormDataChange: !!onFormDataChange, }); if (onChange) { @@ -612,120 +501,286 @@ export const V2Media = forwardRef( if (onFormDataChange && targetColumn) { // ๐Ÿ”‘ ๋‹จ์ผ ํŒŒ์ผ: ์ฒซ ๋ฒˆ์งธ objid๋งŒ ์ „๋‹ฌ (DB ์ปฌ๋Ÿผ์— ์ €์žฅ๋  ๊ฐ’) // ๋ณต์ˆ˜ ํŒŒ์ผ: ์ฝค๋งˆ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ์ „๋‹ฌ - const formValue = config.multiple - ? fileIds.join(',') - : (fileIds[0] || ''); - - console.log("๐Ÿ—‘๏ธ [V2Media] ์‚ญ์ œ ํ›„ formData ์—…๋ฐ์ดํŠธ:", { + const formValue = config.multiple ? fileIds.join(",") : fileIds[0] || ""; + + console.log("๐Ÿ“ [V2Media] formData ์—…๋ฐ์ดํŠธ:", { columnName: targetColumn, fileIds, formValue, + isMultiple: config.multiple, + isRecordMode: effectiveIsRecordMode, }); // (fieldName: string, value: any) ํ˜•์‹์œผ๋กœ ํ˜ธ์ถœ onFormDataChange(targetColumn, formValue); } - toast.success(`${fileName} ์‚ญ์ œ ์™„๋ฃŒ`); - } catch (error) { - console.error("ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); - toast.error("ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ"); - } - }, - [uploadedFiles, onUpdate, id, isRecordMode, onFormDataChange, recordTableName, recordId, effectiveColumnName, getUniqueKey, onChange, config.multiple, columnName], - ); - - // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ - const loadRepresentativeImage = useCallback( - async (file: FileInfo) => { - try { - const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes( - file.fileExt.toLowerCase().replace(".", "") - ); - - if (!isImage) { - setRepresentativeImageUrl(null); - return; + // ๊ทธ๋ฆฌ๋“œ ํŒŒ์ผ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + if (typeof window !== "undefined") { + const refreshEvent = new CustomEvent("refreshFileStatus", { + detail: { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: targetColumn, + targetObjid: targetObjid, + fileCount: updatedFiles.length, + }, + }); + window.dispatchEvent(refreshEvent); } - if (!file.objid || file.objid === "0" || file.objid === "") { - setRepresentativeImageUrl(null); - return; - } - - const response = await apiClient.get(`/files/download/${file.objid}`, { - params: { serverFilename: file.savedFileName }, - responseType: "blob", - }); - - const blob = new Blob([response.data]); - const url = window.URL.createObjectURL(blob); - - if (representativeImageUrl) { - window.URL.revokeObjectURL(representativeImageUrl); - } - - setRepresentativeImageUrl(url); - } catch (error) { - console.error("๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:", error); - setRepresentativeImageUrl(null); + toast.dismiss("file-upload"); + toast.success(`${newFiles.length}๊ฐœ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ`); + } else { + throw new Error(response.message || (response as any).error || "ํŒŒ์ผ ์—…๋กœ๋“œ ์‹คํŒจ"); } - }, - [representativeImageUrl], - ); - - // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์„ค์ • - const handleSetRepresentative = useCallback( - async (file: FileInfo) => { - try { - const { setRepresentativeFile } = await import("@/lib/api/file"); - await setRepresentativeFile(file.objid); - - const updatedFiles = uploadedFiles.map((f) => ({ - ...f, - isRepresentative: f.objid === file.objid, - })); - - setUploadedFiles(updatedFiles); - loadRepresentativeImage(file); - } catch (e) { - console.error("๋Œ€ํ‘œ ํŒŒ์ผ ์„ค์ • ์‹คํŒจ:", e); - } - }, - [uploadedFiles, loadRepresentativeImage] - ); - - // uploadedFiles ๋ณ€๊ฒฝ ์‹œ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ - useEffect(() => { - const representativeFile = uploadedFiles.find(f => f.isRepresentative) || uploadedFiles[0]; - if (representativeFile) { - loadRepresentativeImage(representativeFile); - } else { - setRepresentativeImageUrl(null); + } catch (error) { + console.error("ํŒŒ์ผ ์—…๋กœ๋“œ ์˜ค๋ฅ˜:", error); + setUploadStatus("error"); + toast.dismiss("file-upload"); + toast.error(`์—…๋กœ๋“œ ์˜ค๋ฅ˜: ${error instanceof Error ? error.message : "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); } + }, + [ + config, + uploadedFiles, + onChange, + id, + getUniqueKey, + recordId, + isRecordMode, + recordTableName, + effectiveColumnName, + tableName, + onUpdate, + onFormDataChange, + user, + columnName, + ], + ); + + // ํŒŒ์ผ ๋ทฐ์–ด ์—ด๊ธฐ/๋‹ซ๊ธฐ + const handleFileView = useCallback((file: FileInfo) => { + setViewerFile(file); + setIsViewerOpen(true); + }, []); + + const handleViewerClose = useCallback(() => { + setIsViewerOpen(false); + setViewerFile(null); + }, []); + + // ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ + const handleFileDownload = useCallback(async (file: FileInfo) => { + try { + await downloadFile({ + fileId: file.objid, + serverFilename: file.savedFileName, + originalName: file.realFileName, + }); + toast.success(`${file.realFileName} ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ`); + } catch (error) { + console.error("ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์˜ค๋ฅ˜:", error); + toast.error("ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์‹คํŒจ"); + } + }, []); + + // ํŒŒ์ผ ์‚ญ์ œ + const handleFileDelete = useCallback( + async (file: FileInfo | string) => { + try { + const fileId = typeof file === "string" ? file : file.objid; + const fileName = typeof file === "string" ? "ํŒŒ์ผ" : file.realFileName; + const serverFilename = typeof file === "string" ? "temp_file" : file.savedFileName; + + await deleteFile(fileId, serverFilename); + + const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); + setUploadedFiles(updatedFiles); + + // localStorage ๋ฐฑ์—… + try { + const backupKey = getUniqueKey(); + localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); + } catch (e) { + console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); + } + + // ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + if (typeof window !== "undefined") { + const globalFileState = (window as any).globalFileState || {}; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; + (window as any).globalFileState = globalFileState; + + const syncEvent = new CustomEvent("globalFileStateChanged", { + detail: { + componentId: id, + uniqueKey: uniqueKey, + recordId: recordId, + files: updatedFiles, + fileCount: updatedFiles.length, + timestamp: Date.now(), + action: "delete", + }, + }); + window.dispatchEvent(syncEvent); + } + + if (onUpdate) { + onUpdate({ + uploadedFiles: updatedFiles, + lastFileUpdate: Date.now(), + }); + } + + // onChange ์ฝœ๋ฐฑ + const fileIds = updatedFiles.map((f) => f.objid); + const finalValue = config.multiple ? fileIds : fileIds[0] || ""; + const targetColumn = columnName || effectiveColumnName; + + console.log("๐Ÿ—‘๏ธ [V2Media] ํŒŒ์ผ ์‚ญ์ œ ์™„๋ฃŒ - ๊ฐ’ ์ „๋‹ฌ:", { + columnName: targetColumn, + fileIds, + finalValue, + }); + + if (onChange) { + onChange(finalValue); + } + + // ํผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ - ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ ์‹œ๊ทธ๋‹ˆ์ฒ˜์— ๋งž๊ฒŒ (fieldName, value) ํ˜•์‹ + if (onFormDataChange && targetColumn) { + // ๐Ÿ”‘ ๋‹จ์ผ ํŒŒ์ผ: ์ฒซ ๋ฒˆ์งธ objid๋งŒ ์ „๋‹ฌ (DB ์ปฌ๋Ÿผ์— ์ €์žฅ๋  ๊ฐ’) + // ๋ณต์ˆ˜ ํŒŒ์ผ: ์ฝค๋งˆ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ์ „๋‹ฌ + const formValue = config.multiple ? fileIds.join(",") : fileIds[0] || ""; + + console.log("๐Ÿ—‘๏ธ [V2Media] ์‚ญ์ œ ํ›„ formData ์—…๋ฐ์ดํŠธ:", { + columnName: targetColumn, + fileIds, + formValue, + }); + // (fieldName: string, value: any) ํ˜•์‹์œผ๋กœ ํ˜ธ์ถœ + onFormDataChange(targetColumn, formValue); + } + + toast.success(`${fileName} ์‚ญ์ œ ์™„๋ฃŒ`); + } catch (error) { + console.error("ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); + toast.error("ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ"); + } + }, + [ + uploadedFiles, + onUpdate, + id, + isRecordMode, + onFormDataChange, + recordTableName, + recordId, + effectiveColumnName, + getUniqueKey, + onChange, + config.multiple, + columnName, + ], + ); + + // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ + const loadRepresentativeImage = useCallback( + async (file: FileInfo) => { + try { + const isImage = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes( + file.fileExt.toLowerCase().replace(".", ""), + ); + + if (!isImage) { + setRepresentativeImageUrl(null); + return; + } + + if (!file.objid || file.objid === "0" || file.objid === "") { + setRepresentativeImageUrl(null); + return; + } + + const response = await apiClient.get(`/files/download/${file.objid}`, { + params: { serverFilename: file.savedFileName }, + responseType: "blob", + }); + + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); - return () => { if (representativeImageUrl) { window.URL.revokeObjectURL(representativeImageUrl); } - }; - }, [uploadedFiles]); - // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํ•ธ๋“ค๋Ÿฌ - const handleDragOver = useCallback((e: React.DragEvent) => { + setRepresentativeImageUrl(url); + } catch (error) { + console.error("๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:", error); + setRepresentativeImageUrl(null); + } + }, + [representativeImageUrl], + ); + + // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ์„ค์ • + const handleSetRepresentative = useCallback( + async (file: FileInfo) => { + try { + const { setRepresentativeFile } = await import("@/lib/api/file"); + await setRepresentativeFile(file.objid); + + const updatedFiles = uploadedFiles.map((f) => ({ + ...f, + isRepresentative: f.objid === file.objid, + })); + + setUploadedFiles(updatedFiles); + loadRepresentativeImage(file); + } catch (e) { + console.error("๋Œ€ํ‘œ ํŒŒ์ผ ์„ค์ • ์‹คํŒจ:", e); + } + }, + [uploadedFiles, loadRepresentativeImage], + ); + + // uploadedFiles ๋ณ€๊ฒฝ ์‹œ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ + useEffect(() => { + const representativeFile = uploadedFiles.find((f) => f.isRepresentative) || uploadedFiles[0]; + if (representativeFile) { + loadRepresentativeImage(representativeFile); + } else { + setRepresentativeImageUrl(null); + } + + return () => { + if (representativeImageUrl) { + window.URL.revokeObjectURL(representativeImageUrl); + } + }; + }, [uploadedFiles]); + + // ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ํ•ธ๋“ค๋Ÿฌ + const handleDragOver = useCallback( + (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!readonly && !disabled) { setDragOver(true); } - }, [readonly, disabled]); + }, + [readonly, disabled], + ); - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setDragOver(false); - }, []); + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragOver(false); + }, []); - const handleDrop = useCallback((e: React.DragEvent) => { + const handleDrop = useCallback( + (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragOver(false); @@ -736,93 +791,93 @@ export const V2Media = forwardRef( handleFileUpload(files); } } - }, [readonly, disabled, handleFileUpload]); + }, + [readonly, disabled, handleFileUpload], + ); - // ํŒŒ์ผ ์„ ํƒ - const handleFileSelect = useCallback(() => { - if (fileInputRef.current) { - fileInputRef.current.click(); - } - }, []); + // ํŒŒ์ผ ์„ ํƒ + const handleFileSelect = useCallback(() => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }, []); - const handleInputChange = useCallback((e: React.ChangeEvent) => { + const handleInputChange = useCallback( + (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { handleFileUpload(files); } - e.target.value = ''; - }, [handleFileUpload]); + e.target.value = ""; + }, + [handleFileUpload], + ); - // ํŒŒ์ผ ์„ค์ • - const fileConfig: FileUploadConfig = { - accept: config.accept || "*/*", - multiple: config.multiple || false, - maxSize: config.maxSize || 10 * 1024 * 1024, - disabled: disabled, - readonly: readonly, - }; + // ํŒŒ์ผ ์„ค์ • + const fileConfig: FileUploadConfig = { + accept: config.accept || "*/*", + multiple: config.multiple || false, + maxSize: config.maxSize || 10 * 1024 * 1024, + disabled: disabled, + readonly: readonly, + }; - const showLabel = label && style?.labelDisplay !== false; - const componentWidth = size?.width || style?.width; - const componentHeight = size?.height || style?.height; + const showLabel = label && style?.labelDisplay !== false; + const componentWidth = size?.width || style?.width; + const componentHeight = size?.height || style?.height; - return ( -
- {/* ๋ผ๋ฒจ */} - {showLabel && ( - - )} - - {/* ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ */} -
+ {/* ๋ผ๋ฒจ */} + {showLabel && ( +