254 lines
9.1 KiB
TypeScript
254 lines
9.1 KiB
TypeScript
"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";
|
||
import { ModalRepeaterTableProps, RepeaterColumnConfig } from "./types";
|
||
import { useCalculation } from "./useCalculation";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
interface ModalRepeaterTableComponentProps extends Partial<ModalRepeaterTableProps> {
|
||
config?: ModalRepeaterTableProps;
|
||
}
|
||
|
||
export function ModalRepeaterTableComponent({
|
||
config,
|
||
sourceTable: propSourceTable,
|
||
sourceColumns: propSourceColumns,
|
||
sourceSearchFields: propSourceSearchFields,
|
||
modalTitle: propModalTitle,
|
||
modalButtonText: propModalButtonText,
|
||
multiSelect: propMultiSelect,
|
||
columns: propColumns,
|
||
calculationRules: propCalculationRules,
|
||
value: propValue,
|
||
onChange: propOnChange,
|
||
uniqueField: propUniqueField,
|
||
filterCondition: propFilterCondition,
|
||
companyCode: propCompanyCode,
|
||
className,
|
||
}: ModalRepeaterTableComponentProps) {
|
||
// config prop 우선, 없으면 개별 prop 사용
|
||
const sourceTable = config?.sourceTable || propSourceTable || "";
|
||
|
||
// sourceColumns에서 빈 문자열 필터링
|
||
const rawSourceColumns = config?.sourceColumns || propSourceColumns || [];
|
||
const sourceColumns = rawSourceColumns.filter((col) => col && col.trim() !== "");
|
||
|
||
const sourceSearchFields = config?.sourceSearchFields || propSourceSearchFields || [];
|
||
const modalTitle = config?.modalTitle || propModalTitle || "항목 검색";
|
||
const modalButtonText = config?.modalButtonText || propModalButtonText || "품목 검색";
|
||
const multiSelect = config?.multiSelect ?? propMultiSelect ?? true;
|
||
const calculationRules = config?.calculationRules || propCalculationRules || [];
|
||
const value = config?.value || propValue || [];
|
||
const onChange = config?.onChange || propOnChange || (() => {});
|
||
|
||
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
||
const rawUniqueField = config?.uniqueField || propUniqueField;
|
||
const uniqueField = rawUniqueField === "order_no" && sourceTable === "item_info"
|
||
? "item_number"
|
||
: rawUniqueField;
|
||
|
||
const filterCondition = config?.filterCondition || propFilterCondition || {};
|
||
const companyCode = config?.companyCode || propCompanyCode;
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
|
||
// columns가 비어있으면 sourceColumns로부터 자동 생성
|
||
const columns = React.useMemo((): RepeaterColumnConfig[] => {
|
||
const configuredColumns = config?.columns || propColumns || [];
|
||
|
||
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 [];
|
||
}, [config?.columns, propColumns, sourceColumns]);
|
||
|
||
// 초기 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]);
|
||
|
||
const { calculateRow, calculateAll } = useCalculation(calculationRules);
|
||
|
||
// 초기 데이터에 계산 필드 적용
|
||
useEffect(() => {
|
||
if (value.length > 0 && calculationRules.length > 0) {
|
||
const calculated = calculateAll(value);
|
||
// 값이 실제로 변경된 경우만 업데이트
|
||
if (JSON.stringify(calculated) !== JSON.stringify(value)) {
|
||
onChange(calculated);
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
const handleAddItems = (items: any[]) => {
|
||
console.log("➕ handleAddItems 호출:", items.length, "개 항목");
|
||
console.log("📋 소스 데이터:", items);
|
||
|
||
// 매핑 규칙에 따라 데이터 변환
|
||
const mappedItems = items.map((sourceItem) => {
|
||
const newItem: any = {};
|
||
|
||
columns.forEach((col) => {
|
||
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 === "reference") {
|
||
// 외부 테이블 참조 (TODO: API 호출 필요)
|
||
console.log(` ⏳ 참조 조회 필요: ${col.mapping.referenceTable}.${col.mapping.referenceField}`);
|
||
// 현재는 빈 값으로 설정 (나중에 API 호출로 구현)
|
||
newItem[col.field] = undefined;
|
||
} 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);
|
||
}
|
||
});
|
||
|
||
console.log("📦 변환된 항목:", newItem);
|
||
return newItem;
|
||
});
|
||
|
||
// 계산 필드 업데이트
|
||
const calculatedItems = calculateAll(mappedItems);
|
||
|
||
// 기존 데이터에 추가
|
||
const newData = [...value, ...calculatedItems];
|
||
console.log("✅ 최종 데이터:", newData.length, "개 항목");
|
||
|
||
onChange(newData);
|
||
};
|
||
|
||
const handleRowChange = (index: number, newRow: any) => {
|
||
// 계산 필드 업데이트
|
||
const calculatedRow = calculateRow(newRow);
|
||
|
||
// 데이터 업데이트
|
||
const newData = [...value];
|
||
newData[index] = calculatedRow;
|
||
onChange(newData);
|
||
};
|
||
|
||
const handleRowDelete = (index: number) => {
|
||
const newData = value.filter((_, i) => i !== index);
|
||
onChange(newData);
|
||
};
|
||
|
||
// 컬럼명 -> 라벨명 매핑 생성
|
||
const columnLabels = columns.reduce((acc, col) => {
|
||
acc[col.field] = col.label;
|
||
return acc;
|
||
}, {} as Record<string, string>);
|
||
|
||
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}
|
||
onDataChange={onChange}
|
||
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}
|
||
columnLabels={columnLabels}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|