diff --git a/backend/src/main/java/com/goldenchart/config/LiveStrategyStartupRunner.java b/backend/src/main/java/com/goldenchart/config/LiveStrategyStartupRunner.java index 6fc875b..343362e 100644 --- a/backend/src/main/java/com/goldenchart/config/LiveStrategyStartupRunner.java +++ b/backend/src/main/java/com/goldenchart/config/LiveStrategyStartupRunner.java @@ -40,8 +40,8 @@ public class LiveStrategyStartupRunner implements ApplicationRunner { for (GcWatchlist w : watchlistRepo.findAll()) { Long userId = w.getUserId(); String deviceId = w.getDeviceId(); - if (userId == null && deviceId == null) continue; - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + if (userId == null) continue; + GcAppSettings app = appSettingsService.getEntity(userId, null); if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) || app.getLiveStrategyId() == null) { continue; } diff --git a/backend/src/main/java/com/goldenchart/controller/BacktestingController.java b/backend/src/main/java/com/goldenchart/controller/BacktestingController.java index e70783c..e347019 100644 --- a/backend/src/main/java/com/goldenchart/controller/BacktestingController.java +++ b/backend/src/main/java/com/goldenchart/controller/BacktestingController.java @@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.Map; + /** * 백테스팅 실행 REST API. * @@ -40,42 +42,32 @@ public class BacktestingController { @PostMapping("/run") public ResponseEntity run( @RequestBody BacktestRequest req, - @RequestHeader(value = "X-User-Id", required = false) String userIdHeader, - @RequestHeader(value = "X-Device-Id", required = false) String deviceId) { + @RequestHeader Map headers) { if (req.getBars() == null || req.getBars().isEmpty()) { return ResponseEntity.badRequest().build(); } - Long userId = parseUserId(userIdHeader); + long userId = TradingControllerSupport.requireRegisteredUser(headers); - // DB 저장 파라미터를 기반으로, 요청 params 로 덮어쓰기 - // (요청에 params 없으면 DB 값만 사용 → 사용자가 변경한 파라미터 항상 반영) - if (userId != null || deviceId != null) { - java.util.Map> dbParams = - indicatorSettingsService.getAll(userId, deviceId); - if (!dbParams.isEmpty()) { - java.util.Map> merged = - new java.util.HashMap<>(dbParams); - if (req.getIndicatorParams() != null) { - req.getIndicatorParams().forEach((type, params) -> - merged.merge(type, params, (dbP, reqP) -> { - java.util.Map m = new java.util.HashMap<>(dbP); - m.putAll(reqP); - return m; - }) - ); - } - req.setIndicatorParams(merged); + java.util.Map> dbParams = + indicatorSettingsService.getAll(userId, null); + if (!dbParams.isEmpty()) { + java.util.Map> merged = + new java.util.HashMap<>(dbParams); + if (req.getIndicatorParams() != null) { + req.getIndicatorParams().forEach((type, params) -> + merged.merge(type, params, (dbP, reqP) -> { + java.util.Map 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; } - } } diff --git a/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java b/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java index 4557ffe..e96a1d7 100644 --- a/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java +++ b/backend/src/main/java/com/goldenchart/controller/LiveConditionController.java @@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.Map; + /** * 가상투자 — 실시간 조건 충족 현황 API. * @@ -22,14 +24,8 @@ public class LiveConditionController { public ResponseEntity get( @RequestParam String market, @RequestParam long strategyId, - @RequestHeader(value = "X-User-Id", required = false) String userIdHeader, - @RequestHeader(value = "X-Device-Id", required = false) String deviceId) { - - return ResponseEntity.ok(service.evaluate(parseUserId(userIdHeader), deviceId, market, strategyId)); - } - - private Long parseUserId(String header) { - if (header == null || header.isBlank()) return null; - try { return Long.parseLong(header); } catch (NumberFormatException e) { return null; } + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.evaluate(uid, null, market, strategyId)); } } diff --git a/backend/src/main/java/com/goldenchart/controller/LiveStrategySettingsController.java b/backend/src/main/java/com/goldenchart/controller/LiveStrategySettingsController.java index aa08358..f8f9267 100644 --- a/backend/src/main/java/com/goldenchart/controller/LiveStrategySettingsController.java +++ b/backend/src/main/java/com/goldenchart/controller/LiveStrategySettingsController.java @@ -8,15 +8,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; -/** - * 실시간 전략 체크 설정 REST API. - * - *
- * GET  /api/strategy/settings?market=KRW-BTC   → 현재 설정 조회
- * PUT  /api/strategy/settings                  → 설정 저장/갱신
- * 
- */ @RestController @RequestMapping("/strategy/settings") @RequiredArgsConstructor @@ -27,42 +20,31 @@ public class LiveStrategySettingsController { @GetMapping public ResponseEntity 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)); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.get(uid, null, market)); } @PutMapping public ResponseEntity 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)); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.save(uid, null, dto)); } - /** 실시간 체크 ON + 전략 지정된 종목 목록 (현재 디바이스/유저) */ @GetMapping("/active") public ResponseEntity> 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)); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.listActive(uid, null)); } - /** 관심종목 등 여러 마켓에 동일 설정 일괄 저장 */ @PutMapping("/bulk") public ResponseEntity> 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; } + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.saveBulk(uid, null, req)); } } diff --git a/backend/src/main/java/com/goldenchart/controller/LiveTradingController.java b/backend/src/main/java/com/goldenchart/controller/LiveTradingController.java index 56b5406..0c4705c 100644 --- a/backend/src/main/java/com/goldenchart/controller/LiveTradingController.java +++ b/backend/src/main/java/com/goldenchart/controller/LiveTradingController.java @@ -1,6 +1,5 @@ package com.goldenchart.controller; -import com.goldenchart.dto.LiveOrderRequest; import com.goldenchart.dto.LiveOrderRequest; import com.goldenchart.dto.LiveSummaryDto; import com.goldenchart.dto.LiveTradeDto; @@ -21,27 +20,20 @@ public class LiveTradingController { @GetMapping("/summary") public ResponseEntity summary(@RequestHeader Map h) { - return ResponseEntity.ok(liveTradingService.getSummary(userId(h), deviceId(h))); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(liveTradingService.getSummary(uid)); } @GetMapping("/trades") public ResponseEntity> trades(@RequestHeader Map h) { - return ResponseEntity.ok(liveTradingService.listTrades(userId(h), deviceId(h))); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(liveTradingService.listTrades(uid)); } @PostMapping("/orders") public ResponseEntity placeOrder(@RequestHeader Map h, @RequestBody LiveOrderRequest body) { - return ResponseEntity.ok(liveTradingService.placeManualOrder(userId(h), deviceId(h), body)); - } - - private Long userId(Map 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 h) { - return h.getOrDefault("x-device-id", "anonymous"); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(liveTradingService.placeManualOrder(uid, body)); } } diff --git a/backend/src/main/java/com/goldenchart/controller/PaperTradingController.java b/backend/src/main/java/com/goldenchart/controller/PaperTradingController.java index 7a253d3..4470b61 100644 --- a/backend/src/main/java/com/goldenchart/controller/PaperTradingController.java +++ b/backend/src/main/java/com/goldenchart/controller/PaperTradingController.java @@ -19,37 +19,32 @@ public class PaperTradingController { private final PaperTradingService paperTradingService; - private Long userId(Map h) { - String v = h.get("x-user-id"); - return v != null ? Long.parseLong(v) : null; - } - - private String deviceId(Map h) { - return h.getOrDefault("x-device-id", "anonymous"); - } - @GetMapping("/summary") public ResponseEntity summary(@RequestHeader Map h) { - return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), null)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.getSummary(uid, null)); } @PostMapping("/summary") public ResponseEntity summaryWithMarks( @RequestHeader Map h, @RequestBody(required = false) Map markPrices) { - return ResponseEntity.ok(paperTradingService.getSummary(userId(h), deviceId(h), markPrices)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.getSummary(uid, markPrices)); } @GetMapping("/allocations") public ResponseEntity> allocations(@RequestHeader Map h) { - return ResponseEntity.ok(paperTradingService.listAllocations(userId(h), deviceId(h), null)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.listAllocations(uid, null)); } @PutMapping("/allocations/bulk") public ResponseEntity> bulkAllocations( @RequestHeader Map h, @RequestBody PaperAllocationBulkRequest body) { - return ResponseEntity.ok(paperTradingService.bulkAllocations(userId(h), deviceId(h), body)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.bulkAllocations(uid, body)); } @PatchMapping("/allocations/{symbol}") @@ -57,7 +52,8 @@ public class PaperTradingController { @RequestHeader Map h, @PathVariable String symbol, @RequestBody PaperAllocationPatchRequest body) { - return ResponseEntity.ok(paperTradingService.patchAllocation(userId(h), deviceId(h), symbol, body)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.patchAllocation(uid, symbol, body)); } @GetMapping("/trades") @@ -70,12 +66,13 @@ public class PaperTradingController { @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to, @RequestParam(defaultValue = "0") int page, @RequestParam(required = false) Integer size) { + long uid = TradingControllerSupport.requireRegisteredUser(h); if (size == null && symbol == null && side == null && source == null && from == null && to == null) { - return ResponseEntity.ok(paperTradingService.listTradesLegacy(userId(h), deviceId(h))); + return ResponseEntity.ok(paperTradingService.listTradesLegacy(uid)); } int pageSize = size != null ? size : 50; return ResponseEntity.ok(paperTradingService.listTrades( - userId(h), deviceId(h), symbol, side, source, from, to, page, pageSize)); + uid, symbol, side, source, from, to, page, pageSize)); } @GetMapping("/ledger") @@ -85,7 +82,8 @@ public class PaperTradingController { @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "50") int size) { - return ResponseEntity.ok(paperTradingService.listLedger(userId(h), deviceId(h), from, to, page, size)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.listLedger(uid, from, to, page, size)); } @GetMapping("/snapshots/daily") @@ -93,30 +91,35 @@ public class PaperTradingController { @RequestHeader Map h, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to) { - return ResponseEntity.ok(paperTradingService.listDailySnapshots(userId(h), deviceId(h), from, to)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.listDailySnapshots(uid, from, to)); } @GetMapping("/orders") public ResponseEntity> pendingOrders(@RequestHeader Map h) { - return ResponseEntity.ok(paperTradingService.listPendingOrders(userId(h), deviceId(h))); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.listPendingOrders(uid)); } @PostMapping("/orders") public ResponseEntity placeOrder( @RequestHeader Map h, @RequestBody PaperOrderRequest body) { - return ResponseEntity.ok(paperTradingService.placeOrder(userId(h), deviceId(h), body)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.placeOrder(uid, body)); } @PostMapping("/orders/{id}/cancel") public ResponseEntity cancelOrder( @RequestHeader Map h, @PathVariable Long id) { - return ResponseEntity.ok(paperTradingService.cancelOrder(userId(h), deviceId(h), id)); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.cancelOrder(uid, id)); } @PostMapping("/reset") public ResponseEntity reset(@RequestHeader Map h) { - return ResponseEntity.ok(paperTradingService.resetAccount(userId(h), deviceId(h))); + long uid = TradingControllerSupport.requireRegisteredUser(h); + return ResponseEntity.ok(paperTradingService.resetAccount(uid)); } } diff --git a/backend/src/main/java/com/goldenchart/controller/StrategyController.java b/backend/src/main/java/com/goldenchart/controller/StrategyController.java index 81b8c97..0a512cb 100644 --- a/backend/src/main/java/com/goldenchart/controller/StrategyController.java +++ b/backend/src/main/java/com/goldenchart/controller/StrategyController.java @@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; /** * 투자전략 CRUD REST API. @@ -29,14 +30,17 @@ public class StrategyController { /** 전략 목록 조회 */ @GetMapping public ResponseEntity> 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)); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.list(uid, null)); } /** 단일 전략 조회 */ @GetMapping("/{id}") - public ResponseEntity get(@PathVariable Long id) { + public ResponseEntity get( + @PathVariable Long id, + @RequestHeader Map headers) { + TradingControllerSupport.requireRegisteredUser(headers); return service.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -46,20 +50,26 @@ public class StrategyController { @PostMapping public ResponseEntity 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)); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + return ResponseEntity.ok(service.save(dto, uid, null)); } /** 전략 DSL에 포함된 평가 시간봉 목록 */ @GetMapping("/{id}/timeframes") - public ResponseEntity> timeframes(@PathVariable Long id) { + public ResponseEntity> timeframes( + @PathVariable Long id, + @RequestHeader Map headers) { + TradingControllerSupport.requireRegisteredUser(headers); return ResponseEntity.ok(conditionTimeframes.collectForStrategyList(id)); } /** TIMEFRAME 래핑 누락 DSL 보정 (3분봉 전략이 1분봉으로 평가되는 경우) */ @PostMapping("/{id}/repair-timeframes") - public ResponseEntity repairTimeframes(@PathVariable Long id) { + public ResponseEntity repairTimeframes( + @PathVariable Long id, + @RequestHeader Map headers) { + TradingControllerSupport.requireRegisteredUser(headers); return service.repairTimeframeWrappers(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); @@ -67,13 +77,11 @@ public class StrategyController { /** 전략 삭제 */ @DeleteMapping("/{id}") - public ResponseEntity delete(@PathVariable Long id) { + public ResponseEntity delete( + @PathVariable Long id, + @RequestHeader Map headers) { + TradingControllerSupport.requireRegisteredUser(headers); 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; } - } } diff --git a/backend/src/main/java/com/goldenchart/controller/TradeSignalController.java b/backend/src/main/java/com/goldenchart/controller/TradeSignalController.java index 0b8cad7..ea1e86c 100644 --- a/backend/src/main/java/com/goldenchart/controller/TradeSignalController.java +++ b/backend/src/main/java/com/goldenchart/controller/TradeSignalController.java @@ -7,18 +7,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; -/** - * 매매 시그널 이력 REST API. - * - *
- * GET    /api/trade-signals              → 전체 이력
- * GET    /api/trade-signals?market=      → 종목별 이력
- * DELETE /api/trade-signals/{id}         → 단건 삭제
- * DELETE /api/trade-signals              → 전체 삭제
- * POST   /api/trade-signals/delete-batch → ID 목록 삭제
- * 
- */ @RestController @RequestMapping("/trade-signals") @RequiredArgsConstructor @@ -29,44 +19,36 @@ public class TradeSignalController { @GetMapping public ResponseEntity> 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); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); List result = (market != null && !market.isBlank()) - ? service.listByMarket(userId, deviceId, market) - : service.list(userId, deviceId); + ? service.listByMarket(uid, null, market) + : service.list(uid, null); return ResponseEntity.ok(result); } @DeleteMapping("/{id}") public ResponseEntity deleteOne( @PathVariable Long id, - @RequestHeader(value = "X-User-Id", required = false) String userIdHeader, - @RequestHeader(value = "X-Device-Id", required = false) String deviceId) { - service.deleteOne(id, parseUserId(userIdHeader), deviceId); + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + service.deleteOne(id, uid, null); return ResponseEntity.noContent().build(); } @DeleteMapping - public ResponseEntity> deleteAll( - @RequestHeader(value = "X-User-Id", required = false) String userIdHeader, - @RequestHeader(value = "X-Device-Id", required = false) String deviceId) { - int deleted = service.deleteAll(parseUserId(userIdHeader), deviceId); - return ResponseEntity.ok(java.util.Map.of("deleted", deleted)); + public ResponseEntity> deleteAll(@RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + int deleted = service.deleteAll(uid, null); + return ResponseEntity.ok(Map.of("deleted", deleted)); } @PostMapping("/delete-batch") - public ResponseEntity> deleteBatch( - @RequestBody java.util.List ids, - @RequestHeader(value = "X-User-Id", required = false) String userIdHeader, - @RequestHeader(value = "X-Device-Id", required = false) String deviceId) { - int deleted = service.deleteBatch(ids, parseUserId(userIdHeader), deviceId); - return ResponseEntity.ok(java.util.Map.of("deleted", deleted)); - } - - private Long parseUserId(String h) { - if (h == null || h.isBlank()) return null; - try { return Long.parseLong(h); } catch (NumberFormatException e) { return null; } + public ResponseEntity> deleteBatch( + @RequestBody List ids, + @RequestHeader Map headers) { + long uid = TradingControllerSupport.requireRegisteredUser(headers); + int deleted = service.deleteBatch(ids, uid, null); + return ResponseEntity.ok(Map.of("deleted", deleted)); } } diff --git a/backend/src/main/java/com/goldenchart/controller/TradingControllerSupport.java b/backend/src/main/java/com/goldenchart/controller/TradingControllerSupport.java new file mode 100644 index 0000000..6a75d90 --- /dev/null +++ b/backend/src/main/java/com/goldenchart/controller/TradingControllerSupport.java @@ -0,0 +1,33 @@ +package com.goldenchart.controller; + +import com.goldenchart.trading.TradingAccess; + +import java.util.Map; + +/** 매매·모의·전략 API — 정식 로그인(userId) 필수 */ +public final class TradingControllerSupport { + + private TradingControllerSupport() {} + + public static long requireRegisteredUser(Map headers) { + String raw = headers.get("x-user-id"); + if (raw == null || raw.isBlank()) { + throw new com.goldenchart.trading.TradingAccessDeniedException(); + } + try { + return TradingAccess.requireUserId(Long.parseLong(raw.trim())); + } catch (NumberFormatException e) { + throw new com.goldenchart.trading.TradingAccessDeniedException(); + } + } + + public static Long parseUserId(Map headers) { + String raw = headers.get("x-user-id"); + if (raw == null || raw.isBlank()) return null; + try { + return Long.parseLong(raw.trim()); + } catch (NumberFormatException e) { + return null; + } + } +} diff --git a/backend/src/main/java/com/goldenchart/repository/GcLiveStrategySettingsRepository.java b/backend/src/main/java/com/goldenchart/repository/GcLiveStrategySettingsRepository.java index e0fd2cd..e8e7d16 100644 --- a/backend/src/main/java/com/goldenchart/repository/GcLiveStrategySettingsRepository.java +++ b/backend/src/main/java/com/goldenchart/repository/GcLiveStrategySettingsRepository.java @@ -18,6 +18,9 @@ public interface GcLiveStrategySettingsRepository /** isLiveCheck = true 인 모든 설정 (스케줄러 순회용) */ List findAllByIsLiveCheckTrue(); + /** 정식 로그인 사용자만 — 게스트(device-only) 제외 */ + List findAllByIsLiveCheckTrueAndUserIdIsNotNull(); + /** 디바이스별 실시간 체크 ON 설정 */ List findByDeviceIdAndIsLiveCheckTrue(String deviceId); diff --git a/backend/src/main/java/com/goldenchart/repository/GcLiveTradeRepository.java b/backend/src/main/java/com/goldenchart/repository/GcLiveTradeRepository.java index 031f1a9..6c8f077 100644 --- a/backend/src/main/java/com/goldenchart/repository/GcLiveTradeRepository.java +++ b/backend/src/main/java/com/goldenchart/repository/GcLiveTradeRepository.java @@ -8,6 +8,8 @@ import java.util.Optional; public interface GcLiveTradeRepository extends JpaRepository { List findTop100ByDeviceIdOrderByCreatedAtDesc(String deviceId); + + List findTop100ByUserIdOrderByCreatedAtDesc(Long userId); Optional findTopByDeviceIdAndSymbolAndSideOrderByCreatedAtDesc( String deviceId, String symbol, String side); List findBySymbolAndSideOrderByCreatedAtDesc(String symbol, String side); diff --git a/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java b/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java index 403be41..b7d7ed8 100644 --- a/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java +++ b/backend/src/main/java/com/goldenchart/service/BarCloseStrategyEvaluationService.java @@ -58,6 +58,7 @@ public class BarCloseStrategyEvaluationService { long candleTimeEpoch = barEndEpoch - signalBar.getTimePeriod().getSeconds(); for (GcLiveStrategySettings s : activeSettings) { + if (s.getUserId() == null) continue; if (!conditionTimeframes.usesTimeframe(s.getStrategyId(), ct)) continue; String closeSignal = "NONE"; @@ -82,9 +83,9 @@ public class BarCloseStrategyEvaluationService { market, s.getStrategyId(), null, closeSignal, signalBar.getClosePrice().doubleValue(), candleTimeEpoch, ct, signalExecType); - if (s.getDeviceId() != null) { + if (s.getUserId() != null) { orderExecutionQueue.submitSignal( - s.getDeviceId(), s.getUserId(), market, + s.getUserId(), market, s.getStrategyId(), closeSignal, signalBar.getClosePrice().doubleValue()); } diff --git a/backend/src/main/java/com/goldenchart/service/DashboardService.java b/backend/src/main/java/com/goldenchart/service/DashboardService.java index 6ad3d20..8a6b563 100644 --- a/backend/src/main/java/com/goldenchart/service/DashboardService.java +++ b/backend/src/main/java/com/goldenchart/service/DashboardService.java @@ -51,22 +51,27 @@ public class DashboardService { : watchlistRepo.findByDeviceIdOrderByDisplayOrderAsc(deviceId).size(); out.put("watchlistCount", watchlistCount); - int liveCheckMarkets = (int) liveSettingsRepo.findAllByIsLiveCheckTrue().stream() - .filter(s -> deviceId.equals(s.getDeviceId()) || (userId != null && userId.equals(s.getUserId()))) - .count(); + int liveCheckMarkets = userId != null + ? (int) liveSettingsRepo.findAllByIsLiveCheckTrueAndUserIdIsNotNull().stream() + .filter(s -> userId.equals(s.getUserId())) + .count() + : 0; out.put("liveCheckMarkets", liveCheckMarkets); List active = liveStrategySettingsService.listActive(userId, deviceId); out.put("monitoredMarkets", active.size()); - PaperSummaryDto paper = paperTradingService.getSummary(userId, deviceId, null); - out.put("paper", paper); + if (userId != null) { + out.put("paper", paperTradingService.getSummary(userId, null)); + out.put("live", liveTradingService.getSummary(userId)); + } else { + out.put("paper", PaperSummaryDto.builder().build()); + out.put("live", LiveSummaryDto.builder().build()); + } - LiveSummaryDto live = liveTradingService.getSummary(userId, deviceId); - out.put("live", live); - - List recentSignals = signalRepo - .findByDeviceIdOrderByCreatedAtDesc(deviceId) + List recentSignals = (userId != null + ? signalRepo.findByUserIdOrderByCreatedAtDesc(userId) + : signalRepo.findByDeviceIdOrderByCreatedAtDesc(deviceId != null ? deviceId : "")) .stream().limit(8) .map(s -> TradeSignalDto.builder() .id(s.getId()) diff --git a/backend/src/main/java/com/goldenchart/service/LiveRiskMonitorService.java b/backend/src/main/java/com/goldenchart/service/LiveRiskMonitorService.java index f05a479..84929b5 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveRiskMonitorService.java +++ b/backend/src/main/java/com/goldenchart/service/LiveRiskMonitorService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.goldenchart.dto.BacktestSettingsDto; import com.goldenchart.entity.GcAppSettings; import com.goldenchart.repository.GcAppSettingsRepository; +import com.goldenchart.trading.TradingAccess; import com.goldenchart.trading.TradingExecutionService; import com.goldenchart.trading.TradingMode; import com.goldenchart.trading.pipeline.OrderRequest; @@ -50,8 +51,9 @@ public class LiveRiskMonitorService { if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) continue; if (!liveTradingService.hasApiKeys(app)) continue; if (!TradingMode.fromString(app.getTradingMode()).useLive()) continue; - String deviceId = app.getDeviceId(); - if (deviceId == null || deviceId.isBlank()) continue; + Long userId = app.getUserId(); + if (userId == null || userId <= 0) continue; + String cacheKey = TradingAccess.accountDeviceKey(userId); try { var creds = appSettingsService.resolveUpbitCredentials(app); if (!creds.isComplete()) continue; @@ -67,9 +69,9 @@ public class LiveRiskMonitorService { coins.put(cur, new double[]{bal, avg}); } } - accountCache.put(deviceId, coins); + accountCache.put(cacheKey, coins); } catch (Exception e) { - log.debug("[LiveRisk] account sync {}: {}", deviceId, e.getMessage()); + log.debug("[LiveRisk] account sync userId={}: {}", userId, e.getMessage()); } } } @@ -79,40 +81,51 @@ public class LiveRiskMonitorService { String currency = market.startsWith("KRW-") ? market.substring(4) : market; for (var entry : accountCache.entrySet()) { - String deviceId = entry.getKey(); + String cacheKey = entry.getKey(); double[] pos = entry.getValue().get(currency); if (pos == null || pos[0] <= 0 || pos[1] <= 0) continue; - String key = deviceId + ":" + market; + String cooldownKey = cacheKey + ":" + market; long now = System.currentTimeMillis(); - Long last = exitCooldown.get(key); + Long last = exitCooldown.get(cooldownKey); if (last != null && now - last < COOLDOWN_MS) continue; - GcAppSettings app = appSettingsRepo.findByDeviceId(deviceId).orElse(null); + Long userId = parseUserIdFromCacheKey(cacheKey); + if (userId == null) continue; + GcAppSettings app = appSettingsRepo.findByUserId(userId).orElse(null); if (app == null) continue; - BacktestSettingsDto risk = backtestSettingsService.get(deviceId); + BacktestSettingsDto risk = backtestSettingsService.get(cacheKey); double avg = pos[1]; double pnlPct = (tradePrice - avg) / avg * 100.0; if (Boolean.TRUE.equals(risk.getStopLossEnabled()) && risk.getStopLossPct() != null && pnlPct <= -risk.getStopLossPct().doubleValue()) { - exitCooldown.put(key, now); + exitCooldown.put(cooldownKey, now); tradingExecutionService.executeRiskExit( - OrderRequest.stopLoss(deviceId, app.getUserId(), market, tradePrice, + OrderRequest.stopLoss(cacheKey, userId, market, tradePrice, risk.getStopLossPct().doubleValue())); return; } if (Boolean.TRUE.equals(risk.getTakeProfitEnabled()) && risk.getTakeProfitPct() != null && pnlPct >= risk.getTakeProfitPct().doubleValue()) { - exitCooldown.put(key, now); + exitCooldown.put(cooldownKey, now); tradingExecutionService.executeRiskExit( - OrderRequest.takeProfit(deviceId, app.getUserId(), market, tradePrice, + OrderRequest.takeProfit(cacheKey, userId, market, tradePrice, risk.getTakeProfitPct().doubleValue())); return; } } } + + private static Long parseUserIdFromCacheKey(String cacheKey) { + if (cacheKey == null || !cacheKey.startsWith("user:")) return null; + try { + return Long.parseLong(cacheKey.substring(5)); + } catch (NumberFormatException e) { + return null; + } + } } diff --git a/backend/src/main/java/com/goldenchart/service/LiveTradingService.java b/backend/src/main/java/com/goldenchart/service/LiveTradingService.java index b8b43c2..4782752 100644 --- a/backend/src/main/java/com/goldenchart/service/LiveTradingService.java +++ b/backend/src/main/java/com/goldenchart/service/LiveTradingService.java @@ -8,6 +8,7 @@ import com.goldenchart.dto.UpbitApiCredentials; import com.goldenchart.entity.GcAppSettings; import com.goldenchart.entity.GcLiveTrade; import com.goldenchart.repository.GcLiveTradeRepository; +import com.goldenchart.trading.TradingAccess; import com.goldenchart.trading.TradingMode; import com.goldenchart.upbit.UpbitOrderApiClient; import lombok.RequiredArgsConstructor; @@ -36,8 +37,9 @@ public class LiveTradingService { private final GcLiveTradeRepository tradeRepo; @Transactional(readOnly = true) - public LiveSummaryDto getSummary(Long userId, String deviceId) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + public LiveSummaryDto getSummary(Long userId) { + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); UpbitApiCredentials creds = appSettingsService.resolveUpbitCredentials(app); boolean configured = creds.isComplete(); double krw = 0; @@ -76,22 +78,27 @@ public class LiveTradingService { } @Transactional(readOnly = true) - public List listTrades(Long userId, String deviceId) { - String dev = resolveDeviceId(deviceId); - return tradeRepo.findTop100ByDeviceIdOrderByCreatedAtDesc(dev) + public List listTrades(Long userId) { + long uid = TradingAccess.requireUserId(userId); + return tradeRepo.findTop100ByUserIdOrderByCreatedAtDesc(uid) .stream().map(this::toDto).toList(); } /** * 전략 시그널·SL/TP 에서 호출. */ - public void tryExecuteOnSignal(String deviceId, Long userId, String market, + public void tryExecuteOnSignal(Long userId, String market, Long strategyId, String signalType, double price, String source) { - if (deviceId == null || deviceId.isBlank()) return; + long uid; + try { + uid = TradingAccess.requireUserId(userId); + } catch (Exception e) { + return; + } if (!"BUY".equals(signalType) && !"SELL".equals(signalType)) return; - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + GcAppSettings app = appSettingsService.getEntity(uid, null); TradingMode mode = TradingMode.fromString(app.getTradingMode()); if (!mode.useLive()) return; if (!Boolean.TRUE.equals(app.getLiveAutoTradeEnabled())) return; @@ -118,7 +125,7 @@ public class LiveTradingService { return; } JsonNode res = upbitApi.placeMarketBuy(access, secret, market, budget); - recordTrade(deviceId, userId, market, "BUY", source, strategyId, res, price, budget); + recordTrade(uid, market, "BUY", source, strategyId, res, price, budget); log.info("[LiveTrading] BUY {} ~{} KRW", market, (long) budget); } else { double vol = upbitApi.getCoinBalance(access, secret, market); @@ -127,7 +134,7 @@ public class LiveTradingService { return; } JsonNode res = upbitApi.placeMarketSell(access, secret, market, vol); - recordTrade(deviceId, userId, market, "SELL", source, strategyId, res, price, vol); + recordTrade(uid, market, "SELL", source, strategyId, res, price, vol); log.info("[LiveTrading] SELL {} vol={}", market, vol); } } catch (UpbitOrderApiClient.UpbitApiException e) { @@ -138,11 +145,12 @@ public class LiveTradingService { /** * 알림 모달·수동 주문 — 시장가 매수/매도. */ - public LiveTradeDto placeManualOrder(Long userId, String deviceId, LiveOrderRequest req) { + public LiveTradeDto placeManualOrder(Long userId, LiveOrderRequest req) { + long uid = TradingAccess.requireUserId(userId); if (req == null || req.getMarket() == null || req.getSide() == null) { throw new IllegalArgumentException("market, side required"); } - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + GcAppSettings app = appSettingsService.getEntity(uid, null); UpbitApiCredentials creds = appSettingsService.resolveUpbitCredentials(app); if (!creds.isComplete()) { throw new IllegalStateException("업비트 API 키가 등록되지 않았습니다."); @@ -151,8 +159,6 @@ public class LiveTradingService { String secret = creds.secretKey(); String market = req.getMarket(); String side = req.getSide().toUpperCase(); - String dev = resolveDeviceId(deviceId); - try { if ("BUY".equals(side)) { double budget = req.getKrwAmount() != null && req.getKrwAmount() > 0 @@ -162,8 +168,8 @@ public class LiveTradingService { throw new IllegalStateException("주문 가능 금액이 부족합니다."); } JsonNode res = upbitApi.placeMarketBuy(access, secret, market, budget); - recordTrade(dev, userId, market, "BUY", "MANUAL", req.getStrategyId(), res, 0, budget); - return listTrades(userId, deviceId).get(0); + recordTrade(uid, market, "BUY", "MANUAL", req.getStrategyId(), res, 0, budget); + return listTrades(uid).get(0); } if ("SELL".equals(side)) { double vol = req.getQuantity() != null && req.getQuantity() > 0 @@ -171,8 +177,8 @@ public class LiveTradingService { : upbitApi.getCoinBalance(access, secret, market); if (vol <= 0) throw new IllegalStateException("매도 가능 수량이 없습니다."); JsonNode res = upbitApi.placeMarketSell(access, secret, market, vol); - recordTrade(dev, userId, market, "SELL", "MANUAL", req.getStrategyId(), res, 0, vol); - return listTrades(userId, deviceId).get(0); + recordTrade(uid, market, "SELL", "MANUAL", req.getStrategyId(), res, 0, vol); + return listTrades(uid).get(0); } throw new IllegalArgumentException("side must be BUY or SELL"); } catch (UpbitOrderApiClient.UpbitApiException e) { @@ -198,7 +204,7 @@ public class LiveTradingService { return appSettingsService.hasUpbitApiKeys(app); } - private void recordTrade(String deviceId, Long userId, String market, String side, + private void recordTrade(long userId, String market, String side, String source, Long strategyId, JsonNode res, double refPrice, double amountOrVol) { String uuid = res != null ? res.path("uuid").asText(null) : null; @@ -209,7 +215,7 @@ public class LiveTradingService { : BigDecimal.valueOf(refPrice * executed).setScale(2, RM); tradeRepo.save(GcLiveTrade.builder() - .deviceId(resolveDeviceId(deviceId)) + .deviceId(TradingAccess.accountDeviceKey(userId)) .userId(userId) .symbol(market) .side(side) @@ -239,7 +245,4 @@ public class LiveTradingService { .build(); } - private static String resolveDeviceId(String deviceId) { - return deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous"; - } } diff --git a/backend/src/main/java/com/goldenchart/service/PaperTradingService.java b/backend/src/main/java/com/goldenchart/service/PaperTradingService.java index 0f3dc8b..ea5aa0c 100644 --- a/backend/src/main/java/com/goldenchart/service/PaperTradingService.java +++ b/backend/src/main/java/com/goldenchart/service/PaperTradingService.java @@ -5,6 +5,7 @@ import com.goldenchart.dto.*; import com.goldenchart.entity.*; import com.goldenchart.repository.*; import com.goldenchart.storage.Ta4jStorage; +import com.goldenchart.trading.TradingAccess; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -51,9 +52,10 @@ public class PaperTradingService { // ── 조회 ───────────────────────────────────────────────────────────────── @Transactional(readOnly = true) - public PaperSummaryDto getSummary(Long userId, String deviceId, Map markPrices) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + public PaperSummaryDto getSummary(Long userId, Map markPrices) { + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); List positions = positionRepo.findByAccountIdOrderBySymbolAsc(account.getId()); double stockEval = 0; @@ -121,38 +123,40 @@ public class PaperTradingService { } @Transactional(readOnly = true) - public List listAllocations(Long userId, String deviceId, - Map markPrices) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); - PaperSummaryDto s = getSummary(userId, deviceId, markPrices); + public List listAllocations(Long userId, Map markPrices) { + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); + PaperSummaryDto s = getSummary(uid, markPrices); return allocationService.listWithMetrics(account.getId(), markPrices, s.getTotalAsset()); } - public List bulkAllocations(Long userId, String deviceId, - PaperAllocationBulkRequest req) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + public List bulkAllocations(Long userId, PaperAllocationBulkRequest req) { + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); BigDecimal defaultMax = account.getInitialCapitalSnapshot() != null ? account.getInitialCapitalSnapshot() : app.getPaperInitialCapital(); return allocationService.bulkUpsert(account.getId(), req, defaultMax); } - public PaperAllocationDto patchAllocation(Long userId, String deviceId, String symbol, + public PaperAllocationDto patchAllocation(Long userId, String symbol, PaperAllocationPatchRequest req) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); return allocationService.patch(account.getId(), symbol, req); } @Transactional(readOnly = true) - public PaperPageDto listTrades(Long userId, String deviceId, + public PaperPageDto listTrades(Long userId, String symbol, String side, String source, LocalDateTime from, LocalDateTime to, int page, int size) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); PageRequest pr = PageRequest.of(Math.max(0, page), Math.min(100, Math.max(1, size))); Page result = tradeRepo.findFiltered( account.getId(), symbol, side, source, from, to, pr); @@ -165,39 +169,43 @@ public class PaperTradingService { .build(); } - public List listTradesLegacy(Long userId, String deviceId) { - return listTrades(userId, deviceId, null, null, null, null, null, 0, 100).getContent(); + public List listTradesLegacy(Long userId) { + return listTrades(userId, null, null, null, null, null, 0, 100).getContent(); } @Transactional(readOnly = true) - public PaperPageDto listLedger(Long userId, String deviceId, + public PaperPageDto listLedger(Long userId, LocalDateTime from, LocalDateTime to, int page, int size) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); return ledgerService.list(account.getId(), from, to, page, size); } @Transactional(readOnly = true) - public List listDailySnapshots(Long userId, String deviceId, + public List listDailySnapshots(Long userId, LocalDate from, LocalDate to) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); return snapshotService.list(account.getId(), from, to); } @Transactional(readOnly = true) - public List listPendingOrders(Long userId, String deviceId) { - GcPaperAccount account = getOrCreateAccount(userId, deviceId, - appSettingsService.getEntity(userId, deviceId)); + public List listPendingOrders(Long userId) { + long uid = TradingAccess.requireUserId(userId); + GcPaperAccount account = getOrCreateAccount(uid, + appSettingsService.getEntity(uid, null)); return orderRepo.findByAccountIdAndStatusOrderByCreatedAtDesc(account.getId(), "PENDING") .stream().map(this::toOrderDto).toList(); } // ── 주문 ───────────────────────────────────────────────────────────────── - public PaperPlaceOrderResult placeOrder(Long userId, String deviceId, PaperOrderRequest req) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + public PaperPlaceOrderResult placeOrder(Long userId, PaperOrderRequest req) { + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) { throw new IllegalStateException("모의투자가 비활성화되어 있습니다."); } @@ -217,7 +225,7 @@ public class PaperTradingService { String source = req.getSource() != null ? req.getSource() : "MANUAL"; String orderKind = req.getOrderKind() != null ? req.getOrderKind() : "limit"; - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + GcPaperAccount account = getOrCreateAccount(uid, app); double qty = req.getQuantity() != null ? req.getQuantity() : 0; if (qty <= 0) { qty = resolveAutoQuantity(account, app, req.getMarket(), side, price, @@ -233,14 +241,15 @@ public class PaperTradingService { return PaperPlaceOrderResult.builder().order(toOrderDto(order)).build(); } - PaperTradeDto trade = executeTrade(userId, deviceId, app, req.getMarket(), side, orderKind, source, + PaperTradeDto trade = executeTrade(uid, app, req.getMarket(), side, orderKind, source, req.getStrategyId(), price, qty, null, false); return PaperPlaceOrderResult.builder().trade(trade).build(); } - public PaperOrderDto cancelOrder(Long userId, String deviceId, Long orderId) { - GcPaperAccount account = getOrCreateAccount(userId, deviceId, - appSettingsService.getEntity(userId, deviceId)); + public PaperOrderDto cancelOrder(Long userId, Long orderId) { + long uid = TradingAccess.requireUserId(userId); + GcPaperAccount account = getOrCreateAccount(uid, + appSettingsService.getEntity(uid, null)); GcPaperOrder order = orderRepo.findById(orderId) .orElseThrow(() -> new IllegalArgumentException("주문을 찾을 수 없습니다.")); if (!order.getAccountId().equals(account.getId())) { @@ -255,12 +264,17 @@ public class PaperTradingService { return toOrderDto(order); } - public void tryExecuteOnSignal(String deviceId, Long userId, String market, + public void tryExecuteOnSignal(Long userId, String market, Long strategyId, String signalType, double price) { - if (deviceId == null || deviceId.isBlank()) return; + long uid; + try { + uid = TradingAccess.requireUserId(userId); + } catch (Exception e) { + return; + } if (!"BUY".equals(signalType) && !"SELL".equals(signalType)) return; - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + GcAppSettings app = appSettingsService.getEntity(uid, null); if (!Boolean.TRUE.equals(app.getPaperTradingEnabled())) return; if (!Boolean.TRUE.equals(app.getPaperAutoTradeEnabled())) return; List liveForMarket = liveSettingsRepo.findActiveByMarket(market).stream() @@ -270,7 +284,7 @@ public class PaperTradingService { if (!Boolean.TRUE.equals(app.getLiveStrategyCheck()) && !marketActive) return; try { - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + GcPaperAccount account = getOrCreateAccount(uid, app); String positionMode = liveForMarket.stream() .map(GcLiveStrategySettings::getPositionMode) .filter(m -> m != null && !m.isBlank()) @@ -299,7 +313,7 @@ public class PaperTradingService { if (denom <= 0 || budget < minOrder) return; double qty = budget / denom; if (qty * execPrice < minOrder) return; - executeTrade(userId, deviceId, app, market, "BUY", "market", "STRATEGY", + executeTrade(uid, app, market, "BUY", "market", "STRATEGY", strategyId, price, qty, null, true); log.info("[Paper] STRATEGY BUY {} qty≈{} @ {}", market, qty, price); } else { @@ -308,7 +322,7 @@ public class PaperTradingService { if (pos == null || pos.getQuantity().doubleValue() <= 0) return; double avail = availableSellQty(account.getId(), market, pos.getQuantity().doubleValue()); if (avail <= 0) return; - executeTrade(userId, deviceId, app, market, "SELL", "market", "STRATEGY", + executeTrade(uid, app, market, "SELL", "market", "STRATEGY", strategyId, price, avail, null, true); log.info("[Paper] STRATEGY SELL {} qty={} @ {}", market, avail, price); } @@ -332,11 +346,13 @@ public class PaperTradingService { GcPaperAccount account = accountRepo.findById(order.getAccountId()).orElse(null); if (account == null) return; - GcAppSettings app = appSettingsService.getEntity(account.getUserId(), account.getDeviceId()); + if (account.getUserId() == null) return; + long uid = account.getUserId(); + GcAppSettings app = appSettingsService.getEntity(uid, null); releaseReservation(account, order); double qty = order.getQuantity().doubleValue(); - executeTrade(account.getUserId(), account.getDeviceId(), app, order.getSymbol(), + executeTrade(uid, app, order.getSymbol(), order.getSide(), order.getOrderKind(), order.getSource(), order.getStrategyId(), limit, qty, null, "STRATEGY".equals(order.getSource())); @@ -361,9 +377,10 @@ public class PaperTradingService { return 0; } - public PaperSummaryDto resetAccount(Long userId, String deviceId) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + public PaperSummaryDto resetAccount(Long userId) { + long uid = TradingAccess.requireUserId(userId); + GcAppSettings app = appSettingsService.getEntity(uid, null); + GcPaperAccount account = getOrCreateAccount(uid, app); BigDecimal initial = app.getPaperInitialCapital() != null ? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000); BigDecimal cashBefore = account.getCashBalance(); @@ -405,8 +422,8 @@ public class PaperTradingService { accountRepo.save(account); allocationService.seedFromPositions(account.getId(), initial); - log.info("[Paper] account reset device={} capital={}", deviceId, initial); - return getSummary(userId, deviceId, null); + log.info("[Paper] account reset userId={} capital={}", uid, initial); + return getSummary(uid, null); } // ── 내부 ───────────────────────────────────────────────────────────────── @@ -481,11 +498,11 @@ public class PaperTradingService { return Math.max(0, held - reserved); } - private PaperTradeDto executeTrade(Long userId, String deviceId, GcAppSettings app, + private PaperTradeDto executeTrade(long userId, GcAppSettings app, String market, String side, String orderKind, String source, Long strategyId, double inputPrice, double inputQty, String koreanName, boolean autoTrade) { - GcPaperAccount account = getOrCreateAccount(userId, deviceId, app); + GcPaperAccount account = getOrCreateAccount(userId, app); double feeRate = pct(app.getPaperFeeRatePct(), 0.05); double slip = pct(app.getPaperSlippagePct(), 0); double minOrder = app.getPaperMinOrderKrw() != null @@ -638,14 +655,14 @@ public class PaperTradingService { positionRepo.save(pos); } - private GcPaperAccount getOrCreateAccount(Long userId, String deviceId, GcAppSettings app) { - String dev = deviceId != null && !deviceId.isBlank() ? deviceId : "anonymous"; - return accountRepo.findByDeviceId(dev).orElseGet(() -> { + private GcPaperAccount getOrCreateAccount(long userId, GcAppSettings app) { + return accountRepo.findByUserId(userId).orElseGet(() -> { + String devKey = TradingAccess.accountDeviceKey(userId); BigDecimal initial = app.getPaperInitialCapital() != null ? app.getPaperInitialCapital() : BigDecimal.valueOf(10_000_000); GcPaperAccount a = GcPaperAccount.builder() .userId(userId) - .deviceId(dev) + .deviceId(devKey) .cashBalance(initial) .realizedPnl(BigDecimal.ZERO) .reservedCash(BigDecimal.ZERO) diff --git a/backend/src/main/java/com/goldenchart/trading/TradingAccess.java b/backend/src/main/java/com/goldenchart/trading/TradingAccess.java new file mode 100644 index 0000000..b5ca04f --- /dev/null +++ b/backend/src/main/java/com/goldenchart/trading/TradingAccess.java @@ -0,0 +1,26 @@ +package com.goldenchart.trading; + +/** + * 로그인 사용자(userId) 전용 매매·모의·전략 접근 규칙. + * 게스트(앱 입장만 한 비로그인)는 deviceId만으로 매매 기능을 사용할 수 없다. + */ +public final class TradingAccess { + + private TradingAccess() {} + + /** 모의 계좌 device_id 컬럼(NOT NULL UK)용 합성 키 — 실제 기기와 무관 */ + public static String accountDeviceKey(long userId) { + return "user:" + userId; + } + + public static long requireUserId(Long userId) { + if (userId == null || userId <= 0) { + throw new TradingAccessDeniedException(); + } + return userId; + } + + public static boolean isRegisteredUser(Long userId) { + return userId != null && userId > 0; + } +} diff --git a/backend/src/main/java/com/goldenchart/trading/TradingAccessDeniedException.java b/backend/src/main/java/com/goldenchart/trading/TradingAccessDeniedException.java new file mode 100644 index 0000000..66632a6 --- /dev/null +++ b/backend/src/main/java/com/goldenchart/trading/TradingAccessDeniedException.java @@ -0,0 +1,22 @@ +package com.goldenchart.trading; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * 게스트(비로그인) 또는 미등록 사용자가 매매·모의·전략 API에 접근할 때. + */ +@ResponseStatus(HttpStatus.FORBIDDEN) +public class TradingAccessDeniedException extends RuntimeException { + + public static final String DEFAULT_MESSAGE = + "매매·모의투자·투자전략·자동매매 기능은 정식 로그인 후 이용할 수 있습니다."; + + public TradingAccessDeniedException() { + super(DEFAULT_MESSAGE); + } + + public TradingAccessDeniedException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/goldenchart/trading/TradingExecutionService.java b/backend/src/main/java/com/goldenchart/trading/TradingExecutionService.java index 2fa0ab4..bf4089c 100644 --- a/backend/src/main/java/com/goldenchart/trading/TradingExecutionService.java +++ b/backend/src/main/java/com/goldenchart/trading/TradingExecutionService.java @@ -2,6 +2,7 @@ package com.goldenchart.trading; import com.goldenchart.entity.GcAppSettings; import com.goldenchart.service.AppSettingsService; +import com.goldenchart.trading.TradingAccess; import com.goldenchart.service.LiveTradingService; import com.goldenchart.service.PaperTradingService; import com.goldenchart.trading.pipeline.OrderRequest; @@ -21,32 +22,34 @@ public class TradingExecutionService { private final PaperTradingService paperTradingService; private final LiveTradingService liveTradingService; - public void executeSignal(String deviceId, Long userId, String market, + public void executeSignal(Long userId, String market, Long strategyId, String side, double price) { - GcAppSettings app = appSettingsService.getEntity(userId, deviceId); + if (!TradingAccess.isRegisteredUser(userId)) return; + GcAppSettings app = appSettingsService.getEntity(userId, null); TradingMode mode = TradingMode.fromString(app.getTradingMode()); if (mode.usePaper() && Boolean.TRUE.equals(app.getPaperTradingEnabled())) { - paperTradingService.tryExecuteOnSignal(deviceId, userId, market, strategyId, side, price); + paperTradingService.tryExecuteOnSignal(userId, market, strategyId, side, price); } if (mode.useLive()) { liveTradingService.tryExecuteOnSignal( - deviceId, userId, market, strategyId, side, price, "STRATEGY"); + userId, market, strategyId, side, price, "STRATEGY"); } } public void executeRiskExit(OrderRequest req) { - GcAppSettings app = appSettingsService.getEntity(req.userId(), req.deviceId()); + if (!TradingAccess.isRegisteredUser(req.userId())) return; + GcAppSettings app = appSettingsService.getEntity(req.userId(), null); TradingMode mode = TradingMode.fromString(app.getTradingMode()); String source = req.kind() == OrderRequest.OrderKind.STOP_LOSS ? "STOP_LOSS" : "TAKE_PROFIT"; if (mode.usePaper() && Boolean.TRUE.equals(app.getPaperTradingEnabled())) { paperTradingService.tryExecuteOnSignal( - req.deviceId(), req.userId(), req.market(), null, "SELL", req.price()); + req.userId(), req.market(), null, "SELL", req.price()); } if (mode.useLive()) { liveTradingService.tryExecuteOnSignal( - req.deviceId(), req.userId(), req.market(), null, "SELL", req.price(), source); + req.userId(), req.market(), null, "SELL", req.price(), source); } } } diff --git a/backend/src/main/java/com/goldenchart/trading/pipeline/OrderExecutionQueue.java b/backend/src/main/java/com/goldenchart/trading/pipeline/OrderExecutionQueue.java index cc37742..6ba645b 100644 --- a/backend/src/main/java/com/goldenchart/trading/pipeline/OrderExecutionQueue.java +++ b/backend/src/main/java/com/goldenchart/trading/pipeline/OrderExecutionQueue.java @@ -1,5 +1,6 @@ package com.goldenchart.trading.pipeline; +import com.goldenchart.trading.TradingAccess; import com.goldenchart.trading.TradingExecutionService; import com.google.common.util.concurrent.RateLimiter; import jakarta.annotation.PostConstruct; @@ -64,9 +65,11 @@ public class OrderExecutionQueue { } } - public void submitSignal(String deviceId, Long userId, String market, + public void submitSignal(Long userId, String market, Long strategyId, String signal, double price) { - submit(OrderRequest.strategySignal(deviceId, userId, market, strategyId, signal, price)); + if (!TradingAccess.isRegisteredUser(userId)) return; + submit(OrderRequest.strategySignal( + TradingAccess.accountDeviceKey(userId), userId, market, strategyId, signal, price)); } private void runLoop() { @@ -88,12 +91,12 @@ public class OrderExecutionQueue { } private void executeNow(OrderRequest req) { - if (req.deviceId() == null || req.deviceId().isBlank()) return; + if (!TradingAccess.isRegisteredUser(req.userId())) return; if (!"BUY".equals(req.side()) && !"SELL".equals(req.side())) return; try { if (req.kind() == OrderRequest.OrderKind.STRATEGY_SIGNAL) { tradingExecutionService.executeSignal( - req.deviceId(), req.userId(), req.market(), + req.userId(), req.market(), req.strategyId(), req.side(), req.price()); } else { tradingExecutionService.executeRiskExit(req); diff --git a/backend/src/main/java/com/goldenchart/trading/pipeline/RealtimePositionMonitor.java b/backend/src/main/java/com/goldenchart/trading/pipeline/RealtimePositionMonitor.java index 799d015..3cea2f0 100644 --- a/backend/src/main/java/com/goldenchart/trading/pipeline/RealtimePositionMonitor.java +++ b/backend/src/main/java/com/goldenchart/trading/pipeline/RealtimePositionMonitor.java @@ -8,6 +8,7 @@ import com.goldenchart.repository.GcPaperAccountRepository; import com.goldenchart.repository.GcPaperPositionRepository; import com.goldenchart.service.AppSettingsService; import com.goldenchart.service.BacktestSettingsService; +import com.goldenchart.trading.TradingAccess; import com.goldenchart.trading.TradingExecutionService; import com.goldenchart.trading.TradingMode; import lombok.RequiredArgsConstructor; @@ -53,13 +54,12 @@ public class RealtimePositionMonitor { GcPaperAccount account = accountRepo.findById(pos.getAccountId()).orElse(null); if (account == null) continue; - String deviceId = account.getDeviceId(); Long userId = account.getUserId(); - if (deviceId == null || deviceId.isBlank()) continue; + if (userId == null || userId <= 0) continue; GcAppSettings app; try { - app = appSettingsService.getEntity(userId, deviceId); + app = appSettingsService.getEntity(userId, null); } catch (Exception e) { continue; } @@ -75,7 +75,7 @@ public class RealtimePositionMonitor { double avg = pos.getAvgPrice().doubleValue(); if (avg <= 0) continue; - BacktestSettingsDto risk = backtestSettingsService.get(deviceId); + BacktestSettingsDto risk = backtestSettingsService.get(TradingAccess.accountDeviceKey(userId)); double pnlPct = (tradePrice - avg) / avg * 100.0; if (Boolean.TRUE.equals(risk.getStopLossEnabled()) @@ -84,7 +84,7 @@ public class RealtimePositionMonitor { lastExitMs.put(cooldownKey, now); double sl = risk.getStopLossPct().doubleValue(); tradingExecutionService.executeRiskExit( - OrderRequest.stopLoss(deviceId, userId, market, tradePrice, sl)); + OrderRequest.stopLoss(TradingAccess.accountDeviceKey(userId), userId, market, tradePrice, sl)); log.info("[SL] {} @ {} pnl={}% (limit -{}%)", market, tradePrice, String.format("%.2f", pnlPct), sl); return; @@ -96,7 +96,7 @@ public class RealtimePositionMonitor { lastExitMs.put(cooldownKey, now); double tp = risk.getTakeProfitPct().doubleValue(); tradingExecutionService.executeRiskExit( - OrderRequest.takeProfit(deviceId, userId, market, tradePrice, tp)); + OrderRequest.takeProfit(TradingAccess.accountDeviceKey(userId), userId, market, tradePrice, tp)); log.info("[TP] {} @ {} pnl={}% (target +{}%)", market, tradePrice, String.format("%.2f", pnlPct), tp); return; diff --git a/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java b/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java index 0efa405..1556a9f 100644 --- a/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java +++ b/backend/src/main/java/com/goldenchart/websocket/LiveStrategyScheduler.java @@ -48,7 +48,8 @@ public class LiveStrategyScheduler { */ @Scheduled(fixedDelay = 3000) public void tick() { - List activeSettings = settingsRepo.findAllByIsLiveCheckTrue(); + List activeSettings = + settingsRepo.findAllByIsLiveCheckTrueAndUserIdIsNotNull(); if (activeSettings.isEmpty()) return; for (GcLiveStrategySettings s : activeSettings) { @@ -104,9 +105,9 @@ public class LiveStrategyScheduler { lastBar.getClosePrice().doubleValue(), barStartEpoch, candleType, "REALTIME_TICK" ); - if (s.getDeviceId() != null) { + if (s.getUserId() != null) { orderExecutionQueue.submitSignal( - s.getDeviceId(), s.getUserId(), market, + s.getUserId(), market, s.getStrategyId(), signal, lastBar.getClosePrice().doubleValue()); } diff --git a/backend/src/main/resources/db/migration/V54__trading_user_scope_reset.sql b/backend/src/main/resources/db/migration/V54__trading_user_scope_reset.sql new file mode 100644 index 0000000..d738351 --- /dev/null +++ b/backend/src/main/resources/db/migration/V54__trading_user_scope_reset.sql @@ -0,0 +1,18 @@ +-- V54: 매매 데이터 초기화 + 비회원(device-only) 실시간 전략 체크 OFF +-- 로그인 userId 단일 계좌·자동매매 전환 (기존 device 계좌 데이터 폐기) + +UPDATE gc_live_strategy_settings +SET is_live_check = 0 +WHERE user_id IS NULL; + +DELETE FROM gc_paper_order; +DELETE FROM gc_paper_trade; +DELETE FROM gc_paper_cash_ledger; +DELETE FROM gc_paper_daily_snapshot; +DELETE FROM gc_paper_reset_log; +DELETE FROM gc_paper_symbol_allocation; +DELETE FROM gc_paper_position; +DELETE FROM gc_paper_account; + +DELETE FROM gc_trade_signal; +DELETE FROM gc_live_trade; diff --git a/frontend/src/App.css b/frontend/src/App.css index e9eec52..3987022 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -12974,3 +12974,41 @@ html.theme-light .tam-disclaimer { color: #90a4ae; } .app-download-ios-note { margin-top: 12px; font-size: 11px; color: var(--text3); line-height: 1.45; } + +/* ── 정식 로그인 유도 (게스트·매매 기능) ─────────────────────────────────── */ +.formal-login-gate { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: calc(100vh - 48px); + padding: 32px 24px; + text-align: center; + background: var(--bg); +} +.formal-login-gate__title { + margin: 0 0 12px; + font-size: 20px; + font-weight: 700; + color: var(--text); +} +.formal-login-gate__desc { + margin: 0 0 24px; + max-width: 420px; + font-size: 14px; + line-height: 1.55; + color: var(--text2); +} +.formal-login-gate__btn { + padding: 12px 28px; + border: none; + border-radius: 10px; + background: linear-gradient(135deg, #8b5cf6, #7c3aed); + color: #fff; + font-size: 15px; + font-weight: 600; + cursor: pointer; +} +.formal-login-gate__btn:hover { + filter: brightness(1.06); +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0091b92..b1152e7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -86,6 +86,13 @@ import LiveStrategyPanel from './components/LiveStrategyPanel'; import { TradeNotificationProvider } from './contexts/TradeNotificationContext'; import { VerificationIssueNotificationProvider } from './contexts/VerificationIssueNotificationContext'; import { LiveSignalNotifier, type LiveSignalNotifierHandle } from './components/LiveSignalNotifier'; +import { FormalLoginGate } from './components/FormalLoginGate'; +import { + isFormalLogin, + isTradingMenuPage, + isTradingSettingsCategory, + FORMAL_LOGIN_REQUIRED_MSG, +} from './utils/tradingAccess'; import { NotifyTopMenuBar, ChartToolbarNotify } from './components/NotifyUiBindings'; import { AppNotificationLayer } from './components/AppNotificationLayer'; import TradeNotificationListPage from './components/TradeNotificationListPage'; @@ -163,7 +170,13 @@ function App() { const [guestMode, setGuestMode] = useState(() => isAppEntered() && !getAuthSession(), ); + const formalLogin = isFormalLogin(authUser, guestMode); const [loginOpen, setLoginOpen] = useState(false); + + const requireFormalLogin = useCallback(() => { + window.alert(FORMAL_LOGIN_REQUIRED_MSG); + setLoginOpen(true); + }, []); const [appDownloadOpen, setAppDownloadOpen] = useState(false); // 전역 지표 파라미터 + 시각 설정 DB 동기화 @@ -593,13 +606,17 @@ function App() { }, []); const guardedSetMenuPage = useCallback((page: MenuPage) => { + if (!formalLogin && isTradingMenuPage(page)) { + requireFormalLogin(); + return; + } if (page === 'notifications' && !canMenu('notifications')) return; if (page !== 'notifications' && !canMenu(page)) { setMenuPage(firstAllowedTopMenu(menuPermissions)); return; } setMenuPage(page); - }, [canMenu, menuPermissions]); + }, [canMenu, menuPermissions, formalLogin, requireFormalLogin]); useEffect(() => { if (!menuPermsLoaded) return; @@ -814,12 +831,13 @@ function App() { /** STOMP 구독 대상 — 가상투자 실행 중이면 투자대상 종목, 아니면 관심종목 */ const monitoredMarkets = useMemo(() => { + if (!formalLogin) return []; if (isVirtualActive) return virtualMonitoredMarkets; if (!appDefaults.liveStrategyCheck || !appDefaults.liveStrategyId) return []; const set = new Set(watchlist); if (symbol) set.add(symbol); return [...set]; - }, [isVirtualActive, virtualMonitoredMarkets, watchlist, symbol, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId]); + }, [formalLogin, isVirtualActive, virtualMonitoredMarkets, watchlist, symbol, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId]); const [marketSubscriptions, setMarketSubscriptions] = useState<{ market: string; candleType: string }[]>([]); @@ -1693,7 +1711,7 @@ function App() { markets={monitoredMarkets} marketSubscriptions={liveStompSubscriptions} activeMarket={symbol} - enabled={monitoredMarkets.length > 0} + enabled={formalLogin && monitoredMarkets.length > 0} popupEnabled={appDefaults.tradeAlertPopup ?? true} chartMarkersActive={menuPage === 'chart'} onMarkersChange={markers => { @@ -1737,19 +1755,26 @@ function App() { )} {menuPage === 'strategy' && ( - + formalLogin + ? + : )} {menuPage === 'strategy-editor' && ( - + formalLogin + ? + : )} {/* ── 백테스팅 이력 화면 ─────────────────────────────────────────── */} {menuPage === 'backtest' && ( - + formalLogin + ? + : )} {menuPage === 'paper' && ( + formalLogin ? ( { + if (!formalLogin) { requireFormalLogin(); return; } setSettingsInitialCategory('paper'); setMenuPage('settings'); }} @@ -1767,9 +1793,13 @@ function App() { setMenuPage('chart'); }} /> + ) : ( + + ) )} {menuPage === 'virtual' && ( + formalLogin ? ( saveAppDef({ paperAutoTradeEnabled: v })} onPaperOrderFilled={handlePaperOrderFilled} /> + ) : ( + + ) )} {menuPage === 'trend-search' && ( @@ -1802,6 +1835,7 @@ function App() { {/* ── 설정 화면 ──────────────────────────────────────────────────── */} {menuPage === 'notifications' && canMenu('notifications') && ( + formalLogin ? ( + ) : ( + + ) )} {menuPage === 'settings' && canMenu('settings') && ( { if (opts.strategyName) setBtStrategyName(opts.strategyName); if (useUpbit && (isLoading || bars.length === 0)) return; @@ -2066,16 +2105,25 @@ function App() { if (appDefaults.btAutoPopup) setShowBtResult(true); } : undefined} - onOpenBtSettings={() => setShowBtSettings(true)} - onOpenBtResults={() => setShowBtResult(true)} + onOpenBtSettings={() => { + if (!formalLogin) { requireFormalLogin(); return; } + setShowBtSettings(true); + }} + onOpenBtResults={() => { + if (!formalLogin) { requireFormalLogin(); return; } + setShowBtResult(true); + }} hasBtResult={btMatchesChart} onClearBtMarkers={() => { btMarkerPayloadRef.current = null; btClear(); managerRef.current?.clearBacktestMarkers(); }} - onToggleLiveStrategy={() => setShowLivePanel(v => !v)} - liveStrategyActive={showLivePanel || monitoredMarkets.length > 0} + onToggleLiveStrategy={() => { + if (!formalLogin) { requireFormalLogin(); return; } + setShowLivePanel(v => !v); + }} + liveStrategyActive={formalLogin && (showLivePanel || monitoredMarkets.length > 0)} showChartQuote={useUpbit} chartLiveQuote={liveQuote} chartQuoteLoading={isLoading} @@ -2584,8 +2632,8 @@ function App() { onMarketSelect={handleTradeMarketSelect} availableKrw={paperCashBalance} availableCoinQty={paperCoinQty} - paperTradingEnabled={paperTradingEnabled} - paperAutoTradeEnabled={paperAutoTradeEnabled} + paperTradingEnabled={formalLogin && paperTradingEnabled} + paperAutoTradeEnabled={formalLogin && paperAutoTradeEnabled} onPaperOrderFilled={() => { refreshPaperAccount(symbol); handlePaperOrderFilled(); diff --git a/frontend/src/components/FormalLoginGate.tsx b/frontend/src/components/FormalLoginGate.tsx new file mode 100644 index 0000000..1e80576 --- /dev/null +++ b/frontend/src/components/FormalLoginGate.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { FORMAL_LOGIN_REQUIRED_MSG } from '../utils/tradingAccess'; + +interface Props { + onLogin: () => void; + title?: string; +} + +/** 게스트·비로그인 — 매매 관련 화면 대체 */ +export const FormalLoginGate: React.FC = ({ + onLogin, + title = '정식 로그인이 필요합니다', +}) => ( +
+

{title}

+

{FORMAL_LOGIN_REQUIRED_MSG}

+ +
+); + +export default FormalLoginGate; diff --git a/frontend/src/components/SettingsPage.tsx b/frontend/src/components/SettingsPage.tsx index 07e3185..e1811be 100644 --- a/frontend/src/components/SettingsPage.tsx +++ b/frontend/src/components/SettingsPage.tsx @@ -33,6 +33,7 @@ import { import TradeAlertPopupPreview from './TradeAlertPopupPreview'; import TrendSearchSettingsPanel from './trendSearch/TrendSearchSettingsPanel'; import type { TrendSearchAppSettings } from '../utils/trendSearchAppSettings'; +import { isTradingSettingsCategory } from '../utils/tradingAccess'; import { CHART_LEGEND_SETTING_ITEMS, type ChartLegendVisibility, @@ -168,6 +169,9 @@ interface SettingsPageProps { onTrendSearchSettingsChange?: (s: TrendSearchAppSettings) => void; /** 설정 화면 진입 시 선택할 카테고리 (예: 모의투자 화면에서 링크) */ initialCategory?: SettingsCategoryId; + /** 정식 로그인 여부 (게스트는 매매·전략 설정 탭 차단) */ + isFormalLogin?: boolean; + onRequireFormalLogin?: () => void; } // ── 카테고리 정의 ──────────────────────────────────────────────────────────── @@ -1743,6 +1747,8 @@ const SettingsPage: React.FC = ({ trendSearchSettings, onTrendSearchSettingsChange, initialCategory, + isFormalLogin = true, + onRequireFormalLogin, }) => { const categories = filterCategories(menuPermissions); const [active, setActive] = useState(initialCategory ?? 'general'); @@ -1757,10 +1763,14 @@ const SettingsPage: React.FC = ({ useEffect(() => { if (!initialCategory) return; + if (!isFormalLogin && isTradingSettingsCategory(initialCategory)) { + onRequireFormalLogin?.(); + return; + } if (categories.some(c => c.id === initialCategory)) { setActive(initialCategory); } - }, [initialCategory, categories]); + }, [initialCategory, categories, isFormalLogin, onRequireFormalLogin]); useEffect(() => { if (active !== 'indicators') setIndicatorSaveUi(null); @@ -1948,7 +1958,13 @@ const SettingsPage: React.FC = ({