Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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