package com.goldenchart.service; import com.goldenchart.dto.TradeSignalDto; import com.goldenchart.entity.GcTradeSignal; import com.goldenchart.repository.GcStrategyRepository; import com.goldenchart.repository.GcTradeSignalRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * 매매 시그널 이력 서비스. * 실시간 전략 체크(LiveStrategyEvaluator / LiveStrategyScheduler)에서 * BUY/SELL 시그널이 발생하면 이 서비스를 통해 DB에 저장한다. */ @Service @RequiredArgsConstructor @Slf4j @Transactional public class TradeSignalService { private final GcTradeSignalRepository repo; private final GcStrategyRepository strategyRepo; private final FcmPushService fcmPushService; // ── 저장 ───────────────────────────────────────────────────────────────── public TradeSignalDto save(String deviceId, Long userId, String market, Long strategyId, String strategyName, String signalType, double price, long candleTime, String candleType, String executionType) { String resolvedName = resolveStrategyName(strategyId, strategyName); GcTradeSignal entity = GcTradeSignal.builder() .deviceId(deviceId) .userId(userId) .market(market) .strategyId(strategyId) .strategyName(resolvedName) .signalType(signalType) .price(price) .candleTime(candleTime) .candleType(candleType) .executionType(executionType) .build(); GcTradeSignal saved = repo.save(entity); log.info("[TradeSignal] saved {} {} @ {} market={} userId={} deviceId={}", signalType, candleType, price, market, userId, deviceId); // 로그인 사용자는 live 설정에 userId만 있고 deviceId가 null — FCM은 userId 기준으로 발송 if (userId != null || (deviceId != null && !deviceId.isBlank())) { fcmPushService.sendTradeSignalIfEnabled( deviceId, userId, market, signalType, price, resolvedName, saved.getId()); } return toDto(saved); } // ── 조회 ───────────────────────────────────────────────────────────────── @Transactional(readOnly = true) public List list(Long userId, String deviceId) { List list = userId != null ? repo.findByUserIdOrderByCreatedAtDesc(userId) : repo.findByDeviceIdOrderByCreatedAtDesc(deviceId != null ? deviceId : ""); return list.stream().map(this::toDto).toList(); } @Transactional(readOnly = true) public List listByMarket(Long userId, String deviceId, String market) { List list = userId != null ? repo.findByUserIdAndMarketOrderByCreatedAtDesc(userId, market) : repo.findByDeviceIdAndMarketOrderByCreatedAtDesc( deviceId != null ? deviceId : "", market); return list.stream().map(this::toDto).toList(); } // ── 삭제 ───────────────────────────────────────────────────────────────── public void deleteOne(Long id, Long userId, String deviceId) { if (id == null) return; repo.findById(id).ifPresent(entity -> { if (!owns(entity, userId, deviceId)) return; repo.delete(entity); log.info("[TradeSignal] deleted id={} market={}", id, entity.getMarket()); }); } public int deleteBatch(List ids, Long userId, String deviceId) { if (ids == null || ids.isEmpty()) return 0; List toDelete = ids.stream() .distinct() .map(repo::findById) .filter(java.util.Optional::isPresent) .map(java.util.Optional::get) .filter(e -> owns(e, userId, deviceId)) .toList(); if (toDelete.isEmpty()) return 0; repo.deleteAll(toDelete); log.info("[TradeSignal] batch deleted count={}", toDelete.size()); return toDelete.size(); } public int deleteAll(Long userId, String deviceId) { if (userId != null) { long count = repo.findByUserIdOrderByCreatedAtDesc(userId).size(); repo.deleteByUserId(userId); log.info("[TradeSignal] deleted all for userId={} count≈{}", userId, count); return (int) count; } String dev = deviceId != null ? deviceId : ""; long count = repo.findByDeviceIdOrderByCreatedAtDesc(dev).size(); repo.deleteByDeviceId(dev); log.info("[TradeSignal] deleted all for deviceId={} count≈{}", dev, count); return (int) count; } private static boolean owns(GcTradeSignal entity, Long userId, String deviceId) { if (userId != null && userId.equals(entity.getUserId())) return true; if (deviceId == null || deviceId.isBlank()) return false; return deviceId.equals(entity.getDeviceId()); } private String resolveStrategyName(Long strategyId, String strategyName) { if (strategyName != null && !strategyName.isBlank()) return strategyName; if (strategyId == null) return null; return strategyRepo.findById(strategyId) .map(s -> s.getName()) .orElse(null); } // ── 변환 ───────────────────────────────────────────────────────────────── private TradeSignalDto toDto(GcTradeSignal e) { return TradeSignalDto.builder() .id(e.getId()) .market(e.getMarket()) .strategyId(e.getStrategyId()) .strategyName(e.getStrategyName()) .signalType(e.getSignalType()) .price(e.getPrice()) .candleTime(e.getCandleTime()) .candleType(e.getCandleType()) .executionType(e.getExecutionType()) .createdAt(e.getCreatedAt() != null ? e.getCreatedAt().toString() : null) .build(); } }