diff --git a/src/app/routes.ts b/src/app/routes.ts
index 76e6b905..83d97e1d 100644
--- a/src/app/routes.ts
+++ b/src/app/routes.ts
@@ -8,6 +8,7 @@ export default [
route('sell', 'routes/(main)/sell.tsx'),
route('sell/confirm', 'routes/(main)/sell.confirm.tsx'),
route('mypage', 'routes/(main)/mypage.tsx'),
+ route('chatbot', 'routes/(main)/chatbot.tsx'),
route('mypage/settings', 'routes/(main)/mypage.settings.tsx'),
route('mypage/profile', 'routes/(main)/mypage.profile.tsx'),
]),
diff --git a/src/app/routes/(main)/chatbot.tsx b/src/app/routes/(main)/chatbot.tsx
new file mode 100644
index 00000000..68c46292
--- /dev/null
+++ b/src/app/routes/(main)/chatbot.tsx
@@ -0,0 +1,3 @@
+import { ChatbotPage } from '@pages/chatbot';
+
+export default ChatbotPage;
diff --git a/src/pages/chatbot/index.ts b/src/pages/chatbot/index.ts
new file mode 100644
index 00000000..bd7ab788
--- /dev/null
+++ b/src/pages/chatbot/index.ts
@@ -0,0 +1 @@
+export { default as ChatbotPage } from './ui/ChatbotPage';
diff --git a/src/pages/chatbot/model/types.ts b/src/pages/chatbot/model/types.ts
new file mode 100644
index 00000000..ff21552e
--- /dev/null
+++ b/src/pages/chatbot/model/types.ts
@@ -0,0 +1,10 @@
+export type ChatMessageStatus = 'loading' | 'done';
+
+export type ChatMessageRole = 'bot' | 'user';
+
+export type ChatMessage = {
+ id: string;
+ role: ChatMessageRole;
+ content: string;
+ status?: ChatMessageStatus;
+};
diff --git a/src/pages/chatbot/ui/ChatMessageList.tsx b/src/pages/chatbot/ui/ChatMessageList.tsx
new file mode 100644
index 00000000..6718a55a
--- /dev/null
+++ b/src/pages/chatbot/ui/ChatMessageList.tsx
@@ -0,0 +1,41 @@
+import { ChatBubble } from '@shared/ui/ChatBubble';
+import type { ChatMessage } from '../model/types';
+
+type ChatMessageListProps = {
+ messages: ChatMessage[];
+};
+
+export const ChatMessageList = ({ messages }: ChatMessageListProps) => {
+ if (messages.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {messages.map((message) => {
+ const isBot = message.role === 'bot';
+ if (isBot) {
+ return (
+
+ {message.status === 'loading' ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+};
diff --git a/src/pages/chatbot/ui/ChatbotPage.tsx b/src/pages/chatbot/ui/ChatbotPage.tsx
new file mode 100644
index 00000000..88e48cab
--- /dev/null
+++ b/src/pages/chatbot/ui/ChatbotPage.tsx
@@ -0,0 +1,110 @@
+import { Logo4 } from '@shared/assets/logo';
+import { ChatInput } from '@shared/ui/ChatInput';
+import { useEffect, useRef, useState } from 'react';
+import { ChatMessageList } from './ChatMessageList';
+import type { ChatMessage } from '../model/types';
+
+const INITIAL_BOT_MESSAGE = `루핏이 예상 수리비를 빠르게 계산해드릴게요. 아래 3가지만 알려주세요.
+(견적은 추정치이며 실제 비용은 수리점/부품/상태에 따라 달라질 수 있어요.)
+
+기종: 예) 아이폰 15, 갤럭시 S23
+
+어디가 고장났는지: 예) 액정/배터리/카메라/후면/충전/침수
+
+보험/케어 가입 여부: 예) 애플케어 O/X, 통신사 보험 O/X
+
+가능하면\u00A0원하는\u00A0방향도\u00A0한\u00A0줄로\u00A0적어주세요:\u00A0“정품\u00A0우선”\u00A0/\u00A0“최대한\u00A0저렴하\u2060게”\u00A0/\u00A0“빨리”
+
+예시) “아이폰 15, 액정 깨짐, 애플케어 X, 최대한 저렴하게”
+
+`;
+
+const ChatbotPage = () => {
+ const [message, setMessage] = useState('');
+ const [messages, setMessages] = useState(() => [
+ {
+ id: 'bot-initial',
+ role: 'bot' as const,
+ content: INITIAL_BOT_MESSAGE,
+ status: 'done',
+ },
+ ]);
+ const replyTimeoutRef = useRef(null);
+ const endOfMessagesRef = useRef(null);
+
+ useEffect(() => {
+ return () => {
+ if (replyTimeoutRef.current) {
+ window.clearTimeout(replyTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ endOfMessagesRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
+ }, [messages]);
+
+ const handleSend = (value: string) => {
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return;
+ }
+
+ const userId = `user-${Date.now()}`;
+ const botId = `bot-${Date.now()}`;
+ setMessages((prev) => [
+ ...prev,
+ { id: userId, role: 'user', content: trimmed, status: 'done' },
+ { id: botId, role: 'bot', content: '답변을 준비 중이에요...', status: 'loading' },
+ ]);
+ setMessage('');
+
+ if (replyTimeoutRef.current) {
+ window.clearTimeout(replyTimeoutRef.current);
+ }
+ replyTimeoutRef.current = window.setTimeout(() => {
+ setMessages((prev) =>
+ prev.map((item) =>
+ item.id === botId
+ ? {
+ ...item,
+ status: 'done',
+ content: '입력하신 내용으로 예상 수리비를 준비할게요.\n필요하면 기종/증상 상세를 더 알려주세요!',
+ }
+ : item
+ )
+ );
+ }, 800);
+ };
+
+ return (
+
+ );
+};
+
+export default ChatbotPage;
diff --git a/src/shared/assets/logo/index.ts b/src/shared/assets/logo/index.ts
index 66828455..e4071159 100644
--- a/src/shared/assets/logo/index.ts
+++ b/src/shared/assets/logo/index.ts
@@ -1,5 +1,6 @@
import Logo from './logo.svg?react';
import Logo2 from './logo2.svg?react';
import Logo3 from './logo3.svg?react';
+import Logo4 from './logo4.svg?react';
-export { Logo, Logo2, Logo3 };
+export { Logo, Logo2, Logo3, Logo4 };
diff --git a/src/shared/ui/ChatBubble/ChatBubble.variants.ts b/src/shared/ui/ChatBubble/ChatBubble.variants.ts
index 12b55b57..c1edf249 100644
--- a/src/shared/ui/ChatBubble/ChatBubble.variants.ts
+++ b/src/shared/ui/ChatBubble/ChatBubble.variants.ts
@@ -6,11 +6,11 @@ export const chatBubbleVariants = tv({
bubble: [
'inline-flex',
'items-center',
- 'justify-center',
+ 'justify-start',
'min-h-[60px]',
'px-[31px]',
'py-[18px]',
- 'rounded-(--radius-l)',
+ 'rounded-[var(--radius-l)]',
'max-w-[559px]',
],
text: ['typo-body-1', 'whitespace-pre-wrap', 'break-words'],
@@ -31,6 +31,12 @@ export const chatBubbleVariants = tv({
text: ['text-gray-900'],
meta: ['order-last'],
},
+ chatbotNotice: {
+ root: ['justify-start'],
+ bubble: ['bg-gray-100', 'max-w-[720px]'],
+ text: ['text-gray-900', 'whitespace-pre-line', 'break-keep'],
+ meta: ['order-last'],
+ },
},
},