Skip to content

Commit 80ce555

Browse files
authored
feat: 모의지원 페이지 UI 컴포넌트 공통화 및 멘토 스타일 정렬 (#448)
1 parent b45fcaf commit 80ce555

13 files changed

Lines changed: 242 additions & 235 deletions

apps/web/src/app/university/application/ScorePageContent.tsx

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"use client";
22

33
import { useRouter } from "next/navigation";
4-
import { useEffect, useMemo, useRef, useState } from "react";
4+
import { type FormEvent, useEffect, useMemo, useRef, useState } from "react";
55
import { useGetApplicationsList } from "@/apis/applications";
66
import ConfirmCancelModal from "@/components/modal/ConfirmCancelModal";
77
import ButtonTab from "@/components/ui/ButtonTab";
88
import Tab from "@/components/ui/Tab";
99
import { REGIONS_KO } from "@/constants/university";
1010
import type { ScoreSheet as ScoreSheetType } from "@/types/application";
1111
import type { RegionKo } from "@/types/university";
12+
import ApplicationSectionTitle from "./_components/ApplicationSectionTitle";
1213
import ScoreSearchBar from "./ScoreSearchBar";
14+
import ScoreSearchField from "./ScoreSearchField";
1315
import ScoreSheet from "./ScoreSheet";
1416

1517
const PREFERENCE_CHOICE: ("1순위" | "2순위" | "3순위")[] = ["1순위", "2순위", "3순위"];
@@ -79,7 +81,7 @@ const ScorePageContent = () => {
7981
}, [scoreResponseData, regionFilter, searchValue]);
8082

8183
// (이하 코드는 동일)
82-
const handleSearch = (event: React.FormEvent) => {
84+
const handleSearch = (event: FormEvent) => {
8385
event.preventDefault();
8486
const keyword = searchRef.current?.value || "";
8587
setRegionFilter("");
@@ -91,6 +93,9 @@ const ScorePageContent = () => {
9193
if (searchRef.current) {
9294
searchRef.current.value = keyword;
9395
}
96+
setRegionFilter("");
97+
setSearchValue(keyword);
98+
setSearchActive(false);
9499
};
95100

96101
const handleSearchClick = () => {
@@ -121,35 +126,23 @@ const ScorePageContent = () => {
121126
const hotKeyWords = ["RMIT", "오스트라바", "칼스루에", "그라츠", "추오", "프라하", "보라스", "빈", "메모리얼"];
122127

123128
return (
124-
<div className="gap-4 px-5">
129+
<div className="px-5">
130+
<ApplicationSectionTitle
131+
className="mb-3 mt-5"
132+
title="지원자 현황"
133+
description="지원 순위와 지역 필터로 원하는 학교 현황을 빠르게 확인할 수 있어요."
134+
/>
125135
<ScoreSearchBar onClick={handleSearchClick} textRef={searchRef} searchHandler={handleSearch} />
126136

127137
{searchActive ? (
128-
<div className="p-4 font-sans">
129-
{/* Title for the popular searches section */}
130-
<div className="ml-5 mt-[18px] text-black typo-sb-7">인기 검색</div>
131-
132-
{/* Container for the keyword buttons */}
133-
<div className="ml-5 mt-2.5 flex flex-wrap gap-2">
134-
{hotKeyWords.map((word) => (
135-
<button
136-
key={word}
137-
// Button styling for each keyword
138-
className="flex items-center justify-center gap-2.5 rounded-full bg-k-50 px-3 py-[5px] text-black transition-colors typo-medium-2 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
139-
onClick={() => {
140-
handleSearchField(word);
141-
handleSearch(new Event("submit") as unknown as React.FormEvent);
142-
}}
143-
type="button"
144-
>
145-
{word}
146-
</button>
147-
))}
148-
</div>
138+
<div className="mt-3 rounded-lg bg-white py-4 shadow-sdwB">
139+
<ScoreSearchField keyWords={hotKeyWords} setKeyWord={handleSearchField} />
149140
</div>
150141
) : (
151142
<>
152-
<Tab choices={PREFERENCE_CHOICE} choice={preference} setChoice={setPreference} />
143+
<div className="mt-4 rounded-lg bg-white px-2 shadow-sdwB">
144+
<Tab choices={PREFERENCE_CHOICE} choice={preference} setChoice={setPreference} />
145+
</div>
153146
<ButtonTab
154147
choices={REGIONS_KO}
155148
choice={regionFilter}
@@ -158,10 +151,10 @@ const ScorePageContent = () => {
158151
setSearchValue("");
159152
setRegionFilter(newRegion as RegionKo | "");
160153
}}
161-
style={{ padding: "10px 0 10px 18px" }}
154+
style={{ padding: "10px 0 10px 8px" }}
162155
/>
163156

164-
<div className="mx-auto mt-2.5 flex w-full flex-col gap-3 overflow-x-auto">
157+
<div className="mx-auto mt-3 flex w-full flex-col gap-3 overflow-x-auto pb-4">
165158
{scoreSheets.map((choice) => (
166159
<ScoreSheet key={choice.koreanName} scoreSheet={choice} />
167160
))}

apps/web/src/app/university/application/ScoreSearchBar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
import type { RefObject } from "react";
1+
import type { FormEvent, RefObject } from "react";
22
import { IconSearchFilled } from "@/public/svgs";
33

44
type ScoreSearchBarProps = {
55
onClick: () => void;
66
textRef: RefObject<HTMLInputElement>;
7-
searchHandler: (_e: React.FormEvent) => void;
7+
searchHandler: (_e: FormEvent) => void;
88
};
99

1010
const ScoreSearchBar = ({ onClick, textRef, searchHandler }: ScoreSearchBarProps) => (
1111
<form
1212
onClick={onClick}
1313
onKeyDown={(e) => e.key === "Enter" && onClick()}
14-
className="flex h-[53px] flex-row items-center border-b border-bg-700"
14+
className="flex h-12 flex-row items-center rounded-lg bg-white pl-3 shadow-sdwB"
1515
onSubmit={searchHandler}
1616
role="search"
1717
>
1818
<input
19-
className="w-full border-0 pl-6 text-gray-400 outline-0 typo-regular-1"
19+
className="w-full border-0 pl-2 text-k-500 outline-0 placeholder:text-k-300 typo-regular-2"
2020
placeholder="해외 파견 학교를 검색하세요."
2121
ref={textRef}
2222
/>
23-
<button className="cursor-pointer border-0 bg-white pr-[11px]" type="submit" aria-label="검색">
23+
<button className="cursor-pointer border-0 bg-transparent pr-3" type="submit" aria-label="검색">
2424
<IconSearchFilled />
2525
</button>
2626
</form>

apps/web/src/app/university/application/ScoreSearchField.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ type ScoreSearchFieldProps = {
55

66
const ScoreSearchField = ({ keyWords, setKeyWord }: ScoreSearchFieldProps) => (
77
<div>
8-
<div className="ml-5 mt-[18px] text-black typo-sb-7">인기 검색</div>
9-
<div className="ml-5 mt-[10px] flex flex-wrap gap-2">
8+
<div className="px-5 text-k-800 typo-sb-7">인기 검색</div>
9+
<div className="mt-2 flex flex-wrap gap-2 px-5">
1010
{keyWords.map((keyWord) => (
1111
<button
1212
key={keyWord}
13-
className="flex items-center justify-center gap-[10px] rounded-full bg-bg-50 px-3 py-[5px] text-black typo-medium-2"
13+
className="flex items-center justify-center gap-[10px] rounded-full bg-k-50 px-3 py-[5px] text-k-800 transition-colors hover:bg-k-100 typo-medium-2"
1414
onClick={() => setKeyWord(keyWord)}
1515
type="button"
1616
>

apps/web/src/app/university/application/ScoreSheet.tsx

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,25 @@ const ScoreSheet = ({ scoreSheet }: { scoreSheet: ScoreSheetType }) => {
77
const [tableOpened, setTableOpened] = useState(false);
88

99
return (
10-
// 테이블 전체 컨테이너
11-
<button
12-
onClick={() => setTableOpened(!tableOpened)}
13-
className="w-full overflow-hidden rounded-lg border border-gray-200"
14-
>
15-
{/* 테이블 헤더 */}
16-
<div className="flex h-10 items-center justify-between bg-k-50 px-4">
17-
<p className="truncate text-black typo-sb-7">
10+
<div className="w-full overflow-hidden rounded-lg bg-white shadow-sdwB">
11+
<button
12+
onClick={() => setTableOpened(!tableOpened)}
13+
className="flex min-h-12 w-full items-center justify-between border-b border-k-50 bg-k-50 px-4 text-left"
14+
type="button"
15+
>
16+
<p className="truncate text-k-900 typo-sb-7">
1817
{scoreSheet.koreanName} ({scoreSheet.applicants.length}/{scoreSheet.studentCapacity})
1918
</p>
20-
<button type="button" aria-label="더보기" className="cursor-pointer border-none bg-transparent p-1">
19+
<span className="flex h-6 w-6 items-center justify-center">
2120
<IconExpandMoreFilled className={`transition-transform duration-300 ${tableOpened ? "rotate-180" : ""}`} />
22-
</button>
23-
</div>
21+
</span>
22+
</button>
2423

25-
{/* 테이블 바디 (토글) */}
26-
{tableOpened && (
27-
<div className="divide-y divide-gray-200">
24+
{tableOpened ? (
25+
<div className="divide-y divide-k-50">
2826
{scoreSheet.applicants.map((applicant) => (
29-
<div key={applicant.nicknameForApply} className="flex h-10 items-center px-3 text-gray-600 typo-medium-3">
30-
<span className="min-w-[30px] flex-1 overflow-hidden whitespace-nowrap text-black typo-regular-2">
27+
<div key={applicant.nicknameForApply} className="flex min-h-12 items-center px-4 text-k-600 typo-medium-3">
28+
<span className="min-w-[30px] flex-1 overflow-hidden whitespace-nowrap text-k-900 typo-regular-2">
3129
{applicant.nicknameForApply}
3230
</span>
3331
<span className="min-w-[30px] flex-1 overflow-hidden whitespace-nowrap text-center typo-medium-2">
@@ -39,18 +37,11 @@ const ScoreSheet = ({ scoreSheet }: { scoreSheet: ScoreSheetType }) => {
3937
<span className="min-w-[30px] flex-1 overflow-hidden whitespace-nowrap text-center typo-medium-2">
4038
{applicant.testScore}
4139
</span>
42-
<span className="flex w-[18px] flex-none items-center justify-center">
43-
{/* {applicant.isMine && (
44-
<Link href="/university/application/apply">
45-
<IconEditFilled />
46-
</Link>
47-
)} */}
48-
</span>
4940
</div>
5041
))}
5142
</div>
52-
)}
53-
</button>
43+
) : null}
44+
</div>
5445
);
5546
};
5647

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import BlockBtn from "@/components/button/BlockBtn";
2+
3+
type ApplicationBottomActionBarProps = {
4+
label: string;
5+
onClick: () => void;
6+
};
7+
8+
const ApplicationBottomActionBar = ({ label, onClick }: ApplicationBottomActionBarProps) => {
9+
return (
10+
<div className="fixed bottom-14 w-full max-w-app bg-white">
11+
<div className="mb-[37px] px-5">
12+
<BlockBtn onClick={onClick}>{label}</BlockBtn>
13+
</div>
14+
</div>
15+
);
16+
};
17+
18+
export default ApplicationBottomActionBar;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type ApplicationSectionTitleProps = {
2+
title: string;
3+
description?: string;
4+
className?: string;
5+
};
6+
7+
const ApplicationSectionTitle = ({ title, description, className = "" }: ApplicationSectionTitleProps) => {
8+
return (
9+
<div className={className}>
10+
<h2 className="text-k-900 typo-bold-5">{title}</h2>
11+
{description ? <p className="mt-1 text-k-500 typo-regular-2">{description}</p> : null}
12+
</div>
13+
);
14+
};
15+
16+
export default ApplicationSectionTitle;

apps/web/src/app/university/application/apply/ConfirmStep.tsx

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import clsx from "clsx";
22

3-
import BlockBtn from "@/components/button/BlockBtn";
43
import { IconCheck } from "@/public/svgs/mentor";
54
import type { ListUniversity } from "@/types/university";
5+
import ApplicationBottomActionBar from "../_components/ApplicationBottomActionBar";
6+
import ApplicationSectionTitle from "../_components/ApplicationSectionTitle";
67

78
type ConfirmStepProps = {
89
universityList: ListUniversity[];
@@ -11,42 +12,33 @@ type ConfirmStepProps = {
1112

1213
const ConfirmStep = ({ universityList, onNext }: ConfirmStepProps) => {
1314
return (
14-
<div className="flex flex-col items-center justify-between bg-white p-5 font-sans">
15-
{/* 상단 컨텐츠 */}
16-
<div className="flex w-full flex-1 flex-col items-center justify-center text-center">
15+
<div className="my-5 px-5 pb-40">
16+
<div className="flex items-center justify-center">
1717
<IconCheck />
18-
<h1 className="mt-4 text-secondary typo-bold-1">
19-
지원하기 <span className="text-k-800">완료</span>
20-
</h1>
21-
<p className="mt-2 text-k-500 typo-regular-2">
22-
지원은 총 3번만 수정 가능하며,
23-
<br />
24-
제출 완료 후 성적을 변경 하실 수 없습니다.
25-
</p>
26-
27-
{/* 지원 학교 목록 카드 */}
28-
<div className="mt-8 w-full rounded-lg border border-secondary bg-white p-2 shadow-sdwC">
29-
<div className="space-y-2">
30-
{universityList.map((university, index) => (
31-
<div
32-
key={university.id}
33-
className={clsx(
34-
"flex items-center justify-between px-4 py-5 typo-regular-2",
35-
index < universityList.length - 1 && "border-b border-gray-100",
36-
)}
37-
>
38-
<span className="text-gray-600 typo-medium-2">{index + 1}지망</span>
39-
<span className="text-blue-600 typo-sb-9 hover:underline">{university.koreanName}</span>
40-
</div>
41-
))}
42-
</div>
43-
</div>
4418
</div>
19+
<ApplicationSectionTitle
20+
className="mt-4 text-center"
21+
title="지원 내용을 확인해주세요"
22+
description="제출 후에는 성적을 변경할 수 없습니다. 선택한 학교를 한 번 더 확인해주세요."
23+
/>
4524

46-
{/* 하단 버튼 */}
47-
<div className="mt-10 w-full">
48-
<BlockBtn onClick={onNext}>제출하기</BlockBtn>
25+
<div className="mt-5 rounded-lg bg-white p-4 shadow-sdwB">
26+
<div className="space-y-2">
27+
{universityList.map((university, index) => (
28+
<div
29+
key={university.id}
30+
className={clsx(
31+
"flex items-center justify-between px-4 py-4 typo-regular-2",
32+
index < universityList.length - 1 && "border-b border-k-50",
33+
)}
34+
>
35+
<span className="text-k-500 typo-medium-2">{index + 1}지망</span>
36+
<span className="text-primary typo-sb-9">{university.koreanName}</span>
37+
</div>
38+
))}
39+
</div>
4940
</div>
41+
<ApplicationBottomActionBar label="제출하기" onClick={onNext} />
5042
</div>
5143
);
5244
};

apps/web/src/app/university/application/apply/DoneStep.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { useRouter } from "next/navigation";
22
import BlockBtn from "@/components/button/BlockBtn";
33
import Image from "@/components/ui/FallbackImage";
4+
import ApplicationSectionTitle from "../_components/ApplicationSectionTitle";
45

56
const DoneStep = () => {
67
const router = useRouter();
78
return (
8-
<div className="mt-52 flex h-full flex-col items-center justify-center gap-6">
9-
<Image src="/images/survey-complete-icon.png" width={120} height={120} alt="지원 완료" />
10-
<div className="text-center font-serif text-k-800 typo-sb-2">
11-
학교 지원이
12-
<br />
13-
<span className="text-secondary">완료</span>
14-
되었어요!
9+
<div className="mt-24 px-5">
10+
<div className="rounded-lg bg-white px-6 py-8 text-center shadow-sdwB">
11+
<div className="flex justify-center">
12+
<Image src="/images/survey-complete-icon.png" width={120} height={120} alt="지원 완료" />
13+
</div>
14+
<ApplicationSectionTitle
15+
className="mt-4"
16+
title="학교 지원이 완료되었어요"
17+
description="지원자 현황에서 경쟁률을 바로 확인할 수 있어요."
18+
/>
1519
</div>
1620

17-
<div className="mt-10 flex w-full flex-col gap-3 px-5 pt-8">
21+
<div className="mt-6 flex w-full flex-col gap-3">
1822
<BlockBtn
19-
className="bg-primary-900 text-white"
2023
onClick={() => {
2124
router.push("/university/application");
2225
}}

apps/web/src/app/university/application/apply/EmptyGPA.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import Link from "next/link";
22

33
import { IconSolidConnectionSmallLogo } from "@/public/svgs/my";
4+
import ApplicationSectionTitle from "../_components/ApplicationSectionTitle";
45

56
const EmptyGPA = () => {
67
return (
7-
<div className="mt-52 flex flex-col items-center justify-center gap-2 rounded-lg text-center">
8-
<IconSolidConnectionSmallLogo />
9-
<p className="text-k-300">
10-
지원할 수 있는 성적이 없어요.
11-
<br />
12-
성적부터 입력해 볼까요?
13-
</p>
8+
<div className="mt-24 px-5">
9+
<div className="rounded-lg bg-white px-6 py-8 text-center shadow-sdwB">
10+
<div className="flex justify-center">
11+
<IconSolidConnectionSmallLogo />
12+
</div>
13+
<ApplicationSectionTitle
14+
className="mt-4"
15+
title="지원할 수 있는 성적이 없어요"
16+
description="성적을 먼저 등록하면 모의지원 진행이 가능해요."
17+
/>
18+
</div>
1419
<Link
1520
href="/university/score"
16-
className="mt-2 h-13 w-60 rounded-full bg-gradient-to-l from-primary to-secondary p-4 text-white typo-sb-9"
21+
className="mt-6 flex h-13 w-full items-center justify-center rounded-lg bg-primary text-white typo-medium-1"
1722
>
1823
성적 등록하러가기
1924
</Link>

0 commit comments

Comments
 (0)