164 lines
5.7 KiB
TypeScript
164 lines
5.7 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* TextLayoutTabs.tsx — 텍스트 컴포넌트 설정
|
||
|
|
*
|
||
|
|
* [역할]
|
||
|
|
* - 텍스트 내용 입력 + 데이터 바인딩(queryId/fieldName) 설정
|
||
|
|
* - ComponentSettingsModal 내에 직접 임베드되어 사용
|
||
|
|
*
|
||
|
|
* [사용처]
|
||
|
|
* - TextProperties.tsx (section="data"일 때)
|
||
|
|
*/
|
||
|
|
|
||
|
|
import React, { useCallback, useMemo } from "react";
|
||
|
|
import { Textarea } from "@/components/ui/textarea";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from "@/components/ui/select";
|
||
|
|
import { Type, Database, Link2 } from "lucide-react";
|
||
|
|
import { useReportDesigner } from "@/contexts/ReportDesignerContext";
|
||
|
|
import { Section, TabContent, Field, FieldGroup, InfoBox } from "./shared";
|
||
|
|
import type { ComponentConfig } from "@/types/report";
|
||
|
|
|
||
|
|
interface TextLayoutTabsProps {
|
||
|
|
component: ComponentConfig;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TextLayoutTabs({ component }: TextLayoutTabsProps) {
|
||
|
|
const { updateComponent, queries, getQueryResult } = useReportDesigner();
|
||
|
|
|
||
|
|
const update = useCallback(
|
||
|
|
(updates: Partial<ComponentConfig>) => updateComponent(component.id, updates),
|
||
|
|
[component.id, updateComponent],
|
||
|
|
);
|
||
|
|
|
||
|
|
const selectedQueryFields = useMemo(() => {
|
||
|
|
if (!component.queryId) return [];
|
||
|
|
const result = getQueryResult(component.queryId);
|
||
|
|
if (result?.fields) return result.fields;
|
||
|
|
return [];
|
||
|
|
}, [component.queryId, getQueryResult]);
|
||
|
|
|
||
|
|
const hasBinding = !!(component.queryId && component.fieldName);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<TabContent>
|
||
|
|
{/* 데이터 바인딩 섹션 */}
|
||
|
|
<Section
|
||
|
|
emphasis
|
||
|
|
icon={<Database className="h-3.5 w-3.5" />}
|
||
|
|
title="데이터 바인딩"
|
||
|
|
>
|
||
|
|
<FieldGroup>
|
||
|
|
<Field label="데이터 소스 (쿼리)">
|
||
|
|
<Select
|
||
|
|
value={component.queryId || "none"}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
update({
|
||
|
|
queryId: value === "none" ? undefined : value,
|
||
|
|
fieldName: value === "none" ? undefined : component.fieldName,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-9 text-sm">
|
||
|
|
<SelectValue placeholder="쿼리 선택" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="none">직접 입력 (바인딩 없음)</SelectItem>
|
||
|
|
{queries.map((q) => (
|
||
|
|
<SelectItem key={q.id} value={q.id}>
|
||
|
|
{q.name} ({q.type})
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</Field>
|
||
|
|
|
||
|
|
{component.queryId && (
|
||
|
|
<Field label="바인딩 필드">
|
||
|
|
{selectedQueryFields.length > 0 ? (
|
||
|
|
<Select
|
||
|
|
value={component.fieldName || "none"}
|
||
|
|
onValueChange={(value) =>
|
||
|
|
update({ fieldName: value === "none" ? undefined : value })
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<SelectTrigger className="h-9 text-sm">
|
||
|
|
<SelectValue placeholder="필드 선택" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="none">선택 안함</SelectItem>
|
||
|
|
{selectedQueryFields.map((field) => (
|
||
|
|
<SelectItem key={field} value={field}>
|
||
|
|
{field}
|
||
|
|
</SelectItem>
|
||
|
|
))}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
) : (
|
||
|
|
<Input
|
||
|
|
value={component.fieldName || ""}
|
||
|
|
onChange={(e) => update({ fieldName: e.target.value || undefined })}
|
||
|
|
placeholder="필드명 직접 입력 (예: doc_number)"
|
||
|
|
className="h-9 text-sm"
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</Field>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{!component.queryId && component.fieldName && (
|
||
|
|
<Field label="바인딩 필드 (쿼리 미연결)">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Link2 className="h-3.5 w-3.5 shrink-0 text-amber-500" />
|
||
|
|
<Input
|
||
|
|
value={component.fieldName || ""}
|
||
|
|
onChange={(e) => update({ fieldName: e.target.value || undefined })}
|
||
|
|
placeholder="필드명"
|
||
|
|
className="h-9 text-sm"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<p className="mt-1 text-[11px] text-amber-600">
|
||
|
|
쿼리를 연결하면 실제 데이터가 표시됩니다.
|
||
|
|
</p>
|
||
|
|
</Field>
|
||
|
|
)}
|
||
|
|
</FieldGroup>
|
||
|
|
|
||
|
|
{hasBinding && (
|
||
|
|
<InfoBox variant="blue">
|
||
|
|
쿼리 실행 시 <code className="rounded bg-blue-100 px-1 text-xs font-mono">{`{${component.fieldName}}`}</code> 값이 표시됩니다.
|
||
|
|
</InfoBox>
|
||
|
|
)}
|
||
|
|
</Section>
|
||
|
|
|
||
|
|
{/* 기본값 / 정적 텍스트 */}
|
||
|
|
<Section icon={<Type className="h-3.5 w-3.5" />} title={hasBinding ? "기본값 (데이터 없을 때)" : "텍스트 내용"}>
|
||
|
|
<FieldGroup>
|
||
|
|
<Field label={hasBinding ? "기본 표시 텍스트" : "표시 텍스트"}>
|
||
|
|
<Textarea
|
||
|
|
value={component.defaultValue || ""}
|
||
|
|
onChange={(e) => update({ defaultValue: e.target.value })}
|
||
|
|
placeholder={
|
||
|
|
hasBinding
|
||
|
|
? "데이터가 없을 때 표시할 텍스트"
|
||
|
|
: "텍스트 내용을 입력하세요 (엔터로 줄바꿈 가능)"
|
||
|
|
}
|
||
|
|
className="min-h-[72px] resize-y text-sm"
|
||
|
|
/>
|
||
|
|
</Field>
|
||
|
|
</FieldGroup>
|
||
|
|
</Section>
|
||
|
|
|
||
|
|
<InfoBox variant="gray">
|
||
|
|
폰트, 색상, 정렬 등 스타일 설정은 우측 패널에서 변경할 수 있습니다.
|
||
|
|
</InfoBox>
|
||
|
|
</TabContent>
|
||
|
|
);
|
||
|
|
}
|