diff --git a/.gitignore b/.gitignore index 92c19380..7a18d54f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,10 @@ node_modules/ .env # macOS의 경우 생성되는 파일들 -.DS_Store \ No newline at end of file +.DS_Store + +# vscode 설정 파일 +.vscode/ + +# mission_template_codes 공부용 +mission_template_codes/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f1a41bc2..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cSpell.words": ["codeit", "kakao"] -} diff --git a/README.md b/README.md new file mode 100644 index 00000000..abf78456 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ + + +# 코드잇 스프린트 미션 + +## 미션 목록 + +| 미션 | 날짜 | PR | 주요 내용 | +| ---- | ---------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| 1 | 2025-02-24 | [#10](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/10) | 랜딩 페이지의 HTML 및 CSS 구현 | +| 2 | 2025-03-05 | [#44](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/44) | 회원가입 및 로그인 페이지의 HTML, CSS 구현 | +| 3 | 2025-03-07 | [#60](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/60) | 반응형 디자인 구현(desktop-first, 1920px 이상 큰 모니터 기준), breakpoint: 1919px, 1199px, 767px | +| 4 | 2025-03-18 | [#](https://github.com/codeit-bootcamp-frontend/15-Sprint-Mission/pull/) | JS기능 추가(DOM 요소 조작 및 이벤트 리스너), 회원가입, 로그인 폼 유효성 검사 | + +--- + +## 컨벤션 + +### 명명 규칙 + +1. **이미지 파일**: + + - 이미지 파일 이름은 **소문자**로 작성하고, **언더스코어(\_)**를 사용하여 단어를 구분합니다. + +2. **HTML, CSS, JS 파일, 폴더명**: + + - 파일, 폴더 이름은 **소문자**로 작성하고, **하이픈(-)**을 사용하여 단어를 구분합니다.(kebab-case) + +3. **변수명, 함수명, 프로퍼티 키**: + - camelCase + +### 함수 규칙 + +**화살표 함수**를 사용하되, this바인딩 고려 시 필요한 경우(이벤트 리스너의 콜백함수, 메소드 정의 등) 일반 함수도 사용 가능 합니다. + +### 커밋 규칙 + +1. 커밋 메시지는 소문자로 작성합니다. +2. 커밋 메시지 본문 작성은 선택사항입니다. +3. 타입: 내용 + | **타입** | **내용** | + |------ ---|-----------| + | **feat** | 새로운 기능 추가 | + | **fix** | 버그 수정 | + | **docs** | 문서 변경 (README, Wiki 등) | + | **style** | 코드 스타일 변경 (세미콜론, 공백, 들여쓰기 등) | + | **refactor** | 코드 리팩토링 (기능 변경 없이 코드 구조나 가독성 개선) | + | **perf** | 성능 개선 | + | **test** | 테스트 코드 추가 및 수정 | + | **chore** | 기타 일들 (빌드 스크립트, 환경 설정 등) | + +## 폴더 구조 diff --git a/auth/auth-validation.js b/auth/auth-validation.js new file mode 100644 index 00000000..0214cdea --- /dev/null +++ b/auth/auth-validation.js @@ -0,0 +1,151 @@ +import { ERROR_MESSAGES } from '../constants/auth-validation-messages.js'; + +document.addEventListener('DOMContentLoaded', function () { + const form = document.querySelector('.auth-form'); + const authSubmitButton = document.querySelector('.auth-button'); + const emailInput = document.getElementById('email'); + const passwordInput = document.getElementById('password'); + const nicknameInput = document.getElementById('nickname'); + const confirmPasswordInput = document.getElementById('password-confirm'); + const authType = form.dataset.authType; + + let emailValue; + let nicknameValue; + let passwordValue; + let confirmPasswordValue; + + // 이메일 형식 검증 + const validateEmail = (email) => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(String(email)); + }; + + // 에러 메시지 표시 + const toggleError = (targetInput, message, isInputValid) => { + const inputContainer = targetInput.closest('.input-container'); + if (!inputContainer) return; + const errorContainer = inputContainer.querySelector( + '.validation-error-message', + ); + if (!isInputValid) { + targetInput.classList.add('error-input'); + errorContainer.textContent = message; + errorContainer.classList.add('active'); + } else { + targetInput.classList.remove('error-input'); + errorContainer.textContent = ''; + errorContainer.classList.remove('active'); + } + }; + + // 개별 인풋 검증: 포커스아웃된 input만 검증하고 에러 메시지 표시 + const validateInput = (target) => { + if (target.id === 'email') { + emailValue = emailInput.value.trim(); + + if (emailValue === '') { + toggleError(target, ERROR_MESSAGES.emailRequired, false); + } else if (!validateEmail(emailValue)) { + toggleError(target, ERROR_MESSAGES.invalidEmail, false); + } else { + toggleError(target, '', true); + } + } + + if (target.id === 'nickname' && authType === 'signup') { + nicknameValue = nicknameInput.value.trim(); + if (nicknameValue === '') { + toggleError(target, ERROR_MESSAGES.nicknameRequired, false); + } else { + toggleError(target, '', true); + } + } + + if (target.id === 'password') { + passwordValue = passwordInput.value.trim(); + if (passwordValue === '') { + toggleError(target, ERROR_MESSAGES.passwordRequired, false); + } else if (passwordValue.length < 8) { + toggleError(target, ERROR_MESSAGES.passwordLength, false); + } else { + toggleError(target, '', true); + } + } + + if (target.id === 'password-confirm' && authType === 'signup') { + confirmPasswordValue = confirmPasswordInput.value.trim(); + if (confirmPasswordValue === '') { + toggleError(target, ERROR_MESSAGES.confirmPasswordRequired, false); + } else if (passwordValue !== confirmPasswordValue) { + toggleError(target, ERROR_MESSAGES.passwordMismatch, false); + } else { + toggleError(target, '', true); + } + } + }; + + // 전체 폼 유효성 검사 + const validateForm = () => { + let isFormValid = true; + + emailValue = emailInput.value.trim(); + passwordValue = passwordInput.value.trim(); + nicknameValue = nicknameInput ? nicknameInput.value.trim() : ''; + confirmPasswordValue = confirmPasswordInput + ? confirmPasswordInput.value.trim() + : ''; + + if (emailValue === '' || !validateEmail(emailValue)) isFormValid = false; + if (passwordValue === '' || passwordValue.length < 8) isFormValid = false; + if (authType === 'signup') { + if (nicknameValue === '') isFormValid = false; + if (confirmPasswordValue === '' || passwordValue !== confirmPasswordValue) + isFormValid = false; + } + return isFormValid; + }; + + // 제출 버튼 활성화 상태 + const updateSubmitButtonState = () => { + authSubmitButton.disabled = !validateForm(); + }; + + // 포커스아웃 시 인풋 유효성 검사 + form.addEventListener('focusout', function (event) { + if (event.target.matches('input')) { + validateInput(event.target); + } + }); + + // input 이벤트에 적용(input이 변할때마다 이벤트가 발생하는 것을 막기 위해 넣었지만 원리는 이해 더 필요) + const debounce = (func, delay) => { + let timer; + return function (...args) { + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(this, args); + }, delay); + }; + }; + + // 인풋 입력 시 버튼 상태 업데이트(form이 유효성 검사를 통과하면 focus를 옮기지 않아도 자동 버튼 활성화되도록) + form.addEventListener( + 'input', + debounce((event) => { + updateSubmitButtonState(); + }, 300), + ); + + // 폼 제출 시 전체 폼 검증 후 페이지 이동 처리 + form.addEventListener('submit', function (event) { + event.preventDefault(); + + if (validateForm()) { + if (authType === 'signup') { + window.location.href = '/signup'; + } else if (authType === 'login') { + window.location.href = '/items'; + } + } + }); +}); diff --git a/css/login-signup.css b/auth/auth.css similarity index 77% rename from css/login-signup.css rename to auth/auth.css index cbc34313..80ca925a 100644 --- a/css/login-signup.css +++ b/auth/auth.css @@ -1,9 +1,9 @@ -.login-signup-body { +.auth-body { display: flex; justify-content: center; } -.login-signup-container { +.auth-container { display: flex; flex-direction: column; width: 64rem; @@ -12,8 +12,8 @@ gap: 2.4rem; } -/* login-signup header */ -.login-signup-header { +/* auth header */ +.auth-header { display: flex; justify-content: center; align-items: center; @@ -21,17 +21,20 @@ width: 100%; } -.login-signup-header .logo { +.auth-header .logo { width: 10rem; } -.login-signup-header h1 a { +.auth-header .logo-typo { + width: 30rem; +} + +.auth-header h1 a { font-size: 6rem; - margin-left: 2rem; } -/* login-signup form */ -.login-signup-form { +/* auth form */ +.auth-form { display: flex; flex-direction: column; gap: 2.4rem; @@ -56,7 +59,7 @@ input { width: 100%; } -.login-signup-button { +.auth-button { font-size: 2rem; font-weight: 600; line-height: 3.2rem; @@ -127,10 +130,36 @@ input { text-underline-offset: 2px; } +.validation-error-message { + color: var(--error); + font-size: 1.4rem; + font-weight: 600; + line-height: 2.4rem; + display: none; +} + +.validation-error-message.active { + display: block; + padding: 0.8rem 1.6rem; +} + +.error-input { + border: 1px solid var(--error); +} + +/* responsive */ @media (max-width: 767px) { - .login-signup-container { + .auth-container { margin: 0 16px; max-width: 400px; width: 100%; } + + .auth-header .logo { + width: 9rem; + } + + .auth-header .logo-typo { + width: 25rem; + } } diff --git a/html/login.html b/auth/login.html similarity index 57% rename from html/login.html rename to auth/login.html index 8e76cc77..953e07ec 100644 --- a/html/login.html +++ b/auth/login.html @@ -1,4 +1,4 @@ - +
@@ -13,16 +13,24 @@