"use client"; /** * TextProperties.tsx — 텍스트/레이블 컴포넌트 설정 * * - section="data": TextLayoutTabs (데이터 바인딩 / 텍스트 서식 / 표시 조건) * - section="style": StyleAccordion 패턴 (프리셋 + 폰트 + 색상 + 정렬 + 테두리) */ import { useState, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { ChevronRight, Bold, Italic, Underline, Strikethrough, AlignLeft, AlignCenter, AlignRight } from "lucide-react"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; import { TextLayoutTabs } from "../modals/TextLayoutTabs"; import type { ComponentConfig } from "@/types/report"; const FONT_FAMILIES = [ "Malgun Gothic", "NanumGothic", "NanumMyeongjo", "굴림", "돋움", "바탕", "Times New Roman", "Arial", ]; const TEXT_STYLE_PRESETS = { title: { label: "제목", fontSize: 24, fontWeight: "bold", fontColor: "#111827", lineHeight: 1.3, }, subtitle: { label: "부제목", fontSize: 16, fontWeight: "600", fontColor: "#374151", lineHeight: 1.4, }, body: { label: "본문", fontSize: 12, fontWeight: "normal", fontColor: "#374151", lineHeight: 1.6, }, caption: { label: "캡션", fontSize: 10, fontWeight: "normal", fontColor: "#6b7280", fontStyle: "italic" as const, lineHeight: 1.4, }, header: { label: "헤더", fontSize: 14, fontWeight: "bold", fontColor: "#1e40af", lineHeight: 1.5, backgroundColor: "#eff6ff", borderWidth: 1, borderColor: "#bfdbfe", padding: 8, }, footer: { label: "푸터", fontSize: 9, fontWeight: "normal", fontColor: "#9ca3af", lineHeight: 1.4, }, } as const; interface Props { component: ComponentConfig; /** 우측 패널: "style" | 모달: "data" | 미전달: 전체 표시 (하위 호환) */ section?: "style" | "data"; } function StyleAccordion({ label, isOpen, onToggle, children, }: { label: string; isOpen: boolean; onToggle: () => void; children: React.ReactNode; }) { return (
{isOpen && (
{children}
)}
); } function ColorInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { return (
onChange(e.target.value)} className="h-7 w-7 shrink-0 cursor-pointer rounded border-0 p-0" /> onChange(e.target.value)} className="h-7 border-0 bg-transparent px-1 font-mono text-xs shadow-none focus-visible:ring-0" />
); } function ToggleBtn({ active, onClick, children, title, }: { active: boolean; onClick: () => void; children: React.ReactNode; title?: string; }) { return ( ); } export function TextProperties({ component, section }: Props) { const { updateComponent } = useReportDesigner(); const showStyle = !section || section === "style"; const showData = !section || section === "data"; const [openSections, setOpenSections] = useState>(new Set(["preset"])); const toggleSection = (id: string) => { setOpenSections((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const update = useCallback( (updates: Partial) => updateComponent(component.id, updates), [component.id, updateComponent], ); const applyPreset = useCallback( (key: keyof typeof TEXT_STYLE_PRESETS) => { const { label, ...values } = TEXT_STYLE_PRESETS[key]; update({ ...values, textPreset: key }); }, [update], ); return ( <> {/* 모달(section="data")에서 표시: TextLayoutTabs 3탭 구조 */} {showData && } {/* 우측 패널(section="style")에서 표시: StyleAccordion 패턴 */} {showStyle && (
{/* 프리셋 */} toggleSection("preset")}>
{(Object.entries(TEXT_STYLE_PRESETS) as [keyof typeof TEXT_STYLE_PRESETS, typeof TEXT_STYLE_PRESETS[keyof typeof TEXT_STYLE_PRESETS]][]).map( ([key, preset]) => ( ), )}
{/* 폰트 */} toggleSection("font")}>
update({ fontSize: parseInt(e.target.value) || 12 })} className="h-9 text-xs" />
update({ fontWeight: component.fontWeight === "bold" || component.fontWeight === "700" ? "normal" : "bold" })} title="굵게" > update({ fontStyle: component.fontStyle === "italic" ? "normal" : "italic" })} title="기울임" > update({ textDecoration: component.textDecoration === "underline" ? "none" : "underline" })} title="밑줄" > update({ textDecoration: component.textDecoration === "line-through" ? "none" : "line-through" })} title="취소선" >
{/* 색상 */} toggleSection("color")}>
update({ fontColor: v })} />
update({ backgroundColor: v })} />
{/* 정렬 & 간격 */} toggleSection("align")}>
update({ textAlign: "left" })} title="왼쪽"> update({ textAlign: "center" })} title="가운데"> update({ textAlign: "right" })} title="오른쪽">
update({ letterSpacing: parseFloat(e.target.value) || 0 })} className="h-9 text-xs" />
update({ padding: parseInt(e.target.value) || 0 })} className="h-9 text-xs" />
{/* 테두리 */} toggleSection("border")}>
{(component.borderWidth ?? 0) > 0 && (
update({ borderColor: v })} />
)}
)} ); }