2025-11-14 14:43:53 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Plus } from "lucide-react";
|
|
|
|
|
|
import { ItemSelectionModal } from "./ItemSelectionModal";
|
|
|
|
|
|
import { RepeaterTable } from "./RepeaterTable";
|
2025-11-20 10:16:49 +09:00
|
|
|
|
import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types";
|
2025-11-14 14:43:53 +09:00
|
|
|
|
import { useCalculation } from "./useCalculation";
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-11-20 10:16:49 +09:00
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-11-21 10:12:29 +09:00
|
|
|
|
import { ComponentRendererProps } from "@/types/component";
|
2025-11-14 14:43:53 +09:00
|
|
|
|
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// ✅ ComponentRendererProps 상속으로 필수 props 자동 확보
|
|
|
|
|
|
export interface ModalRepeaterTableComponentProps extends ComponentRendererProps {
|
2025-11-14 14:43:53 +09:00
|
|
|
|
config?: ModalRepeaterTableProps;
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// 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<string, any>;
|
|
|
|
|
|
companyCode?: string;
|
2025-11-14 14:43:53 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 10:16:49 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 외부 테이블에서 참조 값을 조회하는 함수
|
|
|
|
|
|
* @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<any> {
|
|
|
|
|
|
if (joinConditions.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ 조인 조건이 없습니다. 참조 조회를 건너뜁니다.");
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 조인 조건을 WHERE 절로 변환
|
|
|
|
|
|
const whereConditions: Record<string, any> = {};
|
|
|
|
|
|
|
|
|
|
|
|
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 === "=") {
|
|
|
|
|
|
whereConditions[targetField] = value;
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-14 14:43:53 +09:00
|
|
|
|
export function ModalRepeaterTableComponent({
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// ComponentRendererProps (자동 전달)
|
|
|
|
|
|
component,
|
|
|
|
|
|
isDesignMode = false,
|
|
|
|
|
|
isSelected = false,
|
|
|
|
|
|
isInteractive = false,
|
|
|
|
|
|
onClick,
|
|
|
|
|
|
onDragStart,
|
|
|
|
|
|
onDragEnd,
|
|
|
|
|
|
className,
|
|
|
|
|
|
style,
|
|
|
|
|
|
formData,
|
|
|
|
|
|
onFormDataChange,
|
|
|
|
|
|
|
|
|
|
|
|
// ModalRepeaterTable 전용 props
|
2025-11-14 14:43:53 +09:00
|
|
|
|
config,
|
|
|
|
|
|
sourceTable: propSourceTable,
|
|
|
|
|
|
sourceColumns: propSourceColumns,
|
|
|
|
|
|
sourceSearchFields: propSourceSearchFields,
|
2025-11-21 10:12:29 +09:00
|
|
|
|
targetTable: propTargetTable,
|
2025-11-14 14:43:53 +09:00
|
|
|
|
modalTitle: propModalTitle,
|
|
|
|
|
|
modalButtonText: propModalButtonText,
|
|
|
|
|
|
multiSelect: propMultiSelect,
|
|
|
|
|
|
columns: propColumns,
|
|
|
|
|
|
calculationRules: propCalculationRules,
|
|
|
|
|
|
value: propValue,
|
|
|
|
|
|
onChange: propOnChange,
|
|
|
|
|
|
uniqueField: propUniqueField,
|
|
|
|
|
|
filterCondition: propFilterCondition,
|
|
|
|
|
|
companyCode: propCompanyCode,
|
2025-11-21 10:12:29 +09:00
|
|
|
|
|
|
|
|
|
|
...props
|
2025-11-14 14:43:53 +09:00
|
|
|
|
}: ModalRepeaterTableComponentProps) {
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// ✅ config 또는 component.config 또는 개별 prop 우선순위로 병합
|
|
|
|
|
|
const componentConfig = {
|
|
|
|
|
|
...config,
|
|
|
|
|
|
...component?.config,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-14 14:43:53 +09:00
|
|
|
|
// config prop 우선, 없으면 개별 prop 사용
|
2025-11-21 10:12:29 +09:00
|
|
|
|
const sourceTable = componentConfig?.sourceTable || propSourceTable || "";
|
|
|
|
|
|
const targetTable = componentConfig?.targetTable || propTargetTable;
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
|
|
|
|
|
// sourceColumns에서 빈 문자열 필터링
|
2025-11-21 10:12:29 +09:00
|
|
|
|
const rawSourceColumns = componentConfig?.sourceColumns || propSourceColumns || [];
|
|
|
|
|
|
const sourceColumns = rawSourceColumns.filter((col: string) => col && col.trim() !== "");
|
|
|
|
|
|
|
|
|
|
|
|
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 || [];
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// ✅ value는 formData[columnName] 우선, 없으면 prop 사용
|
|
|
|
|
|
const columnName = component?.columnName;
|
|
|
|
|
|
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
|
|
|
|
|
|
2025-11-25 14:23:54 +09:00
|
|
|
|
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출)
|
2025-11-21 10:12:29 +09:00
|
|
|
|
const handleChange = (newData: any[]) => {
|
|
|
|
|
|
// 기존 onChange 콜백 호출 (호환성)
|
|
|
|
|
|
const externalOnChange = componentConfig?.onChange || propOnChange;
|
|
|
|
|
|
if (externalOnChange) {
|
|
|
|
|
|
externalOnChange(newData);
|
|
|
|
|
|
}
|
2025-11-25 14:23:54 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
|
|
|
|
|
|
if (onFormDataChange && columnName) {
|
|
|
|
|
|
onFormDataChange(columnName, newData);
|
|
|
|
|
|
}
|
2025-11-21 10:12:29 +09:00
|
|
|
|
};
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
|
|
|
|
|
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
2025-11-21 10:12:29 +09:00
|
|
|
|
const rawUniqueField = componentConfig?.uniqueField || propUniqueField;
|
2025-11-19 11:48:00 +09:00
|
|
|
|
const uniqueField = rawUniqueField === "order_no" && sourceTable === "item_info"
|
|
|
|
|
|
? "item_number"
|
|
|
|
|
|
: rawUniqueField;
|
|
|
|
|
|
|
2025-11-21 10:12:29 +09:00
|
|
|
|
const filterCondition = componentConfig?.filterCondition || propFilterCondition || {};
|
|
|
|
|
|
const companyCode = componentConfig?.companyCode || propCompanyCode;
|
2025-11-14 14:43:53 +09:00
|
|
|
|
const [modalOpen, setModalOpen] = useState(false);
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
|
|
|
|
|
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
|
|
|
|
|
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
2025-11-21 10:12:29 +09:00
|
|
|
|
const configuredColumns = componentConfig?.columns || propColumns || [];
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
|
|
|
|
|
if (configuredColumns.length > 0) {
|
|
|
|
|
|
console.log("✅ 설정된 columns 사용:", configuredColumns);
|
|
|
|
|
|
return configuredColumns;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
|
|
|
|
|
if (sourceColumns.length > 0) {
|
|
|
|
|
|
console.log("🔄 sourceColumns로부터 자동 생성:", sourceColumns);
|
|
|
|
|
|
const autoColumns: RepeaterColumnConfig[] = sourceColumns.map((field) => ({
|
|
|
|
|
|
field: field,
|
|
|
|
|
|
label: field, // 필드명을 라벨로 사용 (나중에 설정에서 변경 가능)
|
|
|
|
|
|
editable: false, // 기본적으로 읽기 전용
|
|
|
|
|
|
type: "text" as const,
|
|
|
|
|
|
width: "150px",
|
|
|
|
|
|
}));
|
|
|
|
|
|
console.log("📋 자동 생성된 columns:", autoColumns);
|
|
|
|
|
|
return autoColumns;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.warn("⚠️ columns와 sourceColumns 모두 비어있음!");
|
|
|
|
|
|
return [];
|
2025-11-21 10:12:29 +09:00
|
|
|
|
}, [componentConfig?.columns, propColumns, sourceColumns]);
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
|
|
|
|
|
// 초기 props 로깅
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (rawSourceColumns.length !== sourceColumns.length) {
|
|
|
|
|
|
console.warn(`⚠️ sourceColumns 필터링: ${rawSourceColumns.length}개 → ${sourceColumns.length}개 (빈 문자열 제거)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rawUniqueField !== uniqueField) {
|
|
|
|
|
|
console.warn(`⚠️ uniqueField 자동 보정: "${rawUniqueField}" → "${uniqueField}"`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🎬 ModalRepeaterTableComponent 마운트:", {
|
|
|
|
|
|
columnsLength: columns.length,
|
|
|
|
|
|
sourceTable,
|
|
|
|
|
|
sourceColumns,
|
|
|
|
|
|
uniqueField,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (columns.length === 0) {
|
|
|
|
|
|
console.error("❌ columns가 비어있습니다! sourceColumns:", sourceColumns);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.log("✅ columns 설정 완료:", columns.map(c => c.label || c.field).join(", "));
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// value 변경 감지
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
console.log("📦 ModalRepeaterTableComponent value 변경:", {
|
|
|
|
|
|
valueLength: value.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [value]);
|
|
|
|
|
|
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// 🆕 저장 요청 시에만 데이터 전달 (beforeFormSave 이벤트 리스너)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleSaveRequest = async (event: Event) => {
|
|
|
|
|
|
const componentKey = columnName || component?.id || "modal_repeater_data";
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔔 [ModalRepeaterTable] beforeFormSave 이벤트 수신!", {
|
|
|
|
|
|
componentKey,
|
|
|
|
|
|
itemsCount: value.length,
|
|
|
|
|
|
hasOnFormDataChange: !!onFormDataChange,
|
|
|
|
|
|
columnName,
|
|
|
|
|
|
componentId: component?.id,
|
|
|
|
|
|
targetTable,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (value.length === 0) {
|
|
|
|
|
|
console.warn("⚠️ [ModalRepeaterTable] 저장할 데이터 없음");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 15:24:31 +09:00
|
|
|
|
// 🔥 sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거)
|
|
|
|
|
|
console.log("🔍 [ModalRepeaterTable] 필터링 전 데이터:", {
|
|
|
|
|
|
sourceColumns,
|
|
|
|
|
|
sourceTable,
|
|
|
|
|
|
targetTable,
|
|
|
|
|
|
sampleItem: value[0],
|
|
|
|
|
|
itemKeys: value[0] ? Object.keys(value[0]) : [],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const filteredData = value.map((item: any) => {
|
|
|
|
|
|
const filtered: Record<string, any> = {};
|
|
|
|
|
|
|
|
|
|
|
|
Object.keys(item).forEach((key) => {
|
|
|
|
|
|
// sourceColumns에 포함된 컬럼은 제외 (item_info 테이블의 컬럼)
|
|
|
|
|
|
if (sourceColumns.includes(key)) {
|
|
|
|
|
|
console.log(` ⛔ ${key} 제외 (sourceColumn)`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 메타데이터 필드도 제외
|
|
|
|
|
|
if (key.startsWith("_")) {
|
|
|
|
|
|
console.log(` ⛔ ${key} 제외 (메타데이터)`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
filtered[key] = item[key];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return filtered;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ [ModalRepeaterTable] 필터링 후 데이터:", {
|
|
|
|
|
|
filteredItemKeys: filteredData[0] ? Object.keys(filteredData[0]) : [],
|
|
|
|
|
|
sampleFilteredItem: filteredData[0],
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// 🔥 targetTable 메타데이터를 배열 항목에 추가
|
|
|
|
|
|
const dataWithTargetTable = targetTable
|
2025-11-24 15:24:31 +09:00
|
|
|
|
? filteredData.map((item: any) => ({
|
2025-11-21 10:12:29 +09:00
|
|
|
|
...item,
|
|
|
|
|
|
_targetTable: targetTable, // 백엔드가 인식할 메타데이터
|
|
|
|
|
|
}))
|
2025-11-24 15:24:31 +09:00
|
|
|
|
: filteredData;
|
2025-11-21 10:12:29 +09:00
|
|
|
|
|
|
|
|
|
|
// ✅ CustomEvent의 detail에 데이터 추가
|
|
|
|
|
|
if (event instanceof CustomEvent && event.detail) {
|
|
|
|
|
|
event.detail.formData[componentKey] = dataWithTargetTable;
|
|
|
|
|
|
console.log("✅ [ModalRepeaterTable] context.formData에 데이터 추가 완료:", {
|
|
|
|
|
|
key: componentKey,
|
|
|
|
|
|
itemCount: dataWithTargetTable.length,
|
|
|
|
|
|
targetTable: targetTable || "미설정 (화면 설계에서 설정 필요)",
|
|
|
|
|
|
sampleItem: dataWithTargetTable[0],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 onFormDataChange도 호출 (호환성)
|
|
|
|
|
|
if (onFormDataChange) {
|
|
|
|
|
|
onFormDataChange(componentKey, dataWithTargetTable);
|
|
|
|
|
|
console.log("✅ [ModalRepeaterTable] onFormDataChange 호출 완료");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 저장 버튼 클릭 시 데이터 수집
|
|
|
|
|
|
window.addEventListener("beforeFormSave", handleSaveRequest as EventListener);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [value, columnName, component?.id, onFormDataChange, targetTable]);
|
|
|
|
|
|
|
2025-11-14 14:43:53 +09:00
|
|
|
|
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
|
|
|
|
|
|
|
|
|
|
|
// 초기 데이터에 계산 필드 적용
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (value.length > 0 && calculationRules.length > 0) {
|
|
|
|
|
|
const calculated = calculateAll(value);
|
|
|
|
|
|
// 값이 실제로 변경된 경우만 업데이트
|
|
|
|
|
|
if (JSON.stringify(calculated) !== JSON.stringify(value)) {
|
2025-11-24 15:24:31 +09:00
|
|
|
|
handleChange(calculated);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-24 15:24:31 +09:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
2025-11-14 14:43:53 +09:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-11-20 10:16:49 +09:00
|
|
|
|
const handleAddItems = async (items: any[]) => {
|
2025-11-19 11:48:00 +09:00
|
|
|
|
console.log("➕ handleAddItems 호출:", items.length, "개 항목");
|
2025-11-19 17:09:12 +09:00
|
|
|
|
console.log("📋 소스 데이터:", items);
|
|
|
|
|
|
|
2025-11-20 10:16:49 +09:00
|
|
|
|
// 매핑 규칙에 따라 데이터 변환 (비동기 처리)
|
|
|
|
|
|
const mappedItems = await Promise.all(items.map(async (sourceItem) => {
|
2025-11-19 17:09:12 +09:00
|
|
|
|
const newItem: any = {};
|
2025-11-19 11:48:00 +09:00
|
|
|
|
|
2025-11-20 10:16:49 +09:00
|
|
|
|
// ⚠️ 중요: 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) {
|
2025-11-19 17:09:12 +09:00
|
|
|
|
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. 기본값 적용
|
2025-11-14 14:43:53 +09:00
|
|
|
|
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
|
|
|
|
|
|
newItem[col.field] = col.defaultValue;
|
2025-11-19 17:09:12 +09:00
|
|
|
|
console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
}
|
2025-11-20 10:16:49 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-19 17:09:12 +09:00
|
|
|
|
|
|
|
|
|
|
console.log("📦 변환된 항목:", newItem);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
return newItem;
|
2025-11-20 10:16:49 +09:00
|
|
|
|
}));
|
2025-11-14 14:43:53 +09:00
|
|
|
|
|
|
|
|
|
|
// 계산 필드 업데이트
|
2025-11-19 17:09:12 +09:00
|
|
|
|
const calculatedItems = calculateAll(mappedItems);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
|
|
|
|
|
|
// 기존 데이터에 추가
|
2025-11-19 11:48:00 +09:00
|
|
|
|
const newData = [...value, ...calculatedItems];
|
|
|
|
|
|
console.log("✅ 최종 데이터:", newData.length, "개 항목");
|
|
|
|
|
|
|
2025-11-21 10:12:29 +09:00
|
|
|
|
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
|
|
|
|
|
handleChange(newData);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRowChange = (index: number, newRow: any) => {
|
|
|
|
|
|
// 계산 필드 업데이트
|
|
|
|
|
|
const calculatedRow = calculateRow(newRow);
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터 업데이트
|
|
|
|
|
|
const newData = [...value];
|
|
|
|
|
|
newData[index] = calculatedRow;
|
2025-11-21 10:12:29 +09:00
|
|
|
|
|
|
|
|
|
|
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
|
|
|
|
|
handleChange(newData);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleRowDelete = (index: number) => {
|
|
|
|
|
|
const newData = value.filter((_, i) => i !== index);
|
2025-11-21 10:12:29 +09:00
|
|
|
|
|
|
|
|
|
|
// ✅ 통합 onChange 호출 (formData 반영 포함)
|
|
|
|
|
|
handleChange(newData);
|
2025-11-14 14:43:53 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-14 16:19:27 +09:00
|
|
|
|
// 컬럼명 -> 라벨명 매핑 생성
|
|
|
|
|
|
const columnLabels = columns.reduce((acc, col) => {
|
|
|
|
|
|
acc[col.field] = col.label;
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {} as Record<string, string>);
|
|
|
|
|
|
|
2025-11-14 14:43:53 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className={cn("space-y-4", className)}>
|
|
|
|
|
|
{/* 추가 버튼 */}
|
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
|
<div className="text-sm text-muted-foreground">
|
|
|
|
|
|
{value.length > 0 && `${value.length}개 항목`}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
onClick={() => setModalOpen(true)}
|
|
|
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
|
|
|
|
{modalButtonText}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Repeater 테이블 */}
|
|
|
|
|
|
<RepeaterTable
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
data={value}
|
2025-11-21 10:12:29 +09:00
|
|
|
|
onDataChange={handleChange}
|
2025-11-14 14:43:53 +09:00
|
|
|
|
onRowChange={handleRowChange}
|
|
|
|
|
|
onRowDelete={handleRowDelete}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 항목 선택 모달 */}
|
|
|
|
|
|
<ItemSelectionModal
|
|
|
|
|
|
open={modalOpen}
|
|
|
|
|
|
onOpenChange={setModalOpen}
|
|
|
|
|
|
sourceTable={sourceTable}
|
|
|
|
|
|
sourceColumns={sourceColumns}
|
|
|
|
|
|
sourceSearchFields={sourceSearchFields}
|
|
|
|
|
|
multiSelect={multiSelect}
|
|
|
|
|
|
filterCondition={filterCondition}
|
|
|
|
|
|
modalTitle={modalTitle}
|
|
|
|
|
|
alreadySelected={value}
|
|
|
|
|
|
uniqueField={uniqueField}
|
|
|
|
|
|
onSelect={handleAddItems}
|
2025-11-14 16:19:27 +09:00
|
|
|
|
columnLabels={columnLabels}
|
2025-11-14 14:43:53 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|