package com.goldenchart.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.goldenchart.entity.GcIndicatorSettings; import com.goldenchart.repository.GcIndicatorSettingsRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * 전역 지표 파라미터 설정 서비스. * *

프론트엔드 indicatorRegistry.ts 의 defaultParams 를 완전히 대체하며, * 사용자가 변경한 파라미터를 DB 에 저장하고 백엔드 지표 계산 시 우선 적용한다.

*/ @Service @RequiredArgsConstructor @Slf4j @Transactional public class IndicatorSettingsService { private final GcIndicatorSettingsRepository repo; private final ObjectMapper mapper; // ── 공개 API ───────────────────────────────────────────────────────────── /** * 장치/사용자 식별자로 전체 지표 파라미터 맵을 조회한다. * 없으면 빈 맵 반환. * * @return Map<indicatorType, Map<paramKey, paramValue>> */ @Transactional(readOnly = true) public Map> getAll(Long userId, String deviceId) { return findEntity(userId, deviceId) .map(s -> parseParamsJson(s.getParamsJson())) .orElse(Collections.emptyMap()); } /** * 특정 지표의 파라미터를 조회한다. * 없으면 빈 맵 반환. */ @Transactional(readOnly = true) public Map getForType(Long userId, String deviceId, String indicatorType) { Map> all = getAll(userId, deviceId); return all.getOrDefault(indicatorType, Collections.emptyMap()); } /** * 전체 지표 파라미터를 저장(덮어쓰기)한다. * 프론트엔드에서 전체 설정을 한 번에 저장할 때 사용. * * @param allParams Map<indicatorType, Map<paramKey, paramValue>> */ public void saveAll(Long userId, String deviceId, Map> allParams) { GcIndicatorSettings entity = findOrCreate(userId, deviceId); entity.setParamsJson(toJson(allParams)); repo.save(entity); log.debug("[IndicatorSettings] saved {} indicator types for device={}", allParams.size(), deviceId); } /** * 특정 지표의 파라미터만 병합(upsert)한다. * 프론트엔드에서 단일 지표 파라미터를 변경할 때 사용. * * @param indicatorType 지표 타입 (e.g. "RSI", "MACD") * @param params 파라미터 맵 (e.g. {"length": 9, "src": "close"}) */ public void saveForType(Long userId, String deviceId, String indicatorType, Map params) { GcIndicatorSettings entity = findOrCreate(userId, deviceId); Map> all = new HashMap<>(parseParamsJson(entity.getParamsJson())); all.put(indicatorType, params); entity.setParamsJson(toJson(all)); repo.save(entity); log.debug("[IndicatorSettings] saved params for {}:{} device={}", indicatorType, params, deviceId); } // ── 시각 설정 (색상·선굵기·수평선) ────────────────────────────────────── /** * 전체 지표의 시각 설정을 조회한다. * * @return Map<indicatorType, Map{plots, hlines}> */ @Transactional(readOnly = true) public Map> getAllVisual(Long userId, String deviceId) { return findEntity(userId, deviceId) .map(s -> parseVisualJson(s.getVisualConfigJson())) .orElse(Collections.emptyMap()); } /** * 특정 지표의 시각 설정(색상·선굵기·수평선)을 저장(병합)한다. * * @param indicatorType 지표 타입 (e.g. "RSI") * @param visual {@code {plots:[...], hlines:[...]}} 구조의 맵 */ public void saveVisualForType(Long userId, String deviceId, String indicatorType, Map visual) { GcIndicatorSettings entity = findOrCreate(userId, deviceId); Map> all = new HashMap<>(parseVisualJson(entity.getVisualConfigJson())); all.put(indicatorType, visual); entity.setVisualConfigJson(toJson(all)); repo.save(entity); log.debug("[IndicatorSettings] saved visual for {}:{} device={}", indicatorType, visual, deviceId); } /** * 요청 params 에 없는 키를 DB 설정값으로 채운다. * IndicatorController 에서 호출 — 하드코딩 기본값 제거. * * @param requestParams 프론트엔드가 보낸 파라미터 (null 허용) * @param indicatorType 지표 타입 * @return 병합된 파라미터 맵 (DB 값 우선, 없으면 요청값) */ @Transactional(readOnly = true) public Map mergeWithDb(Long userId, String deviceId, Map requestParams, String indicatorType) { Map dbParams = getForType(userId, deviceId, indicatorType); if (dbParams.isEmpty() && (requestParams == null || requestParams.isEmpty())) { return Map.of(); // 둘 다 없으면 빈 맵 반환 (IndicatorService 내부 기본값 사용) } // DB 값 기반 + 요청 파라미터로 덮어쓰기 (요청이 명시적으로 보낸 값 우선) Map merged = new HashMap<>(dbParams); if (requestParams != null) { merged.putAll(requestParams); } return merged; } // ── Private helpers ─────────────────────────────────────────────────────── private Optional findEntity(Long userId, String deviceId) { if (userId != null) return repo.findByUserId(userId); if (deviceId != null) return repo.findByDeviceId(deviceId); return Optional.empty(); } private GcIndicatorSettings findOrCreate(Long userId, String deviceId) { return findEntity(userId, deviceId).orElseGet(() -> repo.save(GcIndicatorSettings.builder() .userId(userId) .deviceId(deviceId) .paramsJson("{}") .build())); } @SuppressWarnings("unchecked") private Map> parseParamsJson(String json) { if (json == null || json.isBlank() || "{}".equals(json.trim())) { return new HashMap<>(); } try { return mapper.readValue(json, new TypeReference>>() {}); } catch (JsonProcessingException e) { log.warn("[IndicatorSettings] JSON 파싱 실패: {}", e.getMessage()); return new HashMap<>(); } } @SuppressWarnings("unchecked") private Map> parseVisualJson(String json) { if (json == null || json.isBlank() || "{}".equals(json.trim())) { return new HashMap<>(); } try { return mapper.readValue(json, new TypeReference>>() {}); } catch (JsonProcessingException e) { log.warn("[IndicatorSettings] visual JSON 파싱 실패: {}", e.getMessage()); return new HashMap<>(); } } private String toJson(Object obj) { try { return mapper.writeValueAsString(obj); } catch (JsonProcessingException e) { log.error("[IndicatorSettings] JSON 직렬화 실패", e); return "{}"; } } }