From 7b38525b6a2b7229034b8281efbda324f8176954 Mon Sep 17 00:00:00 2001 From: finger9999 <161580182+finger9999@users.noreply.github.com> Date: Sun, 6 Jul 2025 05:34:49 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20auth=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/umc/auth/controller/AuthController.java | 4 +++- .../umc/domain/review/controller/ReviewController.java | 7 ++++++- src/main/java/com/umc/global/exception/ErrorCode.java | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/umc/auth/controller/AuthController.java b/src/main/java/com/umc/auth/controller/AuthController.java index 1232e8b..f65e408 100644 --- a/src/main/java/com/umc/auth/controller/AuthController.java +++ b/src/main/java/com/umc/auth/controller/AuthController.java @@ -6,6 +6,8 @@ import com.umc.common.response.ApiResponse; 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 io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -41,7 +43,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..c804808 100644 --- a/src/main/java/com/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/umc/domain/review/controller/ReviewController.java @@ -3,13 +3,18 @@ import com.umc.auth.Jwt.JwtProvider; import com.umc.auth.util.JwtUtil; import com.umc.common.response.ApiResponse; +import com.umc.domain.perfume.dto.PerfumeResponseDto; import com.umc.domain.review.dto.ReviewRequestDTO; 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.exception.BusinessException; import com.umc.global.exception.ErrorCode; import io.jsonwebtoken.JwtException; import io.swagger.v3.oas.annotations.Operation; +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.security.SecurityRequirement; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @@ -58,7 +63,7 @@ public ResponseEntity>> getMyRev log.info("내 리뷰 조회 요청 - Authorization: {}", authorization); if (authorization == null || authorization.trim().isEmpty()) { - throw new RuntimeException("Authorization 헤더가 없습니다. 헤더를 확인해주세요."); + throw new BusinessException(ErrorCode.TOKEN_INVALID); } User user = jwtUtil.getUserFromHeader(authorization); diff --git a/src/main/java/com/umc/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index 28021e5..0bfb70d 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -17,14 +17,14 @@ public enum ErrorCode { VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "VALIDATION_001", "입력값 검증에 실패했습니다."), REQUIRED_FIELD_MISSING(HttpStatus.BAD_REQUEST, "VALIDATION_002", "필수 필드가 누락되었습니다."), - // 로그인 관련 에러 - DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "BUSINESS_002", "이미 존재하는 닉네임입니다. 비밀번호를 다시 입력해주세요."), - // 토큰 관련 에러 - TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "AUTH_001", "유효하지 않은 토큰입니다."), + TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN_4001", "유효하지 않은 토큰입니다."), + + // 로그인 관련 에러 + DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "LOGIN_4001", "이미 존재하는 닉네임입니다. 비밀번호를 다시 입력해주세요."), // 향수 관련 에러 - PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_001", "해당 향수를 찾을 수 없습니다."); + PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_4001", "해당 향수를 찾을 수 없습니다."); // TODO: 비즈니스 로직 개발하면서 필요한 에러코드들 추가 From f11527b6cc397172322e23d5a7fa40eec44e7247 Mon Sep 17 00:00:00 2001 From: finger9999 <161580182+finger9999@users.noreply.github.com> Date: Sun, 6 Jul 2025 06:45:22 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 28 +++++++++++++++---- .../domain/review/service/ReviewService.java | 8 +++++- .../com/umc/global/exception/ErrorCode.java | 15 +++++++--- 3 files changed, 40 insertions(+), 11 deletions(-) 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 c804808..bd8ec29 100644 --- a/src/main/java/com/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/umc/domain/review/controller/ReviewController.java @@ -3,23 +3,20 @@ import com.umc.auth.Jwt.JwtProvider; import com.umc.auth.util.JwtUtil; import com.umc.common.response.ApiResponse; -import com.umc.domain.perfume.dto.PerfumeResponseDto; import com.umc.domain.review.dto.ReviewRequestDTO; 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.exception.BusinessException; +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.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; 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; @@ -39,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, @@ -58,12 +63,17 @@ 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); if (authorization == null || authorization.trim().isEmpty()) { - throw new BusinessException(ErrorCode.TOKEN_INVALID); + throw new RuntimeException("Authorization 헤더가 없습니다. 헤더를 확인해주세요."); } User user = jwtUtil.getUserFromHeader(authorization); @@ -73,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/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index 0bfb70d..bcb6cfb 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -17,14 +17,21 @@ public enum ErrorCode { VALIDATION_ERROR(HttpStatus.BAD_REQUEST, "VALIDATION_001", "입력값 검증에 실패했습니다."), REQUIRED_FIELD_MISSING(HttpStatus.BAD_REQUEST, "VALIDATION_002", "필수 필드가 누락되었습니다."), - // 토큰 관련 에러 - TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN_4001", "유효하지 않은 토큰입니다."), - // 로그인 관련 에러 DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "LOGIN_4001", "이미 존재하는 닉네임입니다. 비밀번호를 다시 입력해주세요."), + //토큰 관련 에러 + TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN_4001", "토큰이 유효하지 않습니다."), + // 향수 관련 에러 - PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_4001", "해당 향수를 찾을 수 없습니다."); + 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: 비즈니스 로직 개발하면서 필요한 에러코드들 추가 From aac73f63ac32cdcacd9442ff6269c35452ea252f Mon Sep 17 00:00:00 2001 From: finger9999 <161580182+finger9999@users.noreply.github.com> Date: Sun, 6 Jul 2025 06:48:30 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/umc/auth/controller/AuthController.java | 6 ++++++ src/main/java/com/umc/global/exception/ErrorCode.java | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/umc/auth/controller/AuthController.java b/src/main/java/com/umc/auth/controller/AuthController.java index f65e408..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,7 @@ 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; @@ -29,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()) diff --git a/src/main/java/com/umc/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index bcb6cfb..a77bd60 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -19,6 +19,8 @@ public enum ErrorCode { // 로그인 관련 에러 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, "TOKEN_4001", "토큰이 유효하지 않습니다."), From 58e59ae45b5125cfa1ea393ea4e3627c4bf0d775 Mon Sep 17 00:00:00 2001 From: Baguette-bbang Date: Sun, 6 Jul 2025 06:53:29 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix=20:=20=EA=B3=B5=EB=B0=A9=20api=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=9E=98=ED=95=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shop/controller/ShopController.java | 48 ++++++++++++++++++- .../umc/domain/shop/dto/ShopResponseDto.java | 17 +++++++ 2 files changed, 63 insertions(+), 2 deletions(-) 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) {