diff --git a/data/users.js b/data/users.js deleted file mode 100644 index 6cadadc28..000000000 --- a/data/users.js +++ /dev/null @@ -1,8 +0,0 @@ -const users = [ - { - email: "test@codeit.com", - password: "codeit101", - }, -]; - -export default users; diff --git a/index.html b/index.html index 6f90cac55..61246e0ff 100644 --- a/index.html +++ b/index.html @@ -39,6 +39,8 @@ + +
@@ -54,7 +56,7 @@ src="images/icons/logo.svg" alt="링크브러리 로고" /> - 로그인 + 로그인 @@ -65,7 +67,7 @@

>를
쉽게 저장하고 관리해 보세요

- 링크 추가하기 + 링크 추가하기
{ + e.preventDefault(); + navigateFolderPage("/pages/auth/signin/"); +}); + +signUpAnchor.addEventListener("click", (e) => { + e.preventDefault(); + navigateFolderPage("/pages/auth/signup/"); +}); + +function navigateFolderPage(href) { + if (localStorage.getItem("accessToken")) { + location.href = "/pages/folder/"; + return; + } + + location.href = href; +} diff --git a/pages/auth/api.js b/pages/auth/api.js new file mode 100644 index 000000000..2d082b29c --- /dev/null +++ b/pages/auth/api.js @@ -0,0 +1,12 @@ +import { fetchApi } from "/scripts/fetchApi.js"; + +const postSignIn = (email, password) => + fetchApi.post("/sign-in", { body: { email, password } }); + +const postCheckEmail = (email) => + fetchApi.post("/check-email", { body: { email } }); + +const postSignUp = (email, password) => + fetchApi.post("/sign-up", { body: { email, password } }); + +export { postCheckEmail, postSignIn, postSignUp }; diff --git a/pages/member/sign.css b/pages/auth/sign.css similarity index 100% rename from pages/member/sign.css rename to pages/auth/sign.css diff --git a/pages/member/sign.js b/pages/auth/sign.js similarity index 67% rename from pages/member/sign.js rename to pages/auth/sign.js index 86b271549..cc4687b47 100644 --- a/pages/member/sign.js +++ b/pages/auth/sign.js @@ -1,4 +1,4 @@ -import users from "/data/users.js"; +import { postCheckEmail } from "./api.js"; import { isEmptyString } from "/scripts/utils.js"; const errorMessage = { @@ -18,17 +18,33 @@ const errorMessage = { }, }; -function handleErrorMessage(element, addOrRemoveHide, errorMessage = "") { - const parent = element.parentElement; - const errorMessageSpan = parent.querySelector(".input-error__message"); +function handleErrorMessage(target, errorMessage = "") { + const parent = target.parentElement; + let errorMessageSpan = parent.querySelector(".input-error__message"); + + if (isEmptyString(errorMessage) && !errorMessageSpan) { + return; + } + + if (!errorMessageSpan) { + errorMessageSpan = document.createElement("span"); + errorMessageSpan.classList.add("input-error__message"); + target.after(errorMessageSpan); + errorMessageSpan.textContent = errorMessage; + return; + } + + if (isEmptyString(errorMessage)) { + errorMessageSpan.remove(); + return; + } errorMessageSpan.textContent = errorMessage; - errorMessageSpan.classList[addOrRemoveHide]("hide"); } function inputValidationFailed(target, errorMessage, eyeIcon) { target.classList.add("input-error"); - handleErrorMessage(target, "remove", errorMessage); + handleErrorMessage(target, errorMessage); if (eyeIcon) { eyeIcon.classList.add("eye-icon__error"); } @@ -36,20 +52,20 @@ function inputValidationFailed(target, errorMessage, eyeIcon) { function inputValidationSucceeded(target, eyeIcon) { target.classList.remove("input-error"); - handleErrorMessage(target, "add"); + handleErrorMessage(target); if (eyeIcon) { eyeIcon.classList.remove("eye-icon__error"); } } -function checkEmailValid(element, checkExist = true) { +async function checkEmailValid(element, checkExist = true) { if (isEmptyString(element.value)) { return errorMessage.email.empty; } if (!isEmailValid(element.value)) { return errorMessage.email.valid; } - if (checkExist && isEmailExist(element.value)) { + if (checkExist && (await isEmailExist(element.value))) { return errorMessage.email.exist; } return ""; @@ -82,14 +98,16 @@ function isPasswordValid(password) { return password.trim().length >= 8 && regex.test(password); } -function isEmailExist(email) { - return users.some((user) => user.email === email); -} - -function isMemberExist(member) { - return users.some( - (user) => user.email === member.email && user.password === member.password - ); +async function isEmailExist(email) { + let emailExist = true; + try { + await postCheckEmail(email); + emailExist = false; + } catch (error) { + console.error(`${error.name}: ${error.message}`); + } finally { + return emailExist; + } } function changePasswordVisibility(passwordInput) { @@ -104,6 +122,10 @@ function changePasswordVisibility(passwordInput) { }; } +function setUserAccessToken({ data }) { + localStorage.setItem("accessToken", data.accessToken); +} + export { changePasswordVisibility, checkEmailValid, @@ -112,5 +134,5 @@ export { errorMessage, inputValidationFailed, inputValidationSucceeded, - isMemberExist, + setUserAccessToken, }; diff --git a/pages/member/signin/index.html b/pages/auth/signin/index.html similarity index 96% rename from pages/member/signin/index.html rename to pages/auth/signin/index.html index 5b7ab0d6e..17aef0e76 100644 --- a/pages/member/signin/index.html +++ b/pages/auth/signin/index.html @@ -68,7 +68,6 @@ name="email" required placeholder="이메일을 입력해주세요." /> -
@@ -83,7 +82,6 @@ class="form__input--eye-off eye-icon" type="button" aria-label="패스워드 보이기"> -
diff --git a/pages/auth/signin/signin.js b/pages/auth/signin/signin.js new file mode 100644 index 000000000..ce98764f3 --- /dev/null +++ b/pages/auth/signin/signin.js @@ -0,0 +1,96 @@ +import { + changePasswordVisibility, + checkEmailValid, + checkPasswordValid, + errorMessage, + inputValidationFailed, + inputValidationSucceeded, + setUserAccessToken, +} from "../sign.js"; + +import { postSignIn } from "../api.js"; +import { isEmptyString } from "/scripts/utils.js"; + +navigateFolderPage("pages/auth/signin/"); + +/** + * email input + */ +const emailInput = document.querySelector("#input-email"); +emailInput.addEventListener("focusout", onEmailFocusout); + +async function onEmailFocusout({ target }) { + const errorMessage = await checkEmailValid(target, false); + if (!isEmptyString(errorMessage)) { + inputValidationFailed(target, errorMessage); + return false; + } + + inputValidationSucceeded(target); + return true; +} + +/** + * password input + */ +const passwordInput = document.querySelector("#input-password"); +const passwordEyeIcon = document.querySelector( + ".form__password .form__input--eye-off" +); + +passwordInput.addEventListener("focusout", onPasswordFocusout); + +function onPasswordFocusout({ target }) { + const errorMessage = checkPasswordValid(target); + if (!isEmptyString(errorMessage)) { + inputValidationFailed(target, errorMessage, passwordEyeIcon); + return false; + } + + inputValidationSucceeded(target, passwordEyeIcon); + return true; +} + +passwordEyeIcon.addEventListener( + "click", + changePasswordVisibility(passwordInput) +); + +/** + * form + */ +const form = document.querySelector(".form"); + +form.addEventListener("submit", onSubmit); + +function onSubmit(e) { + e.preventDefault(); + + const validResult = + onEmailFocusout({ target: emailInput }) && + onPasswordFocusout({ target: passwordInput }); + + if (!validResult) { + return; + } + + doSignIn(emailInput.value, passwordInput.value); +} + +async function doSignIn(email, password) { + try { + const signInResponse = await postSignIn(email, password); + setUserAccessToken(signInResponse); + location.href = "/pages/folder"; + } catch (error) { + console.error(`${error.name}: ${error.message}`); + if (error.name === "AuthApiError") { + inputValidationFailed(emailInput, errorMessage.email.loginFailed); + inputValidationFailed( + passwordInput, + errorMessage.password.loginFailed, + passwordEyeIcon + ); + } + } +} diff --git a/pages/member/signup/index.html b/pages/auth/signup/index.html similarity index 96% rename from pages/member/signup/index.html rename to pages/auth/signup/index.html index ecebc1b85..b73058886 100644 --- a/pages/member/signup/index.html +++ b/pages/auth/signup/index.html @@ -69,7 +69,6 @@ name="email" required placeholder="이메일을 입력해주세요." /> -
@@ -84,7 +83,6 @@ class="form__input--eye-off eye-icon" type="button" aria-label="패스워드 감추기"> -
diff --git a/pages/member/signup/signup.js b/pages/auth/signup/signup.js similarity index 64% rename from pages/member/signup/signup.js rename to pages/auth/signup/signup.js index a1b29d903..4f7866bdb 100644 --- a/pages/member/signup/signup.js +++ b/pages/auth/signup/signup.js @@ -5,18 +5,22 @@ import { checkPasswordsMatch, inputValidationFailed, inputValidationSucceeded, + setUserAccessToken, } from "../sign.js"; +import { postSignUp } from "../api.js"; import { isEmptyString } from "/scripts/utils.js"; +navigateFolderPage("pages/auth/signup/"); + /** * email input */ const emailInput = document.querySelector("#input-email"); -emailInput.addEventListener("focusout", onEmailFocusoutValid); +emailInput.addEventListener("focusout", onEmailFocusout); -function onEmailFocusoutValid({ target }) { - const errorMessage = checkEmailValid(target); +async function onEmailFocusout({ target }) { + const errorMessage = await checkEmailValid(target); if (!isEmptyString(errorMessage)) { inputValidationFailed(target, errorMessage); return false; @@ -34,9 +38,9 @@ const passwordEyeIcon = document.querySelector( ".form__password .form__input--eye-off" ); -passwordInput.addEventListener("focusout", onPasswordFocusoutValid); +passwordInput.addEventListener("focusout", onPasswordFocusout); -function onPasswordFocusoutValid({ target }) { +function onPasswordFocusout({ target }) { const errorMessage = checkPasswordValid(target); if (!isEmptyString(errorMessage)) { inputValidationFailed(target, errorMessage, passwordEyeIcon); @@ -60,9 +64,9 @@ const passwordChkEyeIcon = document.querySelector( ".form__password-chk .form__input--eye-off" ); -passwordInputChk.addEventListener("focusout", onPasswordChkFocusoutValid); +passwordInputChk.addEventListener("focusout", onPasswordChkFocusout); -function onPasswordChkFocusoutValid({ target }) { +function onPasswordChkFocusout({ target }) { const errorMessage = checkPasswordsMatch(target, passwordInput); if (!isEmptyString(errorMessage)) { inputValidationFailed(target, errorMessage, passwordChkEyeIcon); @@ -83,19 +87,29 @@ passwordChkEyeIcon.addEventListener( */ const form = document.querySelector(".form"); -form.addEventListener("submit", onSubmitValid); +form.addEventListener("submit", onSubmit); -function onSubmitValid(e) { +function onSubmit(e) { e.preventDefault(); let result = - onEmailFocusoutValid({ target: emailInput }) && - onPasswordFocusoutValid({ target: passwordInput }) && - onPasswordChkFocusoutValid({ target: passwordInputChk }); + onEmailFocusout({ target: emailInput }) && + onPasswordFocusout({ target: passwordInput }) && + onPasswordChkFocusout({ target: passwordInputChk }); if (!result) { return; } - location.href = "/pages/folder"; + doSignUp(emailInput.value, passwordInput.value); +} + +async function doSignUp(email, password) { + try { + const signUpResponse = await postSignUp(email, password); + setUserAccessToken(signUpResponse); + location.href = "/pages/folder"; + } catch (error) { + console.error(`${error.name}: ${error.message}`); + } } diff --git a/pages/member/signin/signin.js b/pages/member/signin/signin.js deleted file mode 100644 index d2b892d74..000000000 --- a/pages/member/signin/signin.js +++ /dev/null @@ -1,97 +0,0 @@ -import { - changePasswordVisibility, - checkEmailValid, - checkPasswordValid, - errorMessage, - inputValidationFailed, - inputValidationSucceeded, - isMemberExist, -} from "../sign.js"; - -import { isEmptyString } from "/scripts/utils.js"; - -/** - * email input - */ -const emailInput = document.querySelector("#input-email"); -emailInput.addEventListener("focusout", onEmailFocusoutValid); - -function onEmailFocusoutValid({ target }) { - const errorMessage = checkEmailValid(target, false); - if (!isEmptyString(errorMessage)) { - inputValidationFailed(target, errorMessage); - return false; - } - - inputValidationSucceeded(target); - return true; -} - -/** - * password input - */ -const passwordInput = document.querySelector("#input-password"); -const passwordEyeIcon = document.querySelector( - ".form__password .form__input--eye-off" -); - -passwordInput.addEventListener("focusout", onPasswordFocusoutValid); - -function onPasswordFocusoutValid({ target }) { - const errorMessage = checkPasswordValid(target); - if (!isEmptyString(errorMessage)) { - inputValidationFailed(target, errorMessage, passwordEyeIcon); - return false; - } - - inputValidationSucceeded(target, passwordEyeIcon); - return true; -} - -passwordEyeIcon.addEventListener( - "click", - changePasswordVisibility(passwordInput) -); - -/** - * form - */ -const form = document.querySelector(".form"); - -form.addEventListener("submit", onSubmitValid); - -function onSubmitValid(e) { - e.preventDefault(); - - let result = - onEmailFocusoutValid({ target: emailInput }) && - onPasswordFocusoutValid({ target: passwordInput }) && - checkMemberExist(); - - if (!result) { - return; - } - - location.href = "/pages/folder"; -} - -function checkMemberExist() { - const isOurMember = isMemberExist({ - email: emailInput.value, - password: passwordInput.value, - }); - - if (!isOurMember) { - inputValidationFailed(emailInput, errorMessage.email.loginFailed); - inputValidationFailed( - passwordInput, - errorMessage.password.loginFailed, - passwordEyeIcon - ); - return false; - } - - inputValidationSucceeded(emailInput); - inputValidationSucceeded(passwordInput, passwordEyeIcon); - return true; -} diff --git a/scripts/fetchApi.js b/scripts/fetchApi.js new file mode 100644 index 000000000..4e1ff6b10 --- /dev/null +++ b/scripts/fetchApi.js @@ -0,0 +1,43 @@ +const BASE_URL = "https://bootcamp-api.codeit.kr/api"; + +const fetchApi = { + request: async function (url, { options = {}, headers = {} } = {}) { + const accessToken = localStorage.getItem("accessToken"); + const response = await fetch(`${BASE_URL}${url}`, { + ...options, + headers: { + Authorization: accessToken || "", + ...options.headers, + ...headers, + }, + }); + + const result = await response.json(); + + if (!response.ok) { + throw { + name: result?.error?.name || "Error", + message: + result?.error?.message || `HTTP error! status: ${response.status}`, + }; + } + + return result; + }, + + post: async function (url, { body, headers }) { + const options = { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + ...headers, + }, + }; + + const result = await this.request(url, { options }); + return result; + }, +}; + +export { fetchApi };