package roomescape.schedule.business import ScheduleException import com.github.f4b6a3.tsid.TsidFactory import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import roomescape.admin.business.AdminService import roomescape.common.config.next import roomescape.common.dto.AuditInfo import roomescape.common.dto.OperatorInfo import roomescape.schedule.business.domain.ScheduleOverview import roomescape.schedule.exception.ScheduleErrorCode import roomescape.schedule.infrastructure.persistence.ScheduleEntity import roomescape.schedule.infrastructure.persistence.ScheduleEntityFactory import roomescape.schedule.infrastructure.persistence.ScheduleRepository import roomescape.schedule.infrastructure.persistence.ScheduleStatus import roomescape.schedule.web.* import java.time.LocalDate private val log: KLogger = KotlinLogging.logger {} /** * Structure: * - Public: 모두가 접근 가능 * - User: 회원(로그인된 사용자)가 사용 가능 * - All-Admin: 모든 관리자가 사용 가능 * - Store-Admin: 매장 관리자만 사용 가능 * - Other-Service: 다른 서비스에서 호출하는 메서드 * - Common: 공통 메서드 */ @Service class ScheduleService( private val scheduleRepository: ScheduleRepository, private val scheduleValidator: ScheduleValidator, private val tsidFactory: TsidFactory, private val adminService: AdminService ) { // ======================================== // Public (인증 불필요) // ======================================== @Transactional(readOnly = true) fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse { log.info { "[ScheduleService.getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" } val schedules: List = scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date) return schedules.toResponse() .also { log.info { "[ScheduleService.getStoreScheduleByDate] storeId=${storeId}, date=$date 인 ${it.schedules.size}개 일정 조회 완료" } } } // ======================================== // User (회원 로그인 필요) // ======================================== @Transactional fun holdSchedule(id: Long) { val schedule: ScheduleEntity = findOrThrow(id) if (schedule.status == ScheduleStatus.AVAILABLE) { schedule.hold() return } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) } // ======================================== // All-Admin (본사, 매장 모두 사용가능) // ======================================== @Transactional(readOnly = true) fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse { log.info { "[ScheduleService.searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" } val searchDate = date ?: LocalDate.now() val schedules: List = scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, searchDate) .filter { (themeId == null) || (it.themeId == themeId) } .sortedBy { it.time } return schedules.toAdminSummaryListResponse() .also { log.info { "[ScheduleService.searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" } } } @Transactional(readOnly = true) fun findScheduleAudit(id: Long): AuditInfo { log.info { "[ScheduleService.findDetail] 일정 감사 정보 조회 시작: id=$id" } val schedule: ScheduleEntity = findOrThrow(id) val createdBy: OperatorInfo = adminService.findOperatorOrUnknown(schedule.createdBy) val updatedBy: OperatorInfo = adminService.findOperatorOrUnknown(schedule.updatedBy) return AuditInfo(schedule.createdAt, createdBy, schedule.updatedAt, updatedBy) .also { log.info { "[ScheduleService.findDetail] 일정 감사 정보 조회 완료: id=$id" } } } // ======================================== // Store-Admin (매장 관리자 로그인 필요) // ======================================== @Transactional fun createSchedule(storeId: Long, request: ScheduleCreateRequest): ScheduleCreateResponse { log.info { "[ScheduleService.createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" } scheduleValidator.validateCanCreate(storeId, request) val schedule = ScheduleEntityFactory.create( id = tsidFactory.next(), date = request.date, time = request.time, storeId = storeId, themeId = request.themeId ).also { scheduleRepository.save(it) } return ScheduleCreateResponse(schedule.id) .also { log.info { "[ScheduleService.createSchedule] 일정 생성 완료: id=${it.id}" } } } @Transactional fun updateSchedule(id: Long, request: ScheduleUpdateRequest) { log.info { "[ScheduleService.updateSchedule] 일정 수정 시작: id=$id, request=${request}" } if (request.isAllParamsNull()) { log.info { "[ScheduleService.updateSchedule] 일정 변경 사항 없음: id=$id" } return } val schedule: ScheduleEntity = findOrThrow(id).also { scheduleValidator.validateCanUpdate(it, request) } schedule.modifyIfNotNull(request.time, request.status).also { log.info { "[ScheduleService.updateSchedule] 일정 수정 완료: id=$id, request=${request}" } } } @Transactional fun deleteSchedule(id: Long) { log.info { "[ScheduleService.deleteSchedule] 일정 삭제 시작: id=$id" } val schedule: ScheduleEntity = findOrThrow(id).also { scheduleValidator.validateCanDelete(it) } scheduleRepository.delete(schedule).also { log.info { "[ScheduleService.deleteSchedule] 일정 삭제 완료: id=$id" } } } // ======================================== // Other-Service (API 없이 다른 서비스에서 호출) // ======================================== @Transactional(readOnly = true) fun findSummaryById(id: Long): ScheduleSummaryResponse { log.info { "[ScheduleService.findDateTimeById] 일정 개요 조회 시작 : id=$id" } return findOrThrow(id).toSummaryResponse() .also { log.info { "[ScheduleService.findDateTimeById] 일정 개요 조회 완료: id=$id" } } } // ======================================== // Common (공통 메서드) // ======================================== private fun findOrThrow(id: Long): ScheduleEntity { log.info { "[ScheduleService.findOrThrow] 일정 조회 시작: id=$id" } return scheduleRepository.findByIdOrNull(id) ?.also { log.info { "[ScheduleService.findOrThrow] 일정 조회 완료: id=$id" } } ?: run { log.warn { "[ScheduleService.updateSchedule] 일정 조회 실패. id=$id" } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_FOUND) } } }