From edf0d6968908055a79da0fe5a85498ca2f41b2f4 Mon Sep 17 00:00:00 2001 From: smilevictory Date: Thu, 11 Sep 2025 22:38:41 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=B1=97=EB=B4=87=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20api=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ssh/src/components/Layout/Header/index.tsx | 7 + ssh/src/index.css | 62 ++ .../pages/Chatbot/components/appsidebar.tsx | 119 +++- .../Chatbot/components/chat/ChatBubble.tsx | 125 ++++ .../components/chat/ChatDateDivider.tsx | 9 + .../Chatbot/components/chat/chat-utils.ts | 58 ++ .../pages/Chatbot/components/chat/types.ts | 10 + ssh/src/pages/Chatbot/components/chatbot.tsx | 614 ++++++++++++++++-- ssh/src/pages/Chatbot/index.tsx | 10 +- 9 files changed, 932 insertions(+), 82 deletions(-) create mode 100644 ssh/src/pages/Chatbot/components/chat/ChatBubble.tsx create mode 100644 ssh/src/pages/Chatbot/components/chat/ChatDateDivider.tsx create mode 100644 ssh/src/pages/Chatbot/components/chat/chat-utils.ts create mode 100644 ssh/src/pages/Chatbot/components/chat/types.ts diff --git a/ssh/src/components/Layout/Header/index.tsx b/ssh/src/components/Layout/Header/index.tsx index 209a9b8..b460176 100644 --- a/ssh/src/components/Layout/Header/index.tsx +++ b/ssh/src/components/Layout/Header/index.tsx @@ -48,9 +48,16 @@ export default function Header() { {NAV.map((item) => { const href = toAbs(item.seg); const active = isActive(item.seg); + + // ✅ 라벨/seg에 따라 id 부여 (HIGHLIGHT_ELEMENT에서 사용할 고정 셀렉터) + let id: string | undefined; + if (item.seg === ROUTING_PATH.atmmap) id = 'header-atm-button'; + if (item.seg === ROUTING_PATH.voicephishing) id = 'header-phishing-button'; + return ( void; + fontPx: number; + setFontPx: (v: number | ((prev: number) => number)) => void; + volume: number; // 0 ~ 1 + setVolume: (v: number) => void; }) { + // 현재 볼륨으로 0.4초 테스트 사운드 + const playTestSound = async () => { + try { + const Ctx = (window as any).AudioContext || (window as any).webkitAudioContext; + if (!Ctx) return; + const ctx = new Ctx(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.value = 880; + gain.gain.value = Math.max(0, Math.min(1, volume)); + osc.connect(gain).connect(ctx.destination); + osc.start(); + setTimeout(() => { + osc.stop(); + ctx.close(); + }, 400); + } catch (e) { + console.warn('Test sound failed', e); + } + }; + return (
-
-
- Logo -
- 마음 채팅 +
+ Logo
마음 채팅
@@ -58,13 +94,11 @@ export default function AppSidebar({ 새 대화 - 과거 대화 - 관련 부서 연결 @@ -72,38 +106,77 @@ export default function AppSidebar({ - {/* 수정 예정 */}
설정 - 준비중
+ {/* 글자 크기 */}
글자 크기
- -
14px
-
-
- -
-
음성 출력 볼륨
setFontPx(Number(e.target.value))} + className="w-full" + aria-label="글자 크기 슬라이더" /> -
80% (미구현)
+
+ + {/* 음성 출력 볼륨 */} +
+
+ + 음성 출력 볼륨 +
+
+ setVolume(Number(e.target.value) / 100)} + className="w-full" + aria-label="음성 볼륨 슬라이더" + /> + +
+
{Math.round(volume * 100)}%
diff --git a/ssh/src/pages/Chatbot/components/chat/ChatBubble.tsx b/ssh/src/pages/Chatbot/components/chat/ChatBubble.tsx new file mode 100644 index 0000000..e1c461a --- /dev/null +++ b/ssh/src/pages/Chatbot/components/chat/ChatBubble.tsx @@ -0,0 +1,125 @@ +import { memo } from 'react'; +import { cn } from '@/lib/utils'; // shadcn 유틸(없으면 className join 함수로 대체) +import logo2 from '@/assets/logo2.png'; +import { CircleUser } from 'lucide-react'; + +export type ChatRole = 'user' | 'assistant'; + +export type ChatBubbleProps = { + role: ChatRole; + text: string; + time?: string; // "오후 3:21" 같은 표시용 + showAvatar?: boolean; // 첫 말풍선에만 아바타 + showTail?: boolean; // 묶음의 마지막 말풍선에만 꼬리 + stackPosition?: 'single' | 'top' | 'mid' | 'bottom'; + audioUrl?: string; + videoUrl?: string; + fontPx?: number; // 글자 크기 동기화 + volume?: number; +}; + +function formatText(t: string) { + return t.replace(/\n/g, '\n'); +} + +export const ChatBubble = memo(function ChatBubble({ + role, + text, + time, + showAvatar = false, + showTail = true, + stackPosition = 'single', + audioUrl, + videoUrl, + fontPx = 16, + volume = 0.8, +}: ChatBubbleProps) { + const isUser = role === 'user'; + + return ( +
+ {/* 왼쪽 아바타 (assistant) */} + {!isUser && ( +
+ {showAvatar ? ( +
+ bot +
+ ) : ( +
+ )} +
+ )} + + {/* 말풍선 + 시간 */} +
+
+ {formatText(text)} + + {videoUrl && ( +
+
+