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
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import starlight.adapter.ai.util.AiReportResponseParser;
import starlight.application.aireport.required.AiReportQuery;
import starlight.application.expert.required.AiReportSummaryLookupPort;
import starlight.domain.aireport.entity.AiReport;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Component
@RequiredArgsConstructor
public class AiReportJpa implements AiReportQuery {
public class AiReportJpa implements AiReportQuery, AiReportSummaryLookupPort {

private final AiReportRepository aiReportRepository;
private final AiReportResponseParser responseParser;

@Override
public AiReport save(AiReport aiReport) {
Expand All @@ -22,5 +29,21 @@ public AiReport save(AiReport aiReport) {
public Optional<AiReport> findByBusinessPlanId(Long businessPlanId) {
return aiReportRepository.findByBusinessPlanId(businessPlanId);
}
}

@Override
public Map<Long, Integer> findTotalScoresByBusinessPlanIds(List<Long> businessPlanIds) {
if (businessPlanIds == null || businessPlanIds.isEmpty()) {
return Collections.emptyMap();
}

List<AiReport> reports = aiReportRepository.findAllByBusinessPlanIdIn(businessPlanIds);
Map<Long, Integer> totalScoreMap = new HashMap<>();

for (AiReport report : reports) {
Integer totalScore = responseParser.toResponse(report).totalScore();
totalScoreMap.put(report.getBusinessPlanId(), totalScore != null ? totalScore : 0);
}

return totalScoreMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import org.springframework.data.jpa.repository.JpaRepository;
import starlight.domain.aireport.entity.AiReport;

import java.util.Collection;
import java.util.Optional;
import java.util.List;

public interface AiReportRepository extends JpaRepository<AiReport, Long> {

Optional<AiReport> findByBusinessPlanId(Long businessPlanId);
}

List<AiReport> findAllByBusinessPlanIdIn(Collection<Long> businessPlanIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public class ImageController implements ImageApiDoc {

@GetMapping(value = "/upload-url", produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<PreSignedUrlResponse> getPresignedUrl(
@AuthenticationPrincipal AuthenticatedMember authDetails,
@AuthenticationPrincipal AuthenticatedMember authenticatedMember,
@RequestParam String fileName
) {
return ApiResponse.success(presignedUrlReader.getPreSignedUrl(authDetails.getMemberId(), fileName));
return ApiResponse.success(presignedUrlReader.getPreSignedUrl(authenticatedMember.getMemberId(), fileName));
}

@PostMapping("/upload-url/public")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public interface ImageApiDoc {
})
@GetMapping(value = "/v1/image/upload-url", produces = MediaType.APPLICATION_JSON_VALUE)
ApiResponse<PreSignedUrlResponse> getPresignedUrl(
@AuthenticationPrincipal AuthenticatedMember authDetails,
@AuthenticationPrincipal AuthenticatedMember authenticatedMember,
@io.swagger.v3.oas.annotations.Parameter(description = "파일명", required = true) @RequestParam String fileName
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package starlight.adapter.businessplan.persistence;

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import starlight.application.businessplan.required.BusinessPlanQuery;
import starlight.application.expert.required.BusinessPlanLookupPort;
import starlight.domain.businessplan.entity.BusinessPlan;
import starlight.domain.businessplan.exception.BusinessPlanErrorType;
import starlight.domain.businessplan.exception.BusinessPlanException;

import java.util.List;

@Repository
@RequiredArgsConstructor
public class BusinessPlanJpa implements BusinessPlanQuery {
public class BusinessPlanJpa implements BusinessPlanQuery, BusinessPlanLookupPort {

private final BusinessPlanRepository businessPlanRepository;

Expand Down Expand Up @@ -43,4 +46,9 @@ public void delete(BusinessPlan businessPlan) {
public Page<BusinessPlan> findPreviewPage(Long memberId, Pageable pageable) {
return businessPlanRepository.findAllByMemberIdOrderedByLastSavedAt(memberId, pageable);
}

@Override
public List<BusinessPlan> findAllByMemberId(Long memberId) {
return businessPlanRepository.findAllByMemberIdOrderByLastSavedAt(memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.data.repository.query.Param;
import starlight.domain.businessplan.entity.BusinessPlan;

import java.util.List;
import java.util.Optional;

public interface BusinessPlanRepository extends JpaRepository<BusinessPlan, Long> {
Expand All @@ -23,6 +24,14 @@ ORDER BY COALESCE(bp.modifiedAt, bp.createdAt) DESC, bp.id DESC
""")
Page<BusinessPlan> findAllByMemberIdOrderedByLastSavedAt(@Param("memberId") Long memberId, Pageable pageable);

@Query("""
SELECT bp
FROM BusinessPlan bp
WHERE bp.memberId = :memberId
ORDER BY COALESCE(bp.modifiedAt, bp.createdAt) DESC, bp.id DESC
""")
List<BusinessPlan> findAllByMemberIdOrderByLastSavedAt(@Param("memberId") Long memberId);

@Query("""
SELECT DISTINCT bp
FROM BusinessPlan bp
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
package starlight.adapter.expert.webapi;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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.RestController;
import starlight.adapter.expert.webapi.dto.ExpertAiReportBusinessPlanResponse;
import starlight.adapter.expert.webapi.dto.ExpertDetailResponse;
import starlight.adapter.expert.webapi.dto.ExpertListResponse;
import starlight.adapter.expert.webapi.swagger.ExpertQueryApiDoc;
import starlight.adapter.expert.webapi.swagger.ExpertApiDoc;
import starlight.application.expert.provided.ExpertAiReportQueryUseCase;
import starlight.application.expert.provided.ExpertDetailQueryUseCase;
import starlight.shared.auth.AuthenticatedMember;
import starlight.shared.apiPayload.response.ApiResponse;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/experts")
public class ExpertController implements ExpertQueryApiDoc {
public class ExpertController implements ExpertApiDoc {

private final ExpertDetailQueryUseCase expertDetailQuery;
private final ExpertAiReportQueryUseCase expertAiReportQuery;

@GetMapping
public ApiResponse<List<ExpertListResponse>> search() {
Expand All @@ -31,4 +36,14 @@ public ApiResponse<ExpertDetailResponse> detail(
) {
return ApiResponse.success(ExpertDetailResponse.from(expertDetailQuery.findById(expertId)));
}

@GetMapping("/{expertId}/business-plans/ai-reports")
public ApiResponse<List<ExpertAiReportBusinessPlanResponse>> aiReportBusinessPlans(
@PathVariable Long expertId,
@AuthenticationPrincipal AuthenticatedMember authenticatedMember
) {
return ApiResponse.success(ExpertAiReportBusinessPlanResponse.fromAll(
expertAiReportQuery.findAiReportBusinessPlans(expertId, authenticatedMember.getMemberId())
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package starlight.adapter.expert.webapi.dto;

import starlight.application.expert.provided.dto.ExpertAiReportBusinessPlanResult;

import java.util.List;

public record ExpertAiReportBusinessPlanResponse(
Long businessPlanId,
String businessPlanTitle,
Long requestCount,
boolean isOver70
) {
public static ExpertAiReportBusinessPlanResponse from(ExpertAiReportBusinessPlanResult result) {
return new ExpertAiReportBusinessPlanResponse(
result.businessPlanId(),
result.businessPlanTitle(),
result.requestCount(),
result.isOver70()
);
}

public static List<ExpertAiReportBusinessPlanResponse> fromAll(List<ExpertAiReportBusinessPlanResult> results) {
return results.stream()
.map(ExpertAiReportBusinessPlanResponse::from)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
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 org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import starlight.adapter.expert.webapi.dto.ExpertAiReportBusinessPlanResponse;
import starlight.adapter.expert.webapi.dto.ExpertDetailResponse;
import starlight.adapter.expert.webapi.dto.ExpertListResponse;
import starlight.shared.apiPayload.response.ApiResponse;
import starlight.shared.auth.AuthenticatedMember;

import java.util.List;

@Tag(name = "전문가", description = "전문가 관련 API")
public interface ExpertQueryApiDoc {
public interface ExpertApiDoc {

@Operation(
summary = "전문가 목록 조회",
Expand Down Expand Up @@ -180,4 +183,82 @@ public interface ExpertQueryApiDoc {
ApiResponse<ExpertDetailResponse> detail(
@PathVariable Long expertId
);

@Operation(
summary = "전문가 상세 내 AI 리포트 보유 사업계획서 목록",
description = "지정된 전문가의 전문가 상세 페이지에서 로그인한 사용자의 사업계획서 중 AI 리포트가 생성된 항목만 조회합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "성공",
content = @Content(
mediaType = "application/json",
array = @ArraySchema(schema = @Schema(implementation = ExpertAiReportBusinessPlanResponse.class)),
examples = @ExampleObject(
name = "성공 예시",
value = """
{
"result": "SUCCESS",
"data": [
{
"businessPlanId": 10,
"businessPlanTitle": "테스트 사업계획서",
"requestCount": 2,
"isOver70": true
},
{
"businessPlanId": 11,
"businessPlanTitle": "신규 사업계획서",
"requestCount": 0,
"isOver70": false
}
],
"error": null
}
"""
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "500",
description = "조회 오류",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(
name = "전문가 신청 조회 오류",
value = """
{
"result": "ERROR",
"data": null,
"error": {
"code": "EXPERT_APPLICATION_QUERY_ERROR",
"message": "전문가 신청 정보를 조회하는 중에 오류가 발생했습니다."
}
}
"""
),
@ExampleObject(
name = "AI 리포트 파싱 오류",
value = """
{
"result": "ERROR",
"data": null,
"error": {
"code": "AI_RESPONSE_PARSING_FAILED",
"message": "AI 응답 파싱에 실패했습니다."
}
}
"""
)
}
)
)
})
@GetMapping("/{expertId}/business-plans/ai-reports")
ApiResponse<List<ExpertAiReportBusinessPlanResponse>> aiReportBusinessPlans(
@PathVariable Long expertId,
@AuthenticationPrincipal AuthenticatedMember authenticatedMember
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@ public Boolean existsByExpertIdAndBusinessPlanId(Long expertId, Long businessPla
}
}

@Override
public List<Long> findRequestedExpertIds(Long businessPlanId) {
try {
return repository.findRequestedExpertIdsByPlanId(businessPlanId);
} catch (Exception e) {
log.error("신청된 전문가 목록 조회 중 오류가 발생했습니다.", e);
throw new ExpertApplicationException(ExpertApplicationErrorType.EXPERT_APPLICATION_QUERY_ERROR);
}
}

@Override
public ExpertApplication save(ExpertApplication application) {
return repository.save(application);
Expand All @@ -64,4 +54,25 @@ public Map<Long, Long> countByExpertIds(List<Long> expertIds) {
throw new ExpertApplicationException(ExpertApplicationErrorType.EXPERT_APPLICATION_QUERY_ERROR);
}
}

@Override
public Map<Long, Long> countByExpertIdAndBusinessPlanIds(Long expertId, List<Long> businessPlanIds) {
try {
if (expertId == null) {
return Collections.emptyMap();
}
if (businessPlanIds == null || businessPlanIds.isEmpty()) {
return Collections.emptyMap();
}

return repository.countByExpertIdAndBusinessPlanIds(expertId, businessPlanIds).stream()
.collect(Collectors.toMap(
ExpertApplicationRepository.BusinessPlanIdCountProjection::getBusinessPlanId,
p -> (long) p.getCount()
));
} catch (Exception e) {
log.error("사업계획서별 신청 건수 조회 중 오류가 발생했습니다.", e);
throw new ExpertApplicationException(ExpertApplicationErrorType.EXPERT_APPLICATION_QUERY_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,6 @@ public interface ExpertApplicationRepository extends JpaRepository<ExpertApplica

Boolean existsByExpertIdAndBusinessPlanId(Long mentorId, Long businessPlanId);

@Query("""
select distinct e.expertId
from ExpertApplication e
where e.businessPlanId = :businessPlanId
""")
List<Long> findRequestedExpertIdsByPlanId(@Param("businessPlanId") Long businessPlanId);

interface ExpertIdCountProjection {
Long getExpertId();
long getCount();
Expand All @@ -30,4 +23,21 @@ select e.expertId as expertId, count(e) as count
group by e.expertId
""")
List<ExpertIdCountProjection> countByExpertIds(@Param("expertIds") List<Long> expertIds);

interface BusinessPlanIdCountProjection {
Long getBusinessPlanId();
long getCount();
}

@Query("""
select e.businessPlanId as businessPlanId, count(e) as count
from ExpertApplication e
where e.expertId = :expertId
and e.businessPlanId in :businessPlanIds
group by e.businessPlanId
""")
List<BusinessPlanIdCountProjection> countByExpertIdAndBusinessPlanIds(
@Param("expertId") Long expertId,
@Param("businessPlanIds") List<Long> businessPlanIds
);
}
Loading
Loading