diff --git a/src/assets/icons/BookmarkCheckedIcon.svg b/src/assets/icons/BookmarkCheckedIcon.svg new file mode 100644 index 00000000..5f83cdbc --- /dev/null +++ b/src/assets/icons/BookmarkCheckedIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/BookmarkIcon.svg b/src/assets/icons/BookmarkIcon.svg new file mode 100644 index 00000000..d1a26799 --- /dev/null +++ b/src/assets/icons/BookmarkIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/CalendarIcon.svg b/src/assets/icons/CalendarIcon.svg new file mode 100644 index 00000000..8e35de0e --- /dev/null +++ b/src/assets/icons/CalendarIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ClockIcon.svg b/src/assets/icons/ClockIcon.svg new file mode 100644 index 00000000..5d26dc4f --- /dev/null +++ b/src/assets/icons/ClockIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/icons/Home/AlarmIcon.svg b/src/assets/icons/Home/AlarmIcon.svg new file mode 100644 index 00000000..e2ab7756 --- /dev/null +++ b/src/assets/icons/Home/AlarmIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/icons/Home/MessageIcon.svg b/src/assets/icons/Home/MessageIcon.svg new file mode 100644 index 00000000..c673c720 --- /dev/null +++ b/src/assets/icons/Home/MessageIcon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/icons/Home/RightArrowIcon.svg b/src/assets/icons/Home/RightArrowIcon.svg new file mode 100644 index 00000000..72cc2c14 --- /dev/null +++ b/src/assets/icons/Home/RightArrowIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/Home/TopRightArrowIcon.svg b/src/assets/icons/Home/TopRightArrowIcon.svg new file mode 100644 index 00000000..75513d25 --- /dev/null +++ b/src/assets/icons/Home/TopRightArrowIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/LocationIcon.svg b/src/assets/icons/LocationIcon.svg new file mode 100644 index 00000000..2b422eb1 --- /dev/null +++ b/src/assets/icons/LocationIcon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/images/JobIconExample.jpeg b/src/assets/images/JobIconExample.jpeg new file mode 100644 index 00000000..3ef371bf Binary files /dev/null and b/src/assets/images/JobIconExample.jpeg differ diff --git a/src/components/Common/JobPostingCard.tsx b/src/components/Common/JobPostingCard.tsx new file mode 100644 index 00000000..bf7c99a6 --- /dev/null +++ b/src/components/Common/JobPostingCard.tsx @@ -0,0 +1,118 @@ +import Tag from '@/components/Common/Tag'; +import BookmarkIcon from '@/assets/icons/BookmarkIcon.svg?react'; +import BookmarkCheckedIcon from '@/assets/icons/BookmarkCheckedIcon.svg?react'; +import LocationIcon from '@/assets/icons/LocationIcon.svg?react'; +import ClockIcon from '@/assets/icons/ClockIcon.svg?react'; +import CalendarIcon from '@/assets/icons/CalendarIcon.svg?react'; +import { JobPostingItemType } from '@/types/common/jobPostingItem'; +import { calculateTimeAgo } from '@/utils/calculateTimeAgo'; +import { calculateDDay } from '@/utils/calculateDDay'; +import { useState } from 'react'; + +interface JobPostingCardProps { + jobPostingData: JobPostingItemType; +} + +const JobPostingCard = ({ jobPostingData }: JobPostingCardProps) => { + const [isBookmarked, setIsBookmarked] = useState( + jobPostingData?.is_book_marked ?? false, + ); + + return ( +
+
+
+
+
+

{jobPostingData.title}

+
+
+
+ +

+ {jobPostingData.summaries.address} +

+
+
+ +

+ {jobPostingData.summaries.work_period + .replace(/_/g, ' ') + .toLowerCase()} +

+
+
+ +

+ {jobPostingData.summaries.work_days_per_week} days a week +

+
+
+
+
+ + {isBookmarked ? ( + setIsBookmarked(false)} /> + ) : ( + setIsBookmarked(true)} /> + )} +
+
+
+ + + +
+
+

+ {calculateTimeAgo(jobPostingData.created_at)} +

+

+ + {jobPostingData.hourly_rate}KRW + + /Hr +

+
+
+ ); +}; + +export default JobPostingCard; diff --git a/src/components/Common/Tag.tsx b/src/components/Common/Tag.tsx index 05ee6d17..8ecea316 100644 --- a/src/components/Common/Tag.tsx +++ b/src/components/Common/Tag.tsx @@ -26,14 +26,14 @@ const Tag = ({ }: TagProps) => { return (
{hasCheckIcon && } diff --git a/src/components/Home/HomeApplicationCard.tsx b/src/components/Home/HomeApplicationCard.tsx new file mode 100644 index 00000000..ccc4021b --- /dev/null +++ b/src/components/Home/HomeApplicationCard.tsx @@ -0,0 +1,32 @@ +import TopRightArrowIcons from '@/assets/icons/Home/TopRightArrowIcon.svg?react'; +import { OngoingInterviewItemType } from '@/types/home/ongoingInterviewItem'; + +type HomeApplicationCardProps = { + applicationData: OngoingInterviewItemType; +}; + +const HomeApplicationCard = ({ applicationData }: HomeApplicationCardProps) => { + return ( +
+
+ Under the Review +
+
+
+
+
+ {applicationData.title} +
+

+ {applicationData.address_name} +

+
+ +
+
+ ); +}; + +export default HomeApplicationCard; diff --git a/src/components/Home/HomeApplicationList.tsx b/src/components/Home/HomeApplicationList.tsx new file mode 100644 index 00000000..c7f4896f --- /dev/null +++ b/src/components/Home/HomeApplicationList.tsx @@ -0,0 +1,41 @@ +import { OngoingInterviewItemType } from '@/types/home/ongoingInterviewItem'; +import HomeApplicationCard from '@/components/Home/HomeApplicationCard'; + +// 진행중인 인터뷰 더미데이터 +const ONGOING_INTERVIEW_LIST: OngoingInterviewItemType[] = [ + { + id: 12, + icon_img_url: 'aa', + title: '서류 제목이양 야다ㅏ야아닫', + address_name: '주소 주소 주소 주소 주소', + }, + { + id: 23, + icon_img_url: 'aa', + title: '서류2', + address_name: '주소 주소 22', + }, + { + id: 24, + icon_img_url: 'aa', + title: '서류3', + address_name: '주소 주소 33', + }, +]; + +const HomeApplicationList = () => { + return ( +
+

+ Ongoing Application +

+
+ {ONGOING_INTERVIEW_LIST.map((value: OngoingInterviewItemType) => ( + + ))} +
+
+ ); +}; + +export default HomeApplicationList; diff --git a/src/components/Home/HomeGuestBanner.tsx b/src/components/Home/HomeGuestBanner.tsx new file mode 100644 index 00000000..f6e1cca2 --- /dev/null +++ b/src/components/Home/HomeGuestBanner.tsx @@ -0,0 +1,21 @@ +const HomeGuestBanner = () => { + return ( +
+
+

+ Sign In to Unlock Your
+ Gig Opportunities! +

+

+ Get personalized job recommendations, track your applications, and + access exclusive opportunities. Sign in now to get started! +

+ +
+
+ ); +}; + +export default HomeGuestBanner; diff --git a/src/components/Home/HomeHeader.tsx b/src/components/Home/HomeHeader.tsx new file mode 100644 index 00000000..8820c836 --- /dev/null +++ b/src/components/Home/HomeHeader.tsx @@ -0,0 +1,23 @@ +import AlarmIcon from '@/assets/icons/Home/AlarmIcon.svg?react'; + +const HomeHeader = () => { + return ( +
+

Welcome!

+
+

+ Find your
+ perfect job +

+ {/* TODO: 로그인 시에만 표시하기 */} + +
+
+ ); +}; + +export default HomeHeader; diff --git a/src/components/Home/HomeJobPostingList.tsx b/src/components/Home/HomeJobPostingList.tsx new file mode 100644 index 00000000..aab2e625 --- /dev/null +++ b/src/components/Home/HomeJobPostingList.tsx @@ -0,0 +1,161 @@ +import JobPostingCard from '@/components/Common/JobPostingCard'; +import { JobPostingItemType } from '@/types/common/jobPostingItem'; +import RightArrowIcon from '@/assets/icons/Home/RightArrowIcon.svg?react'; +import Tag from '@/components/Common/Tag'; +import { useRef, useState } from 'react'; + +// 공고 목록 더미데이터 +const JOB_POSTING_LIST: JobPostingItemType[] = [ + { + id: 1234567890, + icon_img_url: 'https://example.com/images/icon1.png', + title: 'English Tutor', + summaries: { + address: 'Seoul, South Korea', + work_period: '1_WEEK_TO_1_MONTH', + work_days_per_week: 5, + }, + tags: { + is_recruiting: true, + visa: 'D-2-1', + job_category: 'GENERAL_INTERPRETATION_TRANSLATION', + }, + hourly_rate: 15000, + recruitment_dead_line: '2024-11-01T23:59:59', + created_at: '2024-10-20T10:30:00', + }, + { + id: 9876543210, + icon_img_url: 'https://example.com/images/icon2.png', + title: 'Café Barista', + summaries: { + address: 'Busan, South Korea', + work_period: '3_MONTHS_TO_6_MONTHS', + work_days_per_week: 6, + }, + tags: { + is_recruiting: false, + visa: 'D-4-1', + job_category: 'GENERAL_CAFE', + }, + hourly_rate: 12000, + recruitment_dead_line: '2024-10-21T18:00:00', + created_at: '2024-10-15T09:00:00', + }, +]; + +const enum Menu { + POPULAR = 'POPULAR', + RECENT = 'RECENT', + BOOKMARKS = 'BOOKMARKS', +} + +const HomeJobPostingList = () => { + const scrollRef = useRef<(HTMLDivElement | null)[]>([]); + + const [selectedMenu, setSelectedMenu] = useState(Menu.POPULAR); + + const scrollToSelectedMenu = (menu: Menu) => { + const scrollIndex: { [key: string]: number } = { + POPULAR: 0, + RECENT: 1, + BOOKMARKS: 2, + }; + + const target = scrollRef.current[scrollIndex[menu]]; + if (!target) return; + target.scrollIntoView({ behavior: 'smooth' }); + setSelectedMenu(menu); + }; + + return ( +
+ +
+
(scrollRef.current[0] = e)} + > +
+

🔥 Popular Job Lists for You

+ +
+ {JOB_POSTING_LIST.map((value: JobPostingItemType) => ( + + ))} +
+
(scrollRef.current[1] = e)} + > +
+

🌟 Recently Added Job

+ +
+ {JOB_POSTING_LIST.map((value: JobPostingItemType) => ( + + ))} +
+
(scrollRef.current[2] = e)} + > +
+

🌟 My Bookmarks

+ +
+ {JOB_POSTING_LIST.map((value: JobPostingItemType) => ( + + ))} +
+
+
+ ); +}; + +export default HomeJobPostingList; diff --git a/src/components/Home/HomeRecommendPost.tsx b/src/components/Home/HomeRecommendPost.tsx new file mode 100644 index 00000000..d840fb61 --- /dev/null +++ b/src/components/Home/HomeRecommendPost.tsx @@ -0,0 +1,65 @@ +import MessageIcon from '@/assets/icons/Home/MessageIcon.svg?react'; +import HomeRecommendPostCard from '@/components/Home/HomeRecommendPostCard'; +import { RecommendJobPostingItemType } from '@/types/home/recommendJobPostingItem'; + +// 추천 공고 목록 더미데이터 +const RECOMMEND_JOB_POSTING_LIST: RecommendJobPostingItemType[] = [ + { + id: 1, + title: '공고 제목1', + recruitment_dead_line: '2024.11.01', + job_category: 'GENERAL_INTERPRETATION_TRANSLATION', + }, + { + id: 2, + title: '공고 제목 222 어쩌구 저쩌구', + recruitment_dead_line: '2024.10.25', + job_category: 'GENERAL_CAFE', + }, +]; + +const HomeRecommendPost = () => { + const hasProfile = true; // 이력서 등록여부 + return ( +
+ {hasProfile ? ( + // 이력서 등록 -> 추천 공고 보여주기 + <> + {RECOMMEND_JOB_POSTING_LIST.map( + (value: RecommendJobPostingItemType) => ( + + ), + )} + + ) : ( + // 이력서 미등록 -> 추가 메뉴 보여주기 + <> +
+ +
+

+ 프로필 채우기 +

+
+ 프로필을 작성하면 추천 공고를 열람할 수 있어요 +
+
+
+
+ +
+

+ 공고 찾아보기 +

+
+ 맘에 쏙 드는 공고들을 저장해보세요 +
+
+
+ + )} +
+ ); +}; + +export default HomeRecommendPost; diff --git a/src/components/Home/HomeRecommendPostCard.tsx b/src/components/Home/HomeRecommendPostCard.tsx new file mode 100644 index 00000000..528ce46f --- /dev/null +++ b/src/components/Home/HomeRecommendPostCard.tsx @@ -0,0 +1,35 @@ +import MessageIcon from '@/assets/icons/Home/MessageIcon.svg?react'; +import { RecommendJobPostingItemType } from '@/types/home/recommendJobPostingItem'; +import { calculateDDay } from '@/utils/calculateDDay'; + +interface HomeRecommendPostCardProps { + jobPostingData: RecommendJobPostingItemType; +} + +const HomeRecommendPostCard = ({ + jobPostingData, +}: HomeRecommendPostCardProps) => { + return ( +
+
+
+ {calculateDDay(jobPostingData.recruitment_dead_line)} +
+
+ {jobPostingData.job_category.replace(/_/g, ' ').toLowerCase()} +
+
+ +
+

+ 한은서님을 위한 추천 공고 도착! +

+
+ 공고제목{')'} {jobPostingData.title} +
+
+
+ ); +}; + +export default HomeRecommendPostCard; diff --git a/src/index.css b/src/index.css index 80bb11e9..4f5da4b9 100644 --- a/src/index.css +++ b/src/index.css @@ -14,66 +14,93 @@ body { @layer components { /* 폰트 설정하기 */ + .title-1 { + font-weight: 600; + font-size: 1.75rem; + line-height: 2.25rem; + } + + .title-2 { + font-weight: 600; + font-size: 1.625rem; + line-height: 2rem; + } + .head-1 { font-weight: 600; font-size: 1.5rem; line-height: 2rem; - letter-spacing: -0.0625rem; } .head-2 { font-weight: 600; font-size: 1.25rem; line-height: 1.5rem; - letter-spacing: -0.5%; } .head-3 { font-weight: 600; font-size: 1rem; line-height: 1.25rem; - letter-spacing: -1%; } .body-1 { - font-weight: 500; + font-weight: 400; font-size: 1rem; line-height: 1.5rem; - letter-spacing: -0.015625rem; } .body-2 { - font-weight: 500; + font-weight: 400; font-size: 0.875rem; line-height: 1.25rem; - letter-spacing: -0.015625rem; } .body-3 { - font-weight: 500; + font-weight: 400; font-size: 0.75rem; line-height: 1rem; letter-spacing: 0rem; } - .button { - font-weight: 700; + .button-1 { + font-weight: 600; font-size: 1rem; line-height: 1.25rem; - letter-spacing: -0.0125rem; } - .chips { - font-weight: 700; + .button-2 { + font-weight: 600; font-size: 0.75rem; line-height: 1rem; - letter-spacing: -0.03125rem; } - .caption { - font-weight: 500; + .caption-1 { + font-weight: 400; + font-size: 0.625rem; + line-height: 0.75rem; + } + + .caption-1-sb { + font-weight: 600; font-size: 0.625rem; line-height: 0.75rem; - letter-spacing: 2%; + } + + .caption-2 { + font-weight: 400; + font-size: 0.5rem; + line-height: 0.625rem; + } +} + +@layer utilities { + .no-scrollbar { + -webkit-overflow-scrolling: touch; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + .no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ } } diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index 7dc62189..129ddddb 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -1,5 +1,22 @@ +import HomeApplicationList from '@/components/Home/HomeApplicationList'; +import HomeGuestBanner from '@/components/Home/HomeGuestBanner'; +import HomeHeader from '@/components/Home/HomeHeader'; +import HomeJobPostingList from '@/components/Home/HomeJobPostingList'; +import HomeRecommendPost from '@/components/Home/HomeRecommendPost'; + const HomePage = () => { - return <>홈 페이지임; + return ( + <> + + {/* 로그인을 안한 경우에 보여주는 배너 */} + + {/* 로그인을 하고 이력서를 등록하면 보여주는 추천 공고 리스트 */} + + {/* 현재 진행중인 서류가 있는 경우 */} + + + + ); }; export default HomePage; diff --git a/src/types/common/jobPostingItem.ts b/src/types/common/jobPostingItem.ts new file mode 100644 index 00000000..172cea86 --- /dev/null +++ b/src/types/common/jobPostingItem.ts @@ -0,0 +1,23 @@ +type JobPostingSummariesType = { + address: string; // 위치 정보 + work_period: string; // 근무 기간 + work_days_per_week: number; // // 근무 일자 +}; + +type JobPostingTagType = { + is_recruiting: boolean; // “모집중/마감" + visa: string; // “비자종류" + job_category: string; // “업직종종류", +}; + +export type JobPostingItemType = { + id: number; + is_book_marked?: boolean; // 북마크 여부(로그인 시에만!) + icon_img_url?: string; // 회사 로고 + title: string; // 공고 제목 + summaries: JobPostingSummariesType; + tags: JobPostingTagType; + hourly_rate: number; // 시급 + recruitment_dead_line: string; // 마감일자 + created_at: string; // 등록일자 +}; diff --git a/src/types/home/ongoingInterviewItem.ts b/src/types/home/ongoingInterviewItem.ts new file mode 100644 index 00000000..86a4790b --- /dev/null +++ b/src/types/home/ongoingInterviewItem.ts @@ -0,0 +1,6 @@ +export type OngoingInterviewItemType = { + id: number; + icon_img_url: string; + title: string; + address_name: string; +}; diff --git a/src/types/home/recommendJobPostingItem.ts b/src/types/home/recommendJobPostingItem.ts new file mode 100644 index 00000000..a2e1af45 --- /dev/null +++ b/src/types/home/recommendJobPostingItem.ts @@ -0,0 +1,6 @@ +export type RecommendJobPostingItemType = { + id: number; + title: string; // 공고 제목 + recruitment_dead_line: string; // 마감일자 + job_category: string; // “업직종종류", +}; diff --git a/src/utils/calculateDDay.ts b/src/utils/calculateDDay.ts new file mode 100644 index 00000000..0665b087 --- /dev/null +++ b/src/utils/calculateDDay.ts @@ -0,0 +1,16 @@ +export const calculateDDay = (date: string): string => { + const targetTime = new Date(date).getTime(); + const currentTime = new Date().getTime(); + + const differenceDays = Math.ceil( + (targetTime - currentTime) / (1000 * 60 * 60 * 24), + ); + + if (differenceDays > 0) { + return `D-${differenceDays}`; + } else if (differenceDays === 0) { + return 'D-Day'; + } else { + return `D+${Math.abs(differenceDays)}`; + } +}; diff --git a/src/utils/calculateTimeAgo.ts b/src/utils/calculateTimeAgo.ts new file mode 100644 index 00000000..8fb2da61 --- /dev/null +++ b/src/utils/calculateTimeAgo.ts @@ -0,0 +1,37 @@ +export const calculateTimeAgo = (date: string): string => { + const startTime = new Date(date).getTime(); + const currentTime = new Date().getTime(); + + const MILLISECONDS_IN_MINUTE = 1000 * 60; + const MINUTES_IN_HOUR = 60; + const HOURS_IN_DAY = 24; + const DAYS_IN_WEEK = 7; + const DAYS_IN_MONTH = 30; + const DAYS_IN_YEAR = 365; + const WEEKS_IN_MONTH = 4; + const MONTHS_IN_YEAR = 12; + + const differenceMilliseconds = currentTime - startTime; + const differenceMinutes = differenceMilliseconds / MILLISECONDS_IN_MINUTE; + if (differenceMinutes < MINUTES_IN_HOUR) + return `${Math.floor(differenceMinutes)} mins ago`; + + const differenceHours = differenceMinutes / MINUTES_IN_HOUR; + if (differenceHours < HOURS_IN_DAY) + return `${Math.floor(differenceHours)} hours ago`; + + const differenceDays = differenceHours / HOURS_IN_DAY; + if (differenceDays < DAYS_IN_WEEK) + return `${Math.floor(differenceDays)} days ago`; + + const differenceWeeks = differenceHours / DAYS_IN_WEEK; + if (differenceWeeks < WEEKS_IN_MONTH) + return `${Math.floor(differenceWeeks)} weeks ago`; + + const differenceMonths = differenceDays / DAYS_IN_MONTH; + if (differenceMonths < MONTHS_IN_YEAR) + return `${Math.floor(differenceMonths)} months ago`; + + const differenceYears = differenceDays / DAYS_IN_YEAR; + return `${Math.floor(differenceYears)} years ago`; +}; diff --git a/tailwind.config.js b/tailwind.config.js index 81b8e901..c4b06814 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,11 +6,16 @@ export default { fontFamily: { pretendard: ['"Pretendard"', 'sans-serif'], }, - - backgroundImage:{ - navbarGradient : 'linear-gradient(270deg, #FEFEFE 0.35%, #F4F4F9 175.32%)', - grayGradient : 'linear-gradient(180deg, rgba(255, 255, 255, 0.80) 36.54%, #FFF 94.71%)' - } + + backgroundImage: { + navbarGradient: + 'linear-gradient(270deg, #FEFEFE 0.35%, #F4F4F9 175.32%)', + grayGradient: + 'linear-gradient(180deg, rgba(255, 255, 255, 0.80) 36.54%, #FFF 94.71%)', + }, + boxShadow: { + cardShadow: '0px 0px 1px rgba(0, 0, 0, 0.08)', + }, }, }, plugins: [require('tailwind-scrollbar-hide')],