-
Notifications
You must be signed in to change notification settings - Fork 2
refactor: 이력서 API Tanstack-Query 마이그레이션 #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
invalidhuman
wants to merge
12
commits into
staging
Choose a base branch
from
FRONTEND-348
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a2d0b21
feat:resume 도메인 기본 구조 구성
invalidhuman b604aeb
feat:resume목록 조회 마이그레이션
invalidhuman 78f3c20
refactor:useQuery (GET요청관련) 마이그레이션
invalidhuman 221813b
refactor:useMutation (POST) 마이그레이션
invalidhuman b6c349a
Merge branch 'staging' into FRONTEND-348
invalidhuman 1fa2a41
fix:프로퍼티 오류 수정
invalidhuman 8e1de52
refactor:좋아요, 북마크 캐시 무효화 로직 개선
invalidhuman 093478b
refactor: 쿼리 키 고유성 보장을 위해 일부 함수에 누락되어있던 파라미터 추가
invalidhuman 790c1c8
refactor: 일부 파일에서 타입 정규화, 타입 명시 작업 수행
invalidhuman 35e2536
fix:일부 오류 해결
invalidhuman 3ac9020
refactor:좋아요 로직을 다시 useLike 훅으로 rew
invalidhuman 58e5895
refactor:중복 코드 통합
invalidhuman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }) | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| // 북마크 mutation | ||
| export const useResumeBookmarkMutation = () => { | ||
| const queryClient = useQueryClient() | ||
|
|
||
| return useMutation({ | ||
| mutationFn: (data: LikeBookmarkRequest) => postBookmark(data), | ||
| onSuccess: () => { | ||
| // 북마크 성공 시 관련 쿼리 무효화 | ||
| queryClient.invalidateQueries({ queryKey: resumeKeys.all }) | ||
| }, | ||
| }) | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 이력서 업로드 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 }) | ||
| }, | ||
| }) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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를 추가해 타입 안전성과 디버깅 가독성을 높여주세요.다음과 같이 변경 제안합니다:
📝 Committable suggestion
🤖 Prompt for AI Agents