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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,32 @@ https://github.com/user-attachments/assets/d220c08b-83d8-4d2e-a589-b85a3a8590fc

### 구현 내용
https://github.com/user-attachments/assets/7027bb8a-4f37-4a0d-af8a-0e532979d1fb

# Sprint 3 미션
## 내용
반응형 페이지 구현
[Sprint 3 미션 PR](https://github.com/codeit-bootcamp-frontend/13-Sprint-Mission/pull/41)

### 요구사항
### 기본
- [x] 브라우저에 현재 보이는 화면의 영역(viewport) 너비를 기준으로 분기되는 반응형 디자인을 적용합니다.
- PC: 1200px 이상
- Tablet: 768px 이상 ~ 1199px 이하
- Mobile: 375px 이상 ~ 767px 이하
- 375px 미만 사이즈의 디자인은 고려하지 않습니다

### 랜딩 페이지
- [x] Tablet 사이즈로 작아질 때 “판다마켓” 로고의 왼쪽에 여백 24px, “로그인” 버튼 오른쪽 여백 24px을 유지할 수 있도록 “판다마켓” 로고와 “로그인" 버튼의 간격이 가까워집니다.
- [x] Mobile 사이즈로 작아질 때 “판다마켓” 로고의 왼쪽에 여백 16px, “로그인” 버튼 오른쪽 여백 16px을 유지할 수 있도록 “판다마켓” 로고와 “로그인" 버튼의 간격이 가까워집니다.
- [x] 화면 영역이 줄어들면 “Privacy Policy”, “FAQ”, “codeit-2024”이 있는 영역과 SNS 아이콘들이 있는 영역의 간격이 줄어듭니다.

### 로그인, 회원가입 페이지 공통
- [x] Tablet 사이즈에서 내부 디자인은 PC사이즈와 동일합니다.
- [x] Mobile 사이즈에서 좌우 여백 16px 제외하고 내부 요소들이 너비를 모두 차지합니다.
- [x] Mobile 사이즈에서 내부 요소들의 너비는 기기의 너비가 커지는 만큼 커지지만 400px을 넘지 않습니다.
### [심화]
- [ ] 페이스북, 카카오톡, 디스코드, 트위터 등 SNS에서 Linkbrary 랜딩 페이지(“/”) 공유 시 좌측 예시와 같은 미리보기를 볼 수 있도록 랜딩 페이지 메타 태그를 설정해 주세요.
- [ ] 미리보기에서 제목은 “판다 마켓”, 설명은 “일상의 모든 물건을 거래해보세요”로 설정합니다.

### 구현 내용
https://github.com/user-attachments/assets/01fe7854-b9a6-42f4-b309-c497a66520b5
6 changes: 4 additions & 2 deletions src/pages/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
<div class="content-wrapper">
<div class="form-structure">
<label for="email" class="form-label">이메일</label>
<input id="email" class="form-input" type="email" placeholder="이메일을 입력해주세요">
<input id="email" class="form-input email-input" type="email" placeholder="이메일을 입력해주세요">
<div class="form-warning"></div>
</div>
<div class="form-structure">
<label for="password" class="form-label">비밀번호</label>
<div class="input-wrapper">
<input id="password" class="form-input" type="password" placeholder="비밀번호를 입력해주세요">
<input id="password" class="form-input password-input" type="password" placeholder="비밀번호를 입력해주세요">
<img class="input-icon" src="/src/assets/icons/visibility_on_btn.svg" alt="비밀번호 보기">
</div>
<div class="form-warning"></div>
</div>
<button class="primary-button login-button" disabled>로그인</button>
<div class="social-login">
Expand Down
16 changes: 10 additions & 6 deletions src/pages/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,31 @@
<div class="content-wrapper">
<div class="form-structure">
<label for="email" class="form-label">이메일</label>
<input id="email" class="form-input" type="email" placeholder="이메일을 입력해주세요">
<input id="email" class="form-input email-input" type="email" placeholder="이메일을 입력해주세요">
<div class="form-warning"></div>
</div>
<div class="form-structure">
<label for="nickname" class="form-label">닉네임</label>
<input id="nickname" class="form-input" type="text" placeholder="닉네임을 입력해주세요">
<input id="nickname" class="form-input nickname-input" type="text" placeholder="닉네임을 입력해주세요">
<div class="form-warning"></div>
</div>
<div class="form-structure">
<label for="password" class="form-label">비밀번호</label>
<div class="input-wrapper">
<input id="password" class="form-input" type="password" placeholder="비밀번호를 입력해주세요">
<input id="password" class="form-input password-input" type="password" placeholder="비밀번호를 입력해주세요">
<img class="input-icon" src="/src/assets/icons/visibility_on_btn.svg" alt="비밀번호 보기">
</div>
<div class="form-warning"></div>
</div>
<div class="form-structure">
<label for="password" class="form-label">비밀번호 확인</label>
<label for="check-password" class="form-label">비밀번호 확인</label>
<div class="input-wrapper">
<input id="password" class="form-input" type="password" placeholder="비밀번호를 입력해주세요">
<input id="check-password" class="form-input check-password-input" type="password" placeholder="비밀번호를 입력해주세요">
<img class="input-icon" src="/src/assets/icons/visibility_on_btn.svg" alt="비밀번호 보기">
</div>
<div class="form-warning"></div>
</div>
<button class="primary-button login-button" disabled>회원가입</button>
<button class="primary-button signup-button" disabled>회원가입</button>
<div class="social-login">
<p>간편 로그인하기</p>
<ul class="icon-list">
Expand Down
155 changes: 135 additions & 20 deletions src/scripts/formUtils.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,134 @@
const NOINPUT = 0;
const WRONGINPUT = 1;
const VALIDINPUT = 2;
Comment on lines +1 to +3
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.

(의견/제안) 다음과 같이 하나의 객체에서 다룰 수도 있겠네요 !

Suggested change
const NOINPUT = 0;
const WRONGINPUT = 1;
const VALIDINPUT = 2;
const ERROR_CODE = {
email: {
NOINPUT: "이메일을 입력해주세요.",
WRONGINPUT: "잘못된 이메일 형식입니다.",
},
password: {
NOINPUT: "비밀번호를 입력해주세요.",
WRONGINPUT: "비밀번호를 8자 이상 입력해주세요.",
},
nickname: {
NOINPUT: "닉네임을 입력해주세요.",
},
checkPassword: {
WRONGINPUT: "비밀번호가 일치하지 않습니다.",
},
};

이렇게 되면 해당되는 에러 코드의 메시지를 한 곳에서 관리할 수 있습니다 😊


const visibilityConfig = {
password: {
showPassword: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! 피드백을 적용하셨군요 👍

목적에 더욱 부합된� 이름으로 변경된 것 같아서 뿌듯하네요 😊

type: "text",
src: "/src/assets/icons/visibility_off_btn.svg",
alt: "비밀번호 숨기기",
},
text: {
hidePassword: {
type: "password",
src: "/src/assets/icons/visibility_on_btn.svg",
alt: "비밀번호 보기",
},
};

export function setVisibilityToggle(inputWrapperSelector) {
const inputWrappers = document.querySelectorAll(inputWrapperSelector);
const validators = {
email: (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (email.trim() === "")
return (NOINPUT);
else if (!emailRegex.test(email))
return (WRONGINPUT);
return (VALIDINPUT);
},
password: (password) => {
if (password.length === 0)
return (NOINPUT);
else if (password.length < 8)
return (WRONGINPUT);
return (VALIDINPUT);
},
nickname: (nickname) => {
if (nickname.length === 0)
return (NOINPUT);
return (VALIDINPUT);
},
checkPassword: (password, checkPassword) => {
if (password !== checkPassword)
return (WRONGINPUT);
return (VALIDINPUT);
}
}
Comment on lines +18 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

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

(선택/심화/더 나아가서) 다음과 같이 스키마를 작성해볼 수도 있습니다 !

Suggested change
const validators = {
email: (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (email.trim() === "")
return (NOINPUT);
else if (!emailRegex.test(email))
return (WRONGINPUT);
return (VALIDINPUT);
},
password: (password) => {
if (password.length === 0)
return (NOINPUT);
else if (password.length < 8)
return (WRONGINPUT);
return (VALIDINPUT);
},
nickname: (nickname) => {
if (nickname.length === 0)
return (NOINPUT);
return (VALIDINPUT);
},
checkPassword: (password, checkPassword) => {
if (password !== checkPassword)
return (WRONGINPUT);
return (VALIDINPUT);
}
}
const signUpFormSchema = {
email: {
validate: (email) => {
if (email.trim() === "") return "NOINPUT";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return "WRONGINPUT";
return "VALIDINPUT";
},
message: {
NOINPUT: "이메일을 입력해주세요.",
WRONGINPUT: "잘못된 이메일 형식입니다.",
},
},
password: {
validate: (password) => {
if (password.trim() === "") return "NOINPUT";
if (password.length < 8) return "WRONGINPUT";
return "VALIDINPUT";
},
message: {
NOINPUT: "비밀번호를 입력해주세요.",
WRONGINPUT: "비밀번호는 8자 이상이어야 합니다.",
},
},
nickname: {
validate: (nickname) => {
if (nickname.trim() === "") return "NOINPUT";
return "VALIDINPUT";
},
message: {
NOINPUT: "닉네임을 입력해주세요.",
},
},
checkPassword: {
validate: (password, checkPassword) => {
if (password !== checkPassword) return "WRONGINPUT";
return "VALIDINPUT";
},
message: {
WRONGINPUT: "비밀번호가 일치하지 않습니다.",
},
},
};

창의적이고 도전적으로 문제를 잘 풀이하셔서 어느 정도 난이도 있는 제안도 잘 수행하실 것 같아서 조심스레 제안드려봅니다 ! 😉

Copy link
Collaborator

Choose a reason for hiding this comment

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

(팁 !)추가로 스키마를 쉽게 작성하기 위한 라이브러리도 존재합니다 !

import Joi from "joi";

const schema = Joi.object({
    email: Joi.string().email({ tlds: { allow: false } }).required().messages({
        "string.empty": "이메일을 입력해주세요.",
        "string.email": "잘못된 이메일 형식입니다.",
    }),
    password: Joi.string().min(8).required().messages({
        "string.empty": "비밀번호를 입력해주세요.",
        "string.min": "비밀번호는 8자 이상이어야 합니다.",
    }),
    nickname: Joi.string().required().messages({
        "string.empty": "닉네임을 입력해주세요.",
    }),
    checkPassword: Joi.any().valid(Joi.ref("password")).required().messages({
        "any.only": "비밀번호가 일치하지 않습니다.",
        "any.required": "비밀번호 확인을 입력해주세요.",
    }),
});

라이브러리를 활용하여 더욱 간단하게 스키마와 유효성 검사 로직을 작성하실 수 있습니다 😉😉


export function setVisibilityToggle(parent) {

inputWrappers.forEach((wrapper) => {
wrapper.addEventListener("click", (event) => {
const clickedElement = event.target;
parent.addEventListener("click", (event) => {
const target = event.target;

if (clickedElement.classList.contains("input-icon")) {
const passwordInput = wrapper.querySelector("input");
const toggleButton = clickedElement;
if (target.classList.contains("input-icon")) {
const wrapper = target.closest(".input-wrapper");
const passwordInput = wrapper.querySelector("input");
const toggleButton = target;

const nextState = passwordInput.type === "password" ? visibilityConfig["password"] : visibilityConfig["text"];
const nextState = passwordInput.type === "password" ? visibilityConfig["showPassword"] : visibilityConfig["hidePassword"];

passwordInput.type = nextState.type;
toggleButton.src = nextState.src;
toggleButton.alt = nextState.alt;
}
});
passwordInput.type = nextState.type;
toggleButton.src = nextState.src;
toggleButton.alt = nextState.alt;
}
});
}

function setWarningMessage(warningDiv, checkValid, messages, inputField) {
if (checkValid === NOINPUT) {
warningDiv.textContent = messages.noInput;
inputField.classList.add("input-error");
} else if (checkValid === WRONGINPUT) {
warningDiv.textContent = messages.wrongInput;
inputField.classList.add("input-error");
}
else {
warningDiv.textContent = "";
inputField.classList.remove("input-error");
}
}

export function registerValidationEvents(parent) {
const forms = parent.querySelectorAll(".form-structure");

for (let form of forms) {
form.addEventListener("focusout", (event) => {
const target = event.target;
const warningDiv = form.querySelector(".form-warning");
if (target.classList.contains("email-input")) {
const checkValid = validators.email(target.value);
setWarningMessage(warningDiv, checkValid, {
noInput: "이메일을 입력해주세요.",
wrongInput: "잘못된 이메일 형식입니다.",
}, target);
}
else if (target.classList.contains("password-input")) {
const checkValid = validators.password(target.value);
setWarningMessage(warningDiv, checkValid, {
noInput: "비밀번호를 입력해주세요.",
wrongInput: "비밀번호를 8자 이상 입력해주세요.",
}, target);
}
else if (target.classList.contains("nickname-input")) {
const checkValid = validators.nickname(target.value);
setWarningMessage(warningDiv, checkValid, {
noInput: "닉네임을 입력해주세요.",
}, target);
}
else if (target.classList.contains("check-password-input")) {
const password = parent.querySelector(".password-input").value;
const checkValid = validators.checkPassword(password, target.value);
setWarningMessage(warningDiv, checkValid, {
wrongInput: "비밀번호가 일치하지 않습니다.",
}, target);
}
})
}
}

export function setButtonDisable(parent, buttonClass) {
const button = parent.querySelector(buttonClass);

// 초기 상태 및 입력값 검증 함수
function validateInputs() {
const inputs = parent.querySelectorAll("input");
const allFilled = Array.from(inputs).every(input => input.value.trim() !== "");
button.disabled = !allFilled;
const forms = parent.querySelectorAll(".form-structure");

const allValid = Array.from(forms).every(form => {
const input = form.querySelector("input");
const warning = form.querySelector(".form-warning");

return (input.value !== "" && warning.textContent === "");
});
button.disabled = !allValid;
}

/*
Expand All @@ -53,4 +142,30 @@ export function setButtonDisable(parent, buttonClass) {
validateInputs();
}
});
}

parent.addEventListener("focusout", (event) => {
if (event.target.tagName === "INPUT") {
validateInputs();
}
});
}

export function handleFormSubmission(parent, buttonClass, redirectUrl) {
const button = parent.querySelector(buttonClass);

function validateInputs() {
const forms = parent.querySelectorAll(".form-structure");

return Array.from(forms).every(form => {
const input = form.querySelector("input");
const warning = form.querySelector(".form-warning");
return input.value !== "" && warning.textContent === "";
});
}

button.addEventListener("click", (event) => {
if (validateInputs()) {
location.href = redirectUrl;
}
});
}
8 changes: 5 additions & 3 deletions src/scripts/pages/login.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { setVisibilityToggle, setButtonDisable } from "/src/scripts/formUtils.js";
import { setVisibilityToggle, registerValidationEvents, setButtonDisable, handleFormSubmission } from "/src/scripts/formUtils.js";

setVisibilityToggle(".input-wrapper");

const contentWrapper = document.querySelector(".content-wrapper");
setButtonDisable(contentWrapper, ".primary-button");
setVisibilityToggle(contentWrapper);
registerValidationEvents(contentWrapper);
setButtonDisable(contentWrapper, ".login-button");
handleFormSubmission(contentWrapper, ".login-button", '/src/pages/items.html');
10 changes: 6 additions & 4 deletions src/scripts/pages/signup.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { setVisibilityToggle, setButtonDisable } from "/src/scripts/formUtils.js";

setVisibilityToggle(".input-wrapper");
import { setVisibilityToggle, registerValidationEvents, setButtonDisable, handleFormSubmission } from "/src/scripts/formUtils.js";

const contentWrapper = document.querySelector(".content-wrapper");
setButtonDisable(contentWrapper, ".primary-button");

setVisibilityToggle(contentWrapper);
registerValidationEvents(contentWrapper);
setButtonDisable(contentWrapper, ".signup-button");
handleFormSubmission(contentWrapper, ".signup-button", '/src/pages/signup.html');
17 changes: 15 additions & 2 deletions src/styles/pages/login.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ main {
.form-structure{
display: flex;
flex-direction: column;
gap: 16px;
gap: 8px;
align-items: stretch;
}

.form-label {
font-size: 18px;
font-weight: 700;
margin-bottom: 8px;
line-height: 24px;
color: #1F2937;
}
Expand All @@ -52,10 +53,22 @@ main {
width: 100%;
}

.from-input:focus {
.form-input:focus {
border: 1px solid #3692FF;
}

.input-error {
border: 1px solid #F74747;
}

.form-warning {
padding-left: 16px;
font-size: 14px;
line-height: 24px;
color: #F74747;
font-weight: 600;
}

.input-wrapper {
position: relative;
}
Expand Down
16 changes: 14 additions & 2 deletions src/styles/pages/signup.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,22 @@ main {
width: 100%;
}

.from-input:focus {
.form-input:focus {
border: 1px solid #3692FF;
}

.input-error {
border: 1px solid #F74747;
}

.form-warning {
padding-left: 16px;
font-size: 14px;
line-height: 24px;
color: #F74747;
font-weight: 600;
}

.input-wrapper {
position: relative;
}
Expand All @@ -76,7 +88,7 @@ main {
opacity: 1;
}

.login-button {
.signup-button {
padding: 16px 124px;
font-size: 20px;
font-weight: 600;
Expand Down
Loading