탑스터 웹앱 (1)

일주일동안 꽤 많이 진행되었다. 코드를 디자인한 방법이 정석적인 방법인지는 모르겠지만 어찌 됐든 돌아가기는 한다. 다음주에 어느정도 하려던 기능이 다 갖추어지면 처음부터 천천히 만져볼 생각이다.

이번주에 한 것들

만들어진 탑스터를 이미지로 변환해서 파일로 다운받을 수 있고, 서버에 업로드해서 볼 수 있다. sns 피드처럼 만들고 싶어서 회원가입과 로그인도 간단히 구현했다.

회원가입

 @app.route('/api/join', methods=['POST'])
def post_join():
    data = request.get_json()
    findID = db.user.find_one({"join_id":data['join_id']})
    if findID:
        return jsonify({'msg':"invalid"})
    
    password = data['join_password']
    bpw = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
    str_bpw = bpw.decode('utf-8')
    data['join_password'] = str_bpw
    print(data)
    db.user.insert_one(data)
    return jsonify({'msg':"registered"})

로그인 폼과 회원가입 폼을 같이 공유해서 쓰기 때문에 폼데이터가 아닌 입력한 정보를 그냥 json 형식으로 보냈다. 이미 있는 id면 그냥 리턴하고 새 id인 경우에 회원가입을 진행한다. bcrypt 라이브러리를 이용해서 암호화를 해서 db에 저장한다. 바이너리 형식으로 나왔나?해서 str로 디코딩.

로그인

@app.route('/api/login', methods=['POST'])
def post_login():
    data = request.get_json()
    password = data['login_password']
    
    findID = db.user.find_one({"join_id":data['login_id']},{'_id': False})
    if findID:
        db_bpw = findID['join_password']
        db_id = findID["join_id"]
        checkpw = bcrypt.checkpw(password.encode('utf-8'), db_bpw.encode('utf-8'))
        print(checkpw)
        if checkpw:
            jwt_token = jwt.encode({"id":db_id, 'exp':datetime.utcnow() + timedelta(weeks=5)}, app.config['JWT_SECRET_KEY'], algorithm)
            str_jwt = jwt_token.decode('utf-8')
            db.user.update({'join_id':db_id}, {'$set':{'jwt':str_jwt}})
            return jsonify({'msg':"allowed", "name":db_id,"access_token":str_jwt })
    print(data)
    return jsonify({'msg':"tryagain"})

받아온 데이터의 비밀번호와 db에 있는 비밀번호를 비교한다. db에 있는 값은 암호화된 값. checkpw가 true라면 로그인 과정을 진행한다. jwt 토큰을 발급해서 다시 클라이언트로 보내주자. 토큰의 payload에는 유저 id가 담겨있고, 유효기간은 5주로 설정했다. 리프레쉬토큰에 대해서 알고는 있지만 그건 다음 기회에.

if (msg == "allowed") {
        context.setUser({
          name: response.data.name,
          id: details.id,
        });
        const access_token = response.data.access_token;
        cookies.set("jwt", access_token, {
          path: "/",
          maxAge: 60 * 60 * 24 * 30, //httponly 옵션을 없애니까 된다,,
        });
      } else if (msg == "tryagain") {
        setError("ID와 비밀번호를 정확히 입력해 주세요. ");
      }

클라이언트에서는 받아온 jwt 토큰을 쿠키에 저장했다. universal-cookie 라는 패키지를 이용했음. httpOnly 옵션을 넣었더니 쿠키가 생성이 안됐다. 혹시 몰라서 뺐더니 되더라. 왜그럴까 찾아봤더니 도메인이 .com 같은 류가 아니면 막는다는데,,

html-to-image

npm 라이브러리를 쓰면 dom 요소를 이미지로 변환할 수 있다. 다양한 형태의 이미지파일을 얻을 수 있다.

const onSave = useCallback(() => {
    console.log(canvas.current.firstChild.style.width);
    if (canvas.current === null) {
      return;
    }
    toPng(canvas.current.firstChild, {
      cacheBust: true,
    })
      .then((dataUrl) => {
        console.log(dataUrl);
        const link = document.createElement("a");
        link.download = "my-topster.png";
        link.href = dataUrl;
        link.click();
      })
      .catch((err) => {
        console.log(err);
      });
  }, [canvas]);

useRef 훅으로 해당 dom을 가져와서 toPng 함수에 매개변수로 넘겼다. 이미지를 dataUrl 형식으로 반환하는데, 그대로 이미지 파일로 저장이 되더라.

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAk0AAAIpC...
대충 이런 형태이다.

이미지 업로드

SNS 피드처럼 업로드한 이미지들을 볼 수 있도록 했다. 이미지를 서버쪽에 저장하려고 했지만 어디다 저장할지가 고민. 개인적인 공부를 위해 하는 프로젝트라, 아마존s3 같은 스토리지 서비스를 쓰기엔 굳이 라는 느낌이 들었다. 서버의 로컬 디렉토리에 파일을 저장하기로 했다.

@app.route('/api/upload', methods=['POST'])
def post_topster():
    now = datetime.now()
    now2 = now.strftime("%D_%H%M_%S")
    date = now2.replace('/', '-')
    
    try:
        data = request.get_json()
        userid = request.args.get('user')

        absolute_path = os.path.abspath(__file__)
        path = os.path.dirname(absolute_path)
        path_root = os.path.dirname(path)
        path_user = path_root + '/images/' + userid

        if not os.path.exists(path_user):
            os.makedirs(path_user)

        dataValue = str(data['topsterimage'])
        dataBin=dataValue.split(',')[1]
        imgdata = base64.b64decode(dataBin)
    
        filename = path_user + '/' + date +'.png' 
        data = {"userid":userid, "filename":filename, "like":0,"likeuser": [None], "date":now}
        db.posts.insert_one(data)
        with open(filename, 'wb') as f:
            f.write(imgdata)
        
        return jsonify({'msg':"upload succeeded"})
    except:
        return jsonify({'msg':"error"})

images/'유저id'/'올린시간'.png 디렉토리에 파일을 저장한다. 폴더가 없으면 생성한 후에 저장한다. 자꾸 데이터가 바이너리 형태로 받아져서 (왠지 모르겠다..) 다시 base64로 인코딩해서 저장한다.

이미지 가져오기

const getPost = async () => {
    let userid;
    try {
    .
    .
    .
      const response = await axios({
        method: "GET",
        url: `http://***:5000/api/feed?user=${userid}&search=all&offset=${offset}&limit=5`,
      });
      const results = response.data.feedData;
      results.map((result, index) => {
        .
        .
        .
      );
      //더 불러오기
      if (results.length === 5) {
        setOffset((prev) => prev + 5);
      } else {
        setOffset(false);
      }
    } catch (error) {
      console.log(error);
    }
  };

offset과 limit 값을 쿼리파라미터로 넘겨준다. 값을 성공적으로 받아오면 offset을 limit만큼 키워서 넘겨줌.

#-------피드 가져오기-------
@app.route('/api/feed', methods=['GET'])
def get_feed():
    user = request.args.get('user')
    search = request.args.get('search', None)
    offset = request.args.get('offset', None)
    limit = request.args.get('limit', None)
    print("user:", user,"search:", search,"offset:", offset, "limit:", limit)
    if search :
        if search == "all": #전체 피드
            data = list(db.posts.find({}).sort("_id",-1).skip(int(offset)).limit(int(limit)))
        else: #내가 쓴 피드
            data = list(db.posts.find({'userid':search}).sort("_id",-1)) #새거부터 맨 위로
        newdata = list()
        for i in data:
            #이미지 파일 가져오기
            with open(i['filename'], "rb") as f:
                filedata = f.read()
                encoded = base64.b64encode(filedata)
                topsterimage = "data:image/png;base64," + encoded.decode('utf-8')
            #피드 좋아요 여부 가져오기
            
            likeuser = i['likeuser']
            if user in likeuser:
                likebool = 1
            else:
                likebool = 0
            doc = {'userid':i['userid'], 'topsterImage':topsterimage, 'date':i['date'], '_id':str(i["_id"]), "like":i['like'], "likebool":likebool }
            
            newdata.append(doc)
        return jsonify({"feedData":newdata})      
       
    return jsonify({'msg':"received"})

mongodb 데이터를 가져올 때 .skip 과 .limit 로 가져올 수 있다.

좋은 웹페이지 즐겨찾기