Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: gradeExam #144

Merged
merged 9 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ kapt {

tasks.jacocoTestReport {
reports {
html.required.set(false)
html.required.set(true)
xml.required.set(true)
csv.required.set(false)

html.outputLocation = file(project.layout.buildDirectory.dir("jacoco/index.html"))
xml.outputLocation = file(project.layout.buildDirectory.dir("jacoco/index.xml"))
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
package com.swm_standard.phote.controller

import com.swm_standard.phote.common.resolver.memberId.MemberId
import com.swm_standard.phote.common.responsebody.BaseResponse
import com.swm_standard.phote.dto.GradeExamRequest
import com.swm_standard.phote.dto.GradeExamResponse
import com.swm_standard.phote.dto.ReadExamHistoryDetailResponse
import com.swm_standard.phote.dto.ReadExamHistoryListResponse
import com.swm_standard.phote.service.ExamService
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RequestMapping
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.util.UUID

@RestController
@RequestMapping("/api")
@Tag(name = "Exam", description = "Exam API Document")
class ExamController(
private val examService: ExamService
private val examService: ExamService,
) {
@Operation(summary = "readExamHistoryDetail", description = "문제풀이 기록 상세조회")
@SecurityRequirement(name = "bearer Auth")
@GetMapping("/exam/{id}")
fun readExamHistoryDetail(
@PathVariable(
required = true
) id: UUID
required = true,
) id: UUID,
): BaseResponse<ReadExamHistoryDetailResponse> =
BaseResponse(msg = "문제풀이 기록 상세조회 성공", data = examService.readExamHistoryDetail(id))

Expand All @@ -34,8 +41,21 @@ class ExamController(
@GetMapping("/exams/{workbookId}")
fun readExamHistoryList(
@PathVariable(
required = true
) workbookId: UUID
required = true,
) workbookId: UUID,
): BaseResponse<List<ReadExamHistoryListResponse>> =
BaseResponse(msg = "문제풀이 기록 리스트 조회 성공", data = examService.readExamHistoryList(workbookId))

@Operation(summary = "gradeExam", description = "문제풀이 제출 및 채점")
@SecurityRequirement(name = "bearer Auth")
@PostMapping("/exam/{workbookId}")
fun gradeExam(
@PathVariable(required = true) workbookId: UUID,
@Valid @RequestBody request: List<GradeExamRequest>,
@Parameter(hidden = true) @MemberId memberId: UUID,
): BaseResponse<GradeExamResponse> {
val response = examService.gradeExam(workbookId, request, memberId)

return BaseResponse(msg = "문제 풀이 채점 성공", data = response)
}
}
16 changes: 14 additions & 2 deletions src/main/kotlin/com/swm_standard/phote/dto/ChatGptDtos.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.swm_standard.phote.dto

import com.fasterxml.jackson.annotation.JsonProperty
import com.swm_standard.phote.external.chatgpt.PROMPT
import com.swm_standard.phote.external.chatgpt.CHECK_ANSWER_PROMPT
import com.swm_standard.phote.external.chatgpt.TRANSFORM_QUESTION_PROMPT

data class RequestMessage(
val role: String,
Expand Down Expand Up @@ -38,7 +39,18 @@ data class ChatGPTRequest(
mutableListOf(
RequestMessage(
"user",
mutableListOf(TextContent(PROMPT), ImageContent(ImageInfo(imageUrl))),
mutableListOf(TextContent(TRANSFORM_QUESTION_PROMPT), ImageContent(ImageInfo(imageUrl))),
),
),
)

constructor(model: String, submittedAnswer: String, correctAnswer: String) :
this(
model,
mutableListOf(
RequestMessage(
"user",
mutableListOf(TextContent("$CHECK_ANSWER_PROMPT\n$submittedAnswer \n$correctAnswer")),
),
),
)
Expand Down
27 changes: 23 additions & 4 deletions src/main/kotlin/com/swm_standard/phote/dto/ExamDtos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,41 @@ data class ReadExamHistoryDetail(
val image: String?,
val category: Category,
val answer: String,
val submittedAnswer: String,
val submittedAnswer: String?,
val isCorrect: Boolean,
val sequence: Int
val sequence: Int,
)

data class ReadExamHistoryDetailResponse(
val id: UUID,
val totalCorrect: Int,
val time: Int,
val questions: List<ReadExamHistoryDetail>
val questions: List<ReadExamHistoryDetail>,
)

data class ReadExamHistoryListResponse(
val examId: UUID,
val totalQuantity: Int,
val totalCorrect: Int,
val time: Int,
val sequence: Int
val sequence: Int,
)

data class GradeExamRequest(
val questionId: UUID,
val submittedAnswer: String?,
)

data class GradeExamResponse(
val examId: UUID,
val totalCorrect: Int,
val questionQuantity: Int,
val answers: List<AnswerResponse>,
)

data class AnswerResponse(
val questionId: UUID,
val submittedAnswer: String?,
val correctAnswer: String,
val isCorrect: Boolean,
)
4 changes: 2 additions & 2 deletions src/main/kotlin/com/swm_standard/phote/dto/WorkbookDtos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.swm_standard.phote.entity.Category
import com.swm_standard.phote.entity.Tag
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.PositiveOrZero
import jakarta.validation.constraints.Positive
import java.time.LocalDateTime
import java.util.UUID

Expand Down Expand Up @@ -65,7 +65,7 @@ data class DeleteQuestionInWorkbookResponse(
data class UpdateQuestionSequenceRequest(
@JsonProperty("id")
private val _id: UUID?,
@field:PositiveOrZero(message = "sequence는 0 이상의 정수만 가능합니다.")
@field:Positive(message = "sequence는 1 이상의 정수만 가능합니다.")
@JsonProperty("sequence")
private val _sequence: Int?,
) {
Expand Down
33 changes: 30 additions & 3 deletions src/main/kotlin/com/swm_standard/phote/entity/Answer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,40 @@ data class Answer(
@JoinColumn(name = "exam_id")
val exam: Exam,
@Column(name = "submitted_answer")
val submittedAnswer: String,
val isCorrect: Boolean,
val submittedAnswer: String?,
val sequence: Int,
) : BaseTimeEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "answer_id")
val id: Long = 0

val sequence: Int = 0
var isCorrect: Boolean = false

companion object {
fun createAnswer(
question: Question,
exam: Exam,
submittedAnswer: String?,
sequence: Int,
) = Answer(
question,
exam,
submittedAnswer,
sequence,
)
}

/**
* 객관식 문제면 정오답 체크를 하고 true 반환,
* 주관식 문제면 정오답 여부 판별 없이 false 반환 -> ChatGPT를 이용해야 하기 때문
**/
fun isMultipleAndCheckAnswer(): Boolean {
if (question.category == Category.MULTIPLE) {
isCorrect = submittedAnswer == question.answer
return true
} else {
return false
}
}
}
22 changes: 17 additions & 5 deletions src/main/kotlin/com/swm_standard/phote/entity/Exam.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.swm_standard.phote.entity
import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
Expand All @@ -17,21 +19,31 @@ data class Exam(
@ManyToOne
@JoinColumn(name = "workbook_id")
val workbook: Workbook,
val sequence: Int,
) : BaseTimeEntity() {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Column(name = "exam_id", nullable = false, unique = true)
val id: UUID = UUID.randomUUID()
var id: UUID? = null

val totalCorrect: Int = 0
var totalCorrect: Int = 0

@OneToMany(mappedBy = "exam", cascade = [(CascadeType.REMOVE)])
val answers: MutableList<Answer> = mutableListOf()

val time: Int = 0

val sequence: Int = 0
fun calculateTotalQuantity(): Int = answers.size

fun calculateTotalQuantity(): Int {
return answers.size
companion object {
fun createExam(
member: Member,
workbook: Workbook,
sequence: Int,
) = Exam(member, workbook, sequence)
}

fun increaseTotalCorrect(count: Int) {
totalCorrect += count
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.swm_standard.phote.external.chatgpt

const val PROMPT: String =
"너는 지금부터 텍스트 추출기야. 위에 첨부한 사진에서 문제 문항에 해당하는 텍스트와" +
"객관식 문제라면 선택지 텍스트를 #로 분리해서 추출해줘. 다른 대답없이 텍스트만 보내줘"
const val TRANSFORM_QUESTION_PROMPT: String =
"You are now a text extractor. Extract the text corresponding to the questions from the attached image. " +
"If it’s a multiple-choice question, separate the choices with #. " +
"Send only the extracted text without any other responses."

const val CHECK_ANSWER_PROMPT: String =
"너는 지금부터 주관식 채점기야. 첫번째 문단으로 보내는 제출 답안과 두번째 문단으로 보내는 정답의 유사도를 기준으로 정오답을 판단해줘. " +
"대답은 true 또는 false 로 해줘."
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.swm_standard.phote.repository

import com.swm_standard.phote.entity.Answer
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface AnswerRepository : JpaRepository<Answer, UUID>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.swm_standard.phote.repository

import com.swm_standard.phote.entity.Workbook

interface ExamCustomRepository {
fun findMaxSequenceByWorkbookId(workbook: Workbook): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.swm_standard.phote.repository

import com.querydsl.jpa.impl.JPAQueryFactory
import com.swm_standard.phote.entity.QExam.exam
import com.swm_standard.phote.entity.Workbook
import org.springframework.stereotype.Repository

@Repository
class ExamCustomRepositoryImpl(
private val jpaQueryFactory: JPAQueryFactory,
) : ExamCustomRepository {
override fun findMaxSequenceByWorkbookId(workbook: Workbook): Int {
val maxSequence: Int? =
jpaQueryFactory
.select(exam.sequence.max())
.from(exam)
.where(exam.workbook.eq(workbook))
.fetchOne()

if (maxSequence == null) return 0
return maxSequence
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.swm_standard.phote.entity.Exam
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID

interface ExamRepository : JpaRepository<Exam, UUID> {
interface ExamRepository :
JpaRepository<Exam, UUID>,
ExamCustomRepository {
fun findAllByWorkbookId(workbookId: UUID): List<Exam>
}
Loading
Loading