Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
src/services/bookmark.service.ts
공개 가능
1
import fs from 'fs';
2
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
3
import { User } from '../types/models';
4
5
const DAILY_LIMIT = 10;
6
7
function readUsers(): User[] {
8
try {
9
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
10
} catch {
11
return [];
12
}
13
}
14
15
function writeUsers(users: User[]): void {
16
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
17
}
18
19
function getTodayString(): string {
20
return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
21
}
22
23
export function awardBookmarks(username: string, amount: number): { awarded: number; total: number } {
24
const users = readUsers();
25
const user = users.find(u => u.username === username);
26
if (!user) return { awarded: 0, total: 0 };
27
28
const today = getTodayString();
29
30
// 날짜가 바뀌었으면 일일 카운터 리셋
31
if (user.lastActiveDate !== today) {
32
user.dailyBookmarks = 0;
33
user.lastActiveDate = today;
34
}
35
36
const dailyUsed = user.dailyBookmarks || 0;
37
const remaining = Math.max(0, DAILY_LIMIT - dailyUsed);
38
const awarded = Math.min(amount, remaining);
39
40
if (awarded > 0) {
41
user.bookmarks = (user.bookmarks || 0) + awarded;
42
user.dailyBookmarks = dailyUsed + awarded;
43
writeUsers(users);
44
}
45
46
return { awarded, total: user.bookmarks || 0 };
47
}
48
49
export function getBookmarks(username: string): number {
50
const users = readUsers();
51
const user = users.find(u => u.username === username);
52
return user?.bookmarks || 0;
53
}
54
55
export function getTetrisBookmarks(username: string): number {
56
const users = readUsers();
57
const user = users.find(u => u.username === username);
58
return user?.tetrisBookmarks || 0;
59
}
60
61
export function canClaimTetrisToday(username: string): boolean {
62
const users = readUsers();
63
const user = users.find(u => u.username === username);
64
if (!user) return false;
65
const today = getTodayString();
66
if (user.lastTetrisDate === today && (user.dailyTetrisBookmarks || 0) > 0) return false;
67
return true;
68
}
69
70
export function spendBookmarks(username: string, amount: number): { success: boolean; remaining: number } {
71
const users = readUsers();
72
const user = users.find(u => u.username === username);
73
if (!user) return { success: false, remaining: 0 };
74
75
const current = user.bookmarks || 0;
76
if (current < amount) return { success: false, remaining: current };
77
78
user.bookmarks = current - amount;
79
writeUsers(users);
80
return { success: true, remaining: user.bookmarks };
81
}
82
83
const BADGE_COST = 15;
84
const BADGE_DURATION_DAYS = 3;
85
86
export function getVerifiedUntil(username: string): string | null {
87
const users = readUsers();
88
const user = users.find(u => u.username === username);
89
if (!user || !user.verifiedUntil) return null;
90
return new Date(user.verifiedUntil) > new Date() ? user.verifiedUntil : null;
91
}
92
93
export function purchaseVerificationBadge(username: string): { success: boolean; message: string; remaining: number; verifiedUntil?: string } {
94
const users = readUsers();
95
const user = users.find(u => u.username === username);
96
if (!user) return { success: false, message: '사용자를 찾을 수 없습니다.', remaining: 0 };
97
98
const current = user.bookmarks || 0;
99
if (current < BADGE_COST) {
100
return { success: false, message: `책갈피가 부족합니다. (보유: ${current}개, 필요: ${BADGE_COST}개)`, remaining: current };
101
}
102
103
user.bookmarks = current - BADGE_COST;
104
105
const now = new Date();
106
const currentExpiry = user.verifiedUntil ? new Date(user.verifiedUntil) : null;
107
const baseDate = (currentExpiry && currentExpiry > now) ? currentExpiry : now;
108
baseDate.setDate(baseDate.getDate() + BADGE_DURATION_DAYS);
109
user.verifiedUntil = baseDate.toISOString();
110
111
writeUsers(users);
112
return { success: true, message: '인증마크가 활성화되었습니다!', remaining: user.bookmarks, verifiedUntil: user.verifiedUntil };
113
}
114
115
export function isVerified(username: string): boolean {
116
const users = readUsers();
117
const user = users.find(u => u.username === username);
118
if (!user || !user.verifiedUntil) return false;
119
return new Date(user.verifiedUntil) > new Date();
120
}
121
122
export function getVerifiedUsers(): string[] {
123
const users = readUsers();
124
const now = new Date();
125
return users
126
.filter(u => u.verifiedUntil && new Date(u.verifiedUntil) > now)
127
.map(u => u.username);
128
}
129
130
const TETRIS_DAILY_LIMIT = 10;
131
132
export function awardTetrisBookmarks(username: string, hinanaScore: number): { awarded: number; total: number; alreadyClaimed: boolean } {
133
const users = readUsers();
134
const user = users.find(u => u.username === username);
135
if (!user) return { awarded: 0, total: 0, alreadyClaimed: false };
136
137
const today = getTodayString();
138
139
// 날짜가 바뀌었으면 리셋
140
if (user.lastTetrisDate !== today) {
141
user.dailyTetrisBookmarks = 0;
142
user.lastTetrisDate = today;
143
}
144
145
// 이미 오늘 전환했으면 차단
146
if ((user.dailyTetrisBookmarks || 0) > 0) {
147
return { awarded: 0, total: user.bookmarks || 0, alreadyClaimed: true };
148
}
149
150
const awarded = Math.min(Math.floor(hinanaScore / 3300), TETRIS_DAILY_LIMIT);
151
152
if (awarded > 0) {
153
user.bookmarks = (user.bookmarks || 0) + awarded;
154
user.tetrisBookmarks = (user.tetrisBookmarks || 0) + awarded;
155
user.dailyTetrisBookmarks = awarded;
156
writeUsers(users);
157
}
158
159
return { awarded, total: user.bookmarks || 0, alreadyClaimed: false };
160
}
161
162
export function getAllUsersBookmarks(): { username: string; bookmarks: number; profileImage: string | null }[] {
163
const users = readUsers();
164
return users.map(u => ({ username: u.username, bookmarks: u.bookmarks || 0, profileImage: u.profileImage || null }));
165
}
166
167
export function adminAdjustBookmarks(username: string, amount: number): { success: boolean; message: string; newTotal: number } {
168
const users = readUsers();
169
const user = users.find(u => u.username === username);
170
if (!user) return { success: false, message: '사용자를 찾을 수 없습니다.', newTotal: 0 };
171
172
const current = user.bookmarks || 0;
173
user.bookmarks = Math.max(0, current + amount);
174
writeUsers(users);
175
176
const action = amount > 0 ? '지급' : '차감';
177
return { success: true, message: `${username}에게 책갈피 ${Math.abs(amount)}개 ${action} 완료. (${current} → ${user.bookmarks})`, newTotal: user.bookmarks };
178
}
179
180
export function adminBulkAdjustBookmarks(amount: number): { success: boolean; message: string; results: { username: string; newTotal: number }[] } {
181
const users = readUsers();
182
const results: { username: string; newTotal: number }[] = [];
183
184
users.forEach(u => {
185
const current = u.bookmarks || 0;
186
u.bookmarks = Math.max(0, current + amount);
187
results.push({ username: u.username, newTotal: u.bookmarks });
188
});
189
190
writeUsers(users);
191
const action = amount > 0 ? '지급' : '차감';
192
return { success: true, message: `전체 ${users.length}명에게 책갈피 ${Math.abs(amount)}개 ${action} 완료.`, results };
193
}
194
195
export function setProfileImage(username: string, imagePath: string): { success: boolean; oldImage: string | null } {
196
const users = readUsers();
197
const user = users.find(u => u.username === username);
198
if (!user) return { success: false, oldImage: null };
199
200
const oldImage = user.profileImage || null;
201
user.profileImage = imagePath;
202
writeUsers(users);
203
return { success: true, oldImage };
204
}
205
206
export function getProfileImage(username: string): string | null {
207
const users = readUsers();
208
const user = users.find(u => u.username === username);
209
return user?.profileImage || null;
210
}
211
212
export function deleteProfileImage(username: string): { success: boolean; oldImage: string | null } {
213
const users = readUsers();
214
const user = users.find(u => u.username === username);
215
if (!user) return { success: false, oldImage: null };
216
217
const oldImage = user.profileImage || null;
218
delete user.profileImage;
219
writeUsers(users);
220
return { success: true, oldImage };
221
}
222
223
export function getUserProfileImages(): Record<string, string | null> {
224
const users = readUsers();
225
const map: Record<string, string | null> = {};
226
users.forEach(u => {
227
if (u.profileImage) map[u.username] = u.profileImage;
228
});
229
return map;
230
}
231
232
export function getDailyPostNotif(username: string): boolean {
233
const users = readUsers();
234
const user = users.find(u => u.username === username);
235
return user?.dailyPostNotif ?? true; // 기본값: 활성화
236
}
237
238
export function toggleDailyPostNotif(username: string): boolean {
239
const users = readUsers();
240
const user = users.find(u => u.username === username);
241
if (!user) return false;
242
user.dailyPostNotif = !(user.dailyPostNotif ?? true);
243
writeUsers(users);
244
return user.dailyPostNotif;
245
}
246
247
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
248
249
function getKstDateString(): string {
250
// UTC+9 offset으로 KST 날짜 계산
251
return new Date(Date.now() + 9 * 3600000).toISOString().slice(0, 10);
252
}
253
254
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
255
const users = readUsers();
256
const user = users.find(u => u.username === username);
257
if (!user) return 0;
258
const today = getKstDateString();
259
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
260
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
261
}
262
263
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
264
const users = readUsers();
265
const user = users.find(u => u.username === username);
266
if (!user) return { success: false, remaining: 0 };
267
268
const today = getKstDateString();
269
// 날짜가 바뀌었으면 1300으로 리셋 (누적 없음)
270
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
271
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
272
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
273
}
274
275
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
276
if (remaining < amount) return { success: false, remaining };
277
278
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
279
writeUsers(users);
280
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
281
}
282