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/app/(main)/screen/[screenCode]/page.tsx b/frontend/app/(main)/screen/[screenCode]/page.tsx new file mode 100644 index 00000000..0817065e --- /dev/null +++ b/frontend/app/(main)/screen/[screenCode]/page.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { Loader2 } from "lucide-react"; +import { apiClient } from "@/lib/api/client"; + +/** + * /screen/COMPANY_7_167 → /screens/4153 리다이렉트 + * 메뉴 URL이 screenCode 기반이므로, screenId로 변환 후 이동 + */ +export default function ScreenCodeRedirectPage() { + const params = useParams(); + const router = useRouter(); + const screenCode = params.screenCode as string; + + useEffect(() => { + if (!screenCode) return; + + const numericId = parseInt(screenCode); + if (!isNaN(numericId)) { + router.replace(`/screens/${numericId}`); + return; + } + + const resolve = async () => { + try { + const res = await apiClient.get("/screen-management/screens", { + params: { screenCode }, + }); + const screens = res.data?.data || []; + if (screens.length > 0) { + const id = screens[0].screenId || screens[0].screen_id; + router.replace(`/screens/${id}`); + } else { + router.replace("/"); + } + } catch { + router.replace("/"); + } + }; + resolve(); + }, [screenCode, router]); + + return ( +
+ +
+ ); +} 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 949cd74b..c2be4bb4 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -93,9 +93,12 @@ export const SelectedItemsDetailInputComponent: React.FC { + // sourceKeyField는 config에서 직접 지정 (ConfigPanel 자동 감지에서 설정됨) + return componentConfig.sourceKeyField || "item_id"; + }, [componentConfig.sourceKeyField]); // 🆕 dataSourceId 우선순위: URL 파라미터 > 컴포넌트 설정 > component.id const dataSourceId = useMemo( @@ -472,10 +475,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; @@ -530,7 +539,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 - ))} + ); + })}