Skip to content

Commit 3342ffd

Browse files
committed
๐Ÿ’„ ๋””์ž์ธ ๊ฐœ์„ 
1 parent 73f1036 commit 3342ffd

19 files changed

Lines changed: 1080 additions & 333 deletions

File tree

โ€Žsrc/app/chatbot/[userId]/page.tsxโ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ export default function ChatPage() {
589589
if (!profile) return null
590590

591591
return (
592-
<div className='bg-[rgb(244,246,248)] w-full h-full'>
592+
<div className='bg-gray-50 w-full h-full'>
593593
<div className="flex max-h-full w-full">
594594
<AnimatePresence>
595595
{isSessionPanelOpen && (
@@ -631,7 +631,7 @@ export default function ChatPage() {
631631
</AnimatePresence>
632632

633633
<div className="flex flex-1 flex-col items-center overflow-hidden px-4 pb-8 pt-6 sm:px-8 md:px-10 lg:px-16">
634-
<div className='flex w-full max-w-5xl flex-wrap items-center justify-between gap-2 rounded-2xl bg-[rgb(244,246,248)] pb-4 pt-2'>
634+
<div className='flex w-full max-w-5xl flex-wrap items-center justify-between gap-2 rounded-2xl bg-gray-50 pb-4 pt-2'>
635635
<div className="flex items-center gap-2">
636636
<button
637637
type="button"

โ€Žsrc/app/login/page.tsxโ€Ž

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { useState, useEffect } from 'react'
4+
import { motion } from 'framer-motion'
45
import { useAuthStore, selectLogin, selectIsLogin } from '@/store/AuthStore'
56
import { useRouter } from 'next/navigation'
67
import { Button } from '@/components/Common/Button'
@@ -19,6 +20,17 @@ const LockIcon = () => (
1920
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
2021
</svg>
2122
);
23+
const EyeIcon = () => (
24+
<svg className="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
25+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
26+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
27+
</svg>
28+
);
29+
const EyeOffIcon = () => (
30+
<svg className="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
31+
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
32+
</svg>
33+
);
2234

2335
// Bubblog ๋กœ๊ณ  ์•„์ด์ฝ˜ (์ƒ‰์ƒ์„ ๋ชจ๋…ธํ†ค ๊ทธ๋ผ๋ฐ์ด์…˜์œผ๋กœ ๋ณ€๊ฒฝ)
2436
const BubblogLogoIcon = () => (
@@ -40,6 +52,7 @@ export default function LoginPage() {
4052
const toast = useToast();
4153
const [form, setForm] = useState({ email: '', password: '' });
4254
const [isLoading, setIsLoading] = useState(false);
55+
const [showPassword, setShowPassword] = useState(false);
4356

4457
useEffect(() => {
4558
if (isAuthenticated) {
@@ -70,7 +83,12 @@ export default function LoginPage() {
7083
<BubbleBackgroundCursor />
7184
<BubbleBackground />
7285
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-4rem)] px-4 py-8">
73-
<div className="w-full max-w-lg z-50">
86+
<motion.div
87+
className="w-full max-w-lg z-50"
88+
initial={{ opacity: 0, y: 20 }}
89+
animate={{ opacity: 1, y: 0 }}
90+
transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}
91+
>
7492
<div className="bg-white rounded-2xl border-2 p-8 sm:p-10">
7593
<div className="text-center">
7694
<div className="flex items-center justify-center gap-3 mb-2">
@@ -107,13 +125,21 @@ export default function LoginPage() {
107125
<input
108126
id="password"
109127
name="password"
110-
type="password"
128+
type={showPassword ? 'text' : 'password'}
111129
value={form.password}
112130
onChange={onChange}
113131
required
114132
placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ"
115-
className="w-full pl-12 pr-4 py-3 rounded-lg border border-gray-200 bg-gray-50 focus:bg-white focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition text-lg"
133+
className="w-full pl-12 pr-12 py-3 rounded-lg border border-gray-200 bg-gray-50 focus:bg-white focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition text-lg"
116134
/>
135+
<button
136+
type="button"
137+
onClick={() => setShowPassword(!showPassword)}
138+
className="absolute inset-y-0 right-0 pr-3.5 flex items-center hover:text-gray-600 transition-colors"
139+
aria-label={showPassword ? '๋น„๋ฐ€๋ฒˆํ˜ธ ์ˆจ๊ธฐ๊ธฐ' : '๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ'}
140+
>
141+
{showPassword ? <EyeOffIcon /> : <EyeIcon />}
142+
</button>
117143
</div>
118144
</div>
119145

@@ -135,7 +161,7 @@ export default function LoginPage() {
135161
</a>
136162
</p>
137163
</div>
138-
</div>
164+
</motion.div>
139165
</div>
140166
</div>
141167
);
Lines changed: 159 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// components/SettingsPage.tsx
22
'use client'
33

4-
import React, { useState, useEffect, FormEvent } from 'react'
4+
import React, { useState, useEffect } from 'react'
5+
import { motion, AnimatePresence } from 'framer-motion'
56
import { useAuthStore, selectUserId, selectLogout } from '@/store/AuthStore'
67
import {
78
getUserProfile,
@@ -12,6 +13,10 @@ import {
1213
import { PersonaManager } from '@/components/Persona/PersonaManager'
1314
import ImageUploader from '@/components/Common/ImageUploader'
1415
import { Button } from '@/components/Common/Button'
16+
import { ProfilePreviewCard } from '@/components/Settings/ProfilePreviewCard'
17+
import { SettingsTabs, SettingsTab } from '@/components/Settings/SettingsTabs'
18+
import { DeleteAccountModal } from '@/components/Settings/DeleteAccountModal'
19+
import SettingsSkeleton from '@/components/Skeletons/SettingsSkeleton'
1520
import Image from 'next/image'
1621
import { useRouter } from 'next/navigation'
1722
import { useToast } from '@/contexts/ToastContext'
@@ -22,6 +27,9 @@ export default function SettingsPage() {
2227
const router = useRouter()
2328
const toast = useToast()
2429

30+
// ํƒญ ์ƒํƒœ
31+
const [activeTab, setActiveTab] = useState<SettingsTab>('profile')
32+
2533
// ํ”„๋กœํ•„ ์ƒํƒœ
2634
const [profile, setProfile] = useState<UserProfile | null>(null)
2735
const [nickname, setNickname] = useState('')
@@ -77,108 +85,164 @@ export default function SettingsPage() {
7785
}
7886
}
7987

80-
return (
81-
<div className="max-w-3xl mx-auto p-6 space-y-8">
82-
{/* ํ”„๋กœํ•„ ์„ค์ • ์„น์…˜ */}
83-
<section className="bg-white rounded-xl shadow-md p-6">
84-
<h2 className="text-2xl font-bold mb-4 border-b pb-2">
85-
ํ”„๋กœํ•„ ์„ค์ •
86-
</h2>
87-
88-
{loadingProfile ? (
89-
<p className="text-center text-gray-500">๋กœ๋”ฉ ์ค‘โ€ฆ</p>
90-
) : errorProfile ? (
91-
<p className="text-red-600">{errorProfile}</p>
92-
) : profile ? (
93-
<div className="space-y-6">
94-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
95-
{/* ๋‹‰๋„ค์ž„ ์ž…๋ ฅ */}
96-
<div>
97-
<label className="block mb-2 text-sm font-medium text-gray-700">
98-
๋‹‰๋„ค์ž„
99-
</label>
100-
<input
101-
type="text"
102-
value={nickname}
103-
onChange={e => setNickname(e.target.value)}
104-
className="
105-
w-full rounded-md border border-gray-200
106-
px-4 py-2 text-gray-800
107-
focus:outline-none focus:ring-2 focus:ring-blue-300
108-
"
109-
/>
88+
// ๋กœ๋”ฉ ์ƒํƒœ
89+
if (loadingProfile) {
90+
return <SettingsSkeleton />
91+
}
92+
93+
// ํƒญ ์ฝ˜ํ…์ธ  ๋ Œ๋”๋ง
94+
const renderTabContent = () => {
95+
switch (activeTab) {
96+
case 'profile':
97+
return (
98+
<motion.div
99+
key="profile"
100+
initial={{ opacity: 0, x: 20 }}
101+
animate={{ opacity: 1, x: 0 }}
102+
exit={{ opacity: 0, x: -20 }}
103+
transition={{ duration: 0.3 }}
104+
className="space-y-6"
105+
>
106+
{errorProfile ? (
107+
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
108+
<p className="text-red-600">{errorProfile}</p>
110109
</div>
110+
) : profile ? (
111+
<>
112+
{/* ๋‹‰๋„ค์ž„ ์ž…๋ ฅ */}
113+
<div>
114+
<label className="block mb-2 text-sm font-semibold text-gray-700">
115+
๋‹‰๋„ค์ž„
116+
</label>
117+
<input
118+
type="text"
119+
value={nickname}
120+
onChange={e => setNickname(e.target.value)}
121+
maxLength={20}
122+
className="
123+
w-full rounded-lg border border-gray-300
124+
px-4 py-3 text-gray-800
125+
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
126+
transition-all
127+
"
128+
placeholder="๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•˜์„ธ์š”"
129+
/>
130+
<p className="mt-1 text-xs text-gray-500">
131+
{nickname.length}/20์ž
132+
</p>
133+
</div>
111134

112-
{/* ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋กœ๋” */}
113-
<div className="flex flex-col items-start">
114-
<label className="block mb-2 text-sm font-medium text-gray-700">
115-
ํ”„๋กœํ•„ ์ด๋ฏธ์ง€
116-
</label>
117-
{profileImageUrl && (
118-
<Image
119-
src={profileImageUrl}
120-
alt="ํ”„๋กœํ•„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ"
121-
width={100}
122-
height={100}
123-
className="rounded-full object-cover border mb-2"
135+
{/* ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์—…๋กœ๋” */}
136+
<div>
137+
<label className="block mb-3 text-sm font-semibold text-gray-700">
138+
ํ”„๋กœํ•„ ์ด๋ฏธ์ง€
139+
</label>
140+
<ImageUploader
141+
folder="profile-images"
142+
onUploaded={url => setProfileImageUrl(url)}
124143
/>
125-
)}
126-
<ImageUploader
127-
folder="profile-images"
128-
onUploaded={url => setProfileImageUrl(url)}
144+
</div>
145+
146+
{/* ์ €์žฅ / ํƒˆํ‡ด ๋ฒ„ํŠผ */}
147+
<div className="flex flex-col sm:flex-row justify-between gap-4 pt-6 border-t">
148+
<Button
149+
variant="outline"
150+
className="text-red-600 hover:bg-red-50 border-red-200"
151+
onClick={() => setConfirmOpen(true)}
152+
>
153+
๊ณ„์ • ํƒˆํ‡ด
154+
</Button>
155+
<Button
156+
onClick={saveProfile}
157+
className="bg-blue-600 hover:bg-blue-700"
158+
>
159+
๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ
160+
</Button>
161+
</div>
162+
</>
163+
) : null}
164+
</motion.div>
165+
)
166+
167+
case 'persona':
168+
return (
169+
<motion.div
170+
key="persona"
171+
initial={{ opacity: 0, x: 20 }}
172+
animate={{ opacity: 1, x: 0 }}
173+
exit={{ opacity: 0, x: -20 }}
174+
transition={{ duration: 0.3 }}
175+
>
176+
{userId && <PersonaManager userId={userId} />}
177+
</motion.div>
178+
)
179+
180+
case 'security':
181+
return (
182+
<motion.div
183+
key="security"
184+
initial={{ opacity: 0, x: 20 }}
185+
animate={{ opacity: 1, x: 0 }}
186+
exit={{ opacity: 0, x: -20 }}
187+
transition={{ duration: 0.3 }}
188+
className="text-center py-16"
189+
>
190+
<p className="text-gray-500">๋ณด์•ˆ ์„ค์ •์€ ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.</p>
191+
</motion.div>
192+
)
193+
194+
default:
195+
return null
196+
}
197+
}
198+
199+
return (
200+
<motion.div
201+
initial={{ opacity: 0, y: 20 }}
202+
animate={{ opacity: 1, y: 0 }}
203+
transition={{ duration: 0.4 }}
204+
className="min-h-screen py-8"
205+
>
206+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
207+
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
208+
{/* ์‚ฌ์ด๋“œ๋ฐ” - ํ”„๋กœํ•„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */}
209+
<aside className="lg:col-span-4">
210+
<div className="lg:sticky lg:top-8">
211+
{profile && (
212+
<ProfilePreviewCard
213+
profile={profile}
214+
profileImageUrl={profileImageUrl}
129215
/>
130-
</div>
216+
)}
131217
</div>
218+
</aside>
132219

133-
{/* ์ €์žฅ / ํƒˆํ‡ด ๋ฒ„ํŠผ */}
134-
<div className="flex justify-end gap-4">
135-
<Button
136-
variant="outline"
137-
className="text-red-600 hover:bg-red-50"
138-
onClick={() => setConfirmOpen(true)}
139-
>
140-
ํƒˆํ‡ด
141-
</Button>
142-
<Button onClick={saveProfile}>์ €์žฅ</Button>
143-
</div>
144-
</div>
145-
) : null}
146-
</section>
147-
148-
{/* ํŽ˜๋ฅด์†Œ๋‚˜ ์„ค์ • ์„น์…˜ */}
149-
<section className="bg-white rounded-xl shadow-md p-6">
150-
<h2 className="text-2xl font-bold mb-4 border-b pb-2">
151-
ํŽ˜๋ฅด์†Œ๋‚˜ ์„ค์ •
152-
</h2>
153-
{userId && <PersonaManager userId={userId} />}
154-
</section>
220+
{/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  */}
221+
<main className="lg:col-span-8">
222+
<div className="bg-white rounded-2xl shadow-lg overflow-hidden">
223+
{/* ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ */}
224+
<SettingsTabs
225+
activeTab={activeTab}
226+
onTabChange={setActiveTab}
227+
/>
155228

156-
{/* ํƒˆํ‡ด ํ™•์ธ ๋ชจ๋‹ฌ */}
157-
{confirmOpen && (
158-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
159-
<div className="bg-white rounded-lg p-6 w-80 space-y-4">
160-
<h3 className="text-lg font-semibold">์ •๋ง ๊ณ„์ •์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?</h3>
161-
<p className="text-sm text-gray-600">
162-
์‚ญ์ œ๋œ ๊ณ„์ •์€ ๋ณต๊ตฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
163-
</p>
164-
<div className="flex justify-end gap-2">
165-
<Button
166-
variant="outline"
167-
className="text-gray-700 hover:bg-gray-100"
168-
onClick={() => setConfirmOpen(false)}
169-
>
170-
์ทจ์†Œ
171-
</Button>
172-
<Button
173-
className="bg-red-600 hover:bg-red-700"
174-
onClick={handleWithdraw}
175-
>
176-
์‚ญ์ œ
177-
</Button>
229+
{/* ํƒญ ์ฝ˜ํ…์ธ  */}
230+
<div className="p-6 sm:p-8">
231+
<AnimatePresence mode="wait">
232+
{renderTabContent()}
233+
</AnimatePresence>
234+
</div>
178235
</div>
179-
</div>
236+
</main>
180237
</div>
181-
)}
182-
</div>
238+
</div>
239+
240+
{/* ํƒˆํ‡ด ํ™•์ธ ๋ชจ๋‹ฌ */}
241+
<DeleteAccountModal
242+
isOpen={confirmOpen}
243+
onClose={() => setConfirmOpen(false)}
244+
onConfirm={handleWithdraw}
245+
/>
246+
</motion.div>
183247
)
184248
}

0 commit comments

Comments
ย (0)