diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000..e02130ad76
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..7d01468017
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+[.DS_Store]
+.Ds_Store
diff --git a/landing/faq.html b/landing/faq.html
new file mode 100644
index 0000000000..6612ae1039
--- /dev/null
+++ b/landing/faq.html
@@ -0,0 +1,7 @@
+
+
+
+
+ FAQ
+
+
diff --git a/landing/folder.html b/landing/folder.html
new file mode 100644
index 0000000000..1fc9c127cb
--- /dev/null
+++ b/landing/folder.html
@@ -0,0 +1,7 @@
+
+
+
+
+ 폴더
+
+
diff --git a/landing/images/eye-off.svg b/landing/images/eye-off.svg
new file mode 100644
index 0000000000..bec50d66f7
--- /dev/null
+++ b/landing/images/eye-off.svg
@@ -0,0 +1,6 @@
+
diff --git a/landing/images/eye-on.svg b/landing/images/eye-on.svg
new file mode 100644
index 0000000000..61afee8981
--- /dev/null
+++ b/landing/images/eye-on.svg
@@ -0,0 +1,6 @@
+
diff --git a/landing/images/facebook.svg b/landing/images/facebook.svg
new file mode 100644
index 0000000000..b9c9d49397
--- /dev/null
+++ b/landing/images/facebook.svg
@@ -0,0 +1,3 @@
+
diff --git a/landing/images/google.png b/landing/images/google.png
new file mode 100644
index 0000000000..e49ff34a32
Binary files /dev/null and b/landing/images/google.png differ
diff --git a/landing/images/hero.png b/landing/images/hero.png
new file mode 100644
index 0000000000..11dc528fbf
Binary files /dev/null and b/landing/images/hero.png differ
diff --git a/landing/images/image1.png b/landing/images/image1.png
new file mode 100644
index 0000000000..f6355852b0
Binary files /dev/null and b/landing/images/image1.png differ
diff --git a/landing/images/image2.png b/landing/images/image2.png
new file mode 100644
index 0000000000..b9a3c674af
Binary files /dev/null and b/landing/images/image2.png differ
diff --git a/landing/images/image3.png b/landing/images/image3.png
new file mode 100644
index 0000000000..986a39679f
Binary files /dev/null and b/landing/images/image3.png differ
diff --git a/landing/images/image4.png b/landing/images/image4.png
new file mode 100644
index 0000000000..762bdc960e
Binary files /dev/null and b/landing/images/image4.png differ
diff --git a/landing/images/instagram.svg b/landing/images/instagram.svg
new file mode 100644
index 0000000000..0b9337b075
--- /dev/null
+++ b/landing/images/instagram.svg
@@ -0,0 +1,3 @@
+
diff --git a/landing/images/kakao.svg b/landing/images/kakao.svg
new file mode 100644
index 0000000000..778fe94614
--- /dev/null
+++ b/landing/images/kakao.svg
@@ -0,0 +1,13 @@
+
diff --git a/landing/images/logo.svg b/landing/images/logo.svg
new file mode 100644
index 0000000000..2820220902
--- /dev/null
+++ b/landing/images/logo.svg
@@ -0,0 +1,11 @@
+
diff --git a/landing/images/twitter.svg b/landing/images/twitter.svg
new file mode 100644
index 0000000000..14a6069a18
--- /dev/null
+++ b/landing/images/twitter.svg
@@ -0,0 +1,3 @@
+
diff --git a/landing/images/youtube.svg b/landing/images/youtube.svg
new file mode 100644
index 0000000000..28ed0e8ba6
--- /dev/null
+++ b/landing/images/youtube.svg
@@ -0,0 +1,10 @@
+
diff --git a/landing/index.html b/landing/index.html
new file mode 100644
index 0000000000..e8019fb114
--- /dev/null
+++ b/landing/index.html
@@ -0,0 +1,131 @@
+
+
+
+
+ Linkbrary
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 원하는 링크를 저장하세요
+
+
+ 나중에 읽고 싶은 글, 다시 보고 싶은 영상,
+
+ 사고 싶은 옷, 기억하고 싶은 모든 것을
+
+ 한 공간에 저장하세요.
+
+
+
+
+
+ 링크를 폴더로
+ 관리하세요
+
+
+ 나만의 폴더를 무제한으로 만들고
+
+ 다양하게 활용할 수 있습니다.
+
+
+
+
+
+ 저장한 링크를
+ 공유해 보세요
+
+
+ 여러 링크를 폴더에 담고 공유할 수 있습니다. 가족, 친구, 동료들에게 쉽고 빠르게 링크를
+ 공유해 보세요.
+
+
+
+
+
+ 저장한 링크를
+ 검색해 보세요
+
+ 중요한 정보들을 검색으로 쉽게 찾아보세요.
+
+
+
+
+
+
diff --git a/landing/privacy.html b/landing/privacy.html
new file mode 100644
index 0000000000..abf71aaaf0
--- /dev/null
+++ b/landing/privacy.html
@@ -0,0 +1,7 @@
+
+
+
+
+ Privacy
+
+
diff --git a/landing/signin.html b/landing/signin.html
new file mode 100644
index 0000000000..6708a38073
--- /dev/null
+++ b/landing/signin.html
@@ -0,0 +1,59 @@
+
+
+
+
+ 로그인
+
+
+
+
+
+
+
+
+
+
diff --git a/landing/signup.html b/landing/signup.html
new file mode 100644
index 0000000000..595e8f5f4c
--- /dev/null
+++ b/landing/signup.html
@@ -0,0 +1,67 @@
+
+
+
+
+ 회원가입
+
+
+
+
+
+
+
+
+
+
diff --git a/landing/src/colors.css b/landing/src/colors.css
new file mode 100644
index 0000000000..e139672472
--- /dev/null
+++ b/landing/src/colors.css
@@ -0,0 +1,13 @@
+:root {
+ --primary: #6d6afe;
+ --red: #ff5b56;
+ --black: #111322;
+ --white: #ffffff;
+
+ --gray100: #373740;
+ --gray60: #9fa6b2;
+ --gray20: #ccd5e3;
+ --gray10: #e7effb;
+
+ --background: #f0f6ff;
+}
diff --git a/landing/src/global.css b/landing/src/global.css
new file mode 100644
index 0000000000..f20ab4ae71
--- /dev/null
+++ b/landing/src/global.css
@@ -0,0 +1,2 @@
+@import "./colors.css";
+@import "./reset.css";
diff --git a/landing/src/landing.css b/landing/src/landing.css
new file mode 100644
index 0000000000..5521d673c5
--- /dev/null
+++ b/landing/src/landing.css
@@ -0,0 +1,337 @@
+@import "./global.css";
+
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+nav {
+ display: flex;
+ justify-content: center;
+ position: sticky;
+ top: 0;
+ width: 100%;
+ background-color: #edf7ff;
+}
+
+.gnb {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ height: 6.3rem;
+ padding: 0 3.2rem;
+
+ @media (min-width: 768px) {
+ height: 9.4rem;
+ max-width: 86.3rem;
+ }
+
+ @media (min-width: 1200px) {
+ height: 9.4rem;
+ max-width: 192rem;
+ padding: 0 20rem;
+ }
+}
+
+.logo {
+ height: 1.6rem;
+
+ @media (min-width: 768px) {
+ height: 2.4rem;
+ }
+}
+
+header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ background-color: #edf7ff;
+}
+
+.hero-header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ row-gap: 2.4rem;
+ padding: 2.8rem 3.2rem 0;
+
+ @media (min-width: 768px) {
+ padding: 3.9rem 0 0;
+ row-gap: 4rem;
+ }
+
+ @media (min-width: 1200px) {
+ padding: 7rem 0 0;
+ }
+}
+
+.cta {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 3.7rem;
+ cursor: pointer;
+ background-image: linear-gradient(135deg, var(--primary) 0%, #6ae3fe 100%);
+ border-radius: 0.8rem;
+ color: #f5f5f5;
+ font-size: 1.4rem;
+ font-weight: 600;
+
+ @media (min-width: 768px) {
+ height: 5.4rem;
+ border-radius: 0.8rem;
+ font-size: 1.8rem;
+ }
+}
+
+.cta-short {
+ width: 8rem;
+
+ @media (min-width: 768px) {
+ width: 12.8rem;
+ }
+}
+
+.cta-long {
+ width: 20rem;
+
+ @media (min-width: 768px) {
+ width: 35rem;
+ }
+}
+
+.slogan {
+ width: 24rem;
+ text-align: center;
+ font-size: 3.2rem;
+ line-height: 131.25%;
+ font-weight: 700;
+
+ @media (min-width: 768px) {
+ width: 48rem;
+ font-size: 6.4rem;
+ line-height: 125%;
+ }
+
+ @media (min-width: 1200px) {
+ width: 80rem;
+ }
+}
+
+.slogan-gradient {
+ background-image: linear-gradient(119deg, var(--primary) 0%, #ff9f9f 100%);
+}
+
+.background-clip-text {
+ background-clip: text;
+ -webkit-background-clip: text;
+ color: transparent;
+}
+
+.hero-image {
+ width: 100%;
+ height: fit-content;
+ @media (min-width: 768px) {
+ width: 69.8rem;
+ height: 34.3rem;
+ }
+
+ @media (min-width: 1200px) {
+ width: 120rem;
+ height: 59rem;
+ }
+}
+
+article {
+ padding-bottom: 4rem;
+
+ @media (min-width: 768px) {
+ padding-top: 3rem;
+ padding-bottom: 12rem;
+ }
+
+ @media (min-width: 1200px) {
+ padding-top: 7rem;
+ }
+}
+
+section {
+ display: grid;
+ justify-content: center;
+ row-gap: 1.6rem;
+ width: 100%;
+ height: fit-content;
+ padding: 4rem 3.2rem;
+
+ @media (min-width: 768px) {
+ height: 41.5rem;
+ column-gap: 5.1rem;
+ row-gap: 1rem;
+ padding: 5rem 0;
+ }
+
+ @media (min-width: 1200px) {
+ height: 55rem;
+ column-gap: 15.7rem;
+ }
+}
+
+section:nth-of-type(odd) {
+ grid-template-areas:
+ "title"
+ "image"
+ "description";
+
+ @media (min-width: 768px) {
+ grid-template-areas:
+ ". image"
+ "title image"
+ "description image"
+ ". image";
+ grid-template-columns: 26.2rem 38.5rem;
+ }
+
+ @media (min-width: 1200px) {
+ grid-template-columns: 29.1rem 55rem;
+ }
+}
+
+section:nth-of-type(even) {
+ grid-template:
+ "title"
+ "image"
+ "description";
+
+ @media (min-width: 768px) {
+ grid-template-areas:
+ "image ."
+ "image title"
+ "image description"
+ "image .";
+ grid-template-columns: 38.5rem 26.2rem;
+ }
+
+ @media (min-width: 1200px) {
+ grid-template-columns: 55rem 29.1rem;
+ }
+}
+
+.title {
+ grid-area: title;
+ font-weight: 700;
+ letter-spacing: -0.03rem;
+ font-size: 2.4rem;
+
+ @media (min-width: 768px) {
+ font-size: 4.8rem;
+ line-height: 5.8rem;
+ }
+}
+
+.title-1-gradient {
+ background-image: linear-gradient(117deg, #fe8a8a 2.29%, #a4ceff 100%);
+}
+.title-2-gradient {
+ background-image: linear-gradient(304deg, #6fbaff 0%, #ffd88b 100%);
+}
+.title-3-gradient {
+ background-image: linear-gradient(133deg, #2945c7 0%, #dbe1f8 100%);
+}
+.title-4-gradient {
+ background-image: linear-gradient(310deg, #fe578f 0%, #68e8f9 100%);
+}
+
+.description {
+ grid-area: description;
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: #6b6b6b;
+ line-height: 150%;
+
+ @media (min-width: 768px) {
+ font-size: 1.6rem;
+ }
+}
+
+.line-break-tablet-desktop {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: inline;
+ }
+}
+
+.content-image {
+ grid-area: image;
+ width: 100%;
+ padding-top: 0.4rem;
+
+ @media (min-width: 768px) {
+ width: 38.5rem;
+ height: 31.5rem;
+ padding-top: 0;
+ }
+
+ @media (min-width: 1200px) {
+ width: 55rem;
+ height: 45rem;
+ }
+}
+
+footer {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ height: 16rem;
+ background-color: var(--black);
+}
+
+.footer-box {
+ display: grid;
+ justify-content: space-between;
+ grid-template:
+ "footer-links sns"
+ ". ." 1fr
+ "copyright .";
+ width: 100%;
+ padding: 3.2rem;
+
+ @media (min-width: 768px) {
+ display: flex;
+ justify-content: space-between;
+ grid-template: "copyright footer-links sns";
+ height: fit-content;
+ max-width: 192rem;
+ padding: 3.2rem 10.4rem 0;
+ }
+}
+
+.copyright {
+ grid-area: copyright;
+ color: #676767;
+ font-family: Arial;
+ font-size: 1.6rem;
+}
+
+.footer-links {
+ grid-area: footer-links;
+ display: flex;
+ column-gap: 3rem;
+ padding-right: 1.8rem;
+}
+
+.footer-link {
+ color: #cfcfcf;
+ font-family: Arial;
+ font-size: 1.6rem;
+}
+
+.sns {
+ grid-area: sns;
+ display: flex;
+ column-gap: 1.2rem;
+ height: 2rem;
+}
diff --git a/landing/src/reset.css b/landing/src/reset.css
new file mode 100644
index 0000000000..830b517e4a
--- /dev/null
+++ b/landing/src/reset.css
@@ -0,0 +1,30 @@
+/* user agent stylesheet 초기화 */
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ font-family: "Pretendard";
+ word-break: keep-all;
+}
+
+html,
+body {
+ font-size: 62.5%;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+input:focus {
+ outline: none;
+}
+
+button {
+ border: none;
+ padding: unset;
+ background-color: unset;
+ cursor: pointer;
+}
diff --git a/landing/src/sign.css b/landing/src/sign.css
new file mode 100644
index 0000000000..25caf712b0
--- /dev/null
+++ b/landing/src/sign.css
@@ -0,0 +1,185 @@
+@import "./global.css";
+
+body {
+ background-color: var(--background);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ row-gap: 3rem;
+ padding: 12rem 3.2rem 0;
+
+ @media (min-width: 768px) {
+ padding: 20rem 0 0;
+ }
+
+ @media (min-width: 1200px) {
+ padding: 23.8rem 0 0;
+ }
+}
+
+header {
+ display: flex;
+ flex-direction: column;
+ row-gap: 1.6rem;
+}
+
+.logo-link {
+ display: flex;
+ justify-content: center;
+}
+
+.header-logo {
+ height: 3.8rem;
+}
+
+.header-message {
+ display: flex;
+ column-gap: 0.8rem;
+ font-size: 1.6rem;
+ font-weight: 400;
+ line-height: 150%;
+}
+
+.header-link {
+ height: fit-content;
+ font-size: 1.6rem;
+ font-weight: 600;
+ line-height: normal;
+ color: var(--primary);
+ border-bottom: 0.1rem solid var(--primary);
+}
+
+.sign-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ row-gap: 3.2rem;
+ width: 100%;
+ max-width: 40rem;
+
+ @media (min-width: 768px) {
+ width: 40rem;
+ }
+}
+
+.sign-form {
+ display: flex;
+ flex-direction: column;
+ row-gap: 3rem;
+ width: 100%;
+}
+
+.sign-inputs {
+ display: flex;
+ flex-direction: column;
+ row-gap: 2.4rem;
+}
+
+.sign-input-box {
+ display: flex;
+ flex-direction: column;
+ row-gap: 1.2rem;
+}
+
+.sign-password {
+ position: relative;
+}
+
+.sign-input-label {
+ font-size: 1.4rem;
+ font-weight: 400;
+}
+
+.sign-input {
+ padding: 1.8rem 1.5rem;
+ border-radius: 0.8rem;
+ border: 0.1rem solid var(--gray20);
+ font-size: 1.6rem;
+ line-height: 150%;
+}
+
+.sign-input:focus {
+ border-color: var(--primary);
+}
+
+.sign-input.sign-input-error {
+ border-color: var(--red);
+}
+
+.sign-input.sign-input-error:focus {
+ border-color: var(--red);
+}
+
+.error-message {
+ display: none;
+ margin-top: -0.4rem;
+ font-size: 1.4rem;
+ font-weight: 400;
+ color: var(--red);
+}
+
+.error-message.error-message-on {
+ display: inline-block;
+}
+
+.eye-button {
+ position: absolute;
+ top: 5.1rem;
+ right: 1.5rem;
+ width: 1.6rem;
+ height: 1.6rem;
+}
+
+.cta {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 5.4rem;
+ background-image: linear-gradient(135deg, var(--primary) 0%, #6ae3fe 100%);
+ border-radius: 0.8rem;
+ color: #f5f5f5;
+ font-size: 1.8rem;
+ font-weight: 600;
+}
+
+.sns-box {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 1.2rem 2.4rem;
+ border-radius: 0.8rem;
+ border: 0.1rem solid var(--gray20);
+ background-color: var(--gray10);
+}
+
+.sns-links {
+ display: flex;
+ column-gap: 1.6rem;
+}
+
+.sns-link {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 4.2rem;
+ height: 4.2rem;
+ border-radius: 50%;
+}
+
+.sns-text {
+ font-size: 1.4rem;
+ font-weight: 400;
+ color: var(--gray100);
+}
+
+.google-link {
+ background-color: var(--white);
+ border: 0.1rem solid #d3d4dd;
+}
+
+.kakao-link {
+ padding-top: 0.2rem;
+ background-color: #f5e14b;
+}
diff --git a/landing/src/signin.js b/landing/src/signin.js
new file mode 100644
index 0000000000..77c2c3197c
--- /dev/null
+++ b/landing/src/signin.js
@@ -0,0 +1,96 @@
+import {
+ setInputError,
+ removeInputError,
+ isEmailValid,
+ togglePassword,
+ TEST_USER,
+} from "./utils.js";
+
+const emailInput = document.querySelector("#email");
+const emailErrorMessage = document.querySelector("#email-error-message");
+emailInput.addEventListener("focusout", (event) =>
+ validateEmailInput(event.target.value)
+);
+function validateEmailInput(email) {
+ if (email === "") {
+ setInputError(
+ { input: emailInput, errorMessage: emailErrorMessage },
+ "이메일을 입력해주세요."
+ );
+ return;
+ }
+ if (!isEmailValid(email)) {
+ setInputError(
+ { input: emailInput, errorMessage: emailErrorMessage },
+ "올바른 이메일 주소가 아닙니다."
+ );
+ return;
+ }
+ removeInputError({ input: emailInput, errorMessage: emailErrorMessage });
+}
+
+const passwordInput = document.querySelector("#password");
+const passwordErrorMessage = document.querySelector("#password-error-message");
+passwordInput.addEventListener("focusout", (event) =>
+ validatePasswordInput(event.target.value)
+);
+function validatePasswordInput(password) {
+ if (password === "") {
+ setInputError(
+ { input: passwordInput, errorMessage: passwordErrorMessage },
+ "비밀번호를 입력해주세요."
+ );
+ return;
+ }
+ removeInputError({
+ input: passwordInput,
+ errorMessage: passwordErrorMessage,
+ });
+}
+
+const passwordToggleButton = document.querySelector("#password-toggle");
+passwordToggleButton.addEventListener("click", () =>
+ togglePassword(passwordInput, passwordToggleButton)
+);
+
+const signForm = document.querySelector("#form");
+signForm.addEventListener("submit", submitForm);
+async function submitForm(event) {
+ event.preventDefault();
+
+ const isTestUser =
+ emailInput.value === TEST_USER.email &&
+ passwordInput.value === TEST_USER.password;
+
+ if (isTestUser) {
+ try {
+ const response = await fetch("https://bootcai.codeit.kr/api/sign-in", {
+ headers: { "Content-Type": "application/json" },
+ method: "POST",
+ body: JSON.stringify({
+ email: TEST_USER.email,
+ password: TEST_USER.password,
+ }),
+ });
+
+ const data = await response.json();
+ localStorage.setItem("accessToken", data.accessToken);
+
+ if (localStorage.getItem("accessToken")) {
+ location.href = "/folder";
+ }
+ } catch (error) {
+ console.error("Error:", error);
+ }
+
+ return;
+ }
+ setInputError(
+ { input: emailInput, errorMessage: emailErrorMessage },
+ "이메일을 확인해주세요."
+ );
+ setInputError(
+ { input: passwordInput, errorMessage: passwordErrorMessage },
+ "비밀번호를 확인해주세요."
+ );
+}
diff --git a/landing/src/signup.js b/landing/src/signup.js
new file mode 100644
index 0000000000..6e627f915f
--- /dev/null
+++ b/landing/src/signup.js
@@ -0,0 +1,137 @@
+import {
+ setInputError,
+ removeInputError,
+ isEmailValid,
+ isPasswordValid,
+ togglePassword,
+ TEST_USER,
+} from "./utils.js";
+
+const emailInput = document.querySelector("#email");
+const emailErrorMessage = document.querySelector("#email-error-message");
+emailInput.addEventListener("focusout", (event) =>
+ validateEmailInput(event.target.value)
+);
+async function validateEmailInput(email) {
+ if (email === "") {
+ setInputError(
+ { input: emailInput, errorMessage: emailErrorMessage },
+ "이메일을 입력해주세요."
+ );
+ return false;
+ }
+ if (!isEmailValid(email)) {
+ setInputError(
+ { input: emailInput, errorMessage: emailErrorMessage },
+ "올바른 이메일 주소가 아닙니다."
+ );
+ return false;
+ }
+ try {
+ const response = await fetch("/api/check-email", {
+ method: "POST",
+ body: JSON.stringify({ email: email }),
+ });
+ const data = await response.json();
+ if (data.isEmailTaken) {
+ setInputError(
+ { input: emailInput, errorMessage: emailErrorMessage },
+ "이미 사용 중인 이메일입니다."
+ );
+ return false;
+ }
+ } catch (error) {
+ console.error("Error:", error);
+ return false;
+ }
+ removeInputError({ input: emailInput, errorMessage: emailErrorMessage });
+ return true;
+}
+
+const passwordInput = document.querySelector("#password");
+const passwordErrorMessage = document.querySelector("#password-error-message");
+passwordInput.addEventListener("focusout", (event) =>
+ validatePasswordInput(event.target.value)
+);
+
+function validatePasswordInput(password) {
+ if (password === "" || !isPasswordValid(password)) {
+ setInputError(
+ { input: passwordInput, errorMessage: passwordErrorMessage },
+ "비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요."
+ );
+ return false;
+ }
+ removeInputError({
+ input: passwordInput,
+ errorMessage: passwordErrorMessage,
+ });
+ return true;
+}
+
+const confirmPasswordInput = document.querySelector("#confirm-password");
+const confirmPasswordErrorMessage = document.querySelector(
+ "#confirm-password-error-message"
+);
+confirmPasswordInput.addEventListener("focusout", (event) =>
+ validateConfirmPasswordInput(event.target.value)
+);
+function validateConfirmPasswordInput(confirmPassword) {
+ if (confirmPassword === "" || !isPasswordValid(confirmPassword)) {
+ setInputError(
+ {
+ input: confirmPasswordInput,
+ errorMessage: confirmPasswordErrorMessage,
+ },
+ "비밀번호는 영문, 숫자 조합 8자 이상 입력해 주세요."
+ );
+ return false;
+ }
+ if (passwordInput.value !== confirmPassword) {
+ setInputError(
+ {
+ input: confirmPasswordInput,
+ errorMessage: confirmPasswordErrorMessage,
+ },
+ "비밀번호가 일치하지 않아요."
+ );
+ return false;
+ }
+ removeInputError({
+ input: confirmPasswordInput,
+ errorMessage: confirmPasswordErrorMessage,
+ });
+ return true;
+}
+
+const passwordToggleButton = document.querySelector("#password-toggle");
+passwordToggleButton.addEventListener("click", () =>
+ togglePassword(passwordInput, passwordToggleButton)
+);
+
+const confirmPasswordToggleButton = document.querySelector(
+ "#confirm-password-toggle"
+);
+confirmPasswordToggleButton.addEventListener("click", () =>
+ togglePassword(confirmPasswordInput, confirmPasswordToggleButton)
+);
+
+const signForm = document.querySelector("#form");
+signForm.addEventListener("submit", submitForm);
+function submitForm(event) {
+ event.preventDefault();
+
+ const isEmailInputValid = validateEmailInput(emailInput.value);
+ const isPasswordInputValid = validatePasswordInput(passwordInput.value);
+ const isConfirmPasswordInputValid = validateConfirmPasswordInput(
+ confirmPasswordInput.value
+ );
+
+ if (
+ isEmailInputValid &&
+ isPasswordInputValid &&
+ isConfirmPasswordInputValid
+ ) {
+ location.href = "/folder";
+ }
+}
diff --git a/landing/src/utils.js b/landing/src/utils.js
new file mode 100644
index 0000000000..13abe476c7
--- /dev/null
+++ b/landing/src/utils.js
@@ -0,0 +1,40 @@
+const SIGN_INPUT_ERROR_CLASSNAME = "sign-input-error";
+const ERROR_MESSAGE_CLASSNAME = "error-message-on";
+const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g;
+
+export function setInputError(elements, message) {
+ elements.input.className += ` ${SIGN_INPUT_ERROR_CLASSNAME}`;
+ elements.errorMessage.className += ` ${ERROR_MESSAGE_CLASSNAME}`;
+ elements.errorMessage.textContent = message;
+}
+
+export function removeInputError(elements) {
+ elements.input.classList.remove(SIGN_INPUT_ERROR_CLASSNAME);
+ elements.errorMessage.classList.remove(ERROR_MESSAGE_CLASSNAME);
+ elements.errorMessage.textContent = "";
+}
+
+export function isEmailValid(email) {
+ return new RegExp(EMAIL_REGEX).test(email);
+}
+
+export function isPasswordValid(password) {
+ const isEightLettersOrMore = password.length >= 8;
+ const hasNumberAndCharacter = password.match(/[0-9]/g) && password.match(/[a-zA-Z]/gi);
+ return isEightLettersOrMore && hasNumberAndCharacter;
+}
+
+export function togglePassword(input, toggleButton) {
+ if (input.getAttribute("type") === "password") {
+ input.setAttribute("type", "text");
+ toggleButton.getElementsByTagName("img")[0].setAttribute("src", "./images/eye-on.svg");
+ return;
+ }
+ input.setAttribute("type", "password");
+ toggleButton.getElementsByTagName("img")[0].setAttribute("src", "./images/eye-off.svg");
+}
+
+export const TEST_USER = {
+ email: "test@codeit.com",
+ password: "codeit101",
+};