Skip to content

Commit 934c754

Browse files
committed
feat: SQLite 영구저장, 파서 개선, 데이터 캐시 추가 (v2.6.0)
- API 서버 SQLite 영구 저장소 추가 (modernc.org/sqlite, WAL 모드) - 아이템 판별 v4 로직 (특수 무기 패턴 우선 체크) - 게임 데이터 TTL 캐시 (5분/10분) - 응답 변경 감지 방식 개선 (lastRawChatText) - 골드 파싱 마지막 매칭, 정규식 사전 컴파일 - 전략 프로필 포맷 버그 수정 (rune → fmt.Sprintf) - .gitignore 바이너리 경로 수정
1 parent 62d3bf5 commit 934c754

File tree

11 files changed

+807
-353
lines changed

11 files changed

+807
-353
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ build/
2323
*.exe
2424

2525
# Build binaries
26-
sword-api
27-
sword-macro
26+
/sword-api
27+
/sword-macro
2828
.telemetry_state.json

cmd/sword-api/main.go

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
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+
1520
const (
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+
10641349
func 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"

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.5.5"
22+
const VERSION = "2.6.0"
2323

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

go.mod

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
module github.com/StopDragon/sword-macro-ai
22

3-
go 1.21
3+
go 1.24.0
44

55
require github.com/google/uuid v1.6.0
6+
7+
require (
8+
github.com/dustin/go-humanize v1.0.1 // indirect
9+
github.com/mattn/go-isatty v0.0.20 // indirect
10+
github.com/ncruces/go-strftime v1.0.0 // indirect
11+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
12+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
13+
golang.org/x/sys v0.37.0 // indirect
14+
modernc.org/libc v1.67.6 // indirect
15+
modernc.org/mathutil v1.7.1 // indirect
16+
modernc.org/memory v1.11.0 // indirect
17+
modernc.org/sqlite v1.44.3 // indirect
18+
)

go.sum

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,23 @@
1+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
13
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
24
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
5+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
6+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
7+
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
8+
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
9+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
10+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
11+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
12+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
13+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
15+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
16+
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
17+
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
18+
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
19+
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
20+
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
21+
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
22+
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
23+
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=

0 commit comments

Comments
 (0)