Skip to content

Commit d27bd2a

Browse files
authored
Merge pull request #58 from prgrms-web-devcourse-final-project/style/recommend#54
[style] 챗봇 추천 페이지 ui
2 parents 2790d27 + d4defbf commit d27bd2a

File tree

15 files changed

+288
-48
lines changed

15 files changed

+288
-48
lines changed

src/app/design-system/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { useState } from 'react';
77
import { customToast } from '@/shared/components/toast/CustomToastUtils';
88
import ModalLayout from '@/shared/components/modalPop/ModalLayout';
99
import ConfirmPop from '@/shared/components/modalPop/ConfirmPop';
10-
import ChatInput from '@/shared/components/InputBox/ChatInput';
1110
import SelectBox from '@/shared/components/InputBox/SelectBox';
1211
import LikeBtn from '@/shared/components/like/LikeBtn';
1312
import Share from '@/shared/components/share/Share';
@@ -34,7 +33,6 @@ function Page() {
3433
<Input placeholder="내용을 입력해주세요." id="test" variant="search" />
3534
<Input placeholder="칵테일을 검색해 보세요" id="test" variant="comment" />
3635
<Input placeholder="내용을 입력해주세요." id="test" size="lg" />
37-
<ChatInput placeholder="내용을 입력해주세요" id="test" />
3836
</div>
3937

4038
{/* select */}

src/app/layout.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Metadata } from 'next';
22
import '@/shared/styles/global.css';
33
import { Toaster } from 'react-hot-toast';
4-
import ScrollTopBtn from '@/shared/components/scrollTop/ScrollTopBtn';
54
import Header from '@/shared/components/header/Header';
6-
import Footer from '@/shared/components/footer/Footer';
5+
import FooterWrapper from '@/shared/components/footer/FooterWrapper';
6+
import ScrollTopBtnWrapper from '@/shared/components/scrollTop/ScrollTopBtnWrapper';
77
export const metadata: Metadata = {
88
title: 'SSOUL',
99
description: '칵테일을 좋아하는 사람들을 위한 서비스',
@@ -22,7 +22,7 @@ export default function RootLayout({
2222
<div id="observer-target" className="h-[0.5px]"></div>
2323
{children}
2424
</main>
25-
<Footer />
25+
<FooterWrapper />
2626

2727
<div id="modal-root"></div>
2828
<Toaster
@@ -36,7 +36,7 @@ export default function RootLayout({
3636
}}
3737
/>
3838

39-
<ScrollTopBtn />
39+
<ScrollTopBtnWrapper />
4040
</body>
4141
</html>
4242
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Image from 'next/image';
2+
import Dummy from '@/shared/assets/images/dummy/exampleCocktail.png';
3+
import Link from 'next/link';
4+
import Keep from '@/shared/components/keep/Keep';
5+
6+
function ChatCocktailCard() {
7+
return (
8+
<div className="relative flex flex-col w-full min-w-[200px] rounded-2xl overflow-hidden bg-white shadow-[0_0_12px_rgba(255,255,255,0.4)]">
9+
<Link href="/" className="block relative">
10+
<div className="relative w-full h-[200px]">
11+
<Image src={Dummy} fill className="object-cover" alt="칵테일 이름" />
12+
</div>
13+
14+
<div className="p-3 flex flex-col gap-1 text-center">
15+
<strong className="text-black text-lg">{'진피즈'}</strong>
16+
<span className="text-gray-500 text-sm">+ 상세보기</span>
17+
</div>
18+
</Link>
19+
<Keep className="absolute top-2 right-2 z-50" />
20+
</div>
21+
);
22+
}
23+
export default ChatCocktailCard;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client';
2+
3+
import Send from '@/shared/assets/icons/send_36.svg';
4+
import { keyDown } from '@/shared/utills/keyDown';
5+
import { useState } from 'react';
6+
7+
function ChatForm() {
8+
const handleInput = (e: React.FormEvent<HTMLTextAreaElement>) => {
9+
const target = e.currentTarget;
10+
11+
if (target.value == '') {
12+
target.style.height = '';
13+
}
14+
target.style.height = `${target.scrollHeight}px`;
15+
};
16+
17+
return (
18+
<div className="fixed left-0 bottom-0 w-full px-3 py-4 bg-primary">
19+
<form onSubmit={(e) => e.preventDefault()}>
20+
<div className="flex items-end w-full gap-2">
21+
<label htmlFor="chatInput" className="sr-only">
22+
질문 입력창
23+
</label>
24+
<textarea
25+
id="chatInput"
26+
name="chatInput"
27+
onKeyDown={(e) => keyDown(e)}
28+
onInput={(e) => handleInput(e)}
29+
placeholder="칵테일 추천 질문을 입력해주세요."
30+
className="w-[calc(100%-3rem)] md:w-[calc(100%-3.75rem)] px-4 py-2 md:py-3.5 rounded-lg h-[40px] md:h-[52px] max-h-[160px] md:max-h-[280px] bg-white text-primary placeholder:text-gray-dark resize-none outline-none"
31+
/>
32+
<button
33+
type="button"
34+
aria-label="보내기"
35+
className="flex-center w-10 md:w-13 h-10 md:h-13 rounded-xl border-1 border-white bg-secondary/20"
36+
>
37+
<Send className="text-secondary" />
38+
</button>
39+
</div>
40+
</form>
41+
</div>
42+
);
43+
}
44+
export default ChatForm;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
interface Option {
2+
label: string;
3+
value: string;
4+
}
5+
6+
interface RadioGroupProps {
7+
options: Option[];
8+
value: string;
9+
onChange: (value: string) => void;
10+
}
11+
12+
function ChatRadio({ options, value, onChange }: RadioGroupProps) {
13+
return (
14+
<div role="radiogroup" className="flex flex-col gap-3 mt-5">
15+
{options.map((opt) => (
16+
<label
17+
htmlFor={`radio-${opt.value}`}
18+
key={opt.value}
19+
className="flex items-center gap-2 cursor-pointer"
20+
>
21+
<input
22+
id={`radio-${opt.value}`}
23+
type="radio"
24+
name="radio-group"
25+
value={opt.value}
26+
checked={value === opt.value}
27+
onChange={() => onChange(opt.value)}
28+
className="sr-only"
29+
/>
30+
<span
31+
className={`w-full rounded-3xl px-2 py-1 text-center transition-colors duration-150
32+
${value === opt.value ? 'bg-secondary shadow-[inset_0_0_6px_rgba(255,196,1,1)]' : 'bg-gray-dark/30'}
33+
hover:bg-secondary/100 `}
34+
>
35+
<span>{opt.label}</span>
36+
</span>
37+
</label>
38+
))}
39+
</div>
40+
);
41+
}
42+
export default ChatRadio;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
interface Message {
2+
id: string;
3+
sender: 'ssury' | 'user';
4+
text: string;
5+
type?: 'radio' | 'text';
6+
}
7+
8+
// 메시지 (연속 메시지)
9+
const messages: Message[] = [
10+
{
11+
id: '1',
12+
sender: 'user',
13+
text: '냥냥냥글자가길어지면 어케될까요 너무너무너무 궁금해요 하하하하하하하하하하하하하하하하하하ㅏ',
14+
},
15+
{ id: '2', sender: 'user', text: '배고파요' },
16+
];
17+
18+
function MyChat() {
19+
return (
20+
<article aria-label="내 메시지" className="flex flex-col items-end">
21+
<header className="w-fit">
22+
<strong></strong>
23+
</header>
24+
25+
{/* 메시지 그룹 */}
26+
<div className="flex flex-col items-end gap-3 mt-3 pr-3 max-w-[80%]">
27+
{messages.map((msg) => (
28+
<div
29+
key={msg.id}
30+
className="w-fit min-w-[120px] p-3 rounded-2xl rounded-tr-none bg-tertiary text-white"
31+
>
32+
<p className="whitespace-pre-line">{msg.text}</p>
33+
</div>
34+
))}
35+
</div>
36+
</article>
37+
);
38+
}
39+
export default MyChat;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
'use client';
2+
3+
import Ssury from '@/shared/assets/ssury/ssury_shaker.webp';
4+
import Image from 'next/image';
5+
import { useState } from 'react';
6+
import ChatRadio from './ChatRadio';
7+
import ChatCocktailCard from './ChatCocktailCard';
8+
9+
interface Message {
10+
id: string;
11+
text?: string;
12+
type?: 'radio' | 'text' | 'recommend';
13+
}
14+
15+
function SsuryChat() {
16+
const [selected, setSelected] = useState('option1');
17+
18+
// radio 옵션
19+
const options = [
20+
{ label: '옵션 1', value: 'option1' },
21+
{ label: '옵션 2', value: 'option2' },
22+
{ label: '옵션 3', value: 'option3' },
23+
];
24+
25+
// 메시지 (연속 메시지)
26+
const messages: Message[] = [
27+
{
28+
id: '1',
29+
text: '안녕하세요, 바텐더 쑤리에요. \n 취향에 맞는 칵테일을 추천해드릴게요!',
30+
},
31+
{
32+
id: '2',
33+
text: '어떤 유형으로 찾아드릴까요?',
34+
type: 'radio',
35+
},
36+
{
37+
id: '3',
38+
type: 'recommend',
39+
},
40+
];
41+
42+
return (
43+
<article aria-label="취향추천 챗봇 메시지" className="">
44+
<header className="flex items-end">
45+
<div className="relative w-15 md:w-20 h-15 md:h-20">
46+
<Image
47+
src={Ssury}
48+
alt="쑤리아바타"
49+
width={80}
50+
height={80}
51+
className="object-cover w-15 h-15"
52+
/>
53+
</div>
54+
<strong>쑤리</strong>
55+
</header>
56+
57+
{/* 메시지 그룹 */}
58+
<div className="flex flex-col gap-3 mt-3 pl-3">
59+
{messages.map((msg) => (
60+
<div key={msg.id}>
61+
{msg.type === 'recommend' ? (
62+
<ul className="inline-grid grid-cols-1 sm:grid-cols-3 gap-2 justify-start">
63+
<li>
64+
<ChatCocktailCard />
65+
</li>
66+
<li>
67+
<ChatCocktailCard />
68+
</li>
69+
<li>
70+
<ChatCocktailCard />
71+
</li>
72+
</ul>
73+
) : (
74+
<div className="flex flex-col w-fit max-w-[80%] min-w-[120px] p-3 rounded-2xl rounded-tl-none bg-white text-black">
75+
{msg.text && <p className="whitespace-pre-line">{msg.text}</p>}
76+
77+
{/* radio */}
78+
{msg.type === 'radio' && (
79+
<ChatRadio options={options} value={selected} onChange={setSelected} />
80+
)}
81+
</div>
82+
)}
83+
</div>
84+
))}
85+
</div>
86+
</article>
87+
);
88+
}
89+
export default SsuryChat;

src/app/recommend/page.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
1+
import Bg from '@/shared/assets/images/recommend_bg.webp';
2+
import ChatForm from './components/ChatForm';
3+
import SsuryChat from './components/SsuryChat';
4+
import MyChat from './components/MyChat';
5+
16
function Page() {
2-
return <div>recommend</div>;
7+
return (
8+
<div
9+
className="relative bg-repeat-y bg-top bg-auto w-full flex"
10+
style={{ backgroundImage: `url(${Bg.src})` }}
11+
>
12+
<h1 className="sr-only">취향추천하기</h1>
13+
<div className="page-layout max-w-1024 py-12 ">
14+
<div className="flex flex-col gap-10 pb-20">
15+
<SsuryChat />
16+
<MyChat></MyChat>
17+
</div>
18+
</div>
19+
<ChatForm />
20+
</div>
21+
);
322
}
423
export default Page;
Lines changed: 1 addition & 1 deletion
Loading
274 KB
Loading

0 commit comments

Comments
 (0)