ERP-node/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent...

409 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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, JoinCondition } from "./types";
import { useCalculation } from "./useCalculation";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
interface ModalRepeaterTableComponentProps extends Partial<ModalRepeaterTableProps> {
config?: ModalRepeaterTableProps;
}
/**
* 외부 테이블에서 참조 값을 조회하는 함수
* @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;
}
}
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 = 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 = [...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>
);
}