diff --git a/public/icons/checked.svg b/public/icons/checked.svg new file mode 100644 index 00000000..0daca305 --- /dev/null +++ b/public/icons/checked.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/close.svg b/public/icons/close.svg new file mode 100644 index 00000000..4720abe8 --- /dev/null +++ b/public/icons/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/auth/AuthFirstSection.jsx b/src/auth/AuthFirstSection.jsx new file mode 100644 index 00000000..37063f62 --- /dev/null +++ b/src/auth/AuthFirstSection.jsx @@ -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 ( + <> +

+ 이벤트 응모를 위해 +
+ 간단한 정보를 입력해주세요! +

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + ); +} + +export default AuthFirstSection; diff --git a/src/auth/AuthModal.jsx b/src/auth/AuthModal.jsx new file mode 100644 index 00000000..4bbfde74 --- /dev/null +++ b/src/auth/AuthModal.jsx @@ -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 ( +
+ {page === AUTH_CODE_PAGE ? ( + + ) : ( + + )} + +
+ ); +} + +export default AuthModal; diff --git a/src/auth/AuthSecondSection.jsx b/src/auth/AuthSecondSection.jsx new file mode 100644 index 00000000..03a14408 --- /dev/null +++ b/src/auth/AuthSecondSection.jsx @@ -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 ( + <> +

+ {phone} + {josa}로
+ 인증번호를 전송했어요. +

+
+
+ + + {"errorMessage"} + +
+
+ + +
+
+ + ); +} + +export default AuthSecondSection; diff --git a/src/auth/InputWithTimer.jsx b/src/auth/InputWithTimer.jsx new file mode 100644 index 00000000..1c535af8 --- /dev/null +++ b/src/auth/InputWithTimer.jsx @@ -0,0 +1,14 @@ +import Input from "@/common/Input.jsx"; + +function InputWithTimer({ text, setText, timer, ...otherProps }) { + return ( +
+ + + {timer} + +
+ ); +} + +export default InputWithTimer; diff --git a/src/auth/mock.js b/src/auth/mock.js new file mode 100644 index 00000000..425b25bd --- /dev/null +++ b/src/auth/mock.js @@ -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; diff --git a/src/common/Button.jsx b/src/common/Button.jsx new file mode 100644 index 00000000..9cc56beb --- /dev/null +++ b/src/common/Button.jsx @@ -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 ( + + ); +} + +export default Button; diff --git a/src/common/Input.jsx b/src/common/Input.jsx new file mode 100644 index 00000000..0869f5d0 --- /dev/null +++ b/src/common/Input.jsx @@ -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 ( + setText(e.target.value)} + {...otherProps} + /> + ); +} + +export default Input; diff --git a/src/common/PhoneInput.jsx b/src/common/PhoneInput.jsx new file mode 100644 index 00000000..67dcd29b --- /dev/null +++ b/src/common/PhoneInput.jsx @@ -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 ( + setText(addHyphen(value))} + placeholder="-를 제외한 숫자를 입력하세요." + {...otherProps} + maxLength="13" + /> + ); +} + +export default PhoneInput; diff --git a/src/mock.js b/src/mock.js index e89e74d6..88c1b59f 100644 --- a/src/mock.js +++ b/src/mock.js @@ -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); diff --git a/tailwind.redefine.js b/tailwind.redefine.js index 0b9b72e3..f430caf3 100644 --- a/tailwind.redefine.js +++ b/tailwind.redefine.js @@ -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')" } }; diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..b0803975 --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/api/:path*", + "destination": "http://13.125.76.97:8080/api/:path*" + } + ] +} \ No newline at end of file