diff --git a/backend-node/src/controllers/DashboardController.ts b/backend-node/src/controllers/DashboardController.ts index 0f6f07cc..7d710110 100644 --- a/backend-node/src/controllers/DashboardController.ts +++ b/backend-node/src/controllers/DashboardController.ts @@ -515,6 +515,7 @@ export class DashboardController { }); // ์™ธ๋ถ€ API ํ˜ธ์ถœ + // @ts-ignore - node-fetch dynamic import const fetch = (await import("node-fetch")).default; const response = await fetch(urlObj.toString(), { method: method.toUpperCase(), diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index d228f80d..331f980e 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -295,6 +295,54 @@ export class DynamicFormService { } }); + // ๐Ÿ“ RepeaterInput ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ (JSON ๋ฐฐ์—ด์„ ๊ฐœ๋ณ„ ๋ ˆ์ฝ”๋“œ๋กœ ๋ถ„ํ•ด) + const repeaterData: Array<{ + data: Record[]; + targetTable?: string; + componentId: string; + }> = []; + Object.keys(dataToInsert).forEach((key) => { + const value = dataToInsert[key]; + + // RepeaterInput ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (JSON ๋ฐฐ์—ด ๋ฌธ์ž์—ด) + if ( + typeof value === "string" && + value.trim().startsWith("[") && + value.trim().endsWith("]") + ) { + try { + const parsedArray = JSON.parse(value); + if (Array.isArray(parsedArray) && parsedArray.length > 0) { + console.log( + `๐Ÿ”„ RepeaterInput ๋ฐ์ดํ„ฐ ๊ฐ์ง€: ${key}, ${parsedArray.length}๊ฐœ ํ•ญ๋ชฉ` + ); + + // ์ปดํฌ๋„ŒํŠธ ์„ค์ •์—์„œ targetTable ์ถ”์ถœ (์ปดํฌ๋„ŒํŠธ ID๋ฅผ ํ†ตํ•ด) + // ํ”„๋ก ํŠธ์—”๋“œ์—์„œ { data: [...], targetTable: "..." } ํ˜•์‹์œผ๋กœ ์ „๋‹ฌ๋  ์ˆ˜ ์žˆ์Œ + let targetTable: string | undefined; + let actualData = parsedArray; + + // ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ์— _targetTable์ด ์žˆ๋Š”์ง€ ํ™•์ธ (ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ „๋‹ฌ) + if (parsedArray[0] && parsedArray[0]._targetTable) { + targetTable = parsedArray[0]._targetTable; + actualData = parsedArray.map( + ({ _targetTable, ...item }) => item + ); + } + + repeaterData.push({ + data: actualData, + targetTable, + componentId: key, + }); + delete dataToInsert[key]; // ์›๋ณธ ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋Š” ์ œ๊ฑฐ + } + } catch (parseError) { + console.log(`โš ๏ธ JSON ํŒŒ์‹ฑ ์‹คํŒจ: ${key}`); + } + } + }); + // ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ ์ œ๊ฑฐ Object.keys(dataToInsert).forEach((key) => { if (!tableColumns.includes(key)) { @@ -305,6 +353,9 @@ export class DynamicFormService { } }); + // RepeaterInput ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ๋กœ์ง์€ ๋ฉ”์ธ ์ €์žฅ ํ›„์— ์ฒ˜๋ฆฌ + // (๊ฐ Repeater๊ฐ€ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์ €์žฅ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ) + console.log("๐ŸŽฏ ์‹ค์ œ ํ…Œ์ด๋ธ”์— ์‚ฝ์ž…ํ•  ๋ฐ์ดํ„ฐ:", { tableName, dataToInsert, @@ -388,6 +439,111 @@ export class DynamicFormService { // ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์ค€ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ const insertedRecord = Array.isArray(result) ? result[0] : result; + // ๐Ÿ“ RepeaterInput ๋ฐ์ดํ„ฐ ์ €์žฅ (๊ฐ Repeater๋ฅผ ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ์ €์žฅ) + if (repeaterData.length > 0) { + console.log( + `๐Ÿ”„ RepeaterInput ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹œ์ž‘: ${repeaterData.length}๊ฐœ Repeater` + ); + + for (const repeater of repeaterData) { + const targetTableName = repeater.targetTable || tableName; + console.log( + `๐Ÿ“ Repeater "${repeater.componentId}" โ†’ ํ…Œ์ด๋ธ” "${targetTableName}"์— ${repeater.data.length}๊ฐœ ํ•ญ๋ชฉ ์ €์žฅ` + ); + + // ๋Œ€์ƒ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋ฐ ๊ธฐ๋ณธํ‚ค ์ •๋ณด ์กฐํšŒ + const targetTableColumns = + await this.getTableColumns(targetTableName); + const targetPrimaryKeys = await this.getPrimaryKeys(targetTableName); + + // ์ปฌ๋Ÿผ๋ช…๋งŒ ์ถ”์ถœ + const targetColumnNames = targetTableColumns.map( + (col) => col.columnName + ); + + // ๊ฐ ํ•ญ๋ชฉ์„ ์ €์žฅ + for (let i = 0; i < repeater.data.length; i++) { + const item = repeater.data[i]; + const itemData: Record = { + ...item, + created_by, + updated_by, + regdate: new Date(), + }; + + // ๋Œ€์ƒ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ ํ•„ํ„ฐ๋ง + Object.keys(itemData).forEach((key) => { + if (!targetColumnNames.includes(key)) { + delete itemData[key]; + } + }); + + // ํƒ€์ž… ๋ณ€ํ™˜ ์ ์šฉ + Object.keys(itemData).forEach((columnName) => { + const column = targetTableColumns.find( + (col) => col.columnName === columnName + ); + if (column) { + itemData[columnName] = this.convertValueForPostgreSQL( + itemData[columnName], + column.dataType + ); + } + }); + + // UPSERT ์ฟผ๋ฆฌ ์ƒ์„ฑ + const itemColumns = Object.keys(itemData); + const itemValues: any[] = Object.values(itemData); + const itemPlaceholders = itemValues + .map((_, index) => `$${index + 1}`) + .join(", "); + + let itemUpsertQuery: string; + if (targetPrimaryKeys.length > 0) { + const conflictColumns = targetPrimaryKeys.join(", "); + const updateSet = itemColumns + .filter((col) => !targetPrimaryKeys.includes(col)) + .map((col) => `${col} = EXCLUDED.${col}`) + .join(", "); + + if (updateSet) { + itemUpsertQuery = ` + INSERT INTO ${targetTableName} (${itemColumns.join(", ")}) + VALUES (${itemPlaceholders}) + ON CONFLICT (${conflictColumns}) + DO UPDATE SET ${updateSet} + RETURNING * + `; + } else { + itemUpsertQuery = ` + INSERT INTO ${targetTableName} (${itemColumns.join(", ")}) + VALUES (${itemPlaceholders}) + ON CONFLICT (${conflictColumns}) + DO NOTHING + RETURNING * + `; + } + } else { + itemUpsertQuery = ` + INSERT INTO ${targetTableName} (${itemColumns.join(", ")}) + VALUES (${itemPlaceholders}) + RETURNING * + `; + } + + console.log( + ` ๐Ÿ“ ํ•ญ๋ชฉ ${i + 1}/${repeater.data.length} ์ €์žฅ:`, + itemData + ); + await query(itemUpsertQuery, itemValues); + } + + console.log(` โœ… Repeater "${repeater.componentId}" ์ €์žฅ ์™„๋ฃŒ`); + } + + console.log(`โœ… ๋ชจ๋“  RepeaterInput ๋ฐ์ดํ„ฐ ์ €์žฅ ์™„๋ฃŒ`); + } + // ๐Ÿ”ฅ ์กฐ๊ฑด๋ถ€ ์—ฐ๊ฒฐ ์‹คํ–‰ (INSERT ํŠธ๋ฆฌ๊ฑฐ) try { if (company_code) { @@ -1114,6 +1270,31 @@ export class DynamicFormService { } } + /** + * ํ…Œ์ด๋ธ”์˜ ๊ธฐ๋ณธํ‚ค ์ปฌ๋Ÿผ๋ช… ๋ชฉ๋ก ์กฐํšŒ + */ + async getPrimaryKeys(tableName: string): Promise { + try { + console.log("๐Ÿ”‘ ์„œ๋น„์Šค: ํ…Œ์ด๋ธ” ๊ธฐ๋ณธํ‚ค ์กฐํšŒ ์‹œ์ž‘:", { tableName }); + + const result = await query<{ column_name: string }>( + `SELECT a.attname AS column_name + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = $1::regclass AND i.indisprimary`, + [tableName] + ); + + const primaryKeys = result.map((row) => row.column_name); + console.log("โœ… ์„œ๋น„์Šค: ํ…Œ์ด๋ธ” ๊ธฐ๋ณธํ‚ค ์กฐํšŒ ์„ฑ๊ณต:", primaryKeys); + + return primaryKeys; + } catch (error) { + console.error("โŒ ์„œ๋น„์Šค: ํ…Œ์ด๋ธ” ๊ธฐ๋ณธํ‚ค ์กฐํšŒ ์‹คํŒจ:", error); + throw new Error(`ํ…Œ์ด๋ธ” ๊ธฐ๋ณธํ‚ค ์กฐํšŒ ์‹คํŒจ: ${error}`); + } + } + /** * ์ œ์–ด๊ด€๋ฆฌ ์‹คํ–‰ (ํ™”๋ฉด์— ์„ค์ •๋œ ๊ฒฝ์šฐ) */ diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 095ac938..df2823c8 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -219,7 +219,11 @@ export class EntityJoinService { ]; const separator = config.separator || " - "; - if (displayColumns.length === 1) { + if (displayColumns.length === 0 || !displayColumns[0]) { + // displayColumns๊ฐ€ ๋นˆ ๋ฐฐ์—ด์ด๊ฑฐ๋‚˜ ์ฒซ ๋ฒˆ์งธ ๊ฐ’์ด null/undefined์ธ ๊ฒฝ์šฐ + // ์กฐ์ธ ํ…Œ์ด๋ธ”์˜ referenceColumn์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‚ฌ์šฉ + return `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.aliasColumn}`; + } else if (displayColumns.length === 1) { // ๋‹จ์ผ ์ปฌ๋Ÿผ์ธ ๊ฒฝ์šฐ const col = displayColumns[0]; const isJoinTableColumn = [ diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index c09ab497..f91831d5 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -22,7 +22,7 @@ export default function ScreenViewPage() { const [layout, setLayout] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [formData, setFormData] = useState>({}); + const [formData, setFormData] = useState>({}); // ํ™”๋ฉด ๋„ˆ๋น„์— ๋”ฐ๋ผ Y์ขŒํ‘œ ์œ ์ง€ ์—ฌ๋ถ€ ๊ฒฐ์ • const [preserveYPosition, setPreserveYPosition] = useState(true); @@ -34,7 +34,7 @@ export default function ScreenViewPage() { const [editModalConfig, setEditModalConfig] = useState<{ screenId?: number; modalSize?: "sm" | "md" | "lg" | "xl" | "full"; - editData?: any; + editData?: Record; onSave?: () => void; modalTitle?: string; modalDescription?: string; @@ -70,11 +70,11 @@ export default function ScreenViewPage() { setEditModalOpen(true); }; - // @ts-ignore + // @ts-expect-error - CustomEvent type window.addEventListener("openEditModal", handleOpenEditModal); return () => { - // @ts-ignore + // @ts-expect-error - CustomEvent type window.removeEventListener("openEditModal", handleOpenEditModal); }; }, []); @@ -96,8 +96,18 @@ export default function ScreenViewPage() { } catch (layoutError) { console.warn("๋ ˆ์ด์•„์›ƒ ๋กœ๋“œ ์‹คํŒจ, ๋นˆ ๋ ˆ์ด์•„์›ƒ ์‚ฌ์šฉ:", layoutError); setLayout({ + screenId, components: [], - gridSettings: { columns: 12, gap: 16, padding: 16 }, + gridSettings: { + columns: 12, + gap: 16, + padding: 16, + enabled: true, + size: 8, + color: "#e0e0e0", + opacity: 0.5, + snapToGrid: true, + }, }); } } catch (error) { @@ -174,6 +184,13 @@ export default function ScreenViewPage() { containerWidth={window.innerWidth} screenWidth={screenWidth} preserveYPosition={preserveYPosition} + isDesignMode={false} + formData={formData} + onFormDataChange={(fieldName: string, value: unknown) => { + console.log("๐Ÿ“ page.tsx formData ์—…๋ฐ์ดํŠธ:", fieldName, value); + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }} + screenInfo={{ id: screenId, tableName: screen?.tableName }} /> ) : ( // ๋นˆ ํ™”๋ฉด์ผ ๋•Œ diff --git a/frontend/components/screen/ResponsiveLayoutEngine.tsx b/frontend/components/screen/ResponsiveLayoutEngine.tsx index 6cddf2b6..d879574f 100644 --- a/frontend/components/screen/ResponsiveLayoutEngine.tsx +++ b/frontend/components/screen/ResponsiveLayoutEngine.tsx @@ -10,12 +10,15 @@ import { Breakpoint, BREAKPOINTS } from "@/types/responsive"; import { generateSmartDefaults, ensureResponsiveConfig } from "@/lib/utils/responsiveDefaults"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; -interface ResponsiveLayoutEngineProps { +export interface ResponsiveLayoutEngineProps { components: ComponentData[]; breakpoint: Breakpoint; containerWidth: number; screenWidth?: number; preserveYPosition?: boolean; // true: Y์ขŒํ‘œ ์œ ์ง€ (ํ•˜์ด๋ธŒ๋ฆฌ๋“œ), false: 16px ๊ฐ„๊ฒฉ (๋ฐ˜์‘ํ˜•) + formData?: Record; + onFormDataChange?: (fieldName: string, value: unknown) => void; + screenInfo?: { id: number; tableName?: string }; } /** @@ -33,6 +36,9 @@ export const ResponsiveLayoutEngine: React.FC = ({ containerWidth, screenWidth = 1920, preserveYPosition = false, // ๊ธฐ๋ณธ๊ฐ’: ๋ฐ˜์‘ํ˜• ๋ชจ๋“œ (16px ๊ฐ„๊ฒฉ) + formData, + onFormDataChange, + screenInfo, }) => { // 1๋‹จ๊ณ„: ์ปดํฌ๋„ŒํŠธ๋“ค์„ Y ์œ„์น˜ ๊ธฐ์ค€์œผ๋กœ ํ–‰(row)์œผ๋กœ ๊ทธ๋ฃนํ™” const rows = useMemo(() => { @@ -72,6 +78,18 @@ export const ResponsiveLayoutEngine: React.FC = ({ const responsiveComponents = useMemo(() => { const result = sortedRows.flatMap((row, rowIndex) => row.map((comp, compIndex) => { + // ์ปดํฌ๋„ŒํŠธ์— gridColumns๊ฐ€ ์ด๋ฏธ ์„ค์ •๋˜์–ด ์žˆ์œผ๋ฉด ๊ทธ ๊ฐ’ ์‚ฌ์šฉ + if ((comp as any).gridColumns !== undefined) { + return { + ...comp, + responsiveDisplay: { + gridColumns: (comp as any).gridColumns, + order: compIndex + 1, + hide: false, + }, + }; + } + // ๋ฐ˜์‘ํ˜• ์„ค์ •์ด ์—†์œผ๋ฉด ์ž๋™ ์ƒ์„ฑ const compWithConfig = ensureResponsiveConfig(comp, screenWidth); @@ -140,6 +158,7 @@ export const ResponsiveLayoutEngine: React.FC = ({ gap: "16px", padding: "0 16px", marginTop, + alignItems: "start", // ๊ฐ ์•„์ดํ…œ์ด ์›๋ž˜ ๋†’์ด ์œ ์ง€ }} > {rowComponents.map((comp) => ( @@ -148,9 +167,19 @@ export const ResponsiveLayoutEngine: React.FC = ({ className="responsive-grid-item" style={{ gridColumn: `span ${comp.responsiveDisplay?.gridColumns || gridColumns}`, + height: "auto", // ์ž๋™ ๋†’์ด }} > - + ))} diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 829b6cbe..7d4f6378 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -872,7 +872,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }, []); - // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ + // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ (์›๋ž˜๋Œ€๋กœ ๋ณต์›) useEffect(() => { const loadScreenTable = async () => { const tableName = selectedScreen?.tableName; @@ -915,7 +915,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD columns, }; - setTables([tableInfo]); + setTables([tableInfo]); // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋งŒ ์ €์žฅ (์›๋ž˜๋Œ€๋กœ) } catch (error) { console.error("ํ™”๋ฉด ํ…Œ์ด๋ธ” ์ •๋ณด ๋กœ๋“œ ์‹คํŒจ:", error); setTables([]); @@ -1996,7 +1996,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD "accordion-basic": 12, // ์•„์ฝ”๋””์–ธ (100%) "table-list": 12, // ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ (100%) "image-display": 4, // ์ด๋ฏธ์ง€ ํ‘œ์‹œ (33%) - "split-panel-layout": 12, // ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ (100%) + "split-panel-layout": 6, // ๋ถ„ํ•  ํŒจ๋„ ๋ ˆ์ด์•„์›ƒ (50%) // ์•ก์…˜ ์ปดํฌ๋„ŒํŠธ (ACTION ์นดํ…Œ๊ณ ๋ฆฌ) "button-basic": 1, // ๋ฒ„ํŠผ (8.33%) diff --git a/frontend/components/webtypes/RepeaterInput.tsx b/frontend/components/webtypes/RepeaterInput.tsx index 1c943d59..10be037d 100644 --- a/frontend/components/webtypes/RepeaterInput.tsx +++ b/frontend/components/webtypes/RepeaterInput.tsx @@ -95,7 +95,14 @@ export const RepeaterInput: React.FC = ({ } const newItems = [...items, createEmptyItem()]; setItems(newItems); - onChange?.(newItems); + console.log("โž• RepeaterInput ํ•ญ๋ชฉ ์ถ”๊ฐ€, onChange ํ˜ธ์ถœ:", newItems); + + // targetTable์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ๊ฐ ํ•ญ๋ชฉ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + const dataWithMeta = config.targetTable + ? newItems.map((item) => ({ ...item, _targetTable: config.targetTable })) + : newItems; + + onChange?.(dataWithMeta); }; // ํ•ญ๋ชฉ ์ œ๊ฑฐ @@ -105,7 +112,13 @@ export const RepeaterInput: React.FC = ({ } const newItems = items.filter((_, i) => i !== index); setItems(newItems); - onChange?.(newItems); + + // targetTable์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ๊ฐ ํ•ญ๋ชฉ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + const dataWithMeta = config.targetTable + ? newItems.map((item) => ({ ...item, _targetTable: config.targetTable })) + : newItems; + + onChange?.(dataWithMeta); // ์ ‘ํžŒ ์ƒํƒœ๋„ ์—…๋ฐ์ดํŠธ const newCollapsed = new Set(collapsedItems); @@ -121,7 +134,19 @@ export const RepeaterInput: React.FC = ({ [fieldName]: value, }; setItems(newItems); - onChange?.(newItems); + console.log("โœ๏ธ RepeaterInput ํ•„๋“œ ๋ณ€๊ฒฝ, onChange ํ˜ธ์ถœ:", { + itemIndex, + fieldName, + value, + newItems, + }); + + // targetTable์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ๊ฐ ํ•ญ๋ชฉ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + const dataWithMeta = config.targetTable + ? newItems.map((item) => ({ ...item, _targetTable: config.targetTable })) + : newItems; + + onChange?.(dataWithMeta); }; // ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ ํ† ๊ธ€ diff --git a/frontend/components/webtypes/config/RepeaterConfigPanel.tsx b/frontend/components/webtypes/config/RepeaterConfigPanel.tsx index a527545d..5d534fb6 100644 --- a/frontend/components/webtypes/config/RepeaterConfigPanel.tsx +++ b/frontend/components/webtypes/config/RepeaterConfigPanel.tsx @@ -18,12 +18,20 @@ export interface RepeaterConfigPanelProps { config: RepeaterFieldGroupConfig; onChange: (config: RepeaterFieldGroupConfig) => void; tableColumns?: ColumnInfo[]; + allTables?: Array<{ tableName: string; displayName?: string }>; // ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก + onTableChange?: (tableName: string) => void; // ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์‹œ ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋กœ๋“œ } /** * ๋ฐ˜๋ณต ํ•„๋“œ ๊ทธ๋ฃน ์„ค์ • ํŒจ๋„ */ -export const RepeaterConfigPanel: React.FC = ({ config, onChange, tableColumns = [] }) => { +export const RepeaterConfigPanel: React.FC = ({ + config, + onChange, + tableColumns = [], + allTables = [], + onTableChange, +}) => { const [localFields, setLocalFields] = useState(config.fields || []); const [fieldNamePopoverOpen, setFieldNamePopoverOpen] = useState>({}); @@ -71,8 +79,79 @@ export const RepeaterConfigPanel: React.FC = ({ config handleFieldsChange(newFields); }; + // ํ…Œ์ด๋ธ” ์„ ํƒ Combobox ์ƒํƒœ + const [tableSelectOpen, setTableSelectOpen] = useState(false); + const [tableSearchValue, setTableSearchValue] = useState(""); + + // ํ•„ํ„ฐ๋ง๋œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก + const filteredTables = useMemo(() => { + if (!tableSearchValue) return allTables; + const searchLower = tableSearchValue.toLowerCase(); + return allTables.filter( + (table) => + table.tableName.toLowerCase().includes(searchLower) || table.displayName?.toLowerCase().includes(searchLower), + ); + }, [allTables, tableSearchValue]); + + // ์„ ํƒ๋œ ํ…Œ์ด๋ธ” ํ‘œ์‹œ๋ช… + const selectedTableLabel = useMemo(() => { + if (!config.targetTable) return "ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”"; + const table = allTables.find((t) => t.tableName === config.targetTable); + return table ? table.displayName || table.tableName : config.targetTable; + }, [config.targetTable, allTables]); + return (
+ {/* ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ ํƒ */} +
+ + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {filteredTables.map((table) => ( + { + handleChange("targetTable", currentValue); + setTableSelectOpen(false); + setTableSearchValue(""); + // ์„ ํƒ๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ๋กœ๋“œ + if (onTableChange) { + onTableChange(currentValue); + } + }} + > + + {table.displayName || table.tableName} + + ))} + + + + +

๋ฐ˜๋ณต ํ•„๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”.

+
+ {/* ํ•„๋“œ ์ •์˜ */}
diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 4f6ff820..72eab245 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -77,6 +77,7 @@ export interface DynamicComponentRendererProps { component: ComponentData; isSelected?: boolean; isPreview?: boolean; // ๋ฐ˜์‘ํ˜• ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ + isDesignMode?: boolean; // ๋””์ž์ธ ๋ชจ๋“œ ์—ฌ๋ถ€ (false์ผ ๋•Œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ) onClick?: (e?: React.MouseEvent) => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: () => void; @@ -193,8 +194,34 @@ export const DynamicComponentRenderer: React.FC = autoGeneration: component.autoGeneration, hidden: component.hidden, isInteractive, + isPreview, // ๋ฐ˜์‘ํ˜• ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ + isDesignMode: props.isDesignMode, // ๋””์ž์ธ ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ }); + // onChange ํ•ธ๋“ค๋Ÿฌ - ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ + const handleChange = (value: any) => { + console.log("๐Ÿ”„ DynamicComponentRenderer handleChange ํ˜ธ์ถœ:", { + componentType, + fieldName, + value, + valueType: typeof value, + isArray: Array.isArray(value), + }); + + if (onFormDataChange) { + // RepeaterInput ๊ฐ™์€ ๋ณตํ•ฉ ์ปดํฌ๋„ŒํŠธ๋Š” ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ + // ๋‹จ์ˆœ input ์ปดํฌ๋„ŒํŠธ๋Š” (fieldName, value) ํ˜•ํƒœ๋กœ ์ „๋‹ฌ๋ฐ›์Œ + if (componentType === "repeater-field-group" || componentType === "repeater") { + // fieldName๊ณผ ํ•จ๊ป˜ ์ „๋‹ฌ + console.log("๐Ÿ’พ RepeaterInput ๋ฐ์ดํ„ฐ ์ €์žฅ:", fieldName, value); + onFormDataChange(fieldName, value); + } else { + // ์ด๋ฏธ fieldName์ด ํฌํ•จ๋œ ๊ฒฝ์šฐ๋Š” ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ + onFormDataChange(fieldName, value); + } + } + }; + // ๋ Œ๋”๋Ÿฌ props ๊ตฌ์„ฑ const rendererProps = { component, @@ -215,7 +242,7 @@ export const DynamicComponentRenderer: React.FC = isInteractive, formData, onFormDataChange, - onChange: onFormDataChange, // onChange๋„ ์ „๋‹ฌ + onChange: handleChange, // ๊ฐœ์„ ๋œ onChange ํ•ธ๋“ค๋Ÿฌ ์ „๋‹ฌ tableName, onRefresh, onClose, @@ -237,6 +264,8 @@ export const DynamicComponentRenderer: React.FC = refreshKey, // ๋ฐ˜์‘ํ˜• ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ ์ „๋‹ฌ isPreview, + // ๋””์ž์ธ ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ ์ „๋‹ฌ - isPreview์™€ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ + isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false, }; // ๋ Œ๋”๋Ÿฌ๊ฐ€ ํด๋ž˜์Šค์ธ์ง€ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธ diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index f5b39ee2..ad7238eb 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -88,18 +88,19 @@ export const ButtonPrimaryComponent: React.FC = ({ // ์‚ญ์ œ ์•ก์…˜ ๊ฐ์ง€ ๋กœ์ง (์‹ค์ œ ํ•„๋“œ๋ช… ์‚ฌ์šฉ) const isDeleteAction = () => { - const deleteKeywords = ['์‚ญ์ œ', 'delete', 'remove', '์ œ๊ฑฐ', 'del']; + const deleteKeywords = ["์‚ญ์ œ", "delete", "remove", "์ œ๊ฑฐ", "del"]; return ( - component.componentConfig?.action?.type === 'delete' || - component.config?.action?.type === 'delete' || - component.webTypeConfig?.actionType === 'delete' || - component.text?.toLowerCase().includes('์‚ญ์ œ') || - component.text?.toLowerCase().includes('delete') || - component.label?.toLowerCase().includes('์‚ญ์ œ') || - component.label?.toLowerCase().includes('delete') || - deleteKeywords.some(keyword => - component.config?.buttonText?.toLowerCase().includes(keyword) || - component.config?.text?.toLowerCase().includes(keyword) + component.componentConfig?.action?.type === "delete" || + component.config?.action?.type === "delete" || + component.webTypeConfig?.actionType === "delete" || + component.text?.toLowerCase().includes("์‚ญ์ œ") || + component.text?.toLowerCase().includes("delete") || + component.label?.toLowerCase().includes("์‚ญ์ œ") || + component.label?.toLowerCase().includes("delete") || + deleteKeywords.some( + (keyword) => + component.config?.buttonText?.toLowerCase().includes(keyword) || + component.config?.text?.toLowerCase().includes(keyword), ) ); }; @@ -109,9 +110,9 @@ export const ButtonPrimaryComponent: React.FC = ({ if (isDeleteAction() && !component.style?.labelColor) { // ์‚ญ์ œ ์•ก์…˜์ด๊ณ  ๋ผ๋ฒจ ์ƒ‰์ƒ์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ์ž๋™ ์„ค์ • if (component.style) { - component.style.labelColor = '#ef4444'; + component.style.labelColor = "#ef4444"; } else { - component.style = { labelColor: '#ef4444' }; + component.style = { labelColor: "#ef4444" }; } } }, [component.componentConfig?.action?.type, component.config?.action?.type, component.webTypeConfig?.actionType]); @@ -125,20 +126,20 @@ export const ButtonPrimaryComponent: React.FC = ({ // ๐ŸŽจ ๋™์  ์ƒ‰์ƒ ์„ค์ • (์†์„ฑํŽธ์ง‘ ๋ชจ๋‹ฌ์˜ "์ƒ‰์ƒ" ํ•„๋“œ์™€ ์—ฐ๋™) const getLabelColor = () => { if (isDeleteAction()) { - return component.style?.labelColor || '#ef4444'; // ๋นจ๊ฐ„์ƒ‰ ๊ธฐ๋ณธ๊ฐ’ (Tailwind red-500) + return component.style?.labelColor || "#ef4444"; // ๋นจ๊ฐ„์ƒ‰ ๊ธฐ๋ณธ๊ฐ’ (Tailwind red-500) } - return component.style?.labelColor || '#212121'; // ๊ฒ€์€์ƒ‰ ๊ธฐ๋ณธ๊ฐ’ (shadcn/ui primary) + return component.style?.labelColor || "#212121"; // ๊ฒ€์€์ƒ‰ ๊ธฐ๋ณธ๊ฐ’ (shadcn/ui primary) }; const buttonColor = getLabelColor(); - + // ๊ทธ๋ผ๋ฐ์ด์…˜์šฉ ์–ด๋‘์šด ์ƒ‰์ƒ ๊ณ„์‚ฐ const getDarkColor = (baseColor: string) => { - const hex = baseColor.replace('#', ''); + const hex = baseColor.replace("#", ""); const r = Math.max(0, parseInt(hex.substr(0, 2), 16) - 40); const g = Math.max(0, parseInt(hex.substr(2, 2), 16) - 40); const b = Math.max(0, parseInt(hex.substr(4, 2), 16) - 40); - return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; }; const buttonDarkColor = getDarkColor(buttonColor); @@ -246,6 +247,23 @@ export const ButtonPrimaryComponent: React.FC = ({ currentLoadingToastRef.current = undefined; } + // ์‹คํŒจํ•œ ๊ฒฝ์šฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ + if (!success) { + console.log("โŒ ์•ก์…˜ ์‹คํŒจ, ์˜ค๋ฅ˜ ํ† ์ŠคํŠธ ํ‘œ์‹œ"); + const errorMessage = + actionConfig.errorMessage || + (actionConfig.type === "save" + ? "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + : actionConfig.type === "delete" + ? "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + : actionConfig.type === "submit" + ? "์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + : "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); + toast.error(errorMessage); + return; + } + + // ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ์—๋งŒ ์„ฑ๊ณต ํ† ์ŠคํŠธ ํ‘œ์‹œ // edit ์•ก์…˜์€ ์กฐ์šฉํžˆ ์ฒ˜๋ฆฌ (๋ชจ๋‹ฌ ์—ด๊ธฐ๋งŒ ํ•˜๋ฏ€๋กœ ํ† ์ŠคํŠธ ๋ถˆํ•„์š”) if (actionConfig.type !== "edit") { const successMessage = @@ -268,24 +286,24 @@ export const ButtonPrimaryComponent: React.FC = ({ // ์ €์žฅ/์ˆ˜์ • ์„ฑ๊ณต ์‹œ ์ž๋™ ์ฒ˜๋ฆฌ if (actionConfig.type === "save" || actionConfig.type === "edit") { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { // 1. ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋จผ์ € ๋ฐœ์†ก (๋ชจ๋‹ฌ์ด ๋‹ซํžˆ๊ธฐ ์ „์—) console.log("๐Ÿ”„ ์ €์žฅ/์ˆ˜์ • ํ›„ ํ…Œ์ด๋ธ” ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์†ก"); - window.dispatchEvent(new CustomEvent('refreshTable')); - + window.dispatchEvent(new CustomEvent("refreshTable")); + // 2. ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ (์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด) setTimeout(() => { // EditModal ๋‚ด๋ถ€์ธ์ง€ ํ™•์ธ (isInModal prop ์‚ฌ์šฉ) const isInEditModal = (props as any).isInModal; - + if (isInEditModal) { console.log("๐Ÿšช EditModal ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์†ก"); - window.dispatchEvent(new CustomEvent('closeEditModal')); + window.dispatchEvent(new CustomEvent("closeEditModal")); } - + // ScreenModal์€ ํ•ญ์ƒ ๋‹ซ๊ธฐ console.log("๐Ÿšช ScreenModal ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์†ก"); - window.dispatchEvent(new CustomEvent('closeSaveModal')); + window.dispatchEvent(new CustomEvent("closeSaveModal")); }, 100); } } @@ -301,19 +319,8 @@ export const ButtonPrimaryComponent: React.FC = ({ console.error("โŒ ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ ์˜ค๋ฅ˜:", error); - // ์˜ค๋ฅ˜ ํ† ์ŠคํŠธ ํ‘œ์‹œ - const errorMessage = - actionConfig.errorMessage || - (actionConfig.type === "save" - ? "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." - : actionConfig.type === "delete" - ? "์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." - : actionConfig.type === "submit" - ? "์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." - : "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."); - - console.log("๐Ÿ’ฅ ์˜ค๋ฅ˜ ํ† ์ŠคํŠธ ํ‘œ์‹œ:", errorMessage); - toast.error(errorMessage); + // ์˜ค๋ฅ˜ ํ† ์ŠคํŠธ๋Š” buttonActions.ts์—์„œ ์ด๋ฏธ ํ‘œ์‹œ๋˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋Š” ์ œ๊ฑฐ + // (์ค‘๋ณต ํ† ์ŠคํŠธ ๋ฐฉ์ง€) } }; @@ -379,7 +386,7 @@ export const ButtonPrimaryComponent: React.FC = ({ console.log("โš ๏ธ ์•ก์…˜ ์‹คํ–‰ ์กฐ๊ฑด ๋ถˆ๋งŒ์กฑ:", { isInteractive, hasAction: !!processedConfig.action, - "์ด์œ ": !isInteractive ? "์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ชจ๋“œ ์•„๋‹˜" : "์•ก์…˜ ์—†์Œ", + ์ด์œ : !isInteractive ? "์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ชจ๋“œ ์•„๋‹˜" : "์•ก์…˜ ์—†์Œ", }); // ์•ก์…˜์ด ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๊ธฐ๋ณธ onClick ์‹คํ–‰ onClick?.(); @@ -479,7 +486,7 @@ export const ButtonPrimaryComponent: React.FC = ({ maxHeight: "100%", border: "none", borderRadius: "8px", - background: componentConfig.disabled + background: componentConfig.disabled ? "linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)" : `linear-gradient(135deg, ${buttonColor} 0%, ${buttonDarkColor} 100%)`, color: componentConfig.disabled ? "#9ca3af" : "white", @@ -495,9 +502,7 @@ export const ButtonPrimaryComponent: React.FC = ({ margin: "0", lineHeight: "1", minHeight: "36px", - boxShadow: componentConfig.disabled - ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" - : `0 2px 4px 0 ${buttonColor}33`, // 33์€ 20% ํˆฌ๋ช…๋„ + boxShadow: componentConfig.disabled ? "0 1px 2px 0 rgba(0, 0, 0, 0.05)" : `0 2px 4px 0 ${buttonColor}33`, // 33์€ 20% ํˆฌ๋ช…๋„ // isInteractive ๋ชจ๋“œ์—์„œ๋Š” ์‚ฌ์šฉ์ž ์Šคํƒ€์ผ ์šฐ์„  ์ ์šฉ ...(isInteractive && component.style ? component.style : {}), }} diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index 866af1c0..645cca8b 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -64,7 +64,7 @@ export class RepeaterFieldGroupRenderer extends AutoRegisteringComponentRenderer configPanel: RepeaterConfigPanel, defaultSize: { width: 600, - height: 400, // ์—ฌ๋Ÿฌ ํ•ญ๋ชฉ๊ณผ ํ•„๋“œ๋ฅผ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋„๋ก ๋†’์ด ์„ค์ • + height: 200, // ๊ธฐ๋ณธ ๋†’์ด ์กฐ์ • }, defaultConfig: { fields: [], // ๋นˆ ๋ฐฐ์—ด๋กœ ์‹œ์ž‘ - ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ํ•„๋“œ ์ถ”๊ฐ€ diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index b4bc3ba5..94c4b213 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -55,9 +55,9 @@ export const SplitPanelLayoutComponent: React.FC // ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ const componentStyle: React.CSSProperties = isPreview ? { - // ๋ฐ˜์‘ํ˜• ๋ชจ๋“œ: position relative, width/height 100% + // ๋ฐ˜์‘ํ˜• ๋ชจ๋“œ: position relative, ๊ทธ๋ฆฌ๋“œ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํฌ๊ธฐ ์‚ฌ์šฉ position: "relative", - width: "100%", + // width ์ œ๊ฑฐ - ๊ทธ๋ฆฌ๋“œ ์ปฌ๋Ÿผ์ด ๊ฒฐ์ • height: `${component.style?.height || 600}px`, border: "1px solid #e5e7eb", } @@ -257,19 +257,27 @@ export const SplitPanelLayoutComponent: React.FC return (
{ if (isDesignMode) { e.stopPropagation(); onClick?.(e); } }} - className="flex overflow-hidden rounded-lg bg-white shadow-sm" + className={`flex overflow-hidden rounded-lg bg-white shadow-sm ${isPreview ? "w-full" : ""}`} > {/* ์ขŒ์ธก ํŒจ๋„ */}
@@ -404,7 +412,10 @@ export const SplitPanelLayoutComponent: React.FC )} {/* ์šฐ์ธก ํŒจ๋„ */} -
+
diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index c7036bbd..d53d90e9 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -28,7 +28,7 @@ interface SplitPanelLayoutConfigPanelProps { export const SplitPanelLayoutConfigPanel: React.FC = ({ config, onChange, - tables = [], // ๊ธฐ๋ณธ๊ฐ’ ๋นˆ ๋ฐฐ์—ด + tables = [], // ๊ธฐ๋ณธ๊ฐ’ ๋นˆ ๋ฐฐ์—ด (ํ˜„์žฌ ํ™”๋ฉด ํ…Œ์ด๋ธ”๋งŒ) screenTableName, // ํ˜„์žฌ ํ™”๋ฉด์˜ ํ…Œ์ด๋ธ”๋ช… }) => { const [rightTableOpen, setRightTableOpen] = useState(false); @@ -36,6 +36,32 @@ export const SplitPanelLayoutConfigPanel: React.FC>({}); const [loadingColumns, setLoadingColumns] = useState>({}); + const [allTables, setAllTables] = useState([]); // ์กฐ์ธ ๋ชจ๋“œ์šฉ ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก + + // ๊ด€๊ณ„ ํƒ€์ž… + const relationshipType = config.rightPanel?.relation?.type || "detail"; + + // ์กฐ์ธ ๋ชจ๋“œ์ผ ๋•Œ๋งŒ ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + useEffect(() => { + if (relationshipType === "join") { + const loadAllTables = async () => { + try { + const { tableManagementApi } = await import("@/lib/api/tableManagement"); + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + console.log("โœ… ๋ถ„ํ• ํŒจ๋„ ์กฐ์ธ ๋ชจ๋“œ: ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ", response.data.length, "๊ฐœ"); + setAllTables(response.data); + } + } catch (error) { + console.error("โŒ ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + loadAllTables(); + } else { + // ์ƒ์„ธ ๋ชจ๋“œ์ผ ๋•Œ๋Š” ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋งŒ ์‚ฌ์šฉ + setAllTables([]); + } + }, [relationshipType]); // screenTableName์ด ๋ณ€๊ฒฝ๋˜๋ฉด ์ขŒ์ธก ํŒจ๋„ ํ…Œ์ด๋ธ”์„ ํ•ญ์ƒ ํ™”๋ฉด ํ…Œ์ด๋ธ”๋กœ ์„ค์ • useEffect(() => { @@ -155,8 +181,13 @@ export const SplitPanelLayoutConfigPanel: React.FC @@ -285,7 +316,7 @@ export const SplitPanelLayoutConfigPanel: React.FC ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tables.map((table) => ( + {availableRightTables.map((table) => ( - {table.tableName} - ({table.tableLabel || ""}) + {table.displayName || table.tableName} + {table.displayName && ({table.tableName})} ))} diff --git a/frontend/lib/registry/components/split-panel-layout/index.ts b/frontend/lib/registry/components/split-panel-layout/index.ts index 10c6ee7d..080eab8a 100644 --- a/frontend/lib/registry/components/split-panel-layout/index.ts +++ b/frontend/lib/registry/components/split-panel-layout/index.ts @@ -41,7 +41,7 @@ export const SplitPanelLayoutDefinition = createComponentDefinition({ autoLoad: true, syncSelection: true, } as SplitPanelLayoutConfig, - defaultSize: { width: 1000, height: 600 }, + defaultSize: { width: 800, height: 600 }, configPanel: SplitPanelLayoutConfigPanel, icon: "PanelLeftRight", tags: ["๋ถ„ํ• ", "๋งˆ์Šคํ„ฐ", "๋””ํ…Œ์ผ", "๋ ˆ์ด์•„์›ƒ"], diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index b538e6bc..6352e117 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -155,14 +155,26 @@ export const TableListComponent: React.FC = ({ onSelectedRowsChange, onConfigChange, refreshKey, + tableName, // ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ”๋ช… (screenInfo์—์„œ ์ „๋‹ฌ) }) => { // ์ปดํฌ๋„ŒํŠธ ์„ค์ • const tableConfig = { ...config, ...component.config, ...componentConfig, + // selectedTable์ด ์—†์œผ๋ฉด ํ™”๋ฉด์˜ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + selectedTable: + componentConfig?.selectedTable || component.config?.selectedTable || config?.selectedTable || tableName, } as TableListConfig; + console.log("๐Ÿ” TableListComponent ์ดˆ๊ธฐํ™”:", { + componentConfigSelectedTable: componentConfig?.selectedTable, + componentConfigSelectedTable2: component.config?.selectedTable, + configSelectedTable: config?.selectedTable, + screenTableName: tableName, + finalSelectedTable: tableConfig.selectedTable, + }); + // ๐ŸŽจ ๋™์  ์ƒ‰์ƒ ์„ค์ • (์†์„ฑํŽธ์ง‘ ๋ชจ๋‹ฌ์˜ "์ƒ‰์ƒ" ํ•„๋“œ์™€ ์—ฐ๋™) const buttonColor = component.style?.labelColor || "#212121"; // ๊ธฐ๋ณธ ํŒŒ๋ž€์ƒ‰ const buttonTextColor = component.config?.buttonTextColor || "#ffffff"; @@ -424,20 +436,8 @@ export const TableListComponent: React.FC = ({ } }; - // ๋””๋ฐ”์šด์‹ฑ๋œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ - const fetchTableDataDebounced = useCallback( - debouncedApiCall( - `fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`, - async () => { - return fetchTableDataInternal(); - }, - 200, // 200ms ๋””๋ฐ”์šด์Šค - ), - [tableConfig.selectedTable, currentPage, localPageSize, searchTerm, sortColumn, sortDirection, searchValues], - ); - // ์‹ค์ œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ํ•จ์ˆ˜ - const fetchTableDataInternal = async () => { + const fetchTableDataInternal = useCallback(async () => { if (!tableConfig.selectedTable) { setData([]); return; @@ -448,81 +448,54 @@ export const TableListComponent: React.FC = ({ try { // ๐ŸŽฏ Entity ์กฐ์ธ API ์‚ฌ์šฉ - Entity ์กฐ์ธ์ด ํฌํ•จ๋œ ๋ฐ์ดํ„ฐ ์กฐํšŒ - console.log("๐Ÿ”— Entity ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์‹œ์ž‘:", tableConfig.selectedTable); // Entity ์กฐ์ธ ์ปฌ๋Ÿผ ์ถ”์ถœ (isEntityJoin === true์ธ ์ปฌ๋Ÿผ๋“ค) const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; - // ๐ŸŽฏ ์กฐ์ธ ํƒญ์—์„œ ์ถ”๊ฐ€ํ•œ ์ปฌ๋Ÿผ๋“ค๋„ ํฌํ•จ (์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ) - const joinTabColumns = - tableConfig.columns?.filter( - (col) => - !col.isEntityJoin && - col.columnName.includes("_") && - (col.columnName.includes("dept_code_") || - col.columnName.includes("_dept_code") || - col.columnName.includes("_company_") || - col.columnName.includes("_user_")), // ์กฐ์ธ ํƒญ์—์„œ ์ถ”๊ฐ€ํ•œ ์ปฌ๋Ÿผ ํŒจํ„ด๋“ค - ) || []; + // ๐ŸŽฏ ์กฐ์ธ ํƒญ์—์„œ ์ถ”๊ฐ€ํ•œ ์ปฌ๋Ÿผ๋“ค ์ถ”์ถœ (additionalJoinInfo๊ฐ€ ์žˆ๋Š” ์ปฌ๋Ÿผ๋“ค) + const manualJoinColumns = + tableConfig.columns?.filter((col) => { + return col.additionalJoinInfo !== undefined; + }) || []; console.log( - "๐Ÿ” ์กฐ์ธ ํƒญ ์ปฌ๋Ÿผ๋“ค:", - joinTabColumns.map((c) => c.columnName), + "๐Ÿ”— ์ˆ˜๋™ ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฐ์ง€:", + manualJoinColumns.map((c) => ({ + columnName: c.columnName, + additionalJoinInfo: c.additionalJoinInfo, + })), ); - const additionalJoinColumns = [ - ...entityJoinColumns.map((col) => ({ + // ๐ŸŽฏ ์ถ”๊ฐ€ ์กฐ์ธ ์ปฌ๋Ÿผ ์ •๋ณด ๊ตฌ์„ฑ + const additionalJoinColumns: Array<{ + sourceTable: string; + sourceColumn: string; + joinAlias: string; + referenceTable?: string; + }> = []; + + // Entity ์กฐ์ธ ์ปฌ๋Ÿผ๋“ค + entityJoinColumns.forEach((col) => { + additionalJoinColumns.push({ sourceTable: col.entityJoinInfo!.sourceTable, sourceColumn: col.entityJoinInfo!.sourceColumn, joinAlias: col.entityJoinInfo!.joinAlias, - })), - // ๐ŸŽฏ ์กฐ์ธ ํƒญ์—์„œ ์ถ”๊ฐ€ํ•œ ์ปฌ๋Ÿผ๋“ค๋„ ์ถ”๊ฐ€ (์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ) - ...joinTabColumns - .filter((col) => { - // ์‹ค์ œ API ์‘๋‹ต์— ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ ํ•„ํ„ฐ๋ง - const validJoinColumns = ["dept_code_name", "dept_name"]; - const isValid = validJoinColumns.includes(col.columnName); - if (!isValid) { - console.log(`๐Ÿ” ์กฐ์ธ ํƒญ ์ปฌ๋Ÿผ ์ œ์™ธ: ${col.columnName} (์œ ํšจํ•˜์ง€ ์•Š์Œ)`); - } - return isValid; - }) - .map((col) => { - // ์‹ค์ œ ์กด์žฌํ•˜๋Š” ์กฐ์ธ ์ปฌ๋Ÿผ๋งŒ ์ฒ˜๋ฆฌ - let sourceTable = tableConfig.selectedTable; - let sourceColumn = col.columnName; + }); + }); - if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { - sourceTable = "dept_info"; - sourceColumn = "dept_code"; - } - - console.log(`๐Ÿ” ์กฐ์ธ ํƒญ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); - - return { - sourceTable: sourceTable || tableConfig.selectedTable || "", - sourceColumn: sourceColumn, - joinAlias: col.columnName, - }; - }), - ]; - - // ๐ŸŽฏ ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ํ‘œ์‹œ ์„ค์ • ์ƒ์„ฑ - const screenEntityConfigs: Record = {}; - entityJoinColumns.forEach((col) => { - if (col.entityDisplayConfig) { - const sourceColumn = col.entityJoinInfo!.sourceColumn; - screenEntityConfigs[sourceColumn] = { - displayColumns: col.entityDisplayConfig.displayColumns, - separator: col.entityDisplayConfig.separator || " - ", - }; + // ์ˆ˜๋™ ์กฐ์ธ ์ปฌ๋Ÿผ๋“ค - ์ €์žฅ๋œ ์กฐ์ธ ์ •๋ณด ์‚ฌ์šฉ + manualJoinColumns.forEach((col) => { + if (col.additionalJoinInfo) { + additionalJoinColumns.push({ + sourceTable: col.additionalJoinInfo.sourceTable, + sourceColumn: col.additionalJoinInfo.sourceColumn, + joinAlias: col.additionalJoinInfo.joinAlias, + referenceTable: col.additionalJoinInfo.referenceTable, + }); } }); - console.log("๐Ÿ”— Entity ์กฐ์ธ ์ปฌ๋Ÿผ:", entityJoinColumns); - console.log("๐Ÿ”— ์กฐ์ธ ํƒญ ์ปฌ๋Ÿผ:", joinTabColumns); - console.log("๐Ÿ”— ์ถ”๊ฐ€ Entity ์กฐ์ธ ์ปฌ๋Ÿผ:", additionalJoinColumns); - // console.log("๐ŸŽฏ ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ์„ค์ •:", screenEntityConfigs); + console.log("๐Ÿ”— ์ตœ์ข… ์ถ”๊ฐ€ ์กฐ์ธ ์ปฌ๋Ÿผ:", additionalJoinColumns); const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page: currentPage, @@ -591,7 +564,6 @@ export const TableListComponent: React.FC = ({ sortOrder: sortDirection, enableEntityJoin: true, // ๐ŸŽฏ Entity ์กฐ์ธ ํ™œ์„ฑํ™” additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // ์ถ”๊ฐ€ ์กฐ์ธ ์ปฌ๋Ÿผ - screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // ๐ŸŽฏ ํ™”๋ฉด๋ณ„ ์—”ํ‹ฐํ‹ฐ ์„ค์ • }); if (result) { @@ -661,16 +633,16 @@ export const TableListComponent: React.FC = ({ const actualApiColumns = Object.keys(result.data[0]); console.log("๐Ÿ” API ์‘๋‹ต์˜ ์‹ค์ œ ์ปฌ๋Ÿผ๋“ค:", actualApiColumns); - // ๐ŸŽฏ ์กฐ์ธ ์ปฌ๋Ÿผ ๋งคํ•‘ ํ…Œ์ด๋ธ” (์‚ฌ์šฉ์ž ์„ค์ • โ†’ API ์‘๋‹ต) - // ์‹ค์ œ API ์‘๋‹ต์— ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ ๋งคํ•‘ - const newJoinColumnMapping: Record = { - dept_code_dept_code: "dept_code", // user_info.dept_code - dept_code_status: "status", // user_info.status (dept_info.status๊ฐ€ ์กฐ์ธ๋˜์ง€ ์•Š์Œ) - dept_code_company_name: "dept_name", // dept_info.dept_name (company_name์ด ์กฐ์ธ๋˜์ง€ ์•Š์Œ) - dept_code_name: "dept_code_name", // dept_info.dept_name - dept_name: "dept_name", // dept_info.dept_name - status: "status", // user_info.status - }; + // ๐ŸŽฏ ์กฐ์ธ ์ปฌ๋Ÿผ ๋งคํ•‘ ํ…Œ์ด๋ธ” - ๋™์  ์ƒ์„ฑ + // API ์‘๋‹ต์— ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๊ณผ ์‚ฌ์šฉ์ž ์„ค์ • ์ปฌ๋Ÿผ์„ ๋น„๊ตํ•˜์—ฌ ์ž๋™ ๋งคํ•‘ + const newJoinColumnMapping: Record = {}; + + processedColumns.forEach((col) => { + // API ์‘๋‹ต์— ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉ + if (actualApiColumns.includes(col.columnName)) { + newJoinColumnMapping[col.columnName] = col.columnName; + } + }); // ๐ŸŽฏ ์กฐ์ธ ์ปฌ๋Ÿผ ๋งคํ•‘ ์ƒํƒœ ์—…๋ฐ์ดํŠธ setJoinColumnMapping(newJoinColumnMapping); @@ -795,7 +767,37 @@ export const TableListComponent: React.FC = ({ } finally { setLoading(false); } - }; + }, [ + tableConfig.selectedTable, + tableConfig.columns, + currentPage, + localPageSize, + searchTerm, + sortColumn, + sortDirection, + searchValues, + ]); + + // ๋””๋ฐ”์šด์‹ฑ๋œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ + const fetchTableDataDebounced = useCallback( + debouncedApiCall( + `fetchTableData_${tableConfig.selectedTable}_${currentPage}_${localPageSize}`, + async () => { + return fetchTableDataInternal(); + }, + 200, // 200ms ๋””๋ฐ”์šด์Šค + ), + [ + tableConfig.selectedTable, + currentPage, + localPageSize, + searchTerm, + sortColumn, + sortDirection, + searchValues, + fetchTableDataInternal, + ], + ); // ํŽ˜์ด์ง€ ๋ณ€๊ฒฝ const handlePageChange = (newPage: number) => { @@ -947,12 +949,37 @@ export const TableListComponent: React.FC = ({ } }, [columnLabels]); + // ๐ŸŽฏ ์ปฌ๋Ÿผ ๊ฐœ์ˆ˜์™€ ์ปฌ๋Ÿผ๋ช…์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์˜์กด์„ฑ ์ถ”์  + const columnsKey = useMemo(() => { + if (!tableConfig.columns) return ""; + return tableConfig.columns.map((col) => col.columnName).join(","); + }, [tableConfig.columns]); + useEffect(() => { - if (tableConfig.autoLoad && !isDesignMode) { - fetchTableDataDebounced(); + // autoLoad๊ฐ€ undefined๊ฑฐ๋‚˜ true์ผ ๋•Œ ์ž๋™ ๋กœ๋“œ (๊ธฐ๋ณธ๊ฐ’: true) + const shouldAutoLoad = tableConfig.autoLoad !== false; + + console.log("๐Ÿ” TableList ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์กฐ๊ฑด ์ฒดํฌ:", { + shouldAutoLoad, + isDesignMode, + selectedTable: tableConfig.selectedTable, + autoLoadSetting: tableConfig.autoLoad, + willLoad: shouldAutoLoad && !isDesignMode, + }); + + if (shouldAutoLoad && !isDesignMode) { + console.log("โœ… ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ์ž‘:", tableConfig.selectedTable); + fetchTableDataInternal(); + } else { + console.warn("โš ๏ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ฐจ๋‹จ:", { + reason: !shouldAutoLoad ? "autoLoad=false" : "isDesignMode=true", + shouldAutoLoad, + isDesignMode, + }); } }, [ tableConfig.selectedTable, + columnsKey, // ๐ŸŽฏ ์ปฌ๋Ÿผ์ด ์ถ”๊ฐ€/๋ณ€๊ฒฝ๋  ๋•Œ ๋ฐ์ดํ„ฐ ๋‹ค์‹œ ๋กœ๋“œ (๋ฌธ์ž์—ด ๋น„๊ต) localPageSize, currentPage, searchTerm, @@ -960,6 +987,7 @@ export const TableListComponent: React.FC = ({ sortDirection, columnLabels, searchValues, + fetchTableDataInternal, // ์˜์กด์„ฑ ๋ฐฐ์—ด์— ์ถ”๊ฐ€ ]); // refreshKey ๋ณ€๊ฒฝ ์‹œ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ @@ -992,7 +1020,7 @@ export const TableListComponent: React.FC = ({ }; window.addEventListener("refreshTable", handleRefreshTable); - + return () => { window.removeEventListener("refreshTable", handleRefreshTable); }; @@ -1314,35 +1342,18 @@ export const TableListComponent: React.FC = ({ onDragEnd, }; - // ๋””์ž์ธ ๋ชจ๋“œ์—์„œ์˜ ํ”Œ๋ ˆ์ด์Šคํ™€๋” - if (isDesignMode && !tableConfig.selectedTable) { - return ( -
-
-
-
- -
-
ํ…Œ์ด๋ธ” ๋ฆฌ์ŠคํŠธ
-
- ์„ค์ • ํŒจ๋„์—์„œ ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•ด์ฃผ์„ธ์š” -
-
-
-
- ); - } + // ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ œ๊ฑฐ - ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋„ ๋ฐ”๋กœ ํ…Œ์ด๋ธ” ํ‘œ์‹œ return (
= ({ >
{(tableConfig.title || tableLabel) && ( -

{tableConfig.title || tableLabel}

+

{tableConfig.title || tableLabel}

)}
@@ -1377,16 +1388,14 @@ export const TableListComponent: React.FC = ({ size="sm" onClick={handleRefresh} disabled={loading} - className="group relative rounded-xl border-gray-200/60 bg-white/80 backdrop-blur-sm shadow-sm hover:shadow-md transition-all duration-200 hover:bg-gray-50/80" + className="group relative rounded-xl border-gray-200/60 bg-white/80 shadow-sm backdrop-blur-sm transition-all duration-200 hover:bg-gray-50/80 hover:shadow-md" >
{loading &&
}
- - {loading ? "์ƒˆ๋กœ๊ณ ์นจ ์ค‘..." : "์ƒˆ๋กœ๊ณ ์นจ"} - + {loading ? "์ƒˆ๋กœ๊ณ ์นจ ์ค‘..." : "์ƒˆ๋กœ๊ณ ์นจ"}
@@ -1424,7 +1433,7 @@ export const TableListComponent: React.FC = ({ {/* ํ…Œ์ด๋ธ” ์ปจํ…์ธ  */}
= ({ = ({
{/* ํ‘ธํ„ฐ/ํŽ˜์ด์ง€๋„ค์ด์…˜ */} - {tableConfig.showFooter && tableConfig.pagination?.enabled && ( + {/* showFooter์™€ pagination.enabled์˜ ๊ธฐ๋ณธ๊ฐ’์€ true */} + {tableConfig.showFooter !== false && tableConfig.pagination?.enabled !== false && (
= ({ // ๋ฐ์ดํ„ฐ๋Š” useEffect์—์„œ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋กœ๋“œ๋จ }} - className="rounded-xl border border-gray-200/60 bg-white/80 backdrop-blur-sm px-4 py-2 text-sm font-medium text-gray-700 shadow-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md" + className="rounded-xl border border-gray-200/60 bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm backdrop-blur-sm transition-all duration-200 hover:border-gray-300/60 hover:bg-white hover:shadow-md" > {(tableConfig.pagination?.pageSizeOptions || [10, 20, 50, 100]).map((size) => (