궤도

[백엔드] Node.js + Sequelize + MySQL 시험지를 만들어보자 본문

💻 현생/📃 VIVA

[백엔드] Node.js + Sequelize + MySQL 시험지를 만들어보자

영이오 2021. 2. 10. 18:08

오늘은 사용자 오답노트의 문제를 기반으로 사용자 맞춤 문제를 제공하는 시험지를 만들어 볼 것이다.

 

Q. 맞춤 문제면 딥러닝인가요?

A. 아니요

 

사실 데베에 문제별로 해당 문제의 유형을 저장해놨다. 그니까 사용자가 오답노트에 넣어놓은 문제들의 유형을 뽑아내어 해당 유형의 문제들을 랜덤으로 가져오면 되는 것이다.

 

난이도 선택은 기획 때는 있었으나...지금은 없다. 메인 기능이 아니니까 빠져도 괜찮다...

 

그럼 생성이니까 post만 있으면 될까? 아니다.

미니모의고사 생성시 사용자는 시험지 생성에 참조할 오답노트를 선택할 수 있다. 그러니 get으로 사용자가 선택할 수 있는 오답노트를 알려줘야 한다.


선택 가능한 오답노트 보여주기

 

단순히 프론트에서 넘겨주는 stu_id로 사용자 찾아내서 그 사용자가 갖고 있는 오답노트를 전부 가져오면 되는 것이라고 생각할 수 있다. 하지만 그렇지 않다.

 

오답노트의 문제 유형을 분석해서 맞춤 문제를 뽑아내는 것인데, 오답노트에 문제가 하나도 없다면? 문제를 뽑아낼 수 없다. 그렇기 때문에 사용자가 갖고 있는 오답노트 중에서 문제가 1개라도 있는 오답노트만 보여줘야 한다.

 

 

routes/test.form.js(노트 가져오기)

//오답노트 가져오기
//localhost:3001/api/test/form?stu_id=samdol
router.get('/', async function (req, res, next) {
    let result = await models.student.findOne({
        where: {
            stu_id: req.query.stu_id
        }
    });
    const user = result.dataValues.stu_sn;

    //사용자가 보유한 오답노트
    let notes = await models.incor_note.findAll({
        where: {
            stu_sn: user
        }
    });

    //오답노트가 없음
    if (notes.length == 0) {
        res.send({
            message: 'No incor-note',
            status: 'null'
        });
        return;
    }

    //오답노트 중에서 문제가 들어있는 노트만 보여주기
    let note_list = new Array();
    for (var i in notes) {
        let pbCount = await models.incor_problem.count({
            where: {
                note_sn: notes[i].dataValues.note_sn,
                stu_sn: user
            }
        });
        if (pbCount != 0) {
            note_list.push(notes[i]);
        }
    }

    try {
        res.send({ //노트 정보 넘김
            message: "available notes",
            status: 'success',
            data: {
                note_list
            }
        });
    } catch (err) { //무언가 문제가 생김
        res.send({
            message: "ERROR",
            status: 'fail'
        })
    }
});

별 내용은 없지만...

 

    let result = await models.student.findOne({
        where: {
            stu_id: req.query.stu_id
        }
    });
    const user = result.dataValues.stu_sn;

stu_id를 pk인 stu_sn으로 바꿔주고...

이정도면 차라리 함수화를 할까 싶다. 그럼 굳이 설명할 필요도 없을텐데

 

    //사용자가 보유한 오답노트
    let notes = await models.incor_note.findAll({
        where: {
            stu_sn: user
        }
    });

    //오답노트가 없음
    if (notes.length == 0) {
        res.send({
            message: 'No incor-note',
            status: 'null'
        });
        return;
    }

사용자가 갖고 있는 오답노트를 전부 가져오자. 아직 프론트에서 어떻게 처리할 지는 잘 모르겠지만 오답노트가 없는 경우가 혹시 필요할 수도 있으니 그 경우도 고려한다.

 

    //오답노트 중에서 문제가 들어있는 노트만 보여주기
    let note_list = new Array();
    for (var i in notes) {
        let pbCount = await models.incor_problem.count({
            where: {
                note_sn: notes[i].dataValues.note_sn,
                stu_sn: user
            }
        });
        if (pbCount != 0) {
            note_list.push(notes[i]);
        }
    }

각 오답노트들에 대해 문제 수를 세어보고 0이 아닌 경우에만 note_list에 넣어준다.

 

    try {
        res.send({ //노트 정보 넘김
            message: "available notes",
            status: 'success',
            data: {
                note_list
            }
        });
    } catch (err) { //무언가 문제가 생김
        res.send({
            message: "ERROR",
            status: 'fail'
        })
    }

데이터로 note_list를 넘겨주자

 

미니모의고사를 만들 수 있는 오답노트 목록


미니모의고사 생성

 

미니모의고사에는 총 10문제가 들어갈 것이다. 유형 분류를 대충 17개로 했고, 미니모의고사용 문제들이 720개 있으니 어떤 유형에도 10문제는 넘게 있겠지 뭐...

 

미니모의고사 관련 테이블들이다.

stu_sn은 req.query로 넘어오는 stu_id에서 뽑아낼 수 있고, req.body로는 test_title과 note_sn(참고할 오답노트)가 넘어올 것이다. 그럼 해야할 일을 순서대로 써보겠다.

 

1. stu_sn, test_title로 test테이블에 데이터 넣기

2. note_sn에 해당하는 오답노트 가서 문제 추출하기

3. 추출한 문제들 유형만 뽑아내기(중복 제거)

4. 미니모의고사 문제 후보들 중 뽑아낸 유형과 일치하는 문제들 뽑기

5. 저 중에 10개만 랜덤으로 뽑아서 test_pb_map 테이블에 넣기

 

이 테이블들에 대한 정보도 알아야 아래 나올 코드를 잘 이해할 수 있을 것이다.

 

routes/test.form.js(미니모의고사 생성)

//모의고사 생성하기
//localhost:3001/api/test/form?stu_id=samdol
router.post('/', async function (req, res, next) {
    let body = req.body;

    let result = await models.student.findOne({
        where: {
            stu_id: req.query.stu_id
        }
    });
    const user = result.dataValues.stu_sn;

    //모의고사 생성
    let sn; //생성된 모의고사의 sn
    models.test.create({
        stu_sn: user,
        test_title: body.test_title
    })
        .then(result => {
            sn = result.dataValues.test_sn;
        })
        .catch(err => {
            console.log(err);
        });


    //incor_problem 테이블에서 생성할 때 사용한 오답노트 안에 있는 문제 찾아오기
    let pb_list = new Array();
    let pbs = await models.incor_problem.findAll({
        attributes: ['pb_sn'],
        where: {
            note_sn: body.note_sn,
            stu_sn: user
        }
    });
    for (var i in pbs) {
        pb_list.push(pbs[i].dataValues.pb_sn);
    }

    //문제들 유형 찾아오기
    let d_type_list = new Array();
    let types = await models.problem.findAll({
        attributes: ['pbtype_sn'],
        where: {
            pb_sn: {
                [Op.in]: pb_list
            },
            pbtype_sn: { //문제 유형 null인 것들 제외
                [Op.ne]: null
            }
        }
    });
    for (var i in types) {
        d_type_list.push(types[i].dataValues.pbtype_sn);
    }

    //중복 제거
    let set = new Set(d_type_list);
    let type_list = [...set];

    //init-models.js에 있는데 왜...
    models.problem.belongsTo(models.workbook, { foreignKey: "workbook_sn" });
    models.workbook.hasMany(models.problem, { foreignKey: "workbook_sn" });

    //오답노트 문제들과 유형이 동일하고 교육청 모의고사인 모든 문제들
    let pb_array = new Array();
    let pb_candidate = await models.problem.findAll({
        attributes: ['pb_sn'],
        include: [
            {
                model: models.workbook,
                where: {
                    workbook_publisher: 'Gyoyuk'
                }
            }
        ],
        where: {
            pbtype_sn: {
                [Op.in]: type_list
            }
        }
    });
    for (var i in pb_candidate) {
        pb_array.push(pb_candidate[i].dataValues.pb_sn);
    }

    //10개 뽑기
    let test_pb = new Array();
    for (var i = 0; i < 10; i++) {
        var temp = pb_array.splice(Math.floor(Math.random() * pb_array.length), 1)[0];
        test_pb.push(temp);
    }

    for (var i = 0; i < 10; i++) {
        models.test_pb_map.create({
            test_sn: sn,
            pb_sn: test_pb[i]
        })
            .catch(err => {
                console.log(err);
            });
    }

    try {
        res.send({ //시험지 정보 넘김
            message: "test created",
            status: 'success',
            data: {
                test_title: body.test_title,
                student: user,
                problems: test_pb
            }
        });
    } catch (err) { //무언가 문제가 생김
        res.send({
            message: "ERROR",
            status: 'fail'
        })
    }
});

하나하나 살펴보자

 

    let result = await models.student.findOne({
        where: {
            stu_id: req.query.stu_id
        }
    });
    const user = result.dataValues.stu_sn;

항상 하는 일이고

 

    //모의고사 생성
    let sn; //생성된 모의고사의 sn
    models.test.create({
        stu_sn: user,
        test_title: body.test_title
    })
        .then(result => {
            sn = result.dataValues.test_sn;
        })
        .catch(err => {
            console.log(err);
        });

먼저 test 테이블에 값을 채운다. 이때 들어간 데이터의 test_sn은 test_pb_map 테이블을 채울 때 필요하기 때문에 sn 변수에 따로 담아둔다.

 

    //incor_problem 테이블에서 생성할 때 사용한 오답노트 안에 있는 문제 찾아오기
    let pb_list = new Array();
    let pbs = await models.incor_problem.findAll({
        attributes: ['pb_sn'],
        where: {
            note_sn: body.note_sn,
            stu_sn: user
        }
    });
    for (var i in pbs) {
        pb_list.push(pbs[i].dataValues.pb_sn);
    }

req.body로 note_sn을 받아왔으니 해당하는 오답노트의 문제의 pb_sn을 전부 찾아 pb_list 배열에 저장한다.

 

    //문제들 유형 찾아오기
    let d_type_list = new Array();
    let types = await models.problem.findAll({
        attributes: ['pbtype_sn'],
        where: {
            pb_sn: {
                [Op.in]: pb_list
            },
            pbtype_sn: { //문제 유형 null인 것들 제외
                [Op.ne]: null
            }
        }
    });
    for (var i in types) {
        d_type_list.push(types[i].dataValues.pbtype_sn);
    }

problem 테이블에 가서 각각의 pb_sn의 pbtype_sn을 찾아온다. 문제 유형을 받아온다는 말이다. 다만 일부 문제는 유형이 null인 경우도 있어서 그런 경우는 제외한다. 문제 유형들은 d_type_list 배열에 저장하는데 이 배열은 중복된 값이 있을 수 있으므로

 

    //중복 제거
    let set = new Set(d_type_list);
    let type_list = [...set];

중복 제거를 한 뒤, type_list에 저장한다.

 

이제 내가 몇시간을 삽질하게 한 join부분이 나오는데...

myunji.tistory.com/161

 

[Sequelize] sequelize-auto 도대체 왜 join(include)이 되지 않는가에 대한 고찰

사건은 이렇게 발생했다. let pb_candidate = await models.problem.findAll({ attributes: ['pb_sn'], include: [ { model: models.workbook, where: { workbook_publisher: 'Gyoyuk' } } ], where: { pbtype_sn:..

myunji.tistory.com

대충 여기서 보도록 하고

 

우리 앱에선 교육청 모의고사의 문제들을 미니 모의고사 생성에 사용할 것이다. 그러므로 publisher가 Gyoyuk인 workbook에서 유형에 맞는 문제들을 뽑아내야 하니, workbook 테이블과 problem 테이블을 join 해야 한다.

    //init-models.js에 있는데 왜...
    models.problem.belongsTo(models.workbook, { foreignKey: "workbook_sn" });
    models.workbook.hasMany(models.problem, { foreignKey: "workbook_sn" });

    //오답노트 문제들과 유형이 동일하고 교육청 모의고사인 모든 문제들
    let pb_array = new Array();
    let pb_candidate = await models.problem.findAll({
        attributes: ['pb_sn'],
        include: [
            {
                model: models.workbook,
                where: {
                    workbook_publisher: 'Gyoyuk'
                }
            }
        ],
        where: {
            pbtype_sn: {
                [Op.in]: type_list
            }
        }
    });
    for (var i in pb_candidate) {
        pb_array.push(pb_candidate[i].dataValues.pb_sn);
    }

sequelize에서 join operation을 사용하기 위해선 include를 사용하면 된다. model에 join에 참여할 테이블을 명시하고, where에 조건을 명시하면, workbook 테이블에서 출판사가 교육인 데이터들에 대해서만 join을 진행할 것이다.

이렇게 얻어낸 후보 문제들은 배열로 저장한다.

 

    //10개 뽑기
    let test_pb = new Array();
    for (var i = 0; i < 10; i++) {
        var temp = pb_array.splice(Math.floor(Math.random() * pb_array.length), 1)[0];
        test_pb.push(temp);
    }

우리에겐 10문제만 필요하니 배열에서 랜덤으로 10개만 뽑아 test_pb 배열에 저장한다.

 

    for (var i = 0; i < 10; i++) {
        models.test_pb_map.create({
            test_sn: sn,
            pb_sn: test_pb[i]
        })
            .catch(err => {
                console.log(err);
            });
    }

그리고 이 10문제들을 그대로 test_pb_map에 넣어주면 된다.

 

    try {
        res.send({ //시험지 정보 넘김
            message: "test created",
            status: 'success',
            data: {
                test_title: body.test_title,
                student: user,
                problems: test_pb
            }
        });
    } catch (err) { //무언가 문제가 생김
        res.send({
            message: "ERROR",
            status: 'fail'
        })
    }

마지막으로 정보를 넘기면 끝

 

생성된 미니모의고사
test 테이블
test_pb_map 테이블

DB에도 잘 들어온 것을 볼 수 있다.

Comments