diff --git a/src/main/java/com/spots/domain/ai/service/RecommendService.java b/src/main/java/com/spots/domain/ai/service/RecommendService.java index cfea1a1..56f7af2 100644 --- a/src/main/java/com/spots/domain/ai/service/RecommendService.java +++ b/src/main/java/com/spots/domain/ai/service/RecommendService.java @@ -1,22 +1,19 @@ package com.spots.domain.ai.service; -import static java.math.BigDecimal.valueOf; -import static java.math.RoundingMode.HALF_UP; - import com.spots.domain.ai.dto.request.RecommendLLMRequest; import com.spots.domain.ai.dto.request.RecommendLLMRequest.RecommendProgramData; import com.spots.domain.ai.dto.request.UserInfoServiceRequest; import com.spots.domain.ai.dto.response.WeeklyRecommendResponse; -import com.spots.domain.program.dto.response.ProgramDetailInfoResponse; import com.spots.domain.program.dto.response.ProgramInfoResponse; import com.spots.domain.program.dto.response.TransportData; +import com.spots.domain.program.dto.response.TransportDataRawWithFacility; import com.spots.domain.program.entity.Program; import com.spots.domain.program.repository.ProgramRepository; import com.spots.domain.program.service.ProgramService; +import com.spots.domain.transport.repository.TransitRepository; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -24,7 +21,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StopWatch; @Service @RequiredArgsConstructor @@ -33,6 +29,7 @@ public class RecommendService { private final ProgramService programService; private final ProgramRepository programRepository; + private final TransitRepository transitRepository; private final RecommendLLMService recommendLLMService; private static final double EARTH_RADIUS_KM = 6371.0; @@ -43,17 +40,16 @@ public CompletableFuture recommendWeeklyRoutine( UserInfoServiceRequest request ) { - List programs = - programRepository - .searchPrograms( - request.toProgramInfoServiceRequest(), - 50L, - null, - null - ) - .getContent(); - log.info("programs size: {}", programs.size()); - List recommendProgramDataList = convertToRecommendProgramData(programs, request); + List programs = programRepository + .searchPrograms( + request.toProgramInfoServiceRequest(), + 50L, + null, + null + ) + .getContent(); + + List recommendProgramDataList = convertToRecommendProgramData(programs); RecommendLLMRequest llmRequest = RecommendLLMRequest.from(request, recommendProgramDataList); WeeklyRecommendResponse response = recommendLLMService.createWeeklyPlan(llmRequest); @@ -61,109 +57,43 @@ public CompletableFuture recommendWeeklyRoutine( return CompletableFuture.completedFuture(response); } - private List convertToRecommendProgramData( - List programs, - UserInfoServiceRequest request - ) { - // 성능 측정을 위한 스톱워치 생성 - StopWatch stopWatch = new StopWatch("DataConversionTask"); - - // 1. ID 추출 - List programIds = programs.stream() - .map(ProgramInfoResponse::programId) - .toList(); - - // 2. 배치 조회 측정 - stopWatch.start("Bulk DB Fetch (findAllById)"); - List programEntities = programRepository.findAllById(programIds); - stopWatch.stop(); - - // Map 변환 - Map programMap = programEntities.stream() - .collect(Collectors.toMap(Program::getId, Function.identity())); - - // 3. 루프 처리 시간 측정 준비 - // stream 내부에서 시간을 누적하기 위해 AtomicLong 사용 - AtomicLong totalGetProgramTime = new AtomicLong(0); - AtomicLong totalCalcDistanceTime = new AtomicLong(0); - - stopWatch.start("Stream Processing (Loop)"); - - List result = programs.stream() - .map(programInfoResponse -> { - - // A. 상세 정보 조회 시간 측정 (가장 의심되는 구간) - long startService = System.nanoTime(); - ProgramDetailInfoResponse programDetailInfoResponse = - programService.getProgram(programInfoResponse.programId()); - long endService = System.nanoTime(); - totalGetProgramTime.addAndGet(endService - startService); // 누적 - - List transports = programDetailInfoResponse.transportData(); - - Program program = programMap.get(programInfoResponse.programId()); - - // B. 거리 계산 시간 측정 - double distance = 0.0; - if (program != null && program.getFacility() != null) { - long startCalc = System.nanoTime(); - distance = calculateDistance( - request.latitude(), - request.longitude(), - program.getFacility().getFcltyLa(), - program.getFacility().getFcltyLo() - ); - long endCalc = System.nanoTime(); - totalCalcDistanceTime.addAndGet(endCalc - startCalc); - } - - return new RecommendProgramData( - programInfoResponse, - transports, - distance - ); - }) - .toList(); - - stopWatch.stop(); - - // 4. 결과 로그 출력 - log.info("================ 성능 측정 결과 ================"); - log.info(stopWatch.prettyPrint()); // 전체적인 요약 출력 - log.info(">> [상세] programService.getProgram() 총 소요 시간: {} ms", totalGetProgramTime.get() / 1_000_000); - log.info(">> [상세] 거리 계산 총 소요 시간: {} ms", totalCalcDistanceTime.get() / 1_000_000); - log.info("=============================================="); - - return result; - } - - private Double calculateDistance( - Double userLa, - Double userLo, - Double fcltyLa, - Double fcltyLo + private List convertToRecommendProgramData( + List programs ) { - final double R = EARTH_RADIUS_KM; - - double lat1 = Math.toRadians(userLa); - double lon1 = Math.toRadians(userLo); - double lat2 = Math.toRadians(fcltyLa); - double lon2 = Math.toRadians(fcltyLo); - - double diffLat = lat2 - lat1; - double diffLon = lon2 - lon1; - - double a = - Math.pow(Math.sin(diffLat / 2), 2) - + Math.cos(lat1) - * Math.cos(lat2) - * Math.pow(Math.sin(diffLon / 2), 2); - - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return valueOf(R * c) - .setScale(2, HALF_UP) - .doubleValue(); + List programIds = programs.stream() + .map(ProgramInfoResponse::programId) + .toList(); + List programEntities = programRepository.findAllWithFacility(programIds); + Map programMap = programEntities.stream() + .collect(Collectors.toMap(Program::getId, Function.identity())); + + List facilityIds = programEntities.stream() + .map(program -> program.getFacility().getId()) + .distinct() + .toList(); + + Map> transitMap = transitRepository + .findTop2TransitByFacilityIds(facilityIds) + .stream() + .collect(Collectors.groupingBy( + TransportDataRawWithFacility::facilityId, + Collectors.mapping( + raw -> new TransportData( + raw.transportType(), + raw.transportName(), + raw.transportTime().longValue() + ), + Collectors.toList() + ) + )); + + return programs.stream() + .map(info -> { + Program program = programMap.get(info.programId()); + Long facilityId = program.getFacility().getId(); + List transports = transitMap.getOrDefault(facilityId, List.of()); + return new RecommendProgramData(info, transports, info.distance()); + }) + .toList(); } -} - +} \ No newline at end of file diff --git a/src/main/java/com/spots/domain/program/dto/response/TransportDataRawWithFacility.java b/src/main/java/com/spots/domain/program/dto/response/TransportDataRawWithFacility.java new file mode 100644 index 0000000..7c62508 --- /dev/null +++ b/src/main/java/com/spots/domain/program/dto/response/TransportDataRawWithFacility.java @@ -0,0 +1,10 @@ +package com.spots.domain.program.dto.response; + +public record TransportDataRawWithFacility( + Long facilityId, + String transportType, + String transportName, + Double transportTime +) { + +} diff --git a/src/main/java/com/spots/domain/program/repository/ProgramRepository.java b/src/main/java/com/spots/domain/program/repository/ProgramRepository.java index c46a6a0..0bf022d 100644 --- a/src/main/java/com/spots/domain/program/repository/ProgramRepository.java +++ b/src/main/java/com/spots/domain/program/repository/ProgramRepository.java @@ -1,10 +1,16 @@ package com.spots.domain.program.repository; import com.spots.domain.program.entity.Program; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface ProgramRepository extends JpaRepository, ProgramRepositoryCustom { + @Query("SELECT p FROM Program p JOIN FETCH p.facility WHERE p.id IN :ids") + List findAllWithFacility(@Param("ids") List ids); + } diff --git a/src/main/java/com/spots/domain/transport/repository/TransitRepository.java b/src/main/java/com/spots/domain/transport/repository/TransitRepository.java index b4b7aa8..a4f5f69 100644 --- a/src/main/java/com/spots/domain/transport/repository/TransitRepository.java +++ b/src/main/java/com/spots/domain/transport/repository/TransitRepository.java @@ -1,6 +1,7 @@ package com.spots.domain.transport.repository; import com.spots.domain.program.dto.response.TransportDataRaw; +import com.spots.domain.program.dto.response.TransportDataRawWithFacility; import com.spots.domain.transport.entity.FacilityTransit; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -12,7 +13,7 @@ public interface TransitRepository extends JpaRepository { @Query(value = """ - SELECT DISTINCT ON (rank) + SELECT DISTINCT ON (rank) pbtrnsp_fclty_sdiv_nm AS transportType, bstp_subwayst_nm AS transportName, (wlkg_mvmn_time / 60) + 1 AS transportTime @@ -22,4 +23,17 @@ SELECT DISTINCT ON (rank) """, nativeQuery = true) List findTop2Transit(@Param("facilityId") Long facilityId); + + @Query(value = """ + SELECT DISTINCT ON (facility_id, rank) + facility_id AS facilityId, + pbtrnsp_fclty_sdiv_nm AS transportType, + bstp_subwayst_nm AS transportName, + (wlkg_mvmn_time / 60) + 1 AS transportTime + FROM facility_transit + WHERE facility_id IN (:facilityIds) AND rank IN (1, 2) + ORDER BY facility_id, rank, wlkg_mvmn_time ASC + """, + nativeQuery = true) + List findTop2TransitByFacilityIds(@Param("facilityIds") List facilityIds); } \ No newline at end of file