"use client"; import React, { useEffect, useState } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Trash2, Loader2, X } from "lucide-react"; import { SimpleRepeaterTableProps, SimpleRepeaterColumnConfig } from "./types"; import { cn } from "@/lib/utils"; import { ComponentRendererProps } from "@/types/component"; import { useCalculation } from "./useCalculation"; import { apiClient } from "@/lib/api/client"; export interface SimpleRepeaterTableComponentProps extends ComponentRendererProps { config?: SimpleRepeaterTableProps; // SimpleRepeaterTableProps의 개별 prop들도 지원 (호환성) value?: any[]; onChange?: (newData: any[]) => void; columns?: SimpleRepeaterColumnConfig[]; calculationRules?: any[]; readOnly?: boolean; showRowNumber?: boolean; allowDelete?: boolean; maxHeight?: string; } export function SimpleRepeaterTableComponent({ // ComponentRendererProps (자동 전달) component, isDesignMode = false, isSelected = false, isInteractive = false, onClick, className, formData, onFormDataChange, // SimpleRepeaterTable 전용 props config, value: propValue, onChange: propOnChange, columns: propColumns, calculationRules: propCalculationRules, readOnly: propReadOnly, showRowNumber: propShowRowNumber, allowDelete: propAllowDelete, maxHeight: propMaxHeight, ...props }: SimpleRepeaterTableComponentProps) { // config 또는 component.config 또는 개별 prop 우선순위로 병합 const componentConfig = { ...config, ...component?.config, }; // config prop 우선, 없으면 개별 prop 사용 const columns = componentConfig?.columns || propColumns || []; const calculationRules = componentConfig?.calculationRules || propCalculationRules || []; const readOnly = componentConfig?.readOnly ?? propReadOnly ?? false; const showRowNumber = componentConfig?.showRowNumber ?? propShowRowNumber ?? true; const allowDelete = componentConfig?.allowDelete ?? propAllowDelete ?? true; const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px"; // value는 formData[columnName] 우선, 없으면 prop 사용 const columnName = component?.columnName; const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; // 🆕 로딩 상태 const [isLoading, setIsLoading] = useState(false); const [loadError, setLoadError] = useState(null); // onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출) const handleChange = (newData: any[]) => { // 기존 onChange 콜백 호출 (호환성) const externalOnChange = componentConfig?.onChange || propOnChange; if (externalOnChange) { externalOnChange(newData); } // onFormDataChange 호출하여 EditModal의 groupData 업데이트 if (onFormDataChange && columnName) { onFormDataChange(columnName, newData); } }; // 계산 hook const { calculateRow, calculateAll } = useCalculation(calculationRules); // 🆕 초기 데이터 로드 useEffect(() => { const loadInitialData = async () => { const initialConfig = componentConfig?.initialDataConfig; if (!initialConfig || !initialConfig.sourceTable) { return; // 초기 데이터 설정이 없으면 로드하지 않음 } setIsLoading(true); setLoadError(null); try { // 필터 조건 생성 const filters: Record = {}; if (initialConfig.filterConditions) { for (const condition of initialConfig.filterConditions) { let filterValue = condition.value; // formData에서 값 가져오기 if (condition.valueFromField && formData) { filterValue = formData[condition.valueFromField]; } filters[condition.field] = filterValue; } } // API 호출 const response = await apiClient.post( `/table-management/tables/${initialConfig.sourceTable}/data`, { search: filters, page: 1, size: 1000, // 대량 조회 } ); if (response.data.success && response.data.data?.data) { const loadedData = response.data.data.data; // 1. 기본 데이터 매핑 (Direct & Manual) const baseMappedData = loadedData.map((row: any) => { const mappedRow: any = { ...row }; // 원본 데이터 유지 (조인 키 참조용) for (const col of columns) { if (col.sourceConfig) { if (col.sourceConfig.type === "direct" && col.sourceConfig.sourceColumn) { mappedRow[col.field] = row[col.sourceConfig.sourceColumn]; } else if (col.sourceConfig.type === "manual") { mappedRow[col.field] = col.defaultValue; } // Join은 2단계에서 처리 } else { mappedRow[col.field] = row[col.field] ?? col.defaultValue; } } return mappedRow; }); // 2. 조인 데이터 처리 const joinColumns = columns.filter( (col) => col.sourceConfig?.type === "join" && col.sourceConfig.joinTable && col.sourceConfig.joinKey ); if (joinColumns.length > 0) { // 조인 테이블별로 그룹화 const joinGroups = new Map(); joinColumns.forEach((col) => { const table = col.sourceConfig!.joinTable!; const key = col.sourceConfig!.joinKey!; // refKey가 없으면 key와 동일하다고 가정 (하위 호환성) const refKey = col.sourceConfig!.joinRefKey || key; const groupKey = `${table}:${key}:${refKey}`; if (!joinGroups.has(groupKey)) { joinGroups.set(groupKey, { key, refKey, cols: [] }); } joinGroups.get(groupKey)!.cols.push(col); }); // 각 그룹별로 데이터 조회 및 병합 await Promise.all( Array.from(joinGroups.entries()).map(async ([groupKey, { key, refKey, cols }]) => { const [tableName] = groupKey.split(":"); // 조인 키 값 수집 (중복 제거) const keyValues = Array.from(new Set( baseMappedData .map((row: any) => row[key]) .filter((v: any) => v !== undefined && v !== null) )); if (keyValues.length === 0) return; try { // 조인 테이블 조회 // refKey(타겟 테이블 컬럼)로 검색 const response = await apiClient.post( `/table-management/tables/${tableName}/data`, { search: { [refKey]: keyValues }, // { id: [1, 2, 3] } page: 1, size: 1000, } ); if (response.data.success && response.data.data?.data) { const joinedRows = response.data.data.data; // 조인 데이터 맵 생성 (refKey -> row) const joinMap = new Map(joinedRows.map((r: any) => [r[refKey], r])); // 데이터 병합 baseMappedData.forEach((row: any) => { const keyValue = row[key]; const joinedRow = joinMap.get(keyValue); if (joinedRow) { cols.forEach((col) => { if (col.sourceConfig?.joinColumn) { row[col.field] = joinedRow[col.sourceConfig.joinColumn]; } }); } }); } } catch (error) { console.error(`조인 실패 (${tableName}):`, error); // 실패 시 무시하고 진행 (값은 undefined) } }) ); } const mappedData = baseMappedData; // 계산 필드 적용 const calculatedData = calculateAll(mappedData); handleChange(calculatedData); } } catch (error: any) { console.error("초기 데이터 로드 실패:", error); setLoadError(error.message || "데이터를 불러올 수 없습니다"); } finally { setIsLoading(false); } }; loadInitialData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [componentConfig?.initialDataConfig]); // 초기 데이터에 계산 필드 적용 useEffect(() => { if (value.length > 0 && calculationRules.length > 0) { const calculated = calculateAll(value); // 값이 실제로 변경된 경우만 업데이트 if (JSON.stringify(calculated) !== JSON.stringify(value)) { handleChange(calculated); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 🆕 저장 요청 시 테이블별로 데이터 그룹화 (beforeFormSave 이벤트 리스너) useEffect(() => { const handleSaveRequest = async (event: Event) => { if (value.length === 0) { console.warn("⚠️ [SimpleRepeaterTable] 저장할 데이터 없음"); return; } // 🆕 테이블별로 데이터 그룹화 const dataByTable: Record = {}; for (const row of value) { // 각 행의 데이터를 테이블별로 분리 for (const col of columns) { // 저장 설정이 있고 저장이 활성화된 경우에만 if (col.targetConfig && col.targetConfig.targetTable && col.targetConfig.saveEnabled !== false) { const targetTable = col.targetConfig.targetTable; const targetColumn = col.targetConfig.targetColumn || col.field; // 테이블 그룹 초기화 if (!dataByTable[targetTable]) { dataByTable[targetTable] = []; } // 해당 테이블의 데이터 찾기 또는 생성 let tableRow = dataByTable[targetTable].find((r: any) => r._rowIndex === row._rowIndex); if (!tableRow) { tableRow = { _rowIndex: row._rowIndex }; dataByTable[targetTable].push(tableRow); } // 컬럼 값 저장 tableRow[targetColumn] = row[col.field]; } } } // _rowIndex 제거 Object.keys(dataByTable).forEach((tableName) => { dataByTable[tableName] = dataByTable[tableName].map((row: any) => { const { _rowIndex, ...rest } = row; return rest; }); }); console.log("✅ [SimpleRepeaterTable] 테이블별 저장 데이터:", dataByTable); // CustomEvent의 detail에 테이블별 데이터 추가 if (event instanceof CustomEvent && event.detail) { // 각 테이블별로 데이터 전달 Object.entries(dataByTable).forEach(([tableName, rows]) => { const key = `${columnName || component?.id}_${tableName}`; event.detail.formData[key] = rows.map((row: any) => ({ ...row, _targetTable: tableName, })); }); console.log("✅ [SimpleRepeaterTable] 저장 데이터 준비:", { tables: Object.keys(dataByTable), totalRows: Object.values(dataByTable).reduce((sum, rows) => sum + rows.length, 0), }); } // 기존 onFormDataChange도 호출 (호환성) if (onFormDataChange && columnName) { // 테이블별 데이터를 통합하여 전달 onFormDataChange(columnName, Object.entries(dataByTable).flatMap(([table, rows]) => rows.map((row: any) => ({ ...row, _targetTable: table })) )); } }; // 저장 버튼 클릭 시 데이터 수집 window.addEventListener("beforeFormSave", handleSaveRequest as EventListener); return () => { window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener); }; }, [value, columns, columnName, component?.id, onFormDataChange]); const handleCellEdit = (rowIndex: number, field: string, cellValue: any) => { const newRow = { ...value[rowIndex], [field]: cellValue }; // 계산 필드 업데이트 const calculatedRow = calculateRow(newRow); const newData = [...value]; newData[rowIndex] = calculatedRow; handleChange(newData); }; const handleRowDelete = (rowIndex: number) => { const newData = value.filter((_, i) => i !== rowIndex); handleChange(newData); }; const renderCell = ( row: any, column: SimpleRepeaterColumnConfig, rowIndex: number ) => { const cellValue = row[column.field]; // 계산 필드는 편집 불가 if (column.calculated || !column.editable || readOnly) { return (
{column.type === "number" ? typeof cellValue === "number" ? cellValue.toLocaleString() : cellValue || "0" : cellValue || "-"}
); } // 편집 가능한 필드 switch (column.type) { case "number": return ( handleCellEdit(rowIndex, column.field, parseFloat(e.target.value) || 0) } className="h-7 text-xs" /> ); case "date": return ( handleCellEdit(rowIndex, column.field, e.target.value)} className="h-7 text-xs" /> ); case "select": return ( ); default: // text return ( handleCellEdit(rowIndex, column.field, e.target.value)} className="h-7 text-xs" /> ); } }; // 로딩 중일 때 if (isLoading) { return (

데이터를 불러오는 중...

); } // 에러 발생 시 if (loadError) { return (

데이터 로드 실패

{loadError}

); } return (
{showRowNumber && ( )} {columns.map((col) => ( ))} {!readOnly && allowDelete && ( )} {value.length === 0 ? ( ) : ( value.map((row, rowIndex) => ( {showRowNumber && ( )} {columns.map((col) => ( ))} {!readOnly && allowDelete && ( )} )) )}
# {col.label} {col.required && *} 삭제
표시할 데이터가 없습니다
{rowIndex + 1} {renderCell(row, col, rowIndex)}
); }