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

[오병훈] Sprint10 #309

Conversation

bhoh032019
Copy link
Collaborator

요구사항

기본

상품 등록 페이지

  • 상품 등록 페이지 주소는 "/addboard" 입니다.
  • 게시판 이미지는 최대 한개 업로드가 가능합니다.
  • 각 input의 placeholder 값을 정확히 입력해주세요.
  • 이미지를 제외하고 input 에 모든 값을 입력하면 '등록' 버튼이 활성화 됩니다.

상품 상세 페이지

  • 상품 상세 페이지 주소는 "/board/{id}" 입니다.
  • 댓글 input 값을 입력하면 '등록' 버튼이 활성화 됩니다.
  • 활성화된 '등록' 버튼을 누르면 댓글이 등록됩니다

심화

  • 회원가입, 로그인 api를 사용하여 받은accessToken을 사용하여 게시물 등록을 합니다.
  • '등록' 버튼을 누르면 게시물 상세 페이지로 이동합니다.

주요 변경사항

  • 이전 미션에서 만든 로그인 페이지를 가져와 수정해 로그인 페이지를 제작했습니다.
  • accessToken을 withCredential을 이용해 사용하려하니 CORS에러가 발생해 우선 localstorage를 사용해 accessToken을 관리했습니다.

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@bhoh032019 bhoh032019 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Nov 30, 2024
Comment on lines +2 to +4
import SortIcon from '@public/svgs/ic_sort.svg';
import styles from '@styles/DropdownMenu.module.css';
import { ArticleOrderBy } from '@components/types/articleTypes';
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

@arthurkimdev arthurkimdev left a comment

Choose a reason for hiding this comment

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

지난번 리뷰 반영해주시느라 고생 많으셨습니다! 수고하셨습니다~ 👍

Comment on lines +16 to +49
export function AuthProvider({ children }: AuthProviderProps) {
async function login(credentials: { email: string; password: string }) {
try {
const response = await axiosInstance.post('/auth/signIn', credentials);
const { accessToken } = response.data;

localStorage.setItem('accessToken', accessToken);
console.log('로그인 성공');
} catch (error) {
console.error('로그인 실패:', error);
throw new Error('로그인에 실패했습니다.');
}
}

return (
<AuthContext.Provider
value={{
login,
}}
>
{children}
</AuthContext.Provider>
);
}

export function useAuth() {
const context = useContext(AuthContext);

if (!context) {
throw new Error('반드시 AuthProvider 안에서 사용해야 합니다.');
}

return context;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

context API를 사용할 때는, value를 useMemo로 감싸서 불필요한 리렌더링 방지 처리를 꼭 해줘야해요.
라이브러리(redux, zustand 등) 을 사용 안하게 될 땐, 리랜더링 최적화를 저희가 직접 챙겨야하거든요.

const value = useMemo(() => ({
    login,
  }), [login]);

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );

Copy link
Collaborator

Choose a reason for hiding this comment

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

useMemo뿐 아니라, useCallback도 추가할 수 있습니다.

const login = useCallback(async (credentials: { email: string; password: string }) => {
    try {
      const response = await axiosInstance.post('/auth/signIn', credentials);
      const { accessToken } = response.data;

      localStorage.setItem('accessToken', accessToken);
      console.log('로그인 성공');
    } catch (error) {
      console.error('로그인 실패:', error);
      throw new Error('로그인에 실패했습니다.');
    }
  }, []);

https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-constructed-context-values.md

Comment on lines +35 to +74
const handleInputChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => handleChange(e.target.name, e.target.value);

useEffect(() => {
const { title, content } = values;
if (title && content) {
setIsFormValid(true);
} else {
setIsFormValid(false);
}
}, [values]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

const { title, content } = values;
const accessToken = localStorage.getItem('accessToken');

if (!accessToken) {
alert('로그인이 필요합니다.');
return;
}

const data = {
title,
content,
image:
'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/Sprint_Mission/user/3/1721991853452/5389615.png',
};

try {
const response = await axiosInstance.post('articles', data, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
console.log('게시글 등록 성공:', response.data);
alert('게시글이 등록되었습니다.');
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 파일 전반적으로 아래 처럼 커스텀 훅을 통해 로직이 분리되어 개선이 되면 코드 가독성이 향상되고, 유지보수에 도움이 될 것 같아요. 아래 구조로 한번 고민해보세요.

export default function AddBoardPage({
  initialValues = INITIAL_VALUES,
  initialPreview,
}: BoardCreateFormProps) {
  const { values, isValid, handleChange } = useFormState(initialValues);
  const { submitArticle } = useArticleSubmit();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await submitArticle({
        title: values.title,
        content: values.content,
        image: '...' // 이미지 URL
      });
      alert('게시글이 등록되었습니다.');
    } catch (error) {
      alert('게시글 등록에 실패했습니다.');
      console.error(error);
    }
  };

  return (
    <div className={styles.container}>
      <form onSubmit={handleSubmit}>
        <FormHeader isValid={isValid} />
        <div className={styles['container-body']}>
          <FormField
            label="*제목"
            name="title"
            value={values.title}
            onChange={handleInputChange}
            placeholder="제목을 입력해주세요."
          />
          <FormField
            as="textarea"
            label="*내용"
            name="content"
            value={values.content}
            onChange={handleInputChange}
            placeholder="내용을 입력해주세요."
          />
          <FileInput
            label="이미지"
            name="imgFile"
            value={values.imgFile}
            initialPreview={initialPreview}
            onChange={handleChange}
          />
        </div>
      </form>
    </div>
  );
}

Comment on lines +164 to +194
{comments.map((comment, index) => (
<div className={styles['comment-container']} key={index}>
<div className={styles['comment-header']}>
<div className={styles['comment-content']}>
{comment.content}
</div>
<SortIcon />
</div>
<div className={styles['comment-info']}>
<div className={styles['comment-info-content']}>
{comment.writer.image ? (
<img
className={styles['profile-icon']}
src={comment.writer.image}
/>
) : (
<Profile className={styles['profile-icon']} />
)}
<div className={styles['comment-user-info']}>
<div className={styles['comment-nickname']}>
{comment.writer.nickname}
</div>
<div className={styles['comment-updatedAt']}>
{formatDate(comment.updatedAt)}
</div>
</div>
</div>
</div>
</div>
))}
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 리스트를 map 사용해서 UI 표시할 수 있는 부분은 <CommentList comments={comments} /> 이렇게 별도로 분리해서 가독성을 향상 시키면 좋겠습니다. 😸

Comment on lines +12 to +18
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isEmailValid, setIsEmailValid] = useState(true);
const [isPasswordValid, setIsPasswordValid] = useState(true);
const [isFormValid, setIsFormValid] = useState(false);
const [showPassword, setShowPassword] = useState(false);

Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 객체로도 관리할 수 있겠어요! 🧐

const [formData, setFormData] = useState<LoginFormState>({
    email: '',
    password: ''
  });
  const [validation, setValidation] = useState<ValidationState>({
    email: true,
    password: true,
    form: false
  });

Comment on lines +22 to +28
useEffect(() => {
const emailValid = validateEmail(email);
const passwordValid = password.length >= 8;
setIsEmailValid(emailValid || email === '');
setIsPasswordValid(passwordValid || password === '');
setIsFormValid(emailValid && passwordValid);
}, [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.

아래처럼 검증로직을 보다 더 단순화할 수 있습니다.

  const validateEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

  useEffect(() => {
    const isEmailValid = validateEmail(formData.email);
    const isPasswordValid = formData.password.length >= 8;

    setValidation({
      email: isEmailValid || !formData.email,
      password: isPasswordValid || !formData.password,
      form: isEmailValid && isPasswordValid
    });
  }, [formData]);

@arthurkimdev arthurkimdev merged commit f807e44 into codeit-bootcamp-frontend:Next-오병훈 Dec 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants