diff --git a/public/images/language/default.png b/public/images/language/default.png new file mode 100644 index 00000000..1a17bca5 Binary files /dev/null and b/public/images/language/default.png differ diff --git a/public/images/language/ielts.png b/public/images/language/ielts.png new file mode 100644 index 00000000..6218faaf Binary files /dev/null and b/public/images/language/ielts.png differ diff --git a/public/images/language/toefl_ibt.png b/public/images/language/toefl_ibt.png new file mode 100644 index 00000000..e8968e74 Binary files /dev/null and b/public/images/language/toefl_ibt.png differ diff --git a/public/images/language/toefl_itp.png b/public/images/language/toefl_itp.png new file mode 100644 index 00000000..a27b3750 Binary files /dev/null and b/public/images/language/toefl_itp.png differ diff --git a/public/images/language/toeic.png b/public/images/language/toeic.png new file mode 100644 index 00000000..1a17bca5 Binary files /dev/null and b/public/images/language/toeic.png differ diff --git a/src/app/university/[id]/CollegeBottomSheet.tsx b/src/app/university/[id]/CollegeBottomSheet.tsx deleted file mode 100644 index 4fe22ab7..00000000 --- a/src/app/university/[id]/CollegeBottomSheet.tsx +++ /dev/null @@ -1,348 +0,0 @@ -"use client"; - -import React, { forwardRef, useEffect, useRef, useState } from "react"; -import ReactLinkify from "react-linkify"; - -import clsx from "clsx"; - -import ScrollTab from "@/components/ui/ScrollTab"; -import GoogleEmbedMap from "@/components/ui/map/GoogleEmbedMap"; - -import CollegeReviews from "./CollegeReviews"; -import styles from "./college-bottomsheet.module.css"; - -import { Review } from "@/types/review"; -import { LanguageRequirement, University } from "@/types/university"; - -import { - deleteUniversityFavoriteApi, - getUniversityFavoriteStatusApi, - postUniversityFavoriteApi, -} from "@/api/university"; -import { IconBookmarkFilled, IconBookmarkOutlined } from "@/public/svgs"; - -interface CollegeBottomSheetProps { - collegeId: number; - convertedKoreanName: string; - reviewList: Review[]; - university: University; -} - -const CollegeBottomSheet = ({ collegeId, convertedKoreanName, reviewList, university }: CollegeBottomSheetProps) => { - const pages: string[] = ["어학성적", "학교정보", "지원정보", "지역정보", "파견후기"]; - const [activeTab, setActiveTab] = useState("학교정보"); - const sectionRefs = [ - useRef(null), - useRef(null), - useRef(null), - useRef(null), - useRef(null), - ]; - const [isLiked, setIsLiked] = useState(false); - - useEffect(() => { - const getFavoriteStatus = async () => { - try { - const res = await getUniversityFavoriteStatusApi(collegeId); - setIsLiked(res.data.isLike); - } catch { - // 비로그인 시 무시 - } - }; - getFavoriteStatus(); - }, [collegeId]); - - const toggleLike = () => { - const postLike = async () => { - try { - const res = !isLiked - ? await postUniversityFavoriteApi(collegeId) - : await deleteUniversityFavoriteApi(collegeId); - const { result } = res.data; - if (result === "LIKE_SUCCESS") { - setIsLiked(true); - } else if (result === "LIKE_CANCELED") { - setIsLiked(false); - } - } catch (err) { - if (err.response) { - console.error("Axios response error", err.response); - if (err.response.status === 401 || err.response.status === 403) { - alert("로그인이 필요합니다"); - document.location.href = "/login"; - } else { - alert(err.response.data?.message); - } - } else { - console.error("Error", err.message); - alert(err.message); - } - } - }; - postLike(); - }; - - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const index = sectionRefs.findIndex((ref) => ref.current === entry.target); - setActiveTab(pages[index]); - } - }); - }, - { threshold: 0.3, rootMargin: "-103px 0px -60% 0px" }, - ); - - sectionRefs.forEach((ref) => { - if (ref.current) { - observer.observe(ref.current); - } - }); - - return () => { - sectionRefs.forEach((ref) => { - if (ref.current) { - observer.unobserve(ref.current); - } - }); - }; - }, [sectionRefs]); - - const handleTabClick = (tab: string) => { - setActiveTab(tab); - sectionRefs[pages.findIndex((t) => t === tab)].current?.scrollIntoView({ behavior: "smooth", block: "start" }); - }; - - return ( - <> -
-
- {university.englishName || "대학명"} -
-
-
-
-
{convertedKoreanName || "대학명"}
- -
- - - - - - - - -
- - ); -}; - -export default CollegeBottomSheet; - -const LanguageSection = forwardRef< - HTMLDivElement, - { languageRequirements: LanguageRequirement[]; detailsForLanguage: string } ->(function LanguageSection({ languageRequirements, detailsForLanguage }, ref) { - return ( -
-
- {languageRequirements.map((requirement, index) => ( -
- - {requirement.languageTestType} {requirement.minScore} - -
- ))} -
- -
- ); -}); - -const BasicInfoSection = forwardRef< - HTMLDivElement, - { - homepageUrl: string; - region: string; - country: string; - studentCapacity: number; - englishName: string; - } ->(function BasicInfoSection({ homepageUrl, region, country, studentCapacity, englishName }, ref) { - return ( -
- -
- - -
-
- 파견학교 위치 - - - -
-
- ); -}); - -const ApplyInfoSection = forwardRef< - HTMLDivElement, - { - semesterAvailableForDispatch: string; - semesterRequirement: string; - gpaRequirement: string; - gpaRequirementCriteria: string; - detailsForAccommodation: string; - detailsForMajor: string; - englishCourseUrl: string; - } ->(function ApplyInfoSection( - { - semesterAvailableForDispatch = "정보 없음", - semesterRequirement = "정보 없음", - gpaRequirement = "정보 없음", - gpaRequirementCriteria = "정보 없음", - detailsForAccommodation = "정보 없음", - detailsForMajor = "정보 없음", - englishCourseUrl = "정보 없음", - }, - ref, -) { - return ( -
-
-
- - -
-
- - -
- - -
-
- ); -}); - -const RegionInfoSection = forwardRef< - HTMLDivElement, - { - detailsForLocal: string; - } ->(function RegionInfoSection({ detailsForLocal = "지역 정보가 없습니다." }, ref) { - return ( -
- {/*
사진이 여기 들어갑니다
*/} - -
- ); -}); - -const ReviewSection = forwardRef(function ReviewSection({}, ref) { - return ( -
- 생생한 후기 -
-
- ); -}); - -const BorderBox = ({ - subject, - content, - linkify = false, - className, -}: { - subject: string; - content: string; - linkify?: boolean; - className?: string; -}) => { - if (linkify) { - return ( - -
- {subject} - {content} -
-
- ); - } - return ( -
- {subject} - {content} -
- ); -}; - -const BackgroundBox = ({ - subject, - content, - linkify = true, - className, -}: { - subject: string; - content: string; - linkify?: boolean; - className?: string; -}) => { - if (linkify) { - return ( - -
- {subject} - {content} -
-
- ); - } - return ( -
- {subject} - {content} -
- ); -}; diff --git a/src/app/university/[id]/CollegeDetail.tsx b/src/app/university/[id]/CollegeDetail.tsx deleted file mode 100644 index 4248ec0e..00000000 --- a/src/app/university/[id]/CollegeDetail.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Image from "next/image"; - -interface CollegeDetailProps { - imageUrl: string; -} - -const CollegeDetail = ({ imageUrl }: CollegeDetailProps) => ( -
-
- 학교 이미지 -
-
-); - -export default CollegeDetail; diff --git a/src/app/university/[id]/CollegeReviews.tsx b/src/app/university/[id]/CollegeReviews.tsx deleted file mode 100644 index 2e217b01..00000000 --- a/src/app/university/[id]/CollegeReviews.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState } from "react"; - -import StarFilledIcon from "@/components/ui/icon/star-filled"; - -import styles from "./college-reviews.module.css"; - -import { Review } from "@/types/review"; - -import { IconExpandMoreFilled } from "@/public/svgs/community"; - -const CollegeReviews = ({ style, reviewList }: { style?: React.CSSProperties; reviewList: Review[] }) => ( -
- {reviewList.map((review) => ( - - ))} -
-); - -export default CollegeReviews; - -export const CollegeReviewCard = ({ review }: { review: Review }) => { - const { term, rating, content, dispatchSemester, transportation, buddyProgram } = review; - const [open, setOpen] = useState(false); - const renderStars = () => { - const TOTAL_STARS = 5; - const stars: JSX.Element[] = []; - for (let i = 1; i <= TOTAL_STARS; i += 1) { - if (i <= rating) { - // Full Star - stars.push(); - } else if (i - 0.5 === rating) { - // Half Star - stars.push(); - } else { - // Empty Star - stars.push(); - } - } - return stars; - }; - - if (open) { - return ( -
-
-
{term}
-
{renderStars()}
-
-
- {content} -
-
-
- 수학기간 - {dispatchSemester} -
-
- 교통편 - {transportation} -
-
- 버디프로그램 - {buddyProgram} -
-
- -
- ); - } - return ( -
-
-
{term}
-
{renderStars()}
-
- -
- ); -}; diff --git a/src/app/university/[id]/EnglishSection.tsx b/src/app/university/[id]/EnglishSection.tsx new file mode 100644 index 00000000..ebe7b80f --- /dev/null +++ b/src/app/university/[id]/EnglishSection.tsx @@ -0,0 +1,25 @@ +"use client"; + +import Linkify from "react-linkify"; + +interface EnglishSectionProps { + englishDetail: string; +} + +const EnglishSection = ({ englishDetail }: EnglishSectionProps) => { + return ( + <> +
+
+
영어강의 리스트
+
+ + {englishDetail} + +
+
+ + ); +}; + +export default EnglishSection; diff --git a/src/app/university/[id]/InfoSection.tsx b/src/app/university/[id]/InfoSection.tsx new file mode 100644 index 00000000..0f45668d --- /dev/null +++ b/src/app/university/[id]/InfoSection.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useState } from "react"; + +interface InfoSectionProps { + semesterRequirement: string; + semesterAvailableForDispatch: string; + detailsForApply: string; + detailsForAccommodation: string; +} + +const InfoSection = ({ + semesterRequirement, + semesterAvailableForDispatch, + detailsForApply, + detailsForAccommodation, +}: InfoSectionProps) => { + const [detailsForApplyFold, setDetailsForApplyFold] = useState(true); + const [detailsForAccomodationFold, setDetailsForAccommodationFold] = useState(true); + + return ( +
+
+
+ {/* 최저 이수학기 */} +
+
+ + 최저 이수학기 +
+
+ {semesterRequirement} +
+
+ {/* 파견 가능학기 */} +
+
+ + 파견 가능학기 +
+
+ + {semesterAvailableForDispatch} + +
+
+ {/* 자격요건 */} + {detailsForApplyFold ? ( +
setDetailsForApplyFold(false)} + role="button" + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setDetailsForApplyFold(false); + } + }} + > +
+ + 자격요건 +
+
+ +
+
+ ) : ( +
setDetailsForApplyFold(true)} + role="button" + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setDetailsForApplyFold(true); + } + }} + > +
+
+ + 자격요건 +
+
+ +
+
+
+ {detailsForApply} +
+
+ )} + {/* 기숙사 */} + {detailsForAccomodationFold ? ( +
setDetailsForAccommodationFold(false)} + role="button" + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setDetailsForAccommodationFold(false); + } + }} + > +
+ + 자격요건 +
+
+ +
+
+ ) : ( +
setDetailsForAccommodationFold(true)} + role="button" + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setDetailsForAccommodationFold(true); + } + }} + > +
+
+ + 자격요건 +
+
+ +
+
+
+ {detailsForAccommodation} +
+
+ )} +
+
+ ); +}; + +export default InfoSection; + +const FoldIcon = () => { + return ( + + + + ); +}; + +const UnFoldIcon = () => { + return ( + + + + ); +}; + +const SemesterRequirementIcon = () => { + return ( + + + + + ); +}; + +const SemesterAvailableForDispatchIcon = () => { + return ( + + + + + ); +}; + +const DetailsForApplyIcon = () => { + return ( + + + + + ); +}; + +const DetailsForAccommodationIcon = () => { + return ( + + + + + ); +}; diff --git a/src/app/university/[id]/LanguageSection.tsx b/src/app/university/[id]/LanguageSection.tsx new file mode 100644 index 00000000..315092b2 --- /dev/null +++ b/src/app/university/[id]/LanguageSection.tsx @@ -0,0 +1,61 @@ +"use client"; + +import Image from "next/image"; +import Linkify from "react-linkify"; + +import { formatLanguageTestName, getLanguageTestLogo } from "@/utils/languageUtils"; + +import { LanguageRequirement } from "@/types/university"; + +interface LanguageSectionProps { + languageRequirements: LanguageRequirement[]; + detailsForLanguage: string; +} + +const LanguageSection = ({ languageRequirements, detailsForLanguage }: LanguageSectionProps) => { + return ( + <> +
+
+
어학 성적
+
+
+ {languageRequirements.map((req, idx) => ( + + ))} +
+
+
+
+
+
어학세부 요건
+
+ + {detailsForLanguage} + +
+
+ + ); +}; + +const Language = ({ name, logoUrl, score }: { name: string; logoUrl: string; score: string }) => { + return ( +
+
+
{name}
+
+ 어학시험 +
+
+
{score}
+
+ ); +}; + +export default LanguageSection; diff --git a/src/app/university/[id]/MajorSection.tsx b/src/app/university/[id]/MajorSection.tsx new file mode 100644 index 00000000..ac442104 --- /dev/null +++ b/src/app/university/[id]/MajorSection.tsx @@ -0,0 +1,25 @@ +"use client"; + +import ReactLinkify from "react-linkify"; + +interface MajorSectionProps { + majorDetail: string; +} + +const MajorSection = ({ majorDetail }: MajorSectionProps) => { + return ( + <> +
+
+
전공상세
+
+ + {majorDetail} + +
+
+ + ); +}; + +export default MajorSection; diff --git a/src/app/university/[id]/MapSection.tsx b/src/app/university/[id]/MapSection.tsx new file mode 100644 index 00000000..8b82efd9 --- /dev/null +++ b/src/app/university/[id]/MapSection.tsx @@ -0,0 +1,21 @@ +import GoogleEmbedMap from "@/components/ui/map/GoogleEmbedMap"; + +interface MapSectionProps { + universityEnglishName: string; +} + +const MapSection = ({ universityEnglishName }: MapSectionProps) => { + return ( + <> +
+
+
파견학교 위치
+
+ +
+
+ + ); +}; + +export default MapSection; diff --git a/src/app/university/[id]/SubTitleSection.tsx b/src/app/university/[id]/SubTitleSection.tsx new file mode 100644 index 00000000..9e0d382b --- /dev/null +++ b/src/app/university/[id]/SubTitleSection.tsx @@ -0,0 +1,17 @@ +interface SubTitleSectionProps { + totalDispatchCount: number; + country: string; + studentCapacity: number; +} + +const SubTitleSection = ({ totalDispatchCount, country, studentCapacity }: SubTitleSectionProps) => { + return ( +
+ {totalDispatchCount}회 파견 + {country} + 모집 {studentCapacity}명 +
+ ); +}; + +export default SubTitleSection; diff --git a/src/app/university/[id]/TitleSection.tsx b/src/app/university/[id]/TitleSection.tsx new file mode 100644 index 00000000..1bb0a45c --- /dev/null +++ b/src/app/university/[id]/TitleSection.tsx @@ -0,0 +1,23 @@ +import Image from "next/image"; + +interface TitleSectionProps { + logoUrl: string; + title: string; + subTitle: string; +} + +const TitleSection = ({ logoUrl, title, subTitle }: TitleSectionProps) => { + return ( +
+
+ 대학 로고 +
+ {title} + {subTitle} +
+
+
+ ); +}; + +export default TitleSection; diff --git a/src/app/university/[id]/UniversityDetail.tsx b/src/app/university/[id]/UniversityDetail.tsx new file mode 100644 index 00000000..f2adc6df --- /dev/null +++ b/src/app/university/[id]/UniversityDetail.tsx @@ -0,0 +1,59 @@ +import Image from "next/image"; + +import EnglishSection from "./EnglishSection"; +import InfoSection from "./InfoSection"; +import LanguageSection from "./LanguageSection"; +import MajorSection from "./MajorSection"; +import MapSection from "./MapSection"; +import SubTitleSection from "./SubTitleSection"; +import TitleSection from "./TitleSection"; + +import { University } from "@/types/university"; + +interface UniversityDetailProps { + university: University; +} + +const UniversityDetail = ({ university }: UniversityDetailProps) => { + return ( + <> +
+ 대학 이미지 +
+
+ + {/* TODO: totalDispatchCount 추가시 연동, 나라에 국기 추가 */} + + + + + + +
+
+ + ); +}; + +export default UniversityDetail; diff --git a/src/app/university/[id]/college-bottomsheet.module.css b/src/app/university/[id]/college-bottomsheet.module.css deleted file mode 100644 index 7c2ce2a9..00000000 --- a/src/app/university/[id]/college-bottomsheet.module.css +++ /dev/null @@ -1,54 +0,0 @@ -.flexible-height { - height: calc(100vw / 2); - background: linear-gradient(180deg, rgba(0, 0, 0, 0) 33.85%, rgba(0, 0, 0, 0.6) 86.98%); -} -@media (min-width: 600px) { - .flexible-height { - height: 300px; - } -} - -.bar { - margin-top: 42px; /* 임시 */ - position: sticky; - top: 103px; - - height: 52px; - padding: 0 20px 0 20px; - background: var(--primary-1, #6f90d1); - overflow-x: auto; - white-space: nowrap; - - display: flex; - gap: 10px; - align-items: center; - - color: #fff; - font-family: Pretendard; - font-size: 16px; - font-style: normal; - font-weight: 600; - line-height: normal; -} - -/* 섹션 */ -.scrollOffset { - /* 네비게이션 + 탭 = 56 + 47 = 103 */ - /* 추가 42 */ - padding-top: 145px; - margin-top: -103px; - - display: flex; - flex-direction: column; - gap: 30px; -} -.scrollOffsetWithBar { - /* 네비게이션 + 탭 + 바 = 56 + 47 + 52 = 155 */ - /* 추가 21 */ - padding-top: 176px; - margin-top: -155px; - - display: flex; - flex-direction: column; - gap: 30px; -} diff --git a/src/app/university/[id]/college-reviews.module.css b/src/app/university/[id]/college-reviews.module.css deleted file mode 100644 index c46735f5..00000000 --- a/src/app/university/[id]/college-reviews.module.css +++ /dev/null @@ -1,107 +0,0 @@ -/* Review cards */ -.container { - display: flex; - flex-direction: column; - gap: 24px; -} - -/* Review card */ -.card { - position: relative; - margin: 0 20px 0 20px; - padding: 14px 12px 14px 12px; - border-radius: 6px; - border: 1px solid #bbb; -} - -.firstRow { - display: flex; - justify-content: space-between; -} -.term { - color: #000; - font-family: Pretendard; - font-size: 14px; - font-style: normal; - font-weight: 600; - line-height: 150%; /* 21px */ -} - -.fullStar { - color: gold; /* Full star color */ -} -.halfStar::before { - content: "★"; - color: gold; /* Half star color */ - position: absolute; -} -.halfStar::after { - content: "☆"; - color: gold; /* Half star color */ - position: absolute; - width: 50%; - overflow: hidden; /* This will cover half of the star */ -} -.emptyStar { - color: lightgray; /* Empty star color */ -} - -.content { - margin: 0 2px 0 2px; - color: #000; - font-family: Pretendard; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 150%; /* 21px */ -} - -.infos { - display: flex; - flex-direction: column; - gap: 16px; -} -.infos > div { - margin-left: 7px; - display: flex; -} -.infos > div > span:nth-of-type(1) { - flex-basis: 109px; - color: rgba(0, 0, 0, 0.87); - font-feature-settings: - "clig" off, - "liga" off; - font-family: Pretendard; - font-size: 14px; - font-style: normal; - font-weight: 600; - line-height: 160%; /* 22.4px */ -} -.infos > div > span:nth-of-type(2) { - color: rgba(44, 44, 44, 0.87); - font-feature-settings: - "clig" off, - "liga" off; - font-family: Pretendard; - font-size: 14px; - font-style: normal; - font-weight: 500; - line-height: 160%; /* 22.4px */ -} - -.toggleButton { - position: absolute; - bottom: -15px; - left: 48.5%; - width: 28px; - height: 28px; - - background: white; - border: solid 1px #bbb; - border-radius: 100%; - cursor: pointer; - - display: flex; - justify-content: center; - align-items: center; -} diff --git a/src/app/university/[id]/page.tsx b/src/app/university/[id]/page.tsx index 9fa64d9f..e08e8818 100644 --- a/src/app/university/[id]/page.tsx +++ b/src/app/university/[id]/page.tsx @@ -4,10 +4,8 @@ import { notFound } from "next/navigation"; import TopDetailNavigation from "@/components/layout/TopDetailNavigation"; -import CollegeBottomSheet from "./CollegeBottomSheet"; -import CollegeDetail from "./CollegeDetail"; +import UniversityDetail from "./UniversityDetail"; -import { Review } from "@/types/review"; import { University } from "@/types/university"; import { getUniversityDetailPublicApi } from "@/api/university"; @@ -28,11 +26,11 @@ export async function generateMetadata( // fetch data const res = await getUniversityDetailPublicApi(Number(id)); - const collegeData = res.data; + const universityData = res.data; const convertedKoreanName = - collegeData.term !== process.env.NEXT_PUBLIC_CURRENT_TERM - ? `${collegeData.koreanName}(${collegeData.term})` - : collegeData.koreanName; + universityData.term !== process.env.NEXT_PUBLIC_CURRENT_TERM + ? `${universityData.koreanName}(${universityData.term})` + : universityData.koreanName; return { title: convertedKoreanName, @@ -45,7 +43,6 @@ type CollegeDetailPageProps = { const CollegeDetailPage = async ({ params }: CollegeDetailPageProps) => { const collegeId = Number(params.id); - const reviewList: Review[] = []; let res: { data: University }; try { @@ -54,25 +51,16 @@ const CollegeDetailPage = async ({ params }: CollegeDetailPageProps) => { notFound(); // 404 페이지로 이동 } - const collegeData = res.data; + const universityData = res.data; const convertedKoreanName = - collegeData.term !== process.env.NEXT_PUBLIC_CURRENT_TERM - ? `${collegeData.koreanName}(${collegeData.term})` - : collegeData.koreanName; + universityData.term !== process.env.NEXT_PUBLIC_CURRENT_TERM + ? `${universityData.koreanName}(${universityData.term})` + : universityData.koreanName; return ( <> - - {convertedKoreanName || "대학명"} - - - - + + ); }; diff --git a/src/utils/languageUtils.ts b/src/utils/languageUtils.ts new file mode 100644 index 00000000..7116c8ba --- /dev/null +++ b/src/utils/languageUtils.ts @@ -0,0 +1,16 @@ +// 시험 종류별 로고 URL 맵 +export const logoMap: Record = { + TOEIC: "/images/language/toeic.png", + TOEFL_IBT: "/images/language/toefl_ibt.png", + TOEFL_ITP: "/images/language/toefl_itp.png", + IELTS: "/images/language/ielts.png", +}; + +export const getLanguageTestLogo = (type: string): string => { + return logoMap[type] || "/images/language/default.png"; +}; + +// UNDER_SCORE → "UNDER SCORE" 처리를 위한 헬퍼 +export function formatLanguageTestName(type: string): string { + return type.replace(/_/g, " "); +}