diff --git a/backend-node/src/database/db.ts b/backend-node/src/database/db.ts
index 4c249ac3..6fc10cf1 100644
--- a/backend-node/src/database/db.ts
+++ b/backend-node/src/database/db.ts
@@ -13,9 +13,13 @@ import {
PoolClient,
QueryResult as PgQueryResult,
QueryResultRow,
+ types,
} from "pg";
import config from "../config/environment";
+// DATE 타입(OID 1082)을 문자열로 반환 (타임존 변환에 의한 -1day 버그 방지)
+types.setTypeParser(1082, (val: string) => val);
+
// PostgreSQL 연결 풀
let pool: Pool | null = null;
diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx
index c2bb436d..1f8b0484 100644
--- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx
+++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx
@@ -67,9 +67,12 @@ export const SelectedItemsDetailInputComponent: React.FC {
+ // sourceKeyField는 config에서 직접 지정 (ConfigPanel 자동 감지에서 설정됨)
+ return componentConfig.sourceKeyField || "item_id";
+ }, [componentConfig.sourceKeyField]);
// 🆕 dataSourceId 우선순위: URL 파라미터 > 컴포넌트 설정 > component.id
const dataSourceId = useMemo(
@@ -446,10 +449,16 @@ export const SelectedItemsDetailInputComponent: React.FC = {};
+
+ // sourceKeyField 자동 매핑 (item_id = originalData.id)
+ if (sourceKeyField && item.originalData?.id) {
+ baseRecord[sourceKeyField] = item.originalData.id;
+ }
+
+ // 나머지 autoFillFrom 필드 (sourceKeyField 제외)
additionalFields.forEach((f) => {
- if (f.autoFillFrom && item.originalData) {
+ if (f.name !== sourceKeyField && f.autoFillFrom && item.originalData) {
const value = item.originalData[f.autoFillFrom];
if (value !== undefined && value !== null) {
baseRecord[f.name] = value;
@@ -504,7 +513,7 @@ export const SelectedItemsDetailInputComponent: React.FC {
- const groupFields = additionalFields.filter((f) => f.groupId === group.id);
- groupFields.forEach((field) => {
- if (field.name === sourceKeyField && field.autoFillFrom && item.originalData) {
- sourceKeyValue = item.originalData[field.autoFillFrom] || null;
- }
- });
- });
- }
-
- // 3순위: fallback (최후의 수단)
+ // 2순위: 원본 데이터의 id를 sourceKeyField 값으로 사용 (신규 등록 모드)
if (!sourceKeyValue && item.originalData) {
sourceKeyValue = item.originalData.id || null;
}
diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx
index 61f755a4..1f70e7e0 100644
--- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx
+++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputConfigPanel.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useMemo, useEffect } from "react";
+import React, { useState, useMemo, useEffect, useRef } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Card, CardContent } from "@/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Plus, X, ChevronDown, ChevronRight } from "lucide-react";
-import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup, DisplayItem, DisplayItemType, EmptyBehavior, DisplayFieldFormat } from "./types";
+import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, FieldGroup, DisplayItem, DisplayItemType, EmptyBehavior, DisplayFieldFormat, AutoDetectedFk } from "./types";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Check, ChevronsUpDown } from "lucide-react";
@@ -97,7 +97,10 @@ export const SelectedItemsDetailInputConfigPanel: React.FC>([]);
- const [loadedTargetTableColumns, setLoadedTargetTableColumns] = useState>([]);
+ const [loadedTargetTableColumns, setLoadedTargetTableColumns] = useState>([]);
+
+ // FK 자동 감지 결과
+ const [autoDetectedFks, setAutoDetectedFks] = useState([]);
// 🆕 원본 테이블 컬럼 로드
useEffect(() => {
@@ -130,10 +133,11 @@ export const SelectedItemsDetailInputConfigPanel: React.FC {
if (!config.targetTable) {
setLoadedTargetTableColumns([]);
+ setAutoDetectedFks([]);
return;
}
@@ -149,7 +153,10 @@ export const SelectedItemsDetailInputConfigPanel: React.FC(() => {
+ if (!config.targetTable || loadedTargetTableColumns.length === 0) return [];
+
+ const entityFkColumns = loadedTargetTableColumns.filter(
+ (col) => col.inputType === "entity" && col.referenceTable
+ );
+ if (entityFkColumns.length === 0) return [];
+
+ return entityFkColumns.map((col) => {
+ let mappingType: "source" | "parent" | "unknown" = "unknown";
+ if (config.sourceTable && col.referenceTable === config.sourceTable) {
+ mappingType = "source";
+ } else if (config.sourceTable && col.referenceTable !== config.sourceTable) {
+ mappingType = "parent";
+ }
+ return {
+ columnName: col.columnName,
+ columnLabel: col.columnLabel,
+ referenceTable: col.referenceTable!,
+ referenceColumn: col.referenceColumn || "id",
+ mappingType,
+ };
+ });
+ }, [config.targetTable, config.sourceTable, loadedTargetTableColumns]);
+
+ // 감지 결과를 state에 반영
+ useEffect(() => {
+ setAutoDetectedFks(detectedFks);
+ }, [detectedFks]);
+
+ // 자동 매핑 적용 (최초 1회만, targetTable 변경 시 리셋)
+ useEffect(() => {
+ fkAutoAppliedRef.current = false;
+ }, [config.targetTable]);
+
+ useEffect(() => {
+ if (fkAutoAppliedRef.current || detectedFks.length === 0) return;
+
+ const sourceFk = detectedFks.find((fk) => fk.mappingType === "source");
+ const parentFks = detectedFks.filter((fk) => fk.mappingType === "parent");
+ let changed = false;
+
+ // sourceKeyField 자동 설정
+ if (sourceFk && !config.sourceKeyField) {
+ console.log("🔗 sourceKeyField 자동 설정:", sourceFk.columnName);
+ handleChange("sourceKeyField", sourceFk.columnName);
+ changed = true;
+ }
+
+ // parentDataMapping 자동 생성 (기존에 없을 때만)
+ if (parentFks.length > 0 && (!config.parentDataMapping || config.parentDataMapping.length === 0)) {
+ const autoMappings = parentFks.map((fk) => ({
+ sourceTable: fk.referenceTable,
+ sourceField: "id",
+ targetField: fk.columnName,
+ }));
+ console.log("🔗 parentDataMapping 자동 생성:", autoMappings);
+ handleChange("parentDataMapping", autoMappings);
+ changed = true;
+ }
+
+ if (changed) {
+ fkAutoAppliedRef.current = true;
+ }
+ }, [detectedFks]);
+
// 🆕 필드 그룹 변경 시 로컬 입력 상태 동기화
useEffect(() => {
setLocalFieldGroups(config.fieldGroups || []);
@@ -898,6 +975,37 @@ export const SelectedItemsDetailInputConfigPanel: React.FC최종 데이터를 저장할 테이블
+ {/* FK 자동 감지 결과 표시 */}
+ {autoDetectedFks.length > 0 && (
+
+
+ FK 자동 감지됨 ({autoDetectedFks.length}건)
+
+
+ {autoDetectedFks.map((fk) => (
+
+
+ {fk.mappingType === "source" ? "원본" : fk.mappingType === "parent" ? "부모" : "미분류"}
+
+ {fk.columnName}
+ ->
+ {fk.referenceTable}
+
+ ))}
+
+
+ 엔티티 설정 기반 자동 매핑. sourceKeyField와 parentDataMapping이 자동으로 설정됩니다.
+
+
+ )}
+
{/* 표시할 원본 데이터 컬럼 */}
@@ -961,7 +1069,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC
- {localFields.map((field, index) => (
+ {localFields.map((field, index) => {
+ return (
@@ -1255,7 +1364,8 @@ export const SelectedItemsDetailInputConfigPanel: React.FC
- ))}
+ );
+ })}