전체 학교 리스트
-
+
더보기
@@ -36,15 +29,15 @@ const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => {
diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx
index f1947f50..38f02a8c 100644
--- a/apps/web/src/app/(home)/page.tsx
+++ b/apps/web/src/app/(home)/page.tsx
@@ -1,16 +1,15 @@
import type { Metadata } from "next";
-import dynamic from "next/dynamic";
-import Link from "next/link";
+import nextDynamic from "next/dynamic";
import { getHomeNewsList } from "@/apis/news/server/getNewsList";
import { getCategorizedUniversities, getRecommendedUniversity } from "@/apis/universities/server";
-import { IconIdCard, IconMagnifyingGlass, IconMuseum, IconPaper } from "@/public/svgs/home";
-import { RegionEnumExtend } from "@/types/university";
+import { type ListUniversity, RegionEnumExtend } from "@/types/university";
import FindLastYearScoreBar from "./_ui/FindLastYearScoreBar";
+import HomeEntrySection from "./_ui/HomeEntrySection";
import NewsSectionSkeleton from "./_ui/NewsSection/skeleton";
import PopularUniversitySection from "./_ui/PopularUniversitySection";
import UniversityList from "./_ui/UniversityList";
-const NewsSectionDynamic = dynamic(() => import("./_ui/NewsSection"), {
+const NewsSectionDynamic = nextDynamic(() => import("./_ui/NewsSection"), {
ssr: false,
loading: () =>
,
});
@@ -65,6 +64,20 @@ const structuredData = {
},
};
+const resolveRecommendedUniversitiesHomeUniversityName = (
+ recommendedUniversities: ListUniversity[],
+ allUniversities: ListUniversity[],
+) => {
+ const homeUniversityNameById = new Map(
+ allUniversities.map((university) => [university.id, university.homeUniversityName]),
+ );
+
+ return recommendedUniversities.map((university) => ({
+ ...university,
+ homeUniversityName: university.homeUniversityName ?? homeUniversityNameById.get(university.id),
+ }));
+};
+
const HomePage = async () => {
const newsList = await getHomeNewsList();
const { data } = await getRecommendedUniversity();
@@ -72,73 +85,17 @@ const HomePage = async () => {
// 권역별 전체 대학 리스트를 미리 가져와 빌드합니다
const allRegionsUniversityList = await getCategorizedUniversities();
const allUniversities = allRegionsUniversityList[RegionEnumExtend.ALL] || [];
- const homeUniversityNameById = new Map(
- allUniversities.map((university) => [university.id, university.homeUniversityName]),
+ const resolvedRecommendedUniversities = resolveRecommendedUniversitiesHomeUniversityName(
+ recommendedUniversities,
+ allUniversities,
);
- const resolvedRecommendedUniversities = recommendedUniversities.map((university) => ({
- ...university,
- homeUniversityName: university.homeUniversityName ?? homeUniversityNameById.get(university.id),
- }));
return (
<>
-
-
-
-
- 학교 검색하기
- 모든 학교 목록을 확인해보세요
-
-
-
-
-
-
-
- 성적 입력하기
- 성적을 입력해보세요
-
-
-
-
-
-
-
-
-
- 학교 지원하기
- 학교를 지원해주세요
-
-
-
-
-
-
-
- 지원자 현황 확인
- 경쟁률을 바로 분석해드려요
-
-
-
-
-
-
-
+
실시간 인기있는 파견학교
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index cd9351da..f6d23c80 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -5,6 +5,7 @@ import type { ReactNode } from "react";
import { Toaster } from "react-hot-toast";
import GlobalLayout from "@/components/layout/GlobalLayout";
+import ReissueProvider from "@/components/layout/ReissueProvider";
import QueryProvider from "@/lib/react-query/QueryProvider";
import "@/styles/globals.css";
@@ -75,7 +76,9 @@ const RootLayout = ({ children }: { children: ReactNode }) => (
- {children}
+
+ {children}
+
{
// 필터 검색
const onSubmit: SubmitHandler = (data) => {
- const queryParams = new URLSearchParams();
- const availableCountryCodeSet = new Set(availableCountries.map(([code]) => code as CountryCode));
-
- if (data.languageTestType) {
- queryParams.append("languageTestType", data.languageTestType);
- }
-
- [data.country1, data.country2, data.country3].forEach((code) => {
- if (code && availableCountryCodeSet.has(code)) {
- queryParams.append("countryCode", code);
- }
- });
+ const queryString = buildUniversitySearchQuery({
+ languageTestType: data.languageTestType,
+ countryCodes: [data.country1, data.country2, data.country3],
+ regions: data.regions,
+ availableCountryCodes: availableCountries.map(([code]) => code),
+ }).toString();
- if (data.regions && data.regions.length > 0) {
- data.regions.forEach((region) => queryParams.append("region", region));
- }
-
- const queryString = queryParams.toString();
router.push(`/university/${homeUniversitySlug}?${queryString}`);
};
- const availableCountries = useMemo(() => {
- if (watchedRegions.length === 0) return Object.entries(COUNTRY_CODE_MAP);
- const countrySet = new Set();
- watchedRegions.forEach((region) => REGION_TO_COUNTRIES_MAP[region]?.forEach((country) => countrySet.add(country)));
- return Object.entries(COUNTRY_CODE_MAP).filter(([_, name]) => countrySet.has(name));
- }, [watchedRegions]);
-
- const countrySelectsToRender = useMemo(() => {
- const count = 1 + (watchedCountries[0] ? 1 : 0) + (watchedCountries[1] ? 1 : 0);
- return Array.from({ length: Math.min(count, 3) }, (_, i) => i + 1);
- }, [watchedCountries]);
+ const availableCountries = useMemo(() => getAvailableCountryOptionsByRegions(watchedRegions), [watchedRegions]);
+ const countrySelectsToRender = useMemo(() => getVisibleCountrySelectIndexes(watchedCountries), [watchedCountries]);
return (
<>
@@ -150,9 +132,9 @@ const SearchPageContent = ({ homeUniversitySlug }: SearchPageContentProps) => {
onChange={field.onChange}
placeholder="어학"
placeholderSelect="선택"
- placeholderIcon={}
- icon={}
- options={Object.entries(LANGUAGE_TEST_TYPE_MAP).map(([value, label]) => ({ value, label }))}
+ placeholderIcon={}
+ icon={}
+ options={getLanguageTestOptions()}
/>
)}
/>
@@ -167,7 +149,7 @@ const SearchPageContent = ({ homeUniversitySlug }: SearchPageContentProps) => {
control={control}
render={({ field }) => (
}
+ placeholderIcon={}
placeholderSelect="나라"
value={field.value ?? ""}
onChange={(value) => {
@@ -178,10 +160,12 @@ const SearchPageContent = ({ homeUniversitySlug }: SearchPageContentProps) => {
}
}}
placeholder="관심있는 나라"
- icon={}
- options={availableCountries
- .filter(([code]) => !watchedCountries.includes(code as CountryCode) || code === field.value)
- .map(([value, label]) => ({ value, label }))}
+ icon={}
+ options={getCountryOptions(
+ watchedCountries.map((country) => country ?? ""),
+ index - 1,
+ availableCountries,
+ )}
/>
)}
/>
@@ -200,25 +184,4 @@ const SearchPageContent = ({ homeUniversitySlug }: SearchPageContentProps) => {
);
};
-// 아이콘 컴포넌트
-const LanguageIcon = ({ className }: { className?: string }) => (
-
-);
-
-const LocationIcon = ({ className }: { className?: string }) => (
-
-);
-
export default SearchPageContent;
diff --git a/apps/web/src/app/university/score/submit/gpa/GpaSubmitForm.tsx b/apps/web/src/app/university/score/submit/gpa/GpaSubmitForm.tsx
index 46b59588..8913d584 100644
--- a/apps/web/src/app/university/score/submit/gpa/GpaSubmitForm.tsx
+++ b/apps/web/src/app/university/score/submit/gpa/GpaSubmitForm.tsx
@@ -7,11 +7,10 @@ import { useRouter } from "next/navigation";
import { Suspense, useState } from "react";
import { Controller, type SubmitHandler, useForm } from "react-hook-form";
import { usePostGpaScore } from "@/apis/Scores";
-import CustomDropdown from "@/app/university/CustomDropdown";
import SubmitLinkTab from "@/components/score/SubmitLinkTab";
import SubmitResult, { type InfoRowProps } from "@/components/score/SubmitResult";
+import CustomDropdown from "@/components/search/CustomDropdown";
import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage";
-// CustomDropdown 경로 확인 필요
import { type GpaFormData, gpaSchema } from "./_lib/schema";
const GpaSubmitForm = () => {
diff --git a/apps/web/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx b/apps/web/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx
index 69e677bd..18e3072c 100644
--- a/apps/web/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx
+++ b/apps/web/src/app/university/score/submit/language-test/LanguageTestSubmitForm.tsx
@@ -7,10 +7,9 @@ import { useRouter } from "next/navigation";
import { Suspense, useState } from "react";
import { Controller, type SubmitHandler, useForm } from "react-hook-form";
import { usePostLanguageTestScore } from "@/apis/Scores";
-import CustomDropdown from "@/app/university/CustomDropdown";
import SubmitLinkTab from "@/components/score/SubmitLinkTab";
-// CustomDropdown 경로 확인 필요
import SubmitResult, { type InfoRowProps } from "@/components/score/SubmitResult";
+import CustomDropdown from "@/components/search/CustomDropdown";
import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage";
import { formatLanguageTestScoreWithMax, LanguageTestEnum, languageTestScoreInfo } from "@/types/score";
import { type LanguageTestFormData, languageTestSchema } from "./_lib/schema";
diff --git a/apps/web/src/app/university/search/PageContent.tsx b/apps/web/src/app/university/search/PageContent.tsx
index 25dd7acf..e9a7f9cd 100644
--- a/apps/web/src/app/university/search/PageContent.tsx
+++ b/apps/web/src/app/university/search/PageContent.tsx
@@ -6,18 +6,17 @@ import { useRouter } from "next/navigation";
import { useMemo } from "react";
import { Controller, type SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
-
-// --- 상수, 타입, 아이콘 등 ---
-import {
- COUNTRY_CODE_MAP,
- LANGUAGE_TEST_TYPE_MAP,
- REGION_TO_COUNTRIES_MAP,
- REGIONS_SEARCH,
-} from "@/constants/university";
+import CustomDropdown from "@/components/search/CustomDropdown";
+import { REGIONS_SEARCH } from "@/constants/university";
+import { IconHatColor, IconHatGray, IconLocationColor, IconLocationGray } from "@/public/svgs/search";
import { CountryCode, type HomeUniversitySlug, LanguageTestType } from "@/types/university";
-import CustomDropdown from "../CustomDropdown";
-
-// --- 커스텀 드롭다운 컴포넌트 ---
+import {
+ buildUniversitySearchQuery,
+ getAvailableCountryOptionsByRegions,
+ getCountryOptions,
+ getLanguageTestOptions,
+ getVisibleCountrySelectIndexes,
+} from "@/utils/universitySearchQuery";
// --- Zod 스키마 및 타입 정의 ---
const searchSchema = z.object({
@@ -47,41 +46,19 @@ const SchoolSearchForm = ({ homeUniversitySlug }: SchoolSearchFormProps) => {
const watchedCountries = watch(["country1", "country2", "country3"]);
const onSubmit: SubmitHandler = (data) => {
- const queryParams = new URLSearchParams();
- const availableCountryCodeSet = new Set(availableCountries.map(([code]) => code as CountryCode));
+ const queryString = buildUniversitySearchQuery({
+ searchText: data.searchText,
+ languageTestType: data.languageTestType,
+ countryCodes: [data.country1, data.country2, data.country3],
+ regions: data.regions,
+ availableCountryCodes: availableCountries.map(([code]) => code),
+ }).toString();
- if (data.searchText) {
- queryParams.append("searchText", data.searchText);
- }
- if (data.languageTestType) {
- queryParams.append("languageTestType", data.languageTestType);
- }
-
- [data.country1, data.country2, data.country3].forEach((code) => {
- if (code && availableCountryCodeSet.has(code)) {
- queryParams.append("countryCode", code);
- }
- });
-
- if (data.regions && data.regions.length > 0) {
- data.regions.forEach((region) => queryParams.append("region", region));
- }
-
- const queryString = queryParams.toString();
router.push(`/university/${homeUniversitySlug}?${queryString}`);
};
- const availableCountries = useMemo(() => {
- if (watchedRegions.length === 0) return Object.entries(COUNTRY_CODE_MAP);
- const countrySet = new Set();
- watchedRegions.forEach((region) => REGION_TO_COUNTRIES_MAP[region]?.forEach((country) => countrySet.add(country)));
- return Object.entries(COUNTRY_CODE_MAP).filter(([_, name]) => countrySet.has(name));
- }, [watchedRegions]);
-
- const countrySelectsToRender = useMemo(() => {
- const count = 1 + (watchedCountries[0] ? 1 : 0) + (watchedCountries[1] ? 1 : 0);
- return Array.from({ length: Math.min(count, 3) }, (_, i) => i + 1);
- }, [watchedCountries]);
+ const availableCountries = useMemo(() => getAvailableCountryOptionsByRegions(watchedRegions), [watchedRegions]);
+ const countrySelectsToRender = useMemo(() => getVisibleCountrySelectIndexes(watchedCountries), [watchedCountries]);
return (