package com.goldenchart.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.goldenchart.dto.TrendSearchRequest; import com.goldenchart.dto.TrendSearchResultDto; import com.goldenchart.dto.TrendSearchResultsResponse; import com.goldenchart.entity.GcAppSettings; import com.goldenchart.entity.GcTrendSearchSnapshot; import com.goldenchart.repository.GcAppSettingsRepository; import com.goldenchart.repository.GcTrendSearchSnapshotRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Collections; import java.util.List; import java.util.Optional; @Service @RequiredArgsConstructor @Slf4j public class TrendSearchSnapshotService { private static final TypeReference> RESULT_LIST_TYPE = new TypeReference<>() {}; private final GcTrendSearchSnapshotRepository snapshotRepo; private final GcAppSettingsRepository appSettingsRepo; private final TrendSearchService trendSearchService; private final TrendSearchSettingsHelper settingsHelper; private final ObjectMapper mapper; @Transactional(readOnly = true) public TrendSearchResultsResponse getResults(Long userId, String deviceId) { String scopeKey = TrendSearchScope.key(userId, deviceId); return snapshotRepo.findById(scopeKey) .map(this::toResponse) .orElse(TrendSearchResultsResponse.empty()); } @Transactional public List scanAndPersist(Long userId, String deviceId, TrendSearchRequest request) { GcAppSettings app = findAppSettings(userId, deviceId); TrendSearchRequest req = app != null ? settingsHelper.mergeRequestWithSettings(request, app) : (request != null ? request : new TrendSearchRequest()); List results = trendSearchService.scan(req); persist(scopeKey(userId, deviceId), userId, deviceId, req, results); return results; } @Transactional public void runScheduledScan(Long userId, String deviceId) { String scopeKey = TrendSearchScope.key(userId, deviceId); GcAppSettings app = findAppSettings(userId, deviceId); if (app == null) return; TrendSearchRequest last = snapshotRepo.findById(scopeKey) .map(s -> settingsHelper.parseRequestJson(s.getRequestJson())) .orElse(null); TrendSearchRequest req = settingsHelper.buildRequest(app, last); List results = trendSearchService.scan(req); persist(scopeKey, userId, deviceId, req, results); log.debug("[TrendSearch] scheduled scan scope={} count={}", scopeKey, results.size()); } private void persist(String scopeKey, Long userId, String deviceId, TrendSearchRequest req, List results) { try { String resultsJson = mapper.writeValueAsString(results != null ? results : Collections.emptyList()); String requestJson = mapper.writeValueAsString(req); GcTrendSearchSnapshot snap = snapshotRepo.findById(scopeKey).orElseGet(() -> GcTrendSearchSnapshot.builder().scopeKey(scopeKey).build()); snap.setUserId(userId); snap.setDeviceId(deviceId); snap.setTimeframe(req.getTimeframe() != null ? req.getTimeframe() : "1d"); snap.setScannedAt(LocalDateTime.now()); snap.setRequestJson(requestJson); snap.setResultsJson(resultsJson); snapshotRepo.save(snap); } catch (Exception e) { log.warn("[TrendSearch] persist failed scope={}: {}", scopeKey, e.getMessage()); } } private TrendSearchResultsResponse toResponse(GcTrendSearchSnapshot snap) { List list = Collections.emptyList(); try { if (snap.getResultsJson() != null && !snap.getResultsJson().isBlank()) { list = mapper.readValue(snap.getResultsJson(), RESULT_LIST_TYPE); } } catch (Exception e) { log.warn("[TrendSearch] deserialize results scope={}: {}", snap.getScopeKey(), e.getMessage()); } Instant scannedAt = snap.getScannedAt() != null ? snap.getScannedAt().atZone(ZoneId.systemDefault()).toInstant() : null; return TrendSearchResultsResponse.builder() .results(list) .timeframe(snap.getTimeframe()) .scannedAt(scannedAt) .build(); } private GcAppSettings findAppSettings(Long userId, String deviceId) { if (userId != null) { return appSettingsRepo.findByUserId(userId).orElse(null); } if (deviceId != null && !deviceId.isBlank()) { return appSettingsRepo.findByDeviceId(deviceId).orElse(null); } return null; } private static String scopeKey(Long userId, String deviceId) { return TrendSearchScope.key(userId, deviceId); } /** 스케줄러: 마지막 스캔 시각 (없으면 epoch) */ @Transactional(readOnly = true) public Optional lastScannedAt(Long userId, String deviceId) { return snapshotRepo.findById(TrendSearchScope.key(userId, deviceId)) .map(GcTrendSearchSnapshot::getScannedAt); } }