diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/api/ConferenceController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/api/ConferenceController.kt index 75a3cde7..cbbce4e2 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/conference/api/ConferenceController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/api/ConferenceController.kt @@ -1,11 +1,11 @@ package com.wafflestudio.csereal.core.conference.api +import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.core.conference.dto.ConferenceModifyRequest import com.wafflestudio.csereal.core.conference.dto.ConferencePage import com.wafflestudio.csereal.core.conference.service.ConferenceService import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* @RequestMapping("/api/v1/conference") @RestController @@ -18,4 +18,15 @@ class ConferenceController( return ResponseEntity.ok(conferenceService.getConferencePage()) } + @AuthenticatedStaff + @PatchMapping("/page/conferences") + fun modifyConferencePage( + @RequestBody conferenceModifyRequest: ConferenceModifyRequest + ): ResponseEntity { + return ResponseEntity.ok( + conferenceService.modifyConferences( + conferenceModifyRequest + ) + ) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceEntity.kt index 54a19253..5ff9e60b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceEntity.kt @@ -1,6 +1,8 @@ package com.wafflestudio.csereal.core.conference.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.conference.dto.ConferenceCreateDto +import com.wafflestudio.csereal.core.conference.dto.ConferenceDto import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity import jakarta.persistence.* @@ -18,4 +20,21 @@ class ConferenceEntity( @OneToOne(mappedBy = "conferenceElement", cascade = [CascadeType.ALL], orphanRemoval = true) var researchSearch: ResearchSearchEntity? = null, ) : BaseTimeEntity() { -} \ No newline at end of file + companion object { + fun of( + conferenceCreateDto: ConferenceCreateDto, + conferencePage: ConferencePageEntity, + ) = ConferenceEntity( + code = conferenceCreateDto.code, + abbreviation = conferenceCreateDto.abbreviation, + name = conferenceCreateDto.name, + conferencePage = conferencePage, + ) + } + + fun update(conferenceDto: ConferenceDto) { + this.code = conferenceDto.code + this.abbreviation = conferenceDto.abbreviation + this.name = conferenceDto.name + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferencePageEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferencePageEntity.kt index 4fdc2e14..28a57a71 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferencePageEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferencePageEntity.kt @@ -9,10 +9,10 @@ class ConferencePageEntity( @OneToOne @JoinColumn(name = "author_id") - val author: UserEntity, + var author: UserEntity, - @OneToMany(mappedBy = "conferencePage") + @OneToMany(mappedBy = "conferencePage", cascade = [CascadeType.ALL], orphanRemoval = true) @OrderBy("code ASC") - val conferences: List = mutableListOf() + val conferences: MutableSet = mutableSetOf() ) : BaseTimeEntity() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceRepository.kt new file mode 100644 index 00000000..25b6865b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/database/ConferenceRepository.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.conference.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface ConferenceRepository: JpaRepository \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceCreateDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceCreateDto.kt new file mode 100644 index 00000000..ab2b475e --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceCreateDto.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.conference.dto + +data class ConferenceCreateDto ( + val code: String, + val abbreviation: String, + val name: String, +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceDto.kt index 03178adc..cd8a18c7 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceDto.kt @@ -3,16 +3,18 @@ package com.wafflestudio.csereal.core.conference.dto import com.wafflestudio.csereal.core.conference.database.ConferenceEntity data class ConferenceDto( + val id: Long, val code: String, val abbreviation: String, - val name: String + val name: String, ) { companion object { fun of(conferenceEntity: ConferenceEntity): ConferenceDto { return ConferenceDto( + id = conferenceEntity.id, code = conferenceEntity.code, abbreviation = conferenceEntity.abbreviation, - name = conferenceEntity.name + name = conferenceEntity.name, ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceModifyRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceModifyRequest.kt new file mode 100644 index 00000000..4d5111d0 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferenceModifyRequest.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.conference.dto + +data class ConferenceModifyRequest( + val newConferenceList: List, + val modifiedConferenceList: List, + val deleteConfereceIdList: List +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferencePage.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferencePage.kt index dfb38fb4..640c95e0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferencePage.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/dto/ConferencePage.kt @@ -15,7 +15,9 @@ data class ConferencePage( createdAt = conferencePageEntity.createdAt!!, modifiedAt = conferencePageEntity.modifiedAt!!, author = conferencePageEntity.author.name, - conferenceList = conferencePageEntity.conferences.map { ConferenceDto.of(it) } + conferenceList = conferencePageEntity.conferences.map { + ConferenceDto.of(it) + }.sortedBy { it.code } ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceService.kt index 2bd56077..0829a2a9 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceService.kt @@ -1,20 +1,39 @@ package com.wafflestudio.csereal.core.conference.service +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.conference.database.ConferenceEntity +import com.wafflestudio.csereal.core.conference.database.ConferencePageEntity import com.wafflestudio.csereal.core.conference.database.ConferencePageRepository +import com.wafflestudio.csereal.core.conference.database.ConferenceRepository +import com.wafflestudio.csereal.core.conference.dto.ConferenceCreateDto import com.wafflestudio.csereal.core.conference.dto.ConferenceDto +import com.wafflestudio.csereal.core.conference.dto.ConferenceModifyRequest import com.wafflestudio.csereal.core.conference.dto.ConferencePage +import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity +import com.wafflestudio.csereal.core.research.service.ResearchSearchService +import com.wafflestudio.csereal.core.user.database.UserEntity +import com.wafflestudio.csereal.core.user.database.UserRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.context.request.RequestAttributes +import org.springframework.web.context.request.RequestContextHolder interface ConferenceService { fun getConferencePage(): ConferencePage + fun modifyConferences(conferenceModifyRequest: ConferenceModifyRequest): ConferencePage } @Service @Transactional class ConferenceServiceImpl( - private val conferencePageRepository: ConferencePageRepository + private val conferencePageRepository: ConferencePageRepository, + private val conferenceRepository: ConferenceRepository, + private val userRepository: UserRepository, + private val researchSearchService: ResearchSearchService, ) : ConferenceService { @Transactional(readOnly = true) @@ -23,4 +42,84 @@ class ConferenceServiceImpl( return ConferencePage.of(conferencePage) } -} + @Transactional + override fun modifyConferences(conferenceModifyRequest: ConferenceModifyRequest): ConferencePage { + var user = RequestContextHolder.getRequestAttributes()?.getAttribute( + "loggedInUser", + RequestAttributes.SCOPE_REQUEST + ) as UserEntity? + + if (user == null) { + val oidcUser = SecurityContextHolder.getContext().authentication.principal as OidcUser + val username = oidcUser.idToken.getClaim("username") + + user = userRepository.findByUsername(username) ?: throw CserealException.Csereal404("재로그인이 필요합니다.") + } + + val conferencePage = conferencePageRepository.findAll()[0] + + val newConferenceList = conferenceModifyRequest.newConferenceList.map { + createConferenceWithoutSave(it, conferencePage) + } + + val modifiedConferenceList = conferenceModifyRequest.modifiedConferenceList.map { + modifyConferenceWithoutSave(it) + } + + val deleteConferenceList = conferenceModifyRequest.deleteConfereceIdList.map { + deleteConference(it, conferencePage) + } + + conferencePage.author = user + + return ConferencePage.of(conferencePage) + } + + @Transactional + fun createConferenceWithoutSave( + conferenceCreateDto: ConferenceCreateDto, + conferencePage: ConferencePageEntity, + ): ConferenceEntity { + val newConference = ConferenceEntity.of( + conferenceCreateDto, + conferencePage + ) + conferencePage.conferences.add(newConference) + + newConference.researchSearch = ResearchSearchEntity.create(newConference) + + return newConference + } + + @Transactional + fun modifyConferenceWithoutSave( + conferenceDto: ConferenceDto, + ): ConferenceEntity { + val conferenceEntity = conferenceRepository.findByIdOrNull(conferenceDto.id) + ?: throw CserealException.Csereal404("Conference id:${conferenceDto.id} 가 존재하지 않습니다.") + + conferenceEntity.update(conferenceDto) + + conferenceEntity.researchSearch?.update(conferenceEntity) + ?: let { + conferenceEntity.researchSearch = ResearchSearchEntity.create(conferenceEntity) + } + + return conferenceEntity + } + + @Transactional + fun deleteConference( + id: Long, + conferencePage: ConferencePageEntity, + ) = conferenceRepository.findByIdOrNull(id) + ?. let { + it.isDeleted = true + conferencePage.conferences.remove(it) + + it.researchSearch?.let { + researchSearchService.deleteResearchSearch(it) + } + it.researchSearch = null + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceServiceTest.kt new file mode 100644 index 00000000..7b6ac283 --- /dev/null +++ b/src/test/kotlin/com/wafflestudio/csereal/core/conference/service/ConferenceServiceTest.kt @@ -0,0 +1,160 @@ +package com.wafflestudio.csereal.core.conference.service + +import com.wafflestudio.csereal.core.conference.database.ConferenceEntity +import com.wafflestudio.csereal.core.conference.database.ConferencePageEntity +import com.wafflestudio.csereal.core.conference.database.ConferencePageRepository +import com.wafflestudio.csereal.core.conference.database.ConferenceRepository +import com.wafflestudio.csereal.core.conference.dto.ConferenceCreateDto +import com.wafflestudio.csereal.core.conference.dto.ConferenceDto +import com.wafflestudio.csereal.core.conference.dto.ConferenceModifyRequest +import com.wafflestudio.csereal.core.user.database.Role +import com.wafflestudio.csereal.core.user.database.UserEntity +import com.wafflestudio.csereal.core.user.database.UserRepository +import 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.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.context.request.RequestAttributes +import org.springframework.web.context.request.RequestContextHolder + +@SpringBootTest +@Transactional +class ConferenceServiceTest ( + private val conferenceService: ConferenceService, + private val conferencePageRepository: ConferencePageRepository, + private val conferenceRepository: ConferenceRepository, + private val userRepository: UserRepository, +): BehaviorSpec ({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + beforeSpec { + val user = userRepository.save( + UserEntity( + username = "admin", + name = "admin", + email = "email", + studentId = "studentId", + role = Role.ROLE_STAFF, + ) + ) + + conferencePageRepository.save( + ConferencePageEntity( + author = user, + ) + ) + } + + afterSpec { + conferencePageRepository.deleteAll() + conferenceRepository.deleteAll() + userRepository.deleteAll() + } + + // ConferencePage + Given("Conference를 수정하려고 할 때") { + val userEntity = userRepository.findByUsername("admin")!! + + mockkStatic(RequestContextHolder::class) + val mockRequestAttributes = mockk() + every { + RequestContextHolder.getRequestAttributes() + } returns mockRequestAttributes + every { + mockRequestAttributes.getAttribute( + "loggedInUser", + RequestAttributes.SCOPE_REQUEST + ) + } returns userEntity + + + var conferencePage = conferencePageRepository.findAll().first() + + val conferences = conferenceRepository.saveAll(listOf( + ConferenceEntity( + code = "code1", + name = "name1", + abbreviation = "abbreviation1", + conferencePage = conferencePage, + ), + ConferenceEntity( + code = "code2", + name = "name2", + abbreviation = "abbreviation2", + conferencePage = conferencePage, + ), + ConferenceEntity( + code = "code3", + name = "name3", + abbreviation = "abbreviation3", + conferencePage = conferencePage, + ), + )) + conferencePage = conferencePage.apply { + this.conferences.addAll(conferences) + }.let { + conferencePageRepository.save(it) + } + + When("Conference를 수정한다면") { + val deleteConferenceId = conferences[1].id + val modifiedConference = ConferenceDto( + id = conferences.first().id, + code = "code0", + name = "modifiedName", + abbreviation = "modifiedAbbreviation", + ) + val newConference = ConferenceCreateDto( + code = "code9", + name = "newName", + abbreviation = "newAbbreviation", + ) + val conferenceModifyRequest = ConferenceModifyRequest( + deleteConfereceIdList = listOf(deleteConferenceId), + modifiedConferenceList = listOf(modifiedConference), + newConferenceList = listOf(newConference), + ) + + val conferencePage = conferenceService.modifyConferences(conferenceModifyRequest) + + Then("Conference가 수정되어야 한다.") { + val newConferencePage = conferencePageRepository.findAll().first() + val newConferences = newConferencePage.conferences.sortedBy { it.code } + + newConferences.size shouldBe 3 + newConferences.first().apply { + code shouldBe modifiedConference.code + name shouldBe modifiedConference.name + abbreviation shouldBe modifiedConference.abbreviation + researchSearch?.content shouldBe """ + modifiedName + code0 + modifiedAbbreviation + + """.trimIndent() + } + newConferences[1].apply { + code shouldBe conferences.last().code + name shouldBe conferences.last().name + abbreviation shouldBe conferences.last().abbreviation + } + newConferences.last().apply { + code shouldBe newConference.code + name shouldBe newConference.name + abbreviation shouldBe newConference.abbreviation + researchSearch?.content shouldBe """ + newName + code9 + newAbbreviation + + """.trimIndent() + } + } + } + } +}) \ No newline at end of file