Skip to content

Commit 31455c9

Browse files
authored
Merge pull request #218 from manNomi/refactor/home-universitylist
Refactor/home universitylist
2 parents cb19a4b + 6ae4302 commit 31455c9

File tree

13 files changed

+207
-70
lines changed

13 files changed

+207
-70
lines changed

src/api/university/client/useGetRecommendedUniversity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const getRecommendedUniversity = async ({
99
}: {
1010
queryKey: [string, boolean];
1111
}): Promise<RecommendedUniversityResponse> => {
12-
const endpoint = "/univ-apply-infos/recommend";
12+
const endpoint = "/universities/recommend";
1313

1414
const [, isLogin] = queryKey;
1515
const instance = isLogin ? axiosInstance : publicAxiosInstance;

src/api/university/server/getRecommendedUniversity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import serverFetch from "@/utils/serverFetchUtil";
33
import { RecommendedUniversityResponse } from "../type/response";
44

55
const getRecommendedUniversity = async () => {
6-
const endpoint = "/univ-apply-infos/recommend";
6+
const endpoint = "/universities/recommend";
77

88
const res = await serverFetch<RecommendedUniversityResponse>(endpoint, {
99
isAuth: false, // 인증이 필요 없는 API로 설정

src/api/university/server/getSearchUniversityList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ interface SearchParams {
2020
* - 필요 시 query string 을 동적으로 붙인다.
2121
*/
2222
const getSearchUniversityList = async ({ region, keyword, testType, testScore }: SearchParams = {}) => {
23-
const endpoint = "/univ-apply-infos/search";
23+
const endpoint = "/universities/search";
2424

2525
const params = new URLSearchParams();
2626

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use client";
2+
3+
import Image from "next/image";
4+
import { useEffect, useRef, useState } from "react";
5+
6+
import { News } from "@/types/news";
7+
8+
import { IconSpeaker } from "@/public/svgs";
9+
10+
export type NewsSectionProps = {
11+
newsList: News[];
12+
};
13+
14+
const NewsSection = ({ newsList }: NewsSectionProps) => {
15+
const [visible, setVisible] = useState(false);
16+
const sectionRef = useRef<HTMLDivElement | null>(null);
17+
18+
useEffect(() => {
19+
if (!sectionRef.current) return;
20+
21+
const observer = new window.IntersectionObserver(
22+
([entry]) => {
23+
if (entry.isIntersecting) {
24+
setVisible(true);
25+
observer.disconnect();
26+
}
27+
},
28+
{
29+
rootMargin: "0px",
30+
threshold: 0,
31+
},
32+
);
33+
34+
observer.observe(sectionRef.current);
35+
36+
return () => observer.disconnect();
37+
}, []);
38+
39+
return (
40+
<div ref={sectionRef} className="mt-6 pl-5">
41+
<div className="mb-2.5 flex items-center gap-1.5 font-serif text-base font-semibold text-k-700">
42+
솔커에서 맛보는 소식
43+
<IconSpeaker />
44+
</div>
45+
{!visible ? (
46+
<div className="flex flex-col gap-4">
47+
{Array.from({ length: 3 }).map((_, idx) => (
48+
<div key={idx} className="flex animate-pulse gap-4">
49+
<div className="h-24 w-44 shrink-0 rounded-xl bg-gray-300" />
50+
<div className="mr-5 flex flex-col gap-2">
51+
<div className="h-5 w-32 rounded bg-gray-300" />
52+
<div className="h-4 w-40 rounded bg-gray-200" />
53+
</div>
54+
</div>
55+
))}
56+
</div>
57+
) : (
58+
<div className="flex flex-col gap-4">
59+
{newsList.map((news) => (
60+
<a key={news.id} target="_blank" href={news.url} rel="noreferrer">
61+
<div className="flex gap-4">
62+
<Image
63+
loading="lazy"
64+
className="h-24 w-44 shrink-0 rounded-xl object-cover"
65+
src={news.imageUrl}
66+
alt={news.title}
67+
width={170}
68+
height={90}
69+
/>
70+
<div className="mr-5 flex flex-col gap-0.5">
71+
<div className="text-serif text-sm font-semibold leading-normal text-k-700">{news.title}</div>
72+
<div className="font-serif text-xs font-normal leading-normal text-k-500">{news.description}</div>
73+
</div>
74+
</div>
75+
</a>
76+
))}
77+
</div>
78+
)}
79+
</div>
80+
);
81+
};
82+
83+
export default NewsSection;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const NewsSectionSkeleton = () => (
2+
<div className="mt-6 pl-5">
3+
<div className="mb-2.5 h-5 w-40 animate-pulse rounded bg-k-50" />
4+
<div className="flex flex-col gap-4">
5+
{[...Array(3)].map((_, idx) => (
6+
<div key={idx} className="h-[100px] animate-pulse rounded-xl bg-k-50" />
7+
))}
8+
</div>
9+
</div>
10+
);
11+
12+
export default NewsSectionSkeleton;

src/app/_home/_ui/PopularUniversitySection/index.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
"use client";
2-
1+
//. "use client";
32
import Image from "next/image";
43
import Link from "next/link";
54

6-
import useWheelHandler from "./_hooks/useWheelHandler";
7-
5+
// import useWheelHandler from "./_hooks/useWheelHandler";
86
import { ListUniversity } from "@/types/university";
97

108
type PopularUniversitySectionProps = {
119
universities: ListUniversity[];
1210
};
1311

14-
const PopularUniversitySection = ({ universities }: PopularUniversitySectionProps) => {
15-
const { containerRef } = useWheelHandler();
12+
const PopularUniversitySection = async ({ universities }: PopularUniversitySectionProps) => {
1613
return (
17-
<div ref={containerRef} className="overflow-x-auto">
14+
<div className="overflow-x-auto">
1815
<div className="flex gap-2">
1916
{universities.map((university, index) => (
2017
<Link key={university.id} href={`/university/${university.id}`}>
2118
<div className="relative w-[153px]">
2219
<div className="relative w-[153px]">
23-
<div className="absolute inset-0 h-[120px] rounded-lg bg-gradient-to-b from-transparent via-black/35 to-black/70" />
2420
<Image
2521
className="h-[120px] rounded-lg object-cover"
2622
src={
@@ -31,7 +27,8 @@ const PopularUniversitySection = ({ universities }: PopularUniversitySectionProp
3127
width={153}
3228
height={120}
3329
alt={`${university.koreanName || "대학교"} 배경 이미지`}
34-
priority={index < 3}
30+
priority={index < 3} // 상위 3개는 우선 로딩
31+
loading={index >= 3 ? "lazy" : "eager"}
3532
/>
3633
</div>
3734
<div className="absolute bottom-[9px] left-[10px] z-10 text-sm font-semibold leading-[160%] tracking-[0.15px] text-white">

src/app/_home/_ui/UniversityList/index.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,7 @@ const UniversityList = ({ allRegionsUniversityList }: UniversityListProps) => {
5454
background: "white",
5555
}}
5656
/>
57-
<UniversityCards
58-
colleges={previewUniversities.map((college) => ({
59-
...college,
60-
logoImageUrl: college.logoImageUrl?.replace(/^\/+/, ""),
61-
}))}
62-
showCapacity={false}
63-
/>
57+
<UniversityCards colleges={previewUniversities} showCapacity={false} />
6458
</div>
6559
);
6660
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import clsx from "clsx";
2+
3+
type UniversityListSkeletonProps = {
4+
/** 표시할 카드 개수 (기본 3개) */
5+
cardCount?: number;
6+
/** 표시할 탭 개수 (기본 4개) */
7+
tabCount?: number;
8+
style?: React.CSSProperties;
9+
className?: string;
10+
};
11+
12+
/** UniversityList 전체 로딩 상태 Skeleton */
13+
const UniversityListSkeleton = ({ cardCount = 3, tabCount = 4, style, className }: UniversityListSkeletonProps) => (
14+
<div className={clsx("flex flex-col gap-2", className)} style={style}>
15+
{/* 제목 플레이스홀더 */}
16+
<div className="h-6 w-40 animate-pulse rounded bg-k-50" />
17+
18+
{/* ButtonTab 플레이스홀더 */}
19+
<div className="flex flex-row gap-2 overflow-x-auto">
20+
{[...Array(tabCount)].map((_, idx) => (
21+
<div key={idx} className="h-8 w-20 shrink-0 animate-pulse rounded-full bg-k-50" />
22+
))}
23+
</div>
24+
25+
{/* 카드 리스트 플레이스홀더 */}
26+
<div className="flex flex-col gap-2.5">
27+
{[...Array(cardCount)].map((_, idx) => (
28+
<div
29+
key={idx}
30+
className="relative h-[91px] animate-pulse overflow-hidden rounded-lg border border-solid border-k-100 bg-k-50"
31+
>
32+
<div className="flex h-full justify-between px-5 py-3.5">
33+
{/* 썸네일 자리 */}
34+
<div className="flex gap-[23.5px]">
35+
<div className="flex flex-shrink-0 items-center">
36+
<div className="h-14 w-14 rounded-full bg-k-100" />
37+
</div>
38+
{/* 텍스트 3줄 */}
39+
<div className="flex flex-col justify-center gap-2">
40+
<div className="h-4 w-32 rounded bg-k-100" />
41+
<div className="h-3 w-48 rounded bg-k-100" />
42+
<div className="h-3 w-40 rounded bg-k-100" />
43+
</div>
44+
</div>
45+
{/* 화살표 자리 */}
46+
<div className="flex items-center">
47+
<div className="h-5 w-5 rounded bg-k-100" />
48+
</div>
49+
</div>
50+
</div>
51+
))}
52+
</div>
53+
</div>
54+
);
55+
56+
export default UniversityListSkeleton;

src/app/_home/index.tsx

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
import Image from "next/image";
1+
import dynamic from "next/dynamic";
22
import Link from "next/link";
33

44
import FindLastYearScoreBar from "./_ui/FindLastYearScoreBar";
5+
import NewsSectionSkeleton from "./_ui/NewsSection/skeleton";
56
import PopularUniversitySection from "./_ui/PopularUniversitySection";
6-
import UniversityList from "./_ui/UniversityList";
7+
import UniversityListSkeleton from "./_ui/UniversityList/skeleton";
78

89
import getRecommendedUniversity from "@/api/university/server/getRecommendedUniversity";
910
import { getAllRegionsUniversityList } from "@/api/university/server/getSearchUniversityList";
1011
import { fetchAllNews } from "@/lib/firebaseNews";
11-
import { IconSpeaker } from "@/public/svgs";
1212
import { IconIdCard, IconMagnifyingGlass, IconMuseum, IconPaper } from "@/public/svgs/home";
1313

14+
const NewsSection = dynamic(() => import("./_ui/NewsSection"), {
15+
ssr: false,
16+
loading: () => <NewsSectionSkeleton />,
17+
});
18+
19+
const UniversityListDynamic = dynamic(() => import("./_ui/UniversityList"), {
20+
ssr: false,
21+
loading: () => <UniversityListSkeleton />,
22+
});
23+
1424
const Home = async () => {
1525
const newsList = await fetchAllNews();
1626
const { data } = await getRecommendedUniversity();
@@ -72,35 +82,10 @@ const Home = async () => {
7282
</div>
7383

7484
<div className="p-5">
75-
<UniversityList allRegionsUniversityList={allRegionsUniversityList} />
85+
<UniversityListDynamic allRegionsUniversityList={allRegionsUniversityList} />
7686
</div>
7787

78-
<div className="mt-6 pl-5">
79-
<div className="mb-2.5 flex items-center gap-1.5 font-serif text-base font-semibold text-k-700">
80-
솔커에서 맛보는 소식
81-
<IconSpeaker />
82-
</div>
83-
<div className="flex flex-col gap-4">
84-
{newsList.map((news) => (
85-
<a key={news.id} target="_blank" href={news.url} rel="noreferrer">
86-
<div className="flex gap-4">
87-
<Image
88-
loading="lazy"
89-
className="h-24 w-44 shrink-0 rounded-xl object-cover"
90-
src={news.imageUrl}
91-
alt={news.title}
92-
width={170}
93-
height={90}
94-
/>
95-
<div className="mr-5 flex flex-col gap-0.5">
96-
<div className="text-serif text-sm font-semibold leading-normal text-k-700">{news.title}</div>
97-
<div className="font-serif text-xs font-normal leading-normal text-k-500">{news.description}</div>
98-
</div>
99-
</div>
100-
</a>
101-
))}
102-
</div>
103-
</div>
88+
<NewsSection newsList={newsList} />
10489
</>
10590
);
10691
};

src/app/layout.tsx

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable @next/next/no-css-tags */
22
import type { Metadata, Viewport } from "next";
33
import dynamic from "next/dynamic";
4-
import { Inter } from "next/font/google";
54
import localFont from "next/font/local";
65

76
import GlobalLayout from "@/components/layout/GlobalLayout";
@@ -16,18 +15,23 @@ export const metadata: Metadata = {
1615
description: "솔리드 커넥션. 교환학생의 첫 걸음",
1716
};
1817

19-
const inter = Inter({
20-
subsets: ["latin"],
21-
display: "swap", // FOUT 방지
22-
preload: true,
23-
});
24-
18+
// 🎯 폰트 최적화: 하나의 폰트만 사용
2519
const pretendard = localFont({
2620
src: "../../public/fonts/PretendardVariable.woff2",
27-
display: "swap", // FOUT 방지를 위한 swap 설정
21+
display: "swap",
2822
weight: "45 920",
2923
variable: "--font-pretendard",
30-
preload: true, // 폰트 우선 로딩
24+
preload: true,
25+
// 폰트 로딩 실패 시 fallback 폰트 체인
26+
fallback: [
27+
"system-ui",
28+
"-apple-system",
29+
"BlinkMacSystemFont",
30+
"Apple SD Gothic Neo",
31+
"Malgun Gothic",
32+
"맑은 고딕",
33+
"sans-serif",
34+
],
3135
});
3236

3337
const KakaoScriptLoader = dynamic(() => import("@/lib/ScriptLoader/KakaoScriptLoader"), {
@@ -56,36 +60,41 @@ export const viewport: Viewport = {
5660

5761
const RootLayout = ({ children }: { children: React.ReactNode }) => (
5862
<AlertProvider>
59-
<html lang="ko">
63+
<html lang="ko" className={pretendard.variable}>
6064
<head>
61-
{/* Critical 폰트 preload with high priority */}
65+
{/* 🚀 최우선 폰트 preload */}
6266
<link
6367
rel="preload"
6468
href="/fonts/PretendardVariable.woff2"
6569
as="font"
6670
type="font/woff2"
6771
crossOrigin="anonymous"
6872
/>
69-
{/* Prevent layout shift with font-display: swap */}
73+
74+
{/* 폰트 로딩 최적화를 위한 Critical CSS */}
7075
<style
7176
dangerouslySetInnerHTML={{
7277
__html: `
73-
@font-face {
74-
font-family: 'Pretendard';
75-
src: url('/fonts/PretendardVariable.woff2') format('woff2');
76-
font-weight: 45 920;
77-
font-display: swap;
78-
font-style: normal;
78+
html {
79+
font-family: var(--font-pretendard), system-ui, -apple-system, sans-serif;
80+
font-synthesis: none;
81+
text-rendering: optimizeLegibility;
82+
-webkit-font-smoothing: antialiased;
83+
-moz-osx-font-smoothing: grayscale;
84+
}
85+
.font-loading {
86+
font-family: system-ui, -apple-system, sans-serif;
7987
}
8088
`,
8189
}}
8290
/>
91+
8392
{/* DNS prefetch for external resources */}
8493
<link rel="dns-prefetch" href="//www.googletagmanager.com" />
8594
<link rel="dns-prefetch" href="//connect.facebook.net" />
8695
<link rel="dns-prefetch" href="//t1.kakaocdn.net" />
8796
</head>
88-
<body className={`${pretendard.className} ${inter.className}`}>
97+
<body className={pretendard.className}>
8998
<KakaoScriptLoader />
9099
<AppleScriptLoader />
91100
<GoogleAnalytics gaId="G-V1KLYZC1DS" />

0 commit comments

Comments
 (0)