ERP-node/frontend/components/report/designer/modals/ComponentSettingsModal.tsx

231 lines
7.5 KiB
TypeScript

"use client";
/**
* ComponentSettingsModal.tsx
*
* 인캔버스 설정 모달 — 기능 설정 / 표시 조건 / 미리보기 3탭 구조.
* SettingsModalShell 모듈을 사용하여 모든 컴포넌트가 동일한 모달 형식을 유지.
* card 타입은 Draft 기반 저장/취소 지원.
*/
import { useState, useCallback, useEffect, useRef } from "react";
import { Eye as EyeIcon, Sliders, Layers } from "lucide-react";
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
import { SettingsModalShell, useModalAlert } from "./SettingsModalShell";
import type { ModalTabDef } from "./SettingsModalShell";
import { useUnsavedChangesGuard, UnsavedChangesDialog } from "@/components/common/UnsavedChangesGuard";
import { ConditionalSettingsTab } from "./ConditionalSettingsTab";
import { ComponentPreviewPanel } from "./ComponentPreviewPanel";
import { TextProperties } from "../properties/TextProperties";
import { ImageProperties } from "../properties/ImageProperties";
import { TableProperties } from "../properties/TableProperties";
import { CardProperties } from "../properties/CardProperties";
import { CalculationProperties } from "../properties/CalculationProperties";
import { BarcodeProperties } from "../properties/BarcodeProperties";
import { CheckboxProperties } from "../properties/CheckboxProperties";
import { SignatureProperties } from "../properties/SignatureProperties";
import { PageNumberProperties } from "../properties/PageNumberProperties";
import type { ComponentConfig } from "@/types/report";
const TYPE_LABELS: Record<string, string> = {
text: "텍스트",
label: "레이블",
table: "테이블",
image: "이미지",
divider: "구분선",
signature: "서명",
stamp: "도장",
pageNumber: "페이지 번호",
card: "카드",
calculation: "계산",
barcode: "바코드",
checkbox: "체크박스",
};
interface DataTabProps {
component: ComponentConfig;
onConfigChange?: (updates: Partial<ComponentConfig>) => void;
}
function DataTab({ component, onConfigChange }: DataTabProps) {
switch (component.type) {
case "text":
case "label":
return <TextProperties component={component} section="data" />;
case "table":
return <TableProperties component={component} section="data" />;
case "image":
return <ImageProperties component={component} section="data" />;
case "signature":
case "stamp":
return <SignatureProperties component={component} section="data" />;
case "pageNumber":
return <PageNumberProperties component={component} />;
case "card":
return (
<CardProperties
component={component}
section="data"
onConfigChange={onConfigChange}
/>
);
case "calculation":
return <CalculationProperties component={component} section="data" />;
case "barcode":
return <BarcodeProperties component={component} section="data" />;
case "checkbox":
return <CheckboxProperties component={component} section="data" />;
default:
return <p className="p-4 text-sm text-gray-500"> .</p>;
}
}
const TYPES_WITH_DATA_TAB = new Set([
"text",
"label",
"table",
"image",
"signature",
"stamp",
"pageNumber",
"card",
"calculation",
"barcode",
"checkbox",
]);
export function ComponentSettingsModal() {
const { componentModalTargetId, closeComponentModal, components, updateComponent } = useReportDesigner();
const component = components.find((c) => c.id === componentModalTargetId) ?? null;
const isDivider = component?.type === "divider";
const isOpen = componentModalTargetId !== null && component !== null && !isDivider;
const [activeTab, setActiveTab] = useState("content");
const [localDraft, setLocalDraft] = useState<ComponentConfig | null>(null);
const { alert, clearAlert } = useModalAlert();
const initialSnapshotRef = useRef<string>("");
useEffect(() => {
if (component) {
setLocalDraft(component);
initialSnapshotRef.current = JSON.stringify(component);
clearAlert();
const hasData = TYPES_WITH_DATA_TAB.has(component.type);
setActiveTab(hasData ? "content" : "preview");
}
}, [componentModalTargetId, clearAlert]);
const hasChanges = useCallback(() => {
if (component?.type === "card") {
if (!localDraft) return false;
return JSON.stringify(localDraft) !== initialSnapshotRef.current;
}
if (!component) return false;
return JSON.stringify(component) !== initialSnapshotRef.current;
}, [localDraft, component]);
const isSavingRef = useRef(false);
const guard = useUnsavedChangesGuard({
hasChanges,
onClose: () => {
if (!isSavingRef.current && initialSnapshotRef.current && component) {
const original = JSON.parse(initialSnapshotRef.current) as ComponentConfig;
updateComponent(component.id, original);
}
isSavingRef.current = false;
setLocalDraft(null);
clearAlert();
closeComponentModal();
},
});
const handleSave = useCallback(() => {
if (component?.type === "card" && localDraft) {
updateComponent(localDraft.id, localDraft);
}
isSavingRef.current = true;
initialSnapshotRef.current = component?.type === "card" && localDraft
? JSON.stringify(localDraft)
: JSON.stringify(component);
guard.doClose();
}, [component, localDraft, updateComponent, guard]);
const handleDraftChange = useCallback(
(updates: Partial<ComponentConfig>) => {
setLocalDraft((prev) => {
if (!prev && component) return { ...component, ...updates };
if (!prev) return null;
return { ...prev, ...updates };
});
},
[component],
);
if (!component) return null;
const hasDataTab = TYPES_WITH_DATA_TAB.has(component.type);
const typeLabel = TYPE_LABELS[component.type] ?? component.type;
const isCard = component.type === "card";
const isTable = component.type === "table";
const isText = component.type === "text" || component.type === "label";
const isImage = component.type === "image";
const hideConditionTab = isText || isImage || isDivider;
const hasInternalConditionTab = isCard || isTable || hideConditionTab;
const displayComponent = isCard && localDraft ? localDraft : component;
const tabs: ModalTabDef[] = [
hasDataTab && {
key: "content",
icon: <Sliders className="h-4 w-4" />,
label: "기능 설정",
},
!hasInternalConditionTab && {
key: "conditional",
icon: <EyeIcon className="h-4 w-4" />,
label: "표시 조건",
},
{
key: "preview",
icon: <EyeIcon className="h-4 w-4" />,
label: "미리보기",
},
].filter(Boolean) as ModalTabDef[];
return (
<>
<SettingsModalShell
open={isOpen}
onOpenChange={guard.handleOpenChange}
title={`${typeLabel} 설정`}
icon={<Layers className="h-5 w-5" />}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
onSave={handleSave}
onClose={guard.tryClose}
alert={alert}
>
{activeTab === "content" && hasDataTab && (
<DataTab
component={displayComponent}
onConfigChange={isCard ? handleDraftChange : undefined}
/>
)}
{activeTab === "conditional" && !hasInternalConditionTab && (
<ConditionalSettingsTab component={component} />
)}
{activeTab === "preview" && (
<ComponentPreviewPanel component={displayComponent} />
)}
</SettingsModalShell>
<UnsavedChangesDialog guard={guard} />
</>
);
}