궤도

[백엔드] 학생이 사진을 찍고 채점 결과를 받기까지 백엔드에서 일어나는 일 본문

💻 현생/📃 VIVA

[백엔드] 학생이 사진을 찍고 채점 결과를 받기까지 백엔드에서 일어나는 일

영이오 2021. 5. 18. 17:26

더보기를 누르면 접은 글이 나옵니다.

 

VIVA를 이용해서 학생이 채점을 하려고 한다고 해보자

사진을 찍고 스캔 완료를 누르면...약간의 시간이 흐른 뒤

 

채점표를 받는다.

 

이 과정동안 백엔드에선 어떤 일이 일어날까?

 

먼저 프론트에서 사진을 찍고 백엔드에 보내준다.

그러면 백엔드에서 S3 bucket에 사진을 업로드 하고, 그 url을 프론트에 다시 돌려준다.

 

routes/paper-upload.js

var models = require('../models');
const express = require('express');
const multer = require('multer');
const multerS3 = require('multer-s3');
const aws = require('aws-sdk');
aws.config.loadFromPath(__dirname + "/../config/awsconfig.json");
const s3 = new aws.S3();

const router = express.Router();
var Op = models.Sequelize.Op;
var fs = require('fs');

// multer-optional
var storage = multerS3({ //s3
    s3: s3,
    bucket: 'viva-s3-capstone',
    acl: 'public-read',
    key: function (req, file, cb) {
        cb(null, Math.floor(Math.random() * 1000).toString() + Date.now() + '.' + file.originalname.split('.').pop());
    }
});
var upload = multer({storage: storage});

// Router
router.post("/", upload.array('mark'), (req, res) => {
    try {
        res.send({ //파일 정보 넘김
            message: "upload success",
            status: 'success',
            data: {
                files: req.files
            }
        });
    } catch (err) { //무언가 문제가 생김
        res.send({
            message: "ERROR",
            status: 'fail'
        })
    }
});

module.exports = router;

여기엔 multer라는 미들웨어를 사용하고 있다.

 

npm i multer --save

이렇게 설치하면 되는거고

 

const multerS3 = require('multer-s3');
const aws = require('aws-sdk');
aws.config.loadFromPath(__dirname + "/../config/awsconfig.json");
const s3 = new aws.S3();

aws.config에서 우리의 S3 bucket Access key를 불러오는데

본인의 코드를 github 등에 올릴거라면 이 key를 절대로 올리지 말아야 한다.

 

만약 그대로 Access key를 github에 올리면 어떻게 될까?

로 시작하는 긴 메일

아마존에서 메일이 오고

 

깃가디언에서도 메일이 올 것이다.

 

그리고 git filter-branch 명령어로 이전 커밋 히스토리를 다 지우는 삽질을 해야한다.

 

https://myunji.tistory.com/386?category=1154148 

 

[AWS] Github에 AWS access key를 노출하면 일어나는 일 (따라하지 마세요)

따라할거라면 글을 끝까지 읽고 따라하세요 VIVA 프로젝트를 보면... 이렇게 사진을 업로드해야 하는 부분이 있다. 그래서 S3 bucket을 사용하고 있었는데 https://victorydntmd.tistory.com/70 [Node.js] AWS(1)..

myunji.tistory.com

자세한 이야기는 여기에 써두었다.

더보기

따라할거라면 글을 끝까지 읽고 따라하세요

 

VIVA 프로젝트를 보면...

이렇게 사진을 업로드해야 하는 부분이 있다.

그래서 S3 bucket을 사용하고 있었는데

 

https://victorydntmd.tistory.com/70

 

[Node.js] AWS(1) - S3 사용하기 ( aws-sdk, multer-s3 모듈 )

2019. 08. 11 수정 1. AWS S3( Simple Storage Service )란? S3는 AWS의 스토리지 서비스로 구글 드라이브, 네이버 클라우드와 비슷한 개념입니다. 그런데 S3는 HTTP 프로토콜로 파일 업로드 및 다운로드가 가능하

victorydntmd.tistory.com

이것과 비슷한 블로그를 봤을 것이다.

왜 이 블로그가 아닐 것이라면 여길 봤다면 난 Access key를 그렇게 노출하지 않았을테니까...

 

https://myunji.tistory.com/189?category=1154148 

 

[백엔드] React Native에서 Node.js로 이미지 보내기(feat. multer)

오늘은 프론트(react native)로부터 이미지를 받아와 저장할 것이다. 이렇게 찍히는 이미지들을 저장해야 하는 것인데...열심히 검색을 해보았다. krpeppermint100.medium.com/js-react%EC%97%90%EC%84%9C-expres..

myunji.tistory.com

이때 로컬로 이미지 업로드를 구현하긴 했었다.

 

저기 있는 코드에서 storage 부분만 바꿔서

const multer = require('multer');
const multerS3 = require('multer-s3');
const aws = require('aws-sdk');
aws.config.loadFromPath(__dirname + "/../config/awsconfig.json");
const s3 = new aws.S3();

var storage = multerS3({ 
  s3: s3,
  bucket: 'viva-s3-capstone',
  acl: 'public-read',
  key: function (req, file, cb) {
    cb(null, Math.floor(Math.random() * 1000).toString() + Date.now() + '.' + file.originalname.split('.').pop());
  }
});
var upload = multer({ storage: storage });

이렇게 하면 로컬폴더가 아니라 우리의 S3 bucket에 이미지가 올라간다.

여기까진 잘했다. 하지만 난...

 

aws.config.loadFromPath(__dirname + "/../config/awsconfig.json");
const s3 = new aws.S3();

지금은 이렇게 바꿔놓은 이 부분을

 

aws.config.loadFromPath({
  accessKeyId: "엑세스 키",
  secretAccessKey: "비밀 키",
  region: "ap-northeast-2"
});
const s3 = new aws.S3();

이런식으로 아주 당당하게 노출하고 있었다.

그리고 이걸 프론트엔드와 공유해야하니 깃허브에 올렸다. 당연히 우리 레포지토리는 퍼블릭이었다.

 

그리고...

로 시작하는 긴 메일

AWS에서 메일을 보냈다. 대충 너네 키 노출됐는데 이거 보안상에 큰 문제 생기니까 당장 해결하라는...

아주 당황스러웠다. 왜냐면 우린 변방에 아주 하찮은 학부생들이었고 이걸 이렇게 알아채고 메일을 보내줄거란 생각은 하지 못했다.

 

이런 상황이 처음인지라 우린 당황하면서 일단 레포지토리를 private으로 바꾸고, AWS 계정 정보도 수정하고 bucket도 새로 만들고 아무튼 그랬다.

 

그리고 그땐 한참 YOLO 모델을 위해 라벨링에 집중하던 시기라 잊고 있었는데...

나중에 프로젝트를 제출할 때 소스코드를 제출해야 한다는 것이 떠올랐다.

 

그래서 이런식으로 key를 따로 빼고 gitignore까지 잘해서 업로드했다.

키와 관련된 정보가 안올라간 것을 확인하고 다시 public으로 돌렸는데

 

도대체 왜 노출됐다는건지 이해를 할 수 없었다. 분명히 없는걸 확인했는데?

하지만 난 여전히 겁이 많으므로 다시 private으로 돌리고 원인을 찾으러 떠났다.

 

원인은 내 커밋 히스토리에 있었다.

히스토리에 수정기록이 전부 있으니 당연히 원래 있던 키가 지워지는 과정까지 전부 남아있었고

그 곳에 내 엑세스 키가 여전히 남아있었던 것이다.

 

커밋 히스토리 지우는 법을 찾으러 떠났다...

https://donologue.tistory.com/373

 

github 잘못 올라간 파일 히스토리까지 삭제하기

잘못해서 깃허브에 암호 파일이라던지, env 파일을 올려 난감한 상황에 처할 때가 있습니다. 저도 최근 .pem 파일을 push 해서 난처한 상황에 처하고 말았는데요. 이렇게 푸시까지 이루어진 상황에

donologue.tistory.com

답은 git filter-branch 명령어였다.

 

여기에 원하는 파일의 경로와 이름을 쓰면 깃 히스토리에서 그 파일이 들어간 히스토리만 지워준다고 한다.

내가 지워야하는 히스토리는 routes에 있는 파일 2개와 config에 있는 파일 1개였다.

 

저 블로그에 적혀있는대로

git filter-branch -f --index-filter 'git rm --cached --ignore-unmatch VIVA/routes/파일이름' --prune-empty -- --all

이걸 해줬고

 

git push origin main --force

이걸 했다.

 

지금보니 rm에 force에 무시무시한 명령어가 가득했는데 당황한 새벽의 나는 미처 알아채지 못했다.

아무튼 원래 52 commits 이었는데 이렇게 줄어든 것을 보고 히스토리 삭제도 확인하고 해결했다 싶었다.

 

근데 인텔리제이를 켜보니 세상에

파일이 로컬에서도 사라진 것이다.

 

난 rm이 히스토리만 지워주는 그런 것인줄 알았는데 로컬에서도 파일을 지워버렸다!

나에겐 백업본이 없었지만 다행히 이 레포지토리의 또다른 컨트리뷰터인 프론트 팀원이 파일을 갖고 있어 복구했다.

 

그리고 남은 두 파일에 대해선 미리 백업을 해두고 명령어를 입력했다.

만약 이 레포지토리의 컨트리뷰터가 나 하나였다면..? 그런 끔찍한 일은 상상하고 싶지 않다.

 

아무튼 git filter-branch 명령어를 사용하기전 filter할 파일을 반드시 백업해야한다.

 

난 앞으로 뭐든 삭제할 일이 있으면 무조건 백업할 것이다.

그리고 뭐든 key 어쩌구가 들어가는건 절대...올리지 않을 것이다.


아무튼 보안을 신경썼으리라 믿고 계속 살펴보면

// multer-optional
var storage = multerS3({ //s3
    s3: s3,
    bucket: 'viva-s3-capstone',
    acl: 'public-read',
    key: function (req, file, cb) {
        cb(null, Math.floor(Math.random() * 1000).toString() + Date.now() + '.' + file.originalname.split('.').pop());
    }
});
var upload = multer({storage: storage});

저장소를 S3 bucket으로 설정하는 부분이다. 파일명이 될 key는 그냥 대충 정했다. 정해진 형식이 필요한 건 아니라...

 

// Router
router.post("/", upload.array('mark'), (req, res) => {
    try {
        res.send({ //파일 정보 넘김
            message: "upload success",
            status: 'success',
            data: {
                files: req.files
            }
        });
    } catch (err) { //무언가 문제가 생김
        res.send({
            message: "ERROR",
            status: 'fail'
        })
    }
});

프론트로부터 넘어오는 사진은 여러 장일 것이다. 그러니 upload.array로 처리하자.

만약 1장이라면 upload.single로 처리하면 된다.

 

업로드에 성공한 파일의 정보를 req.files로 다시 프론트에 전달하면 된다.

 

여기까지가 사진 업로드였고...

이 사진을 채점해야 한다.

 

프론트에서 사진 url을 전달해주면 백엔드는 그걸 YOLO 모델이 있는 플라스크 서버에 보내서 객체를 detect한다. 그리고 그 결과를 플라스크 서버에서 넘겨주면 json 데이터를 기반으로 데이터베이스의 답안과 비교해서 채점하면 된다.

 

데이터베이스의 problem 테이블에는 workbook_sn이라는 attribute가 있고, 이건 문제집의 고유 코드이다.

 

그리고 문제별 답이 있는 solution 테이블과 problem 테이블은 pb_sn으로 연결되어 있다.

 

프론트에서 "workbook_sn,url1,url2,url3" 이런식으로 body를 넘겨주기로 했다. 이것만 알아두고 코드를 보자

 

routes/scoring.js

var models = require('../models');
const express = require('express');
var request = require('request');
const router = express.Router();
var Op = models.Sequelize.Op;

router.post('/', function (req, res, next) {
    let body = req.body;
    const file_name = body.file_name;
    const split_file_name = file_name.split(',');
    const w_sn = split_file_name[0]; //workbook_sn

    const YoloResult = (callback)=>{
        const options = {
            method: 'POST',
            uri: "플라스크 api 주소",
            qs: {
                file_name: file_name
            }
        }

        request(options, function (err, res, body) {
            callback(undefined, {
                result:body
            });
        });
    }

    YoloResult((err, {result}={})=>{
        if(err){
            console.log("error!!!!");
            res.send({
                message: "fail",
                status: "fail"
            });
        }
        let json = JSON.parse(result);
        res.send({
            message: "from flask",
            status: "success",
            data:{
                json,
                w_sn
            }
        });
    })

});

module.exports = router;

아직 채점 알고리즘을 만들지 않아서 그냥 YOLO detect 결과만 리턴하는 상태이다.

일단 각 데이터(?)마다 쉼표가 붙어서 오니, 쉼표를 기준으로 문자열을 잘라주고

가장 첫번째 데이터일 workbook_sn을 따로 저장해 둔다.

 

이제 플라스크의 api를 호출해야하는데, request 모듈을 사용한다.

    const YoloResult = (callback)=>{
        const options = {
            method: 'POST',
            uri: "플라스크 api 주소",
            qs: {
                file_name: file_name
            }
        }

        request(options, function (err, res, body) {
            callback(undefined, {
                result:body
            });
        });
    }

post 형식으로 인자를 넘겨줄 것인데, 그냥 리액트가 넘겨준 body를 그대로 다시 넘기는 것이다.

qs: { file_name: file_name }을 보면 알 수 있을 것이다. 이게 넘겨줄 인자다.

 

그리고 request를 호출해서 그 결과를 result에 담았다.

 

    YoloResult((err, {result}={})=>{
        if(err){
            console.log("error!!!!");
            res.send({
                message: "fail",
                status: "fail"
            });
        }
        let json = JSON.parse(result);
        res.send({
            message: "from flask",
            status: "success",
            data:{
                json,
                w_sn
            }
        });
    })

result를 json 형태로 parse하고 workbook_sn과 함께 출력해봤다.

 

이 플라스크 api를 보여주곤 싶지만...우리 YOLO 모델도 있고, 난 YOLO 파트 팀원이 작성한 코드를 이식한 것일 뿐이라...

모델 완성전 시험삼아 request 요청을 해본 게시글이 있다.

 

https://myunji.tistory.com/292?category=1154148 

 

[백엔드] Node.js 서버에서 flask api 호출하기(request 모듈)

우리 프로젝트에서 기술적으로 가장 중요한 부분은 채점 부분이다. 사용자가 사진을 찍어서 업로드한 뒤 채점 결과를 받기까지의 과정을 그림으로 나타내면 일단 이렇게 사진을 업로드 한 뒤

myunji.tistory.com

바로 여기다

 

더보기

오늘은 그냥 리액트-노드-플라스크 사이에서 데이터가 잘 오가는지만 확인하려고 한다.

 

node.js 서버에서 다른 서버의 api를 호출하려면 request 모듈을 사용해야 한다고 한다.

 

npm install request --save

 

app.py

import numpy as np
import json
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/test', methods=['POST'])
def test():
    lists = request.args['file_name']
    lists = lists.split(',')
    data = []
    for list in lists:
        data.append(list)

    return jsonify({
        'result': data
    })

if __name__ == '__main__':
    app.run()

먼저 flask api다.

원래는 욜로 관련 코드도 있었는데 괜히 여기에 쓰면 헷갈리기만 할 것 같아서 지웠다.

 

프론트에서는 파일 이름 사이사이에 쉼표를 넣어서 한 줄로 보내준다고 했다.

예를 들어 http://blahblah/test 파일과 http://blahblah/test1 파일이 있다면

 

http://blahblah/test,http://blahblah/test1

 

이런식으로 오는 것이다. 이러면 길이가 꽤 길어질테니 get말고 post로 처리하기로 했다.

오늘은 테스트용이므로 이렇게 한 줄로 온 데이터를 쉼표기준으로 잘라서 다시 노드에 보내는 것만 하도록 하겠다.

 

@app.route('/test', methods=['POST'])
def test():
    lists = request.args['file_name']
    lists = lists.split(',')
    data = []
    for list in lists:
        data.append(list)

    return jsonify({
        'result': data
    })

flask는 request.args로 인자를 넘긴다고 한다.

넘어온 인자를 lists에 저장한 뒤, 쉼표 기준으로 split한다.

그리고 그 결과를 data 배열에 담아서 node.js로 return 해주는 것이다.

제이슨 형식으로 잘 넘겨주기 위해 return jsonify라고 명시해주었다.

 

이 주소에서 돌아간다고 했으니 http://127.0.0.1:5000/test로 요청을 보내면 될 것이다.

 

routes/scoring.js

var models = require('../models');
const express = require('express');
var request = require('request');
const router = express.Router();
var Op = models.Sequelize.Op;

router.post('/', function (req, res, next) {
    let body = req.body;
    const file_name = body.file_name

    const YoloResult = (callback)=>{
        const options = {
            method: 'POST',
            uri: "http://127.0.0.1:5000/test",
            qs: {
                file_name: file_name
            }
        }

        request(options, function (err, res, body) {
            callback(undefined, {
                result:body
            });
        });
    }

    YoloResult((err, {result}={})=>{
        if(err){
            console.log("error!!!!");
            res.send({
                message: "fail",
                status: "fail"
            });
        }
        let json = JSON.parse(result);
        res.send({
            message: "from flask",
            status: "success",
            data:{
                json
            }
        });
    })

});

module.exports = router;

프론트로부터 post 요청이 들어오면 플라스크 서버의 api를 호출하여 그 결과를 받아 다시 프론트에 넘겨주는 코드이다.

 

    let body = req.body;
    const file_name = body.file_name

    const YoloResult = (callback)=>{
        const options = {
            method: 'POST',
            uri: "http://127.0.0.1:5000/test",
            qs: {
                file_name: file_name
            }
        }

        request(options, function (err, res, body) {
            callback(undefined, {
                result:body
            });
        });
    }

프론트가 한줄로 넘겨준 file_name을 그대로 저장하여 request 요청을 할 때 그대로 보내준다.

 

    const YoloResult = (callback)=>{
        const options = {
            method: 'POST',
            uri: "http://127.0.0.1:5000/test",
            qs: {
                file_name: file_name
            }
        }

이렇게 options에 요청의 종류(method)와 호출할 api의 url(uri), 그리고 넘겨줄 인자(qs)를 명시해준다.

 

        request(options, function (err, res, body) {
            callback(undefined, {
                result:body
            });
        });

아까 본 options을 담아 request 요청을 보낸 뒤 그 결과를 result에 담는다.

이 모든 과정을 YoloResult라는 상수에 담았고

 

    YoloResult((err, {result}={})=>{
        if(err){
            console.log("error!!!!");
            res.send({
                message: "fail",
                status: "fail"
            });
        }
        let json = JSON.parse(result);
        res.send({
            message: "from flask",
            status: "success",
            data:{
                json
            }
        });
    })

이렇게 쓰면 된다.

문제가 없다면 플라스크로부터 넘어온 정보가 result에 담겼을 것이고, 이걸 JSON.parse 하여 프론트로 넘겨준다.

 

한줄로 들어온 test,test1을 test와 test1으로 잘 분리한 것을 확인할 수 있다.


아무튼 result를 JSON 형식으로 parse한 json 변수를 가지고 채점까지 해서 프론트에 넘기면 되겠다.

Comments