"use client"; import React, { useEffect, useRef, useCallback, useMemo } from "react"; 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"; import { useScreenContextOptional, DataReceivable } from "@/contexts/ScreenContext"; import { useSplitPanelContext } from "@/contexts/SplitPanelContext"; import { applyMappingRules } from "@/lib/utils/dataMapping"; import { toast } from "sonner"; /** * Repeater Field Group 컴포넌트 */ const RepeaterFieldGroupComponent: React.FC = (props) => { const { component, value, onChange, readonly, disabled, formData, onFormDataChange, menuObjid } = props; const screenContext = useScreenContextOptional(); const splitPanelContext = useSplitPanelContext(); const receiverRef = useRef(null); // 컴포넌트의 필드명 (formData 키) const fieldName = (component as any).columnName || component.id; // repeaterConfig 또는 componentConfig에서 설정 가져오기 const config = (component as any).repeaterConfig || component.componentConfig || { fields: [] }; // formData에서 값 가져오기 (value prop보다 우선) const rawValue = formData?.[fieldName] ?? value; console.log("🔄 [RepeaterFieldGroup] 렌더링:", { fieldName, hasFormData: !!formData, formDataValue: formData?.[fieldName], propsValue: value, rawValue, }); // 값이 JSON 문자열인 경우 파싱 let parsedValue: any[] = []; if (typeof rawValue === "string") { try { parsedValue = JSON.parse(rawValue); } catch { parsedValue = []; } } else if (Array.isArray(rawValue)) { parsedValue = rawValue; } // 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; // 데이터 수신 핸들러 const handleReceiveData = useCallback((data: any[], mappingRulesOrMode?: any[] | string) => { console.log("📥 [RepeaterFieldGroup] 데이터 수신:", { data, mappingRulesOrMode }); 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; }); console.log("📥 [RepeaterFieldGroup] 정규화된 데이터:", normalizedData); // 기존 데이터에 새 데이터 추가 (기본 모드: append) const currentValue = parsedValueRef.current; // mode가 "replace"인 경우 기존 데이터 대체, 그 외에는 추가 const mode = typeof mappingRulesOrMode === "string" ? mappingRulesOrMode : "append"; const newItems = mode === "replace" ? normalizedData : [...currentValue, ...normalizedData]; console.log("📥 [RepeaterFieldGroup] 최종 데이터:", { currentValue, newItems, mode }); // JSON 문자열로 변환하여 저장 const jsonValue = JSON.stringify(newItems); console.log("📥 [RepeaterFieldGroup] onChange/onFormDataChange 호출:", { jsonValue, hasOnChange: !!onChangeRef.current, hasOnFormDataChange: !!onFormDataChangeRef.current, fieldName: fieldNameRef.current, }); // onFormDataChange가 있으면 우선 사용 (EmbeddedScreen의 formData 상태 업데이트) if (onFormDataChangeRef.current) { onFormDataChangeRef.current(fieldNameRef.current, jsonValue); } // 그렇지 않으면 onChange 사용 else if (onChangeRef.current) { onChangeRef.current(jsonValue); } toast.success(`${normalizedData.length}개 항목이 추가되었습니다`); }, []); // DataReceivable 인터페이스 구현 const dataReceiver = useMemo(() => ({ componentId: component.id, componentType: "repeater-field-group", receiveData: handleReceiveData, }), [component.id, handleReceiveData]); // ScreenContext에 데이터 수신자로 등록 useEffect(() => { if (screenContext && component.id) { console.log("📋 [RepeaterFieldGroup] ScreenContext에 데이터 수신자 등록:", component.id); screenContext.registerDataReceiver(component.id, dataReceiver); return () => { screenContext.unregisterDataReceiver(component.id); }; } }, [screenContext, component.id, dataReceiver]); // SplitPanelContext에 데이터 수신자로 등록 (분할 패널 내에서만) useEffect(() => { const splitPanelPosition = screenContext?.splitPanelPosition; if (splitPanelContext?.isInSplitPanel && splitPanelPosition && component.id) { console.log("🔗 [RepeaterFieldGroup] SplitPanelContext에 데이터 수신자 등록:", { componentId: component.id, position: splitPanelPosition, }); splitPanelContext.registerReceiver(splitPanelPosition, component.id, dataReceiver); receiverRef.current = dataReceiver; return () => { console.log("🔗 [RepeaterFieldGroup] SplitPanelContext에서 데이터 수신자 해제:", component.id); splitPanelContext.unregisterReceiver(splitPanelPosition, component.id); receiverRef.current = null; }; } }, [splitPanelContext, screenContext?.splitPanelPosition, component.id, dataReceiver]); return ( { // 배열을 JSON 문자열로 변환하여 저장 const jsonValue = JSON.stringify(newValue); onChange?.(jsonValue); }} config={config} disabled={disabled} readonly={readonly} menuObjid={menuObjid} 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, height: 200, // 기본 높이 조정 }, 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 ; } } // 컴포넌트 자동 등록 RepeaterFieldGroupRenderer.registerSelf(); // Hot Reload 지원 (개발 모드) if (process.env.NODE_ENV === "development") { RepeaterFieldGroupRenderer.enableHotReload(); }