Conversation
워크스루이 PR은 Ant Design v5에서 v6으로 업그레이드하고, Recoil 상태 관리를 추가하며, 쿠키 기반 인증 및 OAuth 통합을 구현합니다. 또한 채팅방 생성 API, 회원가입 API, OAuth 핸들러를 추가하고, 컴포넌트 UI를 List에서 Card/Space로 리팩토링하며, 제품 상세 페이지에 갤러리 및 채팅 기능을 구현합니다. Changes
예상 코드 리뷰 난도🎯 4 (복잡함) | ⏱️ ~50분 집중 검토가 필요한 영역:
관련 가능 PR
제안 리뷰어
시
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
Summary of ChangesHello @Shin-Sujin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 사용자 인증 및 상호작용 기능을 강화하기 위해 회원가입 API 연동, 카카오를 포함한 소셜 로그인 기능 추가, 그리고 백엔드 API 통신을 위한 Axios 인스턴스 개선을 포함합니다. 또한, 상품 상세 페이지에서 채팅방을 생성할 수 있는 기능이 추가되었으며, 전반적인 UI 컴포넌트의 구조 개선과 Ant Design 라이브러리 버전 업데이트가 이루어졌습니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
안녕하세요. 회원가입 API 연동 및 카카오 로그인 기능 추가 PR에 대한 리뷰입니다.
전반적으로 회원가입과 소셜 로그인 기능의 기반을 잘 마련해주셨습니다. Axios 인스턴스에 인터셉터를 추가하여 인증 토큰을 관리하고, 401 에러를 전역적으로 처리하는 로직은 좋은 접근 방식입니다. 또한, antd v6 업그레이드에 따른 변경사항도 잘 반영되었습니다.
다만, 몇 가지 개선점을 제안드립니다. 코드 곳곳에 남아있는 디버깅용 console.log는 제거가 필요하며, 일부 하드코딩된 값들(API 엔드포인트, 사용자 ID 등)은 동적으로 처리되도록 수정해야 합니다. 또한, 중복된 에러 처리 로직을 정리하고, 일부 컴포넌트의 스타일링 방식을 개선하여 유지보수성을 높일 수 있습니다. 마지막으로, OAuth 핸들러의 중복 코드를 리팩터링하여 더 간결하게 만들 수 있습니다.
자세한 내용은 각 파일에 남긴 개별 코멘트를 참고해주세요.
| const sellerId = 1; // 실제 판매자 ID로 교체 필요 | ||
| const buyerId = 2; // 현재 로그인한 사용자 ID로 교체 필요 |
| const isPublicEndpoint = | ||
| url.includes("/api/v1/chatrooms") || | ||
| url.includes("/api/v1/products") || | ||
| url.includes("/api/v1/images"); |
There was a problem hiding this comment.
| // 401 에러가 발생한 경우 (인증 필요) | ||
| if (error.response?.status === 401) { | ||
| // 로그인 페이지로 리다이렉트 (현재 URL 저장) | ||
| const currentUrl = window.location.pathname; | ||
| router.push(`/login?redirect=${encodeURIComponent(currentUrl)}`); | ||
| } |
| <h1 className="font-bold mr-2 mb-0">닌텐도 스위치 OLED 화이트</h1> | ||
| <Tag color="success" className="!rounded-full !px-2 !py-0.5"> | ||
| 판매중 | ||
| </Tag> | ||
| </div> | ||
| <div className=" flex ml-[20px] mt-[20px]"> | ||
| <h1 className="font-bold text-[30px] ">320,000원</h1> | ||
| <p className="text-lg mt-2 mb-0 ml-1 text-[#697281]">1일전</p> | ||
| </div> | ||
| <div className="font-semi bold ml-[20px]"> | ||
| <p> | ||
| 몇번 안해봤고 배터리 개선판입니다 <br /> | ||
| 기스에 예민해서 직접 보호필름 붙이러 갔었고용 상태 S급이에요 | ||
| <br /> | ||
| 풀구성중에 조이콘 그립 (검정)은 분실하였습니다ㅜㅜ | ||
| <br /> | ||
| 나머지 구성은 모두 있어요❗️ | ||
| <br /> | ||
| 박스 있는데 상태는 안좋아요 <br /> | ||
| <br /> | ||
| 조이콘 캡은 서비스로 드려용 <br /> | ||
| 닌텐도 칩도 팔고 있어요 <br /> | ||
| 같이 구매시 에눌 해드립니다 | ||
| </p> |
| console.log("요청 URL:", response.config.url); | ||
| console.log("응답 데이터:", response.data); | ||
| console.log("요청 데이터:", signupData); |
| <button> | ||
| <ArrowLeftOutlined | ||
| className="text-[20px]" | ||
| onClick={() => history.back()} |
| }; | ||
|
|
||
| const handleSubmit = (e: React.FormEvent) => { | ||
| console.log("회원가입 시도:", { ...formData, agreements }); |
| const handleSignup = async () => { | ||
| setIsLoading(true); | ||
| try { | ||
| console.log("회원가입 시도(handleSignup):", { ...formData, agreements }); |
| {(() => { | ||
| const hasPassword = formData.password.length > 0; | ||
| const isValidPassword = | ||
| hasPassword && validatePassword(formData.password); | ||
| const getBorderColor = () => { | ||
| if (errors.password) return "#ef4444"; | ||
| if (isValidPassword) return "#3b82f6"; | ||
| if (hasPassword) return "#ef4444"; | ||
| return "#374151"; | ||
| }; | ||
|
|
||
| return ( | ||
| <input | ||
| type="password" | ||
| id="password" | ||
| value={formData.password} | ||
| onChange={e => handleChange("password", e.target.value)} | ||
| placeholder="8자 이상 입력해주세요" | ||
| style={{ | ||
| width: "100%", | ||
| height: "3.5rem", | ||
| padding: "0 1rem", | ||
| backgroundColor: "rgba(31, 41, 55, 0.5)", | ||
| borderWidth: "1px", | ||
| borderStyle: "solid", | ||
| borderColor: getBorderColor(), | ||
| borderRadius: "0.75rem", | ||
| color: "#ffffff", | ||
| outline: "none", | ||
| transition: "all 0.2s", | ||
| }} | ||
| className="!w-full !h-14 !px-4 !bg-gray-800/50 !border !rounded-xl !text-white placeholder-gray-500 !focus:outline-none focus:ring-2 focus:ring-blue-500 !focus:border-transparent !transition-all" | ||
| onFocus={e => { | ||
| const borderColor = isValidPassword | ||
| ? "#3b82f6" | ||
| : hasPassword | ||
| ? "#ef4444" | ||
| : "#3b82f6"; | ||
| e.target.style.borderColor = borderColor; | ||
| e.target.style.boxShadow = | ||
| "0 0 0 2px rgba(59, 130, 246, 0.5)"; | ||
| }} | ||
| onBlur={e => { | ||
| e.target.style.boxShadow = "none"; | ||
| e.target.style.borderColor = getBorderColor(); | ||
| }} | ||
| /> | ||
| ); | ||
| })()} |
| // 백엔드 API URL (환경 변수에서 가져오거나 프로덕션 기본값 사용) | ||
| const BACKEND_API_URL = | ||
| process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.8aladin.shop"; | ||
|
|
||
| /** | ||
| * 카카오 OAuth2 로그인 처리 | ||
| */ | ||
| export const handleKakaoLogin = () => { | ||
| try { | ||
| if (typeof window === "undefined") { | ||
| throw new Error( | ||
| "브라우저 환경에서만 카카오 로그인을 사용할 수 있습니다." | ||
| ); | ||
| } | ||
|
|
||
| // 리다이렉트할 프론트엔드 URL (성공 후 돌아올 페이지) | ||
| const redirectUrl = encodeURIComponent( | ||
| `${window.location.origin}/mainpage` | ||
| ); | ||
|
|
||
| // 백엔드의 OAuth2 인증 엔드포인트로 리다이렉트 | ||
| // next 파라미터로 성공 후 리다이렉트할 URL 전달 | ||
| const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/kakao?next=${redirectUrl}`; | ||
|
|
||
| window.location.href = oauth2Url; | ||
| } catch (error) { | ||
| console.error(error); | ||
| alert("카카오 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * 구글 OAuth2 로그인 처리 | ||
| */ | ||
| export const handleGoogleLogin = () => { | ||
| try { | ||
| if (typeof window === "undefined") { | ||
| throw new Error("브라우저 환경에서만 구글 로그인을 사용할 수 있습니다."); | ||
| } | ||
|
|
||
| // 리다이렉트할 프론트엔드 URL (성공 후 돌아올 페이지) | ||
| const redirectUrl = encodeURIComponent( | ||
| `${window.location.origin}/mainpage` | ||
| ); | ||
|
|
||
| // 백엔드의 OAuth2 인증 엔드포인트로 리다이렉트 | ||
| // next 파라미터로 성공 후 리다이렉트할 URL 전달 | ||
| const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/google?next=${redirectUrl}`; | ||
|
|
||
| window.location.href = oauth2Url; | ||
| } catch (error) { | ||
| console.error(error); | ||
| alert("구글 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * 네이버 OAuth2 로그인 처리 | ||
| */ | ||
| export const handleNaverLogin = () => { | ||
| try { | ||
| if (typeof window === "undefined") { | ||
| throw new Error( | ||
| "브라우저 환경에서만 네이버 로그인을 사용할 수 있습니다." | ||
| ); | ||
| } | ||
|
|
||
| // 리다이렉트할 프론트엔드 URL (성공 후 돌아올 페이지) | ||
| const redirectUrl = encodeURIComponent( | ||
| `${window.location.origin}/mainpage` | ||
| ); | ||
|
|
||
| // 백엔드의 OAuth2 인증 엔드포인트로 리다이렉트 | ||
| // next 파라미터로 성공 후 리다이렉트할 URL 전달 | ||
| const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/naver?next=${redirectUrl}`; | ||
|
|
||
| window.location.href = oauth2Url; | ||
| } catch (error) { | ||
| console.error(error); | ||
| alert("네이버 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); | ||
| } | ||
| }; |
There was a problem hiding this comment.
handleKakaoLogin, handleGoogleLogin, handleNaverLogin 함수의 구조가 거의 동일하여 코드 중복이 발생하고 있습니다. 소셜 로그인 공급자(provider) 이름을 인자로 받는 단일 함수로 통합하여 중복을 제거하고 유지보수성을 높일 수 있습니다.
// 백엔드 API URL (환경 변수에서 가져오거나 프로덕션 기본값 사용)
const BACKEND_API_URL =
process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.8aladin.shop";
type AuthProvider = "kakao" | "google" | "naver";
const handleOAuthLogin = (provider: AuthProvider) => {
try {
if (typeof window === "undefined") {
throw new Error(
`브라우저 환경에서만 ${provider} 로그인을 사용할 수 있습니다.`
);
}
// 리다이렉트할 프론트엔드 URL (성공 후 돌아올 페이지)
const redirectUrl = encodeURIComponent(
`${window.location.origin}/mainpage`
);
// 백엔드의 OAuth2 인증 엔드포인트로 리다이렉트
// next 파라미터로 성공 후 리다이렉트할 URL 전달
const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/${provider}?next=${redirectUrl}`;
window.location.href = oauth2Url;
} catch (error) {
console.error(error);
alert(`${provider} 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요.`);
}
};
/**
* 카카오 OAuth2 로그인 처리
*/
export const handleKakaoLogin = () => handleOAuthLogin("kakao");
/**
* 구글 OAuth2 로그인 처리
*/
export const handleGoogleLogin = () => handleOAuthLogin("google");
/**
* 네이버 OAuth2 로그인 처리
*/
export const handleNaverLogin = () => handleOAuthLogin("naver");There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (15)
src/components/interest/InterestItemRow.tsx (1)
30-30: 조건부 스타일링을 고려하세요.
cursor-pointer클래스가 항상 적용되지만onClick이 없을 때는 불필요합니다. 사용자에게 혼란을 줄 수 있습니다.다음과 같이 조건부로 적용하세요:
- className="rounded-xl hover:shadow-sm transition-shadow cursor-pointer" + className={`rounded-xl hover:shadow-sm transition-shadow ${onItemClick ? 'cursor-pointer' : ''}`}src/components/interest/InterestList.tsx (1)
27-36: 빈 목록 상태 처리를 추가하는 것을 고려하세요.현재
items배열이 비어있을 때 아무것도 렌더링되지 않습니다. 사용자 경험 개선을 위해 빈 상태 메시지를 표시하는 것이 좋습니다.다음과 같이 빈 상태 처리를 추가하세요:
<div className="flex flex-col gap-3"> - {items.map(item => ( + {items.length === 0 ? ( + <div className="text-center py-8 text-gray-400"> + 등록된 관심 항목이 없습니다 + </div> + ) : ( + items.map(item => ( - <InterestItemRow - key={item.id ?? item.title} - item={item} - onItemClick={onItemClick} - onRemoveInterest={onRemoveInterest} - /> - ))} + <InterestItemRow + key={item.id ?? item.title} + item={item} + onItemClick={onItemClick} + onRemoveInterest={onRemoveInterest} + /> + )) + )} </div>src/components/profile/ItemRow.tsx (1)
18-23: UsebodyStyleinstead ofstylefor customizing Card body paddingThe current code applies
style={{ padding: 6 }}to the Card wrapper, leaving the default Card body padding unchanged. Since the comment indicates intent to customize "antd body padding", usebodyStyleinstead:- <Card - hoverable - onClick={onClick ? handleClick : undefined} - className="rounded-xl border border-gray-100 shadow-sm hover:shadow-md transition-shadow" - style={{ padding: 6 }} // antd body padding 커스텀 - > + <Card + hoverable + onClick={onClick ? handleClick : undefined} + className="rounded-xl border border-gray-100 shadow-sm hover:shadow-md transition-shadow" + bodyStyle={{ padding: 6 }} // antd body padding 커스텀 + >The
bodyStyleprop in antd 6.0.1 Card is designed specifically for this use case and aligns with the antd API design intent.src/app/login/page.tsx (1)
68-68:next/image컴포넌트 사용 권장Next.js 프로젝트에서는
<img>태그 대신next/image의Image컴포넌트를 사용하는 것이 좋습니다. 자동 이미지 최적화, lazy loading, 레이아웃 시프트 방지 등의 이점을 얻을 수 있습니다.+import Image from "next/image";- <img src="/googleIcon.png" alt="구글" className="w-8 h-8" /> + <Image src="/googleIcon.png" alt="구글" width={32} height={32} />- <img src="/naverIcon.png" alt="네이버" className="w-8 h-8" /> + <Image src="/naverIcon.png" alt="네이버" width={32} height={32} />src/api/axiosInstance.ts (3)
56-59: 공개 엔드포인트 체크 로직 개선 필요
url.includes()방식은 의도치 않은 URL도 매칭될 수 있습니다. 예:/api/v1/admin/chatrooms도 매칭됩니다.- const isPublicEndpoint = - url.includes("/api/v1/chatrooms") || - url.includes("/api/v1/products") || - url.includes("/api/v1/images"); + const publicEndpoints = [ + "/api/v1/chatrooms", + "/api/v1/products", + "/api/v1/images" + ]; + const isPublicEndpoint = publicEndpoints.some( + endpoint => url.startsWith(endpoint) || url === endpoint + );
62-66: 다중 401 응답 시 중복 리다이렉트 방지 필요동시에 여러 API 요청이 401을 반환하면
window.location.href할당이 여러 번 발생할 수 있습니다. 리다이렉트 중복을 방지하는 플래그 추가를 고려해 주세요.+let isRedirecting = false; + axiosInstance.interceptors.response.use( response => { return response; }, error => { if (error.response?.status === 401) { // ...existing checks... - if (!isPublicEndpoint && typeof window !== "undefined" && !window.location.pathname.startsWith("/login")) { + if (!isPublicEndpoint && typeof window !== "undefined" && !window.location.pathname.startsWith("/login") && !isRedirecting) { + isRedirecting = true; const currentUrl = window.location.pathname + window.location.search; window.location.href = `/login?redirect=${encodeURIComponent(currentUrl)}`; } }
77-78: 불필요한 빈 줄 제거파일 끝의 불필요한 빈 줄을 제거해 주세요.
src/api/chat/apis.ts (2)
26-34: 에러 처리 추가 권장
createChatRoom함수에서 API 호출 실패 시 에러 처리가 없습니다. 호출부에서 처리할 수도 있지만, 공통적인 에러 변환이나 로깅을 여기서 처리하면 일관성이 향상됩니다.export async function createChatRoom( request: CreateChatRoomRequest ): Promise<ChatRoomResponse> { - const response = await axiosInstance.post<ChatRoomResponse>( - "/api/v1/chatrooms", - request - ); - return response.data; + try { + const response = await axiosInstance.post<ChatRoomResponse>( + "/api/v1/chatrooms", + request + ); + return response.data; + } catch (error) { + console.error("채팅방 생성 실패:", error); + throw error; + } }
36-79: 더미 데이터 함수 정리 필요
getChatRoomDetails와getMessages는 더미 데이터를 반환하는 함수입니다. 실제 API 연동 전까지 필요하다면 TODO 주석을 추가하거나, 별도의 mock 파일로 분리하는 것이 좋습니다. 또한roomId파라미터가 사용되지 않고 있습니다.-// 더미 데이터 +// TODO: 실제 API 연동 필요 - 현재 더미 데이터 반환 +// eslint-disable-next-line @typescript-eslint/no-unused-vars export async function getChatRoomDetails(roomId: string) {src/utils/oauthHandlers.ts (3)
8-82: 코드 중복을 제거하여 유지보수성을 개선하세요.세 개의 OAuth 핸들러 함수(
handleKakaoLogin,handleGoogleLogin,handleNaverLogin)가 거의 동일한 로직을 가지고 있습니다. 공통 함수로 추출하여 중복을 제거할 수 있습니다.다음과 같이 리팩토링할 수 있습니다:
+const handleOAuthLogin = (provider: 'kakao' | 'google' | 'naver') => { + try { + if (typeof window === "undefined") { + throw new Error(`브라우저 환경에서만 ${provider} 로그인을 사용할 수 있습니다.`); + } + + const redirectUrl = encodeURIComponent( + `${window.location.origin}/mainpage` + ); + + const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/${provider}?next=${redirectUrl}`; + window.location.href = oauth2Url; + } catch (error) { + console.error(error); + alert(`${provider} 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요.`); + } +}; -export const handleKakaoLogin = () => { - try { - if (typeof window === "undefined") { - throw new Error( - "브라우저 환경에서만 카카오 로그인을 사용할 수 있습니다." - ); - } - - const redirectUrl = encodeURIComponent( - `${window.location.origin}/mainpage` - ); - - const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/kakao?next=${redirectUrl}`; - - window.location.href = oauth2Url; - } catch (error) { - console.error(error); - alert("카카오 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); - } -}; +export const handleKakaoLogin = () => handleOAuthLogin('kakao'); -export const handleGoogleLogin = () => { - try { - if (typeof window === "undefined") { - throw new Error("브라우저 환경에서만 구글 로그인을 사용할 수 있습니다."); - } - - const redirectUrl = encodeURIComponent( - `${window.location.origin}/mainpage` - ); - - const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/google?next=${redirectUrl}`; - - window.location.href = oauth2Url; - } catch (error) { - console.error(error); - alert("구글 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); - } -}; +export const handleGoogleLogin = () => handleOAuthLogin('google'); -export const handleNaverLogin = () => { - try { - if (typeof window === "undefined") { - throw new Error( - "브라우저 환경에서만 네이버 로그인을 사용할 수 있습니다." - ); - } - - const redirectUrl = encodeURIComponent( - `${window.location.origin}/mainpage` - ); - - const oauth2Url = `${BACKEND_API_URL}/oauth2/authorization/naver?next=${redirectUrl}`; - - window.location.href = oauth2Url; - } catch (error) { - console.error(error); - alert("네이버 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); - } -}; +export const handleNaverLogin = () => handleOAuthLogin('naver');
10-14: 불필요한 에러 throw를 제거하세요.
typeof window === "undefined"체크에서 에러를 throw하지만 바로 catch하므로 불필요합니다. 조건문으로 직접 처리하는 것이 더 간결합니다.-if (typeof window === "undefined") { - throw new Error( - "브라우저 환경에서만 카카오 로그인을 사용할 수 있습니다." - ); +if (typeof window === "undefined") { + console.error("브라우저 환경에서만 카카오 로그인을 사용할 수 있습니다."); + return; }
28-28: alert 대신 antd의 message를 사용하세요.프로젝트에서 이미 antd를 사용하고 있으므로, 일관된 사용자 경험을 위해
alert()대신message.error()를 사용하는 것이 좋습니다.+import { message } from 'antd'; -alert("카카오 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요."); +message.error("카카오 로그인 설정이 올바르지 않습니다. 관리자에게 문의해주세요.");src/app/products/[id]/page.tsx (2)
54-57: 하드코딩된 사용자 ID를 실제 값으로 교체하세요.TODO 코멘트에 표시된 대로, 실제 판매자 ID와 구매자 ID를 가져와야 합니다. 인증된 사용자 정보를 컨텍스트나 세션에서 가져오는 것을 고려하세요.
실제 사용자 인증 정보를 가져오는 로직 구현을 도와드릴까요? 예를 들어, 사용자 컨텍스트나 JWT 토큰에서 buyerId를 추출하고, 상품 상세 API에서 sellerId를 가져오는 방식으로 개선할 수 있습니다.
96-96: 고정 너비 대신 반응형 디자인을 고려하세요.
w-[600px]와 같은 고정 너비는 다양한 화면 크기에서 문제를 일으킬 수 있습니다.max-w-[600px] w-full과 같은 반응형 클래스 사용을 고려하세요.-<div className="top-0 w-[600px] h-[50px] bg-[#232323] text-white flex items-center justify-center mx-auto"> +<div className="top-0 max-w-[600px] w-full h-[50px] bg-[#232323] text-white flex items-center justify-center mx-auto">이 패턴을 파일의 다른 고정 너비 요소들(lines 120, 134, 178)에도 적용하세요.
src/app/signup/page.tsx (1)
204-252: 비밀번호 입력 렌더링 로직을 단순화하세요.즉시 실행 함수(IIFE)와 인라인 스타일, className이 혼재되어 있어 복잡도가 높습니다. 로직을 컴포넌트 상단으로 분리하고 스타일을 일관되게 적용하는 것이 좋습니다.
+ const hasPassword = formData.password.length > 0; + const isValidPassword = hasPassword && validatePassword(formData.password); + + const getPasswordBorderColor = () => { + if (errors.password) return "border-red-500"; + if (isValidPassword) return "border-blue-500"; + if (hasPassword) return "border-red-500"; + return "border-gray-700"; + }; {/* 비밀번호 */} <div> <label htmlFor="password" className="block text-sm font-medium text-gray-300 mb-2" > 비밀번호 <span className="text-red-400">*</span> </label> - {(() => { - const hasPassword = formData.password.length > 0; - const isValidPassword = - hasPassword && validatePassword(formData.password); - const getBorderColor = () => { - if (errors.password) return "#ef4444"; - if (isValidPassword) return "#3b82f6"; - if (hasPassword) return "#ef4444"; - return "#374151"; - }; - - return ( - <input - type="password" - id="password" - value={formData.password} - onChange={e => handleChange("password", e.target.value)} - placeholder="8자 이상 입력해주세요" - style={{ - width: "100%", - height: "3.5rem", - padding: "0 1rem", - backgroundColor: "rgba(31, 41, 55, 0.5)", - borderWidth: "1px", - borderStyle: "solid", - borderColor: getBorderColor(), - borderRadius: "0.75rem", - color: "#ffffff", - outline: "none", - transition: "all 0.2s", - }} - className="!w-full !h-14 !px-4 !bg-gray-800/50 !border !rounded-xl !text-white placeholder-gray-500 !focus:outline-none focus:ring-2 focus:ring-blue-500 !focus:border-transparent !transition-all" - onFocus={e => { - const borderColor = isValidPassword - ? "#3b82f6" - : hasPassword - ? "#ef4444" - : "#3b82f6"; - e.target.style.borderColor = borderColor; - e.target.style.boxShadow = - "0 0 0 2px rgba(59, 130, 246, 0.5)"; - }} - onBlur={e => { - e.target.style.boxShadow = "none"; - e.target.style.borderColor = getBorderColor(); - }} - /> - ); - })()} + <input + type="password" + id="password" + value={formData.password} + onChange={e => handleChange("password", e.target.value)} + placeholder="8자 이상 입력해주세요" + className={`w-full h-14 px-4 bg-gray-800/50 border ${getPasswordBorderColor()} rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all`} + /> <p className="mt-2 text-sm text-gray-400"> 비밀번호는 최소 8자 이상이며, 영문, 숫자, 특수문자를 포함해야 합니다
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/googleIcon.pngis excluded by!**/*.pngpublic/naverIcon.pngis excluded by!**/*.png
📒 Files selected for processing (14)
.env.dev(1 hunks)package.json(1 hunks)src/api/axiosInstance.ts(3 hunks)src/api/chat/apis.ts(1 hunks)src/api/signup.ts(1 hunks)src/app/login/page.tsx(3 hunks)src/app/products/[id]/page.tsx(1 hunks)src/app/signup/page.tsx(14 hunks)src/components/interest/InterestItemRow.tsx(3 hunks)src/components/interest/InterestList.tsx(1 hunks)src/components/profile/ItemList.tsx(2 hunks)src/components/profile/ItemRow.tsx(2 hunks)src/components/profile/ProfileSection.tsx(1 hunks)src/utils/oauthHandlers.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/app/products/[id]/page.tsx (2)
src/app/mainpage/page.tsx (1)
Page(4-11)src/api/chat/apis.ts (1)
createChatRoom(26-34)
src/app/login/page.tsx (1)
src/utils/oauthHandlers.ts (1)
handleNaverLogin(60-82)
src/app/signup/page.tsx (1)
src/api/signup.ts (1)
signup(20-31)
🔇 Additional comments (12)
src/components/interest/InterestList.tsx (1)
30-30: Remove unnecessary fallback in key propThe
idfield inInterestItemis required (id: number), so the fallback toitem.titlewill never execute. Simplify line 30 tokey={item.id}for clarity.Likely an incorrect or invalid review comment.
src/components/profile/ItemList.tsx (1)
4-4: Space 기반 리스트 렌더링 전환이 자연스럽게 잘 정리되어 있습니다
- antd v6에서 권장하는
orientation="vertical"을 사용하고 있고,items.map으로 명시적으로ItemRow를 렌더링해서 가독성이 좋습니다.key={item.id}와onClick={onItemClick}전달 구조도ItemRow의 타입 정의와 잘 맞습니다.현재 변경만 봤을 때 기능·구조 상 이슈는 없어 보입니다.
antd 6.0.1 Space 컴포넌트에서
orientation프로퍼티가 실제로 사용하는 버전과 일치하는지(향후 breaking 변경 여부 포함) 한 번만 공식 문서로 재확인해 주세요.Also applies to: 22-35
src/components/profile/ItemRow.tsx (1)
4-5: 클릭 핸들링과 Card 전환 구조가 명확합니다
onClick이 있을 때만Card에handleClick을 바인딩하는 패턴이 깔끔하고,onClick(item)도메인 콜백을 잘 감쌉니다.memo(ItemRowBase)로 불필요한 리렌더도 줄여서 리스트 성능 측면에서도 무난해 보입니다.기능 관점에서 현재 구현은 그대로 사용해도 괜찮아 보입니다.
Card의
onClick동작 및hoverable옵션이 사용 중인 antd 6.0.1 버전에서 의도한 대로 동작하는지(예: 포인터 커서, hover 스타일 중복 여부)를 공식 문서나 스토리북으로 한 번만 확인해 주세요.Also applies to: 12-21
.env.dev (1)
1-1: LGTM!
.env파일에서 따옴표 제거는 올바른 형식입니다. 다만, 개발 환경에서 프로덕션 API(api.8aladin.shop)를 직접 사용하는 것이 의도된 것인지 확인해 주세요. 로컬 개발 시 별도의 개발/스테이징 서버를 사용하는 것이 일반적입니다.src/app/login/page.tsx (1)
5-9: OAuth 핸들러 분리 - 좋은 리팩토링입니다OAuth 로그인 로직을
@/utils/oauthHandlers로 분리하여 재사용성과 유지보수성이 향상되었습니다.src/api/axiosInstance.ts (1)
12-22: HttpOnly 쿠키와의 호환성 확인 필요
document.cookie로 accessToken을 읽으려면 해당 쿠키가 HttpOnly로 설정되어 있지 않아야 합니다. 보안상 accessToken은 HttpOnly 쿠키로 설정하는 것이 권장되며, 이 경우 JavaScript에서 접근이 불가능합니다.백엔드에서 쿠키 설정 방식을 확인해 주세요:
- HttpOnly 쿠키 사용 시:
withCredentials: true만으로 충분하며, 브라우저가 자동으로 쿠키를 전송합니다- Non-HttpOnly 쿠키 사용 시: 현재 구현이 동작하지만 XSS 공격에 취약해집니다
src/api/chat/apis.ts (1)
1-34: 채팅방 생성 API 구현 - LGTM인터페이스 정의와 API 함수 구현이 깔끔합니다. TypeScript 제네릭을 적절히 활용하여 타입 안전성을 확보했습니다.
src/components/profile/ProfileSection.tsx (1)
53-53: No changes needed — Space component correctly uses orientation prop for antd v6The migration from
directiontoorientationis correct. The antd v6 Space component API confirms thatorientationis the proper prop name for this version, accepting values of'horizontal' | 'vertical'. Your implementation aligns with the official v6 API.src/api/signup.ts (1)
4-17: LGTM!인터페이스 정의가 명확하고 타입 안정성이 보장됩니다.
src/app/products/[id]/page.tsx (1)
83-87: LGTM!401 에러 시 로그인 페이지로 리다이렉트하면서 현재 URL을 보존하는 로직이 잘 구현되어 있습니다.
src/app/signup/page.tsx (2)
42-57: LGTM!비밀번호 검증 로직이 명확하고 보안 요구사항을 잘 충족합니다.
118-140: LGTM!비동기 회원가입 처리 로직이 잘 구현되어 있습니다. 로딩 상태 관리, 에러 처리, 성공 시 리다이렉트가 적절합니다.
| }, | ||
| "dependencies": { | ||
| "antd": "^5.27.4", | ||
| "antd": "^6.0.1", |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
antd v6 migration guide breaking changes
💡 Result:
Key breaking changes & migration notes for Ant Design v6 (summary):
- Minimum React required: React 18 (v6 targets React 18+, React 19 recommended). Migrate your app to React 18+ before upgrading. [1][2]
- Styling architecture: moved to a pure CSS‑variables styling system / semantic CSS structure (affects theming, custom style overrides and any low‑level CSS assumptions). Update custom themes/CSS accordingly. [1][3]
- IE11 and legacy browser support removed — polyfills/IE workarounds must be removed or kept on the old major. [1][3]
- No codemod/compat package needed when upgrading from v5 — upgrade is intended to be “smooth” from v5 (still verify component API changes you use). [2]
- React Compiler enabled in distributed builds (affects bundling/compat with older React toolchains). [2]
- Remove or replace any v5->v6 deprecated compatibility shims (e.g., patches for React 19 are no longer required). [2]
- Expect semantic/structure changes in components (props or DOM structure may change; test UI and custom style/selectors). [1][3]
Recommended migration steps
- Upgrade React to 18+ (prefer React 19 if possible). [2]
- Run your test suite and visual checks; search console for deprecation warnings and fix usages before upgrading. [1]
- Update custom theming and CSS that relied on former Less/CSS implementation to use the new CSS‑variables approach. [1][3]
- Review components you heavily customized (DOM/className/structure) and test them after upgrade. [1][3]
Sources:
[1] Ant Design v6 migration doc (official). (ant.design)
[2] Ant Design v6 release announcement / GitHub issue. (github.com)
[3] Ant Design X / blog notes on v6 (styling, IE drop, semantic changes). (github.com)
If you want, I can open the official v6 migration page and extract the exact migration checklist and component-level breaking changes for the components you use — tell me which components you rely on.
Citations:
- 1: https://ant.design/docs/react/migration-v6?utm_source=openai
- 2: 🎉 Ant Design 6.0 is Here! 🎉 ant-design/ant-design#55804
- 3: 🎉 Ant Design X 2.0 is here! 🎉 ant-design/x#1357
🏁 Script executed:
# Search for Space component usage in the codebase
rg "Space" --type tsx --type ts --type jsx --type js -i -A 2 -B 2Repository: 8aladin-Project/frontend
Length of output: 95
🏁 Script executed:
# First, check what file types exist in the repo
git ls-files | head -20
# Then search for Space component usage
rg "Space" -i -A 2 -B 2 --max-count 20Repository: 8aladin-Project/frontend
Length of output: 15473
antd v6 メジャーバージョンアップグレード対応確認必須
antdをv5からv6にアップグレードする場合、複数の破壊的変更に対応が必要です。Spaceコンポーネントのdirection → orientation変更を含む、APIの互換性がなくなる変更があります。既に一部ファイル(ItemList.tsx、ProfileSection.tsx)ではv6 APIへの移行が確認できますが、edit/page.tsxなどではまだ旧APIが使用されています。さらに、antd v6ではスタイリングシステムがCSS変数ベースに変更され、React 18以上が必須となるため、公式マイグレーションガイドに従い、すべてのコンポーネント使用状況とカスタムスタイル、テーマをv6互換に更新してください。
🤖 Prompt for AI Agents
In package.json around line 17, upgrading antd to v6 requires a codebase-wide
migration—identify and update all usages of removed/renamed APIs (e.g., Space:
direction → orientation), update files still using v5 APIs (edit/page.tsx and
any others), ensure React is bumped to >=18 in package.json and lockfiles,
migrate custom styles/themes to the new CSS-variables based styling (replace
legacy token usage and overrides with v6 theme tokens), and follow the official
antd v6 migration guide: run a repo-wide search for antd components, update each
to v6 props/exports, adjust imports and custom CSS, then run the app and tests
to verify visual and runtime regressions.
| "react": "^19.1.2", | ||
| "react-dom": "^19.1.2" | ||
| "react-dom": "^19.1.2", | ||
| "recoil": "^0.7.7" |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🌐 Web query:
Recoil React 19 compatibility 2025
💡 Result:
Short answer: Recoil is not reliably compatible with React 19 (as of late 2024–2025). The Recoil repo was archived on Jan 1, 2025 and there are open issues/reports of runtime errors when running Recoil on React 19; most community guidance is to stay on React 18 or migrate to a maintained state library (Zustand/Redux/Jotai, etc.). [1][2][3]
Sources:
- Recoil GitHub (React 19 support issue / repo archived Jan 1, 2025). [1]
- React v19 announcement (React 19 stable release). [2]
- Community reports / Q&A showing Recoil errors on React 19 and recommending alternatives. [3]
🌐 Web query:
Recoil Meta discontinued maintenance status 2025
💡 Result:
Short answer: Recoil (facebookexperimental/Recoil) was archived and is no longer actively maintained — the repository was marked read-only on Jan 1, 2025. [1][2]
Sources:
- GitHub repository archive notice (archived by owner on Jan 1, 2025). [1]
- GitHub issues/discussions and community threads noting the project is no longer actively maintained and linking the archive. [2][3]
🌐 Web query:
Jotai Zustand active development 2025
💡 Result:
Short answer: Yes — both Jotai and Zustand were actively developed in 2025.
- Zustand: active repo and releases in 2025 (pmndrs/zustand GitHub activity and 2025 releases). [1][2]
- Jotai: active repo with recent pushes and ongoing PR activity in 2025; project site and package releases also updated in 2025. [3][4]
Sources:
[1] pmndrs/zustand GitHub.
[2] pmndrs/zustand release notes (2025 releases).
[3] pmndrs/jotai GitHub (last pushed May 27, 2025; active PRs).
[4] jotai.org / package release metadata (2025 releases).
Switch from Recoil to an actively maintained state management library
Recoil was archived and discontinued by Meta on January 1, 2025, and is no longer actively maintained. It has documented compatibility issues with React 19 that cause runtime errors. For long-term project maintenance, consider migrating to actively developed alternatives like Zustand or Jotai.
| console.log("요청 URL:", response.config.url); | ||
| console.log("응답 데이터:", response.data); | ||
| console.log("요청 데이터:", signupData); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
프로덕션 배포 전에 console.log 문을 제거하세요.
디버깅용 console.log 문은 개발 중에는 유용하지만, 프로덕션 코드에는 포함되지 않아야 합니다.
- console.log("요청 URL:", response.config.url);
- console.log("응답 데이터:", response.data);
- console.log("요청 데이터:", signupData);
return response.data;📝 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.
| console.log("요청 URL:", response.config.url); | |
| console.log("응답 데이터:", response.data); | |
| console.log("요청 데이터:", signupData); | |
| return response.data; |
🤖 Prompt for AI Agents
In src/api/signup.ts around lines 27 to 29 there are console.log statements
printing request URL, response data, and request payload; remove these
console.log calls before production and either delete them or replace them with
calls to the application's structured logger at an appropriate level (e.g.,
logger.debug or logger.info) ensuring sensitive data is not logged (mask or omit
PII/credentials) and that logging respects environment/log level configuration.
| const clickEffect = (index: number) => { | ||
| setClickedIndex(index); | ||
| setTimeout(() => setClickedIndex(null), 150); | ||
| console.log("click"); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
프로덕션 배포 전에 console.log 문을 제거하세요.
디버깅용 console.log는 제거되어야 합니다.
setClickedIndex(index);
setTimeout(() => setClickedIndex(null), 150);
- console.log("click");
};📝 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.
| console.log("click"); |
🤖 Prompt for AI Agents
In src/app/products/[id]/page.tsx around line 30 there is a leftover
console.log("click"); used for debugging; remove this console.log before
production (or replace it with the app's structured logger/debug utility if you
need to keep the event for diagnostics), and ensure no other debug console.*
calls remain in this file.
| <button> | ||
| <ArrowLeftOutlined | ||
| className="text-[20px]" | ||
| onClick={() => history.back()} |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
history.back() 대신 router.back()을 사용하세요.
Next.js에서는 useRouter 훅에서 제공하는 router.back()을 사용하는 것이 권장됩니다.
- onClick={() => history.back()}
+ onClick={() => router.back()}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/products/[id]/page.tsx around line 101, replace the direct call to
history.back() with router.back() from Next.js router: import and call the
useRouter hook at the top of the component (const router = useRouter()) and
update the onClick handler to call router.back(); ensure the component is a
client component if not already (add "use client" at file top) so useRouter can
be used.
| }; | ||
|
|
||
| const handleSubmit = (e: React.FormEvent) => { | ||
| console.log("회원가입 시도:", { ...formData, agreements }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
프로덕션 배포 전에 console.log 문을 제거하세요.
디버깅용 로그는 제거되어야 합니다.
const handleSubmit = (e: React.FormEvent) => {
- console.log("회원가입 시도:", { ...formData, agreements });
e.preventDefault();📝 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.
| console.log("회원가입 시도:", { ...formData, agreements }); | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); |
🤖 Prompt for AI Agents
In src/app/signup/page.tsx around line 65, remove the debugging
console.log("회원가입 시도:", { ...formData, agreements });; either delete the line
entirely or replace it with a proper server-side logger that respects
environment (e.g., only logs in non-production) and never prints sensitive user
data.
| const handleSignup = async () => { | ||
| setIsLoading(true); | ||
| try { | ||
| console.log("회원가입 시도(handleSignup):", { ...formData, agreements }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
프로덕션 배포 전에 console.log 문을 제거하세요.
Line 65와 마찬가지로 이 console.log도 제거되어야 합니다.
setIsLoading(true);
try {
- console.log("회원가입 시도(handleSignup):", { ...formData, agreements });
await signup({📝 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.
| console.log("회원가입 시도(handleSignup):", { ...formData, agreements }); | |
| setIsLoading(true); | |
| try { | |
| await signup({ |
🤖 Prompt for AI Agents
In src/app/signup/page.tsx around line 121, remove the development console.log
call "console.log("회원가입 시도(handleSignup):", { ...formData, agreements });"
before production deployment; replace it with no-op or a secure server-side
logging mechanism if needed (do not log sensitive user data client-side),
ensuring no console.log remains in the production build.
| <Card | ||
| className="rounded-xl hover:shadow-sm transition-shadow cursor-pointer" | ||
| bordered | ||
| onClick={onItemClick ? handleRowClick : undefined} | ||
| style={{ padding: 6 }} | ||
| > |
There was a problem hiding this comment.
키보드 접근성 지원이 누락되었습니다.
Card에 onClick이 설정되어 있지만 키보드 사용자가 접근할 수 없습니다. 클릭 가능한 카드는 포커스를 받을 수 있어야 하며 Enter/Space 키로 활성화할 수 있어야 합니다.
다음 diff를 적용하여 키보드 접근성을 추가하세요:
<Card
className="rounded-xl hover:shadow-sm transition-shadow cursor-pointer"
bordered
onClick={onItemClick ? handleRowClick : undefined}
+ tabIndex={onItemClick ? 0 : undefined}
+ onKeyDown={onItemClick ? (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleRowClick();
+ }
+ } : undefined}
+ role={onItemClick ? "button" : undefined}
style={{ padding: 6 }}
>📝 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.
| <Card | |
| className="rounded-xl hover:shadow-sm transition-shadow cursor-pointer" | |
| bordered | |
| onClick={onItemClick ? handleRowClick : undefined} | |
| style={{ padding: 6 }} | |
| > | |
| <Card | |
| className="rounded-xl hover:shadow-sm transition-shadow cursor-pointer" | |
| bordered | |
| onClick={onItemClick ? handleRowClick : undefined} | |
| tabIndex={onItemClick ? 0 : undefined} | |
| onKeyDown={onItemClick ? (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| handleRowClick(); | |
| } | |
| } : undefined} | |
| role={onItemClick ? "button" : undefined} | |
| style={{ padding: 6 }} | |
| > |
🤖 Prompt for AI Agents
In src/components/interest/InterestItemRow.tsx around lines 29 to 34, the Card
has an onClick but lacks keyboard accessibility; make it focusable and
triggerable via keyboard by adding tabIndex={onItemClick ? 0 : undefined},
role="button" (or use a semantic button if appropriate), and a onKeyDown handler
that calls the same handler when Enter or Space is pressed (prevent default for
Space), and ensure any disabled state is reflected with aria-disabled when
onItemClick is absent; keep onClick behavior unchanged.
| {onRemoveInterest && ( | ||
| <div className="flex items-start"> | ||
| <Button | ||
| type="text" | ||
| icon={<HeartOutlined />} | ||
| onClick={handleRemoveClick} | ||
| className="text-red-500" | ||
| > | ||
| 해제 | ||
| </Button> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
아이콘과 액션의 의미가 일치하지 않습니다.
HeartOutlined 아이콘은 일반적으로 "좋아요" 또는 "관심 추가"를 의미하지만, 여기서는 "해제" (제거) 액션에 사용되고 있습니다. 사용자에게 혼란을 줄 수 있습니다.
관심 항목 제거를 나타내는 더 적절한 아이콘을 사용하세요:
- icon={<HeartOutlined />}
+ icon={<HeartFilled className="text-red-500" />}또는 다른 제거 아이콘을 사용:
+import { HeartFilled } from "@ant-design/icons";
+// 또는
+import { CloseOutlined } from "@ant-design/icons";- icon={<HeartOutlined />}
+ icon={<CloseOutlined />}
- className="text-red-500"Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/components/interest/InterestItemRow.tsx around lines 57 to 68, the
HeartOutlined icon is misleading for a "해제" (remove) action; replace it with a
removal-appropriate icon (for example DeleteOutlined, CloseOutlined, or
MinusCircleOutlined) and update any accessible labels/aria-label or tooltip text
to match the removal action so the icon meaning and button action are
consistent.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
Chores
✏️ Tip: You can customize this high-level summary in your review settings.