diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index e275d825..51ab567a 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 ac2377fe..e1242afd 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/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/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/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/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 05d8bdc9..252f5c2b 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 ( @@ -2485,7 +2494,13 @@ export const InteractiveScreenViewer: React.FC = ( setPopupScreen(null); setPopupFormData({}); // ํŒ์—… ๋‹ซ์„ ๋•Œ formData๋„ ์ดˆ๊ธฐํ™” }}> - + {popupScreen?.title || "์ƒ์„ธ ์ •๋ณด"} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 76bd8973..8dfe9ae4 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); @@ -5555,8 +5556,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 +7423,7 @@ export default function ScreenDesigner({

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

โš ๏ธ 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/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/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 && ( +