Compare commits

...

4 Commits

Author SHA1 Message Date
hjlee fbd7e89c8a Merge pull request 'lhj' (#375) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/375
2026-01-21 11:41:35 +09:00
leeheejin 070ca254a2 Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj
; Please enter a commit message to explain why this merge is necessary,
; especially if it merges an updated upstream into a topic branch.
;
; Lines starting with ';' will be ignored, and an empty message aborts
; the commit.
2026-01-21 11:41:06 +09:00
leeheejin 29a4ab7b9d entity-search-iniput 수정 2026-01-21 11:40:47 +09:00
leeheejin e8fc664352 fix: 분할패널 수정 버튼 클릭 시 데이터 불러오기 오류 수정
- Primary Key 컬럼명을 프론트엔드에서 백엔드로 전달하도록 개선
- 백엔드 자동 감지 실패 시에도 클라이언트 제공 값 우선 사용
- Primary Key 찾기 로직 개선 (설정값 > id > ID > non-null 필드)
2026-01-21 10:32:37 +09:00
9 changed files with 232 additions and 41 deletions

View File

@ -606,7 +606,7 @@ router.get(
}); });
} }
const { enableEntityJoin, groupByColumns } = req.query; const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query;
const enableEntityJoinFlag = const enableEntityJoinFlag =
enableEntityJoin === "true" || enableEntityJoin === "true" ||
(typeof enableEntityJoin === "boolean" && enableEntityJoin); (typeof enableEntityJoin === "boolean" && enableEntityJoin);
@ -626,17 +626,22 @@ router.get(
} }
} }
// 🆕 primaryKeyColumn 파싱
const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined;
console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`, { console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`, {
enableEntityJoin: enableEntityJoinFlag, enableEntityJoin: enableEntityJoinFlag,
groupByColumns: groupByColumnsArray, groupByColumns: groupByColumnsArray,
primaryKeyColumn: primaryKeyColumnStr,
}); });
// 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 포함) // 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 + Primary Key 컬럼 포함)
const result = await dataService.getRecordDetail( const result = await dataService.getRecordDetail(
tableName, tableName,
id, id,
enableEntityJoinFlag, enableEntityJoinFlag,
groupByColumnsArray groupByColumnsArray,
primaryKeyColumnStr // 🆕 Primary Key 컬럼명 전달
); );
if (!result.success) { if (!result.success) {

View File

@ -490,7 +490,8 @@ class DataService {
tableName: string, tableName: string,
id: string | number, id: string | number,
enableEntityJoin: boolean = false, enableEntityJoin: boolean = false,
groupByColumns: string[] = [] groupByColumns: string[] = [],
primaryKeyColumn?: string // 🆕 클라이언트에서 전달한 Primary Key 컬럼명
): Promise<ServiceResponse<any>> { ): Promise<ServiceResponse<any>> {
try { try {
// 테이블 접근 검증 // 테이블 접근 검증
@ -499,20 +500,30 @@ class DataService {
return validation.error!; return validation.error!;
} }
// Primary Key 컬럼 찾기 // 🆕 클라이언트에서 전달한 Primary Key 컬럼이 있으면 우선 사용
const pkResult = await query<{ attname: string }>( let pkColumn = primaryKeyColumn || "";
`SELECT a.attname
FROM pg_index i // Primary Key 컬럼이 없으면 자동 감지
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) if (!pkColumn) {
WHERE i.indrelid = $1::regclass AND i.indisprimary`, const pkResult = await query<{ attname: string }>(
[tableName] `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"; // 기본값 pkColumn = "id"; // 기본값
if (pkResult.length > 0) { if (pkResult.length > 0) {
pkColumn = pkResult[0].attname; 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이 활성화된 경우 // 🆕 Entity Join이 활성화된 경우
if (enableEntityJoin) { if (enableEntityJoin) {
const { EntityJoinService } = await import("./entityJoinService"); const { EntityJoinService } = await import("./entityJoinService");

View File

@ -374,8 +374,9 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
const editId = urlParams.get("editId"); const editId = urlParams.get("editId");
const tableName = urlParams.get("tableName") || screenInfo.tableName; const tableName = urlParams.get("tableName") || screenInfo.tableName;
const groupByColumnsParam = urlParams.get("groupByColumns"); 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가 있으면 해당 레코드 조회 // 수정 모드이고 editId가 있으면 해당 레코드 조회
if (mode === "edit" && editId && tableName) { if (mode === "edit" && editId && tableName) {
@ -414,6 +415,11 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
params.groupByColumns = JSON.stringify(groupByColumns); params.groupByColumns = JSON.stringify(groupByColumns);
console.log("✅ [ScreenModal] groupByColumns를 params에 추가:", params.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 요청:", { console.log("📡 [ScreenModal] 실제 API 요청:", {
url: `/data/${tableName}/${editId}`, url: `/data/${tableName}/${editId}`,

View File

@ -11,6 +11,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { dynamicFormApi } from "@/lib/api/dynamicForm"; import { dynamicFormApi } from "@/lib/api/dynamicForm";
import { cascadingRelationApi } from "@/lib/api/cascadingRelation"; import { cascadingRelationApi } from "@/lib/api/cascadingRelation";
import { AutoFillMapping } from "./config";
export function EntitySearchInputComponent({ export function EntitySearchInputComponent({
tableName, tableName,
@ -37,6 +38,8 @@ export function EntitySearchInputComponent({
formData, formData,
// 다중선택 props // 다중선택 props
multiple: multipleProp, multiple: multipleProp,
// 자동 채움 매핑 props
autoFillMappings: autoFillMappingsProp,
// 추가 props // 추가 props
component, component,
isInteractive, isInteractive,
@ -47,6 +50,7 @@ export function EntitySearchInputComponent({
isInteractive?: boolean; isInteractive?: boolean;
onFormDataChange?: (fieldName: string, value: any) => void; onFormDataChange?: (fieldName: string, value: any) => void;
webTypeConfig?: any; // 웹타입 설정 (연쇄관계 등) webTypeConfig?: any; // 웹타입 설정 (연쇄관계 등)
autoFillMappings?: AutoFillMapping[]; // 자동 채움 매핑
}) { }) {
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo" // uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete"; const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
@ -54,6 +58,18 @@ export function EntitySearchInputComponent({
// 다중선택 및 연쇄관계 설정 (props > webTypeConfig > componentConfig 순서) // 다중선택 및 연쇄관계 설정 (props > webTypeConfig > componentConfig 순서)
const config = component?.componentConfig || component?.webTypeConfig || {}; const config = component?.componentConfig || component?.webTypeConfig || {};
const isMultiple = multipleProp ?? config.multiple ?? false; 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; const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode;
@ -309,6 +325,23 @@ export function EntitySearchInputComponent({
console.log("📤 EntitySearchInput -> onFormDataChange:", component.columnName, newValue); 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])); const isSelected = selectedValues.includes(String(option[valueField]));
return ( return (
<CommandItem <CommandItem
key={option[valueField] || index} key={option[valueField] ?? `option-${index}`}
value={`${option[displayField] || ""}-${option[valueField] || ""}`} value={`${option[displayField] || ""}-${option[valueField] || ""}`}
onSelect={() => handleSelectOption(option)} onSelect={() => handleSelectOption(option)}
className="text-xs sm:text-sm" className="text-xs sm:text-sm"
@ -509,7 +542,7 @@ export function EntitySearchInputComponent({
<CommandGroup> <CommandGroup>
{effectiveOptions.map((option, index) => ( {effectiveOptions.map((option, index) => (
<CommandItem <CommandItem
key={option[valueField] || index} key={option[valueField] ?? `select-option-${index}`}
value={`${option[displayField] || ""}-${option[valueField] || ""}`} value={`${option[displayField] || ""}-${option[valueField] || ""}`}
onSelect={() => handleSelectOption(option)} onSelect={() => handleSelectOption(option)}
className="text-xs sm:text-sm" className="text-xs sm:text-sm"

View File

@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react"; import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react";
// allComponents는 현재 사용되지 않지만 향후 확장을 위해 props에 유지 // allComponents는 현재 사용되지 않지만 향후 확장을 위해 props에 유지
import { EntitySearchInputConfig } from "./config"; import { EntitySearchInputConfig, AutoFillMapping } from "./config";
import { tableManagementApi } from "@/lib/api/tableManagement"; import { tableManagementApi } from "@/lib/api/tableManagement";
import { tableTypeApi } from "@/lib/api/screen"; import { tableTypeApi } from "@/lib/api/screen";
import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation"; import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation";
@ -236,6 +236,7 @@ export function EntitySearchInputConfigPanel({
const newConfig = { ...localConfig, ...updates }; const newConfig = { ...localConfig, ...updates };
setLocalConfig(newConfig); setLocalConfig(newConfig);
onConfigChange(newConfig); onConfigChange(newConfig);
console.log("📝 [EntitySearchInput] 설정 업데이트:", { updates, newConfig });
}; };
// 연쇄 드롭다운 활성화/비활성화 // 연쇄 드롭다운 활성화/비활성화
@ -636,9 +637,9 @@ export function EntitySearchInputConfigPanel({
<CommandList> <CommandList>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty> <CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup> <CommandGroup>
{tableColumns.map((column) => ( {tableColumns.map((column, idx) => (
<CommandItem <CommandItem
key={column.columnName} key={column.columnName || `display-col-${idx}`}
value={`${column.displayName || column.columnName}-${column.columnName}`} value={`${column.displayName || column.columnName}-${column.columnName}`}
onSelect={() => { onSelect={() => {
updateConfig({ displayField: column.columnName }); updateConfig({ displayField: column.columnName });
@ -690,9 +691,9 @@ export function EntitySearchInputConfigPanel({
<CommandList> <CommandList>
<CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty> <CommandEmpty className="text-xs sm:text-sm"> .</CommandEmpty>
<CommandGroup> <CommandGroup>
{tableColumns.map((column) => ( {tableColumns.map((column, idx) => (
<CommandItem <CommandItem
key={column.columnName} key={column.columnName || `value-col-${idx}`}
value={`${column.displayName || column.columnName}-${column.columnName}`} value={`${column.displayName || column.columnName}-${column.columnName}`}
onSelect={() => { onSelect={() => {
updateConfig({ valueField: column.columnName }); updateConfig({ valueField: column.columnName });
@ -812,8 +813,8 @@ export function EntitySearchInputConfigPanel({
<SelectValue placeholder="컬럼 선택" /> <SelectValue placeholder="컬럼 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{tableColumns.map((col) => ( {tableColumns.map((col, colIdx) => (
<SelectItem key={col.columnName} value={col.columnName}> <SelectItem key={col.columnName || `modal-col-${colIdx}`} value={col.columnName}>
{col.displayName || col.columnName} {col.displayName || col.columnName}
</SelectItem> </SelectItem>
))} ))}
@ -860,8 +861,8 @@ export function EntitySearchInputConfigPanel({
<SelectValue placeholder="필드 선택" /> <SelectValue placeholder="필드 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{tableColumns.map((col) => ( {tableColumns.map((col, colIdx) => (
<SelectItem key={col.columnName} value={col.columnName}> <SelectItem key={col.columnName || `search-col-${colIdx}`} value={col.columnName}>
{col.displayName || col.columnName} {col.displayName || col.columnName}
</SelectItem> </SelectItem>
))} ))}
@ -919,8 +920,8 @@ export function EntitySearchInputConfigPanel({
<SelectValue placeholder="필드 선택" /> <SelectValue placeholder="필드 선택" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{tableColumns.map((col) => ( {tableColumns.map((col, colIdx) => (
<SelectItem key={col.columnName} value={col.columnName}> <SelectItem key={col.columnName || `additional-col-${colIdx}`} value={col.columnName}>
{col.displayName || col.columnName} {col.displayName || col.columnName}
</SelectItem> </SelectItem>
))} ))}
@ -939,6 +940,105 @@ export function EntitySearchInputConfigPanel({
</div> </div>
</div> </div>
)} )}
{/* 자동 채움 매핑 설정 */}
<div className="border-t pt-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4" />
<h4 className="text-sm font-medium"> </h4>
</div>
<Button
size="sm"
variant="outline"
onClick={() => {
const mappings = localConfig.autoFillMappings || [];
updateConfig({ autoFillMappings: [...mappings, { sourceField: "", targetField: "" }] });
}}
className="h-7 text-xs"
disabled={!localConfig.tableName || isLoadingColumns}
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
<p className="text-muted-foreground text-xs">
.
</p>
{(localConfig.autoFillMappings || []).length > 0 && (
<div className="space-y-2">
{(localConfig.autoFillMappings || []).map((mapping, index) => (
<div key={`autofill-mapping-${index}`} className="flex items-center gap-2 rounded-md border p-2 bg-muted/30">
{/* 소스 필드 (선택된 엔티티) */}
<div className="flex-1">
<Label className="text-[10px] text-muted-foreground mb-1 block"> ()</Label>
<Select
value={mapping.sourceField || ""}
onValueChange={(value) => {
const mappings = [...(localConfig.autoFillMappings || [])];
mappings[index] = { ...mappings[index], sourceField: value };
updateConfig({ autoFillMappings: mappings });
}}
disabled={!localConfig.tableName || isLoadingColumns}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col, colIdx) => (
<SelectItem key={col.columnName || `col-${colIdx}`} value={col.columnName}>
{col.displayName || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 화살표 */}
<div className="flex items-center justify-center pt-4">
<span className="text-muted-foreground text-sm"></span>
</div>
{/* 대상 필드 (폼) */}
<div className="flex-1">
<Label className="text-[10px] text-muted-foreground mb-1 block"> ()</Label>
<Input
value={mapping.targetField || ""}
onChange={(e) => {
const mappings = [...(localConfig.autoFillMappings || [])];
mappings[index] = { ...mappings[index], targetField: e.target.value };
updateConfig({ autoFillMappings: mappings });
}}
placeholder="폼 필드명"
className="h-8 text-xs"
/>
</div>
{/* 삭제 버튼 */}
<Button
size="sm"
variant="ghost"
onClick={() => {
const mappings = [...(localConfig.autoFillMappings || [])];
mappings.splice(index, 1);
updateConfig({ autoFillMappings: mappings });
}}
className="h-8 w-8 p-0 mt-4"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{(localConfig.autoFillMappings || []).length === 0 && (
<div className="text-muted-foreground text-xs text-center py-3 rounded-md border border-dashed">
. + .
</div>
)}
</div>
</div> </div>
); );
} }

View File

@ -37,6 +37,9 @@ export const EntitySearchInputWrapper: React.FC<WebTypeComponentProps> = ({
// placeholder // placeholder
const placeholder = config.placeholder || widget?.placeholder || "항목을 선택하세요"; const placeholder = config.placeholder || widget?.placeholder || "항목을 선택하세요";
// 자동 채움 매핑 설정
const autoFillMappings = config.autoFillMappings || [];
console.log("🏢 EntitySearchInputWrapper 렌더링:", { console.log("🏢 EntitySearchInputWrapper 렌더링:", {
tableName, tableName,
@ -44,6 +47,7 @@ export const EntitySearchInputWrapper: React.FC<WebTypeComponentProps> = ({
valueField, valueField,
uiMode, uiMode,
multiple, multiple,
autoFillMappings,
value, value,
config, config,
}); });
@ -68,6 +72,7 @@ export const EntitySearchInputWrapper: React.FC<WebTypeComponentProps> = ({
value={value} value={value}
onChange={onChange} onChange={onChange}
multiple={multiple} multiple={multiple}
autoFillMappings={autoFillMappings}
component={component} component={component}
isInteractive={props.isInteractive} isInteractive={props.isInteractive}
onFormDataChange={props.onFormDataChange} onFormDataChange={props.onFormDataChange}

View File

@ -148,9 +148,9 @@ export function EntitySearchModal({
</th> </th>
)} )}
{displayColumns.map((col) => ( {displayColumns.map((col, colIdx) => (
<th <th
key={col} key={col || `header-${colIdx}`}
className="px-4 py-2 text-left font-medium text-muted-foreground" className="px-4 py-2 text-left font-medium text-muted-foreground"
> >
{col} {col}
@ -179,7 +179,8 @@ export function EntitySearchModal({
</tr> </tr>
) : ( ) : (
results.map((item, index) => { 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); const isSelected = isItemSelected(item);
return ( return (
<tr <tr
@ -200,8 +201,8 @@ export function EntitySearchModal({
/> />
</td> </td>
)} )}
{displayColumns.map((col) => ( {displayColumns.map((col, colIdx) => (
<td key={`${uniqueKey}-${col}`} className="px-4 py-2"> <td key={`${uniqueKey}-${col || colIdx}`} className="px-4 py-2">
{item[col] || "-"} {item[col] || "-"}
</td> </td>
))} ))}

View File

@ -1,3 +1,9 @@
// 자동 채움 매핑 타입
export interface AutoFillMapping {
sourceField: string; // 선택된 엔티티의 필드 (예: customer_name)
targetField: string; // 폼의 필드 (예: partner_name)
}
export interface EntitySearchInputConfig { export interface EntitySearchInputConfig {
tableName: string; tableName: string;
displayField: string; displayField: string;
@ -18,5 +24,8 @@ export interface EntitySearchInputConfig {
cascadingRelationCode?: string; // 연쇄관계 코드 (WAREHOUSE_LOCATION 등) cascadingRelationCode?: string; // 연쇄관계 코드 (WAREHOUSE_LOCATION 등)
cascadingRole?: "parent" | "child"; // 역할 (부모/자식) cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
cascadingParentField?: string; // 부모 필드의 컬럼명 (자식 역할일 때만 사용) cascadingParentField?: string; // 부모 필드의 컬럼명 (자식 역할일 때만 사용)
// 자동 채움 매핑 설정
autoFillMappings?: AutoFillMapping[]; // 엔티티 선택 시 다른 필드에 자동으로 값 채우기
} }

View File

@ -1590,21 +1590,40 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 커스텀 모달 화면 열기 // 커스텀 모달 화면 열기
const rightTableName = componentConfig.rightPanel?.tableName || ""; 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 primaryKeyName = "id";
let primaryKeyValue: any; 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"; primaryKeyName = "id";
primaryKeyValue = item.id; primaryKeyValue = item.id;
} else if (item.ID !== undefined && item.ID !== null) { } else if (item.ID !== undefined && item.ID !== null) {
primaryKeyName = "ID"; primaryKeyName = "ID";
primaryKeyValue = item.ID; primaryKeyValue = item.ID;
} else { } else {
// 첫 번째 필드를 Primary Key로 간주 // 🔧 첫 번째 non-null 필드를 Primary Key로 간주
const firstKey = Object.keys(item)[0]; const keys = Object.keys(item);
primaryKeyName = firstKey; let found = false;
primaryKeyValue = item[firstKey]; 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("✅ 수정 모달 열기:", { console.log("✅ 수정 모달 열기:", {
@ -1629,7 +1648,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
hasGroupByColumns: groupByColumns.length > 0, hasGroupByColumns: groupByColumns.length > 0,
}); });
// ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns 전달) // ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns + primaryKeyColumn 전달)
window.dispatchEvent( window.dispatchEvent(
new CustomEvent("openScreenModal", { new CustomEvent("openScreenModal", {
detail: { detail: {
@ -1638,6 +1657,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
mode: "edit", mode: "edit",
editId: primaryKeyValue, editId: primaryKeyValue,
tableName: rightTableName, tableName: rightTableName,
primaryKeyColumn: primaryKeyName, // 🆕 Primary Key 컬럼명 전달
...(groupByColumns.length > 0 && { ...(groupByColumns.length > 0 && {
groupByColumns: JSON.stringify(groupByColumns), groupByColumns: JSON.stringify(groupByColumns),
}), }),
@ -1650,6 +1670,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
screenId: modalScreenId, screenId: modalScreenId,
editId: primaryKeyValue, editId: primaryKeyValue,
tableName: rightTableName, tableName: rightTableName,
primaryKeyColumn: primaryKeyName,
groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음", groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음",
}); });