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 && ( +
+
+