@@ -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
206223type 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
245269var 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+
9481044func 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 )
0 commit comments