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/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 44685dc0..fdd104df 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -374,8 +374,9 @@ export const ScreenModal: React.FC = ({ className }) => { const editId = urlParams.get("editId"); const tableName = urlParams.get("tableName") || screenInfo.tableName; const groupByColumnsParam = urlParams.get("groupByColumns"); + const primaryKeyColumn = urlParams.get("primaryKeyColumn"); // πŸ†• Primary Key 컬럼λͺ… - console.log("πŸ“‹ URL νŒŒλΌλ―Έν„° 확인:", { mode, editId, tableName, groupByColumnsParam }); + console.log("πŸ“‹ URL νŒŒλΌλ―Έν„° 확인:", { mode, editId, tableName, groupByColumnsParam, primaryKeyColumn }); // μˆ˜μ • λͺ¨λ“œμ΄κ³  editIdκ°€ 있으면 ν•΄λ‹Ή λ ˆμ½”λ“œ 쑰회 if (mode === "edit" && editId && tableName) { @@ -414,6 +415,11 @@ export const ScreenModal: React.FC = ({ className }) => { params.groupByColumns = JSON.stringify(groupByColumns); console.log("βœ… [ScreenModal] groupByColumnsλ₯Ό params에 μΆ”κ°€:", params.groupByColumns); } + // πŸ†• Primary Key 컬럼λͺ… 전달 (λ°±μ—”λ“œ μžλ™ 감지 μ‹€νŒ¨ μ‹œ μ‚¬μš©) + if (primaryKeyColumn) { + params.primaryKeyColumn = primaryKeyColumn; + console.log("βœ… [ScreenModal] primaryKeyColumn을 params에 μΆ”κ°€:", primaryKeyColumn); + } console.log("πŸ“‘ [ScreenModal] μ‹€μ œ API μš”μ²­:", { url: `/data/${tableName}/${editId}`, 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/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ab387348..9b8e7cf0 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1590,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("βœ… μˆ˜μ • λͺ¨λ‹¬ μ—΄κΈ°:", { @@ -1629,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: { @@ -1638,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), }), @@ -1650,6 +1670,7 @@ export const SplitPanelLayoutComponent: React.FC screenId: modalScreenId, editId: primaryKeyValue, tableName: rightTableName, + primaryKeyColumn: primaryKeyName, groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "μ—†μŒ", });