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
23 changes: 23 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ model User {
aiConversations RoomAiConversation[]
aiMessages RoomAiMessage[]
termsAgreements UserTermsAgreement[]
chatSessions ChatSession[]
profileMediaAsset MediaAsset? @relation("UserProfileAsset", fields: [profileMediaAssetId], references: [id])

@@map("users")
Expand Down Expand Up @@ -360,6 +361,28 @@ model EmotionDiary {
}


model ChatSession {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
userId String @map("user_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
messages ChatMessage[]

@@map("chat_sessions")
}

model ChatMessage {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
sessionId String @map("session_id") @db.Uuid
role String @db.VarChar(10)
content String
createdAt DateTime @default(now()) @map("created_at")
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)

@@map("chat_messages")
}

model RoomAccessLog {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
roomId String? @map("room_id") @db.Uuid
Expand Down
28 changes: 0 additions & 28 deletions src/app/(page)/contact/page.tsx

This file was deleted.

6 changes: 4 additions & 2 deletions src/app/(page)/mypage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ import ConfirmModal from '@/components/ui/ConfirmModal';
import { useUserMe } from '@/domains/user/hooks';
import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded';
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import Chatbot from '@/components/chatbot/Chatbot';
import styles from './page.module.scss';

const PREPARING_ITEMS = ['shop', 'contact'] as const;
const PREPARING_ITEMS = ['shop'] as const;
type PreparingKey = (typeof PREPARING_ITEMS)[number];

const LINK_ITEMS = [
{ key: 'shop', label: '상점', href: '/shop' },
{ key: 'about', label: '서비스 소개', href: '/about' },
{ key: 'privacy', label: '개인정보처리방침', href: '/privacy' },
{ key: 'terms', label: '이용약관', href: '/terms' },
{ key: 'contact', label: '고객문의', href: '/contact' },
] as const;

export default function MyPage() {
Expand Down Expand Up @@ -104,6 +104,8 @@ export default function MyPage() {
onClose={() => setLogoutOpen(false)}
onConfirm={handleLogout}
/>

<Chatbot />
</>
);
}
137 changes: 137 additions & 0 deletions src/app/api/chatbot/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { getSessionUserId } from '@/lib/auth/session'
import { getChatbotResponse, ChatbotTimeoutError, ChatbotGeminiError } from '@/lib/ai/chatbot'
import {
getOrCreateSession,
getSessionMessages,
saveMessage,
clearSession,
} from '@/domains/common/chatbot.service'

function mapRole(role: string): 'user' | 'bot' {
return role.toLowerCase() === 'user' ? 'user' : 'bot'
}

export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
const userId = getSessionUserId(session)
if (!userId) {
return NextResponse.json(
{ success: false, error: '인증이 필요합니다.' },
{ status: 401 }
)
}

const body = await req.json()
const { message } = body as { message: string }
Comment on lines +28 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

오류 유형 분기가 없어 JSON 파싱 실패/ Gemini 실패/타임아웃이 모두 500으로 처리됩니다.

Line 24와 Line 51의 실패를 Line 56-61에서 단일 500으로 처리하면, 클라이언트 복구 동작과 운영 진단이 어렵습니다. 최소한 파싱 실패(400), Gemini 업스트림 실패(502), 타임아웃(504)로 분리해주세요.

As per coding guidelines, "src/app/api/**/*.{ts,tsx}: ... Distinguish between Gemini API failures, JSON parsing failures, and timeouts."

Also applies to: 51-51, 56-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/chatbot/route.ts` around lines 24 - 25, The error handling in the
route handler currently treats JSON parsing failures, Gemini API failures, and
timeouts as a single 500 error, making client recovery and operational
diagnostics difficult. Separate the error handling for the req.json() call at
line 24 with its own try-catch block to return 400 for JSON parsing errors, and
add distinct error handling for the Gemini API call around line 51 to
differentiate between upstream API failures (502) and timeout errors (504), then
update the error response logic in lines 56-61 to check the error type and
return the appropriate HTTP status code accordingly.

Source: Coding guidelines


if (!message || typeof message !== 'string' || message.trim().length === 0) {
return NextResponse.json(
{ success: false, error: '메시지를 입력해주세요.' },
{ status: 400 }
)
}

if (message.length > 500) {
return NextResponse.json(
{ success: false, error: '메시지는 500자 이내로 입력해주세요.' },
{ status: 400 }
)
}

const chatSession = await getOrCreateSession(userId)
const dbMessages = await getSessionMessages(chatSession.id)

const history = dbMessages.map((m) => ({
role: mapRole(m.role),
content: m.content,
}))

await saveMessage(chatSession.id, 'user', message.trim())

const reply = await getChatbotResponse(message.trim(), history)

await saveMessage(chatSession.id, 'bot', reply)

return NextResponse.json({ success: true, data: { reply } })
} catch (error) {
console.error('[Chatbot API Error]', error instanceof Error ? error.message : error)

if (error instanceof ChatbotTimeoutError) {
return NextResponse.json(
{ success: false, error: '응답 시간이 초과되었습니다. 잠시 후 다시 시도해주세요.' },
{ status: 504 }
)
}
if (error instanceof ChatbotGeminiError) {
return NextResponse.json(
{ success: false, error: 'AI 서비스에 일시적인 문제가 발생했습니다. 잠시 후 다시 시도해주세요.' },
{ status: 502 }
)
}

return NextResponse.json(
{ success: false, error: '답변 생성에 실패했습니다. 잠시 후 다시 시도해주세요.' },
{ status: 500 }
)
}
}

export async function GET() {
try {
const session = await getServerSession(authOptions)
const userId = getSessionUserId(session)
if (!userId) {
return NextResponse.json(
{ success: false, error: '인증이 필요합니다.' },
{ status: 401 }
)
}

const chatSession = await getOrCreateSession(userId)
const messages = await getSessionMessages(chatSession.id)

const data = messages.map((m) => ({
role: mapRole(m.role),
content: m.content,
timestamp: m.createdAt.toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit',
}),
}))

return NextResponse.json({ success: true, data: { messages: data } })
} catch (error) {
console.error('[Chatbot API Error]', error instanceof Error ? error.message : error)
return NextResponse.json(
{ success: false, error: '대화 내역을 불러오지 못했습니다.' },
{ status: 500 }
)
}
}

export async function DELETE() {
try {
const session = await getServerSession(authOptions)
const userId = getSessionUserId(session)
if (!userId) {
return NextResponse.json(
{ success: false, error: '인증이 필요합니다.' },
{ status: 401 }
)
}

await clearSession(userId)

return NextResponse.json({ success: true })
} catch (error) {
console.error('[Chatbot API Error]', error instanceof Error ? error.message : error)
return NextResponse.json(
{ success: false, error: '대화 초기화에 실패했습니다.' },
{ status: 500 }
)
}
}
Loading