Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
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