goldenChat base source add
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user