package com.goldenchart.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.goldenchart.entity.GcChartSlot; import com.goldenchart.entity.GcChartWorkspace; import com.goldenchart.repository.GcChartWorkspaceRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 차트 워크스페이스·슬롯 설정 저장/조회 서비스. * userId 또는 deviceId 중 하나를 식별자로 사용한다. */ @Service @RequiredArgsConstructor @Slf4j @Transactional public class ChartSettingsService { private final GcChartWorkspaceRepository workspaceRepo; private final ObjectMapper objectMapper; // ── 워크스페이스 조회 ────────────────────────────────────────────────────── @Transactional(readOnly = true) public GcChartWorkspace getWorkspace(Long userId, String deviceId) { return findWorkspace(userId, deviceId).orElse(null); } @Transactional(readOnly = true) public GcChartWorkspace getWorkspaceWithSlots(Long userId, String deviceId) { if (userId != null) { return workspaceRepo.findWithSlotsByUserId(userId).orElse(null); } return workspaceRepo.findWithSlotsByDeviceId(deviceId).orElse(null); } // ── 워크스페이스 저장 (생성 또는 전체 업데이트) ──────────────────────────── public GcChartWorkspace saveWorkspace(Long userId, String deviceId, String layoutId, String syncOptionsJson) { GcChartWorkspace ws = findWorkspace(userId, deviceId) .orElseGet(() -> GcChartWorkspace.builder() .userId(userId) .deviceId(deviceId) .layoutId(layoutId) .slots(new ArrayList<>()) .build()); ws.setLayoutId(layoutId); ws.setSyncOptionsJson(syncOptionsJson); return workspaceRepo.save(ws); } // ── 슬롯 설정 저장 ───────────────────────────────────────────────────────── public GcChartSlot saveSlot(Long userId, String deviceId, int slotIndex, Map slotData) { GcChartWorkspace ws = findOrCreateWorkspace(userId, deviceId); GcChartSlot slot = ws.getSlots().stream() .filter(s -> s.getSlotIndex() == slotIndex) .findFirst() .orElseGet(() -> { GcChartSlot newSlot = GcChartSlot.builder() .workspace(ws) .slotIndex(slotIndex) .build(); ws.getSlots().add(newSlot); return newSlot; }); applySlotData(slot, slotData); workspaceRepo.save(ws); return slot; } // ── 전체 워크스페이스 + 슬롯 일괄 저장 (프론트 최초 로드 또는 전체 저장) ── public GcChartWorkspace saveAll(Long userId, String deviceId, String layoutId, String syncOptionsJson, List> slotsData) { GcChartWorkspace ws = findOrCreateWorkspace(userId, deviceId); ws.setLayoutId(layoutId); ws.setSyncOptionsJson(syncOptionsJson); // 슬롯 동기화 List existing = ws.getSlots(); for (int i = 0; i < slotsData.size(); i++) { final int idx = i; GcChartSlot slot = existing.stream() .filter(s -> s.getSlotIndex() == idx) .findFirst() .orElseGet(() -> { GcChartSlot ns = GcChartSlot.builder() .workspace(ws).slotIndex(idx).build(); existing.add(ns); return ns; }); applySlotData(slot, slotsData.get(i)); } // 초과 슬롯 제거 existing.removeIf(s -> s.getSlotIndex() >= slotsData.size()); return workspaceRepo.save(ws); } // ── private helpers ──────────────────────────────────────────────────────── private java.util.Optional findWorkspace(Long userId, String deviceId) { if (userId != null) return workspaceRepo.findByUserId(userId); return workspaceRepo.findByDeviceId(deviceId); } private GcChartWorkspace findOrCreateWorkspace(Long userId, String deviceId) { return findWorkspace(userId, deviceId) .orElseGet(() -> workspaceRepo.save(GcChartWorkspace.builder() .userId(userId).deviceId(deviceId).layoutId("1").build())); } private void applySlotData(GcChartSlot slot, Map d) { if (d.containsKey("symbol")) slot.setSymbol((String) d.get("symbol")); if (d.containsKey("timeframe")) slot.setTimeframe((String) d.get("timeframe")); if (d.containsKey("chartType")) slot.setChartType((String) d.get("chartType")); if (d.containsKey("theme")) slot.setTheme((String) d.get("theme")); if (d.containsKey("mode")) slot.setMode((String) d.get("mode")); if (d.containsKey("logScale")) slot.setLogScale(Boolean.parseBoolean(d.get("logScale").toString())); if (d.containsKey("drawingsLocked")) slot.setDrawingsLocked(Boolean.parseBoolean(d.get("drawingsLocked").toString())); if (d.containsKey("drawingsVisible"))slot.setDrawingsVisible(Boolean.parseBoolean(d.get("drawingsVisible").toString())); try { if (d.containsKey("indicators")) slot.setIndicatorsJson(objectMapper.writeValueAsString(d.get("indicators"))); if (d.containsKey("drawings")) slot.setDrawingsJson(objectMapper.writeValueAsString(d.get("drawings"))); if (d.containsKey("paneLayout")) slot.setPaneLayoutJson(objectMapper.writeValueAsString(d.get("paneLayout"))); if (d.containsKey("mainChartStyle")) slot.setMainChartStyleJson(objectMapper.writeValueAsString(d.get("mainChartStyle"))); } catch (JsonProcessingException e) { log.error("슬롯 JSON 직렬화 실패", e); } } }