Project/[SAFU] 1st Project

8. [Client & Server] Mypage와 Edit Userinfo (Oct 26, 2020 ~ Oct 30, 2020회고)

HJChung 2020. 11. 6. 18:09

Mypage -> 개인정보 수정 -> githubID 수정

 

Mypage -> 개인정보 수정 -> password 수정

점점  SAFU 웹페이지가 예뻐지고 있다! SH님께서 CSS를 전적으로 담당해주셔서 시간이 갈 수록 점점 예뻐지고 있다. 

회원가입, 로그인, 로그아웃, 아이디 비번 찾기, 리뷰 카드 랜더링, 로그인 전/후 메인 페이지 까지 구현이 완료 되었다. 

 

로그인 후 Mypage 버튼을 누르면 이 페이지로 이동하며 Mypage는 <Mypage/> 컴포넌트는 회원 정보 출력과 <CardEditList/> 컴포넌트로 구성되어 있다. 

들어가자마자 자신의 회원 정보와 자신이 작성한 리뷰카드들이 보여야 하며

<Mypage/> 컴포넌트의 '회원정보수정' 버튼을 누르면 비밀번호와 githubID를 수정할 수 있는 /Editinfo 페이지로 넘어간다. 

그리고 <CardEditList/> 컴포넌트의 각 카드인 <CardEdit/> 컴포넌트에서 바로 리뷰 수정을 할 수도 있다. 

 

여기서 Oct 26, 2020 ~ Oct 30, 2020 동안 구현한 것은 Mypage와 InfoEdit(개인정보 수정) 컴포넌트 였다!

1. GET Mypage API와 Mypage 컴포넌트 구현 

/Mypage 주소로 들어가자마자 자신의 회원 정보와 자신이 작성한 리뷰카드들이 보여야 하기 때문에 바로 서버에 GET 요청을 보낸다. 

GET Mypage API에서는 [{email: , password: , githubID: }, {{리뷰1}, {리뷰2}, ...}} 이런식의 회원 정보와 작성한 리뷰정보가 담긴 응답이 client로 전달되어야 한다. 

 

이 API는 HI님께서 담당해주셨다. 그런데 HI님과 코드를 함께 리뷰하다보니, 고민할 부분이 생겼다. outer join 방식으로 리뷰를 작성하지 않은 사용자의 경우에도 사용자의 정보를 가져올 수는 있게 짜려고 하다보니 리뷰정보가 아에 담기지 않는 것이 아니라 null값으로 담겨 오기 때문에 랜더링시 'Can not read property name of 'null'' 이런 에러가 발생하게 된다는 것이었다. 


GET Mypage API 구현시 HI님과 고민했던 것을 기록한 것은 <더보기>를 클릭!

더보기
const { reviews } = require('../../models');
const { users } = require('../../models');
const { bootcamp_list } = require('../../models');
module.exports = {
  get: (req, res) => {
    const session = req.session;
    console.log('session', req.session.userid);
    if (session.userid) {
      reviews
        .findAll({
          //해당 user가 작성한 review들만 가져와야하므로 users 테이블에서 데이터를 가져오는게 아니라 reviews 테이블에서 데이터를 가져와야합니다.
          where: {
            users_id: session.userid, //Reviews 테이블 에서는 req.sesssion.userid가 들어있는 column명이 users_id 이다. (그냥 id가 아닙니다)
          },
          include: [
            //이제 나머지 users테이블과 bootcamp_list 테이블에서도 데이터를 받아와야하므로 외래키 연결
            {
              model: bootcamp_list,
              as: 'bootcampname',
              attributes: ['name'],
            },
            {
              model: users,
              right: true, //right outer join
              as: 'useremail',
            },
          ],
        })
        .then((data) => {
          console.log(data);
          res.status(200).json(data); //then을 써서 비동기로 처리해 주어야 합니다. then없이 기존의 방식대로 하면 그냥 이렇게 하면 data는 undefined로 뜹니다.
        })
        .catch((err) => {
          console.log('에러');
          res.status(404).send('err');
        });
    } else {
      res.status(401).send('Unauthorized');
    }
  },
};

이렇게 하면 결과는 

{
	active: true
	bootcamp_id: 1
	bootcampname: {name: "codestates"}
	comment: "자기주도 학습!!!!중심이다"
	createdAt: "2020-10-23T06:49:03.000Z"
	curriculum: "어려움"
	githublink: "https://github.com/codestates/SAFU-server.git"
	id: 1
	level: "어려움"
	price: "비쌈"
	recommend: "추천"
	updatedAt: "2020-10-23T06:49:03.000Z"
	useremail: {email: "jhjyj5414@naver.com", 
    		githubId: "Gracechung-sw", 
            password: "//비밀번호"}
	users_id: 12
}

이렇게 잘 받아옵니다. 

그러나 에러 뿐만 아니라 기능 구현상의 문제가 존재했습니다. 

문제 1) 리뷰카드를 하나도 쓰지 않은 사용자에 대해서는

 reviews
        .findAll({
          //해당 user가 작성한 review들만 가져와야하므로 users 테이블에서 데이터를 가져오는게 아니라 reviews 테이블에서 데이터를 가져와야합니다.
          where: {
            users_id: session.userid, //Reviews 테이블 에서는 req.sesssion.userid가 들어있는 column명이 users_id 이다. (그냥 id가 아닙니다)
          },

이 조건을 만족하는 사람이 없기 때문에 client는 빈 배열을 응답으로 받게 됩니다. 그러나 Mypage에서는 리뷰를 작성하지 않은 사용자에 대해서도 

useremail: {email: "jhjyj5414@naver.com", 
    		githubId: "Gracechung-sw", 
            password: "//비밀번호"}

이 세가지 정보는 받아야만 합니다. 

이 분홍색 박스 쳐 놓은 곳에 랜더링 되어야 하고, 개인정보수정을 누르면 리다이렉트 되는 <Editinfo/> 컴포넌트에게도 props로 전달이 되어야 하기 때문입니다. 
이를 해결하기 위해서 아에 처음에 다 받아온 뒤에 회원의 세션 번호(userid)에 해당하는 것만 걸러주는 것으로 필터링을 하면 
client로

이렇게 리뷰카드를 작성하지 않은 user 정보에 대해서도 응답을 받을 수 있습니다. (review카드를 작성하지 않았기 때문에 null이라고 뜨는 것임)

그런데 여기서 문제가 또 발생합니다. 



문제 2) null 값에 대해서는 (SH님께서 Card Edit 컴포넌트 구현을 어떻게 짜실지는 모르겠지만) 

null 값을 랜더링 하라고 하면 이런 에러가 발생하게 됩니다. 

그래서 제 생각은 

 

Mypage라는 같은 페이지 내에서 사용하는 API이지만 그 페이지에 있는 다른 컴포넌트에서 필요한 데이터가 다르기 때문에

 

GET Mypage라는 하나의 API를 사용하는 것보다.

GET Userinfo, GET Userreview 이런 두 개의 API로 따로 사용하는게 좋을 것 같다는 생각이 계속 듭니다...
그러면 저 위에 언급된 문제는 다 사라지게 되니까요. 

그래서 진짜 다시 GET UserReview(해당 사용자가 작성한 리뷰들을 GET해오는 API)와 GET Userinfo(해당 사용자의 회원가입시 작성한 정보를 GET해오는 API)를 따로 만들어야 하나... 생각을 해보다가.

그냥 아에, 1) 리뷰를 작성한 사용자(즉 CardEdit 에 랜더링 될 것이 있는 사용자)와 2) 리뷰를 한 번도 작성하지 않은 사용자(즉 CardEdit 에 랜더링 될 것이 없는 사용자) 를 조건문으로 분기하면 어떨까라는 생각이 들었다. 

 

정리하면,

1.세션을 확인해서 users 테이블에서 지금 로그인한 사용자의 데이터를 찾는다. 

2. 그 사용자가 작성한 리뷰가 있는지 reviews 테이블에서 찾는다. 

3-1. 작성한 리뷰가 없으면 그냥 사용자 정보만 담아서 응답해주고

3-2. 작성한 리뷰가 있으면 그 리뷰 정보까지 함께 담아서 응답해준다. 

 

그렇게 해서 완성된 server GET Mypage API 코드

const { users } = require('../../models');
const { reviews } = require('../../models');
const { bootcamp_list } = require('../../models');

module.exports = {
  get: (req, res) => {
    const session = req.session;
    const users_session_id = session.userid;
    let result;
    users //1.세션을 확인해서 users 테이블에서 지금 로그인한 사용자의 데이터를 찾는다. 
      .findOne({
        where: {
          id: users_session_id,
        },
      })
      .then((user) => {
        if (!user) {
          res.staatus(401).send('unvalid name');
        } else {
          result = user;
          reviews
            .findAll({ // 2. 그 사용자가 작성한 리뷰가 있는지 reviews 테이블에서 찾는다. 
              where: { users_id: users_session_id, active: true },
              include: [
                {
                  model: users,
                  as: 'useremail',
                  where: {
                    active: true,
                  },
                  attributes: ['email'],
                },
                {
                  model: bootcamp_list,
                  as: 'bootcampname',
                  attributes: ['name'],
                },
              ],
            })
            .then((data) => {
              if (data.length === 0) { //3-1. 작성한 리뷰가 없으면 그냥 사용자 정보만 담아서 응답해주고
                return res.status(200).json([result]);
              } else { //3-2. 작성한 리뷰가 있으면 그 리뷰 정보까지 함께 담아서 응답해준다. 
                return res.status(200).json([result, data]);
              }
            })
            .catch((err) => {
              res.status(500).send('err');
            });
        }
      })
      .catch((err) => {
        res.status(500).send('err');
      });
  },
};

 

그 결과, 리뷰 카드 유무에 상관 없이 컴포넌트에 랜더링 되어야하는 회원 정보를 잘 전달되었고
Client의 <Mypage/>, <CardEdit/>, <Editinfo/> 컴포넌트와 통신 확인 후 정상 랜더링 까지 확인되었다. 

 

  • 리뷰를 작성한 유저의 GET mypage 응답

  • 리뷰를 한 번도 작성하지 않은 유저의 GET mypage 응답

 

이에 맞게 작성한 Client 코드)

//Mypage.js - 로그인 후 Nav 바의 Mypage를 누르면 이 <Mypaage>컴포넌트에서 개인정보 열람과 수정,
// 자신이 작성한 <CardList>가 나타난다.
import axios from 'axios';
import React, { useState, useEffect } from 'react';
import CardEditList from './CardEditList';

function Mypage(props) {
  const [loading, setLoading] = useState(true);
  const [users, setUsers] = useState([]);
  useEffect(() => {
    axios({
      method: 'get',
      url: 'http://localhost:4000/users/read',
    }).then((users) => {
      setUsers(users.data);
      setLoading(false);
    });
  }, []);
  if (loading) return <div>Loading...</div>;
  return (
   {/* 회원 정보가 나타나고,  */}
    <div className="mypage-div">
      <ul className="mypage-ul">
        <li className="mypage-email mypage-li">
          {console.log('users', users[0])}
          <p>E-mail</p>
          <p>{users[0].email}</p>
        </li>
        <li className="mypage-githubID mypage-li">
          <p>Github ID</p>
          <p>{users[0].githubId}</p>
        </li>
        <li className="mypage-infoEdit mypage-li">
          <div>
            {/*여기에 회원정보수정 버튼이 들어가고 그 버튼을 누르면 <Editinfo/> 가 구현된 페이지로 넘어간다.*/}
          </div>
        </li>
      </ul>
      <ul>
      {/* 리뷰 유무에 따라 <CardEditList/> 가 랜더링 되기고, 안되기도 한다.*/}
        {users[1] !== undefined ? (
          <CardEditList userInfo={users[1]}></CardEditList>
        ) : (
          <p>
            아직 작성된 카드가 없습니다.<br></br> 후기를 등록해보세요!
          </p>
        )}
      </ul>
    </div>
  );
}
export default Mypage;

2. PUT editUserInfo API와  Infoedit컴포넌트 구현 

GET Mypage API에서 가져온 회원 정보 중 이메일과 githubID는 화면에 랜더링이 되고, 회원 정보수정 버튼을 클릭하면 

/ Infoedit로 리다이렉트 되면서  Infoedit 컴포넌트로 회원 정보 중 이메일, githubID, password가 props 로 전달

되도록 하였다.  그리고 이렇게 전달된 props값들을  constructor에서 default값으로 state에 저장해두었다.

왜냐하면
1. 회원 정보 수정 중에도 기존 비밀번호가 같은지를 비교 검사해서, 기존 비밀번호와 동일하면 동일하다는 메세지를 출력하여서 수정을 거부하고자 했다. 그래서 
기존 비밀번호가 어딘가 저장되어있어야 한다고 생각했기 때문이고,

2. password와 githubID 둘 중 하나만 변경하고 싶을 수도 있으므로 일단 constructor에서 default값으로 원래 사용자의 정보를 넣어고, 수정칸에 입력없이 수정하기를 클릭하면 그냥 원래 정보로 남아있도록 하기 위해서라도 githubID, 비밀번호가 어딘가 저장되어있어야 한다고 생각했기 때문이다.

 

※ 이런 방식에는 아주 큰 문제가 있다. (Nov 4, 2020 에 받은 피드백)

비밀번호나 사용자의 정보 같은 것들을 client에 '저장' 시켜놓는 것은 MUST NOT!!!!! 절대 하면 안된다!!!! 는 피드백을 받았다. 

그래서 현재 이를 모두 API에서 필요시 바로 받아오는 형태로 코드를 수정하려고 한다. 

 

아무튼.. 피드백을 받기 전 당시 완성된 server PUT editUserInfo API코드

const { users } = require('../../models');

module.exports = {
  put: (req, res) => {
    const { useremail, password, githubId } = req.body;

    users
      .update(
        {
          password: password,
          githubId: githubId,
        },
        {
          where: {
            email: useremail,
          },
        },
      )
      .then((data) => {
        console.log('개인정보수정 완료');
        req.session.destroy((err) => {
          if (err) {
            res.status(400).send('you are currently not logined');
          } else {
            console.log('세션 제거 완료');
            res.status(200).send('정보수정 완료');
          }
        });
      })
      .catch((err) => {
        console.log(err);
        res.sendStatus(500);
      });
  },
};

이에 맞게 작성한 Client 코드)

Mypage 컴포넌트의 회원정보 수정 페이지로 넘어가는 버튼 구현 부분 )

(위의 Mypage 컴포넌트 코드에 주석처리로 버튼 자리라고 남겨놓은 부분에 들어갈 코드이다.)

  <button
      onClick={(e) => {
         e.preventDefault();
          props.history.push({
             pathname: '/Infoedit',
              userInfo: {
                    email: users[0].email,
                    password: users[0].password,
                    githubId: users[0].githubId,
                  },
                });
              }}
            >
       회원정보수정
 </button>

<Infoedit/> 컴포넌트 주요 구현 부분)

(전체 코드는 너무 길어서.. 위에서 언급한 부분에 대한 코드만 적어보자면 아래와 같다.)

import React from 'react';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
axios.defaults.withCredentials = true;

const crypto = require('crypto');

const hash = function (password) {
  return crypto.createHash('sha512').update(password).digest('hex');
};

class Infoedit extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      useremail: this.props.history.location.userInfo.email,
      password: this.props.history.location.userInfo.password,
      passwordCheck: this.props.history.location.userInfo.password,
      githubId: this.props.history.location.userInfo.githubId,
      isAvailedPassword: '',
      isAvailedPasswordCheck: '',
    };
  }

  handleInfoEditValue = (key) => (e) => {
    if (key === 'password') {
      var reg = /^(?=.*?[a-z])(?=.*?[0-9]).{8,}$/;
      var password = e.target.value;
      if (password.length > 0 && false === reg.test(password)) {
        this.setState({
          isAvailedPassword: '비밀번호는 8자 이상이어야 하며, 숫자/소문자를 모두 포함해야 합니다.',
        });
      } else if (hash(password) === this.state.passwordCheck) {
        console.log('password: ', password);
        this.setState({ isAvailedPassword: '이전 비밀번호와 동일합니다.' });
      } else {
        this.setState({ isAvailedPassword: '' });
        this.setState({ [key]: hash(e.target.value) });
      }
    }
    if (key === 'passwordCheck') {
      var passwordCheck = hash(e.target.value);
      if (passwordCheck.length > 0 && this.state.password !== passwordCheck) {
        this.setState({ isAvailedPasswordCheck: '비밀번호가 일치하지 않습니다.' });
      } else {
        this.setState({ isAvailedPasswordCheck: '' });
        this.setState({ [key]: passwordCheck });
      }
    }
    if (key === 'githubId') {
      this.setState({ [key]: e.target.value });
    }
  };

  handleInfoEditButton = () => {
    if (this.state.isAvailedPassword === '' && this.state.isAvailedPasswordCheck === '') {
      axios({
        method: 'put',
        url: 'http://localhost:4000/users/edit',
        data: {
          useremail: this.state.useremail,
          password: this.state.password,
          githubId: this.state.githubId,
        },
      })
        .then((res) => {
          window.location = '/Login';
          console.log('개인정보수정 완료');
        })
        .catch((err) => {
          console.error(err);
        });
    } else {
      alert('수정할 것이 없습니다.');
    }
  };

 

3. 이 이슈카드를 수행하면서 얻은 교훈...

기능 구현을 어떻게 했느냐도 중요하지만 이 이슈카드를 수행하면서는  정말 깨달은 바가 많다. 먼저

논의 후엔 항상 기록으로 남기기! 

API 논의가 발생한 후 논의 결과로 API 수정본이 나온적이 있었다. 그런데 이 결과가 gitbook에 제대로 업데이트가 되지 않았을 뿐만 아니라 나 역시 따로 기록도 해놓지 않았었다. 그래서 다시 한번 같은 문제로 동일한 질문이 나왔다.

추가로 리뷰 데이터를 받아오는 부분에서는 서버파트와 클라이언트 파트가 서로 다르게 이해하고 있었다.

해당 사용자가 작성한 리뷰들을 서버파트 분들은 서버에서 DB에서 데이터를 가져올 때 where로 가져오는 것으로, 

클라이언트 파트 분들(나 포함)은 모든 리뷰들을 가져와서 클라에서 필터링 해주는 것으로 서로 엇갈리게 알고 있었다는 것이다.

이렇게 확실하지 않은 부분이 있어서 다시 한번 질문을 했다. 

 

꼭 논의 후엔 기록으로 남기고, 변동사항이 있으면 바로바로 업데이트 시켜 놓아야겠다는 다짐을 했다. 이런걸 챙기지 못한 팀장이란게 정말 부끄럽다ㅠ

=> 그래서 다음 파이널 프로젝트 때는 처음부터 이렇게 하면 어떨까 라는 생각을 담아서 만든 wiki의 우리 서비스 API문서이다. 초안은 내가 작성해 놓았고 HJ님께서 더 자세한 내용을 작성해주셨다. github.com/codestates/SAFU-client/wiki/API-Document

그리고 회의록도 작성했으면 좋겠다.