diff --git a/systems/notification/notification-adapter-in/BUILD.bazel b/systems/notification/notification-adapter-in/BUILD.bazel index c5a17707..c8cae5de 100644 --- a/systems/notification/notification-adapter-in/BUILD.bazel +++ b/systems/notification/notification-adapter-in/BUILD.bazel @@ -17,5 +17,5 @@ kt_jvm_test( javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", test_class = "hs.kr.entrydsm.notification.adapterin.NotificationAdapterInModuleTest", - deps = MODULE_DEPS + TEST_DEPS, + deps = [":main"] + MODULE_DEPS + TEST_DEPS, ) diff --git a/systems/notification/notification-adapter-in/deps.bzl b/systems/notification/notification-adapter-in/deps.bzl index 5cf03643..93fdf9b7 100644 --- a/systems/notification/notification-adapter-in/deps.bzl +++ b/systems/notification/notification-adapter-in/deps.bzl @@ -1,4 +1,9 @@ -KOTLIN_DEPS = [] +# Dependencies for the notification REST adapter module. +KOTLIN_DEPS = [ + "@maven//:org_springframework_boot_spring_boot_starter_web", + "//systems/notification/notification-application:main", + "//systems/notification/notification-domain:main", +] TEST_DEPS = [ "@maven//:junit_junit", diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/NotificationController.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/NotificationController.kt new file mode 100644 index 00000000..92adcd91 --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/NotificationController.kt @@ -0,0 +1,99 @@ +package hs.kr.entrydsm.notification.adapterin.web + +import hs.kr.entrydsm.notification.adapterin.web.dto.common.ApiResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.common.toResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.FaqDetailResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.FaqSummaryResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.NoticeDetailResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.NoticeSummaryResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.PageResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.RecruitmentGuidelineResponse +import hs.kr.entrydsm.notification.application.port.`in`.NotificationPort +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadFaqPageCommand +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/notification/v11/notifications") +class NotificationController( + private val notificationPort: NotificationPort, +) { + @GetMapping("/notification") + fun getNotices( + @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "10") size: Int, + @RequestParam(required = false) category: String?, + ): ApiResponse> { + val result = notificationPort.getNotices( + ReadNotificationPageCommand.of( + page = page, + size = size, + category = category, + ), + ) + return ApiResponse( + status = 200, + message = "공지 목록 조회 성공", + data = result.toResponse { it.toResponse() }, + ) + } + + @GetMapping("/notification/{id}") + fun getNotice( + @PathVariable id: Long, + ): ApiResponse { + val result = notificationPort.getNotice(id) + return ApiResponse( + status = 200, + message = "공지 상세 조회 성공", + data = result.toResponse(), + ) + } + + @GetMapping("/qna") + fun getFaqs( + @RequestParam(defaultValue = "0") page: Int, + @RequestParam(defaultValue = "10") size: Int, + @RequestParam(required = false) category: String?, + ): ApiResponse> { + val result = notificationPort.getFaqs( + ReadFaqPageCommand.of( + page = page, + size = size, + category = category, + ), + ) + return ApiResponse( + status = 200, + message = "자주 묻는 질문 목록 조회 성공", + data = result.toResponse { it.toResponse() }, + ) + } + + @GetMapping("/qna/{id}") + fun getFaq( + @PathVariable id: Long, + ): ApiResponse { + val result = notificationPort.getFaq(id) + return ApiResponse( + status = 200, + message = "자주 묻는 질문 상세 조회 성공", + data = result.toResponse(), + ) + } + + @GetMapping("/guideline") + fun getRecruitmentGuideline(): ApiResponse { + val result = notificationPort.getRecruitmentGuideline() + return ApiResponse( + status = 200, + message = "전형 요강 상세 조회 성공", + data = result.toResponse(), + ) + } +} + diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ApiResponse.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ApiResponse.kt new file mode 100644 index 00000000..12d793ec --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ApiResponse.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.common + +data class ApiResponse( + val status: Int, + val message: String, + val data: T?, +) + diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ErrorResponse.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ErrorResponse.kt new file mode 100644 index 00000000..3f58f220 --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ErrorResponse.kt @@ -0,0 +1,7 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.common + +data class ErrorResponse( + val status: Int, + val message: String, + val code: String, +) diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ResponseMapper.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ResponseMapper.kt new file mode 100644 index 00000000..dcc2776b --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ResponseMapper.kt @@ -0,0 +1,77 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.common + +import hs.kr.entrydsm.notification.adapterin.web.dto.response.FaqDetailResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.FaqSummaryResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.NoticeDetailResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.NoticeSummaryResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.PageResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.RecruitmentGuidelineResponse +import hs.kr.entrydsm.notification.adapterin.web.dto.response.RecruitmentScheduleResponse +import hs.kr.entrydsm.notification.application.port.`in`.result.FaqDetailResult +import hs.kr.entrydsm.notification.application.port.`in`.result.FaqSummaryResult +import hs.kr.entrydsm.notification.application.port.`in`.result.NoticeDetailResult +import hs.kr.entrydsm.notification.application.port.`in`.result.NoticeSummaryResult +import hs.kr.entrydsm.notification.application.port.`in`.result.PageResult +import hs.kr.entrydsm.notification.application.port.`in`.result.RecruitmentGuidelineResult + +fun NoticeSummaryResult.toResponse(): NoticeSummaryResponse = + NoticeSummaryResponse( + noticeId = noticeId, + title = title, + author = author, + createdAt = createdAt, + ) + +fun NoticeDetailResult.toResponse(): NoticeDetailResponse = + NoticeDetailResponse( + noticeId = noticeId, + title = title, + content = content, + author = author, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) + +fun FaqSummaryResult.toResponse(): FaqSummaryResponse = + FaqSummaryResponse( + faqId = faqId, + category = category, + question = question, + answer = answer, + ) + +fun FaqDetailResult.toResponse(): FaqDetailResponse = + FaqDetailResponse( + faqId = faqId, + category = category, + question = question, + answer = answer, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) + +fun RecruitmentGuidelineResult.toResponse(): RecruitmentGuidelineResponse = + RecruitmentGuidelineResponse( + recruitmentId = recruitmentId, + title = title, + description = description, + schedule = RecruitmentScheduleResponse( + applicationStart = schedule.applicationStart, + applicationEnd = schedule.applicationEnd, + resultAt = schedule.resultAt, + ), + createdAt = createdAt, + updatedAt = updatedAt, + ) + +fun PageResult.toResponse(mapper: (T) -> R): PageResponse = + PageResponse( + content = content.map(mapper), + page = page, + size = size, + totalElements = totalElements, + totalPages = totalPages, + last = last, + ) diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/FaqResponses.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/FaqResponses.kt new file mode 100644 index 00000000..7c74c401 --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/FaqResponses.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.response + +import java.time.LocalDateTime + +data class FaqSummaryResponse( + val faqId: Long, + val category: String, + val question: String, + val answer: String, +) + +data class FaqDetailResponse( + val faqId: Long, + val category: String, + val question: String, + val answer: String, + val viewCount: Int, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/NoticeResponses.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/NoticeResponses.kt new file mode 100644 index 00000000..a98a8564 --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/NoticeResponses.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.response + +import java.time.LocalDateTime + +data class NoticeSummaryResponse( + val noticeId: Long, + val title: String, + val author: String, + val createdAt: LocalDateTime, +) + +data class NoticeDetailResponse( + val noticeId: Long, + val title: String, + val content: String, + val author: String, + val viewCount: Int, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/PageResponse.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/PageResponse.kt new file mode 100644 index 00000000..798256fe --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/PageResponse.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.response + +data class PageResponse( + val content: List, + val page: Int, + val size: Int, + val totalElements: Long, + val totalPages: Int, + val last: Boolean, +) + diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/RecruitmentGuidelineResponse.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/RecruitmentGuidelineResponse.kt new file mode 100644 index 00000000..4c129d2f --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/RecruitmentGuidelineResponse.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.notification.adapterin.web.dto.response + +import java.time.LocalDate +import java.time.LocalDateTime + +data class RecruitmentGuidelineResponse( + val recruitmentId: Long, + val title: String, + val description: String, + val schedule: RecruitmentScheduleResponse, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + +data class RecruitmentScheduleResponse( + val applicationStart: LocalDate, + val applicationEnd: LocalDate, + val resultAt: LocalDate, +) diff --git a/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/exception/GlobalExceptionHandler.kt b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/exception/GlobalExceptionHandler.kt new file mode 100644 index 00000000..defca948 --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/exception/GlobalExceptionHandler.kt @@ -0,0 +1,72 @@ +package hs.kr.entrydsm.notification.adapterin.web.exception + +import hs.kr.entrydsm.notification.adapterin.web.dto.common.ErrorResponse +import hs.kr.entrydsm.notification.application.exception.NotificationNotFoundException +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.HttpMessageNotReadableException +import org.springframework.web.bind.MissingPathVariableException +import org.springframework.web.bind.MissingServletRequestParameterException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException + +@RestControllerAdvice +class GlobalExceptionHandler { + private val logger = LoggerFactory.getLogger(javaClass) + + @ExceptionHandler(NotificationNotFoundException::class) + fun handleNotFound(exception: NotificationNotFoundException): ResponseEntity = + response( + status = HttpStatus.NOT_FOUND, + code = "NOTIFICATION_NOT_FOUND", + message = "notification not found", + ) + + @ExceptionHandler( + IllegalArgumentException::class, + HttpMessageNotReadableException::class, + MethodArgumentNotValidException::class, + MissingPathVariableException::class, + MissingServletRequestParameterException::class, + MethodArgumentTypeMismatchException::class, + ) + fun handleInvalidRequest(exception: Exception): ResponseEntity = + response( + status = HttpStatus.BAD_REQUEST, + code = "INVALID_REQUEST", + message = "invalid request", + ) + + @ExceptionHandler(Exception::class) + fun handleUnhandledException(exception: Exception): ResponseEntity = + response( + status = HttpStatus.INTERNAL_SERVER_ERROR, + code = "INTERNAL_SERVER_ERROR", + message = "internal server error", + ).also { + logger.error( + "Unhandled exception [correlationId={}]", + MDC.get("correlationId") ?: "unknown", + exception, + ) + } + + private fun response( + status: HttpStatus, + code: String, + message: String, + ): ResponseEntity = + ResponseEntity + .status(status) + .body( + ErrorResponse( + status = status.value(), + message = message, + code = code, + ), + ) +} diff --git a/systems/notification/notification-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt b/systems/notification/notification-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt index 5d76ee0c..d09cb167 100644 --- a/systems/notification/notification-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt +++ b/systems/notification/notification-adapter-in/src/test/kotlin/hs/kr/entrydsm/TestMain.kt @@ -1,11 +1,48 @@ package hs.kr.entrydsm.notification.adapterin +import hs.kr.entrydsm.notification.adapterin.web.exception.GlobalExceptionHandler +import hs.kr.entrydsm.notification.application.exception.NotificationNotFoundException +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import org.springframework.http.HttpStatus class NotificationAdapterInModuleTest { @Test fun moduleLoads() { assertTrue(true) } + + @Test + fun notFoundExceptionReturnsStableErrorResponse() { + val response = GlobalExceptionHandler() + .handleNotFound(NotificationNotFoundException("notice not found: id=1")) + + assertEquals(HttpStatus.NOT_FOUND, response.statusCode) + assertEquals(404, response.body?.status) + assertEquals("NOTIFICATION_NOT_FOUND", response.body?.code) + assertEquals("notification not found", response.body?.message) + } + + @Test + fun invalidRequestReturnsStableErrorResponse() { + val response = GlobalExceptionHandler() + .handleInvalidRequest(IllegalArgumentException("page must be greater than or equal to 0")) + + assertEquals(HttpStatus.BAD_REQUEST, response.statusCode) + assertEquals(400, response.body?.status) + assertEquals("INVALID_REQUEST", response.body?.code) + assertEquals("invalid request", response.body?.message) + } + + @Test + fun unhandledExceptionReturnsStableErrorResponse() { + val response = GlobalExceptionHandler() + .handleUnhandledException(RuntimeException("database connection failed")) + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.statusCode) + assertEquals(500, response.body?.status) + assertEquals("INTERNAL_SERVER_ERROR", response.body?.code) + assertEquals("internal server error", response.body?.message) + } } diff --git a/systems/notification/notification-adapter-out/BUILD.bazel b/systems/notification/notification-adapter-out/BUILD.bazel index 51ce08b6..34c5ea7e 100644 --- a/systems/notification/notification-adapter-out/BUILD.bazel +++ b/systems/notification/notification-adapter-out/BUILD.bazel @@ -17,5 +17,5 @@ kt_jvm_test( javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", test_class = "hs.kr.entrydsm.notification.adapterout.NotificationAdapterOutModuleTest", - deps = MODULE_DEPS + TEST_DEPS, + deps = [":main"] + MODULE_DEPS + TEST_DEPS, ) diff --git a/systems/notification/notification-adapter-out/deps.bzl b/systems/notification/notification-adapter-out/deps.bzl index 5cf03643..c78aea27 100644 --- a/systems/notification/notification-adapter-out/deps.bzl +++ b/systems/notification/notification-adapter-out/deps.bzl @@ -1,4 +1,9 @@ -KOTLIN_DEPS = [] +KOTLIN_DEPS = [ + "@maven//:org_springframework_boot_spring_boot_starter_data_jpa", + "@maven//:com_mysql_mysql_connector_j", + "//systems/notification/notification-application:main", + "//systems/notification/notification-domain:main", +] TEST_DEPS = [ "@maven//:junit_junit", diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/FaqJpaEntity.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/FaqJpaEntity.kt new file mode 100644 index 00000000..95896eb2 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/FaqJpaEntity.kt @@ -0,0 +1,49 @@ +package hs.kr.entrydsm.notification.adapterout.entity + +import hs.kr.entrydsm.notification.domain.model.Faq +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.time.LocalDateTime + +@Entity +@Table(name = "faqs") +open class FaqJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + var id: Long? = null, + + @Column(name = "category", nullable = false, length = 50) + var category: String = "", + + @Column(name = "question", nullable = false, length = 255) + var question: String = "", + + @Column(name = "answer", nullable = false, columnDefinition = "TEXT") + var answer: String = "", + + @Column(name = "view_count", nullable = false) + var viewCount: Int = 0, + + @Column(name = "created_at", nullable = false) + var createdAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "updated_at", nullable = false) + var updatedAt: LocalDateTime = LocalDateTime.now(), +) { + fun toDomain(): Faq = + Faq( + id = requireNotNull(id), + category = category, + question = question, + answer = answer, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/NoticeJpaEntity.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/NoticeJpaEntity.kt new file mode 100644 index 00000000..8d0ab485 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/NoticeJpaEntity.kt @@ -0,0 +1,57 @@ +package hs.kr.entrydsm.notification.adapterout.entity + +import hs.kr.entrydsm.notification.domain.model.Notice +import hs.kr.entrydsm.notification.domain.model.NoticeCategory +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.time.LocalDateTime + +@Entity +@Table(name = "notices") +open class NoticeJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + var id: Long? = null, + + @Column(name = "title", nullable = false, length = 255) + var title: String = "", + + @Column(name = "content", nullable = false, columnDefinition = "TEXT") + var content: String = "", + + @Enumerated(EnumType.STRING) + @Column(name = "category", nullable = false, length = 32) + var category: NoticeCategory = NoticeCategory.ADMISSION_NOTICE, + + @Column(name = "author", nullable = false, length = 50) + var author: String = "", + + @Column(name = "view_count", nullable = false) + var viewCount: Int = 0, + + @Column(name = "created_at", nullable = false) + var createdAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "updated_at", nullable = false) + var updatedAt: LocalDateTime = LocalDateTime.now(), +) { + fun toDomain(): Notice = + Notice( + id = requireNotNull(id), + title = title, + content = content, + category = category, + author = author, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/RecruitmentGuidelineJpaEntity.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/RecruitmentGuidelineJpaEntity.kt new file mode 100644 index 00000000..ae03946d --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/RecruitmentGuidelineJpaEntity.kt @@ -0,0 +1,68 @@ +package hs.kr.entrydsm.notification.adapterout.entity + +import hs.kr.entrydsm.notification.domain.model.RecruitmentGuideline +import hs.kr.entrydsm.notification.domain.model.RecruitmentSchedule +import jakarta.persistence.Column +import jakarta.persistence.Embedded +import jakarta.persistence.Embeddable +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.time.LocalDate +import java.time.LocalDateTime + +@Entity +@Table(name = "recruitment_guidelines") +open class RecruitmentGuidelineJpaEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + var id: Long? = null, + + @Column(name = "title", nullable = false, length = 255) + var title: String = "", + + @Column(name = "description", nullable = false, columnDefinition = "TEXT") + var description: String = "", + + @Embedded + var schedule: RecruitmentScheduleEmbeddable = RecruitmentScheduleEmbeddable(), + + @Column(name = "created_at", nullable = false) + var createdAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "updated_at", nullable = false) + var updatedAt: LocalDateTime = LocalDateTime.now(), +) { + fun toDomain(): RecruitmentGuideline = + RecruitmentGuideline( + id = requireNotNull(id), + title = title, + description = description, + schedule = schedule.toDomain(), + createdAt = createdAt, + updatedAt = updatedAt, + ) +} + +@Embeddable +class RecruitmentScheduleEmbeddable( + @Column(name = "application_start", nullable = false) + var applicationStart: LocalDate = LocalDate.MIN, + + @Column(name = "application_end", nullable = false) + var applicationEnd: LocalDate = LocalDate.MIN, + + @Column(name = "result_at", nullable = false) + var resultAt: LocalDate = LocalDate.MIN, +) { + fun toDomain(): RecruitmentSchedule = + RecruitmentSchedule( + applicationStart = applicationStart, + applicationEnd = applicationEnd, + resultAt = resultAt, + ) +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqJpaRepository.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqJpaRepository.kt new file mode 100644 index 00000000..a34230c8 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqJpaRepository.kt @@ -0,0 +1,12 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.adapterout.entity.FaqJpaEntity +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository + +interface FaqJpaRepository : JpaRepository { + fun findAllByOrderByIdAsc(pageable: Pageable): Page + fun findAllByCategoryOrderByIdAsc(category: String, pageable: Pageable): Page +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqPersistenceAdapter.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqPersistenceAdapter.kt new file mode 100644 index 00000000..7d0c0b3a --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqPersistenceAdapter.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadFaqPageCommand +import hs.kr.entrydsm.notification.application.port.out.FaqRepository +import hs.kr.entrydsm.notification.application.port.out.data.PageData +import hs.kr.entrydsm.notification.domain.model.Faq +import org.springframework.data.domain.PageRequest +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +@Transactional(readOnly = true) +class FaqPersistenceAdapter( + private val faqJpaRepository: FaqJpaRepository, +) : FaqRepository { + override fun findPage(command: ReadFaqPageCommand): PageData { + val page = command.category?.let { category -> + faqJpaRepository.findAllByCategoryOrderByIdAsc(category.label, command.toPageRequest()) + } ?: faqJpaRepository.findAllByOrderByIdAsc(command.toPageRequest()) + return PageData( + content = page.content.map { it.toDomain() }, + page = command.page, + size = command.size, + totalElements = page.totalElements, + ) + } + + override fun findById(id: Long): Faq? = + faqJpaRepository.findById(id).orElse(null)?.toDomain() + + private fun ReadFaqPageCommand.toPageRequest(): PageRequest = + PageRequest.of(page, size) +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticeJpaRepository.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticeJpaRepository.kt new file mode 100644 index 00000000..4db1ae7f --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticeJpaRepository.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.adapterout.entity.NoticeJpaEntity +import hs.kr.entrydsm.notification.domain.model.NoticeCategory +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository + +interface NoticeJpaRepository : JpaRepository { + fun findAllByOrderByCreatedAtDescIdDesc(pageable: Pageable): Page + fun findAllByCategoryOrderByCreatedAtDescIdDesc( + category: NoticeCategory, + pageable: Pageable, + ): Page +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticePersistenceAdapter.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticePersistenceAdapter.kt new file mode 100644 index 00000000..e92a589b --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticePersistenceAdapter.kt @@ -0,0 +1,37 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +import hs.kr.entrydsm.notification.application.port.out.NoticeRepository +import hs.kr.entrydsm.notification.application.port.out.data.PageData +import hs.kr.entrydsm.notification.domain.model.Notice +import org.springframework.data.domain.PageRequest +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +@Transactional(readOnly = true) +class NoticePersistenceAdapter( + private val noticeJpaRepository: NoticeJpaRepository, +) : NoticeRepository { + override fun findPage(command: ReadNotificationPageCommand): PageData { + val page = command.category?.let { category -> + noticeJpaRepository.findAllByCategoryOrderByCreatedAtDescIdDesc( + category, + command.toPageRequest(), + ) + } ?: noticeJpaRepository.findAllByOrderByCreatedAtDescIdDesc(command.toPageRequest()) + return PageData( + content = page.content.map { it.toDomain() }, + page = command.page, + size = command.size, + totalElements = page.totalElements, + ) + } + + override fun findById(id: Long): Notice? = + noticeJpaRepository.findById(id).orElse(null)?.toDomain() + + private fun ReadNotificationPageCommand.toPageRequest(): PageRequest = + PageRequest.of(page, size) +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelineJpaRepository.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelineJpaRepository.kt new file mode 100644 index 00000000..74d06d69 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelineJpaRepository.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.adapterout.entity.RecruitmentGuidelineJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface RecruitmentGuidelineJpaRepository : JpaRepository { + fun findTopByOrderByCreatedAtDesc(): RecruitmentGuidelineJpaEntity? +} + diff --git a/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelinePersistenceAdapter.kt b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelinePersistenceAdapter.kt new file mode 100644 index 00000000..26796816 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelinePersistenceAdapter.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.application.port.out.RecruitmentGuidelineRepository +import hs.kr.entrydsm.notification.domain.model.RecruitmentGuideline +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +@Transactional(readOnly = true) +class RecruitmentGuidelinePersistenceAdapter( + private val recruitmentGuidelineJpaRepository: RecruitmentGuidelineJpaRepository, +) : RecruitmentGuidelineRepository { + override fun findCurrent(): RecruitmentGuideline? = + recruitmentGuidelineJpaRepository.findTopByOrderByCreatedAtDesc()?.toDomain() +} + diff --git a/systems/notification/notification-application/BUILD.bazel b/systems/notification/notification-application/BUILD.bazel index a24f5a46..0d840f3a 100644 --- a/systems/notification/notification-application/BUILD.bazel +++ b/systems/notification/notification-application/BUILD.bazel @@ -17,5 +17,5 @@ kt_jvm_test( javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", test_class = "hs.kr.entrydsm.notification.application.NotificationApplicationModuleTest", - deps = MODULE_DEPS + TEST_DEPS, + deps = [":main"] + MODULE_DEPS + TEST_DEPS, ) diff --git a/systems/notification/notification-application/deps.bzl b/systems/notification/notification-application/deps.bzl index 5cf03643..1bf96ca7 100644 --- a/systems/notification/notification-application/deps.bzl +++ b/systems/notification/notification-application/deps.bzl @@ -1,4 +1,6 @@ -KOTLIN_DEPS = [] +KOTLIN_DEPS = [ + "//systems/notification/notification-domain:main", +] TEST_DEPS = [ "@maven//:junit_junit", diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/exception/NotificationNotFoundException.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/exception/NotificationNotFoundException.kt new file mode 100644 index 00000000..b5ec82f7 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/exception/NotificationNotFoundException.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.notification.application.exception + +class NotificationNotFoundException( + message: String, +) : RuntimeException(message) + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/NotificationPort.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/NotificationPort.kt new file mode 100644 index 00000000..1de57da3 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/NotificationPort.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.notification.application.port.`in` + +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadFaqPageCommand +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +import hs.kr.entrydsm.notification.application.port.`in`.result.FaqDetailResult +import hs.kr.entrydsm.notification.application.port.`in`.result.FaqSummaryResult +import hs.kr.entrydsm.notification.application.port.`in`.result.NoticeDetailResult +import hs.kr.entrydsm.notification.application.port.`in`.result.NoticeSummaryResult +import hs.kr.entrydsm.notification.application.port.`in`.result.PageResult +import hs.kr.entrydsm.notification.application.port.`in`.result.RecruitmentGuidelineResult + +interface NotificationPort { + fun getNotices(command: ReadNotificationPageCommand): PageResult + fun getNotice(id: Long): NoticeDetailResult + fun getFaqs(command: ReadFaqPageCommand): PageResult + fun getFaq(id: Long): FaqDetailResult + fun getRecruitmentGuideline(): RecruitmentGuidelineResult +} + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadFaqPageCommand.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadFaqPageCommand.kt new file mode 100644 index 00000000..3fc6f484 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadFaqPageCommand.kt @@ -0,0 +1,29 @@ +package hs.kr.entrydsm.notification.application.port.`in`.command + +import hs.kr.entrydsm.notification.domain.model.FaqCategory + +data class ReadFaqPageCommand( + val page: Int = 0, + val size: Int = 10, + val category: FaqCategory? = null, +) { + init { + require(page >= 0) { "page must be greater than or equal to 0" } + require(size > 0) { "size must be greater than 0" } + } + + fun offset(): Long = page.toLong() * size.toLong() + + companion object { + fun of( + page: Int, + size: Int, + category: String? = null, + ): ReadFaqPageCommand = + ReadFaqPageCommand( + page = page, + size = size, + category = category?.let(FaqCategory::from), + ) + } +} diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadNotificationPageCommand.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadNotificationPageCommand.kt new file mode 100644 index 00000000..5556b356 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadNotificationPageCommand.kt @@ -0,0 +1,29 @@ +package hs.kr.entrydsm.notification.application.port.`in`.command + +import hs.kr.entrydsm.notification.domain.model.NoticeCategory + +data class ReadNotificationPageCommand( + val page: Int = 0, + val size: Int = 10, + val category: NoticeCategory? = null, +) { + init { + require(page >= 0) { "page must be greater than or equal to 0" } + require(size > 0) { "size must be greater than 0" } + } + + fun offset(): Long = page.toLong() * size.toLong() + + companion object { + fun of( + page: Int, + size: Int, + category: String? = null, + ): ReadNotificationPageCommand = + ReadNotificationPageCommand( + page = page, + size = size, + category = category?.let(NoticeCategory::from), + ) + } +} diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/FaqResults.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/FaqResults.kt new file mode 100644 index 00000000..4417e4a8 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/FaqResults.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.notification.application.port.`in`.result + +import java.time.LocalDateTime + +data class FaqSummaryResult( + val faqId: Long, + val category: String, + val question: String, + val answer: String, +) + +data class FaqDetailResult( + val faqId: Long, + val category: String, + val question: String, + val answer: String, + val viewCount: Int, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/NoticeResults.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/NoticeResults.kt new file mode 100644 index 00000000..743e2371 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/NoticeResults.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.notification.application.port.`in`.result + +import java.time.LocalDateTime + +data class NoticeSummaryResult( + val noticeId: Long, + val title: String, + val author: String, + val createdAt: LocalDateTime, +) + +data class NoticeDetailResult( + val noticeId: Long, + val title: String, + val content: String, + val author: String, + val viewCount: Int, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/PageResult.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/PageResult.kt new file mode 100644 index 00000000..6572ad52 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/PageResult.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.notification.application.port.`in`.result + +data class PageResult( + val content: List, + val page: Int, + val size: Int, + val totalElements: Long, + val totalPages: Int, + val last: Boolean, +) + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/RecruitmentGuidelineResult.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/RecruitmentGuidelineResult.kt new file mode 100644 index 00000000..cb39264f --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/RecruitmentGuidelineResult.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.notification.application.port.`in`.result + +import java.time.LocalDate +import java.time.LocalDateTime + +data class RecruitmentGuidelineResult( + val recruitmentId: Long, + val title: String, + val description: String, + val schedule: RecruitmentScheduleResult, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + +data class RecruitmentScheduleResult( + val applicationStart: LocalDate, + val applicationEnd: LocalDate, + val resultAt: LocalDate, +) diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/FaqRepository.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/FaqRepository.kt new file mode 100644 index 00000000..75ea5122 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/FaqRepository.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.notification.application.port.out + +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadFaqPageCommand +import hs.kr.entrydsm.notification.application.port.out.data.PageData +import hs.kr.entrydsm.notification.domain.model.Faq + +interface FaqRepository { + fun findPage(command: ReadFaqPageCommand): PageData + fun findById(id: Long): Faq? +} + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/NoticeRepository.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/NoticeRepository.kt new file mode 100644 index 00000000..2b6880ad --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/NoticeRepository.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.notification.application.port.out + +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +import hs.kr.entrydsm.notification.application.port.out.data.PageData +import hs.kr.entrydsm.notification.domain.model.Notice + +interface NoticeRepository { + fun findPage(command: ReadNotificationPageCommand): PageData + fun findById(id: Long): Notice? +} + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/RecruitmentGuidelineRepository.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/RecruitmentGuidelineRepository.kt new file mode 100644 index 00000000..7abb3447 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/RecruitmentGuidelineRepository.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.notification.application.port.out + +import hs.kr.entrydsm.notification.domain.model.RecruitmentGuideline + +interface RecruitmentGuidelineRepository { + fun findCurrent(): RecruitmentGuideline? +} + diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/data/PageData.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/data/PageData.kt new file mode 100644 index 00000000..bda21eea --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/data/PageData.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.notification.application.port.out.data + +data class PageData( + val content: List, + val page: Int, + val size: Int, + val totalElements: Long, +) { + val totalPages: Int = + if (totalElements == 0L) { + 0 + } else { + ((totalElements - 1) / size).toInt() + 1 + } + + val last: Boolean = page >= totalPages - 1 +} diff --git a/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/service/NotificationService.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/service/NotificationService.kt new file mode 100644 index 00000000..503ea22c --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/service/NotificationService.kt @@ -0,0 +1,107 @@ +package hs.kr.entrydsm.notification.application.service + +import hs.kr.entrydsm.notification.application.exception.NotificationNotFoundException +import hs.kr.entrydsm.notification.application.port.`in`.NotificationPort +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadFaqPageCommand +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +import hs.kr.entrydsm.notification.application.port.`in`.result.FaqDetailResult +import hs.kr.entrydsm.notification.application.port.`in`.result.FaqSummaryResult +import hs.kr.entrydsm.notification.application.port.`in`.result.NoticeDetailResult +import hs.kr.entrydsm.notification.application.port.`in`.result.NoticeSummaryResult +import hs.kr.entrydsm.notification.application.port.`in`.result.PageResult +import hs.kr.entrydsm.notification.application.port.`in`.result.RecruitmentGuidelineResult +import hs.kr.entrydsm.notification.application.port.`in`.result.RecruitmentScheduleResult +import hs.kr.entrydsm.notification.application.port.out.FaqRepository +import hs.kr.entrydsm.notification.application.port.out.NoticeRepository +import hs.kr.entrydsm.notification.application.port.out.RecruitmentGuidelineRepository +import hs.kr.entrydsm.notification.application.port.out.data.PageData +import hs.kr.entrydsm.notification.domain.model.Faq +import hs.kr.entrydsm.notification.domain.model.Notice + +class NotificationService( + private val noticeRepository: NoticeRepository, + private val faqRepository: FaqRepository, + private val recruitmentGuidelineRepository: RecruitmentGuidelineRepository, +) : NotificationPort { + override fun getNotices(command: ReadNotificationPageCommand): PageResult = + noticeRepository.findPage(command).toResult { it.toSummaryResult() } + + override fun getNotice(id: Long): NoticeDetailResult = + noticeRepository.findById(id)?.toDetailResult() + ?: throw NotificationNotFoundException("notice not found: id=$id") + + override fun getFaqs(command: ReadFaqPageCommand): PageResult = + faqRepository.findPage(command).toResult { it.toSummaryResult() } + + override fun getFaq(id: Long): FaqDetailResult = + faqRepository.findById(id)?.toDetailResult() + ?: throw NotificationNotFoundException("faq not found: id=$id") + + override fun getRecruitmentGuideline(): RecruitmentGuidelineResult { + val guideline = recruitmentGuidelineRepository.findCurrent() + ?: throw NotificationNotFoundException("recruitment guideline not found") + return RecruitmentGuidelineResult( + recruitmentId = guideline.id, + title = guideline.title, + description = guideline.description, + schedule = RecruitmentScheduleResult( + applicationStart = guideline.schedule.applicationStart, + applicationEnd = guideline.schedule.applicationEnd, + resultAt = guideline.schedule.resultAt, + ), + createdAt = guideline.createdAt, + updatedAt = guideline.updatedAt, + ) + } + + private fun Notice.toSummaryResult(): NoticeSummaryResult = + NoticeSummaryResult( + noticeId = id, + title = title, + author = author, + createdAt = createdAt, + ) + + private fun Notice.toDetailResult(): NoticeDetailResult = + NoticeDetailResult( + noticeId = id, + title = title, + content = content, + author = author, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) + + private fun Faq.toSummaryResult(): FaqSummaryResult = + FaqSummaryResult( + faqId = id, + category = category, + question = question, + answer = answer, + ) + + private fun Faq.toDetailResult(): FaqDetailResult = + FaqDetailResult( + faqId = id, + category = category, + question = question, + answer = answer, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) + + private fun PageData.toResult( + mapper: (T) -> R, + ): PageResult { + return PageResult( + content = content.map(mapper), + page = page, + size = size, + totalElements = totalElements, + totalPages = totalPages, + last = last, + ) + } +} diff --git a/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt b/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt index d89e765d..676cf6ef 100644 --- a/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt +++ b/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/TestMain.kt @@ -1,11 +1,11 @@ package hs.kr.entrydsm.notification.application -import org.junit.Assert.assertTrue -import org.junit.Test +import hs.kr.entrydsm.notification.application.service.NotificationServiceTest +import org.junit.runner.RunWith +import org.junit.runners.Suite -class NotificationApplicationModuleTest { - @Test - fun moduleLoads() { - assertTrue(true) - } -} +@RunWith(Suite::class) +@Suite.SuiteClasses( + NotificationServiceTest::class, +) +class NotificationApplicationModuleTest diff --git a/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/notification/application/service/NotificationServiceTest.kt b/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/notification/application/service/NotificationServiceTest.kt new file mode 100644 index 00000000..4bd720d7 --- /dev/null +++ b/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/notification/application/service/NotificationServiceTest.kt @@ -0,0 +1,321 @@ +package hs.kr.entrydsm.notification.application.service + +import hs.kr.entrydsm.notification.application.exception.NotificationNotFoundException +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadFaqPageCommand +import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +import hs.kr.entrydsm.notification.application.port.out.FaqRepository +import hs.kr.entrydsm.notification.application.port.out.NoticeRepository +import hs.kr.entrydsm.notification.application.port.out.RecruitmentGuidelineRepository +import hs.kr.entrydsm.notification.application.port.out.data.PageData +import hs.kr.entrydsm.notification.domain.model.Faq +import hs.kr.entrydsm.notification.domain.model.FaqCategory +import hs.kr.entrydsm.notification.domain.model.Notice +import hs.kr.entrydsm.notification.domain.model.NoticeCategory +import hs.kr.entrydsm.notification.domain.model.RecruitmentGuideline +import hs.kr.entrydsm.notification.domain.model.RecruitmentSchedule +import java.time.LocalDate +import java.time.LocalDateTime +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class NotificationServiceTest { + @Test + fun getNoticesReturnsPagedNoticesOrderedByCreatedAtDesc() { + val service = service( + notices = listOf( + notice(id = 1L, title = "older", createdAt = now.minusDays(1)), + notice(id = 2L, title = "newer", createdAt = now), + ), + ) + + val result = service.getNotices(ReadNotificationPageCommand(page = 0, size = 1)) + + assertEquals(1, result.content.size) + assertEquals(2L, result.content.first().noticeId) + assertEquals(2L, result.totalElements) + assertEquals(2, result.totalPages) + assertFalse(result.last) + } + + @Test + fun getNoticesReturnsPagedNoticesFilteredByCategory() { + val service = service( + notices = listOf( + notice( + id = 1L, + title = "admission", + category = NoticeCategory.ADMISSION_NOTICE, + createdAt = now.minusDays(1), + ), + notice( + id = 2L, + title = "prospective", + category = NoticeCategory.PROSPECTIVE_STUDENT, + createdAt = now, + ), + ), + ) + + val result = service.getNotices( + ReadNotificationPageCommand( + page = 0, + size = 10, + category = NoticeCategory.ADMISSION_NOTICE, + ), + ) + + assertEquals(1, result.content.size) + assertEquals(1L, result.content.first().noticeId) + assertEquals("admission", result.content.first().title) + assertEquals(1L, result.totalElements) + assertTrue(result.last) + } + + @Test + fun getNoticeReturnsDetailFields() { + val service = service( + notices = listOf( + notice( + id = 1L, + title = "notice title", + content = "notice content", + author = "admin", + viewCount = 12, + createdAt = now.minusDays(2), + updatedAt = now.minusDays(1), + ), + ), + ) + + val result = service.getNotice(1L) + + assertEquals(1L, result.noticeId) + assertEquals("notice title", result.title) + assertEquals("notice content", result.content) + assertEquals("admin", result.author) + assertEquals(12, result.viewCount) + assertEquals(now.minusDays(2), result.createdAt) + assertEquals(now.minusDays(1), result.updatedAt) + } + + @Test(expected = NotificationNotFoundException::class) + fun getNoticeThrowsWhenNoticeDoesNotExist() { + service().getNotice(1L) + } + + @Test + fun getFaqsReturnsPagedFaqsOrderedById() { + val service = service( + faqs = listOf( + faq(id = 2L, question = "second"), + faq(id = 1L, question = "first"), + ), + ) + + val result = service.getFaqs(ReadFaqPageCommand(page = 0, size = 1)) + + assertEquals(1, result.content.size) + assertEquals(1L, result.content.first().faqId) + assertEquals("first", result.content.first().question) + assertEquals(2L, result.totalElements) + assertFalse(result.last) + } + + @Test + fun getFaqsReturnsPagedFaqsFilteredByCategory() { + val service = service( + faqs = listOf( + faq(id = 1L, category = FaqCategory.ADMISSION.label, question = "admission"), + faq(id = 2L, category = FaqCategory.CAREER.label, question = "career"), + ), + ) + + val result = service.getFaqs( + ReadFaqPageCommand( + page = 0, + size = 10, + category = FaqCategory.ADMISSION, + ), + ) + + assertEquals(1, result.content.size) + assertEquals(1L, result.content.first().faqId) + assertEquals("admission", result.content.first().question) + assertEquals(FaqCategory.ADMISSION.label, result.content.first().category) + assertEquals(1L, result.totalElements) + assertTrue(result.last) + } + + @Test(expected = NotificationNotFoundException::class) + fun getFaqThrowsWhenFaqDoesNotExist() { + service().getFaq(1L) + } + + @Test + fun getRecruitmentGuidelineReturnsCurrentGuideline() { + val guideline = RecruitmentGuideline( + id = 1L, + title = "2027 admission", + description = "guideline", + schedule = RecruitmentSchedule( + applicationStart = LocalDate.parse("2026-10-19"), + applicationEnd = LocalDate.parse("2026-10-23"), + resultAt = LocalDate.parse("2026-10-30"), + ), + createdAt = now.minusDays(3), + updatedAt = now.minusDays(2), + ) + + val result = service(guideline = guideline).getRecruitmentGuideline() + + assertEquals(1L, result.recruitmentId) + assertEquals("2027 admission", result.title) + assertEquals("guideline", result.description) + assertEquals(LocalDate.parse("2026-10-19"), result.schedule.applicationStart) + assertEquals(LocalDate.parse("2026-10-23"), result.schedule.applicationEnd) + assertEquals(LocalDate.parse("2026-10-30"), result.schedule.resultAt) + assertEquals(now.minusDays(3), result.createdAt) + assertEquals(now.minusDays(2), result.updatedAt) + } + + @Test + fun getNoticesHandlesLargePageWithoutIntOverflow() { + val result = service(notices = listOf(notice(id = 1L))) + .getNotices(ReadNotificationPageCommand(page = Int.MAX_VALUE, size = Int.MAX_VALUE)) + + assertTrue(result.content.isEmpty()) + assertEquals(1L, result.totalElements) + assertEquals(1, result.totalPages) + assertTrue(result.last) + } + + @Test(expected = IllegalArgumentException::class) + fun commandRejectsNegativePage() { + ReadNotificationPageCommand(page = -1, size = 10) + } + + @Test(expected = IllegalArgumentException::class) + fun commandRejectsZeroSize() { + ReadNotificationPageCommand(page = 0, size = 0) + } + + private fun service( + notices: List = emptyList(), + faqs: List = emptyList(), + guideline: RecruitmentGuideline? = null, + ): NotificationService = + NotificationService( + noticeRepository = FakeNoticeRepository(notices), + faqRepository = FakeFaqRepository(faqs), + recruitmentGuidelineRepository = FakeRecruitmentGuidelineRepository(guideline), + ) + + private inner class FakeNoticeRepository( + private val notices: List = emptyList(), + ) : NoticeRepository { + override fun findPage(command: ReadNotificationPageCommand): PageData { + val sorted = notices + .filter { command.category == null || it.category == command.category } + .sortedWith(compareByDescending { it.createdAt }.thenByDescending { it.id }) + return sorted.toPageData(command) + } + + override fun findById(id: Long): Notice? = notices.firstOrNull { it.id == id } + } + + private inner class FakeFaqRepository( + private val faqs: List = emptyList(), + ) : FaqRepository { + override fun findPage(command: ReadFaqPageCommand): PageData { + val category = command.category + return faqs + .filter { category == null || it.category == category.label } + .sortedBy { it.id } + .toPageData(command) + } + + override fun findById(id: Long): Faq? = faqs.firstOrNull { it.id == id } + } + + private class FakeRecruitmentGuidelineRepository( + private val guideline: RecruitmentGuideline? = null, + ) : RecruitmentGuidelineRepository { + override fun findCurrent(): RecruitmentGuideline? = guideline + } + + private fun notice( + id: Long, + title: String = "title", + content: String = "content", + category: NoticeCategory = NoticeCategory.ADMISSION_NOTICE, + author: String = "admin", + viewCount: Int = 0, + createdAt: LocalDateTime = now, + updatedAt: LocalDateTime = now, + ): Notice = + Notice( + id = id, + title = title, + content = content, + category = category, + author = author, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) + + private fun faq( + id: Long, + category: String = "입학 문의", + question: String = "question", + answer: String = "answer", + viewCount: Int = 0, + createdAt: LocalDateTime = now, + updatedAt: LocalDateTime = now, + ): Faq = + Faq( + id = id, + category = category, + question = question, + answer = answer, + viewCount = viewCount, + createdAt = createdAt, + updatedAt = updatedAt, + ) + + private fun List.toPageData(command: ReadNotificationPageCommand): PageData { + val fromIndex = command.offset() + .coerceAtMost(size.toLong()) + .toInt() + val toIndex = (fromIndex.toLong() + command.size.toLong()) + .coerceAtMost(size.toLong()) + .toInt() + return PageData( + content = subList(fromIndex, toIndex), + page = command.page, + size = command.size, + totalElements = size.toLong(), + ) + } + + private fun List.toPageData(command: ReadFaqPageCommand): PageData { + val fromIndex = command.offset() + .coerceAtMost(size.toLong()) + .toInt() + val toIndex = (fromIndex.toLong() + command.size.toLong()) + .coerceAtMost(size.toLong()) + .toInt() + return PageData( + content = subList(fromIndex, toIndex), + page = command.page, + size = command.size, + totalElements = size.toLong(), + ) + } + + private companion object { + val now: LocalDateTime = LocalDateTime.parse("2026-08-17T09:00:00") + } +} diff --git a/systems/notification/notification-bootstrap/BUILD.bazel b/systems/notification/notification-bootstrap/BUILD.bazel index 11cdcaa9..ec4ff26c 100644 --- a/systems/notification/notification-bootstrap/BUILD.bazel +++ b/systems/notification/notification-bootstrap/BUILD.bazel @@ -8,7 +8,7 @@ kt_jvm_binary( srcs = glob(["src/main/kotlin/**/*.kt"]), javac_opts = "//:javac_options", kotlinc_opts = "//:kotlinc_options", - main_class = "hs.kr.entrydsm.notification.NotificationBootstrapApplicationKt", + main_class = "hs.kr.entrydsm.notification.ExampleApplicationKt", plugins = ["//:spring_allopen"], resources = glob(["src/main/resources/**"]), deps = MODULE_DEPS + [ diff --git a/systems/notification/notification-bootstrap/deps.bzl b/systems/notification/notification-bootstrap/deps.bzl index bfdb5aef..4d5535f1 100644 --- a/systems/notification/notification-bootstrap/deps.bzl +++ b/systems/notification/notification-bootstrap/deps.bzl @@ -1,11 +1,13 @@ SPRING_DEPS = [ "@maven//:org_springframework_boot_spring_boot_starter_web", "@maven//:org_springframework_boot_spring_boot_starter_actuator", + "@maven//:org_springframework_boot_spring_boot_starter_data_jpa", ] KOTLIN_DEPS = [ "@maven//:org_jetbrains_kotlin_kotlin_reflect", "@maven//:com_fasterxml_jackson_module_jackson_module_kotlin", + "@maven//:com_mysql_mysql_connector_j", ] TEST_DEPS = [ diff --git a/systems/notification/notification-bootstrap/src/main/resources/application-dev.yaml b/systems/notification/notification-bootstrap/src/main/resources/application-dev.yaml new file mode 100644 index 00000000..e33c703a --- /dev/null +++ b/systems/notification/notification-bootstrap/src/main/resources/application-dev.yaml @@ -0,0 +1,17 @@ +spring: + config: + activate: + on-profile: dev + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + open-in-view: false + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect diff --git a/systems/notification/notification-bootstrap/src/main/resources/application-local.yaml b/systems/notification/notification-bootstrap/src/main/resources/application-local.yaml new file mode 100644 index 00000000..bfd56a7a --- /dev/null +++ b/systems/notification/notification-bootstrap/src/main/resources/application-local.yaml @@ -0,0 +1,17 @@ +spring: + config: + activate: + on-profile: local + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + open-in-view: false + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect diff --git a/systems/notification/notification-bootstrap/src/main/resources/application-prod.yaml b/systems/notification/notification-bootstrap/src/main/resources/application-prod.yaml new file mode 100644 index 00000000..f75439f9 --- /dev/null +++ b/systems/notification/notification-bootstrap/src/main/resources/application-prod.yaml @@ -0,0 +1,17 @@ +spring: + config: + activate: + on-profile: prod + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate + open-in-view: false + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect diff --git a/systems/notification/notification-bootstrap/src/main/resources/application.yaml b/systems/notification/notification-bootstrap/src/main/resources/application.yaml index 7d637fcb..1989c59c 100644 --- a/systems/notification/notification-bootstrap/src/main/resources/application.yaml +++ b/systems/notification/notification-bootstrap/src/main/resources/application.yaml @@ -5,8 +5,13 @@ spring: default: local main: lazy-initialization: true + server: shutdown: graceful + +grpc: + port: ${GRPC_PORT} + management: endpoints: web: @@ -16,6 +21,7 @@ management: health: probes: enabled: true + logging: level: root: INFO diff --git a/systems/notification/notification-bootstrap/src/main/resources/db/migration/V028__create_notification_tables.sql b/systems/notification/notification-bootstrap/src/main/resources/db/migration/V028__create_notification_tables.sql new file mode 100644 index 00000000..d4fb2e70 --- /dev/null +++ b/systems/notification/notification-bootstrap/src/main/resources/db/migration/V028__create_notification_tables.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS notices ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + category VARCHAR(32) NOT NULL, + author VARCHAR(50) NOT NULL, + view_count INT NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS faqs ( + id BIGINT NOT NULL AUTO_INCREMENT, + category VARCHAR(50) NOT NULL, + question VARCHAR(255) NOT NULL, + answer TEXT NOT NULL, + view_count INT NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS recruitment_guidelines ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + application_start DATE NOT NULL, + application_end DATE NOT NULL, + result_at DATE NOT NULL, + created_at DATETIME(6) NOT NULL, + updated_at DATETIME(6) NOT NULL, + PRIMARY KEY (id) +); diff --git a/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Faq.kt b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Faq.kt new file mode 100644 index 00000000..e8f82ee3 --- /dev/null +++ b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Faq.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.notification.domain.model + +import java.time.LocalDateTime + +data class Faq( + val id: Long, + val category: String, + val question: String, + val answer: String, + val viewCount: Int, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + diff --git a/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/FaqCategory.kt b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/FaqCategory.kt new file mode 100644 index 00000000..2587edfd --- /dev/null +++ b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/FaqCategory.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.notification.domain.model + +import java.util.Locale + +enum class FaqCategory( + val label: String, +) { + ADMISSION("입학 문의"), + CAREER("진로"), + SCHOOL_LIFE("학교 생활"), + DORMITORY("기숙사"), + ETC("기타"), + ; + + companion object { + fun from(value: String): FaqCategory = + entries.firstOrNull { category -> + category.name == value.uppercase(Locale.ROOT) || category.label == value + } ?: throw IllegalArgumentException("invalid faq category: $value") + } +} diff --git a/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Notice.kt b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Notice.kt new file mode 100644 index 00000000..82c1f522 --- /dev/null +++ b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Notice.kt @@ -0,0 +1,15 @@ +package hs.kr.entrydsm.notification.domain.model + +import java.time.LocalDateTime + +data class Notice( + val id: Long, + val title: String, + val content: String, + val category: NoticeCategory, + val author: String, + val viewCount: Int, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + diff --git a/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/NoticeCategory.kt b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/NoticeCategory.kt new file mode 100644 index 00000000..b05837b9 --- /dev/null +++ b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/NoticeCategory.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.notification.domain.model + +import java.util.Locale + +enum class NoticeCategory( + val label: String, +) { + ADMISSION_NOTICE("입학 공지사항"), + PROSPECTIVE_STUDENT("예비 신입생 안내"), + ; + + companion object { + fun from(value: String): NoticeCategory = + entries.firstOrNull { category -> + category.name == value.uppercase(Locale.ROOT) || category.label == value + } ?: throw IllegalArgumentException("invalid notice category: $value") + } +} diff --git a/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/RecruitmentGuideline.kt b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/RecruitmentGuideline.kt new file mode 100644 index 00000000..795e449c --- /dev/null +++ b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/RecruitmentGuideline.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.notification.domain.model + +import java.time.LocalDate +import java.time.LocalDateTime + +data class RecruitmentGuideline( + val id: Long, + val title: String, + val description: String, + val schedule: RecruitmentSchedule, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime, +) + +data class RecruitmentSchedule( + val applicationStart: LocalDate, + val applicationEnd: LocalDate, + val resultAt: LocalDate, +)