Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
src/services/archive-search.service.ts
공개 가능
1
import fs from 'fs';
2
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
3
import { loadPosts } from './blog.service';
4
import { listJapaneseLoungeEntries } from './japanese-lounge.service';
5
import { getUserAiNotes } from './ai-note.service';
6
import { getUserPersonalNotes } from './personal-note.service';
7
import { listVoicepeakJobs } from './voicepeak.service';
8
9
export type ArchiveSearchItem = { id: string; type: string; typeLabel: string; title: string; excerpt: string; date: string; url: string; restricted: boolean };
10
11
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
12
function plain(value: unknown): string { return String(value || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); }
13
function excerpt(value: unknown): string { const text = plain(value); return text.length > 220 ? `${text.slice(0, 220)}...` : text; }
14
function matches(item: ArchiveSearchItem, query: string): boolean { const haystack = `${item.title}\n${item.excerpt}`.toLocaleLowerCase(); return !query || haystack.includes(query); }
15
16
export function searchArchive(username: string | null, rawQuery: string, rawType: string): ArchiveSearchItem[] {
17
const query = rawQuery.trim().toLocaleLowerCase().slice(0, 120);
18
const type = rawType.trim();
19
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
20
const items: ArchiveSearchItem[] = [];
21
22
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
23
[...(Array.isArray(mainPosts) ? mainPosts : []), ...loadPosts()].forEach((post: any) => {
24
const owner = post.username || post.author;
25
if (post.isPrivate && owner !== username && !isAdmin) return;
26
const id = String(post.id);
27
const isLibraryPost = Boolean(post.createdAt && post.author);
28
items.push({ id: `blog-${id}`, type: 'blog', typeLabel: '블로그', title: plain(post.title) || '제목 없는 기록', excerpt: excerpt(post.content), date: post.createdAt || post.timestamp || '', url: isLibraryPost ? `/hinana/post/${encodeURIComponent(id)}` : `/hinana/index?selectedId=${encodeURIComponent(id)}`, restricted: Boolean(post.isPrivate) });
29
});
30
31
listJapaneseLoungeEntries().forEach((entry) => items.push({ id: `jp-${entry.id}`, type: 'japanese', typeLabel: '일본어 라운지', title: entry.title, excerpt: excerpt(`${entry.japaneseText}\n${entry.koreanText}`), date: entry.updatedAt || entry.createdAt, url: `/hinana/japanese-lounge?entry=${encodeURIComponent(entry.id)}`, restricted: false }));
32
33
if (username) getUserAiNotes(username).forEach((note) => items.push({ id: `ai-${note.id}`, type: 'ai-note', typeLabel: 'AI 노트', title: note.title || note.question, excerpt: excerpt(`${note.question}\n${note.excerpt}`), date: note.updatedAt || note.createdAt, url: '/hinana/ai/notes', restricted: true }));
34
if (isAdmin) getUserPersonalNotes(username!).forEach((note) => items.push({ id: `personal-${note.id}`, type: 'personal-note', typeLabel: '개인 노트', title: note.title, excerpt: excerpt(note.content), date: note.updatedAt || note.createdAt, url: '/hinana/personal-notes', restricted: true }));
35
if (isAdmin) listVoicepeakJobs().forEach((job: any) => items.push({ id: `voice-${job.id}`, type: 'voicepeak', typeLabel: 'Voicepeak', title: job.title, excerpt: excerpt((job.sentences || []).map((sentence: any) => sentence.text).join(' ')), date: new Date(job.updatedAt || job.createdAt).toISOString(), url: '/hinana/voicepeak', restricted: true }));
36
37
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
38
Object.entries(views || {}).forEach(([id, view]: [string, any]) => items.push({ id: `view-${id}`, type: 'ai-view', typeLabel: '공개 AI 원문', title: plain(view.question) || 'AI 대화', excerpt: excerpt(view.answer), date: view.createdAt || '', url: `/hinana/ai/view/${encodeURIComponent(id)}`, restricted: false }));
39
40
return items.filter((item) => (!type || item.type === type) && matches(item, query)).sort((a, b) => Date.parse(b.date || '1970-01-01') - Date.parse(a.date || '1970-01-01')).slice(0, query ? 200 : 30);
41
}
42