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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 35 additions & 35 deletions src/apis/Auth/postAppleAuth.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,46 @@
import { useRouter, useSearchParams } from "next/navigation";

import { AxiosError } from "axios";

import { validateSafeRedirect } from "@/utils/authUtils";

import { AppleAuthRequest, AppleAuthResponse, authApi } from "./api";
import { useMutation } from "@tanstack/react-query";

import type { AxiosError } from "axios";
import { useRouter, useSearchParams } from "next/navigation";
import useAuthStore from "@/lib/zustand/useAuthStore";
import { toast } from "@/lib/zustand/useToastStore";
import { useMutation } from "@tanstack/react-query";
import { validateSafeRedirect } from "@/utils/authUtils";
import { type AppleAuthRequest, type AppleAuthResponse, authApi } from "./api";

/**
* @description 애플 로그인을 위한 useMutation 커스텀 훅
*/
const usePostAppleAuth = () => {
const router = useRouter();
const searchParams = useSearchParams();

return useMutation<AppleAuthResponse, AxiosError, AppleAuthRequest>({
mutationFn: (data) => authApi.postAppleAuth(data),
onSuccess: (data) => {
if (data.isRegistered) {
// 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장
// refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨
useAuthStore.getState().setAccessToken(data.accessToken);

// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
const redirectParam = searchParams.get("redirect");
const safeRedirect = validateSafeRedirect(redirectParam);

toast.success("로그인에 성공했습니다.");
router.replace(safeRedirect);
} else {
// 새로운 회원일 시 - 회원가입 페이지로 이동
router.push(`/sign-up?token=${data.signUpToken}`);
}
},
onError: () => {
toast.error("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
router.push("/login");
},
});
const router = useRouter();
const searchParams = useSearchParams();

return useMutation<AppleAuthResponse, AxiosError, AppleAuthRequest>({
mutationFn: (data) => authApi.postAppleAuth(data),
onSuccess: (data) => {
if (data.isRegistered) {
// 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장
// refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨
useAuthStore.getState().setAccessToken(data.accessToken);

// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
const redirectParam = searchParams.get("redirect");
const safeRedirect = validateSafeRedirect(redirectParam);

toast.success("로그인에 성공했습니다.");

setTimeout(() => {
router.push(safeRedirect);
}, 100);
} else {
// 새로운 회원일 시 - 회원가입 페이지로 이동
router.push(`/sign-up?token=${data.signUpToken}`);
}
},
onError: () => {
toast.error("애플 로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
router.push("/login");
},
});
};

export default usePostAppleAuth;
64 changes: 35 additions & 29 deletions src/apis/Auth/postEmailLogin.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
import { useRouter, useSearchParams } from "next/navigation";

import { AxiosError } from "axios";

import { validateSafeRedirect } from "@/utils/authUtils";

import { EmailLoginRequest, EmailLoginResponse, authApi } from "./api";
import { useMutation } from "@tanstack/react-query";

import type { AxiosError } from "axios";
import { useRouter, useSearchParams } from "next/navigation";
import useAuthStore from "@/lib/zustand/useAuthStore";
import { toast } from "@/lib/zustand/useToastStore";
import { useMutation } from "@tanstack/react-query";
import { validateSafeRedirect } from "@/utils/authUtils";
import {
authApi,
type EmailLoginRequest,
type EmailLoginResponse,
} from "./api";

/**
* @description 이메일 로그인을 위한 useMutation 커스텀 훅
*/
const usePostEmailAuth = () => {
const { setAccessToken } = useAuthStore();
const searchParams = useSearchParams();
const router = useRouter();

return useMutation<EmailLoginResponse, AxiosError, EmailLoginRequest>({
mutationFn: (data) => authApi.postEmailLogin(data),
onSuccess: (data) => {
const { accessToken } = data;

// Zustand persist가 자동으로 localStorage에 저장
// refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨
setAccessToken(accessToken);

// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
const redirectParam = searchParams.get("redirect");
const safeRedirect = validateSafeRedirect(redirectParam);

toast.success("로그인에 성공했습니다.");
router.replace(safeRedirect);
},
});
const { setAccessToken } = useAuthStore();
const searchParams = useSearchParams();
const router = useRouter();

return useMutation<EmailLoginResponse, AxiosError, EmailLoginRequest>({
mutationFn: (data) => authApi.postEmailLogin(data),
onSuccess: (data) => {
const { accessToken } = data;

// Zustand persist가 자동으로 localStorage에 저장
// refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨
setAccessToken(accessToken);

// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
const redirectParam = searchParams.get("redirect");
const safeRedirect = validateSafeRedirect(redirectParam);

toast.success("로그인에 성공했습니다.");

// Zustand persist middleware가 localStorage에 저장할 시간을 보장
// 토큰 저장 후 리다이렉트하여 타이밍 이슈 방지
setTimeout(() => {
router.push(safeRedirect);
}, 100);
},
});
};

export default usePostEmailAuth;
72 changes: 36 additions & 36 deletions src/apis/Auth/postKakaoAuth.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
import { useRouter, useSearchParams } from "next/navigation";

import { AxiosError } from "axios";

import { validateSafeRedirect } from "@/utils/authUtils";

import { KakaoAuthRequest, KakaoAuthResponse, authApi } from "./api";
import { useMutation } from "@tanstack/react-query";

import type { AxiosError } from "axios";
import { useRouter, useSearchParams } from "next/navigation";
import useAuthStore from "@/lib/zustand/useAuthStore";
import { toast } from "@/lib/zustand/useToastStore";
import { useMutation } from "@tanstack/react-query";
import { validateSafeRedirect } from "@/utils/authUtils";
import { authApi, type KakaoAuthRequest, type KakaoAuthResponse } from "./api";

/**
* @description 카카오 로그인을 위한 useMutation 커스텀 훅
*/
const usePostKakaoAuth = () => {
const { setAccessToken } = useAuthStore();
const router = useRouter();
const searchParams = useSearchParams();

return useMutation<KakaoAuthResponse, AxiosError, KakaoAuthRequest>({
mutationFn: (data) => authApi.postKakaoAuth(data),
onSuccess: (data) => {
if (data.isRegistered) {
// 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장
// refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨
setAccessToken(data.accessToken);

// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
const redirectParam = searchParams.get("redirect");
const safeRedirect = validateSafeRedirect(redirectParam);

toast.success("로그인에 성공했습니다.");
router.replace(safeRedirect);
} else {
// 새로운 회원일 시 - 회원가입 페이지로 이동
router.push(`/sign-up?token=${data.signUpToken}`);
}
},
onError: () => {
toast.error("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
router.push("/login");
},
});
const { setAccessToken } = useAuthStore();
const router = useRouter();
const searchParams = useSearchParams();

return useMutation<KakaoAuthResponse, AxiosError, KakaoAuthRequest>({
mutationFn: (data) => authApi.postKakaoAuth(data),
onSuccess: (data) => {
if (data.isRegistered) {
// 기존 회원일 시 - Zustand persist가 자동으로 localStorage에 저장
// refreshToken은 서버에서 HTTP-only 쿠키로 자동 설정됨
setAccessToken(data.accessToken);

// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
const redirectParam = searchParams.get("redirect");
const safeRedirect = validateSafeRedirect(redirectParam);

toast.success("로그인에 성공했습니다.");

setTimeout(() => {
router.push(safeRedirect);
}, 100);
} else {
// 새로운 회원일 시 - 회원가입 페이지로 이동
router.push(`/sign-up?token=${data.signUpToken}`);
}
},
onError: () => {
toast.error("카카오 로그인 중 오류가 발생했습니다. 다시 시도해주세요.");
router.push("/login");
},
});
};

export default usePostKakaoAuth;
18 changes: 12 additions & 6 deletions src/apis/universities/server/getRecommendedUniversity.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import type { ListUniversity } from "@/types/university";
import serverFetch from "@/utils/serverFetchUtil";

import { ListUniversity } from "@/types/university";

type GetRecommendedUniversityResponse = { recommendedUniversities: ListUniversity[] };
type GetRecommendedUniversityResponse = {
recommendedUniversities: ListUniversity[];
};

const getRecommendedUniversity = async () => {
const endpoint = "/univ-apply-infos/recommend";
const endpoint = "/univ-apply-infos/recommend";

const res = await serverFetch<GetRecommendedUniversityResponse>(endpoint);

if (!res.ok) {
console.error(`Failed to fetch recommended universities:`, res.error);
}

const res = await serverFetch<GetRecommendedUniversityResponse>(endpoint);
return res;
return res;
};

export default getRecommendedUniversity;
82 changes: 48 additions & 34 deletions src/apis/universities/server/getSearchUniversitiesByFilter.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,66 @@
import { URLSearchParams } from "url";

import type {
CountryCode,
LanguageTestType,
ListUniversity,
} from "@/types/university";
import serverFetch from "@/utils/serverFetchUtil";

import { CountryCode, LanguageTestType, ListUniversity } from "@/types/university";

interface UniversitySearchResponse {
univApplyInfoPreviews: ListUniversity[];
univApplyInfoPreviews: ListUniversity[];
}

/**
* 필터 검색에 사용될 파라미터 타입
*/
export interface UniversitySearchFilterParams {
languageTestType?: LanguageTestType;
testScore?: number;
countryCode?: CountryCode[];
languageTestType?: LanguageTestType;
testScore?: number;
countryCode?: CountryCode[];
}

export const getSearchUniversitiesByFilter = async (
filters: UniversitySearchFilterParams,
filters: UniversitySearchFilterParams,
): Promise<ListUniversity[]> => {
const params = new URLSearchParams();

if (filters.languageTestType) {
params.append("languageTestType", filters.languageTestType);
}
if (filters.testScore !== undefined) {
params.append("testScore", String(filters.testScore));
}
// countryCode는 여러 개일 수 있으므로 각각 append 해줍니다.
if (filters.countryCode) {
filters.countryCode.forEach((code) => params.append("countryCode", code));
}

// 필터 값이 하나도 없으면 빈 배열을 반환합니다.
if (params.size === 0) {
return [];
}

const endpoint = `/univ-apply-infos/search/filter?${params.toString()}`;
const response = await serverFetch<UniversitySearchResponse>(endpoint);

return response.ok ? response.data.univApplyInfoPreviews : [];
const params = new URLSearchParams();

if (filters.languageTestType) {
params.append("languageTestType", filters.languageTestType);
}
if (filters.testScore !== undefined) {
params.append("testScore", String(filters.testScore));
}
// countryCode는 여러 개일 수 있으므로 각각 append 해줍니다.
if (filters.countryCode) {
filters.countryCode.forEach((code) => params.append("countryCode", code));
}

// 필터 값이 하나도 없으면 빈 배열을 반환합니다.
if (params.size === 0) {
return [];
}

const endpoint = `/univ-apply-infos/search/filter?${params.toString()}`;
const response = await serverFetch<UniversitySearchResponse>(endpoint);

if (!response.ok) {
console.error(`Failed to search universities by filter:`, response.error);
return [];
}

return response.data.univApplyInfoPreviews;
};

export const getSearchUniversitiesAllRegions = async (): Promise<ListUniversity[]> => {
const endpoint = `/univ-apply-infos/search/filter`;
const response = await serverFetch<UniversitySearchResponse>(endpoint);
export const getSearchUniversitiesAllRegions = async (): Promise<
ListUniversity[]
> => {
const endpoint = `/univ-apply-infos/search/filter`;
const response = await serverFetch<UniversitySearchResponse>(endpoint);

if (!response.ok) {
console.error(`Failed to fetch all regions universities:`, response.error);
return [];
}

return response.ok ? response.data.univApplyInfoPreviews : [];
return response.data.univApplyInfoPreviews;
};
Loading