From c9e039a2967741e2a7cb7c0707620d1bad57978f Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Tue, 25 Jul 2023 21:17:12 +0900 Subject: [PATCH 01/27] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=9D=EC=84=B1,=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=9D=BD=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: 패키지 및 엔티티 생성 * :sparkles: BaseTimeEntity 생성, PostEntity 기본 내용 작성 * :sparkles: PostController 생성 및 기본 내용 작성 * :sparkles: postService 생성 * :sparkles: Exceptions.kt 생성 * :sparkles: postDto 생성 * :sparkles: postRepository 생성 및 기본 내용 작성 * :green_heart: application.yaml 로컬 환경에서 작동하도록 설정 * feat: createPost 기능 생성 * refactor: 리뷰 주신 거 수정 * refactor: post -> notice 수정 등 --- .gitignore | 3 ++ .idea/.gitignore | 8 ++++ .idea/.name | 1 + .idea/compiler.xml | 6 +++ .idea/jarRepositories.xml | 20 +++++++++ .idea/kotlinc.xml | 6 +++ .idea/misc.xml | 8 ++++ .idea/vcs.xml | 6 +++ build.gradle.kts | 11 +++++ .../csereal/CserealApplication.kt | 2 + .../wafflestudio/csereal/common/Exceptions.kt | 7 ++++ .../csereal/common/config/BaseTimeEntity.kt | 23 ++++++++++ .../csereal/common/config/SecurityConfig.kt | 22 ++++++++-- .../common/controller/CommonController.kt | 2 +- .../core/notice/api/NoticeController.kt | 26 ++++++++++++ .../core/notice/database/NoticeEntity.kt | 17 ++++++++ .../core/notice/database/NoticeRepository.kt | 6 +++ .../core/notice/dto/CreateNoticeRequest.kt | 7 ++++ .../csereal/core/notice/dto/NoticeDto.kt | 32 ++++++++++++++ .../core/notice/service/NoticeService.kt | 42 +++++++++++++++++++ 20 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/BaseTimeEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt diff --git a/.gitignore b/.gitignore index 7186db51..37848be6 100644 --- a/.gitignore +++ b/.gitignore @@ -277,6 +277,9 @@ gradle-app.setting ######################## Custom +### IntelliJ IDEA ### +.idea + # local db save files db/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..7d15ed6b --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +csereal \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..b589d56e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..fdc392fe --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..4251b727 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..de0c4286 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f36c805c..b81e8ec6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,17 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") } +noArg { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} tasks.withType { kotlinOptions { diff --git a/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt b/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt index c1b5837f..648cca3c 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/CserealApplication.kt @@ -2,7 +2,9 @@ package com.wafflestudio.csereal import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +@EnableJpaAuditing @SpringBootApplication class CserealApplication diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt new file mode 100644 index 00000000..3ef42a8a --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.common + +import org.springframework.http.HttpStatus + +open class CserealException(msg: String, val status: HttpStatus) : RuntimeException(msg) { + class Csereal400(msg: String) : CserealException(msg, HttpStatus.BAD_REQUEST) +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/BaseTimeEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/BaseTimeEntity.kt new file mode 100644 index 00000000..ecd53933 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/BaseTimeEntity.kt @@ -0,0 +1,23 @@ +package com.wafflestudio.csereal.common.config + +import jakarta.persistence.* +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.annotation.LastModifiedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import java.time.LocalDateTime + +@MappedSuperclass +@EntityListeners(AuditingEntityListener::class) +abstract class BaseTimeEntity { + @CreatedDate + @Column(columnDefinition = "datetime(6) default '1999-01-01'") + var createdAt: LocalDateTime? = null + + @LastModifiedDate + @Column(columnDefinition = "datetime(6) default '1999-01-01'") + var modifiedAt: LocalDateTime? = null + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0L +} \ No newline at end of file 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 3d1ad4c2..5ec132d7 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt @@ -3,14 +3,28 @@ package com.wafflestudio.csereal.common.config import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain @Configuration class SpringSecurityConfig { + + // 확인 바람 @Bean - fun filterChain(http: HttpSecurity): SecurityFilterChain { - http.httpBasic().disable() - return http.build() - } + fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = + httpSecurity + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .anyRequest().permitAll() + .and() + .build() + +// @Bean +// fun filterChain(http: HttpSecurity): SecurityFilterChain { +// http.httpBasic().disable() +// return http.build() +// } } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/controller/CommonController.kt b/src/main/kotlin/com/wafflestudio/csereal/common/controller/CommonController.kt index 754c0a41..e21bbe7a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/controller/CommonController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/controller/CommonController.kt @@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.RestController @RestController class CommonController { - @GetMapping("/hello_world") + @GetMapping("/helloworld") fun helloWorld(): String { return "Hello, world!" } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt new file mode 100644 index 00000000..91a9231d --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt @@ -0,0 +1,26 @@ +package com.wafflestudio.csereal.core.notice.api + +import com.wafflestudio.csereal.core.notice.dto.CreateNoticeRequest +import com.wafflestudio.csereal.core.notice.dto.NoticeDto +import com.wafflestudio.csereal.core.notice.service.NoticeService +import org.springframework.web.bind.annotation.* + +@RequestMapping("/notice") +@RestController +class NoticeController( + private val noticeService: NoticeService, +) { + @GetMapping("/{noticeId}") + fun readNotice( + @PathVariable noticeId: Long, + ) : NoticeDto { + return noticeService.readNotice(noticeId) + } + + @PostMapping + fun createNotice( + @RequestBody request: CreateNoticeRequest + ) : NoticeDto { + return noticeService.createNotice(request) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt new file mode 100644 index 00000000..5b55d6a6 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt @@ -0,0 +1,17 @@ +package com.wafflestudio.csereal.core.notice.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity + + +@Entity(name = "notice") +class NoticeEntity( + @Column + var title: String, + + @Column(columnDefinition = "text") + var description: String +): BaseTimeEntity() { + +} \ No newline at end of file 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 new file mode 100644 index 00000000..0ec7f774 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeRepository.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.notice.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface NoticeRepository : JpaRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt new file mode 100644 index 00000000..be251915 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.notice.dto + +data class CreateNoticeRequest( + val title: String, + val description: String +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt new file mode 100644 index 00000000..65e1120c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt @@ -0,0 +1,32 @@ +package com.wafflestudio.csereal.core.notice.dto + +import com.wafflestudio.csereal.core.notice.database.NoticeEntity +import java.time.LocalDateTime + +data class NoticeDto( + val id: Long, + val title: String, + val description: String, + // val postType: String, + // val authorId: Int, + val createdAt: LocalDateTime?, + val modifiedAt: LocalDateTime?, + // val isPublic: Boolean, + // val isSlide: Boolean, + // val isPinned: Boolean, +) { + + companion object { + fun of(entity: NoticeEntity): NoticeDto = entity.run { + NoticeDto( + id = this.id, + title = this.title, + description = this.description, + createdAt = this.createdAt, + modifiedAt = this.modifiedAt + ) + } + + } + +} \ No newline at end of file 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 new file mode 100644 index 00000000..ca321282 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/service/NoticeService.kt @@ -0,0 +1,42 @@ +package com.wafflestudio.csereal.core.notice.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.notice.database.NoticeEntity +import com.wafflestudio.csereal.core.notice.database.NoticeRepository +import com.wafflestudio.csereal.core.notice.dto.CreateNoticeRequest +import com.wafflestudio.csereal.core.notice.dto.NoticeDto +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface NoticeService { + fun readNotice(noticeId: Long): NoticeDto + fun createNotice(request: CreateNoticeRequest): NoticeDto +} + +@Service +class NoticeServiceImpl( + private val noticeRepository: NoticeRepository, +) : NoticeService { + + @Transactional(readOnly = true) + override fun readNotice(noticeId: Long): NoticeDto { + val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) + ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + return NoticeDto.of(notice) + } + + @Transactional + override fun createNotice(request: CreateNoticeRequest): NoticeDto { + // TODO:"아직 날짜가 제대로 안 뜸" + val newNotice = NoticeEntity( + title = request.title, + description = request.description, + ) + + noticeRepository.save(newNotice) + + return NoticeDto.of(newNotice) + + } +} \ No newline at end of file From f849a636364450e45b72c6e257fdd7c7117a74aa Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Wed, 26 Jul 2023 15:11:09 +0900 Subject: [PATCH 02/27] =?UTF-8?q?chore:=20.idea=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 -------- .idea/.name | 1 - .idea/compiler.xml | 6 ------ .idea/jarRepositories.xml | 20 -------------------- .idea/kotlinc.xml | 6 ------ .idea/misc.xml | 8 -------- .idea/vcs.xml | 6 ------ 7 files changed, 55 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 7d15ed6b..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -csereal \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56e..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index fdc392fe..00000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 4251b727..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index de0c4286..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 54ef587e099f2c425a889546412d26a22ac9fd1e Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Sun, 23 Jul 2023 17:44:19 +0900 Subject: [PATCH 03/27] =?UTF-8?q?chore:=20PR=20=ED=85=9C=ED=94=8C=EB=A6=BF?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/pull_request_template.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..4d85e3ed --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## 제목 + +### 한줄 요약 + +PR 요약해주세요 + +### 상세 설명 + +[`Commit Hash1`]: 커밋 설명1 + +[`Commit Hash2`]: 커밋 설명2 + +### TODO + +TODO가 있다면 작성해주시고, 없다면 생략해주세요. From 3a9290674d6897ffbead3c1d9407e770eb451112 Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Wed, 26 Jul 2023 15:35:35 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat:=20=EB=A1=9C=EC=BB=AC=20db=EC=9A=A9?= =?UTF-8?q?=20docker-compose=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20application.yaml=20=EC=88=98=EC=A0=95=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-local.yml | 17 +++++++++++++++++ src/main/resources/application.yaml | 14 ++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 docker-compose-local.yml diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 00000000..ad08265a --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,17 @@ +version: '3.8' +services: + db: + image: mysql:8.0 + cap_add: + - SYS_NICE + environment: + - MYSQL_DATABASE=csereal + - MYSQL_ROOT_PASSWORD=password + ports: + - '3306:3306' + volumes: + - db:/var/lib/mysql + - $PWD/db/init.sql:/docker-entrypoint-initdb.d/init.sql +volumes: + db: + driver: local diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 62f21be6..d3c5150f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,14 +1,19 @@ spring: - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver + profiles: + active: local --- spring: config.activate.on-profile: local jpa: hibernate: - ddl-auto: create + ddl-auto: update show-sql: true + open-in-view: false + datasource: + url: jdbc:mysql://localhost:3306/csereal + username: root + password: password logging.level: default: INFO @@ -17,4 +22,5 @@ spring: config.activate.on-profile: prod jpa: hibernate: - ddl-auto: none \ No newline at end of file + ddl-auto: none + open-in-view: false From c2f7ef8b85343625a91fb24dedc4a6c924cac000 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Fri, 28 Jul 2023 20:26:16 +0900 Subject: [PATCH 05/27] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C,=20?= =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: ExceptionHandler 추가 * feat: updateNotice 추가, valid 추가 * feat: deleteNotice 추가 * feat: enrollTag 기능 추가, noticeTag 연관 엔티티 추가 * feat: 공지사항 작성할 때 태그 생성 및 수정 * fix: 로컬 db 없앰 * fix: pr 리뷰 수정 * fix: pr 리뷰 수정 * fix: noticeTag assign --- build.gradle.kts | 1 + .../common/config/CserealExceptionHandler.kt | 33 +++++++++++ .../core/notice/api/NoticeController.kt | 29 +++++++++- .../core/notice/database/NoticeEntity.kt | 21 ++++++- .../core/notice/database/NoticeTagEntity.kt | 29 ++++++++++ .../notice/database/NoticeTagRepository.kt | 7 +++ .../csereal/core/notice/database/TagEntity.kt | 15 +++++ .../core/notice/database/TagRepository.kt | 6 ++ .../core/notice/dto/CreateNoticeRequest.kt | 9 ++- .../csereal/core/notice/dto/NoticeDto.kt | 8 ++- .../core/notice/dto/UpdateNoticeRequest.kt | 8 +++ .../core/notice/service/NoticeService.kt | 57 ++++++++++++++++++- src/main/resources/application.yaml | 5 ++ 13 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index b81e8ec6..7d5145d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-client") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt new file mode 100644 index 00000000..6cf3d136 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt @@ -0,0 +1,33 @@ +package com.wafflestudio.csereal.common.config + +import com.wafflestudio.csereal.common.CserealException +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.validation.BindingResult +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import java.sql.SQLIntegrityConstraintViolationException + +@RestControllerAdvice +class CserealExceptionHandler { + + // @Valid로 인해 오류 떴을 때 메시지 전송 + @ExceptionHandler(value = [MethodArgumentNotValidException::class]) + fun handle(e: MethodArgumentNotValidException): ResponseEntity { + val bindingResult: BindingResult = e.bindingResult + return ResponseEntity(bindingResult.fieldError?.defaultMessage, HttpStatus.BAD_REQUEST) + } + + // csereal 내부 규정 오류 + @ExceptionHandler(value = [CserealException::class]) + fun handle(e: CserealException): ResponseEntity { + return ResponseEntity(e.message, e.status) + } + + // db에서 중복된 값 있을 때 + @ExceptionHandler(value = [SQLIntegrityConstraintViolationException::class]) + fun handle(e: SQLIntegrityConstraintViolationException): ResponseEntity { + return ResponseEntity("중복된 값이 있습니다.", HttpStatus.CONFLICT) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt index 91a9231d..21131bff 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt @@ -2,7 +2,11 @@ package com.wafflestudio.csereal.core.notice.api import com.wafflestudio.csereal.core.notice.dto.CreateNoticeRequest import com.wafflestudio.csereal.core.notice.dto.NoticeDto +import com.wafflestudio.csereal.core.notice.dto.UpdateNoticeRequest import com.wafflestudio.csereal.core.notice.service.NoticeService +import jakarta.validation.Valid +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @RequestMapping("/notice") @@ -19,8 +23,31 @@ class NoticeController( @PostMapping fun createNotice( - @RequestBody request: CreateNoticeRequest + @Valid @RequestBody request: CreateNoticeRequest ) : NoticeDto { return noticeService.createNotice(request) } + + @PatchMapping("/{noticeId}") + fun updateNotice( + @PathVariable noticeId: Long, + @Valid @RequestBody request: UpdateNoticeRequest, + ) : NoticeDto { + return noticeService.updateNotice(noticeId, request) + } + + @DeleteMapping("/{noticeId}") + fun deleteNotice( + @PathVariable noticeId: Long + ) { + noticeService.deleteNotice(noticeId) + } + + @PostMapping("/tag") + fun enrollTag( + @RequestBody tagName: Map + ) : ResponseEntity { + noticeService.enrollTag(tagName["name"]!!) + return ResponseEntity("등록되었습니다. (tagName: ${tagName["name"]})", HttpStatus.OK) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt index 5b55d6a6..9fdc013a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt @@ -1,17 +1,36 @@ package com.wafflestudio.csereal.core.notice.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.CascadeType import jakarta.persistence.Column import jakarta.persistence.Entity +import jakarta.persistence.OneToMany @Entity(name = "notice") class NoticeEntity( + + @Column + var isDeleted: Boolean = false, + @Column var title: String, @Column(columnDefinition = "text") - var description: String + var description: String, + +// var postType: String, +// +// var isPublic: Boolean, +// +// var isSlide: Boolean, +// +// var isPinned: Boolean, + + @OneToMany(mappedBy = "notice", cascade = [CascadeType.ALL]) + var noticeTags: MutableSet = mutableSetOf() ): BaseTimeEntity() { + + } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt new file mode 100644 index 00000000..c910dabd --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt @@ -0,0 +1,29 @@ +package com.wafflestudio.csereal.core.notice.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.* + +@Entity(name = "noticeTag") +class NoticeTagEntity( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "notice_id") + var notice: NoticeEntity, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + var tag: TagEntity, + + ) : BaseTimeEntity() { + companion object { + + fun createNoticeTag(notice: NoticeEntity, tag: TagEntity) { + val noticeTag = NoticeTagEntity(notice, tag) + notice.noticeTags.add(noticeTag) + tag.noticeTags.add(noticeTag) + } + } + + + +} + diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt new file mode 100644 index 00000000..39a6ce98 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.notice.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface NoticeTagRepository : JpaRepository { + fun deleteAllByNoticeId(noticeId: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt new file mode 100644 index 00000000..67c108b9 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.notice.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.OneToMany + +@Entity(name = "tag") +class TagEntity( + var name: String, + + @OneToMany(mappedBy = "tag") + val noticeTags: MutableSet = mutableSetOf() +) : BaseTimeEntity() { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt new file mode 100644 index 00000000..34a50416 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.notice.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface TagRepository : JpaRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt index be251915..2c660447 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt @@ -1,7 +1,14 @@ package com.wafflestudio.csereal.core.notice.dto +import jakarta.validation.constraints.NotBlank + data class CreateNoticeRequest( + @field:NotBlank(message = "제목은 비어있을 수 없습니다") val title: String, - val description: String + + @field:NotBlank(message = "내용은 비어있을 수 없습니다") + val description: String, + + val tags: List = emptyList() ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt index 65e1120c..a858c1dc 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt @@ -5,6 +5,7 @@ import java.time.LocalDateTime data class NoticeDto( val id: Long, + val isDeleted: Boolean, val title: String, val description: String, // val postType: String, @@ -20,10 +21,15 @@ data class NoticeDto( fun of(entity: NoticeEntity): NoticeDto = entity.run { NoticeDto( id = this.id, + isDeleted = false, title = this.title, description = this.description, + // postType = this.postType, createdAt = this.createdAt, - modifiedAt = this.modifiedAt + modifiedAt = this.modifiedAt, +// isPublic = this.isPublic, +// isSlide = this.isSlide, +// isPinned = this.isPinned, ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt new file mode 100644 index 00000000..ea45d723 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.notice.dto + +data class UpdateNoticeRequest( + val title: String?, + val description: String?, + val tags: List? +) { +} \ No newline at end of file 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 ca321282..168bed68 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 @@ -1,10 +1,10 @@ package com.wafflestudio.csereal.core.notice.service import com.wafflestudio.csereal.common.CserealException -import com.wafflestudio.csereal.core.notice.database.NoticeEntity -import com.wafflestudio.csereal.core.notice.database.NoticeRepository +import com.wafflestudio.csereal.core.notice.database.* import com.wafflestudio.csereal.core.notice.dto.CreateNoticeRequest import com.wafflestudio.csereal.core.notice.dto.NoticeDto +import com.wafflestudio.csereal.core.notice.dto.UpdateNoticeRequest import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -12,31 +12,82 @@ import org.springframework.transaction.annotation.Transactional interface NoticeService { fun readNotice(noticeId: Long): NoticeDto fun createNotice(request: CreateNoticeRequest): NoticeDto + fun updateNotice(noticeId: Long, request: UpdateNoticeRequest): NoticeDto + fun deleteNotice(noticeId: Long) + fun enrollTag(tagName: String) } @Service class NoticeServiceImpl( private val noticeRepository: NoticeRepository, + private val tagRepository: TagRepository, + private val noticeTagRepository: NoticeTagRepository ) : NoticeService { @Transactional(readOnly = true) override fun readNotice(noticeId: Long): NoticeDto { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + if (notice.isDeleted) throw CserealException.Csereal400("삭제된 공지사항입니다.(noticeId: $noticeId)") return NoticeDto.of(notice) } @Transactional override fun createNotice(request: CreateNoticeRequest): NoticeDto { - // TODO:"아직 날짜가 제대로 안 뜸" val newNotice = NoticeEntity( title = request.title, description = request.description, ) + for (tagId in request.tags) { + val tag = tagRepository.findByIdOrNull(tagId) ?: throw CserealException.Csereal400("해당하는 태그가 없습니다") + NoticeTagEntity.createNoticeTag(newNotice, tag) + } + noticeRepository.save(newNotice) return NoticeDto.of(newNotice) } + + @Transactional + override fun updateNotice(noticeId: Long, request: UpdateNoticeRequest): NoticeDto { + val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) + ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId") + if (notice.isDeleted) throw CserealException.Csereal400("삭제된 공지사항입니다.(noticeId: $noticeId") + + notice.title = request.title ?: notice.title + notice.description = request.description ?: notice.description + + if (request.tags != null) { + noticeTagRepository.deleteAllByNoticeId(noticeId) + notice.noticeTags.clear() + for (tagId in request.tags) { + val tag = tagRepository.findByIdOrNull(tagId) ?: throw CserealException.Csereal400("해당하는 태그가 없습니다") + NoticeTagEntity.createNoticeTag(notice, tag) + } + } + + + + return NoticeDto.of(notice) + } + + @Transactional + override fun deleteNotice(noticeId: Long) { + val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) + ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + + notice.isDeleted = true + + } + + override fun enrollTag(tagName: String) { + val newTag = TagEntity( + name = tagName + ) + tagRepository.save(newTag) + } + + //TODO: 이미지 등록, 페이지네이션, 검색 } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d3c5150f..35b3da2a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -5,6 +5,11 @@ spring: --- spring: config.activate.on-profile: local + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/csereal_local?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul + username: root + password: toor jpa: hibernate: ddl-auto: update From 227a3854a8bbf2f67349cfd69c73723d8c2faac2 Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Mon, 31 Jul 2023 20:32:27 +0900 Subject: [PATCH 06/27] =?UTF-8?q?feat:=20=EA=B5=AC=EC=84=B1=EC=9B=90(?= =?UTF-8?q?=EA=B5=90=EC=88=98)=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 교수 엔티티 및 DTO 설계 * feat: 교수 생성 및 조회 --- .../wafflestudio/csereal/common/Exceptions.kt | 3 +- .../core/member/api/ProfessorController.kt | 35 ++++++++ .../core/member/database/CareerEntity.kt | 32 ++++++++ .../core/member/database/EducationEntity.kt | 38 +++++++++ .../core/member/database/ProfessorEntity.kt | 65 +++++++++++++++ .../member/database/ProfessorRepository.kt | 8 ++ .../member/database/ResearchAreaEntity.kt | 27 +++++++ .../csereal/core/member/dto/CareerDto.kt | 19 +++++ .../csereal/core/member/dto/EducationDto.kt | 22 ++++++ .../csereal/core/member/dto/ProfessorDto.kt | 46 +++++++++++ .../core/member/dto/SimpleProfessorDto.kt | 28 +++++++ .../core/member/service/ProfessorService.kt | 79 +++++++++++++++++++ .../core/research/database/LabEntity.kt | 15 ++++ .../core/research/database/LabRepository.kt | 6 ++ 14 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/ResearchAreaEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt index 3ef42a8a..cb0d7b08 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt @@ -4,4 +4,5 @@ import org.springframework.http.HttpStatus open class CserealException(msg: String, val status: HttpStatus) : RuntimeException(msg) { class Csereal400(msg: String) : CserealException(msg, HttpStatus.BAD_REQUEST) -} \ No newline at end of file + class Csereal404(msg: String) : CserealException(msg, HttpStatus.NOT_FOUND) +} 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 new file mode 100644 index 00000000..437c2544 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/ProfessorController.kt @@ -0,0 +1,35 @@ +package com.wafflestudio.csereal.core.member.api + +import com.wafflestudio.csereal.core.member.dto.ProfessorDto +import com.wafflestudio.csereal.core.member.dto.SimpleProfessorDto +import com.wafflestudio.csereal.core.member.service.ProfessorService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RequestMapping("/professor") +@RestController +class ProfessorController( + private val professorService: ProfessorService +) { + + @PostMapping + fun createProfessor(@RequestBody createProfessorRequest: ProfessorDto): ResponseEntity { + return ResponseEntity.ok(professorService.createProfessor(createProfessorRequest)) + } + + @GetMapping("/{professorId}") + fun getProfessor(@PathVariable professorId: Long): ResponseEntity { + return ResponseEntity.ok(professorService.getProfessor(professorId)) + } + + @GetMapping("/active") + fun getActiveProfessors(): ResponseEntity> { + return ResponseEntity.ok(professorService.getActiveProfessors()) + } + + @GetMapping("/inactive") + fun getInactiveProfessors(): ResponseEntity> { + return ResponseEntity.ok(professorService.getInactiveProfessors()) + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt new file mode 100644 index 00000000..30df5357 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt @@ -0,0 +1,32 @@ +package com.wafflestudio.csereal.core.member.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.member.dto.CareerDto +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +@Entity(name = "career") +class CareerEntity( + val duration: String, + val name: String, + val workplace: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "professor_id") + val professor: ProfessorEntity +) : BaseTimeEntity() { + companion object { + fun create(career: CareerDto, professor: ProfessorEntity): CareerEntity { + val careerEntity = CareerEntity( + duration = career.duration, + name = career.name, + workplace = career.workplace, + professor = professor + ) + professor.careers.add(careerEntity) + return careerEntity + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt new file mode 100644 index 00000000..7bdae723 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt @@ -0,0 +1,38 @@ +package com.wafflestudio.csereal.core.member.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.member.dto.EducationDto +import jakarta.persistence.* + +@Entity(name = "education") +class EducationEntity( + val university: String, + val major: String, + + @Enumerated(EnumType.STRING) + val degree: Degree, + + val year: Int, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "professor_id") + val professor: ProfessorEntity +) : BaseTimeEntity() { + companion object { + fun create(education: EducationDto, professor: ProfessorEntity): EducationEntity { + val educationEntity = EducationEntity( + university = education.university, + major = education.major, + degree = education.degree, + year = education.year, + professor = professor + ) + professor.educations.add(educationEntity) + return educationEntity + } + } +} + +enum class Degree { + Bachelor, Master, PhD +} 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 new file mode 100644 index 00000000..09efde16 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorEntity.kt @@ -0,0 +1,65 @@ +package com.wafflestudio.csereal.core.member.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.member.dto.ProfessorDto +import com.wafflestudio.csereal.core.research.database.LabEntity +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany +import java.time.LocalDate + +@Entity(name = "professor") +class ProfessorEntity( + val name: String, + //val profileImage:File + var isActive: Boolean, + var academicRank: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lab_id") + var lab: LabEntity? = null, + + var startDate: LocalDate?, + var endDate: LocalDate?, + val office: String?, + var phone: String?, + var fax: String?, + var email: String?, + var website: String?, + + @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) + val educations: MutableList = mutableListOf(), + + @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) + val researchAreas: MutableSet = mutableSetOf(), + + @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) + val careers: MutableList = mutableListOf(), + + ) : BaseTimeEntity() { + + companion object { + fun of(professorDto: ProfessorDto): ProfessorEntity { + return ProfessorEntity( + name = professorDto.name, + isActive = professorDto.isActive, + academicRank = professorDto.academicRank, + startDate = professorDto.startDate, + endDate = professorDto.endDate, + office = professorDto.office, + phone = professorDto.phone, + fax = professorDto.fax, + email = professorDto.email, + website = professorDto.website + ) + } + } + + fun addLab(lab: LabEntity) { + this.lab = lab + lab.professors.add(this) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt new file mode 100644 index 00000000..d8ae890c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.member.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface ProfessorRepository : JpaRepository { + fun findByIsActiveTrue(): List + fun findByIsActiveFalse(): List +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ResearchAreaEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ResearchAreaEntity.kt new file mode 100644 index 00000000..6f6c8ab4 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ResearchAreaEntity.kt @@ -0,0 +1,27 @@ +package com.wafflestudio.csereal.core.member.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +@Entity(name = "research_area") +class ResearchAreaEntity( + val name: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "professor_id") + val professor: ProfessorEntity +) : BaseTimeEntity() { + companion object { + fun create(name: String, professor: ProfessorEntity): ResearchAreaEntity { + val researchArea = ResearchAreaEntity( + name = name, + professor = professor + ) + professor.researchAreas.add(researchArea) + return researchArea + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt new file mode 100644 index 00000000..ff14cfb2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt @@ -0,0 +1,19 @@ +package com.wafflestudio.csereal.core.member.dto + +import com.wafflestudio.csereal.core.member.database.CareerEntity + +data class CareerDto( + val duration: String, + val name: String, + val workplace: String +) { + companion object { + fun of(careerEntity: CareerEntity): CareerDto { + return CareerDto( + duration = careerEntity.duration, + name = careerEntity.name, + workplace = careerEntity.workplace + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt new file mode 100644 index 00000000..48e2c968 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt @@ -0,0 +1,22 @@ +package com.wafflestudio.csereal.core.member.dto + +import com.wafflestudio.csereal.core.member.database.Degree +import com.wafflestudio.csereal.core.member.database.EducationEntity + +data class EducationDto( + val university: String, + val major: String, + val degree: Degree, + val year: Int +) { + companion object { + fun of(educationEntity: EducationEntity): EducationDto { + return EducationDto( + university = educationEntity.university, + major = educationEntity.major, + degree = educationEntity.degree, + year = educationEntity.year + ) + } + } +} 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 new file mode 100644 index 00000000..d4a52290 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorDto.kt @@ -0,0 +1,46 @@ +package com.wafflestudio.csereal.core.member.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.core.member.database.ProfessorEntity +import java.time.LocalDate + +data class ProfessorDto( + @JsonInclude(JsonInclude.Include.NON_NULL) + var id: Long? = null, + val name: String, + val isActive: Boolean, + 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 +) { + companion object { + fun of(professorEntity: ProfessorEntity): ProfessorDto { + return ProfessorDto( + id = professorEntity.id, + name = professorEntity.name, + isActive = professorEntity.isActive, + academicRank = professorEntity.academicRank, + labId = professorEntity.lab?.id, + startDate = professorEntity.startDate, + endDate = professorEntity.endDate, + office = professorEntity.office, + phone = professorEntity.phone, + fax = professorEntity.fax, + email = professorEntity.email, + website = professorEntity.website, + educations = professorEntity.educations.map { EducationDto.of(it) }, + researchAreas = professorEntity.researchAreas.map { it.name }, + careers = professorEntity.careers.map { CareerDto.of(it) } + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt new file mode 100644 index 00000000..6e070667 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt @@ -0,0 +1,28 @@ +package com.wafflestudio.csereal.core.member.dto + +import com.wafflestudio.csereal.core.member.database.ProfessorEntity + +data class SimpleProfessorDto( + val id: Long, + val name: String, + val academicRank: String, + val labId: Long?, + val labName: String?, + val phone: String?, + val email: String?, + // val imageUri: String +) { + companion object { + fun of(professorEntity: ProfessorEntity): SimpleProfessorDto { + return SimpleProfessorDto( + id = professorEntity.id, + name = professorEntity.name, + academicRank = professorEntity.academicRank, + labId = professorEntity.lab?.id, + labName = professorEntity.lab?.name, + phone = professorEntity.phone, + email = professorEntity.email + ) + } + } +} 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 new file mode 100644 index 00000000..d2f8b582 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt @@ -0,0 +1,79 @@ +package com.wafflestudio.csereal.core.member.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.member.database.* +import com.wafflestudio.csereal.core.member.dto.ProfessorDto +import com.wafflestudio.csereal.core.member.dto.SimpleProfessorDto +import com.wafflestudio.csereal.core.research.database.LabRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface ProfessorService { + fun createProfessor(professorDto: ProfessorDto): ProfessorDto + fun getProfessor(professorId: Long): ProfessorDto + fun getActiveProfessors(): List + fun getInactiveProfessors(): List + fun updateProfessor(updateProfessorRequest: ProfessorDto): ProfessorDto + fun deleteProfessor(professorId: Long) +} + +@Service +@Transactional +class ProfessorServiceImpl( + private val labRepository: LabRepository, + private val professorRepository: ProfessorRepository +) : ProfessorService { + + override fun createProfessor(professorDto: ProfessorDto): ProfessorDto { + val professor = ProfessorEntity.of(professorDto) + + if (professorDto.labId != null) { + val lab = labRepository.findByIdOrNull(professorDto.labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${professorDto.labId}") + professor.addLab(lab) + } + + for (education in professorDto.educations) { + EducationEntity.create(education, professor) + } + + for (researchArea in professorDto.researchAreas) { + ResearchAreaEntity.create(researchArea, professor) + } + + for (career in professorDto.careers) { + CareerEntity.create(career, professor) + } + + professorRepository.save(professor) + + return ProfessorDto.of(professor) + } + + @Transactional(readOnly = true) + override fun getProfessor(professorId: Long): ProfessorDto { + val professor = professorRepository.findByIdOrNull(professorId) + ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: ${professorId}") + return ProfessorDto.of(professor) + } + + @Transactional(readOnly = true) + override fun getActiveProfessors(): List { + return professorRepository.findByIsActiveTrue().map { SimpleProfessorDto.of(it) } + } + + @Transactional(readOnly = true) + override fun getInactiveProfessors(): List { + return professorRepository.findByIsActiveFalse().map { SimpleProfessorDto.of(it) } + } + + override fun updateProfessor(updateProfessorRequest: ProfessorDto): ProfessorDto { + TODO("Not yet implemented") + } + + override fun deleteProfessor(professorId: Long) { + TODO("Not yet implemented") + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt new file mode 100644 index 00000000..06b88e10 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.research.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.member.database.ProfessorEntity +import jakarta.persistence.Entity +import jakarta.persistence.OneToMany + +@Entity(name = "lab") +class LabEntity( + + val name: String, + + @OneToMany(mappedBy = "lab") + val professors: MutableSet = mutableSetOf() +) : BaseTimeEntity() diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt new file mode 100644 index 00000000..84bf190a --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.research.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface LabRepository : JpaRepository { +} From 92a8e8b5b3b991a78f82c43be376eded8cb228a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=B0=ED=98=81=EC=A4=80=20=28HyukJoon=20Woo=29?= Date: Tue, 1 Aug 2023 01:07:57 +0900 Subject: [PATCH 07/27] =?UTF-8?q?Docs:=20Swagger=20=EC=B6=94=EA=B0=80=20(#?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs: Add swagger dependency * Docs: Add basic config for swagger * Docs: Add basic configuration for swagger. --- build.gradle.kts | 1 + .../csereal/common/config/OpenApiConfig.kt | 21 +++++++++++++++++++ src/main/resources/application.yaml | 11 +++++----- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7d5145d6..a0b28802 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-validation") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0") runtimeOnly("com.mysql:mysql-connector-j") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt new file mode 100644 index 00000000..d78341ae --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/OpenApiConfig.kt @@ -0,0 +1,21 @@ +package com.wafflestudio.csereal.common.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class OpenApiConfig { + @Bean + fun openAPI(): OpenAPI { + val info = Info() + .title("컴퓨터공학부 홈페이지 백엔드 API") + .description("컴퓨터공학부 홈페이지 백엔드 API 명세서입니다.") + + return OpenAPI() + .components(Components()) + .info(info) + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 35b3da2a..6af687e6 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,14 +2,15 @@ spring: profiles: active: local +springdoc: + swagger-ui: + path: index.html + api-docs: + path: /api-docs/json + --- spring: config.activate.on-profile: local - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://127.0.0.1:3306/csereal_local?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul - username: root - password: toor jpa: hibernate: ddl-auto: update From 00566f2d4d36b67613856184629ac04514fd8671 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Tue, 1 Aug 2023 21:21:59 +0900 Subject: [PATCH 08/27] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98+=EA=B2=80=EC=83=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: isPublic, isSlide, isPinned 추가 * feat: queryDsl 적용 위해 gradle 추가 fix: javax -> jakarta 변경 * feat: queryDsl 도입 * feat: 키워드+태그 검색 추가 * feat: search query에 isDeleted, isPublic 추가 및 isPinned 우선순위 설정 * fix: requestBody -> requestParam 수정 * feat: 페이지네이션 추가 * fix: 키워드 booleanBuilder 추가, application.yaml 수정 * fix: searchNotice readOnly 추가 * fix: SearchRequest 삭제 * fix: NoticeDto tags 추가 * fix: pr 리뷰 수정 / feat: 검색 기능 보강 및 수정 * fix:코드 수정 * fix: SearchResponse isPinned 추가 * fix: SearchResponse에 total 추가 * fix: 페이지 개수 수정 * fix: searchNotice queryDsl 오류 수정 * fix: local 설정 변경 --- build.gradle.kts | 19 +++-- .../csereal/common/config/QueryDslConfig.kt | 17 ++++ .../core/notice/api/NoticeController.kt | 24 +++--- .../core/notice/database/NoticeEntity.kt | 12 +-- .../core/notice/database/NoticeRepository.kt | 77 ++++++++++++++++++- .../core/notice/dto/CreateNoticeRequest.kt | 8 +- .../csereal/core/notice/dto/NoticeDto.kt | 14 ++-- .../csereal/core/notice/dto/SearchDto.kt | 13 ++++ .../csereal/core/notice/dto/SearchResponse.kt | 8 ++ .../core/notice/dto/UpdateNoticeRequest.kt | 5 +- .../core/notice/service/NoticeService.kt | 38 +++++++-- src/main/resources/application.yaml | 10 ++- 12 files changed, 203 insertions(+), 42 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/QueryDslConfig.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt diff --git a/build.gradle.kts b/build.gradle.kts index a0b28802..4acfd1ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,6 +6,7 @@ plugins { kotlin("jvm") version "1.7.22" kotlin("plugin.spring") version "1.7.22" kotlin("plugin.jpa") version "1.7.22" + kotlin("kapt") version "1.7.10" } group = "com.wafflestudio" @@ -31,17 +32,23 @@ dependencies { runtimeOnly("com.mysql:mysql-connector-j") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") + + //queryDsl + implementation ("com.querydsl:querydsl-jpa:5.0.0:jakarta") + kapt("com.querydsl:querydsl-apt:5.0.0:jakarta") + kapt("jakarta.annotation:jakarta.annotation-api") + kapt("jakarta.persistence:jakarta.persistence-api") } noArg { - annotation("javax.persistence.Entity") - annotation("javax.persistence.MappedSuperclass") - annotation("javax.persistence.Embeddable") + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") } allOpen { - annotation("javax.persistence.Entity") - annotation("javax.persistence.MappedSuperclass") - annotation("javax.persistence.Embeddable") + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") } tasks.withType { diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/QueryDslConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/QueryDslConfig.kt new file mode 100644 index 00000000..ff32458d --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/QueryDslConfig.kt @@ -0,0 +1,17 @@ +package com.wafflestudio.csereal.common.config + +import com.querydsl.jpa.impl.JPAQueryFactory +import jakarta.persistence.EntityManager +import jakarta.persistence.PersistenceContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class QueryDslConfig( + @PersistenceContext + val entityManager: EntityManager, +) { + @Bean + fun jpaQueryFactory(): JPAQueryFactory = + JPAQueryFactory(entityManager) +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt index 21131bff..82e47c0d 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt @@ -1,8 +1,6 @@ package com.wafflestudio.csereal.core.notice.api -import com.wafflestudio.csereal.core.notice.dto.CreateNoticeRequest -import com.wafflestudio.csereal.core.notice.dto.NoticeDto -import com.wafflestudio.csereal.core.notice.dto.UpdateNoticeRequest +import com.wafflestudio.csereal.core.notice.dto.* import com.wafflestudio.csereal.core.notice.service.NoticeService import jakarta.validation.Valid import org.springframework.http.HttpStatus @@ -14,26 +12,34 @@ import org.springframework.web.bind.annotation.* class NoticeController( private val noticeService: NoticeService, ) { + @GetMapping + fun searchNotice( + @RequestParam(required = false) tag : List?, + @RequestParam(required = false) keyword: String?, + @RequestParam(required = false, defaultValue = "0") pageNum: Long + ): ResponseEntity { + return ResponseEntity.ok(noticeService.searchNotice(tag, keyword, pageNum)) + } @GetMapping("/{noticeId}") fun readNotice( @PathVariable noticeId: Long, - ) : NoticeDto { - return noticeService.readNotice(noticeId) + ) : ResponseEntity { + return ResponseEntity.ok(noticeService.readNotice(noticeId)) } @PostMapping fun createNotice( @Valid @RequestBody request: CreateNoticeRequest - ) : NoticeDto { - return noticeService.createNotice(request) + ) : ResponseEntity { + return ResponseEntity.ok(noticeService.createNotice(request)) } @PatchMapping("/{noticeId}") fun updateNotice( @PathVariable noticeId: Long, @Valid @RequestBody request: UpdateNoticeRequest, - ) : NoticeDto { - return noticeService.updateNotice(noticeId, request) + ) : ResponseEntity { + return ResponseEntity.ok(noticeService.updateNotice(noticeId, request)) } @DeleteMapping("/{noticeId}") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt index 9fdc013a..2268cb16 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt @@ -20,12 +20,12 @@ class NoticeEntity( var description: String, // var postType: String, -// -// var isPublic: Boolean, -// -// var isSlide: Boolean, -// -// var isPinned: Boolean, + + var isPublic: Boolean, + + var isSlide: Boolean, + + var isPinned: Boolean, @OneToMany(mappedBy = "notice", cascade = [CascadeType.ALL]) var noticeTags: MutableSet = mutableSetOf() 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 0ec7f774..56661cae 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 @@ -1,6 +1,81 @@ package com.wafflestudio.csereal.core.notice.database +import com.querydsl.core.BooleanBuilder +import com.querydsl.core.types.Projections +import com.querydsl.jpa.impl.JPAQueryFactory +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.notice.database.QNoticeEntity.noticeEntity +import com.wafflestudio.csereal.core.notice.database.QNoticeTagEntity.noticeTagEntity +import com.wafflestudio.csereal.core.notice.dto.NoticeDto +import com.wafflestudio.csereal.core.notice.dto.SearchDto +import com.wafflestudio.csereal.core.notice.dto.SearchResponse import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Component + +interface NoticeRepository : JpaRepository, CustomNoticeRepository { +} + +interface CustomNoticeRepository { + fun searchNotice(tag: List?, keyword: String?, pageNum: Long): SearchResponse +} + +@Component +class NoticeRepositoryImpl( + private val queryFactory: JPAQueryFactory, +) : CustomNoticeRepository { + override fun searchNotice(tag: List?, keyword: String?, pageNum: Long): SearchResponse { + val keywordBooleanBuilder = BooleanBuilder() + val tagsBooleanBuilder = BooleanBuilder() + + if (!keyword.isNullOrEmpty()) { + + val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) + keywordList.forEach { + if (it.length == 1) { + throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") + } else { + keywordBooleanBuilder.and( + noticeEntity.title.contains(it) + .or(noticeEntity.description.contains(it)) + ) + } + } + + } + if (!tag.isNullOrEmpty()) { + tag.forEach { + tagsBooleanBuilder.or( + noticeTagEntity.tag.id.eq(it) + ) + } + } + + val total = queryFactory.select(noticeEntity) + .from(noticeEntity).leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) + .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) + .where(keywordBooleanBuilder).where(tagsBooleanBuilder).fetch().size + + val list = queryFactory.select( + Projections.constructor( + SearchDto::class.java, + noticeEntity.id, + noticeEntity.title, + noticeEntity.createdAt, + noticeEntity.isPinned + ) + ).from(noticeEntity) + .leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) + .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) + .where(keywordBooleanBuilder) + .where(tagsBooleanBuilder) + .orderBy(noticeEntity.isPinned.desc()) + .orderBy(noticeEntity.createdAt.desc()) + .offset(20*pageNum) + .limit(20) + .distinct() + .fetch() + + return SearchResponse(total, list) + } -interface NoticeRepository : JpaRepository { } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt index 2c660447..0b0fda52 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt @@ -9,6 +9,12 @@ data class CreateNoticeRequest( @field:NotBlank(message = "내용은 비어있을 수 없습니다") val description: String, - val tags: List = emptyList() + val tags: List = emptyList(), + + val isPublic: Boolean, + + val isSlide: Boolean, + + val isPinned: Boolean, ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt index a858c1dc..7aeccae8 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt @@ -10,11 +10,12 @@ data class NoticeDto( val description: String, // val postType: String, // val authorId: Int, + val tags: List, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, - // val isPublic: Boolean, - // val isSlide: Boolean, - // val isPinned: Boolean, + val isPublic: Boolean, + val isSlide: Boolean, + val isPinned: Boolean, ) { companion object { @@ -25,11 +26,12 @@ data class NoticeDto( title = this.title, description = this.description, // postType = this.postType, + tags = this.noticeTags.map { it.tag.id }, createdAt = this.createdAt, modifiedAt = this.modifiedAt, -// isPublic = this.isPublic, -// isSlide = this.isSlide, -// isPinned = this.isPinned, + isPublic = this.isPublic, + isSlide = this.isSlide, + isPinned = this.isPinned, ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt new file mode 100644 index 00000000..b05e5c11 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt @@ -0,0 +1,13 @@ +package com.wafflestudio.csereal.core.notice.dto + +import com.querydsl.core.annotations.QueryProjection +import java.time.LocalDateTime + +data class SearchDto @QueryProjection constructor( + val noticeId: Long, + val title: String, + val createdDate: LocalDateTime, + val isPinned: Boolean, +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt new file mode 100644 index 00000000..105a25cf --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.notice.dto + +data class SearchResponse( + val total: Int, + val searchList: List +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt index ea45d723..e7fd1fa9 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt @@ -3,6 +3,9 @@ package com.wafflestudio.csereal.core.notice.dto data class UpdateNoticeRequest( val title: String?, val description: String?, - val tags: List? + val tags: List?, + val isPublic: Boolean?, + val isSlide: Boolean?, + val isPinned: Boolean?, ) { } \ No newline at end of file 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 168bed68..36956733 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 @@ -2,14 +2,13 @@ package com.wafflestudio.csereal.core.notice.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.notice.database.* -import com.wafflestudio.csereal.core.notice.dto.CreateNoticeRequest -import com.wafflestudio.csereal.core.notice.dto.NoticeDto -import com.wafflestudio.csereal.core.notice.dto.UpdateNoticeRequest +import com.wafflestudio.csereal.core.notice.dto.* import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface NoticeService { + fun searchNotice(tag: List?, keyword: String?, pageNum: Long): SearchResponse fun readNotice(noticeId: Long): NoticeDto fun createNotice(request: CreateNoticeRequest): NoticeDto fun updateNotice(noticeId: Long, request: UpdateNoticeRequest): NoticeDto @@ -24,10 +23,20 @@ class NoticeServiceImpl( private val noticeTagRepository: NoticeTagRepository ) : NoticeService { + @Transactional(readOnly = true) + override fun searchNotice( + tag: List?, + keyword: String?, + pageNum: Long + ): SearchResponse { + return noticeRepository.searchNotice(tag, keyword, pageNum) + } + @Transactional(readOnly = true) override fun readNotice(noticeId: Long): NoticeDto { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + if (notice.isDeleted) throw CserealException.Csereal400("삭제된 공지사항입니다.(noticeId: $noticeId)") return NoticeDto.of(notice) } @@ -37,6 +46,9 @@ class NoticeServiceImpl( val newNotice = NoticeEntity( title = request.title, description = request.description, + isPublic = request.isPublic, + isSlide = request.isSlide, + isPinned = request.isPinned, ) for (tagId in request.tags) { @@ -58,19 +70,29 @@ class NoticeServiceImpl( notice.title = request.title ?: notice.title notice.description = request.description ?: notice.description + notice.isPublic = request.isPublic ?: notice.isPublic + notice.isSlide = request.isSlide ?: notice.isSlide + notice.isPinned = request.isPinned ?: notice.isPinned if (request.tags != null) { noticeTagRepository.deleteAllByNoticeId(noticeId) - notice.noticeTags.clear() + + // 원래 태그에서 겹치는 태그만 남기고, 나머지는 없애기 + notice.noticeTags = notice.noticeTags.filter { request.tags.contains(it.tag.id) }.toMutableSet() for (tagId in request.tags) { - val tag = tagRepository.findByIdOrNull(tagId) ?: throw CserealException.Csereal400("해당하는 태그가 없습니다") - NoticeTagEntity.createNoticeTag(notice, tag) + // 겹치는 거 말고, 새로운 태그만 추가 + if(!notice.noticeTags.map { it.tag.id }.contains(tagId)) { + val tag = tagRepository.findByIdOrNull(tagId) ?: throw CserealException.Csereal400("해당하는 태그가 없습니다") + NoticeTagEntity.createNoticeTag(notice, tag) + } } } + return NoticeDto.of(notice) + + - return NoticeDto.of(notice) } @Transactional @@ -89,5 +111,5 @@ class NoticeServiceImpl( tagRepository.save(newTag) } - //TODO: 이미지 등록, 페이지네이션, 검색 + //TODO: 이미지 등록, 글쓴이 함께 조회 } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 6af687e6..02d1312c 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -11,15 +11,17 @@ springdoc: --- spring: config.activate.on-profile: local + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/csereal_local?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul + username: root + password: password + jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false - datasource: - url: jdbc:mysql://localhost:3306/csereal - username: root - password: password logging.level: default: INFO From 38de652f4b9e7eb0abd5f8e64054f252b3563366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=B0=ED=98=81=EC=A4=80=20=28HyukJoon=20Woo=29?= Date: Tue, 1 Aug 2023 21:23:20 +0900 Subject: [PATCH 09/27] =?UTF-8?q?CICD:=20=EB=B0=B0=ED=8F=AC=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=ED=99=94=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CICD: Change expose port and added image tag * CICD: Change ddl-auto to create in prod profile for test * CICD: Added Deploy github action * CICD: Merge jobs to one job * Fix: Change checkout order to first step * CICD: Add context for docker build action * Fix: Change spring profile arg position * CICD: Change openjdk version to 17 * CICD: Change docker compose build image tag to latest * CICD: Change to use ghcr repository. * Fix: change list to string in docker push tags. * Fix: Change registry to ghcr.io * Fix: change env to pass to github action instead of ssh export command * Fix: unwrap bracket. * Fix: wrap Profile with "" * CICD: Add .env file * CICD: Change prod ddl-auto to create (for developing), and add TODO comment. * CICD: Remove cicd/deploy branch for condition. --- .github/workflows/deploy.yaml | 82 +++++++++++++++++++++++++++++ Dockerfile | 4 +- docker-compose.yml | 3 +- src/main/resources/application.yaml | 10 ++-- 4 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/deploy.yaml diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..43e5f370 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,82 @@ +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - + name: Checkout + uses: actions/checkout@v3 + + - + name: Setup Java JDK + uses: actions/setup-java@v3.12.0 + with: + java-version: '17' + distribution: 'adopt' + + - + run: ./gradlew clean bootJar -x test + + - + name: Log in to the Container Registry + uses: docker/login-action@v2.2.0 + with: + registry: ghcr.io + username: ${{github.actor}} + password: ${{secrets.GITHUB_TOKEN}} + + - + name: Build and push Docker image + uses: docker/build-push-action@v4.1.1 + with: + context: . + push: true + tags: | + ghcr.io/wafflestudio/csereal-server/server_image:latest + ghcr.io/wafflestudio/csereal-server/server_image:${{github.sha}} + + - + name: Create .env file + run: | + echo "MYSQL_ROOT_PASSWORD=${{secrets.MYSQL_ROOT_PASSWORD}}" > .env + echo "MYSQL_USER=${{secrets.MYSQL_USER}}" >> .env + echo "MYSQL_PASSWORD=${{secrets.MYSQL_PASSWORD}}" >> .env + echo "MYSQL_DATABASE=${{secrets.MYSQL_DATABASE}}" >> .env + echo "PROFILE=prod" >> .env + + - + name: SCP Command to Transfer Files + uses: appleboy/scp-action@v0.1.4 + with: + host: ${{secrets.SSH_HOST}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + source: "docker-compose.yml, .env" + target: "~/app" + overwrite: true + - + name: SSH Remote Commands + uses: appleboy/ssh-action@v1.0.0 + env: + MYSQL_ROOT_PASSWORD: ${{secrets.MYSQL_ROOT_PASSWORD}} + MYSQL_USER: ${{secrets.MYSQL_USER}} + MYSQL_PASSWORD: ${{secrets.MYSQL_PASSWORD}} + MYSQL_DATABASE: ${{secrets.MYSQL_DATABASE}} + PROFILE: "prod" + with: + host: ${{secrets.SSH_HOST}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + script: | + cd ~/app + docker-compose down + docker-compose pull + docker-compose up -d \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 92e86d2b..cecb2839 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:18-slim +FROM openjdk:17-slim ARG PROFILE=prod @@ -9,4 +9,4 @@ COPY ./build/libs/*.jar /app/app.jar EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar", "-Dspring.profiles.active=${PROFILE}"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=$PROFILE", "app.jar"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 40e187c7..9cf741c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: args: PROFILE: ${PROFILE} ports: - - 8080:8080 + - 80:8080 environment: SPRING_DATASOURCE_URL: "jdbc:mysql://csereal_db_container:3306/${MYSQL_DATABASE}?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} @@ -15,6 +15,7 @@ services: - db networks: - csereal_network + image: ghcr.io/wafflestudio/csereal-server/server_image:latest db: container_name: csereal_db_container image: mysql:8.0 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 02d1312c..8ffade6f 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,6 +2,8 @@ spring: profiles: active: local +datasource: + driver-class-name: com.mysql.cj.jdbc.Driver springdoc: swagger-ui: path: index.html @@ -12,11 +14,9 @@ springdoc: spring: config.activate.on-profile: local datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://127.0.0.1:3306/csereal_local?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul + url: jdbc:mysql://127.0.0.1:3306/csereal?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul username: root password: password - jpa: hibernate: ddl-auto: update @@ -30,5 +30,5 @@ spring: config.activate.on-profile: prod jpa: hibernate: - ddl-auto: none - open-in-view: false + ddl-auto: create # TODO: change to validate (or none) when save actual data to server + open-in-view: false \ No newline at end of file From 1a75adfc8d4cbd5abd9ac7a3f6b0adb8c10b080a Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Sat, 5 Aug 2023 12:51:50 +0900 Subject: [PATCH 10/27] =?UTF-8?q?feat:=20=EA=B5=AC=EC=84=B1=EC=9B=90(?= =?UTF-8?q?=EA=B5=90=EC=88=98)=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 교수 조회시 최종학력이 앞으로 오게끔 정렬 * feat: 교수 수정 및 삭제 API * feat: 학력과 경력 엔티티 필드 변경, 수정 API 구현 --- .../core/member/api/ProfessorController.kt | 14 ++++ .../core/member/database/CareerEntity.kt | 9 +-- .../core/member/database/EducationEntity.kt | 20 +---- .../core/member/database/ProfessorEntity.kt | 20 ++++- .../csereal/core/member/dto/CareerDto.kt | 19 ----- .../csereal/core/member/dto/EducationDto.kt | 22 ------ .../csereal/core/member/dto/ProfessorDto.kt | 8 +- .../core/member/service/ProfessorService.kt | 74 +++++++++++++++---- 8 files changed, 101 insertions(+), 85 deletions(-) delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt 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 437c2544..993ddba8 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 @@ -32,4 +32,18 @@ class ProfessorController( return ResponseEntity.ok(professorService.getInactiveProfessors()) } + @PatchMapping("/{professorId}") + fun updateProfessor( + @PathVariable professorId: Long, + @RequestBody updateProfessorRequest: ProfessorDto + ): ResponseEntity { + return ResponseEntity.ok(professorService.updateProfessor(professorId, updateProfessorRequest)) + } + + @DeleteMapping("/{professorId}") + fun deleteProfessor(@PathVariable professorId: Long): ResponseEntity { + professorService.deleteProfessor(professorId) + return ResponseEntity.ok().build() + } + } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt index 30df5357..103d42a9 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt @@ -1,7 +1,6 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity -import com.wafflestudio.csereal.core.member.dto.CareerDto import jakarta.persistence.Entity import jakarta.persistence.FetchType import jakarta.persistence.JoinColumn @@ -9,20 +8,16 @@ import jakarta.persistence.ManyToOne @Entity(name = "career") class CareerEntity( - val duration: String, val name: String, - val workplace: String, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "professor_id") val professor: ProfessorEntity ) : BaseTimeEntity() { companion object { - fun create(career: CareerDto, professor: ProfessorEntity): CareerEntity { + fun create(name: String, professor: ProfessorEntity): CareerEntity { val careerEntity = CareerEntity( - duration = career.duration, - name = career.name, - workplace = career.workplace, + name = name, professor = professor ) professor.careers.add(careerEntity) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt index 7bdae723..ff4559ce 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt @@ -1,30 +1,20 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity -import com.wafflestudio.csereal.core.member.dto.EducationDto import jakarta.persistence.* @Entity(name = "education") class EducationEntity( - val university: String, - val major: String, - - @Enumerated(EnumType.STRING) - val degree: Degree, - - val year: Int, + val name: String, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "professor_id") val professor: ProfessorEntity ) : BaseTimeEntity() { companion object { - fun create(education: EducationDto, professor: ProfessorEntity): EducationEntity { + fun create(name: String, professor: ProfessorEntity): EducationEntity { val educationEntity = EducationEntity( - university = education.university, - major = education.major, - degree = education.degree, - year = education.year, + name = name, professor = professor ) professor.educations.add(educationEntity) @@ -32,7 +22,3 @@ class EducationEntity( } } } - -enum class Degree { - Bachelor, Master, PhD -} 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 09efde16..8e72b964 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 @@ -13,7 +13,7 @@ import java.time.LocalDate @Entity(name = "professor") class ProfessorEntity( - val name: String, + var name: String, //val profileImage:File var isActive: Boolean, var academicRank: String, @@ -24,7 +24,7 @@ class ProfessorEntity( var startDate: LocalDate?, var endDate: LocalDate?, - val office: String?, + var office: String?, var phone: String?, var fax: String?, var email: String?, @@ -34,7 +34,7 @@ class ProfessorEntity( val educations: MutableList = mutableListOf(), @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) - val researchAreas: MutableSet = mutableSetOf(), + val researchAreas: MutableList = mutableListOf(), @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) val careers: MutableList = mutableListOf(), @@ -59,7 +59,21 @@ class ProfessorEntity( } fun addLab(lab: LabEntity) { + this.lab?.professors?.remove(this) this.lab = lab lab.professors.add(this) } + + fun update(updateProfessorRequest: ProfessorDto) { + this.name = updateProfessorRequest.name + this.isActive = updateProfessorRequest.isActive + 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 + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt deleted file mode 100644 index ff14cfb2..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.wafflestudio.csereal.core.member.dto - -import com.wafflestudio.csereal.core.member.database.CareerEntity - -data class CareerDto( - val duration: String, - val name: String, - val workplace: String -) { - companion object { - fun of(careerEntity: CareerEntity): CareerDto { - return CareerDto( - duration = careerEntity.duration, - name = careerEntity.name, - workplace = careerEntity.workplace - ) - } - } -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt deleted file mode 100644 index 48e2c968..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.wafflestudio.csereal.core.member.dto - -import com.wafflestudio.csereal.core.member.database.Degree -import com.wafflestudio.csereal.core.member.database.EducationEntity - -data class EducationDto( - val university: String, - val major: String, - val degree: Degree, - val year: Int -) { - companion object { - fun of(educationEntity: EducationEntity): EducationDto { - return EducationDto( - university = educationEntity.university, - major = educationEntity.major, - degree = educationEntity.degree, - year = educationEntity.year - ) - } - } -} 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 d4a52290..84372eee 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 @@ -18,9 +18,9 @@ data class ProfessorDto( val fax: String?, val email: String?, val website: String?, - val educations: List, + val educations: List, val researchAreas: List, - val careers: List + val careers: List ) { companion object { fun of(professorEntity: ProfessorEntity): ProfessorDto { @@ -37,9 +37,9 @@ data class ProfessorDto( fax = professorEntity.fax, email = professorEntity.email, website = professorEntity.website, - educations = professorEntity.educations.map { EducationDto.of(it) }, + educations = professorEntity.educations.map { it.name }, researchAreas = professorEntity.researchAreas.map { it.name }, - careers = professorEntity.careers.map { CareerDto.of(it) } + careers = professorEntity.careers.map { it.name } ) } } 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 d2f8b582..9adb7df6 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 @@ -10,11 +10,11 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface ProfessorService { - fun createProfessor(professorDto: ProfessorDto): ProfessorDto + fun createProfessor(createProfessorRequest: ProfessorDto): ProfessorDto fun getProfessor(professorId: Long): ProfessorDto fun getActiveProfessors(): List fun getInactiveProfessors(): List - fun updateProfessor(updateProfessorRequest: ProfessorDto): ProfessorDto + fun updateProfessor(professorId: Long, updateProfessorRequest: ProfessorDto): ProfessorDto fun deleteProfessor(professorId: Long) } @@ -25,24 +25,24 @@ class ProfessorServiceImpl( private val professorRepository: ProfessorRepository ) : ProfessorService { - override fun createProfessor(professorDto: ProfessorDto): ProfessorDto { - val professor = ProfessorEntity.of(professorDto) + override fun createProfessor(createProfessorRequest: ProfessorDto): ProfessorDto { + val professor = ProfessorEntity.of(createProfessorRequest) - if (professorDto.labId != null) { - val lab = labRepository.findByIdOrNull(professorDto.labId) - ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${professorDto.labId}") + if (createProfessorRequest.labId != null) { + val lab = labRepository.findByIdOrNull(createProfessorRequest.labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${createProfessorRequest.labId}") professor.addLab(lab) } - for (education in professorDto.educations) { + for (education in createProfessorRequest.educations) { EducationEntity.create(education, professor) } - for (researchArea in professorDto.researchAreas) { + for (researchArea in createProfessorRequest.researchAreas) { ResearchAreaEntity.create(researchArea, professor) } - for (career in professorDto.careers) { + for (career in createProfessorRequest.careers) { CareerEntity.create(career, professor) } @@ -68,12 +68,60 @@ class ProfessorServiceImpl( return professorRepository.findByIsActiveFalse().map { SimpleProfessorDto.of(it) } } - override fun updateProfessor(updateProfessorRequest: ProfessorDto): ProfessorDto { - TODO("Not yet implemented") + override fun updateProfessor(professorId: Long, updateProfessorRequest: ProfessorDto): ProfessorDto { + + val professor = professorRepository.findByIdOrNull(professorId) + ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: ${professorId}") + + if (updateProfessorRequest.labId != null && updateProfessorRequest.labId != professor.lab?.id) { + val lab = labRepository.findByIdOrNull(updateProfessorRequest.labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${updateProfessorRequest.labId}") + professor.addLab(lab) + } + + professor.update(updateProfessorRequest) + + // 학력 업데이트 + val oldEducations = professor.educations.map { it.name } + + val educationsToRemove = oldEducations - updateProfessorRequest.educations + val educationsToAdd = updateProfessorRequest.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 + + 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 + + professor.careers.removeIf { it.name in careersToRemove } + + for (career in careersToAdd) { + CareerEntity.create(career, professor) + } + + return ProfessorDto.of(professor) } override fun deleteProfessor(professorId: Long) { - TODO("Not yet implemented") + professorRepository.deleteById(professorId) } } From 42249ba6885f34634e80ecd3e5d7d2640b0327c8 Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Sat, 5 Aug 2023 12:52:25 +0900 Subject: [PATCH 11/27] =?UTF-8?q?feat:=20=EA=B5=AC=EC=84=B1=EC=9B=90(?= =?UTF-8?q?=ED=96=89=EC=A0=95=EC=A7=81=EC=9B=90)=20CRUD=20API=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 행정직원 엔티티 및 DTO 설계 * feat: 행정직원 CRUD * feat: 교수 조회시 이름순 정렬 * fix: 교수 연구 분야 Set -> List 로 변경 * feat: 행정직원 주요업무 업데이트 구현 --- .../core/member/api/StaffController.kt | 46 +++++++++++ .../core/member/database/StaffEntity.kt | 44 +++++++++++ .../core/member/database/StaffRepository.kt | 6 ++ .../core/member/database/TaskEntity.kt | 27 +++++++ .../csereal/core/member/dto/StaffDto.kt | 29 +++++++ .../core/member/service/ProfessorService.kt | 4 +- .../core/member/service/StaffService.kt | 76 +++++++++++++++++++ 7 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt 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 new file mode 100644 index 00000000..f10aae6f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/api/StaffController.kt @@ -0,0 +1,46 @@ +package com.wafflestudio.csereal.core.member.api + +import com.wafflestudio.csereal.core.member.dto.StaffDto +import com.wafflestudio.csereal.core.member.service.StaffService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PatchMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/staff") +@RestController +class StaffController( + private val staffService: StaffService +) { + + @PostMapping + fun createStaff(@RequestBody createStaffRequest: StaffDto): ResponseEntity { + return ResponseEntity.ok(staffService.createStaff(createStaffRequest)) + } + + @GetMapping("/{staffId}") + fun getStaff(@PathVariable staffId: Long): ResponseEntity { + return ResponseEntity.ok(staffService.getStaff(staffId)) + } + + @GetMapping + fun getAllStaff(): ResponseEntity> { + return ResponseEntity.ok(staffService.getAllStaff()) + } + + @PatchMapping("/{staffId}") + fun updateStaff(@PathVariable staffId: Long, @RequestBody updateStaffRequest: StaffDto): ResponseEntity { + return ResponseEntity.ok(staffService.updateStaff(staffId, updateStaffRequest)) + } + + @DeleteMapping("/{staffId}") + fun deleteStaff(@PathVariable staffId: Long): ResponseEntity { + staffService.deleteStaff(staffId) + return ResponseEntity.ok().build() + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt new file mode 100644 index 00000000..f950ed43 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt @@ -0,0 +1,44 @@ +package com.wafflestudio.csereal.core.member.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.member.dto.StaffDto +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.OneToMany + +@Entity(name = "staff") +class StaffEntity( + var name: String, + var role: String, + + // profileImage + + var office: String, + var phone: String, + var email: String, + + @OneToMany(mappedBy = "staff", cascade = [CascadeType.ALL], orphanRemoval = true) + val tasks: MutableList = mutableListOf() + +) : BaseTimeEntity() { + + companion object { + fun of(staffDto: StaffDto): StaffEntity { + return StaffEntity( + name = staffDto.name, + role = staffDto.role, + office = staffDto.office, + phone = staffDto.phone, + email = staffDto.email + ) + } + } + + fun update(staffDto: StaffDto) { + this.name = staffDto.name + this.role = staffDto.role + this.office = staffDto.office + this.phone = staffDto.phone + this.email = staffDto.email + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt new file mode 100644 index 00000000..d85ab2d9 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffRepository.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.member.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface StaffRepository : JpaRepository { +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt new file mode 100644 index 00000000..745365dd --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt @@ -0,0 +1,27 @@ +package com.wafflestudio.csereal.core.member.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +@Entity +class TaskEntity( + val name: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "staff_id") + val staff: StaffEntity +) : BaseTimeEntity() { + companion object { + fun create(name: String, staff: StaffEntity): TaskEntity { + val task = TaskEntity( + name = name, + staff = staff + ) + staff.tasks.add(task) + return task + } + } +} 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 new file mode 100644 index 00000000..dfdec200 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/StaffDto.kt @@ -0,0 +1,29 @@ +package com.wafflestudio.csereal.core.member.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.wafflestudio.csereal.core.member.database.StaffEntity + +data class StaffDto( + @JsonInclude(JsonInclude.Include.NON_NULL) + var id: Long? = null, + val name: String, + val role: String, + val office: String, + val phone: String, + val email: String, + val tasks: List +) { + companion object { + fun of(staffEntity: StaffEntity): StaffDto { + return StaffDto( + id = staffEntity.id, + name = staffEntity.name, + role = staffEntity.role, + office = staffEntity.office, + phone = staffEntity.phone, + email = staffEntity.email, + tasks = staffEntity.tasks.map { it.name } + ) + } + } +} 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 9adb7df6..ad025f8f 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 @@ -60,12 +60,12 @@ class ProfessorServiceImpl( @Transactional(readOnly = true) override fun getActiveProfessors(): List { - return professorRepository.findByIsActiveTrue().map { SimpleProfessorDto.of(it) } + return professorRepository.findByIsActiveTrue().map { SimpleProfessorDto.of(it) }.sortedBy { it.name } } @Transactional(readOnly = true) override fun getInactiveProfessors(): List { - return professorRepository.findByIsActiveFalse().map { SimpleProfessorDto.of(it) } + return professorRepository.findByIsActiveFalse().map { SimpleProfessorDto.of(it) }.sortedBy { it.name } } override fun updateProfessor(professorId: Long, updateProfessorRequest: ProfessorDto): ProfessorDto { 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 new file mode 100644 index 00000000..e7249113 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt @@ -0,0 +1,76 @@ +package com.wafflestudio.csereal.core.member.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.member.database.StaffEntity +import com.wafflestudio.csereal.core.member.database.StaffRepository +import com.wafflestudio.csereal.core.member.database.TaskEntity +import com.wafflestudio.csereal.core.member.dto.StaffDto +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface StaffService { + fun createStaff(createStaffRequest: StaffDto): StaffDto + fun getStaff(staffId: Long): StaffDto + fun getAllStaff(): List + fun updateStaff(staffId: Long, updateStaffRequest: StaffDto): StaffDto + fun deleteStaff(staffId: Long) +} + +@Service +@Transactional +class StaffServiceImpl( + private val staffRepository: StaffRepository +) : StaffService { + override fun createStaff(createStaffRequest: StaffDto): StaffDto { + val staff = StaffEntity.of(createStaffRequest) + + for (task in createStaffRequest.tasks) { + TaskEntity.create(task, staff) + } + + staffRepository.save(staff) + + return StaffDto.of(staff) + } + + @Transactional(readOnly = true) + override fun getStaff(staffId: Long): StaffDto { + val staff = staffRepository.findByIdOrNull(staffId) + ?: throw CserealException.Csereal404("해당 행정직원을 찾을 수 없습니다. staffId: ${staffId}") + return StaffDto.of(staff) + } + + @Transactional(readOnly = true) + override fun getAllStaff(): List { + return staffRepository.findAll().map { StaffDto.of(it) }.sortedBy { it.name } + } + + override fun updateStaff(staffId: Long, updateStaffRequest: StaffDto): StaffDto { + + val staff = staffRepository.findByIdOrNull(staffId) + ?: throw CserealException.Csereal404("해당 행정직원을 찾을 수 없습니다. staffId: ${staffId}") + + staff.update(updateStaffRequest) + + // 주요 업무 업데이트 + val oldTasks = staff.tasks.map { it.name } + + val tasksToRemove = oldTasks - updateStaffRequest.tasks + val tasksToAdd = updateStaffRequest.tasks - oldTasks + + staff.tasks.removeIf { it.name in tasksToRemove } + + for (task in tasksToAdd) { + TaskEntity.create(task, staff) + } + + return StaffDto.of(staff) + } + + override fun deleteStaff(staffId: Long) { + staffRepository.deleteById(staffId) + } + + +} From 41bb4fdcd1388ad5b225ad8260ad32dff0fb8d62 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Tue, 8 Aug 2023 21:12:40 +0900 Subject: [PATCH 12/27] =?UTF-8?q?feat:=20news=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80,=20=EB=94=94=EB=B2=A8=EB=A1=AD?= =?UTF-8?q?=20=EB=B0=8F=20=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: news 패키지 생성 * feat: readNews 생성, news 패키지 추가로 인한 명칭 변경 * feat: createNews, enrollTag(새소식) 추가, news 패키지로 인한 명칭 추가 변경 * feat: updateNews, deleteNews 추가 * fix: searchNotice 관련 명칭 변경 * feat: searchNews 추가 * fix: develop 브랜치 반영, 프론트 요구사항 반영 * feat: readNotice, readNews에 이전글 다음글 추가 * 태그 업데이트 코드 리팩터링중 * refactor: 코드 수정, 이전제목 추가 * fix: 게시글 하나일때 read 가능 * fix: prevNext null 없애기 * fix: 이전글 다음글 null 수정 --- build.gradle.kts | 3 + .../csereal/core/news/api/NewsController.kt | 64 ++++++++ .../csereal/core/news/database/NewsEntity.kt | 37 +++++ .../core/news/database/NewsRepository.kt | 139 ++++++++++++++++++ .../core/news/database/NewsTagEntity.kt | 28 ++++ .../core/news/database/NewsTagRepository.kt | 9 ++ .../core/news/database/TagInNewsEntity.kt | 14 ++ .../news/database/TageInNewsRepository.kt | 7 + .../csereal/core/news/dto/NewsDto.kt | 41 ++++++ .../csereal/core/news/dto/NewsSearchDto.kt | 12 ++ .../core/news/dto/NewsSearchResponse.kt | 10 ++ .../csereal/core/news/service/NewsService.kt | 113 ++++++++++++++ .../core/notice/api/NoticeController.kt | 12 +- .../core/notice/database/NoticeEntity.kt | 12 +- .../core/notice/database/NoticeRepository.kt | 101 ++++++++++--- .../core/notice/database/NoticeTagEntity.kt | 4 +- .../notice/database/NoticeTagRepository.kt | 1 + .../{TagEntity.kt => TagInNoticeEntity.kt} | 5 +- .../notice/database/TagInNoticeRepository.kt | 7 + .../core/notice/database/TagRepository.kt | 6 - .../core/notice/dto/CreateNoticeRequest.kt | 20 --- .../csereal/core/notice/dto/NoticeDto.kt | 18 ++- .../dto/{SearchDto.kt => NoticeSearchDto.kt} | 6 +- ...rchResponse.kt => NoticeSearchResponse.kt} | 4 +- .../core/notice/dto/UpdateNoticeRequest.kt | 11 -- .../core/notice/service/NoticeService.kt | 87 +++++------ 26 files changed, 643 insertions(+), 128 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/database/TagInNewsEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/database/TageInNewsRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchResponse.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt rename src/main/kotlin/com/wafflestudio/csereal/core/notice/database/{TagEntity.kt => TagInNoticeEntity.kt} (81%) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeRepository.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt rename src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/{SearchDto.kt => NoticeSearchDto.kt} (63%) rename src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/{SearchResponse.kt => NoticeSearchResponse.kt} (50%) delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 4acfd1ae..3e5a2ca5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,9 @@ dependencies { kapt("com.querydsl:querydsl-apt:5.0.0:jakarta") kapt("jakarta.annotation:jakarta.annotation-api") kapt("jakarta.persistence:jakarta.persistence-api") + + // 태그 제거 + implementation("org.jsoup:jsoup:1.15.4") } noArg { annotation("jakarta.persistence.Entity") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt new file mode 100644 index 00000000..4dea9962 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt @@ -0,0 +1,64 @@ +package com.wafflestudio.csereal.core.news.api + +import com.wafflestudio.csereal.core.news.dto.NewsDto +import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse +import com.wafflestudio.csereal.core.news.service.NewsService +import jakarta.validation.Valid +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RequestMapping("/news") +@RestController +class NewsController( + private val newsService: NewsService, +) { + @GetMapping + fun searchNews( + @RequestParam(required = false) tag: List?, + @RequestParam(required = false) keyword: String?, + @RequestParam(required = false, defaultValue = "0") pageNum:Long + ) : ResponseEntity { + return ResponseEntity.ok(newsService.searchNews(tag, keyword, pageNum)) + } + @GetMapping("/{newsId}") + fun readNews( + @PathVariable newsId: Long, + @RequestParam(required = false) tag : List?, + @RequestParam(required = false) keyword: String?, + ) : ResponseEntity { + return ResponseEntity.ok(newsService.readNews(newsId, tag, keyword)) + } + + @PostMapping + fun createNews( + @Valid @RequestBody request: NewsDto + ) : ResponseEntity { + return ResponseEntity.ok(newsService.createNews(request)) + } + + @PatchMapping("/{newsId}") + fun updateNews( + @PathVariable newsId: Long, + @Valid @RequestBody request: NewsDto, + ) : ResponseEntity { + return ResponseEntity.ok(newsService.updateNews(newsId, request)) + } + + @DeleteMapping("/{newsId}") + fun deleteNews( + @PathVariable newsId: Long + ) { + newsService.deleteNews(newsId) + } + + @PostMapping("/tag") + fun enrollTag( + @RequestBody tagName: Map + ) : ResponseEntity { + newsService.enrollTag(tagName["name"]!!) + return ResponseEntity("등록되었습니다. (tagName: ${tagName["name"]})", HttpStatus.OK) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt new file mode 100644 index 00000000..a2944a97 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt @@ -0,0 +1,37 @@ +package com.wafflestudio.csereal.core.news.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.news.dto.NewsDto +import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.OneToMany + +@Entity(name = "news") +class NewsEntity( + + var isDeleted: Boolean = false, + + var title: String, + + var description: String, + + var isPublic: Boolean, + + var isSlide: Boolean, + + // 새소식 작성란에도 "가장 위에 표시"가 있더라고요, 혹시 쓸지도 모르니까 남겼습니다 + var isPinned: Boolean, + + @OneToMany(mappedBy = "news", cascade = [CascadeType.ALL]) + var newsTags: MutableSet = mutableSetOf() + +): BaseTimeEntity() { + fun update(updateNewsRequest: NewsDto) { + this.title = updateNewsRequest.title + this.description = updateNewsRequest.description + this.isPublic = updateNewsRequest.isPublic + this.isSlide = updateNewsRequest.isSlide + this.isPinned = updateNewsRequest.isPinned + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..3edf6858 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsRepository.kt @@ -0,0 +1,139 @@ +package com.wafflestudio.csereal.core.news.database + +import com.querydsl.core.BooleanBuilder +import com.querydsl.jpa.impl.JPAQueryFactory +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.news.database.QNewsEntity.newsEntity +import com.wafflestudio.csereal.core.news.database.QNewsTagEntity.newsTagEntity +import com.wafflestudio.csereal.core.news.dto.NewsSearchDto +import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse +import org.jsoup.Jsoup +import org.jsoup.parser.Parser +import org.jsoup.safety.Safelist +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Component + +interface NewsRepository : JpaRepository, CustomNewsRepository { + +} + +interface CustomNewsRepository { + fun searchNews(tag: List?, keyword: String?, pageNum: Long): NewsSearchResponse + fun findPrevNextId(newsId: Long, tag: List?, keyword: String?): Array? +} + +@Component +class NewsRepositoryImpl( + private val queryFactory: JPAQueryFactory, +) : CustomNewsRepository { + override fun searchNews(tag: List?, keyword: String?, pageNum: Long): NewsSearchResponse { + val keywordBooleanBuilder = BooleanBuilder() + val tagsBooleanBuilder = BooleanBuilder() + + if(!keyword.isNullOrEmpty()) { + val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) + keywordList.forEach { + if(it.length == 1) { + throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") + } else { + keywordBooleanBuilder.and( + newsEntity.title.contains(it) + .or(newsEntity.description.contains(it)) + ) + } + } + } + if(!tag.isNullOrEmpty()) { + tag.forEach { + tagsBooleanBuilder.or( + newsTagEntity.tag.name.eq(it) + ) + } + } + + val jpaQuery = queryFactory.select(newsEntity).from(newsEntity) + .leftJoin(newsTagEntity).on(newsTagEntity.news.eq(newsEntity)) + .where(newsEntity.isDeleted.eq(false), newsEntity.isPublic.eq(true)) + .where(keywordBooleanBuilder).where(tagsBooleanBuilder) + + val total = jpaQuery.distinct().fetch().size + + val newsEntityList = jpaQuery.orderBy(newsEntity.isPinned.desc()) + .orderBy(newsEntity.createdAt.desc()) + .offset(20*pageNum) //로컬 테스트를 위해 잠시 5로 둘 것, 원래는 20 + .limit(20) + .distinct() + .fetch() + + val newsSearchDtoList : List = newsEntityList.map { + NewsSearchDto( + id = it.id, + title = it.title, + summary = summary(it.description), + createdAt = it.createdAt, + tags = it.newsTags.map { newsTagEntity -> newsTagEntity.tag.id } + ) + } + return NewsSearchResponse(total, newsSearchDtoList) + } + + override fun findPrevNextId(newsId: Long, tag: List?, keyword: String?): Array? { + val keywordBooleanBuilder = BooleanBuilder() + val tagsBooleanBuilder = BooleanBuilder() + + if (!keyword.isNullOrEmpty()) { + val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) + keywordList.forEach { + if(it.length == 1) { + throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") + } else { + keywordBooleanBuilder.and( + newsEntity.title.contains(it) + .or(newsEntity.description.contains(it)) + ) + } + + } + } + if(!tag.isNullOrEmpty()) { + tag.forEach { + tagsBooleanBuilder.or( + newsTagEntity.tag.name.eq(it) + ) + } + } + + val newsSearchDtoList = queryFactory.select(newsEntity).from(newsEntity) + .leftJoin(newsTagEntity).on(newsTagEntity.news.eq(newsEntity)) + .where(newsEntity.isDeleted.eq(false), newsEntity.isPublic.eq(true)) + .where(keywordBooleanBuilder).where(tagsBooleanBuilder) + .orderBy(newsEntity.isPinned.desc()) + .orderBy(newsEntity.createdAt.desc()) + .distinct() + .fetch() + + + val findingId = newsSearchDtoList.indexOfFirst { it.id == newsId } + + val prevNext: Array? + if(findingId == -1) { + prevNext = null + } else if(findingId != 0 && findingId != newsSearchDtoList.size-1) { + prevNext = arrayOf(newsSearchDtoList[findingId+1], newsSearchDtoList[findingId-1]) + } else if(findingId == 0) { + if(newsSearchDtoList.size == 1) { + prevNext = arrayOf(null, null) + } else { + prevNext = arrayOf(newsSearchDtoList[1], null) + } + } else { + prevNext = arrayOf(null, newsSearchDtoList[newsSearchDtoList.size-2]) + } + + return prevNext + } + private fun summary(description: String): String { + val summary = Jsoup.clean(description, Safelist.none()) + return Parser.unescapeEntities(summary, false) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagEntity.kt new file mode 100644 index 00000000..d328f109 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagEntity.kt @@ -0,0 +1,28 @@ +package com.wafflestudio.csereal.core.news.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +@Entity(name = "newsTag") +class NewsTagEntity( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "news_id") + val news: NewsEntity, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + var tag: TagInNewsEntity, + + ) : BaseTimeEntity() { + + companion object { + fun createNewsTag(news: NewsEntity, tag: TagInNewsEntity) { + val newsTag = NewsTagEntity(news, tag) + news.newsTags.add(newsTag) + tag.newsTags.add(newsTag) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagRepository.kt new file mode 100644 index 00000000..3a947ac1 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsTagRepository.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.csereal.core.news.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface NewsTagRepository : JpaRepository { + fun deleteAllByNewsId(newsId: Long) + fun deleteByNewsIdAndTagId(newsId: Long, tagId: Long) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/TagInNewsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/TagInNewsEntity.kt new file mode 100644 index 00000000..16487e95 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/TagInNewsEntity.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.csereal.core.news.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.OneToMany + +@Entity(name = "tag_in_news") +class TagInNewsEntity( + var name: String, + + @OneToMany(mappedBy = "tag") + val newsTags: MutableSet = mutableSetOf() +) : BaseTimeEntity() { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/TageInNewsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/TageInNewsRepository.kt new file mode 100644 index 00000000..db4e2c6f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/TageInNewsRepository.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.news.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface TagInNewsRepository : JpaRepository { + fun findByName(tagName: String): TagInNewsEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt new file mode 100644 index 00000000..da7f987c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt @@ -0,0 +1,41 @@ +package com.wafflestudio.csereal.core.news.dto + +import com.wafflestudio.csereal.core.news.database.NewsEntity +import java.time.LocalDateTime + +data class NewsDto( + val id: Long, + val title: String, + val description: String, + val tags: List, + val createdAt: LocalDateTime?, + val modifiedAt: LocalDateTime?, + val isPublic: Boolean, + val isSlide: Boolean, + val isPinned: Boolean, + val prevId: Long?, + val prevTitle: String?, + val nextId: Long?, + val nextTitle: String?, +) { + companion object { + fun of(entity: NewsEntity, prevNext: Array?) : NewsDto = entity.run { + NewsDto( + id = this.id, + title = this.title, + description = this.description, + tags = this.newsTags.map { it.tag.name }, + createdAt = this.createdAt, + modifiedAt = this.modifiedAt, + isPublic = this.isPublic, + isSlide = this.isSlide, + isPinned = this.isPinned, + prevId = prevNext?.get(0)?.id, + prevTitle = prevNext?.get(0)?.title, + nextId = prevNext?.get(1)?.id, + nextTitle = prevNext?.get(1)?.title, + + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchDto.kt new file mode 100644 index 00000000..0d3cae9f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchDto.kt @@ -0,0 +1,12 @@ +package com.wafflestudio.csereal.core.news.dto + +import com.querydsl.core.annotations.QueryProjection +import java.time.LocalDateTime + +data class NewsSearchDto @QueryProjection constructor( + val id: Long, + val title: String, + var summary: String, + val createdAt: LocalDateTime?, + var tags: List? +) \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchResponse.kt new file mode 100644 index 00000000..92a3a63c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsSearchResponse.kt @@ -0,0 +1,10 @@ +package com.wafflestudio.csereal.core.news.dto + +import com.querydsl.core.annotations.QueryProjection +import java.time.LocalDateTime + +class NewsSearchResponse @QueryProjection constructor( + val total: Int, + val searchList: List +) { +} \ No newline at end of file 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 new file mode 100644 index 00000000..eb638dfb --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/service/NewsService.kt @@ -0,0 +1,113 @@ +package com.wafflestudio.csereal.core.news.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.news.database.* +import com.wafflestudio.csereal.core.news.dto.NewsDto +import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.* + +interface NewsService { + fun searchNews(tag: List?, keyword: String?, pageNum: Long): NewsSearchResponse + fun readNews(newsId: Long, tag: List?, keyword: String?): NewsDto + fun createNews(request: NewsDto): NewsDto + fun updateNews(newsId: Long, request: NewsDto): NewsDto + fun deleteNews(newsId: Long) + fun enrollTag(tagName: String) +} + +@Service +class NewsServiceImpl( + private val newsRepository: NewsRepository, + private val tagInNewsRepository: TagInNewsRepository, + private val newsTagRepository: NewsTagRepository +) : NewsService { + @Transactional(readOnly = true) + override fun searchNews( + tag: List?, + keyword: String?, + pageNum: Long + ): NewsSearchResponse { + return newsRepository.searchNews(tag, keyword, pageNum) + } + + @Transactional(readOnly = true) + override fun readNews( + newsId: Long, + tag: List?, + keyword: String? + ): NewsDto { + val news: NewsEntity = newsRepository.findByIdOrNull(newsId) + ?: throw CserealException.Csereal404("존재하지 않는 새소식입니다.(newsId: $newsId)") + + if (news.isDeleted) throw CserealException.Csereal404("삭제된 새소식입니다.(newsId: $newsId)") + + val prevNext = newsRepository.findPrevNextId(newsId, tag, keyword) + ?: throw CserealException.Csereal400("이전글 다음글이 존재하지 않습니다.(newsId=$newsId)") + + return NewsDto.of(news, prevNext) + } + + @Transactional + override fun createNews(request: NewsDto): NewsDto { + val newNews = NewsEntity( + title = request.title, + description = request.description, + isPublic = request.isPublic, + isSlide = request.isSlide, + isPinned = request.isPinned + ) + + for (tagName in request.tags) { + val tag = tagInNewsRepository.findByName(tagName) ?: throw CserealException.Csereal404("해당하는 태그가 없습니다") + NewsTagEntity.createNewsTag(newNews, tag) + } + + newsRepository.save(newNews) + + return NewsDto.of(newNews, null) + } + + @Transactional + override fun updateNews(newsId: Long, request: NewsDto): NewsDto { + val news: NewsEntity = newsRepository.findByIdOrNull(newsId) + ?: throw CserealException.Csereal404("존재하지 않는 새소식입니다. (newsId: $newsId)") + if (news.isDeleted) throw CserealException.Csereal404("삭제된 새소식입니다.") + news.update(request) + + val oldTags = news.newsTags.map { it.tag.name } + + val tagsToRemove = oldTags - request.tags + val tagsToAdd = request.tags - oldTags + + for(tagName in tagsToRemove) { + val tagId = tagInNewsRepository.findByName(tagName)!!.id + news.newsTags.removeIf { it.tag.name == tagName } + newsTagRepository.deleteByNewsIdAndTagId(newsId, tagId) + } + + for (tagName in tagsToAdd) { + val tag = tagInNewsRepository.findByName(tagName) ?: throw CserealException.Csereal404("해당하는 태그가 없습니다") + NewsTagEntity.createNewsTag(news,tag) + } + + return NewsDto.of(news, null) + } + + @Transactional + override fun deleteNews(newsId: Long) { + val news: NewsEntity = newsRepository.findByIdOrNull(newsId) + ?: throw CserealException.Csereal404("존재하지 않는 새소식입니다.(newsId: $newsId") + + news.isDeleted = true + } + + override fun enrollTag(tagName: String) { + val newTag = TagInNewsEntity( + name = tagName + ) + tagInNewsRepository.save(newTag) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt index 82e47c0d..cb7919bf 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/api/NoticeController.kt @@ -14,22 +14,24 @@ class NoticeController( ) { @GetMapping fun searchNotice( - @RequestParam(required = false) tag : List?, + @RequestParam(required = false) tag : List?, @RequestParam(required = false) keyword: String?, @RequestParam(required = false, defaultValue = "0") pageNum: Long - ): ResponseEntity { + ): ResponseEntity { return ResponseEntity.ok(noticeService.searchNotice(tag, keyword, pageNum)) } @GetMapping("/{noticeId}") fun readNotice( @PathVariable noticeId: Long, + @RequestParam(required = false) tag : List?, + @RequestParam(required = false) keyword: String?, ) : ResponseEntity { - return ResponseEntity.ok(noticeService.readNotice(noticeId)) + return ResponseEntity.ok(noticeService.readNotice(noticeId,tag,keyword)) } @PostMapping fun createNotice( - @Valid @RequestBody request: CreateNoticeRequest + @Valid @RequestBody request: NoticeDto ) : ResponseEntity { return ResponseEntity.ok(noticeService.createNotice(request)) } @@ -37,7 +39,7 @@ class NoticeController( @PatchMapping("/{noticeId}") fun updateNotice( @PathVariable noticeId: Long, - @Valid @RequestBody request: UpdateNoticeRequest, + @Valid @RequestBody request: NoticeDto, ) : ResponseEntity { return ResponseEntity.ok(noticeService.updateNotice(noticeId, request)) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt index 2268cb16..2a543517 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.notice.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.notice.dto.NoticeDto import jakarta.persistence.CascadeType import jakarta.persistence.Column import jakarta.persistence.Entity @@ -10,13 +11,10 @@ import jakarta.persistence.OneToMany @Entity(name = "notice") class NoticeEntity( - @Column var isDeleted: Boolean = false, - @Column var title: String, - @Column(columnDefinition = "text") var description: String, // var postType: String, @@ -31,6 +29,12 @@ class NoticeEntity( var noticeTags: MutableSet = mutableSetOf() ): BaseTimeEntity() { - + fun update(updateNoticeRequest: NoticeDto) { + this.title = updateNoticeRequest.title + this.description = updateNoticeRequest.description + this.isPublic = updateNoticeRequest.isPublic + this.isSlide = updateNoticeRequest.isSlide + this.isPinned = updateNoticeRequest.isPinned + } } \ No newline at end of file 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 56661cae..bf3f8471 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 @@ -1,14 +1,12 @@ package com.wafflestudio.csereal.core.notice.database import com.querydsl.core.BooleanBuilder -import com.querydsl.core.types.Projections import com.querydsl.jpa.impl.JPAQueryFactory import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.notice.database.QNoticeEntity.noticeEntity import com.wafflestudio.csereal.core.notice.database.QNoticeTagEntity.noticeTagEntity -import com.wafflestudio.csereal.core.notice.dto.NoticeDto -import com.wafflestudio.csereal.core.notice.dto.SearchDto -import com.wafflestudio.csereal.core.notice.dto.SearchResponse +import com.wafflestudio.csereal.core.notice.dto.NoticeSearchDto +import com.wafflestudio.csereal.core.notice.dto.NoticeSearchResponse import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Component @@ -16,14 +14,15 @@ interface NoticeRepository : JpaRepository, CustomNoticeRepo } interface CustomNoticeRepository { - fun searchNotice(tag: List?, keyword: String?, pageNum: Long): SearchResponse + fun searchNotice(tag: List?, keyword: String?, pageNum: Long): NoticeSearchResponse + fun findPrevNextId(noticeId: Long, tag: List?, keyword: String?): Array? } @Component class NoticeRepositoryImpl( private val queryFactory: JPAQueryFactory, ) : CustomNoticeRepository { - override fun searchNotice(tag: List?, keyword: String?, pageNum: Long): SearchResponse { + override fun searchNotice(tag: List?, keyword: String?, pageNum: Long): NoticeSearchResponse { val keywordBooleanBuilder = BooleanBuilder() val tagsBooleanBuilder = BooleanBuilder() @@ -45,37 +44,91 @@ class NoticeRepositoryImpl( if (!tag.isNullOrEmpty()) { tag.forEach { tagsBooleanBuilder.or( - noticeTagEntity.tag.id.eq(it) + noticeTagEntity.tag.name.eq(it) ) } } - val total = queryFactory.select(noticeEntity) - .from(noticeEntity).leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) + val jpaQuery = queryFactory.select(noticeEntity).from(noticeEntity) + .leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) - .where(keywordBooleanBuilder).where(tagsBooleanBuilder).fetch().size - - val list = queryFactory.select( - Projections.constructor( - SearchDto::class.java, - noticeEntity.id, - noticeEntity.title, - noticeEntity.createdAt, - noticeEntity.isPinned + .where(keywordBooleanBuilder).where(tagsBooleanBuilder) + + val total = jpaQuery.distinct().fetch().size + + val noticeEntityList = jpaQuery.orderBy(noticeEntity.isPinned.desc()) + .orderBy(noticeEntity.createdAt.desc()) + .offset(20*pageNum) //로컬 테스트를 위해 잠시 5로 둘 것, 원래는 20 + .limit(20) + .distinct() + .fetch() + + val noticeSearchDtoList : List = noticeEntityList.map { + NoticeSearchDto( + id = it.id, + title = it.title, + createdAt = it.createdAt, + isPinned = it.isPinned, ) - ).from(noticeEntity) + } + + return NoticeSearchResponse(total, noticeSearchDtoList) + } + + override fun findPrevNextId(noticeId: Long, tag: List?, keyword: String?): Array? { + val keywordBooleanBuilder = BooleanBuilder() + val tagsBooleanBuilder = BooleanBuilder() + + if (!keyword.isNullOrEmpty()) { + val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) + keywordList.forEach { + if(it.length == 1) { + throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") + } else { + keywordBooleanBuilder.and( + noticeEntity.title.contains(it) + .or(noticeEntity.description.contains(it)) + ) + } + + } + } + if(!tag.isNullOrEmpty()) { + tag.forEach { + tagsBooleanBuilder.or( + noticeTagEntity.tag.name.eq(it) + ) + } + } + + val noticeSearchDtoList = queryFactory.select(noticeEntity).from(noticeEntity) .leftJoin(noticeTagEntity).on(noticeTagEntity.notice.eq(noticeEntity)) .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) - .where(keywordBooleanBuilder) - .where(tagsBooleanBuilder) + .where(keywordBooleanBuilder).where(tagsBooleanBuilder) .orderBy(noticeEntity.isPinned.desc()) .orderBy(noticeEntity.createdAt.desc()) - .offset(20*pageNum) - .limit(20) .distinct() .fetch() - return SearchResponse(total, list) + val findingId = noticeSearchDtoList.indexOfFirst {it.id == noticeId} + + val prevNext : Array? + if(findingId == -1) { + prevNext = arrayOf(null, null) + } else if(findingId != 0 && findingId != noticeSearchDtoList.size-1) { + prevNext = arrayOf(noticeSearchDtoList[findingId+1], noticeSearchDtoList[findingId-1]) + } else if(findingId == 0) { + if(noticeSearchDtoList.size == 1) { + prevNext = arrayOf(null, null) + } else { + prevNext = arrayOf(noticeSearchDtoList[1],null) + } + } else { + prevNext = arrayOf(null, noticeSearchDtoList[noticeSearchDtoList.size-2]) + } + + return prevNext + } } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt index c910dabd..34ba5df3 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagEntity.kt @@ -11,12 +11,12 @@ class NoticeTagEntity( @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "tag_id") - var tag: TagEntity, + var tag: TagInNoticeEntity, ) : BaseTimeEntity() { companion object { - fun createNoticeTag(notice: NoticeEntity, tag: TagEntity) { + fun createNoticeTag(notice: NoticeEntity, tag: TagInNoticeEntity) { val noticeTag = NoticeTagEntity(notice, tag) notice.noticeTags.add(noticeTag) tag.noticeTags.add(noticeTag) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt index 39a6ce98..fc5e3f6b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeTagRepository.kt @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository interface NoticeTagRepository : JpaRepository { fun deleteAllByNoticeId(noticeId: Long) + fun deleteByNoticeIdAndTagId(noticeId: Long, tagId: Long) } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeEntity.kt similarity index 81% rename from src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt rename to src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeEntity.kt index 67c108b9..1eb44da7 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeEntity.kt @@ -1,12 +1,11 @@ package com.wafflestudio.csereal.core.notice.database import com.wafflestudio.csereal.common.config.BaseTimeEntity -import jakarta.persistence.CascadeType import jakarta.persistence.Entity import jakarta.persistence.OneToMany -@Entity(name = "tag") -class TagEntity( +@Entity(name = "tag_in_notice") +class TagInNoticeEntity( var name: String, @OneToMany(mappedBy = "tag") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeRepository.kt new file mode 100644 index 00000000..d576b129 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagInNoticeRepository.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.notice.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface TagInNoticeRepository : JpaRepository { + fun findByName(tagName: String): TagInNoticeEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt deleted file mode 100644 index 34a50416..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.wafflestudio.csereal.core.notice.database - -import org.springframework.data.jpa.repository.JpaRepository - -interface TagRepository : JpaRepository { -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt deleted file mode 100644 index 0b0fda52..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.wafflestudio.csereal.core.notice.dto - -import jakarta.validation.constraints.NotBlank - -data class CreateNoticeRequest( - @field:NotBlank(message = "제목은 비어있을 수 없습니다") - val title: String, - - @field:NotBlank(message = "내용은 비어있을 수 없습니다") - val description: String, - - val tags: List = emptyList(), - - val isPublic: Boolean, - - val isSlide: Boolean, - - val isPinned: Boolean, -) { -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt index 7aeccae8..baf6e9fc 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeDto.kt @@ -5,33 +5,37 @@ import java.time.LocalDateTime data class NoticeDto( val id: Long, - val isDeleted: Boolean, val title: String, val description: String, - // val postType: String, // val authorId: Int, - val tags: List, + val tags: List, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, val isPublic: Boolean, val isSlide: Boolean, val isPinned: Boolean, + val prevId: Long?, + val prevTitle: String?, + val nextId: Long?, + val nextTitle: String? ) { companion object { - fun of(entity: NoticeEntity): NoticeDto = entity.run { + fun of(entity: NoticeEntity, prevNext: Array?): NoticeDto = entity.run { NoticeDto( id = this.id, - isDeleted = false, title = this.title, description = this.description, - // postType = this.postType, - tags = this.noticeTags.map { it.tag.id }, + tags = this.noticeTags.map { it.tag.name }, createdAt = this.createdAt, modifiedAt = this.modifiedAt, isPublic = this.isPublic, isSlide = this.isSlide, isPinned = this.isPinned, + prevId = prevNext?.get(0)?.id, + prevTitle = prevNext?.get(0)?.title, + nextId = prevNext?.get(1)?.id, + nextTitle = prevNext?.get(1)?.title ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeSearchDto.kt similarity index 63% rename from src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt rename to src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeSearchDto.kt index b05e5c11..89898ebb 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeSearchDto.kt @@ -3,10 +3,10 @@ package com.wafflestudio.csereal.core.notice.dto import com.querydsl.core.annotations.QueryProjection import java.time.LocalDateTime -data class SearchDto @QueryProjection constructor( - val noticeId: Long, +data class NoticeSearchDto @QueryProjection constructor( + val id: Long, val title: String, - val createdDate: LocalDateTime, + val createdAt: LocalDateTime?, val isPinned: Boolean, ) { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeSearchResponse.kt similarity index 50% rename from src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt rename to src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeSearchResponse.kt index 105a25cf..c11e21b4 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/NoticeSearchResponse.kt @@ -1,8 +1,8 @@ package com.wafflestudio.csereal.core.notice.dto -data class SearchResponse( +data class NoticeSearchResponse( val total: Int, - val searchList: List + val searchList: List ) { } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt deleted file mode 100644 index e7fd1fa9..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.wafflestudio.csereal.core.notice.dto - -data class UpdateNoticeRequest( - val title: String?, - val description: String?, - val tags: List?, - val isPublic: Boolean?, - val isSlide: Boolean?, - val isPinned: Boolean?, -) { -} \ No newline at end of file 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 36956733..8bae04ae 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 @@ -8,10 +8,10 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface NoticeService { - fun searchNotice(tag: List?, keyword: String?, pageNum: Long): SearchResponse - fun readNotice(noticeId: Long): NoticeDto - fun createNotice(request: CreateNoticeRequest): NoticeDto - fun updateNotice(noticeId: Long, request: UpdateNoticeRequest): NoticeDto + fun searchNotice(tag: List?, keyword: String?, pageNum: Long): NoticeSearchResponse + fun readNotice(noticeId: Long, tag: List?, keyword: String?): NoticeDto + fun createNotice(request: NoticeDto): NoticeDto + fun updateNotice(noticeId: Long, request: NoticeDto): NoticeDto fun deleteNotice(noticeId: Long) fun enrollTag(tagName: String) } @@ -19,30 +19,37 @@ interface NoticeService { @Service class NoticeServiceImpl( private val noticeRepository: NoticeRepository, - private val tagRepository: TagRepository, + private val tagInNoticeRepository: TagInNoticeRepository, private val noticeTagRepository: NoticeTagRepository ) : NoticeService { @Transactional(readOnly = true) override fun searchNotice( - tag: List?, + tag: List?, keyword: String?, pageNum: Long - ): SearchResponse { + ): NoticeSearchResponse { return noticeRepository.searchNotice(tag, keyword, pageNum) } @Transactional(readOnly = true) - override fun readNotice(noticeId: Long): NoticeDto { + override fun readNotice( + noticeId: Long, + tag: List?, + keyword: String? + ): NoticeDto { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) - ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + ?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") - if (notice.isDeleted) throw CserealException.Csereal400("삭제된 공지사항입니다.(noticeId: $noticeId)") - return NoticeDto.of(notice) + if (notice.isDeleted) throw CserealException.Csereal404("삭제된 공지사항입니다.(noticeId: $noticeId)") + + val prevNext = noticeRepository.findPrevNextId(noticeId, tag, keyword) + + return NoticeDto.of(notice, prevNext) } @Transactional - override fun createNotice(request: CreateNoticeRequest): NoticeDto { + override fun createNotice(request: NoticeDto): NoticeDto { val newNotice = NoticeEntity( title = request.title, description = request.description, @@ -51,44 +58,42 @@ class NoticeServiceImpl( isPinned = request.isPinned, ) - for (tagId in request.tags) { - val tag = tagRepository.findByIdOrNull(tagId) ?: throw CserealException.Csereal400("해당하는 태그가 없습니다") + for (tagName in request.tags) { + val tag = tagInNoticeRepository.findByName(tagName) ?: throw CserealException.Csereal404("해당하는 태그가 없습니다") NoticeTagEntity.createNoticeTag(newNotice, tag) } noticeRepository.save(newNotice) - return NoticeDto.of(newNotice) + return NoticeDto.of(newNotice, null) } @Transactional - override fun updateNotice(noticeId: Long, request: UpdateNoticeRequest): NoticeDto { + override fun updateNotice(noticeId: Long, request: NoticeDto): NoticeDto { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) - ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId") - if (notice.isDeleted) throw CserealException.Csereal400("삭제된 공지사항입니다.(noticeId: $noticeId") - - notice.title = request.title ?: notice.title - notice.description = request.description ?: notice.description - notice.isPublic = request.isPublic ?: notice.isPublic - notice.isSlide = request.isSlide ?: notice.isSlide - notice.isPinned = request.isPinned ?: notice.isPinned - - if (request.tags != null) { - noticeTagRepository.deleteAllByNoticeId(noticeId) - - // 원래 태그에서 겹치는 태그만 남기고, 나머지는 없애기 - notice.noticeTags = notice.noticeTags.filter { request.tags.contains(it.tag.id) }.toMutableSet() - for (tagId in request.tags) { - // 겹치는 거 말고, 새로운 태그만 추가 - if(!notice.noticeTags.map { it.tag.id }.contains(tagId)) { - val tag = tagRepository.findByIdOrNull(tagId) ?: throw CserealException.Csereal400("해당하는 태그가 없습니다") - NoticeTagEntity.createNoticeTag(notice, tag) - } - } + ?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + if (notice.isDeleted) throw CserealException.Csereal404("삭제된 공지사항입니다.(noticeId: $noticeId)") + + notice.update(request) + + val oldTags = notice.noticeTags.map { it.tag.name } + + val tagsToRemove = oldTags - request.tags + val tagsToAdd = request.tags - oldTags + + for(tagName in tagsToRemove) { + val tagId = tagInNoticeRepository.findByName(tagName)!!.id + notice.noticeTags.removeIf { it.tag.name == tagName } + noticeTagRepository.deleteByNoticeIdAndTagId(noticeId, tagId) + } + + for(tagName in tagsToAdd) { + val tag = tagInNoticeRepository.findByName(tagName) ?: throw CserealException.Csereal404("해당하는 태그가 없습니다") + NoticeTagEntity.createNoticeTag(notice, tag) } - return NoticeDto.of(notice) + return NoticeDto.of(notice, null) @@ -98,17 +103,17 @@ class NoticeServiceImpl( @Transactional override fun deleteNotice(noticeId: Long) { val notice: NoticeEntity = noticeRepository.findByIdOrNull(noticeId) - ?: throw CserealException.Csereal400("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") + ?: throw CserealException.Csereal404("존재하지 않는 공지사항입니다.(noticeId: $noticeId)") notice.isDeleted = true } override fun enrollTag(tagName: String) { - val newTag = TagEntity( + val newTag = TagInNoticeEntity( name = tagName ) - tagRepository.save(newTag) + tagInNoticeRepository.save(newTag) } //TODO: 이미지 등록, 글쓴이 함께 조회 From 7e3ea8e118a334e94de7516a9b277e2d6482159d Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Thu, 10 Aug 2023 00:37:14 +0900 Subject: [PATCH 13/27] =?UTF-8?q?fix:=20main=EC=97=90=EC=84=9C=20develop?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20pr=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: merge develop to main (#13) * feat: 공지사항 생성, 공지사항 읽기 기능 추가 (#1) * :sparkles: 패키지 및 엔티티 생성 * :sparkles: BaseTimeEntity 생성, PostEntity 기본 내용 작성 * :sparkles: PostController 생성 및 기본 내용 작성 * :sparkles: postService 생성 * :sparkles: Exceptions.kt 생성 * :sparkles: postDto 생성 * :sparkles: postRepository 생성 및 기본 내용 작성 * :green_heart: application.yaml 로컬 환경에서 작동하도록 설정 * feat: createPost 기능 생성 * refactor: 리뷰 주신 거 수정 * refactor: post -> notice 수정 등 * chore: .idea 디렉토리 삭제 * chore: PR 템플릿 생성 (#2) * feat: 로컬 db용 docker-compose 파일 추가 및 application.yaml 수정 (#4) * feat: 공지사항 수정, 삭제, 태그 기능 추가 (#3) * fix: ExceptionHandler 추가 * feat: updateNotice 추가, valid 추가 * feat: deleteNotice 추가 * feat: enrollTag 기능 추가, noticeTag 연관 엔티티 추가 * feat: 공지사항 작성할 때 태그 생성 및 수정 * fix: 로컬 db 없앰 * fix: pr 리뷰 수정 * fix: pr 리뷰 수정 * fix: noticeTag assign * feat: 구성원(교수) 생성 및 조회 API 구현 (#8) * feat: 교수 엔티티 및 DTO 설계 * feat: 교수 생성 및 조회 * Docs: Swagger 추가 (#7) * Docs: Add swagger dependency * Docs: Add basic config for swagger * Docs: Add basic configuration for swagger. * feat: 페이지네이션+검색 기능 추가 (#5) * feat: isPublic, isSlide, isPinned 추가 * feat: queryDsl 적용 위해 gradle 추가 fix: javax -> jakarta 변경 * feat: queryDsl 도입 * feat: 키워드+태그 검색 추가 * feat: search query에 isDeleted, isPublic 추가 및 isPinned 우선순위 설정 * fix: requestBody -> requestParam 수정 * feat: 페이지네이션 추가 * fix: 키워드 booleanBuilder 추가, application.yaml 수정 * fix: searchNotice readOnly 추가 * fix: SearchRequest 삭제 * fix: NoticeDto tags 추가 * fix: pr 리뷰 수정 / feat: 검색 기능 보강 및 수정 * fix:코드 수정 * fix: SearchResponse isPinned 추가 * fix: SearchResponse에 total 추가 * fix: 페이지 개수 수정 * fix: searchNotice queryDsl 오류 수정 * fix: local 설정 변경 * CICD: 배포 자동화 (#6) * CICD: Change expose port and added image tag * CICD: Change ddl-auto to create in prod profile for test * CICD: Added Deploy github action * CICD: Merge jobs to one job * Fix: Change checkout order to first step * CICD: Add context for docker build action * Fix: Change spring profile arg position * CICD: Change openjdk version to 17 * CICD: Change docker compose build image tag to latest * CICD: Change to use ghcr repository. * Fix: change list to string in docker push tags. * Fix: Change registry to ghcr.io * Fix: change env to pass to github action instead of ssh export command * Fix: unwrap bracket. * Fix: wrap Profile with "" * CICD: Add .env file * CICD: Change prod ddl-auto to create (for developing), and add TODO comment. * CICD: Remove cicd/deploy branch for condition. * feat: 구성원(교수) 수정 및 삭제 API (#9) * feat: 교수 조회시 최종학력이 앞으로 오게끔 정렬 * feat: 교수 수정 및 삭제 API * feat: 학력과 경력 엔티티 필드 변경, 수정 API 구현 * feat: 구성원(행정직원) CRUD API (#10) * feat: 행정직원 엔티티 및 DTO 설계 * feat: 행정직원 CRUD * feat: 교수 조회시 이름순 정렬 * fix: 교수 연구 분야 Set -> List 로 변경 * feat: 행정직원 주요업무 업데이트 구현 * feat: news 패키지 추가, 디벨롭 및 프론트에 맞게 엔티티 변경 (#12) * feat: news 패키지 생성 * feat: readNews 생성, news 패키지 추가로 인한 명칭 변경 * feat: createNews, enrollTag(새소식) 추가, news 패키지로 인한 명칭 추가 변경 * feat: updateNews, deleteNews 추가 * fix: searchNotice 관련 명칭 변경 * feat: searchNews 추가 * fix: develop 브랜치 반영, 프론트 요구사항 반영 * feat: readNotice, readNews에 이전글 다음글 추가 * 태그 업데이트 코드 리팩터링중 * refactor: 코드 수정, 이전제목 추가 * fix: 게시글 하나일때 read 가능 * fix: prevNext null 없애기 * fix: 이전글 다음글 null 수정 --------- Co-authored-by: Jo Seonggyu Co-authored-by: 우혁준 (HyukJoon Woo) * hotfix: 사용하지않는 Dto 및 엔티티 삭제 (#14) --------- Co-authored-by: Junhyeong Kim Co-authored-by: 우혁준 (HyukJoon Woo) --- .../core/member/database/CareerEntity.kt | 1 - .../core/member/database/EducationEntity.kt | 1 - .../csereal/core/member/dto/CareerDto.kt | 19 ---------------- .../csereal/core/member/dto/EducationDto.kt | 22 ------------------- .../csereal/core/notice/database/TagEntity.kt | 15 ------------- .../core/notice/database/TagRepository.kt | 6 ----- 6 files changed, 64 deletions(-) delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt index c394d9b8..a6a40da0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/CareerEntity.kt @@ -1,7 +1,6 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity -import com.wafflestudio.csereal.core.member.dto.CareerDto import jakarta.persistence.Entity import jakarta.persistence.FetchType import jakarta.persistence.JoinColumn diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt index a52589d0..ff4559ce 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/EducationEntity.kt @@ -1,7 +1,6 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity -import com.wafflestudio.csereal.core.member.dto.EducationDto import jakarta.persistence.* @Entity(name = "education") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt deleted file mode 100644 index ff14cfb2..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/CareerDto.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.wafflestudio.csereal.core.member.dto - -import com.wafflestudio.csereal.core.member.database.CareerEntity - -data class CareerDto( - val duration: String, - val name: String, - val workplace: String -) { - companion object { - fun of(careerEntity: CareerEntity): CareerDto { - return CareerDto( - duration = careerEntity.duration, - name = careerEntity.name, - workplace = careerEntity.workplace - ) - } - } -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt deleted file mode 100644 index 48e2c968..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/EducationDto.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.wafflestudio.csereal.core.member.dto - -import com.wafflestudio.csereal.core.member.database.Degree -import com.wafflestudio.csereal.core.member.database.EducationEntity - -data class EducationDto( - val university: String, - val major: String, - val degree: Degree, - val year: Int -) { - companion object { - fun of(educationEntity: EducationEntity): EducationDto { - return EducationDto( - university = educationEntity.university, - major = educationEntity.major, - degree = educationEntity.degree, - year = educationEntity.year - ) - } - } -} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt deleted file mode 100644 index 67c108b9..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagEntity.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.wafflestudio.csereal.core.notice.database - -import com.wafflestudio.csereal.common.config.BaseTimeEntity -import jakarta.persistence.CascadeType -import jakarta.persistence.Entity -import jakarta.persistence.OneToMany - -@Entity(name = "tag") -class TagEntity( - var name: String, - - @OneToMany(mappedBy = "tag") - val noticeTags: MutableSet = mutableSetOf() -) : BaseTimeEntity() { -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt deleted file mode 100644 index 34a50416..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/TagRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.wafflestudio.csereal.core.notice.database - -import org.springframework.data.jpa.repository.JpaRepository - -interface TagRepository : JpaRepository { -} \ No newline at end of file From 6c5afe298e92b2cc937396ed4d7196547ee4ff84 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Fri, 11 Aug 2023 20:31:30 +0900 Subject: [PATCH 14/27] =?UTF-8?q?feat:=20seminar=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: createSeminar, readSeminar, updateSeminar 추가 * feat: deleteSeminar, searchSeminar 추가 * fix: distinct 삭제 --- .../core/seminar/api/SeminarController.kt | 51 ++++++++ .../core/seminar/database/SeminarEntity.kt | 96 ++++++++++++++ .../seminar/database/SeminarRepository.kt | 122 ++++++++++++++++++ .../csereal/core/seminar/dto/SeminarDto.kt | 64 +++++++++ .../core/seminar/dto/SeminarSearchDto.kt | 14 ++ .../core/seminar/dto/SeminarSearchResponse.kt | 7 + .../core/seminar/service/SeminarService.kt | 67 ++++++++++ 7 files changed, 421 insertions(+) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchResponse.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/seminar/service/SeminarService.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt new file mode 100644 index 00000000..8b2699d7 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt @@ -0,0 +1,51 @@ +package com.wafflestudio.csereal.core.seminar.api + +import com.wafflestudio.csereal.core.seminar.dto.SeminarDto +import com.wafflestudio.csereal.core.seminar.dto.SeminarSearchResponse +import com.wafflestudio.csereal.core.seminar.service.SeminarService +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RequestMapping("/seminar") +@RestController +class SeminarController ( + private val seminarService: SeminarService, +) { + @GetMapping + fun searchSeminar( + @RequestParam(required = false) keyword: String?, + @RequestParam(required = false, defaultValue = "0") pageNum: Long + ) : ResponseEntity { + return ResponseEntity.ok(seminarService.searchSeminar(keyword, pageNum)) + } + @PostMapping + fun createSeminar( + @Valid @RequestBody request: SeminarDto + ) : ResponseEntity { + return ResponseEntity.ok(seminarService.createSeminar(request)) + } + + @GetMapping("/{seminarId}") + fun readSeminar( + @PathVariable seminarId: Long, + @RequestParam(required = false) keyword: String?, + ) : ResponseEntity { + return ResponseEntity.ok(seminarService.readSeminar(seminarId, keyword)) + } + + @PatchMapping("/{seminarId}") + fun updateSeminar( + @PathVariable seminarId: Long, + @Valid @RequestBody request: SeminarDto, + ) : ResponseEntity { + return ResponseEntity.ok(seminarService.updateSeminar(seminarId, request)) + } + + @DeleteMapping("/{seminarId}") + fun deleteSeminar( + @PathVariable seminarId: Long + ) { + seminarService.deleteSeminar(seminarId) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt new file mode 100644 index 00000000..bc098643 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt @@ -0,0 +1,96 @@ +package com.wafflestudio.csereal.core.seminar.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.seminar.dto.SeminarDto +import jakarta.persistence.Column +import jakarta.persistence.Entity + +@Entity(name = "seminar") +class SeminarEntity( + + var isDeleted: Boolean = false, + + var title: String, + + @Column(columnDefinition = "text") + var description: String, + + @Column(columnDefinition = "text") + var introduction: String, + + var category: String, + + // 연사 정보 + var name: String, + var speakerUrl: String?, + var speakerTitle: String?, + var affiliation: String, + var affiliationUrl: String?, + + var startDate: String?, + var startTime: String?, + var endDate: String?, + var endTime: String?, + + var location: String, + + var host: String?, + + // var profileImage: File, + + // var seminarFile: File, + + var isPublic: Boolean, + + var isSlide: Boolean, + + var additionalNote: String? +): BaseTimeEntity() { + + companion object { + fun of(seminarDto: SeminarDto): SeminarEntity { + return SeminarEntity( + title = seminarDto.title, + description = seminarDto.description, + introduction = seminarDto.introduction, + category = seminarDto.category, + name = seminarDto.name, + speakerUrl = seminarDto.speakerUrl, + speakerTitle = seminarDto.speakerTitle, + affiliation = seminarDto.affiliation, + affiliationUrl = seminarDto.affiliationUrl, + startDate = seminarDto.startDate, + startTime = seminarDto.startTime, + endDate = seminarDto.endDate, + endTime = seminarDto.endTime, + location = seminarDto.location, + host = seminarDto.host, + additionalNote = seminarDto.additionalNote, + isPublic = seminarDto.isPublic, + isSlide = seminarDto.isSlide + ) + } + + } + + fun update(updateSeminarRequest: SeminarDto) { + title = updateSeminarRequest.title + description = updateSeminarRequest.description + introduction = updateSeminarRequest.introduction + category = updateSeminarRequest.category + name = updateSeminarRequest.name + speakerUrl = updateSeminarRequest.speakerUrl + speakerTitle = updateSeminarRequest.speakerTitle + affiliation = updateSeminarRequest.affiliation + affiliationUrl = updateSeminarRequest.affiliationUrl + startDate = updateSeminarRequest.startDate + startTime = updateSeminarRequest.startTime + endDate = updateSeminarRequest.endDate + endTime = updateSeminarRequest.endTime + location = updateSeminarRequest.location + host = updateSeminarRequest.host + additionalNote = updateSeminarRequest.additionalNote + isPublic = updateSeminarRequest.isPublic + isSlide = updateSeminarRequest.isSlide + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..e3df3a59 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarRepository.kt @@ -0,0 +1,122 @@ +package com.wafflestudio.csereal.core.seminar.database + +import com.querydsl.core.BooleanBuilder +import com.querydsl.jpa.impl.JPAQueryFactory +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.seminar.database.QSeminarEntity.seminarEntity +import com.wafflestudio.csereal.core.seminar.dto.SeminarSearchDto +import com.wafflestudio.csereal.core.seminar.dto.SeminarSearchResponse +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Component + +interface SeminarRepository : JpaRepository, CustomSeminarRepository { +} + +interface CustomSeminarRepository { + fun searchSeminar(keyword: String?, pageNum: Long): SeminarSearchResponse + fun findPrevNextId(seminarId: Long, keyword: String?): Array? +} + +@Component +class SeminarRepositoryImpl( + private val queryFactory: JPAQueryFactory, +) : CustomSeminarRepository { + override fun searchSeminar(keyword: String?, pageNum: Long): SeminarSearchResponse { + val keywordBooleanBuilder = BooleanBuilder() + + if (!keyword.isNullOrEmpty()) { + val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) + keywordList.forEach { + if (it.length == 1) { + throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") + } else { + keywordBooleanBuilder.and( + seminarEntity.title.contains(it) + .or(seminarEntity.name.contains(it)) + .or(seminarEntity.affiliation.contains(it)) + .or(seminarEntity.location.contains(it)) + ) + } + } + } + + val jpaQuery = queryFactory.select(seminarEntity).from(seminarEntity) + .where(seminarEntity.isDeleted.eq(false)) + .where(keywordBooleanBuilder) + + val total = jpaQuery.fetch().size + + val seminarEntityList = jpaQuery.orderBy(seminarEntity.createdAt.desc()) + .offset(10*pageNum) + .limit(20) + .fetch() + + val seminarSearchDtoList : MutableList = mutableListOf() + + for(i: Int in 0 until seminarEntityList.size) { + var isYearLast = false + if(i == seminarEntityList.size-1) { + isYearLast = true + } else if(seminarEntityList[i].startDate?.substring(0,4) != seminarEntityList[i+1].startDate?.substring(0,4)) { + isYearLast = true + } + + seminarSearchDtoList.add( + SeminarSearchDto( + id = seminarEntityList[i].id, + title = seminarEntityList[i].title, + startDate = seminarEntityList[i].startDate, + isYearLast = isYearLast, + name = seminarEntityList[i].name, + affiliation = seminarEntityList[i].affiliation, + location = seminarEntityList[i].location + ) + ) + } + + return SeminarSearchResponse(total, seminarSearchDtoList) + } + override fun findPrevNextId(seminarId: Long, keyword: String?): Array? { + val keywordBooleanBuilder = BooleanBuilder() + + if (!keyword.isNullOrEmpty()) { + val keywordList = keyword.split("[^a-zA-Z0-9가-힣]".toRegex()) + keywordList.forEach { + if (it.length == 1) { + throw CserealException.Csereal400("각각의 키워드는 한글자 이상이어야 합니다.") + } else { + keywordBooleanBuilder.and( + seminarEntity.title.contains(it) + .or(seminarEntity.description.contains(it)) + ) + } + } + } + val seminarSearchDtoList = queryFactory.select(seminarEntity).from(seminarEntity) + .where(seminarEntity.isDeleted.eq(false), seminarEntity.isPublic.eq(true)) + .where(keywordBooleanBuilder) + .orderBy(seminarEntity.createdAt.desc()) + .fetch() + + val findingId = seminarSearchDtoList.indexOfFirst { it.id == seminarId } + + val prevNext: Array? + + if (findingId == -1) { + return null + } else if (findingId != 0 && findingId != seminarSearchDtoList.size - 1) { + prevNext = arrayOf(seminarSearchDtoList[findingId + 1], seminarSearchDtoList[findingId - 1]) + } else if (findingId == 0) { + if (seminarSearchDtoList.size == 1) { + prevNext = arrayOf(null, null) + } else { + prevNext = arrayOf(seminarSearchDtoList[1], null) + } + } else { + prevNext = arrayOf(null, seminarSearchDtoList[seminarSearchDtoList.size - 2]) + } + + return prevNext + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt new file mode 100644 index 00000000..b2563385 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt @@ -0,0 +1,64 @@ +package com.wafflestudio.csereal.core.seminar.dto + +import com.wafflestudio.csereal.core.seminar.database.SeminarEntity + +data class SeminarDto( + val id: Long, + val title: String, + val description: String, + val introduction: String, + val category: String, + val name: String, + val speakerUrl: String?, + val speakerTitle: String?, + val affiliation: String, + val affiliationUrl: String?, + val startDate: String?, + val startTime: String?, + val endDate: String?, + val endTime: String?, + val location: String, + val host: String?, + // val profileImage: File, + // val seminarFile: File, + val additionalNote: String?, + val isPublic: Boolean, + val isSlide: Boolean, + val prevId: Long?, + val prevTitle: String?, + val nextId: Long?, + val nextTitle: String? +) { + + companion object { + fun of(entity: SeminarEntity, prevNext: Array?): SeminarDto = entity.run { + SeminarDto( + id = this.id, + title = this.title, + description = this.description, + introduction = this.introduction, + category = this.category, + name = this.name, + speakerUrl = this.speakerUrl, + speakerTitle = this.speakerTitle, + affiliation = this.affiliation, + affiliationUrl = this.affiliationUrl, + startDate = this.startDate, + startTime = this.startTime, + endDate = this.endDate, + endTime = this.endTime, + location = this.location, + host = this.host, + additionalNote = this.additionalNote, + isPublic = this.isPublic, + isSlide = this.isSlide, + prevId = prevNext?.get(0)?.id, + prevTitle = prevNext?.get(0)?.title, + nextId = prevNext?.get(1)?.id, + nextTitle = prevNext?.get(1)?.title + ) + } + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchDto.kt new file mode 100644 index 00000000..69ada786 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchDto.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.csereal.core.seminar.dto + +import com.querydsl.core.annotations.QueryProjection + +data class SeminarSearchDto @QueryProjection constructor( + val id: Long, + val title: String, + val startDate: String?, + val isYearLast: Boolean, + val name: String, + val affiliation: String?, + val location: String +) { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchResponse.kt new file mode 100644 index 00000000..b591ac7c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarSearchResponse.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.seminar.dto + +data class SeminarSearchResponse( + val total: Int, + val searchList: List +) { +} \ No newline at end of file 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 new file mode 100644 index 00000000..cf6a5cba --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/service/SeminarService.kt @@ -0,0 +1,67 @@ +package com.wafflestudio.csereal.core.seminar.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.seminar.database.SeminarEntity +import com.wafflestudio.csereal.core.seminar.database.SeminarRepository +import com.wafflestudio.csereal.core.seminar.dto.SeminarDto +import com.wafflestudio.csereal.core.seminar.dto.SeminarSearchResponse +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface SeminarService { + fun searchSeminar(keyword: String?, pageNum: Long): SeminarSearchResponse + fun createSeminar(request: SeminarDto): SeminarDto + fun readSeminar(seminarId: Long, keyword: String?): SeminarDto + fun updateSeminar(seminarId: Long, request: SeminarDto): SeminarDto + fun deleteSeminar(seminarId: Long) +} + +@Service +class SeminarServiceImpl( + private val seminarRepository: SeminarRepository +) : SeminarService { + @Transactional(readOnly = true) + override fun searchSeminar(keyword: String?, pageNum: Long): SeminarSearchResponse { + return seminarRepository.searchSeminar(keyword, pageNum) + } + + @Transactional + override fun createSeminar(request: SeminarDto): SeminarDto { + val newSeminar = SeminarEntity.of(request) + + seminarRepository.save(newSeminar) + + return SeminarDto.of(newSeminar, null) + } + + @Transactional(readOnly = true) + override fun readSeminar(seminarId: Long, keyword: String?): SeminarDto { + val seminar: SeminarEntity = seminarRepository.findByIdOrNull(seminarId) + ?: throw CserealException.Csereal404("존재하지 않는 세미나입니다.(seminarId: $seminarId)") + + if (seminar.isDeleted) throw CserealException.Csereal400("삭제된 세미나입니다. (seminarId: $seminarId)") + + val prevNext = seminarRepository.findPrevNextId(seminarId, keyword) + + return SeminarDto.of(seminar, prevNext) + } + + @Transactional + override fun updateSeminar(seminarId: Long, request: SeminarDto): SeminarDto { + val seminar: SeminarEntity = seminarRepository.findByIdOrNull(seminarId) + ?: throw CserealException.Csereal404("존재하지 않는 세미나입니다") + if(seminar.isDeleted) throw CserealException.Csereal404("삭제된 세미나입니다. (seminarId: $seminarId)") + + seminar.update(request) + + return SeminarDto.of(seminar, null) + } + @Transactional + override fun deleteSeminar(seminarId: Long) { + val seminar: SeminarEntity = seminarRepository.findByIdOrNull(seminarId) + ?: throw CserealException.Csereal404("존재하지 않는 세미나입니다.(seminarId=$seminarId") + + seminar.isDeleted = true + } +} \ No newline at end of file From a550011fff0b99561c0a3cceca2f5be29c5e0e74 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Sat, 12 Aug 2023 00:28:47 +0900 Subject: [PATCH 15/27] =?UTF-8?q?hotfix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20dto=20=EC=82=AD=EC=A0=9C=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * hotfix: 불필요한 dto 삭제 * build.gradle 수정 --- .../core/notice/dto/CreateNoticeRequest.kt | 20 ------------------- .../csereal/core/notice/dto/SearchDto.kt | 13 ------------ .../csereal/core/notice/dto/SearchResponse.kt | 8 -------- .../core/notice/dto/UpdateNoticeRequest.kt | 11 ---------- 4 files changed, 52 deletions(-) delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt deleted file mode 100644 index 0b0fda52..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/CreateNoticeRequest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.wafflestudio.csereal.core.notice.dto - -import jakarta.validation.constraints.NotBlank - -data class CreateNoticeRequest( - @field:NotBlank(message = "제목은 비어있을 수 없습니다") - val title: String, - - @field:NotBlank(message = "내용은 비어있을 수 없습니다") - val description: String, - - val tags: List = emptyList(), - - val isPublic: Boolean, - - val isSlide: Boolean, - - val isPinned: Boolean, -) { -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt deleted file mode 100644 index b05e5c11..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchDto.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.wafflestudio.csereal.core.notice.dto - -import com.querydsl.core.annotations.QueryProjection -import java.time.LocalDateTime - -data class SearchDto @QueryProjection constructor( - val noticeId: Long, - val title: String, - val createdDate: LocalDateTime, - val isPinned: Boolean, -) { - -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt deleted file mode 100644 index 105a25cf..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/SearchResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.wafflestudio.csereal.core.notice.dto - -data class SearchResponse( - val total: Int, - val searchList: List -) { - -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt deleted file mode 100644 index e7fd1fa9..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/dto/UpdateNoticeRequest.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.wafflestudio.csereal.core.notice.dto - -data class UpdateNoticeRequest( - val title: String?, - val description: String?, - val tags: List?, - val isPublic: Boolean?, - val isSlide: Boolean?, - val isPinned: Boolean?, -) { -} \ No newline at end of file From e2b3decfcf4c9345347bd89027b489a8a0d97ec1 Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Mon, 14 Aug 2023 13:17:27 +0900 Subject: [PATCH 16/27] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20uri?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 이미지 uri 필드 추가 및 isActive 대신 status 추가 * fix: 행정직원 전체 조회 응답에서 task 삭제 * fix: 교수진 페이지 응답 수정 --- .../core/member/api/ProfessorController.kt | 3 +- .../core/member/api/StaffController.kt | 3 +- .../core/member/database/ProfessorEntity.kt | 23 +++++++-------- .../member/database/ProfessorRepository.kt | 4 +-- .../core/member/database/StaffEntity.kt | 5 ++-- .../csereal/core/member/dto/ProfessorDto.kt | 14 +++++++--- .../core/member/dto/ProfessorPageDto.kt | 6 ++++ .../core/member/dto/SimpleProfessorDto.kt | 5 ++-- .../csereal/core/member/dto/SimpleStaffDto.kt | 28 +++++++++++++++++++ .../csereal/core/member/dto/StaffDto.kt | 7 +++-- .../core/member/service/ProfessorService.kt | 18 +++++++++--- .../core/member/service/StaffService.kt | 7 +++-- 12 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorPageDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt 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 993ddba8..6caff3a0 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,6 +1,7 @@ package com.wafflestudio.csereal.core.member.api 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.http.ResponseEntity @@ -23,7 +24,7 @@ class ProfessorController( } @GetMapping("/active") - fun getActiveProfessors(): ResponseEntity> { + fun getActiveProfessors(): ResponseEntity { return ResponseEntity.ok(professorService.getActiveProfessors()) } 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 f10aae6f..391a3e58 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,5 +1,6 @@ package com.wafflestudio.csereal.core.member.api +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.http.ResponseEntity @@ -29,7 +30,7 @@ class StaffController( } @GetMapping - fun getAllStaff(): ResponseEntity> { + fun getAllStaff(): ResponseEntity> { return ResponseEntity.ok(staffService.getAllStaff()) } 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 4f18b176..10b06484 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 @@ -3,12 +3,7 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.research.database.LabEntity -import jakarta.persistence.CascadeType -import jakarta.persistence.Entity -import jakarta.persistence.FetchType -import jakarta.persistence.JoinColumn -import jakarta.persistence.ManyToOne -import jakarta.persistence.OneToMany +import jakarta.persistence.* import java.time.LocalDate @Entity(name = "professor") @@ -16,8 +11,9 @@ class ProfessorEntity( var name: String, - //val profileImage:File - var isActive: Boolean, + @Enumerated(EnumType.STRING) + var status: ProfessorStatus, + var academicRank: String, @ManyToOne(fetch = FetchType.LAZY) @@ -43,13 +39,14 @@ class ProfessorEntity( @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) val careers: MutableList = mutableListOf(), - ) : BaseTimeEntity() { + var imageUri: String? = null +) : BaseTimeEntity() { companion object { fun of(professorDto: ProfessorDto): ProfessorEntity { return ProfessorEntity( name = professorDto.name, - isActive = professorDto.isActive, + status = professorDto.status, academicRank = professorDto.academicRank, startDate = professorDto.startDate, endDate = professorDto.endDate, @@ -70,7 +67,7 @@ class ProfessorEntity( fun update(updateProfessorRequest: ProfessorDto) { this.name = updateProfessorRequest.name - this.isActive = updateProfessorRequest.isActive + this.status = updateProfessorRequest.status this.academicRank = updateProfessorRequest.academicRank this.startDate = updateProfessorRequest.startDate this.endDate = updateProfessorRequest.endDate @@ -81,3 +78,7 @@ class ProfessorEntity( this.website = updateProfessorRequest.website } } + +enum class ProfessorStatus { + ACTIVE, INACTIVE, VISITING +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt index d8ae890c..f190d236 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/ProfessorRepository.kt @@ -3,6 +3,6 @@ package com.wafflestudio.csereal.core.member.database import org.springframework.data.jpa.repository.JpaRepository interface ProfessorRepository : JpaRepository { - fun findByIsActiveTrue(): List - fun findByIsActiveFalse(): List + fun findByStatus(status: ProfessorStatus): List + fun findByStatusNot(status: ProfessorStatus): List } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt index f950ed43..0f243b5e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt @@ -11,15 +11,14 @@ class StaffEntity( var name: String, var role: String, - // profileImage - var office: String, var phone: String, var email: String, @OneToMany(mappedBy = "staff", cascade = [CascadeType.ALL], orphanRemoval = true) - val tasks: MutableList = mutableListOf() + val tasks: MutableList = mutableListOf(), + var imageUri: String? = null ) : BaseTimeEntity() { companion object { 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 84372eee..75fee843 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 @@ -2,15 +2,17 @@ package com.wafflestudio.csereal.core.member.dto import com.fasterxml.jackson.annotation.JsonInclude 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, val name: String, - val isActive: Boolean, + val status: ProfessorStatus, val academicRank: String, val labId: Long?, + val labName: String?, val startDate: LocalDate?, val endDate: LocalDate?, val office: String?, @@ -20,16 +22,19 @@ data class ProfessorDto( val website: String?, val educations: List, val researchAreas: List, - val careers: List + val careers: List, + @JsonInclude(JsonInclude.Include.NON_NULL) + var imageUri: String? = null ) { companion object { fun of(professorEntity: ProfessorEntity): ProfessorDto { return ProfessorDto( id = professorEntity.id, name = professorEntity.name, - isActive = professorEntity.isActive, + status = professorEntity.status, academicRank = professorEntity.academicRank, labId = professorEntity.lab?.id, + labName = professorEntity.lab?.name, startDate = professorEntity.startDate, endDate = professorEntity.endDate, office = professorEntity.office, @@ -39,7 +44,8 @@ data class ProfessorDto( website = professorEntity.website, educations = professorEntity.educations.map { it.name }, researchAreas = professorEntity.researchAreas.map { it.name }, - careers = professorEntity.careers.map { it.name } + careers = professorEntity.careers.map { it.name }, + imageUri = professorEntity.imageUri ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorPageDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorPageDto.kt new file mode 100644 index 00000000..467c171e --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/ProfessorPageDto.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.member.dto + +data class ProfessorPageDto( + val description: String, + val professors: List +) diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt index 6e070667..b3c3682b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt @@ -10,7 +10,7 @@ data class SimpleProfessorDto( val labName: String?, val phone: String?, val email: String?, - // val imageUri: String + val imageUri: String? ) { companion object { fun of(professorEntity: ProfessorEntity): SimpleProfessorDto { @@ -21,7 +21,8 @@ data class SimpleProfessorDto( labId = professorEntity.lab?.id, labName = professorEntity.lab?.name, phone = professorEntity.phone, - email = professorEntity.email + email = professorEntity.email, + imageUri = professorEntity.imageUri ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt new file mode 100644 index 00000000..750f9801 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt @@ -0,0 +1,28 @@ +package com.wafflestudio.csereal.core.member.dto + +import com.wafflestudio.csereal.core.member.database.StaffEntity + +data class SimpleStaffDto( + val id: Long, + val name: String, + val role: String, + val office: String, + val phone: String, + val email: String, + val imageUri: String? +) { + + companion object { + fun of(staffEntity: StaffEntity): SimpleStaffDto { + return SimpleStaffDto( + id = staffEntity.id, + name = staffEntity.name, + role = staffEntity.role, + office = staffEntity.office, + phone = staffEntity.phone, + email = staffEntity.email, + imageUri = staffEntity.imageUri + ) + } + } +} 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 dfdec200..8cd1ee06 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 @@ -11,7 +11,9 @@ data class StaffDto( val office: String, val phone: String, val email: String, - val tasks: List + val tasks: List, + @JsonInclude(JsonInclude.Include.NON_NULL) + val imageUri: String? = null ) { companion object { fun of(staffEntity: StaffEntity): StaffDto { @@ -22,7 +24,8 @@ data class StaffDto( office = staffEntity.office, phone = staffEntity.phone, email = staffEntity.email, - tasks = staffEntity.tasks.map { it.name } + tasks = staffEntity.tasks.map { it.name }, + imageUri = staffEntity.imageUri ) } } 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 a2a1b1a4..5710d416 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,7 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.member.database.* import com.wafflestudio.csereal.core.member.dto.ProfessorDto +import com.wafflestudio.csereal.core.member.dto.ProfessorPageDto import com.wafflestudio.csereal.core.member.dto.SimpleProfessorDto import com.wafflestudio.csereal.core.research.database.LabRepository import org.springframework.data.repository.findByIdOrNull @@ -13,7 +14,7 @@ interface ProfessorService { fun createProfessor(createProfessorRequest: ProfessorDto): ProfessorDto fun getProfessor(professorId: Long): ProfessorDto - fun getActiveProfessors(): List + fun getActiveProfessors(): ProfessorPageDto fun getInactiveProfessors(): List fun updateProfessor(professorId: Long, updateProfessorRequest: ProfessorDto): ProfessorDto fun deleteProfessor(professorId: Long) @@ -60,13 +61,22 @@ class ProfessorServiceImpl( } @Transactional(readOnly = true) - override fun getActiveProfessors(): List { - return professorRepository.findByIsActiveTrue().map { SimpleProfessorDto.of(it) }.sortedBy { it.name } + override fun getActiveProfessors(): ProfessorPageDto { + val description = "컴퓨터공학부는 35명의 훌륭한 교수진과 최신 시설을 갖추고 400여 명의 학부생과 350여 명의 대학원생에게 세계 최고 " + + "수준의 교육 연구 환경을 제공하고 있다. 2005년에는 서울대학교 최초로 외국인 정교수인 Robert Ian McKay 교수를 임용한 것을 " + + "시작으로 교내에서 가장 국제화가 활발하게 이루어지고 있는 학부로 평가받고 있다. 현재 훌륭한 외국인 교수님 두 분이 학부 학생들의 " + + "교육 및 연구 지도에 총력을 기울이고 있다.\n\n다수의 외국인 학부생, 대학원생이 재학 중에 있으며 매 학기 전공 필수 과목을 비롯한 " + + "30% 이상의 과목이 영어로 개설되고 있어 외국인 학생의 학업을 돕는 동시에 한국인 학생이 세계로 진출하는 초석이 되고 있다. 또한 " + + "CSE int’l Luncheon을 개최하여 학부 내 외국인 구성원의 화합과 생활의 불편함을 최소화하는 등 학부 차원에서 최선을 다하고 있다." + val professors = professorRepository.findByStatusNot(ProfessorStatus.INACTIVE).map { SimpleProfessorDto.of(it) } + .sortedBy { it.name } + return ProfessorPageDto(description, professors) } @Transactional(readOnly = true) override fun getInactiveProfessors(): List { - return professorRepository.findByIsActiveFalse().map { SimpleProfessorDto.of(it) }.sortedBy { it.name } + return professorRepository.findByStatus(ProfessorStatus.INACTIVE).map { SimpleProfessorDto.of(it) } + .sortedBy { it.name } } override fun updateProfessor(professorId: Long, updateProfessorRequest: ProfessorDto): ProfessorDto { 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 e7249113..d76137fb 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 @@ -4,6 +4,7 @@ import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.member.database.StaffEntity import com.wafflestudio.csereal.core.member.database.StaffRepository import com.wafflestudio.csereal.core.member.database.TaskEntity +import com.wafflestudio.csereal.core.member.dto.SimpleStaffDto import com.wafflestudio.csereal.core.member.dto.StaffDto import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -12,7 +13,7 @@ import org.springframework.transaction.annotation.Transactional interface StaffService { fun createStaff(createStaffRequest: StaffDto): StaffDto fun getStaff(staffId: Long): StaffDto - fun getAllStaff(): List + fun getAllStaff(): List fun updateStaff(staffId: Long, updateStaffRequest: StaffDto): StaffDto fun deleteStaff(staffId: Long) } @@ -42,8 +43,8 @@ class StaffServiceImpl( } @Transactional(readOnly = true) - override fun getAllStaff(): List { - return staffRepository.findAll().map { StaffDto.of(it) }.sortedBy { it.name } + override fun getAllStaff(): List { + return staffRepository.findAll().map { SimpleStaffDto.of(it) }.sortedBy { it.name } } override fun updateStaff(staffId: Long, updateStaffRequest: StaffDto): StaffDto { From 0813562e2c5606530a6232c05eeeb0dd0e01d5e2 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Tue, 15 Aug 2023 21:29:32 +0900 Subject: [PATCH 17/27] =?UTF-8?q?feat:=20introduction=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80,=20undergraduate=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: createUndergraduate, readUndergraduate 추가 * feat: readAllCourses, createCourse, readCourse 추가 * introduction 패키지 추가 * fix: dto에서 postType 삭제 * fix: postType pathVariable->requestBody 수정, 오타 수정 * fix: 프론트와 협의하여 이름 등 변경 * feat: academics 패키지 대학원도 가능하도록 추가 * feat: 장학제도 세부 장학금 create, read 추가 * fix: 수정 * fix:수정 --- .../csereal/core/about/api/AboutController.kt | 58 +++++++++++++ .../core/about/database/AboutEntity.kt | 38 +++++++++ .../core/about/database/AboutRepository.kt | 8 ++ .../core/about/database/LocationEntity.kt | 27 +++++++ .../csereal/core/about/dto/AboutDto.kt | 35 ++++++++ .../core/about/service/AboutService.kt | 70 ++++++++++++++++ .../core/academics/api/AcademicsController.kt | 81 +++++++++++++++++++ .../academics/database/AcademicsEntity.kt | 37 +++++++++ .../academics/database/AcademicsRepository.kt | 8 ++ .../core/academics/database/CourseEntity.kt | 41 ++++++++++ .../academics/database/CourseRepository.kt | 8 ++ .../core/academics/database/StudentType.kt | 5 ++ .../core/academics/dto/AcademicsDto.kt | 30 +++++++ .../csereal/core/academics/dto/CourseDto.kt | 29 +++++++ .../academics/service/AcademicsService.kt | 70 ++++++++++++++++ .../core/notice/database/NoticeEntity.kt | 2 - 16 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt new file mode 100644 index 00000000..09e902db --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt @@ -0,0 +1,58 @@ +package com.wafflestudio.csereal.core.about.api + +import com.wafflestudio.csereal.core.about.dto.AboutDto +import com.wafflestudio.csereal.core.about.service.AboutService +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/about") +@RestController +class AboutController( + private val aboutService: AboutService +) { + // postType -> 학부 소개: overview, 연혁: history, 졸업생 진로: future-careers, 연락처: contact + // 위에 있는 항목은 name = null + + // postType: student-clubs / name -> 가디언, 바쿠스, 사커301, 슈타인, 스눕스, 와플스튜디오, 유피넬 + // postType: facilities / name -> 학부-행정실, S-Lab, 소프트웨어-실습실, 하드웨어-실습실, 해동학술정보실, 학생-공간-및-동아리-방, 세미나실, 서버실 + // postType: directions / name -> by-public-transit, by-car, from-far-away + + // Todo: 전체 image, file, 학부장 인사말(greetings) signature + @PostMapping + fun createAbout( + @Valid @RequestBody request: AboutDto + ) : ResponseEntity { + return ResponseEntity.ok(aboutService.createAbout(request)) + } + + // read 목록이 하나 + @GetMapping("/{postType}") + fun readAbout( + @PathVariable postType: String, + ) : ResponseEntity { + return ResponseEntity.ok(aboutService.readAbout(postType)) + } + + @GetMapping("/student-clubs") + fun readAllClubs() : ResponseEntity> { + return ResponseEntity.ok(aboutService.readAllClubs()) + } + + @GetMapping("/facilities") + fun readAllFacilities() : ResponseEntity> { + return ResponseEntity.ok(aboutService.readAllFacilities()) + } + + + @GetMapping("/directions") + fun readAllDirections() : ResponseEntity> { + return ResponseEntity.ok(aboutService.readAllDirections()) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt new file mode 100644 index 00000000..5952e2ff --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt @@ -0,0 +1,38 @@ +package com.wafflestudio.csereal.core.about.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.about.dto.AboutDto +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.OneToMany + +@Entity(name = "about") +class AboutEntity( + var postType: String, + + var name: String, + + var engName: String?, + + var description: String, + + var year: Int?, + + var isPublic: Boolean, + + @OneToMany(mappedBy = "about", cascade = [CascadeType.ALL], orphanRemoval = true) + val locations: MutableList = mutableListOf() +) : BaseTimeEntity() { + companion object { + fun of(aboutDto: AboutDto): AboutEntity { + return AboutEntity( + postType = aboutDto.postType, + name = aboutDto.name, + engName = aboutDto.engName, + description = aboutDto.description, + year = aboutDto.year, + isPublic = aboutDto.isPublic, + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt new file mode 100644 index 00000000..fe269e39 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.about.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface AboutRepository : JpaRepository { + fun findAllByPostTypeOrderByName(postType: String): List + fun findByPostType(postType: String): AboutEntity +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt new file mode 100644 index 00000000..3e3ad0de --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/LocationEntity.kt @@ -0,0 +1,27 @@ +package com.wafflestudio.csereal.core.about.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne + +@Entity(name = "location") +class LocationEntity( + val name: String, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "about_id") + val about: AboutEntity +) : BaseTimeEntity() { + companion object { + fun create(name: String, about: AboutEntity): LocationEntity { + val locationEntity = LocationEntity( + name = name, + about = about + ) + about.locations.add(locationEntity) + return locationEntity + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt new file mode 100644 index 00000000..83a466f6 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt @@ -0,0 +1,35 @@ +package com.wafflestudio.csereal.core.about.dto + + +import com.wafflestudio.csereal.core.about.database.AboutEntity +import java.time.LocalDateTime + +data class AboutDto( + val id: Long, + val postType: String, + val name: String, + val engName: String?, + val description: String, + val year: Int?, + val createdAt: LocalDateTime?, + val modifiedAt: LocalDateTime?, + val isPublic: Boolean, + val locations: List? +) { + companion object { + fun of(entity: AboutEntity) : AboutDto = entity.run { + AboutDto( + id = this.id, + postType = this.postType, + name = this.name, + engName = this.engName, + description = this.description, + year = this.year, + createdAt = this.createdAt, + modifiedAt = this.modifiedAt, + isPublic = this.isPublic, + locations = this.locations.map { it.name } + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt new file mode 100644 index 00000000..77b5b5ba --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt @@ -0,0 +1,70 @@ +package com.wafflestudio.csereal.core.about.service + +import com.wafflestudio.csereal.core.about.database.AboutEntity +import com.wafflestudio.csereal.core.about.database.AboutRepository +import com.wafflestudio.csereal.core.about.database.LocationEntity +import com.wafflestudio.csereal.core.about.dto.AboutDto +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface AboutService { + fun createAbout(request: AboutDto): AboutDto + fun readAbout(postType: String): AboutDto + fun readAllClubs() : List + fun readAllFacilities() : List + fun readAllDirections(): List +} + +@Service +class AboutServiceImpl( + private val aboutRepository: AboutRepository +) : AboutService { + @Transactional + override fun createAbout(request: AboutDto): AboutDto { + val newAbout = AboutEntity.of(request) + + if(request.locations != null) { + for (location in request.locations) { + LocationEntity.create(location, newAbout) + } + } + + aboutRepository.save(newAbout) + + return AboutDto.of(newAbout) + } + + @Transactional(readOnly = true) + override fun readAbout(postType: String): AboutDto { + val about = aboutRepository.findByPostType(postType) + + return AboutDto.of(about) + } + + @Transactional(readOnly = true) + override fun readAllClubs(): List { + val clubs = aboutRepository.findAllByPostTypeOrderByName("student-clubs").map { + AboutDto.of(it) + } + + return clubs + } + + @Transactional(readOnly = true) + override fun readAllFacilities(): List { + val facilities = aboutRepository.findAllByPostTypeOrderByName("facilities").map { + AboutDto.of(it) + } + + return facilities + } + + @Transactional(readOnly = true) + override fun readAllDirections(): List { + val directions = aboutRepository.findAllByPostTypeOrderByName("directions").map { + AboutDto.of(it) + } + + return directions + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..047a8850 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/api/AcademicsController.kt @@ -0,0 +1,81 @@ +package com.wafflestudio.csereal.core.academics.api + +import com.wafflestudio.csereal.core.academics.database.StudentType +import com.wafflestudio.csereal.core.academics.dto.CourseDto +import com.wafflestudio.csereal.core.academics.dto.AcademicsDto +import com.wafflestudio.csereal.core.academics.service.AcademicsService +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/academics") +@RestController +class AcademicsController( + private val academicsService: AcademicsService +) { + + // postType -> 학부 안내: guide, 필수 교양 과목: general-studies-requirements, + // 전공 이수 표준 형태: curriculum, 졸업 규청: degree-requirements, + // 교과목 변경 내역: course-changes, 장학제도: scholarship + //Todo: 이미지, 파일 추가 필요 + @PostMapping("/{studentType}") + fun createAcademics( + @PathVariable studentType: StudentType, + @Valid @RequestBody request: AcademicsDto + ) : ResponseEntity { + return ResponseEntity.ok(academicsService.createAcademics(studentType, request)) + } + + @GetMapping("/{studentType}/{postType}") + fun readAcademics( + @PathVariable studentType: StudentType, + @PathVariable postType: String, + ): ResponseEntity { + return ResponseEntity.ok(academicsService.readAcademics(studentType, postType)) + } + + //교과목 정보: courses + @PostMapping("/{studentType}/course") + fun createCourse( + @PathVariable studentType: StudentType, + @Valid @RequestBody request: CourseDto + ) : ResponseEntity { + return ResponseEntity.ok(academicsService.createCourse(studentType, request)) + } + + @GetMapping("/{studentType}/courses") + fun readAllCourses( + @PathVariable studentType: StudentType, + ) : ResponseEntity> { + return ResponseEntity.ok(academicsService.readAllCourses(studentType)) + } + + @GetMapping("/course/{name}") + fun readCourse( + @PathVariable name: String + ): ResponseEntity { + return ResponseEntity.ok(academicsService.readCourse(name)) + } + + // 장학금 + @PostMapping("/{studentType}/scholarship") + fun createScholarship( + @PathVariable studentType: StudentType, + @Valid @RequestBody request: AcademicsDto + ) : ResponseEntity { + return ResponseEntity.ok(academicsService.createAcademics(studentType, request)) + } + + @GetMapping("/scholarship/{name}") + fun readScholarship( + @PathVariable name: String + ) : ResponseEntity { + return ResponseEntity.ok(academicsService.readScholarship(name)) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt new file mode 100644 index 00000000..2f70cb24 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt @@ -0,0 +1,37 @@ +package com.wafflestudio.csereal.core.academics.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.academics.dto.AcademicsDto +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Entity(name = "academics") +class AcademicsEntity( + @Enumerated(EnumType.STRING) + var studentType: StudentType, + + var postType: String, + + var name: String, + + var description: String, + + var year: Int?, + + var isPublic: Boolean, + +): BaseTimeEntity() { + companion object { + fun of(studentType: StudentType, academicsDto: AcademicsDto): AcademicsEntity { + return AcademicsEntity( + studentType = studentType, + postType = academicsDto.postType, + name = academicsDto.name, + description = academicsDto.description, + year = academicsDto.year, + isPublic = academicsDto.isPublic, + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt new file mode 100644 index 00000000..fcd58f5c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.academics.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface AcademicsRepository : JpaRepository { + fun findByStudentTypeAndPostType(studentType: StudentType, postType: String) : AcademicsEntity + fun findByName(name: String): AcademicsEntity +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt new file mode 100644 index 00000000..74f4b0bb --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt @@ -0,0 +1,41 @@ +package com.wafflestudio.csereal.core.academics.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.academics.dto.CourseDto +import jakarta.persistence.Entity + +@Entity(name = "course") +class CourseEntity( + var isDeleted: Boolean = false, + + var studentType: StudentType, + + var classification: String, + + var number: String, + + var name: String, + + var credit: Int, + + var year: String, + + var courseURL: String?, + + var description: String? +): BaseTimeEntity() { + companion object { + fun of(studentType: StudentType, courseDto: CourseDto): CourseEntity { + return CourseEntity( + studentType = studentType, + classification = courseDto.classification, + number = courseDto.number, + name = courseDto.name.replace(" ","-"), + credit = courseDto.credit, + year = courseDto.year, + courseURL = courseDto.courseURL, + description = courseDto.description + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt new file mode 100644 index 00000000..777c3961 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.academics.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface CourseRepository : JpaRepository { + fun findAllByStudentTypeOrderByYearAsc(studentType: StudentType) : List + fun findByName(name: String) : CourseEntity +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt new file mode 100644 index 00000000..ebc750af --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.academics.database + +enum class StudentType { + GRADUATE,UNDERGRADUATE +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt new file mode 100644 index 00000000..f62b2f4f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt @@ -0,0 +1,30 @@ +package com.wafflestudio.csereal.core.academics.dto + +import com.wafflestudio.csereal.core.academics.database.AcademicsEntity +import java.time.LocalDateTime + +data class AcademicsDto( + val id: Long, + val postType: String, + val name: String, + val description: String, + val year: Int?, + val createdAt: LocalDateTime?, + val modifiedAt: LocalDateTime?, + val isPublic: Boolean, +) { + companion object { + fun of(entity: AcademicsEntity) : AcademicsDto = entity.run { + AcademicsDto( + id = this.id, + postType = this.postType, + name = this.name, + description = this.description, + year = this.year, + createdAt = this.createdAt, + modifiedAt = this.modifiedAt, + isPublic = this.isPublic, + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt new file mode 100644 index 00000000..bb2fffc3 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/CourseDto.kt @@ -0,0 +1,29 @@ +package com.wafflestudio.csereal.core.academics.dto + +import com.wafflestudio.csereal.core.academics.database.CourseEntity + +data class CourseDto( + val id: Long, + val classification: String, + val number: String, + val name: String, + val credit: Int, + val year: String, + val courseURL: String?, + val description: String? +) { + companion object { + fun of(entity: CourseEntity): CourseDto = entity.run { + CourseDto( + id = this.id, + classification = this.classification, + number = this.number, + name = this.name, + credit = this.credit, + year = this.year, + courseURL = this.courseURL, + description = this.description, + ) + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..8e9e7105 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt @@ -0,0 +1,70 @@ +package com.wafflestudio.csereal.core.academics.service + +import com.wafflestudio.csereal.core.academics.database.* +import com.wafflestudio.csereal.core.academics.dto.CourseDto +import com.wafflestudio.csereal.core.academics.dto.AcademicsDto +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface AcademicsService { + fun createAcademics(studentType: StudentType, request: AcademicsDto): AcademicsDto + fun readAcademics(studentType: StudentType, postType: String): AcademicsDto + fun readAllCourses(studentType: StudentType): List + fun createCourse(studentType: StudentType, request: CourseDto): CourseDto + fun readCourse(name: String): CourseDto + fun readScholarship(name:String): AcademicsDto +} + +@Service +class AcademicsServiceImpl( + private val academicsRepository: AcademicsRepository, + private val courseRepository: CourseRepository, +) : AcademicsService { + @Transactional + override fun createAcademics(studentType: StudentType, request: AcademicsDto): AcademicsDto { + val newAcademics = AcademicsEntity.of(studentType, request) + + academicsRepository.save(newAcademics) + + return AcademicsDto.of(newAcademics) + } + + @Transactional(readOnly = true) + override fun readAcademics(studentType: StudentType, postType: String): AcademicsDto { + val academics : AcademicsEntity = academicsRepository.findByStudentTypeAndPostType(studentType, postType) + + return AcademicsDto.of(academics) + } + + @Transactional + override fun readAllCourses(studentType: StudentType): List { + val courseDtoList = courseRepository.findAllByStudentTypeOrderByYearAsc(studentType).map { + CourseDto.of(it) + } + return courseDtoList + } + @Transactional + override fun createCourse(studentType: StudentType, request: CourseDto): CourseDto { + val course = CourseEntity.of(studentType, request) + + courseRepository.save(course) + + return CourseDto.of(course) + } + + @Transactional + override fun readCourse(name: String): CourseDto { + val course : CourseEntity = courseRepository.findByName(name) + + return CourseDto.of(course) + } + + @Transactional + override fun readScholarship(name: String): AcademicsDto { + val scholarship : AcademicsEntity = academicsRepository.findByName(name) + + return AcademicsDto.of(scholarship) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt index 2a543517..79075550 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/notice/database/NoticeEntity.kt @@ -17,8 +17,6 @@ class NoticeEntity( var description: String, -// var postType: String, - var isPublic: Boolean, var isSlide: Boolean, From cffac4108c7c00e7ec53b170fa634ef79327ffd8 Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Tue, 15 Aug 2023 22:43:08 +0900 Subject: [PATCH 18/27] =?UTF-8?q?feat:=20admissions,=20research=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: admissions-학부 에서 create, read 추가 * feat: admissions-대학원도 작성 가능하도록 추가 * feat: createResearch 추가 * feat: createLab 추가 * fix: 다른 패키지에 맞게 수정 * research-groups, research-centers에서 read, update 추가 * fix: admissions, research에서 프론트와 협의하여 이름 등 수정 * fix: 오타 수정 * fix: enum 추가, pr 리뷰 반영 --- .../admissions/api/AdmissionsController.kt | 44 ++++++ .../admissions/database/AdmissionPostType.kt | 5 + .../admissions/database/AdmissionsEntity.kt | 30 ++++ .../database/AdmissionsRepository.kt | 8 ++ .../core/admissions/database/StudentType.kt | 5 + .../core/admissions/dto/AdmissionsDto.kt | 29 ++++ .../admissions/service/AdmissionsService.kt | 50 +++++++ .../core/research/api/ResearchController.kt | 52 +++++++ .../core/research/database/LabEntity.kt | 41 +++++- .../core/research/database/LabRepository.kt | 1 + .../core/research/database/ResearchEntity.kt | 34 +++++ .../research/database/ResearchPostType.kt | 5 + .../research/database/ResearchRepository.kt | 8 ++ .../csereal/core/research/dto/LabDto.kt | 36 +++++ .../csereal/core/research/dto/ResearchDto.kt | 33 +++++ .../research/dto/ResearchGroupResponse.kt | 7 + .../core/research/service/ResearchService.kt | 133 ++++++++++++++++++ .../dto/introductionMaterialDto.kt | 8 ++ 18 files changed, 525 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchGroupResponse.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchService.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/resource/introductionMaterial/dto/introductionMaterialDto.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt new file mode 100644 index 00000000..0eee0bcc --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt @@ -0,0 +1,44 @@ +package com.wafflestudio.csereal.core.admissions.api + +import com.wafflestudio.csereal.core.admissions.database.AdmissionPostType +import com.wafflestudio.csereal.core.admissions.database.StudentType +import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto +import com.wafflestudio.csereal.core.admissions.service.AdmissionsService +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RequestMapping("/admissions") +@RestController +class AdmissionsController( + private val admissionsService: AdmissionsService +) { + @PostMapping + fun createAdmissions( + @RequestParam studentType: StudentType, + @Valid @RequestBody request: AdmissionsDto + ) : AdmissionsDto { + return admissionsService.createAdmissions(studentType, request) + } + + @GetMapping + fun readAdmissionsMain( + @RequestParam studentType: StudentType, + ) : ResponseEntity { + return ResponseEntity.ok(admissionsService.readAdmissionsMain(studentType)) + } + @GetMapping("/undergraduate") + fun readUndergraduateAdmissions( + @RequestParam postType: AdmissionPostType + ) : ResponseEntity { + return ResponseEntity.ok(admissionsService.readUndergraduateAdmissions(postType)) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt new file mode 100644 index 00000000..493f01c0 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.admissions.database + +enum class AdmissionPostType { + MAIN, EARLY_ADMISSION, REGULAR_ADMISSION, +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt new file mode 100644 index 00000000..b6fd07d5 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt @@ -0,0 +1,30 @@ +package com.wafflestudio.csereal.core.admissions.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Entity(name = "admissions") +class AdmissionsEntity( + @Enumerated(EnumType.STRING) + var studentType: StudentType, + @Enumerated(EnumType.STRING) + val postType: AdmissionPostType, + val title: String, + val description: String, + val isPublic: Boolean, +): BaseTimeEntity() { + companion object { + fun of(studentType: StudentType, admissionsDto: AdmissionsDto) : AdmissionsEntity { + return AdmissionsEntity( + studentType = studentType, + postType = admissionsDto.postType, + title = admissionsDto.title, + description = admissionsDto.description, + isPublic = admissionsDto.isPublic + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt new file mode 100644 index 00000000..74c3f1e8 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.admissions.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface AdmissionsRepository : JpaRepository { + fun findByStudentTypeAndPostType(studentType: StudentType, postType: AdmissionPostType): AdmissionsEntity + fun findByPostType(postType: AdmissionPostType) : AdmissionsEntity +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt new file mode 100644 index 00000000..b549da95 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.admissions.database + +enum class StudentType { + GRADUATE, UNDERGRADUATE +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt new file mode 100644 index 00000000..cce8196b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt @@ -0,0 +1,29 @@ +package com.wafflestudio.csereal.core.admissions.dto + +import com.wafflestudio.csereal.core.admissions.database.AdmissionPostType +import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity +import java.time.LocalDateTime + +data class AdmissionsDto( + val id: Long, + val postType: AdmissionPostType, + val title: String, + val description: String, + val createdAt: LocalDateTime?, + val modifiedAt: LocalDateTime?, + val isPublic: Boolean, +) { + companion object { + fun of(entity: AdmissionsEntity) : AdmissionsDto = entity.run { + AdmissionsDto( + id = this.id, + postType = this.postType, + title = this.title, + description = this.description, + createdAt = this.createdAt, + modifiedAt = this.modifiedAt, + isPublic = this.isPublic + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt new file mode 100644 index 00000000..d8289811 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt @@ -0,0 +1,50 @@ +package com.wafflestudio.csereal.core.admissions.service + +import com.wafflestudio.csereal.core.admissions.database.AdmissionPostType +import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity +import com.wafflestudio.csereal.core.admissions.database.AdmissionsRepository +import com.wafflestudio.csereal.core.admissions.database.StudentType +import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface AdmissionsService { + fun createAdmissions(studentType: StudentType, request: AdmissionsDto): AdmissionsDto + fun readAdmissionsMain(studentType: StudentType): AdmissionsDto + fun readUndergraduateAdmissions(postType: AdmissionPostType): AdmissionsDto + +} + +@Service +class AdmissionsServiceImpl( + private val admissionsRepository: AdmissionsRepository +) : AdmissionsService { + @Transactional + override fun createAdmissions(studentType: StudentType, request: AdmissionsDto): AdmissionsDto { + val newAdmissions: AdmissionsEntity = AdmissionsEntity.of(studentType, request) + + admissionsRepository.save(newAdmissions) + + return AdmissionsDto.of(newAdmissions) + } + + @Transactional(readOnly = true) + override fun readAdmissionsMain(studentType: StudentType): AdmissionsDto { + return if (studentType == StudentType.UNDERGRADUATE) { + AdmissionsDto.of(admissionsRepository.findByStudentTypeAndPostType(StudentType.UNDERGRADUATE, AdmissionPostType.MAIN)) + } else { + AdmissionsDto.of(admissionsRepository.findByStudentTypeAndPostType(StudentType.GRADUATE, AdmissionPostType.MAIN)) + } + } + + @Transactional(readOnly = true) + override fun readUndergraduateAdmissions(postType: AdmissionPostType): AdmissionsDto { + return if (postType == AdmissionPostType.EARLY_ADMISSION) { + AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionPostType.EARLY_ADMISSION)) + } else { + AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionPostType.REGULAR_ADMISSION)) + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt new file mode 100644 index 00000000..ff5607f7 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/api/ResearchController.kt @@ -0,0 +1,52 @@ +package com.wafflestudio.csereal.core.research.api + +import com.wafflestudio.csereal.core.research.dto.LabDto +import com.wafflestudio.csereal.core.research.dto.ResearchDto +import com.wafflestudio.csereal.core.research.dto.ResearchGroupResponse +import com.wafflestudio.csereal.core.research.service.ResearchService +import jakarta.validation.Valid +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RequestMapping("/research") +@RestController +class ResearchController( + private val researchService: ResearchService +) { + @PostMapping + fun createResearchDetail( + @Valid @RequestBody request: ResearchDto + ) : ResponseEntity { + return ResponseEntity.ok(researchService.createResearchDetail(request)) + } + + @GetMapping("/groups") + fun readAllResearchGroups() : ResponseEntity { + return ResponseEntity.ok(researchService.readAllResearchGroups()) + } + + @GetMapping("/centers") + fun readAllResearchCenters() : ResponseEntity> { + return ResponseEntity.ok(researchService.readAllResearchCenters()) + } + + @PatchMapping("/{researchId}") + fun updateResearchDetail( + @PathVariable researchId: Long, + @Valid @RequestBody request: ResearchDto + ) : ResponseEntity { + return ResponseEntity.ok(researchService.updateResearchDetail(researchId, request)) + } + + @PostMapping("/lab") + fun createLab( + @Valid @RequestBody request: LabDto + ) : ResponseEntity { + return ResponseEntity.ok(researchService.createLab(request)) + } + + @GetMapping("/labs") + fun readAllLabs() : ResponseEntity> { + return ResponseEntity.ok(researchService.readAllLabs()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt index 06b88e10..3720f60e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabEntity.kt @@ -2,8 +2,8 @@ package com.wafflestudio.csereal.core.research.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.core.member.database.ProfessorEntity -import jakarta.persistence.Entity -import jakarta.persistence.OneToMany +import com.wafflestudio.csereal.core.research.dto.LabDto +import jakarta.persistence.* @Entity(name = "lab") class LabEntity( @@ -11,5 +11,38 @@ class LabEntity( val name: String, @OneToMany(mappedBy = "lab") - val professors: MutableSet = mutableSetOf() -) : BaseTimeEntity() + val professors: MutableSet = mutableSetOf(), + + val location: String?, + val tel: String?, + val acronym: String?, + val pdf: String?, + val youtube: String?, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "research_id") + var research: ResearchEntity, + + val description: String?, + + val websiteURL: String?, + val isPublic: Boolean, + +) : BaseTimeEntity() { + companion object { + fun of(researchGroup: ResearchEntity, labDto: LabDto) : LabEntity { + return LabEntity( + name = labDto.name, + location = labDto.location, + tel = labDto.tel, + acronym = labDto.acronym, + pdf = labDto.introductionMaterials?.pdf, + youtube = labDto.introductionMaterials?.youtube, + research = researchGroup, + description = labDto.description, + websiteURL = labDto.websiteURL, + isPublic = labDto.isPublic, + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt index 84bf190a..c7e15692 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/LabRepository.kt @@ -3,4 +3,5 @@ package com.wafflestudio.csereal.core.research.database import org.springframework.data.jpa.repository.JpaRepository interface LabRepository : JpaRepository { + fun findAllByOrderByName(): List } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt new file mode 100644 index 00000000..ad3d294f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchEntity.kt @@ -0,0 +1,34 @@ +package com.wafflestudio.csereal.core.research.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.research.dto.ResearchDto +import jakarta.persistence.* + +@Entity(name = "research") +class ResearchEntity( + @Enumerated(EnumType.STRING) + var postType: ResearchPostType, + + var name: String, + + var description: String?, + + var websiteURL: String?, + + var isPublic: Boolean, + + @OneToMany(mappedBy = "research", cascade = [CascadeType.ALL], orphanRemoval = true) + var labs: MutableList = mutableListOf() +): BaseTimeEntity() { + companion object { + fun of(researchDto: ResearchDto) : ResearchEntity { + return ResearchEntity( + postType = researchDto.postType, + name = researchDto.name, + description = researchDto.description, + websiteURL = researchDto.websiteURL, + isPublic = researchDto.isPublic + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt new file mode 100644 index 00000000..c86016b0 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchPostType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.research.database + +enum class ResearchPostType { + GROUPS, CENTERS, LABS +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt new file mode 100644 index 00000000..e9a9fd8a --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/database/ResearchRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.research.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface ResearchRepository : JpaRepository { + fun findByName(name: String): ResearchEntity? + fun findAllByPostTypeOrderByName(postType: ResearchPostType): List +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt new file mode 100644 index 00000000..82f95b23 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/LabDto.kt @@ -0,0 +1,36 @@ +package com.wafflestudio.csereal.core.research.dto + +import com.wafflestudio.csereal.core.research.database.LabEntity +import com.wafflestudio.csereal.core.resource.introductionMaterial.dto.IntroductionMaterialDto + +data class LabDto( + val id: Long, + val name: String, + val professors: List?, + val location: String?, + val tel: String?, + val acronym: String?, + val introductionMaterials: IntroductionMaterialDto?, + val group: String, + val description: String?, + val websiteURL: String?, + val isPublic: Boolean, +) { + companion object { + fun of(entity: LabEntity): LabDto = entity.run { + LabDto( + id = this.id, + name = this.name, + professors = this.professors.map { it.name }, + location = this.location, + tel = this.tel, + acronym = this.acronym, + introductionMaterials = IntroductionMaterialDto(this.pdf, this.youtube), + group = this.research.name, + description = this.description, + websiteURL = this.websiteURL, + isPublic = this.isPublic + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt new file mode 100644 index 00000000..e81ba4fd --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchDto.kt @@ -0,0 +1,33 @@ +package com.wafflestudio.csereal.core.research.dto + +import com.wafflestudio.csereal.core.research.database.ResearchEntity +import com.wafflestudio.csereal.core.research.database.ResearchPostType +import java.time.LocalDateTime + +data class ResearchDto( + val id: Long, + val postType: ResearchPostType, + val name: String, + val description: String?, + val websiteURL: String?, + val createdAt: LocalDateTime?, + val modifiedAt: LocalDateTime?, + val isPublic: Boolean, + val labsId: List? +) { + companion object { + fun of(entity: ResearchEntity) = entity.run { + ResearchDto( + id = this.id, + postType = this.postType, + name = this.name, + description = this.description, + websiteURL = this.websiteURL, + createdAt = this.createdAt, + modifiedAt = this.modifiedAt, + isPublic = this.isPublic, + labsId = this.labs.map { it.id } + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchGroupResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchGroupResponse.kt new file mode 100644 index 00000000..6bd08a2c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/dto/ResearchGroupResponse.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.research.dto + +data class ResearchGroupResponse( + val description: String, + val groups: List +) { +} \ No newline at end of file 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 new file mode 100644 index 00000000..e42bc7c7 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchService.kt @@ -0,0 +1,133 @@ +package com.wafflestudio.csereal.core.research.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.member.database.ProfessorRepository +import com.wafflestudio.csereal.core.research.database.* +import com.wafflestudio.csereal.core.research.dto.LabDto +import com.wafflestudio.csereal.core.research.dto.ResearchDto +import com.wafflestudio.csereal.core.research.dto.ResearchGroupResponse +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface ResearchService { + fun createResearchDetail(request: ResearchDto): ResearchDto + fun readAllResearchGroups(): ResearchGroupResponse + fun readAllResearchCenters(): List + fun updateResearchDetail(researchId: Long, request: ResearchDto): ResearchDto + fun createLab(request: LabDto): LabDto + + fun readAllLabs(): List +} + +@Service +class ResearchServiceImpl( + private val researchRepository: ResearchRepository, + private val labRepository: LabRepository, + private val professorRepository: ProfessorRepository +) : ResearchService { + @Transactional + override fun createResearchDetail(request: ResearchDto): ResearchDto { + val newResearch = ResearchEntity.of(request) + if(request.labsId != null) { + + for(labId in request.labsId) { + val lab = labRepository.findByIdOrNull(labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") + newResearch.labs.add(lab) + lab.research = newResearch + } + } + + researchRepository.save(newResearch) + + return ResearchDto.of(newResearch) + } + + @Transactional(readOnly = true) + override fun readAllResearchGroups(): ResearchGroupResponse { + // Todo: description 수정 필요 + val description = "세계가 주목하는 컴퓨터공학부의 많은 교수들은 ACM, IEEE 등 " + + "세계적인 컴퓨터관련 주요 학회에서 국제학술지 편집위원, 국제학술회의 위원장, 기조연설자 등으로 활발하게 활동하고 있습니다. " + + "정부 지원과제, 민간 산업체 지원 연구과제 등도 성공적으로 수행, 우수한 성과들을 내놓고 있으며, " + + "오늘도 인류가 꿈꾸는 행복하고 편리한 세상을 위해 변화와 혁신, 연구와 도전을 계속하고 있습니다." + + val researchGroups = researchRepository.findAllByPostTypeOrderByName(ResearchPostType.GROUPS).map { + ResearchDto.of(it) + } + + return ResearchGroupResponse(description, researchGroups) + } + + @Transactional(readOnly = true) + override fun readAllResearchCenters(): List { + val researchCenters = researchRepository.findAllByPostTypeOrderByName(ResearchPostType.CENTERS).map { + ResearchDto.of(it) + } + + return researchCenters + } + @Transactional + override fun updateResearchDetail(researchId: Long, request: ResearchDto): ResearchDto { + val research = researchRepository.findByIdOrNull(researchId) + ?: throw CserealException.Csereal404("해당 게시글을 찾을 수 없습니다.(researchId=$researchId)") + + if(request.labsId != null) { + for(labId in request.labsId) { + val lab = labRepository.findByIdOrNull(labId) + ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다.(labId=$labId)") + + } + + val oldLabs = research.labs.map { it.id } + + val labsToRemove = oldLabs - request.labsId + val labsToAdd = request.labsId - oldLabs + + research.labs.removeIf { it.id in labsToRemove} + + for(labsToAddId in labsToAdd) { + val lab = labRepository.findByIdOrNull(labsToAddId)!! + research.labs.add(lab) + lab.research = research + + } + } + + return ResearchDto.of(research) + } + + @Transactional + override fun createLab(request: LabDto): LabDto { + val researchGroup = researchRepository.findByName(request.group) + ?: throw CserealException.Csereal404("해당 연구그룹을 찾을 수 없습니다.(researchGroupId = ${request.group}") + + if(researchGroup.postType != ResearchPostType.GROUPS) { + throw CserealException.Csereal404("해당 게시글은 연구그룹이어야 합니다.") + } + + // get을 우선 구현하기 위해 빼겠습니다 + /* + if(request.professorsId != null) { + for(professorId in request.professorsId) { + val professor = professorRepository.findByIdOrNull(professorId) + ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다.(professorId = $professorId") + } + } + + */ + val newLab = LabEntity.of(researchGroup, request) + + labRepository.save(newLab) + return LabDto.of(newLab) + } + + @Transactional(readOnly = true) + override fun readAllLabs(): List { + val labs = labRepository.findAllByOrderByName().map { + LabDto.of(it) + } + + return labs + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/introductionMaterial/dto/introductionMaterialDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/introductionMaterial/dto/introductionMaterialDto.kt new file mode 100644 index 00000000..f4c9873c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/introductionMaterial/dto/introductionMaterialDto.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.resource.introductionMaterial.dto + +// Todo: IntroductionMaterial이 연구실에만 쓰이는지 확인할 것 +data class IntroductionMaterialDto( + val pdf: String?, + val youtube: String?, +) { +} \ No newline at end of file From d2c341c06e23cde7a6937fb36788bf06d2649c29 Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Tue, 22 Aug 2023 14:35:29 +0900 Subject: [PATCH 19/27] =?UTF-8?q?feat:=20oidc=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20(#27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: oidc 로그인 * feat: TaskEntity name 추가 * feat: 배포 설정 * feat: 유저 정보에 학번 추가, 로그인 시 sub claim 확인 * feat: 배포 테스트 위해 redirect-uri 변경 * fix: groups claim 소문자로 수정 * feat: idsnucse 다운 되었을때 에러 처리 * feat: idsnucse 다운 되었을때 에러 처리 --- .github/workflows/deploy.yaml | 4 +- docker-compose.yml | 11 ++- .../wafflestudio/csereal/common/Exceptions.kt | 1 + .../common/config/CserealExceptionHandler.kt | 10 +- .../csereal/common/config/JwtConfig.kt | 20 ++++ .../common/config/RestTemplateConfig.kt | 15 +++ .../csereal/common/config/SecurityConfig.kt | 55 ++++++++--- .../core/member/database/TaskEntity.kt | 2 +- .../csereal/core/user/database/UserEntity.kt | 23 +++++ .../core/user/database/UserRepository.kt | 7 ++ .../user/service/CustomOidcUserService.kt | 96 +++++++++++++++++++ src/main/resources/application.yaml | 28 +++++- 12 files changed, 247 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/JwtConfig.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/RestTemplateConfig.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/user/service/CustomOidcUserService.kt diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 5b3d3600..4a002e96 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -53,6 +53,7 @@ jobs: echo "MYSQL_PASSWORD=${{secrets.MYSQL_PASSWORD}}" >> .env echo "MYSQL_DATABASE=${{secrets.MYSQL_DATABASE}}" >> .env echo "PROFILE=prod" >> .env + echo "OIDC_CLIENT_SECRET_DEV=${{secrets.OIDC_CLIENT_SECRET_DEV}}" >> .env - name: SCP Command to Transfer Files @@ -73,6 +74,7 @@ jobs: MYSQL_PASSWORD: ${{secrets.MYSQL_PASSWORD}} MYSQL_DATABASE: ${{secrets.MYSQL_DATABASE}} PROFILE: "prod" + OIDC_CLIENT_SECRET_DEV: ${{secrets.OIDC_CLIENT_SECRET_DEV}} with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} @@ -82,4 +84,4 @@ jobs: source .env docker-compose down docker-compose pull - docker-compose up -d \ No newline at end of file + docker-compose up -d diff --git a/docker-compose.yml b/docker-compose.yml index 9cf741c8..ac6b0f24 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: SPRING_DATASOURCE_URL: "jdbc:mysql://csereal_db_container:3306/${MYSQL_DATABASE}?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} + OIDC_CLIENT_SECRET_DEV: ${OIDC_CLIENT_SECRET_DEV} depends_on: - db networks: @@ -24,11 +25,11 @@ services: volumes: - ./db:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - MYSQL_USER: ${MYSQL_USER} - MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} networks: - csereal_network networks: - csereal_network: \ No newline at end of file + csereal_network: diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt index cb0d7b08..2809d4f6 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt @@ -5,4 +5,5 @@ import org.springframework.http.HttpStatus open class CserealException(msg: String, val status: HttpStatus) : RuntimeException(msg) { class Csereal400(msg: String) : CserealException(msg, HttpStatus.BAD_REQUEST) class Csereal404(msg: String) : CserealException(msg, HttpStatus.NOT_FOUND) + class Csereal401(msg: String) : CserealException(msg, HttpStatus.UNAUTHORIZED) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt index 6cf3d136..c93f7b80 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/CserealExceptionHandler.kt @@ -7,6 +7,8 @@ import org.springframework.validation.BindingResult import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.client.ResourceAccessException +import org.springframework.web.client.RestClientException import java.sql.SQLIntegrityConstraintViolationException @RestControllerAdvice @@ -30,4 +32,10 @@ class CserealExceptionHandler { fun handle(e: SQLIntegrityConstraintViolationException): ResponseEntity { return ResponseEntity("중복된 값이 있습니다.", HttpStatus.CONFLICT) } -} \ No newline at end of file + + // oidc provider 서버에 문제가 있을때 + @ExceptionHandler(value = [RestClientException::class]) + fun handle(e: RestClientException): ResponseEntity { + return ResponseEntity("idsnucse error: ${e.message}", HttpStatus.BAD_GATEWAY) + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/JwtConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/JwtConfig.kt new file mode 100644 index 00000000..76af30f3 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/JwtConfig.kt @@ -0,0 +1,20 @@ +package com.wafflestudio.csereal.common.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory +import org.springframework.security.oauth2.client.registration.ClientRegistration +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm +import org.springframework.security.oauth2.jwt.JwtDecoderFactory + +@Configuration +class JwtConfig { + + @Bean + fun idTokenDecoderFactory(): JwtDecoderFactory { + val idTokenDecoderFactory = OidcIdTokenDecoderFactory() + idTokenDecoderFactory.setJwsAlgorithmResolver { SignatureAlgorithm.ES256 } + return idTokenDecoderFactory + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/RestTemplateConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/RestTemplateConfig.kt new file mode 100644 index 00000000..e7497867 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/RestTemplateConfig.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.common.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.client.RestTemplate + +@Configuration +class RestTemplateConfig { + + @Bean + fun restTemplate(): RestTemplate { + return RestTemplate() + } + +} 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 5ec132d7..5dbb4636 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt @@ -1,30 +1,57 @@ package com.wafflestudio.csereal.common.config +import com.wafflestudio.csereal.core.user.service.CustomOidcUserService +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.core.Authentication import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler +import org.springframework.web.client.RestTemplate @Configuration -class SpringSecurityConfig { +@EnableWebSecurity +class SecurityConfig( + private val customOidcUserService: CustomOidcUserService +) { - // 확인 바람 @Bean - fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain = - httpSecurity - .csrf().disable() - .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + fun filterChain(http: HttpSecurity): SecurityFilterChain { + return http.csrf().disable() + .oauth2Login() + .loginPage("/oauth2/authorization/idsnucse") + .userInfoEndpoint().oidcUserService(customOidcUserService).and() .and() - .authorizeRequests() + .logout() + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + .invalidateHttpSession(true) + .clearAuthentication(true) + .deleteCookies("JSESSIONID") + .and() + .authorizeHttpRequests() + .requestMatchers("/login").authenticated() .anyRequest().permitAll() .and() .build() + } + + @Bean + fun oidcLogoutSuccessHandler(): LogoutSuccessHandler { + return object : SimpleUrlLogoutSuccessHandler() { + override fun onLogoutSuccess( + request: HttpServletRequest?, + response: HttpServletResponse?, + authentication: Authentication? + ) { + super.setDefaultTargetUrl("/") + super.onLogoutSuccess(request, response, authentication) + } + } + } -// @Bean -// fun filterChain(http: HttpSecurity): SecurityFilterChain { -// http.httpBasic().disable() -// return http.build() -// } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt index 745365dd..d30a96b3 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/TaskEntity.kt @@ -6,7 +6,7 @@ import jakarta.persistence.FetchType import jakarta.persistence.JoinColumn import jakarta.persistence.ManyToOne -@Entity +@Entity(name = "task") class TaskEntity( val name: String, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserEntity.kt new file mode 100644 index 00000000..2dca2073 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserEntity.kt @@ -0,0 +1,23 @@ +package com.wafflestudio.csereal.core.user.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Entity(name = "users") +class UserEntity( + + val username: String, + val name: String, + val email: String, + val studentId: String, + + @Enumerated(EnumType.STRING) + val role: Role?, + + ) : BaseTimeEntity() + +enum class Role { + ROLE_STAFF, ROLE_GRADUATE, ROLE_PROFESSOR +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserRepository.kt new file mode 100644 index 00000000..38b8b367 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/user/database/UserRepository.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.core.user.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface UserRepository : JpaRepository { + fun findByUsername(username: String): UserEntity? +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/user/service/CustomOidcUserService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/user/service/CustomOidcUserService.kt new file mode 100644 index 00000000..551ce684 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/user/service/CustomOidcUserService.kt @@ -0,0 +1,96 @@ +package com.wafflestudio.csereal.core.user.service + +import com.wafflestudio.csereal.common.CserealException +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.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.util.LinkedMultiValueMap +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.exchange + +@Service +class CustomOidcUserService( + private val userRepository: UserRepository, + private val restTemplate: RestTemplate +) : OAuth2UserService { + + override fun loadUser(userRequest: OidcUserRequest): OidcUser { + val oidcUser = DefaultOidcUser( + userRequest.clientRegistration.scopes.map { SimpleGrantedAuthority("SCOPE_$it") }, + userRequest.idToken + ) + + val username = oidcUser.idToken.getClaim("username") + val user = userRepository.findByUsername(username) + + if (user == null) { + val userInfoAttributes = fetchUserInfo(userRequest) + createUser(username, userInfoAttributes) + } + + return oidcUser + } + + private fun fetchUserInfo(userRequest: OidcUserRequest): Map { + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_FORM_URLENCODED + } + + val body = LinkedMultiValueMap().apply { + add("access_token", userRequest.accessToken.tokenValue) + } + + val requestEntity = HttpEntity(body, headers) + + val userInfoResponse = restTemplate.exchange>( + userRequest.clientRegistration.providerDetails.userInfoEndpoint.uri, + HttpMethod.POST, requestEntity, Map::class.java + ) + + if (userInfoResponse.body?.get("sub") != userRequest.idToken.getClaim("sub")) { + throw CserealException.Csereal401("Authentication failed") + } + + return userInfoResponse.body ?: emptyMap() + } + + @Transactional + fun createUser(username: String, userInfo: Map) { + + val name = userInfo["name"] as String + val email = userInfo["email"] as String + val studentId = userInfo["student_id"] as String + + val groups = userInfo["groups"] as List + val role = if ("staff" in groups) { + Role.ROLE_STAFF + } else if ("professor" in groups) { + Role.ROLE_PROFESSOR + } else if ("graduate" in groups) { + Role.ROLE_GRADUATE + } else { + null + } + + val newUser = UserEntity( + username = username, + name = name, + email = email, + studentId = studentId, + role = role + ) + + userRepository.save(newUser) + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8ffade6f..6f1e5d75 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,9 +1,28 @@ spring: profiles: active: local + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + security: + oauth2: + client: + registration: + idsnucse: + client-id: waffle-dev-local-testing + client-secret: ${OIDC_CLIENT_SECRET_DEV} + authorization-grant-type: authorization_code + redirect-uri: http://cse-dev-waffle.bacchus.io/login/oauth2/code/idsnucse + scope: openid, profile, email + provider: + idsnucse: + issuer-uri: https://id-dev.bacchus.io/o + jwk-set-uri: https://id-dev.bacchus.io/o/jwks + +server: + servlet: + session: + timeout: 7200 # 2시간 -datasource: - driver-class-name: com.mysql.cj.jdbc.Driver springdoc: swagger-ui: path: index.html @@ -24,6 +43,9 @@ spring: open-in-view: false logging.level: default: INFO + org: + springframework: + security: DEBUG --- spring: @@ -31,4 +53,4 @@ spring: jpa: hibernate: ddl-auto: create # TODO: change to validate (or none) when save actual data to server - open-in-view: false \ No newline at end of file + open-in-view: false From e05b5ad2ab7c23d6ff4ab2d70ca1d9d5ea1b8a8e Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Tue, 22 Aug 2023 15:43:10 +0900 Subject: [PATCH 20/27] =?UTF-8?q?feat:=20cors=20=EC=84=A4=EC=A0=95=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../csereal/common/config/SecurityConfig.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 5dbb4636..8c38d9d2 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt @@ -11,7 +11,9 @@ import org.springframework.security.core.Authentication import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.logout.LogoutSuccessHandler import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler -import org.springframework.web.client.RestTemplate +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource +import org.springframework.web.cors.UrlBasedCorsConfigurationSource @Configuration @@ -23,6 +25,8 @@ class SecurityConfig( @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { return http.csrf().disable() + .cors() + .and() .oauth2Login() .loginPage("/oauth2/authorization/idsnucse") .userInfoEndpoint().oidcUserService(customOidcUserService).and() @@ -54,4 +58,16 @@ class SecurityConfig( } } + @Bean + fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("http://localhost:3000", "http://cse-dev-waffle.bacchus.io:3000") + configuration.allowedMethods = listOf("*") + configuration.allowedHeaders = listOf("*") + configuration.maxAge = 3000 + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source + } + } From 28fa788f5495186cdcedd25065565414b4fd082c Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Tue, 22 Aug 2023 16:52:07 +0900 Subject: [PATCH 21/27] =?UTF-8?q?fix:=20cors=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: cors 설정 * feat: cors 설정 --- .../csereal/common/config/WebConfig.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt new file mode 100644 index 00000000..3c1a296c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt @@ -0,0 +1,18 @@ +package com.wafflestudio.csereal.common.config + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig : WebMvcConfigurer { + + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000", "http://cse-dev-waffle.bacchus.io:3000") + .allowedMethods("*") + .allowedHeaders("*") + .maxAge(3000) + } + +} From 3e0f8e7ec04226784f5c95ada2e2714ba5d28e2f Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Wed, 23 Aug 2023 15:29:39 +0900 Subject: [PATCH 22/27] fix: CORS (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: cors 설정 * feat: cors 설정 * feat: cors 설정 --- .../com/wafflestudio/csereal/common/config/SecurityConfig.kt | 2 +- .../kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 2468a89a..f34e8c9a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/SecurityConfig.kt @@ -62,7 +62,7 @@ class SecurityConfig( @Bean fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() - configuration.allowedOrigins = listOf("http://localhost:3000", "http://cse-dev-waffle.bacchus.io:3000") + configuration.allowedOrigins = listOf("*") configuration.allowedMethods = listOf("*") configuration.allowedHeaders = listOf("*") configuration.maxAge = 3000 diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt b/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt index 3c1a296c..02795670 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/config/WebConfig.kt @@ -9,7 +9,7 @@ class WebConfig : WebMvcConfigurer { override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") - .allowedOrigins("http://localhost:3000", "http://cse-dev-waffle.bacchus.io:3000") + .allowedOrigins("*") .allowedMethods("*") .allowedHeaders("*") .maxAge(3000) From a86542c339925a9111850de67ad1a91ff73d84be Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Thu, 24 Aug 2023 21:14:22 +0900 Subject: [PATCH 23/27] =?UTF-8?q?fix:=20about,=20academics,=20admissions?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=88=98=EC=A0=95=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: admissions, about 컨트롤러 변경 * fix: academics 컨트롤러 수정 * 커밋중 * fix: pr 리뷰 반영 * feat: readMain 추가 * pr 리뷰 반영 --- .../csereal/core/about/api/AboutController.kt | 9 +-- .../core/about/database/AboutEntity.kt | 20 ++--- .../core/about/database/AboutPostType.kt | 5 ++ .../core/about/database/AboutRepository.kt | 4 +- .../csereal/core/about/dto/AboutDto.kt | 6 +- .../core/about/service/AboutService.kt | 28 +++++-- .../core/academics/api/AcademicsController.kt | 32 ++++---- .../academics/database/AcademicsEntity.kt | 12 ++- .../academics/database/AcademicsPostType.kt | 5 ++ .../academics/database/AcademicsRepository.kt | 2 +- .../database/AcademicsStudentType.kt | 5 ++ .../core/academics/database/CourseEntity.kt | 4 +- .../academics/database/CourseRepository.kt | 2 +- .../core/academics/database/StudentType.kt | 5 -- .../core/academics/dto/AcademicsDto.kt | 2 - .../academics/service/AcademicsService.kt | 73 +++++++++++++------ .../admissions/api/AdmissionsController.kt | 30 +++++--- .../admissions/database/AdmissionPostType.kt | 5 -- .../admissions/database/AdmissionsEntity.kt | 11 +-- .../admissions/database/AdmissionsPostType.kt | 5 ++ .../database/AdmissionsRepository.kt | 3 +- .../core/admissions/database/StudentType.kt | 5 -- .../core/admissions/dto/AdmissionsDto.kt | 5 -- .../admissions/service/AdmissionsService.kt | 52 ++++++++----- .../csereal/core/main/api/MainController.kt | 18 +++++ .../core/main/database/MainRepository.kt | 70 ++++++++++++++++++ .../csereal/core/main/dto/MainResponse.kt | 11 +++ .../csereal/core/main/dto/NewsResponse.kt | 12 +++ .../csereal/core/main/dto/NoticeResponse.kt | 11 +++ .../csereal/core/main/service/MainService.kt | 25 +++++++ 30 files changed, 333 insertions(+), 144 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutPostType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsPostType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt delete mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/main/database/MainRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/main/dto/MainResponse.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NewsResponse.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NoticeResponse.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt index 09e902db..4a1c87c5 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RequestMapping("/about") @@ -16,19 +17,17 @@ import org.springframework.web.bind.annotation.RestController class AboutController( private val aboutService: AboutService ) { - // postType -> 학부 소개: overview, 연혁: history, 졸업생 진로: future-careers, 연락처: contact - // 위에 있는 항목은 name = null - // postType: student-clubs / name -> 가디언, 바쿠스, 사커301, 슈타인, 스눕스, 와플스튜디오, 유피넬 // postType: facilities / name -> 학부-행정실, S-Lab, 소프트웨어-실습실, 하드웨어-실습실, 해동학술정보실, 학생-공간-및-동아리-방, 세미나실, 서버실 // postType: directions / name -> by-public-transit, by-car, from-far-away // Todo: 전체 image, file, 학부장 인사말(greetings) signature - @PostMapping + @PostMapping("/{postType}") fun createAbout( + @PathVariable postType: String, @Valid @RequestBody request: AboutDto ) : ResponseEntity { - return ResponseEntity.ok(aboutService.createAbout(request)) + return ResponseEntity.ok(aboutService.createAbout(postType, request)) } // read 목록이 하나 diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt index 5952e2ff..369e06a9 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt @@ -2,36 +2,28 @@ package com.wafflestudio.csereal.core.about.database import com.wafflestudio.csereal.common.config.BaseTimeEntity import com.wafflestudio.csereal.core.about.dto.AboutDto -import jakarta.persistence.CascadeType -import jakarta.persistence.Entity -import jakarta.persistence.OneToMany +import jakarta.persistence.* @Entity(name = "about") class AboutEntity( - var postType: String, - - var name: String, - + @Enumerated(EnumType.STRING) + var postType: AboutPostType, + var name: String?, var engName: String?, - var description: String, - var year: Int?, - var isPublic: Boolean, - @OneToMany(mappedBy = "about", cascade = [CascadeType.ALL], orphanRemoval = true) val locations: MutableList = mutableListOf() ) : BaseTimeEntity() { companion object { - fun of(aboutDto: AboutDto): AboutEntity { + fun of(postType: AboutPostType, aboutDto: AboutDto): AboutEntity { return AboutEntity( - postType = aboutDto.postType, + postType = postType, name = aboutDto.name, engName = aboutDto.engName, description = aboutDto.description, year = aboutDto.year, - isPublic = aboutDto.isPublic, ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutPostType.kt new file mode 100644 index 00000000..ab12f058 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutPostType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.about.database + +enum class AboutPostType { + OVERVIEW, HISTORY, FUTURE_CAREERS, CONTACT, STUDENT_CLUBS, FACILITIES, DIRECTIONS, +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt index fe269e39..a66fe46b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutRepository.kt @@ -3,6 +3,6 @@ package com.wafflestudio.csereal.core.about.database import org.springframework.data.jpa.repository.JpaRepository interface AboutRepository : JpaRepository { - fun findAllByPostTypeOrderByName(postType: String): List - fun findByPostType(postType: String): AboutEntity + fun findAllByPostTypeOrderByName(postType: AboutPostType): List + fun findByPostType(postType: AboutPostType): AboutEntity } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt index 83a466f6..63652e47 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt @@ -6,28 +6,24 @@ import java.time.LocalDateTime data class AboutDto( val id: Long, - val postType: String, - val name: String, + val name: String?, val engName: String?, val description: String, val year: Int?, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, - val isPublic: Boolean, val locations: List? ) { companion object { fun of(entity: AboutEntity) : AboutDto = entity.run { AboutDto( id = this.id, - postType = this.postType, name = this.name, engName = this.engName, description = this.description, year = this.year, createdAt = this.createdAt, modifiedAt = this.modifiedAt, - isPublic = this.isPublic, locations = this.locations.map { it.name } ) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt index 77b5b5ba..79b6c8a2 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt @@ -1,6 +1,8 @@ package com.wafflestudio.csereal.core.about.service +import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.about.database.AboutEntity +import com.wafflestudio.csereal.core.about.database.AboutPostType import com.wafflestudio.csereal.core.about.database.AboutRepository import com.wafflestudio.csereal.core.about.database.LocationEntity import com.wafflestudio.csereal.core.about.dto.AboutDto @@ -8,7 +10,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface AboutService { - fun createAbout(request: AboutDto): AboutDto + fun createAbout(postType: String, request: AboutDto): AboutDto fun readAbout(postType: String): AboutDto fun readAllClubs() : List fun readAllFacilities() : List @@ -20,8 +22,9 @@ class AboutServiceImpl( private val aboutRepository: AboutRepository ) : AboutService { @Transactional - override fun createAbout(request: AboutDto): AboutDto { - val newAbout = AboutEntity.of(request) + override fun createAbout(postType: String, request: AboutDto): AboutDto { + val enumPostType = makeStringToEnum(postType) + val newAbout = AboutEntity.of(enumPostType, request) if(request.locations != null) { for (location in request.locations) { @@ -36,14 +39,15 @@ class AboutServiceImpl( @Transactional(readOnly = true) override fun readAbout(postType: String): AboutDto { - val about = aboutRepository.findByPostType(postType) + val enumPostType = makeStringToEnum(postType) + val about = aboutRepository.findByPostType(enumPostType) return AboutDto.of(about) } @Transactional(readOnly = true) override fun readAllClubs(): List { - val clubs = aboutRepository.findAllByPostTypeOrderByName("student-clubs").map { + val clubs = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.STUDENT_CLUBS).map { AboutDto.of(it) } @@ -52,7 +56,7 @@ class AboutServiceImpl( @Transactional(readOnly = true) override fun readAllFacilities(): List { - val facilities = aboutRepository.findAllByPostTypeOrderByName("facilities").map { + val facilities = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.FACILITIES).map { AboutDto.of(it) } @@ -61,10 +65,20 @@ class AboutServiceImpl( @Transactional(readOnly = true) override fun readAllDirections(): List { - val directions = aboutRepository.findAllByPostTypeOrderByName("directions").map { + val directions = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.DIRECTIONS).map { AboutDto.of(it) } return directions } + + private fun makeStringToEnum(postType: String) : AboutPostType { + try { + val upperPostType = postType.replace("-","_").uppercase() + return AboutPostType.valueOf(upperPostType) + + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("해당하는 enum을 찾을 수 없습니다") + } + } } \ No newline at end of file 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 047a8850..c56e332b 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 @@ -1,6 +1,5 @@ package com.wafflestudio.csereal.core.academics.api -import com.wafflestudio.csereal.core.academics.database.StudentType import com.wafflestudio.csereal.core.academics.dto.CourseDto import com.wafflestudio.csereal.core.academics.dto.AcademicsDto import com.wafflestudio.csereal.core.academics.service.AcademicsService @@ -11,6 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RequestMapping("/academics") @@ -19,30 +19,28 @@ class AcademicsController( private val academicsService: AcademicsService ) { - // postType -> 학부 안내: guide, 필수 교양 과목: general-studies-requirements, - // 전공 이수 표준 형태: curriculum, 졸업 규청: degree-requirements, - // 교과목 변경 내역: course-changes, 장학제도: scholarship //Todo: 이미지, 파일 추가 필요 - @PostMapping("/{studentType}") + @PostMapping("/{studentType}/{postType}") fun createAcademics( - @PathVariable studentType: StudentType, + @PathVariable studentType: String, + @PathVariable postType: String, @Valid @RequestBody request: AcademicsDto ) : ResponseEntity { - return ResponseEntity.ok(academicsService.createAcademics(studentType, request)) + return ResponseEntity.ok(academicsService.createAcademics(studentType, postType, request)) } @GetMapping("/{studentType}/{postType}") fun readAcademics( - @PathVariable studentType: StudentType, + @PathVariable studentType: String, @PathVariable postType: String, ): ResponseEntity { return ResponseEntity.ok(academicsService.readAcademics(studentType, postType)) } - //교과목 정보: courses + //교과목 정보 @PostMapping("/{studentType}/course") fun createCourse( - @PathVariable studentType: StudentType, + @PathVariable studentType: String, @Valid @RequestBody request: CourseDto ) : ResponseEntity { return ResponseEntity.ok(academicsService.createCourse(studentType, request)) @@ -50,14 +48,14 @@ class AcademicsController( @GetMapping("/{studentType}/courses") fun readAllCourses( - @PathVariable studentType: StudentType, + @PathVariable studentType: String, ) : ResponseEntity> { return ResponseEntity.ok(academicsService.readAllCourses(studentType)) } - @GetMapping("/course/{name}") + @GetMapping("/course") fun readCourse( - @PathVariable name: String + @RequestParam name: String ): ResponseEntity { return ResponseEntity.ok(academicsService.readCourse(name)) } @@ -65,15 +63,15 @@ class AcademicsController( // 장학금 @PostMapping("/{studentType}/scholarship") fun createScholarship( - @PathVariable studentType: StudentType, + @PathVariable studentType: String, @Valid @RequestBody request: AcademicsDto ) : ResponseEntity { - return ResponseEntity.ok(academicsService.createAcademics(studentType, request)) + return ResponseEntity.ok(academicsService.createAcademics(studentType, "scholarship", request)) } - @GetMapping("/scholarship/{name}") + @GetMapping("/scholarship") fun readScholarship( - @PathVariable name: String + @RequestParam name: String ) : ResponseEntity { return ResponseEntity.ok(academicsService.readScholarship(name)) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt index 2f70cb24..18bc1a08 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsEntity.kt @@ -9,24 +9,22 @@ import jakarta.persistence.Enumerated @Entity(name = "academics") class AcademicsEntity( @Enumerated(EnumType.STRING) - var studentType: StudentType, + var studentType: AcademicsStudentType, - var postType: String, + @Enumerated(EnumType.STRING) + var postType: AcademicsPostType, var name: String, - var description: String, - var year: Int?, - var isPublic: Boolean, ): BaseTimeEntity() { companion object { - fun of(studentType: StudentType, academicsDto: AcademicsDto): AcademicsEntity { + fun of(studentType: AcademicsStudentType, postType: AcademicsPostType, academicsDto: AcademicsDto): AcademicsEntity { return AcademicsEntity( studentType = studentType, - postType = academicsDto.postType, + postType = postType, name = academicsDto.name, description = academicsDto.description, year = academicsDto.year, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsPostType.kt new file mode 100644 index 00000000..02a4a8ad --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsPostType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.academics.database + +enum class AcademicsPostType { + GUIDE, GENERAL_STUDIES_REQUIREMENTS, CURRICULUM, DEGREE_REQUIREMENTS, COURSE_CHANGES, SCHOLARSHIP +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt index fcd58f5c..e1f251c3 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsRepository.kt @@ -3,6 +3,6 @@ package com.wafflestudio.csereal.core.academics.database import org.springframework.data.jpa.repository.JpaRepository interface AcademicsRepository : JpaRepository { - fun findByStudentTypeAndPostType(studentType: StudentType, postType: String) : AcademicsEntity + fun findByStudentTypeAndPostType(studentType: AcademicsStudentType, postType: AcademicsPostType) : AcademicsEntity fun findByName(name: String): AcademicsEntity } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt new file mode 100644 index 00000000..c153b2a6 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/AcademicsStudentType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.academics.database + +enum class AcademicsStudentType { + UNDERGRADUATE, GRADUATE +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt index 74f4b0bb..5d410cad 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseEntity.kt @@ -8,7 +8,7 @@ import jakarta.persistence.Entity class CourseEntity( var isDeleted: Boolean = false, - var studentType: StudentType, + var studentType: AcademicsStudentType, var classification: String, @@ -25,7 +25,7 @@ class CourseEntity( var description: String? ): BaseTimeEntity() { companion object { - fun of(studentType: StudentType, courseDto: CourseDto): CourseEntity { + fun of(studentType: AcademicsStudentType, courseDto: CourseDto): CourseEntity { return CourseEntity( studentType = studentType, classification = courseDto.classification, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt index 777c3961..f8e017cc 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/CourseRepository.kt @@ -3,6 +3,6 @@ package com.wafflestudio.csereal.core.academics.database import org.springframework.data.jpa.repository.JpaRepository interface CourseRepository : JpaRepository { - fun findAllByStudentTypeOrderByYearAsc(studentType: StudentType) : List + fun findAllByStudentTypeOrderByYearAsc(studentType: AcademicsStudentType) : List fun findByName(name: String) : CourseEntity } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt deleted file mode 100644 index ebc750af..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/database/StudentType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wafflestudio.csereal.core.academics.database - -enum class StudentType { - GRADUATE,UNDERGRADUATE -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt index f62b2f4f..fadb6c5b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/dto/AcademicsDto.kt @@ -5,7 +5,6 @@ import java.time.LocalDateTime data class AcademicsDto( val id: Long, - val postType: String, val name: String, val description: String, val year: Int?, @@ -17,7 +16,6 @@ data class AcademicsDto( fun of(entity: AcademicsEntity) : AcademicsDto = entity.run { AcademicsDto( id = this.id, - postType = this.postType, name = this.name, description = this.description, year = this.year, 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 8e9e7105..4a2b7609 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsService.kt @@ -1,5 +1,7 @@ package com.wafflestudio.csereal.core.academics.service +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.about.database.AboutPostType import com.wafflestudio.csereal.core.academics.database.* import com.wafflestudio.csereal.core.academics.dto.CourseDto import com.wafflestudio.csereal.core.academics.dto.AcademicsDto @@ -7,10 +9,10 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface AcademicsService { - fun createAcademics(studentType: StudentType, request: AcademicsDto): AcademicsDto - fun readAcademics(studentType: StudentType, postType: String): AcademicsDto - fun readAllCourses(studentType: StudentType): List - fun createCourse(studentType: StudentType, request: CourseDto): CourseDto + fun createAcademics(studentType: String, postType: String, request: AcademicsDto): AcademicsDto + fun readAcademics(studentType: String, postType: String): AcademicsDto + fun createCourse(studentType: String, request: CourseDto): CourseDto + fun readAllCourses(studentType: String): List fun readCourse(name: String): CourseDto fun readScholarship(name:String): AcademicsDto } @@ -21,8 +23,11 @@ class AcademicsServiceImpl( private val courseRepository: CourseRepository, ) : AcademicsService { @Transactional - override fun createAcademics(studentType: StudentType, request: AcademicsDto): AcademicsDto { - val newAcademics = AcademicsEntity.of(studentType, request) + override fun createAcademics(studentType: String, postType: String, request: AcademicsDto): AcademicsDto { + val enumStudentType = makeStringToAcademicsStudentType(studentType) + val enumPostType = makeStringToAcademicsPostType(postType) + + val newAcademics = AcademicsEntity.of(enumStudentType, enumPostType, request) academicsRepository.save(newAcademics) @@ -30,41 +35,67 @@ class AcademicsServiceImpl( } @Transactional(readOnly = true) - override fun readAcademics(studentType: StudentType, postType: String): AcademicsDto { - val academics : AcademicsEntity = academicsRepository.findByStudentTypeAndPostType(studentType, postType) + override fun readAcademics(studentType: String, postType: String): AcademicsDto { + + val enumStudentType = makeStringToAcademicsStudentType(studentType) + val enumPostType = makeStringToAcademicsPostType(postType) + + val academics = academicsRepository.findByStudentTypeAndPostType(enumStudentType, enumPostType) return AcademicsDto.of(academics) } @Transactional - override fun readAllCourses(studentType: StudentType): List { - val courseDtoList = courseRepository.findAllByStudentTypeOrderByYearAsc(studentType).map { - CourseDto.of(it) - } - return courseDtoList - } - @Transactional - override fun createCourse(studentType: StudentType, request: CourseDto): CourseDto { - val course = CourseEntity.of(studentType, request) + override fun createCourse(studentType: String, request: CourseDto): CourseDto { + val enumStudentType = makeStringToAcademicsStudentType(studentType) + val course = CourseEntity.of(enumStudentType, request) courseRepository.save(course) return CourseDto.of(course) } - @Transactional + @Transactional(readOnly = true) + override fun readAllCourses(studentType: String): List { + val enumStudentType = makeStringToAcademicsStudentType(studentType) + + val courseDtoList = courseRepository.findAllByStudentTypeOrderByYearAsc(enumStudentType).map { + CourseDto.of(it) + } + return courseDtoList + } + + @Transactional(readOnly = true) override fun readCourse(name: String): CourseDto { - val course : CourseEntity = courseRepository.findByName(name) + val course = courseRepository.findByName(name) return CourseDto.of(course) } - @Transactional + @Transactional(readOnly = true) override fun readScholarship(name: String): AcademicsDto { - val scholarship : AcademicsEntity = academicsRepository.findByName(name) + val scholarship = academicsRepository.findByName(name) return AcademicsDto.of(scholarship) } + private fun makeStringToAcademicsStudentType(postType: String) : AcademicsStudentType { + try { + val upperPostType = postType.replace("-","_").uppercase() + return AcademicsStudentType.valueOf(upperPostType) + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("해당하는 enum을 찾을 수 없습니다") + } + } + + private fun makeStringToAcademicsPostType(postType: String) : AcademicsPostType { + try { + val upperPostType = postType.replace("-","_").uppercase() + return AcademicsPostType.valueOf(upperPostType) + + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("해당하는 enum을 찾을 수 없습니다") + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt index 0eee0bcc..de4f581e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/api/AdmissionsController.kt @@ -1,7 +1,5 @@ package com.wafflestudio.csereal.core.admissions.api -import com.wafflestudio.csereal.core.admissions.database.AdmissionPostType -import com.wafflestudio.csereal.core.admissions.database.StudentType import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto import com.wafflestudio.csereal.core.admissions.service.AdmissionsService import jakarta.validation.Valid @@ -19,26 +17,34 @@ import org.springframework.web.bind.annotation.RestController class AdmissionsController( private val admissionsService: AdmissionsService ) { - @PostMapping - fun createAdmissions( - @RequestParam studentType: StudentType, + @PostMapping("/undergraduate") + fun createUndergraduateAdmissions( + @RequestParam postType: String, @Valid @RequestBody request: AdmissionsDto ) : AdmissionsDto { - return admissionsService.createAdmissions(studentType, request) + return admissionsService.createUndergraduateAdmissions(postType, request) } - @GetMapping - fun readAdmissionsMain( - @RequestParam studentType: StudentType, - ) : ResponseEntity { - return ResponseEntity.ok(admissionsService.readAdmissionsMain(studentType)) + @PostMapping("/graduate") + fun createGraduateAdmissions( + @Valid @RequestBody request: AdmissionsDto + ) : AdmissionsDto { + return admissionsService.createGraduateAdmissions(request) } + @GetMapping("/undergraduate") fun readUndergraduateAdmissions( - @RequestParam postType: AdmissionPostType + @RequestParam postType: String ) : ResponseEntity { return ResponseEntity.ok(admissionsService.readUndergraduateAdmissions(postType)) } + @GetMapping("/graduate") + fun readGraduateAdmissions() : ResponseEntity { + return ResponseEntity.ok(admissionsService.readGraduateAdmissions()) + } + + + } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt deleted file mode 100644 index 493f01c0..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionPostType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wafflestudio.csereal.core.admissions.database - -enum class AdmissionPostType { - MAIN, EARLY_ADMISSION, REGULAR_ADMISSION, -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt index b6fd07d5..e12e9716 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsEntity.kt @@ -9,21 +9,16 @@ import jakarta.persistence.Enumerated @Entity(name = "admissions") class AdmissionsEntity( @Enumerated(EnumType.STRING) - var studentType: StudentType, - @Enumerated(EnumType.STRING) - val postType: AdmissionPostType, + val postType: AdmissionsPostType, val title: String, val description: String, - val isPublic: Boolean, ): BaseTimeEntity() { companion object { - fun of(studentType: StudentType, admissionsDto: AdmissionsDto) : AdmissionsEntity { + fun of(postType: AdmissionsPostType, admissionsDto: AdmissionsDto) : AdmissionsEntity { return AdmissionsEntity( - studentType = studentType, - postType = admissionsDto.postType, + postType = postType, title = admissionsDto.title, description = admissionsDto.description, - isPublic = admissionsDto.isPublic ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt new file mode 100644 index 00000000..104cf01b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsPostType.kt @@ -0,0 +1,5 @@ +package com.wafflestudio.csereal.core.admissions.database + +enum class AdmissionsPostType { + GRADUATE, UNDERGRADUATE_EARLY_ADMISSION, UNDERGRADUATE_REGULAR_ADMISSION, +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt index 74c3f1e8..f4e2c1b0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/AdmissionsRepository.kt @@ -3,6 +3,5 @@ package com.wafflestudio.csereal.core.admissions.database import org.springframework.data.jpa.repository.JpaRepository interface AdmissionsRepository : JpaRepository { - fun findByStudentTypeAndPostType(studentType: StudentType, postType: AdmissionPostType): AdmissionsEntity - fun findByPostType(postType: AdmissionPostType) : AdmissionsEntity + fun findByPostType(postType: AdmissionsPostType) : AdmissionsEntity } \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt deleted file mode 100644 index b549da95..00000000 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/database/StudentType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.wafflestudio.csereal.core.admissions.database - -enum class StudentType { - GRADUATE, UNDERGRADUATE -} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt index cce8196b..c49c7e7a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/dto/AdmissionsDto.kt @@ -1,28 +1,23 @@ package com.wafflestudio.csereal.core.admissions.dto -import com.wafflestudio.csereal.core.admissions.database.AdmissionPostType import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity import java.time.LocalDateTime data class AdmissionsDto( val id: Long, - val postType: AdmissionPostType, val title: String, val description: String, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, - val isPublic: Boolean, ) { companion object { fun of(entity: AdmissionsEntity) : AdmissionsDto = entity.run { AdmissionsDto( id = this.id, - postType = this.postType, title = this.title, description = this.description, createdAt = this.createdAt, modifiedAt = this.modifiedAt, - isPublic = this.isPublic ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt index d8289811..e79a67cf 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt @@ -1,17 +1,18 @@ package com.wafflestudio.csereal.core.admissions.service -import com.wafflestudio.csereal.core.admissions.database.AdmissionPostType +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.admissions.database.AdmissionsPostType import com.wafflestudio.csereal.core.admissions.database.AdmissionsEntity import com.wafflestudio.csereal.core.admissions.database.AdmissionsRepository -import com.wafflestudio.csereal.core.admissions.database.StudentType import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface AdmissionsService { - fun createAdmissions(studentType: StudentType, request: AdmissionsDto): AdmissionsDto - fun readAdmissionsMain(studentType: StudentType): AdmissionsDto - fun readUndergraduateAdmissions(postType: AdmissionPostType): AdmissionsDto + fun createUndergraduateAdmissions(postType: String, request: AdmissionsDto): AdmissionsDto + fun createGraduateAdmissions(request: AdmissionsDto): AdmissionsDto + fun readUndergraduateAdmissions(postType: String): AdmissionsDto + fun readGraduateAdmissions(): AdmissionsDto } @@ -20,8 +21,19 @@ class AdmissionsServiceImpl( private val admissionsRepository: AdmissionsRepository ) : AdmissionsService { @Transactional - override fun createAdmissions(studentType: StudentType, request: AdmissionsDto): AdmissionsDto { - val newAdmissions: AdmissionsEntity = AdmissionsEntity.of(studentType, request) + override fun createUndergraduateAdmissions(postType: String, request: AdmissionsDto): AdmissionsDto { + val enumPostType = makeStringToAdmissionsPostType(postType) + + val newAdmissions = AdmissionsEntity.of(enumPostType, request) + + admissionsRepository.save(newAdmissions) + + return AdmissionsDto.of(newAdmissions) + } + + @Transactional + override fun createGraduateAdmissions(request: AdmissionsDto): AdmissionsDto { + val newAdmissions: AdmissionsEntity = AdmissionsEntity.of(AdmissionsPostType.GRADUATE, request) admissionsRepository.save(newAdmissions) @@ -29,22 +41,26 @@ class AdmissionsServiceImpl( } @Transactional(readOnly = true) - override fun readAdmissionsMain(studentType: StudentType): AdmissionsDto { - return if (studentType == StudentType.UNDERGRADUATE) { - AdmissionsDto.of(admissionsRepository.findByStudentTypeAndPostType(StudentType.UNDERGRADUATE, AdmissionPostType.MAIN)) - } else { - AdmissionsDto.of(admissionsRepository.findByStudentTypeAndPostType(StudentType.GRADUATE, AdmissionPostType.MAIN)) + override fun readUndergraduateAdmissions(postType: String): AdmissionsDto { + return when (postType) { + "early" -> AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionsPostType.UNDERGRADUATE_EARLY_ADMISSION)) + "regular" -> AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionsPostType.UNDERGRADUATE_REGULAR_ADMISSION)) + else -> throw CserealException.Csereal404("해당하는 페이지를 찾을 수 없습니다.") } } @Transactional(readOnly = true) - override fun readUndergraduateAdmissions(postType: AdmissionPostType): AdmissionsDto { - return if (postType == AdmissionPostType.EARLY_ADMISSION) { - AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionPostType.EARLY_ADMISSION)) - } else { - AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionPostType.REGULAR_ADMISSION)) - } + override fun readGraduateAdmissions(): AdmissionsDto { + return AdmissionsDto.of(admissionsRepository.findByPostType(AdmissionsPostType.GRADUATE)) } + private fun makeStringToAdmissionsPostType(postType: String) : AdmissionsPostType { + try { + val upperPostType = postType.replace("-","_").uppercase() + return AdmissionsPostType.valueOf(upperPostType) + } catch (e: IllegalArgumentException) { + throw CserealException.Csereal400("해당하는 enum을 찾을 수 없습니다") + } + } } \ No newline at end of file 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 new file mode 100644 index 00000000..7aa259d1 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt @@ -0,0 +1,18 @@ +package com.wafflestudio.csereal.core.main.api + +import com.wafflestudio.csereal.core.main.dto.MainResponse +import com.wafflestudio.csereal.core.main.service.MainService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RequestMapping +@RestController +class MainController( + private val mainService: MainService, +) { + @GetMapping + fun readMain() : MainResponse { + return mainService.readMain() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/database/MainRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/database/MainRepository.kt new file mode 100644 index 00000000..f7fcf906 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/database/MainRepository.kt @@ -0,0 +1,70 @@ +package com.wafflestudio.csereal.core.main.database + +import com.querydsl.core.QueryFactory +import com.querydsl.core.types.Projections +import com.querydsl.jpa.impl.JPAQueryFactory +import com.sun.tools.javac.Main +import com.wafflestudio.csereal.core.main.dto.MainResponse +import com.wafflestudio.csereal.core.main.dto.NewsResponse +import com.wafflestudio.csereal.core.main.dto.NoticeResponse +import com.wafflestudio.csereal.core.news.database.QNewsEntity.newsEntity +import com.wafflestudio.csereal.core.notice.database.QNoticeEntity.noticeEntity +import com.wafflestudio.csereal.core.notice.database.QNoticeTagEntity.noticeTagEntity +import com.wafflestudio.csereal.core.notice.database.QTagInNoticeEntity.tagInNoticeEntity + +import org.springframework.stereotype.Component + +interface MainRepository { + fun readMainSlide(): List + fun readMainNoticeTotal(): List + fun readMainNoticeTag(tag: String): List +} + +@Component +class MainRepositoryImpl( + private val queryFactory: JPAQueryFactory, +) : MainRepository { + override fun readMainSlide(): List { + return queryFactory.select( + Projections.constructor( + NewsResponse::class.java, + newsEntity.id, + newsEntity.title, + newsEntity.createdAt + ) + ).from(newsEntity) + .where(newsEntity.isDeleted.eq(false), newsEntity.isPublic.eq(true), newsEntity.isSlide.eq(true)) + .orderBy(newsEntity.isPinned.desc()).orderBy(newsEntity.createdAt.desc()) + .limit(20).fetch() + } + + override fun readMainNoticeTotal(): List { + return queryFactory.select( + Projections.constructor( + NoticeResponse::class.java, + noticeEntity.id, + noticeEntity.title, + noticeEntity.createdAt + ) + ).from(noticeEntity) + .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) + .orderBy(noticeEntity.isPinned.desc()).orderBy(noticeEntity.createdAt.desc()) + .limit(6).fetch() + } + override fun readMainNoticeTag(tag: String): List { + return queryFactory.select( + Projections.constructor( + NoticeResponse::class.java, + noticeTagEntity.notice.id, + noticeTagEntity.notice.title, + noticeTagEntity.notice.createdAt, + ) + ).from(noticeTagEntity) + .rightJoin(noticeEntity).on(noticeTagEntity.notice.eq(noticeEntity)) + .rightJoin(tagInNoticeEntity).on(noticeTagEntity.tag.eq(tagInNoticeEntity)) + .where(noticeTagEntity.tag.name.eq(tag)) + .where(noticeEntity.isDeleted.eq(false), noticeEntity.isPublic.eq(true)) + .orderBy(noticeEntity.isPinned.desc()).orderBy(noticeEntity.createdAt.desc()) + .limit(6).distinct().fetch() + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/MainResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/MainResponse.kt new file mode 100644 index 00000000..dc7a98ce --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/MainResponse.kt @@ -0,0 +1,11 @@ +package com.wafflestudio.csereal.core.main.dto + +data class MainResponse( + val slide: List, + val noticeTotal: List, + val noticeAdmissions: List, + val noticeUndergraduate: List, + val noticeGraduate: List +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NewsResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NewsResponse.kt new file mode 100644 index 00000000..688c9627 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NewsResponse.kt @@ -0,0 +1,12 @@ +package com.wafflestudio.csereal.core.main.dto + +import com.querydsl.core.annotations.QueryProjection +import java.time.LocalDateTime + +data class NewsResponse @QueryProjection constructor( + val id: Long, + val title: String, + val createdAt: LocalDateTime? +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NoticeResponse.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NoticeResponse.kt new file mode 100644 index 00000000..ed64b596 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/dto/NoticeResponse.kt @@ -0,0 +1,11 @@ +package com.wafflestudio.csereal.core.main.dto + +import com.querydsl.core.annotations.QueryProjection +import java.time.LocalDateTime + +data class NoticeResponse @QueryProjection constructor( + val id: Long, + val title: String, + val createdAt: LocalDateTime?, +){ +} \ No newline at end of file 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 new file mode 100644 index 00000000..67a05b9b --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.csereal.core.main.service + +import com.wafflestudio.csereal.core.main.database.MainRepository +import com.wafflestudio.csereal.core.main.dto.MainResponse +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +interface MainService { + fun readMain() : MainResponse +} + +@Service +class MainServiceImpl( + private val mainRepository: MainRepository +) : MainService { + @Transactional(readOnly = true) + override fun readMain(): MainResponse { + val slide = mainRepository.readMainSlide() + val noticeTotal = mainRepository.readMainNoticeTotal() + val noticeAdmissions = mainRepository.readMainNoticeTag("admissions") + val noticeUndergraduate = mainRepository.readMainNoticeTag("undergraduate") + val noticeGraduate = mainRepository.readMainNoticeTag("graduate") + return MainResponse(slide, noticeTotal, noticeAdmissions, noticeUndergraduate, noticeGraduate) + } +} \ No newline at end of file From cc256bff5736b43f150281c11ae9d8991a46827a Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Fri, 25 Aug 2023 00:45:15 +0900 Subject: [PATCH 24/27] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EB=B0=8F=20=EC=A0=95=EA=B8=B0=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=20API=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: oidc 로그인 * feat: TaskEntity name 추가 * feat: 배포 설정 * feat: 유저 정보에 학번 추가, 로그인 시 sub claim 확인 * feat: 배포 테스트 위해 redirect-uri 변경 * fix: groups claim 소문자로 수정 * feat: 예약 엔티티 및 DTO 설계 * feat: 일반 예약 및 정기 예약 --- .../wafflestudio/csereal/common/Exceptions.kt | 1 + .../reservation/api/ReservceController.kt | 53 ++++++++++++ .../reservation/database/ReservationEntity.kt | 69 ++++++++++++++++ .../database/ReservationRepository.kt | 15 ++++ .../core/reservation/database/RoomEntity.kt | 21 +++++ .../reservation/database/RoomRepository.kt | 6 ++ .../core/reservation/dto/ReservationDto.kt | 34 ++++++++ .../core/reservation/dto/ReserveRequest.kt | 14 ++++ .../reservation/service/ReservationService.kt | 81 +++++++++++++++++++ 9 files changed, 294 insertions(+) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt index 2809d4f6..ead4693b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/Exceptions.kt @@ -6,4 +6,5 @@ open class CserealException(msg: String, val status: HttpStatus) : RuntimeExcept class Csereal400(msg: String) : CserealException(msg, HttpStatus.BAD_REQUEST) 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) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt new file mode 100644 index 00000000..82e9962a --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt @@ -0,0 +1,53 @@ +package com.wafflestudio.csereal.core.reservation.api + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.reservation.dto.ReservationDto +import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest +import com.wafflestudio.csereal.core.reservation.service.ReservationService +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime +import java.util.UUID + +@RequestMapping("/reservation") +@RestController +class ReservationController( + private val reservationService: ReservationService +) { + + @PostMapping + fun reserveRoom( + @AuthenticationPrincipal principal: OidcUser?, + @RequestBody reserveRequest: ReserveRequest + ): List { + if (principal == null) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + val username = principal.idToken.getClaim("username") + return reservationService.reserveRoom(username, reserveRequest) + } + + @DeleteMapping("/{reservationId}") + fun cancelSpecific(@AuthenticationPrincipal principal: OidcUser?, @PathVariable reservationId: Long) { + if (principal == null) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + reservationService.cancelSpecific(reservationId) + } + + @DeleteMapping("/recurring/{recurrenceId}") + fun cancelRecurring(@AuthenticationPrincipal principal: OidcUser?, @PathVariable recurrenceId: UUID) { + if (principal == null) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + reservationService.cancelRecurring(recurrenceId) + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt new file mode 100644 index 00000000..8412f229 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationEntity.kt @@ -0,0 +1,69 @@ +package com.wafflestudio.csereal.core.reservation.database + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest +import com.wafflestudio.csereal.core.user.database.UserEntity +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.PrePersist +import jakarta.persistence.PreUpdate +import java.time.LocalDateTime +import java.util.* + +@Entity(name = "reservation") +class ReservationEntity( + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + val user: UserEntity, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id") + val room: RoomEntity, + + val title: String, + val contactEmail: String, + val contactPhone: String, + val purpose: String, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + + val recurrenceId: UUID? = null + +) : BaseTimeEntity() { + + @PrePersist + @PreUpdate + fun validateDates() { + if (startTime.isAfter(endTime) || startTime.isEqual(endTime)) { + throw CserealException.Csereal400("종료 시각은 시작 시각 이후여야 합니다.") + } + } + + companion object { + fun create( + user: UserEntity, + room: RoomEntity, + reserveRequest: ReserveRequest, + start: LocalDateTime, + end: LocalDateTime, + recurrenceId: UUID + ): ReservationEntity { + return ReservationEntity( + user = user, + room = room, + title = reserveRequest.title, + contactEmail = reserveRequest.contactEmail, + contactPhone = reserveRequest.contactPhone, + purpose = reserveRequest.purpose, + startTime = start, + endTime = end, + recurrenceId = recurrenceId + ) + } + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt new file mode 100644 index 00000000..827db80d --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.csereal.core.reservation.database + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime +import java.util.UUID + +interface ReservationRepository : JpaRepository { + + @Query("SELECT r FROM reservation r WHERE r.room.id = :roomId AND ((:start <= r.startTime AND r.startTime < :end) OR (:start < r.endTime AND r.endTime <= :end) OR (r.startTime <= :start AND r.endTime >= :end))") + fun findByRoomIdAndTimeOverlap(roomId: Long, start: LocalDateTime, end: LocalDateTime): List + + fun deleteAllByRecurrenceId(recurrenceId: UUID) + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt new file mode 100644 index 00000000..f76d1c48 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomEntity.kt @@ -0,0 +1,21 @@ +package com.wafflestudio.csereal.core.reservation.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Entity(name = "room") +class RoomEntity( + val name: String?, + val location: String, + + val capacity: Int, + + @Enumerated(EnumType.STRING) + val type: RoomType +) : BaseTimeEntity() + +enum class RoomType { + SEMINAR, LAB, LECTURE +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt new file mode 100644 index 00000000..c7c9b562 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/RoomRepository.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.csereal.core.reservation.database + +import org.springframework.data.jpa.repository.JpaRepository + +interface RoomRepository : JpaRepository { +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt new file mode 100644 index 00000000..b868d4b2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt @@ -0,0 +1,34 @@ +package com.wafflestudio.csereal.core.reservation.dto + +import com.wafflestudio.csereal.core.reservation.database.ReservationEntity +import java.time.LocalDateTime + +data class ReservationDto( + val id: Long, + val title: String, + val purpose: String, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + val roomName: String?, + val roomLocation: String, + val userName: String, + val contactEmail: String, + val contactPhone: String +) { + companion object { + fun of(reservationEntity: ReservationEntity): ReservationDto { + return ReservationDto( + id = reservationEntity.id, + title = reservationEntity.title, + purpose = reservationEntity.purpose, + startTime = reservationEntity.startTime, + endTime = reservationEntity.endTime, + roomName = reservationEntity.room.name, + roomLocation = reservationEntity.room.location, + userName = reservationEntity.user.username, + contactEmail = reservationEntity.contactEmail, + contactPhone = reservationEntity.contactPhone + ) + } + } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt new file mode 100644 index 00000000..55befd76 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReserveRequest.kt @@ -0,0 +1,14 @@ +package com.wafflestudio.csereal.core.reservation.dto + +import java.time.LocalDateTime + +data class ReserveRequest( + val roomId: Long, + val title: String, + val contactEmail: String, + val contactPhone: String, + val purpose: String, + val startTime: LocalDateTime, + val endTime: LocalDateTime, + val recurringWeeks: Int? = null +) 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 new file mode 100644 index 00000000..8adba80c --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/service/ReservationService.kt @@ -0,0 +1,81 @@ +package com.wafflestudio.csereal.core.reservation.service + +import com.wafflestudio.csereal.common.CserealException +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.user.database.Role +import com.wafflestudio.csereal.core.user.database.UserRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.util.* + +interface ReservationService { + fun reserveRoom( + username: String, + reserveRequest: ReserveRequest + ): List + + fun cancelSpecific(reservationId: Long) + fun cancelRecurring(recurrenceId: UUID) +} + +@Service +@Transactional +class ReservationServiceImpl( + private val reservationRepository: ReservationRepository, + private val userRepository: UserRepository, + private val roomRepository: RoomRepository +) : ReservationService { + + override fun reserveRoom( + username: String, + reserveRequest: ReserveRequest + ): List { + val user = userRepository.findByUsername(username) ?: throw CserealException.Csereal404("재로그인이 필요합니다.") + val room = + roomRepository.findByIdOrNull(reserveRequest.roomId) ?: throw CserealException.Csereal404("Room Not Found") + + if (user.role == null) { + throw CserealException.Csereal401("권한이 없습니다.") + } + + val reservations = mutableListOf() + + val recurrenceId = UUID.randomUUID() + + val numberOfWeeks = reserveRequest.recurringWeeks ?: 1 + + for (week in 0 until numberOfWeeks) { + val start = reserveRequest.startTime.plusWeeks(week.toLong()) + val end = reserveRequest.endTime.plusWeeks(week.toLong()) + + // 중복 예약 방지 + val overlappingReservations = reservationRepository.findByRoomIdAndTimeOverlap( + reserveRequest.roomId, + start, + end + ) + if (overlappingReservations.isNotEmpty()) { + throw CserealException.Csereal409("${week}주차 해당 시간에 이미 예약이 있습니다.") + } + + val newReservation = ReservationEntity.create(user, room, reserveRequest, start, end, recurrenceId) + reservations.add(newReservation) + } + + reservationRepository.saveAll(reservations) + + return reservations.map { ReservationDto.of(it) } + } + + override fun cancelSpecific(reservationId: Long) { + reservationRepository.deleteById(reservationId) + } + + override fun cancelRecurring(recurrenceId: UUID) { + reservationRepository.deleteAllByRecurrenceId(recurrenceId) + } + +} From 13161c9c580474376abb1259d7d2c0b2a908b0ca Mon Sep 17 00:00:00 2001 From: Junhyeong Kim Date: Tue, 29 Aug 2023 03:00:43 +0900 Subject: [PATCH 25/27] =?UTF-8?q?feat:=20=EC=98=88=EC=95=BD=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 권한 관리 위해 커스텀 어노테이션 생성 * feat: 예약 단건, 주별, 월별 조회 API * fix: 로컬 DB 설정 수정 * feat: 유저 조회 중복 쿼리 방지 * feat: 예약 응답에 recurrenceId 추가 * feat: 동시 예약 방지 * fix: 시큐리티 로그 다시 로컬에서만 보이도록 변경 * feat: 목데이터 날라가지 않도록 ddl-auto 수정 --- docker-compose-local.yml | 1 - .../csereal/common/aop/Authenticated.kt | 9 +++ .../csereal/common/aop/SecurityAspect.kt | 53 +++++++++++++++ .../reservation/api/ReservceController.kt | 66 +++++++++++++------ .../database/ReservationRepository.kt | 9 +++ .../core/reservation/dto/ReservationDto.kt | 3 + .../reservation/service/ReservationService.kt | 54 ++++++++++----- src/main/resources/application.yaml | 14 ++-- 8 files changed, 166 insertions(+), 43 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/aop/Authenticated.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt diff --git a/docker-compose-local.yml b/docker-compose-local.yml index ad08265a..39e075e9 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -11,7 +11,6 @@ services: - '3306:3306' volumes: - db:/var/lib/mysql - - $PWD/db/init.sql:/docker-entrypoint-initdb.d/init.sql volumes: db: driver: local diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/aop/Authenticated.kt b/src/main/kotlin/com/wafflestudio/csereal/common/aop/Authenticated.kt new file mode 100644 index 00000000..fc1dc109 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/aop/Authenticated.kt @@ -0,0 +1,9 @@ +package com.wafflestudio.csereal.common.aop + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class AuthenticatedStaff + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class AuthenticatedForReservation diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt b/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt new file mode 100644 index 00000000..99dfe6e2 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/aop/SecurityAspect.kt @@ -0,0 +1,53 @@ +package com.wafflestudio.csereal.common.aop + +import com.wafflestudio.csereal.common.CserealException +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.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestAttributes +import org.springframework.web.context.request.RequestContextHolder + +@Aspect +@Component +class SecurityAspect(private val userRepository: UserRepository) { + + @Before("@annotation(AuthenticatedStaff)") + fun checkStaffAuthentication() { + val user = getLoginUser() + + if (user.role != Role.ROLE_STAFF) { + throw CserealException.Csereal401("권한이 없습니다.") + } + } + + @Before("@annotation(AuthenticatedForReservation)") + fun checkReservationAuthentication() { + val user = getLoginUser() + + if (user.role == null) { + throw CserealException.Csereal401("권한이 없습니다.") + } + } + + private fun getLoginUser(): UserEntity { + val authentication = SecurityContextHolder.getContext().authentication + val principal = authentication.principal + + if (principal !is OidcUser) { + throw CserealException.Csereal401("로그인이 필요합니다.") + } + + val username = principal.idToken.getClaim("username") + val user = userRepository.findByUsername(username) ?: throw CserealException.Csereal404("재로그인이 필요합니다.") + + RequestContextHolder.getRequestAttributes()?.setAttribute("loggedInUser", user, RequestAttributes.SCOPE_REQUEST) + + return user + } + +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt index 82e9962a..e0dff854 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/api/ReservceController.kt @@ -1,18 +1,21 @@ package com.wafflestudio.csereal.core.reservation.api -import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.aop.AuthenticatedForReservation +import com.wafflestudio.csereal.common.aop.AuthenticatedStaff import com.wafflestudio.csereal.core.reservation.dto.ReservationDto import com.wafflestudio.csereal.core.reservation.dto.ReserveRequest import com.wafflestudio.csereal.core.reservation.service.ReservationService -import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.security.oauth2.core.oidc.user.OidcUser +import org.springframework.format.annotation.DateTimeFormat +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate import java.time.LocalDateTime import java.util.UUID @@ -22,32 +25,55 @@ class ReservationController( private val reservationService: ReservationService ) { + @GetMapping("/month") + @AuthenticatedForReservation + fun getMonthlyReservations( + @RequestParam roomId: Long, + @RequestParam year: Int, + @RequestParam month: Int + ): ResponseEntity> { + val start = LocalDateTime.of(year, month, 1, 0, 0) + val end = start.plusMonths(1) + return ResponseEntity.ok(reservationService.getRoomReservationsBetween(roomId, start, end)) + } + + @GetMapping("/week") + @AuthenticatedForReservation + fun getWeeklyReservations( + @RequestParam roomId: Long, + @RequestParam year: Int, + @RequestParam month: Int, + @RequestParam day: Int, + ): ResponseEntity> { + val start = LocalDateTime.of(year, month, day, 0, 0) + val end = start.plusDays(7) + return ResponseEntity.ok(reservationService.getRoomReservationsBetween(roomId, start, end)) + } + + @GetMapping("/{reservationId}") + @AuthenticatedStaff + fun getReservation(@PathVariable reservationId: Long): ResponseEntity { + return ResponseEntity.ok(reservationService.getReservation(reservationId)) + } + @PostMapping + @AuthenticatedForReservation fun reserveRoom( - @AuthenticationPrincipal principal: OidcUser?, @RequestBody reserveRequest: ReserveRequest - ): List { - if (principal == null) { - throw CserealException.Csereal401("로그인이 필요합니다.") - } - val username = principal.idToken.getClaim("username") - return reservationService.reserveRoom(username, reserveRequest) + ): ResponseEntity> { + return ResponseEntity.ok(reservationService.reserveRoom(reserveRequest)) } @DeleteMapping("/{reservationId}") - fun cancelSpecific(@AuthenticationPrincipal principal: OidcUser?, @PathVariable reservationId: Long) { - if (principal == null) { - throw CserealException.Csereal401("로그인이 필요합니다.") - } - reservationService.cancelSpecific(reservationId) + @AuthenticatedForReservation + fun cancelSpecific(@PathVariable reservationId: Long): ResponseEntity { + return ResponseEntity.ok(reservationService.cancelSpecific(reservationId)) } @DeleteMapping("/recurring/{recurrenceId}") - fun cancelRecurring(@AuthenticationPrincipal principal: OidcUser?, @PathVariable recurrenceId: UUID) { - if (principal == null) { - throw CserealException.Csereal401("로그인이 필요합니다.") - } - reservationService.cancelRecurring(recurrenceId) + @AuthenticatedForReservation + fun cancelRecurring(@PathVariable recurrenceId: UUID): ResponseEntity { + return ResponseEntity.ok(reservationService.cancelRecurring(recurrenceId)) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt index 827db80d..dd750501 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/database/ReservationRepository.kt @@ -1,15 +1,24 @@ package com.wafflestudio.csereal.core.reservation.database +import jakarta.persistence.LockModeType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query import java.time.LocalDateTime import java.util.UUID interface ReservationRepository : JpaRepository { + @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT r FROM reservation r WHERE r.room.id = :roomId AND ((:start <= r.startTime AND r.startTime < :end) OR (:start < r.endTime AND r.endTime <= :end) OR (r.startTime <= :start AND r.endTime >= :end))") fun findByRoomIdAndTimeOverlap(roomId: Long, start: LocalDateTime, end: LocalDateTime): List + fun findByRoomIdAndStartTimeBetweenOrderByStartTimeAsc( + roomId: Long, + start: LocalDateTime, + end: LocalDateTime + ): List + fun deleteAllByRecurrenceId(recurrenceId: UUID) } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt index b868d4b2..f3c3ff3e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/reservation/dto/ReservationDto.kt @@ -2,9 +2,11 @@ package com.wafflestudio.csereal.core.reservation.dto import com.wafflestudio.csereal.core.reservation.database.ReservationEntity import java.time.LocalDateTime +import java.util.UUID data class ReservationDto( val id: Long, + val recurrenceId: UUID?, val title: String, val purpose: String, val startTime: LocalDateTime, @@ -19,6 +21,7 @@ data class ReservationDto( fun of(reservationEntity: ReservationEntity): ReservationDto { return ReservationDto( id = reservationEntity.id, + recurrenceId = reservationEntity.recurrenceId, title = reservationEntity.title, purpose = reservationEntity.purpose, startTime = reservationEntity.startTime, 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 8adba80c..5941bd4b 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 @@ -4,19 +4,22 @@ import com.wafflestudio.csereal.common.CserealException 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.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 +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 +import java.time.LocalDateTime import java.util.* interface ReservationService { - fun reserveRoom( - username: String, - reserveRequest: ReserveRequest - ): List - + fun reserveRoom(reserveRequest: ReserveRequest): List + fun getRoomReservationsBetween(roomId: Long, start: LocalDateTime, end: LocalDateTime): List + fun getReservation(reservationId: Long): ReservationDto fun cancelSpecific(reservationId: Long) fun cancelRecurring(recurrenceId: UUID) } @@ -29,18 +32,22 @@ class ReservationServiceImpl( private val roomRepository: RoomRepository ) : ReservationService { - override fun reserveRoom( - username: String, - reserveRequest: ReserveRequest - ): List { - val user = userRepository.findByUsername(username) ?: throw CserealException.Csereal404("재로그인이 필요합니다.") - val room = - roomRepository.findByIdOrNull(reserveRequest.roomId) ?: throw CserealException.Csereal404("Room Not Found") + override fun reserveRoom(reserveRequest: ReserveRequest): List { + var user = RequestContextHolder.getRequestAttributes()?.getAttribute( + "loggedInUser", + RequestAttributes.SCOPE_REQUEST + ) as UserEntity? - if (user.role == null) { - throw CserealException.Csereal401("권한이 없습니다.") + 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 room = + roomRepository.findByIdOrNull(reserveRequest.roomId) ?: throw CserealException.Csereal404("Room Not Found") + val reservations = mutableListOf() val recurrenceId = UUID.randomUUID() @@ -70,6 +77,23 @@ class ReservationServiceImpl( return reservations.map { ReservationDto.of(it) } } + @Transactional(readOnly = true) + override fun getRoomReservationsBetween( + roomId: Long, + start: LocalDateTime, + end: LocalDateTime + ): List { + return reservationRepository.findByRoomIdAndStartTimeBetweenOrderByStartTimeAsc(roomId, start, end) + .map { ReservationDto.of(it) } + } + + @Transactional(readOnly = true) + override fun getReservation(reservationId: Long): ReservationDto { + val reservationEntity = + reservationRepository.findByIdOrNull(reservationId) ?: throw CserealException.Csereal404("예약을 찾을 수 없습니다.") + return ReservationDto.of(reservationEntity) + } + override fun cancelSpecific(reservationId: Long) { reservationRepository.deleteById(reservationId) } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b49fa8e6..20bcae44 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -29,12 +29,6 @@ springdoc: api-docs: path: /api-docs/json -logging.level: - default: INFO - org: - springframework: - security: DEBUG - --- spring: config.activate.on-profile: local @@ -48,10 +42,16 @@ spring: show-sql: true open-in-view: false +logging.level: + default: INFO + org: + springframework: + security: DEBUG + --- spring: config.activate.on-profile: prod jpa: hibernate: - ddl-auto: create # TODO: change to validate (or none) when save actual data to server + ddl-auto: update # TODO: change to validate (or none) when save actual data to server open-in-view: false From 191f6f6bd3140e3c5e9b0b6f1a645e9ea0da83ba Mon Sep 17 00:00:00 2001 From: Jo Seonggyu Date: Tue, 29 Aug 2023 22:15:45 +0900 Subject: [PATCH 26/27] =?UTF-8?q?feat:=20about,=20member,=20news,=20semina?= =?UTF-8?q?r=20=EB=A9=94=EC=9D=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: uploadImage 추가 * feat: about, member 사진 업로드 추가 * feat: news, seminar 사진 업로드 추가 * refactor: imageEntity 추가 리팩토링 * fix: gif 삭제 * fix: application.yaml 수정 * fix: newsService 태그 -> 이미지로 순서 변경 * fix: pr 리뷰 수정 * fix: extension 없애고, mainImage로 바꾸고, uuid 없애기 * fix: var 삭제 --------- Co-authored-by: Junhyeong Kim --- build.gradle.kts | 6 + .../common/controller/ContentEntityType.kt | 7 ++ .../csereal/core/about/api/AboutController.kt | 14 +-- .../core/about/database/AboutEntity.kt | 12 +- .../csereal/core/about/dto/AboutDto.kt | 8 +- .../core/about/service/AboutService.kt | 28 +++-- .../core/member/api/ProfessorController.kt | 8 +- .../core/member/api/StaffController.kt | 17 ++- .../core/member/database/ProfessorEntity.kt | 11 +- .../core/member/database/StaffEntity.kt | 11 +- .../csereal/core/member/dto/ProfessorDto.kt | 7 +- .../core/member/dto/SimpleProfessorDto.kt | 7 +- .../csereal/core/member/dto/SimpleStaffDto.kt | 6 +- .../csereal/core/member/dto/StaffDto.kt | 7 +- .../core/member/service/ProfessorService.kt | 40 ++++-- .../core/member/service/StaffService.kt | 31 +++-- .../csereal/core/news/api/NewsController.kt | 6 +- .../csereal/core/news/database/NewsEntity.kt | 24 +++- .../csereal/core/news/dto/NewsDto.kt | 5 +- .../csereal/core/news/service/NewsService.kt | 35 +++--- .../mainImage/api/MainImageController.kt | 13 ++ .../mainImage/database/MainImageEntity.kt | 19 +++ .../mainImage/database/MainImageRepository.kt | 8 ++ .../resource/mainImage/dto/MainImageDto.kt | 8 ++ .../mainImage/service/MainImageService.kt | 118 ++++++++++++++++++ .../core/seminar/api/SeminarController.kt | 7 +- .../core/seminar/database/SeminarEntity.kt | 16 ++- .../csereal/core/seminar/dto/SeminarDto.kt | 9 +- .../core/seminar/service/SeminarService.kt | 26 +++- src/main/resources/application.yaml | 18 +++ 30 files changed, 419 insertions(+), 113 deletions(-) create mode 100644 src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/api/MainImageController.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageRepository.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/dto/MainImageDto.kt create mode 100644 src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 575460ed..60751723 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,12 @@ dependencies { // 태그 제거 implementation("org.jsoup:jsoup:1.15.4") + // 이미지 업로드 + implementation("commons-io:commons-io:2.11.0") + + // 썸네일 보여주기 + implementation("net.coobird:thumbnailator:0.4.19") + } noArg { annotation("jakarta.persistence.Entity") diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt b/src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt new file mode 100644 index 00000000..0631bd6e --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/common/controller/ContentEntityType.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.csereal.common.controller + +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity + +interface ContentEntityType { + fun bringMainImage(): MainImageEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt index 4a1c87c5..07e25126 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/api/AboutController.kt @@ -4,13 +4,8 @@ import com.wafflestudio.csereal.core.about.dto.AboutDto import com.wafflestudio.csereal.core.about.service.AboutService import jakarta.validation.Valid import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RequestMapping("/about") @RestController @@ -25,9 +20,10 @@ class AboutController( @PostMapping("/{postType}") fun createAbout( @PathVariable postType: String, - @Valid @RequestBody request: AboutDto + @Valid @RequestPart("request") request: AboutDto, + @RequestPart("image") image: MultipartFile?, ) : ResponseEntity { - return ResponseEntity.ok(aboutService.createAbout(postType, request)) + return ResponseEntity.ok(aboutService.createAbout(postType, request, image)) } // read 목록이 하나 diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt index 369e06a9..55f23e6b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt @@ -1,7 +1,9 @@ package com.wafflestudio.csereal.core.about.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.controller.ContentEntityType import com.wafflestudio.csereal.core.about.dto.AboutDto +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import jakarta.persistence.* @Entity(name = "about") @@ -14,8 +16,14 @@ class AboutEntity( var year: Int?, @OneToMany(mappedBy = "about", cascade = [CascadeType.ALL], orphanRemoval = true) - val locations: MutableList = mutableListOf() -) : BaseTimeEntity() { + val locations: MutableList = mutableListOf(), + + @OneToOne + var mainImage: MainImageEntity? = null, + +) : BaseTimeEntity(), ContentEntityType { + override fun bringMainImage(): MainImageEntity? = mainImage + companion object { fun of(postType: AboutPostType, aboutDto: AboutDto): AboutEntity { return AboutEntity( diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt index 63652e47..fc0612e4 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/dto/AboutDto.kt @@ -12,10 +12,11 @@ data class AboutDto( val year: Int?, val createdAt: LocalDateTime?, val modifiedAt: LocalDateTime?, - val locations: List? + val locations: List?, + val imageURL: String? ) { companion object { - fun of(entity: AboutEntity) : AboutDto = entity.run { + fun of(entity: AboutEntity, imageURL: String?) : AboutDto = entity.run { AboutDto( id = this.id, name = this.name, @@ -24,7 +25,8 @@ data class AboutDto( year = this.year, createdAt = this.createdAt, modifiedAt = this.modifiedAt, - locations = this.locations.map { it.name } + locations = this.locations.map { it.name }, + imageURL = imageURL ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt index 79b6c8a2..5cac574a 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt @@ -6,11 +6,13 @@ import com.wafflestudio.csereal.core.about.database.AboutPostType import com.wafflestudio.csereal.core.about.database.AboutRepository import com.wafflestudio.csereal.core.about.database.LocationEntity import com.wafflestudio.csereal.core.about.dto.AboutDto +import com.wafflestudio.csereal.core.resource.mainImage.service.ImageService import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile interface AboutService { - fun createAbout(postType: String, request: AboutDto): AboutDto + fun createAbout(postType: String, request: AboutDto, image: MultipartFile?): AboutDto fun readAbout(postType: String): AboutDto fun readAllClubs() : List fun readAllFacilities() : List @@ -19,10 +21,11 @@ interface AboutService { @Service class AboutServiceImpl( - private val aboutRepository: AboutRepository + private val aboutRepository: AboutRepository, + private val imageService: ImageService, ) : AboutService { @Transactional - override fun createAbout(postType: String, request: AboutDto): AboutDto { + override fun createAbout(postType: String, request: AboutDto, image: MultipartFile?): AboutDto { val enumPostType = makeStringToEnum(postType) val newAbout = AboutEntity.of(enumPostType, request) @@ -32,23 +35,30 @@ class AboutServiceImpl( } } + if(image != null) { + imageService.uploadImage(newAbout, image) + } aboutRepository.save(newAbout) - return AboutDto.of(newAbout) + val imageURL = imageService.createImageURL(newAbout.mainImage) + + return AboutDto.of(newAbout, imageURL) } @Transactional(readOnly = true) override fun readAbout(postType: String): AboutDto { val enumPostType = makeStringToEnum(postType) val about = aboutRepository.findByPostType(enumPostType) + val imageURL = imageService.createImageURL(about.mainImage) - return AboutDto.of(about) + return AboutDto.of(about, imageURL) } @Transactional(readOnly = true) override fun readAllClubs(): List { val clubs = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.STUDENT_CLUBS).map { - AboutDto.of(it) + val imageURL = imageService.createImageURL(it.mainImage) + AboutDto.of(it, imageURL) } return clubs @@ -57,7 +67,8 @@ class AboutServiceImpl( @Transactional(readOnly = true) override fun readAllFacilities(): List { val facilities = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.FACILITIES).map { - AboutDto.of(it) + val imageURL = imageService.createImageURL(it.mainImage) + AboutDto.of(it, imageURL) } return facilities @@ -66,7 +77,8 @@ class AboutServiceImpl( @Transactional(readOnly = true) override fun readAllDirections(): List { val directions = aboutRepository.findAllByPostTypeOrderByName(AboutPostType.DIRECTIONS).map { - AboutDto.of(it) + val imageURL = imageService.createImageURL(it.mainImage) + AboutDto.of(it, imageURL) } return directions 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 6caff3a0..b1ee1d72 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 @@ -6,6 +6,7 @@ import com.wafflestudio.csereal.core.member.dto.SimpleProfessorDto import com.wafflestudio.csereal.core.member.service.ProfessorService import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RequestMapping("/professor") @RestController @@ -14,8 +15,11 @@ class ProfessorController( ) { @PostMapping - fun createProfessor(@RequestBody createProfessorRequest: ProfessorDto): ResponseEntity { - return ResponseEntity.ok(professorService.createProfessor(createProfessorRequest)) + fun createProfessor( + @RequestPart("request") createProfessorRequest: ProfessorDto, + @RequestPart("image") image: MultipartFile?, + ): ResponseEntity { + return ResponseEntity.ok(professorService.createProfessor(createProfessorRequest, image)) } @GetMapping("/{professorId}") 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 391a3e58..26e0ac86 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 @@ -4,14 +4,8 @@ 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.http.ResponseEntity -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PatchMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RequestMapping("/staff") @RestController @@ -20,8 +14,11 @@ class StaffController( ) { @PostMapping - fun createStaff(@RequestBody createStaffRequest: StaffDto): ResponseEntity { - return ResponseEntity.ok(staffService.createStaff(createStaffRequest)) + fun createStaff( + @RequestPart("request") createStaffRequest: StaffDto, + @RequestPart("image") image: MultipartFile?, + ): ResponseEntity { + return ResponseEntity.ok(staffService.createStaff(createStaffRequest,image)) } @GetMapping("/{staffId}") 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 9e1a24e4..15000bec 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 @@ -1,8 +1,10 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.controller.ContentEntityType import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.research.database.LabEntity +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import jakarta.persistence.* import java.time.LocalDate @@ -39,8 +41,11 @@ class ProfessorEntity( @OneToMany(mappedBy = "professor", cascade = [CascadeType.ALL], orphanRemoval = true) val careers: MutableList = mutableListOf(), - var imageUri: String? = null -) : BaseTimeEntity() { + @OneToOne + var mainImage: MainImageEntity? = null, + +) : BaseTimeEntity(), ContentEntityType { + override fun bringMainImage(): MainImageEntity? = mainImage companion object { fun of(professorDto: ProfessorDto): ProfessorEntity { @@ -54,7 +59,7 @@ class ProfessorEntity( phone = professorDto.phone, fax = professorDto.fax, email = professorDto.email, - website = professorDto.website + website = professorDto.website, ) } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt index e4c37f82..abfafbe0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/database/StaffEntity.kt @@ -1,10 +1,13 @@ package com.wafflestudio.csereal.core.member.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.controller.ContentEntityType import com.wafflestudio.csereal.core.member.dto.StaffDto +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import jakarta.persistence.CascadeType import jakarta.persistence.Entity import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne @Entity(name = "staff") class StaffEntity( @@ -18,9 +21,11 @@ class StaffEntity( @OneToMany(mappedBy = "staff", cascade = [CascadeType.ALL], orphanRemoval = true) val tasks: MutableList = mutableListOf(), - var imageUri: String? = null + @OneToOne + var mainImage: MainImageEntity? = null, -) : BaseTimeEntity() { + ) : BaseTimeEntity(), ContentEntityType { + override fun bringMainImage(): MainImageEntity? = mainImage companion object { fun of(staffDto: StaffDto): StaffEntity { @@ -29,7 +34,7 @@ class StaffEntity( role = staffDto.role, office = staffDto.office, phone = staffDto.phone, - email = staffDto.email + email = staffDto.email, ) } } 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 d6a2204b..c48f0a5b 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 @@ -24,11 +24,11 @@ data class ProfessorDto( val researchAreas: List, val careers: List, @JsonInclude(JsonInclude.Include.NON_NULL) - var imageUri: String? = null + var imageURL: String? = null ) { companion object { - fun of(professorEntity: ProfessorEntity): ProfessorDto { + fun of(professorEntity: ProfessorEntity, imageURL: String?): ProfessorDto { return ProfessorDto( id = professorEntity.id, name = professorEntity.name, @@ -46,8 +46,9 @@ data class ProfessorDto( educations = professorEntity.educations.map { it.name }, researchAreas = professorEntity.researchAreas.map { it.name }, careers = professorEntity.careers.map { it.name }, - imageUri = professorEntity.imageUri + imageURL = imageURL, ) } + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt index b3c3682b..bfe05446 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleProfessorDto.kt @@ -10,10 +10,10 @@ data class SimpleProfessorDto( val labName: String?, val phone: String?, val email: String?, - val imageUri: String? + val imageURL: String? ) { companion object { - fun of(professorEntity: ProfessorEntity): SimpleProfessorDto { + fun of(professorEntity: ProfessorEntity, imageURL: String?): SimpleProfessorDto { return SimpleProfessorDto( id = professorEntity.id, name = professorEntity.name, @@ -22,8 +22,9 @@ data class SimpleProfessorDto( labName = professorEntity.lab?.name, phone = professorEntity.phone, email = professorEntity.email, - imageUri = professorEntity.imageUri + imageURL = imageURL ) } + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt index 750f9801..c46c5067 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/dto/SimpleStaffDto.kt @@ -9,11 +9,11 @@ data class SimpleStaffDto( val office: String, val phone: String, val email: String, - val imageUri: String? + val imageURL: String? ) { companion object { - fun of(staffEntity: StaffEntity): SimpleStaffDto { + fun of(staffEntity: StaffEntity, imageURL: String?): SimpleStaffDto { return SimpleStaffDto( id = staffEntity.id, name = staffEntity.name, @@ -21,7 +21,7 @@ data class SimpleStaffDto( office = staffEntity.office, phone = staffEntity.phone, email = staffEntity.email, - imageUri = staffEntity.imageUri + imageURL = imageURL ) } } 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 8cd1ee06..69a1d9fd 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 @@ -13,10 +13,10 @@ data class StaffDto( val email: String, val tasks: List, @JsonInclude(JsonInclude.Include.NON_NULL) - val imageUri: String? = null + val imageURL: String? = null ) { companion object { - fun of(staffEntity: StaffEntity): StaffDto { + fun of(staffEntity: StaffEntity, imageURL: String?): StaffDto { return StaffDto( id = staffEntity.id, name = staffEntity.name, @@ -25,8 +25,9 @@ data class StaffDto( phone = staffEntity.phone, email = staffEntity.email, tasks = staffEntity.tasks.map { it.name }, - imageUri = staffEntity.imageUri + imageURL = imageURL ) } + } } 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 5710d416..c53746b1 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 @@ -6,14 +6,14 @@ 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.research.database.LabRepository +import com.wafflestudio.csereal.core.resource.mainImage.service.ImageService import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile interface ProfessorService { - - fun createProfessor(createProfessorRequest: ProfessorDto): ProfessorDto - fun getProfessor(professorId: Long): ProfessorDto + fun createProfessor(createProfessorRequest: ProfessorDto, image: MultipartFile?): ProfessorDto fun getProfessor(professorId: Long): ProfessorDto fun getActiveProfessors(): ProfessorPageDto fun getInactiveProfessors(): List fun updateProfessor(professorId: Long, updateProfessorRequest: ProfessorDto): ProfessorDto @@ -24,12 +24,11 @@ interface ProfessorService { @Transactional class ProfessorServiceImpl( private val labRepository: LabRepository, - private val professorRepository: ProfessorRepository + private val professorRepository: ProfessorRepository, + private val imageService: ImageService, ) : ProfessorService { - - override fun createProfessor(createProfessorRequest: ProfessorDto): ProfessorDto { + override fun createProfessor(createProfessorRequest: ProfessorDto, image: MultipartFile?): ProfessorDto { val professor = ProfessorEntity.of(createProfessorRequest) - if (createProfessorRequest.labId != null) { val lab = labRepository.findByIdOrNull(createProfessorRequest.labId) ?: throw CserealException.Csereal404("해당 연구실을 찾을 수 없습니다. LabId: ${createProfessorRequest.labId}") @@ -48,16 +47,25 @@ class ProfessorServiceImpl( CareerEntity.create(career, professor) } + if(image != null) { + imageService.uploadImage(professor, image) + } + professorRepository.save(professor) - return ProfessorDto.of(professor) + val imageURL = imageService.createImageURL(professor.mainImage) + + return ProfessorDto.of(professor, imageURL) } @Transactional(readOnly = true) override fun getProfessor(professorId: Long): ProfessorDto { val professor = professorRepository.findByIdOrNull(professorId) ?: throw CserealException.Csereal404("해당 교수님을 찾을 수 없습니다. professorId: ${professorId}") - return ProfessorDto.of(professor) + + val imageURL = imageService.createImageURL(professor.mainImage) + + return ProfessorDto.of(professor, imageURL) } @Transactional(readOnly = true) @@ -68,14 +76,20 @@ class ProfessorServiceImpl( "교육 및 연구 지도에 총력을 기울이고 있다.\n\n다수의 외국인 학부생, 대학원생이 재학 중에 있으며 매 학기 전공 필수 과목을 비롯한 " + "30% 이상의 과목이 영어로 개설되고 있어 외국인 학생의 학업을 돕는 동시에 한국인 학생이 세계로 진출하는 초석이 되고 있다. 또한 " + "CSE int’l Luncheon을 개최하여 학부 내 외국인 구성원의 화합과 생활의 불편함을 최소화하는 등 학부 차원에서 최선을 다하고 있다." - val professors = professorRepository.findByStatusNot(ProfessorStatus.INACTIVE).map { SimpleProfessorDto.of(it) } + val professors = professorRepository.findByStatusNot(ProfessorStatus.INACTIVE).map { + val imageURL = imageService.createImageURL(it.mainImage) + SimpleProfessorDto.of(it, imageURL) + } .sortedBy { it.name } return ProfessorPageDto(description, professors) } @Transactional(readOnly = true) override fun getInactiveProfessors(): List { - return professorRepository.findByStatus(ProfessorStatus.INACTIVE).map { SimpleProfessorDto.of(it) } + return professorRepository.findByStatus(ProfessorStatus.INACTIVE).map { + val imageURL = imageService.createImageURL(it.mainImage) + SimpleProfessorDto.of(it, imageURL) + } .sortedBy { it.name } } @@ -128,7 +142,9 @@ class ProfessorServiceImpl( CareerEntity.create(career, professor) } - return ProfessorDto.of(professor) + val imageURL = imageService.createImageURL(professor.mainImage) + + return ProfessorDto.of(professor, imageURL) } override fun deleteProfessor(professorId: Long) { 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 d76137fb..722b29fd 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 @@ -6,12 +6,14 @@ import com.wafflestudio.csereal.core.member.database.StaffRepository import com.wafflestudio.csereal.core.member.database.TaskEntity import com.wafflestudio.csereal.core.member.dto.SimpleStaffDto import com.wafflestudio.csereal.core.member.dto.StaffDto +import com.wafflestudio.csereal.core.resource.mainImage.service.ImageService import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile interface StaffService { - fun createStaff(createStaffRequest: StaffDto): StaffDto + fun createStaff(createStaffRequest: StaffDto, image: MultipartFile?): StaffDto fun getStaff(staffId: Long): StaffDto fun getAllStaff(): List fun updateStaff(staffId: Long, updateStaffRequest: StaffDto): StaffDto @@ -21,30 +23,43 @@ interface StaffService { @Service @Transactional class StaffServiceImpl( - private val staffRepository: StaffRepository + private val staffRepository: StaffRepository, + private val imageService: ImageService, ) : StaffService { - override fun createStaff(createStaffRequest: StaffDto): StaffDto { + override fun createStaff(createStaffRequest: StaffDto, image: MultipartFile?): StaffDto { val staff = StaffEntity.of(createStaffRequest) for (task in createStaffRequest.tasks) { TaskEntity.create(task, staff) } + if(image != null) { + imageService.uploadImage(staff, image) + } + staffRepository.save(staff) - return StaffDto.of(staff) + val imageURL = imageService.createImageURL(staff.mainImage) + + return StaffDto.of(staff, imageURL) } @Transactional(readOnly = true) override fun getStaff(staffId: Long): StaffDto { val staff = staffRepository.findByIdOrNull(staffId) ?: throw CserealException.Csereal404("해당 행정직원을 찾을 수 없습니다. staffId: ${staffId}") - return StaffDto.of(staff) + + val imageURL = imageService.createImageURL(staff.mainImage) + + return StaffDto.of(staff, imageURL) } @Transactional(readOnly = true) override fun getAllStaff(): List { - return staffRepository.findAll().map { SimpleStaffDto.of(it) }.sortedBy { it.name } + return staffRepository.findAll().map { + val imageURL = imageService.createImageURL(it.mainImage) + SimpleStaffDto.of(it, imageURL) + }.sortedBy { it.name } } override fun updateStaff(staffId: Long, updateStaffRequest: StaffDto): StaffDto { @@ -66,7 +81,9 @@ class StaffServiceImpl( TaskEntity.create(task, staff) } - return StaffDto.of(staff) + val imageURL = imageService.createImageURL(staff.mainImage) + + return StaffDto.of(staff, imageURL) } override fun deleteStaff(staffId: Long) { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt index 4dea9962..62fd590b 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/api/NewsController.kt @@ -7,6 +7,7 @@ import jakarta.validation.Valid import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RequestMapping("/news") @RestController @@ -32,9 +33,10 @@ class NewsController( @PostMapping fun createNews( - @Valid @RequestBody request: NewsDto + @Valid @RequestPart("request") request: NewsDto, + @RequestPart("image") image: MultipartFile?, ) : ResponseEntity { - return ResponseEntity.ok(newsService.createNews(request)) + return ResponseEntity.ok(newsService.createNews(request,image)) } @PatchMapping("/{newsId}") diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt index a2944a97..541f0d34 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/database/NewsEntity.kt @@ -1,11 +1,10 @@ package com.wafflestudio.csereal.core.news.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.controller.ContentEntityType import com.wafflestudio.csereal.core.news.dto.NewsDto -import jakarta.persistence.CascadeType -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.OneToMany +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity +import jakarta.persistence.* @Entity(name = "news") class NewsEntity( @@ -23,10 +22,25 @@ class NewsEntity( // 새소식 작성란에도 "가장 위에 표시"가 있더라고요, 혹시 쓸지도 모르니까 남겼습니다 var isPinned: Boolean, + @OneToOne + var mainImage: MainImageEntity? = null, + @OneToMany(mappedBy = "news", cascade = [CascadeType.ALL]) var newsTags: MutableSet = mutableSetOf() -): BaseTimeEntity() { +): BaseTimeEntity(), ContentEntityType { + override fun bringMainImage() = mainImage + companion object { + fun of(newsDto: NewsDto): NewsEntity { + return NewsEntity( + title = newsDto.title, + description = newsDto.description, + isPublic = newsDto.isPublic, + isSlide = newsDto.isSlide, + isPinned = newsDto.isPinned, + ) + } + } fun update(updateNewsRequest: NewsDto) { this.title = updateNewsRequest.title this.description = updateNewsRequest.description diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt index da7f987c..effec738 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/news/dto/NewsDto.kt @@ -17,9 +17,10 @@ data class NewsDto( val prevTitle: String?, val nextId: Long?, val nextTitle: String?, + val imageURL: String?, ) { companion object { - fun of(entity: NewsEntity, prevNext: Array?) : NewsDto = entity.run { + fun of(entity: NewsEntity, imageURL: String?, prevNext: Array?) : NewsDto = entity.run { NewsDto( id = this.id, title = this.title, @@ -34,7 +35,7 @@ data class NewsDto( prevTitle = prevNext?.get(0)?.title, nextId = prevNext?.get(1)?.id, nextTitle = prevNext?.get(1)?.title, - + imageURL = imageURL, ) } } 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 eb638dfb..fb07aada 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 @@ -4,15 +4,16 @@ import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.core.news.database.* import com.wafflestudio.csereal.core.news.dto.NewsDto import com.wafflestudio.csereal.core.news.dto.NewsSearchResponse +import com.wafflestudio.csereal.core.resource.mainImage.service.ImageService import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.util.* +import org.springframework.web.multipart.MultipartFile interface NewsService { fun searchNews(tag: List?, keyword: String?, pageNum: Long): NewsSearchResponse fun readNews(newsId: Long, tag: List?, keyword: String?): NewsDto - fun createNews(request: NewsDto): NewsDto + fun createNews(request: NewsDto, image: MultipartFile?): NewsDto fun updateNews(newsId: Long, request: NewsDto): NewsDto fun deleteNews(newsId: Long) fun enrollTag(tagName: String) @@ -22,7 +23,8 @@ interface NewsService { class NewsServiceImpl( private val newsRepository: NewsRepository, private val tagInNewsRepository: TagInNewsRepository, - private val newsTagRepository: NewsTagRepository + private val newsTagRepository: NewsTagRepository, + private val imageService: ImageService, ) : NewsService { @Transactional(readOnly = true) override fun searchNews( @@ -44,30 +46,32 @@ class NewsServiceImpl( if (news.isDeleted) throw CserealException.Csereal404("삭제된 새소식입니다.(newsId: $newsId)") + val imageURL = imageService.createImageURL(news.mainImage) + val prevNext = newsRepository.findPrevNextId(newsId, tag, keyword) ?: throw CserealException.Csereal400("이전글 다음글이 존재하지 않습니다.(newsId=$newsId)") - return NewsDto.of(news, prevNext) + return NewsDto.of(news, imageURL, prevNext) } @Transactional - override fun createNews(request: NewsDto): NewsDto { - val newNews = NewsEntity( - title = request.title, - description = request.description, - isPublic = request.isPublic, - isSlide = request.isSlide, - isPinned = request.isPinned - ) + override fun createNews(request: NewsDto, image: MultipartFile?): NewsDto { + val newNews = NewsEntity.of(request) for (tagName in request.tags) { val tag = tagInNewsRepository.findByName(tagName) ?: throw CserealException.Csereal404("해당하는 태그가 없습니다") NewsTagEntity.createNewsTag(newNews, tag) } + if(image != null) { + imageService.uploadImage(newNews, image) + } + newsRepository.save(newNews) - return NewsDto.of(newNews, null) + val imageURL = imageService.createImageURL(newNews.mainImage) + + return NewsDto.of(newNews, imageURL, null) } @Transactional @@ -93,7 +97,10 @@ class NewsServiceImpl( NewsTagEntity.createNewsTag(news,tag) } - return NewsDto.of(news, null) + val imageURL = imageService.createImageURL(news.mainImage) + + + return NewsDto.of(news, imageURL, null) } @Transactional diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/api/MainImageController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/api/MainImageController.kt new file mode 100644 index 00000000..e74a26da --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/api/MainImageController.kt @@ -0,0 +1,13 @@ +package com.wafflestudio.csereal.core.resource.mainImage.api + +import com.wafflestudio.csereal.core.resource.mainImage.service.ImageService +import org.springframework.web.bind.annotation.* + + +@RequestMapping("/image") +@RestController +class MainImageController( + private val imageService: ImageService +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt new file mode 100644 index 00000000..28273401 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageEntity.kt @@ -0,0 +1,19 @@ +package com.wafflestudio.csereal.core.resource.mainImage.database + +import com.wafflestudio.csereal.common.config.BaseTimeEntity +import jakarta.persistence.* + + +@Entity(name = "mainImage") +class MainImageEntity( + val isDeleted : Boolean? = true, + + @Column(unique = true) + val filename: String, + + val imagesOrder: Int, + val size: Long, + + ) : BaseTimeEntity() { + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageRepository.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageRepository.kt new file mode 100644 index 00000000..f67b9dbc --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/database/MainImageRepository.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.resource.mainImage.database + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MainImageRepository : JpaRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/dto/MainImageDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/dto/MainImageDto.kt new file mode 100644 index 00000000..6ca97407 --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/dto/MainImageDto.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.csereal.core.resource.mainImage.dto + +data class MainImageDto( + val filename: String, + val imagesOrder: Int, + val size: Long, +) { +} \ No newline at end of file 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 new file mode 100644 index 00000000..ec75e77f --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt @@ -0,0 +1,118 @@ +package com.wafflestudio.csereal.core.resource.mainImage.service + +import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.common.controller.ContentEntityType +import com.wafflestudio.csereal.core.about.database.AboutEntity +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.resource.mainImage.database.MainImageRepository +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity +import com.wafflestudio.csereal.core.resource.mainImage.dto.MainImageDto +import com.wafflestudio.csereal.core.seminar.database.SeminarEntity +import net.coobird.thumbnailator.Thumbnailator +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import org.apache.commons.io.FilenameUtils +import java.lang.invoke.WrongMethodTypeException +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.fileSize +import kotlin.io.path.name + + +interface ImageService { + fun uploadImage( + contentEntityType: ContentEntityType, + requestImage: MultipartFile, + ): MainImageDto + fun createImageURL(image: MainImageEntity?) : String? +} + +@Service +class ImageServiceImpl( + private val imageRepository: MainImageRepository, + @Value("\${csereal.upload.path}") + private val path: String, +) : ImageService { + + @Transactional + override fun uploadImage( + contentEntity: ContentEntityType, + requestImage: MultipartFile, + ): MainImageDto { + Files.createDirectories(Paths.get(path)) + + val extension = FilenameUtils.getExtension(requestImage.originalFilename) + + if(!listOf("jpg", "jpeg", "png").contains(extension)) { + throw CserealException.Csereal400("파일의 형식은 jpg, jpeg, png 중 하나여야 합니다.") + } + + val timeMillis = System.currentTimeMillis() + + val filename = "${timeMillis}_${requestImage.originalFilename}" + val totalFilename = path + filename + val saveFile = Paths.get("$totalFilename.$extension") + requestImage.transferTo(saveFile) + + val totalThumbnailFilename = "${path}thumbnail_$filename" + val thumbnailFile = Paths.get("$totalThumbnailFilename.$extension") + Thumbnailator.createThumbnail(saveFile.toFile(), thumbnailFile.toFile(), 100, 100); + + val image = MainImageEntity( + filename = filename, + imagesOrder = 1, + size = requestImage.size, + ) + + val thumbnail = MainImageEntity( + filename = thumbnailFile.name, + imagesOrder = 1, + size = thumbnailFile.fileSize() + ) + + connectImageToEntity(contentEntity, image) + imageRepository.save(image) + imageRepository.save(thumbnail) + + return MainImageDto( + filename = filename, + imagesOrder = 1, + size = requestImage.size + ) + } + + @Transactional + override fun createImageURL(image: MainImageEntity?) : String? { + return if(image != null) { + "http://cse-dev-waffle.bacchus.io/image/${image.filename}" + } else null + } + + private fun connectImageToEntity(contentEntity: ContentEntityType, image: MainImageEntity) { + when (contentEntity) { + is NewsEntity -> { + contentEntity.mainImage = image + } + is SeminarEntity -> { + contentEntity.mainImage = image + } + is AboutEntity -> { + contentEntity.mainImage = image + } + is ProfessorEntity -> { + contentEntity.mainImage = image + } + is StaffEntity -> { + contentEntity.mainImage = image + } + else -> { + throw WrongMethodTypeException("해당하는 엔티티가 없습니다") + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt index 8b2699d7..f46cca7f 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/api/SeminarController.kt @@ -6,6 +6,7 @@ import com.wafflestudio.csereal.core.seminar.service.SeminarService import jakarta.validation.Valid import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RequestMapping("/seminar") @RestController @@ -21,10 +22,10 @@ class SeminarController ( } @PostMapping fun createSeminar( - @Valid @RequestBody request: SeminarDto + @Valid @RequestPart("request") request: SeminarDto, + @RequestPart("image") image: MultipartFile? ) : ResponseEntity { - return ResponseEntity.ok(seminarService.createSeminar(request)) - } + return ResponseEntity.ok(seminarService.createSeminar(request,image)) } @GetMapping("/{seminarId}") fun readSeminar( diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt index bc098643..e7b48fe0 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/database/SeminarEntity.kt @@ -1,9 +1,12 @@ package com.wafflestudio.csereal.core.seminar.database import com.wafflestudio.csereal.common.config.BaseTimeEntity +import com.wafflestudio.csereal.common.controller.ContentEntityType +import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import com.wafflestudio.csereal.core.seminar.dto.SeminarDto import jakarta.persistence.Column import jakarta.persistence.Entity +import jakarta.persistence.OneToOne @Entity(name = "seminar") class SeminarEntity( @@ -36,16 +39,18 @@ class SeminarEntity( var host: String?, - // var profileImage: File, - // var seminarFile: File, var isPublic: Boolean, var isSlide: Boolean, - var additionalNote: String? -): BaseTimeEntity() { + var additionalNote: String?, + + @OneToOne + var mainImage: MainImageEntity? = null, +): BaseTimeEntity(), ContentEntityType { + override fun bringMainImage(): MainImageEntity? = mainImage companion object { fun of(seminarDto: SeminarDto): SeminarEntity { @@ -67,10 +72,9 @@ class SeminarEntity( host = seminarDto.host, additionalNote = seminarDto.additionalNote, isPublic = seminarDto.isPublic, - isSlide = seminarDto.isSlide + isSlide = seminarDto.isSlide, ) } - } fun update(updateSeminarRequest: SeminarDto) { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt index b2563385..2eda7a96 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/seminar/dto/SeminarDto.kt @@ -19,7 +19,6 @@ data class SeminarDto( val endTime: String?, val location: String, val host: String?, - // val profileImage: File, // val seminarFile: File, val additionalNote: String?, val isPublic: Boolean, @@ -27,11 +26,12 @@ data class SeminarDto( val prevId: Long?, val prevTitle: String?, val nextId: Long?, - val nextTitle: String? + val nextTitle: String?, + val imageURL: String?, ) { companion object { - fun of(entity: SeminarEntity, prevNext: Array?): SeminarDto = entity.run { + fun of(entity: SeminarEntity, imageURL: String?, prevNext: Array?): SeminarDto = entity.run { SeminarDto( id = this.id, title = this.title, @@ -55,7 +55,8 @@ data class SeminarDto( prevId = prevNext?.get(0)?.id, prevTitle = prevNext?.get(0)?.title, nextId = prevNext?.get(1)?.id, - nextTitle = prevNext?.get(1)?.title + nextTitle = prevNext?.get(1)?.title, + imageURL = imageURL, ) } 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 cf6a5cba..274a178e 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 @@ -1,6 +1,7 @@ package com.wafflestudio.csereal.core.seminar.service import com.wafflestudio.csereal.common.CserealException +import com.wafflestudio.csereal.core.resource.mainImage.service.ImageService import com.wafflestudio.csereal.core.seminar.database.SeminarEntity import com.wafflestudio.csereal.core.seminar.database.SeminarRepository import com.wafflestudio.csereal.core.seminar.dto.SeminarDto @@ -8,10 +9,11 @@ import com.wafflestudio.csereal.core.seminar.dto.SeminarSearchResponse import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile interface SeminarService { fun searchSeminar(keyword: String?, pageNum: Long): SeminarSearchResponse - fun createSeminar(request: SeminarDto): SeminarDto + fun createSeminar(request: SeminarDto, image: MultipartFile?): SeminarDto fun readSeminar(seminarId: Long, keyword: String?): SeminarDto fun updateSeminar(seminarId: Long, request: SeminarDto): SeminarDto fun deleteSeminar(seminarId: Long) @@ -19,7 +21,8 @@ interface SeminarService { @Service class SeminarServiceImpl( - private val seminarRepository: SeminarRepository + private val seminarRepository: SeminarRepository, + private val imageService: ImageService, ) : SeminarService { @Transactional(readOnly = true) override fun searchSeminar(keyword: String?, pageNum: Long): SeminarSearchResponse { @@ -27,12 +30,19 @@ class SeminarServiceImpl( } @Transactional - override fun createSeminar(request: SeminarDto): SeminarDto { + override fun createSeminar(request: SeminarDto, image: MultipartFile?): SeminarDto { val newSeminar = SeminarEntity.of(request) + if(image != null) { + imageService.uploadImage(newSeminar, image) + } + seminarRepository.save(newSeminar) - return SeminarDto.of(newSeminar, null) + val imageURL = imageService.createImageURL(newSeminar.mainImage) + + + return SeminarDto.of(newSeminar, imageURL, null) } @Transactional(readOnly = true) @@ -42,9 +52,11 @@ class SeminarServiceImpl( if (seminar.isDeleted) throw CserealException.Csereal400("삭제된 세미나입니다. (seminarId: $seminarId)") + val imageURL = imageService.createImageURL(seminar.mainImage) + val prevNext = seminarRepository.findPrevNextId(seminarId, keyword) - return SeminarDto.of(seminar, prevNext) + return SeminarDto.of(seminar, imageURL, prevNext) } @Transactional @@ -55,7 +67,9 @@ class SeminarServiceImpl( seminar.update(request) - return SeminarDto.of(seminar, null) + val imageURL = imageService.createImageURL(seminar.mainImage) + + return SeminarDto.of(seminar, imageURL, null) } @Transactional override fun deleteSeminar(seminarId: Long) { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 20bcae44..9990d0e5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -23,12 +23,20 @@ server: session: timeout: 7200 # 2시간 + springdoc: swagger-ui: path: index.html api-docs: path: /api-docs/json +servlet: + multipart: + enabled: true + max-request-size: 100MB + max-file-size: 10MB + + --- spring: config.activate.on-profile: local @@ -48,6 +56,9 @@ logging.level: springframework: security: DEBUG +csereal: + upload: + path: /app/image/ --- spring: config.activate.on-profile: prod @@ -55,3 +66,10 @@ spring: hibernate: ddl-auto: update # TODO: change to validate (or none) when save actual data to server open-in-view: false + + + +csereal: + upload: + path: /app/image/ + From 455ded86bb2f341f8a683ac8765d0d84d87576ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9A=B0=ED=98=81=EC=A4=80=20=28HyukJoon=20Woo=29?= Date: Tue, 29 Aug 2023 22:49:04 +0900 Subject: [PATCH 27/27] CICD: Change deploy port to 8080 (#40) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ac6b0f24..f129819c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: args: PROFILE: ${PROFILE} ports: - - 80:8080 + - 8080:8080 environment: SPRING_DATASOURCE_URL: "jdbc:mysql://csereal_db_container:3306/${MYSQL_DATABASE}?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" SPRING_DATASOURCE_USERNAME: ${MYSQL_USER}