From 47623a711ca5c84e84b40637f73f9e9a10d6f616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Thu, 30 Jul 2026 19:54:47 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat(notification):=20notification=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC=EC=84=B1=20?= =?UTF-8?q?#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification-application/deps.bzl | 5 +- .../NotificationNotFoundException.kt | 6 + .../application/port/in/NotificationPort.kt | 18 +++ .../in/command/ReadNotificationPageCommand.kt | 7 ++ .../application/port/in/result/FaqResults.kt | 21 ++++ .../port/in/result/NoticeResults.kt | 21 ++++ .../application/port/in/result/PageResult.kt | 11 ++ .../in/result/RecruitmentGuidelineResult.kt | 19 +++ .../application/port/out/FaqRepository.kt | 9 ++ .../application/port/out/NoticeRepository.kt | 9 ++ .../out/RecruitmentGuidelineRepository.kt | 8 ++ .../service/NotificationService.kt | 118 ++++++++++++++++++ .../entrydsm/notification/domain/model/Faq.kt | 14 +++ .../notification/domain/model/Notice.kt | 14 +++ .../domain/model/RecruitmentGuideline.kt | 19 +++ 15 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/exception/NotificationNotFoundException.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/NotificationPort.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadNotificationPageCommand.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/FaqResults.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/NoticeResults.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/PageResult.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/result/RecruitmentGuidelineResult.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/FaqRepository.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/NoticeRepository.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/RecruitmentGuidelineRepository.kt create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/service/NotificationService.kt create mode 100644 systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Faq.kt create mode 100644 systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Notice.kt create mode 100644 systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/RecruitmentGuideline.kt diff --git a/systems/notification/notification-application/deps.bzl b/systems/notification/notification-application/deps.bzl index 5cf03643..e7ec7138 100644 --- a/systems/notification/notification-application/deps.bzl +++ b/systems/notification/notification-application/deps.bzl @@ -1,4 +1,7 @@ -KOTLIN_DEPS = [] +KOTLIN_DEPS = [ + "@maven//:org_springframework_boot_spring_boot_starter", + "//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..b161d4ba --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/NotificationPort.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.notification.application.port.`in` + +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: ReadNotificationPageCommand): 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/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..881e0b05 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadNotificationPageCommand.kt @@ -0,0 +1,7 @@ +package hs.kr.entrydsm.notification.application.port.`in`.command + +data class ReadNotificationPageCommand( + val page: Int = 0, + val size: Int = 10, +) + 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..3ee8f084 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/FaqRepository.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.notification.application.port.out + +import hs.kr.entrydsm.notification.domain.model.Faq + +interface FaqRepository { + fun findAll(): List + 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..47fcf20a --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/NoticeRepository.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.notification.application.port.out + +import hs.kr.entrydsm.notification.domain.model.Notice + +interface NoticeRepository { + fun findAll(): List + 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/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..56a7e529 --- /dev/null +++ b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/service/NotificationService.kt @@ -0,0 +1,118 @@ +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.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.domain.model.Faq +import hs.kr.entrydsm.notification.domain.model.Notice +import org.springframework.stereotype.Service + +@Service +class NotificationService( + private val noticeRepository: NoticeRepository, + private val faqRepository: FaqRepository, + private val recruitmentGuidelineRepository: RecruitmentGuidelineRepository, +) : NotificationPort { + override fun getNotices(command: ReadNotificationPageCommand): PageResult = + noticeRepository.findAll() + .sortedByDescending { it.createdAt } + .toPage(command) { it.toSummaryResult() } + + override fun getNotice(id: Long): NoticeDetailResult = + noticeRepository.findById(id)?.toDetailResult() + ?: throw NotificationNotFoundException("notice not found: id=$id") + + override fun getFaqs(command: ReadNotificationPageCommand): PageResult = + faqRepository.findAll() + .sortedBy { it.id } + .toPage(command) { 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 List.toPage( + command: ReadNotificationPageCommand, + mapper: (T) -> R, + ): PageResult { + require(command.page >= 0) { "page must be greater than or equal to 0" } + require(command.size > 0) { "size must be greater than 0" } + + val fromIndex = (command.page * command.size).coerceAtMost(size) + val toIndex = (fromIndex + command.size).coerceAtMost(size) + val totalPages = if (isEmpty()) 0 else ((size - 1) / command.size) + 1 + return PageResult( + content = subList(fromIndex, toIndex).map(mapper), + page = command.page, + size = command.size, + totalElements = size.toLong(), + totalPages = totalPages, + last = command.page >= totalPages - 1, + ) + } +} 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/Notice.kt b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Notice.kt new file mode 100644 index 00000000..9b5dcad7 --- /dev/null +++ b/systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/Notice.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.notification.domain.model + +import java.time.LocalDateTime + +data class Notice( + val id: 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-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, +) From bd445dcc42aeffd8db14f220451ed665fce4d992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Mon, 17 Aug 2026 17:21:20 +0900 Subject: [PATCH 02/11] =?UTF-8?q?fix(notification):=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B2=BD=EA=B3=84=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EA=B2=80=EC=A6=9D=20=EB=B3=B4?= =?UTF-8?q?=EC=99=84=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification-application/BUILD.bazel | 2 +- .../notification-application/deps.bzl | 1 - .../in/command/ReadNotificationPageCommand.kt | 8 +- .../service/NotificationService.kt | 13 +- .../test/kotlin/hs/kr/entrydsm/TestMain.kt | 16 +- .../service/NotificationServiceTest.kt | 213 ++++++++++++++++++ 6 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/notification/application/service/NotificationServiceTest.kt 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 e7ec7138..1bf96ca7 100644 --- a/systems/notification/notification-application/deps.bzl +++ b/systems/notification/notification-application/deps.bzl @@ -1,5 +1,4 @@ KOTLIN_DEPS = [ - "@maven//:org_springframework_boot_spring_boot_starter", "//systems/notification/notification-domain:main", ] 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 index 881e0b05..0c886d97 100644 --- 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 @@ -3,5 +3,11 @@ package hs.kr.entrydsm.notification.application.port.`in`.command data class ReadNotificationPageCommand( val page: Int = 0, val size: Int = 10, -) +) { + 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() +} 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 index 56a7e529..021d4f84 100644 --- 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 @@ -15,9 +15,7 @@ import hs.kr.entrydsm.notification.application.port.out.NoticeRepository import hs.kr.entrydsm.notification.application.port.out.RecruitmentGuidelineRepository import hs.kr.entrydsm.notification.domain.model.Faq import hs.kr.entrydsm.notification.domain.model.Notice -import org.springframework.stereotype.Service -@Service class NotificationService( private val noticeRepository: NoticeRepository, private val faqRepository: FaqRepository, @@ -100,11 +98,12 @@ class NotificationService( command: ReadNotificationPageCommand, mapper: (T) -> R, ): PageResult { - require(command.page >= 0) { "page must be greater than or equal to 0" } - require(command.size > 0) { "size must be greater than 0" } - - val fromIndex = (command.page * command.size).coerceAtMost(size) - val toIndex = (fromIndex + command.size).coerceAtMost(size) + val fromIndex = command.offset() + .coerceAtMost(size.toLong()) + .toInt() + val toIndex = (fromIndex.toLong() + command.size.toLong()) + .coerceAtMost(size.toLong()) + .toInt() val totalPages = if (isEmpty()) 0 else ((size - 1) / command.size) + 1 return PageResult( content = subList(fromIndex, toIndex).map(mapper), 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..5d7741d9 --- /dev/null +++ b/systems/notification/notification-application/src/test/kotlin/hs/kr/entrydsm/notification/application/service/NotificationServiceTest.kt @@ -0,0 +1,213 @@ +package hs.kr.entrydsm.notification.application.service + +import hs.kr.entrydsm.notification.application.exception.NotificationNotFoundException +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.domain.model.Faq +import hs.kr.entrydsm.notification.domain.model.Notice +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 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(ReadNotificationPageCommand(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(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 class FakeNoticeRepository( + private val notices: List = emptyList(), + ) : NoticeRepository { + override fun findAll(): List = notices + override fun findById(id: Long): Notice? = notices.firstOrNull { it.id == id } + } + + private class FakeFaqRepository( + private val faqs: List = emptyList(), + ) : FaqRepository { + override fun findAll(): List = faqs + 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", + author: String = "admin", + viewCount: Int = 0, + createdAt: LocalDateTime = now, + updatedAt: LocalDateTime = now, + ): Notice = + Notice( + id = id, + title = title, + content = content, + 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 companion object { + val now: LocalDateTime = LocalDateTime.parse("2026-08-17T09:00:00") + } +} From 94bbf28454640ab3d98cc150256a6b6c7f6db600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Thu, 30 Jul 2026 19:56:11 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat(notification):=20notification=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=EC=84=B1=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification-adapter-in/deps.bzl | 6 +- .../adapterin/web/NotificationController.kt | 84 +++++++++++++++++++ .../adapterin/web/dto/common/ApiResponse.kt | 8 ++ .../adapterin/web/dto/common/ErrorResponse.kt | 7 ++ .../web/dto/common/ResponseMapper.kt | 77 +++++++++++++++++ .../web/dto/response/FaqResponses.kt | 21 +++++ .../web/dto/response/NoticeResponses.kt | 21 +++++ .../web/dto/response/PageResponse.kt | 11 +++ .../response/RecruitmentGuidelineResponse.kt | 19 +++++ .../web/exception/GlobalExceptionHandler.kt | 72 ++++++++++++++++ .../notification-adapter-out/deps.bzl | 7 +- .../adapterout/entity/FaqJpaEntity.kt | 49 +++++++++++ .../adapterout/entity/NoticeJpaEntity.kt | 49 +++++++++++ .../entity/RecruitmentGuidelineJpaEntity.kt | 68 +++++++++++++++ .../adapterout/repository/FaqJpaRepository.kt | 9 ++ .../repository/FaqPersistenceAdapter.kt | 19 +++++ .../repository/NoticeJpaRepository.kt | 9 ++ .../repository/NoticePersistenceAdapter.kt | 19 +++++ .../RecruitmentGuidelineJpaRepository.kt | 9 ++ .../RecruitmentGuidelinePersistenceAdapter.kt | 16 ++++ 20 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/NotificationController.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ApiResponse.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ErrorResponse.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/common/ResponseMapper.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/FaqResponses.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/NoticeResponses.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/PageResponse.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/dto/response/RecruitmentGuidelineResponse.kt create mode 100644 systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/exception/GlobalExceptionHandler.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/FaqJpaEntity.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/NoticeJpaEntity.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/RecruitmentGuidelineJpaEntity.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqJpaRepository.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqPersistenceAdapter.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticeJpaRepository.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticePersistenceAdapter.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelineJpaRepository.kt create mode 100644 systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/RecruitmentGuidelinePersistenceAdapter.kt diff --git a/systems/notification/notification-adapter-in/deps.bzl b/systems/notification/notification-adapter-in/deps.bzl index 5cf03643..3c4c75ce 100644 --- a/systems/notification/notification-adapter-in/deps.bzl +++ b/systems/notification/notification-adapter-in/deps.bzl @@ -1,4 +1,8 @@ -KOTLIN_DEPS = [] +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..c11ea82f --- /dev/null +++ b/systems/notification/notification-adapter-in/src/main/kotlin/hs/kr/entrydsm/notification/adapterin/web/NotificationController.kt @@ -0,0 +1,84 @@ +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.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, + ): ApiResponse> { + val result = notificationPort.getNotices(ReadNotificationPageCommand(page = page, size = size)) + 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, + ): ApiResponse> { + val result = notificationPort.getFaqs(ReadNotificationPageCommand(page = page, size = size)) + 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..6c15eee3 --- /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 = exception.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 = exception.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-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..c48382d6 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/entity/NoticeJpaEntity.kt @@ -0,0 +1,49 @@ +package hs.kr.entrydsm.notification.adapterout.entity + +import hs.kr.entrydsm.notification.domain.model.Notice +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 = "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 = "", + + @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, + 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..e698a457 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqJpaRepository.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.adapterout.entity.FaqJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface FaqJpaRepository : JpaRepository { + fun findAllByOrderByIdAsc(): List +} + 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..d065f9e1 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/FaqPersistenceAdapter.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.application.port.out.FaqRepository +import hs.kr.entrydsm.notification.domain.model.Faq +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +@Transactional(readOnly = true) +class FaqPersistenceAdapter( + private val faqJpaRepository: FaqJpaRepository, +) : FaqRepository { + override fun findAll(): List = + faqJpaRepository.findAllByOrderByIdAsc().map { it.toDomain() } + + override fun findById(id: Long): Faq? = + faqJpaRepository.findById(id).orElse(null)?.toDomain() +} + 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..69211122 --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticeJpaRepository.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.adapterout.entity.NoticeJpaEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface NoticeJpaRepository : JpaRepository { + fun findAllByOrderByCreatedAtDesc(): List +} + 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..7bd11bfd --- /dev/null +++ b/systems/notification/notification-adapter-out/src/main/kotlin/hs/kr/entrydsm/notification/adapterout/repository/NoticePersistenceAdapter.kt @@ -0,0 +1,19 @@ +package hs.kr.entrydsm.notification.adapterout.repository + +import hs.kr.entrydsm.notification.application.port.out.NoticeRepository +import hs.kr.entrydsm.notification.domain.model.Notice +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Repository +@Transactional(readOnly = true) +class NoticePersistenceAdapter( + private val noticeJpaRepository: NoticeJpaRepository, +) : NoticeRepository { + override fun findAll(): List = + noticeJpaRepository.findAllByOrderByCreatedAtDesc().map { it.toDomain() } + + override fun findById(id: Long): Notice? = + noticeJpaRepository.findById(id).orElse(null)?.toDomain() +} + 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() +} + From f351b11ed17e2741c9d57d15da3e7cc1c3d61388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Mon, 17 Aug 2026 17:40:01 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix(notification):=20=EA=B3=B5=EC=A7=80?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=B4=EC=99=84=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification-adapter-out/BUILD.bazel | 2 +- .../adapterout/repository/FaqJpaRepository.kt | 4 ++- .../repository/FaqPersistenceAdapter.kt | 17 +++++++++-- .../repository/NoticeJpaRepository.kt | 4 ++- .../repository/NoticePersistenceAdapter.kt | 17 +++++++++-- .../application/port/out/FaqRepository.kt | 4 ++- .../application/port/out/NoticeRepository.kt | 4 ++- .../application/port/out/data/PageData.kt | 17 +++++++++++ .../service/NotificationService.kt | 29 ++++++------------ .../service/NotificationServiceTest.kt | 30 ++++++++++++++++--- 10 files changed, 95 insertions(+), 33 deletions(-) create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/data/PageData.kt 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/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 index e698a457..489874b0 100644 --- 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 @@ -1,9 +1,11 @@ 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(): List + fun findAllByOrderByIdAsc(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 index d065f9e1..cf6237bf 100644 --- 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 @@ -1,7 +1,10 @@ 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.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 @@ -10,10 +13,20 @@ import org.springframework.transaction.annotation.Transactional class FaqPersistenceAdapter( private val faqJpaRepository: FaqJpaRepository, ) : FaqRepository { - override fun findAll(): List = - faqJpaRepository.findAllByOrderByIdAsc().map { it.toDomain() } + override fun findPage(command: ReadNotificationPageCommand): PageData { + val page = 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 ReadNotificationPageCommand.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 index 69211122..cacad39f 100644 --- 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 @@ -1,9 +1,11 @@ package hs.kr.entrydsm.notification.adapterout.repository import hs.kr.entrydsm.notification.adapterout.entity.NoticeJpaEntity +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository interface NoticeJpaRepository : JpaRepository { - fun findAllByOrderByCreatedAtDesc(): List + fun findAllByOrderByCreatedAtDescIdDesc(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 index 7bd11bfd..ccee5652 100644 --- 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 @@ -1,7 +1,10 @@ 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 @@ -10,10 +13,20 @@ import org.springframework.transaction.annotation.Transactional class NoticePersistenceAdapter( private val noticeJpaRepository: NoticeJpaRepository, ) : NoticeRepository { - override fun findAll(): List = - noticeJpaRepository.findAllByOrderByCreatedAtDesc().map { it.toDomain() } + override fun findPage(command: ReadNotificationPageCommand): PageData { + val page = 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-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 index 3ee8f084..15a3d09c 100644 --- 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 @@ -1,9 +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.Faq interface FaqRepository { - fun findAll(): List + fun findPage(command: ReadNotificationPageCommand): 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 index 47fcf20a..2b6880ad 100644 --- 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 @@ -1,9 +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 findAll(): List + 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/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 index 021d4f84..10a78937 100644 --- 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 @@ -13,6 +13,7 @@ import hs.kr.entrydsm.notification.application.port.`in`.result.RecruitmentSched 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 @@ -22,18 +23,14 @@ class NotificationService( private val recruitmentGuidelineRepository: RecruitmentGuidelineRepository, ) : NotificationPort { override fun getNotices(command: ReadNotificationPageCommand): PageResult = - noticeRepository.findAll() - .sortedByDescending { it.createdAt } - .toPage(command) { it.toSummaryResult() } + 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: ReadNotificationPageCommand): PageResult = - faqRepository.findAll() - .sortedBy { it.id } - .toPage(command) { it.toSummaryResult() } + faqRepository.findPage(command).toResult { it.toSummaryResult() } override fun getFaq(id: Long): FaqDetailResult = faqRepository.findById(id)?.toDetailResult() @@ -94,24 +91,16 @@ class NotificationService( updatedAt = updatedAt, ) - private fun List.toPage( - command: ReadNotificationPageCommand, + private fun PageData.toResult( mapper: (T) -> R, ): PageResult { - val fromIndex = command.offset() - .coerceAtMost(size.toLong()) - .toInt() - val toIndex = (fromIndex.toLong() + command.size.toLong()) - .coerceAtMost(size.toLong()) - .toInt() - val totalPages = if (isEmpty()) 0 else ((size - 1) / command.size) + 1 return PageResult( - content = subList(fromIndex, toIndex).map(mapper), - page = command.page, - size = command.size, - totalElements = size.toLong(), + content = content.map(mapper), + page = page, + size = size, + totalElements = totalElements, totalPages = totalPages, - last = command.page >= totalPages - 1, + last = last, ) } } 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 index 5d7741d9..98b68dad 100644 --- 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 @@ -5,6 +5,7 @@ import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificatio 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 import hs.kr.entrydsm.notification.domain.model.RecruitmentGuideline @@ -149,17 +150,23 @@ class NotificationServiceTest { recruitmentGuidelineRepository = FakeRecruitmentGuidelineRepository(guideline), ) - private class FakeNoticeRepository( + private inner class FakeNoticeRepository( private val notices: List = emptyList(), ) : NoticeRepository { - override fun findAll(): List = notices + override fun findPage(command: ReadNotificationPageCommand): PageData { + val sorted = notices.sortedWith(compareByDescending { it.createdAt }.thenByDescending { it.id }) + return sorted.toPageData(command) + } + override fun findById(id: Long): Notice? = notices.firstOrNull { it.id == id } } - private class FakeFaqRepository( + private inner class FakeFaqRepository( private val faqs: List = emptyList(), ) : FaqRepository { - override fun findAll(): List = faqs + override fun findPage(command: ReadNotificationPageCommand): PageData = + faqs.sortedBy { it.id }.toPageData(command) + override fun findById(id: Long): Faq? = faqs.firstOrNull { it.id == id } } @@ -207,6 +214,21 @@ class NotificationServiceTest { 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 companion object { val now: LocalDateTime = LocalDateTime.parse("2026-08-17T09:00:00") } From 11b97b93c37bce6a3aacc256f804d1efaac7ddef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Wed, 19 Aug 2026 23:32:20 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix(notification):=20=EA=B3=B5=EC=A7=80?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EB=B3=B4=EC=99=84=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapterin/web/NotificationController.kt | 9 +++- .../adapterout/entity/NoticeJpaEntity.kt | 8 ++++ .../repository/NoticeJpaRepository.kt | 5 +++ .../repository/NoticePersistenceAdapter.kt | 7 +++- .../in/command/ReadNotificationPageCommand.kt | 16 ++++++++ .../service/NotificationServiceTest.kt | 41 ++++++++++++++++++- .../notification/domain/model/Notice.kt | 1 + .../domain/model/NoticeCategory.kt | 18 ++++++++ 8 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/NoticeCategory.kt 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 index c11ea82f..11f88fdb 100644 --- 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 @@ -25,8 +25,15 @@ class NotificationController( fun getNotices( @RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "10") size: Int, + @RequestParam(required = false) category: String?, ): ApiResponse> { - val result = notificationPort.getNotices(ReadNotificationPageCommand(page = page, size = size)) + val result = notificationPort.getNotices( + ReadNotificationPageCommand.of( + page = page, + size = size, + category = category, + ), + ) return ApiResponse( status = 200, message = "공지 목록 조회 성공", 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 index c48382d6..8d0ab485 100644 --- 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 @@ -1,8 +1,11 @@ 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 @@ -23,6 +26,10 @@ open class NoticeJpaEntity( @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 = "", @@ -40,6 +47,7 @@ open class NoticeJpaEntity( id = requireNotNull(id), title = title, content = content, + category = category, author = author, viewCount = viewCount, createdAt = createdAt, 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 index cacad39f..4db1ae7f 100644 --- 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 @@ -1,11 +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 index ccee5652..e92a589b 100644 --- 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 @@ -14,7 +14,12 @@ class NoticePersistenceAdapter( private val noticeJpaRepository: NoticeJpaRepository, ) : NoticeRepository { override fun findPage(command: ReadNotificationPageCommand): PageData { - val page = noticeJpaRepository.findAllByOrderByCreatedAtDescIdDesc(command.toPageRequest()) + 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, 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 index 0c886d97..5556b356 100644 --- 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 @@ -1,8 +1,11 @@ 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" } @@ -10,4 +13,17 @@ data class ReadNotificationPageCommand( } 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/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 index 98b68dad..3db46989 100644 --- 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 @@ -8,6 +8,7 @@ import hs.kr.entrydsm.notification.application.port.out.RecruitmentGuidelineRepo 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 +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 @@ -36,6 +37,40 @@ class NotificationServiceTest { 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( @@ -154,7 +189,9 @@ class NotificationServiceTest { private val notices: List = emptyList(), ) : NoticeRepository { override fun findPage(command: ReadNotificationPageCommand): PageData { - val sorted = notices.sortedWith(compareByDescending { it.createdAt }.thenByDescending { it.id }) + val sorted = notices + .filter { command.category == null || it.category == command.category } + .sortedWith(compareByDescending { it.createdAt }.thenByDescending { it.id }) return sorted.toPageData(command) } @@ -180,6 +217,7 @@ class NotificationServiceTest { id: Long, title: String = "title", content: String = "content", + category: NoticeCategory = NoticeCategory.ADMISSION_NOTICE, author: String = "admin", viewCount: Int = 0, createdAt: LocalDateTime = now, @@ -189,6 +227,7 @@ class NotificationServiceTest { id = id, title = title, content = content, + category = category, author = author, viewCount = viewCount, createdAt = createdAt, 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 index 9b5dcad7..82c1f522 100644 --- 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 @@ -6,6 +6,7 @@ data class Notice( val id: Long, val title: String, val content: String, + val category: NoticeCategory, val author: String, val viewCount: Int, val createdAt: 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") + } +} From f970f61f55501bb3e1f129d1c53ed5a07cd946fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Tue, 25 Aug 2026 00:53:04 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix(notification):=20QnA=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=B3=B4=EC=99=84=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapterin/web/NotificationController.kt | 10 +++- .../adapterout/repository/FaqJpaRepository.kt | 1 + .../repository/FaqPersistenceAdapter.kt | 10 ++-- .../application/port/in/NotificationPort.kt | 3 +- .../port/in/command/ReadFaqPageCommand.kt | 29 ++++++++++ .../application/port/out/FaqRepository.kt | 4 +- .../service/NotificationService.kt | 3 +- .../service/NotificationServiceTest.kt | 53 +++++++++++++++++-- .../notification/domain/model/FaqCategory.kt | 21 ++++++++ 9 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/in/command/ReadFaqPageCommand.kt create mode 100644 systems/notification/notification-domain/src/main/kotlin/hs/kr/entrydsm/notification/domain/model/FaqCategory.kt 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 index 11f88fdb..92adcd91 100644 --- 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 @@ -9,6 +9,7 @@ import hs.kr.entrydsm.notification.adapterin.web.dto.response.NoticeSummaryRespo 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 @@ -57,8 +58,15 @@ class NotificationController( fun getFaqs( @RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "10") size: Int, + @RequestParam(required = false) category: String?, ): ApiResponse> { - val result = notificationPort.getFaqs(ReadNotificationPageCommand(page = page, size = size)) + val result = notificationPort.getFaqs( + ReadFaqPageCommand.of( + page = page, + size = size, + category = category, + ), + ) return ApiResponse( status = 200, message = "자주 묻는 질문 목록 조회 성공", 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 index 489874b0..a34230c8 100644 --- 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 @@ -7,5 +7,6 @@ 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 index cf6237bf..7d0c0b3a 100644 --- 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 @@ -1,6 +1,6 @@ package hs.kr.entrydsm.notification.adapterout.repository -import hs.kr.entrydsm.notification.application.port.`in`.command.ReadNotificationPageCommand +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 @@ -13,8 +13,10 @@ import org.springframework.transaction.annotation.Transactional class FaqPersistenceAdapter( private val faqJpaRepository: FaqJpaRepository, ) : FaqRepository { - override fun findPage(command: ReadNotificationPageCommand): PageData { - val page = faqJpaRepository.findAllByOrderByIdAsc(command.toPageRequest()) + 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, @@ -26,7 +28,7 @@ class FaqPersistenceAdapter( override fun findById(id: Long): Faq? = faqJpaRepository.findById(id).orElse(null)?.toDomain() - private fun ReadNotificationPageCommand.toPageRequest(): PageRequest = + private fun ReadFaqPageCommand.toPageRequest(): PageRequest = PageRequest.of(page, size) } 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 index b161d4ba..1de57da3 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -11,7 +12,7 @@ import hs.kr.entrydsm.notification.application.port.`in`.result.RecruitmentGuide interface NotificationPort { fun getNotices(command: ReadNotificationPageCommand): PageResult fun getNotice(id: Long): NoticeDetailResult - fun getFaqs(command: ReadNotificationPageCommand): PageResult + 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/out/FaqRepository.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/port/out/FaqRepository.kt index 15a3d09c..75ea5122 100644 --- 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 @@ -1,11 +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.`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: ReadNotificationPageCommand): PageData + 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/service/NotificationService.kt b/systems/notification/notification-application/src/main/kotlin/hs/kr/entrydsm/notification/application/service/NotificationService.kt index 10a78937..503ea22c 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -29,7 +30,7 @@ class NotificationService( noticeRepository.findById(id)?.toDetailResult() ?: throw NotificationNotFoundException("notice not found: id=$id") - override fun getFaqs(command: ReadNotificationPageCommand): PageResult = + override fun getFaqs(command: ReadFaqPageCommand): PageResult = faqRepository.findPage(command).toResult { it.toSummaryResult() } override fun getFaq(id: Long): FaqDetailResult = 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 index 3db46989..4bd720d7 100644 --- 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 @@ -1,12 +1,14 @@ 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 @@ -112,7 +114,7 @@ class NotificationServiceTest { ), ) - val result = service.getFaqs(ReadNotificationPageCommand(page = 0, size = 1)) + val result = service.getFaqs(ReadFaqPageCommand(page = 0, size = 1)) assertEquals(1, result.content.size) assertEquals(1L, result.content.first().faqId) @@ -121,6 +123,31 @@ class NotificationServiceTest { 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) @@ -201,8 +228,13 @@ class NotificationServiceTest { private inner class FakeFaqRepository( private val faqs: List = emptyList(), ) : FaqRepository { - override fun findPage(command: ReadNotificationPageCommand): PageData = - faqs.sortedBy { it.id }.toPageData(command) + 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 } } @@ -268,6 +300,21 @@ class NotificationServiceTest { ) } + 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-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") + } +} From 258a8dc4e15ea0b89a37aaf4a66d24cee996ac07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Thu, 30 Jul 2026 19:58:15 +0900 Subject: [PATCH 07/11] =?UTF-8?q?test(notification):=20notification=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=8F=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=A0=95=EB=A6=AC=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification-bootstrap/BUILD.bazel | 2 +- .../notification-bootstrap/deps.bzl | 2 ++ .../src/main/resources/application-dev.yaml | 17 +++++++++++++++++ .../src/main/resources/application-prod.yaml | 17 +++++++++++++++++ .../src/main/resources/application.yaml | 8 +++++++- 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 systems/notification/notification-bootstrap/src/main/resources/application-dev.yaml create mode 100644 systems/notification/notification-bootstrap/src/main/resources/application-prod.yaml 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-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..0d20e95f 100644 --- a/systems/notification/notification-bootstrap/src/main/resources/application.yaml +++ b/systems/notification/notification-bootstrap/src/main/resources/application.yaml @@ -2,11 +2,16 @@ spring: application: name: notification profiles: - default: local + default: dev main: lazy-initialization: true + server: shutdown: graceful + +grpc: + port: ${GRPC_PORT:9090} + management: endpoints: web: @@ -16,6 +21,7 @@ management: health: probes: enabled: true + logging: level: root: INFO From 5796fff4964115c8ce9025fc44173c638a188851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Mon, 17 Aug 2026 18:21:11 +0900 Subject: [PATCH 08/11] =?UTF-8?q?chore(notification):=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20DB=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EB=B3=B4=EC=99=84=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-local.yaml | 17 ++++++++++ .../src/main/resources/application.yaml | 4 +-- .../V028__create_notification_tables.sql | 33 +++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 systems/notification/notification-bootstrap/src/main/resources/application-local.yaml create mode 100644 systems/notification/notification-bootstrap/src/main/resources/db/migration/V028__create_notification_tables.sql 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.yaml b/systems/notification/notification-bootstrap/src/main/resources/application.yaml index 0d20e95f..1989c59c 100644 --- a/systems/notification/notification-bootstrap/src/main/resources/application.yaml +++ b/systems/notification/notification-bootstrap/src/main/resources/application.yaml @@ -2,7 +2,7 @@ spring: application: name: notification profiles: - default: dev + default: local main: lazy-initialization: true @@ -10,7 +10,7 @@ server: shutdown: graceful grpc: - port: ${GRPC_PORT:9090} + port: ${GRPC_PORT} management: endpoints: 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..fc07fb9a --- /dev/null +++ b/systems/notification/notification-bootstrap/src/main/resources/db/migration/V028__create_notification_tables.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS notices ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(255) NOT NULL, + content TEXT 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) +); From cd6b4fc5e358a5c71c35ba45c13794f644f036e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Mon, 17 Aug 2026 18:22:50 +0900 Subject: [PATCH 09/11] =?UTF-8?q?test(notification):=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification-adapter-in/BUILD.bazel | 2 +- .../notification-adapter-in/deps.bzl | 1 + .../web/exception/GlobalExceptionHandler.kt | 4 +-- .../test/kotlin/hs/kr/entrydsm/TestMain.kt | 26 +++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) 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 3c4c75ce..93fdf9b7 100644 --- a/systems/notification/notification-adapter-in/deps.bzl +++ b/systems/notification/notification-adapter-in/deps.bzl @@ -1,3 +1,4 @@ +# Dependencies for the notification REST adapter module. KOTLIN_DEPS = [ "@maven//:org_springframework_boot_spring_boot_starter_web", "//systems/notification/notification-application:main", 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 index 6c15eee3..defca948 100644 --- 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 @@ -23,7 +23,7 @@ class GlobalExceptionHandler { response( status = HttpStatus.NOT_FOUND, code = "NOTIFICATION_NOT_FOUND", - message = exception.message ?: "notification not found", + message = "notification not found", ) @ExceptionHandler( @@ -38,7 +38,7 @@ class GlobalExceptionHandler { response( status = HttpStatus.BAD_REQUEST, code = "INVALID_REQUEST", - message = exception.message ?: "invalid request", + message = "invalid request", ) @ExceptionHandler(Exception::class) 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..e115991c 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,37 @@ 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) + } } From 497992bb72b0a6d12fc7b1703dd856355f1af840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Mon, 17 Aug 2026 18:29:37 +0900 Subject: [PATCH 10/11] =?UTF-8?q?test(notification):=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=9D=91=EB=8B=B5=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/test/kotlin/hs/kr/entrydsm/TestMain.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 e115991c..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 @@ -34,4 +34,15 @@ class NotificationAdapterInModuleTest { 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) + } } From a65a99764330651a713fe8bd1f8009d1a3490000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=80=EC=9C=A4?= Date: Wed, 19 Aug 2026 23:37:39 +0900 Subject: [PATCH 11/11] =?UTF-8?q?chore(notification):=20=EA=B3=B5=EC=A7=80?= =?UTF-8?q?=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EB=B3=B4=EC=99=84=20#28?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/migration/V028__create_notification_tables.sql | 1 + 1 file changed, 1 insertion(+) 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 index fc07fb9a..d4fb2e70 100644 --- 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 @@ -2,6 +2,7 @@ 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,