Skip to content

Commit

Permalink
Merge pull request #71 from softeerbootcamp4th/feature/12-commentary
Browse files Browse the repository at this point in the history
[feat] 기대평 작성 기능 추가
  • Loading branch information
darkdulgi authored Aug 6, 2024
2 parents da93efc + 0a0559d commit a8c5d69
Show file tree
Hide file tree
Showing 38 changed files with 810 additions and 119 deletions.
Binary file added public/icons/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/[email protected]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import Input from "@/common/Input.jsx";

const ONE_MINUTES = 60;

function InputWithTimer({ text, setText, timer, ...otherProps }) {
const minute = Math.floor(timer / ONE_MINUTES);
const seconds = timer % ONE_MINUTES;

return (
<div className="w-full flex items-center relative">
<Input text={text} setText={setText} {...otherProps} />
<span className="absolute text-body-s text-red-400 font-bold right-4">
{timer}
{minute}:{seconds.toString().padStart(2, "0")}
</span>
</div>
);
Expand Down
90 changes: 90 additions & 0 deletions src/auth/AuthCode/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useState } from "react";
import InputWithTimer from "./InputWithTimer.jsx";
import useTimer from "./useTimer.js";
import submitAuthCode from "./submitAuthCode.js";
import requestAuthCode from "../requestAuthCode.js";
import Button from "@/common/Button.jsx";

const AUTH_MAX_DURATION = 5 * 60;

function AuthSecondSection({ name, phone, onComplete }) {
// 상태
const [authCode, setAuthCode] = useState("");
const [timer, resetTimer] = useTimer(AUTH_MAX_DURATION);
const [errorMessage, setErrorMessage] = useState("");

// 인증코드 재전송 동작
function retryAuthCode() {
requestAuthCode(name, phone)
.then(() => {
setErrorMessage("");
setAuthCode("");
resetTimer();
})
.catch((error) => setErrorMessage(error.message));
}

// 인증코드 전송 동작
function onSubmit(e) {
e.preventDefault();
submitAuthCode(name, phone, authCode)
.then(() => {
setErrorMessage("");
onComplete(true);
})
.catch((error) => {
setErrorMessage(error.message);
});
}

const josa = "013678".includes(phone[phone.length - 1]) ? "으" : "";
return (
<div className="w-full h-[calc(100svh-2rem)] max-h-[40.625rem] p-6 min-[520px]:px-20 py-10 relative flex flex-col gap-14">
<p className="text-body-l font-bold text-neutral-700">
{phone}
{josa}<br />
인증번호를 전송했어요.
</p>
<form
className="flex flex-col flex-grow w-full relative pb-4 gap-4 group"
onSubmit={onSubmit}
>
<div className="flex flex-col flex-grow justify-center items-center gap-7 px-0.5 relative h-0">
<InputWithTimer
text={authCode}
setText={setAuthCode}
timer={timer}
required
minLength="6"
maxLength="6"
placeholder="인증번호를 입력해주세요"
isError={errorMessage !== "" || timer === 0}
/>
<span className="absolute bottom-5 text-detail-l font-bold text-red-400">
{errorMessage || (timer === 0 ? "입력시간이 종료되었습니다." : "")}
</span>
</div>
<div className="w-full flex flex-wrap justify-center gap-5">
<Button
styleType="filled"
type="submit"
className="w-36 min-h-14"
disabled={timer === 0}
>
인증 완료하기
</Button>
<Button
styleType="ghost"
type="button"
className="min-h-14"
onClick={retryAuthCode}
>
재전송
</Button>
</div>
</form>
</div>
);
}

export default AuthSecondSection;
29 changes: 29 additions & 0 deletions src/auth/AuthCode/submitAuthCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { fetchServer, handleError } from "@/common/fetchServer.js";
import { EVENT_ID } from "@/common/constants.js";
import tokenSaver from "../tokenSaver.js";

async function submitAuthCode(name, phoneNumber, authCode) {
try {
const body = {
name,
phoneNumber: phoneNumber.replace(/\D+/g, ""),
authCode,
};
const { token } = await fetchServer(
`/api/v1/event-user/check-auth/${EVENT_ID}`,
{
method: "post",
body,
},
);
tokenSaver.set(token);
return "";
} catch (e) {
return handleError({
400: "잘못된 요청 형식입니다.",
401: "인증번호가 틀렸습니다. 다시 입력하세요.",
})(e);
}
}

export default submitAuthCode;
32 changes: 32 additions & 0 deletions src/auth/AuthCode/useTimer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState, useRef, useEffect, useCallback } from "react";
import IntervalController from "@/common/IntervalController.js";

function useTimer(remainTime) {
const [timer, setTimer] = useState(remainTime);
const intervalController = useRef(new IntervalController(1000));

useEffect(() => {
const ticker = intervalController.current;

function decreaseTime() {
setTimer((timer) => (timer > 0 ? timer - 1 : 0));
}
ticker.addEventListener("interval", decreaseTime);
ticker.start();

return () => {
ticker.end();
ticker.removeEventListener("interval", decreaseTime);
};
}, []);

const resetTimer = useCallback(() => {
setTimer(remainTime);
intervalController.current.end();
intervalController.current.start();
}, [remainTime]);

return [timer, resetTimer];
}

export default useTimer;
32 changes: 22 additions & 10 deletions src/auth/AuthModal.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
import { useState, useContext } from "react";
import AuthFirstSection from "./AuthFirstSection.jsx";
import AuthSecondSection from "./AuthSecondSection.jsx";
import InfoInputStage from "./InfoInput";
import AuthCodeStage from "./AuthCode";
import UserFindStage from "./UserFind";
import { ModalCloseContext } from "@/modal/modal.jsx";

const AUTH_INPUT_PAGE = Symbol("input");
const AUTH_CODE_PAGE = Symbol("code");
const AUTH_FIND_PAGE = Symbol("find");

function AuthModal() {
function AuthModal({ onComplete: onCompleteCallback }) {
const close = useContext(ModalCloseContext);
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [page, setPage] = useState(AUTH_INPUT_PAGE);
function onComplete(isFreshMember) {
onCompleteCallback(isFreshMember);
close();
}

const firstSectionProps = {
name,
setName,
phone,
setPhone,
goNext: () => setPage(AUTH_CODE_PAGE),
goFindUser: () => setPage(AUTH_FIND_PAGE),
};
const secondSectionProps = { name, phone, onComplete };
const findSectionProps = {
onComplete,
goPrev: () => setPage(AUTH_INPUT_PAGE),
};
const secondSectionProps = { name, phone };

const containerClass = `w-[calc(100%-1rem)] max-w-[31.25rem] shadow bg-white relative flex flex-col gap-14`;

return (
<div className="w-[calc(100%-1rem)] max-w-[31.25rem] h-[calc(100svh-2rem)] max-h-[40.625rem] p-6 sm:p-10 py-10 shadow bg-white relative flex flex-col gap-14">
{page === AUTH_CODE_PAGE ? (
<AuthSecondSection {...secondSectionProps} />
) : (
<AuthFirstSection {...firstSectionProps} />
)}
<div className={containerClass}>
{page === AUTH_INPUT_PAGE && <InfoInputStage {...firstSectionProps} />}
{page === AUTH_CODE_PAGE && <AuthCodeStage {...secondSectionProps} />}
{page === AUTH_FIND_PAGE && <UserFindStage {...findSectionProps} />}
<button
className="absolute top-10 right-8"
onClick={close}
Expand Down
42 changes: 0 additions & 42 deletions src/auth/AuthSecondSection.jsx

This file was deleted.

41 changes: 16 additions & 25 deletions src/auth/AuthFirstSection.jsx → src/auth/InfoInput/index.jsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
import { useState } from "react";
import requestAuthCode from "../requestAuthCode.js";
import Input from "@/common/Input.jsx";
import PhoneInput from "@/common/PhoneInput.jsx";
import Button from "@/common/Button.jsx";
import { fetchServer, HTTPError } from "@/common/fetchServer.js";

function AuthFirstSection({ name, setName, phone, setPhone, goNext }) {
function AuthFirstSection({
name,
setName,
phone,
setPhone,
goNext,
goFindUser,
}) {
const [errorMessage, setErrorMessage] = useState("");

const checkboxStyle = `size-4 appearance-none
border border-neutral-300 checked:bg-blue-400 checked:border-0
checked:bg-checked bg-center`;

async function onSubmit(e) {
function onSubmit(e) {
e.preventDefault();
try {
const body = { name, phoneNumber: phone.replace(/\D+/g, "") };
await fetchServer("/api/v1/event-user/send-auth", {
method: "post",
body,
});
setErrorMessage("");
goNext();
} catch (e) {
if (e instanceof HTTPError) {
if (e.status === 400) return setErrorMessage("잘못된 요청 형식입니다.");
if (e.status === 409)
return setErrorMessage("등록된 참여자 정보가 있습니다.");
return setErrorMessage("서버와의 통신 중 오류가 발생했습니다.");
}
console.error(e);
setErrorMessage(
"알 수 없는 오류입니다. 프론트엔드 개발자에게 제보하세요.",
);
}
requestAuthCode(name, phone)
.then(() => goNext())
.catch((error) => setErrorMessage(error.message));
}

return (
<>
<div className="w-full h-[calc(100svh-2rem)] max-h-[40.625rem] p-6 min-[520px]:px-20 py-10 relative flex flex-col gap-14">
<p className="text-body-l font-bold text-neutral-700">
이벤트 응모를 위해
<br />
Expand Down Expand Up @@ -95,12 +85,13 @@ function AuthFirstSection({ name, setName, phone, setPhone, goNext }) {
<button
type="button"
className="absolute top-[calc(100%+1.25rem)] text-detail-l font-medium text-neutral-300"
onClick={goFindUser}
>
이미 정보를 입력하신 적이 있으신가요?
</button>
</div>
</form>
</>
</div>
);
}

Expand Down
Loading

0 comments on commit a8c5d69

Please sign in to comment.