Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/why-what-how.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## 🚀 Why - 해결하려는 문제가 무엇인가요?

- `어떤 문제를 해결하고자 하나요?`

- `어떤 배경이 있었나요?`

## ✅ What - 무엇이 변경됐나요?

- `구현한 기능 요약`

- `주요 변경사항`

## 🛠️ How - 어떻게 해결했나요?

- `핵심 로직 설명`

- `예외 사항, 고민 포인트 등`

## 🖼️ Attachment

- `화면 이미지, 결과 캡처 등 첨부`

## 💬 기타 코멘트

- `리뷰어에게 전하고 싶은 말, 테스트 방법, 주의할 점 등`
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ node_modules/
dist/
*.log
/docs/
/AGENTS.md
1 change: 0 additions & 1 deletion src/main/java/starlight/StarlightApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 44 additions & 12 deletions src/main/java/starlight/adapter/expert/persistence/ExpertJpa.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

패키지 경로 떼고 import 하면 좋을 것 같은데 혹시 의도된걸까용

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동일한 이름의 LookupPort가 두 패키지에 있어서 충돌 때문에 FQN을 사용했습니다. 현재 저희팀 컨벤션 상, FQN을 유지하는 방법이 최선일 것 같은데, 네이밍 자체를 변경하는 게 좋을까요 ?
@2ghrms


private final ExpertRepository repository;

Expand All @@ -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<Expert> 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<Expert> findAllWithDetails() {
public List<Expert> findAllWithCareersTagsCategories() {
try {
return repository.findAllWithDetails();
List<Long> ids = repository.findAllIds();
return fetchWithCollections(ids);
} catch (Exception e) {
log.error("전문가 목록 조회 중 오류가 발생했습니다.", e);
throw new ExpertException(ExpertErrorType.EXPERT_QUERY_ERROR);
Expand All @@ -50,19 +65,36 @@ public List<Expert> findAllWithDetails() {
@Override
public List<Expert> findByAllCategories(Collection<TagCategory> categories) {
try {
return repository.findByAllCategories(categories, categories.size());
List<Expert> experts = repository.findByAllCategories(categories, categories.size());
if (experts.isEmpty()) {
return experts;
}

List<Long> 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<Long, Expert> findExpertMapByIds(Set<Long> expertIds) {

List<Expert> experts = repository.findAllWithDetailsByIds(expertIds);
public Map<Long, Expert> findByIds(Set<Long> expertIds) {
List<Expert> experts = repository.findAllByIds(expertIds);

return experts.stream()
.collect(Collectors.toMap(Expert::getId, Function.identity()));
}

private List<Expert> fetchWithCollections(List<Long> ids) {
List<Expert> experts = repository.fetchExpertsWithCareersByIds(ids);
repository.fetchExpertsWithTagsByIds(ids);
repository.fetchExpertsWithCategoriesByIds(ids);
return experts;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Expert, Long> {

@Query("select distinct e from Expert e")
@EntityGraph(attributePaths = {"categories", "careers", "tags"})
List<Expert> findAllWithDetails();
@Query("select e.id from Expert e")
List<Long> findAllIds();

@Query("select distinct e from Expert e left join fetch e.careers where e.id in :ids")
List<Expert> fetchExpertsWithCareersByIds(@Param("ids") List<Long> ids);

@Query("select distinct e from Expert e left join fetch e.tags where e.id in :ids")
List<Expert> fetchExpertsWithTagsByIds(@Param("ids") List<Long> ids);

@Query("select distinct e from Expert e left join fetch e.categories where e.id in :ids")
List<Expert> fetchExpertsWithCategoriesByIds(@Param("ids") List<Long> ids);

@Query("select distinct e from Expert e where e.id in :expertIds")
@EntityGraph(attributePaths = {"categories", "careers", "tags"})
List<Expert> findAllWithDetailsByIds(Set<Long> expertIds);
List<Expert> findAllByIds(Set<Long> expertIds);

@Query("""
select distinct e from Expert e where e.id in (
Expand All @@ -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<Expert> findByAllCategories(@Param("cats") Collection<TagCategory> 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<Expert> findByIdWithDetails(@Param("id") Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,16 +21,19 @@
@RequestMapping("/v1/experts")
public class ExpertController implements ExpertQueryApiDoc {

private final ExpertFinder expertFinder;
private final ExpertDetailQueryUseCase expertDetailQuery;

@GetMapping
public ApiResponse<List<ExpertDetailResponse>> search(
public ApiResponse<List<ExpertListResponse>> search(
@RequestParam(name = "categories", required = false) Set<TagCategory> categories
) {
List<Expert> 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<ExpertDetailResponse> detail(
@PathVariable Long expertId
) {
return ApiResponse.success(ExpertDetailResponse.from(expertDetailQuery.findById(expertId)));
}
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,32 +23,33 @@ public record ExpertDetailResponse(

Integer mentoringPriceWon,

List<String> careers,

List<String> tags,
List<ExpertCareerResponse> careers,

List<String> categories
List<String> tags
) {
public static ExpertDetailResponse from(Expert expert) {
List <String> categories = expert.getCategories().stream()
.map(TagCategory::name)
.distinct()
public static ExpertDetailResponse from(ExpertDetailResult result) {
List<ExpertCareerResponse> 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<ExpertDetailResponse> fromAll(Collection<Expert> experts){
return experts.stream().map(ExpertDetailResponse::from).toList();
public static List<ExpertDetailResponse> fromAllResults(List<ExpertDetailResult> results) {
return results.stream()
.map(ExpertDetailResponse::from)
.toList();
}
}
Loading
Loading