diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/config/EventNotificationSwagger.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/config/EventNotificationSwagger.kt new file mode 100644 index 00000000..5fa04be4 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/config/EventNotificationSwagger.kt @@ -0,0 +1,30 @@ +package com.backgu.amaker.api.notification.config + +import com.backgu.amaker.api.notification.dto.request.NotificationQueryRequest +import com.backgu.amaker.api.notification.dto.response.EventNotificationViewResponse +import com.backgu.amaker.common.http.response.ApiResult +import com.backgu.amaker.common.security.jwt.authentication.JwtAuthentication +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PathVariable + +interface EventNotificationSwagger { + @Operation(summary = "이벤트 알림 조회", description = "이벤트 알림을 조회합니다.") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "이벤트 알림 리스트 성공", + ), + ], + ) + fun getNotifications( + @AuthenticationPrincipal token: JwtAuthentication, + @PathVariable("workspace-id") workspaceId: Long, + @ModelAttribute notificationQueryRequest: NotificationQueryRequest, + ): ResponseEntity> +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/config/NotificationConfig.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/config/NotificationConfig.kt index 2a65b845..348607e7 100644 --- a/api/src/main/kotlin/com/backgu/amaker/api/notification/config/NotificationConfig.kt +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/config/NotificationConfig.kt @@ -1,9 +1,16 @@ package com.backgu.amaker.api.notification.config +import com.backgu.amaker.application.notification.service.NotificationQueryService +import com.backgu.amaker.infra.jpa.notification.repository.EventNotificationRepository import com.backgu.amaker.infra.notification.kafka.config.NotificationKafkaProducerConfig +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import @Configuration @Import(NotificationKafkaProducerConfig::class) -class NotificationConfig +class NotificationConfig { + @Bean + fun notificationQueryService(notificationRepository: EventNotificationRepository): NotificationQueryService = + NotificationQueryService(notificationRepository) +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/controller/EventNotificationController.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/controller/EventNotificationController.kt new file mode 100644 index 00000000..cb76d862 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/controller/EventNotificationController.kt @@ -0,0 +1,51 @@ +package com.backgu.amaker.api.notification.controller + +import com.backgu.amaker.api.notification.config.EventNotificationSwagger +import com.backgu.amaker.api.notification.dto.request.NotificationQueryRequest +import com.backgu.amaker.api.notification.dto.response.EventNotificationViewResponse +import com.backgu.amaker.api.notification.service.NotificationFacadeService +import com.backgu.amaker.common.http.ApiHandler +import com.backgu.amaker.common.http.response.ApiResult +import com.backgu.amaker.common.security.jwt.authentication.JwtAuthentication +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1/workspaces/{workspace-id}/notifications") +class EventNotificationController( + private val notificationFacadeService: NotificationFacadeService, + private val apiHandler: ApiHandler, +) : EventNotificationSwagger { + @GetMapping + override fun getNotifications( + @AuthenticationPrincipal token: JwtAuthentication, + @PathVariable("workspace-id") workspaceId: Long, + @ModelAttribute notificationQueryRequest: NotificationQueryRequest, + ): ResponseEntity> { + val pageable = + PageRequest.of( + notificationQueryRequest.page, + notificationQueryRequest.size, + Sort.by("id").ascending(), + ) + + return ResponseEntity.ok( + apiHandler.onSuccess( + EventNotificationViewResponse.of( + notificationFacadeService.getNotifications( + token.id, + workspaceId, + pageable, + ), + ), + ), + ) + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/controller/NotificationSwagger.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/controller/NotificationSwagger.kt new file mode 100644 index 00000000..87335008 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/controller/NotificationSwagger.kt @@ -0,0 +1,29 @@ +package com.backgu.amaker.api.notification.controller + +import com.backgu.amaker.api.event.dto.response.ReplyEventDetailResponse +import com.backgu.amaker.common.http.response.ApiResult +import com.backgu.amaker.common.security.jwt.authentication.JwtAuthentication +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.PathVariable + +@Tag(name = "notification", description = "알림 API") +interface NotificationSwagger { + @Operation(summary = "알림 리스트 조회", description = "알림 내역들을 조회합니다.") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "알림내역 조회 성공", + ), + ], + ) + fun getNotifications( + @AuthenticationPrincipal token: JwtAuthentication, + @PathVariable("workspace-id") workspaceId: Long, + ): ResponseEntity> +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/EventNotificationDto.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/EventNotificationDto.kt new file mode 100644 index 00000000..f30b8a24 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/EventNotificationDto.kt @@ -0,0 +1,22 @@ +package com.backgu.amaker.api.notification.dto + +import com.backgu.amaker.domain.notifiacation.EventNotification + +data class EventNotificationDto( + val id: Long, + val title: String, + val content: String, + val userId: String, + val eventId: Long, +) { + companion object { + fun from(eventNotification: EventNotification): EventNotificationDto = + EventNotificationDto( + id = eventNotification.id, + title = eventNotification.title, + content = eventNotification.content, + userId = eventNotification.userId, + eventId = eventNotification.eventId, + ) + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/NotificationQueryDto.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/NotificationQueryDto.kt new file mode 100644 index 00000000..bfbe56cb --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/NotificationQueryDto.kt @@ -0,0 +1,6 @@ +package com.backgu.amaker.api.notification.dto + +data class NotificationQueryDto( + val notificationId: Long, + val size: Int, +) diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/request/NotificationQueryRequest.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/request/NotificationQueryRequest.kt new file mode 100644 index 00000000..c2094a53 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/request/NotificationQueryRequest.kt @@ -0,0 +1,17 @@ +package com.backgu.amaker.api.notification.dto.request + +import com.backgu.amaker.api.notification.dto.NotificationQueryDto +import io.swagger.v3.oas.annotations.media.Schema + +data class NotificationQueryRequest( + @Schema(description = "읽어올 페이지 번호", example = "2", defaultValue = "0") + val page: Int = 0, + @Schema(description = "읽어올 응답의 개수", example = "100", defaultValue = "20") + val size: Int = 20, +) { + fun toDto(notificationId: Long): NotificationQueryDto = + NotificationQueryDto( + notificationId = notificationId, + size = size, + ) +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/response/EventNotificationResponse.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/response/EventNotificationResponse.kt new file mode 100644 index 00000000..a458c10b --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/response/EventNotificationResponse.kt @@ -0,0 +1,28 @@ +package com.backgu.amaker.api.notification.dto.response + +import com.backgu.amaker.api.notification.dto.EventNotificationDto +import io.swagger.v3.oas.annotations.media.Schema + +data class EventNotificationResponse( + @Schema(description = "이벤트 id", example = "1") + val id: Long, + @Schema(description = "이벤트 제목", example = "이벤트 제목") + val title: String, + @Schema(description = "이벤트 내용", example = "이벤트 내용") + val content: String, + @Schema(description = "유저 id", example = "2") + val userId: String, + @Schema(description = "이벤트 id", example = "2") + val eventId: Long, +) { + companion object { + fun of(eventNotification: EventNotificationDto) = + EventNotificationResponse( + id = eventNotification.id, + title = eventNotification.title, + content = eventNotification.content, + userId = eventNotification.userId, + eventId = eventNotification.eventId, + ) + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/response/EventNotificationViewResponse.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/response/EventNotificationViewResponse.kt new file mode 100644 index 00000000..cbce093c --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/dto/response/EventNotificationViewResponse.kt @@ -0,0 +1,34 @@ +package com.backgu.amaker.api.notification.dto.response + +import com.backgu.amaker.api.notification.dto.EventNotificationDto +import com.backgu.amaker.common.http.response.PageResponse +import org.springframework.data.domain.Page + +class EventNotificationViewResponse( + override val content: List, + override val pageNumber: Int, + override val pageSize: Int, + override val totalElements: Long, + override val totalPages: Int, + override val hasNext: Boolean, + override val hasPrevious: Boolean, + override val isFirst: Boolean, + override val isLast: Boolean, +) : PageResponse { + companion object { + fun of(eventNotifications: Page): EventNotificationViewResponse { + val content = eventNotifications.content.map { EventNotificationResponse.of(it) } + return EventNotificationViewResponse( + content = content, + pageNumber = eventNotifications.number, + pageSize = eventNotifications.size, + totalElements = eventNotifications.totalElements, + totalPages = eventNotifications.totalPages, + hasNext = eventNotifications.hasNext(), + hasPrevious = eventNotifications.hasPrevious(), + isFirst = eventNotifications.isFirst, + isLast = eventNotifications.isLast, + ) + } + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/notification/service/NotificationFacadeService.kt b/api/src/main/kotlin/com/backgu/amaker/api/notification/service/NotificationFacadeService.kt new file mode 100644 index 00000000..12b5b954 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/notification/service/NotificationFacadeService.kt @@ -0,0 +1,42 @@ +package com.backgu.amaker.api.notification.service + +import com.backgu.amaker.api.notification.dto.EventNotificationDto +import com.backgu.amaker.application.event.service.EventAssignedUserService +import com.backgu.amaker.application.event.service.ReactionCommentService +import com.backgu.amaker.application.event.service.ReactionEventService +import com.backgu.amaker.application.event.service.ReactionOptionService +import com.backgu.amaker.application.event.service.ReplyCommentService +import com.backgu.amaker.application.event.service.ReplyEventService +import com.backgu.amaker.application.notification.service.NotificationQueryService +import com.backgu.amaker.application.user.service.UserService +import com.backgu.amaker.application.workspace.WorkspaceUserService +import org.springframework.context.ApplicationEventPublisher +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class NotificationFacadeService( + private val userService: UserService, + private val replyEventService: ReplyEventService, + private val reactionEventService: ReactionEventService, + private val eventAssignedUserService: EventAssignedUserService, + private val replyCommentService: ReplyCommentService, + private val reactionCommentService: ReactionCommentService, + private val reactionOptionService: ReactionOptionService, + private val workspaceUserService: WorkspaceUserService, + private val notificationQueryService: NotificationQueryService, + private val eventPublisher: ApplicationEventPublisher, +) { + fun getNotifications( + userId: String, + workspaceId: Long, + pageable: Pageable, + ): Page { + workspaceUserService.validateUserInWorkspace(userId, workspaceId) + + return notificationQueryService.getNotification(userId, workspaceId, pageable).map { EventNotificationDto.from(it) } + } +} diff --git a/api/src/main/resources/logback-spring.xml b/api/src/main/resources/logback-spring.xml deleted file mode 100644 index c15afa9e..00000000 --- a/api/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level){ERROR=red, WARN=yellow, INFO=green, DEBUG=blue} %logger{36} - %msg%n - - - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level){ERROR=red, WARN=yellow, INFO=green, DEBUG=blue} %logger{36} - %msg%n - - - - - - - - - - - ./logs/api.log - - ./logs/api-%d{yyyy-MM-dd}.log - 30 - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - diff --git a/api/src/test/kotlin/com/backgu/amaker/api/event/service/EventCommentFacadeServiceTest.kt b/api/src/test/kotlin/com/backgu/amaker/api/event/service/NotificationFacadeServiceTest.kt similarity index 99% rename from api/src/test/kotlin/com/backgu/amaker/api/event/service/EventCommentFacadeServiceTest.kt rename to api/src/test/kotlin/com/backgu/amaker/api/event/service/NotificationFacadeServiceTest.kt index df1ac3f0..c8c912fb 100644 --- a/api/src/test/kotlin/com/backgu/amaker/api/event/service/EventCommentFacadeServiceTest.kt +++ b/api/src/test/kotlin/com/backgu/amaker/api/event/service/NotificationFacadeServiceTest.kt @@ -18,7 +18,7 @@ import kotlin.test.Test @DisplayName("EventFacadeService 테스트") @Transactional -class EventCommentFacadeServiceTest : IntegrationTest() { +class NotificationFacadeServiceTest : IntegrationTest() { @Autowired lateinit var eventCommentFacadeService: EventCommentFacadeService diff --git a/domain/src/main/kotlin/com/backgu/amaker/domain/event/ReactionEvent.kt b/domain/src/main/kotlin/com/backgu/amaker/domain/event/ReactionEvent.kt index 84e35619..dc0de88a 100644 --- a/domain/src/main/kotlin/com/backgu/amaker/domain/event/ReactionEvent.kt +++ b/domain/src/main/kotlin/com/backgu/amaker/domain/event/ReactionEvent.kt @@ -12,7 +12,7 @@ class ReactionEvent( updatedAt: LocalDateTime = LocalDateTime.now(), ) : Event(id, eventTitle, deadLine, notificationStartTime, notificationInterval, createdAt, updatedAt) { companion object { - const val EVENT_TYPE = "REPLY" + const val EVENT_TYPE = "REACTION" } fun createReactionOption(contents: List) = diff --git a/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/EventNotification.kt b/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/EventNotification.kt new file mode 100644 index 00000000..79f03931 --- /dev/null +++ b/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/EventNotification.kt @@ -0,0 +1,9 @@ +package com.backgu.amaker.domain.notifiacation + +class EventNotification( + val id: Long, + val title: String, + val content: String, + val userId: String, + val eventId: Long, +) diff --git a/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/Notification.kt b/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/Notification.kt index 226c064e..9369e284 100644 --- a/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/Notification.kt +++ b/domain/src/main/kotlin/com/backgu/amaker/domain/notifiacation/Notification.kt @@ -8,7 +8,5 @@ interface Notification : Serializable { val keyPrefix: String val keyValue: String - fun getNotificationKey(): String { - return "$keyPrefix:$keyValue" - } + fun getNotificationKey(): String = "$keyPrefix:$keyValue" } diff --git a/infra/src/main/kotlin/com/backgu/amaker/application/notification/service/NotificationQueryService.kt b/infra/src/main/kotlin/com/backgu/amaker/application/notification/service/NotificationQueryService.kt new file mode 100644 index 00000000..f5750986 --- /dev/null +++ b/infra/src/main/kotlin/com/backgu/amaker/application/notification/service/NotificationQueryService.kt @@ -0,0 +1,20 @@ +package com.backgu.amaker.application.notification.service + +import com.backgu.amaker.domain.notifiacation.EventNotification +import com.backgu.amaker.infra.jpa.notification.repository.EventNotificationRepository +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class NotificationQueryService( + private val eventNotificationRepository: EventNotificationRepository, +) { + fun getNotification( + userId: String, + workspaceId: Long, + page: Pageable, + ): Page = eventNotificationRepository.findByUserId(userId, workspaceId, page).map { it.toDomain() } +} diff --git a/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/entity/EventNotificationEntity.kt b/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/entity/EventNotificationEntity.kt index 241faa40..93cf36d1 100644 --- a/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/entity/EventNotificationEntity.kt +++ b/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/entity/EventNotificationEntity.kt @@ -1,5 +1,6 @@ package com.backgu.amaker.infra.jpa.notification.entity +import com.backgu.amaker.domain.notifiacation.EventNotification import jakarta.persistence.Column import jakarta.persistence.DiscriminatorValue import jakarta.persistence.Entity @@ -14,4 +15,13 @@ class EventNotificationEntity( userId: String, @Column(nullable = false) val eventId: Long, -) : NotificationEntity(title = title, content = content, userId = userId) +) : NotificationEntity(title = title, content = content, userId = userId) { + fun toDomain(): EventNotification = + EventNotification( + id = id, + title = title, + content = content, + userId = userId, + eventId = eventId, + ) +} diff --git a/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/repository/EventNotificationRepository.kt b/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/repository/EventNotificationRepository.kt index 598de340..2dc563b5 100644 --- a/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/repository/EventNotificationRepository.kt +++ b/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/notification/repository/EventNotificationRepository.kt @@ -1,6 +1,27 @@ package com.backgu.amaker.infra.jpa.notification.repository import com.backgu.amaker.infra.jpa.notification.entity.EventNotificationEntity +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query -interface EventNotificationRepository : JpaRepository +interface EventNotificationRepository : JpaRepository { + @Query( + """ + SELECT en + FROM EventNotification en + INNER JOIN fetch Event e ON e.id = en.eventId + INNER JOIN fetch Chat ch ON ch.id = e.id + INNER JOIN fetch ChatRoom cr ON cr.id = ch.chatRoomId + WHERE en.userId = :userId + AND cr.workspaceId = :workspaceId + order by en.id desc + """, + ) + fun findByUserId( + userId: String, + workspaceId: Long, + pageable: Pageable, + ): Page +} diff --git a/notification/src/main/resources/application.yaml b/notification/src/main/resources/application.yaml index 847a7478..ce728f28 100644 --- a/notification/src/main/resources/application.yaml +++ b/notification/src/main/resources/application.yaml @@ -5,9 +5,9 @@ spring: web-application-type: none datasource: - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} + url: jdbc:mysql://localhost:3306/amaker + username: root + password: 1234 driver-class-name: com.mysql.cj.jdbc.Driver jpa: @@ -19,8 +19,8 @@ spring: mail: host: smtp.gmail.com port: 587 - username: ${MAIL_USERNAME} - password: ${MAIL_PASSWORD} + username: dltmd202@gmail.com + password: qndschbjumjufmod properties: mail: smtp: @@ -57,6 +57,6 @@ amazon: region: ${AWS_REGION} fcm: - file: hi.json + file: fcm.json base-url: https://fcm.googleapis.com/v1/projects/a-maker-2b48b auth-url: https://www.googleapis.com/auth/cloud-platform