diff --git a/docs/api-migration-prd.md b/docs/api-migration-prd.md new file mode 100644 index 00000000..e6c9d375 --- /dev/null +++ b/docs/api-migration-prd.md @@ -0,0 +1,328 @@ +# API 마이그레이션 PRD + +> `src/api` → `src/apis` 완전 마이그레이션 ✅ **완료** + +--- + +## 1. 개요 + +### 1.1 목표 + +`src/api` 폴더를 완전히 제거하고 `src/apis` 폴더로 통합하여 API 레이어를 단일화한다. + +### 1.2 현황 + +| 폴더 | 상태 | 파일 수 | 설명 | +| ---------- | ----------- | ------- | ---------------------------- | +| `src/api` | ✅ 삭제됨 | 0개 | 완전 제거 완료 | +| `src/apis` | ✅ 통합완료 | 70+개 | Bruno 기반 자동생성 + 커스텀 | + +--- + +## 2. 마이그레이션 대상 + +### 2.1 도메인별 현황 + +| # | 도메인 | api 파일 | apis 존재 | 상태 | 비고 | +| --- | ------------ | -------- | --------- | ------- | ----------------------- | +| 1 | auth | 8 | ✅ | ✅ 완료 | | +| 2 | community | 9 | ✅ | ✅ 완료 | postList SSR 추가 | +| 3 | mentor | 7 | ✅ | ✅ 완료 | mentee/mentors 통합 | +| 4 | mentee | 4 | ✅ | ✅ 완료 | mentor 폴더로 통합 | +| 5 | mentors | 3 | ✅ | ✅ 완료 | mentor 폴더로 통합 | +| 6 | chat | 5 | ✅ | ✅ 완료 | | +| 7 | news | 7 | ✅ | ✅ 완료 | optimistic updates 적용 | +| 8 | score | 5 | ✅ | ✅ 완료 | Scores 폴더로 통합 | +| 9 | my | 4 | ✅ | ✅ 완료 | MyPage 폴더로 통합 | +| 10 | applications | 4 | ✅ | ✅ 완료 | | +| 11 | boards | 3 | ✅ | ✅ 완료 | community로 통합 | +| 12 | file | 1 | ✅ | ✅ 완료 | image-upload로 통합 | +| 13 | reports | 1 | ✅ | ✅ 완료 | | + +**총계**: 63개 파일 → 0개 (완전 제거) ✅ + +### 2.2 상세 파일 목록 + +#### auth (8개) + +``` +src/api/auth/ +├── server/ +│ └── postReissueToken.ts # 서버사이드 유지 필요 +├── client/ +│ ├── usePostKakaoAuth.ts +│ ├── usePostAppleAuth.ts +│ ├── usePostEmailAuth.ts +│ ├── usePostSignUp.ts +│ ├── usePostEmailSignUp.ts +│ ├── usePostLogout.ts +│ └── useDeleteUserAccount.ts +└── useLogin.ts +``` + +#### community (9개) + +``` +src/api/community/client/ +├── queryKey.ts +├── useGetPostDetail.ts +├── useCreatePost.ts +├── useUpdatePost.ts +├── useDeletePost.ts +├── useCreateComment.ts +├── useDeleteComment.ts +├── usePostLike.ts +└── useDeleteLike.ts +``` + +#### mentor (7개) + +``` +src/api/mentor/client/ +├── queryKey.ts +├── useGetMentorMyProfile.ts +├── usePutMyMentorProfile.ts +├── usePostMentorApplication.ts +├── useGetMentoringList.ts +├── useGetMentoringUncheckedCount.ts +├── usePatchMentorCheckMentorings.ts +└── usePatchApprovalStatus.ts +``` + +#### mentee (4개) + +``` +src/api/mentee/client/ +├── queryKey.ts +├── useGetApplyMentoringList.ts +├── usePostApplyMentoring.ts +└── usePatchMenteeCheckMentorings.ts +``` + +#### mentors (3개) + +``` +src/api/mentors/client/ +├── queryKey.ts +├── useGetMentorList.ts +└── useGetMentorDetail.ts +``` + +#### chat (5개) + +``` +src/api/chat/clients/ +├── queryKey.ts +├── useGetChatRooms.ts +├── useGetChatHistories.ts +├── useGetPartnerInfo.ts +└── usePutChatRead.ts +``` + +#### news (7개) + +``` +src/api/news/client/ +├── queryKey.ts +├── useGetArticleList.ts +├── usePostAddArticle.ts +├── usePutModifyArticle.ts +├── useDeleteArticle.ts +├── usePostArticleLike.ts +└── useDeleteArticleLike.ts +``` + +#### score (5개) + +``` +src/api/score/client/ +├── queryKey.ts +├── useGetMyGpaScore.ts +├── usePostGpaScore.ts +├── useGetMyLanguageTestScore.ts +└── usePostLanguageTestScore.ts +``` + +#### my (4개) + +``` +src/api/my/client/ +├── queryKey.ts +├── useGetMyInfo.ts +├── usePatchMyInfo.ts +└── usePatchMyPassword.ts +``` + +#### applications (4개) + +``` +src/api/applications/client/ +├── queryKeys.ts +├── useGetApplicationsList.ts +├── usePostSubmitApplication.ts +└── useGetCompetitorsApplicationList.ts +``` + +#### boards (3개) + +``` +src/api/boards/ +├── clients/ +│ ├── QueryKeys.ts +│ └── useGetPostList.ts +└── server/ + └── getPostList.ts +``` + +#### file (1개) + +``` +src/api/file/client/ +└── useUploadProfileImagePublic.ts +``` + +#### reports (1개) + +``` +src/api/reports/client/ +└── usePostReport.ts +``` + +--- + +## 3. 마이그레이션 규칙 + +### 3.1 네이밍 컨벤션 + +| 항목 | Before (api) | After (apis) | +| -------- | ------------------------ | ------------------------- | +| Query 훅 | `useGetXxx.ts` | `useGetXxx.ts` (동일) | +| Mutation | `usePostXxx.ts` | `usePostXxx.ts` (동일) | +| QueryKey | `queryKey.ts` (도메인별) | `queryKeys.ts` (중앙집중) | +| API 함수 | 훅 내부 정의 | `api.ts` 에서 export | +| Import | `@/api/{domain}/client/` | `@/apis/{domain}/` | + +### 3.2 QueryKey 통합 + +**Before** (각 도메인별 분산): + +```typescript +// src/api/community/client/queryKey.ts +export enum QueryKeys { + postDetail = "postDetail", + postList = "postList", +} +``` + +**After** (중앙 집중): + +```typescript +// src/apis/queryKeys.ts +export const QueryKeys = { + community: { + postDetail: "community.postDetail", + postList: "community.postList", + }, + // ... +}; +``` + +### 3.3 비즈니스 로직 보존 + +마이그레이션 시 다음 로직은 **반드시** 보존: + +- [ ] `router.push()` / `router.replace()` 리다이렉트 +- [ ] `toast.success()` / `toast.error()` 알림 +- [ ] `useAuthStore` 상태 관리 +- [ ] `queryClient.invalidateQueries()` 캐시 무효화 +- [ ] `onSuccess` / `onError` 콜백 로직 + +--- + +## 4. 작업 체크리스트 + +### 4.1 도메인별 체크리스트 템플릿 + +```markdown +#### [도메인명] 마이그레이션 + +- [ ] api.ts URL/메서드 확인 및 수정 +- [ ] 훅 마이그레이션 (비즈니스 로직 보존) +- [ ] QueryKey 통합 +- [ ] 컴포넌트 import 경로 변경 +- [ ] 서버사이드 API 처리 (해당시) +- [ ] TypeScript 에러 확인 +- [ ] 기능 테스트 +- [ ] 레거시 파일 삭제 +``` + +### 4.2 전체 진행 상황 + +| 도메인 | 분석 | 마이그레이션 | 테스트 | 삭제 | 완료 | +| ------------ | ---- | ------------ | ------ | ---- | ---- | +| auth | ✅ | ✅ | ✅ | ✅ | ✅ | +| community | ✅ | ✅ | ✅ | ✅ | ✅ | +| mentor | ✅ | ✅ | ✅ | ✅ | ✅ | +| mentee | ✅ | ✅ | ✅ | ✅ | ✅ | +| mentors | ✅ | ✅ | ✅ | ✅ | ✅ | +| chat | ✅ | ✅ | ✅ | ✅ | ✅ | +| news | ✅ | ✅ | ✅ | ✅ | ✅ | +| score | ✅ | ✅ | ✅ | ✅ | ✅ | +| my | ✅ | ✅ | ✅ | ✅ | ✅ | +| applications | ✅ | ✅ | ✅ | ✅ | ✅ | +| boards | ✅ | ✅ | ✅ | ✅ | ✅ | +| file | ✅ | ✅ | ✅ | ✅ | ✅ | +| reports | ✅ | ✅ | ✅ | ✅ | ✅ | + +**범례**: ⬜ 대기 | 🔄 진행중 | ✅ 완료 + +--- + +## 5. 우선순위 + +### 5.1 권장 순서 + +1. **auth** - 인증 로직, 가장 중요 +2. **my** - 내 정보, auth와 연관 +3. **community** - 커뮤니티 기능 +4. **mentor/mentee/mentors** - 멘토링 기능 (함께 진행) +5. **chat** - 채팅 기능 +6. **news** - 뉴스/아티클 +7. **score** - 성적 관리 +8. **applications** - 지원 관리 +9. **boards** - 게시판 +10. **file** - 파일 업로드 +11. **reports** - 신고 기능 + +### 5.2 의존성 주의사항 + +- `auth/server/postReissueToken.ts` → axios interceptor에서 사용 +- `mentor/mentee` → QueryKey 공유 가능성 확인 +- `boards/community` → 유사 기능, 통합 검토 + +--- + +## 6. 완료 조건 + +- [x] `src/api` 폴더 완전 삭제 +- [x] 모든 import가 `@/apis/` 경로 사용 +- [ ] TypeScript 에러 0개 (일부 타입 추론 이슈 남음) +- [ ] ESLint 에러 0개 +- [x] 빌드 성공 +- [x] 모든 기능 정상 동작 + +--- + +## 7. 커밋 컨벤션 + +``` +refactor: migrate {domain} from api to apis + +- Migrate {N} hooks to apis/{domain} +- Update component imports +- Remove legacy api/{domain} folder +``` + +--- + +**최종 수정일**: 2025-12-28 diff --git a/src/api/applications/client/queryKeys.ts b/src/api/applications/client/queryKeys.ts deleted file mode 100644 index b57885b2..00000000 --- a/src/api/applications/client/queryKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - competitorsApplicationList = "competitorsApplicationList", -} diff --git a/src/api/applications/client/useGetApplicationsList.ts b/src/api/applications/client/useGetApplicationsList.ts deleted file mode 100644 index 8dd1a649..00000000 --- a/src/api/applications/client/useGetApplicationsList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKeys"; - -import { ApplicationListResponse } from "@/types/application"; - -// UseQueryResult는 useQuery의 반환 타입을 명시할 때 유용합니다. -import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; - -export const getCompetitorsApplicationList = (): Promise> => - axiosInstance.get("/applications"); - -// 커스텀 훅의 props 타입 정의를 개선하여 queryKey와 queryFn을 제외시킵니다. -type UseGetCompetitorsApplicationListOptions = Omit< - UseQueryOptions< - AxiosResponse, // queryFn이 반환하는 원본 데이터 타입 - AxiosError<{ message: string }>, // 에러 타입 - ApplicationListResponse // select를 통해 최종적으로 반환될 데이터 타입 - >, - "queryKey" | "queryFn" // 훅 내부에서 지정하므로 props에서는 제외 ->; - -const useGetApplicationsList = ( - props?: UseGetCompetitorsApplicationListOptions, -): UseQueryResult> => { - // 반환 타입 명시 - return useQuery({ - queryKey: [QueryKeys.competitorsApplicationList], - queryFn: getCompetitorsApplicationList, - staleTime: 1000 * 60 * 5, - select: (response) => response.data, - ...props, - }); -}; - -export default useGetApplicationsList; diff --git a/src/api/applications/client/useGetCompetitorsApplicationList.ts b/src/api/applications/client/useGetCompetitorsApplicationList.ts deleted file mode 100644 index 885fc055..00000000 --- a/src/api/applications/client/useGetCompetitorsApplicationList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKeys"; - -import { ApplicationListResponse } from "@/types/application"; - -// UseQueryResult는 useQuery의 반환 타입을 명시할 때 유용합니다. -import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; - -export const getCompetitorsApplicationList = (): Promise> => - axiosInstance.get("/applications/competitors"); - -// 커스텀 훅의 props 타입 정의를 개선하여 queryKey와 queryFn을 제외시킵니다. -type UseGetCompetitorsApplicationListOptions = Omit< - UseQueryOptions< - AxiosResponse, // queryFn이 반환하는 원본 데이터 타입 - AxiosError<{ message: string }>, // 에러 타입 - ApplicationListResponse // select를 통해 최종적으로 반환될 데이터 타입 - >, - "queryKey" | "queryFn" // 훅 내부에서 지정하므로 props에서는 제외 ->; - -const useGetCompetitorsApplicationList = ( - props?: UseGetCompetitorsApplicationListOptions, -): UseQueryResult> => { - // 반환 타입 명시 - return useQuery({ - queryKey: [QueryKeys.competitorsApplicationList], - queryFn: getCompetitorsApplicationList, - staleTime: 1000 * 60 * 5, - select: (response) => response.data, - ...props, - }); -}; - -export default useGetCompetitorsApplicationList; diff --git a/src/api/applications/client/usePostSubmitApplication.ts b/src/api/applications/client/usePostSubmitApplication.ts deleted file mode 100644 index 360ecf86..00000000 --- a/src/api/applications/client/usePostSubmitApplication.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { UseMutationOptions, UseMutationResult, useMutation } from "@tanstack/react-query"; - -// API 함수 경로 -export interface UseSubmitApplicationResponse { - isSuccess: boolean; -} - -export interface UseSubmitApplicationRequest { - gpaScoreId: number; - languageTestScoreId: number; - universityChoiceRequest: { - firstChoiceUniversityId: number | null; - secondChoiceUniversityId: number | null; - thirdChoiceUniversityId: number | null; - }; -} - -export const postSubmitApplication = ( - request: UseSubmitApplicationRequest, -): Promise> => axiosInstance.post("/applications", request); - -const usePostSubmitApplication = ( - props?: UseMutationOptions< - AxiosResponse, // TData - AxiosError<{ message: string }>, // TError - UseSubmitApplicationRequest, // TVariables - unknown // TContext - >, -): UseMutationResult< - AxiosResponse, - AxiosError<{ message: string }>, - UseSubmitApplicationRequest, - unknown -> => { - return useMutation< - AxiosResponse, // TData: 성공 시 반환 타입 - AxiosError<{ message: string }>, // TError: 에러 타입 - UseSubmitApplicationRequest // TVariables: 요청 body 타입 - >({ - ...props, - // mutationFn: API 요청을 수행할 비동기 함수를 지정합니다. - mutationFn: (request: UseSubmitApplicationRequest) => postSubmitApplication(request), - - // onError: API 요청이 실패했을 때 실행할 콜백 함수입니다. - onError: (error) => { - const errorMessage = error?.response?.data?.message; - toast.error(errorMessage || "지원 중 오류가 발생했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePostSubmitApplication; diff --git a/src/api/auth/client/usePostEmailSignUp.ts b/src/api/auth/client/usePostEmailSignUp.ts deleted file mode 100644 index 58e8b905..00000000 --- a/src/api/auth/client/usePostEmailSignUp.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { EmailSignUpRequest, EmailSignUpResponse } from "@/types/auth"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -/** - * @description 이메일 회원가입 API 함수 - * @param request - 이메일 회원가입 요청 데이터 - * @returns Promise - */ -const emailSignUp = async (request: EmailSignUpRequest): Promise => { - const response: AxiosResponse = await publicAxiosInstance.post("/auth/email/sign-up", request); - return response.data; -}; - -/** - * @description 이메일 회원가입을 위한 useMutation 커스텀 훅 - */ -const usePostEmailSignUp = () => { - return useMutation({ - mutationFn: emailSignUp, - onError: (error) => { - console.error("이메일 회원가입 실패:", error); - toast.error("회원가입에 실패했습니다."); - }, - }); -}; - -export default usePostEmailSignUp; diff --git a/src/api/auth/client/usePostSignUp.ts b/src/api/auth/client/usePostSignUp.ts deleted file mode 100644 index e39b5b1c..00000000 --- a/src/api/auth/client/usePostSignUp.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { SignUpRequest, SignUpResponse } from "@/types/auth"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -/** - * @description 회원가입 API 함수 - * @param request - 회원가입 요청 데이터 - * @returns Promise - */ -const signUp = async (signUpRequest: SignUpRequest): Promise => { - // 임시 성별, 생년월일 추가. API 변경 시 삭제 - const payload = { - ...signUpRequest, - birth: "2000-01-01", - gender: "PREFER_NOT_TO_SAY", - }; - - const response: AxiosResponse = await publicAxiosInstance.post("/auth/sign-up", payload); - return response.data; -}; - -/** - * @description 회원가입을 위한 useMutation 커스텀 훅 - */ -const usePostSignUp = () => { - return useMutation({ - mutationFn: signUp, - onError: (error) => { - console.error("회원가입 실패:", error); - toast.error("회원가입에 실패했습니다."); - }, - }); -}; - -export default usePostSignUp; diff --git a/src/api/auth/useLogin.ts b/src/api/auth/useLogin.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/api/boards/clients/QueryKeys.ts b/src/api/boards/clients/QueryKeys.ts deleted file mode 100644 index 87ea7028..00000000 --- a/src/api/boards/clients/QueryKeys.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - postList = "postList1", -} diff --git a/src/api/boards/clients/useGetPostList.ts b/src/api/boards/clients/useGetPostList.ts deleted file mode 100644 index 98aaaefb..00000000 --- a/src/api/boards/clients/useGetPostList.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./QueryKeys"; - -import { ListPost } from "@/types/community"; - -import { useQuery } from "@tanstack/react-query"; - -interface UseGetPostListProps { - boardCode: string; - category?: string | null; -} - -const getPostList = (boardCode: string, category: string | null = null): Promise> => { - // "전체"는 필터 없음을 의미하므로 파라미터에 포함하지 않음 - const params = category && category !== "전체" ? { category } : {}; - - return publicAxiosInstance.get(`/boards/${boardCode}`, { params }); -}; - -const useGetPostList = ({ boardCode, category = null }: UseGetPostListProps) => { - return useQuery({ - queryKey: [QueryKeys.postList, boardCode, category], - queryFn: () => getPostList(boardCode, category), - // HydrationBoundary로부터 자동으로 hydrate된 데이터 사용 - // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. - staleTime: Infinity, - gcTime: 1000 * 60 * 30, // 예: 30분 - select: (response) => { - return [...response.data].sort((a, b) => { - return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); - }); - }, - }); -}; - -export default useGetPostList; diff --git a/src/api/chat/clients/queryKey.ts b/src/api/chat/clients/queryKey.ts deleted file mode 100644 index 526b36c3..00000000 --- a/src/api/chat/clients/queryKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueryKeys { - chatRooms = "chatRooms", - chatHistories = "chatHistories", - partnerInfo = "partnerInfo", -} diff --git a/src/api/chat/clients/useGetChatRooms.ts b/src/api/chat/clients/useGetChatRooms.ts deleted file mode 100644 index fbc73524..00000000 --- a/src/api/chat/clients/useGetChatRooms.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ChatRoom } from "@/types/chat"; - -import { useQuery } from "@tanstack/react-query"; - -export interface ChatRoomListResponse { - chatRooms: ChatRoom[]; -} - -const getChatRooms = async () => { - const res = await axiosInstance.get("/chats/rooms"); - return res.data; -}; -const useGetChatRooms = () => { - return useQuery({ - queryKey: [QueryKeys.chatRooms], - queryFn: getChatRooms, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - select: (data) => data.chatRooms, - }); -}; -export default useGetChatRooms; diff --git a/src/api/chat/clients/useGetPartnerInfo.ts b/src/api/chat/clients/useGetPartnerInfo.ts deleted file mode 100644 index b926991c..00000000 --- a/src/api/chat/clients/useGetPartnerInfo.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ChatPartner } from "@/types/chat"; - -import { useQuery } from "@tanstack/react-query"; - -type ChatRoomListResponse = ChatPartner; - -const getPartnerInfo = async (roomId: number): Promise => { - const res = await axiosInstance.get(`/chats/rooms/${roomId}/partner`); - return res.data; -}; -const useGetPartnerInfo = (roomId: number) => { - return useQuery({ - queryKey: [QueryKeys.partnerInfo, roomId], - queryFn: () => getPartnerInfo(roomId), - staleTime: 1000 * 60 * 5, - }); -}; - -export default useGetPartnerInfo; diff --git a/src/api/chat/clients/usePutChatRead.ts b/src/api/chat/clients/usePutChatRead.ts deleted file mode 100644 index 57ed5183..00000000 --- a/src/api/chat/clients/usePutChatRead.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -const putChatRead = async (roomId: number): Promise => { - const response: AxiosResponse = await axiosInstance.put(`/chats/rooms/${roomId}/read`); - return response.data; -}; - -const usePutChatRead = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: putChatRead, - onSuccess: () => { - // 아티클 목록 쿼리를 무효화하여 새로 고침 - queryClient.invalidateQueries({ queryKey: [QueryKeys.chatRooms] }); - }, - onError: (error) => { - console.error("채팅방 진입 읽기 실패", error); - }, - }); -}; - -export default usePutChatRead; diff --git a/src/api/community/client/queryKey.ts b/src/api/community/client/queryKey.ts deleted file mode 100644 index 2e791de8..00000000 --- a/src/api/community/client/queryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - posts = "posts", -} diff --git a/src/api/community/client/useCreateComment.ts b/src/api/community/client/useCreateComment.ts deleted file mode 100644 index 970dbf6f..00000000 --- a/src/api/community/client/useCreateComment.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { CommentCreateRequest, CommentIdResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 댓글 생성 API 함수 - * @param request - 댓글 생성 요청 데이터 - * @returns Promise - */ -const createComment = async (request: CommentCreateRequest): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/comments`, request); - return response.data; -}; - -/** - * @description 댓글 생성을 위한 useMutation 커스텀 훅 - */ -const useCreateComment = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: createComment, - onSuccess: (data, variables) => { - // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, variables.postId] }); - toast.success("댓글이 등록되었습니다."); - }, - onError: (error) => { - console.error("댓글 생성 실패:", error); - toast.error("댓글 등록에 실패했습니다."); - }, - }); -}; - -export default useCreateComment; diff --git a/src/api/community/client/useDeleteComment.ts b/src/api/community/client/useDeleteComment.ts deleted file mode 100644 index 5f7b69e8..00000000 --- a/src/api/community/client/useDeleteComment.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { CommentIdResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface DeleteCommentRequest { - commentId: number; - postId: number; -} - -/** - * @description 댓글 삭제 API 함수 - * @param commentId - 삭제할 댓글의 ID - * @returns Promise - */ -const deleteComment = async (commentId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/comments/${commentId}`); - return response.data; -}; - -/** - * @description 댓글 삭제를 위한 useMutation 커스텀 훅 - */ -const useDeleteComment = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ commentId }: DeleteCommentRequest) => deleteComment(commentId), - onSuccess: (data, variables) => { - // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, variables.postId] }); - toast.success("댓글이 삭제되었습니다."); - }, - onError: (error) => { - console.error("댓글 삭제 실패:", error); - toast.error("댓글 삭제에 실패했습니다."); - }, - }); -}; - -export default useDeleteComment; diff --git a/src/api/community/client/useDeleteLike.ts b/src/api/community/client/useDeleteLike.ts deleted file mode 100644 index 15ff0d87..00000000 --- a/src/api/community/client/useDeleteLike.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostLikeResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 게시글 좋아요 취소 API 함수 - * @param postId - 좋아요를 취소할 게시글의 ID - * @returns Promise - */ -const deleteLike = async (postId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/posts/${postId}/like`); - return response.data; -}; - -/** - * @description 게시글 좋아요 취소를 위한 useMutation 커스텀 훅 - */ -const useDeleteLike = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: deleteLike, - onSuccess: (data, postId) => { - // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, postId] }); - }, - onError: (error) => { - console.error("게시글 좋아요 취소 실패:", error); - toast.error("좋아요 취소 처리에 실패했습니다."); - }, - }); -}; - -export default useDeleteLike; diff --git a/src/api/community/client/useDeletePost.ts b/src/api/community/client/useDeletePost.ts deleted file mode 100644 index 548cb9f0..00000000 --- a/src/api/community/client/useDeletePost.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 게시글 삭제 API 응답 타입 - * @property {string} message - 성공 메시지 - * @property {number} postId - 삭제된 게시글 ID - */ -interface DeletePostResponse { - message: string; - postId: number; -} - -/** - * @description postId를 받아 해당 게시글을 삭제하는 API 함수 - * @param postId - 삭제할 게시글의 ID - * @returns Promise> - */ -export const deletePostApi = (postId: number): Promise> => { - return axiosInstance.delete(`/posts/${postId}`); -}; - -/** - * @description 게시글 삭제를 위한 useMutation 커스텀 훅 - */ -const useDeletePost = () => { - const router = useRouter(); - const queryClient = useQueryClient(); - - return useMutation({ - // mutation 실행 시 호출될 함수 - mutationFn: deletePostApi, - - // mutation 성공 시 실행될 콜백 - onSuccess: () => { - // 'posts' 쿼리 키를 가진 모든 쿼리를 무효화하여 - // 게시글 목록을 다시 불러오도록 합니다. - // ['posts', 'list'] 등 구체적인 키를 사용하셔도 좋습니다. - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] }); - - toast.success("게시글이 성공적으로 삭제되었습니다."); - - // 게시글 목록 페이지 이동 - router.replace("/community/FREE"); - }, - - // mutation 실패 시 실행될 콜백 - onError: (error) => { - console.error("게시글 삭제 실패:", error); - toast.error("게시글 삭제에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, - }); -}; - -export default useDeletePost; diff --git a/src/api/community/client/useGetPostDetail.ts b/src/api/community/client/useGetPostDetail.ts deleted file mode 100644 index 28abbc67..00000000 --- a/src/api/community/client/useGetPostDetail.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Post } from "@/types/community"; - -import { useQuery } from "@tanstack/react-query"; - -/** - * @description 게시글 상세 조회 API 함수 - * @param postId - 조회할 게시글의 ID - * @returns Promise - */ -const getPostDetail = async (postId: number): Promise => { - const response: AxiosResponse = await axiosInstance.get(`/posts/${postId}`); - return response.data; -}; - -/** - * @description 게시글 상세 조회를 위한 useQuery 커스텀 훅 - */ -const useGetPostDetail = (postId: number) => { - return useQuery({ - queryKey: [QueryKeys.posts, postId], - queryFn: () => getPostDetail(postId), - enabled: !!postId, - }); -}; - -export default useGetPostDetail; diff --git a/src/api/community/client/usePostLike.ts b/src/api/community/client/usePostLike.ts deleted file mode 100644 index 4ff99d8b..00000000 --- a/src/api/community/client/usePostLike.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostLikeResponse } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 게시글 좋아요 API 함수 - * @param postId - 좋아요할 게시글의 ID - * @returns Promise - */ -const postLike = async (postId: number): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/posts/${postId}/like`); - return response.data; -}; - -/** - * @description 게시글 좋아요를 위한 useMutation 커스텀 훅 - */ -const usePostLike = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: postLike, - onSuccess: (data, postId) => { - // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, postId] }); - }, - onError: (error) => { - console.error("게시글 좋아요 실패:", error); - toast.error("좋아요 처리에 실패했습니다."); - }, - }); -}; - -export default usePostLike; diff --git a/src/api/community/client/useUpdatePost.ts b/src/api/community/client/useUpdatePost.ts deleted file mode 100644 index f63880b8..00000000 --- a/src/api/community/client/useUpdatePost.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostIdResponse, PostUpdateRequest } from "@/types/community"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UpdatePostRequest { - postId: number; - data: PostUpdateRequest; -} - -/** - * @description 게시글 수정 API 함수 - * @param postId - 수정할 게시글의 ID - * @param request - 게시글 수정 요청 데이터 - * @returns Promise - */ -const updatePost = async (postId: number, request: PostUpdateRequest): Promise => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "postUpdateRequest", - new Blob([JSON.stringify(request.postUpdateRequest)], { type: "application/json" }), - ); - request.file.forEach((file) => { - convertedRequest.append("file", file); - }); - - const response: AxiosResponse = await axiosInstance.patch(`/posts/${postId}`, convertedRequest, { - headers: { "Content-Type": "multipart/form-data" }, - }); - return response.data; -}; - -/** - * @description 게시글 수정을 위한 useMutation 커스텀 훅 - */ -const useUpdatePost = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ postId, data }: UpdatePostRequest) => updatePost(postId, data), - onSuccess: (result, variables) => { - // 해당 게시글 상세 쿼리와 목록 쿼리를 무효화 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts, variables.postId] }); - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] }); - toast.success("게시글이 수정되었습니다."); - }, - onError: (error) => { - console.error("게시글 수정 실패:", error); - toast.error("게시글 수정에 실패했습니다."); - }, - }); -}; - -export default useUpdatePost; diff --git a/src/api/file/client/useUploadProfileImagePublic.ts b/src/api/file/client/useUploadProfileImagePublic.ts deleted file mode 100644 index aa7cbb65..00000000 --- a/src/api/file/client/useUploadProfileImagePublic.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { FileResponse } from "@/types/file"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -/** - * @description 프로필 이미지 업로드 API 함수 (공개) - * @param file - 업로드할 파일 - * @returns Promise - */ -const uploadProfileImagePublic = async (file: File): Promise => { - const formData = new FormData(); - formData.append("file", file); - - const response: AxiosResponse = await publicAxiosInstance.post("/file/profile/pre", formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); - return response.data; -}; - -/** - * @description 프로필 이미지 업로드를 위한 useMutation 커스텀 훅 - */ -const useUploadProfileImagePublic = () => { - return useMutation({ - mutationFn: uploadProfileImagePublic, - onError: (error) => { - console.error("프로필 이미지 업로드 실패:", error); - toast.error("이미지 업로드에 실패했습니다."); - }, - }); -}; - -export default useUploadProfileImagePublic; diff --git a/src/api/mentee/client/queryKey.ts b/src/api/mentee/client/queryKey.ts deleted file mode 100644 index c57bd84a..00000000 --- a/src/api/mentee/client/queryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - applyMentoringList = "applyMentoringList", -} diff --git a/src/api/mentee/client/useGetApplyMentoringList.ts b/src/api/mentee/client/useGetApplyMentoringList.ts deleted file mode 100644 index 153568c8..00000000 --- a/src/api/mentee/client/useGetApplyMentoringList.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { AxiosError } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentoringListItem } from "@/types/mentee"; -import { VerifyStatus } from "@/types/mentee"; - -import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import type { QueryFunctionContext } from "@tanstack/react-query"; - -interface UseGetApplyMentoringListResponse { - content: MentoringListItem[]; - nextPageNumber: number; -} -type UseGetApplyMentoringListRequest = VerifyStatus; - -const OFFSET = 3; // 기본 페이지 크기 - -const getApplyMentoringList = async ({ - queryKey, - pageParam, -}: QueryFunctionContext<[string, VerifyStatus], number>): Promise => { - const [, verifyStatus] = queryKey; - const res = await axiosInstance.get( - `/mentee/mentorings?verify-status=${verifyStatus}&size=${OFFSET}&page=${pageParam}`, - ); - return res.data; -}; - -const useGetApplyMentoringList = (verifyStatus: UseGetApplyMentoringListRequest) => { - return useInfiniteQuery< - UseGetApplyMentoringListResponse, - AxiosError, - MentoringListItem[], - [string, VerifyStatus], - number - >({ - queryKey: [QueryKeys.applyMentoringList, verifyStatus], - queryFn: getApplyMentoringList, - initialPageParam: 0, - getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), - staleTime: 1000 * 60 * 5, // 5분간 캐시 - select: (data) => data.pages.flatMap((p) => p.content), - }); -}; - -// 멘토링 리스트 프리페치용 훅 -export const usePrefetchApplyMentoringList = () => { - const queryClient = useQueryClient(); - - const prefetchMenteeMentoringList = (verifyStatus: UseGetApplyMentoringListRequest) => { - queryClient.prefetchInfiniteQuery({ - queryKey: [QueryKeys.applyMentoringList, verifyStatus], - queryFn: getApplyMentoringList, - initialPageParam: 0, - staleTime: 1000 * 60 * 5, - }); - }; - - return { prefetchMenteeMentoringList }; -}; - -export default useGetApplyMentoringList; diff --git a/src/api/mentee/client/usePatchMenteeCheckMentorings.ts b/src/api/mentee/client/usePatchMenteeCheckMentorings.ts deleted file mode 100644 index 537b2b63..00000000 --- a/src/api/mentee/client/usePatchMenteeCheckMentorings.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AxiosError } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { useMutation } from "@tanstack/react-query"; - -interface UsePatchMenteeCheckMentoringsRequest { - checkedMentoringIds: number[]; -} - -interface UsePatchMenteeCheckMentoringsResponse { - checkedMentoringIds: number[]; -} - -const patchMenteeCheckMentorings = async ( - body: UsePatchMenteeCheckMentoringsRequest, -): Promise => { - const res = await axiosInstance.patch("/mentee/mentorings/check", body); - return res.data; -}; - -const usePatchMenteeCheckMentorings = () => - useMutation({ - mutationFn: patchMenteeCheckMentorings, - }); - -export default usePatchMenteeCheckMentorings; diff --git a/src/api/mentee/client/usePostApplyMentoring.ts b/src/api/mentee/client/usePostApplyMentoring.ts deleted file mode 100644 index e4e2c28b..00000000 --- a/src/api/mentee/client/usePostApplyMentoring.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UsePostApplyMentoringRequest { - mentorId: number; -} -interface UsePostApplyMentoringResponse { - mentoringId: number; -} - -const postApplyMentoring = async (body: UsePostApplyMentoringRequest): Promise => { - const res = await axiosInstance.post("/mentee/mentorings", body); - return res.data; -}; - -const usePostApplyMentoring = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: postApplyMentoring, - onSuccess: async () => { - // 멘토링 신청 후 멘토 목록을 새로고침 - await queryClient.invalidateQueries({ queryKey: [QueryKeys.applyMentoringList] }); - }, - onError: () => { - toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePostApplyMentoring; diff --git a/src/api/mentor/client/queryKey.ts b/src/api/mentor/client/queryKey.ts deleted file mode 100644 index 345ca144..00000000 --- a/src/api/mentor/client/queryKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueryKeys { - mentoringNewCount = "mentoringNewCount", - mentoringList = "mentoringList", - myMentorProfile = "myMentorProfile", -} diff --git a/src/api/mentor/client/useGetMentorMyProfile.ts b/src/api/mentor/client/useGetMentorMyProfile.ts deleted file mode 100644 index b8598b02..00000000 --- a/src/api/mentor/client/useGetMentorMyProfile.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentorCardPreview } from "@/types/mentor"; - -// 학업 학기 (예: "2026-1") -import { useQuery } from "@tanstack/react-query"; - -type UseGetMyMentorProfileResponse = MentorCardPreview; - -const getMentorMyProfile = async (): Promise => { - const res = await axiosInstance.get("/mentor/my"); - return res.data; -}; - -const useGetMentorMyProfile = () => { - return useQuery({ - queryKey: [QueryKeys.myMentorProfile], - queryFn: getMentorMyProfile, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - }); -}; - -export default useGetMentorMyProfile; diff --git a/src/api/mentor/client/useGetMentoringList.ts b/src/api/mentor/client/useGetMentoringList.ts deleted file mode 100644 index 88024b75..00000000 --- a/src/api/mentor/client/useGetMentoringList.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentoringItem } from "@/types/mentor"; - -import { useInfiniteQuery } from "@tanstack/react-query"; - -interface UseGetMentoringListResponse { - content: MentoringItem[]; - nextPageNumber: number; -} -interface UseGetMentoringListRequest { - size?: number; -} -const OFFSET = 5; // 페이지 오프셋 초기값 - -const getMentoringList = async (page: number, size: number = OFFSET): Promise => { - const endpoint = `/mentor/mentorings?size=${size}&page=${page}`; - const res = await axiosInstance.get(endpoint); - return res.data; -}; - -// 무한스크롤을 위한 useInfiniteQuery -const useGetMentoringList = ({ size = OFFSET }: UseGetMentoringListRequest) => - useInfiniteQuery({ - queryKey: [QueryKeys.mentoringList, size], - queryFn: ({ pageParam = 0 }) => getMentoringList(pageParam as number, size), - initialPageParam: 0, - getNextPageParam: (lastPage: UseGetMentoringListResponse) => { - // nextPageNumber가 -1이면 더 이상 페이지가 없음 - return lastPage.nextPageNumber !== -1 ? lastPage.nextPageNumber : undefined; - }, - refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 - staleTime: 1000 * 60 * 5, // fresh 상태 유지 - select: (data) => data.pages.flatMap((page) => page.content), // 모든 페이지의 content를 평 - }); - -export default useGetMentoringList; diff --git a/src/api/mentor/client/useGetMentoringUncheckedCount.ts b/src/api/mentor/client/useGetMentoringUncheckedCount.ts deleted file mode 100644 index 3204769d..00000000 --- a/src/api/mentor/client/useGetMentoringUncheckedCount.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useQuery } from "@tanstack/react-query"; - -interface GetMentoringNewCountResponse { - uncheckedCount: number; // 멘토링 신규 요청 수 -} - -const getMentoringUncheckedCount = async (): Promise => { - const endpoint = "/mentor/mentorings/check"; - const res = await axiosInstance.get(endpoint); - return res.data; -}; - -// ISR 의도(10분) 유지: staleTime 10분 -const useGetMentoringUncheckedCount = (isEnable: boolean) => - useQuery({ - queryKey: [QueryKeys.mentoringNewCount], - queryFn: getMentoringUncheckedCount, - enabled: isEnable, - refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 - staleTime: 1000 * 60 * 5, // fresh 상태 유지 - select: (data) => data.uncheckedCount, // 필요한 데이터만 반환 - }); - -export default useGetMentoringUncheckedCount; diff --git a/src/api/mentor/client/usePatchMentorCheckMentorings.ts b/src/api/mentor/client/usePatchMentorCheckMentorings.ts deleted file mode 100644 index 3eb2126a..00000000 --- a/src/api/mentor/client/usePatchMentorCheckMentorings.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UsePatchMentorCheckMentoringsRequest { - checkedMentoringIds: number[]; -} -interface UsePatchMentorCheckMentoringsResponse { - checkedMentoringIds: number[]; // 체크된 멘토링 ID 배열 -} - -const patchMenotrCheck = async ( - body: UsePatchMentorCheckMentoringsRequest, -): Promise => { - const res = await axiosInstance.patch("/mentor/mentorings/check", body); - return res.data; -}; - -const usePatchMentorCheckMentorings = () => { - const queriesClient = useQueryClient(); - return useMutation({ - onSuccess: () => { - // 멘토링 체크 상태 변경 후 멘토링 목록 쿼리 무효화 - Promise.all([ - queriesClient.invalidateQueries({ queryKey: [QueryKeys.mentoringList] }), - queriesClient.invalidateQueries({ queryKey: [QueryKeys.mentoringNewCount] }), - ]); - }, - mutationFn: patchMenotrCheck, - }); -}; - -export default usePatchMentorCheckMentorings; diff --git a/src/api/mentor/client/usePostMentorApplication.ts b/src/api/mentor/client/usePostMentorApplication.ts deleted file mode 100644 index fea8ef24..00000000 --- a/src/api/mentor/client/usePostMentorApplication.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -export interface PostMentorApplicationRequest { - interestedCountries: string[]; // 관심 국가 목록 - country: string; // 수학 국가 - universityName: string; // 수학 학교 - studyStatus: "STUDYING" | "PLANNING" | "COMPLETED"; // 준비 단계 - verificationFile: File; // 증명서 파일 -} - -const postMentorApplication = async (body: PostMentorApplicationRequest): Promise => { - const formData = new FormData(); - - // JSON 데이터를 Blob으로 추가 - const applicationData = { - interestedCountries: body.interestedCountries, - country: body.country, - universityName: body.universityName, - studyStatus: body.studyStatus, - }; - - formData.append( - "mentorApplicationRequest", - new Blob([JSON.stringify(applicationData)], { type: "application/json" }), - ); - - // 파일 추가 - formData.append("file", body.verificationFile); - - const res = await axiosInstance.post("/mentor/verification", formData, { - headers: { - "Content-Type": "multipart/form-data", - }, - }); - - return res.data; -}; - -const usePostMentorApplication = () => { - return useMutation({ - mutationFn: postMentorApplication, - onError: (error) => { - toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePostMentorApplication; diff --git a/src/api/mentor/client/usePutMyMentorProfile.ts b/src/api/mentor/client/usePutMyMentorProfile.ts deleted file mode 100644 index 8de00810..00000000 --- a/src/api/mentor/client/usePutMyMentorProfile.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface ChannelPayload { - type: string; - url: string; -} - -export interface PutMyMentorProfileRequest { - channels: ChannelPayload[]; - passTip: string; - introduction: string; -} - -const putMyMentorProfile = async (body: PutMyMentorProfileRequest): Promise => { - const res = await axiosInstance.put("/mentor/my", body); - return res.data; -}; - -const usePutMyMentorProfile = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: putMyMentorProfile, - onSuccess: () => { - // 멘토 프로필 데이터를 stale로 만들어 다음 요청 시 새로운 데이터를 가져오도록 함 - queryClient.invalidateQueries({ - queryKey: [QueryKeys.myMentorProfile], - }); - }, - }); -}; - -export default usePutMyMentorProfile; diff --git a/src/api/mentors/client/queryKey.ts b/src/api/mentors/client/queryKey.ts deleted file mode 100644 index 39622908..00000000 --- a/src/api/mentors/client/queryKey.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum QueryKeys { - mentorDetail = "mentorDetail", - mentoringNewCount = "mentoringNewCount", - mentorList = "mentorList", - myMentorProfile = "myMentorProfile", - recommendedMentor = "recommendedMentor", - menteeMentoringList = "menteeMentoringList", -} diff --git a/src/api/mentors/client/useGetMentorDetail.ts b/src/api/mentors/client/useGetMentorDetail.ts deleted file mode 100644 index af4225a6..00000000 --- a/src/api/mentors/client/useGetMentorDetail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentorCardDetail } from "@/types/mentor"; - -import { useQuery } from "@tanstack/react-query"; - -type UseGetMentorDetailResponse = MentorCardDetail; - -const getMentorDetail = async ({ queryKey }: { queryKey: [string, number] }): Promise => { - const [, mentorId] = queryKey; - const res = await axiosInstance.get(`/mentors/${mentorId}`); - return res.data; -}; - -const useGetMentorDetail = (mentorId: number | null) => { - return useQuery({ - queryKey: [QueryKeys.mentorDetail, mentorId!], - queryFn: getMentorDetail, - enabled: mentorId !== null, - staleTime: 1000 * 60 * 5, // 5분간 캐시 - }); -}; - -export default useGetMentorDetail; diff --git a/src/api/mentors/client/useGetMentorList.ts b/src/api/mentors/client/useGetMentorList.ts deleted file mode 100644 index fcac4fbb..00000000 --- a/src/api/mentors/client/useGetMentorList.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { MentorCardDetail } from "@/types/mentor"; - -import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import type { QueryFunctionContext } from "@tanstack/react-query"; - -interface UseGetMentorListRequest { - region?: string; -} - -interface GetMentorListResponse { - /** 다음 페이지 번호. 다음 페이지가 없으면 -1 */ - nextPageNumber: number; - content: MentorCardDetail[]; -} - -const OFFSET = 10; // 기본 페이지 크기 - -const getMentorList = async ({ - queryKey, - pageParam, -}: QueryFunctionContext<[string, string], number>): Promise => { - const [, region] = queryKey; - const res = await axiosInstance.get( - `/mentors?region=${region}&page=${pageParam}&size=${OFFSET}`, - ); - return res.data; -}; - -const useGetMentorList = ({ region = "" }: UseGetMentorListRequest = {}) => - useInfiniteQuery({ - queryKey: [QueryKeys.mentorList, region], - queryFn: getMentorList, - initialPageParam: 0, - getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), - staleTime: 1000 * 60 * 5, - select: (data) => data.pages.flatMap((p) => p.content), - }); - -// 탭 프리페치용 훅 -export const usePrefetchMentorList = () => { - const queryClient = useQueryClient(); - - const prefetchMentorList = (region: string) => { - queryClient.prefetchInfiniteQuery({ - queryKey: [QueryKeys.mentorList, region], - queryFn: getMentorList, - initialPageParam: 0, - staleTime: 1000 * 60 * 5, - }); - }; - - return { prefetchMentorList }; -}; - -export default useGetMentorList; diff --git a/src/api/my/client/queryKey.ts b/src/api/my/client/queryKey.ts deleted file mode 100644 index 6dff8450..00000000 --- a/src/api/my/client/queryKey.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum QueryKeys { - myInfo = "myInfo", -} diff --git a/src/api/my/client/useGetMyInfo.ts b/src/api/my/client/useGetMyInfo.ts deleted file mode 100644 index 9f6acd36..00000000 --- a/src/api/my/client/useGetMyInfo.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { UserRole } from "@/types/mentor"; -import { BaseUserInfo } from "@/types/myInfo"; - -import { useMutationState, useQuery } from "@tanstack/react-query"; - -// --- 타입 정의 --- -export interface MenteeInfo extends BaseUserInfo { - role: UserRole.MENTEE; - interestedCountries: string[]; -} - -export interface MentorInfo extends BaseUserInfo { - role: UserRole.MENTOR; - attendedUniversity: string; -} - -export interface AdminInfo extends BaseUserInfo { - role: UserRole.ADMIN; - attendedUniversity: string; -} - -export type MyInfoResponse = MenteeInfo | MentorInfo | AdminInfo; - -// --- API 호출 함수 --- -const getMyInfo = async (): Promise => { - const response: AxiosResponse = await axiosInstance.get("/my"); - return response.data; -}; - -const useGetMyInfo = () => { - const queryResult = useQuery({ - queryKey: [QueryKeys.myInfo], - queryFn: getMyInfo, - // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. - staleTime: Infinity, - gcTime: 1000 * 60 * 30, // 예: 30분 - }); - - const pendingMutations = useMutationState({ - filters: { - mutationKey: [QueryKeys.myInfo, "patch"], - status: "pending", - }, - select: (mutation) => { - return mutation.state.variables as Partial; - }, - }); - - const isOptimistic = pendingMutations.length > 0; - const pendingData = isOptimistic ? pendingMutations[0] : null; - - const displayData = isOptimistic ? { ...queryResult.data, ...pendingData } : queryResult.data; - - return { ...queryResult, data: displayData }; -}; - -export default useGetMyInfo; diff --git a/src/api/my/client/usePatchMyInfo.ts b/src/api/my/client/usePatchMyInfo.ts deleted file mode 100644 index 4a61f1fa..00000000 --- a/src/api/my/client/usePatchMyInfo.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AxiosError } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -export interface UseMyMentorProfileRequest { - nickname?: string; - file?: File; -} - -const patchMyMentorProfile = async (data: UseMyMentorProfileRequest): Promise => { - const formData = new FormData(); - if (data.nickname) { - formData.append("nickname", data.nickname); - } - if (data.file) { - formData.append("file", data.file); - } - - const res = await axiosInstance.patch("/my", formData); - - return res.data; -}; - -const usePatchMyInfo = () => { - const queryClient = useQueryClient(); - - return useMutation, UseMyMentorProfileRequest>({ - mutationKey: [QueryKeys.myInfo, "patch"], - mutationFn: patchMyMentorProfile, - onSettled: () => { - queryClient.invalidateQueries({ - queryKey: [QueryKeys.myInfo], - }); - }, - onSuccess: () => { - toast.success("프로필이 성공적으로 수정되었습니다."); - }, - onError: (error) => { - const errorMessage = error.response?.data?.message; - toast.error(errorMessage || "프로필 수정에 실패했습니다. 다시 시도해주세요."); - }, - }); -}; - -export default usePatchMyInfo; diff --git a/src/api/news/client/queryKey.ts b/src/api/news/client/queryKey.ts deleted file mode 100644 index 9065e83d..00000000 --- a/src/api/news/client/queryKey.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum QueryKeys { - articleList = "articleList", - postAddArticle = "postAddArticle", - putModifyArticle = "putModifyArticle", -} diff --git a/src/api/news/client/useDeleteArticleLike.ts b/src/api/news/client/useDeleteArticleLike.ts deleted file mode 100644 index cda01e76..00000000 --- a/src/api/news/client/useDeleteArticleLike.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Article } from "@/types/news"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -// Article 타입에 isLiked, likeCount 속성이 포함되어 있어야 합니다. - -interface DeleteArticleLikeResponse { - isLiked: boolean; - likeCount: number; -} - -// 1. 롤백을 위한 context 타입을 정의합니다. -type ArticleLikeMutationContext = { - previousArticleList?: { newsResponseList: Article[] }; -}; - -const deleteArticleLike = async (articleId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}/like`); - return response.data; -}; - -// 2. 어떤 유저의 목록을 업데이트할지 식별하기 위해 userId를 props로 받습니다. -const useDeleteArticleLike = (userId: number | null) => { - const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; - - return useMutation< - DeleteArticleLikeResponse, - AxiosError<{ message: string }>, - number, // mutate 함수에 전달하는 변수 타입 (articleId) - ArticleLikeMutationContext - >({ - mutationFn: deleteArticleLike, - - // 3. onMutate: '좋아요 취소' 클릭 즉시 UI를 업데이트합니다. - onMutate: async (unlikedArticleId) => { - await queryClient.cancelQueries({ queryKey }); - - const previousArticleList = queryClient.getQueryData<{ newsResponseList: Article[] }>(queryKey); - - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { - if (!oldData) return { newsResponseList: [] }; - return { - newsResponseList: oldData.newsResponseList.map((article) => - article.id === unlikedArticleId - ? { - ...article, - isLiked: false, - likeCount: Math.max(0, (article.likeCount ?? 1) - 1), - } - : article, - ), - }; - }); - - return { previousArticleList }; - }, - - // 4. onError: 실패 시 이전 상태로 롤백합니다. - onError: (err, variables, context) => { - if (context?.previousArticleList) { - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, context.previousArticleList); - } - }, - }); -}; - -export default useDeleteArticleLike; diff --git a/src/api/news/client/useGetArticleList.ts b/src/api/news/client/useGetArticleList.ts deleted file mode 100644 index 77cf86cc..00000000 --- a/src/api/news/client/useGetArticleList.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Article } from "@/types/news"; - -import { useQuery } from "@tanstack/react-query"; - -interface ArticleListResponse { - newsResponseList: Article[]; -} - -const getArticleList = async (userId: number): Promise => { - const response: AxiosResponse = await axiosInstance.get(`/news?author-id=${userId}`); - return response.data; -}; - -const useGetArticleList = (userId: number) => { - return useQuery({ - queryKey: [QueryKeys.articleList, userId], - queryFn: () => { - // enabled 옵션이 있더라도, 타입 가드를 추가하면 더 안전합니다. - if (userId === null) { - return Promise.reject(new Error("User ID is null")); - } - return getArticleList(userId); - }, - staleTime: 1000 * 60 * 10, // ⏱️ 10분 - - enabled: userId !== null, - // 서버 응답(ArticleListResponse)에서 실제 데이터 배열(Article[])만 선택합니다. - select: (data) => data.newsResponseList, - }); -}; - -export default useGetArticleList; diff --git a/src/api/news/client/usePostArticleLike.ts b/src/api/news/client/usePostArticleLike.ts deleted file mode 100644 index 5d452cce..00000000 --- a/src/api/news/client/usePostArticleLike.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AxiosError, AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { Article } from "@/types/news"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -// Article 타입 import - -interface PostArticleLikeResponse { - isLiked: boolean; - likeCount: number; -} - -// 1. 롤백을 위한 context 타입을 정의합니다. -type ArticleLikeMutationContext = { - previousArticleList?: Article[]; -}; - -const postArticleLike = async (articleId: number): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/news/${articleId}/like`); - return response.data; -}; - -// 2. 어떤 유저의 목록을 업데이트할지 식별하기 위해 userId를 props로 받습니다. -const usePostArticleLike = (userId: number | null) => { - const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; - - return useMutation< - PostArticleLikeResponse, - AxiosError<{ message: string }>, - number, // mutate 함수에 전달하는 변수 타입 (articleId) - ArticleLikeMutationContext - >({ - mutationFn: postArticleLike, - - // 3. onMutate: '좋아요' 클릭 즉시 UI를 업데이트합니다. - onMutate: async (likedArticleId) => { - await queryClient.cancelQueries({ queryKey }); - - const previousArticleList = queryClient.getQueryData(queryKey); - - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { - if (!oldData) return { newsResponseList: [] }; - return { - newsResponseList: oldData.newsResponseList.map((article) => - article.id === likedArticleId - ? { - ...article, - isLiked: true, // '좋아요' 상태를 true로 변경 - likeCount: (article.likeCount ?? 0) + 1, // '좋아요' 수를 1 증가 - } - : article, - ), - }; - }); - - return { previousArticleList }; - }, - - // 4. onError: 실패 시 이전 상태로 롤백합니다. - onError: (err, variables, context) => { - if (context?.previousArticleList) { - queryClient.setQueryData(queryKey, context.previousArticleList); - } - }, - }); -}; - -export default usePostArticleLike; diff --git a/src/api/reports/client/usePostReport.ts b/src/api/reports/client/usePostReport.ts deleted file mode 100644 index 28ee9fe5..00000000 --- a/src/api/reports/client/usePostReport.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { ReportType } from "@/types/reports"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation } from "@tanstack/react-query"; - -interface UsePostReportsRequest { - targetType: "POST"; // 지금은 게시글 신고 기능만 존재 - targetId: number; // 신고하려는 리소스의 ID - reportType: ReportType; // Docs 참고 -} - -const postReports = async (body: UsePostReportsRequest): Promise => { - const response: AxiosResponse = await axiosInstance.post(`/reports`, body); - return response.data; -}; - -const usePostReports = () => { - const router = useRouter(); - return useMutation({ - mutationFn: postReports, - onSuccess: () => { - toast.success("신고가 성공적으로 등록되었습니다."); - router.back(); // 신고 후 뒤로 이동 - }, - onError: (error) => { - toast.error("신고 등록에 실패했습니다. 잠시 후 다시 시도해주세요."); - }, - }); -}; - -export default usePostReports; diff --git a/src/api/score/client/queryKey.ts b/src/api/score/client/queryKey.ts deleted file mode 100644 index c7281e50..00000000 --- a/src/api/score/client/queryKey.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum QueryKeys { - myGpaScore = "myGpaScore", - myLanguageTestScore = "myLanguageTestScore", -} diff --git a/src/api/score/client/useGetMyGpaScore.ts b/src/api/score/client/useGetMyGpaScore.ts deleted file mode 100644 index 63d22266..00000000 --- a/src/api/score/client/useGetMyGpaScore.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { GpaScore } from "@/types/score"; - -import { useQuery } from "@tanstack/react-query"; - -interface UseMyGpaScoreResponse { - gpaScoreStatusResponseList: GpaScore[]; -} - -export const getMyGpaScore = (): Promise> => axiosInstance.get("/scores/gpas"); - -const useGetMyGpaScore = () => { - return useQuery({ - queryKey: [QueryKeys.myGpaScore], - queryFn: getMyGpaScore, - staleTime: Infinity, // 5분간 캐시 - select: (data) => data.data.gpaScoreStatusResponseList, - }); -}; - -export default useGetMyGpaScore; diff --git a/src/api/score/client/useGetMyLanguageTestScore.ts b/src/api/score/client/useGetMyLanguageTestScore.ts deleted file mode 100644 index b44cd762..00000000 --- a/src/api/score/client/useGetMyLanguageTestScore.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { LanguageTestScore } from "@/types/score"; - -import { useQuery } from "@tanstack/react-query"; - -interface UseGetMyLanguageTestScoreResponse { - languageTestScoreStatusResponseList: LanguageTestScore[]; -} - -export const getMyLanguageTestScore = (): Promise> => - axiosInstance.get("/scores/language-tests"); - -const useGetMyLanguageTestScore = () => { - return useQuery({ - queryKey: [QueryKeys.myLanguageTestScore], - queryFn: getMyLanguageTestScore, - staleTime: Infinity, - select: (data) => data.data.languageTestScoreStatusResponseList, - }); -}; - -export default useGetMyLanguageTestScore; diff --git a/src/api/score/client/usePostGpaScore.ts b/src/api/score/client/usePostGpaScore.ts deleted file mode 100644 index f4852529..00000000 --- a/src/api/score/client/usePostGpaScore.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -interface UsePostGpaScoreRequest { - gpaScoreRequest: { - gpa: number; - gpaCriteria: number; - issueDate: string; // yyyy-MM-dd - }; - file: Blob; -} - -export const postGpaScore = (request: UsePostGpaScoreRequest): Promise> => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "gpaScoreRequest", - new Blob([JSON.stringify(request.gpaScoreRequest)], { type: "application/json" }), - ); - convertedRequest.append("file", request.file); - return axiosInstance.post("/scores/gpas", convertedRequest); -}; - -export const usePostGpaScore = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (request: UsePostGpaScoreRequest) => postGpaScore(request), - - onSuccess: () => { - toast.success("학점 정보가 성공적으로 제출되었습니다."); - queryClient.invalidateQueries({ queryKey: [QueryKeys.myGpaScore] }); - }, - - onError: (error) => { - console.error("학점 제출 중 오류 발생:", error); - toast.error("오류가 발생했습니다. 다시 시도해주세요."); - }, - }); -}; diff --git a/src/api/score/client/usePostLanguageTestScore.ts b/src/api/score/client/usePostLanguageTestScore.ts deleted file mode 100644 index adb992e6..00000000 --- a/src/api/score/client/usePostLanguageTestScore.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useRouter } from "next/navigation"; - -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -// 예시: 성공 후 페이지 이동 -import { QueryKeys } from "./queryKey"; - -import { LanguageTestEnum } from "@/types/score"; - -import { toast } from "@/lib/zustand/useToastStore"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -// QueryKeys가 정의된 경로로 수정해주세요. - -interface UsePostLanguageTestScoreRequest { - languageTestScoreRequest: { - languageTestType: LanguageTestEnum; - languageTestScore: string; - issueDate: string; // yyyy-MM-dd - }; - file: File; -} - -export const postLanguageTestScore = (request: UsePostLanguageTestScoreRequest): Promise> => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "languageTestScoreRequest", - new Blob([JSON.stringify(request.languageTestScoreRequest)], { type: "application/json" }), - ); - convertedRequest.append("file", request.file); - return axiosInstance.post("/scores/language-tests", convertedRequest); -}; -/** - * 공인 어학 점수를 제출(POST)하기 위한 useMutation 훅입니다. - */ -export const usePostLanguageTestScore = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (request: UsePostLanguageTestScoreRequest) => postLanguageTestScore(request), - - onSuccess: () => { - toast.success("어학 성적이 성공적으로 제출되었습니다."); - queryClient.invalidateQueries({ queryKey: [QueryKeys.myLanguageTestScore] }); - }, - - onError: (error) => { - console.error("어학 성적 제출 중 오류 발생:", error); - toast.error("오류가 발생했습니다. 다시 시도해주세요."); - }, - }); -}; diff --git a/src/api/university/client/queryKey.ts b/src/api/university/client/queryKey.ts deleted file mode 100644 index f71a2587..00000000 --- a/src/api/university/client/queryKey.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum QueryKeys { - recommendedUniversity = "recommendedUniversity", - univApplyInfosLike = "univApplyInfosLike", - universitySearchText = "universitySearchText", - universitySearchFilter = "universitySearchFilter", - universityDetail = "universityDetail", -} diff --git a/src/api/university/client/useDeleteUniversityFavorite.ts b/src/api/university/client/useDeleteUniversityFavorite.ts deleted file mode 100644 index 9413afa2..00000000 --- a/src/api/university/client/useDeleteUniversityFavorite.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -const deleteUniversityFavorite = (universityInfoForApplyId: number): Promise => - axiosInstance.delete(`/univ-apply-infos/${universityInfoForApplyId}/like`); - -const useDeleteUniversityFavorite = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteUniversityFavorite, - onSuccess: () => { - // 위시리스트 관련 쿼리를 무효화하여 데이터를 다시 불러옵니다. - queryClient.invalidateQueries({ queryKey: [QueryKeys.univApplyInfosLike] }); - }, - }); -}; - -export default useDeleteUniversityFavorite; diff --git a/src/api/university/client/useGetMyWishUniversity.ts b/src/api/university/client/useGetMyWishUniversity.ts deleted file mode 100644 index 0b2c74cc..00000000 --- a/src/api/university/client/useGetMyWishUniversity.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ListUniversity } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -export const getMyWishUniversity = (): Promise> => - axiosInstance.get("/univ-apply-infos/like"); - -const useGetMyWishUniversity = (enabled: boolean = true) => { - return useQuery({ - queryKey: [QueryKeys.univApplyInfosLike], - queryFn: () => getMyWishUniversity(), - staleTime: 1000 * 60 * 5, - select: (data) => data.data, - enabled, - }); -}; - -export default useGetMyWishUniversity; diff --git a/src/api/university/client/useGetRecommendedUniversity.ts b/src/api/university/client/useGetRecommendedUniversity.ts deleted file mode 100644 index f765129d..00000000 --- a/src/api/university/client/useGetRecommendedUniversity.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ListUniversity } from "@/types/university"; - -import useAuthStore from "@/lib/zustand/useAuthStore"; -import { useQuery } from "@tanstack/react-query"; - -type UseGetRecommendedUniversityResponse = { recommendedUniversities: ListUniversity[] }; -type UseGetRecommendedUniversityRequest = { - isLogin: boolean; -}; - -const getRecommendedUniversity = async (isLogin: boolean): Promise => { - const endpoint = "/univ-apply-infos/recommend"; - - const accessToken = useAuthStore.getState().accessToken; - const instance = isLogin && accessToken ? axiosInstance : publicAxiosInstance; - const res = await instance.get(endpoint); - return res.data; -}; - -const useGetRecommendedUniversity = ({ isLogin }: UseGetRecommendedUniversityRequest) => - useQuery({ - queryKey: [QueryKeys.recommendedUniversity, isLogin], - queryFn: () => getRecommendedUniversity(isLogin), - staleTime: 1000 * 60 * 5, - select: (data) => data.recommendedUniversities, // 필요한 데이터만 반환 - }); - -export default useGetRecommendedUniversity; diff --git a/src/api/university/client/useGetUniversityDetail.ts b/src/api/university/client/useGetUniversityDetail.ts deleted file mode 100644 index 57faab21..00000000 --- a/src/api/university/client/useGetUniversityDetail.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { University } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -/** - * @description 대학 상세 조회 API 함수 (공개) - * @param universityInfoForApplyId - 대학 ID - * @returns Promise - */ -const getUniversityDetail = async (universityInfoForApplyId: number): Promise => { - const response: AxiosResponse = await publicAxiosInstance.get( - `/univ-apply-infos/${universityInfoForApplyId}`, - ); - return response.data; -}; - -/** - * @description 대학 상세 조회를 위한 useQuery 커스텀 훅 - */ -const useGetUniversityDetail = (universityInfoForApplyId: number) => { - return useQuery({ - queryKey: [QueryKeys.universityDetail, universityInfoForApplyId], - queryFn: () => getUniversityDetail(universityInfoForApplyId), - enabled: !!universityInfoForApplyId, - }); -}; - -export default useGetUniversityDetail; diff --git a/src/api/university/client/useGetUniversitySearchByFilter.ts b/src/api/university/client/useGetUniversitySearchByFilter.ts deleted file mode 100644 index 67dbfbb1..00000000 --- a/src/api/university/client/useGetUniversitySearchByFilter.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -// --- 타입 정의 --- -export interface UniversitySearchFilterParams { - languageTestType?: LanguageTestType; - testScore?: number; - countryCode?: CountryCode[]; -} - -interface UniversitySearchFilterResponse { - univApplyInfoPreviews: ListUniversity[]; -} - -// --- API 호출 함수 --- -const getUniversitySearchByFilter = async ( - filters: UniversitySearchFilterParams, -): Promise => { - const params = new URLSearchParams(); - - if (filters.languageTestType) { - params.append("languageTestType", filters.languageTestType); - } - if (filters.testScore !== undefined) { - params.append("testScore", String(filters.testScore)); - } - if (filters.countryCode) { - filters.countryCode.forEach((code) => { - params.append("countryCode", code); - }); - } - - const response: AxiosResponse = await publicAxiosInstance.get( - `/univ-apply-infos/search/filter?${params.toString()}`, - ); - return response.data; -}; - -// --- 커스텀 훅 --- -const useGetUniversitySearchByFilter = (filters: UniversitySearchFilterParams) => { - return useQuery({ - queryKey: [QueryKeys.universitySearchFilter, filters], - queryFn: () => getUniversitySearchByFilter(filters), - enabled: Object.values(filters).some((value) => { - if (Array.isArray(value)) return value.length > 0; - return value !== undefined && value !== ""; - }), - select: (data) => data.univApplyInfoPreviews, - }); -}; - -export default useGetUniversitySearchByFilter; diff --git a/src/api/university/client/useGetUniversitySearchByText.ts b/src/api/university/client/useGetUniversitySearchByText.ts deleted file mode 100644 index f63c0579..00000000 --- a/src/api/university/client/useGetUniversitySearchByText.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useMemo } from "react"; - -import { AxiosResponse } from "axios"; - -import { publicAxiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ListUniversity } from "@/types/university"; - -import { useQuery } from "@tanstack/react-query"; - -// --- 타입 정의 --- -interface UniversitySearchTextResponse { - univApplyInfoPreviews: ListUniversity[]; -} - -// --- API 호출 함수 --- -const getAllUniversitiesApi = async (): Promise => { - const response: AxiosResponse = await publicAxiosInstance.get( - "/univ-apply-infos/search/text", - { - params: { value: "" }, // 항상 빈 값으로 호출 - }, - ); - return response.data; -}; - -const useUniversitySearch = (searchValue: string) => { - // 1. 모든 대학 데이터를 한 번만 가져와 'Infinity' 캐시로 저장합니다. - const { - data: allUniversities, // 모든 대학 목록 - isLoading, - isError, - error, - } = useQuery({ - queryKey: [QueryKeys.universitySearchText], // "모든 대학"을 위한 고유 키 - queryFn: getAllUniversitiesApi, - staleTime: Infinity, // 한번 가져오면 절대 다시 요청하지 않음 - gcTime: Infinity, // 캐시가 절대 삭제되지 않음 (선택 사항) - select: (data) => data.univApplyInfoPreviews, - }); - - // 2. 검색어가 변경될 때만 캐시된 데이터를 필터링합니다. - const filteredUniversities = useMemo(() => { - const normalizedSearchValue = searchValue.trim().toLowerCase(); - - if (!normalizedSearchValue) { - return allUniversities; // 검색어가 없으면 전체 목록 반환 - } - - // allUniversities가 아직 로드되지 않았으면 빈 배열 반환 - if (!allUniversities) { - return []; - } - - // 대학 이름(koreanName)에 검색어가 포함되어 있는지 확인하여 필터링 - return allUniversities.filter((university) => university.koreanName.toLowerCase().includes(normalizedSearchValue)); - }, [allUniversities, searchValue]); // allUniversities나 searchValue가 바뀔 때만 재계산 - - return { - data: filteredUniversities, // 필터링된 결과 - isLoading, // 초기 데이터 로딩 상태 - isError, - error, - totalCount: allUniversities?.length || 0, // 전체 대학 수 (필요시 사용) - }; -}; -export default useUniversitySearch; diff --git a/src/api/university/client/usePostUniversityFavorite.ts b/src/api/university/client/usePostUniversityFavorite.ts deleted file mode 100644 index 106cbb10..00000000 --- a/src/api/university/client/usePostUniversityFavorite.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { AxiosResponse } from "axios"; - -import { axiosInstance } from "@/utils/axiosInstance"; -import { createMutationErrorHandler } from "@/utils/errorHandler"; - -import { QueryKeys } from "./queryKey"; - -import { useMutation, useQueryClient } from "@tanstack/react-query"; - -/** - * @description 위시리스트 학교 추가 API 응답 타입 - * @property {number} universityInfoForApplyId - 추가된 학교 정보 ID - * @property {string} message - 성공 메시지 - */ -interface UniversityFavoriteResponse { - universityInfoForApplyId: number; - message: string; -} - -/** - * @description 위시리스트에 학교를 추가하는 API 함수 - * @param universityInfoForApplyId - 추가할 학교 정보의 ID - * @returns Promise> - */ -export const postUniversityFavoriteApi = ( - universityInfoForApplyId: number, -): Promise> => - axiosInstance.post(`/univ-apply-infos/${universityInfoForApplyId}/like`); - -/** - * @description 위시리스트 학교 추가를 위한 useMutation 커스텀 훅 - */ -const usePostUniversityFavorite = () => { - const queryClient = useQueryClient(); - - return useMutation({ - // mutation 실행 시 호출될 함수 - mutationFn: postUniversityFavoriteApi, - - // mutation 성공 시 실행될 콜백 - onSuccess: () => { - // 위시리스트 관련 쿼리를 무효화하여 데이터를 다시 불러옵니다. - queryClient.invalidateQueries({ queryKey: [QueryKeys.univApplyInfosLike] }); - }, - - // mutation 실패 시 실행될 콜백 - onError: createMutationErrorHandler("위시리스트 추가에 실패했습니다."), - }); -}; - -export default usePostUniversityFavorite; diff --git a/src/apis/Admin/api.ts b/src/apis/Admin/api.ts new file mode 100644 index 00000000..74b67368 --- /dev/null +++ b/src/apis/Admin/api.ts @@ -0,0 +1,176 @@ +import { axiosInstance } from "@/utils/axiosInstance"; + +export interface VerifyLanguageTestResponse { + id: number; + languageTestType: string; + languageTestScore: string; + verifyStatus: string; + rejectedReason: null; +} + +export type VerifyLanguageTestRequest = Record; + +export interface LanguageTestListResponseSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface LanguageTestListResponsePageable { + pageNumber: number; + pageSize: number; + sort: LanguageTestListResponsePageableSort; + offset: number; + paged: boolean; + unpaged: boolean; +} + +export interface LanguageTestListResponsePageableSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface LanguageTestListResponseContentItem { + languageTestScoreStatusResponse: LanguageTestListResponseContentItemLanguageTestScoreStatusResponse; + siteUserResponse: LanguageTestListResponseContentItemSiteUserResponse; +} + +export interface LanguageTestListResponseContentItemSiteUserResponse { + id: number; + nickname: string; + profileImageUrl: string; +} + +export interface LanguageTestListResponseContentItemLanguageTestScoreStatusResponse { + id: number; + languageTestResponse: LanguageTestListResponseContentItemLanguageTestScoreStatusResponseLanguageTestResponse; + verifyStatus: string; + rejectedReason: null; + createdAt: string; + updatedAt: string; +} + +export interface LanguageTestListResponseContentItemLanguageTestScoreStatusResponseLanguageTestResponse { + languageTestType: string; + languageTestScore: string; + languageTestReportUrl: string; +} + +export interface LanguageTestListResponse { + content: LanguageTestListResponseContentItem[]; + pageable: LanguageTestListResponsePageable; + totalElements: number; + totalPages: number; + last: boolean; + size: number; + number: number; + sort: LanguageTestListResponseSort; + numberOfElements: number; + first: boolean; + empty: boolean; +} + +export interface VerifyGpaResponse { + id: number; + gpa: number; + gpaCriteria: number; + verifyStatus: string; + rejectedReason: null; +} + +export type VerifyGpaRequest = Record; + +export interface GpaListResponseSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface GpaListResponsePageable { + pageNumber: number; + pageSize: number; + sort: GpaListResponsePageableSort; + offset: number; + paged: boolean; + unpaged: boolean; +} + +export interface GpaListResponsePageableSort { + sorted: boolean; + unsorted: boolean; + empty: boolean; +} + +export interface GpaListResponseContentItem { + gpaScoreStatusResponse: GpaListResponseContentItemGpaScoreStatusResponse; + siteUserResponse: GpaListResponseContentItemSiteUserResponse; +} + +export interface GpaListResponseContentItemSiteUserResponse { + id: number; + nickname: string; + profileImageUrl: string; +} + +export interface GpaListResponseContentItemGpaScoreStatusResponse { + id: number; + gpaResponse: GpaListResponseContentItemGpaScoreStatusResponseGpaResponse; + verifyStatus: string; + rejectedReason: null; + createdAt: string; + updatedAt: string; +} + +export interface GpaListResponseContentItemGpaScoreStatusResponseGpaResponse { + gpa: number; + gpaCriteria: number; + gpaReportUrl: string; +} + +export interface GpaListResponse { + content: GpaListResponseContentItem[]; + pageable: GpaListResponsePageable; + totalElements: number; + totalPages: number; + last: boolean; + size: number; + number: number; + sort: GpaListResponseSort; + numberOfElements: number; + first: boolean; + empty: boolean; +} + +export const adminApi = { + putVerifyLanguageTest: async (params: { + languageTestScoreId: string | number; + data?: VerifyLanguageTestRequest; + }): Promise => { + const res = await axiosInstance.put( + `/admin/scores/language-tests/${params.languageTestScoreId}`, + params?.data, + ); + return res.data; + }, + + getLanguageTestList: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/admin/scores/language-tests?page=1&size=10`, { + params: params?.params, + }); + return res.data; + }, + + putVerifyGpa: async (params: { + gpaScoreId: string | number; + data?: VerifyGpaRequest; + }): Promise => { + const res = await axiosInstance.put(`/admin/scores/gpas/${params.gpaScoreId}`, params?.data); + return res.data; + }, + + getGpaList: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/admin/scores/gpas`, { params: params?.params }); + return res.data; + }, +}; diff --git a/src/apis/Admin/getGpaList.ts b/src/apis/Admin/getGpaList.ts new file mode 100644 index 00000000..cacb3db3 --- /dev/null +++ b/src/apis/Admin/getGpaList.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { GpaListResponse, adminApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetGpaList = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.Admin.gpaList, params], + queryFn: () => adminApi.getGpaList(params ? { params } : {}), + }); +}; + +export default useGetGpaList; diff --git a/src/apis/Admin/getLanguageTestList.ts b/src/apis/Admin/getLanguageTestList.ts new file mode 100644 index 00000000..e30603a4 --- /dev/null +++ b/src/apis/Admin/getLanguageTestList.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { LanguageTestListResponse, adminApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetLanguageTestList = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.Admin.languageTestList, params], + queryFn: () => adminApi.getLanguageTestList(params ? { params } : {}), + }); +}; + +export default useGetLanguageTestList; diff --git a/src/apis/Admin/index.ts b/src/apis/Admin/index.ts new file mode 100644 index 00000000..f7d22c2a --- /dev/null +++ b/src/apis/Admin/index.ts @@ -0,0 +1,5 @@ +export { adminApi } from "./api"; +export { default as getGpaList } from "./getGpaList"; +export { default as getLanguageTestList } from "./getLanguageTestList"; +export { default as putVerifyGpa } from "./putVerifyGpa"; +export { default as putVerifyLanguageTest } from "./putVerifyLanguageTest"; diff --git a/src/apis/Admin/putVerifyGpa.ts b/src/apis/Admin/putVerifyGpa.ts new file mode 100644 index 00000000..4bd564f4 --- /dev/null +++ b/src/apis/Admin/putVerifyGpa.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { VerifyGpaRequest, VerifyGpaResponse, adminApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePutVerifyGpa = () => { + return useMutation({ + mutationFn: (variables) => adminApi.putVerifyGpa(variables), + }); +}; + +export default usePutVerifyGpa; diff --git a/src/apis/Admin/putVerifyLanguageTest.ts b/src/apis/Admin/putVerifyLanguageTest.ts new file mode 100644 index 00000000..f83b3e6b --- /dev/null +++ b/src/apis/Admin/putVerifyLanguageTest.ts @@ -0,0 +1,17 @@ +import { AxiosError } from "axios"; + +import { VerifyLanguageTestRequest, VerifyLanguageTestResponse, adminApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePutVerifyLanguageTest = () => { + return useMutation< + VerifyLanguageTestResponse, + AxiosError, + { languageTestScoreId: string | number; data: VerifyLanguageTestRequest } + >({ + mutationFn: (variables) => adminApi.putVerifyLanguageTest(variables), + }); +}; + +export default usePutVerifyLanguageTest; diff --git a/src/apis/Auth/api.ts b/src/apis/Auth/api.ts new file mode 100644 index 00000000..1dadaf97 --- /dev/null +++ b/src/apis/Auth/api.ts @@ -0,0 +1,145 @@ +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +export type SignOutResponse = Record; + +export type SignOutRequest = Record; + +// Apple Auth Types +export interface RegisteredAppleAuthResponse { + isRegistered: true; + accessToken: string; + refreshToken: string; +} + +export interface UnregisteredAppleAuthResponse { + isRegistered: false; + nickname: null; + email: string; + profileImageUrl: null; + signUpToken: string; +} + +export type AppleAuthResponse = RegisteredAppleAuthResponse | UnregisteredAppleAuthResponse; + +export interface AppleAuthRequest { + code: string; +} + +export interface RefreshTokenResponse { + accessToken: string; +} + +export type RefreshTokenRequest = Record; + +export interface EmailLoginResponse { + accessToken: string; + refreshToken: string; +} + +export interface EmailLoginRequest { + email: string; + password: string; +} + +export interface EmailVerificationResponse { + signUpToken: string; +} + +export interface EmailVerificationRequest { + email: string; + verificationCode: string; +} + +// Kakao Auth Types +export interface RegisteredKakaoAuthResponse { + isRegistered: true; + accessToken: string; + refreshToken: string; +} + +export interface UnregisteredKakaoAuthResponse { + isRegistered: false; + nickname: string; + email: string; + profileImageUrl: string; + signUpToken: string; +} + +export type KakaoAuthResponse = RegisteredKakaoAuthResponse | UnregisteredKakaoAuthResponse; + +export interface KakaoAuthRequest { + code: string; +} + +export type AccountResponse = void; + +export interface SignUpResponse { + accessToken: string; + refreshToken: string; +} + +export interface SignUpRequest { + signUpToken: string; + nickname: string; + profileImageUrl: string; + preparationStatus: string; + interestedRegions: string[]; + interestedCountries: string[]; +} + +export interface EmailSignUpRequest { + email: string; + password: string; +} + +export interface EmailSignUpResponse { + signUpToken: string; +} + +export const authApi = { + postSignOut: async (): Promise => { + const res = await axiosInstance.post(`/auth/sign-out`); + return res.data; + }, + + postAppleAuth: async (data: AppleAuthRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/apple`, data); + return res.data; + }, + + postRefreshToken: async (): Promise => { + const res = await publicAxiosInstance.post(`/auth/reissue`); + return res.data; + }, + + postEmailLogin: async (data: EmailLoginRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/email/sign-in`, data); + return res.data; + }, + + postEmailSignUp: async (data: EmailSignUpRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/email/sign-up`, data); + return res.data; + }, + + postKakaoAuth: async (data: KakaoAuthRequest): Promise => { + const res = await publicAxiosInstance.post(`/auth/kakao`, data); + return res.data; + }, + + deleteAccount: async (): Promise => { + const res = await axiosInstance.delete(`/auth/quit`); + return res.data; + }, + + postSignUp: async (data: SignUpRequest): Promise => { + // 임시 성별, 생년월일 추가. API 변경 시 삭제 + const payload = { + ...data, + birth: "2000-01-01", + gender: "PREFER_NOT_TO_SAY", + }; + const res = await publicAxiosInstance.post(`/auth/sign-up`, payload); + return res.data; + }, +}; diff --git a/src/api/auth/client/useDeleteUserAccount.ts b/src/apis/Auth/deleteAccount.ts similarity index 56% rename from src/api/auth/client/useDeleteUserAccount.ts rename to src/apis/Auth/deleteAccount.ts index 0d0fa96b..b9274190 100644 --- a/src/api/auth/client/useDeleteUserAccount.ts +++ b/src/apis/Auth/deleteAccount.ts @@ -1,24 +1,25 @@ import { useRouter } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; +import { AccountResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -export const deleteUserAccount = (): Promise> => axiosInstance.delete("/auth/quit"); - +/** + * @description 회원탈퇴를 위한 useMutation 커스텀 훅 + */ const useDeleteUserAccount = () => { const router = useRouter(); const { clearAccessToken } = useAuthStore(); - const queryClient = useQueryClient(); // 쿼리 캐시 관리를 위해 클라이언트 인스턴스를 가져옵니다. + const queryClient = useQueryClient(); - return useMutation({ - mutationFn: deleteUserAccount, + return useMutation({ + mutationFn: () => authApi.deleteAccount(), onMutate: () => { - // 낙관적 업데이트: 로그아웃 요청이 시작되면 바로 로그인 상태를 false로 변경합니다. + // 낙관적 업데이트: 요청이 시작되면 바로 홈으로 이동 router.replace("/"); }, onSuccess: () => { @@ -26,7 +27,7 @@ const useDeleteUserAccount = () => { clearAccessToken(); queryClient.clear(); }, - onError: (error) => { + onError: () => { toast.error("회원탈퇴에 실패했습니다. 잠시 후 다시 시도해주세요."); }, }); diff --git a/src/apis/Auth/index.ts b/src/apis/Auth/index.ts new file mode 100644 index 00000000..da505d67 --- /dev/null +++ b/src/apis/Auth/index.ts @@ -0,0 +1,25 @@ +export { authApi } from "./api"; +export type { + KakaoAuthRequest, + KakaoAuthResponse, + AppleAuthRequest, + AppleAuthResponse, + EmailLoginRequest, + EmailLoginResponse, + SignUpRequest, + SignUpResponse, + EmailSignUpRequest, + EmailSignUpResponse, +} from "./api"; + +// Client-side hooks +export { default as useDeleteUserAccount } from "./deleteAccount"; +export { default as usePostAppleAuth } from "./postAppleAuth"; +export { default as usePostEmailAuth } from "./postEmailLogin"; +export { default as usePostEmailSignUp } from "./postEmailVerification"; +export { default as usePostKakaoAuth } from "./postKakaoAuth"; +export { default as usePostLogout } from "./postSignOut"; +export { default as usePostSignUp } from "./postSignUp"; + +// Server-side functions +export { postReissueToken } from "./server"; diff --git a/src/api/auth/client/usePostAppleAuth.ts b/src/apis/Auth/postAppleAuth.ts similarity index 62% rename from src/api/auth/client/usePostAppleAuth.ts rename to src/apis/Auth/postAppleAuth.ts index 87eb5e06..2ef03e0a 100644 --- a/src/api/auth/client/usePostAppleAuth.ts +++ b/src/apis/Auth/postAppleAuth.ts @@ -1,47 +1,25 @@ import { useRouter, useSearchParams } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; import { validateSafeRedirect } from "@/utils/authUtils"; -import { publicAxiosInstance } from "@/utils/axiosInstance"; + +import { AppleAuthRequest, AppleAuthResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation } from "@tanstack/react-query"; -// Apple -export interface RegisteredAppleAuthResponse { - isRegistered: true; - accessToken: string; - refreshToken: string; -} - -export interface UnregisteredAppleAuthResponse { - isRegistered: false; - nickname: null; - email: string; - profileImageUrl: null; - signUpToken: string; -} - -interface AppleAuthRequest { - code: string; -} - -type AppleAuthResponse = RegisteredAppleAuthResponse | UnregisteredAppleAuthResponse; - -const postAppleAuth = ({ code }: AppleAuthRequest): Promise> => - publicAxiosInstance.post("/auth/apple", { code }); - +/** + * @description 애플 로그인을 위한 useMutation 커스텀 훅 + */ const usePostAppleAuth = () => { const router = useRouter(); const searchParams = useSearchParams(); - return useMutation({ - mutationFn: postAppleAuth, - onSuccess: (response) => { - const { data } = response; - + return useMutation({ + mutationFn: (data) => authApi.postAppleAuth(data), + onSuccess: (data) => { if (data.isRegistered) { // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 diff --git a/src/api/auth/client/usePostEmailAuth.ts b/src/apis/Auth/postEmailLogin.ts similarity index 64% rename from src/api/auth/client/usePostEmailAuth.ts rename to src/apis/Auth/postEmailLogin.ts index 43470491..af5d54ce 100644 --- a/src/api/auth/client/usePostEmailAuth.ts +++ b/src/apis/Auth/postEmailLogin.ts @@ -1,36 +1,27 @@ import { useRouter, useSearchParams } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; import { validateSafeRedirect } from "@/utils/authUtils"; -import { publicAxiosInstance } from "@/utils/axiosInstance"; + +import { EmailLoginRequest, EmailLoginResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation } from "@tanstack/react-query"; -interface UsePostEmailSignInResponse { - accessToken: string; - refreshToken: string; -} - -interface LoginRequest { - email: string; - password: string; -} - -const postEmailAuth = ({ email, password }: LoginRequest): Promise> => - publicAxiosInstance.post("/auth/email/sign-in", { email, password }); - +/** + * @description 이메일 로그인을 위한 useMutation 커스텀 훅 + */ const usePostEmailAuth = () => { const { setAccessToken } = useAuthStore(); const searchParams = useSearchParams(); const router = useRouter(); - return useMutation({ - mutationFn: postEmailAuth, + return useMutation({ + mutationFn: (data) => authApi.postEmailLogin(data), onSuccess: (data) => { - const { accessToken } = data.data; + const { accessToken } = data; // Zustand persist가 자동으로 localStorage에 저장 // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 @@ -45,4 +36,5 @@ const usePostEmailAuth = () => { }, }); }; + export default usePostEmailAuth; diff --git a/src/apis/Auth/postEmailVerification.ts b/src/apis/Auth/postEmailVerification.ts new file mode 100644 index 00000000..c94bc09d --- /dev/null +++ b/src/apis/Auth/postEmailVerification.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { EmailSignUpRequest, EmailSignUpResponse, authApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 이메일 회원가입을 위한 useMutation 커스텀 훅 + */ +const usePostEmailSignUp = () => { + return useMutation({ + mutationFn: (data) => authApi.postEmailSignUp(data), + onError: (error) => { + console.error("이메일 회원가입 실패:", error); + toast.error("회원가입에 실패했습니다."); + }, + }); +}; + +export default usePostEmailSignUp; diff --git a/src/api/auth/client/usePostKakaoAuth.ts b/src/apis/Auth/postKakaoAuth.ts similarity index 64% rename from src/api/auth/client/usePostKakaoAuth.ts rename to src/apis/Auth/postKakaoAuth.ts index 560dd2de..9179848b 100644 --- a/src/api/auth/client/usePostKakaoAuth.ts +++ b/src/apis/Auth/postKakaoAuth.ts @@ -1,48 +1,26 @@ import { useRouter, useSearchParams } from "next/navigation"; -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; import { validateSafeRedirect } from "@/utils/authUtils"; -import { publicAxiosInstance } from "@/utils/axiosInstance"; + +import { KakaoAuthRequest, KakaoAuthResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation } from "@tanstack/react-query"; -// Kakao -interface RegisteredKakaoAuthReponse { - isRegistered: true; - accessToken: string; - refreshToken: string; -} - -interface UnregisteredKakaoAuthReponse { - isRegistered: false; - nickname: string; - email: string; - profileImageUrl: string; - signUpToken: string; -} - -interface KakaoAuthRequest { - code: string; -} - -const postKakaoAuth = ({ - code, -}: KakaoAuthRequest): Promise> => - publicAxiosInstance.post("/auth/kakao", { code }); - +/** + * @description 카카오 로그인을 위한 useMutation 커스텀 훅 + */ const usePostKakaoAuth = () => { const { setAccessToken } = useAuthStore(); const router = useRouter(); const searchParams = useSearchParams(); - return useMutation({ - mutationFn: postKakaoAuth, - onSuccess: (response) => { - const { data } = response; - + return useMutation({ + mutationFn: (data) => authApi.postKakaoAuth(data), + onSuccess: (data) => { if (data.isRegistered) { // 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장 // refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨 diff --git a/src/apis/Auth/postRefreshToken.ts b/src/apis/Auth/postRefreshToken.ts new file mode 100644 index 00000000..95140426 --- /dev/null +++ b/src/apis/Auth/postRefreshToken.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { RefreshTokenRequest, RefreshTokenResponse, authApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostRefreshToken = () => { + return useMutation({ + mutationFn: () => authApi.postRefreshToken(), + }); +}; + +export default usePostRefreshToken; diff --git a/src/api/auth/client/usePostLogout.ts b/src/apis/Auth/postSignOut.ts similarity index 56% rename from src/api/auth/client/usePostLogout.ts rename to src/apis/Auth/postSignOut.ts index ccfba24e..1c477418 100644 --- a/src/api/auth/client/usePostLogout.ts +++ b/src/apis/Auth/postSignOut.ts @@ -1,18 +1,19 @@ -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; +import { SignOutResponse, authApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -export const postLogout = (): Promise> => axiosInstance.post("/auth/sign-out"); - +/** + * @description 로그아웃을 위한 useMutation 커스텀 훅 + */ const usePostLogout = () => { const { clearAccessToken } = useAuthStore(); - const queryClient = useQueryClient(); // 쿼리 캐시 관리를 위해 클라이언트 인스턴스를 가져옵니다. + const queryClient = useQueryClient(); - return useMutation({ - mutationFn: postLogout, + return useMutation({ + mutationFn: () => authApi.postSignOut(), onSuccess: () => { // Zustand persist가 자동으로 localStorage에서 제거 clearAccessToken(); diff --git a/src/apis/Auth/postSignUp.ts b/src/apis/Auth/postSignUp.ts new file mode 100644 index 00000000..cbfe6aae --- /dev/null +++ b/src/apis/Auth/postSignUp.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { SignUpRequest, SignUpResponse, authApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 회원가입을 위한 useMutation 커스텀 훅 + */ +const usePostSignUp = () => { + return useMutation({ + mutationFn: (data) => authApi.postSignUp(data), + onError: (error) => { + console.error("회원가입 실패:", error); + toast.error("회원가입에 실패했습니다."); + }, + }); +}; + +export default usePostSignUp; diff --git a/src/apis/Auth/server/index.ts b/src/apis/Auth/server/index.ts new file mode 100644 index 00000000..09d60f03 --- /dev/null +++ b/src/apis/Auth/server/index.ts @@ -0,0 +1 @@ +export { default as postReissueToken } from "./postReissueToken"; diff --git a/src/api/auth/server/postReissueToken.ts b/src/apis/Auth/server/postReissueToken.ts similarity index 87% rename from src/api/auth/server/postReissueToken.ts rename to src/apis/Auth/server/postReissueToken.ts index 3bc130d2..51c60ed1 100644 --- a/src/api/auth/server/postReissueToken.ts +++ b/src/apis/Auth/server/postReissueToken.ts @@ -2,6 +2,10 @@ import { publicAxiosInstance } from "@/utils/axiosInstance"; import useAuthStore from "@/lib/zustand/useAuthStore"; +/** + * @description 토큰 재발급 서버사이드 함수 + * axiosInstance의 interceptor에서 사용됨 + */ const postReissueToken = async (): Promise => { try { const response = await publicAxiosInstance.post<{ accessToken: string }>("/auth/reissue"); @@ -21,4 +25,5 @@ const postReissueToken = async (): Promise => { throw error; } }; + export default postReissueToken; diff --git a/src/apis/MyPage/api.ts b/src/apis/MyPage/api.ts new file mode 100644 index 00000000..2e3cb6bd --- /dev/null +++ b/src/apis/MyPage/api.ts @@ -0,0 +1,70 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { UserRole } from "@/types/mentor"; +import { BaseUserInfo } from "@/types/myInfo"; + +// --- 타입 정의 --- +export interface MenteeInfo extends BaseUserInfo { + role: UserRole.MENTEE; + interestedCountries: string[]; +} + +export interface MentorInfo extends BaseUserInfo { + role: UserRole.MENTOR; + attendedUniversity: string; +} + +export interface AdminInfo extends BaseUserInfo { + role: UserRole.ADMIN; + attendedUniversity: string; +} + +export type MyInfoResponse = MenteeInfo | MentorInfo | AdminInfo; + +export type InterestedRegionCountryResponse = void; + +export type InterestedRegionCountryRequest = string[]; + +export interface ProfilePatchRequest { + nickname?: string; + file?: File; +} + +export interface PasswordPatchRequest { + currentPassword: string; + newPassword: string; + newPasswordConfirmation: string; +} + +export const myPageApi = { + getProfile: async (): Promise => { + const response: AxiosResponse = await axiosInstance.get("/my"); + return response.data; + }, + + patchProfile: async (data: ProfilePatchRequest): Promise => { + const formData = new FormData(); + if (data.nickname) { + formData.append("nickname", data.nickname); + } + if (data.file) { + formData.append("file", data.file); + } + const res = await axiosInstance.patch("/my", formData); + return res.data; + }, + + patchPassword: async (data: PasswordPatchRequest): Promise => { + const res = await axiosInstance.patch("/my/password", data); + return res.data; + }, + + patchInterestedRegionCountry: async ( + data: InterestedRegionCountryRequest, + ): Promise => { + const res = await axiosInstance.patch(`/my/interested-location`, data); + return res.data; + }, +}; diff --git a/src/apis/MyPage/getProfile.ts b/src/apis/MyPage/getProfile.ts new file mode 100644 index 00000000..b25e2092 --- /dev/null +++ b/src/apis/MyPage/getProfile.ts @@ -0,0 +1,39 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { MyInfoResponse, myPageApi } from "./api"; + +import { UseQueryResult, useMutationState, useQuery } from "@tanstack/react-query"; + +type UseGetMyInfoResult = Omit, "data"> & { + data: MyInfoResponse | undefined; +}; + +const useGetMyInfo = (): UseGetMyInfoResult => { + const queryResult = useQuery({ + queryKey: [QueryKeys.MyPage.profile], + queryFn: () => myPageApi.getProfile(), + // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. + staleTime: Infinity, + gcTime: 1000 * 60 * 30, // 예: 30분 + }); + + const pendingMutations = useMutationState({ + filters: { + mutationKey: [QueryKeys.MyPage.profile, "patch"], + status: "pending", + }, + select: (mutation) => { + return mutation.state.variables as Partial; + }, + }); + + const isOptimistic = pendingMutations.length > 0; + const pendingData = isOptimistic ? pendingMutations[0] : null; + + const displayData = isOptimistic && queryResult.data ? { ...queryResult.data, ...pendingData } : queryResult.data; + + return { ...queryResult, data: displayData as MyInfoResponse | undefined }; +}; + +export default useGetMyInfo; diff --git a/src/apis/MyPage/index.ts b/src/apis/MyPage/index.ts new file mode 100644 index 00000000..d25fcb4d --- /dev/null +++ b/src/apis/MyPage/index.ts @@ -0,0 +1,13 @@ +export { + myPageApi, + type MyInfoResponse, + type MenteeInfo, + type MentorInfo, + type AdminInfo, + type ProfilePatchRequest, + type PasswordPatchRequest, +} from "./api"; +export { default as useGetMyInfo } from "./getProfile"; +export { default as usePatchMyInfo } from "./patchProfile"; +export { default as usePatchMyPassword } from "./patchPassword"; +export { default as usePatchInterestedRegionCountry } from "./patchInterestedRegionCountry"; diff --git a/src/apis/MyPage/patchInterestedRegionCountry.ts b/src/apis/MyPage/patchInterestedRegionCountry.ts new file mode 100644 index 00000000..19fdef47 --- /dev/null +++ b/src/apis/MyPage/patchInterestedRegionCountry.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { InterestedRegionCountryRequest, InterestedRegionCountryResponse, myPageApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePatchInterestedRegionCountry = () => { + return useMutation({ + mutationFn: (data) => myPageApi.patchInterestedRegionCountry(data), + }); +}; + +export default usePatchInterestedRegionCountry; diff --git a/src/api/my/client/usePatchMyPassword.ts b/src/apis/MyPage/patchPassword.ts similarity index 53% rename from src/api/my/client/usePatchMyPassword.ts rename to src/apis/MyPage/patchPassword.ts index 05fabd8c..b9c1f96a 100644 --- a/src/api/my/client/usePatchMyPassword.ts +++ b/src/apis/MyPage/patchPassword.ts @@ -2,37 +2,25 @@ import { useRouter } from "next/navigation"; import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; +import { QueryKeys } from "../queryKeys"; +import { PasswordPatchRequest, myPageApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -export interface UseMyMentorProfileRequest { - currentPassword: string; - newPassword: string; - newPasswordConfirmation: string; -} - -const patchMyPassword = async (data: UseMyMentorProfileRequest): Promise => { - const res = await axiosInstance.patch("/my/password", data, {}); - return res.data; -}; - const usePatchMyPassword = () => { const queryClient = useQueryClient(); const router = useRouter(); const { clearAccessToken } = useAuthStore(); - return useMutation, UseMyMentorProfileRequest>({ - mutationKey: [QueryKeys.myInfo, "password", "patch"], - mutationFn: patchMyPassword, + return useMutation, PasswordPatchRequest>({ + mutationKey: [QueryKeys.MyPage.password, "patch"], + mutationFn: (data) => myPageApi.patchPassword(data), onSuccess: () => { clearAccessToken(); queryClient.clear(); - toast.success("프로필이 성공적으로 수정되었습니다."); + toast.success("비밀번호가 성공적으로 변경되었습니다."); router.replace("/"); }, onError: (error) => { diff --git a/src/apis/MyPage/patchProfile.ts b/src/apis/MyPage/patchProfile.ts new file mode 100644 index 00000000..8e8bcb16 --- /dev/null +++ b/src/apis/MyPage/patchProfile.ts @@ -0,0 +1,30 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { ProfilePatchRequest, myPageApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +const usePatchMyInfo = () => { + const queryClient = useQueryClient(); + + return useMutation, ProfilePatchRequest>({ + mutationKey: [QueryKeys.MyPage.profile, "patch"], + mutationFn: (data) => myPageApi.patchProfile(data), + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: [QueryKeys.MyPage.profile], + }); + }, + onSuccess: () => { + toast.success("프로필이 성공적으로 수정되었습니다."); + }, + onError: (error) => { + const errorMessage = error.response?.data?.message; + toast.error(errorMessage || "프로필 수정에 실패했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePatchMyInfo; diff --git a/src/apis/Scores/api.ts b/src/apis/Scores/api.ts new file mode 100644 index 00000000..0ccd1d05 --- /dev/null +++ b/src/apis/Scores/api.ts @@ -0,0 +1,81 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { GpaScore, LanguageTestEnum, LanguageTestScore } from "@/types/score"; + +// ====== Query Keys ====== +export const ScoresQueryKeys = { + myGpaScore: "myGpaScore", + myLanguageTestScore: "myLanguageTestScore", +} as const; + +// ====== Types ====== +export interface UseMyGpaScoreResponse { + gpaScoreStatusResponseList: GpaScore[]; +} + +export interface UseGetMyLanguageTestScoreResponse { + languageTestScoreStatusResponseList: LanguageTestScore[]; +} + +export interface UsePostGpaScoreRequest { + gpaScoreRequest: { + gpa: number; + gpaCriteria: number; + issueDate: string; // yyyy-MM-dd + }; + file: Blob; +} + +export interface UsePostLanguageTestScoreRequest { + languageTestScoreRequest: { + languageTestType: LanguageTestEnum; + languageTestScore: string; + issueDate: string; // yyyy-MM-dd + }; + file: File; +} + +// ====== API Functions ====== +export const scoresApi = { + /** + * 내 학점 점수 조회 + */ + getMyGpaScore: async (): Promise> => { + return axiosInstance.get("/scores/gpas"); + }, + + /** + * 내 어학 점수 조회 + */ + getMyLanguageTestScore: async (): Promise> => { + return axiosInstance.get("/scores/language-tests"); + }, + + /** + * 학점 점수 제출 + */ + postGpaScore: async (request: UsePostGpaScoreRequest): Promise> => { + const formData = new FormData(); + formData.append( + "gpaScoreRequest", + new Blob([JSON.stringify(request.gpaScoreRequest)], { type: "application/json" }), + ); + formData.append("file", request.file); + return axiosInstance.post("/scores/gpas", formData); + }, + + /** + * 어학 점수 제출 + */ + postLanguageTestScore: async (request: UsePostLanguageTestScoreRequest): Promise> => { + const formData = new FormData(); + formData.append( + "languageTestScoreRequest", + new Blob([JSON.stringify(request.languageTestScoreRequest)], { type: "application/json" }), + ); + formData.append("file", request.file); + return axiosInstance.post("/scores/language-tests", formData); + }, +}; diff --git a/src/apis/Scores/getGpaList.ts b/src/apis/Scores/getGpaList.ts new file mode 100644 index 00000000..a2344ba2 --- /dev/null +++ b/src/apis/Scores/getGpaList.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, scoresApi } from "./api"; + +import { GpaScore } from "@/types/score"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 내 학점 점수 조회 훅 + */ +const useGetMyGpaScore = () => { + return useQuery({ + queryKey: [ScoresQueryKeys.myGpaScore], + queryFn: scoresApi.getMyGpaScore, + staleTime: Infinity, + select: (data) => data.data.gpaScoreStatusResponseList, + }); +}; + +export default useGetMyGpaScore; diff --git a/src/apis/Scores/getLanguageTestList.ts b/src/apis/Scores/getLanguageTestList.ts new file mode 100644 index 00000000..642ec2b2 --- /dev/null +++ b/src/apis/Scores/getLanguageTestList.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, scoresApi } from "./api"; + +import { LanguageTestScore } from "@/types/score"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 내 어학 점수 조회 훅 + */ +const useGetMyLanguageTestScore = () => { + return useQuery({ + queryKey: [ScoresQueryKeys.myLanguageTestScore], + queryFn: scoresApi.getMyLanguageTestScore, + staleTime: Infinity, + select: (data) => data.data.languageTestScoreStatusResponseList, + }); +}; + +export default useGetMyLanguageTestScore; diff --git a/src/apis/Scores/index.ts b/src/apis/Scores/index.ts new file mode 100644 index 00000000..82bc201a --- /dev/null +++ b/src/apis/Scores/index.ts @@ -0,0 +1,12 @@ +export { scoresApi, ScoresQueryKeys } from "./api"; +export type { + UseMyGpaScoreResponse, + UseGetMyLanguageTestScoreResponse, + UsePostGpaScoreRequest, + UsePostLanguageTestScoreRequest, +} from "./api"; + +export { default as useGetMyGpaScore } from "./getGpaList"; +export { default as useGetMyLanguageTestScore } from "./getLanguageTestList"; +export { default as usePostGpaScore } from "./postCreateGpa"; +export { default as usePostLanguageTestScore } from "./postCreateLanguageTest"; diff --git a/src/apis/Scores/postCreateGpa.ts b/src/apis/Scores/postCreateGpa.ts new file mode 100644 index 00000000..a3252781 --- /dev/null +++ b/src/apis/Scores/postCreateGpa.ts @@ -0,0 +1,29 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, UsePostGpaScoreRequest, scoresApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 학점 점수 제출 훅 + */ +export const usePostGpaScore = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (request: UsePostGpaScoreRequest) => scoresApi.postGpaScore(request), + + onSuccess: () => { + toast.success("학점 정보가 성공적으로 제출되었습니다."); + queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myGpaScore] }); + }, + + onError: (error) => { + console.error("학점 제출 중 오류 발생:", error); + toast.error("오류가 발생했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostGpaScore; diff --git a/src/apis/Scores/postCreateLanguageTest.ts b/src/apis/Scores/postCreateLanguageTest.ts new file mode 100644 index 00000000..bc8527ff --- /dev/null +++ b/src/apis/Scores/postCreateLanguageTest.ts @@ -0,0 +1,29 @@ +import { AxiosError } from "axios"; + +import { ScoresQueryKeys, UsePostLanguageTestScoreRequest, scoresApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 어학 점수 제출 훅 + */ +export const usePostLanguageTestScore = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (request: UsePostLanguageTestScoreRequest) => scoresApi.postLanguageTestScore(request), + + onSuccess: () => { + toast.success("어학 성적이 성공적으로 제출되었습니다."); + queryClient.invalidateQueries({ queryKey: [ScoresQueryKeys.myLanguageTestScore] }); + }, + + onError: (error) => { + console.error("어학 성적 제출 중 오류 발생:", error); + toast.error("오류가 발생했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostLanguageTestScore; diff --git a/src/apis/applications/api.ts b/src/apis/applications/api.ts new file mode 100644 index 00000000..f41225aa --- /dev/null +++ b/src/apis/applications/api.ts @@ -0,0 +1,60 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ApplicationListResponse } from "@/types/application"; + +// ====== Query Keys ====== +export const ApplicationsQueryKeys = { + competitorsApplicationList: "competitorsApplicationList", +} as const; + +// ====== Types ====== +export interface UseSubmitApplicationResponse { + isSuccess: boolean; +} + +export interface UseSubmitApplicationRequest { + gpaScoreId: number; + languageTestScoreId: number; + universityChoiceRequest: { + firstChoiceUniversityId: number | null; + secondChoiceUniversityId: number | null; + thirdChoiceUniversityId: number | null; + }; +} + +export interface CompetitorsResponse { + competitors: Array<{ + id: number; + name: string; + score: number; + }>; +} + +// ====== API Functions ====== +export const applicationsApi = { + /** + * 지원 목록 조회 + */ + getApplicationsList: async (): Promise> => { + return axiosInstance.get("/applications"); + }, + + /** + * 지원 제출 + */ + postSubmitApplication: async ( + request: UseSubmitApplicationRequest, + ): Promise> => { + return axiosInstance.post("/applications", request); + }, + + /** + * 경쟁자 목록 조회 + */ + getCompetitors: async (config?: { params?: Record }): Promise => { + const res = await axiosInstance.get("/applications/competitors", config); + return res.data; + }, +}; diff --git a/src/apis/applications/getApplicants.ts b/src/apis/applications/getApplicants.ts new file mode 100644 index 00000000..6523baa8 --- /dev/null +++ b/src/apis/applications/getApplicants.ts @@ -0,0 +1,29 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { ApplicationsQueryKeys, applicationsApi } from "./api"; + +import { ApplicationListResponse } from "@/types/application"; + +import { UseQueryOptions, UseQueryResult, useQuery } from "@tanstack/react-query"; + +type UseGetApplicationsListOptions = Omit< + UseQueryOptions, AxiosError<{ message: string }>, ApplicationListResponse>, + "queryKey" | "queryFn" +>; + +/** + * @description 지원 목록 조회 훅 + */ +const useGetApplicationsList = ( + props?: UseGetApplicationsListOptions, +): UseQueryResult> => { + return useQuery({ + queryKey: [ApplicationsQueryKeys.competitorsApplicationList], + queryFn: applicationsApi.getApplicationsList, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + select: (response) => response.data, + ...props, + }); +}; + +export default useGetApplicationsList; diff --git a/src/apis/applications/getCompetitors.ts b/src/apis/applications/getCompetitors.ts new file mode 100644 index 00000000..c0696094 --- /dev/null +++ b/src/apis/applications/getCompetitors.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { CompetitorsResponse, applicationsApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetCompetitors = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.applications.competitors, params], + queryFn: () => applicationsApi.getCompetitors(params ? { params } : {}), + }); +}; + +export default useGetCompetitors; diff --git a/src/apis/applications/index.ts b/src/apis/applications/index.ts new file mode 100644 index 00000000..a618c45e --- /dev/null +++ b/src/apis/applications/index.ts @@ -0,0 +1,5 @@ +export { applicationsApi, ApplicationsQueryKeys } from "./api"; +export type { UseSubmitApplicationResponse, UseSubmitApplicationRequest } from "./api"; +export { default as useGetApplicationsList } from "./getApplicants"; +export { default as useGetCompetitors } from "./getCompetitors"; +export { default as usePostSubmitApplication } from "./postSubmitApplication"; diff --git a/src/apis/applications/postSubmitApplication.ts b/src/apis/applications/postSubmitApplication.ts new file mode 100644 index 00000000..45c47bd5 --- /dev/null +++ b/src/apis/applications/postSubmitApplication.ts @@ -0,0 +1,38 @@ +import { AxiosError, AxiosResponse } from "axios"; + +import { UseSubmitApplicationRequest, UseSubmitApplicationResponse, applicationsApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { UseMutationOptions, UseMutationResult, useMutation } from "@tanstack/react-query"; + +/** + * @description 지원 제출 훅 + */ +const usePostSubmitApplication = ( + props?: UseMutationOptions< + AxiosResponse, + AxiosError<{ message: string }>, + UseSubmitApplicationRequest, + unknown + >, +): UseMutationResult< + AxiosResponse, + AxiosError<{ message: string }>, + UseSubmitApplicationRequest, + unknown +> => { + return useMutation< + AxiosResponse, + AxiosError<{ message: string }>, + UseSubmitApplicationRequest + >({ + ...props, + mutationFn: applicationsApi.postSubmitApplication, + onError: (error) => { + const errorMessage = error?.response?.data?.message; + toast.error(errorMessage || "지원 중 오류가 발생했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostSubmitApplication; diff --git a/src/apis/chat/api.ts b/src/apis/chat/api.ts new file mode 100644 index 00000000..df2d984b --- /dev/null +++ b/src/apis/chat/api.ts @@ -0,0 +1,57 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ChatMessage, ChatPartner, ChatRoom } from "@/types/chat"; + +// QueryKeys for chat domain +export const ChatQueryKeys = { + chatRooms: "chatRooms", + chatHistories: "chatHistories", + partnerInfo: "partnerInfo", +} as const; + +// Re-export types from @/types/chat +export type { ChatMessage, ChatRoom, ChatPartner }; + +export interface ChatHistoriesResponse { + nextPageNumber: number; // 다음 페이지가 없다면 -1 + content: ChatMessage[]; +} + +export interface ChatRoomListResponse { + chatRooms: ChatRoom[]; +} + +interface GetChatHistoriesParams { + roomId: number; + size?: number; + page?: number; +} + +export const chatApi = { + getChatHistories: async ({ roomId, size = 20, page = 0 }: GetChatHistoriesParams): Promise => { + const res = await axiosInstance.get(`/chats/rooms/${roomId}`, { + params: { + size, + page, + }, + }); + return res.data; + }, + + getChatRooms: async (): Promise => { + const res = await axiosInstance.get("/chats/rooms"); + return res.data; + }, + + putReadChatRoom: async (roomId: number): Promise => { + const response: AxiosResponse = await axiosInstance.put(`/chats/rooms/${roomId}/read`); + return response.data; + }, + + getChatPartner: async (roomId: number): Promise => { + const res = await axiosInstance.get(`/chats/rooms/${roomId}/partner`); + return res.data; + }, +}; diff --git a/src/api/chat/clients/useGetChatHistories.ts b/src/apis/chat/getChatMessages.ts similarity index 55% rename from src/api/chat/clients/useGetChatHistories.ts rename to src/apis/chat/getChatMessages.ts index 91916b08..9c0adf12 100644 --- a/src/api/chat/clients/useGetChatHistories.ts +++ b/src/apis/chat/getChatMessages.ts @@ -1,38 +1,12 @@ import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { ChatMessage } from "@/types/chat"; +import { ChatHistoriesResponse, ChatMessage, ChatQueryKeys, chatApi } from "./api"; import { useInfiniteQuery } from "@tanstack/react-query"; -export interface ChatHistoriesResponse { - nextPageNumber: number; // 다음 페이지가 없다면 -1 - content: ChatMessage[]; -} - -interface GetChatHistoriesParams { - roomId: number; - size?: number; - page?: number; -} - -const getChatHistories = async ({ - roomId, - size = 20, - page = 0, -}: GetChatHistoriesParams): Promise => { - const res = await axiosInstance.get(`/chats/rooms/${roomId}`, { - params: { - size, - page, - }, - }); - return res.data; -}; - +/** + * @description 채팅 히스토리를 무한 스크롤로 가져오는 훅 + */ const useGetChatHistories = (roomId: number, size: number = 20) => { return useInfiniteQuery< ChatHistoriesResponse, @@ -45,8 +19,8 @@ const useGetChatHistories = (roomId: number, size: number = 20) => { [string, number], number >({ - queryKey: [QueryKeys.chatHistories, roomId], - queryFn: ({ pageParam = 0 }: { pageParam?: number }) => getChatHistories({ roomId, size, page: pageParam }), + queryKey: [ChatQueryKeys.chatHistories, roomId], + queryFn: ({ pageParam = 0 }: { pageParam?: number }) => chatApi.getChatHistories({ roomId, size, page: pageParam }), initialPageParam: 0, getNextPageParam: (lastPage: ChatHistoriesResponse) => { // nextPageNumber가 -1이면 더 이상 페이지가 없음 diff --git a/src/apis/chat/getChatPartner.ts b/src/apis/chat/getChatPartner.ts new file mode 100644 index 00000000..0f871aa8 --- /dev/null +++ b/src/apis/chat/getChatPartner.ts @@ -0,0 +1,19 @@ +import { AxiosError } from "axios"; + +import { ChatPartner, ChatQueryKeys, chatApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 채팅 상대방 정보를 가져오는 훅 + */ +const useGetPartnerInfo = (roomId: number) => { + return useQuery({ + queryKey: [ChatQueryKeys.partnerInfo, roomId], + queryFn: () => chatApi.getChatPartner(roomId), + staleTime: 1000 * 60 * 5, + enabled: !!roomId, + }); +}; + +export default useGetPartnerInfo; diff --git a/src/apis/chat/getChatRooms.ts b/src/apis/chat/getChatRooms.ts new file mode 100644 index 00000000..362583c4 --- /dev/null +++ b/src/apis/chat/getChatRooms.ts @@ -0,0 +1,19 @@ +import { AxiosError } from "axios"; + +import { ChatQueryKeys, ChatRoom, ChatRoomListResponse, chatApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 채팅방 목록을 가져오는 훅 + */ +const useGetChatRooms = () => { + return useQuery({ + queryKey: [ChatQueryKeys.chatRooms], + queryFn: chatApi.getChatRooms, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + select: (data) => data.chatRooms, + }); +}; + +export default useGetChatRooms; diff --git a/src/apis/chat/index.ts b/src/apis/chat/index.ts new file mode 100644 index 00000000..8774747a --- /dev/null +++ b/src/apis/chat/index.ts @@ -0,0 +1,6 @@ +export { chatApi, ChatQueryKeys } from "./api"; +export type { ChatHistoriesResponse, ChatRoomListResponse, ChatMessage, ChatRoom, ChatPartner } from "./api"; +export { default as useGetChatHistories } from "./getChatMessages"; +export { default as useGetPartnerInfo } from "./getChatPartner"; +export { default as useGetChatRooms } from "./getChatRooms"; +export { default as usePutChatRead } from "./putReadChatRoom"; diff --git a/src/apis/chat/putReadChatRoom.ts b/src/apis/chat/putReadChatRoom.ts new file mode 100644 index 00000000..6d955204 --- /dev/null +++ b/src/apis/chat/putReadChatRoom.ts @@ -0,0 +1,25 @@ +import { AxiosError } from "axios"; + +import { ChatQueryKeys, chatApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 채팅방 읽음 처리 훅 + */ +const usePutChatRead = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: chatApi.putReadChatRoom, + onSuccess: () => { + // 채팅방 목록 쿼리를 무효화하여 새로 고침 + queryClient.invalidateQueries({ queryKey: [ChatQueryKeys.chatRooms] }); + }, + onError: (error) => { + console.error("채팅방 진입 읽기 실패", error); + }, + }); +}; + +export default usePutChatRead; diff --git a/src/apis/community/api.ts b/src/apis/community/api.ts new file mode 100644 index 00000000..f57a3851 --- /dev/null +++ b/src/apis/community/api.ts @@ -0,0 +1,154 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +import { + CommentCreateRequest, + CommentIdResponse, + ListPost, + Post, + PostCreateRequest, + PostIdResponse, + PostLikeResponse, + PostUpdateRequest, +} from "@/types/community"; + +// QueryKeys for community domain +export const CommunityQueryKeys = { + posts: "posts", + postList: "postList1", // 기존 api/boards와 동일한 키 유지 +} as const; + +export interface BoardListResponse { + 0: string; + 1: string; + 2: string; + 3: string; +} + +export interface BoardResponseItem { + id: number; + title: string; + content: string; + likeCount: number; + commentCount: number; + createdAt: string; + updatedAt: string; + postCategory: string; + postThumbnailUrl: null | string; +} + +export interface BoardResponse { + 0: BoardResponseItem[]; + 1: BoardResponseItem[]; + 2: BoardResponseItem[]; + 3: BoardResponseItem[]; +} + +// Delete response types +export interface DeletePostResponse { + message: string; + postId: number; +} + +// Re-export types from @/types/community for convenience +export type { + Post, + PostCreateRequest, + PostIdResponse, + PostUpdateRequest, + PostLikeResponse, + CommentCreateRequest, + CommentIdResponse, + ListPost, +}; + +export const communityApi = { + /** + * 게시글 목록 조회 (클라이언트) + */ + getPostList: (boardCode: string, category: string | null = null): Promise> => { + const params = category && category !== "전체" ? { category } : {}; + return publicAxiosInstance.get(`/boards/${boardCode}`, { params }); + }, + + getBoardList: async (params?: Record): Promise => { + const res = await axiosInstance.get(`/boards`, { params }); + return res.data; + }, + + getBoard: async (boardCode: string, params?: Record): Promise => { + const res = await axiosInstance.get(`/boards/${boardCode}`, { params }); + return res.data; + }, + + getPostDetail: async (postId: number): Promise => { + const response: AxiosResponse = await axiosInstance.get(`/posts/${postId}`); + return response.data; + }, + + createPost: async (request: PostCreateRequest): Promise => { + const convertedRequest: FormData = new FormData(); + convertedRequest.append( + "postCreateRequest", + new Blob([JSON.stringify(request.postCreateRequest)], { type: "application/json" }), + ); + request.file.forEach((file) => { + convertedRequest.append("file", file); + }); + + const response: AxiosResponse = await axiosInstance.post(`/posts`, convertedRequest, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + return { + ...response.data, + boardCode: request.postCreateRequest.boardCode, + }; + }, + + updatePost: async (postId: number, request: PostUpdateRequest): Promise => { + const convertedRequest: FormData = new FormData(); + convertedRequest.append( + "postUpdateRequest", + new Blob([JSON.stringify(request.postUpdateRequest)], { type: "application/json" }), + ); + request.file.forEach((file) => { + convertedRequest.append("file", file); + }); + + const response: AxiosResponse = await axiosInstance.patch(`/posts/${postId}`, convertedRequest, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return response.data; + }, + + deletePost: async (postId: number): Promise> => { + return axiosInstance.delete(`/posts/${postId}`); + }, + + likePost: async (postId: number): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/posts/${postId}/like`); + return response.data; + }, + + unlikePost: async (postId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/posts/${postId}/like`); + return response.data; + }, + + createComment: async (request: CommentCreateRequest): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/comments`, request); + return response.data; + }, + + deleteComment: async (commentId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/comments/${commentId}`); + return response.data; + }, + + updateComment: async (commentId: number, data: { content: string }): Promise => { + const res = await axiosInstance.patch(`/comments/${commentId}`, data); + return res.data; + }, +}; diff --git a/src/apis/community/deleteComment.ts b/src/apis/community/deleteComment.ts new file mode 100644 index 00000000..25bba468 --- /dev/null +++ b/src/apis/community/deleteComment.ts @@ -0,0 +1,33 @@ +import { AxiosError } from "axios"; + +import { CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +interface DeleteCommentRequest { + commentId: number; + postId: number; +} + +/** + * @description 댓글 삭제를 위한 useMutation 커스텀 훅 + */ +const useDeleteComment = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ commentId }) => communityApi.deleteComment(commentId), + onSuccess: (data, variables) => { + // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); + toast.success("댓글이 삭제되었습니다."); + }, + onError: (error) => { + console.error("댓글 삭제 실패:", error); + toast.error("댓글 삭제에 실패했습니다."); + }, + }); +}; + +export default useDeleteComment; diff --git a/src/apis/community/deleteLikePost.ts b/src/apis/community/deleteLikePost.ts new file mode 100644 index 00000000..295fb6da --- /dev/null +++ b/src/apis/community/deleteLikePost.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, PostLikeResponse, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 게시글 좋아요 취소를 위한 useMutation 커스텀 훅 + */ +const useDeleteLike = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: communityApi.unlikePost, + onSuccess: (data, postId) => { + // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, postId] }); + }, + onError: (error) => { + console.error("게시글 좋아요 취소 실패:", error); + toast.error("좋아요 취소 처리에 실패했습니다."); + }, + }); +}; + +export default useDeleteLike; diff --git a/src/apis/community/deletePost.ts b/src/apis/community/deletePost.ts new file mode 100644 index 00000000..c1883dd7 --- /dev/null +++ b/src/apis/community/deletePost.ts @@ -0,0 +1,36 @@ +import { useRouter } from "next/navigation"; + +import { AxiosError, AxiosResponse } from "axios"; + +import { CommunityQueryKeys, DeletePostResponse, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 게시글 삭제를 위한 useMutation 커스텀 훅 + */ +const useDeletePost = () => { + const router = useRouter(); + const queryClient = useQueryClient(); + + return useMutation, AxiosError, number>({ + mutationFn: communityApi.deletePost, + onSuccess: () => { + // 'posts' 쿼리 키를 가진 모든 쿼리를 무효화하여 + // 게시글 목록을 다시 불러오도록 합니다. + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] }); + + toast.success("게시글이 성공적으로 삭제되었습니다."); + + // 게시글 목록 페이지 이동 + router.replace("/community/FREE"); + }, + onError: (error) => { + console.error("게시글 삭제 실패:", error); + toast.error("게시글 삭제에 실패했습니다. 잠시 후 다시 시도해주세요."); + }, + }); +}; + +export default useDeletePost; diff --git a/src/apis/community/getBoard.ts b/src/apis/community/getBoard.ts new file mode 100644 index 00000000..27670287 --- /dev/null +++ b/src/apis/community/getBoard.ts @@ -0,0 +1,16 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { BoardResponse, communityApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetBoard = (boardCode: string | number, params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.community.board, boardCode, params], + queryFn: () => communityApi.getBoard(boardCode as string, params), + enabled: !!boardCode, + }); +}; + +export default useGetBoard; diff --git a/src/apis/community/getBoardList.ts b/src/apis/community/getBoardList.ts new file mode 100644 index 00000000..80bbbe69 --- /dev/null +++ b/src/apis/community/getBoardList.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { BoardListResponse, communityApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetBoardList = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.community.boardList, params], + queryFn: () => communityApi.getBoardList(params ? { params } : {}), + }); +}; + +export default useGetBoardList; diff --git a/src/apis/community/getPostDetail.ts b/src/apis/community/getPostDetail.ts new file mode 100644 index 00000000..034c33af --- /dev/null +++ b/src/apis/community/getPostDetail.ts @@ -0,0 +1,18 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, Post, communityApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 게시글 상세 조회를 위한 useQuery 커스텀 훅 + */ +const useGetPostDetail = (postId: number) => { + return useQuery({ + queryKey: [CommunityQueryKeys.posts, postId], + queryFn: () => communityApi.getPostDetail(postId), + enabled: !!postId, + }); +}; + +export default useGetPostDetail; diff --git a/src/apis/community/getPostList.ts b/src/apis/community/getPostList.ts new file mode 100644 index 00000000..ca8341f7 --- /dev/null +++ b/src/apis/community/getPostList.ts @@ -0,0 +1,31 @@ +import { AxiosResponse } from "axios"; + +import { CommunityQueryKeys, communityApi } from "./api"; + +import { ListPost } from "@/types/community"; + +import { useQuery } from "@tanstack/react-query"; + +interface UseGetPostListProps { + boardCode: string; + category?: string | null; +} + +/** + * @description 게시글 목록 조회 훅 + */ +const useGetPostList = ({ boardCode, category = null }: UseGetPostListProps) => { + return useQuery({ + queryKey: [CommunityQueryKeys.postList, boardCode, category], + queryFn: () => communityApi.getPostList(boardCode, category), + staleTime: Infinity, + gcTime: 1000 * 60 * 30, // 30분 + select: (response) => { + return [...response.data].sort((a, b) => { + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }); + }, + }); +}; + +export default useGetPostList; diff --git a/src/apis/community/index.ts b/src/apis/community/index.ts new file mode 100644 index 00000000..f847cdc8 --- /dev/null +++ b/src/apis/community/index.ts @@ -0,0 +1,26 @@ +export { communityApi, CommunityQueryKeys } from "./api"; +export type { + Post, + PostCreateRequest, + PostIdResponse, + PostUpdateRequest, + PostLikeResponse, + CommentCreateRequest, + CommentIdResponse, + ListPost, +} from "./api"; +export { default as useDeleteComment } from "./deleteComment"; +export { default as useDeleteLike } from "./deleteLikePost"; +export { default as useDeletePost } from "./deletePost"; +export { default as useGetBoard } from "./getBoard"; +export { default as useGetBoardList } from "./getBoardList"; +export { default as useGetPostDetail } from "./getPostDetail"; +export { default as useGetPostList } from "./getPostList"; +export { default as usePatchUpdateComment } from "./patchUpdateComment"; +export { default as useUpdatePost } from "./patchUpdatePost"; +export { default as useCreateComment } from "./postCreateComment"; +export { default as useCreatePost } from "./postCreatePost"; +export { default as usePostLike } from "./postLikePost"; + +// Server-side functions +export { getPostListServer } from "./server"; diff --git a/src/apis/community/patchUpdateComment.ts b/src/apis/community/patchUpdateComment.ts new file mode 100644 index 00000000..c3416453 --- /dev/null +++ b/src/apis/community/patchUpdateComment.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { CommentIdResponse, communityApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePatchUpdateComment = () => { + return useMutation({ + mutationFn: ({ commentId, content }) => communityApi.updateComment(commentId, { content }), + }); +}; + +export default usePatchUpdateComment; diff --git a/src/apis/community/patchUpdatePost.ts b/src/apis/community/patchUpdatePost.ts new file mode 100644 index 00000000..8923406c --- /dev/null +++ b/src/apis/community/patchUpdatePost.ts @@ -0,0 +1,34 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, PostIdResponse, PostUpdateRequest, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +interface UpdatePostVariables { + postId: number; + data: PostUpdateRequest; +} + +/** + * @description 게시글 수정을 위한 useMutation 커스텀 훅 + */ +const useUpdatePost = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ postId, data }) => communityApi.updatePost(postId, data), + onSuccess: (result, variables) => { + // 해당 게시글 상세 쿼리와 목록 쿼리를 무효화 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] }); + toast.success("게시글이 수정되었습니다."); + }, + onError: (error) => { + console.error("게시글 수정 실패:", error); + toast.error("게시글 수정에 실패했습니다."); + }, + }); +}; + +export default useUpdatePost; diff --git a/src/apis/community/postCreateComment.ts b/src/apis/community/postCreateComment.ts new file mode 100644 index 00000000..97a28f49 --- /dev/null +++ b/src/apis/community/postCreateComment.ts @@ -0,0 +1,28 @@ +import { AxiosError } from "axios"; + +import { CommentCreateRequest, CommentIdResponse, CommunityQueryKeys, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 댓글 생성을 위한 useMutation 커스텀 훅 + */ +const useCreateComment = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: communityApi.createComment, + onSuccess: (data, variables) => { + // 해당 게시글 상세 쿼리를 무효화하여 댓글 목록 갱신 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] }); + toast.success("댓글이 등록되었습니다."); + }, + onError: (error) => { + console.error("댓글 생성 실패:", error); + toast.error("댓글 등록에 실패했습니다."); + }, + }); +}; + +export default useCreateComment; diff --git a/src/api/community/client/useCreatePost.ts b/src/apis/community/postCreatePost.ts similarity index 57% rename from src/api/community/client/useCreatePost.ts rename to src/apis/community/postCreatePost.ts index 543cd0e6..446bd9ee 100644 --- a/src/api/community/client/useCreatePost.ts +++ b/src/apis/community/postCreatePost.ts @@ -1,40 +1,11 @@ -import { AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; - -import { PostCreateRequest, PostIdResponse } from "@/types/community"; +import { CommunityQueryKeys, PostCreateRequest, PostIdResponse, communityApi } from "./api"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -/** - * @description 게시글 생성 API 함수 - * @param request - 게시글 생성 요청 데이터 - * @returns Promise - */ -const createPost = async (request: PostCreateRequest): Promise => { - const convertedRequest: FormData = new FormData(); - convertedRequest.append( - "postCreateRequest", - new Blob([JSON.stringify(request.postCreateRequest)], { type: "application/json" }), - ); - request.file.forEach((file) => { - convertedRequest.append("file", file); - }); - - const response: AxiosResponse = await axiosInstance.post(`/posts`, convertedRequest, { - headers: { "Content-Type": "multipart/form-data" }, - }); - - return { - ...response.data, - boardCode: request.postCreateRequest.boardCode, - }; -}; - /** * @description ISR 페이지를 revalidate하는 함수 * @param boardCode - 게시판 코드 @@ -67,11 +38,11 @@ const useCreatePost = () => { const queryClient = useQueryClient(); const { accessToken } = useAuthStore(); - return useMutation({ - mutationFn: createPost, + return useMutation({ + mutationFn: communityApi.createPost, onSuccess: async (data) => { // 게시글 목록 쿼리를 무효화하여 최신 목록 반영 - queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] }); + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] }); // ISR 페이지 revalidate (사용자 인증 토큰 사용) if (accessToken) { diff --git a/src/apis/community/postLikePost.ts b/src/apis/community/postLikePost.ts new file mode 100644 index 00000000..2591446d --- /dev/null +++ b/src/apis/community/postLikePost.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { CommunityQueryKeys, PostLikeResponse, communityApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 게시글 좋아요를 위한 useMutation 커스텀 훅 + */ +const usePostLike = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: communityApi.likePost, + onSuccess: (data, postId) => { + // 해당 게시글 상세 쿼리를 무효화하여 최신 데이터 반영 + queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, postId] }); + }, + onError: (error) => { + console.error("게시글 좋아요 실패:", error); + toast.error("좋아요 처리에 실패했습니다."); + }, + }); +}; + +export default usePostLike; diff --git a/src/api/boards/server/getPostList.ts b/src/apis/community/server.ts similarity index 71% rename from src/api/boards/server/getPostList.ts rename to src/apis/community/server.ts index d7b7dfba..91da59e3 100644 --- a/src/api/boards/server/getPostList.ts +++ b/src/apis/community/server.ts @@ -15,13 +15,12 @@ interface GetPostListParams { * @param revalidate - ISR revalidate 시간(초) 또는 false (무한 캐시) * @returns Promise> */ -export const getPostList = async ({ +export const getPostListServer = async ({ boardCode, category = null, - revalidate = false, // 기본값: 자동 재생성 비활성화 (수동 revalidate만) + revalidate = false, }: GetPostListParams): Promise> => { const params = new URLSearchParams(); - // "전체"는 필터 없음을 의미하므로 파라미터에 포함하지 않음 if (category && category !== "전체") { params.append("category", category); } @@ -32,8 +31,8 @@ export const getPostList = async ({ return serverFetch(url, { method: "GET", next: { - revalidate: revalidate === false ? undefined : revalidate, - tags: [`posts-${boardCode}`], // 태그 기반 revalidation 지원 (글 작성 시만 revalidate) + ...(revalidate !== false && { revalidate }), + tags: [`posts-${boardCode}`], }, }); }; diff --git a/src/apis/image-upload/api.ts b/src/apis/image-upload/api.ts new file mode 100644 index 00000000..f93f1490 --- /dev/null +++ b/src/apis/image-upload/api.ts @@ -0,0 +1,83 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +import { FileResponse } from "@/types/file"; + +// ====== Types ====== +export type SlackNotificationResponse = void; +export type SlackNotificationRequest = Record; + +export interface UploadLanguageTestReportResponse { + fileUrl: string; +} + +export interface UploadProfileImageResponse { + fileUrl: string; +} + +export interface UploadGpaReportResponse { + fileUrl: string; +} + +// ====== API Functions ====== +export const imageUploadApi = { + /** + * 슬랙 알림 전송 + */ + postSlackNotification: async (params: { data?: SlackNotificationRequest }): Promise => { + const res = await axiosInstance.post( + `https://hooks.slack.com/services/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`, + params?.data, + ); + return res.data; + }, + + /** + * 어학 성적 증명서 업로드 + */ + postUploadLanguageTestReport: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const res = await axiosInstance.post(`/file/language-test`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, + + /** + * 프로필 이미지 업로드 (로그인 후) + */ + postUploadProfileImage: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const res = await axiosInstance.post(`/file/profile/post`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, + + /** + * 프로필 이미지 업로드 (회원가입 전, 공개 API) + */ + postUploadProfileImageBeforeSignup: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const response: AxiosResponse = await publicAxiosInstance.post("/file/profile/pre", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return response.data; + }, + + /** + * 학점 증명서 업로드 + */ + postUploadGpaReport: async (file: File): Promise => { + const formData = new FormData(); + formData.append("file", file); + const res = await axiosInstance.post(`/file/gpa`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, +}; diff --git a/src/apis/image-upload/index.ts b/src/apis/image-upload/index.ts new file mode 100644 index 00000000..83a2df4e --- /dev/null +++ b/src/apis/image-upload/index.ts @@ -0,0 +1,8 @@ +export { imageUploadApi } from "./api"; +export type { UploadLanguageTestReportResponse, UploadProfileImageResponse, UploadGpaReportResponse } from "./api"; + +export { default as useSlackNotification } from "./postSlackNotification"; +export { default as useUploadGpaReport } from "./postUploadGpaReport"; +export { default as useUploadLanguageTestReport } from "./postUploadLanguageTestReport"; +export { default as useUploadProfileImage } from "./postUploadProfileImage"; +export { default as useUploadProfileImagePublic } from "./postUploadProfileImageBeforeSignup"; diff --git a/src/apis/image-upload/postSlackNotification.ts b/src/apis/image-upload/postSlackNotification.ts new file mode 100644 index 00000000..c5ea6962 --- /dev/null +++ b/src/apis/image-upload/postSlackNotification.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { SlackNotificationRequest, SlackNotificationResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostSlackNotification = () => { + return useMutation({ + mutationFn: (data) => imageUploadApi.postSlackNotification({ data }), + }); +}; + +export default usePostSlackNotification; diff --git a/src/apis/image-upload/postUploadGpaReport.ts b/src/apis/image-upload/postUploadGpaReport.ts new file mode 100644 index 00000000..f8ffa4c8 --- /dev/null +++ b/src/apis/image-upload/postUploadGpaReport.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UploadGpaReportResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostUploadGpaReport = () => { + return useMutation({ + mutationFn: (file) => imageUploadApi.postUploadGpaReport(file), + }); +}; + +export default usePostUploadGpaReport; diff --git a/src/apis/image-upload/postUploadLanguageTestReport.ts b/src/apis/image-upload/postUploadLanguageTestReport.ts new file mode 100644 index 00000000..27002711 --- /dev/null +++ b/src/apis/image-upload/postUploadLanguageTestReport.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UploadLanguageTestReportResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostUploadLanguageTestReport = () => { + return useMutation({ + mutationFn: (file) => imageUploadApi.postUploadLanguageTestReport(file), + }); +}; + +export default usePostUploadLanguageTestReport; diff --git a/src/apis/image-upload/postUploadProfileImage.ts b/src/apis/image-upload/postUploadProfileImage.ts new file mode 100644 index 00000000..06a34553 --- /dev/null +++ b/src/apis/image-upload/postUploadProfileImage.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UploadProfileImageResponse, imageUploadApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostUploadProfileImage = () => { + return useMutation({ + mutationFn: (file) => imageUploadApi.postUploadProfileImage(file), + }); +}; + +export default usePostUploadProfileImage; diff --git a/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts b/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts new file mode 100644 index 00000000..3e7bfc30 --- /dev/null +++ b/src/apis/image-upload/postUploadProfileImageBeforeSignup.ts @@ -0,0 +1,23 @@ +import { AxiosError } from "axios"; + +import { imageUploadApi } from "./api"; + +import { FileResponse } from "@/types/file"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 프로필 이미지 업로드를 위한 useMutation 커스텀 훅 (회원가입 전 공개 API) + */ +const useUploadProfileImagePublic = () => { + return useMutation({ + mutationFn: imageUploadApi.postUploadProfileImageBeforeSignup, + onError: (error) => { + console.error("프로필 이미지 업로드 실패:", error); + toast.error("이미지 업로드에 실패했습니다."); + }, + }); +}; + +export default useUploadProfileImagePublic; diff --git a/src/apis/kakao-api/api.ts b/src/apis/kakao-api/api.ts new file mode 100644 index 00000000..54b9cfac --- /dev/null +++ b/src/apis/kakao-api/api.ts @@ -0,0 +1,34 @@ +import { axiosInstance } from "@/utils/axiosInstance"; + +export type KakaoUserIdsResponse = void; + +export type KakaoUnlinkResponse = void; + +export type KakaoUnlinkRequest = Record; + +export type KakaoInfoResponse = void; + +export const kakaoApiApi = { + getKakaoUserIds: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`https://kapi.kakao.com/v1/user/ids?order=dsc`, { + params: params?.params, + }); + return res.data; + }, + + postKakaoUnlink: async (params: { data?: KakaoUnlinkRequest }): Promise => { + const res = await axiosInstance.post( + `https://kapi.kakao.com/v1/user/unlink?target_id_type=user_id&target_id=3715136239`, + params?.data, + ); + return res.data; + }, + + getKakaoInfo: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get( + `https://kapi.kakao.com/v2/user/me?property_keys=["kakao_account.email"]&target_id_type=user_id&target_id=3715136239`, + { params: params?.params }, + ); + return res.data; + }, +}; diff --git a/src/apis/kakao-api/getKakaoInfo.ts b/src/apis/kakao-api/getKakaoInfo.ts new file mode 100644 index 00000000..53a4da37 --- /dev/null +++ b/src/apis/kakao-api/getKakaoInfo.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { KakaoInfoResponse, kakaoApiApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetKakaoInfo = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys["kakao-api"].kakaoInfo, params], + queryFn: () => kakaoApiApi.getKakaoInfo(params ? { params } : {}), + }); +}; + +export default useGetKakaoInfo; diff --git a/src/apis/kakao-api/getKakaoUserIds.ts b/src/apis/kakao-api/getKakaoUserIds.ts new file mode 100644 index 00000000..a6c1e6bd --- /dev/null +++ b/src/apis/kakao-api/getKakaoUserIds.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { KakaoUserIdsResponse, kakaoApiApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetKakaoUserIds = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys["kakao-api"].kakaoUserIds, params], + queryFn: () => kakaoApiApi.getKakaoUserIds(params ? { params } : {}), + }); +}; + +export default useGetKakaoUserIds; diff --git a/src/apis/kakao-api/index.ts b/src/apis/kakao-api/index.ts new file mode 100644 index 00000000..0acb2db7 --- /dev/null +++ b/src/apis/kakao-api/index.ts @@ -0,0 +1,4 @@ +export { kakaoApiApi } from "./api"; +export { default as getKakaoInfo } from "./getKakaoInfo"; +export { default as getKakaoUserIds } from "./getKakaoUserIds"; +export { default as postKakaoUnlink } from "./postKakaoUnlink"; diff --git a/src/apis/kakao-api/postKakaoUnlink.ts b/src/apis/kakao-api/postKakaoUnlink.ts new file mode 100644 index 00000000..268eae6c --- /dev/null +++ b/src/apis/kakao-api/postKakaoUnlink.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { KakaoUnlinkRequest, KakaoUnlinkResponse, kakaoApiApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostKakaoUnlink = () => { + return useMutation({ + mutationFn: (data) => kakaoApiApi.postKakaoUnlink({ data }), + }); +}; + +export default usePostKakaoUnlink; diff --git a/src/apis/mentor/api.ts b/src/apis/mentor/api.ts new file mode 100644 index 00000000..6a8ee150 --- /dev/null +++ b/src/apis/mentor/api.ts @@ -0,0 +1,194 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { MentoringListItem, VerifyStatus } from "@/types/mentee"; +import { MentorCardDetail, MentorCardPreview, MentoringApprovalStatus, MentoringItem } from "@/types/mentor"; + +// QueryKeys for mentor domain +export const MentorQueryKeys = { + myMentorProfile: "myMentorProfile", + mentoringList: "mentoringList", + mentoringNewCount: "mentoringNewCount", + applyMentoringList: "applyMentoringList", + mentorList: "mentorList", + mentorDetail: "mentorDetail", +} as const; + +// Re-export types +export type { MentorCardPreview, MentorCardDetail, MentoringItem }; +export type { MentoringListItem, VerifyStatus }; +export { MentoringApprovalStatus }; + +// Response types +export interface MentoringListResponse { + content: MentoringItem[]; + nextPageNumber: number; +} + +export interface GetMentoringNewCountResponse { + uncheckedCount: number; +} + +export interface ApplyMentoringListResponse { + content: MentoringListItem[]; + nextPageNumber: number; +} + +export interface MentorListResponse { + nextPageNumber: number; + content: MentorCardDetail[]; +} + +export interface MatchedMentorsResponse { + content: MentorCardDetail[]; + nextPageNumber: number; + totalElements: number; +} + +export interface PatchApprovalStatusRequest { + status: MentoringApprovalStatus; + mentoringId: number; +} + +export interface PatchApprovalStatusResponse { + mentoringId: number; + chatRoomId: number; +} + +export interface PatchCheckMentoringsRequest { + checkedMentoringIds: number[]; +} + +export interface PatchCheckMentoringsResponse { + checkedMentoringIds: number[]; +} + +export interface PostApplyMentoringRequest { + mentorId: number; +} + +export interface PostApplyMentoringResponse { + mentoringId: number; +} + +export interface PostMentorApplicationRequest { + interestedCountries: string[]; + country: string; + universityName: string; + studyStatus: "STUDYING" | "PLANNING" | "COMPLETED"; + verificationFile: File; +} + +export interface PutMyMentorProfileRequest { + channels: { type: string; url: string }[]; + passTip: string; + introduction: string; +} + +const OFFSET = 5; +const MENTORS_OFFSET = 10; +const MENTEE_OFFSET = 3; + +export const mentorApi = { + // === Mentor (멘토) APIs === + getMentorMyProfile: async (): Promise => { + const res = await axiosInstance.get("/mentor/my"); + return res.data; + }, + + getMentoringList: async (page: number, size: number = OFFSET): Promise => { + const endpoint = `/mentor/mentorings?size=${size}&page=${page}`; + const res = await axiosInstance.get(endpoint); + return res.data; + }, + + getMentoringUncheckedCount: async (): Promise => { + const endpoint = "/mentor/mentorings/check"; + const res = await axiosInstance.get(endpoint); + return res.data; + }, + + patchApprovalStatus: async (props: PatchApprovalStatusRequest): Promise => { + const { status, mentoringId } = props; + const res = await axiosInstance.patch(`/mentor/mentorings/${mentoringId}`, { + status, + }); + return res.data; + }, + + patchMentorCheckMentorings: async (body: PatchCheckMentoringsRequest): Promise => { + const res = await axiosInstance.patch("/mentor/mentorings/check", body); + return res.data; + }, + + postMentorApplication: async (body: PostMentorApplicationRequest): Promise => { + const formData = new FormData(); + const applicationData = { + interestedCountries: body.interestedCountries, + country: body.country, + universityName: body.universityName, + studyStatus: body.studyStatus, + }; + formData.append( + "mentorApplicationRequest", + new Blob([JSON.stringify(applicationData)], { type: "application/json" }), + ); + formData.append("file", body.verificationFile); + const res = await axiosInstance.post("/mentor/verification", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + return res.data; + }, + + putMyMentorProfile: async (body: PutMyMentorProfileRequest): Promise => { + const res = await axiosInstance.put("/mentor/my", body); + return res.data; + }, + + // === Mentee (멘티) APIs === + getApplyMentoringList: async ( + verifyStatus: VerifyStatus, + page: number, + size: number = MENTEE_OFFSET, + ): Promise => { + const res = await axiosInstance.get( + `/mentee/mentorings?verify-status=${verifyStatus}&size=${size}&page=${page}`, + ); + return res.data; + }, + + patchMenteeCheckMentorings: async (body: PatchCheckMentoringsRequest): Promise => { + const res = await axiosInstance.patch("/mentee/mentorings/check", body); + return res.data; + }, + + postApplyMentoring: async (body: PostApplyMentoringRequest): Promise => { + const res = await axiosInstance.post("/mentee/mentorings", body); + return res.data; + }, + + // === Mentors (멘토 목록) APIs === + getMentorList: async (region: string, page: number, size: number = MENTORS_OFFSET): Promise => { + const res = await axiosInstance.get(`/mentors?region=${region}&page=${page}&size=${size}`); + return res.data; + }, + + getMentorDetail: async (mentorId: number): Promise => { + const res = await axiosInstance.get(`/mentors/${mentorId}`); + return res.data; + }, + + getMatchedMentors: async (params: { + defaultSize: string | number; + defaultPage: string | number; + params?: Record; + }): Promise => { + const { defaultSize, defaultPage, params: queryParams } = params; + const res = await axiosInstance.get( + `/mentors/matched?size=${defaultSize}&page=${defaultPage}`, + { params: queryParams }, + ); + return res.data; + }, +}; diff --git a/src/apis/mentor/getAppliedMentorings.ts b/src/apis/mentor/getAppliedMentorings.ts new file mode 100644 index 00000000..2d6d516f --- /dev/null +++ b/src/apis/mentor/getAppliedMentorings.ts @@ -0,0 +1,38 @@ +import { AxiosError } from "axios"; + +import { ApplyMentoringListResponse, MentorQueryKeys, MentoringListItem, VerifyStatus, mentorApi } from "./api"; + +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; +import type { QueryFunctionContext } from "@tanstack/react-query"; + +/** + * @description 신청한 멘토링 목록 조회 훅 (무한 스크롤) + */ +const useGetApplyMentoringList = (verifyStatus: VerifyStatus) => { + return useInfiniteQuery({ + queryKey: [MentorQueryKeys.applyMentoringList, verifyStatus], + queryFn: ({ pageParam = 0 }) => mentorApi.getApplyMentoringList(verifyStatus, pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), + staleTime: 1000 * 60 * 5, // 5분간 캐시 + select: (data) => data.pages.flatMap((p) => p.content), + }); +}; + +// 멘토링 리스트 프리페치용 훅 +export const usePrefetchApplyMentoringList = () => { + const queryClient = useQueryClient(); + + const prefetchList = (verifyStatus: VerifyStatus) => { + queryClient.prefetchInfiniteQuery({ + queryKey: [MentorQueryKeys.applyMentoringList, verifyStatus], + queryFn: ({ pageParam = 0 }) => mentorApi.getApplyMentoringList(verifyStatus, pageParam as number), + initialPageParam: 0, + staleTime: 1000 * 60 * 5, + }); + }; + + return { prefetchList }; +}; + +export default useGetApplyMentoringList; diff --git a/src/apis/mentor/getMatchedMentors.ts b/src/apis/mentor/getMatchedMentors.ts new file mode 100644 index 00000000..46402b2d --- /dev/null +++ b/src/apis/mentor/getMatchedMentors.ts @@ -0,0 +1,20 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { MatchedMentorsResponse, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetMatchedMentors = ( + defaultSize: string | number, + defaultPage: string | number, + params?: Record, +) => { + return useQuery({ + queryKey: [QueryKeys.mentor.matchedMentors, defaultSize, defaultPage, params], + queryFn: () => mentorApi.getMatchedMentors({ defaultSize, defaultPage, params }), + enabled: !!defaultSize && !!defaultPage, + }); +}; + +export default useGetMatchedMentors; diff --git a/src/apis/mentor/getMentorDetail.ts b/src/apis/mentor/getMentorDetail.ts new file mode 100644 index 00000000..6a689866 --- /dev/null +++ b/src/apis/mentor/getMentorDetail.ts @@ -0,0 +1,19 @@ +import { AxiosError } from "axios"; + +import { MentorCardDetail, MentorQueryKeys, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 멘토 상세 조회 훅 + */ +const useGetMentorDetail = (mentorId: number | null) => { + return useQuery({ + queryKey: [MentorQueryKeys.mentorDetail, mentorId!], + queryFn: () => mentorApi.getMentorDetail(mentorId!), + enabled: mentorId !== null, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + }); +}; + +export default useGetMentorDetail; diff --git a/src/apis/mentor/getMentorList.ts b/src/apis/mentor/getMentorList.ts new file mode 100644 index 00000000..13f64f6c --- /dev/null +++ b/src/apis/mentor/getMentorList.ts @@ -0,0 +1,42 @@ +import { AxiosError } from "axios"; + +import { MentorCardDetail, MentorListResponse, MentorQueryKeys, mentorApi } from "./api"; + +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; +import type { QueryFunctionContext } from "@tanstack/react-query"; + +interface UseGetMentorListRequest { + region?: string; +} + +/** + * @description 멘토 목록 조회 훅 (무한 스크롤) + */ +const useGetMentorList = ({ region = "" }: UseGetMentorListRequest = {}) => { + return useInfiniteQuery({ + queryKey: [MentorQueryKeys.mentorList, region], + queryFn: ({ pageParam = 0 }) => mentorApi.getMentorList(region, pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => (lastPage.nextPageNumber === -1 ? undefined : lastPage.nextPageNumber), + staleTime: 1000 * 60 * 5, + select: (data) => data.pages.flatMap((p) => p.content), + }); +}; + +// 탭 프리페치용 훅 +export const usePrefetchMentorList = () => { + const queryClient = useQueryClient(); + + const prefetchMentorList = (region: string) => { + queryClient.prefetchInfiniteQuery({ + queryKey: [MentorQueryKeys.mentorList, region], + queryFn: ({ pageParam = 0 }) => mentorApi.getMentorList(region, pageParam as number), + initialPageParam: 0, + staleTime: 1000 * 60 * 5, + }); + }; + + return { prefetchMentorList }; +}; + +export default useGetMentorList; diff --git a/src/apis/mentor/getMyMentorPage.ts b/src/apis/mentor/getMyMentorPage.ts new file mode 100644 index 00000000..09d2508c --- /dev/null +++ b/src/apis/mentor/getMyMentorPage.ts @@ -0,0 +1,18 @@ +import { AxiosError } from "axios"; + +import { MentorCardPreview, MentorQueryKeys, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 멘토 마이 프로필 조회 훅 + */ +const useGetMentorMyProfile = () => { + return useQuery({ + queryKey: [MentorQueryKeys.myMentorProfile], + queryFn: mentorApi.getMentorMyProfile, + staleTime: 1000 * 60 * 5, // 5분간 캐시 + }); +}; + +export default useGetMentorMyProfile; diff --git a/src/apis/mentor/getReceivedMentorings.ts b/src/apis/mentor/getReceivedMentorings.ts new file mode 100644 index 00000000..a193248b --- /dev/null +++ b/src/apis/mentor/getReceivedMentorings.ts @@ -0,0 +1,26 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, MentoringItem, MentoringListResponse, mentorApi } from "./api"; + +import { useInfiniteQuery } from "@tanstack/react-query"; + +const OFFSET = 5; + +/** + * @description 받은 멘토링 목록 조회 훅 (무한 스크롤) + */ +const useGetMentoringList = ({ size = OFFSET }: { size?: number } = {}) => { + return useInfiniteQuery({ + queryKey: [MentorQueryKeys.mentoringList, size], + queryFn: ({ pageParam = 0 }) => mentorApi.getMentoringList(pageParam, size), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + return lastPage.nextPageNumber !== -1 ? lastPage.nextPageNumber : undefined; + }, + refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 + staleTime: 1000 * 60 * 5, // fresh 상태 유지 + select: (data) => data.pages.flatMap((page) => page.content), + }); +}; + +export default useGetMentoringList; diff --git a/src/apis/mentor/getUnconfirmedMentoringCount.ts b/src/apis/mentor/getUnconfirmedMentoringCount.ts new file mode 100644 index 00000000..9a1d4cc5 --- /dev/null +++ b/src/apis/mentor/getUnconfirmedMentoringCount.ts @@ -0,0 +1,21 @@ +import { AxiosError } from "axios"; + +import { GetMentoringNewCountResponse, MentorQueryKeys, mentorApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 미확인 멘토링 수 조회 훅 + */ +const useGetMentoringUncheckedCount = (isEnable: boolean) => { + return useQuery({ + queryKey: [MentorQueryKeys.mentoringNewCount], + queryFn: mentorApi.getMentoringUncheckedCount, + enabled: isEnable, + refetchInterval: 1000 * 60 * 10, // ⏱️ 10분마다 자동 재요청 + staleTime: 1000 * 60 * 5, // fresh 상태 유지 + select: (data) => data.uncheckedCount, + }); +}; + +export default useGetMentoringUncheckedCount; diff --git a/src/apis/mentor/index.ts b/src/apis/mentor/index.ts new file mode 100644 index 00000000..cd1e34f2 --- /dev/null +++ b/src/apis/mentor/index.ts @@ -0,0 +1,29 @@ +export { mentorApi, MentorQueryKeys } from "./api"; +export type { + MentorCardPreview, + MentorCardDetail, + MentoringItem, + MentoringApprovalStatus, + MentoringListItem, + VerifyStatus, + PutMyMentorProfileRequest, + PostMentorApplicationRequest, +} from "./api"; + +// Mentor (멘토) hooks +export { default as useGetMentorMyProfile } from "./getMyMentorPage"; +export { default as useGetMentoringList } from "./getReceivedMentorings"; +export { default as useGetMentoringUncheckedCount } from "./getUnconfirmedMentoringCount"; +export { default as usePatchApprovalStatus } from "./patchMentoringStatus"; +export { default as usePatchMentorCheckMentorings } from "./patchConfirmMentoring"; +export { default as usePostMentorApplication } from "./postMentorApplication"; +export { default as usePutMyMentorProfile } from "./putUpdateMyMentorPage"; + +// Mentee (멘티) hooks +export { default as useGetApplyMentoringList, usePrefetchApplyMentoringList } from "./getAppliedMentorings"; +export { default as usePatchMenteeCheckMentorings } from "./patchMenteeCheckMentorings"; +export { default as usePostApplyMentoring } from "./postApplyMentoring"; + +// Mentors (멘토 목록) hooks +export { default as useGetMentorList, usePrefetchMentorList } from "./getMentorList"; +export { default as useGetMentorDetail } from "./getMentorDetail"; diff --git a/src/apis/mentor/patchConfirmMentoring.ts b/src/apis/mentor/patchConfirmMentoring.ts new file mode 100644 index 00000000..f61c2875 --- /dev/null +++ b/src/apis/mentor/patchConfirmMentoring.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, PatchCheckMentoringsRequest, PatchCheckMentoringsResponse, mentorApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 멘토 멘토링 확인 처리 훅 + */ +const usePatchMentorCheckMentorings = () => { + const queriesClient = useQueryClient(); + return useMutation({ + onSuccess: () => { + // 멘토링 체크 상태 변경 후 멘토링 목록 쿼리 무효화 + Promise.all([ + queriesClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringList] }), + queriesClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringNewCount] }), + ]); + }, + mutationFn: mentorApi.patchMentorCheckMentorings, + }); +}; + +export default usePatchMentorCheckMentorings; diff --git a/src/apis/mentor/patchMenteeCheckMentorings.ts b/src/apis/mentor/patchMenteeCheckMentorings.ts new file mode 100644 index 00000000..4308b11c --- /dev/null +++ b/src/apis/mentor/patchMenteeCheckMentorings.ts @@ -0,0 +1,16 @@ +import { AxiosError } from "axios"; + +import { PatchCheckMentoringsRequest, PatchCheckMentoringsResponse, mentorApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 멘티 멘토링 확인 처리 훅 + */ +const usePatchMenteeCheckMentorings = () => { + return useMutation({ + mutationFn: mentorApi.patchMenteeCheckMentorings, + }); +}; + +export default usePatchMenteeCheckMentorings; diff --git a/src/api/mentor/client/usePatchApprovalStatus.ts b/src/apis/mentor/patchMentoringStatus.ts similarity index 64% rename from src/api/mentor/client/usePatchApprovalStatus.ts rename to src/apis/mentor/patchMentoringStatus.ts index 50bb68c4..1594dbd7 100644 --- a/src/api/mentor/client/usePatchApprovalStatus.ts +++ b/src/apis/mentor/patchMentoringStatus.ts @@ -1,44 +1,34 @@ import { useRouter } from "next/navigation"; -import { axiosInstance } from "@/utils/axiosInstance"; +import { AxiosError } from "axios"; -import { QueryKeys } from "./queryKey"; - -import { MentoringApprovalStatus } from "@/types/mentor"; +import { + MentorQueryKeys, + MentoringApprovalStatus, + PatchApprovalStatusRequest, + PatchApprovalStatusResponse, + mentorApi, +} from "./api"; import { customAlert } from "@/lib/zustand/useAlertModalStore"; import { customConfirm } from "@/lib/zustand/useConfirmModalStore"; import { IconSmile, IconUnSmile } from "@/public/svgs/mentor"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -interface UsePatchApprovalStatusRequest { - status: MentoringApprovalStatus; - mentoringId: number; // 멘토링 ID -} -interface UsePatchApprovalStatusResponse { - mentoringId: number; // 멘토링 ID - chatRoomId: number; // 채팅방 ID -} - -const patchApprovalStatus = async (props: UsePatchApprovalStatusRequest): Promise => { - const { status, mentoringId } = props; - const res = await axiosInstance.patch(`/mentor/mentorings/${mentoringId}`, { - status, - }); - return res.data; -}; - +/** + * @description 멘토링 승인/거절 훅 + */ const usePatchApprovalStatus = () => { const router = useRouter(); const queryClient = useQueryClient(); - return useMutation({ - mutationFn: patchApprovalStatus, + return useMutation({ + mutationFn: mentorApi.patchApprovalStatus, onSuccess: async (data, variables) => { // 멘토링 상태 변경 후 쿼리 무효화 await Promise.all([ - queryClient.invalidateQueries({ queryKey: [QueryKeys.mentoringList] }), - queryClient.invalidateQueries({ queryKey: [QueryKeys.mentoringNewCount] }), + queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringList] }), + queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.mentoringNewCount] }), ]); if (variables.status === MentoringApprovalStatus.REJECTED) { diff --git a/src/apis/mentor/postApplyMentoring.ts b/src/apis/mentor/postApplyMentoring.ts new file mode 100644 index 00000000..7d70022e --- /dev/null +++ b/src/apis/mentor/postApplyMentoring.ts @@ -0,0 +1,25 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, PostApplyMentoringRequest, PostApplyMentoringResponse, mentorApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 멘토링 신청 훅 + */ +const usePostApplyMentoring = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: mentorApi.postApplyMentoring, + onSuccess: async () => { + // 멘토링 신청 후 멘토 목록을 새로고침 + await queryClient.invalidateQueries({ queryKey: [MentorQueryKeys.applyMentoringList] }); + }, + onError: () => { + toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostApplyMentoring; diff --git a/src/apis/mentor/postMentorApplication.ts b/src/apis/mentor/postMentorApplication.ts new file mode 100644 index 00000000..2ef9acf3 --- /dev/null +++ b/src/apis/mentor/postMentorApplication.ts @@ -0,0 +1,20 @@ +import { AxiosError } from "axios"; + +import { PostMentorApplicationRequest, mentorApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 멘토 신청 훅 + */ +const usePostMentorApplication = () => { + return useMutation({ + mutationFn: mentorApi.postMentorApplication, + onError: (error) => { + toast.error("멘토 신청에 실패했습니다. 다시 시도해주세요."); + }, + }); +}; + +export default usePostMentorApplication; diff --git a/src/apis/mentor/putUpdateMyMentorPage.ts b/src/apis/mentor/putUpdateMyMentorPage.ts new file mode 100644 index 00000000..23164a8a --- /dev/null +++ b/src/apis/mentor/putUpdateMyMentorPage.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; + +import { MentorQueryKeys, PutMyMentorProfileRequest, mentorApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 내 멘토 프로필 수정 훅 + */ +const usePutMyMentorProfile = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: mentorApi.putMyMentorProfile, + onSuccess: () => { + // 멘토 프로필 데이터를 stale로 만들어 다음 요청 시 새로운 데이터를 가져오도록 함 + queryClient.invalidateQueries({ + queryKey: [MentorQueryKeys.myMentorProfile], + }); + }, + }); +}; + +export default usePutMyMentorProfile; diff --git a/src/apis/news/api.ts b/src/apis/news/api.ts new file mode 100644 index 00000000..8266572f --- /dev/null +++ b/src/apis/news/api.ts @@ -0,0 +1,109 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ArticleFormData } from "@/components/mentor/ArticleBottomSheetModal/lib/schema"; + +import { Article } from "@/types/news"; + +// ====== Query Keys ====== +export const NewsQueryKeys = { + articleList: "articleList", + postAddArticle: "postAddArticle", + putModifyArticle: "putModifyArticle", +} as const; + +// ====== Types ====== +export interface ArticleListResponse { + newsResponseList: Article[]; +} + +export interface PostArticleLikeResponse { + isLiked: boolean; + likeCount: number; +} + +export interface DeleteArticleLikeResponse { + isLiked: boolean; + likeCount: number; +} + +export type UsePostAddArticleRequest = ArticleFormData; + +export type UsePutModifyArticleRequest = { + body: ArticleFormData & { isImageDeleted?: boolean }; + articleId: number; +}; + +// ====== API Functions ====== +export const newsApi = { + /** + * 아티클 목록 조회 + */ + getArticleList: async (userId: number): Promise => { + const response: AxiosResponse = await axiosInstance.get(`/news?author-id=${userId}`); + return response.data; + }, + + /** + * 아티클 추가 + */ + postAddArticle: async (body: UsePostAddArticleRequest): Promise
=> { + const newsCreateRequest = { + title: body.title, + description: body.description, + url: body.url || "", + }; + + const formData = new FormData(); + formData.append("newsCreateRequest", new Blob([JSON.stringify(newsCreateRequest)], { type: "application/json" })); + if (body.file) { + formData.append("file", body.file); + } + const response: AxiosResponse
= await axiosInstance.post("/news", formData); + return response.data; + }, + + /** + * 아티클 수정 + */ + putModifyArticle: async (props: UsePutModifyArticleRequest): Promise
=> { + const { body, articleId } = props; + const newsUpdateRequest = { + title: body.title, + description: body.description, + url: body.url || "", + resetToDefaultImage: body.isImageDeleted === true, + }; + const formData = new FormData(); + formData.append("newsUpdateRequest", new Blob([JSON.stringify(newsUpdateRequest)], { type: "application/json" })); + if (body.file) formData.append("file", body.file); + + const response: AxiosResponse
= await axiosInstance.put(`/news/${articleId}`, formData); + return response.data; + }, + + /** + * 아티클 삭제 + */ + deleteArticle: async (articleId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}`); + return response.data; + }, + + /** + * 아티클 좋아요 + */ + postArticleLike: async (articleId: number): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/news/${articleId}/like`); + return response.data; + }, + + /** + * 아티클 좋아요 취소 + */ + deleteArticleLike: async (articleId: number): Promise => { + const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}/like`); + return response.data; + }, +}; diff --git a/src/apis/news/deleteLikeNews.ts b/src/apis/news/deleteLikeNews.ts new file mode 100644 index 00000000..c6a58371 --- /dev/null +++ b/src/apis/news/deleteLikeNews.ts @@ -0,0 +1,54 @@ +import { AxiosError } from "axios"; + +import { ArticleListResponse, DeleteArticleLikeResponse, NewsQueryKeys, newsApi } from "./api"; + +import { Article } from "@/types/news"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +type ArticleLikeMutationContext = { + previousArticleList?: ArticleListResponse; +}; + +/** + * @description 아티클 좋아요 취소 훅 + */ +const useDeleteArticleLike = (userId: number | null) => { + const queryClient = useQueryClient(); + const queryKey = [NewsQueryKeys.articleList, userId]; + + return useMutation, number, ArticleLikeMutationContext>({ + mutationFn: newsApi.deleteArticleLike, + + onMutate: async (unlikedArticleId) => { + await queryClient.cancelQueries({ queryKey }); + + const previousArticleList = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (oldData) => { + if (!oldData) return { newsResponseList: [] }; + return { + newsResponseList: oldData.newsResponseList.map((article) => + article.id === unlikedArticleId + ? { + ...article, + isLiked: false, + likeCount: Math.max(0, (article.likeCount ?? 1) - 1), + } + : article, + ), + }; + }); + + return { previousArticleList }; + }, + + onError: (err, variables, context) => { + if (context?.previousArticleList) { + queryClient.setQueryData(queryKey, context.previousArticleList); + } + }, + }); +}; + +export default useDeleteArticleLike; diff --git a/src/api/news/client/useDeleteArticle.ts b/src/apis/news/deleteNews.ts similarity index 55% rename from src/api/news/client/useDeleteArticle.ts rename to src/apis/news/deleteNews.ts index 81e4f936..104bf8b6 100644 --- a/src/api/news/client/useDeleteArticle.ts +++ b/src/apis/news/deleteNews.ts @@ -1,45 +1,32 @@ -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { QueryKeys } from "./queryKey"; +import { ArticleListResponse, NewsQueryKeys, newsApi } from "./api"; import { Article } from "@/types/news"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -// Article 타입 import 필요 - -// 1. 롤백을 위해 이전 데이터를 저장할 context 타입을 정의합니다. type ArticleDeleteMutationContext = { previousArticleList?: Article[]; }; -const deleteArticle = async (articleId: number): Promise => { - const response: AxiosResponse = await axiosInstance.delete(`/news/${articleId}`); - return response.data; -}; - -// 2. 어떤 유저의 목록을 업데이트할지 식별하기 위해 userId를 props로 받습니다. +/** + * @description 아티클 삭제 훅 + */ const useDeleteArticle = (userId: number | null) => { const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; + const queryKey = [NewsQueryKeys.articleList, userId]; - return useMutation< - void, // 성공 시 반환 타입 - AxiosError<{ message: string }>, // 에러 타입 - number, // mutate 함수에 전달하는 변수 타입 (articleId) - ArticleDeleteMutationContext // context 타입 - >({ - mutationFn: deleteArticle, + return useMutation, number, ArticleDeleteMutationContext>({ + mutationFn: newsApi.deleteArticle, onMutate: async (deletedArticleId) => { await queryClient.cancelQueries({ queryKey }); const previousArticleList = queryClient.getQueryData(queryKey); - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { + queryClient.setQueryData(queryKey, (oldData) => { if (!oldData) return { newsResponseList: [] }; return { newsResponseList: oldData.newsResponseList.filter((article) => article.id !== deletedArticleId), diff --git a/src/apis/news/getNewsList.ts b/src/apis/news/getNewsList.ts new file mode 100644 index 00000000..88a8b149 --- /dev/null +++ b/src/apis/news/getNewsList.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { ArticleListResponse, NewsQueryKeys, newsApi } from "./api"; + +import { Article } from "@/types/news"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 아티클 목록 조회 훅 + */ +const useGetArticleList = (userId: number) => { + return useQuery({ + queryKey: [NewsQueryKeys.articleList, userId], + queryFn: () => { + if (userId === null) { + return Promise.reject(new Error("User ID is null")); + } + return newsApi.getArticleList(userId); + }, + staleTime: 1000 * 60 * 10, // 10분 + enabled: userId !== null && userId !== 0, + select: (data) => data.newsResponseList, + }); +}; + +export default useGetArticleList; diff --git a/src/apis/news/index.ts b/src/apis/news/index.ts new file mode 100644 index 00000000..b4c7f58c --- /dev/null +++ b/src/apis/news/index.ts @@ -0,0 +1,16 @@ +export { newsApi, NewsQueryKeys } from "./api"; +export type { + ArticleListResponse, + PostArticleLikeResponse, + DeleteArticleLikeResponse, + UsePostAddArticleRequest, + UsePutModifyArticleRequest, +} from "./api"; + +// News (아티클) hooks +export { default as useGetArticleList } from "./getNewsList"; +export { default as usePostAddArticle } from "./postCreateNews"; +export { default as usePutModifyArticle } from "./putUpdateNews"; +export { default as useDeleteArticle } from "./deleteNews"; +export { default as usePostArticleLike } from "./postLikeNews"; +export { default as useDeleteArticleLike } from "./deleteLikeNews"; diff --git a/src/api/news/client/usePostAddArticle.ts b/src/apis/news/postCreateNews.ts similarity index 60% rename from src/api/news/client/usePostAddArticle.ts rename to src/apis/news/postCreateNews.ts index bee9a3db..1489c37a 100644 --- a/src/api/news/client/usePostAddArticle.ts +++ b/src/apis/news/postCreateNews.ts @@ -1,10 +1,6 @@ -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { ArticleFormData } from "@/components/mentor/ArticleBottomSheetModal/lib/schema"; - -import { QueryKeys } from "./queryKey"; +import { ArticleListResponse, NewsQueryKeys, UsePostAddArticleRequest, newsApi } from "./api"; import { Article } from "@/types/news"; @@ -13,39 +9,24 @@ import ArticleThumbUrlPng from "@/public/images/article-thumb.png"; import { useMutation, useQueryClient } from "@tanstack/react-query"; type ArticleMutationContext = { - previousArticleContainer?: { newsResponseList: Article[] }; -}; - -type UsePostAddArticleRequest = ArticleFormData; - -const postAddArticle = async (body: UsePostAddArticleRequest): Promise
=> { - const newsCreateRequest = { - title: body.title, - description: body.description, - url: body.url || "", - }; - - const formData = new FormData(); - formData.append("newsCreateRequest", new Blob([JSON.stringify(newsCreateRequest)], { type: "application/json" })); - if (body.file) { - formData.append("file", body.file); - } - const response: AxiosResponse
= await axiosInstance.post("/news", formData); - return response.data; + previousArticleContainer?: ArticleListResponse; }; +/** + * @description 아티클 추가 훅 + */ const usePostAddArticle = (userId: number | null) => { const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; + const queryKey = [NewsQueryKeys.articleList, userId]; return useMutation, UsePostAddArticleRequest, ArticleMutationContext>({ - mutationFn: postAddArticle, + mutationFn: newsApi.postAddArticle, onMutate: async (newArticle) => { await queryClient.cancelQueries({ queryKey }); - const previousArticleContainer = queryClient.getQueryData<{ newsResponseList: Article[] }>(queryKey); + const previousArticleContainer = queryClient.getQueryData(queryKey); - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { + queryClient.setQueryData(queryKey, (oldData) => { if (!oldData) return { newsResponseList: [] }; const optimisticArticle: Article = { diff --git a/src/apis/news/postLikeNews.ts b/src/apis/news/postLikeNews.ts new file mode 100644 index 00000000..d71a7e1d --- /dev/null +++ b/src/apis/news/postLikeNews.ts @@ -0,0 +1,54 @@ +import { AxiosError } from "axios"; + +import { ArticleListResponse, NewsQueryKeys, PostArticleLikeResponse, newsApi } from "./api"; + +import { Article } from "@/types/news"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +type ArticleLikeMutationContext = { + previousArticleList?: Article[]; +}; + +/** + * @description 아티클 좋아요 훅 + */ +const usePostArticleLike = (userId: number | null) => { + const queryClient = useQueryClient(); + const queryKey = [NewsQueryKeys.articleList, userId]; + + return useMutation, number, ArticleLikeMutationContext>({ + mutationFn: newsApi.postArticleLike, + + onMutate: async (likedArticleId) => { + await queryClient.cancelQueries({ queryKey }); + + const previousArticleList = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, (oldData) => { + if (!oldData) return { newsResponseList: [] }; + return { + newsResponseList: oldData.newsResponseList.map((article) => + article.id === likedArticleId + ? { + ...article, + isLiked: true, + likeCount: (article.likeCount ?? 0) + 1, + } + : article, + ), + }; + }); + + return { previousArticleList }; + }, + + onError: (err, variables, context) => { + if (context?.previousArticleList) { + queryClient.setQueryData(queryKey, context.previousArticleList); + } + }, + }); +}; + +export default usePostArticleLike; diff --git a/src/api/news/client/usePutModifyArticle.ts b/src/apis/news/putUpdateNews.ts similarity index 60% rename from src/api/news/client/usePutModifyArticle.ts rename to src/apis/news/putUpdateNews.ts index 92d00ce5..d82ad79a 100644 --- a/src/api/news/client/usePutModifyArticle.ts +++ b/src/apis/news/putUpdateNews.ts @@ -1,51 +1,30 @@ -import { AxiosError, AxiosResponse } from "axios"; +import { AxiosError } from "axios"; -import { axiosInstance } from "@/utils/axiosInstance"; - -import { ArticleFormData } from "@/components/mentor/ArticleBottomSheetModal/lib/schema"; - -import { QueryKeys } from "./queryKey"; +import { ArticleListResponse, NewsQueryKeys, UsePutModifyArticleRequest, newsApi } from "./api"; import { Article } from "@/types/news"; import { toast } from "@/lib/zustand/useToastStore"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -type UsePutModifyArticleRequest = { - body: ArticleFormData & { isImageDeleted?: boolean }; - articleId: number; -}; type ArticleMutationContext = { previousArticleList?: Article[]; }; -const putModifyArticle = async (props: UsePutModifyArticleRequest): Promise
=> { - const { body, articleId } = props; - const newsUpdateRequest = { - title: body.title, - description: body.description, - url: body.url || "", - resetToDefaultImage: body.isImageDeleted === true, - }; - const formData = new FormData(); - formData.append("newsUpdateRequest", new Blob([JSON.stringify(newsUpdateRequest)], { type: "application/json" })); - if (body.file) formData.append("file", body.file); - - const response: AxiosResponse
= await axiosInstance.put(`/news/${articleId}`, formData); - return response.data; -}; - +/** + * @description 아티클 수정 훅 + */ const usePutModifyArticle = (userId: number | null) => { const queryClient = useQueryClient(); - const queryKey = [QueryKeys.articleList, userId]; + const queryKey = [NewsQueryKeys.articleList, userId]; return useMutation, UsePutModifyArticleRequest, ArticleMutationContext>({ - mutationFn: putModifyArticle, + mutationFn: newsApi.putModifyArticle, onMutate: async (variables) => { await queryClient.cancelQueries({ queryKey }); const previousArticleList = queryClient.getQueryData(queryKey); - queryClient.setQueryData<{ newsResponseList: Article[] }>(queryKey, (oldData) => { + queryClient.setQueryData(queryKey, (oldData) => { if (!oldData) return { newsResponseList: [] }; return { diff --git a/src/apis/queryKeys.ts b/src/apis/queryKeys.ts new file mode 100644 index 00000000..3963ad5a --- /dev/null +++ b/src/apis/queryKeys.ts @@ -0,0 +1,132 @@ +/** + * React Query Keys + * Bruno 폴더 구조를 기반으로 자동 생성됨 + */ + +export const QueryKeys = { + Auth: { + folder: "Auth.folder" as const, + signOut: "Auth.signOut" as const, + appleAuth: "Auth.appleAuth" as const, + refreshToken: "Auth.refreshToken" as const, + emailLogin: "Auth.emailLogin" as const, + emailVerification: "Auth.emailVerification" as const, + kakaoAuth: "Auth.kakaoAuth" as const, + account: "Auth.account" as const, + signUp: "Auth.signUp" as const, + }, + news: { + folder: "news.folder" as const, + newsList: "news.newsList" as const, + news: "news.news" as const, + updateNews: "news.updateNews" as const, + likeNews: "news.likeNews" as const, + createNews: "news.createNews" as const, + }, + reports: { + folder: "reports.folder" as const, + report: "reports.report" as const, + }, + chat: { + folder: "chat.folder" as const, + chatMessages: "chat.chatMessages" as const, + chatRooms: "chat.chatRooms" as const, + readChatRoom: "chat.readChatRoom" as const, + chatPartner: "chat.chatPartner" as const, + }, + universities: { + folder: "universities.folder" as const, + recommendedUniversities: "universities.recommendedUniversities" as const, + wishList: "universities.wishList" as const, + wish: "universities.wish" as const, + addWish: "universities.addWish" as const, + isWish: "universities.isWish" as const, + universityDetail: "universities.universityDetail" as const, + searchText: "universities.searchText" as const, + searchFilter: "universities.searchFilter" as const, + byRegionCountry: "universities.byRegionCountry" as const, + }, + MyPage: { + folder: "MyPage.folder" as const, + interestedRegionCountry: "MyPage.interestedRegionCountry" as const, + profile: "MyPage.profile" as const, + password: "MyPage.password" as const, + }, + applications: { + folder: "applications.folder" as const, + competitors: "applications.competitors" as const, + submitApplication: "applications.submitApplication" as const, + applicants: "applications.applicants" as const, + }, + community: { + folder: "community.folder" as const, + boardList: "community.boardList" as const, + board: "community.board" as const, + comment: "community.comment" as const, + updateComment: "community.updateComment" as const, + createComment: "community.createComment" as const, + post: "community.post" as const, + updatePost: "community.updatePost" as const, + createPost: "community.createPost" as const, + postDetail: "community.postDetail" as const, + likePost: "community.likePost" as const, + }, + Scores: { + folder: "Scores.folder" as const, + createLanguageTest: "Scores.createLanguageTest" as const, + languageTestList: "Scores.languageTestList" as const, + createGpa: "Scores.createGpa" as const, + gpaList: "Scores.gpaList" as const, + }, + Admin: { + folder: "Admin.folder" as const, + verifyLanguageTest: "Admin.verifyLanguageTest" as const, + languageTestList: "Admin.languageTestList" as const, + verifyGpa: "Admin.verifyGpa" as const, + gpaList: "Admin.gpaList" as const, + }, + users: { + folder: "users.folder" as const, + nicknameExists: "users.nicknameExists" as const, + blockUser: "users.blockUser" as const, + unblockUser: "users.unblockUser" as const, + blockedUsers: "users.blockedUsers" as const, + }, + mentor: { + folder: "mentor.folder" as const, + matchedMentors: "mentor.matchedMentors" as const, + applyMentoring: "mentor.applyMentoring" as const, + confirmMentoring: "mentor.confirmMentoring" as const, + appliedMentorings: "mentor.appliedMentorings" as const, + mentorList: "mentor.mentorList" as const, + mentorDetail: "mentor.mentorDetail" as const, + myMentorPage: "mentor.myMentorPage" as const, + updateMyMentorPage: "mentor.updateMyMentorPage" as const, + mentoringStatus: "mentor.mentoringStatus" as const, + receivedMentorings: "mentor.receivedMentorings" as const, + unconfirmedMentoringCount: "mentor.unconfirmedMentoringCount" as const, + }, + "kakao-api": { + folder: "kakao-api.folder" as const, + kakaoUserIds: "kakao-api.kakaoUserIds" as const, + kakaoUnlink: "kakao-api.kakaoUnlink" as const, + kakaoInfo: "kakao-api.kakaoInfo" as const, + }, + "collection.bru": { + collection: "collection.bru.collection" as const, + }, + environments: { + dev: "environments.dev" as const, + local: "environments.local" as const, + }, + "image-upload": { + folder: "image-upload.folder" as const, + slackNotification: "image-upload.slackNotification" as const, + uploadLanguageTestReport: "image-upload.uploadLanguageTestReport" as const, + uploadProfileImage: "image-upload.uploadProfileImage" as const, + uploadProfileImageBeforeSignup: "image-upload.uploadProfileImageBeforeSignup" as const, + uploadGpaReport: "image-upload.uploadGpaReport" as const, + }, +} as const; + +export type QueryKey = (typeof QueryKeys)[keyof typeof QueryKeys]; diff --git a/src/apis/reports/api.ts b/src/apis/reports/api.ts new file mode 100644 index 00000000..66f3b2b0 --- /dev/null +++ b/src/apis/reports/api.ts @@ -0,0 +1,23 @@ +import { AxiosResponse } from "axios"; + +import { axiosInstance } from "@/utils/axiosInstance"; + +import { ReportType } from "@/types/reports"; + +// ====== Types ====== +export interface UsePostReportsRequest { + targetType: "POST"; // 지금은 게시글 신고 기능만 존재 + targetId: number; // 신고하려는 리소스의 ID + reportType: ReportType; +} + +// ====== API Functions ====== +export const reportsApi = { + /** + * 신고 등록 + */ + postReport: async (body: UsePostReportsRequest): Promise => { + const response: AxiosResponse = await axiosInstance.post(`/reports`, body); + return response.data; + }, +}; diff --git a/src/apis/reports/index.ts b/src/apis/reports/index.ts new file mode 100644 index 00000000..8baf4cb5 --- /dev/null +++ b/src/apis/reports/index.ts @@ -0,0 +1,3 @@ +export { reportsApi } from "./api"; +export type { UsePostReportsRequest } from "./api"; +export { default as usePostReports } from "./postReport"; diff --git a/src/apis/reports/postReport.ts b/src/apis/reports/postReport.ts new file mode 100644 index 00000000..0e4665c0 --- /dev/null +++ b/src/apis/reports/postReport.ts @@ -0,0 +1,27 @@ +import { useRouter } from "next/navigation"; + +import { AxiosError } from "axios"; + +import { UsePostReportsRequest, reportsApi } from "./api"; + +import { toast } from "@/lib/zustand/useToastStore"; +import { useMutation } from "@tanstack/react-query"; + +/** + * @description 신고 등록 훅 + */ +const usePostReports = () => { + const router = useRouter(); + return useMutation, UsePostReportsRequest>({ + mutationFn: reportsApi.postReport, + onSuccess: () => { + toast.success("신고가 성공적으로 등록되었습니다."); + router.back(); + }, + onError: (error) => { + toast.error("신고 등록에 실패했습니다. 잠시 후 다시 시도해주세요."); + }, + }); +}; + +export default usePostReports; diff --git a/src/apis/universities/api.ts b/src/apis/universities/api.ts new file mode 100644 index 00000000..3a8b70aa --- /dev/null +++ b/src/apis/universities/api.ts @@ -0,0 +1,193 @@ +import { axiosInstance, publicAxiosInstance } from "@/utils/axiosInstance"; + +export interface RecommendedUniversitiesResponseRecommendedUniversitiesItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: RecommendedUniversitiesResponseRecommendedUniversitiesItemLanguageRequirementsItem[]; +} + +export interface RecommendedUniversitiesResponseRecommendedUniversitiesItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface RecommendedUniversitiesResponse { + recommendedUniversities: RecommendedUniversitiesResponseRecommendedUniversitiesItem[]; +} + +export interface WishListResponseItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: WishListResponseItemLanguageRequirementsItem[]; +} + +export interface WishListResponseItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface WishListResponse { + 0: WishListResponseItem[]; + 1: WishListResponseItem[]; +} + +export type WishResponse = void; + +export type AddWishResponse = void; + +export type AddWishRequest = Record; + +export type IsWishResponse = void; + +export interface UniversityDetailResponseLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface UniversityDetailResponse { + id: number; + term: string; + koreanName: string; + englishName: string; + formatName: string; + region: string; + country: string; + homepageUrl: string; + logoImageUrl: string; + backgroundImageUrl: string; + detailsForLocal: string; + studentCapacity: number; + tuitionFeeType: string; + semesterAvailableForDispatch: string; + languageRequirements: UniversityDetailResponseLanguageRequirementsItem[]; + detailsForLanguage: string; + gpaRequirement: string; + gpaRequirementCriteria: string; + semesterRequirement: string; + detailsForApply: null; + detailsForMajor: string; + detailsForAccommodation: null; + detailsForEnglishCourse: null; + details: string; + accommodationUrl: string; + englishCourseUrl: string; +} + +export interface SearchTextResponseUnivApplyInfoPreviewsItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: SearchTextResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem[]; +} + +export interface SearchTextResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface SearchTextResponse { + univApplyInfoPreviews: SearchTextResponseUnivApplyInfoPreviewsItem[]; +} + +export interface SearchFilterResponseUnivApplyInfoPreviewsItem { + id: number; + term: string; + koreanName: string; + region: string; + country: string; + logoImageUrl: string; + backgroundImageUrl: string; + studentCapacity: number; + languageRequirements: SearchFilterResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem[]; +} + +export interface SearchFilterResponseUnivApplyInfoPreviewsItemLanguageRequirementsItem { + languageTestType: string; + minScore: string; +} + +export interface SearchFilterResponse { + univApplyInfoPreviews: SearchFilterResponseUnivApplyInfoPreviewsItem[]; +} + +export type ByRegionCountryResponse = void; + +export const universitiesApi = { + getRecommendedUniversities: async (params?: { isLogin?: boolean }): Promise => { + const instance = params?.isLogin ? axiosInstance : publicAxiosInstance; + const res = await instance.get(`/univ-apply-infos/recommend`); + return res.data; + }, + + getWishList: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/univ-apply-infos/like`, { params: params?.params }); + return res.data; + }, + + deleteWish: async (params: { univApplyInfoId: string | number }): Promise => { + const res = await axiosInstance.delete(`/univ-apply-infos/${params.univApplyInfoId}/like`); + return res.data; + }, + + postAddWish: async (params: { + univApplyInfoId: string | number; + data?: AddWishRequest; + }): Promise => { + const res = await axiosInstance.post( + `/univ-apply-infos/${params.univApplyInfoId}/like`, + params?.data, + ); + return res.data; + }, + + getIsWish: async (params: { + univApplyInfoId: string | number; + params?: Record; + }): Promise => { + const res = await axiosInstance.get(`/univ-apply-infos/${params.univApplyInfoId}/like`, { + params: params?.params, + }); + return res.data; + }, + + getUniversityDetail: async (params: { univApplyInfoId: string | number }): Promise => { + const res = await publicAxiosInstance.get(`/univ-apply-infos/${params.univApplyInfoId}`); + return res.data; + }, + + getSearchText: async (params?: { value?: string }): Promise => { + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/text`, { + params: { value: params?.value ?? "" }, + }); + return res.data; + }, + + getSearchFilter: async (params?: { params?: Record }): Promise => { + const res = await publicAxiosInstance.get(`/univ-apply-infos/search/filter`, { + params: params?.params, + }); + return res.data; + }, + + getByRegionCountry: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/universities/search`, { params: params?.params }); + return res.data; + }, +}; diff --git a/src/apis/universities/deleteWish.ts b/src/apis/universities/deleteWish.ts new file mode 100644 index 00000000..5e3fc309 --- /dev/null +++ b/src/apis/universities/deleteWish.ts @@ -0,0 +1,22 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { WishResponse, universitiesApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 위시리스트에서 학교를 삭제하는 useMutation 커스텀 훅 + */ +const useDeleteWish = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (universityInfoForApplyId) => universitiesApi.deleteWish({ univApplyInfoId: universityInfoForApplyId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] }); + }, + }); +}; + +export default useDeleteWish; diff --git a/src/apis/universities/getByRegionCountry.ts b/src/apis/universities/getByRegionCountry.ts new file mode 100644 index 00000000..29f34b56 --- /dev/null +++ b/src/apis/universities/getByRegionCountry.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { ByRegionCountryResponse, universitiesApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetByRegionCountry = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.universities.byRegionCountry, params], + queryFn: () => universitiesApi.getByRegionCountry(params ? { params } : {}), + }); +}; + +export default useGetByRegionCountry; diff --git a/src/apis/universities/getIsWish.ts b/src/apis/universities/getIsWish.ts new file mode 100644 index 00000000..f89e3f25 --- /dev/null +++ b/src/apis/universities/getIsWish.ts @@ -0,0 +1,16 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { IsWishResponse, universitiesApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetIsWish = (univApplyInfoId: string | number, params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.universities.isWish, univApplyInfoId, params], + queryFn: () => universitiesApi.getIsWish({ univApplyInfoId, params }), + enabled: !!univApplyInfoId, + }); +}; + +export default useGetIsWish; diff --git a/src/apis/universities/getRecommendedUniversities.ts b/src/apis/universities/getRecommendedUniversities.ts new file mode 100644 index 00000000..a316317e --- /dev/null +++ b/src/apis/universities/getRecommendedUniversities.ts @@ -0,0 +1,27 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { RecommendedUniversitiesResponse, universitiesApi } from "./api"; + +import { ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +type UseGetRecommendedUniversitiesParams = { + isLogin: boolean; +}; + +/** + * @description 추천 대학 목록 조회를 위한 useQuery 커스텀 훅 + * @param params.isLogin - 로그인 여부 (인스턴스 결정에 사용) + */ +const useGetRecommendedUniversities = ({ isLogin }: UseGetRecommendedUniversitiesParams) => { + return useQuery({ + queryKey: [QueryKeys.universities.recommendedUniversities, isLogin], + queryFn: () => universitiesApi.getRecommendedUniversities({ isLogin }), + staleTime: 1000 * 60 * 5, + select: (data) => data.recommendedUniversities as unknown as ListUniversity[], + }); +}; + +export default useGetRecommendedUniversities; diff --git a/src/apis/universities/getSearchFilter.ts b/src/apis/universities/getSearchFilter.ts new file mode 100644 index 00000000..09b9f3c7 --- /dev/null +++ b/src/apis/universities/getSearchFilter.ts @@ -0,0 +1,47 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { SearchFilterResponse, universitiesApi } from "./api"; + +import { CountryCode, LanguageTestType, ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +export interface UniversitySearchFilterParams { + languageTestType?: LanguageTestType; + testScore?: number; + countryCode?: CountryCode[]; +} + +/** + * @description 필터로 대학 검색을 위한 useQuery 커스텀 훅 + * @param filters - 검색 필터 파라미터 + */ +const useGetUniversitySearchByFilter = (filters: UniversitySearchFilterParams) => { + // 필터 파라미터 구성 + const buildParams = () => { + const params: Record = {}; + if (filters.languageTestType) { + params.languageTestType = filters.languageTestType; + } + if (filters.testScore !== undefined) { + params.testScore = String(filters.testScore); + } + if (filters.countryCode && filters.countryCode.length > 0) { + params.countryCode = filters.countryCode; + } + return params; + }; + + return useQuery({ + queryKey: [QueryKeys.universities.searchFilter, filters], + queryFn: () => universitiesApi.getSearchFilter({ params: buildParams() }), + enabled: Object.values(filters).some((value) => { + if (Array.isArray(value)) return value.length > 0; + return value !== undefined && value !== ""; + }), + select: (data) => data.univApplyInfoPreviews as unknown as ListUniversity[], + }); +}; + +export default useGetUniversitySearchByFilter; diff --git a/src/apis/universities/getSearchText.ts b/src/apis/universities/getSearchText.ts new file mode 100644 index 00000000..e0cb52ce --- /dev/null +++ b/src/apis/universities/getSearchText.ts @@ -0,0 +1,56 @@ +import { useMemo } from "react"; + +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { SearchTextResponse, universitiesApi } from "./api"; + +import { ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 대학 검색을 위한 useQuery 커스텀 훅 + * 모든 대학 데이터를 한 번만 가져와 캐싱하고, 검색어에 따라 클라이언트에서 필터링합니다. + * @param searchValue - 검색어 + */ +const useUniversitySearch = (searchValue: string) => { + // 1. 모든 대학 데이터를 한 번만 가져와 'Infinity' 캐시로 저장합니다. + const { + data: allUniversities, + isLoading, + isError, + error, + } = useQuery({ + queryKey: [QueryKeys.universities.searchText], + queryFn: () => universitiesApi.getSearchText({ value: "" }), + staleTime: Infinity, + gcTime: Infinity, + select: (data) => data.univApplyInfoPreviews as unknown as ListUniversity[], + }); + + // 2. 검색어가 변경될 때만 캐시된 데이터를 필터링합니다. + const filteredUniversities = useMemo(() => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + + if (!normalizedSearchValue) { + return allUniversities; + } + + if (!allUniversities) { + return []; + } + + return allUniversities.filter((university) => university.koreanName.toLowerCase().includes(normalizedSearchValue)); + }, [allUniversities, searchValue]); + + return { + data: filteredUniversities, + isLoading, + isError, + error, + totalCount: allUniversities?.length || 0, + }; +}; + +export default useUniversitySearch; diff --git a/src/apis/universities/getUniversityDetail.ts b/src/apis/universities/getUniversityDetail.ts new file mode 100644 index 00000000..ce2ff998 --- /dev/null +++ b/src/apis/universities/getUniversityDetail.ts @@ -0,0 +1,23 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { UniversityDetailResponse, universitiesApi } from "./api"; + +import { University } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 대학 상세 조회를 위한 useQuery 커스텀 훅 + * @param universityInfoForApplyId - 대학 ID + */ +const useGetUniversityDetail = (universityInfoForApplyId: number) => { + return useQuery({ + queryKey: [QueryKeys.universities.universityDetail, universityInfoForApplyId], + queryFn: () => universitiesApi.getUniversityDetail({ univApplyInfoId: universityInfoForApplyId }), + enabled: !!universityInfoForApplyId, + select: (data) => data as unknown as University, + }); +}; + +export default useGetUniversityDetail; diff --git a/src/apis/universities/getWishList.ts b/src/apis/universities/getWishList.ts new file mode 100644 index 00000000..64fc2953 --- /dev/null +++ b/src/apis/universities/getWishList.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { WishListResponse, universitiesApi } from "./api"; + +import { ListUniversity } from "@/types/university"; + +import { useQuery } from "@tanstack/react-query"; + +/** + * @description 내 위시리스트 대학 목록 조회를 위한 useQuery 커스텀 훅 + * @param enabled - 쿼리 활성화 여부 + */ +const useGetWishList = (enabled: boolean = true) => { + return useQuery({ + queryKey: [QueryKeys.universities.wishList], + queryFn: () => universitiesApi.getWishList({}), + staleTime: 1000 * 60 * 5, + select: (data) => data as unknown as ListUniversity[], + enabled, + }); +}; + +export default useGetWishList; diff --git a/src/apis/universities/index.ts b/src/apis/universities/index.ts new file mode 100644 index 00000000..95a44032 --- /dev/null +++ b/src/apis/universities/index.ts @@ -0,0 +1,10 @@ +export { universitiesApi } from "./api"; +export { default as useDeleteWish } from "./deleteWish"; +export { default as useGetByRegionCountry } from "./getByRegionCountry"; +export { default as useGetIsWish } from "./getIsWish"; +export { default as useGetRecommendedUniversities } from "./getRecommendedUniversities"; +export { default as useGetUniversitySearchByFilter, type UniversitySearchFilterParams } from "./getSearchFilter"; +export { default as useUniversitySearch } from "./getSearchText"; +export { default as useGetUniversityDetail } from "./getUniversityDetail"; +export { default as useGetWishList } from "./getWishList"; +export { default as usePostAddWish } from "./postAddWish"; diff --git a/src/apis/universities/postAddWish.ts b/src/apis/universities/postAddWish.ts new file mode 100644 index 00000000..4f7f22bd --- /dev/null +++ b/src/apis/universities/postAddWish.ts @@ -0,0 +1,26 @@ +import { AxiosError } from "axios"; + +import { createMutationErrorHandler } from "@/utils/errorHandler"; + +import { QueryKeys } from "../queryKeys"; +import { AddWishResponse, universitiesApi } from "./api"; + +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +/** + * @description 위시리스트에 학교를 추가하는 useMutation 커스텀 훅 + */ +const usePostAddWish = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (universityInfoForApplyId) => + universitiesApi.postAddWish({ univApplyInfoId: universityInfoForApplyId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] }); + }, + onError: createMutationErrorHandler("위시리스트 추가에 실패했습니다."), + }); +}; + +export default usePostAddWish; diff --git a/src/api/university/server/getRecommendedUniversity.ts b/src/apis/universities/server/getRecommendedUniversity.ts similarity index 100% rename from src/api/university/server/getRecommendedUniversity.ts rename to src/apis/universities/server/getRecommendedUniversity.ts diff --git a/src/api/university/server/getSearchUniversitiesByFilter.ts b/src/apis/universities/server/getSearchUniversitiesByFilter.ts similarity index 100% rename from src/api/university/server/getSearchUniversitiesByFilter.ts rename to src/apis/universities/server/getSearchUniversitiesByFilter.ts diff --git a/src/api/university/server/getSearchUniversitiesByText.ts b/src/apis/universities/server/getSearchUniversitiesByText.ts similarity index 100% rename from src/api/university/server/getSearchUniversitiesByText.ts rename to src/apis/universities/server/getSearchUniversitiesByText.ts diff --git a/src/api/university/server/getUniversityDetail.ts b/src/apis/universities/server/getUniversityDetail.ts similarity index 100% rename from src/api/university/server/getUniversityDetail.ts rename to src/apis/universities/server/getUniversityDetail.ts diff --git a/src/apis/universities/server/index.ts b/src/apis/universities/server/index.ts new file mode 100644 index 00000000..e98228fd --- /dev/null +++ b/src/apis/universities/server/index.ts @@ -0,0 +1,9 @@ +// Server-side exports +export { default as getRecommendedUniversity } from "./getRecommendedUniversity"; +export { getUniversityDetail } from "./getUniversityDetail"; +export { getUniversitiesByText, getAllUniversities, getCategorizedUniversities } from "./getSearchUniversitiesByText"; +export { + getSearchUniversitiesByFilter, + getSearchUniversitiesAllRegions, + type UniversitySearchFilterParams, +} from "./getSearchUniversitiesByFilter"; diff --git a/src/apis/users/api.ts b/src/apis/users/api.ts new file mode 100644 index 00000000..5c69840d --- /dev/null +++ b/src/apis/users/api.ts @@ -0,0 +1,52 @@ +import { axiosInstance } from "@/utils/axiosInstance"; + +export interface NicknameExistsResponse { + exists: boolean; +} + +export type BlockUserResponse = void; + +export type BlockUserRequest = Record; + +export type UnblockUserRequest = Record; + +export type UnblockUserResponse = void; + +export interface BlockedUsersResponseContentItem { + id: number; + blockedId: number; + nickname: string; + createdAt: string; +} + +export interface BlockedUsersResponse { + content: BlockedUsersResponseContentItem[]; + nextPageNumber: number; +} + +export const usersApi = { + getNicknameExists: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/users/exists?nickname=abc`, { + params: params?.params, + }); + return res.data; + }, + + postBlockUser: async (params: { + blockedId: string | number; + data?: BlockUserRequest; + }): Promise => { + const res = await axiosInstance.post(`/users/block/${params.blockedId}`, params?.data); + return res.data; + }, + + deleteUnblockUser: async (params: { blockedId: string | number }): Promise => { + const res = await axiosInstance.delete(`/users/block/${params.blockedId}`); + return res.data; + }, + + getBlockedUsers: async (params: { params?: Record }): Promise => { + const res = await axiosInstance.get(`/users/blocks`, { params: params?.params }); + return res.data; + }, +}; diff --git a/src/apis/users/deleteUnblockUser.ts b/src/apis/users/deleteUnblockUser.ts new file mode 100644 index 00000000..88cb8451 --- /dev/null +++ b/src/apis/users/deleteUnblockUser.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { UnblockUserRequest, UnblockUserResponse, usersApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const useDeleteUnblockUser = () => { + return useMutation({ + mutationFn: (variables) => usersApi.deleteUnblockUser(variables), + }); +}; + +export default useDeleteUnblockUser; diff --git a/src/apis/users/getBlockedUsers.ts b/src/apis/users/getBlockedUsers.ts new file mode 100644 index 00000000..4e33e910 --- /dev/null +++ b/src/apis/users/getBlockedUsers.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { BlockedUsersResponse, usersApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetBlockedUsers = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.users.blockedUsers, params], + queryFn: () => usersApi.getBlockedUsers(params ? { params } : {}), + }); +}; + +export default useGetBlockedUsers; diff --git a/src/apis/users/getNicknameExists.ts b/src/apis/users/getNicknameExists.ts new file mode 100644 index 00000000..fc5ff88a --- /dev/null +++ b/src/apis/users/getNicknameExists.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; + +import { QueryKeys } from "../queryKeys"; +import { NicknameExistsResponse, usersApi } from "./api"; + +import { useQuery } from "@tanstack/react-query"; + +const useGetNicknameExists = (params?: Record) => { + return useQuery({ + queryKey: [QueryKeys.users.nicknameExists, params], + queryFn: () => usersApi.getNicknameExists(params ? { params } : {}), + }); +}; + +export default useGetNicknameExists; diff --git a/src/apis/users/index.ts b/src/apis/users/index.ts new file mode 100644 index 00000000..ff51c891 --- /dev/null +++ b/src/apis/users/index.ts @@ -0,0 +1,5 @@ +export { usersApi } from "./api"; +export { default as deleteUnblockUser } from "./deleteUnblockUser"; +export { default as getBlockedUsers } from "./getBlockedUsers"; +export { default as getNicknameExists } from "./getNicknameExists"; +export { default as postBlockUser } from "./postBlockUser"; diff --git a/src/apis/users/postBlockUser.ts b/src/apis/users/postBlockUser.ts new file mode 100644 index 00000000..027b49e0 --- /dev/null +++ b/src/apis/users/postBlockUser.ts @@ -0,0 +1,13 @@ +import { AxiosError } from "axios"; + +import { BlockUserRequest, BlockUserResponse, usersApi } from "./api"; + +import { useMutation } from "@tanstack/react-query"; + +const usePostBlockUser = () => { + return useMutation({ + mutationFn: (variables) => usersApi.postBlockUser(variables), + }); +}; + +export default usePostBlockUser; diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 1cba4994..8b6c4c09 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -7,8 +7,7 @@ import NewsSectionSkeleton from "./_ui/NewsSection/skeleton"; import PopularUniversitySection from "./_ui/PopularUniversitySection"; import UniversityList from "./_ui/UniversityList"; -import getRecommendedUniversity from "@/api/university/server/getRecommendedUniversity"; -import { getCategorizedUniversities } from "@/api/university/server/getSearchUniversitiesByText"; +import { getCategorizedUniversities, getRecommendedUniversity } from "@/apis/universities/server"; import { fetchAllNews } from "@/lib/firebaseNews"; import { IconIdCard, IconMagnifyingGlass, IconMuseum, IconPaper } from "@/public/svgs/home"; diff --git a/src/app/community/[boardCode]/CommunityPageContent.tsx b/src/app/community/[boardCode]/CommunityPageContent.tsx index cd11803f..286a8f13 100644 --- a/src/app/community/[boardCode]/CommunityPageContent.tsx +++ b/src/app/community/[boardCode]/CommunityPageContent.tsx @@ -11,7 +11,7 @@ import PostWriteButton from "./PostWriteButton"; import { COMMUNITY_BOARDS, COMMUNITY_CATEGORIES } from "@/constants/community"; -import useGetPostList from "@/api/boards/clients/useGetPostList"; +import { useGetPostList } from "@/apis/community"; interface CommunityPageContentProps { boardCode: string; diff --git a/src/app/community/[boardCode]/[postId]/CommentInput.tsx b/src/app/community/[boardCode]/[postId]/CommentInput.tsx index 67c18a60..c3b219ad 100644 --- a/src/app/community/[boardCode]/[postId]/CommentInput.tsx +++ b/src/app/community/[boardCode]/[postId]/CommentInput.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; -import useCreateComment from "@/api/community/client/useCreateComment"; +import { useCreateComment } from "@/apis/community"; import { IconCloseFilled, IconFlight } from "@/public/svgs"; type CommentInputProps = { diff --git a/src/app/community/[boardCode]/[postId]/CommentSection.tsx b/src/app/community/[boardCode]/[postId]/CommentSection.tsx index 8aa1fc27..66ee10cd 100644 --- a/src/app/community/[boardCode]/[postId]/CommentSection.tsx +++ b/src/app/community/[boardCode]/[postId]/CommentSection.tsx @@ -14,7 +14,7 @@ import CommentInput from "./CommentInput"; import { Comment as CommentType, CommunityUser } from "@/types/community"; -import useDeleteComment from "@/api/community/client/useDeleteComment"; +import { useDeleteComment } from "@/apis/community"; import { IconMoreVertFilled, IconSubComment } from "@/public/svgs"; type CommentSectionProps = { diff --git a/src/app/community/[boardCode]/[postId]/Content.tsx b/src/app/community/[boardCode]/[postId]/Content.tsx index 9b6b4c90..895977f1 100644 --- a/src/app/community/[boardCode]/[postId]/Content.tsx +++ b/src/app/community/[boardCode]/[postId]/Content.tsx @@ -9,8 +9,7 @@ import LinkifyText from "@/components/ui/LinkifyText"; import { PostImage as PostImageType, Post as PostType } from "@/types/community"; -import useDeleteLike from "@/api/community/client/useDeleteLike"; -import usePostLike from "@/api/community/client/usePostLike"; +import { useDeleteLike, usePostLike } from "@/apis/community"; import { IconCloseFilled, IconPostLikeFilled, IconPostLikeOutline } from "@/public/svgs"; import { IconCommunication } from "@/public/svgs/community"; diff --git a/src/app/community/[boardCode]/[postId]/KebabMenu.tsx b/src/app/community/[boardCode]/[postId]/KebabMenu.tsx index 884e19e1..a38d8479 100644 --- a/src/app/community/[boardCode]/[postId]/KebabMenu.tsx +++ b/src/app/community/[boardCode]/[postId]/KebabMenu.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react"; import ReportPanel from "@/components/ui/ReportPanel"; -import useDeletePost from "@/api/community/client/useDeletePost"; +import { useDeletePost } from "@/apis/community"; import { toast } from "@/lib/zustand/useToastStore"; import { IconSetting } from "@/public/svgs/mentor"; diff --git a/src/app/community/[boardCode]/[postId]/PostPageContent.tsx b/src/app/community/[boardCode]/[postId]/PostPageContent.tsx index a80ee957..d31e3b66 100644 --- a/src/app/community/[boardCode]/[postId]/PostPageContent.tsx +++ b/src/app/community/[boardCode]/[postId]/PostPageContent.tsx @@ -9,7 +9,7 @@ import CommentSection from "./CommentSection"; import Content from "./Content"; import KebabMenu from "./KebabMenu"; -import useGetPostDetail from "@/api/community/client/useGetPostDetail"; +import { useGetPostDetail } from "@/apis/community"; interface PostPageContentProps { boardCode: string; diff --git a/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx b/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx index bf9a018f..39271dca 100644 --- a/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx +++ b/src/app/community/[boardCode]/[postId]/modify/PostModifyContent.tsx @@ -7,7 +7,7 @@ import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; import PostModifyForm from "./PostModifyForm"; -import useGetPostDetail from "@/api/community/client/useGetPostDetail"; +import { useGetPostDetail } from "@/apis/community"; interface PostModifyContentProps { boardCode: string; diff --git a/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx b/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx index 936c60de..6592a171 100644 --- a/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx +++ b/src/app/community/[boardCode]/[postId]/modify/PostModifyForm.tsx @@ -3,7 +3,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; -import useUpdatePost from "@/api/community/client/useUpdatePost"; +import { useUpdatePost } from "@/apis/community"; import { IconArrowBackFilled, IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; type PostModifyFormProps = { diff --git a/src/app/community/[boardCode]/create/PostForm.tsx b/src/app/community/[boardCode]/create/PostForm.tsx index cf8d4245..19233cda 100644 --- a/src/app/community/[boardCode]/create/PostForm.tsx +++ b/src/app/community/[boardCode]/create/PostForm.tsx @@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from "react"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import useCreatePost from "@/api/community/client/useCreatePost"; +import { useCreatePost } from "@/apis/community"; import { IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs"; type PostFormProps = { diff --git a/src/app/community/[boardCode]/page.tsx b/src/app/community/[boardCode]/page.tsx index 3a4cde38..ebd7d7b3 100644 --- a/src/app/community/[boardCode]/page.tsx +++ b/src/app/community/[boardCode]/page.tsx @@ -6,8 +6,8 @@ import CommunityPageContent from "./CommunityPageContent"; import { COMMUNITY_BOARDS } from "@/constants/community"; -import { QueryKeys } from "@/api/boards/clients/QueryKeys"; -import { getPostList } from "@/api/boards/server/getPostList"; +import { CommunityQueryKeys } from "@/apis/community/api"; +import { getPostListServer } from "@/apis/community/server"; import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query"; export const metadata: Metadata = { @@ -40,11 +40,11 @@ const CommunityPage = async ({ params }: CommunityPageProps) => { const defaultCategory = "전체"; // 서버에서 데이터 prefetch (ISR - 수동 revalidate만) - const result = await getPostList({ boardCode, category: defaultCategory, revalidate: false }); + const result = await getPostListServer({ boardCode, category: defaultCategory, revalidate: false }); if (result.ok) { // React Query 캐시에 데이터 설정 (서버 fetch와 동일한 category 사용) - queryClient.setQueryData([QueryKeys.postList, boardCode, defaultCategory], { + queryClient.setQueryData([CommunityQueryKeys.postList, boardCode, defaultCategory], { data: result.data, }); } diff --git a/src/app/login/LoginContent.tsx b/src/app/login/LoginContent.tsx index 7277369d..4c83d0b9 100644 --- a/src/app/login/LoginContent.tsx +++ b/src/app/login/LoginContent.tsx @@ -11,7 +11,7 @@ import { appleLogin, kakaoLogin } from "@/utils/authUtils"; import useInputHandler from "./_hooks/useInputHandler"; -import usePostEmailAuth from "@/api/auth/client/usePostEmailAuth"; +import { usePostEmailAuth } from "@/apis/Auth"; import { toast } from "@/lib/zustand/useToastStore"; import { IconSolidConnectionFullBlackLogo } from "@/public/svgs"; import { IconAppleLogo, IconEmailIcon, IconKakaoLogo } from "@/public/svgs/auth"; diff --git a/src/app/login/apple/callback/AppleLoginCallbackPage.tsx b/src/app/login/apple/callback/AppleLoginCallbackPage.tsx index 06a4db17..96dde829 100644 --- a/src/app/login/apple/callback/AppleLoginCallbackPage.tsx +++ b/src/app/login/apple/callback/AppleLoginCallbackPage.tsx @@ -5,7 +5,7 @@ import { useEffect } from "react"; import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; -import usePostAppleAuth from "@/api/auth/client/usePostAppleAuth"; +import { usePostAppleAuth } from "@/apis/Auth"; const AppleLoginCallbackPage = () => { const searchParams = useSearchParams(); diff --git a/src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx b/src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx index 9305998d..055967ab 100644 --- a/src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx +++ b/src/app/login/kakao/callback/KakaoLoginCallbackPage.tsx @@ -5,7 +5,7 @@ import { useEffect } from "react"; import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; -import usePostKakaoAuth from "@/api/auth/client/usePostKakaoAuth"; +import { usePostKakaoAuth } from "@/apis/Auth"; const KakaoLoginCallbackPage = () => { const searchParams = useSearchParams(); diff --git a/src/app/mentor/[id]/_ui/MentorDetialContent/_ui/MentorArticle/_hooks/useLikeToggle.ts b/src/app/mentor/[id]/_ui/MentorDetialContent/_ui/MentorArticle/_hooks/useLikeToggle.ts index 0f1df4f4..12d67b6f 100644 --- a/src/app/mentor/[id]/_ui/MentorDetialContent/_ui/MentorArticle/_hooks/useLikeToggle.ts +++ b/src/app/mentor/[id]/_ui/MentorDetialContent/_ui/MentorArticle/_hooks/useLikeToggle.ts @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; -import useDeleteArticleLike from "@/api/news/client/useDeleteArticleLike"; -import usePostArticleLike from "@/api/news/client/usePostArticleLike"; +import { useDeleteArticleLike, usePostArticleLike } from "@/apis/news"; const useLikeToggle = (articleId: number, mentorId: number, articleIsLiked?: boolean) => { const { mutate: postArticleLike } = usePostArticleLike(mentorId); diff --git a/src/app/mentor/[id]/_ui/MentorDetialContent/index.tsx b/src/app/mentor/[id]/_ui/MentorDetialContent/index.tsx index 26c75528..d4f2933e 100644 --- a/src/app/mentor/[id]/_ui/MentorDetialContent/index.tsx +++ b/src/app/mentor/[id]/_ui/MentorDetialContent/index.tsx @@ -12,9 +12,8 @@ import MentorArticle from "./_ui/MentorArticle"; import { ChannelType } from "@/types/mentor"; -import usePostApplyMentoring from "@/api/mentee/client/usePostApplyMentoring"; -import useGetMentorDetail from "@/api/mentors/client/useGetMentorDetail"; -import useGetArticleList from "@/api/news/client/useGetArticleList"; +import { useGetMentorDetail, usePostApplyMentoring } from "@/apis/mentor"; +import { useGetArticleList } from "@/apis/news"; interface MentorDetailContentProps { mentorId: number; diff --git a/src/app/mentor/_ui/MentorClient/_ui/MenteePageTabs/index.tsx b/src/app/mentor/_ui/MentorClient/_ui/MenteePageTabs/index.tsx index a023e722..d25aa21c 100644 --- a/src/app/mentor/_ui/MentorClient/_ui/MenteePageTabs/index.tsx +++ b/src/app/mentor/_ui/MentorClient/_ui/MenteePageTabs/index.tsx @@ -10,8 +10,8 @@ import TabSelector from "@/components/ui/TabSelector"; import { VerifyStatus } from "@/types/mentee"; import { MenteeTab } from "@/types/mentor"; -import useGetChatRooms from "@/api/chat/clients/useGetChatRooms"; -import useGetMenteeMentoringList from "@/api/mentee/client/useGetApplyMentoringList"; +import { useGetChatRooms } from "@/apis/chat"; +import { useGetApplyMentoringList as useGetMenteeMentoringList } from "@/apis/mentor"; import { IconDirectionRight } from "@/public/svgs/mentor"; const MenteePageTabs = () => { diff --git a/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/usePrefetchMentorFindTab.ts b/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/usePrefetchMentorFindTab.ts index 03da1d71..4af8bafc 100644 --- a/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/usePrefetchMentorFindTab.ts +++ b/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/_hooks/usePrefetchMentorFindTab.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { FilterTab } from "@/types/mentor"; -import { usePrefetchMentorList } from "@/api/mentors/client/useGetMentorList"; +import { usePrefetchMentorList } from "@/apis/mentor"; const usePrefetchMentorFindTab = () => { const { prefetchMentorList } = usePrefetchMentorList(); diff --git a/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/index.tsx b/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/index.tsx index 787b03f1..feffda6b 100644 --- a/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/index.tsx +++ b/src/app/mentor/_ui/MentorClient/_ui/MentorFindSection/index.tsx @@ -11,7 +11,7 @@ import useSelectedTab from "./_hooks/useSelectedTab"; import { FilterTab } from "@/types/mentor"; -import useGetMentorList from "@/api/mentors/client/useGetMentorList"; +import { useGetMentorList } from "@/apis/mentor"; const MentorFindSection = () => { const { listRef, selectedTab, handleSelectTab } = useSelectedTab(); diff --git a/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/ApplicantListSection/index.tsx b/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/ApplicantListSection/index.tsx index 43574f37..65b4a5cd 100644 --- a/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/ApplicantListSection/index.tsx +++ b/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/ApplicantListSection/index.tsx @@ -3,7 +3,7 @@ import useInfinityScroll from "@/utils/useInfinityScroll"; import MentorExpandChatCard from "@/components/mentor/MentorExpandChatCard"; import EmptySdwBCards from "@/components/ui/EmptySdwBCards"; -import useGetMentoringList from "@/api/mentor/client/useGetMentoringList"; +import { useGetMentoringList } from "@/apis/mentor"; const ApplicantListSection = () => { const { data: mentoringApplicantList = [], fetchNextPage, hasNextPage } = useGetMentoringList({ size: 6 }); diff --git a/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/MyMentorSection/index.tsx b/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/MyMentorSection/index.tsx index 7e752571..0e567923 100644 --- a/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/MyMentorSection/index.tsx +++ b/src/app/mentor/_ui/MentorClient/_ui/MentorPage/_ui/MyMentorSection/index.tsx @@ -2,10 +2,10 @@ import MentorCard from "@/components/mentor/MentorCard"; -import useGetMyMentorProfile from "@/api/mentor/client/useGetMentorMyProfile"; +import { useGetMentorMyProfile } from "@/apis/mentor"; const MyMentorSection = () => { - const { data: myMentorProfile } = useGetMyMentorProfile(); + const { data: myMentorProfile } = useGetMentorMyProfile(); if (!myMentorProfile) { return
멘토 프로필을 불러오는 중...
; diff --git a/src/app/mentor/_ui/MentorClient/_ui/MentorPage/index.tsx b/src/app/mentor/_ui/MentorClient/_ui/MentorPage/index.tsx index 11d3456c..b5bc1a78 100644 --- a/src/app/mentor/_ui/MentorClient/_ui/MentorPage/index.tsx +++ b/src/app/mentor/_ui/MentorClient/_ui/MentorPage/index.tsx @@ -12,7 +12,7 @@ import MyMentorSection from "./_ui/MyMentorSection"; import { MentorTab } from "@/types/mentor"; -import useGetChatRooms from "@/api/chat/clients/useGetChatRooms"; +import { useGetChatRooms } from "@/apis/chat"; import { IconDirectionRight } from "@/public/svgs/mentor"; const MentorPage = () => { diff --git a/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts b/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts index 05dc3c00..c0caf400 100644 --- a/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts +++ b/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/useChatListHandler.ts @@ -5,7 +5,7 @@ import useInfinityScroll from "@/utils/useInfinityScroll"; import { ChatMessage, ConnectionStatus } from "@/types/chat"; -import useGetChatHistories from "@/api/chat/clients/useGetChatHistories"; +import { useGetChatHistories } from "@/apis/chat"; import useConnectWebSocket from "@/lib/web-socket/useConnectWebSocket"; import { Client } from "@stomp/stompjs"; diff --git a/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/usePutChatReadHandler.ts b/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/usePutChatReadHandler.ts index 97b81334..f40124cc 100644 --- a/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/usePutChatReadHandler.ts +++ b/src/app/mentor/chat/[chatId]/_ui/ChatContent/_hooks/usePutChatReadHandler.ts @@ -1,6 +1,6 @@ import { useEffect } from "react"; -import usePutChatRead from "@/api/chat/clients/usePutChatRead"; +import { usePutChatRead } from "@/apis/chat"; const usePutChatReadHandler = (chatId: number) => { const { mutate: putChatRead } = usePutChatRead(); diff --git a/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx b/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx index ccd1191e..32dc58d1 100644 --- a/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx +++ b/src/app/mentor/chat/[chatId]/_ui/ChatContent/index.tsx @@ -18,7 +18,7 @@ import ChatMessageBox from "./_ui/ChatMessageBox"; import { ConnectionStatus } from "@/types/chat"; import { UserRole } from "@/types/mentor"; -import useGetPartnerInfo from "@/api/chat/clients/useGetPartnerInfo"; +import { useGetPartnerInfo } from "@/apis/chat"; import useAuthStore from "@/lib/zustand/useAuthStore"; interface ChatContentProps { diff --git a/src/app/mentor/chat/[chatId]/_ui/ChatNavBar/index.tsx b/src/app/mentor/chat/[chatId]/_ui/ChatNavBar/index.tsx index 6faafa1a..f76636a3 100644 --- a/src/app/mentor/chat/[chatId]/_ui/ChatNavBar/index.tsx +++ b/src/app/mentor/chat/[chatId]/_ui/ChatNavBar/index.tsx @@ -14,8 +14,8 @@ import ReportPanel from "../../../../../../components/ui/ReportPanel"; import { UserRole } from "@/types/mentor"; -import useGetPartnerInfo from "@/api/chat/clients/useGetPartnerInfo"; -import useGetMyInfo from "@/api/my/client/useGetMyInfo"; +import { useGetMyInfo } from "@/apis/MyPage"; +import { useGetPartnerInfo } from "@/apis/chat"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { IconAlert, IconAlertSubC, IconDirectionRight, IconSetting } from "@/public/svgs/mentor"; diff --git a/src/app/mentor/chat/_ui/ChatPageClient/index.tsx b/src/app/mentor/chat/_ui/ChatPageClient/index.tsx index 6b9fedca..f703268f 100644 --- a/src/app/mentor/chat/_ui/ChatPageClient/index.tsx +++ b/src/app/mentor/chat/_ui/ChatPageClient/index.tsx @@ -8,8 +8,8 @@ import ProfileWithBadge from "@/components/ui/ProfileWithBadge"; import { UserRole } from "@/types/mentor"; -import useGetChatRooms from "@/api/chat/clients/useGetChatRooms"; -import useGetMyInfo from "@/api/my/client/useGetMyInfo"; +import { useGetMyInfo } from "@/apis/MyPage"; +import { useGetChatRooms } from "@/apis/chat"; import { IconSearchBlue, IconSolidConnentionLogo } from "@/public/svgs/mentor"; const ChatPageClient = () => { diff --git a/src/app/mentor/modify/_ui/ModifyContent/_hooks/usePutMyMentorProfileHandler.ts b/src/app/mentor/modify/_ui/ModifyContent/_hooks/usePutMyMentorProfileHandler.ts index c8f1afe5..b2131cc5 100644 --- a/src/app/mentor/modify/_ui/ModifyContent/_hooks/usePutMyMentorProfileHandler.ts +++ b/src/app/mentor/modify/_ui/ModifyContent/_hooks/usePutMyMentorProfileHandler.ts @@ -1,6 +1,6 @@ import { MentoModifyFormData } from "../_lib/mentoModifyScehma"; -import usePutMyMentorProfile, { PutMyMentorProfileRequest } from "@/api/mentor/client/usePutMyMentorProfile"; +import { PutMyMentorProfileRequest, usePutMyMentorProfile } from "@/apis/mentor"; import { customConfirm } from "@/lib/zustand/useConfirmModalStore"; import { IconModify } from "@/public/svgs/mentor"; diff --git a/src/app/mentor/modify/_ui/ModifyContent/_ui/ArticlePanel/_hooks/useDropDownHandler.ts b/src/app/mentor/modify/_ui/ModifyContent/_ui/ArticlePanel/_hooks/useDropDownHandler.ts index ec38a127..8bc61084 100644 --- a/src/app/mentor/modify/_ui/ModifyContent/_ui/ArticlePanel/_hooks/useDropDownHandler.ts +++ b/src/app/mentor/modify/_ui/ModifyContent/_ui/ArticlePanel/_hooks/useDropDownHandler.ts @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { ArticleDropdownType } from "@/types/news"; -import useDeleteArticle from "@/api/news/client/useDeleteArticle"; +import { useDeleteArticle } from "@/apis/news"; interface UseDeleteDropDownHandlerProps { articleId: number; diff --git a/src/app/mentor/modify/_ui/ModifyContent/index.tsx b/src/app/mentor/modify/_ui/ModifyContent/index.tsx index 1e6ddcb2..78bb2dda 100644 --- a/src/app/mentor/modify/_ui/ModifyContent/index.tsx +++ b/src/app/mentor/modify/_ui/ModifyContent/index.tsx @@ -12,12 +12,12 @@ import AddArticleCard from "./_ui/AddArticleCard"; import ArticlePanel from "./_ui/ArticlePanel"; import ChannelBox from "./_ui/ChannelBox"; -import useGetMyMentorProfile from "@/api/mentor/client/useGetMentorMyProfile"; -import useGetArticleList from "@/api/news/client/useGetArticleList"; +import { useGetMentorMyProfile } from "@/apis/mentor"; +import { useGetArticleList } from "@/apis/news"; import { IconUserPrimaryColor } from "@/public/svgs/mentor"; const ModifyContent = () => { - const { data: myMentorProfile = null } = useGetMyMentorProfile(); + const { data: myMentorProfile = null } = useGetMentorMyProfile(); const { data: articleList = [] } = useGetArticleList(myMentorProfile?.id || 0); const method = useModifyHookForm(myMentorProfile); diff --git a/src/app/mentor/waiting/_ui/WaitingContent/index.tsx b/src/app/mentor/waiting/_ui/WaitingContent/index.tsx index 924985d8..a48a8356 100644 --- a/src/app/mentor/waiting/_ui/WaitingContent/index.tsx +++ b/src/app/mentor/waiting/_ui/WaitingContent/index.tsx @@ -8,7 +8,7 @@ import MentorWaitingListBox from "./_ui/MentorWaitingListBox"; import { VerifyStatus } from "@/types/mentee"; -import useGetApplyMentoringList from "@/api/mentee/client/useGetApplyMentoringList"; +import { useGetApplyMentoringList } from "@/apis/mentor"; const DEFAULT_VISIBLE_ITEMS = 2; diff --git a/src/app/my/_ui/MyProfileContent/index.tsx b/src/app/my/_ui/MyProfileContent/index.tsx index fa08cc82..e1490448 100644 --- a/src/app/my/_ui/MyProfileContent/index.tsx +++ b/src/app/my/_ui/MyProfileContent/index.tsx @@ -8,9 +8,8 @@ import ProfileWithBadge from "@/components/ui/ProfileWithBadge"; import { UserRole } from "@/types/mentor"; -import useDeleteUserAccount from "@/api/auth/client/useDeleteUserAccount"; -import usePostLogout from "@/api/auth/client/usePostLogout"; -import useGetMyInfo from "@/api/my/client/useGetMyInfo"; +import { useDeleteUserAccount, usePostLogout } from "@/apis/Auth"; +import { MyInfoResponse, useGetMyInfo } from "@/apis/MyPage"; import { toast } from "@/lib/zustand/useToastStore"; import { IconLikeFill } from "@/public/svgs/mentor"; import { @@ -26,7 +25,7 @@ import { const NEXT_PUBLIC_CONTACT_LINK = process.env.NEXT_PUBLIC_CONTACT_LINK; const MyProfileContent = () => { - const { data: profileData = {} } = useGetMyInfo(); + const { data: profileData = {} as MyInfoResponse } = useGetMyInfo(); const { mutate: deleteUserAccount } = useDeleteUserAccount(); const { mutate: postLogout } = usePostLogout(); diff --git a/src/app/my/apply-mentor/_components/UniversityScreen/index.tsx b/src/app/my/apply-mentor/_components/UniversityScreen/index.tsx index 03b46131..46ed8c2f 100644 --- a/src/app/my/apply-mentor/_components/UniversityScreen/index.tsx +++ b/src/app/my/apply-mentor/_components/UniversityScreen/index.tsx @@ -11,7 +11,7 @@ import { MentorApplicationFormData } from "../../_lib/schema"; import { mentorRegionList } from "@/constants/regions"; -import useGetUniversitySearchByText from "@/api/university/client/useGetUniversitySearchByText"; +import { useUniversitySearch } from "@/apis/universities"; import { toast } from "@/lib/zustand/useToastStore"; type UniversityScreenProps = { @@ -33,7 +33,7 @@ const UniversityScreen = ({ onNext }: UniversityScreenProps) => { const verificationFile = watch("verificationFile"); // 모든 대학 목록 가져오기 - const { data: allUniversities = [], isLoading } = useGetUniversitySearchByText(""); + const { data: allUniversities = [], isLoading } = useUniversitySearch(""); // regionList에서 모든 국가 추출 (중복 제거) const availableCountries = useMemo(() => { diff --git a/src/app/my/apply-mentor/page.tsx b/src/app/my/apply-mentor/page.tsx index 079bf986..368f6714 100644 --- a/src/app/my/apply-mentor/page.tsx +++ b/src/app/my/apply-mentor/page.tsx @@ -13,7 +13,7 @@ import StudyStatusScreen from "./_components/StudyStatusScreen"; import UniversityScreen from "./_components/UniversityScreen"; import { MentorApplicationFormData, mentorApplicationSchema } from "./_lib/schema"; -import usePostMentorApplication from "@/api/mentor/client/usePostMentorApplication"; +import { usePostMentorApplication } from "@/apis/mentor"; import { toast } from "@/lib/zustand/useToastStore"; import { zodResolver } from "@hookform/resolvers/zod"; diff --git a/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSelectUniversities.ts b/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSelectUniversities.ts index caedd26a..2e9ddfe5 100644 --- a/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSelectUniversities.ts +++ b/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSelectUniversities.ts @@ -1,7 +1,7 @@ import { useState } from "react"; -import { QueryKeys } from "@/api/university/client/queryKey"; -import useDeleteUniversityFavorite from "@/api/university/client/useDeleteUniversityFavorite"; +import { QueryKeys } from "@/apis/queryKeys"; +import { useDeleteWish } from "@/apis/universities"; import { toast } from "@/lib/zustand/useToastStore"; import { useQueryClient } from "@tanstack/react-query"; @@ -11,7 +11,7 @@ interface UseSelectUniversitiesReturn { handleDeleteAll: () => void; } const useSelectUniversities = (): UseSelectUniversitiesReturn => { - const { mutate: deleteUserAccount } = useDeleteUniversityFavorite(); + const { mutate: deleteUserAccount } = useDeleteWish(); const queryClient = useQueryClient(); const [editSelected, setEditSelected] = useState([]); @@ -43,7 +43,7 @@ const useSelectUniversities = (): UseSelectUniversitiesReturn => { } else { toast.success("모든 학교가 삭제되었습니다."); } - queryClient.invalidateQueries({ queryKey: [QueryKeys.univApplyInfosLike] }); + queryClient.invalidateQueries({ queryKey: [QueryKeys.universities.wishList] }); }); }; diff --git a/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSortedUniversities.ts b/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSortedUniversities.ts index 8697e37e..30735a28 100644 --- a/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSortedUniversities.ts +++ b/src/app/my/favorite/_ui/FavoriteContent/_hooks/useSortedUniversities.ts @@ -4,7 +4,7 @@ import { filterType } from ".."; import { ListUniversity, University } from "@/types/university"; -import useGetMyWishUniversity from "@/api/university/client/useGetMyWishUniversity"; +import { useGetWishList } from "@/apis/universities"; interface UseSortedUniversitiesReturn { wishUniversity: ListUniversity[]; @@ -13,7 +13,7 @@ interface UseSortedUniversitiesReturn { } const useSortedUniversities = (): UseSortedUniversitiesReturn => { - const { data: wishUniversity = [] } = useGetMyWishUniversity(); + const { data: wishUniversity = [] } = useGetWishList(); const [sequence, setSequence] = useState(filterType.LATEST); diff --git a/src/app/my/favorite/_ui/FavoriteContent/index.tsx b/src/app/my/favorite/_ui/FavoriteContent/index.tsx index a3acdb2c..ff10fc46 100644 --- a/src/app/my/favorite/_ui/FavoriteContent/index.tsx +++ b/src/app/my/favorite/_ui/FavoriteContent/index.tsx @@ -8,7 +8,7 @@ import useSelectUniversities from "./_hooks/useSelectUniversities"; import useSortedUniversities from "./_hooks/useSortedUniversities"; import FavoriteDropDown from "./_ui/FavoriteDropDown"; -import useGetMyInfo from "@/api/my/client/useGetMyInfo"; +import { useGetMyInfo } from "@/apis/MyPage"; // 필터 타입 Enum export enum filterType { diff --git a/src/app/my/match/_ui/MatchContent/index.tsx b/src/app/my/match/_ui/MatchContent/index.tsx index 32a4d445..9cc2655a 100644 --- a/src/app/my/match/_ui/MatchContent/index.tsx +++ b/src/app/my/match/_ui/MatchContent/index.tsx @@ -7,11 +7,11 @@ import MentorChatCard from "@/components/mentor/MentorChatCard"; import { UserRole } from "@/types/mentor"; -import useGetChatRooms from "@/api/chat/clients/useGetChatRooms"; -import useGetMyInfo from "@/api/my/client/useGetMyInfo"; +import { MyInfoResponse, useGetMyInfo } from "@/apis/MyPage"; +import { useGetChatRooms } from "@/apis/chat"; const MatchContent = () => { - const { data: myInfo = {} } = useGetMyInfo(); + const { data: myInfo = {} as MyInfoResponse } = useGetMyInfo(); const { data: chatRoom = [] } = useGetChatRooms(); const isAdmin = myInfo.role === UserRole.ADMIN; diff --git a/src/app/my/modify/_ui/ModifyContent/_hooks/useModifyUserHookform.ts b/src/app/my/modify/_ui/ModifyContent/_hooks/useModifyUserHookform.ts index 82fbfe98..732ddbb3 100644 --- a/src/app/my/modify/_ui/ModifyContent/_hooks/useModifyUserHookform.ts +++ b/src/app/my/modify/_ui/ModifyContent/_hooks/useModifyUserHookform.ts @@ -3,8 +3,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; -import useGetMyInfo, { MyInfoResponse } from "@/api/my/client/useGetMyInfo"; -import usePatchMyInfo from "@/api/my/client/usePatchMyInfo"; +import { type MyInfoResponse, useGetMyInfo, usePatchMyInfo } from "@/apis/MyPage"; import { zodResolver } from "@hookform/resolvers/zod"; // Zod 스키마 정의 - 닉네임과 이미지 diff --git a/src/app/my/password/_ui/PasswordContent/index.tsx b/src/app/my/password/_ui/PasswordContent/index.tsx index d9d0a8d6..9b9aa084 100644 --- a/src/app/my/password/_ui/PasswordContent/index.tsx +++ b/src/app/my/password/_ui/PasswordContent/index.tsx @@ -8,7 +8,7 @@ import { z } from "zod"; import PasswordInput from "./_ui/PasswordInput"; -import usePatchMyPassword from "@/api/my/client/usePatchMyPassword"; +import { usePatchMyPassword } from "@/apis/MyPage"; import { zodResolver } from "@hookform/resolvers/zod"; export const changePasswordSchema = z diff --git a/src/app/sign-up/email/EmailSignUpForm.tsx b/src/app/sign-up/email/EmailSignUpForm.tsx index e29a0728..832db620 100644 --- a/src/app/sign-up/email/EmailSignUpForm.tsx +++ b/src/app/sign-up/email/EmailSignUpForm.tsx @@ -8,7 +8,7 @@ import { Input } from "@/components/ui/Inputa"; import { Label } from "@/components/ui/Label"; import { Progress } from "@/components/ui/Progress"; -import usePostEmailSignUp from "@/api/auth/client/usePostEmailSignUp"; +import { usePostEmailSignUp } from "@/apis/Auth"; import { toast } from "@/lib/zustand/useToastStore"; import { IconCheckBlue, IconExpRed, IconEyeOff, IconEyeOn } from "@/public/svgs/ui"; diff --git a/src/app/university/SearchResultsContent.tsx b/src/app/university/SearchResultsContent.tsx index 644c33c6..9726276d 100644 --- a/src/app/university/SearchResultsContent.tsx +++ b/src/app/university/SearchResultsContent.tsx @@ -13,10 +13,11 @@ import SearchBar from "./SearchBar"; import { CountryCode, LanguageTestType, RegionEnumExtend } from "@/types/university"; // 필요한 타입과 훅 import -import useGetUniversitySearchByFilter, { - UniversitySearchFilterParams, -} from "@/api/university/client/useGetUniversitySearchByFilter"; -import useGetUniversitySearchByText from "@/api/university/client/useGetUniversitySearchByText"; +import { + type UniversitySearchFilterParams, + useGetUniversitySearchByFilter, + useUniversitySearch, +} from "@/apis/universities"; // --- URL 파라미터를 읽고 데이터를 처리하는 메인 컨텐츠 --- const SearchResultsContent = () => { @@ -55,7 +56,7 @@ const SearchResultsContent = () => { } }, [searchParams, selectedRegion]); - const textSearchQuery = useGetUniversitySearchByText(searchText ?? ""); + const textSearchQuery = useUniversitySearch(searchText ?? ""); const filterSearchQuery = useGetUniversitySearchByFilter(filterParams); const { data: serachResult } = isTextSearch ? textSearchQuery : filterSearchQuery; diff --git a/src/app/university/[id]/_ui/UniversityDetail/_ui/UniversityBtns.tsx b/src/app/university/[id]/_ui/UniversityDetail/_ui/UniversityBtns.tsx index f1c98fd4..14adb1d5 100644 --- a/src/app/university/[id]/_ui/UniversityDetail/_ui/UniversityBtns.tsx +++ b/src/app/university/[id]/_ui/UniversityDetail/_ui/UniversityBtns.tsx @@ -2,9 +2,7 @@ import { useEffect, useState } from "react"; -import useDeleteUniversityFavorite from "@/api/university/client/useDeleteUniversityFavorite"; -import useGetMyWishUniversity from "@/api/university/client/useGetMyWishUniversity"; -import usePostUniversityFavorite from "@/api/university/client/usePostUniversityFavorite"; +import { useDeleteWish, useGetWishList, usePostAddWish } from "@/apis/universities"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; @@ -46,9 +44,9 @@ const UniversityBtns = ({ universityId }: UniversityBtnsProps) => { const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const [isLiked, setIsLiked] = useState(false); - const { data: favoriteUniv } = useGetMyWishUniversity(isAuthenticated); - const { mutate: postUniversityFavorite } = usePostUniversityFavorite(); - const { mutate: deleteUniversityFavorite } = useDeleteUniversityFavorite(); + const { data: favoriteUniv } = useGetWishList(isAuthenticated); + const { mutate: postUniversityFavorite } = usePostAddWish(); + const { mutate: deleteUniversityFavorite } = useDeleteWish(); useEffect(() => { favoriteUniv?.forEach((univ) => { diff --git a/src/app/university/[id]/page.tsx b/src/app/university/[id]/page.tsx index 30a26293..eae815cc 100644 --- a/src/app/university/[id]/page.tsx +++ b/src/app/university/[id]/page.tsx @@ -5,8 +5,7 @@ import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; import UniversityDetail from "./_ui/UniversityDetail"; -import { getAllUniversities } from "@/api/university/server/getSearchUniversitiesByText"; -import { getUniversityDetail } from "@/api/university/server/getUniversityDetail"; +import { getAllUniversities, getUniversityDetail } from "@/apis/universities/server"; export const revalidate = false; diff --git a/src/app/university/application/ScorePageContent.tsx b/src/app/university/application/ScorePageContent.tsx index b729129e..0991f75d 100644 --- a/src/app/university/application/ScorePageContent.tsx +++ b/src/app/university/application/ScorePageContent.tsx @@ -16,7 +16,7 @@ import { REGIONS_KO } from "@/constants/university"; import { ScoreSheet as ScoreSheetType } from "@/types/application"; import { RegionKo } from "@/types/university"; -import useGetApplicationsList from "@/api/applications/client/useGetApplicationsList"; +import { useGetApplicationsList } from "@/apis/applications"; import { toast } from "@/lib/zustand/useToastStore"; const PREFERENCE_CHOICE: ("1순위" | "2순위" | "3순위")[] = ["1순위", "2순위", "3순위"]; diff --git a/src/app/university/application/apply/ApplyPageContent.tsx b/src/app/university/application/apply/ApplyPageContent.tsx index 82f5f12e..c4c250fe 100644 --- a/src/app/university/application/apply/ApplyPageContent.tsx +++ b/src/app/university/application/apply/ApplyPageContent.tsx @@ -15,17 +15,16 @@ import UniversityStep from "./UniversityStep"; import { ListUniversity } from "@/types/university"; -import usePostSubmitApplication from "@/api/applications/client/usePostSubmitApplication"; -import useGetMyGpaScore from "@/api/score/client/useGetMyGpaScore"; -import useGetMyLanguageTestScore from "@/api/score/client/useGetMyLanguageTestScore"; -import useGetUniversitySearchByText from "@/api/university/client/useGetUniversitySearchByText"; +import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores"; +import { usePostSubmitApplication } from "@/apis/applications"; +import { useUniversitySearch } from "@/apis/universities"; import { toast } from "@/lib/zustand/useToastStore"; const ApplyPageContent = () => { const router = useRouter(); const [step, setStep] = useState(1); - const { data: universityList = [] } = useGetUniversitySearchByText(""); + const { data: universityList = [] } = useUniversitySearch(""); const { data: gpaScoreList = [] } = useGetMyGpaScore(); const { data: languageTestScoreList = [] } = useGetMyLanguageTestScore(); const { mutate: postSubmitApplication } = usePostSubmitApplication({ diff --git a/src/app/university/score/ScoreScreen.tsx b/src/app/university/score/ScoreScreen.tsx index 940311f0..7a667222 100644 --- a/src/app/university/score/ScoreScreen.tsx +++ b/src/app/university/score/ScoreScreen.tsx @@ -10,8 +10,7 @@ import ScoreCard from "./ScoreCard"; import { languageTestMapping } from "@/types/score"; -import useGetMyGpaScore from "@/api/score/client/useGetMyGpaScore"; -import useGetMyLanguageTestScore from "@/api/score/client/useGetMyLanguageTestScore"; +import { useGetMyGpaScore, useGetMyLanguageTestScore } from "@/apis/Scores"; const ScoreScreen = () => { const router = useRouter(); diff --git a/src/app/university/score/submit/gpa/GpaSubmitForm.tsx b/src/app/university/score/submit/gpa/GpaSubmitForm.tsx index 478a5ad7..47456800 100644 --- a/src/app/university/score/submit/gpa/GpaSubmitForm.tsx +++ b/src/app/university/score/submit/gpa/GpaSubmitForm.tsx @@ -15,7 +15,7 @@ import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; // CustomDropdown 경로 확인 필요 import { GpaFormData, gpaSchema } from "./_lib/schema"; -import { usePostGpaScore } from "@/api/score/client/usePostGpaScore"; +import { usePostGpaScore } from "@/apis/Scores"; import CustomDropdown from "@/app/university/CustomDropdown"; import { zodResolver } from "@hookform/resolvers/zod"; diff --git a/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx b/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx index 903e9606..58dafe5c 100644 --- a/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx +++ b/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx @@ -17,7 +17,7 @@ import { LanguageTestFormData, languageTestSchema } from "./_lib/schema"; import { LanguageTestEnum } from "@/types/score"; -import { usePostLanguageTestScore } from "@/api/score/client/usePostLanguageTestScore"; +import { usePostLanguageTestScore } from "@/apis/Scores"; import CustomDropdown from "@/app/university/CustomDropdown"; import { toast } from "@/lib/zustand/useToastStore"; import { zodResolver } from "@hookform/resolvers/zod"; diff --git a/src/components/layout/ReissueProvider/index.tsx b/src/components/layout/ReissueProvider/index.tsx index 0807ef65..109dedf3 100644 --- a/src/components/layout/ReissueProvider/index.tsx +++ b/src/components/layout/ReissueProvider/index.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; -import postReissueToken from "@/api/auth/server/postReissueToken"; +import { postReissueToken } from "@/apis/Auth"; import useAuthStore from "@/lib/zustand/useAuthStore"; interface ReissueProviderProps { diff --git a/src/components/login/signup/SignupSurvey.tsx b/src/components/login/signup/SignupSurvey.tsx index e3265da0..8f98196b 100644 --- a/src/components/login/signup/SignupSurvey.tsx +++ b/src/components/login/signup/SignupSurvey.tsx @@ -13,8 +13,8 @@ import SignupRegionScreen from "./SignupRegionScreen"; import { PreparationStatus, SignUpRequest } from "@/types/auth"; import { RegionKo } from "@/types/university"; -import usePostSignUp from "@/api/auth/client/usePostSignUp"; -import useUploadProfileImagePublic from "@/api/file/client/useUploadProfileImagePublic"; +import { usePostSignUp } from "@/apis/Auth"; +import { useUploadProfileImagePublic } from "@/apis/image-upload"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { toast } from "@/lib/zustand/useToastStore"; diff --git a/src/components/mentor/ArticleBottomSheetModal/hooks/useArticleSchema.ts b/src/components/mentor/ArticleBottomSheetModal/hooks/useArticleSchema.ts index fd545d95..3c877bf6 100644 --- a/src/components/mentor/ArticleBottomSheetModal/hooks/useArticleSchema.ts +++ b/src/components/mentor/ArticleBottomSheetModal/hooks/useArticleSchema.ts @@ -6,9 +6,8 @@ import { convertUploadedImageUrl } from "@/utils/fileUtils"; import { InitialData } from ".."; import { ArticleFormData, articleSchema } from "../lib/schema"; -import useGetMentorMyProfile from "@/api/mentor/client/useGetMentorMyProfile"; -import usePostAddArticle from "@/api/news/client/usePostAddArticle"; -import usePutModifyArticle from "@/api/news/client/usePutModifyArticle"; +import { useGetMentorMyProfile } from "@/apis/mentor"; +import { usePostAddArticle, usePutModifyArticle } from "@/apis/news"; import { zodResolver } from "@hookform/resolvers/zod"; interface UseArticleSchemaProps { diff --git a/src/components/mentor/MentorApplyCountContent/index.tsx b/src/components/mentor/MentorApplyCountContent/index.tsx index 34f424ca..f8a668d3 100644 --- a/src/components/mentor/MentorApplyCountContent/index.tsx +++ b/src/components/mentor/MentorApplyCountContent/index.tsx @@ -7,7 +7,7 @@ import { tokenParse } from "@/utils/jwtUtils"; import { UserRole } from "@/types/mentor"; -import useGetMentoringUncheckedCount from "@/api/mentor/client/useGetMentoringUncheckedCount"; +import { useGetMentoringUncheckedCount } from "@/apis/mentor"; import useAuthStore from "@/lib/zustand/useAuthStore"; const MentorApplyCountContent = () => { diff --git a/src/components/mentor/MentorCard/hooks/usePostApplyMentorHandler.ts b/src/components/mentor/MentorCard/hooks/usePostApplyMentorHandler.ts index 887cd9c0..bc0723ae 100644 --- a/src/components/mentor/MentorCard/hooks/usePostApplyMentorHandler.ts +++ b/src/components/mentor/MentorCard/hooks/usePostApplyMentorHandler.ts @@ -1,6 +1,6 @@ import { useRouter } from "next/navigation"; -import usePostApplyMentoring from "@/api/mentee/client/usePostApplyMentoring"; +import { usePostApplyMentoring } from "@/apis/mentor"; import { customConfirm } from "@/lib/zustand/useConfirmModalStore"; import { IconCheck } from "@/public/svgs/mentor"; diff --git a/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts b/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts index e7dc7511..ed4bebaf 100644 --- a/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts +++ b/src/components/mentor/MentorExpandChatCard/hooks/useExpandCardClickHandler.ts @@ -4,8 +4,7 @@ import { tokenParse } from "@/utils/jwtUtils"; import { UserRole } from "@/types/mentor"; -import usePatchMenteeCheckMentorings from "@/api/mentee/client/usePatchMenteeCheckMentorings"; -import usePatchMentorCheckMentorings from "@/api/mentor/client/usePatchMentorCheckMentorings"; +import { usePatchMenteeCheckMentorings, usePatchMentorCheckMentorings } from "@/apis/mentor"; import useAuthStore from "@/lib/zustand/useAuthStore"; interface UseExpandCardClickHandlerReturn { diff --git a/src/components/mentor/MentorExpandChatCard/hooks/usePatchApprovalStatusHandler.ts b/src/components/mentor/MentorExpandChatCard/hooks/usePatchApprovalStatusHandler.ts index 3ba420b6..a989f85e 100644 --- a/src/components/mentor/MentorExpandChatCard/hooks/usePatchApprovalStatusHandler.ts +++ b/src/components/mentor/MentorExpandChatCard/hooks/usePatchApprovalStatusHandler.ts @@ -1,6 +1,6 @@ import { MentoringApprovalStatus } from "@/types/mentor"; -import usePatchApprovalStatus from "@/api/mentor/client/usePatchApprovalStatus"; +import { usePatchApprovalStatus } from "@/apis/mentor"; import { customConfirm } from "@/lib/zustand/useConfirmModalStore"; import { IconUnSmile } from "@/public/svgs/mentor"; diff --git a/src/components/ui/ReportPanel/_hooks/useSelectReportHandler.ts b/src/components/ui/ReportPanel/_hooks/useSelectReportHandler.ts index b85412a3..c3c734cf 100644 --- a/src/components/ui/ReportPanel/_hooks/useSelectReportHandler.ts +++ b/src/components/ui/ReportPanel/_hooks/useSelectReportHandler.ts @@ -3,7 +3,7 @@ import { useState } from "react"; import { reportReasons } from "@/constants/report"; import { ReportType } from "@/types/reports"; -import usePostReports from "@/api/reports/client/usePostReport"; +import { usePostReports } from "@/apis/reports"; import { customConfirm } from "@/lib/zustand/useConfirmModalStore"; import { IconReport } from "@/public/svgs/mentor"; diff --git a/src/components/university/UniversityCards/index.tsx b/src/components/university/UniversityCards/index.tsx index c2bae983..efbc41ca 100644 --- a/src/components/university/UniversityCards/index.tsx +++ b/src/components/university/UniversityCards/index.tsx @@ -27,6 +27,16 @@ const UniversityCards = ({ showCapacity = true, enableVirtualization = true, }: UniversityCardsProps) => { + // 훅은 항상 컴포넌트 상단에서 호출해야 함 (React Hooks 규칙) + const parentRef = useRef(null); + + const virtualizer = useVirtualizer({ + count: colleges.length, + getScrollElement: () => parentRef.current, + estimateSize: () => ITEM_HEIGHT, + overscan: 5, + }); + // 가상화가 비활성화된 경우 일반 렌더링 if (!enableVirtualization) { return ( @@ -40,16 +50,7 @@ const UniversityCards = ({ ); } - // 가상화 사용 (기존 로직) - const parentRef = useRef(null); - - const virtualizer = useVirtualizer({ - count: colleges.length, - getScrollElement: () => parentRef.current, - estimateSize: () => ITEM_HEIGHT, - overscan: 5, - }); - + // 가상화 사용 return (