Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
src/services/hinana-rail.service.ts
공개 가능
1
import fs from 'fs';
2
import path from 'path';
3
import { createHash, randomUUID } from 'crypto';
4
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
5
6
export const RAIL_COLLECTION_TYPES = ['KTX-I', 'KTX-산천', 'SRT', 'KTX-이음', 'KTX-청룡', 'ITX-청춘'];
7
export const RAIL_TYPES = [...RAIL_COLLECTION_TYPES, 'KTX (미확인)', 'ITX-새마을', 'ITX-마음', '무궁화호', '기타'];
8
export interface RailTrip {
9
id: string; date: string; departure: string; arrival: string; departureTime: string; arrivalTime: string;
10
trainType: string; trainNumber: string; seat: string; seatClass: string; fare: number | null;
11
distance: number | null; line: string; note: string; photos: string[]; createdAt: string;
12
}
13
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
14
export function railUserDir(username: string): string {
15
return path.join(root, createHash('sha256').update(username).digest('hex'));
16
}
17
export function readRailTrips(username: string): RailTrip[] {
18
try {
19
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
20
if (!Array.isArray(rows)) throw new Error('Invalid rail data');
21
return rows;
22
} catch (err: any) { if (err.code === 'ENOENT') return []; throw err; }
23
}
24
function writeTrips(username: string, trips: RailTrip[]): void {
25
const dir = railUserDir(username);
26
fs.mkdirSync(dir, { recursive: true });
27
const temp = path.join(dir, 'trips.tmp');
28
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
29
fs.renameSync(temp, path.join(dir, 'trips.json'));
30
}
31
export function validateRailTrip(input: any): Omit<RailTrip, 'id' | 'photos' | 'createdAt'> {
32
const text = (key: string, max = 80) => typeof input[key] === 'string' ? input[key].trim().slice(0, max) : '';
33
const date = text('date', 10);
34
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(date)) || new Date(date).toISOString().slice(0, 10) !== date) throw new Error('이용 날짜를 확인해 주세요.');
35
const departure = text('departure'), arrival = text('arrival');
36
if (!departure || !arrival || departure === arrival) throw new Error('출발역과 도착역을 확인해 주세요.');
37
const departureTime = text('departureTime'), arrivalTime = text('arrivalTime');
38
for (const time of [departureTime, arrivalTime]) if (time && !/^([01]\d|2[0-3]):[0-5]\d$/.test(time)) throw new Error('시간을 확인해 주세요.');
39
const number = (key: string, max: number) => {
40
if (input[key] === '' || input[key] == null) return null;
41
const value = Number(input[key]);
42
if (!Number.isFinite(value) || value < 0 || value > max) throw new Error('금액 또는 거리를 확인해 주세요.');
43
return value;
44
};
45
const trainType = text('trainType');
46
if (!RAIL_TYPES.includes(trainType)) throw new Error('차종을 선택해 주세요.');
47
return { date, departure, arrival, departureTime, arrivalTime, trainType, trainNumber: text('trainNumber', 20), seat: text('seat', 40), seatClass: text('seatClass', 30), fare: number('fare', 10000000), distance: number('distance', 10000), line: text('line'), note: text('note', 2000) };
48
}
49
export function saveRailTrip(username: string, input: any, photos: string[], id?: string): RailTrip {
50
const fields = validateRailTrip(input);
51
const trips = readRailTrips(username);
52
const index = id ? trips.findIndex(t => t.id === id) : -1;
53
if (id && index < 0) throw new Error('기록을 찾을 수 없습니다.');
54
const previous = index >= 0 ? trips[index] : null;
55
const trip = { ...fields, id: previous?.id || randomUUID(), createdAt: previous?.createdAt || new Date().toISOString(), photos: [...(previous?.photos || []), ...photos] };
56
if (trip.photos.length > 6) throw new Error('사진은 기록당 6장까지 가능합니다.');
57
if (index >= 0) trips[index] = trip; else trips.unshift(trip);
58
writeTrips(username, trips);
59
return trip;
60
}
61
export function deleteRailTrip(username: string, id: string): boolean {
62
const trips = readRailTrips(username), trip = trips.find(t => t.id === id);
63
if (!trip) return false;
64
writeTrips(username, trips.filter(t => t.id !== id));
65
for (const photo of trip.photos) fs.rmSync(path.join(railUserDir(username), photo), { force: true });
66
return true;
67
}
68