Skip to content

Commit 9f09a9c

Browse files
authored
✨ Add a guests page (#23)
### πŸ“ μž‘μ—… λ‚΄μš© - μ°Έμ—¬μž λͺ…단 νŽ˜μ΄μ§€λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. - μ°Έμ—¬μž λͺ…단 νŽ˜μ΄μ§€ UI와 λ™μΌν•΄μ§€κ²Œ 일정 상세 νŽ˜μ΄μ§€ cssλ₯Ό μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. - `RootLayout`의 <main>에 있던 `items-center justify-center` 속성을 μ œκ±°ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€λ₯Έ νŽ˜μ΄μ§€μ—μ„œ 쒌우 및 μƒν•˜ 넓이가 κ°•μ œλ‘œ μ΅œμ†Œ 넓이가 λ˜λŠ” λ¬Έμ œκ°€ μžˆμ–΄ μ‚­μ œν–ˆμŠ΅λ‹ˆλ‹€. - 둜그인 νŽ˜μ΄μ§€, νšŒμ›κ°€μž… νŽ˜μ΄μ§€, νšŒμ›κ°€μž… μž…λ ₯ νŽ˜μ΄μ§€λ₯Ό `flex-1 flex items-center justify-center` μ†μ„±μœΌλ‘œ κ°μŒŒμŠ΅λ‹ˆλ‹€. - `package.json`에 `yarn fix` λͺ…λ Ήμ–΄λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. μž…λ ₯ν•˜λ©΄ CI 였λ₯˜λ₯Ό safe ν•˜κ²Œ μˆ˜μ •ν•  수 μžˆλŠ” λΆ€λΆ„λ§Œ μˆ˜μ •ν•΄μ€λ‹ˆλ‹€. ### πŸ“Έ μŠ€ν¬λ¦°μƒ· (선택) <img width="1759" height="1391" alt="image" src="https://github.com/user-attachments/assets/49913ff6-48ca-44dc-994b-56dceca734d8" /> ### πŸš€ 리뷰 μš”κ΅¬μ‚¬ν•­ (선택) - κ°•μ œ μ·¨μ†Œλ₯Ό λˆŒλ €μ„ λ•Œ λͺ¨λ‹¬μ΄ λ‚˜μ˜€κ³ , μ·¨μ†Œν•˜λ©΄ ν† μŠ€νŠΈκ°€ ν‘œμ‹œλ˜λŠ” λ°©μ‹μœΌλ‘œ λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. λͺ¨λ‹¬μ„ λ“œλ‘­λ‹€μš΄μœΌλ‘œ λ°”κΎΈλŠ”κ²Œ μ’‹μ„κΉŒμš”? - μ €λ²ˆμ— 전체 배경을 ν•˜μ–€μƒ‰μœΌλ‘œ λ°”κΎΈλ‹€ λ³΄λ‹ˆ, 둜그인 νŽ˜μ΄μ§€ 등이 배경색과 메인 μ½˜ν…μΈ  λͺ¨λ‘ 배경이 ν•˜μ–€μƒ‰μž…λ‹ˆλ‹€. 색상을 μˆ˜μ •ν•˜λŠ”κ²Œ μ’‹μ„κΉŒμš”?
1 parent 118eff1 commit 9f09a9c

File tree

8 files changed

+574
-371
lines changed

8 files changed

+574
-371
lines changed

β€Žpackage.jsonβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"check:types": "tsc",
1111
"check:format": "biome check .",
1212
"check:unused": "knip",
13-
"check-all": "yarn check:types && yarn check:format && yarn check:unused"
13+
"check-all": "yarn check:types && yarn check:format && yarn check:unused",
14+
"fix": "yarn biome check --write ."
1415
},
1516
"dependencies": {
1617
"@radix-ui/react-alert-dialog": "^1.1.15",

β€Žsrc/layouts/RootLayout.tsxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function RootLayout() {
77
<div className="flex flex-col min-h-screen">
88
<Header />
99
<main
10-
className="flex w-full flex-1 flex-col items-center justify-center bg-white px-3 xs:px-0"
10+
className="flex w-full flex-1 flex-col bg-white px-3 xs:px-0"
1111
style={{ padding: '2rem' }}
1212
>
1313
<Outlet />

β€Žsrc/routes.tsβ€Ž

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createBrowserRouter } from 'react-router';
22
import RootLayout from './layouts/RootLayout';
33
import Event from './routes/Event';
4+
import Guests from './routes/Guests';
45
import Home from './routes/Home';
56
import Login from './routes/Login';
67
import RegisterChoice from './routes/RegisterChoice';
@@ -22,6 +23,9 @@ export const router = createBrowserRouter([
2223
{
2324
path: '/event/:id',
2425
Component: RootLayout,
25-
children: [{ index: true, Component: Event }],
26+
children: [
27+
{ index: true, Component: Event },
28+
{ path: 'guests', Component: Guests },
29+
],
2630
},
2731
]);

β€Žsrc/routes/Event.tsxβ€Ž

Lines changed: 64 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,14 @@ export default function Event() {
124124
}, [id]);
125125

126126
const handleDelete = () => {
127-
if (confirm('정말 이 일정을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) {
128-
// μ‚­μ œ API ν•„μš”
129-
console.info('Deleting event...');
130-
navigate('/');
131-
}
127+
// μ‚­μ œ API ν•„μš”
128+
console.info('Deleting event...');
129+
toast.error('일정이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
130+
navigate('/');
132131
};
133132

134133
const handleCopyLink = () => {
135134
navigator.clipboard.writeText(joinLink);
136-
// sonner μ‚¬μš©: μ•„μ£Ό κ°„κ²°ν•©λ‹ˆλ‹€.
137135
toast.success('링크가 λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€!', {
138136
description: 'μ°Έμ—¬μžμ—κ²Œ μ£Όμ†Œλ₯Ό κ³΅μœ ν•΄ λ³΄μ„Έμš”.',
139137
});
@@ -142,69 +140,70 @@ export default function Event() {
142140
if (!schedule) return null;
143141

144142
return (
145-
<div className="min-h-screen bg-white flex flex-col relative pb-20">
146-
{/* 1. 상단 λ„€λΉ„κ²Œμ΄μ…˜*/}
147-
<div className="max-w-screen-xl mx-auto w-full flex items-center justify-between px-6 py-8 sm:px-10">
148-
<Button
149-
variant="ghost"
150-
size="icon"
151-
onClick={() => navigate(-1)}
152-
className="rounded-full"
153-
>
154-
<IconChevronLeft />
155-
</Button>
156-
<h1 className="text-2xl sm:text-3xl font-bold flex-1 ml-6 truncate text-black">
157-
{schedule.title}
158-
</h1>
143+
<div className="min-h-screen relative pb-20">
144+
{/* 1. 상단 λ„€λΉ„κ²Œμ΄μ…˜ */}
145+
<div className="w-full flex justify-center">
146+
<div className="max-w-2xl min-w-[320px] w-[90%] flex items-center justify-between px-6 py-8">
147+
<Button
148+
variant="ghost"
149+
size="icon"
150+
onClick={() => navigate(-1)}
151+
className="rounded-full"
152+
>
153+
<IconChevronLeft />
154+
</Button>
155+
<h1 className="text-2xl sm:text-3xl font-bold flex-1 ml-4 truncate text-black">
156+
{schedule.title}
157+
</h1>
159158

160-
<DropdownMenu>
161-
<DropdownMenuTrigger asChild>
162-
<Button variant="ghost" size="icon" className="rounded-full">
163-
<IconMoreVertical />
164-
</Button>
165-
</DropdownMenuTrigger>
166-
<DropdownMenuContent className="w-40" align="end">
167-
<DropdownMenuItem
168-
onClick={() => navigate('edit')}
169-
className="cursor-pointer"
170-
>
171-
일정 μˆ˜μ •ν•˜κΈ°
172-
</DropdownMenuItem>
159+
<DropdownMenu>
160+
<DropdownMenuTrigger asChild>
161+
<Button variant="ghost" size="icon" className="rounded-full">
162+
<IconMoreVertical />
163+
</Button>
164+
</DropdownMenuTrigger>
165+
<DropdownMenuContent className="w-40" align="end">
166+
<DropdownMenuItem
167+
onClick={() => navigate('edit')}
168+
className="cursor-pointer"
169+
>
170+
일정 μˆ˜μ •ν•˜κΈ°
171+
</DropdownMenuItem>
173172

174-
{/* μ‚­μ œ λ²„νŠΌμ€ AlertDialog와 μ—°κ²° */}
175-
<AlertDialog>
176-
<AlertDialogTrigger asChild>
177-
<div className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus:bg-accent focus:text-accent-foreground text-red-600 font-medium">
178-
일정 μ‚­μ œν•˜κΈ°
179-
</div>
180-
</AlertDialogTrigger>
181-
<AlertDialogContent>
182-
<AlertDialogHeader>
183-
<AlertDialogTitle>
184-
정말 일정을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
185-
</AlertDialogTitle>
186-
<AlertDialogDescription>
187-
μ‚­μ œλœ 일정은 볡ꡬ할 수 μ—†μœΌλ©°, λͺ¨λ“  μ°Έμ—¬ 정보가 ν•¨κ»˜
188-
μ‚¬λΌμ§‘λ‹ˆλ‹€.
189-
</AlertDialogDescription>
190-
</AlertDialogHeader>
191-
<AlertDialogFooter>
192-
<AlertDialogCancel>μ·¨μ†Œ</AlertDialogCancel>
193-
<AlertDialogAction
194-
onClick={handleDelete}
195-
className="bg-red-600 hover:bg-red-700"
196-
>
197-
μ‚­μ œ
198-
</AlertDialogAction>
199-
</AlertDialogFooter>
200-
</AlertDialogContent>
201-
</AlertDialog>
202-
</DropdownMenuContent>
203-
</DropdownMenu>
173+
<AlertDialog>
174+
<AlertDialogTrigger asChild>
175+
<div className="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent text-red-600 font-medium">
176+
일정 μ‚­μ œν•˜κΈ°
177+
</div>
178+
</AlertDialogTrigger>
179+
<AlertDialogContent>
180+
<AlertDialogHeader>
181+
<AlertDialogTitle>
182+
정말 일정을 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
183+
</AlertDialogTitle>
184+
<AlertDialogDescription>
185+
μ‚­μ œλœ 일정은 볡ꡬ할 수 μ—†μœΌλ©°, λͺ¨λ“  μ°Έμ—¬ 정보가 ν•¨κ»˜
186+
μ‚¬λΌμ§‘λ‹ˆλ‹€.
187+
</AlertDialogDescription>
188+
</AlertDialogHeader>
189+
<AlertDialogFooter>
190+
<AlertDialogCancel>μ·¨μ†Œ</AlertDialogCancel>
191+
<AlertDialogAction
192+
onClick={handleDelete}
193+
className="bg-red-600 hover:bg-red-700"
194+
>
195+
μ‚­μ œ
196+
</AlertDialogAction>
197+
</AlertDialogFooter>
198+
</AlertDialogContent>
199+
</AlertDialog>
200+
</DropdownMenuContent>
201+
</DropdownMenu>
202+
</div>
204203
</div>
205204

206205
{/* 2. 메인 μ½˜ν…μΈ */}
207-
<div className="max-w-2xl mx-auto w-full px-6 flex flex-col items-start gap-10">
206+
<div className="max-w-2xl min-w-[320px] mx-auto w-[90%] px-6 flex flex-col items-start gap-10">
208207
{/* 일정 정보 (μ™Όμͺ½ μ •λ ¬) */}
209208
<div className="text-left space-y-3 w-full">
210209
<p className="text-lg sm:text-xl font-bold text-black">
@@ -272,7 +271,7 @@ export default function Event() {
272271
</h2>
273272
<Button
274273
variant="link"
275-
onClick={() => navigate('participants')}
274+
onClick={() => navigate('guests')}
276275
className="text-base font-bold text-black p-0 h-auto"
277276
>
278277
더보기

β€Žsrc/routes/Guests.tsxβ€Ž

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { useEffect, useState } from 'react';
2+
import { useNavigate, useParams } from 'react-router';
3+
import { toast } from 'sonner';
4+
5+
// shadcn UI μ»΄ν¬λ„ŒνŠΈ
6+
import {
7+
AlertDialog,
8+
AlertDialogAction,
9+
AlertDialogCancel,
10+
AlertDialogContent,
11+
AlertDialogDescription,
12+
AlertDialogFooter,
13+
AlertDialogHeader,
14+
AlertDialogTitle,
15+
AlertDialogTrigger,
16+
} from '@/components/ui/alert-dialog';
17+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
18+
import { Button } from '@/components/ui/button';
19+
20+
// SVG μ•„μ΄μ½˜ μ»΄ν¬λ„ŒνŠΈ
21+
const IconChevronLeft = () => (
22+
<svg
23+
width="24"
24+
height="24"
25+
viewBox="0 0 24 24"
26+
fill="none"
27+
stroke="currentColor"
28+
strokeWidth="2"
29+
strokeLinecap="round"
30+
strokeLinejoin="round"
31+
>
32+
<path d="m15 18-6-6 6-6" />
33+
</svg>
34+
);
35+
36+
interface GuestResponse {
37+
registration_id: number;
38+
name: string;
39+
email: string;
40+
profile_image: string | null;
41+
}
42+
43+
export default function Guests() {
44+
// API둜 뢈러올 registrations id
45+
// const { id } = useParams<{ id: string }>();
46+
const navigate = useNavigate();
47+
48+
const [guests, setGuests] = useState<GuestResponse[]>([]);
49+
50+
useEffect(() => {
51+
// 데이터 ν•˜λ“œμ½”λ”©
52+
const mockGuests: GuestResponse[] = [
53+
{
54+
registration_id: 1,
55+
name: '이쀀엽',
56+
email: 'jun411@snu.ac.kr',
57+
profile_image: 'https://github.com/shadcn.png',
58+
},
59+
{
60+
registration_id: 2,
61+
name: '이름2',
62+
email: '이메일@example.com',
63+
profile_image: null,
64+
},
65+
{
66+
registration_id: 3,
67+
name: '이름3',
68+
email: '이메일@example.com',
69+
profile_image: null,
70+
},
71+
{
72+
registration_id: 4,
73+
name: '이름4',
74+
email: '이메일@example.com',
75+
profile_image: null,
76+
},
77+
{
78+
registration_id: 5,
79+
name: '이름5',
80+
email: '이메일@example.com',
81+
profile_image: null,
82+
},
83+
{
84+
registration_id: 6,
85+
name: '이름6',
86+
email: '이메일@example.com',
87+
profile_image: null,
88+
},
89+
{
90+
registration_id: 7,
91+
name: '이름7',
92+
email: '이메일@example.com',
93+
profile_image: null,
94+
},
95+
{
96+
registration_id: 8,
97+
name: '이름8',
98+
email: '이메일@example.com',
99+
profile_image: null,
100+
},
101+
];
102+
103+
setGuests(mockGuests);
104+
}, []);
105+
106+
const handleCancelGuest = (regId: number, name: string) => {
107+
// μ‚­μ œ API ν•„μš”
108+
setGuests((prev) => prev.filter((g) => g.registration_id !== regId));
109+
toast.success(`${name} λ‹˜μ˜ μ°Έμ—¬κ°€ μ·¨μ†Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.`);
110+
};
111+
112+
return (
113+
<div className="min-h-screen relative pb-20">
114+
{/* 1. 상단 λ„€λΉ„κ²Œμ΄μ…˜ */}
115+
<div className="w-full flex justify-center">
116+
<div className="max-w-2xl min-w-[320px] w-[90%] flex items-center px-6 py-8">
117+
<Button
118+
variant="ghost"
119+
size="icon"
120+
onClick={() => navigate(-1)}
121+
className="rounded-full"
122+
>
123+
<IconChevronLeft />
124+
</Button>
125+
<h1 className="text-2xl sm:text-3xl font-bold ml-4 text-black">
126+
μ°Έμ—¬μž λͺ…단({guests.length})
127+
</h1>
128+
</div>
129+
</div>
130+
131+
{/* 2. μ°Έμ—¬μž 리슀트 */}
132+
<div className="max-w-2xl min-w-[320px] mx-auto w-[90%] px-6 flex flex-col gap-8 mt-4">
133+
{guests.map((guest) => (
134+
<div
135+
key={guest.registration_id}
136+
className="flex items-center justify-between w-full"
137+
>
138+
<div className="flex items-center gap-4">
139+
<Avatar className="w-16 h-16 border-none shadow-sm">
140+
<AvatarImage src={guest.profile_image || undefined} />
141+
<AvatarFallback className="bg-black text-white text-xs">
142+
{guest.name.slice(0, 2)}
143+
</AvatarFallback>
144+
</Avatar>
145+
<div className="flex flex-col">
146+
<span className="text-xl font-bold text-black">
147+
{guest.name}
148+
</span>
149+
<span className="text-gray-400 text-lg">{guest.email}</span>
150+
</div>
151+
</div>
152+
153+
{/* κ°•μ œμ·¨μ†Œ λ²„νŠΌ */}
154+
<AlertDialog>
155+
<AlertDialogTrigger asChild>
156+
<Button
157+
variant="secondary"
158+
className="bg-[#333333] hover:bg-black text-white rounded-lg px-4 py-6 text-base font-bold"
159+
>
160+
κ°•μ œμ·¨μ†Œ
161+
</Button>
162+
</AlertDialogTrigger>
163+
<AlertDialogContent>
164+
<AlertDialogHeader>
165+
<AlertDialogTitle>
166+
<strong>{guest.name}</strong> λ‹˜μ˜ 신청을 μ·¨μ†Œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?
167+
</AlertDialogTitle>
168+
<AlertDialogDescription>
169+
μ·¨μ†Œ ν›„ 원볡이 μ–΄λ ΅μŠ΅λ‹ˆλ‹€. μ·¨μ†Œ 메일이 μ°Έμ—¬μžμ—κ²Œ
170+
μ „μ†‘λ©λ‹ˆλ‹€.
171+
</AlertDialogDescription>
172+
</AlertDialogHeader>
173+
<AlertDialogFooter>
174+
<AlertDialogCancel>μ‹ μ²­ μœ μ§€ν•˜κΈ°</AlertDialogCancel>
175+
<AlertDialogAction
176+
onClick={() =>
177+
handleCancelGuest(guest.registration_id, guest.name)
178+
}
179+
className="bg-red-600 hover:bg-red-700"
180+
>
181+
μ·¨μ†Œν•˜κΈ°
182+
</AlertDialogAction>
183+
</AlertDialogFooter>
184+
</AlertDialogContent>
185+
</AlertDialog>
186+
</div>
187+
))}
188+
</div>
189+
</div>
190+
);
191+
}

0 commit comments

Comments
Β (0)