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 { getUserPersonalNotes } from './personal-note.service';
6
import { listVoicepeakJobs } from './voicepeak.service';
7
8
export type ArchiveSearchItem = { id: string; type: string; typeLabel: string; title: string; excerpt: string; date: string; url: string; restricted: boolean };
9
10
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
11
function plain(value: unknown): string { return String(value || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); }
12
function excerpt(value: unknown): string { const text = plain(value); return text.length > 220 ? `${text.slice(0, 220)}...` : text; }
13
function matches(item: ArchiveSearchItem, query: string): boolean { const haystack = `${item.title}\n${item.excerpt}`.toLocaleLowerCase(); return !query || haystack.includes(query); }
14
15
export function searchArchive(username: string | null, rawQuery: string, rawType: string): ArchiveSearchItem[] {
16
const query = rawQuery.trim().toLocaleLowerCase().slice(0, 120);
17
const type = rawType.trim();
18
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
19
const items: ArchiveSearchItem[] = [];
20
21
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
22
[...(Array.isArray(mainPosts) ? mainPosts : []), ...loadPosts()].forEach((post: any) => {
23
const owner = post.username || post.author;
24
if (post.isPrivate && owner !== username && !isAdmin) return;
25
const id = String(post.id);
26
const isLibraryPost = Boolean(post.createdAt && post.author);
27
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) });
28
});
29
30
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 }));
31
32
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 }));
33
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 }));
34
35
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);
36
}
37