-
Notifications
You must be signed in to change notification settings - Fork 3
[Feature/#280] 네이버, 구글 로그인 연동 및 로그인 모달 설정 #284
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
📝 WalkthroughWalkthrough네이버 및 구글 OAuth 소셜 로그인을 지원하도록 시스템을 확장했습니다. 새로운 로그인 제공자 API를 추가하고, 기존 Kakao 로직을 일반화하며, 모달 기반 로그인 흐름으로 전환했습니다. 팟 필터링 파라미터도 Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App as React App
participant Modal as LoginModal
participant OAuth as OAuth Provider<br/>(Google/Naver/Kakao)
participant CallbackPage as Callback Page
participant Server as API Server
participant AppHome as Home/App
User->>App: 로그인 필요한 액션 실행
App->>App: 접근 토큰 확인
alt No Token
App->>Modal: LoginModal 열기
User->>Modal: OAuth 제공자 선택
Modal->>OAuth: OAuth 인증 URL로 리다이렉트
User->>OAuth: 인증 진행
OAuth-->>CallbackPage: code와 함께 리다이렉트<br/>(loginType 포함)
end
CallbackPage->>CallbackPage: useParams에서 loginType 추출
CallbackPage->>CallbackPage: useGetSignIn(loginType) 호출
CallbackPage->>Server: signInType 기반 로그인<br/>(Google/Naver/Kakao)
Server-->>CallbackPage: tokenServiceResponse 반환
CallbackPage->>AppHome: 토큰 저장 및 네비게이션
AppHome->>User: 홈 또는 회원가입 화면 표시
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
Sohyunnnn
left a comment
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: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/SignUp/components/CompleteModal/CompleteModal.tsx (1)
13-17: 회원가입 완료 모달의 동작 변경이 적절합니다.취소와 확인 버튼이 모두 홈으로 이동하도록 변경한 것은 회원가입 완료 시나리오에서 합리적인 UX입니다. 다만
onModalCancelprop이CompleteModal컴포넌트에서 사용되지 않고 있으므로 제거를 권장합니다.제거 대상:
CompleteModal.tsx라인 13-17:onModalCancelprop 선언 및 함수 파라미터 제거SignUp.tsx라인 119:CompleteModal호출 시onModalCancel={handleModalCancel}전달 제거♻️ 제안 변경사항
CompleteModal.tsx:
interface CompleteModalProps { - onModalCancel: () => void; } -function CompleteModal({ onModalCancel }: CompleteModalProps) { +function CompleteModal() { const navigate = useNavigate();SignUp.tsx:
- <CompleteModal onModalCancel={handleModalCancel} /> + <CompleteModal />
🤖 Fix all issues with AI agents
In @src/apis/hooks/pots/useGetPots.ts:
- Around line 5-15: The queryKey for useGetPots must include size and ensure
recruitmentRoles is normalized to null for stable caching: inside useGetPots
compute a normalizedRecruitmentRoles (e.g., const normalizedRecruitmentRoles =
recruitmentRoles ?? null or normalize empty/undefined arrays to null) and use
queryKey: ["pots", page, size, normalizedRecruitmentRoles, onlyMine]; keep the
queryFn calling GetPots with the original params (page, size, recruitmentRoles,
onlyMine) but use the normalized value only for the key.
In @src/apis/hooks/users/useGetSignIn.ts:
- Around line 10-19: The switch in mutationFn of useGetSignIn.ts currently falls
back to getNaverLogIn for any unknown or undefined signInType; update it to
validate signInType and handle unknown values explicitly by throwing an Error or
returning a rejected Promise with a clear message (e.g., "Unknown signInType:
<value>") instead of calling getNaverLogIn; keep the existing cases for "google"
-> getGoogleLogIn and "kakao" -> getKakaoLogIn and ensure callers can surface
the error for easier debugging.
In @src/components/commons/Modal/LoginModal/LoginModal.tsx:
- Around line 23-37: Replace the string-template OAuth URL construction for
googleLoginLink, kakaoLoginLink, and naverLoginLink with URLSearchParams-based
builders: build a params object including client_id, redirect_uri,
response_type, scope (where applicable) and a generated state value; append
params.toString() to the provider base auth URL, store the generated state (e.g.
in sessionStorage) for later callback verification, and ensure you URL-encode
redirect_uri/scope by relying on URLSearchParams so callbacks won’t break across
environments and CSRF state checks can be performed on callback handling.
In @src/pages/Callback/Callback.tsx:
- Around line 8-11: loginType from useParams must be validated before using
useGetSignIn/mutate: check that loginType is a non-empty string and one of the
allowed values (e.g., "google" or "naver"); if invalid or undefined, perform a
client redirect to home (or call navigate("/")) and do not call useGetSignIn or
trigger mutate. Ensure this validation occurs early in the Callback component
(before invoking useGetSignIn or using mutate with code) and keep
isCodeProcessed/code handling unchanged.
In @src/pages/Pots/pages/AllPot/AllPotPage.tsx:
- Around line 32-39: The empty if in handleClick causes clicks on "내가 만든 팟" when
not logged in to do nothing; replace it with an early return that triggers the
app's login flow (e.g., call showLoginModal() or openLoginModal() if available,
otherwise show a toast/alert) before returning, and only run setCurrentPage(1),
setSelectedCategory(partName) and setIsMyPot(partName === "내가 만든 팟") in the else
branch; ensure you reference the existing login modal/toast helper (or add one)
so the user is prompted to authenticate rather than seeing no response.
- Around line 25-30: partMap[selectedCategory] can be undefined for cases like
"전체보기", so pass null instead of undefined to the hook; update the useGetPots
call in AllPotPage to ensure recruitmentRoles is either a string or null (e.g.,
replace recruitmentRoles: partMap[selectedCategory] with recruitmentRoles:
partMap[selectedCategory] ?? null) so it matches GetPotsParams (string | null)
and stabilizes server/cache behavior; keep the rest of the parameters
(currentPage, size, onlyMine/isMyPot) unchanged.
In @src/routes/router.tsx:
- Around line 97-99: The callback route currently requires a loginType param
(path: `${routes.callback}/:loginType`) which will fail to match `/callback`
redirects; update the route definition so the loginType is optional (use
`:loginType?` in the path) or register an additional parallel route for
`${routes.callback}` that also renders CallbackPage; modify the entry where path
is defined (the route object referencing routes.callback and element:
<CallbackPage />) so the component loads for both `/callback` and
`/callback/:loginType`, preserving existing useGetSignIn behavior.
🧹 Nitpick comments (5)
src/pages/Pots/PotMain.tsx (1)
21-26: 탭 가드가 “막기만” 해서 UX가 끊기고, 라벨 문자열 의존이라 취약
tab.label === "지원한 팟"대신tab.path === routes.pot.applied로 가드 조건을 잡는 게 안전합니다.e.preventDefault()만 하면 무반응처럼 보일 수 있어서, 여기서 로그인 모달을 열거나(이미 다른 화면에서 쓰는 패턴처럼) 안내 토스트라도 띄우는 편이 좋아요.- 추가로, URL로 직접
/pot/applied진입하는 케이스는 여기서 못 막으니 페이지/라우트 레벨에서도 인증 가드가 있는지 확인이 필요합니다.src/apis/types/pot.ts (1)
40-45:recruitmentRoles이름(복수)과 params 타입(string | null) 의미 불일치 가능필터가 진짜 “복수 role”을 받는다면
string[]로 올리거나,string을 유지할 거면 CSV/구분자 규칙을 타입/주석으로 고정해 두는 게 안전합니다(백엔드 스펙과 불일치 시 필터가 조용히 깨질 수 있음).src/pages/Home/Home.tsx (1)
22-24: 모달 기반 로그인 유도 전환은 흐름이 깔끔합니다.비로그인 시 리다이렉트 대신
LoginModal을 여는 방식이 다른 화면들과도 패턴이 통일돼서 좋습니다. 다만 현재처럼localStorage.getItem("accessToken")존재만으로 분기하면 만료 토큰에서 UX가 꼬일 수 있어(“팟 만들러 가기” 이동 후 401 등), 가능하면 중앙 auth 상태/만료 검증 로직으로 점진적 정리만 고려해 주세요.Also applies to: 27-35, 70-72
src/apis/potAPI.ts (1)
32-44: 파라미터 rename 반영은 OK이며,recruitmentRoles는null정규화가 더 안전합니다.지금 변경은 타입/호출부와 일관성이 맞습니다. 런타임에서
recruitmentRoles가undefined로 들어올 여지가 있으면(상위 매핑 실패 등) 요청 파라미터가 흔들릴 수 있으니, 가능하면 여기서도recruitmentRoles: recruitmentRoles ?? null로 한 번 더 가드하는 걸 권장합니다(상위에서 이미 보장하면 생략 가능).src/components/layouts/SideBar/SideBar.tsx (1)
84-99: private route 접근 제어 로직 확인비로그인 사용자가 나의팟/채팅 메뉴 클릭 시 로그인 모달을 띄우는 로직이 올바르게 구현되었습니다.
⚡ 성능 최적화 제안 (선택사항)
accessToken체크를 map 내부에서 반복하는 대신 외부에서 한 번만 수행하도록 개선할 수 있습니다:+ const accessToken = localStorage.getItem("accessToken"); + return ( <> <div css={mainContainer(top)}> <div css={container}> {menuItems.map(({ to, icon, activeIcon, label }, index) => { - const accessToken = localStorage.getItem("accessToken"); const isPrivateRoute = to === routes.myPot.base || to === routes.chat;
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
src/apis/hooks/pots/useGetPots.tssrc/apis/hooks/users/useGetSignIn.tssrc/apis/potAPI.tssrc/apis/types/pot.tssrc/apis/userAPI.tssrc/components/cards/CtaCard/CtaCard.tsxsrc/components/commons/Modal/LoginModal/LoginModal.tsxsrc/components/layouts/SideBar/SideBar.tsxsrc/pages/Callback/Callback.tsxsrc/pages/Home/Home.tsxsrc/pages/Home/components/Feed/Feed.tsxsrc/pages/Pots/PotMain.tsxsrc/pages/Pots/pages/AllPot/AllPotPage.tsxsrc/pages/SignUp/components/CompleteModal/CompleteModal.tsxsrc/routes/router.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-18T19:02:42.735Z
Learnt from: yuseoyeon
Repo: STACKPOT/STACKPOT-FE PR: 265
File: src/apis/types/pot.ts:103-108
Timestamp: 2025-08-18T19:02:42.735Z
Learning: GetPotApplicationResponse 타입의 potRole 필드가 Role에서 PotRole 객체로 변경되었을 때, 실제 사용처는 ApplicantsInformation.tsx와 StartPotModal.tsx에서 applicant.potRole.name 형태로 접근하는 부분들이며, 다른 컴포넌트의 potRole 변수들과 혼동하지 않아야 함
Applied to files:
src/apis/types/pot.tssrc/apis/potAPI.tssrc/pages/Pots/pages/AllPot/AllPotPage.tsxsrc/apis/hooks/pots/useGetPots.ts
🧬 Code graph analysis (7)
src/apis/userAPI.ts (2)
src/apis/axios/apiUtils.ts (1)
apiGet(5-11)src/apis/types/user.ts (1)
LogInResponse(5-9)
src/components/layouts/SideBar/SideBar.tsx (10)
src/components/layouts/Header/Header.style.ts (1)
iconStyle(49-54)src/pages/Home/components/Feed/Feed.style.ts (1)
iconStyle(36-40)src/components/layouts/SideBar/SideBar.style.ts (4)
iconStyle(29-32)potIconStyle(34-37)mainContainer(5-14)container(16-27)src/pages/SignUp/components/Section/Section.style.ts (2)
potIconStyle(19-23)container(4-9)src/pages/SignUp/SignUp.style.ts (2)
mainContainer(10-16)container(4-9)src/pages/SignUp/components/ContractsSection/ContractsSection.style.ts (1)
container(4-9)src/pages/Pots/PotMain.style.ts (1)
container(4-9)src/pages/MyPage/MyPage.style.ts (1)
container(4-13)src/components/commons/TemperatureBar/TemperatureBar.style.ts (1)
container(4-10)src/pages/SignUp/components/CategorySelection/CategorySelection.style.ts (1)
container(4-8)
src/apis/potAPI.ts (2)
src/apis/types/pot.ts (2)
GetPotsParams(40-45)PotsResponse(47-51)src/apis/axios/apiUtils.ts (1)
authApiGet(21-27)
src/pages/Pots/pages/AllPot/AllPotPage.tsx (1)
src/constants/categories.ts (1)
partMap(21-28)
src/components/cards/CtaCard/CtaCard.tsx (1)
src/components/cards/CtaCard/CtaCard.style.ts (4)
container(4-19)profileImageStyle(20-25)bodyTextStyle(26-31)buttonContainer(33-35)
src/apis/hooks/users/useGetSignIn.ts (1)
src/apis/userAPI.ts (3)
getGoogleLogIn(31-33)getKakaoLogIn(35-37)getNaverLogIn(39-41)
src/apis/hooks/pots/useGetPots.ts (2)
src/apis/types/pot.ts (1)
GetPotsParams(40-45)src/apis/potAPI.ts (1)
GetPots(32-44)
🔇 Additional comments (5)
src/pages/Home/components/Feed/Feed.tsx (1)
25-26: 모달 기반 로그인 유도 전환은 일관되고 자연스러움비로그인 시
navigate대신LoginModal을 띄우는 흐름으로 바뀐 점은 UX 측면에서 좋아요. 다만 토큰을localStorage.getItem(...)로 즉시 조회하는 구조라, “로그인 완료 후” 이 화면이 어떻게 갱신되는지(리다이렉트/리로드로 자연스럽게 재마운트 되는지)만 한 번 확인해 주세요.Also applies to: 50-51, 66-73, 175-177
src/components/cards/CtaCard/CtaCard.tsx (1)
10-12: 버튼 클릭 시handleClick이 2번 호출되는 버그 가능(버블링)현재 카드 컨테이너와 내부 버튼 모두
onClick={handleClick}이라 버튼을 누르면 버블링으로 핸들러가 중복 호출될 수 있습니다(로그인 모달/네비게이션이 2번 트리거). 한쪽만 남기거나 버튼에서stopPropagation()처리 권장입니다.🐛 제안 수정안(버튼에서 버블링 차단)
- <div css={buttonContainer}> - <Button variant="cta" onClick={handleClick}> + <div css={buttonContainer}> + <Button + variant="cta" + onClick={(e) => { + e.stopPropagation(); + handleClick(); + }} + > {type == "feed" ? "피드 작성" : "팟 만들기"} </Button> </div>추가로
<img ... />에는alt를 넣어주고,type == "feed"은type === "feed"로 맞추는 편이 좋아요.Also applies to: 20-34, 37-54
⛔ Skipped due to learnings
Learnt from: yuseoyeon Repo: STACKPOT/STACKPOT-FE PR: 265 File: src/components/cards/PotDetailCard/MyPotCard.tsx:109-118 Timestamp: 2025-08-19T10:45:00.586Z Learning: Button 컴포넌트(src/components/commons/Button/Button.tsx)는 내부적으로 onClick 핸들러에서 e.stopPropagation()을 호출하여 이벤트 버블링을 자동으로 방지함src/apis/userAPI.ts (2)
31-33: 구글 로그인 API 구현 확인 완료기존 Kakao 로그인과 동일한 패턴으로 구현되어 일관성이 유지되고 있습니다.
39-41: 네이버 로그인 API 구현 확인 완료기존 OAuth 로그인 함수들과 동일한 패턴으로 구현되어 코드 일관성이 유지됩니다.
src/components/layouts/SideBar/SideBar.tsx (1)
23-23: 로그인 모달 상태 관리 구현 확인LoginModal 컴포넌트를 import하고
isLoginModalOpen상태로 제어하는 구조가 명확합니다.Also applies to: 54-54
| const useGetPots = ({ | ||
| page, | ||
| size, | ||
| recruitmentRole, | ||
| recruitmentRoles, | ||
| onlyMine, | ||
| }: GetPotsParams) => { | ||
| return useQuery({ | ||
| queryKey: ["pots", page, recruitmentRole, onlyMine], | ||
| queryFn: () => GetPots({ page, size, recruitmentRole, onlyMine }), | ||
| queryKey: ["pots", page, recruitmentRoles, onlyMine], | ||
| queryFn: () => GetPots({ page, size, recruitmentRoles, onlyMine }), | ||
| select: (data) => data.result, | ||
| }); |
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.
queryKey에 size를 포함하고 recruitmentRoles는 null로 정규화하세요.
현재 queryKey: ["pots", page, recruitmentRoles, onlyMine]는 size가 달라져도 같은 캐시를 재사용할 수 있어 데이터가 뒤섞일 수 있습니다(특히 다른 페이지/컴포넌트에서 size를 다르게 쓰면 재현). 또한 recruitmentRoles가 런타임에서 undefined가 될 여지가 있으면(상위 매핑 실패) 키 안정성이 떨어집니다.
제안 diff
return useQuery({
- queryKey: ["pots", page, recruitmentRoles, onlyMine],
- queryFn: () => GetPots({ page, size, recruitmentRoles, onlyMine }),
+ queryKey: ["pots", page, size, recruitmentRoles ?? null, onlyMine],
+ queryFn: () => GetPots({ page, size, recruitmentRoles: recruitmentRoles ?? null, onlyMine }),
select: (data) => data.result,
});📝 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 useGetPots = ({ | |
| page, | |
| size, | |
| recruitmentRole, | |
| recruitmentRoles, | |
| onlyMine, | |
| }: GetPotsParams) => { | |
| return useQuery({ | |
| queryKey: ["pots", page, recruitmentRole, onlyMine], | |
| queryFn: () => GetPots({ page, size, recruitmentRole, onlyMine }), | |
| queryKey: ["pots", page, recruitmentRoles, onlyMine], | |
| queryFn: () => GetPots({ page, size, recruitmentRoles, onlyMine }), | |
| select: (data) => data.result, | |
| }); | |
| const useGetPots = ({ | |
| page, | |
| size, | |
| recruitmentRoles, | |
| onlyMine, | |
| }: GetPotsParams) => { | |
| return useQuery({ | |
| queryKey: ["pots", page, size, recruitmentRoles ?? null, onlyMine], | |
| queryFn: () => GetPots({ page, size, recruitmentRoles: recruitmentRoles ?? null, onlyMine }), | |
| select: (data) => data.result, | |
| }); |
🤖 Prompt for AI Agents
In @src/apis/hooks/pots/useGetPots.ts around lines 5 - 15, The queryKey for
useGetPots must include size and ensure recruitmentRoles is normalized to null
for stable caching: inside useGetPots compute a normalizedRecruitmentRoles
(e.g., const normalizedRecruitmentRoles = recruitmentRoles ?? null or normalize
empty/undefined arrays to null) and use queryKey: ["pots", page, size,
normalizedRecruitmentRoles, onlyMine]; keep the queryFn calling GetPots with the
original params (page, size, recruitmentRoles, onlyMine) but use the normalized
value only for the key.
| const googleLoginLink = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${ | ||
| import.meta.env.VITE_REST_GOOGLE_API_KEY | ||
| }&redirect_uri=${ | ||
| import.meta.env.VITE_GOOGLE_REDIRECT_URI | ||
| }&response_type=code&scope=email`; | ||
|
|
||
| const kakaoLoginLink = `https://kauth.kakao.com/oauth/authorize?client_id=${ | ||
| import.meta.env.VITE_REST_API_KEY | ||
| }&redirect_uri=${import.meta.env.VITE_REDIRECT_URI}&response_type=code | ||
| &scope=account_email | ||
| &prompt=login`; | ||
| const naverLoginLink = ""; | ||
|
|
||
| const naverLoginLink = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${ | ||
| import.meta.env.VITE_REST_NAVER_API_KEY | ||
| }&redirect_uri=${import.meta.env.VITE_NAVER_REDIRECT_URI}`; |
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.
OAuth URL은 URLSearchParams로 구성하고 redirect_uri/scope/state를 보장하세요.
현재 문자열 템플릿 방식은 redirect_uri/scope 인코딩 누락으로 로컬/배포 환경별로 깨지기 쉽고, (특히 Naver는) state 누락 시 동작 실패 또는 CSRF 방어 공백이 생길 수 있습니다. URLSearchParams + state(생성 후 sessionStorage 등에 저장, 콜백에서 검증)로 통일하는 편이 안전합니다.
제안 diff (개념 예시)
const LoginModal: React.FC<LoginModalProps> = ({ onCancel }) => {
- const googleLoginLink = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
- import.meta.env.VITE_REST_GOOGLE_API_KEY
- }&redirect_uri=${
- import.meta.env.VITE_GOOGLE_REDIRECT_URI
- }&response_type=code&scope=email`;
+ const oauthState = crypto.randomUUID();
+ sessionStorage.setItem("oauth_state", oauthState);
+
+ const googleLoginLink = `https://accounts.google.com/o/oauth2/v2/auth?${new URLSearchParams({
+ client_id: import.meta.env.VITE_REST_GOOGLE_API_KEY, // (명칭은 client_id인지 확인)
+ redirect_uri: import.meta.env.VITE_GOOGLE_REDIRECT_URI,
+ response_type: "code",
+ scope: "openid email", // 서버 요구 스코프에 맞게 조정
+ state: oauthState,
+ }).toString()}`;
const kakaoLoginLink = `https://kauth.kakao.com/oauth/authorize?client_id=${
import.meta.env.VITE_REST_API_KEY
}&redirect_uri=${import.meta.env.VITE_REDIRECT_URI}&response_type=code
&scope=account_email
&prompt=login`;
- const naverLoginLink = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${
- import.meta.env.VITE_REST_NAVER_API_KEY
- }&redirect_uri=${import.meta.env.VITE_NAVER_REDIRECT_URI}`;
+ const naverLoginLink = `https://nid.naver.com/oauth2.0/authorize?${new URLSearchParams({
+ response_type: "code",
+ client_id: import.meta.env.VITE_REST_NAVER_API_KEY,
+ redirect_uri: import.meta.env.VITE_NAVER_REDIRECT_URI,
+ state: oauthState,
+ }).toString()}`;📝 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 googleLoginLink = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${ | |
| import.meta.env.VITE_REST_GOOGLE_API_KEY | |
| }&redirect_uri=${ | |
| import.meta.env.VITE_GOOGLE_REDIRECT_URI | |
| }&response_type=code&scope=email`; | |
| const kakaoLoginLink = `https://kauth.kakao.com/oauth/authorize?client_id=${ | |
| import.meta.env.VITE_REST_API_KEY | |
| }&redirect_uri=${import.meta.env.VITE_REDIRECT_URI}&response_type=code | |
| &scope=account_email | |
| &prompt=login`; | |
| const naverLoginLink = ""; | |
| const naverLoginLink = `https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=${ | |
| import.meta.env.VITE_REST_NAVER_API_KEY | |
| }&redirect_uri=${import.meta.env.VITE_NAVER_REDIRECT_URI}`; | |
| const oauthState = crypto.randomUUID(); | |
| sessionStorage.setItem("oauth_state", oauthState); | |
| const googleLoginLink = `https://accounts.google.com/o/oauth2/v2/auth?${new URLSearchParams({ | |
| client_id: import.meta.env.VITE_REST_GOOGLE_API_KEY, | |
| redirect_uri: import.meta.env.VITE_GOOGLE_REDIRECT_URI, | |
| response_type: "code", | |
| scope: "openid email", | |
| state: oauthState, | |
| }).toString()}`; | |
| const kakaoLoginLink = `https://kauth.kakao.com/oauth/authorize?client_id=${ | |
| import.meta.env.VITE_REST_API_KEY | |
| }&redirect_uri=${import.meta.env.VITE_REDIRECT_URI}&response_type=code | |
| &scope=account_email | |
| &prompt=login`; | |
| const naverLoginLink = `https://nid.naver.com/oauth2.0/authorize?${new URLSearchParams({ | |
| response_type: "code", | |
| client_id: import.meta.env.VITE_REST_NAVER_API_KEY, | |
| redirect_uri: import.meta.env.VITE_NAVER_REDIRECT_URI, | |
| state: oauthState, | |
| }).toString()}`; |
🤖 Prompt for AI Agents
In @src/components/commons/Modal/LoginModal/LoginModal.tsx around lines 23 - 37,
Replace the string-template OAuth URL construction for googleLoginLink,
kakaoLoginLink, and naverLoginLink with URLSearchParams-based builders: build a
params object including client_id, redirect_uri, response_type, scope (where
applicable) and a generated state value; append params.toString() to the
provider base auth URL, store the generated state (e.g. in sessionStorage) for
later callback verification, and ensure you URL-encode redirect_uri/scope by
relying on URLSearchParams so callbacks won’t break across environments and CSRF
state checks can be performed on callback handling.
| const { loginType } = useParams(); | ||
| const isCodeProcessed = useRef(false); | ||
| const code = new URL(window.location.href).searchParams.get("code"); | ||
| const { mutate } = useGetSignIn(); | ||
| const { mutate } = useGetSignIn(loginType); |
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.
🧩 Analysis chain
🏁 Script executed:
fd "Callback\.tsx" --type fRepository: STACKPOT/STACKPOT-FE
Length of output: 95
🏁 Script executed:
cat -n src/pages/Callback/Callback.tsxRepository: STACKPOT/STACKPOT-FE
Length of output: 1019
🏁 Script executed:
fd "useGetSignIn" --type fRepository: STACKPOT/STACKPOT-FE
Length of output: 100
🏁 Script executed:
cat -n src/apis/hooks/users/useGetSignIn.tsRepository: STACKPOT/STACKPOT-FE
Length of output: 1344
🏁 Script executed:
rg "Callback" --type tsx --type ts -B 2 -A 2 | head -50Repository: STACKPOT/STACKPOT-FE
Length of output: 91
🏁 Script executed:
rg "Callback" -B 2 -A 2 | head -50Repository: STACKPOT/STACKPOT-FE
Length of output: 1746
🏁 Script executed:
cat -n src/routes/router.tsx | grep -A 5 -B 5 "callback"Repository: STACKPOT/STACKPOT-FE
Length of output: 454
🏁 Script executed:
rg "callback" -i src/constants/routesRepository: STACKPOT/STACKPOT-FE
Length of output: 124
🏁 Script executed:
fd "routes" -type f | grep -i constRepository: STACKPOT/STACKPOT-FE
Length of output: 234
🏁 Script executed:
fd "routes" --type file | grep -i constRepository: STACKPOT/STACKPOT-FE
Length of output: 87
🏁 Script executed:
cat -n src/constants/routes.tsRepository: STACKPOT/STACKPOT-FE
Length of output: 1066
🏁 Script executed:
rg "useParams" src/pages/Callback/Callback.tsx -A 3 -B 3Repository: STACKPOT/STACKPOT-FE
Length of output: 506
🏁 Script executed:
rg "navigate.*callback\|href.*callback\|to.*callback" -iRepository: STACKPOT/STACKPOT-FE
Length of output: 46
🏁 Script executed:
rg "callback" -i | grep -E "href|to=|navigate|link" | head -20Repository: STACKPOT/STACKPOT-FE
Length of output: 46
🏁 Script executed:
rg "routes\.callback|/callback" -B 3 -A 3Repository: STACKPOT/STACKPOT-FE
Length of output: 1046
🏁 Script executed:
rg "google|kakao|naver" -i src/pages/Callback/ src/apis/hooks/users/Repository: STACKPOT/STACKPOT-FE
Length of output: 522
loginType 유효성 검증 필수
URL 파라미터로 받은 loginType이 undefined이거나 유효하지 않은 값일 경우, useGetSignIn 내부의 switch 문에서 기본값으로 네이버 로그인을 실행합니다. 예를 들어, 사용자가 /callback/google이 아닌 /callback으로 이동하거나 잘못된 파라미터를 전달하면 자동으로 네이버 로그인이 시도되어 인증 실패로 이어집니다.
사용자가 구글 로그인을 시도했으나 loginType이 undefined 또는 잘못된 값인 경우, 예상과 다른 네이버 로그인이 실행되어 인증 오류가 발생할 수 있습니다. 이를 방지하기 위해 loginType의 유효성을 검증하고 유효하지 않은 경우 홈으로 리디렉션해야 합니다.
🛡️ loginType 검증 로직 제안
const Callback: React.FC = () => {
const { loginType } = useParams();
const isCodeProcessed = useRef(false);
const code = new URL(window.location.href).searchParams.get("code");
+
+ // loginType 유효성 검증
+ useEffect(() => {
+ if (!loginType || !["google", "kakao", "naver"].includes(loginType)) {
+ console.error("Invalid login type:", loginType);
+ navigate(routes.home);
+ }
+ }, [loginType, navigate]);
+
const { mutate } = useGetSignIn(loginType);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @src/pages/Callback/Callback.tsx around lines 8 - 11, loginType from
useParams must be validated before using useGetSignIn/mutate: check that
loginType is a non-empty string and one of the allowed values (e.g., "google" or
"naver"); if invalid or undefined, perform a client redirect to home (or call
navigate("/")) and do not call useGetSignIn or trigger mutate. Ensure this
validation occurs early in the Callback component (before invoking useGetSignIn
or using mutate with code) and keep isCodeProcessed/code handling unchanged.
| const { data } = useGetPots({ | ||
| page: currentPage, | ||
| size: 9, | ||
| recruitmentRole: partMap[selectedCategory], | ||
| recruitmentRoles: partMap[selectedCategory], | ||
| onlyMine: isMyPot, | ||
| }); |
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.
recruitmentRoles에 undefined가 들어가지 않게 null 처리하세요.
partMap[selectedCategory]는 selectedCategory === "전체보기"(기본값) 같은 케이스에서 런타임 undefined가 될 수 있어요. GetPotsParams.recruitmentRoles가 string | null인 만큼, “전체보기/내가 만든 팟”은 null로 내려 서버/캐시 동작을 안정화하는 편이 안전합니다.
제안 diff
const { data } = useGetPots({
page: currentPage,
size: 9,
- recruitmentRoles: partMap[selectedCategory],
+ recruitmentRoles: partMap[selectedCategory] ?? null,
onlyMine: isMyPot,
});🤖 Prompt for AI Agents
In @src/pages/Pots/pages/AllPot/AllPotPage.tsx around lines 25 - 30,
partMap[selectedCategory] can be undefined for cases like "전체보기", so pass null
instead of undefined to the hook; update the useGetPots call in AllPotPage to
ensure recruitmentRoles is either a string or null (e.g., replace
recruitmentRoles: partMap[selectedCategory] with recruitmentRoles:
partMap[selectedCategory] ?? null) so it matches GetPotsParams (string | null)
and stabilizes server/cache behavior; keep the rest of the parameters
(currentPage, size, onlyMine/isMyPot) unchanged.
| path: `${routes.callback}/:loginType`, | ||
| element: <CallbackPage />, | ||
| }, |
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.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the package.json to confirm react-router-dom version
find . -name "package.json" -type f | head -5Repository: STACKPOT/STACKPOT-FE
Length of output: 78
🏁 Script executed:
# Let's examine the router.tsx file
find . -name "router.tsx" -type fRepository: STACKPOT/STACKPOT-FE
Length of output: 87
🏁 Script executed:
# Search for OAuth configuration and callback route setup
rg "callback|oauth|loginType" -i --type tsx --type ts -A 3Repository: STACKPOT/STACKPOT-FE
Length of output: 91
🌐 Web query:
react-router-dom 7.1.1 optional parameters syntax support :param?
💡 Result:
Yes. React Router v7 (including 7.1.1) supports optional path segments using a trailing ? on the segment — e.g. "/:lang?/categories" or "/blog/:id?" will match with or without that segment. [1][2]
Examples:
- <Route path="/:lang?/categories" … /> matches /categories and /en/categories. [1]
- <Route path="/blog/:id?" … /> matches /blog and /blog/10. [1]
Sources: React Router docs on optional segments and the v7 changelog/release (7.1.1). [1][2]
🏁 Script executed:
cat package.json | grep -A 2 -B 2 "react-router"Repository: STACKPOT/STACKPOT-FE
Length of output: 231
🏁 Script executed:
cat -n ./src/routes/router.tsx | sed -n '80,110p'Repository: STACKPOT/STACKPOT-FE
Length of output: 1072
🏁 Script executed:
# Search for OAuth callback configurations and redirect URIs
grep -r "redirect_uri\|callback\|loginType" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | head -20Repository: STACKPOT/STACKPOT-FE
Length of output: 747
🏁 Script executed:
# Check for routes.callback definition and any related config
grep -r "routes\." --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/null | grep -i "callback\|constant" | head -15Repository: STACKPOT/STACKPOT-FE
Length of output: 219
🏁 Script executed:
# Search for .env files and environment configuration
find . -name ".env*" -type f 2>/dev/null | head -10Repository: STACKPOT/STACKPOT-FE
Length of output: 46
🏁 Script executed:
# Check for any .env.example or similar
cat .env.example 2>/dev/null || cat .env 2>/dev/null || echo "No .env found in root"Repository: STACKPOT/STACKPOT-FE
Length of output: 85
🏁 Script executed:
# Search for VITE_REDIRECT_URI references to understand the actual redirect URL pattern
grep -r "VITE_REDIRECT_URI\|VITE_NAVER_REDIRECT_URI" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" . 2>/dev/nullRepository: STACKPOT/STACKPOT-FE
Length of output: 312
🏁 Script executed:
# Check the LoginModal to see exactly how redirect_uri is constructed
cat -n ./src/components/commons/Modal/LoginModal/LoginModal.tsx | grep -A 5 -B 5 "redirect_uri"Repository: STACKPOT/STACKPOT-FE
Length of output: 1041
🏁 Script executed:
# Check if there's a git history showing if plain /callback route was removed
git log --oneline --all -20 2>/dev/null | head -20Repository: STACKPOT/STACKPOT-FE
Length of output: 989
🏁 Script executed:
# Check the Callback component to see how it handles loginType
cat -n ./src/pages/Callback/Callback.tsx | head -50Repository: STACKPOT/STACKPOT-FE
Length of output: 1019
🏁 Script executed:
# Search for useGetSignIn hook to see what loginType values it expects
find . -name "*useGetSignIn*" -o -name "*SignIn*" | grep -i hookRepository: STACKPOT/STACKPOT-FE
Length of output: 143
🏁 Script executed:
# Look for test files or documentation mentioning callback route
find . -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "callback\|loginType" 2>/dev/null | head -5Repository: STACKPOT/STACKPOT-FE
Length of output: 46
🏁 Script executed:
# Search for how loginType is determined or what values it can have
grep -r "loginType\|provider" --include="*.ts" --include="*.tsx" . 2>/dev/null | grep -v node_modules | head -20Repository: STACKPOT/STACKPOT-FE
Length of output: 1780
🏁 Script executed:
# Check the useGetSignIn hook implementation
cat -n ./src/apis/hooks/users/useGetSignIn.tsRepository: STACKPOT/STACKPOT-FE
Length of output: 1344
🏁 Script executed:
# Search for where loginType values are set or defined (e.g., 'google', 'naver', 'kakao')
grep -r "google\|naver\|kakao" --include="*.ts" --include="*.tsx" . 2>/dev/null | grep -v node_modules | grep -i "type\|provider\|login" | head -15Repository: STACKPOT/STACKPOT-FE
Length of output: 1622
🏁 Script executed:
# Check git diff or recent changes to understand if this was a recent modification
git log --oneline -p -- ./src/routes/router.tsx 2>/dev/null | head -50Repository: STACKPOT/STACKPOT-FE
Length of output: 1077
콜백 라우트 호환성 깨질 위험 - :loginType 필수 파라미터 문제
path: \${routes.callback}/:loginType`은 loginType을 필수 파라미터로 요구합니다. OAuth 제공자(Google, Kakao, Naver)의 redirect_uri가 /callback형태로 설정되어 있거나, 경로에loginType`이 포함되지 않으면 라우트가 매칭되지 않아 NotFound로 떨어집니다.
useGetSignIn이 undefined를 받을 수 있도록 설계되었지만(기본값: naver), 라우트 매칭이 실패하면 컴포넌트 자체가 로드되지 않으므로 이 방어 장치는 작동하지 않습니다.
react-router-dom 7.1.1은 optional 파라미터 문법(?)을 지원하므로, 다음 중 하나로 수정할 것을 권장합니다:
✅ 권장 해결책
- path: `${routes.callback}/:loginType`,
+ path: `${routes.callback}/:loginType?`,이렇게 하면 /callback과 /callback/google 모두 매칭됩니다.
또는 하위호환을 위해 두 라우트를 병렬 등록:
+ { path: routes.callback, element: <CallbackPage /> },
{ path: `${routes.callback}/:loginType`, element: <CallbackPage /> },📝 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.
| path: `${routes.callback}/:loginType`, | |
| element: <CallbackPage />, | |
| }, | |
| path: `${routes.callback}/:loginType?`, | |
| element: <CallbackPage />, | |
| }, |
🤖 Prompt for AI Agents
In @src/routes/router.tsx around lines 97 - 99, The callback route currently
requires a loginType param (path: `${routes.callback}/:loginType`) which will
fail to match `/callback` redirects; update the route definition so the
loginType is optional (use `:loginType?` in the path) or register an additional
parallel route for `${routes.callback}` that also renders CallbackPage; modify
the entry where path is defined (the route object referencing routes.callback
and element: <CallbackPage />) so the component loads for both `/callback` and
`/callback/:loginType`, preserving existing useGetSignIn behavior.
💻 관련 이슈
💡 작업내용
🧐 참고 사항
🖼️ 스크린샷 or 실행영상
2026-01-09.140556.mp4
️✅ 체크리스트 (PR 올리기 전 아래 내용을 확인해 주세요)