diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateClubReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateClubReq.kt new file mode 100644 index 00000000..eeb3af14 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateClubReq.kt @@ -0,0 +1,11 @@ +package com.wafflestudio.csereal.core.about.api.req + +data class CreateClubReq( + val ko: ClubReqBody, + val en: ClubReqBody +) + +data class ClubReqBody( + val name: String, + val description: String +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateCompanyReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateCompanyReq.kt new file mode 100644 index 00000000..c8663d07 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateCompanyReq.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.about.api.req + +data class CreateCompanyReq( + val name: String, + val url: String?, + val year: Int +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateFacReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateFacReq.kt new file mode 100644 index 00000000..d9ead5e0 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/CreateFacReq.kt @@ -0,0 +1,12 @@ +package com.wafflestudio.csereal.core.about.api.req + +data class CreateFacReq( + val ko: FacDto, + val en: FacDto +) + +data class FacDto( + val name: String, + val description: String, + val locations: MutableList +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateAboutReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateAboutReq.kt new file mode 100644 index 00000000..ad64c3d1 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateAboutReq.kt @@ -0,0 +1,12 @@ +package com.wafflestudio.csereal.core.about.api.req + +data class UpdateAboutReq( + val ko: BasicAbout, + val en: BasicAbout, + val removeImage: Boolean +) + +data class BasicAbout( + val description: String, + val deleteIds: List +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateClubReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateClubReq.kt new file mode 100644 index 00000000..bf8c8966 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateClubReq.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.csereal.core.about.api.req + +import com.wafflestudio.csereal.core.about.dto.ClubDto + +data class UpdateClubReq( + val ko: ClubDto, + val en: ClubDto, + val removeImage: Boolean +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateDescriptionReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateDescriptionReq.kt new file mode 100644 index 00000000..0ea22f11 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateDescriptionReq.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.about.api.req + +data class UpdateDescriptionReq( + val koDescription: String, + val enDescription: String +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateFacReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateFacReq.kt new file mode 100644 index 00000000..045c5340 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/req/UpdateFacReq.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.about.api.req + +data class UpdateFacReq( + val ko: FacDto, + val en: FacDto, + val removeImage: Boolean +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/v1/AboutController.kt similarity index 56% rename from src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt rename to src/main/kotlin/com/wafflestudio/csereal/core/about/api/v1/AboutController.kt index c4f00fd8..756054ba 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/v1/AboutController.kt @@ -1,21 +1,17 @@ -package com.wafflestudio.csereal.core.about.api +package com.wafflestudio.csereal.core.about.api.v1 -import com.wafflestudio.csereal.common.aop.AuthenticatedStaff import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.about.api.req.* import com.wafflestudio.csereal.core.about.api.res.AboutSearchResBody import com.wafflestudio.csereal.core.about.dto.* -import com.wafflestudio.csereal.core.about.dto.AboutRequest -import com.wafflestudio.csereal.core.about.dto.FutureCareersRequest import com.wafflestudio.csereal.core.about.service.AboutService import jakarta.validation.Valid import jakarta.validation.constraints.Positive -import org.springframework.context.annotation.Profile import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* -import org.springframework.web.multipart.MultipartFile @RequestMapping("/api/v1/about") -@RestController +@RestController("AboutControllerV1") class AboutController( private val aboutService: AboutService ) { @@ -24,18 +20,6 @@ class AboutController( // postType: directions / name -> by-public-transit, by-car, from-far-away // Todo: 학부장 인사말(greetings) signature - @AuthenticatedStaff - @PostMapping("/{postType}") - fun createAbout( - @PathVariable postType: String, - @Valid - @RequestPart("request") - request: AboutDto, - @RequestPart("mainImage") mainImage: MultipartFile?, - @RequestPart("attachments") attachments: List? - ): ResponseEntity { - return ResponseEntity.ok(aboutService.createAbout(postType, request, mainImage, attachments)) - } // read 목록이 하나 @GetMapping("/{postType}") @@ -46,6 +30,7 @@ class AboutController( return ResponseEntity.ok(aboutService.readAbout(language, postType)) } + @Deprecated("Use V2 API") @GetMapping("/student-clubs") fun readAllClubs( @RequestParam(required = false, defaultValue = "ko") language: String @@ -101,56 +86,4 @@ class AboutController( pageNum, amount ) - - @Profile("!prod") - @PostMapping("/migrate") - fun migrateAbout( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(aboutService.migrateAbout(requestList)) - } - - @Profile("!prod") - @PostMapping("/future-careers/migrate") - fun migrateFutureCareers( - @RequestBody request: FutureCareersRequest - ): ResponseEntity { - return ResponseEntity.ok(aboutService.migrateFutureCareers(request)) - } - - @Profile("!prod") - @PostMapping("/student-clubs/migrate") - fun migrateStudentClubs( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(aboutService.migrateStudentClubs(requestList)) - } - - @Profile("!prod") - @PostMapping("/facilities/migrate") - fun migrateFacilities( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(aboutService.migrateFacilities(requestList)) - } - - @Profile("!prod") - @PostMapping("/directions/migrate") - fun migrateDirections( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(aboutService.migrateDirections(requestList)) - } - - @Profile("!prod") - @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/api/v2/AboutController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/v2/AboutController.kt new file mode 100644 index 00000000..16ffb2ed --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/v2/AboutController.kt @@ -0,0 +1,84 @@ +package com.wafflestudio.csereal.core.about.api.v2 + +import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.core.about.api.req.* +import com.wafflestudio.csereal.core.about.dto.GroupedClubDto +import com.wafflestudio.csereal.core.about.service.AboutService +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile + +@RequestMapping("/api/v2/about") +@RestController +class AboutController( + private val aboutService: AboutService +) { + @GetMapping("/student-clubs") + fun readAllClubs(): List = aboutService.readAllGroupedClubs() + + @AuthenticatedStaff + @PostMapping("/student-clubs") + fun createClub( + @RequestPart request: CreateClubReq, + @RequestPart mainImage: MultipartFile? + ) = aboutService.createClub(request, mainImage) + + @AuthenticatedStaff + @PutMapping("/student-clubs") + fun updateClub( + @RequestPart request: UpdateClubReq, + @RequestPart newMainImage: MultipartFile? + ) = aboutService.updateClub(request, newMainImage) + + @AuthenticatedStaff + @DeleteMapping("/student-clubs/{id}") + fun deleteClub(@PathVariable id: Long) = aboutService.deleteClub(id) + + @AuthenticatedStaff + @PutMapping("/{postType}") + fun updateAbout( + @PathVariable postType: String, + @RequestPart request: UpdateAboutReq, + @RequestPart newMainImage: MultipartFile?, + @RequestPart newAttachments: List? + ) = aboutService.updateAbout(postType, request, newMainImage, newAttachments) + + @AuthenticatedStaff + @PostMapping("/facilities") + fun createFacilities(@RequestPart request: CreateFacReq, @RequestPart mainImage: MultipartFile?) = + aboutService.createFacilities(request, mainImage) + + @AuthenticatedStaff + @PutMapping("/facilities/{id}") + fun updateFacility( + @PathVariable id: Long, + @RequestPart request: UpdateFacReq, + @RequestPart newMainImage: MultipartFile? + ) = aboutService.updateFacility(id, request, newMainImage) + + @AuthenticatedStaff + @DeleteMapping("/facilities/{id}") + fun deleteFacility(@PathVariable id: Long) = aboutService.deleteFacility(id) + + @AuthenticatedStaff + @PutMapping("/directions/{id}") + fun updateDirection(@PathVariable id: Long, @RequestBody request: UpdateDescriptionReq) = + aboutService.updateDirection(id, request) + + @AuthenticatedStaff + @PutMapping("/future-careers") + fun updateFutureCareersPage(@RequestBody request: UpdateDescriptionReq) = + aboutService.updateFutureCareersPage(request) + + @AuthenticatedStaff + @PostMapping("/future-careers/company") + fun createCompany(@RequestBody request: CreateCompanyReq) = aboutService.createCompany(request) + + @AuthenticatedStaff + @PutMapping("/future-careers/company/{id}") + fun updateCompany(@PathVariable id: Long, @RequestBody request: CreateCompanyReq) = + aboutService.updateCompany(id, request) + + @AuthenticatedStaff + @DeleteMapping("/future-careers/company/{id}") + fun deleteCompany(@PathVariable id: Long) = aboutService.deleteCompany(id) +} 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 6aeeacb2..a604bc96 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 @@ -22,8 +22,6 @@ class AboutEntity( @Column(columnDefinition = "mediumText") var description: String, - var year: Int?, - @Column(columnDefinition = "TEXT") @Convert(converter = StringListConverter::class) var locations: MutableList = mutableListOf(), @@ -31,7 +29,7 @@ class AboutEntity( @OneToMany(mappedBy = "about", cascade = [CascadeType.ALL], orphanRemoval = true) var attachments: MutableList = mutableListOf(), - @OneToOne + @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) var mainImage: MainImageEntity? = null, @Column(columnDefinition = "TEXT") @@ -52,7 +50,6 @@ class AboutEntity( language = languageType, name = aboutDto.name, description = aboutDto.description, - year = aboutDto.year, locations = aboutDto.locations?.toMutableList() ?: mutableListOf(), searchContent = "" ) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutLanguageEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutLanguageEntity.kt new file mode 100644 index 00000000..f6c1853c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutLanguageEntity.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.about.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.* + +@Entity(name = "about_language") +class AboutLanguageEntity( + @OneToOne + @JoinColumn(name = "korean_id") + val koAbout: AboutEntity, + + @OneToOne + @JoinColumn(name = "english_id") + val enAbout: AboutEntity +) : BaseTimeEntity() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutLanguageRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutLanguageRepository.kt new file mode 100644 index 00000000..f4c06ed9 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutLanguageRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.about.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface AboutLanguageRepository : JpaRepository { + fun findByKoAbout(koAboutEntity: AboutEntity): AboutLanguageEntity? + fun findByEnAbout(enAboutEntity: AboutEntity): AboutLanguageEntity? +} 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 9470cd50..6572b8e9 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 @@ -14,6 +14,7 @@ interface AboutRepository : JpaRepository, AboutCustomReposit languageType: LanguageType, postType: AboutPostType ): List + fun findByLanguageAndPostType( languageType: LanguageType, postType: AboutPostType diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/CompanyRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/CompanyRepository.kt index 321605c6..a4aef4c9 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/CompanyRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/CompanyRepository.kt @@ -3,5 +3,5 @@ package com.wafflestudio.csereal.core.about.database import org.springframework.data.jpa.repository.JpaRepository interface CompanyRepository : JpaRepository { - fun findAllByOrderByYearDesc(): List + fun findAllByOrderByNameDesc(): List } 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 fc35fbbc..5395e113 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 @@ -12,7 +12,6 @@ data class AboutDto( val language: String, val name: String?, val description: String, - val year: Int?, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, val locations: List?, @@ -30,7 +29,6 @@ data class AboutDto( language = LanguageType.makeLowercase(this.language), name = this.name, description = this.description, - year = this.year, createdAt = this.createdAt, modifiedAt = this.modifiedAt, locations = this.locations, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/GroupedClubDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/GroupedClubDto.kt new file mode 100644 index 00000000..4d9a977e --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/GroupedClubDto.kt @@ -0,0 +1,21 @@ +package com.wafflestudio.csereal.core.about.dto + +import com.wafflestudio.csereal.core.about.database.AboutEntity + +data class GroupedClubDto( + val ko: ClubDto, + val en: ClubDto +) + +data class ClubDto( + val id: Long, + val name: String, + val description: String, + val imageURL: String? +) { + companion object { + fun of(aboutEntity: AboutEntity, imageURL: String?): ClubDto { + return ClubDto(aboutEntity.id, aboutEntity.name!!, aboutEntity.description, imageURL) + } + } +} 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 bf889423..43a471f7 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 @@ -13,7 +13,6 @@ data class StudentClubDto( val name: String, val engName: String, val description: String, - val year: Int?, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, val locations: List?, @@ -34,7 +33,6 @@ data class StudentClubDto( name = name, engName = engName, description = this.description, - year = this.year, createdAt = this.createdAt, modifiedAt = this.modifiedAt, locations = this.locations, 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 7c676e7d..dd660988 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 @@ -2,6 +2,7 @@ package com.wafflestudio.csereal.core.about.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.about.api.req.* import com.wafflestudio.csereal.core.about.api.res.AboutSearchElementDto import com.wafflestudio.csereal.core.about.api.res.AboutSearchResBody import com.wafflestudio.csereal.core.about.database.* @@ -17,18 +18,31 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile interface AboutService { - fun createAbout( + fun readAbout(language: String, postType: String): AboutDto + fun updateAbout( postType: String, - request: AboutDto, - mainImage: MultipartFile?, - attachments: List? - ): AboutDto + request: UpdateAboutReq, + newMainImage: MultipartFile?, + newAttachments: List? + ) + + fun createClub(request: CreateClubReq, mainImage: MultipartFile?) + fun updateClub(request: UpdateClubReq, newMainImage: MultipartFile?) + fun deleteClub(id: Long) - fun readAbout(language: String, postType: String): AboutDto fun readAllClubs(language: String): List + fun readAllGroupedClubs(): List + fun createFacilities(request: CreateFacReq, mainImage: MultipartFile?) + fun updateFacility(id: Long, request: UpdateFacReq, newMainImage: MultipartFile?) + fun deleteFacility(id: Long) fun readAllFacilities(language: String): List fun readAllDirections(language: String): List + fun updateDirection(id: Long, request: UpdateDescriptionReq) + fun updateFutureCareersPage(request: UpdateDescriptionReq) fun readFutureCareers(language: String): FutureCareersPage + fun createCompany(request: CreateCompanyReq) + fun updateCompany(id: Long, request: CreateCompanyReq) + fun deleteCompany(id: Long) fun searchTopAbout( keyword: String, @@ -44,17 +58,6 @@ interface AboutService { pageNum: Int, amount: Int ): AboutSearchResBody - - 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 @@ -63,47 +66,131 @@ class AboutServiceImpl( private val companyRepository: CompanyRepository, private val statRepository: StatRepository, private val mainImageService: MainImageService, - private val attachmentService: AttachmentService + private val attachmentService: AttachmentService, + private val aboutLanguageRepository: AboutLanguageRepository ) : AboutService { + + @Transactional(readOnly = true) + override fun readAbout(language: String, postType: String): AboutDto { + val languageType = LanguageType.makeStringToLanguageType(language) + val enumPostType = makeStringToEnum(postType) + val about = aboutRepository.findByLanguageAndPostType(languageType, enumPostType) + val imageURL = mainImageService.createImageURL(about.mainImage) + val attachmentResponses = attachmentService.createAttachmentResponses(about.attachments) + + return AboutDto.of(about, imageURL, attachmentResponses) + } + @Transactional - override fun createAbout( + override fun updateAbout( postType: String, - request: AboutDto, - mainImage: MultipartFile?, - attachments: List? - ): AboutDto { + request: UpdateAboutReq, + newMainImage: MultipartFile?, + newAttachments: List? + ) { val enumPostType = makeStringToEnum(postType) - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - var newAbout = AboutEntity.of(enumPostType, enumLanguageType, request) + val languages = listOf(LanguageType.KO, LanguageType.EN) + val abouts = languages.map { lang -> + aboutRepository.findByLanguageAndPostType(lang, enumPostType).apply { + description = if (lang == LanguageType.KO) request.ko.description else request.en.description + } + } + + abouts.forEach { it.syncSearchContent() } + + if (newMainImage != null) { + abouts.forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + mainImageService.uploadMainImage(it, newMainImage) + } + } else if (request.removeImage) { + abouts.forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + it.mainImage = null + } + } + + attachmentService.deleteAttachments(request.ko.deleteIds + request.en.deleteIds) + + if (newAttachments != null) { + abouts.forEach { attachmentService.uploadAllAttachments(it, newAttachments) } + } + } + + @Transactional + override fun createClub(request: CreateClubReq, mainImage: MultipartFile?) { + val langToReq = listOf( + LanguageType.KO to request.ko, + LanguageType.EN to request.en + ) + + val clubs = langToReq.map { (lang, req) -> + AboutEntity( + AboutPostType.STUDENT_CLUBS, + lang, + req.name, + req.description, + searchContent = "" + ).apply { syncSearchContent() } + } if (mainImage != null) { - mainImageService.uploadMainImage(newAbout, mainImage) + clubs.forEach { mainImageService.uploadMainImage(it, mainImage) } } - if (attachments != null) { - attachmentService.uploadAllAttachments(newAbout, attachments) + aboutRepository.save(clubs[0]) + aboutRepository.save(clubs[1]) + aboutLanguageRepository.save(AboutLanguageEntity(clubs[0], clubs[1])) + } + + @Transactional + override fun updateClub(request: UpdateClubReq, newMainImage: MultipartFile?) { + val (ko, en) = listOf(request.ko.id, request.en.id).map { id -> + aboutRepository.findByIdOrNull(id) ?: throw CserealException.Csereal404("club not found") } - syncSearchOfAbout(newAbout) + if (ko.language != LanguageType.KO || en.language != LanguageType.EN) { + throw CserealException.Csereal400("language doesn't match") + } - newAbout = aboutRepository.save(newAbout) + listOf(ko to request.ko, en to request.en).forEach { (club, clubDto) -> + updateClubDetails(club, clubDto) + } - val imageURL = mainImageService.createImageURL(newAbout.mainImage) - val attachmentResponses = - attachmentService.createAttachmentResponses(newAbout.attachments) + if (newMainImage != null) { + listOf(ko, en).forEach { club -> + club.mainImage?.let { image -> mainImageService.removeImage(image) } + mainImageService.uploadMainImage(club, newMainImage) + } + } else if (request.removeImage) { + listOf(ko, en).forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + it.mainImage = null + } + } + } - return AboutDto.of(newAbout, imageURL, attachmentResponses) + private fun updateClubDetails(club: AboutEntity, clubDto: ClubDto) { + club.name = clubDto.name + club.description = clubDto.description + club.syncSearchContent() } - @Transactional(readOnly = true) - override fun readAbout(language: String, postType: String): AboutDto { - val languageType = LanguageType.makeStringToLanguageType(language) - val enumPostType = makeStringToEnum(postType) - val about = aboutRepository.findByLanguageAndPostType(languageType, enumPostType) - val imageURL = mainImageService.createImageURL(about.mainImage) - val attachmentResponses = attachmentService.createAttachmentResponses(about.attachments) + @Transactional + override fun deleteClub(id: Long) { + val club = aboutRepository.findByIdOrNull(id) ?: throw CserealException.Csereal404("club not found") + val aboutLanguage = when (club.language) { + LanguageType.KO -> aboutLanguageRepository.findByKoAbout(club) + LanguageType.EN -> aboutLanguageRepository.findByEnAbout(club) + } - return AboutDto.of(about, imageURL, attachmentResponses) + listOf(aboutLanguage!!.koAbout, aboutLanguage.enAbout).forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + } + + aboutLanguageRepository.delete(aboutLanguage) + aboutRepository.delete(aboutLanguage.koAbout) + aboutRepository.delete(aboutLanguage.enAbout) } @Transactional(readOnly = true) @@ -125,6 +212,103 @@ class AboutServiceImpl( return clubs } + @Transactional(readOnly = true) + override fun readAllGroupedClubs(): List { + val clubs = aboutLanguageRepository.findAll().filter { it.koAbout.postType == AboutPostType.STUDENT_CLUBS } + .sortedBy { it.koAbout.name } + return clubs.map { + val imageURL = mainImageService.createImageURL(it.koAbout.mainImage) + GroupedClubDto(ko = ClubDto.of(it.koAbout, imageURL), en = ClubDto.of(it.enAbout, imageURL)) + } + } + + @Transactional + override fun createFacilities(request: CreateFacReq, mainImage: MultipartFile?) { + val langToReq = listOf( + LanguageType.KO to request.ko, + LanguageType.EN to request.en + ) + + val facilities = langToReq.map { (lang, req) -> + AboutEntity( + AboutPostType.FACILITIES, + lang, + req.name, + req.description, + searchContent = "", + locations = req.locations + ).apply { syncSearchContent() } + } + + if (mainImage != null) { + facilities.forEach { mainImageService.uploadMainImage(it, mainImage) } + } + aboutRepository.save(facilities[0]) + aboutRepository.save(facilities[1]) + aboutLanguageRepository.save(AboutLanguageEntity(facilities[0], facilities[1])) + } + + @Transactional + override fun updateFacility(id: Long, request: UpdateFacReq, newMainImage: MultipartFile?) { + val facility = aboutRepository.findByIdOrNull(id) ?: throw CserealException.Csereal404("id not found") + + val corresponding = when (facility.language) { + LanguageType.KO -> aboutLanguageRepository.findByKoAbout(facility)!!.enAbout + LanguageType.EN -> aboutLanguageRepository.findByEnAbout(facility)!!.koAbout + } + + when (facility.language) { + LanguageType.KO -> { + updateFacility(facility, request.ko) + updateFacility(corresponding, request.en) + } + + LanguageType.EN -> { + updateFacility(facility, request.en) + updateFacility(corresponding, request.ko) + } + } + + facility.syncSearchContent() + corresponding.syncSearchContent() + + if (newMainImage != null) { + listOf(facility, corresponding).forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + mainImageService.uploadMainImage(it, newMainImage) + } + } else if (request.removeImage) { + listOf(facility, corresponding).forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + it.mainImage = null + } + } + } + + private fun updateFacility(facility: AboutEntity, facDto: FacDto) { + facility.name = facDto.name + facility.description = facDto.description + facility.locations = facDto.locations + } + + @Transactional + override fun deleteFacility(id: Long) { + val facility = aboutRepository.findByIdOrNull(id) ?: throw CserealException.Csereal404("id not found") + + val facilityLanguage = when (facility.language) { + LanguageType.KO -> aboutLanguageRepository.findByKoAbout(facility) + LanguageType.EN -> aboutLanguageRepository.findByEnAbout(facility) + } + + listOf(facilityLanguage!!.koAbout, facilityLanguage.enAbout).forEach { + it.mainImage?.let { image -> mainImageService.removeImage(image) } + } + + aboutLanguageRepository.delete(facilityLanguage) + aboutRepository.delete(facilityLanguage.koAbout) + aboutRepository.delete(facilityLanguage.enAbout) + } + @Transactional(readOnly = true) override fun readAllFacilities(language: String): List { val languageType = LanguageType.makeStringToLanguageType(language) @@ -158,6 +342,43 @@ class AboutServiceImpl( return directions } + @Transactional + override fun updateDirection(id: Long, request: UpdateDescriptionReq) { + val direction = aboutRepository.findByIdOrNull(id) ?: throw CserealException.Csereal404("direction not found") + + val corresponding = when (direction.language) { + LanguageType.KO -> aboutLanguageRepository.findByKoAbout(direction)!!.enAbout + LanguageType.EN -> aboutLanguageRepository.findByEnAbout(direction)!!.koAbout + } + + when (direction.language) { + LanguageType.KO -> { + direction.description = request.koDescription + corresponding.description = request.enDescription + } + + LanguageType.EN -> { + direction.description = request.enDescription + corresponding.description = request.koDescription + } + } + + direction.syncSearchContent() + corresponding.syncSearchContent() + } + + @Transactional + override fun updateFutureCareersPage(request: UpdateDescriptionReq) { + val ko = aboutRepository.findByLanguageAndPostType(LanguageType.KO, AboutPostType.FUTURE_CAREERS) + val en = aboutRepository.findByLanguageAndPostType(LanguageType.EN, AboutPostType.FUTURE_CAREERS) + + ko.description = request.koDescription + en.description = request.enDescription + + ko.syncSearchContent() + en.syncSearchContent() + } + @Transactional override fun readFutureCareers(language: String): FutureCareersPage { val languageType = LanguageType.makeStringToLanguageType(language) @@ -183,12 +404,30 @@ class AboutServiceImpl( FutureCareersStatDto(i, bachelor, master, doctor) ) } - val companyList = companyRepository.findAllByOrderByYearDesc().map { + val companyList = companyRepository.findAllByOrderByNameDesc().map { FutureCareersCompanyDto.of(it) } return FutureCareersPage(description, statList, companyList) } + @Transactional + override fun createCompany(request: CreateCompanyReq) { + companyRepository.save(CompanyEntity(request.name, request.url, request.year)) + } + + @Transactional + override fun updateCompany(id: Long, request: CreateCompanyReq) { + val company = companyRepository.findByIdOrNull(id) ?: throw CserealException.Csereal404("company not found") + company.name = request.name + company.url = request.url + company.year = request.year + } + + @Transactional + override fun deleteCompany(id: Long) { + companyRepository.deleteById(id) + } + @Transactional(propagation = Propagation.REQUIRES_NEW) @EventListener fun refreshSearchListener(event: RefreshSearchEvent) { @@ -248,221 +487,6 @@ class AboutServiceImpl( ) } - @Transactional - override fun migrateAbout(requestList: List): List { - // Todo: add about migrate search - 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, - description = description, - year = null, - createdAt = null, - modifiedAt = null, - locations = null, - imageURL = null, - attachments = listOf() - ) - - val languageType = LanguageType.makeStringToLanguageType(language) - var newAbout = AboutEntity.of(enumPostType, languageType, aboutDto) - syncSearchOfAbout(newAbout) - - newAbout = aboutRepository.save(newAbout) - - list.add(AboutDto.of(newAbout, null, listOf())) - } - return list - } - - @Transactional - override fun migrateFutureCareers(request: FutureCareersRequest): FutureCareersPage { - // Todo: add about migrate search - val description = request.description - val language = request.language - val statList = mutableListOf() - val companyList = mutableListOf() - - val aboutDto = AboutDto( - id = null, - language = language, - name = null, - description = description, - year = null, - createdAt = null, - modifiedAt = null, - locations = null, - imageURL = null, - attachments = listOf() - ) - - val languageType = LanguageType.makeStringToLanguageType(language) - - var newAbout = AboutEntity.of(AboutPostType.FUTURE_CAREERS, languageType, aboutDto) - - for (stat in request.stat) { - val year = stat.year - val bachelorList = mutableListOf() - val masterList = mutableListOf() - val doctorList = mutableListOf() - - for (bachelor in stat.bachelor) { - val newBachelor = StatEntity.of(year, Degree.BACHELOR, bachelor) - statRepository.save(newBachelor) - - bachelorList.add(bachelor) - } - for (master in stat.master) { - val newMaster = StatEntity.of(year, Degree.MASTER, master) - statRepository.save(newMaster) - - masterList.add(master) - } - for (doctor in stat.doctor) { - val newDoctor = StatEntity.of(year, Degree.DOCTOR, doctor) - statRepository.save(newDoctor) - - doctorList.add(doctor) - } - } - - for (company in request.companies) { - val newCompany = CompanyEntity.of(company) - companyRepository.save(newCompany) - - companyList.add(company) - } - - syncSearchOfAbout(newAbout) - newAbout = aboutRepository.save(newAbout) - - return FutureCareersPage(description, statList.toList(), companyList.toList()) - } - - @Transactional - override fun migrateStudentClubs(requestList: List): List { - val list = mutableListOf() - - for (request in requestList) { - val language = request.language - val name = request.name.split("(")[0] - val engName = request.name.split("(")[1].replaceFirst(")", "") - - val aboutDto = AboutDto( - id = null, - language = language, - name = name, - description = request.description, - year = null, - createdAt = null, - modifiedAt = null, - locations = null, - imageURL = null, - attachments = listOf() - ) - val languageType = LanguageType.makeStringToLanguageType(language) - - var newAbout = AboutEntity.of(AboutPostType.STUDENT_CLUBS, languageType, aboutDto) - - syncSearchOfAbout(newAbout) - newAbout = aboutRepository.save(newAbout) - - list.add(StudentClubDto.of(newAbout, name, engName, null, listOf())) - } - return list - } - - @Transactional - override fun migrateFacilities(requestList: List): List = - // Todo: add about migrate search - requestList.map { - AboutDto( - id = null, - language = it.language, - name = it.name, - description = it.description, - year = null, - createdAt = null, - modifiedAt = null, - locations = it.locations, - imageURL = null, - attachments = listOf() - ).let { dto -> - AboutEntity.of( - AboutPostType.FACILITIES, - LanguageType.makeStringToLanguageType(it.language), - dto - ) - }.also { - syncSearchOfAbout(it) - } - }.let { - aboutRepository.saveAll(it) - }.map { - FacilityDto.of(it) - } - - @Transactional - override fun migrateDirections(requestList: List): List { - // Todo: add about migrate search - val list = mutableListOf() - - for (request in requestList) { - val language = request.language - val name = request.name - val description = request.description - - val aboutDto = AboutDto( - id = null, - language = language, - name = name, - description = description, - year = null, - createdAt = null, - modifiedAt = null, - locations = null, - imageURL = null, - attachments = listOf() - ) - - val languageType = LanguageType.makeStringToLanguageType(language) - var newAbout = AboutEntity.of(AboutPostType.DIRECTIONS, languageType, aboutDto) - syncSearchOfAbout(newAbout) - - newAbout = aboutRepository.save(newAbout) - - list.add(DirectionDto.of(newAbout)) - } - 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/database/ScholarshipLanguageEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipLanguageEntity.kt index f5b78ded..c4be3e5e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipLanguageEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/ScholarshipLanguageEntity.kt @@ -5,11 +5,11 @@ import jakarta.persistence.* @Entity(name = "scholarship_language") class ScholarshipLanguageEntity( - @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) + @OneToOne @JoinColumn(name = "korean_id") val koScholarship: ScholarshipEntity, - @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], orphanRemoval = true) + @OneToOne @JoinColumn(name = "english_id") val enScholarship: ScholarshipEntity ) : BaseTimeEntity() 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 2cfb5182..58b7b57b 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 @@ -411,6 +411,8 @@ class AcademicsServiceImpl( academicsSearch = AcademicsSearchEntity.create(this) } + scholarshipRepository.save(koScholarship) + scholarshipRepository.save(enScholarship) scholarshipLanguageRepository.save(ScholarshipLanguageEntity(koScholarship, enScholarship)) } @@ -465,6 +467,8 @@ class AcademicsServiceImpl( } scholarshipLanguageRepository.delete(scholarshipLanguage!!) + scholarshipRepository.delete(scholarshipLanguage.koScholarship) + scholarshipRepository.delete(scholarshipLanguage.enScholarship) } private fun makeStringToAcademicsStudentType(postType: String): AcademicsStudentType { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/ProfessorController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/ProfessorController.kt index ccd98157..de8c4b53 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/ProfessorController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/ProfessorController.kt @@ -43,9 +43,9 @@ class ProfessorController( @PostMapping(consumes = ["multipart/form-data"]) fun createProfessor( @RequestPart("request") requestBody: CreateProfessorLanguagesReqBody, - @RequestPart("image") image: MultipartFile? + @RequestPart("mainImage") mainImage: MultipartFile? ): ProfessorLanguagesDto = - professorService.createProfessorLanguages(requestBody, image) + professorService.createProfessorLanguages(requestBody, mainImage) @AuthenticatedStaff @PutMapping("/{koProfessorId}/{enProfessorId}", consumes = ["multipart/form-data"]) @@ -57,10 +57,10 @@ class ProfessorController( @RequestPart("request") requestBody: ModifyProfessorLanguagesReqBody, @Parameter(description = "image 교체할 경우 업로드. Request Body의 removeImage 관계없이 변경됨.") - @RequestPart("newImage") - newImage: MultipartFile? + @RequestPart("newMainImage") + newMainImage: MultipartFile? ): ProfessorLanguagesDto = - professorService.updateProfessorLanguages(koProfessorId, enProfessorId, requestBody, newImage) + professorService.updateProfessorLanguages(koProfessorId, enProfessorId, requestBody, newMainImage) @AuthenticatedStaff @DeleteMapping("/{koProfessorId}/{enProfessorId}") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/StaffController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/StaffController.kt index aed58275..9f575bdd 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/StaffController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/v2/StaffController.kt @@ -42,8 +42,8 @@ class StaffController( @PostMapping(consumes = ["multipart/form-data"]) fun createStaff( @RequestPart("request") createStaffLanguagesReqBody: CreateStaffLanguagesReqBody, - @RequestPart("image") image: MultipartFile? - ): StaffLanguagesDto = staffService.createStaffLanguages(createStaffLanguagesReqBody, image) + @RequestPart("mainImage") mainImage: MultipartFile? + ): StaffLanguagesDto = staffService.createStaffLanguages(createStaffLanguagesReqBody, mainImage) @AuthenticatedStaff @PutMapping("/{koStaffId}/{enStaffId}", consumes = ["multipart/form-data"]) @@ -55,10 +55,10 @@ class StaffController( @RequestPart("request") modifyStaffLanguageReq: ModifyStaffLanguagesReqBody, @Parameter(description = "image 교체할 경우 업로드. Request Body의 removeImage 관계없이 변경됨.") - @RequestPart("newImage") - newImage: MultipartFile? + @RequestPart("newMainImage") + newMainImage: MultipartFile? ): StaffLanguagesDto = - staffService.updateStaffLanguages(koStaffId, enStaffId, modifyStaffLanguageReq, newImage) + staffService.updateStaffLanguages(koStaffId, enStaffId, modifyStaffLanguageReq, newMainImage) @AuthenticatedStaff @DeleteMapping("/{koStaffId}/{enStaffId}") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorEventService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorEventService.kt new file mode 100644 index 00000000..44da5b19 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorEventService.kt @@ -0,0 +1,120 @@ +package com.wafflestudio.csereal.core.member.service + +import com.wafflestudio.csereal.core.member.database.MemberSearchEntity +import com.wafflestudio.csereal.core.member.database.ProfessorEntity +import com.wafflestudio.csereal.core.member.database.ProfessorRepository +import com.wafflestudio.csereal.core.research.database.LabRepository +import com.wafflestudio.csereal.core.research.event.LabCreatedEvent +import com.wafflestudio.csereal.core.research.event.LabDeletedEvent +import com.wafflestudio.csereal.core.research.event.LabModifiedEvent +import org.springframework.context.event.EventListener +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface ProfessorEventService { + fun labDeletedEventListener(event: LabDeletedEvent) + fun labModifiedEventListener(event: LabModifiedEvent) + fun labCreatedEventListener(event: LabCreatedEvent) +} + +@Service +class ProfessorEventServiceImpl( + private val professorRepository: ProfessorRepository, + private val labRepository: LabRepository +) : ProfessorEventService { + @EventListener + @Transactional + override fun labCreatedEventListener(event: LabCreatedEvent) { + if (event.professorIds.isEmpty()) { + return + } + + val lab = labRepository.findByIdOrNull(event.id)!! + val professors = professorRepository.findAllById(event.professorIds) + .takeIf { it.size == event.professorIds.size }!! + + // TODO: Consider professor's before lab value + professors.forEach { + it.lab = lab + upsertProfessorSearchIndex(it) + } + } + + @EventListener + @Transactional + override fun labModifiedEventListener(event: LabModifiedEvent) { + val lab = labRepository.findByIdOrNull(event.id)!! + + val oldProfessorIds = event.professorIdsModified.first + val oldProfessors = oldProfessorIds.let { p -> + professorRepository.findAllById(p) + .takeIf { it.size == p.size }!! + } + + val newProfessorIds = event.professorIdsModified.second + val newProfessors = newProfessorIds.let { p -> + professorRepository.findAllById(p) + .takeIf { it.size == p.size }!! + } + + when { + oldProfessors.isEmpty() && newProfessors.isEmpty() -> {} + + oldProfessors.isEmpty() && newProfessors.isNotEmpty() -> { + newProfessors.forEach { + it.lab = lab + upsertProfessorSearchIndex(it) + } + } + + oldProfessors.isNotEmpty() && newProfessors.isEmpty() -> { + oldProfessors.forEach { + it.lab = null + upsertProfessorSearchIndex(it) + } + } + + oldProfessorIds == newProfessorIds -> { + oldProfessors.forEach { upsertProfessorSearchIndex(it) } + } + + else -> { + val removeProfessorIds = oldProfessorIds - newProfessorIds + oldProfessors.forEach { + if (it.id in removeProfessorIds) { + it.lab = null + } + upsertProfessorSearchIndex(it) + } + + val addProfessorIds = newProfessorIds - oldProfessorIds + newProfessors.forEach { + if (it.id in addProfessorIds) { + it.lab = lab + } + upsertProfessorSearchIndex(it) + } + } + } + } + + @EventListener + @Transactional + override fun labDeletedEventListener(event: LabDeletedEvent) { + val lab = labRepository.findByIdOrNull(event.id)!! + val professors = professorRepository.findAllById(event.professorIds) + .takeIf { it.size == event.professorIds.size }!! + + professors.forEach { + it.lab = null + upsertProfessorSearchIndex(it) + } + } + + @Transactional + fun upsertProfessorSearchIndex(professor: ProfessorEntity) { + professor.memberSearch?.update(professor) + ?: let { professor.memberSearch = MemberSearchEntity.create(professor) } + } +} 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 ef79fe9c..781f6598 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 @@ -252,6 +252,7 @@ class ProfessorServiceImpl( ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: $professorId") // Lab 업데이트 + // 기존 연구실이 제거되지 않는 이상 수동으로 교수의 Lab을 제거할 수 없음 val outdatedLabId = professor.lab?.id if (updateReq.labId != null && updateReq.labId != professor.lab?.id) { val lab = labRepository.findByIdOrNull(updateReq.labId) ?: throw CserealException.Csereal404( 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 deleted file mode 100644 index cc3b82fd..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.wafflestudio.csereal.core.research.api - -import com.wafflestudio.csereal.common.aop.AuthenticatedStaff -import com.wafflestudio.csereal.common.enums.LanguageType -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.service.ResearchSearchService -import com.wafflestudio.csereal.core.research.service.ResearchService -import jakarta.validation.Valid -import jakarta.validation.constraints.Positive -import org.springframework.context.annotation.Profile -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.* -import org.springframework.web.multipart.MultipartFile - -@RequestMapping("/api/v1/research") -@RestController -class ResearchController( - private val researchService: ResearchService, - private val researchSearchService: ResearchSearchService -) { - @AuthenticatedStaff - @PostMapping - fun createResearchDetail( - @Valid - @RequestPart("request") - request: ResearchDto, - @RequestPart("mainImage") mainImage: MultipartFile?, - @RequestPart("attachments") attachments: List? - ): ResponseEntity { - return ResponseEntity.ok(researchService.createResearchDetail(request, mainImage, attachments)) - } - - @GetMapping("/groups") - fun readAllResearchGroups( - @RequestParam(required = false, defaultValue = "ko") language: String - ): ResponseEntity { - return ResponseEntity.ok(researchService.readAllResearchGroups(language)) - } - - @GetMapping("/centers") - fun readAllResearchCenters( - @RequestParam(required = false, defaultValue = "ko") language: String - ): ResponseEntity> { - return ResponseEntity.ok(researchService.readAllResearchCenters(language)) - } - - @AuthenticatedStaff - @PatchMapping("/{researchId}") - fun updateResearchDetail( - @PathVariable researchId: Long, - @Valid - @RequestPart("request") - request: ResearchDto, - @RequestPart("mainImage") mainImage: MultipartFile?, - @RequestPart("attachments") attachments: List? - ): ResponseEntity { - return ResponseEntity.ok(researchService.updateResearchDetail(researchId, request, mainImage, attachments)) - } - - @AuthenticatedStaff - @PostMapping("/lab") - fun createLab( - @Valid - @RequestPart("request") - request: LabDto, - @RequestPart("pdf") pdf: MultipartFile? - ): ResponseEntity { - return ResponseEntity.ok(researchService.createLab(request, pdf)) - } - - @GetMapping("/labs") - fun readAllLabs( - @RequestParam(required = false, defaultValue = "ko") language: String - ): ResponseEntity> { - return ResponseEntity.ok(researchService.readAllLabs(language)) - } - - @GetMapping("/lab/{labId}") - fun readLab( - @PathVariable labId: Long - ): ResponseEntity { - return ResponseEntity.ok(researchService.readLab(labId)) - } - - /** - * Research Group 수정은 일단 제외하였음. - */ - @AuthenticatedStaff - @PatchMapping("/lab/{labId}") - fun updateLab( - @PathVariable labId: Long, - @Valid - @RequestPart("request") - request: LabUpdateRequest, - @RequestPart("pdf") pdf: MultipartFile? - ): ResponseEntity { - return ResponseEntity.ok(researchService.updateLab(labId, request, pdf)) - } - - @Profile("!prod") - @PostMapping("/migrate") - fun migrateResearchDetail( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(researchService.migrateResearchDetail(requestList)) - } - - @Profile("!prod") - @PostMapping("/lab/migrate") - fun migrateLabs( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(researchService.migrateLabs(requestList)) - } - - @Profile("!prod") - @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 - ) - ) - } - - @Profile("!prod") - @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) @Valid @Positive number: Int, - @RequestParam(required = true, defaultValue = "ko") language: String, - @RequestParam(required = false, defaultValue = "30") @Valid @Positive amount: Int - ) = researchSearchService.searchTopResearch( - keyword, - LanguageType.makeStringToLanguageType(language), - number, - amount - ) - - @GetMapping("/search") - fun searchPage( - @RequestParam(required = true) keyword: String, - @RequestParam(required = true) pageSize: Int, - @RequestParam(required = true) pageNum: Int, - @RequestParam(required = true, defaultValue = "ko") language: String, - @RequestParam(required = false, defaultValue = "30") @Valid @Positive amount: Int - ) = researchSearchService.searchResearch( - keyword, - LanguageType.makeStringToLanguageType(language), - pageSize, - pageNum, - amount - ) -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/CreateLabLanguageReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/CreateLabLanguageReqBody.kt new file mode 100644 index 00000000..4f57fd4c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/CreateLabLanguageReqBody.kt @@ -0,0 +1,18 @@ +package com.wafflestudio.csereal.core.research.api.req + +data class CreateLabLanguageReqBody( + val ko: CreateLabReqBody, + val en: CreateLabReqBody +) + +data class CreateLabReqBody( + val name: String, + val description: String?, + val groupId: Long?, + val professorIds: Set, + val location: String?, + val tel: String?, + val acronym: String?, + val youtube: String?, + val websiteURL: String? +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/CreateResearchLanguageReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/CreateResearchLanguageReqBody.kt new file mode 100644 index 00000000..327b8054 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/CreateResearchLanguageReqBody.kt @@ -0,0 +1,44 @@ +package com.wafflestudio.csereal.core.research.api.req + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.wafflestudio.csereal.core.research.type.ResearchType + +data class CreateResearchLanguageReqBody( + val ko: CreateResearchSealedReqBody, + val en: CreateResearchSealedReqBody +) { + fun valid() = ko.type == en.type + fun valid(type: ResearchType) = ko.valid(type) && en.valid(type) +} + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = CreateResearchGroupReqBody::class, names = ["GROUPS", "groups"]), + JsonSubTypes.Type(value = CreateResearchCenterReqBody::class, names = ["CENTERS", "centers"]) +) +sealed class CreateResearchSealedReqBody( + val type: ResearchType, + open val name: String, + open val description: String, + open val mainImageUrl: String? +) { + fun valid(type: ResearchType) = this.type == type +} + +data class CreateResearchGroupReqBody( + override val name: String, + override val description: String, + override val mainImageUrl: String? +) : CreateResearchSealedReqBody(ResearchType.GROUPS, name, description, mainImageUrl) + +data class CreateResearchCenterReqBody( + override val name: String, + override val description: String, + override val mainImageUrl: String?, + val websiteURL: String? +) : CreateResearchSealedReqBody(ResearchType.CENTERS, name, description, mainImageUrl) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/ModifyLabLanguageReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/ModifyLabLanguageReqBody.kt new file mode 100644 index 00000000..2f7607c9 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/ModifyLabLanguageReqBody.kt @@ -0,0 +1,19 @@ +package com.wafflestudio.csereal.core.research.api.req + +data class ModifyLabLanguageReqBody( + val ko: ModifyLabReqBody, + val en: ModifyLabReqBody +) + +data class ModifyLabReqBody( + val name: String, + val description: String?, + val location: String?, + val tel: String?, + val acronym: String?, + val youtube: String?, + val websiteURL: String?, + val groupId: Long?, + val professorIds: Set, + val removePdf: Boolean +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/ModifyResearchLanguageReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/ModifyResearchLanguageReqBody.kt new file mode 100644 index 00000000..a0f1d760 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/req/ModifyResearchLanguageReqBody.kt @@ -0,0 +1,41 @@ +package com.wafflestudio.csereal.core.research.api.req + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.wafflestudio.csereal.core.research.type.ResearchType + +data class ModifyResearchLanguageReqBody( + val ko: ModifyResearchSealedReqBody, + val en: ModifyResearchSealedReqBody +) { + fun valid() = ko.type == en.type +} + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type" +) +@JsonSubTypes( + JsonSubTypes.Type(value = ModifyResearchGroupReqBody::class, names = ["GROUPS", "groups"]), + JsonSubTypes.Type(value = ModifyResearchCenterReqBody::class, names = ["CENTERS", "centers"]) +) +sealed class ModifyResearchSealedReqBody( + val type: ResearchType, + open val name: String, + open val description: String, + open val removeImage: Boolean +) + +data class ModifyResearchGroupReqBody( + override val name: String, + override val description: String, + override val removeImage: Boolean +) : ModifyResearchSealedReqBody(ResearchType.GROUPS, name, description, removeImage) + +data class ModifyResearchCenterReqBody( + override val name: String, + override val description: String, + override val removeImage: Boolean, + val websiteURL: String? +) : ModifyResearchSealedReqBody(ResearchType.CENTERS, name, description, removeImage) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/res/ResearchSearchResElement.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/res/ResearchSearchResElement.kt index a5874bfa..98e89c92 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/res/ResearchSearchResElement.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/res/ResearchSearchResElement.kt @@ -3,15 +3,14 @@ package com.wafflestudio.csereal.core.research.api.res import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.common.utils.substringAroundKeyword -import com.wafflestudio.csereal.core.research.database.ResearchPostType import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity -import com.wafflestudio.csereal.core.research.database.ResearchSearchType +import com.wafflestudio.csereal.core.research.type.ResearchRelatedType data class ResearchSearchResElement( val id: Long, val language: String, val name: String, - val researchType: ResearchSearchType, + val researchType: ResearchRelatedType, val partialDescription: String, val boldStartIdx: Int, val boldEndIdx: Int @@ -36,10 +35,7 @@ data class ResearchSearchResElement( id = it.id, name = it.name, language = it.language.let { ln -> LanguageType.makeLowercase(ln) }, - researchType = when (it.postType) { - ResearchPostType.GROUPS -> ResearchSearchType.RESEARCH_GROUP - ResearchPostType.CENTERS -> ResearchSearchType.RESEARCH_CENTER - }, + researchType = it.postType.ofResearchRelatedType(), partialDescription = partialDesc, boldStartIdx = startIdx ?: 0, boldEndIdx = startIdx?.plus(keyword.length) ?: 0 @@ -59,7 +55,7 @@ data class ResearchSearchResElement( id = it.id, name = it.name, language = it.language.let { ln -> LanguageType.makeLowercase(ln) }, - researchType = ResearchSearchType.LAB, + researchType = ResearchRelatedType.LAB, partialDescription = partialDesc, boldStartIdx = startIdx ?: 0, boldEndIdx = startIdx?.plus(keyword.length) ?: 0 @@ -79,7 +75,7 @@ data class ResearchSearchResElement( id = it.id, name = it.name, language = it.language.let { ln -> LanguageType.makeLowercase(ln) }, - researchType = ResearchSearchType.CONFERENCE, + researchType = ResearchRelatedType.CONFERENCE, partialDescription = partialDesc, boldStartIdx = startIdx ?: 0, boldEndIdx = startIdx?.plus(keyword.length) ?: 0 diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/v1/ResearchController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/v1/ResearchController.kt new file mode 100644 index 00000000..06cab295 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/v1/ResearchController.kt @@ -0,0 +1,78 @@ +package com.wafflestudio.csereal.core.research.api.v1 + +import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.research.dto.LabDto +import com.wafflestudio.csereal.core.research.dto.ResearchDto +import com.wafflestudio.csereal.core.research.dto.ResearchGroupResponse +import com.wafflestudio.csereal.core.research.service.LabService +import com.wafflestudio.csereal.core.research.service.ResearchSearchService +import com.wafflestudio.csereal.core.research.service.ResearchService +import jakarta.validation.Valid +import jakarta.validation.constraints.Positive +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RequestMapping("/api/v1/research") +@RestController("ResearchControllerV1") +@Deprecated(message = "Use V2 API") +class ResearchController( + private val researchService: ResearchService, + private val researchSearchService: ResearchSearchService, + private val labService: LabService +) { + @GetMapping("/groups") + fun readAllResearchGroups( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity { + return ResponseEntity.ok(researchService.readAllResearchGroupsDeprecated(language)) + } + + @GetMapping("/centers") + fun readAllResearchCenters( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity> { + return ResponseEntity.ok(researchService.readAllResearchCentersDeprecated(language)) + } + + @GetMapping("/labs") + fun readAllLabs( + @RequestParam(required = false, defaultValue = "ko") language: String + ): ResponseEntity> { + return ResponseEntity.ok(labService.readAllLabs(language)) + } + + @GetMapping("/lab/{labId}") + fun readLab( + @PathVariable labId: Long + ): ResponseEntity { + return ResponseEntity.ok(labService.readLab(labId)) + } + + @GetMapping("/search/top") + fun searchTop( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true) @Valid @Positive number: Int, + @RequestParam(required = true, defaultValue = "ko") language: String, + @RequestParam(required = false, defaultValue = "30") @Valid @Positive amount: Int + ) = researchSearchService.searchTopResearch( + keyword, + LanguageType.makeStringToLanguageType(language), + number, + amount + ) + + @GetMapping("/search") + fun searchPage( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true) pageSize: Int, + @RequestParam(required = true) pageNum: Int, + @RequestParam(required = true, defaultValue = "ko") language: String, + @RequestParam(required = false, defaultValue = "30") @Valid @Positive amount: Int + ) = researchSearchService.searchResearch( + keyword, + LanguageType.makeStringToLanguageType(language), + pageSize, + pageNum, + amount + ) +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/v2/ResearchController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/v2/ResearchController.kt new file mode 100644 index 00000000..619cffb6 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/v2/ResearchController.kt @@ -0,0 +1,158 @@ +package com.wafflestudio.csereal.core.research.api.v2 + +import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.research.api.req.CreateLabLanguageReqBody +import com.wafflestudio.csereal.core.research.api.req.CreateResearchLanguageReqBody +import com.wafflestudio.csereal.core.research.api.req.ModifyLabLanguageReqBody +import com.wafflestudio.csereal.core.research.api.req.ModifyResearchLanguageReqBody +import com.wafflestudio.csereal.core.research.dto.* +import com.wafflestudio.csereal.core.research.service.LabService +import com.wafflestudio.csereal.core.research.service.ResearchSearchService +import com.wafflestudio.csereal.core.research.service.ResearchService +import com.wafflestudio.csereal.core.research.type.ResearchType +import io.swagger.v3.oas.annotations.Parameter +import jakarta.validation.Valid +import jakarta.validation.constraints.Positive +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile + +@RequestMapping("/api/v2/research") +@RestController +class ResearchController( + private val researchService: ResearchService, + private val labService: LabService, + private val researchSearchService: ResearchSearchService +) { + // Research APIs + + @GetMapping("/{researchId:[0-9]+}") + fun readResearch( + @Positive + @PathVariable(required = true) + researchId: Long + ): ResearchLanguageDto { + return researchService.readResearchLanguage(researchId) + } + + @GetMapping("/{researchType:[a-z A-Z]+}") + fun readAllResearch( + @PathVariable(required = true) researchType: String, + @RequestParam(required = false, defaultValue = "ko") language: String + ): List { + val researchTypeEnum = ResearchType.fromJsonValue(researchType) + val languageEnum = LanguageType.makeStringToLanguageType(language) + return researchService.readAllResearch(languageEnum, researchTypeEnum) + } + + @AuthenticatedStaff + @PostMapping(consumes = ["multipart/form-data"]) + fun createResearchGroup( + @RequestPart("request") request: CreateResearchLanguageReqBody, + @RequestPart("mainImage") mainImage: MultipartFile? + ): ResearchLanguageDto = researchService.createResearchLanguage(request, mainImage) + + @AuthenticatedStaff + @PutMapping("/{koreanId}/{englishId}", consumes = ["multipart/form-data"]) + fun updateResearch( + @PathVariable @Positive + koreanId: Long, + @PathVariable @Positive + englishId: Long, + @RequestPart("request") request: ModifyResearchLanguageReqBody, + + @Parameter(description = "image 교체할 경우 업로드. Request Body의 removeImage 관계없이 변경됨.") + @RequestPart("newMainImage") + newMainImage: MultipartFile? + ): ResearchLanguageDto { + return researchService.updateResearchLanguage(koreanId, englishId, request, newMainImage) + } + + @AuthenticatedStaff + @DeleteMapping("/{koreanId}/{englishId}") + fun deleteResearch( + @PathVariable @Positive + koreanId: Long, + @PathVariable @Positive + englishId: Long + ) { + researchService.deleteResearchLanguage(koreanId, englishId) + } + + // Lab APIs + + @GetMapping("/lab") + fun readAllLabs( + @RequestParam(required = false, defaultValue = "ko") language: String + ): List = labService.readAllLabs(language) + + // TODO: Change to Language Unified API + @GetMapping("/lab/{labId}") + fun readLab( + @PathVariable labId: Long + ): LabLanguageDto = labService.readLabLanguage(labId) + + @AuthenticatedStaff + @PostMapping("/lab", consumes = ["multipart/form-data"]) + fun createLab( + @Valid + @RequestPart("request") + request: CreateLabLanguageReqBody, + + @RequestPart("pdf") pdf: MultipartFile? + ): LabLanguageDto = labService.createLabLanguage(request, pdf) + + @AuthenticatedStaff + @PutMapping("/lab/{koreanLabId}/{englishLabId}", consumes = ["multipart/form-data"]) + fun updateLab( + @PathVariable @Positive + koreanLabId: Long, + @PathVariable @Positive + englishLabId: Long, + @Valid + @RequestPart("request") + request: ModifyLabLanguageReqBody, + @RequestPart("pdf") pdf: MultipartFile? + ): LabLanguageDto = labService.updateLabLanguage(koreanLabId, englishLabId, request, pdf) + + @AuthenticatedStaff + @DeleteMapping("/lab/{koreanLabId}/{englishLabId}") + fun deleteLab( + @PathVariable @Positive + koreanLabId: Long, + @PathVariable @Positive + englishLabId: Long + ) { + labService.deleteLabLanguage(koreanLabId, englishLabId) + } + + // Search APIs + + @GetMapping("/search/top") + fun searchTop( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true) @Valid @Positive number: Int, + @RequestParam(required = true, defaultValue = "ko") language: String, + @RequestParam(required = false, defaultValue = "30") @Valid @Positive amount: Int + ) = researchSearchService.searchTopResearch( + keyword, + LanguageType.makeStringToLanguageType(language), + number, + amount + ) + + @GetMapping("/search") + fun searchPage( + @RequestParam(required = true) keyword: String, + @RequestParam(required = true) pageSize: Int, + @RequestParam(required = true) pageNum: Int, + @RequestParam(required = true, defaultValue = "ko") language: String, + @RequestParam(required = false, defaultValue = "30") @Valid @Positive amount: Int + ) = researchSearchService.searchResearch( + keyword, + LanguageType.makeStringToLanguageType(language), + pageSize, + pageNum, + amount + ) +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt index 6a77b365..86a1d24d 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt @@ -3,7 +3,6 @@ package com.wafflestudio.csereal.core.research.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.core.member.database.ProfessorEntity -import com.wafflestudio.csereal.core.research.dto.LabDto import com.wafflestudio.csereal.core.research.dto.LabUpdateRequest import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity import jakarta.persistence.* @@ -11,48 +10,36 @@ import jakarta.persistence.* @Entity(name = "lab") class LabEntity( var language: LanguageType, + var name: String, - @OneToMany(mappedBy = "lab") - val professors: MutableSet = mutableSetOf(), + @Column(columnDefinition = "mediumText") + var description: String?, - var location: String?, - var tel: String?, var acronym: String?, - @OneToOne - var pdf: AttachmentEntity? = null, + var location: String?, + + var websiteURL: String?, + + var tel: String?, var youtube: String?, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "research_id") - var research: ResearchEntity, + var research: ResearchEntity? = null, - @Column(columnDefinition = "mediumText") - var description: String?, - var websiteURL: String?, + @OneToOne + var pdf: AttachmentEntity? = null, + + @OneToMany(mappedBy = "lab") + var professors: MutableSet = mutableSetOf(), @OneToOne(mappedBy = "lab", cascade = [CascadeType.ALL], orphanRemoval = true) var researchSearch: ResearchSearchEntity? = null ) : BaseTimeEntity() { - companion object { - fun of(languageType: LanguageType, labDto: LabDto, researchGroup: ResearchEntity): LabEntity { - return LabEntity( - language = languageType, - name = labDto.name, - location = labDto.location, - tel = labDto.tel, - acronym = labDto.acronym, - youtube = labDto.youtube, - research = researchGroup, - description = labDto.description, - websiteURL = labDto.websiteURL - ) - } - } - fun updateWithoutProfessor(labUpdateRequest: LabUpdateRequest) { this.name = labUpdateRequest.name this.location = labUpdateRequest.location diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt index 17294647..eccfe488 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt @@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository interface LabRepository : JpaRepository { fun findAllByLanguageOrderByName(languageType: LanguageType): List - fun findByName(name: String): LabEntity? + fun findByIdAndLanguage(id: Long, languageType: LanguageType): LabEntity? } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt index e7911d6f..40a18422 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt @@ -1,43 +1,38 @@ package com.wafflestudio.csereal.core.research.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.enums.LanguageType import com.wafflestudio.csereal.core.research.dto.ResearchDto -import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity +import com.wafflestudio.csereal.core.research.type.ResearchType import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import jakarta.persistence.* @Entity(name = "research") class ResearchEntity( @Enumerated(EnumType.STRING) - var postType: ResearchPostType, + val postType: ResearchType, @Enumerated(EnumType.STRING) - var language: LanguageType, + val language: LanguageType, var name: String, @Column(columnDefinition = "mediumText") var description: String?, - var websiteURL: String?, + var websiteURL: String? = null, - @OneToMany(mappedBy = "research", cascade = [CascadeType.ALL], orphanRemoval = true) - var labs: MutableList = mutableListOf(), + @OneToMany(mappedBy = "research", cascade = [CascadeType.PERSIST]) + var labs: MutableSet = mutableSetOf(), @OneToOne var mainImage: MainImageEntity? = null, - @OneToMany(mappedBy = "research", cascade = [CascadeType.ALL], orphanRemoval = true) - var attachments: MutableList = mutableListOf(), - @OneToOne(mappedBy = "research", cascade = [CascadeType.ALL], orphanRemoval = true) var researchSearch: ResearchSearchEntity? = null -) : BaseTimeEntity(), MainImageContentEntityType, AttachmentContentEntityType { +) : BaseTimeEntity(), MainImageContentEntityType { override fun bringMainImage() = mainImage - override fun bringAttachments() = attachments companion object { fun of(languageType: LanguageType, researchDto: ResearchDto): ResearchEntity { @@ -50,10 +45,4 @@ class ResearchEntity( ) } } - - fun updateWithoutLabImageAttachment(researchDto: ResearchDto) { - this.postType = researchDto.postType - this.name = researchDto.name - this.description = researchDto.description - } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchLanguageEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchLanguageEntity.kt new file mode 100644 index 00000000..992faadf --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchLanguageEntity.kt @@ -0,0 +1,23 @@ +package com.wafflestudio.csereal.core.research.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.research.type.ResearchRelatedType +import jakarta.persistence.* + +@Entity(name = "research_language") +@Table( + uniqueConstraints = [ + UniqueConstraint(columnNames = ["korean_id", "english_id", "type"]) + ] +) +class ResearchLanguageEntity( + @Column(nullable = false) + val koreanId: Long, + + @Column(nullable = false) + val englishId: Long, + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + val type: ResearchRelatedType +) : BaseTimeEntity() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchLanguageRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchLanguageRepository.kt new file mode 100644 index 00000000..4e64de5b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchLanguageRepository.kt @@ -0,0 +1,81 @@ +package com.wafflestudio.csereal.core.research.database + +import com.querydsl.core.Tuple +import com.querydsl.jpa.impl.JPAQueryFactory +import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.research.database.QResearchLanguageEntity.researchLanguageEntity +import com.wafflestudio.csereal.core.research.type.ResearchRelatedType +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +interface ResearchLanguageRepository : JpaRepository, ResearchLanguageCustomRepository { + fun existsByKoreanIdAndEnglishIdAndType(koreanId: Long, englishId: Long, type: ResearchRelatedType): Boolean + fun findByKoreanIdAndEnglishIdAndType( + koreanId: Long, + englishId: Long, + type: ResearchRelatedType + ): ResearchLanguageEntity? +} + +interface ResearchLanguageCustomRepository { + fun findResearchPairById(id: Long): Map? + fun findLabPairById(id: Long): Map? +} + +@Repository +class ResearchLanguageCustomRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : ResearchLanguageCustomRepository { + override fun findResearchPairById(id: Long): Map? { + val ko = QResearchEntity("ko") + val en = QResearchEntity("en") + + val tuple = queryFactory.select(ko, en) + .from(researchLanguageEntity) + .join(ko).on(researchLanguageEntity.koreanId.eq(ko.id)) + .join(en).on(researchLanguageEntity.englishId.eq(en.id)) + .where( + researchLanguageEntity.type.`in`( + listOf( + ResearchRelatedType.RESEARCH_GROUP, + ResearchRelatedType.RESEARCH_CENTER + ) + ), + researchLanguageEntity.koreanId.eq(id).or( + researchLanguageEntity.englishId.eq(id) + ) + ).fetchOne() + + return tuple?.let { + mapOf( + LanguageType.KO to it[ko]!!, + LanguageType.EN to it[en]!! + ) + } + } + + override fun findLabPairById(id: Long): Map? { + val ko = QLabEntity("ko") + val en = QLabEntity("en") + + val tuple: Tuple? = queryFactory.select(ko, en) + .from(researchLanguageEntity) + .join(ko).on(researchLanguageEntity.koreanId.eq(ko.id)) + .join(en).on(researchLanguageEntity.englishId.eq(en.id)) + .where( + researchLanguageEntity.type.eq( + ResearchRelatedType.LAB + ), + researchLanguageEntity.koreanId.eq(id).or( + researchLanguageEntity.englishId.eq(id) + ) + ).fetchOne() + + return tuple?.let { + mapOf( + LanguageType.KO to it[ko]!!, + LanguageType.EN to it[en]!! + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt deleted file mode 100644 index 4e5d6447..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.wafflestudio.csereal.core.research.database - -enum class ResearchPostType( - val krName: String -) { - GROUPS("연구 그룹"), - CENTERS("연구 센터"); -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt index 6a22d921..8e803b49 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt @@ -1,12 +1,14 @@ package com.wafflestudio.csereal.core.research.database import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.research.type.ResearchType import org.springframework.data.jpa.repository.JpaRepository interface ResearchRepository : JpaRepository { - fun findByName(name: String): ResearchEntity? fun findAllByPostTypeAndLanguageOrderByName( - postType: ResearchPostType, + postType: ResearchType, languageType: LanguageType ): List + + fun findByIdAndPostType(id: Long, postType: ResearchType): ResearchEntity? } 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 397c73c5..cf9cd6bb 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 @@ -68,7 +68,7 @@ class ResearchSearchEntity( lab.tel?.let { appendLine(it) } lab.acronym?.let { appendLine(it) } lab.youtube?.let { appendLine(it) } - appendLine(lab.research.name) + lab.research?.let { appendLine(it.name) } lab.description?.let { appendLine(cleanTextFromHtml(it)) } @@ -106,10 +106,3 @@ class ResearchSearchEntity( this.content = createContent(conference) } } - -enum class ResearchSearchType { - RESEARCH_GROUP, - RESEARCH_CENTER, - LAB, - CONFERENCE; -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt index 74ed33d7..b0142310 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt @@ -8,13 +8,13 @@ data class LabDto( val id: Long, val language: String, val name: String, - val professors: List?, + val professors: List, val location: String?, val tel: String?, val acronym: String?, val pdf: AttachmentResponse?, val youtube: String?, - val group: String, + val group: String?, val description: String?, val websiteURL: String? ) { @@ -30,7 +30,7 @@ data class LabDto( acronym = this.acronym, pdf = pdf, youtube = this.youtube, - group = this.research.name, + group = this.research?.name, description = this.description, websiteURL = this.websiteURL ) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabLanguageDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabLanguageDto.kt new file mode 100644 index 00000000..e7e4348d --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabLanguageDto.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.research.dto + +data class LabLanguageDto( + val ko: LabDto, + val en: LabDto +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt index 77e6ffb4..352aef0f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt @@ -2,13 +2,13 @@ package com.wafflestudio.csereal.core.research.dto import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.core.research.database.ResearchEntity -import com.wafflestudio.csereal.core.research.database.ResearchPostType +import com.wafflestudio.csereal.core.research.type.ResearchType import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentResponse import java.time.LocalDateTime data class ResearchDto( val id: Long, - val postType: ResearchPostType, + val postType: ResearchType, val language: String, val name: String, val description: String?, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchLanguageDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchLanguageDto.kt new file mode 100644 index 00000000..3043258e --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchLanguageDto.kt @@ -0,0 +1,71 @@ +package com.wafflestudio.csereal.core.research.dto + +import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.research.database.ResearchEntity +import com.wafflestudio.csereal.core.research.type.ResearchType + +data class ResearchLanguageDto( + val ko: ResearchSealedDto, + val en: ResearchSealedDto +) { + fun valid() = ko.type == en.type + fun valid(researchType: ResearchType) = ko.valid(researchType) && en.valid(researchType) +} + +sealed class ResearchSealedDto( + val type: ResearchType, + open val id: Long, + open val language: LanguageType, + open val name: String, + open val description: String, + open val mainImageUrl: String? +) { + fun valid(researchType: ResearchType) = this.type == researchType + + companion object { + fun of(entity: ResearchEntity, imageUrl: String?) = when (entity.postType) { + ResearchType.GROUPS -> ResearchGroupDto.of(entity, imageUrl) + ResearchType.CENTERS -> ResearchCenterDto.of(entity, imageUrl) + } + } +} + +data class ResearchGroupDto( + override val id: Long, + override val language: LanguageType, + override val name: String, + override val description: String, + override val mainImageUrl: String?, + val labs: List +) : ResearchSealedDto(ResearchType.GROUPS, id, language, name, description, mainImageUrl) { + companion object { + fun of(entity: ResearchEntity, imageUrl: String?) = ResearchGroupDto( + id = entity.id, + language = entity.language, + name = entity.name, + description = entity.description!!, + mainImageUrl = imageUrl, + labs = entity.labs.map { ResearchLabResponse(it.id, it.name) } + ) + } +} + +data class ResearchCenterDto( + override val id: Long, + override val language: LanguageType, + override val name: String, + override val description: String, + override val mainImageUrl: String?, + val websiteURL: String? +) : ResearchSealedDto(ResearchType.CENTERS, id, language, name, description, mainImageUrl) { + companion object { + fun of(entity: ResearchEntity, imageUrl: String?) = ResearchCenterDto( + id = entity.id, + language = entity.language, + name = entity.name, + description = entity.description!!, + mainImageUrl = imageUrl, + websiteURL = entity.websiteURL + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabCreatedEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabCreatedEvent.kt new file mode 100644 index 00000000..914ecfe1 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabCreatedEvent.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.research.event + +data class LabCreatedEvent( + val id: Long, + val researchId: Long?, + val professorIds: Set +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabDeletedEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabDeletedEvent.kt new file mode 100644 index 00000000..95dd8ad8 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabDeletedEvent.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.research.event + +data class LabDeletedEvent( + val id: Long, + val researchId: Long?, + val professorIds: Set +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabModifiedEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabModifiedEvent.kt new file mode 100644 index 00000000..10186fe3 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/event/LabModifiedEvent.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.research.event + +data class LabModifiedEvent( + val id: Long, + val researchIdModified: Pair, + val professorIdsModified: Pair, Set> +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/LabEventService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/LabEventService.kt new file mode 100644 index 00000000..7c40f02b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/LabEventService.kt @@ -0,0 +1,81 @@ +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.LabEntity +import com.wafflestudio.csereal.core.research.database.LabRepository +import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity +import org.springframework.context.event.EventListener +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface LabEventService { + fun professorCreatedEventListener(professorCreatedEvent: ProfessorCreatedEvent) + fun professorDeletedEventListener(professorDeletedEvent: ProfessorDeletedEvent) + fun professorModifiedEventListener(professorModifiedEvent: ProfessorModifiedEvent) +} + +@Service +class LabEventServiceImpl( + private val labRepository: LabRepository +) : LabEventService { + @EventListener + @Transactional + override fun professorCreatedEventListener(professorCreatedEvent: ProfessorCreatedEvent) { + val lab = professorCreatedEvent.labId?.let { + labRepository.findByIdOrNull(it) + } ?: return + + upsertLabSearchIndex(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 + upsertLabSearchIndex(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?.apply { + // if lab still has professor, remove it + professors.removeIf { it.id == professorModifiedEvent.id } + }?.let { + upsertLabSearchIndex(it) + } + + afterLab?.let { + upsertLabSearchIndex(it) + } + } + + @Transactional + fun upsertLabSearchIndex(lab: LabEntity) { + lab.researchSearch?.update(lab) ?: let { + lab.researchSearch = ResearchSearchEntity.create(lab) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/LabService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/LabService.kt new file mode 100644 index 00000000..5fb3e562 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/LabService.kt @@ -0,0 +1,293 @@ +package com.wafflestudio.csereal.core.research.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.common.properties.EndpointProperties +import com.wafflestudio.csereal.common.utils.startsWithEnglish +import com.wafflestudio.csereal.core.member.database.ProfessorRepository +import com.wafflestudio.csereal.core.research.api.req.CreateLabLanguageReqBody +import com.wafflestudio.csereal.core.research.api.req.CreateLabReqBody +import com.wafflestudio.csereal.core.research.api.req.ModifyLabLanguageReqBody +import com.wafflestudio.csereal.core.research.api.req.ModifyLabReqBody +import com.wafflestudio.csereal.core.research.database.* +import com.wafflestudio.csereal.core.research.dto.LabDto +import com.wafflestudio.csereal.core.research.dto.LabLanguageDto +import com.wafflestudio.csereal.core.research.event.LabCreatedEvent +import com.wafflestudio.csereal.core.research.event.LabDeletedEvent +import com.wafflestudio.csereal.core.research.event.LabModifiedEvent +import com.wafflestudio.csereal.core.research.type.ResearchRelatedType +import com.wafflestudio.csereal.core.research.type.ResearchType +import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity +import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService +import org.springframework.context.ApplicationEventPublisher +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +interface LabService { + fun readLabLanguage(labId: Long): LabLanguageDto + fun readLab(labId: Long): LabDto + fun readAllLabs(language: String): List + + fun createLab(language: LanguageType, request: CreateLabReqBody, pdf: MultipartFile?): LabDto + fun createLabLanguage(request: CreateLabLanguageReqBody, pdf: MultipartFile?): LabLanguageDto + + fun updateLabLanguage( + koreanId: Long, + englishId: Long, + request: ModifyLabLanguageReqBody, + pdf: MultipartFile? + ): LabLanguageDto + + fun updateLab(language: LanguageType, labId: Long, request: ModifyLabReqBody, pdf: MultipartFile?): LabDto + + fun deleteLabLanguage(koreanId: Long, englishId: Long) + fun deleteLab(id: Long) +} + +@Service +class LabServiceImpl( + private val attachmentService: AttachmentService, + private val researchSearchService: ResearchSearchService, + private val labRepository: LabRepository, + private val researchLanguageRepository: ResearchLanguageRepository, + private val researchRepository: ResearchRepository, + private val professorRepository: ProfessorRepository, + private val endpointProperties: EndpointProperties, + private val applicationEventPublisher: ApplicationEventPublisher +) : LabService { + // TODO: Solve N+1 Problem + @Transactional(readOnly = true) + override fun readAllLabs(language: String): List { + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + val labs = labRepository.findAllByLanguageOrderByName(enumLanguageType).map { + val attachmentResponse = + attachmentService.createOneAttachmentResponse(it.pdf) + LabDto.of(it, attachmentResponse) + }.sortedWith { a, b -> + when { + startsWithEnglish(a.name) && !startsWithEnglish(b.name) -> 1 + !startsWithEnglish(a.name) && startsWithEnglish(b.name) -> -1 + else -> a.name.compareTo(b.name) + } + } + + return labs + } + + @Transactional(readOnly = true) + override fun readLabLanguage(labId: Long): LabLanguageDto { + val labMap = researchLanguageRepository.findLabPairById(labId) + ?.takeIf { it.isNotEmpty() } + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") + + val ko = labMap[LanguageType.KO]!! + val en = labMap[LanguageType.EN]!! + + val koAttachmentResponse = attachmentService.createOneAttachmentResponse(ko.pdf) + val enAttachmentResponse = attachmentService.createOneAttachmentResponse(en.pdf) + + return LabLanguageDto( + LabDto.of(ko, koAttachmentResponse), + LabDto.of(en, enAttachmentResponse) + ) + } + + @Transactional(readOnly = true) + override fun readLab(labId: Long): LabDto { + val lab = labRepository.findByIdOrNull(labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") + + val attachmentResponse = + attachmentService.createOneAttachmentResponse(lab.pdf) + + return LabDto.of(lab, attachmentResponse) + } + + private fun createPdfURL(pdf: AttachmentEntity): String { + return "${endpointProperties.backend}/v1/file/${pdf.filename}" + } + + @Transactional + override fun createLabLanguage(request: CreateLabLanguageReqBody, pdf: MultipartFile?): LabLanguageDto { + val koLabDto = createLab(LanguageType.KO, request.ko, pdf) + val enLabDto = createLab(LanguageType.EN, request.en, pdf) + + researchLanguageRepository.save( + ResearchLanguageEntity( + koreanId = koLabDto.id, + englishId = enLabDto.id, + type = ResearchRelatedType.LAB + ) + ) + + return LabLanguageDto(koLabDto, enLabDto) + } + + @Transactional + override fun createLab(language: LanguageType, request: CreateLabReqBody, pdf: MultipartFile?): LabDto { + val researchGroup = request.groupId?.let { + researchRepository.findByIdOrNull(request.groupId) + ?: throw CserealException.Csereal404("해당 연구그룹을 찾을 수 없습니다.(researchGroupId = $it)") + }?.apply { + if (this.postType != ResearchType.GROUPS) { + throw CserealException.Csereal404("해당 id 연구그룹이 아닙니다.(researchGroupId = ${this.id})") + } + } + + val professors = professorRepository.findAllById(request.professorIds) + .also { + if (it.size < request.professorIds.size) { + throw CserealException.Csereal404("해당 교수님들을 찾을 수 없습니다.(professorIds = ${request.professorIds})") + } + if (it.any { p -> p.lab != null }) { + throw CserealException.Csereal400("이미 다른 연구실에 속한 교수님이 존재합니다.") + } + } + + val newLab = LabEntity( + language = language, + name = request.name, + description = request.description, + acronym = request.acronym, + location = request.location, + websiteURL = request.websiteURL, + tel = request.tel, + youtube = request.youtube, + research = researchGroup, + professors = professors.toMutableSet() + ).apply { + pdf?.let { + attachmentService.uploadAttachmentInLabEntity(this, it) + } + }.also { + upsertLabSearchIndex(it) + } + + val newSavedLab = labRepository.save(newLab) + + applicationEventPublisher.publishEvent( + LabCreatedEvent( + newSavedLab.id, + request.groupId, + request.professorIds + ) + ) + + return LabDto.of(newSavedLab, attachmentService.createOneAttachmentResponse(newSavedLab.pdf)) + } + + @Transactional + override fun updateLabLanguage( + koreanId: Long, + englishId: Long, + request: ModifyLabLanguageReqBody, + pdf: MultipartFile? + ): LabLanguageDto { + val koLabDto = updateLab(LanguageType.KO, koreanId, request.ko, pdf) + val enLabDto = updateLab(LanguageType.EN, englishId, request.en, pdf) + + return LabLanguageDto(koLabDto, enLabDto) + } + + @Transactional + override fun updateLab( + language: LanguageType, + labId: Long, + request: ModifyLabReqBody, + pdf: MultipartFile? + ): LabDto { + val labEntity = labRepository.findByIdAndLanguage(labId, language) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") + + val oldGroup = labEntity.research + val newGroup = request.groupId?.let { + researchRepository.findByIdAndPostType(it, ResearchType.GROUPS) + ?: throw CserealException.Csereal404("해당 연구그룹을 찾을 수 없습니다.(researchGroupId = $it)") + } + + val oldProfessors = labEntity.professors + val newProfessors = professorRepository.findAllById(request.professorIds) + .also { + if (it.size < request.professorIds.size) { + throw CserealException.Csereal404("해당 교수님들을 찾을 수 없습니다.(professorIds = ${request.professorIds})") + } + + if (!(it.all { p -> p.lab == null || p.lab!!.id == labId })) { + throw CserealException.Csereal400("이미 다른 연구실에 속한 교수님이 존재합니다.") + } + } + + labEntity.apply { + name = request.name + description = request.description + location = request.location + tel = request.tel + acronym = request.acronym + youtube = request.youtube + websiteURL = request.websiteURL + research = newGroup + professors = newProfessors.toMutableSet() + } + + // update pdf + if ((pdf != null || request.removePdf) && (labEntity.pdf != null)) { + attachmentService.deleteAttachment(labEntity.pdf!!) + } + pdf?.let { + attachmentService.uploadAttachmentInLabEntity(labEntity, it) + } + + // update researchSearch + upsertLabSearchIndex(labEntity) + + applicationEventPublisher.publishEvent( + LabModifiedEvent( + labId, + oldGroup?.id to newGroup?.id, + oldProfessors.map { it.id }.toSet() to request.professorIds + ) + ) + + return LabDto.of(labEntity, attachmentService.createOneAttachmentResponse(labEntity.pdf)) + } + + @Transactional + override fun deleteLabLanguage(koreanId: Long, englishId: Long) { + val labLanguage = researchLanguageRepository.findByKoreanIdAndEnglishIdAndType( + koreanId, + englishId, + ResearchRelatedType.LAB + ) ?: throw CserealException.Csereal404("해당 연구실 언어 쌍을 찾을 수 없습니다.: koreanId=$koreanId, englishId=$englishId") + + deleteLab(koreanId) + deleteLab(englishId) + researchLanguageRepository.delete(labLanguage) + } + + @Transactional + override fun deleteLab(id: Long) { + val lab = labRepository.findByIdOrNull(id) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$id)") + + applicationEventPublisher.publishEvent( + LabDeletedEvent( + lab.id, + lab.research?.id, + lab.professors.map { it.id }.toSet() + ) + ) + + lab.pdf?.let { attachmentService.deleteAttachment(it) } + + labRepository.delete(lab) + } + + @Transactional + fun upsertLabSearchIndex(lab: LabEntity) { + lab.researchSearch?.update(lab) ?: let { + lab.researchSearch = ResearchSearchEntity.create(lab) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchEventService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchEventService.kt new file mode 100644 index 00000000..190b8e77 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchEventService.kt @@ -0,0 +1,105 @@ +package com.wafflestudio.csereal.core.research.service + +import com.wafflestudio.csereal.core.research.database.LabRepository +import com.wafflestudio.csereal.core.research.database.ResearchEntity +import com.wafflestudio.csereal.core.research.database.ResearchRepository +import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity +import com.wafflestudio.csereal.core.research.event.LabCreatedEvent +import com.wafflestudio.csereal.core.research.event.LabDeletedEvent +import com.wafflestudio.csereal.core.research.event.LabModifiedEvent +import org.springframework.context.event.EventListener +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface ResearchEventService { + fun labDeletedEventListener(event: LabDeletedEvent) + fun labModifiedEventListener(event: LabModifiedEvent) + fun labCreatedEventListener(event: LabCreatedEvent) +} + +@Service +class ResearchEventServiceImpl( + private val researchRepository: ResearchRepository, + private val labRepository: LabRepository +) : ResearchEventService { + @EventListener + @Transactional + override fun labCreatedEventListener(event: LabCreatedEvent) { + if (event.researchId == null) { + return + } + + val lab = labRepository.findByIdOrNull(event.id)!! + val research = researchRepository.findByIdOrNull(event.researchId)!! // should exist + research.labs.add(lab) + + upsertResearchSearchIndex(research) + } + + @EventListener + @Transactional + override fun labModifiedEventListener(event: LabModifiedEvent) { + val lab = labRepository.findByIdOrNull(event.id)!! + + val oldResearch = event.researchIdModified.first?.let { researchRepository.findByIdOrNull(it)!! } + val newResearch = event.researchIdModified.second?.let { researchRepository.findByIdOrNull(it)!! } + + when { + oldResearch == null && newResearch == null -> {} + oldResearch == null && newResearch != null -> { + newResearch.apply { + labs.add(lab) + }.let { + upsertResearchSearchIndex(it) + } + } + + oldResearch != null && newResearch == null -> { + oldResearch.apply { + labs.remove(lab) + }.let { + upsertResearchSearchIndex(it) + } + } + + oldResearch!!.id == newResearch!!.id -> { + upsertResearchSearchIndex(oldResearch) + } + + else -> { + oldResearch.apply { + labs.remove(lab) + }.let { + upsertResearchSearchIndex(it) + } + newResearch.apply { + labs.add(lab) + }.let { + upsertResearchSearchIndex(it) + } + } + } + } + + @EventListener + @Transactional + override fun labDeletedEventListener(event: LabDeletedEvent) { + if (event.researchId == null) { + return + } + + val lab = labRepository.findByIdOrNull(event.id)!! + val research = researchRepository.findByIdOrNull(event.researchId)!! + research.labs.remove(lab) + + upsertResearchSearchIndex(research) + } + + @Transactional + fun upsertResearchSearchIndex(research: ResearchEntity) { + research.researchSearch?.update(research) ?: let { + research.researchSearch = ResearchSearchEntity.create(research) + } + } +} 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 84877958..3ffed9f7 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 @@ -3,25 +3,14 @@ package com.wafflestudio.csereal.core.research.service import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.core.conference.database.ConferenceRepository import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent -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.api.res.ResearchSearchResBody -import com.wafflestudio.csereal.core.research.database.ResearchRepository +import com.wafflestudio.csereal.core.research.database.* import org.springframework.context.event.EventListener -import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Propagation 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, language: LanguageType, number: Int, amount: Int): ResearchSearchResBody fun searchResearch( keyword: String, @@ -30,6 +19,7 @@ interface ResearchSearchService { pageNum: Int, amount: Int ): ResearchSearchResBody + fun deleteResearchSearch(researchSearchEntity: ResearchSearchEntity) } @Service @@ -72,58 +62,6 @@ class ResearchSearchServiceImpl( ) } - @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) - } - } - @EventListener @Transactional(propagation = Propagation.REQUIRES_NEW) fun refreshSearchListener(event: RefreshSearchEvent) { 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 64167802..c98c87e9 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 @@ -1,14 +1,12 @@ package com.wafflestudio.csereal.core.research.service import com.wafflestudio.csereal.common.CserealException -import com.wafflestudio.csereal.common.properties.EndpointProperties import com.wafflestudio.csereal.common.enums.LanguageType -import com.wafflestudio.csereal.common.utils.startsWithEnglish -import com.wafflestudio.csereal.core.member.database.ProfessorRepository +import com.wafflestudio.csereal.core.research.api.req.* import com.wafflestudio.csereal.core.research.database.* import com.wafflestudio.csereal.core.research.dto.* -import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity -import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService +import com.wafflestudio.csereal.core.research.type.ResearchRelatedType +import com.wafflestudio.csereal.core.research.type.ResearchType import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -16,374 +14,281 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile interface ResearchService { - fun createResearchDetail( - request: ResearchDto, - mainImage: MultipartFile?, - attachments: List? - ): ResearchDto - - fun readAllResearchGroups(language: String): ResearchGroupResponse - fun readAllResearchCenters(language: String): List - fun updateResearchDetail( + fun createResearchLanguage(req: CreateResearchLanguageReqBody, mainImage: MultipartFile?): ResearchLanguageDto + fun createResearch( + language: LanguageType, + request: CreateResearchSealedReqBody, + mainImage: MultipartFile? + ): ResearchSealedDto + + fun updateResearchLanguage( + koreanId: Long, + englishId: Long, + req: ModifyResearchLanguageReqBody, + updateImage: MultipartFile? + ): ResearchLanguageDto + + fun updateResearch( researchId: Long, - request: ResearchDto, - mainImage: MultipartFile?, - attachments: List? - ): ResearchDto - - fun createLab(request: LabDto, pdf: MultipartFile?): LabDto - fun readAllLabs(language: String): List - fun readLab(labId: Long): LabDto - 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 + request: ModifyResearchSealedReqBody, + updateImage: MultipartFile? + ): ResearchSealedDto + + fun deleteResearchLanguage(koreanId: Long, englishId: Long) + fun deleteResearch(researchId: Long) + + fun readResearchLanguage(id: Long): ResearchLanguageDto + fun readAllResearch(language: LanguageType, type: ResearchType): List - fun migrateLabPdf(labId: Long, pdf: MultipartFile?): LabDto + fun readAllResearchGroupsDeprecated(language: String): ResearchGroupResponse + fun readAllResearchCentersDeprecated(language: String): List } @Service class ResearchServiceImpl( private val researchRepository: ResearchRepository, - private val labRepository: LabRepository, - private val professorRepository: ProfessorRepository, - private val mainImageService: MainImageService, - private val attachmentService: AttachmentService, - private val endpointProperties: EndpointProperties + private val researchLanguageRepository: ResearchLanguageRepository, + private val mainImageService: MainImageService ) : ResearchService { @Transactional - override fun createResearchDetail( - request: ResearchDto, - mainImage: MultipartFile?, - attachments: List? - ): ResearchDto { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newResearch = ResearchEntity.of(enumLanguageType, request) - - if (request.labs != null) { - for (lab in request.labs) { - val labEntity = labRepository.findByIdOrNull(lab.id) - ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=${lab.id})") - newResearch.labs.add(labEntity) - labEntity.research = newResearch - } + override fun createResearchLanguage( + req: CreateResearchLanguageReqBody, + mainImage: MultipartFile? + ): ResearchLanguageDto { + if (!req.valid()) { + throw CserealException.Csereal400("두 언어의 research type이 일치하지 않습니다.") } - if (mainImage != null) { - mainImageService.uploadMainImage(newResearch, mainImage) - } + val ko = createResearch(LanguageType.KO, req.ko, mainImage) + val en = createResearch(LanguageType.EN, req.en, mainImage) + researchLanguageRepository.save( + ResearchLanguageEntity( + koreanId = ko.id, + englishId = en.id, + type = req.ko.type.ofResearchRelatedType() + ) + ) - if (attachments != null) { - attachmentService.uploadAllAttachments(newResearch, attachments) - } + return ResearchLanguageDto(ko, en) + } - newResearch.researchSearch = ResearchSearchEntity.create(newResearch) + @Transactional + override fun createResearch( + language: LanguageType, + request: CreateResearchSealedReqBody, + mainImage: MultipartFile? + ): ResearchSealedDto { + // Common fields + val newResearch = ResearchEntity( + postType = request.type, + language = language, + name = request.name, + description = request.description + ) + + // Type specific fields + when (request) { + is CreateResearchGroupReqBody -> {} + is CreateResearchCenterReqBody -> newResearch.websiteURL = request.websiteURL + } - researchRepository.save(newResearch) + // Create Research Search Index + upsertResearchSearchIndex(newResearch) + // Main Image + if (mainImage != null) { + mainImageService.uploadMainImage(newResearch, mainImage) + } val imageURL = mainImageService.createImageURL(newResearch.mainImage) - val attachmentResponses = - attachmentService.createAttachmentResponses(newResearch.attachments) - return ResearchDto.of(newResearch, imageURL, attachmentResponses) + return ResearchSealedDto.of( + researchRepository.save(newResearch), + imageURL + ) } - @Transactional(readOnly = true) - override fun readAllResearchGroups(language: String): ResearchGroupResponse { - // Todo: description 수정 필요 - val description = "세계가 주목하는 컴퓨터공학부의 많은 교수들은 ACM, IEEE 등 " + - "세계적인 컴퓨터관련 주요 학회에서 국제학술지 편집위원, 국제학술회의 위원장, " + - "기조연설자 등으로 활발하게 활동하고 있습니다. 정부 지원과제, 민간 산업체 지원 " + - "연구과제 등도 성공적으로 수행, 우수한 성과들을 내놓고 있으며, 오늘도 인류가 " + - "꿈꾸는 행복하고 편리한 세상을 위해 변화와 혁신, 연구와 도전을 계속하고 있습니다." - - val enumLanguageType = LanguageType.makeStringToLanguageType(language) - val researchGroups = - researchRepository.findAllByPostTypeAndLanguageOrderByName( - ResearchPostType.GROUPS, - enumLanguageType - ).map { - val imageURL = mainImageService.createImageURL(it.mainImage) - val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) - - ResearchDto.of(it, imageURL, attachmentResponses) - } - - return ResearchGroupResponse(description, researchGroups) - } + @Transactional + override fun updateResearchLanguage( + koreanId: Long, + englishId: Long, + req: ModifyResearchLanguageReqBody, + updateImage: MultipartFile? + ): ResearchLanguageDto { + if (!req.valid()) { + throw CserealException.Csereal404("두 언어의 research type이 일치하지 않습니다.") + } - @Transactional(readOnly = true) - override fun readAllResearchCenters(language: String): List { - val enumLanguageType = LanguageType.makeStringToLanguageType(language) - val researchCenters = - researchRepository.findAllByPostTypeAndLanguageOrderByName( - ResearchPostType.CENTERS, - enumLanguageType - ).map { - val imageURL = mainImageService.createImageURL(it.mainImage) - val attachmentResponses = attachmentService.createAttachmentResponses(it.attachments) + val type = req.ko.type + if (!researchLanguageRepository.existsByKoreanIdAndEnglishIdAndType( + koreanId, + englishId, + type.ofResearchRelatedType() + ) + ) { + throw CserealException.Csereal404("해당 Research 언어 쌍을 찾을 수 없습니다.") + } - ResearchDto.of(it, imageURL, attachmentResponses) - } + val koreanUpdatedDto = updateResearch(koreanId, req.ko, updateImage) + val englishUpdatedDto = updateResearch(englishId, req.en, updateImage) - return researchCenters + return ResearchLanguageDto(koreanUpdatedDto, englishUpdatedDto) } @Transactional - override fun updateResearchDetail( + override fun updateResearch( researchId: Long, - request: ResearchDto, - mainImage: MultipartFile?, - attachments: List? - ): ResearchDto { + request: ModifyResearchSealedReqBody, + updateImage: MultipartFile? + ): ResearchSealedDto { val research = researchRepository.findByIdOrNull(researchId) ?: throw CserealException.Csereal404("해당 게시글을 찾을 수 없습니다.(researchId=$researchId)") + val originalName = research.name - if (request.labs != null) { - for (lab in request.labs) { - val labEntity = labRepository.findByIdOrNull(lab.id) - ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=${lab.id})") - } - - val oldLabs = research.labs.map { it.id } - - val labsToRemove = oldLabs - request.labs.map { it.id } - val labsToAdd = request.labs.map { it.id } - oldLabs - - research.labs.removeIf { it.id in labsToRemove } + // Update common fields + research.apply { + name = request.name + description = request.description + } - for (labsToAddId in labsToAdd) { - val lab = labRepository.findByIdOrNull(labsToAddId)!! - research.labs.add(lab) - lab.research = research + // Update type specific fields + when (request) { + is ModifyResearchGroupReqBody -> {} + is ModifyResearchCenterReqBody -> { + research.websiteURL = request.websiteURL } } - if (mainImage != null) { - mainImageService.uploadMainImage(research, mainImage) - } else { + // Update image + // remove old image + if (research.mainImage != null && (request.removeImage || updateImage != null)) { + mainImageService.removeImage(research.mainImage!!) research.mainImage = null } - - if (attachments != null) { - research.attachments.clear() - attachmentService.uploadAllAttachments(research, attachments) - } else { - research.attachments.clear() + // upload new image + updateImage?.let { + mainImageService.uploadMainImage(research, it) } - val imageURL = mainImageService.createImageURL(research.mainImage) - val attachmentResponses = attachmentService.createAttachmentResponses(research.attachments) - research.updateWithoutLabImageAttachment(request) + // update search index + upsertResearchSearchIndex(research) - research.researchSearch?.update(research) - ?: let { - research.researchSearch = ResearchSearchEntity.create(research) + // TODO: Extract this to handle in event handler + // upsert labs in research group if name changed + if (originalName != research.name) { + research.labs.forEach { + upsertLabSearchIndex(it) } - - return ResearchDto.of(research, imageURL, attachmentResponses) - } - - @Transactional - override fun createLab(request: LabDto, pdf: MultipartFile?): LabDto { - val researchGroup = researchRepository.findByName(request.group!!) - ?: throw CserealException.Csereal404("해당 연구그룹을 찾을 수 없습니다.(researchGroupId = ${request.group})") - - if (researchGroup.postType != ResearchPostType.GROUPS) { - throw CserealException.Csereal404("해당 게시글은 연구그룹이어야 합니다.") } - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newLab = LabEntity.of(enumLanguageType, request, researchGroup) - - if (request.professors != null) { - for (professor in request.professors) { - val professorEntity = professorRepository.findByIdOrNull(professor.id) - ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다.(professorId = ${professor.id}") - - newLab.professors.add(professorEntity) - professorEntity.lab = newLab - } - } - - if (pdf != null) { - attachmentService.uploadAttachmentInLabEntity(newLab, pdf) - } - - newLab.researchSearch = ResearchSearchEntity.create(newLab) - - labRepository.save(newLab) - - val attachmentResponse = - attachmentService.createOneAttachmentResponse(newLab.pdf) - - return LabDto.of(newLab, attachmentResponse) - } - - @Transactional(readOnly = true) - override fun readAllLabs(language: String): List { - val enumLanguageType = LanguageType.makeStringToLanguageType(language) - val labs = labRepository.findAllByLanguageOrderByName(enumLanguageType).map { - val attachmentResponse = - attachmentService.createOneAttachmentResponse(it.pdf) - LabDto.of(it, attachmentResponse) - }.sortedWith { a, b -> - when { - startsWithEnglish(a.name) && !startsWithEnglish(b.name) -> 1 - !startsWithEnglish(a.name) && startsWithEnglish(b.name) -> -1 - else -> a.name.compareTo(b.name) - } - } - - return labs + return ResearchSealedDto.of(research, imageURL) } @Transactional - override fun readLab(labId: Long): LabDto { - val lab = labRepository.findByIdOrNull(labId) - ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") - - val attachmentResponse = - attachmentService.createOneAttachmentResponse(lab.pdf) - - return LabDto.of(lab, attachmentResponse) - } - - private fun createPdfURL(pdf: AttachmentEntity): String { - return "${endpointProperties.backend}/v1/file/${pdf.filename}" + override fun deleteResearchLanguage(koreanId: Long, englishId: Long) { + val researchLanguage = researchLanguageRepository.findByKoreanIdAndEnglishIdAndType( + koreanId, + englishId, + ResearchRelatedType.RESEARCH_GROUP + ) ?: researchLanguageRepository.findByKoreanIdAndEnglishIdAndType( + koreanId, + englishId, + ResearchRelatedType.RESEARCH_CENTER + ) ?: throw CserealException.Csereal404("해당 Research 언어 쌍을 찾을 수 없습니다.") + + deleteResearch(koreanId) + deleteResearch(englishId) + researchLanguageRepository.delete(researchLanguage) } - // TODO: professor search update @Transactional - override fun updateLab(labId: Long, request: LabUpdateRequest, pdf: MultipartFile?): LabDto { - val labEntity = labRepository.findByIdOrNull(labId) - ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") - - labEntity.updateWithoutProfessor(request) - - // update professor - val removedProfessorIds = labEntity.professors.map { it.id } - request.professorIds - val addedProfessorIds = request.professorIds - labEntity.professors.map { it.id } + override fun deleteResearch(researchId: Long) { + val research = researchRepository.findByIdOrNull(researchId) + ?: throw CserealException.Csereal404("해당 게시글을 찾을 수 없습니다.(researchId=$researchId)") - removedProfessorIds.forEach { - val professor = professorRepository.findByIdOrNull(it) - ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다.(professorId=$it)") - labEntity.professors.remove( - professor - ) - professor.lab = null + research.mainImage?.let { + mainImageService.removeImage(it) } - addedProfessorIds.forEach { - val professor = professorRepository.findByIdOrNull(it) - ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다.(professorId=$it)") - labEntity.professors.add( - professor - ) - professor.lab = labEntity + research.labs.forEach { + it.research = null } - // update pdf - if (request.pdfModified) { - labEntity.pdf?.let { attachmentService.deleteAttachmentDeprecated(it) } - - pdf?.let { - val attachmentDto = attachmentService.uploadAttachmentInLabEntity(labEntity, it) - } + // TODO: Extract this to event handler + // update search index to remove research + research.labs.forEach { + upsertLabSearchIndex(it) } - // update researchSearch - labEntity.researchSearch?.update(labEntity) - ?: let { - labEntity.researchSearch = ResearchSearchEntity.create(labEntity) - } - - val attachmentResponse = - attachmentService.createOneAttachmentResponse(labEntity.pdf) - - return LabDto.of(labEntity, attachmentResponse) + researchRepository.delete(research) } - @Transactional - override fun migrateResearchDetail(requestList: List): List { - val list = mutableListOf() - for (request in requestList) { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newResearch = ResearchEntity.of(enumLanguageType, request) + @Transactional(readOnly = true) + override fun readResearchLanguage(id: Long): ResearchLanguageDto { + val researchMap = researchLanguageRepository.findResearchPairById(id) + ?: throw CserealException.Csereal404("해당 Research 언어 쌍을 찾을 수 없습니다.(id=$id)") + + val ko = researchMap[LanguageType.KO]!! + val en = researchMap[LanguageType.EN]!! + return ResearchLanguageDto( + ResearchSealedDto.of(ko, mainImageService.createImageURL(ko.mainImage)), + ResearchSealedDto.of(en, mainImageService.createImageURL(en.mainImage)) + ) + } - newResearch.researchSearch = ResearchSearchEntity.create(newResearch) + @Transactional(readOnly = true) + override fun readAllResearch(language: LanguageType, type: ResearchType): List = + researchRepository.findAllByPostTypeAndLanguageOrderByName(type, language) + .map { ResearchSealedDto.of(it, mainImageService.createImageURL(it.mainImage)) } - researchRepository.save(newResearch) + @Transactional(readOnly = true) + override fun readAllResearchGroupsDeprecated(language: String): ResearchGroupResponse { + // Todo: description 수정 필요 + val description = "세계가 주목하는 컴퓨터공학부의 많은 교수들은 ACM, IEEE 등 " + + "세계적인 컴퓨터관련 주요 학회에서 국제학술지 편집위원, 국제학술회의 위원장, " + + "기조연설자 등으로 활발하게 활동하고 있습니다. 정부 지원과제, 민간 산업체 지원 " + + "연구과제 등도 성공적으로 수행, 우수한 성과들을 내놓고 있으며, 오늘도 인류가 " + + "꿈꾸는 행복하고 편리한 세상을 위해 변화와 혁신, 연구와 도전을 계속하고 있습니다." - list.add(ResearchDto.of(newResearch, null, listOf())) - } + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + val researchGroups = + researchRepository.findAllByPostTypeAndLanguageOrderByName( + ResearchType.GROUPS, + enumLanguageType + ).map { + val imageURL = mainImageService.createImageURL(it.mainImage) + ResearchDto.of(it, imageURL, emptyList()) + } - return list + return ResearchGroupResponse(description, researchGroups) } - @Transactional - override fun migrateLabs(requestList: List): List { - val list = mutableListOf() - for (request in requestList) { - val researchGroup = researchRepository.findByName(request.group) - ?: throw CserealException.Csereal404("해당 연구그룹을 찾을 수 없습니다.(researchGroupName = ${request.group})") - - if (researchGroup.postType != ResearchPostType.GROUPS) { - throw CserealException.Csereal404("해당 게시글은 연구그룹이어야 합니다.") + @Transactional(readOnly = true) + override fun readAllResearchCentersDeprecated(language: String): List { + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + val researchCenters = + researchRepository.findAllByPostTypeAndLanguageOrderByName( + ResearchType.CENTERS, + enumLanguageType + ).map { + val imageURL = mainImageService.createImageURL(it.mainImage) + ResearchDto.of(it, imageURL, emptyList()) } - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newLab = LabEntity.of(enumLanguageType, request, researchGroup) - - newLab.researchSearch = ResearchSearchEntity.create(newLab) - - labRepository.save(newLab) - - list.add(LabDto.of(newLab, null)) - } - return list + return researchCenters } @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) + fun upsertResearchSearchIndex(research: ResearchEntity) { + research.researchSearch?.update(research) ?: let { + research.researchSearch = ResearchSearchEntity.create(research) } - - 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("해당 연구실을 찾을 수 없습니다.") - - if (pdf != null) { - val attachmentDto = attachmentService.uploadAttachmentInLabEntity(lab, pdf) + fun upsertLabSearchIndex(lab: LabEntity) { + lab.researchSearch?.update(lab) ?: let { + lab.researchSearch = ResearchSearchEntity.create(lab) } - - val attachmentResponse = - attachmentService.createOneAttachmentResponse(lab.pdf) - - return LabDto.of(lab, attachmentResponse) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/type/ResearchType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/type/ResearchType.kt new file mode 100644 index 00000000..866a6cd2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/type/ResearchType.kt @@ -0,0 +1,38 @@ +package com.wafflestudio.csereal.core.research.type + +import com.wafflestudio.csereal.common.CserealException + +enum class ResearchRelatedType { + RESEARCH_GROUP, + RESEARCH_CENTER, + LAB, + CONFERENCE; + + fun ofResearchType() = when (this) { + RESEARCH_GROUP -> ResearchType.GROUPS + RESEARCH_CENTER -> ResearchType.CENTERS + else -> throw IllegalArgumentException("ResearchRelatedType $this does not have corresponding ResearchType") + } +} + +enum class ResearchType( + val krName: String +) { + GROUPS("연구 그룹"), + CENTERS("연구 센터"); + + fun ofResearchRelatedType() = when (this) { + GROUPS -> ResearchRelatedType.RESEARCH_GROUP + CENTERS -> ResearchRelatedType.RESEARCH_CENTER + } + + companion object { + fun fromJsonValue(value: String) = try { + ResearchType.valueOf( + value.uppercase().replace('-', '_') + ) + } catch (e: Exception) { + throw CserealException.Csereal404("잘못된 Research Type이 주어졌습니다.") + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt index 567e0462..b0169dcc 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/database/AttachmentEntity.kt @@ -8,7 +8,6 @@ import com.wafflestudio.csereal.core.academics.database.ScholarshipEntity import com.wafflestudio.csereal.core.news.database.NewsEntity import com.wafflestudio.csereal.core.notice.database.NoticeEntity import com.wafflestudio.csereal.core.research.database.LabEntity -import com.wafflestudio.csereal.core.research.database.ResearchEntity import com.wafflestudio.csereal.core.seminar.database.SeminarEntity import jakarta.persistence.* @@ -50,10 +49,6 @@ class AttachmentEntity( @JoinColumn(name = "lab_id") var lab: LabEntity? = null, - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "research_id") - var research: ResearchEntity? = null, - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "scholarship_id") var scholarship: ScholarshipEntity? = null diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt index d39fc1d6..3e676eac 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/attachment/service/AttachmentService.kt @@ -8,7 +8,6 @@ import com.wafflestudio.csereal.core.academics.database.AcademicsEntity import com.wafflestudio.csereal.core.news.database.NewsEntity import com.wafflestudio.csereal.core.notice.database.NoticeEntity import com.wafflestudio.csereal.core.research.database.LabEntity -import com.wafflestudio.csereal.core.research.database.ResearchEntity import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEntity import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentRepository import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentDto @@ -80,7 +79,7 @@ class AttachmentServiceImpl( @Transactional override fun uploadAllAttachments( - contentEntity: AttachmentContentEntityType, + contentEntityType: AttachmentContentEntityType, requestAttachments: List ): List { Files.createDirectories(Paths.get(path)) @@ -101,7 +100,7 @@ class AttachmentServiceImpl( size = requestAttachment.size ) - connectAttachmentToEntity(contentEntity, attachment) + connectAttachmentToEntity(contentEntityType, attachment) //Todo: update에서도 uploadAllAttachments 사용, 이에 따른 attachmentsOrder에 대한 조정 필요 attachmentRepository.save(attachment) @@ -212,11 +211,6 @@ class AttachmentServiceImpl( contentEntity.attachments.add(attachment) attachment.academics = contentEntity } - - is ResearchEntity -> { - contentEntity.attachments.add(attachment) - attachment.research = contentEntity - } } } } diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorServiceTest.kt index 34da731b..be4442d5 100644 --- a/src/test/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorServiceTest.kt +++ b/src/test/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorServiceTest.kt @@ -6,7 +6,8 @@ import com.wafflestudio.csereal.core.member.api.req.ModifyProfessorReqBody import com.wafflestudio.csereal.core.member.database.MemberSearchRepository import com.wafflestudio.csereal.core.member.database.ProfessorRepository import com.wafflestudio.csereal.core.member.database.ProfessorStatus -import com.wafflestudio.csereal.core.research.database.* +import com.wafflestudio.csereal.core.research.database.LabEntity +import com.wafflestudio.csereal.core.research.database.LabRepository import io.kotest.core.spec.style.BehaviorSpec import io.kotest.extensions.spring.SpringTestExtension import io.kotest.extensions.spring.SpringTestLifecycleMode @@ -25,26 +26,18 @@ class ProfessorServiceTest( private val professorService: ProfessorService, private val professorRepository: ProfessorRepository, private val labRepository: LabRepository, - private val memberSearchRepository: MemberSearchRepository, - private val researchRepository: ResearchRepository + private val memberSearchRepository: MemberSearchRepository ) : BehaviorSpec({ extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) afterContainer { professorRepository.deleteAll() - researchRepository.deleteAll() + labRepository.deleteAll() } Given("이미지 없는 교수를 생성하려고 할 때") { val date = LocalDate.now() - val researchEntity = ResearchEntity( - language = LanguageType.KO, - name = "researchName", - description = null, - websiteURL = null, - postType = ResearchPostType.GROUPS - ) var labEntity = LabEntity( language = LanguageType.KO, name = "labName", @@ -53,11 +46,8 @@ class ProfessorServiceTest( acronym = null, youtube = null, description = null, - websiteURL = null, - research = researchEntity + websiteURL = null ) - researchEntity.labs.add(labEntity) - researchRepository.save(researchEntity) labEntity = labRepository.save(labEntity) val professorCreateReq = CreateProfessorReqBody( @@ -139,37 +129,30 @@ class ProfessorServiceTest( Given("생성되어 있는 간단한 교수에 대하여") { val date = LocalDate.now() - val researchEntity = ResearchEntity( - language = LanguageType.KO, - name = "researchName", - description = null, - websiteURL = null, - postType = ResearchPostType.GROUPS - ) - val labEntity1 = LabEntity( - language = LanguageType.KO, - name = "labName1", - location = null, - tel = null, - acronym = null, - youtube = null, - description = null, - websiteURL = null, - research = researchEntity + val labEntity1 = labRepository.save( + LabEntity( + language = LanguageType.KO, + name = "labName1", + location = null, + tel = null, + acronym = null, + youtube = null, + description = null, + websiteURL = null + ) ) - val labEntity2 = LabEntity( - language = LanguageType.KO, - name = "labName2", - location = null, - tel = null, - acronym = null, - youtube = null, - description = null, - websiteURL = null, - research = researchEntity + val labEntity2 = labRepository.save( + LabEntity( + language = LanguageType.KO, + name = "labName2", + location = null, + tel = null, + acronym = null, + youtube = null, + description = null, + websiteURL = null + ) ) - researchEntity.labs.addAll(listOf(labEntity1, labEntity2)) - researchRepository.save(researchEntity) val createdProfessorDto = professorService.createProfessor( LanguageType.KO, 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 deleted file mode 100644 index 2229fd99..00000000 --- a/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchSearchServiceTest.kt +++ /dev/null @@ -1,277 +0,0 @@ -package com.wafflestudio.csereal.core.reseach.service - -import com.wafflestudio.csereal.common.enums.LanguageType -import com.wafflestudio.csereal.core.member.api.req.CreateProfessorReqBody -import com.wafflestudio.csereal.core.member.api.req.ModifyProfessorReqBody -import com.wafflestudio.csereal.core.member.database.ProfessorRepository -import com.wafflestudio.csereal.core.member.database.ProfessorStatus -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( - LanguageType.KO, - CreateProfessorReqBody( - name = "professor1", - email = null, - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - labId = 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( - LanguageType.KO, - CreateProfessorReqBody( - name = "professor2", - email = null, - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - labId = 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( - language = LanguageType.KO, - name = "research", - postType = ResearchPostType.GROUPS, - description = null, - websiteURL = null - ) - ) - - // Save lab - val labDto = LabDto( - id = -1, - language = "ko", - 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, - language = "ko", - 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( - LanguageType.KO, - CreateProfessorReqBody( - name = "newProfessor", - email = "email", - status = ProfessorStatus.ACTIVE, - academicRank = "academicRank", - labId = createdLabDto.id, - 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, - professor2.run { - ModifyProfessorReqBody( - name = "updateProfessor", - status = status, - academicRank = academicRank, - labId = createdEmptyLabDto.id, - startDate = startDate, - endDate = endDate, - office = office, - phone = phone, - fax = fax, - email = email, - website = website, - educations = educations.map { it.name }, - researchAreas = researchAreas.map { it.name }, - careers = careers.map { it.name }, - removeImage = false - ) - }, - 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() - } - } - } - } -} diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchServiceTest.kt index 171650a1..c941b4fc 100644 --- a/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchServiceTest.kt +++ b/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchServiceTest.kt @@ -1,20 +1,19 @@ package com.wafflestudio.csereal.core.reseach.service -import com.wafflestudio.csereal.common.enums.LanguageType -import com.wafflestudio.csereal.core.member.database.ProfessorEntity import com.wafflestudio.csereal.core.member.database.ProfessorRepository -import com.wafflestudio.csereal.core.member.database.ProfessorStatus -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.dto.LabUpdateRequest -import com.wafflestudio.csereal.core.research.dto.ResearchDto +import com.wafflestudio.csereal.core.research.api.req.* +import com.wafflestudio.csereal.core.research.database.LabRepository +import com.wafflestudio.csereal.core.research.database.ResearchLanguageRepository +import com.wafflestudio.csereal.core.research.database.ResearchRepository +import com.wafflestudio.csereal.core.research.database.ResearchSearchRepository +import com.wafflestudio.csereal.core.research.dto.ResearchLanguageDto +import com.wafflestudio.csereal.core.research.dto.ResearchSealedDto import com.wafflestudio.csereal.core.research.service.ResearchService +import com.wafflestudio.csereal.core.research.type.ResearchRelatedType 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.test.context.ActiveProfiles @@ -25,6 +24,7 @@ import org.springframework.transaction.annotation.Transactional @Transactional class ResearchServiceTest( private val researchService: ResearchService, + private val researchLanguageRepository: ResearchLanguageRepository, private val professorRepository: ProfessorRepository, private val labRepository: LabRepository, private val researchRepository: ResearchRepository, @@ -37,368 +37,234 @@ class ResearchServiceTest( afterSpec { professorRepository.deleteAll() + researchLanguageRepository.deleteAll() researchRepository.deleteAll() labRepository.deleteAll() researchSearchRepository.deleteAll() } - // Research - Given("간단한 Research를 생성하려고 할 때") { - val researchDto = ResearchDto( - id = -1, - language = "ko", - name = "name", - postType = ResearchPostType.CENTERS, - description = "description", - websiteURL = null, - createdAt = null, - modifiedAt = null, - labs = null, - imageURL = null, - attachments = null + // TODO: Add edge test cases + // TODO: Add search index test cases + Given("Create Research Center Request Body") { + val koCreateResearchCenterReqBody = CreateResearchCenterReqBody( + name = "한국어 연구소", + description = "한국어 연구소입니다.", + mainImageUrl = null, + websiteURL = "https://www.koreanlab.com" ) - When("Research를 생성한다면") { - val createdResearchDto = researchService.createResearchDetail( - researchDto, - null, - null - ) - - Then("Research가 생성되어야 한다") { - val research = researchRepository.findByIdOrNull(createdResearchDto.id) - research shouldNotBe null - researchRepository.count() shouldBe 1 - } + val enCreateResearchCenterReqBody = CreateResearchCenterReqBody( + name = "English Research Center", + description = "This is English Research Center.", + mainImageUrl = null, + websiteURL = "https://www.englishlab.com" + ) - Then("생성된 Research의 내용이 Dto와 동일해야 한다.") { - val research = researchRepository.findByIdOrNull(createdResearchDto.id)!! - research.name shouldBe researchDto.name - research.postType shouldBe researchDto.postType - research.description shouldBe researchDto.description - } + val createResearchCenterReqBody = CreateResearchLanguageReqBody( + ko = koCreateResearchCenterReqBody, + en = enCreateResearchCenterReqBody + ) - Then("검색 엔티티가 생성되어야 한다.") { - val research = researchRepository.findByIdOrNull(createdResearchDto.id)!! - val researchSearch = research.researchSearch - researchSearch shouldNotBe null - researchSearch!!.language shouldBe LanguageType.KO - - researchSearch!!.content shouldBe - """ - name - 연구 센터 - description - - """.trimIndent() + When("Create Research Center") { + val researchCenter = researchService.createResearchLanguage(createResearchCenterReqBody, null) + + Then("Research Center should be created") { + val pair = researchLanguageRepository.findAll() + .also { it.size shouldBe 1 } + pair[0].type shouldBe ResearchRelatedType.RESEARCH_CENTER + + val (koId, enId) = pair[0].koreanId to pair[0].englishId + val koResearchCenter = researchRepository.findByIdOrNull(koId)!! + val enResearchCenter = researchRepository.findByIdOrNull(enId)!! + ResearchLanguageDto( + ko = ResearchSealedDto.of(koResearchCenter, null), + en = ResearchSealedDto.of(enResearchCenter, null) + ) shouldBe researchCenter } } } - Given("간단한 Research를 수정하려고 할 때") { - val researchDto = ResearchDto( - id = -1, - language = "ko", - name = "name", - postType = ResearchPostType.CENTERS, - description = "description", - websiteURL = null, - createdAt = null, - modifiedAt = null, - labs = null, - imageURL = null, - attachments = null + Given("Create Research Group Request Body") { + val koCreateResearchGroupReqBody = CreateResearchGroupReqBody( + name = "한국어 연구 그룹", + description = "한국어 연구 그룹입니다.", + mainImageUrl = null ) - val createdResearchDto = researchService.createResearchDetail( - researchDto, - null, - null + val enCreateResearchGroupReqBody = CreateResearchGroupReqBody( + name = "English Research Group", + description = "This is English Research Group.", + mainImageUrl = null ) - When("Research를 수정한다면") { - val researchUpdateRequest = ResearchDto( - id = createdResearchDto.id, - language = "ko", - name = "name2", - postType = ResearchPostType.GROUPS, - description = "description2", - websiteURL = null, - createdAt = null, - modifiedAt = null, - labs = null, - imageURL = null, - attachments = null - ) - - researchService.updateResearchDetail( - createdResearchDto.id, - researchUpdateRequest, - null, - null - ) - - Then("Research가 수정되어야 한다") { - val research = researchRepository.findByIdOrNull(createdResearchDto.id)!! - research.name shouldBe researchUpdateRequest.name - research.postType shouldBe researchUpdateRequest.postType - research.description shouldBe researchUpdateRequest.description - } + val createResearchGroupReqBody = CreateResearchLanguageReqBody( + ko = koCreateResearchGroupReqBody, + en = enCreateResearchGroupReqBody + ) - Then("검색 엔티티가 수정되어야 한다.") { - val research = researchRepository.findByIdOrNull(createdResearchDto.id)!! - val researchSearch = research.researchSearch - researchSearch shouldNotBe null - - researchSearch!!.content shouldBe - """ - name2 - 연구 그룹 - description2 - - """.trimIndent() + When("Create Research Group") { + val researchGroup = researchService.createResearchLanguage(createResearchGroupReqBody, null) + + Then("Research Group should be created") { + val pair = researchLanguageRepository.findAll() + .also { it.size shouldBe 1 } + pair[0].type shouldBe ResearchRelatedType.RESEARCH_GROUP + + val (koId, enId) = pair[0].koreanId to pair[0].englishId + val koResearchGroup = researchRepository.findByIdOrNull(koId)!! + val enResearchGroup = researchRepository.findByIdOrNull(enId)!! + ResearchLanguageDto( + ko = ResearchSealedDto.of(koResearchGroup, null), + en = ResearchSealedDto.of(enResearchGroup, null) + ) shouldBe researchGroup } } } - // Lab - Given("pdf 없는 Lab을 생성하려고 할 때") { - // Save professors - val professor1 = professorRepository.save( - ProfessorEntity( - language = LanguageType.KO, - name = "professor1", - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - email = null, - fax = null, - office = null, - phone = null, - website = null, - startDate = null, - endDate = null - ) - ) - val professor2 = professorRepository.save( - ProfessorEntity( - language = LanguageType.KO, - name = "professor2", - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - email = null, - fax = null, - office = null, - phone = null, - website = null, - startDate = null, - endDate = null - ) + Given("Research Center Exists") { + val koCreateResearchCenterReqBody = CreateResearchCenterReqBody( + name = "한국어 연구소", + description = "한국어 연구소입니다.", + mainImageUrl = null, + websiteURL = "https://www.koreanlab.com" ) - // Save research - val research = researchRepository.save( - ResearchEntity( - language = LanguageType.KO, - name = "research", - postType = ResearchPostType.GROUPS, - description = null, - websiteURL = null - ) + val enCreateResearchCenterReqBody = CreateResearchCenterReqBody( + name = "English Research Center", + description = "This is English Research Center.", + mainImageUrl = null, + websiteURL = "https://www.englishlab.com" ) - val labDto = LabDto( - id = -1, - language = "ko", - 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 createResearchCenterReqBody = CreateResearchLanguageReqBody( + ko = koCreateResearchCenterReqBody, + en = enCreateResearchCenterReqBody ) - When("Lab을 생성한다면") { - val createdLabDto = researchService.createLab(labDto, null) + val researchCenter = researchService.createResearchLanguage(createResearchCenterReqBody, null) - Then("Lab이 생성되어야 한다") { - val lab = labRepository.findByIdOrNull(createdLabDto.id) - lab shouldNotBe null - labRepository.count() shouldBe 1 - } + When("Update Research Center") { + val koUpdateResearchCenterReqBody = ModifyResearchCenterReqBody( + name = "한국어 연구소 수정", + description = "한국어 연구소입니다. 수정", + websiteURL = "https://www.koreanlabbbb.com", + removeImage = false + ) - Then("생성된 Lab의 내용이 Dto와 동일해야 한다.") { - val lab = labRepository.findByIdOrNull(createdLabDto.id)!! - lab.name shouldBe labDto.name - lab.acronym shouldBe labDto.acronym - lab.description shouldBe labDto.description - lab.location shouldBe labDto.location - lab.tel shouldBe labDto.tel - lab.websiteURL shouldBe labDto.websiteURL - lab.youtube shouldBe labDto.youtube - lab.research shouldBe research - lab.professors shouldBe mutableSetOf(professor1, professor2) + val enUpdateResearchCenterReqBody = ModifyResearchCenterReqBody( + name = "English Research Center Update", + description = "This is English Research Center. Update", + websiteURL = "https://www.englishlabbbb.com", + removeImage = false + ) + + val updateResearchCenterReqBody = ModifyResearchLanguageReqBody( + ko = koUpdateResearchCenterReqBody, + en = enUpdateResearchCenterReqBody + ) + + val modifiedResearchCenter = researchService.updateResearchLanguage( + researchCenter.ko.id, + researchCenter.en.id, + updateResearchCenterReqBody, + null + ) + + Then("Research Center should be updated") { + val pair = researchLanguageRepository.findAll() + .also { it.size shouldBe 1 } + pair[0].type shouldBe ResearchRelatedType.RESEARCH_CENTER + val (koId, enId) = pair[0].koreanId to pair[0].englishId + koId shouldBe researchCenter.ko.id + enId shouldBe researchCenter.en.id + + val koResearchCenter = researchRepository.findByIdOrNull(koId)!! + val enResearchCenter = researchRepository.findByIdOrNull(enId)!! + ResearchLanguageDto( + ko = ResearchSealedDto.of(koResearchCenter, null), + en = ResearchSealedDto.of(enResearchCenter, null) + ) shouldBe modifiedResearchCenter } + } + + When("Delete Research Center") { + researchService.deleteResearchLanguage(researchCenter.ko.id, researchCenter.en.id) - Then("검색 엔티티가 생성되어야 한다.") { - val lab = labRepository.findByIdOrNull(createdLabDto.id)!! - val researchSearch = lab.researchSearch - researchSearch shouldNotBe null - researchSearch!!.language shouldBe LanguageType.KO - - researchSearch!!.content shouldBe - """ - name - professor1 - professor2 - location - tel - acronym - youtube - research - description - websiteURL - - """.trimIndent() + Then("Research Center should be deleted") { + researchLanguageRepository.findAll() shouldBe emptyList() + researchRepository.findAll() shouldBe emptyList() } } } - Given("간단한 Lab을 수정할 경우") { - // Save professors - val professor1 = professorRepository.save( - ProfessorEntity( - language = LanguageType.KO, - name = "professor1", - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - email = null, - fax = null, - office = null, - phone = null, - website = null, - startDate = null, - endDate = null - ) - ) - val professor2 = professorRepository.save( - ProfessorEntity( - language = LanguageType.KO, - name = "professor2", - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - email = null, - fax = null, - office = null, - phone = null, - website = null, - startDate = null, - endDate = null - ) + Given("Research Group Exists") { + val koCreateResearchGroupReqBody = CreateResearchGroupReqBody( + name = "한국어 연구 그룹", + description = "한국어 연구 그룹입니다.", + mainImageUrl = null ) - // Save research - val research = researchRepository.save( - ResearchEntity( - language = LanguageType.KO, - name = "research", - postType = ResearchPostType.GROUPS, - description = null, - websiteURL = null - ) + val enCreateResearchGroupReqBody = CreateResearchGroupReqBody( + name = "English Research Group", + description = "This is English Research Group.", + mainImageUrl = null ) - // Save lab - val labDto = LabDto( - id = -1, - language = "ko", - 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 createResearchGroupReqBody = CreateResearchLanguageReqBody( + ko = koCreateResearchGroupReqBody, + en = enCreateResearchGroupReqBody ) - val createdLabDto = researchService.createLab(labDto, null) - val createdLab = labRepository.findByIdOrNull(createdLabDto.id)!! - - When("pdf를 제외하고 Lab을 수정한다면") { - val professor3 = professorRepository.save( - ProfessorEntity( - language = LanguageType.KO, - name = "professor3", - status = ProfessorStatus.ACTIVE, - academicRank = "professor", - email = null, - fax = null, - office = null, - phone = null, - website = null, - startDate = null, - endDate = null - ) + val researchGroup = researchService.createResearchLanguage(createResearchGroupReqBody, null) + + When("Update Research Group") { + val koUpdateResearchGroupReqBody = ModifyResearchGroupReqBody( + name = "한국어 연구 그룹 수정", + description = "한국어 연구 그룹입니다. 수정", + removeImage = false + ) + + val enUpdateResearchGroupReqBody = ModifyResearchGroupReqBody( + name = "English Research Group Update", + description = "This is English Research Group. Update", + removeImage = false ) - val labUpdateRequest = LabUpdateRequest( - name = "name2", - professorIds = listOf(professor1.id, professor3.id), - acronym = "acronym2", - description = "description2", - location = "location2", - tel = "tel2", - websiteURL = "websiteURL2", - youtube = "youtube2", - pdfModified = false + val updateResearchGroupReqBody = ModifyResearchLanguageReqBody( + ko = koUpdateResearchGroupReqBody, + en = enUpdateResearchGroupReqBody ) - researchService.updateLab(createdLab.id, labUpdateRequest, null) - - Then("Lab이 수정되어야 한다.") { - val lab = labRepository.findByIdOrNull(createdLab.id)!! - lab.name shouldBe labUpdateRequest.name - lab.acronym shouldBe labUpdateRequest.acronym - lab.description shouldBe labUpdateRequest.description - lab.location shouldBe labUpdateRequest.location - lab.tel shouldBe labUpdateRequest.tel - lab.websiteURL shouldBe labUpdateRequest.websiteURL - lab.youtube shouldBe labUpdateRequest.youtube - lab.research shouldBe research - lab.professors shouldBe mutableSetOf(professor1, professor3) + val modifiedResearchGroup = researchService.updateResearchLanguage( + researchGroup.ko.id, + researchGroup.en.id, + updateResearchGroupReqBody, + null + ) + + Then("Research Group should be updated") { + val pair = researchLanguageRepository.findAll() + .also { it.size shouldBe 1 } + pair[0].type shouldBe ResearchRelatedType.RESEARCH_GROUP + val (koId, enId) = pair[0].koreanId to pair[0].englishId + koId shouldBe researchGroup.ko.id + enId shouldBe researchGroup.en.id + + val koResearchGroup = researchRepository.findByIdOrNull(koId)!! + val enResearchGroup = researchRepository.findByIdOrNull(enId)!! + ResearchLanguageDto( + ko = ResearchSealedDto.of(koResearchGroup, null), + en = ResearchSealedDto.of(enResearchGroup, null) + ) shouldBe modifiedResearchGroup } + } + + When("Delete Research Group") { + researchService.deleteResearchLanguage(researchGroup.ko.id, researchGroup.en.id) - Then("검색 엔티티가 수정되어야 한다.") { - val lab = labRepository.findByIdOrNull(createdLab.id)!! - val researchSearch = lab.researchSearch - researchSearch shouldNotBe null - - researchSearch!!.content shouldBe - """ - name2 - professor1 - professor3 - location2 - tel2 - acronym2 - youtube2 - research - description2 - websiteURL2 - - """.trimIndent() + Then("Research Group should be deleted") { + researchLanguageRepository.findAll() shouldBe emptyList() + researchRepository.findAll() shouldBe emptyList() } } }