diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index 574f1cf8..a7757397 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -606,7 +606,7 @@ router.get( }); } - const { enableEntityJoin, groupByColumns } = req.query; + const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query; const enableEntityJoinFlag = enableEntityJoin === "true" || (typeof enableEntityJoin === "boolean" && enableEntityJoin); @@ -626,17 +626,22 @@ router.get( } } + // ๐Ÿ†• primaryKeyColumn ํŒŒ์‹ฑ + const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined; + console.log(`๐Ÿ” ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ: ${tableName}/${id}`, { enableEntityJoin: enableEntityJoinFlag, groupByColumns: groupByColumnsArray, + primaryKeyColumn: primaryKeyColumnStr, }); - // ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ (Entity Join ์˜ต์…˜ + ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ ํฌํ•จ) + // ๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ (Entity Join ์˜ต์…˜ + ๊ทธ๋ฃนํ•‘ ์˜ต์…˜ + Primary Key ์ปฌ๋Ÿผ ํฌํ•จ) const result = await dataService.getRecordDetail( tableName, id, enableEntityJoinFlag, - groupByColumnsArray + groupByColumnsArray, + primaryKeyColumnStr // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ ); if (!result.success) { diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 8c6e63f0..60de20db 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -490,7 +490,8 @@ class DataService { tableName: string, id: string | number, enableEntityJoin: boolean = false, - groupByColumns: string[] = [] + groupByColumns: string[] = [], + primaryKeyColumn?: string // ๐Ÿ†• ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ Primary Key ์ปฌ๋Ÿผ๋ช… ): Promise> { try { // ํ…Œ์ด๋ธ” ์ ‘๊ทผ ๊ฒ€์ฆ @@ -499,20 +500,30 @@ class DataService { return validation.error!; } - // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ - const pkResult = await query<{ attname: string }>( - `SELECT a.attname - 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] - ); + // ๐Ÿ†• ํด๋ผ์ด์–ธํŠธ์—์„œ ์ „๋‹ฌํ•œ Primary Key ์ปฌ๋Ÿผ์ด ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ + let pkColumn = primaryKeyColumn || ""; + + // Primary Key ์ปฌ๋Ÿผ์ด ์—†์œผ๋ฉด ์ž๋™ ๊ฐ์ง€ + if (!pkColumn) { + const pkResult = await query<{ attname: string }>( + `SELECT a.attname + 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] + ); - let pkColumn = "id"; // ๊ธฐ๋ณธ๊ฐ’ - if (pkResult.length > 0) { - pkColumn = pkResult[0].attname; + pkColumn = "id"; // ๊ธฐ๋ณธ๊ฐ’ + if (pkResult.length > 0) { + pkColumn = pkResult[0].attname; + } + console.log(`๐Ÿ”‘ [getRecordDetail] ์ž๋™ ๊ฐ์ง€๋œ Primary Key:`, pkResult); + } else { + console.log(`๐Ÿ”‘ [getRecordDetail] ํด๋ผ์ด์–ธํŠธ ์ œ๊ณต Primary Key: ${pkColumn}`); } + console.log(`๐Ÿ”‘ [getRecordDetail] ํ…Œ์ด๋ธ”: ${tableName}, Primary Key ์ปฌ๋Ÿผ: ${pkColumn}, ์กฐํšŒ ID: ${id}`); + // ๐Ÿ†• Entity Join์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ if (enableEntityJoin) { const { EntityJoinService } = await import("./entityJoinService"); diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 25d96927..86762b64 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -334,6 +334,10 @@ export class EntityJoinService { ); }); + // ๐Ÿ”ง _label ๋ณ„์นญ ์ค‘๋ณต ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ Set + // ๊ฐ™์€ sourceColumn์—์„œ ์—ฌ๋Ÿฌ ์กฐ์ธ ์„ค์ •์ด ์žˆ์„ ๋•Œ _label์€ ์ฒซ ๋ฒˆ์งธ๋งŒ ์ƒ์„ฑ + const generatedLabelAliases = new Set(); + const joinColumns = joinConfigs .map((config) => { const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; @@ -368,16 +372,26 @@ export class EntityJoinService { // _label ํ•„๋“œ๋„ ํ•จ๊ป˜ SELECT (ํ”„๋ก ํŠธ์—”๋“œ getColumnUniqueValues์šฉ) // sourceColumn_label ํ˜•์‹์œผ๋กœ ์ถ”๊ฐ€ - resultColumns.push( - `COALESCE(${alias}.${col}::TEXT, '') AS ${config.sourceColumn}_label` - ); + // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€: ๊ฐ™์€ sourceColumn์—์„œ _label์€ ์ฒซ ๋ฒˆ์งธ๋งŒ ์ƒ์„ฑ + const labelAlias = `${config.sourceColumn}_label`; + if (!generatedLabelAliases.has(labelAlias)) { + resultColumns.push( + `COALESCE(${alias}.${col}::TEXT, '') AS ${labelAlias}` + ); + generatedLabelAliases.add(labelAlias); + } // ๐Ÿ†• referenceColumn (PK)๋„ ํ•ญ์ƒ SELECT (parentDataMapping์šฉ) // ์˜ˆ: customer_code, item_number ๋“ฑ // col๊ณผ ๋™์ผํ•ด๋„ ๋ณ„๋„์˜ alias๋กœ ์ถ”๊ฐ€ (customer_code as customer_code) - resultColumns.push( - `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}` - ); + // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€: referenceColumn๋„ ํ•œ ๋ฒˆ๋งŒ ์ถ”๊ฐ€ + const refColAlias = config.referenceColumn; + if (!generatedLabelAliases.has(refColAlias)) { + resultColumns.push( + `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${refColAlias}` + ); + generatedLabelAliases.add(refColAlias); + } } else { resultColumns.push( `COALESCE(main.${col}::TEXT, '') AS ${config.aliasColumn}` @@ -392,6 +406,11 @@ export class EntityJoinService { const individualAlias = `${config.sourceColumn}_${col}`; + // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€: ๊ฐ™์€ alias๊ฐ€ ์ด๋ฏธ ์ƒ์„ฑ๋˜์—ˆ์œผ๋ฉด ์Šคํ‚ต + if (generatedLabelAliases.has(individualAlias)) { + return; + } + if (isJoinTableColumn) { // ์กฐ์ธ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์€ ์กฐ์ธ ๋ณ„์นญ ์‚ฌ์šฉ resultColumns.push( @@ -403,6 +422,7 @@ export class EntityJoinService { `COALESCE(main.${col}::TEXT, '') AS ${individualAlias}` ); } + generatedLabelAliases.add(individualAlias); }); // ๐Ÿ†• referenceColumn (PK)๋„ ํ•จ๊ป˜ SELECT (parentDataMapping์šฉ) @@ -410,11 +430,13 @@ export class EntityJoinService { config.referenceTable && config.referenceTable !== tableName; if ( isJoinTableColumn && - !displayColumns.includes(config.referenceColumn) + !displayColumns.includes(config.referenceColumn) && + !generatedLabelAliases.has(config.referenceColumn) // ๐Ÿ”ง ์ค‘๋ณต ๋ฐฉ์ง€ ) { resultColumns.push( `COALESCE(${alias}.${config.referenceColumn}::TEXT, '') AS ${config.referenceColumn}` ); + generatedLabelAliases.add(config.referenceColumn); } } diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 9dad459c..b409d8bf 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -921,11 +921,12 @@ export class TableManagementService { ...layout.properties, widgetType: inputType, inputType: inputType, - // componentConfig ๋‚ด๋ถ€์˜ type๋„ ์—…๋ฐ์ดํŠธ + // componentConfig ๋‚ด๋ถ€์˜ type, inputType, webType ๋ชจ๋‘ ์—…๋ฐ์ดํŠธ componentConfig: { ...layout.properties?.componentConfig, type: newComponentType, inputType: inputType, + webType: inputType, // ํ”„๋ก ํŠธ์—”๋“œ SelectBasicComponent์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ ๋กœ๋”ฉ ์—ฌ๋ถ€ ํŒ๋‹จ์— ์‚ฌ์šฉ }, }; @@ -941,7 +942,7 @@ export class TableManagementService { ); logger.info( - `ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ: screen_id=${layout.screen_id}, component_id=${layout.component_id}, widgetType=${inputType}, componentType=${newComponentType}` + `ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์—…๋ฐ์ดํŠธ: screen_id=${layout.screen_id}, component_id=${layout.component_id}, widgetType=${inputType}, webType=${inputType}, componentType=${newComponentType}` ); } diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 64fe38b8..cddbb73f 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -928,12 +928,39 @@ export const ExcelUploadModal: React.FC = ({ {field.inputType === "entity" ? ( handleColumnLabelChange(colName, e.target.value)} + placeholder={col?.columnLabel || colName} + className="h-6 flex-1 text-xs" + /> + + + + ); + })} + +

+ * ์ €์žฅ ์„ค์ •์€ ํ•„๋“œ ์ •์˜์—์„œ "ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ"๋กœ ์„ค์ •ํ•˜์„ธ์š” +

+ + )} + {/* ์„ ํƒ ์„ค์ • */} {(config.subDataLookup?.lookup?.displayColumns?.length || 0) > 0 && (
@@ -1351,35 +1491,106 @@ export const RepeaterConfigPanel: React.FC = ({ {/* ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์ด ์•„๋‹ ๋•Œ๋งŒ ํ‘œ์‹œ ๋ชจ๋“œ ์„ ํƒ */} {field.type !== "category" && ( -
-
- - -
+
+
+
+ + +
-
-
- updateField(index, { required: checked as boolean })} - /> - +
+
+ updateField(index, { required: checked as boolean })} + /> + +
+ + {/* ์ˆจ๊น€ ์ฒดํฌ๋ฐ•์Šค */} +
+ updateField(index, { isHidden: checked as boolean })} + /> + +
+ + {/* ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ */} + {config.subDataLookup?.enabled && ( +
+
+ { + updateField(index, { + subDataSource: { + enabled: checked as boolean, + sourceColumn: field.subDataSource?.sourceColumn || "", + }, + }); + }} + /> + +
+ + {field.subDataSource?.enabled && ( +
+ + +

+ ์žฌ๊ณ  ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ์ด ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค +

+
+ )} +
+ )}
)} diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 5045a43b..f1604337 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -11,6 +11,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { dynamicFormApi } from "@/lib/api/dynamicForm"; import { cascadingRelationApi } from "@/lib/api/cascadingRelation"; +import { AutoFillMapping } from "./config"; export function EntitySearchInputComponent({ tableName, @@ -37,6 +38,8 @@ export function EntitySearchInputComponent({ formData, // ๋‹ค์ค‘์„ ํƒ props multiple: multipleProp, + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ props + autoFillMappings: autoFillMappingsProp, // ์ถ”๊ฐ€ props component, isInteractive, @@ -47,6 +50,7 @@ export function EntitySearchInputComponent({ isInteractive?: boolean; onFormDataChange?: (fieldName: string, value: any) => void; webTypeConfig?: any; // ์›นํƒ€์ž… ์„ค์ • (์—ฐ์‡„๊ด€๊ณ„ ๋“ฑ) + autoFillMappings?: AutoFillMapping[]; // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ }) { // uiMode๊ฐ€ ์žˆ์œผ๋ฉด ์šฐ์„  ์‚ฌ์šฉ, ์—†์œผ๋ฉด modeProp ์‚ฌ์šฉ, ๊ธฐ๋ณธ๊ฐ’ "combo" const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; @@ -54,6 +58,18 @@ export function EntitySearchInputComponent({ // ๋‹ค์ค‘์„ ํƒ ๋ฐ ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • (props > webTypeConfig > componentConfig ์ˆœ์„œ) const config = component?.componentConfig || component?.webTypeConfig || {}; const isMultiple = multipleProp ?? config.multiple ?? false; + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • (props > config) + const autoFillMappings: AutoFillMapping[] = autoFillMappingsProp ?? config.autoFillMappings ?? []; + + // ๋””๋ฒ„๊ทธ: ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • ํ™•์ธ + console.log("๐Ÿ”ง [EntitySearchInput] ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ •:", { + autoFillMappingsProp, + configAutoFillMappings: config.autoFillMappings, + effectiveAutoFillMappings: autoFillMappings, + isInteractive, + hasOnFormDataChange: !!onFormDataChange, + }); // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ์ถ”์ถœ const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; @@ -309,6 +325,23 @@ export function EntitySearchInputComponent({ console.log("๐Ÿ“ค EntitySearchInput -> onFormDataChange:", component.columnName, newValue); } } + + // ๐Ÿ†• ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์ ์šฉ + if (autoFillMappings.length > 0 && isInteractive && onFormDataChange && fullData) { + console.log("๐Ÿ”„ ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์ ์šฉ:", { mappings: autoFillMappings, fullData }); + + for (const mapping of autoFillMappings) { + if (mapping.sourceField && mapping.targetField) { + const sourceValue = fullData[mapping.sourceField]; + if (sourceValue !== undefined) { + onFormDataChange(mapping.targetField, sourceValue); + console.log(` โœ… ${mapping.sourceField} โ†’ ${mapping.targetField}:`, sourceValue); + } else { + console.log(` โš ๏ธ ${mapping.sourceField} ๊ฐ’์ด ์—†์Œ`); + } + } + } + } }; // ๋‹ค์ค‘์„ ํƒ ๋ชจ๋“œ์—์„œ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ์ œ๊ฑฐ @@ -436,7 +469,7 @@ export function EntitySearchInputComponent({ const isSelected = selectedValues.includes(String(option[valueField])); return ( handleSelectOption(option)} className="text-xs sm:text-sm" @@ -509,7 +542,7 @@ export function EntitySearchInputComponent({ {effectiveOptions.map((option, index) => ( handleSelectOption(option)} className="text-xs sm:text-sm" diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx index fb75daa4..22a52aab 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputConfigPanel.tsx @@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react"; // allComponents๋Š” ํ˜„์žฌ ์‚ฌ์šฉ๋˜์ง€ ์•Š์ง€๋งŒ ํ–ฅํ›„ ํ™•์žฅ์„ ์œ„ํ•ด props์— ์œ ์ง€ -import { EntitySearchInputConfig } from "./config"; +import { EntitySearchInputConfig, AutoFillMapping } from "./config"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { tableTypeApi } from "@/lib/api/screen"; import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation"; @@ -236,6 +236,7 @@ export function EntitySearchInputConfigPanel({ const newConfig = { ...localConfig, ...updates }; setLocalConfig(newConfig); onConfigChange(newConfig); + console.log("๐Ÿ“ [EntitySearchInput] ์„ค์ • ์—…๋ฐ์ดํŠธ:", { updates, newConfig }); }; // ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™” @@ -636,9 +637,9 @@ export function EntitySearchInputConfigPanel({ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tableColumns.map((column) => ( + {tableColumns.map((column, idx) => ( { updateConfig({ displayField: column.columnName }); @@ -690,9 +691,9 @@ export function EntitySearchInputConfigPanel({ ํ•„๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. - {tableColumns.map((column) => ( + {tableColumns.map((column, idx) => ( { updateConfig({ valueField: column.columnName }); @@ -812,8 +813,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -860,8 +861,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -919,8 +920,8 @@ export function EntitySearchInputConfigPanel({ - {tableColumns.map((col) => ( - + {tableColumns.map((col, colIdx) => ( + {col.displayName || col.columnName} ))} @@ -939,6 +940,105 @@ export function EntitySearchInputConfigPanel({
)} + + {/* ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • */} +
+
+
+ +

์ž๋™ ์ฑ„์›€ ๋งคํ•‘

+
+ +
+

+ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์„ ํƒํ•˜๋ฉด ์†Œ์Šค ํ•„๋“œ์˜ ๊ฐ’์ด ๋Œ€์ƒ ํ•„๋“œ์— ์ž๋™์œผ๋กœ ์ฑ„์›Œ์ง‘๋‹ˆ๋‹ค. +

+ + {(localConfig.autoFillMappings || []).length > 0 && ( +
+ {(localConfig.autoFillMappings || []).map((mapping, index) => ( +
+ {/* ์†Œ์Šค ํ•„๋“œ (์„ ํƒ๋œ ์—”ํ‹ฐํ‹ฐ) */} +
+ + +
+ + {/* ํ™”์‚ดํ‘œ */} +
+ โ†’ +
+ + {/* ๋Œ€์ƒ ํ•„๋“œ (ํผ) */} +
+ + { + const mappings = [...(localConfig.autoFillMappings || [])]; + mappings[index] = { ...mappings[index], targetField: e.target.value }; + updateConfig({ autoFillMappings: mappings }); + }} + placeholder="ํผ ํ•„๋“œ๋ช…" + className="h-8 text-xs" + /> +
+ + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + +
+ ))} +
+ )} + + {(localConfig.autoFillMappings || []).length === 0 && ( +
+ ๋งคํ•‘์ด ์—†์Šต๋‹ˆ๋‹ค. + ์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ๋งคํ•‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +
+ )} +
); } diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx index dd6ed5c4..f8a3a22e 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputWrapper.tsx @@ -37,6 +37,9 @@ export const EntitySearchInputWrapper: React.FC = ({ // placeholder const placeholder = config.placeholder || widget?.placeholder || "ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”"; + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • + const autoFillMappings = config.autoFillMappings || []; console.log("๐Ÿข EntitySearchInputWrapper ๋ Œ๋”๋ง:", { tableName, @@ -44,6 +47,7 @@ export const EntitySearchInputWrapper: React.FC = ({ valueField, uiMode, multiple, + autoFillMappings, value, config, }); @@ -68,6 +72,7 @@ export const EntitySearchInputWrapper: React.FC = ({ value={value} onChange={onChange} multiple={multiple} + autoFillMappings={autoFillMappings} component={component} isInteractive={props.isInteractive} onFormDataChange={props.onFormDataChange} diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx index 555efe9b..422dfbfa 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchModal.tsx @@ -148,9 +148,9 @@ export function EntitySearchModal({ ์„ ํƒ )} - {displayColumns.map((col) => ( + {displayColumns.map((col, colIdx) => ( {col} @@ -179,7 +179,8 @@ export function EntitySearchModal({ ) : ( results.map((item, index) => { - const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`; + // null๊ณผ undefined ๋ชจ๋‘ ์ฒดํฌํ•˜์—ฌ ์œ ๋‹ˆํฌ ํ‚ค ์ƒ์„ฑ + const uniqueKey = item[valueField] != null ? `${item[valueField]}` : `row-${index}`; const isSelected = isItemSelected(item); return ( )} - {displayColumns.map((col) => ( - + {displayColumns.map((col, colIdx) => ( + {item[col] || "-"} ))} diff --git a/frontend/lib/registry/components/entity-search-input/config.ts b/frontend/lib/registry/components/entity-search-input/config.ts index fab81c9f..3dae8779 100644 --- a/frontend/lib/registry/components/entity-search-input/config.ts +++ b/frontend/lib/registry/components/entity-search-input/config.ts @@ -1,3 +1,9 @@ +// ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ํƒ€์ž… +export interface AutoFillMapping { + sourceField: string; // ์„ ํƒ๋œ ์—”ํ‹ฐํ‹ฐ์˜ ํ•„๋“œ (์˜ˆ: customer_name) + targetField: string; // ํผ์˜ ํ•„๋“œ (์˜ˆ: partner_name) +} + export interface EntitySearchInputConfig { tableName: string; displayField: string; @@ -18,5 +24,8 @@ export interface EntitySearchInputConfig { cascadingRelationCode?: string; // ์—ฐ์‡„๊ด€๊ณ„ ์ฝ”๋“œ (WAREHOUSE_LOCATION ๋“ฑ) cascadingRole?: "parent" | "child"; // ์—ญํ•  (๋ถ€๋ชจ/์ž์‹) cascadingParentField?: string; // ๋ถ€๋ชจ ํ•„๋“œ์˜ ์ปฌ๋Ÿผ๋ช… (์ž์‹ ์—ญํ• ์ผ ๋•Œ๋งŒ ์‚ฌ์šฉ) + + // ์ž๋™ ์ฑ„์›€ ๋งคํ•‘ ์„ค์ • + autoFillMappings?: AutoFillMapping[]; // ์—”ํ‹ฐํ‹ฐ ์„ ํƒ ์‹œ ๋‹ค๋ฅธ ํ•„๋“œ์— ์ž๋™์œผ๋กœ ๊ฐ’ ์ฑ„์šฐ๊ธฐ } diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 57bc2e8a..13cb1a68 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -7,6 +7,8 @@ import React, { useState, useMemo, useCallback, useEffect, useRef } from "react"; import { cn } from "@/lib/utils"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; import { PivotGridProps, PivotResult, @@ -50,6 +52,10 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; +// ==================== ์ƒ์ˆ˜ ==================== + +const PIVOT_STATE_VERSION = "1.0"; // ์ƒํƒœ ์ €์žฅ ๋ฒ„์ „ (ํ˜ธํ™˜์„ฑ ์ฒดํฌ์šฉ) + // ==================== ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ==================== // ์…€ ๋ณ‘ํ•ฉ ์ •๋ณด ๊ณ„์‚ฐ @@ -128,7 +134,10 @@ const RowHeaderCell: React.FC = ({
{row.hasChildren && ( )} + + {/* ์ƒํƒœ ์œ ์ง€ ์ฒดํฌ๋ฐ•์Šค */} +
+ setPersistState(checked === true)} + className="h-3.5 w-3.5" + /> + +
{/* ์ฐจํŠธ ํ† ๊ธ€ */} {chartConfig && ( @@ -1674,144 +1771,235 @@ export const PivotGridComponent: React.FC = ({ className="flex-1 overflow-auto focus:outline-none" style={{ maxHeight: enableVirtualScroll && containerHeight > 0 ? containerHeight : undefined, - minHeight: 100 // ์ตœ์†Œ ๋†’์ด ๋ณด์žฅ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + // ์ตœ์†Œ 200px ๋ณด์žฅ + ๋ฐ์ดํ„ฐ์— ๋งž๊ฒŒ ์กฐ์ • (์ตœ๋Œ€ 400px) + minHeight: Math.max( + 200, // ์ ˆ๋Œ€ ์ตœ์†Œ๊ฐ’ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + Math.min(400, (sortedFlatRows.length + 3) * ROW_HEIGHT + 50) + ) }} tabIndex={0} onKeyDown={handleKeyDown} > - {/* ์—ด ํ—ค๋” */} - - {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (ํ–‰ ํ•„๋“œ ๋ผ๋ฒจ + ํ•„ํ„ฐ) */} - + {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (์ฒซ ๋ฒˆ์งธ ๋ ˆ๋ฒจ์—๋งŒ ํ‘œ์‹œ) */} + {levelIdx === 0 && ( + + )} + + {/* ์—ด ํ—ค๋” ์…€ - ํ•ด๋‹น ๋ ˆ๋ฒจ */} + {levelCells.map((cell, cellIdx) => ( + ))} - {rowFields.length === 0 && ํ•ญ๋ชฉ} - - - {/* ์—ด ํ—ค๋” ์…€ */} - {flatColumns.map((col, idx) => ( - )} - colSpan={dataFields.length || 1} - style={{ width: columnWidths[idx] || "auto", minWidth: 50 }} - onClick={dataFields.length === 1 ? () => handleSort(dataFields[0].field) : undefined} - > -
- {col.caption || "(์ „์ฒด)"} - {dataFields.length === 1 && } -
- {/* ์—ด ๋ฆฌ์‚ฌ์ด์ฆˆ ํ•ธ๋“ค */} -
handleResizeStart(idx, e)} - /> - - ))} - - {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} - {totals?.showRowGrandTotals && ( -
)} - colSpan={dataFields.length || 1} - rowSpan={dataFields.length > 1 ? 2 : 1} - > - ์ด๊ณ„ - - )} - - {/* ์—ด ํ•„๋“œ ํ•„ํ„ฐ (ํ—ค๋” ์˜ค๋ฅธ์ชฝ ๋์— ํ‘œ์‹œ) */} - {columnFields.length > 0 && ( + + )) + ) : ( + // ์—ด ํ•„๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ: ๋‹จ์ผ ํ–‰ + - )} - + + {/* ์—ด ํ—ค๋” ์…€ (์—ด ํ•„๋“œ ์—†์„ ๋•Œ) */} + {flatColumns.map((col, idx) => ( + + ))} + + {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} + {totals?.showRowGrandTotals && ( + + )} + + )} {/* ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋ผ๋ฒจ (๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ํ•„๋“œ์ธ ๊ฒฝ์šฐ) */} {dataFields.length > 1 && ( diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx index 37f0862b..448c92a5 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridConfigPanel.tsx @@ -16,6 +16,7 @@ import { PivotAreaType, AggregationType, FieldDataType, + DateGroupInterval, } from "./types"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; @@ -202,6 +203,28 @@ const AreaDropZone: React.FC = ({ )} + {/* ํ–‰/์—ด ์˜์—ญ์—์„œ ๋‚ ์งœ ํƒ€์ž…์ผ ๋•Œ ๊ทธ๋ฃนํ™” ์˜ต์…˜ */} + {(area === "row" || area === "column") && field.dataType === "date" && ( + + )} + + {/* ํ•„ํ„ฐ ์•„์ด์ฝ˜ (ํ•„ํ„ฐ ์ ์šฉ ์‹œ) */} + {hasFilter && ( + + )} + {/* ํ•„๋“œ ๋ผ๋ฒจ */} + + + + + ํ•„ํ„ฐ๋งŒ ์ดˆ๊ธฐํ™” + {filteredFieldCount > 0 && ( + + ({filteredFieldCount}๊ฐœ) + + )} + + + + ํ•„๋“œ ๋ฐฐ์น˜ ์ดˆ๊ธฐํ™” + + + + + ์ „์ฒด ์ดˆ๊ธฐํ™” + + + + + {/* ์ ‘๊ธฐ ๋ฒ„ํŠผ */} + {onToggleCollapse && ( - - )} + )} + {/* ๋“œ๋ž˜๊ทธ ์˜ค๋ฒ„๋ ˆ์ด */} diff --git a/frontend/lib/registry/components/pivot-grid/types.ts b/frontend/lib/registry/components/pivot-grid/types.ts index 87ba2414..d4d8b1e5 100644 --- a/frontend/lib/registry/components/pivot-grid/types.ts +++ b/frontend/lib/registry/components/pivot-grid/types.ts @@ -304,6 +304,7 @@ export interface PivotHeaderNode { level: number; // ๊นŠ์ด children?: PivotHeaderNode[]; // ์ž์‹ ๋…ธ๋“œ isExpanded: boolean; // ํ™•์žฅ ์ƒํƒœ + hasChildren: boolean; // ์ž์‹ ์กด์žฌ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ (๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ ์žˆ์Œ) path: string[]; // ๊ฒฝ๋กœ (๋“œ๋ฆด๋‹ค์šด์šฉ) subtotal?: PivotCellValue[]; // ์†Œ๊ณ„ span?: number; // colspan/rowspan @@ -330,8 +331,11 @@ export interface PivotResult { // ํ”Œ๋žซ ํ–‰ ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) flatRows: PivotFlatRow[]; - // ํ”Œ๋žซ ์—ด ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) + // ํ”Œ๋žซ ์—ด ๋ชฉ๋ก (๋ Œ๋”๋ง์šฉ) - ๋ฆฌํ”„ ๋…ธ๋“œ๋งŒ flatColumns: PivotFlatColumn[]; + + // ์—ด ํ—ค๋” ๋ ˆ๋ฒจ๋ณ„ (๋‹ค์ค‘ ํ–‰ ํ—ค๋”์šฉ) + columnHeaderLevels: PivotColumnHeaderCell[][]; // ์ดํ•ฉ๊ณ„ grandTotals: { @@ -360,6 +364,14 @@ export interface PivotFlatColumn { isTotal?: boolean; } +// ์—ด ํ—ค๋” ์…€ (๋‹ค์ค‘ ํ–‰ ํ—ค๋”์šฉ) +export interface PivotColumnHeaderCell { + caption: string; // ํ‘œ์‹œ ํ…์ŠคํŠธ + colSpan: number; // ๋ณ‘ํ•ฉํ•  ์—ด ์ˆ˜ + path: string[]; // ์ „์ฒด ๊ฒฝ๋กœ + level: number; // ๋ ˆ๋ฒจ (0๋ถ€ํ„ฐ ์‹œ์ž‘) +} + // ==================== ์ƒํƒœ ๊ด€๋ฆฌ ==================== export interface PivotGridState { diff --git a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts index 02dd4608..35893dea 100644 --- a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts +++ b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts @@ -10,6 +10,7 @@ import { PivotFlatRow, PivotFlatColumn, PivotCellValue, + PivotColumnHeaderCell, DateGroupInterval, AggregationType, SummaryDisplayMode, @@ -76,6 +77,31 @@ export function pathToKey(path: string[]): string { return path.join("||"); } +/** + * ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ (์—ด ์ „์ฒด ํ™•์žฅ์šฉ) + */ +function generateAllPaths( + data: Record[], + fields: PivotFieldConfig[] +): string[] { + const allPaths: string[] = []; + + // ๊ฐ ๋ ˆ๋ฒจ๊นŒ์ง€์˜ ๊ณ ์œ  ๊ฒฝ๋กœ ์ˆ˜์ง‘ + for (let depth = 1; depth <= fields.length; depth++) { + const fieldsAtDepth = fields.slice(0, depth); + const pathSet = new Set(); + + data.forEach((row) => { + const path = fieldsAtDepth.map((f) => getFieldValue(row, f)); + pathSet.add(pathToKey(path)); + }); + + pathSet.forEach((pathKey) => allPaths.push(pathKey)); + } + + return allPaths; +} + /** * ํ‚ค๋ฅผ ๊ฒฝ๋กœ๋กœ ๋ณ€ํ™˜ */ @@ -129,6 +155,7 @@ function buildHeaderTree( caption: key, level: 0, isExpanded: expandedPaths.has(pathKey), + hasChildren: remainingFields.length > 0, // ๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ž์‹ ์žˆ์Œ path: path, span: 1, }; @@ -195,6 +222,7 @@ function buildChildNodes( caption: key, level: level, isExpanded: expandedPaths.has(pathKey), + hasChildren: remainingFields.length > 0, // ๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์ž์‹ ์žˆ์Œ path: path, span: 1, }; @@ -238,7 +266,7 @@ function flattenRows(nodes: PivotHeaderNode[]): PivotFlatRow[] { level: node.level, caption: node.caption, isExpanded: node.isExpanded, - hasChildren: !!(node.children && node.children.length > 0), + hasChildren: node.hasChildren, // ๋…ธ๋“œ์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ด (๋‹ค์Œ ๋ ˆ๋ฒจ ํ•„๋“œ ์กด์žฌ ์—ฌ๋ถ€ ๊ธฐ์ค€) }); if (node.isExpanded && node.children) { @@ -324,6 +352,66 @@ function getMaxColumnLevel( return Math.min(maxLevel, totalFields - 1); } +/** + * ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” ์ƒ์„ฑ + * ๊ฐ ๋ ˆ๋ฒจ๋ณ„๋กœ ์…€๊ณผ colSpan ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ + */ +function buildColumnHeaderLevels( + nodes: PivotHeaderNode[], + totalLevels: number +): PivotColumnHeaderCell[][] { + if (totalLevels === 0 || nodes.length === 0) { + return []; + } + + const levels: PivotColumnHeaderCell[][] = Array.from( + { length: totalLevels }, + () => [] + ); + + // ๋ฆฌํ”„ ๋…ธ๋“œ ์ˆ˜ ๊ณ„์‚ฐ (colSpan ๊ณ„์‚ฐ์šฉ) + function countLeaves(node: PivotHeaderNode): number { + if (!node.children || node.children.length === 0 || !node.isExpanded) { + return 1; + } + return node.children.reduce((sum, child) => sum + countLeaves(child), 0); + } + + // ํŠธ๋ฆฌ ์ˆœํšŒํ•˜๋ฉฐ ๊ฐ ๋ ˆ๋ฒจ์— ์…€ ์ถ”๊ฐ€ + function traverse(node: PivotHeaderNode, level: number) { + const colSpan = countLeaves(node); + + levels[level].push({ + caption: node.caption, + colSpan, + path: node.path, + level, + }); + + if (node.children && node.isExpanded) { + for (const child of node.children) { + traverse(child, level + 1); + } + } else if (level < totalLevels - 1) { + // ํ™•์žฅ๋˜์ง€ ์•Š์€ ๋…ธ๋“œ๋Š” ๋‹ค์Œ ๋ ˆ๋ฒจ๋“ค์— ๋นˆ ์…€๋กœ ์ฑ„์›€ + for (let i = level + 1; i < totalLevels; i++) { + levels[i].push({ + caption: "", + colSpan, + path: node.path, + level: i, + }); + } + } + } + + for (const node of nodes) { + traverse(node, 0); + } + + return levels; +} + // ==================== ๋ฐ์ดํ„ฐ ๋งคํŠธ๋ฆญ์Šค ์ƒ์„ฑ ==================== /** @@ -733,12 +821,11 @@ export function processPivotData( uniqueValues.forEach((val) => expandedRowSet.add(val)); } - if (expandedColumnPaths.length === 0 && columnFields.length > 0) { - const firstField = columnFields[0]; - const uniqueValues = new Set( - filteredData.map((row) => getFieldValue(row, firstField)) - ); - uniqueValues.forEach((val) => expandedColSet.add(val)); + // ์—ด์€ ํ•ญ์ƒ ์ „์ฒด ํ™•์žฅ (์—ด ํ—ค๋”๋Š” ํ™•์žฅ/์ถ•์†Œ UI๊ฐ€ ์—†์Œ) + // ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์—ด ๊ฒฝ๋กœ๋ฅผ ํ™•์žฅ ์ƒํƒœ๋กœ ์„ค์ • + if (columnFields.length > 0) { + const allColumnPaths = generateAllPaths(filteredData, columnFields); + allColumnPaths.forEach((pathKey) => expandedColSet.add(pathKey)); } // ํ—ค๋” ํŠธ๋ฆฌ ์ƒ์„ฑ @@ -786,6 +873,12 @@ export function processPivotData( grandTotals.grand ); + // ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” ์ƒ์„ฑ + const columnHeaderLevels = buildColumnHeaderLevels( + columnHeaders, + columnFields.length + ); + return { rowHeaders, columnHeaders, @@ -797,6 +890,7 @@ export function processPivotData( caption: path[path.length - 1] || "", span: 1, })), + columnHeaderLevels, grandTotals, }; } diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index 2aefb047..63e1cbb9 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -287,12 +287,18 @@ const RepeaterFieldGroupComponent: React.FC = (props) => if (onChange && items.length > 0) { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // ๐Ÿ†• subDataSource ์„ค์ •์ด ์žˆ๋Š” ํ•„๋“œ ๋ชฉ๋ก (ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์—ฐ๋™) + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: targetTable, _originalItemIds: itemIds, // ๐Ÿ†• ์›๋ณธ ID ๋ชฉ๋ก๋„ ํ•จ๊ป˜ ์ „๋‹ฌ _existingRecord: !!item.id, // ๐Ÿ†• ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ ํ”Œ๋ž˜๊ทธ (id๊ฐ€ ์žˆ์œผ๋ฉด ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ) _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก + _repeaterFieldsConfig: fieldsConfig, // ๐Ÿ†• ํ•„๋“œ ์„ค์ • (subDataSource ๋“ฑ) })); onChange(dataWithMeta); } @@ -393,11 +399,17 @@ const RepeaterFieldGroupComponent: React.FC = (props) => if (items.length > 0) { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // ๐Ÿ†• subDataSource ์„ค์ •์ด ์žˆ๋Š” ํ•„๋“œ ๋ชฉ๋ก + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: effectiveTargetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก + _repeaterFieldsConfig: fieldsConfig, // ๐Ÿ†• ํ•„๋“œ ์„ค์ • (subDataSource ๋“ฑ) })); onChange(dataWithMeta); } else { @@ -681,6 +693,11 @@ const RepeaterFieldGroupComponent: React.FC = (props) => (newValue: any[]) => { // ๐Ÿ†• RepeaterFieldGroup์ด ๊ด€๋ฆฌํ•˜๋Š” ํ•„๋“œ ๋ชฉ๋ก ์ถ”์ถœ const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // ๐Ÿ†• subDataSource ์„ค์ •์ด ์žˆ๋Š” ํ•„๋“œ ๋ชฉ๋ก + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); // ๐Ÿ†• ๋ชจ๋“  ํ•ญ๋ชฉ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ let valueWithMeta = newValue.map((item: any) => ({ @@ -688,6 +705,7 @@ const RepeaterFieldGroupComponent: React.FC = (props) => _targetTable: effectiveTargetTable || targetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // ๐Ÿ†• ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ ๋ชฉ๋ก + _repeaterFieldsConfig: fieldsConfig, // ๐Ÿ†• ํ•„๋“œ ์„ค์ • (subDataSource ๋“ฑ) })); // ๐Ÿ†• ๋ถ„ํ•  ํŒจ๋„์—์„œ ์šฐ์ธก์ธ ๊ฒฝ์šฐ, FK ๊ฐ’ ์ถ”๊ฐ€ diff --git a/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx b/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx index 5baf0fe0..51be5a64 100644 --- a/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx +++ b/frontend/lib/registry/components/repeater-field-group/SubDataLookupPanel.tsx @@ -78,8 +78,20 @@ export const SubDataLookupPanel: React.FC = ({ return config.lookup.columnLabels?.[columnName] || columnName; }; - // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๋ชฉ๋ก - const displayColumns = config.lookup.displayColumns || []; + // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๋ชฉ๋ก (columnOrder๊ฐ€ ์žˆ์œผ๋ฉด ์ˆœ์„œ ์ ์šฉ) + const displayColumns = useMemo(() => { + const columns = config.lookup.displayColumns || []; + const columnOrder = config.lookup.columnOrder; + + if (columnOrder && columnOrder.length > 0) { + // columnOrder ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ (displayColumns์— ์žˆ๋Š” ๊ฒƒ๋งŒ) + const orderedCols = columnOrder.filter(col => columns.includes(col)); + // columnOrder์— ์—†์ง€๋งŒ displayColumns์— ์žˆ๋Š” ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + const remainingCols = columns.filter(col => !columnOrder.includes(col)); + return [...orderedCols, ...remainingCols]; + } + return columns; + }, [config.lookup.displayColumns, config.lookup.columnOrder]); // ์š”์•ฝ ์ •๋ณด ํ‘œ์‹œ์šฉ ์„ ํƒ ์ƒํƒœ const summaryText = useMemo(() => { diff --git a/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts b/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts index b2c44e3d..2753dd16 100644 --- a/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts +++ b/frontend/lib/registry/components/repeater-field-group/useSubDataLookup.ts @@ -197,10 +197,18 @@ export function useSubDataLookup(props: UseSubDataLookupProps): UseSubDataLookup return "์„ ํƒ ์•ˆ๋จ"; } - const { displayColumns, columnLabels } = config.lookup; + const { displayColumns, columnLabels, columnOrder } = config.lookup; const parts: string[] = []; - displayColumns.forEach((col) => { + // columnOrder๊ฐ€ ์žˆ์œผ๋ฉด ์ˆœ์„œ ์ ์šฉ, ์—†์œผ๋ฉด displayColumns ์ˆœ์„œ + let orderedColumns = displayColumns; + if (columnOrder && columnOrder.length > 0) { + const orderedCols = columnOrder.filter(col => displayColumns.includes(col)); + const remainingCols = displayColumns.filter(col => !columnOrder.includes(col)); + orderedColumns = [...orderedCols, ...remainingCols]; + } + + orderedColumns.forEach((col) => { const value = selectedItem[col]; if (value !== undefined && value !== null && value !== "") { const label = columnLabels?.[col] || col; diff --git a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx index c896e1f2..697e0f69 100644 --- a/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx +++ b/frontend/lib/registry/components/select-basic/SelectBasicComponent.tsx @@ -299,12 +299,14 @@ const SelectBasicComponent: React.FC = ({ tableName: component.tableName, columnName: component.columnName, webType, + menuObjid, // ๐Ÿ†• menuObjid ๋กœ๊น… ์ถ”๊ฐ€ }); setIsLoadingCategories(true); import("@/lib/api/tableCategoryValue").then(({ getCategoryValues }) => { - getCategoryValues(component.tableName!, component.columnName!) + // ๐Ÿ†• menuObjid๋ฅผ 4๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌ (์นดํ…Œ๊ณ ๋ฆฌ ์Šค์ฝ”ํ”„ ์ ์šฉ) + getCategoryValues(component.tableName!, component.columnName!, false, menuObjid) .then((response) => { console.log("๐Ÿ” [SelectBasic] ์นดํ…Œ๊ณ ๋ฆฌ API ์‘๋‹ต:", response); @@ -324,6 +326,7 @@ const SelectBasicComponent: React.FC = ({ activeValuesCount: activeValues.length, options, sampleOption: options[0], + menuObjid, // ๐Ÿ†• menuObjid ๋กœ๊น… ์ถ”๊ฐ€ }); setCategoryOptions(options); @@ -339,7 +342,7 @@ const SelectBasicComponent: React.FC = ({ }); }); } - }, [webType, component.tableName, component.columnName]); + }, [webType, component.tableName, component.columnName, menuObjid]); // ๐Ÿ†• menuObjid ์˜์กด์„ฑ ์ถ”๊ฐ€ // ๋””๋ฒ„๊น…: menuObjid๊ฐ€ ์ œ๋Œ€๋กœ ์ „๋‹ฌ๋˜๋Š”์ง€ ํ™•์ธ useEffect(() => { diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 0113a9a8..9b8e7cf0 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -33,6 +33,7 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; @@ -171,6 +172,12 @@ export const SplitPanelLayoutComponent: React.FC const [rightSearchQuery, setRightSearchQuery] = useState(""); const [isLoadingLeft, setIsLoadingLeft] = useState(false); const [isLoadingRight, setIsLoadingRight] = useState(false); + + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๊ด€๋ จ ์ƒํƒœ + const [activeTabIndex, setActiveTabIndex] = useState(0); // 0 = ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„), 1+ = ์ถ”๊ฐ€ ํƒญ + const [tabsData, setTabsData] = useState>({}); // ํƒญ๋ณ„ ๋ฐ์ดํ„ฐ ์บ์‹œ + const [tabsLoading, setTabsLoading] = useState>({}); // ํƒญ๋ณ„ ๋กœ๋”ฉ ์ƒํƒœ + const [rightTableColumns, setRightTableColumns] = useState([]); // ์šฐ์ธก ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด const [expandedItems, setExpandedItems] = useState>(new Set()); // ํŽผ์ณ์ง„ ํ•ญ๋ชฉ๋“ค const [leftColumnLabels, setLeftColumnLabels] = useState>({}); // ์ขŒ์ธก ์ปฌ๋Ÿผ ๋ผ๋ฒจ @@ -917,11 +924,11 @@ export const SplitPanelLayoutComponent: React.FC const { entityJoinApi } = await import("@/lib/api/entityJoin"); // ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด ์ƒ์„ฑ - // ๐Ÿ”ง ๊ด€๊ณ„ ํ•„ํ„ฐ๋ง์€ ์ •ํ™•ํ•œ ๊ฐ’ ๋งค์นญ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ equals ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ - // (entity ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ๊ฒฝ์šฐ ๊ธฐ๋ณธ contains ์—ฐ์‚ฐ์ž๊ฐ€ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์‹คํŒจํ•จ) + // ๐Ÿ”ง entity ํƒ€์ž… ์ปฌ๋Ÿผ์€ ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•˜๋ฏ€๋กœ operator: 'equals' ์‚ฌ์šฉ const searchConditions: Record = {}; keys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { + // ์—ฐ๊ฒฐ ํ•„ํ„ฐ๋Š” ์ •ํ™•ํ•œ ๊ฐ’ ๋งค์นญ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ equals ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ searchConditions[key.rightColumn] = { value: leftItem[key.leftColumn], operator: "equals", @@ -1006,12 +1013,145 @@ export const SplitPanelLayoutComponent: React.FC ], ); + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ํ•จ์ˆ˜ + const loadTabData = useCallback( + async (tabIndex: number, leftItem: any) => { + const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1]; + if (!tabConfig || !leftItem || isDesignMode) return; + + const tabTableName = tabConfig.tableName; + if (!tabTableName) return; + + setTabsLoading((prev) => ({ ...prev, [tabIndex]: true })); + try { + // ์กฐ์ธ ํ‚ค ํ™•์ธ + const keys = tabConfig.relation?.keys; + const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn; + const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn; + + let resultData: any[] = []; + + if (leftColumn && rightColumn) { + // ์กฐ์ธ ์กฐ๊ฑด์ด ์žˆ๋Š” ๊ฒฝ์šฐ + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const searchConditions: Record = {}; + + if (keys && keys.length > 0) { + // ๋ณตํ•ฉํ‚ค + // ๐Ÿ”ง entity ํƒ€์ž… ์ปฌ๋Ÿผ์€ ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•˜๋ฏ€๋กœ operator: 'equals' ์‚ฌ์šฉ + keys.forEach((key) => { + if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { + searchConditions[key.rightColumn] = { + value: leftItem[key.leftColumn], + operator: "equals", + }; + } + }); + } else { + // ๋‹จ์ผํ‚ค + // ๐Ÿ”ง entity ํƒ€์ž… ์ปฌ๋Ÿผ์€ ์ฝ”๋“œ ๊ฐ’์œผ๋กœ ์ •ํ™•ํžˆ ๋งค์นญํ•ด์•ผ ํ•˜๋ฏ€๋กœ operator: 'equals' ์‚ฌ์šฉ + const leftValue = leftItem[leftColumn]; + if (leftValue !== undefined) { + searchConditions[rightColumn] = { + value: leftValue, + operator: "equals", + }; + } + } + + console.log(`๐Ÿ”— [์ถ”๊ฐ€ํƒญ ${tabIndex}] ์กฐํšŒ ์กฐ๊ฑด:`, searchConditions); + + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + search: searchConditions, + enableEntityJoin: true, + size: 1000, + }); + + resultData = result.data || []; + } else { + // ์กฐ์ธ ์กฐ๊ฑด์ด ์—†๋Š” ๊ฒฝ์šฐ: ์ „์ฒด ๋ฐ์ดํ„ฐ ์กฐํšŒ (๋…๋ฆฝ ํƒญ) + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + enableEntityJoin: true, + size: 1000, + }); + resultData = result.data || []; + } + + // ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ ์šฉ + const dataFilter = tabConfig.dataFilter; + if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + resultData = resultData.filter((item: any) => { + return dataFilter.conditions.every((cond: any) => { + const value = item[cond.column]; + const condValue = cond.value; + switch (cond.operator) { + case "equals": + return value === condValue; + case "notEquals": + return value !== condValue; + case "contains": + return String(value).includes(String(condValue)); + default: + return true; + } + }); + }); + } + + // ์ค‘๋ณต ์ œ๊ฑฐ ์ ์šฉ + const deduplication = tabConfig.deduplication; + if (deduplication?.enabled && deduplication.groupByColumn) { + const groupedMap = new Map(); + resultData.forEach((item) => { + const key = String(item[deduplication.groupByColumn] || ""); + const existing = groupedMap.get(key); + if (!existing) { + groupedMap.set(key, item); + } else { + // keepStrategy์— ๋”ฐ๋ผ ์œ ์ง€ํ•  ํ•ญ๋ชฉ ๊ฒฐ์ • + const sortCol = deduplication.sortColumn || "start_date"; + const existingVal = existing[sortCol]; + const newVal = item[sortCol]; + if (deduplication.keepStrategy === "latest" && newVal > existingVal) { + groupedMap.set(key, item); + } else if (deduplication.keepStrategy === "earliest" && newVal < existingVal) { + groupedMap.set(key, item); + } + } + }); + resultData = Array.from(groupedMap.values()); + } + + console.log(`๐Ÿ”— [์ถ”๊ฐ€ํƒญ ${tabIndex}] ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ:`, resultData.length); + setTabsData((prev) => ({ ...prev, [tabIndex]: resultData })); + } catch (error) { + console.error(`์ถ”๊ฐ€ํƒญ ${tabIndex} ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:`, error); + toast({ + title: "๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ", + description: `ํƒญ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`, + variant: "destructive", + }); + } finally { + setTabsLoading((prev) => ({ ...prev, [tabIndex]: false })); + } + }, + [componentConfig.rightPanel?.additionalTabs, isDesignMode, toast], + ); + // ์ขŒ์ธก ํ•ญ๋ชฉ ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ const handleLeftItemSelect = useCallback( (item: any) => { setSelectedLeftItem(item); setExpandedRightItems(new Set()); // ์ขŒ์ธก ํ•ญ๋ชฉ ๋ณ€๊ฒฝ ์‹œ ์šฐ์ธก ํ™•์žฅ ์ดˆ๊ธฐํ™” - loadRightData(item); + setTabsData({}); // ๐Ÿ†• ๋ชจ๋“  ํƒญ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (activeTabIndex === 0) { + loadRightData(item); + } else { + loadTabData(activeTabIndex, item); + } // ๐Ÿ†• modalDataStore์— ์„ ํƒ๋œ ์ขŒ์ธก ํ•ญ๋ชฉ ์ €์žฅ (๋‹จ์ผ ์„ ํƒ) const leftTableName = componentConfig.leftPanel?.tableName; @@ -1022,7 +1162,30 @@ export const SplitPanelLayoutComponent: React.FC }); } }, - [loadRightData, componentConfig.leftPanel?.tableName, isDesignMode], + [loadRightData, loadTabData, activeTabIndex, componentConfig.leftPanel?.tableName, isDesignMode], + ); + + // ๐Ÿ†• ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + const handleTabChange = useCallback( + (newTabIndex: number) => { + setActiveTabIndex(newTabIndex); + + // ์„ ํƒ๋œ ์ขŒ์ธก ํ•ญ๋ชฉ์ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ํƒญ์˜ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (selectedLeftItem) { + if (newTabIndex === 0) { + // ๊ธฐ๋ณธ ํƒญ: ์šฐ์ธก ํŒจ๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋กœ๋“œ + if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) { + loadRightData(selectedLeftItem); + } + } else { + // ์ถ”๊ฐ€ ํƒญ: ํ•ด๋‹น ํƒญ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๋กœ๋“œ + if (!tabsData[newTabIndex]) { + loadTabData(newTabIndex, selectedLeftItem); + } + } + } + }, + [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData], ); // ์šฐ์ธก ํ•ญ๋ชฉ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ @@ -1427,21 +1590,40 @@ export const SplitPanelLayoutComponent: React.FC // ์ปค์Šคํ…€ ๋ชจ๋‹ฌ ํ™”๋ฉด ์—ด๊ธฐ const rightTableName = componentConfig.rightPanel?.tableName || ""; - // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) + // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: ์„ค์ •๊ฐ’ > id > ID > non-null ํ•„๋“œ) + // ๐Ÿ”ง ์„ค์ •์—์„œ primaryKeyColumn ์ง€์ • ๊ฐ€๋Šฅ + const configuredPrimaryKey = componentConfig.rightPanel?.editButton?.primaryKeyColumn; + let primaryKeyName = "id"; let primaryKeyValue: any; - if (item.id !== undefined && item.id !== null) { + if (configuredPrimaryKey && item[configuredPrimaryKey] !== undefined && item[configuredPrimaryKey] !== null) { + // ์„ค์ •๋œ Primary Key ์‚ฌ์šฉ + primaryKeyName = configuredPrimaryKey; + primaryKeyValue = item[configuredPrimaryKey]; + } else if (item.id !== undefined && item.id !== null) { primaryKeyName = "id"; primaryKeyValue = item.id; } else if (item.ID !== undefined && item.ID !== null) { primaryKeyName = "ID"; primaryKeyValue = item.ID; } else { - // ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ - const firstKey = Object.keys(item)[0]; - primaryKeyName = firstKey; - primaryKeyValue = item[firstKey]; + // ๐Ÿ”ง ์ฒซ ๋ฒˆ์งธ non-null ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ + const keys = Object.keys(item); + let found = false; + for (const key of keys) { + if (item[key] !== undefined && item[key] !== null) { + primaryKeyName = key; + primaryKeyValue = item[key]; + found = true; + break; + } + } + // ๋ชจ๋“  ํ•„๋“œ๊ฐ€ null์ด๋ฉด ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ ์‚ฌ์šฉ + if (!found && keys.length > 0) { + primaryKeyName = keys[0]; + primaryKeyValue = item[keys[0]]; + } } console.log("โœ… ์ˆ˜์ • ๋ชจ๋‹ฌ ์—ด๊ธฐ:", { @@ -1466,7 +1648,7 @@ export const SplitPanelLayoutComponent: React.FC hasGroupByColumns: groupByColumns.length > 0, }); - // ScreenModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ID + groupByColumns ์ „๋‹ฌ) + // ScreenModal ์—ด๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ (URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ID + groupByColumns + primaryKeyColumn ์ „๋‹ฌ) window.dispatchEvent( new CustomEvent("openScreenModal", { detail: { @@ -1475,6 +1657,7 @@ export const SplitPanelLayoutComponent: React.FC mode: "edit", editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, // ๐Ÿ†• Primary Key ์ปฌ๋Ÿผ๋ช… ์ „๋‹ฌ ...(groupByColumns.length > 0 && { groupByColumns: JSON.stringify(groupByColumns), }), @@ -1487,6 +1670,7 @@ export const SplitPanelLayoutComponent: React.FC screenId: modalScreenId, editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "์—†์Œ", }); @@ -2539,6 +2723,34 @@ export const SplitPanelLayoutComponent: React.FC className="flex flex-shrink-0 flex-col" > + {/* ๐Ÿ†• ํƒญ ๋ฐ” (์ถ”๊ฐ€ ํƒญ์ด ์žˆ์„ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 && ( +
+ handleTabChange(Number(value))} + className="w-full" + > + + + {componentConfig.rightPanel?.title || "๊ธฐ๋ณธ"} + + {componentConfig.rightPanel?.additionalTabs?.map((tab, index) => ( + + {tab.label || `ํƒญ ${index + 1}`} + + ))} + + +
+ )} >
- {componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„"} + {activeTabIndex === 0 + ? componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„" + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.title || + componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.label || + "์šฐ์ธก ํŒจ๋„"} {!isDesignMode && (
- {componentConfig.rightPanel?.showAdd && ( - - )} + {/* ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + {activeTabIndex === 0 + ? componentConfig.rightPanel?.showAdd && ( + + ) + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.showAdd && ( + + )} {/* ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •/์‚ญ์ œ๋Š” ๊ฐ ์นด๋“œ์—์„œ ์ฒ˜๋ฆฌ */}
)} @@ -2580,20 +2804,231 @@ export const SplitPanelLayoutComponent: React.FC
)} - {/* ์šฐ์ธก ๋ฐ์ดํ„ฐ */} - {isLoadingRight ? ( - // ๋กœ๋”ฉ ์ค‘ -
-
- -

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

-
-
- ) : rightData ? ( - // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ - Array.isArray(rightData) ? ( - // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ - (() => { + {/* ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง */} + {activeTabIndex > 0 ? ( + // ์ถ”๊ฐ€ ํƒญ ์ปจํ…์ธ  + (() => { + const currentTabConfig = componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]; + const currentTabData = tabsData[activeTabIndex] || []; + const isTabLoading = tabsLoading[activeTabIndex]; + + if (isTabLoading) { + return ( +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ); + } + + if (!selectedLeftItem) { + return ( +
+

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

+
+ ); + } + + if (currentTabData.length === 0) { + return ( +
+

๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+
+ ); + } + + // ํƒญ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง (๋ชฉ๋ก/ํ…Œ์ด๋ธ” ๋ชจ๋“œ) + const isTableMode = currentTabConfig?.displayMode === "table"; + + if (isTableMode) { + // ํ…Œ์ด๋ธ” ๋ชจ๋“œ + const displayColumns = currentTabConfig?.columns || []; + const columnsToShow = + displayColumns.length > 0 + ? displayColumns.map((col) => ({ + ...col, + label: col.label || col.name, + })) + : Object.keys(currentTabData[0] || {}) + .filter(shouldShowField) + .slice(0, 8) + .map((key) => ({ name: key, label: key })); + + return ( +
+
0 ? 2 : 1} - > -
- {rowFields.map((f, idx) => ( -
- {f.caption} - { - const newFields = fields.map((fld) => - fld.field === field.field && fld.area === "row" - ? { ...fld, filterValues: values, filterType: type } - : fld - ); - handleFieldsChange(newFields); - }} - trigger={ - - } - /> - {idx < rowFields.length - 1 && /} -
+ {/* ๋‹ค์ค‘ ํ–‰ ์—ด ํ—ค๋” */} + {columnHeaderLevels.length > 0 ? ( + // ์—ด ํ•„๋“œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ: ๊ฐ ๋ ˆ๋ฒจ๋ณ„๋กœ ํ–‰ ์ƒ์„ฑ + columnHeaderLevels.map((levelCells, levelIdx) => ( +
1 ? 1 : 0)} + > +
+ {rowFields.map((f, idx) => ( +
+ {f.caption} + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "row" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + {idx < rowFields.length - 1 && /} +
+ ))} + {rowFields.length === 0 && ํ•ญ๋ชฉ} +
+
+
+ {cell.caption || "(์ „์ฒด)"} + {levelIdx === columnHeaderLevels.length - 1 && dataFields.length === 1 && ( + + )} +
+
1 ? 1 : 0)} + > + ์ด๊ณ„ + 0 && ( + 1 ? 1 : 0)} + > +
+ {columnFields.map((f) => ( + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "column" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + ))} +
+
1 ? 2 : 1} > -
- {columnFields.map((f) => ( - { - const newFields = fields.map((fld) => - fld.field === field.field && fld.area === "column" - ? { ...fld, filterValues: values, filterType: type } - : fld - ); - handleFieldsChange(newFields); - }} - trigger={ - - } - /> +
+ {rowFields.map((f, idx) => ( +
+ {f.caption} + { + const newFields = fields.map((fld) => + fld.field === field.field && fld.area === "row" + ? { ...fld, filterValues: values, filterType: type } + : fld + ); + handleFieldsChange(newFields); + }} + trigger={ + + } + /> + {idx < rowFields.length - 1 && /} +
))} + {rowFields.length === 0 && ํ•ญ๋ชฉ}
handleSort(dataFields[0].field) : undefined} + > +
+ {col.caption || "(์ „์ฒด)"} + {dataFields.length === 1 && } +
+
handleResizeStart(idx, e)} + /> +
1 ? 2 : 1} + > + ์ด๊ณ„ +
+ + + {columnsToShow.map((col: any) => ( + + ))} + {(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && ( + + )} + + + + {currentTabData.map((item: any, idx: number) => ( + + {columnsToShow.map((col: any) => ( + + ))} + {(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && ( + + )} + + ))} + +
+ {col.label} + ์ž‘์—…
+ {formatCellValue(col.name, item[col.name], {}, col.format)} + +
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} +
+
+
+ ); + } else { + // ๋ชฉ๋ก (์นด๋“œ) ๋ชจ๋“œ + const displayColumns = currentTabConfig?.columns || []; + const summaryCount = currentTabConfig?.summaryColumnCount ?? 3; + const showLabel = currentTabConfig?.summaryShowLabel ?? true; + + return ( +
+ {currentTabData.map((item: any, idx: number) => { + const itemId = item.id || idx; + const isExpanded = expandedRightItems.has(itemId); + + // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๊ฒฐ์ • + const columnsToShow = + displayColumns.length > 0 + ? displayColumns + : Object.keys(item) + .filter(shouldShowField) + .slice(0, 8) + .map((key) => ({ name: key, label: key })); + + const summaryColumns = columnsToShow.slice(0, summaryCount); + const detailColumns = columnsToShow.slice(summaryCount); + + return ( +
+
toggleRightItemExpansion(itemId)} + > +
+
+ {summaryColumns.map((col: any) => ( +
+ {showLabel && ( + {col.label}: + )} + + {formatCellValue(col.name, item[col.name], {}, col.format)} + +
+ ))} +
+
+
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} + {detailColumns.length > 0 && + (isExpanded ? ( + + ) : ( + + ))} +
+
+ {isExpanded && detailColumns.length > 0 && ( +
+
+ {detailColumns.map((col: any) => ( +
+ {col.label}: + {formatCellValue(col.name, item[col.name], {}, col.format)} +
+ ))} +
+
+ )} +
+ ); + })} +
+ ); + } + })() + ) : ( + /* ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„) ๋ฐ์ดํ„ฐ */ + <> + {isLoadingRight ? ( + // ๋กœ๋”ฉ ์ค‘ +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ) : rightData ? ( + // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ + Array.isArray(rightData) ? ( + // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ + (() => { // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง const filteredData = rightSearchQuery ? rightData.filter((item) => { @@ -3023,14 +3458,16 @@ export const SplitPanelLayoutComponent: React.FC
- ) : ( - // ์„ ํƒ ์—†์Œ -
-
-

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

-

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

-
-
+ ) : ( + // ์„ ํƒ ์—†์Œ +
+
+

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

+

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

+
+
+ )} + )} diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 78abf111..9793acd8 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -41,7 +41,7 @@ import { Lock, } from "lucide-react"; import * as XLSX from "xlsx"; -import { FileText, ChevronRightIcon } from "lucide-react"; +import { FileText, ChevronRightIcon, Search } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -455,6 +455,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์ปฌ๋Ÿผ ํ—ค๋” ํ•„ํ„ฐ ์ƒํƒœ (์ƒ๋‹จ์—์„œ ์„ ์–ธ) const [headerFilters, setHeaderFilters] = useState>>({}); + const [headerLikeFilters, setHeaderLikeFilters] = useState>({}); // LIKE ๊ฒ€์ƒ‰์šฉ const [openFilterColumn, setOpenFilterColumn] = useState(null); // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ - filteredData๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ @@ -488,6 +489,22 @@ export const TableListComponent: React.FC = ({ }); } + // 2-1. ๐Ÿ†• LIKE ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ ์šฉ + if (Object.keys(headerLikeFilters).length > 0) { + result = result.filter((row) => { + return Object.entries(headerLikeFilters).every(([columnName, searchText]) => { + if (!searchText || searchText.trim() === "") return true; + + // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ + const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; + const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue).toLowerCase() : ""; + + // LIKE ๊ฒ€์ƒ‰ (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ) + return cellStr.includes(searchText.toLowerCase()); + }); + }); + } + // 3. ๐Ÿ†• Filter Builder ์ ์šฉ if (filterGroups.length > 0) { result = result.filter((row) => { @@ -541,7 +558,7 @@ export const TableListComponent: React.FC = ({ } return result; - }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, headerLikeFilters, filterGroups]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -2671,19 +2688,41 @@ export const TableListComponent: React.FC = ({ const value = row[mappedColumnName]; // ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘๋œ ๊ฐ’ ์ฒ˜๋ฆฌ - if (categoryMappings[col.columnName] && value !== null && value !== undefined) { - const mapping = categoryMappings[col.columnName][String(value)]; - if (mapping) { - return mapping.label; + if (value !== null && value !== undefined) { + const valueStr = String(value); + + // ๋””๋ฒ„๊ทธ ๋กœ๊ทธ (์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’์ธ ๊ฒฝ์šฐ๋งŒ) + if (valueStr.startsWith("CATEGORY_")) { + console.log("๐Ÿ” [์—‘์…€๋‹ค์šด๋กœ๋“œ] ์นดํ…Œ๊ณ ๋ฆฌ ๋ณ€ํ™˜ ์‹œ๋„:", { + columnName: col.columnName, + value: valueStr, + hasMappings: !!categoryMappings[col.columnName], + mappingsKeys: categoryMappings[col.columnName] ? Object.keys(categoryMappings[col.columnName]).slice(0, 5) : [], + }); } + + if (categoryMappings[col.columnName]) { + // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ์ค‘๋ณต ๊ฐ’ ์ฒ˜๋ฆฌ + if (valueStr.includes(",")) { + const values = valueStr.split(",").map((v) => v.trim()).filter((v) => v); + const labels = values.map((v) => { + const mapping = categoryMappings[col.columnName][v]; + return mapping ? mapping.label : v; + }); + return labels.join(", "); + } + // ๋‹จ์ผ ๊ฐ’ ์ฒ˜๋ฆฌ + const mapping = categoryMappings[col.columnName][valueStr]; + if (mapping) { + return mapping.label; + } + } + + return value; } // null/undefined ์ฒ˜๋ฆฌ - if (value === null || value === undefined) { - return ""; - } - - return value; + return ""; }); }); @@ -2935,6 +2974,7 @@ export const TableListComponent: React.FC = ({ headerFilters: Object.fromEntries( Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set)]), ), + headerLikeFilters, // LIKE ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ €์žฅ pageSize: localPageSize, timestamp: Date.now(), }; @@ -2955,6 +2995,7 @@ export const TableListComponent: React.FC = ({ frozenColumnCount, showGridLines, headerFilters, + headerLikeFilters, localPageSize, ]); @@ -2991,6 +3032,9 @@ export const TableListComponent: React.FC = ({ }); setHeaderFilters(filters); } + if (state.headerLikeFilters) { + setHeaderLikeFilters(state.headerLikeFilters); + } } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ๋ณต์› ์‹คํŒจ:", error); } @@ -5737,7 +5781,7 @@ export const TableListComponent: React.FC = ({ }} className={cn( "hover:bg-primary/20 ml-1 rounded p-0.5 transition-colors", - headerFilters[column.columnName]?.size > 0 && "text-primary bg-primary/10", + (headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && "text-primary bg-primary/10", )} title="ํ•„ํ„ฐ" > @@ -5745,7 +5789,7 @@ export const TableListComponent: React.FC = ({ e.stopPropagation()} > @@ -5754,16 +5798,42 @@ export const TableListComponent: React.FC = ({ ํ•„ํ„ฐ: {columnLabels[column.columnName] || column.displayName} - {headerFilters[column.columnName]?.size > 0 && ( + {(headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && ( )} -
+ {/* LIKE ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ */} +
+ + { + setHeaderLikeFilters((prev) => ({ + ...prev, + [column.columnName]: e.target.value, + })); + }} + className="border-input bg-background placeholder:text-muted-foreground h-7 w-full rounded-md border pl-7 pr-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" + onClick={(e) => e.stopPropagation()} + /> +
+ {/* ๊ตฌ๋ถ„์„  */} +
๋˜๋Š” ๊ฐ’ ์„ ํƒ:
+
{columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { const isSelected = headerFilters[column.columnName]?.has(val); return ( diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index debf58b2..a4b6074c 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -707,12 +707,19 @@ export class ButtonActionExecutor { if (repeaterJsonKeys.length > 0) { console.log("๐Ÿ”„ [handleSave] RepeaterFieldGroup JSON ๋ฌธ์ž์—ด ๊ฐ์ง€:", repeaterJsonKeys); - + // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (RepeaterFieldGroup ์ €์žฅ ์ „์— ์‹คํ–‰) - console.log("๐Ÿ” [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); - + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ ์ฒดํฌ: formData.id๊ฐ€ ์กด์žฌํ•˜๋ฉด UPDATE ๋ชจ๋“œ์ด๋ฏ€๋กœ ์ฑ„๋ฒˆ ์ฝ”๋“œ ์žฌํ• ๋‹น ๊ธˆ์ง€ + const isEditModeRepeater = + context.formData.id !== undefined && context.formData.id !== null && context.formData.id !== ""; + + console.log("๐Ÿ” [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘", { + isEditMode: isEditModeRepeater, + formDataId: context.formData.id, + }); + const fieldsWithNumberingRepeater: Record = {}; - + // formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ ์ฐพ๊ธฐ for (const [key, value] of Object.entries(context.formData)) { if (key.endsWith("_numberingRuleId") && value) { @@ -721,22 +728,27 @@ export class ButtonActionExecutor { console.log(`๐ŸŽฏ [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ํ•„๋“œ ๋ฐœ๊ฒฌ: ${fieldName} โ†’ ๊ทœ์น™ ${value}`); } } - + console.log("๐Ÿ“‹ [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ:", fieldsWithNumberingRepeater); - - // ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์žˆ๋Š” ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ - if (Object.keys(fieldsWithNumberingRepeater).length > 0) { - console.log("๐ŸŽฏ [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)"); + + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ์—์„œ๋Š” ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋›ฐ๊ธฐ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€) + // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ์—์„œ๋งŒ allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ๋ฒˆํ˜ธ ํ• ๋‹น + if (Object.keys(fieldsWithNumberingRepeater).length > 0 && !isEditModeRepeater) { + console.log( + "๐ŸŽฏ [handleSave-RepeaterFieldGroup] ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ - ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)", + ); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); - + for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) { try { console.log(`๐Ÿ”„ [handleSave-RepeaterFieldGroup] ${fieldName} ํ•„๋“œ์— ๋Œ€ํ•ด allocateCode ํ˜ธ์ถœ: ${ruleId}`); const allocateResult = await allocateNumberingCode(ruleId); - + if (allocateResult.success && allocateResult.data?.generatedCode) { const newCode = allocateResult.data.generatedCode; - console.log(`โœ… [handleSave-RepeaterFieldGroup] ${fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${context.formData[fieldName]} โ†’ ${newCode}`); + console.log( + `โœ… [handleSave-RepeaterFieldGroup] ${fieldName} ์ƒˆ ์ฝ”๋“œ ํ• ๋‹น: ${context.formData[fieldName]} โ†’ ${newCode}`, + ); context.formData[fieldName] = newCode; } else { console.warn(`โš ๏ธ [handleSave-RepeaterFieldGroup] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์‹คํŒจ:`, allocateResult.error); @@ -745,9 +757,11 @@ export class ButtonActionExecutor { console.error(`โŒ [handleSave-RepeaterFieldGroup] ${fieldName} ์ฝ”๋“œ ํ• ๋‹น ์˜ค๋ฅ˜:`, allocateError); } } + } else if (isEditModeRepeater) { + console.log("โญ๏ธ [handleSave-RepeaterFieldGroup] ์ˆ˜์ • ๋ชจ๋“œ - ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋œ€ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€)"); } - - console.log("โœ… [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); + + console.log("โœ… [handleSave-RepeaterFieldGroup] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ ์™„๋ฃŒ"); // ๐Ÿ†• ์ƒ๋‹จ ํผ ๋ฐ์ดํ„ฐ(๋งˆ์Šคํ„ฐ ์ •๋ณด) ์ถ”์ถœ // RepeaterFieldGroup JSON๊ณผ ์ปดํฌ๋„ŒํŠธ ํ‚ค๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€๊ฐ€ ๋งˆ์Šคํ„ฐ ์ •๋ณด @@ -803,7 +817,7 @@ export class ButtonActionExecutor { for (const item of parsedData) { // ๋ฉ”ํƒ€ ํ•„๋“œ ์ œ๊ฑฐ - const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, ...itemData } = item; + const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, _subDataSelection, _subDataMaxValue, ...itemData } = item; // ๐Ÿ”ง ํ’ˆ๋ชฉ ๊ณ ์œ  ํ•„๋“œ๋งŒ ์ถ”์ถœ (RepeaterFieldGroup ์„ค์ • ๊ธฐ๋ฐ˜) const itemOnlyData: Record = {}; @@ -812,6 +826,42 @@ export class ButtonActionExecutor { itemOnlyData[field] = itemData[field]; } }); + + // ๐Ÿ†• ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์„ ํƒ์—์„œ ๊ฐ’ ์ถ”์ถœ (subDataSource ์„ค์ • ๊ธฐ๋ฐ˜) + // ํ•„๋“œ ์ •์˜์—์„œ subDataSource.enabled๊ฐ€ true์ด๊ณ  sourceColumn์ด ์„ค์ •๋œ ํ•„๋“œ๋งŒ ์ฒ˜๋ฆฌ + if (_subDataSelection && typeof _subDataSelection === 'object') { + // _repeaterFieldsConfig์—์„œ subDataSource ์„ค์ • ํ™•์ธ + const fieldsConfig = item._repeaterFieldsConfig as Array<{ + name: string; + subDataSource?: { enabled: boolean; sourceColumn: string }; + }> | undefined; + + if (fieldsConfig && Array.isArray(fieldsConfig)) { + fieldsConfig.forEach((fieldConfig) => { + if (fieldConfig.subDataSource?.enabled && fieldConfig.subDataSource?.sourceColumn) { + const targetField = fieldConfig.name; // ํ•„๋“œ๋ช… = ์ €์žฅํ•  ์ปฌ๋Ÿผ๋ช… + const sourceColumn = fieldConfig.subDataSource.sourceColumn; + const sourceValue = _subDataSelection[sourceColumn]; + + if (sourceValue !== undefined && sourceValue !== null) { + itemOnlyData[targetField] = sourceValue; + console.log(`๐Ÿ“‹ [handleSave] ํ•˜์œ„ ๋ฐ์ดํ„ฐ ๊ฐ’ ๋งคํ•‘: ${sourceColumn} โ†’ ${targetField} = ${sourceValue}`); + } + } + }); + } else { + // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ: fieldsConfig๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ + Object.keys(_subDataSelection).forEach((subDataKey) => { + if (itemOnlyData[subDataKey] === undefined || itemOnlyData[subDataKey] === null || itemOnlyData[subDataKey] === '') { + const subDataValue = _subDataSelection[subDataKey]; + if (subDataValue !== undefined && subDataValue !== null) { + itemOnlyData[subDataKey] = subDataValue; + console.log(`๐Ÿ“‹ [handleSave] ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์„ ํƒ ๊ฐ’ ์ถ”๊ฐ€ (๋ ˆ๊ฑฐ์‹œ): ${subDataKey} = ${subDataValue}`); + } + } + }); + } + } // ๐Ÿ”ง ๋งˆ์Šคํ„ฐ ์ •๋ณด + ํ’ˆ๋ชฉ ๊ณ ์œ  ์ •๋ณด ๋ณ‘ํ•ฉ // masterFields: ์ƒ๋‹จ ํผ์—์„œ ์ˆ˜์ •ํ•œ ์ตœ์‹  ๋งˆ์Šคํ„ฐ ์ •๋ณด @@ -1915,7 +1965,16 @@ export class ButtonActionExecutor { } // ๐ŸŽฏ ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€) - console.log("๐Ÿ” [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘"); + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ ์ฒดํฌ: formData.id ๋˜๋Š” originalGroupedData๊ฐ€ ์žˆ์œผ๋ฉด UPDATE ๋ชจ๋“œ + const isEditModeUniversal = + (formData.id !== undefined && formData.id !== null && formData.id !== "") || + originalGroupedData.length > 0; + + console.log("๐Ÿ” [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์ฒดํฌ ์‹œ์ž‘", { + isEditMode: isEditModeUniversal, + formDataId: formData.id, + originalGroupedDataCount: originalGroupedData.length, + }); const fieldsWithNumbering: Record = {}; @@ -1941,9 +2000,12 @@ export class ButtonActionExecutor { console.log("๐Ÿ“‹ [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์„ค์ •๋œ ํ•„๋“œ:", fieldsWithNumbering); - // ๐Ÿ”ฅ ์ €์žฅ ์‹œ์ ์— allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€ - if (Object.keys(fieldsWithNumbering).length > 0) { - console.log("๐ŸŽฏ [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)"); + // ๐Ÿ”ง ์ˆ˜์ • ๋ชจ๋“œ์—์„œ๋Š” ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋›ฐ๊ธฐ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€) + // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ์—์„œ๋งŒ allocateCode ํ˜ธ์ถœํ•˜์—ฌ ์ƒˆ ๋ฒˆํ˜ธ ํ• ๋‹น + if (Object.keys(fieldsWithNumbering).length > 0 && !isEditModeUniversal) { + console.log( + "๐ŸŽฏ [handleUniversalFormModalTableSectionSave] ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ - ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์‹œ์ž‘ (allocateCode ํ˜ธ์ถœ)", + ); const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) { @@ -1970,6 +2032,8 @@ export class ButtonActionExecutor { // ์˜ค๋ฅ˜ ์‹œ ๊ธฐ์กด ๊ฐ’ ์œ ์ง€ } } + } else if (isEditModeUniversal) { + console.log("โญ๏ธ [handleUniversalFormModalTableSectionSave] ์ˆ˜์ • ๋ชจ๋“œ - ์ฑ„๋ฒˆ ์ฝ”๋“œ ํ• ๋‹น ๊ฑด๋„ˆ๋œ€ (๊ธฐ์กด ๋ฒˆํ˜ธ ์œ ์ง€)"); } console.log("โœ… [handleUniversalFormModalTableSectionSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ• ๋‹น ์™„๋ฃŒ"); @@ -4737,7 +4801,24 @@ export class ButtonActionExecutor { const filteredRow: Record = {}; visibleColumns!.forEach((columnName: string) => { const label = columnLabels?.[columnName] || columnName; - filteredRow[label] = row[columnName]; + let value = row[columnName]; + + // ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ๋ฅผ ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (CATEGORY_๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฐ’) + if (value && typeof value === "string" && value.includes("CATEGORY_")) { + // ๋จผ์ € _label ํ•„๋“œ ํ™•์ธ (API์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒฝ์šฐ) + const labelFieldName = `${columnName}_label`; + if (row[labelFieldName]) { + value = row[labelFieldName]; + } else { + // _value_label ํ•„๋“œ ํ™•์ธ + const valueLabelFieldName = `${columnName}_value_label`; + if (row[valueLabelFieldName]) { + value = row[valueLabelFieldName]; + } + } + } + + filteredRow[label] = value; }); return filteredRow; }); @@ -5010,8 +5091,15 @@ export class ButtonActionExecutor { value = row[`${columnName}_name`]; } // ์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ํ•„๋“œ๋Š” ๋ผ๋ฒจ๋กœ ๋ณ€ํ™˜ (๋ฐฑ์—”๋“œ์—์„œ ์ •์˜๋œ ์ปฌ๋Ÿผ๋งŒ) - else if (categoryMap[columnName] && typeof value === "string" && categoryMap[columnName][value]) { - value = categoryMap[columnName][value]; + else if (categoryMap[columnName] && typeof value === "string") { + // ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ + if (value.includes(",")) { + const values = value.split(",").map((v) => v.trim()).filter((v) => v); + const labels = values.map((v) => categoryMap[columnName][v] || v); + value = labels.join(", "); + } else if (categoryMap[columnName][value]) { + value = categoryMap[columnName][value]; + } } filteredRow[label] = value; diff --git a/frontend/lib/utils/excelExport.ts b/frontend/lib/utils/excelExport.ts index 52c22f5a..6bd97624 100644 --- a/frontend/lib/utils/excelExport.ts +++ b/frontend/lib/utils/excelExport.ts @@ -116,8 +116,10 @@ export async function importFromExcel( return; } - // JSON์œผ๋กœ ๋ณ€ํ™˜ - const jsonData = XLSX.utils.sheet_to_json(worksheet); + // JSON์œผ๋กœ ๋ณ€ํ™˜ (๋นˆ ์…€๋„ ํฌํ•จํ•˜์—ฌ ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‚ค ์œ ์ง€) + const jsonData = XLSX.utils.sheet_to_json(worksheet, { + defval: "", // ๋นˆ ์…€์— ๋นˆ ๋ฌธ์ž์—ด ํ• ๋‹น + }); console.log("โœ… ์—‘์…€ ๊ฐ€์ ธ์˜ค๊ธฐ ์™„๋ฃŒ:", { sheetName: targetSheetName, diff --git a/frontend/types/repeater.ts b/frontend/types/repeater.ts index 2362210b..c7f0fa98 100644 --- a/frontend/types/repeater.ts +++ b/frontend/types/repeater.ts @@ -43,9 +43,19 @@ export interface CalculationFormula { * ํ•„๋“œ ํ‘œ์‹œ ๋ชจ๋“œ * - input: ์ž…๋ ฅ ํ•„๋“œ๋กœ ํ‘œ์‹œ (ํŽธ์ง‘ ๊ฐ€๋Šฅ) * - readonly: ์ฝ๊ธฐ ์ „์šฉ ํ…์ŠคํŠธ๋กœ ํ‘œ์‹œ + * - hidden: ์ˆจ๊น€ (UI์— ํ‘œ์‹œ๋˜์ง€ ์•Š์ง€๋งŒ ๋ฐ์ดํ„ฐ์— ํฌํ•จ๋จ) * - (์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž…์€ ์ž๋™์œผ๋กœ ๋ฐฐ์ง€๋กœ ํ‘œ์‹œ๋จ) */ -export type RepeaterFieldDisplayMode = "input" | "readonly"; +export type RepeaterFieldDisplayMode = "input" | "readonly" | "hidden"; + +/** + * ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ์†Œ์Šค ์„ค์ • + * ํ•„๋“œ ๊ฐ’์„ ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์‚ฌ์šฉ + */ +export interface SubDataSourceConfig { + enabled: boolean; // ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + sourceColumn: string; // ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ…Œ์ด๋ธ”์˜ ์†Œ์Šค ์ปฌ๋Ÿผ (์˜ˆ: lot_number) +} /** * ๋ฐ˜๋ณต ๊ทธ๋ฃน ๋‚ด ๊ฐœ๋ณ„ ํ•„๋“œ ์ •์˜ @@ -60,6 +70,8 @@ export interface RepeaterFieldDefinition { options?: Array<{ label: string; value: string }>; // select์šฉ width?: string; // ํ•„๋“œ ๋„ˆ๋น„ (์˜ˆ: "200px", "50%") displayMode?: RepeaterFieldDisplayMode; // ํ‘œ์‹œ ๋ชจ๋“œ: input(์ž…๋ ฅ), readonly(์ฝ๊ธฐ์ „์šฉ) + isHidden?: boolean; // ์ˆจ๊น€ ์—ฌ๋ถ€ (true๋ฉด ํ…Œ์ด๋ธ”์— ํ‘œ์‹œ ์•ˆ ํ•จ, ๋ฐ์ดํ„ฐ๋Š” ์ €์žฅ) + subDataSource?: SubDataSourceConfig; // ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ ์„ค์ • categoryCode?: string; // category ํƒ€์ž…์ผ ๋•Œ ์‚ฌ์šฉํ•  ์นดํ…Œ๊ณ ๋ฆฌ ์ฝ”๋“œ formula?: CalculationFormula; // ๊ณ„์‚ฐ์‹ (type์ด "calculated"์ผ ๋•Œ ์‚ฌ์šฉ) numberFormat?: { @@ -113,6 +125,14 @@ export type RepeaterData = RepeaterItemData[]; // ํ’ˆ๋ชฉ ์„ ํƒ ์‹œ ์žฌ๊ณ /๋‹จ๊ฐ€ ๋“ฑ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  ์„ ํƒํ•˜๋Š” ๊ธฐ๋Šฅ // ============================================================ +/** + * ์„ ํƒ ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋งคํ•‘ ์„ค์ • + */ +export interface SubDataFieldMapping { + sourceColumn: string; // ์กฐํšŒ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (์˜ˆ: lot_number) + targetField: string; // ์ €์žฅ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ (์˜ˆ: lot_number) ๋˜๋Š” "" (์„ ํƒ์•ˆํ•จ) +} + /** * ํ•˜์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ…Œ์ด๋ธ” ์„ค์ • */ @@ -121,6 +141,8 @@ export interface SubDataLookupSettings { linkColumn: string; // ์ƒ์œ„ ๋ฐ์ดํ„ฐ์™€ ์—ฐ๊ฒฐํ•  ์ปฌ๋Ÿผ (์˜ˆ: item_code) displayColumns: string[]; // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ๋“ค (์˜ˆ: ["warehouse_code", "location_code", "quantity"]) columnLabels?: Record; // ์ปฌ๋Ÿผ ๋ผ๋ฒจ (์˜ˆ: { warehouse_code: "์ฐฝ๊ณ " }) + columnOrder?: string[]; // ์ปฌ๋Ÿผ ํ‘œ์‹œ ์ˆœ์„œ (์—†์œผ๋ฉด displayColumns ์ˆœ์„œ ์‚ฌ์šฉ) + fieldMappings?: SubDataFieldMapping[]; // ์„ ํƒ ๋ฐ์ดํ„ฐ ์ €์žฅ ๋งคํ•‘ (์กฐํšŒ ์ปฌ๋Ÿผ โ†’ ์ €์žฅ ์ปฌ๋Ÿผ) additionalFilters?: Record; // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด }