package com.caliverse.admin.domain.service; import com.caliverse.admin.domain.dao.admin.BattleMapper; import com.caliverse.admin.domain.datacomponent.MetaDataHandler; import com.caliverse.admin.domain.entity.BattleEvent; import com.caliverse.admin.domain.entity.HISTORYTYPEDETAIL; import com.caliverse.admin.domain.entity.log.LogAction; import com.caliverse.admin.domain.entity.log.LogStatus; import com.caliverse.admin.domain.entity.metadata.MetaBattleConfigData; import com.caliverse.admin.domain.entity.metadata.MetaBattleRewardData; import com.caliverse.admin.domain.entity.metadata.MetaGameFFAConfigData; import com.caliverse.admin.domain.entity.metadata.MetaGameModeData; import com.caliverse.admin.domain.request.BattleEventRequest; import com.caliverse.admin.domain.response.BattleEventResponse; import com.caliverse.admin.dynamodb.service.DynamodbBattleEventService; import com.caliverse.admin.global.common.annotation.BusinessProcess; import com.caliverse.admin.global.common.annotation.RequestLog; import com.caliverse.admin.global.common.code.CommonCode; import com.caliverse.admin.global.common.code.ErrorCode; import com.caliverse.admin.global.common.code.SuccessCode; import com.caliverse.admin.global.common.constants.CommonConstants; import com.caliverse.admin.global.common.constants.MysqlConstants; import com.caliverse.admin.global.common.utils.CommonUtils; import com.caliverse.admin.mongodb.service.MysqlHistoryLogService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RequestParam; import java.time.*; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @Service @RequiredArgsConstructor @Slf4j public class BattleEventService { private final DynamodbBattleEventService dynamodbBattleEventService; private final BattleMapper battleMapper; private final MetaDataHandler metaDataHandler; private final HistoryService historyService; private final ObjectMapper objectMapper; private final MysqlHistoryLogService mysqlHistoryLogService; //전투시스템 설정 데이터 public BattleEventResponse getBattleConfigList(){ List list = metaDataHandler.getMetaBattleConfigsListData(); return BattleEventResponse.builder() .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .resultData(BattleEventResponse.ResultData.builder() .battleConfigList(list) .build() ) .build(); } //전투시스템 보상 데이터 public BattleEventResponse getBattleRewardList(){ List list = metaDataHandler.getMetaBattleRewardsListData(); return BattleEventResponse.builder() .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .resultData(BattleEventResponse.ResultData.builder() .battleRewardList(list) .build() ) .build(); } //전투시스템 게임모드 데이터 public BattleEventResponse getGameModeList(){ List list = metaDataHandler.getMetaGameModeListData(); return BattleEventResponse.builder() .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .resultData(BattleEventResponse.ResultData.builder() .gameModeList(list) .build() ) .build(); } // 전투시스템 이벤트 조회 @RequestLog public BattleEventResponse getBattleEventList(@RequestParam Map requestParam){ requestParam = CommonUtils.pageSetting(requestParam); List list = battleMapper.getBattleEventList(requestParam); int allCnt = battleMapper.getAllCnt(requestParam); return BattleEventResponse.builder() .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .resultData(BattleEventResponse.ResultData.builder() .battleEventList(list) .total(battleMapper.getTotal()) .totalAll(allCnt) .pageNo(requestParam.get("page_no")!=null? Integer.parseInt(requestParam.get("page_no")):1) .build() ) .build(); } // 전투시스템 이벤트 상세조회 @RequestLog public BattleEventResponse getBattleEventDetail(Long id){ String email = CommonUtils.getAdmin() != null ? CommonUtils.getAdmin().getEmail() : ""; log.info("getBattleEventDetail user: {}, id: {}", email, id); BattleEvent battleEvent = battleMapper.getBattleEventDetail(id); return BattleEventResponse.builder() .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .resultData(BattleEventResponse.ResultData.builder() .battleEvent(battleEvent) .build()) .build(); } // 전투시스템 이벤트 저장 @BusinessProcess(action = LogAction.BATTLE_EVENT) @Transactional(transactionManager = "transactionManager") @RequestLog public BattleEventResponse postBattleEvent(BattleEventRequest battleEventRequest){ if(battleEventRequest.getRepeatType().equals(BattleEvent.BATTLE_REPEAT_TYPE.NONE)){ LocalDateTime start_dt = battleEventRequest.getEventStartDt(); LocalDateTime end_dt = start_dt.plusHours(12); battleEventRequest.setEventEndDt(end_dt); }else{ // 일자만 필요해서 UTC시간으로 변경되다보니 한국시간(+9)을 더해서 마지막시간으로 설정 LocalDateTime end_dt_kst = battleEventRequest.getEventEndDt() .plusHours(9) .withHour(23) .withMinute(59) .withSecond(59) .withNano(0); battleEventRequest.setEventEndDt(end_dt_kst); } battleEventRequest.setInstanceId(CommonConstants.BATTLE_INSTANCE_ID); //고정값으로 넣고 추후 맵정보가 늘어나면 선택하는 걸로 if(battleEventRequest.getGameModeId().equals(0) || battleEventRequest.getRoundCount().equals(0) ){ return BattleEventResponse.builder() .status(CommonCode.ERROR.getHttpStatus()) .result(ErrorCode.ERROR_BATTLE_EVENT_TIME_OVER.toString()) .build(); } // metadata기준 되기전에 운영툴에서 입력값받아서 계산하던 부분 // int operation_time = ffACalcEndTime(battleEventRequest); List existingList = battleMapper.getCheckBattleEventList(battleEventRequest); boolean isTime = isTimeOverlapping(existingList, battleEventRequest); if(isTime){ log.warn("battle Schedule duplication start_dt: {}, end_dt: {}, operation_time: {}", battleEventRequest.getEventStartDt(), battleEventRequest.getEventEndDt(), battleEventRequest.getEventOperationTime()); return BattleEventResponse.builder() .status(CommonCode.ERROR.getHttpStatus()) .result(ErrorCode.ERROR_BATTLE_EVENT_TIME_OVER.toString()) .build(); } // dynamoDB 기준으로 id를 관리하려고했으나 종료된 데이터를 주기적으로 지워주다보니 id가 꼬일수 있어서 운영DB기준으로 변경 // int next_event_id = dynamodbBattleEventService.getEventId() + 1; // battleEventRequest.setEventId(next_event_id); int result = battleMapper.postBattleEvent(battleEventRequest); log.info("AdminToolDB BattleEvent Save id: {}", battleEventRequest.getId()); long battle_event_id = battleEventRequest.getId(); battleEventRequest.setEventId((int) battle_event_id); dynamodbBattleEventService.insertBattleEvent(battleEventRequest); return BattleEventResponse.builder() .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .resultData(BattleEventResponse.ResultData.builder() .message(SuccessCode.SAVE.getMessage()) .build()) .build(); } // 전투시스템 이벤트 수정 @BusinessProcess(action = LogAction.BATTLE_EVENT) @Transactional(transactionManager = "transactionManager") @RequestLog public BattleEventResponse updateBattleEvent(Long id, BattleEventRequest battleEventRequest) { battleEventRequest.setId(id); battleEventRequest.setUpdateBy(CommonUtils.getAdmin().getId()); battleEventRequest.setUpdateDt(LocalDateTime.now()); BattleEvent before_info = battleMapper.getBattleEventDetail(id); if(!before_info.getStatus().equals(BattleEvent.BATTLE_STATUS.STOP)){ return BattleEventResponse.builder() .status(CommonCode.ERROR.getHttpStatus()) .result(ErrorCode.ERROR_BATTLE_EVENT_STATUS_IMPOSSIBLE.toString()) .build(); } List existingList = battleMapper.getCheckBattleEventList(battleEventRequest); boolean isTime = isTimeOverlapping(existingList, battleEventRequest); if(isTime){ return BattleEventResponse.builder() .status(CommonCode.ERROR.getHttpStatus()) .result(ErrorCode.ERROR_BATTLE_EVENT_TIME_OVER.toString()) .build(); } // int operation_time = ffACalcEndTime(battleEventRequest); // battleEventRequest.setEventOperationTime(operation_time); // 일자만 필요해서 UTC시간으로 변경되다보니 한국시간(+9)을 더해서 마지막시간으로 설정 LocalDateTime end_dt_kst = battleEventRequest.getEventEndDt() .plusHours(9) .withHour(23) .withMinute(59) .withSecond(59) .withNano(0); battleEventRequest.setEventEndDt(end_dt_kst); int result = battleMapper.updateBattleEvent(battleEventRequest); log.info("AdminToolDB BattleEvent Update Complete id: {}", id); dynamodbBattleEventService.updateBattleEvent(battleEventRequest); return BattleEventResponse.builder() .resultData(BattleEventResponse.ResultData.builder() .build()) .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .build(); } // 전투시스템 이벤트 중단 @BusinessProcess(action = LogAction.BATTLE_EVENT) @Transactional(transactionManager = "transactionManager") @RequestLog public BattleEventResponse updateStopBattleEvent(Long id){ Map map = new HashMap<>(); BattleEvent info = battleMapper.getBattleEventDetail(id); if(info.getStatus().equals(BattleEvent.BATTLE_STATUS.RUNNING)){ return BattleEventResponse.builder() .status(CommonCode.ERROR.getHttpStatus()) .result(ErrorCode.ERROR_BATTLE_EVENT_STATUS_START_IMPOSSIBLE.toString()) .build(); } map.put("id", id); map.put("status", BattleEvent.BATTLE_STATUS.STOP); map.put("updateBy", CommonUtils.getAdmin().getId()); int result = battleMapper.updateStatusBattleEvent(map); log.info("BattleEvent Stop Complete id: {}", id); dynamodbBattleEventService.updateStopBattleEvent(info); return BattleEventResponse.builder() .resultData(BattleEventResponse.ResultData.builder() .build()) .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .build(); } // 전투시스템 이벤트 삭제(사용안함) @BusinessProcess(action = LogAction.BATTLE_EVENT) @Transactional(transactionManager = "transactionManager") @RequestLog public BattleEventResponse deleteBattleEvent(BattleEventRequest battleEventRequest){ Map map = new HashMap<>(); AtomicBoolean is_falil = new AtomicBoolean(false); battleEventRequest.getList().forEach( item->{ Long id = item.getId(); BattleEvent info = battleMapper.getBattleEventDetail(id); if(!info.getStatus().equals(BattleEvent.BATTLE_STATUS.STOP)){ is_falil.set(true); return; } map.put("id", id); map.put("updateBy", CommonUtils.getAdmin().getId()); int result = battleMapper.deleteBattleEvent(map); log.info("BattleEvent Delete Complete id: {}", id); // dynamodbLandAuctionService.cancelLandAuction(auction_info); } ); if(is_falil.get()){ return BattleEventResponse.builder() .status(CommonCode.ERROR.getHttpStatus()) .result(ErrorCode.ERROR_AUCTION_STATUS_IMPOSSIBLE.toString()) .build(); } return BattleEventResponse.builder() .resultData(BattleEventResponse.ResultData.builder() .build()) .status(CommonCode.SUCCESS.getHttpStatus()) .result(CommonCode.SUCCESS.getResult()) .build(); } public List getScheduleBattleEventList(){ return battleMapper.getScheduleBattleEventList(); } @Transactional(transactionManager = "transactionManager") public void updateBattleEventStatus(Map map){ try{ battleMapper.updateStatusBattleEvent(map); log.info("BattleEvent status changed: {}", map.get("status")); }catch(Exception e){ log.error("BattleEvent Status Update Fail map: {}", map); } } // 이벤트 동작 시간 계산 private int ffACalcEndTime(BattleEventRequest battleEventRequest){ MetaGameModeData gameModeData = metaDataHandler.getMetaGameModeListData().stream() .filter(data -> data.getId().equals(battleEventRequest.getGameModeId())) .findFirst() .orElse(null); if(gameModeData == null) return 0; MetaGameFFAConfigData config = metaDataHandler.getMetaGameFFAConfigListData().stream() .filter(data -> data.getId().equals(gameModeData.getModeConfigId())) .findFirst() .orElse(null); // MetaBattleConfigData config = metaDataHandler.getMetaBattleConfigsListData().stream() // .filter(data -> data.getId().equals(battleEventRequest.getConfigId())) // .findFirst() // .orElse(null); if(config == null) return 0; int round_time = config.getRoundTime(); int round_count = battleEventRequest.getRoundCount(); int round_wait_time = config.getNextRoundWaitTime(); int result_wait_time = config.getResultUIWaitTime(); int server_wait_time = CommonConstants.BATTLE_SERVER_WAIT_TIME; int total_time = round_time + ((round_count - 1) * (round_time + round_wait_time)) + result_wait_time + server_wait_time; return total_time; // 초 } private boolean isTimeOverlapping(List existingList, BattleEventRequest battleEventRequest){ LocalDateTime newStartDt = battleEventRequest.getEventStartDt(); LocalDateTime newEndDt = battleEventRequest.getEventEndDt(); LocalDate newStartDate = newStartDt.toLocalDate(); LocalDate newEndDate = newEndDt.toLocalDate(); LocalTime newStartTime = newStartDt.toLocalTime(); LocalTime newEndTime = newStartTime.plusSeconds(battleEventRequest.getEventOperationTime() + CommonConstants.BATTLE_SERVER_WAIT_TIME); BattleEvent.BATTLE_REPEAT_TYPE newRepeatType = battleEventRequest.getRepeatType(); return existingList.stream().anyMatch(existingEvent -> { if (battleEventRequest.getId() != null && battleEventRequest.getId().equals(existingEvent.getId())) { return false; } LocalDateTime existingStartDt = existingEvent.getEventStartDt(); LocalDateTime existingEndDt = existingEvent.getEventEndDt(); LocalDate existingStartDate = existingStartDt.toLocalDate(); LocalDate existingEndDate = existingEndDt.toLocalDate(); LocalTime existingStartTime = existingStartDt.toLocalTime(); LocalTime existingEndTime = existingStartTime.plusSeconds(existingEvent.getEventOperationTime() + CommonConstants.BATTLE_SERVER_WAIT_TIME); BattleEvent.BATTLE_REPEAT_TYPE existingRepeatType = existingEvent.getRepeatType(); // 1. 두 이벤트가 모두 NONE 타입인 경우 if (newRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.NONE && existingRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.NONE) { if (newStartDate.equals(existingStartDate)) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } return false; } // 2. NONE 타입과 DAY 타입 간의 중복 체크 if (newRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.NONE && existingRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.DAY) { if (isDateInRange(newStartDate, existingStartDate, existingEndDate)) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } return false; } // 3. DAY 타입과 NONE 타입 간의 중복 체크 if (newRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.DAY && existingRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.NONE) { if (isDateInRange(existingStartDate, newStartDate, newEndDate)) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } return false; } // 4. 두 이벤트가 모두 DAY 타입인 경우 if (newRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.DAY && existingRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.DAY) { if (datesOverlap(newStartDate, newEndDate, existingStartDate, existingEndDate)) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } return false; } // 5. NONE 타입과 요일 타입 간의 중복 체크 if (newRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.NONE && isWeekdayType(existingRepeatType)) { if (isDateInRange(newStartDate, existingStartDate, existingEndDate)) { DayOfWeek noneDayOfWeek = newStartDate.getDayOfWeek(); DayOfWeek weekdayType = getDayOfWeekFromRepeatType(existingRepeatType); if (noneDayOfWeek == weekdayType) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } } return false; } // 6. 요일 타입과 NONE 타입 간의 중복 체크 if (isWeekdayType(newRepeatType) && existingRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.NONE) { if (isDateInRange(existingStartDate, newStartDate, newEndDate)) { DayOfWeek noneDayOfWeek = existingStartDate.getDayOfWeek(); DayOfWeek weekdayType = getDayOfWeekFromRepeatType(newRepeatType); if (noneDayOfWeek == weekdayType) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } } return false; } // 7. DAY 타입과 요일 타입 간의 중복 체크 if (newRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.DAY && isWeekdayType(existingRepeatType)) { if (datesOverlap(newStartDate, newEndDate, existingStartDate, existingEndDate)) { if (hasOverlappingWeekday(newStartDate, newEndDate, existingStartDate, existingEndDate, getDayOfWeekFromRepeatType(existingRepeatType))) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } } return false; } // 8. 요일 타입과 DAY 타입 간의 중복 체크 if (isWeekdayType(newRepeatType) && existingRepeatType == BattleEvent.BATTLE_REPEAT_TYPE.DAY) { if (datesOverlap(newStartDate, newEndDate, existingStartDate, existingEndDate)) { if (hasOverlappingWeekday(newStartDate, newEndDate, existingStartDate, existingEndDate, getDayOfWeekFromRepeatType(newRepeatType))) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } } return false; } // 9. 두 이벤트가 모두 요일 타입인 경우 if (isWeekdayType(newRepeatType) && isWeekdayType(existingRepeatType)) { if (newRepeatType == existingRepeatType) { if (datesOverlap(newStartDate, newEndDate, existingStartDate, existingEndDate)) { return !existingStartTime.isAfter(newEndTime) && !newStartTime.isAfter(existingEndTime); } } return false; } return false; }); } private boolean isDateInRange(LocalDate date, LocalDate rangeStart, LocalDate rangeEnd) { return !date.isBefore(rangeStart) && !date.isAfter(rangeEnd); } private boolean datesOverlap(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) { return !start1.isAfter(end2) && !start2.isAfter(end1); } private boolean isWeekdayType(BattleEvent.BATTLE_REPEAT_TYPE repeatType) { return repeatType == BattleEvent.BATTLE_REPEAT_TYPE.SUNDAY || repeatType == BattleEvent.BATTLE_REPEAT_TYPE.MONDAY || repeatType == BattleEvent.BATTLE_REPEAT_TYPE.TUESDAY || repeatType == BattleEvent.BATTLE_REPEAT_TYPE.WEDNESDAY || repeatType == BattleEvent.BATTLE_REPEAT_TYPE.THURSDAY || repeatType == BattleEvent.BATTLE_REPEAT_TYPE.FRIDAY || repeatType == BattleEvent.BATTLE_REPEAT_TYPE.SATURDAY; } private DayOfWeek getDayOfWeekFromRepeatType(BattleEvent.BATTLE_REPEAT_TYPE repeatType) { switch (repeatType) { case MONDAY: return DayOfWeek.MONDAY; case TUESDAY: return DayOfWeek.TUESDAY; case WEDNESDAY: return DayOfWeek.WEDNESDAY; case THURSDAY: return DayOfWeek.THURSDAY; case FRIDAY: return DayOfWeek.FRIDAY; case SATURDAY: return DayOfWeek.SATURDAY; case SUNDAY: return DayOfWeek.SUNDAY; default: throw new IllegalArgumentException("Invalid repeat type: " + repeatType); } } private boolean hasOverlappingWeekday( LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2, DayOfWeek dayOfWeek) { // 두 범위의 교집합 계산 LocalDate overlapStart = start1.isAfter(start2) ? start1 : start2; LocalDate overlapEnd = end1.isBefore(end2) ? end1 : end2; if (overlapStart.isAfter(overlapEnd)) { return false; } // 겹치는 날짜 범위 내에 해당 요일이 있는지 확인 LocalDate current = overlapStart; while (!current.isAfter(overlapEnd)) { if (current.getDayOfWeek() == dayOfWeek) { return true; } current = current.plusDays(1); } return false; } }