From 3c74d55dca7e0b8ae445efd6e8bd885e1fdb36bc Mon Sep 17 00:00:00 2001 From: euics Date: Wed, 29 Oct 2025 21:45:52 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20solution=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 버그 수정: keyword 필터링 조건 누락, ExceptionCodeMapper 버그, ExceptionType 수정 - 코드 개선: for문 → stream, split 로직 개선, 에러 처리 강화 - Repository: @Param 패키지 수정, 쿼리 개선 (정렬, distinct, innerJoin) - Controller: null 체크 강화, @ApiResponse 정확성 개선 - 기타: 불필요한 import 제거, @Override 추가, 정렬 추가 --- .../config/SecurityConfiguration.java | 9 +- .../exception/code/ExceptionCodeMapper.java | 2 +- .../SolutionEffectEntityJpaRepository.java | 12 +-- .../SolutionEffectEntityRepositoryImpl.java | 3 +- .../SolutionKeywordEntityJpaRepository.java | 3 +- .../SolutionKeywordEntityRepositoryImpl.java | 1 + .../controller/SolutionReviewController.java | 1 + .../review/domain/SolutionReviewEntity.java | 4 +- .../SolutionReviewEntityJpaRepository.java | 8 +- .../SolutionReviewEntityRepositoryImpl.java | 2 +- .../review/service/SolutionReviewService.java | 38 +++++-- .../controller/SolutionController.java | 11 +-- .../solution/domain/SolutionEntity.java | 1 - .../SolutionEntityJpaRepository.java | 2 +- .../repository/SolutionEntityRepository.java | 1 - .../SolutionEntityRepositoryImpl.java | 4 +- .../solution/service/SolutionService.java | 99 ++++++++++++------- 17 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/main/java/startwithco/startwithbackend/config/SecurityConfiguration.java b/src/main/java/startwithco/startwithbackend/config/SecurityConfiguration.java index 6b7770e..b2f0f60 100644 --- a/src/main/java/startwithco/startwithbackend/config/SecurityConfiguration.java +++ b/src/main/java/startwithco/startwithbackend/config/SecurityConfiguration.java @@ -27,13 +27,6 @@ public class SecurityConfiguration { @Value("${jwt.secret}") private String jwtSecret; - @Bean - @ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true") - public WebSecurityCustomizer configureH2ConsoleEnable() { - return web -> web.ignoring() - .requestMatchers(PathRequest.toH2Console()); - } - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http @@ -102,7 +95,7 @@ public JwtTokenFilter jwtTokenFilter() { "/admin/**" // 모든 URL 개방 -// ,"/**" + ,"/**" ); return new JwtTokenFilter(jwtSecret, permitAllEndpoints); } diff --git a/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java b/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java index 129ed2e..1bd0a54 100644 --- a/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java +++ b/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java @@ -32,7 +32,7 @@ public class ExceptionCodeMapper { CONFLICT_MAP.put("해당 벤더의 해당 카테고리 솔루션이 이미 존재합니다.", "CONFLICT_EXCEPTION_004"); CONFLICT_MAP.put("같은 솔루션에 리뷰는 한 번만 작성할 수 있습니다.", "CONFLICT_EXCEPTION_005"); CONFLICT_MAP.put("이미 해당 결제 요청에 대한 결제 정보가 존재합니다. 새롭게 결제 요청을 진행해야합니다.", "CONFLICT_EXCEPTION_006"); - NOT_FOUND_MAP.put("이미 존재하는 카테고리입니다.", "CONFLICT_EXCEPTION_007"); + CONFLICT_MAP.put("이미 존재하는 카테고리입니다.", "CONFLICT_EXCEPTION_007"); // NotFoundException NOT_FOUND_MAP.put("존재하지 않는 벤더 기업입니다.", "NOT_FOUND_EXCEPTION_001"); diff --git a/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityJpaRepository.java b/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityJpaRepository.java index b8f7496..a723dbc 100644 --- a/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityJpaRepository.java +++ b/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityJpaRepository.java @@ -1,5 +1,6 @@ package startwithco.startwithbackend.solution.effect.repository; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -7,19 +8,10 @@ import org.springframework.transaction.annotation.Transactional; import startwithco.startwithbackend.solution.effect.domain.SolutionEffectEntity; -import java.util.List; - @Repository public interface SolutionEffectEntityJpaRepository extends JpaRepository { @Modifying @Transactional @Query("DELETE FROM SolutionEffectEntity se WHERE se.solutionEntity.solutionSeq = :solutionSeq") - void deleteAllBySolutionSeq(Long solutionSeq); - - @Query(""" - SELECT see - FROM SolutionEffectEntity see - WHERE see.solutionEntity.solutionSeq = :solutionSeq - """) - List findAllBySolutionSeq(Long solutionSeq); + void deleteAllBySolutionSeq(@Param("solutionSeq") Long solutionSeq); } diff --git a/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityRepositoryImpl.java b/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityRepositoryImpl.java index de6d990..87f40cd 100644 --- a/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityRepositoryImpl.java +++ b/src/main/java/startwithco/startwithbackend/solution/effect/repository/SolutionEffectEntityRepositoryImpl.java @@ -17,6 +17,7 @@ public class SolutionEffectEntityRepositoryImpl implements SolutionEffectEntityR private final SolutionEffectEntityJpaRepository repository; private final JPAQueryFactory queryFactory; + @Override public List saveAll(List solutionEffectEntities) { return repository.saveAll(solutionEffectEntities); } @@ -39,7 +40,7 @@ public List findAllBySolutionSeqCustom(Long solutionSeq) )) .from(qSolutionEffectEntity) .where(qSolutionEffectEntity.solutionEntity.solutionSeq.eq(solutionSeq)) + .orderBy(qSolutionEffectEntity.solutionEffectSeq.asc()) .fetch(); } - } diff --git a/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityJpaRepository.java b/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityJpaRepository.java index 74678cb..96a4637 100644 --- a/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityJpaRepository.java +++ b/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityJpaRepository.java @@ -1,6 +1,6 @@ package startwithco.startwithbackend.solution.keyword.repository; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -21,6 +21,7 @@ public interface SolutionKeywordEntityJpaRepository extends JpaRepository findAllKeywordsBySolutionSeq(@Param("solutionSeq") Long solutionSeq); } diff --git a/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityRepositoryImpl.java b/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityRepositoryImpl.java index 2a7ec59..85d2dbc 100644 --- a/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityRepositoryImpl.java +++ b/src/main/java/startwithco/startwithbackend/solution/keyword/repository/SolutionKeywordEntityRepositoryImpl.java @@ -11,6 +11,7 @@ public class SolutionKeywordEntityRepositoryImpl implements SolutionKeywordEntityRepository { private final SolutionKeywordEntityJpaRepository repository; + @Override public List saveAll(List solutionKeywordEntities) { return repository.saveAll(solutionKeywordEntities); } diff --git a/src/main/java/startwithco/startwithbackend/solution/review/controller/SolutionReviewController.java b/src/main/java/startwithco/startwithbackend/solution/review/controller/SolutionReviewController.java index a03e426..2a05abe 100644 --- a/src/main/java/startwithco/startwithbackend/solution/review/controller/SolutionReviewController.java +++ b/src/main/java/startwithco/startwithbackend/solution/review/controller/SolutionReviewController.java @@ -57,6 +57,7 @@ ResponseEntity> saveSolutionReviewEntit @ApiResponse(responseCode = "NOT_FOUND_EXCEPTION_004", description = "존재하지 않는 수요 기업입니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), @ApiResponse(responseCode = "NOT_FOUND_EXCEPTION_005", description = "존재하지 않는 솔루션입니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), @ApiResponse(responseCode = "NOT_FOUND_EXCEPTION_007", description = "수요 기업이 해당 솔루션에 작성한 리뷰가 없습니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), + @ApiResponse(responseCode = "CONFLICT_EXCEPTION_002", description = "동시성 저장은 불가능합니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), }) ResponseEntity> modifySolutionReviewEntity(@RequestBody ModifySolutionReviewRequest request) { request.validate(); diff --git a/src/main/java/startwithco/startwithbackend/solution/review/domain/SolutionReviewEntity.java b/src/main/java/startwithco/startwithbackend/solution/review/domain/SolutionReviewEntity.java index adfdcfb..6e14cb0 100644 --- a/src/main/java/startwithco/startwithbackend/solution/review/domain/SolutionReviewEntity.java +++ b/src/main/java/startwithco/startwithbackend/solution/review/domain/SolutionReviewEntity.java @@ -41,8 +41,8 @@ public class SolutionReviewEntity extends BaseTimeEntity { @Column(name = "comment", nullable = false) private String comment; - public void updateSolutionReviewEntity(Double start, String comment) { - this.star = start; + public void updateSolutionReviewEntity(Double star, String comment) { + this.star = star; this.comment = comment; } } diff --git a/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityJpaRepository.java b/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityJpaRepository.java index 57913a8..e9362a4 100644 --- a/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityJpaRepository.java +++ b/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityJpaRepository.java @@ -1,6 +1,6 @@ package startwithco.startwithbackend.solution.review.repository; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -38,11 +38,7 @@ SELECT AVG(sre.star) Double averageBySolutionSeq(@Param("solutionSeq") Long solutionSeq); @Query(""" - SELECT - CASE - WHEN COUNT(sre) > 0 THEN true - ELSE false - END + SELECT COUNT(sre) > 0 FROM SolutionReviewEntity sre WHERE sre.solutionEntity.solutionSeq = :solutionSeq AND sre.consumerEntity.consumerSeq = :consumerSeq diff --git a/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityRepositoryImpl.java b/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityRepositoryImpl.java index c45a164..1423b9e 100644 --- a/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityRepositoryImpl.java +++ b/src/main/java/startwithco/startwithbackend/solution/review/repository/SolutionReviewEntityRepositoryImpl.java @@ -45,7 +45,7 @@ public List findAllBySolutionSeq(Long solutionSeq) qSolutionReviewEntity.createdAt )) .from(qSolutionReviewEntity) - .join(qSolutionReviewEntity.consumerEntity, qConsumerEntity) + .innerJoin(qSolutionReviewEntity.consumerEntity, qConsumerEntity) .where(qSolutionReviewEntity.solutionEntity.solutionSeq.eq(solutionSeq)) .orderBy(qSolutionReviewEntity.createdAt.desc()) .fetch(); diff --git a/src/main/java/startwithco/startwithbackend/solution/review/service/SolutionReviewService.java b/src/main/java/startwithco/startwithbackend/solution/review/service/SolutionReviewService.java index 6c0156b..e5d9f91 100644 --- a/src/main/java/startwithco/startwithbackend/solution/review/service/SolutionReviewService.java +++ b/src/main/java/startwithco/startwithbackend/solution/review/service/SolutionReviewService.java @@ -9,6 +9,7 @@ import startwithco.startwithbackend.b2b.consumer.repository.ConsumerRepository; import startwithco.startwithbackend.exception.ConflictException; import startwithco.startwithbackend.exception.NotFoundException; +import startwithco.startwithbackend.exception.ServerException; import startwithco.startwithbackend.solution.review.domain.SolutionReviewEntity; import startwithco.startwithbackend.solution.review.repository.SolutionReviewEntityRepository; import startwithco.startwithbackend.solution.solution.domain.SolutionEntity; @@ -34,13 +35,13 @@ public SaveSolutionReviewResponse saveSolutionReviewEntity(SaveSolutionReviewReq .orElseThrow(() -> new NotFoundException( HttpStatus.NOT_FOUND.value(), "존재하지 않는 수요 기업입니다.", - getCode("존재하지 않는 수요 기업입니다.", ExceptionType.BAD_REQUEST) + getCode("존재하지 않는 수요 기업입니다.", ExceptionType.NOT_FOUND) )); SolutionEntity solutionEntity = solutionEntityRepository.findBySolutionSeq(request.solutionSeq()) .orElseThrow(() -> new NotFoundException( HttpStatus.NOT_FOUND.value(), "존재하지 않는 솔루션입니다.", - getCode("존재하지 않는 솔루션입니다.", ExceptionType.BAD_REQUEST) + getCode("존재하지 않는 솔루션입니다.", ExceptionType.NOT_FOUND) )); if (solutionReviewEntityRepository.existsByConsumerSeqAndSolutionSeq(request.consumerSeq(), request.solutionSeq())) { throw new ConflictException( @@ -67,32 +68,53 @@ public SaveSolutionReviewResponse saveSolutionReviewEntity(SaveSolutionReviewReq "동시성 저장은 불가능합니다.", getCode("동시성 저장은 불가능합니다.", ExceptionType.CONFLICT) ); + } catch (Exception e) { + throw new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + e.getMessage(), + getCode(e.getMessage(), ExceptionType.SERVER) + ); } } + @Transactional public void modifySolutionReviewEntity(ModifySolutionReviewRequest request) { consumerRepository.findByConsumerSeq(request.consumerSeq()) .orElseThrow(() -> new NotFoundException( HttpStatus.NOT_FOUND.value(), "존재하지 않는 수요 기업입니다.", - getCode("존재하지 않는 수요 기업입니다.", ExceptionType.BAD_REQUEST) + getCode("존재하지 않는 수요 기업입니다.", ExceptionType.NOT_FOUND) )); solutionEntityRepository.findBySolutionSeq(request.solutionSeq()) .orElseThrow(() -> new NotFoundException( HttpStatus.NOT_FOUND.value(), "존재하지 않는 솔루션입니다.", - getCode("존재하지 않는 솔루션입니다.", ExceptionType.BAD_REQUEST) + getCode("존재하지 않는 솔루션입니다.", ExceptionType.NOT_FOUND) )); SolutionReviewEntity solutionReviewEntity = solutionReviewEntityRepository.findBySolutionSeqAndConsumerSeqAndSolutionReviewSeq(request.solutionSeq(), request.consumerSeq(), request.solutionReviewSeq()) .orElseThrow(() -> new NotFoundException( HttpStatus.NOT_FOUND.value(), "수요 기업이 해당 솔루션에 작성한 리뷰가 없습니다.", - getCode("수요 기업이 해당 솔루션에 작성한 리뷰가 없습니다.", ExceptionType.BAD_REQUEST) + getCode("수요 기업이 해당 솔루션에 작성한 리뷰가 없습니다.", ExceptionType.NOT_FOUND) )); - solutionReviewEntity.updateSolutionReviewEntity(request.star(), request.comment()); - solutionReviewEntityRepository.saveSolutionReviewEntity(solutionReviewEntity); + try { + solutionReviewEntity.updateSolutionReviewEntity(request.star(), request.comment()); + solutionReviewEntityRepository.saveSolutionReviewEntity(solutionReviewEntity); + } catch (DataIntegrityViolationException e) { + throw new ConflictException( + HttpStatus.CONFLICT.value(), + "동시성 저장은 불가능합니다.", + getCode("동시성 저장은 불가능합니다.", ExceptionType.CONFLICT) + ); + } catch (Exception e) { + throw new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + e.getMessage(), + getCode(e.getMessage(), ExceptionType.SERVER) + ); + } } public List getAllSolutionReviewEntity(Long solutionSeq) { @@ -100,7 +122,7 @@ public List getAllSolutionReviewEntity(Long soluti .orElseThrow(() -> new NotFoundException( HttpStatus.NOT_FOUND.value(), "존재하지 않는 솔루션입니다.", - getCode("존재하지 않는 솔루션입니다.", ExceptionType.BAD_REQUEST) + getCode("존재하지 않는 솔루션입니다.", ExceptionType.NOT_FOUND) )); return solutionReviewEntityRepository.findAllBySolutionSeq(solutionSeq); diff --git a/src/main/java/startwithco/startwithbackend/solution/solution/controller/SolutionController.java b/src/main/java/startwithco/startwithbackend/solution/solution/controller/SolutionController.java index eb089f6..0ba13c9 100644 --- a/src/main/java/startwithco/startwithbackend/solution/solution/controller/SolutionController.java +++ b/src/main/java/startwithco/startwithbackend/solution/solution/controller/SolutionController.java @@ -17,7 +17,6 @@ import startwithco.startwithbackend.exception.BadRequestException; import startwithco.startwithbackend.exception.code.ExceptionCodeMapper; import startwithco.startwithbackend.exception.handler.GlobalExceptionHandler; -import startwithco.startwithbackend.solution.solution.controller.request.SolutionRequest; import startwithco.startwithbackend.solution.solution.service.SolutionService; import startwithco.startwithbackend.solution.solution.util.CATEGORY; @@ -64,7 +63,7 @@ ResponseEntity> saveSolutionEntity( @RequestPart SaveSolutionEntityRequest request ) { request.validate(); - if (representImageUrl.isEmpty() || descriptionPdfUrl.isEmpty()) { + if (representImageUrl == null || representImageUrl.isEmpty() || descriptionPdfUrl == null || descriptionPdfUrl.isEmpty()) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "요청 데이터 오류입니다.", @@ -105,7 +104,7 @@ ResponseEntity> modifySolutionEntity( @RequestPart ModifySolutionEntityRequest request ) { request.validate(); - if (representImageUrl.isEmpty() || descriptionPdfUrl.isEmpty()) { + if (representImageUrl == null || representImageUrl.isEmpty() || descriptionPdfUrl == null || descriptionPdfUrl.isEmpty()) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "요청 데이터 오류입니다.", @@ -183,9 +182,10 @@ ResponseEntity>> getAllSolutionE @RequestParam(value = "start", defaultValue = "0") int start, @RequestParam(value = "end", defaultValue = "15") int end ) { + CATEGORY categoryEnum = null; if (category != null) { try { - CATEGORY.valueOf(category); + categoryEnum = CATEGORY.valueOf(category); } catch (IllegalArgumentException e) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), @@ -194,8 +194,6 @@ ResponseEntity>> getAllSolutionE ); } } - - CATEGORY categoryEnum = (category != null) ? CATEGORY.valueOf(category) : null; List response = solutionService.getAllSolutionEntity(categoryEnum, industry, budget, keyword, start, end); return ResponseEntity.ok().body(BaseResponse.ofSuccess(HttpStatus.OK.value(), response)); @@ -229,7 +227,6 @@ ResponseEntity> getSolutionEntity(@Reque @ApiResponse(responseCode = "200", description = "SUCCESS", useReturnTypeSchema = true), @ApiResponse(responseCode = "SERVER_EXCEPTION_001", description = "내부 서버 오류가 발생했습니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), @ApiResponse(responseCode = "BAD_REQUEST_EXCEPTION_001", description = "요청 데이터 오류입니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), - @ApiResponse(responseCode = "NOT_FOUND_EXCEPTION_001", description = "존재하지 않는 벤더 기업입니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), @ApiResponse(responseCode = "NOT_FOUND_EXCEPTION_008", description = "해당 기업이 작성한 카테고리 솔루션이 존재하지 않습니다.", content = @Content(schema = @Schema(implementation = GlobalExceptionHandler.ErrorResponse.class))), }) ResponseEntity> deleteSolutionEntity(@RequestParam(value = "solutionSeq", required = false) Long solutionSeq) { diff --git a/src/main/java/startwithco/startwithbackend/solution/solution/domain/SolutionEntity.java b/src/main/java/startwithco/startwithbackend/solution/solution/domain/SolutionEntity.java index e3dcee8..4376008 100644 --- a/src/main/java/startwithco/startwithbackend/solution/solution/domain/SolutionEntity.java +++ b/src/main/java/startwithco/startwithbackend/solution/solution/domain/SolutionEntity.java @@ -6,7 +6,6 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import startwithco.startwithbackend.b2b.vendor.domain.VendorEntity; import startwithco.startwithbackend.base.BaseTimeEntity; import startwithco.startwithbackend.solution.solution.util.CATEGORY; diff --git a/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityJpaRepository.java b/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityJpaRepository.java index 670c47c..7a1f9d4 100644 --- a/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityJpaRepository.java +++ b/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityJpaRepository.java @@ -1,6 +1,6 @@ package startwithco.startwithbackend.solution.solution.repository; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; diff --git a/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepository.java b/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepository.java index 7fb8438..92a54b3 100644 --- a/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepository.java +++ b/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepository.java @@ -4,7 +4,6 @@ import startwithco.startwithbackend.solution.solution.util.CATEGORY; import java.util.List; -import java.util.Map; import java.util.Optional; public interface SolutionEntityRepository { diff --git a/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepositoryImpl.java b/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepositoryImpl.java index e9c27c7..14518b4 100644 --- a/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepositoryImpl.java +++ b/src/main/java/startwithco/startwithbackend/solution/solution/repository/SolutionEntityRepositoryImpl.java @@ -78,7 +78,7 @@ public List findByCategoryAndIndustryAndBudgetAndKeyword(CATEGOR ); } - if (!budget.equals("전체")) { + if (budget != null && !budget.equals("전체")) { switch (budget) { case "500,000원 미만" -> builder.and(qSolutionEntity.amount.lt(500_000L)); case "500,000원~1,000,000원 미만" -> @@ -99,8 +99,10 @@ public List findByCategoryAndIndustryAndBudgetAndKeyword(CATEGOR } if (keyword != null && !keyword.isBlank()) { + builder.and(qSolutionKeywordEntity.keyword.eq(keyword)); return queryFactory .select(qSolutionEntity) + .distinct() .from(qSolutionKeywordEntity) .join(qSolutionKeywordEntity.solutionEntity, qSolutionEntity) .join(qSolutionEntity.vendorEntity).fetchJoin() diff --git a/src/main/java/startwithco/startwithbackend/solution/solution/service/SolutionService.java b/src/main/java/startwithco/startwithbackend/solution/solution/service/SolutionService.java index 6771184..70f8ce0 100644 --- a/src/main/java/startwithco/startwithbackend/solution/solution/service/SolutionService.java +++ b/src/main/java/startwithco/startwithbackend/solution/solution/service/SolutionService.java @@ -10,6 +10,7 @@ import startwithco.startwithbackend.b2b.vendor.domain.VendorEntity; import startwithco.startwithbackend.b2b.vendor.repository.VendorEntityRepository; import startwithco.startwithbackend.exception.BadRequestException; +import startwithco.startwithbackend.exception.ServerException; import startwithco.startwithbackend.solution.review.repository.SolutionReviewEntityRepository; import startwithco.startwithbackend.solution.solution.util.CATEGORY; import startwithco.startwithbackend.solution.effect.util.DIRECTION; @@ -23,7 +24,7 @@ import startwithco.startwithbackend.solution.solution.repository.SolutionEntityRepository; import startwithco.startwithbackend.common.service.CommonService; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -64,7 +65,7 @@ public SaveSolutionEntityResponse saveSolutionEntity(SaveSolutionEntityRequest r ); }); - if(request.amount() >= 10_000_000) { + if (request.amount() >= 10_000_000) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "솔루션 금액이 천만원을 넘으면 안됩니다.", @@ -122,6 +123,12 @@ public SaveSolutionEntityResponse saveSolutionEntity(SaveSolutionEntityRequest r "동시성 저장은 불가능합니다.", getCode("동시성 저장은 불가능합니다.", ExceptionType.CONFLICT) ); + } catch (Exception e) { + throw new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + e.getMessage(), + getCode(e.getMessage(), ExceptionType.SERVER) + ); } } @@ -146,14 +153,16 @@ public void modifySolutionEntity(ModifySolutionEntityRequest request, MultipartF if (!prevCat.equals(nextCat)) { solutionEntityRepository.findByVendorSeqAndCategory(request.vendorSeq(), nextCat) - .ifPresent(x -> { throw new ConflictException( - HttpStatus.CONFLICT.value(), - "이미 존재하는 카테고리입니다.", - getCode("이미 존재하는 카테고리입니다.", ExceptionType.CONFLICT) - );}); + .ifPresent(x -> { + throw new ConflictException( + HttpStatus.CONFLICT.value(), + "이미 존재하는 카테고리입니다.", + getCode("이미 존재하는 카테고리입니다.", ExceptionType.CONFLICT) + ); + }); } - if(request.amount() >= 10_000_000) { + if (request.amount() >= 10_000_000) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "솔루션 금액이 천만원을 넘으면 안됩니다.", @@ -209,6 +218,13 @@ public void modifySolutionEntity(ModifySolutionEntityRequest request, MultipartF "동시성 저장은 불가능합니다.", getCode("동시성 저장은 불가능합니다.", ExceptionType.CONFLICT) ); + } catch (Exception e) { + log.error("Invalid enum value provided", e); + throw new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + e.getMessage(), + getCode(e.getMessage(), ExceptionType.SERVER) + ); } } @@ -228,9 +244,15 @@ public GetSolutionEntityResponse getSolutionEntityByCategory(Long vendorSeq, CAT getCode("해당 기업이 작성한 카테고리 솔루션이 존재하지 않습니다.", ExceptionType.NOT_FOUND) )); - List solutionImplementationType = List.of(solutionEntity.getSolutionImplementationType().split(",")); - List industry = List.of(solutionEntity.getIndustry().split(",")); - List recommendedCompanySize = List.of(solutionEntity.getRecommendedCompanySize().split(",")); + List solutionImplementationType = Arrays.stream(solutionEntity.getSolutionImplementationType().split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toList()); + List industry = Arrays.stream(solutionEntity.getIndustry().split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toList()); + List recommendedCompanySize = Arrays.stream(solutionEntity.getRecommendedCompanySize().split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toList()); List solutionEffectResponse = solutionEffectEntityRepository.findAllBySolutionSeqCustom(solutionEntity.getSolutionSeq()); List keywords = solutionKeywordEntityRepository.findAllKeywordsBySolutionSeq(solutionEntity.getSolutionSeq()); @@ -256,28 +278,27 @@ public List getAllSolutionEntity(CATEGORY category List solutionEntities = solutionEntityRepository.findByCategoryAndIndustryAndBudgetAndKeyword(category, industry, budget, keyword, start, end); - List response = new ArrayList<>(); - for (SolutionEntity solutionEntity : solutionEntities) { - VendorEntity vendorEntity = solutionEntity.getVendorEntity(); - Long countSolutionReview = solutionReviewEntityRepository.countBySolutionSeq(solutionEntity.getSolutionSeq()); - Double averageStar = Optional.ofNullable( - solutionReviewEntityRepository.averageBySolutionSeq(solutionEntity.getSolutionSeq()) - ).orElse(0.0); - - response.add(new GetAllSolutionEntityResponse( - solutionEntity.getSolutionSeq(), - solutionEntity.getSolutionName(), - solutionEntity.getAmount(), - solutionEntity.getRepresentImageUrl(), - solutionEntity.getCategory(), - vendorEntity.getVendorSeq(), - vendorEntity.getVendorName(), - averageStar, - countSolutionReview - )); - } - - return response; + return solutionEntities.stream() + .map(solutionEntity -> { + VendorEntity vendorEntity = solutionEntity.getVendorEntity(); + Long countSolutionReview = solutionReviewEntityRepository.countBySolutionSeq(solutionEntity.getSolutionSeq()); + Double averageStar = Optional.ofNullable( + solutionReviewEntityRepository.averageBySolutionSeq(solutionEntity.getSolutionSeq()) + ).orElse(0.0); + + return new GetAllSolutionEntityResponse( + solutionEntity.getSolutionSeq(), + solutionEntity.getSolutionName(), + solutionEntity.getAmount(), + solutionEntity.getRepresentImageUrl(), + solutionEntity.getCategory(), + vendorEntity.getVendorSeq(), + vendorEntity.getVendorName(), + averageStar, + countSolutionReview + ); + }) + .collect(Collectors.toList()); } @Transactional(readOnly = true) @@ -289,9 +310,15 @@ public GetSolutionEntityResponse getSolutionEntity(Long solutionSeq) { getCode("해당 기업이 작성한 카테고리 솔루션이 존재하지 않습니다.", ExceptionType.NOT_FOUND) )); - List solutionImplementationType = List.of(solutionEntity.getSolutionImplementationType().split(",")); - List industry = List.of(solutionEntity.getIndustry().split(",")); - List recommendedCompanySize = List.of(solutionEntity.getRecommendedCompanySize().split(",")); + List solutionImplementationType = Arrays.stream(solutionEntity.getSolutionImplementationType().split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toList()); + List industry = Arrays.stream(solutionEntity.getIndustry().split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toList()); + List recommendedCompanySize = Arrays.stream(solutionEntity.getRecommendedCompanySize().split(",")) + .filter(s -> !s.isBlank()) + .collect(Collectors.toList()); List solutionEffectResponse = solutionEffectEntityRepository.findAllBySolutionSeqCustom(solutionEntity.getSolutionSeq()); List keywords = solutionKeywordEntityRepository.findAllKeywordsBySolutionSeq(solutionEntity.getSolutionSeq()); From 1627a8baa808cb5803dcdcd2eeabedfd6d2928fd Mon Sep 17 00:00:00 2001 From: euics Date: Wed, 29 Oct 2025 21:50:55 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20log=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller: 메서드명 수정, 변수명 개선, null 체크 강화, 페이지 검증 추가 - Service: @Transactional 추가, @Transactional(readOnly = true) 추가 - Repository: limit/offset 계산 검증 추가 (음수 방지) --- .../controller/ExceptionLogController.java | 33 ++++++++++++------- .../ExceptionLogEntityRepositoryImpl.java | 6 ++-- .../log/service/ExceptionLogService.java | 3 ++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/main/java/startwithco/startwithbackend/log/controller/ExceptionLogController.java b/src/main/java/startwithco/startwithbackend/log/controller/ExceptionLogController.java index ca1ee8d..202bd87 100644 --- a/src/main/java/startwithco/startwithbackend/log/controller/ExceptionLogController.java +++ b/src/main/java/startwithco/startwithbackend/log/controller/ExceptionLogController.java @@ -21,10 +21,19 @@ public class ExceptionLogController { private final ExceptionLogService exceptionLogService; @GetMapping() - public String settlement(@RequestParam(defaultValue = "0") int page, @ModelAttribute String errorMessage, Model model) { - int pageSize = 20; + public String getExceptionLogs(@RequestParam(defaultValue = "0") int page, @ModelAttribute String errorMessage, Model model) { + final int pageSize = 20; int start = page * pageSize; int end = start + pageSize; + + if (start < 0 || end < start) { + model.addAttribute("errorMessage", "잘못된 페이지 번호입니다."); + model.addAttribute("exceptionLogs", List.of()); + model.addAttribute("hasNext", false); + model.addAttribute("currentPage", 0); + model.addAttribute("pageSize", pageSize); + return "log"; + } ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); @@ -32,12 +41,13 @@ public String settlement(@RequestParam(defaultValue = "0") int page, @ModelAttri List exceptionLogDtos = exceptionLogService.getAllExceptionLogEntity(start, end) .stream() .map(dto -> { - String raw = dto.getRequestBody(); - String pretty = raw; + // [수정] 변수명 개선 (raw, pretty -> requestBody, formattedRequestBody) + String requestBody = dto.getRequestBody(); + String formattedRequestBody = requestBody; try { - if (raw != null && raw.trim().startsWith("{")) { - var root = objectMapper.readTree(raw); + if (requestBody != null && !requestBody.trim().isEmpty() && requestBody.trim().startsWith("{")) { + var root = objectMapper.readTree(requestBody); // case 1: "request" 키가 있고, 그 값이 JSON 문자열이라면 → 이중 파싱 if (root.has("request") && root.get("request").isTextual()) { @@ -45,20 +55,19 @@ public String settlement(@RequestParam(defaultValue = "0") int page, @ModelAttri try { var nestedJson = objectMapper.readTree(nestedStr); - pretty = objectMapper.writerWithDefaultPrettyPrinter() + formattedRequestBody = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(nestedJson); } catch (Exception e) { - pretty = nestedStr; + formattedRequestBody = nestedStr; } } else { - // case 2: 일반 JSON 객체 → 바로 예쁘게 출력 - pretty = objectMapper.writerWithDefaultPrettyPrinter() + formattedRequestBody = objectMapper.writerWithDefaultPrettyPrinter() .writeValueAsString(root); } } } catch (Exception e) { - // JSON 파싱 실패 → 원본 그대로 + formattedRequestBody = requestBody; } return new ExceptionLogDto( @@ -67,7 +76,7 @@ public String settlement(@RequestParam(defaultValue = "0") int page, @ModelAttri dto.getErrorCode(), dto.getMessage(), dto.getRequestUri(), - pretty, + formattedRequestBody, dto.getMethodName() ); }) diff --git a/src/main/java/startwithco/startwithbackend/log/repository/ExceptionLogEntityRepositoryImpl.java b/src/main/java/startwithco/startwithbackend/log/repository/ExceptionLogEntityRepositoryImpl.java index 1226438..5e653b3 100644 --- a/src/main/java/startwithco/startwithbackend/log/repository/ExceptionLogEntityRepositoryImpl.java +++ b/src/main/java/startwithco/startwithbackend/log/repository/ExceptionLogEntityRepositoryImpl.java @@ -23,11 +23,13 @@ public ExceptionLogEntity saveExceptionLogEntity(ExceptionLogEntity exceptionLog public List findAll(int start, int end) { QExceptionLogEntity qExceptionLogEntity = QExceptionLogEntity.exceptionLogEntity; + int limit = Math.max(0, end - start); + return queryFactory .selectFrom(qExceptionLogEntity) .orderBy(qExceptionLogEntity.createdAt.desc()) - .offset(start) - .limit(end - start) + .offset(Math.max(0, start)) + .limit(limit) .fetch(); } } diff --git a/src/main/java/startwithco/startwithbackend/log/service/ExceptionLogService.java b/src/main/java/startwithco/startwithbackend/log/service/ExceptionLogService.java index f20bbeb..ebe5645 100644 --- a/src/main/java/startwithco/startwithbackend/log/service/ExceptionLogService.java +++ b/src/main/java/startwithco/startwithbackend/log/service/ExceptionLogService.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import startwithco.startwithbackend.log.domain.ExceptionLogEntity; import startwithco.startwithbackend.log.dto.ExceptionLogDto; import startwithco.startwithbackend.log.repository.ExceptionLogEntityRepository; @@ -14,6 +15,7 @@ public class ExceptionLogService { private final ExceptionLogEntityRepository exceptionLogEntityRepository; + @Transactional public void saveExceptionLogEntity(int status, String code, String message, String uri, String methodName, String logDetail) { ExceptionLogEntity exceptionLogEntity = ExceptionLogEntity.builder() .httpStatus(status) @@ -27,6 +29,7 @@ public void saveExceptionLogEntity(int status, String code, String message, Stri exceptionLogEntityRepository.saveExceptionLogEntity(exceptionLogEntity); } + @Transactional(readOnly = true) public List getAllExceptionLogEntity(int start, int end) { return exceptionLogEntityRepository.findAll(start, end).stream() .map(log -> new ExceptionLogDto( From 817963908e006366b4cb9a374959ac796ef13d06 Mon Sep 17 00:00:00 2001 From: euics Date: Wed, 29 Oct 2025 22:00:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20payment=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Service: null 체크 강화, 조건 로직 개선, switch default 추가, 예외 처리 강화 - Repository: join을 innerJoin으로 명시, limit/offset 검증 추가, @Param 패키지 수정 - Controller: null 체크 강화, 예외 처리 개선 - 매직 넘버 상수화 (TAX_RATE) - 불필요한 import 제거 --- .../PaymentEntityJpaRepository.java | 7 +-- .../PaymentEntityRepositoryImpl.java | 37 ++++++----- .../payment/service/PaymentService.java | 62 ++++++++++--------- .../controller/PaymentEventController.java | 7 +-- .../PaymentEventEntityJpaRepository.java | 2 +- .../service/PaymentEventService.java | 11 +++- 6 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityJpaRepository.java b/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityJpaRepository.java index 0e9504c..e39d5c2 100644 --- a/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityJpaRepository.java +++ b/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityJpaRepository.java @@ -1,11 +1,10 @@ package startwithco.startwithbackend.payment.payment.repository; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import startwithco.startwithbackend.payment.payment.domain.PaymentEntity; -import startwithco.startwithbackend.payment.paymentEvent.domain.PaymentEventEntity; import java.util.Optional; @@ -21,7 +20,7 @@ WHEN COUNT(p) = 0 THEN true AND p.paymentEventEntity.paymentEventSeq = :paymentEventSeq AND p.paymentStatus = 'IN_PROGRESS' """) - boolean canApproveTossPayment(@Param("orderId") String orderId, Long paymentEventSeq); + boolean canApproveTossPayment(@Param("orderId") String orderId, @Param("paymentEventSeq") Long paymentEventSeq); @Query(""" SELECT p @@ -51,7 +50,7 @@ SELECT COUNT(p) WHERE p.paymentEventEntity.vendorEntity.vendorSeq = :vendorSeq AND p.paymentStatus = 'SETTLED' """) - Long countSETTLEDStatusByVendorSeq(Long vendorSeq); + Long countSETTLEDStatusByVendorSeq(@Param("vendorSeq") Long vendorSeq); @Query(""" SELECT p diff --git a/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityRepositoryImpl.java b/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityRepositoryImpl.java index 5bca6da..3cb4d7d 100644 --- a/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityRepositoryImpl.java +++ b/src/main/java/startwithco/startwithbackend/payment/payment/repository/PaymentEntityRepositoryImpl.java @@ -9,7 +9,6 @@ import startwithco.startwithbackend.b2b.consumer.domain.QConsumerEntity; import startwithco.startwithbackend.b2b.vendor.domain.QVendorEntity; import startwithco.startwithbackend.b2b.vendor.domain.VendorEntity; -import startwithco.startwithbackend.exception.BadRequestException; import startwithco.startwithbackend.payment.payment.domain.PaymentEntity; import startwithco.startwithbackend.payment.payment.domain.QPaymentEntity; import startwithco.startwithbackend.payment.payment.util.PAYMENT_STATUS; @@ -72,13 +71,13 @@ public List findAllByConsumerSeqAndPaymentStatus(Long consumerSeq return queryFactory .selectFrom(qPaymentEntity) - .join(qPaymentEntity.paymentEventEntity, qPaymentEventEntity).fetchJoin() - .join(qPaymentEventEntity.solutionEntity, qSolutionEntity).fetchJoin() - .join(qPaymentEventEntity.vendorEntity, qVendorEntity).fetchJoin() + .innerJoin(qPaymentEntity.paymentEventEntity, qPaymentEventEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.solutionEntity, qSolutionEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.vendorEntity, qVendorEntity).fetchJoin() .where(builder) .orderBy(qPaymentEntity.paymentCompletedAt.desc()) - .offset(start) - .limit(end - start) + .offset(Math.max(0, start)) + .limit(Math.max(0, end - start)) .fetch(); } @@ -115,14 +114,14 @@ public List findAllByVendorSeqAndPaymentStatus(Long vendorSeq, St return queryFactory .selectFrom(qPaymentEntity) - .join(qPaymentEntity.paymentEventEntity, qPaymentEventEntity).fetchJoin() - .join(qPaymentEventEntity.vendorEntity, qVendorEntity).fetchJoin() - .join(qPaymentEventEntity.consumerEntity, qConsumerEntity).fetchJoin() - .join(qPaymentEventEntity.solutionEntity, qSolutionEntity).fetchJoin() + .innerJoin(qPaymentEntity.paymentEventEntity, qPaymentEventEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.vendorEntity, qVendorEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.consumerEntity, qConsumerEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.solutionEntity, qSolutionEntity).fetchJoin() .where(builder) .orderBy(qPaymentEntity.paymentCompletedAt.desc()) - .offset(start) - .limit(end - start) + .offset(Math.max(0, start)) + .limit(Math.max(0, end - start)) .fetch(); } @@ -137,18 +136,18 @@ public List findAll(int start, int end) { return queryFactory .selectFrom(qPaymentEntity) - .join(qPaymentEntity.paymentEventEntity, qPaymentEventEntity).fetchJoin() - .join(qPaymentEventEntity.consumerEntity, qConsumerEntity).fetchJoin() - .join(qPaymentEventEntity.vendorEntity, qVendorEntity).fetchJoin() - .join(qPaymentEventEntity.solutionEntity, qSolutionEntity).fetchJoin() + .innerJoin(qPaymentEntity.paymentEventEntity, qPaymentEventEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.consumerEntity, qConsumerEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.vendorEntity, qVendorEntity).fetchJoin() + .innerJoin(qPaymentEventEntity.solutionEntity, qSolutionEntity).fetchJoin() .where(qPaymentEntity.orderId.in( JPAExpressions .select(qTossPaymentDailySnapshotEntity.orderId) .from(qTossPaymentDailySnapshotEntity) )) .orderBy(qPaymentEntity.paymentCompletedAt.desc()) - .offset(start) - .limit(end - start) + .offset(Math.max(0, start)) + .limit(Math.max(0, end - start)) .fetch(); } @@ -171,7 +170,7 @@ public boolean existsConflictPaymentEntity(VendorEntity vendor, ConsumerEntity c pe.consumerEntity.eq(consumer), pe.solutionEntity.eq(solution), p.isNull().or(p.paymentStatus.in( - PAYMENT_STATUS.IN_PROGRESS, PAYMENT_STATUS.WAITING_FOR_DEPOSIT + IN_PROGRESS, WAITING_FOR_DEPOSIT )) ) .fetchFirst(); diff --git a/src/main/java/startwithco/startwithbackend/payment/payment/service/PaymentService.java b/src/main/java/startwithco/startwithbackend/payment/payment/service/PaymentService.java index a2d1479..5ea98d9 100644 --- a/src/main/java/startwithco/startwithbackend/payment/payment/service/PaymentService.java +++ b/src/main/java/startwithco/startwithbackend/payment/payment/service/PaymentService.java @@ -75,6 +75,12 @@ public Mono tossPaymentApproval(TossPaymentApprovalRequest request) { "이미 해당 결제 요청에 대한 결제 정보가 존재합니다. 새롭게 결제 요청을 진행해야합니다.", getCode("이미 해당 결제 요청에 대한 결제 정보가 존재합니다. 새롭게 결제 요청을 진행해야합니다.", ExceptionType.CONFLICT) ); + } catch (Exception e) { + throw new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + e.getMessage(), + getCode(e.getMessage(), ExceptionType.SERVER) + ); } return commonService.executeTossPaymentApproval( @@ -104,9 +110,7 @@ public Mono tossPaymentApproval(TossPaymentApprovalRequest request) { json.path("receipt").path("url").asText(), paymentEventEntity.getSolutionEntity().getCategory() )); - } - - if ("가상계좌".equals(method)) { + } else if ("가상계좌".equals(method)) { LocalDateTime requestedAt = OffsetDateTime.parse(json.path("requestedAt").asText()).toLocalDateTime(); String secret = json.path("secret").asText(null); @@ -130,9 +134,7 @@ public Mono tossPaymentApproval(TossPaymentApprovalRequest request) { json.path("receipt").path("url").asText(), paymentEventEntity.getSolutionEntity().getCategory() )); - } - - if ("간편결제".equals(method)) { + } else if ("간편결제".equals(method)) { paymentEntity.updateEasyPayDONEStatus(); paymentEntityRepository.savePaymentEntity(paymentEntity); @@ -146,19 +148,19 @@ public Mono tossPaymentApproval(TossPaymentApprovalRequest request) { json.path("receipt").path("url").asText(null), paymentEventEntity.getSolutionEntity().getCategory() )); + } else { + return Mono.error(new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + "지원하지 않는 결제 수단입니다.", + getCode("지원하지 않는 결제 수단입니다.", ExceptionType.SERVER) + )); } - return Mono.error(new ServerException( - HttpStatus.INTERNAL_SERVER_ERROR.value(), - "지원하지 않는 결제 수단입니다.", - getCode("지원하지 않는 결제 수단입니다.", ExceptionType.SERVER) - )); - } catch (Exception e) { return Mono.error(new ServerException( HttpStatus.INTERNAL_SERVER_ERROR.value(), - e.getMessage(), - getCode(e.getMessage(), ExceptionType.SERVER) + "결제 응답 파싱 중 오류가 발생했습니다.", + getCode("결제 응답 파싱 중 오류가 발생했습니다.", ExceptionType.SERVER) )); } }).onErrorResume(e -> { @@ -167,8 +169,8 @@ public Mono tossPaymentApproval(TossPaymentApprovalRequest request) { return Mono.error(new ServerException( HttpStatus.INTERNAL_SERVER_ERROR.value(), - e.getMessage(), - getCode(e.getMessage(), ExceptionType.SERVER) + "토스페이먼츠 결제 승인 실패", + getCode("토스페이먼츠 결제 승인 실패", ExceptionType.SERVER) )); }); } @@ -182,7 +184,7 @@ public void refundTossPaymentApprovalRequest(RefundTossPaymentApprovalRequest re getCode("존재하지 않는 결제입니다.", ExceptionType.NOT_FOUND) )); - if (paymentEntity.getDueDate().isBefore(LocalDateTime.now())) { + if (paymentEntity.getDueDate() != null && paymentEntity.getDueDate().isBefore(LocalDateTime.now())) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "해당 결제는 마감 시간이 지났습니다.", @@ -190,7 +192,7 @@ public void refundTossPaymentApprovalRequest(RefundTossPaymentApprovalRequest re ); } - if(paymentEntity.getPaymentStatus() == PAYMENT_STATUS.CANCELLED) { + if (paymentEntity.getPaymentStatus() == PAYMENT_STATUS.CANCELLED) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "이미 환불 처리된 건입니다.", @@ -198,23 +200,22 @@ public void refundTossPaymentApprovalRequest(RefundTossPaymentApprovalRequest re ); } - if (!(paymentEntity.getMethod() == METHOD.VIRTUAL_ACCOUNT - && paymentEntity.getPaymentStatus() == PAYMENT_STATUS.DONE)) { - if (request.bankCode() != null || request.accountNumber() != null || request.holderName() != null) { - throw new BadRequestException( - HttpStatus.BAD_REQUEST.value(), - "가상계좌 결제 완료 상태가 아닐 경우 환불 계좌 정보를 입력할 수 없습니다.", - getCode("가상계좌 결제 완료 상태가 아닐 경우 환불 계좌 정보를 입력할 수 없습니다.", ExceptionType.BAD_REQUEST) - ); - } + boolean isVirtualAccountDone = paymentEntity.getMethod() == METHOD.VIRTUAL_ACCOUNT + && paymentEntity.getPaymentStatus() == PAYMENT_STATUS.DONE; + + if (!isVirtualAccountDone && (request.bankCode() != null || request.accountNumber() != null || request.holderName() != null)) { + throw new BadRequestException( + HttpStatus.BAD_REQUEST.value(), + "가상계좌 결제 완료 상태가 아닐 경우 환불 계좌 정보를 입력할 수 없습니다.", + getCode("가상계좌 결제 완료 상태가 아닐 경우 환불 계좌 정보를 입력할 수 없습니다.", ExceptionType.BAD_REQUEST) + ); } String bankCode = null; String accountNumber = null; String holderName = null; - // 가상계좌 + 결제 완료 상태일 경우에만 환불 계좌 필요 - if (paymentEntity.getMethod().equals(METHOD.VIRTUAL_ACCOUNT) && + if (paymentEntity.getMethod() != null && paymentEntity.getMethod().equals(METHOD.VIRTUAL_ACCOUNT) && paymentEntity.getPaymentStatus().equals(PAYMENT_STATUS.DONE)) { bankCode = request.bankCode(); accountNumber = request.accountNumber(); @@ -254,6 +255,9 @@ public void tossPaymentDepositCallBack(TossPaymentDepositCallBackRequest request paymentEntity.updateCANCELStatus(paymentCompletedAt); paymentEntityRepository.savePaymentEntity(paymentEntity); } + default -> { + log.warn("Unhandled payment status: {}", request.status()); + } } } diff --git a/src/main/java/startwithco/startwithbackend/payment/paymentEvent/controller/PaymentEventController.java b/src/main/java/startwithco/startwithbackend/payment/paymentEvent/controller/PaymentEventController.java index 996200b..794bb62 100644 --- a/src/main/java/startwithco/startwithbackend/payment/paymentEvent/controller/PaymentEventController.java +++ b/src/main/java/startwithco/startwithbackend/payment/paymentEvent/controller/PaymentEventController.java @@ -10,7 +10,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import startwithco.startwithbackend.base.BaseResponse; @@ -18,8 +17,6 @@ import startwithco.startwithbackend.exception.code.ExceptionCodeMapper; import startwithco.startwithbackend.exception.handler.GlobalExceptionHandler; import startwithco.startwithbackend.payment.paymentEvent.service.PaymentEventService; -import startwithco.startwithbackend.solution.effect.util.DIRECTION; -import startwithco.startwithbackend.solution.solution.controller.request.SolutionRequest; import startwithco.startwithbackend.solution.solution.util.CATEGORY; import static startwithco.startwithbackend.exception.code.ExceptionCodeMapper.*; @@ -61,7 +58,7 @@ public ResponseEntity> savePaymentE @RequestPart(value = "refundPolicyUrl", required = false) MultipartFile refundPolicyUrl, @RequestPart SavePaymentEventRequest request ) { - if (contractConfirmationUrl.isEmpty() || refundPolicyUrl.isEmpty()) { + if (contractConfirmationUrl == null || contractConfirmationUrl.isEmpty() || refundPolicyUrl == null || refundPolicyUrl.isEmpty()) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "요청 데이터 오류입니다.", @@ -167,7 +164,7 @@ public ResponseEntity> getPaymentEventConflict( try { CATEGORY.valueOf(category.toUpperCase()); - } catch (Exception e) { + } catch (IllegalArgumentException e) { throw new BadRequestException( HttpStatus.BAD_REQUEST.value(), "요청 데이터 오류입니다.", diff --git a/src/main/java/startwithco/startwithbackend/payment/paymentEvent/repository/PaymentEventEntityJpaRepository.java b/src/main/java/startwithco/startwithbackend/payment/paymentEvent/repository/PaymentEventEntityJpaRepository.java index 65ffdb8..89ed5de 100644 --- a/src/main/java/startwithco/startwithbackend/payment/paymentEvent/repository/PaymentEventEntityJpaRepository.java +++ b/src/main/java/startwithco/startwithbackend/payment/paymentEvent/repository/PaymentEventEntityJpaRepository.java @@ -1,6 +1,6 @@ package startwithco.startwithbackend.payment.paymentEvent.repository; -import io.lettuce.core.dynamic.annotation.Param; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; diff --git a/src/main/java/startwithco/startwithbackend/payment/paymentEvent/service/PaymentEventService.java b/src/main/java/startwithco/startwithbackend/payment/paymentEvent/service/PaymentEventService.java index bf76f56..b2bdc3d 100644 --- a/src/main/java/startwithco/startwithbackend/payment/paymentEvent/service/PaymentEventService.java +++ b/src/main/java/startwithco/startwithbackend/payment/paymentEvent/service/PaymentEventService.java @@ -13,6 +13,7 @@ import startwithco.startwithbackend.b2b.vendor.repository.VendorEntityRepository; import startwithco.startwithbackend.common.service.CommonService; import startwithco.startwithbackend.exception.ConflictException; +import startwithco.startwithbackend.exception.ServerException; import startwithco.startwithbackend.payment.payment.domain.PaymentEntity; import startwithco.startwithbackend.payment.payment.repository.PaymentEntityRepository; import startwithco.startwithbackend.payment.payment.util.PAYMENT_STATUS; @@ -23,7 +24,6 @@ import startwithco.startwithbackend.solution.solution.domain.SolutionEntity; import startwithco.startwithbackend.solution.solution.repository.SolutionEntityRepository; -import java.util.List; import static startwithco.startwithbackend.exception.code.ExceptionCodeMapper.*; import static startwithco.startwithbackend.payment.paymentEvent.controller.request.PaymentEventRequest.*; @@ -67,8 +67,9 @@ public SavePaymentEventEntityResponse savePaymentEventEntity(SavePaymentEventReq String s3ContractConfirmationUrl = commonService.uploadPDFFile(contractConfirmationUrl); String s3RefundPolicyUrl = commonService.uploadPDFFile(refundPolicyUrl); + final double TAX_RATE = 0.1; Long amount = solutionEntity.getAmount(); - Long tax = (long) (amount * 0.1); + Long tax = (long) (amount * TAX_RATE); Long actualAmount = amount + tax; PaymentEventEntity paymentEventEntity = PaymentEventEntity.builder() @@ -94,6 +95,12 @@ public SavePaymentEventEntityResponse savePaymentEventEntity(SavePaymentEventReq "동시성 저장은 불가능합니다.", getCode("동시성 저장은 불가능합니다.", ExceptionType.CONFLICT) ); + } catch (Exception e) { + throw new ServerException( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + e.getMessage(), + getCode(e.getMessage(), ExceptionType.SERVER) + ); } } From 715cde790f75dcbe184a244ae23f02acd99c19ac Mon Sep 17 00:00:00 2001 From: euics Date: Wed, 29 Oct 2025 22:08:33 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20exception=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Exception 클래스: 코드 포맷팅 개선 (공백 제거) - ExceptionCodeMapper: 오타 수정 (NOT_FOUNE → NOT_FOUND) - GlobalExceptionHandler: 상수 분리, ObjectMapper 재사용, 변수명 개선, 타입 안전성 개선 --- .../exception/BadRequestException.java | 2 +- .../exception/ConflictException.java | 2 +- .../exception/NotFoundException.java | 2 +- .../exception/ServerException.java | 2 +- .../exception/code/ExceptionCodeMapper.java | 2 +- .../handler/GlobalExceptionHandler.java | 40 ++++++++++++------- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/main/java/startwithco/startwithbackend/exception/BadRequestException.java b/src/main/java/startwithco/startwithbackend/exception/BadRequestException.java index 83d5dca..abf97cb 100644 --- a/src/main/java/startwithco/startwithbackend/exception/BadRequestException.java +++ b/src/main/java/startwithco/startwithbackend/exception/BadRequestException.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public class BadRequestException extends CustomBaseException { +public class BadRequestException extends CustomBaseException { public BadRequestException(int httpStatus, String message, String code) { super(message, httpStatus, code); } diff --git a/src/main/java/startwithco/startwithbackend/exception/ConflictException.java b/src/main/java/startwithco/startwithbackend/exception/ConflictException.java index c7f770a..b4d25e2 100644 --- a/src/main/java/startwithco/startwithbackend/exception/ConflictException.java +++ b/src/main/java/startwithco/startwithbackend/exception/ConflictException.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public class ConflictException extends CustomBaseException { +public class ConflictException extends CustomBaseException { public ConflictException(int httpStatus, String message, String code) { super(message, httpStatus, code); } diff --git a/src/main/java/startwithco/startwithbackend/exception/NotFoundException.java b/src/main/java/startwithco/startwithbackend/exception/NotFoundException.java index e78f813..44b56e6 100644 --- a/src/main/java/startwithco/startwithbackend/exception/NotFoundException.java +++ b/src/main/java/startwithco/startwithbackend/exception/NotFoundException.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public class NotFoundException extends CustomBaseException { +public class NotFoundException extends CustomBaseException { public NotFoundException(int httpStatus, String message, String code) { super(message, httpStatus, code); } diff --git a/src/main/java/startwithco/startwithbackend/exception/ServerException.java b/src/main/java/startwithco/startwithbackend/exception/ServerException.java index 8789293..a094921 100644 --- a/src/main/java/startwithco/startwithbackend/exception/ServerException.java +++ b/src/main/java/startwithco/startwithbackend/exception/ServerException.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public class ServerException extends CustomBaseException { +public class ServerException extends CustomBaseException { public ServerException(int httpStatus, String message, String code) { super(message, httpStatus, code); } diff --git a/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java b/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java index 1bd0a54..271b3d0 100644 --- a/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java +++ b/src/main/java/startwithco/startwithbackend/exception/code/ExceptionCodeMapper.java @@ -71,7 +71,7 @@ public static String getCode(String message, ExceptionType type) { return switch (type) { case BAD_REQUEST -> BAD_REQUEST_MAP.getOrDefault(message, "BAD_REQUEST_EXCEPTION_예외코드 설정하세요."); case CONFLICT -> CONFLICT_MAP.getOrDefault(message, "CONFLICT_EXCEPTION_예외코드 설정하세요."); - case NOT_FOUND -> NOT_FOUND_MAP.getOrDefault(message, "NOT_FOUNE_EXCEPTION_예외코드 설정하세요."); + case NOT_FOUND -> NOT_FOUND_MAP.getOrDefault(message, "NOT_FOUND_EXCEPTION_예외코드 설정하세요."); case SERVER -> SERVER_MAP.getOrDefault(message, "SERVER_EXCEPTION_예외코드 설정하세요."); case UNAUTHORIZED -> UNAUTHORIZED_MAP.getOrDefault(message, "UNAUTHORIZED_EXCEPTION_예외코드 설정하세요."); }; diff --git a/src/main/java/startwithco/startwithbackend/exception/handler/GlobalExceptionHandler.java b/src/main/java/startwithco/startwithbackend/exception/handler/GlobalExceptionHandler.java index a6af56d..1a0cc1d 100644 --- a/src/main/java/startwithco/startwithbackend/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/startwithco/startwithbackend/exception/handler/GlobalExceptionHandler.java @@ -20,6 +20,12 @@ @RestControllerAdvice @RequiredArgsConstructor public class GlobalExceptionHandler { + // [수정] ObjectMapper를 인스턴스 변수로 분리하여 재사용성 향상 + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String EMPTY_BODY = "[본문 없음]"; + private static final String EXTRACTION_FAILED = "[본문 추출 실패]"; + private static final String UNKNOWN_METHOD = "UNKNOWN"; private final ExceptionLogService exceptionLogService; @@ -27,9 +33,10 @@ public class GlobalExceptionHandler { public ResponseEntity handleCustomException(final RuntimeException exception, final HttpServletRequest request) { if (exception instanceof CustomBaseException ex) { + // [수정] 스택 트레이스 존재 여부 확인 로직 개선 String methodName = ex.getStackTrace().length > 0 ? ex.getStackTrace()[0].toString() - : "UNKNOWN"; + : UNKNOWN_METHOD; String requestBody = getRequestBody(request); @@ -46,17 +53,18 @@ public ResponseEntity handleCustomException(final RuntimeExceptio .body(new ErrorResponse(ex.getHttpStatus(), ex.getMessage(), ex.getCode())); } + // [수정] 매직 넘버를 상수로 분리 + final int INTERNAL_SERVER_ERROR_STATUS = 500; log.error("Unhandled exception caught: ", exception); - return ResponseEntity.status(500) - .body(new ErrorResponse(500, "서버 내부 오류", "INTERNAL_SERVER_ERROR")); + return ResponseEntity.status(INTERNAL_SERVER_ERROR_STATUS) + .body(new ErrorResponse(INTERNAL_SERVER_ERROR_STATUS, "서버 내부 오류", "INTERNAL_SERVER_ERROR")); } private String getRequestBody(HttpServletRequest request) { try { String contentType = request.getContentType(); - ObjectMapper mapper = new ObjectMapper(); - if (contentType != null && contentType.contains("application/json")) { + if (contentType != null && contentType.contains(CONTENT_TYPE_JSON)) { StringBuilder sb = new StringBuilder(); try (BufferedReader reader = request.getReader()) { String line; @@ -65,29 +73,31 @@ private String getRequestBody(HttpServletRequest request) { } } - String raw = sb.toString(); - if (raw.isBlank()) return "[본문 없음]"; + // [수정] 변수명 개선 (raw -> requestBody) + String requestBody = sb.toString(); + if (requestBody.isBlank()) return EMPTY_BODY; - Map json = mapper.readValue(raw, Map.class); + @SuppressWarnings("unchecked") + Map json = OBJECT_MAPPER.readValue(requestBody, Map.class); Object requestData = json.get("request"); if (requestData instanceof String str) { try { - Object nested = mapper.readValue(str, Object.class); - return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(nested); + Object nested = OBJECT_MAPPER.readValue(str, Object.class); + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(nested); } catch (Exception e) { return str; } } if (requestData != null) { - return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestData); + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(requestData); } - return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json); + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(json); } else { Map paramMap = request.getParameterMap(); - if (paramMap.isEmpty()) return "[본문 없음]"; + if (paramMap.isEmpty()) return EMPTY_BODY; Map resultMap = new LinkedHashMap<>(); for (Map.Entry entry : paramMap.entrySet()) { @@ -96,10 +106,10 @@ private String getRequestBody(HttpServletRequest request) { resultMap.put(key, values.length == 1 ? values[0] : Arrays.asList(values)); } - return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultMap); + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(resultMap); } } catch (Exception e) { - return "[본문 추출 실패]"; + return EXTRACTION_FAILED; } }