276 lines
9.3 KiB
TypeScript
276 lines
9.3 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { ConditionalContainerProps, ConditionalSection } from "./types";
|
|
import { ConditionalSectionViewer } from "./ConditionalSectionViewer";
|
|
import { cn } from "@/lib/utils";
|
|
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
|
import type { DataProvidable } from "@/types/data-transfer";
|
|
|
|
/**
|
|
* 조건부 컨테이너 컴포넌트
|
|
* 상단 셀렉트박스 값에 따라 하단에 다른 UI를 표시
|
|
*/
|
|
export function ConditionalContainerComponent({
|
|
config,
|
|
controlField: propControlField,
|
|
controlLabel: propControlLabel,
|
|
sections: propSections,
|
|
defaultValue: propDefaultValue,
|
|
showBorder: propShowBorder,
|
|
spacing: propSpacing,
|
|
value,
|
|
onChange,
|
|
formData,
|
|
onFormDataChange,
|
|
isDesignMode = false,
|
|
onUpdateComponent,
|
|
onDeleteComponent,
|
|
onSelectComponent,
|
|
selectedComponentId,
|
|
onHeightChange,
|
|
componentId,
|
|
style,
|
|
className,
|
|
groupedData, // 🆕 그룹 데이터
|
|
onSave, // 🆕 EditModal의 handleSave 콜백
|
|
initialData, // 🆕 수정 모드: 초기 데이터 (발주일, 담당자, 메모 등)
|
|
}: ConditionalContainerProps) {
|
|
// 🔍 디버그: initialData 수신 확인
|
|
React.useEffect(() => {
|
|
console.log("[ConditionalContainer] initialData 수신:", {
|
|
hasInitialData: !!initialData,
|
|
initialDataKeys: initialData ? Object.keys(initialData) : [],
|
|
initialData,
|
|
});
|
|
}, [initialData]);
|
|
|
|
// 화면 컨텍스트 (데이터 제공자로 등록)
|
|
const screenContext = useScreenContextOptional();
|
|
|
|
// config prop 우선, 없으면 개별 prop 사용
|
|
const controlField = config?.controlField || propControlField || "condition";
|
|
const controlLabel = config?.controlLabel || propControlLabel || "조건 선택";
|
|
const sections = config?.sections || propSections || [];
|
|
const defaultValue = config?.defaultValue || propDefaultValue || sections[0]?.condition;
|
|
const showBorder = config?.showBorder ?? propShowBorder ?? true;
|
|
const spacing = config?.spacing || propSpacing || "normal";
|
|
|
|
// 초기값 계산 (한 번만)
|
|
const initialValue = React.useMemo(() => {
|
|
return value || formData?.[controlField] || defaultValue || "";
|
|
}, []); // 의존성 없음 - 마운트 시 한 번만 계산
|
|
|
|
// 현재 선택된 값
|
|
const [selectedValue, setSelectedValue] = useState<string>(initialValue);
|
|
|
|
// 최신 값을 ref로 유지 (클로저 문제 방지)
|
|
const selectedValueRef = React.useRef(selectedValue);
|
|
selectedValueRef.current = selectedValue; // 렌더링마다 업데이트 (useEffect 대신)
|
|
|
|
// 콜백 refs (의존성 제거)
|
|
const onChangeRef = React.useRef(onChange);
|
|
const onFormDataChangeRef = React.useRef(onFormDataChange);
|
|
onChangeRef.current = onChange;
|
|
onFormDataChangeRef.current = onFormDataChange;
|
|
|
|
// 값 변경 핸들러 - 의존성 없음
|
|
const handleValueChange = React.useCallback((newValue: string) => {
|
|
// 같은 값이면 무시
|
|
if (newValue === selectedValueRef.current) return;
|
|
|
|
setSelectedValue(newValue);
|
|
|
|
if (onChangeRef.current) {
|
|
onChangeRef.current(newValue);
|
|
}
|
|
|
|
if (onFormDataChangeRef.current) {
|
|
onFormDataChangeRef.current(controlField, newValue);
|
|
}
|
|
}, [controlField]);
|
|
|
|
// sectionsRef 추가 (dataProvider에서 사용)
|
|
const sectionsRef = React.useRef(sections);
|
|
React.useEffect(() => {
|
|
sectionsRef.current = sections;
|
|
}, [sections]);
|
|
|
|
// dataProvider를 useMemo로 감싸서 불필요한 재생성 방지
|
|
const dataProvider = React.useMemo<DataProvidable>(() => ({
|
|
componentId: componentId || "conditional-container",
|
|
componentType: "conditional-container",
|
|
|
|
getSelectedData: () => {
|
|
// ref를 통해 최신 값 참조 (클로저 문제 방지)
|
|
const currentValue = selectedValueRef.current;
|
|
const currentSections = sectionsRef.current;
|
|
return [{
|
|
[controlField]: currentValue,
|
|
condition: currentValue,
|
|
label: currentSections.find(s => s.condition === currentValue)?.label || currentValue,
|
|
}];
|
|
},
|
|
|
|
getAllData: () => {
|
|
const currentSections = sectionsRef.current;
|
|
return currentSections.map(section => ({
|
|
condition: section.condition,
|
|
label: section.label,
|
|
}));
|
|
},
|
|
|
|
clearSelection: () => {
|
|
// 조건부 컨테이너는 초기화하지 않음
|
|
console.log("조건부 컨테이너는 선택 초기화를 지원하지 않습니다.");
|
|
},
|
|
}), [componentId, controlField]); // selectedValue, sections는 ref로 참조
|
|
|
|
// 화면 컨텍스트에 데이터 제공자로 등록
|
|
useEffect(() => {
|
|
if (screenContext && componentId) {
|
|
screenContext.registerDataProvider(componentId, dataProvider);
|
|
|
|
return () => {
|
|
screenContext.unregisterDataProvider(componentId);
|
|
};
|
|
}
|
|
}, [screenContext, componentId, dataProvider]);
|
|
|
|
// 컨테이너 높이 측정용 ref
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const previousHeightRef = useRef<number>(0);
|
|
|
|
// 높이 변화 감지 및 콜백 호출
|
|
useEffect(() => {
|
|
if (!containerRef.current || isDesignMode || !onHeightChange) return;
|
|
|
|
const resizeObserver = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
const newHeight = entry.contentRect.height;
|
|
|
|
// 높이가 실제로 변경되었을 때만 콜백 호출
|
|
if (Math.abs(newHeight - previousHeightRef.current) > 5) {
|
|
console.log(`📏 조건부 컨테이너 높이 변화: ${previousHeightRef.current}px → ${newHeight}px`);
|
|
previousHeightRef.current = newHeight;
|
|
onHeightChange(newHeight);
|
|
}
|
|
}
|
|
});
|
|
|
|
resizeObserver.observe(containerRef.current);
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
};
|
|
}, [isDesignMode, onHeightChange, selectedValue]); // selectedValue 변경 시에도 감지
|
|
|
|
// 간격 스타일
|
|
const spacingClass = {
|
|
tight: "space-y-2",
|
|
normal: "space-y-4",
|
|
loose: "space-y-8",
|
|
}[spacing];
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={cn("w-full flex flex-col", spacingClass, className)}
|
|
style={style}
|
|
>
|
|
{/* 제어 셀렉트박스 */}
|
|
<div className="space-y-2 flex-shrink-0">
|
|
<Label htmlFor={controlField} className="text-xs sm:text-sm">
|
|
{controlLabel}
|
|
</Label>
|
|
<Select value={selectedValue} onValueChange={handleValueChange}>
|
|
<SelectTrigger
|
|
id={controlField}
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
>
|
|
<SelectValue placeholder="선택하세요" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sections.map((section) => (
|
|
<SelectItem key={section.id} value={section.condition}>
|
|
{section.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 조건별 섹션들 */}
|
|
<div className="flex-1 min-h-0">
|
|
{isDesignMode ? (
|
|
// 디자인 모드: 모든 섹션 표시
|
|
<div className={spacingClass}>
|
|
{sections.map((section) => (
|
|
<ConditionalSectionViewer
|
|
key={section.id}
|
|
sectionId={section.id}
|
|
condition={section.condition}
|
|
label={section.label}
|
|
screenId={section.screenId}
|
|
screenName={section.screenName}
|
|
isActive={selectedValue === section.condition}
|
|
isDesignMode={isDesignMode}
|
|
showBorder={showBorder}
|
|
formData={formData}
|
|
onFormDataChange={onFormDataChange}
|
|
groupedData={groupedData}
|
|
onSave={onSave}
|
|
controlField={controlField}
|
|
selectedCondition={selectedValue}
|
|
initialData={initialData}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
// 실행 모드: 활성 섹션만 표시
|
|
sections.map((section) =>
|
|
selectedValue === section.condition ? (
|
|
<ConditionalSectionViewer
|
|
key={section.id}
|
|
sectionId={section.id}
|
|
condition={section.condition}
|
|
label={section.label}
|
|
screenId={section.screenId}
|
|
screenName={section.screenName}
|
|
isActive={true}
|
|
isDesignMode={false}
|
|
showBorder={showBorder}
|
|
formData={formData}
|
|
onFormDataChange={onFormDataChange}
|
|
groupedData={groupedData}
|
|
onSave={onSave}
|
|
controlField={controlField}
|
|
selectedCondition={selectedValue}
|
|
initialData={initialData}
|
|
/>
|
|
) : null
|
|
)
|
|
)}
|
|
|
|
{/* 섹션이 없는 경우 안내 */}
|
|
{sections.length === 0 && isDesignMode && (
|
|
<div className="flex items-center justify-center min-h-[200px] border-2 border-dashed border-muted-foreground/30 rounded-lg bg-muted/20">
|
|
<p className="text-sm text-muted-foreground">
|
|
설정 패널에서 조건을 추가하세요
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|