- 라우팅 react-router v7, 서버상태 TanStack Query - orval 자동 생성 API 클라이언트(src/api/generated) - 견적/상품/협력사/견적설정 페이지, shadcn UI, 토큰(tokens.css) - 실행: 루트 docker compose (front :3001), README 그에 맞게 정리 - AI Studio 스캐폴드 잔재 제거(metadata.json, 중복 DESIGN_TOKENS.md), 타이틀 NegoData, vite.config 죽은코드 제거 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
3.9 KiB
TypeScript
128 lines
3.9 KiB
TypeScript
import React from 'react';
|
|
|
|
interface SlateLeaf {
|
|
text: string;
|
|
bold?: boolean;
|
|
italic?: boolean;
|
|
color?: 'primary' | 'success' | 'warning' | 'destructive';
|
|
}
|
|
|
|
interface SlateNode {
|
|
type?: 'paragraph' | 'block-quote' | 'heading-one' | 'heading-two' | 'list-item';
|
|
children: (SlateLeaf | SlateNode)[];
|
|
}
|
|
|
|
interface SlateRendererProps {
|
|
nodes?: any[];
|
|
variables?: Record<string, string | number>;
|
|
}
|
|
|
|
export default function SlateRenderer({ nodes, variables = {} }: SlateRendererProps) {
|
|
if (!nodes || !Array.isArray(nodes)) {
|
|
return null;
|
|
}
|
|
|
|
// Helper to replace variable strings like {target_price} or {partner_type}
|
|
const replaceVariables = (text: string): string => {
|
|
let result = text;
|
|
Object.entries(variables).forEach(([key, val]) => {
|
|
const placeholder = `{${key}}`;
|
|
if (result.includes(placeholder)) {
|
|
if (typeof val === 'number') {
|
|
result = result.replace(new RegExp(placeholder, 'g'), val.toLocaleString('ko-KR'));
|
|
} else {
|
|
result = result.replace(new RegExp(placeholder, 'g'), String(val));
|
|
}
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
|
|
const renderLeaf = (leaf: SlateLeaf, key: string) => {
|
|
const text = replaceVariables(leaf.text);
|
|
let classes = '';
|
|
|
|
if (leaf.bold) {
|
|
classes += ' font-bold ';
|
|
}
|
|
if (leaf.italic) {
|
|
classes += ' italic ';
|
|
}
|
|
|
|
if (leaf.color === 'primary') {
|
|
return (
|
|
<span key={key} className={`${classes} text-primary font-bold underline decoration-2 decoration-gray-400 dark:decoration-gray-600`}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
if (leaf.color === 'success') {
|
|
return (
|
|
<span key={key} className={`${classes} text-success font-semibold px-1 py-0.5 rounded bg-emerald-50 dark:bg-emerald-950/40 border border-emerald-200/50 dark:border-emerald-900/40`}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
if (leaf.color === 'warning') {
|
|
return (
|
|
<span key={key} className={`${classes} text-warning font-semibold px-1 py-0.5 rounded bg-amber-50 dark:bg-amber-950/40 border border-amber-200/50`}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
if (leaf.color === 'destructive') {
|
|
return (
|
|
<span key={key} className={`${classes} text-destructive font-semibold`}>
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span key={key} className={classes || undefined}>
|
|
{text}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const renderNode = (node: any, index: number): React.ReactNode => {
|
|
const key = `node-${index}`;
|
|
|
|
// If it's pure leaf structure (contains no children arrays, but has text attribute)
|
|
if (node.text !== undefined) {
|
|
return renderLeaf(node as SlateLeaf, key);
|
|
}
|
|
|
|
const childrenElements = (node.children || []).map((child: any, childIdx: number) => {
|
|
if (child.text !== undefined) {
|
|
return renderLeaf(child as SlateLeaf, `${key}-${childIdx}`);
|
|
}
|
|
return renderNode(child, childIdx);
|
|
});
|
|
|
|
switch (node.type) {
|
|
case 'heading-one':
|
|
return <h1 key={key} className="text-xl font-bold tracking-tight text-foreground my-2">{childrenElements}</h1>;
|
|
case 'heading-two':
|
|
return <h2 key={key} className="text-lg font-semibold tracking-tight text-foreground my-1.5">{childrenElements}</h2>;
|
|
case 'block-quote':
|
|
return (
|
|
<blockquote key={key} className="border-l-4 border-foreground/50 bg-muted/60 px-4 py-2.5 my-3 rounded-r text-sm text-foreground/90 font-mono">
|
|
{childrenElements}
|
|
</blockquote>
|
|
);
|
|
case 'list-item':
|
|
return <li key={key} className="list-disc ml-5 my-1 text-sm text-foreground/80">{childrenElements}</li>;
|
|
case 'paragraph':
|
|
default:
|
|
return <p key={key} className="text-sm leading-relaxed text-foreground/85 my-1 whitespace-pre-line">{childrenElements}</p>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
{nodes.map((node, i) => renderNode(node, i))}
|
|
</div>
|
|
);
|
|
}
|