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}로
+ 인증번호를 전송했어요.
+
+
+ >
+ );
+}
+
+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