Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/apis/hooks/pots/useGetPots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import { GetPotsParams } from "apis/types/pot";
const useGetPots = ({
page,
size,
recruitmentRole,
recruitmentRoles,
onlyMine,
}: GetPotsParams) => {
return useQuery({
queryKey: ["pots", page, recruitmentRole, onlyMine],
queryFn: () => GetPots({ page, size, recruitmentRole, onlyMine }),
queryKey: ["pots", page, size, recruitmentRoles ?? null, onlyMine],
queryFn: () =>
GetPots({
page,
size,
recruitmentRoles: recruitmentRoles ?? null,
onlyMine,
}),
select: (data) => data.result,
});
};
Expand Down
17 changes: 14 additions & 3 deletions src/apis/hooks/users/useGetSignIn.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import routes from "@constants/routes";
import { useMutation } from "@tanstack/react-query";
import { getKakaoLogIn } from "apis/userAPI";
import { getGoogleLogIn, getKakaoLogIn, getNaverLogIn } from "apis/userAPI";
import { useNavigate } from "react-router-dom";

const useGetSignIn = () => {
const useGetSignIn = (signInType: string | undefined) => {
const navigate = useNavigate();

return useMutation({
mutationFn: (code: string) => getKakaoLogIn(code),
mutationFn: (code: string) => {
switch (signInType) {
case "google":
return getGoogleLogIn(code);
case "kakao":
return getKakaoLogIn(code);
case "naver":
return getNaverLogIn(code);
default:
throw new Error(`Invalid login type: ${signInType}`);
}
},
onSuccess: (data) => {
if (data.result) {
const { accessToken, refreshToken } = data.result.tokenServiceResponse;
Expand Down
4 changes: 2 additions & 2 deletions src/apis/potAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ export const PostPot = async (postPotParams: PostPotParams) => {
export const GetPots = async ({
page,
size,
recruitmentRole,
recruitmentRoles,
onlyMine,
}: GetPotsParams) => {
return authApiGet<PotsResponse>("pots", {
page,
size,
recruitmentRole,
recruitmentRoles,
onlyMine,
});
};
Expand Down
2 changes: 1 addition & 1 deletion src/apis/types/pot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface RecruitmentDetailResponse {
export interface GetPotsParams {
page: number;
size: number;
recruitmentRole: string | null;
recruitmentRoles: string | null;
onlyMine: boolean;
}

Expand Down
17 changes: 17 additions & 0 deletions src/apis/userAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,18 @@ import {
PostPotResponse,
} from "./types/pot";

export const getGoogleLogIn = async (code: string) => {
return apiGet<LogInResponse>("/users/oauth/google", { code });
};

export const getKakaoLogIn = async (code: string) => {
return apiGet<LogInResponse>("/users/oauth/kakao", { code });
};

export const getNaverLogIn = async (code: string) => {
return apiGet<LogInResponse>("/users/oauth/naver", { code });
};

export const GetMyUser = async () => {
return authApiGet<GetUserResponse>("/users");
};
Expand All @@ -53,6 +61,15 @@ export const postNickname = async (nickname: string) => {
});
};

// export const GetMyPage = async ({ dataType }: GetMyPageParams) => {
// if (dataType === "feed") {
// return authApiGet<MyPageResponse>("/users/mypages");
// } else if (dataType === "pot") {
// return authApiGet<MyPageResponse>("/users/mypages", { dataType });
// } else {
// return authApiGet<DescriptionResponse>("/users/description");
// }
// };
export const getMyPageFeeds = async ({
nextCursor,
size,
Expand Down
38 changes: 24 additions & 14 deletions src/components/cards/CtaCard/CtaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from "./CtaCard.style";
import { useNavigate } from "react-router-dom";
import routes from "@constants/routes";
import { useState } from "react";
import LoginModal from "@components/commons/Modal/LoginModal/LoginModal";
import { SproutImage } from "@assets/images";

interface CtaCardProps {
Expand All @@ -15,10 +17,13 @@ interface CtaCardProps {

const CtaCard: React.FC<CtaCardProps> = ({ type }: CtaCardProps) => {
const navigate = useNavigate();
const accessToken = localStorage.getItem("accessToken");
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);

const handleClick = () => {
if (accessToken) {
const token = localStorage.getItem("accessToken");
if (token === null) {
setIsLoginModalOpen(true);
} else {
if (type === "feed") {
navigate(routes.writePost);
} else if (type === "pot") {
Expand All @@ -29,19 +34,24 @@ const CtaCard: React.FC<CtaCardProps> = ({ type }: CtaCardProps) => {
};

return (
<div css={container} onClick={handleClick}>
<img css={profileImageStyle} src={SproutImage} />
<p css={bodyTextStyle}>
{type == "feed"
? "오늘 작업하다가 무슨 일이 있었냐면..."
: "꿈을 현실로 옮길 시간이에요. 팟을 만들고 팀원을 모집해 볼까요?"}
</p>
<div css={buttonContainer}>
<Button variant="cta" onClick={handleClick}>
{type == "feed" ? "피드 작성" : "팟 만들기"}
</Button>
<>
<div css={container} onClick={handleClick}>
<img css={profileImageStyle} src={SproutImage} />
<p css={bodyTextStyle}>
{type == "feed"
? "오늘 작업하다가 무슨 일이 있었냐면..."
: "꿈을 현실로 옮길 시간이에요. 팟을 만들고 팀원을 모집해 볼까요?"}
</p>
<div css={buttonContainer}>
<Button variant="cta" onClick={handleClick}>
{type == "feed" ? "피드 작성" : "팟 만들기"}
</Button>
</div>
</div>
</div>
{isLoginModalOpen && (
<LoginModal onCancel={() => setIsLoginModalOpen(false)} />
)}
</>
);
};
export default CtaCard;
12 changes: 10 additions & 2 deletions src/components/commons/Modal/LoginModal/LoginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ interface LoginModalProps {
}

const LoginModal: React.FC<LoginModalProps> = ({ onCancel }) => {
const googleLoginLink = "";
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}`;
Comment on lines +23 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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 handleLogin = (link: string) => {
window.location.href = link;
Expand Down
83 changes: 52 additions & 31 deletions src/components/layouts/SideBar/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React, { useState, useEffect } from "react";
import { NavLink } from "react-router-dom";
import { MyPotIcon, HomeIcon, HomeFilledIcon, PotIcon, LogoFilledIcon, ChatIcon, MyPotFilledIcon, ChatFilledIcon } from "@assets/svgs";
import {
MyPotIcon,
HomeIcon,
HomeFilledIcon,
PotIcon,
LogoFilledIcon,
ChatIcon,
MyPotFilledIcon,
ChatFilledIcon,
} from "@assets/svgs";
import {
container,
iconStyle,
Expand All @@ -11,37 +20,38 @@ import {
menuItemStyle,
} from "./SideBar.style";
import routes from "@constants/routes";


import LoginModal from "@components/commons/Modal/LoginModal/LoginModal";

const menuItems = [
{
to: routes.home,
icon: <HomeIcon css={iconStyle} />,
activeIcon: <HomeFilledIcon css={iconStyle} />,
label: '홈'
label: "홈",
},
{
to: routes.pot.base,
icon: <PotIcon css={potIconStyle} />,
activeIcon: <LogoFilledIcon css={potIconStyle} />,
label: '모든팟',
label: "모든팟",
},
{
to: routes.myPot.base,
icon: <MyPotIcon css={iconStyle} />,
activeIcon: <MyPotFilledIcon css={iconStyle} />,
label: '나의팟',
}, {
label: "나의팟",
},
{
to: routes.chat,
icon: <ChatIcon css={iconStyle} />,
activeIcon: <ChatFilledIcon css={iconStyle} />,
label: '채팅',
label: "채팅",
},
];

const SideBar: React.FC = () => {
const [top, setTop] = useState(0);
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);

useEffect(() => {
const initialTop = window.innerHeight / 2 + window.scrollY;
Expand All @@ -67,31 +77,42 @@ const SideBar: React.FC = () => {
}, []);

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;
const link = `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`;
<>
<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;

return (
<NavLink
key={index}
to={!accessToken && isPrivateRoute ? link : to}
style={({ isActive }) => getNavLinkStyle(isActive)}
css={menuItemStyle}
>
{({ isActive }) => (
<>
{isActive ? activeIcon : icon}
<p css={labelStyle}>{label}</p>
</>
)}
</NavLink>
);
})}
return (
<NavLink
key={index}
to={to}
style={({ isActive }) => getNavLinkStyle(isActive)}
css={menuItemStyle}
onClick={(e) => {
if (!accessToken && isPrivateRoute) {
e.preventDefault();
setIsLoginModalOpen(true);
}
}}
>
{({ isActive }) => (
<>
{isActive ? activeIcon : icon}
<p css={labelStyle}>{label}</p>
</>
)}
</NavLink>
);
})}
</div>
</div>
</div>
{isLoginModalOpen && (
<LoginModal onCancel={() => setIsLoginModalOpen(false)} />
)}
</>
);
};

Expand Down
13 changes: 12 additions & 1 deletion src/pages/Callback/Callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import { useEffect, useRef } from "react";
import useGetSignIn from "apis/hooks/users/useGetSignIn";
import { LoadingSpinnerIcon } from "@assets/svgs";
import { container, iconStyle } from "./Callback.style";
import { useNavigate, useParams } from "react-router-dom";
import routes from "@constants/routes";

const Callback: React.FC = () => {
const navigate = useNavigate();
const { loginType } = useParams();
const isCodeProcessed = useRef(false);
const code = new URL(window.location.href).searchParams.get("code");
const { mutate } = useGetSignIn();
const { mutate } = useGetSignIn(loginType);
Comment on lines +10 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "Callback\.tsx" --type f

Repository: STACKPOT/STACKPOT-FE

Length of output: 95


🏁 Script executed:

cat -n src/pages/Callback/Callback.tsx

Repository: STACKPOT/STACKPOT-FE

Length of output: 1019


🏁 Script executed:

fd "useGetSignIn" --type f

Repository: STACKPOT/STACKPOT-FE

Length of output: 100


🏁 Script executed:

cat -n src/apis/hooks/users/useGetSignIn.ts

Repository: STACKPOT/STACKPOT-FE

Length of output: 1344


🏁 Script executed:

rg "Callback" --type tsx --type ts -B 2 -A 2 | head -50

Repository: STACKPOT/STACKPOT-FE

Length of output: 91


🏁 Script executed:

rg "Callback" -B 2 -A 2 | head -50

Repository: 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/routes

Repository: STACKPOT/STACKPOT-FE

Length of output: 124


🏁 Script executed:

fd "routes" -type f | grep -i const

Repository: STACKPOT/STACKPOT-FE

Length of output: 234


🏁 Script executed:

fd "routes" --type file | grep -i const

Repository: STACKPOT/STACKPOT-FE

Length of output: 87


🏁 Script executed:

cat -n src/constants/routes.ts

Repository: STACKPOT/STACKPOT-FE

Length of output: 1066


🏁 Script executed:

rg "useParams" src/pages/Callback/Callback.tsx -A 3 -B 3

Repository: STACKPOT/STACKPOT-FE

Length of output: 506


🏁 Script executed:

rg "navigate.*callback\|href.*callback\|to.*callback" -i

Repository: STACKPOT/STACKPOT-FE

Length of output: 46


🏁 Script executed:

rg "callback" -i | grep -E "href|to=|navigate|link" | head -20

Repository: STACKPOT/STACKPOT-FE

Length of output: 46


🏁 Script executed:

rg "routes\.callback|/callback" -B 3 -A 3

Repository: 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 파라미터로 받은 loginTypeundefined이거나 유효하지 않은 값일 경우, useGetSignIn 내부의 switch 문에서 기본값으로 네이버 로그인을 실행합니다. 예를 들어, 사용자가 /callback/google이 아닌 /callback으로 이동하거나 잘못된 파라미터를 전달하면 자동으로 네이버 로그인이 시도되어 인증 실패로 이어집니다.

사용자가 구글 로그인을 시도했으나 loginTypeundefined 또는 잘못된 값인 경우, 예상과 다른 네이버 로그인이 실행되어 인증 오류가 발생할 수 있습니다. 이를 방지하기 위해 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.


useEffect(() => {
if (code && !isCodeProcessed.current) {
Expand All @@ -15,6 +19,13 @@ const Callback: React.FC = () => {
}
}, [code, mutate]);

useEffect(() => {
if (!loginType || !["google", "kakao", "naver"].includes(loginType)) {
console.error("Invalid loginType:", loginType);
navigate(routes.home);
}
}, [loginType]);

return (
<main css={container}>
<LoadingSpinnerIcon css={iconStyle} />
Expand Down
12 changes: 7 additions & 5 deletions src/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,19 @@ import "swiper";
import { useNavigate } from "react-router-dom";
import routes from "@constants/routes";
import { Feed, PopularPots } from "./components";
import { useState } from "react";
import LoginModal from "@components/commons/Modal/LoginModal/LoginModal";

const Home: React.FC = () => {
const navigate = useNavigate();
const link = `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 [isLoginModalOpen, setIsLoginModalOpen] = useState(false);

const handleClick = () => {
const token = localStorage.getItem("accessToken");
if (token) {
navigate(routes.createPot);
} else {
window.location.href = link;
setIsLoginModalOpen(true);
}
};

Expand Down Expand Up @@ -68,6 +67,9 @@ const Home: React.FC = () => {
</div>
</div>
</main>
{isLoginModalOpen && (
<LoginModal onCancel={() => setIsLoginModalOpen(false)} />
)}
</>
);
};
Expand Down
Loading