<Project> 밀리의 서재

Youje0·2022년 12월 4일
0

😏 2차 프로젝트

마지막 프로젝트? 라고 할 수 있는 2차 프로젝트가 끝났다.
나름 1차에 비해 수월하게 진행한 것 같기도하고 재미도 있었던 것 같다.
그럼 2차 프로젝트를 마치며 그에 대한 회고를 해보겠다.

1. 밀리의 서재를 선택한 이유?

일단 UI적으로 밀리의 서재가 너무 이뻤고 후보군 중 가장 기능적으로도 구현할 것이 많았다.

또한 우리팀의 프론트엔드가 3명이였는데 1차땐 4명이였던걸 감안해서 너무 복잡하고

이것저것 UI가 많이 들어가는 사이트를 제외하니 밀리의 서재가 가장 적합했던 것 같다.

2. 밀리의 서재에서 맡은 파트는 ?

  • 로그인 및 회원가입 비밀번호 변경

  • 검색 탭 UI

  • 검색 기능

  • 카테고리 내부 UI

크게 위의 4가지를 맡아서 구현하였다.
물론 작게작게 구현한 것도 여러가지 있다.

3. 상세하게 어떤걸 만들었나요 ?

1-1. 로그인 기능

function Login() {
<!-- id 및 password를 저장하는 state -->
  const [email, setEmail] = useState();
  const [password, setPassword] = useState();
  const navigate = useNavigate();

  const emailHandler = e => {
    setEmail(e.currentTarget.value);
  };

  const passwordHandler = e => {
    setPassword(e.currentTarget.value);
  };

<!-- onclick시 back에 해당 email과 password를 전송하고 token이 있을 시 home으로 이동 -->
  const POSTloginInfo = () => {
    fetch('http://localhost:8000/user/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
      }),
    })
      .then(res => res.json())
      .then(res => {
        if (res.token) {
          localStorage.setItem('token', res.token);
          navigate(`/home`);
        }
      });
  };
 
  const onKeyPress = event => {
    if (event.key === 'Enter') {
      POSTloginInfo();
    }
  };

1-2. 회원가입

function Signup() {
  const [email, setEmail] = useState('');
  const [nickName, setName] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [validEmail, setValidEmail] = useState(false);
  const [validPassword, setValidPassword] = useState(false);

  const EMAIL_REGEX = /^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
  const PWD_REGEX =
    /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{10,}$/;

  const navigate = useNavigate();

  useEffect(() => {
    setValidEmail(EMAIL_REGEX.test(email));
  }, [email]);

  useEffect(() => {
    setValidPassword(PWD_REGEX.test(password));
  }, [password]);

  const onEmailHandler = event => {
    setEmail(event.currentTarget.value);
  };
  const onNameHandler = event => {
    setName(event.currentTarget.value);
  };
  const onPasswordHandler = event => {
    setPassword(event.currentTarget.value);
  };

  const onConfirmPasswordHandler = event => {
    setConfirmPassword(event.currentTarget.value);
  };

  const POSTUserInfo = () => {
    fetch('http://localhost:8000/user/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
        nickname: nickName,
      }),
    }).then(res => res.json());

    if (password && validPassword) {
      navigate(`/login`);
      alert('회원가입 완료!');
    }
  };

  const onKeyPress = event => {
    if (event.key === 'Enter') {
      POSTUserInfo();
    }
  };

회원가입은 유효성 검사 후 맞지 않으면 p태그로 작성하고 display none으로 숨겨두었던
경고 문구가 뜨게 만들었다.

1-3 비밀번호 변경

function PasswordChange() {
  const [certification, setCertification] = useState(false);
  const [certificationNumber, setCertificationNumber] = useState([]);
  const [certificationInput, setCertificationInput] = useState('');
  const [email, setEmail] = useState('');
  const [passwordChange, setPasswordChange] = useState('');
  const [passwordChangeCheck, setPasswordChangeCheck] = useState('');
  const [validEmail, setValidEmail] = useState(false);
  const [validPassword, setValidPassword] = useState(false);
  const refSend = useRef(null);
  const refPasswordChange = useRef(null);
  const refPasswordChangeCheck = useRef(null);
  const refChange = useRef(null);
  const refCertificationInputBox = useRef(null);
  const navi = useNavigate();

  const EMAIL_REGEX = /^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
  const PWD_REGEX =
    /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{10,}$/;

  useEffect(() => {
    setValidEmail(EMAIL_REGEX.test(email));
  }, [email]);

  useEffect(() => {
    setValidPassword(PWD_REGEX.test(passwordChange));
  }, [passwordChange]);

  useEffect(() => {
    if (certification === true) {
      fetch('http://localhost:8000/mail', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email: email,
        }),
      })
        .then(res => res.json())
        .then(json => setCertificationNumber(json));
      refSend.current.style.display = 'none';
      refChange.current.style.display = 'block';
      refCertificationInputBox.current.style.display = 'block';
      alert('인증번호 발송완료!');
      return setCertification(false);
    }
  }, [certification]);

  const emailInputHandler = event => {
    setEmail(event.currentTarget.value);
  };

  const certificationInputHandler = event => {
    setCertificationInput(event.currentTarget.value);
  };
  const passwordChangeInputHandler = event => {
    setPasswordChange(event.currentTarget.value);
  };
  const passwordChangeCheckInputHandler = event => {
    setPasswordChangeCheck(event.currentTarget.value);
  };

  const setCertificationSameHandler = () => {
    if (certificationInput == checkNum) {
      // setCertificationSame(true);
      alert('인증 완료!');
      refPasswordChange.current.style.display = 'block';
      refPasswordChangeCheck.current.style.display = 'block';
    } else if (certificationInput != checkNum) {
      // setCertificationSame(false);
      alert('인증번호가 다릅니다.');
    }
  };

  const checkNum = certificationNumber.authenNum;

  const changePassword = () => {
    if (passwordChange == passwordChangeCheck) {
      fetch('http://localhost:8000/user/changepw', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          email: email,
          password: passwordChange,
        }),
      }).then(res => res(alert('비밀번호 변경되었습니다.!')));
      navi('/login');
    } else if (passwordChange != passwordChangeCheck) {
      alert('비밀번호가 다릅니다.');
    }
  };

만들었던 기능 중 가장 재밌는 기능이였는데 실제로 이메일과 연동하여 이메일로 인증번호를 받아

그 인증번호가 같으면 비밀번호를 변경 할 수 있게 만들었다.

프론트쪽보단 백에서 구현할게 더 많았던 기능이지만 그래도 뭔가 구현하고 실제로

이메일과 연동되서 비밀번호를 변경하니 완성하고 엄청 뿌듯했던 기능이였던 것 같다.

2-1 검색 탭 UI


CSS 적인 부분이 많고 기능적인 코드는 많이 없지만 실제 밀리의 서재랑 거의 비슷하게

만든 것 같아 나름 만족스럽다.

https://velog.io/@xhdckd12/%EB%82%B4-%EC%86%90%EC%9C%BC%EB%A1%9C-%EC%A7%81%EC%A0%91-%EB%A7%8C%EB%93%A0-carousel

혼자 끙끙대며 고민해서 직접 만든 carousel..

별거 아닌 기능이지만 직접 만들어서 기억에 많이 남는 기능이다.

3. 검색 기능

function SearchBar() {
  const [searchClick, setSearchClick] = useState(false);
  const [searchBarContent, setSearchBarContent] = useState('');
  const [searchDisplay, setSearchDisplay] = useState(true);
  const [book, setBook] = useState([]);
  const refSearchBar = useRef(null);

  useEffect(() => {
    if (searchBarContent !== '')
      fetch('http://localhost:8000/category/search', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          text: searchBarContent,
        }),
      })
        .then(res => res.json())
        .then(json => setBook(json.data));
  }, [searchBarContent]);

<!-- 클릭 시 z-index가 100이 되어 가장 상단에 render 됨. -->
  const searchBarOpen = () => {
    setSearchClick(true);
    refSearchBar.current.style.zIndex = 100;
  };
<!-- 닫기를 눌렀을때 다시 zindex 값을 낮춰 밑으로 내림 -->
  const searchBarClose = () => {
    setSearchClick(false);
    refSearchBar.current.style.zIndex = 50;
  };

  const inputBarHanle = e => {
    setSearchBarContent(e.target.value);
  };

<!-- 검색창에 아무것도 입력하지 않았을 경우 특정 메세지가 보이게 함. -->
  useEffect(() => {
    if (searchBarContent === '') {
      setSearchDisplay(true);
    } else if (searchBarContent !== '') {
      setSearchDisplay(false);
    }
  }, [searchBarContent]);

이번 프로젝트 중 가장 어려웠던 파트가 검색이였던 것 같은데 검색 창 클릭시 modal 식으로

아예 다른 UI를 보여줘야했어서 사실 경험이 없었기 때문에 어떻게 구현할지 몰라

일단 z-index를 사용하여 가장 밑에 항상 렌더시켜놓다가 searchBarOpen 함수가 호출되었을때

z-index값을 높여서 화면에 보여지게하는 방식으로 만들었다.

사실 검색해보면 여러가지 방법이 있었을거 같은데 뭔가 검색없이 혼자 생각해서 만들어서

더 뿌듯했던 것 같다.

4. 카테고리 기능

function Categorydetails() {
  const [bookList, setBookList] = useState([]);
  const { id } = useParams();
  const [popup, setPopup] = useState(false);

  useEffect(() => {
    setPopup(false);
  }, []);

  useEffect(() => {
    fetch(`http://localhost:8000/category/${id}`)
      .then(res => res.json())
      .then(data => setBookList(data.data[0]));
  }, []);

  const books = bookList.books;

  return (
    <div>
      <div className="categoryDetailWrap">
        <div className="categoryTapHead">
          <h2>{bookList.content}</h2>
        </div>
        <article className="categoryName">
          <strong className="showAll" onClick={() => setPopup(true)}>
            {bookList.content} 전체보기
            <span className="showAllSpan"></span>
          </strong>
          {popup && <Filtermodal setPopup={setPopup} popup={popup} />}
        </article>
        <div className="bookList">
          <div className="bookListName">{bookList.content} 인기 도서</div>
          <div className="bookListWraper">
            <ul className="booksWraper">
              {books &&
                books.map(book => {
                  const { title, cover_img, author_name, id } = book;
                  return (
                    <CategoryList
                      key={id}
                      title={title}
                      cover_img={cover_img}
                      author_name={author_name}
                      idx={id}
                    />
                  );
                })}
            </ul>
          </div>
        </div>
      </div>
      <footer className="footer" />
    </div>
  );
}

<!-- CategoryList -->

function CategoryList({ author_name, cover_img, title, idx }) {
  const navi = useNavigate();
  const goToDetails = () => {
    navi(`/bookDetail/${idx}`);
  };
  return (
    <div>
      <li onClick={goToDetails} className="books">
        <div className="flexBox">
          <img className="flexBoxImg" alt="" src={cover_img} />
          <strong className="title">{title}</strong>
          <p className="authorName">{author_name}</p>
        </div>
      </li>
    </div>
  );
}

4. 프로젝트 중 어려웠던 부분?

사실 이번 2차땐 엄청 어렵다고 느끼는 부분은 많이 없었던 것 같다.
굳이 꼽자면 3번 검색 기능이 어려웠는데 열심히 혼자서 막 검색해보고 찾아보고 하다가
back 이랑 소통해서 만드는게 훨씬 쉽고 깔끔한 결과가 나와서 back이랑 소통해서 해결했다.
검색 기능을 만들면서 back과의 소통이 얼마나 중요한지 느끼게 된 것 같다.

5. 회고를 마치며..

프로젝트 중엔 몰랐는데 막상 프로젝트가 끝나고 회고하며 생각해보니 막상 이번 프로젝트는
뭔가 보수적으로 진행한 것 같다. 아마 1차때 뭣도 모르고 이것저것 건드리다 맨날 밤샜는데 막상 밤새 고민해도 해결 못하고.. 그랬던 기억 때문에 이번 프로젝트는 좀 보수적으로 한게 아닐까 싶다.

뭔가 2차땐 새로운 기술이나 새로운 라이브러리를 활용해서 더 멋지게 만들고 싶었는데..
그래도 이번 2차 프로젝트때 느낀점은 내가 실력이 정말 많이 늘었다는걸 또 다시 느꼈다.

1차때도 정말 많이 성장했다고 느꼈었는데 2차를 끝낸지금 다시 회상해보면 어떤 기능을 구현하고자 할때 무조건 검색부터 하는게 아닌 스스로 생각해서 어느정도 로직을 어떻게 구현할지 가닥을 머리속에서 잡아놓고 거기에 필요한 메소드나 기술등을 검색해서 활용하는 수준까지 온 것 같다.

이제 더 이상 어떠한 고민도없이 복붙해서 해결하는 코딩이 아닌 스스로 어떻게 구현할지 고민하며 활용하는 개발자가 된 것 같아 나름 만족스럽게 진행한 프로젝트라고 생각한다.

6. 시연영상

https://www.youtube.com/watch?v=VrlCtUhPVYQ&ab_channel=%EC%9C%A0%EC%A0%9C%EC%98%81%ED%99%94%EC%9D%B4%ED%8C%85

✨사용한 기술 스택

backEnd

  • EXPRESS
  • MYSQL
  • NODE.JS

frontEnd

  • HTML/CSS
  • JavaScript(ES6+)
  • React
  • SASS
profile
ㅠㅠ

0개의 댓글