Skip to content

Commit d1527a9

Browse files
authored
Merge branch 'develop' into feat/#86/savepage
2 parents b458ed1 + 71c9fe0 commit d1527a9

39 files changed

+991
-46
lines changed

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"@tanstack/react-query": "^5.90.5",
1616
"axios": "^1.12.2",
1717
"clsx": "^2.1.1",
18+
"lottie-react": "^2.4.1",
1819
"next": "15.5.4",
1920
"react": "19.1.0",
2021
"react-dom": "19.1.0",
@@ -24,23 +25,23 @@
2425
},
2526
"devDependencies": {
2627
"@eslint/eslintrc": "^3",
28+
"@radix-ui/react-popover": "^1.1.15",
29+
"@radix-ui/react-progress": "^1.1.7",
30+
"@radix-ui/react-slot": "^1.2.3",
2731
"@tailwindcss/postcss": "^4",
2832
"@types/node": "^20",
2933
"@types/react": "^19",
3034
"@types/react-dom": "^19",
3135
"autoprefixer": "^10.4.21",
3236
"class-variance-authority": "^0.7.1",
37+
"date-fns": "^4.1.0",
3338
"eslint": "^9",
3439
"eslint-config-next": "15.5.4",
40+
"lucide-react": "^0.540.0",
3541
"postcss": "^8.5.6",
42+
"react-day-picker": "^9.11.1",
3643
"svg-sprite-loader": "^6.0.11",
3744
"tailwindcss": "4.1.14",
38-
"typescript": "^5",
39-
"@radix-ui/react-progress": "^1.1.7",
40-
"@radix-ui/react-popover": "^1.1.15",
41-
"date-fns": "^4.1.0",
42-
"react-day-picker": "^9.11.1",
43-
"@radix-ui/react-slot": "^1.2.3",
44-
"lucide-react": "^0.540.0"
45+
"typescript": "^5"
4546
}
4647
}

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/assets/card.png

94.5 KB
Loading

public/assets/card2.png

12.4 KB
Loading

public/assets/sample1.jpg

99.1 KB
Loading

public/lottie/loading_revised.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"nm":"Main Scene","ddd":0,"h":180,"w":180,"meta":{"g":"@lottiefiles/creator 1.58.0"},"layers":[{"ty":0,"nm":"Nested Scene 1","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[80,80]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[90,90]},"r":{"a":1,"k":[{"o":{"x":0.7,"y":0.064},"i":{"x":0.3,"y":0.936},"s":[0],"t":0},{"o":{"x":0.7,"y":0.064},"i":{"x":0.3,"y":0.936},"s":[360],"t":50},{"o":{"x":0.7,"y":0.064},"i":{"x":0.3,"y":0.936},"s":[720],"t":100},{"s":[1080],"t":150}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"w":160,"h":160,"refId":"precomp_Shape Layer - SVG_Pyi9z_WozP_02f09327-b264-4238-b974-1580b3747f32","ind":1}],"v":"5.7.0","fr":30,"op":150,"ip":0,"assets":[{"nm":"Nested Scene 1","id":"precomp_Shape Layer - SVG_Pyi9z_WozP_02f09327-b264-4238-b974-1580b3747f32","layers":[{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":150,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[83.07499999999999,80]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[83.07499999999999,80]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","bm":0,"hd":false,"nm":"Group 1","np":2,"it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 1","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.83,0],[0,0.83],[-0.83,0],[0,-0.83]],"o":[[0,0.83],[-0.83,0],[0,-0.83],[0.83,0],[0,0]],"v":[[150.15,91.65],[148.65,93.15],[147.15,91.65],[148.65,90.15],[150.15,91.65]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.549,0.9412,0.949]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[148.64999999999998,91.64999999999998]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[148.64999999999998,91.64999999999998]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 2","np":2,"it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 2","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.55,0],[0,0.55],[-0.55,0],[0,-0.55]],"o":[[0,0.55],[-0.55,0],[0,-0.55],[0.55,0],[0,0]],"v":[[153.15,87.15],[152.15,88.15],[151.15,87.15],[152.15,86.15],[153.15,87.15]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.1098,0.851,0.6902]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[152.14999999999998,87.14999999999998]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[152.14999999999998,87.14999999999998]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 3","np":2,"it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 3","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.55,0],[0,0.55],[-0.55,0],[0,-0.55]],"o":[[0,0.55],[-0.55,0],[0,-0.55],[0.55,0],[0,0]],"v":[[131.15,77.15],[130.15,78.15],[129.15,77.15],[130.15,76.15],[131.15,77.15]]}}},{"ty":"fl","bm":0,"hd":false,"nm":"Fill","c":{"a":0,"k":[0.6392,0.9412,0.8784]},"r":1,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[130.14999999999998,77.15]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[130.14999999999998,77.15]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 4","np":2,"it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 4","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0.08,-0.75],[0,0],[-0.17,-0.21],[0,0],[0.74,-0.15],[0,0],[0.14,-0.23],[0,0],[0.37,0.65],[0,0],[0.26,0.07],[0,0],[-0.51,0.56],[0,0],[0.02,0.27],[0,0],[-0.69,-0.31],[0,0],[-0.25,0.1],[0,0]],"o":[[0.7,-0.28],[0,0],[-0.03,0.27],[0,0],[0.48,0.58],[0,0],[-0.26,0.06],[0,0],[-0.4,0.64],[0,0],[-0.13,-0.24],[0,0],[-0.73,-0.19],[0,0],[0.18,-0.2],[0,0],[-0.05,-0.75],[0,0],[0.25,0.11],[0,0],[0,0]],"v":[[147.66,70.22],[149.03,71.26],[148.38,77.15],[148.61,77.9],[152.4,82.47],[151.83,84.08],[146.02,85.29],[145.38,85.74],[142.21,90.75],[140.5,90.72],[137.55,85.57],[136.93,85.09],[131.18,83.63],[130.69,81.99],[134.68,77.59],[134.93,76.86],[134.55,70.94],[135.96,69.96],[141.37,72.39],[142.15,72.41],[147.66,70.22]]}}},{"ty":"gf","bm":0,"hd":false,"nm":"paint1_linear_625_3738","e":{"a":0,"k":[140.64,91.73]},"g":{"p":3,"k":{"a":0,"k":[0,0.7019607843137254,0.9607843137254902,0.9607843137254902,0.48,0.12941176470588237,0.8705882352941177,0.9019607843137255,1,0.10980392156862745,0.8509803921568627,0.6901960784313725]}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[141.69,71.26]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[141.52878571428568,80.54489285714288]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[141.52878571428568,80.54489285714288]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"gr","bm":0,"hd":false,"nm":"Group 5","np":2,"it":[{"ty":"sh","bm":0,"hd":false,"nm":"Path 5","d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-7.93,-3.28],[-6.22,-6.22],[-3.37,-8.13],[-0.08,-8.58],[0,-0.05],[0,-0.03],[0,-0.14],[0,0],[2.32,0],[0.25,2.25],[0,0],[2.91,7.04],[5.39,5.38],[7.04,2.91],[7.61,0],[0,0],[0,2.32],[-2.25,0.25],[0,0],[-0.13,0],[-0.03,0],[-0.05,0]],"o":[[8.58,0.08],[8.13,3.37],[6.22,6.22],[3.28,7.93],[0,0.05],[0,0.03],[0,0.14],[0,0],[-0.25,2.25],[-2.32,0],[0,0],[0,-7.61],[-2.91,-7.04],[-5.39,-5.38],[-7.03,-2.91],[0,0],[-2.25,-0.25],[0,-2.32],[0,0],[0.13,0],[0.03,0],[0.05,0],[0,0]],"v":[[80.65,13],[105.64,18.1],[127.38,32.62],[141.9,54.36],[147,79.35],[147,79.5],[147,79.59],[147,80],[146.97,80],[142.5,84],[138.03,80],[137.99,80],[133.57,57.81],[121,39],[102.19,26.43],[80,22.01],[80,21.97],[76,17.5],[80,13.03],[80,13],[80.4,13],[80.5,13],[80.65,13]]}}},{"ty":"gf","bm":0,"hd":false,"nm":"paint0_linear_625_3738","e":{"a":0,"k":[143.27,84]},"g":{"p":4,"k":{"a":0,"k":[0,0.7019607843137254,0.9607843137254902,0.9607843137254902,0.43,0.4,0.9098039215686274,0.9294117647058824,0.72,0.10980392156862745,0.8509803921568627,0.6901960784313725,1,0.0392156862745098,0.7686274509803922,0.6196078431372549]}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[79.73,13]},"r":2,"o":{"a":0,"k":100}},{"ty":"tr","a":{"a":0,"k":[111.5,48.5]},"s":{"a":0,"k":[100,100]},"sk":{"a":0,"k":0},"p":{"a":0,"k":[111.5,48.5]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":1}]}]}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use client';
2+
3+
import { cn } from '@/shared/lib';
4+
import { cva, type VariantProps } from 'class-variance-authority';
5+
6+
const chatBubbleStyle = cva(
7+
`
8+
flex items-center
9+
px-[1.9rem] py-[1.3rem]
10+
rounded-[2rem]
11+
text-label-lg foreground
12+
break-words
13+
w-fit max-w-[80%]
14+
`,
15+
{
16+
variants: {
17+
variant: {
18+
received: 'bg-gray-100 self-start',
19+
sent: 'bg-mint-100 self-end',
20+
},
21+
},
22+
defaultVariants: {
23+
variant: 'received',
24+
},
25+
},
26+
);
27+
28+
interface ChattingProps extends VariantProps<typeof chatBubbleStyle> {
29+
message: string;
30+
}
31+
32+
export default function Chatting({ message, variant }: ChattingProps) {
33+
return (
34+
<div className='w-full flex flex-col'>
35+
<div className={cn(chatBubbleStyle({ variant }))}>{message}</div>
36+
</div>
37+
);
38+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use client';
2+
import { Icon } from '@/shared/icons';
3+
import { cn } from '@/shared/lib';
4+
import { cva } from 'class-variance-authority';
5+
import { useChattingInput } from '@/shared/hooks/useChattingInput';
6+
7+
const inputWrapperStyle = cva(
8+
'flex items-center justify-between w-full bg-gray-100 px-[0.6rem] py-[0.7rem] rounded-[2rem]',
9+
);
10+
11+
interface ChattingInputProps {
12+
onSend?: (text: string) => void;
13+
}
14+
15+
16+
export default function ChattingInput({ onSend }: ChattingInputProps) {
17+
const {
18+
message,
19+
setMessage,
20+
inputRef,
21+
handleSubmit,
22+
handleKeyDown,
23+
} = useChattingInput({ onSend });
24+
25+
return (
26+
<div
27+
className={cn(
28+
'fixed bottom-0 left-1/2 -translate-x-1/2 w-full bg-gray-100 px-[0.6rem] py-[0.7rem] flex items-center gap-[0.8rem]',
29+
)}
30+
>
31+
<div
32+
className={cn(
33+
inputWrapperStyle(),
34+
'flex-1 h-[4rem] bg-white border border-gray-200 rounded-[2rem] flex items-center pl-[1.4rem] pr-[1.2rem] py-[1rem]',
35+
)}
36+
>
37+
<input
38+
ref={inputRef}
39+
value={message}
40+
onChange={(e) => setMessage(e.target.value)}
41+
onKeyDown={handleKeyDown}
42+
type='text'
43+
placeholder='무엇이든 물어보세요'
44+
className='w-full bg-transparent outline-none text-label-lg placeholder:text-gray-300 text-gray-900'
45+
/>
46+
</div>
47+
48+
<button
49+
type='button'
50+
onClick={handleSubmit}
51+
className={cn(
52+
'w-[4rem] h-[4rem] flex justify-center items-center rounded-[2rem] bg-mint-500 flex-shrink-0',
53+
)}
54+
>
55+
<Icon name='backto' size={20} color='gray-50' rotate={90} />
56+
</button>
57+
</div>
58+
);
59+
}

src/pages/chatbot/index.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use client';
2+
3+
import { useState, useEffect, useRef } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import { cva } from 'class-variance-authority';
6+
import { cn } from '@/shared/lib';
7+
import { Header } from '@/shared/components';
8+
import Chatting from '@/pages/chatbot/components/ChattingBubble';
9+
import ChattingInput from '@/pages/chatbot/components/ChattingInput';
10+
11+
const chatPageStyle = cva(
12+
'relative w-full h-dvh overflow-hidden bg-white flex flex-col',
13+
);
14+
15+
const mainStyle = cva(
16+
'relative w-full flex-1 pt-[14.4rem] pb-[10rem] px-[2.4rem] overflow-auto flex flex-col gap-[0.6rem]',
17+
);
18+
19+
const introStyle = cva('flex flex-col items-start gap-[1rem]');
20+
21+
export default function ChatPage() {
22+
const router = useRouter();
23+
const bottomRef = useRef<HTMLDivElement>(null);
24+
25+
type Message = {
26+
id: number;
27+
text: string;
28+
variant: 'received' | 'sent';
29+
};
30+
31+
const [messages, setMessages] = useState<Message[]>([]);
32+
33+
// 새로운 채팅 자동 스크롤
34+
useEffect(() => {
35+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
36+
}, [messages]);
37+
38+
// 메세지 전송 핸들러
39+
const handleSend = (text: string) => {
40+
if (!text.trim()) return;
41+
const newMsg = { id: Date.now(), text, variant: 'sent' as const };
42+
setMessages((prev) => [...prev, newMsg]);
43+
};
44+
45+
return (
46+
<div className={cn(chatPageStyle())}>
47+
{/* 헤더 고정 */}
48+
<div className='fixed top-0 left-0 w-full z-10'>
49+
<Header title='ChatBot' onClick={() => router.back()} />
50+
</div>
51+
52+
{/* 메인 콘텐츠 */}
53+
<main className={cn(mainStyle())}>
54+
{/* 로고 + 기본 멘트 */}
55+
<div className={cn(introStyle())}>
56+
{/* 로고 자리 (임시) */}
57+
<div className='w-[6rem] h-[6rem] rounded-full bg-gray-200 flex-shrink-0' />
58+
<Chatting
59+
message='안녕하세요, 글다에요! 부천시 여행에 대한 정보를 쉽게 알려드릴게요.'
60+
variant='received'
61+
/>
62+
<Chatting
63+
message='원하시는 정보를 물어봐주세요!'
64+
variant='received'
65+
/>
66+
</div>
67+
68+
{/* 사용자 메시지 */}
69+
{messages
70+
.filter((msg) => msg.id > 2)
71+
.map((msg) => (
72+
<Chatting key={msg.id} message={msg.text} variant={msg.variant} />
73+
))}
74+
75+
{/* 스크롤 */}
76+
<div ref={bottomRef} />
77+
</main>
78+
79+
{/* 입력창 */}
80+
<ChattingInput onSend={handleSend} />
81+
</div>
82+
);
83+
}

src/pages/events/[id].tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Header, EventCard } from '@/shared/components';
2+
import DateTag from '@/pages/events/components/DateTag';
3+
import { cn } from '@/shared/lib';
4+
import { eventData } from '@/shared/constants/events/eventsData';
5+
import Image from 'next/image';
6+
import { useRouter } from 'next/router';
7+
8+
const EventDetailPage = () => {
9+
const router = useRouter();
10+
const { id } = router.query;
11+
12+
const event = eventData.find((e) => e.id === Number(id));
13+
if (!event) return null;
14+
15+
const { name, address, description, startDate, endDate, imageSrc } = event;
16+
17+
return (
18+
<div
19+
className={cn(
20+
'relative w-full min-h-[100vh] overflow-auto',
21+
)}
22+
>
23+
<Header
24+
title='행사명'
25+
onClick={() => router.back()}
26+
className={cn('fixed top-0 left-0 right-0 z-50')}
27+
/>
28+
29+
<main
30+
className={cn(
31+
'flex flex-col items-center justify-start',
32+
'px-[2.4rem] pt-[calc(10rem+1.4rem)]'
33+
)}
34+
>
35+
{/* 행사 기간 */}
36+
<div className={cn('flex justify-center w-[18.4rem] mt-[1.3rem]')}>
37+
<DateTag startDate={startDate} endDate={endDate} />
38+
</div>
39+
40+
{/* 대표 이미지 */}
41+
<section
42+
className={cn(
43+
'relative w-full flex justify-center max-w-[35.4rem]',
44+
'mt-[1rem]'
45+
)}
46+
>
47+
{imageSrc ? (
48+
<Image
49+
src={imageSrc}
50+
alt={`${name} 이미지`}
51+
width={354}
52+
height={430}
53+
className={cn('w-full h-auto object-cover rounded-[2rem]')}
54+
/>
55+
) : (
56+
<div
57+
className={cn('w-full h-[43.6rem] bg-gray-200 rounded-[2rem]')}
58+
/>
59+
)}
60+
</section>
61+
62+
{/* 행사 카드 */}
63+
<div
64+
className={cn(
65+
'flex flex-col items-center w-full gap-[0.8rem]',
66+
'mt-[0.8rem]'
67+
)}
68+
>
69+
<EventCard
70+
name={name}
71+
address={address}
72+
description={description}
73+
variant='gray'
74+
size='large'
75+
/>
76+
77+
{/* 관련 행사 */}
78+
<div
79+
className={cn(
80+
'grid grid-cols-2 gap-[1.2rem] justify-items-center w-full max-w-[35.4rem]'
81+
)}
82+
>
83+
<div className={cn('w-[17rem]')}>
84+
<EventCard
85+
name='관련 행사'
86+
address=''
87+
description=''
88+
variant='gray'
89+
size='small'
90+
/>
91+
</div>
92+
<div className={cn('w-[17rem]')}>
93+
<EventCard
94+
name='관련 행사'
95+
address=''
96+
description=''
97+
variant='gray'
98+
size='small'
99+
/>
100+
</div>
101+
</div>
102+
</div>
103+
</main>
104+
</div>
105+
);
106+
};
107+
108+
export default EventDetailPage;

0 commit comments

Comments
 (0)