Skip to content

Commit 47c4301

Browse files
StopDragonclaude
andcommitted
Add telemetry v2 with detailed item and sword tracking
- Track all items (not just hidden) with name extraction - Add sword-specific battle statistics and win rates - Add session tracking with ROI, max drawdown, Sharpe ratio - Add risk calculator and strategy profiles - New API endpoints: /api/stats/swords, /api/stats/hidden, /api/stats/upset, /api/stats/items - Schema version bump to v2 with backwards compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 30cfddd commit 47c4301

File tree

9 files changed

+2678
-42
lines changed

9 files changed

+2678
-42
lines changed

cmd/sword-api/main.go

Lines changed: 271 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type GameData struct {
5353
// 텔레메트리 구조체
5454
// ========================
5555

56+
// === v1 통계 ===
5657
type TelemetryStats struct {
5758
TotalCycles int `json:"total_cycles"`
5859
SuccessfulCycles int `json:"successful_cycles"`
@@ -77,6 +78,43 @@ type TelemetryStats struct {
7778
FarmingAttempts int `json:"farming_attempts"`
7879
HiddenFound int `json:"hidden_found"`
7980
TrashFound int `json:"trash_found"`
81+
82+
// === v2 새로 추가 ===
83+
SwordBattleStats map[string]*SwordBattleStat `json:"sword_battle_stats,omitempty"`
84+
HiddenFoundByName map[string]int `json:"hidden_found_by_name,omitempty"`
85+
UpsetStatsByDiff map[int]*UpsetStat `json:"upset_stats_by_diff,omitempty"`
86+
SwordSaleStats map[string]*SwordSaleStat `json:"sword_sale_stats,omitempty"`
87+
ItemFarmingStats map[string]*ItemFarmingStat `json:"item_farming_stats,omitempty"`
88+
}
89+
90+
// === v2 구조체들 ===
91+
92+
// SwordBattleStat 검 종류별 배틀 통계
93+
type SwordBattleStat struct {
94+
BattleCount int `json:"battle_count"`
95+
BattleWins int `json:"battle_wins"`
96+
UpsetAttempts int `json:"upset_attempts"`
97+
UpsetWins int `json:"upset_wins"`
98+
}
99+
100+
// UpsetStat 레벨차별 역배 통계
101+
type UpsetStat struct {
102+
Attempts int `json:"attempts"`
103+
Wins int `json:"wins"`
104+
GoldEarned int `json:"gold_earned"`
105+
}
106+
107+
// SwordSaleStat 검 종류별 판매 통계
108+
type SwordSaleStat struct {
109+
TotalPrice int `json:"total_price"`
110+
Count int `json:"count"`
111+
}
112+
113+
// ItemFarmingStat 아이템별 파밍 통계
114+
type ItemFarmingStat struct {
115+
TotalCount int `json:"total_count"`
116+
HiddenCount int `json:"hidden_count"`
117+
NormalCount int `json:"normal_count"`
80118
}
81119

82120
type TelemetryPayload struct {
@@ -108,10 +146,22 @@ type StatsStore struct {
108146
hiddenFound int
109147
salesCount int
110148
salesTotalGold int
149+
150+
// === v2 통계 ===
151+
swordBattleStats map[string]*SwordBattleStat
152+
hiddenFoundByName map[string]int
153+
upsetStatsByDiff map[int]*UpsetStat
154+
swordSaleStats map[string]*SwordSaleStat
155+
itemFarmingStats map[string]*ItemFarmingStat
111156
}
112157

113158
var stats = &StatsStore{
114-
enhanceByLevel: make(map[int]int),
159+
enhanceByLevel: make(map[int]int),
160+
swordBattleStats: make(map[string]*SwordBattleStat),
161+
hiddenFoundByName: make(map[string]int),
162+
upsetStatsByDiff: make(map[int]*UpsetStat),
163+
swordSaleStats: make(map[string]*SwordSaleStat),
164+
itemFarmingStats: make(map[string]*ItemFarmingStat),
115165
}
116166

117167
// ========================
@@ -213,6 +263,7 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) {
213263

214264
// 통계 업데이트
215265
stats.mu.Lock()
266+
// v1 통계
216267
stats.enhanceAttempts += payload.Stats.EnhanceAttempts
217268
stats.enhanceSuccess += payload.Stats.EnhanceSuccess
218269
stats.enhanceFail += payload.Stats.EnhanceFail
@@ -229,6 +280,54 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) {
229280
stats.hiddenFound += payload.Stats.HiddenFound
230281
stats.salesCount += payload.Stats.SalesCount
231282
stats.salesTotalGold += payload.Stats.SalesTotalGold
283+
284+
// v2 통계 (schema_version >= 2)
285+
if payload.SchemaVersion >= 2 {
286+
// 검 종류별 배틀 통계
287+
for name, stat := range payload.Stats.SwordBattleStats {
288+
if stats.swordBattleStats[name] == nil {
289+
stats.swordBattleStats[name] = &SwordBattleStat{}
290+
}
291+
stats.swordBattleStats[name].BattleCount += stat.BattleCount
292+
stats.swordBattleStats[name].BattleWins += stat.BattleWins
293+
stats.swordBattleStats[name].UpsetAttempts += stat.UpsetAttempts
294+
stats.swordBattleStats[name].UpsetWins += stat.UpsetWins
295+
}
296+
297+
// 히든 이름별 통계
298+
for name, cnt := range payload.Stats.HiddenFoundByName {
299+
stats.hiddenFoundByName[name] += cnt
300+
}
301+
302+
// 레벨차별 역배 통계
303+
for diff, stat := range payload.Stats.UpsetStatsByDiff {
304+
if stats.upsetStatsByDiff[diff] == nil {
305+
stats.upsetStatsByDiff[diff] = &UpsetStat{}
306+
}
307+
stats.upsetStatsByDiff[diff].Attempts += stat.Attempts
308+
stats.upsetStatsByDiff[diff].Wins += stat.Wins
309+
stats.upsetStatsByDiff[diff].GoldEarned += stat.GoldEarned
310+
}
311+
312+
// 검 판매 통계
313+
for key, stat := range payload.Stats.SwordSaleStats {
314+
if stats.swordSaleStats[key] == nil {
315+
stats.swordSaleStats[key] = &SwordSaleStat{}
316+
}
317+
stats.swordSaleStats[key].TotalPrice += stat.TotalPrice
318+
stats.swordSaleStats[key].Count += stat.Count
319+
}
320+
321+
// 아이템 파밍 통계
322+
for name, stat := range payload.Stats.ItemFarmingStats {
323+
if stats.itemFarmingStats[name] == nil {
324+
stats.itemFarmingStats[name] = &ItemFarmingStat{}
325+
}
326+
stats.itemFarmingStats[name].TotalCount += stat.TotalCount
327+
stats.itemFarmingStats[name].HiddenCount += stat.HiddenCount
328+
stats.itemFarmingStats[name].NormalCount += stat.NormalCount
329+
}
330+
}
232331
stats.mu.Unlock()
233332

234333
log.Printf("[텔레메트리] 세션=%s 버전=%s OS=%s", payload.SessionID[:8], payload.AppVersion, payload.OSType)
@@ -317,6 +416,168 @@ func handleHealth(w http.ResponseWriter, r *http.Request) {
317416
})
318417
}
319418

419+
// === v2 API 엔드포인트 ===
420+
421+
// 검 종류별 승률 랭킹
422+
func handleSwordStats(w http.ResponseWriter, r *http.Request) {
423+
w.Header().Set("Content-Type", "application/json")
424+
w.Header().Set("Access-Control-Allow-Origin", "*")
425+
426+
stats.mu.RLock()
427+
defer stats.mu.RUnlock()
428+
429+
type SwordEntry struct {
430+
Name string `json:"name"`
431+
BattleCount int `json:"battle_count"`
432+
WinRate float64 `json:"win_rate"`
433+
UpsetWinRate float64 `json:"upset_win_rate"`
434+
}
435+
436+
var swords []SwordEntry
437+
for name, stat := range stats.swordBattleStats {
438+
winRate := 0.0
439+
upsetWinRate := 0.0
440+
if stat.BattleCount > 0 {
441+
winRate = float64(stat.BattleWins) / float64(stat.BattleCount) * 100
442+
}
443+
if stat.UpsetAttempts > 0 {
444+
upsetWinRate = float64(stat.UpsetWins) / float64(stat.UpsetAttempts) * 100
445+
}
446+
swords = append(swords, SwordEntry{
447+
Name: name,
448+
BattleCount: stat.BattleCount,
449+
WinRate: winRate,
450+
UpsetWinRate: upsetWinRate,
451+
})
452+
}
453+
454+
json.NewEncoder(w).Encode(map[string]interface{}{
455+
"swords": swords,
456+
})
457+
}
458+
459+
// 히든 검 출현 확률
460+
func handleHiddenStats(w http.ResponseWriter, r *http.Request) {
461+
w.Header().Set("Content-Type", "application/json")
462+
w.Header().Set("Access-Control-Allow-Origin", "*")
463+
464+
stats.mu.RLock()
465+
defer stats.mu.RUnlock()
466+
467+
type HiddenEntry struct {
468+
Name string `json:"name"`
469+
Count int `json:"count"`
470+
Rate float64 `json:"rate"`
471+
}
472+
473+
var hidden []HiddenEntry
474+
for name, cnt := range stats.hiddenFoundByName {
475+
rate := 0.0
476+
if stats.farmingAttempts > 0 {
477+
rate = float64(cnt) / float64(stats.farmingAttempts) * 100
478+
}
479+
hidden = append(hidden, HiddenEntry{
480+
Name: name,
481+
Count: cnt,
482+
Rate: rate,
483+
})
484+
}
485+
486+
json.NewEncoder(w).Encode(map[string]interface{}{
487+
"total_farming": stats.farmingAttempts,
488+
"hidden": hidden,
489+
})
490+
}
491+
492+
// 역배 실측 승률
493+
func handleUpsetStats(w http.ResponseWriter, r *http.Request) {
494+
w.Header().Set("Content-Type", "application/json")
495+
w.Header().Set("Access-Control-Allow-Origin", "*")
496+
497+
stats.mu.RLock()
498+
defer stats.mu.RUnlock()
499+
500+
// 이론 승률
501+
theoryRates := map[int]float64{
502+
1: 35.0,
503+
2: 20.0,
504+
3: 10.0,
505+
}
506+
507+
type DiffStat struct {
508+
Attempts int `json:"attempts"`
509+
Wins int `json:"wins"`
510+
WinRate float64 `json:"win_rate"`
511+
Theory float64 `json:"theory"`
512+
GoldEarned int `json:"gold_earned"`
513+
}
514+
515+
byDiff := make(map[string]DiffStat)
516+
for diff := 1; diff <= 3; diff++ {
517+
stat := stats.upsetStatsByDiff[diff]
518+
winRate := 0.0
519+
attempts := 0
520+
wins := 0
521+
gold := 0
522+
if stat != nil {
523+
attempts = stat.Attempts
524+
wins = stat.Wins
525+
gold = stat.GoldEarned
526+
if attempts > 0 {
527+
winRate = float64(wins) / float64(attempts) * 100
528+
}
529+
}
530+
byDiff[fmt.Sprintf("%d", diff)] = DiffStat{
531+
Attempts: attempts,
532+
Wins: wins,
533+
WinRate: winRate,
534+
Theory: theoryRates[diff],
535+
GoldEarned: gold,
536+
}
537+
}
538+
539+
json.NewEncoder(w).Encode(map[string]interface{}{
540+
"by_level_diff": byDiff,
541+
})
542+
}
543+
544+
// 아이템 파밍 통계
545+
func handleItemStats(w http.ResponseWriter, r *http.Request) {
546+
w.Header().Set("Content-Type", "application/json")
547+
w.Header().Set("Access-Control-Allow-Origin", "*")
548+
549+
stats.mu.RLock()
550+
defer stats.mu.RUnlock()
551+
552+
type ItemEntry struct {
553+
Name string `json:"name"`
554+
TotalCount int `json:"total_count"`
555+
HiddenCount int `json:"hidden_count"`
556+
NormalCount int `json:"normal_count"`
557+
HiddenRate float64 `json:"hidden_rate"`
558+
}
559+
560+
var items []ItemEntry
561+
for name, stat := range stats.itemFarmingStats {
562+
hiddenRate := 0.0
563+
if stat.TotalCount > 0 {
564+
hiddenRate = float64(stat.HiddenCount) / float64(stat.TotalCount) * 100
565+
}
566+
items = append(items, ItemEntry{
567+
Name: name,
568+
TotalCount: stat.TotalCount,
569+
HiddenCount: stat.HiddenCount,
570+
NormalCount: stat.NormalCount,
571+
HiddenRate: hiddenRate,
572+
})
573+
}
574+
575+
json.NewEncoder(w).Encode(map[string]interface{}{
576+
"total_farming": stats.farmingAttempts,
577+
"items": items,
578+
})
579+
}
580+
320581
func generateSignature(sessionID, period string) string {
321582
h := sha256.Sum256([]byte(sessionID + period + appSecret))
322583
return hex.EncodeToString(h[:])[:16]
@@ -334,11 +595,20 @@ func main() {
334595
http.HandleFunc("/api/game-data", handleGameData)
335596
http.HandleFunc("/api/telemetry", handleTelemetry)
336597
http.HandleFunc("/api/stats/detailed", handleStatsDetailed)
598+
// v2 엔드포인트
599+
http.HandleFunc("/api/stats/swords", handleSwordStats)
600+
http.HandleFunc("/api/stats/hidden", handleHiddenStats)
601+
http.HandleFunc("/api/stats/upset", handleUpsetStats)
602+
http.HandleFunc("/api/stats/items", handleItemStats)
337603

338604
log.Printf("🚀 Sword API 서버 시작 (포트: %s)", port)
339605
log.Printf(" /api/game-data - 게임 데이터 조회")
340606
log.Printf(" /api/telemetry - 텔레메트리 수신")
341607
log.Printf(" /api/stats/detailed - 커뮤니티 통계")
608+
log.Printf(" /api/stats/swords - 검 종류별 승률 (v2)")
609+
log.Printf(" /api/stats/hidden - 히든 검 출현 확률 (v2)")
610+
log.Printf(" /api/stats/upset - 역배 실측 승률 (v2)")
611+
log.Printf(" /api/stats/items - 아이템 파밍 통계 (v2)")
342612

343613
if err := http.ListenAndServe(":"+port, nil); err != nil {
344614
log.Fatal(err)

0 commit comments

Comments
 (0)