From 6e9cbcd773232717a7c7feee7e75ed128f4bd744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Fri, 29 May 2026 14:19:17 +0900 Subject: [PATCH 01/22] feat(fe): impl login page layout --- apps/frontend/app/(client)/login/layout.tsx | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 apps/frontend/app/(client)/login/layout.tsx diff --git a/apps/frontend/app/(client)/login/layout.tsx b/apps/frontend/app/(client)/login/layout.tsx new file mode 100644 index 0000000000..2c18c08f4b --- /dev/null +++ b/apps/frontend/app/(client)/login/layout.tsx @@ -0,0 +1,45 @@ +import { SignUpHeader } from '@/components/auth/SignUpPage/SignUpHeader' +import codedangLogoWhite from '@/public/logos/codedang-with-text-white.svg' +import Image from 'next/image' +import { HeaderTitleProvider } from '../(main)/_contexts/HeaderTitleContext' + +export default function LogInLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + +
+ +
+
+
+
+
+ Codedang +

+ 온라인 코딩 교육 & 대회 플랫폼 +

+
+
+ +
+ {children} +
+
+
+
+
+ ) +} From e3d709b57f7c702773808895b4e5376c378b096e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Fri, 29 May 2026 14:19:52 +0900 Subject: [PATCH 02/22] feat(fe): impl login page --- apps/frontend/app/(client)/login/page.tsx | 124 ++++++++++++---------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/apps/frontend/app/(client)/login/page.tsx b/apps/frontend/app/(client)/login/page.tsx index e2d02694e8..a56d3b206e 100644 --- a/apps/frontend/app/(client)/login/page.tsx +++ b/apps/frontend/app/(client)/login/page.tsx @@ -1,61 +1,69 @@ 'use client' -import { AuthModal } from '@/components/auth/AuthModal' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle -} from '@/components/shadcn/dialog' -import { useSession } from '@/libs/hooks/useSession' -import { useAuthModalStore } from '@/stores/authModal' -import type { Route } from 'next' -import { useRouter, useSearchParams } from 'next/navigation' -import { useEffect } from 'react' - -export default function LoginPage() { - const { currentModal, hideModal, showSignIn } = useAuthModalStore( - (state) => state - ) - const session = useSession() - const router = useRouter() - const searchParams = useSearchParams() - - useEffect(() => { - showSignIn() - }, [showSignIn]) - - useEffect(() => { - if (session) { - const redirectUrl = searchParams.get('redirectUrl') - if (redirectUrl) { - router.push(redirectUrl as Route) - } else { - router.push('/') - } - } - }, [session, router, searchParams]) - - return ( - - { - e.preventDefault() - }} - onInteractOutside={(e) => { - e.preventDefault() - }} - onEscapeKeyDown={(e) => { - e.preventDefault() - }} - hideCloseButton={true} - className="!h-[620px] !w-[380px] rounded-[10px]" - > - - - - - - - ) +import { LogInPage } from '@/components/auth/LogInPage/LogInPage' + +export default function Page() { + return } + +// 'use client' + +// import { AuthModal } from '@/components/auth/AuthModal' +// import { +// Dialog, +// DialogContent, +// DialogHeader, +// DialogTitle +// } from '@/components/shadcn/dialog' +// import { useSession } from '@/libs/hooks/useSession' +// import { useAuthModalStore } from '@/stores/authModal' +// import type { Route } from 'next' +// import { useRouter, useSearchParams } from 'next/navigation' +// import { useEffect } from 'react' + +// export default function LoginPage() { +// const { currentModal, hideModal, showSignIn } = useAuthModalStore( +// (state) => state +// ) +// const session = useSession() +// const router = useRouter() +// const searchParams = useSearchParams() + +// useEffect(() => { +// showSignIn() +// }, [showSignIn]) + +// useEffect(() => { +// if (session) { +// const redirectUrl = searchParams.get('redirectUrl') +// if (redirectUrl) { +// router.push(redirectUrl as Route) +// } else { +// router.push('/') +// } +// } +// }, [session, router, searchParams]) + +// return ( +// +// { +// e.preventDefault() +// }} +// onInteractOutside={(e) => { +// e.preventDefault() +// }} +// onEscapeKeyDown={(e) => { +// e.preventDefault() +// }} +// hideCloseButton={true} +// className="!h-[620px] !w-[380px] rounded-[10px]" +// > +// +// +// +// +// +// +// ) +// } From ba526100b0b63a65b7ac66ca8d758350be5440ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Fri, 29 May 2026 14:34:44 +0900 Subject: [PATCH 03/22] feat(fe): add logo and text --- .../components/auth/LogInPage/LogInPage.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 apps/frontend/components/auth/LogInPage/LogInPage.tsx diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx new file mode 100644 index 0000000000..c2899402eb --- /dev/null +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -0,0 +1,21 @@ +'use client' + +import codedangLogo from '@/public/logos/codedang-editor.svg' +import Image from 'next/image' + +export function LogInPage() { + return ( +
+
+ codedang +

코드당에 어서오세요!

+
+
+ ) +} From f23fc5acc4527997ab5f390396678a3a43a2493a Mon Sep 17 00:00:00 2001 From: Dayeon Jeong Date: Sat, 30 May 2026 12:11:11 +0900 Subject: [PATCH 04/22] Update apps/frontend/components/auth/LogInPage/LogInPage.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- apps/frontend/components/auth/LogInPage/LogInPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index c2899402eb..8a52e30260 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -5,7 +5,7 @@ import Image from 'next/image' export function LogInPage() { return ( -
+
Date: Sat, 30 May 2026 12:11:28 +0900 Subject: [PATCH 05/22] Update apps/frontend/app/(client)/login/layout.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- apps/frontend/app/(client)/login/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/app/(client)/login/layout.tsx b/apps/frontend/app/(client)/login/layout.tsx index 2c18c08f4b..422545b7a8 100644 --- a/apps/frontend/app/(client)/login/layout.tsx +++ b/apps/frontend/app/(client)/login/layout.tsx @@ -34,7 +34,7 @@ export default function LogInLayout({
-
+
{children}
From 88c70546d78285e766a6af5749d80cd91a12511c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sat, 30 May 2026 13:42:58 +0900 Subject: [PATCH 06/22] feat(fe): imple id and pw field --- .../components/auth/LogInPage/LogInPage.tsx | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index 8a52e30260..d29953e518 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -1,11 +1,62 @@ 'use client' +import { Input } from '@/components/shadcn/input' import codedangLogo from '@/public/logos/codedang-editor.svg' +import type { Route } from 'next' +import { signIn } from 'next-auth/react' import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import type { SubmitHandler } from 'react-hook-form' +import { FaEyeSlash } from 'react-icons/fa' +import { toast } from 'sonner' + +interface SignInInput { + nickname: string + password: string +} export function LogInPage() { + const [isSignInDisabled, setIsSignInDisabled] = useState(false) + const [isPasswordVisible, setIsPasswordVisible] = useState(false) + const router = useRouter() + const posthog = usePostHog() + const { register, handleSubmit } = useForm() + + const onSubmit: SubmitHandler = async (data) => { + setIsSignInDisabled(true) + try { + const res = await signIn('credentials', { + nickname: data.nickname, + password: data.password, + redirect: false + }) + if (!res?.error) { + posthog.identify(data.nickname) + router.push('/') + router.refresh() + + toast.success(`Welcome back, ${data.nickname}!`, { + style: { + transform: 'translateY(30px)' + } + }) + } else { + toast.error('Failed to log in') + } + } catch (error) { + console.error('Error during login:', error) + toast.error('An unexpected error occurred') + } finally { + setIsSignInDisabled(false) + } + } + return ( -
+

코드당에 어서오세요!

+ +
+
+ +
+ + +
+
+ +
+ + + + +
+
) } From 8631289224575807ff381a01e2319a90d8fd12b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sat, 30 May 2026 14:45:40 +0900 Subject: [PATCH 07/22] feat(fe): social icon images --- apps/frontend/public/icons/github-symbol.svg | 4 ++++ apps/frontend/public/icons/google-symbol.svg | 7 +++++++ apps/frontend/public/icons/kakaotalk-symbol.svg | 16 ++++++++++++++++ apps/frontend/public/icons/naver-symbol.svg | 15 +++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 apps/frontend/public/icons/github-symbol.svg create mode 100644 apps/frontend/public/icons/google-symbol.svg create mode 100644 apps/frontend/public/icons/kakaotalk-symbol.svg create mode 100644 apps/frontend/public/icons/naver-symbol.svg diff --git a/apps/frontend/public/icons/github-symbol.svg b/apps/frontend/public/icons/github-symbol.svg new file mode 100644 index 0000000000..5262419416 --- /dev/null +++ b/apps/frontend/public/icons/github-symbol.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/frontend/public/icons/google-symbol.svg b/apps/frontend/public/icons/google-symbol.svg new file mode 100644 index 0000000000..5e5701a929 --- /dev/null +++ b/apps/frontend/public/icons/google-symbol.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/frontend/public/icons/kakaotalk-symbol.svg b/apps/frontend/public/icons/kakaotalk-symbol.svg new file mode 100644 index 0000000000..2abaa09e4e --- /dev/null +++ b/apps/frontend/public/icons/kakaotalk-symbol.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/icons/naver-symbol.svg b/apps/frontend/public/icons/naver-symbol.svg new file mode 100644 index 0000000000..85097f7371 --- /dev/null +++ b/apps/frontend/public/icons/naver-symbol.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From 658839f92e7f3c86cdca928303352489950a616f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sat, 30 May 2026 14:46:10 +0900 Subject: [PATCH 08/22] feat(fe): impl login section --- .../components/auth/LogInPage/LogInPage.tsx | 81 ++++++++++++++++++- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index d29953e518..59e1909c6e 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -103,24 +103,99 @@ export function LogInPage() {
-
+
+
+
+ {/* 아직 페이지 없음 */} + {/* + 비밀번호를 잊으셨나요? + */} + + 비밀번호를 잊으셨나요? + +
+ +
+ +
+

+ SNS 계정으로 간편하게 시작하기 +

+ +
+ {/* Google */} + + + {/* Kakao */} + + + {/* GitHub */} + + + {/* Naver */} + +
+
) } From 399ab2334a0d6c64f39dfdd0a80a835ae06b1a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sat, 30 May 2026 14:56:04 +0900 Subject: [PATCH 09/22] fix(fe): fix id field autocomplete --- apps/frontend/components/auth/LogInPage/LogInPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index 59e1909c6e..e41b0d6117 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -71,6 +71,7 @@ export function LogInPage() {
From cc0799e59c68f216297f50231b3495a9e46e8e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sat, 30 May 2026 16:21:09 +0900 Subject: [PATCH 10/22] feat(fe): add login page layout --- apps/frontend/app/(client)/login/layout.tsx | 6 +++--- apps/frontend/components/auth/LogInPage/LogInPage.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/frontend/app/(client)/login/layout.tsx b/apps/frontend/app/(client)/login/layout.tsx index 422545b7a8..de3d159285 100644 --- a/apps/frontend/app/(client)/login/layout.tsx +++ b/apps/frontend/app/(client)/login/layout.tsx @@ -18,8 +18,8 @@ export default function LogInLayout({ style={{ backgroundImage: "url('/signup/background.png')" }} />
-
-
+
+
Codedang
-
+
{children}
diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index e41b0d6117..4db05a4fed 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -56,7 +56,7 @@ export function LogInPage() { } return ( -
+
Date: Sat, 30 May 2026 16:23:43 +0900 Subject: [PATCH 11/22] feat(fe): add signup page layout --- apps/frontend/app/(client)/signup/layout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/app/(client)/signup/layout.tsx b/apps/frontend/app/(client)/signup/layout.tsx index 6845043930..63b05a88bb 100644 --- a/apps/frontend/app/(client)/signup/layout.tsx +++ b/apps/frontend/app/(client)/signup/layout.tsx @@ -18,8 +18,8 @@ export default function SignUpLayout({ style={{ backgroundImage: "url('/signup/background.png')" }} />
-
-
+
+
Codedang
-
+
{children}
From 5147446e74c0caa751458c00d86cda1666a9c6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Mon, 1 Jun 2026 19:34:29 +0900 Subject: [PATCH 12/22] feat(fe): integrate account recovery into login page --- .../components/auth/RecoverAccount/FindUserId.tsx | 6 +++--- .../components/auth/RecoverAccount/RecoverAccount.tsx | 11 ++++++++--- .../auth/RecoverAccount/ResetPasswordEmailVerify.tsx | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/frontend/components/auth/RecoverAccount/FindUserId.tsx b/apps/frontend/components/auth/RecoverAccount/FindUserId.tsx index 8eae95a02f..2710827c52 100644 --- a/apps/frontend/components/auth/RecoverAccount/FindUserId.tsx +++ b/apps/frontend/components/auth/RecoverAccount/FindUserId.tsx @@ -95,7 +95,7 @@ export function FindUserId() { className="flex w-full flex-col gap-6 px-2" >
-

+

Find User ID

-
+ {/*
-
+
*/} ) } diff --git a/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx b/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx index 01fccbc2b4..59c2a3f916 100644 --- a/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx +++ b/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx @@ -9,14 +9,19 @@ import { FindUserId } from './FindUserId' import { ResetPassword } from './ResetPassword' import { ResetPasswordEmailVerify } from './ResetPasswordEmailVerify' -export function RecoverAccount() { +interface RecoverAccountProps { + onBackToSignIn: () => void +} + +export function RecoverAccount({ onBackToSignIn }: RecoverAccountProps) { const { showSignIn } = useAuthModalStore((state) => state) const { modalPage, backModal } = useRecoverAccountModalStore((state) => state) return ( -
+
+

+ 코드당에 어서오세요! +

-
-
- +
+ +
+ + +
+
- +
+ +
+ +
- -
- -
- {/* 아직 페이지 없음 */} - {/* - 비밀번호를 잊으셨나요? - */} - - 비밀번호를 잊으셨나요? - -
+ + + 회원가입 + +
-
+
-
-

- SNS 계정으로 간편하게 시작하기 -

+
+

SNS 계정으로 시작하기

-
- {/* Google */} - +
+ {/* Google */} + - {/* Kakao */} - + {/* Kakao */} + - {/* GitHub */} - + {/* GitHub */} + - {/* Naver */} -
- Naver - +
+ + ) : ( +
+ setAuthView('login')} />
-
+ )}
) } From 3ae80d5342909e66790502a5818a97e3e6c9e9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Mon, 1 Jun 2026 20:55:19 +0900 Subject: [PATCH 14/22] feat(fe): add reusable social login info modal --- .../components/auth/LogInPage/InfoModal.tsx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 apps/frontend/components/auth/LogInPage/InfoModal.tsx diff --git a/apps/frontend/components/auth/LogInPage/InfoModal.tsx b/apps/frontend/components/auth/LogInPage/InfoModal.tsx new file mode 100644 index 0000000000..1661e11bb0 --- /dev/null +++ b/apps/frontend/components/auth/LogInPage/InfoModal.tsx @@ -0,0 +1,88 @@ +'use client' + +import { Button } from '@/components/shadcn/button' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/shadcn/dialog' +import { cn } from '@/libs/utils' +import infoIcon from '@/public/icons/icon-info-blue.svg' +import Image from 'next/image' + +interface InfoModalButton { + text: string + onClick: () => void +} + +interface InfoModalProps { + open: boolean + onOpenChange: (open: boolean) => void + title: string + description?: string + primaryButton: InfoModalButton + secondaryButton?: InfoModalButton + className?: string +} + +export function InfoModal({ + open, + onOpenChange, + title, + description, + primaryButton, + secondaryButton, + className +}: InfoModalProps) { + return ( + + event.preventDefault()} + onEscapeKeyDown={(event) => event.preventDefault()} + onOpenAutoFocus={(event) => event.preventDefault()} + > + + info + +
+ + {title} + + + {description && ( +

+ {description} +

+ )} +
+
+ + + {secondaryButton && ( + + )} + + + +
+
+ ) +} From 73ba1cb028811c70302466d41f9c80be4e0d3d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Mon, 1 Jun 2026 20:56:08 +0900 Subject: [PATCH 15/22] feat(fe): add social unlinked modal to login page --- .../components/auth/LogInPage/LogInPage.tsx | 314 ++++++++++-------- 1 file changed, 171 insertions(+), 143 deletions(-) diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index 581ccc2dee..a244e8dccb 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -1,12 +1,13 @@ 'use client' +import { InfoModal } from '@/components/auth/LogInPage/InfoModal' import { Input } from '@/components/shadcn/input' import codedangLogo from '@/public/logos/codedang-editor.svg' import type { Route } from 'next' import { signIn } from 'next-auth/react' import Image from 'next/image' import Link from 'next/link' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { useState } from 'react' import { useForm } from 'react-hook-form' @@ -26,6 +27,9 @@ export function LogInPage() { const [isPasswordVisible, setIsPasswordVisible] = useState(false) const router = useRouter() const posthog = usePostHog() + const searchParams = useSearchParams() + const isSocialUnlinkedModalOpen = + searchParams.get('modal') === 'social-unlinked' const { register, handleSubmit } = useForm() const handleKakaoLogin = () => { @@ -62,169 +66,193 @@ export function LogInPage() { } return ( -
- {authView === 'login' ? ( - <> -
- codedang -

- 코드당에 어서오세요! -

-
- -
-
- +
+ {authView === 'login' ? ( + <> +
+ codedang -
+

+ 코드당에 어서오세요! +

+
+ + +
+
+ + +
+
+ +
-
- -
+ +
+ + + 회원가입 +
- -
- - - - 회원가입 - -
-
+
-
-

SNS 계정으로 시작하기

+
+

+ SNS 계정으로 시작하기 +

-
- {/* Google */} - +
+ {/* Google */} + - {/* Kakao */} - + {/* Kakao */} + - {/* GitHub */} - + {/* GitHub */} + - {/* Naver */} - + {/* Naver */} + +
+ + ) : ( +
+ setAuthView('login')} />
- - ) : ( -
- setAuthView('login')} /> -
- )} -
+ )} +
+ { + if (!open) { + router.replace('/login' as Route) + } + }} + title={'SNS 정보를 확인했어요.\n코드당에 처음 오시네요!'} + description="기존 회원은 로그인 후 소셜 연동이 가능해요" + secondaryButton={{ + text: '로그인', + onClick: () => router.replace('/login' as Route) + }} + primaryButton={{ + text: '회원가입 이어서하기', + onClick: () => router.replace('/signup' as Route) + }} + /> + ) } From 3f33fd6394846863ad77cd502a20bc559ebfdbf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sun, 21 Jun 2026 17:59:57 +0900 Subject: [PATCH 16/22] fix(fe): fix info modal icon import path --- apps/frontend/components/auth/LogInPage/InfoModal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/frontend/components/auth/LogInPage/InfoModal.tsx b/apps/frontend/components/auth/LogInPage/InfoModal.tsx index 1661e11bb0..950f877957 100644 --- a/apps/frontend/components/auth/LogInPage/InfoModal.tsx +++ b/apps/frontend/components/auth/LogInPage/InfoModal.tsx @@ -9,7 +9,6 @@ import { DialogTitle } from '@/components/shadcn/dialog' import { cn } from '@/libs/utils' -import infoIcon from '@/public/icons/icon-info-blue.svg' import Image from 'next/image' interface InfoModalButton { @@ -48,7 +47,12 @@ export function InfoModal({ onOpenAutoFocus={(event) => event.preventDefault()} > - info + info
From 6c8562e9acbcaaeb24fc360dd4da3105bbf73c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sun, 21 Jun 2026 18:19:53 +0900 Subject: [PATCH 17/22] feat(fe): capture and store oauthToken from kakao signup redirect --- .../components/auth/LogInPage/LogInPage.tsx | 11 ++++++++++- apps/frontend/stores/socialAuth.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/stores/socialAuth.ts diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index a244e8dccb..79a2e1d584 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -3,13 +3,14 @@ import { InfoModal } from '@/components/auth/LogInPage/InfoModal' import { Input } from '@/components/shadcn/input' import codedangLogo from '@/public/logos/codedang-editor.svg' +import { useSocialAuthStore } from '@/stores/socialAuth' import type { Route } from 'next' import { signIn } from 'next-auth/react' import Image from 'next/image' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { usePostHog } from 'posthog-js/react' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import type { SubmitHandler } from 'react-hook-form' import { FaEyeSlash } from 'react-icons/fa' @@ -30,8 +31,16 @@ export function LogInPage() { const searchParams = useSearchParams() const isSocialUnlinkedModalOpen = searchParams.get('modal') === 'social-unlinked' + const setOauthToken = useSocialAuthStore((state) => state.setOauthToken) const { register, handleSubmit } = useForm() + useEffect(() => { + const oauthToken = searchParams.get('oauthToken') + if (oauthToken) { + setOauthToken(oauthToken) + } + }, [searchParams, setOauthToken]) + const handleKakaoLogin = () => { window.location.href = `${process.env.NEXT_PUBLIC_BASEURL}/auth/kakao` } diff --git a/apps/frontend/stores/socialAuth.ts b/apps/frontend/stores/socialAuth.ts new file mode 100644 index 0000000000..f464e96d23 --- /dev/null +++ b/apps/frontend/stores/socialAuth.ts @@ -0,0 +1,13 @@ +import { create } from 'zustand' + +interface SocialAuthStore { + oauthToken: string | null + setOauthToken: (oauthToken: string) => void + clearOauthToken: () => void +} + +export const useSocialAuthStore = create((set) => ({ + oauthToken: null, + setOauthToken: (oauthToken: string) => set({ oauthToken }), + clearOauthToken: () => set({ oauthToken: null }) +})) From f2e3eef9f5b939298e0e74fa161842f9149b8787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sun, 21 Jun 2026 18:20:45 +0900 Subject: [PATCH 18/22] feat(fe): establish nextauth session after kakao login redirect via reissue --- apps/frontend/app/(client)/layout.tsx | 2 + .../components/auth/SocialLoginHandler.tsx | 73 +++++++++++++++++++ apps/frontend/libs/auth/authOptions.ts | 12 +++ apps/frontend/libs/auth/authorizeSocial.ts | 35 +++++++++ 4 files changed, 122 insertions(+) create mode 100644 apps/frontend/components/auth/SocialLoginHandler.tsx create mode 100644 apps/frontend/libs/auth/authorizeSocial.ts diff --git a/apps/frontend/app/(client)/layout.tsx b/apps/frontend/app/(client)/layout.tsx index 98282049cc..a46faf9852 100644 --- a/apps/frontend/app/(client)/layout.tsx +++ b/apps/frontend/app/(client)/layout.tsx @@ -1,5 +1,6 @@ 'use client' +import { SocialLoginHandler } from '@/components/auth/SocialLoginHandler' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -18,6 +19,7 @@ export default function MainLayout({ return ( + {children} {process.env.NODE_ENV === 'development' && ( diff --git a/apps/frontend/components/auth/SocialLoginHandler.tsx b/apps/frontend/components/auth/SocialLoginHandler.tsx new file mode 100644 index 0000000000..a26d4aa8f8 --- /dev/null +++ b/apps/frontend/components/auth/SocialLoginHandler.tsx @@ -0,0 +1,73 @@ +'use client' + +import { getJWTExpire } from '@/libs/auth/getJWTExpire' +import { baseUrl } from '@/libs/constants' +import type { User } from '@/types/type' +import { signIn, useSession } from 'next-auth/react' +import { useRouter } from 'next/navigation' +import { useEffect, useRef } from 'react' + +/** + * Establishes a NextAuth session for users who just logged in via Kakao redirect. + * The BE sets the refresh_token cookie and redirects straight to '/', but that cookie + * is httpOnly and path-scoped to /auth/reissue, so NextAuth's session never gets created + * automatically. This calls /auth/reissue directly from the browser (same-origin in + * stage/prod, so the cookie is sent automatically) to mint a session. + */ +export function SocialLoginHandler() { + const { data: session, status } = useSession() + const router = useRouter() + const attempted = useRef(false) + + useEffect(() => { + if (status !== 'unauthenticated' || session || attempted.current) { + return + } + attempted.current = true + + const establishSocialSession = async () => { + try { + const reissueRes = await fetch(`${baseUrl}/auth/reissue`, { + credentials: 'include' + }) + if (!reissueRes.ok) { + return + } + + const accessToken = reissueRes.headers.get('authorization') + if (!accessToken) { + return + } + + const accessTokenExpires = getJWTExpire(accessToken) + + const userRes = await fetch(`${baseUrl}/user`, { + headers: { Authorization: accessToken } + }) + if (!userRes.ok) { + return + } + + const user: User = await userRes.json() + + const result = await signIn('social', { + username: user.username, + role: user.role, + accessToken, + accessTokenExpires: String(accessTokenExpires), + redirect: false + }) + + if (!result?.error) { + router.refresh() + } + } catch (error) { + console.error('Failed to establish social login session:', error) + } + } + + establishSocialSession() + }, [status, session, router]) + + return null +} diff --git a/apps/frontend/libs/auth/authOptions.ts b/apps/frontend/libs/auth/authOptions.ts index 2176399d74..85c4db4736 100644 --- a/apps/frontend/libs/auth/authOptions.ts +++ b/apps/frontend/libs/auth/authOptions.ts @@ -2,6 +2,7 @@ import type { NextAuthOptions, Session, User } from 'next-auth' import type { JWT } from 'next-auth/jwt' import CredentialsProvider from 'next-auth/providers/credentials' import { authorize } from './authorize' +import { authorizeSocial } from './authorizeSocial' export const authOptions: NextAuthOptions = { providers: [ @@ -12,6 +13,17 @@ export const authOptions: NextAuthOptions = { password: { label: 'password', type: 'password' } }, authorize + }), + CredentialsProvider({ + id: 'social', + name: 'Social', + credentials: { + username: { label: 'username', type: 'text' }, + role: { label: 'role', type: 'text' }, + accessToken: { label: 'accessToken', type: 'text' }, + accessTokenExpires: { label: 'accessTokenExpires', type: 'text' } + }, + authorize: authorizeSocial }) ], session: { diff --git a/apps/frontend/libs/auth/authorizeSocial.ts b/apps/frontend/libs/auth/authorizeSocial.ts new file mode 100644 index 0000000000..cabe187659 --- /dev/null +++ b/apps/frontend/libs/auth/authorizeSocial.ts @@ -0,0 +1,35 @@ +import type { User } from 'next-auth' + +interface SocialCredential { + username: string + role: string + accessToken: string + accessTokenExpires: string +} + +/** + * Build a NextAuth user from tokens already fetched client-side via /auth/reissue. + * Unlike the password-based authorize(), this never calls the backend itself — + * the browser must call /auth/reissue beforehand because the refresh_token cookie + * is httpOnly and scoped to that path, so only a direct browser request carries it. + * The literal refreshToken value can't be read back (httpOnly), so it's left empty. + */ +export const authorizeSocial = >( + credential?: C +): User | null => { + const { username, role, accessToken, accessTokenExpires } = (credential ?? + {}) as unknown as SocialCredential + + if (!username || !role || !accessToken || !accessTokenExpires) { + return null + } + + return { + username, + role, + accessToken, + accessTokenExpires: Number(accessTokenExpires), + refreshToken: '', + refreshTokenExpires: 0 + } as User +} From cca5da77deac3056bf4570e599f2f54e0ea5ee58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sun, 21 Jun 2026 20:26:21 +0900 Subject: [PATCH 19/22] feat(fe): implement social login handler formatting --- .../frontend/components/auth/SocialLoginHandler.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/apps/frontend/components/auth/SocialLoginHandler.tsx b/apps/frontend/components/auth/SocialLoginHandler.tsx index a26d4aa8f8..c124bf9717 100644 --- a/apps/frontend/components/auth/SocialLoginHandler.tsx +++ b/apps/frontend/components/auth/SocialLoginHandler.tsx @@ -7,13 +7,6 @@ import { signIn, useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import { useEffect, useRef } from 'react' -/** - * Establishes a NextAuth session for users who just logged in via Kakao redirect. - * The BE sets the refresh_token cookie and redirects straight to '/', but that cookie - * is httpOnly and path-scoped to /auth/reissue, so NextAuth's session never gets created - * automatically. This calls /auth/reissue directly from the browser (same-origin in - * stage/prod, so the cookie is sent automatically) to mint a session. - */ export function SocialLoginHandler() { const { data: session, status } = useSession() const router = useRouter() @@ -33,23 +26,18 @@ export function SocialLoginHandler() { if (!reissueRes.ok) { return } - const accessToken = reissueRes.headers.get('authorization') if (!accessToken) { return } - const accessTokenExpires = getJWTExpire(accessToken) - const userRes = await fetch(`${baseUrl}/user`, { headers: { Authorization: accessToken } }) if (!userRes.ok) { return } - const user: User = await userRes.json() - const result = await signIn('social', { username: user.username, role: user.role, @@ -57,7 +45,6 @@ export function SocialLoginHandler() { accessTokenExpires: String(accessTokenExpires), redirect: false }) - if (!result?.error) { router.refresh() } From 7e97b19b890e0390097209aa85119210ef22d65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sun, 21 Jun 2026 21:41:40 +0900 Subject: [PATCH 20/22] fix(fe): center logo vertically within login/signup background image --- apps/frontend/app/(client)/login/layout.tsx | 2 +- apps/frontend/app/(client)/signup/layout.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/(client)/login/layout.tsx b/apps/frontend/app/(client)/login/layout.tsx index de3d159285..2460751b89 100644 --- a/apps/frontend/app/(client)/login/layout.tsx +++ b/apps/frontend/app/(client)/login/layout.tsx @@ -19,7 +19,7 @@ export default function LogInLayout({ />
-
+
Codedang
-
+
Codedang Date: Sun, 21 Jun 2026 21:42:08 +0900 Subject: [PATCH 21/22] feat(fe): link social account after login and show social login promo modal --- .../components/auth/LogInPage/InfoModal.tsx | 56 ++++++++++++---- .../components/auth/LogInPage/LogInPage.tsx | 66 +++++++++++++++++-- .../libs/auth/socialLoginPromoDismissal.ts | 14 ++++ 3 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 apps/frontend/libs/auth/socialLoginPromoDismissal.ts diff --git a/apps/frontend/components/auth/LogInPage/InfoModal.tsx b/apps/frontend/components/auth/LogInPage/InfoModal.tsx index 950f877957..bf589710cc 100644 --- a/apps/frontend/components/auth/LogInPage/InfoModal.tsx +++ b/apps/frontend/components/auth/LogInPage/InfoModal.tsx @@ -1,6 +1,7 @@ 'use client' import { Button } from '@/components/shadcn/button' +import { Checkbox } from '@/components/shadcn/checkbox' import { Dialog, DialogContent, @@ -10,6 +11,7 @@ import { } from '@/components/shadcn/dialog' import { cn } from '@/libs/utils' import Image from 'next/image' +import { useState } from 'react' interface InfoModalButton { text: string @@ -24,6 +26,8 @@ interface InfoModalProps { primaryButton: InfoModalButton secondaryButton?: InfoModalButton className?: string + dismissForTodayLabel?: string + onDismissForToday?: () => void } export function InfoModal({ @@ -33,8 +37,18 @@ export function InfoModal({ description, primaryButton, secondaryButton, - className + className, + dismissForTodayLabel, + onDismissForToday }: InfoModalProps) { + const [hideToday, setHideToday] = useState(false) + + const withDismissCheck = (onClick: () => void) => () => { + if (hideToday) { + onDismissForToday?.() + } + onClick() + } return ( - - {secondaryButton && ( + +
+ {secondaryButton && ( + + )} + - )} +
- + {dismissForTodayLabel && onDismissForToday && ( + + )}
diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index 79a2e1d584..2afd69c5a1 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -2,6 +2,11 @@ import { InfoModal } from '@/components/auth/LogInPage/InfoModal' import { Input } from '@/components/shadcn/input' +import { + dismissSocialLoginPromoForToday, + isSocialLoginPromoDismissedToday +} from '@/libs/auth/socialLoginPromoDismissal' +import { safeFetcherWithAuth } from '@/libs/utils' import codedangLogo from '@/public/logos/codedang-editor.svg' import { useSocialAuthStore } from '@/stores/socialAuth' import type { Route } from 'next' @@ -26,12 +31,15 @@ export function LogInPage() { const [authView, setAuthView] = useState<'login' | 'recover'>('login') const [isSignInDisabled, setIsSignInDisabled] = useState(false) const [isPasswordVisible, setIsPasswordVisible] = useState(false) + const [isSocialPromoModalOpen, setIsSocialPromoModalOpen] = useState(false) const router = useRouter() const posthog = usePostHog() const searchParams = useSearchParams() const isSocialUnlinkedModalOpen = searchParams.get('modal') === 'social-unlinked' - const setOauthToken = useSocialAuthStore((state) => state.setOauthToken) + const { oauthToken, setOauthToken, clearOauthToken } = useSocialAuthStore( + (state) => state + ) const { register, handleSubmit } = useForm() useEffect(() => { @@ -45,24 +53,54 @@ export function LogInPage() { window.location.href = `${process.env.NEXT_PUBLIC_BASEURL}/auth/kakao` } + const handleSocialPromoLater = () => { + setIsSocialPromoModalOpen(false) + router.push('/') + router.refresh() + } + + const handleSocialPromoGoToSettings = () => { + setIsSocialPromoModalOpen(false) + router.push('/settings' as Route) // 추후 변경 + router.refresh() + } + const onSubmit: SubmitHandler = async (data) => { setIsSignInDisabled(true) try { const res = await signIn('credentials', { - nickname: data.nickname, + username: data.nickname, password: data.password, redirect: false }) if (!res?.error) { posthog.identify(data.nickname) - router.push('/') - router.refresh() + + let justLinkedSocial = false + if (oauthToken) { + try { + await safeFetcherWithAuth.post('auth/social-link', { + json: { oauthToken } + }) + clearOauthToken() + justLinkedSocial = true + } catch (error) { + console.error('Failed to link social account:', error) + } + } toast.success(`Welcome back, ${data.nickname}!`, { style: { transform: 'translateY(30px)' } }) + + if (!justLinkedSocial && !isSocialLoginPromoDismissedToday()) { + setIsSocialPromoModalOpen(true) + } else { + router.push('/') + router.refresh() + } } else { toast.error('Failed to log in') } @@ -262,6 +300,26 @@ export function LogInPage() { onClick: () => router.replace('/signup' as Route) }} /> + { + if (!open) { + handleSocialPromoLater() + } + }} + title="소셜 로그인이 도입되었어요!" + description="설정에서 SNS를 연동해서 편하게 로그인 할 수 있어요" + secondaryButton={{ + text: '나중에 하기', + onClick: handleSocialPromoLater + }} + primaryButton={{ + text: '로그인하러 가기', + onClick: handleSocialPromoGoToSettings + }} + dismissForTodayLabel="오늘 하루 동안 안보기" + onDismissForToday={dismissSocialLoginPromoForToday} + /> ) } diff --git a/apps/frontend/libs/auth/socialLoginPromoDismissal.ts b/apps/frontend/libs/auth/socialLoginPromoDismissal.ts new file mode 100644 index 0000000000..b7fbb82eb4 --- /dev/null +++ b/apps/frontend/libs/auth/socialLoginPromoDismissal.ts @@ -0,0 +1,14 @@ +const STORAGE_KEY = 'social-login-promo-dismissed-date' + +const getTodayString = () => new Date().toDateString() + +export const isSocialLoginPromoDismissedToday = () => { + if (typeof window === 'undefined') { + return false + } + return localStorage.getItem(STORAGE_KEY) === getTodayString() +} + +export const dismissSocialLoginPromoForToday = () => { + localStorage.setItem(STORAGE_KEY, getTodayString()) +} From 73166cc6a18154b455d324dc9e5c48d09adc359f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EB=8B=A4=EC=97=B0?= Date: Sun, 21 Jun 2026 22:21:37 +0900 Subject: [PATCH 22/22] refactor(fe): apply gemini review suggestions for login page --- apps/frontend/app/(client)/login/page.tsx | 69 +-------- .../components/auth/LogInPage/LogInPage.tsx | 10 +- .../auth/RecoverAccount/FindUserId.tsx | 131 ++++++++---------- .../auth/RecoverAccount/RecoverAccount.tsx | 2 - 4 files changed, 75 insertions(+), 137 deletions(-) diff --git a/apps/frontend/app/(client)/login/page.tsx b/apps/frontend/app/(client)/login/page.tsx index a56d3b206e..a4a1680d7b 100644 --- a/apps/frontend/app/(client)/login/page.tsx +++ b/apps/frontend/app/(client)/login/page.tsx @@ -1,69 +1,12 @@ 'use client' import { LogInPage } from '@/components/auth/LogInPage/LogInPage' +import { Suspense } from 'react' export default function Page() { - return + return ( + + + + ) } - -// 'use client' - -// import { AuthModal } from '@/components/auth/AuthModal' -// import { -// Dialog, -// DialogContent, -// DialogHeader, -// DialogTitle -// } from '@/components/shadcn/dialog' -// import { useSession } from '@/libs/hooks/useSession' -// import { useAuthModalStore } from '@/stores/authModal' -// import type { Route } from 'next' -// import { useRouter, useSearchParams } from 'next/navigation' -// import { useEffect } from 'react' - -// export default function LoginPage() { -// const { currentModal, hideModal, showSignIn } = useAuthModalStore( -// (state) => state -// ) -// const session = useSession() -// const router = useRouter() -// const searchParams = useSearchParams() - -// useEffect(() => { -// showSignIn() -// }, [showSignIn]) - -// useEffect(() => { -// if (session) { -// const redirectUrl = searchParams.get('redirectUrl') -// if (redirectUrl) { -// router.push(redirectUrl as Route) -// } else { -// router.push('/') -// } -// } -// }, [session, router, searchParams]) - -// return ( -// -// { -// e.preventDefault() -// }} -// onInteractOutside={(e) => { -// e.preventDefault() -// }} -// onEscapeKeyDown={(e) => { -// e.preventDefault() -// }} -// hideCloseButton={true} -// className="!h-[620px] !w-[380px] rounded-[10px]" -// > -// -// -// -// -// -// -// ) -// } diff --git a/apps/frontend/components/auth/LogInPage/LogInPage.tsx b/apps/frontend/components/auth/LogInPage/LogInPage.tsx index 2afd69c5a1..7122c907d0 100644 --- a/apps/frontend/components/auth/LogInPage/LogInPage.tsx +++ b/apps/frontend/components/auth/LogInPage/LogInPage.tsx @@ -6,6 +6,7 @@ import { dismissSocialLoginPromoForToday, isSocialLoginPromoDismissedToday } from '@/libs/auth/socialLoginPromoDismissal' +import { baseUrl } from '@/libs/constants' import { safeFetcherWithAuth } from '@/libs/utils' import codedangLogo from '@/public/logos/codedang-editor.svg' import { useSocialAuthStore } from '@/stores/socialAuth' @@ -50,7 +51,11 @@ export function LogInPage() { }, [searchParams, setOauthToken]) const handleKakaoLogin = () => { - window.location.href = `${process.env.NEXT_PUBLIC_BASEURL}/auth/kakao` + window.location.href = `${baseUrl}/auth/kakao` + } + + const handleComingSoonLogin = () => { + toast.info('준비 중인 기능입니다') } const handleSocialPromoLater = () => { @@ -204,6 +209,7 @@ export function LogInPage() { {/* Google */} - ) : ( - - )} +
+ {userId ? ( + ) : ( + -
- - {/*
+ )} -
*/} - +
+ ) } diff --git a/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx b/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx index 59c2a3f916..fd00eb50b4 100644 --- a/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx +++ b/apps/frontend/components/auth/RecoverAccount/RecoverAccount.tsx @@ -1,7 +1,6 @@ 'use client' import codedangLogo from '@/public/logos/codedang-with-text.svg' -import { useAuthModalStore } from '@/stores/authModal' import { useRecoverAccountModalStore } from '@/stores/recoverAccountModal' import Image from 'next/image' import { IoMdArrowBack } from 'react-icons/io' @@ -14,7 +13,6 @@ interface RecoverAccountProps { } export function RecoverAccount({ onBackToSignIn }: RecoverAccountProps) { - const { showSignIn } = useAuthModalStore((state) => state) const { modalPage, backModal } = useRecoverAccountModalStore((state) => state) return (