Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[우지석] Week14 #448

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
76 changes: 76 additions & 0 deletions components/EmailInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// components/EmailInput.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

파일명 보면 알 수 있는 부분이라 없어도 되는 주석인 것 같아요!!


import React, { useState, useEffect } from "react";
import classNames from "classnames/bind";
import styles from "@/styles/signin.module.scss";

interface EmailInputProps {
value: string;
onChange: (value: string) => void;
error?: string;
}

export const emailCheck = (email: string) => {
const emailForm =
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i;
return emailForm.test(email);
};
Comment on lines +13 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 요런 유틸성 함수는 별도 utils 폴더에 분리해서 관리하곤 합니다!
지금 emailCheck 함수를 export 하고 있는데, 다른 파일에서도 사용하고 있는 것 같아요
그렇다면 더더욱 분리하시는 것을 추천드립니다!

리액트 컴포넌트 파일에는 보통 리액트 컴포넌트 하나만 default export 하는 식으로 작성합니다


export function EmailInput({ value, onChange, error }: EmailInputProps) {
const cx = classNames.bind(styles);
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거는 컴포넌트 렌더링 될 때마다 매번 다시 선언되지 않아도 되는 코드라서 컴포넌트 바깥쪽에 두셔도 돼요

저는 보통 import 문과 컴포넌트 사이에 둡니다

const [isFocused, setIsFocused] = useState(false);
const [emailErrorText, setEmailErrorText] = useState("");

useEffect(() => {
setEmailErrorText(error || "");
}, [error]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const emailInput = e.target.value;
onChange(emailInput);
};

const handleBlur = () => {
setIsFocused(false);
if (value) {
if (!emailCheck(value)) {
setEmailErrorText("올바른 이메일 주소가 아닙니다.");
} else {
setEmailErrorText("");
}
} else {
setEmailErrorText("이메일을 입력해 주세요.");
}
};

const handleFocus = () => {
setIsFocused(true);
};

const isError = !isFocused && (error || emailErrorText);

return (
<div className={cx("input__section")}>
<label className={cx("text")}>
이메일 <br />
Copy link
Collaborator

Choose a reason for hiding this comment

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

불필요해보이는 br 이네요!

</label>
<input
id="email"
placeholder="이메일을 입력해 주세요."
className={cx("user-input", { "error-input": isError })}
type="email"
name="email"
value={value}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
/>
<div
id="email-errorText"
className={cx("errortext", { error: !isError })}
>
{emailErrorText}
</div>
</div>
);
}
70 changes: 70 additions & 0 deletions components/PasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useState } from "react";
import classNames from "classnames/bind";
import styles from "@/styles/signin.module.scss";

interface PasswordInputProps {
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
error?: string;
id?: string;
}

export function PasswordInput({
value,
onChange,
onBlur,
error,
id,
}: PasswordInputProps) {
const cx = classNames.bind(styles);
const [isFocused, setIsFocused] = useState(false);
const [errorText, setErrorText] = useState("");

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value);
};

const handleBlur = () => {
setIsFocused(false);
if (onBlur) {
onBlur();
}
if (!value) {
setErrorText("비밀번호를 입력해 주세요.");
} else {
setErrorText("");
}
};

const handleFocus = () => {
setIsFocused(true);
};

const isError = !isFocused && (error || errorText);

return (
<div className={cx("input__section", "password-section")}>
<label className={cx("text")}>
비밀번호 <br />
</label>
<input
id={id || "password"}
placeholder="비밀번호를 입력해 주세요."
className={cx("user-input", { "error-input": isError })}
type="password"
name="password"
value={value}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
/>
<div
id="password-errorText"
className={cx("errortext", { error: !isError })}
>
{error || errorText}
</div>
</div>
);
}
51 changes: 51 additions & 0 deletions components/SignInForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";
import { EmailInput } from "./EmailInput";
import { PasswordInput } from "./PasswordInput";
import styles from "@/styles/signin.module.scss";
import classNames from "classnames/bind";

export function SignIn() {
const cx = classNames.bind(styles);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [emailError, setEmailError] = useState("");
const [passwordError, setPasswordError] = useState("");
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
Comment on lines +10 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

다른 파일들처럼 적당히 빈 줄을 추가해주면 더 읽기 편할 것 같아요!

e.preventDefault();
Copy link
Collaborator

Choose a reason for hiding this comment

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

form event 잘 활용하셨네요 👍🏽

setEmailError("");
setPasswordError("");
const data = { email, password };
Copy link
Collaborator

Choose a reason for hiding this comment

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

data 도 틀린 건 아닌데 보통 post 요청에 같이 보내는 객체는 body 라고 많이 부릅니다 !

try {
const response = await axios.post(
"https://bootcamp-api.codeit.kr/api/sign-in",
Copy link
Collaborator

Choose a reason for hiding this comment

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

api 호스트는 api 사용하는 부분마다 반복될 것 같아서 상수로 분리해서 관리하면 더 좋을 것 같아요!

data
);
if (response.status === 200) {
router.push("/folder");
}
Comment on lines +26 to +28
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기에 response 내의 토큰을 로컬스토리지에 저장하는 로직 추가해주시면 될 것 같네요!

} catch (error: any) {
if (error.response) {
if (error.response.status === 400) {
setEmailError("이메일을 확인해 주세요.");
setPasswordError("비밀번호를 확인해 주세요.");
}
}
}
};
return (
<form className={cx("input")} onSubmit={handleSubmit}>
<EmailInput value={email} onChange={setEmail} error={emailError} />
<PasswordInput
value={password}
onChange={setPassword}
error={passwordError}
/>
<button type="submit" id="btn" className={cx("signin", "button")}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

id='btn' 은 활용하는 곳이 있나요?
파트1 부터 넘어온 것 && 사용하는 곳 없다면 삭제해주세요!

로그인
</button>
</form>
);
}
66 changes: 66 additions & 0 deletions components/SignUpForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import axios from "axios";
import { EmailInput, emailCheck } from "./EmailInput";
import { SignUpPassword, isValidPassword } from "./SignUpPassword";
import styles from "@/styles/signup.module.scss";
import classNames from "classnames/bind";

export function SignUp() {
const cx = classNames.bind(styles);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [emailError, setEmailError] = useState("");
const router = useRouter();

const signUp = async (e: React.FormEvent) => {
e.preventDefault();
const emailChecked = emailCheck(email);
const validPassword = isValidPassword(password);
Comment on lines +19 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

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

emailCheck , isValidPassword 둘 다 동일하게 유효성 검증을 하는 함수인데
네이밍 방식도 다르고 사용한 단어 (valid, check) 도 달라서 좀 헷갈리는 것 같아요

멘토링 때 말씀드린대로 함수는 동사로 시작하게 작성해보시면 좋을 것 같고,
동일한 역할을 하는 함수라면 동일한 단어를 사용해서 일관성 있게 작성하면 더 좋을 것 같습니다!

함수의 리턴값이 boolean 형이니 변수도 boolean 형에 맞게 지어주시면 좋을 것 같고요!

Suggested change
const emailChecked = emailCheck(email);
const validPassword = isValidPassword(password);
const isEmailValid = validateEmail(email);
const isPasswordValid = validatePassword(password);

const passwordMatch = password === confirmPassword;

if (emailChecked && validPassword && passwordMatch) {
const checkEmailUrl = "https://bootcamp-api.codeit.kr/api/check-email";
Copy link
Collaborator

Choose a reason for hiding this comment

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

요거는 submit 시점이 아니라 email 인풋 blur 시점에 확인해야 하는 것 같아요! (요구사항 확인)

const checkEmailData = { email };
try {
const checkEmailResponse = await axios.post(
checkEmailUrl,
checkEmailData
);
if (checkEmailResponse.status === 409) {
setEmailError("이미 사용 중인 이메일입니다.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

스크린샷처럼 signup페이지에서 중복된 이메일을 적어서 폼제출을 했을때 409에러는 발생하는데
setEamilError가 실행이 안되는같아요.....

배포 url 이 있어서 확인이 가능했습니다!!

Screenshot 2024-05-21 at 11 47 53 PM

409 에러 났을 때 콘솔에 'hello2' 가 찍히는 걸 보니 47라인이 실행됐다는 건데요
그 말은 error 가 나서 catch 구문으로 넘어갔다는 거겠죠?

27~30 라인에서 axios 요청하면서 에러가 났기 때문에 31라인 이후로 진행되지 않고 46라인으로 이동한거에요

  1. catch 문에서 error의 status 가 409 인 경우 처리를 해주는 식으로 처리하시면 될 것 같습니다!
  2. 지금 submit 시점에 하나의 try~catch 문 안에서 두 개의 api (이메일 중복 확인, 회원가입) 을 처리하고 있는데
    하나의 catch 문 내에서 두 api 의 에러 케이스를 같이 커버하면 코드가 섞일 수도 있고 하니
    api 마다 각각 try~catch 로 감싸면 좋을 것 같아요
    어차피 요구사항 상 두 개의 api 가 실행되어야 하는 시점이 별개라서 요구사항에 맞춰 수정하면 자연스럽게 정리될 것 같긴 합니다

console.log("hello~");
return;
} else {
setEmailError("");
}

const signUpUrl = "https://bootcamp-api.codeit.kr/api/sign-up";
const signUpData = { email, password };
const signUpResponse = await axios.post(signUpUrl, signUpData);
if (signUpResponse.status === 200) {
router.push("/folder");
}
} catch (error) {
console.log("Error:", error);
console.log("hello2");
}
}
};

return (
<form className={cx("signup__form")} onSubmit={signUp}>
<EmailInput value={email} onChange={setEmail} error={emailError} />
<SignUpPassword
password={password}
confirmPassword={confirmPassword}
onPasswordChange={setPassword}
onConfirmPasswordChange={setConfirmPassword}
/>
<button type="submit" className={cx("signup__button")}>
회원가입
</button>
</form>
);
}
60 changes: 60 additions & 0 deletions components/SignUpPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState } from "react";
import { PasswordInput } from "./PasswordInput";

interface SignUpPasswordProps {
password: string;
confirmPassword: string;
onBlur?: () => void;
onPasswordChange: (value: string) => void;
onConfirmPasswordChange: (value: string) => void;
}

export const isValidPassword = (password: string) => {
const passwordForm = /(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;
return passwordForm.test(password);
};

export function SignUpPassword({
password,
confirmPassword,
onPasswordChange,
onConfirmPasswordChange,
}: SignUpPasswordProps) {
const [passwordError, setPasswordError] = useState("");
const [confirmPasswordError, setConfirmPasswordError] = useState("");

const validatePassword = () => {
const validPassword = isValidPassword(password);
if (!validPassword) {
setPasswordError("비밀번호는 영문,숫자 조합 8자 이상 입력해 주세요.");
} else {
setPasswordError("");
}
};

const checkPassword = () => {
if (password !== confirmPassword) {
setConfirmPasswordError("비밀번호가 일치하지 않아요.");
} else {
setConfirmPasswordError("");
}
};

return (
<>
<PasswordInput
value={password}
onChange={onPasswordChange}
onBlur={validatePassword}
error={passwordError}
/>
<PasswordInput
value={confirmPassword}
onChange={onConfirmPasswordChange}
onBlur={checkPassword}
error={confirmPasswordError}
id="password-confirm"
/>
</>
);
}
16 changes: 16 additions & 0 deletions components/passwordToggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
$(document).ready(function () {
Copy link
Collaborator

Choose a reason for hiding this comment

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

갑분제이쿼리 어디서 나온거죠 ㅋㅋ 급한건 아니니 시간되면 고치시고 그리 중요한 부분은 아니라 냅두셔도 됩니다

$(".password-section i").on("click", function () {
$("input").toggleClass("active");
if ($("input").hasClass("active")) {
$(this)
.attr("class", "fa fa-eye-slash fa-lg")
.prev("input")
.attr("type", "text");
} else {
$(this)
.attr("class", "fa fa-eye fa-lg")
.prev("input")
.attr("type", "password");
}
});
});
Loading
Loading