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
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"jsbarcode": "^3.11.6",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"marked": "^17.0.1",
"mui": "^1.0.0",
"next": "15.3.3",
"quagga": "0.12.1",
Expand Down
20 changes: 15 additions & 5 deletions client/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions client/src/app/admin/ai/LeftList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client"

import useAuth from "@/hooks/useAuth"
import { Stack, Typography, Box, Button } from "@mui/material"
import useAiChat from "./chat/useAiChat"
import { useEffect, useState } from "react"
import { AIChatRoom } from "@server/entity/ai/aiChatRoom"
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"
import dayjs from "dayjs"

export default function AdminAIChatLeftComponent() {
const { authUserData } = useAuth()
const {
getChatRooms,
selectedChatRoomId,
setSelectedChatRoomId,
setSelectedChatRoom,
chatRooms,
} = useAiChat()

useEffect(() => {
getChatRooms()
}, [])

return (
<Stack
width={280}
height="100%"
borderRight="1px solid #e0e0e0"
bgcolor="#f8f9fa"
>
<Stack
height={60}
direction="row"
alignItems="center"
justifyContent="space-between"
px={2}
borderBottom="1px solid #e0e0e0"
bgcolor="white"
>
<Typography variant="h6" fontWeight="bold">
채팅 목록
</Typography>
<Typography variant="caption" color="text.secondary">
{authUserData?.name}님
</Typography>
</Stack>

<Stack flex="1" overflow="auto" p={2} gap={1}>
{/* 새 채팅 만들기 버튼 */}
<Button
startIcon={<AddCircleOutlineIcon />}
fullWidth
variant="outlined"
sx={{ mb: 1, borderRadius: 2, bgcolor: "white" }}
onClick={() => {
setSelectedChatRoomId("")
setSelectedChatRoom(null)
}}
>
새 채팅
</Button>

{chatRooms.map((room) => (
<Box
key={room.id}
onClick={() => setSelectedChatRoomId(room.id)}
sx={{
p: 2,
cursor: "pointer",
borderRadius: 2,
bgcolor: selectedChatRoomId === room.id ? "white" : "transparent",
boxShadow:
selectedChatRoomId === room.id
? "0 2px 4px rgba(0,0,0,0.05)"
: "none",
transition: "all 0.2s",
"&:hover": {
bgcolor:
selectedChatRoomId === room.id ? "white" : "rgba(0,0,0,0.04)",
},
border:
selectedChatRoomId === room.id
? "1px solid #e0e0e0"
: "1px solid transparent",
}}
>
<Typography
variant="subtitle2"
noWrap
color={
selectedChatRoomId === room.id ? "primary.main" : "text.primary"
}
fontWeight={selectedChatRoomId === room.id ? "bold" : "normal"}
>
{room.title || "새로운 채팅"}
</Typography>
<Typography
variant="caption"
color="text.secondary"
display="block"
noWrap
>
{dayjs(room.createdAt).format("YY-MM-DD HH:mm")}
</Typography>
</Box>
))}
</Stack>
</Stack>
)
}
138 changes: 138 additions & 0 deletions client/src/app/admin/ai/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"use client"

import useAiChat from "./useAiChat"
import { marked } from "marked"
import { AIChat } from "@server/entity/ai/aiChat"
import { Box, Stack, Typography, Avatar, CircularProgress } from "@mui/material"
import SmartToyIcon from "@mui/icons-material/SmartToy"
import PersonIcon from "@mui/icons-material/Person"
import MoreHorizIcon from "@mui/icons-material/MoreHoriz"

import { useRef, useEffect } from "react"

export enum ChatType {
USER = "user",
AI = "ai",
SYSTEM = "system",
}

export default function AdminAIChatComponent() {
const { selectedChatRoom, isAiReplying } = useAiChat()
const scrollRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollIntoView({ behavior: "smooth" })
}
}, [selectedChatRoom?.chats, isAiReplying])

return (
<Stack gap={2} padding={2} minHeight="calc(100% - 40px)" bgcolor="#f5f7f9">
{selectedChatRoom &&
selectedChatRoom.chats &&
selectedChatRoom.chats.map((chat) => (
<ChatComponent key={chat.id} chat={chat} />
))}
{isAiReplying && <AiLoadingComponent />}
<div style={{ minHeight: "20px" }} ref={scrollRef} />
</Stack>
)
}

function AiLoadingComponent() {
return (
<Stack direction="row" alignItems="flex-start" gap={2}>
<Avatar
sx={{
bgcolor: "secondary.main",
width: 32,
height: 32,
}}
>
<SmartToyIcon fontSize="small" />
</Avatar>
<Box
sx={{
py: 1.5,
px: 2,
borderRadius: 2,
bgcolor: "white",
color: "text.primary",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
borderTopLeftRadius: 0,
display: "flex",
alignItems: "center",
}}
>
<MoreHorizIcon
color="disabled"
sx={{
animation: "pulse 1.5s infinite ease-in-out",
"@keyframes pulse": {
"0%": { opacity: 0.3 },
"50%": { opacity: 1 },
"100%": { opacity: 0.3 },
},
}}
/>
</Box>
</Stack>
)
}

function ChatComponent({ chat }: { chat: AIChat }) {
const isUser = chat.type === ChatType.USER

return (
<Stack
gap={2}
alignItems="flex-start"
direction={isUser ? "row-reverse" : "row"}
>
<Avatar
sx={{
bgcolor: isUser ? "primary.main" : "secondary.main",
width: 32,
height: 32,
}}
>
{isUser ? (
<PersonIcon fontSize="small" />
) : (
<SmartToyIcon fontSize="small" />
)}
</Avatar>

<Box
maxWidth="70%"
sx={{
px: 2,
borderRadius: 2,
bgcolor: isUser ? "primary.main" : "white",
color: isUser ? "white" : "text.primary",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)", // 부드러운 그림자
borderTopRightRadius: isUser ? 0 : 2, // 말풍선 꼬리 느낌 (선택)
borderTopLeftRadius: !isUser ? 0 : 2,
"& a": { color: isUser ? "#fff" : "primary.main" }, // 링크 색상 조정
"& code": {
fontFamily: "monospace",
bgcolor: isUser ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.05)",
borderRadius: 1,
px: 0.5,
},
"& pre": {
bgcolor: isUser ? "rgba(0,0,0,0.2)" : "#f5f5f5",
p: 1,
borderRadius: 1,
overflow: "auto",
},
}}
>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(chat.message) }}
style={{ lineHeight: 1.6, fontSize: "0.95rem" }}
/>
</Box>
</Stack>
)
}
Loading