Skip to content

Commit 6364644

Browse files
committed
feat: 텔레메트리 v3 스키마, 서버 실데이터 반영, 하드코딩 제거 (v2.7.0)
- 텔레메트리 스키마 v2→v3: 레벨별 강화 상세, 모드, 강화비용, 사이클시간, 배틀손실 추가 - 서버: 실측 데이터 기반 강화 확률/배틀 승률 반환 (minSampleSize=10) - 서버: enhance_level_detail 테이블 추가, /api/stats/enhance-levels 엔드포인트 - 서버: SQLite v3 마이그레이션 (ALTER TABLE + COALESCE) - risk.go: 하드코딩된 확률/비용 테이블 제거, game API 데이터 사용 - 배틀 패배 시 골드 손실 추적 (goldChange 음수 처리) - dead code 정리: 미사용 RecordEnhance/RecordBattle 제거 - risk.go 중복 formatGold 제거 → game.FormatGold 사용
1 parent 634ef2e commit 6364644

File tree

6 files changed

+359
-206
lines changed

6 files changed

+359
-206
lines changed

cmd/sword-api/main.go

Lines changed: 187 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ type TelemetryStats struct {
163163
SwordSaleStats map[string]*SwordSaleStat `json:"sword_sale_stats,omitempty"`
164164
SwordEnhanceStats map[string]*SwordEnhanceStat `json:"sword_enhance_stats,omitempty"`
165165
ItemFarmingStats map[string]*ItemFarmingStat `json:"item_farming_stats,omitempty"`
166+
167+
// === v3 새로 추가 ===
168+
EnhanceLevelDetail map[int]*EnhanceLevelStat `json:"enhance_level_detail,omitempty"`
169+
EnhanceCostTotal int `json:"enhance_cost_total"`
170+
CycleTimeTotal float64 `json:"cycle_time_total"`
171+
BattleGoldLost int `json:"battle_gold_lost"`
166172
}
167173

168174
// === v2 구조체들 ===
@@ -201,6 +207,17 @@ type ItemFarmingStat struct {
201207
TotalCount int `json:"total_count"`
202208
SpecialCount int `json:"special_count"`
203209
NormalCount int `json:"normal_count"`
210+
TrashCount int `json:"trash_count"`
211+
}
212+
213+
// === v3 구조체들 ===
214+
215+
// EnhanceLevelStat 레벨별 강화 상세 통계
216+
type EnhanceLevelStat struct {
217+
Attempts int `json:"attempts"`
218+
Success int `json:"success"`
219+
Fail int `json:"fail"`
220+
Destroy int `json:"destroy"`
204221
}
205222

206223
type TelemetryPayload struct {
@@ -209,6 +226,7 @@ type TelemetryPayload struct {
209226
OSType string `json:"os_type"`
210227
SessionID string `json:"session_id"`
211228
Period string `json:"period"`
229+
Mode string `json:"mode,omitempty"` // v3: 현재 모드
212230
Stats TelemetryStats `json:"stats"`
213231
}
214232

@@ -240,6 +258,12 @@ type StatsStore struct {
240258
swordSaleStats map[string]*SwordSaleStat
241259
swordEnhanceStats map[string]*SwordEnhanceStat
242260
itemFarmingStats map[string]*ItemFarmingStat
261+
262+
// === v3 통계 ===
263+
enhanceLevelDetail map[int]*EnhanceLevelStat
264+
enhanceCostTotal int
265+
cycleTimeTotal float64
266+
battleGoldLost int
243267
}
244268

245269
var stats = &StatsStore{
@@ -250,6 +274,7 @@ var stats = &StatsStore{
250274
swordSaleStats: make(map[string]*SwordSaleStat),
251275
swordEnhanceStats: make(map[string]*SwordEnhanceStat),
252276
itemFarmingStats: make(map[string]*ItemFarmingStat),
277+
enhanceLevelDetail: make(map[int]*EnhanceLevelStat),
253278
}
254279

255280
// ========================
@@ -329,13 +354,14 @@ func getGameData() GameData {
329354
enhanceRates := make([]EnhanceRate, len(defaultEnhanceRates))
330355
copy(enhanceRates, defaultEnhanceRates)
331356

332-
// 레벨별 강화 통계가 있으면 실측 데이터로 대체
333-
for _, stat := range stats.swordEnhanceStats {
334-
// 전체 강화 통계로 계산 (검 종류 무관)
335-
if stat.Attempts >= minSampleSize {
336-
// 레벨별 통계가 아닌 전체 통계이므로, 개별 레벨 업데이트는 추후 구현
337-
// 현재는 전체 성공률만 로깅
338-
break
357+
// v3: 레벨별 강화 상세 통계가 있으면 실측 확률로 대체
358+
for i := range enhanceRates {
359+
lvl := enhanceRates[i].Level
360+
if detail, ok := stats.enhanceLevelDetail[lvl]; ok && detail.Attempts >= minSampleSize {
361+
total := float64(detail.Attempts)
362+
enhanceRates[i].SuccessRate = float64(detail.Success) / total * 100
363+
enhanceRates[i].KeepRate = float64(detail.Fail) / total * 100
364+
enhanceRates[i].DestroyRate = float64(detail.Destroy) / total * 100
339365
}
340366
}
341367

@@ -502,7 +528,26 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) {
502528
stats.itemFarmingStats[name].TotalCount += stat.TotalCount
503529
stats.itemFarmingStats[name].SpecialCount += stat.SpecialCount
504530
stats.itemFarmingStats[name].NormalCount += stat.NormalCount
531+
stats.itemFarmingStats[name].TrashCount += stat.TrashCount
532+
}
533+
}
534+
535+
// v3 통계 (schema_version >= 3)
536+
if payload.SchemaVersion >= 3 {
537+
// 레벨별 강화 상세 통계
538+
for lvl, stat := range payload.Stats.EnhanceLevelDetail {
539+
if stats.enhanceLevelDetail[lvl] == nil {
540+
stats.enhanceLevelDetail[lvl] = &EnhanceLevelStat{}
541+
}
542+
stats.enhanceLevelDetail[lvl].Attempts += stat.Attempts
543+
stats.enhanceLevelDetail[lvl].Success += stat.Success
544+
stats.enhanceLevelDetail[lvl].Fail += stat.Fail
545+
stats.enhanceLevelDetail[lvl].Destroy += stat.Destroy
505546
}
547+
548+
stats.enhanceCostTotal += payload.Stats.EnhanceCostTotal
549+
stats.cycleTimeTotal += payload.Stats.CycleTimeTotal
550+
stats.battleGoldLost += payload.Stats.BattleGoldLost
506551
}
507552
stats.mu.Unlock()
508553

@@ -511,7 +556,11 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) {
511556
go saveToDB()
512557
}
513558

514-
log.Printf("[텔레메트리] 세션=%s 버전=%s OS=%s", payload.SessionID[:8], payload.AppVersion, payload.OSType)
559+
modeStr := payload.Mode
560+
if modeStr == "" {
561+
modeStr = "-"
562+
}
563+
log.Printf("[텔레메트리] 세션=%s 버전=%s OS=%s 모드=%s v%d", payload.SessionID[:8], payload.AppVersion, payload.OSType, modeStr, payload.SchemaVersion)
515564

516565
w.WriteHeader(http.StatusOK)
517566
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
@@ -678,12 +727,10 @@ func handleUpsetStats(w http.ResponseWriter, r *http.Request) {
678727
stats.mu.RLock()
679728
defer stats.mu.RUnlock()
680729

681-
// 이론 승률 (레벨 차이 1-20)
682-
theoryRates := map[int]float64{
683-
1: 35.0, 2: 20.0, 3: 10.0, 4: 5.0, 5: 3.0,
684-
6: 2.0, 7: 1.5, 8: 1.0, 9: 0.7, 10: 0.5,
685-
11: 0.35, 12: 0.25, 13: 0.18, 14: 0.12, 15: 0.08,
686-
16: 0.05, 17: 0.03, 18: 0.02, 19: 0.01, 20: 0.005,
730+
// 이론 승률: defaultBattleRewards에서 추출
731+
theoryRates := make(map[int]float64)
732+
for _, br := range defaultBattleRewards {
733+
theoryRates[br.LevelDiff] = br.WinRate
687734
}
688735

689736
type DiffStat struct {
@@ -945,6 +992,55 @@ func handleOptimalSellPoint(w http.ResponseWriter, r *http.Request) {
945992
})
946993
}
947994

995+
// v3: 레벨별 강화 실측 통계
996+
func handleEnhanceLevelDetail(w http.ResponseWriter, r *http.Request) {
997+
w.Header().Set("Content-Type", "application/json")
998+
w.Header().Set("Access-Control-Allow-Origin", "*")
999+
1000+
stats.mu.RLock()
1001+
defer stats.mu.RUnlock()
1002+
1003+
type LevelEntry struct {
1004+
Level int `json:"level"`
1005+
Attempts int `json:"attempts"`
1006+
Success int `json:"success"`
1007+
Fail int `json:"fail"`
1008+
Destroy int `json:"destroy"`
1009+
SuccessRate float64 `json:"success_rate"`
1010+
KeepRate float64 `json:"keep_rate"`
1011+
DestroyRate float64 `json:"destroy_rate"`
1012+
Default bool `json:"is_default"` // 기본값 사용 여부
1013+
}
1014+
1015+
var levels []LevelEntry
1016+
for _, def := range defaultEnhanceRates {
1017+
entry := LevelEntry{
1018+
Level: def.Level,
1019+
SuccessRate: def.SuccessRate,
1020+
KeepRate: def.KeepRate,
1021+
DestroyRate: def.DestroyRate,
1022+
Default: true,
1023+
}
1024+
if detail, ok := stats.enhanceLevelDetail[def.Level]; ok && detail.Attempts > 0 {
1025+
entry.Attempts = detail.Attempts
1026+
entry.Success = detail.Success
1027+
entry.Fail = detail.Fail
1028+
entry.Destroy = detail.Destroy
1029+
total := float64(detail.Attempts)
1030+
entry.SuccessRate = float64(detail.Success) / total * 100
1031+
entry.KeepRate = float64(detail.Fail) / total * 100
1032+
entry.DestroyRate = float64(detail.Destroy) / total * 100
1033+
entry.Default = detail.Attempts < minSampleSize
1034+
}
1035+
levels = append(levels, entry)
1036+
}
1037+
1038+
json.NewEncoder(w).Encode(map[string]interface{}{
1039+
"min_sample_size": minSampleSize,
1040+
"levels": levels,
1041+
})
1042+
}
1043+
9481044
func generateSignature(sessionID, period string) string {
9491045
h := sha256.Sum256([]byte(sessionID + period + getAppSecret()))
9501046
return hex.EncodeToString(h[:])[:16]
@@ -1001,6 +1097,9 @@ func validateTelemetryPayload(p *TelemetryPayload) error {
10011097
if len(p.Stats.ItemFarmingStats) > maxMapEntries {
10021098
return fmt.Errorf("item_farming_stats too many entries")
10031099
}
1100+
if len(p.Stats.EnhanceLevelDetail) > maxMapEntries {
1101+
return fmt.Errorf("enhance_level_detail too many entries")
1102+
}
10041103

10051104
// 맵 키 길이 검증
10061105
for name := range p.Stats.SwordBattleStats {
@@ -1058,6 +1157,22 @@ func validateStatValues(s *TelemetryStats) error {
10581157
}
10591158
}
10601159

1160+
// v3 값 검증
1161+
if s.EnhanceCostTotal < 0 || s.BattleGoldLost < 0 {
1162+
return fmt.Errorf("negative v3 gold values")
1163+
}
1164+
if s.CycleTimeTotal < 0 {
1165+
return fmt.Errorf("negative cycle time")
1166+
}
1167+
for lvl, stat := range s.EnhanceLevelDetail {
1168+
if lvl < 0 || lvl > 20 {
1169+
return fmt.Errorf("invalid enhance level detail: %d", lvl)
1170+
}
1171+
if stat != nil && (stat.Attempts < 0 || stat.Success < 0 || stat.Fail < 0 || stat.Destroy < 0) {
1172+
return fmt.Errorf("negative enhance level detail for level %d", lvl)
1173+
}
1174+
}
1175+
10611176
// 역배 레벨차 검증 (1-20 허용)
10621177
for diff, stat := range s.UpsetStatsByDiff {
10631178
if diff < 1 || diff > 20 {
@@ -1147,7 +1262,16 @@ func initDB() error {
11471262
name TEXT PRIMARY KEY,
11481263
total_count INTEGER DEFAULT 0,
11491264
special_count INTEGER DEFAULT 0,
1150-
normal_count INTEGER DEFAULT 0
1265+
normal_count INTEGER DEFAULT 0,
1266+
trash_count INTEGER DEFAULT 0
1267+
)`,
1268+
// v3 테이블
1269+
`CREATE TABLE IF NOT EXISTS enhance_level_detail (
1270+
level INTEGER PRIMARY KEY,
1271+
attempts INTEGER DEFAULT 0,
1272+
success INTEGER DEFAULT 0,
1273+
fail INTEGER DEFAULT 0,
1274+
destroy INTEGER DEFAULT 0
11511275
)`,
11521276
}
11531277

@@ -1157,6 +1281,17 @@ func initDB() error {
11571281
}
11581282
}
11591283

1284+
// v3 마이그레이션: global_stats에 새 컬럼 추가 (이미 있으면 무시)
1285+
migrations := []string{
1286+
"ALTER TABLE global_stats ADD COLUMN enhance_cost_total INTEGER DEFAULT 0",
1287+
"ALTER TABLE global_stats ADD COLUMN cycle_time_total REAL DEFAULT 0",
1288+
"ALTER TABLE global_stats ADD COLUMN battle_gold_lost INTEGER DEFAULT 0",
1289+
"ALTER TABLE item_farming_stats ADD COLUMN trash_count INTEGER DEFAULT 0",
1290+
}
1291+
for _, m := range migrations {
1292+
db.Exec(m) // 이미 존재하면 에러 → 무시
1293+
}
1294+
11601295
// global_stats 초기 행 (없으면 생성)
11611296
db.Exec("INSERT OR IGNORE INTO global_stats (id) VALUES (1)")
11621297

@@ -1168,12 +1303,13 @@ func loadFromDB() error {
11681303
stats.mu.Lock()
11691304
defer stats.mu.Unlock()
11701305

1171-
// global_stats 로드
1172-
row := db.QueryRow("SELECT enhance_attempts, enhance_success, enhance_fail, enhance_destroy, battle_count, battle_wins, upset_attempts, upset_wins, battle_gold, farming_attempts, special_found, sales_count, sales_total_gold FROM global_stats WHERE id=1")
1306+
// global_stats 로드 (v3 컬럼 포함)
1307+
row := db.QueryRow("SELECT enhance_attempts, enhance_success, enhance_fail, enhance_destroy, battle_count, battle_wins, upset_attempts, upset_wins, battle_gold, farming_attempts, special_found, sales_count, sales_total_gold, COALESCE(enhance_cost_total,0), COALESCE(cycle_time_total,0), COALESCE(battle_gold_lost,0) FROM global_stats WHERE id=1")
11731308
if err := row.Scan(
11741309
&stats.enhanceAttempts, &stats.enhanceSuccess, &stats.enhanceFail, &stats.enhanceDestroy,
11751310
&stats.battleCount, &stats.battleWins, &stats.upsetAttempts, &stats.upsetWins, &stats.battleGold,
11761311
&stats.farmingAttempts, &stats.specialFound, &stats.salesCount, &stats.salesTotalGold,
1312+
&stats.enhanceCostTotal, &stats.cycleTimeTotal, &stats.battleGoldLost,
11771313
); err != nil && err != sql.ErrNoRows {
11781314
return fmt.Errorf("global_stats 로드 실패: %v", err)
11791315
}
@@ -1262,19 +1398,33 @@ func loadFromDB() error {
12621398
}
12631399

12641400
// item_farming_stats 로드
1265-
rows, err = db.Query("SELECT name, total_count, special_count, normal_count FROM item_farming_stats")
1401+
rows, err = db.Query("SELECT name, total_count, special_count, normal_count, COALESCE(trash_count,0) FROM item_farming_stats")
12661402
if err != nil {
12671403
return fmt.Errorf("item_farming_stats 로드 실패: %v", err)
12681404
}
12691405
defer rows.Close()
12701406
for rows.Next() {
12711407
var name string
12721408
s := &ItemFarmingStat{}
1273-
if err := rows.Scan(&name, &s.TotalCount, &s.SpecialCount, &s.NormalCount); err == nil {
1409+
if err := rows.Scan(&name, &s.TotalCount, &s.SpecialCount, &s.NormalCount, &s.TrashCount); err == nil {
12741410
stats.itemFarmingStats[name] = s
12751411
}
12761412
}
12771413

1414+
// v3: enhance_level_detail 로드
1415+
rows, err = db.Query("SELECT level, attempts, success, fail, destroy FROM enhance_level_detail")
1416+
if err != nil {
1417+
return fmt.Errorf("enhance_level_detail 로드 실패: %v", err)
1418+
}
1419+
defer rows.Close()
1420+
for rows.Next() {
1421+
var level int
1422+
s := &EnhanceLevelStat{}
1423+
if err := rows.Scan(&level, &s.Attempts, &s.Success, &s.Fail, &s.Destroy); err == nil {
1424+
stats.enhanceLevelDetail[level] = s
1425+
}
1426+
}
1427+
12781428
log.Printf("📦 DB에서 통계 로드 완료")
12791429
return nil
12801430
}
@@ -1290,15 +1440,17 @@ func saveToDB() {
12901440
}
12911441
defer tx.Rollback()
12921442

1293-
// global_stats 저장
1443+
// global_stats 저장 (v3 컬럼 포함)
12941444
tx.Exec(`UPDATE global_stats SET
12951445
enhance_attempts=?, enhance_success=?, enhance_fail=?, enhance_destroy=?,
12961446
battle_count=?, battle_wins=?, upset_attempts=?, upset_wins=?, battle_gold=?,
1297-
farming_attempts=?, special_found=?, sales_count=?, sales_total_gold=?
1447+
farming_attempts=?, special_found=?, sales_count=?, sales_total_gold=?,
1448+
enhance_cost_total=?, cycle_time_total=?, battle_gold_lost=?
12981449
WHERE id=1`,
12991450
stats.enhanceAttempts, stats.enhanceSuccess, stats.enhanceFail, stats.enhanceDestroy,
13001451
stats.battleCount, stats.battleWins, stats.upsetAttempts, stats.upsetWins, stats.battleGold,
13011452
stats.farmingAttempts, stats.specialFound, stats.salesCount, stats.salesTotalGold,
1453+
stats.enhanceCostTotal, stats.cycleTimeTotal, stats.battleGoldLost,
13021454
)
13031455

13041456
// enhance_by_level 저장
@@ -1337,8 +1489,14 @@ func saveToDB() {
13371489

13381490
// item_farming_stats 저장
13391491
for name, s := range stats.itemFarmingStats {
1340-
tx.Exec("INSERT OR REPLACE INTO item_farming_stats (name, total_count, special_count, normal_count) VALUES (?, ?, ?, ?)",
1341-
name, s.TotalCount, s.SpecialCount, s.NormalCount)
1492+
tx.Exec("INSERT OR REPLACE INTO item_farming_stats (name, total_count, special_count, normal_count, trash_count) VALUES (?, ?, ?, ?, ?)",
1493+
name, s.TotalCount, s.SpecialCount, s.NormalCount, s.TrashCount)
1494+
}
1495+
1496+
// v3: enhance_level_detail 저장
1497+
for lvl, s := range stats.enhanceLevelDetail {
1498+
tx.Exec("INSERT OR REPLACE INTO enhance_level_detail (level, attempts, success, fail, destroy) VALUES (?, ?, ?, ?, ?)",
1499+
lvl, s.Attempts, s.Success, s.Fail, s.Destroy)
13421500
}
13431501

13441502
if err := tx.Commit(); err != nil {
@@ -1376,18 +1534,21 @@ func main() {
13761534
http.HandleFunc("/api/stats/enhance", handleEnhanceStats)
13771535
http.HandleFunc("/api/stats/sales", handleSaleStats)
13781536
http.HandleFunc("/api/strategy/optimal-sell-point", handleOptimalSellPoint)
1537+
// v3 엔드포인트
1538+
http.HandleFunc("/api/stats/enhance-levels", handleEnhanceLevelDetail)
13791539

13801540
log.Printf("🚀 Sword API 서버 시작 (포트: %s)", port)
1381-
log.Printf(" /api/game-data - 게임 데이터 조회")
1382-
log.Printf(" /api/telemetry - 텔레메트리 수신")
1541+
log.Printf(" /api/game-data - 게임 데이터 조회 (실측 확률 반영)")
1542+
log.Printf(" /api/telemetry - 텔레메트리 수신 (v3 스키마)")
13831543
log.Printf(" /api/stats/detailed - 커뮤니티 통계")
13841544
log.Printf(" /api/stats/swords - 검 종류별 승률 (v2)")
13851545
log.Printf(" /api/stats/special - 특수 검 출현 확률 (v2)")
13861546
log.Printf(" /api/stats/upset - 역배 실측 승률 (v2)")
13871547
log.Printf(" /api/stats/items - 아이템 파밍 통계 (v2)")
13881548
log.Printf(" /api/stats/enhance - 검 종류별 강화 성공률 (v2)")
13891549
log.Printf(" /api/stats/sales - 검+레벨별 판매 통계 (v2)")
1390-
log.Printf(" /api/strategy/optimal-sell-point - 최적 판매 시점 (v2)")
1550+
log.Printf(" /api/stats/enhance-levels - 레벨별 강화 확률 (v3)")
1551+
log.Printf(" /api/strategy/optimal-sell-point - 최적 판매 시점")
13911552

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

cmd/sword-macro/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func init() {
1919
runtime.LockOSThread()
2020
}
2121

22-
const VERSION = "2.6.2"
22+
const VERSION = "2.7.0"
2323

2424
func main() {
2525
// Windows 콘솔 ANSI 지원 활성화 및 UTF-8 설정

0 commit comments

Comments
 (0)