diff --git a/css/auth.css b/css/auth.css
index d3510369..6ad0c1a0 100644
--- a/css/auth.css
+++ b/css/auth.css
@@ -1,5 +1,5 @@
-@import "global.css";
@import "reset.css";
+@import "global.css";
.body {
display: flex;
@@ -51,40 +51,37 @@
display: flex;
justify-content: space-between;
align-items: center;
- background-color: var(--gray100);
+ background-color: var(--gray-100);
border-radius: 12px;
height: 3.5rem;
padding: 1rem 1.5rem;
}
-/*
-:focus-within이란 - 스스로 :focus 의사 클래스와 일치하거나, 그 자손 중 하나가 :focus와 일치하는 요소를 나타냄
-*/
.form__field-input-wrapper--focused:focus-within {
- outline: 2px solid var(--blue100);
+ outline: 2px solid var(--blue-100);
}
.form__field-input {
- color: var(--gray800);
+ color: var(--gray-800);
font-weight: 400;
- background-color: var(--gray100);
+ background-color: var(--gray-100);
width: 100%;
}
.form__field-input::placeholder {
- color: var(--gray400);
+ color: var(--gray-400);
}
-.form__field-toggle-button {
+.form__field-button {
background-color: inherit;
cursor: pointer;
}
.form__submit-button {
- color: var(--gray100);
+ color: var(--gray-100);
font-size: 1.25rem;
font-weight: 600;
- background-color: var(--blue100);
+ background-color: var(--blue-100);
border-radius: 2.5rem;
width: 100%;
padding: 1rem 0;
@@ -92,7 +89,7 @@
}
.form__submit-button--disabled:disabled {
- background-color: var(--gray400);
+ background-color: var(--gray-400);
cursor: not-allowed;
}
@@ -100,7 +97,7 @@
display: flex;
justify-content: space-between;
align-items: center;
- background-color: #e6f2ff;
+ background-color: var(--blue-50);
width: 100%;
padding: 1rem 2.5rem;
}
@@ -117,7 +114,7 @@
}
.helper-text__link {
- color: var(--blue100);
+ color: var(--blue-100);
text-decoration: underline;
}
@@ -128,3 +125,11 @@
max-width: 400px;
}
}
+
+.error {
+ outline: 2px solid var(--red);
+}
+
+.form__msg-container {
+ color: var(--red);
+}
diff --git a/css/global.css b/css/global.css
index 866fb18a..05374537 100644
--- a/css/global.css
+++ b/css/global.css
@@ -10,24 +10,28 @@
font-family: "Pretendard-Regular", sans-serif;
font-size: 16px;
- --blue100: #3692ff;
- --blue200: #1967d6;
- --blue300: #1251aa;
+ --white: #ffffff;
+ --white-light: #fcfcfc;
+ --white-muted: #dfdfdf;
+
+ --blue-50: #cfe5ff;
+ --blue-100: #3692ff;
+ --blue-200: #1967d6;
+ --blue-300: #1251aa;
--red: #f74747;
- --gray50: #f9fafb;
- --gray100: #f3f4f6;
- --gray200: #e5e7eb;
- --gray400: #9ca3af;
- --gray500: #6b7280;
- --gray600: #4b5563;
- --gray700: #374151;
- --gray800: #1f2937;
- --gray900: #111827;
+ --gray-50: #f9fafb;
+ --gray-100: #f3f4f6;
+ --gray-200: #e5e7eb;
+ --gray-400: #9ca3af;
+ --gray-500: #6b7280;
+ --gray-600: #4b5563;
+ --gray-700: #374151;
+ --gray-800: #1f2937;
+ --gray-900: #111827;
}
-/* Tablet, Mobile 기기에 따라 폰트 크기 조정*/
@media (768px <= width < 1200px) {
:root {
font-size: 14px;
diff --git a/css/landing.css b/css/landing.css
index dbbbe422..23467cd4 100644
--- a/css/landing.css
+++ b/css/landing.css
@@ -1,9 +1,9 @@
-@import "global.css";
@import "reset.css";
+@import "global.css";
.header {
- background-color: #ffffff;
- border-bottom: solid 1px #dfdfdf;
+ background-color: var(--white);
+ border-bottom: solid 1px var(--gray-300);
position: sticky;
top: 0px;
padding: 0.5rem 0;
@@ -24,9 +24,9 @@
justify-content: center;
align-items: center;
flex-shrink: 0;
- color: #ffffff;
+ color: var(--white);
font-weight: 600;
- background-color: var(--blue100);
+ background-color: var(--blue-100);
height: 48px;
width: 128px;
border-radius: 8px;
@@ -34,14 +34,14 @@
}
.header__button--primary:visited {
- color: var(--gray100);
+ color: var(--gray-100);
}
.hero {
display: flex;
justify-content: center;
align-items: end;
- background-color: #cfe5ff;
+ background-color: var(--blue-50);
height: 540px;
}
@@ -63,8 +63,8 @@
}
.hero__button {
- color: #ffffff;
- background-color: var(--blue100);
+ color: var(--white);
+ background-color: var(--blue-100);
border-radius: 40px;
padding: 1.25rem 7.5rem;
cursor: pointer;
@@ -96,7 +96,7 @@
display: flex;
justify-content: space-evenly;
align-items: center;
- background-color: #fcfcfc;
+ background-color: var(--white-light);
gap: 2.5rem;
padding: 0 1rem;
}
@@ -150,8 +150,8 @@
.footer {
font-weight: 400;
- color: var(--gray400);
- background-color: var(--gray900);
+ color: var(--gray-400);
+ background-color: var(--gray-900);
height: 160px;
padding-top: 40px;
}
diff --git a/js/auth.js b/js/auth.js
index c368f6c5..73c2a258 100644
--- a/js/auth.js
+++ b/js/auth.js
@@ -1,18 +1,112 @@
-import { isEmpty } from "./utils.js";
+import {
+ isEmpty,
+ isEmailValid,
+ isPwdValid,
+ applyClass,
+ removeClass,
+} from "./utils.js";
+const ERROR_EMAIL_EMPTY = "이메일을 입력해주세요.";
+const ERROR_EMAIL_PATTERN = "잘못된 이메일 형식입니다.";
+const ERROR_PASSWORD_EMPTY = "비밀번호를 입력해주세요.";
+const ERROR_PASSWORD_PATTERN = "비밀번호를 8자 이상 입력해주세요.";
+
+const IMG_VISIBLE_ON = "../assets/ic_visibility_on.svg";
+const IMG_VISIBLE_OFF = "../assets/ic_visibility_off.svg";
+
+const formInputWrappers = document.querySelectorAll(
+ ".form__field-input-wrapper"
+);
const formInputs = document.querySelectorAll(".form__field-input");
-const submitButton = document.querySelector(".form__submit-button--disabled");
+const submitButton = document.querySelector(".form__submit-button");
+const emailInput = document.querySelector("#email");
+const emailInputWrapper = document.querySelector(
+ ".form__field-input-wrapper--email"
+);
+const emailMsgContainer = document.querySelector(".form__msg-container--email");
+const pwdInput = document.querySelector("#password");
+const pwdInputWrapper = document.querySelector(
+ ".form__field-input-wrapper--password"
+);
+const pwdMsgContainer = document.querySelector(
+ ".form__msg-container--password"
+);
+
+const pwdVisiblityButton = document.querySelector(
+ ".form__field-button.form__field-button--password"
+);
+const pwdVisiblityButtonImg = document.querySelector(
+ ".form__field-image.form__field-image--password"
+);
+
+const handleInputFocusin = () => {
+ submitButton.disabled = true;
+};
+
+formInputs.forEach((inputNode) =>
+ inputNode.addEventListener("focusin", handleInputFocusin)
+);
+
+export const isFormValid = () => {
+ const isInputsEmpty = Array.from(formInputs).some((input) =>
+ isEmpty(input.value)
+ );
+ const isInputsValid = Array.from(formInputWrappers).every(
+ (input) => !input.classList.contains("error")
+ );
+
+ return !isInputsEmpty && isInputsValid;
+};
-const updateSubmitButton = () => {
- const isFormFilled = Array.from(formInputs)
- .map((input) => input.value)
- .every((string) => !isEmpty(string));
+const handleEmailFocusout = (e) => {
+ const email = e.target.value;
- submitButton.disabled = !isFormFilled;
+ if (!isEmailValid(email)) {
+ applyClass(emailInputWrapper, "error");
+ emailMsgContainer.textContent = isEmpty(email)
+ ? ERROR_EMAIL_EMPTY
+ : ERROR_EMAIL_PATTERN;
+ return;
+ } else {
+ removeClass(emailInputWrapper, "error");
+ emailMsgContainer.textContent = "";
+ }
+
+ submitButton.disabled = !isFormValid();
+};
+
+const handlePwdFocusout = (e) => {
+ const pwd = e.target.value;
+
+ if (!isPwdValid(pwd)) {
+ applyClass(pwdInputWrapper, "error");
+ pwdMsgContainer.textContent = isEmpty(pwd)
+ ? ERROR_PASSWORD_EMPTY
+ : ERROR_PASSWORD_PATTERN;
+ return;
+ } else {
+ removeClass(pwdInputWrapper, "error");
+ pwdMsgContainer.textContent = "";
+ }
+
+ submitButton.disabled = !isFormValid();
};
-formInputs.forEach((input) => {
- input.addEventListener("input", updateSubmitButton);
-});
+emailInput.addEventListener("focusout", handleEmailFocusout);
+pwdInput.addEventListener("focusout", handlePwdFocusout);
+
+export const createVisibilityToggleHandler = (input, image) => {
+ let visible = false;
+
+ return function onClickToggleButton() {
+ visible = !visible;
+
+ input.type = visible ? "text" : "password";
+ image.src = visible ? IMG_VISIBLE_ON : IMG_VISIBLE_OFF;
+ };
+};
-updateSubmitButton();
+pwdVisiblityButton.addEventListener(
+ "click",
+ createVisibilityToggleHandler(pwdInput, pwdVisiblityButtonImg)
+);
diff --git a/js/signup.js b/js/signup.js
new file mode 100644
index 00000000..97a1f0f8
--- /dev/null
+++ b/js/signup.js
@@ -0,0 +1,69 @@
+import { isEmpty, isPwdMatched, applyClass, removeClass } from "./utils.js";
+import { isFormValid, createVisibilityToggleHandler } from "./auth.js";
+
+const ERROR_PASSWORD_MISMATCH = "비밀번호가 일치하지 않습니다.";
+const ERROR_NICKNAME_EMPTY = "닉네임을 입력해주세요.";
+
+const nicknameInput = document.querySelector("#nickname");
+const nicknameInputWrapper = document.querySelector(
+ ".form__field-input-wrapper--nickname"
+);
+const nicknameMsgContainer = document.querySelector(
+ ".form__msg-container--nickname"
+);
+const pwdInput = document.querySelector("#password");
+const confirmPwdInput = document.querySelector("#confirm-password");
+const confirmPwdInputWrapper = document.querySelector(
+ ".form__field-input-wrapper--confirm-password"
+);
+const confirmPwdMsgContainer = document.querySelector(
+ ".form__msg-container--confirm-password"
+);
+
+const confirmPwdVisiblityButton = document.querySelector(
+ ".form__field-button.form__field-button--confirm-password"
+);
+const confirmPwdVisiblityButtonImg = document.querySelector(
+ ".form__field-image.form__field-image--confirm-password"
+);
+
+const submitButton = document.querySelector(".form__submit-button");
+
+const handleNicknameFocusout = (e) => {
+ const nickname = e.target.value;
+
+ if (isEmpty(nickname)) {
+ applyClass(nicknameInputWrapper, "error");
+ nicknameMsgContainer.textContent = ERROR_NICKNAME_EMPTY;
+ return;
+ } else {
+ removeClass(nicknameInputWrapper, "error");
+ nicknameMsgContainer.textContent = "";
+ }
+
+ submitButton.disabled = !isFormValid();
+};
+
+const handleConfirmPwdFocusout = (e) => {
+ const pwd = pwdInput.value;
+ const confirmPwd = e.target.value;
+
+ if (!isPwdMatched(pwd, confirmPwd)) {
+ applyClass(confirmPwdInputWrapper, "error");
+ confirmPwdMsgContainer.textContent = ERROR_PASSWORD_MISMATCH;
+ return;
+ } else {
+ removeClass(confirmPwdInputWrapper, "error");
+ confirmPwdMsgContainer.textContent = "";
+ }
+
+ submitButton.disabled = !isFormValid();
+};
+
+nicknameInput.addEventListener("focusout", handleNicknameFocusout);
+confirmPwdInput.addEventListener("focusout", handleConfirmPwdFocusout);
+
+confirmPwdVisiblityButton.addEventListener(
+ "click",
+ createVisibilityToggleHandler(confirmPwdInput, confirmPwdVisiblityButtonImg)
+);
diff --git a/js/utils.js b/js/utils.js
index e10aa189..066147b0 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -1 +1,19 @@
+const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+const pwdRegex = /^[a-zA-Z0-9]{8,}$/;
+
export const isEmpty = (text) => text.trim() === "";
+export const isEmailValid = (email) => emailRegex.test(email);
+export const isPwdValid = (pwd) => pwdRegex.test(pwd);
+export const isPwdMatched = (pwd, confirm) => pwd === confirm;
+
+export const applyClass = (target, className) => {
+ if (!target.classList.contains(className)) {
+ target.classList.add(className);
+ }
+};
+
+export const removeClass = (target, className) => {
+ if (target.classList.contains(className)) {
+ target.classList.remove(className);
+ }
+};
diff --git a/pages/login.html b/pages/login.html
index 51144193..519ca0f4 100644
--- a/pages/login.html
+++ b/pages/login.html
@@ -11,11 +11,11 @@
-