diff --git a/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java b/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java index ae589b4..ad552a3 100644 --- a/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java +++ b/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java @@ -7,7 +7,7 @@ @Getter @RequiredArgsConstructor public enum AuthErrorCase implements ErrorCase { - UNAUTHORIZED(401, 1001, "인증 정보가 없거나 userId를 추출할 수 없습니다."); + UNAUTHORIZED(401, 7001, "인증 정보가 없거나 userId를 추출할 수 없습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java index 8401614..f949312 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -3,9 +3,7 @@ import com.wayble.server.auth.resolver.CurrentUser; import com.wayble.server.common.response.CommonResponse; -import com.wayble.server.user.dto.UserPlaceRemoveRequestDto; -import com.wayble.server.user.dto.UserPlaceRequestDto; -import com.wayble.server.user.dto.UserPlaceSummaryDto; +import com.wayble.server.user.dto.*; import com.wayble.server.user.service.UserPlaceService; import com.wayble.server.wayblezone.dto.WaybleZoneListResponseDto; import io.swagger.v3.oas.annotations.Operation; @@ -27,19 +25,25 @@ public class UserPlaceController { private final UserPlaceService userPlaceService; @PostMapping - @Operation(summary = "유저 장소 저장", description = "유저가 웨이블존을 장소로 저장합니다.") + @Operation(summary = "웨이블존 저장할 리스트 생성", + description = "제목과 색상을 받아 웨이블존을 저장할 리스트를 생성합니다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "장소 저장 성공"), - @ApiResponse(responseCode = "400", description = "이미 저장한 장소입니다."), - @ApiResponse(responseCode = "404", description = "해당 유저 또는 웨이블존이 존재하지 않음"), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.") + @ApiResponse(responseCode = "200", description = "리스트 생성 성공"), + @ApiResponse(responseCode = "400", description = "동일한 리스트명이 이미 존재") }) - public CommonResponse saveUserPlace( + public CommonResponse createPlaceList( @Parameter(hidden = true) @CurrentUser Long userId, - @RequestBody @Valid UserPlaceRequestDto request + @RequestBody @Valid UserPlaceCreateRequestDto request ) { - userPlaceService.saveUserPlace(userId, request); // userId 파라미터로 넘김 - return CommonResponse.success("장소가 저장되었습니다."); + Long placeId = userPlaceService.createPlaceList(userId, request); + return CommonResponse.success( + UserPlaceCreateResponseDto.builder() + .placeId(placeId) + .title(request.title()) + .color((request.color() == null || request.color().isBlank()) ? "GRAY" : request.color()) + .message("리스트가 생성되었습니다.") + .build() + ); } @GetMapping @@ -57,6 +61,38 @@ public CommonResponse> getMyPlaceSummaries( return CommonResponse.success(summaries); } + @DeleteMapping + @Operation( + summary = "장소에서 웨이블존 제거", + description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "제거 성공"), + @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.") + }) + public CommonResponse removeZoneFromPlace( + @Parameter(hidden = true) @CurrentUser Long userId, + @RequestBody @Valid UserPlaceRemoveRequestDto request + ) { + userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); + return CommonResponse.success("제거되었습니다."); + } + + @PostMapping("/zones") + @Operation(summary = "리스트에 웨이블존 추가", + description = "placeId와 waybleZoneId 배열을 받아 여러 웨이블존을 리스트에 추가합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "웨이블존 추가 성공"), + @ApiResponse(responseCode = "404", description = "유저/리스트/웨이블존을 찾을 수 없음") + }) + public CommonResponse addZonesToPlace( + @Parameter(hidden = true) @CurrentUser Long userId, + @RequestBody @Valid UserPlaceAddZonesRequestDto request + ) { + userPlaceService.addZonesToPlace(userId, request.placeId(), request.waybleZoneIds()); + return CommonResponse.success("리스트에 웨이블존이 추가되었습니다."); + } @GetMapping("/zones") @Operation(summary = "특정 장소 내 웨이블존 목록 조회(페이징)", @@ -75,22 +111,4 @@ public CommonResponse> getZonesInPlace( Page zones = userPlaceService.getZonesInPlace(userId, placeId, page, size); return CommonResponse.success(zones); } - - @DeleteMapping - @Operation( - summary = "장소에서 웨이블존 제거", - description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "제거 성공"), - @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.") - }) - public CommonResponse removeZoneFromPlace( - @Parameter(hidden = true) @CurrentUser Long userId, - @RequestBody @Valid UserPlaceRemoveRequestDto request - ) { - userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); - return CommonResponse.success("제거되었습니다."); - } } diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java new file mode 100644 index 0000000..bd1524c --- /dev/null +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java @@ -0,0 +1,10 @@ +package com.wayble.server.user.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record UserPlaceAddZonesRequestDto( + @NotNull Long placeId, + @NotEmpty List waybleZoneIds +) {} diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java new file mode 100644 index 0000000..816c20d --- /dev/null +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java @@ -0,0 +1,8 @@ +package com.wayble.server.user.dto; + +import jakarta.validation.constraints.NotBlank; + +public record UserPlaceCreateRequestDto( + @NotBlank(message = "제목은 필수입니다.") String title, + String color +) {} diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java new file mode 100644 index 0000000..a8be930 --- /dev/null +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java @@ -0,0 +1,11 @@ +package com.wayble.server.user.dto; + +import lombok.Builder; + +@Builder +public record UserPlaceCreateResponseDto( + Long placeId, + String title, + String color, + String message +) {} diff --git a/src/main/java/com/wayble/server/user/exception/UserErrorCase.java b/src/main/java/com/wayble/server/user/exception/UserErrorCase.java index 031808b..1d28320 100644 --- a/src/main/java/com/wayble/server/user/exception/UserErrorCase.java +++ b/src/main/java/com/wayble/server/user/exception/UserErrorCase.java @@ -22,7 +22,8 @@ public enum UserErrorCase implements ErrorCase { NICKNAME_REQUIRED(400, 1012,"nickname 파라미터는 필수입니다."), NICKNAME_DUPLICATED(409,1013, "이미 사용 중인 닉네임입니다."), PLACE_NOT_FOUND(404, 1014, "저장된 장소를 찾을 수 없습니다."), - PLACE_MAPPING_NOT_FOUND(404, 1015, "해당 장소에 해당 웨이블존이 없습니다."); + PLACE_MAPPING_NOT_FOUND(404, 1015, "해당 장소에 해당 웨이블존이 없습니다."), + PLACE_TITLE_DUPLICATED(400, 1016, "동일한 이름의 리스트가 이미 있습니다."); private final Integer httpStatusCode; private final Integer errorCode; diff --git a/src/main/java/com/wayble/server/user/service/UserPlaceService.java b/src/main/java/com/wayble/server/user/service/UserPlaceService.java index 194f852..c5292a8 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -2,6 +2,7 @@ import com.wayble.server.common.exception.ApplicationException; +import com.wayble.server.user.dto.UserPlaceCreateRequestDto; import com.wayble.server.user.dto.UserPlaceRequestDto; import com.wayble.server.user.dto.UserPlaceSummaryDto; import com.wayble.server.user.entity.User; @@ -22,7 +23,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor @@ -34,43 +37,57 @@ public class UserPlaceService { private final UserPlaceWaybleZoneMappingRepository mappingRepository; @Transactional - public void saveUserPlace(Long userId, UserPlaceRequestDto request) { - // 유저 존재 확인 + public Long createPlaceList(Long userId, UserPlaceCreateRequestDto request) { User user = userRepository.findById(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); - // 웨이블존 존재 확인 - WaybleZone waybleZone = waybleZoneRepository.findById(request.waybleZoneId()) - .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND)); + userPlaceRepository.findByUser_IdAndTitle(userId, request.title()) + .ifPresent(p -> { throw new ApplicationException(UserErrorCase.PLACE_TITLE_DUPLICATED); }); - // 중복 저장 확인 - boolean duplicated = mappingRepository.existsByUserPlace_User_IdAndWaybleZone_Id(userId, request.waybleZoneId()); - if (duplicated) { - throw new ApplicationException(UserErrorCase.PLACE_ALREADY_SAVED); + String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color(); + + UserPlace saved = userPlaceRepository.save( + UserPlace.builder() + .title(request.title()) + .color(color) + .user(user) + .savedCount(0) + .build() + ); + return saved.getId(); + } + + @Transactional + public void addZonesToPlace(Long userId, Long placeId, List waybleZoneIds) { + UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND)); + + Set uniqueIds = new LinkedHashSet<>(waybleZoneIds); + + int added = 0; + for (Long zoneId : uniqueIds) { + WaybleZone zone = waybleZoneRepository.findById(zoneId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND)); + + boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, zoneId); + if (exists) continue; + + mappingRepository.save(UserPlaceWaybleZoneMapping.builder() + .userPlace(place) + .waybleZone(zone) + .build()); + + place.increaseCount(); + zone.addLikes(1); + waybleZoneRepository.save(zone); + + added++; } - String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color(); - UserPlace userPlace = userPlaceRepository.findByUser_IdAndTitle(userId, request.title()) - .orElseGet(() -> userPlaceRepository.save( - UserPlace.builder() - .title(request.title()) - .color(color) - .user(user) - .build() - )); - - mappingRepository.save(UserPlaceWaybleZoneMapping.builder() - .userPlace(userPlace) - .waybleZone(waybleZone) - .build()); - - userPlace.increaseCount(); - userPlaceRepository.save(userPlace); - - waybleZone.addLikes(1); - waybleZoneRepository.save(waybleZone); + if (added > 0) userPlaceRepository.save(place); } + @Transactional(readOnly = true) public List getMyPlaceSummaries(Long userId, String sort) { userRepository.findById(userId)