package com.goldenchart.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.goldenchart.dto.StrategyDto; import com.goldenchart.entity.GcStrategy; import com.goldenchart.repository.GcStrategyRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Service @RequiredArgsConstructor @Slf4j public class StrategyService { private final GcStrategyRepository repository; private final ObjectMapper objectMapper; private final StrategyConditionTimeframeService conditionTimeframes; private final LiveStrategyEvaluator liveStrategyEvaluator; private final StrategyDslTimeframeNormalizer dslTimeframeNormalizer; // ── 조회 ────────────────────────────────────────────────────────────────── public List list(Long userId, String deviceId) { List entities = userId != null ? repository.findByUserIdOrderByUpdatedAtDesc(userId) : repository.findByDeviceIdOrderByUpdatedAtDesc(deviceId != null ? deviceId : ""); return entities.stream().map(this::toDto).toList(); } public Optional findById(Long id) { return repository.findById(id).map(this::toDto); } // ── 저장 / 수정 ─────────────────────────────────────────────────────────── @Transactional public StrategyDto save(StrategyDto dto, Long userId, String deviceId) { GcStrategy entity; if (dto.getId() != null) { entity = repository.findById(dto.getId()).orElseGet(GcStrategy::new); } else { entity = new GcStrategy(); entity.setUserId(userId); entity.setDeviceId(deviceId); } entity.setName(dto.getName() != null ? dto.getName() : "전략"); entity.setDescription(dto.getDescription()); if (dto.getEnabled() != null) entity.setEnabled(dto.getEnabled()); else if (entity.getEnabled() == null) entity.setEnabled(true); try { String strategyName = entity.getName() != null ? entity.getName() : dto.getName(); String buyJson = dto.getBuyCondition() != null ? objectMapper.writeValueAsString(dto.getBuyCondition()) : null; String sellJson = dto.getSellCondition() != null ? objectMapper.writeValueAsString(dto.getSellCondition()) : null; if (buyJson != null && sellJson != null) { sellJson = dslTimeframeNormalizer.alignSellStartTimeframeToBuy(buyJson, sellJson); } entity.setBuyConditionJson( buyJson != null ? dslTimeframeNormalizer.normalizeJson(buyJson, strategyName) : null); entity.setSellConditionJson( sellJson != null ? dslTimeframeNormalizer.normalizeJson(sellJson, strategyName) : null); if (dto.getFlowLayout() != null && !dto.getFlowLayout().isNull()) { entity.setFlowLayoutJson(objectMapper.writeValueAsString(dto.getFlowLayout())); } } catch (Exception e) { log.warn("전략 DSL 직렬화 실패: {}", e.getMessage()); } GcStrategy saved = repository.save(entity); conditionTimeframes.invalidate(saved.getId()); liveStrategyEvaluator.invalidateStrategy(saved.getId()); return toDto(saved); } /** * DB에 저장된 매수/매도 DSL에 TIMEFRAME 래핑이 없으면 보정 후 저장. * (편집기 재저장 없이 기존 3분봉 전략 등을 즉시 수정할 때) */ @Transactional public Optional repairTimeframeWrappers(Long id) { return repository.findById(id).map(entity -> { boolean changed = false; if (entity.getBuyConditionJson() != null && !entity.getBuyConditionJson().isBlank()) { String fixed = dslTimeframeNormalizer.normalizeJson( entity.getBuyConditionJson(), entity.getName()); if (!fixed.equals(entity.getBuyConditionJson())) { entity.setBuyConditionJson(fixed); changed = true; } } String sellJson = entity.getSellConditionJson(); if (entity.getBuyConditionJson() != null && !entity.getBuyConditionJson().isBlank() && sellJson != null && !sellJson.isBlank()) { String aligned = dslTimeframeNormalizer.alignSellStartTimeframeToBuy( entity.getBuyConditionJson(), sellJson); if (!aligned.equals(sellJson)) { entity.setSellConditionJson(aligned); sellJson = aligned; changed = true; } } if (sellJson != null && !sellJson.isBlank()) { String fixed = dslTimeframeNormalizer.normalizeJson(sellJson, entity.getName()); if (!fixed.equals(entity.getSellConditionJson())) { entity.setSellConditionJson(fixed); changed = true; } } if (!changed) return toDto(entity); GcStrategy saved = repository.save(entity); conditionTimeframes.invalidate(saved.getId()); liveStrategyEvaluator.invalidateStrategy(saved.getId()); log.info("[Strategy] TIMEFRAME 래핑 보정 strategyId={}", id); return toDto(saved); }); } // ── 삭제 ────────────────────────────────────────────────────────────────── @Transactional public void delete(Long id) { conditionTimeframes.invalidate(id); liveStrategyEvaluator.invalidateStrategy(id); repository.deleteById(id); } // ── 변환 ────────────────────────────────────────────────────────────────── private StrategyDto toDto(GcStrategy e) { StrategyDto dto = new StrategyDto(); dto.setId(e.getId()); dto.setName(e.getName()); dto.setDescription(e.getDescription()); dto.setEnabled(e.getEnabled()); dto.setCreatedAt(e.getCreatedAt()); dto.setUpdatedAt(e.getUpdatedAt()); try { if (e.getBuyConditionJson() != null) dto.setBuyCondition(objectMapper.readTree(e.getBuyConditionJson())); if (e.getSellConditionJson() != null) dto.setSellCondition(objectMapper.readTree(e.getSellConditionJson())); if (e.getFlowLayoutJson() != null && !e.getFlowLayoutJson().isBlank()) dto.setFlowLayout(objectMapper.readTree(e.getFlowLayoutJson())); } catch (Exception ex) { log.warn("전략 DSL 역직렬화 실패: {}", ex.getMessage()); } return dto; } }