238 lines
8.5 KiB
TypeScript
238 lines
8.5 KiB
TypeScript
"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<ComponentRendererProps> = (props) => {
|
|
const { component, value, onChange, readonly, disabled, formData, onFormDataChange, menuObjid } = props;
|
|
const screenContext = useScreenContextOptional();
|
|
const splitPanelContext = useSplitPanelContext();
|
|
const receiverRef = useRef<DataReceivable | null>(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<DataReceivable>(() => ({
|
|
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 (
|
|
<RepeaterInput
|
|
value={parsedValue}
|
|
onChange={(newValue) => {
|
|
// 배열을 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 <RepeaterFieldGroupComponent {...this.props} />;
|
|
}
|
|
}
|
|
|
|
// 컴포넌트 자동 등록
|
|
RepeaterFieldGroupRenderer.registerSelf();
|
|
|
|
// Hot Reload 지원 (개발 모드)
|
|
if (process.env.NODE_ENV === "development") {
|
|
RepeaterFieldGroupRenderer.enableHotReload();
|
|
}
|