"use client"; import React, { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Plus, Columns } from "lucide-react"; import { ItemSelectionModal } from "./ItemSelectionModal"; import { RepeaterTable } from "./RepeaterTable"; import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition, DynamicDataSourceOption } from "./types"; import { useCalculation } from "./useCalculation"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { ComponentRendererProps } from "@/types/component"; // ✅ ComponentRendererProps 상속으로 필수 props 자동 확보 export interface ModalRepeaterTableComponentProps extends ComponentRendererProps { config?: ModalRepeaterTableProps; // ModalRepeaterTableProps의 개별 prop들도 지원 (호환성) sourceTable?: string; sourceColumns?: string[]; sourceSearchFields?: string[]; targetTable?: string; modalTitle?: string; modalButtonText?: string; multiSelect?: boolean; columns?: RepeaterColumnConfig[]; calculationRules?: any[]; value?: any[]; onChange?: (newData: any[]) => void; uniqueField?: string; filterCondition?: Record; companyCode?: string; } /** * 외부 테이블에서 참조 값을 조회하는 함수 * @param referenceTable 참조 테이블명 (예: "customer_item_mapping") * @param referenceField 참조할 컬럼명 (예: "basic_price") * @param joinConditions 조인 조건 배열 * @param sourceItem 소스 데이터 (모달에서 선택한 항목) * @param currentItem 현재 빌드 중인 항목 (이미 설정된 필드들) * @returns 참조된 값 또는 undefined */ async function fetchReferenceValue( referenceTable: string, referenceField: string, joinConditions: JoinCondition[], sourceItem: any, currentItem: any ): Promise { if (joinConditions.length === 0) { console.warn("⚠️ 조인 조건이 없습니다. 참조 조회를 건너뜁니다."); return undefined; } try { // 조인 조건을 WHERE 절로 변환 const whereConditions: Record = {}; for (const condition of joinConditions) { const { sourceTable = "target", sourceField, targetField, operator = "=" } = condition; // 소스 테이블에 따라 값을 가져오기 let value: any; if (sourceTable === "source") { // 소스 테이블 (item_info 등): 모달에서 선택한 원본 데이터 value = sourceItem[sourceField]; console.log(` 📘 소스 테이블에서 값 가져오기: ${sourceField} =`, value); } else { // 저장 테이블 (sales_order_mng 등): 반복 테이블에 이미 복사된 값 value = currentItem[sourceField]; console.log(` 📗 저장 테이블(반복테이블)에서 값 가져오기: ${sourceField} =`, value); } if (value === undefined || value === null) { console.warn(`⚠️ 조인 조건의 소스 필드 "${sourceField}" 값이 없습니다. (sourceTable: ${sourceTable})`); return undefined; } // 연산자가 "=" 인 경우만 지원 (확장 가능) if (operator === "=") { // 숫자형 ID인 경우 숫자로 변환 (문자열 '189' → 숫자 189) // 백엔드에서 entity 타입 컬럼 검색 시 문자열이면 ILIKE 검색을 수행하므로 // 정확한 ID 매칭을 위해 숫자로 변환해야 함 let convertedValue = value; if (targetField.endsWith('_id') || targetField === 'id') { const numValue = Number(value); if (!isNaN(numValue)) { convertedValue = numValue; console.log(` 🔢 ID 타입 변환: ${targetField} = "${value}" → ${numValue}`); } } whereConditions[targetField] = convertedValue; } else { console.warn(`⚠️ 연산자 "${operator}"는 아직 지원되지 않습니다.`); } } console.log(`🔍 참조 조회 API 호출:`, { table: referenceTable, field: referenceField, where: whereConditions, }); // API 호출: 테이블 데이터 조회 (POST 방식) const requestBody = { search: whereConditions, // ✅ filters → search 변경 (백엔드 파라미터명) size: 1, // 첫 번째 결과만 가져오기 page: 1, }; console.log("📤 API 요청 Body:", JSON.stringify(requestBody, null, 2)); const response = await apiClient.post( `/table-management/tables/${referenceTable}/data`, requestBody ); console.log("📥 API 전체 응답:", { success: response.data.success, dataLength: response.data.data?.data?.length, // ✅ data.data.data 구조 total: response.data.data?.total, // ✅ data.data.total firstRow: response.data.data?.data?.[0], // ✅ data.data.data[0] }); if (response.data.success && response.data.data?.data?.length > 0) { const firstRow = response.data.data.data[0]; // ✅ data.data.data[0] const value = firstRow[referenceField]; console.log(`✅ 참조 조회 성공:`, { table: referenceTable, field: referenceField, value, fullRow: firstRow, }); return value; } else { console.warn(`⚠️ 참조 조회 결과 없음:`, { table: referenceTable, where: whereConditions, responseData: response.data.data, total: response.data.total, }); return undefined; } } catch (error) { console.error(`❌ 참조 조회 API 오류:`, error); return undefined; } } export function ModalRepeaterTableComponent({ // ComponentRendererProps (자동 전달) component, isDesignMode = false, isSelected = false, isInteractive = false, onClick, onDragStart, onDragEnd, className, style, formData, onFormDataChange, // ModalRepeaterTable 전용 props config, sourceTable: propSourceTable, sourceColumns: propSourceColumns, sourceSearchFields: propSourceSearchFields, targetTable: propTargetTable, modalTitle: propModalTitle, modalButtonText: propModalButtonText, multiSelect: propMultiSelect, columns: propColumns, calculationRules: propCalculationRules, value: propValue, onChange: propOnChange, uniqueField: propUniqueField, filterCondition: propFilterCondition, companyCode: propCompanyCode, // 🆕 그룹 데이터 (EditModal에서 전달, 같은 그룹의 여러 품목) groupedData, ...props }: ModalRepeaterTableComponentProps & { groupedData?: Record[] }) { // ✅ config 또는 component.config 또는 개별 prop 우선순위로 병합 const componentConfig = { ...config, ...component?.config, }; // config prop 우선, 없으면 개별 prop 사용 const sourceTable = componentConfig?.sourceTable || propSourceTable || ""; const targetTable = componentConfig?.targetTable || propTargetTable; // sourceColumns에서 빈 문자열 필터링 const rawSourceColumns = componentConfig?.sourceColumns || propSourceColumns || []; const sourceColumns = rawSourceColumns.filter((col: string) => col && col.trim() !== ""); // 모달 컬럼 라벨 (컬럼명 -> 표시 라벨) const sourceColumnLabels = componentConfig?.sourceColumnLabels || {}; const sourceSearchFields = componentConfig?.sourceSearchFields || propSourceSearchFields || []; const modalTitle = componentConfig?.modalTitle || propModalTitle || "항목 검색"; const modalButtonText = componentConfig?.modalButtonText || propModalButtonText || "품목 검색"; const multiSelect = componentConfig?.multiSelect ?? propMultiSelect ?? true; const calculationRules = componentConfig?.calculationRules || propCalculationRules || []; // 모달 필터 설정 const modalFilters = componentConfig?.modalFilters || []; // ✅ value는 groupedData 우선, 없으면 formData[columnName], 없으면 prop 사용 const columnName = component?.columnName; // 🆕 groupedData가 전달되면 (EditModal에서 그룹 조회 결과) 우선 사용 const externalValue = (() => { if (groupedData && groupedData.length > 0) { return groupedData; } return (columnName && formData?.[columnName]) || componentConfig?.value || propValue || []; })(); // 빈 객체 판단 함수 (수정 모달의 실제 데이터는 유지) const isEmptyRow = (item: any): boolean => { if (!item || typeof item !== 'object') return true; // id가 있으면 실제 데이터 (수정 모달) if (item.id) return false; // 모든 값이 비어있는지 확인 (계산 필드 제외) const hasValue = Object.entries(item).some(([key, value]) => { // 계산 필드나 메타데이터는 제외 if (key.startsWith('_') || key === 'total_amount') return false; // 실제 값이 있는지 확인 return value !== undefined && value !== null && value !== '' && value !== 0 && value !== '0' && value !== '0.00'; }); return !hasValue; }; // 🆕 내부 상태로 데이터 관리 (즉시 UI 반영을 위해) const [localValue, setLocalValue] = useState(() => { return externalValue.filter((item) => !isEmptyRow(item)); }); // 🆕 외부 값(formData, propValue) 변경 시 내부 상태 동기화 useEffect(() => { // 빈 객체 필터링 const filteredValue = externalValue.filter((item) => !isEmptyRow(item)); // 외부 값이 변경되었고, 내부 값과 다른 경우에만 동기화 if (JSON.stringify(filteredValue) !== JSON.stringify(localValue)) { setLocalValue(filteredValue); } }, [externalValue]); // ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출 + 납기일 일괄 적용) const handleChange = (newData: any[]) => { // 🆕 납기일 일괄 적용 로직 (납기일 필드가 있는 경우만) let processedData = newData; // 납기일 필드 찾기 (item_due_date, delivery_date, due_date 등) const dateField = columns.find( (col) => col.field === "item_due_date" || col.field === "delivery_date" || col.field === "due_date" ); if (dateField && !isDeliveryDateApplied && newData.length > 0) { // 현재 상태: 납기일이 있는 행과 없는 행 개수 체크 const itemsWithDate = newData.filter((item) => item[dateField.field]); const itemsWithoutDate = newData.filter((item) => !item[dateField.field]); // 정확히 1개만 날짜가 있고, 나머지는 모두 비어있을 때 일괄 적용 if (itemsWithDate.length === 1 && itemsWithoutDate.length > 0) { const selectedDate = itemsWithDate[0][dateField.field]; processedData = newData.map((item) => ({ ...item, [dateField.field]: selectedDate, // 모든 행에 동일한 납기일 적용 })); setIsDeliveryDateApplied(true); // 플래그 활성화 } } // 🆕 수주일 일괄 적용 로직 (order_date 필드가 있는 경우만) const orderDateField = columns.find( (col) => col.field === "order_date" || col.field === "ordered_date" ); if (orderDateField && !isOrderDateApplied && newData.length > 0) { // ⚠️ 중요: 원본 newData를 참조해야 납기일의 영향을 받지 않음 const itemsWithOrderDate = newData.filter((item) => item[orderDateField.field]); const itemsWithoutOrderDate = newData.filter((item) => !item[orderDateField.field]); // ✅ 조건: 모든 행이 비어있는 초기 상태 → 어느 행에서든 첫 선택 시 전체 적용 if (itemsWithOrderDate.length === 1 && itemsWithoutOrderDate.length === newData.length - 1) { const selectedOrderDate = itemsWithOrderDate[0][orderDateField.field]; processedData = processedData.map((item) => ({ ...item, [orderDateField.field]: selectedOrderDate, })); setIsOrderDateApplied(true); // 플래그 활성화 } } // 🆕 내부 상태 즉시 업데이트 (UI 즉시 반영) - 일괄 적용된 데이터로 업데이트 setLocalValue(processedData); // 기존 onChange 콜백 호출 (호환성) const externalOnChange = componentConfig?.onChange || propOnChange; if (externalOnChange) { externalOnChange(processedData); } // 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트 if (onFormDataChange && columnName) { onFormDataChange(columnName, processedData); } }; // uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경 const rawUniqueField = componentConfig?.uniqueField || propUniqueField; const uniqueField = rawUniqueField === "order_no" && sourceTable === "item_info" ? "item_number" : rawUniqueField; const filterCondition = componentConfig?.filterCondition || propFilterCondition || {}; const companyCode = componentConfig?.companyCode || propCompanyCode; const [modalOpen, setModalOpen] = useState(false); // 체크박스 선택 상태 const [selectedRows, setSelectedRows] = useState>(new Set()); // 균등 분배 트리거 (값이 변경되면 RepeaterTable에서 균등 분배 실행) const [equalizeWidthsTrigger, setEqualizeWidthsTrigger] = useState(0); // 🆕 납기일 일괄 적용 플래그 (딱 한 번만 실행) const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false); // 🆕 수주일 일괄 적용 플래그 (딱 한 번만 실행) const [isOrderDateApplied, setIsOrderDateApplied] = useState(false); // 🆕 동적 데이터 소스 활성화 상태 (컬럼별로 현재 선택된 옵션 ID) const [activeDataSources, setActiveDataSources] = useState>({}); // columns가 비어있으면 sourceColumns로부터 자동 생성 const columns = React.useMemo((): RepeaterColumnConfig[] => { const configuredColumns = componentConfig?.columns || propColumns || []; if (configuredColumns.length > 0) { return configuredColumns; } // columns가 비어있으면 sourceColumns로부터 자동 생성 if (sourceColumns.length > 0) { const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({ field: field, label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능) editable: false, // 기본적으로 읽기 전용 type: "text" as const, width: "150px", })); return autoColumns; } console.warn("⚠️ [ModalRepeaterTable] columns와 sourceColumns 모두 비어있음!"); return []; }, [componentConfig?.columns, propColumns, sourceColumns]); // 초기 props 검증 useEffect(() => { if (rawSourceColumns.length !== sourceColumns.length) { console.warn(`⚠️ [ModalRepeaterTable] sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개`); } if (rawUniqueField !== uniqueField) { console.warn(`⚠️ [ModalRepeaterTable] uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`); } if (columns.length === 0) { console.error("❌ [ModalRepeaterTable] columns가 비어있습니다!", { sourceColumns }); } }, []); // 🆕 저장 요청 시에만 데이터 전달 (beforeFormSave 이벤트 리스너) useEffect(() => { const handleSaveRequest = async (event: Event) => { const componentKey = columnName || component?.id || "modal_repeater_data"; if (localValue.length === 0) { console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음"); return; } // sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거) // 단, columnMappings에 정의된 컬럼은 저장해야 하므로 제외하지 않음 const mappedFields = columns .filter(col => col.mapping?.type === "source" && col.mapping?.sourceField) .map(col => col.field); const filteredData = localValue.map((item: any) => { const filtered: Record = {}; Object.keys(item).forEach((key) => { // 메타데이터 필드 제외 if (key.startsWith("_")) { return; } // sourceColumns에 포함되어 있지만 columnMappings에도 정의된 경우 → 저장함 if (mappedFields.includes(key)) { filtered[key] = item[key]; return; } // sourceColumns에만 있고 매핑 안 된 경우 → 제외 (조인 전용) if (sourceColumns.includes(key)) { return; } // 나머지는 모두 저장 filtered[key] = item[key]; }); return filtered; }); // targetTable 메타데이터를 배열 항목에 추가 const dataWithTargetTable = targetTable ? filteredData.map((item: any) => ({ ...item, _targetTable: targetTable, // 백엔드가 인식할 메타데이터 })) : filteredData; // CustomEvent의 detail에 데이터 추가 if (event instanceof CustomEvent && event.detail) { event.detail.formData[componentKey] = dataWithTargetTable; console.log("✅ [ModalRepeaterTable] 저장 데이터 준비:", { key: componentKey, itemCount: dataWithTargetTable.length, targetTable: targetTable || "미설정", }); } // 기존 onFormDataChange도 호출 (호환성) if (onFormDataChange) { onFormDataChange(componentKey, dataWithTargetTable); } }; // 저장 버튼 클릭 시 데이터 수집 window.addEventListener("beforeFormSave", handleSaveRequest as EventListener); return () => { window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener); }; }, [localValue, columnName, component?.id, onFormDataChange, targetTable]); const { calculateRow, calculateAll } = useCalculation(calculationRules); /** * 동적 데이터 소스 변경 시 호출 * 해당 컬럼의 모든 행 데이터를 새로운 소스에서 다시 조회 */ const handleDataSourceChange = async (columnField: string, optionId: string) => { console.log(`🔄 데이터 소스 변경: ${columnField} → ${optionId}`); // 활성화 상태 업데이트 setActiveDataSources((prev) => ({ ...prev, [columnField]: optionId, })); // 해당 컬럼 찾기 const column = columns.find((col) => col.field === columnField); if (!column?.dynamicDataSource?.enabled) { console.warn(`⚠️ 컬럼 "${columnField}"에 동적 데이터 소스가 설정되지 않음`); return; } // 선택된 옵션 찾기 const option = column.dynamicDataSource.options.find((opt) => opt.id === optionId); if (!option) { console.warn(`⚠️ 옵션 "${optionId}"을 찾을 수 없음`); return; } // 모든 행에 대해 새 값 조회 const updatedData = await Promise.all( localValue.map(async (row, index) => { try { const newValue = await fetchDynamicValue(option, row); console.log(` ✅ 행 ${index}: ${columnField} = ${newValue}`); return { ...row, [columnField]: newValue, }; } catch (error) { console.error(` ❌ 행 ${index} 조회 실패:`, error); return row; } }) ); // 계산 필드 업데이트 후 데이터 반영 const calculatedData = calculateAll(updatedData); handleChange(calculatedData); }; /** * 동적 데이터 소스 옵션에 따라 값 조회 */ async function fetchDynamicValue( option: DynamicDataSourceOption, rowData: any ): Promise { if (option.sourceType === "table" && option.tableConfig) { // 테이블 직접 조회 (단순 조인) const { tableName, valueColumn, joinConditions } = option.tableConfig; const whereConditions: Record = {}; for (const cond of joinConditions) { let value = rowData[cond.sourceField]; if (value === undefined || value === null) { console.warn(`⚠️ 조인 조건의 소스 필드 "${cond.sourceField}" 값이 없음`); return undefined; } // 숫자형 ID인 경우 숫자로 변환 if (cond.targetField.endsWith('_id') || cond.targetField === 'id') { const numValue = Number(value); if (!isNaN(numValue)) { value = numValue; } } whereConditions[cond.targetField] = value; } console.log(`🔍 테이블 조회: ${tableName}`, whereConditions); const response = await apiClient.post( `/table-management/tables/${tableName}/data`, { search: whereConditions, size: 1, page: 1 } ); if (response.data.success && response.data.data?.data?.length > 0) { return response.data.data.data[0][valueColumn]; } return undefined; } else if (option.sourceType === "multiTable" && option.multiTableConfig) { // 테이블 복합 조인 (2개 이상 테이블 순차 조인) const { joinChain, valueColumn } = option.multiTableConfig; if (!joinChain || joinChain.length === 0) { console.warn("⚠️ 조인 체인이 비어있습니다."); return undefined; } console.log(`🔗 복합 조인 시작: ${joinChain.length}단계`); // 현재 값을 추적 (첫 단계는 현재 행에서 시작) let currentValue: any = null; let currentRow: any = null; for (let i = 0; i < joinChain.length; i++) { const step = joinChain[i]; const { tableName, joinCondition, outputField } = step; // 조인 조건 값 가져오기 let fromValue: any; if (i === 0) { // 첫 번째 단계: 현재 행에서 값 가져오기 fromValue = rowData[joinCondition.fromField]; console.log(` 📍 단계 ${i + 1}: 현재행.${joinCondition.fromField} = ${fromValue}`); } else { // 이후 단계: 이전 조회 결과에서 값 가져오기 fromValue = currentRow?.[joinCondition.fromField] || currentValue; console.log(` 📍 단계 ${i + 1}: 이전결과.${joinCondition.fromField} = ${fromValue}`); } if (fromValue === undefined || fromValue === null) { console.warn(`⚠️ 단계 ${i + 1}: 조인 조건 값이 없습니다. (${joinCondition.fromField})`); return undefined; } // 테이블 조회 // 숫자형 ID인 경우 숫자로 변환 let convertedFromValue = fromValue; if (joinCondition.toField.endsWith('_id') || joinCondition.toField === 'id') { const numValue = Number(fromValue); if (!isNaN(numValue)) { convertedFromValue = numValue; } } const whereConditions: Record = { [joinCondition.toField]: convertedFromValue }; console.log(` 🔍 단계 ${i + 1}: ${tableName} 조회`, whereConditions); try { const response = await apiClient.post( `/table-management/tables/${tableName}/data`, { search: whereConditions, size: 1, page: 1 } ); if (response.data.success && response.data.data?.data?.length > 0) { currentRow = response.data.data.data[0]; currentValue = outputField ? currentRow[outputField] : currentRow; console.log(` ✅ 단계 ${i + 1} 성공:`, { outputField, value: currentValue }); } else { console.warn(` ⚠️ 단계 ${i + 1}: 조회 결과 없음`); return undefined; } } catch (error) { console.error(` ❌ 단계 ${i + 1} 조회 실패:`, error); return undefined; } } // 최종 값 반환 (마지막 테이블에서 valueColumn 가져오기) const finalValue = currentRow?.[valueColumn]; console.log(`🎯 복합 조인 완료: ${valueColumn} = ${finalValue}`); return finalValue; } else if (option.sourceType === "api" && option.apiConfig) { // 전용 API 호출 (복잡한 다중 조인) const { endpoint, method = "GET", parameterMappings, responseValueField } = option.apiConfig; // 파라미터 빌드 const params: Record = {}; for (const mapping of parameterMappings) { const value = rowData[mapping.sourceField]; if (value !== undefined && value !== null) { params[mapping.paramName] = value; } } console.log(`🔍 API 호출: ${method} ${endpoint}`, params); let response; if (method === "POST") { response = await apiClient.post(endpoint, params); } else { response = await apiClient.get(endpoint, { params }); } if (response.data.success && response.data.data) { // responseValueField로 값 추출 (중첩 경로 지원: "data.price") const keys = responseValueField.split("."); let value = response.data.data; for (const key of keys) { value = value?.[key]; } return value; } return undefined; } return undefined; } // 초기 데이터에 계산 필드 적용 useEffect(() => { if (localValue.length > 0 && calculationRules.length > 0) { const calculated = calculateAll(localValue); // 값이 실제로 변경된 경우만 업데이트 if (JSON.stringify(calculated) !== JSON.stringify(localValue)) { handleChange(calculated); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleAddItems = async (items: any[]) => { console.log("➕ handleAddItems 호출:", items.length, "개 항목"); console.log("📋 소스 데이터:", items); // 매핑 규칙에 따라 데이터 변환 (비동기 처리) const mappedItems = await Promise.all(items.map(async (sourceItem) => { const newItem: any = {}; // ⚠️ 중요: reference 매핑은 다른 컬럼에 의존할 수 있으므로 // 1단계: source/manual 매핑을 먼저 처리 // 2단계: reference 매핑을 나중에 처리 const referenceColumns: typeof columns = []; const otherColumns: typeof columns = []; for (const col of columns) { if (col.mapping?.type === "reference") { referenceColumns.push(col); } else { otherColumns.push(col); } } // 1단계: source/manual 컬럼 먼저 처리 for (const col of otherColumns) { console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping); // 1. 매핑 규칙이 있는 경우 if (col.mapping) { if (col.mapping.type === "source") { // 소스 테이블 컬럼에서 복사 const sourceField = col.mapping.sourceField; if (sourceField && sourceItem[sourceField] !== undefined) { newItem[col.field] = sourceItem[sourceField]; console.log(` ✅ 소스 복사: ${sourceField} → ${col.field}:`, newItem[col.field]); } else { console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`); } } else if (col.mapping.type === "manual") { // 사용자 입력 (빈 값) newItem[col.field] = undefined; console.log(` ✏️ 수동 입력 필드`); } } // 2. 매핑 규칙이 없는 경우 - 소스 데이터에서 같은 필드명으로 복사 else if (sourceItem[col.field] !== undefined) { newItem[col.field] = sourceItem[col.field]; console.log(` 📝 직접 복사: ${col.field}:`, newItem[col.field]); } // 3. 기본값 적용 if (col.defaultValue !== undefined && newItem[col.field] === undefined) { newItem[col.field] = col.defaultValue; console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue); } } // 2단계: reference 컬럼 처리 (다른 컬럼들이 모두 설정된 후) console.log("🔗 2단계: reference 컬럼 처리 시작"); for (const col of referenceColumns) { console.log(`🔄 컬럼 "${col.field}" 참조 매핑 처리:`, col.mapping); // 외부 테이블 참조 (API 호출) console.log(` ⏳ 참조 조회 시작: ${col.mapping?.referenceTable}.${col.mapping?.referenceField}`); try { const referenceValue = await fetchReferenceValue( col.mapping!.referenceTable!, col.mapping!.referenceField!, col.mapping!.joinCondition || [], sourceItem, newItem ); if (referenceValue !== null && referenceValue !== undefined) { newItem[col.field] = referenceValue; console.log(` ✅ 참조 조회 성공: ${col.field}:`, referenceValue); } else { newItem[col.field] = undefined; console.warn(` ⚠️ 참조 조회 결과 없음`); } } catch (error) { console.error(` ❌ 참조 조회 오류:`, error); newItem[col.field] = undefined; } // 기본값 적용 if (col.defaultValue !== undefined && newItem[col.field] === undefined) { newItem[col.field] = col.defaultValue; console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue); } } console.log("📦 변환된 항목:", newItem); return newItem; })); // 계산 필드 업데이트 const calculatedItems = calculateAll(mappedItems); // 기존 데이터에 추가 const newData = [...localValue, ...calculatedItems]; console.log("✅ 최종 데이터:", newData.length, "개 항목"); // ✅ 통합 onChange 호출 (formData 반영 포함) handleChange(newData); }; const handleRowChange = (index: number, newRow: any) => { // 계산 필드 업데이트 const calculatedRow = calculateRow(newRow); // 데이터 업데이트 const newData = [...localValue]; newData[index] = calculatedRow; // ✅ 통합 onChange 호출 (formData 반영 포함) handleChange(newData); }; const handleRowDelete = (index: number) => { const newData = localValue.filter((_, i) => i !== index); // ✅ 통합 onChange 호출 (formData 반영 포함) handleChange(newData); }; // 선택된 항목 일괄 삭제 핸들러 const handleBulkDelete = () => { if (selectedRows.size === 0) return; // 선택되지 않은 항목만 남김 const newData = localValue.filter((_, index) => !selectedRows.has(index)); // 데이터 업데이트 및 선택 상태 초기화 handleChange(newData); setSelectedRows(new Set()); }; // 컬럼명 -> 라벨명 매핑 생성 (sourceColumnLabels 우선, 없으면 columns에서 가져옴) const columnLabels = columns.reduce((acc, col) => { // sourceColumnLabels에 정의된 라벨 우선 사용 acc[col.field] = sourceColumnLabels[col.field] || col.label; return acc; }, { ...sourceColumnLabels } as Record); return (
{/* 추가 버튼 */}
{localValue.length > 0 && `${localValue.length}개 항목`} {selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`} {columns.length > 0 && ( )}
{selectedRows.size > 0 && ( )}
{/* Repeater 테이블 */} {/* 항목 선택 모달 */}
); }