Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthrough새로운 친구 관리, 방 삭제, 사용자 검색 기능을 추가합니다. API 레이어, 타입 정의, React Query 훅, UI 컴포넌트를 일관된 패턴으로 구현하고 테스트 페이지 레이아웃을 재구성합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant FriendCard
participant RemoveFriendButton
participant useRemoveFriend Hook
participant QueryClient
participant API as /api/v1/friends/{slug}
User->>FriendCard: 친구 카드 렌더링
FriendCard->>RemoveFriendButton: 버튼 렌더링
User->>RemoveFriendButton: 클릭
RemoveFriendButton->>useRemoveFriend Hook: mutate(targetSlug)
useRemoveFriend Hook->>API: DELETE 요청
API-->>useRemoveFriend Hook: boolean 응답
useRemoveFriend Hook->>QueryClient: ["friends"] 캐시 무효화
QueryClient-->>FriendCard: 친구 목록 갱신
FriendCard-->>User: 업데이트된 목록 표시
sequenceDiagram
actor User
participant UserSearchBox
participant useSearchUsers Hook
participant UserSearchList
participant API as /api/v1/user-profiles
User->>UserSearchBox: 검색어 입력
UserSearchBox->>useSearchUsers Hook: params.query 전달
useSearchUsers Hook->>API: GET 요청
API-->>useSearchUsers Hook: SearchUsersResponse 반환
useSearchUsers Hook-->>UserSearchList: 검색 결과 전달
UserSearchList->>UserSearchList: SearchUser[] 매핑
UserSearchList-->>User: UserSearchCard 목록 렌더링
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 이유: 광범위한 파일 분산 (30개 이상 신규 파일), 일관된 패턴의 반복 구현 (API → 타입 → 훅 → UI), 친구 관리, 방 삭제, 사용자 검색 등 여러 도메인을 포괄하는 다양한 기능 추가. 각 기능별로 타입 안정성, API 응답 처리, React Query 설정, UI 상태 관리를 세밀하게 검토해야 하며, 테스트 페이지 레이아웃 변경도 함께 검토 필요. Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧹 Recent nitpick comments
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (6)
🧰 Additional context used📓 Path-based instructions (3)**/*⚙️ CodeRabbit configuration file
Files:
**/api/**/*.ts*⚙️ CodeRabbit configuration file
Files:
**/ui/**/*.tsx⚙️ CodeRabbit configuration file
Files:
🧬 Code graph analysis (5)src/features/friend/remove/hooks/useRemoveFriend.ts (3)
src/features/friend/requests/hooks/useFetchReceivedFriendRequest.ts (3)
src/features/friend/requests/api/fetchReceivedFriendRequests.ts (3)
src/entities/room/api/deleteRoom.ts (2)
src/features/friend/list/ui/FriendsList.tsx (2)
🔇 Additional comments (7)
✏️ Tip: You can disable this entire section by setting 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 @aryu1217, 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은 애플리케이션에 포괄적인 친구 관리 기능을 도입하고, 기존 방 생성 기능을 개선하며 방 삭제 기능을 추가합니다. 또한, 사용자 검색 기능을 통해 다른 사용자를 찾고 친구 요청을 보낼 수 있도록 합니다. 이러한 변경 사항들은 사용자 상호작용을 풍부하게 하고, 전반적인 코드 구조를 개선하여 유지보수성을 높이는 데 중점을 둡니다. 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
이번 PR은 친구 관련 기능(목록, 검색, 추가/삭제, 요청)을 대거 추가하는 내용이네요. 전반적으로 기능별로 코드가 잘 구조화되어 있고, React Query를 일관되게 사용하여 데이터 페칭 및 상태 관리를 잘 처리하고 있습니다. 몇몇 리팩토링을 통해 프로젝트 구조도 개선된 점이 좋습니다. 다만, 리뷰에서 지적한 몇 가지 치명적인 오류와 오타, 디버그용 코드 등이 있으니 머지 전에 꼭 수정이 필요해 보입니다. 특히 백엔드와의 데이터 불일치 문제와 잘못된 import는 신중하게 다루어야 할 부분입니다.
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/features/room/list/ui/RoomListTest.tsx (1)
3-49: 테스트 페이지(/test)의 프로덕션 노출 방지 및 인라인 props 최적화 필요
/test라우트는 현재 환경 플래그나 인증 가드 없이 접근 가능하며,RoomsListTest의DeleteRoomButton이 의도치 않게 프로덕션에 노출될 수 있습니다. 테스트용 UI는 배포 번들에서 제외하거나 개발 환경에만 노출되도록 별도 가드를 추가해야 합니다.또한
RoomCard에 전달되는actionsprop이 매번 새로운 객체로 생성되고 있습니다. 리스트 렌더링 시 인라인 JSX 객체/함수를 피하고, 필요하면useMemo로 감싸거나 컴포넌트 외부에서 정의하세요.src/app/test/page.tsx (1)
1-16: production에서 /test 페이지 노출을 반드시 차단하세요 (DeleteRoomButton, CreateRoomTest 등 테스트 기능 위험)이 페이지에는
DeleteRoomButton이 실제로 포함되어 있어(RoomsListTest의 각 방 옆에 표시) production 배포 시 사용자가 실제 방을 삭제할 수 있는 상황이 발생합니다. 또한 CreateRoomTest로도 테스트용 방이 대량 생성될 수 있습니다.Server Component로 전환하여
notFound()가드를 추가하는 것이 권장됩니다:제안 diff
-"use client"; - import GoogleLoginButton from "@/src/features/auth/login-with-google/ui/googleLoginButton"; import LogoutButton from "@/src/features/auth/logout/ui/logoutButton"; import NicknameEditForm from "@/src/features/user/profile/ui/NicknameEditForm"; -import { useRouter } from "next/navigation"; +import { notFound } from "next/navigation"; import IsLogin from "@/src/entities/user/ui/IsLogin"; import CreateRoomTest from "@/src/features/room/create/ui/CreateRoomTest"; import FriendsList from "@/src/features/friend/list/ui/FriendsList"; import RoomsListTest from "@/src/features/room/list/ui/RoomListTest"; import UserSearchBox from "@/src/features/user/search/ui/UserSearchBox"; import FriendsRequestList from "@/src/features/friend/requests/ui/FriendsReceivedRequestList"; export default function TestPage() { - const router = useRouter(); + if (process.env.NODE_ENV === "production") notFound(); return ( <div className="bg-white p-4 space-y-4"> <GoogleLoginButton /> <LogoutButton /> @@ - <button - onClick={() => router.push("/home")} - className="border cursor-pointer" - > - go to home page - </button> + <a href="/home" className="border cursor-pointer inline-block px-2 py-1"> + go to home page + </a> </div> ); }주의: IsLogin, NicknameEditForm 등 모든 child 컴포넌트가 이미 client component("use client" 지정 또는 hooks 사용)이므로, Server Component 전환 시에도 정상 작동합니다. 다만 router.push()는
<a>태그로 대체할 수 있는 간단한 네비게이션만 가능합니다. 더 복잡한 라우팅이 필요하면 child component에서 처리하세요.
🤖 Fix all issues with AI agents
In `@src/entities/room/api/deleteRoom.ts`:
- Around line 4-9: The deleteRoom function builds the request URL using the raw
slug which can contain characters like /, ?, #, % and break URL parsing or cause
security issues; update the URL construction in deleteRoom to pass slug through
encodeURIComponent (e.g., use `/api/v1/rooms/${encodeURIComponent(slug)}`) so
the path parameter is safely encoded before calling axiosInstance.delete and
returning res.data.result.
In `@src/entities/room/ui/RoomCard.tsx`:
- Around line 1-8: The file declares a local RoomTag type and uses
React.ReactNode without importing React which will cause TS errors; replace the
local RoomTag with the canonical type from src/entities/room/model/types by
adding an import like `import type { RoomTag } from
'src/entities/room/model/types'` and import the React node type instead of
referencing the namespace (e.g., `import type { ReactNode } from 'react'`) then
update the Props type to use ReactNode and the imported RoomTag; ensure the
identifiers referenced are Props, RoomTag, and React.ReactNode in RoomCard.tsx
so the compiler resolves types correctly.
In `@src/features/friend/list/model/types.ts`:
- Around line 1-4: FetchFriendsParams.lastId is typed as number but Friend.id is
a string, causing a type mismatch for cursor pagination; update
FetchFriendsParams (the FetchFriendsParams type and its lastId property) to use
string (or a shared domain alias like FriendId) so it matches the Friend.id
type, and update any callers to pass/handle string IDs accordingly.
In `@src/features/friend/list/ui/FriendCard.tsx`:
- Around line 6-16: The FriendCard UI is using swapped backend fields (passing
friend.nickname into RemoveFriendButton as targetSlug), which breaks domain
meaning; move the swap into the API/adapter layer by normalizing results in
fetchFriends so the returned Friend objects always have the correct slug and
nickname. Update fetchFriends (or its response mapping) to map backendSlug->slug
and backendNickname->nickname (or swap fields accordingly) so FriendCard can
pass RemoveFriendButton targetSlug={friend.slug} without special-casing; leave
UI components (FriendCard, RemoveFriendButton) unchanged and rely on normalized
data from fetchFriends.
In `@src/features/friend/remove/api/removeFriend.ts`:
- Around line 5-11: The removeFriend function lacks an explicit return type;
update its signature to return Promise<boolean> (i.e., declare removeFriend(...)
: Promise<boolean>) to match other API helpers like
sendFriendRequest/acceptFriendRequest and ensure type stability; optionally, if
you prefer defensive encoding, wrap targetSlug with encodeURIComponent when
building the URL (but encoding is not required per existing similar APIs).
In `@src/features/friend/remove/hooks/useRemoveFriend.ts`:
- Around line 3-18: 현재 useRemoveFriend에서 제네릭으로만 사용하는 RemoveFriendParams를 일반
import로 가져오고 있으므로 런타임 번들에 불필요하게 포함될 수 있습니다; import 문에서 RemoveFriendParams를 타입
전용으로 변경하세요 (즉, RemoveFriendParams를 type-only import로 바꿔서 불필요한 런타임 import를 방지하고
컴파일러 설정과 호환되게 만듭니다), 대상 식별자는 파일의 RemoveFriendParams와 useRemoveFriend 함수입니다.
In `@src/features/friend/requests/api/sendFriendRequest.ts`:
- Around line 6-14: The sendFriendRequest function assumes res.data.result
exists; add explicit response validation and handling: after calling
axiosInstance.post in sendFriendRequest, check HTTP status (handle 204 No
Content) and verify res.data is an object and typeof res.data.result ===
"boolean" before returning it; if validation fails, either throw a descriptive
error or return a safe default (e.g., false) and document the contract in a
comment referencing ApiResponse<boolean>, sendFriendRequest, and axiosInstance
so future callers know the expected backend shape.
In `@src/features/friend/requests/hooks/useFetchRecivedFriendRequest.ts`:
- Around line 15-21: The queryKey in useFetchRecivedFriendRequest (queryKey:
["friendRequests","recived", ...]) has a spelling typo ("recived") that can
prevent cache invalidation; change it to "received" and ideally centralize the
key by using a shared constant (e.g., FRIEND_REQUESTS_KEY or a tuple factory)
used by useFetchRecivedFriendRequest and any invalidateQueries callers so both
use the exact same identifier; update any references to the old key string to
the new constant to ensure invalidation (e.g., where accept/reject handlers call
invalidateQueries).
- Around line 3-14: Replace the server-side ApiError import with the project's
shared error type: remove the import from "next/dist/server/api-utils" and
import the shared ApiError (e.g., from "src/shared/api/api-error" or the
project's shared path) and update the generic on useQuery in
useFetchRecivedFriendRequest to use that shared ApiError type so the client hook
no longer depends on next/dist/server.
In `@src/features/friend/requests/ui/FriendsReceivedRequestList.tsx`:
- Around line 26-30: The list currently renders FriendRequestCard directly under
a <ul>, breaking semantics/accessibility; update FriendsReceivedRequestList to
place each FriendRequestCard inside an <li> (e.g., map to <li
key={item.requestId}><FriendRequestCard item={item} /></li>) or alternatively
change FriendRequestCard’s root element to an <li> and keep the existing
mapping; ensure keys remain on the <li> and that any styling/props on
FriendRequestCard still apply.
🧹 Nitpick comments (31)
src/features/user/profile/ui/NicknameEditForm.tsx (1)
9-48: 닉네임 변경 시 중복체크 결과(stale) 무효화/제출 조건을 명확히 하는 게 안전합니다.
현재는nickname이 바뀌어도isAvailable이 유지될 수 있고,isAvailable !== true여도 제출이 가능합니다(의도라면 OK).제안 diff
-import { useState } from "react"; +import { useEffect, useState } from "react"; import { useUpdateMe } from "../hooks/useUpdateMe"; import CheckNicknameButton from "@/src/features/user/profile/ui/CheckNicknameButton"; export default function NicknameEditForm() { const { mutate, isPending, error } = useUpdateMe(); const [nickname, setNickname] = useState(""); const [isAvailable, setIsAvailable] = useState<boolean | null>(null); + useEffect(() => { + setIsAvailable(null); + }, [nickname]); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); const trimmed = nickname.trim(); if (!trimmed) return; mutate({ nickname: trimmed }); }; @@ <button type="submit" className="border px-3 py-1 cursor-pointer" - disabled={isPending} + disabled={isPending || isAvailable !== true} > {isPending ? "변경 중..." : "변경하기"} </button>src/features/friend/remove/model/types.ts (1)
1-3: API/훅/UI 간 파라미터 타입 공유 👍
다만targetSlug: string은 추후UserSlug같은 도메인 타입 별칭(브랜딩)으로 의미를 더 드러내면 확장/안전성에 도움이 됩니다.src/entities/room/api/createRoom.ts (1)
5-18: 응답 타입 중복 제거 +slug누락 가드 추가를 권장합니다.
지금은res.data.result.slug가 없으면{ slug: undefined }가 반환될 수 있습니다.제안 diff
import { axiosInstance } from "@/src/shared/api/axiosInstance"; import type { ApiResponse } from "@/src/shared/api/types"; import type { CreateRoomPayload, CreateRoomResult } from "./types"; -type CreateRoomResponse = ApiResponse<{ slug: string }>; +type CreateRoomResponse = ApiResponse<CreateRoomResult>; export async function createRoom( payload: CreateRoomPayload ): Promise<CreateRoomResult> { const res = await axiosInstance.post<CreateRoomResponse>( "/api/v1/rooms", payload ); const slug = res.data.result.slug; + if (!slug) { + throw new Error("createRoom: missing slug in response"); + } return { slug }; }src/features/friend/remove/ui/RemoveFriendButton.tsx (1)
3-17: 친구 삭제 UX: 확인(confirm) + 실패(error) 처리 경로를 UI에 노출 권장지금은
isPending만 반영되고 실패 시 사용자가 알 수 있는 경로가 없습니다(네트워크/권한/서버 오류). 최소한onError토스트/인라인 메시지 또는 확인 다이얼로그(실수 방지)를 추가하는 게 좋아요. 추가로RemoveFriendParams는 type-only import 권장,onClick인라인 함수는 필요 시 핸들러로 분리해 props 변경/리렌더 노이즈를 줄일 수 있습니다.Proposed fix (type-only import + click handler 분리 예시)
import { useRemoveFriend } from "../hooks/useRemoveFriend"; -import { RemoveFriendParams } from "../model/types"; +import type { RemoveFriendParams } from "../model/types"; export default function RemoveFriendButton({ targetSlug }: RemoveFriendParams) { const { mutate, isPending } = useRemoveFriend(); + const handleClick = () => { + // 필요 시: if (!confirm("정말 친구를 삭제할까요?")) return; + mutate({ targetSlug }); + }; return ( <button type="button" className="border cursor-pointer" - onClick={() => mutate({ targetSlug })} + onClick={handleClick} disabled={isPending} > {isPending ? "삭제중..." : "친구 삭제"} </button> ); }src/features/room/create/ui/CreateRoomTest.tsx (2)
5-46:React.FormEvent참조/trim 중복/빈 tags 전송 형태는 한 번 정리하는 게 안전
- Line 33:
React.FormEvent는 프로젝트 설정에 따라React식별자가 스코프에 없어서 TS 에러가 날 수 있어FormEvent를 type import해서 쓰는 편이 안전합니다.- Line 41:
password.trim()이 2번 호출됩니다(작지만 불필요).- Line 42:
tags: selectedTagSlugs는 항상 배열을 보냅니다. 서버가 “미지정”을undefined로 기대하는 경우가 있어, 빈 배열 전송이 허용되는지 확인해 주세요.Proposed fix (FormEvent type import + trim 1회)
-import { useState } from "react"; +import { useState } from "react"; +import type { FormEvent } from "react"; import { useCreateRoom } from "@/src/features/room/create/model/useCreateRoom"; import { useRoomTags } from "@/src/entities/room/hooks/useRoomTags"; @@ - const onSubmit = (e: React.FormEvent) => { + const onSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); const t = title.trim(); if (!t) return; + const pw = password.trim(); mutate({ title: t, - password: password.trim() ? password.trim() : undefined, + password: pw ? pw : undefined, tags: selectedTagSlugs, }); };
75-113: 태그 선택 버튼 접근성/표시값(슬러그 vs 이름) 개선 여지현재 “선택됨” 영역은 slug를 그대로 노출합니다(Line 109-112). 테스트 UI라면 괜찮지만, 사람이 읽는 용도면 tag name으로 보여주는 편이 유용합니다. 또한 토글 버튼은
aria-pressed를 주면 선택 상태를 더 명확히 표현할 수 있어요.src/features/user/search/model/types.ts (1)
1-19: 도메인 타입 관점에서id: number는 브랜딩(또는 명명)으로 의미를 더 드러내는 편 추천가이드라인 관점에서
id: number는 의미가 약해서(ProfileId/UserId등) 최소한 별칭/브랜딩으로 의도를 드러내는 게 좋아요. 또한UserRelationship이"NONE" | "FRIEND"로 고정되어 있는데, 서버가 추후"REQUESTED","RECEIVED","BLOCKED"같은 상태를 추가할 가능성이 있어 스펙을 확인하고 확장 가능한 형태로 맞추는 걸 권장합니다.User도 type-only import가 안전합니다.Proposed fix (type-only import + branded id 예시)
-import { User } from "@/src/entities/user/model/types"; +import type { User } from "@/src/entities/user/model/types"; +declare const userIdBrand: unique symbol; +export type UserId = number & { readonly [userIdBrand]: void }; + export type UserRelationship = "NONE" | "FRIEND"; export type SearchUser = User & { - id: number; + id: UserId; relationship: UserRelationship; };src/features/user/search/hooks/useSearchUsers.ts (1)
6-12:queryKey/queryFn에서query정규화(trim) 일관성 맞추기
Line 8-10에서enabled는 trim 기준인데,queryKey와 실제 요청은 rawparams.query를 사용해서"a"vs"a "가 다른 캐시/요청으로 취급될 수 있어요.const query = params.query.trim()으로 정규화 후queryKey와searchUsers입력에 동일하게 쓰는 쪽을 권장합니다.
또한 검색은 입력 변화가 잦아서, 가능하면queryFn에서 React Query의signal을searchUsers로 전달해 이전 요청을 취소(abort)할 수 있게 해두면 UX/트래픽에 유리합니다.제안 diff
export function useSearchUsers(params: SearchUserParams) { + const query = params.query.trim(); return useQuery<SearchUsersResponse, ApiError>({ - queryKey: ["searchUsers", params.query, params.lastId, params.limit], - queryFn: () => searchUsers(params), - enabled: params.query.trim().length > 0, + queryKey: ["searchUsers", query, params.lastId, params.limit], + queryFn: ({ signal }) => searchUsers({ ...params, query }, { signal }), + enabled: query.length > 0, retry: false, }); }src/features/friend/requests/model/types.ts (1)
1-26: ID/날짜 필드에 의미 있는 타입 별칭(또는 브랜디드 타입) 도입 고려
requestId,requesterId,createdAt가 모두number|string원시 타입이라 다른 ID/문자열과 섞여도 컴파일 타임에 잡기 어렵습니다. 최소한type FriendRequestId = number,type UserId = number,type IsoDateTimeString = string같은 별칭을 두고 여기서 사용하거나(더 좋게는) API 모듈에서createdAt을Date로 변환해 도메인 타입으로 올리는 구조를 추천합니다. (코딩 가이드라인의 “도메인 타입으로 의미 드러내기” 기준)src/features/friend/requests/hooks/useSendFriendRequest.ts (2)
8-16:invalidateQueries(["searchUsers"])로 인한 피처 간 결합도 줄이기(키 상수화 추천)
친구요청 훅이["searchUsers"]를 직접 알고 있어서friend/requests → user/search로 결합이 생깁니다.src/features/user/search/model/queryKeys.ts같은 곳에searchUsersQueryKey(...)(또는SEARCH_USERS_QUERY_KEY)를 export하고 여기서는 그 상수를 사용하면 변경에 강해져요.
추가로 Line 13은mutationFn: sendFriendRequest로 단순화 가능합니다.제안 diff(개념)
return useMutation<boolean, ApiError, SendFriendRequestPayload>({ mutationKey: ["friendRequests", "send"], - mutationFn: (payload) => sendFriendRequest(payload), - onSuccess: () => qc.invalidateQueries({ queryKey: ["searchUsers"] }), + mutationFn: sendFriendRequest, + onSuccess: () => qc.invalidateQueries({ queryKey: ["searchUsers"] }), });
18-18: TODO는 이슈로 트래킹 권장
“보낸 친구요청 목록”은 범위가 커질 수 있어서, 코멘트로 남기기보다 이슈/태스크로 분리해 스펙(페이징 키, 응답 타입, UI/캐시 전략) 확정 후 진행하는 게 좋아요.src/features/friend/requests/api/acceptFriendRequest.ts (1)
5-13: 코드베이스 전체 API 패턴 일관성 고려하여 가드 추가 검토 필요현재
acceptFriendRequest와 모든 friend API(fetchFriends,removeFriend,sendFriendRequest등)가 동일하게res.data.result를 직접 반환하는 패턴을 사용 중입니다.ApiResponse<T> = { result: T }스펙이 명확하고 axiosInstance 응답 인터셉터가 에러를 처리하고 있어 현재 상태는 안전합니다.다만 타입 단언만으로는 런타임 검증이 없다는 점은 여전히 유효합니다. 응답 가드를 추가한다면, 일관성 있게 모든 API에 동일하게 적용하거나 공통 유틸 함수로 추출하는 것을 권장합니다. 단일 API에만 추가하면 패턴 일관성이 깨집니다.
src/features/friend/requests/hooks/useAcceptFriendRequest.ts (1)
8-19: 쿼리키 상수화로 invalidate 누락 리스크를 줄이세요
["friendRequests", "received"]/["friends"]/["searchUsers"]가 다른 곳과 철자/구조가 조금이라도 어긋나면 무효화가 조용히 실패합니다. 이 훅/관련 훅들에서 쿼리키를 상수(또는 queryKeys 유틸)로 통일하는 걸 권장합니다.제안 diff
export function useAcceptFriendRequest() { const qc = useQueryClient(); + const receivedRequestsKey = ["friendRequests", "received"] as const; + const friendsKey = ["friends"] as const; + const searchUsersKey = ["searchUsers"] as const; + return useMutation<boolean, ApiError, AcceptFriendRequestParams>({ mutationKey: ["friendRequests", "accept"], mutationFn: (params) => acceptFriendRequest(params), onSuccess: () => { - qc.invalidateQueries({ queryKey: ["friendRequests", "received"] }); - qc.invalidateQueries({ queryKey: ["friends"] }); - qc.invalidateQueries({ queryKey: ["searchUsers"] }); + qc.invalidateQueries({ queryKey: receivedRequestsKey }); + qc.invalidateQueries({ queryKey: friendsKey }); + qc.invalidateQueries({ queryKey: searchUsersKey }); }, }); }src/features/friend/requests/ui/AcceptFriendRequestButton.tsx (2)
3-9: UI props를 요청 DTO 타입과 분리하는 게 안전합니다
AcceptFriendRequestParams는 “API 요청 파라미터” 성격이라 UI props로 직접 쓰면 결합도가 커집니다(필드 추가/변경 시 UI가 연쇄 수정).Pick또는 별도Props타입으로 분리 권장합니다.제안 diff
import { useAcceptFriendRequest } from "../hooks/useAcceptFriendRequest"; -import { AcceptFriendRequestParams } from "../model/types"; +import type { AcceptFriendRequestParams } from "../model/types"; + +type Props = Pick<AcceptFriendRequestParams, "requestId">; export default function AcceptFriendRequestButton({ requestId, -}: AcceptFriendRequestParams) { +}: Props) { const { mutate, isPending, isError, error } = useAcceptFriendRequest();
11-23: 에러 메시지 컨테이너에role="alert"추가를 고려하세요현재도 동작은 하지만, 실패 메시지를 즉시 알리려면
role="alert"정도는 붙이는 편이 낫습니다.제안 diff
- {isError && <div className="border">{error?.message}</div>} + {isError && ( + <div className="border" role="alert"> + {error?.message} + </div> + )}src/features/friend/requests/ui/FriendRequestCard.tsx (1)
12-16:createdAt원문 문자열 노출은 UX 이슈가 될 수 있어요서버가 ISO 문자열을 내려주면 그대로 보이기 쉬워서, 최소한 로케일 포맷팅(또는 상대시간)을 적용하는 쪽을 권장합니다.
src/features/friend/requests/api/fetchRecivedFriendRequests.ts (1)
1-16: API 모듈에서 “DTO → Domain 정규화” 책임을 갖는 구조를 고려하세요현재는
ReceivedFriendRequestsResponse를 그대로 반환하는데, UI가 응답 shape(예:createdAt: string)에 직접 의존하기 쉬워집니다. 가능하면...Dto타입을 분리하고 이 계층에서 도메인 형태로 매핑하는 패턴을 추천합니다(가이드라인 기준).
추가로 파일명fetchRecivedFriendRequests.ts는Received로 통일하는 게 좋습니다(향후 검색/자동완성/쿼리키 문자열과 함께 오타 전파 위험).src/features/friend/requests/ui/FriendsReceivedRequestList.tsx (1)
3-4:Recived/컴포넌트 명칭 오타로 유지보수 비용 증가 (Received로 통일 권장)
- Line 3, Line 17:
useFetchRecivedFriendRequest,recived등 오타가 타입/훅/쿼리키까지 전파된 상태로 보여서, 이후 검색/리팩터링/온보딩에 계속 비용이 생깁니다.- Line 6: 파일명은
FriendsReceivedRequestList.tsx인데 컴포넌트는FriendsRequestList라서 import 시 혼란 여지가 큽니다.제안 diff (이 파일 내에서 가능한 범위)
-import { useFetchRecivedFriendRequest } from "../hooks/useFetchRecivedFriendRequest"; +import { useFetchRecivedFriendRequest } from "../hooks/useFetchRecivedFriendRequest"; import FriendRequestCard from "./FriendRequestCard"; -export default function FriendsRequestList() { +export default function FriendsReceivedRequestList() { const { data, isLoading, isError } = useFetchRecivedFriendRequest({ limit: 20, }); - const recived = data?.items ?? []; + const received = data?.items ?? []; ... - {recived.length === 0 ? ( + {received.length === 0 ? ( ... - {recived.map((item) => ( + {received.map((item) => ( <FriendRequestCard key={item.requestId} item={item} /> ))}Also applies to: 6-7, 17-17
src/features/friend/requests/ui/SendFriendRequestButton.tsx (2)
13-22: disabled 상태인데cursor-pointer고정 + 에러 메시지 무스타일 노출
- Line 15-19: disabled인데도 포인터 커서가 유지됩니다(UX 미세 저하).
- Line 22:
error.message를 그대로 노출하면 서버 메시지(내부 사유)가 그대로 사용자에게 보일 수 있어, 사용자 친화 문구로 매핑하거나 최소한 스타일/role을 주는 편이 안전합니다.제안 diff
<button type="button" - className="border cursor-pointer" + className="border cursor-pointer disabled:cursor-not-allowed disabled:opacity-60" onClick={() => mutate({ targetSlug })} disabled={isPending} > {isPending ? "요청중..." : "친구추가"} </button> - {isError && <div>{error?.message}</div>} + {isError && ( + <div className="text-sm text-red-600" role="alert"> + 요청에 실패했습니다. + </div> + )}
6-23: 보낸 친구요청 목록 기능 추가 시 해당 쿼리키도 invalidate 필요Line 14의
onSuccess는 현재["searchUsers"]만 invalidate하는데, 이는 검색 결과의 "요청중" 상태를 갱신하기 위해 적절합니다. 다만 코드의 18번 줄 주석에서 이미 언급했듯이, 향후 "보낸 친구요청 목록" 기능이 추가된다면 해당 쿼리키도 함께 invalidate해야 할 것으로 보입니다.참고로 친구 목록(
["friends"])은 친구 요청 수락(useAcceptFriendRequest) 또는 삭제(useRemoveFriend)할 때만 invalidate되므로, 요청 발송 시점에는 invalidate하지 않아도 됩니다.src/features/user/search/ui/UserSearchCard.tsx (2)
3-4: 모듈 경계: user/search UI가 friend/requests UI에 직접 의존(결합도↑)
Line 3, Line 23:UserSearchCard가 “유저 검색 카드” 역할을 넘어 “친구요청 보내기 액션”까지 내장해서, user feature가 friend feature에 직접 결합됩니다.
- 추천:
UserSearchCard는 표시만 담당하고, 우측 액션은 상위(페이지/리스트)에서 주입(render prop/slot)하도록 바꾸면 책임이 더 명확해집니다. (코딩 가이드의 “모듈 경계 최우선” 기준)제안 diff (slot 주입)
-import SendFriendRequestButton from "@/src/features/friend/requests/ui/SendFriendRequestButton"; import type { SearchUser } from "../model/types"; -export default function UserSearchCard({ user }: { user: SearchUser }) { +export default function UserSearchCard({ + user, + right, +}: { + user: SearchUser; + right?: React.ReactNode; +}) { return ( <div className="border p-3 text-black flex justify-between"> ... - <div> - <SendFriendRequestButton targetSlug={user.slug} /> - </div> + <div>{right}</div> </div> ); }Also applies to: 6-25
16-19: relationship 값이 내부 enum/raw 값으로 노출될 가능성
Line 16-19:{user.relationship}그대로 출력하면 사용자에게 의미 없는 값(예:PENDING,NONE등)이 보일 수 있어 매핑 테이블로 UI 문구를 분리하는 편이 안전합니다.src/features/user/search/ui/UserSearchList.tsx (1)
11-18: 리스트 key는 slug보다id가 더 안전 (충돌/변경 가능성)
Line 16:SearchUser에id: number가 있는데도user.slug를 key로 쓰고 있습니다. slug가 변경되거나 중복될 여지가 있으면 렌더링 안정성이 깨질 수 있어user.id가 더 안전합니다.제안 diff
{items.map((user) => ( - <UserSearchCard key={user.slug} user={user} /> + <UserSearchCard key={user.id} user={user} /> ))}src/features/user/search/ui/UserSearchBox.tsx (2)
8-10: 타이핑마다 즉시 검색 호출 → 디바운스/Deferred 적용 권장
Line 10, Line 19: 현재는 입력 변경마다 쿼리가 바뀌어 호출될 가능성이 높습니다. 서버/쿼터/UX 측면에서useDeferredValue또는 디바운스(예: 200~300ms) 적용을 추천합니다.제안 diff (useDeferredValue)
import { useState } from "react"; +import { useDeferredValue } from "react"; import { useSearchUsers } from "../hooks/useSearchUsers"; import UserSearchList from "./UserSearchList"; export default function UserSearchBox() { const [query, setQuery] = useState(""); + const deferredQuery = useDeferredValue(query); - const { data, isLoading, isError } = useSearchUsers({ query }); + const { data, isLoading, isError } = useSearchUsers({ query: deferredQuery });Also applies to: 15-20
14-20: 입력 접근성: label/aria-label 없이 placeholder만 존재
Line 15-18: placeholder는 라벨을 대체하지 못하니(특히 스크린리더/자동완성), 최소aria-label이라도 추가하는 게 안전합니다.제안 diff
<input className="border px-2 py-1 w-full" placeholder="유저 검색 (slug/닉네임)" + aria-label="유저 검색 (slug/닉네임)" value={query} onChange={(e) => setQuery(e.target.value)} />src/features/friend/list/ui/FriendsList.tsx (1)
6-29:console.log제거 + 리스트 key는slug보다id우선 권장
- Line 8의
console.log(data)는 프로덕션 번들/로그 노이즈라 제거하는 게 좋아요.- Line 26에서 key로
f.slug를 쓰는데, slug가 변경 가능하거나 유니크 보장이 약하면 리렌더/상태 꼬임이 생깁니다.Friend.id가 있으면key={f.id}가 더 안전합니다.Proposed fix
export default function FriendsList() { const { data, isLoading, isError } = useFriendsList({ size: 20 }); - console.log(data); if (isLoading) return <div className="border p-4 text-black">친구목록 로딩중...</div>; if (isError) return <div className="border p-4 text-black">친구목록 로딩 실패</div>; @@ <ul className="space-y-2"> {friends.map((f) => ( - <FriendCard key={f.slug} friend={f} /> + <FriendCard key={f.id} friend={f} /> ))} </ul> )} </div> ); }src/features/friend/list/hooks/useFriendsList.ts (1)
7-12: queryKey는 괜찮지만, params 확장 대비 “객체 key” 형태도 고려해볼 만해요현재는
lastId/size만 키에 들어가서 문제는 적지만, 필드가 늘면 배열 key가 길어지고 실수하기 쉬워집니다.queryKey: ["friends", { lastId: ..., size: ... }]처럼 객체로 묶으면 유지보수성이 좋아집니다.src/entities/friend/model/types.ts (1)
1-11:FriendsListResponse가 “도메인 타입”인지 “DTO”인지 경계가 애매합니다.
src/entities/friend/model/types.ts에FriendsListResponse(items/hasNext)가 들어가면 UI/네트워크 응답 형태가 엔티티 레벨로 전파될 수 있어요. DTO라면src/features/.../api(또는src/entities/.../api) 쪽으로 두고toDomain매핑을 통해Friend[]같은 도메인 형태로만 노출하는 쪽을 권장합니다. (가이드라인의**/api/**/*.ts*책임 기준)또한
Friend.id: string인데, 페이지네이션 커서가lastId?: number로 설계돼 있으면 타입 충돌 여지가 큽니다(서버 스펙 확인 필요).src/features/friend/list/api/fetchFriends.ts (1)
1-15: API 모듈에서 “raw 응답 타입”을 그대로 반환하지 않게 정규화(toDomain)하는 구조가 더 안전합니다.현재
fetchFriends()가ApiResponse<FriendsListResponse>의result를 그대로 리턴해서, UI/훅이 네트워크 DTO 형태에 결합될 가능성이 큽니다.FriendsListResponse를FriendsListDto로 두고, 여기서toDomain변환(예:Friend[]+hasNext)까지 책임지는 형태를 권장합니다. (코딩 가이드라인**/api/**/*.ts*기준)추가로
FetchFriendsParams.lastId?: number와Friend.id: string의 커서 타입 불일치 가능성은 실제 런타임 버그(페이지네이션 끊김)로 이어질 수 있으니 서버 스펙과 반드시 맞춰주세요.src/app/test/page.tsx (2)
21-45: 2단 레이아웃 반응형 처리 + 불필요한 래퍼 제거를 권장합니다
- 현재
flex gap-42단 고정이라 모바일에서 가독성이 급격히 떨어질 수 있어flex-col md:flex-row같은 반응형 분기가 있으면 좋겠습니다.FriendsRequestList를 감싸는<div>(Line 38-40)는 역할이 없어서 제거해도 됩니다.- (유지보수 관점) “닉네임/검색”, “방/친구”처럼 섹션 경계가 명확하니
<section>+ 제목 컴포넌트 같은 얕은 구조화도 고려할 만합니다.예시(클래스만):
- <div className="flex gap-4"> + <div className="flex flex-col md:flex-row gap-4"> ... - <div className="flex gap-4"> + <div className="flex flex-col md:flex-row gap-4"> ... - <div> - <FriendsRequestList /> - </div> + <FriendsRequestList />
5-12: FriendsRequestList 파일/컴포넌트 네이밍 정합성만 확인해 주세요
FriendsRequestList를FriendsReceivedRequestList파일에서 가져오고 있어(임포트 경로 기준) “받은 요청/보낸 요청/전체 요청”이 늘어날 때 혼동될 여지가 있습니다.
가능하면 파일명/컴포넌트명/표시 텍스트가 같은 축(Received/Sent/Requests)으로 정렬되도록 정리해 두는 걸 권장합니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (40)
src/app/test/page.tsxsrc/entities/friend/model/types.tssrc/entities/room/api/createRoom.tssrc/entities/room/api/deleteRoom.tssrc/entities/room/hooks/useDeleteRoom.tssrc/entities/room/ui/RoomCard.tsxsrc/features/friend/list/api/fetchFriends.tssrc/features/friend/list/hooks/useFriendsList.tssrc/features/friend/list/model/types.tssrc/features/friend/list/ui/FriendCard.tsxsrc/features/friend/list/ui/FriendsList.tsxsrc/features/friend/remove/api/removeFriend.tssrc/features/friend/remove/hooks/useRemoveFriend.tssrc/features/friend/remove/model/types.tssrc/features/friend/remove/ui/RemoveFriendButton.tsxsrc/features/friend/requests/api/acceptFriendRequest.tssrc/features/friend/requests/api/fetchRecivedFriendRequests.tssrc/features/friend/requests/api/sendFriendRequest.tssrc/features/friend/requests/hooks/useAcceptFriendRequest.tssrc/features/friend/requests/hooks/useFetchRecivedFriendRequest.tssrc/features/friend/requests/hooks/useSendFriendRequest.tssrc/features/friend/requests/model/types.tssrc/features/friend/requests/ui/AcceptFriendRequestButton.tsxsrc/features/friend/requests/ui/FriendRequestCard.tsxsrc/features/friend/requests/ui/FriendsReceivedRequestList.tsxsrc/features/friend/requests/ui/SendFriendRequestButton.tsxsrc/features/room/create/ui/CreateRoomTest.tsxsrc/features/room/delete/ui/DeleteRoomButton.tsxsrc/features/room/list/ui/RoomListTest.tsxsrc/features/user/profile/api/updateMe.tssrc/features/user/profile/hooks/useUpdateMe.tssrc/features/user/profile/model/types.tssrc/features/user/profile/ui/CheckNicknameButton.tsxsrc/features/user/profile/ui/NicknameEditForm.tsxsrc/features/user/search/api/searchUsers.tssrc/features/user/search/hooks/useSearchUsers.tssrc/features/user/search/model/types.tssrc/features/user/search/ui/UserSearchBox.tsxsrc/features/user/search/ui/UserSearchCard.tsxsrc/features/user/search/ui/UserSearchList.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*
⚙️ CodeRabbit configuration file
**/*: - 리뷰는 한국어로 작성.
- 유지보수/확장성 관점에서 모듈 경계(의존성 방향, 책임 분리)가 적절한지 최우선으로 확인.
- 파일/함수 책임이 과도하면 응집도/결합도 기준으로 분리/통합 개선안을 제시.
- TypeScript에서 any/과도한 as 캐스팅을 지양하고, 도메인 타입(예: ProfileId 같은 ID 타입)으로 의미를 드러내는지 확인.
- React에서 상태 위치 미스, 불필요한 리렌더링, derived state 남발, useEffect 남용을 지적하고 개선안을 제시.
Files:
src/features/friend/remove/model/types.tssrc/entities/friend/model/types.tssrc/entities/room/api/deleteRoom.tssrc/features/user/search/api/searchUsers.tssrc/features/friend/requests/hooks/useSendFriendRequest.tssrc/features/friend/remove/api/removeFriend.tssrc/features/friend/requests/ui/AcceptFriendRequestButton.tsxsrc/features/user/search/ui/UserSearchCard.tsxsrc/features/user/search/ui/UserSearchBox.tsxsrc/features/friend/list/hooks/useFriendsList.tssrc/features/friend/requests/api/sendFriendRequest.tssrc/features/friend/requests/api/fetchRecivedFriendRequests.tssrc/features/friend/list/api/fetchFriends.tssrc/entities/room/hooks/useDeleteRoom.tssrc/features/friend/list/ui/FriendCard.tsxsrc/features/friend/list/ui/FriendsList.tsxsrc/features/friend/requests/hooks/useAcceptFriendRequest.tssrc/features/friend/requests/ui/FriendRequestCard.tsxsrc/features/friend/requests/hooks/useFetchRecivedFriendRequest.tssrc/features/friend/requests/model/types.tssrc/entities/room/ui/RoomCard.tsxsrc/features/friend/remove/ui/RemoveFriendButton.tsxsrc/features/friend/remove/hooks/useRemoveFriend.tssrc/features/user/search/model/types.tssrc/features/friend/requests/ui/FriendsReceivedRequestList.tsxsrc/features/friend/requests/api/acceptFriendRequest.tssrc/features/friend/list/model/types.tssrc/features/user/search/hooks/useSearchUsers.tssrc/features/room/create/ui/CreateRoomTest.tsxsrc/features/friend/requests/ui/SendFriendRequestButton.tsxsrc/features/room/delete/ui/DeleteRoomButton.tsxsrc/features/user/search/ui/UserSearchList.tsxsrc/features/user/profile/ui/NicknameEditForm.tsxsrc/app/test/page.tsxsrc/entities/room/api/createRoom.tssrc/features/room/list/ui/RoomListTest.tsx
**/api/**/*.ts*
⚙️ CodeRabbit configuration file
**/api/**/*.ts*: - API 모듈은 DTO 타입 + 네트워크 호출 + toDomain 매핑까지 책임지는 구조를 우선 권장.
- UI에서 raw DTO(res.json())를 직접 다루지 않도록, 반환 타입은 Domain(User 등)으로 정규화.
- fetch 옵션(credentials, headers 등) 규약이 일관적인지 확인.
Files:
src/entities/room/api/deleteRoom.tssrc/features/user/search/api/searchUsers.tssrc/features/friend/remove/api/removeFriend.tssrc/features/friend/requests/api/sendFriendRequest.tssrc/features/friend/requests/api/fetchRecivedFriendRequests.tssrc/features/friend/list/api/fetchFriends.tssrc/features/friend/requests/api/acceptFriendRequest.tssrc/entities/room/api/createRoom.ts
**/ui/**/*.tsx
⚙️ CodeRabbit configuration file
**/ui/**/*.tsx: - 상태 위치가 적절한지(상/하위 이동 필요성) 지적하고 개선안 제시.
- 파생 값은 state로 두지 말고 계산(useMemo는 필요할 때만).
- 리스트 key 안정성, 불필요한 re-render 유발 props(익명 함수/객체) 지적.
Files:
src/features/friend/requests/ui/AcceptFriendRequestButton.tsxsrc/features/user/search/ui/UserSearchCard.tsxsrc/features/user/search/ui/UserSearchBox.tsxsrc/features/friend/list/ui/FriendCard.tsxsrc/features/friend/list/ui/FriendsList.tsxsrc/features/friend/requests/ui/FriendRequestCard.tsxsrc/entities/room/ui/RoomCard.tsxsrc/features/friend/remove/ui/RemoveFriendButton.tsxsrc/features/friend/requests/ui/FriendsReceivedRequestList.tsxsrc/features/room/create/ui/CreateRoomTest.tsxsrc/features/friend/requests/ui/SendFriendRequestButton.tsxsrc/features/room/delete/ui/DeleteRoomButton.tsxsrc/features/user/search/ui/UserSearchList.tsxsrc/features/user/profile/ui/NicknameEditForm.tsxsrc/features/room/list/ui/RoomListTest.tsx
src/app/**/*.ts*
⚙️ CodeRabbit configuration file
src/app/**/*.ts*: - Next.js App Router 규칙 준수: server/client 컴포넌트 경계가 적절한지 확인.
- 가능하면 데이터 패칭은 async 서버 컴포넌트/route handler에서 처리하고, 클라에서 불필요한 fetch/useEffect를 줄일 것.
- "use client"는 꼭 필요한 곳에만.
Files:
src/app/test/page.tsx
🧬 Code graph analysis (29)
src/entities/room/api/deleteRoom.ts (2)
src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/features/user/search/api/searchUsers.ts (3)
src/features/user/search/model/types.ts (2)
SearchUserParams(15-19)SearchUsersResponse(10-13)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/features/friend/requests/hooks/useSendFriendRequest.ts (3)
src/shared/api/api-error.ts (1)
ApiError(1-11)src/features/friend/requests/model/types.ts (1)
SendFriendRequestPayload(1-3)src/features/friend/requests/api/sendFriendRequest.ts (1)
sendFriendRequest(6-14)
src/features/friend/remove/api/removeFriend.ts (3)
src/features/friend/remove/model/types.ts (1)
RemoveFriendParams(1-3)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/features/friend/requests/ui/AcceptFriendRequestButton.tsx (2)
src/features/friend/requests/model/types.ts (1)
AcceptFriendRequestParams(24-26)src/features/friend/requests/hooks/useAcceptFriendRequest.ts (1)
useAcceptFriendRequest(8-20)
src/features/user/search/ui/UserSearchCard.tsx (2)
src/features/user/search/model/types.ts (1)
SearchUser(5-8)src/features/friend/requests/ui/SendFriendRequestButton.tsx (1)
SendFriendRequestButton(6-25)
src/features/user/search/ui/UserSearchBox.tsx (2)
src/features/user/search/hooks/useSearchUsers.ts (1)
useSearchUsers(6-13)src/features/user/search/ui/UserSearchList.tsx (1)
UserSearchList(6-20)
src/features/friend/list/hooks/useFriendsList.ts (4)
src/features/friend/list/model/types.ts (1)
FetchFriendsParams(1-4)src/entities/friend/model/types.ts (1)
FriendsListResponse(8-11)src/shared/api/api-error.ts (1)
ApiError(1-11)src/features/friend/list/api/fetchFriends.ts (1)
fetchFriends(6-15)
src/features/friend/requests/api/sendFriendRequest.ts (3)
src/features/friend/requests/model/types.ts (1)
SendFriendRequestPayload(1-3)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/features/friend/requests/api/fetchRecivedFriendRequests.ts (3)
src/features/friend/requests/model/types.ts (2)
FetchReceivedFriendRequestsParams(19-22)ReceivedFriendRequestsResponse(14-17)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/features/friend/list/api/fetchFriends.ts (4)
src/features/friend/list/model/types.ts (1)
FetchFriendsParams(1-4)src/entities/friend/model/types.ts (1)
FriendsListResponse(8-11)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/entities/room/hooks/useDeleteRoom.ts (1)
src/entities/room/api/deleteRoom.ts (1)
deleteRoom(4-9)
src/features/friend/list/ui/FriendCard.tsx (2)
src/entities/friend/model/types.ts (1)
Friend(1-6)src/features/friend/remove/ui/RemoveFriendButton.tsx (1)
RemoveFriendButton(6-19)
src/features/friend/list/ui/FriendsList.tsx (2)
src/features/friend/list/hooks/useFriendsList.ts (1)
useFriendsList(7-13)src/features/friend/list/ui/FriendCard.tsx (1)
FriendCard(7-19)
src/features/friend/requests/hooks/useAcceptFriendRequest.ts (3)
src/shared/api/api-error.ts (1)
ApiError(1-11)src/features/friend/requests/model/types.ts (1)
AcceptFriendRequestParams(24-26)src/features/friend/requests/api/acceptFriendRequest.ts (1)
acceptFriendRequest(5-13)
src/features/friend/requests/ui/FriendRequestCard.tsx (2)
src/features/friend/requests/model/types.ts (1)
ReceivedFriendRequest(5-12)src/features/friend/requests/ui/AcceptFriendRequestButton.tsx (1)
AcceptFriendRequestButton(6-25)
src/features/friend/requests/hooks/useFetchRecivedFriendRequest.ts (3)
src/features/friend/requests/model/types.ts (2)
FetchReceivedFriendRequestsParams(19-22)ReceivedFriendRequestsResponse(14-17)src/shared/api/api-error.ts (1)
ApiError(1-11)src/features/friend/requests/api/fetchRecivedFriendRequests.ts (1)
fetchReceivedFriendRequests(8-16)
src/entities/room/ui/RoomCard.tsx (1)
src/entities/room/model/types.ts (1)
RoomTag(1-4)
src/features/friend/remove/ui/RemoveFriendButton.tsx (2)
src/features/friend/remove/model/types.ts (1)
RemoveFriendParams(1-3)src/features/friend/remove/hooks/useRemoveFriend.ts (1)
useRemoveFriend(8-19)
src/features/friend/remove/hooks/useRemoveFriend.ts (3)
src/shared/api/api-error.ts (1)
ApiError(1-11)src/features/friend/remove/model/types.ts (1)
RemoveFriendParams(1-3)src/features/friend/remove/api/removeFriend.ts (1)
removeFriend(5-11)
src/features/user/search/model/types.ts (1)
src/entities/user/model/types.ts (1)
User(1-5)
src/features/friend/requests/ui/FriendsReceivedRequestList.tsx (2)
src/features/friend/requests/hooks/useFetchRecivedFriendRequest.ts (1)
useFetchRecivedFriendRequest(11-23)src/features/friend/requests/ui/FriendRequestCard.tsx (1)
FriendRequestCard(6-23)
src/features/friend/requests/api/acceptFriendRequest.ts (3)
src/features/friend/requests/model/types.ts (1)
AcceptFriendRequestParams(24-26)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)src/shared/api/types.ts (1)
ApiResponse(1-1)
src/features/room/create/ui/CreateRoomTest.tsx (2)
src/features/room/create/model/useCreateRoom.ts (1)
useCreateRoom(12-23)src/entities/room/hooks/useRoomTags.ts (1)
useRoomTags(4-11)
src/features/friend/requests/ui/SendFriendRequestButton.tsx (2)
src/features/friend/requests/model/types.ts (1)
SendFriendRequestPayload(1-3)src/features/friend/requests/hooks/useSendFriendRequest.ts (1)
useSendFriendRequest(8-16)
src/features/room/delete/ui/DeleteRoomButton.tsx (1)
src/entities/room/hooks/useDeleteRoom.ts (1)
useDeleteRoom(4-14)
src/app/test/page.tsx (7)
src/features/user/profile/ui/NicknameEditForm.tsx (1)
NicknameEditForm(7-57)src/features/user/search/ui/UserSearchBox.tsx (1)
UserSearchBox(7-28)src/entities/user/ui/IsLogin.tsx (1)
IsLogin(3-21)src/features/room/create/ui/CreateRoomTest.tsx (1)
CreateRoomTest(9-135)src/features/room/list/ui/RoomListTest.tsx (1)
RoomsListTest(8-50)src/features/friend/requests/ui/FriendsReceivedRequestList.tsx (1)
FriendsRequestList(6-34)src/features/friend/list/ui/FriendsList.tsx (1)
FriendsList(6-32)
src/entities/room/api/createRoom.ts (3)
src/shared/api/types.ts (1)
ApiResponse(1-1)src/entities/room/api/types.ts (2)
CreateRoomPayload(1-5)CreateRoomResult(7-9)src/shared/api/axiosInstance.ts (1)
axiosInstance(5-12)
src/features/room/list/ui/RoomListTest.tsx (3)
src/entities/room/ui/RoomCard.tsx (1)
RoomCard(10-33)src/features/room/join/JoinRoomButton.tsx (1)
JoinRoomButton(9-20)src/features/room/delete/ui/DeleteRoomButton.tsx (1)
DeleteRoomButton(9-22)
🔇 Additional comments (5)
src/features/user/profile/ui/NicknameEditForm.tsx (1)
5-5: 디렉토리 이동에 따른 import 경로 정리 적절합니다.
user-profile → user/profile 정리 흐름에 맞게 의존 경로가 일관되게 맞춰졌습니다.src/features/user/search/api/searchUsers.ts (1)
1-13: 코드는 기존 코드베이스 패턴과 일치하므로 수정 불필요리뷰 가이드라인의 "DTO + 네트워크 호출 + toDomain 매핑" 권장과 달리, 현재 코드베이스의 모든 API 모듈이 동일한 패턴을 따릅니다:
ApiResponse<T>래핑 후res.data.result직접 반환.SearchUsersResponse는 이미 User 도메인 엔티티 기반으로 정규화되어 있으며, 엔드포인트(/api/v1/user-profiles)와 params 처리(query 필수, lastId/limit 선택)도 일관성 있게 구성되어 있습니다. 추가 매핑은 불필요합니다.src/features/friend/list/ui/FriendsList.tsx (1)
7-13:isLoading사용이 react-query 버전에 따라 깨질 수 있어요(특히 v5)
@tanstack/react-queryv5에서는isPending중심으로 바뀐 케이스가 많아서, 현재 프로젝트 버전에 맞게isLoading/isPending플래그를 확인하는 게 안전합니다.src/features/friend/list/hooks/useFriendsList.ts (1)
1-13: react-query 제네릭/옵션 형태가 현재 설치 버전과 정확히 맞는지 확인 필요
useQuery<FriendsListResponse, ApiError>({...})및 상태 플래그 네이밍은 버전에 따라 차이가 나서(특히 v4↔v5), 프로젝트 의존성 버전 기준으로 타입 에러/런타임 동작을 한번 확인해 주세요.src/entities/room/hooks/useDeleteRoom.ts (1)
1-14: 쿼리 키 정합성만 확인되면 전반적으로 깔끔합니다.
onSuccess에서["rooms"]를 invalidate 하는 방식이useRoomsQuery의 실제queryKey(예:["rooms"]vs["rooms", filters])와 의도대로 매칭되는지만 확인해주세요. 가능하면mutationFn: deleteRoom로도 간단히 줄일 수 있어요.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
Title
feat: 친구 기능 전체 구현
Summary
친구 검색/요청/수락·거절/목록 조회/삭제까지 친구 관련 플로우 전반 구현
각 기능을 FSD 기준으로 feature 단위로 분리하고, 버튼/카드 UI 연결 및 상태 처리(로딩/비활성) 반영
React Query 기반으로 서버 상태 갱신(invalidate 등) 포함하여 화면 동기화
Notes
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.