Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
91 changes: 56 additions & 35 deletions scripts/sql/mail-reservation-queries.sql
Original file line number Diff line number Diff line change
@@ -1,45 +1,66 @@
-- 메일 예약 DB 확인용 SQL (mail_reservation / mail 테이블)

-- 1. 예약 전체 목록 (예약 시각 기준 최신순)
SELECT
r.id AS reservation_id,
r.mail_id,
r.status AS reservation_status,
r.reservation_time AS reservation_time,
m.id AS mail_id,
m.sender_email_address,
m.mail_subject,
m.mail_body,
m.body_format,
GROUP_CONCAT(
CASE
WHEN ra.type = 'TO' THEN ra.email_address
END
ORDER BY ra.id SEPARATOR ','
) AS to_addresses,
GROUP_CONCAT(
CASE
WHEN ra.type = 'CC' THEN ra.email_address
END
ORDER BY ra.id SEPARATOR ','
) AS cc_addresses,
GROUP_CONCAT(
CASE
WHEN ra.type = 'BCC' THEN ra.email_address
END
ORDER BY ra.id SEPARATOR ','
) AS bcc_addresses
FROM
mail_reservation r
JOIN mail m ON r.mail_id = m.id
LEFT JOIN mail_recipient_address ra ON ra.mail_id = m.id
WHERE
m.sender_email_address = 'glen.urssu@gmail.com'
-- 예약(미발송)만 보고 싶으면 아래 주석 해제
AND r.status IN ('SCHEDULED', 'PENDING_SEND')
GROUP BY
r.id,
r.status,
r.reservation_time,
m.id,
m.sender_email_address,
m.mail_subject,
m.mail_body,
m.body_format
FROM mail_reservation r
LEFT JOIN mail m ON m.id = r.mail_id
ORDER BY r.reservation_time DESC;
ORDER BY r.reservation_time;\G

-- 2. 예약 건수만 확인
SELECT COUNT(*) AS reservation_count FROM mail_reservation;

-- 3. 아직 발송 시점이 안 된 예약 (미래 시각) — 스케줄러가 아직 안 건드림
SELECT
r.id,
r.mail_id,
r.reservation_time,
m.mail_subject
FROM mail_reservation r
LEFT JOIN mail m ON m.id = r.mail_id
WHERE r.reservation_time > NOW()
ORDER BY r.reservation_time;

-- 4. 발송 대상이어야 하는데 남아 있는 예약 (과거 시각) — 스케줄러가 처리 못 한 것
SELECT
r.id,
r.mail_id,
r.reservation_time,
r.id AS reservation_id,
r.status AS reservation_status,
r.reservation_time AS reservation_time,
m.id AS mail_id,
m.sender_email_address,
m.mail_subject,
m.sender_email_address
FROM mail_reservation r
LEFT JOIN mail m ON m.id = r.mail_id
WHERE r.reservation_time <= NOW()
ORDER BY r.reservation_time;

-- 5. 예약은 있는데 메일이 삭제된 경우 (스케줄러에서 warn 나는 경우)
SELECT r.id, r.mail_id, r.reservation_time
FROM mail_reservation r
LEFT JOIN mail m ON m.id = r.mail_id
WHERE m.id IS NULL;
m.mail_body,
m.body_format,
ra.type AS recipient_type, -- TO / CC / BCC
ra.email_address AS recipient_address
FROM
mail_reservation r
JOIN mail m ON r.mail_id = m.id
LEFT JOIN mail_recipient_address ra ON ra.mail_id = m.id
WHERE
m.sender_email_address = 'glen.urssu@gmail.com'
AND r.status IN ('SCHEDULED', 'PENDING_SEND') -- 필요 시 예약건만
ORDER BY r.reservation_time, r.id, ra.type, ra.id;
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ class FormResponseToApplicantProcessor(
applicationDateTime = userResponse.createTime,
applicationSemester = applicationSemester,
academicSemester = userResponse.getAnswer(question.academicSemesterQuestion) ?: "",
availableTimes = availableTimeParser.parse(userResponse.getAll(question.availableTimeQuestion))
availableTimes = availableTimeParser.parse(
responseItems = userResponse.responseItems,
availableTimeQuestion = question.availableTimeQuestion,
),
)

return ApplicantSyncInfo(applicant, formId, userResponse.responseId)
Expand Down Expand Up @@ -88,7 +91,10 @@ class FormResponseToApplicantProcessor(
applicationDateTime = userResponse.createTime,
applicationSemester = applicantSyncMapping.applicationSemester,
academicSemester = userResponse.getAnswer(applicantSyncMapping.academicSemesterQuestion) ?: "",
availableTimes = availableTimeParser.parse(userResponse.getAll(applicantSyncMapping.availableTimeQuestion)),
availableTimes = availableTimeParser.parse(
responseItems = userResponse.responseItems,
availableTimeQuestion = applicantSyncMapping.availableTimeQuestion,
),
)

return ApplicantSyncInfo(applicant, applicantSyncMapping.formId, userResponse.responseId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ data class MailReservationDetailResponse(
val reservationTime: Instant,
@Schema(description = "예약 상태", example = "SCHEDULED", allowableValues = ["SCHEDULED", "PENDING_SEND", "SENT"])
val status: MailReservationStatus,
@Schema(description = "발신자 이메일", example = "sender@example.com")
val senderEmailAddress: String,
val mailSubject: String,
val mailBody: String,
val bodyFormat: String,
Expand All @@ -26,6 +28,7 @@ data class MailReservationDetailResponse(
mailId = detail.mailId,
reservationTime = detail.reservationTime,
status = detail.status,
senderEmailAddress = detail.senderEmailAddress,
mailSubject = detail.mailSubject,
mailBody = detail.mailBody,
bodyFormat = detail.bodyFormat.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ data class MailReservationListItem(
val reservationTime: Instant,
@Schema(description = "예약 상태", example = "SCHEDULED", allowableValues = ["SCHEDULED", "PENDING_SEND", "SENT"])
val status: MailReservationStatus,
@Schema(description = "발신자 이메일", example = "sender@example.com")
val senderEmailAddress: String,
val mailSubject: String,
@Schema(description = "대표 수신자 이메일 (수신자가 없으면 null)", example = "receiver@example.com", nullable = true)
val primaryReceiverEmailAddress: String?,
Expand All @@ -30,6 +32,7 @@ data class MailReservationListResponse(
mailId = detail.mailId,
reservationTime = detail.reservationTime,
status = detail.status,
senderEmailAddress = detail.senderEmailAddress,
mailSubject = detail.mailSubject,
primaryReceiverEmailAddress = detail.receiverEmailAddresses.firstOrNull(),
hasAttachments = detail.hasAttachments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.yourssu.scouter.common.implement.domain.mail.MailWriter
import com.yourssu.scouter.common.implement.domain.user.UserReader
import com.yourssu.scouter.common.implement.support.exception.MailReservationAlreadyProcessedException
import com.yourssu.scouter.common.implement.support.exception.MailReservationNotYetDueException
import com.yourssu.scouter.hrms.business.domain.member.MemberPrivacyService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.Instant
Expand All @@ -33,6 +34,7 @@ class MailService(
private val mailRepository: MailRepository,
private val oauth2Service: OAuth2Service,
private val mailSender: MailSender,
private val memberPrivacyService: MemberPrivacyService,
) {
companion object {
private val log = LoggerFactory.getLogger(MailService::class.java)
Expand Down Expand Up @@ -65,8 +67,13 @@ class MailService(
fun getPendingReservationStatuses(userId: Long): List<PendingMailReservationStatus> {
val user = userReader.readById(userId)
val now = Instant.now()
val senderEmail = user.getEmailAddress()
val reservations = mailReservationReader.readAllBeforeBySenderEmail(now, senderEmail)
val reservations =
if (memberPrivacyService.isPrivilegedUser(userId)) {
mailReservationReader.readAllBefore(now)
} else {
val senderEmails = memberPrivacyService.getActiveTeamMemberEmails(userId).toList()
mailReservationReader.readAllBeforeBySenderEmails(now, senderEmails)
}
return reservations.map { reservation ->
val mail = mailRepository.findById(reservation.mailId) ?: run {
log.warn("예약의 메일을 찾을 수 없음: reservationId={}, mailId={}", reservation.id, reservation.mailId)
Expand Down Expand Up @@ -195,9 +202,13 @@ class MailService(
}

fun getUserMailReservations(userId: Long): List<MailReservationDetail> {
val user = userReader.readById(userId)
val senderEmail = user.getEmailAddress()
val reservations = mailReservationReader.readAllBySenderEmail(senderEmail)
val reservations =
if (memberPrivacyService.isPrivilegedUser(userId)) {
mailReservationReader.readAll()
} else {
val senderEmails = memberPrivacyService.getActiveTeamMemberEmails(userId).toList()
mailReservationReader.readAllBySenderEmails(senderEmails)
}
return reservations.map { reservation ->
val mail =
mailRepository.findById(reservation.mailId)
Expand All @@ -212,7 +223,6 @@ class MailService(
userId: Long,
reservationId: Long,
): MailReservationDetail {
val user = userReader.readById(userId)
val reservation =
mailReservationReader.readById(reservationId)
?: throw MailReservationNotFoundException("예약을 찾을 수 없습니다. reservationId=$reservationId")
Expand All @@ -221,8 +231,10 @@ class MailService(
mailRepository.findById(reservation.mailId)
?: throw MailReservationNotFoundException("예약 메일을 찾을 수 없습니다. reservationId=$reservationId, mailId=${reservation.mailId}")

val senderEmail = user.getEmailAddress()
if (mail.senderEmailAddress != senderEmail) {
val canAccess =
memberPrivacyService.isPrivilegedUser(userId) ||
memberPrivacyService.getActiveTeamMemberEmails(userId).contains(mail.senderEmailAddress)
if (!canAccess) {
throw MailReservationAccessDeniedException("예약에 접근할 수 없습니다. reservationId=$reservationId")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class MailReservationReader(
private val mailReservationRepository: MailReservationRepository,
) {

fun readAll(): List<MailReservation> {
return mailReservationRepository.findAll()
}

fun readAllBefore(time: Instant): List<MailReservation> {
return mailReservationRepository.findAllByReservationTimeLessThanEqual(time)
}
Expand All @@ -23,10 +27,18 @@ class MailReservationReader(
return mailReservationRepository.findAllByReservationTimeLessThanEqualAndSenderEmail(time, senderEmail)
}

fun readAllBeforeBySenderEmails(time: Instant, senderEmails: List<String>): List<MailReservation> {
return mailReservationRepository.findAllByReservationTimeLessThanEqualAndSenderEmails(time, senderEmails)
}

fun readAllBySenderEmail(senderEmail: String): List<MailReservation> {
return mailReservationRepository.findAllBySenderEmail(senderEmail)
}

fun readAllBySenderEmails(senderEmails: List<String>): List<MailReservation> {
return mailReservationRepository.findAllBySenderEmails(senderEmails)
}

fun readAllBySenderEmailAndReservationTimeBetween(
senderEmail: String,
from: Instant,
Expand All @@ -35,6 +47,14 @@ class MailReservationReader(
return mailReservationRepository.findAllBySenderEmailAndReservationTimeBetween(senderEmail, from, to)
}

fun readAllBySenderEmailsAndReservationTimeBetween(
senderEmails: List<String>,
from: Instant,
to: Instant,
): List<MailReservation> {
return mailReservationRepository.findAllBySenderEmailsAndReservationTimeBetween(senderEmails, from, to)
}

fun readById(id: Long): MailReservation? {
return mailReservationRepository.findById(id)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface MailReservationRepository {

fun save(mailReservation: MailReservation): MailReservation

fun findAll(): List<MailReservation>

fun findAllByReservationTimeLessThanEqual(reservationTime: Instant): List<MailReservation>

fun findAllByReservationTimeLessThanEqualAndStatusNot(
Expand All @@ -15,14 +17,24 @@ interface MailReservationRepository {

fun findAllByReservationTimeLessThanEqualAndSenderEmail(time: Instant, senderEmail: String): List<MailReservation>

fun findAllByReservationTimeLessThanEqualAndSenderEmails(time: Instant, senderEmails: List<String>): List<MailReservation>

fun findAllBySenderEmail(senderEmail: String): List<MailReservation>

fun findAllBySenderEmails(senderEmails: List<String>): List<MailReservation>

fun findAllBySenderEmailAndReservationTimeBetween(
senderEmail: String,
from: Instant,
to: Instant,
): List<MailReservation>

fun findAllBySenderEmailsAndReservationTimeBetween(
senderEmails: List<String>,
from: Instant,
to: Instant,
): List<MailReservation>

fun findById(id: Long): MailReservation?

fun deleteById(id: Long)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ interface JpaMailReservationRepository : JpaRepository<MailReservationEntity, Lo
@Param("senderEmail") senderEmail: String,
): List<MailReservationEntity>

@Query(
"SELECT r FROM MailReservationEntity r, MailEntity m WHERE r.mailId = m.id " +
"AND r.reservationTime <= :time AND m.senderEmailAddress IN :senderEmails",
)
fun findAllByReservationTimeLessThanEqualAndSenderEmails(
@Param("time") time: Instant,
@Param("senderEmails") senderEmails: List<String>,
): List<MailReservationEntity>

@Query(
"SELECT r FROM MailReservationEntity r, MailEntity m WHERE r.mailId = m.id " +
"AND m.senderEmailAddress = :senderEmail",
Expand All @@ -32,6 +41,14 @@ interface JpaMailReservationRepository : JpaRepository<MailReservationEntity, Lo
@Param("senderEmail") senderEmail: String,
): List<MailReservationEntity>

@Query(
"SELECT r FROM MailReservationEntity r, MailEntity m WHERE r.mailId = m.id " +
"AND m.senderEmailAddress IN :senderEmails",
)
fun findAllBySenderEmails(
@Param("senderEmails") senderEmails: List<String>,
): List<MailReservationEntity>

@Query(
"SELECT r FROM MailReservationEntity r, MailEntity m WHERE r.mailId = m.id " +
"AND m.senderEmailAddress = :senderEmail " +
Expand All @@ -42,4 +59,15 @@ interface JpaMailReservationRepository : JpaRepository<MailReservationEntity, Lo
@Param("from") from: Instant,
@Param("to") to: Instant,
): List<MailReservationEntity>

@Query(
"SELECT r FROM MailReservationEntity r, MailEntity m WHERE r.mailId = m.id " +
"AND m.senderEmailAddress IN :senderEmails " +
"AND r.reservationTime BETWEEN :from AND :to",
)
fun findAllBySenderEmailsAndReservationTimeBetween(
@Param("senderEmails") senderEmails: List<String>,
@Param("from") from: Instant,
@Param("to") to: Instant,
): List<MailReservationEntity>
}
Loading