Skip to content

Conversation

@leeleeleeleejun
Copy link
Member

@leeleeleeleejun leeleeleeleejun commented Nov 20, 2025

#️⃣연관된 이슈

📝작업 내용

프로필 페이지의 구조를 개선하고, 찜 기능의 UX와 데이터 동기화 로직을 강화했습니다.

1. 프로필 페이지 컴포넌트 구조 리팩토링

프로필 페이지 내에서 실제 API 데이터를 사용하는 부분이 '유저 프로필 정보' 부분에만 사용되어 있어, 관심사 분리를 위해 구조를 변경했습니다.

2.찜 쿼리 무효화(Invalidation) 로직 개선

  • 버그 수정: 찜 동작 후 페이지를 이탈했다가 재방문 시, 캐싱된 데이터로 인해 버튼 상태가 최신화되지 않는 문제를 해결했습니다.
  • 로직 강화: 좋아요 추가/취소 성공 시 '찜 리스트' 쿼리뿐만 아니라 '맛집 상세 정보' 쿼리도 함께 무효화시켜 데이터 정합성을 맞췄습니다. (Promise.all 활용하여 병렬로 처리)

3. 찜하기 버튼 낙관적 업데이트 적용

  • UX 개선: 네트워크 응답을 기다리지 않고 버튼 클릭 즉시 UI가 반응하도록 낙관적 업데이트를 도입했습니다.
  • 안정성 확보: API 요청 실패 시 이전 상태로 즉시 롤백(Rollback)되도록 에러 핸들링을 추가했습니다.
  • 중복 방지: 네트워크 요청 중 중복 클릭을 방지하는 로직(isPending)을 추가했습니다.

스크린샷 (선택)

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

Summary by CodeRabbit

  • 새로운 기능

    • 프로필 영역 개편: 프로필 사진·닉네임·배경색 표시 및 메뉴 항목 추가(등록현황, 기프티콘, 이용약관)
    • 프로필 메뉴 아이템 및 사용자 프로필 컴포넌트 추가
    • 사용자 조회 쿼리/스키마와 사용자 데이터 서비스 추가
    • 외부 이미지 도메인 허용 추가
  • 버그 수정

    • 좋아요/취소 후 관련 목록과 장소 상세 캐시를 함께 갱신하도록 개선
    • 좋아요 버튼의 낙관적 UI, 동시성 방지 및 롤백 처리 추가
  • 리팩토링

    • 여러 API 응답 처리 로직 간소화 (중복 추출 제거)

✏️ Tip: You can customize this high-level summary in your review settings.

- axiosInstance.interceptors.response 에서 응답 처리 일괄 실행
- 사용자 프로필 및 메뉴 항목 컴포넌트 분리
- UserProfile에서만 api 데이터 사용함
- 낙관적 업데이트 도입 (UI 먼저 변경 후 api실행)
@leeleeleeleejun leeleeleeleejun self-assigned this Nov 20, 2025
@leeleeleeleejun leeleeleeleejun linked an issue Nov 20, 2025 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

사용자 프로필 조회 기능을 추가하고 프로필 페이지를 재구성했으며, API 서비스 응답 처리 일부를 단순화하고 좋아요 뮤테이션 성공 시 관련 캐시(byLike 및 detail)를 병렬로 무효화하도록 변경했습니다. 또한 Next 이미지 원격 패턴과 USER API 경로를 추가했습니다.

Changes

Cohort / File(s) 변경 요약
좋아요 뮤테이션 변경
apps/web/app/_apis/mutations/useAddLike.ts, apps/web/app/_apis/mutations/useRemoveLike.ts, apps/web/app/_apis/services/like.ts
onSuccess 핸들러가 응답을 수신하도록 변경; 응답에서 placeId 추출 후 존재 시 byLike와 해당 detail(placeId) 쿼리를 병렬 무효화. 서비스 응답 처리 단순화 및 placeId 타입(string→number) 변경.
사용자 쿼리/스키마/서비스 추가
apps/web/app/_apis/queries/user.ts, apps/web/app/_apis/schemas/user.ts, apps/web/app/_apis/services/user.ts
UserQueryKeys, useUserQueries.detail(), UserSchema, User 타입, getUserData() 서비스 추가 (API_PATH.USER 사용, UserSchema로 파싱).
API 서비스 응답 처리 리팩토링
apps/web/app/_apis/services/place.ts, apps/web/app/_apis/services/event.ts, apps/web/app/_apis/services/category.ts, apps/web/app/_apis/services/request.ts
Axios 응답 중첩 구조 제거: const { data: response }; const { data } = responseconst { data } = await axios... 패턴으로 단순화.
프로필 UI 컴포넌트 추가/재구성
apps/web/app/profile/_components/UserProfile/UserProfile.tsx, apps/web/app/profile/_components/UserProfile/index.ts, apps/web/app/profile/_components/ProfileMenuItem/ProfileMenuItem.tsx, apps/web/app/profile/_components/ProfileMenuItem/index.ts, apps/web/app/profile/page.tsx, apps/web/app/profile/ProfilePage.tsx
UserProfileProfileMenuItem 컴포넌트 추가, profile/page.tsx를 HydrationBoundaryPage로 프리페치 및 새 구성으로 교체, 기존 ProfilePage.tsx 삭제.
LikeButton 최적화
apps/web/app/places/[id]/_components/LikeButton/LikeButton.tsx
낙관적 업데이트, 동시 작업 차단(isPending), 실패 시 롤백, props 변경 시 상태 동기화(useEffect) 적용.
상수 및 설정 업데이트
apps/web/app/_constants/path.ts, apps/web/next.config.ts
API_PATH.USER = '/users/me' 추가 및 Next.js 이미지 remotePatterns에 NEXT_PUBLIC_API_URL_HOST 기반 원격 호스트 패턴 추가.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant LikeBtn as LikeButton
    participant Mutation as Like Mutation
    participant QC as QueryClient
    participant API as API
    rect rgb(230, 245, 255)
    User->>LikeBtn: 좋아요 클릭
    alt isPending
        LikeBtn->>User: 무시(작업 중)
    else
        LikeBtn->>LikeBtn: 낙관적 업데이트(isLiked 토글)
        LikeBtn->>Mutation: toggleLikeMutate 호출
        Mutation->>API: POST/DELETE
    end
    end
    alt 성공
        API-->>Mutation: { placeId }
        Mutation->>QC: invalidateQueries(byLike)
        Mutation->>QC: invalidateQueries(detail(placeId))
        QC-->>LikeBtn: 업데이트된 데이터
    else 실패
        Mutation->>LikeBtn: onError
        LikeBtn->>LikeBtn: isLiked 롤백
    end
Loading
sequenceDiagram
    participant Page as Profile Page
    participant HB as HydrationBoundary
    participant QC as QueryClient
    participant UP as UserProfile
    participant API as API
    Page->>HB: prefetch(useUserQueries.detail)
    HB->>QC: prefetch 쿼리 등록
    QC->>API: getUserData 요청
    API-->>QC: 사용자 데이터
    QC-->>HB: 캐시 저장
    HB->>UP: 렌더 트리로 진입
    UP->>QC: useSuspenseQuery(useUserQueries.detail)
    QC-->>UP: 캐시된 데이터 반환
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 분

  • 주의 필요 영역:
    • useAddLike.ts, useRemoveLike.ts: onSuccess 비동기 처리 및 병렬 invalidate 검증
    • LikeButton.tsx: 낙관적 업데이트/롤백 및 동시성 경계
    • UserProfile.tsx / profile/page.tsx: HydrationBoundary + useSuspenseQuery 사용 흐름 검증
    • 서비스 리팩토링: 응답 처리 단순화가 모든 호출에 일관되게 적용되었는지 확인

Possibly related PRs

Suggested labels

✨ Feature, 🔨 Refactor

Suggested reviewers

  • jcw1031
  • gihhyeon

Poem

🐰 새 프로필이 고개 들어 반기네
좋아요는 깡충, 낙관은 날갯짓 🐇
캐시는 깨끗이, 화면은 반짝 ✨
당근밭에서 코드를 씻어냈다네 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경 사항의 핵심을 정확히 반영합니다. 유저 정보 조회 API 로직 구현과 찜하기 낙관적 업데이트 도입이 명확하게 표현되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#67

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91df22c and 3b6309e.

📒 Files selected for processing (2)
  • apps/web/app/_apis/schemas/user.ts (1 hunks)
  • apps/web/app/_constants/path.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/_apis/schemas/user.ts
🔇 Additional comments (1)
apps/web/app/_constants/path.ts (1)

48-48: LGTM! 경로 형식이 일관성 있게 수정되었습니다.

USER 상수가 다른 경로들과 동일하게 선행 슬래시(/)를 포함하여 올바르게 추가되었습니다. /users/me는 현재 인증된 사용자 정보를 조회하는 표준 REST 패턴을 따르며, AUTH 관련 경로와 가까운 위치에 논리적으로 배치되어 있습니다.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
apps/web/app/profile/page.tsx (1)

33-39: 외부 URL을 상수로 분리하는 것을 권장합니다.

Notion URL이 하드코딩되어 있습니다. 향후 URL 변경이나 환경별 분기를 위해 CLIENT_PATH 또는 별도의 상수 파일로 관리하는 것이 좋습니다.

+// apps/web/app/_constants/path.ts에 추가
+export const EXTERNAL_LINKS = {
+  TERMS_OF_SERVICE: 'https://www.notion.so/woopaca/722d2e1180f94eeead36ec09436d4576?pvs=4',
+}

// page.tsx
-          <ProfileMenuItem
-            href={
-              'https://www.notion.so/woopaca/722d2e1180f94eeead36ec09436d4576?pvs=4'
-            }
-            title={'이용약관'}
-            icon={'paper'}
-          />
+          <ProfileMenuItem
+            href={EXTERNAL_LINKS.TERMS_OF_SERVICE}
+            title={'이용약관'}
+            icon={'paper'}
+          />
apps/web/app/_apis/schemas/user.ts (1)

3-7: HEX 색상 코드 유효성 검증을 추가하는 것을 권장합니다.

profileBackgroundHexCode가 단순 문자열로 정의되어 있어 잘못된 형식의 값이 들어올 수 있습니다. 정규식을 사용한 검증을 추가하면 런타임 에러를 방지할 수 있습니다.

 export const UserSchema = z.object({
   nickname: z.string(),
   profileImageUrl: z.url(),
-  profileBackgroundHexCode: z.string(),
+  profileBackgroundHexCode: z
+    .string()
+    .regex(/^[0-9A-Fa-f]{6}$/, 'Invalid hex color code (expected format: RRGGBB)'),
 })
apps/web/app/profile/_components/UserProfile/UserProfile.tsx (1)

19-25: 이미지 품질 설정을 재검토해주세요.

quality={100}은 최고 품질 설정으로 파일 크기가 커질 수 있습니다. 프로필 이미지는 작은 크기(35x35px)이므로 quality={80} 정도로도 충분한 품질을 얻을 수 있으며 성능이 개선됩니다.

       <Image
         src={profileImageUrl}
         alt={'profileImage'}
         width={35}
         height={35}
-        quality={100}
+        quality={80}
       />
apps/web/app/places/[id]/_components/LikeButton/LikeButton.tsx (1)

48-60: 로딩 상태에 대한 시각적 피드백을 추가하는 것을 권장합니다.

isPending 상태일 때 버튼의 투명도를 조절하거나 커서 스타일을 변경하면 사용자에게 더 명확한 피드백을 제공할 수 있습니다.

   return (
     <motion.button
       onClick={onClick}
       animate={{ scale: isAnimating ? 1.3 : 1 }}
       transition={{ duration: 0.2, ease: 'easeOut' }}
-      className='relative flex items-center justify-center p-2'
+      className='relative flex items-center justify-center p-2'
+      style={{ 
+        opacity: isPending ? 0.6 : 1,
+        cursor: isPending ? 'not-allowed' : 'pointer'
+      }}
     >
apps/web/app/profile/_components/ProfileMenuItem/ProfileMenuItem.tsx (1)

11-19: 내부 링크에는 Next.js Link 컴포넌트 사용을 권장합니다.

현재 <a> 태그를 사용하고 있어 내부 페이지 이동 시 전체 페이지가 새로고침됩니다. Next.js의 Link 컴포넌트를 사용하면 클라이언트 사이드 네비게이션으로 더 빠른 전환이 가능합니다.

외부 링크(Notion)는 <a> 태그가 적절하므로, href가 외부 링크인지 판단하여 조건부로 렌더링하는 것도 고려해보세요.

+import Link from 'next/link'
 import { Icon, IconType } from '@repo/ui/components/Icon'
 import { Flex, JustifyBetween } from '@repo/ui/components/Layout'
 import { Text } from '@repo/ui/components/Text'

 type Props = {
   href: string
   title: string
   icon: IconType
 }

-export const ProfileMenuItem = ({ href, title, icon }: Props) => (
-  <JustifyBetween as={'a'} href={href}>
+export const ProfileMenuItem = ({ href, title, icon }: Props) => {
+  const isExternalLink = href.startsWith('http')
+  const Component = isExternalLink ? 'a' : Link
+  
+  return (
+  <JustifyBetween as={Component} href={href}>
     <Flex className={'gap-2.5'}>
       <Icon type={icon} size={18} />
       <Text variant={'body1'}>{title}</Text>
     </Flex>
     <Icon type={'arrowRight'} size={18} color={'--color-gray-200'} />
   </JustifyBetween>
-)
+  )
+}
apps/web/app/_apis/mutations/useAddLike.ts (1)

10-22: 좋아요 성공 시 리스트/상세 동시 무효화 설계 좋습니다만, placeId 누락 시 최소 로깅을 권장합니다

addLike 응답에서 placeId를 꺼내서 찜 리스트와 상세 정보를 동시에 무효화하는 흐름은 버그 재발을 막는 데 충분해 보입니다. 다만 현재는 placeId가 없으면 조용히 return해서, 서버 응답 이상이나 스키마 변경 시 캐시가 갱신되지 않아도 원인을 찾기 어려울 수 있습니다.

아주 간단히라도 로그를 남기는 쪽을 고려해 보세요:

-      if (!placeId) return
+      if (!placeId) {
+        console.error('addLike 응답에 placeId가 없습니다.', response)
+        return
+      }

이 정도만 있어도 예상치 못한 응답 형태를 빠르게 캐치하는 데 도움이 됩니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8decdd and 91df22c.

📒 Files selected for processing (19)
  • apps/web/app/_apis/mutations/useAddLike.ts (1 hunks)
  • apps/web/app/_apis/mutations/useRemoveLike.ts (1 hunks)
  • apps/web/app/_apis/queries/user.ts (1 hunks)
  • apps/web/app/_apis/schemas/user.ts (1 hunks)
  • apps/web/app/_apis/services/category.ts (1 hunks)
  • apps/web/app/_apis/services/event.ts (1 hunks)
  • apps/web/app/_apis/services/like.ts (1 hunks)
  • apps/web/app/_apis/services/place.ts (3 hunks)
  • apps/web/app/_apis/services/request.ts (1 hunks)
  • apps/web/app/_apis/services/user.ts (1 hunks)
  • apps/web/app/_constants/path.ts (1 hunks)
  • apps/web/app/places/[id]/_components/LikeButton/LikeButton.tsx (2 hunks)
  • apps/web/app/profile/ProfilePage.tsx (0 hunks)
  • apps/web/app/profile/_components/ProfileMenuItem/ProfileMenuItem.tsx (1 hunks)
  • apps/web/app/profile/_components/ProfileMenuItem/index.ts (1 hunks)
  • apps/web/app/profile/_components/UserProfile/UserProfile.tsx (1 hunks)
  • apps/web/app/profile/_components/UserProfile/index.ts (1 hunks)
  • apps/web/app/profile/page.tsx (1 hunks)
  • apps/web/next.config.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/app/profile/ProfilePage.tsx
🧰 Additional context used
🧬 Code graph analysis (12)
apps/web/app/_apis/services/category.ts (4)
apps/web/app/categories/[id]/CategoryDetailPage.tsx (1)
  • data (15-55)
apps/web/app/_components/Categories/Categories.tsx (2)
  • data (8-20)
  • category (15-17)
apps/web/app/categories/[id]/page.tsx (1)
  • generateMetadata (7-27)
apps/web/app/_apis/queries/category.ts (1)
  • queryOptions (12-16)
apps/web/app/_apis/services/event.ts (5)
apps/web/app/_apis/schemas/event.ts (4)
  • PublicEventSchema (3-8)
  • PrivateEvent (31-31)
  • PrivateEventSchema (10-21)
  • EventResult (32-32)
apps/web/app/_apis/mutations/useParticipationEvent.ts (1)
  • body (10-12)
apps/web/app/_apis/queries/event.ts (3)
  • EventQueryKeys (10-10)
  • queryOptions (16-20)
  • queryOptions (21-25)
apps/web/app/events/lucky-draw/_components/Pages/Participation/Participation.tsx (1)
  • useDisclosure (13-52)
apps/web/app/events/lucky-draw/_components/Pages/Participation/ParticipationModal.tsx (1)
  • participationEvent (29-31)
apps/web/app/_apis/services/user.ts (2)
apps/web/app/_apis/schemas/user.ts (2)
  • User (9-9)
  • UserSchema (3-7)
apps/web/app/_constants/path.ts (1)
  • API_PATH (4-49)
apps/web/app/_apis/mutations/useRemoveLike.ts (1)
apps/web/app/_apis/queries/place.ts (1)
  • PlaceQueryKeys (13-25)
apps/web/app/places/[id]/_components/LikeButton/LikeButton.tsx (3)
apps/web/app/_apis/services/like.ts (2)
  • addLike (9-12)
  • removeLike (14-19)
apps/web/app/_apis/mutations/useAddLike.ts (1)
  • useAddLike (5-27)
apps/web/app/_apis/mutations/useRemoveLike.ts (1)
  • useRemoveLike (5-27)
apps/web/app/profile/_components/UserProfile/UserProfile.tsx (1)
apps/web/app/_apis/queries/user.ts (1)
  • useUserQueries (9-15)
apps/web/app/_apis/services/place.ts (2)
apps/web/app/_constants/path.ts (1)
  • API_PATH (4-49)
apps/web/app/_apis/schemas/place.ts (5)
  • BasePlaceSchema (27-33)
  • BasePlace (76-76)
  • PlaceByMapSchema (35-38)
  • PlaceDetail (78-78)
  • PlaceByPreviewSchema (48-55)
apps/web/app/profile/page.tsx (5)
apps/web/app/page.tsx (1)
  • Page (25-59)
apps/web/app/HydrationBoundaryPage.tsx (1)
  • HydrationBoundaryPage (29-44)
apps/web/app/_apis/queries/user.ts (1)
  • useUserQueries (9-15)
apps/web/app/profile/_components/UserProfile/UserProfile.tsx (1)
  • UserProfile (9-32)
apps/web/app/profile/_components/ProfileMenuItem/ProfileMenuItem.tsx (1)
  • ProfileMenuItem (11-19)
apps/web/app/_apis/services/request.ts (2)
apps/web/app/_constants/path.ts (1)
  • API_PATH (4-49)
apps/web/app/_apis/schemas/request.ts (2)
  • RequestSchema (7-13)
  • RequestDetail (23-23)
apps/web/app/_apis/services/like.ts (1)
apps/web/app/_constants/path.ts (1)
  • API_PATH (4-49)
apps/web/app/_apis/mutations/useAddLike.ts (1)
apps/web/app/_apis/queries/place.ts (1)
  • PlaceQueryKeys (13-25)
apps/web/app/_apis/queries/user.ts (1)
apps/web/app/_apis/services/user.ts (1)
  • getUserData (5-8)
🔇 Additional comments (15)
apps/web/app/_apis/services/category.ts (1)

6-6: 코드 간소화가 잘 적용되었습니다.

불필요한 중간 변수를 제거하여 axios 응답에서 직접 data를 구조 분해하는 방식으로 개선되었습니다. 이 패턴이 PR 전체에서 일관되게 적용되어 코드 가독성이 향상되었습니다.

apps/web/app/_apis/services/place.ts (1)

28-31: 일관된 리팩토링이 잘 적용되었습니다.

모든 장소 관련 API 함수에서 응답 구조 분해 패턴이 일관되게 개선되었습니다. getSearchPlaceByKakao 함수는 별도의 axios 인스턴스를 사용하므로 적절하게 제외되었습니다.

Also applies to: 38-41, 50-58, 62-63, 88-91, 95-96

apps/web/app/_apis/services/event.ts (1)

13-14: 일관된 코드 개선이 적용되었습니다.

이벤트 관련 모든 API 함수에서 응답 구조 분해 방식이 통일되어 코드의 일관성이 향상되었습니다.

Also applies to: 18-19, 26-27, 31-32

apps/web/app/profile/_components/UserProfile/index.ts (1)

1-1: 표준 배럴 익스포트 패턴이 적절하게 사용되었습니다.

컴포넌트를 모듈화하고 깔끔한 import 경로를 제공하는 표준적인 구조입니다.

apps/web/app/profile/_components/ProfileMenuItem/index.ts (1)

1-1: 일관된 컴포넌트 구조가 적용되었습니다.

UserProfile과 동일한 배럴 익스포트 패턴을 따라 일관성 있는 코드 구조를 유지하고 있습니다.

apps/web/app/_apis/services/user.ts (1)

5-8: 새로운 유저 서비스가 기존 패턴을 잘 따르고 있습니다.

다른 서비스 파일들과 일관된 구조로 구현되었으며, Zod 스키마를 통한 응답 검증도 적절하게 적용되었습니다. PR 전체의 리팩토링 방향과도 일치합니다.

단, API_PATH.USERapps/web/app/_constants/path.ts에서 선행 슬래시 없이 정의되어 있으므로, 해당 파일의 리뷰 코멘트를 참고하여 경로 형식을 통일하는 것을 권장합니다.

apps/web/app/_apis/mutations/useRemoveLike.ts (1)

10-22: 병렬 쿼리 무효화 로직이 잘 구현되었습니다.

찜 제거 후 '찜 리스트'와 '맛집 상세 정보' 쿼리를 동시에 무효화하여 캐시 일관성을 확보했습니다. useAddLike와 동일한 패턴을 따라 코드의 일관성도 좋습니다.

apps/web/app/profile/page.tsx (1)

14-21: 서버 사이드 프리페칭 패턴이 올바르게 적용되었습니다.

HydrationBoundaryPage를 사용하여 유저 데이터를 미리 가져오고 UserProfile 컴포넌트에 주입하는 구조가 좋습니다. 다른 페이지(예: page.tsx)와 일관된 패턴을 따르고 있습니다.

apps/web/app/_apis/services/like.ts (1)

9-12: 코드 간소화가 잘 적용되었습니다.

불필요한 중간 변수를 제거하고 직접 구조 분해하여 가독성이 향상되었습니다.

apps/web/app/_apis/services/request.ts (1)

10-18: 응답 처리 로직 간소화가 잘 적용되었습니다.

like.ts와 동일한 패턴으로 중간 변수를 제거하여 코드베이스 전체의 일관성이 향상되었습니다.

apps/web/app/profile/_components/UserProfile/UserProfile.tsx (1)

9-32: 컴포넌트 구조와 데이터 페칭이 올바르게 구현되었습니다.

useSuspenseQuery를 사용하여 유저 데이터를 가져오고 렌더링하는 패턴이 좋습니다. React 19의 Suspense 패턴과 잘 통합되어 있습니다.

apps/web/app/places/[id]/_components/LikeButton/LikeButton.tsx (3)

17-20: 중복 클릭 방지 로직이 잘 구현되었습니다.

양쪽 mutation의 pending 상태를 모두 확인하여 요청 중 중복 클릭을 방지하는 것이 좋습니다.


22-42: 낙관적 업데이트와 에러 롤백이 올바르게 구현되었습니다.

UI를 먼저 업데이트하고 API 실패 시 이전 상태로 되돌리는 패턴이 잘 적용되었습니다. 사용자 경험이 크게 개선될 것입니다.


44-46: useEffect의 필요성을 재검토해주세요.

props가 변경될 때 상태를 동기화하는 로직이지만, 낙관적 업데이트 중에 initIsLiked가 변경되면 의도하지 않은 상태 재설정이 발생할 수 있습니다.

만약 사용자가 좋아요를 클릭한 직후(낙관적 업데이트 적용 후) 백그라운드 쿼리 재페칭으로 initIsLiked가 업데이트되면, useEffect가 낙관적 상태를 덮어쓸 위험이 있습니다.

다음 시나리오를 테스트해보시기 바랍니다:

  1. 좋아요 버튼 클릭 (낙관적 업데이트)
  2. API 요청 진행 중
  3. 다른 쿼리 무효화로 인해 상세 정보가 재페칭되어 initIsLiked가 업데이트
  4. useEffect가 트리거되어 낙관적 상태가 덮어써지는지 확인

대안으로는 mutation의 onSuccess에서만 상태를 업데이트하거나, useEffect에 isPending 체크를 추가하는 방법을 고려해보세요:

useEffect(() => {
  if (!isPending) {
    setIsLiked(initIsLiked)
  }
}, [initIsLiked, isPending])
apps/web/app/_apis/queries/user.ts (1)

1-15: 유저 쿼리 키/헬퍼 분리 패턴이 깔끔하고 재사용성 좋아 보입니다

UserQueryKeys로 키를 모듈화하고, useUserQueries.detail()queryKey/queryFn을 한 곳에서 관리하는 구조가 다른 쿼리 모듈(PlaceQueryKeys 등)과도 일관되고, 서버/클라이언트 어디서든 재사용하기 좋은 형태입니다. 현재 요구사항 범위에서는 추가 손볼 부분 없이 그대로 사용해도 무방해 보입니다.

Comment on lines 4 to 7
type Response = {
placeId: string
placeId: number
message: string
}
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

Response 타입 변경을 백엔드 API와 검증해주세요.

placeId의 타입이 string에서 number로 변경되었습니다. 백엔드 API 응답 스펙과 일치하는지 확인이 필요합니다. 만약 백엔드가 문자열을 반환한다면 런타임 에러가 발생할 수 있습니다.

백엔드 API 응답 형식을 확인하기 위한 스크립트:


🏁 Script executed:

#!/bin/bash
# 백엔드 API 스펙 문서나 타입 정의를 확인합니다.
# OpenAPI/Swagger 문서가 있다면 해당 엔드포인트의 응답 스키마를 확인하세요.

# 코드베이스에서 placeId 타입 사용을 확인
rg -nP --type=ts 'placeId.*:.*string' -A2 -B2

Length of output: 1658


추가 검증을 위해 코드베이스 내 Response 타입 사용 패턴과 API 응답 스펙을 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# 코드베이스에서 Response 타입 사용처를 확인
rg -nP --type=ts 'Response' apps/web/app/_apis/services/like.ts -A5 -B5

Length of output: 550


🏁 Script executed:

#!/bin/bash
# Response 타입이 어디서 사용되는지 확인
rg -nP --type=ts 'addLike|removeLike' apps/web -A3 -B3

Length of output: 4277


🏁 Script executed:

#!/bin/bash
# 백엔드 API 명세나 타입 정의 파일 찾기
fd -e ts -e d.ts -e json | grep -i 'api\|spec\|schema\|types' | head -20

Length of output: 819


🏁 Script executed:

#!/bin/bash
# like.ts 파일의 전체 내용 확인
cat apps/web/app/_apis/services/like.ts

Length of output: 502



---

🏁 Script executed:

```shell
#!/bin/bash
# place 스키마 확인
cat apps/web/app/_apis/schemas/place.ts

Length of output: 1935


🏁 Script executed:

#!/bin/bash
# like 관련 스키마가 있는지 확인
rg -nP --type=ts 'placeId.*number|placeId.*string' apps/web/app/_apis/schemas/

Length of output: 160


🏁 Script executed:

#!/bin/bash
# 백엔드 API 경로 확인 - placeId가 어떻게 사용되는지
rg -nP 'PLACES\.LIKE' apps/web --type=ts -B2 -A2

Length of output: 2178


🏁 Script executed:

#!/bin/bash
# useAddLike 및 useRemoveLike에서 Response placeId 사용 전체 확인
cat apps/web/app/_apis/mutations/useAddLike.ts && echo "---" && cat apps/web/app/_apis/mutations/useRemoveLike.ts

Length of output: 1532



---

🏁 Script executed:

```shell
#!/bin/bash
# Mock 응답 데이터 확인
cat apps/web/app/_mocks/handlers/likeHandlers.ts

Length of output: 464


🏁 Script executed:

#!/bin/bash
# PlaceQueryKeys.detail 함수 확인 - String() 변환이 필요한 이유 확인
rg -nP 'detail.*:' apps/web/app/_apis/queries/place.ts -B5 -A5

Length of output: 842


Response 타입의 placeIdnumber에서 string으로 수정해주세요.

Mock 데이터(apps/web/app/_mocks/handlers/likeHandlers.ts)에서 백엔드 응답은 placeId: '1'(문자열)로 정의되어 있으나, Response 타입(apps/web/app/_apis/services/like.ts)은 placeId: number로 선언되어 있습니다.

현재 코드에서 useAddLike.tsuseRemoveLike.tsString(placeId) 변환을 하는 것도 이 불일치 때문입니다. TypeScript는 런타임 타입 체크를 하지 않으므로, 실제 백엔드도 문자열을 반환한다면 예상치 못한 타입 에러가 발생할 수 있습니다.

수정 위치: apps/web/app/_apis/services/like.ts 4-7줄

type Response = {
  placeId: string
  message: string
}
🤖 Prompt for AI Agents
In apps/web/app/_apis/services/like.ts around lines 4 to 7, the Response.type
declares placeId as number but the mock/backend returns a string; change placeId
to string in the Response type (placeId: string) and then update callers (e.g.,
remove or adjust String(placeId) conversions in useAddLike/useRemoveLike and any
other usages) so TypeScript types align with the backend/mock response.

Comment on lines +38 to +42
{
protocol: 'https',
hostname: process.env.NEXT_PUBLIC_API_URL_HOST || '', //테스트용 주소
pathname: '/**',
},
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

환경 변수 미설정 시 빈 문자열 폴백으로 인한 이미지 로딩 실패 가능성

NEXT_PUBLIC_API_URL_HOST 환경 변수가 설정되지 않으면 hostname이 빈 문자열이 되어 Next.js 이미지 최적화가 실패할 수 있습니다.

다음 중 하나의 방식으로 수정하는 것을 권장합니다:

옵션 1: 유효한 기본값 제공

      {
        protocol: 'https',
-       hostname: process.env.NEXT_PUBLIC_API_URL_HOST || '', //테스트용 주소
+       hostname: process.env.NEXT_PUBLIC_API_URL_HOST || 'localhost', //테스트용 주소
        pathname: '/**',
      },

옵션 2: 환경 변수가 설정된 경우에만 패턴 추가

    remotePatterns: [
      // ... existing patterns
      ...(process.env.NEXT_PUBLIC_API_URL_HOST
        ? [
            {
              protocol: 'https' as const,
              hostname: process.env.NEXT_PUBLIC_API_URL_HOST,
              pathname: '/**',
            },
          ]
        : []),
    ],
🤖 Prompt for AI Agents
In apps/web/next.config.ts around lines 38 to 42, hostname is set to
process.env.NEXT_PUBLIC_API_URL_HOST || '' which yields an empty string when the
env var is missing and can break Next.js image optimization; fix by either
providing a sensible default host (e.g. 'localhost' or your CDN host) instead of
'', or conditionally add the remotePattern only when
process.env.NEXT_PUBLIC_API_URL_HOST is truthy (i.e., wrap the pattern in a
conditional spread so no pattern with an empty hostname is included), ensuring
the resulting remotePatterns array contains only valid hostnames.

@leeleeleeleejun leeleeleeleejun merged commit 08b41cb into develop Nov 20, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 유저 정보 조회 api 관련 로직 구현

2 participants