-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdatabase.js
More file actions
312 lines (278 loc) · 10.5 KB
/
database.js
File metadata and controls
312 lines (278 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
// Node.js v22.5+ 내장 SQLite 사용 (외부 패키지 불필요)
const { DatabaseSync } = require("node:sqlite");
const path = require("path");
const fs = require("fs");
const DB_PATH = process.env.DB_PATH || path.join(__dirname, "fines.db");
const dbDir = path.dirname(DB_PATH);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const db = new DatabaseSync(DB_PATH);
// ── 테이블 초기화 ────────────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS fines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL DEFAULT '',
user_id TEXT NOT NULL,
username TEXT NOT NULL,
word_used TEXT NOT NULL,
message_content TEXT,
amount INTEGER NOT NULL DEFAULT 5000,
paid INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'auto',
reporter_id TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
);
CREATE TABLE IF NOT EXISTS settings (
guild_id TEXT NOT NULL DEFAULT '',
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (guild_id, key)
);
`);
// 기존 DB 마이그레이션 (컬럼이 없을 때만 추가)
for (const col of [
`ALTER TABLE fines ADD COLUMN reporter_id TEXT`,
`ALTER TABLE fines ADD COLUMN status TEXT NOT NULL DEFAULT 'auto'`,
`ALTER TABLE fines ADD COLUMN message_content TEXT`,
`ALTER TABLE fines ADD COLUMN message_id TEXT`,
`ALTER TABLE fines ADD COLUMN guild_id TEXT NOT NULL DEFAULT ''`,
]) {
try { db.exec(col); } catch { /* 이미 존재하면 무시 */ }
}
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_fines_guild ON fines(guild_id)`); } catch {}
// settings 테이블 guild_id 마이그레이션 (기존 DB에 guild_id 컬럼 없는 경우)
const settingsCols = db.prepare(`PRAGMA table_info(settings)`).all();
if (!settingsCols.some(c => c.name === 'guild_id')) {
db.exec(`
ALTER TABLE settings RENAME TO settings_old;
CREATE TABLE settings (
guild_id TEXT NOT NULL DEFAULT '',
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (guild_id, key)
);
INSERT INTO settings (guild_id, key, value) SELECT '', key, value FROM settings_old;
DROP TABLE settings_old;
`);
}
// 기본 설정값 삽입 (이미 있으면 무시) — 마이그레이션 이후 실행
db.prepare(`INSERT OR IGNORE INTO settings (guild_id, key, value) VALUES ('', 'fine_amount', '5000')`).run();
db.prepare(`INSERT OR IGNORE INTO settings (guild_id, key, value) VALUES ('', 'false_report_threshold', '3')`).run();
// ── 설정 ─────────────────────────────────────────────────────────────────────
function getSetting(guildId, key) {
const row = db.prepare(`SELECT value FROM settings WHERE guild_id = ? AND key = ?`).get(guildId, key);
if (row) return row.value;
return db.prepare(`SELECT value FROM settings WHERE guild_id = '' AND key = ?`).get(key)?.value;
}
function setSetting(guildId, key, value) {
db.prepare(`INSERT OR REPLACE INTO settings (guild_id, key, value) VALUES (?, ?, ?)`).run(guildId, key, String(value));
}
function getFineAmount(guildId) {
return parseInt(getSetting(guildId, "fine_amount") || "5000", 10);
}
function getFalseReportThreshold(guildId) {
return parseInt(getSetting(guildId, "false_report_threshold") || "3", 10);
}
// ── 벌금 CRUD ────────────────────────────────────────────────────────────────
/**
* 벌금/신고 기록 추가
* @param {{
* guildId: string, userId: string, username: string, wordUsed: string, amount: number,
* messageContent?: string, messageId?: string, reporterId?: string, status?: 'auto'|'pending'
* }} param
*/
function addFine({ guildId, userId, username, wordUsed, amount, messageContent = null, messageId = null, reporterId = null, status = "auto" }) {
return db
.prepare(
`INSERT INTO fines (guild_id, user_id, username, word_used, amount, message_content, message_id, reporter_id, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.run(guildId, userId, username, wordUsed, amount, messageContent, messageId, reporterId, status);
}
/**
* Discord 메시지 ID로 이미 처리된 벌금/신고가 있는지 확인
* @param {string} guildId
* @param {string} messageId
* @returns {object|undefined}
*/
function findFineByMessageId(guildId, messageId) {
return db
.prepare(`SELECT id, status FROM fines WHERE guild_id = ? AND message_id = ? AND status != 'rejected'`)
.get(guildId, messageId);
}
/**
* 전체 미납 벌금 목록 — auto/approved 만 집계
*/
function getAllUnpaidSummary(guildId) {
return db
.prepare(
`SELECT user_id, username, COUNT(*) AS count, SUM(amount) AS total
FROM fines
WHERE guild_id = ? AND paid = 0 AND status IN ('auto', 'approved')
GROUP BY user_id
ORDER BY total DESC`
)
.all(guildId);
}
/**
* 특정 유저의 미납 벌금 상세 내역 — auto/approved 만
*/
function getUserUnpaidFines(guildId, userId) {
return db
.prepare(
`SELECT id, word_used, message_content, amount, reporter_id, status, created_at
FROM fines
WHERE guild_id = ? AND user_id = ? AND paid = 0 AND status IN ('auto', 'approved')
ORDER BY created_at DESC`
)
.all(guildId, userId);
}
/**
* 특정 유저의 전체 벌금 내역 (납부 포함, 모든 상태)
*/
function getUserAllFines(guildId, userId) {
return db
.prepare(
`SELECT id, word_used, message_content, amount, paid, reporter_id, status, created_at
FROM fines
WHERE guild_id = ? AND user_id = ?
ORDER BY created_at DESC`
)
.all(guildId, userId);
}
/**
* 특정 유저의 미납 벌금 납부 처리 — auto/approved 만
*/
function markUserPaid(guildId, userId) {
return db
.prepare(`UPDATE fines SET paid = 1 WHERE guild_id = ? AND user_id = ? AND paid = 0 AND status IN ('auto', 'approved')`)
.run(guildId, userId).changes;
}
// ── 신고 검토 ─────────────────────────────────────────────────────────────────
/**
* 검토 대기 중인 신고 목록
*/
function getPendingReports(guildId) {
return db
.prepare(
`SELECT id, user_id, username, message_content, reporter_id, amount, created_at
FROM fines
WHERE guild_id = ? AND status = 'pending'
ORDER BY created_at ASC`
)
.all(guildId);
}
/**
* 신고 승인 → 벌금 확정
*/
function approveReport(id) {
return db.prepare(`UPDATE fines SET status = 'approved' WHERE id = ? AND status = 'pending'`).run(id).changes;
}
/**
* 신고 기각 → 벌금 취소
*/
function rejectReport(id) {
return db.prepare(`UPDATE fines SET status = 'rejected' WHERE id = ? AND status = 'pending'`).run(id).changes;
}
/**
* 벌금 강제 취소 (관리자) — pending/auto/approved 모두 가능
* @returns {number} 변경된 행 수 (0이면 이미 rejected이거나 없는 ID 또는 타 길드)
*/
function cancelFine(guildId, id) {
return db.prepare(`UPDATE fines SET status = 'rejected' WHERE id = ? AND guild_id = ? AND status != 'rejected'`).run(id, guildId).changes;
}
/**
* ID로 단일 벌금 기록 조회
*/
function getFineById(id) {
return db.prepare(`SELECT * FROM fines WHERE id = ?`).get(id);
}
/**
* 특정 신고자의 기각된 신고 수
*/
function getReporterRejectedCount(guildId, reporterId) {
const row = db
.prepare(`SELECT COUNT(*) AS count FROM fines WHERE guild_id = ? AND reporter_id = ? AND status = 'rejected'`)
.get(guildId, reporterId);
return row?.count ?? 0;
}
// ── 통계 ─────────────────────────────────────────────────────────────────────
function getStats(guildId) {
return db
.prepare(
`SELECT
COUNT(*) AS total_count,
SUM(CASE WHEN status IN ('auto','approved') THEN amount END) AS total_amount,
SUM(CASE WHEN status IN ('auto','approved') AND paid=0 THEN amount END) AS unpaid_amount,
SUM(CASE WHEN status IN ('auto','approved') AND paid=1 THEN amount END) AS paid_amount,
COUNT(DISTINCT CASE WHEN status IN ('auto','approved') THEN user_id END) AS unique_users,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending_count,
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) AS rejected_count,
COUNT(CASE WHEN reporter_id IS NOT NULL THEN 1 END) AS reported_count
FROM fines
WHERE guild_id = ?`
)
.get(guildId);
}
function getWordRanking(guildId) {
return db
.prepare(
`SELECT word_used, COUNT(*) AS count
FROM fines
WHERE guild_id = ? AND status IN ('auto', 'approved')
GROUP BY word_used
ORDER BY count DESC
LIMIT 10`
)
.all(guildId);
}
/**
* 허위 신고 벌금 통계 (건수 + 합계)
*/
function getFalseReportFinesStats(guildId) {
return db
.prepare(
`SELECT COUNT(*) AS count, COALESCE(SUM(amount), 0) AS total
FROM fines
WHERE guild_id = ? AND word_used = '[허위 신고]' AND status IN ('auto', 'approved')`
)
.get(guildId);
}
/**
* 전체 적발 유저 목록 (납부 여부 무관)
*/
function getAllCaughtUsers(guildId) {
return db
.prepare(
`SELECT user_id, username, COUNT(*) AS count, SUM(amount) AS total,
SUM(CASE WHEN paid = 0 THEN amount ELSE 0 END) AS unpaid
FROM fines
WHERE guild_id = ? AND status IN ('auto', 'approved')
GROUP BY user_id
ORDER BY count DESC`
)
.all(guildId);
}
module.exports = {
addFine,
findFineByMessageId,
getAllUnpaidSummary,
getAllCaughtUsers,
getUserUnpaidFines,
getUserAllFines,
markUserPaid,
getPendingReports,
approveReport,
rejectReport,
cancelFine,
getFineById,
getReporterRejectedCount,
getStats,
getWordRanking,
getFalseReportFinesStats,
getSetting,
setSetting,
getFineAmount,
getFalseReportThreshold,
};