Skip to content
Merged
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
51 changes: 27 additions & 24 deletions apps/web/src/apis/Auth/postAppleAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,36 @@ import { type AppleAuthRequest, type AppleAuthResponse, authApi } from "./api";
* @description 애플 로그인을 위한 useMutation 커스텀 훅
*/
const usePostAppleAuth = () => {
const router = useRouter();
const searchParams = useSearchParams();
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);
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);
// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
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");
},
});
toast.success("로그인에 성공했습니다.");

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

export default usePostAppleAuth;
7 changes: 6 additions & 1 deletion apps/web/src/apis/Auth/postEmailLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ const usePostEmailAuth = () => {
const safeRedirect = validateSafeRedirect(redirectParam);

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

// Zustand persist middleware가 localStorage에 저장할 시간을 보장
// 토큰 저장 후 리다이렉트하여 타이밍 이슈 방지
setTimeout(() => {
router.push(safeRedirect);
}, 100);
},
});
};
Expand Down
53 changes: 28 additions & 25 deletions apps/web/src/apis/Auth/postKakaoAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,37 @@ import { authApi, type KakaoAuthRequest, type KakaoAuthResponse } from "./api";
* @description 카카오 로그인을 위한 useMutation 커스텀 훅
*/
const usePostKakaoAuth = () => {
const { setAccessToken } = useAuthStore();
const router = useRouter();
const searchParams = useSearchParams();
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);
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);
// 안전한 리다이렉트 처리 - 오픈 리다이렉트 방지
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");
},
});
toast.success("로그인에 성공했습니다.");

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

export default usePostKakaoAuth;
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import serverFetch from "@/utils/serverFetchUtil";
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);
return res;
const res = await serverFetch<GetRecommendedUniversityResponse>(endpoint);

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

return res;
};

export default getRecommendedUniversity;
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,60 @@ import type { CountryCode, LanguageTestType, ListUniversity } from "@/types/univ
import serverFetch from "@/utils/serverFetchUtil";

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);

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

return response.data.univApplyInfoPreviews;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,54 @@ import serverFetch from "@/utils/serverFetchUtil";

// --- 타입 정의 ---
interface UniversitySearchResponse {
univApplyInfoPreviews: ListUniversity[];
univApplyInfoPreviews: ListUniversity[];
}

export const getUniversitiesByText = async (value: string): Promise<ListUniversity[]> => {
if (value === null || value === undefined) {
return [];
}
const endpoint = `/univ-apply-infos/search/text?value=${encodeURIComponent(value)}`;
const response = await serverFetch<UniversitySearchResponse>(endpoint);
return response.ok ? response.data.univApplyInfoPreviews : [];
export const getUniversitiesByText = async (
value: string,
): Promise<ListUniversity[]> => {
if (value === null || value === undefined) {
return [];
}
const endpoint = `/univ-apply-infos/search/text?value=${encodeURIComponent(value)}`;
const response = await serverFetch<UniversitySearchResponse>(endpoint);

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

return response.data.univApplyInfoPreviews;
};

export const getAllUniversities = async (): Promise<ListUniversity[]> => {
return getUniversitiesByText("");
return getUniversitiesByText("");
};

export const getCategorizedUniversities = async (): Promise<AllRegionsUniversityList> => {
// 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다.
const allUniversities = await getAllUniversities();

const categorizedList: AllRegionsUniversityList = {
[RegionEnumExtend.ALL]: allUniversities,
[RegionEnumExtend.AMERICAS]: [],
[RegionEnumExtend.EUROPE]: [],
[RegionEnumExtend.ASIA]: [],
[RegionEnumExtend.CHINA]: [],
};
if (!allUniversities) return categorizedList;

for (const university of allUniversities) {
const region = university.region as RegionEnumExtend; // API 응답의 region 타입을 enum으로 간주

if (region && Object.hasOwn(categorizedList, region)) {
categorizedList[region].push(university);
}
}

return categorizedList;
};
export const getCategorizedUniversities =
async (): Promise<AllRegionsUniversityList> => {
// 1. 단 한 번의 API 호출로 모든 대학 데이터를 가져옵니다.
const allUniversities = await getAllUniversities();

const categorizedList: AllRegionsUniversityList = {
[RegionEnumExtend.ALL]: allUniversities,
[RegionEnumExtend.AMERICAS]: [],
[RegionEnumExtend.EUROPE]: [],
[RegionEnumExtend.ASIA]: [],
[RegionEnumExtend.CHINA]: [],
};
if (!allUniversities) return categorizedList;

for (const university of allUniversities) {
const region = university.region as RegionEnumExtend; // API 응답의 region 타입을 enum으로 간주

if (region && Object.hasOwn(categorizedList, region)) {
categorizedList[region].push(university);
}
}

return categorizedList;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@ import serverFetch from "@/utils/serverFetchUtil";
export const getUniversityDetail = async (universityInfoForApplyId: number): Promise<University | undefined> => {
const result = await serverFetch<University>(`/univ-apply-infos/${universityInfoForApplyId}`);

if (!result.ok) {
console.error(`Failed to fetch university detail (ID: ${universityInfoForApplyId}):`, result.error);
return undefined;
}

return result.data;
};
Loading
Loading