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 @@ 판다마켓 로고 -
+
+
-
+
diff --git a/pages/signup.html b/pages/signup.html index e70539b6..fc5d0bb0 100644 --- a/pages/signup.html +++ b/pages/signup.html @@ -11,11 +11,11 @@ 판다마켓 로고 - +
+
+
-
+
-
+
@@ -111,4 +126,5 @@ +