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: 학사 및 교과 검색 기능 추가 #177

Merged
merged 20 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
13 changes: 13 additions & 0 deletions src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,16 @@ fun substringAroundKeyword(keyword: String, content: String, amount: Int): Pair<
(index - frontIndex) to content.substring(frontIndex, backIndex)
}
}

fun exchangePageNum(pageSize: Int, pageNum: Int, total: Long): Int {
// Validate
if (!(pageSize > 0 && pageNum > 0 && total >= 0)) {
throw RuntimeException()
}

return if ((pageNum - 1) * pageSize < total) {
pageNum
} else {
Math.ceil(total.toDouble() / pageSize).toInt()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.wafflestudio.csereal.common.aop.AuthenticatedStaff
import com.wafflestudio.csereal.core.academics.dto.*
import com.wafflestudio.csereal.core.academics.service.AcademicsService
import com.wafflestudio.csereal.core.academics.dto.ScholarshipDto
import com.wafflestudio.csereal.core.academics.service.AcademicsSearchService
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
Expand All @@ -12,7 +13,8 @@ import org.springframework.web.multipart.MultipartFile
@RequestMapping("/api/v1/academics")
@RestController
class AcademicsController(
private val academicsService: AcademicsService
private val academicsService: AcademicsService,
private val academicsSearchService: AcademicsSearchService
) {
@AuthenticatedStaff
@PostMapping("/{studentType}/{postType}")
Expand Down Expand Up @@ -95,4 +97,24 @@ class AcademicsController(
fun getScholarship(@PathVariable scholarshipId: Long): ResponseEntity<ScholarshipDto> {
return ResponseEntity.ok(academicsService.readScholarship(scholarshipId))
}

@GetMapping("/search/top")
fun searchTop(
@RequestParam(required = true) keyword: String,
@RequestParam(required = true) number: Int
) = academicsSearchService.searchTopAcademics(
keyword = keyword,
number = number
)

@GetMapping("/search")
fun searchAcademics(
@RequestParam(required = true) keyword: String,
@RequestParam(required = true) pageSize: Int,
@RequestParam(required = true) pageNum: Int
) = academicsSearchService.searchAcademics(
keyword = keyword,
pageSize = pageSize,
pageNum = pageNum
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ class AcademicsEntity(
@Enumerated(EnumType.STRING)
var postType: AcademicsPostType,

var name: String?,
var name: String,
var description: String,
var year: Int?,
var time: String?,

@OneToMany(mappedBy = "academics", cascade = [CascadeType.ALL], orphanRemoval = true)
var attachments: MutableList<AttachmentEntity> = mutableListOf()
var attachments: MutableList<AttachmentEntity> = mutableListOf(),

@OneToOne(mappedBy = "academics", cascade = [CascadeType.ALL], orphanRemoval = true)
var academicsSearch: AcademicsSearchEntity? = null

) : BaseTimeEntity(), AttachmentContentEntityType {
override fun bringAttachments() = attachments
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.wafflestudio.csereal.core.academics.database

import com.wafflestudio.csereal.common.config.BaseTimeEntity
import com.wafflestudio.csereal.common.utils.cleanTextFromHtml
import jakarta.persistence.*

@Entity(name = "academics_search")
class AcademicsSearchEntity(
@Column(columnDefinition = "TEXT", nullable = false)
var content: String,

@OneToOne
@JoinColumn(name = "academics_id")
val academics: AcademicsEntity? = null,

@OneToOne
@JoinColumn(name = "course_id")
val course: CourseEntity? = null,

@OneToOne
@JoinColumn(name = "scholarship_id")
val scholarship: ScholarshipEntity? = null

) : BaseTimeEntity() {
companion object {
fun create(academics: AcademicsEntity): AcademicsSearchEntity {
return AcademicsSearchEntity(
academics = academics,
content = createContent(academics)
)
}

fun create(course: CourseEntity): AcademicsSearchEntity {
return AcademicsSearchEntity(
course = course,
content = createContent(course)
)
}

fun create(scholarship: ScholarshipEntity): AcademicsSearchEntity {
return AcademicsSearchEntity(
scholarship = scholarship,
content = createContent(scholarship)
)
}

fun createContent(academics: AcademicsEntity): String {
val sb = StringBuilder()
academics.name.let { sb.appendLine(it) }
academics.time?.let { sb.appendLine(it) }
academics.year?.let { sb.appendLine(it) }
sb.appendLine(academics.studentType.value)
sb.appendLine(
cleanTextFromHtml(
academics.description
)
)

return sb.toString()
}

fun createContent(course: CourseEntity) =
course.let {
val sb = StringBuilder()
sb.appendLine(it.studentType.value)
sb.appendLine(it.classification)
sb.appendLine(it.code)
sb.appendLine(it.name)
sb.appendLine(it.credit)
sb.appendLine(it.grade)
it.description?.let { desc ->
sb.appendLine(cleanTextFromHtml(desc))
}

sb.toString()
}

fun createContent(scholarship: ScholarshipEntity) =
scholarship.let {
val sb = StringBuilder()
sb.appendLine(it.studentType.value)
sb.appendLine(it.name)
sb.appendLine(
cleanTextFromHtml(it.description)
)
sb.toString()
}
}

fun update(academics: AcademicsEntity) {
this.content = createContent(academics)
}

fun update(course: CourseEntity) {
this.content = createContent(course)
}

fun update(scholarship: ScholarshipEntity) {
this.content = createContent(scholarship)
}

@PrePersist
@PreUpdate
fun checkType() {
if (!(
(academics != null && course == null && scholarship == null) ||
(academics == null && course != null && scholarship == null) ||
(academics == null && course == null && scholarship != null)
)
) {
throw IllegalStateException("AcademicsSearchEntity must have only one type of entity")
}
}

fun ofType() =
when {
academics != null && course == null && scholarship == null -> AcademicsSearchType.ACADEMICS
academics == null && course != null && scholarship == null -> AcademicsSearchType.COURSE
academics == null && course == null && scholarship != null -> AcademicsSearchType.SCHOLARSHIP
else -> throw IllegalStateException("AcademicsSearchEntity must have only one type of entity")
}
}

enum class AcademicsSearchType {
ACADEMICS,
COURSE,
SCHOLARSHIP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.wafflestudio.csereal.core.academics.database

import com.querydsl.jpa.impl.JPAQuery
import com.querydsl.jpa.impl.JPAQueryFactory
import com.wafflestudio.csereal.common.repository.CommonRepository
import com.wafflestudio.csereal.core.academics.database.QAcademicsEntity.academicsEntity
import com.wafflestudio.csereal.core.academics.database.QAcademicsSearchEntity.academicsSearchEntity
import com.wafflestudio.csereal.core.academics.database.QCourseEntity.courseEntity
import com.wafflestudio.csereal.core.academics.database.QScholarshipEntity.scholarshipEntity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import com.wafflestudio.csereal.common.utils.exchangePageNum

interface AcademicsSearchRepository : JpaRepository<AcademicsSearchEntity, Long>, AcademicsSearchCustomRepository

interface AcademicsSearchCustomRepository {
fun searchAcademics(keyword: String, pageSize: Int, pageNum: Int): Pair<List<AcademicsSearchEntity>, Long>
fun searchTopAcademics(keyword: String, number: Int): List<AcademicsSearchEntity>
}

@Repository
class AcademicsSearchCustomRepositoryImpl(
private val queryFactory: JPAQueryFactory,
private val commonRepository: CommonRepository
) : AcademicsSearchCustomRepository {
override fun searchTopAcademics(keyword: String, number: Int): List<AcademicsSearchEntity> {
return searchQuery(keyword)
.limit(number.toLong())
.fetch()
}

override fun searchAcademics(
keyword: String,
pageSize: Int,
pageNum: Int
): Pair<List<AcademicsSearchEntity>, Long> {
val query = searchQuery(keyword)
val total = getSearchCount(keyword)

val validPageNum = exchangePageNum(pageSize, pageNum, total)
val validOffset = (if (validPageNum >= 1) validPageNum - 1 else 0) * pageSize.toLong()
val queryResult = query.offset(validOffset)
.limit(pageSize.toLong())
.fetch()

return queryResult to total
}

fun searchQuery(keyword: String): JPAQuery<AcademicsSearchEntity> {
val searchDoubleTemplate = commonRepository.searchFullSingleTextTemplate(
keyword,
academicsSearchEntity.content
)

return queryFactory.selectFrom(
academicsSearchEntity
).leftJoin(
academicsSearchEntity.academics,
academicsEntity
).fetchJoin()
.leftJoin(
academicsSearchEntity.course,
courseEntity
).fetchJoin()
.leftJoin(
academicsSearchEntity.scholarship,
scholarshipEntity
).fetchJoin()
.where(
searchDoubleTemplate.gt(0.0)
)
}

fun getSearchCount(keyword: String): Long {
val searchDoubleTemplate = commonRepository.searchFullSingleTextTemplate(
keyword,
academicsSearchEntity.content
)

return queryFactory.select(
academicsSearchEntity.countDistinct()
).from(academicsSearchEntity)
.where(
searchDoubleTemplate.gt(0.0)
).fetchOne()!!
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wafflestudio.csereal.core.academics.database

enum class AcademicsStudentType {
UNDERGRADUATE, GRADUATE
enum class AcademicsStudentType(val value: String) {
UNDERGRADUATE("학부"),
GRADUATE("대학원");
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEnti
import jakarta.persistence.CascadeType
import jakarta.persistence.Entity
import jakarta.persistence.OneToMany
import jakarta.persistence.OneToOne

@Entity(name = "course")
class CourseEntity(
Expand All @@ -27,7 +28,10 @@ class CourseEntity(
var description: String?,

@OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true)
var attachments: MutableList<AttachmentEntity> = mutableListOf()
var attachments: MutableList<AttachmentEntity> = mutableListOf(),

@OneToOne(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true)
var academicsSearch: AcademicsSearchEntity? = null

) : BaseTimeEntity(), AttachmentContentEntityType {
override fun bringAttachments() = attachments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ class ScholarshipEntity(
val name: String,

@Column(columnDefinition = "text")
val description: String
val description: String,

@OneToOne(mappedBy = "scholarship", cascade = [CascadeType.ALL], orphanRemoval = true)
var academicsSearch: AcademicsSearchEntity? = null

) : BaseTimeEntity() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentResponse
import java.time.LocalDateTime

data class AcademicsDto(
val id: Long,
val name: String?,
val id: Long = -1, // TODO: Seperate to multiple DTOs or set this as nullable
val name: String,
val description: String,
val year: Int?,
val time: String?,
val createdAt: LocalDateTime?,
val modifiedAt: LocalDateTime?,
val attachments: List<AttachmentResponse>?
val year: Int? = null,
val time: String? = null,
val createdAt: LocalDateTime? = null,
val modifiedAt: LocalDateTime? = null,
val attachments: List<AttachmentResponse>? = null
) {
companion object {
fun of(entity: AcademicsEntity, attachmentResponses: List<AttachmentResponse>): AcademicsDto = entity.run {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.wafflestudio.csereal.core.academics.dto

import com.wafflestudio.csereal.core.academics.database.AcademicsSearchEntity

data class AcademicsSearchPageResponse(
val academics: List<AcademicsSearchResponseElement>,
val total: Long
) {
companion object {
fun of(
academics: List<AcademicsSearchEntity>,
total: Long
) = AcademicsSearchPageResponse(
academics = academics.map(AcademicsSearchResponseElement::of),
total = total
)
}
}
Loading
Loading