미니 플젝 웹 개발일지23.03.02-23.03.06

최창수·2023년 3월 6일
0

0. 프로젝트 목표

개요

3월 10일 자정까지 실제 작동하는 웹 페이지 만들고 배포하는 것이 목표이다. 선택한 주제는 '각 팀(게시판)별로 모아 볼 수 있는 버킷리스트'이다.
역할 분담은 프론트엔드 3인, 백엔드 2인으로 나누어 진행하였으며 이중 나는 백엔드를 담당하였다.

프론트 엔드 구현방식

아래 이미지들은 완성되지 않은 중간결과물이다.
메인페이지에서는 각 팀의 목록을 보여준다. '조 등록하기'버튼을 누르면 새로운 팀을 등록할 수 있는 페이지로 이동한다. '버킷리스트가기'를 누르면 각 조의 버킷리스트를 보여주는 페이지로 이동한다.
버킷리스트 페이지에서는 각 조별 버킷리스트를 표시하고, 버킷리스트 등록 및 삭제, 팀 전체 삭제가 가능하다.
조 등록하기 페이지에서는 새로운 조를 등록할 수 있다. 조를 등록하기 위해서는 조 이름, 조장 이름, 조를 관리하기위해 사용하는 비밀번호를 입력해야한다.

API

우리 프로젝트에서 필요한 API들은 다음과 같다:

  1. 조를 만드는 API(/teams, POST)
  2. 조 목록을 불러오는 API(/teams, GET)
  3. 조를 삭제하는 API(/teams/<team_name_id>, DELETE)
  4. 버킷리스트를 등록하는 API(/list/<team_name_id>, POST)
  5. 버킷리스트를 보여주는 API(/list/<team_name_id>, GET)
  6. 버킷리스트를 삭제하는 API(/list/<team_name_id>, DELETE)
  7. 버킷리스트를 수정하는 API(/list/<team_name_id>, PUT)

이중 내가 맡은 API는 1, 2, 3, 5번 API이다.

DB

필요한 DB의 구성은 다음과 같다.
A. teamDB: 각 조들의 정보를 저장하는 DB

_idteam_nameleader_namepw
-조 이름조장 이름(닉네임)암호화된 비밀번호
1fa031핑핑이집사단스폰지밥asdasdf2%&%112d

B. contentDB: 각 버킷리스트들의 정보를 저장하는 DB

_idteam_namecontentpwdone
-조 이름내용암호화된 비밀번호완료 여부
1fa031핑핑이집사단밥먹기asdasdf2%&%112d0

의존성

다음과 같은 패키지들이 사용되었다.

import datetime # 작성 시간 기록을 위한 패키지
from pymongo import MongoClient #DB
#웹 서버 구축
from flask import Flask, render_template, request, jsonify, app
import bcrypt as bc # 암호화 패키지

1. 구현된 기능들

ⅰ- 조를 만드는 API

front-end로부터 팀 이름, 팀장 이름(닉네임), 비밀번호를 받아온다. 이후 적절한 입력인지 체크한 뒤 DB에 입력한 내용을 저장하는 기능이다. 조별로 나누는 이유는 주제별, 팀 별로 버킷리스트를 모아서 보면 편리할것이라고 생각하였기 때문이다.
전체 코드:

@app.route("/teams", methods=["POST"])#조 생성 API
def post_team():
    team_name_receive = request.form['team_name_give']
    leader_name_receive = request.form['leader_name_give']
    pw_receive=request.form['pw_give']
    hashed_pw=bc.hashpw(pw_receive.encode("utf-8"), bc.gensalt()).decode("utf-8")
    #각 입력값이 비어있을 경우 등록 실패
    if team_name_receive =="" :
        return jsonify({ 
        'result': 'fail_00', 
        'msg': '조 이름을 입력하세요.'
        })
    if leader_name_receive =="" :
        return jsonify({ 
        'result': 'fail_01', 
        'msg': '팀장 닉네임을 입력하세요.'
        })
    if pw_receive =="" :
        return jsonify({ 
        'result': 'fail_02', 
        'msg': '비밀번호를 입력하세요.'
        })
    if db.teamDB.find_one({
    'team_name':team_name_receive
    }) != None:
        return jsonify({ 
        'result': 'fail_03', 
        'msg': '이미 존재하는 조이름입니다.'
        })
    if db.teamDB.find_one({
    'leader_name':leader_name_receive
    }) != None:
        return jsonify({ 
        'result': 'fail_04', 
        'msg': '이미 존재하는 닉네임입니다.'
        })
    
    #db저장
    doc = {
        'team_name' : team_name_receive,
        'leader_name' : leader_name_receive,
        'pw' : hashed_pw
    }
    db.teamDB.insert_one(doc)
    return jsonify({ 
    'result': 'success', 
    'msg': '조가 생성되었습니다.'
    })

부분기능 1: 비밀번호 암호화

hashed_pw=bc.hashpw(pw_receive.encode("utf-8"), bc.gensalt()).decode("utf-8")

DB에 비밀번호가 그대로 저장될 경우 취약할 수 있다. 따라서 비밀번호를 암호화하여 DB에 저장하는 기능이 필요하다. 이를 위해 bcrypt패키지의 hashpw()함수와 gensalt()를 이용하였다. 이를 이용하면 입력된 password에 salt라고 불리는 새로운 문자열을 이어붙인 뒤 이를 암호화하여 저장할 수 있다. 암호화된 비밀번호를 사용자가 추후 입력한 비밀번호와 일치하는지 확인하기 위해서는 checkpw()함수를 이용하여야 한다.
이때 bcrypt의 함수들을 이용하기 위해서는 인수로 사용된 문자열들을 utf-8로 인코딩하여야한다. 그렇지 않을 경우 오류가 발생한다.

부분기능 2: 유효한 입력인지 확인

    #각 입력값이 비어있을 경우 등록 실패
    if team_name_receive =="" :
        return jsonify({ 
        'result': 'fail_00', 
        'msg': '조 이름을 입력하세요.'
        })
    if leader_name_receive =="" :
        return jsonify({ 
        'result': 'fail_01', 
        'msg': '팀장 닉네임을 입력하세요.'
        })
    if pw_receive =="" :
        return jsonify({ 
        'result': 'fail_02', 
        'msg': '비밀번호를 입력하세요.'
        })

만약 아무 내용도 없는 문자열을 사용하는 것을 허용할 경우 의미없는 데이터들이 쌓일 수도 있고, 일부 데이터나 주소에 접근하기 어려운 경우가 생길 수 있다. 따라서 이를 방지하기 위해 아무것도 입력하지 않는 것을 막는 기능을 추가한다. if문을 이용해 각 입력이 ""(빈 문자열)과 같은지 확인하고, 같을 시 제대로 된 입력을 하라는 메세지를 출력하도록 한뒤 함수를 강제로 종료시킨다.

부분기능 3: 중복된 값인지 확인

    if db.teamDB.find_one({
    'team_name':team_name_receive
    }) != None:
        return jsonify({ 
        'result': 'fail_03', 
        'msg': '이미 존재하는 조이름입니다.'
        })
    if db.teamDB.find_one({
    'leader_name':leader_name_receive
    }) != None:
        return jsonify({ 
        'result': 'fail_04', 
        'msg': '이미 존재하는 닉네임입니다.'
        })

서로 다른 조나 조장의 이름이 일치할 경우, 사용자가 혼란을 일으킬 수 있고, 각 조를 조의 이름(team_name)을 통해 구별하는 현 프로젝트의 한계상 오류가 발생할 수 있다. 따라서 이를 방지하기 위한 기능을 넣었다.
먼저 DB를 조회하여 입력된 조 이름과 같은 조이름을 가진 record가 있는지 파악한다. 없다면, 다시 DB를 조회하여 입력된 조장 이름과 같은 조장 이름을 가진 record가 있는지 파악한다.

부분기능 4: DB에 저장

    #db저장
    doc = {
        'team_name' : team_name_receive,
        'leader_name' : leader_name_receive,
        'pw' : hashed_pw
    }
    db.teamDB.insert_one(doc)
    return jsonify({ 
    'result': 'success', 
    'msg': '조가 생성되었습니다.'
    })

모든 조건을 만족하여 통과하였으면 DB에 입력된 내용을 저장하도록한다. doc는 DB에서 하나의 레코드를 이루게된다. 레코드 안에는 조이름, 조장이름, 암호화된 비밀번호가 저장된다.
DB에 반영된 뒤 front-end에는 성공했음을 알리고 적절한 메시지를 출력하도록 한다.

ⅱ- 조 목록을 불러오는 API

front-end에서 전체 조 목록을 표시하고 사용자가 해당 조 중에서 하나를 선택해 해당 조의 페이지로 이동하고 거기서 버킷리스트를 작성할 수 있어야한다. 따라서 메인페이지에서 GET요청을 보내면 존재하는 모든 조들의 이름을 리스트형태로 제공해야한다.

전체코드:

@app.route("/teams", methods=["GET"]) #조 표시 API
def get_team():
    #DB로 부터 비밀번호를 제외한 데이터 불러오기
    team_data = list(db.teamDB.find({},{'_id':False, 'pw':False,}))

    return jsonify({'result':team_data})

/teams API로 요청이 오면 teamDB로 부터 모든 record들을 가져온다. 이때 암호화된 비밀번호는 불필요하며 보안상 위협이 될 수 있으므로 가져오지 않는다.
가져온 데이터를 json화 하여 front-end에 넘긴다.

ⅲ- 조를 삭제하는 API

조장 혹은 조장이 조를 생성할때 지정한 비밀번호를 아는 사용자는 해당 조가 해체되는 등 더이상 해당 페이지를 이용하지 않을 때 조를 삭제할 수 있어야 하고, 또한 한번데 같은 조에서 적은 버킷리스트를 삭제하는 기능이 있으면 편리하기 때문이 해당 기능을 만들었다.

전체코드:

@app.route("/teams/<string:team_name_id>",methods=["DELETE"])# 조 데이터 삭제 API
def delete_team(team_name_id):
    #프론트로부터 받을 데이터들
    input_leader_name_receive = request.form['input_leader_name_give']
    input_pw_receive=request.form['input_pw_give']
    #존재하지 않는 ID(닉네임), ID불일치 확인
    if not list(db.teamDB.find({'leader_name':input_leader_name_receive},{})):
        return jsonify({ 'result': 'fail', 'msg': '옳지않은 ID 혹은 PW입니다.'})
    team = db.teamDB.find_one({'team_name':team_name_id, 'leader_name':input_leader_name_receive})
    if team == None:
        return jsonify({ 'result': 'fail', 'msg': '옳지않은 ID 혹은 PW입니다.'})
    #데이터 가져오고, 비밀번호 비교
    hashed_pw=team['pw']
    if not bc.checkpw(input_pw_receive.encode("utf-8"),hashed_pw.encode("utf-8")):
        return jsonify({ 'result': 'fail', 'msg': '옳지않은 ID 혹은 PW입니다.'})
    #데이터 삭제
    db.teamDB.delete_one({'team_name':team['team_name'], 'leader_name':team['leader_name']})
    db.contentDB.delete_many({'team_name':team['team_name']})
    return  jsonify({ 'result': 'success', 'msg': '조의 모든 데이터가 삭제되었습니다.'})

부분기능 1: Path Variable

@app.route("/teams/<string:team_name_id>",methods=["DELETE"])
def delete_team(team_name_id):

front-end가 /teams/에 DELETE요청을 보낼 경우 url뒤에 붙은 team_name_id에는 조의 이름이 온다. 이것을 기준으로 삼아 DB에서 삭제할 record들을 찾는다.

부분기능 2: 유효하지 않은 입력 차단

if not list(db.teamDB.find({'leader_name':input_leader_name_receive},{})):
        return jsonify({ 'result': 'fail', 'msg': '옳지않은 ID 혹은 PW입니다.'})
    team = db.teamDB.find_one({'team_name':team_name_id, 'leader_name':input_leader_name_receive})
    if team == None:
        return jsonify({ 'result': 'fail', 'msg': '옳지않은 ID 혹은 PW입니다.'})

유효하지 않은 입력으로 인해 입력된 조 이름 혹은 조장 이름이 실제로 존재하지 않는다면, 삭제를 진행하지 않고 올바르지 않은 입력이라고 알려주어 제대로 삭제된 경우와 차이를 두어야한다.
먼저 teamDB를 조회해 입력된 조장이름이 실존하는 조장이름인지 확인한다.
그리고 다시 teamDB를 조회해 입력된 조장이름과 path variable로 주어진 조 이름을 동시에 만족하는 record(실제로 삭제해야하는 대상)이 존재하는지 확인한다.
둘을 분리한 이유는 현재로서는 조장이름이 중복되지 않게 하였지만 추후 한 조장이 여러 조를 운영할 수 있게 만들 수도 있기 때문이다.

부분기능 3: 비밀번호 확인

    if not bc.checkpw(input_pw_receive.encode("utf-8"),hashed_pw.encode("utf-8")):
        return jsonify({ 'result': 'fail', 'msg': '옳지않은 ID 혹은 PW입니다.'})

비밀번호가 일치하지 않는다면 삭제가 진행되서는 안된다. 비밀번호는 teamDB에 암호화되어 저장되어있으므로 bcryptcheckpw()함수를 이용해 비교한다. hashpw()와 마찬가지로 utf-8로 인코딩하여 비교한다. 일치할 경우 True, 불일치할 경우False를 반환한다.

부분기능 4: 데이터 삭제

 db.teamDB.delete_one({'team_name':team['team_name'], 'leader_name':team['leader_name']})
    db.contentDB.delete_many({'team_name':team['team_name']})
    return  jsonify({ 'result': 'success', 'msg': '조의 모든 데이터가 삭제되었습니다.'})

모든 조건문을 통과하면 데이터가 삭제된다. 먼저 teamDB에서 team_name과 leader_name이 입력된 값과 일치하는 record하나를 삭제한다.
이후 contentDB에서 team_name이 입력된 값과 일치하는 모든 record들을 삭제한다.

profile
Hallow Word!

0개의 댓글