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
File renamed without changes
65 changes: 46 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,77 +9,103 @@
<meta property="og:title" content="판다 마켓" />
<meta property="og:description" content="일상의 모든 물건을 거래해보세요" />
<meta property="og:image" content="/images/og_img.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="판다 마켓" />
<meta name="twitter:description" content="일상의 모든 물건을 거래해보세요" />
<meta name="twitter:image" content="/images/og_img.png" />
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/static/pretendard-dynamic-subset.min.css" />
<title>판다마켓</title>
<link rel="stylesheet" href="styles/reset.css">
<link rel="stylesheet" href="styles/global.css">
<link rel="stylesheet" href="styles/variables.css">
<link rel="stylesheet" href="styles/mobile.css" media="(max-width: 767px)">
<link rel="stylesheet" href="styles/tablet.css" media="(min-width: 768px) and (max-width: 1199px)">
<link rel="stylesheet" href="styles/desktop.css" media="(min-width: 1200px)">
<link rel="stylesheet" href="styles/index.css">
</head>

<body>
<!-- header start -->
<header>
<!-- nav start -->
<nav>
<a href="/" aria-label="홈으로 이동">
<!-- <a href="/" aria-label="홈으로 이동">
<img src="/images/Property1-2=sm.png"
srcset="/images/Property1-2=Typo.png 743w, /images/Property1-2=sm.png 744w"
sizes="(max-width: 743px) 100vw, 744px" alt="판다마켓 로고" /> -->
<picture>
<source srcset="/images/Property1-2=Typo.png" media="(max-width: 743px)" />
<img src="/images/Property 1-2=sm.png" alt="판다마켓 로고" />
<img src="/images/Property1-2=sm.png" alt="판다마켓 로고" />
</picture>
</a>
<a href="/login.html" class="btn-small-40" aria-label="로그인으로 이동">로그인</a>
</nav>
<!-- nav end -->
</header>
<!-- header end -->

<!-- main start -->
<main>
<section id="hero" class="banner">
<!-- hero section start -->
<section class="banner">
<div class="wrapper">
<h1>일상의 모든 물건을<br>거래해 보세요</h1>
<a href="items.html"><button class="btn" aria-label="아이템으로 이동">구경하러 가기</button></a>
<div class="hero-title">
<h1>일상의 모든 물건을 <span class="heading-line-break">거래해 보세요</span></h1>
<a href="items.html">
<button class="btn-large" aria-label="아이템으로 이동">구경하러 가기</button>
</a>
</div>
<img src="/images/Img_home_top.png" alt="" aria-hidden="true" />
</div>
</section>
<!-- hero section end -->

<!-- features section start -->
<section class="features wrapper wrapper--narrow">
<!-- feature div start -->
<div class="feature">
<img src="images/Img_home_01.png" alt="아이템 콘텐츠 이미지" />
<div class="feature-content">
<p class="feature-tag">Hot item</p>
<h1>인기 상품을<br />확인해 보세요</h1>
<h1>인기 상품을<span class="heading-line-break"> 확인해 보세요</span></h1>
<p class="feature-description">
가장 HOT한 중고거래 물품을<br />판다마켓에서 확인해 보세요
</p>
</div>
</div>
<!-- feature div end -->
<!-- feature div start -->
<div class="feature">
<img src="images/Img_home_02.png" alt="아이템 콘텐츠 이미지">
<div class="feature-content">
<p class="feature-tag">Search</p>
<h1>구매를 원하는<br>상품을 검색하세요</h1>
<h1>구매를 원하는<span class="heading-line-break"> 상품을 검색하세요</span></h1>
<p class="feature-description">구매하고 싶은 물품은 검색해서<br>쉽게 찾아보세요</p>
</div>
<img src="images/Img_home_02.png" alt="아이템 콘텐츠 이미지">
</div>
<!-- feature div end -->
<!-- feature div start -->
<div class="feature">
<img src="images/Img_home_03.png" alt="아이템 콘텐츠 이미지" />
<div class="feature-content">
<p class="feature-tag">Register</p>
<h1 class="font-fat">판매를 원하는<br>상품을 등록하세요</h1>
<h1 class="font-fat">판매를 원하는<span class="heading-line-break"> 상품을 등록하세요</span></h1>
<p class="feature-description">
어떤 물건이든 판매하고 싶은 상품을<br>쉽게 등록하세요</p>
</div>
</div>
<!-- feature div end -->
</section>
<section id="closing-banner" class="banner">
<!-- features section end -->

<!-- closing banner start -->
<section class="banner">
<div class="wrapper">
<h1>믿을 수 있는<br>판다마켓 중고 거래</h1>
<h1>믿을 수 있는<span class="heading-line-break"> 판다마켓 중고 거래</span></h1>
<img src="/images/Img_home_bottom.png" alt="" aria-hidden="true" />
</div>
</section>
<!-- closing banner end -->
</main>
<!-- main end -->

<!-- footer start -->
<footer>
<div class="footer-copyright">© codeit - 2024</div>
<div class="footer-copyright">© codeit - 2024</div>
<div id="footer--menu">
<a href="/privacy.html" aria-label="Privacy Policy로 이동">Privacy Policy</a>
<a href="/faq.html" aria-label="FAQ로 이동">FAQ</a>
Expand All @@ -91,6 +117,7 @@ <h1>믿을 수 있는<br>판다마켓 중고 거래</h1>
<a href="https://www.instagram.com/" aria-label="인스타그램으로 이동"><img src="/images/ic_instagram.svg"></a>
</div>
</footer>
<!-- footer end -->
</body>

</html>
24 changes: 14 additions & 10 deletions login.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,52 +11,56 @@
<link rel="stylesheet" href="styles/global.css" />
<link rel="stylesheet" href="styles/variables.css" />
<link rel="stylesheet" href="styles/auth.css" />
<link rel="stylesheet" href="styles/auth-mobile.css" media="(max-width: 767px)" />
</head>

<body>
<main class="wrapper">
<!-- 로고 -->
<header class="header">
<a href="/" aria-label="홈으로 이동">
<img src="/images/Property 1=lg.png" alt="판다마켓 로고" class="login-logo" width="398px"/>
<img src="/images/Property 1=lg.png" alt="판다마켓 로고" class="login-logo" width="398px" />
</a>
</header>

<!-- 로그인 폼 -->
<section class="auth-form-container">
<form class="login-form">
<form class="auth-form">
<div class="input-field">
<label for="email">이메일</label>
<input type="email" id="email" name="email" required placeholder="이메일을 입력해주세요." />
<input type="email" id="email" name="email" data-validate="email" autocomplete="email" required placeholder="이메일을 입력해주세요." />
<span class="error-message" id="email-error"></span>
</div>
<div class="input-field">
<label for="password">비밀번호</label>
<div class="input-wrapper">
<input type="password" id="password" name="password" required placeholder="패스워드를 입력해주세요."/>
<button class="toggle-password">
<input type="password" id="password" data-validate="password" name="password" autocomplete="current-password" required placeholder="패스워드를 입력해주세요." />
<button type="button" class="toggle-password" data-target="password">
<img src="/images/btn_none_visibility_on_24px.png" alt="패스워드 보이지 않음" width="24" height="24">
</button>
</div>
<span class="error-message" id="password-error"></span>
</div>
<button type="submit" class="btn">로그인</button>
<button type="submit" class="btn" disabled>로그인</button>
</form>

<!-- 간편 로그인 -->
<div class="social-login">
<p>간편 로그인하기</p>
<div class="social-buttons">
<a href="https://www.google.co.kr/?hl=ko"><img src="/images/Component 2.png" width="42" height="42" aria-label="구글로 이동해서 로그인 하기"/></a>
<a href="https://www.kakaocorp.com/page/service/service/KakaoTalk"><img src="/images/Component 3.png" width="42" height="42" aria-label="카카오로 이동해서 로그인 하기"/></a>
<a href="https://www.google.co.kr/?hl=ko"><img src="/images/Component 2.png" width="42" height="42"
aria-label="구글로 이동해서 로그인 하기" /></a>
<a href="https://www.kakaocorp.com/page/service/service/KakaoTalk"><img
src="/images/Component 3.png" width="42" height="42" aria-label="카카오로 이동해서 로그인 하기" /></a>
</div>
</div>

<!-- 회원가입 유도 -->
<div class="signup-link">
<p>판다마켓이 처음인가요?</p><a href="/signup.html" aria-label="회원가입으로 이동">회원가입</a>
<p>판다마켓이 처음인가요?</p><a href="/signup.html" aria-label="회원가입으로 이동">회원가입</a>
</div>
</section>
</main>
<script type="module" src="/scripts/auth.js"></script>
</body>

</html>
52 changes: 52 additions & 0 deletions scripts/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
validateInputs,
checkAllInputsValid,
togglePasswordVisibility,
debounce,
} from "./utils.js";
/**
* 변수 정의
*/
const inputArr = document.querySelectorAll("[data-validate]");
const submitBtn = document.querySelector(".btn");
const togglePasswordBtns = document.querySelectorAll(".toggle-password");
const form = document.querySelector(".auth-form");
const debouncedCheckAll = debounce(() => {
checkAllInputsValid(inputArr, submitBtn);
}, 120);
/**
* 이벤트 리스너
*/
// 입력란(input)
form.addEventListener("input", (e) => {
const input = e.target.closest("[data-validate]");
if (!input) return;
validateInputs(input);
debouncedCheckAll();
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 상황에서는 Debounce를 사용하는 것이 적절하지 않습니다.

이유는:

  • 사용자가 입력한 값에 대한 검증과 피드백은 즉각적인 편이 UX를 생각했을때 좋습니다. 즉, 사용자가 입력한 값에 대한 검증은 올바르지 않은 값인것이 확인되었을때 바로 알려주는것이 좋고, 이 과정에서 불필요한 지연이 발생된다면 오히려 사용자 경험을 저해할 수 있습니다.
  • 현재 validation 로직이 구현되어있는것은 정규표현식을 이용한 매우 가벼운 비교 연산입니다. 따라서 성능 부담이 그다지 크지 않기때문에 쓰로틀링/디바운싱을 적용하는 효용을 비교해봤을때, 오히려 사용자 경험을 저해하게되어 좋지 않습니다.

});
Copy link
Collaborator

Choose a reason for hiding this comment

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

함수 선언부 블락끼리는 그 사이에 한칸씩 띄워주세요!

form.addEventListener(
"blur",
(e) => {
const input = e.target.closest("[data-validate]");
if (!input) return;
validateInputs(input);
checkAllInputsValid(inputArr, submitBtn);
},
true
);
// 패스워드 토글
togglePasswordBtns.forEach((button) => {
button.addEventListener("click", () => togglePasswordVisibility(button));
});
// submit
submitBtn.addEventListener("click", (e) => {
e.preventDefault();
if (!submitBtn.disabled) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳! 예외처리 좋고, redirectMap 객체를 조건문 안에서 선언한것도 괜찮네요 👍
만약 이 객체가 여러 파일에서 재사용될 필요가 있다면 이런식으로 상수화해두고 constants.js와 같은 파일이름으로 모듈화해주는것도 괜찮겠죠?

// constants.js

export const REDIRECT_MAP = {
  "/login.html": "/items.html",
  "/signup.html": "/login.html",
};

const redirectMap = {
"/login.html": "/items.html",
"/signup.html": "/login.html",
};
const target = redirectMap[window.location.pathname];
if (target) window.location.href = target;
}
});
60 changes: 60 additions & 0 deletions scripts/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { validators } from "./validators.js";
/**
* debounce code
*/
export function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
/**
* toggle password
*/
export const togglePasswordVisibility = (button) => {
const targetId = button.getAttribute("data-target");
const passwordInput = document.getElementById(targetId);
if (!passwordInput) return;
const img = button.querySelector("img");
if (passwordInput.type === "password") {
passwordInput.type = "text";
img.src = "/images/btn_visibility_on_24px.png";
img.alt = "비밀번호 보이는 중";
} else {
passwordInput.type = "password";
img.src = "/images/btn_none_visibility_on_24px.png";
img.alt = "비밀번호 숨겨진 상태";
}
Comment on lines +22 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

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

if else문을 써서 처리해주는것도 나쁘지않지만, 구조를 생각해봤을때 조건이 여러개 붙는게 아닌 둘중에 하나로 토글되는 방식이니까, 이런 방식을 사용하면 의도가 좀 더 명확히 파악되고 코드도 간결해질수있겠네요 :)

  const isPasswordVisible = passwordInput.type === "password";

  passwordInput.type = isPasswordVisible ? "text" : "password";
  img.src = isPasswordVisible
    ? "/images/btn_visibility_on_24px.png"
    : "/images/btn_none_visibility_on_24px.png";
  img.alt = isPasswordVisible ? "비밀번호 보이는 중" : "비밀번호 숨겨진 상태";

};
/*
* Validate User Input
*/
export const validateInputs = (inputEl) => {
const validateType = inputEl.dataset.validate;
const { value, id } = inputEl;
if (!validateType || !validators[validateType]) return;
Comment on lines +37 to +38
Copy link
Collaborator

Choose a reason for hiding this comment

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

코드 흐름을 좀 더 개선해보려면 38번째라인을 37번째 라인보다 위로 갈 수 있게 순서를 바꾸면 더 좋을것같아요.

const { isValid, message } = validators[validateType](value);
const errMsg = document.getElementById(`${id}-error`);
if (errMsg) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

40, 41사이에 한칸 띄워주세요 :)

errMsg.textContent = isValid ? "" : message;
}
if (isValid) {
inputEl.classList.remove("input-error");
} else {
inputEl.classList.add("input-error");
}
Comment on lines +44 to +48
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 조건문도 classList.toggle을 사용하면 이렇게 간소화할수있답니다!

참고

Suggested change
if (isValid) {
inputEl.classList.remove("input-error");
} else {
inputEl.classList.add("input-error");
}
inputEl.classList.toggle("input-error", !isValid);

return isValid;
};
/*
* 모든 입력값이 유효한지 확인하는 함수
*/
export const checkAllInputsValid = (inputArr, submitBtn) => {
// 1) 모든 필드에 대해 validateInputs 호출 → 에러 메시지 업데이트
const results = Array.from(inputArr).map((input) => validateInputs(input));
// 2) map 결과로만 버튼 활성화/비활성화 결정
const allValid = results.every(Boolean);
submitBtn.disabled = !allValid;
};
33 changes: 33 additions & 0 deletions scripts/validators.js
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳! 유효성 검사 로직은 프로그램내에서 UI와 결합될 필요없이 재사용될 수 있으니, 따로 분리해두고 모듈화하시는게 좋죠 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 함수 정의
/*
Validators
*/
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export const validators = {
email: (value) => {
if (!value) return { isValid: false, message: "이메일을 입력해주세요." };
if (!emailRegex.test(value))
return { isValid: false, message: "이메일 형식에 맞지 않습니다." };
return { isValid: true, message: "" };
},
nickname: (value) => {
if (!value) return { isValid: false, message: "닉네임을 입력해주세요." };
return { isValid: true, message: "" };
},
password: (value) => {
if (!value) return { isValid: false, message: "비밀번호를 입력해주세요." };
if (value.length < 8)
return { isValid: false, message: "비밀번호를 8자리 이상 입력해주세요." };
return { isValid: true, message: "" };
},
confirmPassword: (value) => {
const passwordValue = document.getElementById("password").value;
if (!value)
return { isValid: false, message: "비밀번호를 한번 더 입력해주세요." };
if (value.length < 8)
return { isValid: false, message: "비밀번호를 8자리 이상 입력해주세요." };
if (value !== passwordValue)
return { isValid: false, message: "비밀번호가 일치하지 않습니다." };
return { isValid: true, message: "" };
},
};
Loading
Loading