o2o-negosium-original/negodata/front/src/components/SlateRenderer.tsx
Mina Choi 87524bf6e8 [feat] negodata/front: 프론트 초기 구축 (Vite+React, orval API 클라이언트, 견적/상품/협력사 페이지)
- 라우팅 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>
2026-06-16 17:18:08 +09:00

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>
);
}