Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
248 changes: 248 additions & 0 deletions src/api/resume/apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import {
ResumeQueryParams,
ResumeDetail,
BestResumeResponse,
UserResumeResponse,
LikeBookmarkRequest,
ResumeUploadRequest,
} from './types'

const RESUME_API_BASE = '/api/resumes'

// 이력서 목록 조회
export async function getResumeList({
position = [],
year = [],
category = '전체',
cursorId,
limit = 10,
sortBy = 'CREATEDAT',
}: ResumeQueryParams) {
try {
// URLSearchParams를 사용하여 동적으로 쿼리 문자열 생성
const params = new URLSearchParams()

if (position.length > 0)
position.forEach((p) => params.append('position', p))
if (year.length > 0) year.forEach((y) => params.append('year', y))
// 카테고리 변환 (한글 -> 영어)
const categoryMap: { [key: string]: string } = {
전체: '',
이력서: 'RESUME',
포트폴리오: 'PORTFOLIO',
ICT: 'ICT',
OTHER: 'OTHER',
}

// category가 undefined이거나 null일 때 기본값 처리
const safeCategory = category || '전체'
const mappedCategory = categoryMap[safeCategory] || ''

// 카테고리가 빈 문자열이 아닐 때만 파라미터에 추가
if (mappedCategory) {
params.append('category', mappedCategory)
}

if (cursorId != undefined) {
params.append('cursorId', cursorId.toString())
}
params.append('limit', limit.toString())
params.append('sortBy', sortBy)

const response = await fetch(`${RESUME_API_BASE}?${params.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})

if (!response.ok) {
throw new Error(
`Error: 이력서 목록 조회 실패: ${response.status} ${response.statusText}`,
)
}

const result = await response.json()
return result
} catch (error) {
throw error // 에러를 호출한 함수에 다시 전달
}
}

// 이력서 상세 조회
export const fetchResumeById = async (
resumeId: number,
): Promise<ResumeDetail> => {
try {
const response = await fetch(`${RESUME_API_BASE}/${resumeId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})

if (!response.ok) {
throw new Error(
`Failed to fetch resume with id ${resumeId}. Status: ${response.status}`,
)
}

const result = await response.json()
return result
} catch (error: any) {
throw error
}
}

// 인기 이력서 조회
export const fetchBestResumes = async (
cursorId?: number,
limit: number = 12,
setAuthModalOpen?: (open: boolean) => void,
): Promise<BestResumeResponse> => {
try {
const params = new URLSearchParams()
params.append('limit', String(limit))

if (typeof cursorId === 'number') {
params.append('cursorId', String(cursorId))
}

const response = await fetch(
`${RESUME_API_BASE}/best?${params.toString()}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
)

// 401 Unauthorized 응답 처리
if (response.status === 401) {
setAuthModalOpen?.(true)
throw new Error('로그인이 필요합니다.')
}

if (!response.ok) {
throw new Error(`인기 이력서 조회 실패: ${response.status}`)
}

const result = await response.json()
return result
} catch (error: any) {
throw error
}
}

// 사용자 이력서 목록 조회
export const fetchUserResumes = async (
userId: number,
cursor?: number,
limit: number = 10,
): Promise<UserResumeResponse> => {
try {
// 쿼리 파라미터 구성
const params = new URLSearchParams()
if (cursor !== undefined) {
params.append('cursor', cursor.toString())
}
params.append('limit', limit.toString())

const queryString = params.toString()
const url = `${RESUME_API_BASE}/user/${userId}${queryString ? `?${queryString}` : ''}`

const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
})

if (!response.ok) {
throw new Error(
`Failed to fetch resumes for user. Status: ${response.status}`,
)
}

const result = await response.json()
return result // 새로운 API 응답 구조 그대로 반환
} catch (error: any) {
throw error
}
}

// 좋아요 처리
export const postLike = async (data: LikeBookmarkRequest): Promise<void> => {
const response = await fetch('/api/likes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contentId: data.contentId,
category: data.category,
likeStatus: data.likeStatus,
}),
credentials: 'include',
})

if (!response.ok) {
throw new Error('좋아요 처리 중 오류가 발생했습니다.')
}
}

// 북마크 처리
export const postBookmark = async (
data: LikeBookmarkRequest,
): Promise<void> => {
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contentId: data.contentId,
category: data.category,
bookmarkStatus: data.bookmarkStatus,
}),
credentials: 'include',
})

if (!response.ok) {
throw new Error('북마크 처리 중 오류가 발생했습니다.')
}
}

// 이력서 업로드
export const uploadResume = async (
file: File,
data: ResumeUploadRequest,
): Promise<any> => {
const formData = new FormData()
formData.append('file', file)
formData.append('request', JSON.stringify(data))

const response = await fetch(RESUME_API_BASE, {
method: 'POST',
body: formData,
credentials: 'include',
})

if (!response.ok) {
const errorData = await response.json().catch(() => null)

if (response.status === 400) {
throw new Error('존재하지 않는 카테고리입니다.')
}

if (response.status === 401) {
throw new Error(errorData?.message || '로그인이 필요합니다.')
}

throw new Error(`이력서 업로드 실패: ${response.status}`)
}

return response.json()
}
5 changes: 5 additions & 0 deletions src/api/resume/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './types'
export * from './apis'
export * from './queries'
export * from './keys'
export * from './mutations'
23 changes: 23 additions & 0 deletions src/api/resume/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// resume 관련 queryKey 생성 함수
import type { ResumeQueryParams } from './types'

export const resumeKeys = {
// 모든 이력서 관련 키의 기본값
all: ['resumes'] as const,

// 이력서 목록 키
lists: () => [...resumeKeys.all, 'list'] as const,
list: (params: ResumeQueryParams) => [...resumeKeys.lists(), params] as const,
// 페이지별 쿼리 키
page: (params: ResumeQueryParams, pageNumber: number) =>
[...resumeKeys.list(params), 'page', pageNumber] as const,

// 이력서 상세 키
detail: (id: number) => [...resumeKeys.all, 'detail', id] as const,

// 인기 이력서 목록 키
bestList: () => [...resumeKeys.all, 'best'] as const,

// 사용자 이력서 목록 키
userList: (userId: number) => [...resumeKeys.all, 'user', userId] as const,
}
44 changes: 44 additions & 0 deletions src/api/resume/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { resumeKeys } from './keys'
import { postLike, postBookmark, uploadResume } from './apis'
import { LikeBookmarkRequest, ResumeUploadRequest } from './types'

// 좋아요 mutation
export const useResumeLikeMutation = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: (data: LikeBookmarkRequest) => postLike(data),
onSuccess: () => {
// 좋아요 성공 시 관련 쿼리 무효화
queryClient.invalidateQueries({ queryKey: resumeKeys.all })
},
})
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

무차별 캐시 무효화 대신 “타깃팅” + 타입 제네릭 추가를 권장

좋아요 후 resumeKeys.all 전체 무효화는 과도합니다. 상세/리스트/베스트만 갱신하도록 좁히고, useMutation 제네릭과 mutationKey를 추가해 타입 안전성과 디버깅 가독성을 높여주세요.

다음과 같이 변경 제안합니다:

-  return useMutation({
-    mutationFn: (data: LikeBookmarkRequest) => postLike(data),
-    onSuccess: () => {
-      // 좋아요 성공 시 관련 쿼리 무효화
-      queryClient.invalidateQueries({ queryKey: resumeKeys.all })
-    },
-  })
+  return useMutation<void, Error, LikeBookmarkRequest>({
+    mutationKey: [...resumeKeys.all, 'like'],
+    mutationFn: postLike,
+    onSuccess: async (_data, variables) => {
+      await Promise.all([
+        queryClient.invalidateQueries({ queryKey: resumeKeys.detail(variables.contentId) }),
+        queryClient.invalidateQueries({ queryKey: resumeKeys.lists() }),
+        queryClient.invalidateQueries({ queryKey: resumeKeys.bestList() }),
+      ])
+    },
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return useMutation({
mutationFn: (data: LikeBookmarkRequest) => postLike(data),
onSuccess: () => {
// 좋아요 성공 시 관련 쿼리 무효화
queryClient.invalidateQueries({ queryKey: resumeKeys.all })
},
})
return useMutation<void, Error, LikeBookmarkRequest>({
mutationKey: [...resumeKeys.all, 'like'],
mutationFn: postLike,
onSuccess: async (_data, variables) => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: resumeKeys.detail(variables.contentId) }),
queryClient.invalidateQueries({ queryKey: resumeKeys.lists() }),
queryClient.invalidateQueries({ queryKey: resumeKeys.bestList() }),
])
},
})
🤖 Prompt for AI Agents
In src/api/resume/mutations.ts around lines 10 to 16, the mutation currently
invalidates resumeKeys.all which is too broad; update useMutation to include
proper generics (e.g. useMutation<ResponseType, Error, LikeBookmarkRequest>) and
add a descriptive mutationKey (e.g. ['resume','like']) and replace the single
invalidateQueries({ queryKey: resumeKeys.all }) with targeted invalidations such
as queryClient.invalidateQueries({ queryKey: resumeKeys.detail(id) }),
queryClient.invalidateQueries({ queryKey: resumeKeys.list() }), and
queryClient.invalidateQueries({ queryKey: resumeKeys.best() }) (use the
appropriate id or params from the mutation input) so only detail/list/best
caches refresh.

}

// 북마크 mutation
export const useResumeBookmarkMutation = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: (data: LikeBookmarkRequest) => postBookmark(data),
onSuccess: () => {
// 북마크 성공 시 관련 쿼리 무효화
queryClient.invalidateQueries({ queryKey: resumeKeys.all })
},
})
}

// 이력서 업로드 mutation
export const useResumeUploadMutation = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: ({ file, data }: { file: File; data: ResumeUploadRequest }) =>
uploadResume(file, data),
onSuccess: () => {
// 이력서 업로드 성공 시 관련 쿼리 무효화
queryClient.invalidateQueries({ queryKey: resumeKeys.all })
},
})
}
Loading