본문 바로가기
  • 실행력이 모든걸 결정한다
개발 프로젝트/[팀] Web - SQL 교육 사이트

[팀 프로젝트] SQL 교육 사이트 - 채점 로직 상세 설명

by 김코더 김주역 2022. 11. 3.
반응형

동작 과정

1. Unit 1 테스트 페이지 접속

i) "http://localhost:8080/test/unit/1"에 접속한다.

@RequestMapping("/unit/{test_num}")
public String unit(@PathVariable("test_num") String test_num, Model model) {
    model.addAttribute("test_num", test_num);
    return "test/unit/test"+test_num;
}

- TestController의 unit() 컨트롤러 메소드에 매핑되어 resources/templates/test/unit/test1.mustache 뷰로 이동한다.

 

ii) 3문제를 다 풀고 답안 제출을 누른다.

 

 

2-1. submit 이벤트 처리

- Ajax를 사용하기 때문에 제출 버튼을 눌러도 <form>이 다른 페이지로 넘어가지 않고, resources/static/js/test.js의 리스너가 submit 이벤트를 감지해서 아래 리스너가 실행된다.

$('#answer-form').submit(function() {
    startTimeout();
    $('#result-container').show();
    $('html, body').animate({scrollTop: $('#result-container').offset().top}, 200);
    var serializedFormData = $('#answer-form').serializeObject();
    console.log(serializedFormData)
    $.ajax({
        url:"http://localhost:8080/test/grading",
        data: JSON.stringify(serializedFormData),
        type: "POST",
        dataType: "json",
        contentType : "application/json"
    })
    .done(function(json) {
        console.log(json);
        var questionList = json.questionList;
        $('#correct-count').text("맞은 개수 : "+json.correctCount+"/"+json.questionList.length);
        for(var i=0;i<questionList.length;i++) {
            var question = questionList[i];
            var result = question.sqlResult;
            if(question.isCorrect) {
                $('#result'+(i+1)+' .correct').show();
                $('#result'+(i+1)+' .incorrect').hide();
            }
            else {
                $('#result'+(i+1)+' .incorrect').show();
                $('#result'+(i+1)+' .correct').hide();
            }
            $('#user-answer-'+(i+1)).text(question.userAnswer);
            $('#result'+(i+1)+' .result-table > tbody').empty();
            if(question.errorMsg!=null) $('#result'+(i+1)+' .result-table > tbody:last').append('<tr><td style="border:none">'+question.errorMsg+'</td></tr>');
            if(result!=null) {
                for(var r=0;r<result.length;r++) {
                    var rowStr="<tr>";
                    if(r==0) rowStr="<tr style=\"background-color:#EEEEEE\">";
                    for(var c=0;c<result[r].length;c++) rowStr+="<td>"+result[r][c]+"</td>";
                    rowStr+="</tr>"
                    $('#result'+(i+1)+' .result-table > tbody:last').append(rowStr);
                    console.log(rowStr);
                }
            }
        }
    })
    .fail(function(xhr, status, errorThrown){
        alert("요청 실패");
    })
    return false;
});

 

위의 리스너는 아래와 같은 절차로 동작한다.

부분1

startTimeout(); // 과도한 Ajax 호출을 방지하기 위해 버튼을 5초간 비활성화 (아래 이미지의 빨간 상자)
$('#result-container').show(); // 빈 결과 컨테이너를 표시(아래 이미지는 최종 결과 화면이므로 결과가 나오는 것임)(아래 이미지의 초록 상자)

 

부분2

$('html, body').animate({scrollTop: $('#result-container').offset().top}, 200); // 결과 컨테이너로 스크롤 다운
var serializedFormData = $('#answer-form').serializeObject(); // 사용자가 입력한 form을 json으로 변환

 

부분3

- Ajax 처리 과정은 [2-2. 채점 로직]에서 설명할 것임

$.ajax({ // JSON으로 변환한 답안을 애플리케이션의 채점 컨트롤러로 Ajax 전송
    url:"http://localhost:8080/test/grading",
    data: JSON.stringify(serializedFormData),
    type: "POST",
    dataType: "json",
    contentType : "application/json"
})
.done(function(json) { // 채점에 성공했다면 결과 컨트롤러에 결과 출력
    console.log(json);
    var questionList = json.questionList;
    $('#correct-count').text("맞은 개수 : "+json.correctCount+"/"+json.questionList.length);
    for(var i=0;i<questionList.length;i++) {
        var question = questionList[i];
        var result = question.sqlResult;
        if(question.isCorrect) {
            $('#result'+(i+1)+' .correct').show();
            $('#result'+(i+1)+' .incorrect').hide();
        }
        else {
            $('#result'+(i+1)+' .incorrect').show();
            $('#result'+(i+1)+' .correct').hide();
        }
        $('#user-answer-'+(i+1)).text(question.userAnswer);
        $('#result'+(i+1)+' .result-table > tbody').empty();
        if(question.errorMsg!=null) $('#result'+(i+1)+' .result-table > tbody:last').append('<tr><td style="border:none">'+question.errorMsg+'</td></tr>');
        if(result!=null) {
            for(var r=0;r<result.length;r++) {
                var rowStr="<tr>";
                if(r==0) rowStr="<tr style=\"background-color:#EEEEEE\">";
                for(var c=0;c<result[r].length;c++) rowStr+="<td>"+result[r][c]+"</td>";
                rowStr+="</tr>"
                $('#result'+(i+1)+' .result-table > tbody:last').append(rowStr);
                console.log(rowStr);
            }
        }
    }
})
.fail(function(xhr, status, errorThrown){ // 채점에 실패했다면 경고창 표시
    alert("요청 실패");
})

 

부분4

return false;
// 리턴값을 true로 한다면 submit 버튼이 정상동작하여 다른 페이지로 이동하게됨
// 다시 한번 언급하지만, 우리는 Ajax를 사용하기 때문에 다른 페이지로 이동하지 않고 채점 결과를 보여줌

 

 

2-2. 채점 로직

i) JSON으로 변환한 답안을 애플리케이션의 채점 컨트롤러로 Ajax 전송

- $.ajax의 url에 의해 GradingController 컨트롤러로 요청 전송

url:"http://localhost:8080/test/grading"

 

@RestController
@RequestMapping("/test/grading")
public class GradingController {

    @Autowired GradingService gradingService;

    @PostMapping
    public TestResult gradingUnit(@RequestBody Map<String, Object> userAnswer, @ModelAttribute SQLData sqlData, BindingResult bindingResult) { // {"1": 4, "2": "select * from member;"}
        return gradingService.grade(userAnswer, sqlData, bindingResult);
    }
}

 

ii) GradingService로 이동

<GradingService에 있는 각 메소드의 역할 설명>

  • grade() : 채점할 유닛을 결정하고 채점 메소드를 호출한다.
  • validateAndGetResult() : 사용자가 입력한 SQL 답안에 DB 금지어(UPDATE, DROP, DELETE 등)가 포함되어 있거나 문법에 오류가 있다면 Question의 errorMsg에 에러 메시지를 담고 SQL 결과는 null로 나타낸다. SQL 답안에 문제가 없다면 SQL 결과를 반환한다.
  • getSqlResultForShow() : 뷰 화면에(사용자에게) 표시할 SQL 결과를 생성한다.

  • showSqlResult() : 개발자 확인용 메소드로, SQL 답안의 결과를 콘솔창에서 확인해보고 싶을 때 사용하면 된다.
showSqlResult(question3.getSqlResult());
  • gradeUnit1() : Unit 1의 채점 로직을 담은 메소드다. Unit x에 대한 채점 로직을 담은 메소드의 이름은 gradeUnitx로 작명하면 된다.

 

iii) 채점

- 채점의 결과는 gradeUnitx() 메소드 내부에서 TestResult 객체에 담으면 된다. TestResult 객체는 grade()를 거쳐 GradingController의 gradingUnit()로 전달된다.

- GradingController에 @RestController를 붙였기 때문에 TestResult 오브젝트를 리턴할 때 JSON으로 변환된다. 즉, Ajax는 TestResult를 json 형태로 받게 되는 것이다.

<Ajax 결과>

 

- 객관식 채점 예시

Question question1 = new Question();
question1.setNum(1); // 문제 번호는 1
question1.setUserAnswer(String.valueOf(answer1)); // 사용자의 답안 저장
if(answer1==5) { // 답이 5인 경우
    question1.setIsCorrect(true); // 정답 처리
    testResult.setCorrectCount(testResult.getCorrectCount()+1); // 총 맞은 개수를 1 카운트
}

 

- SQL문 채점 예시

Question question3 = new Question();
question3.setNum(3); // 문제 번호는 3
question3.setUserAnswer(answer3); // 사용자의 답안 저장

List<LinkedHashMap<String, Object>> sqlResult = validateAndGetSqlResult(answer3, sqlData, bindingResult, question3); // 사용자의 답안을 검증하고 sql 결과를 가져온다.
question3.setSqlResult(getSqlResultForShow(question3, sqlResult)); // 사용자에게 보여줄 sql 결과를 List<List<String>> 타입으로 생성 후 저장
// showSqlResult(question3.getSqlResult()); // 유저가 생성한 sql 결과를 확인

List<Employee> employeeList = employeeRepository.findAll(); // 실제 답안을 레포지토리에서 가져옴

// ★★★이 부분부터는 여러분들의 채점 방식에 따라 크게 차이가 있을 수 있습니다.★★★
int correctCount=0;
if(sqlResult!=null&&sqlResult.size()==5&&sqlResult.get(0).size()==3) { // 결과 튜플은 5개이고, 결과 필드는 3개여야 함
    for(int i=0;i<5;i++) {
        if(sqlResult.get(i).containsKey("NAME")&&sqlResult.get(i).get("NAME").toString().equals(employeeList.get(i).getName())) correctCount++;
        if(sqlResult.get(i).containsKey("POSITION")&&sqlResult.get(i).get("POSITION").toString().equals(employeeList.get(i).getPosition())) correctCount++;
        if(sqlResult.get(i).containsKey("SALARY")&&sqlResult.get(i).get("SALARY").toString().equals(String.valueOf(employeeList.get(i).getSalary()))) correctCount++;
    }
}
if(correctCount==15) { // 결과 테이블의 모든 셀이 실제 답안과 동일한 경우
    question3.setIsCorrect(true); // 정답 처리
    testResult.setCorrectCount(testResult.getCorrectCount()+1); // 총 맞은 개수를 1 카운트
}

 

gradeUnit1() 메소드 전체 코드

public TestResult gradeUnit1(Map<String, Object> userAnswer, SQLData sqlData, BindingResult bindingResult) {
    TestResult testResult = new TestResult();

    int answer1 = Integer.parseInt(userAnswer.get("question1").toString());
    int answer2 = Integer.parseInt(userAnswer.get("question2").toString());
    String answer3 = userAnswer.get("question3").toString();

    List<Question> questionList = new ArrayList<>();

    // 문제 1 채점
    Question question1 = new Question();
    question1.setNum(1);
    question1.setUserAnswer(String.valueOf(answer1));
    if(answer1==5) {
        question1.setIsCorrect(true);
        testResult.setCorrectCount(testResult.getCorrectCount()+1);
    }


    // 문제 2 채점
    Question question2 = new Question();
    question2.setNum(2);
    question2.setUserAnswer(String.valueOf(answer2));
    if(answer2==2) {
        question2.setIsCorrect(true);
        testResult.setCorrectCount(testResult.getCorrectCount()+1);
    }


    // 문제 3 채점
    Question question3 = new Question();
    question3.setNum(3);
    question3.setUserAnswer(answer3);

    List<LinkedHashMap<String, Object>> sqlResult = validateAndGetSqlResult(answer3, sqlData, bindingResult, question3); // 사용자의 답안을 검증하고 sql 결과를 가져온다.
    question3.setSqlResult(getSqlResultForShow(question3, sqlResult)); // 사용자에게 보여줄 sql 결과를 List<List<String>> 타입으로 생성 후 저장
    // showSqlResult(question3.getSqlResult()); // 유저가 생성한 sql 결과를 확인

    List<Employee> employeeList = employeeRepository.findAll(); // 답안
    int correctCount=0;
    if(sqlResult!=null&&sqlResult.size()==5&&sqlResult.get(0).size()==3) {
        for(int i=0;i<5;i++) {
            if(sqlResult.get(i).containsKey("NAME")&&sqlResult.get(i).get("NAME").toString().equals(employeeList.get(i).getName())) correctCount++;
            if(sqlResult.get(i).containsKey("POSITION")&&sqlResult.get(i).get("POSITION").toString().equals(employeeList.get(i).getPosition())) correctCount++;
            if(sqlResult.get(i).containsKey("SALARY")&&sqlResult.get(i).get("SALARY").toString().equals(String.valueOf(employeeList.get(i).getSalary()))) correctCount++;
        }
    }
    if(correctCount==15) {
        question3.setIsCorrect(true);
        testResult.setCorrectCount(testResult.getCorrectCount()+1);
    }

    questionList.add(question1);
    questionList.add(question2);
    questionList.add(question3);
    testResult.setQuestionList(questionList);
    return testResult;
}

 

 

코드를 모두 최적화했기 때문에 여러분들은 딱 2가지만 하면 됩니다.

  • 문제 출제 : testx.mustache 파일을 생성해서 작성 (기본적으로 작성된 틀을 제외하면 웬만하면 70~80라인 정도 작업하면 될 것으로 예상)
  • 문제 채점 : GradingService의 gradeUnitx() 메소드에서 채점을 진행하고, 채점 결과는 TestResult에 담아서 반환

 

반응형

댓글