diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE/standard.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE/standard.md diff --git a/.github/PULL_REQUEST_TEMPLATE/why-what-how.md b/.github/PULL_REQUEST_TEMPLATE/why-what-how.md new file mode 100644 index 00000000..21a5a0a5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/why-what-how.md @@ -0,0 +1,25 @@ +## ๐Ÿš€ Why - ํ•ด๊ฒฐํ•˜๋ ค๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฌด์—‡์ธ๊ฐ€์š”? + +- `์–ด๋–ค ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ•˜๋‚˜์š”?` + +- `์–ด๋–ค ๋ฐฐ๊ฒฝ์ด ์žˆ์—ˆ๋‚˜์š”?` + +## โœ… What - ๋ฌด์—‡์ด ๋ณ€๊ฒฝ๋๋‚˜์š”? + +- `๊ตฌํ˜„ํ•œ ๊ธฐ๋Šฅ ์š”์•ฝ` + +- `์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ` + +## ๐Ÿ› ๏ธ How - ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ–ˆ๋‚˜์š”? + +- `ํ•ต์‹ฌ ๋กœ์ง ์„ค๋ช…` + +- `์˜ˆ์™ธ ์‚ฌํ•ญ, ๊ณ ๋ฏผ ํฌ์ธํŠธ ๋“ฑ` + +## ๐Ÿ–ผ๏ธ Attachment + +- `ํ™”๋ฉด ์ด๋ฏธ์ง€, ๊ฒฐ๊ณผ ์บก์ฒ˜ ๋“ฑ ์ฒจ๋ถ€` + +## ๐Ÿ’ฌ ๊ธฐํƒ€ ์ฝ”๋ฉ˜ํŠธ + +- `๋ฆฌ๋ทฐ์–ด์—๊ฒŒ ์ „ํ•˜๊ณ  ์‹ถ์€ ๋ง, ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•, ์ฃผ์˜ํ•  ์  ๋“ฑ` diff --git a/.gitignore b/.gitignore index b1eddcdb..735e7529 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ node_modules/ dist/ *.log /docs/ +/AGENTS.md diff --git a/src/main/java/starlight/StarlightApplication.java b/src/main/java/starlight/StarlightApplication.java index d254238b..d5e94dbf 100644 --- a/src/main/java/starlight/StarlightApplication.java +++ b/src/main/java/starlight/StarlightApplication.java @@ -2,7 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing diff --git a/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java b/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java index e3a18e2b..03ecf615 100644 --- a/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java +++ b/src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import starlight.application.expert.required.ExpertQuery; +import starlight.application.expert.required.ExpertQueryPort; import starlight.domain.expert.entity.Expert; import starlight.domain.expert.enumerate.TagCategory; import starlight.domain.expert.exception.ExpertErrorType; @@ -19,7 +19,9 @@ @Slf4j @Component @RequiredArgsConstructor -public class ExpertJpa implements ExpertQuery { +public class ExpertJpa implements ExpertQueryPort, + starlight.application.expertReport.required.ExpertLookupPort, + starlight.application.expertApplication.required.ExpertLookupPort { private final ExpertRepository repository; @@ -31,16 +33,29 @@ public Expert findById(Long id) { } @Override - public Expert findByIdWithDetails(Long id) { - return repository.findByIdWithDetails(id).orElseThrow( - () -> new ExpertException(ExpertErrorType.EXPERT_NOT_FOUND) - ); + public Expert findByIdWithCareersAndTags(Long id) { + try { + List experts = repository.fetchExpertsWithCareersByIds(List.of(id)); + if (experts.isEmpty()) { + throw new ExpertException(ExpertErrorType.EXPERT_NOT_FOUND); + } + + repository.fetchExpertsWithTagsByIds(List.of(id)); + + return experts.get(0); + } catch (ExpertException e) { + throw e; + } catch (Exception e) { + log.error("์ „๋ฌธ๊ฐ€ ์ƒ์„ธ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + throw new ExpertException(ExpertErrorType.EXPERT_QUERY_ERROR); + } } @Override - public List findAllWithDetails() { + public List findAllWithCareersTagsCategories() { try { - return repository.findAllWithDetails(); + List ids = repository.findAllIds(); + return fetchWithCollections(ids); } catch (Exception e) { log.error("์ „๋ฌธ๊ฐ€ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", e); throw new ExpertException(ExpertErrorType.EXPERT_QUERY_ERROR); @@ -50,7 +65,18 @@ public List findAllWithDetails() { @Override public List findByAllCategories(Collection categories) { try { - return repository.findByAllCategories(categories, categories.size()); + 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); @@ -58,11 +84,17 @@ public List findByAllCategories(Collection categories) { } @Override - public Map findExpertMapByIds(Set expertIds) { - - List experts = repository.findAllWithDetailsByIds(expertIds); + public Map findByIds(Set expertIds) { + List experts = repository.findAllByIds(expertIds); return experts.stream() .collect(Collectors.toMap(Expert::getId, Function.identity())); } + + private List fetchWithCollections(List ids) { + List experts = repository.fetchExpertsWithCareersByIds(ids); + repository.fetchExpertsWithTagsByIds(ids); + repository.fetchExpertsWithCategoriesByIds(ids); + return experts; + } } diff --git a/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java b/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java index dfe059d9..c4de2cb9 100644 --- a/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java +++ b/src/main/java/starlight/adapter/expert/persistence/ExpertRepository.java @@ -1,6 +1,5 @@ package starlight.adapter.expert.persistence; -import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -9,18 +8,24 @@ import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.Set; public interface ExpertRepository extends JpaRepository { - @Query("select distinct e from Expert e") - @EntityGraph(attributePaths = {"categories", "careers", "tags"}) - List findAllWithDetails(); + @Query("select e.id from Expert e") + List findAllIds(); + + @Query("select distinct e from Expert e left join fetch e.careers where e.id in :ids") + List fetchExpertsWithCareersByIds(@Param("ids") List ids); + + @Query("select distinct e from Expert e left join fetch e.tags where e.id in :ids") + List fetchExpertsWithTagsByIds(@Param("ids") List ids); + + @Query("select distinct e from Expert e left join fetch e.categories where e.id in :ids") + List fetchExpertsWithCategoriesByIds(@Param("ids") List ids); @Query("select distinct e from Expert e where e.id in :expertIds") - @EntityGraph(attributePaths = {"categories", "careers", "tags"}) - List findAllWithDetailsByIds(Set expertIds); + List findAllByIds(Set expertIds); @Query(""" select distinct e from Expert e where e.id in ( @@ -31,16 +36,6 @@ select distinct e from Expert e where e.id in ( group by e2.id having count(distinct c2) = :size) """) - @EntityGraph(attributePaths = {"categories", "careers", "tags"}) List findByAllCategories(@Param("cats") Collection cats, @Param("size") long size); - - @Query(""" - select e from Expert e - left join fetch e.categories - left join fetch e.careers - left join fetch e.tags - where e.id = :id - """) - Optional findByIdWithDetails(@Param("id") Long id); } diff --git a/src/main/java/starlight/adapter/expert/webapi/ExpertController.java b/src/main/java/starlight/adapter/expert/webapi/ExpertController.java index 4b5cf662..95748612 100644 --- a/src/main/java/starlight/adapter/expert/webapi/ExpertController.java +++ b/src/main/java/starlight/adapter/expert/webapi/ExpertController.java @@ -2,13 +2,14 @@ import lombok.RequiredArgsConstructor; 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.ExpertFinder; -import starlight.domain.expert.entity.Expert; +import starlight.application.expert.provided.ExpertDetailQueryUseCase; import starlight.domain.expert.enumerate.TagCategory; import starlight.shared.apiPayload.response.ApiResponse; @@ -20,16 +21,19 @@ @RequestMapping("/v1/experts") public class ExpertController implements ExpertQueryApiDoc { - private final ExpertFinder expertFinder; + private final ExpertDetailQueryUseCase expertDetailQuery; @GetMapping - public ApiResponse> search( + public ApiResponse> search( @RequestParam(name = "categories", required = false) Set categories ) { - List experts = (categories == null || categories.isEmpty()) - ? expertFinder.loadAll() - : expertFinder.findByAllCategories(categories); + return ApiResponse.success(ExpertListResponse.fromAll(expertDetailQuery.search(categories))); + } - return ApiResponse.success(ExpertDetailResponse.fromAll(experts)); + @GetMapping("/{expertId}") + public ApiResponse detail( + @PathVariable Long expertId + ) { + return ApiResponse.success(ExpertDetailResponse.from(expertDetailQuery.findById(expertId))); } } diff --git a/src/main/java/starlight/adapter/expert/webapi/dto/ExpertCareerResponse.java b/src/main/java/starlight/adapter/expert/webapi/dto/ExpertCareerResponse.java new file mode 100644 index 00000000..68631303 --- /dev/null +++ b/src/main/java/starlight/adapter/expert/webapi/dto/ExpertCareerResponse.java @@ -0,0 +1,30 @@ +package starlight.adapter.expert.webapi.dto; + +import starlight.application.expert.provided.dto.ExpertCareerResult; + +import java.time.LocalDateTime; + +public record ExpertCareerResponse( + Long id, + + Integer orderIndex, + + String careerTitle, + + String careerExplanation, + + LocalDateTime careerStartedAt, + + LocalDateTime careerEndedAt +) { + public static ExpertCareerResponse from(ExpertCareerResult result) { + return new ExpertCareerResponse( + result.id(), + result.orderIndex(), + result.careerTitle(), + result.careerExplanation(), + result.careerStartedAt(), + result.careerEndedAt() + ); + } +} diff --git a/src/main/java/starlight/adapter/expert/webapi/dto/ExpertDetailResponse.java b/src/main/java/starlight/adapter/expert/webapi/dto/ExpertDetailResponse.java index f02ce085..e1b956c3 100644 --- a/src/main/java/starlight/adapter/expert/webapi/dto/ExpertDetailResponse.java +++ b/src/main/java/starlight/adapter/expert/webapi/dto/ExpertDetailResponse.java @@ -1,17 +1,20 @@ package starlight.adapter.expert.webapi.dto; -import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; - -import java.util.Collection; +import starlight.application.expert.provided.dto.ExpertDetailResult; import java.util.List; public record ExpertDetailResponse( Long id, + Long applicationCount, + String name, + String oneLineIntroduction, + + String detailedIntroduction, + String profileImageUrl, Long workedPeriod, @@ -20,32 +23,33 @@ public record ExpertDetailResponse( Integer mentoringPriceWon, - List careers, - - List tags, + List careers, - List categories + List tags ) { - public static ExpertDetailResponse from(Expert expert) { - List categories = expert.getCategories().stream() - .map(TagCategory::name) - .distinct() + public static ExpertDetailResponse from(ExpertDetailResult result) { + List careers = result.careers().stream() + .map(ExpertCareerResponse::from) .toList(); return new ExpertDetailResponse( - expert.getId(), - expert.getName(), - expert.getProfileImageUrl(), - expert.getWorkedPeriod(), - expert.getEmail(), - expert.getMentoringPriceWon(), - expert.getCareers(), - expert.getTags().stream().distinct().toList(), - categories + result.id(), + result.applicationCount(), + result.name(), + result.oneLineIntroduction(), + result.detailedIntroduction(), + result.profileImageUrl(), + result.workedPeriod(), + result.email(), + result.mentoringPriceWon(), + careers, + result.tags() ); } - public static List fromAll(Collection experts){ - return experts.stream().map(ExpertDetailResponse::from).toList(); + public static List fromAllResults(List results) { + return results.stream() + .map(ExpertDetailResponse::from) + .toList(); } } diff --git a/src/main/java/starlight/adapter/expert/webapi/dto/ExpertListResponse.java b/src/main/java/starlight/adapter/expert/webapi/dto/ExpertListResponse.java new file mode 100644 index 00000000..f654c14e --- /dev/null +++ b/src/main/java/starlight/adapter/expert/webapi/dto/ExpertListResponse.java @@ -0,0 +1,57 @@ +package starlight.adapter.expert.webapi.dto; + +import starlight.application.expert.provided.dto.ExpertCareerResult; +import starlight.application.expert.provided.dto.ExpertDetailResult; + +import java.util.List; + +public record ExpertListResponse( + Long id, + String name, + String oneLineIntroduction, + String profileImageUrl, + Long workedPeriod, + String email, + List careers, + List tags, + List categories +) { + private static final int MAX_CAREERS = 3; + + public static ExpertListResponse from(ExpertDetailResult result) { + List careers = result.careers().stream() + .limit(MAX_CAREERS) + .map(ExpertCareerSummaryResponse::from) + .toList(); + + return new ExpertListResponse( + result.id(), + result.name(), + result.oneLineIntroduction(), + result.profileImageUrl(), + result.workedPeriod(), + result.email(), + careers, + result.tags(), + result.categories() + ); + } + + public static List fromAll(List results) { + return results.stream() + .map(ExpertListResponse::from) + .toList(); + } + + public record ExpertCareerSummaryResponse( + Integer orderIndex, + String careerTitle + ) { + public static ExpertCareerSummaryResponse from(ExpertCareerResult result) { + return new ExpertCareerSummaryResponse( + result.orderIndex(), + result.careerTitle() + ); + } + } +} 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 99187fc4..c578be96 100644 --- a/src/main/java/starlight/adapter/expert/webapi/swagger/ExpertQueryApiDoc.java +++ b/src/main/java/starlight/adapter/expert/webapi/swagger/ExpertQueryApiDoc.java @@ -8,8 +8,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; 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; @@ -36,7 +38,7 @@ public interface ExpertQueryApiDoc { description = "์„ฑ๊ณต", content = @Content( mediaType = "application/json", - array = @ArraySchema(schema = @Schema(implementation = ExpertDetailResponse.class)), + array = @ArraySchema(schema = @Schema(implementation = ExpertListResponse.class)), examples = @ExampleObject( name = "์„ฑ๊ณต ์˜ˆ์‹œ", value = """ @@ -46,19 +48,28 @@ public interface ExpertQueryApiDoc { { "id": 1, "name": "ํ™๊ธธ๋™", + "oneLineIntroduction": "ํ•œ ์ค„ ์†Œ๊ฐœ", "profileImageUrl": "https://cdn.example.com/profiles/1.png", + "workedPeriod": 6, "email": "hong@example.com", - "mentoringPriceWon": 50000, - "careers": ["A์‚ฌ PO (2019-2022)","B์Šคํƒ€ํŠธ์—… PM (2023-)"], + "careers": [ + { "orderIndex": 0, "careerTitle": "A์‚ฌ PO (2019-2022)" }, + { "orderIndex": 1, "careerTitle": "B์Šคํƒ€ํŠธ์—… PM (2023-)" } + ], + "tags": ["B2B", "SaaS", "PM"], "categories": ["์„ฑ์žฅ ์ „๋žต","ํŒ€ ์—ญ๋Ÿ‰"] }, { "id": 2, "name": "์ด์˜ํฌ", + "oneLineIntroduction": "ํ•œ ์ค„ ์†Œ๊ฐœ", "profileImageUrl": "https://cdn.example.com/profiles/2.png", + "workedPeriod": 4, "email": "lee@example.com", - "mentoringPriceWon": 70000, - "careers": ["C๊ธฐ์—… ๋ฐ์ดํ„ฐ๋ถ„์„ (2020-)"], + "careers": [ + { "orderIndex": 0, "careerTitle": "C๊ธฐ์—… ๋ฐ์ดํ„ฐ๋ถ„์„ (2020-)" } + ], + "tags": ["๋ฐ์ดํ„ฐ", "๋ถ„์„"], "categories": ["์‹œ์žฅ์„ฑ/BM","์ง€ํ‘œ/๋ฐ์ดํ„ฐ"] } ], @@ -70,8 +81,24 @@ public interface ExpertQueryApiDoc { ), }) @GetMapping - ApiResponse> search( + ApiResponse> search( @RequestParam(name = "categories", required = false) Set categories ); + + @Operation(summary = "์ „๋ฌธ๊ฐ€ ์ƒ์„ธ ์กฐํšŒ") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "์„ฑ๊ณต", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ExpertDetailResponse.class) + ) + ), + }) + @GetMapping("/{expertId}") + ApiResponse detail( + @PathVariable Long expertId + ); } diff --git a/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java b/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java index 5b57ab7c..59aa3650 100644 --- a/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java +++ b/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationJpa.java @@ -3,17 +3,21 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import starlight.application.expert.required.ExpertApplicationLookupPort; import starlight.application.expertApplication.required.ExpertApplicationQuery; import starlight.domain.expertApplication.entity.ExpertApplication; import starlight.domain.expertApplication.exception.ExpertApplicationErrorType; import starlight.domain.expertApplication.exception.ExpertApplicationException; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Slf4j @Component @RequiredArgsConstructor -public class ExpertApplicationJpa implements ExpertApplicationQuery { +public class ExpertApplicationJpa implements ExpertApplicationQuery, ExpertApplicationLookupPort { private final ExpertApplicationRepository repository; @@ -41,4 +45,22 @@ public List findRequestedExpertIds(Long businessPlanId) { public ExpertApplication save(ExpertApplication application) { return repository.save(application); } + + @Override + public Map countByExpertIds(List expertIds) { + try { + if (expertIds == null || expertIds.isEmpty()) { + return Collections.emptyMap(); + } + + return repository.countByExpertIds(expertIds).stream() + .collect(Collectors.toMap( + ExpertApplicationRepository.ExpertIdCountProjection::getExpertId, + p -> (long) p.getCount() + )); + } catch (Exception e) { + log.error("์ „๋ฌธ๊ฐ€๋ณ„ ์‹ ์ฒญ ๊ฑด์ˆ˜ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", e); + throw new ExpertApplicationException(ExpertApplicationErrorType.EXPERT_APPLICATION_QUERY_ERROR); + } + } } diff --git a/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationRepository.java b/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationRepository.java index a912bd5b..a25d8f61 100644 --- a/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationRepository.java +++ b/src/main/java/starlight/adapter/expertApplication/persistence/ExpertApplicationRepository.java @@ -17,4 +17,17 @@ public interface ExpertApplicationRepository extends JpaRepository findRequestedExpertIdsByPlanId(@Param("businessPlanId") Long businessPlanId); + + interface ExpertIdCountProjection { + Long getExpertId(); + long getCount(); + } + + @Query(""" + select e.expertId as expertId, count(e) as count + from ExpertApplication e + where e.expertId in :expertIds + group by e.expertId + """) + List countByExpertIds(@Param("expertIds") List expertIds); } diff --git a/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java b/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java index 7d46e333..ec5b1843 100644 --- a/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java +++ b/src/main/java/starlight/adapter/expertApplication/webapi/ExpertApplicationController.java @@ -8,8 +8,8 @@ import org.springframework.web.multipart.MultipartFile; import starlight.adapter.auth.security.auth.AuthDetails; import starlight.adapter.expertApplication.webapi.swagger.ExpertApplicationApiDoc; -import starlight.application.expertApplication.provided.ExpertApplicationService; -import starlight.application.expertApplication.required.ExpertApplicationQuery; +import starlight.application.expertApplication.provided.ExpertApplicationQueryUseCase; +import starlight.application.expertApplication.provided.ExpertApplicationServiceUseCase; import starlight.shared.apiPayload.response.ApiResponse; import java.util.List; @@ -20,14 +20,14 @@ @RequestMapping("/v1/expert-applications") public class ExpertApplicationController implements ExpertApplicationApiDoc { - private final ExpertApplicationQuery finder; - private final ExpertApplicationService expertApplicationService; + private final ExpertApplicationQueryUseCase queryUseCase; + private final ExpertApplicationServiceUseCase applicationServiceUseCase; @GetMapping public ApiResponse> search( @RequestParam Long businessPlanId ) { - return ApiResponse.success(finder.findRequestedExpertIds(businessPlanId)); + return ApiResponse.success(queryUseCase.findRequestedExpertIds(businessPlanId)); } @PostMapping(value = "/{expertId}/request", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @@ -37,7 +37,7 @@ public ApiResponse requestFeedback( @RequestParam("file") MultipartFile file, @AuthenticationPrincipal AuthDetails auth ) throws Exception { - expertApplicationService.requestFeedback(expertId, businessPlanId, file, auth.getUser().getName()); + applicationServiceUseCase.requestFeedback(expertId, businessPlanId, file, auth.getUser().getName()); return ApiResponse.success("ํ”ผ๋“œ๋ฐฑ ์š”์ฒญ์ด ์ „๋‹ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } } diff --git a/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java b/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java index 5b62567c..a593b39e 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/ExpertReportController.java @@ -8,7 +8,7 @@ import starlight.adapter.expertReport.webapi.dto.ExpertReportResponse; import starlight.adapter.expertReport.webapi.dto.UpsertExpertReportRequest; import starlight.adapter.expertReport.webapi.mapper.ExpertReportMapper; -import starlight.application.expertReport.provided.ExpertReportService; +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; @@ -23,7 +23,7 @@ public class ExpertReportController { private final ExpertReportMapper mapper; - private final ExpertReportService expertReportService; + private final ExpertReportServiceUseCase expertReportService; @Operation(summary = "์ „๋ฌธ๊ฐ€ ๋ฆฌํฌํŠธ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. (์‚ฌ์šฉ์ž ์‚ฌ์šฉ)") @GetMapping @@ -75,4 +75,4 @@ public ApiResponse save( return ApiResponse.success(ExpertReportResponse.from(report)); } -} \ 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 d53d86c8..069b5120 100644 --- a/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportResponse.java +++ b/src/main/java/starlight/adapter/expertReport/webapi/dto/ExpertReportResponse.java @@ -1,6 +1,7 @@ package starlight.adapter.expertReport.webapi.dto; import starlight.adapter.expert.webapi.dto.ExpertDetailResponse; +import starlight.application.expert.provided.dto.ExpertDetailResult; import starlight.domain.expert.entity.Expert; import starlight.domain.expertReport.entity.ExpertReport; import starlight.domain.expertReport.enumerate.SubmitStatus; @@ -20,7 +21,7 @@ public record ExpertReportResponse( ) { public static ExpertReportResponse fromEntities(ExpertReport report, Expert expert) { return new ExpertReportResponse( - ExpertDetailResponse.from(expert), + ExpertDetailResponse.from(ExpertDetailResult.from(expert, 0L)), report.getSubmitStatus(), report.canEdit(), report.getOverallComment(), @@ -41,4 +42,4 @@ public static ExpertReportResponse from(ExpertReport report) { .toList() ); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java b/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java index f4cf0f5d..77f14435 100644 --- a/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java +++ b/src/main/java/starlight/adapter/order/persistence/OrderRepositoryJpa.java @@ -3,7 +3,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import starlight.application.order.provided.OrdersQuery; -import starlight.domain.expertReport.entity.ExpertReport; import starlight.domain.order.exception.OrderErrorType; import starlight.domain.order.exception.OrderException; import starlight.domain.order.order.Orders; diff --git a/src/main/java/starlight/adapter/order/webapi/OrderController.java b/src/main/java/starlight/adapter/order/webapi/OrderController.java index 8b35a7d2..3e70cd1d 100644 --- a/src/main/java/starlight/adapter/order/webapi/OrderController.java +++ b/src/main/java/starlight/adapter/order/webapi/OrderController.java @@ -15,7 +15,6 @@ import starlight.adapter.order.webapi.dto.response.OrderPrepareResponse; import starlight.application.order.provided.OrderPaymentService; import starlight.application.order.provided.dto.PaymentHistoryItemDto; -import starlight.application.usage.provided.UsageCreditPort; import starlight.domain.order.order.Orders; import starlight.shared.apiPayload.response.ApiResponse; @@ -28,7 +27,6 @@ public class OrderController { private final OrderPaymentService orderPaymentService; - private final UsageCreditPort usageCreditPort; @PostMapping("/request") public ApiResponse prepareOrder( diff --git a/src/main/java/starlight/application/expert/ExpertDetailQueryService.java b/src/main/java/starlight/application/expert/ExpertDetailQueryService.java new file mode 100644 index 00000000..3e31c3a2 --- /dev/null +++ b/src/main/java/starlight/application/expert/ExpertDetailQueryService.java @@ -0,0 +1,49 @@ +package starlight.application.expert; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import starlight.application.expert.provided.ExpertDetailQueryUseCase; +import starlight.application.expert.provided.dto.ExpertDetailResult; +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 +@Transactional(readOnly = true) +public class ExpertDetailQueryService implements ExpertDetailQueryUseCase { + + private final ExpertQueryPort expertQueryPort; + private final ExpertApplicationLookupPort expertApplicationLookupPort; + + @Override + public List search(Set categories) { + List experts = (categories == null || categories.isEmpty()) + ? expertQueryPort.findAllWithCareersTagsCategories() + : expertQueryPort.findByAllCategories(categories); + + List expertIds = experts.stream() + .map(Expert::getId) + .toList(); + + Map countMap = expertApplicationLookupPort.countByExpertIds(expertIds); + + return experts.stream() + .map(expert -> ExpertDetailResult.from(expert, countMap.getOrDefault(expert.getId(), 0L))) + .toList(); + } + + @Override + public ExpertDetailResult findById(Long expertId) { + Expert expert = expertQueryPort.findByIdWithCareersAndTags(expertId); + Map countMap = expertApplicationLookupPort.countByExpertIds(List.of(expertId)); + long count = countMap.getOrDefault(expertId, 0L); + return ExpertDetailResult.from(expert, count); + } +} diff --git a/src/main/java/starlight/application/expert/ExpertQueryService.java b/src/main/java/starlight/application/expert/ExpertQueryService.java deleted file mode 100644 index c21c258e..00000000 --- a/src/main/java/starlight/application/expert/ExpertQueryService.java +++ /dev/null @@ -1,47 +0,0 @@ -package starlight.application.expert; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import starlight.application.expert.provided.ExpertFinder; -import starlight.application.expert.required.ExpertQuery; -import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ExpertQueryService implements ExpertFinder { - - private final ExpertQuery expertQuery; - - @Override - public Expert findById(Long id) { - return expertQuery.findById(id); - } - - @Override - public Expert findByIdWithDetails(Long id) { - return expertQuery.findByIdWithDetails(id); - } - - @Override - public List loadAll() { - return expertQuery.findAllWithDetails(); - } - - @Override - public List findByAllCategories(Collection categories) { - return expertQuery.findByAllCategories(categories); - } - - @Override - public Map findByIds(Set expertIds) { - return expertQuery.findExpertMapByIds(expertIds); - } -} diff --git a/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java b/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java new file mode 100644 index 00000000..5e6ccb98 --- /dev/null +++ b/src/main/java/starlight/application/expert/provided/ExpertDetailQueryUseCase.java @@ -0,0 +1,14 @@ +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); + + ExpertDetailResult findById(Long expertId); +} diff --git a/src/main/java/starlight/application/expert/provided/ExpertFinder.java b/src/main/java/starlight/application/expert/provided/ExpertFinder.java deleted file mode 100644 index 5fa1a25a..00000000 --- a/src/main/java/starlight/application/expert/provided/ExpertFinder.java +++ /dev/null @@ -1,21 +0,0 @@ -package starlight.application.expert.provided; - -import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public interface ExpertFinder { - Expert findById(Long id); - - Expert findByIdWithDetails(Long id); - - List loadAll(); - - List findByAllCategories(Collection categories); - - Map findByIds(Set expertIds); -} diff --git a/src/main/java/starlight/application/expert/provided/dto/ExpertCareerResult.java b/src/main/java/starlight/application/expert/provided/dto/ExpertCareerResult.java new file mode 100644 index 00000000..839e8454 --- /dev/null +++ b/src/main/java/starlight/application/expert/provided/dto/ExpertCareerResult.java @@ -0,0 +1,25 @@ +package starlight.application.expert.provided.dto; + +import starlight.domain.expert.entity.ExpertCareer; + +import java.time.LocalDateTime; + +public record ExpertCareerResult( + Long id, + Integer orderIndex, + String careerTitle, + String careerExplanation, + LocalDateTime careerStartedAt, + LocalDateTime careerEndedAt +) { + public static ExpertCareerResult from(ExpertCareer expertCareer) { + return new ExpertCareerResult( + expertCareer.getId(), + expertCareer.getOrderIndex(), + expertCareer.getCareerTitle(), + expertCareer.getCareerExplanation(), + expertCareer.getCareerStartedAt(), + expertCareer.getCareerEndedAt() + ); + } +} diff --git a/src/main/java/starlight/application/expert/provided/dto/ExpertDetailResult.java b/src/main/java/starlight/application/expert/provided/dto/ExpertDetailResult.java new file mode 100644 index 00000000..0c0a8a38 --- /dev/null +++ b/src/main/java/starlight/application/expert/provided/dto/ExpertDetailResult.java @@ -0,0 +1,51 @@ +package starlight.application.expert.provided.dto; + +import starlight.domain.expert.entity.Expert; +import starlight.domain.expert.enumerate.TagCategory; + +import java.util.List; + +public record ExpertDetailResult( + Long id, + Long applicationCount, + String name, + String oneLineIntroduction, + String detailedIntroduction, + String profileImageUrl, + Long workedPeriod, + String email, + Integer mentoringPriceWon, + List careers, + List tags, + List categories +) { + public static ExpertDetailResult from(Expert expert, long applicationCount) { + List careers = expert.getCareers().stream() + .map(ExpertCareerResult::from) + .toList(); + + List categories = expert.getCategories().stream() + .map(TagCategory::name) + .distinct() + .toList(); + + List tags = expert.getTags().stream() + .distinct() + .toList(); + + return new ExpertDetailResult( + expert.getId(), + applicationCount, + expert.getName(), + expert.getOneLineIntroduction(), + expert.getDetailedIntroduction(), + expert.getProfileImageUrl(), + expert.getWorkedPeriod(), + expert.getEmail(), + expert.getMentoringPriceWon(), + careers, + tags, + categories + ); + } +} diff --git a/src/main/java/starlight/application/expert/required/ExpertApplicationLookupPort.java b/src/main/java/starlight/application/expert/required/ExpertApplicationLookupPort.java new file mode 100644 index 00000000..3bf3f858 --- /dev/null +++ b/src/main/java/starlight/application/expert/required/ExpertApplicationLookupPort.java @@ -0,0 +1,9 @@ +package starlight.application.expert.required; + +import java.util.List; +import java.util.Map; + +public interface ExpertApplicationLookupPort { + + Map countByExpertIds(List expertIds); +} diff --git a/src/main/java/starlight/application/expert/required/ExpertQuery.java b/src/main/java/starlight/application/expert/required/ExpertQueryPort.java similarity index 52% rename from src/main/java/starlight/application/expert/required/ExpertQuery.java rename to src/main/java/starlight/application/expert/required/ExpertQueryPort.java index 925dcd7a..c1b1d922 100644 --- a/src/main/java/starlight/application/expert/required/ExpertQuery.java +++ b/src/main/java/starlight/application/expert/required/ExpertQueryPort.java @@ -5,18 +5,12 @@ import java.util.Collection; import java.util.List; -import java.util.Map; -import java.util.Set; -public interface ExpertQuery { +public interface ExpertQueryPort { - Expert findById(Long id); + Expert findByIdWithCareersAndTags(Long id); - Expert findByIdWithDetails(Long id); - - Map findExpertMapByIds(Set expertIds); - - List findAllWithDetails(); + List findAllWithCareersTagsCategories(); List findByAllCategories(Collection categories); } diff --git a/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java b/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java new file mode 100644 index 00000000..5c7765c1 --- /dev/null +++ b/src/main/java/starlight/application/expertApplication/ExpertApplicationQueryService.java @@ -0,0 +1,22 @@ +package starlight.application.expertApplication; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import starlight.application.expertApplication.provided.ExpertApplicationQueryUseCase; +import starlight.application.expertApplication.required.ExpertApplicationQuery; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ExpertApplicationQueryService implements ExpertApplicationQueryUseCase { + + private final ExpertApplicationQuery expertApplicationQuery; + + @Override + public List findRequestedExpertIds(Long businessPlanId) { + return expertApplicationQuery.findRequestedExpertIds(businessPlanId); + } +} diff --git a/src/main/java/starlight/application/expertApplication/ExpertApplicationServiceImpl.java b/src/main/java/starlight/application/expertApplication/ExpertApplicationService.java similarity index 93% rename from src/main/java/starlight/application/expertApplication/ExpertApplicationServiceImpl.java rename to src/main/java/starlight/application/expertApplication/ExpertApplicationService.java index 2f6e3e6e..5c8eef07 100644 --- a/src/main/java/starlight/application/expertApplication/ExpertApplicationServiceImpl.java +++ b/src/main/java/starlight/application/expertApplication/ExpertApplicationService.java @@ -8,11 +8,11 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import starlight.application.businessplan.required.BusinessPlanQuery; -import starlight.application.expert.required.ExpertQuery; import starlight.application.expertApplication.event.FeedbackRequestDto; -import starlight.application.expertApplication.provided.ExpertApplicationService; +import starlight.application.expertApplication.provided.ExpertApplicationServiceUseCase; +import starlight.application.expertApplication.required.ExpertLookupPort; import starlight.application.expertApplication.required.ExpertApplicationQuery; -import starlight.application.expertReport.provided.ExpertReportService; +import starlight.application.expertReport.provided.ExpertReportServiceUseCase; import starlight.domain.businessplan.entity.BusinessPlan; import starlight.domain.businessplan.enumerate.PlanStatus; import starlight.domain.expert.entity.Expert; @@ -27,13 +27,13 @@ @Slf4j @Service @RequiredArgsConstructor -public class ExpertApplicationServiceImpl implements ExpertApplicationService { +public class ExpertApplicationService implements ExpertApplicationServiceUseCase { - private final ExpertQuery expertQuery; + private final ExpertLookupPort expertLookupPort; private final BusinessPlanQuery planQuery; private final ExpertApplicationQuery applicationQuery; private final ApplicationEventPublisher eventPublisher; - private final ExpertReportService expertReportService; + private final ExpertReportServiceUseCase expertReportService; private static final long MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB private static final String ALLOWED_CONTENT_TYPE = "application/pdf"; @@ -48,7 +48,7 @@ public void requestFeedback(Long expertId, Long planId, MultipartFile file, Stri validateFile(file); BusinessPlan plan = planQuery.getOrThrow(planId); - Expert expert = expertQuery.findById(expertId); + Expert expert = expertLookupPort.findById(expertId); plan.updateStatus(PlanStatus.EXPERT_MATCHED); diff --git a/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationQueryUseCase.java b/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationQueryUseCase.java new file mode 100644 index 00000000..0366b856 --- /dev/null +++ b/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationQueryUseCase.java @@ -0,0 +1,8 @@ +package starlight.application.expertApplication.provided; + +import java.util.List; + +public interface ExpertApplicationQueryUseCase { + + List findRequestedExpertIds(Long businessPlanId); +} diff --git a/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationService.java b/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationServiceUseCase.java similarity index 83% rename from src/main/java/starlight/application/expertApplication/provided/ExpertApplicationService.java rename to src/main/java/starlight/application/expertApplication/provided/ExpertApplicationServiceUseCase.java index 17e63974..46e7cdbf 100644 --- a/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationService.java +++ b/src/main/java/starlight/application/expertApplication/provided/ExpertApplicationServiceUseCase.java @@ -4,7 +4,7 @@ import java.io.IOException; -public interface ExpertApplicationService { +public interface ExpertApplicationServiceUseCase { void requestFeedback(Long expertId, Long planId, MultipartFile file, String menteeName) throws IOException; } diff --git a/src/main/java/starlight/application/expertApplication/required/ExpertLookupPort.java b/src/main/java/starlight/application/expertApplication/required/ExpertLookupPort.java new file mode 100644 index 00000000..d0e1d74a --- /dev/null +++ b/src/main/java/starlight/application/expertApplication/required/ExpertLookupPort.java @@ -0,0 +1,8 @@ +package starlight.application.expertApplication.required; + +import starlight.domain.expert.entity.Expert; + +public interface ExpertLookupPort { + + Expert findById(Long id); +} diff --git a/src/main/java/starlight/application/expertReport/ExpertReportServiceImpl.java b/src/main/java/starlight/application/expertReport/ExpertReportService.java similarity index 91% rename from src/main/java/starlight/application/expertReport/ExpertReportServiceImpl.java rename to src/main/java/starlight/application/expertReport/ExpertReportService.java index 44295a5d..2511bc49 100644 --- a/src/main/java/starlight/application/expertReport/ExpertReportServiceImpl.java +++ b/src/main/java/starlight/application/expertReport/ExpertReportService.java @@ -6,9 +6,9 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import starlight.application.businessplan.required.BusinessPlanQuery; -import starlight.application.expert.provided.ExpertFinder; -import starlight.application.expertReport.provided.ExpertReportService; +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.domain.businessplan.entity.BusinessPlan; import starlight.domain.businessplan.enumerate.PlanStatus; @@ -26,7 +26,7 @@ @Service @RequiredArgsConstructor @Transactional -public class ExpertReportServiceImpl implements ExpertReportService { +public class ExpertReportService implements ExpertReportServiceUseCase { @Value("${feedback-token.token-length}") private int tokenLength; @@ -38,7 +38,7 @@ public class ExpertReportServiceImpl implements ExpertReportService { private String feedbackBaseUrl; private final ExpertReportQuery expertReportQuery; - private final ExpertFinder expertFinder; + private final ExpertLookupPort expertLookupPort; private final BusinessPlanQuery businessPlanQuery; private final SecureRandom secureRandom = new SecureRandom(); @@ -87,7 +87,7 @@ public ExpertReportWithExpertDto getExpertReportWithExpert(String token) { ExpertReport report = expertReportQuery.findByTokenWithDetails(token); report.incrementViewCount(); - Expert expert = expertFinder.findByIdWithDetails(report.getExpertId()); + Expert expert = expertLookupPort.findByIdWithCareersAndTags(report.getExpertId()); return ExpertReportWithExpertDto.of(report, expert); } @@ -101,7 +101,7 @@ public List getExpertReportsWithExpertByBusinessPlanI .map(ExpertReport::getExpertId) .collect(Collectors.toSet()); - Map expertsMap = expertFinder.findByIds(expertIds); + Map expertsMap = expertLookupPort.findByIds(expertIds); return reports.stream() .map(report -> { @@ -125,4 +125,4 @@ private String generateToken() { return token.toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/starlight/application/expertReport/provided/ExpertReportService.java b/src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java similarity index 93% rename from src/main/java/starlight/application/expertReport/provided/ExpertReportService.java rename to src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java index 1a6242e4..8afc0108 100644 --- a/src/main/java/starlight/application/expertReport/provided/ExpertReportService.java +++ b/src/main/java/starlight/application/expertReport/provided/ExpertReportServiceUseCase.java @@ -7,7 +7,7 @@ import java.util.List; -public interface ExpertReportService{ +public interface ExpertReportServiceUseCase{ String createExpertReportLink(Long expertId, Long businessPlanId); @@ -16,4 +16,4 @@ public interface ExpertReportService{ ExpertReportWithExpertDto getExpertReportWithExpert(String token); List getExpertReportsWithExpertByBusinessPlanId(Long businessPlanId); -} \ No newline at end of file +} diff --git a/src/main/java/starlight/application/expertReport/required/ExpertLookupPort.java b/src/main/java/starlight/application/expertReport/required/ExpertLookupPort.java new file mode 100644 index 00000000..fef360ac --- /dev/null +++ b/src/main/java/starlight/application/expertReport/required/ExpertLookupPort.java @@ -0,0 +1,13 @@ +package starlight.application.expertReport.required; + +import starlight.domain.expert.entity.Expert; + +import java.util.Map; +import java.util.Set; + +public interface ExpertLookupPort { + + Expert findByIdWithCareersAndTags(Long id); + + Map findByIds(Set expertIds); +} diff --git a/src/main/java/starlight/bootstrap/ObjectStorageConfig.java b/src/main/java/starlight/bootstrap/ObjectStorageConfig.java index 01fcaabd..f587fe84 100644 --- a/src/main/java/starlight/bootstrap/ObjectStorageConfig.java +++ b/src/main/java/starlight/bootstrap/ObjectStorageConfig.java @@ -7,7 +7,6 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; -import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import java.net.URI; diff --git a/src/main/java/starlight/bootstrap/SecurityConfig.java b/src/main/java/starlight/bootstrap/SecurityConfig.java index bd76996f..7a88438a 100644 --- a/src/main/java/starlight/bootstrap/SecurityConfig.java +++ b/src/main/java/starlight/bootstrap/SecurityConfig.java @@ -71,7 +71,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/actuator/health").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/", "/index.html", "/ops.html", "/payment.html", "/api/payment/**").permitAll() - .requestMatchers("/v1/auth/**","/v1/user/**", "/v1/experts").permitAll() + .requestMatchers("/v1/auth/**","/v1/user/**", "/v1/experts", "/v1/experts/*").permitAll() .requestMatchers("/login/**", "/oauth2/**", "/login/oauth2/**", "/public/**").permitAll() .requestMatchers("/v3/api-docs/**", "/v1/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**").permitAll() diff --git a/src/main/java/starlight/bootstrap/SwaggerConfig.java b/src/main/java/starlight/bootstrap/SwaggerConfig.java index 366f0f7d..01551e76 100644 --- a/src/main/java/starlight/bootstrap/SwaggerConfig.java +++ b/src/main/java/starlight/bootstrap/SwaggerConfig.java @@ -7,7 +7,6 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/starlight/domain/expert/entity/Expert.java b/src/main/java/starlight/domain/expert/entity/Expert.java index da0c009d..f1ce0af0 100644 --- a/src/main/java/starlight/domain/expert/entity/Expert.java +++ b/src/main/java/starlight/domain/expert/entity/Expert.java @@ -30,15 +30,19 @@ public class Expert extends AbstractEntity { @Column(nullable = false, length = 320) private String email; + @Column + private String oneLineIntroduction; + + @Column + private String detailedIntroduction; + @Min(0) @Column private Integer mentoringPriceWon; - @ElementCollection - @CollectionTable(name = "expert_careers", joinColumns = @JoinColumn(name = "expert_id")) - @Column(name = "career_text", length = 300, nullable = false) - @OrderColumn(name = "order_index") - private List careers = new ArrayList<>(); + @OneToMany(mappedBy = "expert", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("orderIndex ASC") + private List careers = new ArrayList<>(); @ElementCollection @CollectionTable(name = "expert_tags", joinColumns = @JoinColumn(name = "expert_id")) diff --git a/src/main/java/starlight/domain/expert/entity/ExpertCareer.java b/src/main/java/starlight/domain/expert/entity/ExpertCareer.java new file mode 100644 index 00000000..cd151cae --- /dev/null +++ b/src/main/java/starlight/domain/expert/entity/ExpertCareer.java @@ -0,0 +1,53 @@ +package starlight.domain.expert.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import starlight.shared.AbstractEntity; + +import java.time.LocalDateTime; + +@Getter +@Entity +@Table(name = "expert_careers") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ExpertCareer extends AbstractEntity { + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "expert_id", nullable = false) + private Expert expert; + + @Column(name="order_index", nullable=false) + private Integer orderIndex; + + @Column(name = "career_title", length = 300, nullable = false) + private String careerTitle; + + @Column(name = "career_explanation", length = 300) + private String careerExplanation; + + @Column(name = "career_started_at", nullable = false) + private LocalDateTime careerStartedAt; + + @Column(name = "career_ended_at", nullable = false) + private LocalDateTime careerEndedAt; + + public static ExpertCareer of(Expert expert, int orderIndex, String title, String explanation, LocalDateTime startedAt, LocalDateTime endedAt) { + ExpertCareer expertCareer = new ExpertCareer(); + expertCareer.expert = expert; + expertCareer.orderIndex = orderIndex; + expertCareer.careerTitle = title; + expertCareer.careerExplanation = explanation; + expertCareer.careerStartedAt = startedAt; + expertCareer.careerEndedAt = endedAt; + return expertCareer; + } + + public void update(String title, String explanation, LocalDateTime startedAt, LocalDateTime endedAt) { + this.careerTitle = title; + this.careerExplanation = explanation; + this.careerStartedAt = startedAt; + this.careerEndedAt = endedAt; + } +} diff --git a/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java b/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java index a74a759d..e4f58941 100644 --- a/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java +++ b/src/test/java/starlight/adapter/expert/persistence/ExpertRepositoryTest.java @@ -6,10 +6,12 @@ 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; @@ -53,8 +55,22 @@ private Expert expert(String name, Set cats) throws Exception { Expert e = ctor.newInstance(); ReflectionTestUtils.setField(e, "name", name); ReflectionTestUtils.setField(e, "email", name.toLowerCase() + "@example.com"); - ReflectionTestUtils.setField(e, "careers", List.of("career1", "career2")); + 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 ab3a903b..061c3884 100644 --- a/src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java +++ b/src/test/java/starlight/adapter/expert/webapi/ExpertControllerTest.java @@ -1,6 +1,5 @@ package starlight.adapter.expert.webapi; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -11,16 +10,14 @@ import org.springframework.context.annotation.FilterType; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import starlight.adapter.auth.security.filter.JwtFilter; -import starlight.application.expert.provided.ExpertFinder; -import starlight.domain.expert.entity.Expert; +import starlight.application.expert.provided.ExpertDetailQueryUseCase; +import starlight.application.expert.provided.dto.ExpertCareerResult; +import starlight.application.expert.provided.dto.ExpertDetailResult; import starlight.domain.expert.enumerate.TagCategory; -import java.lang.reflect.Constructor; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -40,31 +37,34 @@ class ExpertControllerTest { @Autowired MockMvc mockMvc; - @Autowired ObjectMapper om; - - @MockitoBean ExpertFinder expertFinder; - @MockitoBean JpaMetamodelMappingContext jpaMetamodelMappingContext; // โ† ํ•„๋“œ๋กœ ์ถ”๊ฐ€! + @MockitoBean + ExpertDetailQueryUseCase expertDetailQuery; + @MockitoBean JpaMetamodelMappingContext jpaMetamodelMappingContext; @Test @DisplayName("์นดํ…Œ๊ณ ๋ฆฌ ๋ฏธ์ „๋‹ฌ ์‹œ ์ „์ฒด ์กฐํšŒ") void listAll() throws Exception { - Expert e1 = expert(1L, "ํ™๊ธธ๋™", + ExpertDetailResult e1 = expertResult(1L, "ํ™๊ธธ๋™", Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY)); - when(expertFinder.loadAll()).thenReturn(List.of(e1)); + when(expertDetailQuery.search(null)).thenReturn(List.of(e1)); mockMvc.perform(get("/v1/experts")) .andExpect(status().isOk()) .andExpect(jsonPath("$.result").value("SUCCESS")) - .andExpect(jsonPath("$.data[0].name").value("ํ™๊ธธ๋™")); + .andExpect(jsonPath("$.data[0].name").value("ํ™๊ธธ๋™")) + .andExpect(jsonPath("$.data[0].careers.length()").value(3)) + .andExpect(jsonPath("$.data[0].careers[0].orderIndex").exists()) + .andExpect(jsonPath("$.data[0].careers[0].careerTitle").exists()) + .andExpect(jsonPath("$.data[0].applicationCount").doesNotExist()); } @Test @DisplayName("์นดํ…Œ๊ณ ๋ฆฌ AND ๋งค์นญ (?categories=A&categories=B)") void searchByAllCategories_multiParams() throws Exception { - Expert e1 = expert(2L, "์ด์˜ํฌ", + ExpertDetailResult e1 = expertResult(2L, "์ด์˜ํฌ", Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY)); - when(expertFinder.findByAllCategories(Set.of( + when(expertDetailQuery.search(Set.of( TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY ))).thenReturn(List.of(e1)); @@ -73,16 +73,17 @@ void searchByAllCategories_multiParams() throws Exception { .param("categories", "TEAM_CAPABILITY")) .andExpect(status().isOk()) .andExpect(jsonPath("$.result").value("SUCCESS")) - .andExpect(jsonPath("$.data[0].name").value("์ด์˜ํฌ")); + .andExpect(jsonPath("$.data[0].name").value("์ด์˜ํฌ")) + .andExpect(jsonPath("$.data[0].careers.length()").value(3)); } @Test @DisplayName("์นดํ…Œ๊ณ ๋ฆฌ AND ๋งค์นญ (์ฝค๋งˆ ๊ตฌ๋ถ„)") void searchByAllCategories_commaSeparated() throws Exception { - Expert e1 = expert(3L, "๋ฐ•์ฒ ์ˆ˜", + ExpertDetailResult e1 = expertResult(3L, "๋ฐ•์ฒ ์ˆ˜", Set.of(TagCategory.MARKET_BM, TagCategory.METRIC_DATA)); - when(expertFinder.findByAllCategories(Set.of( + when(expertDetailQuery.search(Set.of( TagCategory.MARKET_BM, TagCategory.METRIC_DATA ))).thenReturn(List.of(e1)); @@ -90,21 +91,48 @@ void searchByAllCategories_commaSeparated() throws Exception { .param("categories", "MARKET_BM,METRIC_DATA")) .andExpect(status().isOk()) .andExpect(jsonPath("$.result").value("SUCCESS")) - .andExpect(jsonPath("$.data[0].name").value("๋ฐ•์ฒ ์ˆ˜")); + .andExpect(jsonPath("$.data[0].name").value("๋ฐ•์ฒ ์ˆ˜")) + .andExpect(jsonPath("$.data[0].careers.length()").value(3)); + } + + @Test + @DisplayName("์ „๋ฌธ๊ฐ€ ์ƒ์„ธ ์กฐํšŒ") + void detail() throws Exception { + ExpertDetailResult result = expertResult(10L, "๊น€์ฒ ์ˆ˜", + Set.of(TagCategory.MARKET_BM, TagCategory.GROWTH_STRATEGY)); + when(expertDetailQuery.findById(10L)).thenReturn(result); + + mockMvc.perform(get("/v1/experts/10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.result").value("SUCCESS")) + .andExpect(jsonPath("$.data.id").value(10L)) + .andExpect(jsonPath("$.data.applicationCount").value(0)) + .andExpect(jsonPath("$.data.categories").doesNotExist()) + .andExpect(jsonPath("$.data.tags").isArray()); } // helper - private Expert expert(Long id, String name, Set cats) throws Exception { - Constructor ctor = Expert.class.getDeclaredConstructor(); - ctor.setAccessible(true); - Expert e = ctor.newInstance(); - ReflectionTestUtils.setField(e, "id", id); - ReflectionTestUtils.setField(e, "name", name); - ReflectionTestUtils.setField(e, "email", name + "@example.com"); - ReflectionTestUtils.setField(e, "profileImageUrl", "https://cdn.example.com/" + id + ".png"); - ReflectionTestUtils.setField(e, "mentoringPriceWon", 50000); - ReflectionTestUtils.setField(e, "careers", List.of("A์‚ฌ PO", "B์‚ฌ PM")); - ReflectionTestUtils.setField(e, "categories", new LinkedHashSet<>(cats)); - return e; + private ExpertDetailResult expertResult(Long id, String name, Set cats) throws Exception { + List careers = List.of( + new ExpertCareerResult(1L, 0, "A์‚ฌ PO", "์„ค๋ช…", null, null), + new ExpertCareerResult(2L, 1, "B์‚ฌ PM", "์„ค๋ช…", null, null), + new ExpertCareerResult(3L, 2, "C์‚ฌ ๋ฆฌ๋“œ", "์„ค๋ช…", null, null), + new ExpertCareerResult(4L, 3, "D์‚ฌ CTO", "์„ค๋ช…", null, null) + ); + + return new ExpertDetailResult( + id, + 0L, + name, + "ํ•œ์ค„์†Œ๊ฐœ", + "์ƒ์„ธ์†Œ๊ฐœ", + "https://cdn.example.com/" + id + ".png", + 12L, + name + "@example.com", + 50000, + careers, + List.of("tag1", "tag2"), + cats.stream().map(TagCategory::name).toList() + ); } -} \ No newline at end of file +} diff --git a/src/test/java/starlight/application/expert/ExpertQueryServiceTest.java b/src/test/java/starlight/application/expert/ExpertQueryServiceTest.java deleted file mode 100644 index b630f70f..00000000 --- a/src/test/java/starlight/application/expert/ExpertQueryServiceTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package starlight.application.expert; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.test.util.ReflectionTestUtils; -import starlight.application.expert.required.ExpertQuery; -import starlight.domain.expert.entity.Expert; -import starlight.domain.expert.enumerate.TagCategory; - -import java.lang.reflect.Constructor; -import java.util.List; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class ExpertQueryServiceTest { - - @Mock ExpertQuery expertQueryPort; - @InjectMocks ExpertQueryService sut; // System Under Test - - @Test - @DisplayName("์ „์ฒด ์กฐํšŒ๋Š” ํฌํŠธ์˜ findAllWithDetails๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค") - void loadAll() throws Exception { - when(expertQueryPort.findAllWithDetails()).thenReturn(List.of(expert(1L))); - - var result = sut.loadAll(); - - assertThat(result).hasSize(1); - verify(expertQueryPort, times(1)).findAllWithDetails(); - } - - @Test - @DisplayName("์นดํ…Œ๊ณ ๋ฆฌ AND ๋งค์นญ ์กฐํšŒ๋Š” ํฌํŠธ์˜ findByAllCategories๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค") - void findByAllCategories() throws Exception { - Set cats = Set.of(TagCategory.GROWTH_STRATEGY, TagCategory.TEAM_CAPABILITY); - when(expertQueryPort.findByAllCategories(cats)).thenReturn(List.of(expert(2L))); - - var result = sut.findByAllCategories(cats); - - assertThat(result).hasSize(1); - ArgumentCaptor> captor = ArgumentCaptor.forClass(Set.class); - verify(expertQueryPort, times(1)).findByAllCategories(captor.capture()); - assertThat(captor.getValue()).containsExactlyInAnyOrderElementsOf(cats); - } - - private Expert expert(Long id) throws Exception { - Constructor ctor = Expert.class.getDeclaredConstructor(); - ctor.setAccessible(true); - Expert e = ctor.newInstance(); - ReflectionTestUtils.setField(e, "id", id); - ReflectionTestUtils.setField(e, "name", "tester"); - ReflectionTestUtils.setField(e, "email", "t@example.com"); - return e; - } -} 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 e88e1454..ea412b97 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" @@ -25,7 +25,20 @@ - bootstrap ## ์œ ์˜์‚ฌํ•ญ -- ์–ด๋Œ‘ํ„ฐ๋Š” ํ•ญ์ƒ ํฌํŠธ(์ธํ„ฐํŽ˜์ด์Šค) ์—๋งŒ ์˜์กดํ•œ๋‹ค. +- ์–ด๋Œ‘ํ„ฐ๋Š” ํ•ญ์ƒ ํฌํŠธ(์ธํ„ฐํŽ˜์ด์Šค)์—๋งŒ ์˜์กดํ•œ๋‹ค. - ์–ด๋Œ‘ํ„ฐ โ†” ์–ด๋Œ‘ํ„ฐ ์ง์ ‘ ์˜์กด ๊ธˆ์ง€ (ํ•„์š”ํ•˜๋ฉด ์ƒˆ ํฌํŠธ๋ฅผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ •์˜). -- ๊ณตํ†ต ๋Šฅ๋ ฅ์€ ๊ณต์šฉ ํฌํŠธ 1๊ฐœ๋กœ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์—์„œ ์žฌ์‚ฌ์šฉํ•œ๋‹ค. +- Inbound `provided`๋Š” ํ•ด๋‹น ๋„๋ฉ”์ธ์˜ ์œ ์Šค์ผ€์ด์Šค๋งŒ ๋…ธ์ถœํ•œ๋‹ค. +- Outbound ํฌํŠธ๋Š” ์†Œ๋น„์ž ๋„๋ฉ”์ธ์—์„œ ์ •์˜ํ•œ๋‹ค(`application//required`). +- Cross-domain ์กฐํšŒ๋Š” `OtherDomainLookupPort` ๊ทœ์น™์„ ๋”ฐ๋ฅธ๋‹ค. +- Response DTO๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ DTO๋กœ๋งŒ ๋ณ€ํ™˜ํ•˜๊ณ  ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ง์ ‘ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. +## ๋„ค์ด๋ฐ ๊ทœ์น™ ์š”์•ฝ +- Provided (inbound): `*UseCase` +- Required (outbound): `*Port` +- Cross-domain lookup: `OtherDomainLookupPort` +- ์ปฌ๋ ‰์…˜์„ ํ•จ๊ป˜ ๋กœ๋”ฉํ•˜๋Š” ๊ฒฝ์šฐ ์ด๋ฆ„์— ์ปฌ๋ ‰์…˜์„ ๋ช…์‹œํ•œ๋‹ค. + - ์˜ˆ: `findAllWithCareersTagsCategories`, `findByIdWithCareersAndTags` + - ์˜ˆ: `fetchExpertsWithCareersByIds` + +## ๋กœ์ปฌ ์‹คํ–‰ +- `./gradlew bootRun --args='--spring.profiles.active=dev'`