diff --git a/README.md b/README.md index 90542066..9520f67e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# 03. 스프린트 미션 3 +# 03. 스프린트 미션 4 ## 요구사항 -### 스프린트 미션 3 시안 +### 스프린트 미션 4 시안 -- [실습 과제 디자인 Figma](https://www.figma.com/design/IVkRlYWHY74QlgmxqA99Ym/%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EB%AF%B8%EC%85%98?node-id=63-3453) +- [실습 과제 디자인 Figma](https://www.figma.com/design/IVkRlYWHY74QlgmxqA99Ym/%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EB%AF%B8%EC%85%98?node-id=63-3454) ### 기본 요구사항 @@ -14,31 +14,29 @@ ### 체크리스트 [기본] -#### 공통 -- [x] 브라우저에 현재 보이는 화면의 영역(viewport) 너비를 기준으로 분기되는 반응형 디자인을 적용합니다. - - PC: 1200px ~ - - Tablet: 768px ~ 1199px - - Mobile: 375px ~ 767px +#### 로그인 -#### 랜딩 페이지 -- [x] 헤더 좌우 여백 수정 - - Tablet: 24px - - Mobile: 16px -- [x] 화면 영역이 줄어들면 “Privacy Policy”, “FAQ”, “codeit-2024”이 있는 영역과 SNS 아이콘들이 있는 영역의 간격이 줄어듭니다. +- [x] 이메일 input에서 focus out 할 때, 값이 없을 경우 input에 빨강색 테두리와 아래에 “이메일을 입력해주세요.” 빨강색 에러 메세지를 보입니다. +- [x] 이메일 input에서 focus out 할 때, 이메일 형식에 맞지 않는 경우 input에 빨강색 테두리와 아래에 “잘못된 이메일 형식입니다” 빨강색 에러 메세지를 보입니다. -#### 로그인, 회원가입 페이지 -- [x] Tablet: 내부 디자인은 PC사이즈와 동일 -- [x] Mobile: 좌우 여백 16px, 내부 요소들이 너비를 모두 차지 -- [x] Mobile: 내부 요소 `max-width: 400px` +- [x] 비밀번호 input에서 focus out 할 때, 값이 없을 경우 아래에 “비밀번호를 입력해주세요.” 에러 메세지를 보입니다 +- [x] 비밀번호 input에서 focus out 할 때, 값이 8자 미만일 경우 아래에 “비밀번호를 8자 이상 입력해주세요.” 에러 메세지를 보입니다. + +- [x] input 에 빈 값이 있거나 에러 메세지가 있으면 ‘로그인’ 버튼은 비활성화 됩니다. +- [x] Input 에 유효한 값을 입력하면 ‘로그인' 버튼이 활성화 됩니다. +- [x] 활성화된 ‘로그인’ 버튼을 누르면 “/items” 로 이동합니다 + + +#### 회원가입 ### 체크리스트 [심화] -- [x] SNS에 랜딩 페이지(“/”) 공유 시 미리보기를 볼 수 있도록 메타 태그 설정 -- [x] 미리보기 제목은 “판다 마켓”, 설명은 “일상의 모든 물건을 거래해보세요”로 설정 +- [x] 눈 모양 아이콘 클릭시 비밀번호의 문자열이 보이기도 하고, 가려지기도 합니다. +- [x] 비밀번호의 문자열이 가려질 때는 눈 모양 아이콘에는 사선이 그어져있고, 비밀번호의 문자열이 보일 때는 사선이 없는 눈 모양 아이콘이 보이도록 합니다. ## 주요 변경사항 -### 스프린트 미션 2 리뷰 반영 +### 스프린트 미션 3 리뷰 반영 - [login.js](./scripts/login.js), [signup.js](./scripts/signup.js) - 변수 지정 및 함수명 변경을 통한 가독성 확보 - [login.html](./login.html), [signup.html](./signup.html) @@ -47,26 +45,21 @@ - [login.css](./styles/login.css), [signup.css](./styles/signup.css) - `signup.css`의 중복되는 스타일 제거 -### 스프린트 미션 3 -- +### 스프린트 미션 4 +- [members.js](./scripts/common/members.js) 추가를 통한 공통 함수 분리 ## 스크린샷 -### 랜딩 페이지 헤더 - Tablet -![tablet-page-image-landing-page](./assets/screenshot/landing-page-tablet.png) - -### 랜딩 페이지 헤더 - Mobile -![mobile-page-image-landing-page](./assets/screenshot/landing-page-mobile.png) - -### 로그인 - Tablet -![tablet-page-image-login](./assets/screenshot/login-page-tablet.png) - -### 회원가입 - Mobile -![mobile-page-image-signup](./assets/screenshot/signup-page-mobile.png) +### 로그인 +- 에러문구 표시, 비밀번호 입력 보기, 로그인 버튼 활성화, 페이지 이동 +![longin-gif-login-page](./assets/screenshot/login-page-desktop.gif) -### 랜딩 페이지 공유 -![open-graph-image-landing-page](./assets/screenshot/landing-page-open-graph.jpg) +### 회원가입 +- 에러문구 표시, 회원가입 버튼 활성화, 페이지 이동 +![signup-gif-signup-page](./assets/screenshot/signup-page-desktop.gif) ## 멘토에게 -- \ No newline at end of file +- ``과 같이 `type="module"` 속성을 주어야만 `login.js` 파일에서 `/common/members.js` 파일을 불러올 수 있는 것으로 보입니다. 해당 속성을 사용해야만 하는 이유가 궁금합니다. +- 공통함수 [members.js](./scripts/common/members.js)가 코드량이 많다보니, 더 효율적이고 가독성 좋게 바꿀 수 있는 방법이 있을지 궁금합니다. +- [login.js](/scripts/login.js), [signup.js](./scripts/signup.js) 두 파일이 비슷하게 생긴 점, `signup.html`에서 두 스크립트 파일을 불러오는 점에 개선점이 있을지 궁금합니다. \ No newline at end of file diff --git a/assets/screenshot/login-page-desktop.gif b/assets/screenshot/login-page-desktop.gif new file mode 100644 index 00000000..1a850a83 Binary files /dev/null and b/assets/screenshot/login-page-desktop.gif differ diff --git a/assets/screenshot/signup-page-desktop.gif b/assets/screenshot/signup-page-desktop.gif new file mode 100644 index 00000000..2dccd98b Binary files /dev/null and b/assets/screenshot/signup-page-desktop.gif differ diff --git a/login.html b/login.html index c13877f1..4607d0df 100644 --- a/login.html +++ b/login.html @@ -19,18 +19,18 @@ -
-
+ +
-
+
-
+
-
+
-
@@ -55,6 +55,6 @@

- + diff --git a/scripts/common/members.js b/scripts/common/members.js new file mode 100644 index 00000000..67bf3796 --- /dev/null +++ b/scripts/common/members.js @@ -0,0 +1,103 @@ +/** + * 비밀번호 입력 보기 버튼 클릭 로직 + * @param {*} e - 클릭 이벤트 + */ +export function onVisibilityChange(e) { + if (e.target.localName !== "button") return; + const button = e.target; + const input = document.getElementById(button.id).previousElementSibling; + if (input.type === "password") { + button.classList.add("checked"); + input.setAttribute("type", "text"); + } else { + button.classList.remove("checked"); + input.setAttribute("type", "password"); + } +} + +/** + * 포커스 아웃 시 입력값 체크 이벤트 + * @param {*} e + * @returns + */ +export function onInputFocusOut(e) { + if (e.target.localName !== "input") return; + const input = e.target; + if (!checkValidation(input)) input.classList.add("error"); + else input.classList.remove("error"); +} + +/** + * 입력값 유효성 검사 + * @param {*} input + * @returns + */ +function checkValidation(input) { + const { id: type, value } = input; + let result = false, + className = "", + errorMsg = ""; + + switch (type) { + case "email": + result = input.value.length > 0 && input.validity.valid; + className = "error-email"; + if (!result) + errorMsg = value.length === 0 ? "이메일을 입력해주세요." : "잘못된 이메일 형식입니다"; + break; + case "nickname": + result = value.length > 0; + className = "error-nickname"; + if (!result) errorMsg = "닉네임을 입력해주세요."; + break; + case "pwd": + result = value.length >= 8; + className = "error-pwd"; + if (!result) + errorMsg = + value.length === 0 ? "비밀번호를 입력해주세요." : "비밀번호를 8자 이상 입력해주세요"; + break; + case "pwd-check": + result = value === document.getElementById("pwd").value; + className = "error-pwd-check"; + if (!result) errorMsg = "비밀번호가 일치하지 않습니다."; + break; + default: + return; + } + + const inputField = input.parentElement?.parentElement; + const errorParagraph = document.getElementsByClassName(className)[0]; + + if (result) errorParagraph && inputField?.removeChild(errorParagraph); + else if (errorParagraph) errorParagraph.textContent = errorMsg; + else { + const newErrorParagraph = document.createElement("p"); + newErrorParagraph.classList.add("text-error", "text-md", "text-semibold", className); + newErrorParagraph.textContent = errorMsg; + inputField?.append(newErrorParagraph); + } + return result; +} + +/** + * input 값에 따른 form button 활성화/비활성화 처리 + * @param {*} btn - 활성화 처리할 버튼 엘리먼트 + */ +export function onFormInputChange(btn) { + let isEmptyInput = false; + const form = document.getElementsByTagName("form")[0]; + for (let inputField of form.children) { + if (!inputField.classList.value.includes("input-field")) break; + const inputFieldWrapper = inputField.children[1]; + const inputElement = inputFieldWrapper.children[0]; + if (inputElement?.value.length === 0) { + isEmptyInput = true; + break; + } + checkValidation(inputElement); + } + const errorText = document.getElementsByClassName("text-error"); + if (errorText.length === 0 && !isEmptyInput) btn.removeAttribute("disabled"); + else btn.setAttribute("disabled", "true"); +} diff --git a/scripts/login.js b/scripts/login.js index cb7eab16..8d8fa481 100644 --- a/scripts/login.js +++ b/scripts/login.js @@ -1,36 +1,23 @@ -const emailInputField = document.getElementById("email"); -const pwdInputField = document.getElementById("pwd"); +import { onFormInputChange, onInputFocusOut, onVisibilityChange } from "./common/members.js"; + +const loginForm = document.getElementsByClassName("form-login")[0]; +const emailInputField = document.getElementById("input-wrapper-email"); +const pwdInputField = document.getElementById("input-wrapper-pwd"); const visiblilityBtn = document.getElementById("visibility"); const loginBtn = document.getElementById("btn-submit-login"); -visiblilityBtn.addEventListener("click", onVisibilityChange); - -emailInputField.addEventListener("change", onLoginInputChange); -pwdInputField.addEventListener("change", onLoginInputChange); +// input 이벤트 핸들러를 통한 버튼 활성화 처리 +loginForm?.addEventListener("input", () => onFormInputChange(loginBtn)); -/** - * 비밀번호 입력 보기 버튼 클릭 로직 - * @param {*} e - 클릭 이벤트 - */ -function onVisibilityChange(e) { - if (e.target.classList.value.includes("checked")) { - visiblilityBtn.classList.remove("checked"); - pwdInputField.setAttribute("type", "password"); - } else { - visiblilityBtn.classList.add("checked"); - pwdInputField.setAttribute("type", "text"); - } -} +// focusout 이벤트 핸들러를 통한 에러 메세지 표시 +emailInputField.addEventListener("focusout", onInputFocusOut); +pwdInputField.addEventListener("focusout", onInputFocusOut); -/** - * 로그인 버튼 활성화 로직 - */ -function onLoginInputChange() { - if (!loginBtn) return; - const email = emailInputField.value; - const pwd = pwdInputField.value; +// click 이벤트 핸들러를 통한 비밀번호 입력 확인 처리 +visiblilityBtn.addEventListener("click", onVisibilityChange); - // TODO: 입력 체크 로직 - if (email && pwd) loginBtn.removeAttribute("disabled"); - else loginBtn.setAttribute("disabled", "true"); -} +// submit 이벤트 핸들러를 통한 페이지 이동 처리 +loginForm?.addEventListener("submit", (e) => { + e.preventDefault(); + window.location.href = "/items"; +}); diff --git a/scripts/signup.js b/scripts/signup.js index ab1f4ca6..f34135e4 100644 --- a/scripts/signup.js +++ b/scripts/signup.js @@ -1,40 +1,23 @@ -const nicknameInputField = document.getElementById("nickname"); -const pwdCheckInputField = document.getElementById("pwd-check"); +import { onFormInputChange, onInputFocusOut, onVisibilityChange } from "./common/members.js"; + +const signupForm = document.getElementsByClassName("form-signup")[0]; +const nicknameInputField = document.getElementById("input-wrapper-nickname"); +const pwdCheckInputField = document.getElementById("input-wrapper-pwd-check"); const visiblilityAgainBtn = document.getElementById("visibility-again"); const signupBtn = document.getElementById("btn-submit-signup"); -visiblilityAgainBtn.addEventListener("click", onVisibilityAgainChange); - -emailInputField.addEventListener("change", onSignupInputChange); -nicknameInputField.addEventListener("change", onSignupInputChange); -pwdInputField.addEventListener("change", onSignupInputChange); -pwdCheckInputField.addEventListener("change", onSignupInputChange); +// input 이벤트 핸들러를 통한 버튼 활성화 처리 +signupForm.addEventListener("input", () => onFormInputChange(signupBtn)); -/** - * 비밀번호 확인 입력 보기 버튼 클릭 로직 - * @param {*} e - 클릭 로직 - */ -function onVisibilityAgainChange(e) { - if (e.target.classList.value.includes("checked")) { - visiblilityAgainBtn.classList.remove("checked"); - pwdCheckInputField.setAttribute("type", "password"); - } else { - visiblilityAgainBtn.classList.add("checked"); - pwdCheckInputField.setAttribute("type", "text"); - } -} +// focusout 이벤트 핸들러를 통한 에러 메세지 표시 +nicknameInputField.addEventListener("focusout", onInputFocusOut); +pwdCheckInputField.addEventListener("focusout", onInputFocusOut); -/** - * 회원가입 버튼 활성화 로직 - */ -function onSignupInputChange() { - if (!signupBtn) return; - const email = emailInputField.value; - const nickname = nicknameInputField.value; - const pwd = pwdInputField.value; - const pwdCheck = pwdCheckInputField.value; +// click 이벤트 핸들러를 통한 비밀번호 입력 확인 처리 +visiblilityAgainBtn.addEventListener("click", onVisibilityChange); - // TODO: 입력 체크 로직 - if (email && nickname && pwd && pwd === pwdCheck) signupBtn.removeAttribute("disabled"); - else signupBtn.setAttribute("disabled", "true"); -} +// submit 이벤트 핸들러를 통한 페이지 이동 처리 +signupForm.addEventListener("submit", (e) => { + e.preventDefault(); + window.location.href = "/signin"; +}); diff --git a/signup.html b/signup.html index 8ea5f694..308de2a2 100644 --- a/signup.html +++ b/signup.html @@ -20,31 +20,31 @@ -
-
+ +
-
+
-
+
-
+
-
+
-
+
-
-
+
-
+
-
@@ -69,7 +69,7 @@

- - + + diff --git a/styles/global.css b/styles/global.css index c684e199..946ca27f 100644 --- a/styles/global.css +++ b/styles/global.css @@ -27,6 +27,9 @@ a { .text-primary-100 { color: var(--primary-100); } +.text-error { + color: var(--error-red); +} /* font */ @font-face { /* 로고 폰트 */ diff --git a/styles/home.css b/styles/home.css index 08fb3b7f..dcf4f122 100644 --- a/styles/home.css +++ b/styles/home.css @@ -44,9 +44,10 @@ header .wrapper a.button { main .banner { background-color: #cfe5ff; padding-top: 200px; + gap: 7px; } main .banner .wrapper { - gap: 7px; + gap: 32px; } main .banner .wrapper a.button { width: 357px; @@ -75,6 +76,7 @@ main #bottom-banner { main .banner, main .banner .wrapper { flex-direction: column; + gap: 24px; } main .banner .wrapper a { margin: 0 auto 132px; @@ -126,6 +128,7 @@ main #bottom-banner { /* Mobile */ main .banner .wrapper { text-align: center; + gap: 18px; } main .banner .wrapper h1, main .banner .wrapper .heading { diff --git a/styles/login.css b/styles/login.css index d83fde0a..6967051f 100644 --- a/styles/login.css +++ b/styles/login.css @@ -3,7 +3,7 @@ main#members { flex-direction: column; } main#members input, -main#members p { +main#members p:not(.text-error) { color: #1f2937; } main#members img#logo { @@ -16,6 +16,17 @@ main#members > form { justify-content: stretch; } /* Input Field */ +.input-field label { + margin-bottom: 16px; +} +.input-field p.text-error { + position: relative; + top: 8px; + left: 16px; +} +.input-field.error .input-wrapper { + border: 1px solid var(--error-red); +} .input-wrapper { gap: 10px; padding: 16px 24px;