From 20eecddbaef1975523c529fa5cfbe9454606e787 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Wed, 15 Jul 2026 11:03:04 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B5=9C=EA=B7=BC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=86=8C=EC=85=9C=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=A7=90=ED=92=8D=EC=84=A0=20=ED=91=9C=EC=8B=9C(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/icon/index.ts | 1 + src/assets/icon/polygon.svg | 3 +++ src/components/auth/RecentLoginDialog.tsx | 12 ++++++++++++ src/components/auth/index.ts | 1 + src/hooks/auth/login/useAppleLogin.ts | 3 +++ src/hooks/auth/login/useKakaoOAuth.ts | 3 +++ src/pages/auth/LoginPage.tsx | 15 ++++++++++++++- src/utils/auth/recentLoginProvider.ts | 21 +++++++++++++++++++++ 8 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/assets/icon/polygon.svg create mode 100644 src/components/auth/RecentLoginDialog.tsx create mode 100644 src/utils/auth/recentLoginProvider.ts diff --git a/src/assets/icon/index.ts b/src/assets/icon/index.ts index 316ef920..611197b4 100644 --- a/src/assets/icon/index.ts +++ b/src/assets/icon/index.ts @@ -54,6 +54,7 @@ export { default as RestoraionSparkleIcon } from "./restoration-sparkle.svg?reac export { default as TooltipXIcon } from "./tooltip-x.svg?react"; export { default as AiRestoreIcon } from "./ai-restore.svg?react"; export { default as FindersLogoFooterIcon } from "./finders-logo-footer.svg?react"; +export { default as PolygonIcon } from "./polygon.svg?react"; // 소셜 / 브랜드 export { default as AppleIcon } from "./apple.svg?react"; diff --git a/src/assets/icon/polygon.svg b/src/assets/icon/polygon.svg new file mode 100644 index 00000000..064f664c --- /dev/null +++ b/src/assets/icon/polygon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/auth/RecentLoginDialog.tsx b/src/components/auth/RecentLoginDialog.tsx new file mode 100644 index 00000000..5e2bcf5c --- /dev/null +++ b/src/components/auth/RecentLoginDialog.tsx @@ -0,0 +1,12 @@ +import { PolygonIcon } from "@/assets/icon"; + +export function RecentLoginDialog() { + return ( +
+
+ 최근에 로그인 했어요 +
+ +
+ ); +} diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts index 6c44ab9d..20e3807b 100644 --- a/src/components/auth/index.ts +++ b/src/components/auth/index.ts @@ -5,3 +5,4 @@ export { ActionButton } from "./ActionButton"; export { TermsAgreementItem } from "./TermsAgreementItem"; export { TermsContent } from "./TermsContent"; export { TermsAccordionRow } from "./TermsAccordionRow"; +export { RecentLoginDialog } from "./RecentLoginDialog"; diff --git a/src/hooks/auth/login/useAppleLogin.ts b/src/hooks/auth/login/useAppleLogin.ts index 280eeab9..0c3ca0a3 100644 --- a/src/hooks/auth/login/useAppleLogin.ts +++ b/src/hooks/auth/login/useAppleLogin.ts @@ -1,5 +1,6 @@ import { useState } from "react"; import { loginWithApple } from "@/utils/auth/appleSdk"; +import { setRecentLoginProvider } from "@/utils/auth/recentLoginProvider"; import { tokenStorage } from "@/utils/tokenStorage"; import { useOauth } from "./useOauth"; import { useAuthStore } from "@/store/useAuth.store"; @@ -25,6 +26,8 @@ export function useAppleLogin({ onSuccess: (res) => { const data = res.data; + setRecentLoginProvider("APPLE"); + if ("accessToken" in data) { tokenStorage.setTokens({ accessToken: data.accessToken, diff --git a/src/hooks/auth/login/useKakaoOAuth.ts b/src/hooks/auth/login/useKakaoOAuth.ts index 1cd93c28..d5def19c 100644 --- a/src/hooks/auth/login/useKakaoOAuth.ts +++ b/src/hooks/auth/login/useKakaoOAuth.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from "react"; import { useSearchParams } from "react-router"; import { consumeAndValidateKakaoState } from "@/utils/auth/kakaoOauth"; +import { setRecentLoginProvider } from "@/utils/auth/recentLoginProvider"; import { tokenStorage } from "@/utils/tokenStorage"; import { useOauth } from "./useOauth"; import { useAuthStore } from "@/store/useAuth.store"; @@ -28,6 +29,8 @@ export function useKakaoOauth({ onSuccess: (res) => { const data = res.data; + setRecentLoginProvider("KAKAO"); + if ("accessToken" in data) { // 기존 회원: accessToken 저장 후 메인으로 (refreshToken은 httpOnly 쿠키) tokenStorage.setTokens({ diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index e6f31daf..e7316481 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -1,4 +1,4 @@ -import { AppleButton, KakaoButton } from "@/components/auth"; +import { AppleButton, KakaoButton, RecentLoginDialog } from "@/components/auth"; import { CTA_Button } from "@/components/common"; import { Link, useNavigate, useSearchParams } from "react-router"; import { useEffect, useMemo, useState } from "react"; @@ -9,6 +9,10 @@ import { Capacitor3KakaoLogin } from "capacitor3-kakao-login"; import { isNativeApp } from "@/utils/auth/envUtils"; import { isAndroidApp } from "@/utils/platform"; import { oauth } from "@/apis/auth"; +import { + getRecentLoginProvider, + setRecentLoginProvider, +} from "@/utils/auth/recentLoginProvider"; import { tokenStorage } from "@/utils/tokenStorage"; import { consumeRedirectAfterLogin } from "../demoDay/redirectAfterLogin"; import { SplashScreen } from "@capacitor/splash-screen"; // 앱 초기 스플래시 제어용 @@ -34,6 +38,9 @@ export function LoginPage() { const [authCheckStatus, setAuthCheckStatus] = useState("pending"); + // 최근에 로그인했던 SNS (버튼 위 안내용) + const [recentLoginProvider] = useState(() => getRecentLoginProvider()); + // 앱 실행 시 네이티브 스플래시 숨김 & 백그라운드 토큰 검사 useEffect(() => { // 앱인 경우 기본 제공되는 스플래시를 즉시 가리고 리액트 애니메이션 띄우기 @@ -155,6 +162,8 @@ export function LoginPage() { const data = response.data; + setRecentLoginProvider("KAKAO"); + if ("isNewMember" in data && data.isNewMember === true) { await tokenStorage.setTokens({ accessToken: null, @@ -260,9 +269,13 @@ export function LoginPage() { className={`mx-auto max-w-sm ${ui.footerAnim}`} >
+ {recentLoginProvider === "APPLE" && !isAndroidApp() && ( + + )} {!isAndroidApp() && ( )} + {recentLoginProvider === "KAKAO" && }
diff --git a/src/utils/auth/recentLoginProvider.ts b/src/utils/auth/recentLoginProvider.ts new file mode 100644 index 00000000..26c3a8b0 --- /dev/null +++ b/src/utils/auth/recentLoginProvider.ts @@ -0,0 +1,21 @@ +import type { SocialProvider } from "@/types/auth"; + +const RECENT_LOGIN_PROVIDER_KEY = "finders:recentLoginProvider"; + +// 로그아웃과 무관하게 남아야 해서 useAuthStore(persist)와 분리된 별도 키로 관리 +export function setRecentLoginProvider(provider: SocialProvider) { + try { + localStorage.setItem(RECENT_LOGIN_PROVIDER_KEY, provider); + } catch (e) { + console.error("최근 로그인 정보 저장에 실패했습니다.", e); + } +} + +export function getRecentLoginProvider(): SocialProvider | null { + try { + const value = localStorage.getItem(RECENT_LOGIN_PROVIDER_KEY); + return value === "KAKAO" || value === "APPLE" ? value : null; + } catch { + return null; + } +} From 055d0a74bd51f8e089b612a4cea320e94cb992c6 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Wed, 15 Jul 2026 11:03:44 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=A4=91=EB=B3=B5(MEMBER=5F412)=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=A4=91=EB=B3=B5=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/member/phone.constant.ts | 2 ++ src/hooks/auth/onBoarding/useOnBoardingForm.ts | 15 ++++++++++++++- src/hooks/member/index.ts | 5 ++++- src/hooks/member/useRequestPhoneVerification.ts | 11 +++++++++++ src/pages/auth/OnBoarding.tsx | 10 ++++++++++ 5 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/constants/member/phone.constant.ts diff --git a/src/constants/member/phone.constant.ts b/src/constants/member/phone.constant.ts new file mode 100644 index 00000000..9528d527 --- /dev/null +++ b/src/constants/member/phone.constant.ts @@ -0,0 +1,2 @@ +// 서버 ErrorCode: 인증번호 발송 시 이미 가입된 전화번호 +export const PHONE_ALREADY_REGISTERED_CODE = "MEMBER_412"; diff --git a/src/hooks/auth/onBoarding/useOnBoardingForm.ts b/src/hooks/auth/onBoarding/useOnBoardingForm.ts index 524396e6..432296ee 100644 --- a/src/hooks/auth/onBoarding/useOnBoardingForm.ts +++ b/src/hooks/auth/onBoarding/useOnBoardingForm.ts @@ -3,10 +3,12 @@ import { useNavigate } from "react-router"; import type { TermsType } from "@/types/auth"; import { useDebouncedValue, useDebouncedTrue } from "@/hooks/common"; import { + extractPhoneVerifyErrorCode, useConfirmPhoneVerification, useNicknameCheck, useRequestPhoneVerification, } from "@/hooks/member"; +import { PHONE_ALREADY_REGISTERED_CODE } from "@/constants/member/phone.constant"; import { tokenStorage } from "@/utils/tokenStorage"; import { useSocialSignup } from "./useSignUp"; import { useAuthStore } from "@/store/useAuth.store"; @@ -42,6 +44,7 @@ export function useOnBoardingForm(options?: Options) { const [phoneVerifyMessage, setPhoneVerifyMessage] = useState(""); // 성공 문구 const [phoneVerifyError, setPhoneVerifyError] = useState(""); // 실패 문구 + const [isDuplicatePhone, setIsDuplicatePhone] = useState(false); // 이미 가입된 번호 안내 // 닉네임 인증 const debouncedNickname = useDebouncedValue(nickname, 400); @@ -133,7 +136,13 @@ export function useOnBoardingForm(options?: Options) { } // prettier-ignore }, - onError: (e) => console.error(e.message), + onError: (e) => { + if (extractPhoneVerifyErrorCode(e) === PHONE_ALREADY_REGISTERED_CODE) { + setIsDuplicatePhone(true); + return; + } + console.error(e.message); + }, }); const { mutate: confirmCode, isPending: isConfirmingCode } = @@ -316,5 +325,9 @@ export function useOnBoardingForm(options?: Options) { phoneBorderClass, phoneTextClass, lockPhoneForm, + + // 이미 가입된 번호 안내 + isDuplicatePhone, + closeDuplicatePhoneDialog: () => setIsDuplicatePhone(false), }; } diff --git a/src/hooks/member/index.ts b/src/hooks/member/index.ts index ba13e2dc..34e9668f 100644 --- a/src/hooks/member/index.ts +++ b/src/hooks/member/index.ts @@ -1,5 +1,8 @@ export { useNicknameCheck } from "./useCheckNickName"; -export { useRequestPhoneVerification } from "./useRequestPhoneVerification"; +export { + useRequestPhoneVerification, + extractPhoneVerifyErrorCode, +} from "./useRequestPhoneVerification"; export { useConfirmPhoneVerification } from "./useConfirmPhoneVerification"; export { useMe, type MeResponse, ME_QUERY_KEY } from "./useMe"; export { useEditMe } from "./useEditMe"; diff --git a/src/hooks/member/useRequestPhoneVerification.ts b/src/hooks/member/useRequestPhoneVerification.ts index 320eb1dc..5ae86915 100644 --- a/src/hooks/member/useRequestPhoneVerification.ts +++ b/src/hooks/member/useRequestPhoneVerification.ts @@ -1,4 +1,5 @@ import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { isAxiosError } from "axios"; import type { ApiResponse } from "@/types/common/apiResponse"; import { requestPhoneVerification } from "@/apis/member"; import type { @@ -18,3 +19,13 @@ export function useRequestPhoneVerification( ...options, }); } + +// axios 에러에서 서버 ApiResponse.code(MEMBER_xxx) 추출 +export function extractPhoneVerifyErrorCode( + error: unknown, +): string | undefined { + if (isAxiosError<{ code?: string }>(error)) { + return error.response?.data?.code ?? error.code; + } + return error instanceof Error ? error.message : undefined; +} diff --git a/src/pages/auth/OnBoarding.tsx b/src/pages/auth/OnBoarding.tsx index 50bd0582..79c4fb46 100644 --- a/src/pages/auth/OnBoarding.tsx +++ b/src/pages/auth/OnBoarding.tsx @@ -3,6 +3,7 @@ import { useLocation, useNavigate } from "react-router"; import { formatMMSS } from "@/utils/time"; import { ActionButton, InputForm } from "@/components/auth"; import { CTA_Button } from "@/components/common"; +import { DialogBox } from "@/components/common/DialogBox"; import { useOnBoardingForm } from "@/hooks/auth/onBoarding"; import type { TermsType } from "@/types/auth"; @@ -121,6 +122,15 @@ export function OnBoardingPage() { onClick={f.handleSubmit} /> + + navigate("/auth/login", { replace: true })} + onCancel={f.closeDuplicatePhoneDialog} + /> ); } From e53d1df3359a71e1b26fc7df29dc707f064240e5 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Wed, 15 Jul 2026 11:55:16 +0900 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=EC=97=90=EB=9F=AC=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C(MEMBER=5F412)=20=EC=9C=A0=EC=8B=A4=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/member/requestPhoneVerification.api.ts | 2 +- src/hooks/member/useRequestPhoneVerification.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/apis/member/requestPhoneVerification.api.ts b/src/apis/member/requestPhoneVerification.api.ts index 1d6f1b8e..d6f531d8 100644 --- a/src/apis/member/requestPhoneVerification.api.ts +++ b/src/apis/member/requestPhoneVerification.api.ts @@ -17,7 +17,7 @@ export async function requestPhoneVerification( const body = res.data; if (!body.success) { - throw new Error(body.message); + throw Object.assign(new Error(body.message), { code: body.code }); } return body; diff --git a/src/hooks/member/useRequestPhoneVerification.ts b/src/hooks/member/useRequestPhoneVerification.ts index 5ae86915..5810b19d 100644 --- a/src/hooks/member/useRequestPhoneVerification.ts +++ b/src/hooks/member/useRequestPhoneVerification.ts @@ -20,12 +20,15 @@ export function useRequestPhoneVerification( }); } -// axios 에러에서 서버 ApiResponse.code(MEMBER_xxx) 추출 +// axios 에러(HTTP 에러 상태) 또는 200 + success:false로 던져진 에러 모두에서 서버 ApiResponse.code(MEMBER_xxx) 추출 export function extractPhoneVerifyErrorCode( error: unknown, ): string | undefined { if (isAxiosError<{ code?: string }>(error)) { return error.response?.data?.code ?? error.code; } + if (error instanceof Error && "code" in error) { + return (error as Error & { code?: string }).code; + } return error instanceof Error ? error.message : undefined; } From 7151fb970f4fb94d6c2f49375bcf21884ecea7a0 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Wed, 15 Jul 2026 12:31:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=ED=95=98=EB=93=9C=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EC=8B=9C=20?= =?UTF-8?q?=EC=95=B1=20=EC=A2=85=EB=A3=8C=20=ED=99=95=EC=9D=B8=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/RootLayout.tsx | 16 ++++++++++++++-- src/pages/auth/LoginPage.tsx | 2 +- src/pages/auth/TermsAgreementPage.tsx | 5 ++++- src/router/Router.tsx | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index c3b3bb47..9b071bf1 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -1,12 +1,14 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useNavigate, Outlet } from "react-router"; import { App } from "@capacitor/app"; import { Capacitor } from "@capacitor/core"; +import { DialogBox } from "@/components/common/DialogBox"; import GlobalLoginDialog from "@/components/common/GlobalLoginDialog"; import { useNewPostState } from "@/store/useNewPostState.store"; export default function RootLayout() { const navigate = useNavigate(); + const [isExitDialogOpen, setIsExitDialogOpen] = useState(false); useEffect(() => { if (!Capacitor.isNativePlatform()) return; @@ -23,7 +25,8 @@ export default function RootLayout() { if (canGoBack) { navigate(-1); } else { - App.exitApp(); + // 뒤로 갈 히스토리가 없음(=뒤로가기 시 앱 종료): 즉시 종료 대신 확인 다이얼로그 + setIsExitDialogOpen(true); } }); @@ -35,6 +38,15 @@ export default function RootLayout() { return (
+ setIsExitDialogOpen(false)} + confirmText="네" + onConfirm={() => App.exitApp()} + /> {/* safe-area + 중앙 레이아웃(PC) + 패딩(모바일) */}
{/* Rootlayout으로 감싸진 모든 컴포넌트 렌더링*/} diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index e7316481..7532a50d 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -249,7 +249,7 @@ export function LoginPage() { > navigate("/mainpage", { replace: true })} color="orange" size="compact" /> diff --git a/src/pages/auth/TermsAgreementPage.tsx b/src/pages/auth/TermsAgreementPage.tsx index ff420940..164d0e22 100644 --- a/src/pages/auth/TermsAgreementPage.tsx +++ b/src/pages/auth/TermsAgreementPage.tsx @@ -19,7 +19,10 @@ export function TermsAgreementPage() { const handleConfirm = () => { if (!requiredAllChecked) return; - navigate("/auth/onboarding", { state: { agreedTermTypes } }); + navigate("/auth/onboarding", { + state: { agreedTermTypes }, + replace: true, + }); }; return ( diff --git a/src/router/Router.tsx b/src/router/Router.tsx index 68bcbd99..15d41fb6 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -267,7 +267,7 @@ const router = createBrowserRouter([ { Component: RootLayout, children: [ - { index: true, element: }, + { index: true, element: }, // auth prefix를 한 번만 { path: "auth", children: authRoutes }, From d878b8603f281297bde0a06263076000de90a3de Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Wed, 15 Jul 2026 15:49:28 +0900 Subject: [PATCH 5/8] =?UTF-8?q?refactor:=20=EB=A7=90=ED=92=8D=EC=84=A0=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=A1=B0=EC=A0=95(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/RecentLoginDialog.tsx | 12 ++++++++---- src/pages/auth/LoginPage.tsx | 22 +++++++++++++++++++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/components/auth/RecentLoginDialog.tsx b/src/components/auth/RecentLoginDialog.tsx index 5e2bcf5c..92f4b709 100644 --- a/src/components/auth/RecentLoginDialog.tsx +++ b/src/components/auth/RecentLoginDialog.tsx @@ -1,12 +1,16 @@ import { PolygonIcon } from "@/assets/icon"; -export function RecentLoginDialog() { +interface RecentLoginDialogProps { + className?: string; +} + +export function RecentLoginDialog({ className = "" }: RecentLoginDialogProps) { return ( -
-
+
+
최근에 로그인 했어요
- +
); } diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index 7532a50d..32fd2aef 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -272,10 +272,26 @@ export function LoginPage() { {recentLoginProvider === "APPLE" && !isAndroidApp() && ( )} - {!isAndroidApp() && ( - +
+ {!isAndroidApp() && ( + + )} + {recentLoginProvider === "KAKAO" && !isAndroidApp() && ( + + )} +
+ {recentLoginProvider === "KAKAO" && isAndroidApp() && ( + )} - {recentLoginProvider === "KAKAO" && }
From 54a9339014eab5f96d75aa8f57635d3be0da6075 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Wed, 15 Jul 2026 15:50:56 +0900 Subject: [PATCH 6/8] =?UTF-8?q?refactor:=20=EB=B2=84=ED=8A=BC=20gap=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/auth/LoginPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index 32fd2aef..489d1fb8 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -268,7 +268,7 @@ export function LoginPage() { key={ui.footerKey} className={`mx-auto max-w-sm ${ui.footerAnim}`} > -
+
{recentLoginProvider === "APPLE" && !isAndroidApp() && ( )} From 2405d68d1797d1ddba1c3825cf4cf3062cd5c371 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Mon, 3 Aug 2026 17:36:32 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EA=B0=80=EC=9E=85=20=EB=AF=B8?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20SNS=EA=B0=80?= =?UTF-8?q?=20=EA=B8=B0=EB=A1=9D=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/auth/login/useAppleLogin.ts | 4 ++-- src/hooks/auth/login/useKakaoOAuth.ts | 4 ++-- src/pages/auth/LoginPage.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hooks/auth/login/useAppleLogin.ts b/src/hooks/auth/login/useAppleLogin.ts index 0c3ca0a3..cb0e8dfa 100644 --- a/src/hooks/auth/login/useAppleLogin.ts +++ b/src/hooks/auth/login/useAppleLogin.ts @@ -26,9 +26,9 @@ export function useAppleLogin({ onSuccess: (res) => { const data = res.data; - setRecentLoginProvider("APPLE"); - if ("accessToken" in data) { + // 가입이 끝난 계정만 "최근 로그인"으로 기록 (신규 회원은 아직 계정이 없음) + setRecentLoginProvider("APPLE"); tokenStorage.setTokens({ accessToken: data.accessToken, signupToken: null, diff --git a/src/hooks/auth/login/useKakaoOAuth.ts b/src/hooks/auth/login/useKakaoOAuth.ts index d5def19c..442f67c0 100644 --- a/src/hooks/auth/login/useKakaoOAuth.ts +++ b/src/hooks/auth/login/useKakaoOAuth.ts @@ -29,10 +29,10 @@ export function useKakaoOauth({ onSuccess: (res) => { const data = res.data; - setRecentLoginProvider("KAKAO"); - if ("accessToken" in data) { // 기존 회원: accessToken 저장 후 메인으로 (refreshToken은 httpOnly 쿠키) + // 가입이 끝난 계정만 "최근 로그인"으로 기록 (신규 회원은 아직 계정이 없음) + setRecentLoginProvider("KAKAO"); tokenStorage.setTokens({ accessToken: data.accessToken, signupToken: null, diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx index 73ab431f..aeb28a67 100644 --- a/src/pages/auth/LoginPage.tsx +++ b/src/pages/auth/LoginPage.tsx @@ -162,8 +162,6 @@ export function LoginPage() { const data = response.data; - setRecentLoginProvider("KAKAO"); - if ("isNewMember" in data && data.isNewMember === true) { await tokenStorage.setTokens({ accessToken: null, @@ -171,6 +169,8 @@ export function LoginPage() { }); navigate("/auth/onboarding", { replace: true }); } else if ("member" in data) { + // 가입이 끝난 계정만 "최근 로그인"으로 기록 (신규 회원은 아직 계정이 없음) + setRecentLoginProvider("KAKAO"); await tokenStorage.setTokens({ accessToken: data.accessToken, signupToken: null, From 5db36c08af36284767a402787efbb934f7f578f3 Mon Sep 17 00:00:00 2001 From: jeonbinggu Date: Mon, 3 Aug 2026 17:37:13 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=EC=95=88=EB=93=9C=EB=A1=9C=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=ED=95=98=EB=93=9C=EC=9B=A8=EC=96=B4=20=EB=92=A4?= =?UTF-8?q?=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=8F=99=EC=9E=91=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0(#346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/RootLayout.tsx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx index 9b071bf1..8fc989ac 100644 --- a/src/layouts/RootLayout.tsx +++ b/src/layouts/RootLayout.tsx @@ -15,6 +15,13 @@ export default function RootLayout() { // Promise 자체를 붙잡아, resolve 타이밍과 무관하게 클린업에서 리스너를 제거한다. const handlePromise = App.addListener("backButton", ({ canGoBack }) => { + // 종료 다이얼로그가 떠 있으면 뒤로가기는 다이얼로그만 닫는다 + // (DialogBox의 useDismiss는 pointerdown/Escape만 처리 — 안드로이드 back은 안 잡힘) + if (isExitDialogOpen) { + setIsExitDialogOpen(false); + return; + } + // 게시글 등록 직후엔 하드웨어 뒤로가기도 이전 작성 단계가 아니라 피드로 이동 if (useNewPostState.getState().isNewPost) { useNewPostState.getState().setIsNewPost(false); @@ -24,16 +31,26 @@ export default function RootLayout() { if (canGoBack) { navigate(-1); - } else { - // 뒤로 갈 히스토리가 없음(=뒤로가기 시 앱 종료): 즉시 종료 대신 확인 다이얼로그 - setIsExitDialogOpen(true); + return; } + + // 가입 플로우(약관/온보딩)는 전 구간 replace라 히스토리가 비어 있다. + // 여기서 앱을 닫으면 입력 중이던 가입 정보가 날아가므로 로그인으로 되돌린다. + // (이벤트 발생 시점에 읽어야 리스너 재등록 없이 최신 경로가 잡힌다) + const { pathname } = window.location; + if (pathname.startsWith("/auth/") && pathname !== "/auth/login") { + navigate("/auth/login", { replace: true }); + return; + } + + // 뒤로 갈 히스토리가 없음(=뒤로가기 시 앱 종료): 즉시 종료 대신 확인 다이얼로그 + setIsExitDialogOpen(true); }); return () => { handlePromise.then((handle) => handle.remove()); }; - }, [navigate]); + }, [navigate, isExitDialogOpen]); return (