From ac94264c84b6453f89602ea9254120280bc2c464 Mon Sep 17 00:00:00 2001 From: finger9999 <161580182+finger9999@users.noreply.github.com> Date: Sun, 6 Jul 2025 08:12:49 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=99=84=EC=84=B1!!!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/auth/controller/AuthController.java | 9 +++++++ .../java/com/umc/auth/dto/LoginRequest.java | 2 +- .../review/controller/ReviewController.java | 10 +++++--- .../domain/review/service/ReviewService.java | 25 +++++++++++-------- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/umc/auth/controller/AuthController.java b/src/main/java/com/umc/auth/controller/AuthController.java index c1c265c..f7c7cea 100644 --- a/src/main/java/com/umc/auth/controller/AuthController.java +++ b/src/main/java/com/umc/auth/controller/AuthController.java @@ -37,6 +37,15 @@ public class AuthController { }) @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody LoginRequest request) { + + if (request.nickname() == null || request.nickname().trim().isEmpty()) { + throw new BusinessException(ErrorCode.LOGIN_NICKNAME_EMPTY); + } + + if (request.password() == null || request.password().trim().isEmpty()) { + throw new BusinessException(ErrorCode.LOGIN_PASSWORD_EMPTY); + } + User user = userRepository.findByNickname(request.nickname()) .orElseGet(() -> { // 없으면 자동 회원가입 diff --git a/src/main/java/com/umc/auth/dto/LoginRequest.java b/src/main/java/com/umc/auth/dto/LoginRequest.java index 6783a2e..3ea512c 100644 --- a/src/main/java/com/umc/auth/dto/LoginRequest.java +++ b/src/main/java/com/umc/auth/dto/LoginRequest.java @@ -3,4 +3,4 @@ import jakarta.validation.constraints.NotBlank; // 요청 DTO -public record LoginRequest(@NotBlank String nickname, @NotBlank String password) {} \ No newline at end of file +public record LoginRequest(String nickname, String password) {} \ No newline at end of file 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 bd8ec29..4e0fddb 100644 --- a/src/main/java/com/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/umc/domain/review/controller/ReviewController.java @@ -3,11 +3,14 @@ import com.umc.auth.Jwt.JwtProvider; import com.umc.auth.util.JwtUtil; import com.umc.common.response.ApiResponse; +import com.umc.domain.perfume.entity.Perfume; +import com.umc.domain.perfume.repository.PerfumeRepository; 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.config.SwaggerConfig; +import com.umc.global.exception.BusinessException; import com.umc.global.exception.ErrorCode; import io.jsonwebtoken.JwtException; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -29,6 +32,7 @@ public class ReviewController { private final ReviewService reviewService; private final JwtUtil jwtUtil; + private final PerfumeRepository perfumeRepository; @PostMapping("/{perfumeId}") @Operation( @@ -39,7 +43,6 @@ public class ReviewController { @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 @@ -53,10 +56,11 @@ public ResponseEntity> cre log.info("리뷰 생성 요청 - perfumeId: {}, Authorization: {}", perfumeId, authorization); if (authorization == null || authorization.trim().isEmpty()) { - throw new RuntimeException("Authorization 헤더가 없습니다. 헤더를 확인해주세요."); + throw new BusinessException(ErrorCode.TOKEN_INVALID); } User user = jwtUtil.getUserFromHeader(authorization); + ReviewResponseDTO.CreateReviewReponseDTO result = reviewService.createReview(perfumeId, user.getId(), request); return ResponseEntity.ok(ApiResponse.success("리뷰가 성공적으로 등록되었습니다.", result)); } @@ -73,7 +77,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/domain/review/service/ReviewService.java b/src/main/java/com/umc/domain/review/service/ReviewService.java index 04caa1b..94773d2 100644 --- a/src/main/java/com/umc/domain/review/service/ReviewService.java +++ b/src/main/java/com/umc/domain/review/service/ReviewService.java @@ -32,9 +32,12 @@ public ReviewResponseDTO.CreateReviewReponseDTO createReview(Long perfumeId, Lon .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); if (request.getDescription() == null || request.getDescription().trim().isEmpty()) { - throw new BusinessException(ErrorCode.VALIDATION_ERROR, "리뷰 내용은 비어 있을 수 없습니다."); + throw new BusinessException(ErrorCode.REVIEW_DESCRIPTION_EMPTY); } + Perfume perfume = perfumeRepository.findById(perfumeId) + .orElseThrow(() -> new BusinessException(ErrorCode.PERFUME_NOT_FOUND)); + Review review = ReviewConverter.toEntity(perfumeId, userId, request); Review saved = reviewRepository.save(review); @@ -43,24 +46,24 @@ public ReviewResponseDTO.CreateReviewReponseDTO createReview(Long perfumeId, Lon @Transactional(readOnly = true) public List getMyReviews(Long userId) { + // 사용자 검증 + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + // 내가 작성한 리뷰 목록 조회 List reviews = reviewRepository.findByUserId(userId); return reviews.stream() - .map(review -> { - Perfume perfume = perfumeRepository.findById(review.getPerfumeId()) - .orElseThrow(() -> new IllegalArgumentException("해당 향수가 존재하지 않습니다.")); - - ReviewResponseDTO.PerfumeDTO perfumeDTO = ReviewResponseDTO.PerfumeDTO.builder() - .id(perfume.getId()) - .build(); - - return ReviewConverter.toMyReviewDTO(review); - }) + .map(ReviewConverter::toMyReviewDTO) // Perfume 조회 제거 .collect(Collectors.toList()); } @Transactional(readOnly = true) public List getReviewsByPerfumeId(Long perfumeId) { + + perfumeRepository.findById(perfumeId) + .orElseThrow(() -> new BusinessException(ErrorCode.PERFUME_NOT_FOUND)); + List reviews = reviewRepository.findByPerfumeIdOrderByCreatedAtDesc(perfumeId); return reviews.stream().map(review -> { From 81e4047dc35a3176a0957d590b59c48eeafe9ba5 Mon Sep 17 00:00:00 2001 From: Baguette-bbang Date: Sun, 6 Jul 2025 08:22:50 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat=20:=20perpume=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../perfume/controller/PerfumeController.java | 295 ++++++------------ .../perfume/service/PerfumeService.java | 36 ++- .../com/umc/global/exception/ErrorCode.java | 6 + 3 files changed, 125 insertions(+), 212 deletions(-) diff --git a/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java b/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java index ac476e8..0aae46c 100644 --- a/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java +++ b/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java @@ -7,6 +7,9 @@ import com.umc.domain.perfume.service.PerfumeService; import java.util.List; import com.umc.domain.user.entity.User; +import com.umc.global.config.SwaggerConfig.ApiErrorExample; +import com.umc.global.config.SwaggerConfig.ApiErrorExamples; +import com.umc.global.exception.ErrorCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -44,20 +47,16 @@ public class PerfumeController { responseCode = "200", description = "향수 생성 성공", content = @Content(schema = @Schema(implementation = PerfumeResponseDto.class)) - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "잘못된 요청 (파일 형식 오류, 크기 초과 등)" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "401", - description = "인증 실패 (JWT 토큰 오류)" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "500", - description = "서버 오류" ) }) + @ApiErrorExamples({ + ErrorCode.PERFUME_FILE_EMPTY, + ErrorCode.PERFUME_FILE_SIZE_EXCEEDED, + ErrorCode.PERFUME_INVALID_FILE_TYPE, + ErrorCode.PERFUME_CREATION_FAILED, + ErrorCode.TOKEN_INVALID, + ErrorCode.USER_NOT_FOUND + }) public ApiResponse createPerfume( @Parameter(description = "소스 타입 (AUDIO 또는 IMAGE)", required = true) @RequestParam("sourceType") SourceType sourceType, @@ -67,42 +66,21 @@ public ApiResponse createPerfume( HttpServletRequest request) { - try { - // Authorization 헤더 추출 및 검증 - String authorization = request.getHeader("Authorization"); - log.info("향수 생성 요청 - sourceType: {}, fileName: {}, fileSize: {}MB", - sourceType, - file.getOriginalFilename(), - String.format("%.2f", file.getSize() / (1024.0 * 1024.0))); - - if (authorization == null || authorization.trim().isEmpty()) { - log.warn("Authorization 헤더가 없는 요청"); - throw new RuntimeException("Authorization 헤더가 필요합니다. Bearer 토큰을 포함해주세요."); - } - - if (!authorization.startsWith("Bearer ")) { - log.warn("잘못된 Authorization 헤더 형식: {}", authorization); - throw new RuntimeException("Authorization 헤더는 'Bearer ' 형식이어야 합니다."); - } - - // JWT 토큰에서 사용자 정보 추출 - User user = jwtUtil.getUserFromHeader(authorization); - log.info("인증된 사용자: {} (ID: {})", user.getNickname(), user.getId()); - - // 향수 생성 - PerfumeResponseDto response = perfumeService.createPerfume(sourceType, file, user); - - log.info("향수 생성 성공 - 향수 ID: {}, 사용자: {}", response.getId(), user.getNickname()); - - return ApiResponse.success(response); - - } catch (RuntimeException e) { - log.error("향수 생성 실패 - 사용자 오류: {}", e.getMessage()); - throw e; // RuntimeException은 그대로 던져서 GlobalExceptionHandler에서 처리 - } catch (Exception e) { - log.error("향수 생성 실패 - 시스템 오류: ", e); - throw new RuntimeException("향수 생성 중 예상하지 못한 오류가 발생했습니다."); - } + log.info("향수 생성 요청 - sourceType: {}, fileName: {}, fileSize: {}MB", + sourceType, + file.getOriginalFilename(), + String.format("%.2f", file.getSize() / (1024.0 * 1024.0))); + + // JWT 토큰에서 사용자 정보 추출 + User user = jwtUtil.getUserFromHeader(request.getHeader("Authorization")); + log.info("인증된 사용자: {} (ID: {})", user.getNickname(), user.getId()); + + // 향수 생성 + PerfumeResponseDto response = perfumeService.createPerfume(sourceType, file, user); + + log.info("향수 생성 성공 - 향수 ID: {}, 사용자: {}", response.getId(), user.getNickname()); + + return ApiResponse.success(response); } @GetMapping("/{id}") @@ -115,37 +93,25 @@ public ApiResponse createPerfume( responseCode = "200", description = "향수 조회 성공", content = @Content(schema = @Schema(implementation = PerfumeResponseDto.class)) - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "404", - description = "향수를 찾을 수 없음" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "잘못된 ID 형식" ) }) + @ApiErrorExamples({ + ErrorCode.PERFUME_NOT_FOUND, + ErrorCode.PERFUME_INVALID_INPUT_VALUE + }) public ApiResponse getPerfume( @Parameter(description = "향수 ID", required = true) @PathVariable Long id) { - try { - log.info("향수 조회 요청 - id: {}", id); - - PerfumeResponseDto response = perfumeService.getPerfume(id); - - log.info("향수 조회 성공 - id: {}", id); - - return ApiResponse.success(response); - - } catch (RuntimeException e) { - log.error("향수 조회 실패 - ID: {}, 오류: {}", id, e.getMessage()); - throw e; - } catch (Exception e) { - log.error("향수 조회 실패 - 시스템 오류: ", e); - throw new RuntimeException("향수 조회 중 예상하지 못한 오류가 발생했습니다."); - } + log.info("향수 조회 요청 - id: {}", id); + + PerfumeResponseDto response = perfumeService.getPerfume(id); + + log.info("향수 조회 성공 - id: {}", id); + + return ApiResponse.success(response); } + @GetMapping("/user/{userId}") @Operation( @@ -157,37 +123,25 @@ public ApiResponse getPerfume( responseCode = "200", description = "향수 목록 조회 성공", content = @Content(schema = @Schema(implementation = PerfumeResponseDto.class)) - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "404", - description = "사용자를 찾을 수 없음" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "잘못된 사용자 ID 형식" ) }) + @ApiErrorExamples({ + ErrorCode.USER_NOT_FOUND, + ErrorCode.INVALID_INPUT_VALUE + }) public ApiResponse> getUserPerfumes( @Parameter(description = "사용자 ID", required = true) @PathVariable Long userId) { - try { - log.info("사용자 향수 목록 조회 요청 - userId: {}", userId); - - List response = perfumeService.getUserPerfumes(userId); - - log.info("사용자 향수 목록 조회 성공 - userId: {}, 향수 개수: {}", userId, response.size()); - - return ApiResponse.success(response); - - } catch (RuntimeException e) { - log.error("사용자 향수 목록 조회 실패 - userId: {}, 오류: {}", userId, e.getMessage()); - throw e; - } catch (Exception e) { - log.error("사용자 향수 목록 조회 실패 - 시스템 오류: ", e); - throw new RuntimeException("향수 목록 조회 중 예상하지 못한 오류가 발생했습니다."); - } + log.info("사용자 향수 목록 조회 요청 - userId: {}", userId); + + List response = perfumeService.getUserPerfumes(userId); + + log.info("사용자 향수 목록 조회 성공 - userId: {}, 향수 개수: {}", userId, response.size()); + + return ApiResponse.success(response); } + @GetMapping("/my") @Operation( @@ -200,41 +154,23 @@ public ApiResponse> getUserPerfumes( responseCode = "200", description = "내 향수 목록 조회 성공", content = @Content(schema = @Schema(implementation = PerfumeResponseDto.class)) - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "401", - description = "인증 실패" ) }) + @ApiErrorExample(ErrorCode.TOKEN_INVALID) public ApiResponse> getMyPerfumes(HttpServletRequest request) { - try { - // Authorization 헤더 추출 및 검증 - String authorization = request.getHeader("Authorization"); - - if (authorization == null || authorization.trim().isEmpty()) { - throw new RuntimeException("Authorization 헤더가 필요합니다."); - } - - // JWT 토큰에서 사용자 정보 추출 - User user = jwtUtil.getUserFromHeader(authorization); - - log.info("내 향수 목록 조회 요청 - 사용자: {} (ID: {})", user.getNickname(), user.getId()); - - List response = perfumeService.getUserPerfumes(user.getId()); - - log.info("내 향수 목록 조회 성공 - 사용자: {}, 향수 개수: {}", user.getNickname(), response.size()); - - return ApiResponse.success(response); - - } catch (RuntimeException e) { - log.error("내 향수 목록 조회 실패 - 오류: {}", e.getMessage()); - throw e; - } catch (Exception e) { - log.error("내 향수 목록 조회 실패 - 시스템 오류: ", e); - throw new RuntimeException("내 향수 목록 조회 중 예상하지 못한 오류가 발생했습니다."); - } + // JWT 토큰에서 사용자 정보 추출 + User user = jwtUtil.getUserFromHeader(request.getHeader("Authorization")); + + log.info("내 향수 목록 조회 요청 - 사용자: {} (ID: {})", user.getNickname(), user.getId()); + + List response = perfumeService.getUserPerfumes(user.getId()); + + log.info("내 향수 목록 조회 성공 - 사용자: {}, 향수 개수: {}", user.getNickname(), response.size()); + + return ApiResponse.success(response); } + @DeleteMapping("/{id}") @Operation( @@ -246,54 +182,33 @@ public ApiResponse> getMyPerfumes(HttpServletRequest re @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", description = "향수 삭제 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "404", - description = "향수를 찾을 수 없음" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "403", - description = "삭제 권한 없음 (본인이 생성한 향수가 아님)" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "401", - description = "인증 실패" ) }) + @ApiErrorExamples({ + ErrorCode.PERFUME_NOT_FOUND, + ErrorCode.PERFUME_ACCESS_DENIED, + ErrorCode.PERFUME_INVALID_INPUT_VALUE, + ErrorCode.TOKEN_INVALID + }) public ApiResponse deletePerfume( @Parameter(description = "향수 ID", required = true) @PathVariable Long id, HttpServletRequest request) { - try { - log.info("향수 삭제 요청 - id: {}", id); - - // Authorization 헤더 추출 및 검증 - String authorization = request.getHeader("Authorization"); - - if (authorization == null || authorization.trim().isEmpty()) { - throw new RuntimeException("Authorization 헤더가 필요합니다."); - } - - // JWT 토큰에서 사용자 정보 추출 - User user = jwtUtil.getUserFromHeader(authorization); - log.info("향수 삭제 요청 - 향수 ID: {}, 사용자: {} (ID: {})", id, user.getNickname(), user.getId()); - - perfumeService.deletePerfume(id, user); - - log.info("향수 삭제 성공 - 향수 ID: {}, 사용자: {}", id, user.getNickname()); - - return ApiResponse.success("향수가 성공적으로 삭제되었습니다."); - - } catch (RuntimeException e) { - log.error("향수 삭제 실패 - ID: {}, 오류: {}", id, e.getMessage()); - throw e; - } catch (Exception e) { - log.error("향수 삭제 실패 - 시스템 오류: ", e); - throw new RuntimeException("향수 삭제 중 예상하지 못한 오류가 발생했습니다."); - } + log.info("향수 삭제 요청 - id: {}", id); + + // JWT 토큰에서 사용자 정보 추출 + User user = jwtUtil.getUserFromHeader(request.getHeader("Authorization")); + log.info("향수 삭제 요청 - 향수 ID: {}, 사용자: {} (ID: {})", id, user.getNickname(), user.getId()); + + perfumeService.deletePerfume(id, user); + + log.info("향수 삭제 성공 - 향수 ID: {}, 사용자: {}", id, user.getNickname()); + + return ApiResponse.success("향수가 성공적으로 삭제되었습니다."); } + @GetMapping("/recommend") @Operation( @@ -305,41 +220,29 @@ public ApiResponse deletePerfume( responseCode = "200", description = "향수 추천 성공", content = @Content(schema = @Schema(implementation = PerfumeResponseDto.class)) - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "잘못된 요청 (잘못된 sourceType)" ) }) + @ApiErrorExample(ErrorCode.PERFUME_INVALID_SOURCE_TYPE) public ApiResponse> recommendPerfume( @Parameter(description = "소스 타입 (AUDIO 또는 IMAGE)", required = true) @RequestParam("sourceType") String sourceType) { - - try { - log.info("향수 추천 요청 - sourceType: {}", sourceType); - - // sourceType을 내부 enum으로 변환 - SourceType internalSourceType; - if ("AUDIO".equalsIgnoreCase(sourceType)) { - internalSourceType = SourceType.RECOMMEND_AUDIO; - } else if ("IMAGE".equalsIgnoreCase(sourceType)) { - internalSourceType = SourceType.RECOMMEND_IMAGE; - } else { - throw new RuntimeException("잘못된 sourceType입니다. AUDIO 또는 IMAGE만 사용 가능합니다."); - } - - List response = perfumeService.recommendPerfumes(internalSourceType); - - log.info("향수 추천 성공 - sourceType: {}, 추천 개수: {}", sourceType, response.size()); - - return ApiResponse.success(response); - - } catch (RuntimeException e) { - log.error("향수 추천 실패 - sourceType: {}, 오류: {}", sourceType, e.getMessage()); - throw e; - } catch (Exception e) { - log.error("향수 추천 실패 - 시스템 오류: ", e); - throw new RuntimeException("향수 추천 중 예상하지 못한 오류가 발생했습니다."); + + log.info("향수 추천 요청 - sourceType: {}", sourceType); + + // sourceType을 내부 enum으로 변환 + SourceType internalSourceType; + if ("AUDIO".equalsIgnoreCase(sourceType)) { + internalSourceType = SourceType.RECOMMEND_AUDIO; + } else if ("IMAGE".equalsIgnoreCase(sourceType)) { + internalSourceType = SourceType.RECOMMEND_IMAGE; + } else { + throw new RuntimeException("잘못된 sourceType입니다. AUDIO 또는 IMAGE만 사용 가능합니다."); } + + List response = perfumeService.recommendPerfumes(internalSourceType); + + log.info("향수 추천 성공 - sourceType: {}, 추천 개수: {}", sourceType, response.size()); + + return ApiResponse.success(response); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/com/umc/domain/perfume/service/PerfumeService.java b/src/main/java/com/umc/domain/perfume/service/PerfumeService.java index a633f2f..4c0df13 100644 --- a/src/main/java/com/umc/domain/perfume/service/PerfumeService.java +++ b/src/main/java/com/umc/domain/perfume/service/PerfumeService.java @@ -6,6 +6,8 @@ import com.umc.domain.perfume.repository.PerfumeRepository; 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 lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -33,7 +35,7 @@ public PerfumeResponseDto createPerfume(SourceType sourceType, MultipartFile fil try { // 0. 사용자 존재 확인 (Foreign Key 제약 조건 해결) User existingUser = userRepository.findById(user.getId()) - .orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다: " + user.getId())); + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); // 1. 파일 유효성 검증 validateFile(file, sourceType); @@ -67,9 +69,11 @@ public PerfumeResponseDto createPerfume(SourceType sourceType, MultipartFile fil PerfumeResponseDto dto = PerfumeResponseDto.from(savedPerfume); return dto.withClientSourceType(convertToClientSourceType(savedPerfume.getSourceType())); + } catch (BusinessException e) { + throw e; // BusinessException은 그대로 전파 } catch (Exception e) { log.error("향수 생성 중 오류 발생: ", e); - throw new RuntimeException("향수 생성에 실패했습니다: " + e.getMessage()); + throw new BusinessException(ErrorCode.PERFUME_CREATION_FAILED); } } @@ -79,28 +83,28 @@ public PerfumeResponseDto createPerfume(SourceType sourceType, MultipartFile fil private void validateFile(MultipartFile file, SourceType sourceType) { // 파일 존재 검증 if (file == null || file.isEmpty()) { - throw new RuntimeException("파일이 비어있습니다."); + throw new BusinessException(ErrorCode.PERFUME_FILE_EMPTY); } // 파일 크기 검증 long maxSize = sourceType == SourceType.AUDIO ? 25 * 1024 * 1024 : 20 * 1024 * 1024; // 25MB for audio, 20MB for image if (file.getSize() > maxSize) { - throw new RuntimeException("파일 크기가 너무 큽니다. " + (maxSize / (1024 * 1024)) + "MB 이하로 업로드해주세요."); + throw new BusinessException(ErrorCode.PERFUME_FILE_SIZE_EXCEEDED); } // 파일 형식 검증 String contentType = file.getContentType(); if (contentType == null) { - throw new RuntimeException("파일 형식을 확인할 수 없습니다."); + throw new BusinessException(ErrorCode.PERFUME_INVALID_FILE_TYPE); } if (sourceType == SourceType.AUDIO) { if (!contentType.startsWith("audio/")) { - throw new RuntimeException("오디오 파일만 업로드 가능합니다. 현재 파일 형식: " + contentType); + throw new BusinessException(ErrorCode.PERFUME_INVALID_FILE_TYPE); } } else if (sourceType == SourceType.IMAGE) { if (!contentType.startsWith("image/")) { - throw new RuntimeException("이미지 파일만 업로드 가능합니다. 현재 파일 형식: " + contentType); + throw new BusinessException(ErrorCode.PERFUME_INVALID_FILE_TYPE); } } @@ -118,11 +122,11 @@ private void validateFile(MultipartFile file, SourceType sourceType) { @Transactional(readOnly = true) public PerfumeResponseDto getPerfume(Long id) { if (id == null || id <= 0) { - throw new RuntimeException("유효하지 않은 향수 ID입니다: " + id); + throw new BusinessException(ErrorCode.PERFUME_INVALID_INPUT_VALUE); } Perfume perfume = perfumeRepository.findById(id) - .orElseThrow(() -> new RuntimeException("향수를 찾을 수 없습니다: " + id)); + .orElseThrow(() -> new BusinessException(ErrorCode.PERFUME_NOT_FOUND)); log.info("향수 조회 완료 - ID: {}, 사용자: {}", id, perfume.getUser().getNickname()); @@ -137,11 +141,11 @@ public PerfumeResponseDto getPerfume(Long id) { @Transactional(readOnly = true) public List getUserPerfumes(Long userId) { if (userId == null || userId <= 0) { - throw new RuntimeException("유효하지 않은 사용자 ID입니다: " + userId); + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE); } User user = userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다: " + userId)); + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); List perfumes = perfumeRepository.findByUserOrderByCreatedAtDesc(user); @@ -161,15 +165,15 @@ public List getUserPerfumes(Long userId) { */ public void deletePerfume(Long id, User user) { if (id == null || id <= 0) { - throw new RuntimeException("유효하지 않은 향수 ID입니다: " + id); + throw new BusinessException(ErrorCode.PERFUME_INVALID_INPUT_VALUE); } Perfume perfume = perfumeRepository.findById(id) - .orElseThrow(() -> new RuntimeException("향수를 찾을 수 없습니다: " + id)); + .orElseThrow(() -> new BusinessException(ErrorCode.PERFUME_NOT_FOUND)); // 본인이 생성한 향수인지 확인 if (perfume.getUser() == null || !perfume.getUser().getId().equals(user.getId())) { - throw new RuntimeException("본인이 생성한 향수만 삭제할 수 있습니다."); + throw new BusinessException(ErrorCode.PERFUME_ACCESS_DENIED); } // 향수 삭제 @@ -183,11 +187,11 @@ public void deletePerfume(Long id, User user) { @Transactional(readOnly = true) public List recommendPerfumes(SourceType sourceType) { if (sourceType == null) { - throw new RuntimeException("sourceType이 필요합니다."); + throw new BusinessException(ErrorCode.PERFUME_INVALID_SOURCE_TYPE); } if (sourceType != SourceType.RECOMMEND_AUDIO && sourceType != SourceType.RECOMMEND_IMAGE) { - throw new RuntimeException("추천 API는 RECOMMEND_AUDIO 또는 RECOMMEND_IMAGE 타입만 사용 가능합니다."); + throw new BusinessException(ErrorCode.PERFUME_INVALID_SOURCE_TYPE); } // DB에서 추천 타입에 해당하는 향수들을 최대 10개 조회 diff --git a/src/main/java/com/umc/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index a77bd60..a714cc7 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -28,6 +28,12 @@ public enum ErrorCode { // 향수 관련 에러 PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_4001", "해당 향수를 찾을 수 없습니다."), PERFUME_INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "PERFUME_4002", "perfumeId가 잘못된 형식입니다."), + PERFUME_FILE_EMPTY(HttpStatus.BAD_REQUEST, "PERFUME_4003", "파일이 비어있습니다."), + PERFUME_FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "PERFUME_4004", "파일 크기가 제한을 초과했습니다."), + PERFUME_INVALID_FILE_TYPE(HttpStatus.BAD_REQUEST, "PERFUME_4005", "지원하지 않는 파일 형식입니다."), + PERFUME_INVALID_SOURCE_TYPE(HttpStatus.BAD_REQUEST, "PERFUME_4006", "잘못된 소스 타입입니다."), + PERFUME_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "PERFUME_5001", "향수 생성에 실패했습니다."), + PERFUME_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PERFUME_4007", "해당 향수에 대한 접근 권한이 없습니다."), // 리뷰 관련 에러 REVIEW_DESCRIPTION_EMPTY(HttpStatus.BAD_REQUEST, "REVIEW_4001", "리뷰 내용은 비어 있을 수 없습니다."), From 4ac05f1e830c003771b9f1efd2a9f58eabbc529b Mon Sep 17 00:00:00 2001 From: Baguette-bbang Date: Sun, 6 Jul 2025 08:35:56 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20shop=20error=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shop/controller/ShopController.java | 34 +++----- .../domain/shop/service/ShopServiceImpl.java | 81 ++++++++++++++----- .../com/umc/global/exception/ErrorCode.java | 4 + 3 files changed, 79 insertions(+), 40 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 9573a5e..3ccfe89 100644 --- a/src/main/java/com/umc/domain/shop/controller/ShopController.java +++ b/src/main/java/com/umc/domain/shop/controller/ShopController.java @@ -3,6 +3,8 @@ import com.umc.common.response.ApiResponse; import com.umc.domain.shop.dto.ShopResponseDto; import com.umc.domain.shop.service.ShopService; +import com.umc.global.config.SwaggerConfig.ApiErrorExamples; +import com.umc.global.exception.ErrorCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -34,16 +36,12 @@ public class ShopController { 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 = "서버 오류" ) }) + @ApiErrorExamples({ + ErrorCode.SHOP_INVALID_COORDINATES, + ErrorCode.SHOP_SEARCH_FAILED + }) public ApiResponse> getNearbyShops( @Parameter(description = "위도 (latitude)", required = true, example = "37.5665") @RequestParam double lat, @@ -51,18 +49,12 @@ public ApiResponse> getNearbyShops( @Parameter(description = "경도 (longitude)", required = true, example = "126.9780") @RequestParam double 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("근처 매장 조회 중 오류가 발생했습니다."); - } + log.info("근처 매장 조회 요청 - lat: {}, lng: {}", lat, lng); + + List response = shopService.findNearbyShops(lat, lng); + + log.info("근처 매장 조회 성공 - 매장 개수: {}", response.size()); + + return ApiResponse.success("근처 매장이 성공적으로 조회되었습니다.", response); } } diff --git a/src/main/java/com/umc/domain/shop/service/ShopServiceImpl.java b/src/main/java/com/umc/domain/shop/service/ShopServiceImpl.java index f476902..02e9ca3 100644 --- a/src/main/java/com/umc/domain/shop/service/ShopServiceImpl.java +++ b/src/main/java/com/umc/domain/shop/service/ShopServiceImpl.java @@ -3,7 +3,10 @@ import com.umc.domain.shop.dto.ShopResponseDto; import com.umc.domain.shop.entity.Shop; import com.umc.domain.shop.repository.ShopRepository; +import com.umc.global.exception.BusinessException; +import com.umc.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.Comparator; @@ -13,30 +16,70 @@ @Service @RequiredArgsConstructor +@Slf4j public class ShopServiceImpl implements ShopService { private final ShopRepository shopRepository; @Override public List findNearbyShops(double latitude, double longitude) { - // 위도 1도 ≈ 111km / 경도는 위도에 따라 다름 (서울 기준 대략 1도 ≈ 88km) - // 대략 20km 반경을 위도/경도 기준으로 환산하면: - double latRange = 4.0; // 20km / 111km ≈ 0.18 - double lngRange = 4.0; // 20km / 88km ≈ 0.23 - - int limit = 5; - - List shops = shopRepository.findByLatitudeBetweenAndLongitudeBetween( - latitude - latRange, latitude + latRange, - longitude - lngRange, longitude + lngRange - ); - - return shops.stream() - .sorted(Comparator.comparingDouble(s -> - distance(latitude, longitude, s.getLatitude(), s.getLongitude()) - )) - .limit(limit) - .map(ShopResponseDto::from) - .toList(); + try { + // 위도/경도 유효성 검증 + validateCoordinates(latitude, longitude); + + log.info("근처 매장 검색 시작 - lat: {}, lng: {}", latitude, longitude); + + // 위도 1도 ≈ 111km / 경도는 위도에 따라 다름 (서울 기준 대략 1도 ≈ 88km) + // 대략 20km 반경을 위도/경도 기준으로 환산하면: + double latRange = 4.0; // 20km / 111km ≈ 0.18 + double lngRange = 4.0; // 20km / 88km ≈ 0.23 + + int limit = 5; + + List shops = shopRepository.findByLatitudeBetweenAndLongitudeBetween( + latitude - latRange, latitude + latRange, + longitude - lngRange, longitude + lngRange + ); + + List result = shops.stream() + .sorted(Comparator.comparingDouble(s -> + distance(latitude, longitude, s.getLatitude(), s.getLongitude()) + )) + .limit(limit) + .map(ShopResponseDto::from) + .toList(); + + log.info("근처 매장 검색 완료 - 검색된 매장 수: {}", result.size()); + return result; + + } catch (BusinessException e) { + throw e; // BusinessException은 그대로 전파 + } catch (Exception e) { + log.error("매장 검색 중 오류 발생: ", e); + throw new BusinessException(ErrorCode.SHOP_SEARCH_FAILED); + } + } + + /** + * 위도/경도 유효성 검증 + */ + private void validateCoordinates(double latitude, double longitude) { + // 위도는 -90 ~ 90, 경도는 -180 ~ 180 범위 + if (latitude < -90 || latitude > 90) { + log.warn("잘못된 위도 값: {}", latitude); + throw new BusinessException(ErrorCode.SHOP_INVALID_COORDINATES); + } + + if (longitude < -180 || longitude > 180) { + log.warn("잘못된 경도 값: {}", longitude); + throw new BusinessException(ErrorCode.SHOP_INVALID_COORDINATES); + } + + // NaN 또는 무한대 값 체크 + if (Double.isNaN(latitude) || Double.isInfinite(latitude) || + Double.isNaN(longitude) || Double.isInfinite(longitude)) { + log.warn("유효하지 않은 좌표 값 - lat: {}, lng: {}", latitude, longitude); + throw new BusinessException(ErrorCode.SHOP_INVALID_COORDINATES); + } } } diff --git a/src/main/java/com/umc/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index a714cc7..5a9877b 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -38,6 +38,10 @@ public enum ErrorCode { // 리뷰 관련 에러 REVIEW_DESCRIPTION_EMPTY(HttpStatus.BAD_REQUEST, "REVIEW_4001", "리뷰 내용은 비어 있을 수 없습니다."), + // 매장 관련 에러 + SHOP_INVALID_COORDINATES(HttpStatus.BAD_REQUEST, "SHOP_4001", "잘못된 위도 또는 경도 값입니다."), + SHOP_SEARCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SHOP_5001", "매장 검색에 실패했습니다."), + // 유저 관련 에러 USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_4001", "해당 사용자를 찾을 수 없습니다."); From 202b4e7c2f6a0349ea74b48665d9d0feb3d841bb Mon Sep 17 00:00:00 2001 From: Baguette-bbang Date: Sun, 6 Jul 2025 09:17:00 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat=20:=20authorization=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 --- src/main/java/com/umc/auth/util/JwtUtil.java | 38 +++++++- .../file/controller/FileUploadController.java | 95 ------------------- .../perfume/controller/PerfumeController.java | 14 ++- .../review/controller/ReviewController.java | 23 ++--- .../user/controller/UserController.java | 14 ++- .../com/umc/global/exception/ErrorCode.java | 2 + 6 files changed, 62 insertions(+), 124 deletions(-) delete mode 100644 src/main/java/com/umc/domain/file/controller/FileUploadController.java diff --git a/src/main/java/com/umc/auth/util/JwtUtil.java b/src/main/java/com/umc/auth/util/JwtUtil.java index af1578e..2ad3d5d 100644 --- a/src/main/java/com/umc/auth/util/JwtUtil.java +++ b/src/main/java/com/umc/auth/util/JwtUtil.java @@ -3,12 +3,16 @@ import com.umc.auth.Jwt.JwtProvider; 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 lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @Component @RequiredArgsConstructor +@Slf4j public class JwtUtil { private final JwtProvider jwtProvider; @@ -35,19 +39,43 @@ public Long getUserIdFromToken(String token) { * 토큰에서 사용자 정보를 조회합니다. */ public User getUserFromToken(String token) { - Long userId = getUserIdFromToken(token); - return userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다: " + userId)); + try { + Long userId = getUserIdFromToken(token); + return userRepository.findById(userId) + .orElseThrow(() -> { + log.warn("토큰에서 추출한 사용자 ID로 사용자를 찾을 수 없습니다: {}", userId); + return new BusinessException(ErrorCode.USER_NOT_FOUND); + }); + } catch (BusinessException e) { + throw e; // BusinessException은 그대로 전파 + } catch (Exception e) { + log.warn("토큰 파싱 중 오류 발생: {}", e.getMessage()); + throw new BusinessException(ErrorCode.TOKEN_INVALID); + } } /** * Authorization 헤더에서 사용자 정보를 조회합니다. */ public User getUserFromHeader(String authorizationHeader) { + // Authorization 헤더 존재 여부 검증 + if (authorizationHeader == null || authorizationHeader.trim().isEmpty()) { + log.warn("Authorization 헤더가 없습니다."); + throw new BusinessException(ErrorCode.TOKEN_MISSING); + } + + // Bearer 형식 검증 + if (!authorizationHeader.startsWith("Bearer ")) { + log.warn("잘못된 Authorization 헤더 형식: {}", authorizationHeader); + throw new BusinessException(ErrorCode.TOKEN_MALFORMED); + } + String token = extractTokenFromHeader(authorizationHeader); - if (token == null) { - throw new RuntimeException("유효한 토큰이 없습니다."); + if (token == null || token.trim().isEmpty()) { + log.warn("토큰 추출 실패 - Authorization: {}", authorizationHeader); + throw new BusinessException(ErrorCode.TOKEN_MALFORMED); } + return getUserFromToken(token); } } \ No newline at end of file diff --git a/src/main/java/com/umc/domain/file/controller/FileUploadController.java b/src/main/java/com/umc/domain/file/controller/FileUploadController.java deleted file mode 100644 index fc7de60..0000000 --- a/src/main/java/com/umc/domain/file/controller/FileUploadController.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.umc.domain.file.controller; - -import com.umc.domain.file.dto.FileUploadResponse; -import com.umc.domain.file.service.GoogleDriveService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/files") -@RequiredArgsConstructor -@Slf4j -public class FileUploadController { - - private final GoogleDriveService googleDriveService; - - /** - * 파일 업로드 - */ - @PostMapping("/upload/{recordId}") - public ResponseEntity uploadFile( - @PathVariable String recordId, - @RequestParam("file") MultipartFile file) { - - try { - if (file.isEmpty()) { - return ResponseEntity.badRequest().body("파일이 비어있습니다."); - } - - FileUploadResponse response = googleDriveService.uploadFile(file, recordId); - - return ResponseEntity.ok(Map.of( - "success", true, - "data", response - )); - - } catch (IOException e) { - log.error("파일 업로드 실패", e); - return ResponseEntity.internalServerError().body(Map.of( - "success", false, - "error", "파일 업로드에 실패했습니다: " + e.getMessage() - )); - } - } - - /** - * 레코드의 모든 파일 조회 - */ - @GetMapping("/record/{recordId}") - public ResponseEntity getRecordFiles(@PathVariable String recordId) { - try { - Map> files = googleDriveService.getRecordFiles(recordId); - - return ResponseEntity.ok(Map.of( - "success", true, - "data", files - )); - - } catch (IOException e) { - log.error("파일 조회 실패", e); - return ResponseEntity.internalServerError().body(Map.of( - "success", false, - "error", "파일 조회에 실패했습니다: " + e.getMessage() - )); - } - } - - /** - * 파일 삭제 - */ - @DeleteMapping("/{fileId}") - public ResponseEntity deleteFile(@PathVariable String fileId) { - try { - googleDriveService.deleteFile(fileId); - - return ResponseEntity.ok(Map.of( - "success", true, - "message", "파일이 성공적으로 삭제되었습니다." - )); - - } catch (IOException e) { - log.error("파일 삭제 실패", e); - return ResponseEntity.internalServerError().body(Map.of( - "success", false, - "error", "파일 삭제에 실패했습니다: " + e.getMessage() - )); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java b/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java index 0aae46c..584e601 100644 --- a/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java +++ b/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java @@ -54,6 +54,8 @@ public class PerfumeController { ErrorCode.PERFUME_FILE_SIZE_EXCEEDED, ErrorCode.PERFUME_INVALID_FILE_TYPE, ErrorCode.PERFUME_CREATION_FAILED, + ErrorCode.TOKEN_MISSING, + ErrorCode.TOKEN_MALFORMED, ErrorCode.TOKEN_INVALID, ErrorCode.USER_NOT_FOUND }) @@ -156,7 +158,12 @@ public ApiResponse> getUserPerfumes( content = @Content(schema = @Schema(implementation = PerfumeResponseDto.class)) ) }) - @ApiErrorExample(ErrorCode.TOKEN_INVALID) + @ApiErrorExamples({ + ErrorCode.TOKEN_MISSING, + ErrorCode.TOKEN_MALFORMED, + ErrorCode.TOKEN_INVALID, + ErrorCode.USER_NOT_FOUND + }) public ApiResponse> getMyPerfumes(HttpServletRequest request) { // JWT 토큰에서 사용자 정보 추출 @@ -188,7 +195,10 @@ public ApiResponse> getMyPerfumes(HttpServletRequest re ErrorCode.PERFUME_NOT_FOUND, ErrorCode.PERFUME_ACCESS_DENIED, ErrorCode.PERFUME_INVALID_INPUT_VALUE, - ErrorCode.TOKEN_INVALID + ErrorCode.TOKEN_MISSING, + ErrorCode.TOKEN_MALFORMED, + ErrorCode.TOKEN_INVALID, + ErrorCode.USER_NOT_FOUND }) public ApiResponse deletePerfume( @Parameter(description = "향수 ID", required = true) 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 4e0fddb..6a77b95 100644 --- a/src/main/java/com/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/com/umc/domain/review/controller/ReviewController.java @@ -41,7 +41,10 @@ public class ReviewController { security = @SecurityRequirement(name = "bearerAuth") ) @SwaggerConfig.ApiErrorExamples({ + ErrorCode.TOKEN_MISSING, + ErrorCode.TOKEN_MALFORMED, ErrorCode.TOKEN_INVALID, + ErrorCode.USER_NOT_FOUND, ErrorCode.PERFUME_NOT_FOUND, ErrorCode.PERFUME_INVALID_INPUT_VALUE, ErrorCode.REVIEW_DESCRIPTION_EMPTY, @@ -52,14 +55,9 @@ public ResponseEntity> cre @RequestBody ReviewRequestDTO.CreateReviewRequestDTO request, HttpServletRequest httpRequest ) { - String authorization = httpRequest.getHeader("Authorization"); - log.info("리뷰 생성 요청 - perfumeId: {}, Authorization: {}", perfumeId, authorization); - - if (authorization == null || authorization.trim().isEmpty()) { - throw new BusinessException(ErrorCode.TOKEN_INVALID); - } + log.info("리뷰 생성 요청 - perfumeId: {}", perfumeId); - User user = jwtUtil.getUserFromHeader(authorization); + User user = jwtUtil.getUserFromHeader(httpRequest.getHeader("Authorization")); ReviewResponseDTO.CreateReviewReponseDTO result = reviewService.createReview(perfumeId, user.getId(), request); return ResponseEntity.ok(ApiResponse.success("리뷰가 성공적으로 등록되었습니다.", result)); @@ -68,19 +66,16 @@ public ResponseEntity> cre @GetMapping("/me") @Operation(summary = "내가 작성한 리뷰 목록 조회", security = @SecurityRequirement(name = "bearerAuth")) @SwaggerConfig.ApiErrorExamples({ + ErrorCode.TOKEN_MISSING, + ErrorCode.TOKEN_MALFORMED, 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); - } + log.info("내 리뷰 조회 요청"); - User user = jwtUtil.getUserFromHeader(authorization); + User user = jwtUtil.getUserFromHeader(httpRequest.getHeader("Authorization")); List result = reviewService.getMyReviews(user.getId()); return ResponseEntity.ok(ApiResponse.success("내가 작성한 리뷰 목록 조회 성공", result)); } diff --git a/src/main/java/com/umc/domain/user/controller/UserController.java b/src/main/java/com/umc/domain/user/controller/UserController.java index 748d7ef..1bd8099 100644 --- a/src/main/java/com/umc/domain/user/controller/UserController.java +++ b/src/main/java/com/umc/domain/user/controller/UserController.java @@ -37,17 +37,15 @@ public class UserController { @Operation(summary = "자신의 닉네임 조회", description = "JWT 토큰을 기반으로 현재 유저의 닉네임을 조회합니다.", security = @SecurityRequirement(name = "bearerAuth")) @SwaggerConfig.ApiErrorExamples({ - ErrorCode.TOKEN_INVALID + ErrorCode.TOKEN_MISSING, + ErrorCode.TOKEN_MALFORMED, + ErrorCode.TOKEN_INVALID, + ErrorCode.USER_NOT_FOUND }) public ResponseEntity> getMyNickname(HttpServletRequest request) { - String authorization = request.getHeader("Authorization"); - log.info("닉네임 조회 요청 - Authorization: {}", authorization); + log.info("닉네임 조회 요청"); - if (authorization == null || authorization.trim().isEmpty()) { - throw new BusinessException(ErrorCode.TOKEN_INVALID, "Authorization 헤더가 없습니다."); - } - - User user = jwtUtil.getUserFromHeader(authorization); + User user = jwtUtil.getUserFromHeader(request.getHeader("Authorization")); UserResponseDTO.MyNameDTO response = UserConverter.toMyNameDTO(user); return ResponseEntity.ok(ApiResponse.success("닉네임 조회 성공", response)); diff --git a/src/main/java/com/umc/global/exception/ErrorCode.java b/src/main/java/com/umc/global/exception/ErrorCode.java index 5a9877b..c2125c0 100644 --- a/src/main/java/com/umc/global/exception/ErrorCode.java +++ b/src/main/java/com/umc/global/exception/ErrorCode.java @@ -24,6 +24,8 @@ public enum ErrorCode { //토큰 관련 에러 TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "TOKEN_4001", "토큰이 유효하지 않습니다."), + TOKEN_MISSING(HttpStatus.UNAUTHORIZED, "TOKEN_4002", "Authorization 헤더가 필요합니다."), + TOKEN_MALFORMED(HttpStatus.UNAUTHORIZED, "TOKEN_4003", "토큰 형식이 올바르지 않습니다."), // 향수 관련 에러 PERFUME_NOT_FOUND(HttpStatus.NOT_FOUND, "PERFUME_4001", "해당 향수를 찾을 수 없습니다."), From a760239187c0292c1c78f3f664af81097a8a4dce Mon Sep 17 00:00:00 2001 From: Baguette-bbang Date: Sun, 6 Jul 2025 09:32:20 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat=20:=20=ED=83=80=EC=9E=85=20validation?= =?UTF-8?q?=20400=20=EC=97=90=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../perfume/service/PerfumeService.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/umc/domain/perfume/service/PerfumeService.java b/src/main/java/com/umc/domain/perfume/service/PerfumeService.java index 4c0df13..6de8f2e 100644 --- a/src/main/java/com/umc/domain/perfume/service/PerfumeService.java +++ b/src/main/java/com/umc/domain/perfume/service/PerfumeService.java @@ -33,24 +33,27 @@ public class PerfumeService { */ public PerfumeResponseDto createPerfume(SourceType sourceType, MultipartFile file, User user) { try { - // 0. 사용자 존재 확인 (Foreign Key 제약 조건 해결) + // 0. sourceType 검증 + validateSourceType(sourceType); + + // 1. 사용자 존재 확인 (Foreign Key 제약 조건 해결) User existingUser = userRepository.findById(user.getId()) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - // 1. 파일 유효성 검증 + // 2. 파일 유효성 검증 validateFile(file, sourceType); - // 2. GPT를 통한 향수 정보 생성 (임시 URL 사용) + // 3. GPT를 통한 향수 정보 생성 (임시 URL 사용) String tempUrl = "/temp/" + UUID.randomUUID().toString(); Perfume perfume = perfumeGptService.generatePerfume(sourceType, tempUrl, file); - // 3. 검증된 사용자 정보 설정 + // 4. 검증된 사용자 정보 설정 perfume.setUser(existingUser); - // 4. 데이터베이스에 저장 (ID 생성을 위해) + // 5. 데이터베이스에 저장 (ID 생성을 위해) Perfume savedPerfume = perfumeRepository.save(perfume); - // 5. 파일을 구글 드라이브에 업로드하고 URL 업데이트 + // 6. 파일을 구글 드라이브에 업로드하고 URL 업데이트 try { perfumeGptService.uploadFileAndUpdateUrl(savedPerfume, file); log.info("파일 업로드 및 URL 업데이트 완료 - 향수 ID: {}", savedPerfume.getId()); @@ -59,13 +62,13 @@ public PerfumeResponseDto createPerfume(SourceType sourceType, MultipartFile fil // 파일 업로드 실패해도 향수는 생성됨 (임시 URL 유지) } - // 6. 업데이트된 URL로 데이터베이스 저장 + // 7. 업데이트된 URL로 데이터베이스 저장 savedPerfume = perfumeRepository.save(savedPerfume); log.info("향수 생성 완료 - ID: {}, 사용자: {}, 타입: {}", savedPerfume.getId(), existingUser.getNickname(), sourceType); - // 6. 응답 DTO 생성 및 반환 (sourceType을 클라이언트용으로 변환) + // 8. 응답 DTO 생성 및 반환 (sourceType을 클라이언트용으로 변환) PerfumeResponseDto dto = PerfumeResponseDto.from(savedPerfume); return dto.withClientSourceType(convertToClientSourceType(savedPerfume.getSourceType())); @@ -114,7 +117,20 @@ private void validateFile(MultipartFile file, SourceType sourceType) { contentType); } - + /** + * sourceType 유효성 검증 + */ + private void validateSourceType(SourceType sourceType) { + if (sourceType == null) { + throw new BusinessException(ErrorCode.PERFUME_INVALID_SOURCE_TYPE); + } + + if (sourceType != SourceType.AUDIO && sourceType != SourceType.IMAGE) { + throw new BusinessException(ErrorCode.PERFUME_INVALID_SOURCE_TYPE); + } + + log.info("sourceType 검증 완료 - 타입: {}", sourceType); + } /** * 향수 조회 From fc917c7e9cf862363d42089725bde43069660291 Mon Sep 17 00:00:00 2001 From: Baguette-bbang Date: Sun, 6 Jul 2025 09:36:57 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat=20:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=ED=83=80=EC=9E=85=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/umc/domain/perfume/controller/PerfumeController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java b/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java index 584e601..7861144 100644 --- a/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java +++ b/src/main/java/com/umc/domain/perfume/controller/PerfumeController.java @@ -9,6 +9,7 @@ import com.umc.domain.user.entity.User; import com.umc.global.config.SwaggerConfig.ApiErrorExample; import com.umc.global.config.SwaggerConfig.ApiErrorExamples; +import com.umc.global.exception.BusinessException; import com.umc.global.exception.ErrorCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -246,7 +247,7 @@ public ApiResponse> recommendPerfume( } else if ("IMAGE".equalsIgnoreCase(sourceType)) { internalSourceType = SourceType.RECOMMEND_IMAGE; } else { - throw new RuntimeException("잘못된 sourceType입니다. AUDIO 또는 IMAGE만 사용 가능합니다."); + throw new BusinessException(ErrorCode.PERFUME_INVALID_SOURCE_TYPE); } List response = perfumeService.recommendPerfumes(internalSourceType);