goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,85 @@
package com.goldenchart.controller;
import com.goldenchart.dto.*;
import com.goldenchart.service.AdminService;
import com.goldenchart.service.RolePermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
public class AdminController {
private final AdminService adminService;
private final RolePermissionService rolePermissionService;
private Long requireUserId(@RequestHeader(value = "X-User-Id", required = false) Long userId) {
if (userId == null) {
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
}
rolePermissionService.requireAdmin(userId);
return userId;
}
@PostMapping("/verify-password")
public ResponseEntity<Map<String, Boolean>> verifyPassword(
@RequestHeader("X-User-Id") Long userId,
@RequestBody AdminVerifyRequest body) {
adminService.verifyAdminPassword(userId, body != null ? body.getPassword() : null);
return ResponseEntity.ok(Map.of("ok", true));
}
@GetMapping("/users")
public List<UserDto> listUsers(@RequestHeader("X-User-Id") Long userId) {
requireUserId(userId);
return adminService.listUsers();
}
@PostMapping("/users")
public UserDto createUser(
@RequestHeader("X-User-Id") Long userId,
@RequestBody CreateUserRequest body) {
requireUserId(userId);
return adminService.createUser(body);
}
@PutMapping("/users/{id}")
public UserDto updateUser(
@RequestHeader("X-User-Id") Long userId,
@PathVariable Long id,
@RequestBody UpdateUserRequest body) {
requireUserId(userId);
return adminService.updateUser(id, body, userId);
}
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(
@RequestHeader("X-User-Id") Long userId,
@PathVariable Long id) {
requireUserId(userId);
adminService.deleteUser(id, userId);
return ResponseEntity.noContent().build();
}
@GetMapping("/role-permissions")
public Map<String, Map<String, Boolean>> listRolePermissions(
@RequestHeader("X-User-Id") Long userId) {
requireUserId(userId);
return rolePermissionService.listAllRolePermissions();
}
@PutMapping("/role-permissions")
public ResponseEntity<Void> updateRolePermissions(
@RequestHeader("X-User-Id") Long userId,
@RequestBody RolePermissionsUpdateRequest body) {
requireUserId(userId);
rolePermissionService.updateRolePermissions(body);
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,84 @@
package com.goldenchart.controller;
import com.goldenchart.service.AppSettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 앱 전역 차트 기본 설정 REST API.
*
* <p>프론트엔드에 하드코딩된 기본값들(기본 심볼·타임프레임·테마·캔들 색상 등)을
* 장치/사용자별로 DB 에서 관리한다.</p>
*
* <pre>
* GET /app-settings — 현재 기본 설정 로드 (앱 시작 시 호출)
* PUT /app-settings — 기본 설정 저장
* </pre>
*
* 반환 구조:
* <pre>
* {
* "defaultSymbol": "KRW-BTC",
* "defaultTimeframe": "1D",
* "defaultChartType": "candlestick",
* "defaultTheme": "dark",
* "defaultLogScale": false,
* "defaultLayoutId": "1",
* "mainChartStyle": { "upColor":"#ff6b6b", "downColor":"#4dabf7", ... },
* "syncOptions": { "symbol":false, "interval":false, "crosshair":true, ... }
* }
* </pre>
*/
@RestController
@RequestMapping("/app-settings")
@RequiredArgsConstructor
public class AppSettingsController {
private final AppSettingsService service;
// ── GET ───────────────────────────────────────────────────────────────────
/**
* 앱 시작 시 기본 설정 로드.
* DB 에 데이터가 없으면 엔티티 기본값(hardcoded fallback)을 반환한다.
*/
@GetMapping
public ResponseEntity<Map<String, Object>> get(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.get(parseUserId(userIdHeader), deviceId));
}
// ── PUT ───────────────────────────────────────────────────────────────────
/**
* 기본 설정 저장.
* 변경된 필드만 보내도 되며, 없는 키는 기존 값 유지.
*
* <p>프론트엔드에서 호출해야 하는 시점:
* <ul>
* <li>테마 변경 시 (defaultTheme)</li>
* <li>캔들 색상 변경 시 (mainChartStyle)</li>
* <li>레이아웃 변경 시 (defaultLayoutId)</li>
* <li>기본 심볼/타임프레임 변경 시</li>
* <li>동기화 옵션 변경 시 (syncOptions)</li>
* </ul>
*/
@PutMapping
public ResponseEntity<Map<String, Object>> save(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
@RequestBody Map<String, Object> data) {
return ResponseEntity.ok(service.save(parseUserId(userIdHeader), deviceId, data));
}
// ── helpers ───────────────────────────────────────────────────────────────
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,45 @@
package com.goldenchart.controller;
import com.goldenchart.dto.LoginRequest;
import com.goldenchart.dto.LoginResponse;
import com.goldenchart.dto.MenuPermissionsResponse;
import com.goldenchart.service.AuthService;
import com.goldenchart.service.RolePermissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final RolePermissionService rolePermissionService;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest body) {
return ResponseEntity.ok(authService.login(body.getUsername(), body.getPassword()));
}
@GetMapping("/me")
public ResponseEntity<LoginResponse> me(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader) {
Long userId = parseUserId(userIdHeader);
return authService.me(userId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/** 로그인 사용자 역할·메뉴 권한 (미로그인 시 GUEST) */
@GetMapping("/menu-permissions")
public MenuPermissionsResponse menuPermissions(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader) {
return rolePermissionService.resolveForUserId(parseUserId(userIdHeader));
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,46 @@
package com.goldenchart.controller;
import com.goldenchart.entity.GcBacktestResult;
import com.goldenchart.repository.GcBacktestResultRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 백테스팅 실행 이력 조회 API.
*
* <pre>
* GET /backtest-results?deviceId=xxx → 이력 목록 (최신순)
* GET /backtest-results/{id} → 단건 상세 조회
* DELETE /backtest-results/{id} → 삭제
* </pre>
*/
@RestController
@RequestMapping("/backtest-results")
@RequiredArgsConstructor
public class BacktestResultController {
private final GcBacktestResultRepository resultRepository;
@GetMapping
public ResponseEntity<List<GcBacktestResult>> list(
@RequestParam(required = false, defaultValue = "default") String deviceId) {
return ResponseEntity.ok(resultRepository.findByDeviceIdOrderByCreatedAtDesc(deviceId));
}
@GetMapping("/{id}")
public ResponseEntity<GcBacktestResult> get(@PathVariable Long id) {
return resultRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
if (!resultRepository.existsById(id)) return ResponseEntity.notFound().build();
resultRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,29 @@
package com.goldenchart.controller;
import com.goldenchart.dto.BacktestSettingsDto;
import com.goldenchart.service.BacktestSettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/backtest-settings")
@RequiredArgsConstructor
@CrossOrigin(origins = "*")
public class BacktestSettingsController {
private final BacktestSettingsService service;
@GetMapping
public ResponseEntity<BacktestSettingsDto> get(
@RequestHeader(value = "X-Device-Id", defaultValue = "default") String deviceId) {
return ResponseEntity.ok(service.get(deviceId));
}
@PostMapping
public ResponseEntity<BacktestSettingsDto> save(
@RequestHeader(value = "X-Device-Id", defaultValue = "default") String deviceId,
@RequestBody BacktestSettingsDto dto) {
return ResponseEntity.ok(service.save(deviceId, dto));
}
}
@@ -0,0 +1,81 @@
package com.goldenchart.controller;
import com.goldenchart.dto.BacktestRequest;
import com.goldenchart.dto.BacktestResponse;
import com.goldenchart.service.BacktestingService;
import com.goldenchart.service.IndicatorSettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 백테스팅 실행 REST API.
*
* <p>POST /backtesting/run 에 BacktestRequest 를 전송하면
* Ta4j 로 백테스팅을 수행하여 매수/매도 시그널과 통계를 반환합니다.
*/
@RestController
@RequestMapping("/backtesting")
@RequiredArgsConstructor
public class BacktestingController {
private final BacktestingService backtestingService;
private final IndicatorSettingsService indicatorSettingsService;
/**
* 백테스팅 실행.
*
* <p>요청 예시:
* <pre>
* POST /backtesting/run
* {
* "strategyId": 1, // DB 전략 ID (선택)
* "buyCondition": {...}, // DSL 직접 전달 시 (strategyId 없을 때 사용)
* "sellCondition": {...},
* "bars": [{time, open, high, low, close, volume}, ...],
* "timeframe": "1D"
* }
* </pre>
*/
@PostMapping("/run")
public ResponseEntity<BacktestResponse> run(
@RequestBody BacktestRequest req,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
if (req.getBars() == null || req.getBars().isEmpty()) {
return ResponseEntity.badRequest().build();
}
Long userId = parseUserId(userIdHeader);
// DB 저장 파라미터를 기반으로, 요청 params 로 덮어쓰기
// (요청에 params 없으면 DB 값만 사용 → 사용자가 변경한 파라미터 항상 반영)
if (userId != null || deviceId != null) {
java.util.Map<String, java.util.Map<String, Object>> dbParams =
indicatorSettingsService.getAll(userId, deviceId);
if (!dbParams.isEmpty()) {
java.util.Map<String, java.util.Map<String, Object>> merged =
new java.util.HashMap<>(dbParams);
if (req.getIndicatorParams() != null) {
req.getIndicatorParams().forEach((type, params) ->
merged.merge(type, params, (dbP, reqP) -> {
java.util.Map<String, Object> m = new java.util.HashMap<>(dbP);
m.putAll(reqP);
return m;
})
);
}
req.setIndicatorParams(merged);
}
}
BacktestResponse response = backtestingService.run(req);
return ResponseEntity.ok(response);
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,64 @@
package com.goldenchart.controller;
import com.goldenchart.dto.CandleBarDto;
import com.goldenchart.service.HistoricalDataService;
import com.goldenchart.websocket.DynamicSubscriptionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 캔들 데이터 REST API 컨트롤러.
*
* context-path: /api
* endpoint: GET /api/candles/history
*
* 명세서 5.1 포맷:
* Query Params: market=KRW-BTC, type=1m, to=2026-05-20T10:17:00Z, count=200
* Response: CandleBarDto[] (time, open, high, low, close, volume, rsi)
*/
@RestController
@RequestMapping("/candles")
@RequiredArgsConstructor
@Slf4j
public class CandleController {
private final HistoricalDataService historicalDataService;
private final DynamicSubscriptionManager subscriptionManager;
/**
* 과거 캔들 데이터 조회.
*
* @param market 업비트 마켓 코드 (e.g. "KRW-BTC")
* @param type 캔들 타입 (e.g. "1m", "5m", "1h", "1d")
* @param to 기준 시간 이전(exclusive) ISO-8601 (e.g. "2026-05-20T10:00:00" 또는 "...Z")
* @param count 요청 캔들 수 (기본값 200, 최대 200)
* @return 시간 오름차순 CandleBarDto 배열
*/
@GetMapping("/history")
public ResponseEntity<List<CandleBarDto>> getHistory(
@RequestParam String market,
@RequestParam(defaultValue = "1d") String type,
@RequestParam(required = false) String to,
@RequestParam(defaultValue = "200") int count) {
log.info("[CandleController] history 요청: market={}, type={}, to={}, count={}", market, type, to, count);
List<CandleBarDto> bars = historicalDataService.getHistory(market, type, to, count);
return ResponseEntity.ok(bars);
}
/**
* 차트 실시간용 — 업비트 WS·Ta4j warm-up 고정 (STOMP 구독과 병행).
*/
@PostMapping("/watch")
public ResponseEntity<Void> watch(
@RequestParam String market,
@RequestParam(defaultValue = "1m") String type) {
subscriptionManager.ensureMarketPinned(market, type);
subscriptionManager.ensureMarketPinned(market, "1m");
return ResponseEntity.ok().build();
}
}
@@ -0,0 +1,146 @@
package com.goldenchart.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.entity.GcChartSlot;
import com.goldenchart.entity.GcChartWorkspace;
import com.goldenchart.service.ChartSettingsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 차트 워크스페이스 설정 REST API.
*
* GET /api/chart/workspace - 전체 설정 로드 (프론트 시작 시 호출)
* POST /api/chart/workspace - 전체 설정 저장
* PUT /api/chart/workspace/layout - 레이아웃만 변경
* PUT /api/chart/slot/{index} - 특정 슬롯 저장
*/
@RestController
@RequestMapping("/chart")
@RequiredArgsConstructor
@Slf4j
public class ChartSettingsController {
private final ChartSettingsService settingsService;
private final ObjectMapper objectMapper;
/** 헤더에서 userId 또는 deviceId 추출 */
private Long userId(Map<String, String> headers) {
String v = headers.get("x-user-id");
return v != null ? Long.parseLong(v) : null;
}
private String deviceId(Map<String, String> headers) {
return headers.getOrDefault("x-device-id", "anonymous");
}
/**
* 프론트엔드 시작 시 전체 워크스페이스 로드.
* DB에 저장된 설정이 없으면 204 반환 → 프론트는 기본값 사용.
*/
@GetMapping("/workspace")
public ResponseEntity<?> getWorkspace(
@RequestHeader Map<String, String> headers) {
GcChartWorkspace ws = settingsService.getWorkspaceWithSlots(
userId(headers), deviceId(headers));
if (ws == null) return ResponseEntity.noContent().build();
return ResponseEntity.ok(toDto(ws));
}
/**
* 전체 워크스페이스 저장 (자동 저장 / 명시 저장).
*/
@PostMapping("/workspace")
public ResponseEntity<?> saveWorkspace(
@RequestHeader Map<String, String> headers,
@RequestBody Map<String, Object> body) {
Long uid = userId(headers);
String did = deviceId(headers);
String layoutId = (String) body.getOrDefault("layoutId", "1");
String syncJson = body.containsKey("syncOptions")
? toJson(body.get("syncOptions")) : null;
@SuppressWarnings("unchecked")
List<Map<String, Object>> slotsData = (List<Map<String, Object>>) body.get("slots");
GcChartWorkspace ws = settingsService.saveAll(uid, did, layoutId, syncJson,
slotsData != null ? slotsData : List.of());
return ResponseEntity.ok(toDto(ws));
}
/**
* 레이아웃만 변경.
*/
@PutMapping("/workspace/layout")
public ResponseEntity<?> updateLayout(
@RequestHeader Map<String, String> headers,
@RequestBody Map<String, Object> body) {
String layoutId = (String) body.get("layoutId");
String syncJson = body.containsKey("syncOptions") ? toJson(body.get("syncOptions")) : null;
GcChartWorkspace ws = settingsService.saveWorkspace(
userId(headers), deviceId(headers), layoutId, syncJson);
return ResponseEntity.ok(Map.of("layoutId", ws.getLayoutId()));
}
/**
* 특정 슬롯 설정 저장.
*/
@PutMapping("/slot/{slotIndex}")
public ResponseEntity<?> saveSlot(
@RequestHeader Map<String, String> headers,
@PathVariable int slotIndex,
@RequestBody Map<String, Object> body) {
GcChartSlot slot = settingsService.saveSlot(
userId(headers), deviceId(headers), slotIndex, body);
return ResponseEntity.ok(slotToDto(slot));
}
// ── DTO 변환 ────────────────────────────────────────────────────────────────
private Map<String, Object> toDto(GcChartWorkspace ws) {
Map<String, Object> dto = new HashMap<>();
dto.put("id", ws.getId());
dto.put("layoutId", ws.getLayoutId());
dto.put("syncOptions", parseJson(ws.getSyncOptionsJson()));
dto.put("slots", ws.getSlots().stream()
.map(this::slotToDto)
.collect(Collectors.toList()));
return dto;
}
private Map<String, Object> slotToDto(GcChartSlot s) {
Map<String, Object> dto = new HashMap<>();
dto.put("slotIndex", s.getSlotIndex());
dto.put("symbol", s.getSymbol());
dto.put("timeframe", s.getTimeframe());
dto.put("chartType", s.getChartType());
dto.put("theme", s.getTheme());
dto.put("mode", s.getMode());
dto.put("logScale", s.getLogScale());
dto.put("drawingsLocked", s.getDrawingsLocked());
dto.put("drawingsVisible", s.getDrawingsVisible());
dto.put("indicators", parseJson(s.getIndicatorsJson()));
dto.put("drawings", parseJson(s.getDrawingsJson()));
dto.put("paneLayout", parseJson(s.getPaneLayoutJson()));
dto.put("mainChartStyle", parseJson(s.getMainChartStyleJson()));
return dto;
}
private Object parseJson(String json) {
if (json == null) return null;
try { return objectMapper.readValue(json, Object.class); }
catch (Exception e) { return null; }
}
private String toJson(Object obj) {
try { return objectMapper.writeValueAsString(obj); }
catch (Exception e) { return null; }
}
}
@@ -0,0 +1,27 @@
package com.goldenchart.controller;
import com.goldenchart.service.DashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/dashboard")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
@GetMapping("/summary")
public ResponseEntity<Map<String, Object>> summary(@RequestHeader Map<String, String> h) {
Long userId = null;
String uid = h.get("x-user-id");
if (uid != null && !uid.isBlank()) {
try { userId = Long.parseLong(uid); } catch (NumberFormatException ignored) { }
}
String deviceId = h.getOrDefault("x-device-id", "anonymous");
return ResponseEntity.ok(dashboardService.buildSummary(userId, deviceId));
}
}
@@ -0,0 +1,56 @@
package com.goldenchart.controller;
import com.goldenchart.dto.FcmTokenDto;
import com.goldenchart.service.FcmPushService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/fcm")
@RequiredArgsConstructor
public class FcmController {
private final FcmPushService fcmPushService;
private Long userId(Map<String, String> h) {
String v = h.get("x-user-id");
return v != null ? Long.parseLong(v) : null;
}
private String deviceId(Map<String, String> h) {
return h.getOrDefault("x-device-id", "anonymous");
}
@PostMapping("/token")
public ResponseEntity<Void> register(@RequestHeader Map<String, String> h,
@RequestBody FcmTokenDto body) {
fcmPushService.registerToken(
body.getUserId() != null ? body.getUserId() : userId(h),
body.getDeviceId() != null ? body.getDeviceId() : deviceId(h),
body.getToken());
return ResponseEntity.ok().build();
}
@DeleteMapping("/token")
public ResponseEntity<Void> delete(@RequestHeader Map<String, String> h) {
fcmPushService.deleteByDevice(deviceId(h));
return ResponseEntity.ok().build();
}
@PostMapping("/test")
public ResponseEntity<Map<String, Object>> test(@RequestHeader Map<String, String> h) {
fcmPushService.sendTest(userId(h), deviceId(h));
return ResponseEntity.ok(Map.of(
"ok", true,
"available", fcmPushService.isAvailable()
));
}
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> status() {
return ResponseEntity.ok(Map.of("available", fcmPushService.isAvailable()));
}
}
@@ -0,0 +1,101 @@
package com.goldenchart.controller;
import com.goldenchart.dto.IndicatorRequest;
import com.goldenchart.dto.IndicatorResponse;
import com.goldenchart.service.IndicatorService;
import com.goldenchart.service.IndicatorSettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 지표 계산 REST API.
*
* <p>파라미터 우선순위 (높은 순):
* <ol>
* <li>요청 body 의 params (프론트엔드가 명시적으로 보낸 값)</li>
* <li>DB gc_indicator_settings 에 저장된 사용자 설정값</li>
* <li>IndicatorService 내부 코드 기본값 (최후 fallback)</li>
* </ol>
* 이 순서로 병합하여 프론트엔드와 백엔드가 항상 동일한 파라미터로 계산된다.
* </p>
*/
@RestController
@RequestMapping("/indicators")
@RequiredArgsConstructor
public class IndicatorController {
private final IndicatorService indicatorService;
private final IndicatorSettingsService settingsService;
/**
* 지표값 계산.
*
* <p>헤더:
* <ul>
* <li>X-Device-Id — 비회원 기기 식별자</li>
* <li>X-User-Id — 회원 ID (선택)</li>
* </ul>
*/
@PostMapping("/calculate")
public ResponseEntity<IndicatorResponse> calculate(
@RequestBody IndicatorRequest req,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
if (req.getBars() == null || req.getBars().isEmpty()) {
return ResponseEntity.badRequest().build();
}
Long userId = parseUserId(userIdHeader);
/*
* DB 설정값 + 요청 파라미터 병합.
* - DB 값 기반으로 시작
* - 요청 params 는 DB 값 위에 덮어쓰기 (명시적 전송이 더 최신)
*/
Map<String, Object> mergedParams = settingsService.mergeWithDb(
userId, deviceId,
req.getParams(),
req.getIndicatorType());
IndicatorResponse response = indicatorService.calculate(
req.getBars(), req.getIndicatorType(), mergedParams, req.getTimeframe());
response.setSymbol(req.getSymbol());
response.setTimeframe(req.getTimeframe());
return ResponseEntity.ok(response);
}
/**
* 지원 지표 목록 반환.
* frontend indicatorRegistry.ts 의 INDICATOR_REGISTRY 와 동기화.
*/
@GetMapping("/list")
public ResponseEntity<?> list() {
return ResponseEntity.ok(java.util.Map.of(
"supported", java.util.List.of(
"SMA", "EMA", "WMA", "HMA", "VWMA", "DEMA", "TEMA", "RMA", "SMMA", "LSMA", "MACross",
"BollingerBands", "KeltnerChannels", "DonchianChannels", "BBPercentB", "BBBandWidth",
"RSI", "Stochastic", "StochRSI", "CCI", "WilliamsPercentRange",
"AwesomeOscillator", "DPO",
"MACD", "Momentum", "ROC", "TSI",
"ADX", "DMI", "Aroon", "IchimokuCloud", "ParabolicSAR",
"Choppiness", "MassIndex",
"ATR", "StandardDeviation", "HistoricalVolatility",
"OBV", "MFI", "ChaikinMF",
"VolumeOscillator", "VR", "Disparity",
"Psychological", "InvestPsychological",
"TRIX", "ADX", "DMI"
)
));
}
// ── helpers ───────────────────────────────────────────────────────────────
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,137 @@
package com.goldenchart.controller;
import com.goldenchart.service.IndicatorSettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 전역 지표 파라미터 CRUD REST API.
*
* <p>프론트엔드 indicatorRegistry.ts 의 defaultParams 를 완전히 대체하는
* 사용자별 지표 파라미터를 저장/조회한다.</p>
*
* <pre>
* GET /indicator-settings — 전체 지표 파라미터 조회
* GET /indicator-settings/{type} — 특정 지표 파라미터 조회
* PUT /indicator-settings — 전체 지표 파라미터 저장 (덮어쓰기)
* PATCH /indicator-settings/{type} — 특정 지표 파라미터 병합 저장
* </pre>
*/
@RestController
@RequestMapping("/indicator-settings")
@RequiredArgsConstructor
public class IndicatorSettingsController {
private final IndicatorSettingsService service;
// ── 헤더 파싱 헬퍼 ────────────────────────────────────────────────────────
private Long userId(String userIdHeader) {
if (userIdHeader == null || userIdHeader.isBlank()) return null;
try { return Long.parseLong(userIdHeader); } catch (NumberFormatException e) { return null; }
}
// ── GET: 전체 파라미터 조회 ────────────────────────────────────────────────
/**
* 이 장치/사용자의 모든 지표 파라미터를 반환.
* 프론트엔드 앱 시작 시 호출하여 전역 설정을 로드한다.
*/
@GetMapping
public ResponseEntity<Map<String, Map<String, Object>>> getAll(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
Map<String, Map<String, Object>> result = service.getAll(userId(userIdHeader), deviceId);
return ResponseEntity.ok(result);
}
// ── GET: 특정 지표 파라미터 조회 ──────────────────────────────────────────
@GetMapping("/{indicatorType}")
public ResponseEntity<Map<String, Object>> getForType(
@PathVariable String indicatorType,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.getForType(userId(userIdHeader), deviceId, indicatorType));
}
// ── PUT: 전체 파라미터 저장 ────────────────────────────────────────────────
/**
* 모든 지표 파라미터를 한 번에 저장(덮어쓰기).
*
* Request body:
* {
* "RSI": {"length": 9, "src": "close"},
* "MACD": {"fastLength": 12, "slowLength": 26, "signalLength": 9},
* "BollingerBands":{"length": 20, "mult": 2.0},
* ...
* }
*/
@PutMapping
public ResponseEntity<Void> saveAll(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
@RequestBody Map<String, Map<String, Object>> allParams) {
service.saveAll(userId(userIdHeader), deviceId, allParams);
return ResponseEntity.noContent().build();
}
// ── PATCH: 특정 지표 파라미터 병합 저장 ────────────────────────────────────
/**
* 특정 지표의 파라미터만 업데이트.
* 나머지 지표 설정은 유지된다.
*
* PATCH /indicator-settings/RSI
* Body: {"length": 14, "src": "close"}
*/
@PatchMapping("/{indicatorType}")
public ResponseEntity<Void> saveForType(
@PathVariable String indicatorType,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
@RequestBody Map<String, Object> params) {
service.saveForType(userId(userIdHeader), deviceId, indicatorType, params);
return ResponseEntity.noContent().build();
}
// ── GET: 전체 시각 설정 조회 ───────────────────────────────────────────────
/**
* 모든 지표의 시각 설정(색상·선굵기·수평선)을 반환.
* 프론트엔드 앱 시작 시 호출하여 전역 시각 설정을 로드한다.
*
* GET /indicator-settings/visual
*/
@GetMapping("/visual")
public ResponseEntity<Map<String, Map<String, Object>>> getAllVisual(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.getAllVisual(userId(userIdHeader), deviceId));
}
// ── PATCH: 특정 지표 시각 설정 저장 ───────────────────────────────────────
/**
* 특정 지표의 시각 설정(색상·선굵기·수평선)을 병합 저장.
*
* PATCH /indicator-settings/RSI/visual
* Body: {
* "plots": [{"id":"plot0","color":"#ff0000","lineWidth":2,"type":"line"}],
* "hlines": [{"price":70,"color":"#EF5350","visible":true}]
* }
*/
@PatchMapping("/{indicatorType}/visual")
public ResponseEntity<Void> saveVisualForType(
@PathVariable String indicatorType,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId,
@RequestBody Map<String, Object> visual) {
service.saveVisualForType(userId(userIdHeader), deviceId, indicatorType, visual);
return ResponseEntity.noContent().build();
}
}
@@ -0,0 +1,68 @@
package com.goldenchart.controller;
import com.goldenchart.dto.LiveStrategyBulkRequest;
import com.goldenchart.dto.LiveStrategySettingsDto;
import com.goldenchart.service.LiveStrategySettingsService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 실시간 전략 체크 설정 REST API.
*
* <pre>
* GET /api/strategy/settings?market=KRW-BTC → 현재 설정 조회
* PUT /api/strategy/settings → 설정 저장/갱신
* </pre>
*/
@RestController
@RequestMapping("/strategy/settings")
@RequiredArgsConstructor
public class LiveStrategySettingsController {
private final LiveStrategySettingsService service;
@GetMapping
public ResponseEntity<LiveStrategySettingsDto> get(
@RequestParam(value = "market", defaultValue = "KRW-BTC") String market,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.get(parseUserId(userIdHeader), deviceId, market));
}
@PutMapping
public ResponseEntity<LiveStrategySettingsDto> put(
@RequestBody LiveStrategySettingsDto dto,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.save(parseUserId(userIdHeader), deviceId, dto));
}
/** 실시간 체크 ON + 전략 지정된 종목 목록 (현재 디바이스/유저) */
@GetMapping("/active")
public ResponseEntity<List<LiveStrategySettingsDto>> listActive(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.listActive(parseUserId(userIdHeader), deviceId));
}
/** 관심종목 등 여러 마켓에 동일 설정 일괄 저장 */
@PutMapping("/bulk")
public ResponseEntity<List<LiveStrategySettingsDto>> putBulk(
@RequestBody LiveStrategyBulkRequest req,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.saveBulk(parseUserId(userIdHeader), deviceId, req));
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,47 @@
package com.goldenchart.controller;
import com.goldenchart.dto.LiveOrderRequest;
import com.goldenchart.dto.LiveOrderRequest;
import com.goldenchart.dto.LiveSummaryDto;
import com.goldenchart.dto.LiveTradeDto;
import com.goldenchart.service.LiveTradingService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/live")
@RequiredArgsConstructor
public class LiveTradingController {
private final LiveTradingService liveTradingService;
@GetMapping("/summary")
public ResponseEntity<LiveSummaryDto> summary(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(liveTradingService.getSummary(userId(h), deviceId(h)));
}
@GetMapping("/trades")
public ResponseEntity<List<LiveTradeDto>> trades(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(liveTradingService.listTrades(userId(h), deviceId(h)));
}
@PostMapping("/orders")
public ResponseEntity<LiveTradeDto> placeOrder(@RequestHeader Map<String, String> h,
@RequestBody LiveOrderRequest body) {
return ResponseEntity.ok(liveTradingService.placeManualOrder(userId(h), deviceId(h), body));
}
private Long userId(Map<String, String> h) {
String v = h.get("x-user-id");
if (v == null || v.isBlank()) return null;
try { return Long.parseLong(v); } catch (NumberFormatException e) { return null; }
}
private String deviceId(Map<String, String> h) {
return h.getOrDefault("x-device-id", "anonymous");
}
}
@@ -0,0 +1,58 @@
package com.goldenchart.controller;
import com.goldenchart.dto.PaperOrderRequest;
import com.goldenchart.dto.PaperSummaryDto;
import com.goldenchart.dto.PaperTradeDto;
import com.goldenchart.service.PaperTradingService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/paper")
@RequiredArgsConstructor
public class PaperTradingController {
private final PaperTradingService paperTradingService;
private Long userId(Map<String, String> h) {
String v = h.get("x-user-id");
return v != null ? Long.parseLong(v) : null;
}
private String deviceId(Map<String, String> h) {
return h.getOrDefault("x-device-id", "anonymous");
}
@GetMapping("/summary")
public ResponseEntity<PaperSummaryDto> summary(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), null));
}
@PostMapping("/summary")
public ResponseEntity<PaperSummaryDto> summaryWithMarks(
@RequestHeader Map<String, String> h,
@RequestBody(required = false) Map<String, Double> markPrices) {
return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), markPrices));
}
@GetMapping("/trades")
public ResponseEntity<List<PaperTradeDto>> trades(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.listTrades(userId(h), deviceId(h)));
}
@PostMapping("/orders")
public ResponseEntity<PaperTradeDto> placeOrder(
@RequestHeader Map<String, String> h,
@RequestBody PaperOrderRequest body) {
return ResponseEntity.ok(paperTradingService.placeOrder(userId(h), deviceId(h), body));
}
@PostMapping("/reset")
public ResponseEntity<PaperSummaryDto> reset(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(paperTradingService.resetAccount(userId(h), deviceId(h)));
}
}
@@ -0,0 +1,63 @@
package com.goldenchart.controller;
import com.goldenchart.dto.StrategyDto;
import com.goldenchart.service.StrategyService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 투자전략 CRUD REST API.
*
* <p>헤더:
* <ul>
* <li>X-Device-Id — 비회원 기기 식별자 (필수)</li>
* <li>X-User-Id — 회원 ID (선택)</li>
* </ul>
*/
@RestController
@RequestMapping("/strategies")
@RequiredArgsConstructor
public class StrategyController {
private final StrategyService service;
/** 전략 목록 조회 */
@GetMapping
public ResponseEntity<List<StrategyDto>> list(
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.list(parseUserId(userIdHeader), deviceId));
}
/** 단일 전략 조회 */
@GetMapping("/{id}")
public ResponseEntity<StrategyDto> get(@PathVariable Long id) {
return service.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/** 전략 저장 (id 없으면 생성, 있으면 수정) */
@PostMapping
public ResponseEntity<StrategyDto> save(
@RequestBody StrategyDto dto,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
return ResponseEntity.ok(service.save(dto, parseUserId(userIdHeader), deviceId));
}
/** 전략 삭제 */
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
private Long parseUserId(String header) {
if (header == null || header.isBlank()) return null;
try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,43 @@
package com.goldenchart.controller;
import com.goldenchart.dto.TradeSignalDto;
import com.goldenchart.service.TradeSignalService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 매매 시그널 이력 REST API.
*
* <pre>
* GET /api/trade-signals → 전체 이력
* GET /api/trade-signals?market= → 종목별 이력
* </pre>
*/
@RestController
@RequestMapping("/trade-signals")
@RequiredArgsConstructor
public class TradeSignalController {
private final TradeSignalService service;
@GetMapping
public ResponseEntity<List<TradeSignalDto>> list(
@RequestParam(required = false) String market,
@RequestHeader(value = "X-User-Id", required = false) String userIdHeader,
@RequestHeader(value = "X-Device-Id", required = false) String deviceId) {
Long userId = parseUserId(userIdHeader);
List<TradeSignalDto> result = (market != null && !market.isBlank())
? service.listByMarket(userId, deviceId, market)
: service.list(userId, deviceId);
return ResponseEntity.ok(result);
}
private Long parseUserId(String h) {
if (h == null || h.isBlank()) return null;
try { return Long.parseLong(h); } catch (NumberFormatException e) { return null; }
}
}
@@ -0,0 +1,133 @@
package com.goldenchart.controller;
import com.goldenchart.entity.GcWatchlist;
import com.goldenchart.entity.GcHoldings;
import com.goldenchart.repository.GcHoldingsRepository;
import com.goldenchart.repository.GcWatchlistRepository;
import com.goldenchart.service.WatchlistService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 관심종목 / 보유종목 REST API.
*
* <p>관심종목 추가/삭제 시 {@link WatchlistService} 가 실시간 전략 체크 대상과 자동 연동한다.
*/
@RestController
@RequiredArgsConstructor
@Transactional
public class WatchlistController {
private final WatchlistService watchlistService;
private final GcWatchlistRepository watchlistRepo;
private final GcHoldingsRepository holdingsRepo;
private Long userId(Map<String, String> h) {
String v = h.get("x-user-id"); return v != null ? Long.parseLong(v) : null;
}
private String deviceId(Map<String, String> h) {
return h.getOrDefault("x-device-id", "anonymous");
}
// ── 관심종목 ──────────────────────────────────────────────────────────────
@GetMapping("/watchlist")
@Transactional(readOnly = true)
public ResponseEntity<List<GcWatchlist>> getWatchlist(@RequestHeader Map<String, String> h) {
return ResponseEntity.ok(watchlistService.list(userId(h), deviceId(h)));
}
@PostMapping("/watchlist")
public ResponseEntity<GcWatchlist> addWatchlist(
@RequestHeader Map<String, String> h,
@RequestBody Map<String, Object> body) {
Long uid = userId(h);
String symbol = (String) body.get("symbol");
GcWatchlist item = watchlistService.add(
uid,
deviceId(h),
symbol,
(String) body.get("koreanName"),
(String) body.get("englishName"),
body.containsKey("displayOrder") ? (Integer) body.get("displayOrder") : null
);
return ResponseEntity.ok(item);
}
@DeleteMapping("/watchlist/{symbol}")
public ResponseEntity<Void> removeWatchlist(
@RequestHeader Map<String, String> h,
@PathVariable String symbol) {
watchlistService.remove(userId(h), deviceId(h), symbol);
return ResponseEntity.noContent().build();
}
@PutMapping("/watchlist/order")
public ResponseEntity<Void> reorderWatchlist(
@RequestHeader Map<String, String> h,
@RequestBody List<Map<String, Object>> items) {
items.forEach(item -> {
String symbol = (String) item.get("symbol");
int order = (Integer) item.get("displayOrder");
Long uid = userId(h);
watchlistService.list(uid, deviceId(h)).stream()
.filter(w -> w.getSymbol().equals(symbol))
.findFirst()
.ifPresent(w -> { w.setDisplayOrder(order); watchlistRepo.save(w); });
});
return ResponseEntity.ok().build();
}
// ── 보유종목 ──────────────────────────────────────────────────────────────
@GetMapping("/holdings")
@Transactional(readOnly = true)
public ResponseEntity<List<GcHoldings>> getHoldings(@RequestHeader Map<String, String> h) {
Long uid = userId(h);
return ResponseEntity.ok(uid != null
? holdingsRepo.findByUserIdOrderByDisplayOrderAsc(uid)
: holdingsRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId(h)));
}
@PostMapping("/holdings")
public ResponseEntity<GcHoldings> addHoldings(
@RequestHeader Map<String, String> h,
@RequestBody Map<String, Object> body) {
Long uid = userId(h);
String did = uid == null ? deviceId(h) : null;
String symbol = (String) body.get("symbol");
GcHoldings item = (uid != null
? holdingsRepo.findByUserIdOrderByDisplayOrderAsc(uid)
: holdingsRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId(h)))
.stream().filter(hld -> hld.getSymbol().equals(symbol)).findFirst()
.orElseGet(() -> GcHoldings.builder().userId(uid).deviceId(did).symbol(symbol).build());
item.setKoreanName((String) body.get("koreanName"));
item.setEnglishName((String) body.get("englishName"));
if (body.containsKey("avgPrice"))
item.setAvgPrice(new BigDecimal(body.get("avgPrice").toString()));
if (body.containsKey("quantity"))
item.setQuantity(new BigDecimal(body.get("quantity").toString()));
if (body.containsKey("displayOrder"))
item.setDisplayOrder((Integer) body.get("displayOrder"));
return ResponseEntity.ok(holdingsRepo.save(item));
}
@DeleteMapping("/holdings/{symbol}")
public ResponseEntity<Void> removeHoldings(
@RequestHeader Map<String, String> h,
@PathVariable String symbol) {
Long uid = userId(h);
if (uid != null) holdingsRepo.deleteByUserIdAndSymbol(uid, symbol);
else holdingsRepo.deleteByDeviceIdAndSymbol(deviceId(h), symbol);
return ResponseEntity.noContent().build();
}
}