From 2c726a0f32d5202dab631b63624827dd9efa42dc Mon Sep 17 00:00:00 2001 From: Park Junyoung Date: Tue, 3 Mar 2026 22:13:41 +0900 Subject: [PATCH 01/39] =?UTF-8?q?=F0=9F=9A=80=20Deploy=20via=20GitHub=20Ac?= =?UTF-8?q?tions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yml | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/deploy-dev.yml diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..baa34c7 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,39 @@ +name: deploy-dev + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + deploy: + name: Deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20.11.1' + + - name: Build & Export + env: + VITE_API_BASE_URL: ${{ secrets.DEV_API_BASE_URL }} + VITE_GOOGLE_CLIENT_ID: ${{ secrets.DEV_GOOGLE_CLIENT_ID }} + VITE_REDIRECT_URI: ${{ secrets.DEV_REDIRECT_URI }} + run: | + yarn install + yarn build + + - name: Deploy to S3 and Invalidate Cloudfront + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ap-northeast-2 + run: | + aws s3 sync ./dist s3://waffle-team4-web-temp --delete + aws cloudfront create-invalidation --distribution-id E2HDLBJVBNM8HH --paths "/*" \ No newline at end of file From be9508d0ecdaf8ce5df0ab45161f90b1b36a8168 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sat, 7 Mar 2026 04:29:27 +0900 Subject: [PATCH 02/39] =?UTF-8?q?=F0=9F=91=BD=EF=B8=8F=20Add=20an=20API=20?= =?UTF-8?q?to=20cancel=20registrations=20(#115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ์‹ ์ฒญ ์ทจ์†Œ api๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. - ์‹ ์ฒญ ์ทจ์†Œ ๋กœ์ง์— ์—ฐ๊ฒฐํ•˜๊ณ , msw์˜ ์‹ ์ฒญ ์ทจ์†Œ๋„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ) ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ) - ์˜ค๋Š˜ ์ฐฌ๊ทœ๋‹˜์ด ๋ง์”€ํ•ด์ฃผ์‹  ๋‚ด์šฉ์ธ๋ฐ, ํ˜„์žฌ ๋ชจ์ง‘ ์ข…๋ฃŒ๋œ ์ผ์ •์—์„œ๋„ ์ฐธ์—ฌ ์ทจ์†Œ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๋ง‰๋Š”๋‹ค๊ณ  ํ•˜๋ฉด ์ถ”๊ฐ€์ ์ธ ์ˆ˜์ •์ด ํ•„์š”ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค... - ์ด PR๊ณผ ์—ฐ๊ด€ ์—†๋Š” ์ผ์ด๊ธด ํ•œ๋ฐ, dev ๋ธŒ๋ Œ์น˜๋Š” ๋ฆฌ๋ทฐ ์š”๊ตฌ ์—†์ด ๋ฐ”๋กœ ๋จธ์ง€๊ฐ€ ๋˜๋Š” ๊ฒƒ ๊ฐ™์•„ 'Settings-Rules-Rulesets'์—์„œ `main` ๋‚ด์šฉ ๋ณต์‚ฌํ•ด์„œ `dev`๋ฅผ ์ƒˆ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. --- src/api/registrations/registration.ts | 5 +++++ src/hooks/useEventDetail.ts | 5 ++--- src/mocks/handlers/event.ts | 6 ++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/api/registrations/registration.ts b/src/api/registrations/registration.ts index d9ecf29..a446ff7 100644 --- a/src/api/registrations/registration.ts +++ b/src/api/registrations/registration.ts @@ -23,3 +23,8 @@ export async function patchRegistration( const response = await apiClient.patch(`/registrations/${id}`, data); return response.data; } + +// ์‹ ์ฒญ ์ทจ์†Œ (DELETE /api/registrations/:id) +export async function deleteRegistration(id: string): Promise { + await apiClient.delete(`/registrations/${id}`); +} diff --git a/src/hooks/useEventDetail.ts b/src/hooks/useEventDetail.ts index f6efa0d..19890f8 100644 --- a/src/hooks/useEventDetail.ts +++ b/src/hooks/useEventDetail.ts @@ -1,6 +1,7 @@ import { deleteEvent, getEventDetail } from '@/api/events/event'; import { getGuests, joinEvent } from '@/api/events/registrations'; import { + deleteRegistration, getRegistrationDetail, patchRegistration, } from '@/api/registrations/registration'; @@ -136,9 +137,7 @@ export default function useEventDetail(id?: string) { const handleCancelEvent = async (registrationId: string) => { setLoading(true); try { - await patchRegistration(registrationId, { - status: 'CANCELED', - }); + await deleteRegistration(registrationId); if (id) { removeGuestRegistration(id); await handleFetchDetail(id); diff --git a/src/mocks/handlers/event.ts b/src/mocks/handlers/event.ts index 6452b13..2dfe05b 100644 --- a/src/mocks/handlers/event.ts +++ b/src/mocks/handlers/event.ts @@ -83,6 +83,12 @@ export const eventHandlers = [ }); }), + // ์‹ ์ฒญ ์ทจ์†Œ (DELETE /registrations/:id) + http.delete(path('/registrations/:id'), async () => { + await delay(200); + return new HttpResponse(null, { status: 200 }); + }), + // 5. ์ผ์ • ์ƒ์„ฑ (POST /events) http.post(path('/events'), async ({ request }) => { const body = (await request.json()) as CreateEventRequest; From 365e90e233215a6bde0182057fc339c8c2cc4b0a Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sat, 7 Mar 2026 21:15:09 +0900 Subject: [PATCH 03/39] =?UTF-8?q?=F0=9F=92=84=20Fix=20the=20event=20page?= =?UTF-8?q?=20&=20user=20avatar=20UI=20(#117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ ์ฐธ์—ฌ์ž ํŽ˜์ด์ง€์™€ ์ผ์ • ์ƒ์„ธ ํŽ˜์ด์ง€์˜ ์ฐธ์—ฌ์ž ๋ฏธ๋ฆฌ๋ณด๊ธฐ ui๊ฐ€ ๊นจ์ง€๋Š” ๋ฌธ์ œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. - ์•„๋ฐ”ํƒ€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๋””์ž์ธ์„ `UserAvatar` ์ปดํฌ๋„ŒํŠธ๋กœ ๋งŒ๋“ค๊ณ  ํ—ค๋”, ์ฐธ์—ฌ์ž ๋ฏธ๋ฆฌ๋ณด๊ธฐ, ์ฐธ์—ฌ์ž ํŽ˜์ด์ง€์— ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ) ์Šคํฌ๋ฆฐ์ƒท 2026-03-06 234933 ์Šคํฌ๋ฆฐ์ƒท 2026-03-06 234940 ์Šคํฌ๋ฆฐ์ƒท 2026-03-06 234953 ์Šคํฌ๋ฆฐ์ƒท 2026-03-06 234959 ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ) - ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๊ฐ€ ์—†์„ ๋•Œ ๋””์ž์ธ์€ ์ž„์˜๋กœ ๋งŒ๋“ค์–ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ํ˜น์‹œ ์–ด๋– ์‹ ๊ฐ€์š”..? - ํ—ค๋” ๋ถ€๋ถ„์˜ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์•„๋ฐ”ํƒ€๋„ ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ํฌ๊ธฐ๊ฐ€ ๋ณ€ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. - ํ—ค๋” ๋†’์ด๊ฐ€ ๋ณ€ํ•˜๋ฉด ๋‹ค๋ฅธ ํŽ˜์ด์ง€์—์„œ ๋ณธ๋ฌธ์„ ๊ฐ€๋ฆฌ๋Š” ๋“ฑ ์˜ค๋ฅ˜๊ฐ€ ๋‚ ๊นŒ ์‹ถ์–ด ๊ธฐ์กด ํฌ๊ธฐ์˜€๋˜ 64px๋กœ ๊ณ ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์‚ฌ์‹ค ํ—ค๋” ์•„๋ฐ”ํƒ€ ํฌ๊ธฐ๋Š” ๋ณ€ํ•˜์ง€ ์•Š์•„๋„ ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค. ์›น์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํฌ๊ธฐ๋ฅผ ๋ณด๋ฉด ํ—ค๋”๊ฐ€ ๋„ˆ๋ฌด ๊ฝ‰ ์ฐจ์žˆ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค๊ธฐ๋„ ํ•ด์„œ ๊ทธ๋ƒฅ ๋ชจ๋ฐ”์ผ ๋ฒ„์ „์˜ ํฌ๊ธฐ๋กœ ์•„๋ฐ”ํƒ€๋ฅผ ๊ณ ์ •ํ• ๊นŒ์š”? - ์—ฌ๋Ÿฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ์„œ๋กœ ์ข…์†์ ์ธ ๋‚ด์šฉ์ด ์—†์œผ๋ฉด, ์ž‘์—…๋ฌผ2 ๋ธŒ๋žœ์น˜๋ฅผ ์ž‘์—…๋ฌผ1 ๋ธŒ๋žœ์น˜์—์„œ ๋งŒ๋“œ๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ main(dev)๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋”ฐ๋กœ ๋งŒ๋“ค๋ผ๋Š” ์ œ๋ฏธ๋‚˜์ด์˜ ์กฐ์–ธ์„ ๋ฐ›์•„ ์ด๋ฒˆ ์ž‘์—…์€ `feat/api-registragion-delete`์ด ์•„๋‹Œ `dev`๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌ๋ทฐํ•˜๊ธฐ ๋” ํŽธํ•˜์‹œ๋ฉด ์•ž์œผ๋กœ ์ด๋Ÿฐ ์‹์œผ๋กœ ์ž‘์—…ํ• ๊ฒŒ์š”. --------- Co-authored-by: Park Junyoung --- src/components/GuestsPreview.tsx | 22 +++++++-------------- src/components/Header.tsx | 4 ++-- src/components/ProfileButton.tsx | 12 +++++++----- src/components/UserAvatar.tsx | 33 ++++++++++++++++++++++++++++++++ src/mocks/handlers/event.ts | 2 +- src/routes/Guests.tsx | 26 ++++++++++++------------- 6 files changed, 62 insertions(+), 37 deletions(-) create mode 100644 src/components/UserAvatar.tsx diff --git a/src/components/GuestsPreview.tsx b/src/components/GuestsPreview.tsx index 770e5fb..1d3e2e1 100644 --- a/src/components/GuestsPreview.tsx +++ b/src/components/GuestsPreview.tsx @@ -1,4 +1,4 @@ -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import UserAvatar from '@/components/UserAvatar'; import type { UserPreview } from '@/types/events'; import type { EventId } from '@/types/schemas'; import { ChevronRightIcon } from 'lucide-react'; @@ -36,27 +36,19 @@ export default function GuestsPreview({ {/* ์•„๋ฐ”ํƒ€ ๋ฐ ํ”Œ๋Ÿฌ์Šค ์•„์ด์ฝ˜ ๋ฐฐ์น˜ ์˜์—ญ */} -
+
{/* ์ตœ๋Œ€ 5๊ฐœ์˜ ์•„๋ฐ”ํƒ€ ๋…ธ์ถœ */} {guests.map((p) => ( - - - - {p.name?.slice(0, 2)} - - + name={p.name} + imageUrl={p.profileImage ?? undefined} + /> ))} {/* ์กฐ๊ฑด๋ถ€ ํ”Œ๋Ÿฌ์Šค(+) ์•„์ด์ฝ˜ (์ตœ๋Œ€ 6๋ฒˆ์งธ ์ž๋ฆฌ์— ๋ฐฐ์น˜) */} {showPlusIcon && ( -
+
+{extraCount} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index b1b787b..44e081f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -24,8 +24,8 @@ export default function Header() { }, [isLoggedIn, refreshUser]); return ( -
-
+
+
logo

๋ชจ์ด๋ฐ

diff --git a/src/components/ProfileButton.tsx b/src/components/ProfileButton.tsx index 29d9e1d..a22d100 100644 --- a/src/components/ProfileButton.tsx +++ b/src/components/ProfileButton.tsx @@ -1,4 +1,4 @@ -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import UserAvatar from '@/components/UserAvatar'; import { DropdownMenu, DropdownMenuContent, @@ -30,10 +30,12 @@ export default function ProfileButton({ diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx new file mode 100644 index 0000000..0616395 --- /dev/null +++ b/src/components/UserAvatar.tsx @@ -0,0 +1,33 @@ +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +interface UserAvatarProps { + name?: string | null; + imageUrl?: string | null; + className?: string; // ์ปจํ…Œ์ด๋„ˆ(Avatar)์šฉ ํด๋ž˜์Šค + fallbackClassName?: string; // ์ด๋‹ˆ์…œ(AvatarFallback)์šฉ ํด๋ž˜์Šค +} + +export default function UserAvatar({ + name, + imageUrl, + className = '', + fallbackClassName = '', +}: UserAvatarProps) { + return ( + + + + {name?.slice(0, 3)} + + + ); +} diff --git a/src/mocks/handlers/event.ts b/src/mocks/handlers/event.ts index 2dfe05b..c9707b8 100644 --- a/src/mocks/handlers/event.ts +++ b/src/mocks/handlers/event.ts @@ -60,7 +60,7 @@ export const eventHandlers = [ name: `์ฐธ์—ฌ์ž ${i + 1}`, email: i < 8 ? `user${i}@example.com` : null, status: i < 8 ? 'CONFIRMED' : 'WAITLISTED', - profileImage: 'https://github.com/shadcn.png', + profileImage: i != 1 ? 'https://github.com/shadcn.png' : undefined, createdAt: new Date().toISOString(), waitingNum: i >= 8 ? i - 7 : null, })), diff --git a/src/routes/Guests.tsx b/src/routes/Guests.tsx index 1a0cd8b..3f4fc22 100644 --- a/src/routes/Guests.tsx +++ b/src/routes/Guests.tsx @@ -6,6 +6,7 @@ import { useNavigate, useParams } from 'react-router'; import { toast } from 'sonner'; import useInfiniteGuests from '../hooks/useInfiniteGuests'; +import UserAvatar from '@/components/UserAvatar'; // shadcn UI ์ปดํฌ๋„ŒํŠธ import { AlertDialog, @@ -18,7 +19,6 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import type { GuestStatus } from '@/types/schemas'; import { ChevronLeftIcon, Loader2 } from 'lucide-react'; @@ -118,24 +118,22 @@ export default function Guests() { page.participants.map((guest) => (
-
- - - - {guest.name?.slice(0, 2)} - - -
- +
+ + +
+ {guest.name} {guest.email ? ( - {guest.email} + + {guest.email} + ) : null} {guest.status === 'WAITLISTED' && ( - + ๋Œ€๊ธฐ {guest.waitingNum}๋ฒˆ )} @@ -148,7 +146,7 @@ export default function Guests() { From 24e9456ec09f3b225fbe122ee4bc33c87550b191 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sun, 8 Mar 2026 22:56:07 +0900 Subject: [PATCH 04/39] =?UTF-8?q?=F0=9F=92=84=20No=20special=20characters?= =?UTF-8?q?=20/=20Modify=20event=20invitations=20/=20Use=20destructive=20c?= =?UTF-8?q?olour=20(#119)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ํšŒ์›๊ฐ€์ž… ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ์— ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ์š”๊ตฌํ•˜๋Š” ๋กœ์ง์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. - ์ผ์ • ์ƒ์„ธ ํŽ˜์ด์ง€์—์„œ ๋ชจ์ž„ ๋งํฌ ๋ณต์‚ฌ(ํ…์ŠคํŠธ์™€ ํ•จ๊ป˜ ๋ณต์‚ฌ)๋ฅผ ํ•  ๋•Œ ๋ณต์‚ฌ๋œ ๊ธ€์˜ ๋ฌธ๊ตฌ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.('[๋ชจ์ด๋ฐ' ์ œ๊ฑฐ, ์ฐธ์—ฌ ๋งํฌ๊ฐ€ ์ œ๋ชฉ ๋‹ค์Œ์— ๋‚˜์˜ค๊ฒŒ) - ํ—ค๋”-๋“œ๋กญ๋‹ค์šด์—์„œ '๋กœ๊ทธ์•„์›ƒ'์ด ํ…์ŠคํŠธ ์ƒ‰์ƒ์„ ๊ฒ€์€์ƒ‰์—์„œ ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ) ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ) --------- Co-authored-by: Park Junyoung --- src/components/EventDetailContent.tsx | 2 +- src/components/EventForm.tsx | 2 +- src/components/ProfileButton.tsx | 4 +++- src/routes/EventEdit.tsx | 2 +- src/routes/EventMain.tsx | 14 +++++++------- src/routes/ProfileEdit.tsx | 2 +- src/routes/SignUp.tsx | 10 ++-------- 7 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/components/EventDetailContent.tsx b/src/components/EventDetailContent.tsx index 7a1efff..18c1431 100644 --- a/src/components/EventDetailContent.tsx +++ b/src/components/EventDetailContent.tsx @@ -49,7 +49,7 @@ export function EventDetailContent({ view, event }: EventDetailContentProps) {
- + {getRemainingTime( view, event.registrationEndsAt, diff --git a/src/components/EventForm.tsx b/src/components/EventForm.tsx index 2c96f94..3053dba 100644 --- a/src/components/EventForm.tsx +++ b/src/components/EventForm.tsx @@ -170,7 +170,7 @@ export function EventForm({ await handleFormSubmit(data); }; - const errorTextStyle = 'mt-1 text-xs text-red-500 font-medium'; + const errorTextStyle = 'mt-1 text-xs text-destructive font-medium'; return (
diff --git a/src/components/ProfileButton.tsx b/src/components/ProfileButton.tsx index a22d100..6008e91 100644 --- a/src/components/ProfileButton.tsx +++ b/src/components/ProfileButton.tsx @@ -47,7 +47,9 @@ export default function ProfileButton({ ํ”„๋กœํ•„ ์ˆ˜์ • - ๋กœ๊ทธ์•„์›ƒ + + ๋กœ๊ทธ์•„์›ƒ + diff --git a/src/routes/EventEdit.tsx b/src/routes/EventEdit.tsx index 63444d7..19ccc3c 100644 --- a/src/routes/EventEdit.tsx +++ b/src/routes/EventEdit.tsx @@ -37,7 +37,7 @@ export default function EventEdit() { if (isDeleted || !id) { return (
-
+
diff --git a/src/routes/EventMain.tsx b/src/routes/EventMain.tsx index f1791de..517f0fe 100644 --- a/src/routes/EventMain.tsx +++ b/src/routes/EventMain.tsx @@ -75,7 +75,7 @@ export default function EventMain() { if (isDeleted || !id) { return (
-
+
@@ -363,16 +363,16 @@ function ActionButton({ let textToCopy = joinLink; if (copyDetail) { - textToCopy = `[๋ชจ์ด๋ฐ] ${event.title} + textToCopy = `${event.title} + +๐Ÿ”— ์ฐธ์—ฌ ๋งํฌ: +${joinLink} ๐Ÿ“… ์ผ์‹œ: ${event.startsAt ? formatEventDate(event.startsAt) : '๋ฏธ์ •'} ${event.endsAt ? `- ${formatEventDate(event.endsAt)}` : ''} ๐Ÿ“ ์žฅ์†Œ: ${event.location || '๋ฏธ์ •'} ๐Ÿ“ ์ƒ์„ธ ๋‚ด์šฉ: -${event.description} - -๐Ÿ”— ์ฐธ์—ฌ ๋งํฌ: -${joinLink}`; +${event.description}`; } navigator.clipboard.writeText(textToCopy); @@ -460,7 +460,7 @@ ${joinLink}`; diff --git a/src/routes/ProfileEdit.tsx b/src/routes/ProfileEdit.tsx index bc5e12a..a6442ca 100644 --- a/src/routes/ProfileEdit.tsx +++ b/src/routes/ProfileEdit.tsx @@ -119,7 +119,7 @@ export default function SignUp() { } }; - const errorTextStyle = 'mt-1 text-xs text-red-500 font-medium'; + const errorTextStyle = 'mt-1 text-xs text-destructive font-medium'; return (
diff --git a/src/routes/SignUp.tsx b/src/routes/SignUp.tsx index e501295..12a46b7 100644 --- a/src/routes/SignUp.tsx +++ b/src/routes/SignUp.tsx @@ -66,7 +66,6 @@ export default function SignUp() { password: { isLongEnough: password.length >= 8, hasNumber: /[0-9]/.test(password), - hasSpecial: /[!@#$%^&*()]/.test(password), }, isPasswordMatch: password === confirmPassword && confirmPassword.length > 0, @@ -116,7 +115,7 @@ export default function SignUp() { }); }; - const errorTextStyle = 'mt-1 text-xs text-red-500 font-medium'; + const errorTextStyle = 'mt-1 text-xs text-destructive font-medium'; if (isSent) { return ; @@ -217,7 +216,7 @@ export default function SignUp() { }`} value={password} onChange={(e) => setPassword(e.target.value)} - placeholder="8์ž ์ด์ƒ, ์ˆซ์ž, ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ" + placeholder="8์ž ์ด์ƒ, ์ˆซ์ž ํฌํ•จ" /> {showErrors && !password && ( @@ -235,11 +234,6 @@ export default function SignUp() { > {validations.password.hasNumber ? 'โœ“' : 'โ—‹'} ์ˆซ์ž ํฌํ•จ -
  • - {validations.password.hasSpecial ? 'โœ“' : 'โ—‹'} ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ -
  • )}
    From 701b173902685317b941068756a3585808e02347 Mon Sep 17 00:00:00 2001 From: PARK Junyoung Date: Sun, 8 Mar 2026 23:51:40 +0900 Subject: [PATCH 05/39] =?UTF-8?q?=F0=9F=9A=A9=20No=20special=20characters?= =?UTF-8?q?=20in=20`ProfileEdit`=20(#121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ํ”„๋กœํ•„ ์ˆ˜์ • ํŽ˜์ด์ง€์— ๋‚จ์•„ ์žˆ๋˜ ๋น„๋ฐ€๋ฒˆํ˜ธ ํŠน์ˆ˜๋ฌธ์ž ์š”๊ฑด์„ ์‚ญ์ œํ•˜์˜€์Šต๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท ์—†์Œ ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ ์—†์Œ --- src/routes/ProfileEdit.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/routes/ProfileEdit.tsx b/src/routes/ProfileEdit.tsx index a6442ca..a884034 100644 --- a/src/routes/ProfileEdit.tsx +++ b/src/routes/ProfileEdit.tsx @@ -64,12 +64,9 @@ export default function SignUp() { password: { isLongEnough: password.length >= 8, hasNumber: /[0-9]/.test(password), - hasSpecial: /[!@#$%^&*()]/.test(password), isValid: password.length === 0 || - (password.length >= 8 && - /[0-9]/.test(password) && - /[!@#$%^&*()]/.test(password)), + (password.length >= 8 && /[0-9]/.test(password)), }, isPasswordMatch: password === confirmPassword, }; @@ -204,11 +201,6 @@ export default function SignUp() { > {validations.password.hasNumber ? 'โœ“' : 'โ—‹'} ์ˆซ์ž ํฌํ•จ -
  • - {validations.password.hasSpecial ? 'โœ“' : 'โ—‹'} ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ -
  • )}
    From 2be5430b100004fe50c62f46909640845975a335 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Mon, 9 Mar 2026 01:58:02 +0900 Subject: [PATCH 06/39] =?UTF-8?q?=F0=9F=9A=A9=20Remove=20image=20upload=20?= =?UTF-8?q?feature=20(#122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - `SignUp`๊ณผ `ProfileEdit`์—์„œ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€์™€ ๊ด€๋ จ๋œ ๋กœ์ง์„ ๋ชจ๋‘ ์ฃผ์„์ฒ˜๋ฆฌ ํ–ˆ์Šต๋‹ˆ๋‹ค. - unused variable ci ์˜ค๋ฅ˜ ๋•Œ๋ฌธ์— `api/images/images.ts`์™€ `types/images.ts` ํŒŒ์ผ ์ „์ฒด๋ฅผ ์ฃผ์„์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ) ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ) --------- Co-authored-by: Park Junyoung --- src/routes/ProfileEdit.tsx | 132 +++++++++++++++---------------- src/routes/SignUp.tsx | 157 +++++++++++++++++-------------------- 2 files changed, 138 insertions(+), 151 deletions(-) diff --git a/src/routes/ProfileEdit.tsx b/src/routes/ProfileEdit.tsx index a884034..6b34858 100644 --- a/src/routes/ProfileEdit.tsx +++ b/src/routes/ProfileEdit.tsx @@ -1,10 +1,10 @@ -import { uploadImage } from '@/api/images/images'; +// import { uploadImage } from '@/api/images/images'; import { patchMe } from '@/api/users/me'; import { Button } from '@/components/ui/button'; import useAuth from '@/hooks/useAuth'; import useAuthStore from '@/hooks/useAuthStore'; import { Loader2 } from 'lucide-react'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import { toast } from 'sonner'; @@ -17,9 +17,9 @@ export default function SignUp() { const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - const [previewUrl, setPreviewUrl] = useState(null); - const [imageKey, setImageKey] = useState(null); - const [isUploading, setIsUploading] = useState(false); + // const [previewUrl, setPreviewUrl] = useState(null); + // const [imageKey, setImageKey] = useState(null); + // const [isUploading, setIsUploading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [showErrors, setShowErrors] = useState(false); @@ -28,35 +28,35 @@ export default function SignUp() { const passwordRef = useRef(null); const confirmPasswordRef = useRef(null); - // ์‚ฌ์ง„์ด ์„ ํƒ๋  ๋•Œ๋งˆ๋‹ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL ์ƒ์„ฑ - const handlePhotoChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; + // // ์‚ฌ์ง„์ด ์„ ํƒ๋  ๋•Œ๋งˆ๋‹ค ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL ์ƒ์„ฑ + // const handlePhotoChange = async (e: React.ChangeEvent) => { + // const file = e.target.files?.[0]; + // if (!file) return; - // ๋กœ์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ - const objectUrl = URL.createObjectURL(file); - setPreviewUrl(objectUrl); + // // ๋กœ์ปฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ƒ์„ฑ + // const objectUrl = URL.createObjectURL(file); + // setPreviewUrl(objectUrl); - // ์„œ๋ฒ„์— ์ฆ‰์‹œ ์—…๋กœ๋“œํ•˜์—ฌ Key ํ™•๋ณด - setIsUploading(true); - try { - const res = await uploadImage({ image: file }, 'profile-image'); - setImageKey(res.key); // ๋‚˜์ค‘์— handleSignUp์— ๋ณด๋‚ผ ๊ฐ’ - } catch (error) { - console.error('Image upload failed:', error); - } finally { - setIsUploading(false); - } - }; + // // ์„œ๋ฒ„์— ์ฆ‰์‹œ ์—…๋กœ๋“œํ•˜์—ฌ Key ํ™•๋ณด + // setIsUploading(true); + // try { + // const res = await uploadImage({ image: file }, 'profile-image'); + // setImageKey(res.key); // ๋‚˜์ค‘์— handleSignUp์— ๋ณด๋‚ผ ๊ฐ’ + // } catch (error) { + // console.error('Image upload failed:', error); + // } finally { + // setIsUploading(false); + // } + // }; - // ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL ํ•ด์ œ - useEffect(() => { - return () => { - if (previewUrl && previewUrl.startsWith('blob:')) { - URL.revokeObjectURL(previewUrl); - } - }; - }, [previewUrl]); + // // ๋ฏธ๋ฆฌ๋ณด๊ธฐ URL ํ•ด์ œ + // useEffect(() => { + // return () => { + // if (previewUrl && previewUrl.startsWith('blob:')) { + // URL.revokeObjectURL(previewUrl); + // } + // }; + // }, [previewUrl]); const validations = useMemo(() => { return { @@ -76,10 +76,10 @@ export default function SignUp() { e.preventDefault(); setShowErrors(true); - if (isUploading) { - toast.error('์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”.'); - return; - } + // if (isUploading) { + // toast.error('์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”.'); + // return; + // } if (!validations.isNameValid) { nameRef.current?.focus(); @@ -100,7 +100,8 @@ export default function SignUp() { try { const updateData = { name, - profileImage: imageKey || undefined, + // profileImage: imageKey || undefined, + profileImage: undefined, ...(password && { password }), }; @@ -120,13 +121,14 @@ export default function SignUp() { return (
    -
    -

    - ํšŒ์› ์ •๋ณด ์ˆ˜์ • -

    +
    +
    +

    ํšŒ์› ์ •๋ณด ์ˆ˜์ •

    +

    ํšŒ์› ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ด ์ฃผ์„ธ์š”.

    +
    -
    -
    + + {/*
    {previewUrl ? ( -
    +
    */} -
    - +
    + setName(e.target.value)} placeholder="์ด๋ฆ„์„ ์ž…๋ ฅํ•˜์„ธ์š”" @@ -172,17 +172,15 @@ export default function SignUp() { )}
    -
    - +
    + 0 && !validations.password.isValid - ? 'border-red-400 focus:ring-red-100' - : 'border-gray-300 focus:ring-blue-500' + ? 'border-destructive focus:ring-destructive/10' + : 'border-border focus:ring-primary' }`} value={password} onChange={(e) => setPassword(e.target.value)} @@ -205,17 +203,15 @@ export default function SignUp() { )}
    -
    - +
    + 0 && !validations.isPasswordMatch - ? 'border-red-400 focus:ring-red-100' - : 'border-gray-300 focus:ring-blue-500' + ? 'border-destructive focus:ring-destructive/10' + : 'border-border focus:ring-primary' }`} value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} @@ -229,12 +225,12 @@ export default function SignUp() { )}
    -
    +
    From 9221adfd7ac162ad09238fcade7416c16dc95faf Mon Sep 17 00:00:00 2001 From: PARK Junyoung Date: Mon, 9 Mar 2026 14:00:12 +0900 Subject: [PATCH 07/39] =?UTF-8?q?=E2=9C=A8=20Add=20a=20404=20error=20page?= =?UTF-8?q?=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” URL๋กœ ์ ‘๊ทผํ–ˆ์„ ๋•Œ ๋„์šธ 404 Not Found ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2026-03-09 02 44 18 ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ - ํŽ˜์ด์ง€ ๋””์ž์ธ์€ ์ œ๊ฐ€ ๋งˆ์Œ๋Œ€๋กœ ํ•ด๋ดค๋Š”๋ฐ, ์ˆ˜์ •ํ•˜์…”๋„ ์ƒ๊ด€์—†์Šต๋‹ˆ๋‹ค. --- src/routes.ts | 6 +++ src/routes/NotFound.tsx | 88 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/routes/NotFound.tsx diff --git a/src/routes.ts b/src/routes.ts index c34f2b4..3d8fe05 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -9,6 +9,7 @@ import Home from '@/routes/Home'; import Login from '@/routes/Login'; import LoginEmail from '@/routes/LoginEmail'; import NewEvent from '@/routes/NewEvent'; +import NotFound from '@/routes/NotFound'; import ProfileEdit from '@/routes/ProfileEdit'; import SignUp from '@/routes/SignUp'; import SocialCallback from '@/routes/SocialCallback'; @@ -51,6 +52,11 @@ export const router = createBrowserRouter([ Component: ProfileEdit, handle: { title: 'ํ”„๋กœํ•„ ์ˆ˜์ • - ๋ชจ์ด๋ฐ' }, }, + { + path: '*', + Component: NotFound, + handle: { title: '๋ชจ์ด๋ฐ' }, + }, ], }, { diff --git a/src/routes/NotFound.tsx b/src/routes/NotFound.tsx new file mode 100644 index 0000000..5aae0f2 --- /dev/null +++ b/src/routes/NotFound.tsx @@ -0,0 +1,88 @@ +import { Button } from '@/components/ui/button'; +import { motion } from 'framer-motion'; +import { Link } from 'react-router'; + +export default function NotFound() { + return ( +
    +
    + +
    + + 404 + +
    +
    +
    +
    + +
    +

    ๊ธธ์„ ์žƒ์œผ์…จ๋‚˜์š”?

    +
    +
    +

    + ์š”์ฒญํ•˜์‹  ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. +
    + ์ž…๋ ฅํ•˜์‹  ์ฃผ์†Œ๋ฅผ ๋‹ค์‹œ ํ•œ๋ฒˆ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. +

    +
    +
    + +
    +
    +
    + //
    + // + + //
    + //

    ๊ธธ์„ ์žƒ์œผ์…จ๋‚˜์š”?

    + //

    + // ์š”์ฒญํ•˜์‹  ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + //
    + // ์ž…๋ ฅํ•˜์‹  ์ฃผ์†Œ๊ฐ€ ์ •ํ™•ํ•œ์ง€ ๋‹ค์‹œ ํ•œ๋ฒˆ ํ™•์ธํ•ด์ฃผ์„ธ์š”. + //

    + //
    + + // + // + // + // + + // {/* Background Decorative Elements */} + //
    + //
    + //
    + //
    + //
    + ); +} From 005ab2813164fe8eece04c671a0714a5b5210ad2 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Mon, 16 Mar 2026 03:02:31 +0900 Subject: [PATCH 08/39] =?UTF-8?q?=E2=9C=A8=20Add=20post-login=20redirectio?= =?UTF-8?q?n=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ๐Ÿ“ ์ž‘์—… ๋‚ด์šฉ - ๋กœ๊ทธ์ธ / ์†Œ์…œ ๋กœ๊ทธ์ธ / ํšŒ์›๊ฐ€์ž… ์‹œ ํ˜„์žฌ ํŽ˜์ด์ง€๋ฅผ `zustand`์— ์ €์žฅํ•˜๊ณ , ์„ฑ๊ณต ์‹œ ๋‹ค์‹œ ๊ธฐ์กด ํŽ˜์ด์ง€๋กœ ๋Œ์•„์˜ค๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. - ์ผ์ • ์‹ ์ฒญ ํŽ˜์ด์ง€(EventRegister)์—์„œ ๋กœ๊ทธ์ธ ๋“ฑ์„ ํ•˜๋ฉด, public id๋ฅผ ์ด์šฉํ•ด์„œ ์ผ์ • ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋ฉ๋‹ˆ๋‹ค. ### ๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ) ### ๐Ÿš€ ๋ฆฌ๋ทฐ ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ) - ์†Œ์…œ ๋กœ๊ทธ์ธ๊ณผ ํšŒ์›๊ฐ€์ž…์€ ๋กœ์ปฌ์—์„œ ํ™•์ธํ•ด๋ณด์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค... --- src/components/AuthBox.tsx | 12 +++++++++++- src/components/Header.tsx | 19 +++++++++++++++--- src/hooks/useAuth.ts | 37 ++++++++++++++++++++++++++++++++---- src/hooks/useAuthStore.ts | 13 +++++++++++++ src/routes/EventRegister.tsx | 20 +++++++++++++++---- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/components/AuthBox.tsx b/src/components/AuthBox.tsx index 3280d9b..6dc9fed 100644 --- a/src/components/AuthBox.tsx +++ b/src/components/AuthBox.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button'; import { GOOGLE_AUTH_URL } from '@/constants/auth'; +import useAuthStore from '@/hooks/useAuthStore'; import { Link } from 'react-router'; function Separator() { @@ -80,12 +81,21 @@ function ContinueWithGoogle() { type AuthMode = 'sign-up' | 'login'; export default function AuthBox({ mode }: { mode: AuthMode }) { + const setRedirectUrl = useAuthStore((state) => state.setRedirectUrl); return (
    ์†Œ์…œ ๊ณ„์ •์œผ๋กœ ๊ฐ„ํŽธ {mode === 'sign-up' ? 'ํšŒ์›๊ฐ€์ž…' : '๋กœ๊ทธ์ธ'} - + { + if (!useAuthStore.getState().redirectUrl) { + setRedirectUrl(location.pathname + location.search); + } + }} + >
    diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 44e081f..1121b01 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,11 +1,14 @@ import ProfileButton from '@/components/ProfileButton'; import { Button } from '@/components/ui/button'; import useAuth from '@/hooks/useAuth'; +import useAuthStore from '@/hooks/useAuthStore'; import { useEffect } from 'react'; import { Link } from 'react-router'; export default function Header() { - const { user, isLoggedIn, handleLogout, refreshUser } = useAuth(); + const { user, handleLogout, refreshUser } = useAuth(); + const isLoggedIn = useAuthStore((state) => state.isLoggedIn); + const setRedirectUrl = useAuthStore((state) => state.setRedirectUrl); useEffect(() => { // 1. ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ฉด ํƒ€์ด๋จธ๋ฅผ ๋Œ๋ฆด ํ•„์š”๊ฐ€ ์—†์Œ @@ -33,12 +36,22 @@ export default function Header() {
    {!isLoggedIn || !user ? ( <> - + + setRedirectUrl(location.pathname + location.search) + } + > - + + setRedirectUrl(location.pathname + location.search) + } + > diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 2d70d7a..640ce1f 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -20,6 +20,7 @@ export default function useAuth() { const login = useAuthStore((state) => state.login); const logout = useAuthStore((state) => state.logout); const updateUser = useAuthStore((state) => state.updateUser); + const setRedirectUrl = useAuthStore((state) => state.setRedirectUrl); // 5. ๋กœ๊ทธ์•„์›ƒ ๋กœ์ง (handleLogout์„ ๋จผ์ € ์ •์˜ํ•จ) const handleLogout = useCallback(async () => { @@ -38,12 +39,26 @@ export default function useAuth() { const { token } = await loginApi(data); const user = await getMeApi(token); login(user, token); // Zustand ์Šคํ† ์–ด ์—…๋ฐ์ดํŠธ - navigate('/'); // ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ + + const { redirectUrl, redirectTimestamp } = useAuthStore.getState(); + const ONE_HOUR = 60 * 60 * 1000; + + if ( + redirectUrl && + redirectTimestamp && + Date.now() - redirectTimestamp < ONE_HOUR + ) { + setRedirectUrl(null); + navigate(redirectUrl); + } else { + setRedirectUrl(null); + navigate('/'); // ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ + } } catch (error) { console.error('Login failed:', error); } }, - [login, navigate] + [login, navigate, setRedirectUrl] ); // 2. ์ด๋ฉ”์ผ ํšŒ์›๊ฐ€์ž… ๋กœ์ง @@ -69,12 +84,26 @@ export default function useAuth() { const { token } = await socialApi(data); const user = await getMeApi(token); login(user, token); - navigate('/'); + + const { redirectUrl, redirectTimestamp } = useAuthStore.getState(); + const ONE_HOUR = 60 * 60 * 1000; + + if ( + redirectUrl && + redirectTimestamp && + Date.now() - redirectTimestamp < ONE_HOUR + ) { + setRedirectUrl(null); + navigate(redirectUrl); + } else { + setRedirectUrl(null); + navigate('/'); + } } catch (error) { console.error('Social login failed:', error); } }, - [login, navigate] + [login, navigate, setRedirectUrl] ); // 4. ๋‚ด ์ •๋ณด ๋™๊ธฐํ™” diff --git a/src/hooks/useAuthStore.ts b/src/hooks/useAuthStore.ts index 85a2058..4c55606 100644 --- a/src/hooks/useAuthStore.ts +++ b/src/hooks/useAuthStore.ts @@ -18,6 +18,11 @@ interface AuthState { setGuestRegistration: (eventId: string, registrationId: string) => void; // ์‹ ์ฒญ ์ทจ์†Œ ์‹œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•˜๋Š” ์•ก์…˜ removeGuestRegistration: (eventId: string) => void; + // ์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฐ ํšŒ์›๊ฐ€์ž… ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ์œ„ํ•œ ์ƒํƒœ + redirectUrl: string | null; + redirectTimestamp: number | null; + // ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์•ก์…˜ + setRedirectUrl: (url: string | null) => void; } const useAuthStore = create()( @@ -27,6 +32,8 @@ const useAuthStore = create()( token: null, isLoggedIn: false, guestRegistrations: {}, + redirectUrl: null, + redirectTimestamp: null, login: (user, token) => set({ @@ -58,6 +65,12 @@ const useAuthStore = create()( delete newRegistrations[eventId]; return { guestRegistrations: newRegistrations }; }), + + setRedirectUrl: (url) => + set({ + redirectUrl: url, + redirectTimestamp: url ? Date.now() : null, + }), }), { name: 'auth-storage', // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ๋  ํ‚ค ์ด๋ฆ„ diff --git a/src/routes/EventRegister.tsx b/src/routes/EventRegister.tsx index c2967b7..75186e9 100644 --- a/src/routes/EventRegister.tsx +++ b/src/routes/EventRegister.tsx @@ -2,6 +2,7 @@ import LoadingSkeleton from '@/components/LoadingSkeleton'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import useAuthStore from '@/hooks/useAuthStore'; import type { JoinEventRequest } from '@/types/events'; import { ChevronLeftIcon } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -13,6 +14,7 @@ import useEventDetail from '../hooks/useEventDetail'; export default function EventRegister() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const setRedirectUrl = useAuthStore((state) => state.setRedirectUrl); const { loading, data, handleFetchDetail, handleJoinEvent } = useEventDetail(); @@ -41,7 +43,7 @@ export default function EventRegister() { const { event } = data; - const handleSubmit = async (e: React.FormEvent) => { + const onRegisterSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!id) return; @@ -96,7 +98,10 @@ export default function EventRegister() { {/* ์‹ ์ฒญ ํผ ์„น์…˜ */} - +
    @@ -144,7 +149,10 @@ export default function EventRegister() { type="button" variant="moiming" className="w-[75%] h-14 bg-blue-400 text-base" - onClick={() => navigate('/login')} + onClick={() => { + setRedirectUrl(`/event/${id}`); + navigate('/login'); + }} > ๋กœ๊ทธ์ธํ•˜๊ธฐ @@ -152,7 +160,10 @@ export default function EventRegister() { - - handleKeyDown(e)} - className="w-[36px] bg-transparent text-center focus:outline-none placeholder:text-muted-foreground/50 p-0" - placeholder="YYYY" - maxLength={4} - disabled={disabled} - /> - . - handleKeyDown(e, yearRef)} - className="w-[20px] bg-transparent text-center focus:outline-none placeholder:text-muted-foreground/50 p-0" - placeholder="MM" - maxLength={2} - disabled={disabled} - /> - . - handleKeyDown(e, monthRef)} - className="w-[20px] bg-transparent text-center focus:outline-none placeholder:text-muted-foreground/50 p-0" - placeholder="DD" - maxLength={2} - disabled={disabled} - /> - {dayOfWeek()} - - - - - - { - if (e.key === 'Backspace' && hour === '') { - e.preventDefault(); - ampmRef.current?.focus(); - } - }} - className="w-[20px] bg-transparent text-center focus:outline-none placeholder:text-muted-foreground/50 p-0" - placeholder="HH" - maxLength={2} - disabled={disabled} - /> - : - handleKeyDown(e, hourRef)} - className="w-[20px] bg-transparent text-center focus:outline-none placeholder:text-muted-foreground/50 p-0" - placeholder="MM" - maxLength={2} - disabled={disabled} - /> - -
    -
    - ); -} diff --git a/src/components/EventDetailContent.tsx b/src/components/EventDetailContent.tsx index 18c1431..da5aa8f 100644 --- a/src/components/EventDetailContent.tsx +++ b/src/components/EventDetailContent.tsx @@ -1,7 +1,7 @@ import type { DetailedEvent } from '@/types/events'; import type { EventViewType } from '@/types/events'; +import { formatEventDate, getRemainingTime } from '@/utils/date'; import { Calendar, Clock, MapPin, User } from 'lucide-react'; -import { formatEventDate, getRemainingTime } from '../utils/date'; interface EventDetailContentProps { view: EventViewType; @@ -14,57 +14,66 @@ interface ShortEventDetailContentProps { export function EventDetailContent({ view, event }: EventDetailContentProps) { return ( -
    -
    -
    -

    - {event.title} -

    -
    -

    -

    ์ผ์‹œ

    {' '} +
    +
    +

    {event.title}

    +
    +
    +
    + + ์ผ์‹œ +
    + {event.startsAt ? formatEventDate(event.startsAt) : '๋ฏธ์ •'}{' '} - {event.endsAt ? `- ${formatEventDate(event.endsAt)}` : ''} -

    -

    -

    ์žฅ์†Œ

    {event.location || '๋ฏธ์ •'} -

    -

    -

    ์ •์›

    {event.totalApplicants}/{event.capacity}๋ช… -

    + {event.endsAt ? `~ ${formatEventDate(event.endsAt)}` : ''} +
    -
    - -
    -
    - -
    - ์‹ ์ฒญ ๊ธฐ๊ฐ„: - - {event.registrationStartsAt - ? formatEventDate(event.registrationStartsAt) + ' - ' - : ''} - {formatEventDate(event.registrationEndsAt)} - +
    +
    + + ์žฅ์†Œ
    + {event.location || '๋ฏธ์ •'}
    -
    - - {getRemainingTime( - view, - event.registrationEndsAt, - event.registrationStartsAt - )} +
    +
    + + ์ •์› +
    + + {event.totalApplicants}/{event.capacity}๋ช…
    +
    -
    - {event.description} +
    +
    +
    + + ์‹ ์ฒญ ๊ธฐ๊ฐ„: +
    + + {event.registrationStartsAt + ? formatEventDate(event.registrationStartsAt) + ' ~ ' + : ''} + {formatEventDate(event.registrationEndsAt)} +
    +
    + + {getRemainingTime( + view, + event.registrationEndsAt, + event.registrationStartsAt + )} + +
    +
    -
    -
    +
    +

    {event.description}

    +
    ); } diff --git a/src/components/EventForm.tsx b/src/components/EventForm.tsx index 3053dba..ff56cd0 100644 --- a/src/components/EventForm.tsx +++ b/src/components/EventForm.tsx @@ -1,5 +1,6 @@ -import { ControlledDateTimePicker } from '@/components/ControlledDateTimePicker'; import { InputWithPlusMinusButtons } from '@/components/InputWithPlusMinusButtton'; +import SimpleDateTimePicker from '@/components/SimpleDateTimePicker'; +import Subheader from '@/components/Subheader'; import { AlertDialog, AlertDialogAction, @@ -24,7 +25,7 @@ import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { formatEventDate } from '@/utils/date'; import { zodResolver } from '@hookform/resolvers/zod'; -import { ChevronLeftIcon, Loader2 } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -170,48 +171,51 @@ export function EventForm({ await handleFormSubmit(data); }; - const errorTextStyle = 'mt-1 text-xs text-destructive font-medium'; + const errorTextStyle = 'mt-0.5 text-xs text-destructive font-medium'; return ( -
    +
    {/* Top navigation UI */} -
    -
    - -

    - {pageTitle} -

    -
    -
    + - {/* Stepper / Tabs */} -
    -
    +
    + {/* Stepper / Tabs */} +
    -
    -
    - + {/* Form */} + {/* STEP 1: Basic Info */} {step === 1 && ( - +
    {/* Name */} - - + + ๋ชจ์ž„ ์ด๋ฆ„ - * + * @@ -262,223 +281,166 @@ export function EventForm({ )} - {/* Start recruiting now Toggle */} - - - ์ง€๊ธˆ๋ถ€ํ„ฐ ๋ชจ์ง‘ํ•˜๊ธฐ - - ์ผ์ •์„ ๋งŒ๋“  ์ฆ‰์‹œ ์ฐธ๊ฐ€ ์‹ ์ฒญ์„ ์‹œ์ž‘ํ•ด์š”. - - + {/* Event Start */} + + + ๋ชจ์ž„ ์‹œ์ž‘์ผ์‹œ + ( - { - field.onChange(checked); - if (checked) { - setValue('regiStartDate', new Date()); + { + field.onChange(date); + if (date && isBounded) { + const newEnd = new Date( + date.getTime() + 60 * 60 * 1000 + ); + setValue('eventEndDate', newEnd); } }} + placeholder="์–ธ์ œ ๋ชจ์ด๋‚˜์š”?" /> )} /> + {errors.eventStartDate && ( +

    + {errors.eventStartDate.message} +

    + )} + {!errors.eventStartDate && errors.regiEndDate && ( +

    + {errors.regiEndDate.message} +

    + )}
    - {/* Registration Start */} - {!isFromNow && ( - -
    - - ์‹ ์ฒญ ์‹œ์ž‘ ์‹œ๊ฐ„ - * - - + + + ๋ชจ์ž„ ์ข…๋ฃŒ์ผ์‹œ + + ( + { + field.onChange(checked); + if (checked) { + const start = getValues('eventStartDate'); + const newEnd = start + ? new Date(start.getTime() + 60 * 60 * 1000) + : new Date(); + setValue('eventEndDate', newEnd); + } else { + setValue('eventEndDate', undefined); + } + }} + /> + )} + /> + + {isBounded && ( + <> + ( + + )} /> - {errors.regiStartDate && ( + {errors.eventEndDate && (

    - {errors.regiStartDate.message} + {errors.eventEndDate.message}

    )} -
    -
    - )} + + )} +
    - {/* Registration End */} - - - ์‹ ์ฒญ ๋งˆ๊ฐ ์‹œ๊ฐ„ - * - - + ๋ชจ์ž„ ์žฅ์†Œ + { - if (date) { - const newEventStart = new Date( - date.getTime() + 24 * 60 * 60 * 1000 - ); - setValue('eventStartDate', newEventStart); - - if (isBounded) { - setValue( - 'eventEndDate', - new Date(newEventStart.getTime() + 60 * 60 * 1000) - ); - } - } - }} + name="location" + render={({ field }) => ( + + )} /> - {errors.regiEndDate && ( -

    - {errors.regiEndDate.message} -

    - )}
    - {/* Capacity */} - - - - ๋ชจ์ž„ ์ •์› - * - - - ์‹ ์ฒญ ๋งˆ๊ฐ์ผ์‹œ ์ด์ „์— ์ •์› ์ดˆ๊ณผ ์‹œ, ๋Œ€๊ธฐ์ž๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ - ์ทจ์†Œ ์—ฌ์„์— ๋”ฐ๋ผ ์ฐธ์—ฌ์ž๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. - - {errors.capacity && ( -

    - {errors.capacity.message} -

    - )} -
    + {/* Description */} + + ์„ค๋ช… ( - )} /> - -
    - -
    )} {/* STEP 2: Event Description */} {step === 2 && ( - + {/* Summary Card */} -
    -

    - {getValues('title')} -

    -
    -

    - ์‹ ์ฒญ ๊ธฐ๊ฐ„: - {isFromNow - ? '์ง€๊ธˆ๋ถ€ํ„ฐ' - : formatEventDate( - getValues('regiStartDate').toString() - )}{' '} - - {formatEventDate(getValues('regiEndDate').toString())} -

    -

    - ์ผ์ • ์ •์›: - {getValues('capacity')}๋ช… -

    -
    +
    +

    {getValues('title')}

    +

    + ์ผ์‹œ: {formatEventDate(getValues('eventStartDate'))} + {isBounded && + ` ~ ${formatEventDate(getValues('eventEndDate'))}`} +
    + ์žฅ์†Œ: {getValues('location') || '๋ฏธ์ •'} +

    - - {/* Event Start */} - -
    - - ๋ชจ์ž„ ์‹œ์ž‘ ์‹œ๊ฐ„ - * - - { - if (date && isBounded) { - const newEnd = new Date( - date.getTime() + 60 * 60 * 1000 - ); - setValue('eventEndDate', newEnd); - } - }} - /> - {errors.eventStartDate && ( -

    - {errors.eventStartDate.message} -

    - )} - {!errors.eventStartDate && errors.regiEndDate && ( -

    - {errors.regiEndDate.message} -

    - )} -
    -
    - - {/* Event End Toggle */} + + {/* Start recruiting now Toggle */} - ํ•ด์–ด์ง€๋Š” ๋•Œ๋„ ์ž…๋ ฅํ•˜๊ธฐ + ์ง€๊ธˆ๋ถ€ํ„ฐ ๋ชจ์ง‘ํ•˜๊ธฐ - ๋ชจ์ž„์ด ์–ธ์ œ ๋๋‚˜๋Š”์ง€ ์•Œ๋ ค์ฃผ์„ธ์š”. + ์ผ์ •์„ ๋งŒ๋“  ์ฆ‰์‹œ ์ฐธ๊ฐ€ ์‹ ์ฒญ์„ ์‹œ์ž‘ํ•ด์š”. ( { field.onChange(checked); if (checked) { - const start = getValues('eventStartDate'); - const newEnd = start - ? new Date(start.getTime() + 60 * 60 * 1000) - : new Date(); - setValue('eventEndDate', newEnd); - } else { - setValue('eventEndDate', undefined); + setValue('regiStartDate', new Date()); } }} /> @@ -486,111 +448,105 @@ export function EventForm({ /> - {/* Event End Date Picker */} - {isBounded && ( - -
    - - ๋ชจ์ž„ ์ข…๋ฃŒ ์‹œ๊ฐ„ - * - - - {errors.eventEndDate && ( -

    - {errors.eventEndDate.message} -

    + {/* Registration Start */} + {!isFromNow && ( + + + ์‹ ์ฒญ ์‹œ์ž‘์ผ์‹œ + * + + ( + )} -
    + /> + {errors.regiStartDate && ( +

    + {errors.regiStartDate.message} +

    + )}
    )} - {/* Location */} - - ๋ชจ์ž„ ์žฅ์†Œ + {/* Registration End */} + + + ์‹ ์ฒญ ๋งˆ๊ฐ์ผ์‹œ + * + ( - { + field.onChange(date); + if (date) { + const newEventStart = new Date( + date.getTime() + 24 * 60 * 60 * 1000 + ); + setValue('eventStartDate', newEventStart); + + if (isBounded) { + setValue( + 'eventEndDate', + new Date( + newEventStart.getTime() + 60 * 60 * 1000 + ) + ); + } + } + }} + placeholder="์–ธ์ œ ๋งˆ๊ฐํ• ๊นŒ์š”?" /> )} /> + {errors.regiEndDate && ( +

    + {errors.regiEndDate.message} +

    + )}
    - {/* Description */} - - ์„ค๋ช… + {/* Capacity */} + + + + ๋ชจ์ž„ ์ •์› + * + + + ์‹ ์ฒญ ๋งˆ๊ฐ ์ด์ „์— ์ •์› ์ดˆ๊ณผ ์‹œ ๋Œ€๊ธฐ์ž๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉฐ, ์ทจ์†Œ + ์—ฌ์„์— ๋”ฐ๋ผ ์ฐธ์—ฌ์ž๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. + + ( -