Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 29 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

### 기본 요구사항

Expand All @@ -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)
Expand All @@ -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)

## 멘토에게

-
- `<script type="module" src="./scripts/login.js"></script>`과 같이 `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`에서 두 스크립트 파일을 불러오는 점에 개선점이 있을지 궁금합니다.
Binary file added assets/screenshot/login-page-desktop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/screenshot/signup-page-desktop.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 7 additions & 7 deletions login.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@
<a href="/">
<img src="assets/images/logo/panda-market-logo.png" alt="로고 이미지" id="logo" />
</a>
<form class="grid-center gap-24">
<div class="input-field grid gap-16">
<form class="form-login grid-center gap-24">
<div class="input-field grid">
<label for="email" class="text-2lg text-bold">이메일</label>
<div class="input-wrapper flex-left radius-12">
<div class="input-wrapper flex-left radius-12" id="input-wrapper-email">
<input id="email" type="email" value="" placeholder="이메일을 입력해주세요" />
</div>
</div>
<div class="input-field grid gap-16">
<div class="input-field grid">
<label for="pwd" class="text-2lg text-bold">비밀번호</label>
<div class="input-wrapper flex-sides radius-12">
<div class="input-wrapper flex-sides radius-12" id="input-wrapper-pwd">
<input id="pwd" type="password" value="" placeholder="비밀번호를 입력해주세요" />
<button type="button" alt="비밀번호 입력 보기" id="visibility" />
<button type="button" aria-label ="비밀번호 입력 보기" id="visibility" />
</div>
</div>
<button type="submit" id="btn-submit-login" disabled>로그인</button>
Expand All @@ -55,6 +55,6 @@
</p>
</form>
</main>
<script src="./scripts/login.js"></script>
<script type="module" src="./scripts/login.js"></script>
</body>
</html>
103 changes: 103 additions & 0 deletions scripts/common/members.js
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

비밀번호 숨기는 로직을 하나의 함수로 처리 하셨군요! 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

html dataset을 이용해서 input id를 가져오는 방법도 있겠네요~! (참고만 해주세요!)

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;
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

previousElementSibling, parentsElement 등을 사용해서 dom 접근하는 코드가 많이 보이는데 dataset, id 등을 좀 더 활용해 보셔도 좋을 거 같습니다 :)

기존에 사용하고 계시는 dom 접근법은 간단하게 사용하면 괜찮지만, 갈수록 유지 보수가 어려워집니다!
HTML 구조에 따라 코드가 쉽게 깨지는 문제가 있고 html 구조를 같이 봐야 하기 때문에 코드를 이해하기가 힘들어지죠 😢

우선 참고만 해주세요~!

const inputField = input.parentElement?.parentElement;
Copy link
Collaborator

Choose a reason for hiding this comment

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

이런 부분들은 가독성을 위해 함수로 추상화 하시면 좋습니다~!
handleErrorMessage 같은 함수 내에 해당 로직을 정리해놓으면, 함수 이름만 보고 여기서 에러 메세지를 다루는구나~ 하고 이해할 수 있지만 지금의 경우는 한 줄 한 줄 읽어야하죠 :)

나중에 에러 메세지 관련된 부분을 이해해야 하거나 수정해야 할 때는 해당 함수만 찾아가면 되구요!

const errorParagraph = document.getElementsByClassName(className)[0];

if (result) errorParagraph && inputField?.removeChild(errorParagraph);
Copy link
Collaborator

Choose a reason for hiding this comment

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

약간은 복잡한 느낌이 있는데,

<span id="email-error"></span>

미리 html에 정의하고 display, content만 수정할 수도 있을 거 같네요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

참고만 해주세요!

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;
Copy link
Collaborator

Choose a reason for hiding this comment

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

위와 마찬가지 입니다 :) 추상화로 가독성을 높여주세요!

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");
}
47 changes: 17 additions & 30 deletions scripts/login.js
Original file line number Diff line number Diff line change
@@ -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";
});
51 changes: 17 additions & 34 deletions scripts/signup.js
Original file line number Diff line number Diff line change
@@ -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";
});
Loading
Loading