Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.weeth.domain.attendance.domain.repository

import com.weeth.domain.attendance.domain.entity.Attendance
import com.weeth.domain.attendance.domain.enums.AttendanceStatus
import com.weeth.domain.session.domain.entity.Session
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice

Expand All @@ -17,4 +18,11 @@ interface AttendanceReader {
status: AttendanceStatus,
pageable: Pageable,
): Slice<Attendance>

fun findAllBySession(session: Session): List<Attendance>

fun findBySessionAndUserId(
session: Session,
userId: Long,
): Attendance?
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ interface AttendanceRepository :
): List<Attendance>

@EntityGraph(attributePaths = ["clubMember", "clubMember.user"])
fun findAllBySession(session: Session): List<Attendance>
override fun findAllBySession(session: Session): List<Attendance>

@Query(
"SELECT a FROM Attendance a JOIN FETCH a.clubMember cm JOIN FETCH cm.user WHERE a.session = :session AND cm.user.id = :userId",
)
override fun findBySessionAndUserId(
@Param("session") session: Session,
@Param("userId") userId: Long,
): Attendance?

@Query(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.weeth.domain.schedule.application.dto.response

import com.weeth.domain.club.domain.enums.MemberRole
import io.swagger.v3.oas.annotations.media.Schema

data class AttendeeResponse(
@field:Schema(description = "이름", example = "홍길동")
val name: String,
@field:Schema(description = "학과", example = "컴퓨터공학과")
val department: String?,
@field:Schema(description = "권한", example = "USER")
val role: MemberRole,
@field:Schema(description = "프로필 이미지 URL")
val profileImageUrl: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.weeth.domain.schedule.application.dto.response

enum class ScheduleAttendanceStatus {
UPCOMING, // 출석 예정
OPEN, // 지금 출석 가능
COMPLETED, // 출석 완료
ABSENT, // 결석
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.weeth.domain.schedule.application.dto.response

import com.weeth.domain.schedule.domain.enums.Type
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

data class ScheduleDetailResponse(
@field:Schema(description = "일정 ID", example = "1")
val id: Long,
@field:Schema(description = "일정 유형", example = "SESSION")
val type: Type,
@field:Schema(description = "제목", example = "1주차 정기모임")
val title: String,
@field:Schema(description = "설명")
val description: String?,
@field:Schema(description = "장소", example = "가천대 체육관")
val location: String?,
@field:Schema(description = "시작 시간")
val start: LocalDateTime,
@field:Schema(description = "종료 시간")
val end: LocalDateTime,
@field:Schema(description = "생성자 이름", example = "홍길동")
val creatorName: String?,
@field:Schema(description = "내 출석 상태 (SESSION만, EVENT는 null)")
val myAttendanceStatus: ScheduleAttendanceStatus?,
@field:Schema(description = "출석 완료 시간 (COMPLETED일 때만, 나머지는 null)")
val attendedAt: LocalDateTime?,
@field:Schema(description = "총 참석자 수 (SESSION만, EVENT는 null)")
val totalAttendees: Int?,
@field:Schema(description = "참석자 목록 (SESSION만, EVENT는 null)")
val attendees: List<AttendeeResponse>?,
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package com.weeth.domain.schedule.application.mapper

import com.weeth.domain.attendance.domain.entity.Attendance
import com.weeth.domain.file.domain.port.FileAccessUrlPort
import com.weeth.domain.schedule.application.dto.response.AttendeeResponse
import com.weeth.domain.schedule.application.dto.response.ScheduleAttendanceStatus
import com.weeth.domain.schedule.application.dto.response.ScheduleDetailResponse
import com.weeth.domain.schedule.application.dto.response.ScheduleResponse
import com.weeth.domain.schedule.domain.entity.Event
import com.weeth.domain.schedule.domain.enums.Type
import com.weeth.domain.session.domain.entity.Session
import org.springframework.stereotype.Component
import java.time.LocalDateTime

@Component
class ScheduleMapper {
class ScheduleMapper(
private val fileAccessUrlPort: FileAccessUrlPort,
) {
fun toResponse(event: Event): ScheduleResponse =
ScheduleResponse(
id = event.id,
Expand All @@ -29,4 +37,49 @@ class ScheduleMapper {
location = session.location,
cardinal = session.cardinal,
)

fun toDetailResponse(event: Event): ScheduleDetailResponse =
ScheduleDetailResponse(
id = event.id,
type = Type.EVENT,
title = event.title,
description = event.content,
location = event.location,
start = event.start,
end = event.end,
creatorName = event.user?.name,
myAttendanceStatus = null,
attendedAt = null,
totalAttendees = null,
attendees = null,
)

fun toDetailResponse(
session: Session,
attendances: List<Attendance>,
myAttendanceStatus: ScheduleAttendanceStatus,
attendedAt: LocalDateTime?,
): ScheduleDetailResponse =
ScheduleDetailResponse(
id = session.id,
type = Type.SESSION,
title = session.title,
description = session.content,
location = session.location,
start = session.start,
end = session.end,
creatorName = session.user?.name,
myAttendanceStatus = myAttendanceStatus,
attendedAt = attendedAt,
totalAttendees = attendances.size,
attendees = attendances.map { toAttendeeResponse(it) },
)

private fun toAttendeeResponse(attendance: Attendance): AttendeeResponse =
AttendeeResponse(
name = attendance.clubMember.user.name,
department = attendance.clubMember.user.department,
role = attendance.clubMember.memberRole,
profileImageUrl = attendance.clubMember.profileImageStorageKey?.let { fileAccessUrlPort.resolve(it) },
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package com.weeth.domain.schedule.application.usecase.query

import com.weeth.domain.attendance.domain.entity.Attendance
import com.weeth.domain.attendance.domain.enums.AttendanceStatus
import com.weeth.domain.attendance.domain.repository.AttendanceReader
import com.weeth.domain.club.domain.service.ClubMemberPolicy
import com.weeth.domain.schedule.application.dto.response.EventResponse
import com.weeth.domain.schedule.application.dto.response.ScheduleAttendanceStatus
import com.weeth.domain.schedule.application.dto.response.ScheduleDetailResponse
import com.weeth.domain.schedule.application.dto.response.ScheduleResponse
import com.weeth.domain.schedule.application.exception.EventNotFoundException
import com.weeth.domain.schedule.application.mapper.EventMapper
import com.weeth.domain.schedule.application.mapper.ScheduleMapper
import com.weeth.domain.schedule.domain.enums.Type
import com.weeth.domain.schedule.domain.repository.EventRepository
import com.weeth.domain.session.application.exception.SessionNotFoundException
import com.weeth.domain.session.domain.entity.Session
import com.weeth.domain.session.domain.enums.SessionStatus
import com.weeth.domain.session.domain.repository.SessionReader
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
Expand All @@ -18,6 +27,7 @@ import java.time.LocalDateTime
class GetScheduleQueryService(
private val eventRepository: EventRepository,
private val sessionReader: SessionReader,
private val attendanceReader: AttendanceReader,
private val clubMemberPolicy: ClubMemberPolicy,
private val scheduleMapper: ScheduleMapper,
private val eventMapper: EventMapper,
Expand All @@ -38,24 +48,82 @@ class GetScheduleQueryService(
fun findMonthly(
clubId: Long,
userId: Long,
cardinal: Int,
start: LocalDateTime,
end: LocalDateTime,
): List<ScheduleResponse> {
clubMemberPolicy.getActiveMember(clubId, userId)

val events =
eventRepository
.findByClubIdAndStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(clubId, end, start)
.findByClubIdAndCardinalAndDateRange(clubId, cardinal, start, end)
.map { scheduleMapper.toResponse(it) }

val sessions =
sessionReader
.findAllByClubIdAndStartBetween(clubId, start, end)
.findAllByClubIdAndCardinalAndStartBetween(clubId, cardinal, start, end)
.map { scheduleMapper.toResponse(it) }

return (events + sessions).sortedBy { it.start }
}

fun findDetail(
clubId: Long,
userId: Long,
id: Long,
type: Type,
): ScheduleDetailResponse {
clubMemberPolicy.getActiveMember(clubId, userId)
return when (type) {
Type.EVENT -> {
val event = eventRepository.findByIdOrNull(id) ?: throw EventNotFoundException()
if (event.club.id != clubId) throw EventNotFoundException()
scheduleMapper.toDetailResponse(event)
}

Type.SESSION -> {
val session = sessionReader.getById(id)
if (session.club.id != clubId) throw SessionNotFoundException()
val allAttendances = attendanceReader.findAllBySession(session)
val myAttendance = attendanceReader.findBySessionAndUserId(session, userId)
val myStatus = deriveAttendanceStatus(myAttendance, session)
val attendedAt = myAttendance?.modifiedAt?.takeIf { myStatus == ScheduleAttendanceStatus.COMPLETED }
scheduleMapper.toDetailResponse(session, allAttendances, myStatus, attendedAt)
}
}
}

private fun deriveAttendanceStatus(
attendance: Attendance?,
session: Session,
): ScheduleAttendanceStatus {
val now = LocalDateTime.now()
if (attendance == null) {
return when {
now.isBefore(session.start.minusMinutes(10)) -> ScheduleAttendanceStatus.UPCOMING
session.status == SessionStatus.OPEN && session.isCheckInAllowed(now) -> ScheduleAttendanceStatus.OPEN
else -> ScheduleAttendanceStatus.ABSENT
}
}
return when (attendance.status) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if 문 대신 쓰면 좋을 kotlin 문법입니당 한 번 찾아보시면 좋을 것 같아용

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 감사합니당~~👍🏻

AttendanceStatus.ATTEND -> {
ScheduleAttendanceStatus.COMPLETED
}

AttendanceStatus.ABSENT -> {
ScheduleAttendanceStatus.ABSENT
}

AttendanceStatus.PENDING -> {
val isOpen =
session.status == SessionStatus.OPEN &&
!now.isBefore(session.start) &&
!now.isAfter(session.end)
if (isOpen) ScheduleAttendanceStatus.OPEN else ScheduleAttendanceStatus.UPCOMING
}
}
}

fun findYearly(
clubId: Long,
userId: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,12 @@ interface EventReader {
end: LocalDateTime,
): List<Event>

fun findByClubIdAndCardinalAndDateRange(
clubId: Long,
cardinal: Int,
start: LocalDateTime,
end: LocalDateTime,
): List<Event>

fun findAllByCardinal(cardinal: Int): List<Event>
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,22 @@ interface EventRepository :
@Param("end") end: LocalDateTime,
@Param("start") start: LocalDateTime,
): List<Event>

@Query(
"SELECT e FROM Event e WHERE e.club.id = :clubId AND e.cardinal = :cardinal AND e.start <= :end AND e.end >= :start ORDER BY e.start ASC",
)
fun findByClubIdAndCardinalAndStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(
@Param("clubId") clubId: Long,
@Param("cardinal") cardinal: Int,
@Param("end") end: LocalDateTime,
@Param("start") start: LocalDateTime,
): List<Event>

override fun findByClubIdAndCardinalAndDateRange(
clubId: Long,
cardinal: Int,
start: LocalDateTime,
end: LocalDateTime,
): List<Event> =
findByClubIdAndCardinalAndStartLessThanEqualAndEndGreaterThanEqualOrderByStartAsc(clubId, cardinal, end, start)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.weeth.domain.schedule.presentation

import com.weeth.domain.schedule.application.dto.response.ScheduleDetailResponse
import com.weeth.domain.schedule.application.dto.response.ScheduleResponse
import com.weeth.domain.schedule.application.usecase.query.GetScheduleQueryService
import com.weeth.domain.schedule.domain.enums.Type
import com.weeth.global.auth.annotation.CurrentUser
import com.weeth.global.common.response.CommonResponse
import com.weeth.global.common.web.TsidParam
Expand All @@ -11,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.format.annotation.DateTimeFormat
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
Expand All @@ -28,16 +31,32 @@ class ScheduleController(
@TsidParam
@TsidPathVariable clubId: Long,
@Parameter(hidden = true) @CurrentUser userId: Long,
@RequestParam cardinal: Int,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) start: LocalDateTime,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) end: LocalDateTime,
): CommonResponse<List<ScheduleResponse>> =
CommonResponse.success(
ScheduleResponseCode.SCHEDULE_MONTHLY_FIND_SUCCESS,
getScheduleQueryService.findMonthly(clubId, userId, start, end),
getScheduleQueryService.findMonthly(clubId, userId, cardinal, start, end),
)

@GetMapping("/{id}")
@Operation(summary = "일정 상세 조회")
fun findDetail(
@TsidParam
@TsidPathVariable clubId: Long,
@Parameter(hidden = true) @CurrentUser userId: Long,
@PathVariable id: Long,
@RequestParam type: Type,
): CommonResponse<ScheduleDetailResponse> =
CommonResponse.success(
ScheduleResponseCode.SCHEDULE_DETAIL_FIND_SUCCESS,
getScheduleQueryService.findDetail(clubId, userId, id, type),
)

@Deprecated("사용하지 않는 API")
@GetMapping("/yearly")
@Operation(summary = "연도별 일정 조회")
@Operation(summary = "연도별 일정 조회", deprecated = true)
fun findByYearly(
@TsidParam
@TsidPathVariable clubId: Long,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ enum class ScheduleResponseCode(
EVENT_FIND_SUCCESS(10803, HttpStatus.OK, "일정이 성공적으로 조회되었습니다."),
SCHEDULE_MONTHLY_FIND_SUCCESS(10804, HttpStatus.OK, "월별 일정이 성공적으로 조회되었습니다."),
SCHEDULE_YEARLY_FIND_SUCCESS(10805, HttpStatus.OK, "연도별 일정이 성공적으로 조회되었습니다."),
SCHEDULE_DETAIL_FIND_SUCCESS(10806, HttpStatus.OK, "일정 상세가 성공적으로 조회되었습니다."),
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ interface SessionReader {
end: LocalDateTime,
): List<Session>

fun findAllByClubIdAndCardinalAndStartBetween(
clubId: Long,
cardinal: Int,
start: LocalDateTime,
end: LocalDateTime,
): List<Session>

fun findAllByClubIdAndCardinalIn(
clubId: Long,
cardinals: List<Int>,
Expand Down
Loading