"use client"; import React, { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { ComponentRendererProps } from "@/types/component"; import { SelectedItemsDetailInputConfig, AdditionalFieldDefinition, ItemData, GroupEntry, DisplayItem } 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 { Badge } from "@/components/ui/badge"; 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 * as LucideIcons 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", inputMode: "inline", // 🆕 기본값 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] ); // 전체 레지스트리를 가져와서 컴포넌트 내부에서 필터링 (캐싱 문제 회피) const dataRegistry = useModalDataStore((state) => state.dataRegistry); const modalData = useMemo( () => dataRegistry[dataSourceId] || [], [dataRegistry, dataSourceId] ); const updateItemData = useModalDataStore((state) => state.updateItemData); // 🆕 새로운 데이터 구조: 품목별로 여러 개의 상세 데이터 const [items, setItems] = useState([]); // 🆕 입력 모드 상태 (modal 모드일 때 사용) const [isEditing, setIsEditing] = useState(false); const [editingItemId, setEditingItemId] = useState(null); // 현재 편집 중인 품목 ID const [editingGroupId, setEditingGroupId] = useState(null); // 현재 편집 중인 그룹 ID const [editingDetailId, setEditingDetailId] = useState(null); // 현재 편집 중인 항목 ID // 🆕 코드 카테고리별 옵션 캐싱 const [codeOptions, setCodeOptions] = useState>>({}); // 디버깅 로그 useEffect(() => { console.log("📍 [SelectedItemsDetailInput] 설정 확인:", { inputMode: componentConfig.inputMode, urlDataSourceId, configDataSourceId: componentConfig.dataSourceId, componentId: component.id, finalDataSourceId: dataSourceId, isEditing, editingItemId, }); }, [urlDataSourceId, componentConfig.dataSourceId, component.id, dataSourceId, componentConfig.inputMode, isEditing, editingItemId]); // 🆕 필드에 codeCategory가 있으면 자동으로 옵션 로드 useEffect(() => { const loadCodeOptions = async () => { console.log("🔄 [loadCodeOptions] 시작:", { additionalFields: componentConfig.additionalFields, targetTable: componentConfig.targetTable, }); // 🆕 code/category 타입 필드 + codeCategory가 있는 필드 모두 처리 const codeFields = componentConfig.additionalFields?.filter( (field) => field.inputType === "code" || field.inputType === "category" ); console.log("🔍 [loadCodeOptions] code/category 필드:", codeFields); if (!codeFields || codeFields.length === 0) { console.log("⚠️ [loadCodeOptions] code/category 타입 필드가 없습니다"); 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]); // 🆕 모달 데이터를 ItemData 구조로 변환 (그룹별 구조) useEffect(() => { if (modalData && modalData.length > 0) { console.log("📦 [SelectedItemsDetailInput] 데이터 수신:", modalData); // 🆕 각 품목마다 빈 fieldGroups 객체를 가진 ItemData 생성 const groups = componentConfig.fieldGroups || []; const newItems: ItemData[] = modalData.map((item) => { const fieldGroups: Record = {}; // 각 그룹에 대해 빈 배열 초기화 groups.forEach((group) => { fieldGroups[group.id] = []; }); // 그룹이 없으면 기본 그룹 생성 if (groups.length === 0) { fieldGroups["default"] = []; } // 🔧 modalData의 구조 확인: item.originalData가 있으면 그것을 사용, 없으면 item 자체를 사용 const actualData = (item as any).originalData || item; return { id: String(item.id), originalData: actualData, // 🔧 실제 데이터 추출 fieldGroups, }; }); setItems(newItems); console.log("✅ [SelectedItemsDetailInput] items 설정 완료:", { itemsLength: newItems.length, groups: groups.map(g => g.id), firstItem: newItems[0], }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [modalData, component.id, componentConfig.fieldGroups]); // onFormDataChange는 의존성에서 제외 // 🆕 저장 요청 시에만 데이터 전달 (이벤트 리스너 방식) useEffect(() => { const handleSaveRequest = () => { if (items.length > 0 && onFormDataChange) { const dataToSave = { [component.id || "selected_items"]: items }; console.log("📝 [SelectedItemsDetailInput] 저장 요청 시 데이터 전달:", dataToSave); onFormDataChange(dataToSave); } }; // 저장 버튼 클릭 시 데이터 수집 window.addEventListener("beforeFormSave", handleSaveRequest); return () => { window.removeEventListener("beforeFormSave", handleSaveRequest); }; }, [items, 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?.(); }; // 🆕 그룹별 필드 변경 핸들러: itemId + groupId + entryId + fieldName const handleFieldChange = useCallback((itemId: string, groupId: string, entryId: string, fieldName: string, value: any) => { setItems((prevItems) => { return prevItems.map((item) => { if (item.id !== itemId) return item; const groupEntries = item.fieldGroups[groupId] || []; const existingEntryIndex = groupEntries.findIndex((e) => e.id === entryId); if (existingEntryIndex >= 0) { // 기존 entry 업데이트 (항상 이 경로로만 진입) const updatedEntries = [...groupEntries]; updatedEntries[existingEntryIndex] = { ...updatedEntries[existingEntryIndex], [fieldName]: value, }; return { ...item, fieldGroups: { ...item.fieldGroups, [groupId]: updatedEntries, }, }; } else { // 이 경로는 발생하면 안 됨 (handleAddGroupEntry에서 미리 추가함) console.warn("⚠️ entry가 없는데 handleFieldChange 호출됨:", { itemId, groupId, entryId }); return item; } }); }); }, []); // 🆕 품목 제거 핸들러 const handleRemoveItem = (itemId: string) => { setItems((prevItems) => prevItems.filter((item) => item.id !== itemId)); }; // 🆕 그룹 항목 추가 핸들러 (특정 그룹에 새 항목 추가) const handleAddGroupEntry = (itemId: string, groupId: string) => { const newEntryId = `entry-${Date.now()}`; // 🔧 미리 빈 entry를 추가하여 리렌더링 방지 setItems((prevItems) => { return prevItems.map((item) => { if (item.id !== itemId) return item; const groupEntries = item.fieldGroups[groupId] || []; const newEntry: GroupEntry = { id: newEntryId }; return { ...item, fieldGroups: { ...item.fieldGroups, [groupId]: [...groupEntries, newEntry], }, }; }); }); setIsEditing(true); setEditingItemId(itemId); setEditingDetailId(newEntryId); setEditingGroupId(groupId); }; // 🆕 그룹 항목 제거 핸들러 const handleRemoveGroupEntry = (itemId: string, groupId: string, entryId: string) => { setItems((prevItems) => prevItems.map((item) => { if (item.id !== itemId) return item; return { ...item, fieldGroups: { ...item.fieldGroups, [groupId]: (item.fieldGroups[groupId] || []).filter((e) => e.id !== entryId), }, }; }) ); }; // 🆕 그룹 항목 편집 핸들러 (클릭하면 수정 가능) const handleEditGroupEntry = (itemId: string, groupId: string, entryId: string) => { setIsEditing(true); setEditingItemId(itemId); setEditingGroupId(groupId); setEditingDetailId(entryId); }; // 🆕 다음 품목으로 이동 const handleNextItem = () => { const currentIndex = items.findIndex((item) => item.id === editingItemId); if (currentIndex < items.length - 1) { // 다음 품목으로 const nextItem = items[currentIndex + 1]; setEditingItemId(nextItem.id); const groups = componentConfig.fieldGroups || []; const firstGroupId = groups.length > 0 ? groups[0].id : "default"; const newEntryId = `entry-${Date.now()}`; setEditingDetailId(newEntryId); setEditingGroupId(firstGroupId); setIsEditing(true); } else { // 마지막 품목이면 편집 모드 종료 setIsEditing(false); setEditingItemId(null); setEditingDetailId(null); setEditingGroupId(null); } }; // 🆕 개별 필드 렌더링 (itemId, groupId, entryId, entry 데이터 전달) const renderField = (field: AdditionalFieldDefinition, itemId: string, groupId: string, entryId: string, entry: GroupEntry) => { const value = entry[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(itemId, groupId, entryId, 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(itemId, groupId, entryId, 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(itemId, groupId, entryId, field.name, e.target.value)} className="h-8 text-xs sm:h-10 sm:text-sm" /> ); case "checkbox": case "boolean": case "bool": return ( handleFieldChange(itemId, groupId, entryId, field.name, checked)} disabled={componentConfig.disabled || componentConfig.readonly} /> ); case "textarea": return (