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: 과목 선정 로직 구현 - 1단계 #27

Merged
merged 9 commits into from
Feb 5, 2025
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.yourssu.soongpt.domain.course.storage
package com.yourssu.soongpt.domain.course

import com.yourssu.soongpt.domain.course.implement.Classification
import com.yourssu.soongpt.domain.course.implement.Course
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.yourssu.soongpt.domain.course.storage
package com.yourssu.soongpt.domain.course

import com.querydsl.jpa.impl.JPAQueryFactory
import com.yourssu.soongpt.domain.course.QCourseEntity.courseEntity
import com.yourssu.soongpt.domain.course.implement.Classification
import com.yourssu.soongpt.domain.course.implement.Course
import com.yourssu.soongpt.domain.course.implement.CourseRepository
import com.yourssu.soongpt.domain.course.storage.QCourseEntity.courseEntity
import com.yourssu.soongpt.domain.course.implement.Courses
import com.yourssu.soongpt.domain.departmentGrade.storage.QDepartmentGradeEntity.departmentGradeEntity
import com.yourssu.soongpt.domain.target.storage.QTargetEntity.targetEntity
import org.springframework.data.jpa.repository.JpaRepository
Expand Down Expand Up @@ -40,6 +41,26 @@ class CourseRepositoryImpl(
.fetch()
.map { it.toDomain() }
}

override fun findByDepartmentIdAndCourseName(
departmentId: Long,
courseName: String,
classification: Classification
): Courses {
val courses = jpaQueryFactory.selectFrom(courseEntity)
.innerJoin(targetEntity)
.on(courseEntity.id.eq(targetEntity.courseId))
.innerJoin(departmentGradeEntity)
.on(targetEntity.departmentGradeId.eq(departmentGradeEntity.id))
.where(
departmentGradeEntity.departmentId.eq(departmentId),
courseEntity.courseName.eq(courseName),
courseEntity.classification.eq(classification)
)
.fetch()
.map { it.toDomain() }
return Courses(courses)
}
}

interface CourseJpaRepository : JpaRepository<CourseEntity, Long> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.yourssu.soongpt.domain.course.implement

import com.yourssu.soongpt.domain.course.implement.exception.CourseNotFoundException
import org.springframework.stereotype.Component

@Component
Expand All @@ -17,4 +18,24 @@ class CourseReader(
fun findAllByDepartmentIdInGeneralRequired(departmentId: Long): List<Course> {
return courseRepository.findAllByDepartmentId(departmentId, Classification.GENERAL_REQUIRED)
}

fun findAllByCourseNameInMajorRequired(departmentId: Long, courseName: String): Courses {
return findAllByCourseName(departmentId, courseName, Classification.MAJOR_REQUIRED)
}

fun findAllByCourseNameInMajorElective(departmentId: Long, courseName: String): Courses {
return findAllByCourseName(departmentId, courseName, Classification.MAJOR_ELECTIVE)
}

fun findAllByCourseNameInGeneralRequired(departmentId: Long, courseName: String): Courses {
return findAllByCourseName(departmentId, courseName, Classification.GENERAL_REQUIRED)
}

private fun findAllByCourseName(departmentId: Long, courseName: String, classification: Classification): Courses {
val courses = courseRepository.findByDepartmentIdAndCourseName(departmentId, courseName, classification)
if (courses.isEmpty()) {
throw CourseNotFoundException(courseName = courseName)
}
return courses
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ interface CourseRepository {
fun save(course: Course): Course
fun findAllByDepartmentId(departmentId: Long, classification: Classification): List<Course>
fun getAll(ids: List<Long>): List<Course>
fun findByDepartmentIdAndCourseName(departmentId: Long, courseName: String, classification: Classification): Courses
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.yourssu.soongpt.domain.course.implement

class Courses(
val courses: List<Course>,
) {
fun isEmpty(): Boolean {
return courses.isEmpty()
}

fun unpackNameAndProfessor(): List<Pair<String, String>> {
return courses.map { it.courseName to it.professorName!! }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.yourssu.soongpt.domain.course.implement

import com.yourssu.soongpt.domain.course.implement.exception.InvalidTimetableRequestException
import org.springframework.stereotype.Component

@Component
class CoursesFactory {
fun generateTimetableCandidates(
coursesCandidates: List<Courses>
): List<Courses> {
return coursesCandidates.fold(listOf(emptyList<Course>())) { acc, courses ->
acc.flatMap { currentCombination ->
courses.courses.map { course -> currentCombination + course }
}
}.map { Courses(it) }
}

fun validateEmpty(timetableCandidates: List<Courses>) {
if (timetableCandidates.isEmpty()) {
throw InvalidTimetableRequestException()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.yourssu.soongpt.domain.course.implement.exception

import com.yourssu.soongpt.common.handler.NotFoundException

class CourseNotFoundException(
val courseName: String = "",
) : NotFoundException(message = "해당하는 과목이 없습니다. 과목 이름: {$courseName}") {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.yourssu.soongpt.domain.course.implement.exception

class InvalidCoursesException : RuntimeException()
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.yourssu.soongpt.domain.course.implement.exception

import com.yourssu.soongpt.common.handler.BadRequestException

class InvalidTimetableRequestException : BadRequestException(message = "시간표가 나올 수 있는 경우의 수가 없습니다.") {

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ data class RatingCreatedRequest(
val course: String,
val professor: String,
val courseCode: String,
val star: Int,
val star: Double,
) {
fun toCommand(): RatingCreatedCommand {
return RatingCreatedCommand(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class RatingCreatedCommand(
val course: String,
val professor: String,
val courseCode: String,
val star: Int,
val star: Double,
) {
fun toRating(): Rating {
return Rating(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ data class RatingResponse(
val course: String,
val professor: String,
val courseCode: String,
val star: Int,
val star: Double,
val point: Double,
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Rating(
val courseName: String,
val professorName: String,
val courseCode: String,
val star: Int,
val star: Double = 3.91,
var point: Double = 50.0,
) {
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package com.yourssu.soongpt.domain.rating.implement

import com.yourssu.soongpt.domain.course.implement.Courses
import com.yourssu.soongpt.domain.rating.implement.exception.RatingNotFoundByCourseNameException
import org.springframework.stereotype.Component

@Component
class RatingReader(
private val ratingRepository: RatingRepository,
) {
fun findAllBy(courses: Courses): List<Rating> {
return courses.unpackNameAndProfessor().mapNotNull { (courseName, professorName) ->
ratingRepository.findByCourseNameAndProfessorName(courseName, professorName)
}
}


fun findBy(courseName: String, professorName: String): Rating {
return ratingRepository.findByCourseNameAndProfessorName(courseName, professorName)
?: throw RatingNotFoundByCourseNameException()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class RatingEntity(
val courseCode: String,

@Column(name = "star", nullable = false)
val star: Int,
val star: Double,

@Column(name = "point", nullable = false)
val point: Double,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package com.yourssu.soongpt.domain.timetable.application

import com.yourssu.soongpt.common.business.dto.Response
import com.yourssu.soongpt.domain.timetable.application.dto.TimetableCreatedRequest
import com.yourssu.soongpt.domain.timetable.business.TimetableService
import com.yourssu.soongpt.domain.timetable.business.dto.TimetableResponse
import com.yourssu.soongpt.domain.timetable.business.dto.TimetableResponses
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/api/timetables")
class TimetableController(
private val timetableService: TimetableService,
) {
@PostMapping
fun createTimetable(@RequestBody request: TimetableCreatedRequest): ResponseEntity<Response<TimetableResponses>> {
val responses = timetableService.createTimetable(request.toCommand())
return ResponseEntity.ok(Response(result=responses))
}


@GetMapping("/{id}")
fun getTimetable(@PathVariable id: Long): ResponseEntity<Response<TimetableResponse>> {
val response = timetableService.getTimeTable(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.yourssu.soongpt.domain.timetable.application.dto

import com.yourssu.soongpt.domain.timetable.business.dto.TimetableCreatedCommand
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull
import org.hibernate.validator.constraints.Range

data class TimetableCreatedRequest(
@field:Range(min = 15, max = 25, message = "학번은 15부터 25까지 가능합니다.")
val schoolId: Int,

@field:NotBlank(message = "학과는 필수 입력값입니다.")
val department: String,

@field:Range(min = 1, max = 5, message = "학년은 1부터 5까지 가능합니다.")
val grade: Int,

val isChapel: Boolean = false,

@field:NotNull
val majorRequiredCourses: List<String>,

@field:NotNull
val majorElectiveCourses: List<String>,

@field:NotNull
val generalRequiredCourses: List<String>,
) {
fun toCommand(): TimetableCreatedCommand {
return TimetableCreatedCommand(
departmentName = department,
grade = grade,
isChapel = isChapel,
majorRequiredCourses = majorRequiredCourses,
majorElectiveCourses = majorElectiveCourses,
generalRequiredCourses = generalRequiredCourses,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,68 @@
package com.yourssu.soongpt.domain.timetable.business

import com.yourssu.soongpt.domain.course.implement.CourseReader
import com.yourssu.soongpt.domain.course.implement.Courses
import com.yourssu.soongpt.domain.course.implement.CoursesFactory
import com.yourssu.soongpt.domain.courseTime.implement.CourseTimeReader
import com.yourssu.soongpt.domain.department.implement.DepartmentReader
import com.yourssu.soongpt.domain.timetable.business.dto.TimetableCourseResponse
import com.yourssu.soongpt.domain.timetable.business.dto.TimetableCreatedCommand
import com.yourssu.soongpt.domain.timetable.business.dto.TimetableResponse
import com.yourssu.soongpt.domain.timetable.implement.TimetableCourseReader
import com.yourssu.soongpt.domain.timetable.implement.TimetableReader
import com.yourssu.soongpt.domain.timetable.business.dto.TimetableResponses
import com.yourssu.soongpt.domain.timetable.implement.*
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class TimetableService(
private val timetableReader: TimetableReader,
private val timetableWriter: TimetableWriter,
private val timetableCourseWriter: TimetableCourseWriter,
private val timetableCourseReader: TimetableCourseReader,
private val courseTimeReader: CourseTimeReader,
private val departmentReader: DepartmentReader,
private val courseReader: CourseReader,
private val coursesFactory: CoursesFactory,
) {
@Transactional
fun createTimetable(command: TimetableCreatedCommand): TimetableResponses {
val department = departmentReader.getByName(command.departmentName)
val majorRequiredCourses =
command.majorRequiredCourses.map { courseReader.findAllByCourseNameInMajorRequired(department.id!!, it) }
val majorElectiveCourses =
command.majorElectiveCourses.map { courseReader.findAllByCourseNameInMajorElective(department.id!!, it) }
val generalRequiredCourses =
command.generalRequiredCourses.map { courseReader.findAllByCourseNameInGeneralRequired(department.id!!, it)
}

val coursesCandidates =
coursesFactory.generateTimetableCandidates(majorRequiredCourses + majorElectiveCourses + generalRequiredCourses)
coursesFactory.validateEmpty(coursesCandidates)

val responses = ArrayList<TimetableResponse>()
for (courses in coursesCandidates) {
val timetable = timetableWriter.save(Timetable(tag = Tag.DEFAULT))
saveTimetableCourses(courses, timetable)
responses.add(
TimetableResponse(
timetable.id!!,
timetable.tag.name,
toTimetableCourseResponses(courses)
)
)
}
return TimetableResponses(responses)
}

private fun toTimetableCourseResponses(courses: Courses) =
courses.courses.map { TimetableCourseResponse.from(it, courseTimeReader.findAllByCourseId(it.id!!)) }

private fun saveTimetableCourses(courses: Courses, timetable: Timetable) {
for (course in courses.courses) {
timetableCourseWriter.save(TimetableCourse(timetableId = timetable.id!!, courseId = course.id!!))
}
}

fun getTimeTable(id: Long): TimetableResponse {
val timetable = timetableReader.get(id)
val courses = timetableCourseReader.findAllCourseByTimetableId(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.yourssu.soongpt.domain.timetable.business.dto

data class TimetableCreatedCommand(
val departmentName: String,
val grade: Int,
val isChapel: Boolean,
val majorRequiredCourses: List<String>,
val majorElectiveCourses: List<String>,
val generalRequiredCourses: List<String>,
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.yourssu.soongpt.domain.timetable.business.dto

data class TimetableResponses(
val timetables: List<TimetableResponse>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ enum class CourseFixture(
classification = Classification.GENERAL_REQUIRED,
courseCode = 3,
credit = 3,
)
),
;

fun toDomain(courseCode: Int = this.courseCode): Course {
Expand Down
Loading