본문으로 건너뛰기
Public Source Viewer

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

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

Redacted View
src/services/novel.service.ts
공개 가능
1 import fs from 'fs';
2 import path from 'path';
3 import { createHash, randomUUID } from 'crypto';
4 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
5 import { parseProject, readerHTML } from '../shared/novel-reader';
6 export type NovelWork = { id: string; sourceId: string; title: string; author: string; description: string; excerpt: string; chapterCount: number; characterCount: number; fontSize: number; writing: string; theme: string; createdAt: string; updatedAt: string; revision: string; url: string };
7 type Record = { work: NovelWork; project: ReturnType<typeof parseProject> };
8 const validId = (id: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id);
9 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
10 function file(id: string) { if (!validId(id)) throw new Error('작품 주소가 올바르지 않습니다.'); return path.join(root(), `${id}.json`); }
11 function record(id: string): Record | null {
12 if (!validId(id)) return null;
13 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
14 }
15 export function listNovelWorks(): NovelWork[] {
16 let entries: string[];
17 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
18 return entries.filter(name => validId(name.replace(/\.json$/, '')) && name.endsWith('.json')).map(name => record(name.slice(0, -5))!.work).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id));
19 }
20 export function getNovelWork(id: string) { return record(id)?.work || null; }
21 export function novelReader(id: string) { const entry = record(id); return entry ? readerHTML(entry.project) : null; }
22 export function publishNovel(raw: string, description: unknown = undefined): NovelWork {
23 const project = parseProject(raw);
24 if (description === undefined) description = project.description;
25 if (!project.title.trim()) throw new Error('작품명을 입력해 주세요.');
26 if (!project.chapters.some(c => c.text.trim())) throw new Error('내용이 있는 글을 하나 이상 작성해 주세요.');
27 if (typeof description !== 'string' || description.length > 500) throw new Error('작품 소개는 500자 이하여야 합니다.');
28 const existing = listNovelWorks().find(work => work.sourceId === project.id);
29 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
30 if (existing?.revision === revision) return existing;
31 const now = new Date().toISOString(), id = existing?.id || randomUUID();
32 const work: NovelWork = { id, sourceId: project.id, title: project.title, author: project.author, description: description.trim(), excerpt: project.chapters.map(c => c.text).join(' ').replace(/\s+/g, ' ').slice(0, 180), chapterCount: project.chapters.length, characterCount: project.chapters.reduce((sum, c) => sum + Array.from(c.text).length, 0), fontSize: project.settings.fontSize, writing: project.settings.writing, theme: project.settings.theme, createdAt: existing?.createdAt || now, updatedAt: now, revision, url: `/hinana/works/${id}` };
33 fs.mkdirSync(root(), { recursive: true });
34 const temporary = path.join(root(), `.${id}.${randomUUID()}.tmp`);
35 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
36 finally { fs.rmSync(temporary, { force: true }); }
37 return work;
38 }
39 export function deleteNovelWork(id: string): boolean { if (!record(id)) return false; fs.unlinkSync(file(id)); return true; }
40