diff --git a/src/main/java/starlight/adapter/ncp/clova/infra/ClovaStudioClient.java b/src/main/java/starlight/adapter/aireport/infrastructure/clova/infra/ClovaStudioClient.java similarity index 64% rename from src/main/java/starlight/adapter/ncp/clova/infra/ClovaStudioClient.java rename to src/main/java/starlight/adapter/aireport/infrastructure/clova/infra/ClovaStudioClient.java index 07e35068..55a5a4ab 100644 --- a/src/main/java/starlight/adapter/ncp/clova/infra/ClovaStudioClient.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/clova/infra/ClovaStudioClient.java @@ -1,20 +1,27 @@ -package starlight.adapter.ncp.clova.infra; +package starlight.adapter.aireport.infrastructure.clova.infra; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import starlight.shared.dto.infrastructure.ClovaStudioResponse; -import starlight.adapter.ncp.clova.util.ClovaUtil; +import starlight.adapter.aireport.infrastructure.clova.util.ClovaUtil; import java.util.Map; +/** + * Clova Studio API 호출 클라이언트. + * + * @deprecated 1.4.0 현재 사용 경로가 없으며, 필요 시 재도입할 수 있어 유지 중입니다. + * 대체 구현체는 없습니다. + */ @Component +@Deprecated(since = "1.4.0", forRemoval = false) public class ClovaStudioClient { private final RestClient restClient; - public ClovaStudioClient(@Qualifier("clovaClient") RestClient restClient) { + public ClovaStudioClient(@Qualifier("clovaStudioRestClient") RestClient restClient) { this.restClient = restClient; } @@ -28,4 +35,4 @@ public ClovaStudioResponse check(String systemMsg, String userMsg, int criteriaS .retrieve() .body(ClovaStudioResponse.class); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/adapter/ncp/clova/util/ClovaUtil.java b/src/main/java/starlight/adapter/aireport/infrastructure/clova/util/ClovaUtil.java similarity index 89% rename from src/main/java/starlight/adapter/ncp/clova/util/ClovaUtil.java rename to src/main/java/starlight/adapter/aireport/infrastructure/clova/util/ClovaUtil.java index 3513f6a6..1a7cbb26 100644 --- a/src/main/java/starlight/adapter/ncp/clova/util/ClovaUtil.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/clova/util/ClovaUtil.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.clova.util; +package starlight.adapter.aireport.infrastructure.clova.util; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -8,6 +8,13 @@ import java.util.List; import java.util.Map; +/** + * Clova Studio API 요청/응답 보조 유틸리티. + * + * @deprecated 1.4.0 현재 사용 경로가 없으며, 필요 시 재도입할 수 있어 유지 중입니다. + * 대체 구현체는 없습니다. + */ +@Deprecated(since = "1.4.0", forRemoval = false) public final class ClovaUtil { public static Map buildClovaRequestBody(String systemMsg, String userMsg, int n){ diff --git a/src/main/java/starlight/adapter/ncp/ocr/ClovaOcrProvider.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProvider.java similarity index 80% rename from src/main/java/starlight/adapter/ncp/ocr/ClovaOcrProvider.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProvider.java index 0711e00b..d36d7709 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/ClovaOcrProvider.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProvider.java @@ -1,15 +1,15 @@ -package starlight.adapter.ncp.ocr; +package starlight.adapter.aireport.infrastructure.ocr; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import starlight.adapter.ncp.ocr.exception.OcrException; -import starlight.adapter.ncp.ocr.infra.ClovaOcrClient; -import starlight.adapter.ncp.ocr.infra.PdfDownloadClient; -import starlight.adapter.ncp.ocr.util.OcrResponseMerger; -import starlight.adapter.ncp.ocr.util.OcrTextExtractor; -import starlight.adapter.ncp.ocr.util.PdfUtils; -import starlight.application.infrastructure.provided.OcrProvider; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.infra.ClovaOcrClient; +import starlight.adapter.aireport.infrastructure.ocr.infra.PdfDownloadClient; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrResponseMerger; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrTextExtractor; +import starlight.adapter.aireport.infrastructure.ocr.util.PdfUtils; +import starlight.application.aireport.required.OcrProvider; import starlight.shared.dto.infrastructure.OcrResponse; import java.util.ArrayList; diff --git a/src/main/java/starlight/adapter/ncp/ocr/dto/ClovaOcrRequest.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/dto/ClovaOcrRequest.java similarity index 95% rename from src/main/java/starlight/adapter/ncp/ocr/dto/ClovaOcrRequest.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/dto/ClovaOcrRequest.java index 251a019f..c1ab7dcd 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/dto/ClovaOcrRequest.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/dto/ClovaOcrRequest.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.dto; +package starlight.adapter.aireport.infrastructure.ocr.dto; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/src/main/java/starlight/adapter/ncp/ocr/exception/OcrErrorType.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/exception/OcrErrorType.java similarity index 92% rename from src/main/java/starlight/adapter/ncp/ocr/exception/OcrErrorType.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/exception/OcrErrorType.java index 263e7180..b5dd6713 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/exception/OcrErrorType.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/exception/OcrErrorType.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.exception; +package starlight.adapter.aireport.infrastructure.ocr.exception; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/starlight/adapter/ncp/ocr/exception/OcrException.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/exception/OcrException.java similarity index 79% rename from src/main/java/starlight/adapter/ncp/ocr/exception/OcrException.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/exception/OcrException.java index 31128d49..3cdbfed9 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/exception/OcrException.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/exception/OcrException.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.exception; +package starlight.adapter.aireport.infrastructure.ocr.exception; import starlight.shared.apiPayload.exception.ErrorType; import starlight.shared.apiPayload.exception.GlobalException; diff --git a/src/main/java/starlight/adapter/ncp/ocr/infra/ClovaOcrClient.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/infra/ClovaOcrClient.java similarity index 80% rename from src/main/java/starlight/adapter/ncp/ocr/infra/ClovaOcrClient.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/infra/ClovaOcrClient.java index 9427738b..6b75a4c5 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/infra/ClovaOcrClient.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/infra/ClovaOcrClient.java @@ -1,12 +1,12 @@ -package starlight.adapter.ncp.ocr.infra; +package starlight.adapter.aireport.infrastructure.ocr.infra; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import starlight.adapter.ncp.ocr.dto.ClovaOcrRequest; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.dto.ClovaOcrRequest; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; import starlight.shared.dto.infrastructure.OcrResponse; @Slf4j diff --git a/src/main/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClient.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClient.java similarity index 88% rename from src/main/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClient.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClient.java index 188532bb..48f7f53f 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClient.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClient.java @@ -1,12 +1,12 @@ -package starlight.adapter.ncp.ocr.infra; +package starlight.adapter.aireport.infrastructure.ocr.infra; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; import java.net.URI; @@ -18,7 +18,7 @@ public class PdfDownloadClient { private final RestClient pdfDownloadClient; - public PdfDownloadClient(@Qualifier("downloadClient") RestClient downloadClient) { + public PdfDownloadClient(@Qualifier("pdfDownloadRestClient") RestClient downloadClient) { this.pdfDownloadClient = downloadClient; } diff --git a/src/main/java/starlight/adapter/ncp/ocr/util/OcrResponseMerger.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrResponseMerger.java similarity index 92% rename from src/main/java/starlight/adapter/ncp/ocr/util/OcrResponseMerger.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrResponseMerger.java index 4c2f21cb..13829709 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/util/OcrResponseMerger.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrResponseMerger.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import starlight.shared.dto.infrastructure.OcrResponse; diff --git a/src/main/java/starlight/adapter/ncp/ocr/util/OcrTextExtractor.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrTextExtractor.java similarity index 98% rename from src/main/java/starlight/adapter/ncp/ocr/util/OcrTextExtractor.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrTextExtractor.java index e52f872a..54b2c684 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/util/OcrTextExtractor.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrTextExtractor.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import starlight.shared.dto.infrastructure.OcrResponse; diff --git a/src/main/java/starlight/adapter/ncp/ocr/util/PdfUtils.java b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/PdfUtils.java similarity index 90% rename from src/main/java/starlight/adapter/ncp/ocr/util/PdfUtils.java rename to src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/PdfUtils.java index c11eab05..b681d3b1 100644 --- a/src/main/java/starlight/adapter/ncp/ocr/util/PdfUtils.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/ocr/util/PdfUtils.java @@ -1,9 +1,9 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.pdmodel.PDDocument; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/src/main/java/starlight/adapter/ncp/storage/NcpPresignedUrlProvider.java b/src/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.java similarity index 94% rename from src/main/java/starlight/adapter/ncp/storage/NcpPresignedUrlProvider.java rename to src/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.java index eca9cb4c..9c04da39 100644 --- a/src/main/java/starlight/adapter/ncp/storage/NcpPresignedUrlProvider.java +++ b/src/main/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProvider.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.storage; +package starlight.adapter.aireport.infrastructure.storage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,7 +12,7 @@ import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; -import starlight.application.infrastructure.provided.PresignedUrlProvider; +import starlight.application.aireport.required.PresignedUrlProvider; import starlight.shared.dto.infrastructure.PreSignedUrlResponse; import java.net.URLEncoder; @@ -25,7 +25,7 @@ public class NcpPresignedUrlProvider implements PresignedUrlProvider { private final S3Client ncpS3Client; - private final S3Presigner s3Presigner; + private final S3Presigner ncpS3Presigner; @Value("${cloud.ncp.object-storage.bucket-name}") private String bucket; @@ -55,7 +55,7 @@ public PreSignedUrlResponse getPreSignedUrl(Long userId, String originalFileName .putObjectRequest(putObjectRequest) .build(); - PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest); + PresignedPutObjectRequest presignedRequest = ncpS3Presigner.presignPutObject(presignRequest); String presignedUrl = presignedRequest.url().toString(); String objectUrl = buildObjectUrl(key); diff --git a/src/main/java/starlight/adapter/ncp/webapi/ImageController.java b/src/main/java/starlight/adapter/aireport/webapi/ImageController.java similarity index 86% rename from src/main/java/starlight/adapter/ncp/webapi/ImageController.java rename to src/main/java/starlight/adapter/aireport/webapi/ImageController.java index 7ff465b4..061f20f5 100644 --- a/src/main/java/starlight/adapter/ncp/webapi/ImageController.java +++ b/src/main/java/starlight/adapter/aireport/webapi/ImageController.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.webapi; +package starlight.adapter.aireport.webapi; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -6,8 +6,8 @@ import org.springframework.web.bind.annotation.*; import starlight.adapter.auth.security.auth.AuthDetails; import starlight.shared.dto.infrastructure.PreSignedUrlResponse; -import starlight.application.infrastructure.provided.PresignedUrlProvider; -import starlight.adapter.ncp.webapi.swagger.ImageApiDoc; +import starlight.application.aireport.required.PresignedUrlProvider; +import starlight.adapter.aireport.webapi.swagger.ImageApiDoc; import starlight.shared.apiPayload.response.ApiResponse; @RestController diff --git a/src/main/java/starlight/adapter/ncp/webapi/swagger/ImageApiDoc.java b/src/main/java/starlight/adapter/aireport/webapi/swagger/ImageApiDoc.java similarity index 98% rename from src/main/java/starlight/adapter/ncp/webapi/swagger/ImageApiDoc.java rename to src/main/java/starlight/adapter/aireport/webapi/swagger/ImageApiDoc.java index 8919e99e..d69e4d87 100644 --- a/src/main/java/starlight/adapter/ncp/webapi/swagger/ImageApiDoc.java +++ b/src/main/java/starlight/adapter/aireport/webapi/swagger/ImageApiDoc.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.webapi.swagger; +package starlight.adapter.aireport.webapi.swagger; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; diff --git a/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java b/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java index 03ecf615..3173b666 100644 --- a/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java +++ b/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java @@ -5,11 +5,9 @@ import org.springframework.stereotype.Component; import starlight.application.expert.required.ExpertQueryPort; import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; import starlight.domain.expert.exception.ExpertErrorType; import starlight.domain.expert.exception.ExpertException; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -62,27 +60,6 @@ public List findAllWithCareersTagsCategories() { } } - @Override - public List findByAllCategories(Collection categories) { - try { - List experts = repository.findByAllCategories(categories, categories.size()); - if (experts.isEmpty()) { - return experts; - } - - List ids = experts.stream() - .map(Expert::getId) - .toList(); - - fetchWithCollections(ids); - - return experts; - } catch (Exception e) { - log.error("전문가 목록 필터링 중 오류가 발생했습니다.", e); - throw new ExpertException(ExpertErrorType.EXPERT_QUERY_ERROR); - } - } - @Override public Map findByIds(Set expertIds) { List experts = repository.findAllByIds(expertIds); diff --git a/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java b/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java index c4de2cb9..6d22b0d4 100644 --- a/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java +++ b/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java @@ -4,9 +4,6 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; - -import java.util.Collection; import java.util.List; import java.util.Set; @@ -27,15 +24,4 @@ public interface ExpertRepository extends JpaRepository { @Query("select distinct e from Expert e where e.id in :expertIds") List findAllByIds(Set expertIds); - @Query(""" - select distinct e from Expert e where e.id in ( - select e2.id - from Expert e2 - join e2.categories c2 - where c2 in :cats - group by e2.id - having count(distinct c2) = :size) - """) - List findByAllCategories(@Param("cats") Collection cats, - @Param("size") long size); } diff --git a/src/main/java/starlight/adapter/expert/webapi/ExpertController.java b/src/main/java/starlight/adapter/expert/webapi/ExpertController.java index 95748612..c9ed683d 100644 --- a/src/main/java/starlight/adapter/expert/webapi/ExpertController.java +++ b/src/main/java/starlight/adapter/expert/webapi/ExpertController.java @@ -4,17 +4,14 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import starlight.adapter.expert.webapi.dto.ExpertDetailResponse; import starlight.adapter.expert.webapi.dto.ExpertListResponse; import starlight.adapter.expert.webapi.swagger.ExpertQueryApiDoc; import starlight.application.expert.provided.ExpertDetailQueryUseCase; -import starlight.domain.expert.enumerate.TagCategory; import starlight.shared.apiPayload.response.ApiResponse; import java.util.List; -import java.util.Set; @RestController @RequiredArgsConstructor @@ -24,10 +21,8 @@ public class ExpertController implements ExpertQueryApiDoc { private final ExpertDetailQueryUseCase expertDetailQuery; @GetMapping - public ApiResponse> search( - @RequestParam(name = "categories", required = false) Set categories - ) { - return ApiResponse.success(ExpertListResponse.fromAll(expertDetailQuery.search(categories))); + public ApiResponse> search() { + return ApiResponse.success(ExpertListResponse.fromAll(expertDetailQuery.searchAll())); } @GetMapping("/{expertId}") diff --git a/src/main/java/starlight/adapter/expert/webapi/swagger/ExpertQueryApiDoc.java b/src/main/java/starlight/adapter/expert/webapi/swagger/ExpertQueryApiDoc.java index c578be96..25baa299 100644 --- a/src/main/java/starlight/adapter/expert/webapi/swagger/ExpertQueryApiDoc.java +++ b/src/main/java/starlight/adapter/expert/webapi/swagger/ExpertQueryApiDoc.java @@ -9,28 +9,18 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; import starlight.adapter.expert.webapi.dto.ExpertDetailResponse; import starlight.adapter.expert.webapi.dto.ExpertListResponse; -import starlight.domain.expert.enumerate.TagCategory; import starlight.shared.apiPayload.response.ApiResponse; import java.util.List; -import java.util.Set; @Tag(name = "전문가", description = "전문가 관련 API") public interface ExpertQueryApiDoc { @Operation( - summary = "전문가 검색(AND 매칭)", - description = """ - 카테고리 파라미터가 없으면 전체 전문가를 반환합니다. - \n카테고리를 하나 이상 전달하면 **전달된 모든 카테고리**를 보유한 전문가만 반환합니다(AND 매칭). - \n MARKET_BM: 시장성/BM, TEAM_CAPABILITY: 팀 역량, PROBLEM_DEFINITION: 문제 정의, GROWTH_STRATEGY: 성장 전략, METRIC_DATA: 지표/데이터 - \nSwagger UI에서는 'Add item'으로 항목을 추가하면 ?categories=A&categories=B 형태로 전송됩니다. - \n예) GET /v1/experts?categories=GROWTH_STRATEGY&categories=TEAM_CAPABILITY - \n예) GET /v1/experts?categories=GROWTH_STRATEGY,TEAM_CAPABILITY - """ + summary = "전문가 목록 조회", + description = "전체 전문가 목록을 반환합니다." ) @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -79,12 +69,47 @@ public interface ExpertQueryApiDoc { ) ) ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "전문가 조회 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_QUERY_ERROR", + "message": "전문가 정보를 조회하는 중에 오류가 발생했습니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "신청 건수 조회 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_APPLICATION_QUERY_ERROR", + "message": "전문가 신청 정보를 조회하는 중에 오류가 발생했습니다." + } + } + """ + ) + ) + ) }) @GetMapping - ApiResponse> search( - @RequestParam(name = "categories", required = false) - Set categories - ); + ApiResponse> search(); @Operation(summary = "전문가 상세 조회") @ApiResponses({ @@ -96,6 +121,60 @@ ApiResponse> search( schema = @Schema(implementation = ExpertDetailResponse.class) ) ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "전문가 조회 실패", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + name = "전문가 없음", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_NOT_FOUND", + "message": "해당 전문가를 찾을 수 없습니다." + } + } + """ + ), + @ExampleObject( + name = "조회 오류", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_QUERY_ERROR", + "message": "전문가 정보를 조회하는 중에 오류가 발생했습니다." + } + } + """ + ) + } + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "신청 건수 조회 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_APPLICATION_QUERY_ERROR", + "message": "전문가 신청 정보를 조회하는 중에 오류가 발생했습니다." + } + } + """ + ) + ) + ) }) @GetMapping("/{expertId}") ApiResponse detail( diff --git a/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java b/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpaPort.java similarity index 95% rename from src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java rename to src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpaPort.java index 59aa3650..459172b4 100644 --- a/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java +++ b/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpaPort.java @@ -4,7 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import starlight.application.expert.required.ExpertApplicationLookupPort; -import starlight.application.expertApplication.required.ExpertApplicationQuery; +import starlight.application.expertApplication.required.ExpertApplicationQueryPort; import starlight.domain.expertApplication.entity.ExpertApplication; import starlight.domain.expertApplication.exception.ExpertApplicationErrorType; import starlight.domain.expertApplication.exception.ExpertApplicationException; @@ -17,7 +17,7 @@ @Slf4j @Component @RequiredArgsConstructor -public class ExpertApplicationJpa implements ExpertApplicationQuery, ExpertApplicationLookupPort { +public class ExpertApplicationJpaPort implements ExpertApplicationQueryPort, ExpertApplicationLookupPort { private final ExpertApplicationRepository repository; diff --git a/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java b/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java index ec5b1843..bc8d6e0f 100644 --- a/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java +++ b/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java @@ -9,7 +9,7 @@ import starlight.adapter.auth.security.auth.AuthDetails; import starlight.adapter.expertApplication.webapi.swagger.ExpertApplicationApiDoc; import starlight.application.expertApplication.provided.ExpertApplicationQueryUseCase; -import starlight.application.expertApplication.provided.ExpertApplicationServiceUseCase; +import starlight.application.expertApplication.provided.ExpertApplicationCommandUseCase; import starlight.shared.apiPayload.response.ApiResponse; import java.util.List; @@ -21,7 +21,7 @@ public class ExpertApplicationController implements ExpertApplicationApiDoc { private final ExpertApplicationQueryUseCase queryUseCase; - private final ExpertApplicationServiceUseCase applicationServiceUseCase; + private final ExpertApplicationCommandUseCase applicationServiceUseCase; @GetMapping public ApiResponse> search( diff --git a/src/main/java/starlight/adapter/expertApplication/webapi/swagger/ExpertApplicationApiDoc.java b/src/main/java/starlight/adapter/expertApplication/webapi/swagger/ExpertApplicationApiDoc.java index b73f81b4..83db09db 100644 --- a/src/main/java/starlight/adapter/expertApplication/webapi/swagger/ExpertApplicationApiDoc.java +++ b/src/main/java/starlight/adapter/expertApplication/webapi/swagger/ExpertApplicationApiDoc.java @@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -15,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import starlight.adapter.auth.security.auth.AuthDetails; +import starlight.shared.apiPayload.response.ApiResponse; import java.util.List; @@ -26,7 +26,7 @@ public interface ExpertApplicationApiDoc { description = "특정 사업계획서에 피드백을 요청한 전문가들의 ID 목록을 조회합니다." ) @ApiResponses({ - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", description = "조회 성공", content = @Content( @@ -42,27 +42,46 @@ public interface ExpertApplicationApiDoc { ) ) ), - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "404", - description = "사업계획서를 찾을 수 없음", + description = "사업계획서 없음", content = @Content( mediaType = "application/json", examples = @ExampleObject( value = """ { - "result": "ERROR", - "data": null, - "error": { - "code": "BUSINESS_PLAN_NOT_FOUND", - "message": "사업계획서를 찾을 수 없습니다." - } + "result": "ERROR", + "data": null, + "error": { + "code": "BUSINESS_PLAN_NOT_FOUND", + "message": "해당 사업계획서가 존재하지 않습니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "조회 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_APPLICATION_QUERY_ERROR", + "message": "전문가 신청 정보를 조회하는 중에 오류가 발생했습니다." + } } """ ) ) ) }) - starlight.shared.apiPayload.response.ApiResponse> search( + ApiResponse> search( @Parameter( description = "사업계획서 ID", required = true, @@ -83,7 +102,7 @@ starlight.shared.apiPayload.response.ApiResponse> search( security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponses({ - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", description = "피드백 요청 성공", content = @Content( @@ -99,26 +118,65 @@ starlight.shared.apiPayload.response.ApiResponse> search( ) ) ), - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "400", - description = "잘못된 요청 (파일 없음, 파일 형식 오류 등)", + description = "잘못된 요청 (파일 없음)", content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "빈 파일", value = """ { - "result": "ERROR", - "data": null, - "error": { - "code": "INVALID_FILE", - "message": "유효하지 않은 파일입니다." - } + "result": "ERROR", + "data": null, + "error": { + "code": "EMPTY_FILE", + "message": "업로드할 파일이 비어 있습니다." + } } """ ) ) ), - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "413", + description = "파일 크기 초과", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "FILE_SIZE_EXCEEDED", + "message": "파일 크기는 최대 20MB까지 업로드 가능합니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "415", + description = "지원하지 않는 파일 형식", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "UNSUPPORTED_FILE_TYPE", + "message": "지원되지 않는 파일 형식입니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "404", description = "전문가 또는 사업계획서를 찾을 수 없음", content = @Content( @@ -145,7 +203,7 @@ starlight.shared.apiPayload.response.ApiResponse> search( "data": null, "error": { "code": "BUSINESS_PLAN_NOT_FOUND", - "message": "사업계획서를 찾을 수 없습니다." + "message": "해당 사업계획서가 존재하지 않습니다." } } """ @@ -153,7 +211,7 @@ starlight.shared.apiPayload.response.ApiResponse> search( } ) ), - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "409", description = "이미 피드백을 요청한 전문가", content = @Content( @@ -172,23 +230,39 @@ starlight.shared.apiPayload.response.ApiResponse> search( ) ) ), - @ApiResponse( + @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "500", - description = "서버 오류 (파일 처리 실패, 이메일 발송 실패 등)", + description = "서버 오류 (파일 처리 실패 등)", content = @Content( mediaType = "application/json", - examples = @ExampleObject( - value = """ - { - "result": "ERROR", - "data": null, - "error": { - "code": "INTERNAL_SERVER_ERROR", - "message": "서버 오류가 발생했습니다." + examples = { + @ExampleObject( + name = "파일 읽기 실패", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "FILE_READ_ERROR", + "message": "파일을 읽는 중에 오류가 발생했습니다." + } + } + """ + ), + @ExampleObject( + name = "피드백 요청 처리 실패", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_FEEDBACK_REQUEST_FAILED", + "message": "전문가 피드백 요청에 실패했습니다." + } + } + """ + ) } - } - """ - ) ) ) }) @@ -200,7 +274,7 @@ starlight.shared.apiPayload.response.ApiResponse> search( schema = @Schema(implementation = FeedbackRequestSchema.class) ) ) - starlight.shared.apiPayload.response.ApiResponse requestFeedback( + ApiResponse requestFeedback( @Parameter( description = "전문가 ID", required = true, @@ -248,4 +322,4 @@ class FeedbackRequestSchema { ) public MultipartFile file; } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportJpa.java b/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportJpa.java index 4cd23163..3848a622 100644 --- a/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportJpa.java +++ b/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportJpa.java @@ -3,7 +3,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import starlight.application.expertReport.required.ExpertReportQuery; +import starlight.application.expertReport.required.ExpertReportCommandPort; +import starlight.application.expertReport.required.ExpertReportQueryPort; import starlight.domain.expertReport.entity.ExpertReport; import starlight.domain.expertReport.exception.ExpertReportErrorType; import starlight.domain.expertReport.exception.ExpertReportException; @@ -13,12 +14,12 @@ @Slf4j @Component @RequiredArgsConstructor -public class ExpertReportJpa implements ExpertReportQuery { +public class ExpertReportJpa implements ExpertReportQueryPort, ExpertReportCommandPort { private final ExpertReportRepository repository; @Override - public ExpertReport getOrThrow(Long id) { + public ExpertReport findByIdOrThrow(Long id) { return repository.findById(id).orElseThrow( () -> new ExpertReportException(ExpertReportErrorType.EXPERT_REPORT_NOT_FOUND) ); @@ -40,14 +41,14 @@ public boolean existsByToken(String token) { } @Override - public ExpertReport findByTokenWithDetails(String token) { + public ExpertReport findByTokenWithComments(String token) { return repository.findByToken(token).orElseThrow( () -> new ExpertReportException(ExpertReportErrorType.EXPERT_REPORT_NOT_FOUND) ); } @Override - public List findAllByBusinessPlanId(Long businessPlanId) { + public List findAllByBusinessPlanIdOrderByCreatedAtDesc(Long businessPlanId) { return repository.findAllByBusinessPlanIdOrderByCreatedAtDesc(businessPlanId); } } diff --git a/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportRepository.java b/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportRepository.java index b46a4b66..449b4c5d 100644 --- a/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportRepository.java +++ b/src/main/java/starlight/adapter/expertReport/persistence/ExpertReportRepository.java @@ -11,9 +11,9 @@ public interface ExpertReportRepository extends JpaRepository findByToken(String token); - @EntityGraph(attributePaths = {"details"}) + @EntityGraph(attributePaths = {"comments"}) List findAllByBusinessPlanIdOrderByCreatedAtDesc(Long businessPlanId); } diff --git a/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java b/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java index a593b39e..83a2e8ba 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java @@ -1,17 +1,16 @@ package starlight.adapter.expertReport.webapi; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import starlight.adapter.expertReport.webapi.dto.ExpertReportResponse; import starlight.adapter.expertReport.webapi.dto.UpsertExpertReportRequest; import starlight.adapter.expertReport.webapi.mapper.ExpertReportMapper; +import starlight.adapter.expertReport.webapi.swagger.ExpertReportApiDoc; import starlight.application.expertReport.provided.ExpertReportServiceUseCase; import starlight.application.expertReport.provided.dto.ExpertReportWithExpertDto; import starlight.domain.expertReport.entity.ExpertReport; -import starlight.domain.expertReport.entity.ExpertReportDetail; +import starlight.domain.expertReport.entity.ExpertReportComment; import starlight.shared.apiPayload.response.ApiResponse; import java.util.List; @@ -19,13 +18,11 @@ @RestController @RequestMapping("/v1/expert-reports") @RequiredArgsConstructor -@Tag(name = "전문가", description = "전문가 관련 API") -public class ExpertReportController { +public class ExpertReportController implements ExpertReportApiDoc { private final ExpertReportMapper mapper; private final ExpertReportServiceUseCase expertReportService; - @Operation(summary = "전문가 리포트 목록을 조회합니다. (사용자 사용)") @GetMapping public ApiResponse> getExpertReports( @RequestParam Long businessPlanId @@ -43,7 +40,6 @@ public ApiResponse> getExpertReports( return ApiResponse.success(responses); } - @Operation(summary = "전문가 리포트를 조회합니다. (전문가 사용)") @GetMapping("/{token}") public ApiResponse getExpertReport( @PathVariable String token @@ -58,18 +54,17 @@ public ApiResponse getExpertReport( return ApiResponse.success(response); } - @Operation(summary = "전문가 리포트를 저장합니다 (전문가 사용)") @PostMapping("/{token}") - public ApiResponse save( + public ApiResponse save( @PathVariable String token, @Valid @RequestBody UpsertExpertReportRequest request ) { - List details = mapper.toEntityList(request.details()); + List comments = mapper.toEntityList(request.comments()); ExpertReport report = expertReportService.saveReport( token, request.overallComment(), - details, + comments, request.saveType() ); diff --git a/src/main/java/starlight/adapter/expertReport/webapi/dto/CreateExpertReportDetailRequest.java b/src/main/java/starlight/adapter/expertReport/webapi/dto/CreateExpertReportCommentRequest.java similarity index 80% rename from src/main/java/starlight/adapter/expertReport/webapi/dto/CreateExpertReportDetailRequest.java rename to src/main/java/starlight/adapter/expertReport/webapi/dto/CreateExpertReportCommentRequest.java index b18164ca..464f2ee7 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/dto/CreateExpertReportDetailRequest.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/dto/CreateExpertReportCommentRequest.java @@ -4,10 +4,10 @@ import jakarta.validation.constraints.NotNull; import starlight.domain.expertReport.enumerate.CommentType; -public record CreateExpertReportDetailRequest( +public record CreateExpertReportCommentRequest( @NotNull(message = "평가 타입은 필수입니다") - CommentType commentType, + CommentType type, @NotBlank(message = "내용은 필수입니다") String content -) { } \ No newline at end of file +) { } diff --git a/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportCommentResponse.java b/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportCommentResponse.java new file mode 100644 index 00000000..dcac60f5 --- /dev/null +++ b/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportCommentResponse.java @@ -0,0 +1,17 @@ +package starlight.adapter.expertReport.webapi.dto; + +import starlight.domain.expertReport.entity.ExpertReportComment; +import starlight.domain.expertReport.enumerate.CommentType; + +public record ExpertReportCommentResponse( + CommentType type, + + String content +) { + public static ExpertReportCommentResponse from(ExpertReportComment comment) { + return new ExpertReportCommentResponse( + comment.getType(), + comment.getContent() + ); + } +} diff --git a/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportDetailResponse.java b/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportDetailResponse.java deleted file mode 100644 index 0390448b..00000000 --- a/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportDetailResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package starlight.adapter.expertReport.webapi.dto; - -import starlight.domain.expertReport.entity.ExpertReportDetail; -import starlight.domain.expertReport.enumerate.CommentType; - -public record ExpertReportDetailResponse( - CommentType commentType, - - String content -) { - public static ExpertReportDetailResponse from(ExpertReportDetail detail) { - return new ExpertReportDetailResponse( - detail.getCommentType(), - detail.getContent() - ); - } -} \ No newline at end of file diff --git a/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportResponse.java b/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportResponse.java index 069b5120..54c4b8ab 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportResponse.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportResponse.java @@ -17,7 +17,7 @@ public record ExpertReportResponse( String overallComment, - List details + List comments ) { public static ExpertReportResponse fromEntities(ExpertReport report, Expert expert) { return new ExpertReportResponse( @@ -25,8 +25,8 @@ public static ExpertReportResponse fromEntities(ExpertReport report, Expert expe report.getSubmitStatus(), report.canEdit(), report.getOverallComment(), - report.getDetails().stream() - .map(ExpertReportDetailResponse::from) + report.getComments().stream() + .map(ExpertReportCommentResponse::from) .toList() ); } @@ -37,8 +37,8 @@ public static ExpertReportResponse from(ExpertReport report) { report.getSubmitStatus(), report.canEdit(), report.getOverallComment(), - report.getDetails().stream() - .map(ExpertReportDetailResponse::from) + report.getComments().stream() + .map(ExpertReportCommentResponse::from) .toList() ); } diff --git a/src/main/java/starlight/adapter/expertReport/webapi/dto/UpsertExpertReportRequest.java b/src/main/java/starlight/adapter/expertReport/webapi/dto/UpsertExpertReportRequest.java index 1f2c429c..93b49471 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/dto/UpsertExpertReportRequest.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/dto/UpsertExpertReportRequest.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import starlight.domain.expertReport.enumerate.SaveType; +import starlight.adapter.expertReport.webapi.dto.CreateExpertReportCommentRequest; import java.util.List; @@ -12,5 +13,5 @@ public record UpsertExpertReportRequest( String overallComment, - List<@Valid CreateExpertReportDetailRequest> details -) { } \ No newline at end of file + List<@Valid CreateExpertReportCommentRequest> comments +) { } diff --git a/src/main/java/starlight/adapter/expertReport/webapi/mapper/ExpertReportMapper.java b/src/main/java/starlight/adapter/expertReport/webapi/mapper/ExpertReportMapper.java index 89cb75e0..46410631 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/mapper/ExpertReportMapper.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/mapper/ExpertReportMapper.java @@ -1,21 +1,21 @@ package starlight.adapter.expertReport.webapi.mapper; import org.springframework.stereotype.Component; -import starlight.adapter.expertReport.webapi.dto.CreateExpertReportDetailRequest; -import starlight.domain.expertReport.entity.ExpertReportDetail; +import starlight.adapter.expertReport.webapi.dto.CreateExpertReportCommentRequest; +import starlight.domain.expertReport.entity.ExpertReportComment; import java.util.List; @Component public class ExpertReportMapper { - public ExpertReportDetail toEntity(CreateExpertReportDetailRequest dto) { - return ExpertReportDetail.create( - dto.commentType(), + public ExpertReportComment toEntity(CreateExpertReportCommentRequest dto) { + return ExpertReportComment.create( + dto.type(), dto.content() ); } - public List toEntityList(List dtos) { + public List toEntityList(List dtos) { return dtos.stream() .map(this::toEntity) .toList(); diff --git a/src/main/java/starlight/adapter/expertReport/webapi/swagger/ExpertReportApiDoc.java b/src/main/java/starlight/adapter/expertReport/webapi/swagger/ExpertReportApiDoc.java new file mode 100644 index 00000000..0ca877db --- /dev/null +++ b/src/main/java/starlight/adapter/expertReport/webapi/swagger/ExpertReportApiDoc.java @@ -0,0 +1,222 @@ +package starlight.adapter.expertReport.webapi.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import starlight.adapter.expertReport.webapi.dto.ExpertReportResponse; +import starlight.adapter.expertReport.webapi.dto.UpsertExpertReportRequest; +import starlight.shared.apiPayload.response.ApiResponse; + +import java.util.List; + +@Tag(name = "전문가 리포트", description = "전문가 피드백 리포트 API") +public interface ExpertReportApiDoc { + + @Operation(summary = "전문가 리포트 목록 조회 (사용자)") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = ExpertReportResponse.class)) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "사업계획서 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "BUSINESS_PLAN_NOT_FOUND", + "message": "해당 사업계획서가 존재하지 않습니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "전문가 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_NOT_FOUND", + "message": "해당 전문가를 찾을 수 없습니다." + } + } + """ + ) + ) + ) + }) + @GetMapping + ApiResponse> getExpertReports( + @RequestParam Long businessPlanId + ); + + @Operation(summary = "전문가 리포트 단건 조회 (전문가)") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ExpertReportResponse.class) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "전문가 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_NOT_FOUND", + "message": "해당 전문가를 찾을 수 없습니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "리포트 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_REPORT_NOT_FOUND", + "message": "해당 전문가 리포트를 찾을 수 없습니다." + } + } + """ + ) + ) + ) + }) + @GetMapping("/{token}") + ApiResponse getExpertReport( + @PathVariable String token + ); + + @Operation(summary = "전문가 리포트 저장 (전문가)") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ExpertReportResponse.class) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "요청 오류", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + name = "이미 제출됨", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "ALREADY_SUBMITTED", + "message": "이미 전문가 피드백을 제출하였습니다." + } + } + """ + ), + @ExampleObject( + name = "요청 만료", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "REPORT_EXPIRED", + "message": "전문가 피드백 요청 기간이 만료되었습니다." + } + } + """ + ) + } + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "사업계획서 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "BUSINESS_PLAN_NOT_FOUND", + "message": "해당 사업계획서가 존재하지 않습니다." + } + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "리포트 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "EXPERT_REPORT_NOT_FOUND", + "message": "해당 전문가 리포트를 찾을 수 없습니다." + } + } + """ + ) + ) + ) + }) + @PostMapping("/{token}") + ApiResponse save( + @PathVariable String token, + @Valid @RequestBody UpsertExpertReportRequest request + ); +} diff --git a/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java b/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java index 77f14435..976c7fa4 100644 --- a/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java +++ b/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java @@ -2,7 +2,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import starlight.application.order.provided.OrdersQuery; +import starlight.application.order.required.OrderCommandPort; +import starlight.application.order.required.OrderQueryPort; import starlight.domain.order.exception.OrderErrorType; import starlight.domain.order.exception.OrderException; import starlight.domain.order.order.Orders; @@ -12,7 +13,7 @@ @Repository @RequiredArgsConstructor -public class OrderRepositoryJpa implements OrdersQuery { +public class OrderRepositoryJpa implements OrderQueryPort, OrderCommandPort { private final OrdersRepository repository; diff --git a/src/main/java/starlight/adapter/order/toss/TossClient.java b/src/main/java/starlight/adapter/order/toss/TossClient.java index ee8640ea..52489ad0 100644 --- a/src/main/java/starlight/adapter/order/toss/TossClient.java +++ b/src/main/java/starlight/adapter/order/toss/TossClient.java @@ -68,7 +68,7 @@ public TossClientResponse.Cancel cancel(String paymentKey, String reason) { } catch (Exception e) { log.error("토스 환불 요청 중 에러발생: {}", e.getMessage(), e); log.error("paymentKey: {}, reason: {}", paymentKey, reason); - throw new OrderException(OrderErrorType.TOSS_CLIENT_CONFIRM_ERROR); + throw new OrderException(OrderErrorType.TOSS_CLIENT_CANCEL_ERROR); } } diff --git a/src/main/java/starlight/adapter/order/webapi/OrderController.java b/src/main/java/starlight/adapter/order/webapi/OrderController.java index 3e70cd1d..2970d3d5 100644 --- a/src/main/java/starlight/adapter/order/webapi/OrderController.java +++ b/src/main/java/starlight/adapter/order/webapi/OrderController.java @@ -1,11 +1,11 @@ package starlight.adapter.order.webapi; -import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import starlight.adapter.auth.security.auth.AuthDetails; +import starlight.adapter.order.webapi.swagger.OrderApiDoc; import starlight.application.order.provided.dto.TossClientResponse; import starlight.adapter.order.webapi.dto.request.OrderCancelRequest; import starlight.adapter.order.webapi.dto.request.OrderConfirmRequest; @@ -13,7 +13,7 @@ import starlight.adapter.order.webapi.dto.response.OrderCancelResponse; import starlight.adapter.order.webapi.dto.response.OrderConfirmResponse; import starlight.adapter.order.webapi.dto.response.OrderPrepareResponse; -import starlight.application.order.provided.OrderPaymentService; +import starlight.application.order.provided.OrderPaymentServiceUseCase; import starlight.application.order.provided.dto.PaymentHistoryItemDto; import starlight.domain.order.order.Orders; import starlight.shared.apiPayload.response.ApiResponse; @@ -22,11 +22,10 @@ @RestController @RequiredArgsConstructor -@Tag(name = "결제", description = "결제 관련 API") @RequestMapping("/v1/orders") -public class OrderController { +public class OrderController implements OrderApiDoc { - private final OrderPaymentService orderPaymentService; + private final OrderPaymentServiceUseCase orderPaymentService; @PostMapping("/request") public ApiResponse prepareOrder( diff --git a/src/main/java/starlight/adapter/order/webapi/swagger/OrderApiDoc.java b/src/main/java/starlight/adapter/order/webapi/swagger/OrderApiDoc.java new file mode 100644 index 00000000..dfe9f393 --- /dev/null +++ b/src/main/java/starlight/adapter/order/webapi/swagger/OrderApiDoc.java @@ -0,0 +1,333 @@ +package starlight.adapter.order.webapi.swagger; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import starlight.adapter.auth.security.auth.AuthDetails; +import starlight.adapter.order.webapi.dto.request.OrderCancelRequest; +import starlight.adapter.order.webapi.dto.request.OrderConfirmRequest; +import starlight.adapter.order.webapi.dto.request.OrderPrepareRequest; +import starlight.adapter.order.webapi.dto.response.OrderCancelResponse; +import starlight.adapter.order.webapi.dto.response.OrderConfirmResponse; +import starlight.adapter.order.webapi.dto.response.OrderPrepareResponse; +import starlight.application.order.provided.dto.PaymentHistoryItemDto; +import starlight.shared.apiPayload.response.ApiResponse; + +import java.util.List; + +@Tag(name = "결제", description = "결제 관련 API") +public interface OrderApiDoc { + + @Operation(summary = "결제 준비", security = @SecurityRequirement(name = "Bearer Authentication")) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = OrderPrepareResponse.class), + examples = @ExampleObject( + value = """ + { + "result": "SUCCESS", + "data": { + "orderCode": "O-20250101-0001", + "amount": 150000, + "productCode": "USAGE_10" + }, + "error": null + } + """ + ) + ) + ) + , + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "요청 오류", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + name = "유효하지 않은 이용권", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "INVALID_USAGE_PRODUCT", + "message": "유효하지 않은 이용권 금액입니다." + } + } + """ + ), + @ExampleObject( + name = "주문번호-구매자 불일치", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "ORDER_CODE_BUYER_MISMATCH", + "message": "이미 존재하는 주문번호입니다. (구매자 상이)" + } + } + """ + ), + @ExampleObject( + name = "주문번호-이용권 불일치", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "ORDER_PRODUCT_MISMATCH", + "message": "이미 존재하는 주문번호입니다. (이용권 금액 상이)" + } + } + """ + ), + @ExampleObject( + name = "이미 결제된 주문", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "ALREADY_PAID", + "message": "이미 결제가 완료된 주문입니다." + } + } + """ + ) + } + ) + ) + }) + @PostMapping("/request") + ApiResponse prepareOrder( + @Valid @RequestBody OrderPrepareRequest request, + @AuthenticationPrincipal AuthDetails authDetails + ); + + @Operation(summary = "결제 승인", security = @SecurityRequirement(name = "Bearer Authentication")) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = OrderConfirmResponse.class) + ) + ) + , + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "요청 오류", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + name = "결제 금액 불일치", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "PAYMENT_AMOUNT_MISMATCH", + "message": "주문 금액과 결제 금액이 일치하지 않습니다." + } + } + """ + ), + @ExampleObject( + name = "승인 가능한 결제 없음", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "NO_REQUESTED_PAYMENT", + "message": "승인 가능한 결제 시도가 존재하지 않습니다." + } + } + """ + ), + @ExampleObject( + name = "결제 상태 오류", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "INVALID_ORDER_STATE_FOR_PAYMENT", + "message": "주문 생성 상태에서만 결제 가능합니다." + } + } + """ + ), + @ExampleObject( + name = "PG 승인 실패", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "TOSS_CLIENT_CONFIRM_ERROR", + "message": "토스 결제 요청 중 오류가 발생했습니다." + } + } + """ + ) + } + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "주문 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "ORDER_NOT_FOUND", + "message": "주문을 찾을 수 없습니다." + } + } + """ + ) + ) + ) + }) + @PostMapping("/confirm") + ApiResponse confirmPayment( + @Valid @RequestBody OrderConfirmRequest request, + @AuthenticationPrincipal AuthDetails authDetails + ); + + @Operation(summary = "결제 취소") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = OrderCancelResponse.class) + ) + ) + , + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "요청 오류", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject( + name = "취소 가능한 결제 이력 없음", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "NO_PAYMENT_RECORDS", + "message": "주문에 결제 이력이 존재하지 않습니다." + } + } + """ + ), + @ExampleObject( + name = "paymentKey 누락", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "NO_PAYMENT_KEY", + "message": "paymentKey가 없어 PG 취소를 수행할 수 없습니다." + } + } + """ + ), + @ExampleObject( + name = "결제 상태 오류", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "INVALID_ORDER_STATE_FOR_CANCEL", + "message": "결제 완료 상태에서만 취소 가능합니다." + } + } + """ + ), + @ExampleObject( + name = "PG 취소 실패", + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "TOSS_CLIENT_CANCEL_ERROR", + "message": "토스 결제 취소 요청 중 오류가 발생했습니다." + } + } + """ + ) + } + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "주문 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "result": "ERROR", + "data": null, + "error": { + "code": "ORDER_NOT_FOUND", + "message": "주문을 찾을 수 없습니다." + } + } + """ + ) + ) + ) + }) + @PostMapping("/cancel") + ApiResponse cancelPayment( + @Valid @RequestBody OrderCancelRequest request + ); + + @Operation(summary = "내 결제 내역 조회", security = @SecurityRequirement(name = "Bearer Authentication")) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + array = @ArraySchema(schema = @Schema(implementation = PaymentHistoryItemDto.class)) + ) + ) + }) + @GetMapping + ApiResponse> getMyPayments( + @AuthenticationPrincipal AuthDetails authDetails + ); +} diff --git a/src/main/java/starlight/adapter/usage/persistence/UsageHistoryRepositoryJpa.java b/src/main/java/starlight/adapter/usage/persistence/UsageHistoryJpa.java similarity index 63% rename from src/main/java/starlight/adapter/usage/persistence/UsageHistoryRepositoryJpa.java rename to src/main/java/starlight/adapter/usage/persistence/UsageHistoryJpa.java index 803689b9..55583b5b 100644 --- a/src/main/java/starlight/adapter/usage/persistence/UsageHistoryRepositoryJpa.java +++ b/src/main/java/starlight/adapter/usage/persistence/UsageHistoryJpa.java @@ -2,17 +2,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import starlight.application.usage.provided.UsageHistoryQuery; +import starlight.application.usage.required.UsageHistoryCommandPort; import starlight.domain.order.wallet.UsageHistory; @Repository @RequiredArgsConstructor -public class UsageHistoryRepositoryJpa implements UsageHistoryQuery { +public class UsageHistoryJpa implements UsageHistoryCommandPort { private final UsageHistoryRepository repository; @Override - public UsageHistory save(UsageHistory usageHistory){ + public UsageHistory save(UsageHistory usageHistory) { return repository.save(usageHistory); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/adapter/usage/persistence/UsageWalletRepositoryJpa.java b/src/main/java/starlight/adapter/usage/persistence/UsageWalletJpa.java similarity index 56% rename from src/main/java/starlight/adapter/usage/persistence/UsageWalletRepositoryJpa.java rename to src/main/java/starlight/adapter/usage/persistence/UsageWalletJpa.java index c8bdf4c6..df5dbbb2 100644 --- a/src/main/java/starlight/adapter/usage/persistence/UsageWalletRepositoryJpa.java +++ b/src/main/java/starlight/adapter/usage/persistence/UsageWalletJpa.java @@ -2,24 +2,25 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import starlight.application.usage.provided.UsageWalletQuery; +import starlight.application.usage.required.UsageWalletCommandPort; +import starlight.application.usage.required.UsageWalletQueryPort; import starlight.domain.order.wallet.UsageWallet; import java.util.Optional; @Repository @RequiredArgsConstructor -public class UsageWalletRepositoryJpa implements UsageWalletQuery { +public class UsageWalletJpa implements UsageWalletQueryPort, UsageWalletCommandPort { private final UsageWalletRepository repository; @Override - public Optional findByUserId(Long userId){ + public Optional findByUserId(Long userId) { return repository.findByUserId(userId); } @Override - public UsageWallet save(UsageWallet usageWallet){ + public UsageWallet save(UsageWallet usageWallet) { return repository.save(usageWallet); } } diff --git a/src/main/java/starlight/application/aireport/AiReportServiceImpl.java b/src/main/java/starlight/application/aireport/AiReportServiceImpl.java index 0db896a8..18fb173c 100644 --- a/src/main/java/starlight/application/aireport/AiReportServiceImpl.java +++ b/src/main/java/starlight/application/aireport/AiReportServiceImpl.java @@ -15,7 +15,7 @@ import starlight.application.businessplan.provided.dto.BusinessPlanResponse; import starlight.application.businessplan.required.BusinessPlanQuery; import starlight.application.businessplan.util.BusinessPlanContentExtractor; -import starlight.application.infrastructure.provided.OcrProvider; +import starlight.application.aireport.required.OcrProvider; import starlight.domain.aireport.entity.AiReport; import starlight.domain.aireport.exception.AiReportErrorType; import starlight.domain.aireport.exception.AiReportException; diff --git a/src/main/java/starlight/application/infrastructure/provided/OcrProvider.java b/src/main/java/starlight/application/aireport/required/OcrProvider.java similarity index 76% rename from src/main/java/starlight/application/infrastructure/provided/OcrProvider.java rename to src/main/java/starlight/application/aireport/required/OcrProvider.java index e3a25fbd..0e8050d5 100644 --- a/src/main/java/starlight/application/infrastructure/provided/OcrProvider.java +++ b/src/main/java/starlight/application/aireport/required/OcrProvider.java @@ -1,4 +1,4 @@ -package starlight.application.infrastructure.provided; +package starlight.application.aireport.required; import starlight.shared.dto.infrastructure.OcrResponse; diff --git a/src/main/java/starlight/application/infrastructure/provided/PresignedUrlProvider.java b/src/main/java/starlight/application/aireport/required/PresignedUrlProvider.java similarity index 80% rename from src/main/java/starlight/application/infrastructure/provided/PresignedUrlProvider.java rename to src/main/java/starlight/application/aireport/required/PresignedUrlProvider.java index 78f060e7..0a262be0 100644 --- a/src/main/java/starlight/application/infrastructure/provided/PresignedUrlProvider.java +++ b/src/main/java/starlight/application/aireport/required/PresignedUrlProvider.java @@ -1,4 +1,4 @@ -package starlight.application.infrastructure.provided; +package starlight.application.aireport.required; import starlight.shared.dto.infrastructure.PreSignedUrlResponse; diff --git a/src/main/java/starlight/application/expert/ExpertDetailQueryService.java b/src/main/java/starlight/application/expert/ExpertDetailQueryService.java index 3e31c3a2..58770292 100644 --- a/src/main/java/starlight/application/expert/ExpertDetailQueryService.java +++ b/src/main/java/starlight/application/expert/ExpertDetailQueryService.java @@ -8,11 +8,9 @@ import starlight.application.expert.required.ExpertApplicationLookupPort; import starlight.application.expert.required.ExpertQueryPort; import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; import java.util.List; import java.util.Map; -import java.util.Set; @Service @RequiredArgsConstructor @@ -23,10 +21,8 @@ public class ExpertDetailQueryService implements ExpertDetailQueryUseCase { private final ExpertApplicationLookupPort expertApplicationLookupPort; @Override - public List search(Set categories) { - List experts = (categories == null || categories.isEmpty()) - ? expertQueryPort.findAllWithCareersTagsCategories() - : expertQueryPort.findByAllCategories(categories); + public List searchAll() { + List experts = expertQueryPort.findAllWithCareersTagsCategories(); List expertIds = experts.stream() .map(Expert::getId) diff --git a/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java b/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java index 5e6ccb98..705b16ee 100644 --- a/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java +++ b/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java @@ -1,14 +1,12 @@ package starlight.application.expert.provided; -import starlight.domain.expert.enumerate.TagCategory; import starlight.application.expert.provided.dto.ExpertDetailResult; import java.util.List; -import java.util.Set; public interface ExpertDetailQueryUseCase { - List search(Set categories); + List searchAll(); ExpertDetailResult findById(Long expertId); } diff --git a/src/main/java/starlight/application/expert/required/ExpertQueryPort.java b/src/main/java/starlight/application/expert/required/ExpertQueryPort.java index c1b1d922..62d3ef26 100644 --- a/src/main/java/starlight/application/expert/required/ExpertQueryPort.java +++ b/src/main/java/starlight/application/expert/required/ExpertQueryPort.java @@ -1,9 +1,6 @@ package starlight.application.expert.required; import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; - -import java.util.Collection; import java.util.List; public interface ExpertQueryPort { @@ -12,5 +9,4 @@ public interface ExpertQueryPort { List findAllWithCareersTagsCategories(); - List findByAllCategories(Collection categories); } diff --git a/src/main/java/starlight/application/expertApplication/ExpertApplicationService.java b/src/main/java/starlight/application/expertApplication/ExpertApplicationCommandService.java similarity index 87% rename from src/main/java/starlight/application/expertApplication/ExpertApplicationService.java rename to src/main/java/starlight/application/expertApplication/ExpertApplicationCommandService.java index 5c8eef07..106963d7 100644 --- a/src/main/java/starlight/application/expertApplication/ExpertApplicationService.java +++ b/src/main/java/starlight/application/expertApplication/ExpertApplicationCommandService.java @@ -9,13 +9,15 @@ import org.springframework.web.multipart.MultipartFile; import starlight.application.businessplan.required.BusinessPlanQuery; import starlight.application.expertApplication.event.FeedbackRequestDto; -import starlight.application.expertApplication.provided.ExpertApplicationServiceUseCase; +import starlight.application.expertApplication.provided.ExpertApplicationCommandUseCase; import starlight.application.expertApplication.required.ExpertLookupPort; -import starlight.application.expertApplication.required.ExpertApplicationQuery; +import starlight.application.expertApplication.required.ExpertApplicationQueryPort; import starlight.application.expertReport.provided.ExpertReportServiceUseCase; import starlight.domain.businessplan.entity.BusinessPlan; import starlight.domain.businessplan.enumerate.PlanStatus; +import starlight.domain.businessplan.exception.BusinessPlanException; import starlight.domain.expert.entity.Expert; +import starlight.domain.expert.exception.ExpertException; import starlight.domain.expertApplication.entity.ExpertApplication; import starlight.domain.expertApplication.exception.ExpertApplicationErrorType; import starlight.domain.expertApplication.exception.ExpertApplicationException; @@ -27,13 +29,13 @@ @Slf4j @Service @RequiredArgsConstructor -public class ExpertApplicationService implements ExpertApplicationServiceUseCase { +public class ExpertApplicationCommandService implements ExpertApplicationCommandUseCase { private final ExpertLookupPort expertLookupPort; private final BusinessPlanQuery planQuery; - private final ExpertApplicationQuery applicationQuery; + private final ExpertApplicationQueryPort applicationQueryPort; private final ApplicationEventPublisher eventPublisher; - private final ExpertReportServiceUseCase expertReportService; + private final ExpertReportServiceUseCase expertReportUseCase; private static final long MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB private static final String ALLOWED_CONTENT_TYPE = "application/pdf"; @@ -55,6 +57,8 @@ public void requestFeedback(Long expertId, Long planId, MultipartFile file, Stri registerApplicationRecord(expertId, planId); publishEmailEvent(expert, plan, file, menteeName); + } catch (ExpertApplicationException | BusinessPlanException | ExpertException e) { + throw e; } catch (Exception e) { log.error("Failed to request Feedback. planId={}, expertId={}", planId, expertId, e); throw new ExpertApplicationException(ExpertApplicationErrorType.EXPERT_FEEDBACK_REQUEST_FAILED); @@ -77,12 +81,12 @@ private void validateFile(MultipartFile file) { } public void registerApplicationRecord(Long expertId, Long planId) { - if (applicationQuery.existsByExpertIdAndBusinessPlanId(expertId, planId)) { + if (applicationQueryPort.existsByExpertIdAndBusinessPlanId(expertId, planId)) { throw new ExpertApplicationException(ExpertApplicationErrorType.APPLICATION_ALREADY_EXISTS); } ExpertApplication application = ExpertApplication.create(planId, expertId); - applicationQuery.save(application); + applicationQueryPort.save(application); } private String generateFilename(MultipartFile file, BusinessPlan plan, String menteeName) { @@ -122,6 +126,6 @@ protected void publishEmailEvent(Expert expert, BusinessPlan plan, MultipartFile } private String buildFeedbackRequestUrl(Long expertId, Long planId) { - return expertReportService.createExpertReportLink(expertId, planId); + return expertReportUseCase.createExpertReportLink(expertId, planId); } } diff --git a/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java b/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java index 5c7765c1..ad5fe060 100644 --- a/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java +++ b/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java @@ -3,8 +3,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import starlight.application.businessplan.required.BusinessPlanQuery; import starlight.application.expertApplication.provided.ExpertApplicationQueryUseCase; -import starlight.application.expertApplication.required.ExpertApplicationQuery; +import starlight.application.expertApplication.required.ExpertApplicationQueryPort; import java.util.List; @@ -13,10 +14,12 @@ @Transactional(readOnly = true) public class ExpertApplicationQueryService implements ExpertApplicationQueryUseCase { - private final ExpertApplicationQuery expertApplicationQuery; + private final ExpertApplicationQueryPort expertApplicationQueryPort; + private final BusinessPlanQuery businessPlanQuery; @Override public List findRequestedExpertIds(Long businessPlanId) { - return expertApplicationQuery.findRequestedExpertIds(businessPlanId); + businessPlanQuery.getOrThrow(businessPlanId); + return expertApplicationQueryPort.findRequestedExpertIds(businessPlanId); } } diff --git a/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationServiceUseCase.java b/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationCommandUseCase.java similarity index 83% rename from src/main/java/starlight/application/expertApplication/provided/ExpertApplicationServiceUseCase.java rename to src/main/java/starlight/application/expertApplication/provided/ExpertApplicationCommandUseCase.java index 46e7cdbf..7820c594 100644 --- a/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationServiceUseCase.java +++ b/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationCommandUseCase.java @@ -4,7 +4,7 @@ import java.io.IOException; -public interface ExpertApplicationServiceUseCase { +public interface ExpertApplicationCommandUseCase { void requestFeedback(Long expertId, Long planId, MultipartFile file, String menteeName) throws IOException; } diff --git a/src/main/java/starlight/application/expertApplication/required/ExpertApplicationQuery.java b/src/main/java/starlight/application/expertApplication/required/ExpertApplicationQueryPort.java similarity index 88% rename from src/main/java/starlight/application/expertApplication/required/ExpertApplicationQuery.java rename to src/main/java/starlight/application/expertApplication/required/ExpertApplicationQueryPort.java index 33ad33ed..4c22587c 100644 --- a/src/main/java/starlight/application/expertApplication/required/ExpertApplicationQuery.java +++ b/src/main/java/starlight/application/expertApplication/required/ExpertApplicationQueryPort.java @@ -4,7 +4,7 @@ import java.util.List; -public interface ExpertApplicationQuery { +public interface ExpertApplicationQueryPort { Boolean existsByExpertIdAndBusinessPlanId(Long expertId, Long businessPlanId); List findRequestedExpertIds(Long businessPlanId); diff --git a/src/main/java/starlight/application/expertReport/ExpertReportService.java b/src/main/java/starlight/application/expertReport/ExpertReportService.java index 2511bc49..5502f3ff 100644 --- a/src/main/java/starlight/application/expertReport/ExpertReportService.java +++ b/src/main/java/starlight/application/expertReport/ExpertReportService.java @@ -3,18 +3,20 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import starlight.application.businessplan.required.BusinessPlanQuery; import starlight.application.expertReport.provided.ExpertReportServiceUseCase; import starlight.application.expertReport.provided.dto.ExpertReportWithExpertDto; import starlight.application.expertReport.required.ExpertLookupPort; -import starlight.application.expertReport.required.ExpertReportQuery; +import starlight.application.expertReport.required.ExpertReportCommandPort; +import starlight.application.expertReport.required.ExpertReportQueryPort; import starlight.domain.businessplan.entity.BusinessPlan; import starlight.domain.businessplan.enumerate.PlanStatus; import starlight.domain.expert.entity.Expert; +import starlight.domain.expert.exception.ExpertErrorType; +import starlight.domain.expert.exception.ExpertException; import starlight.domain.expertReport.entity.ExpertReport; -import starlight.domain.expertReport.entity.ExpertReportDetail; +import starlight.domain.expertReport.entity.ExpertReportComment; import starlight.domain.expertReport.enumerate.SaveType; import java.security.SecureRandom; @@ -37,7 +39,8 @@ public class ExpertReportService implements ExpertReportServiceUseCase { @Value("${feedback-token.base-url}") private String feedbackBaseUrl; - private final ExpertReportQuery expertReportQuery; + private final ExpertReportQueryPort expertReportQuery; + private final ExpertReportCommandPort expertReportCommand; private final ExpertLookupPort expertLookupPort; private final BusinessPlanQuery businessPlanQuery; private final SecureRandom secureRandom = new SecureRandom(); @@ -50,7 +53,7 @@ public String createExpertReportLink( String token = generateToken(); ExpertReport report = ExpertReport.create(expertId, businessPlanId, token); - expertReportQuery.save(report); + expertReportCommand.save(report); return feedbackBaseUrl + token; } @@ -59,13 +62,13 @@ public String createExpertReportLink( public ExpertReport saveReport( String token, String overallComment, - List details, + List comments, SaveType saveType ) { - ExpertReport report = expertReportQuery.findByTokenWithDetails(token); + ExpertReport report = expertReportQuery.findByTokenWithComments(token); report.updateOverallComment(overallComment); - report.updateDetails(details); + report.updateComments(comments); switch (saveType) { case TEMPORARY -> { @@ -79,12 +82,12 @@ public ExpertReport saveReport( } - return expertReportQuery.save(report); + return expertReportCommand.save(report); } @Override public ExpertReportWithExpertDto getExpertReportWithExpert(String token) { - ExpertReport report = expertReportQuery.findByTokenWithDetails(token); + ExpertReport report = expertReportQuery.findByTokenWithComments(token); report.incrementViewCount(); Expert expert = expertLookupPort.findByIdWithCareersAndTags(report.getExpertId()); @@ -95,13 +98,18 @@ public ExpertReportWithExpertDto getExpertReportWithExpert(String token) { @Override @Transactional(readOnly = true) public List getExpertReportsWithExpertByBusinessPlanId(Long businessPlanId) { - List reports = expertReportQuery.findAllByBusinessPlanId(businessPlanId); + businessPlanQuery.getOrThrow(businessPlanId); + + List reports = expertReportQuery.findAllByBusinessPlanIdOrderByCreatedAtDesc(businessPlanId); Set expertIds = reports.stream() .map(ExpertReport::getExpertId) .collect(Collectors.toSet()); Map expertsMap = expertLookupPort.findByIds(expertIds); + if (!expertIds.isEmpty() && expertsMap.size() != expertIds.size()) { + throw new ExpertException(ExpertErrorType.EXPERT_NOT_FOUND); + } return reports.stream() .map(report -> { diff --git a/src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java b/src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java index 8afc0108..386e47ea 100644 --- a/src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java +++ b/src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java @@ -2,7 +2,7 @@ import starlight.application.expertReport.provided.dto.ExpertReportWithExpertDto; import starlight.domain.expertReport.entity.ExpertReport; -import starlight.domain.expertReport.entity.ExpertReportDetail; +import starlight.domain.expertReport.entity.ExpertReportComment; import starlight.domain.expertReport.enumerate.SaveType; import java.util.List; @@ -11,7 +11,7 @@ public interface ExpertReportServiceUseCase{ String createExpertReportLink(Long expertId, Long businessPlanId); - ExpertReport saveReport(String token, String overallComment, List details, SaveType saveType); + ExpertReport saveReport(String token, String overallComment, List comments, SaveType saveType); ExpertReportWithExpertDto getExpertReportWithExpert(String token); diff --git a/src/main/java/starlight/application/expertReport/required/ExpertReportCommandPort.java b/src/main/java/starlight/application/expertReport/required/ExpertReportCommandPort.java new file mode 100644 index 00000000..6e6bcf01 --- /dev/null +++ b/src/main/java/starlight/application/expertReport/required/ExpertReportCommandPort.java @@ -0,0 +1,10 @@ +package starlight.application.expertReport.required; + +import starlight.domain.expertReport.entity.ExpertReport; + +public interface ExpertReportCommandPort { + + ExpertReport save(ExpertReport expertReport); + + void delete(ExpertReport expertReport); +} diff --git a/src/main/java/starlight/application/expertReport/required/ExpertReportQuery.java b/src/main/java/starlight/application/expertReport/required/ExpertReportQuery.java deleted file mode 100644 index 6611e5dd..00000000 --- a/src/main/java/starlight/application/expertReport/required/ExpertReportQuery.java +++ /dev/null @@ -1,20 +0,0 @@ -package starlight.application.expertReport.required; - -import starlight.domain.expertReport.entity.ExpertReport; - -import java.util.List; - -public interface ExpertReportQuery { - - ExpertReport getOrThrow(Long id); - - ExpertReport save(ExpertReport expertReport); - - void delete(ExpertReport expertReport); - - boolean existsByToken(String token); - - ExpertReport findByTokenWithDetails(String token); - - List findAllByBusinessPlanId(Long businessPlanId); -} diff --git a/src/main/java/starlight/application/expertReport/required/ExpertReportQueryPort.java b/src/main/java/starlight/application/expertReport/required/ExpertReportQueryPort.java new file mode 100644 index 00000000..fa0e839e --- /dev/null +++ b/src/main/java/starlight/application/expertReport/required/ExpertReportQueryPort.java @@ -0,0 +1,16 @@ +package starlight.application.expertReport.required; + +import starlight.domain.expertReport.entity.ExpertReport; + +import java.util.List; + +public interface ExpertReportQueryPort { + + ExpertReport findByIdOrThrow(Long id); + + boolean existsByToken(String token); + + ExpertReport findByTokenWithComments(String token); + + List findAllByBusinessPlanIdOrderByCreatedAtDesc(Long businessPlanId); +} diff --git a/src/main/java/starlight/application/order/OrderPaymentServiceImpl.java b/src/main/java/starlight/application/order/OrderPaymentService.java similarity index 84% rename from src/main/java/starlight/application/order/OrderPaymentServiceImpl.java rename to src/main/java/starlight/application/order/OrderPaymentService.java index 489cb499..8169faf0 100644 --- a/src/main/java/starlight/application/order/OrderPaymentServiceImpl.java +++ b/src/main/java/starlight/application/order/OrderPaymentService.java @@ -5,12 +5,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import starlight.adapter.order.toss.TossClient; -import starlight.application.order.provided.dto.TossClientResponse; import starlight.adapter.order.webapi.dto.request.OrderCancelRequest; -import starlight.application.order.provided.OrderPaymentService; -import starlight.application.order.provided.OrdersQuery; import starlight.application.order.provided.dto.PaymentHistoryItemDto; -import starlight.application.usage.provided.UsageCreditPort; +import starlight.application.order.provided.OrderPaymentServiceUseCase; +import starlight.application.order.provided.dto.TossClientResponse; +import starlight.application.order.required.OrderCommandPort; +import starlight.application.order.required.OrderQueryPort; +import starlight.application.order.required.UsageCreditChargePort; import starlight.domain.order.enumerate.OrderStatus; import starlight.domain.order.enumerate.UsageProductType; import starlight.domain.order.exception.OrderErrorType; @@ -28,11 +29,12 @@ @Service @RequiredArgsConstructor @Transactional -public class OrderPaymentServiceImpl implements OrderPaymentService { +public class OrderPaymentService implements OrderPaymentServiceUseCase { private final TossClient tossClient; - private final OrdersQuery ordersQuery; - private final UsageCreditPort usageCreditPort; + private final OrderQueryPort orderQueryPort; + private final OrderCommandPort orderCommandPort; + private final UsageCreditChargePort usageCreditChargePort; /** * 결제 전 주문 준비 @@ -50,7 +52,7 @@ public Orders prepare(String orderCodeStr, Long buyerId, String productCode) { Money money = Money.krw(product.getPrice()); OrderCode orderCode = OrderCode.of(orderCodeStr); - return ordersQuery.findByOrderCode(orderCodeStr) + return orderQueryPort.findByOrderCode(orderCodeStr) .map(existing -> { existing.validateSameBuyer(buyerId); existing.validateSameProduct(product); @@ -60,7 +62,7 @@ public Orders prepare(String orderCodeStr, Long buyerId, String productCode) { .orElseGet(() -> { Orders newOrder = Orders.newUsageOrder(orderCode, buyerId, money, product); newOrder.addPaymentAttempt(money); - return ordersQuery.save(newOrder); + return orderCommandPort.save(newOrder); }); } @@ -75,7 +77,7 @@ public Orders prepare(String orderCodeStr, Long buyerId, String productCode) { @Override public Orders confirm(String orderCodeStr, String paymentKey, Long buyerId) { - Orders order = ordersQuery.getByOrderCodeOrThrow(orderCodeStr); + Orders order = orderQueryPort.getByOrderCodeOrThrow(orderCodeStr); UsageProductType product = UsageProductType.fromCode(order.getUsageProductCode()); long expectedAmount = product.getPrice(); @@ -99,13 +101,13 @@ public Orders confirm(String orderCodeStr, String paymentKey, Long buyerId) { ); order.markPaid(); - usageCreditPort.chargeForOrder( + usageCreditChargePort.chargeForOrder( order.getBuyerId(), order.getId(), product.getUsageCount() ); - return ordersQuery.save(order); + return orderCommandPort.save(order); } /** @@ -117,7 +119,7 @@ public Orders confirm(String orderCodeStr, String paymentKey, Long buyerId) { @Override public TossClientResponse.Cancel cancel(OrderCancelRequest request) { - Orders order = ordersQuery.getByOrderCodeOrThrow(request.orderCode()); + Orders order = orderQueryPort.getByOrderCodeOrThrow(request.orderCode()); PaymentRecords payment = order.getLatestDoneOrThrow(); payment.ensureHasPaymentKey(); @@ -129,14 +131,14 @@ public TossClientResponse.Cancel cancel(OrderCancelRequest request) { payment.markCanceled(); order.cancel(); - ordersQuery.save(order); + orderCommandPort.save(order); return response; } public List getPaymentHistory(Long buyerId) { // 1) 이 회원(buyer)의 주문 전체를 최신순으로 가져오기 - List orders = ordersQuery.findAllWithPaymentsByBuyerIdOrderByCreatedAtDesc(buyerId); + List orders = orderQueryPort.findAllWithPaymentsByBuyerIdOrderByCreatedAtDesc(buyerId); return orders.stream() // 결제완료(PAID) 주문만 @@ -164,4 +166,4 @@ public List getPaymentHistory(Long buyerId) { }) .toList(); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/application/order/provided/OrderPaymentService.java b/src/main/java/starlight/application/order/provided/OrderPaymentServiceUseCase.java similarity index 92% rename from src/main/java/starlight/application/order/provided/OrderPaymentService.java rename to src/main/java/starlight/application/order/provided/OrderPaymentServiceUseCase.java index ff293f2a..3b7cfd8d 100644 --- a/src/main/java/starlight/application/order/provided/OrderPaymentService.java +++ b/src/main/java/starlight/application/order/provided/OrderPaymentServiceUseCase.java @@ -1,13 +1,13 @@ package starlight.application.order.provided; -import starlight.application.order.provided.dto.TossClientResponse; import starlight.adapter.order.webapi.dto.request.OrderCancelRequest; import starlight.application.order.provided.dto.PaymentHistoryItemDto; +import starlight.application.order.provided.dto.TossClientResponse; import starlight.domain.order.order.Orders; import java.util.List; -public interface OrderPaymentService{ +public interface OrderPaymentServiceUseCase { Orders prepare(String orderCodeStr, Long buyerId, String productCode); diff --git a/src/main/java/starlight/application/order/required/OrderCommandPort.java b/src/main/java/starlight/application/order/required/OrderCommandPort.java new file mode 100644 index 00000000..0c17e806 --- /dev/null +++ b/src/main/java/starlight/application/order/required/OrderCommandPort.java @@ -0,0 +1,8 @@ +package starlight.application.order.required; + +import starlight.domain.order.order.Orders; + +public interface OrderCommandPort { + + Orders save(Orders order); +} diff --git a/src/main/java/starlight/application/order/provided/OrdersQuery.java b/src/main/java/starlight/application/order/required/OrderQueryPort.java similarity index 72% rename from src/main/java/starlight/application/order/provided/OrdersQuery.java rename to src/main/java/starlight/application/order/required/OrderQueryPort.java index 16b5dfc3..c6dd84cb 100644 --- a/src/main/java/starlight/application/order/provided/OrdersQuery.java +++ b/src/main/java/starlight/application/order/required/OrderQueryPort.java @@ -1,17 +1,15 @@ -package starlight.application.order.provided; +package starlight.application.order.required; import starlight.domain.order.order.Orders; import java.util.List; import java.util.Optional; -public interface OrdersQuery { +public interface OrderQueryPort { Optional findByOrderCode(String orderCode); List findAllWithPaymentsByBuyerIdOrderByCreatedAtDesc(Long buyerId); Orders getByOrderCodeOrThrow(String orderCode); - - Orders save(Orders order); -} \ No newline at end of file +} diff --git a/src/main/java/starlight/application/order/required/UsageCreditChargePort.java b/src/main/java/starlight/application/order/required/UsageCreditChargePort.java new file mode 100644 index 00000000..f1811f24 --- /dev/null +++ b/src/main/java/starlight/application/order/required/UsageCreditChargePort.java @@ -0,0 +1,6 @@ +package starlight.application.order.required; + +public interface UsageCreditChargePort { + + void chargeForOrder(Long userId, Long orderId, int usageCount); +} diff --git a/src/main/java/starlight/application/usage/UsageCreditService.java b/src/main/java/starlight/application/usage/UsageCreditChargeService.java similarity index 62% rename from src/main/java/starlight/application/usage/UsageCreditService.java rename to src/main/java/starlight/application/usage/UsageCreditChargeService.java index 5b888b72..30818003 100644 --- a/src/main/java/starlight/application/usage/UsageCreditService.java +++ b/src/main/java/starlight/application/usage/UsageCreditChargeService.java @@ -3,9 +3,10 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import starlight.application.usage.provided.UsageCreditPort; -import starlight.application.usage.provided.UsageHistoryQuery; -import starlight.application.usage.provided.UsageWalletQuery; +import starlight.application.order.required.UsageCreditChargePort; +import starlight.application.usage.required.UsageHistoryCommandPort; +import starlight.application.usage.required.UsageWalletCommandPort; +import starlight.application.usage.required.UsageWalletQueryPort; import starlight.domain.order.exception.OrderErrorType; import starlight.domain.order.exception.OrderException; import starlight.domain.order.wallet.UsageHistory; @@ -14,10 +15,11 @@ @Service @RequiredArgsConstructor @Transactional -public class UsageCreditService implements UsageCreditPort { +public class UsageCreditChargeService implements UsageCreditChargePort { - private final UsageWalletQuery usageWalletQuery; - private final UsageHistoryQuery usageHistoryQuery; + private final UsageWalletQueryPort usageWalletQueryPort; + private final UsageWalletCommandPort usageWalletCommandPort; + private final UsageHistoryCommandPort usageHistoryCommandPort; /** * 주문 결제가 완료되었을 때 사용권(지갑)을 충전한다. @@ -33,15 +35,15 @@ public void chargeForOrder(Long userId, Long orderId, int usageCount) { } // 지갑 조회 or 생성 - UsageWallet wallet = usageWalletQuery.findByUserId(userId) - .orElseGet(() -> usageWalletQuery.save(UsageWallet.init(userId))); + UsageWallet wallet = usageWalletQueryPort.findByUserId(userId) + .orElseGet(() -> usageWalletCommandPort.save(UsageWallet.init(userId))); // 사용권 충전 wallet.chargeAiReport(usageCount); - usageWalletQuery.save(wallet); + usageWalletCommandPort.save(wallet); // 이력 기록 - usageHistoryQuery.save( + usageHistoryCommandPort.save( UsageHistory.charged( userId, usageCount, diff --git a/src/main/java/starlight/application/usage/provided/UsageCreditPort.java b/src/main/java/starlight/application/usage/provided/UsageCreditPort.java deleted file mode 100644 index 724eeb4c..00000000 --- a/src/main/java/starlight/application/usage/provided/UsageCreditPort.java +++ /dev/null @@ -1,6 +0,0 @@ -package starlight.application.usage.provided; - -public interface UsageCreditPort { - - void chargeForOrder(Long userId, Long orderId, int usageCount); -} diff --git a/src/main/java/starlight/application/usage/provided/UsageHistoryQuery.java b/src/main/java/starlight/application/usage/required/UsageHistoryCommandPort.java similarity index 54% rename from src/main/java/starlight/application/usage/provided/UsageHistoryQuery.java rename to src/main/java/starlight/application/usage/required/UsageHistoryCommandPort.java index 2f9c10af..a6a2c149 100644 --- a/src/main/java/starlight/application/usage/provided/UsageHistoryQuery.java +++ b/src/main/java/starlight/application/usage/required/UsageHistoryCommandPort.java @@ -1,8 +1,8 @@ -package starlight.application.usage.provided; +package starlight.application.usage.required; import starlight.domain.order.wallet.UsageHistory; -public interface UsageHistoryQuery { +public interface UsageHistoryCommandPort { UsageHistory save(UsageHistory usageHistory); } diff --git a/src/main/java/starlight/application/usage/required/UsageWalletCommandPort.java b/src/main/java/starlight/application/usage/required/UsageWalletCommandPort.java new file mode 100644 index 00000000..26a94fac --- /dev/null +++ b/src/main/java/starlight/application/usage/required/UsageWalletCommandPort.java @@ -0,0 +1,8 @@ +package starlight.application.usage.required; + +import starlight.domain.order.wallet.UsageWallet; + +public interface UsageWalletCommandPort { + + UsageWallet save(UsageWallet usageWallet); +} diff --git a/src/main/java/starlight/application/usage/provided/UsageWalletQuery.java b/src/main/java/starlight/application/usage/required/UsageWalletQueryPort.java similarity index 51% rename from src/main/java/starlight/application/usage/provided/UsageWalletQuery.java rename to src/main/java/starlight/application/usage/required/UsageWalletQueryPort.java index 37f99ee9..a36b1772 100644 --- a/src/main/java/starlight/application/usage/provided/UsageWalletQuery.java +++ b/src/main/java/starlight/application/usage/required/UsageWalletQueryPort.java @@ -1,12 +1,10 @@ -package starlight.application.usage.provided; +package starlight.application.usage.required; import starlight.domain.order.wallet.UsageWallet; import java.util.Optional; -public interface UsageWalletQuery { +public interface UsageWalletQueryPort { Optional findByUserId(Long userId); - - UsageWallet save(UsageWallet usageWallet); } diff --git a/src/main/java/starlight/bootstrap/ObjectStorageConfig.java b/src/main/java/starlight/bootstrap/ObjectStorageConfig.java index f587fe84..f987b340 100644 --- a/src/main/java/starlight/bootstrap/ObjectStorageConfig.java +++ b/src/main/java/starlight/bootstrap/ObjectStorageConfig.java @@ -35,7 +35,7 @@ public S3Client ncpS3Client() { } @Bean - public S3Presigner s3Presigner() { + public S3Presigner ncpS3Presigner() { return S3Presigner.builder() .region(Region.of("kr-standard")) .endpointOverride(URI.create(endpoint)) @@ -44,4 +44,4 @@ public S3Presigner s3Presigner() { )) .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/bootstrap/RestClientConfig.java b/src/main/java/starlight/bootstrap/RestClientConfig.java index 047d4ff5..5c59261e 100644 --- a/src/main/java/starlight/bootstrap/RestClientConfig.java +++ b/src/main/java/starlight/bootstrap/RestClientConfig.java @@ -47,8 +47,8 @@ public RestClient clovaOcrRestClient( * - UA만 지정 (일부 서버 호환) * 필요 없으면 이 빈은 제거해도 됨. */ - @Bean(name = "downloadClient") - public RestClient downloadClient() { + @Bean(name = "pdfDownloadRestClient") + public RestClient pdfDownloadRestClient() { JdkClientHttpRequestFactory factory = new JdkClientHttpRequestFactory(); factory.setReadTimeout(Duration.ofSeconds(60)); @@ -58,8 +58,8 @@ public RestClient downloadClient() { .build(); } - @Bean(name = "clovaClient") - public RestClient clovaStudioClient( + @Bean(name = "clovaStudioRestClient") + public RestClient clovaStudioRestClient( @Value("${cloud.ncp.studio.host}") String clovaHost, @Value("${cloud.ncp.studio.api-key}") String apiKey, @Value("${cloud.ncp.studio.model}") String model @@ -97,4 +97,4 @@ public RestClient tossRestClient( .defaultHeader("Accept", "application/json") .build(); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/domain/expertApplication/exception/ExpertApplicationErrorType.java b/src/main/java/starlight/domain/expertApplication/exception/ExpertApplicationErrorType.java index 72c97a8a..01eba326 100644 --- a/src/main/java/starlight/domain/expertApplication/exception/ExpertApplicationErrorType.java +++ b/src/main/java/starlight/domain/expertApplication/exception/ExpertApplicationErrorType.java @@ -10,8 +10,8 @@ public enum ExpertApplicationErrorType implements ErrorType { // 전문가 신청 관련 오류 타입 정의 - EXPERT_APPLICATION_QUERY_ERROR(HttpStatus.NOT_FOUND, "전문가 정보를 조회하는 중에 오류가 발생했습니다."), - EXPERT_APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 전문가를 찾을 수 없습니다."), + EXPERT_APPLICATION_QUERY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "전문가 신청 정보를 조회하는 중에 오류가 발생했습니다."), + EXPERT_APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 전문가 신청을 찾을 수 없습니다."), APPLICATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "이미 신청한 전문가입니다."), EXPERT_FEEDBACK_REQUEST_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "전문가 피드백 요청에 실패했습니다."), diff --git a/src/main/java/starlight/domain/expertReport/entity/ExpertReport.java b/src/main/java/starlight/domain/expertReport/entity/ExpertReport.java index 1a98d48a..914eccbb 100644 --- a/src/main/java/starlight/domain/expertReport/entity/ExpertReport.java +++ b/src/main/java/starlight/domain/expertReport/entity/ExpertReport.java @@ -51,7 +51,7 @@ public class ExpertReport extends AbstractEntity { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(nullable = false) - private List details = new ArrayList<>(); + private List comments = new ArrayList<>(); // 7일의 기한을 가지고 기한 내에만 수정가능하다. // -> expiredAt, submitStatus 필드로 관리 @@ -120,12 +120,12 @@ public void updateOverallComment(String overallComment) { this.overallComment = overallComment; } - public void updateDetails(List newDetails) { - Assert.notNull(newDetails, "details는 null일 수 없습니다"); + public void updateComments(List newComments) { + Assert.notNull(newComments, "comments는 null일 수 없습니다"); validateCanEdit(); - this.details.clear(); - this.details.addAll(newDetails); + this.comments.clear(); + this.comments.addAll(newComments); } public void incrementViewCount() { diff --git a/src/main/java/starlight/domain/expertReport/entity/ExpertReportDetail.java b/src/main/java/starlight/domain/expertReport/entity/ExpertReportComment.java similarity index 62% rename from src/main/java/starlight/domain/expertReport/entity/ExpertReportDetail.java rename to src/main/java/starlight/domain/expertReport/entity/ExpertReportComment.java index 0a985304..35aef9fa 100644 --- a/src/main/java/starlight/domain/expertReport/entity/ExpertReportDetail.java +++ b/src/main/java/starlight/domain/expertReport/entity/ExpertReportComment.java @@ -11,23 +11,23 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ExpertReportDetail extends AbstractEntity { +public class ExpertReportComment extends AbstractEntity { @Enumerated(EnumType.STRING) @Column(nullable = false, length = 30) - private CommentType commentType; + private CommentType type; @Column(columnDefinition = "TEXT", nullable = false) private String content; - public static ExpertReportDetail create(CommentType commentType, String content) { - Assert.notNull(commentType, "commentType은 필수입니다"); + public static ExpertReportComment create(CommentType type, String content) { + Assert.notNull(type, "type은 필수입니다"); Assert.hasText(content, "content는 필수입니다"); - ExpertReportDetail detail = new ExpertReportDetail(); - detail.commentType = commentType; - detail.content = content; - return detail; + ExpertReportComment comment = new ExpertReportComment(); + comment.type = type; + comment.content = content; + return comment; } public void update(String content) { diff --git a/src/main/java/starlight/domain/expertReport/exception/ExpertReportErrorType.java b/src/main/java/starlight/domain/expertReport/exception/ExpertReportErrorType.java index d05c677e..ee2160a2 100644 --- a/src/main/java/starlight/domain/expertReport/exception/ExpertReportErrorType.java +++ b/src/main/java/starlight/domain/expertReport/exception/ExpertReportErrorType.java @@ -10,7 +10,7 @@ public enum ExpertReportErrorType implements ErrorType { // 전문가 피드백 신청 관련 오류 - EXPERT_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 전문가를 찾을 수 없습니다."), + EXPERT_REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 전문가 리포트를 찾을 수 없습니다."), ALREADY_SUBMITTED(HttpStatus.BAD_REQUEST, "이미 전문가 피드백을 제출하였습니다."), REPORT_EXPIRED(HttpStatus.BAD_REQUEST, "전문가 피드백 요청 기간이 만료되었습니다."), ; diff --git a/src/test/java/starlight/adapter/ncp/ocr/ClovaOcrProviderTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProviderTest.java similarity index 94% rename from src/test/java/starlight/adapter/ncp/ocr/ClovaOcrProviderTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProviderTest.java index cd372635..1a155607 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/ClovaOcrProviderTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/ClovaOcrProviderTest.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr; +package starlight.adapter.aireport.infrastructure.ocr; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -8,13 +8,14 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; -import starlight.adapter.ncp.ocr.infra.ClovaOcrClient; -import starlight.adapter.ncp.ocr.infra.PdfDownloadClient; -import starlight.adapter.ncp.ocr.util.OcrResponseMerger; -import starlight.adapter.ncp.ocr.util.OcrTextExtractor; -import starlight.adapter.ncp.ocr.util.PdfUtils; +import starlight.adapter.aireport.infrastructure.ocr.ClovaOcrProvider; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.infra.ClovaOcrClient; +import starlight.adapter.aireport.infrastructure.ocr.infra.PdfDownloadClient; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrResponseMerger; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrTextExtractor; +import starlight.adapter.aireport.infrastructure.ocr.util.PdfUtils; import starlight.shared.dto.infrastructure.OcrResponse; import java.util.List; diff --git a/src/test/java/starlight/adapter/ncp/ocr/infra/ClovaOcrClientTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/ClovaOcrClientTest.java similarity index 93% rename from src/test/java/starlight/adapter/ncp/ocr/infra/ClovaOcrClientTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/ClovaOcrClientTest.java index 651ac187..63c87924 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/infra/ClovaOcrClientTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/ClovaOcrClientTest.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.infra; +package starlight.adapter.aireport.infrastructure.ocr.infra; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -8,9 +8,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.web.client.RestClient; -import starlight.adapter.ncp.ocr.dto.ClovaOcrRequest; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.dto.ClovaOcrRequest; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.infra.ClovaOcrClient; import starlight.shared.dto.infrastructure.OcrResponse; import static org.assertj.core.api.Assertions.assertThat; diff --git a/src/test/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClientIntegrationTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClientIntegrationTest.java similarity index 94% rename from src/test/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClientIntegrationTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClientIntegrationTest.java index 442f13cb..660b1980 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClientIntegrationTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClientIntegrationTest.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.ocr.infra; +package starlight.adapter.aireport.infrastructure.ocr.infra; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -9,8 +9,9 @@ import org.junit.jupiter.api.Test; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.web.client.RestClient; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.infra.PdfDownloadClient; import java.io.IOException; import java.time.Duration; diff --git a/src/test/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClientTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClientTest.java similarity index 96% rename from src/test/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClientTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClientTest.java index 83842a7f..55f6aad0 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/infra/PdfDownloadClientTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/infra/PdfDownloadClientTest.java @@ -1,12 +1,13 @@ -package starlight.adapter.ncp.ocr.infra; +package starlight.adapter.aireport.infrastructure.ocr.infra; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClient; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.infra.PdfDownloadClient; import java.net.URI; diff --git a/src/test/java/starlight/adapter/ncp/ocr/util/OcrResponseMergerUnitTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrResponseMergerUnitTest.java similarity index 97% rename from src/test/java/starlight/adapter/ncp/ocr/util/OcrResponseMergerUnitTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrResponseMergerUnitTest.java index 9c9dce45..3bf8c1dd 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/util/OcrResponseMergerUnitTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrResponseMergerUnitTest.java @@ -1,7 +1,8 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrResponseMerger; import starlight.shared.dto.infrastructure.OcrResponse; import java.util.ArrayList; diff --git a/src/test/java/starlight/adapter/ncp/ocr/util/OcrTextExtractorUnitTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrTextExtractorUnitTest.java similarity index 98% rename from src/test/java/starlight/adapter/ncp/ocr/util/OcrTextExtractorUnitTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrTextExtractorUnitTest.java index 8af0db54..8f883ee5 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/util/OcrTextExtractorUnitTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/OcrTextExtractorUnitTest.java @@ -1,7 +1,8 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrTextExtractor; import starlight.shared.dto.infrastructure.OcrResponse; import java.util.List; diff --git a/src/test/java/starlight/adapter/ncp/ocr/util/PdfUtilsUnitTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/PdfUtilsUnitTest.java similarity index 96% rename from src/test/java/starlight/adapter/ncp/ocr/util/PdfUtilsUnitTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/PdfUtilsUnitTest.java index f822afbe..34ffb5a4 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/util/PdfUtilsUnitTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/PdfUtilsUnitTest.java @@ -1,11 +1,12 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import starlight.adapter.ncp.ocr.exception.OcrErrorType; -import starlight.adapter.ncp.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrErrorType; +import starlight.adapter.aireport.infrastructure.ocr.exception.OcrException; +import starlight.adapter.aireport.infrastructure.ocr.util.PdfUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/src/test/java/starlight/adapter/ncp/ocr/util/PrivateConstructorTests.java b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/PrivateConstructorTests.java similarity index 91% rename from src/test/java/starlight/adapter/ncp/ocr/util/PrivateConstructorTests.java rename to src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/PrivateConstructorTests.java index 1513ae9a..16ae4dc1 100644 --- a/src/test/java/starlight/adapter/ncp/ocr/util/PrivateConstructorTests.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/ocr/util/PrivateConstructorTests.java @@ -1,7 +1,10 @@ -package starlight.adapter.ncp.ocr.util; +package starlight.adapter.aireport.infrastructure.ocr.util; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrResponseMerger; +import starlight.adapter.aireport.infrastructure.ocr.util.OcrTextExtractor; +import starlight.adapter.aireport.infrastructure.ocr.util.PdfUtils; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; diff --git a/src/test/java/starlight/adapter/ncp/storage/NcpPresignedUrlProviderUnitTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProviderUnitTest.java similarity index 98% rename from src/test/java/starlight/adapter/ncp/storage/NcpPresignedUrlProviderUnitTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProviderUnitTest.java index 7e7426d7..22344d18 100644 --- a/src/test/java/starlight/adapter/ncp/storage/NcpPresignedUrlProviderUnitTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/storage/NcpPresignedUrlProviderUnitTest.java @@ -1,4 +1,4 @@ -package starlight.adapter.ncp.storage; +package starlight.adapter.aireport.infrastructure.storage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -14,6 +14,7 @@ import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; +import starlight.adapter.aireport.infrastructure.storage.NcpPresignedUrlProvider; import starlight.shared.dto.infrastructure.PreSignedUrlResponse; import java.net.URL; diff --git a/src/test/java/starlight/adapter/ncp/webapi/ImageControllerIntegrationTest.java b/src/test/java/starlight/adapter/aireport/infrastructure/webapi/ImageControllerIntegrationTest.java similarity index 89% rename from src/test/java/starlight/adapter/ncp/webapi/ImageControllerIntegrationTest.java rename to src/test/java/starlight/adapter/aireport/infrastructure/webapi/ImageControllerIntegrationTest.java index 7d26dcf6..a376b77b 100644 --- a/src/test/java/starlight/adapter/ncp/webapi/ImageControllerIntegrationTest.java +++ b/src/test/java/starlight/adapter/aireport/infrastructure/webapi/ImageControllerIntegrationTest.java @@ -1,34 +1,25 @@ -package starlight.adapter.ncp.webapi; +package starlight.adapter.aireport.infrastructure.webapi; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; -import org.springframework.context.annotation.Import; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.MediaType; -import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import starlight.adapter.aireport.webapi.ImageController; import starlight.adapter.auth.security.auth.AuthDetails; import starlight.adapter.auth.security.filter.JwtFilter; +import starlight.application.aireport.required.PresignedUrlProvider; +import starlight.bootstrap.SecurityConfig; import starlight.domain.member.entity.Member; import starlight.domain.member.enumerate.MemberType; -import starlight.shared.dto.infrastructure.PreSignedUrlResponse; -import starlight.application.infrastructure.provided.PresignedUrlProvider; -import starlight.bootstrap.SecurityConfig; - -import java.util.List; import static org.mockito.BDDMockito.*; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; diff --git a/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java b/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java deleted file mode 100644 index e4f58941..00000000 --- a/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package starlight.adapter.expert.persistence; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.util.ReflectionTestUtils; -import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.entity.ExpertCareer; -import starlight.domain.expert.enumerate.TagCategory; - -import jakarta.persistence.EntityManager; -import java.lang.reflect.Constructor; -import java.time.LocalDateTime; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -class ExpertRepositoryTest { - - @Autowired ExpertRepository repository; - @Autowired EntityManager em; - - @Test - @DisplayName("findByAllCategories: 전달된 모든 카테고리를 가진 Expert만 조회된다(AND)") - void findByAllCategories_AND() throws Exception { - // given - Expert a = expert("A", - Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY)); - Expert b = expert("B", - Set.of(TagCategory.GROWTH_STRATEGY)); // 조건 미충족 - Expert c = expert("C", - Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY, TagCategory.METRIC_DATA)); - - em.persist(a); em.persist(b); em.persist(c); - em.flush(); em.clear(); - - // when - List found = repository.findByAllCategories( - Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY), - 2L // size - ); - - // then - assertThat(found).extracting("name").containsExactlyInAnyOrder("A", "C"); - } - - // ---- helpers ---- - private Expert expert(String name, Set cats) throws Exception { - Constructor ctor = Expert.class.getDeclaredConstructor(); - ctor.setAccessible(true); - Expert e = ctor.newInstance(); - ReflectionTestUtils.setField(e, "name", name); - ReflectionTestUtils.setField(e, "email", name.toLowerCase() + "@example.com"); - ReflectionTestUtils.setField(e, "careers", List.of( - career(e, 1, "career1"), - career(e, 2, "career2") - )); - ReflectionTestUtils.setField(e, "categories", new LinkedHashSet<>(cats)); - return e; - } - - private ExpertCareer career(Expert expert, int orderIndex, String title) { - return ExpertCareer.of( - expert, - orderIndex, - title, - "desc", - LocalDateTime.now().minusMonths(1), - LocalDateTime.now() - ); - } -} diff --git a/src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java b/src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java index 061c3884..64bb8bee 100644 --- a/src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java +++ b/src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java @@ -42,11 +42,11 @@ class ExpertControllerTest { @MockitoBean JpaMetamodelMappingContext jpaMetamodelMappingContext; @Test - @DisplayName("카테고리 미전달 시 전체 조회") + @DisplayName("전문가 전체 조회") void listAll() throws Exception { ExpertDetailResult e1 = expertResult(1L, "홍길동", Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY)); - when(expertDetailQuery.search(null)).thenReturn(List.of(e1)); + when(expertDetailQuery.searchAll()).thenReturn(List.of(e1)); mockMvc.perform(get("/v1/experts")) .andExpect(status().isOk()) @@ -58,43 +58,6 @@ void listAll() throws Exception { .andExpect(jsonPath("$.data[0].applicationCount").doesNotExist()); } - @Test - @DisplayName("카테고리 AND 매칭 (?categories=A&categories=B)") - void searchByAllCategories_multiParams() throws Exception { - ExpertDetailResult e1 = expertResult(2L, "이영희", - Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY)); - - when(expertDetailQuery.search(Set.of( - TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY - ))).thenReturn(List.of(e1)); - - mockMvc.perform(get("/v1/experts") - .param("categories", "GROWTH_STRATEGY") - .param("categories", "TEAM_CAPABILITY")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.result").value("SUCCESS")) - .andExpect(jsonPath("$.data[0].name").value("이영희")) - .andExpect(jsonPath("$.data[0].careers.length()").value(3)); - } - - @Test - @DisplayName("카테고리 AND 매칭 (콤마 구분)") - void searchByAllCategories_commaSeparated() throws Exception { - ExpertDetailResult e1 = expertResult(3L, "박철수", - Set.of(TagCategory.MARKET_BM, TagCategory.METRIC_DATA)); - - when(expertDetailQuery.search(Set.of( - TagCategory.MARKET_BM, TagCategory.METRIC_DATA - ))).thenReturn(List.of(e1)); - - mockMvc.perform(get("/v1/experts") - .param("categories", "MARKET_BM,METRIC_DATA")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.result").value("SUCCESS")) - .andExpect(jsonPath("$.data[0].name").value("박철수")) - .andExpect(jsonPath("$.data[0].careers.length()").value(3)); - } - @Test @DisplayName("전문가 상세 조회") void detail() throws Exception { diff --git a/src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java b/src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java index f52c326f..98f8b997 100644 --- a/src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java +++ b/src/test/java/starlight/application/aireport/AiReportServiceImplIntegrationTest.java @@ -17,10 +17,10 @@ import starlight.adapter.businessplan.persistence.BusinessPlanRepository; import starlight.application.aireport.provided.dto.AiReportResponse; import starlight.application.aireport.required.AiReportGrader; +import starlight.application.aireport.required.OcrProvider; import starlight.application.businessplan.provided.BusinessPlanService; import starlight.application.businessplan.provided.dto.BusinessPlanResponse; import starlight.application.businessplan.util.BusinessPlanContentExtractor; -import starlight.application.infrastructure.provided.OcrProvider; import starlight.domain.aireport.entity.AiReport; import starlight.domain.businessplan.entity.BusinessPlan; import starlight.domain.businessplan.entity.SubSection; @@ -46,10 +46,6 @@ class AiReportServiceImplIntegrationTest { AiReportRepository aiReportRepository; @Autowired EntityManager em; - @Autowired - ObjectMapper objectMapper; - @Autowired - AiReportResponseParser responseParser; @TestConfiguration static class TestBeans { diff --git a/src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java b/src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java index ffdcdf3f..b8a07ad3 100644 --- a/src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java +++ b/src/test/java/starlight/application/aireport/AiReportServiceImplUnitTest.java @@ -7,10 +7,10 @@ import starlight.application.aireport.provided.dto.AiReportResponse; import starlight.application.aireport.required.AiReportGrader; import starlight.application.aireport.required.AiReportQuery; +import starlight.application.aireport.required.OcrProvider; import starlight.application.businessplan.provided.BusinessPlanService; import starlight.application.businessplan.required.BusinessPlanQuery; import starlight.application.businessplan.util.BusinessPlanContentExtractor; -import starlight.application.infrastructure.provided.OcrProvider; import starlight.domain.aireport.entity.AiReport; import starlight.domain.aireport.exception.AiReportErrorType; import starlight.domain.aireport.exception.AiReportException; diff --git a/src/test/java/starlight/domain/expertReport/entity/ExpertReportTest.java b/src/test/java/starlight/domain/expertReport/entity/ExpertReportTest.java index 066a71b2..79e7baa4 100644 --- a/src/test/java/starlight/domain/expertReport/entity/ExpertReportTest.java +++ b/src/test/java/starlight/domain/expertReport/entity/ExpertReportTest.java @@ -104,32 +104,32 @@ void editAfterSubmit_ThrowsException() { } @Test - @DisplayName("Details 업데이트 - null 예외") - void updateDetails_Null_ThrowsException() { + @DisplayName("Comments 업데이트 - null 예외") + void updateComments_Null_ThrowsException() { // given ExpertReport report = ExpertReport.create(1L, 10L, "token"); // when & then - assertThatThrownBy(() -> report.updateDetails(null)) + assertThatThrownBy(() -> report.updateComments(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("details는 null일 수 없습니다"); + .hasMessageContaining("comments는 null일 수 없습니다"); } @Test - @DisplayName("Details 업데이트 - 성공") - void updateDetails_Success() { + @DisplayName("Comments 업데이트 - 성공") + void updateComments_Success() { // given ExpertReport report = ExpertReport.create(1L, 10L, "token"); - List details = List.of( - ExpertReportDetail.create(CommentType.STRENGTH, "좋습니다"), - ExpertReportDetail.create(CommentType.WEAKNESS, "개선 필요") + List comments = List.of( + ExpertReportComment.create(CommentType.STRENGTH, "좋습니다"), + ExpertReportComment.create(CommentType.WEAKNESS, "개선 필요") ); // when - report.updateDetails(details); + report.updateComments(comments); // then - assertThat(report.getDetails()).hasSize(2); + assertThat(report.getComments()).hasSize(2); } @Test @@ -148,28 +148,28 @@ void incrementViewCount_Success() { } @Test - @DisplayName("ExpertReportDetail 생성 - 성공") - void createDetail_Success() { + @DisplayName("ExpertReportComment 생성 - 성공") + void createComment_Success() { // given CommentType type = CommentType.STRENGTH; String content = "시장 분석이 우수합니다."; // when - ExpertReportDetail detail = ExpertReportDetail.create(type, content); + ExpertReportComment comment = ExpertReportComment.create(type, content); // then - assertThat(detail).isNotNull(); - assertThat(detail.getCommentType()).isEqualTo(type); - assertThat(detail.getContent()).isEqualTo(content); + assertThat(comment).isNotNull(); + assertThat(comment.getType()).isEqualTo(type); + assertThat(comment.getContent()).isEqualTo(content); } @Test - @DisplayName("ExpertReportDetail 생성 - content empty 예외") - void createDetail_EmptyContent_ThrowsException() { + @DisplayName("ExpertReportComment 생성 - content empty 예외") + void createComment_EmptyContent_ThrowsException() { // when & then assertThatThrownBy(() -> - ExpertReportDetail.create(CommentType.STRENGTH, "")) + ExpertReportComment.create(CommentType.STRENGTH, "")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("content는 필수입니다"); } -} \ No newline at end of file +} diff --git "a/\352\260\234\353\260\234\352\260\200\354\235\264\353\223\234.md" "b/\352\260\234\353\260\234\352\260\200\354\235\264\353\223\234.md" index ea412b97..bfc144b2 100644 --- "a/\352\260\234\353\260\234\352\260\200\354\235\264\353\223\234.md" +++ "b/\352\260\234\353\260\234\352\260\200\354\235\264\353\223\234.md" @@ -31,6 +31,8 @@ - Outbound 포트는 소비자 도메인에서 정의한다(`application//required`). - Cross-domain 조회는 `OtherDomainLookupPort` 규칙을 따른다. - Response DTO는 애플리케이션 DTO로만 변환하고 엔티티를 직접 받지 않는다. +- 도메인 의미가 있는 포트는 소비자 도메인 `required`에 둔다. +- 순수 인프라 포트는 shared/infrastructure 패키지로 분리할 수 있다. ## 네이밍 규칙 요약 - Provided (inbound): `*UseCase` diff --git "a/\353\217\204\353\251\224\354\235\270\353\252\250\353\215\270.md" "b/\353\217\204\353\251\224\354\235\270\353\252\250\353\215\270.md" index 8e8e6277..2a579693 100644 --- "a/\353\217\204\353\251\224\354\235\270\353\252\250\353\215\270.md" +++ "b/\353\217\204\353\251\224\354\235\270\353\252\250\353\215\270.md" @@ -275,7 +275,7 @@ _Aggregate Root_ - `viewCount`: `int` - 조회 횟수 - `overallComment`: `String` - 전체 코멘트 (TEXT) - `submitStatus`: `SubmitStatus` - 제출 상태 (기본값: PENDING) -- `details`: `List` - 리포트 상세 목록 (1:N 관계) +- `comments`: `List` - 리포트 코멘트 목록 (1:N 관계) #### 행위 - `static create()`: 전문가 리포트 생성 (전문가ID, 사업계획서ID, 토큰) - 만료일시는 생성 후 7일 @@ -286,7 +286,7 @@ _Aggregate Root_ - `temporarySave()`: 임시 저장 - `submit()`: 리포트 제출 - `updateOverallComment()`: 전체 코멘트 업데이트 -- `updateDetails()`: 리포트 상세 목록 업데이트 +- `updateComments()`: 리포트 코멘트 목록 업데이트 - `incrementViewCount()`: 조회 횟수 증가 #### 규칙 @@ -297,15 +297,15 @@ _Aggregate Root_ - expiredAt가 현재 시간보다 이전이면 EXPIRED 상태로 변경된다. - 동일한 사업계획서와 전문가 조합에 대해 1개의 리포트만 존재할 수 있다 (유니크 제약 조건). -### 리포트 상세(ExpertReportDetail) +### 리포트 코멘트(ExpertReportComment) _Entity_ #### 속성 - `id`: `Long` -- `commentType`: `CommentType` - 코멘트 타입 (STRENGTH, WEAKNESS) +- `type`: `CommentType` - 코멘트 타입 (STRENGTH, WEAKNESS) - `content`: `String` - 내용 (TEXT) #### 행위 -- `static create()`: 리포트 상세 생성 (코멘트타입, 내용) +- `static create()`: 리포트 코멘트 생성 (코멘트타입, 내용) - `update()`: 내용 업데이트 ### 제출 상태(SubmitStatus) @@ -406,6 +406,30 @@ _Enum_ #### 상수 - 사용 크레딧 상품 타입 (1회권, 2회권 등) +### 사용 지갑(UsageWallet) +_Aggregate Root_ +#### 속성 +- `id`: `Long` +- `userId`: `Long` - 사용자 ID +- `aiReportRemainingCount`: `int` - 남은 AI 리포트 사용 가능 횟수 + +#### 행위 +- `static init()`: 지갑 초기화 +- `chargeAiReport()`: 사용권 충전 +- `useAiReport()`: 사용권 사용 + +### 사용 이력(UsageHistory) +_Entity_ +#### 속성 +- `id`: `Long` +- `userId`: `Long` - 사용자 ID +- `changedCount`: `int` - 증감 수량 +- `remainingCount`: `int` - 변경 후 잔여 수량 +- `orderId`: `Long` - 주문 ID (충전 연동) + +#### 행위 +- `static charged()`: 충전 이력 생성 + ### 주문 코드(OrderCode) _Value Object_ #### 속성 @@ -424,4 +448,4 @@ _Value Object_ - `static of()`: Money 생성 (금액, 통화) - `static krw()`: 한국 원화 Money 생성 ---- \ No newline at end of file +--- diff --git "a/\354\232\251\354\226\264\354\202\254\354\240\204.md" "b/\354\232\251\354\226\264\354\202\254\354\240\204.md" index a046a8eb..19997895 100644 --- "a/\354\232\251\354\226\264\354\202\254\354\240\204.md" +++ "b/\354\232\251\354\226\264\354\202\254\354\240\204.md" @@ -14,12 +14,13 @@ | 성장 전략 | GrowthTactic | 사업계획서의 네 번째 섹션. 비즈니스 모델, 자금조달 계획, 시장진입 및 성과창출 전략을 포함한다. | | 팀 역량 | TeamCompetence | 사업계획서의 다섯 번째 섹션. 창업자의 역량, 팀 역량을 포함한다. | | 피드백 신청 | ExpertApplication | 창업자가 특정 전문가에게 자신의 사업계획서에 대한 피드백을 요청하는 신청. 동일한 사업계획서에 대해 동일한 전문가에게는 1회만 신청 가능하다. | -| 전문가 리포트 | ExpertReport | 전문가가 사업계획서에 대한 피드백을 작성한 리포트. 전체 코멘트와 각 섹션별 상세 코멘트(강점/약점)를 포함한다. 7일의 평가 기한을 가진다. | -| 리포트 상세 | ExpertReportDetail | 전문가 리포트의 세부 내용. 강점(STRENGTH) 또는 약점(WEAKNESS) 타입의 코멘트를 포함한다. | +| 전문가 리포트 | ExpertReport | 전문가가 사업계획서에 대한 피드백을 작성한 리포트. 전체 코멘트와 각 코멘트(강점/약점)를 포함한다. 7일의 평가 기한을 가진다. | +| 리포트 코멘트 | ExpertReportComment | 전문가 리포트의 세부 코멘트. 강점(STRENGTH) 또는 약점(WEAKNESS) 타입의 코멘트를 포함한다. | | AI 리포트 | AiReport | AI가 사업계획서를 분석하여 자동으로 생성한 리포트. JSON 형태로 저장된다. | | 주문 | Orders | 전문가 피드백 신청을 위한 결제 주문. 토스페이먼츠를 통해 결제를 처리한다. | | 결제 기록 | PaymentRecords | 주문에 대한 결제 시도 및 완료 기록. 여러 번의 결제 시도를 기록할 수 있다. | -| 사용 크레딧 | UsageCredit | 전문가 피드백 신청에 사용하는 크레딧(이용권). 1회권, 2회권 등의 상품이 있다. | +| 사용 지갑 | UsageWallet | 사용자별 이용권 잔여 수량을 관리하는 지갑. | +| 사용 이력 | UsageHistory | 이용권 충전/사용 이력을 기록한다. | | 사업계획서 상태 | PlanStatus | 사업계획서의 진행 상태. STARTED(시작됨), WRITTEN_COMPLETED(작성 완료), AI_REVIEWED(AI 리뷰 완료), EXPERT_MATCHED(전문가 매칭 완료), FINALIZED(최종 완료)가 있다. | | 제출 상태 | SubmitStatus | 전문가 리포트의 제출 상태. PENDING(평가 전), TEMPORARY_SAVED(임시 저장), SUBMITTED(제출 완료), EXPIRED(만료됨)가 있다. | | 코멘트 타입 | CommentType | 전문가 리포트 상세의 코멘트 타입. STRENGTH(강점), WEAKNESS(약점)이 있다. | @@ -27,4 +28,3 @@ | 주문 상태 | OrderStatus | 주문의 결제 상태. NEW(주문 생성됨, 결제 전), PAID(결제 완료), CANCELED(주문/결제 취소)가 있다. | | 인증 정보 | Credential | 회원의 비밀번호 등 인증 관련 정보. Member와 1:1 관계이다. | | 원시 JSON | RawJson | 원본 데이터를 JSON 형태로 저장하기 위한 값 객체. | -