From 4a1494abea438f24b3e65604c96cb194539aaf20 Mon Sep 17 00:00:00 2001 From: junhokim Date: Sun, 1 Mar 2026 17:50:47 +0900 Subject: [PATCH] =?UTF-8?q?feat.=20Update=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trip/application/in/TripService.java | 39 ++++++++-- .../in/request/TripUpdateRequest.java | 74 ++++++++++++++++++ .../in/response/TripUpdateResponse.java | 78 +++++++++++++++++++ .../in/usecase/TripManageUseCase.java | 4 + .../com/retrip/trip/domain/entity/Trip.java | 49 ++++++++++-- .../exception/TripUpdateFailedException.java | 12 +++ .../domain/exception/common/ErrorCode.java | 7 +- .../in/presentation/rest/TripController.java | 15 +++- src/main/resources/data.sql | 7 +- 9 files changed, 265 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/retrip/trip/application/in/request/TripUpdateRequest.java create mode 100644 src/main/java/com/retrip/trip/application/in/response/TripUpdateResponse.java create mode 100644 src/main/java/com/retrip/trip/domain/exception/TripUpdateFailedException.java diff --git a/src/main/java/com/retrip/trip/application/in/TripService.java b/src/main/java/com/retrip/trip/application/in/TripService.java index 093103c..5ff7aa1 100644 --- a/src/main/java/com/retrip/trip/application/in/TripService.java +++ b/src/main/java/com/retrip/trip/application/in/TripService.java @@ -4,17 +4,15 @@ import com.retrip.trip.application.in.response.*; import com.retrip.trip.application.in.usecase.*; import com.retrip.trip.application.out.crypto.TripPasswordEncoder; -import com.retrip.trip.application.out.repository.*; -import com.retrip.trip.domain.entity.Itinerary; -import com.retrip.trip.domain.entity.Trip; -import com.retrip.trip.domain.entity.TripConfirmationDemand; +import com.retrip.trip.application.out.repository.TripConfirmationDemandRepository; +import com.retrip.trip.application.out.repository.TripItineraryQueryRepository; +import com.retrip.trip.application.out.repository.TripQueryRepository; +import com.retrip.trip.application.out.repository.TripRepository; import com.retrip.trip.domain.entity.*; import com.retrip.trip.domain.exception.TripNotFoundException; import com.retrip.trip.domain.exception.common.BusinessException; import com.retrip.trip.domain.exception.common.InvalidValueException; -import com.retrip.trip.domain.vo.TripPassword; -import com.retrip.trip.domain.vo.TripPeriod; -import com.retrip.trip.domain.vo.TripStatus; +import com.retrip.trip.domain.vo.*; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -56,6 +54,31 @@ public TripCreateResponse createTripWithItineraries(UUID memberId, TripCreateReq return TripCreateResponse.of(savedTrip); } + @Override + public TripUpdateResponse updateTrip(UUID memberId, UUID tripId, TripUpdateRequest request) { + Trip trip = findTrip(tripId); + + TripTitle tripTitle = request.toTripTitle(); + TripDescription tripDescription = request.toTripDescription(); + TripPeriod tripPeriod = request.toTripPeriod(); + TripHashTags tripHashTags = request.toHashTags(trip); + + //List itineraries = tripItineraryQueryRepository.findByIdsWithItineraryDetails(trip.getItinerariesIds()); + trip.update( + memberId, + request.locationId(), + tripTitle, + tripDescription, + tripHashTags, + request.maxParticipants(), + request.imageUrl(), + request.category() + ); + trip.updatePeriod(tripPeriod, memberId); + + return TripUpdateResponse.of(trip); + } + @Override public TripUpdateVisibilityResponse updateTripVisibility(UUID tripId, TripUpdateVisibilityRequest request) { Trip trip = findTrip(tripId); @@ -70,7 +93,7 @@ public Page getTrips(Pageable page) { Page tripsPage = tripQueryRepository.findTrips(page); List trips = tripsPage.getContent(); List hashTags = tripQueryRepository.findHashTags(trips); - + return new PageImpl<>(TripResponse.of(trips, hashTags), page, tripsPage.getTotalElements()); } diff --git a/src/main/java/com/retrip/trip/application/in/request/TripUpdateRequest.java b/src/main/java/com/retrip/trip/application/in/request/TripUpdateRequest.java new file mode 100644 index 0000000..ccbb2f7 --- /dev/null +++ b/src/main/java/com/retrip/trip/application/in/request/TripUpdateRequest.java @@ -0,0 +1,74 @@ +package com.retrip.trip.application.in.request; + +import com.retrip.trip.domain.entity.Trip; +import com.retrip.trip.domain.entity.TripHashTags; +import com.retrip.trip.domain.vo.*; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; + +@Schema(description = "여행 수정 Request") +public record TripUpdateRequest( + @Schema(description = "여행 제목", example = "유럽 배낭여행") + @NotNull + String title, + + @Schema(description = "여행 설명", example = "파리, 런던, 로마를 여행하는 일정입니다.") + String description, + + @Schema(description = "여행 위치 ID", example = "550e8400-e29b-41d4-a716-446655440001") + @NotNull + UUID locationId, + + @Schema(description = "여행 시작 날짜", example = "2025-06-15") + @FutureOrPresent + LocalDate start, + + @Schema(description = "여행 종료 날짜", example = "2025-06-25") + @FutureOrPresent + LocalDate end, + + @Schema(description = "여행 최대 참가 인원") + Integer maxParticipants, + + @Schema(description = "HashTag") + List hashTags, + + @Schema(description = "여행 대표 이미지 URL") + String imageUrl, + + @Schema(description = "여행 카테고리") + TripCategory category +) { + public TripTitle toTripTitle() { + if (title != null) { + return new TripTitle(title); + } + return null; + } + + public TripDescription toTripDescription() { + if (description != null) { + return new TripDescription(description); + } + return null; + } + + public TripPeriod toTripPeriod() { + if (start != null && end != null) { + return new TripPeriod(start, end); + } + return null; + } + + public TripHashTags toHashTags(Trip trip) { + if (hashTags != null) { + return new TripHashTags(trip, hashTags); + } + return null; + } +} diff --git a/src/main/java/com/retrip/trip/application/in/response/TripUpdateResponse.java b/src/main/java/com/retrip/trip/application/in/response/TripUpdateResponse.java new file mode 100644 index 0000000..428b375 --- /dev/null +++ b/src/main/java/com/retrip/trip/application/in/response/TripUpdateResponse.java @@ -0,0 +1,78 @@ +package com.retrip.trip.application.in.response; + +import com.retrip.trip.domain.entity.Trip; +import com.retrip.trip.domain.entity.TripHashTag; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Schema(description = "여행 생성 Response") +public record TripUpdateResponse( + @Schema(description = "여행 ID", example = "550e8400-e29b-41d4-a716-446655440000") + UUID id, + + @Schema(description = "여행 목적지 ID", example = "550e8400-e29b-41d4-a716-446655440001") + UUID destinationId, + + @Schema(description = "여행 제목", example = "파리 여행") + String title, + + @Schema(description = "여행 설명") + String description, + + @Schema(description = "여행 시작 날짜") + LocalDate start, + + @Schema(description = "여행 종료 날짜") + LocalDate end, + + @Schema(description = "여행 최대 참가 인원") + int maxParticipants, + + @Schema(description = "여행 카테고리") + List hashTags, + + @Schema(description = "여행 카테고리") + String category, + + @Schema(description = "여행 대표 이미지 url") + String imageUrl, + + @Schema(description = "여행 일정 리스트") + List itineraries +) { + public static TripUpdateResponse of(Trip trip) { + return new TripUpdateResponse( + trip.getId(), + trip.getDestinationId(), + trip.getTitle().getValue(), + trip.getDescription().getValue(), + trip.getPeriod().getStart(), + trip.getPeriod().getEnd(), + trip.getTripParticipants().getMaxParticipants(), + trip.getHashTags().getValues().stream().map(TripHashTag::getName).toList(), + trip.getCategory().getViewName(), + trip.getImageUrl(), + trip.getItineraries() == null ? new ArrayList<>() : + trip.getItineraries().getValues().stream() + .map(i -> new ItineraryUpdateResponse(i.getId(), i.getName(), i.getDate())) + .toList() + ); + } + + @Schema(description = "여행 일정 Response") + private record ItineraryUpdateResponse( + @Schema(description = "일정 ID") + UUID id, + + @Schema(description = "일정 이름") + String name, + + @Schema(description = "일정 날짜") + LocalDate date + ) { + } +} diff --git a/src/main/java/com/retrip/trip/application/in/usecase/TripManageUseCase.java b/src/main/java/com/retrip/trip/application/in/usecase/TripManageUseCase.java index 5cde42a..530289d 100644 --- a/src/main/java/com/retrip/trip/application/in/usecase/TripManageUseCase.java +++ b/src/main/java/com/retrip/trip/application/in/usecase/TripManageUseCase.java @@ -1,8 +1,10 @@ package com.retrip.trip.application.in.usecase; import com.retrip.trip.application.in.request.TripCreateRequest; +import com.retrip.trip.application.in.request.TripUpdateRequest; import com.retrip.trip.application.in.request.TripUpdateVisibilityRequest; import com.retrip.trip.application.in.response.TripCreateResponse; +import com.retrip.trip.application.in.response.TripUpdateResponse; import com.retrip.trip.application.in.response.TripUpdateVisibilityResponse; import java.util.List; @@ -13,6 +15,8 @@ public interface TripManageUseCase { TripCreateResponse createTripWithItineraries(UUID memberId, TripCreateRequest request); + TripUpdateResponse updateTrip(UUID memberId, UUID tripId, TripUpdateRequest request); + TripUpdateVisibilityResponse updateTripVisibility(UUID tripId, TripUpdateVisibilityRequest request); void banMembers(UUID memberId, UUID tripId, List memberIds); diff --git a/src/main/java/com/retrip/trip/domain/entity/Trip.java b/src/main/java/com/retrip/trip/domain/entity/Trip.java index 812d639..c64bb73 100644 --- a/src/main/java/com/retrip/trip/domain/entity/Trip.java +++ b/src/main/java/com/retrip/trip/domain/entity/Trip.java @@ -1,9 +1,6 @@ package com.retrip.trip.domain.entity; -import com.retrip.trip.domain.exception.LeaderCannotLeaveException; -import com.retrip.trip.domain.exception.NotParticipantException; -import com.retrip.trip.domain.exception.PeriodUpdateFailedException; -import com.retrip.trip.domain.exception.TripNotReadyException; +import com.retrip.trip.domain.exception.*; import com.retrip.trip.domain.exception.common.BusinessException; import com.retrip.trip.domain.vo.*; import jakarta.persistence.*; @@ -205,4 +202,46 @@ public void updateVisibility(boolean isOpen) { public boolean isNotTripRecruitingStatus() { return !TripStatus.RECRUITING.equals(status); } -} \ No newline at end of file + + + public void update( + UUID memberId, + UUID destinationId, + TripTitle tripTitle, + TripDescription tripDescription, + TripHashTags tripHashTags, + Integer maxParticipants, + String imageUrl, + TripCategory category + ) { + if (!tripParticipants.updatableByLeader(memberId)) { + throw new TripUpdateFailedException(); + } + + if (destinationId != null) { + this.destinationId = destinationId; + } + + if (tripTitle != null) { + this.title = tripTitle; + } + if (tripDescription != null) { + this.description = tripDescription; + } + + if (tripHashTags != null) { + this.hashTags.getValues().clear(); + this.hashTags.getValues().addAll(tripHashTags.getValues()); + } + + if (maxParticipants != null) { + this.tripParticipants.updateMaxParticipants(maxParticipants, memberId); + } + if (imageUrl != null) { + this.imageUrl = imageUrl; + } + if (category != null) { + this.category = category; + } + } +} diff --git a/src/main/java/com/retrip/trip/domain/exception/TripUpdateFailedException.java b/src/main/java/com/retrip/trip/domain/exception/TripUpdateFailedException.java new file mode 100644 index 0000000..7b1cdaa --- /dev/null +++ b/src/main/java/com/retrip/trip/domain/exception/TripUpdateFailedException.java @@ -0,0 +1,12 @@ +package com.retrip.trip.domain.exception; + +import com.retrip.trip.domain.exception.common.BusinessException; +import com.retrip.trip.domain.exception.common.ErrorCode; + +public class TripUpdateFailedException extends BusinessException { + private static final ErrorCode errorCode = ErrorCode.TRIP_UPDATE_FAIL; + + public TripUpdateFailedException() { + super(errorCode); + } +} diff --git a/src/main/java/com/retrip/trip/domain/exception/common/ErrorCode.java b/src/main/java/com/retrip/trip/domain/exception/common/ErrorCode.java index 7b2e2d6..50bf8ba 100644 --- a/src/main/java/com/retrip/trip/domain/exception/common/ErrorCode.java +++ b/src/main/java/com/retrip/trip/domain/exception/common/ErrorCode.java @@ -15,7 +15,7 @@ public enum ErrorCode { ILLEGAL_ARGUMENT(BAD_REQUEST, "Common-006", "Illegal argument"), TRIP_NOT_FOUND(BAD_REQUEST, "Trip-001", "트립 엔티티를 찾을 수 없습니다."), - PERIOD_UPDATE_FAIL(INTERNAL_SERVER_ERROR, "Trip-002", "여행 일정을 변경할 수 없습니다."), + PERIOD_UPDATE_FAIL(BAD_REQUEST, "Trip-002", "여행 일정을 변경할 수 없습니다."), NOT_TRIP_LEADER(BAD_REQUEST, "Trip-003", "여행 리더가 아니면 접근할 수 없습니다."), TRIP_INVITATION_DUPLICATE(BAD_REQUEST, "Trip-004", "사용자를 여행에 중복 초대할 수 없습니다."), MEMBER_IS_NOT_LEADER(BAD_REQUEST, "Trip-005", "여행 리더가 아니면 접근할 수 없습니다."), @@ -39,8 +39,8 @@ public enum ErrorCode { TRIP_PASSWORD_INVALID(BAD_REQUEST, "Trip-023", "비공개 여행 비밀번호의 길이가 적절하지 않습니다."), TRIP_DEMAND_NOT_ALLOWED(BAD_REQUEST, "Trip-024", "이미 참여를 요청했거나 참여 중인 여행이므로 참여 요청을 다시 할 수 없습니다."), TRIP_DEMAND_STATUS_NOT_PENDING(BAD_REQUEST, "Trip-025", "현재 참여 요청 상태가 ‘대기’가 아니므로 해당 요청을 수행할 수 없습니다."), - INVALID_MAX_PARTICIPANTS_VALUE(BAD_REQUEST,"Trip-026","최대 참여 인원은 1명 이상이어야 합니다."), - MAX_PARTICIPANTS_LESS_THAN_CURRENT(BAD_REQUEST,"Trip-027","현재 참여 인원보다 적은 수로 변경할 수 없습니다."), + INVALID_MAX_PARTICIPANTS_VALUE(BAD_REQUEST, "Trip-026", "최대 참여 인원은 1명 이상이어야 합니다."), + MAX_PARTICIPANTS_LESS_THAN_CURRENT(BAD_REQUEST, "Trip-027", "현재 참여 인원보다 적은 수로 변경할 수 없습니다."), INVALID_HASHTAG_LENGTH(BAD_REQUEST, "Trip-028", "HashTag는 1~10자 사이여야 합니다."), PRIVATE_TRIP_PASSWORD_REQUIRED(BAD_REQUEST, "Trip-029", "비공개 여행은 비밀번호를 반드시 입력해야 합니다."), TRIP_DAY_MUST_BE_POSITIVE(BAD_REQUEST, "Trip-030", "여행 일차는 1보다 작을 수 없습니다."), @@ -58,6 +58,7 @@ public enum ErrorCode { TRIP_PASSWORD_MISMATCH(BAD_REQUEST, "Trip-042", "여행 비밀번호가 일치하지 않습니다."), VOTE_MODIFY_FORBIDDEN(BAD_REQUEST, "Trip-043", "투표를 만든 사람이 아니면 수정, 종료, 삭제 할 수 없습니다."), EXTENSION_NOT_FOUND(BAD_REQUEST, "Trip-044", "지원하지 않는 이미지 확장자입니다."), + TRIP_UPDATE_FAIL(BAD_REQUEST, "Trip-045", "여행을 수정할 수 없습니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/retrip/trip/infra/adapter/in/presentation/rest/TripController.java b/src/main/java/com/retrip/trip/infra/adapter/in/presentation/rest/TripController.java index 07a170c..2c7370d 100644 --- a/src/main/java/com/retrip/trip/infra/adapter/in/presentation/rest/TripController.java +++ b/src/main/java/com/retrip/trip/infra/adapter/in/presentation/rest/TripController.java @@ -61,6 +61,19 @@ public ApiResponse createTrip( return ApiResponse.created(trip); } + @Operation( + summary = "여행 수정", + description = "여행을 수정하는 API" + ) + @PutMapping("/{tripId}") + public ApiResponse updateTrip( + @WithUserContext UserContext userContext, + @PathVariable UUID tripId, + @RequestBody TripUpdateRequest request) { + TripUpdateResponse trip = tripManageUseCase.updateTrip(userContext.memberId(), tripId, request); + return ApiResponse.ok(trip); + } + @Operation( summary = "일정이 포함된 여행 생성", description = "일정이 포함된 여행을 생성하는 API -> 이거는 사용하는지 확인해봐야함 일정을 별도로 생기는거로 바뀌었던 거 같아서" @@ -79,7 +92,7 @@ public ApiResponse createTripWithItineraries( description = "여행 공개 여부를 변경하는 API" ) @ApiErrorCodeExamples({TRIP_NOT_FOUND, PRIVATE_TRIP_PASSWORD_REQUIRED, TRIP_PASSWORD_INVALID}) - @PutMapping("/{tripId}") + @PutMapping("/open/{tripId}") public ApiResponse updateTripVisibility( @PathVariable UUID tripId, @RequestBody TripUpdateVisibilityRequest request) { TripUpdateVisibilityResponse trip = tripManageUseCase.updateTripVisibility(tripId, request); diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index b5179de..0764d5c 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -44,13 +44,14 @@ VALUES (random_uuid(), x'11111111111111111111111111111111', x'999999999999999999 -- 방장(HOST) 데이터 INSERT INTO trip_participant (id, trip_id, member_id, status, role, created_at) -VALUES (random_uuid(), x'11111111111111111111111111111111', x'99999999999999999999999999999993', 1, 'HOST', now()); +VALUES (random_uuid(), x'11111111111111111111111111111111', x'99999999999999999999999999999993', 1, 'LEADER', now()); -- 일반 참여자(MEMBER) 데이터 (Member ID를 다르게 설정) INSERT INTO trip_participant (id, trip_id, member_id, status, role, created_at) -VALUES (random_uuid(), x'11111111111111111111111111111112', x'99999999999999999999999999999991', 1, 'HOST', now()); +VALUES (random_uuid(), x'11111111111111111111111111111112', x'99999999999999999999999999999991', 1, 'LEADER', now()); INSERT INTO trip_participant (id, trip_id, member_id, status, role, created_at) -VALUES (random_uuid(), x'11111111111111111111111111111112', x'99999999999999999999999999999992', 1, 'MEMBER', now()); +VALUES (random_uuid(), x'11111111111111111111111111111112', x'99999999999999999999999999999992', 1, 'PARTICIPANT', + now()); -- 7. Vote (투표 생성) INSERT INTO vote (id, trip_id, title, description, status, version, max_selections, anonymous, allow_add_option,