diff --git a/src/main/java/side/onetime/controller/EventController.java b/src/main/java/side/onetime/controller/EventController.java index e0de833..341b00e 100644 --- a/src/main/java/side/onetime/controller/EventController.java +++ b/src/main/java/side/onetime/controller/EventController.java @@ -5,7 +5,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import side.onetime.dto.event.request.CreateEventRequest; -import side.onetime.dto.event.request.ModifyUserCreatedEventTitleRequest; +import side.onetime.dto.event.request.ModifyUserCreatedEventRequest; import side.onetime.dto.event.response.*; import side.onetime.global.common.ApiResponse; import side.onetime.global.common.status.SuccessStatus; @@ -131,23 +131,29 @@ public ResponseEntity> removeUserCreatedEvent( } /** - * 유저가 생성한 이벤트 제목 수정 API. + * 유저가 생성한 이벤트 수정 API. * - * 이 API는 인증된 유저가 생성한 특정 이벤트의 제목을 수정합니다. + * 이 API는 인증된 유저가 생성한 특정 이벤트의 제목, 시간, 설문 범위를 수정합니다. + * 수정 가능한 항목은 다음과 같습니다: + * - 이벤트 제목 + * - 시작 시간 및 종료 시간 + * - 날짜 또는 요일 범위 + * + * 요청 데이터에 따라 변경 사항을 반영하며, 필요에 따라 기존 스케줄 데이터를 삭제하거나 새로운 스케줄을 생성합니다. * * @param authorizationHeader 인증된 유저의 토큰 - * @param eventId 제목을 수정할 이벤트의 ID - * @param modifyUserCreatedEventTitleRequest 새로운 제목 정보가 담긴 요청 데이터 + * @param eventId 수정할 이벤트의 ID + * @param modifyUserCreatedEventRequest 새로운 이벤트 정보가 담긴 요청 데이터 (제목, 시간, 범위 등) * @return 수정 성공 여부 */ - @PutMapping("/{event_id}") - public ResponseEntity> modifyUserCreatedEventTitle( + @PatchMapping("/{event_id}") + public ResponseEntity> modifyUserCreatedEvent( @RequestHeader("Authorization") String authorizationHeader, @PathVariable("event_id") String eventId, - @Valid @RequestBody ModifyUserCreatedEventTitleRequest modifyUserCreatedEventTitleRequest) { + @Valid @RequestBody ModifyUserCreatedEventRequest modifyUserCreatedEventRequest) { - eventService.modifyUserCreatedEventTitle(authorizationHeader, eventId, modifyUserCreatedEventTitleRequest); - return ApiResponse.onSuccess(SuccessStatus._MODIFY_USER_CREATED_EVENT_TITLE); + eventService.modifyUserCreatedEvent(authorizationHeader, eventId, modifyUserCreatedEventRequest); + return ApiResponse.onSuccess(SuccessStatus._MODIFY_USER_CREATED_EVENT); } /** diff --git a/src/main/java/side/onetime/domain/Event.java b/src/main/java/side/onetime/domain/Event.java index e4cfd02..9753e14 100644 --- a/src/main/java/side/onetime/domain/Event.java +++ b/src/main/java/side/onetime/domain/Event.java @@ -62,6 +62,14 @@ public void updateTitle(String title) { this.title = title; } + public void updateStartTime(String startTime) { + this.startTime = startTime; + } + + public void updateEndTime(String endTime) { + this.endTime = endTime; + } + public void addQrFileName(String qrFileName) { this.qrFileName = qrFileName; } diff --git a/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventRequest.java b/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventRequest.java new file mode 100644 index 0000000..7507f57 --- /dev/null +++ b/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventRequest.java @@ -0,0 +1,19 @@ +package side.onetime.dto.event.request; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ModifyUserCreatedEventRequest( + @NotBlank(message = "제목은 필수 값입니다.") String title, + @NotBlank(message = "시작 시간은 필수 값입니다.") String startTime, + @NotBlank(message = "종료 시간은 필수 값입니다.") String endTime, + @NotNull(message = "설문 범위는 필수 값입니다.") List ranges +) { +} diff --git a/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventTitleRequest.java b/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventTitleRequest.java deleted file mode 100644 index 22d684d..0000000 --- a/src/main/java/side/onetime/dto/event/request/ModifyUserCreatedEventTitleRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package side.onetime.dto.event.request; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; -import jakarta.validation.constraints.NotBlank; - -@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ModifyUserCreatedEventTitleRequest( - @NotBlank(message = "변경할 제목은 필수 값입니다.") String title -) { -} diff --git a/src/main/java/side/onetime/global/common/status/SuccessStatus.java b/src/main/java/side/onetime/global/common/status/SuccessStatus.java index f1c8914..68b0ea9 100644 --- a/src/main/java/side/onetime/global/common/status/SuccessStatus.java +++ b/src/main/java/side/onetime/global/common/status/SuccessStatus.java @@ -19,7 +19,7 @@ public enum SuccessStatus implements BaseCode { _GET_MOST_POSSIBLE_TIME(HttpStatus.OK, "200", "가장 많이 되는 시간 조회에 성공했습니다."), _GET_USER_PARTICIPATED_EVENTS(HttpStatus.OK, "200", "유저 참여 이벤트 목록 조회에 성공했습니다."), _REMOVE_USER_CREATED_EVENT(HttpStatus.OK, "200", "유저가 생성한 이벤트 삭제에 성공했습니다."), - _MODIFY_USER_CREATED_EVENT_TITLE(HttpStatus.OK, "200", "유저가 생성한 이벤트 제목 수정에 성공했습니다."), + _MODIFY_USER_CREATED_EVENT(HttpStatus.OK, "200", "유저가 생성한 이벤트 수정에 성공했습니다."), _GET_EVENT_QR_CODE(HttpStatus.OK, "200", "이벤트 QR 코드 조회에 성공했습니다."), // Member _REGISTER_MEMBER(HttpStatus.CREATED, "201", "멤버 등록에 성공했습니다."), diff --git a/src/main/java/side/onetime/repository/custom/EventRepositoryCustom.java b/src/main/java/side/onetime/repository/custom/EventRepositoryCustom.java index 27cc87e..259211f 100644 --- a/src/main/java/side/onetime/repository/custom/EventRepositoryCustom.java +++ b/src/main/java/side/onetime/repository/custom/EventRepositoryCustom.java @@ -4,4 +4,6 @@ public interface EventRepositoryCustom { void deleteEvent(Event event); + void deleteSchedulesByRange(Event event, String range); + void deleteSchedulesByTime(Event event, String time); } diff --git a/src/main/java/side/onetime/repository/custom/EventRepositoryImpl.java b/src/main/java/side/onetime/repository/custom/EventRepositoryImpl.java index 960c8e2..aeea72e 100644 --- a/src/main/java/side/onetime/repository/custom/EventRepositoryImpl.java +++ b/src/main/java/side/onetime/repository/custom/EventRepositoryImpl.java @@ -46,4 +46,51 @@ public void deleteEvent(Event e) { .where(event.eq(e)) .execute(); } + + /** + * 특정 범위에 해당하는 스케줄 삭제 메서드. + * + * 이벤트와 연결된 특정 범위(DATE 또는 DAY)에 해당하는 모든 스케줄 및 관련 데이터를 삭제합니다. + * 삭제 순서는 외래 키 제약 조건을 고려하여, + * Selection → Schedule 순으로 진행됩니다. + * + * @param event 이벤트 객체 + * @param range 삭제할 범위 (DATE 또는 DAY) + */ + @Override + public void deleteSchedulesByRange(Event event, String range) { + queryFactory.delete(selection) + .where(selection.schedule.event.eq(event) + .and(selection.schedule.date.eq(range) + .or(selection.schedule.day.eq(range)))) + .execute(); + + queryFactory.delete(schedule) + .where(schedule.event.eq(event) + .and(schedule.date.eq(range).or(schedule.day.eq(range)))) + .execute(); + } + + /** + * 특정 시간에 해당하는 스케줄 삭제 메서드. + * + * 이벤트와 연결된 특정 시간(HH:mm 형식)에 해당하는 모든 스케줄 및 관련 데이터를 삭제합니다. + * 삭제 순서는 외래 키 제약 조건을 고려하여, + * Selection → Schedule 순으로 진행됩니다. + * + * @param event 이벤트 객체 + * @param time 삭제할 시간 (HH:mm 형식) + */ + @Override + public void deleteSchedulesByTime(Event event, String time) { + queryFactory.delete(selection) + .where(selection.schedule.event.eq(event) + .and(selection.schedule.time.eq(time))) + .execute(); + + queryFactory.delete(schedule) + .where(schedule.event.eq(event) + .and(schedule.time.eq(time))) + .execute(); + } } diff --git a/src/main/java/side/onetime/service/EventService.java b/src/main/java/side/onetime/service/EventService.java index c1726a4..0ebb552 100644 --- a/src/main/java/side/onetime/service/EventService.java +++ b/src/main/java/side/onetime/service/EventService.java @@ -8,7 +8,7 @@ import side.onetime.domain.enums.Category; import side.onetime.domain.enums.EventStatus; import side.onetime.dto.event.request.CreateEventRequest; -import side.onetime.dto.event.request.ModifyUserCreatedEventTitleRequest; +import side.onetime.dto.event.request.ModifyUserCreatedEventRequest; import side.onetime.dto.event.response.*; import side.onetime.exception.CustomException; import side.onetime.exception.status.EventErrorStatus; @@ -23,7 +23,6 @@ import side.onetime.util.QrUtil; import side.onetime.util.S3Util; -import java.time.LocalTime; import java.util.*; import java.util.stream.Collectors; @@ -142,13 +141,13 @@ private String generateAndUploadQrCode(UUID eventId) { */ @Transactional protected void createAndSaveDateSchedules(Event event, List ranges, String startTime, String endTime) { - List timeSets = DateUtil.createTimeSets(startTime, endTime); + List timeSets = DateUtil.createTimeSets(startTime, endTime); List schedules = ranges.stream() .flatMap(range -> timeSets.stream() .map(time -> Schedule.builder() .event(event) .date(range) - .time(String.valueOf(time)) + .time(time) .build())) .collect(Collectors.toList()); scheduleRepository.saveAll(schedules); @@ -165,13 +164,13 @@ protected void createAndSaveDateSchedules(Event event, List ranges, Stri */ @Transactional protected void createAndSaveDaySchedules(Event event, List ranges, String startTime, String endTime) { - List timeSets = DateUtil.createTimeSets(startTime, endTime); + List timeSets = DateUtil.createTimeSets(startTime, endTime); List schedules = ranges.stream() .flatMap(range -> timeSets.stream() .map(time -> Schedule.builder() .event(event) .day(range) - .time(String.valueOf(time)) + .time(time) .build())) .collect(Collectors.toList()); scheduleRepository.saveAll(schedules); @@ -451,17 +450,145 @@ public void removeUserCreatedEvent(String authorizationHeader, String eventId) { } /** - * 유저가 생성한 이벤트 제목 수정 메서드. - * 인증된 유저가 생성한 특정 이벤트의 제목을 수정합니다. + * 유저가 생성한 이벤트 수정 메서드. + * 인증된 유저가 생성한 특정 이벤트를 수정합니다. * * @param authorizationHeader 인증된 유저의 토큰 * @param eventId 수정할 이벤트의 ID - * @param modifyUserCreatedEventTitleRequest 새로운 제목 데이터 + * @param modifyUserCreatedEventRequest 새로운 이벤트 데이터 */ @Transactional - public void modifyUserCreatedEventTitle(String authorizationHeader, String eventId, ModifyUserCreatedEventTitleRequest modifyUserCreatedEventTitleRequest) { + public void modifyUserCreatedEvent(String authorizationHeader, String eventId, ModifyUserCreatedEventRequest modifyUserCreatedEventRequest) { EventParticipation eventParticipation = verifyUserIsEventCreator(authorizationHeader, eventId); - eventParticipation.getEvent().updateTitle(modifyUserCreatedEventTitleRequest.title()); + Event event = eventParticipation.getEvent(); + + updateEventTitle(event, modifyUserCreatedEventRequest.title()); + updateEventRanges(event, event.getSchedules(), modifyUserCreatedEventRequest.ranges(), modifyUserCreatedEventRequest.startTime(), modifyUserCreatedEventRequest.endTime()); + updateEventTimes(event, event.getSchedules(), modifyUserCreatedEventRequest.startTime(), modifyUserCreatedEventRequest.endTime()); + } + + /** + * 이벤트 제목 업데이트 메서드. + * 이벤트의 제목을 새로운 제목으로 업데이트합니다. + * + * @param event 이벤트 객체 + * @param newTitle 새로운 제목 + */ + protected void updateEventTitle(Event event, String newTitle) { + if (newTitle != null) { + event.updateTitle(newTitle); + } + } + + /** + * 이벤트 범위 업데이트 메서드. + * 기존의 범위를 새로운 범위로 업데이트하며, 삭제 및 생성 대상 범위를 처리합니다. + * + * @param event 이벤트 객체 + * @param schedules 기존 스케줄 목록 + * @param newRanges 새로운 범위 리스트 + * @param newStartTime 새로 설정할 시작 시간 + * @param newEndTime 새로 설정할 종료 시간 + */ + protected void updateEventRanges(Event event, List schedules, List newRanges, String newStartTime, String newEndTime) { + Set existRanges = event.getCategory() == Category.DATE + ? schedules.stream().map(Schedule::getDate).filter(Objects::nonNull).collect(Collectors.toSet()) + : schedules.stream().map(Schedule::getDay).filter(Objects::nonNull).collect(Collectors.toSet()); + + // 삭제 대상 처리 + existRanges.stream() + .filter(range -> !newRanges.contains(range)) + .forEach(range -> eventRepository.deleteSchedulesByRange(event, range)); + + // 생성 대상 처리 + List rangesToCreate = newRanges.stream() + .filter(range -> !existRanges.contains(range)) + .collect(Collectors.toList()); + + if (!rangesToCreate.isEmpty()) { + if (event.getCategory() == Category.DATE) { + createAndSaveDateSchedules(event, rangesToCreate, newStartTime, newEndTime); + } else if (event.getCategory() == Category.DAY) { + createAndSaveDaySchedules(event, rangesToCreate, newStartTime, newEndTime); + } + } + } + + /** + * 이벤트 시간 업데이트 메서드. + * 기존의 시간대를 새로운 시간대로 업데이트하며, 삭제 및 생성 대상 시간대를 처리합니다. + * + * @param event 이벤트 객체 + * @param schedules 기존 스케줄 목록 + * @param newStartTime 새로 설정할 시작 시간 + * @param newEndTime 새로 설정할 종료 시간 + */ + protected void updateEventTimes(Event event, List schedules, String newStartTime, String newEndTime) { + if (!event.getStartTime().equals(newStartTime) || !event.getEndTime().equals(newEndTime)) { + List newTimeSets = DateUtil.createTimeSets(newStartTime, newEndTime); + + Set existTimes = schedules.stream() + .map(Schedule::getTime) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + // 삭제 대상 시간 처리 + existTimes.stream() + .filter(time -> !newTimeSets.contains(time)) + .forEach(time -> eventRepository.deleteSchedulesByTime(event, time)); + + // 생성 대상 시간 처리 + List timesToCreate = newTimeSets.stream() + .filter(newTime -> !existTimes.contains(newTime)) + .toList(); + + if (!timesToCreate.isEmpty()) { + if (event.getCategory() == Category.DATE) { + createSchedulesForTime(event, extractExistingRanges(schedules, event.getCategory()), timesToCreate, true); + } else if (event.getCategory() == Category.DAY) { + createSchedulesForTime(event, extractExistingRanges(schedules, event.getCategory()), timesToCreate, false); + } + } + + event.updateStartTime(newStartTime); + event.updateEndTime(newEndTime); + } + } + + /** + * 기존 범위 추출 메서드. + * 스케줄 목록에서 현재 존재하는 날짜 또는 요일 범위를 추출합니다. + * + * @param schedules 스케줄 목록 + * @param category 이벤트의 카테고리 (날짜 또는 요일) + * @return 현재 존재하는 범위 집합 + */ + private Set extractExistingRanges(List schedules, Category category) { + return category == Category.DATE + ? schedules.stream().map(Schedule::getDate).filter(Objects::nonNull).collect(Collectors.toSet()) + : schedules.stream().map(Schedule::getDay).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + /** + * 시간 기반 스케줄 생성 메서드. + * 시간대를 기반으로 새로운 스케줄을 생성하고 저장합니다. + * + * @param event 이벤트 객체 + * @param ranges 범위 리스트 (날짜 또는 요일) + * @param timesToCreate 새로 생성할 시간 리스트 + * @param isDateBased 날짜 기반 여부 + */ + private void createSchedulesForTime(Event event, Set ranges, List timesToCreate, boolean isDateBased) { + List newSchedules = ranges.stream() + .flatMap(range -> timesToCreate.stream() + .map(time -> Schedule.builder() + .event(event) + .date(isDateBased ? range : null) + .day(!isDateBased ? range : null) + .time(time) + .build())) + .collect(Collectors.toList()); + scheduleRepository.saveAll(newSchedules); } /** diff --git a/src/main/java/side/onetime/util/DateUtil.java b/src/main/java/side/onetime/util/DateUtil.java index 3de011c..0064353 100644 --- a/src/main/java/side/onetime/util/DateUtil.java +++ b/src/main/java/side/onetime/util/DateUtil.java @@ -28,8 +28,8 @@ public class DateUtil { * @param end 종료 시간 (HH:mm 형식) * @return 30분 간격의 시간 리스트 */ - public static List createTimeSets(String start, String end) { - List timeSets = new ArrayList<>(); + public static List createTimeSets(String start, String end) { + List timeSets = new ArrayList<>(); boolean isEndTimeMidnight = end.equals("24:00"); if (isEndTimeMidnight) { @@ -41,12 +41,12 @@ public static List createTimeSets(String start, String end) { LocalTime currentTime = startTime; while (!currentTime.isAfter(endTime.minusMinutes(30))) { - timeSets.add(currentTime); + timeSets.add(String.valueOf(currentTime)); currentTime = currentTime.plusMinutes(30); } if (isEndTimeMidnight) { - timeSets.add(LocalTime.of(23, 30)); + timeSets.add("23:30"); } return timeSets; diff --git a/src/test/java/side/onetime/event/EventControllerTest.java b/src/test/java/side/onetime/event/EventControllerTest.java index 5ce4100..f208268 100644 --- a/src/test/java/side/onetime/event/EventControllerTest.java +++ b/src/test/java/side/onetime/event/EventControllerTest.java @@ -19,7 +19,7 @@ import side.onetime.domain.enums.Category; import side.onetime.domain.enums.EventStatus; import side.onetime.dto.event.request.CreateEventRequest; -import side.onetime.dto.event.request.ModifyUserCreatedEventTitleRequest; +import side.onetime.dto.event.request.ModifyUserCreatedEventRequest; import side.onetime.dto.event.response.*; import side.onetime.service.EventService; import side.onetime.util.JwtUtil; @@ -388,18 +388,24 @@ public void removeUserCreatedEvent() throws Exception { } @Test - @DisplayName("유저가 생성한 이벤트 제목을 수정한다.") + @DisplayName("유저가 생성한 이벤트를 수정한다.") public void modifyUserCreatedEventTitle() throws Exception { // given String eventId = UUID.randomUUID().toString(); - ModifyUserCreatedEventTitleRequest request = new ModifyUserCreatedEventTitleRequest("수정할 이벤트 제목"); + ModifyUserCreatedEventRequest request = new ModifyUserCreatedEventRequest( + "수정된 이벤트 제목", + "09:00", + "18:00", + List.of("2024.12.10", "2024.12.11") + ); String requestContent = new ObjectMapper().writeValueAsString(request); - Mockito.doNothing().when(eventService).modifyUserCreatedEventTitle(anyString(), anyString(), any(ModifyUserCreatedEventTitleRequest.class)); + Mockito.doNothing().when(eventService) + .modifyUserCreatedEvent(anyString(), anyString(), any(ModifyUserCreatedEventRequest.class)); // when - ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.put("/api/v1/events/{event_id}", eventId) + ResultActions resultActions = this.mockMvc.perform(RestDocumentationRequestBuilders.patch("/api/v1/events/{event_id}", eventId) .header(HttpHeaders.AUTHORIZATION, "Bearer sampleToken") .contentType(MediaType.APPLICATION_JSON) .content(requestContent) @@ -410,7 +416,7 @@ public void modifyUserCreatedEventTitle() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.is_success").value(true)) .andExpect(jsonPath("$.code").value("200")) - .andExpect(jsonPath("$.message").value("유저가 생성한 이벤트 제목 수정에 성공했습니다.")) + .andExpect(jsonPath("$.message").value("유저가 생성한 이벤트 수정에 성공했습니다.")) // docs .andDo(MockMvcRestDocumentationWrapper.document("event/modify-user-created-event-title", @@ -419,12 +425,15 @@ public void modifyUserCreatedEventTitle() throws Exception { resource( ResourceSnippetParameters.builder() .tag("Event API") - .description("유저가 생성한 이벤트 제목을 수정한다.") + .description("유저가 생성한 이벤트를 수정한다.") .pathParameters( parameterWithName("event_id").description("수정할 이벤트의 ID [예시 : dd099816-2b09-4625-bf95-319672c25659]") ) .requestFields( - fieldWithPath("title").type(JsonFieldType.STRING).description("새로운 이벤트 제목") + fieldWithPath("title").type(JsonFieldType.STRING).description("새로운 이벤트 제목"), + fieldWithPath("start_time").type(JsonFieldType.STRING).description("시작 시간 (HH:mm)"), + fieldWithPath("end_time").type(JsonFieldType.STRING).description("종료 시간 (HH:mm)"), + fieldWithPath("ranges").type(JsonFieldType.ARRAY).description("수정할 설문 범위 [예: 날짜 리스트]") ) .responseFields( fieldWithPath("is_success").type(JsonFieldType.BOOLEAN).description("성공 여부"),