diff --git a/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt b/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt index 9d3fbbb7..0f3b1340 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt @@ -3,9 +3,11 @@ package com.wafflestudio.csereal import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.scheduling.annotation.EnableAsync @EnableJpaAuditing @SpringBootApplication +@EnableAsync class CserealApplication fun main(args: Array) { diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt b/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt index 9eb21d03..1b56827a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.common.aop import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.mockauth.CustomPrincipal import com.wafflestudio.csereal.core.user.database.Role import com.wafflestudio.csereal.core.user.database.UserEntity import com.wafflestudio.csereal.core.user.database.UserRepository @@ -38,6 +39,11 @@ class SecurityAspect(private val userRepository: UserRepository) { val authentication = SecurityContextHolder.getContext().authentication val principal = authentication.principal + // for dev Mock User + if (principal is CustomPrincipal) { + return principal.userEntity + } + if (principal !is OidcUser) { throw CserealException.Csereal401("로그인이 필요합니다.") } diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt index 08367526..f1ca9ef4 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.core.Authentication @@ -18,6 +19,7 @@ import org.springframework.web.cors.CorsConfiguration import org.springframework.web.cors.CorsConfigurationSource import org.springframework.web.cors.UrlBasedCorsConfigurationSource +@Profile("!test") @Configuration @EnableWebSecurity @EnableConfigurationProperties(EndpointProperties::class) diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/CustomPrincipal.kt b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/CustomPrincipal.kt new file mode 100644 index 00000000..16eec3c1 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/CustomPrincipal.kt @@ -0,0 +1,10 @@ +package com.wafflestudio.csereal.common.mockauth + +import com.wafflestudio.csereal.core.user.database.UserEntity +import java.security.Principal + +data class CustomPrincipal(val userEntity: UserEntity) : Principal { + override fun getName(): String { + return userEntity.username + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/DevAuthController.kt b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/DevAuthController.kt new file mode 100644 index 00000000..19110220 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/DevAuthController.kt @@ -0,0 +1,49 @@ +package com.wafflestudio.csereal.common.mockauth + +import com.wafflestudio.csereal.core.user.database.Role +import com.wafflestudio.csereal.core.user.database.UserEntity +import com.wafflestudio.csereal.core.user.database.UserRepository +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.ResponseEntity +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.context.SecurityContextRepository +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +//TODO: 정식 릴리즈 후에는 dev 서버에서만 가능하게 +@RestController +@RequestMapping("/api") +class DevAuthController( + private val authenticationManager: AuthenticationManager, + private val userRepository: UserRepository, + private val securityContextRepository: SecurityContextRepository +) { + + @GetMapping("/mock-login") + fun mockLogin(request: HttpServletRequest, response: HttpServletResponse): ResponseEntity { + val mockUser = userRepository.findByUsername("devUser") + ?: userRepository.save(UserEntity("devUser", "Mock", "mock@abc.com", "0000-00000", Role.ROLE_STAFF)) + val customPrincipal = CustomPrincipal(mockUser) + val authenticationToken = UsernamePasswordAuthenticationToken( + customPrincipal, + null, + listOf( + SimpleGrantedAuthority("ROLE_STAFF") + ) + ) + + val authentication = authenticationManager.authenticate(authenticationToken) + SecurityContextHolder.getContext().authentication = authentication + + securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response) + + request.getSession(true) + + return ResponseEntity.ok().body("Mock user authenticated") + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/DevAuthenticationProvider.kt b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/DevAuthenticationProvider.kt new file mode 100644 index 00000000..cfdf0761 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/DevAuthenticationProvider.kt @@ -0,0 +1,26 @@ +package com.wafflestudio.csereal.common.mockauth + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.user.database.UserRepository +import org.springframework.security.authentication.AuthenticationProvider +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.stereotype.Component + +@Component +class DevAuthenticationProvider(private val userRepository: UserRepository) : AuthenticationProvider { + + override fun authenticate(authentication: Authentication): Authentication? { + val username = authentication.name + val userEntity = + userRepository.findByUsername(username) ?: throw CserealException.Csereal404("Mock User not found") + + val customPrincipal = CustomPrincipal(userEntity) + return UsernamePasswordAuthenticationToken(customPrincipal, null, listOf(SimpleGrantedAuthority("ROLE_STAFF"))) + } + + override fun supports(authentication: Class<*>): Boolean { + return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/MockAuthConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/MockAuthConfig.kt new file mode 100644 index 00000000..5f535263 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/mockauth/MockAuthConfig.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.csereal.common.mockauth + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.security.web.context.SecurityContextRepository + +@Configuration +class MockAuthConfig( + private val devAuthenticationProvider: DevAuthenticationProvider +) { + @Bean + fun authenticationManager(http: HttpSecurity): AuthenticationManager { + http.authenticationProvider(devAuthenticationProvider) + return http.getSharedObject(AuthenticationManagerBuilder::class.java).build() + } + + @Bean + fun securityContextRepository(): SecurityContextRepository { + return HttpSessionSecurityContextRepository() + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/properties/LanguageType.kt b/src/main/kotlin/com/wafflestudio/csereal/common/properties/LanguageType.kt new file mode 100644 index 00000000..92b6f511 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/properties/LanguageType.kt @@ -0,0 +1,21 @@ +package com.wafflestudio.csereal.common.properties + +import com.wafflestudio.csereal.common.CserealException + +enum class LanguageType { + KO, EN; + + companion object { + fun makeStringToLanguageType(language: String): LanguageType { + try { + val upperLanguageType = language.uppercase() + return LanguageType.valueOf(upperLanguageType) + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("해당하는 enum을 찾을 수 없습니다") + } + } + + // dto로 통신할 때 소문자로 return + fun makeLowercase(languageType: LanguageType): String = languageType.toString().lowercase() + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/utils/StringListConverter.kt b/src/main/kotlin/com/wafflestudio/csereal/common/utils/StringListConverter.kt new file mode 100644 index 00000000..a6227833 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/utils/StringListConverter.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.common.utils + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter + +@Converter +class StringListConverter : AttributeConverter, String> { + override fun convertToDatabaseColumn(p0: MutableList?): String = + ObjectMapper().writeValueAsString(p0 ?: mutableListOf()) + + override fun convertToEntityAttribute(p0: String?): MutableList = + ObjectMapper().readValue(p0 ?: "[]") +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt b/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt index 06f32bea..f0f59159 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt @@ -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() + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt index c028b641..61043e39 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt @@ -36,24 +36,31 @@ class AboutController( // read 목록이 하나 @GetMapping("/{postType}") fun readAbout( + @RequestParam(required = false, defaultValue = "ko") language: String, @PathVariable postType: String ): ResponseEntity { - return ResponseEntity.ok(aboutService.readAbout(postType)) + return ResponseEntity.ok(aboutService.readAbout(language, postType)) } @GetMapping("/student-clubs") - fun readAllClubs(): ResponseEntity> { - return ResponseEntity.ok(aboutService.readAllClubs()) + fun readAllClubs( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity> { + return ResponseEntity.ok(aboutService.readAllClubs(language)) } @GetMapping("/facilities") - fun readAllFacilities(): ResponseEntity> { - return ResponseEntity.ok(aboutService.readAllFacilities()) + fun readAllFacilities( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity> { + return ResponseEntity.ok(aboutService.readAllFacilities(language)) } @GetMapping("/directions") - fun readAllDirections(): ResponseEntity> { - return ResponseEntity.ok(aboutService.readAllDirections()) + fun readAllDirections( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity> { + return ResponseEntity.ok(aboutService.readAllDirections(language)) } @GetMapping("/future-careers") @@ -95,4 +102,15 @@ class AboutController( ): ResponseEntity> { return ResponseEntity.ok(aboutService.migrateDirections(requestList)) } + + @PatchMapping("/migrateImage/{aboutId}") + fun migrateAboutImageAndAttachment( + @PathVariable aboutId: Long, + @RequestPart("mainImage") mainImage: MultipartFile?, + @RequestPart("attachments") attachments: List? + ): ResponseEntity { + return ResponseEntity.ok( + aboutService.migrateAboutImageAndAttachments(aboutId, mainImage, attachments) + ) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt index dbe06392..2b97fd06 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt @@ -3,6 +3,8 @@ package com.wafflestudio.csereal.core.about.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.common.controller.AttachmentContentEntityType import com.wafflestudio.csereal.common.controller.MainImageContentEntityType +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.common.utils.StringListConverter import com.wafflestudio.csereal.core.about.dto.AboutDto import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity @@ -12,16 +14,18 @@ import jakarta.persistence.* class AboutEntity( @Enumerated(EnumType.STRING) var postType: AboutPostType, + @Enumerated(EnumType.STRING) + var language: LanguageType = LanguageType.KO, var name: String?, - var engName: String?, @Column(columnDefinition = "mediumText") var description: String, var year: Int?, - @OneToMany(mappedBy = "about", cascade = [CascadeType.ALL], orphanRemoval = true) - val locations: MutableList = mutableListOf(), + @Column(columnDefinition = "TEXT") + @Convert(converter = StringListConverter::class) + var locations: MutableList = mutableListOf(), @OneToMany(mappedBy = "") var attachments: MutableList = mutableListOf(), @@ -34,13 +38,14 @@ class AboutEntity( override fun bringAttachments(): List = attachments companion object { - fun of(postType: AboutPostType, aboutDto: AboutDto): AboutEntity { + fun of(postType: AboutPostType, languageType: LanguageType, aboutDto: AboutDto): AboutEntity { return AboutEntity( postType = postType, + language = languageType, name = aboutDto.name, - engName = aboutDto.engName, description = aboutDto.description, - year = aboutDto.year + year = aboutDto.year, + locations = aboutDto.locations?.toMutableList() ?: mutableListOf() ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt index 85d5331a..b98a26fd 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt @@ -1,8 +1,15 @@ package com.wafflestudio.csereal.core.about.database +import com.wafflestudio.csereal.common.properties.LanguageType import org.springframework.data.jpa.repository.JpaRepository interface AboutRepository : JpaRepository { - fun findAllByPostTypeOrderByName(postType: AboutPostType): List - fun findByPostType(postType: AboutPostType): AboutEntity + fun findAllByLanguageAndPostTypeOrderByName( + languageType: LanguageType, + postType: AboutPostType + ): List + fun findByLanguageAndPostType( + languageType: LanguageType, + postType: AboutPostType + ): AboutEntity } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt deleted file mode 100644 index 38929681..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.wafflestudio.csereal.core.about.database - -import com.wafflestudio.csereal.common.config.BaseTimeEntity -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne - -@Entity(name = "location") -class LocationEntity( - val name: String, - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "about_id") - val about: AboutEntity -) : BaseTimeEntity() { - companion object { - fun create(name: String, about: AboutEntity): LocationEntity { - val locationEntity = LocationEntity( - name = name, - about = about - ) - about.locations.add(locationEntity) - return locationEntity - } - } -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt index 72aa0bde..3dd181b6 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.about.dto import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.about.database.AboutEntity import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentResponse import java.time.LocalDateTime @@ -8,8 +9,8 @@ import java.time.LocalDateTime data class AboutDto( @JsonInclude(JsonInclude.Include.NON_NULL) val id: Long? = null, + val language: String, val name: String?, - val engName: String?, val description: String, val year: Int?, val createdAt: LocalDateTime?, @@ -26,13 +27,13 @@ data class AboutDto( ): AboutDto = entity.run { AboutDto( id = this.id, + language = LanguageType.makeLowercase(this.language), name = this.name, - engName = this.engName, description = this.description, year = this.year, createdAt = this.createdAt, modifiedAt = this.modifiedAt, - locations = this.locations.map { it.name }, + locations = this.locations, imageURL = imageURL, attachments = attachmentResponses ) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutRequest.kt index 80aa8c28..2c81496c 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutRequest.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutRequest.kt @@ -2,5 +2,6 @@ package com.wafflestudio.csereal.core.about.dto data class AboutRequest( val postType: String, + val language: String, val description: String ) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/DirectionDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/DirectionDto.kt index 3507c5df..bd2ae452 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/DirectionDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/DirectionDto.kt @@ -1,21 +1,22 @@ package com.wafflestudio.csereal.core.about.dto import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.about.database.AboutEntity data class DirectionDto( @JsonInclude(JsonInclude.Include.NON_NULL) val id: Long? = null, + val language: String, val name: String, - val engName: String, val description: String ) { companion object { fun of(entity: AboutEntity): DirectionDto = entity.run { DirectionDto( id = this.id, + language = LanguageType.makeLowercase(this.language), name = this.name!!, - engName = this.engName!!, description = this.description ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FacilityDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FacilityDto.kt index ac18d04b..71ce1d5d 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FacilityDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FacilityDto.kt @@ -1,11 +1,13 @@ package com.wafflestudio.csereal.core.about.dto import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.about.database.AboutEntity data class FacilityDto( @JsonInclude(JsonInclude.Include.NON_NULL) val id: Long? = null, + val language: String, val name: String, val description: String, val locations: List @@ -14,9 +16,10 @@ data class FacilityDto( fun of(entity: AboutEntity): FacilityDto = entity.run { FacilityDto( id = this.id, + language = LanguageType.makeLowercase(this.language), name = this.name!!, description = this.description, - locations = this.locations.map { it.name } + locations = this.locations ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FutureCareersRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FutureCareersRequest.kt index 425e6a13..c7341cef 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FutureCareersRequest.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/FutureCareersRequest.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.about.dto data class FutureCareersRequest( + val language: String, val description: String, val stat: List, val companies: List diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/StudentClubDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/StudentClubDto.kt index ed536178..89b70984 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/StudentClubDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/StudentClubDto.kt @@ -1,21 +1,22 @@ package com.wafflestudio.csereal.core.about.dto import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.about.database.AboutEntity data class StudentClubDto( @JsonInclude(JsonInclude.Include.NON_NULL) val id: Long? = null, + val language: String, val name: String, - val engName: String, val description: String ) { companion object { fun of(entity: AboutEntity): StudentClubDto = entity.run { StudentClubDto( id = this.id, + language = LanguageType.makeLowercase(this.language), name = this.name!!, - engName = this.engName!!, description = this.description ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt index ba1f8612..c63b6c3a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt @@ -1,13 +1,12 @@ package com.wafflestudio.csereal.core.about.service import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.about.database.* import com.wafflestudio.csereal.core.about.dto.* -import com.wafflestudio.csereal.core.about.dto.FutureCareersPage -import com.wafflestudio.csereal.core.about.dto.AboutRequest -import com.wafflestudio.csereal.core.about.dto.FutureCareersRequest import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile @@ -20,16 +19,21 @@ interface AboutService { attachments: List? ): AboutDto - fun readAbout(postType: String): AboutDto - fun readAllClubs(): List - fun readAllFacilities(): List - fun readAllDirections(): List + fun readAbout(language: String, postType: String): AboutDto + fun readAllClubs(language: String): List + fun readAllFacilities(language: String): List + fun readAllDirections(language: String): List fun readFutureCareers(): FutureCareersPage fun migrateAbout(requestList: List): List fun migrateFutureCareers(request: FutureCareersRequest): FutureCareersPage fun migrateStudentClubs(requestList: List): List fun migrateFacilities(requestList: List): List fun migrateDirections(requestList: List): List + fun migrateAboutImageAndAttachments( + aboutId: Long, + mainImage: MultipartFile?, + attachments: List? + ): AboutDto } @Service @@ -48,13 +52,8 @@ class AboutServiceImpl( attachments: List? ): AboutDto { val enumPostType = makeStringToEnum(postType) - val newAbout = AboutEntity.of(enumPostType, request) - - if (request.locations != null) { - for (location in request.locations) { - LocationEntity.create(location, newAbout) - } - } + val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) + val newAbout = AboutEntity.of(enumPostType, enumLanguageType, request) if (mainImage != null) { mainImageService.uploadMainImage(newAbout, mainImage) @@ -72,9 +71,10 @@ class AboutServiceImpl( } @Transactional(readOnly = true) - override fun readAbout(postType: String): AboutDto { + override fun readAbout(language: String, postType: String): AboutDto { + val languageType = LanguageType.makeStringToLanguageType(language) val enumPostType = makeStringToEnum(postType) - val about = aboutRepository.findByPostType(enumPostType) + val about = aboutRepository.findByLanguageAndPostType(languageType, enumPostType) val imageURL = mainImageService.createImageURL(about.mainImage) val attachmentResponses = attachmentService.createAttachmentResponses(about.attachments) @@ -82,34 +82,40 @@ class AboutServiceImpl( } @Transactional(readOnly = true) - override fun readAllClubs(): List { - val clubs = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.STUDENT_CLUBS).map { - val imageURL = mainImageService.createImageURL(it.mainImage) - val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) - AboutDto.of(it, imageURL, attachmentResponses) - } + override fun readAllClubs(language: String): List { + val languageType = LanguageType.makeStringToLanguageType(language) + val clubs = + aboutRepository.findAllByLanguageAndPostTypeOrderByName(languageType, AboutPostType.STUDENT_CLUBS).map { + val imageURL = mainImageService.createImageURL(it.mainImage) + val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) + AboutDto.of(it, imageURL, attachmentResponses) + } return clubs } @Transactional(readOnly = true) - override fun readAllFacilities(): List { - val facilities = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.FACILITIES).map { - val imageURL = mainImageService.createImageURL(it.mainImage) - val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) - AboutDto.of(it, imageURL, attachmentResponses) - } + override fun readAllFacilities(language: String): List { + val languageType = LanguageType.makeStringToLanguageType(language) + val facilities = + aboutRepository.findAllByLanguageAndPostTypeOrderByName(languageType, AboutPostType.FACILITIES).map { + val imageURL = mainImageService.createImageURL(it.mainImage) + val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) + AboutDto.of(it, imageURL, attachmentResponses) + } return facilities } @Transactional(readOnly = true) - override fun readAllDirections(): List { - val directions = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.DIRECTIONS).map { - val imageURL = mainImageService.createImageURL(it.mainImage) - val attachments = attachmentService.createAttachmentResponses(it.attachments) - AboutDto.of(it, imageURL, attachments) - } + override fun readAllDirections(language: String): List { + val languageType = LanguageType.makeStringToLanguageType(language) + val directions = + aboutRepository.findAllByLanguageAndPostTypeOrderByName(languageType, AboutPostType.DIRECTIONS).map { + val imageURL = mainImageService.createImageURL(it.mainImage) + val attachments = attachmentService.createAttachmentResponses(it.attachments) + AboutDto.of(it, imageURL, attachments) + } return directions } @@ -153,13 +159,15 @@ class AboutServiceImpl( val list = mutableListOf() for (request in requestList) { + val language = request.language + val description = request.description val enumPostType = makeStringToEnum(request.postType) val aboutDto = AboutDto( id = null, + language = language, name = null, - engName = null, - description = request.description, + description = description, year = null, createdAt = null, modifiedAt = null, @@ -167,7 +175,9 @@ class AboutServiceImpl( imageURL = null, attachments = listOf() ) - val newAbout = AboutEntity.of(enumPostType, aboutDto) + + val languageType = LanguageType.makeStringToLanguageType(language) + val newAbout = AboutEntity.of(enumPostType, languageType, aboutDto) aboutRepository.save(newAbout) @@ -179,13 +189,14 @@ class AboutServiceImpl( @Transactional override fun migrateFutureCareers(request: FutureCareersRequest): FutureCareersPage { val description = request.description + val language = request.language val statList = mutableListOf() val companyList = mutableListOf() val aboutDto = AboutDto( id = null, + language = language, name = null, - engName = null, description = description, year = null, createdAt = null, @@ -194,7 +205,9 @@ class AboutServiceImpl( imageURL = null, attachments = listOf() ) - val newAbout = AboutEntity.of(AboutPostType.FUTURE_CAREERS, aboutDto) + + val languageType = LanguageType.makeStringToLanguageType(language) + val newAbout = AboutEntity.of(AboutPostType.FUTURE_CAREERS, languageType, aboutDto) aboutRepository.save(newAbout) for (stat in request.stat) { @@ -238,10 +251,13 @@ class AboutServiceImpl( val list = mutableListOf() for (request in requestList) { + val language = request.language + val name = request.name + val aboutDto = AboutDto( id = null, - name = request.name, - engName = request.engName, + language = language, + name = name, description = request.description, year = null, createdAt = null, @@ -250,7 +266,8 @@ class AboutServiceImpl( imageURL = null, attachments = listOf() ) - val newAbout = AboutEntity.of(AboutPostType.STUDENT_CLUBS, aboutDto) + val languageType = LanguageType.makeStringToLanguageType(language) + val newAbout = AboutEntity.of(AboutPostType.STUDENT_CLUBS, languageType, aboutDto) aboutRepository.save(newAbout) @@ -260,46 +277,46 @@ class AboutServiceImpl( } @Transactional - override fun migrateFacilities(requestList: List): List { - val list = mutableListOf() - - for (request in requestList) { - val aboutDto = AboutDto( + override fun migrateFacilities(requestList: List): List = + requestList.map { + AboutDto( id = null, - name = request.name, - engName = null, - description = request.description, + language = it.language, + name = it.name, + description = it.description, year = null, createdAt = null, modifiedAt = null, - locations = null, + locations = it.locations, imageURL = null, attachments = listOf() - ) - - val newAbout = AboutEntity.of(AboutPostType.FACILITIES, aboutDto) - - for (location in request.locations) { - LocationEntity.create(location, newAbout) + ).let { dto -> + AboutEntity.of( + AboutPostType.FACILITIES, + LanguageType.makeStringToLanguageType(it.language), + dto + ) } - - aboutRepository.save(newAbout) - - list.add(FacilityDto.of(newAbout)) + }.let { + aboutRepository.saveAll(it) + }.map { + FacilityDto.of(it) } - return list - } @Transactional override fun migrateDirections(requestList: List): List { val list = mutableListOf() for (request in requestList) { + val language = request.language + val name = request.name + val description = request.description + val aboutDto = AboutDto( id = null, - name = request.name, - engName = request.engName, - description = request.description, + language = language, + name = name, + description = description, year = null, createdAt = null, modifiedAt = null, @@ -308,7 +325,8 @@ class AboutServiceImpl( attachments = listOf() ) - val newAbout = AboutEntity.of(AboutPostType.DIRECTIONS, aboutDto) + val languageType = LanguageType.makeStringToLanguageType(language) + val newAbout = AboutEntity.of(AboutPostType.DIRECTIONS, languageType, aboutDto) aboutRepository.save(newAbout) @@ -317,6 +335,25 @@ class AboutServiceImpl( return list } + @Transactional + override fun migrateAboutImageAndAttachments( + aboutId: Long, + mainImage: MultipartFile?, + attachments: List? + ): AboutDto { + val about = aboutRepository.findByIdOrNull(aboutId) + ?: throw CserealException.Csereal404("해당 소개는 존재하지 않습니다.") + + if (mainImage != null) { + mainImageService.uploadMainImage(about, mainImage) + } + + val imageURL = mainImageService.createImageURL(about.mainImage) + val attachmentResponses = attachmentService.createAttachmentResponses(about.attachments) + + return AboutDto.of(about, imageURL, attachmentResponses) + } + private fun makeStringToEnum(postType: String): AboutPostType { try { val upperPostType = postType.replace("-", "_").uppercase() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt index 279a3a82..55df6ce2 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt @@ -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.* @@ -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}") @@ -57,16 +59,18 @@ class AcademicsController( @GetMapping("/{studentType}/courses") fun readAllCourses( + @RequestParam(required = false, defaultValue = "ko") language: String, @PathVariable studentType: String ): ResponseEntity> { - return ResponseEntity.ok(academicsService.readAllCourses(studentType)) + return ResponseEntity.ok(academicsService.readAllCourses(language, studentType)) } @GetMapping("/course") fun readCourse( + @RequestParam(required = false, defaultValue = "ko") language: String, @RequestParam name: String ): ResponseEntity { - return ResponseEntity.ok(academicsService.readCourse(name)) + return ResponseEntity.ok(academicsService.readCourse(language, name)) } @GetMapping("/undergraduate/general-studies-requirements") @@ -95,4 +99,24 @@ class AcademicsController( fun getScholarship(@PathVariable scholarshipId: Long): ResponseEntity { 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 + ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt index 2b90f96d..e5e55b91 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt @@ -2,6 +2,7 @@ package com.wafflestudio.csereal.core.academics.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.common.controller.AttachmentContentEntityType +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.academics.dto.AcademicsDto import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity import jakarta.persistence.* @@ -13,14 +14,19 @@ class AcademicsEntity( @Enumerated(EnumType.STRING) var postType: AcademicsPostType, + @Enumerated(EnumType.STRING) + var language: LanguageType, - 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 = mutableListOf() + var attachments: MutableList = mutableListOf(), + + @OneToOne(mappedBy = "academics", cascade = [CascadeType.ALL], orphanRemoval = true) + var academicsSearch: AcademicsSearchEntity? = null ) : BaseTimeEntity(), AttachmentContentEntityType { override fun bringAttachments() = attachments @@ -29,11 +35,13 @@ class AcademicsEntity( fun of( studentType: AcademicsStudentType, postType: AcademicsPostType, + languageType: LanguageType, academicsDto: AcademicsDto ): AcademicsEntity { return AcademicsEntity( studentType = studentType, postType = postType, + language = languageType, name = academicsDto.name, description = academicsDto.description, year = academicsDto.year, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsSearchEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsSearchEntity.kt new file mode 100644 index 00000000..ce50ce3b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsSearchEntity.kt @@ -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 +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsSearchRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsSearchRepository.kt new file mode 100644 index 00000000..cd16fb10 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsSearchRepository.kt @@ -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, AcademicsSearchCustomRepository + +interface AcademicsSearchCustomRepository { + fun searchAcademics(keyword: String, pageSize: Int, pageNum: Int): Pair, Long> + fun searchTopAcademics(keyword: String, number: Int): List +} + +@Repository +class AcademicsSearchCustomRepositoryImpl( + private val queryFactory: JPAQueryFactory, + private val commonRepository: CommonRepository +) : AcademicsSearchCustomRepository { + override fun searchTopAcademics(keyword: String, number: Int): List { + return searchQuery(keyword) + .limit(number.toLong()) + .fetch() + } + + override fun searchAcademics( + keyword: String, + pageSize: Int, + pageNum: Int + ): Pair, 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 { + 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()!! + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt index 2da6e67a..13e53321 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt @@ -1,5 +1,6 @@ package com.wafflestudio.csereal.core.academics.database -enum class AcademicsStudentType { - UNDERGRADUATE, GRADUATE +enum class AcademicsStudentType(val value: String) { + UNDERGRADUATE("학부"), + GRADUATE("대학원"); } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt index 5f6727ef..792c0507 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt @@ -2,11 +2,10 @@ package com.wafflestudio.csereal.core.academics.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.common.controller.AttachmentContentEntityType +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.academics.dto.CourseDto import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity -import jakarta.persistence.CascadeType -import jakarta.persistence.Entity -import jakarta.persistence.OneToMany +import jakarta.persistence.* @Entity(name = "course") class CourseEntity( @@ -14,27 +13,29 @@ class CourseEntity( var studentType: AcademicsStudentType, - var classification: String, + @Enumerated(EnumType.STRING) + var language: LanguageType, + var classification: String, var code: String, - var name: String, - var credit: Int, - var grade: String, - var description: String?, @OneToMany(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true) - var attachments: MutableList = mutableListOf() + var attachments: MutableList = mutableListOf(), + + @OneToOne(mappedBy = "course", cascade = [CascadeType.ALL], orphanRemoval = true) + var academicsSearch: AcademicsSearchEntity? = null ) : BaseTimeEntity(), AttachmentContentEntityType { override fun bringAttachments() = attachments companion object { - fun of(studentType: AcademicsStudentType, courseDto: CourseDto): CourseEntity { + fun of(studentType: AcademicsStudentType, languageType: LanguageType, courseDto: CourseDto): CourseEntity { return CourseEntity( studentType = studentType, + language = languageType, classification = courseDto.classification, code = courseDto.code, name = courseDto.name.replace(" ", "-"), diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt index 3ed6f69e..c48ec048 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt @@ -1,8 +1,15 @@ package com.wafflestudio.csereal.core.academics.database +import com.wafflestudio.csereal.common.properties.LanguageType import org.springframework.data.jpa.repository.JpaRepository interface CourseRepository : JpaRepository { - fun findAllByStudentTypeOrderByNameAsc(studentType: AcademicsStudentType): List - fun findByName(name: String): CourseEntity + fun findAllByLanguageAndStudentTypeOrderByNameAsc( + languageType: LanguageType, + studentType: AcademicsStudentType + ): List + fun findByLanguageAndName( + languageType: LanguageType, + name: String + ): CourseEntity } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipEntity.kt index 3fc88cea..6aee54fd 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipEntity.kt @@ -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() { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt index e3775e33..1530c621 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt @@ -1,23 +1,26 @@ package com.wafflestudio.csereal.core.academics.dto +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.academics.database.AcademicsEntity 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 language: String, + val name: String, val description: String, - val year: Int?, - val time: String?, - val createdAt: LocalDateTime?, - val modifiedAt: LocalDateTime?, - val attachments: List? + val year: Int? = null, + val time: String? = null, + val createdAt: LocalDateTime? = null, + val modifiedAt: LocalDateTime? = null, + val attachments: List? = null ) { companion object { fun of(entity: AcademicsEntity, attachmentResponses: List): AcademicsDto = entity.run { AcademicsDto( id = this.id, + language = LanguageType.makeLowercase(this.language), name = this.name, description = this.description, year = this.year, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchPageResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchPageResponse.kt new file mode 100644 index 00000000..36b6b79f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchPageResponse.kt @@ -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, + val total: Long +) { + companion object { + fun of( + academics: List, + total: Long + ) = AcademicsSearchPageResponse( + academics = academics.map(AcademicsSearchResponseElement::of), + total = total + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchResponseElement.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchResponseElement.kt new file mode 100644 index 00000000..7854a72c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchResponseElement.kt @@ -0,0 +1,43 @@ +package com.wafflestudio.csereal.core.academics.dto + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.academics.database.AcademicsSearchEntity +import com.wafflestudio.csereal.core.academics.database.AcademicsSearchType + +data class AcademicsSearchResponseElement( + val id: Long, + val name: String, + val academicsType: AcademicsSearchType +) { + companion object { + fun of(academicsSearch: AcademicsSearchEntity): AcademicsSearchResponseElement { + return when { + academicsSearch.academics != null && + academicsSearch.course == null && + academicsSearch.scholarship == null -> + AcademicsSearchResponseElement( + id = academicsSearch.academics!!.id, + name = academicsSearch.academics!!.name, + academicsType = AcademicsSearchType.ACADEMICS + ) + academicsSearch.academics == null && + academicsSearch.course != null && + academicsSearch.scholarship == null -> + AcademicsSearchResponseElement( + id = academicsSearch.course!!.id, + name = academicsSearch.course!!.name, + academicsType = AcademicsSearchType.COURSE + ) + academicsSearch.academics == null && + academicsSearch.course == null && + academicsSearch.scholarship != null -> + AcademicsSearchResponseElement( + id = academicsSearch.scholarship!!.id, + name = academicsSearch.scholarship!!.name, + academicsType = AcademicsSearchType.SCHOLARSHIP + ) + else -> throw CserealException.Csereal401("AcademicsSearchEntity의 연결이 올바르지 않습니다.") + } + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchTopResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchTopResponse.kt new file mode 100644 index 00000000..232b2fe2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsSearchTopResponse.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.academics.dto + +import com.wafflestudio.csereal.core.academics.database.AcademicsSearchEntity + +data class AcademicsSearchTopResponse( + val topAcademics: List +) { + companion object { + fun of( + topAcademics: List + ) = AcademicsSearchTopResponse( + topAcademics = topAcademics.map(AcademicsSearchResponseElement::of) + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt index dedc13f6..e1f680fc 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt @@ -1,10 +1,12 @@ package com.wafflestudio.csereal.core.academics.dto +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.academics.database.CourseEntity import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentResponse data class CourseDto( val id: Long, + val language: String, val classification: String, val code: String, val name: String, @@ -17,6 +19,7 @@ data class CourseDto( fun of(entity: CourseEntity, attachmentResponses: List): CourseDto = entity.run { CourseDto( id = this.id, + language = LanguageType.makeLowercase(this.language), classification = this.classification, code = this.code, name = this.name, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt new file mode 100644 index 00000000..eb2738b2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt @@ -0,0 +1,39 @@ +package com.wafflestudio.csereal.core.academics.service + +import com.wafflestudio.csereal.core.academics.database.AcademicsSearchRepository +import com.wafflestudio.csereal.core.academics.dto.AcademicsSearchPageResponse +import com.wafflestudio.csereal.core.academics.dto.AcademicsSearchTopResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface AcademicsSearchService { + fun searchAcademics(keyword: String, pageSize: Int, pageNum: Int): AcademicsSearchPageResponse + fun searchTopAcademics(keyword: String, number: Int): AcademicsSearchTopResponse +} + +@Service +class AcademicsSearchServiceImpl( + private val academicsSearchRepository: AcademicsSearchRepository +) : AcademicsSearchService { + @Transactional(readOnly = true) + override fun searchTopAcademics(keyword: String, number: Int) = + AcademicsSearchTopResponse.of( + academicsSearchRepository.searchTopAcademics( + keyword = keyword, + number = number + ) + ) + + @Transactional(readOnly = true) + override fun searchAcademics(keyword: String, pageSize: Int, pageNum: Int) = + academicsSearchRepository.searchAcademics( + keyword = keyword, + pageSize = pageSize, + pageNum = pageNum + ).let { + AcademicsSearchPageResponse.of( + academics = it.first, + total = it.second + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt index 2773b6d5..176d5b8b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.academics.service import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.academics.database.* import com.wafflestudio.csereal.core.academics.dto.* import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService @@ -23,13 +24,17 @@ interface AcademicsService { fun readAcademicsYearResponses(studentType: String, postType: String): List fun readGeneralStudies(): GeneralStudiesPageResponse fun createCourse(studentType: String, request: CourseDto, attachments: List?): CourseDto - fun readAllCourses(studentType: String): List - fun readCourse(name: String): CourseDto + fun readAllCourses(language: String, studentType: String): List + fun readCourse(language: String, name: String): CourseDto fun createScholarshipDetail(studentType: String, request: ScholarshipDto): ScholarshipDto fun readAllScholarship(studentType: String): ScholarshipPageResponse fun readScholarship(scholarshipId: Long): ScholarshipDto } +// TODO: add Update, Delete method +// remember to update academicsSearch Field on Update method +// remember to mark delete of academicsSearch Field on Delete mark method + @Service class AcademicsServiceImpl( private val academicsRepository: AcademicsRepository, @@ -46,13 +51,18 @@ class AcademicsServiceImpl( ): AcademicsDto { val enumStudentType = makeStringToAcademicsStudentType(studentType) val enumPostType = makeStringToAcademicsPostType(postType) - - val newAcademics = AcademicsEntity.of(enumStudentType, enumPostType, request) + val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) + val newAcademics = AcademicsEntity.of(enumStudentType, enumPostType, enumLanguageType, request) if (attachments != null) { attachmentService.uploadAllAttachments(newAcademics, attachments) } + // create search data + newAcademics.apply { + academicsSearch = AcademicsSearchEntity.create(this) + } + academicsRepository.save(newAcademics) val attachmentResponses = attachmentService.createAttachmentResponses(newAcademics.attachments) @@ -87,6 +97,7 @@ class AcademicsServiceImpl( return academicsYearResponses } + @Transactional(readOnly = true) override fun readGeneralStudies(): GeneralStudiesPageResponse { val academicsEntity = academicsRepository.findByStudentTypeAndPostType( AcademicsStudentType.UNDERGRADUATE, @@ -103,12 +114,18 @@ class AcademicsServiceImpl( @Transactional override fun createCourse(studentType: String, request: CourseDto, attachments: List?): CourseDto { val enumStudentType = makeStringToAcademicsStudentType(studentType) - val newCourse = CourseEntity.of(enumStudentType, request) + val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) + + val newCourse = CourseEntity.of(enumStudentType, enumLanguageType, request) if (attachments != null) { attachmentService.uploadAllAttachments(newCourse, attachments) } + // create search data + newCourse.apply { + academicsSearch = AcademicsSearchEntity.create(this) + } courseRepository.save(newCourse) val attachmentResponses = attachmentService.createAttachmentResponses(newCourse.attachments) @@ -117,19 +134,21 @@ class AcademicsServiceImpl( } @Transactional(readOnly = true) - override fun readAllCourses(studentType: String): List { + override fun readAllCourses(language: String, studentType: String): List { val enumStudentType = makeStringToAcademicsStudentType(studentType) - - val courseDtoList = courseRepository.findAllByStudentTypeOrderByNameAsc(enumStudentType).map { - val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) - CourseDto.of(it, attachmentResponses) - } + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + val courseDtoList = + courseRepository.findAllByLanguageAndStudentTypeOrderByNameAsc(enumLanguageType, enumStudentType).map { + val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) + CourseDto.of(it, attachmentResponses) + } return courseDtoList } @Transactional(readOnly = true) - override fun readCourse(name: String): CourseDto { - val course = courseRepository.findByName(name) + override fun readCourse(language: String, name: String): CourseDto { + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + val course = courseRepository.findByLanguageAndName(enumLanguageType, name) val attachmentResponses = attachmentService.createAttachmentResponses(course.attachments) return CourseDto.of(course, attachmentResponses) @@ -138,9 +157,14 @@ class AcademicsServiceImpl( @Transactional override fun createScholarshipDetail(studentType: String, request: ScholarshipDto): ScholarshipDto { val enumStudentType = makeStringToAcademicsStudentType(studentType) - val newScholarship = ScholarshipEntity.of(enumStudentType, request) + var newScholarship = ScholarshipEntity.of(enumStudentType, request) + + // create search data + newScholarship.apply { + academicsSearch = AcademicsSearchEntity.create(this) + } - scholarshipRepository.save(newScholarship) + newScholarship = scholarshipRepository.save(newScholarship) return ScholarshipDto.of(newScholarship) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt index 5b716ef9..b25edb3c 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt @@ -1,17 +1,15 @@ package com.wafflestudio.csereal.core.admissions.api import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.core.admissions.api.req.AdmissionReqBody import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto -import com.wafflestudio.csereal.core.admissions.dto.AdmissionsRequest +import com.wafflestudio.csereal.core.admissions.api.req.AdmissionMigrateElem import com.wafflestudio.csereal.core.admissions.service.AdmissionsService +import com.wafflestudio.csereal.core.admissions.type.AdmissionsMainType +import com.wafflestudio.csereal.core.admissions.type.AdmissionsPostType import jakarta.validation.Valid -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.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RequestMapping("/api/v1/admissions") @RestController @@ -19,40 +17,43 @@ class AdmissionsController( private val admissionsService: AdmissionsService ) { @AuthenticatedStaff - @PostMapping("/undergraduate/{postType}") - fun createUndergraduateAdmissions( - @PathVariable postType: String, + @PostMapping("/{mainTypeStr}/{postTypeStr}") + fun createAdmission( + @PathVariable(required = true) mainTypeStr: String, + @PathVariable(required = true) postTypeStr: String, @Valid @RequestBody - request: AdmissionsDto + req: AdmissionReqBody ): AdmissionsDto { - return admissionsService.createUndergraduateAdmissions(postType, request) + val mainType = AdmissionsMainType.fromJsonValue(mainTypeStr) + val postType = AdmissionsPostType.fromJsonValue(postTypeStr) + return admissionsService.createAdmission(req, mainType, postType) } - @AuthenticatedStaff - @PostMapping("/graduate") - fun createGraduateAdmissions( - @Valid @RequestBody - request: AdmissionsDto + @GetMapping("/{mainTypeStr}/{postTypeStr}") + fun readAdmission( + @PathVariable(required = true) mainTypeStr: String, + @PathVariable(required = true) postTypeStr: String, + @RequestParam(required = true, defaultValue = "ko") language: String ): AdmissionsDto { - return admissionsService.createGraduateAdmissions(request) - } - - @GetMapping("/undergraduate/{postType}") - fun readUndergraduateAdmissions( - @PathVariable postType: String - ): ResponseEntity { - return ResponseEntity.ok(admissionsService.readUndergraduateAdmissions(postType)) + val mainType = AdmissionsMainType.fromJsonValue(mainTypeStr) + val postType = AdmissionsPostType.fromJsonValue(postTypeStr) + val languageType = LanguageType.makeStringToLanguageType(language) + return admissionsService.readAdmission(mainType, postType, languageType) } - @GetMapping("/graduate") - fun readGraduateAdmissions(): ResponseEntity { - return ResponseEntity.ok(admissionsService.readGraduateAdmissions()) - } + @GetMapping("/search") + fun searchTopAdmissions( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true, defaultValue = "ko") language: String, + @RequestParam(required = true) number: Int + ) = admissionsService.searchTopAdmission( + keyword, + LanguageType.makeStringToLanguageType(language), + number + ) @PostMapping("/migrate") fun migrateAdmissions( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(admissionsService.migrateAdmissions(requestList)) - } + @RequestBody reqList: List<@Valid AdmissionMigrateElem> + ): List = admissionsService.migrateAdmissions(reqList) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/req/AdmissionMigrateElem.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/req/AdmissionMigrateElem.kt new file mode 100644 index 00000000..6589041c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/req/AdmissionMigrateElem.kt @@ -0,0 +1,11 @@ +package com.wafflestudio.csereal.core.admissions.api.req + +import org.jetbrains.annotations.NotNull + +data class AdmissionMigrateElem( + @field:NotNull val name: String?, + val mainType: String, + val postType: String, + val language: String, + @field:NotNull val description: String? +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/req/AdmissionReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/req/AdmissionReqBody.kt new file mode 100644 index 00000000..5489f175 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/req/AdmissionReqBody.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.csereal.core.admissions.api.req + +import org.jetbrains.annotations.NotNull + +data class AdmissionReqBody( + @field:NotNull val name: String?, + val language: String = "ko", + @field:NotNull val description: String? +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/res/AdmissionSearchResElem.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/res/AdmissionSearchResElem.kt new file mode 100644 index 00000000..a1e7d015 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/res/AdmissionSearchResElem.kt @@ -0,0 +1,28 @@ +package com.wafflestudio.csereal.core.admissions.api.res + +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity + +data class AdmissionSearchResBody( + val admissions: List +) + +data class AdmissionSearchResElem( + val id: Long, + val name: String, + val mainType: String, + val postType: String, + val language: String +) { + companion object { + fun of( + admissions: AdmissionsEntity + ) = AdmissionSearchResElem( + id = admissions.id, + name = admissions.name, + mainType = admissions.mainType.toJsonValue(), + postType = admissions.postType.toJsonValue(), + language = LanguageType.makeLowercase(admissions.language) + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt index a2f50be2..5f37583e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt @@ -1,28 +1,94 @@ package com.wafflestudio.csereal.core.admissions.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.common.utils.cleanTextFromHtml +import com.wafflestudio.csereal.core.admissions.api.req.AdmissionReqBody import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto +import com.wafflestudio.csereal.core.admissions.type.AdmissionsMainType +import com.wafflestudio.csereal.core.admissions.type.AdmissionsPostType import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EnumType import jakarta.persistence.Enumerated +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint @Entity(name = "admissions") +@Table( + uniqueConstraints = [ + UniqueConstraint(columnNames = ["language", "mainType", "postType"]) + ] +) class AdmissionsEntity( + var name: String, + + @Enumerated(EnumType.STRING) + val language: LanguageType, + + @Enumerated(EnumType.STRING) + val mainType: AdmissionsMainType, + @Enumerated(EnumType.STRING) val postType: AdmissionsPostType, - val pageName: String, @Column(columnDefinition = "mediumText") - val description: String + val description: String, + + @Column(nullable = false, columnDefinition = "mediumText") + var searchContent: String ) : BaseTimeEntity() { companion object { - fun of(postType: AdmissionsPostType, pageName: String, admissionsDto: AdmissionsDto): AdmissionsEntity { - return AdmissionsEntity( + fun of( + mainType: AdmissionsMainType, + postType: AdmissionsPostType, + name: String, + admissionsDto: AdmissionsDto + ) = AdmissionsEntity( + mainType = mainType, + postType = postType, + name = name, + description = admissionsDto.description, + language = LanguageType.makeStringToLanguageType(admissionsDto.language), + searchContent = createSearchContent( + name = name, + mainType = mainType, postType = postType, - pageName = pageName, + language = LanguageType.makeStringToLanguageType(admissionsDto.language), description = admissionsDto.description ) - } + ) + + fun of( + mainType: AdmissionsMainType, + postType: AdmissionsPostType, + req: AdmissionReqBody + ) = AdmissionsEntity( + mainType = mainType, + postType = postType, + name = req.name!!, + description = req.description!!, + language = LanguageType.makeStringToLanguageType(req.language), + searchContent = createSearchContent( + name = req.name, + mainType = mainType, + postType = postType, + language = LanguageType.makeStringToLanguageType(req.language), + description = req.description + ) + ) + + fun createSearchContent( + name: String, + mainType: AdmissionsMainType, + postType: AdmissionsPostType, + language: LanguageType, + description: String + ) = StringBuilder().apply { + appendLine(name) + appendLine(mainType.getLanguageValue(language)) + appendLine(postType.getLanguageValue(language)) + appendLine(cleanTextFromHtml(description)) + }.toString() } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt deleted file mode 100644 index be0adf3f..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wafflestudio.csereal.core.admissions.database - -enum class AdmissionsPostType { - GRADUATE, UNDERGRADUATE_EARLY_ADMISSION, UNDERGRADUATE_REGULAR_ADMISSION, -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt index 02c2c5c1..34979cb0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt @@ -1,7 +1,48 @@ package com.wafflestudio.csereal.core.admissions.database +import com.querydsl.jpa.impl.JPAQueryFactory +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.common.repository.CommonRepository +import com.wafflestudio.csereal.core.admissions.database.QAdmissionsEntity.admissionsEntity +import com.wafflestudio.csereal.core.admissions.type.AdmissionsMainType +import com.wafflestudio.csereal.core.admissions.type.AdmissionsPostType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository -interface AdmissionsRepository : JpaRepository { - fun findByPostType(postType: AdmissionsPostType): AdmissionsEntity +interface AdmissionsRepository : JpaRepository, AdmissionsCustomRepository { + fun findByMainTypeAndPostTypeAndLanguage( + mainType: AdmissionsMainType, + postType: AdmissionsPostType, + language: LanguageType + ): AdmissionsEntity? +} + +interface AdmissionsCustomRepository { + fun searchTopAdmissions(keyword: String, language: LanguageType, number: Int): List +} + +@Repository +class AdmissionsCustomRepositoryImpl( + private val commonRepository: CommonRepository, + private val queryFactory: JPAQueryFactory +) : AdmissionsCustomRepository { + override fun searchTopAdmissions( + keyword: String, + language: LanguageType, + number: Int + ): List = + searchQueryOfLanguage(keyword, language) + .limit(number.toLong()) + .fetch() + + fun searchQueryOfLanguage(keyword: String, language: LanguageType) = + queryFactory.selectFrom( + admissionsEntity + ).where( + commonRepository.searchFullSingleTextTemplate( + keyword, + admissionsEntity.searchContent + ).gt(0.0), + admissionsEntity.language.eq(language) + ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt index 890f4825..80c28352 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt @@ -1,23 +1,30 @@ package com.wafflestudio.csereal.core.admissions.dto -import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity import java.time.LocalDateTime data class AdmissionsDto( - @JsonInclude(JsonInclude.Include.NON_NULL) - val id: Long? = null, + val id: Long, + val name: String, + val mainType: String, + val postType: String, + val language: String, val description: String, - val createdAt: LocalDateTime?, - val modifiedAt: LocalDateTime? + val createdAt: LocalDateTime, + val modifiedAt: LocalDateTime ) { companion object { fun of(entity: AdmissionsEntity): AdmissionsDto = entity.run { AdmissionsDto( id = this.id, + name = this.name, + mainType = this.mainType.toJsonValue(), + postType = this.postType.toJsonValue(), + language = LanguageType.makeLowercase(this.language), description = this.description, - createdAt = this.createdAt, - modifiedAt = this.modifiedAt + createdAt = this.createdAt!!, + modifiedAt = this.modifiedAt!! ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsRequest.kt deleted file mode 100644 index 4fd1e458..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.wafflestudio.csereal.core.admissions.dto - -data class AdmissionsRequest( - val postType: String, - val description: String -) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt index 4f943785..a31e65d3 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt @@ -1,20 +1,35 @@ package com.wafflestudio.csereal.core.admissions.service import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.core.admissions.api.req.AdmissionMigrateElem +import com.wafflestudio.csereal.core.admissions.api.req.AdmissionReqBody +import com.wafflestudio.csereal.core.admissions.api.res.AdmissionSearchResElem import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity -import com.wafflestudio.csereal.core.admissions.database.AdmissionsPostType import com.wafflestudio.csereal.core.admissions.database.AdmissionsRepository import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto -import com.wafflestudio.csereal.core.admissions.dto.AdmissionsRequest +import com.wafflestudio.csereal.core.admissions.type.AdmissionsMainType +import com.wafflestudio.csereal.core.admissions.type.AdmissionsPostType import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface AdmissionsService { - fun createUndergraduateAdmissions(postType: String, request: AdmissionsDto): AdmissionsDto - fun createGraduateAdmissions(request: AdmissionsDto): AdmissionsDto - fun readUndergraduateAdmissions(postType: String): AdmissionsDto - fun readGraduateAdmissions(): AdmissionsDto - fun migrateAdmissions(requestList: List): List + fun createAdmission( + req: AdmissionReqBody, + mainType: AdmissionsMainType, + postType: AdmissionsPostType + ): AdmissionsDto + + fun readAdmission( + mainType: AdmissionsMainType, + postType: AdmissionsPostType, + language: LanguageType + ): AdmissionsDto + + fun migrateAdmissions(requestList: List): List + + @Transactional(readOnly = true) + fun searchTopAdmission(keyword: String, language: LanguageType, number: Int): List } @Service @@ -22,87 +37,56 @@ class AdmissionsServiceImpl( private val admissionsRepository: AdmissionsRepository ) : AdmissionsService { @Transactional - override fun createUndergraduateAdmissions(postType: String, request: AdmissionsDto): AdmissionsDto { - val enumPostType = makeStringToAdmissionsPostType(postType) - - val pageName = when (enumPostType) { - AdmissionsPostType.UNDERGRADUATE_EARLY_ADMISSION -> "수시 모집" - AdmissionsPostType.UNDERGRADUATE_REGULAR_ADMISSION -> "정시 모집" - else -> throw CserealException.Csereal404("해당하는 페이지를 찾을 수 없습니다.") - } - - val newAdmissions = AdmissionsEntity.of(enumPostType, pageName, request) - - admissionsRepository.save(newAdmissions) - - return AdmissionsDto.of(newAdmissions) - } - - @Transactional - override fun createGraduateAdmissions(request: AdmissionsDto): AdmissionsDto { - val newAdmissions: AdmissionsEntity = AdmissionsEntity.of(AdmissionsPostType.GRADUATE, "전기/후기 모집", request) - - admissionsRepository.save(newAdmissions) - - return AdmissionsDto.of(newAdmissions) + override fun createAdmission( + req: AdmissionReqBody, + mainType: AdmissionsMainType, + postType: AdmissionsPostType + ) = admissionsRepository.save( + AdmissionsEntity.of(mainType, postType, req) + ).let { + AdmissionsDto.of(it) } @Transactional(readOnly = true) - override fun readUndergraduateAdmissions(postType: String): AdmissionsDto { - return when (postType) { - "early" -> AdmissionsDto.of( - admissionsRepository.findByPostType(AdmissionsPostType.UNDERGRADUATE_EARLY_ADMISSION) - ) - - "regular" -> AdmissionsDto.of( - admissionsRepository.findByPostType(AdmissionsPostType.UNDERGRADUATE_REGULAR_ADMISSION) - ) - - else -> throw CserealException.Csereal404("해당하는 페이지를 찾을 수 없습니다.") - } - } + override fun readAdmission( + mainType: AdmissionsMainType, + postType: AdmissionsPostType, + language: LanguageType + ) = admissionsRepository.findByMainTypeAndPostTypeAndLanguage( + mainType, + postType, + language + )?.let { AdmissionsDto.of(it) } + ?: throw CserealException.Csereal404("해당하는 페이지를 찾을 수 없습니다.") @Transactional(readOnly = true) - override fun readGraduateAdmissions(): AdmissionsDto { - return AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionsPostType.GRADUATE)) - } + override fun searchTopAdmission(keyword: String, language: LanguageType, number: Int) = + admissionsRepository.searchTopAdmissions(keyword, language, number).map { + AdmissionSearchResElem.of(it) + } @Transactional - override fun migrateAdmissions(requestList: List): List { - val list = mutableListOf() - - for (request in requestList) { - val enumPostType = makeStringToAdmissionsPostType(request.postType) - - val pageName = when (enumPostType) { - AdmissionsPostType.UNDERGRADUATE_EARLY_ADMISSION -> "수시 모집" - AdmissionsPostType.UNDERGRADUATE_REGULAR_ADMISSION -> "정시 모집" - AdmissionsPostType.GRADUATE -> "대학원" - else -> throw CserealException.Csereal404("해당하는 페이지를 찾을 수 없습니다.") - } - - val admissionsDto = AdmissionsDto( - id = null, - description = request.description, - createdAt = null, - modifiedAt = null + override fun migrateAdmissions(requestList: List) = requestList.map { + val mainType = AdmissionsMainType.fromJsonValue(it.mainType) + val postType = AdmissionsPostType.fromJsonValue(it.postType) + val language = LanguageType.makeStringToLanguageType(it.language) + AdmissionsEntity( + name = it.name!!, + mainType = mainType, + postType = postType, + language = language, + description = it.description!!, + searchContent = AdmissionsEntity.createSearchContent( + name = it.name, + mainType = mainType, + postType = postType, + language = language, + description = it.description ) - - val newAdmissions = AdmissionsEntity.of(enumPostType, pageName, admissionsDto) - - admissionsRepository.save(newAdmissions) - - list.add(AdmissionsDto.of(newAdmissions)) - } - return list - } - - private fun makeStringToAdmissionsPostType(postType: String): AdmissionsPostType { - try { - val upperPostType = postType.replace("-", "_").uppercase() - return AdmissionsPostType.valueOf(upperPostType) - } catch (e: IllegalArgumentException) { - throw CserealException.Csereal400("해당하는 enum을 찾을 수 없습니다") - } + ) + }.let { + admissionsRepository.saveAll(it) + }.map { + AdmissionsDto.of(it) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/type/AdmissionsMainType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/type/AdmissionsMainType.kt new file mode 100644 index 00000000..6e3776c9 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/type/AdmissionsMainType.kt @@ -0,0 +1,30 @@ +package com.wafflestudio.csereal.core.admissions.type + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType + +enum class AdmissionsMainType( + val ko: String, + val en: String +) { + UNDERGRADUATE("학부", "Undergraduate"), + GRADUATE("대학원", "Graduate"), + INTERNATIONAL("International", "International"); + + fun getLanguageValue(language: LanguageType) = when (language) { + LanguageType.KO -> this.ko + LanguageType.EN -> this.en + } + + fun toJsonValue() = this.name.lowercase() + + companion object { + fun fromJsonValue(field: String) = try { + field + .uppercase() + .let { AdmissionsMainType.valueOf(it) } + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("존재하지 않는 Admission Main Type입니다.") + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/type/AdmissionsPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/type/AdmissionsPostType.kt new file mode 100644 index 00000000..571ff67b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/type/AdmissionsPostType.kt @@ -0,0 +1,37 @@ +package com.wafflestudio.csereal.core.admissions.type + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType + +enum class AdmissionsPostType( + val ko: String, + val en: String +) { + // For graduate, undergraduate + EARLY_ADMISSION("수시 모집", "Early Admission"), + REGULAR_ADMISSION("정시 모집", "Regular Admission"), + + // For international + UNDERGRADUATE("Undergraduate", "Undergraduate"), + GRADUATE("Graduate", "Graduate"), + EXCHANGE_VISITING("Exchange/Visiting Program", "Exchange/Visiting Program"), + SCHOLARSHIPS("Scholarships", "Scholarships") ; + + fun getLanguageValue(language: LanguageType) = when (language) { + LanguageType.KO -> this.ko + LanguageType.EN -> this.en + } + + fun toJsonValue() = this.name.lowercase() + + companion object { + fun fromJsonValue(field: String) = + try { + field.replace('-', '_') + .uppercase() + .let { AdmissionsPostType.valueOf(it) } + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("잘못된 Admission Post Type이 주어졌습니다.") + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt index 548f39f9..d4e0f771 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt @@ -62,4 +62,12 @@ class ProfessorController( ): ResponseEntity> { return ResponseEntity.ok(professorService.migrateProfessors(requestList)) } + + @PatchMapping("/migragteImage/{professorId}") + fun migrateProfessorImage( + @PathVariable professorId: Long, + @RequestPart("mainImage") mainImage: MultipartFile + ): ResponseEntity { + return ResponseEntity.ok(professorService.migrateProfessorImage(professorId, mainImage)) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt index eed5164c..6f298f94 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt @@ -29,8 +29,10 @@ class StaffController( } @GetMapping - fun getAllStaff(): ResponseEntity> { - return ResponseEntity.ok(staffService.getAllStaff()) + fun getAllStaff( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity> { + return ResponseEntity.ok(staffService.getAllStaff(language)) } @AuthenticatedStaff @@ -55,4 +57,12 @@ class StaffController( ): ResponseEntity> { return ResponseEntity.ok(staffService.migrateStaff(requestList)) } + + @PatchMapping("/migrateImage/{staffId}") + fun migrateStaffImage( + @PathVariable staffId: Long, + @RequestPart("mainImage") mainImage: MultipartFile + ): ResponseEntity { + return ResponseEntity.ok(staffService.migrateStaffImage(staffId, mainImage)) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt index c9b704f6..60fc24b6 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt @@ -2,15 +2,16 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.common.controller.MainImageContentEntityType +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.member.dto.StaffDto import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity -import jakarta.persistence.CascadeType -import jakarta.persistence.Entity -import jakarta.persistence.OneToMany -import jakarta.persistence.OneToOne +import jakarta.persistence.* @Entity(name = "staff") class StaffEntity( + @Enumerated(EnumType.STRING) + var language: LanguageType, + var name: String, var role: String, @@ -30,8 +31,9 @@ class StaffEntity( override fun bringMainImage(): MainImageEntity? = mainImage companion object { - fun of(staffDto: StaffDto): StaffEntity { + fun of(languageType: LanguageType, staffDto: StaffDto): StaffEntity { return StaffEntity( + language = languageType, name = staffDto.name, role = staffDto.role, office = staffDto.office, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt index a293b642..6ce765aa 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt @@ -1,5 +1,8 @@ package com.wafflestudio.csereal.core.member.database +import com.wafflestudio.csereal.common.properties.LanguageType import org.springframework.data.jpa.repository.JpaRepository -interface StaffRepository : JpaRepository +interface StaffRepository : JpaRepository { + fun findAllByLanguage(languageType: LanguageType): List +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt index bf75f902..1c16482a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt @@ -1,11 +1,13 @@ package com.wafflestudio.csereal.core.member.dto import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.member.database.StaffEntity data class StaffDto( @JsonInclude(JsonInclude.Include.NON_NULL) var id: Long? = null, + val language: String, val name: String, val role: String, val office: String, @@ -19,6 +21,7 @@ data class StaffDto( fun of(staffEntity: StaffEntity, imageURL: String?): StaffDto { return StaffDto( id = staffEntity.id, + language = LanguageType.makeLowercase(staffEntity.language), name = staffEntity.name, role = staffEntity.role, office = staffEntity.office, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorCreatedEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorCreatedEvent.kt new file mode 100644 index 00000000..6d45931c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorCreatedEvent.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.member.event + +import com.wafflestudio.csereal.core.member.database.ProfessorEntity + +data class ProfessorCreatedEvent( + val id: Long, + val labId: Long? +) { + companion object { + fun of(professor: ProfessorEntity) = ProfessorCreatedEvent( + id = professor.id, + labId = professor.lab?.id + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorDeletedEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorDeletedEvent.kt new file mode 100644 index 00000000..3f249ba4 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorDeletedEvent.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.member.event + +import com.wafflestudio.csereal.core.member.database.ProfessorEntity + +data class ProfessorDeletedEvent( + val id: Long, + val labId: Long? +) { + companion object { + fun of(professor: ProfessorEntity) = ProfessorDeletedEvent( + id = professor.id, + labId = professor.lab?.id + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorModifiedEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorModifiedEvent.kt new file mode 100644 index 00000000..78b6e310 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/event/ProfessorModifiedEvent.kt @@ -0,0 +1,17 @@ +package com.wafflestudio.csereal.core.member.event + +import com.wafflestudio.csereal.core.member.database.ProfessorEntity + +data class ProfessorModifiedEvent( + val id: Long, + val beforeLabId: Long?, + val afterLabId: Long? +) { + companion object { + fun of(updatedProfessor: ProfessorEntity, beforeLabId: Long?) = ProfessorModifiedEvent( + id = updatedProfessor.id, + beforeLabId, + afterLabId = updatedProfessor.lab?.id + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt index 97e69ee2..ebc22b35 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt @@ -5,8 +5,12 @@ import com.wafflestudio.csereal.core.member.database.* import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.member.dto.ProfessorPageDto import com.wafflestudio.csereal.core.member.dto.SimpleProfessorDto +import com.wafflestudio.csereal.core.member.event.ProfessorCreatedEvent +import com.wafflestudio.csereal.core.member.event.ProfessorDeletedEvent +import com.wafflestudio.csereal.core.member.event.ProfessorModifiedEvent import com.wafflestudio.csereal.core.research.database.LabRepository import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -24,6 +28,7 @@ interface ProfessorService { ): ProfessorDto fun deleteProfessor(professorId: Long) fun migrateProfessors(requestList: List): List + fun migrateProfessorImage(professorId: Long, mainImage: MultipartFile): ProfessorDto } @Service @@ -31,7 +36,8 @@ interface ProfessorService { class ProfessorServiceImpl( private val labRepository: LabRepository, private val professorRepository: ProfessorRepository, - private val mainImageService: MainImageService + private val mainImageService: MainImageService, + private val applicationEventPublisher: ApplicationEventPublisher ) : ProfessorService { override fun createProfessor(createProfessorRequest: ProfessorDto, mainImage: MultipartFile?): ProfessorDto { val professor = ProfessorEntity.of(createProfessorRequest) @@ -63,6 +69,10 @@ class ProfessorServiceImpl( val imageURL = mainImageService.createImageURL(professor.mainImage) + applicationEventPublisher.publishEvent( + ProfessorCreatedEvent.of(professor) + ) + return ProfessorDto.of(professor, imageURL) } @@ -109,6 +119,8 @@ class ProfessorServiceImpl( val professor = professorRepository.findByIdOrNull(professorId) ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: $professorId") + val outdatedLabId = professor.lab?.id + if (updateProfessorRequest.labId != null && updateProfessorRequest.labId != professor.lab?.id) { val lab = labRepository.findByIdOrNull(updateProfessorRequest.labId) ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${updateProfessorRequest.labId}") @@ -162,13 +174,24 @@ class ProfessorServiceImpl( // 검색 엔티티 업데이트 professor.memberSearch!!.update(professor) + // update event 생성 + applicationEventPublisher.publishEvent( + ProfessorModifiedEvent.of(professor, outdatedLabId) + ) + val imageURL = mainImageService.createImageURL(professor.mainImage) return ProfessorDto.of(professor, imageURL) } override fun deleteProfessor(professorId: Long) { + val professorEntity = professorRepository.findByIdOrNull(professorId) ?: return + professorRepository.deleteById(professorId) + + applicationEventPublisher.publishEvent( + ProfessorDeletedEvent.of(professorEntity) + ) } @Transactional @@ -203,4 +226,16 @@ class ProfessorServiceImpl( } return list } + + @Transactional + override fun migrateProfessorImage(professorId: Long, mainImage: MultipartFile): ProfessorDto { + val professor = professorRepository.findByIdOrNull(professorId) + ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: $professorId") + + mainImageService.uploadMainImage(professor, mainImage) + + val imageURL = mainImageService.createImageURL(professor.mainImage) + + return ProfessorDto.of(professor, imageURL) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt index 7ee17d23..32d5f9a4 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType import com.wafflestudio.csereal.core.member.database.MemberSearchEntity import com.wafflestudio.csereal.core.member.database.StaffEntity import com.wafflestudio.csereal.core.member.database.StaffRepository @@ -16,10 +17,11 @@ import org.springframework.web.multipart.MultipartFile interface StaffService { fun createStaff(createStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto fun getStaff(staffId: Long): StaffDto - fun getAllStaff(): List + fun getAllStaff(language: String): List fun updateStaff(staffId: Long, updateStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto fun deleteStaff(staffId: Long) fun migrateStaff(requestList: List): List + fun migrateStaffImage(staffId: Long, mainImage: MultipartFile): StaffDto } @Service @@ -29,7 +31,8 @@ class StaffServiceImpl( private val mainImageService: MainImageService ) : StaffService { override fun createStaff(createStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto { - val staff = StaffEntity.of(createStaffRequest) + val enumLanguageType = LanguageType.makeStringToLanguageType(createStaffRequest.language) + val staff = StaffEntity.of(enumLanguageType, createStaffRequest) for (task in createStaffRequest.tasks) { TaskEntity.create(task, staff) @@ -59,8 +62,10 @@ class StaffServiceImpl( } @Transactional(readOnly = true) - override fun getAllStaff(): List { - return staffRepository.findAll().map { + override fun getAllStaff(language: String): List { + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + + return staffRepository.findAllByLanguage(enumLanguageType).map { val imageURL = mainImageService.createImageURL(it.mainImage) SimpleStaffDto.of(it, imageURL) }.sortedBy { it.name } @@ -107,12 +112,15 @@ class StaffServiceImpl( val list = mutableListOf() for (request in requestList) { - val staff = StaffEntity.of(request) + val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) + val staff = StaffEntity.of(enumLanguageType, request) for (task in request.tasks) { TaskEntity.create(task, staff) } + staff.memberSearch = MemberSearchEntity.create(staff) + staffRepository.save(staff) list.add(StaffDto.of(staff, null)) @@ -120,4 +128,16 @@ class StaffServiceImpl( return list } + + @Transactional + override fun migrateStaffImage(staffId: Long, mainImage: MultipartFile): StaffDto { + val staff = staffRepository.findByIdOrNull(staffId) + ?: throw CserealException.Csereal404("해당 행정직원을 찾을 수 없습니다. staffId: $staffId") + + mainImageService.uploadMainImage(staff, mainImage) + + val imageURL = mainImageService.createImageURL(staff.mainImage) + + return StaffDto.of(staff, imageURL) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt index 55832b73..7ee21f75 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt @@ -1,10 +1,8 @@ package com.wafflestudio.csereal.core.research.api import com.wafflestudio.csereal.common.aop.AuthenticatedStaff -import com.wafflestudio.csereal.core.research.dto.LabDto -import com.wafflestudio.csereal.core.research.dto.LabUpdateRequest -import com.wafflestudio.csereal.core.research.dto.ResearchDto -import com.wafflestudio.csereal.core.research.dto.ResearchGroupResponse +import com.wafflestudio.csereal.core.research.dto.* +import com.wafflestudio.csereal.core.research.service.ResearchSearchService import com.wafflestudio.csereal.core.research.service.ResearchService import jakarta.validation.Valid import org.springframework.http.ResponseEntity @@ -14,7 +12,8 @@ import org.springframework.web.multipart.MultipartFile @RequestMapping("/api/v1/research") @RestController class ResearchController( - private val researchService: ResearchService + private val researchService: ResearchService, + private val researchSearchService: ResearchSearchService ) { @AuthenticatedStaff @PostMapping @@ -102,4 +101,42 @@ class ResearchController( ): ResponseEntity> { return ResponseEntity.ok(researchService.migrateLabs(requestList)) } + + @PatchMapping("/migrateImageAndAttachments/{researchId}") + fun migrateResearchDetailImageAndAttachments( + @PathVariable researchId: Long, + @RequestPart("mainImage") mainImage: MultipartFile?, + @RequestPart("attachments") attachments: List? + ): ResponseEntity { + return ResponseEntity.ok( + researchService.migrateResearchDetailImageAndAttachments( + researchId, + mainImage, + attachments + ) + ) + } + + @PatchMapping("/lab/migratePdf/{labId}") + fun migrateLabPdf( + @PathVariable labId: Long, + @RequestPart("pdf") pdf: MultipartFile? + ): ResponseEntity { + return ResponseEntity.ok( + researchService.migrateLabPdf(labId, pdf) + ) + } + + @GetMapping("/search/top") + fun searchTop( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true) number: Int + ) = researchSearchService.searchTopResearch(keyword, number) + + @GetMapping("/search") + fun searchPage( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true) pageSize: Int, + @RequestParam(required = true) pageNum: Int + ) = researchSearchService.searchResearch(keyword, pageSize, pageNum) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchEntity.kt index bb3f75ad..96dea54f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchEntity.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.research.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.utils.cleanTextFromHtml import com.wafflestudio.csereal.core.conference.database.ConferenceEntity import jakarta.persistence.* @@ -46,7 +47,9 @@ class ResearchSearchEntity( fun createContent(research: ResearchEntity) = StringBuilder().apply { appendLine(research.name) appendLine(research.postType.krName) - research.description?.let { appendLine(it) } + research.description?.let { + appendLine(cleanTextFromHtml(it)) + } research.labs.forEach { appendLine(it.name) } }.toString() @@ -58,7 +61,9 @@ class ResearchSearchEntity( lab.acronym?.let { appendLine(it) } lab.youtube?.let { appendLine(it) } appendLine(lab.research.name) - lab.description?.let { appendLine(it) } + lab.description?.let { + appendLine(cleanTextFromHtml(it)) + } lab.websiteURL?.let { appendLine(it) } }.toString() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchRepository.kt index e9bf51d0..b9bf2b6d 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchSearchRepository.kt @@ -1,14 +1,94 @@ package com.wafflestudio.csereal.core.research.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.conference.database.QConferenceEntity.conferenceEntity +import com.wafflestudio.csereal.core.research.database.QLabEntity.labEntity +import com.wafflestudio.csereal.core.research.database.QResearchEntity.researchEntity +import com.wafflestudio.csereal.core.research.database.QResearchSearchEntity.researchSearchEntity import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository interface ResearchSearchRepository : JpaRepository, ResearchSearchRepositoryCustom -interface ResearchSearchRepositoryCustom +interface ResearchSearchRepositoryCustom { + fun searchTopResearch(keyword: String, number: Int): List + + fun searchResearch(keyword: String, pageSize: Int, pageNum: Int): Pair, Long> +} @Repository class ResearchSearchRepositoryCustomImpl( - private val jpaQueryFactory: JPAQueryFactory -) : ResearchSearchRepositoryCustom + private val queryFactory: JPAQueryFactory, + private val commonRepository: CommonRepository +) : ResearchSearchRepositoryCustom { + override fun searchTopResearch(keyword: String, number: Int): List { + return searchQuery(keyword) + .limit(number.toLong()) + .fetch() + } + + override fun searchResearch(keyword: String, pageSize: Int, pageNum: Int): Pair, 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 { + val searchDoubleTemplate = commonRepository.searchFullSingleTextTemplate( + keyword, + researchSearchEntity.content + ) + + return queryFactory.selectFrom( + researchSearchEntity + ).leftJoin( + researchSearchEntity.lab, + labEntity + ).fetchJoin() + .leftJoin( + researchSearchEntity.research, + researchEntity + ).fetchJoin() + .leftJoin( + researchSearchEntity.conferenceElement, + conferenceEntity + ).fetchJoin() + .where( + searchDoubleTemplate.gt(0.0) + ) + } + + fun getSearchCount(keyword: String): Long { + val searchDoubleTemplate = commonRepository.searchFullSingleTextTemplate( + keyword, + researchSearchEntity.content + ) + + return queryFactory.select( + researchSearchEntity + .countDistinct() + ).from( + researchSearchEntity + ).where( + searchDoubleTemplate.gt(0.0) + ).fetchOne()!! + } + + fun exchangePageNum(pageSize: Int, pageNum: Int, total: Long): Int { + return if ((pageNum - 1) * pageSize < total) { + pageNum + } else { + Math.ceil(total.toDouble() / pageSize).toInt() + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchPageResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchPageResponse.kt new file mode 100644 index 00000000..59cf3b61 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchPageResponse.kt @@ -0,0 +1,18 @@ +package com.wafflestudio.csereal.core.research.dto + +import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity + +data class ResearchSearchPageResponse( + val researches: List, + val total: Long +) { + companion object { + fun of( + researches: List, + total: Long + ) = ResearchSearchPageResponse( + researches = researches.map(ResearchSearchResponseElement::of), + total = total + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchResponseElement.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchResponseElement.kt new file mode 100644 index 00000000..ea5e5b37 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchResponseElement.kt @@ -0,0 +1,55 @@ +package com.wafflestudio.csereal.core.research.dto + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity +import com.wafflestudio.csereal.core.research.database.ResearchSearchType + +data class ResearchSearchResponseElement( + val id: Long, + val name: String, + val researchType: ResearchSearchType +) { + companion object { + fun of( + researchSearchEntity: ResearchSearchEntity + ): ResearchSearchResponseElement = + when { + researchSearchEntity.research != null && + researchSearchEntity.lab == null && + researchSearchEntity.conferenceElement == null + -> researchSearchEntity.research!!.let { + ResearchSearchResponseElement( + id = it.id, + name = it.name, + researchType = ResearchSearchType.RESEARCH + ) + } + + researchSearchEntity.lab != null && + researchSearchEntity.research == null && + researchSearchEntity.conferenceElement == null + -> researchSearchEntity.lab!!.let { + ResearchSearchResponseElement( + id = it.id, + name = it.name, + researchType = ResearchSearchType.LAB + ) + } + + researchSearchEntity.conferenceElement != null && + researchSearchEntity.research == null && + researchSearchEntity.lab == null + -> researchSearchEntity.conferenceElement!!.let { + ResearchSearchResponseElement( + id = it.id, + name = it.name, + researchType = ResearchSearchType.CONFERENCE + ) + } + + else -> throw CserealException.Csereal401( + "ResearchSearchEntity의 연결이 올바르지 않습니다." + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchTopResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchTopResponse.kt new file mode 100644 index 00000000..784e2c47 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchSearchTopResponse.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.research.dto + +import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity + +data class ResearchSearchTopResponse( + val topResearches: List +) { + companion object { + fun of( + topResearches: List + ) = ResearchSearchTopResponse( + topResearches = topResearches.map(ResearchSearchResponseElement::of) + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt index 7749331f..23af349a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt @@ -1,18 +1,95 @@ package com.wafflestudio.csereal.core.research.service +import com.wafflestudio.csereal.core.member.event.ProfessorCreatedEvent +import com.wafflestudio.csereal.core.member.event.ProfessorDeletedEvent +import com.wafflestudio.csereal.core.member.event.ProfessorModifiedEvent +import com.wafflestudio.csereal.core.research.database.LabRepository import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity import com.wafflestudio.csereal.core.research.database.ResearchSearchRepository +import com.wafflestudio.csereal.core.research.dto.ResearchSearchPageResponse +import com.wafflestudio.csereal.core.research.dto.ResearchSearchTopResponse +import org.springframework.context.event.EventListener +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface ResearchSearchService { + fun professorCreatedEventListener(professorCreatedEvent: ProfessorCreatedEvent) + fun professorDeletedEventListener(professorDeletedEvent: ProfessorDeletedEvent) + fun professorModifiedEventListener(professorModifiedEvent: ProfessorModifiedEvent) fun deleteResearchSearch(researchSearchEntity: ResearchSearchEntity) + fun searchTopResearch(keyword: String, number: Int): ResearchSearchTopResponse + fun searchResearch(keyword: String, pageSize: Int, pageNum: Int): ResearchSearchPageResponse } @Service class ResearchSearchServiceImpl( + private val labRepository: LabRepository, private val researchSearchRepository: ResearchSearchRepository ) : ResearchSearchService { + @Transactional(readOnly = true) + override fun searchTopResearch(keyword: String, number: Int): ResearchSearchTopResponse = + ResearchSearchTopResponse.of( + researchSearchRepository.searchTopResearch(keyword, number) + ) + + @Transactional(readOnly = true) + override fun searchResearch(keyword: String, pageSize: Int, pageNum: Int): ResearchSearchPageResponse = + researchSearchRepository.searchResearch(keyword, pageSize, pageNum).let { + ResearchSearchPageResponse.of(it.first, it.second) + } + + @EventListener + @Transactional + override fun professorCreatedEventListener(professorCreatedEvent: ProfessorCreatedEvent) { + val lab = professorCreatedEvent.labId?.let { + labRepository.findByIdOrNull(it) + } ?: return + + lab.researchSearch?.update(lab) ?: let { + lab.researchSearch = ResearchSearchEntity.create(lab) + } + } + + @EventListener + @Transactional + override fun professorDeletedEventListener(professorDeletedEvent: ProfessorDeletedEvent) { + val lab = professorDeletedEvent.labId?.let { + labRepository.findByIdOrNull(it) + } ?: return + + // if lab still has professor, remove it + lab.professors.removeIf { it.id == professorDeletedEvent.id } + + // update search data + lab.researchSearch?.update(lab) + } + + @EventListener + @Transactional + override fun professorModifiedEventListener(professorModifiedEvent: ProfessorModifiedEvent) { + val beforeLab = professorModifiedEvent.beforeLabId?.let { + labRepository.findByIdOrNull(it) + } + + val afterLab = professorModifiedEvent.afterLabId?.let { + labRepository.findByIdOrNull(it) + } + + if (beforeLab != null && beforeLab == afterLab) { + beforeLab.researchSearch?.update(beforeLab) + } + + beforeLab?.run { + // if lab still has professor, remove it + professors.removeIf { it.id == professorModifiedEvent.id } + researchSearch?.update(this) + } + + afterLab?.run { + researchSearch?.update(this) + } + } @Transactional override fun deleteResearchSearch( diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchService.kt index 48b92497..2dc97539 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchService.kt @@ -35,6 +35,12 @@ interface ResearchService { fun updateLab(labId: Long, request: LabUpdateRequest, pdf: MultipartFile?): LabDto fun migrateResearchDetail(requestList: List): List fun migrateLabs(requestList: List): List + fun migrateResearchDetailImageAndAttachments( + researchId: Long, + mainImage: MultipartFile?, + attachments: List? + ): ResearchDto + fun migrateLabPdf(labId: Long, pdf: MultipartFile?): LabDto } @Service @@ -319,4 +325,41 @@ class ResearchServiceImpl( } return list } + + @Transactional + override fun migrateResearchDetailImageAndAttachments( + researchId: Long, + mainImage: MultipartFile?, + attachments: List? + ): ResearchDto { + val researchDetail = researchRepository.findByIdOrNull(researchId) + ?: throw CserealException.Csereal404("해당 연구내용을 찾을 수 없습니다.") + + if (mainImage != null) { + mainImageService.uploadMainImage(researchDetail, mainImage) + } + + if (attachments != null) { + attachmentService.uploadAllAttachments(researchDetail, attachments) + } + + val imageURL = mainImageService.createImageURL(researchDetail.mainImage) + val attachmentResponses = attachmentService.createAttachmentResponses(researchDetail.attachments) + + return ResearchDto.of(researchDetail, imageURL, attachmentResponses) + } + + @Transactional + override fun migrateLabPdf(labId: Long, pdf: MultipartFile?): LabDto { + val lab = labRepository.findByIdOrNull(labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.") + + var pdfURL = "" + if (pdf != null) { + val attachmentDto = attachmentService.uploadAttachmentInLabEntity(lab, pdf) + pdfURL = "${endpointProperties.backend}/v1/file/${attachmentDto.filename}" + } + + return LabDto.of(lab, pdfURL) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt index cb5914ec..870a93bd 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt @@ -34,7 +34,10 @@ class SeminarEntity( // 연사 정보 var name: String, + + @Column(columnDefinition = "varchar(2047)") var speakerURL: String?, + var speakerTitle: String?, var affiliation: String, var affiliationURL: String?, diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 36fe2706..1817e8e4 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -3,20 +3,6 @@ spring: active: local datasource: driver-class-name: com.mysql.cj.jdbc.Driver - security: - oauth2: - client: - registration: - idsnucse: - client-id: waffle-dev-local-testing - client-secret: ${OIDC_CLIENT_SECRET_DEV} - authorization-grant-type: authorization_code - scope: openid, profile, email - redirect-uri: http://localhost:8080/api/v1/login/oauth2/code/idsnucse - provider: - idsnucse: - issuer-uri: https://id-dev.bacchus.io/o - jwk-set-uri: https://id-dev.bacchus.io/o/jwks servlet: multipart: enabled: true @@ -64,6 +50,20 @@ spring: properties: hibernate: dialect: com.wafflestudio.csereal.common.config.MySQLDialectCustom + security: + oauth2: + client: + registration: + idsnucse: + client-id: cse-waffle-dev + client-secret: ${OIDC_CLIENT_SECRET} + authorization-grant-type: authorization_code + scope: openid, profile, email + redirect-uri: http://localhost:8080/api/v1/login/oauth2/code/idsnucse + provider: + idsnucse: + issuer-uri: https://id.snucse.org/o + jwk-set-uri: https://id.snucse.org/o/jwks logging.level: default: INFO diff --git a/src/test/kotlin/com/wafflestudio/csereal/common/util/UtilsTest.kt b/src/test/kotlin/com/wafflestudio/csereal/common/util/UtilsTest.kt index 54965232..a54c0d31 100644 --- a/src/test/kotlin/com/wafflestudio/csereal/common/util/UtilsTest.kt +++ b/src/test/kotlin/com/wafflestudio/csereal/common/util/UtilsTest.kt @@ -1,7 +1,9 @@ package com.wafflestudio.csereal.common.util import com.wafflestudio.csereal.common.utils.cleanTextFromHtml +import com.wafflestudio.csereal.common.utils.exchangePageNum import com.wafflestudio.csereal.common.utils.substringAroundKeyword +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe @@ -101,4 +103,46 @@ class UtilsTest : BehaviorSpec({ } } } + + Given("Using exchangePageNum to get valid page number") { + When("Given variables are not positive") { + val totalMinus = Triple(1, 1, -1) + val pageSizeZero = Triple(0, 1, 1) + val pageNumZero = Triple(1, 0, 1) + + Then("should throw AssertionError") { + shouldThrow { + exchangePageNum(totalMinus.first, totalMinus.second, totalMinus.third) + } + shouldThrow { + exchangePageNum(pageSizeZero.first, pageSizeZero.second, pageSizeZero.third) + } + shouldThrow { + exchangePageNum(pageNumZero.first, pageNumZero.second, pageNumZero.third) + } + } + } + + When("Given page is in the range") { + val pageSize = 10 + val total = 100L + val pageNum = 3 + + Then("Should return pageNum itself") { + val resultPageNum = exchangePageNum(pageSize, pageNum, total) + resultPageNum shouldBe pageNum + } + } + + When("Given page is out of range (bigger)") { + val pageSize = 10 + val total = 104L + val pageNum = 15 + + Then("Should return last page number") { + val resultPageNum = exchangePageNum(pageSize, pageNum, total) + resultPageNum shouldBe 11 + } + } + } }) diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/academics/AcademicsServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/academics/AcademicsServiceTest.kt new file mode 100644 index 00000000..d5968a7d --- /dev/null +++ b/src/test/kotlin/com/wafflestudio/csereal/core/academics/AcademicsServiceTest.kt @@ -0,0 +1,78 @@ +//package com.wafflestudio.csereal.core.academics +// +//import com.wafflestudio.csereal.core.academics.database.AcademicsRepository +//import com.wafflestudio.csereal.core.academics.database.AcademicsSearchRepository +//import com.wafflestudio.csereal.core.academics.dto.AcademicsDto +//import com.wafflestudio.csereal.core.academics.service.AcademicsService +//import io.kotest.core.spec.style.BehaviorSpec +//import io.kotest.extensions.spring.SpringTestExtension +//import io.kotest.extensions.spring.SpringTestLifecycleMode +//import io.kotest.matchers.shouldBe +//import io.kotest.matchers.shouldNotBe +//import jakarta.transaction.Transactional +//import org.springframework.boot.test.context.SpringBootTest +//import org.springframework.data.repository.findByIdOrNull +//import org.springframework.test.context.ActiveProfiles +// +// +// TODO: Fix test issue +//@SpringBootTest +//@ActiveProfiles("test") +//@Transactional +//class AcademicsServiceTest ( +// private val academicsService: AcademicsService, +// private val academicsRepository: AcademicsRepository, +// private val academicsSearchRepository: AcademicsSearchRepository, +//): BehaviorSpec({ +// extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) +// +// Given("첨부파일 없는 Academics를 생성하려고 할 때") { +// val studentType = "undergraduate" +// val enumPostType = "guide" +// val request = AcademicsDto( +// name = "name", +// description = "

description

", +// year = 2023, +// time = "12:43", +// ) +// +// When("Academics를 생성한다면") { +// val returnDto = academicsService.createAcademics(studentType, enumPostType, request, null) +// +// Then("Academics가 생성되어야 한다.") { +// val id = returnDto.id +// val savedAcademics = academicsRepository.findByIdOrNull(id) +// +// savedAcademics shouldNotBe null +// savedAcademics!!.let { +// it.name shouldBe request.name +// it.description shouldBe request.description +// it.year shouldBe request.year +// it.time shouldBe request.time +// } +// } +// +// Then("검색 데이터가 생성되어야 한다.") { +// val savedAcademics = academicsRepository.findByIdOrNull(returnDto.id)!! +// val createdSearch = savedAcademics.academicsSearch?.id.let { +// academicsSearchRepository.findByIdOrNull(it) +// } +// +// createdSearch shouldNotBe null +// createdSearch!!.let { +// it.academics shouldBe savedAcademics +// it.course shouldBe null +// it.scholarship shouldBe null +// it.content shouldBe """ +// name +// 12:43 +// 2023 +// 학부생 +// description +// +// """.trimIndent() +// } +// } +// } +// } +//}) diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsServiceTest.kt new file mode 100644 index 00000000..3d8d1888 --- /dev/null +++ b/src/test/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsServiceTest.kt @@ -0,0 +1,134 @@ +package com.wafflestudio.csereal.core.admissions.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.core.admissions.api.req.AdmissionReqBody +import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity +import com.wafflestudio.csereal.core.admissions.database.AdmissionsRepository +import com.wafflestudio.csereal.core.admissions.type.AdmissionsMainType +import com.wafflestudio.csereal.core.admissions.type.AdmissionsPostType +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Profile +import org.springframework.data.repository.findByIdOrNull +import org.springframework.transaction.annotation.Transactional + +@SpringBootTest +@Profile("test") +@Transactional +class AdmissionsServiceTest( + private val admissionsService: AdmissionsService, + private val admissionsRepository: AdmissionsRepository +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + afterContainer { + admissionsRepository.deleteAll() + } + + Given("AdmissionReqBody, AdmissionMainType, AdmissionPostType이 주어졌을 때") { + val req = AdmissionReqBody( + name = "name", + language = "ko", + description = "

description

" + ) + val mainType = AdmissionsMainType.UNDERGRADUATE + val postType = AdmissionsPostType.REGULAR_ADMISSION + + When("createAdmission이 호출되면") { + val result = admissionsService.createAdmission(req, mainType, postType) + + Then("주어진 정보와 일치하는 AdmissionDto가 반환된다.") { + result.name shouldBe req.name + result.mainType shouldBe mainType.toJsonValue() + result.postType shouldBe postType.toJsonValue() + result.language shouldBe req.language + result.description shouldBe req.description + } + + Then("주어진 정보와 일치하는 AdmissionEnitity가 생성된다.") { + val entity = admissionsRepository.findByIdOrNull(result.id) + entity shouldNotBe null + entity!!.name shouldBe req.name + entity.mainType shouldBe mainType + entity.postType shouldBe postType + entity.language shouldBe LanguageType.makeStringToLanguageType(req.language) + entity.description shouldBe req.description + } + + Then("검색 정보가 잘 생성되어야 한다.") { + val entity = admissionsRepository.findByIdOrNull(result.id)!! + entity.searchContent shouldBe """ + ${req.name} + ${mainType.getLanguageValue(LanguageType.KO)} + ${postType.getLanguageValue(LanguageType.KO)} + description + + """.trimIndent() + } + } + } + Given("AdmissionReqBody에 잘못된 Language가 주어졌을 때") { + val req = AdmissionReqBody( + name = "name", + language = "wrong", + description = "description" + ) + val mainType = AdmissionsMainType.UNDERGRADUATE + val postType = AdmissionsPostType.REGULAR_ADMISSION + When("createAdmission이 호출되면") { + Then("Csereal400 에러가 발생한다.") { + shouldThrow { + admissionsService.createAdmission(req, mainType, postType) + } + } + } + } + + Given("Admission이 존재할 때") { + val admission = AdmissionsEntity( + name = "name", + mainType = AdmissionsMainType.INTERNATIONAL, + postType = AdmissionsPostType.EXCHANGE_VISITING, + language = LanguageType.EN, + description = "description", + searchContent = "ss" + ).let { + admissionsRepository.save(it) + } + + When("존재하는 readAdmission이 호출되면") { + val mainType = AdmissionsMainType.INTERNATIONAL + val postType = AdmissionsPostType.EXCHANGE_VISITING + val language = LanguageType.EN + val result = admissionsService.readAdmission(mainType, postType, language) + + Then("주어진 정보와 일치하는 AdmissionDto가 반환된다.") { + result.let { + it.name shouldBe admission.name + it.mainType shouldBe admission.mainType.toJsonValue() + it.postType shouldBe admission.postType.toJsonValue() + it.language shouldBe LanguageType.makeLowercase(admission.language) + it.description shouldBe admission.description + } + } + } + + When("존재하지 않는 readAdmission이 호출되면") { + Then("Csereal404 에러가 발생한다.") { + shouldThrow { + admissionsService.readAdmission( + AdmissionsMainType.UNDERGRADUATE, + AdmissionsPostType.REGULAR_ADMISSION, + LanguageType.KO + ) + } + } + } + } +}) diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt index c0d61496..37043fad 100644 --- a/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt +++ b/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt @@ -27,6 +27,7 @@ class StaffServiceTest( Given("이미지 없는 행정직원을 생성하려고 할 떄") { val staffDto = StaffDto( + language = "ko", name = "name", role = "role", office = "office", @@ -75,6 +76,7 @@ class StaffServiceTest( Given("이미지 없는 행정직원을 수정할 때") { val staffDto = StaffDto( + language = "ko", name = "name", role = "role", office = "office", @@ -87,6 +89,7 @@ class StaffServiceTest( When("행정직원을 수정하면") { val updateStaffDto = StaffDto( + language = "ko", name = "name2", role = "role2", office = "office2", diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchSearchServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchSearchServiceTest.kt new file mode 100644 index 00000000..032743ba --- /dev/null +++ b/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchSearchServiceTest.kt @@ -0,0 +1,254 @@ +package com.wafflestudio.csereal.core.reseach.service + +import com.wafflestudio.csereal.core.member.database.ProfessorRepository +import com.wafflestudio.csereal.core.member.database.ProfessorStatus +import com.wafflestudio.csereal.core.member.dto.ProfessorDto +import com.wafflestudio.csereal.core.member.service.ProfessorService +import com.wafflestudio.csereal.core.research.database.* +import com.wafflestudio.csereal.core.research.dto.LabDto +import com.wafflestudio.csereal.core.research.dto.LabProfessorResponse +import com.wafflestudio.csereal.core.research.service.ResearchSearchService +import com.wafflestudio.csereal.core.research.service.ResearchService +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@SpringBootTest +@Transactional +class ResearchSearchServiceTest( + private val researchSearchService: ResearchSearchService, + private val professorRepository: ProfessorRepository, + private val professorService: ProfessorService, + private val labRepository: LabRepository, + private val researchRepository: ResearchRepository, + private val researchSearchRepository: ResearchSearchRepository, + private val researchService: ResearchService +) : BehaviorSpec() { + init { + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + beforeSpec { + } + + afterSpec { + professorRepository.deleteAll() + labRepository.deleteAll() + researchSearchRepository.deleteAll() + } + + // Event Listener Test + Given("기존 lab이 존재할 때") { + // Save professors + val professor1Dto = professorService.createProfessor( + createProfessorRequest = ProfessorDto( + name = "professor1", + email = null, + status = ProfessorStatus.ACTIVE, + academicRank = "professor", + labId = null, + labName = null, + startDate = null, + endDate = null, + office = null, + phone = null, + fax = null, + website = null, + educations = emptyList(), + researchAreas = emptyList(), + careers = emptyList() + ), + mainImage = null + ) + val professor2Dto = professorService.createProfessor( + createProfessorRequest = ProfessorDto( + name = "professor2", + email = null, + status = ProfessorStatus.ACTIVE, + academicRank = "professor", + labId = null, + labName = null, + startDate = null, + endDate = null, + office = null, + phone = null, + fax = null, + website = null, + educations = emptyList(), + researchAreas = emptyList(), + careers = emptyList() + ), + mainImage = null + ) + + val professor1 = professorRepository.findByIdOrNull(professor1Dto.id)!! + val professor2 = professorRepository.findByIdOrNull(professor2Dto.id)!! + + // Save research + val research = researchRepository.save( + ResearchEntity( + name = "research", + postType = ResearchPostType.GROUPS, + description = null + ) + ) + + // Save lab + val labDto = LabDto( + id = -1, + name = "name", + professors = listOf( + LabProfessorResponse(professor1.id, professor1.name), + LabProfessorResponse(professor2.id, professor2.name) + ), + acronym = "acronym", + description = "

description

", + group = "research", + pdf = null, + location = "location", + tel = "tel", + websiteURL = "websiteURL", + youtube = "youtube" + ) + + val emptyLabDto = LabDto( + id = -1, + name = "nameE", + professors = listOf(), + acronym = "acronymE", + description = "

descriptionE

", + group = "research", + pdf = null, + location = "locationE", + tel = "telE", + websiteURL = "websiteURLE", + youtube = "youtubeE" + ) + + val createdLabDto = researchService.createLab(labDto, null) + val createdEmptyLabDto = researchService.createLab(emptyLabDto, null) + + When("professor가 제거된다면") { + professorService.deleteProfessor(professor1.id) + + Then("검색 엔티티의 내용이 변경된다") { + val lab = labRepository.findByIdOrNull(createdLabDto.id)!! + val search = lab.researchSearch + + search shouldNotBe null + search!!.content shouldBe + """ + name + professor2 + location + tel + acronym + youtube + research + description + websiteURL + + """.trimIndent() + } + } + + When("professor가 추가된다면") { + val process3CreatedDto = professorService.createProfessor( + createProfessorRequest = ProfessorDto( + name = "newProfessor", + email = "email", + status = ProfessorStatus.ACTIVE, + academicRank = "academicRank", + labId = createdLabDto.id, + labName = null, + startDate = LocalDate.now(), + endDate = LocalDate.now(), + office = "office", + phone = "phone", + fax = "fax", + website = "website", + educations = listOf("education1", "education2"), + researchAreas = listOf("researchArea1", "researchArea2"), + careers = listOf("career1", "career2") + ), + mainImage = null + ) + + Then("검색 엔티티의 내용이 변경된다") { + val lab = labRepository.findByIdOrNull(createdLabDto.id)!! + val search = lab.researchSearch + + search shouldNotBe null + search!!.content shouldBe + """ + name + professor2 + newProfessor + location + tel + acronym + youtube + research + description + websiteURL + + """.trimIndent() + } + } + + When("professor가 수정된다면") { + professorService.updateProfessor( + professor2.id, + ProfessorDto.of(professor2, null) + .copy(name = "updateProfessor", labId = createdEmptyLabDto.id), + mainImage = null + ) + + Then("예전 검색 데이터에서 빠져야 한다.") { + val lab = labRepository.findByIdOrNull(createdLabDto.id)!! + val search = lab.researchSearch + + search shouldNotBe null + search!!.content shouldBe + """ + name + newProfessor + location + tel + acronym + youtube + research + description + websiteURL + + """.trimIndent() + } + + Then("새로운 검색 데이터에 포함되어야 한다.") { + val lab = labRepository.findByIdOrNull(createdEmptyLabDto.id)!! + val search = lab.researchSearch + + search shouldNotBe null + search!!.content shouldBe + """ + nameE + updateProfessor + locationE + telE + acronymE + youtubeE + research + descriptionE + websiteURLE + + """.trimIndent() + } + } + } + } +}