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
Binary file added img/icon/btn_visibility_off_24px.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions js/constant/constant.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MIN_PW_LEN = 8;
89 changes: 89 additions & 0 deletions js/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import authFormUtils from "./utils/authFormUtils.js";

// DOM elements
const inputEmail = document.querySelector("#email");
const inputPw = document.querySelector("#password");
const detailEmail = document.querySelector(".email-detail");
const detailPw = document.querySelector(".password-detail");
const togglePw = document.querySelector(".toggle-visibility");
const loginBtn = document.querySelector(".login-btn");

// variables
const inputTags = document.querySelectorAll(".login-form input");
const verifiationList = (() => {
const map = {};
inputTags.forEach((el) => {
map[el.id] = false;
});

return map;
})();

// function
const verifyInputs = () => {
authFormUtils.verify(verifiationList)
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
authFormUtils.verify(verifiationList)
const isValid = authFormUtils.verify(verifiationList)

사소하지만, 이런식으로 변수로 만들어주면 함수 내부를 들여다보지않아도 실행 결과값에 대한 예측이 좀 더 쉬워지겠죠? :)

? authFormUtils.enableBtn(loginBtn)
: authFormUtils.disableBtn(loginBtn);
};

// initialize
/**
* 뒤로 가기 등 페이지를 복구했을 때, 브라우저가 입력값을 복원하는 과정에서 요구사항의 focusout 이벤트가 발생하지 않음.
* 따라서, pageshow 이벤트 발생 후 email 값이 있는 경우 (입력값이 복원된 경우) 강제로 focusout 이벤트 실행
*/
window.addEventListener("pageshow", () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

코드들이 일련의 흐름으로 실행된다기보다는 흩어져있어 파악이 어려워보여요.
이벤트리스너의 콜백에 직접적으로 관련 로직을 작성하기보다는 개별 함수로 작업을 분리해주고, 이런식으로 initialize를 담당하는 함수를 하나 만들어보면 어떨까요?

// 페이지 복원 시 폼 상태 검증
const validateFormOnRestore = () => {
  const inputs = [
    { element: inputEmail, detail: detailEmail, field: 'email' },
    { element: inputPw, detail: detailPw, field: 'password' }
  ];
  
  inputs.forEach(({ element, detail, field }) => {
    if (element.value) {
      validateField(element, detail, field);
    }
  });
};

// 이벤트 리스너 설정
const setupEventListeners = () => {
  // 페이지 복원 이벤트
  window.addEventListener('pageshow', validateFormOnRestore);
  
  // 이메일 입력 필드
  inputEmail.addEventListener('focusout', (e) => {
    validateField(e.target, detailEmail, 'email');
  });
  
  // 비밀번호 입력 필드
  inputPw.addEventListener('focusout', (e) => {
    validateField(e.target, detailPw, 'password');
  });
  
  // 비밀번호 표시 토글
  togglePw.addEventListener('click', togglePasswordVisibility);
};

// 초기화
setupEventListeners();

Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 작성한다면 코드 중복을 제거하는데도 효과가 있고, 이벤트가 처리되는 흐름을 구조화할수있고, 모든 이벤트 리스너를 한곳에서 관리함으로써 각 이벤트 핸들러가 좀 더 명확한 의도와 역할을 가지고 실행될수있게끔 처리해줄수있겠죠?

if (inputEmail.value) inputEmail.dispatchEvent(new Event("focusout"));
});

// events
inputEmail.addEventListener("focusout", (e) => {
const isCorrect = authFormUtils.isCorrectEmail(e.target.value);

if (!e.target.value) {
authFormUtils.changeDetail(e, detailEmail, "이메일을 입력해주세요");
verifiationList.email = false;
} else if (!isCorrect) {
authFormUtils.changeDetail(e, detailEmail, "잘못된 이메일 형식입니다.");
verifiationList.email = false;
} else {
authFormUtils.changeDetail(e, detailEmail);
verifiationList.email = true;
}
Comment on lines +40 to +51
Copy link
Collaborator

Choose a reason for hiding this comment

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

유효성 검사 규칙에 따른 유효성 검사 결과를 반환하는 형태의 함수가 모듈화되어있지않아 페이지 단위의 js에서 매번 유효성 검사를 수행하기위한 코드 블락을 작성해야하는게 비효율적으로 보여요.

이런식으로 간소화해보면 어떨까요?

예시)

// 유효성 검사 함수들
export const validators = {
  email: (value) => {
    if (!value) return { isValid: false, message: '이메일을 입력해주세요' };
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(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: '' };
  }
};

// 입력 필드 검증 함수
const validateField = (inputEl, detailEl, field) => {
  const value = inputEl.value.trim();
  const result = validators[field](value);
  
  if (result.isValid) {
    authFormUtils.changeDetail(inputEl, detailEl);
  } else {
    authFormUtils.changeDetail(inputEl, detailEl, result.message);
  }
  
  updateFormState(field, value, result.isValid);
};


verifyInputs();
});

inputPw.addEventListener("focusout", (e) => {
const isCorrect = authFormUtils.isCorrectPw(e.target.value);

if (!e.target.value) {
authFormUtils.changeDetail(e, detailPw, "비밀번호를 입력해주세요");
verifiationList.password = false;
} else if (!isCorrect) {
authFormUtils.changeDetail(e, detailPw, "비밀번호를 8자 이상 입력해주세요");
verifiationList.password = false;
} else {
authFormUtils.changeDetail(e, detailPw);
verifiationList.password = true;
}

authFormUtils.verify(verifiationList)
? authFormUtils.enableBtn(loginBtn)
: authFormUtils.disableBtn(loginBtn);

verifyInputs();
});

togglePw.addEventListener("click", (e) => {
if (inputPw.type === "password") {
inputPw.type = "text";
e.target.src = "../img/icon/btn_visibility_on_24px.png";
e.target.alt = "비밀번호 표시 켜짐";
e.target.setAttribute("aria-pressed", "true");
} else {
inputPw.type = "password";
e.target.src = "../img/icon/btn_visibility_off_24px.png";
e.target.alt = "비밀번호 표시 꺼짐";
e.target.setAttribute("aria-pressed", "false");
}
});
152 changes: 152 additions & 0 deletions js/sign_up.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import authFormUtils from "./utils/authFormUtils.js";

// DOM elements
const inputEmail = document.querySelector("#email");
const inputNickname = document.querySelector("#nickname");
const inputPw = document.querySelector("#password");
const inputPwRepeat = document.querySelector("#password-repeat");
const detailEmail = document.querySelector(".email-detail");
const detailNickname = document.querySelector(".nickname-detail");
const detailPw = document.querySelector(".password-detail");
const detailPwRepaet = document.querySelector(".password-repeat-detail");
const togglePw = document.querySelector(".toggle-visibility");
const togglePwRepeat = document.querySelector(".toggle-repeat-visibility");
const signUpBtn = document.querySelector(".sign-up-btn");

// variables
const inputTags = document.querySelectorAll(".sign-up-form input");
const verifiationList = (() => {
const map = {};
inputTags.forEach((el) => {
map[el.id] = false;
});

return map;
})();

// function
const verifyInputs = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 함수가 하는 역할이 input을 유효성검사하기보다는, 유효성 검사 결과를 button UI state에 반영하는일에 가깝지않을까요?

authFormUtils.verify(verifiationList)
? authFormUtils.enableBtn(signUpBtn)
: authFormUtils.disableBtn(signUpBtn);
};

// initialize
/**
* 뒤로 가기 등 페이지를 복구했을 때, 브라우저가 입력값을 복원하는 과정에서 요구사항의 focusout 이벤트가 발생하지 않음.
* 따라서, pageshow 이벤트 발생 후 email 값이 있는 경우 (입력값이 복원된 경우) 강제로 focusout 이벤트 실행
*/
window.addEventListener("pageshow", () => {
if (inputEmail.value) inputEmail.dispatchEvent(new Event("focusout"));
});

// events
inputEmail.addEventListener("focusout", (e) => {
const isCorrect = authFormUtils.isCorrectEmail(e.target.value);

if (!e.target.value) {
authFormUtils.changeDetail(e, detailEmail, "이메일을 입력해주세요");
verifiationList.email = false;
} else if (!isCorrect) {
authFormUtils.changeDetail(e, detailEmail, "잘못된 이메일 형식입니다");
verifiationList.email = false;
} else {
authFormUtils.changeDetail(e, detailEmail);
verifiationList.email = true;
}

verifyInputs();
});

inputNickname.addEventListener("focusout", (e) => {
const isCorrect = authFormUtils.isCorrectNickname(e.target.value);

if (!e.target.value) {
authFormUtils.changeDetail(e, detailNickname, "닉네임을 입력해주세요");
verifiationList.nickname = false;
} else if (!isCorrect) {
authFormUtils.changeDetail(e, detailNickname, "잘못된 닉네임 형식입니다");
verifiationList.nickname = false;
} else {
authFormUtils.changeDetail(e, detailNickname);
verifiationList.nickname = true;
}

verifyInputs();
});

inputPw.addEventListener("focusout", (e) => {
const isCorrect = authFormUtils.isCorrectPw(e.target.value);

if (!e.target.value) {
authFormUtils.changeDetail(e, detailPw, "비밀번호를 입력해주세요");
verifiationList.password = false;
} else if (!isCorrect) {
authFormUtils.changeDetail(e, detailPw, "비밀번호를 8자 이상 입력해주세요");
verifiationList.password = false;
} else {
authFormUtils.changeDetail(e, detailPw);
verifiationList.password = true;
}

if (inputPwRepeat.value) inputPwRepeat.dispatchEvent(new Event("focusout"));

verifyInputs();
});

inputPwRepeat.addEventListener("focusout", (e) => {
const isCorrect = authFormUtils.isCorrectPw(e.target.value);
const isSame = inputPw.value === e.target.value;

if (!e.target.value) {
authFormUtils.changeDetail(e, detailPwRepaet, "비밀번호를 입력해주세요");
verifiationList["password-repeat"] = false;
} else if (!isCorrect) {
authFormUtils.changeDetail(
e,
detailPwRepaet,
"비밀번호를 8자 이상 입력해주세요"
);
verifiationList["password-repeat"] = false;
} else if (!isSame) {
authFormUtils.changeDetail(
e,
detailPwRepaet,
"비밀번호가 일치하지 않습니다"
);
verifiationList["password-repeat"] = false;
} else {
authFormUtils.changeDetail(e, detailPwRepaet);
verifiationList["password-repeat"] = true;
}

verifyInputs();
});

togglePw.addEventListener("click", (e) => {
if (inputPw.type === "password") {
inputPw.type = "text";
e.target.src = "../img/icon/btn_visibility_on_24px.png";
e.target.alt = "비밀번호 표시 켜짐";
e.target.setAttribute("aria-pressed", "true");
} else {
inputPw.type = "password";
e.target.src = "../img/icon/btn_visibility_off_24px.png";
e.target.alt = "비밀번호 표시 꺼짐";
e.target.setAttribute("aria-pressed", "false");
}
});

togglePwRepeat.addEventListener("click", (e) => {
if (inputPwRepeat.type === "password") {
inputPwRepeat.type = "text";
e.target.src = "../img/icon/btn_visibility_on_24px.png";
e.target.alt = "비밀번호 반복 표시 켜짐";
e.target.setAttribute("aria-pressed", "true");
} else {
inputPwRepeat.type = "password";
e.target.src = "../img/icon/btn_visibility_off_24px.png";
e.target.alt = "비밀번호 반복 표시 꺼짐";
e.target.setAttribute("aria-pressed", "false");
}
});
49 changes: 49 additions & 0 deletions js/utils/authFormUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MIN_PW_LEN } from "../constant/constant.js";

export const isCorrectEmail = (str) => {
const emailRegex = /^[a-zA-Z0-9]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(str);
};

export const isCorrectNickname = (str) => {
return true;
};

export const isCorrectPw = (pw) => {
return MIN_PW_LEN <= pw.length;
};

export const changeDetail = (event, detailElement, text = "") => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

함수 이름이 좀 모호하게 느껴져요. 결국 이 함수가 하는 일이 유효성 검사 상태에 따른 UI state 업데이트라면 updateFieldState 정도는 어떨까요?

const action = text ? "add" : "remove";
const action2 = !text ? "add" : "remove";
event.target.classList[action]("incorrect-input");
event.target.classList[action2]("correct-input");
detailElement.textContent = text;
text
? detailElement.classList.remove("hidden")
: detailElement.classList.add("hidden");
};

export const verify = (verifiationList) => {
const verifiations = Object.values(verifiationList);
return verifiations.includes(false) ? false : true;
};

export const disableBtn = (btnElement) => {
btnElement.disabled = true;
};

export const enableBtn = (btnElement) => {
btnElement.disabled = false;
};

const authFormUtils = {
isCorrectEmail,
isCorrectNickname,
isCorrectPw,
changeDetail,
verify,
disableBtn,
enableBtn,
};
export default authFormUtils;
24 changes: 18 additions & 6 deletions login.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@
<a class="logo" href="/" aria-label="홈으로 이동">
<img src="./img/logo/lg.png" width="396" height="132" />
</a>
<form method="POST" class="login-form" name="login-form">
<form
method="POST"
action="./items.html"
class="login-form"
name="login-form"
>
<div class="input-box">
<label for="email">이메일</label>
<input
id="email"
type="email"
name="email"
placeholder="입력"
placeholder="이메일을 입력해주세요"
autocomplete="email"
/>
<span class="email-detail hidden"></span>
</div>
<div class="input-box">
<label for="password">비밀번호</label>
Expand All @@ -35,15 +41,19 @@
name="password"
placeholder="비밀번호를 입력해주세요"
/>
<span class="password-detail hidden"></span>
<img
class="toggle-visiblity"
src="./img/icon/btn_visibility_on_24px.png"
alt="비밀번호 표시 토글 버튼"
class="toggle-visibility"
src="./img/icon/btn_visibility_off_24px.png"
alt="비밀번호 표시 꺼짐"
aria-pressed="false"
width="24"
height="24"
/>
</div>
<button type="submit" class="login-btn">로그인</button>
<button type="submit" disabled class="login-btn disable-btn">
로그인
</button>
</form>

<div class="oauth flex-between-center">
Expand Down Expand Up @@ -74,5 +84,7 @@
</p>
</div>
</main>

<script type="module" src="./js/login.js"></script>
</body>
</html>
Loading
Loading