Compare commits
No commits in common. "fbd7e89c8afc43595b763e2708c10ac23d976a23" and "97256f8db0d50a3be6e435e4061c749b7bafab08" have entirely different histories.
fbd7e89c8a
...
97256f8db0
|
|
@ -606,7 +606,7 @@ router.get(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query;
|
const { enableEntityJoin, groupByColumns } = req.query;
|
||||||
const enableEntityJoinFlag =
|
const enableEntityJoinFlag =
|
||||||
enableEntityJoin === "true" ||
|
enableEntityJoin === "true" ||
|
||||||
(typeof enableEntityJoin === "boolean" && enableEntityJoin);
|
(typeof enableEntityJoin === "boolean" && enableEntityJoin);
|
||||||
|
|
@ -626,22 +626,17 @@ 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 옵션 + 그룹핑 옵션 + Primary Key 컬럼 포함)
|
// 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 포함)
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -490,8 +490,7 @@ 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 {
|
||||||
// 테이블 접근 검증
|
// 테이블 접근 검증
|
||||||
|
|
@ -500,30 +499,20 @@ class DataService {
|
||||||
return validation.error!;
|
return validation.error!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 클라이언트에서 전달한 Primary Key 컬럼이 있으면 우선 사용
|
// Primary Key 컬럼 찾기
|
||||||
let pkColumn = primaryKeyColumn || "";
|
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 = "id"; // 기본값
|
||||||
if (!pkColumn) {
|
if (pkResult.length > 0) {
|
||||||
const pkResult = await query<{ attname: string }>(
|
pkColumn = pkResult[0].attname;
|
||||||
`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]
|
|
||||||
);
|
|
||||||
|
|
||||||
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이 활성화된 경우
|
// 🆕 Entity Join이 활성화된 경우
|
||||||
if (enableEntityJoin) {
|
if (enableEntityJoin) {
|
||||||
const { EntityJoinService } = await import("./entityJoinService");
|
const { EntityJoinService } = await import("./entityJoinService");
|
||||||
|
|
|
||||||
|
|
@ -374,9 +374,8 @@ 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, primaryKeyColumn });
|
console.log("📋 URL 파라미터 확인:", { mode, editId, tableName, groupByColumnsParam });
|
||||||
|
|
||||||
// 수정 모드이고 editId가 있으면 해당 레코드 조회
|
// 수정 모드이고 editId가 있으면 해당 레코드 조회
|
||||||
if (mode === "edit" && editId && tableName) {
|
if (mode === "edit" && editId && tableName) {
|
||||||
|
|
@ -415,11 +414,6 @@ 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}`,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ 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,
|
||||||
|
|
@ -38,8 +37,6 @@ export function EntitySearchInputComponent({
|
||||||
formData,
|
formData,
|
||||||
// 다중선택 props
|
// 다중선택 props
|
||||||
multiple: multipleProp,
|
multiple: multipleProp,
|
||||||
// 자동 채움 매핑 props
|
|
||||||
autoFillMappings: autoFillMappingsProp,
|
|
||||||
// 추가 props
|
// 추가 props
|
||||||
component,
|
component,
|
||||||
isInteractive,
|
isInteractive,
|
||||||
|
|
@ -50,7 +47,6 @@ 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";
|
||||||
|
|
@ -59,18 +55,6 @@ export function EntitySearchInputComponent({
|
||||||
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;
|
||||||
// cascadingParentField: ConfigPanel에서 저장되는 필드명
|
// cascadingParentField: ConfigPanel에서 저장되는 필드명
|
||||||
|
|
@ -325,23 +309,6 @@ 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} 값이 없음`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 다중선택 모드에서 개별 항목 제거
|
// 다중선택 모드에서 개별 항목 제거
|
||||||
|
|
@ -469,7 +436,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] ?? `option-${index}`}
|
key={option[valueField] || 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"
|
||||||
|
|
@ -542,7 +509,7 @@ export function EntitySearchInputComponent({
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{effectiveOptions.map((option, index) => (
|
{effectiveOptions.map((option, index) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option[valueField] ?? `select-option-${index}`}
|
key={option[valueField] || 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"
|
||||||
|
|
|
||||||
|
|
@ -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, AutoFillMapping } from "./config";
|
import { EntitySearchInputConfig } 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,7 +236,6 @@ export function EntitySearchInputConfigPanel({
|
||||||
const newConfig = { ...localConfig, ...updates };
|
const newConfig = { ...localConfig, ...updates };
|
||||||
setLocalConfig(newConfig);
|
setLocalConfig(newConfig);
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
console.log("📝 [EntitySearchInput] 설정 업데이트:", { updates, newConfig });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 연쇄 드롭다운 활성화/비활성화
|
// 연쇄 드롭다운 활성화/비활성화
|
||||||
|
|
@ -637,9 +636,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, idx) => (
|
{tableColumns.map((column) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={column.columnName || `display-col-${idx}`}
|
key={column.columnName}
|
||||||
value={`${column.displayName || column.columnName}-${column.columnName}`}
|
value={`${column.displayName || column.columnName}-${column.columnName}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
updateConfig({ displayField: column.columnName });
|
updateConfig({ displayField: column.columnName });
|
||||||
|
|
@ -691,9 +690,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, idx) => (
|
{tableColumns.map((column) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={column.columnName || `value-col-${idx}`}
|
key={column.columnName}
|
||||||
value={`${column.displayName || column.columnName}-${column.columnName}`}
|
value={`${column.displayName || column.columnName}-${column.columnName}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
updateConfig({ valueField: column.columnName });
|
updateConfig({ valueField: column.columnName });
|
||||||
|
|
@ -813,8 +812,8 @@ export function EntitySearchInputConfigPanel({
|
||||||
<SelectValue placeholder="컬럼 선택" />
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tableColumns.map((col, colIdx) => (
|
{tableColumns.map((col) => (
|
||||||
<SelectItem key={col.columnName || `modal-col-${colIdx}`} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.displayName || col.columnName}
|
{col.displayName || col.columnName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -861,8 +860,8 @@ export function EntitySearchInputConfigPanel({
|
||||||
<SelectValue placeholder="필드 선택" />
|
<SelectValue placeholder="필드 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tableColumns.map((col, colIdx) => (
|
{tableColumns.map((col) => (
|
||||||
<SelectItem key={col.columnName || `search-col-${colIdx}`} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.displayName || col.columnName}
|
{col.displayName || col.columnName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -920,8 +919,8 @@ export function EntitySearchInputConfigPanel({
|
||||||
<SelectValue placeholder="필드 선택" />
|
<SelectValue placeholder="필드 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tableColumns.map((col, colIdx) => (
|
{tableColumns.map((col) => (
|
||||||
<SelectItem key={col.columnName || `additional-col-${colIdx}`} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
{col.displayName || col.columnName}
|
{col.displayName || col.columnName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|
@ -940,105 +939,6 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,16 +38,12 @@ 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,
|
||||||
displayField,
|
displayField,
|
||||||
valueField,
|
valueField,
|
||||||
uiMode,
|
uiMode,
|
||||||
multiple,
|
multiple,
|
||||||
autoFillMappings,
|
|
||||||
value,
|
value,
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
|
@ -72,7 +68,6 @@ 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}
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,9 @@ export function EntitySearchModal({
|
||||||
선택
|
선택
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
{displayColumns.map((col, colIdx) => (
|
{displayColumns.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col || `header-${colIdx}`}
|
key={col}
|
||||||
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,8 +179,7 @@ export function EntitySearchModal({
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
results.map((item, index) => {
|
results.map((item, index) => {
|
||||||
// null과 undefined 모두 체크하여 유니크 키 생성
|
const uniqueKey = item[valueField] !== undefined ? `${item[valueField]}` : `row-${index}`;
|
||||||
const uniqueKey = item[valueField] != null ? `${item[valueField]}` : `row-${index}`;
|
|
||||||
const isSelected = isItemSelected(item);
|
const isSelected = isItemSelected(item);
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
|
|
@ -201,8 +200,8 @@ export function EntitySearchModal({
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{displayColumns.map((col, colIdx) => (
|
{displayColumns.map((col) => (
|
||||||
<td key={`${uniqueKey}-${col || colIdx}`} className="px-4 py-2">
|
<td key={`${uniqueKey}-${col}`} className="px-4 py-2">
|
||||||
{item[col] || "-"}
|
{item[col] || "-"}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,3 @@
|
||||||
// 자동 채움 매핑 타입
|
|
||||||
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;
|
||||||
|
|
@ -24,8 +18,5 @@ export interface EntitySearchInputConfig {
|
||||||
cascadingRelationCode?: string; // 연쇄관계 코드 (WAREHOUSE_LOCATION 등)
|
cascadingRelationCode?: string; // 연쇄관계 코드 (WAREHOUSE_LOCATION 등)
|
||||||
cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
|
cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
|
||||||
cascadingParentField?: string; // 부모 필드의 컬럼명 (자식 역할일 때만 사용)
|
cascadingParentField?: string; // 부모 필드의 컬럼명 (자식 역할일 때만 사용)
|
||||||
|
|
||||||
// 자동 채움 매핑 설정
|
|
||||||
autoFillMappings?: AutoFillMapping[]; // 엔티티 선택 시 다른 필드에 자동으로 값 채우기
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1590,40 +1590,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// 커스텀 모달 화면 열기
|
// 커스텀 모달 화면 열기
|
||||||
const rightTableName = componentConfig.rightPanel?.tableName || "";
|
const rightTableName = componentConfig.rightPanel?.tableName || "";
|
||||||
|
|
||||||
// Primary Key 찾기 (우선순위: 설정값 > id > ID > non-null 필드)
|
// Primary Key 찾기 (우선순위: id > ID > 첫 번째 필드)
|
||||||
// 🔧 설정에서 primaryKeyColumn 지정 가능
|
|
||||||
const configuredPrimaryKey = componentConfig.rightPanel?.editButton?.primaryKeyColumn;
|
|
||||||
|
|
||||||
let primaryKeyName = "id";
|
let primaryKeyName = "id";
|
||||||
let primaryKeyValue: any;
|
let primaryKeyValue: any;
|
||||||
|
|
||||||
if (configuredPrimaryKey && item[configuredPrimaryKey] !== undefined && item[configuredPrimaryKey] !== null) {
|
if (item.id !== undefined && item.id !== 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 {
|
||||||
// 🔧 첫 번째 non-null 필드를 Primary Key로 간주
|
// 첫 번째 필드를 Primary Key로 간주
|
||||||
const keys = Object.keys(item);
|
const firstKey = Object.keys(item)[0];
|
||||||
let found = false;
|
primaryKeyName = firstKey;
|
||||||
for (const key of keys) {
|
primaryKeyValue = item[firstKey];
|
||||||
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("✅ 수정 모달 열기:", {
|
||||||
|
|
@ -1648,7 +1629,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
hasGroupByColumns: groupByColumns.length > 0,
|
hasGroupByColumns: groupByColumns.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns + primaryKeyColumn 전달)
|
// ScreenModal 열기 이벤트 발생 (URL 파라미터로 ID + groupByColumns 전달)
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("openScreenModal", {
|
new CustomEvent("openScreenModal", {
|
||||||
detail: {
|
detail: {
|
||||||
|
|
@ -1657,7 +1638,6 @@ 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),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1670,7 +1650,6 @@ 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) : "없음",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue