Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
view/hinana/rail.ejs
공개 가능
1
<!doctype html>
2
<html lang="ko">
3
<head>
4
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
5
<title>히나나레일 · 비나래 라운지</title>
6
<meta name="color-scheme" content="light dark">
7
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet">
8
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
9
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
10
<style>
11
:root{color-scheme:light;--bg:#fff;--panel:#f5f7f8;--text:#152327;--muted:#607078;--line:#dde5e6;--accent:#087c79;--blue:#246ac0}body.dark-mode{color-scheme:dark;--bg:#101414;--panel:#1c2324;--text:#edf3f2;--muted:#a5b5b7;--line:#354143;--accent:#63d4bb;--blue:#88b5fa}*{box-sizing:border-box;letter-spacing:0}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.6 system-ui,sans-serif}a{color:var(--accent)}button,input,select,textarea{font:inherit}button{cursor:pointer}button:disabled{opacity:.5;cursor:wait}header{border-bottom:1px solid var(--line);padding:14px 24px;display:flex;align-items:center;justify-content:space-between;gap:20px}header img{width:120px;height:30px;object-fit:contain}main{max-width:1120px;margin:auto;padding:28px 20px}h1{font-size:28px;margin:0}h2{font-size:18px;margin:24px 0 12px}h3{font-size:15px;margin:0}p{margin:6px 0}.muted,small{color:var(--muted)}.head,.toolbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}.btn{border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--text);padding:9px 14px;display:inline-flex;gap:8px;align-items:center}.primary{background:var(--accent);color:var(--bg);border-color:var(--accent)}.icon{width:38px;height:38px;display:inline-grid;place-items:center;padding:0}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border-block:1px solid var(--line);margin:24px 0}.stats>div{padding:18px 12px}.stats strong{display:block;font-size:24px;color:var(--accent)}nav.tabs{display:flex;border-bottom:1px solid var(--line);gap:8px;overflow:auto}nav.tabs button{border:0;border-bottom:3px solid transparent;padding:12px;background:none;color:var(--muted);white-space:nowrap}nav.tabs button.active{color:var(--accent);border-color:var(--accent)}[hidden]{display:none!important}.trip{border-bottom:1px solid var(--line);padding:20px 0}.trip-title{font-size:18px;font-weight:700}.trip-meta{display:flex;flex-wrap:wrap;gap:12px;color:var(--muted)}.photos{display:flex;gap:8px;overflow:auto;margin-top:10px}.photos img{width:110px;height:80px;object-fit:cover;border-radius:4px}.note{white-space:pre-wrap;overflow-wrap:anywhere}.fleet{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-top:20px}.vehicle{padding:20px;border:1px solid var(--line);border-radius:6px;background:var(--panel)}.vehicle i{font-size:30px;color:var(--blue)}.vehicle strong{display:block;margin-top:10px}.vehicle.done{border-color:var(--accent)}.columns{display:grid;grid-template-columns:1fr 1fr;gap:30px}.count-row{display:flex;justify-content:space-between;gap:15px;border-bottom:1px solid var(--line);padding:10px 0;overflow-wrap:anywhere}#map{height:380px;margin-top:20px;z-index:0}.empty{padding:45px 0;color:var(--muted)}dialog{width:min(740px,calc(100% - 24px));max-height:90vh;overflow:auto;border:1px solid var(--line);border-radius:8px;padding:24px;background:var(--bg);color:var(--text)}dialog::backdrop{background:#0009}.fields{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:20px 0}label{display:flex;flex-direction:column;gap:5px;font-size:13px;color:var(--muted)}input,select,textarea{width:100%;min-width:0;padding:10px;border:1px solid var(--line);border-radius:5px;background:var(--panel);color:var(--text)}textarea{min-height:80px;resize:vertical}.wide{grid-column:1/-1}.upload{border-block:1px solid var(--line);padding:14px 0;margin-top:16px}#message,#draft-message{white-space:pre-wrap;color:var(--blue)}#source-preview{max-width:100%;max-height:180px;margin-top:10px}.filter{max-width:300px}.footer-actions{display:flex;gap:10px;justify-content:flex-end}.warn{color:#bd5818}@media(max-width:650px){main{padding:20px 14px}header{padding:12px 14px}.stats{grid-template-columns:1fr 1fr}.columns{grid-template-columns:1fr;gap:0}.fields{grid-template-columns:1fr}.wide{grid-column:auto}h1{font-size:24px}.trip-title{font-size:16px}dialog{padding:16px}}
12
</style>
13
<link rel="stylesheet" href="/css/lounge-modern.css">
14
<link rel="stylesheet" href="/css/hinana-rail.css">
15
<link rel="stylesheet" href="/css/site-ui.css?v=3">
16
<link rel="stylesheet" href="/css/site-app.css?v=1">
17
</head>
18
<body data-ui-page="rail" class="ui-shell <%= theme === 'dark' ? 'dark-mode' : '' %>">
19
<%- include('partials/site-header', { uiPage: 'rail' }) %>
20
<main id="ui-content" tabindex="-1" class="page-body">
21
<div class="head page-title"><div><h1><i class="bi bi-train-front"></i> 히나나레일</h1><p class="muted">나의 철도 여행 기록</p></div><button class="btn primary" id="new-trip"><i class="bi bi-plus-lg"></i> 승차 기록</button></div>
22
<div class="stats" id="stats"></div>
23
<nav class="tabs" aria-label="승차 기록 보기"><button class="active" data-tab="history">승차 기록</button><button data-tab="collection">차량 도감</button><button data-tab="routes">지도 · 통계</button></nav>
24
<p id="message" role="status"></p>
25
<section id="history"><div class="toolbar" style="margin-top:20px"><h2 style="margin:0">여행 목록</h2><input class="filter" id="search" type="search" placeholder="역 · 차종 · 노선 검색" aria-label="승차 기록 검색"></div><div id="trips"></div></section>
26
[SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
27
<section id="routes" hidden><div id="map"></div><p class="muted">주요 역 사이의 직선 연결도 · 실제 철도 선형과 운행 거리가 아닙니다.</p><p id="unmapped" class="muted"></p><div class="columns"><div><h2>노선별 탑승</h2><div id="lines"></div><h2>승차 구간</h2><div id="segments"></div></div><div><h2>역 방문 기록</h2><div id="stations"></div></div></div></section>
28
</main>
29
<footer class="site-footer"><div class="footer-brand"><img src="/image/sign.png" alt="비나래" class="footer-logo"><div><div class="footer-title">비나래 라운지</div><div>Archive My Lounge</div></div></div><div><div class="footer-links"><a href="/hinana/index">Archive</a><a href="/hinana/lounge">Lounge</a><a href="/hinana/gallery">Gallery</a><a href="/hinana/plaza">Plaza</a></div><div class="footer-copy">2024~2026. 비나래 | hinana.moe</div></div></footer>
30
<dialog id="editor"><form id="trip-form"><div class="head"><h2 style="margin:0" id="editor-title">승차 기록</h2><button type="button" class="btn icon" id="close-editor" title="닫기" aria-label="닫기"><i class="bi bi-x-lg"></i></button></div>
31
<div class="upload"><label>영수증 / 승차권 이미지<input id="receipt" type="file" accept="image/png,image/jpeg,image/webp"></label><p class="muted">인식 시 이미지가 OpenAI로 전송됩니다. 원본은 기록에 자동 첨부되지 않습니다.</p><button type="button" class="btn" id="recognize"><i class="bi bi-scan"></i> 이미지 인식</button><img id="source-preview" alt="선택한 영수증" hidden><p id="draft-message" role="status"></p></div>
32
<input name="id" type="hidden"><div class="fields">
33
<label>이용 날짜<input name="date" type="date" required></label><label>차종<select name="trainType"><% trainTypes.forEach(type => { %><option><%= type %></option><% }) %></select></label>
34
<label>출발역<input name="departure" maxlength="80" required></label><label>도착역<input name="arrival" maxlength="80" required></label>
35
<label>출발 시간<input name="departureTime" type="time"></label><label>도착 시간<input name="arrivalTime" type="time"></label>
36
<label>열차 번호<input name="trainNumber" maxlength="20"></label><label>좌석 등급<input name="seatClass" placeholder="특실 / 일반실" maxlength="30"></label>
37
<label>호차 · 좌석<input name="seat" maxlength="40"></label><label>결제 금액 (원)<input name="fare" type="number" min="0" max="10000000" step="1"></label>
38
<div class="wide route-lookup"><button type="button" class="btn" id="lookup-route"><i class="bi bi-search"></i> 노선 · 거리 조회</button><span id="route-message" role="status"></span><a id="route-source" target="_blank" rel="noopener" hidden>조회 자료</a></div>
39
<label>노선<input name="line" maxlength="80"></label><label>승차 거리 (km)<input name="distance" type="number" min="0" max="10000" step="0.1"></label>
40
<label class="wide">메모<textarea name="note" maxlength="2000"></textarea></label><label class="wide">여행 사진 (최대 6장, 장당 8MB)<input name="photos" type="file" multiple accept="image/png,image/jpeg,image/webp"></label></div>
41
<p id="save-message" role="alert"></p><div class="footer-actions"><button type="button" class="btn" id="cancel">취소</button><button class="btn primary" id="save"><i class="bi bi-check-lg"></i> 확인 후 저장</button></div></form></dialog>
42
<dialog id="trip-detail" aria-labelledby="detail-title"><div class="head"><h2 id="detail-title" style="margin:0">승차 기록</h2><button type="button" class="btn icon" id="close-detail" title="닫기" aria-label="닫기"><i class="bi bi-x-lg"></i></button></div><div id="detail-content"></div><div class="footer-actions"><button type="button" class="btn" id="edit-detail"><i class="bi bi-pencil"></i> 수정</button><button type="button" class="btn primary" id="create-ticket"><i class="bi bi-ticket-perforated"></i> 기념 승차권 생성</button></div><section id="souvenir" hidden aria-label="기념 승차권"><figure class="souvenir-preview"><canvas id="ticket-canvas" width="1600" height="800" role="img" aria-label="기념 승차권"></canvas><figcaption id="ticket-caption"></figcaption></figure><div class="head"><p class="muted souvenir-notice">기념용 이미지 · 실제 승차권이 아니며 탑승에 사용할 수 없습니다.</p><button type="button" class="btn" id="download-ticket"><i class="bi bi-download"></i> PNG 저장</button></div></section><p id="ticket-message" role="status"></p></dialog>
43
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
44
<script src="/js/hinana-rail-ticket.js?v=archive-2"></script>
45
<script src="/js/hinana-rail.js?v=souvenir-archive-2"></script>
46
</body></html>
47