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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions apps/web/app/_apis/mutations/useAddLike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@ export const useAddLike = () => {

return useMutation({
mutationFn: async (id: string) => await addLike(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [...PlaceQueryKeys.byLike()] })
onSuccess: async (response) => {
const { placeId } = response

if (!placeId) return

await Promise.all([
queryClient.invalidateQueries({
queryKey: [...PlaceQueryKeys.byLike()],
}),
queryClient.invalidateQueries({
queryKey: [...PlaceQueryKeys.detail(String(placeId))],
}),
])
},
// 공통 에러 처리 필요
onError: (error) => console.error(error),
Expand Down
15 changes: 13 additions & 2 deletions apps/web/app/_apis/mutations/useRemoveLike.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@ export const useRemoveLike = () => {

return useMutation({
mutationFn: async (id: string) => await removeLike(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [...PlaceQueryKeys.byLike()] })
onSuccess: async (response) => {
const { placeId } = response

if (!placeId) return

await Promise.all([
queryClient.invalidateQueries({
queryKey: [...PlaceQueryKeys.byLike()],
}),
queryClient.invalidateQueries({
queryKey: [...PlaceQueryKeys.detail(String(placeId))],
}),
])
},
// 공통 에러 처리 필요
onError: (error) => console.error(error),
Expand Down
15 changes: 15 additions & 0 deletions apps/web/app/_apis/queries/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { queryOptions } from '@tanstack/react-query'
import { getUserData } from '@/_apis/services/user'

export const UserQueryKeys = {
all: () => ['user'] as const,
detail: () => [...UserQueryKeys.all(), 'detail'] as const,
}

export const useUserQueries = {
detail: () =>
queryOptions({
queryKey: UserQueryKeys.detail(),
queryFn: getUserData,
}),
}
9 changes: 9 additions & 0 deletions apps/web/app/_apis/schemas/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod'

export const UserSchema = z.object({
nickname: z.string(),
profileImageUrl: z.url(),
profileBackgroundHexCode: z.string(),
})

export type User = z.infer<typeof UserSchema>
3 changes: 1 addition & 2 deletions apps/web/app/_apis/services/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { API_PATH } from '@/_constants/path'
import { CategorySchema, Category } from '../schemas/category'

export const getCategories = async (): Promise<Category[]> => {
const { data: response } = await axiosInstance.get(API_PATH.CATEGORY)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.CATEGORY)
return CategorySchema.array().parse(data)
}
15 changes: 4 additions & 11 deletions apps/web/app/_apis/services/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,24 @@ import {
} from '@/_apis/schemas/event'

export const getPublicEventInfo = async (): Promise<PublicEvent> => {
const { data: response } = await axiosInstance.get(API_PATH.EVENT.INFO)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.EVENT.INFO)
return PublicEventSchema.parse(data)
}

export const getPrivateEventInfo = async (): Promise<PrivateEvent> => {
const { data: response } = await axiosInstance.get(API_PATH.EVENT.INFO)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.EVENT.INFO)
return PrivateEventSchema.parse(data)
}

export const participationEvent = async (body: {
eventId: string
ticketsCount: number
}) => {
const { data: response } = await axiosInstance.post(
API_PATH.EVENT.PARTICIPATIONS,
body,
)
const { data } = response
const { data } = await axiosInstance.post(API_PATH.EVENT.PARTICIPATIONS, body)
return data
}

export const getEventResult = async (): Promise<EventResult | null> => {
const { data: response } = await axiosInstance.get(API_PATH.EVENT.RESULT)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.EVENT.RESULT)
return EventResultSchema.parse(data)
}
10 changes: 3 additions & 7 deletions apps/web/app/_apis/services/like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,18 @@ import axiosInstance from '@/_lib/axiosInstance'
import { API_PATH } from '@/_constants/path'

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


export const addLike = async (placeId: string): Promise<Response> => {
const { data: response } = await axiosInstance.post(
API_PATH.PLACES.LIKE.POST(placeId),
)
const { data } = response
const { data } = await axiosInstance.post(API_PATH.PLACES.LIKE.POST(placeId))
return data
}

export const removeLike = async (placeId: string): Promise<Response> => {
const { data: response } = await axiosInstance.delete(
const { data } = await axiosInstance.delete(
API_PATH.PLACES.LIKE.DELETE(placeId),
)
const { data } = response
return data
}
18 changes: 6 additions & 12 deletions apps/web/app/_apis/services/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,19 @@ export const getPlacesByRanking = async (
sort: RankingPlaceSort,
campus: CampusType,
): Promise<BasePlace[]> => {
const { data: response } = await axiosInstance.get(
const { data } = await axiosInstance.get(
API_PATH.PLACES.BY_RANKING(sort, campus),
)
const { data } = response
return BasePlaceSchema.array().parse(data)
}

export const getPlacesByCategory = async (
id: string,
campus: CampusType,
): Promise<BasePlace[]> => {
const { data: response } = await axiosInstance.get(
const { data } = await axiosInstance.get(
API_PATH.PLACES.BY_CATEGORY(id, campus),
)
const { data } = response
return BasePlaceSchema.array().parse(data)
}

Expand All @@ -49,21 +47,19 @@ export const getPlacesByMap = async ({
maxLatitude,
maxLongitude,
}: MapBounds): Promise<PlaceByMap[]> => {
const { data: response } = await axiosInstance.get(
const { data } = await axiosInstance.get(
API_PATH.PLACES.BY_MAP({
minLatitude,
minLongitude,
maxLatitude,
maxLongitude,
}),
)
const { data } = response
return PlaceByMapSchema.array().parse(data)
}

export const getPlaceDetail = async (id: string): Promise<PlaceDetail> => {
const { data: response } = await axiosInstance.get(API_PATH.PLACES.DETAIL(id))
const { data } = response
const { data } = await axiosInstance.get(API_PATH.PLACES.DETAIL(id))
return PlaceDetailSchema.parse(data)
}

Expand All @@ -89,16 +85,14 @@ export const getSearchPlaceByKakao = async ({
export const getPlaceByPreview = async (
kakaoPlaceId: string,
): Promise<PlaceByPreview> => {
const { data: response } = await axiosInstance.get(
const { data } = await axiosInstance.get(
API_PATH.PLACES.NEW.PREVIEW(kakaoPlaceId),
)
const { data } = response
return PlaceByPreviewSchema.parse(data)
}

export const getPlacesByLike = async (): Promise<BasePlace[]> => {
const { data: response } = await axiosInstance.get(API_PATH.PLACES.LIKE.GET)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.PLACES.LIKE.GET)
return BasePlaceSchema.array().parse(data)
}

Expand Down
8 changes: 2 additions & 6 deletions apps/web/app/_apis/services/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,11 @@ import {
} from '@/_apis/schemas/request'

export const getRequests = async (): Promise<Request[]> => {
const { data: response } = await axiosInstance.get(API_PATH.REQUEST.LIST)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.REQUEST.LIST)
return RequestSchema.array().parse(data)
}

export const getRequestDetail = async (id: string): Promise<RequestDetail> => {
const { data: response } = await axiosInstance.get(
API_PATH.REQUEST.DETAIL(id),
)
const { data } = response
const { data } = await axiosInstance.get(API_PATH.REQUEST.DETAIL(id))
return RequestDetailSchema.parse(data)
}
8 changes: 8 additions & 0 deletions apps/web/app/_apis/services/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axiosInstance from '@/_lib/axiosInstance'
import { API_PATH } from '@/_constants/path'
import { User, UserSchema } from '@/_apis/schemas/user'

export const getUserData = async (): Promise<User> => {
const { data } = await axiosInstance.get(API_PATH.USER)
return UserSchema.parse(data)
}
1 change: 1 addition & 0 deletions apps/web/app/_constants/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const API_PATH = {
`/auth/oauth2?code=${code}&redirectUri=${redirectUri}`,
TOKEN: '/auth/token',
},
USER: '/users/me',
}

export const CLIENT_PATH = {
Expand Down
35 changes: 26 additions & 9 deletions apps/web/app/places/[id]/_components/LikeButton/LikeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState } from 'react'
import { useEffect, useState } from 'react'
import { motion } from 'motion/react'
import { Icon } from '@repo/ui/components/Icon'
import { useAddLike } from '@/_apis/mutations/useAddLike'
Expand All @@ -14,20 +14,37 @@ type Props = {
export const LikeButton = ({ placeId, initIsLiked }: Props) => {
const [isLiked, setIsLiked] = useState(initIsLiked)
const [isAnimating, setIsAnimating] = useState(false)
const { mutate: addLike } = useAddLike()
const { mutate: removeLike } = useRemoveLike()
const toggleLikeMutate = isLiked ? removeLike : addLike
const { mutate: addLike, isPending: isAdding } = useAddLike()
const { mutate: removeLike, isPending: isRemoving } = useRemoveLike()

const isPending = isAdding || isRemoving

const onClick = () => {
if (isPending) return

// 현재 상태 저장 (에러 시 롤백용)
const prevIsLiked = isLiked
// 낙관적 업데이트 (UI 먼저 변경)
const nextIsLiked = !prevIsLiked
setIsLiked(nextIsLiked)

const toggleLikeMutate = nextIsLiked ? addLike : removeLike

// 애니메이션 트리거
if (nextIsLiked) {
setIsAnimating(true)
setTimeout(() => setIsAnimating(false), 200)
}

toggleLikeMutate(placeId, {
onSuccess: () => {
setIsLiked((prev) => !prev)
setIsAnimating(true)
setTimeout(() => setIsAnimating(false), 200) // 0.2초 동안 팝 애니메이션
},
onError: () => setIsLiked(prevIsLiked), // 실패 시 롤백
})
}

useEffect(() => {
setIsLiked(initIsLiked)
}, [initIsLiked])

return (
<motion.button
onClick={onClick}
Expand Down
62 changes: 0 additions & 62 deletions apps/web/app/profile/ProfilePage.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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}>
<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>
)
1 change: 1 addition & 0 deletions apps/web/app/profile/_components/ProfileMenuItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ProfileMenuItem } from './ProfileMenuItem'
Loading