diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a314622 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ktlint_disabled_rules = no-wildcard-imports,import-ordering,comment-spacing \ No newline at end of file diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts deleted file mode 100644 index a6e17cc..0000000 --- a/build-logic/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -plugins { - `kotlin-dsl` - id("casper.documentation-convention") -} - -group = "io.casper.build" -version = "1.0.0" - -repositories { - mavenCentral() -} \ No newline at end of file diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts deleted file mode 100644 index 2af0ef2..0000000 --- a/build-logic/settings.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -rootProject.name = "build-logic" - -pluginManagement { - repositories { - gradlePluginPortal() - mavenCentral() - } - - // convention 모듈 참조 - includeBuild("../casper-convention") -} - -dependencyResolutionManagement { - repositories { - mavenCentral() - } -} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/io/casper/build/TestClass.kt b/build-logic/src/main/kotlin/io/casper/build/TestClass.kt deleted file mode 100644 index 6a5992d..0000000 --- a/build-logic/src/main/kotlin/io/casper/build/TestClass.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.casper.build - -/** - * 이 클래스는 KDoc 주석 검사 테스트를 위한 용도입니다. - * 이제 올바른 KDoc 주석 형식을 사용합니다. - */ -class TestClass { - - /** - * 이 함수는 빌드 로직에서 사용하는 테스트 함수입니다. - */ - fun testFunction() { - println("이 함수는 문서화 검사를 테스트하기 위한 용도입니다.") - } -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7822ed8..ab8e23c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { kotlin("plugin.spring") version "1.9.23" id("org.springframework.boot") version "3.4.4" id("io.spring.dependency-management") version "1.1.7" - id("org.jlleitschuh.gradle.ktlint").version("12.1.1") + id("org.jlleitschuh.gradle.ktlint").version("11.5.1") id("io.gitlab.arturbosch.detekt") version "1.23.6" id("casper.documentation-convention") } @@ -40,7 +40,6 @@ group = "hs.kr.entrydsm" version = "0.0.1-SNAPSHOT" dependencies { - implementation("org.springframework.boot:spring-boot-starter") implementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.springframework.boot:spring-boot-starter-test") @@ -76,3 +75,7 @@ tasks.withType().configureEach { jvmTarget = ("17") // Detekt가 사용하는 JVM 타겟을 Java 17로 지정 } + +tasks.withType { + mainClass.set("hs.kr.entrydsm.feed.CasperFeedApplication") +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..b22ed73 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt new file mode 100644 index 0000000..1aae290 --- /dev/null +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -0,0 +1,54 @@ +object Dependencies { + // Spring Boot + const val SPRING_BOOT_STARTER = "org.springframework.boot:spring-boot-starter" + const val SPRING_BOOT_STARTER_WEB = "org.springframework.boot:spring-boot-starter-web" + const val SPRING_BOOT_STARTER_DATA_JPA = "org.springframework.boot:spring-boot-starter-data-jpa" + const val SPRING_BOOT_STARTER_DATA_REDIS = "org.springframework.boot:spring-boot-starter-data-redis" + const val SPRING_BOOT_STARTER_SECURITY = "org.springframework.boot:spring-boot-starter-security" + const val SPRING_BOOT_STARTER_VALIDATION = "org.springframework.boot:spring-boot-starter-validation" + const val SPRING_BOOT_STARTER_TEST = "org.springframework.boot:spring-boot-starter-test" + + // Kotlin + const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect" + const val KOTLIN_TEST_JUNIT5 = "org.jetbrains.kotlin:kotlin-test-junit5" + + // Database + const val MYSQL_CONNECTOR = "com.mysql:mysql-connector-j" + + // JSON + const val JACKSON_MODULE_KOTLIN = "com.fasterxml.jackson.module:jackson-module-kotlin" + const val ORG_JSON = "org.json:json:${DependencyVersion.ORG_JSON}" + + // JWT + const val JWT_API = "io.jsonwebtoken:jjwt-api:${DependencyVersion.JWT}" + const val JWT_IMPL = "io.jsonwebtoken:jjwt-impl:${DependencyVersion.JWT}" + const val JWT_JACKSON = "io.jsonwebtoken:jjwt-jackson:${DependencyVersion.JWT}" + + // MapStruct + const val MAPSTRUCT = "org.mapstruct:mapstruct:${DependencyVersion.MAPSTRUCT}" + const val MAPSTRUCT_PROCESSOR = "org.mapstruct:mapstruct-processor:${DependencyVersion.MAPSTRUCT}" + + // Test + const val JUNIT_PLATFORM_LAUNCHER = "org.junit.platform:junit-platform-launcher" + + // gRPC + const val GRPC_NETTY_SHADED = "io.grpc:grpc-netty-shaded:${DependencyVersion.GRPC}" + const val GRPC_PROTOBUF = "io.grpc:grpc-protobuf:${DependencyVersion.GRPC}" + const val GRPC_STUB = "io.grpc:grpc-stub:${DependencyVersion.GRPC}" + const val GRPC_KOTLIN_STUB = "io.grpc:grpc-kotlin-stub:${DependencyVersion.GRPC_KOTLIN}" + const val PROTOBUF_KOTLIN = "com.google.protobuf:protobuf-kotlin:${DependencyVersion.PROTOBUF}" + const val GRPC_TESTING = "io.grpc:grpc-testing:${DependencyVersion.GRPC}" + + + // swagger + const val SWAGGER = "org.springdoc:springdoc-openapi-starter-webmvc-ui:${DependencyVersion.SWAGGER_VERSION}" + + // AWS + const val AWS = "com.amazonaws:aws-java-sdk-s3:${DependencyVersion.AWS}" + + // open feign + const val OPEN_FEIGN = "org.springframework.cloud:spring-cloud-starter-openfeign:${DependencyVersion.OPEN_FEIGN_VERSION}" + + // Kafka + const val KAFKA = "org.springframework.kafka:spring-kafka" +} diff --git a/buildSrc/src/main/kotlin/DependencyVersion.kt b/buildSrc/src/main/kotlin/DependencyVersion.kt new file mode 100644 index 0000000..17d8889 --- /dev/null +++ b/buildSrc/src/main/kotlin/DependencyVersion.kt @@ -0,0 +1,20 @@ +object DependencyVersion { + const val KOTLIN = "1.9.25" + const val SPRING_BOOT = "3.4.4" + const val SPRING_DEPENDENCY_MANAGEMENT = "1.1.7" + const val DETEKT = "1.23.6" + const val KTLINT = "12.1.1" + + const val JWT = "0.11.5" + const val ORG_JSON = "20230227" + const val MAPSTRUCT = "1.6.0" + + const val GRPC = "1.61.1" + const val GRPC_KOTLIN = "1.4.1" + const val PROTOBUF = "3.25.3" + + const val SWAGGER_VERSION = "2.5.0" + const val AWS = "1.12.281" + + const val OPEN_FEIGN_VERSION = "3.1.4" +} diff --git a/buildSrc/src/main/kotlin/Plugin.kt b/buildSrc/src/main/kotlin/Plugin.kt new file mode 100644 index 0000000..84e3329 --- /dev/null +++ b/buildSrc/src/main/kotlin/Plugin.kt @@ -0,0 +1,11 @@ +object Plugin { + const val KOTLIN_JVM = "org.jetbrains.kotlin.jvm" + const val KOTLIN_SPRING = "org.jetbrains.kotlin.plugin.spring" + const val KOTLIN_KAPT = "org.jetbrains.kotlin.kapt" + const val SPRING_BOOT = "org.springframework.boot" + const val SPRING_DEPENDENCY_MANAGEMENT = "io.spring.dependency-management" + const val DETEKT = "io.gitlab.arturbosch.detekt" + const val KTLINT = "org.jlleitschuh.gradle.ktlint" + const val CASPER_DOCUMENTATION = "casper.documentation-convention" + const val PROTOBUF = "com.google.protobuf" +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/PluginVersion.kt b/buildSrc/src/main/kotlin/PluginVersion.kt new file mode 100644 index 0000000..9b16506 --- /dev/null +++ b/buildSrc/src/main/kotlin/PluginVersion.kt @@ -0,0 +1,8 @@ +object PluginVersion { + const val KOTLIN_VERSION = "1.9.25" + const val SPRING_BOOT_VERSION = "3.4.4" + const val SPRING_DEPENDENCY_MANAGEMENT_VERSION = "1.1.7" + const val DETEKT_VERSION = "1.23.6" + const val KTLINT_VERSION = "12.1.1" + const val PROTOBUF_VERSION = "0.9.4" +} \ No newline at end of file diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/CasperFeedApplication.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/CasperFeedApplication.kt new file mode 100644 index 0000000..dafa47b --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/CasperFeedApplication.kt @@ -0,0 +1,24 @@ +package hs.kr.entrydsm.feed + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan +import org.springframework.boot.runApplication + +/** + * CasperFeed 애플리케이션의 메인 클래스입니다. + * + * 이 클래스는 스프링 부트 애플리케이션을 시작하고 자동 구성을 활성화합니다. + * `@ConfigurationPropertiesScan` 어노테이션을 통해 설정 프로퍼티 클래스들을 스캔합니다. + */ +@SpringBootApplication +@ConfigurationPropertiesScan +class CasperFeedApplication + +/** + * 애플리케이션의 진입점입니다. + * + * @param args 명령행 인자 배열 + */ +fun main(args: Array) { + runApplication(*args) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/attachFile/AttachFileWebAdapter.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/attachFile/AttachFileWebAdapter.kt new file mode 100644 index 0000000..ea40f4f --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/attachFile/AttachFileWebAdapter.kt @@ -0,0 +1,42 @@ +package hs.kr.entrydsm.feed.adapter.`in`.attachFile + +import hs.kr.entrydsm.feed.application.attachFile.service.CreateAttachFileService +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +/** + * 첨부 파일 관련 HTTP 요청을 처리하는 웹 어댑터 클래스입니다. + * + * 이 클래스는 첨부 파일 업로드와 관련된 HTTP 엔드포인트를 제공하며, + * 클라이언트의 요청을 적절한 서비스 메서드로 라우팅합니다. + * + * @property createAttachFileService 첨부 파일 비즈니스 로직을 처리하는 서비스 + */ +@RestController +@RequestMapping("/attach-file") +class AttachFileWebAdapter( + private val createAttachFileUseCase: CreateAttachFileService, +) { + /** + * 하나 이상의 첨부 파일을 업로드하고, 업로드된 파일 정보를 반환합니다. + * + * 이 메서드는 클라이언트로부터 전송된 첨부 파일을 받아 서버에 저장하고, + * 저장된 파일에 대한 정보(파일명, 다운로드 URL 등)를 반환합니다. + * + * @param attachFile 업로드할 첨부 파일 목록 (multipart/form-data 형식의 'attach_file' 파라미터로 전달) + * @return 업로드된 첨부 파일 정보 목록 (CreateAttachFileResponse 리스트) + * + * @throws org.springframework.web.multipart.MultipartException 파일 업로드에 실패한 경우 + * @throws java.io.IOException 파일 저장 중 I/O 오류가 발생한 경우 + * @throws hs.kr.entrydsm.feed.global.error.exception.InternalServerErrorException 내부 서버 오류가 발생한 경우 + * + * @see CreateAttachFileService.execute + */ + @PostMapping + fun createAttachFile( + @RequestPart(value = "attach_file") attachFile: List, + ) = createAttachFileUseCase.execute(attachFile) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/attachFile/dto/response/CreateAttachFileResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/attachFile/dto/response/CreateAttachFileResponse.kt new file mode 100644 index 0000000..edf81da --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/attachFile/dto/response/CreateAttachFileResponse.kt @@ -0,0 +1,12 @@ +package hs.kr.entrydsm.feed.adapter.`in`.attachFile.dto.response + +/** + * 첨부 파일 생성 응답을 위한 데이터 클래스입니다. + * + * @property fileName 원본 파일 이름 + * @property url 업로드된 파일에 접근할 수 있는 URL + */ +data class CreateAttachFileResponse( + val fileName: String, + val url: String, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/NoticeWebAdapter.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/NoticeWebAdapter.kt new file mode 100644 index 0000000..19a128e --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/NoticeWebAdapter.kt @@ -0,0 +1,122 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request.CreateNoticeRequest +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request.UpdateNoticeRequest +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryDetailsNoticeResponse +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryListNoticeResponse +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryNoticeTitleResponse +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.UploadNoticeImageResponse +import hs.kr.entrydsm.feed.application.notice.port.`in`.CreateNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.`in`.DeleteNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.`in`.QueryDetailsNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.`in`.QueryNoticeListByTypeUseCase +import hs.kr.entrydsm.feed.application.notice.port.`in`.QueryNoticeTitleUseCase +import hs.kr.entrydsm.feed.application.notice.port.`in`.UpdateNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.`in`.UploadNoticeImageUseCase +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +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 +import java.util.UUID + +/** + * 공지사항 관련 HTTP 요청을 처리하는 웹 어댑터 클래스입니다. + * + * 이 클래스는 공지사항과 관련된 모든 HTTP 엔드포인트를 제공하며, + * 클라이언트의 요청을 적절한 서비스 메서드로 라우팅합니다. + * + * @property noticeService 공지사항 비즈니스 로직을 처리하는 서비스 + */ +@RestController +@RequestMapping("/notice") +class NoticeWebAdapter( + private val createNoticeUseCase: CreateNoticeUseCase, + private val updateNoticeUseCase: UpdateNoticeUseCase, + private val deleteNoticeUseCase: DeleteNoticeUseCase, + private val queryDetailsNoticeUseCase: QueryDetailsNoticeUseCase, + private val queryNoticeTitleUseCase: QueryNoticeTitleUseCase, + private val uploadNoticeImageUseCase: UploadNoticeImageUseCase, + private val queryListNoticeListByTypeUseCase: QueryNoticeListByTypeUseCase, +) { + /** + * 새로운 공지사항을 생성합니다. + * + * @param createNoticeRequest 공지사항 생성 요청 데이터 + */ + @ResponseStatus(value = HttpStatus.CREATED) + @PostMapping + fun createNotice( + @RequestBody @Valid + createNoticeRequest: CreateNoticeRequest, + ) { + createNoticeUseCase.execute(createNoticeRequest) + } + + /** + * 기존 공지사항을 수정합니다. + * + * @param id 수정할 공지사항의 고유 식별자 + * @param updateNoticeRequest 공지사항 수정 요청 데이터 + * @return 수정 결과에 대한 응답 엔티티 + */ + @PatchMapping("/{notice-id}") + fun updateNotice( + @PathVariable(name = "notice-id") id: UUID, + @RequestBody updateNoticeRequest: UpdateNoticeRequest, + ): ResponseEntity = updateNoticeUseCase.execute(id, updateNoticeRequest) + + /** + * 공지사항에 첨부할 이미지를 업로드합니다. + * + * @param image 업로드할 이미지 파일 + * @return 업로드된 이미지 정보가 포함된 응답 객체 + */ + @PostMapping("/image") + fun uploadImage( + @RequestPart(name = "photo") image: MultipartFile, + ): UploadNoticeImageResponse = uploadNoticeImageUseCase.execute(image) + + /** + * 모든 공지사항의 제목 목록을 조회합니다. + * + * @return 공지사항 제목 목록이 포함된 응답 객체 리스트 + */ + @GetMapping("/title") + fun queryNoticeTitle(): List = queryNoticeTitleUseCase.execute() + + /** + * 특정 공지사항의 상세 정보를 조회합니다. + * + * @param noticeId 조회할 공지사항의 고유 식별자 + * @return 공지사항 상세 정보가 포함된 응답 객체 + */ + @GetMapping("/{notice-id}") + fun queryDetailsNotice( + @PathVariable(name = "notice-id", required = true) + noticeId: UUID, + ): QueryDetailsNoticeResponse = queryDetailsNoticeUseCase.execute(noticeId) + + /** + * 특정 유형의 공지사항 목록을 조회합니다. + * + * @param noticeType 조회할 공지사항 유형 (선택 사항) + * @return 해당 유형의 공지사항 목록이 포함된 응답 객체 + */ + @GetMapping + fun queryNoticeListByType( + @RequestParam("type") noticeType: NoticeType?, + ): QueryListNoticeResponse = queryListNoticeListByTypeUseCase.execute(noticeType) + + /** + * 특정 공지사항을 삭제합니다. + * + * @param id 삭제할 공지사항의 고유 식별자 + */ + @ResponseStatus(value = HttpStatus.NO_CONTENT) + @DeleteMapping("/{notice-id}") + fun deleteNotice( + @PathVariable(name = "notice-id")id: UUID, + ) = deleteNoticeUseCase.execute(id) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/request/CreateNoticeRequest.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/request/CreateNoticeRequest.kt new file mode 100644 index 0000000..1dae02b --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/request/CreateNoticeRequest.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request + +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size + +/** + * 공지사항 생성을 위한 요청 데이터 클래스입니다. + * + * 이 클래스는 클라이언트로부터 전달받은 공지사항 생성 요청 데이터를 담고 있으며, + * 유효성 검증을 위한 어노테이션들이 적용되어 있습니다. + * + * @property title 공지사항 제목 (필수, 최대 100자) + * @property content 공지사항 내용 (필수, 최대 5000자) + * @property isPinned 공지사항 상단 고정 여부 (필수) + * @property type 공지사항 유형 (필수, GUIDE 또는 NOTICE) + * @property fileName 공지사항에 첨부된 이미지 파일명 (선택) + * @property attachFileName 공지사항에 첨부된 파일들의 원본 파일명 목록 (선택) + */ +data class CreateNoticeRequest( + @field:NotBlank(message = "title은 null, 공백, 띄어쓰기를 허용하지 않습니다.") + @Size(max = 100, message = "title은 최대 100자까지 가능합니다.") + val title: String, + @field:NotBlank(message = "content은 null, 공백, 띄어쓰기를 허용하지 않습니다.") + @Size(max = 5000, message = "content은 최대 5000자까지 가능합니다.") + val content: String, + @field:NotNull(message = "Pinned는 null일수가 없습니다") + val isPinned: Boolean, + @field:NotNull(message = "type은 null일수가 없습니다") + val type: NoticeType, + val fileName: String? = null, + val attachFileName: List? = null, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/request/UpdateNoticeRequest.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/request/UpdateNoticeRequest.kt new file mode 100644 index 0000000..7cc8a18 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/request/UpdateNoticeRequest.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request + +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size + +/** + * 공지사항 수정을 위한 요청 데이터 클래스입니다. + * + * 이 클래스는 클라이언트로부터 전달받은 공지사항 수정 요청 데이터를 담고 있으며, + * 유효성 검증을 위한 어노테이션들이 적용되어 있습니다. + * + * @property title 수정할 공지사항 제목 (필수, 최대 100자) + * @property content 수정할 공지사항 내용 (필수, 최대 5000자) + * @property isPinned 공지사항 상단 고정 여부 (필수) + * @property type 공지사항 유형 (필수, GUIDE 또는 NOTICE) + * @property fileName 수정할 공지사항의 이미지 파일명 (선택, null인 경우 기존 이미지 유지) + * @property attachFileName 수정할 공지사항의 첨부 파일명 목록 (선택, 비어있을 수 있음) + */ +data class UpdateNoticeRequest( + @field:NotBlank(message = "title은 null, 공백, 띄어쓰기를 허용하지 않습니다.") + @Size(max = 100, message = "title은 최대 100자까지 가능합니다.") + val title: String, + @field:NotBlank(message = "content은 null, 공백, 띄어쓰기를 허용하지 않습니다.") + @Size(max = 5000, message = "content은 최대 5000자까지 가능합니다.") + val content: String, + @field:NotNull(message = "Pinned은 null일수가 없습니다") + val isPinned: Boolean, + @field:NotNull(message = "type은 null일수가 없습니다") + val type: NoticeType, + val fileName: String? = null, + val attachFileName: List? = emptyList(), +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/NoticeResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/NoticeResponse.kt new file mode 100644 index 0000000..a152a78 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/NoticeResponse.kt @@ -0,0 +1,22 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response + +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import java.time.LocalDateTime +import java.util.* + +/** + * 공지사항 목록 조회 응답을 위한 데이터 클래스입니다. + * + * @property id 공지사항 고유 식별자 + * @property title 공지사항 제목 + * @property type 공지사항 유형 + * @property isPinned 공지사항 고정 여부 + * @property createdAt 공지사항 생성 일시 + */ +data class NoticeResponse( + val id: UUID, + val title: String, + val type: NoticeType, + val isPinned: Boolean, + val createdAt: LocalDateTime, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryDetailsNoticeResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryDetailsNoticeResponse.kt new file mode 100644 index 0000000..dead3d3 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryDetailsNoticeResponse.kt @@ -0,0 +1,38 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response + +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import java.time.LocalDateTime + +/** + * 공지사항 상세 조회 응답을 위한 데이터 클래스입니다. + * + * @property title 공지사항 제목 + * @property content 공지사항 내용 + * @property createdAt 공지사항 생성 일시 + * @property type 공지사항 유형 + * @property imageURL 공지사항에 첨부된 이미지 URL (없을 수 있음) + * @property imageName 공지사항에 첨부된 이미지 파일명 (없을 수 있음) + * @property attachFiles 공지사항에 첨부된 파일 목록 (없을 수 있음) + * @property isPinned 공지사항 고정 여부 + */ +data class QueryDetailsNoticeResponse( + val title: String, + val content: String, + val createdAt: LocalDateTime, + val type: NoticeType, + val imageURL: String?, + val imageName: String?, + val attachFiles: List = emptyList(), + val isPinned: Boolean, +) + +/** + * 공지사항에 첨부된 파일 정보를 나타내는 데이터 클래스입니다. + * + * @property attachFileUrl 첨부 파일 다운로드 URL + * @property attachFileName 첨부 파일 원본 이름 + */ +data class AttachFileElement( + val attachFileUrl: String, + val attachFileName: String, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryListNoticeResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryListNoticeResponse.kt new file mode 100644 index 0000000..91ac602 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryListNoticeResponse.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response + +/** + * 공지사항 목록 조회 응답을 위한 데이터 클래스입니다. + * 여러 개의 공지사항 정보를 리스트 형태로 반환할 때 사용됩니다. + * + * @property notices 공지사항 정보 목록 (NoticeResponse 리스트) + */ +data class QueryListNoticeResponse( + val notices: List, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryNoticeTitleResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryNoticeTitleResponse.kt new file mode 100644 index 0000000..69a1f86 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/QueryNoticeTitleResponse.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response + +import java.time.LocalDateTime +import java.util.UUID + +/** + * 공지사항 제목 목록 조회 응답을 위한 데이터 클래스입니다. + * 주로 공지사항 목록에서 제목만 보여줄 때 사용됩니다. + * + * @property id 공지사항 고유 식별자 + * @property title 공지사항 제목 + * @property createdAt 공지사항 생성 일시 + */ +data class QueryNoticeTitleResponse( + val id: UUID, + val title: String, + val createdAt: LocalDateTime, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/UploadNoticeImageResponse.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/UploadNoticeImageResponse.kt new file mode 100644 index 0000000..d5fb836 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/in/notice/dto/response/UploadNoticeImageResponse.kt @@ -0,0 +1,12 @@ +package hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response + +/** + * 공지사항 이미지 업로드 응답을 위한 데이터 클래스입니다. + * + * @property fileUrl 업로드된 이미지 파일에 접근할 수 있는 URL + * @property fileName 업로드된 이미지 파일의 원본 이름 + */ +data class UploadNoticeImageResponse( + val fileUrl: String, + val fileName: String, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/entity/attachFile/AttachFileJpaEntity.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/entity/attachFile/AttachFileJpaEntity.kt new file mode 100644 index 0000000..528210c --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/entity/attachFile/AttachFileJpaEntity.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.feed.adapter.out.entity.attachFile + +import jakarta.persistence.Entity +import jakarta.persistence.Id + +/** + * 첨부 파일 정보를 데이터베이스에 저장하기 위한 JPA 엔티티 클래스입니다. + * + * @property uploadedFileName AWS S3에 업로드된 파일명 (UUID 등으로 인코딩된 파일명) + * @property originalAttachFileName 원본 첨부 파일명 (사용자가 업로드한 원본 파일명) + */ +@Entity(name = "tbl_attach_file") +class AttachFileJpaEntity( + @Id + val uploadedFileName: String, // aws s3에 올라가는 fileName + var originalAttachFileName: String, // 인코딩 되기 전 첨부파일 이름 ex): 서프수행.hwp +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/entity/notice/NoticeJpaEntity.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/entity/notice/NoticeJpaEntity.kt new file mode 100644 index 0000000..0454100 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/entity/notice/NoticeJpaEntity.kt @@ -0,0 +1,46 @@ +package hs.kr.entrydsm.feed.adapter.out.entity.notice + +import hs.kr.entrydsm.feed.adapter.out.entity.attachFile.AttachFileJpaEntity +import hs.kr.entrydsm.feed.global.entity.BaseEntity +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.FetchType +import jakarta.persistence.JoinColumn +import jakarta.persistence.OneToMany +import java.util.* + +/** + * 공지사항 정보를 데이터베이스에 저장하기 위한 JPA 엔티티 클래스입니다. + * + * @property title 공지사항 제목 (최대 100자) + * @property content 공지사항 내용 (최대 5000자) + * @property fileName 첨부 파일명 (선택 사항) + * @property attachFile 공지사항에 첨부된 파일 목록 (Lazy 로딩) + * @property adminId 공지사항을 작성한 관리자 ID (UUID) + * @property isPinned 상단 고정 여부 + * @property type 공지사항 유형 (NoticeType ENUM) + * @param id 엔티티의 고유 식별자 (생성 시 자동 생성됨) + */ +@Entity(name = "tbl_notice") +class NoticeJpaEntity( + id: UUID? = null, + @Column(name = "title", length = 100, nullable = false) + var title: String, + @Column(name = "content", length = 5000, nullable = false) + var content: String, + @Column(name = "file_name", nullable = true) + var fileName: String? = null, + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "noticeId") + var attachFile: List? = emptyList(), + @Column(name = "admin_id", nullable = false, columnDefinition = "BINARY(16)") + var adminId: UUID, + @Column(nullable = false) + var isPinned: Boolean, + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + var type: NoticeType, +) : BaseEntity(id) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/mapper/attachFile/AttachFileMapper.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/mapper/attachFile/AttachFileMapper.kt new file mode 100644 index 0000000..2919113 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/mapper/attachFile/AttachFileMapper.kt @@ -0,0 +1,28 @@ +package hs.kr.entrydsm.feed.adapter.out.mapper.attachFile + +import hs.kr.entrydsm.feed.adapter.out.entity.attachFile.AttachFileJpaEntity +import hs.kr.entrydsm.feed.model.attachFile.AttachFile +import org.mapstruct.Mapper + +/** + * 첨부 파일(AttachFile) 도메인 모델과 JPA 엔티티 간의 변환을 담당하는 매퍼 인터페이스입니다. + * MapStruct를 사용하여 구현체가 자동으로 생성됩니다. + */ +@Mapper(componentModel = "spring") +interface AttachFileMapper { + /** + * JPA 엔티티를 AttachFile 도메인 모델로 변환합니다. + * + * @param entity 변환할 AttachFileJpaEntity 인스턴스 + * @return 변환된 AttachFile 도메인 모델 + */ + fun toModel(entity: AttachFileJpaEntity): AttachFile + + /** + * AttachFile 도메인 모델을 JPA 엔티티로 변환합니다. + * + * @param model 변환할 AttachFile 도메인 모델 + * @return 변환된 AttachFileJpaEntity 인스턴스 + */ + fun toEntity(model: AttachFile): AttachFileJpaEntity +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/mapper/notice/NoticeMapper.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/mapper/notice/NoticeMapper.kt new file mode 100644 index 0000000..44a4bd2 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/mapper/notice/NoticeMapper.kt @@ -0,0 +1,28 @@ +package hs.kr.entrydsm.feed.adapter.out.mapper.notice + +import hs.kr.entrydsm.feed.adapter.out.entity.notice.NoticeJpaEntity +import hs.kr.entrydsm.feed.model.notice.Notice +import org.mapstruct.Mapper + +/** + * Notice 도메인 모델과 JPA 엔티티 간의 변환을 담당하는 매퍼 인터페이스입니다. + * MapStruct를 사용하여 구현체가 자동으로 생성됩니다. + */ +@Mapper(componentModel = "spring") +interface NoticeMapper { + /** + * Notice 도메인 모델을 JPA 엔티티로 변환합니다. + * + * @param model 변환할 Notice 도메인 모델 + * @return 변환된 NoticeJpaEntity 인스턴스 + */ + fun toEntity(model: Notice): NoticeJpaEntity + + /** + * JPA 엔티티를 Notice 도메인 모델로 변환합니다. + * + * @param entity 변환할 NoticeJpaEntity 인스턴스 + * @return 변환된 Notice 도메인 모델 + */ + fun toModel(entity: NoticeJpaEntity): Notice +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/attachFile/AttachFilePersistenceAdapter.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/attachFile/AttachFilePersistenceAdapter.kt new file mode 100644 index 0000000..a3cce8a --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/attachFile/AttachFilePersistenceAdapter.kt @@ -0,0 +1,63 @@ +package hs.kr.entrydsm.feed.adapter.out.persistence.attachFile + +import hs.kr.entrydsm.feed.adapter.out.mapper.attachFile.AttachFileMapper +import hs.kr.entrydsm.feed.adapter.out.persistence.attachFile.repository.AttachFileRepository +import hs.kr.entrydsm.feed.application.attachFile.port.out.DeleteAttachFilePort +import hs.kr.entrydsm.feed.application.attachFile.port.out.ExistsAttachFilePort +import hs.kr.entrydsm.feed.application.attachFile.port.out.FindAttachFilePort +import hs.kr.entrydsm.feed.application.attachFile.port.out.SaveAttachFilePort +import hs.kr.entrydsm.feed.model.attachFile.AttachFile +import org.springframework.stereotype.Component + +/** + * 첨부 파일 도메인과 데이터베이스 간의 상호작용을 담당하는 어댑터 클래스입니다. + * 첨부 파일의 CRUD 작업을 처리하며, 파일명을 통한 조회 및 삭제 기능을 제공합니다. + * + * @property attachFileRepository 첨부 파일 엔티티를 데이터베이스에서 조작하기 위한 리포지토리 + * @property attachFileMapper 첨부 파일 도메인 객체와 엔티티 간의 변환을 담당하는 매퍼 + */ +@Component +class AttachFilePersistenceAdapter( + private val attachFileRepository: AttachFileRepository, + private val attachFileMapper: AttachFileMapper, +) : ExistsAttachFilePort, DeleteAttachFilePort, SaveAttachFilePort, FindAttachFilePort { + + /** + * 주어진 원본 파일명을 가진 첨부 파일이 존재하는지 확인합니다. + * + * @param attachFileName 확인할 원본 첨부 파일명 + * @return 파일이 존재하면 true, 그렇지 않으면 false 반환 + */ + override fun existsByOriginalAttachFileName(attachFileName: String): Boolean { + return attachFileRepository.existsByOriginalAttachFileName(attachFileName) + } + + /** + * 주어진 원본 파일명을 가진 모든 첨부 파일을 삭제합니다. + * + * @param attachFileName 삭제할 첨부 파일의 원본 파일명 + */ + override fun deleteByOriginalAttachFileName(attachFileName: String) { + attachFileRepository.deleteByOriginalAttachFileName(attachFileName) + } + + /** + * 첨부 파일을 저장하거나 업데이트합니다. + * + * @param attachFile 저장할 첨부 파일 도메인 객체 + * @return 저장된 첨부 파일 도메인 객체 + */ + override fun save(attachFile: AttachFile): AttachFile { + return attachFileMapper.toModel(attachFileRepository.save(attachFileMapper.toEntity(attachFile))) + } + + /** + * 주어진 원본 파일명을 가진 모든 첨부 파일을 조회합니다. + * + * @param attachFileName 조회할 첨부 파일의 원본 파일명 + * @return 조회된 첨부 파일 도메인 객체 목록, 없을 경우 null 반환 + */ + override fun findByOriginalAttachFileName(attachFileName: String): List? { + return attachFileRepository.findByOriginalAttachFileName(attachFileName)?.map { attachFileMapper.toModel(it) } + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/attachFile/repository/AttachFileRepository.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/attachFile/repository/AttachFileRepository.kt new file mode 100644 index 0000000..fddde5b --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/attachFile/repository/AttachFileRepository.kt @@ -0,0 +1,33 @@ +package hs.kr.entrydsm.feed.adapter.out.persistence.attachFile.repository + +import hs.kr.entrydsm.feed.adapter.out.entity.attachFile.AttachFileJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +/** + * 첨부 파일 데이터에 접근하기 위한 JPA Repository 인터페이스입니다. + * 첨부 파일 엔티티의 CRUD 및 커스텀 쿼리 메서드를 제공합니다. + */ +interface AttachFileRepository : JpaRepository { + /** + * 원본 파일명으로 첨부 파일 엔티티를 조회합니다. + * + * @param attachFileName 조회할 원본 파일명 + * @return 조회된 첨부 파일 엔티티 목록 (없을 경우 null 반환) + */ + fun findByOriginalAttachFileName(attachFileName: String): List? + + /** + * 원본 파일명에 해당하는 모든 첨부 파일 엔티티를 삭제합니다. + * + * @param attachFileName 삭제할 원본 파일명 + */ + fun deleteByOriginalAttachFileName(attachFileName: String) + + /** + * 주어진 원본 파일명을 가진 첨부 파일이 존재하는지 확인합니다. + * + * @param attachFileName 확인할 원본 파일명 + * @return 파일이 존재하면 true, 그렇지 않으면 false + */ + fun existsByOriginalAttachFileName(attachFileName: String): Boolean +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/notice/NoticePersistenceAdapter.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/notice/NoticePersistenceAdapter.kt new file mode 100644 index 0000000..5d87fa1 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/notice/NoticePersistenceAdapter.kt @@ -0,0 +1,73 @@ +package hs.kr.entrydsm.feed.adapter.out.persistence.notice + +import hs.kr.entrydsm.feed.adapter.out.mapper.notice.NoticeMapper +import hs.kr.entrydsm.feed.adapter.out.persistence.notice.repository.NoticeRepository +import hs.kr.entrydsm.feed.application.notice.port.out.DeleteNoticePort +import hs.kr.entrydsm.feed.application.notice.port.out.FindNoticePort +import hs.kr.entrydsm.feed.application.notice.port.out.SaveNoticePort +import hs.kr.entrydsm.feed.model.notice.Notice +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Component +import java.util.UUID + +/** + * 공지사항 도메인과 데이터베이스 간의 상호작용을 담당하는 어댑터 클래스입니다. + * + * @property noticeRepository 공지사항 엔티티를 데이터베이스에서 조작하기 위한 리포지토리 + * @property noticeMapper 공지사항 도메인 객체와 엔티티 간의 변환을 담당하는 매퍼 + */ +@Component +class NoticePersistenceAdapter( + private val noticeRepository: NoticeRepository, + private val noticeMapper: NoticeMapper, +) : SaveNoticePort, FindNoticePort, DeleteNoticePort { + + /** + * 공지사항을 저장하거나 업데이트합니다. + * + * @param notice 저장 또는 업데이트할 공지사항 도메인 객체 + */ + override fun saveNotice(notice: Notice) { + noticeRepository.save(noticeMapper.toEntity(notice)) + } + + /** + * 주어진 ID에 해당하는 공지사항을 조회합니다. + * + * @param noticeId 조회할 공지사항의 고유 식별자 + * @return 조회된 공지사항 도메인 객체, 없을 경우 null 반환 + */ + override fun findByIdOrNull(noticeId: UUID): Notice? = + noticeRepository.findByIdOrNull(noticeId)?.let { + noticeMapper.toModel(it) + } + + /** + * 공지사항을 삭제합니다. + * + * @param notice 삭제할 공지사항 도메인 객체 + */ + override fun deleteNotice(notice: Notice) { + noticeRepository.delete(noticeMapper.toEntity(notice)) + } + + /** + * 모든 공지사항을 조회합니다. + * + * @return 모든 공지사항 도메인 객체 목록 (없을 경우 빈 목록 반환) + */ + override fun findAll(): List { + return noticeRepository.findAll().map { noticeMapper.toModel(it) } + } + + /** + * 지정된 유형의 모든 공지사항을 조회합니다. + * + * @param noticeType 조회할 공지사항의 유형 (GUIDE 또는 NOTICE) + * @return 지정된 유형의 공지사항 도메인 객체 목록 (없을 경우 빈 목록 반환) + */ + override fun findAllByType(noticeType: NoticeType): List { + return noticeRepository.findAllByType(noticeType).map { noticeMapper.toModel(it) } + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/notice/repository/NoticeRepository.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/notice/repository/NoticeRepository.kt new file mode 100644 index 0000000..3d7d81c --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/adapter/out/persistence/notice/repository/NoticeRepository.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.feed.adapter.out.persistence.notice.repository + +import hs.kr.entrydsm.feed.adapter.out.entity.notice.NoticeJpaEntity +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +/** + * 공지사항 데이터에 접근하기 위한 JPA Repository 인터페이스입니다. + * 공지사항 엔티티의 CRUD 및 커스텀 쿼리 메서드를 제공합니다. + */ +interface NoticeRepository : JpaRepository { + /** + * 주어진 유형에 해당하는 모든 공지사항 엔티티를 조회합니다. + * + * @param type 조회할 공지사항 유형 + * @return 조회된 공지사항 엔티티 목록 (없을 경우 빈 목록 반환) + */ + fun findAllByType(type: NoticeType): List +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/in/CreateAttachFileUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/in/CreateAttachFileUseCase.kt new file mode 100644 index 0000000..240ad88 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/in/CreateAttachFileUseCase.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.feed.application.attachFile.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.attachFile.dto.response.CreateAttachFileResponse +import org.springframework.web.multipart.MultipartFile + +/** + * 첨부 파일 관련 비즈니스 로직을 정의한 인터페이스입니다. + * 첨부 파일 업로드 기능을 제공합니다. + */ +interface CreateAttachFileUseCase { + /** + * 하나 이상의 첨부 파일을 업로드합니다. + * + * @param attachFile 업로드할 첨부 파일 목록 + * @return 생성된 첨부 파일 정보 응답 목록 + */ + fun execute(attachFile: List): List +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/DeleteAttachFilePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/DeleteAttachFilePort.kt new file mode 100644 index 0000000..2c0b8a1 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/DeleteAttachFilePort.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.application.attachFile.port.out + +/** + * 첨부 파일 삭제를 위한 포트 인터페이스입니다. + * 첨부 파일을 삭제하는 메서드를 정의합니다. + */ +interface DeleteAttachFilePort { + /** + * 원본 첨부 파일명으로 첨부 파일을 삭제합니다. + * + * @param attachFileName 삭제할 첨부 파일명 + */ + fun deleteByOriginalAttachFileName(attachFileName: String) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/ExistsAttachFilePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/ExistsAttachFilePort.kt new file mode 100644 index 0000000..8e0ec12 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/ExistsAttachFilePort.kt @@ -0,0 +1,15 @@ +package hs.kr.entrydsm.feed.application.attachFile.port.out + +/** + * 첨부 파일 존재 여부 확인을 위한 포트 인터페이스입니다. + * 첨부 파일의 존재 여부를 확인하는 메서드를 정의합니다. + */ +interface ExistsAttachFilePort { + /** + * 원본 첨부 파일명으로 첨부 파일의 존재 여부를 확인합니다. + * + * @param attachFileName 확인할 첨부 파일명 + * @return 파일이 존재하면 true, 그렇지 않으면 false + */ + fun existsByOriginalAttachFileName(attachFileName: String): Boolean +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/FindAttachFilePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/FindAttachFilePort.kt new file mode 100644 index 0000000..5c2c2ae --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/FindAttachFilePort.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.feed.application.attachFile.port.out + +import hs.kr.entrydsm.feed.model.attachFile.AttachFile + +/** + * 첨부 파일 조회를 위한 포트 인터페이스입니다. + * 첨부 파일 도메인 객체를 조회하는 메서드를 정의합니다. + */ +interface FindAttachFilePort { + /** + * 원본 첨부 파일명으로 첨부 파일 목록을 조회합니다. + * + * @param attachFileName 조회할 첨부 파일명 + * @return 조회된 첨부 파일 목록, 없을 경우 null + */ + fun findByOriginalAttachFileName(attachFileName: String): List? +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/SaveAttachFilePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/SaveAttachFilePort.kt new file mode 100644 index 0000000..e9f8724 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/port/out/SaveAttachFilePort.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.feed.application.attachFile.port.out + +import hs.kr.entrydsm.feed.model.attachFile.AttachFile + +/** + * 첨부 파일 저장을 위한 포트 인터페이스입니다. + * 첨부 파일 도메인 객체를 저장하는 메서드를 정의합니다. + */ +interface SaveAttachFilePort { + /** + * 첨부 파일을 저장하거나 업데이트합니다. + * + * @param attachFile 저장할 첨부 파일 도메인 객체 + * @return 저장된 첨부 파일 도메인 객체 (ID가 할당됨) + */ + fun save(attachFile: AttachFile): AttachFile +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/service/CreateAttachFileService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/service/CreateAttachFileService.kt new file mode 100644 index 0000000..3d72272 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/attachFile/service/CreateAttachFileService.kt @@ -0,0 +1,65 @@ +package hs.kr.entrydsm.feed.application.attachFile.service + +import hs.kr.entrydsm.feed.adapter.`in`.attachFile.dto.response.CreateAttachFileResponse +import hs.kr.entrydsm.feed.application.attachFile.port.`in`.CreateAttachFileUseCase +import hs.kr.entrydsm.feed.application.attachFile.port.out.DeleteAttachFilePort +import hs.kr.entrydsm.feed.application.attachFile.port.out.ExistsAttachFilePort +import hs.kr.entrydsm.feed.application.attachFile.port.out.SaveAttachFilePort +import hs.kr.entrydsm.feed.infrastructure.s3.PathList +import hs.kr.entrydsm.feed.infrastructure.s3.util.FileUtil +import hs.kr.entrydsm.feed.model.attachFile.AttachFile +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +/** + * 첨부 파일 도메인에 대한 비즈니스 로직을 처리하는 서비스 클래스입니다. + * + * 이 클래스는 첨부 파일과 관련된 모든 비즈니스 로직을 캡슐화하며, + * 파일 업로드, 조회, 삭제 등의 기능을 제공합니다. + * + * @property fileUtil 파일 업로드/다운로드와 관련된 유틸리티 + * @property existsAttachFilePort 첨부 파일 존재 여부 확인을 위한 포트 + * @property deleteAttachFilePort 첨부 파일 삭제를 위한 포트 + * @property saveAttachFilePort 첨부 파일 저장을 위한 포트 + */ +@Service +class CreateAttachFileService( + private val fileUtil: FileUtil, + private val existsAttachFilePort: ExistsAttachFilePort, + private val deleteAttachFilePort: DeleteAttachFilePort, + private val saveAttachFilePort: SaveAttachFilePort, +) : CreateAttachFileUseCase { + /** + * 첨부 파일을 업로드하고 저장된 파일 정보를 반환합니다. + * + * @param attachFile 업로드할 파일 목록 (MultipartFile) + * @return 업로드된 파일 정보 목록 (CreateAttachFileResponse 리스트) + * + * @throws Exception 파일 업로드 중 오류가 발생한 경우 + * + * 이 메서드는 다음 작업을 수행합니다: + * 1. 이미 동일한 이름의 파일이 존재하면 삭제합니다. + * 2. 파일을 S3에 업로드합니다. + * 3. 업로드된 파일 정보를 데이터베이스에 저장합니다. + * 4. 업로드된 파일에 접근할 수 있는 URL을 생성하여 응답을 반환합니다. + */ + override fun execute(attachFile: List): List { + val attachFileResponses = mutableListOf() + + attachFile.forEach { file -> + if (existsAttachFilePort.existsByOriginalAttachFileName(file.originalFilename!!)) { + deleteAttachFilePort.deleteByOriginalAttachFileName(file.originalFilename!!) + } + val uploadedFilename = fileUtil.upload(file, PathList.ATTACH_FILE) + val attachFile = + AttachFile( + uploadedFileName = uploadedFilename, + originalAttachFileName = file.originalFilename!!, + ) + saveAttachFilePort.save(attachFile) + val url = fileUtil.generateObjectUrl(uploadedFilename, PathList.ATTACH_FILE) + attachFileResponses.add(CreateAttachFileResponse(file.originalFilename!!, url)) + } + return attachFileResponses + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/exception/AttachFileNotFoundException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/exception/AttachFileNotFoundException.kt new file mode 100644 index 0000000..b9b4d0b --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/exception/AttachFileNotFoundException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.application.notice.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * 첨부 파일을 찾을 수 없을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (404) + * @property message 에러 메시지 + */ +object AttachFileNotFoundException : CasperException( + ErrorCode.ATTACH_FILE_NOT_FOUND, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/exception/NoticeNotFoundException.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/exception/NoticeNotFoundException.kt new file mode 100644 index 0000000..266b0aa --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/exception/NoticeNotFoundException.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.feed.application.notice.exception + +import hs.kr.entrydsm.feed.global.error.exception.CasperException +import hs.kr.entrydsm.feed.global.error.exception.ErrorCode + +/** + * 공지사항을 찾을 수 없을 때 발생하는 예외 클래스입니다. + * + * @property status HTTP 상태 코드 (404) + * @property message 에러 메시지 + */ +object NoticeNotFoundException : CasperException( + ErrorCode.NOTICE_NOT_FOUND, +) diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/CreateNoticeUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/CreateNoticeUseCase.kt new file mode 100644 index 0000000..896478c --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/CreateNoticeUseCase.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request.CreateNoticeRequest + +/** + * 공지사항 생성을 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 새로운 공지사항을 생성하는 역할을 담당합니다. + */ +interface CreateNoticeUseCase { + /** + * 새로운 공지사항을 생성합니다. + * + * @param request 공지사항 생성 요청 데이터 + */ + fun execute(request: CreateNoticeRequest) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/DeleteNoticeUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/DeleteNoticeUseCase.kt new file mode 100644 index 0000000..cb0fb78 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/DeleteNoticeUseCase.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import java.util.UUID + +/** + * 공지사항 삭제를 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 특정 공지사항을 삭제하는 역할을 담당합니다. + */ +interface DeleteNoticeUseCase { + /** + * 특정 공지사항을 삭제합니다. + * + * @param noticeId 삭제할 공지사항의 고유 식별자 + */ + fun execute(noticeId: UUID) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryDetailsNoticeUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryDetailsNoticeUseCase.kt new file mode 100644 index 0000000..610d870 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryDetailsNoticeUseCase.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryDetailsNoticeResponse +import java.util.UUID + +/** + * 공지사항 상세 조회를 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 특정 공지사항의 상세 정보를 조회하는 역할을 담당합니다. + */ +interface QueryDetailsNoticeUseCase { + /** + * 특정 공지사항의 상세 정보를 조회합니다. + * + * @param noticeId 조회할 공지사항의 고유 식별자 + * @return 공지사항 상세 정보 응답 + */ + fun execute(noticeId: UUID): QueryDetailsNoticeResponse +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryNoticeListByTypeUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryNoticeListByTypeUseCase.kt new file mode 100644 index 0000000..dd1c98b --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryNoticeListByTypeUseCase.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryListNoticeResponse +import hs.kr.entrydsm.feed.model.notice.type.NoticeType + +/** + * 유형별 공지사항 목록 조회를 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 특정 유형의 공지사항 목록을 조회하는 역할을 담당합니다. + */ +interface QueryNoticeListByTypeUseCase { + /** + * 특정 유형의 공지사항 목록을 조회합니다. + * + * @param noticeType 조회할 공지사항 유형 (null인 경우 모든 유형 포함) + * @return 공지사항 목록 응답 + */ + fun execute(noticeType: NoticeType?): QueryListNoticeResponse +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryNoticeTitleUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryNoticeTitleUseCase.kt new file mode 100644 index 0000000..fbd6a46 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/QueryNoticeTitleUseCase.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryNoticeTitleResponse + +/** + * 공지사항 제목 조회를 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 공지사항의 제목 목록을 조회하는 역할을 담당합니다. + */ +interface QueryNoticeTitleUseCase { + /** + * 공지사항 제목 목록을 조회합니다. + * + * @return 공지사항 제목 목록 응답 + */ + fun execute(): List +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/UpdateNoticeUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/UpdateNoticeUseCase.kt new file mode 100644 index 0000000..cee04a8 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/UpdateNoticeUseCase.kt @@ -0,0 +1,23 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request.UpdateNoticeRequest +import org.springframework.http.ResponseEntity +import java.util.UUID + +/** + * 공지사항 수정을 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 기존 공지사항을 수정하는 역할을 담당합니다. + */ +interface UpdateNoticeUseCase { + /** + * 특정 공지사항을 수정합니다. + * + * @param noticeId 수정할 공지사항의 고유 식별자 + * @param request 공지사항 수정 요청 데이터 + * @return 수정 결과 응답 엔티티 + */ + fun execute( + noticeId: UUID, + request: UpdateNoticeRequest, + ): ResponseEntity +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/UploadNoticeImageUseCase.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/UploadNoticeImageUseCase.kt new file mode 100644 index 0000000..9f204dc --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/in/UploadNoticeImageUseCase.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.feed.application.notice.port.`in` + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.UploadNoticeImageResponse +import org.springframework.web.multipart.MultipartFile + +/** + * 공지사항 이미지 업로드를 위한 유스케이스 인터페이스입니다. + * 공지사항 도메인에서 공지사항에 첨부할 이미지를 업로드하는 역할을 담당합니다. + */ +interface UploadNoticeImageUseCase { + /** + * 공지사항에 첨부할 이미지를 업로드합니다. + * + * @param image 업로드할 이미지 파일 + * @return 이미지 업로드 결과 응답 + */ + fun execute(image: MultipartFile): UploadNoticeImageResponse +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/DeleteNoticePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/DeleteNoticePort.kt new file mode 100644 index 0000000..e0bce98 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/DeleteNoticePort.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.feed.application.notice.port.out + +import hs.kr.entrydsm.feed.model.notice.Notice + +/** + * 공지사항 삭제를 위한 포트 인터페이스입니다. + * 공지사항 도메인 객체를 삭제하는 메서드를 정의합니다. + */ +interface DeleteNoticePort { + /** + * 공지사항을 삭제합니다. + * + * @param notice 삭제할 공지사항 도메인 객체 + */ + fun deleteNotice(notice: Notice) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/FindNoticePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/FindNoticePort.kt new file mode 100644 index 0000000..28fecd7 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/FindNoticePort.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.feed.application.notice.port.out + +import hs.kr.entrydsm.feed.model.notice.Notice +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import java.util.UUID + +/** + * 공지사항 조회를 위한 포트 인터페이스입니다. + * 공지사항 도메인 객체를 다양한 방식으로 조회하는 메서드를 정의합니다. + */ +interface FindNoticePort { + /** + * 공지사항 ID로 공지사항을 조회합니다. + * + * @param noticeId 조회할 공지사항의 고유 식별자 + * @return 조회된 공지사항 객체, 없을 경우 null + */ + fun findByIdOrNull(noticeId: UUID): Notice? + + /** + * 특정 유형의 모든 공지사항을 조회합니다. + * + * @param noticeType 조회할 공지사항 유형 + * @return 해당 유형의 공지사항 목록 (없을 경우 빈 목록) + */ + fun findAllByType(noticeType: NoticeType): List + + /** + * 모든 공지사항을 조회합니다. + * + * @return 모든 공지사항 목록 (없을 경우 빈 목록) + */ + fun findAll(): List +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/SaveNoticePort.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/SaveNoticePort.kt new file mode 100644 index 0000000..3a54122 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/port/out/SaveNoticePort.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.feed.application.notice.port.out + +import hs.kr.entrydsm.feed.model.notice.Notice + +/** + * 공지사항 저장을 위한 포트 인터페이스입니다. + * 공지사항 도메인 객체를 저장하거나 업데이트하는 메서드를 정의합니다. + */ +interface SaveNoticePort { + /** + * 공지사항을 저장하거나 업데이트합니다. + * + * @param notice 저장하거나 업데이트할 공지사항 도메인 객체 + */ + fun saveNotice(notice: Notice) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/CreateNoticeService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/CreateNoticeService.kt new file mode 100644 index 0000000..f3f7f69 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/CreateNoticeService.kt @@ -0,0 +1,54 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request.CreateNoticeRequest +import hs.kr.entrydsm.feed.application.attachFile.port.out.FindAttachFilePort +import hs.kr.entrydsm.feed.application.notice.exception.AttachFileNotFoundException +import hs.kr.entrydsm.feed.application.notice.port.`in`.CreateNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.out.SaveNoticePort +import hs.kr.entrydsm.feed.global.utils.admin.AdminUtils +import hs.kr.entrydsm.feed.model.notice.Notice +import org.springframework.stereotype.Service + +/** + * 공지사항 생성을 처리하는 서비스 클래스입니다. + * + * @property adminUtils 관리자 인증 유틸리티 + * @property findAttachFilePort 첨부 파일 조회를 위한 포트 + * @property saveNoticePort 공지사항 저장을 위한 포트 + */ +@Service +class CreateNoticeService( + private val adminUtils: AdminUtils, + private val findAttachFilePort: FindAttachFilePort, + private val saveNoticePort: SaveNoticePort, +) : CreateNoticeUseCase { + /** + * 새로운 공지사항을 생성합니다. + * + * @param request 공지사항 생성 요청 데이터 + * @throws hs.kr.entrydsm.feed.application.notice.exception.AttachFileNotFoundException 첨부 파일을 찾을 수 없는 경우 + * @throws hs.kr.entrydsm.feed.global.exception.UnauthorizedException 관리자 인증에 실패한 경우 + */ + override fun execute(request: CreateNoticeRequest) { + val admin = adminUtils.getCurrentAdminId() + val attachFiles = + request.attachFileName?.let { fileNames -> + fileNames.flatMap { fileName -> + val files = findAttachFilePort.findByOriginalAttachFileName(fileName) + files ?: throw AttachFileNotFoundException + } + } ?: emptyList() + + saveNoticePort.saveNotice( + Notice( + title = request.title, + content = request.content, + type = request.type, + isPinned = request.isPinned, + adminId = admin, + fileName = request.fileName, + attachFile = attachFiles, + ), + ) + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/DeleteNoticeService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/DeleteNoticeService.kt new file mode 100644 index 0000000..9401424 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/DeleteNoticeService.kt @@ -0,0 +1,31 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.application.notice.exception.NoticeNotFoundException +import hs.kr.entrydsm.feed.application.notice.port.`in`.DeleteNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.out.DeleteNoticePort +import hs.kr.entrydsm.feed.application.notice.port.out.FindNoticePort +import org.springframework.stereotype.Service +import java.util.UUID + +/** + * 공지사항 삭제를 처리하는 서비스 클래스입니다. + * + * @property findNoticePort 공지사항 조회를 위한 포트 + * @property deleteNoticePort 공지사항 삭제를 위한 포트 + */ +@Service +class DeleteNoticeService( + private val findNoticePort: FindNoticePort, + private val deleteNoticePort: DeleteNoticePort, +) : DeleteNoticeUseCase { + /** + * 지정된 ID의 공지사항을 삭제합니다. + * + * @param noticeId 삭제할 공지사항의 고유 식별자 + * @throws hs.kr.entrydsm.feed.application.notice.exception.NoticeNotFoundException 지정된 ID의 공지사항을 찾을 수 없는 경우 + */ + override fun execute(noticeId: UUID) { + val notice = findNoticePort.findByIdOrNull(noticeId) ?: throw NoticeNotFoundException + deleteNoticePort.deleteNotice(notice) + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryDetailsNoticeService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryDetailsNoticeService.kt new file mode 100644 index 0000000..6475d81 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryDetailsNoticeService.kt @@ -0,0 +1,69 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.AttachFileElement +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryDetailsNoticeResponse +import hs.kr.entrydsm.feed.application.notice.exception.NoticeNotFoundException +import hs.kr.entrydsm.feed.application.notice.port.`in`.QueryDetailsNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.out.FindNoticePort +import hs.kr.entrydsm.feed.infrastructure.s3.PathList +import hs.kr.entrydsm.feed.infrastructure.s3.util.FileUtil +import org.springframework.stereotype.Service +import java.util.UUID + +/** + * 공지사항 상세 조회를 처리하는 서비스 클래스입니다. + * + * @property findNoticePort 공지사항 조회를 위한 포트 + * @property fileUtil 파일 URL 생성을 위한 유틸리티 + */ +@Service +class QueryDetailsNoticeService( + private val findNoticePort: FindNoticePort, + private val fileUtil: FileUtil, +) : QueryDetailsNoticeUseCase { + /** + * 공지사항의 상세 정보를 조회합니다. + * + * @param noticeId 조회할 공지사항의 고유 식별자 + * @return 공지사항 상세 정보 응답 + * @throws hs.kr.entrydsm.feed.application.notice.exception.NoticeNotFoundException 지정된 ID의 공지사항을 찾을 수 없는 경우 + */ + override fun execute(noticeId: UUID): QueryDetailsNoticeResponse { + val notice = findNoticePort.findByIdOrNull(noticeId) ?: throw NoticeNotFoundException + val imageURL = notice.fileName?.let { getUrl(it, PathList.NOTICE) } + + val attachFile = + notice.attachFile?.map { + // 첨부파일 이름과 url을 묶어서 여러개 반환하기때문에 List로 묶는다 + AttachFileElement( + attachFileUrl = getUrl(it.uploadedFileName, PathList.ATTACH_FILE), + attachFileName = it.originalAttachFileName, + ) + } + + return notice.run { + QueryDetailsNoticeResponse( + title = title, + content = content, + createdAt = createdAt, + type = type, + imageURL = imageURL, + imageName = fileName, + attachFiles = attachFile!!, + isPinned = isPinned, + ) + } + } + + /** + * 파일 경로와 파일명으로 URL을 생성합니다. + * + * @param file 파일명 + * @param path 파일 경로 + * @return 생성된 파일 URL + */ + private fun getUrl( + file: String, + path: String, + ) = fileUtil.generateObjectUrl(file, path) +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryNoticeListByTypeService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryNoticeListByTypeService.kt new file mode 100644 index 0000000..39dc2bf --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryNoticeListByTypeService.kt @@ -0,0 +1,53 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.NoticeResponse +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryListNoticeResponse +import hs.kr.entrydsm.feed.application.notice.port.`in`.QueryNoticeListByTypeUseCase +import hs.kr.entrydsm.feed.application.notice.port.out.FindNoticePort +import hs.kr.entrydsm.feed.model.notice.Notice +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import org.springframework.stereotype.Service + +/** + * 유형별 공지사항 목록 조회를 처리하는 서비스 클래스입니다. + * + * @property findNoticePort 공지사항 조회를 위한 포트 + */ +@Service +class QueryNoticeListByTypeService( + private val findNoticePort: FindNoticePort, +) : QueryNoticeListByTypeUseCase { + /** + * 공지사항 목록을 조회합니다. 유형별로 필터링할 수 있습니다. + * + * @param noticeType 조회할 공지사항 유형 (null인 경우 모든 유형 조회) + * @return 공지사항 목록 응답 (고정 공지가 상단에 정렬됨) + */ + override fun execute(noticeType: NoticeType?): QueryListNoticeResponse { + val notices = + getNoticeList(noticeType).map { it -> + NoticeResponse( + id = it.id!!, + title = it.title, + isPinned = it.isPinned, + type = it.type, + createdAt = it.createdAt, + ) + }.sortedWith( + compareByDescending { it.isPinned } + .thenByDescending { it.createdAt }, + ) + + return QueryListNoticeResponse(notices) + } + + /** + * 공지사항 목록을 조회합니다. 유형별로 필터링할 수 있습니다. + * + * @param noticeType 조회할 공지사항 유형 (null인 경우 모든 유형 조회) + * @return 공지사항 목록 + */ + private fun getNoticeList(noticeType: NoticeType?): List { + return noticeType?.let { findNoticePort.findAllByType(it) } ?: findNoticePort.findAll() + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryNoticeTitleService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryNoticeTitleService.kt new file mode 100644 index 0000000..20445e5 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/QueryNoticeTitleService.kt @@ -0,0 +1,33 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.QueryNoticeTitleResponse +import hs.kr.entrydsm.feed.application.notice.port.`in`.QueryNoticeTitleUseCase +import hs.kr.entrydsm.feed.application.notice.port.out.FindNoticePort +import org.springframework.stereotype.Service + +/** + * 공지사항 제목 조회를 처리하는 서비스 클래스입니다. + * + * @property findNoticePort 공지사항 조회를 위한 포트 + */ +@Service +class QueryNoticeTitleService( + private val findNoticePort: FindNoticePort, +) : QueryNoticeTitleUseCase { + /** + * 모든 공지사항의 제목과 작성일을 조회합니다. + * + * @return 공지사항 제목 목록 (최신순으로 정렬) + */ + override fun execute(): List { + return findNoticePort.findAll() + .map { + it -> + QueryNoticeTitleResponse( + id = it.id!!, + title = it.title, + it.createdAt, + ) + }.sortedByDescending { it.createdAt } + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/UpdateNoticeService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/UpdateNoticeService.kt new file mode 100644 index 0000000..7a21bec --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/UpdateNoticeService.kt @@ -0,0 +1,83 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.request.UpdateNoticeRequest +import hs.kr.entrydsm.feed.application.attachFile.port.out.FindAttachFilePort +import hs.kr.entrydsm.feed.application.notice.exception.AttachFileNotFoundException +import hs.kr.entrydsm.feed.application.notice.exception.NoticeNotFoundException +import hs.kr.entrydsm.feed.application.notice.port.`in`.UpdateNoticeUseCase +import hs.kr.entrydsm.feed.application.notice.port.out.FindNoticePort +import hs.kr.entrydsm.feed.global.utils.admin.AdminUtils +import hs.kr.entrydsm.feed.infrastructure.s3.PathList +import hs.kr.entrydsm.feed.infrastructure.s3.util.FileUtil +import hs.kr.entrydsm.feed.model.attachFile.AttachFile +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Service +import java.util.UUID + +/** + * 공지사항 수정을 처리하는 서비스 클래스입니다. + * + * @property findNoticePort 공지사항 조회를 위한 포트 + * @property fileUtil 파일 업로드 유틸리티 + * @property adminUtils 관리자 인증 유틸리티 + * @property findAttachFilePort 첨부 파일 조회를 위한 포트 + */ +@Service +class UpdateNoticeService( + private val findNoticePort: FindNoticePort, + private val fileUtil: FileUtil, + private val adminUtils: AdminUtils, + private val findAttachFilePort: FindAttachFilePort, +) : UpdateNoticeUseCase { + /** + * 공지사항을 수정합니다. + * + * @param noticeId 수정할 공지사항의 고유 식별자 + * @param request 공지사항 수정 요청 데이터 + * @return 수정된 이미지 URL이 포함된 응답 (이미지가 없는 경우 NO_CONTENT) + * @throws hs.kr.entrydsm.feed.application.notice.exception.NoticeNotFoundException 지정된 ID의 공지사항을 찾을 수 없는 경우 + * @throws hs.kr.entrydsm.feed.application.notice.exception.AttachFileNotFoundException 첨부 파일을 찾을 수 없는 경우 + * @throws hs.kr.entrydsm.feed.global.exception.UnauthorizedException 관리자 인증에 실패한 경우 + */ + override fun execute( + noticeId: UUID, + request: UpdateNoticeRequest, + ): ResponseEntity { + val adminId = adminUtils.getCurrentAdminId() + + val notice = findNoticePort.findByIdOrNull(noticeId) ?: throw NoticeNotFoundException + val fileName = request.fileName + val attachFiles = findAttachFiles(request.attachFileName) + + request.run { + notice.updateNotice( + newTitle = title, + newContent = content, + newIsPinned = isPinned, + newType = type, + newFileName = fileName, + newAdminId = adminId, + newAttachFile = attachFiles, + ) + } + + return fileName?.let { + ResponseEntity.ok(fileUtil.generateObjectUrl(it, PathList.NOTICE)) + } ?: ResponseEntity(HttpStatus.NO_CONTENT) + } + + /** + * 파일명 목록으로 첨부 파일 목록을 조회합니다. + * + * @param fileNameList 조회할 파일명 목록 (null인 경우 빈 목록 반환) + * @return 조회된 첨부 파일 목록 + * @throws hs.kr.entrydsm.feed.application.notice.exception.AttachFileNotFoundException 첨부 파일을 찾을 수 없는 경우 + */ + private fun findAttachFiles(fileNameList: List?): List { + return fileNameList?.flatMap { fileName -> + val fileList = findAttachFilePort.findByOriginalAttachFileName(fileName) + fileList ?: throw AttachFileNotFoundException + } ?: emptyList() + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/UploadNoticeImageService.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/UploadNoticeImageService.kt new file mode 100644 index 0000000..1cc5d5b --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/application/notice/service/UploadNoticeImageService.kt @@ -0,0 +1,31 @@ +package hs.kr.entrydsm.feed.application.notice.service + +import hs.kr.entrydsm.feed.adapter.`in`.notice.dto.response.UploadNoticeImageResponse +import hs.kr.entrydsm.feed.application.notice.port.`in`.UploadNoticeImageUseCase +import hs.kr.entrydsm.feed.infrastructure.s3.PathList +import hs.kr.entrydsm.feed.infrastructure.s3.util.FileUtil +import jakarta.transaction.Transactional +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +/** + * 공지사항 이미지 업로드를 처리하는 서비스 클래스입니다. + * + * @property fileUtil 파일 업로드 유틸리티 + */ +@Transactional +@Service +class UploadNoticeImageService( + private val fileUtil: FileUtil, +) : UploadNoticeImageUseCase { + /** + * 공지사항에 첨부할 이미지를 업로드합니다. + * + * @param image 업로드할 이미지 파일 + * @return 업로드된 이미지의 URL과 파일명이 포함된 응답 + */ + override fun execute(image: MultipartFile): UploadNoticeImageResponse { + val fileName = fileUtil.upload(image, PathList.NOTICE) + return UploadNoticeImageResponse(fileUtil.generateObjectUrl(fileName, PathList.NOTICE), fileName) + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/notice/Notice.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/notice/Notice.kt new file mode 100644 index 0000000..12f4f7a --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/notice/Notice.kt @@ -0,0 +1,63 @@ +package hs.kr.entrydsm.feed.model.notice + +import hs.kr.entrydsm.feed.model.attachFile.AttachFile +import hs.kr.entrydsm.feed.model.notice.type.NoticeType +import java.time.LocalDateTime +import java.util.UUID + +/** + * 공지사항 도메인 모델 클래스입니다. + * + * @property id 공지사항의 고유 식별자 (생성 시 자동 할당) + * @property title 공지사항 제목 + * @property content 공지사항 내용 + * @property fileName 첨부 파일명 (선택 사항) + * @property attachFile 첨부 파일 목록 (선택 사항) + * @property adminId 공지사항을 작성한 관리자 ID + * @property isPinned 상단 고정 여부 + * @property type 공지사항 유형 + * @property createdAt 공지사항 생성 일시 (기본값: 현재 시간) + */ +data class Notice( + val id: UUID? = null, + val title: String, + val content: String, + val fileName: String? = null, + val attachFile: List? = emptyList(), + val adminId: UUID, + val isPinned: Boolean, + val type: NoticeType, + val createdAt: LocalDateTime = LocalDateTime.now(), +) { + /** + * 공지사항 정보를 업데이트합니다. + * + * @param newTitle 새로운 제목 + * @param newContent 새로운 내용 + * @param newFileName 새로운 파일명 (선택 사항) + * @param newAttachFile 새로운 첨부 파일 목록 (선택 사항) + * @param newAdminId 수정한 관리자 ID + * @param newIsPinned 새로운 상단 고정 여부 + * @param newType 새로운 공지사항 유형 + * @return 업데이트된 Notice 객체 + */ + fun updateNotice( + newTitle: String, + newContent: String, + newFileName: String?, + newAttachFile: List? = emptyList(), + newAdminId: UUID, + newIsPinned: Boolean, + newType: NoticeType, + ): Notice { + return copy( + title = newTitle, + content = newContent, + fileName = newFileName, + attachFile = newAttachFile, + adminId = newAdminId, + isPinned = newIsPinned, + type = newType, + ) + } +} diff --git a/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/notice/type/NoticeType.kt b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/notice/type/NoticeType.kt new file mode 100644 index 0000000..1795c36 --- /dev/null +++ b/casper-feed/src/main/kotlin/hs/kr/entrydsm/feed/model/notice/type/NoticeType.kt @@ -0,0 +1,12 @@ +package hs.kr.entrydsm.feed.model.notice.type + +/** + * 공지사항의 유형을 나타내는 열거형 클래스입니다. + * + * @property GUIDE 안내사항을 나타내는 유형입니다. (가이드) + * @property NOTICE 공지사항을 나타내는 유형입니다. (일반 공지) + */ +enum class NoticeType { + GUIDE, + NOTICE, +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle.kts b/settings.gradle.kts index fc3189a..88a27fd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,7 +2,6 @@ rootProject.name = "Casper-Feed" pluginManagement { includeBuild("casper-convention") - includeBuild("build-logic") repositories { gradlePluginPortal() mavenCentral() @@ -15,3 +14,4 @@ dependencyResolutionManagement { } } +include("casper-feed") diff --git a/src/main/kotlin/hs/kr/entrydsm/feed/CasperFeedApplication.kt b/src/main/kotlin/hs/kr/entrydsm/feed/CasperFeedApplication.kt deleted file mode 100644 index 8f4e864..0000000 --- a/src/main/kotlin/hs/kr/entrydsm/feed/CasperFeedApplication.kt +++ /dev/null @@ -1,11 +0,0 @@ -package hs.kr.entrydsm.feed - -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication - -@SpringBootApplication -class CasperFeedApplication - -fun main(args: Array) { - runApplication(*args) -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index e5a9199..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=Casper-Feed diff --git a/src/test/kotlin/hs/kr/entrydsm/feed/CasperFeedApplicationTests.kt b/src/test/kotlin/hs/kr/entrydsm/feed/CasperFeedApplicationTests.kt deleted file mode 100644 index f496918..0000000 --- a/src/test/kotlin/hs/kr/entrydsm/feed/CasperFeedApplicationTests.kt +++ /dev/null @@ -1,11 +0,0 @@ -package hs.kr.entrydsm.feed - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class CasperFeedApplicationTests { - @Test - fun contextLoads() { - } -}