@@ -2,6 +2,7 @@ package main
22
33import (
44 "crypto/sha256"
5+ "database/sql"
56 "encoding/hex"
67 "encoding/json"
78 "fmt"
@@ -10,8 +11,12 @@ import (
1011 "os"
1112 "sync"
1213 "time"
14+
15+ _ "modernc.org/sqlite"
1316)
1417
18+ var db * sql.DB
19+
1520const (
1621 defaultAppSecret = "sw0rd-m4cr0-2026-s3cr3t-k3y" // 환경변수 없을 때 기본값
1722 appSecretEnvVar = "SWORD_APP_SECRET"
@@ -501,6 +506,11 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) {
501506 }
502507 stats .mu .Unlock ()
503508
509+ // SQLite에 영구 저장
510+ if db != nil {
511+ go saveToDB ()
512+ }
513+
504514 log .Printf ("[텔레메트리] 세션=%s 버전=%s OS=%s" , payload .SessionID [:8 ], payload .AppVersion , payload .OSType )
505515
506516 w .WriteHeader (http .StatusOK )
@@ -1061,7 +1071,292 @@ func validateStatValues(s *TelemetryStats) error {
10611071 return nil
10621072}
10631073
1074+ // ========================
1075+ // SQLite 영구 저장소
1076+ // ========================
1077+
1078+ func initDB () error {
1079+ dbPath := os .Getenv ("DB_PATH" )
1080+ if dbPath == "" {
1081+ dbPath = "./sword-stats.db"
1082+ }
1083+
1084+ var err error
1085+ db , err = sql .Open ("sqlite" , dbPath )
1086+ if err != nil {
1087+ return fmt .Errorf ("DB 열기 실패: %v" , err )
1088+ }
1089+
1090+ // WAL 모드 (동시 읽기/쓰기 성능 향상)
1091+ if _ , err := db .Exec ("PRAGMA journal_mode=WAL" ); err != nil {
1092+ return fmt .Errorf ("WAL 설정 실패: %v" , err )
1093+ }
1094+
1095+ // 테이블 생성
1096+ tables := []string {
1097+ `CREATE TABLE IF NOT EXISTS global_stats (
1098+ id INTEGER PRIMARY KEY DEFAULT 1,
1099+ enhance_attempts INTEGER DEFAULT 0,
1100+ enhance_success INTEGER DEFAULT 0,
1101+ enhance_fail INTEGER DEFAULT 0,
1102+ enhance_destroy INTEGER DEFAULT 0,
1103+ battle_count INTEGER DEFAULT 0,
1104+ battle_wins INTEGER DEFAULT 0,
1105+ upset_attempts INTEGER DEFAULT 0,
1106+ upset_wins INTEGER DEFAULT 0,
1107+ battle_gold INTEGER DEFAULT 0,
1108+ farming_attempts INTEGER DEFAULT 0,
1109+ special_found INTEGER DEFAULT 0,
1110+ sales_count INTEGER DEFAULT 0,
1111+ sales_total_gold INTEGER DEFAULT 0
1112+ )` ,
1113+ `CREATE TABLE IF NOT EXISTS enhance_by_level (
1114+ level INTEGER PRIMARY KEY,
1115+ count INTEGER DEFAULT 0
1116+ )` ,
1117+ `CREATE TABLE IF NOT EXISTS sword_battle_stats (
1118+ name TEXT PRIMARY KEY,
1119+ battle_count INTEGER DEFAULT 0,
1120+ battle_wins INTEGER DEFAULT 0,
1121+ upset_attempts INTEGER DEFAULT 0,
1122+ upset_wins INTEGER DEFAULT 0
1123+ )` ,
1124+ `CREATE TABLE IF NOT EXISTS special_found_by_name (
1125+ name TEXT PRIMARY KEY,
1126+ count INTEGER DEFAULT 0
1127+ )` ,
1128+ `CREATE TABLE IF NOT EXISTS upset_stats_by_diff (
1129+ level_diff INTEGER PRIMARY KEY,
1130+ attempts INTEGER DEFAULT 0,
1131+ wins INTEGER DEFAULT 0,
1132+ gold_earned INTEGER DEFAULT 0
1133+ )` ,
1134+ `CREATE TABLE IF NOT EXISTS sword_sale_stats (
1135+ key TEXT PRIMARY KEY,
1136+ total_price INTEGER DEFAULT 0,
1137+ count INTEGER DEFAULT 0
1138+ )` ,
1139+ `CREATE TABLE IF NOT EXISTS sword_enhance_stats (
1140+ name TEXT PRIMARY KEY,
1141+ attempts INTEGER DEFAULT 0,
1142+ success INTEGER DEFAULT 0,
1143+ fail INTEGER DEFAULT 0,
1144+ destroy INTEGER DEFAULT 0
1145+ )` ,
1146+ `CREATE TABLE IF NOT EXISTS item_farming_stats (
1147+ name TEXT PRIMARY KEY,
1148+ total_count INTEGER DEFAULT 0,
1149+ special_count INTEGER DEFAULT 0,
1150+ normal_count INTEGER DEFAULT 0
1151+ )` ,
1152+ }
1153+
1154+ for _ , ddl := range tables {
1155+ if _ , err := db .Exec (ddl ); err != nil {
1156+ return fmt .Errorf ("테이블 생성 실패: %v" , err )
1157+ }
1158+ }
1159+
1160+ // global_stats 초기 행 (없으면 생성)
1161+ db .Exec ("INSERT OR IGNORE INTO global_stats (id) VALUES (1)" )
1162+
1163+ log .Printf ("📦 SQLite DB 초기화 완료: %s" , dbPath )
1164+ return nil
1165+ }
1166+
1167+ func loadFromDB () error {
1168+ stats .mu .Lock ()
1169+ defer stats .mu .Unlock ()
1170+
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" )
1173+ if err := row .Scan (
1174+ & stats .enhanceAttempts , & stats .enhanceSuccess , & stats .enhanceFail , & stats .enhanceDestroy ,
1175+ & stats .battleCount , & stats .battleWins , & stats .upsetAttempts , & stats .upsetWins , & stats .battleGold ,
1176+ & stats .farmingAttempts , & stats .specialFound , & stats .salesCount , & stats .salesTotalGold ,
1177+ ); err != nil && err != sql .ErrNoRows {
1178+ return fmt .Errorf ("global_stats 로드 실패: %v" , err )
1179+ }
1180+
1181+ // enhance_by_level 로드
1182+ rows , err := db .Query ("SELECT level, count FROM enhance_by_level" )
1183+ if err != nil {
1184+ return fmt .Errorf ("enhance_by_level 로드 실패: %v" , err )
1185+ }
1186+ defer rows .Close ()
1187+ for rows .Next () {
1188+ var level , count int
1189+ if err := rows .Scan (& level , & count ); err == nil {
1190+ stats .enhanceByLevel [level ] = count
1191+ }
1192+ }
1193+
1194+ // sword_battle_stats 로드
1195+ rows , err = db .Query ("SELECT name, battle_count, battle_wins, upset_attempts, upset_wins FROM sword_battle_stats" )
1196+ if err != nil {
1197+ return fmt .Errorf ("sword_battle_stats 로드 실패: %v" , err )
1198+ }
1199+ defer rows .Close ()
1200+ for rows .Next () {
1201+ var name string
1202+ s := & SwordBattleStat {}
1203+ if err := rows .Scan (& name , & s .BattleCount , & s .BattleWins , & s .UpsetAttempts , & s .UpsetWins ); err == nil {
1204+ stats .swordBattleStats [name ] = s
1205+ }
1206+ }
1207+
1208+ // special_found_by_name 로드
1209+ rows , err = db .Query ("SELECT name, count FROM special_found_by_name" )
1210+ if err != nil {
1211+ return fmt .Errorf ("special_found_by_name 로드 실패: %v" , err )
1212+ }
1213+ defer rows .Close ()
1214+ for rows .Next () {
1215+ var name string
1216+ var count int
1217+ if err := rows .Scan (& name , & count ); err == nil {
1218+ stats .specialFoundByName [name ] = count
1219+ }
1220+ }
1221+
1222+ // upset_stats_by_diff 로드
1223+ rows , err = db .Query ("SELECT level_diff, attempts, wins, gold_earned FROM upset_stats_by_diff" )
1224+ if err != nil {
1225+ return fmt .Errorf ("upset_stats_by_diff 로드 실패: %v" , err )
1226+ }
1227+ defer rows .Close ()
1228+ for rows .Next () {
1229+ var diff int
1230+ s := & UpsetStat {}
1231+ if err := rows .Scan (& diff , & s .Attempts , & s .Wins , & s .GoldEarned ); err == nil {
1232+ stats .upsetStatsByDiff [diff ] = s
1233+ }
1234+ }
1235+
1236+ // sword_sale_stats 로드
1237+ rows , err = db .Query ("SELECT key, total_price, count FROM sword_sale_stats" )
1238+ if err != nil {
1239+ return fmt .Errorf ("sword_sale_stats 로드 실패: %v" , err )
1240+ }
1241+ defer rows .Close ()
1242+ for rows .Next () {
1243+ var key string
1244+ s := & SwordSaleStat {}
1245+ if err := rows .Scan (& key , & s .TotalPrice , & s .Count ); err == nil {
1246+ stats .swordSaleStats [key ] = s
1247+ }
1248+ }
1249+
1250+ // sword_enhance_stats 로드
1251+ rows , err = db .Query ("SELECT name, attempts, success, fail, destroy FROM sword_enhance_stats" )
1252+ if err != nil {
1253+ return fmt .Errorf ("sword_enhance_stats 로드 실패: %v" , err )
1254+ }
1255+ defer rows .Close ()
1256+ for rows .Next () {
1257+ var name string
1258+ s := & SwordEnhanceStat {}
1259+ if err := rows .Scan (& name , & s .Attempts , & s .Success , & s .Fail , & s .Destroy ); err == nil {
1260+ stats .swordEnhanceStats [name ] = s
1261+ }
1262+ }
1263+
1264+ // item_farming_stats 로드
1265+ rows , err = db .Query ("SELECT name, total_count, special_count, normal_count FROM item_farming_stats" )
1266+ if err != nil {
1267+ return fmt .Errorf ("item_farming_stats 로드 실패: %v" , err )
1268+ }
1269+ defer rows .Close ()
1270+ for rows .Next () {
1271+ var name string
1272+ s := & ItemFarmingStat {}
1273+ if err := rows .Scan (& name , & s .TotalCount , & s .SpecialCount , & s .NormalCount ); err == nil {
1274+ stats .itemFarmingStats [name ] = s
1275+ }
1276+ }
1277+
1278+ log .Printf ("📦 DB에서 통계 로드 완료" )
1279+ return nil
1280+ }
1281+
1282+ func saveToDB () {
1283+ stats .mu .RLock ()
1284+ defer stats .mu .RUnlock ()
1285+
1286+ tx , err := db .Begin ()
1287+ if err != nil {
1288+ log .Printf ("[DB] 트랜잭션 시작 실패: %v" , err )
1289+ return
1290+ }
1291+ defer tx .Rollback ()
1292+
1293+ // global_stats 저장
1294+ tx .Exec (`UPDATE global_stats SET
1295+ enhance_attempts=?, enhance_success=?, enhance_fail=?, enhance_destroy=?,
1296+ battle_count=?, battle_wins=?, upset_attempts=?, upset_wins=?, battle_gold=?,
1297+ farming_attempts=?, special_found=?, sales_count=?, sales_total_gold=?
1298+ WHERE id=1` ,
1299+ stats .enhanceAttempts , stats .enhanceSuccess , stats .enhanceFail , stats .enhanceDestroy ,
1300+ stats .battleCount , stats .battleWins , stats .upsetAttempts , stats .upsetWins , stats .battleGold ,
1301+ stats .farmingAttempts , stats .specialFound , stats .salesCount , stats .salesTotalGold ,
1302+ )
1303+
1304+ // enhance_by_level 저장
1305+ for level , count := range stats .enhanceByLevel {
1306+ tx .Exec ("INSERT OR REPLACE INTO enhance_by_level (level, count) VALUES (?, ?)" , level , count )
1307+ }
1308+
1309+ // sword_battle_stats 저장
1310+ for name , s := range stats .swordBattleStats {
1311+ tx .Exec ("INSERT OR REPLACE INTO sword_battle_stats (name, battle_count, battle_wins, upset_attempts, upset_wins) VALUES (?, ?, ?, ?, ?)" ,
1312+ name , s .BattleCount , s .BattleWins , s .UpsetAttempts , s .UpsetWins )
1313+ }
1314+
1315+ // special_found_by_name 저장
1316+ for name , count := range stats .specialFoundByName {
1317+ tx .Exec ("INSERT OR REPLACE INTO special_found_by_name (name, count) VALUES (?, ?)" , name , count )
1318+ }
1319+
1320+ // upset_stats_by_diff 저장
1321+ for diff , s := range stats .upsetStatsByDiff {
1322+ tx .Exec ("INSERT OR REPLACE INTO upset_stats_by_diff (level_diff, attempts, wins, gold_earned) VALUES (?, ?, ?, ?)" ,
1323+ diff , s .Attempts , s .Wins , s .GoldEarned )
1324+ }
1325+
1326+ // sword_sale_stats 저장
1327+ for key , s := range stats .swordSaleStats {
1328+ tx .Exec ("INSERT OR REPLACE INTO sword_sale_stats (key, total_price, count) VALUES (?, ?, ?)" ,
1329+ key , s .TotalPrice , s .Count )
1330+ }
1331+
1332+ // sword_enhance_stats 저장
1333+ for name , s := range stats .swordEnhanceStats {
1334+ tx .Exec ("INSERT OR REPLACE INTO sword_enhance_stats (name, attempts, success, fail, destroy) VALUES (?, ?, ?, ?, ?)" ,
1335+ name , s .Attempts , s .Success , s .Fail , s .Destroy )
1336+ }
1337+
1338+ // item_farming_stats 저장
1339+ 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 )
1342+ }
1343+
1344+ if err := tx .Commit (); err != nil {
1345+ log .Printf ("[DB] 커밋 실패: %v" , err )
1346+ }
1347+ }
1348+
10641349func main () {
1350+ // SQLite 초기화
1351+ if err := initDB (); err != nil {
1352+ log .Printf ("⚠️ DB 초기화 실패 (인메모리 모드로 동작): %v" , err )
1353+ } else {
1354+ defer db .Close ()
1355+ if err := loadFromDB (); err != nil {
1356+ log .Printf ("⚠️ DB 로드 실패: %v" , err )
1357+ }
1358+ }
1359+
10651360 port := os .Getenv ("PORT" )
10661361 if port == "" {
10671362 port = "8080"
0 commit comments