diff --git a/api/src/main/kotlin/com/backgu/amaker/api/chat/service/ChatFacadeService.kt b/api/src/main/kotlin/com/backgu/amaker/api/chat/service/ChatFacadeService.kt index f08ac988..27b9292a 100644 --- a/api/src/main/kotlin/com/backgu/amaker/api/chat/service/ChatFacadeService.kt +++ b/api/src/main/kotlin/com/backgu/amaker/api/chat/service/ChatFacadeService.kt @@ -185,9 +185,9 @@ class ChatFacadeService( userMap: Map, ): ChatWithUserDto<*> = if (ChatType.isEventChat(chat.chatType)) { - val event = eventMap[chat.id] ?: throw BusinessException(StatusCode.EVENT_NOT_FOUND) - val eventUsers = eventUserMap[event.id]?.mapNotNull { userMap[it.userId] } ?: emptyList() - val finishedNumber = eventUserMap[event.id]?.count { it.isFinished } ?: 0 + val event: Event = eventMap[chat.id] ?: throw BusinessException(StatusCode.EVENT_NOT_FOUND) + val eventUsers: List = eventUserMap[event.id]?.mapNotNull { userMap[it.userId] } ?: emptyList() + val finishedNumber: Int = eventUserMap[event.id]?.count { it.isFinished } ?: 0 EventChatWithUserDto.of(chat, EventWithUserDto.of(event, eventUsers, finishedNumber)) } else { DefaultChatWithUserDto.of(chat) diff --git a/api/src/main/kotlin/com/backgu/amaker/api/event/controller/EventQueryController.kt b/api/src/main/kotlin/com/backgu/amaker/api/event/controller/EventQueryController.kt new file mode 100644 index 00000000..95a8c6c8 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/event/controller/EventQueryController.kt @@ -0,0 +1,42 @@ +package com.backgu.amaker.api.event.controller + +import com.backgu.amaker.api.event.dto.response.EventBriefResponse +import com.backgu.amaker.api.event.service.EventFacadeService +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 com.backgu.amaker.domain.event.EventStatus +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.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController +import java.util.Locale + +@RestController +@RequestMapping("/api/v1/workspaces/{workspace-id}/events") +class EventQueryController( + private val eventFacadeService: EventFacadeService, + private val apiHandler: ApiHandler, +) : EventQuerySwagger { + @GetMapping + override fun getOngoingEvent( + @AuthenticationPrincipal token: JwtAuthentication, + @PathVariable("workspace-id") workspaceId: Long, + @RequestParam status: String, + ): ResponseEntity>> { + val eventStatus = EventStatus.valueOf(status.uppercase(Locale.getDefault())) + return ResponseEntity.ok().body( + apiHandler.onSuccess( + eventFacadeService + .getEvents( + token.id, + workspaceId, + eventStatus, + ).map { EventBriefResponse.of(it, token.id) }, + ), + ) + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/event/controller/EventQuerySwagger.kt b/api/src/main/kotlin/com/backgu/amaker/api/event/controller/EventQuerySwagger.kt new file mode 100644 index 00000000..6e88ff7a --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/event/controller/EventQuerySwagger.kt @@ -0,0 +1,30 @@ +package com.backgu.amaker.api.event.controller + +import com.backgu.amaker.api.event.dto.response.EventBriefResponse +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 = "event-query", description = "이벤트 조회 API") +interface EventQuerySwagger { + @Operation(summary = "진행중인 이벤트 조회", description = "진행중인 이벤트 리스트 조회입니다.") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "진행중인 이벤트 리스트 조회 성공", + ), + ], + ) + fun getOngoingEvent( + @AuthenticationPrincipal token: JwtAuthentication, + @PathVariable("workspace-id") workspaceId: Long, + status: String, + ): ResponseEntity>> +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/event/dto/EventWithUserAndChatRoomDto.kt b/api/src/main/kotlin/com/backgu/amaker/api/event/dto/EventWithUserAndChatRoomDto.kt new file mode 100644 index 00000000..7b3a9a41 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/event/dto/EventWithUserAndChatRoomDto.kt @@ -0,0 +1,39 @@ +package com.backgu.amaker.api.event.dto + +import com.backgu.amaker.api.user.dto.UserDto +import com.backgu.amaker.domain.chat.Chat +import com.backgu.amaker.domain.event.Event +import com.backgu.amaker.domain.user.User +import java.time.LocalDateTime + +data class EventWithUserAndChatRoomDto( + val id: Long, + val chatRoomId: Long, + val eventTitle: String, + val deadLine: LocalDateTime, + val notificationStartTime: LocalDateTime, + val notificationInterval: Int, + val users: List, + val finishedCount: Int, + val totalAssignedCount: Int, +) { + companion object { + fun of( + event: Event, + chat: Chat, + users: List, + finishedCount: Int = 0, + ): EventWithUserAndChatRoomDto = + EventWithUserAndChatRoomDto( + id = event.id, + chatRoomId = chat.chatRoomId, + eventTitle = event.eventTitle, + deadLine = event.deadLine, + notificationStartTime = event.notificationStartTime, + notificationInterval = event.notificationInterval, + users = users.map { UserDto.of(it) }, + finishedCount = finishedCount, + totalAssignedCount = users.size, + ) + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/event/dto/response/EventBriefResponse.kt b/api/src/main/kotlin/com/backgu/amaker/api/event/dto/response/EventBriefResponse.kt new file mode 100644 index 00000000..2e448768 --- /dev/null +++ b/api/src/main/kotlin/com/backgu/amaker/api/event/dto/response/EventBriefResponse.kt @@ -0,0 +1,76 @@ +package com.backgu.amaker.api.event.dto.response + +import com.backgu.amaker.api.event.dto.EventWithUserAndChatRoomDto +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class EventBriefResponse( + @Schema( + description = "이벤트 아이디", + example = "2", + ) + val eventId: Long, + @Schema( + description = "채팅방 아이디", + example = "21", + ) + val chatRoomId: Long, + @Schema( + description = "이벤트 제목", + example = "우리 어디서 만날지", + ) + val eventTitle: String, + @Schema( + description = "데드라인", + example = "2024-07-24T07:39:37.598", + ) + val deadLine: LocalDateTime, + @Schema( + description = "알림 보낼 시작 시간", + example = "2024-07-24T06:09:37.598", + ) + val notificationStartTime: LocalDateTime, + @Schema( + description = "알림 주기", + example = "15", + ) + val notificationInterval: Int, + @Schema( + description = "유저 사진 리스트", + example = "[\"http://a-maker.com/hi1.png\", \"http://a-maker.com/hi2.png\"]", + ) + val users: List, + @Schema( + description = "완료된 이벤트 수", + example = "2", + ) + val finishedCount: Int, + @Schema( + description = "총 배정된 이벤트 수", + example = "5", + ) + val totalAssignedCount: Int, + @Schema( + description = "이벤트가 자신의 것인지", + example = "true", + ) + val isMine: Boolean, +) { + companion object { + fun of( + event: EventWithUserAndChatRoomDto, + userId: String, + ) = EventBriefResponse( + eventId = event.id, + chatRoomId = event.chatRoomId, + eventTitle = event.eventTitle, + deadLine = event.deadLine, + notificationStartTime = event.notificationStartTime, + notificationInterval = event.notificationInterval, + users = event.users.map { it.picture }, + finishedCount = event.finishedCount, + totalAssignedCount = event.totalAssignedCount, + isMine = event.users.any { it.id == userId }, + ) + } +} diff --git a/api/src/main/kotlin/com/backgu/amaker/api/event/service/EventFacadeService.kt b/api/src/main/kotlin/com/backgu/amaker/api/event/service/EventFacadeService.kt index 35230131..774340f1 100644 --- a/api/src/main/kotlin/com/backgu/amaker/api/event/service/EventFacadeService.kt +++ b/api/src/main/kotlin/com/backgu/amaker/api/event/service/EventFacadeService.kt @@ -1,5 +1,6 @@ package com.backgu.amaker.api.event.service +import com.backgu.amaker.api.event.dto.EventWithUserAndChatRoomDto import com.backgu.amaker.api.event.dto.ReactionEventCreateDto import com.backgu.amaker.api.event.dto.ReactionEventDetailDto import com.backgu.amaker.api.event.dto.ReactionEventDto @@ -12,14 +13,20 @@ import com.backgu.amaker.application.chat.service.ChatRoomService import com.backgu.amaker.application.chat.service.ChatRoomUserService import com.backgu.amaker.application.chat.service.ChatService import com.backgu.amaker.application.event.service.EventAssignedUserService +import com.backgu.amaker.application.event.service.EventService import com.backgu.amaker.application.event.service.ReactionEventService import com.backgu.amaker.application.event.service.ReactionOptionService import com.backgu.amaker.application.event.service.ReplyEventService import com.backgu.amaker.application.user.service.UserService +import com.backgu.amaker.application.workspace.WorkspaceUserService import com.backgu.amaker.common.exception.BusinessException import com.backgu.amaker.common.status.StatusCode import com.backgu.amaker.domain.chat.Chat import com.backgu.amaker.domain.chat.ChatType +import com.backgu.amaker.domain.event.Event +import com.backgu.amaker.domain.event.EventAssignedUser +import com.backgu.amaker.domain.event.EventStatus +import com.backgu.amaker.domain.user.User import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -36,6 +43,8 @@ class EventFacadeService( private val reactionOptionService: ReactionOptionService, private val eventAssignedUserService: EventAssignedUserService, private val eventPublisher: ApplicationEventPublisher, + private val eventService: EventService, + private val workspaceUserService: WorkspaceUserService, ) { @Transactional fun getReplyEvent( @@ -196,4 +205,29 @@ class EventFacadeService( return ReactionEventDto.of(reactionEvent, reactionOptions) } + + fun getEvents( + userId: String, + workspaceId: Long, + eventStatus: EventStatus, + ): List { + workspaceUserService.validateUserInWorkspace(userId, workspaceId) + val events: List = eventService.findEventByWorkspaceId(workspaceId) + val eventUserMap: Map> = + eventAssignedUserService.findByEventIdsToEventIdMapped(events.map { it.id }) + val filteredEvents = + events.filter { eventStatus.filter(it, eventUserMap[it.id] ?: emptyList()) } + val chatToMap = chatService.findAllByIdsToMap(filteredEvents.map { it.id }) + + val userMap = userService.findAllByUserIdsToMap(eventUserMap.values.flatten().map { it.userId }) + + return filteredEvents.map { event: Event -> + val eventAssignedUsers: List = + eventUserMap[event.id]?.mapNotNull { userMap[it.userId] } ?: emptyList() + val finishedNumber = eventUserMap[event.id]?.count { it.isFinished } ?: 0 + val chat = chatToMap[event.id] ?: throw BusinessException(StatusCode.CHAT_NOT_FOUND) + + EventWithUserAndChatRoomDto.of(event, chat, eventAssignedUsers, finishedNumber) + } + } } diff --git a/domain/src/main/kotlin/com/backgu/amaker/domain/event/Event.kt b/domain/src/main/kotlin/com/backgu/amaker/domain/event/Event.kt index 2b26fc11..054aa019 100644 --- a/domain/src/main/kotlin/com/backgu/amaker/domain/event/Event.kt +++ b/domain/src/main/kotlin/com/backgu/amaker/domain/event/Event.kt @@ -29,4 +29,8 @@ abstract class Event( finishedCount, totalAssignedCount, ) + + fun isAchieved(user: List): Boolean = user.size == user.count { it.isFinished } + + fun isClosed(): Boolean = LocalDateTime.now().isAfter(deadLine) } diff --git a/domain/src/main/kotlin/com/backgu/amaker/domain/event/EventStatus.kt b/domain/src/main/kotlin/com/backgu/amaker/domain/event/EventStatus.kt new file mode 100644 index 00000000..45554180 --- /dev/null +++ b/domain/src/main/kotlin/com/backgu/amaker/domain/event/EventStatus.kt @@ -0,0 +1,27 @@ +package com.backgu.amaker.domain.event + +enum class EventStatus { + ONGOING { + override fun filter( + event: Event, + assignedUsers: List, + ): Boolean = !event.isAchieved(assignedUsers) && !event.isClosed() + }, + EXPIRED { + override fun filter( + event: Event, + assignedUsers: List, + ): Boolean = event.isClosed() && !event.isAchieved(assignedUsers) + }, + COMPLETED { + override fun filter( + event: Event, + assignedUsers: List, + ): Boolean = event.isAchieved(assignedUsers) + }, ; + + abstract fun filter( + event: Event, + assignedUsers: List, + ): Boolean +} diff --git a/domain/src/test/kotlin/com/backgu/amaker/domain/event/EventAssignedUserTest.kt b/domain/src/test/kotlin/com/backgu/amaker/domain/event/EventAssignedUserTest.kt index 461b4e38..6e408907 100644 --- a/domain/src/test/kotlin/com/backgu/amaker/domain/event/EventAssignedUserTest.kt +++ b/domain/src/test/kotlin/com/backgu/amaker/domain/event/EventAssignedUserTest.kt @@ -8,7 +8,7 @@ import kotlin.test.Test class EventAssignedUserTest { @Test @DisplayName("isFinished 업데이트 테스트") - fun updateIsFinished() { + fun updateIsAchieved() { // given val eventAssignedUser = EventAssignedUser( diff --git a/infra/src/main/kotlin/com/backgu/amaker/application/chat/service/ChatService.kt b/infra/src/main/kotlin/com/backgu/amaker/application/chat/service/ChatService.kt index 879087e9..d6d3b1a4 100644 --- a/infra/src/main/kotlin/com/backgu/amaker/application/chat/service/ChatService.kt +++ b/infra/src/main/kotlin/com/backgu/amaker/application/chat/service/ChatService.kt @@ -29,5 +29,8 @@ class ChatService( fun findAllByIds(chatIds: List): List = chatRepository.findAllById(chatIds).map { it.toDomain() } + fun findAllByIdsToMap(chatIds: List): Map = + chatRepository.findAllById(chatIds).map { it.toDomain() }.associateBy { it.id } + fun getById(chatId: Long) = chatRepository.findByIdOrNull(chatId)?.toDomain() ?: throw BusinessException(StatusCode.CHAT_NOT_FOUND) } diff --git a/infra/src/main/kotlin/com/backgu/amaker/application/event/service/EventService.kt b/infra/src/main/kotlin/com/backgu/amaker/application/event/service/EventService.kt index 95cdfc6a..d43fb861 100644 --- a/infra/src/main/kotlin/com/backgu/amaker/application/event/service/EventService.kt +++ b/infra/src/main/kotlin/com/backgu/amaker/application/event/service/EventService.kt @@ -18,4 +18,6 @@ class EventService( fun findEventByIdsToMap(eventIds: List): Map = eventRepository.findAllById(eventIds).map { it.toDomain() }.associateBy { it.id } + + fun findEventByWorkspaceId(workspaceId: Long): List = eventRepository.findAllByWorkspaceId(workspaceId).map { it.toDomain() } } diff --git a/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/event/repository/EventRepository.kt b/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/event/repository/EventRepository.kt index 4350d949..af555512 100644 --- a/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/event/repository/EventRepository.kt +++ b/infra/src/main/kotlin/com/backgu/amaker/infra/jpa/event/repository/EventRepository.kt @@ -2,5 +2,14 @@ package com.backgu.amaker.infra.jpa.event.repository import com.backgu.amaker.infra.jpa.event.entity.EventEntity import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query -interface EventRepository : JpaRepository +interface EventRepository : JpaRepository { + @Query( + "SELECT e FROM Event e " + + "inner join Chat c on e.id = c.id " + + "inner join ChatRoom ch on ch.id = c.chatRoomId " + + "WHERE ch.workspaceId = :workspaceId", + ) + fun findAllByWorkspaceId(workspaceId: Long): List +}