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
94 changes: 89 additions & 5 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"chromatic": "chromatic --exit-zero-on-changes"
},
"dependencies": {
"@chatscope/chat-ui-kit-react": "^2.1.1",
"@chatscope/chat-ui-kit-styles": "^1.4.0",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"axios": "^1.12.2",
Expand Down
3 changes: 3 additions & 0 deletions public/icons/ic_gpt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export const runtime = "edge";

export async function POST(req: Request) {
const { message } = await req.json();

const res = await fetch(`${process.env.OPENAI_API_URL}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-5-nano",
messages: [
{
role: "system",
content:
"당신은 와인 추천 전문 어시스턴트입니다. 모든 대답은 자연스러운 한국어로 2~3문장 이내로 간결하게 작성하세요. 사용자의 질문이 와인과 관련이 없으면 '이 AI는 와인에 대한 내용만 답변 가능합니다.'라고만 답하세요. 추천 시에는 간단한 이유를 한 문장으로 덧붙이세요. 불확실한 정보는 추측하지 말고 '해당 정보는 정확히 알 수 없습니다.'라고 답하세요. 이미지나 그림, 사진을 생성하거나 설명해 달라는 요청이 있을 경우 '이 AI는 이미지 관련 기능을 지원하지 않습니다.'라고만 답하세요.",
},
{ role: "user", content: message },
],
}),
});

if (!res.ok) {
const err = await res.text();
return new Response(JSON.stringify({ error: err }), { status: 500 });
}

const data = await res.json();
const reply = data.choices?.[0]?.message?.content ?? "";
return new Response(JSON.stringify({ reply }), {
headers: { "Content-Type": "application/json" },
});
}
1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import QueryProvider from "@/providers/query-provider";
import getMe from "@/api/user/get-me";
import KaKaoInitializer from "@/lib/kakao-initializer";
import ToastProvider from "@/providers/toast/toast-provider";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import { ToastContainer } from "react-toastify";

export function generateMetadata() {
Expand Down
38 changes: 38 additions & 0 deletions src/components/button/chat-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { cn } from "@/lib/utils";
import { IconButton } from "@/components";
import { usePathname } from "next/navigation";
import { useState } from "react";
import ChatBot from "../chat-bot/chat-bot";

/**
* 채팅 봇을 열기 위한 버튼 컴포넌트
* @author jikwon
*/

const ChatButton = () => {
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);

if (pathname !== "/wines") return null;

return (
<>
<IconButton
icon="GptIcon"
iconSize="md"
className={cn(
"relative h-[40px] w-[40px] rounded-full border-gray-300",
"tablet:h-[50px] tablet:w-[50px]",
"pc:h-[50px] pc:w-[50px]"
)}
aria-label="채팅 봇 열기"
onClick={() => setIsOpen(!isOpen)}
/>
{isOpen && <ChatBot open={isOpen} />}
</>
);
};

export default ChatButton;
Loading