diff --git a/src/main/java/com/umc/auth/controller/AuthController.java b/src/main/java/com/umc/auth/controller/AuthController.java index 1232e8b..c1c265c 100644 --- a/src/main/java/com/umc/auth/controller/AuthController.java +++ b/src/main/java/com/umc/auth/controller/AuthController.java @@ -6,6 +6,9 @@ import com.umc.common.response.ApiResponse; import com.umc.domain.user.entity.User; import com.umc.domain.user.repository.UserRepository; +import com.umc.global.config.SwaggerConfig; +import com.umc.global.exception.BusinessException; +import com.umc.global.exception.ErrorCode; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -27,6 +30,11 @@ public class AuthController { private final JwtProvider jwtProvider; @Operation(summary = "로그인 (회원가입 없음)", description = "닉네임 + 비밀번호로 로그인 요청. 닉네임이 존재하지 않으면 자동으로 유저 생성 후 로그인합니다. 기존에 등록된 닉네임인 경우 비밀번호 검증 후 로그인합니다.") + @SwaggerConfig.ApiErrorExamples({ + ErrorCode.DUPLICATE_NICKNAME, + ErrorCode.LOGIN_NICKNAME_EMPTY, + ErrorCode.LOGIN_PASSWORD_EMPTY + }) @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody LoginRequest request) { User user = userRepository.findByNickname(request.nickname()) @@ -41,7 +49,7 @@ public ResponseEntity> login(@Valid @RequestBody Logi // 기존 유저인 경우 비밀번호 검증 if (!passwordEncoder.matches(request.password(), user.getPassword())) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + throw new BusinessException(ErrorCode.DUPLICATE_NICKNAME); } // JWT 발급 diff --git a/src/main/java/com/umc/domain/review/controller/ReviewController.java b/src/main/java/com/umc/domain/review/controller/ReviewController.java index 5da907f..bd8ec29 100644 --- a/src/main/java/com/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/umc/domain/review/controller/ReviewController.java @@ -7,14 +7,16 @@ import com.umc.domain.review.dto.ReviewResponseDTO; import com.umc.domain.review.service.ReviewService; import com.umc.domain.user.entity.User; +import com.umc.global.config.SwaggerConfig; import com.umc.global.exception.ErrorCode; import io.jsonwebtoken.JwtException; -import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -34,6 +36,14 @@ public class ReviewController { description = "특정 향수에 대한 리뷰를 작성합니다.", security = @SecurityRequirement(name = "bearerAuth") ) + @SwaggerConfig.ApiErrorExamples({ + ErrorCode.TOKEN_INVALID, + ErrorCode.PERFUME_NOT_FOUND, + ErrorCode.USER_NOT_FOUND, + ErrorCode.PERFUME_INVALID_INPUT_VALUE, + ErrorCode.REVIEW_DESCRIPTION_EMPTY, + ErrorCode.INTERNAL_SERVER_ERROR + }) public ResponseEntity> createReview( @PathVariable Long perfumeId, @RequestBody ReviewRequestDTO.CreateReviewRequestDTO request, @@ -53,6 +63,11 @@ public ResponseEntity> cre @GetMapping("/me") @Operation(summary = "내가 작성한 리뷰 목록 조회", security = @SecurityRequirement(name = "bearerAuth")) + @SwaggerConfig.ApiErrorExamples({ + ErrorCode.TOKEN_INVALID, + ErrorCode.USER_NOT_FOUND, + ErrorCode.INTERNAL_SERVER_ERROR + }) public ResponseEntity>> getMyReviews(HttpServletRequest httpRequest) { String authorization = httpRequest.getHeader("Authorization"); log.info("내 리뷰 조회 요청 - Authorization: {}", authorization); @@ -68,6 +83,12 @@ public ResponseEntity>> getMyRev @GetMapping("/{perfumeId}") @Operation(summary = "특정 향수 리뷰 목록 조회") + @SwaggerConfig.ApiErrorExamples({ + ErrorCode.PERFUME_NOT_FOUND, + ErrorCode.USER_NOT_FOUND, + ErrorCode.PERFUME_INVALID_INPUT_VALUE, + ErrorCode.INTERNAL_SERVER_ERROR + }) public ResponseEntity>> getReviewsByPerfume( @PathVariable Long perfumeId) { diff --git a/src/main/java/com/umc/domain/review/service/ReviewService.java b/src/main/java/com/umc/domain/review/service/ReviewService.java index a1b8e0d..04caa1b 100644 --- a/src/main/java/com/umc/domain/review/service/ReviewService.java +++ b/src/main/java/com/umc/domain/review/service/ReviewService.java @@ -9,6 +9,8 @@ import com.umc.domain.review.repository.ReviewRepository; import com.umc.domain.user.entity.User; import com.umc.domain.user.repository.UserRepository; +import com.umc.global.exception.BusinessException; +import com.umc.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,7 +29,11 @@ public class ReviewService { @Transactional public ReviewResponseDTO.CreateReviewReponseDTO createReview(Long perfumeId, Long userId, ReviewRequestDTO.CreateReviewRequestDTO request) { User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + if (request.getDescription() == null || request.getDescription().trim().isEmpty()) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "리뷰 내용은 비어 있을 수 없습니다."); + } Review review = ReviewConverter.toEntity(perfumeId, userId, request); Review saved = reviewRepository.save(review); diff --git a/src/main/java/com/umc/domain/shop/controller/ShopController.java b/src/main/java/com/umc/domain/shop/controller/ShopController.java index c92191f..9573a5e 100644 --- a/src/main/java/com/umc/domain/shop/controller/ShopController.java +++ b/src/main/java/com/umc/domain/shop/controller/ShopController.java @@ -1,8 +1,16 @@ package com.umc.domain.shop.controller; +import com.umc.common.response.ApiResponse; import com.umc.domain.shop.dto.ShopResponseDto; import com.umc.domain.shop.service.ShopService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -10,15 +18,51 @@ @RestController @RequestMapping("/api/shops") @RequiredArgsConstructor +@Slf4j +@Tag(name = "매장 API", description = "매장 정보 조회 API") public class ShopController { private final ShopService shopService; @GetMapping("/nearby") - public List getNearbyShops( + @Operation( + summary = "근처 매장 조회", + description = "현재 위치(위도, 경도)를 기준으로 근처 매장 목록을 조회합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "근처 매장 조회 성공", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청 (잘못된 위도/경도 값)" + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "서버 오류" + ) + }) + public ApiResponse> getNearbyShops( + @Parameter(description = "위도 (latitude)", required = true, example = "37.5665") @RequestParam double lat, + + @Parameter(description = "경도 (longitude)", required = true, example = "126.9780") @RequestParam double lng ) { - return shopService.findNearbyShops(lat, lng); + try { + log.info("근처 매장 조회 요청 - lat: {}, lng: {}", lat, lng); + + List response = shopService.findNearbyShops(lat, lng); + + log.info("근처 매장 조회 성공 - 매장 개수: {}", response.size()); + + return ApiResponse.success("근처 매장이 성공적으로 조회되었습니다.", response); + + } catch (Exception e) { + log.error("근처 매장 조회 실패 - 오류: {}", e.getMessage()); + throw new RuntimeException("근처 매장 조회 중 오류가 발생했습니다."); + } } } diff --git a/src/main/java/com/umc/domain/shop/dto/ShopResponseDto.java b/src/main/java/com/umc/domain/shop/dto/ShopResponseDto.java index b69eecf..b6ad271 100644 --- a/src/main/java/com/umc/domain/shop/dto/ShopResponseDto.java +++ b/src/main/java/com/umc/domain/shop/dto/ShopResponseDto.java @@ -1,20 +1,37 @@ package com.umc.domain.shop.dto; import com.umc.domain.shop.entity.Shop; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; @Getter @Builder +@Schema(description = "매장 응답") public class ShopResponseDto { + @Schema(description = "매장 ID", example = "1") private Long id; + + @Schema(description = "매장명", example = "향수 전문점") private String title; + + @Schema(description = "연락처", example = "02-1234-5678") private String contact; + + @Schema(description = "주소", example = "서울특별시 강남구 테헤란로 123") private String address; + + @Schema(description = "매장 URL", example = "https://example.com/shop") private String shopUrl; + + @Schema(description = "매장 설명", example = "다양한 향수를 취급하는 전문 매장입니다.") private String description; + + @Schema(description = "위도", example = "37.5665") private Double latitude; + + @Schema(description = "경도", example = "126.9780") private Double longitude; public static ShopResponseDto from(Shop shop) { diff --git a/src/main/java/com/umc/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index 28021e5..a77bd60 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -18,13 +18,22 @@ public enum ErrorCode { REQUIRED_FIELD_MISSING(HttpStatus.BAD_REQUEST, "VALIDATION_002", "필수 필드가 누락되었습니다."), // 로그인 관련 에러 - DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "BUSINESS_002", "이미 존재하는 닉네임입니다. 비밀번호를 다시 입력해주세요."), + DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "LOGIN_4001", "이미 존재하는 닉네임입니다. 비밀번호를 다시 입력해주세요."), + LOGIN_NICKNAME_EMPTY(HttpStatus.BAD_REQUEST, "LOGIN_4002", "닉네임을 입력해주세요."), + LOGIN_PASSWORD_EMPTY(HttpStatus.BAD_REQUEST, "LOGIN_4002", "비밀번호를 입력해주세요."), - // 토큰 관련 에러 - TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "AUTH_001", "유효하지 않은 토큰입니다."), + //토큰 관련 에러 + TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN_4001", "토큰이 유효하지 않습니다."), // 향수 관련 에러 - PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_001", "해당 향수를 찾을 수 없습니다."); + PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_4001", "해당 향수를 찾을 수 없습니다."), + PERFUME_INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "PERFUME_4002", "perfumeId가 잘못된 형식입니다."), + + // 리뷰 관련 에러 + REVIEW_DESCRIPTION_EMPTY(HttpStatus.BAD_REQUEST, "REVIEW_4001", "리뷰 내용은 비어 있을 수 없습니다."), + + // 유저 관련 에러 + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_4001", "해당 사용자를 찾을 수 없습니다."); // TODO: 비즈니스 로직 개발하면서 필요한 에러코드들 추가