"use client"; import React, { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { ComponentRendererProps } from "@/types/component"; import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition } from "./types"; import { useModalDataStore, ModalDataItem } from "@/stores/modalDataStore"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { X } from "lucide-react"; import { commonCodeApi } from "@/lib/api/commonCode"; import { cn } from "@/lib/utils"; export interface SelectedItemsDetailInputComponentProps extends ComponentRendererProps { config?: SelectedItemsDetailInputConfig; } /** * SelectedItemsDetailInput 컴포넌트 * 선택된 항목들의 상세 정보를 입력하는 컴포넌트 */ export const SelectedItemsDetailInputComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, isInteractive = false, onClick, onDragStart, onDragEnd, config, className, style, formData, onFormDataChange, screenId, ...props }) => { // 🆕 URL 파라미터에서 dataSourceId 읽기 const searchParams = useSearchParams(); const urlDataSourceId = searchParams?.get("dataSourceId") || undefined; // 컴포넌트 설정 const componentConfig = useMemo(() => ({ dataSourceId: component.id || "default", displayColumns: [], additionalFields: [], layout: "grid", showIndex: true, allowRemove: false, emptyMessage: "전달받은 데이터가 없습니다.", targetTable: "", ...config, ...component.config, } as SelectedItemsDetailInputConfig), [config, component.config, component.id]); // 🆕 dataSourceId 우선순위: URL 파라미터 > 컴포넌트 설정 > component.id const dataSourceId = useMemo( () => urlDataSourceId || componentConfig.dataSourceId || component.id || "default", [urlDataSourceId, componentConfig.dataSourceId, component.id] ); // 디버깅 로그 useEffect(() => { console.log("📍 [SelectedItemsDetailInput] dataSourceId 결정:", { urlDataSourceId, configDataSourceId: componentConfig.dataSourceId, componentId: component.id, finalDataSourceId: dataSourceId, }); }, [urlDataSourceId, componentConfig.dataSourceId, component.id, dataSourceId]); // 전체 레지스트리를 가져와서 컴포넌트 내부에서 필터링 (캐싱 문제 회피) const dataRegistry = useModalDataStore((state) => state.dataRegistry); const modalData = useMemo( () => dataRegistry[dataSourceId] || [], [dataRegistry, dataSourceId] ); const updateItemData = useModalDataStore((state) => state.updateItemData); // 로컬 상태로 데이터 관리 const [items, setItems] = useState([]); // 🆕 코드 카테고리별 옵션 캐싱 const [codeOptions, setCodeOptions] = useState>>({}); // 🆕 필드에 codeCategory가 있으면 자동으로 옵션 로드 useEffect(() => { const loadCodeOptions = async () => { // 🆕 code/category 타입 필드 + codeCategory가 있는 필드 모두 처리 const codeFields = componentConfig.additionalFields?.filter( (field) => field.inputType === "code" || field.inputType === "category" ); if (!codeFields || codeFields.length === 0) return; const newOptions: Record> = { ...codeOptions }; // 🆕 대상 테이블의 컬럼 메타데이터에서 codeCategory 가져오기 const targetTable = componentConfig.targetTable; let targetTableColumns: any[] = []; if (targetTable) { try { const { tableTypeApi } = await import("@/lib/api/screen"); const columnsResponse = await tableTypeApi.getColumns(targetTable); targetTableColumns = columnsResponse || []; } catch (error) { console.error("❌ 대상 테이블 컬럼 조회 실패:", error); } } for (const field of codeFields) { // 이미 codeCategory가 있으면 사용 let codeCategory = field.codeCategory; // 🆕 codeCategory가 없으면 대상 테이블 컬럼에서 찾기 if (!codeCategory && targetTableColumns.length > 0) { const columnMeta = targetTableColumns.find( (col: any) => (col.columnName || col.column_name) === field.name ); if (columnMeta) { codeCategory = columnMeta.codeCategory || columnMeta.code_category; console.log(`🔍 필드 "${field.name}"의 codeCategory를 메타데이터에서 찾음:`, codeCategory); } } if (!codeCategory) { console.warn(`⚠️ 필드 "${field.name}"의 codeCategory를 찾을 수 없습니다`); continue; } // 이미 로드된 옵션이면 스킵 if (newOptions[codeCategory]) continue; try { const response = await commonCodeApi.options.getOptions(codeCategory); if (response.success && response.data) { newOptions[codeCategory] = response.data.map((opt) => ({ label: opt.label, value: opt.value, })); console.log(`✅ 코드 옵션 로드 완료: ${codeCategory}`, newOptions[codeCategory]); } } catch (error) { console.error(`❌ 코드 옵션 로드 실패: ${codeCategory}`, error); } } setCodeOptions(newOptions); }; loadCodeOptions(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [componentConfig.additionalFields, componentConfig.targetTable]); // 모달 데이터가 변경되면 로컬 상태 업데이트 useEffect(() => { if (modalData && modalData.length > 0) { console.log("📦 [SelectedItemsDetailInput] 데이터 수신:", modalData); setItems(modalData); // formData에도 반영 (초기 로드 시에만) if (onFormDataChange && items.length === 0) { onFormDataChange({ [component.id || "selected_items"]: modalData }); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modalData, component.id]); // onFormDataChange는 의존성에서 제외 // 스타일 계산 const componentStyle: React.CSSProperties = { width: "100%", height: "100%", ...component.style, ...style, }; // 디자인 모드 스타일 if (isDesignMode) { componentStyle.border = "1px dashed #cbd5e1"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; componentStyle.padding = "16px"; componentStyle.borderRadius = "8px"; } // 이벤트 핸들러 const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(); }; // 필드 값 변경 핸들러 const handleFieldChange = useCallback((itemId: string | number, fieldName: string, value: any) => { // 상태 업데이트 setItems((prevItems) => { const updatedItems = prevItems.map((item) => item.id === itemId ? { ...item, additionalData: { ...item.additionalData, [fieldName]: value, }, } : item ); // formData에도 반영 (디바운스 없이 즉시 반영) if (onFormDataChange) { onFormDataChange({ [component.id || "selected_items"]: updatedItems }); } return updatedItems; }); // 스토어에도 업데이트 updateItemData(dataSourceId, itemId, { [fieldName]: value }); }, [dataSourceId, updateItemData, onFormDataChange, component.id]); // 항목 제거 핸들러 const handleRemoveItem = (itemId: string | number) => { setItems((prevItems) => prevItems.filter((item) => item.id !== itemId)); }; // 개별 필드 렌더링 const renderField = (field: AdditionalFieldDefinition, item: ModalDataItem) => { const value = item.additionalData?.[field.name] || field.defaultValue || ""; const commonProps = { value: value || "", disabled: componentConfig.disabled || componentConfig.readonly, placeholder: field.placeholder, required: field.required, }; // 🆕 inputType이 있으면 우선 사용, 없으면 field.type 사용 const renderType = field.inputType || field.type; // 🆕 inputType에 따라 적절한 컴포넌트 렌더링 switch (renderType) { // 기본 타입들 case "text": case "varchar": case "char": return ( handleFieldChange(item.id, field.name, e.target.value)} maxLength={field.validation?.maxLength} className="h-8 text-xs sm:h-10 sm:text-sm" /> ); case "number": case "int": case "integer": case "bigint": case "decimal": case "numeric": return ( handleFieldChange(item.id, field.name, e.target.value)} min={field.validation?.min} max={field.validation?.max} className="h-8 text-xs sm:h-10 sm:text-sm" /> ); case "date": case "timestamp": case "datetime": return ( handleFieldChange(item.id, field.name, e.target.value)} className="h-8 text-xs sm:h-10 sm:text-sm" /> ); case "checkbox": case "boolean": case "bool": return ( handleFieldChange(item.id, field.name, checked)} disabled={componentConfig.disabled || componentConfig.readonly} /> ); case "textarea": return (