Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
228 changes: 228 additions & 0 deletions src/api/resume/apis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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) {

Check warning on line 67 in src/api/resume/apis.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add logic to this catch clause or eliminate it and rethrow the exception automatically.

See more on https://sonarcloud.io/project/issues?id=Techeer-Hogwarts_frontend&issues=AZrQC3D373gg9spoFPqT&open=AZrQC3D373gg9spoFPqT&pullRequest=148
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}` : ''}`

Check warning on line 153 in src/api/resume/apis.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not use nested template literals.

See more on https://sonarcloud.io/project/issues?id=Techeer-Hogwarts_frontend&issues=AZrQC3D373gg9spoFPqU&open=AZrQC3D373gg9spoFPqU&pullRequest=148

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 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,
}
37 changes: 37 additions & 0 deletions src/api/resume/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { resumeKeys } from './keys'
import { postBookmark, uploadResume } from './apis'
import { LikeBookmarkRequest, ResumeUploadRequest } from './types'

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

return useMutation<void, Error, LikeBookmarkRequest>({
mutationKey: [...resumeKeys.all, 'bookmark'],
mutationFn: postBookmark,
onSuccess: async (_data, variables) => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: resumeKeys.detail(variables.contentId),
}),
queryClient.invalidateQueries({ queryKey: resumeKeys.lists() }),
queryClient.invalidateQueries({ queryKey: resumeKeys.bestList() }),
])
},
})
}

// 이력서 업로드 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 })
},
})
}
88 changes: 88 additions & 0 deletions src/api/resume/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import {
getResumeList,
fetchResumeById,
fetchBestResumes,
fetchUserResumes,
} from './apis'
import { ResumeQueryParams } from './types'
import { resumeKeys } from './keys'

// 이력서 목록 조회 (무한 스크롤)
export const useResumeListQuery = ({
position = [],
year = [],
category = '전체',
sortBy = 'CREATEDAT',
initialLimit = 10,
pageLimit = 12,
}: ResumeQueryParams & {
initialLimit?: number
pageLimit?: number
}) => {
return useInfiniteQuery({
queryKey: [
...resumeKeys.list({ position, year, category, sortBy }),
{ initialLimit, pageLimit },
],
queryFn: ({ pageParam }) => {
// 첫 페이지는 initialLimit, 이후 페이지는 pageLimit
const limit = !pageParam ? initialLimit : pageLimit

Check warning on line 30 in src/api/resume/queries.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=Techeer-Hogwarts_frontend&issues=AZrQC3AM73gg9spoFPqS&open=AZrQC3AM73gg9spoFPqS&pullRequest=148
return getResumeList({
position,
year,
category,
cursorId: pageParam,
limit,
sortBy,
})
},
initialPageParam: undefined as number | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNext ? lastPage.nextCursor : undefined,
staleTime: 5 * 60 * 1000, // 5분
gcTime: 10 * 60 * 1000, // 10분
})
}

// 이력서 상세 조회
export const useResumeDetailQuery = (resumeId: number) => {
return useQuery({
queryKey: resumeKeys.detail(resumeId),
queryFn: () => fetchResumeById(resumeId),
staleTime: 10 * 60 * 1000, // 10분
gcTime: 30 * 60 * 1000, // 30분
enabled: !!resumeId, // resumeId가 있을 때만 실행
})
}

// 인기 이력서 목록 조회 (무한 스크롤)
export const useBestResumeListQuery = (
setAuthModalOpen: (open: boolean) => void,
limit: number = 12,
) => {
return useInfiniteQuery({
queryKey: [...resumeKeys.bestList(), limit],
queryFn: ({ pageParam }) =>
fetchBestResumes(pageParam, limit, setAuthModalOpen),
initialPageParam: undefined as number | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNext ? lastPage.nextCursor : undefined,
staleTime: 5 * 60 * 1000, // 5분
gcTime: 10 * 60 * 1000, // 10분
})
}

// 사용자 이력서 목록 조회 (무한 스크롤)
export const useUserResumeListQuery = (userId: number, limit: number = 10) => {
return useInfiniteQuery({
queryKey: [...resumeKeys.userList(userId), limit],
queryFn: ({ pageParam }) => fetchUserResumes(userId, pageParam, limit),
initialPageParam: undefined as number | undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNext ? lastPage.nextCursor : undefined,
staleTime: 5 * 60 * 1000, // 5분
gcTime: 10 * 60 * 1000, // 10분
enabled: !!userId, // userId가 있을 때만 실행
})
}
Loading