From 2732cb54298dddb152473e11b88fc0e23e9d097c Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Sun, 10 May 2026 21:52:36 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(web):=20=EB=B9=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=99=88=20=ED=95=99=EA=B5=90=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(home)/_ui/HomeActionLinks/index.tsx | 58 ++++++++++++ .../app/(home)/_ui/HomeEntrySection/index.tsx | 18 ++++ .../_ui/HomeEntrySection/skeleton/index.tsx | 20 +++++ .../_hooks/useHomeUniversitySearch.ts | 69 +++++++++++++++ .../_ui/HomeUniversitySearchSection/index.tsx | 88 +++++++++++++++++++ .../_hooks/useHomeUniversityList.ts | 50 +++++++++++ .../UniversityList/_hooks/useRegionHandler.ts | 23 ----- .../app/(home)/_ui/UniversityList/index.tsx | 23 ++--- apps/web/src/app/(home)/page.tsx | 87 +++++------------- apps/web/src/app/layout.tsx | 5 +- .../search/_ui/SearchPageContent.tsx | 43 ++++----- .../score/submit/gpa/GpaSubmitForm.tsx | 3 +- .../language-test/LanguageTestSubmitForm.tsx | 3 +- .../src/app/university/search/PageContent.tsx | 48 ++++------ .../src/components/layout/ReissueProvider.tsx | 1 + .../layout/ReissueProvider/index.tsx | 59 ++++++------- .../search}/CustomDropdown.tsx | 18 ++-- apps/web/src/lib/zustand/useAuthStore.ts | 7 ++ apps/web/src/utils/universitySearchQuery.ts | 74 ++++++++++++++++ 19 files changed, 490 insertions(+), 207 deletions(-) create mode 100644 apps/web/src/app/(home)/_ui/HomeActionLinks/index.tsx create mode 100644 apps/web/src/app/(home)/_ui/HomeEntrySection/index.tsx create mode 100644 apps/web/src/app/(home)/_ui/HomeEntrySection/skeleton/index.tsx create mode 100644 apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/_hooks/useHomeUniversitySearch.ts create mode 100644 apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/index.tsx create mode 100644 apps/web/src/app/(home)/_ui/UniversityList/_hooks/useHomeUniversityList.ts delete mode 100644 apps/web/src/app/(home)/_ui/UniversityList/_hooks/useRegionHandler.ts rename apps/web/src/{app/university => components/search}/CustomDropdown.tsx (83%) create mode 100644 apps/web/src/utils/universitySearchQuery.ts diff --git a/apps/web/src/app/(home)/_ui/HomeActionLinks/index.tsx b/apps/web/src/app/(home)/_ui/HomeActionLinks/index.tsx new file mode 100644 index 00000000..38b65bf6 --- /dev/null +++ b/apps/web/src/app/(home)/_ui/HomeActionLinks/index.tsx @@ -0,0 +1,58 @@ +import Link from "next/link"; + +import { IconIdCard, IconMagnifyingGlass, IconMuseum, IconPaper } from "@/public/svgs/home"; + +const HomeActionLinks = () => { + return ( +
+
+ +
+ 학교 검색하기 + 모든 학교 목록을 확인해보세요 +
+
+ +
+ + +
+ 성적 입력하기 + 성적을 입력해보세요 +
+
+ +
+ +
+
+ +
+ 학교 지원하기 + 학교를 지원해주세요 +
+
+ +
+ + +
+ 지원자 현황 확인 + 경쟁률을 바로 분석해드려요 +
+
+ +
+ +
+
+ ); +}; + +export default HomeActionLinks; diff --git a/apps/web/src/app/(home)/_ui/HomeEntrySection/index.tsx b/apps/web/src/app/(home)/_ui/HomeEntrySection/index.tsx new file mode 100644 index 00000000..b4fc2072 --- /dev/null +++ b/apps/web/src/app/(home)/_ui/HomeEntrySection/index.tsx @@ -0,0 +1,18 @@ +"use client"; + +import useAuthStore from "@/lib/zustand/useAuthStore"; +import HomeActionLinks from "../HomeActionLinks"; +import HomeUniversitySearchSection from "../HomeUniversitySearchSection"; +import HomeEntrySectionSkeleton from "./skeleton"; + +const HomeEntrySection = () => { + const { isAuthenticated, isInitialized } = useAuthStore(); + + if (!isInitialized) { + return ; + } + + return isAuthenticated ? : ; +}; + +export default HomeEntrySection; diff --git a/apps/web/src/app/(home)/_ui/HomeEntrySection/skeleton/index.tsx b/apps/web/src/app/(home)/_ui/HomeEntrySection/skeleton/index.tsx new file mode 100644 index 00000000..f5c16732 --- /dev/null +++ b/apps/web/src/app/(home)/_ui/HomeEntrySection/skeleton/index.tsx @@ -0,0 +1,20 @@ +const HomeEntrySectionSkeleton = () => ( +
+
+ +
+ {[...Array(3)].map((_, index) => ( +
+ ))} +
+ +
+
+
+
+ +
+
+); + +export default HomeEntrySectionSkeleton; diff --git a/apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/_hooks/useHomeUniversitySearch.ts b/apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/_hooks/useHomeUniversitySearch.ts new file mode 100644 index 00000000..61bb5346 --- /dev/null +++ b/apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/_hooks/useHomeUniversitySearch.ts @@ -0,0 +1,69 @@ +import { useRouter } from "next/navigation"; +import { useMemo, useState } from "react"; + +import { HOME_UNIVERSITY_LIST } from "@/constants/university"; +import type { HomeUniversitySlug } from "@/types/university"; +import { + buildUniversitySearchQuery, + getCountryOptionsByIndex, + getLanguageTestOptions, +} from "@/utils/universitySearchQuery"; + +const MAX_COUNTRY_SELECT_COUNT = 3; + +const createEmptyCountries = () => Array(MAX_COUNTRY_SELECT_COUNT).fill(""); + +const useHomeUniversitySearch = () => { + const router = useRouter(); + const [selectedHomeUniversitySlug, setSelectedHomeUniversitySlug] = useState( + HOME_UNIVERSITY_LIST[0].slug, + ); + const [languageTestType, setLanguageTestType] = useState(""); + const [countries, setCountries] = useState(createEmptyCountries); + + const languageOptions = useMemo(() => getLanguageTestOptions(), []); + + const visibleCountryCount = useMemo(() => { + const selectedCount = countries.filter(Boolean).length; + return Math.min(selectedCount + 1, MAX_COUNTRY_SELECT_COUNT); + }, [countries]); + + const countryOptionsByIndex = useMemo( + () => getCountryOptionsByIndex({ countries, visibleCount: visibleCountryCount }), + [countries, visibleCountryCount], + ); + + const handleCountryChange = (index: number, value: string) => { + setCountries((prevCountries) => + prevCountries.map((country, countryIndex) => { + if (countryIndex < index) return country; + if (countryIndex === index) return value; + return value ? country : ""; + }), + ); + }; + + const submitSearch = () => { + const queryString = buildUniversitySearchQuery({ + languageTestType, + countryCodes: countries, + }).toString(); + + router.push(`/university/${selectedHomeUniversitySlug}${queryString ? `?${queryString}` : ""}`); + }; + + return { + homeUniversities: HOME_UNIVERSITY_LIST, + selectedHomeUniversitySlug, + setSelectedHomeUniversitySlug, + languageTestType, + setLanguageTestType, + countries, + languageOptions, + countryOptionsByIndex, + handleCountryChange, + submitSearch, + }; +}; + +export default useHomeUniversitySearch; diff --git a/apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/index.tsx b/apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/index.tsx new file mode 100644 index 00000000..b84ae01c --- /dev/null +++ b/apps/web/src/app/(home)/_ui/HomeUniversitySearchSection/index.tsx @@ -0,0 +1,88 @@ +"use client"; + +import clsx from "clsx"; + +import CustomDropdown from "@/components/search/CustomDropdown"; +import { IconHatColor, IconHatGray, IconLocationColor, IconLocationGray } from "@/public/svgs/search"; +import useHomeUniversitySearch from "./_hooks/useHomeUniversitySearch"; + +const HomeUniversitySearchSection = () => { + const { + homeUniversities, + selectedHomeUniversitySlug, + setSelectedHomeUniversitySlug, + languageTestType, + setLanguageTestType, + countries, + languageOptions, + countryOptionsByIndex, + handleCountryChange, + submitSearch, + } = useHomeUniversitySearch(); + + return ( +
+

파견 학교 찾기

+ +
+ {homeUniversities.map((university) => { + const isSelected = university.slug === selectedHomeUniversitySlug; + + return ( + + ); + })} +
+ +
+ } + icon={} + options={languageOptions} + /> + + {countryOptionsByIndex.map((countryOptions, index) => { + return ( + handleCountryChange(index, value)} + placeholder="관심있는 나라" + placeholderSelect="나라" + placeholderIcon={} + icon={} + options={countryOptions} + /> + ); + })} +
+ + +
+ ); +}; + +export default HomeUniversitySearchSection; diff --git a/apps/web/src/app/(home)/_ui/UniversityList/_hooks/useHomeUniversityList.ts b/apps/web/src/app/(home)/_ui/UniversityList/_hooks/useHomeUniversityList.ts new file mode 100644 index 00000000..d28f2385 --- /dev/null +++ b/apps/web/src/app/(home)/_ui/UniversityList/_hooks/useHomeUniversityList.ts @@ -0,0 +1,50 @@ +import { type Dispatch, type SetStateAction, useMemo, useState } from "react"; + +import { HOME_UNIVERSITY_LIST, isMatchedHomeUniversityName } from "@/constants/university"; +import { type AllRegionsUniversityList, type ListUniversity, RegionEnumExtend } from "@/types/university"; + +const ALL_HOME_UNIVERSITY_CHOICE = "전체"; +const PREVIEW_UNIVERSITY_COUNT = 3; + +const useHomeUniversityList = (allRegionsUniversityList: AllRegionsUniversityList) => { + const [selectedHomeUniversity, setSelectedHomeUniversity] = useState(ALL_HOME_UNIVERSITY_CHOICE); + const handleHomeUniversityChange: Dispatch> = (nextHomeUniversity) => { + setSelectedHomeUniversity((prevHomeUniversity) => { + const resolvedHomeUniversity = + typeof nextHomeUniversity === "function" ? nextHomeUniversity(prevHomeUniversity) : nextHomeUniversity; + + return resolvedHomeUniversity ?? ALL_HOME_UNIVERSITY_CHOICE; + }); + }; + const homeUniversityChoices = useMemo( + () => [ALL_HOME_UNIVERSITY_CHOICE, ...HOME_UNIVERSITY_LIST.map((university) => university.shortName)], + [], + ); + + const allUniversities = allRegionsUniversityList[RegionEnumExtend.ALL] ?? []; + const selectedUniversityInfo = useMemo( + () => HOME_UNIVERSITY_LIST.find((university) => university.shortName === selectedHomeUniversity), + [selectedHomeUniversity], + ); + + const universities: ListUniversity[] = useMemo(() => { + if (!selectedUniversityInfo) return allUniversities; + + return allUniversities.filter((university) => + isMatchedHomeUniversityName(university.homeUniversityName, selectedUniversityInfo.name), + ); + }, [allUniversities, selectedUniversityInfo]); + + const previewUniversities = useMemo(() => universities.slice(0, PREVIEW_UNIVERSITY_COUNT), [universities]); + const moreHref = selectedUniversityInfo ? `/university/${selectedUniversityInfo.slug}` : "/university"; + + return { + selectedHomeUniversity, + setSelectedHomeUniversity: handleHomeUniversityChange, + homeUniversityChoices, + previewUniversities, + moreHref, + }; +}; + +export default useHomeUniversityList; diff --git a/apps/web/src/app/(home)/_ui/UniversityList/_hooks/useRegionHandler.ts b/apps/web/src/app/(home)/_ui/UniversityList/_hooks/useRegionHandler.ts deleted file mode 100644 index 12b467cf..00000000 --- a/apps/web/src/app/(home)/_ui/UniversityList/_hooks/useRegionHandler.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useState } from "react"; - -import { RegionEnumExtend } from "@/types/university"; - -const useRegionHandler = () => { - const [region, setRegion] = useState(RegionEnumExtend.ALL); - - const handleRegionChange = (newRegion: RegionEnumExtend | null) => { - setRegion(newRegion); - }; - useEffect(() => { - if (region === null) { - setRegion(RegionEnumExtend.ALL); - } - }, [region]); - - return { - region, - handleRegionChange, - }; -}; - -export default useRegionHandler; diff --git a/apps/web/src/app/(home)/_ui/UniversityList/index.tsx b/apps/web/src/app/(home)/_ui/UniversityList/index.tsx index 5722b8ab..2de196f5 100644 --- a/apps/web/src/app/(home)/_ui/UniversityList/index.tsx +++ b/apps/web/src/app/(home)/_ui/UniversityList/index.tsx @@ -1,34 +1,27 @@ "use client"; import Link from "next/link"; -import { useMemo } from "react"; import ButtonTab from "@/components/ui/ButtonTab"; import UniversityCards from "@/components/university/UniversityCards"; import { IconDirectionRight } from "@/public/svgs/mentor"; -import { type AllRegionsUniversityList, type ListUniversity, RegionEnumExtend } from "@/types/university"; -import useRegionHandler from "./_hooks/useRegionHandler"; +import type { AllRegionsUniversityList } from "@/types/university"; +import useHomeUniversityList from "./_hooks/useHomeUniversityList"; interface UniversityListProps { allRegionsUniversityList: AllRegionsUniversityList; } const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => { - const { region, handleRegionChange } = useRegionHandler(); - const choices = Object.values(RegionEnumExtend); + const { selectedHomeUniversity, setSelectedHomeUniversity, homeUniversityChoices, previewUniversities, moreHref } = + useHomeUniversityList(allRegionsUniversityList); - const universities: ListUniversity[] = useMemo( - () => allRegionsUniversityList[region || RegionEnumExtend.ALL] ?? [], - [allRegionsUniversityList, region], - ); - // 홈 카드 영역에는 최대 3개만 노출 - const previewUniversities: ListUniversity[] = useMemo(() => universities.slice(0, 3), [universities]); return (
전체 학교 리스트 - + 더보기 @@ -36,9 +29,9 @@ const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => {
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 ( <>