2025-10-16 15:05:24 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
import React, { useEffect, useRef, useCallback, useMemo, useState } from "react";
|
2025-10-16 15:05:24 +09:00
|
|
|
|
import { Layers } from "lucide-react";
|
|
|
|
|
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
|
|
|
|
|
import { ComponentDefinition, ComponentCategory, ComponentRendererProps } from "@/types/component";
|
|
|
|
|
|
import { RepeaterInput } from "@/components/webtypes/RepeaterInput";
|
|
|
|
|
|
import { RepeaterConfigPanel } from "@/components/webtypes/config/RepeaterConfigPanel";
|
2025-11-28 14:56:11 +09:00
|
|
|
|
import { useScreenContextOptional, DataReceivable } from "@/contexts/ScreenContext";
|
|
|
|
|
|
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
|
|
|
|
|
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
|
|
|
|
|
import { toast } from "sonner";
|
2025-11-28 18:35:34 +09:00
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-10-16 15:05:24 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Repeater Field Group 컴포넌트
|
|
|
|
|
|
*/
|
|
|
|
|
|
const RepeaterFieldGroupComponent: React.FC<ComponentRendererProps> = (props) => {
|
2025-11-28 14:56:11 +09:00
|
|
|
|
const { component, value, onChange, readonly, disabled, formData, onFormDataChange, menuObjid } = props;
|
|
|
|
|
|
const screenContext = useScreenContextOptional();
|
|
|
|
|
|
const splitPanelContext = useSplitPanelContext();
|
|
|
|
|
|
const receiverRef = useRef<DataReceivable | null>(null);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 그룹화된 데이터를 저장하는 상태
|
|
|
|
|
|
const [groupedData, setGroupedData] = useState<any[] | null>(null);
|
|
|
|
|
|
const [isLoadingGroupData, setIsLoadingGroupData] = useState(false);
|
|
|
|
|
|
const groupDataLoadedRef = useRef(false);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 원본 데이터 ID 목록 (삭제 추적용)
|
|
|
|
|
|
const [originalItemIds, setOriginalItemIds] = useState<string[]>([]);
|
2025-11-28 14:56:11 +09:00
|
|
|
|
|
2025-12-15 15:40:29 +09:00
|
|
|
|
// 🆕 DB에서 로드한 컬럼 정보 (webType 등)
|
|
|
|
|
|
const [columnInfo, setColumnInfo] = useState<Record<string, any>>({});
|
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// 컴포넌트의 필드명 (formData 키)
|
|
|
|
|
|
const fieldName = (component as any).columnName || component.id;
|
2025-10-16 15:05:24 +09:00
|
|
|
|
|
|
|
|
|
|
// repeaterConfig 또는 componentConfig에서 설정 가져오기
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const rawConfig = (component as any).repeaterConfig || component.componentConfig || { fields: [] };
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 그룹화 설정 (예: groupByColumn: "inbound_number")
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const groupByColumn = rawConfig.groupByColumn;
|
|
|
|
|
|
const targetTable = rawConfig.targetTable;
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 DB 컬럼 정보를 적용한 config 생성 (webType → type 매핑)
|
|
|
|
|
|
const config = useMemo(() => {
|
|
|
|
|
|
const rawFields = rawConfig.fields || [];
|
|
|
|
|
|
console.log("📋 [RepeaterFieldGroup] config 생성:", {
|
|
|
|
|
|
rawFieldsCount: rawFields.length,
|
|
|
|
|
|
rawFieldNames: rawFields.map((f: any) => f.name),
|
|
|
|
|
|
columnInfoKeys: Object.keys(columnInfo),
|
|
|
|
|
|
hasColumnInfo: Object.keys(columnInfo).length > 0,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const fields = rawFields.map((field: any) => {
|
|
|
|
|
|
const colInfo = columnInfo[field.name];
|
|
|
|
|
|
// DB의 webType 또는 web_type을 field.type으로 적용
|
|
|
|
|
|
const dbWebType = colInfo?.webType || colInfo?.web_type;
|
|
|
|
|
|
|
|
|
|
|
|
// 타입 오버라이드 조건:
|
|
|
|
|
|
// 1. field.type이 없거나
|
|
|
|
|
|
// 2. field.type이 'direct'(기본값)이고 DB에 더 구체적인 타입이 있는 경우
|
|
|
|
|
|
const shouldOverride = !field.type || (field.type === "direct" && dbWebType && dbWebType !== "text");
|
|
|
|
|
|
|
|
|
|
|
|
if (colInfo && dbWebType && shouldOverride) {
|
|
|
|
|
|
console.log(`✅ [RepeaterFieldGroup] 필드 타입 매핑: ${field.name} → ${dbWebType}`);
|
|
|
|
|
|
return { ...field, type: dbWebType };
|
|
|
|
|
|
}
|
|
|
|
|
|
return field;
|
|
|
|
|
|
});
|
|
|
|
|
|
return { ...rawConfig, fields };
|
|
|
|
|
|
}, [rawConfig, columnInfo]);
|
2025-10-16 15:05:24 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// formData에서 값 가져오기 (value prop보다 우선)
|
|
|
|
|
|
const rawValue = formData?.[fieldName] ?? value;
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 수정 모드 감지: formData에 id가 있고, fieldName으로 값을 찾지 못한 경우
|
|
|
|
|
|
// formData 자체를 배열의 첫 번째 항목으로 사용 (단일 행 수정 시)
|
|
|
|
|
|
const isEditMode = formData?.id && !rawValue && !value;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 반복 필드 그룹의 필드들이 formData에 있는지 확인
|
|
|
|
|
|
const configFields = config.fields || [];
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const hasRepeaterFieldsInFormData =
|
|
|
|
|
|
configFields.length > 0 && configFields.some((field: any) => formData?.[field.name] !== undefined);
|
2025-11-28 18:35:34 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 formData와 config.fields의 필드 이름 매칭 확인
|
|
|
|
|
|
const matchingFields = configFields.filter((field: any) => formData?.[field.name] !== undefined);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 그룹 키 값 (예: formData.inbound_number)
|
|
|
|
|
|
const groupKeyValue = groupByColumn ? formData?.[groupByColumn] : null;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 분할 패널 위치 및 좌측 선택 데이터 확인
|
|
|
|
|
|
const splitPanelPosition = screenContext?.splitPanelPosition;
|
|
|
|
|
|
const isRightPanel = splitPanelPosition === "right";
|
|
|
|
|
|
const selectedLeftData = splitPanelContext?.selectedLeftData;
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 연결 필터 설정에서 FK 컬럼 정보 가져오기
|
|
|
|
|
|
// screen-split-panel에서 설정한 linkedFilters 사용
|
|
|
|
|
|
const linkedFilters = splitPanelContext?.linkedFilters || [];
|
|
|
|
|
|
const getLinkedFilterValues = splitPanelContext?.getLinkedFilterValues;
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 FK 컬럼 설정 우선순위:
|
|
|
|
|
|
// 1. linkedFilters에서 targetTable에 해당하는 설정 찾기
|
|
|
|
|
|
// 2. config.fkColumn (컴포넌트 설정)
|
|
|
|
|
|
// 3. config.groupByColumn (그룹화 컬럼)
|
|
|
|
|
|
let fkSourceColumn: string | null = null;
|
|
|
|
|
|
let fkTargetColumn: string | null = null;
|
|
|
|
|
|
let linkedFilterTargetTable: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
// linkedFilters에서 FK 컬럼 찾기
|
|
|
|
|
|
if (linkedFilters.length > 0 && selectedLeftData) {
|
|
|
|
|
|
// 첫 번째 linkedFilter 사용 (일반적으로 하나만 설정됨)
|
|
|
|
|
|
const linkedFilter = linkedFilters[0];
|
|
|
|
|
|
fkSourceColumn = linkedFilter.sourceColumn;
|
|
|
|
|
|
|
|
|
|
|
|
// targetColumn이 "테이블명.컬럼명" 형식일 수 있음 → 분리
|
|
|
|
|
|
// 예: "dtg_maintenance_history.serial_no" → table: "dtg_maintenance_history", column: "serial_no"
|
|
|
|
|
|
const targetColumnParts = linkedFilter.targetColumn.split(".");
|
|
|
|
|
|
if (targetColumnParts.length === 2) {
|
|
|
|
|
|
linkedFilterTargetTable = targetColumnParts[0];
|
|
|
|
|
|
fkTargetColumn = targetColumnParts[1];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fkTargetColumn = linkedFilter.targetColumn;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 targetTable 우선순위: config.targetTable > linkedFilters에서 추출한 테이블
|
|
|
|
|
|
const effectiveTargetTable = targetTable || linkedFilterTargetTable;
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 DB에서 컬럼 정보 로드 (webType 등)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadColumnInfo = async () => {
|
|
|
|
|
|
if (!effectiveTargetTable) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${effectiveTargetTable}/columns`);
|
|
|
|
|
|
console.log("📋 [RepeaterFieldGroup] 컬럼 정보 응답:", response.data);
|
|
|
|
|
|
|
|
|
|
|
|
// 응답 구조에 따라 데이터 추출
|
|
|
|
|
|
// 실제 응답: { success: true, data: { columns: [...], page, size, total, totalPages } }
|
|
|
|
|
|
let columns: any[] = [];
|
|
|
|
|
|
if (response.data?.success && response.data?.data) {
|
|
|
|
|
|
// data.columns가 배열인 경우 (실제 응답 구조)
|
|
|
|
|
|
if (Array.isArray(response.data.data.columns)) {
|
|
|
|
|
|
columns = response.data.data.columns;
|
|
|
|
|
|
}
|
|
|
|
|
|
// data가 배열인 경우
|
|
|
|
|
|
else if (Array.isArray(response.data.data)) {
|
|
|
|
|
|
columns = response.data.data;
|
|
|
|
|
|
}
|
|
|
|
|
|
// data 자체가 객체이고 배열이 아닌 경우 (키-값 형태)
|
|
|
|
|
|
else if (typeof response.data.data === "object") {
|
|
|
|
|
|
columns = Object.values(response.data.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// success 없이 바로 배열인 경우
|
|
|
|
|
|
else if (Array.isArray(response.data)) {
|
|
|
|
|
|
columns = response.data;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📋 [RepeaterFieldGroup] 파싱된 컬럼 배열:", columns.length, "개");
|
|
|
|
|
|
|
|
|
|
|
|
if (columns.length > 0) {
|
|
|
|
|
|
const colMap: Record<string, any> = {};
|
|
|
|
|
|
columns.forEach((col: any) => {
|
|
|
|
|
|
// columnName 또는 column_name 또는 name 키 사용
|
|
|
|
|
|
const colName = col.columnName || col.column_name || col.name;
|
|
|
|
|
|
if (colName) {
|
|
|
|
|
|
colMap[colName] = col;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
setColumnInfo(colMap);
|
|
|
|
|
|
console.log("📋 [RepeaterFieldGroup] 컬럼 정보 로드 완료:", {
|
|
|
|
|
|
table: effectiveTargetTable,
|
|
|
|
|
|
columns: Object.keys(colMap),
|
|
|
|
|
|
webTypes: Object.entries(colMap).map(
|
|
|
|
|
|
([name, info]: [string, any]) => `${name}: ${info.webType || info.web_type || "unknown"}`,
|
|
|
|
|
|
),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ [RepeaterFieldGroup] 컬럼 정보 로드 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadColumnInfo();
|
|
|
|
|
|
}, [effectiveTargetTable]);
|
|
|
|
|
|
|
|
|
|
|
|
// linkedFilters가 없으면 config에서 가져오기
|
|
|
|
|
|
const fkColumn = fkTargetColumn || config.fkColumn || config.groupByColumn;
|
|
|
|
|
|
const fkValue =
|
|
|
|
|
|
fkSourceColumn && selectedLeftData
|
|
|
|
|
|
? selectedLeftData[fkSourceColumn]
|
|
|
|
|
|
: fkColumn && selectedLeftData
|
|
|
|
|
|
? selectedLeftData[fkColumn]
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔄 [RepeaterFieldGroup] 렌더링:", {
|
|
|
|
|
|
fieldName,
|
|
|
|
|
|
hasFormData: !!formData,
|
2025-11-28 18:35:34 +09:00
|
|
|
|
formDataId: formData?.id,
|
2025-11-28 14:56:11 +09:00
|
|
|
|
formDataValue: formData?.[fieldName],
|
|
|
|
|
|
propsValue: value,
|
|
|
|
|
|
rawValue,
|
2025-11-28 18:35:34 +09:00
|
|
|
|
isEditMode,
|
|
|
|
|
|
hasRepeaterFieldsInFormData,
|
|
|
|
|
|
configFieldNames: configFields.map((f: any) => f.name),
|
|
|
|
|
|
formDataKeys: formData ? Object.keys(formData) : [],
|
|
|
|
|
|
matchingFieldNames: matchingFields.map((f: any) => f.name),
|
|
|
|
|
|
groupByColumn,
|
|
|
|
|
|
groupKeyValue,
|
|
|
|
|
|
targetTable,
|
2025-12-15 15:40:29 +09:00
|
|
|
|
linkedFilterTargetTable,
|
|
|
|
|
|
effectiveTargetTable,
|
2025-11-28 18:35:34 +09:00
|
|
|
|
hasGroupedData: groupedData !== null,
|
|
|
|
|
|
groupedDataLength: groupedData?.length,
|
2025-12-15 15:40:29 +09:00
|
|
|
|
// 🆕 분할 패널 관련 정보
|
|
|
|
|
|
linkedFiltersCount: linkedFilters.length,
|
|
|
|
|
|
linkedFilters: linkedFilters.map((f) => `${f.sourceColumn} → ${f.targetColumn}`),
|
|
|
|
|
|
fkSourceColumn,
|
|
|
|
|
|
fkTargetColumn,
|
|
|
|
|
|
splitPanelPosition,
|
|
|
|
|
|
isRightPanel,
|
|
|
|
|
|
hasSelectedLeftData: !!selectedLeftData,
|
|
|
|
|
|
// 🆕 selectedLeftData 상세 정보 (디버깅용)
|
|
|
|
|
|
selectedLeftDataId: selectedLeftData?.id,
|
|
|
|
|
|
selectedLeftDataFkValue: fkSourceColumn ? selectedLeftData?.[fkSourceColumn] : "N/A",
|
|
|
|
|
|
selectedLeftData: selectedLeftData ? JSON.stringify(selectedLeftData).slice(0, 200) : null,
|
|
|
|
|
|
fkColumn,
|
|
|
|
|
|
fkValue,
|
2025-11-28 14:56:11 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 수정 모드에서 그룹화된 데이터 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadGroupedData = async () => {
|
|
|
|
|
|
// 이미 로드했거나 조건이 맞지 않으면 스킵
|
|
|
|
|
|
if (groupDataLoadedRef.current) return;
|
|
|
|
|
|
if (!isEditMode || !groupByColumn || !groupKeyValue || !targetTable) return;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
console.log("📥 [RepeaterFieldGroup] 그룹 데이터 로드 시작:", {
|
|
|
|
|
|
groupByColumn,
|
|
|
|
|
|
groupKeyValue,
|
|
|
|
|
|
targetTable,
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
setIsLoadingGroupData(true);
|
|
|
|
|
|
groupDataLoadedRef.current = true;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
try {
|
|
|
|
|
|
// API 호출: 같은 그룹 키를 가진 모든 데이터 조회
|
|
|
|
|
|
// search 파라미터 사용 (filters가 아닌 search)
|
|
|
|
|
|
const response = await apiClient.post(`/table-management/tables/${targetTable}/data`, {
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 100, // 충분히 큰 값
|
|
|
|
|
|
search: { [groupByColumn]: groupKeyValue },
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
console.log("🔍 [RepeaterFieldGroup] API 응답 구조:", {
|
|
|
|
|
|
success: response.data?.success,
|
|
|
|
|
|
hasData: !!response.data?.data,
|
|
|
|
|
|
dataType: typeof response.data?.data,
|
|
|
|
|
|
dataKeys: response.data?.data ? Object.keys(response.data.data) : [],
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 응답 구조: { success, data: { data: [...], total, page, totalPages } }
|
|
|
|
|
|
if (response.data?.success && response.data?.data?.data) {
|
|
|
|
|
|
const items = response.data.data.data; // 실제 데이터 배열
|
|
|
|
|
|
console.log("✅ [RepeaterFieldGroup] 그룹 데이터 로드 완료:", {
|
|
|
|
|
|
count: items.length,
|
|
|
|
|
|
groupByColumn,
|
|
|
|
|
|
groupKeyValue,
|
|
|
|
|
|
firstItem: items[0],
|
|
|
|
|
|
});
|
|
|
|
|
|
setGroupedData(items);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 원본 데이터 ID 목록 저장 (삭제 추적용)
|
2025-12-01 10:19:20 +09:00
|
|
|
|
const itemIds = items.map((item: any) => String(item.id || item.po_item_id || item.item_id)).filter(Boolean);
|
2025-11-28 18:35:34 +09:00
|
|
|
|
setOriginalItemIds(itemIds);
|
|
|
|
|
|
console.log("📋 [RepeaterFieldGroup] 원본 데이터 ID 목록 저장:", itemIds);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:19:20 +09:00
|
|
|
|
// 🆕 SplitPanelContext에 기존 항목 ID 등록 (좌측 테이블 필터링용)
|
|
|
|
|
|
if (splitPanelContext?.addItemIds && itemIds.length > 0) {
|
|
|
|
|
|
splitPanelContext.addItemIds(itemIds);
|
|
|
|
|
|
}
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// onChange 호출하여 부모에게 알림
|
|
|
|
|
|
if (onChange && items.length > 0) {
|
2026-01-12 17:24:25 +09:00
|
|
|
|
// 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출
|
|
|
|
|
|
const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name);
|
2025-11-28 18:35:34 +09:00
|
|
|
|
const dataWithMeta = items.map((item: any) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
_targetTable: targetTable,
|
|
|
|
|
|
_originalItemIds: itemIds, // 🆕 원본 ID 목록도 함께 전달
|
2025-12-05 17:28:44 +09:00
|
|
|
|
_existingRecord: !!item.id, // 🆕 기존 레코드 플래그 (id가 있으면 기존 레코드)
|
2026-01-12 17:24:25 +09:00
|
|
|
|
_repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록
|
2025-11-28 18:35:34 +09:00
|
|
|
|
}));
|
|
|
|
|
|
onChange(dataWithMeta);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn("⚠️ [RepeaterFieldGroup] 그룹 데이터 로드 실패:", response.data);
|
|
|
|
|
|
setGroupedData([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ [RepeaterFieldGroup] 그룹 데이터 로드 오류:", error);
|
|
|
|
|
|
setGroupedData([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoadingGroupData(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
loadGroupedData();
|
|
|
|
|
|
}, [isEditMode, groupByColumn, groupKeyValue, targetTable, onChange]);
|
|
|
|
|
|
|
2025-12-15 15:40:29 +09:00
|
|
|
|
// 🆕 분할 패널에서 좌측 데이터 선택 시 FK 기반으로 데이터 로드
|
|
|
|
|
|
// 좌측 테이블의 serial_no 등을 기준으로 우측 repeater 데이터 필터링
|
|
|
|
|
|
const prevFkValueRef = useRef<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadDataByFK = async () => {
|
|
|
|
|
|
// 우측 패널이 아니면 스킵
|
|
|
|
|
|
if (!isRightPanel) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 fkValue가 없거나 빈 값이면 빈 상태로 초기화
|
|
|
|
|
|
if (!fkValue || fkValue === "" || fkValue === null || fkValue === undefined) {
|
|
|
|
|
|
console.log("🔄 [RepeaterFieldGroup] FK 값 없음 - 빈 상태로 초기화:", {
|
|
|
|
|
|
fkColumn,
|
|
|
|
|
|
fkValue,
|
|
|
|
|
|
prevFkValue: prevFkValueRef.current,
|
|
|
|
|
|
});
|
|
|
|
|
|
// 이전에 데이터가 있었다면 초기화
|
|
|
|
|
|
if (prevFkValueRef.current !== null) {
|
|
|
|
|
|
setGroupedData([]);
|
|
|
|
|
|
setOriginalItemIds([]);
|
|
|
|
|
|
onChange?.([]);
|
|
|
|
|
|
prevFkValueRef.current = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FK 컬럼이나 타겟 테이블이 없으면 스킵
|
|
|
|
|
|
if (!fkColumn || !effectiveTargetTable) {
|
|
|
|
|
|
console.log("⏭️ [RepeaterFieldGroup] FK 기반 로드 스킵 (설정 부족):", {
|
|
|
|
|
|
fkColumn,
|
|
|
|
|
|
effectiveTargetTable,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 같은 FK 값으로 이미 로드했으면 스킵
|
|
|
|
|
|
const currentFkValueStr = String(fkValue);
|
|
|
|
|
|
if (prevFkValueRef.current === currentFkValueStr) {
|
|
|
|
|
|
console.log("⏭️ [RepeaterFieldGroup] 같은 FK 값 - 스킵:", currentFkValueStr);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
prevFkValueRef.current = currentFkValueStr;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📥 [RepeaterFieldGroup] 분할 패널 FK 기반 데이터 로드:", {
|
|
|
|
|
|
fkColumn,
|
|
|
|
|
|
fkValue,
|
|
|
|
|
|
effectiveTargetTable,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoadingGroupData(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// API 호출: FK 값을 기준으로 데이터 조회
|
|
|
|
|
|
const response = await apiClient.post(`/table-management/tables/${effectiveTargetTable}/data`, {
|
|
|
|
|
|
page: 1,
|
|
|
|
|
|
size: 100,
|
|
|
|
|
|
search: { [fkColumn]: fkValue },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.data?.success) {
|
|
|
|
|
|
const items = response.data?.data?.data || [];
|
|
|
|
|
|
console.log("✅ [RepeaterFieldGroup] FK 기반 데이터 로드 완료:", {
|
|
|
|
|
|
count: items.length,
|
|
|
|
|
|
fkColumn,
|
|
|
|
|
|
fkValue,
|
|
|
|
|
|
effectiveTargetTable,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 데이터가 있든 없든 항상 상태 업데이트 (빈 배열도 명확히 설정)
|
|
|
|
|
|
setGroupedData(items);
|
|
|
|
|
|
|
|
|
|
|
|
// 원본 데이터 ID 목록 저장
|
|
|
|
|
|
const itemIds = items.map((item: any) => String(item.id)).filter(Boolean);
|
|
|
|
|
|
setOriginalItemIds(itemIds);
|
|
|
|
|
|
|
|
|
|
|
|
// onChange 호출 (effectiveTargetTable 사용)
|
|
|
|
|
|
if (onChange) {
|
|
|
|
|
|
if (items.length > 0) {
|
2026-01-12 17:24:25 +09:00
|
|
|
|
// 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출
|
|
|
|
|
|
const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const dataWithMeta = items.map((item: any) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
_targetTable: effectiveTargetTable,
|
|
|
|
|
|
_existingRecord: !!item.id,
|
2026-01-12 17:24:25 +09:00
|
|
|
|
_repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록
|
2025-12-15 15:40:29 +09:00
|
|
|
|
}));
|
|
|
|
|
|
onChange(dataWithMeta);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 🆕 데이터가 없으면 빈 배열 전달 (이전 데이터 클리어)
|
|
|
|
|
|
console.log("ℹ️ [RepeaterFieldGroup] FK 기반 데이터 없음 - 빈 상태로 초기화");
|
|
|
|
|
|
onChange([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// API 실패 시 빈 배열로 설정
|
|
|
|
|
|
console.log("⚠️ [RepeaterFieldGroup] FK 기반 데이터 로드 실패 - 빈 상태로 초기화");
|
|
|
|
|
|
setGroupedData([]);
|
|
|
|
|
|
setOriginalItemIds([]);
|
|
|
|
|
|
onChange?.([]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("❌ [RepeaterFieldGroup] FK 기반 데이터 로드 오류:", error);
|
|
|
|
|
|
setGroupedData([]);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsLoadingGroupData(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadDataByFK();
|
|
|
|
|
|
}, [isRightPanel, fkColumn, fkValue, effectiveTargetTable, onChange]);
|
|
|
|
|
|
|
2025-10-16 15:05:24 +09:00
|
|
|
|
// 값이 JSON 문자열인 경우 파싱
|
|
|
|
|
|
let parsedValue: any[] = [];
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 그룹화된 데이터가 설정되어 있으면 우선 사용 (빈 배열 포함!)
|
|
|
|
|
|
// groupedData가 null이 아니면 (빈 배열이라도) 해당 값을 사용
|
|
|
|
|
|
if (groupedData !== null) {
|
2025-11-28 18:35:34 +09:00
|
|
|
|
parsedValue = groupedData;
|
|
|
|
|
|
} else if (isEditMode && hasRepeaterFieldsInFormData && !groupByColumn) {
|
|
|
|
|
|
// 그룹화 설정이 없는 경우에만 단일 행 사용
|
|
|
|
|
|
console.log("📝 [RepeaterFieldGroup] 수정 모드 - formData를 초기 데이터로 사용", {
|
|
|
|
|
|
formDataId: formData?.id,
|
|
|
|
|
|
matchingFieldsCount: matchingFields.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
parsedValue = [{ ...formData }];
|
|
|
|
|
|
} else if (typeof rawValue === "string" && rawValue.trim() !== "") {
|
|
|
|
|
|
// 빈 문자열이 아닌 경우에만 JSON 파싱 시도
|
2025-10-16 15:05:24 +09:00
|
|
|
|
try {
|
2025-11-28 14:56:11 +09:00
|
|
|
|
parsedValue = JSON.parse(rawValue);
|
2025-10-16 15:05:24 +09:00
|
|
|
|
} catch {
|
|
|
|
|
|
parsedValue = [];
|
|
|
|
|
|
}
|
2025-11-28 14:56:11 +09:00
|
|
|
|
} else if (Array.isArray(rawValue)) {
|
|
|
|
|
|
parsedValue = rawValue;
|
2025-10-16 15:05:24 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// parsedValue를 ref로 관리하여 최신 값 유지
|
|
|
|
|
|
const parsedValueRef = useRef(parsedValue);
|
|
|
|
|
|
parsedValueRef.current = parsedValue;
|
|
|
|
|
|
|
|
|
|
|
|
// onChange를 ref로 관리
|
|
|
|
|
|
const onChangeRef = useRef(onChange);
|
|
|
|
|
|
onChangeRef.current = onChange;
|
|
|
|
|
|
|
|
|
|
|
|
// onFormDataChange를 ref로 관리
|
|
|
|
|
|
const onFormDataChangeRef = useRef(onFormDataChange);
|
|
|
|
|
|
onFormDataChangeRef.current = onFormDataChange;
|
|
|
|
|
|
|
|
|
|
|
|
// fieldName을 ref로 관리
|
|
|
|
|
|
const fieldNameRef = useRef(fieldName);
|
|
|
|
|
|
fieldNameRef.current = fieldName;
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// config를 ref로 관리
|
|
|
|
|
|
const configRef = useRef(config);
|
|
|
|
|
|
configRef.current = config;
|
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// 데이터 수신 핸들러
|
|
|
|
|
|
const handleReceiveData = useCallback((data: any[], mappingRulesOrMode?: any[] | string) => {
|
|
|
|
|
|
console.log("📥 [RepeaterFieldGroup] 데이터 수신:", { data, mappingRulesOrMode });
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
if (!data || data.length === 0) {
|
|
|
|
|
|
toast.warning("전달할 데이터가 없습니다");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 매핑 규칙이 배열인 경우에만 적용
|
|
|
|
|
|
let processedData = data;
|
|
|
|
|
|
if (Array.isArray(mappingRulesOrMode) && mappingRulesOrMode.length > 0) {
|
|
|
|
|
|
processedData = applyMappingRules(data, mappingRulesOrMode);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터 정규화: 각 항목에서 실제 데이터 추출
|
|
|
|
|
|
// 데이터가 {0: {...}, inbound_type: "..."} 형태인 경우 처리
|
|
|
|
|
|
const normalizedData = processedData.map((item: any) => {
|
|
|
|
|
|
// item이 {0: {...실제데이터...}, 추가필드: 값} 형태인 경우
|
|
|
|
|
|
if (item && typeof item === "object" && item[0] && typeof item[0] === "object") {
|
|
|
|
|
|
// 0번 인덱스의 데이터와 나머지 필드를 병합
|
|
|
|
|
|
const { 0: originalData, ...additionalFields } = item;
|
|
|
|
|
|
return { ...originalData, ...additionalFields };
|
|
|
|
|
|
}
|
|
|
|
|
|
return item;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 🆕 정의된 필드만 필터링 (불필요한 필드 제거)
|
|
|
|
|
|
// 반복 필드 그룹에 정의된 필드 + 시스템 필드만 유지
|
|
|
|
|
|
const definedFields = configRef.current.fields || [];
|
|
|
|
|
|
const definedFieldNames = new Set(definedFields.map((f: any) => f.name));
|
2025-12-05 17:28:44 +09:00
|
|
|
|
// 시스템 필드 및 필수 필드 추가 (id는 제외 - 새 레코드로 처리하기 위해)
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const systemFields = new Set([
|
|
|
|
|
|
"_targetTable",
|
|
|
|
|
|
"_isNewItem",
|
|
|
|
|
|
"created_date",
|
|
|
|
|
|
"updated_date",
|
|
|
|
|
|
"writer",
|
|
|
|
|
|
"company_code",
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2025-11-28 18:35:34 +09:00
|
|
|
|
const filteredData = normalizedData.map((item: any) => {
|
|
|
|
|
|
const filteredItem: Record<string, any> = {};
|
2025-12-15 15:40:29 +09:00
|
|
|
|
Object.keys(item).forEach((key) => {
|
2025-12-05 17:28:44 +09:00
|
|
|
|
// 🆕 id 필드는 제외 (새 레코드로 저장되도록)
|
2025-12-15 15:40:29 +09:00
|
|
|
|
if (key === "id") {
|
2025-12-05 17:28:44 +09:00
|
|
|
|
return; // id 필드 제외
|
|
|
|
|
|
}
|
2025-11-28 18:35:34 +09:00
|
|
|
|
// 정의된 필드이거나 시스템 필드인 경우만 포함
|
|
|
|
|
|
if (definedFieldNames.has(key) || systemFields.has(key)) {
|
|
|
|
|
|
filteredItem[key] = item[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-05 17:28:44 +09:00
|
|
|
|
// 🆕 새 항목임을 표시하는 플래그 추가
|
|
|
|
|
|
filteredItem._isNewItem = true;
|
2025-11-28 18:35:34 +09:00
|
|
|
|
return filteredItem;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
console.log("📥 [RepeaterFieldGroup] 정규화된 데이터:", normalizedData);
|
2025-11-28 18:35:34 +09:00
|
|
|
|
console.log("📥 [RepeaterFieldGroup] 필터링된 데이터:", filteredData);
|
2025-11-28 14:56:11 +09:00
|
|
|
|
|
|
|
|
|
|
// 기존 데이터에 새 데이터 추가 (기본 모드: append)
|
|
|
|
|
|
const currentValue = parsedValueRef.current;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// mode가 "replace"인 경우 기존 데이터 대체, 그 외에는 추가
|
|
|
|
|
|
const mode = typeof mappingRulesOrMode === "string" ? mappingRulesOrMode : "append";
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
let newItems: any[];
|
|
|
|
|
|
let addedCount = 0;
|
|
|
|
|
|
let duplicateCount = 0;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
if (mode === "replace") {
|
|
|
|
|
|
newItems = filteredData;
|
|
|
|
|
|
addedCount = filteredData.length;
|
|
|
|
|
|
} else {
|
2025-12-05 17:28:44 +09:00
|
|
|
|
// 🆕 중복 체크: item_code를 기준으로 이미 존재하는 항목 제외 (id는 사용하지 않음)
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const existingItemCodes = new Set(currentValue.map((item: any) => item.item_code).filter(Boolean));
|
|
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
const uniqueNewItems = filteredData.filter((item: any) => {
|
2025-12-05 17:28:44 +09:00
|
|
|
|
const itemCode = item.item_code;
|
|
|
|
|
|
if (itemCode && existingItemCodes.has(itemCode)) {
|
2025-12-01 10:09:19 +09:00
|
|
|
|
duplicateCount++;
|
|
|
|
|
|
return false; // 중복 항목 제외
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
newItems = [...currentValue, ...uniqueNewItems];
|
|
|
|
|
|
addedCount = uniqueNewItems.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 15:40:29 +09:00
|
|
|
|
console.log("📥 [RepeaterFieldGroup] 최종 데이터:", {
|
|
|
|
|
|
currentValue,
|
|
|
|
|
|
newItems,
|
2025-12-01 10:09:19 +09:00
|
|
|
|
mode,
|
|
|
|
|
|
addedCount,
|
|
|
|
|
|
duplicateCount,
|
|
|
|
|
|
});
|
2025-11-28 14:56:11 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
// 🆕 groupedData 상태도 직접 업데이트 (UI 즉시 반영)
|
|
|
|
|
|
setGroupedData(newItems);
|
2025-11-28 14:56:11 +09:00
|
|
|
|
|
2025-12-01 10:19:20 +09:00
|
|
|
|
// 🆕 SplitPanelContext에 추가된 항목 ID 등록 (좌측 테이블 필터링용)
|
2025-12-05 17:28:44 +09:00
|
|
|
|
// item_code를 기준으로 등록 (id는 새 레코드라 없을 수 있음)
|
2025-12-01 10:19:20 +09:00
|
|
|
|
if (splitPanelContext?.addItemIds && addedCount > 0) {
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const newItemCodes = newItems.map((item: any) => String(item.item_code)).filter(Boolean);
|
2025-12-05 17:28:44 +09:00
|
|
|
|
splitPanelContext.addItemIds(newItemCodes);
|
2025-12-01 10:19:20 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// JSON 문자열로 변환하여 저장
|
|
|
|
|
|
const jsonValue = JSON.stringify(newItems);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
console.log("📥 [RepeaterFieldGroup] onChange/onFormDataChange 호출:", {
|
|
|
|
|
|
jsonValue,
|
2025-11-28 14:56:11 +09:00
|
|
|
|
hasOnChange: !!onChangeRef.current,
|
|
|
|
|
|
hasOnFormDataChange: !!onFormDataChangeRef.current,
|
|
|
|
|
|
fieldName: fieldNameRef.current,
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
// onFormDataChange가 있으면 우선 사용 (EmbeddedScreen의 formData 상태 업데이트)
|
|
|
|
|
|
if (onFormDataChangeRef.current) {
|
|
|
|
|
|
onFormDataChangeRef.current(fieldNameRef.current, jsonValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 그렇지 않으면 onChange 사용
|
|
|
|
|
|
else if (onChangeRef.current) {
|
|
|
|
|
|
onChangeRef.current(jsonValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
// 결과 메시지 표시
|
|
|
|
|
|
if (addedCount > 0) {
|
|
|
|
|
|
if (duplicateCount > 0) {
|
|
|
|
|
|
toast.success(`${addedCount}개 항목이 추가되었습니다 (${duplicateCount}개 중복 제외)`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.success(`${addedCount}개 항목이 추가되었습니다`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (duplicateCount > 0) {
|
|
|
|
|
|
toast.warning(`${duplicateCount}개 항목이 이미 추가되어 있습니다`);
|
|
|
|
|
|
}
|
2025-11-28 14:56:11 +09:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// DataReceivable 인터페이스 구현
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const dataReceiver = useMemo<DataReceivable>(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
componentType: "repeater-field-group",
|
|
|
|
|
|
receiveData: handleReceiveData,
|
|
|
|
|
|
}),
|
|
|
|
|
|
[component.id, handleReceiveData],
|
|
|
|
|
|
);
|
2025-11-28 14:56:11 +09:00
|
|
|
|
|
|
|
|
|
|
// ScreenContext에 데이터 수신자로 등록
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (screenContext && component.id) {
|
|
|
|
|
|
console.log("📋 [RepeaterFieldGroup] ScreenContext에 데이터 수신자 등록:", component.id);
|
|
|
|
|
|
screenContext.registerDataReceiver(component.id, dataReceiver);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
return () => {
|
|
|
|
|
|
screenContext.unregisterDataReceiver(component.id);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [screenContext, component.id, dataReceiver]);
|
|
|
|
|
|
|
|
|
|
|
|
// SplitPanelContext에 데이터 수신자로 등록 (분할 패널 내에서만)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const splitPanelPosition = screenContext?.splitPanelPosition;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
if (splitPanelContext?.isInSplitPanel && splitPanelPosition && component.id) {
|
|
|
|
|
|
console.log("🔗 [RepeaterFieldGroup] SplitPanelContext에 데이터 수신자 등록:", {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
position: splitPanelPosition,
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
splitPanelContext.registerReceiver(splitPanelPosition, component.id, dataReceiver);
|
|
|
|
|
|
receiverRef.current = dataReceiver;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-11-28 14:56:11 +09:00
|
|
|
|
return () => {
|
|
|
|
|
|
console.log("🔗 [RepeaterFieldGroup] SplitPanelContext에서 데이터 수신자 해제:", component.id);
|
|
|
|
|
|
splitPanelContext.unregisterReceiver(splitPanelPosition, component.id);
|
|
|
|
|
|
receiverRef.current = null;
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [splitPanelContext, screenContext?.splitPanelPosition, component.id, dataReceiver]);
|
|
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
// 🆕 전역 이벤트 리스너 (splitPanelDataTransfer)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleSplitPanelDataTransfer = (event: CustomEvent) => {
|
|
|
|
|
|
const { data, mode, mappingRules } = event.detail;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
console.log("📥 [RepeaterFieldGroup] splitPanelDataTransfer 이벤트 수신:", {
|
|
|
|
|
|
dataCount: data?.length,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
});
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
// 우측 패널의 리피터 필드 그룹만 데이터를 수신
|
|
|
|
|
|
const splitPanelPosition = screenContext?.splitPanelPosition;
|
|
|
|
|
|
if (splitPanelPosition === "right" && data && data.length > 0) {
|
|
|
|
|
|
handleReceiveData(data, mappingRules || mode || "append");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener);
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
2025-12-01 10:09:19 +09:00
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [screenContext?.splitPanelPosition, handleReceiveData, component.id]);
|
|
|
|
|
|
|
2025-12-01 10:19:20 +09:00
|
|
|
|
// 🆕 RepeaterInput에서 항목 변경 시 SplitPanelContext의 addedItemIds 동기화
|
2025-12-15 15:40:29 +09:00
|
|
|
|
const handleRepeaterChange = useCallback(
|
|
|
|
|
|
(newValue: any[]) => {
|
2026-01-12 17:24:25 +09:00
|
|
|
|
// 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출
|
|
|
|
|
|
const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 모든 항목에 메타데이터 추가
|
|
|
|
|
|
let valueWithMeta = newValue.map((item: any) => ({
|
|
|
|
|
|
...item,
|
|
|
|
|
|
_targetTable: effectiveTargetTable || targetTable,
|
|
|
|
|
|
_existingRecord: !!item.id,
|
|
|
|
|
|
_repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 분할 패널에서 우측인 경우, FK 값 추가
|
|
|
|
|
|
if (isRightPanel && fkColumn && fkValue) {
|
|
|
|
|
|
valueWithMeta = valueWithMeta.map((item: any) => {
|
|
|
|
|
|
if (item._isNewItem) {
|
|
|
|
|
|
console.log("🔗 [RepeaterFieldGroup] 새 항목에 FK 값 추가:", { fkColumn, fkValue });
|
|
|
|
|
|
return { ...item, [fkColumn]: fkValue };
|
2025-12-15 15:40:29 +09:00
|
|
|
|
}
|
2026-01-12 17:24:25 +09:00
|
|
|
|
return item;
|
2025-12-15 15:40:29 +09:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 배열을 JSON 문자열로 변환하여 저장
|
|
|
|
|
|
const jsonValue = JSON.stringify(valueWithMeta);
|
|
|
|
|
|
console.log("📤 [RepeaterFieldGroup] 데이터 변경:", {
|
|
|
|
|
|
fieldName,
|
|
|
|
|
|
itemCount: valueWithMeta.length,
|
|
|
|
|
|
isRightPanel,
|
|
|
|
|
|
hasScreenContextUpdateFormData: !!screenContext?.updateFormData,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 분할 패널 우측에서는 ScreenContext.updateFormData만 사용
|
|
|
|
|
|
// (중복 저장 방지: onChange/onFormDataChange는 부모에게 전달되어 다시 formData로 돌아옴)
|
|
|
|
|
|
if (isRightPanel && screenContext?.updateFormData) {
|
|
|
|
|
|
screenContext.updateFormData(fieldName, jsonValue);
|
|
|
|
|
|
console.log("📤 [RepeaterFieldGroup] screenContext.updateFormData 호출 (우측 패널):", { fieldName });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 분할 패널이 아니거나 좌측 패널인 경우 기존 방식 사용
|
|
|
|
|
|
onChange?.(jsonValue);
|
|
|
|
|
|
if (onFormDataChange) {
|
|
|
|
|
|
onFormDataChange(fieldName, jsonValue);
|
|
|
|
|
|
console.log("📤 [RepeaterFieldGroup] onFormDataChange(props) 호출:", { fieldName });
|
|
|
|
|
|
}
|
2025-12-01 10:19:20 +09:00
|
|
|
|
}
|
2025-12-15 15:40:29 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 groupedData 상태도 업데이트
|
|
|
|
|
|
setGroupedData(valueWithMeta);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 SplitPanelContext의 addedItemIds 동기화
|
|
|
|
|
|
if (splitPanelContext?.isInSplitPanel && screenContext?.splitPanelPosition === "right") {
|
|
|
|
|
|
// 현재 항목들의 ID 목록
|
|
|
|
|
|
const currentIds = newValue
|
|
|
|
|
|
.map((item: any) => String(item.id || item.po_item_id || item.item_id))
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
// 기존 addedItemIds와 비교하여 삭제된 ID 찾기
|
|
|
|
|
|
const addedIds = splitPanelContext.addedItemIds;
|
|
|
|
|
|
const removedIds = Array.from(addedIds).filter((id) => !currentIds.includes(id));
|
|
|
|
|
|
|
|
|
|
|
|
if (removedIds.length > 0) {
|
|
|
|
|
|
console.log("🗑️ [RepeaterFieldGroup] 삭제된 항목 ID 제거:", removedIds);
|
|
|
|
|
|
splitPanelContext.removeItemIds(removedIds);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 새로 추가된 ID가 있으면 등록
|
|
|
|
|
|
const newIds = currentIds.filter((id: string) => !addedIds.has(id));
|
|
|
|
|
|
if (newIds.length > 0) {
|
|
|
|
|
|
console.log("➕ [RepeaterFieldGroup] 새 항목 ID 추가:", newIds);
|
|
|
|
|
|
splitPanelContext.addItemIds(newIds);
|
|
|
|
|
|
}
|
2025-12-01 10:19:20 +09:00
|
|
|
|
}
|
2025-12-15 15:40:29 +09:00
|
|
|
|
},
|
|
|
|
|
|
[
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
onFormDataChange,
|
|
|
|
|
|
splitPanelContext,
|
|
|
|
|
|
screenContext?.splitPanelPosition,
|
|
|
|
|
|
screenContext?.updateFormData,
|
|
|
|
|
|
isRightPanel,
|
|
|
|
|
|
effectiveTargetTable,
|
2026-01-12 17:24:25 +09:00
|
|
|
|
targetTable,
|
2025-12-15 15:40:29 +09:00
|
|
|
|
fkColumn,
|
|
|
|
|
|
fkValue,
|
|
|
|
|
|
fieldName,
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 config에 effectiveTargetTable 병합 (linkedFilters에서 추출된 테이블도 포함)
|
|
|
|
|
|
const effectiveConfig = {
|
|
|
|
|
|
...config,
|
|
|
|
|
|
targetTable: effectiveTargetTable || config.targetTable,
|
|
|
|
|
|
};
|
2025-12-01 10:19:20 +09:00
|
|
|
|
|
2025-10-16 15:05:24 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<RepeaterInput
|
|
|
|
|
|
value={parsedValue}
|
2025-12-01 10:19:20 +09:00
|
|
|
|
onChange={handleRepeaterChange}
|
2025-12-15 15:40:29 +09:00
|
|
|
|
config={effectiveConfig}
|
2025-10-16 15:05:24 +09:00
|
|
|
|
disabled={disabled}
|
|
|
|
|
|
readonly={readonly}
|
2025-11-28 14:56:11 +09:00
|
|
|
|
menuObjid={menuObjid}
|
2025-10-16 15:05:24 +09:00
|
|
|
|
className="w-full"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Repeater Field Group 렌더러
|
|
|
|
|
|
* 여러 필드를 가진 항목들을 동적으로 추가/제거할 수 있는 컴포넌트
|
|
|
|
|
|
*/
|
|
|
|
|
|
export class RepeaterFieldGroupRenderer extends AutoRegisteringComponentRenderer {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 컴포넌트 정의
|
|
|
|
|
|
*/
|
|
|
|
|
|
static componentDefinition: ComponentDefinition = {
|
|
|
|
|
|
id: "repeater-field-group",
|
|
|
|
|
|
name: "반복 필드 그룹",
|
|
|
|
|
|
nameEng: "Repeater Field Group",
|
|
|
|
|
|
description: "여러 필드를 가진 항목들을 동적으로 추가/제거할 수 있는 반복 가능한 필드 그룹",
|
|
|
|
|
|
category: ComponentCategory.INPUT,
|
|
|
|
|
|
webType: "array", // 배열 데이터를 다룸
|
|
|
|
|
|
icon: Layers,
|
|
|
|
|
|
component: RepeaterFieldGroupRenderer,
|
|
|
|
|
|
configPanel: RepeaterConfigPanel,
|
|
|
|
|
|
defaultSize: {
|
|
|
|
|
|
width: 600,
|
2025-10-17 15:31:23 +09:00
|
|
|
|
height: 200, // 기본 높이 조정
|
2025-10-16 15:05:24 +09:00
|
|
|
|
},
|
|
|
|
|
|
defaultConfig: {
|
|
|
|
|
|
fields: [], // 빈 배열로 시작 - 사용자가 직접 필드 추가
|
|
|
|
|
|
minItems: 1, // 기본 1개 항목
|
|
|
|
|
|
maxItems: 20,
|
|
|
|
|
|
addButtonText: "항목 추가",
|
|
|
|
|
|
allowReorder: true,
|
|
|
|
|
|
showIndex: true,
|
|
|
|
|
|
collapsible: false,
|
|
|
|
|
|
layout: "grid",
|
|
|
|
|
|
showDivider: true,
|
|
|
|
|
|
emptyMessage: "필드를 먼저 정의하세요.",
|
|
|
|
|
|
},
|
|
|
|
|
|
tags: ["repeater", "fieldgroup", "dynamic", "multi", "form", "array", "fields"],
|
|
|
|
|
|
author: "System",
|
|
|
|
|
|
version: "1.0.0",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 컴포넌트 렌더링
|
|
|
|
|
|
*/
|
|
|
|
|
|
render(): React.ReactElement {
|
|
|
|
|
|
return <RepeaterFieldGroupComponent {...this.props} />;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 자동 등록
|
|
|
|
|
|
RepeaterFieldGroupRenderer.registerSelf();
|
|
|
|
|
|
|
|
|
|
|
|
// Hot Reload 지원 (개발 모드)
|
|
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
|
|
|
|
RepeaterFieldGroupRenderer.enableHotReload();
|
|
|
|
|
|
}
|