diff --git a/.github/workflows/proxy.yaml b/.github/workflows/proxy.yaml index cf1f509b..be0b8763 100644 --- a/.github/workflows/proxy.yaml +++ b/.github/workflows/proxy.yaml @@ -21,6 +21,8 @@ jobs: run: | echo "URL=${{secrets.URL}}" > .env echo "LOCAL_IP=${{secrets.LOCAL_IP}}" >> .env + echo "CERTIFICATE=${{secrets.CERTIFICATE}}" >> .env + echo "PRIVATE_KEY=${{secrets.PRIVATE_KEY}}" >> .env - name: SCP Command to Transfer Files diff --git a/.github/workflows/proxy_dev.yaml b/.github/workflows/proxy_dev.yaml index f1e8d553..77e833d5 100644 --- a/.github/workflows/proxy_dev.yaml +++ b/.github/workflows/proxy_dev.yaml @@ -4,7 +4,7 @@ on: - develop paths: - docker-compose-caddy.yml - - caddy/Caddyfile + - caddy/Caddyfile.dev - .github/workflows/proxy.yaml jobs: @@ -29,7 +29,7 @@ jobs: host: ${{secrets.SSH_HOST_DEV}} username: ${{secrets.SSH_USER}} key: ${{secrets.SSH_KEY}} - source: "docker-compose-caddy.yml, .env, caddy/Caddyfile" + source: "docker-compose-caddy.yml, .env, caddy/Caddyfile.dev" target: "~/proxy" overwrite: true @@ -42,6 +42,7 @@ jobs: key: ${{secrets.SSH_KEY}} script: | cd ~/proxy + mv caddy/Caddyfile.dev caddy/Caddyfile source .env docker-compose -f docker-compose-caddy.yml down docker-compose -f docker-compose-caddy.yml up -d diff --git a/.gitignore b/.gitignore index 38b301f6..9461fef3 100644 --- a/.gitignore +++ b/.gitignore @@ -288,4 +288,5 @@ db/ # caddy server caddy/* -!caddy/Caddyfile \ No newline at end of file +!caddy/Caddyfile +!caddy/Caddyfile.dev \ No newline at end of file diff --git a/caddy/Caddyfile b/caddy/Caddyfile index e9d2a891..4d4d64dc 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -1,9 +1,11 @@ {$URL} { + tls {$CERTIFICATE} {$PRIVATE_KEY} + # Frontend reverse_proxy host.docker.internal:3000 @backend_denied { - path /swagger-ui/* /api-docs/* /api/v1/search/refresh + path /api/v1/search/refresh not remote_ip {$LOCAL_IP} } abort @backend_denied diff --git a/caddy/Caddyfile.dev b/caddy/Caddyfile.dev new file mode 100644 index 00000000..f38f4e5f --- /dev/null +++ b/caddy/Caddyfile.dev @@ -0,0 +1,17 @@ +{$URL} { + # Frontend + reverse_proxy host.docker.internal:3000 + + # Backend + reverse_proxy /api/* host.docker.internal:8080 + + # Old file serving + reverse_proxy /sites/default/files/* host.docker.internal:8080 + + # Login + reverse_proxy /oauth2/authorization/idsnucse host.docker.internal:8080 + + # Swagger + reverse_proxy /swagger-ui/* host.docker.internal:8080 + reverse_proxy /api-docs/* host.docker.internal:8080 +} \ No newline at end of file diff --git a/docker-compose-caddy.yml b/docker-compose-caddy.yml index ae223282..30eb326f 100644 --- a/docker-compose-caddy.yml +++ b/docker-compose-caddy.yml @@ -9,9 +9,12 @@ services: - ./caddy/Caddyfile:/etc/caddy/Caddyfile - ./caddy/data:/data - ./caddy/config:/config + - ./caddy/certs:/certs environment: URL: ${URL} LOCAL_IP: ${LOCAL_IP} + CERTIFICATE: ${CERTIFICATE} + PRIVATE_KEY: ${PRIVATE_KEY} extra_hosts: - host.docker.internal:host-gateway restart: always diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/CserealException.kt b/src/main/kotlin/com/wafflestudio/csereal/common/CserealException.kt index ead4693b..1d0201fa 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/CserealException.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/CserealException.kt @@ -7,4 +7,5 @@ open class CserealException(msg: String, val status: HttpStatus) : RuntimeExcept class Csereal404(msg: String) : CserealException(msg, HttpStatus.NOT_FOUND) class Csereal401(msg: String) : CserealException(msg, HttpStatus.UNAUTHORIZED) class Csereal409(msg: String) : CserealException(msg, HttpStatus.CONFLICT) + class Csereal403(msg: String) : CserealException(msg, HttpStatus.FORBIDDEN) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt index 68a93b0b..70371939 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt @@ -13,6 +13,7 @@ class OpenApiConfig { val info = Info() .title("컴퓨터공학부 홈페이지 백엔드 API") .description("컴퓨터공학부 홈페이지 백엔드 API 명세서입니다.") + .version("1") return OpenAPI() .components(Components()) diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt index f1ca9ef4..860e6abf 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt @@ -33,23 +33,32 @@ class SecurityConfig( @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { return http - .cors().and() - .csrf().disable() - .oauth2Login() - .loginPage("$loginPage/oauth2/authorization/idsnucse") - .redirectionEndpoint() - .baseUri("/api/v1/login/oauth2/code/idsnucse").and() - .userInfoEndpoint().oidcUserService(customOidcUserService).and() - .successHandler(CustomAuthenticationSuccessHandler(endpointProperties.frontend)).and() - .logout() - .logoutUrl("/api/v1/logout") - .logoutSuccessHandler(oidcLogoutSuccessHandler()) - .invalidateHttpSession(true) - .clearAuthentication(true) - .deleteCookies("JSESSIONID").and() - .authorizeHttpRequests() - .requestMatchers("/api/v1/login").authenticated() - .anyRequest().permitAll().and() + .cors { } + .csrf { it.disable() } + .oauth2Login { oauth2 -> + oauth2 + .loginPage("$loginPage/oauth2/authorization/idsnucse") + .redirectionEndpoint { redirect -> + redirect.baseUri("/api/v1/login/oauth2/code/idsnucse") + } + .userInfoEndpoint { userInfo -> + userInfo.oidcUserService(customOidcUserService) + } + .successHandler(CustomAuthenticationSuccessHandler(endpointProperties.frontend)) + } + .logout { logout -> + logout + .logoutUrl("/api/v1/logout") + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID") + } + .authorizeHttpRequests { auth -> + auth + .requestMatchers("/api/v1/login").authenticated() + .anyRequest().permitAll() + } .build() } diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt b/src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt deleted file mode 100644 index cd448be5..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.wafflestudio.csereal.common.controller - -import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity - -interface ContentEntityType { - fun bringMainImage(): MainImageEntity? -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/enums/LanguageType.kt b/src/main/kotlin/com/wafflestudio/csereal/common/enums/LanguageType.kt index 9f3265ba..c773f5b4 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/enums/LanguageType.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/enums/LanguageType.kt @@ -5,6 +5,7 @@ import com.wafflestudio.csereal.common.CserealException enum class LanguageType { KO, EN; + // TODO: Define custom deserializer, serializer companion object { fun makeStringToLanguageType(language: String): LanguageType { try { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt index 9b1ea36d..36cd8fc3 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt @@ -2,13 +2,13 @@ package com.wafflestudio.csereal.core.academics.api import com.wafflestudio.csereal.common.aop.AuthenticatedStaff import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.academics.api.req.UpdateSingleReq import com.wafflestudio.csereal.core.academics.dto.* import com.wafflestudio.csereal.core.academics.service.AcademicsService import com.wafflestudio.csereal.core.academics.dto.ScholarshipDto import com.wafflestudio.csereal.core.academics.service.AcademicsSearchService import jakarta.validation.Valid import 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 @@ -42,6 +42,15 @@ class AcademicsController( return ResponseEntity.ok(academicsService.readGuide(language, studentType)) } + @AuthenticatedStaff + @PutMapping("/{studentType}/guide") + fun updateGuide( + @RequestParam(required = false, defaultValue = "ko") language: String, + @PathVariable studentType: String, + @RequestPart request: UpdateSingleReq, + @RequestPart newAttachments: List? + ) = academicsService.updateGuide(language, studentType, request, newAttachments) + @GetMapping("/undergraduate/general-studies-requirements") fun readGeneralStudiesRequirements( @RequestParam(required = false, defaultValue = "ko") language: String @@ -96,6 +105,14 @@ class AcademicsController( return ResponseEntity.ok(academicsService.readDegreeRequirements(language)) } + @AuthenticatedStaff + @PutMapping("/undergraduate/degree-requirements") + fun updateDegreeRequirements( + @RequestParam(required = false, defaultValue = "ko") language: String, + @RequestPart request: UpdateSingleReq, + @RequestPart newAttachments: List? + ) = academicsService.updateDegreeRequirements(language, request, newAttachments) + @AuthenticatedStaff @PostMapping("/{studentType}/scholarshipDetail") fun createScholarshipDetail( @@ -121,52 +138,6 @@ class AcademicsController( return ResponseEntity.ok(academicsService.readScholarship(scholarshipId)) } - @Profile("!prod") - @PostMapping("/{studentType}/{postType}/migrate") - fun migrateAcademicsDetail( - @PathVariable studentType: String, - @PathVariable postType: String, - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok( - academicsService.migrateAcademicsDetail(studentType, postType, requestList) - ) - } - - @Profile("!prod") - @PostMapping("/course/migrate/{studentType}") - fun migrateCourses( - @PathVariable studentType: String, - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(academicsService.migrateCourses(studentType, requestList)) - } - - @Profile("!prod") - @PostMapping("/{studentType}/scholarshipDetail/migrate") - fun migrateScholarshipDetail( - @PathVariable studentType: String, - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok( - academicsService.migrateScholarshipDetail(studentType, requestList) - ) - } - - @Profile("!prod") - @PatchMapping("/migrateAttachment/{academicsId}") - fun migrateAcademicsDetailAttachments( - @PathVariable academicsId: Long, - @RequestPart("attachments") attachments: List? - ): ResponseEntity { - return ResponseEntity.ok( - academicsService.migrateAcademicsDetailAttachments( - academicsId, - attachments - ) - ) - } - @GetMapping("/search/top") fun searchTop( @RequestParam(required = true) keyword: String, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/req/UpdateSingleReq.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/req/UpdateSingleReq.kt new file mode 100644 index 00000000..4131f8b3 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/req/UpdateSingleReq.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.academics.api.req + +data class UpdateSingleReq( + val description: String, + val deleteIds: List +) 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 8cda491f..75e0e157 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 @@ -2,6 +2,7 @@ package com.wafflestudio.csereal.core.academics.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.academics.api.req.UpdateSingleReq import com.wafflestudio.csereal.core.academics.database.* import com.wafflestudio.csereal.core.academics.dto.* import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService @@ -29,6 +30,7 @@ interface AcademicsService { fun readGeneralStudiesRequirements(language: String): GeneralStudiesRequirementsPageResponse fun readDegreeRequirements(language: String): DegreeRequirementsPageResponse + fun updateDegreeRequirements(language: String, request: UpdateSingleReq, newAttachments: List?) fun createCourse( studentType: String, request: CourseDto, @@ -44,22 +46,12 @@ interface AcademicsService { fun readAllScholarship(language: String, studentType: String): ScholarshipPageResponse fun readScholarship(scholarshipId: Long): ScholarshipDto - fun migrateAcademicsDetail( - studentType: String, - postType: String, - requestList: List - ): List - - fun migrateCourses(studentType: String, requestList: List): List - fun migrateScholarshipDetail( + fun updateGuide( + language: String, studentType: String, - requestList: List - ): List - - fun migrateAcademicsDetailAttachments( - academicsId: Long, - attachments: List? - ): AcademicsDto + request: UpdateSingleReq, + newAttachments: List? + ) } // TODO: add Update, Delete method @@ -119,6 +111,34 @@ class AcademicsServiceImpl( return GuidePageResponse.of(academicsEntity, attachmentResponses) } + @Transactional + override fun updateGuide( + language: String, + studentType: String, + request: UpdateSingleReq, + newAttachments: List? + ) { + val languageType = LanguageType.makeStringToLanguageType(language) + val enumStudentType = makeStringToAcademicsStudentType(studentType) + + val academicsEntity = + academicsRepository.findByLanguageAndStudentTypeAndPostType( + languageType, + enumStudentType, + AcademicsPostType.GUIDE + ) + + academicsEntity.description = request.description + academicsEntity.academicsSearch?.update(academicsEntity) ?: let { + academicsEntity.academicsSearch = AcademicsSearchEntity.create(academicsEntity) + } + + attachmentService.deleteAttachments(request.deleteIds) + if (newAttachments != null) { + attachmentService.uploadAllAttachments(academicsEntity, newAttachments) + } + } + @Transactional(readOnly = true) override fun readAcademicsYearResponses( language: String, @@ -178,6 +198,32 @@ class AcademicsServiceImpl( return DegreeRequirementsPageResponse.of(academicsEntity, attachments) } + @Transactional + override fun updateDegreeRequirements( + language: String, + request: UpdateSingleReq, + newAttachments: List? + ) { + val enumLanguageType = LanguageType.makeStringToLanguageType(language) + + val academicsEntity = + academicsRepository.findByLanguageAndStudentTypeAndPostType( + enumLanguageType, + AcademicsStudentType.UNDERGRADUATE, + AcademicsPostType.DEGREE_REQUIREMENTS + ) + + academicsEntity.description = request.description + academicsEntity.academicsSearch?.update(academicsEntity) ?: let { + academicsEntity.academicsSearch = AcademicsSearchEntity.create(academicsEntity) + } + + attachmentService.deleteAttachments(request.deleteIds) + if (newAttachments != null) { + attachmentService.uploadAllAttachments(academicsEntity, newAttachments) + } + } + @Transactional override fun createCourse( studentType: String, @@ -270,104 +316,6 @@ class AcademicsServiceImpl( return ScholarshipDto.of(scholarship) } - @Transactional - override fun migrateAcademicsDetail( - studentType: String, - postType: String, - requestList: List - ): List { - val enumStudentType = makeStringToAcademicsStudentType(studentType) - val enumPostType = makeStringToAcademicsPostType(postType) - val list = mutableListOf() - for (request in requestList) { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newAcademics = AcademicsEntity.of( - enumStudentType, - enumPostType, - enumLanguageType, - request - ) - - newAcademics.apply { - academicsSearch = AcademicsSearchEntity.create(this) - } - - academicsRepository.save(newAcademics) - - list.add(AcademicsDto.of(newAcademics, listOf())) - } - - return list - } - - @Transactional - override fun migrateCourses( - studentType: String, - requestList: List - ): List { - val enumStudentType = makeStringToAcademicsStudentType(studentType) - val list = mutableListOf() - for (request in requestList) { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newCourse = CourseEntity.of(enumStudentType, enumLanguageType, request) - - newCourse.apply { - academicsSearch = AcademicsSearchEntity.create(this) - } - courseRepository.save(newCourse) - - list.add(CourseDto.of(newCourse, listOf())) - } - - return list - } - - @Transactional - override fun migrateScholarshipDetail( - studentType: String, - requestList: List - ): List { - val enumStudentType = makeStringToAcademicsStudentType(studentType) - val list = mutableListOf() - for (request in requestList) { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val newScholarship = ScholarshipEntity.of( - enumLanguageType, - enumStudentType, - request - ) - - newScholarship.apply { - academicsSearch = AcademicsSearchEntity.create(this) - } - - scholarshipRepository.save(newScholarship) - - list.add(ScholarshipDto.of(newScholarship)) - } - - return list - } - - @Transactional - override fun migrateAcademicsDetailAttachments( - academicsId: Long, - attachments: List? - ): AcademicsDto { - val academics = academicsRepository.findByIdOrNull(academicsId) - ?: throw CserealException.Csereal404("해당 내용을 찾을 수 없습니다.") - - if (attachments != null) { - attachmentService.uploadAllAttachments(academics, attachments) - } - - val attachmentResponses = attachmentService.createAttachmentResponses( - academics.attachments - ) - - return AcademicsDto.of(academics, attachmentResponses) - } - private fun makeStringToAcademicsStudentType(postType: String): AcademicsStudentType { try { val upperPostType = postType.replace("-", "_").uppercase() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/internal/api/InternalController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/internal/api/InternalController.kt new file mode 100644 index 00000000..a58378cc --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/internal/api/InternalController.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.csereal.core.internal.api + +import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.core.internal.dto.InternalDto +import com.wafflestudio.csereal.core.internal.service.InternalService +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/v1/internal") +class InternalController( + private val internalService: InternalService +) { + @GetMapping + fun getInternal(): InternalDto = + internalService.getInternal() + + @PutMapping + @AuthenticatedStaff + fun putInternal( + @RequestBody @Valid + req: InternalDto + ): InternalDto = + internalService.modifyInternal(req) +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/internal/database/InternalEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/internal/database/InternalEntity.kt new file mode 100644 index 00000000..20704a34 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/internal/database/InternalEntity.kt @@ -0,0 +1,22 @@ +package com.wafflestudio.csereal.core.internal.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.internal.dto.InternalDto +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.validation.constraints.NotBlank + +@Entity(name = "internal") +class InternalEntity( + @NotBlank + @Column(columnDefinition = "TEXT") + var description: String +) : BaseTimeEntity() { + fun update(dto: InternalDto) { + description = dto.description + } + + companion object { + fun of(dto: InternalDto) = InternalEntity(dto.description) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/internal/database/InternalRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/internal/database/InternalRepository.kt new file mode 100644 index 00000000..ba836b5f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/internal/database/InternalRepository.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.internal.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface InternalRepository : JpaRepository { + fun findFirstByOrderByModifiedAtDesc(): InternalEntity +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/internal/dto/InternalDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/internal/dto/InternalDto.kt new file mode 100644 index 00000000..9361be0c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/internal/dto/InternalDto.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.internal.dto + +import com.wafflestudio.csereal.core.internal.database.InternalEntity +import jakarta.validation.constraints.NotBlank + +data class InternalDto( + @field:NotBlank + val description: String +) { + companion object { + fun from(internal: InternalEntity) = InternalDto( + description = internal.description + ) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/internal/service/InternalService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/internal/service/InternalService.kt new file mode 100644 index 00000000..6beeb3d6 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/internal/service/InternalService.kt @@ -0,0 +1,44 @@ +package com.wafflestudio.csereal.core.internal.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.internal.database.InternalEntity +import com.wafflestudio.csereal.core.internal.database.InternalRepository +import com.wafflestudio.csereal.core.internal.dto.InternalDto +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface InternalService { + fun getInternal(): InternalDto + fun modifyInternal(updateDto: InternalDto): InternalDto +} + +@Service +class InternalServiceImpl( + private val internalRepository: InternalRepository +) : InternalService { + @Transactional + override fun getInternal(): InternalDto = + if (internalRepository.count() == 0L) { + throw CserealException.Csereal400("Internal이 존재하지 않습니다.") + } else { + internalRepository.findFirstByOrderByModifiedAtDesc().let { + InternalDto.from(it) + } + } + + @Transactional + override fun modifyInternal(updateDto: InternalDto): InternalDto = + when (internalRepository.count()) { + 0L -> internalRepository.save(InternalEntity.of(updateDto)) + + 1L -> internalRepository.findFirstByOrderByModifiedAtDesc() + .apply { update(updateDto) } + + else -> { + internalRepository.deleteAll() + internalRepository.save(InternalEntity.of(updateDto)) + } + }.let { + InternalDto.from(it) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt index 89e08f89..33312d20 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt @@ -15,12 +15,11 @@ class MainController( ) { @GetMapping fun readMain( - @RequestParam(required = false, defaultValue = "3") + @RequestParam(required = false) @Positive - importantCnt: Int - ): MainResponse { - return mainService.readMain(importantCnt) - } + importantCnt: Int? + ): MainResponse = + mainService.readMain(importantCnt) @GetMapping("/search/refresh") fun refreshSearches() { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt index b3c1b658..cf19f355 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt @@ -14,9 +14,9 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface MainService { - fun readMain(importantCnt: Int): MainResponse + fun readMain(importantCnt: Int?): MainResponse fun refreshSearch() - fun readMainImportant(cnt: Int): List + fun readMainImportant(cnt: Int? = null): List } @Service @@ -28,7 +28,7 @@ class MainServiceImpl( private val newsRepository: NewsRepository ) : MainService { @Transactional(readOnly = true) - override fun readMain(importantCnt: Int): MainResponse { + override fun readMain(importantCnt: Int?): MainResponse { val slides = mainRepository.readMainSlide() val noticeTotal = mainRepository.readMainNoticeTotal() @@ -43,13 +43,15 @@ class MainServiceImpl( } @Transactional(readOnly = true) - override fun readMainImportant(cnt: Int): List = + override fun readMainImportant(cnt: Int?): List = mutableListOf().apply { addAll(noticeRepository.findImportantNotice(cnt)) addAll(seminarRepository.findImportantSeminar(cnt)) addAll(newsRepository.findImportantNews(cnt)) sortByDescending { it.createdAt } - }.take(cnt) + }.let { + if (cnt != null) it.take(cnt) else it + } override fun refreshSearch() { eventPublisher.publishEvent(RefreshSearchEvent()) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt index 1cc860e0..da0622f9 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt @@ -1,11 +1,13 @@ package com.wafflestudio.csereal.core.member.api import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.core.member.api.req.CreateProfessorReqBody +import com.wafflestudio.csereal.core.member.api.req.ModifyProfessorReqBody import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.member.dto.ProfessorPageDto import com.wafflestudio.csereal.core.member.dto.SimpleProfessorDto import com.wafflestudio.csereal.core.member.service.ProfessorService -import org.springframework.context.annotation.Profile +import io.swagger.v3.oas.annotations.Parameter import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @@ -19,10 +21,10 @@ class ProfessorController( @AuthenticatedStaff @PostMapping fun createProfessor( - @RequestPart("request") createProfessorRequest: ProfessorDto, + @RequestPart("request") createProfessorRequest: CreateProfessorReqBody, @RequestPart("mainImage") mainImage: MultipartFile? - ): ResponseEntity { - return ResponseEntity.ok(professorService.createProfessor(createProfessorRequest, mainImage)) + ): ProfessorDto { + return professorService.createProfessor(createProfessorRequest, mainImage) } @GetMapping("/{professorId}") @@ -45,14 +47,17 @@ class ProfessorController( } @AuthenticatedStaff - @PatchMapping("/{professorId}") + @PutMapping("/{professorId}") fun updateProfessor( @PathVariable professorId: Long, - @RequestPart("request") updateProfessorRequest: ProfessorDto, - @RequestPart("mainImage") mainImage: MultipartFile? + @RequestPart("request") updateProfessorRequest: ModifyProfessorReqBody, + + @Parameter(description = "image 교체할 경우 업로드. Request Body의 removeImage 관계없이 변경됨.") + @RequestPart("newImage") + newImage: MultipartFile? ): ResponseEntity { return ResponseEntity.ok( - professorService.updateProfessor(professorId, updateProfessorRequest, mainImage) + professorService.updateProfessor(professorId, updateProfessorRequest, newImage) ) } @@ -62,21 +67,4 @@ class ProfessorController( professorService.deleteProfessor(professorId) return ResponseEntity.ok().build() } - - @Profile("!prod") - @PostMapping("/migrate") - fun migrateProfessors( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(professorService.migrateProfessors(requestList)) - } - - @Profile("!prod") - @PatchMapping("/migrateImage/{professorId}") - fun migrateProfessorImage( - @PathVariable professorId: Long, - @RequestPart("mainImage") mainImage: MultipartFile - ): ResponseEntity { - return ResponseEntity.ok(professorService.migrateProfessorImage(professorId, mainImage)) - } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt index d31a1ca2..137fa786 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt @@ -1,10 +1,13 @@ package com.wafflestudio.csereal.core.member.api import com.wafflestudio.csereal.common.aop.AuthenticatedStaff +import com.wafflestudio.csereal.core.member.api.req.CreateStaffReqBody +import com.wafflestudio.csereal.core.member.api.req.ModifyStaffReqBody import com.wafflestudio.csereal.core.member.dto.SimpleStaffDto import com.wafflestudio.csereal.core.member.dto.StaffDto import com.wafflestudio.csereal.core.member.service.StaffService -import org.springframework.context.annotation.Profile +import io.swagger.v3.oas.annotations.Parameter +import jakarta.validation.constraints.Positive import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile @@ -18,14 +21,16 @@ class StaffController( @AuthenticatedStaff @PostMapping fun createStaff( - @RequestPart("request") createStaffRequest: StaffDto, - @RequestPart("mainImage") mainImage: MultipartFile? - ): ResponseEntity { - return ResponseEntity.ok(staffService.createStaff(createStaffRequest, mainImage)) - } + @RequestPart("request") createStaffRequest: CreateStaffReqBody, + @RequestPart("image") image: MultipartFile? + ): StaffDto = + staffService.createStaff(createStaffRequest, image) @GetMapping("/{staffId}") - fun getStaff(@PathVariable staffId: Long): ResponseEntity { + fun getStaff( + @PathVariable @Positive + staffId: Long + ): ResponseEntity { return ResponseEntity.ok(staffService.getStaff(staffId)) } @@ -36,36 +41,26 @@ class StaffController( return ResponseEntity.ok(staffService.getAllStaff(language)) } + // swagger explanation @AuthenticatedStaff + @PutMapping("/{staffId}") fun updateStaff( - @PathVariable staffId: Long, - @RequestPart("request") updateStaffRequest: StaffDto, - @RequestPart("mainImage") mainImage: MultipartFile? - ): ResponseEntity { - return ResponseEntity.ok(staffService.updateStaff(staffId, updateStaffRequest, mainImage)) - } + @PathVariable @Positive + staffId: Long, + @RequestPart("request") modifyStaffReq: ModifyStaffReqBody, + @Parameter(description = "image 교체할 경우 업로드. Request Body의 removeImage 관계없이 변경됨.") + @RequestPart("newImage") + newImage: MultipartFile? + ): StaffDto = + staffService.updateStaff(staffId, modifyStaffReq, newImage) @AuthenticatedStaff @DeleteMapping("/{staffId}") - fun deleteStaff(@PathVariable staffId: Long): ResponseEntity { + fun deleteStaff( + @PathVariable @Positive + staffId: Long + ): ResponseEntity { staffService.deleteStaff(staffId) return ResponseEntity.ok().build() } - - @Profile("!prod") - @PostMapping("/migrate") - fun migrateStaff( - @RequestBody requestList: List - ): ResponseEntity> { - return ResponseEntity.ok(staffService.migrateStaff(requestList)) - } - - @Profile("!prod") - @PatchMapping("/migrateImage/{staffId}") - fun migrateStaffImage( - @PathVariable staffId: Long, - @RequestPart("mainImage") mainImage: MultipartFile - ): ResponseEntity { - return ResponseEntity.ok(staffService.migrateStaffImage(staffId, mainImage)) - } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/CreateProfessorReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/CreateProfessorReqBody.kt new file mode 100644 index 00000000..7985d020 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/CreateProfessorReqBody.kt @@ -0,0 +1,22 @@ +package com.wafflestudio.csereal.core.member.api.req + +import com.wafflestudio.csereal.core.member.database.ProfessorStatus +import java.time.LocalDate + +data class CreateProfessorReqBody( + val language: String, + val name: String, + val status: ProfessorStatus, + val academicRank: String, + val labId: Long?, + val startDate: LocalDate?, + val endDate: LocalDate?, + val office: String?, + val phone: String?, + val fax: String?, + val email: String?, + val website: String?, + val educations: List, + val researchAreas: List, + val careers: List +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/CreateStaffReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/CreateStaffReqBody.kt new file mode 100644 index 00000000..4a522395 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/CreateStaffReqBody.kt @@ -0,0 +1,11 @@ +package com.wafflestudio.csereal.core.member.api.req + +data class CreateStaffReqBody( + val language: String, + val name: String, + val role: String, + val office: String, + val phone: String, + val email: String, + val tasks: List +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/ModifyProfessorReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/ModifyProfessorReqBody.kt new file mode 100644 index 00000000..be793013 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/ModifyProfessorReqBody.kt @@ -0,0 +1,23 @@ +package com.wafflestudio.csereal.core.member.api.req + +import com.wafflestudio.csereal.core.member.database.ProfessorStatus +import java.time.LocalDate + +data class ModifyProfessorReqBody( + val language: String, + val name: String, + val status: ProfessorStatus, + val academicRank: String, + val labId: Long?, + val startDate: LocalDate?, + val endDate: LocalDate?, + val office: String?, + val phone: String?, + val fax: String?, + val email: String?, + val website: String?, + val educations: List, + val researchAreas: List, + val careers: List, + val removeImage: Boolean +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/ModifyStaffReqBody.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/ModifyStaffReqBody.kt new file mode 100644 index 00000000..c8521ada --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/req/ModifyStaffReqBody.kt @@ -0,0 +1,12 @@ +package com.wafflestudio.csereal.core.member.api.req + +data class ModifyStaffReqBody( + val language: String, + val name: String, + val role: String, + val office: String, + val phone: String, + val email: String, + val tasks: List, + val removeImage: Boolean +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/MemberSearchEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/MemberSearchEntity.kt index 6c4d0d13..dd75e75c 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/MemberSearchEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/MemberSearchEntity.kt @@ -9,7 +9,7 @@ class MemberSearchEntity( @Column(columnDefinition = "TEXT") var content: String, - val language: LanguageType, + var language: LanguageType, @OneToOne @JoinColumn(name = "professor_id") @@ -91,10 +91,12 @@ class MemberSearchEntity( } fun update(professor: ProfessorEntity) { + this.language = professor.language this.content = createContent(professor) } fun update(staff: StaffEntity) { + this.language = staff.language this.content = createContent(staff) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorEntity.kt index b0f50258..b330d62b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorEntity.kt @@ -74,19 +74,6 @@ class ProfessorEntity( this.lab = lab lab.professors.add(this) } - - fun update(updateProfessorRequest: ProfessorDto) { - this.name = updateProfessorRequest.name - this.status = updateProfessorRequest.status - this.academicRank = updateProfessorRequest.academicRank - this.startDate = updateProfessorRequest.startDate - this.endDate = updateProfessorRequest.endDate - this.office = updateProfessorRequest.office - this.phone = updateProfessorRequest.phone - this.fax = updateProfessorRequest.fax - this.email = updateProfessorRequest.email - this.website = updateProfessorRequest.website - } } enum class ProfessorStatus( diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorDto.kt index 36423d7b..732bada6 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorDto.kt @@ -1,14 +1,12 @@ package com.wafflestudio.csereal.core.member.dto -import com.fasterxml.jackson.annotation.JsonInclude import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.core.member.database.ProfessorEntity import com.wafflestudio.csereal.core.member.database.ProfessorStatus import java.time.LocalDate data class ProfessorDto( - @JsonInclude(JsonInclude.Include.NON_NULL) - var id: Long? = null, + var id: Long, val language: String, val name: String, val status: ProfessorStatus, @@ -25,9 +23,7 @@ data class ProfessorDto( val educations: List, val researchAreas: List, val careers: List, - @JsonInclude(JsonInclude.Include.NON_NULL) var imageURL: String? = null - ) { companion object { fun of(professorEntity: ProfessorEntity, imageURL: String?): ProfessorDto { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt index 59eebfaf..6194c736 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt @@ -1,12 +1,10 @@ package com.wafflestudio.csereal.core.member.dto -import com.fasterxml.jackson.annotation.JsonInclude import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.core.member.database.StaffEntity data class StaffDto( - @JsonInclude(JsonInclude.Include.NON_NULL) - var id: Long? = null, + var id: Long, val language: String, val name: String, val role: String, @@ -14,8 +12,7 @@ data class StaffDto( val phone: String, val email: String, val tasks: List, - @JsonInclude(JsonInclude.Include.NON_NULL) - val imageURL: String? = null + val imageURL: String? ) { companion object { fun of(staffEntity: StaffEntity, imageURL: String?): StaffDto { 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 5555329d..a27da618 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 @@ -3,6 +3,8 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.common.enums.LanguageType import com.wafflestudio.csereal.common.utils.startsWithEnglish +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.* import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.member.dto.ProfessorPageDto @@ -19,19 +21,16 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile interface ProfessorService { - fun createProfessor(createProfessorRequest: ProfessorDto, mainImage: MultipartFile?): ProfessorDto + fun createProfessor(createProfessorRequest: CreateProfessorReqBody, mainImage: MultipartFile?): ProfessorDto fun getProfessor(professorId: Long): ProfessorDto fun getActiveProfessors(language: String): ProfessorPageDto fun getInactiveProfessors(language: String): List fun updateProfessor( professorId: Long, - updateProfessorRequest: ProfessorDto, + updateProfessorRequest: ModifyProfessorReqBody, mainImage: MultipartFile? ): ProfessorDto - fun deleteProfessor(professorId: Long) - fun migrateProfessors(requestList: List): List - fun migrateProfessorImage(professorId: Long, mainImage: MultipartFile): ProfessorDto } @Service @@ -43,11 +42,26 @@ class ProfessorServiceImpl( private val applicationEventPublisher: ApplicationEventPublisher ) : ProfessorService { override fun createProfessor( - createProfessorRequest: ProfessorDto, + createProfessorRequest: CreateProfessorReqBody, mainImage: MultipartFile? ): ProfessorDto { val enumLanguageType = LanguageType.makeStringToLanguageType(createProfessorRequest.language) - val professor = ProfessorEntity.of(enumLanguageType, createProfessorRequest) + val professor = createProfessorRequest.run { + ProfessorEntity( + language = enumLanguageType, + name = name, + status = status, + academicRank = academicRank, + startDate = startDate, + endDate = endDate, + office = office, + phone = phone, + fax = fax, + email = email, + website = website + ) + } + if (createProfessorRequest.labId != null) { val lab = labRepository.findByIdOrNull(createProfessorRequest.labId) ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${createProfessorRequest.labId}") @@ -160,62 +174,76 @@ class ProfessorServiceImpl( override fun updateProfessor( professorId: Long, - updateProfessorRequest: ProfessorDto, - mainImage: MultipartFile? + updateReq: ModifyProfessorReqBody, + newImage: MultipartFile? ): ProfessorDto { val professor = professorRepository.findByIdOrNull(professorId) ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: $professorId") + // Lab 업데이트 val outdatedLabId = professor.lab?.id - - if (updateProfessorRequest.labId != null && updateProfessorRequest.labId != professor.lab?.id) { - val lab = labRepository.findByIdOrNull(updateProfessorRequest.labId) + if (updateReq.labId != null && updateReq.labId != professor.lab?.id) { + val lab = labRepository.findByIdOrNull(updateReq.labId) ?: throw CserealException.Csereal404( - "해당 연구실을 찾을 수 없습니다. LabId: ${updateProfessorRequest.labId}" + "해당 연구실을 찾을 수 없습니다. LabId: ${updateReq.labId}" ) professor.addLab(lab) } - professor.update(updateProfessorRequest) + // 교수 정보 업데이트 + updateReq.let { + professor.run { + language = LanguageType.makeStringToLanguageType(it.language) + name = it.name + status = it.status + academicRank = it.academicRank + startDate = it.startDate + endDate = it.endDate + office = it.office + phone = it.phone + fax = it.fax + email = it.email + website = it.website + } + } - if (mainImage != null) { - mainImageService.uploadMainImage(professor, mainImage) - } else { - professor.mainImage = null + // Main Image 업데이트 + if (updateReq.removeImage && newImage == null) { + if (professor.mainImage != null) { + mainImageService.removeImage(professor.mainImage!!) + professor.mainImage = null + } + } else if (newImage != null) { + professor.mainImage?.let { + mainImageService.removeImage(it) + } + mainImageService.uploadMainImage(professor, newImage) } // 학력 업데이트 val oldEducations = professor.educations.map { it.name } - val educationsToRemove = oldEducations - updateProfessorRequest.educations - val educationsToAdd = updateProfessorRequest.educations - oldEducations - + val educationsToRemove = oldEducations - updateReq.educations + val educationsToAdd = updateReq.educations - oldEducations professor.educations.removeIf { it.name in educationsToRemove } - for (education in educationsToAdd) { EducationEntity.create(education, professor) } // 연구 분야 업데이트 val oldResearchAreas = professor.researchAreas.map { it.name } - - val researchAreasToRemove = oldResearchAreas - updateProfessorRequest.researchAreas - val researchAreasToAdd = updateProfessorRequest.researchAreas - oldResearchAreas - + val researchAreasToRemove = oldResearchAreas - updateReq.researchAreas + val researchAreasToAdd = updateReq.researchAreas - oldResearchAreas professor.researchAreas.removeIf { it.name in researchAreasToRemove } - for (researchArea in researchAreasToAdd) { ResearchAreaEntity.create(researchArea, professor) } // 경력 업데이트 val oldCareers = professor.careers.map { it.name } - - val careersToRemove = oldCareers - updateProfessorRequest.careers - val careersToAdd = updateProfessorRequest.careers - oldCareers - + val careersToRemove = oldCareers - updateReq.careers + val careersToAdd = updateReq.careers - oldCareers professor.careers.removeIf { it.name in careersToRemove } - for (career in careersToAdd) { CareerEntity.create(career, professor) } @@ -229,65 +257,20 @@ class ProfessorServiceImpl( ) val imageURL = mainImageService.createImageURL(professor.mainImage) - return ProfessorDto.of(professor, imageURL) } override fun deleteProfessor(professorId: Long) { val professorEntity = professorRepository.findByIdOrNull(professorId) ?: return - professorRepository.deleteById(professorId) + professorEntity.mainImage?.let { + mainImageService.removeImage(it) + } + + professorRepository.delete(professorEntity) applicationEventPublisher.publishEvent( ProfessorDeletedEvent.of(professorEntity) ) } - - @Transactional - override fun migrateProfessors(requestList: List): List { - val list = mutableListOf() - - for (request in requestList) { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val professor = ProfessorEntity.of(enumLanguageType, request) - if (request.labName != null) { - val lab = labRepository.findByName(request.labName) - ?: throw CserealException.Csereal404( - "해당 연구실을 찾을 수 없습니다. LabName: ${request.labName}" - ) - professor.addLab(lab) - } - - for (education in request.educations) { - EducationEntity.create(education, professor) - } - - for (researchArea in request.researchAreas) { - ResearchAreaEntity.create(researchArea, professor) - } - - for (career in request.careers) { - CareerEntity.create(career, professor) - } - - professor.memberSearch = MemberSearchEntity.create(professor) - - professorRepository.save(professor) - - list.add(ProfessorDto.of(professor, null)) - } - return list - } - - @Transactional - override fun migrateProfessorImage(professorId: Long, mainImage: MultipartFile): ProfessorDto { - val professor = professorRepository.findByIdOrNull(professorId) - ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: $professorId") - - mainImageService.uploadMainImage(professor, mainImage) - - val imageURL = mainImageService.createImageURL(professor.mainImage) - - return ProfessorDto.of(professor, imageURL) - } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt index bf4cff86..d914354e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt @@ -2,6 +2,8 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.member.api.req.CreateStaffReqBody +import com.wafflestudio.csereal.core.member.api.req.ModifyStaffReqBody import com.wafflestudio.csereal.core.member.database.MemberSearchEntity import com.wafflestudio.csereal.core.member.database.StaffEntity import com.wafflestudio.csereal.core.member.database.StaffRepository @@ -15,13 +17,11 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile interface StaffService { - fun createStaff(createStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto fun getStaff(staffId: Long): StaffDto fun getAllStaff(language: String): List - fun updateStaff(staffId: Long, updateStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto fun deleteStaff(staffId: Long) - fun migrateStaff(requestList: List): List - fun migrateStaffImage(staffId: Long, mainImage: MultipartFile): StaffDto + fun createStaff(createStaffRequest: CreateStaffReqBody, mainImage: MultipartFile?): StaffDto + fun updateStaff(staffId: Long, req: ModifyStaffReqBody, newImage: MultipartFile?): StaffDto } @Service @@ -30,12 +30,20 @@ class StaffServiceImpl( private val staffRepository: StaffRepository, private val mainImageService: MainImageService ) : StaffService { - override fun createStaff(createStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto { - val enumLanguageType = LanguageType.makeStringToLanguageType(createStaffRequest.language) - val staff = StaffEntity.of(enumLanguageType, createStaffRequest) + override fun createStaff(createStaffRequest: CreateStaffReqBody, mainImage: MultipartFile?): StaffDto { + val staff = createStaffRequest.run { + StaffEntity( + language = LanguageType.makeStringToLanguageType(language), + name, + role, + office, + phone, + email + ) + } - for (task in createStaffRequest.tasks) { - TaskEntity.create(task, staff) + createStaffRequest.tasks.forEach { + TaskEntity.create(it, staff) } if (mainImage != null) { @@ -78,23 +86,35 @@ class StaffServiceImpl( return sortedStaff } - override fun updateStaff(staffId: Long, updateStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto { + override fun updateStaff(staffId: Long, req: ModifyStaffReqBody, newImage: MultipartFile?): StaffDto { val staff = staffRepository.findByIdOrNull(staffId) ?: throw CserealException.Csereal404("해당 행정직원을 찾을 수 없습니다. staffId: $staffId") - staff.update(updateStaffRequest) + staff.run { + language = LanguageType.makeStringToLanguageType(req.language) + name = req.name + role = req.role + office = req.office + phone = req.phone + email = req.email + } - if (mainImage != null) { - mainImageService.uploadMainImage(staff, mainImage) - } else { - staff.mainImage = null + if (req.removeImage && newImage == null) { + if (staff.mainImage != null) { + mainImageService.removeImage(staff.mainImage!!) + staff.mainImage = null + } + } else if (newImage != null) { + staff.mainImage ?. let { + mainImageService.removeImage(it) + } + mainImageService.uploadMainImage(staff, newImage) } // 주요 업무 업데이트 val oldTasks = staff.tasks.map { it.name } - - val tasksToRemove = oldTasks - updateStaffRequest.tasks - val tasksToAdd = updateStaffRequest.tasks - oldTasks + val tasksToRemove = oldTasks - req.tasks + val tasksToAdd = req.tasks - oldTasks staff.tasks.removeIf { it.name in tasksToRemove } @@ -103,7 +123,9 @@ class StaffServiceImpl( } // 검색 엔티티 업데이트 - staff.memberSearch?.update(staff) + staff.memberSearch?.update(staff) ?: let { + staff.memberSearch = MemberSearchEntity.create(staff) + } val imageURL = mainImageService.createImageURL(staff.mainImage) @@ -111,40 +133,11 @@ class StaffServiceImpl( } override fun deleteStaff(staffId: Long) { - staffRepository.deleteById(staffId) - } - - @Transactional - override fun migrateStaff(requestList: List): List { - val list = mutableListOf() - - for (request in requestList) { - val enumLanguageType = LanguageType.makeStringToLanguageType(request.language) - val staff = StaffEntity.of(enumLanguageType, request) - - for (task in request.tasks) { - TaskEntity.create(task, staff) + staffRepository.findByIdOrNull(staffId) ?. let { staff -> + staff.mainImage?.let { + mainImageService.removeImage(it) } - - staff.memberSearch = MemberSearchEntity.create(staff) - - staffRepository.save(staff) - - list.add(StaffDto.of(staff, null)) + staffRepository.delete(staff) } - - return list - } - - @Transactional - override fun migrateStaffImage(staffId: Long, mainImage: MultipartFile): StaffDto { - val staff = staffRepository.findByIdOrNull(staffId) - ?: throw CserealException.Csereal404("해당 행정직원을 찾을 수 없습니다. staffId: $staffId") - - mainImageService.uploadMainImage(staff, mainImage) - - val imageURL = mainImageService.createImageURL(staff.mainImage) - - return StaffDto.of(staff, imageURL) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt index 48cbe0bd..f5326b1f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt @@ -58,7 +58,7 @@ interface CustomNewsRepository { ): NewsTotalSearchDto fun readAllSlides(pageNum: Long, pageSize: Int): AdminSlidesResponse - fun findImportantNews(cnt: Int): List + fun findImportantNews(cnt: Int? = null): List } @Repository @@ -240,7 +240,7 @@ class NewsRepositoryImpl( ) } - override fun findImportantNews(cnt: Int): List = + override fun findImportantNews(cnt: Int?): List = queryFactory.select( Projections.constructor( MainImportantResponse::class.java, @@ -258,6 +258,8 @@ class NewsRepositoryImpl( newsEntity.isDeleted.isFalse() ).orderBy( newsEntity.createdAt.desc() - ).limit(cnt.toLong()) + ).let { + if (cnt != null) it.limit(cnt.toLong()) else it + } .fetch() } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt index dd08cce7..7cb7045d 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt @@ -143,7 +143,7 @@ class NewsServiceImpl( mainImageService.uploadMainImage(news, newMainImage) } - attachmentService.deleteAttachments(request.deleteIds) + attachmentService.deleteAttachmentsDeprecated(request.deleteIds) if (newAttachments != null) { attachmentService.uploadAllAttachments(news, newAttachments) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt index 4b42ff23..4c748674 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt @@ -45,7 +45,7 @@ interface CustomNoticeRepository { fun totalSearchNotice(keyword: String, number: Int, stringLength: Int, isStaff: Boolean): NoticeTotalSearchResponse - fun findImportantNotice(cnt: Int): List + fun findImportantNotice(cnt: Int? = null): List } @Component @@ -188,7 +188,7 @@ class NoticeRepositoryImpl( return NoticeSearchResponse(total, noticeSearchDtoList) } - override fun findImportantNotice(cnt: Int): List = + override fun findImportantNotice(cnt: Int?): List = queryFactory.select( Projections.constructor( MainImportantResponse::class.java, @@ -206,6 +206,8 @@ class NoticeRepositoryImpl( noticeEntity.isDeleted.isFalse() ).orderBy( noticeEntity.createdAt.desc() - ).limit(cnt.toLong()) + ).let { + if (cnt != null) { it.limit(cnt.toLong()) } else it + } .fetch() } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt index a929d997..88b59a06 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt @@ -142,7 +142,7 @@ class NoticeServiceImpl( notice.update(request) - attachmentService.deleteAttachments(request.deleteIds) + attachmentService.deleteAttachmentsDeprecated(request.deleteIds) if (newAttachments != null) { attachmentService.uploadAllAttachments(notice, newAttachments) 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 5070294b..cc0b7c63 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 @@ -289,7 +289,7 @@ class ResearchServiceImpl( // update pdf if (request.pdfModified) { - labEntity.pdf?.let { attachmentService.deleteAttachment(it) } + labEntity.pdf?.let { attachmentService.deleteAttachmentDeprecated(it) } pdf?.let { val attachmentDto = attachmentService.uploadAttachmentInLabEntity(labEntity, it) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt index 780f574a..ffd7588f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt @@ -5,6 +5,7 @@ import com.wafflestudio.csereal.core.reservation.database.* import com.wafflestudio.csereal.core.reservation.dto.ReservationDto import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest import com.wafflestudio.csereal.core.reservation.dto.SimpleReservationDto +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 org.springframework.data.repository.findByIdOrNull @@ -41,6 +42,10 @@ class ReservationServiceImpl( RequestAttributes.SCOPE_REQUEST ) as UserEntity? ?: userRepository.findByUsername("devUser")!! + if (reserveRequest.roomId == 8L && user.role != Role.ROLE_STAFF) { + throw CserealException.Csereal403("교수회의실 예약 행정실 문의 바람") + } + val room = roomRepository.findByIdOrNull(reserveRequest.roomId) ?: throw CserealException.Csereal404("Room Not Found") 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 15d7410e..fecb4a42 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 @@ -14,8 +14,10 @@ import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentEnti import com.wafflestudio.csereal.core.resource.attachment.database.AttachmentRepository import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentDto import com.wafflestudio.csereal.core.resource.attachment.dto.AttachmentResponse +import com.wafflestudio.csereal.core.resource.common.event.FileDeleteEvent import com.wafflestudio.csereal.core.seminar.database.SeminarEntity import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -33,11 +35,14 @@ interface AttachmentService { contentEntityType: AttachmentContentEntityType, requestAttachments: List ): List + fun createOneAttachmentResponse(attachment: AttachmentEntity?): AttachmentResponse? fun createAttachmentResponses(attachments: List?): List - fun deleteAttachments(ids: List?) fun deleteAttachment(attachment: AttachmentEntity) + fun deleteAttachments(ids: List?) + fun deleteAttachmentsDeprecated(ids: List?) + fun deleteAttachmentDeprecated(attachment: AttachmentEntity) } @Service @@ -45,7 +50,8 @@ class AttachmentServiceImpl( private val attachmentRepository: AttachmentRepository, @Value("\${csereal.upload.path}") private val path: String, - private val endpointProperties: EndpointProperties + private val endpointProperties: EndpointProperties, + private val eventPublisher: ApplicationEventPublisher ) : AttachmentService { override fun uploadAttachmentInLabEntity(labEntity: LabEntity, requestAttachment: MultipartFile): AttachmentDto { Files.createDirectories(Paths.get(path)) @@ -148,7 +154,7 @@ class AttachmentServiceImpl( } @Transactional - override fun deleteAttachments(ids: List?) { + override fun deleteAttachmentsDeprecated(ids: List?) { if (ids != null) { for (id in ids) { val attachment = attachmentRepository.findByIdOrNull(id) @@ -158,6 +164,29 @@ class AttachmentServiceImpl( } } + @Transactional + override fun deleteAttachmentDeprecated(attachment: AttachmentEntity) { + attachment.isDeleted = true + } + + @Transactional + override fun deleteAttachment(attachment: AttachmentEntity) { + val fileDirectory = path + attachment.filename + attachmentRepository.delete(attachment) + eventPublisher.publishEvent(FileDeleteEvent(fileDirectory)) + } + + @Transactional + override fun deleteAttachments(ids: List?) { + if (ids != null) { + for (id in ids) { + val attachment = attachmentRepository.findByIdOrNull(id) + ?: throw CserealException.Csereal404("id:${id}인 첨부파일을 찾을 수 없습니다.") + deleteAttachment(attachment) + } + } + } + private fun connectAttachmentToEntity(contentEntity: AttachmentContentEntityType, attachment: AttachmentEntity) { when (contentEntity) { is NewsEntity -> { @@ -196,9 +225,4 @@ class AttachmentServiceImpl( } } } - - @Transactional - override fun deleteAttachment(attachment: AttachmentEntity) { - attachment.isDeleted = true - } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/event/FileDeleteEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/event/FileDeleteEvent.kt new file mode 100644 index 00000000..add3a7d0 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/event/FileDeleteEvent.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.resource.common.event + +data class FileDeleteEvent( + val filename: String +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/service/CommonFileService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/service/CommonFileService.kt new file mode 100644 index 00000000..8f458006 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/common/service/CommonFileService.kt @@ -0,0 +1,27 @@ +package com.wafflestudio.csereal.core.resource.common.service + +import com.wafflestudio.csereal.core.resource.common.event.FileDeleteEvent +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import org.springframework.transaction.event.TransactionPhase +import org.springframework.transaction.event.TransactionalEventListener +import java.io.File + +interface CommonFileService { + fun removeFile(fileDeleteEvent: FileDeleteEvent) +} + +@Service +class CommonFileServiceImpl() : CommonFileService { + private val log = LoggerFactory.getLogger(this::class.java) + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + override fun removeFile(fileDeleteEvent: FileDeleteEvent) { + val file = File(fileDeleteEvent.filename) + if (file.exists()) { + if (!file.delete()) { + log.warn("${file.path} is not deleted.") + } + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt index 614ced84..39ea9b2b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt @@ -8,6 +8,7 @@ import com.wafflestudio.csereal.core.member.database.ProfessorEntity import com.wafflestudio.csereal.core.member.database.StaffEntity import com.wafflestudio.csereal.core.news.database.NewsEntity import com.wafflestudio.csereal.core.research.database.ResearchEntity +import com.wafflestudio.csereal.core.resource.common.event.FileDeleteEvent import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageRepository import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import com.wafflestudio.csereal.core.resource.mainImage.dto.MainImageDto @@ -17,6 +18,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import org.apache.commons.io.FilenameUtils +import org.springframework.context.ApplicationEventPublisher import java.lang.invoke.WrongMethodTypeException import java.nio.file.Files import java.nio.file.Paths @@ -28,6 +30,8 @@ interface MainImageService { ): MainImageDto fun createImageURL(image: MainImageEntity?): String? + + fun removeImage(image: MainImageEntity) } @Service @@ -35,7 +39,8 @@ class MainImageServiceImpl( private val mainImageRepository: MainImageRepository, @Value("\${csereal.upload.path}") private val path: String, - private val endpointProperties: EndpointProperties + private val endpointProperties: EndpointProperties, + private val eventPublisher: ApplicationEventPublisher ) : MainImageService { @Transactional @@ -74,6 +79,7 @@ class MainImageServiceImpl( ) } + // TODO: `MainImageEntity`의 메서드로 refactoring하기. @Transactional override fun createImageURL(mainImage: MainImageEntity?): String? { return if (mainImage != null) { @@ -83,6 +89,14 @@ class MainImageServiceImpl( } } + @Transactional + override fun removeImage(image: MainImageEntity) { + val fileDirectory = path + image.filename + mainImageRepository.delete(image) + eventPublisher.publishEvent(FileDeleteEvent(fileDirectory)) + } + + // TODO: 각 entity의 interface로 refactoring하기. private fun connectMainImageToEntity(contentEntity: MainImageContentEntityType, mainImage: MainImageEntity) { when (contentEntity) { is NewsEntity -> { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarRepository.kt index 1f946907..7466430f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarRepository.kt @@ -40,7 +40,7 @@ interface CustomSeminarRepository { isStaff: Boolean ): SeminarSearchResponse - fun findImportantSeminar(cnt: Int): List + fun findImportantSeminar(cnt: Int? = null): List } @Component @@ -137,7 +137,7 @@ class SeminarRepositoryImpl( return SeminarSearchResponse(total, seminarSearchDtoList) } - override fun findImportantSeminar(cnt: Int): List = + override fun findImportantSeminar(cnt: Int?): List = queryFactory.select( Projections.constructor( MainImportantResponse::class.java, @@ -155,6 +155,8 @@ class SeminarRepositoryImpl( seminarEntity.isPrivate.isFalse() ).orderBy( seminarEntity.createdAt.desc() - ).limit(cnt.toLong()) + ).let { + if (cnt != null) it.limit(cnt.toLong()) else it + } .fetch() } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/service/SeminarService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/service/SeminarService.kt index 796c1c6b..51d5dcc3 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/service/SeminarService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/service/SeminarService.kt @@ -118,7 +118,7 @@ class SeminarServiceImpl( mainImageService.uploadMainImage(seminar, newMainImage) } - attachmentService.deleteAttachments(request.deleteIds) + attachmentService.deleteAttachmentsDeprecated(request.deleteIds) if (newAttachments != null) { attachmentService.uploadAllAttachments(seminar, newAttachments) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 835e2c45..b8369d32 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -25,7 +25,9 @@ server: max: 32 servlet: session: - timeout: 7200 # 2시간 + timeout: 32400 # 9시간 + cookie: + same-site: lax springdoc: paths-to-match: diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/internal/service/InternalServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/internal/service/InternalServiceTest.kt new file mode 100644 index 00000000..528c6386 --- /dev/null +++ b/src/test/kotlin/com/wafflestudio/csereal/core/internal/service/InternalServiceTest.kt @@ -0,0 +1,104 @@ +package com.wafflestudio.csereal.core.internal.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.internal.database.InternalEntity +import com.wafflestudio.csereal.core.internal.database.InternalRepository +import com.wafflestudio.csereal.core.internal.dto.InternalDto +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.extensions.spring.SpringTestExtension +import io.kotest.extensions.spring.SpringTestLifecycleMode +import io.kotest.matchers.longs.shouldBeGreaterThan +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.repository.findByIdOrNull +import org.springframework.transaction.annotation.Transactional + +@SpringBootTest +@Transactional +class InternalServiceTest( + private val internalService: InternalService, + private val internalRepository: InternalRepository +) : BehaviorSpec({ + extensions(SpringTestExtension(SpringTestLifecycleMode.Root)) + + Given("Internal이 비어있을 때") { + When("Get을 실행하면") { + Then("400 에러를 발생시켜야 한다.") { + val exc: CserealException.Csereal400 = shouldThrow { + internalService.getInternal() + } + } + } + When("Modify를 실행하면") { + val desc = "Test" + val modifyDto = InternalDto(desc) + val returnDto = internalService.modifyInternal(modifyDto) + Then("새로운 entity를 생성해야 한다.") { + internalRepository.count() shouldBe 1L + } + Then("생성된 entity의 설명이 일치해야 한다.") { + internalRepository.findFirstByOrderByModifiedAtDesc().description shouldBe desc + } + } + } + + Given("Internal이 하나 있는 경우 (정상적인 상황)") { + val desc = "

Hello

" + val originalEntity = internalRepository.save(InternalEntity(desc)) + When("Get을 실행하면") { + val dto = internalService.getInternal() + Then("해당 dto가 반환되어야 한다.") { + dto shouldBe InternalDto(desc) + } + } + When("Modify를 실행하면") { + val modDesc = "

Bye

" + val modDto = InternalDto(modDesc) + val returnDto = internalService.modifyInternal(modDto) + + Then("기존 entity의 설명이 변경되어야 한다.") { + internalRepository.findByIdOrNull(originalEntity.id)!!.description shouldBe modDesc + } + Then("변경된 entity의 설명이 반환되어야 한다.") { + returnDto shouldBe InternalDto(modDesc) + } + } + } + + Given("Internal이 여러개 있는 경우") { + val desc = "

Hello

" + val originalEntity = internalRepository.save(InternalEntity(desc)) + val desc2 = "

Bye

" + val originalEntity2 = internalRepository.save(InternalEntity(desc2)) + val largestId = maxOf(originalEntity.id, originalEntity2.id) + + When("Get을 실행하면") { + val dto = internalService.getInternal() + Then("가장 최근 entity의 설명이 반환되어야 한다.") { + dto shouldBe InternalDto(desc2) + } + } + + When("Modify를 실행하면") { + val modDesc = "

BBBBB

" + val modDto = InternalDto(modDesc) + val returnDto = internalService.modifyInternal(modDto) + + Then("기존 entity들이 모두 삭제되어야 한다.") { + internalRepository.findByIdOrNull(originalEntity.id) shouldBe null + internalRepository.findByIdOrNull(originalEntity2.id) shouldBe null + internalRepository.count() shouldBe 1L + } + Then("새로운 entity가 생성되어야 한다.") { + internalRepository.findFirstByOrderByModifiedAtDesc().let { + it.id shouldBeGreaterThan largestId + it.description shouldBe modDesc + } + } + Then("변경된 entity의 설명이 반환되어야 한다.") { + returnDto shouldBe InternalDto(modDesc) + } + } + } +}) 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 1f880acd..c6b22697 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 @@ -1,10 +1,11 @@ package com.wafflestudio.csereal.core.member.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.MemberSearchRepository import com.wafflestudio.csereal.core.member.database.ProfessorRepository import com.wafflestudio.csereal.core.member.database.ProfessorStatus -import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.research.database.* import io.kotest.core.spec.style.BehaviorSpec import io.kotest.extensions.spring.SpringTestExtension @@ -59,14 +60,13 @@ class ProfessorServiceTest( researchRepository.save(researchEntity) labEntity = labRepository.save(labEntity) - val professorDto = ProfessorDto( + val professorCreateReq = CreateProfessorReqBody( language = "ko", name = "name", email = "email", status = ProfessorStatus.ACTIVE, academicRank = "academicRank", labId = labEntity.id, - labName = null, startDate = date, endDate = date, office = "office", @@ -79,7 +79,7 @@ class ProfessorServiceTest( ) When("교수를 생성한다면") { - val createdProfessorDto = professorService.createProfessor(professorDto, null) + val createdProfessorDto = professorService.createProfessor(professorCreateReq, null) Then("교수가 생성되어야 한다") { professorRepository.count() shouldBe 1 @@ -89,20 +89,20 @@ class ProfessorServiceTest( Then("교수의 정보가 일치해야 한다") { val professorEntity = professorRepository.findByIdOrNull(createdProfessorDto.id)!! - professorEntity.name shouldBe professorDto.name - professorEntity.email shouldBe professorDto.email - professorEntity.status shouldBe professorDto.status - professorEntity.academicRank shouldBe professorDto.academicRank + professorEntity.name shouldBe professorCreateReq.name + professorEntity.email shouldBe professorCreateReq.email + professorEntity.status shouldBe professorCreateReq.status + professorEntity.academicRank shouldBe professorCreateReq.academicRank professorEntity.lab shouldBe labEntity - professorEntity.startDate shouldBe professorDto.startDate - professorEntity.endDate shouldBe professorDto.endDate - professorEntity.office shouldBe professorDto.office - professorEntity.phone shouldBe professorDto.phone - professorEntity.fax shouldBe professorDto.fax - professorEntity.website shouldBe professorDto.website - professorEntity.educations.map { it.name } shouldBe professorDto.educations - professorEntity.researchAreas.map { it.name } shouldBe professorDto.researchAreas - professorEntity.careers.map { it.name } shouldBe professorDto.careers + professorEntity.startDate shouldBe professorCreateReq.startDate + professorEntity.endDate shouldBe professorCreateReq.endDate + professorEntity.office shouldBe professorCreateReq.office + professorEntity.phone shouldBe professorCreateReq.phone + professorEntity.fax shouldBe professorCreateReq.fax + professorEntity.website shouldBe professorCreateReq.website + professorEntity.educations.map { it.name } shouldBe professorCreateReq.educations + professorEntity.researchAreas.map { it.name } shouldBe professorCreateReq.researchAreas + professorEntity.careers.map { it.name } shouldBe professorCreateReq.careers } Then("교수의 검색 정보가 생성되어야 한다") { @@ -173,14 +173,13 @@ class ProfessorServiceTest( researchRepository.save(researchEntity) val createdProfessorDto = professorService.createProfessor( - ProfessorDto( + CreateProfessorReqBody( language = "ko", name = "name", email = "email", status = ProfessorStatus.ACTIVE, academicRank = "academicRank", labId = labEntity1.id, - labName = null, startDate = date, endDate = date, office = "office", @@ -195,7 +194,8 @@ class ProfessorServiceTest( ) When("교수 정보를 수정하면") { - val toModifyProfessorDto = createdProfessorDto.copy( + val modifyProfessorReq = ModifyProfessorReqBody( + language = "ko", name = "modifiedName", email = "modifiedEmail", status = ProfessorStatus.INACTIVE, @@ -209,12 +209,13 @@ class ProfessorServiceTest( website = "modifiedWebsite", educations = listOf("education1", "modifiedEducation2", "modifiedEducation3"), researchAreas = listOf("researchArea1", "modifiedResearchArea2", "modifiedResearchArea3"), - careers = listOf("career1", "modifiedCareer2", "modifiedCareer3") + careers = listOf("career1", "modifiedCareer2", "modifiedCareer3"), + removeImage = false ) val modifiedProfessorDto = professorService.updateProfessor( - toModifyProfessorDto.id!!, - toModifyProfessorDto, + createdProfessorDto.id, + modifyProfessorReq, null ) @@ -223,20 +224,20 @@ class ProfessorServiceTest( val professorEntity = professorRepository.findByIdOrNull(modifiedProfessorDto.id) professorEntity shouldNotBe null - professorEntity!!.name shouldBe toModifyProfessorDto.name - professorEntity.email shouldBe toModifyProfessorDto.email - professorEntity.status shouldBe toModifyProfessorDto.status - professorEntity.academicRank shouldBe toModifyProfessorDto.academicRank + professorEntity!!.name shouldBe modifyProfessorReq.name + professorEntity.email shouldBe modifyProfessorReq.email + professorEntity.status shouldBe modifyProfessorReq.status + professorEntity.academicRank shouldBe modifyProfessorReq.academicRank professorEntity.lab shouldBe labEntity2 - professorEntity.startDate shouldBe toModifyProfessorDto.startDate - professorEntity.endDate shouldBe toModifyProfessorDto.endDate - professorEntity.office shouldBe toModifyProfessorDto.office - professorEntity.phone shouldBe toModifyProfessorDto.phone - professorEntity.fax shouldBe toModifyProfessorDto.fax - professorEntity.website shouldBe toModifyProfessorDto.website - professorEntity.educations.map { it.name } shouldBe toModifyProfessorDto.educations - professorEntity.researchAreas.map { it.name } shouldBe toModifyProfessorDto.researchAreas - professorEntity.careers.map { it.name } shouldBe toModifyProfessorDto.careers + professorEntity.startDate shouldBe modifyProfessorReq.startDate + professorEntity.endDate shouldBe modifyProfessorReq.endDate + professorEntity.office shouldBe modifyProfessorReq.office + professorEntity.phone shouldBe modifyProfessorReq.phone + professorEntity.fax shouldBe modifyProfessorReq.fax + professorEntity.website shouldBe modifyProfessorReq.website + professorEntity.educations.map { it.name } shouldBe modifyProfessorReq.educations + professorEntity.researchAreas.map { it.name } shouldBe modifyProfessorReq.researchAreas + professorEntity.careers.map { it.name } shouldBe modifyProfessorReq.careers } Then("검색 정보가 수정되어야 한다.") { diff --git a/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt b/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt index 9ec09da7..52fd4795 100644 --- a/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt +++ b/src/test/kotlin/com/wafflestudio/csereal/core/member/service/StaffServiceTest.kt @@ -1,9 +1,10 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.enums.LanguageType +import com.wafflestudio.csereal.core.member.api.req.CreateStaffReqBody +import com.wafflestudio.csereal.core.member.api.req.ModifyStaffReqBody import com.wafflestudio.csereal.core.member.database.MemberSearchRepository import com.wafflestudio.csereal.core.member.database.StaffRepository -import com.wafflestudio.csereal.core.member.dto.StaffDto import io.kotest.core.spec.style.BehaviorSpec import io.kotest.extensions.spring.SpringTestExtension import io.kotest.extensions.spring.SpringTestLifecycleMode @@ -29,7 +30,7 @@ class StaffServiceTest( } Given("이미지 없는 행정직원을 생성하려고 할 떄") { - val staffDto = StaffDto( + val createStaffReq = CreateStaffReqBody( language = "ko", name = "name", role = "role", @@ -40,7 +41,7 @@ class StaffServiceTest( ) When("행정직원을 생성하면") { - val createdStaffDto = staffService.createStaff(staffDto, null) + val createdStaffDto = staffService.createStaff(createStaffReq, null) Then("행정직원이 생성된다") { staffRepository.count() shouldBe 1 @@ -48,13 +49,13 @@ class StaffServiceTest( } Then("행정직원의 정보가 일치한다") { - val staffEntity = staffRepository.findByIdOrNull(createdStaffDto.id!!)!! - staffEntity.name shouldBe staffDto.name - staffEntity.role shouldBe staffDto.role - staffEntity.office shouldBe staffDto.office - staffEntity.phone shouldBe staffDto.phone - staffEntity.email shouldBe staffDto.email - staffEntity.tasks.map { it.name } shouldBe staffDto.tasks + val staffEntity = staffRepository.findByIdOrNull(createdStaffDto.id)!! + staffEntity.name shouldBe createStaffReq.name + staffEntity.role shouldBe createStaffReq.role + staffEntity.office shouldBe createStaffReq.office + staffEntity.phone shouldBe createStaffReq.phone + staffEntity.email shouldBe createStaffReq.email + staffEntity.tasks.map { it.name } shouldBe createStaffReq.tasks } Then("검색 정보가 생성된다") { @@ -79,7 +80,7 @@ class StaffServiceTest( } Given("이미지 없는 행정직원을 수정할 때") { - val staffDto = StaffDto( + val createStaffReq = CreateStaffReqBody( language = "ko", name = "name", role = "role", @@ -88,37 +89,37 @@ class StaffServiceTest( email = "email", tasks = listOf("task1", "task2") ) - - val createdStaffDto = staffService.createStaff(staffDto, null) + val createdStaffDto = staffService.createStaff(createStaffReq, null) When("행정직원을 수정하면") { - val updateStaffDto = StaffDto( + val modifyStaffReq = ModifyStaffReqBody( language = "ko", name = "name2", role = "role2", office = "office2", phone = "phone2", email = "email2", - tasks = listOf("task1", "task3", "task4") + tasks = listOf("task1", "task3", "task4"), + removeImage = false ) - val updatedStaffDto = staffService.updateStaff(createdStaffDto.id!!, updateStaffDto, null) + val updatedStaffDto = staffService.updateStaff(createdStaffDto.id, modifyStaffReq, null) Then("행정직원의 정보가 일치한다") { staffRepository.count() shouldBe 1 - val staffEntity = staffRepository.findByIdOrNull(updatedStaffDto.id!!)!! - staffEntity.name shouldBe updateStaffDto.name - staffEntity.role shouldBe updateStaffDto.role - staffEntity.office shouldBe updateStaffDto.office - staffEntity.phone shouldBe updateStaffDto.phone - staffEntity.email shouldBe updateStaffDto.email - staffEntity.tasks.map { it.name } shouldBe updateStaffDto.tasks + val staffEntity = staffRepository.findByIdOrNull(updatedStaffDto.id)!! + staffEntity.name shouldBe modifyStaffReq.name + staffEntity.role shouldBe modifyStaffReq.role + staffEntity.office shouldBe modifyStaffReq.office + staffEntity.phone shouldBe modifyStaffReq.phone + staffEntity.email shouldBe modifyStaffReq.email + staffEntity.tasks.map { it.name } shouldBe modifyStaffReq.tasks } Then("검색 정보가 수정된다") { memberSearchRepository.count() shouldBe 1 - val staffEntity = staffRepository.findByIdOrNull(updatedStaffDto.id!!)!! + val staffEntity = staffRepository.findByIdOrNull(updatedStaffDto.id)!! val memberSearch = staffEntity.memberSearch!! memberSearch.content shouldBe """ 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 index 83460145..3da6fb84 100644 --- a/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchSearchServiceTest.kt +++ b/src/test/kotlin/com/wafflestudio/csereal/core/reseach/service/ResearchSearchServiceTest.kt @@ -1,9 +1,10 @@ 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.dto.ProfessorDto import com.wafflestudio.csereal.core.member.service.ProfessorService import com.wafflestudio.csereal.core.research.database.* import com.wafflestudio.csereal.core.research.dto.LabDto @@ -47,14 +48,13 @@ class ResearchSearchServiceTest( Given("기존 lab이 존재할 때") { // Save professors val professor1Dto = professorService.createProfessor( - createProfessorRequest = ProfessorDto( + CreateProfessorReqBody( language = "ko", name = "professor1", email = null, status = ProfessorStatus.ACTIVE, academicRank = "professor", labId = null, - labName = null, startDate = null, endDate = null, office = null, @@ -68,14 +68,13 @@ class ResearchSearchServiceTest( mainImage = null ) val professor2Dto = professorService.createProfessor( - createProfessorRequest = ProfessorDto( + CreateProfessorReqBody( language = "ko", name = "professor2", email = null, status = ProfessorStatus.ACTIVE, academicRank = "professor", labId = null, - labName = null, startDate = null, endDate = null, office = null, @@ -166,14 +165,13 @@ class ResearchSearchServiceTest( When("professor가 추가된다면") { val process3CreatedDto = professorService.createProfessor( - createProfessorRequest = ProfessorDto( + CreateProfessorReqBody( language = "ko", name = "newProfessor", email = "email", status = ProfessorStatus.ACTIVE, academicRank = "academicRank", labId = createdLabDto.id, - labName = null, startDate = LocalDate.now(), endDate = LocalDate.now(), office = "office", @@ -212,8 +210,26 @@ class ResearchSearchServiceTest( When("professor가 수정된다면") { professorService.updateProfessor( professor2.id, - ProfessorDto.of(professor2, null) - .copy(name = "updateProfessor", labId = createdEmptyLabDto.id), + professor2.run { + ModifyProfessorReqBody( + language = "ko", + 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 )