From f2a292b9dac22999ba2ed39051ff4d21a4d1388b Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 21 Feb 2026 01:17:41 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=EB=8C=80=ED=95=99=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=B3=B8=20=EC=9D=B4=EB=8F=99=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EB=A5=BC=20=EC=9D=B8=ED=95=98=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/university/search/PageContent.tsx | 6 ++++-- apps/web/src/app/university/search/SearchBar.tsx | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/university/search/PageContent.tsx b/apps/web/src/app/university/search/PageContent.tsx index 469816dd..11e4b30e 100644 --- a/apps/web/src/app/university/search/PageContent.tsx +++ b/apps/web/src/app/university/search/PageContent.tsx @@ -10,11 +10,12 @@ import { z } from "zod"; // --- 상수, 타입, 아이콘 등 --- import { COUNTRY_CODE_MAP, + HOME_UNIVERSITY_TO_SLUG_MAP, LANGUAGE_TEST_TYPE_MAP, REGION_TO_COUNTRIES_MAP, REGIONS_SEARCH, } from "@/constants/university"; -import { CountryCode, LanguageTestType } from "@/types/university"; +import { CountryCode, HomeUniversity, LanguageTestType } from "@/types/university"; import CustomDropdown from "../CustomDropdown"; // --- 커스텀 드롭다운 컴포넌트 --- @@ -33,6 +34,7 @@ type SearchFormData = z.infer; // --- 메인 폼 컴포넌트 --- const SchoolSearchForm = () => { const router = useRouter(); + const defaultHomeUniversitySlug = HOME_UNIVERSITY_TO_SLUG_MAP[HomeUniversity.INHA]; const { handleSubmit, control, watch, setValue } = useForm({ resolver: zodResolver(searchSchema), @@ -59,7 +61,7 @@ const SchoolSearchForm = () => { }); const queryString = queryParams.toString(); - router.push(`/university?${queryString}`); + router.push(`/university/${defaultHomeUniversitySlug}?${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..b9f718b1 100644 --- a/apps/web/src/app/university/search/SearchBar.tsx +++ b/apps/web/src/app/university/search/SearchBar.tsx @@ -4,6 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; import { type SubmitHandler, useForm } from "react-hook-form"; import { z } from "zod"; +import { HOME_UNIVERSITY_TO_SLUG_MAP } from "@/constants/university"; +import { HomeUniversity } from "@/types/university"; const searchSchema = z.object({ searchText: z.string().min(1, "검색어를 입력해주세요.").max(50, "최대 50자까지 입력 가능합니다."), @@ -27,6 +29,7 @@ interface SearchBarProps { // --- 폼 로직을 관리하는 부모 컴포넌트 --- const SearchBar = ({ initText }: SearchBarProps) => { const router = useRouter(); + const defaultHomeUniversitySlug = HOME_UNIVERSITY_TO_SLUG_MAP[HomeUniversity.INHA]; const { register, @@ -47,7 +50,7 @@ const SearchBar = ({ initText }: SearchBarProps) => { } const queryString = queryParams.toString(); - router.push(`/university?${queryString}`); + router.push(`/university/${defaultHomeUniversitySlug}?${queryString}`); }; return ( From e2f46b269dcb5d7814b067759428d6d4f80284f0 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 21 Feb 2026 01:38:02 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EB=8C=80=ED=95=99=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=83=81=EB=8B=A8=20=ED=99=88=EB=8C=80=ED=95=99=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EC=9D=84=20=EA=B8=B0=EB=B3=B8=20=EC=9D=B8?= =?UTF-8?q?=ED=95=98=EB=8C=80=EB=A1=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/university/search/PageContent.tsx | 12 +++-- .../src/app/university/search/SearchBar.tsx | 9 ++-- .../university/search/SearchClientContent.tsx | 49 +++++++++++++++++++ apps/web/src/app/university/search/page.tsx | 8 +-- 4 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/app/university/search/SearchClientContent.tsx diff --git a/apps/web/src/app/university/search/PageContent.tsx b/apps/web/src/app/university/search/PageContent.tsx index 11e4b30e..b6e8966a 100644 --- a/apps/web/src/app/university/search/PageContent.tsx +++ b/apps/web/src/app/university/search/PageContent.tsx @@ -10,12 +10,11 @@ import { z } from "zod"; // --- 상수, 타입, 아이콘 등 --- import { COUNTRY_CODE_MAP, - HOME_UNIVERSITY_TO_SLUG_MAP, LANGUAGE_TEST_TYPE_MAP, REGION_TO_COUNTRIES_MAP, REGIONS_SEARCH, } from "@/constants/university"; -import { CountryCode, HomeUniversity, LanguageTestType } from "@/types/university"; +import { CountryCode, type HomeUniversitySlug, LanguageTestType } from "@/types/university"; import CustomDropdown from "../CustomDropdown"; // --- 커스텀 드롭다운 컴포넌트 --- @@ -31,10 +30,13 @@ const searchSchema = z.object({ }); type SearchFormData = z.infer; +interface SchoolSearchFormProps { + homeUniversitySlug: HomeUniversitySlug; +} + // --- 메인 폼 컴포넌트 --- -const SchoolSearchForm = () => { +const SchoolSearchForm = ({ homeUniversitySlug }: SchoolSearchFormProps) => { const router = useRouter(); - const defaultHomeUniversitySlug = HOME_UNIVERSITY_TO_SLUG_MAP[HomeUniversity.INHA]; const { handleSubmit, control, watch, setValue } = useForm({ resolver: zodResolver(searchSchema), @@ -61,7 +63,7 @@ const SchoolSearchForm = () => { }); const queryString = queryParams.toString(); - router.push(`/university/${defaultHomeUniversitySlug}?${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 b9f718b1..7c0d1d4d 100644 --- a/apps/web/src/app/university/search/SearchBar.tsx +++ b/apps/web/src/app/university/search/SearchBar.tsx @@ -4,8 +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 { HOME_UNIVERSITY_TO_SLUG_MAP } from "@/constants/university"; -import { HomeUniversity } from "@/types/university"; +import type { HomeUniversitySlug } from "@/types/university"; const searchSchema = z.object({ searchText: z.string().min(1, "검색어를 입력해주세요.").max(50, "최대 50자까지 입력 가능합니다."), @@ -25,11 +24,11 @@ const SearchIcon = () => ( interface SearchBarProps { initText?: string; + homeUniversitySlug: HomeUniversitySlug; } // --- 폼 로직을 관리하는 부모 컴포넌트 --- -const SearchBar = ({ initText }: SearchBarProps) => { +const SearchBar = ({ initText, homeUniversitySlug }: SearchBarProps) => { const router = useRouter(); - const defaultHomeUniversitySlug = HOME_UNIVERSITY_TO_SLUG_MAP[HomeUniversity.INHA]; const { register, @@ -50,7 +49,7 @@ const SearchBar = ({ initText }: SearchBarProps) => { } const queryString = queryParams.toString(); - router.push(`/university/${defaultHomeUniversitySlug}?${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 () => {

오직 나를 위한

맞춤 파견 학교 찾기

-
- -
- +
From bf4012bd5568805598837ce4d09537f059db2e92 Mon Sep 17 00:00:00 2001 From: manNomi Date: Sat, 21 Feb 2026 01:50:12 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=ED=99=88=EB=8C=80=ED=95=99=20?= =?UTF-8?q?=EB=8C=80=ED=95=99=20=EB=AA=A9=EB=A1=9D=20URL=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=ED=95=84=ED=84=B0=EB=A5=BC=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_ui/UniversityListContent.tsx | 24 +++++- .../app/university/[homeUniversity]/page.tsx | 75 +++++++++++++++++-- 2 files changed, 88 insertions(+), 11 deletions(-) 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} /> );