diff --git a/.stylelintrc.json b/.stylelintrc.json index 6a802cfa..601707b6 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -2,6 +2,7 @@ "extends": ["stylelint-config-standard", "stylelint-prettier/recommended"], "plugins": ["stylelint-order"], "rules": { + "media-feature-range-notation": "prefix", "selector-class-pattern": [ "^[a-z][a-z0-9-]*(?:__[a-z0-9-]+)*(?:--[a-z0-9-]+)*$", { diff --git a/README.md b/README.md index de0b0885..6b7fe17e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# 스프린트 미션 3 +# 스프린트 미션 4 ## 배포 링크 > https://gentle-lamington-bbd035.netlify.app/ -## 1. 요구사항 +## 요구사항 -### 1-1. 공통 요구사항 +### 공통 요구사항 - [x] “판다마켓” 클릭 시 루트 페이지(‘/’)로 이동한다. - [x] 클릭으로 기능이 동작해야 하는 경우, 사용자가 클릭할 수 있는 요소임을 알 수 있도록 `cursor: pointer`를 설정한다. @@ -44,8 +44,6 @@ #### 메타 태그 - [x] 페이스북, 카카오톡, 디스코드, 트위터 등 SNS에서 Linkbrary 랜딩 페이지(“/”) 공유 시 좌측 예시와 같은 미리보기를 볼 수 있도록 랜딩 페이지 메타 태그를 설정한다. - > 미리보기에서 제목은 “판다 마켓”, 설명은 “일상의 모든 물건을 거래해보세요”로 설정합니다. - > 주소와 이미지는 자유롭게 설정하세요. ### 로그인 및 회원가입 페이지 ('/login, /signup') @@ -60,75 +58,63 @@ - [x] 회원가입 버튼 클릭 시 "/signup" 페이지로 이동한다. - [x] 로그인 버튼 클릭 시 "/login" 페이지로 이동한다. +- [x] 활성화된 ‘로그인’ 버튼을 누르면 “/items” 로 이동한다. + +#### 유효성 검사 + +- [x] 이메일 input에서 focus out 할 때, 값이 없을 경우 input에 빨강색 테두리와 아래에 “이메일을 입력해주세요.” 빨강색 에러 메세지를 보인다. +- [x] 이메일 input에서 focus out 할 때, 이메일 형식에 맞지 않는 경우 input에 빨강색 테두리와 아래에 “잘못된 이메일 형식입니다” 빨강색 에러 메세지를 보인다. +- [x] 비밀번호 input에서 focus out 할 때, 값이 없을 경우 아래에 “비밀번호를 입력해주세요.” 에러 메세지를 보인다. +- [x] 비밀번호 input에서 focus out 할 때, 값이 8자 미만일 경우 아래에 “비밀번호를 8자 이상 입력해주세요.” 에러 메세지를 보인다. +- [x] input 에 빈 값이 있거나 에러 메세지가 있으면 ‘로그인’ 버튼은 비활성화 되고, Input 에 유효한 값을 입력하면 ‘로그인' 버튼이 활성화 된다. +- [ ]눈 모양 아이콘 클릭시 비밀번호의 문자열이 보이기도 하고, 가려지기도 합니다. + 비밀번호의 문자열이 가려질 때는 눈 모양 아이콘에는 사선이 그어져있고, 비밀번호의 문자열이 보일 때는 사선이 없는 눈 모양 아이콘이 보이도록 합니다. ### 피드백 수정사항 -- [ ] 공통 스타일 정리 -- [ ] 미디어 쿼리 작성 순서 변경 및 문법 수정 -- [ ] 반응형 이미지 최적화 +- [x] 공통 스타일 정리 +- [x] 미디어 쿼리 작성 순서 변경 및 문법 수정 +- [ ] 반응형 이미지 태그 최적화 - [ ] husky 도입 -- [ ] 디렉토리 구조 변경 > ref: https://github.com/codeit-bootcamp-frontend/16-Sprint-Mission/pull/92 ## 프로젝트 구조 -- `/assets` - - - `/icon` - - `/img` - - `/logo` - - `/fonts` - -- `/styles` - - - `reset.css` - - `common.css` - - `variables.css` - - `font.css` - - `/pages` - - `home.css` - - `signup.css` - - `login.css` - -- `index.html` +- `/src` + + - `/js` # Javascript + + - `/constants` + - `/service` + - `/utils` + - `redirector.js` # 페이지 이동 함수 + - `/components` + - `InputFieldHandler.js` # 입력 필드내 유효성 검증 및 에러 메시지 랜더링 + - `FormValidator.js` # 폼 전체 필드 관리 + - `/validators` # 유효성 검증 스크립트 폴더 + - `emailValidator.js` + - `passwordValidator.js` + + - `/styles` # CSS + - `/base` + - `reset.css` # html 기본 스타일 초기화 + - `variables.css` # css 전역 변수 지정 + - `font.css` # fontface 지정 + - `/pages` + - `home.css` + - `signup.css` + - `login.css` + - `common.css` # 공통 스타일 - `/pages` + - `signup.html` - `login.html` -## 구현 사항 - -### 전역 설정 - -- css를 `reset.css`, `common.css`, `variables.css`로 분리했습니다. -- 자주 사용되는 구조는 css에서 common.css에서 사용하도록 하였습니다. -- layout, color, font-size, space 등을 `variables.css`에서 전역 변수로 관리합니다. -- 컬러 변수 네이밍을 시멘틱하게 변경했습니다. -- `font-size: clamp(12px, 1.6vw, 16px)` 를 통해, 폰트 사이즈가 12px에서 16px 사이로 유연하게 조절됩니다. -- 미디어 쿼리를 활용해 태블릿 및 모바일을 지원합니다. -- 웹 폰트에서 압축률이 좋은 `woff2` 로컬 폰트로 변경했습니다. -- 접근성 향상을 위해 `aria-label`을 지정했습니다. -- 보다 나은 클래스 구조화를 위해, **BEM 네이밍 방법론**과 **보조 클래스**를 사용했습니다. -- `stylelint`를 사용하여 BEM 네이밍 및 스타일을 검사합니다. -- `prettier`을 사용하여 포맷팅을 자동화했습니다. - -### 랜딩 페이지 (index.html) - -- 크게 header, main, footer 영역으로 나뉘어 있습니다. -- `
` 의 각 `
` 안에는 hero, feature, cta 로 구성되어 있습니다. -- `.hero`, `.cta` 클래스는 하위 박스인 `.container` 에서, 여백과 콘텐츠 관련 요구사항을 만족했습니다. -- 링크로 연결되는 항목들은 ``태그로 감싸서, 클릭시 특정 페이지들로 이동할 수 있습니다. -- 폰트, 이미지, 몇몇 여백들은 `vw`를 활용해서 동적으로 크기가 조절됩니다. -- 새 창으로 열리는 페이지들은 `target="_blank" rel="noopener"`속성을 사용하여, 보안상 취약점이 발생하고 퍼포먼스가 저하되는 문제를 해결했습니다. -- 상단 GNB는 고정되도록 설정하였습니다. -- OG와 twitter의 메타 정보를 등록했습니다. - -### 회원가입 및 로그인 페이지 - -- 주요 내용은 `form`필드 내에 구성하였습니다. -- 자주 사용하는 항목은 common.css에서 재사용합니다. +- `index.html` -## 질문 +## 구현 사항 -1. 현제 스펙 웹사이트의 디렉토리 구조 관리 방법이 궁금합니다. +- Javascript를 모듈화해서 가독성을 향상시켰습니다. +- SOLID 원칙에 기반하여, 메서드와 클래스를 분리했습니다. diff --git a/assets/.DS_Store b/assets/.DS_Store index 529a594e..179b23dd 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/assets/icon/btn_visiblity_on.png b/assets/icon/btn_visibility_on.png similarity index 100% rename from assets/icon/btn_visiblity_on.png rename to assets/icon/btn_visibility_on.png diff --git a/index.html b/index.html index 8067dd5d..f2c8721f 100644 --- a/index.html +++ b/index.html @@ -37,11 +37,12 @@ - - - - - + + + + + +
diff --git a/pages/login.html b/pages/login.html index 9f667dd4..30cc61f5 100644 --- a/pages/login.html +++ b/pages/login.html @@ -3,11 +3,12 @@ 로그인 - 판다마켓 - - - - - + + + + + +
diff --git a/pages/signup.html b/pages/signup.html index 7a789621..55daec9c 100644 --- a/pages/signup.html +++ b/pages/signup.html @@ -3,11 +3,12 @@ 회원가입 - 판다마켓 - - - - - + + + + + +
diff --git a/src/js/components/FormValidator.js b/src/js/components/FormValidator.js new file mode 100644 index 00000000..9d7cf235 --- /dev/null +++ b/src/js/components/FormValidator.js @@ -0,0 +1,72 @@ +// src/js/components/FormValidator.js +import { InputFieldHandler } from './InputFieldHandler.js'; +import { validateEmail } from '../validators/emailValidator.js'; +import { validatePassword } from '../validators/passwordValidator.js'; + +/** + * FormValidator 클래스 + * - 로그인/회원가입 폼의 두 개 필드를 관리 + * - 버튼 활성 토글 + * - 제출 후 '/items'로 이동 + */ +export class FormValidator { + /** + * 필드 초기화 및 의존객체인 필드 핸들러를 생성합니다. + * @param {HTMLFormElement} formEl - 폼 요소 + */ + constructor(formEl) { + this.form = formEl; + this.submitBtn = this.form.querySelector('button[type=submit]'); + + // 각 InputFieldHander 생성 + this.emailHandler = new InputFieldHandler( + this.form.querySelector('#email'), + validateEmail, + 'form-field__input--error', + 'form-field__error-message' + ); + + this.passwordHandler = new InputFieldHandler( + this.form.querySelector('#password'), + validatePassword, + 'form-field__input--error', + 'form-field__error-message' + ); + + this._attachEvents(); + this._updateButtonState(); + } + + /** 이벤트 연결: + * input -> 버튼토글 + * submit -> 이동 + */ + _attachEvents() { + const inputs = [this.emailHandler.inputEl, this.passwordHandler.inputEl]; + + inputs.forEach((el) => { + el.addEventListener('input', () => this._updateButtonState()); + }); + + this.form.addEventListener('submit', (e) => this._handleSubmit(e)); + } + + /** 버튼 활성/비활성 상태 업데이트 */ + _updateButtonState() { + const validEmail = this.emailHandler.validate(); + const validPwd = this.passwordHandler.validate(); + this.submitBtn.disabled = !(validEmail && validPwd); + } + + /** + * 폼 제출 핸들러 + * @param {SubmitEvent} event + */ + _handleSubmit(event) { + event.preventDefault(); + // 최종 검사 + if (!this.submitBtn.disabled) { + window.location.href = '/items'; + } + } +} diff --git a/src/js/components/InputFieldHandler.js b/src/js/components/InputFieldHandler.js new file mode 100644 index 00000000..fae40362 --- /dev/null +++ b/src/js/components/InputFieldHandler.js @@ -0,0 +1,68 @@ +// src/js/components/InputFieldHandler.js + +/** + * 입력 필드를 관리합니다. + * - focusout 시 유효성 검사 + * - 에러 클래스 토글 + * - 에러 메시지 렌더링 + */ +export class InputFieldHandler { + /** + * @param {HTMLInputElement} inputEl 입력 요소 + * @param {function(string): {valid: boolean, message: string}} validateFn 유효성 검사 함수 + * ex) validateEmail, validatePassword + * @param {string} errorClass 에러 시 추가할 CSS 클래스 + * @param {string} errorMsgClass 에러 메시지 요소에 붙일 CSS 클래스 + */ + constructor(inputEl, validateFn, errorClass, errorMsgClass) { + this.inputEl = inputEl; + this.validateFn = validateFn; + this.errorClass = errorClass; + this.errorMsgClass = errorMsgClass; + this.formField = this.inputEl.closest('.form-field'); //가장 가까운 Form-field를 찾음 + + this.inputEl.addEventListener('focusout', () => this.validate()); + } + + /** + * 입력값 유효성을 검사합니다. + * @returns {boolean} 유효하면 true + */ + validate() { + const value = this.inputEl.value.trim(); + const { valid, message } = this.validateFn(value); + + if (!valid) { + this._showError(message); + return false; + } + + this._clearError(); + return true; + } + + /** 에러 스타일 및 메시지 표시 */ + _showError(message) { + //에러 클래스 추가 + this.inputEl.classList.add(this.errorClass); + //에러 엘리멘트 없다면 생성 + let msgEl = this.formField.querySelector(`.${this.errorMsgClass}`); + if (!msgEl) { + msgEl = document.createElement('p'); + msgEl.className = this.errorMsgClass; + this.formField.appendChild(msgEl); + } + //텍스트 갱신 + msgEl.textContent = message; + } + + /** 에러 스타일 및 메시지 제거 */ + _clearError() { + //css 제거 + this.inputEl.classList.remove(this.errorClass); + //요소 제거 + const msgEl = this.formField.querySelector(`.${this.errorMsgClass}`); + //에러 요소가 존재한다면 제거합니다. + if (msgEl) msgEl.remove(); + } +} diff --git a/src/js/components/PasswordToggle.js b/src/js/components/PasswordToggle.js new file mode 100644 index 00000000..7737c7a0 --- /dev/null +++ b/src/js/components/PasswordToggle.js @@ -0,0 +1,49 @@ +/** + * PasswordToggle + * - 버튼 클릭으로 연관된 비밀번호 input의 type을 토글하고 + * 버튼의 아이콘 클래스를 온/오프 상태로 전환합니다. + */ +export class PasswordToggle { + /** + * @param {HTMLButtonElement} toggleButton - 눈 모양 버튼 요소 + */ + constructor(toggleButton) { + this.button = toggleButton; + // 토글 버튼 바로 위에 있는 input 요소를 찾습니다. + this.input = this.button + .closest('.form-field__group') + .querySelector('input[type="password"], input[type="text"]'); + this._attachEvent(); + //console.log('이벤트 적용'); + } + //이벤트를 적용함 + _attachEvent() { + this.button.addEventListener('click', () => this._toggleVisibility()); + } + //비밀번호와 아이콘을 토글합니다. + _toggleVisibility() { + //console.log('토글 클릭!', this.input.type); + if (this._isPasswordHidden()) { + this._showPassword(); + } else { + this._hidePassword(); + } + } + + /** 현재 입력 타입이 'password'인지 확인 */ + _isPasswordHidden() { + return this.input.type === 'password'; + } + + /** 비밀번호를 표시하고, 버튼 아이콘을 변경 */ + _showPassword() { + this.input.type = 'text'; + this.button.classList.add('form-field__toggle-password--visible'); + } + + /** 비밀번호를 가리고, 버튼 아이콘을 원래대로 */ + _hidePassword() { + this.input.type = 'password'; + this.button.classList.remove('form-field__toggle-password--visible'); + } +} diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 00000000..a3cb6f59 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,17 @@ +//main.js + +import { FormValidator } from '/src/js/components/FormValidator.js'; +import { PasswordToggle } from '/src/js/components/PasswordToggle.js'; + +document.addEventListener('DOMContentLoaded', () => { + // FormValidator 초기화 + document.querySelectorAll('.login__form, .signup__form').forEach((form) => { + new FormValidator(form); + }); + + // PasswordToggle 초기화 + document.querySelectorAll('.form-field__toggle-password').forEach((btn) => { + new PasswordToggle(btn); + //console.log(btn); + }); +}); diff --git a/src/js/utils/redirector.js b/src/js/utils/redirector.js new file mode 100644 index 00000000..1a3dc9ef --- /dev/null +++ b/src/js/utils/redirector.js @@ -0,0 +1,9 @@ +// src/js/utils/Redirector.js + +/** + * 지정한 경로로 페이지를 이동합니다. + * @param {string} path + */ +export function redirectTo(path) { + window.location.href = path; +} diff --git a/src/js/validators/emailValidator.js b/src/js/validators/emailValidator.js new file mode 100644 index 00000000..7a9630eb --- /dev/null +++ b/src/js/validators/emailValidator.js @@ -0,0 +1,13 @@ +//이메일 유효성을 검증하는 모듈입니다. +//adress@domain.domain2 형태 검증 +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export function validateEmail(value) { + if (!value) { + return { valid: false, message: '이메일을 입력해주세요.' }; + } + if (!EMAIL_REGEX.test(value)) { + return { valid: false, message: '잘못된 이메일 형식입니다.' }; + } + return { valid: true, message: '' }; +} diff --git a/src/js/validators/passwordValidator.js b/src/js/validators/passwordValidator.js new file mode 100644 index 00000000..2ed43984 --- /dev/null +++ b/src/js/validators/passwordValidator.js @@ -0,0 +1,10 @@ +//비밀번호 유효성을 검증하는 파일입니다. +export function validatePassword(value) { + if (!value) { + return { valid: false, message: '비밀번호를 입력해주세요.' }; + } + if (value.length < 8) { + return { valid: false, message: '비밀번호를 8자 이상 입력해주세요.' }; + } + return { valid: true, message: '' }; +} diff --git a/styles/font.css b/src/styles/base/font.css similarity index 100% rename from styles/font.css rename to src/styles/base/font.css diff --git a/styles/reset.css b/src/styles/base/reset.css similarity index 100% rename from styles/reset.css rename to src/styles/base/reset.css diff --git a/styles/variables.css b/src/styles/base/variables.css similarity index 96% rename from styles/variables.css rename to src/styles/base/variables.css index cf06240b..ad7c3113 100644 --- a/styles/variables.css +++ b/src/styles/base/variables.css @@ -31,6 +31,9 @@ --color-neutral-800: #1f2937; --color-neutral-900: #111827; + /* Color- Error */ + --color-error: red; + /* Spacing */ --spacing-xs: 1rem; --spacing-sm: 1.125rem; diff --git a/styles/common.css b/src/styles/common.css similarity index 84% rename from styles/common.css rename to src/styles/common.css index ea628371..c6bec3a3 100644 --- a/styles/common.css +++ b/src/styles/common.css @@ -29,7 +29,12 @@ body { padding: var(--spacing-xs) clamp(71px, 15vw, 124px); } -@media (width <= 767px) { +.button:disabled { + background-color: #9ca3af; + cursor: not-allowed; +} + +@media (max-width: 767px) { .button { font-size: 1.125rem; padding: 0.75rem 71px; diff --git a/styles/pages/home.css b/src/styles/pages/home.css similarity index 64% rename from styles/pages/home.css rename to src/styles/pages/home.css index d0808747..fd33baba 100644 --- a/styles/pages/home.css +++ b/src/styles/pages/home.css @@ -17,11 +17,16 @@ display: flex; justify-content: space-between; align-items: center; - padding: 9px var(--container-padding); - max-width: var(----container-max-width); + padding: 9px var(--spacing-xs); + max-width: var(--container-max-width); margin: 0 auto; } +.header__logo-img { + max-width: none; + height: auto; +} + .header__login-link { background-color: var(--color-primary-100); color: white; @@ -37,7 +42,7 @@ main { width: 100%; - margin: 70px auto 0; + margin: 70px auto 0; /* GNB의 높이만큼 제거 */ } /* hero-section */ @@ -48,7 +53,7 @@ main { width: 100%; height: 540px; display: flex; - align-items: end; + align-items: center; justify-content: center; flex: 1; } @@ -56,21 +61,32 @@ main { .hero__container { max-width: var(----container-max-width); width: 100%; - padding: 0 var(--container-padding); + height: 100%; + padding: 0; display: flex; + flex-direction: column; align-items: center; - justify-content: center; + justify-content: space-between; } .hero__contents { - padding-bottom: 60px; + padding-top: 60px; + display: flex; + flex-direction: column; + gap: var(--spacing-sm); } .hero__title { color: var(--secondary-700); - font-size: var(--font-title); font-weight: 700; letter-spacing: 140%; + text-align: center; + font-size: var(--font-title-sm); + margin: 0; +} + +.hero__title br { + display: block; } .hero__items-link { @@ -81,12 +97,11 @@ main { .hero__img { max-width: 746px; width: 100%; - height: auto; } /* feature-section */ .feature { - padding: 138px 0; + padding: var(--spacing-sm); white-space: nowrap; } @@ -95,19 +110,13 @@ main { display: flex; justify-content: center; align-items: center; - gap: 50px; - background-color: var(--color-secondary-50); - border-radius: 12px; - max-width: 988px; - width: 100%; -} - -.feature--reverse .feature__container { - flex-direction: row-reverse; + gap: var(--spacing-xs); + background: none; + flex-direction: column; } .feature__img { - max-width: 579px; + max-width: var(--mobile-max-width); width: 100%; height: auto; } @@ -119,6 +128,7 @@ main { } .feature--reverse .feature__contents { + margin-right: 0; margin-left: auto; text-align: right; } @@ -130,15 +140,15 @@ main { } .feature__title { - font-size: var(--font-title); - color: var(--color-neutral-700); - font-weight: 700; - letter-spacing: 2%; - line-height: 140%; + font-size: var(--font-title-xs); +} + +.feature__title br { + display: none; } .feature__content { - font-size: var(--font-content); + font-size: var(--font-content-sm); color: var(--color-neutral-700); font-weight: 500; } @@ -159,21 +169,28 @@ main { .cta__container { max-width: 1920px; width: 100%; - padding: 0 var(--container-padding); + height: 100%; display: flex; + flex-direction: column; align-items: center; - justify-content: center; + justify-content: space-between; + padding: 0; } .cta__title { color: var(--color-neutral-700); - font-size: var(--font-title); + font-size: var(--font-title-sm); font-weight: 700; letter-spacing: 140%; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; } .cta__img { - max-width: 746px; + max-width: var(--tablet-max-width); width: 100%; height: auto; } @@ -194,12 +211,15 @@ main { align-items: center; max-width: var(----container-max-width); width: 100%; - padding: 32px var(--container-padding); + padding: 32px; margin: 0 auto; + position: relative; } .footer__copyright { - color: var(--color-neutral-400); + color: var(--color-neutral-200); + position: absolute; + top: 80px; } .footer__links { @@ -222,9 +242,8 @@ main { gap: 12px; } -/* Tablet: 768px ~ 1199px ------------------- */ -@media (width <= 1199px) { - /* header */ +/* Tablet 768px ~ 1199px */ +@media (min-width: 768px) { .header__container { padding: 9px var(--spacing-md); } @@ -244,7 +263,6 @@ main { .hero__contents { padding-top: 84px; - display: flex; flex-direction: column; align-items: center; justify-content: center; @@ -253,7 +271,8 @@ main { } .hero__title { - margin: 0; + text-align: left; + font-size: var(--font-title); } .hero__title br { @@ -266,7 +285,6 @@ main { height: auto; } - /* feature */ .feature { padding: var(--spacing-lg); } @@ -274,25 +292,9 @@ main { .feature__container { flex-direction: column; background: none; + gap: 50px; } - .feature--reverse .feature__container { - flex-direction: column; - } - - .feature--reverse .feature__contents { - margin-right: 0; - } - - .feature__title br { - display: none; - } - - .feature__img { - max-width: var(--tablet-max-width); - } - - /* cta */ .cta { height: 927px; } @@ -319,68 +321,102 @@ main { height: auto; } - /* footer */ .footer__container { + position: static; padding: 32px var(--container-padding-tablet); } + + .footer__copyright { + position: static; + top: auto; + } } -/* Mobile: 375px ~ 767px ------------ */ -@media (width <= 767px) { - /* header */ +/* Desktop and up: 1200px and above ------------------- */ +@media (min-width: 1200px) { .header__container { - padding: 9px var(--spacing-xs); + padding: 9px var(--container-padding); } - /* hero */ .hero { + position: relative; + background-color: var(--color-secondary-200); + width: 100%; height: 540px; + display: flex; + align-items: end; + justify-content: center; + flex: 1; + } + + .hero__container { + max-width: var(--container-max-width); + padding: 0 var(--container-padding); + flex-direction: row; + align-items: end; + justify-content: space-between; + } + + .hero__contents { + padding-top: 0; + justify-content: center; + padding-bottom: 100px; } .hero__title { - font-size: var(--font-title-sm); - text-align: center; + font-size: var(--font-title); } .hero__title br { - display: block; + display: initial; } - /* feature */ - .feature { - padding: var(--spacing-sm); + .feature__container { + display: flex; + flex-direction: row; + background-color: var(--color-secondary-50); + gap: 50px; } - .feature__container { - gap: 16px; + .feature--reverse .feature__container { + flex-direction: row-reverse; } .feature__title { - font-size: var(--font-title-xs); + font-weight: 700; + font-size: var(--font-title); } - .feature__content { - font-size: var(--font-content-sm); + .feature__title br { + display: initial; + } + + .feature__contents { + font-weight: 500; + font-size: var(--font-content); } - /* cta */ .cta { height: 540px; } - .cta__title { - font-size: var(--font-title-sm); + .cta__container { + max-width: var(--container-max-width); + flex-direction: row; + align-items: end; + justify-content: center; } - /* footer */ + .cta__title { + max-height: 400px; + } - .footer__container { - position: relative; - padding: 32px; + .cta__img { + max-width: 746px; } - .footer__copyright { - position: absolute; - top: 80px; + .footer__container { + position: static; + padding: 32px var(--container-padding); } } diff --git a/styles/pages/login.css b/src/styles/pages/login.css similarity index 77% rename from styles/pages/login.css rename to src/styles/pages/login.css index 8b0ca386..f81f30e5 100644 --- a/styles/pages/login.css +++ b/src/styles/pages/login.css @@ -4,8 +4,8 @@ } .login__container { - margin: var(--container-top-margin) 1rem 0; - max-width: var(--auth-container-max-width); + margin: var(--container-top-margin-sm) 1rem 0; + max-width: var(--auth-container-max-width-sm); width: 100%; display: flex; flex-direction: column; @@ -39,6 +39,7 @@ .form-field__input { width: 100%; height: 56px; + border: 1px solid var(--color-neutral-200); border-radius: 12px; background-color: var(--color-neutral-100); color: var(--color-neutral-800); @@ -46,6 +47,10 @@ padding-right: var(--spacing-lg); } +.form-field__input--error { + border-color: var(--color-error); +} + .form-field__group { position: relative; width: 100%; @@ -63,6 +68,16 @@ color: transparent; /* 텍스트 없는 버튼 */ } +.form-field__toggle-password--visible { + background: url('/assets/icon/btn_visibility_on.png') no-repeat center; +} + +.form-field__error-message { + color: var(--color-error); + font-size: 0.875rem; + margin-top: 0.25rem; +} + /* 소셜 로그인 클래스 */ .social-login { @@ -78,7 +93,7 @@ .social-login__label { color: var(--color-neutral-800); - size: 1rem; + font-size: 1rem; font-weight: 500; } @@ -107,9 +122,9 @@ text-decoration: underline; } -@media (width <= 767px) { +@media (min-width: 768px) { .login__container { - margin-top: var(--container-top-margin-sm); - max-width: var(--auth-container-max-width-sm); + margin-top: var(--container-top-margin); + max-width: var(--auth-container-max-width); } } diff --git a/styles/pages/signup.css b/src/styles/pages/signup.css similarity index 77% rename from styles/pages/signup.css rename to src/styles/pages/signup.css index 7551fc75..e4133fa5 100644 --- a/styles/pages/signup.css +++ b/src/styles/pages/signup.css @@ -4,8 +4,8 @@ } .signup__container { - margin: var(--container-top-margin) 1rem 0; - max-width: var(--auth-container-max-width); + margin: var(--container-top-margin-sm) 1rem 0; + max-width: var(--auth-container-max-width-sm); width: 100%; display: flex; flex-direction: column; @@ -39,6 +39,7 @@ .form-field__input { width: 100%; height: 56px; + border: 1px solid var(--color-neutral-200); border-radius: 12px; background-color: var(--color-neutral-100); color: var(--color-neutral-800); @@ -46,6 +47,10 @@ padding-right: var(--spacing-lg); } +.form-field__input--error { + border-color: var(--color-error); +} + .form-field__group { position: relative; width: 100%; @@ -63,6 +68,16 @@ color: transparent; /* 텍스트 없는 버튼 */ } +.form-field__toggle-password--visible { + background: url('/assets/icon/btn_visibility_on.png') no-repeat center; +} + +.form-field__error-message { + color: var(--color-error); + font-size: 0.875rem; + margin-top: 0.25rem; +} + /* 소셜 로그인 클래스 */ .social-login { @@ -78,7 +93,7 @@ .social-login__label { color: var(--color-neutral-800); - size: 1rem; + font-size: 1rem; font-weight: 500; } @@ -107,9 +122,9 @@ text-decoration: underline; } -@media (width <= 767px) { +@media (min-width: 768px) { .signup__container { - margin-top: var(--container-top-margin-sm); - max-width: var(--auth-container-max-width-sm); + margin-top: var(--container-top-margin); + max-width: var(--auth-container-max-width); } }