feat(banner): 관리자 예약 기간 관리와 입력 검증을 추가한다

This commit is contained in:
2026-10-27 20:48:03 +09:00
parent 9a4607449c
commit 98e68fc994
6 changed files with 443 additions and 8 deletions
@@ -46,7 +46,9 @@ class AdminContentBannerQueryRepositoryImpl(
audioContentBanner.series.id,
audioContentBanner.series.title,
audioContentBanner.link,
audioContentBanner.isAdult
audioContentBanner.isAdult,
audioContentBanner.startDate,
audioContentBanner.endDate
)
)
.from(audioContentBanner)
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository
import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository
@@ -15,6 +16,11 @@ import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.time.format.ResolverStyle
@Service
class AdminContentBannerService(
@@ -31,7 +37,12 @@ class AdminContentBannerService(
) {
@Transactional
fun createAudioContentMainBanner(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateContentBannerRequest::class.java)
val fields = objectMapper.readTree(requestString)
validateReservationTypes(fields)
val request = objectMapper.treeToValue(fields, CreateContentBannerRequest::class.java)
val startDate = parseReservationDate(request.startDate)
val endDate = parseReservationDate(request.endDate)?.withSecond(59)
validateReservationRange(startDate, endDate)
if (request.type == AudioContentBannerType.CREATOR && request.creatorId == null) {
throw SodaException(messageKey = "admin.content.banner.creator_required")
}
@@ -79,6 +90,8 @@ class AdminContentBannerService(
audioContentBanner.creator = creator
audioContentBanner.series = series
audioContentBanner.tab = tab
audioContentBanner.startDate = startDate
audioContentBanner.endDate = endDate
repository.save(audioContentBanner)
val fileName = generateFileName()
@@ -92,10 +105,20 @@ class AdminContentBannerService(
@Transactional
fun updateAudioContentMainBanner(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateContentBannerRequest::class.java)
val fields = objectMapper.readTree(requestString)
validateReservationTypes(fields)
val request = objectMapper.treeToValue(fields, UpdateContentBannerRequest::class.java)
val audioContentBanner = repository.findByIdOrNull(request.id)
?: throw SodaException(messageKey = "common.error.invalid_request")
val startDate = if (fields.has("startDate")) parseReservationDate(request.startDate) else audioContentBanner.startDate
val endDate = if (fields.has("endDate")) {
parseReservationDate(request.endDate)?.withSecond(59)
} else {
audioContentBanner.endDate
}
validateReservationRange(startDate, endDate)
if (image != null) {
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
@@ -106,6 +129,9 @@ class AdminContentBannerService(
audioContentBanner.thumbnailImage = imagePath
}
audioContentBanner.startDate = startDate
audioContentBanner.endDate = endDate
if (request.isAdult != null) {
audioContentBanner.isAdult = request.isAdult
}
@@ -185,4 +211,32 @@ class AdminContentBannerService(
fun getAudioContentMainBannerList(tabId: Long?): List<GetAdminContentBannerResponse> {
return repository.getAudioContentMainBannerList(tabId = tabId ?: 1)
}
private fun validateReservationTypes(fields: JsonNode) {
if (listOf(fields["startDate"], fields["endDate"]).any { it != null && !it.isNull && !it.isTextual }) {
throw SodaException(messageKey = "common.error.invalid_request")
}
}
private fun parseReservationDate(value: String?): LocalDateTime? {
if (value == null) return null
if (value.length != 16) throw SodaException(messageKey = "common.error.invalid_request")
return try {
LocalDateTime.parse(
value,
DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm").withResolverStyle(ResolverStyle.STRICT)
)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
} catch (_: DateTimeParseException) {
throw SodaException(messageKey = "common.error.invalid_request")
}
}
private fun validateReservationRange(startDate: LocalDateTime?, endDate: LocalDateTime?) {
if (startDate != null && endDate != null && startDate > endDate) {
throw SodaException(messageKey = "common.error.invalid_request")
}
}
}
@@ -11,5 +11,7 @@ data class CreateContentBannerRequest(
val creatorId: Long?,
val seriesId: Long?,
val link: String?,
val isAdult: Boolean
val isAdult: Boolean,
val startDate: String? = null,
val endDate: String? = null
)
@@ -2,8 +2,11 @@ package kr.co.vividnext.sodalive.admin.content.banner
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
data class GetAdminContentBannerResponse @QueryProjection constructor(
data class GetAdminContentBannerResponse(
val id: Long,
val tabId: Long?,
val type: AudioContentBannerType,
@@ -15,5 +18,35 @@ data class GetAdminContentBannerResponse @QueryProjection constructor(
val seriesId: Long?,
val seriesTitle: String?,
val link: String?,
val isAdult: Boolean
)
val isAdult: Boolean,
val startDate: String? = null,
val endDate: String? = null
) {
@QueryProjection
constructor(
id: Long,
tabId: Long?,
type: AudioContentBannerType,
thumbnailImageUrl: String,
eventId: Long?,
eventThumbnailImage: String?,
creatorId: Long?,
creatorNickname: String?,
seriesId: Long?,
seriesTitle: String?,
link: String?,
isAdult: Boolean,
startDate: LocalDateTime?,
endDate: LocalDateTime?
) : this(
id, tabId, type, thumbnailImageUrl, eventId, eventThumbnailImage, creatorId, creatorNickname,
seriesId, seriesTitle, link, isAdult, formatDate(startDate), formatDate(endDate)
)
companion object {
private fun formatDate(value: LocalDateTime?): String? = value
?.atZone(ZoneId.of("UTC"))
?.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
?.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
}
}
@@ -11,5 +11,7 @@ data class UpdateContentBannerRequest(
val seriesId: Long?,
val link: String?,
val isAdult: Boolean?,
val isActive: Boolean?
val isActive: Boolean?,
val startDate: String? = null,
val endDate: String? = null
)