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] 본인인증 모달 섹션 첫번째 섹션 추가 스타일 추가 #67

Merged
merged 13 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/icons/checked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
107 changes: 107 additions & 0 deletions src/auth/AuthFirstSection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState } from "react";
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 }) {
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) {
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(
"알 수 없는 오류입니다. 프론트엔드 개발자에게 제보하세요.",
);
}
}

return (
<>
<p className="text-body-l font-bold text-neutral-700">
이벤트 응모를 위해
<br />
간단한 정보를 입력해주세요!
</p>
<form
className="flex flex-col flex-grow w-[calc(100%+0.25rem)] -left-0.5 relative gap-4 pb-4 group"
onSubmit={onSubmit}
>
<div className="flex flex-col flex-grow gap-7 px-0.5 relative h-0 overflow-y-auto">
<div className="flex flex-col gap-6">
<label className="flex flex-col gap-3">
<span className="text-body-m font-bold">이름</span>
<Input
text={name}
setText={setName}
placeholder="ex) 홍길동"
required
minLength="2"
/>
</label>
<label className="flex flex-col gap-3">
<span className="text-body-m font-bold">전화번호</span>
<PhoneInput
text={phone}
setText={setPhone}
required
isError={errorMessage !== ""}
/>
<p className="w-full h-4 text-detail-l text-red-500">
{errorMessage}
</p>
</label>
</div>
<div className="flex flex-col gap-4 text-detail-l">
<label className="flex gap-2 items-center">
<input type="checkbox" className={checkboxStyle} required />
<span className="font-bold text-neutral-600">
개인정보 수집 동의(필수)
</span>
<span className="font-medium text-neutral-300">자세히 보기</span>
</label>
<label className="flex gap-2 items-center">
<input type="checkbox" className={checkboxStyle} />
<span className="font-bold text-neutral-600">
마케팅 수신 동의(선택)
</span>
<span className="font-medium text-neutral-300">자세히 보기</span>
</label>
</div>
</div>
<div className="w-full flex justify-center relative">
<Button styleType="filled" type="submit" className="w-36 min-h-14">
인증 요청하기
</Button>
<button
type="button"
className="absolute top-[calc(100%+1.25rem)] text-detail-l font-medium text-neutral-300"
>
이미 정보를 입력하신 적이 있으신가요?
</button>
</div>
</form>
</>
);
}

export default AuthFirstSection;
41 changes: 41 additions & 0 deletions src/auth/AuthModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useState, useContext } from "react";
import AuthFirstSection from "./AuthFirstSection.jsx";
import AuthSecondSection from "./AuthSecondSection.jsx";
import { ModalCloseContext } from "@/modal/modal.jsx";

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

function AuthModal() {
const close = useContext(ModalCloseContext);
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [page, setPage] = useState(AUTH_INPUT_PAGE);
const firstSectionProps = {
name,
setName,
phone,
setPhone,
goNext: () => setPage(AUTH_CODE_PAGE),
};
const secondSectionProps = { name, phone };

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} />
)}
<button
className="absolute top-10 right-8"
onClick={close}
aria-label="닫기"
>
<img src="/icons/close.svg" alt="닫기" width="24" height="24" />
</button>
</div>
);
}

export default AuthModal;
42 changes: 42 additions & 0 deletions src/auth/AuthSecondSection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useState } from "react";
import InputWithTimer from "./InputWithTimer.jsx";
import Button from "@/common/Button.jsx";

function AuthSecondSection({ phone }) {
const [authNumber, setAuthNumber] = useState("");
//const [ errorMessage, setErrorMessage ] = useState("");

const josa = "013678".includes(phone[phone.length - 1]) ? "으" : "";
return (
<>
<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">
<div className="flex flex-col flex-grow justify-center items-center gap-7 px-0.5 relative h-0">
<InputWithTimer
text={authNumber}
setText={setAuthNumber}
required
placeholder="인증번호를 입력해주세요"
/>
<span className="absolute bottom-5 text-detail-l font-bold text-red-400">
{"errorMessage"}
</span>
</div>
<div className="w-full flex justify-center gap-5">
<Button styleType="filled" type="submit" className="w-36 min-h-14">
인증 완료하기
</Button>
<Button styleType="ghost" type="button" className="min-h-14">
재전송
</Button>
</div>
</form>
</>
);
}

export default AuthSecondSection;
14 changes: 14 additions & 0 deletions src/auth/InputWithTimer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Input from "@/common/Input.jsx";

function InputWithTimer({ text, setText, timer, ...otherProps }) {
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}
</span>
</div>
);
}

export default InputWithTimer;
26 changes: 26 additions & 0 deletions src/auth/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { http, HttpResponse } from "msw";

const handlers = [
http.post("/api/v1/event-user/send-auth", async ({ request }) => {
const { name, phoneNumber } = await request.json();
if (phoneNumber === "01019991999")
return HttpResponse.json(
{ error: "중복된 사용자가 있음" },
{ status: 409 },
);
if (name.length < 2)
return HttpResponse.json(
{ error: "응답 내용이 잘못됨" },
{ status: 400 },
);
if (phoneNumber.length >= 12)
return HttpResponse.json(
{ error: "응답 내용이 잘못됨" },
{ status: 400 },
);

return HttpResponse.json({ return: true });
}),
];

export default handlers;
33 changes: 33 additions & 0 deletions src/common/Button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function Button({
styleType,
children,
type = "button",
className,
...otherProps
}) {
const isSubmit = !(type === "reset" || type === "button");

const filledStyle = `bg-black text-white active:bg-neutral-700 active:text-neutral-200
hover:bg-neutral-700 disabled:bg-neutral-600 disabled:text-neutral-400
${isSubmit ? "group-[:invalid]:bg-neutral-600 group-[:invalid]:text-neutral-400" : ""}`;

const ghostStyle = `bg-white text-black shadow-[0_0_0_2px_inset_currentColor]
active:bg-neutral-50 active:text-neutral-400 hover:bg-neutral-50
disabled:text-neutral-200 ${isSubmit ? "group-[:invalid]:text-neutral-200" : ""}`;

const defaultStyle = `px-6 py-4 text-body-m font-bold text-center
disabled:cursor-default ${isSubmit ? "group-[:invalid]:cursor-default" : ""}`;

const typedStyle = styleType === "filled" ? filledStyle : ghostStyle;
return (
<button
className={`${defaultStyle} ${typedStyle} ${className}`}
type={type}
{...otherProps}
>
{children}
</button>
);
}

export default Button;
22 changes: 22 additions & 0 deletions src/common/Input.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
function Input({ text, setText, isError, ...otherProps }) {
const inputboxStyle = `w-full h-14 p-4 bg-neutral-50 rounded text-body-m font-medium
focus:bg-white focus:outline-neutral-800
placeholder:text-neutral-200`;

const errorStyle = `bg-white outline outline-red-500 focus:outline-red-500`;

const errorInputStyle = `invalid:outline invalid:outline-red-500
invalid:focus:outline invalid:focus:outline-red-500`;

return (
<input
className={`${inputboxStyle} ${isError ? errorStyle : ""} ${/^\s*$/.test(text) ? "" : errorInputStyle}`}
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
{...otherProps}
/>
);
}

export default Input;
24 changes: 24 additions & 0 deletions src/common/PhoneInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Input from "./Input.jsx";

function PhoneInput({ text, setText, ...otherProps }) {
function addHyphen(value) {
const plain = value.replace(/\D/g, "");

if (plain.length < 4) return plain;
if (plain.length <= 7) return plain.replace(/^(\d{3})(\d{0,4})$/, "$1-$2");
if (plain.length <= 10)
return plain.replace(/^(\d{3})(\d{3})(\d{0,4})$/, "$1-$2-$3");
return plain.replace(/^(\d{3})(\d{4})(\d{4,})$/, "$1-$2-$3");
}
return (
<Input
text={text}
setText={(value) => setText(addHyphen(value))}
placeholder="-를 제외한 숫자를 입력하세요."
{...otherProps}
maxLength="13"
/>
);
}

export default PhoneInput;
3 changes: 2 additions & 1 deletion src/mock.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { setupWorker } from "msw/browser";
import commentHandler from "./comment/mock.js";
import authHandler from "./auth/mock.js";

// mocking은 기본적으로 각 feature 폴더 내의 mock.js로 정의합니다.
// 새로운 feature의 mocking을 추가하셨으면, mock.js의 setupWorker 내부 함수에 인자를 spread 연산자를 이용해 추가해주세요.
// 예시 : export default setupWorker(...authHandler, ...questionHandler, ...articleHandler);
export default setupWorker(...commentHandler);
export default setupWorker(...commentHandler, ...authHandler);
3 changes: 3 additions & 0 deletions tailwind.redefine.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,8 @@ export default {
},
transitionTimingFunction: {
'in-out-cubic': 'cubic-bezier(0.645, 0.045, 0.355, 1.000)'
},
backgroundImage: {
"checked": "url('/icons/checked.svg')"
}
};
8 changes: 8 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"rewrites": [
{
"source": "/api/:path*",
"destination": "http://13.125.76.97:8080/api/:path*"
}
]
}