자산 현황 및 자동매매 페이지 제거:
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 11m20s
Some checks failed
Build Push and Restart Compose / deploy (push) Failing after 11m20s
- `/templates/pages/asset.html`, `/templates/pages/autotrade.html` HTML 템플릿 삭제. - `/static/js/asset.js`, `/static/js/autotrade.js` 클라이언트 스크립트 제거. - 관련 함수 및 초기화 로직 삭제 (자산 조회 및 자동매매 기능 비활성화).
This commit is contained in:
@@ -66,6 +66,23 @@ func GetAutoTradeService() *AutoTradeService {
|
||||
SelectedThemes: []models.ThemeRef{},
|
||||
},
|
||||
}
|
||||
// DB에서 데이터 복원
|
||||
if rules := dbLoadRules(); len(rules) > 0 {
|
||||
autoTradeService.rules = rules
|
||||
log.Printf("DB에서 규칙 %d개 로드", len(rules))
|
||||
}
|
||||
if positions := dbLoadActivePositions(); len(positions) > 0 {
|
||||
autoTradeService.positions = positions
|
||||
log.Printf("DB에서 활성 포지션 %d개 로드", len(positions))
|
||||
}
|
||||
if ws := dbLoadWatchSource(); ws != nil {
|
||||
autoTradeService.watchSource = *ws
|
||||
log.Printf("DB에서 감시소스 로드")
|
||||
}
|
||||
if logs := dbLoadRecentLogs(maxLogEntries); len(logs) > 0 {
|
||||
autoTradeService.logs = logs
|
||||
log.Printf("DB에서 로그 %d건 로드", len(logs))
|
||||
}
|
||||
}
|
||||
return autoTradeService
|
||||
}
|
||||
@@ -93,6 +110,7 @@ func (s *AutoTradeService) SetWatchSource(ws models.AutoTradeWatchSource) {
|
||||
s.mu.Lock()
|
||||
s.watchSource = ws
|
||||
s.mu.Unlock()
|
||||
dbUpsertWatchSource(ws)
|
||||
|
||||
sources := "없음"
|
||||
if ws.UseScanner && len(ws.SelectedThemes) > 0 {
|
||||
@@ -245,6 +263,7 @@ func (s *AutoTradeService) AddRule(rule models.AutoTradeRule) models.AutoTradeRu
|
||||
s.mu.Lock()
|
||||
s.rules = append(s.rules, rule)
|
||||
s.mu.Unlock()
|
||||
dbInsertRule(rule)
|
||||
s.addLog("info", "", fmt.Sprintf("규칙 추가: %s", rule.Name))
|
||||
return rule
|
||||
}
|
||||
@@ -258,6 +277,7 @@ func (s *AutoTradeService) UpdateRule(id string, updated models.AutoTradeRule) b
|
||||
updated.ID = id
|
||||
updated.CreatedAt = r.CreatedAt
|
||||
s.rules[i] = updated
|
||||
dbUpdateRule(updated)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -271,6 +291,7 @@ func (s *AutoTradeService) DeleteRule(id string) bool {
|
||||
for i, r := range s.rules {
|
||||
if r.ID == id {
|
||||
s.rules = append(s.rules[:i], s.rules[i+1:]...)
|
||||
dbDeleteRule(id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -284,6 +305,7 @@ func (s *AutoTradeService) ToggleRule(id string) (bool, bool) {
|
||||
for i, r := range s.rules {
|
||||
if r.ID == id {
|
||||
s.rules[i].Enabled = !r.Enabled
|
||||
dbUpdateRule(s.rules[i])
|
||||
return true, s.rules[i].Enabled
|
||||
}
|
||||
}
|
||||
@@ -311,6 +333,24 @@ func (s *AutoTradeService) GetPositions() []*models.AutoTradePosition {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTrades 종료된 거래 내역 반환 (DB에서 조회, 없으면 메모리에서 필터)
|
||||
func (s *AutoTradeService) GetTrades(limit int) []*models.AutoTradePosition {
|
||||
if trades := dbLoadClosedPositions(limit); trades != nil {
|
||||
return trades
|
||||
}
|
||||
// DB 없으면 메모리에서 closed 포지션 반환
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var result []*models.AutoTradePosition
|
||||
for _, p := range s.positions {
|
||||
if p.Status == "closed" {
|
||||
cp := *p
|
||||
result = append(result, &cp)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLogs 최근 로그 반환
|
||||
func (s *AutoTradeService) GetLogs() []models.AutoTradeLog {
|
||||
s.mu.RLock()
|
||||
@@ -322,6 +362,11 @@ func (s *AutoTradeService) GetLogs() []models.AutoTradeLog {
|
||||
|
||||
// GetStats 오늘 통계 반환 (매매 횟수, 손익)
|
||||
func (s *AutoTradeService) GetStats() (tradeCount int, totalPL int64) {
|
||||
// DB가 있으면 DB에서 조회 (종료된 포지션 포함)
|
||||
if db != nil {
|
||||
return dbGetTodayStats()
|
||||
}
|
||||
// DB 없으면 메모리에서 계산
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -532,6 +577,7 @@ func (s *AutoTradeService) checkEntries() {
|
||||
s.cooldown[code] = time.Now()
|
||||
s.mu.Unlock()
|
||||
|
||||
dbInsertPosition(pos)
|
||||
s.addLog("info", code, fmt.Sprintf("매수 주문 접수: %s %d주 (주문번호: %s, RiseScore: %d)", sig.Name, qty, result.OrderNo, sig.RiseScore))
|
||||
}
|
||||
}
|
||||
@@ -648,6 +694,7 @@ func (s *AutoTradeService) checkPending() {
|
||||
p.StopLoss = stopLoss
|
||||
p.TakeProfit = takeProfit
|
||||
p.Status = "open"
|
||||
dbUpdatePosition(p)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -709,6 +756,7 @@ func (s *AutoTradeService) executeSell(pos *models.AutoTradePosition, reason str
|
||||
p.ExitTime = time.Now()
|
||||
p.ExitPrice = exitPrice
|
||||
p.ExitReason = reason
|
||||
dbUpdatePosition(p)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
@@ -771,6 +819,11 @@ func (s *AutoTradeService) addLog(level, code, message string) {
|
||||
broadcaster := s.logBroadcaster
|
||||
s.mu.Unlock()
|
||||
|
||||
// debug 로그는 DB에 저장하지 않음 (빈도 높음)
|
||||
if level != "debug" {
|
||||
dbInsertLog(entry)
|
||||
}
|
||||
|
||||
// WS 브로드캐스트 (락 밖에서 호출해 데드락 방지)
|
||||
if broadcaster != nil {
|
||||
broadcaster(entry)
|
||||
@@ -873,6 +926,7 @@ func (s *AutoTradeService) evalExitReason(code string, pos *models.AutoTradePosi
|
||||
if p, ok := s.positions[code]; ok && p.Status == "open" {
|
||||
p.StopLoss1Touches++
|
||||
touches = p.StopLoss1Touches
|
||||
dbUpdatePosition(p)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -14,8 +13,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// cntrStrCacheEntry getCntrStr 캐시 항목
|
||||
@@ -24,14 +21,30 @@ type cntrStrCacheEntry struct {
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// apiJob 큐에 넣을 API 요청 작업
|
||||
type apiJob struct {
|
||||
req *http.Request
|
||||
result chan apiResult
|
||||
}
|
||||
|
||||
// apiResult 워커의 응답
|
||||
type apiResult struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// KiwoomClient 키움증권 REST API HTTP 클라이언트
|
||||
// 모든 API 호출은 단일 워커 큐를 통해 순차 처리 (429 방지)
|
||||
// 1초 윈도우 내 처리 시간 합산 → 남은 시간 대기 후 다음 배치
|
||||
type KiwoomClient struct {
|
||||
httpClient *http.Client
|
||||
tokenService *TokenService
|
||||
limiter *rate.Limiter
|
||||
cntrStrCache sync.Map // stockCode → cntrStrCacheEntry (5초 TTL)
|
||||
queue chan apiJob // API 요청 큐
|
||||
cntrStrCache sync.Map // stockCode → cntrStrCacheEntry (5초 TTL)
|
||||
}
|
||||
|
||||
const apiQueueSize = 256 // 큐 버퍼 크기
|
||||
|
||||
var kiwoomClient *KiwoomClient
|
||||
|
||||
// GetKiwoomClient 키움 클라이언트 싱글턴 반환
|
||||
@@ -40,14 +53,39 @@ func GetKiwoomClient() *KiwoomClient {
|
||||
kiwoomClient = &KiwoomClient{
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
tokenService: GetTokenService(),
|
||||
// 초당 1건, 버스트 1 → 완전 직렬화 (키움 API 실질 한도 ~1req/s per API ID)
|
||||
limiter: rate.NewLimiter(rate.Limit(1), 1),
|
||||
queue: make(chan apiJob, apiQueueSize),
|
||||
}
|
||||
go kiwoomClient.worker()
|
||||
}
|
||||
return kiwoomClient
|
||||
}
|
||||
|
||||
// post 공통 POST 요청 (api-id 헤더, JSON body, Rate Limit 적용, 429 재시도)
|
||||
// worker 단일 고루틴이 큐에서 작업을 꺼내 순차 실행
|
||||
// 1건 실행 후 (1초 - 처리시간)만큼 대기 → 다음 1건
|
||||
func (k *KiwoomClient) worker() {
|
||||
for job := range k.queue {
|
||||
start := time.Now()
|
||||
resp, err := k.httpClient.Do(job.req)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
job.result <- apiResult{resp: resp, err: err}
|
||||
|
||||
// 1초 - 처리시간 = 대기시간 (처리가 1초 이상이면 대기 없이 즉시 다음)
|
||||
if wait := time.Second - elapsed; wait > 0 {
|
||||
time.Sleep(wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// enqueue HTTP 요청을 큐에 넣고 응답 대기
|
||||
func (k *KiwoomClient) enqueue(req *http.Request) (*http.Response, error) {
|
||||
ch := make(chan apiResult, 1)
|
||||
k.queue <- apiJob{req: req, result: ch}
|
||||
res := <-ch
|
||||
return res.resp, res.err
|
||||
}
|
||||
|
||||
// post 공통 POST 요청 (api-id 헤더, JSON body, 큐 기반 순차 처리, 429 재시도)
|
||||
func (k *KiwoomClient) post(apiID string, path string, body map[string]string) ([]byte, error) {
|
||||
const maxRetries = 3
|
||||
backoff := 1 * time.Second
|
||||
@@ -55,10 +93,6 @@ func (k *KiwoomClient) post(apiID string, path string, body map[string]string) (
|
||||
data, _ := json.Marshal(body)
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if err := k.limiter.Wait(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("Rate Limit 대기 실패: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", config.App.BaseURL+path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("요청 생성 실패: %w", err)
|
||||
@@ -70,7 +104,7 @@ func (k *KiwoomClient) post(apiID string, path string, body map[string]string) (
|
||||
req.Header.Set("cont-yn", "N")
|
||||
req.Header.Set("next-key", "")
|
||||
|
||||
resp, err := k.httpClient.Do(req)
|
||||
resp, err := k.enqueue(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API 요청 실패: %w", err)
|
||||
}
|
||||
@@ -109,10 +143,6 @@ func (k *KiwoomClient) post(apiID string, path string, body map[string]string) (
|
||||
|
||||
// postPaged 연속조회 지원 POST 요청 - 응답 헤더(cont-yn, next-key) 함께 반환
|
||||
func (k *KiwoomClient) postPaged(apiID, path string, body map[string]string, contYn, nextKey string) ([]byte, string, string, error) {
|
||||
if err := k.limiter.Wait(context.Background()); err != nil {
|
||||
return nil, "", "", fmt.Errorf("Rate Limit 대기 실패: %w", err)
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest("POST", config.App.BaseURL+path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
@@ -125,13 +155,13 @@ func (k *KiwoomClient) postPaged(apiID, path string, body map[string]string, con
|
||||
req.Header.Set("cont-yn", contYn)
|
||||
req.Header.Set("next-key", nextKey)
|
||||
|
||||
resp, err := k.httpClient.Do(req)
|
||||
resp, err := k.enqueue(req)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("API 요청 실패: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("응답 읽기 실패: %w", err)
|
||||
}
|
||||
@@ -171,14 +201,14 @@ func (k *KiwoomClient) fetchPrice(stkCd string) (*models.StockPrice, error) {
|
||||
}
|
||||
|
||||
var result struct {
|
||||
StkNm string `json:"stk_nm"`
|
||||
CurPrc string `json:"cur_prc"`
|
||||
PredPre string `json:"pred_pre"`
|
||||
FluRt string `json:"flu_rt"`
|
||||
TrdeQty string `json:"trde_qty"`
|
||||
OpenPric string `json:"open_pric"`
|
||||
HighPric string `json:"high_pric"`
|
||||
LowPric string `json:"low_pric"`
|
||||
StkNm string `json:"stk_nm"`
|
||||
CurPrc string `json:"cur_prc"`
|
||||
PredPre string `json:"pred_pre"`
|
||||
FluRt string `json:"flu_rt"`
|
||||
TrdeQty string `json:"trde_qty"`
|
||||
OpenPric string `json:"open_pric"`
|
||||
HighPric string `json:"high_pric"`
|
||||
LowPric string `json:"low_pric"`
|
||||
ReturnCode int `json:"return_code"`
|
||||
ReturnMsg string `json:"return_msg"`
|
||||
}
|
||||
@@ -259,12 +289,12 @@ func (k *KiwoomClient) GetDailyChart(stockCode string) ([]models.CandleData, err
|
||||
|
||||
var result struct {
|
||||
StkDdwkmm []struct {
|
||||
Date string `json:"date"`
|
||||
OpenPric string `json:"open_pric"`
|
||||
HighPric string `json:"high_pric"`
|
||||
LowPric string `json:"low_pric"`
|
||||
Date string `json:"date"`
|
||||
OpenPric string `json:"open_pric"`
|
||||
HighPric string `json:"high_pric"`
|
||||
LowPric string `json:"low_pric"`
|
||||
ClosePric string `json:"close_pric"`
|
||||
TrdeQty string `json:"trde_qty"`
|
||||
TrdeQty string `json:"trde_qty"`
|
||||
} `json:"stk_ddwkmm"`
|
||||
ReturnCode int `json:"return_code"`
|
||||
ReturnMsg string `json:"return_msg"`
|
||||
@@ -378,15 +408,15 @@ func (k *KiwoomClient) GetTopVolumeStocks(market string, count int) ([]models.St
|
||||
}
|
||||
|
||||
body, err := k.post("ka10030", "/api/dostk/rkinfo", map[string]string{
|
||||
"mrkt_tp": mrktTp,
|
||||
"sort_tp": "1", // 거래량 기준 정렬
|
||||
"mrkt_tp": mrktTp,
|
||||
"sort_tp": "1", // 거래량 기준 정렬
|
||||
"mang_stk_incls": "1", // 관리종목 미포함
|
||||
"crd_tp": "0",
|
||||
"trde_qty_tp": "0",
|
||||
"pric_tp": "0",
|
||||
"trde_prica_tp": "0",
|
||||
"mrkt_open_tp": "0",
|
||||
"stex_tp": "3", // 통합
|
||||
"crd_tp": "0",
|
||||
"trde_qty_tp": "0",
|
||||
"pric_tp": "0",
|
||||
"trde_prica_tp": "0",
|
||||
"mrkt_open_tp": "0",
|
||||
"stex_tp": "3", // 통합
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -485,15 +515,15 @@ func (k *KiwoomClient) GetTopFluctuation(market string, ascending bool, count in
|
||||
}
|
||||
|
||||
body, err := k.post("ka10027", "/api/dostk/rkinfo", map[string]string{
|
||||
"mrkt_tp": mrktTp,
|
||||
"sort_tp": sortTp,
|
||||
"trde_qty_cnd": "0000", // 거래량 전체
|
||||
"stk_cnd": "0", // 종목조건 전체
|
||||
"crd_cnd": "0", // 신용조건 전체
|
||||
"updown_incls": "1", // 상하한포함
|
||||
"pric_cnd": "0", // 가격조건 전체
|
||||
"trde_prica_cnd": "0", // 거래대금조건 전체
|
||||
"stex_tp": "1", // KRX
|
||||
"mrkt_tp": mrktTp,
|
||||
"sort_tp": sortTp,
|
||||
"trde_qty_cnd": "0000", // 거래량 전체
|
||||
"stk_cnd": "0", // 종목조건 전체
|
||||
"crd_cnd": "0", // 신용조건 전체
|
||||
"updown_incls": "1", // 상하한포함
|
||||
"pric_cnd": "0", // 가격조건 전체
|
||||
"trde_prica_cnd": "0", // 거래대금조건 전체
|
||||
"stex_tp": "1", // KRX
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"stocksearch/config"
|
||||
"stocksearch/models"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -14,10 +15,18 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
kiwoomWSURL = "wss://api.kiwoom.com:10000/api/dostk/websocket"
|
||||
writeTimeout = 10 * time.Second // 쓰기 타임아웃
|
||||
)
|
||||
|
||||
// kiwoomWSURL 키움 WS 서버 URL (모의투자 여부에 따라 분기)
|
||||
func kiwoomWSURL() string {
|
||||
base := config.App.BaseURL
|
||||
if strings.Contains(base, "mockapi") {
|
||||
return "wss://mockapi.kiwoom.com:10000/api/dostk/websocket"
|
||||
}
|
||||
return "wss://api.kiwoom.com:10000/api/dostk/websocket"
|
||||
}
|
||||
|
||||
// KiwoomWSClient 키움증권 실시간 WebSocket 클라이언트
|
||||
type KiwoomWSClient struct {
|
||||
tokenService *TokenService
|
||||
@@ -90,7 +99,7 @@ func (k *KiwoomWSClient) Connect() error {
|
||||
// dial WSS 연결 수립 후 로그인 패킷 전송
|
||||
func (k *KiwoomWSClient) dial() (*websocket.Conn, error) {
|
||||
// HTTP 헤더 없이 연결 (키움 WS는 헤더 인증 불필요)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(kiwoomWSURL, nil)
|
||||
conn, _, err := websocket.DefaultDialer.Dial(kiwoomWSURL(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ type ScannerService struct {
|
||||
stockSvc *StockService
|
||||
analysis *AnalysisService
|
||||
mu sync.RWMutex
|
||||
enabled int32 // atomic: 1=켜짐(기본), 0=꺼짐
|
||||
enabled int32 // atomic: 1=켜짐(기본), 0=꺼짐
|
||||
signals []SignalStock
|
||||
history map[string]*cntrHistory // 종목별 체결강도 이력
|
||||
volumeHistory map[string]*volumeHist // 종목별 거래량 이력
|
||||
@@ -406,25 +406,17 @@ func (s *ScannerService) scan() {
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// ── 호가잔량 병렬 조회 (체결강도 상승 종목에 한해) ────────────────
|
||||
if len(signals) > 0 {
|
||||
var wg sync.WaitGroup
|
||||
for i := range signals {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
ask, bid, _, err := s.kiwoom.getOrderBook(signals[idx].Code)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signals[idx].TotalAskVol = ask
|
||||
signals[idx].TotalBidVol = bid
|
||||
if bid > 0 {
|
||||
signals[idx].AskBidRatio = float64(ask) / float64(bid)
|
||||
}
|
||||
}(i)
|
||||
// ── 호가잔량 순차 조회 (체결강도 상승 종목에 한해) ────────────────
|
||||
for i := range signals {
|
||||
ask, bid, _, err := s.kiwoom.getOrderBook(signals[i].Code)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
signals[i].TotalAskVol = ask
|
||||
signals[i].TotalBidVol = bid
|
||||
if bid > 0 {
|
||||
signals[i].AskBidRatio = float64(ask) / float64(bid)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// ── 최종 스코어 및 신호 유형 계산 (호가잔량 포함) ────────────────
|
||||
@@ -642,25 +634,17 @@ func (s *ScannerService) AnalyzeWatchlist(codes []string) []SignalStock {
|
||||
})
|
||||
}
|
||||
|
||||
// Phase 3: 호가잔량 병렬 조회
|
||||
if len(signals) > 0 {
|
||||
var wg sync.WaitGroup
|
||||
for i := range signals {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
ask, bid, _, err := s.kiwoom.getOrderBook(signals[idx].Code)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signals[idx].TotalAskVol = ask
|
||||
signals[idx].TotalBidVol = bid
|
||||
if bid > 0 {
|
||||
signals[idx].AskBidRatio = float64(ask) / float64(bid)
|
||||
}
|
||||
}(i)
|
||||
// Phase 3: 호가잔량 순차 조회
|
||||
for i := range signals {
|
||||
ask, bid, _, err := s.kiwoom.getOrderBook(signals[i].Code)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
signals[i].TotalAskVol = ask
|
||||
signals[i].TotalBidVol = bid
|
||||
if bid > 0 {
|
||||
signals[i].AskBidRatio = float64(ask) / float64(bid)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Phase 4: 스코어 및 신호 유형 계산
|
||||
|
||||
Reference in New Issue
Block a user