' + escapeHtml(title) + '
' + (summary ? '' + escapeHtml(summary) + '
' : '') + '' + '본문이 아직 없습니다.
') + '// views/post.js — 게시물 상세 화면 // 라우트: #/post/:postNo // 구성: 카테고리 → 제목 → 요약 → 날짜 → 마크다운 본문 // // 표지 이미지와 하단 갤러리는 두지 않는다. // 이미지는 본문 안에서 마크다운으로 직접 배치한다. (  ) import { api } from '../api.js'; import { escapeHtml, formatDate, setBusy, toast } from '../ui.js'; import { renderMarkdown } from '../markdown.js'; /** accent 는 CSS 값으로 들어가므로 헥사 색상만 통과시킨다. */ function safeAccent(value) { const text = typeof value === 'string' ? value.trim() : ''; return /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text) ? text : ''; } /** 정수 안전 변환 */ function toInt(value) { const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : 0; } /** '12' 처럼 양의 정수인지 확인한다. */ function parsePostNo(value) { const text = String(value === null || value === undefined ? '' : value).trim(); if (!/^\d{1,9}$/.test(text)) return 0; const n = Number(text); return n > 0 ? n : 0; } /** 찾을 수 없음 / 오류 화면 */ function stateHtml(icon, title, detail) { return ( '
' ); } /** * 게시물 본문 마크업. * 사용자 데이터는 모두 escapeHtml 을 거치고, 본문만 renderMarkdown 결과를 넣는다. */ function postHtml(post) { const title = String((post && post.title) || '(제목 없음)'); const catName = String((post && post.categoryName) || '미분류'); const catSlug = String((post && post.categorySlug) || ''); const accent = safeAccent(post && post.categoryAccent); const summary = post && post.summary ? String(post.summary) : ''; const created = formatDate(post && post.createdAt); const updated = formatDate(post && post.updatedAt); const catHref = catSlug ? '#/?cat=' + encodeURIComponent(catSlug) : '#/'; let html = '' + escapeHtml(summary) + '
' : '') + '' + '본문이 아직 없습니다.
') + '불러오는 중…