o2o-site-AEO/solution/frontend/src/features/builder/canvas/SlateScriptRenderer.tsx
Mina Choi c85c577349 이름: solution/front → solution/frontend
`backend` 옆에 `front` 가 있을 이유가 없었다. negosium 의 negodata/front 를 그대로
베꼈고 그게 왜 front 인지는 따져보지 않았다 — 근거 없이 들여온 이름이라 바로잡는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uYhHQdssRubirPirrdJJC
2026-08-31 15:27:16 +09:00

146 lines
4.6 KiB
TypeScript

import React from 'react';
export interface SlateTextNode {
text: string;
bold?: boolean;
italic?: boolean;
underline?: boolean;
code?: boolean;
}
export interface SlateElementNode {
type?: string;
speaker?: string;
role?: 'buyer' | 'seller' | 'host' | 'guest' | 'ai' | 'customer';
time?: string;
children: (SlateElementNode | SlateTextNode)[];
}
interface SlateScriptRendererProps {
nodes?: (SlateElementNode | SlateTextNode)[];
primaryColor?: string;
className?: string;
}
export const SlateScriptRenderer: React.FC<SlateScriptRendererProps> = ({
nodes,
primaryColor = '#18181b',
className = '',
}) => {
if (!nodes || nodes.length === 0) {
return null;
}
const renderTextNode = (node: SlateTextNode, idx: number) => {
let content: React.ReactNode = node.text;
if (node.bold) content = <strong key={idx}>{content}</strong>;
if (node.italic) content = <em key={idx}>{content}</em>;
if (node.underline) content = <u key={idx}>{content}</u>;
if (node.code) {
content = (
<code key={idx} className="bg-slate-100 px-1 py-0.5 rounded text-[11px] font-mono">
{content}
</code>
);
}
return <React.Fragment key={idx}>{content}</React.Fragment>;
};
const renderElement = (node: SlateElementNode, idx: number) => {
switch (node.type) {
case 'chat-dialog':
case 'dialog':
case 'negotiation': {
const isHost = node.role === 'seller' || node.role === 'host' || node.role === 'ai';
return (
<div
key={idx}
className={`flex items-start gap-3 my-2.5 ${isHost ? 'flex-row-reverse' : 'flex-row'}`}
>
{/* Avatar */}
<div
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0 shadow-2xs ${
isHost ? 'bg-slate-900 text-white' : 'bg-slate-200 text-slate-700'
}`}
>
{node.speaker ? node.speaker.slice(0, 1) : isHost ? '호' : '손'}
</div>
{/* Bubble */}
<div className={`max-w-[75%] space-y-1 ${isHost ? 'text-right' : 'text-left'}`}>
<div className="flex items-center gap-1.5 text-[11px] text-slate-400">
<span className="font-semibold text-slate-700">{node.speaker || (isHost ? '호스트' : '게스트')}</span>
{node.time && <span>· {node.time}</span>}
</div>
<div
className={`px-3.5 py-2.5 rounded-2xl text-xs leading-relaxed inline-block text-left ${
isHost
? 'bg-slate-900 text-white rounded-tr-xs shadow-2xs'
: 'bg-slate-100 text-slate-800 rounded-tl-xs'
}`}
>
{node.children.map((child, cIdx) =>
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
)}
</div>
</div>
</div>
);
}
case 'heading-two':
case 'h2':
return (
<h3 key={idx} className="text-base font-bold text-slate-900 mt-4 mb-2">
{node.children.map((child, cIdx) =>
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
)}
</h3>
);
case 'quote':
case 'blockquote':
return (
<blockquote
key={idx}
className="border-l-2 pl-3 my-3 text-xs italic text-slate-600 border-slate-400"
>
{node.children.map((child, cIdx) =>
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
)}
</blockquote>
);
case 'callout':
case 'info-box':
return (
<div
key={idx}
className="p-3.5 my-3 bg-slate-50 border border-slate-200 rounded-xl text-xs text-slate-700 leading-relaxed"
>
{node.children.map((child, cIdx) =>
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
)}
</div>
);
default:
return (
<p key={idx} className="text-xs text-slate-600 leading-relaxed my-1.5">
{node.children.map((child, cIdx) =>
'text' in child ? renderTextNode(child, cIdx) : renderElement(child, cIdx)
)}
</p>
);
}
};
return (
<div className={`slate-editor-script space-y-1 ${className}`}>
{nodes.map((node, idx) =>
'text' in node ? renderTextNode(node, idx) : renderElement(node, idx)
)}
</div>
);
};