diff --git a/public/icons/register@1x.png b/public/icons/register@1x.png
new file mode 100644
index 00000000..6490bb88
Binary files /dev/null and b/public/icons/register@1x.png differ
diff --git a/public/icons/register@2x.png b/public/icons/register@2x.png
new file mode 100644
index 00000000..9a66fdc3
Binary files /dev/null and b/public/icons/register@2x.png differ
diff --git a/public/icons/waiting@1x.png b/public/icons/waiting@1x.png
new file mode 100644
index 00000000..45e551d9
Binary files /dev/null and b/public/icons/waiting@1x.png differ
diff --git a/public/icons/waiting@2x.png b/public/icons/waiting@2x.png
new file mode 100644
index 00000000..9da958b9
Binary files /dev/null and b/public/icons/waiting@2x.png differ
diff --git a/src/auth/InputWithTimer.jsx b/src/auth/AuthCode/InputWithTimer.jsx
similarity index 68%
rename from src/auth/InputWithTimer.jsx
rename to src/auth/AuthCode/InputWithTimer.jsx
index 1c535af8..09014ee8 100644
--- a/src/auth/InputWithTimer.jsx
+++ b/src/auth/AuthCode/InputWithTimer.jsx
@@ -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 (
- {page === AUTH_CODE_PAGE ? (
-
- ) : (
-
- )}
+
+ {page === AUTH_INPUT_PAGE &&
}
+ {page === AUTH_CODE_PAGE &&
}
+ {page === AUTH_FIND_PAGE &&
}
);
}
diff --git a/src/auth/UserFind/index.jsx b/src/auth/UserFind/index.jsx
new file mode 100644
index 00000000..777defe2
--- /dev/null
+++ b/src/auth/UserFind/index.jsx
@@ -0,0 +1,74 @@
+import { useState } from "react";
+import requestLogin from "./requestLogin.js";
+import Input from "@/common/Input.jsx";
+import PhoneInput from "@/common/PhoneInput.jsx";
+import Button from "@/common/Button.jsx";
+
+function AuthFindSection({ goPrev, onComplete }) {
+ const [name, setName] = useState("");
+ const [phone, setPhone] = useState("");
+ const [errorMessage, setErrorMessage] = useState("");
+
+ function onSubmit(e) {
+ e.preventDefault();
+ requestLogin(name, phone)
+ .then(() => {
+ setErrorMessage("");
+ onComplete(false);
+ })
+ .catch((error) => setErrorMessage(error.message));
+ }
+
+ return (
+
+
+ 등록했던 정보를
+
+ 다시 한 번 입력해주세요!
+
+
+
+ );
+}
+
+export default AuthFindSection;
diff --git a/src/auth/UserFind/requestLogin.js b/src/auth/UserFind/requestLogin.js
new file mode 100644
index 00000000..a8d10fcc
--- /dev/null
+++ b/src/auth/UserFind/requestLogin.js
@@ -0,0 +1,21 @@
+import { fetchServer, handleError } from "@/common/fetchServer.js";
+import tokenSaver from "../tokenSaver.js";
+
+async function requestLogin(name, phoneNumber) {
+ try {
+ const body = { name, phoneNumber: phoneNumber.replace(/\D+/g, "") };
+ const { token } = await fetchServer("/api/v1/event-user/login", {
+ method: "post",
+ body,
+ });
+ tokenSaver.set(token);
+ return "";
+ } catch (e) {
+ return handleError({
+ 400: "잘못된 요청 형식입니다.",
+ 404: "등록된 참여자 정보가 없습니다.",
+ })(e);
+ }
+}
+
+export default requestLogin;
diff --git a/src/auth/Welcome/index.jsx b/src/auth/Welcome/index.jsx
new file mode 100644
index 00000000..edff69e9
--- /dev/null
+++ b/src/auth/Welcome/index.jsx
@@ -0,0 +1,39 @@
+import { useContext } from "react";
+import { ModalCloseContext } from "@/modal/modal.jsx";
+import Button from "@/common/Button.jsx";
+
+function WelcomeModal() {
+ const close = useContext(ModalCloseContext);
+
+ return (
+
+
+ 정보가
+
+ 등록되었습니다!
+
+
+
+
+ 확인
+
+
+
+
+
+
+ );
+}
+
+export default WelcomeModal;
diff --git a/src/auth/mock.js b/src/auth/mock.js
index 425b25bd..778d58f1 100644
--- a/src/auth/mock.js
+++ b/src/auth/mock.js
@@ -1,5 +1,11 @@
import { http, HttpResponse } from "msw";
+function isValidInput(name, phoneNumber) {
+ return (
+ name.length >= 2 && phoneNumber.length < 12 && phoneNumber.startsWith("01")
+ );
+}
+
const handlers = [
http.post("/api/v1/event-user/send-auth", async ({ request }) => {
const { name, phoneNumber } = await request.json();
@@ -8,18 +14,45 @@ const handlers = [
{ error: "중복된 사용자가 있음" },
{ status: 409 },
);
- if (name.length < 2)
+ if (!isValidInput(name, phoneNumber))
return HttpResponse.json(
{ error: "응답 내용이 잘못됨" },
{ status: 400 },
);
- if (phoneNumber.length >= 12)
+
+ return HttpResponse.json({ return: true });
+ }),
+
+ http.post(
+ "/api/v1/event-user/check-auth/:eventFrameId",
+ async ({ request }) => {
+ const { name, phoneNumber, authCode } = await request.json();
+
+ if (!isValidInput(name, phoneNumber))
+ return HttpResponse.json(
+ { error: "응답 내용이 잘못됨" },
+ { status: 400 },
+ );
+ if (authCode !== "726679")
+ return HttpResponse.json(
+ { error: "인증번호 일치 안 함" },
+ { status: 401 },
+ );
+ return HttpResponse.json({ token: "test_token" });
+ },
+ ),
+
+ http.post("/api/v1/event-user/login", async ({ request }) => {
+ const { name, phoneNumber } = await request.json();
+
+ if (!isValidInput(name, phoneNumber))
return HttpResponse.json(
{ error: "응답 내용이 잘못됨" },
{ status: 400 },
);
-
- return HttpResponse.json({ return: true });
+ if (name !== "오렌지" || phoneNumber !== "01019991999")
+ return HttpResponse.json({ error: "사용자 없음" }, { status: 404 });
+ return HttpResponse.json({ token: "test_token" });
}),
];
diff --git a/src/auth/requestAuthCode.js b/src/auth/requestAuthCode.js
new file mode 100644
index 00000000..af1cf2ca
--- /dev/null
+++ b/src/auth/requestAuthCode.js
@@ -0,0 +1,19 @@
+import { fetchServer, handleError } from "@/common/fetchServer.js";
+
+async function requestAuthCode(name, phoneNumber) {
+ try {
+ const body = { name, phoneNumber: phoneNumber.replace(/\D+/g, "") };
+ await fetchServer("/api/v1/event-user/send-auth", {
+ method: "post",
+ body,
+ });
+ return "";
+ } catch (e) {
+ return handleError({
+ 400: "잘못된 요청 형식입니다.",
+ 409: "등록된 참여자 정보가 있습니다.",
+ })(e);
+ }
+}
+
+export default requestAuthCode;
diff --git a/src/auth/tokenSaver.js b/src/auth/tokenSaver.js
new file mode 100644
index 00000000..4bf00373
--- /dev/null
+++ b/src/auth/tokenSaver.js
@@ -0,0 +1,35 @@
+import { TOKEN_ID } from "@/common/constants.js";
+
+class TokenSaver {
+ initialized = false;
+ token = null;
+ init() {
+ if (typeof window === "undefined") return;
+ this.token = localStorage.getItem(TOKEN_ID) ?? null;
+ this.initialized = true;
+ }
+ get() {
+ if (this.initialized) return this.token;
+ this.init();
+ return this.token;
+ }
+ set(token) {
+ this.token = token;
+ if (typeof window !== "undefined") localStorage.setItem(TOKEN_ID, token);
+ this.initialzed = true;
+ }
+ has() {
+ if (this.initialized) return this.token !== null;
+ this.init();
+ return this.token !== null;
+ }
+ remove() {
+ this.token = null;
+ if (typeof window !== "undefined") localStorage.removeItem(TOKEN_ID);
+ this.initialzed = true;
+ }
+}
+
+const tokenSaver = new TokenSaver();
+
+export default tokenSaver;
diff --git a/src/comment/commentForm/index.jsx b/src/comment/commentForm/index.jsx
new file mode 100644
index 00000000..1c0a2a10
--- /dev/null
+++ b/src/comment/commentForm/index.jsx
@@ -0,0 +1,78 @@
+import { useState } from "react";
+import CommentSuccessModal from "../modals/CommentSuccessModal.jsx";
+import CommentNegativeModal from "../modals/CommentNegativeModal.jsx";
+import CommentNoUserModal from "../modals/CommentNoUserModal.jsx";
+import NoServerModal from "@/common/NoServerModal.jsx";
+
+import Button from "@/common/Button.jsx";
+import { fetchServer, handleError } from "@/common/fetchServer.js";
+import { EVENT_ID } from "@/common/constants.js";
+import openModal from "@/modal/openModal.js";
+
+const submitCommentErrorHandle = {
+ 400: "negative",
+ 401: "unauthorized",
+ 409: "하루에 1번만 기대평을 등록할 수 있습니다.",
+ offline: "offline",
+};
+
+function CommentForm() {
+ const [errorMessage, setErrorMessage] = useState("");
+
+ const successModal =
;
+ const negativeModal =
;
+ const noUserModal =
;
+ const noServerModal =
;
+
+ async function onSubmit(e) {
+ e.preventDefault();
+ const commentDom = e.target.elements.comment;
+ const content = commentDom.value;
+ if (content.length < 10 || content.length > 50) return;
+
+ commentDom.value = "";
+ setErrorMessage("");
+ try {
+ await fetchServer(`/api/v1/comment/${EVENT_ID}`, {
+ method: "post",
+ body: { content },
+ }).catch(handleError(submitCommentErrorHandle));
+ openModal(successModal);
+ } catch (e) {
+ switch (e.message) {
+ case submitCommentErrorHandle[400]:
+ return openModal(negativeModal);
+ case submitCommentErrorHandle[401]:
+ return openModal(noUserModal);
+ case submitCommentErrorHandle["offline"]:
+ return openModal(noServerModal);
+ default:
+ setErrorMessage(e.message);
+ }
+ }
+ }
+
+ return (
+
+ );
+}
+
+export default CommentForm;
diff --git a/src/comment/index.jsx b/src/comment/index.jsx
index 685abab3..d7c384d1 100644
--- a/src/comment/index.jsx
+++ b/src/comment/index.jsx
@@ -1,7 +1,8 @@
-import CommentCarousel from "./commentCarousel";
-import decoration from "./assets/decoration.svg";
import { useRef } from "react";
+import CommentCarousel from "./commentCarousel";
+import CommentForm from "./commentForm";
import useSectionInitialize from "../scroll/useSectionInitialize";
+import decoration from "./assets/decoration.svg";
function CommentSection() {
const SECTION_IDX = 3;
@@ -34,6 +35,7 @@ function CommentSection() {
+