-
Notifications
You must be signed in to change notification settings - Fork 4
fix : localStoradge 사용하는 코드 제거했습니다 #240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Suggested reviewers
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/app/mentor/_ui/MentorClient/_hooks/useRouterHandler.ts(0 hunks)src/app/mentor/_ui/MentorClient/index.tsx(1 hunks)
💤 Files with no reviewable changes (1)
- src/app/mentor/_ui/MentorClient/_hooks/useRouterHandler.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/app/mentor/_ui/MentorClient/index.tsx (1)
src/utils/jwtUtils.ts (1)
getUserRoleFromJwt(93-99)
🔇 Additional comments (3)
src/app/mentor/_ui/MentorClient/index.tsx (3)
3-4: SSR 안전성 향상: localStorage 의존을 제거한 역할 판별 전환 좋습니다.
- 빌드 타임/SSR 환경에서 안전한 jwtUtils 경유 접근으로 리스크를 낮췄습니다.
- 클라이언트 컴포넌트 특성에도 불구, 공용 유틸로 일원화한 점이 유지보수에 유리합니다.
10-11: 멘토 전용 UI 전체 주석으로 멘토 사용자에게 빈 화면이 노출됩니다.
- 임시 상태라면 간단한 플레이스홀더(예: “멘토 전용 페이지 준비 중”)라도 노출하는 편이 UX에 안전합니다.
- 아니면 기능 플래그/조건부 렌더링으로 단계적 활성화를 권장합니다.
아래처럼 최소 안내를 노출하는 방식을 고려해 주세요(참고용):
{isMentor ? ( <> {/* <MentorPageTabs /> */} {/* <MyMentorSection /> */} <p style={{ padding: 16 }}>멘토 전용 페이지가 준비 중입니다.</p> </> ) : ( // ... )}
3-4: 레거시 localStorage 사용 잔존 및 getAccessToken SSR 안전성 확인 요청
레거시 localStorage 사용 현황 탐색
- src/utils/authUtils.ts: loadAccessToken/loadRefreshToken 래퍼함수로 localStorage 참조
- src/app/my/MyContent.tsx: accessToken·refreshToken 직접 removeItem 호출
- src/app/login/**CallbackPage.tsx: saveAccessToken/saveRefreshToken 임포트
- src/app/mentor/.../useFirstVisit.ts: 방문 여부 기록을 위해 getItem/setItem 사용
- src/components/login/signup/SignupSurvey.tsx: saveAccessToken/saveRefreshToken 임포트
getAccessToken SSR 안전성 검증
- getAccessToken은 zustand store(getState())만 조회
- window, localStorage, document 접근이 없어 SSR 환경에서도 예외 없이 null 반환 가능
위 결과를 바탕으로 이번 PR 목표와 일치하는지, 남은 레거시 localStorage 접근을 언제·어떻게 제거할지 검토 부탁드립니다.
| import { UserRole } from "@/types/mentor"; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
타입 정합성 제안: getUserRoleFromJwt를 UserRole | null 반환으로 강화하세요.
- 현재 string | null → enum 비교는 런타임 동작은 가능하나 타입 안전성이 떨어집니다.
- 유틸의 반환 타입을 UserRole | null로 좁히면 비교·분기 시 오타/케이스 미스매치를 줄일 수 있습니다.
jwtUtils.ts에 다음과 같은 형태로 보완을 제안합니다(파일 외 참고 코드):
// utils/jwtUtils.ts
import { UserRole } from "@/types/mentor";
const toUserRole = (role: unknown): UserRole | null => {
if (typeof role !== "string") return null;
const upper = role.toUpperCase();
return Object.values(UserRole).includes(upper as UserRole) ? (upper as UserRole) : null;
};
export const getUserRoleFromJwt = (): UserRole | null => {
const token = getAccessToken();
if (!token) return null;
const decoded = parseJwt(token);
return toUserRole(decoded?.role);
};🤖 Prompt for AI Agents
In src/app/mentor/_ui/MentorClient/index.tsx around lines 8-9, the file imports
UserRole but getUserRoleFromJwt currently returns string | null which weakens
type safety when comparing to the UserRole enum; change the utility so it
returns UserRole | null by adding a safe converter that verifies the decoded
role is a string, normalizes/case-maps it to the enum format, checks inclusion
against Object.values(UserRole), and returns the matched UserRole or null;
update getUserRoleFromJwt to use this converter and adjust any call sites to
expect UserRole | null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반영했습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const userRole = getUserRoleFromJwt(); | ||
| const isMentor = userRole === UserRole.MENTOR; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
역할 문자열 케이스/널 안전성 보완을 권장합니다.
- 토큰의 role이 소문자/대문자 혼재 시, enum 비교가 실패할 수 있습니다.
- 널 안전 연산자와 대문자 정규화로 비교를 견고하게 만드세요.
아래 변경으로 케이스·널 안전 비교를 적용할 수 있습니다.
- const userRole = getUserRoleFromJwt();
- const isMentor = userRole === UserRole.MENTOR;
+ const role = getUserRoleFromJwt();
+ const isMentor = role?.toUpperCase() === UserRole.MENTOR;추가로, 로그인 전(null)·알 수 없는 역할 처리 시 현재는 멘티 화면으로 폴백됩니다. 의도된 동작인지 확인을 부탁드립니다. 비의도라면 “로그인이 필요합니다” 또는 중립 플레이스홀더를 권장합니다.
getUserRoleFromJwt 함수 반환값과 UserRole enum 정의를 확인해 봐야 할 것 같아요! 아래 스크립트를 실행해 주세요:
🏁 Script executed:
#!/bin/bash
# 1) getUserRoleFromJwt 함수 정의 확인
rg -n "getUserRoleFromJwt" -A5
# 2) UserRole enum 정의 확인
rg -n "enum UserRole" -A10Length of output: 4173
토큰 역할 비교에 대소문자 및 null 안정성 강화 필요
현재 getUserRoleFromJwt()는 string | null을 반환하며, UserRole enum은 "MENTOR"/"MENTEE" 대문자만을 지원하므로 토큰에 담긴 역할이 소문자나 혼합 대소문자일 경우 비교에 실패할 수 있습니다.
아래 절차에 따라 코드를 수정하고, 로그인 전(null) 상태의 화면 전환 의도를 함께 검토해주세요.
- 역할 문자열 정규화 적용
role?.toUpperCase()로 대문자 일관화
- null 안전 비교 도입
- optional chaining(
?)으로null혹은undefined시 에러 방지
- optional chaining(
- 로그인 전·알 수 없는 역할(fallback) 처리 의도 확인
- 현재
isMentor가false가 되면 자동으로 멘티 화면으로 전환됩니다. - 의도되지 않은 동작이라면 “로그인이 필요합니다” 안내 또는 중립 플레이스홀더 도입을 고려하세요.
- 현재
- const userRole = getUserRoleFromJwt();
- const isMentor = userRole === UserRole.MENTOR;
+ const role = getUserRoleFromJwt();
+ const isMentor = role?.toUpperCase() === UserRole.MENTOR;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const userRole = getUserRoleFromJwt(); | |
| const isMentor = userRole === UserRole.MENTOR; | |
| const role = getUserRoleFromJwt(); | |
| const isMentor = role?.toUpperCase() === UserRole.MENTOR; |
🤖 Prompt for AI Agents
In src/app/mentor/_ui/MentorClient/index.tsx around lines 14-16, the code
compares the raw role string from getUserRoleFromJwt() directly to
UserRole.MENTOR which is brittle for null/undefined and mixed-case tokens;
update to normalize and null-safe check by using optional chaining and
toUpperCase() on the returned role before comparing (e.g., role?.toUpperCase()
=== UserRole.MENTOR), and adjust the UI flow for null/unknown roles so that a
false is not assumed to mean "mentee" — explicitly handle unauthenticated or
unknown-role states with a login prompt or neutral placeholder if that is the
intended behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 아이디어 반영했습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🔭 Outside diff range comments (1)
src/utils/authUtils.ts (1)
2-2: 1) SSR 안전성: localStorage 의존성 제거와 isAuthenticated 리팩터가 필요해요.
- 이 PR의 목적과 달리 localStorage 유틸을 아직 import하고 있어 SSR/정적 빌드에서 다시 터질 수 있어요.
- 토큰은 zustand 스토어(getAccessToken) 또는 HTTP-only 쿠키 기반 리프레시 플로우를 사용하도록 합쳐주세요.
- 서버 사이드에서 안전하게 동작하도록 window 접근도 방어하면 좋아요.
아래처럼 수정을 제안드립니다.
-import { isTokenExpired } from "./jwtUtils"; -import { loadAccessToken, loadRefreshToken } from "./localStorage"; +import { isTokenExpired } from "./jwtUtils"; +import { getAccessToken } from "@/lib/zustand/useTokenStore"; -export const isAuthenticated = () => { - if (!isTokenExpired(loadAccessToken()) || !isTokenExpired(loadRefreshToken())) { - return true; - } - return false; -}; +export const isAuthenticated = () => { + // SSR 안전 가드 + if (typeof window === "undefined") return false; + const accessToken = typeof getAccessToken === "function" ? getAccessToken() : null; + return !isTokenExpired(accessToken); +};참고: 만약 getAccessToken 유틸이 없다면 useTokenStore.getState().accessToken을 래핑해 export하는 헬퍼를 추가해 주세요. 원한다면 해당 헬퍼도 같이 제안드릴게요.
Also applies to: 4-9
🧹 Nitpick comments (9)
src/app/login/_hooks/useInputHandler.ts (1)
1-1: 1) 불필요한 리렌더 줄이기: 핸들러를 useCallback으로 메모이즈해요.
- input onChange에 물리는 핸들러는 참조가 고정되면 자식 렌더 비용이 줄어듭니다.
-import { useState } from "react"; +import { useState, useCallback } from "react"; - const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const handleEmailChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value.trim(); setShowPasswordField(value.length > 0); - }; + }, []);Also applies to: 13-16
src/api/auth/client/usePostEmailAuth.ts (2)
18-29: 1) 응답 언래핑과 타입 강화: React Query에서 data만 반환하도록 단순화해요.
- AxiosResponse 대신 비즈니스 데이터로 동작하면 onSuccess, select 등에서 타입 안정성이 높아집니다.
- 실패 재시도는 로그인에서 과도할 수 있어 retry: false 옵션을 고려해요.
-const postEmailAuth = ({ email, password }: LoginRequest): Promise<AxiosResponse<UsePostEmailSignInResponse>> => - publicAxiosInstance.post("/auth/email/sign-in", { email, password }); +const postEmailAuth = async ({ email, password }: LoginRequest): Promise<UsePostEmailSignInResponse> => { + const { data } = await publicAxiosInstance.post("/auth/email/sign-in", { email, password }); + return data; +} - return useMutation({ - mutationFn: postEmailAuth, - onSuccess: (data) => { - const { accessToken } = data.data; + return useMutation<UsePostEmailSignInResponse, unknown, LoginRequest>({ + mutationFn: postEmailAuth, + retry: false, + onSuccess: (data) => { + const { accessToken } = data; setAccessToken(accessToken); }, });
21-29: 2) 기본 흐름은 좋습니다.
- 훅 구성과 토큰 저장 타이밍은 자연스럽습니다.
- UI에서 에러 UX를 통일하려면 onError에서 콜백을 받아 처리하게 하거나, 훅 내에서 toasting을 일관 적용하는 것도 고려해 보세요.
src/app/login/apple/callback/AppleLoginCallbackPage.tsx (1)
16-19: 2) 코드 파라미터 없음 처리: 무한 스피너를 피하도록 폴백을 추가하세요.
- code가 없는 경우 로그인 페이지로 돌려보내거나 에러 메시지를 보여주면 UX가 좋아집니다.
- if (code) { - postAppleAuth({ code }); - } + if (code) { + postAppleAuth({ code }); + } else { + // TODO: 전역 토스트로 교체 권장 + window.location.replace("/login"); + }src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx (1)
14-21: code 파라미터가 없을 때 무한 스피너를 피하고 복구 경로를 주세요.
- code가 없으면 현재는 CloudSpinnerPage만 계속 노출됩니다.
- 사용자 경험을 위해 로그인으로 되돌리거나 오류 안내를 보여 주세요.
선택지:
- 간단: code가 falsy면 router.replace("/login?error=oauth_missing_code")로 복귀.
- 대안: 이 페이지는 스피너만 유지하고, 훅 내부(onError)에서 미인증 상태를 감지해 /login으로 보내도록 통일(팀 합의 필요).
src/api/auth/client/usePostKakaoAuth.ts (2)
51-54: 사용하지 않는 매개변수 정리 및 로깅 확장 제안.
- onError의 error 변수가 사용되지 않습니다(린트 경고 가능).
- 원인 파악을 위해 최소한 콘솔 로깅을 남기거나 변수명을 _error로 변경해 주세요.
- onError: (error) => { - alert("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); - router.push("/login"); - }, + onError: (_error) => { + // console.error("Kakao auth error:", _error); + alert("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); + router.push("/login"); + },
37-41: 타입 추론 개선: mutation에서 AxiosResponse 대신 데이터만 다루도록 단순화.
- onSuccess에서 불필요한 response 래핑을 벗기고 data만 사용하면 타입과 가독성이 좋아집니다.
- useMutation 제네릭과 mutationFn을 아래처럼 정리하면 onSuccess 시그니처도 간결해집니다.
- return useMutation({ - mutationFn: postKakaoAuth, - onSuccess: (response) => { - const { data } = response; + return useMutation< + RegisteredKakaoAuthReponse | UnregisteredKakaoAuthReponse, + unknown, + KakaoAuthRequest + >({ + mutationFn: async ({ code }) => (await publicAxiosInstance.post("/auth/kakao", { code })).data, + onSuccess: (data) => { if (data.isRegistered) { // 기존 회원일 시 - 토큰 저장하고 홈으로 이동 setAccessToken(data.accessToken); router.push("/"); } else { // 새로운 회원일 시 - 회원가입 페이지로 이동 - router.push(`/sign-up?token=${data.signUpToken}`); + router.push(`/sign-up?token=${encodeURIComponent(data.signUpToken)}`); } },src/app/login/LoginContent.tsx (2)
36-43: UX 강화(선택): 유효성 기반 버튼 활성화와 폼 모드.
- 즉각적인 유효성 피드백을 위해 RHF를 onChange 모드로 두고 isValid를 사용하면 좋습니다.
- 비밀번호 영역 표시 전에는 제출 버튼을 비활성화하여 불필요한 에러 표시를 줄일 수 있습니다.
} = useForm<LoginFormData>({ - resolver: zodResolver(loginSchema), + resolver: zodResolver(loginSchema), + mode: "onChange", defaultValues: { email: "", password: "", }, }); ... - <button + <button type="submit" - disabled={isPending} + disabled={isPending || !showPasswordField || !isValid} className="flex h-11 w-full items-center justify-center rounded-lg bg-primary p-2.5 font-serif text-base font-medium text-white disabled:cursor-not-allowed disabled:opacity-50" >Also applies to: 100-107
74-80: 자동완성 힌트 추가로 로그인 UX 개선.
- 브라우저 자동완성 도움을 위해 email/password 필드에 autoComplete 속성을 부여해 주세요.
- 접근성과 입력 편의가 향상됩니다.
- <input + <input type="email" placeholder="이메일" className="h-[41px] w-full rounded-lg border px-5 py-3 font-serif text-xs font-normal leading-normal text-k-400 focus:outline-none" {...register("email", { onChange: handleEmailChange, })} + autoComplete="email" /> ... - <input + <input type="password" placeholder="비밀번호" className="h-[41px] w-full rounded-lg border px-5 py-3 font-serif text-xs font-normal leading-normal text-k-400 focus:outline-none" {...register("password")} + autoComplete="current-password" />Also applies to: 90-97
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
public/svgs/auth/apple-logo.svgis excluded by!**/*.svgpublic/svgs/auth/email-icon.svgis excluded by!**/*.svgpublic/svgs/auth/kakao-logo.svgis excluded by!**/*.svg
📒 Files selected for processing (12)
public/svgs/auth/index.ts(1 hunks)src/api/auth/client/usePostAppleAuth.ts(1 hunks)src/api/auth/client/usePostEmailAuth.ts(1 hunks)src/api/auth/client/usePostKakaoAuth.ts(1 hunks)src/app/login/AppleLoginButton.tsx(0 hunks)src/app/login/EmailSignUpButton.tsx(0 hunks)src/app/login/KakaoLoginButton.tsx(0 hunks)src/app/login/LoginContent.tsx(2 hunks)src/app/login/_hooks/useInputHandler.ts(1 hunks)src/app/login/apple/callback/AppleLoginCallbackPage.tsx(1 hunks)src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx(1 hunks)src/utils/authUtils.ts(1 hunks)
💤 Files with no reviewable changes (3)
- src/app/login/KakaoLoginButton.tsx
- src/app/login/AppleLoginButton.tsx
- src/app/login/EmailSignUpButton.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-06T06:19:56.705Z
Learnt from: manNomi
PR: solid-connection/solid-connect-web#235
File: src/app/mentor/_ui/MentorClient/_hooks/useRouterHandler.ts:16-17
Timestamp: 2025-08-06T06:19:56.705Z
Learning: manNomi는 useRouterHandler 훅에서 로그인 체크 및 리다이렉트 로직을 useEffect를 통해 관리하여 사이드 이펙트를 적절히 처리하고 있다.
Applied to files:
src/app/login/LoginContent.tsx
🧬 Code Graph Analysis (5)
src/utils/authUtils.ts (1)
src/types/auth.ts (1)
appleOAuth2CodeResponse(65-70)
src/api/auth/client/usePostKakaoAuth.ts (3)
src/types/auth.ts (2)
RegisteredKakaoAuthReponse(36-40)UnregisteredKakaoAuthReponse(42-48)src/utils/axiosInstance.ts (1)
publicAxiosInstance(183-186)src/lib/zustand/useTokenStore.ts (1)
setAccessToken(35-37)
src/api/auth/client/usePostEmailAuth.ts (2)
src/utils/axiosInstance.ts (1)
publicAxiosInstance(183-186)src/lib/zustand/useTokenStore.ts (1)
setAccessToken(35-37)
src/app/login/LoginContent.tsx (2)
src/utils/authUtils.ts (2)
kakaoLogin(23-31)appleLogin(33-54)public/svgs/auth/index.ts (3)
IconKakaoLogo(14-14)IconEmailIcon(16-16)IconAppleLogo(15-15)
src/api/auth/client/usePostAppleAuth.ts (2)
src/utils/axiosInstance.ts (1)
publicAxiosInstance(183-186)src/lib/zustand/useTokenStore.ts (1)
setAccessToken(35-37)
🔇 Additional comments (5)
src/utils/authUtils.ts (1)
26-27: 환경변수 누락 점검 요청
- src/utils/authUtils.ts 26–27
redirectUri: \${process.env.NEXT_PUBLIC_WEB_URL}/login/kakao/callback``- src/utils/authUtils.ts 40–43
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_IDscope: process.env.NEXT_PUBLIC_APPLE_SCOPEredirectURI: \${process.env.NEXT_PUBLIC_WEB_URL}/login/apple/callback``위 위치에서 참조되는 NEXT_PUBLIC_WEB_URL, NEXT_PUBLIC_APPLE_CLIENT_ID, NEXT_PUBLIC_APPLE_SCOPE가 빌드·런타임 환경 모두에서 누락 없이 설정되어 있는지 반드시 확인해 주세요. 누락 시 SDK 동작 또는 리디렉션이 실패할 수 있습니다.
src/api/auth/client/usePostAppleAuth.ts (1)
42-49: 2) 토큰/라우팅 처리 흐름은 적절합니다.
- 기존 회원/신규 회원 분기와 액세스 토큰 저장 시점이 명확합니다.
- Kakao 플로우와 일관성 유지도 잘 되어 있어요.
src/api/auth/client/usePostKakaoAuth.ts (1)
1-59: 😎 SSR/정적 빌드 안전성 검증
다음 세 가지 사항을 점검해 주세요:
- localStorage 직접 호출 탐색
• src/app/mentor/.../useFirstVisit.ts, src/app/my/MyContent.tsx, src/components/login/signup/SignupSurvey.tsx 등에서 여전히 localStorage.getItem/setItem/removeItem이 남아 있습니다.- Zustand persist 미사용 확인
•useTokenStore에persist미들웨어가 적용되지 않아 토큰은 메모리만 사용되지만,src/utils/localStorage.ts의 함수 이름이 혼동을 일으킬 수 있습니다.- 클라이언트 사이드 방어 보강
• localStorage 접근 구문에use client선언을 추가하거나if (typeof window !== 'undefined')조건문으로 SSR 시점 오류를 방지해주세요.위 항목을 반영해 클라이언트 전용 코드에서만 localStorage를 사용하도록 조치 부탁드립니다.
public/svgs/auth/index.ts (1)
1-20: 아이콘 집합 확장 LGTM.
- 신규 아이콘 3종 import/export 일관성 좋습니다.
- 로그인 페이지에서의 사용 목적 주석도 명확합니다.
src/app/login/LoginContent.tsx (1)
10-17: 🚨 appleLogin 유틸 함수 내 router.push 수정 요청앗!
router객체 없이router.push를 호출해서 런타임 오류가 발생할 위험이 있어요. 조심조심~
- 라우터 객체 미정의 호출 수정 필요
└──src/utils/authUtils.ts49행의router.push는router가 선언/임포트되지 않아 오류를 유발합니다.- 유틸 함수 내 라우팅은 브라우저 내장 API로 대체
└──window.location.href를 사용해 리다이렉트하도록 변경해주세요.- 또는 라우팅 책임은 컴포넌트/훅으로 위임
└── 호출부(컴포넌트/커스텀 훅)에서useRouter를 사용해 라우팅 로직을 처리하세요.예시 변경안:
- if (res.authorization) { - router.push(`/login/apple/callback?code=${res.authorization.code}`); - } + if (res.authorization) { + window.location.href = `${process.env.NEXT_PUBLIC_WEB_URL}/login/apple/callback?code=${res.authorization.code}`; + }Likely an incorrect or invalid review comment.
| return useMutation({ | ||
| mutationFn: postAppleAuth, | ||
| onSuccess: (response) => { | ||
| const { data } = response; | ||
|
|
||
| if (data.isRegistered) { | ||
| // 기존 회원일 시 - 토큰 저장하고 홈으로 이동 | ||
| setAccessToken(data.accessToken); | ||
| router.push("/"); | ||
| } else { | ||
| // 새로운 회원일 시 - 회원가입 페이지로 이동 | ||
| router.push(`/sign-up?token=${data.signUpToken}`); | ||
| } | ||
| }, | ||
| onError: (error) => { | ||
| alert("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); | ||
| router.push("/login"); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
1) 재시도/중복 처리: 로그인 실패 재시도와 다중 알림을 막아요.
- React Query 기본 retry(3)로 인해 동일 오류에 3번 alert가 뜰 수 있어요.
- callback 페이지의 Strict Mode 이펙트 중복 호출과 겹치면 서버에 이중 요청이 갑니다.
- return useMutation({
+ return useMutation<AxiosResponse<AppleAuthResponse>, unknown, AppleAuthRequest>({
mutationFn: postAppleAuth,
+ retry: false,
onSuccess: (response) => {
const { data } = response;
@@
onError: (error) => {
- alert("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
+ // TODO: 전역 토스트로 교체 권장
+ alert("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
router.push("/login");
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return useMutation({ | |
| mutationFn: postAppleAuth, | |
| onSuccess: (response) => { | |
| const { data } = response; | |
| if (data.isRegistered) { | |
| // 기존 회원일 시 - 토큰 저장하고 홈으로 이동 | |
| setAccessToken(data.accessToken); | |
| router.push("/"); | |
| } else { | |
| // 새로운 회원일 시 - 회원가입 페이지로 이동 | |
| router.push(`/sign-up?token=${data.signUpToken}`); | |
| } | |
| }, | |
| onError: (error) => { | |
| alert("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); | |
| router.push("/login"); | |
| }, | |
| }); | |
| return useMutation<AxiosResponse<AppleAuthResponse>, unknown, AppleAuthRequest>({ | |
| mutationFn: postAppleAuth, | |
| retry: false, | |
| onSuccess: (response) => { | |
| const { data } = response; | |
| if (data.isRegistered) { | |
| // 기존 회원일 시 - 토큰 저장하고 홈으로 이동 | |
| setAccessToken(data.accessToken); | |
| router.push("/"); | |
| } else { | |
| // 새로운 회원일 시 - 회원가입 페이지로 이동 | |
| router.push(`/sign-up?token=${data.signUpToken}`); | |
| } | |
| }, | |
| onError: (error) => { | |
| // TODO: 전역 토스트로 교체 권장 | |
| alert("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요."); | |
| router.push("/login"); | |
| }, | |
| }); |
🤖 Prompt for AI Agents
src/api/auth/client/usePostAppleAuth.ts around lines 37 to 55: the mutation
currently uses React Query default retry which can produce repeated alerts and
duplicate server requests under Strict Mode; set retry: 0 (or false) in the
useMutation options to disable automatic retries, and add a simple
duplicate-handling guard for the onError alert (e.g., maintain a local ref/flag
that ensures the alert and router.push("/login") run only once per mutation) or
prevent duplicate mutate calls at the caller by checking isLoading before
calling mutate.
| interface RegisteredKakaoAuthReponse { | ||
| isRegistered: true; | ||
| accessToken: string; | ||
| refreshToken: string; | ||
| } | ||
|
|
||
| interface UnregisteredKakaoAuthReponse { | ||
| isRegistered: false; | ||
| nickname: string; | ||
| email: string; | ||
| profileImageUrl: string; | ||
| signUpToken: string; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
중복 타입 선언 제거: 공용 타입을 import해서 DRY를 지켜요.
- 같은 인터페이스가 src/types/auth.ts에도 존재합니다.
- 타입을 재정의하면 불일치 위험이 커져 유지보수가 어려워집니다.
+import { RegisteredKakaoAuthReponse, UnregisteredKakaoAuthReponse } from "@/types/auth";
-// Kakao
-interface RegisteredKakaoAuthReponse {
- isRegistered: true;
- accessToken: string;
- refreshToken: string;
-}
-
-interface UnregisteredKakaoAuthReponse {
- isRegistered: false;
- nickname: string;
- email: string;
- profileImageUrl: string;
- signUpToken: string;
-}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| interface RegisteredKakaoAuthReponse { | |
| isRegistered: true; | |
| accessToken: string; | |
| refreshToken: string; | |
| } | |
| interface UnregisteredKakaoAuthReponse { | |
| isRegistered: false; | |
| nickname: string; | |
| email: string; | |
| profileImageUrl: string; | |
| signUpToken: string; | |
| } | |
| import { RegisteredKakaoAuthReponse, UnregisteredKakaoAuthReponse } from "@/types/auth"; |
🤖 Prompt for AI Agents
In src/api/auth/client/usePostKakaoAuth.ts around lines 11 to 23, you have
duplicate local interfaces (RegisteredKakaoAuthReponse and
UnregisteredKakaoAuthReponse) that already exist in src/types/auth.ts; remove
these local type definitions and import the shared types from src/types/auth.ts
instead, updating the import statement and any references to use the exact
exported type names (fixing any spelling mismatches) so the file relies on the
single source of truth and avoids drift.
| } else { | ||
| // 새로운 회원일 시 - 회원가입 페이지로 이동 | ||
| router.push(`/sign-up?token=${data.signUpToken}`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
쿼리 인코딩: signUpToken을 URL에 안전하게 전달해요.
- signUpToken에 예약문자가 포함되면 링크가 깨질 수 있습니다.
- encodeURIComponent로 방지하세요.
- router.push(`/sign-up?token=${data.signUpToken}`);
+ router.push(`/sign-up?token=${encodeURIComponent(data.signUpToken)}`);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } else { | |
| // 새로운 회원일 시 - 회원가입 페이지로 이동 | |
| router.push(`/sign-up?token=${data.signUpToken}`); | |
| } | |
| } else { | |
| // 새로운 회원일 시 - 회원가입 페이지로 이동 | |
| router.push(`/sign-up?token=${encodeURIComponent(data.signUpToken)}`); | |
| } |
🤖 Prompt for AI Agents
In src/api/auth/client/usePostKakaoAuth.ts around lines 46 to 49, the code
appends data.signUpToken directly into the query string which can break the URL
if the token contains reserved characters; update the router.push call to
URL-encode the token by passing encodeURIComponent(data.signUpToken) (and
defensively ensure data.signUpToken is defined before encoding) so the token is
transmitted safely in the query parameter.
| useEffect(() => { | ||
| const code = searchParams?.get("code"); | ||
| if (code) { | ||
| sendCodeToBackend(code); | ||
| postAppleAuth({ code }); | ||
| } | ||
| }, [searchParams]); | ||
| }, [searchParams, postAppleAuth]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
1) 이펙트 중복 호출 가드: Strict Mode에서 중복 전송을 막아요.
- 개발 환경에서 useEffect가 두 번 실행되어 동일 코드가 두 번 전송될 수 있어요.
- useRef 가드로 한 번만 호출되도록 해주세요.
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
@@
const { mutate: postAppleAuth } = usePostAppleAuth();
+ const calledRef = useRef(false);
@@
- useEffect(() => {
+ useEffect(() => {
const code = searchParams?.get("code");
- if (code) {
- postAppleAuth({ code });
+ if (!calledRef.current && code) {
+ calledRef.current = true;
+ postAppleAuth({ code });
}
- }, [searchParams, postAppleAuth]);
+ }, [searchParams, postAppleAuth]);🤖 Prompt for AI Agents
In src/app/login/apple/callback/AppleLoginCallbackPage.tsx around lines 14 to
20, the useEffect may run twice in React Strict Mode and cause duplicate
postAppleAuth calls; add a useRef boolean (e.g., calledRef) initialized to
false, check it inside the effect and only call postAppleAuth when
calledRef.current is false, then set calledRef.current = true immediately after
calling; ensure the ref is used in the dependency-free effect (or keep current
dependencies) so the postAppleAuth is executed only once during the component
lifecycle in development and production.
| useEffect(() => { | ||
| const code = searchParams?.get("code"); | ||
| if (code) { | ||
| sendCodeToBackend(code); | ||
| postKakaoAuth({ code }); | ||
| } | ||
| }, [searchParams]); | ||
| }, [searchParams, postKakaoAuth]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
중복 호출 방지: StrictMode/의존성 갱신으로 이중 로그인 요청이 발생할 수 있어요.
- useEffect는 React StrictMode와 searchParams 객체 갱신으로 두 번 이상 실행될 수 있어, 같은 code에 대해 postKakaoAuth가 중복 호출될 수 있습니다.
- code 단위로 한 번만 호출되도록 ref 가드를 추가해 주세요.
적용 예시:
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
const KakaoLoginCallbackPage = () => {
const searchParams = useSearchParams();
- const { mutate: postKakaoAuth } = usePostKakaoAuth();
+ const { mutate: postKakaoAuth } = usePostKakaoAuth();
+ const processedCodeRef = useRef<string | null>(null);
useEffect(() => {
const code = searchParams?.get("code");
- if (code) {
- postKakaoAuth({ code });
- }
+ if (code && processedCodeRef.current !== code) {
+ processedCodeRef.current = code;
+ postKakaoAuth({ code });
+ }
}, [searchParams, postKakaoAuth]);🤖 Prompt for AI Agents
In src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx around lines 14 to
20, the useEffect can call postKakaoAuth multiple times (React StrictMode or
searchParams updates) causing duplicate login requests; add a ref-based guard
that tracks which codes have been processed (e.g., a useRef storing last
processed code or a Set of processed codes), check the ref before calling
postKakaoAuth and only call when the code is present and not yet processed, then
mark the code as processed in the ref immediately after initiating the call so
the effect cannot trigger a duplicate request for the same code.
| const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.key === "Enter") { | ||
| handleEmailLogin(); | ||
| } | ||
| }; | ||
|
|
||
| const kakaoLogin = () => { | ||
| if (window.Kakao && window.Kakao.Auth) { | ||
| window.Kakao.Auth.authorize({ | ||
| redirectUri: `${process.env.NEXT_PUBLIC_WEB_URL}/login/kakao/callback`, | ||
| }); | ||
| } else { | ||
| alert("Kakao SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); | ||
| } | ||
| }; | ||
|
|
||
| const appleLogin = async () => { | ||
| if (!window.AppleID || !window.AppleID.auth) { | ||
| alert("Apple SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); | ||
| return; | ||
| } | ||
|
|
||
| window.AppleID.auth.init({ | ||
| clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID, | ||
| scope: process.env.NEXT_PUBLIC_APPLE_SCOPE, | ||
| redirectURI: `${process.env.NEXT_PUBLIC_WEB_URL}/login/apple/callback`, | ||
| usePopup: true, | ||
| }); | ||
|
|
||
| try { | ||
| const res: appleOAuth2CodeResponse = await window.AppleID.auth.signIn(); | ||
| if (res.authorization) { | ||
| router.push(`/login/apple/callback?code=${res.authorization.code}`); | ||
| } | ||
| } catch (error) { | ||
| console.log(error); | ||
| } | ||
| }; | ||
|
|
||
| const handleEmailLogin = async () => { | ||
| if (!email.trim()) { | ||
| alert("이메일을 입력해주세요."); | ||
| return; | ||
| } | ||
| if (!password) { | ||
| alert("비밀번호를 입력해주세요."); | ||
| return; | ||
| } | ||
| try { | ||
| const response = await emailAuthApi(email, password); | ||
| saveAccessToken(response.data.accessToken); | ||
| saveRefreshToken(response.data.refreshToken); | ||
| router.push("/"); | ||
| } catch (error: any) { | ||
| console.error(error); | ||
| alert(error.response?.data?.message || "로그인에 실패했습니다."); | ||
| handleSubmit(onSubmit)(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
중복 제출 방지: onKeyDown + form onSubmit 동시 사용은 이중 요청을 유발할 수 있어요.
- 입력 칸에서 Enter 시 keydown 핸들러와 브라우저 기본 submit이 모두 실행되어 postEmailAuth가 두 번 불릴 가능성이 있습니다.
- RHF의 만으로 Enter 제출이 동작하므로 onKeyDown을 제거해 주세요.
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- handleSubmit(onSubmit)();
- }
- };
...
- onKeyDown={handleKeyDown}
+ // Enter 제출은 form onSubmit으로 일원화
...
- onKeyDown={handleKeyDown}
+ // Enter 제출은 form onSubmit으로 일원화Also applies to: 80-81, 95-96
🤖 Prompt for AI Agents
In src/app/login/LoginContent.tsx around lines 48-52 (and similarly at 80-81 and
95-96), remove the onKeyDown Enter handler from the input elements because it
can cause duplicate submissions together with the form's onSubmit; rely solely
on RHF's <form onSubmit={handleSubmit(onSubmit)}> for Enter-to-submit behavior,
and ensure any lingering onKeyDown props are deleted or not passed so
postEmailAuth is only invoked once.
src/utils/authUtils.ts
Outdated
| export const kakaoLogin = () => { | ||
| if (window.Kakao && window.Kakao.Auth) { | ||
| window.Kakao.Auth.authorize({ | ||
| redirectUri: `${process.env.NEXT_PUBLIC_WEB_URL}/login/kakao/callback`, | ||
| }); | ||
| } else { | ||
| alert("Kakao SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요."); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
4) SSR 가드 추가: window/alert 접근 전에 typeof window 체크가 필요해요.
- SSR 환경에서 함수가 호출되면 window/alert 접근으로 크래시할 수 있어요.
- 간단한 가드로 서버에서 no-op 처리하면 안전합니다.
export const kakaoLogin = () => {
- if (window.Kakao && window.Kakao.Auth) {
+ if (typeof window === "undefined") return;
+ if (window.Kakao && window.Kakao.Auth) {
window.Kakao.Auth.authorize({
redirectUri: `${process.env.NEXT_PUBLIC_WEB_URL}/login/kakao/callback`,
});
} else {
alert("Kakao SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요.");
}
};
export const appleLogin = async () => {
- if (!window.AppleID || !window.AppleID.auth) {
+ if (typeof window === "undefined") return;
+ if (!window.AppleID || !window.AppleID.auth) {
alert("Apple SDK를 불러오는 중입니다. 잠시 후 다시 시도해주세요.");
return;
}덧: window.Kakao / window.AppleID는 전역 타입 선언이 없으면 TS 경고가 날 수 있어요. 전역 타입에 아래를 추가하는 것도 추천합니다.
- declare global interface Window { Kakao?: any; AppleID?: any; }
필요하시면 global.d.ts 패치도 제안드릴게요.
Also applies to: 33-54
🤖 Prompt for AI Agents
In src/utils/authUtils.ts around lines 23 to 31 (and also apply same change to
33-54): current code directly accesses window and alert which will crash under
SSR; add a typeof window !== "undefined" guard (and check typeof
window.Kakao/AppleID) so the function becomes a no-op on the server, and use a
safe client-only fallback that calls alert only when window is defined. Also add
(or recommend) a global type declaration like declare global interface Window {
Kakao?: any; AppleID?: any; } in global.d.ts to remove TypeScript warnings.
src/utils/authUtils.ts
Outdated
| }); | ||
|
|
||
| try { | ||
| const res: appleOAuth2CodeResponse = await window.AppleID.auth.signIn(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
2) 타입 누락: appleOAuth2CodeResponse import가 필요해요.
- 현재 파일에서 appleOAuth2CodeResponse를 사용하지만 import가 없어서 TS 에러가 납니다.
아래 한 줄을 상단에 추가해 주세요.
+import type { appleOAuth2CodeResponse } from "@/types/auth";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const res: appleOAuth2CodeResponse = await window.AppleID.auth.signIn(); | |
| // at the top of src/utils/authUtils.ts | |
| import type { appleOAuth2CodeResponse } from "@/types/auth"; |
🤖 Prompt for AI Agents
In src/utils/authUtils.ts around line 47, the type appleOAuth2CodeResponse is
used but not imported; add a top-level import for it such as: import type {
appleOAuth2CodeResponse } from '<the-apple-signin-library-your-project-uses>';
placing it with the other imports and using the correct module path that exports
that type.
src/utils/authUtils.ts
Outdated
| router.push(`/login/apple/callback?code=${res.authorization.code}`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3) router 미정의 사용: 유틸 파일에서 router.push는 동작하지 않아요.
- 해당 파일은 훅/컴포넌트가 아니어서 useRouter를 쓸 수 없고, 지금은 router 변수가 정의되어 있지 않아 런타임 에러가 납니다.
- 유틸에서는 window.location으로 내비게이션하는 편이 안전합니다.
아래처럼 교체해 주세요.
- router.push(`/login/apple/callback?code=${res.authorization.code}`);
+ window.location.href = `/login/apple/callback?code=${encodeURIComponent(res.authorization.code)}`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| router.push(`/login/apple/callback?code=${res.authorization.code}`); | |
| } | |
| window.location.href = `/login/apple/callback?code=${encodeURIComponent(res.authorization.code)}`; | |
| } |
🤖 Prompt for AI Agents
In src/utils/authUtils.ts around lines 49-50, router is undefined in this
non-component utility and router.push causes a runtime error; replace the
router-based navigation with a window.location navigation (e.g., use
window.location.href or window.location.assign with the same
/login/apple/callback URL including res.authorization.code) so the utility
performs a full-page redirect safely.
관련 이슈
작업 내용
특이 사항
빌드 시 SSR 환경에서 localStorage가 존재하지 않아 오류가 발생했었습니다.
이번 수정으로 정적 빌드가 정상적으로 완료됩니다.
빌드 전 localStorage 코드 제거를 깜빡하고 커밋하여 오류가 발생했습니다. 죄송합니다.