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/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 75937daa..8dfe9ae4 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -5556,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(); @@ -7419,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/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 2029f473..4fd27cb0 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -189,13 +189,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; }} > @@ -208,7 +206,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/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-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 bb9306c8..f56b0fb3 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 = {}; @@ -1534,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], ); // ์ขŒ์ธก ํ•ญ๋ชฉ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ (๋™์ผ ํ•ญ๋ชฉ ์žฌํด๋ฆญ ์‹œ ์„ ํƒ ํ•ด์ œ โ†’ ์ „์ฒด ๋ฐ์ดํ„ฐ ํ‘œ์‹œ) @@ -1562,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; } @@ -2781,14 +2788,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); - // ์ถ”๊ฐ€ ํƒญ๋„ ์ „์ฒด ๋ฐ์ดํ„ฐ ๋กœ๋“œ + } + // ์ถ”๊ฐ€ ํƒญ: ๋ฉ”์ธ ํŒจ๋„์ด "detail"(์„ ํƒ ์‹œ ํ‘œ์‹œ)์ด๋ฉด ์ถ”๊ฐ€ ํƒญ๋„ ์ดˆ๊ธฐ ๋กœ๋“œํ•˜์ง€ ์•Š์Œ + if (relationshipType !== "detail") { const tabs = componentConfig.rightPanel?.additionalTabs; if (tabs && tabs.length > 0) { - tabs.forEach((_: any, idx: number) => { - loadTabData(idx + 1, null); + tabs.forEach((tab: any, idx: number) => { + const tabRelType = tab.relation?.type || "join"; + if (tabRelType === "join") { + loadTabData(idx + 1, null); + } }); } } @@ -3738,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 (
@@ -4645,7 +4669,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})} + + ))} + + + + +
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 ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ์šฉ } // ๊ธฐ๋ณธ ์„ค์ •๊ฐ’