Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions components/admin-accounts-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,10 @@ export function AdminAccountsContent() {

<main className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-6">
{/* Admin Users Card */}
<Card className="border border-[#E5E5E5] shadow-sm flex-1 flex flex-col">
<CardHeader className="flex flex-row items-center justify-between py-2 px-6">
<Card className="flex min-w-0 flex-1 flex-col border border-[#E5E5E5] shadow-sm">
<CardHeader className="flex min-w-0 flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-6">
<CardTitle
className="min-w-0 shrink"
style={{
fontSize: "20px",
fontWeight: 700,
Expand All @@ -398,6 +399,7 @@ export function AdminAccountsContent() {
</CardTitle>
<Button
size="sm"
className="w-full shrink-0 sm:w-auto"
onClick={handleOpenGenerateModal}
style={{
backgroundColor: "#3B82F6",
Expand All @@ -409,7 +411,7 @@ export function AdminAccountsContent() {
+ 관리자 번호 발급
</Button>
</CardHeader>
<CardContent className="px-0 pb-0 pt-0 flex-1 flex flex-col overflow-hidden">
<CardContent className="flex min-w-0 flex-1 flex-col overflow-hidden px-0 pb-0 pt-0">
{listError && !isLoading && (
<p
className="mx-6 mb-3 rounded-lg border border-[#FEE2E2] bg-[#FEF2F2] px-4 py-3 text-sm text-[#DC2626]"
Expand All @@ -418,8 +420,9 @@ export function AdminAccountsContent() {
{listError}
</p>
)}
<ScrollArea className="flex-1 h-[520px]">
<Table className="w-full table-fixed">
<div className="min-w-0 flex-1 overflow-x-auto">
<ScrollArea className="h-[520px]">
<Table className="w-full min-w-[1048px] table-fixed">
<TableHeader>
<TableRow className="border-t border-[#E5E5E5] hover:bg-transparent">
<TableHead
Expand Down Expand Up @@ -512,19 +515,25 @@ export function AdminAccountsContent() {
return (
<TableRow key={user.id} className="border-t border-[#E5E5E5] hover:bg-[#F9FAFB]">
<TableCell
className="w-[220px]"
className="max-w-[220px] min-w-0 overflow-hidden w-[220px]"
style={{
fontSize: "14px",
fontWeight: 500,
color: "#1A1A1A",
paddingLeft: "24px",
}}
>
<div className="flex items-center gap-2">
{user.adminNumber}
<div className="flex min-w-0 max-w-full items-center gap-2">
<span
className="min-w-0 truncate whitespace-nowrap"
title={user.adminNumber}
>
{user.adminNumber}
</span>
{isMaster && (
<Badge
variant="default"
className="shrink-0"
style={{
fontSize: "10px",
fontWeight: 600,
Expand All @@ -539,13 +548,18 @@ export function AdminAccountsContent() {
</div>
</TableCell>
<TableCell
className="w-[320px]"
className="max-w-[320px] min-w-0 overflow-hidden w-[320px]"
style={{
fontSize: "14px",
color: "#6B7280",
}}
>
{user.email}
<span
className="block max-w-full truncate whitespace-nowrap"
title={user.email}
>
{user.email}
</span>
</TableCell>
<TableCell className="w-[108px] text-center align-middle">
<div className="flex justify-center items-center">
Expand Down Expand Up @@ -649,6 +663,7 @@ export function AdminAccountsContent() {
</TableBody>
</Table>
</ScrollArea>
</div>
</CardContent>
</Card>

Expand Down
106 changes: 74 additions & 32 deletions components/ai-assistant-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
"use client"

import { useState, useCallback, useEffect, useRef } from "react"
import { useState, useCallback, useEffect, useRef, type WheelEvent } from "react"
import { ChevronLeft, ChevronRight, Send, Bot, User } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { useChatSocket } from "@/hooks/use-chat-socket"
import { updateTokenUsage, getChatHistory } from "@/lib/api/chat"
import { MarkdownContent } from "@/components/markdown-content"

interface Message {
id: number
role: "assistant" | "user"
content: string
}

const MESSAGE_BUBBLE_BASE_CLASS =
"min-w-0 max-w-[280px] break-words rounded-2xl px-4 py-3 text-sm leading-relaxed [overflow-wrap:anywhere]"

/** 회색 말풍선 — 코드 블록 대비 */
const CHAT_MARKDOWN_ASSISTANT_CLASS =
"[&_pre]:border-[#E5E7EB] [&_pre]:bg-white [&_code]:bg-white"

/** 파란 말풍선 — 밝은 배경용 MarkdownContent 오버라이드 */
const CHAT_MARKDOWN_USER_CLASS =
"text-white [&_p]:text-white [&_strong]:text-white [&_em]:text-white/90 [&_li]:text-white [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_blockquote]:border-white/40 [&_blockquote]:text-white/90 [&_a]:text-blue-100 [&_hr]:border-white/30 [&_code]:bg-white/20 [&_code]:text-white [&_pre]:border-white/25 [&_pre]:bg-[#1D4ED8] [&_pre]:text-white [&_table]:border-white/25 [&_thead]:bg-white/15 [&_th]:text-white [&_td]:text-white/95"

/** 공용 Textarea(min-h-16) 오버라이드 — 내용에 따라 늘어나되 최대 15줄, 이후 내부 스크롤 */
const CHAT_INPUT_CLASS =
"!min-h-9 max-h-[15lh] min-w-0 flex-1 resize-none overflow-y-auto leading-5 break-words border-[#D0D0D0] bg-white [overflow-wrap:anywhere] whitespace-pre-wrap"

interface AiAssistantSidebarProps {
isOpen: boolean
onToggle: () => void
Expand Down Expand Up @@ -40,8 +56,8 @@ export function AiAssistantSidebar({
const [currentTurn, setCurrentTurn] = useState(1)
const [isHistoryLoaded, setIsHistoryLoaded] = useState(false)

// 메시지 목록 하단 자동 스크롤
const messagesEndRef = useRef<HTMLDivElement | null>(null)
// 메시지 목록 컨테이너 내부 스크롤 (scrollIntoView는 페이지 전체 스크롤을 유발할 수 있음)
const messagesScrollRef = useRef<HTMLDivElement | null>(null)

// ── 채팅 히스토리 초기 로드 ────────────────────────────────────────────────
useEffect(() => {
Expand Down Expand Up @@ -84,10 +100,22 @@ export function AiAssistantSidebar({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// 메시지가 추가될 때 하단으로 스크롤
// 메시지 목록 컨테이너 내부만 하단으로 스크롤 (scrollIntoView 사용 금지)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const container = messagesScrollRef.current
if (!container) return
requestAnimationFrame(() => {
container.scrollTo({ top: container.scrollHeight, behavior: "smooth" })
})
}, [messages, isSending])

// 메시지 목록 위에서 휠 시 페이지 스크롤로 전파되지 않도록 (목록이 스크롤 가능할 때만)
const handleMessagesWheel = useCallback((event: WheelEvent<HTMLDivElement>) => {
const container = event.currentTarget
if (container.scrollHeight > container.clientHeight) {
event.stopPropagation()
}
}, [])

// ── WebSocket 메시지 수신 핸들러 ─────────────────────────────────────────────
// usedTokens를 의존성에서 제거: 변경 시 useChatSocket 이펙트가 재실행되어 WS 재연결됨
Expand Down Expand Up @@ -173,12 +201,12 @@ export function AiAssistantSidebar({

{/* Expanded Sidebar */}
<aside
className={`fixed right-0 top-[73px] h-[calc(100vh-73px)] w-[400px] bg-white border-l border-[#D0D0D0] shadow-2xl transition-transform duration-300 ease-in-out z-30 flex flex-col ${
className={`fixed right-0 top-[73px] z-30 flex h-[calc(100vh-73px)] min-h-0 w-[400px] flex-col overflow-hidden border-l border-[#D0D0D0] bg-white shadow-2xl transition-transform duration-300 ease-in-out ${
isOpen ? "translate-x-0" : "translate-x-full"
}`}
>
{/* Sidebar Header */}
<div className="flex items-center justify-between px-4 py-4 border-b border-[#E5E7EB]">
<div className="flex shrink-0 items-center justify-between px-4 py-4 border-b border-[#E5E7EB]">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-[#2563EB] rounded-lg flex items-center justify-center">
<Bot className="w-4 h-4 text-white" />
Expand All @@ -192,7 +220,7 @@ export function AiAssistantSidebar({

{/* 히스토리 로딩 중 표시 */}
{!isHistoryLoaded && (
<div className="px-4 py-2 text-xs text-[#6B7280] bg-[#F9FAFB] border-b border-[#E5E7EB] flex items-center gap-1">
<div className="flex shrink-0 items-center gap-1 border-b border-[#E5E7EB] bg-[#F9FAFB] px-4 py-2 text-xs text-[#6B7280]">
<svg className="animate-spin w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
Expand All @@ -201,53 +229,67 @@ export function AiAssistantSidebar({
</div>
)}

{/* Chat Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* 메시지 목록 — 가운데 영역만 스크롤 (일반 채팅앱 UX) */}
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div
ref={messagesScrollRef}
onWheel={handleMessagesWheel}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-4 pr-3 space-y-4 [touch-action:pan-y]"
>
{messages.map((message) => (
<div key={message.id} className={`flex gap-3 ${message.role === "user" ? "flex-row-reverse" : ""}`}>
<div
key={message.id}
className={`flex min-w-0 gap-3 ${message.role === "user" ? "flex-row-reverse" : ""}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ${
message.role === "assistant" ? "bg-[#2563EB]" : "bg-[#6B7280]"
}`}
>
{message.role === "assistant" ? (
<Bot className="w-4 h-4 text-white" />
<Bot className="h-4 w-4 text-white" />
) : (
<User className="w-4 h-4 text-white" />
<User className="h-4 w-4 text-white" />
)}
</div>
<div
className={`max-w-[280px] px-4 py-3 rounded-2xl text-sm leading-relaxed whitespace-pre-wrap break-words ${
className={`${MESSAGE_BUBBLE_BASE_CLASS} ${
message.role === "assistant"
? "bg-[#F3F4F6] text-[#1F2937] rounded-tl-sm"
: "bg-[#2563EB] text-white rounded-tr-sm"
? "bg-[#F3F4F6] rounded-tl-sm"
: "bg-[#2563EB] rounded-tr-sm"
}`}
>
{message.content}
<MarkdownContent
content={message.content}
className={
message.role === "assistant"
? CHAT_MARKDOWN_ASSISTANT_CLASS
: CHAT_MARKDOWN_USER_CLASS
}
/>
</div>
</div>
))}

{/* 응답 대기 중 타이핑 인디케이터 */}
{isSending && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-[#2563EB] flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4 text-white" />
<div className="flex min-w-0 gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#2563EB]">
<Bot className="h-4 w-4 text-white" />
</div>
<div className="px-4 py-3 rounded-2xl bg-[#F3F4F6] rounded-tl-sm flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-[#9CA3AF] animate-bounce [animation-delay:-0.3s]" />
<span className="w-1.5 h-1.5 rounded-full bg-[#9CA3AF] animate-bounce [animation-delay:-0.15s]" />
<span className="w-1.5 h-1.5 rounded-full bg-[#9CA3AF] animate-bounce" />
<div className="flex items-center gap-1 rounded-2xl rounded-tl-sm bg-[#F3F4F6] px-4 py-3">
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-[#9CA3AF] [animation-delay:-0.3s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-[#9CA3AF] [animation-delay:-0.15s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-[#9CA3AF]" />
</div>
</div>
)}

<div ref={messagesEndRef} />
</div>
</div>

{/* Input Area */}
<div className="p-4 border-t border-[#E5E7EB] bg-[#F9FAFB]">
<div className="flex gap-2">
<div className="shrink-0 overflow-hidden border-t border-[#E5E7EB] bg-[#F9FAFB] p-4">
<div className="flex items-end gap-2">
<Textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
Expand All @@ -259,13 +301,13 @@ export function AiAssistantSidebar({
}}
placeholder={isConnected ? "메시지를 입력하세요…" : "연결 대기 중…"}
rows={1}
className="flex-1 bg-white border-[#D0D0D0] resize-none whitespace-normal break-words"
className={CHAT_INPUT_CLASS}
disabled={isSending || !isConnected}
/>
<Button
onClick={handleSend}
size="icon"
className="bg-[#2563EB] hover:bg-[#1D4ED8] text-white px-3 py-2 disabled:opacity-50 disabled:cursor-not-allowed"
className="shrink-0 bg-[#2563EB] hover:bg-[#1D4ED8] text-white px-3 py-2 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSending || !inputValue.trim() || !isConnected}
>
<Send className="w-4 h-4" />
Expand Down
Loading
Loading