Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
47b9ca1
fix(login.css, signup.css): css 문법 오류 수정
DreamPaste Apr 30, 2025
1997c7d
fix: 미디어쿼리 문법을 prefix 표기 방식으로 변경
DreamPaste Apr 30, 2025
b8615bd
fix(home.css): postcss-mobile-first 플러그인 테스트를 위해 mobile-first 리팩토링 중지
DreamPaste Apr 30, 2025
65b2bab
refactor(home.css): mobile-first 로 리팩토링
DreamPaste Apr 30, 2025
f8755d7
refactor(login.css, signup.css): mobile-first로 리팩토링
DreamPaste Apr 30, 2025
46e9486
docs(readme.md): 미션 4 요구사항 추가
DreamPaste May 2, 2025
285c58d
refactor: 디렉토리 구조 변경
DreamPaste May 2, 2025
54b9281
feat(validateEmail.js): 이메일 유효성 검증 로직 추가
DreamPaste May 2, 2025
e2011da
feat(passwordValidator.js): 비밀번호 유효성 검증 로직 추가
DreamPaste May 2, 2025
b9424e5
feat(FormValidatorClass.js): 유효성을 관리하는 클래스 추가
DreamPaste May 2, 2025
e246083
fix(FormValidatorClass): 메서드 분리
DreamPaste May 2, 2025
3015029
fix(common.css): 활성화 상태가 제어되지 않는 문제 수정
DreamPaste May 2, 2025
2026936
feat(FormValidator): 로그인 버튼을 누르면 items로 이동하는 기능 추가
DreamPaste May 2, 2025
637381f
docs(readme): 문서 수정
DreamPaste May 2, 2025
cd30e11
docs(readme.md): 프로젝트 최신화 반영
DreamPaste May 5, 2025
e481954
refactor(FormValidator.js): 클래스와 메서드를 분리해서 가독성을 높였습니다.
DreamPaste May 7, 2025
d775268
fix(InputFieldHandler.js): 에러 메시지 요소가 일관되지 않은 위치로 삽입되던 문제를 수정했습니다.
DreamPaste May 7, 2025
5f7207b
feat(PasswordToggle.js): 비밀번호를 보이거나 안보이도록 토글할 수 있도록 변경했습니다.
DreamPaste May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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-]+)*$",
{
Expand Down
108 changes: 47 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# 스프린트 미션 3
# 스프린트 미션 4

## 배포 링크

> https://gentle-lamington-bbd035.netlify.app/

## 1. 요구사항
## 요구사항

### 1-1. 공통 요구사항
### 공통 요구사항

- [x] “판다마켓” 클릭 시 루트 페이지(‘/’)로 이동한다.
- [x] 클릭으로 기능이 동작해야 하는 경우, 사용자가 클릭할 수 있는 요소임을 알 수 있도록 `cursor: pointer`를 설정한다.
Expand Down Expand Up @@ -44,8 +44,6 @@
#### 메타 태그

- [x] 페이스북, 카카오톡, 디스코드, 트위터 등 SNS에서 Linkbrary 랜딩 페이지(“/”) 공유 시 좌측 예시와 같은 미리보기를 볼 수 있도록 랜딩 페이지 메타 태그를 설정한다.
> 미리보기에서 제목은 “판다 마켓”, 설명은 “일상의 모든 물건을 거래해보세요”로 설정합니다.
> 주소와 이미지는 자유롭게 설정하세요.

### 로그인 및 회원가입 페이지 ('/login, /signup')

Expand All @@ -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 영역으로 나뉘어 있습니다.
- `<main>` 의 각 `<section>` 안에는 hero, feature, cta 로 구성되어 있습니다.
- `.hero`, `.cta` 클래스는 하위 박스인 `.container` 에서, 여백과 콘텐츠 관련 요구사항을 만족했습니다.
- 링크로 연결되는 항목들은 `<a>`태그로 감싸서, 클릭시 특정 페이지들로 이동할 수 있습니다.
- 폰트, 이미지, 몇몇 여백들은 `vw`를 활용해서 동적으로 크기가 조절됩니다.
- 새 창으로 열리는 페이지들은 `target="_blank" rel="noopener"`속성을 사용하여, 보안상 취약점이 발생하고 퍼포먼스가 저하되는 문제를 해결했습니다.
- 상단 GNB는 고정되도록 설정하였습니다.
- OG와 twitter의 메타 정보를 등록했습니다.

### 회원가입 및 로그인 페이지

- 주요 내용은 `form`필드 내에 구성하였습니다.
- 자주 사용하는 항목은 common.css에서 재사용합니다.
- `index.html`

## 질문
## 구현 사항

1. 현제 스펙 웹사이트의 디렉토리 구조 관리 방법이 궁금합니다.
- Javascript를 모듈화해서 가독성을 향상시켰습니다.
- SOLID 원칙에 기반하여, 메서드와 클래스를 분리했습니다.
Binary file modified assets/.DS_Store
Binary file not shown.
File renamed without changes
11 changes: 6 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@
<meta property="og:title" content="판다 마켓" />
<meta property="og:description" content="일상의 모든 물건을 거래해보세요" />

<link rel="stylesheet" href="styles/reset.css" />
<link rel="stylesheet" href="styles/font.css" />
<link rel="stylesheet" href="styles/variables.css" />
<link rel="stylesheet" href="styles/common.css" />
<link rel="stylesheet" href="styles/pages/home.css" />
<link rel="stylesheet" href="/src/styles/base/reset.css" />
<link rel="stylesheet" href="/src/styles/base/font.css" />
<link rel="stylesheet" href="/src/styles/base/variables.css" />
<link rel="stylesheet" href="/src/styles/common.css" />
<link rel="stylesheet" href="/src/styles/pages/home.css" />
<script type="module" src="/src/js/main.js"></script>
</head>
<body>
<header class="header">
Expand Down
11 changes: 6 additions & 5 deletions pages/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>로그인 - 판다마켓</title>
<link rel="stylesheet" href="/styles/reset.css" />
<link rel="stylesheet" href="styles/font.css" />
<link rel="stylesheet" href="/styles/variables.css" />
<link rel="stylesheet" href="/styles/common.css" />
<link rel="stylesheet" href="/styles/pages/login.css" />
<link rel="stylesheet" href="/src/styles/base/reset.css" />
<link rel="stylesheet" href="/src/styles/base/font.css" />
<link rel="stylesheet" href="/src/styles/base/variables.css" />
<link rel="stylesheet" href="/src/styles/common.css" />
<link rel="stylesheet" href="/src/styles/pages/login.css" />
<script type="module" src="/src/js/main.js"></script>
</head>
<body>
<main class="login">
Expand Down
11 changes: 6 additions & 5 deletions pages/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>회원가입 - 판다마켓</title>
<link rel="stylesheet" href="/styles/reset.css" />
<link rel="stylesheet" href="styles/font.css" />
<link rel="stylesheet" href="/styles/variables.css" />
<link rel="stylesheet" href="/styles/common.css" />
<link rel="stylesheet" href="/styles/pages/signup.css" />
<link rel="stylesheet" href="/src/styles/base/reset.css" />
<link rel="stylesheet" href="/src/styles/base/font.css" />
<link rel="stylesheet" href="/src/styles/base/variables.css" />
<link rel="stylesheet" href="/src/styles/common.css" />
<link rel="stylesheet" href="/src/styles/pages/signup.css" />
<script type="module" src="/src/js/main.js"></script>
</head>
<body>
<main class="signup">
Expand Down
72 changes: 72 additions & 0 deletions src/js/components/FormValidator.js
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FormValidator, InputFieldHandler의 경우 클래스 계층간의 위계가 명확하지않아 유지보수에 어려움이 생길것같아요.

이왕 클래스 문법을 사용하셨으니 클래스 상속을 활용해 더 체계적이고 객체지향적인 구조로 리팩토링 해볼까요? 즉, 먼저 기본이 되는 최상위 클래스인 FormField 클래스를 만들고, 이를 상속받는 InputTextField 클래스를 구현하는 방식은 어떨까요?

  • FormField
export class FormField {
  /**
   * @param {HTMLInputElement} inputEl - Input element
   * @param {string} errorClass - CSS class for error state
   * @param {string} errorMsgClass - CSS class for error message
   */
  constructor(inputEl, errorClass, errorMsgClass) {
    if (!inputEl) {
      throw new Error('Input element is required');
    }

    this.inputEl = inputEl;
    this.errorClass = errorClass;
    this.errorMsgClass = errorMsgClass;
    this.formField = this.inputEl.closest('.form-field');
    this.isValid = false;

    this._initializeEventListeners();
  }
  ...
}
  • InputTextField
import { FormField } from './FormField.js';

/**
 * InputTextField class
 * Handles text input field validation
 * Extends FormField with specific validation logic
 */
export class InputTextField extends FormField {
  /**
   * @param {HTMLInputElement} inputEl - Input element
   * @param {function(string): {valid: boolean, message: string}} validator - Validation function
   * @param {string} errorClass - CSS class for error state
   * @param {string} errorMsgClass - CSS class for error message
   */
  constructor(inputEl, validator, errorClass, errorMsgClass) {
    super(inputEl, errorClass, errorMsgClass);
    this.validator = validator;
  }

  /**
   * Validate the input field using the provided validator
   * @returns {boolean} True if input is valid
   */
  validate() {
    const value = this.getValue();
    const { valid, message } = this.validator(value);

    if (!valid) {
      this._showError(message);
      return false;
    }

    this._clearError();
    return true;
  }
}

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'
);
Comment on lines +21 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스 문법을 사용하셨군요!
이렇게 emailHandler와 passwordHanlder를 직접적으로 생성자 메서드 안에서 써주기보다는 연관된 작업끼리는 초기화를 담당하는 함수 하나로 묶어서 관리해주면 어떨까요?

  constructor(formEl) {
    this.form = formEl;
    this.submitBtn = this.form.querySelector('button[type=submit]');

    this._initializeFieldHandlers();
    this._attachEvents();
    this._updateButtonState();
  }
  _initializeFieldHandlers() {
    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'
    );
  }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 바꿔보면 몇가지 장점이 생깁니다 :)

  • 클래스 계층 구조와 역할이 명확해집니다.
    FormField는, form 안에서 기본 필드 기능을 제공하는 추상 클래스이고, InputTextField는 FormField를 상속받아 좀 더 구체적으로 구체적인 텍스트 입력 필드에 관련된 기능을 제공하는 클래스입니다.

  • 코드 재사용 및 기능 확장이 용이해집니다. 폼 전체적으로 공통되는 기능은 FormField에서 관리하고, 해당 공통 기능을 상속받아 구체적인 필드에 특화된 기능을 관리하는것은 FormField를 상속받은 클래스에서만 관리합니다.

  • 코드를 수정할때, 클래스 계층간의 역할이 명확하다보니 공통되는 기능을 수정 시 한곳에서만 수정하면되니까 테스트도 용이해지고, 수정 및 확장에 용이해집니다.


this._attachEvents();
this._updateButtonState();
}

/** 이벤트 연결:
* input -> 버튼토글
* submit -> 이동
*/
_attachEvents() {
const inputs = [this.emailHandler.inputEl, this.passwordHandler.inputEl];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 함수의 경우에 inputs를 함수 내부에서 변수를 만들어 관리하기보다는 필요한 입력 필드들을 매개변수로 받게끔 만들면 외부에서 의존성을 주입하게되니까 훨씬 변경에 유연한 함수를 만들수있을것같아요.


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';
}
}
}
68 changes: 68 additions & 0 deletions src/js/components/InputFieldHandler.js
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스는 위에 드린 코멘트 참고해서 상속을 사용한 객체지향 친화적인 패턴으로 리팩토링해보시면 좋을것같아요! :)

Original file line number Diff line number Diff line change
@@ -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();
}
}
49 changes: 49 additions & 0 deletions src/js/components/PasswordToggle.js
Original file line number Diff line number Diff line change
@@ -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');
}
}
Loading
Loading