Skip to content
Merged

Dev #67

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4bae54e
Merge pull request #50 from Team-Saion/feature/SAIO-258
lso5507 Jul 18, 2026
371128d
Merge pull request #52 from Team-Saion/feature/SAIO-260
lso5507 Jul 18, 2026
ac3e072
fix[SAIO-164]: dev/main에서 사라진 일정-홈 연동 변경사항 복구
ggamnunq Jul 18, 2026
959366f
Merge pull request #54 from Team-Saion/feature/SAIO-164-restore
ggamnunq Jul 18, 2026
d3d290f
fix[SAIO-164]: 홈 써클 멤버 화면 me -> isMe
ggamnunq Jul 18, 2026
e7dc612
Merge pull request #55 from Team-Saion/feature/SAIO-164-restore
ggamnunq Jul 18, 2026
78c42ec
Merge pull request #56 from Team-Saion/feature/SAIO-262
lso5507 Jul 21, 2026
27c643f
Merge pull request #58 from Team-Saion/feature/SAIO-263
lso5507 Jul 22, 2026
99840bf
Merge pull request #60 from Team-Saion/feature/SAIO-264
lso5507 Jul 22, 2026
961e46f
feat[SAIO-265]: 일정 생성 알림 연동
ggamnunq Jul 23, 2026
2fc31d3
Merge pull request #62 from Team-Saion/feature/SAIO-267
lso5507 Jul 23, 2026
b0d5255
feat[SAIO-256]: 일정 D-7 / D-1 / D-day 리마인드 알림 연동
ggamnunq Jul 23, 2026
c79c76f
Merge pull request #64 from Team-Saion/feature/SAIO-268
lso5507 Jul 23, 2026
6566b16
feat[SAIO-265]: 일정 확인 알림 연동
ggamnunq Jul 23, 2026
6f38c39
test[SAIO-256]: 일정 알림 연동에 맞춰 일정 컨트롤러 테스트 fixture 보완
ggamnunq Jul 23, 2026
11367a2
Merge branch 'main' into feature/SAIO-265
ggamnunq Jul 23, 2026
bc9b2f2
fix[SAIO-265]: 일정 알림 migration 버전 충돌 해소
ggamnunq Jul 23, 2026
a63dbb4
Merge branch 'dev' into feature/SAIO-265
ggamnunq Jul 23, 2026
31cf356
Merge pull request #66 from Team-Saion/feature/SAIO-265
ggamnunq Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.unicorn.server.domain.home.port.dto

import com.fasterxml.jackson.annotation.JsonProperty
import com.unicorn.server.domain.member.enums.AvatarColor

data class HomeMemberDto(
val memberId: String,
val nickname: String,
val avatarColor: AvatarColor,
val profileImageKey: String?,
@get:JsonProperty("isMe")
val isMe: Boolean,
val role: String,
)
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.unicorn.server.domain.home.port.dto

import com.unicorn.server.domain.schedule.port.dto.ScheduleView
import com.unicorn.server.domain.schedule.port.dto.ScheduleSummaryResult

data class HomeView(
val circle: HomeCircleDto,
val members: List<HomeMemberDto>,
val canInvite: Boolean,
val mainSchedule: ScheduleView?,
val schedules: List<ScheduleView>,
val mainSchedule: ScheduleSummaryResult?,
val schedules: List<ScheduleSummaryResult>,
val totalScheduleCount: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ class HomeQueryService(
val visibleMembers = getVisibleMembers(circleId, requesterId)

val today = LocalDate.now()
val circleIdVo = CircleId.of(circleId)
// 메인(가장 임박한 1건) + 나머지 최대 3건을 한 번의 조회로 가져와 분리한다.
val upcomingSchedules = scheduleQueryInPort.findUpcomingSchedulesByCircleId(circleIdVo, today, MAIN_AND_UPCOMING_LIMIT)
return HomeView(
circle = HomeCircleDto(circle.id, circle.name, circle.ownerId),
members = visibleMembers,
canInvite = visibleMembers.size == 1,
mainSchedule = scheduleQueryInPort.findMainScheduleByCircleId(CircleId.of(circleId), today),
schedules = scheduleQueryInPort.findUpcomingSchedulesByCircleId(CircleId.of(circleId), today, 3),
totalScheduleCount = scheduleQueryInPort.countByCircleId(CircleId.of(circleId)),
mainSchedule = upcomingSchedules.firstOrNull(),
schedules = upcomingSchedules.drop(1),
totalScheduleCount = scheduleQueryInPort.countByCircleId(circleIdVo),
)
}

Expand All @@ -64,4 +67,9 @@ class HomeQueryService(
role = membership.role,
)
}

companion object {
// 메인 일정 1건 + 홈에 노출할 나머지 일정 3건
private const val MAIN_AND_UPCOMING_LIMIT = 4
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.unicorn.server.domain.schedule.enums

enum class ScheduleReminderType {
D7,
D1,
DDAY_ALL_DAY,
DDAY_TIMED,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.unicorn.server.domain.schedule.enums

// 긴급도가 높은 레벨부터 선언한다. maxDayThreshold는 "dDay < maxDayThreshold"를 만족할 때 해당 레벨로 판단하며,
// null이면 상한이 없는 catch-all 레벨을 의미한다. 기준 변경/레벨 추가는 이 enum 수정만으로 반영된다.
enum class UrgencyLevel(val maxDayThreshold: Int?) {
URGENT(10),
NORMAL(null),
;

companion object {
fun from(dDay: Int?): UrgencyLevel {
if (dDay == null) return NORMAL
return entries.firstOrNull { it.maxDayThreshold != null && dDay < it.maxDayThreshold }
?: NORMAL
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.unicorn.server.domain.schedule.event

import com.unicorn.server.common.domain.Event

class ScheduleConfirmationRequestDueEvent(
val scheduleId: String,
val circleId: String,
val scheduleCreatorMemberId: String,
val scheduleTitle: String,
) : Event()
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.unicorn.server.domain.schedule.event

import com.unicorn.server.common.domain.Event

class ScheduleConfirmedEvent(
val scheduleId: String,
val circleId: String,
val scheduleCreatorMemberId: String,
val confirmerMemberId: String,
val scheduleTitle: String,
) : Event()
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.unicorn.server.domain.schedule.event

import com.unicorn.server.common.domain.Event

class ScheduleCreatedEvent(
val scheduleId: String,
val circleId: String,
val creatorMemberId: String,
val scheduleTitle: String,
) : Event()
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.unicorn.server.domain.schedule.event

import com.unicorn.server.common.domain.Event

class ScheduleDeletedEvent(
val scheduleId: String,
val circleId: String,
val deletedByMemberId: String,
val scheduleTitle: String,
) : Event()
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.unicorn.server.domain.schedule.event

import com.unicorn.server.common.domain.Event
import com.unicorn.server.domain.schedule.enums.ScheduleReminderType
import java.time.LocalTime

class ScheduleReminderDueEvent(
val reminderType: ScheduleReminderType,
val scheduleId: String,
val circleId: String,
val scheduleTitle: String,
val startTime: LocalTime?,
) : Event()
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.unicorn.server.domain.schedule.port.dto

import com.unicorn.server.domain.schedule.enums.ScheduleStatus
import com.unicorn.server.domain.schedule.enums.UrgencyLevel
import com.unicorn.server.domain.schedule.vo.ScheduleId
import java.time.LocalDate
import java.time.LocalDateTime
Expand All @@ -18,6 +19,7 @@ data class ScheduleDetailResult(
val memo: String?,
val status: ScheduleStatus,
val dDay: Int?,
val urgencyLevel: UrgencyLevel,
val progressRate: Int,
val confirmations: List<ConfirmationCountResult>,
val myConfirmation: MyConfirmationInfo?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.unicorn.server.domain.schedule.port.dto

import com.unicorn.server.domain.schedule.enums.ScheduleStatus
import com.unicorn.server.domain.schedule.enums.UrgencyLevel
import com.unicorn.server.domain.schedule.vo.ScheduleId
import java.time.LocalDate
import java.time.LocalTime
Expand All @@ -16,5 +17,6 @@ data class ScheduleSummaryResult(
val needConfirm: Boolean,
val status: ScheduleStatus,
val dDay: Int?,
val urgencyLevel: UrgencyLevel,
val progressRate: Int,
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.unicorn.server.domain.schedule.port.`in`

import com.unicorn.server.domain.circle.vo.CircleId
import com.unicorn.server.domain.schedule.port.dto.ScheduleView
import com.unicorn.server.domain.schedule.port.dto.ScheduleSummaryResult
import java.time.LocalDate

interface GetSchedulesForCircleInPort {
fun findMainScheduleByCircleId(circleId: CircleId, today: LocalDate): ScheduleView?
fun findUpcomingSchedulesByCircleId(circleId: CircleId, today: LocalDate, limit: Int): List<ScheduleView>
// 아직 종료되지 않은(예정 + 진행 중) 일정을 임박한 순서로 최대 limit개 반환한다. 첫 항목이 가장 임박한 일정이다.
fun findUpcomingSchedulesByCircleId(circleId: CircleId, today: LocalDate, limit: Int): List<ScheduleSummaryResult>
fun countByCircleId(circleId: CircleId): Long
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.unicorn.server.domain.schedule.port.`in`

import java.time.LocalDateTime

interface ScheduleConfirmationRequestInPort {
fun dispatchDue(now: LocalDateTime)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.unicorn.server.domain.schedule.port.`in`

import com.unicorn.server.domain.schedule.vo.ScheduleId

interface ScheduleConfirmationStatusInPort {
fun hasConfirmed(scheduleId: ScheduleId, memberId: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.unicorn.server.domain.schedule.port.`in`

import java.time.LocalDateTime

interface ScheduleReminderInPort {
fun dispatchDaily(now: LocalDateTime)

fun dispatchTimed(now: LocalDateTime)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.unicorn.server.domain.schedule.port.out
import com.unicorn.server.domain.schedule.Schedule
import com.unicorn.server.domain.schedule.port.dto.SchedulePageCursor
import com.unicorn.server.domain.schedule.vo.ScheduleId
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime

interface ScheduleOutPort {
fun save(schedule: Schedule): Schedule
Expand All @@ -13,7 +16,37 @@ interface ScheduleOutPort {

fun findActiveByCircleId(
circleId: String,
today: LocalDate,
cursor: SchedulePageCursor?,
size: Int,
): List<Schedule>

fun findActiveByStartDateAndCreatedBefore(
startDate: LocalDate,
createdBefore: LocalDateTime,
): List<Schedule>

fun findActiveAllDayByStartDateAndCreatedBefore(
startDate: LocalDate,
createdBefore: LocalDateTime,
): List<Schedule>

fun findActiveTimedByStartAtAndCreatedBefore(
startDate: LocalDate,
startTime: LocalTime,
createdBefore: LocalDateTime,
): List<Schedule>

fun findActiveConfirmationRequiredCreatedBetween(
createdFrom: LocalDateTime,
createdBefore: LocalDateTime,
): List<Schedule>

fun findUpcomingByCircleId(
circleId: String,
today: LocalDate,
limit: Int,
): List<Schedule>

fun countActiveByCircleId(circleId: String): Long
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.unicorn.server.domain.schedule.service

import com.unicorn.server.common.exception.BusinessException
import com.unicorn.server.common.port.out.event.EventPublisher
import com.unicorn.server.domain.schedule.Schedule
import com.unicorn.server.domain.schedule.event.ScheduleCreatedEvent
import com.unicorn.server.domain.schedule.event.ScheduleDeletedEvent
import com.unicorn.server.domain.schedule.exception.ScheduleErrorCode
import com.unicorn.server.domain.schedule.port.`in`.CreateScheduleInPort
import com.unicorn.server.domain.schedule.port.`in`.DeleteScheduleInPort
Expand All @@ -23,6 +26,7 @@ class ScheduleCommandService(
private val scheduleConfirmationOutPort: ScheduleConfirmationOutPort,
private val circleAccessOutPort: CircleAccessOutPort,
private val scheduleIdGenerator: ScheduleIdGenerator,
private val eventPublisher: EventPublisher,
) : CreateScheduleInPort, UpdateScheduleInPort, DeleteScheduleInPort {

override fun create(command: CreateScheduleCommand): ScheduleId {
Expand All @@ -47,7 +51,16 @@ class ScheduleCommandService(
createdBy = command.memberId,
)

return scheduleOutPort.save(schedule).id
val savedSchedule = scheduleOutPort.save(schedule)
eventPublisher.publish(
ScheduleCreatedEvent(
scheduleId = savedSchedule.id.value,
circleId = savedSchedule.circleId,
creatorMemberId = savedSchedule.createdBy,
scheduleTitle = savedSchedule.title,
),
)
return savedSchedule.id
}

override fun update(command: UpdateScheduleCommand) {
Expand Down Expand Up @@ -97,6 +110,14 @@ class ScheduleCommandService(
schedule.delete(memberId)
scheduleOutPort.save(schedule)
scheduleConfirmationOutPort.deleteAllByScheduleId(scheduleId)
eventPublisher.publish(
ScheduleDeletedEvent(
scheduleId = schedule.id.value,
circleId = schedule.circleId,
deletedByMemberId = memberId,
scheduleTitle = schedule.title,
),
)
}

private fun canModify(createdBy: String, circleId: String, memberId: String): Boolean =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.unicorn.server.domain.schedule.service

import com.unicorn.server.common.port.out.event.EventPublisher
import com.unicorn.server.domain.schedule.Schedule
import com.unicorn.server.domain.schedule.event.ScheduleConfirmationRequestDueEvent
import com.unicorn.server.domain.schedule.port.`in`.ScheduleConfirmationRequestInPort
import com.unicorn.server.domain.schedule.port.out.ScheduleOutPort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.LocalTime

@Service
@Transactional
class ScheduleConfirmationRequestService(
private val scheduleOutPort: ScheduleOutPort,
private val eventPublisher: EventPublisher,
) : ScheduleConfirmationRequestInPort {

override fun dispatchDue(now: LocalDateTime) {
val windowEnd = now.withSecond(0).withNano(0).minusHours(24)
val windowStart = windowEnd.minusMinutes(1)
scheduleOutPort.findActiveConfirmationRequiredCreatedBetween(windowStart, windowEnd)
.filter { it.startsAt().isAfterOrEqual(it.createdAt.plusHours(24)) }
.forEach { schedule ->
eventPublisher.publish(
ScheduleConfirmationRequestDueEvent(
scheduleId = schedule.id.value,
circleId = schedule.circleId,
scheduleCreatorMemberId = schedule.createdBy,
scheduleTitle = schedule.title,
),
)
}
}

private fun Schedule.startsAt(): LocalDateTime =
LocalDateTime.of(startDate, startTime ?: LocalTime.MIDNIGHT)

private fun LocalDateTime.isAfterOrEqual(other: LocalDateTime): Boolean =
isAfter(other) || isEqual(other)
}
Loading
Loading