diff --git a/apps/web/src/app/university/[homeUniversity]/_ui/UniversityListContent.tsx b/apps/web/src/app/university/[homeUniversity]/_ui/UniversityListContent.tsx index 97df47b1..0d397c5a 100644 --- a/apps/web/src/app/university/[homeUniversity]/_ui/UniversityListContent.tsx +++ b/apps/web/src/app/university/[homeUniversity]/_ui/UniversityListContent.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import FloatingUpBtn from "@/components/ui/FloatingUpBtn"; import UniversityCards from "@/components/university/UniversityCards"; @@ -14,11 +14,27 @@ interface UniversityListContentProps { universities: ListUniversity[]; homeUniversity: HomeUniversityInfo; homeUniversitySlug: string; + initialSearchText?: string; + initialRegion?: RegionEnumExtend | "전체"; } -const UniversityListContent = ({ universities, homeUniversity, homeUniversitySlug }: UniversityListContentProps) => { - const [searchText, setSearchText] = useState(""); - const [selectedRegion, setSelectedRegion] = useState("전체"); +const UniversityListContent = ({ + universities, + homeUniversity, + homeUniversitySlug, + initialSearchText = "", + initialRegion = "전체", +}: UniversityListContentProps) => { + const [searchText, setSearchText] = useState(initialSearchText.trim()); + const [selectedRegion, setSelectedRegion] = useState(initialRegion); + + useEffect(() => { + setSearchText(initialSearchText.trim()); + }, [initialSearchText]); + + useEffect(() => { + setSelectedRegion(initialRegion); + }, [initialRegion]); // 검색어 및 지역 필터링 const filteredUniversities = useMemo(() => { diff --git a/apps/web/src/app/university/[homeUniversity]/page.tsx b/apps/web/src/app/university/[homeUniversity]/page.tsx index f9490a38..422c1be0 100644 --- a/apps/web/src/app/university/[homeUniversity]/page.tsx +++ b/apps/web/src/app/university/[homeUniversity]/page.tsx @@ -1,10 +1,15 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; -import { getSearchUniversitiesAllRegions } from "@/apis/universities/server"; +import { getSearchUniversitiesAllRegions, getSearchUniversitiesByFilter } from "@/apis/universities/server"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import { getHomeUniversityBySlug, HOME_UNIVERSITY_SLUGS, isMatchedHomeUniversityName } from "@/constants/university"; -import type { HomeUniversitySlug } from "@/types/university"; +import { + COUNTRY_CODE_MAP, + getHomeUniversityBySlug, + HOME_UNIVERSITY_SLUGS, + isMatchedHomeUniversityName, +} from "@/constants/university"; +import { type CountryCode, type HomeUniversitySlug, LanguageTestType, RegionEnumExtend } from "@/types/university"; import UniversityListContent from "./_ui/UniversityListContent"; @@ -19,6 +24,42 @@ export async function generateStaticParams() { type PageProps = { params: Promise<{ homeUniversity: string }>; + searchParams?: Record; +}; + +type SearchParamValue = string | string[] | undefined; + +const getSearchParamValues = (value: SearchParamValue): string[] => { + if (!value) { + return []; + } + + return Array.isArray(value) ? value : [value]; +}; + +const getFirstSearchParamValue = (value: SearchParamValue): string => { + if (Array.isArray(value)) { + return value[0] ?? ""; + } + + return value ?? ""; +}; + +const isCountryCode = (value: string): value is CountryCode => { + return Object.hasOwn(COUNTRY_CODE_MAP, value); +}; + +const isLanguageTestType = (value: string): value is LanguageTestType => { + return Object.values(LanguageTestType).includes(value as LanguageTestType); +}; + +const isRegionFilterValue = (value: string): value is RegionEnumExtend => { + return ( + value === RegionEnumExtend.AMERICAS || + value === RegionEnumExtend.EUROPE || + value === RegionEnumExtend.ASIA || + value === RegionEnumExtend.CHINA + ); }; export async function generateMetadata({ params }: PageProps): Promise { @@ -37,8 +78,14 @@ export async function generateMetadata({ params }: PageProps): Promise }; } -const UniversityListPage = async ({ params }: PageProps) => { +const UniversityListPage = async ({ params, searchParams }: PageProps) => { const { homeUniversity } = await params; + const initialSearchText = getFirstSearchParamValue(searchParams?.searchText).trim(); + const languageTestTypeParam = getFirstSearchParamValue(searchParams?.languageTestType).trim(); + const countryCodeParams = getSearchParamValues(searchParams?.countryCode).filter(isCountryCode); + const regionParams = getSearchParamValues(searchParams?.region).filter(isRegionFilterValue); + const initialRegion = regionParams.length === 1 ? regionParams[0] : RegionEnumExtend.ALL; + const languageTestType = isLanguageTestType(languageTestTypeParam) ? languageTestTypeParam : undefined; // 유효한 슬러그인지 확인 if (!HOME_UNIVERSITY_SLUGS.includes(homeUniversity as HomeUniversitySlug)) { @@ -51,14 +98,26 @@ const UniversityListPage = async ({ params }: PageProps) => { notFound(); } - // 전체 대학 목록을 서버에서 가져옴 (ISR 캐시됨) - const allUniversities = await getSearchUniversitiesAllRegions(); + const shouldUseFilterApi = Boolean(languageTestType) || countryCodeParams.length > 0; + + const allUniversities = shouldUseFilterApi + ? await getSearchUniversitiesByFilter({ + languageTestType, + countryCode: countryCodeParams.length > 0 ? countryCodeParams : undefined, + }) + : await getSearchUniversitiesAllRegions(); // homeUniversityName으로 프론트에서 필터링 - const filteredUniversities = allUniversities.filter((university) => + let filteredUniversities = allUniversities.filter((university) => isMatchedHomeUniversityName(university.homeUniversityName, universityInfo.name), ); + if (regionParams.length > 0) { + filteredUniversities = filteredUniversities.filter((university) => + regionParams.includes(university.region as RegionEnumExtend), + ); + } + return ( <> @@ -66,6 +125,8 @@ const UniversityListPage = async ({ params }: PageProps) => { universities={filteredUniversities} homeUniversity={universityInfo} homeUniversitySlug={homeUniversity} + initialSearchText={initialSearchText} + initialRegion={initialRegion} /> ); diff --git a/apps/web/src/app/university/search/PageContent.tsx b/apps/web/src/app/university/search/PageContent.tsx index 469816dd..b6e8966a 100644 --- a/apps/web/src/app/university/search/PageContent.tsx +++ b/apps/web/src/app/university/search/PageContent.tsx @@ -14,7 +14,7 @@ import { REGION_TO_COUNTRIES_MAP, REGIONS_SEARCH, } from "@/constants/university"; -import { CountryCode, LanguageTestType } from "@/types/university"; +import { CountryCode, type HomeUniversitySlug, LanguageTestType } from "@/types/university"; import CustomDropdown from "../CustomDropdown"; // --- 커스텀 드롭다운 컴포넌트 --- @@ -30,8 +30,12 @@ const searchSchema = z.object({ }); type SearchFormData = z.infer; +interface SchoolSearchFormProps { + homeUniversitySlug: HomeUniversitySlug; +} + // --- 메인 폼 컴포넌트 --- -const SchoolSearchForm = () => { +const SchoolSearchForm = ({ homeUniversitySlug }: SchoolSearchFormProps) => { const router = useRouter(); const { handleSubmit, control, watch, setValue } = useForm({ @@ -59,7 +63,7 @@ const SchoolSearchForm = () => { }); const queryString = queryParams.toString(); - router.push(`/university?${queryString}`); + router.push(`/university/${homeUniversitySlug}?${queryString}`); }; const availableCountries = useMemo(() => { diff --git a/apps/web/src/app/university/search/SearchBar.tsx b/apps/web/src/app/university/search/SearchBar.tsx index 70ed40d0..7c0d1d4d 100644 --- a/apps/web/src/app/university/search/SearchBar.tsx +++ b/apps/web/src/app/university/search/SearchBar.tsx @@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { type SubmitHandler, useForm } from "react-hook-form"; import { z } from "zod"; +import type { HomeUniversitySlug } from "@/types/university"; const searchSchema = z.object({ searchText: z.string().min(1, "검색어를 입력해주세요.").max(50, "최대 50자까지 입력 가능합니다."), @@ -23,9 +24,10 @@ const SearchIcon = () => ( interface SearchBarProps { initText?: string; + homeUniversitySlug: HomeUniversitySlug; } // --- 폼 로직을 관리하는 부모 컴포넌트 --- -const SearchBar = ({ initText }: SearchBarProps) => { +const SearchBar = ({ initText, homeUniversitySlug }: SearchBarProps) => { const router = useRouter(); const { @@ -47,7 +49,7 @@ const SearchBar = ({ initText }: SearchBarProps) => { } const queryString = queryParams.toString(); - router.push(`/university?${queryString}`); + router.push(`/university/${homeUniversitySlug}?${queryString}`); }; return ( diff --git a/apps/web/src/app/university/search/SearchClientContent.tsx b/apps/web/src/app/university/search/SearchClientContent.tsx new file mode 100644 index 00000000..22233b47 --- /dev/null +++ b/apps/web/src/app/university/search/SearchClientContent.tsx @@ -0,0 +1,49 @@ +"use client"; + +import clsx from "clsx"; +import { useState } from "react"; +import { HOME_UNIVERSITY_LIST, HOME_UNIVERSITY_TO_SLUG_MAP } from "@/constants/university"; +import { HomeUniversity, type HomeUniversitySlug } from "@/types/university"; +import SchoolSearchForm from "./PageContent"; +import SearchBar from "./SearchBar"; + +const SearchClientContent = () => { + const [selectedHomeUniversitySlug, setSelectedHomeUniversitySlug] = useState( + HOME_UNIVERSITY_TO_SLUG_MAP[HomeUniversity.INHA], + ); + + return ( + <> +
+ {HOME_UNIVERSITY_LIST.map((university) => { + const isSelected = university.slug === selectedHomeUniversitySlug; + + return ( + + ); + })} +
+ +
+ +
+ + + + ); +}; + +export default SearchClientContent; diff --git a/apps/web/src/app/university/search/page.tsx b/apps/web/src/app/university/search/page.tsx index fb4333c6..a019c2cd 100644 --- a/apps/web/src/app/university/search/page.tsx +++ b/apps/web/src/app/university/search/page.tsx @@ -3,8 +3,7 @@ import dynamic from "next/dynamic"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -const SearchBar = dynamic(() => import("./SearchBar"), { ssr: false }); -const SchoolSearchForm = dynamic(() => import("./PageContent"), { ssr: false }); +const SearchClientContent = dynamic(() => import("./SearchClientContent"), { ssr: false }); export const metadata: Metadata = { title: "파견 학교 목록", @@ -18,10 +17,7 @@ const Page = async () => {

오직 나를 위한

맞춤 파견 학교 찾기

-
- -
- +