Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 28기 신입모집 최신화 #127

Merged
merged 11 commits into from
Aug 30, 2024
8 changes: 4 additions & 4 deletions app/recruit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
"use client";

import { RECRUIT } from "@/src/constants/recruit/recruit.ko";
import { Footer } from "@/src/components/common/Footer";
import { Faq } from "@/src/components/recruit/Faq";
import { Fileds } from "@/src/components/recruit/Fields";
import { Recruit } from "@/src/components/recruit/Recruit";
import { Subscription } from "@/src/components/recruit/Subscription";
import { Waiting } from "@/src/components/recruit/Waiting";
import { useInsertionEffect, useRef, useState } from "react";
import useRecruitStatus from "../../src/hooks/useRecruitStatus";

const RecruitPage = () => {
const [emailInputValue, setEmailInputValue] = useState("");
const recruitRef = useRef<HTMLDivElement>(null);

const { recruitStatus } = useRecruitStatus();
const scrollToRecruit = () =>
window.scrollTo({
top: recruitRef.current?.offsetTop,
Expand All @@ -33,9 +32,10 @@ const RecruitPage = () => {
return (
<>
<div className="m-auto max-w-[1920px] px-24">
{!RECRUIT.IS_ON && (
{recruitStatus !== "OPEN" && (
<section>
<Waiting
recruitStatus={recruitStatus}
inputValue={emailInputValue}
onInputChange={(e) => setEmailInputValue(e.target.value)}
scrollToRecruit={scrollToRecruit}
Expand Down
54 changes: 49 additions & 5 deletions docs/Recruit.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,67 @@ const FAQ = {
string의 값들을 관리하기 위해서 모아두었습니다.
안에 소개 글을 바꾸고 싶다면 여기 있는 내용을 바꾸면 됩니다.

IS_ON은 전역적으로 영향을 받으며 true는 리크루트를 하고 있는 상태, false는 리크루트를 하지 않는 상태입니다.
true일때는 메인 페이지 하단에 날자가 쓰여집니다. false일때 리크루트 구독 페이지가 활성화 됩니다.
### IS_ON (Deprecated 됨)

~~IS_ON은 전역적으로 영향을 받으며 true는 리크루트를 하고 있는 상태, false는 리크루트를 하지 않는 상태입니다.
true일때는 메인 페이지 하단에 날자가 쓰여집니다. false일때 리크루트 구독 페이지가 활성화 됩니다.~~

IS_ON 속성은 Deprecated 되었습니다. 그 이유는 다음과 같습니다.

- 관리자가 수동으로 상태를 변경해주어야 합니다. 그러므로 번거롭습니다.
- 아직 신입모집을 하고 있는 상태는 아니지만 준비된 상태를 나타내기 힘듭니다. 즉, READY | OPEN | CLOSED 의 상태가 필요합니다.

### START_DATE, END_DATE

위의 `IS_ON`이 해결하지 못하는 문제를 해결하기 위해 두가지 속성과 한 가지 훅으로 관리합니다.
이 섹션에서는 두가지 속성에 대해 설명합니다.

**START_DATE**

신입모집을 모집을 시작하는 UTC 시간을 나타냅니다.

**END_DATE**

신입모집의 마감하는 UTC 시간을 나타냅니다.

> ⚠️ 반드시 신입모집 시작 일주일 전에는 해당 값과 기수 값을 바꿔주어야 합니다.

우리는 이제 두 값만 설정해둔다면, 쉽게 상태를 얻을 수 있습니다. 이는 `useRecruitStatus()`훅을 통해 구현합니다.

### useRecruitStatus()

hook을 사용하기 전에, 이를 가정합니다.

- `START_DATE` 는 항상 `END_DATE`보다 이른 시간이어야 합니다.
- 두 속성은 항상 UTC로 정의 되어야 합니다.

훅은 다음과 같이 동작합나다.

- 만약 현재 시간이 `START_DATE`보다 이전이면, 훅은 `READY`를 반환합니다.
- 만약 현재 시간이 `START_DATE`와 `END_DATE` 사이 이면, 상태는 `OPEN`을 반환합니다.
- 만약 현재 시간이 `END_DATE`이후면, 훅은 `CLOSED`를 반환합니다.

#### how to use

```ts
const { recruitStatus } = useRecruitStatus();
```

GENERATION은 해당 진행되는 기수입니다. 2023년도 1학기 기준 25기를 모집하였습니다.

```ts
const RECRUIT = {
TITLE: "recruit", // recruit string 입니다.
CONTENT: string, // recruit에 대한 소개입니다.
IS_ON: boolean, // 리크루트의 진행 상황에 대해 나타냅니다.
IS_ON: boolean, // DEPRECATED: 리크루트의 진행 상황에 대해 나타냅니다.
START_DATE: Date, // 신입모집 시작 시간을 UTC로 표현합니다.
END_DATE: Date, // 신입모집 서류 마감 시간을 UTC로 표현합니다.
GENERATION: number, // 해당 기수에 대해서 입니다.
SCHEDULE: { // 4개면 가장 좋습니다.
TEXT: string, // 스케줄에 대해서 설명하는 string 입니다.
DATE: string, //스케쥴이 언제까지 인지 설명하는 string 입니다. 추후 RECRUIT_FLOAT도 변경해야 합니다.
}[],
WATING: { // IS_ON이 false일 때 활성화 되는 곳입니다.
WATING: { // RecruitStatus가 OPEN이 아닐 때 나타나는 정보들 입니다.
TITLE: "comming soon", // warting string 입니다.
CONTENT: string, // 마감되었다는 글입니다. \n으로 띄어쓰기를 할 수 있습니다.
EMAIL_ALERT: string, // email로 받을 때 소개 글입니다. "입력한 정보의 보유기간은 모집 알림 전송시까지 보관됩니다.",
Expand Down Expand Up @@ -76,6 +121,5 @@ const RECRUIT_FLOAT = { // 메인페이지에 뜨는 부분입니다.
HOUR: "hour",
MINUTE: "minute",
SECOND: "second",
RECRUIT_START_DATE: "2023-08-04", // 시작하는 날짜 입니다. "yyyy-mm-dd" 형태를 유지해 주세요.
};
```
3 changes: 1 addition & 2 deletions src/components/main/Intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import econovationBlackLogo from "/public/images/econovation-black.svg";
import { InfinityAutoScroll } from "../common/InfinityAutoScroll";
import { ABOUT, JOBS } from "@/src/constants/main.ko";
import { Fragment } from "react";
import { RECRUIT } from "@/src/constants/recruit/recruit.ko";
import { RecruitFloat } from "./RecruitFloat";
import { cn } from "@/src/functions/util";

export const Intro = () => {
return (
<>
{RECRUIT.IS_ON && <RecruitFloat />}
2yunseong marked this conversation as resolved.
Show resolved Hide resolved
<RecruitFloat />
<h1>
<Image
className="w-full"
Expand Down
197 changes: 98 additions & 99 deletions src/components/main/RecruitFloat.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,115 @@
"use client";

import { RECRUIT_FLOAT } from "@/src/constants/recruit/recruit.ko";
import gsap from "gsap";
import {
RECRUIT,
RECRUIT_FLOAT,
type RecruitStatus,
} from "@/src/constants/recruit/recruit.ko";
import Image from "next/image";
import { useRef } from "react";
import { LinkTo } from "../common/LinkTo";
import { useTimeDiffer } from "@/src/hooks/useTimeDiffer";
import useFloatAnimation from "../../hooks/useFloatAnimation";
import useRecruitStatus from "../../hooks/useRecruitStatus";
import HoverText from "./RecruitFloat/HoverText";
import RecruitTimer from "./RecruitFloat/RecruitTimer";
import { exhaustiveError } from "../../functions/util";

export const RecruitFloat = () => {
const recruitDate = new Date(RECRUIT_FLOAT.RECRUIT_START_DATE);
const floatRef = useRef<HTMLDivElement>(null);
const floatDetailRef = useRef<HTMLDivElement>(null);
const { days, hours, minutes, seconds } = useTimeDiffer(recruitDate);

const showDetail = () => {
gsap.to(floatDetailRef.current, {
duration: 0.5,
bottom: "-1rem",
ease: "back.out",
});
gsap.to(floatRef.current, {
duration: 0.5,
bottom: "-150%",
ease: "back.in",
});
};
const renderwordByRecruitStatus = (recruitStatus: RecruitStatus) => {
switch (recruitStatus) {
case "READY":
return (
<span>
<span className="text-white">시작</span>까지
</span>
);
case "OPEN":
return (
<span>
<span className="text-white">마감</span>까지
</span>
);
case "CLOSED":
return <></>;
default:
exhaustiveError(recruitStatus);
}
};

const closeDetail = () => {
gsap.to(floatDetailRef.current, {
duration: 0.5,
bottom: "-200%",
ease: "back.in",
});
gsap.to(floatRef.current, {
duration: 0.5,
bottom: "-1rem",
ease: "back.out",
});
};
export const RecruitFloat = () => {
const recruitStartDate = new Date(RECRUIT.START_DATE);
const recruitEndDate = new Date(RECRUIT.END_DATE);
const { floatDetailRef, floatRef, showDetail, closeDetail } =
useFloatAnimation();
const { days, hours, minutes, seconds } = useTimeDiffer(recruitStartDate);
const {
days: endDays,
hours: endHours,
minutes: endMinutes,
seconds: endSeconds,
} = useTimeDiffer(recruitEndDate);
const { recruitStatus } = useRecruitStatus();

return (
<div
className="fixed bottom-0 h-16 w-full max-w-[1600px] z-[200]"
onMouseEnter={showDetail}
onMouseLeave={closeDetail}
>
<div
className="bg-black absolute -bottom-4 rounded-t-3xl text-white pt-4 pb-8 px-8 text-2xl uppercase group font-bold"
ref={floatRef}
>
{RECRUIT_FLOAT.ECONO_IS_RECRUITING}
</div>
if (recruitStatus !== "CLOSED") {
return (
<div
className="absolute -bottom-[200%] pb-8 pt-4 px-8 w-[calc(100%-6rem)] bg-black text-white rounded-t-3xl font-semibold"
ref={floatDetailRef}
className="fixed bottom-0 z-[200] h-16 w-full max-w-[1600px]"
onMouseEnter={showDetail}
onMouseLeave={closeDetail}
>
<div className="pb-2 border-b-white border-b-2 flex justify-between px-4">
<div className="flex gap-4 items-baseline">
<div className="uppercase text-3xl">
{RECRUIT_FLOAT.ECONO_GENERTAION_RECRUIT_EN}
</div>
<div>{RECRUIT_FLOAT.ECONO_GENERTAION_RECRUIT_KR}</div>
</div>
<div className="flex gap-8">
<div className="flex gap-4">
<div className="flex flex-col items-center mr-4">
<div className="text-3xl">{days}</div>
<div className="text-[0.5rem] uppercase">
{RECRUIT_FLOAT.DAY}
</div>
</div>
<div className="flex flex-col items-center">
<div className="text-3xl opacity-90">{hours}</div>
<div className="text-[0.5rem] uppercase">
{RECRUIT_FLOAT.HOUR}
</div>
</div>
<Image
className="mb-4"
src={require("/public/icons/colon.svg").default}
alt="colon"
/>
<div className="flex flex-col items-center">
<div className="text-3xl opacity-90">{minutes}</div>
<div className="text-[0.5rem] uppercase">
{RECRUIT_FLOAT.MINUTE}
</div>
<div
className="group absolute -bottom-4 rounded-t-3xl bg-black px-8 pb-8 pt-4 text-2xl font-bold uppercase text-white"
ref={floatRef}
>
<HoverText status={recruitStatus} days={days} />
</div>
<div
className="absolute -bottom-[200%] w-[calc(100%-6rem)] rounded-t-3xl bg-black px-8 pb-8 pt-4 font-semibold text-white"
ref={floatDetailRef}
>
<div className="flex justify-between border-b-2 border-b-white px-4 pb-2">
<div className="flex items-baseline gap-4">
<div className="text-3xl uppercase">
{RECRUIT_FLOAT.ECONO_GENERTAION_RECRUIT_EN}
</div>
<Image
className="mb-4"
src={require("/public/icons/colon.svg").default}
alt="colon"
/>
<div className="flex flex-col items-center">
<div className="text-3xl opacity-90">{seconds}</div>
<div className="text-[0.5rem] uppercase">
{RECRUIT_FLOAT.SECOND}
</div>
<div className="text-neutral-300">
<span>에코노베이션</span>
<span className="text-white">` {RECRUIT.GENERTAION}기 `</span>
<span>신입 모집&nbsp;</span>
{renderwordByRecruitStatus(recruitStatus)}
</div>
</div>
<LinkTo
link="RECRUIT"
className="bg-white rounded-full h-10 w-10 flex justify-center items-center"
>
<Image
src={require("/public/icons/right-arrow.svg").default}
alt="right-arrow"
/>
</LinkTo>
<div className="flex gap-8">
{recruitStatus === "OPEN" && (
<RecruitTimer
days={endDays}
hours={endHours}
minutes={endMinutes}
seconds={endSeconds}
/>
)}
{recruitStatus === "READY" && (
<RecruitTimer
days={days}
hours={hours}
minutes={minutes}
seconds={seconds}
/>
)}

<LinkTo
link="RECRUIT"
className="flex h-10 w-10 items-center justify-center rounded-full bg-white"
>
<Image
src={require("/public/icons/right-arrow.svg").default}
alt="right-arrow"
/>
</LinkTo>
</div>
</div>
</div>
</div>
</div>
);
);
}
return <></>;
};
24 changes: 24 additions & 0 deletions src/components/main/RecruitFloat/HoverText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
RECRUIT_FLOAT,
type RecruitStatus,
} from "../../../constants/recruit/recruit.ko";

interface HoverTextProps {
status: RecruitStatus;
days?: number;
}

const HoverText = ({ status, days }: HoverTextProps) => {
const hoverComponents = {
READY: (
<span>
{RECRUIT_FLOAT.ECONO_READY_FOR_RECRUIT} D-{days}
</span>
),
OPEN: <span>{RECRUIT_FLOAT.ECONO_IS_RECRUITING}</span>,
CLOSED: <span />,
};
return hoverComponents[status] ?? <span />;
};

export default HoverText;
Loading