Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
6 changes: 3 additions & 3 deletions src/apis/hooks/pots/useGetPots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ 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, recruitmentRoles, onlyMine],
queryFn: () => GetPots({ page, size, recruitmentRoles, onlyMine }),
select: (data) => data.result,
});
Comment on lines 5 to 21
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

queryKeysize를 포함하고 recruitmentRolesnull로 정규화하세요.

현재 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.

Suggested change
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.

};
Expand Down
15 changes: 12 additions & 3 deletions src/apis/hooks/users/useGetSignIn.ts
Original file line number Diff line number Diff line change
@@ -1,15 +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";
import { useAuthStore } from "stores/useAuthStore";

const useGetSignIn = () => {
const useGetSignIn = (signInType: string | undefined) => {
const navigate = useNavigate();
const setRole = useAuthStore((state) => state.setRole);

return useMutation({
mutationFn: (code: string) => getKakaoLogIn(code),
mutationFn: (code: string) => {
switch (signInType) {
case "google":
return getGoogleLogIn(code);
case "kakao":
return getKakaoLogIn(code);
default:
return getNaverLogIn(code);
}
},
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
4 changes: 2 additions & 2 deletions 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 Expand Up @@ -209,4 +209,4 @@ export interface PatchPotCompleteBody {

export interface PatchDescriptionBody {
userDescription: string;
}
}
37 changes: 23 additions & 14 deletions src/apis/userAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,31 @@ import {
TokenServiceResponse,
DescriptionResponse,
} from "./types/user";
import { PatchDescriptionBody, PatchPotCompleteBody, PostPotResponse } from "./types/pot";
import {
PatchDescriptionBody,
PatchPotCompleteBody,
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");
};
export const patchSignIn = async ({
role,
interest
}: postSignInPayload) => {
export const patchSignIn = async ({ role, interest }: postSignInPayload) => {
return authApiPatch<SignInResponse>("/users/profile", {
role,
interest
interest,
});
};

Expand All @@ -46,13 +55,15 @@ export const getNickname = async (role: Role) => {
};

export const postNickname = async (nickname: string) => {
return authApiPost<TokenServiceResponse>("/users/nickname/save", undefined, { nickname });
return authApiPost<TokenServiceResponse>("/users/nickname/save", undefined, {
nickname,
});
};

export const GetMyPage = async ({ dataType }: GetMyPageParams) => {
if (dataType === 'feed') {
if (dataType === "feed") {
return authApiGet<MyPageResponse>("/users/mypages");
} else if (dataType === 'pot') {
} else if (dataType === "pot") {
return authApiGet<MyPageResponse>("/users/mypages", { dataType });
} else {
return authApiGet<DescriptionResponse>("/users/description");
Expand Down Expand Up @@ -97,8 +108,6 @@ export const patchFinishedPot = async (
return authApiPatch<PostPotResponse>(`/users/${potId}`, body);
};

export const patchDescription = async (
body: PatchDescriptionBody,
) => {
return authApiPatch('/users/description', body);
}
export const patchDescription = async (body: PatchDescriptionBody) => {
return authApiPatch("/users/description", body);
};
48 changes: 30 additions & 18 deletions src/components/cards/CtaCard/CtaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useNavigate } from "react-router-dom";
import routes from "@constants/routes";
import { useEffect, useState } from "react";
import { roleImages } from "@constants/roleImage";
import LoginModal from "@components/commons/Modal/LoginModal/LoginModal";

interface CtaCardProps {
type: "pot" | "feed";
Expand All @@ -17,36 +18,47 @@ interface CtaCardProps {
const CtaCard: React.FC<CtaCardProps> = ({ type }: CtaCardProps) => {
const navigate = useNavigate();
const [roleProfileImage, setRoleProfileImage] = useState<string>("");
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);

useEffect(() => {
const role = localStorage.getItem("role");
const role = localStorage.getItem("role") ?? "UNKNOWN";
const profileImage = roleImages[role as keyof typeof roleImages] || "";
setRoleProfileImage(profileImage);
}, [localStorage.getItem("role")]);

const handleClick = () => {
if (type === "feed") {
navigate(routes.writePost);
} else if (type === "pot") {
navigate(routes.createPot);
const token = localStorage.getItem("accessToken");
if (token === null) {
setIsLoginModalOpen(true);
} else {
if (type === "feed") {
navigate(routes.writePost);
} else if (type === "pot") {
navigate(routes.createPot);
}
window.scrollTo(0, 0);
}
window.scrollTo(0, 0);
};

return (
<div css={container} onClick={handleClick}>
<img css={profileImageStyle} src={roleProfileImage} />
<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={roleProfileImage} />
<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}`;

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
4 changes: 3 additions & 1 deletion src/pages/Callback/Callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { useEffect, useRef } from "react";
import useGetSignIn from "apis/hooks/users/useGetSignIn";
import { LoadingSpinnerIcon } from "@assets/svgs";
import { container, iconStyle } from "./Callback.style";
import { useParams } from "react-router-dom";

const Callback: React.FC = () => {
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 Down
Loading