package kr.co.vividnext.sodalive.notice import kr.co.vividnext.sodalive.common.SodaException import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime import java.time.ZoneId import java.time.ZoneOffset import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException import java.time.format.ResolverStyle import java.time.temporal.ChronoUnit @Service @Transactional(readOnly = true) class ServiceNoticeService( private val repository: ServiceServiceNoticeRepository, private val nowProvider: () -> LocalDateTime = { LocalDateTime.now(ZoneOffset.UTC) } ) { @Transactional fun save(request: CreateNoticeRequest): Long { if (request.title.isBlank()) throw SodaException(messageKey = "notice.error.title_required") if (request.content.isBlank()) throw SodaException(messageKey = "notice.error.content_required") val publishAt = request.publishAt?.let { parsePublishAt(it, nowProvider()) } val notice = request.toEntity(publishAt) return repository.save(notice).id!! } @Transactional fun update(request: UpdateNoticeRequest, publishAtPresent: Boolean = false) { if (request.id <= 0) throw SodaException(messageKey = "common.error.invalid_request") if (request.title.isNullOrBlank() && request.content.isNullOrBlank() && !publishAtPresent) { throw SodaException(messageKey = "notice.error.update_required") } val notice = repository.findByIdForUpdate(request.id) ?: throw SodaException(messageKey = "common.error.invalid_request") if (publishAtPresent) { val now = nowProvider() val currentPublishAt = notice.publishAt if (!notice.isActive || !notice.isScheduled || currentPublishAt == null || currentPublishAt <= now) { throw SodaException(messageKey = "common.error.invalid_request") } notice.publishAt = request.publishAt?.let { parsePublishAt(it, now) } ?: now.truncatedTo(ChronoUnit.SECONDS) notice.isScheduled = request.publishAt != null } if (!request.title.isNullOrBlank()) notice.title = request.title if (!request.content.isNullOrBlank()) notice.content = request.content } @Transactional fun delete(id: Long) { if (id <= 0) throw SodaException(messageKey = "common.error.invalid_request") val notice = repository.findByIdForUpdate(id) ?: throw SodaException(messageKey = "common.error.invalid_request") notice.isActive = false } fun getNoticeList(pageable: Pageable, timezone: String): GetNoticeResponse { val now = nowProvider() val totalCount = repository.getNoticeTotalCount(now) val noticeList = repository.getNoticeList(pageable, now) return GetNoticeResponse(totalCount, noticeList) } fun getLatestNotice(): NoticeItem? { val now = nowProvider() return repository.getLatestNotice(now) } private fun parsePublishAt(value: String, now: LocalDateTime): LocalDateTime { if (value.length != 19) throw SodaException(messageKey = "common.error.invalid_request") val utc = try { LocalDateTime.parse(value, PUBLISH_AT_FORMAT) .atZone(ZoneId.of("Asia/Seoul")) .withZoneSameInstant(ZoneOffset.UTC) .toLocalDateTime() } catch (_: DateTimeParseException) { throw SodaException(messageKey = "common.error.invalid_request") } if (utc < MIN_TIMESTAMP || utc > MAX_TIMESTAMP || utc <= now) { throw SodaException(messageKey = "common.error.invalid_request") } return utc } companion object { private val PUBLISH_AT_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss") .withResolverStyle(ResolverStyle.STRICT) private val MIN_TIMESTAMP = LocalDateTime.of(1970, 1, 1, 0, 0, 1) private val MAX_TIMESTAMP = LocalDateTime.of(2038, 1, 19, 3, 14, 7) } }