This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { dev } from '$app/environment';
|
||||
import updateHistoryMarkdown from '../../../UPDATES.md?raw';
|
||||
import { renderReleaseMarkdown } from '$lib/release-notes/markdown';
|
||||
import { RELEASES_PAGE_URL } from '$lib/release-notes/release-note-service';
|
||||
import { currentAppVersion, releaseNotes } from '$lib/release-notes/release-note-store';
|
||||
import { versionTagIsNewer } from '$lib/release-notes/version';
|
||||
import { parseUpdateHistoryDocument } from '$lib/update-history-document.ts';
|
||||
|
||||
let previewUpdate = false;
|
||||
const updateHistoryDocument = parseUpdateHistoryDocument(updateHistoryMarkdown);
|
||||
|
||||
onMount(() => {
|
||||
previewUpdate = dev && new URLSearchParams(window.location.search).get('previewUpdate') === '1';
|
||||
@@ -16,6 +18,20 @@
|
||||
$: latestRelease = $releaseNotes.releases[0] ?? null;
|
||||
$: updateAvailable = latestRelease !== null && (previewUpdate || versionTagIsNewer(latestRelease.tagName, currentAppVersion));
|
||||
|
||||
function updatedDateTime(value: string | null): string {
|
||||
if (!value) return '시각 미등록';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function publishedDate(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
@@ -27,25 +43,22 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $releaseNotes.status === 'available' && latestRelease}
|
||||
<aside class:available={updateAvailable} class="update-notice" aria-live="polite">
|
||||
<aside class:available={updateAvailable} class="update-notice" aria-live="polite">
|
||||
<div class="notice-header">
|
||||
<div class="version-summary">
|
||||
<span class="label">현재 버전</span>
|
||||
<strong>v{currentAppVersion.replace(/^v/i, '')}</strong>
|
||||
<span class="label">최근 업데이트</span>
|
||||
<strong><time datetime={updateHistoryDocument.updatedAt ?? undefined}>{updatedDateTime(updateHistoryDocument.updatedAt)}</time></strong>
|
||||
{#if previewUpdate}
|
||||
<span class="status"><b>업데이트 알림 미리보기</b></span>
|
||||
{:else if updateAvailable}
|
||||
<span class="status"><b>{latestRelease.tagName}</b> 업데이트를 사용할 수 있습니다.</span>
|
||||
{:else}
|
||||
<span class="status">최신 버전입니다.</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<a class="action" href={RELEASES_PAGE_URL} target="_blank" rel="noreferrer">업데이트 내역 확인 <span aria-hidden="true">→</span></a>
|
||||
<a class="action" href="/updates">업데이트 내역 확인 <span aria-hidden="true">→</span></a>
|
||||
</div>
|
||||
|
||||
{#if updateAvailable}
|
||||
{#if updateAvailable && latestRelease}
|
||||
<section class="latest-note" aria-labelledby="latest-release-title">
|
||||
<header>
|
||||
<div>
|
||||
@@ -62,8 +75,7 @@
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
</aside>
|
||||
{/if}
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.update-notice { margin-bottom: 12px; border: 1px solid #2c3440; border-radius: 12px; background: #151a22; overflow: hidden; }
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseUpdateHistoryDocument } from '../update-history-document.ts';
|
||||
|
||||
test('업데이트 Markdown의 최근 업데이트 시각을 본문과 분리한다', () => {
|
||||
const document = parseUpdateHistoryDocument('---\nupdatedAt: 2026-08-31T14:15:17+09:00\n---\n\n## 업데이트');
|
||||
|
||||
assert.equal(document.updatedAt, '2026-08-31T14:15:17+09:00');
|
||||
assert.equal(document.body, '\n## 업데이트');
|
||||
});
|
||||
|
||||
test('메타데이터가 없는 Markdown도 그대로 표시한다', () => {
|
||||
const markdown = '## 업데이트';
|
||||
assert.deepEqual(parseUpdateHistoryDocument(markdown), { updatedAt: null, body: markdown });
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
export type UpdateHistoryDocument = {
|
||||
updatedAt: string | null;
|
||||
body: string;
|
||||
};
|
||||
|
||||
export function parseUpdateHistoryDocument(markdown: string): UpdateHistoryDocument {
|
||||
const frontmatter = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
||||
if (!frontmatter) return { updatedAt: null, body: markdown };
|
||||
|
||||
const updatedAt = frontmatter[1]
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.match(/^updatedAt:\s*(.+?)\s*$/)?.[1] ?? null)
|
||||
.find((value): value is string => value !== null) ?? null;
|
||||
|
||||
return {
|
||||
updatedAt,
|
||||
body: markdown.slice(frontmatter[0].length)
|
||||
};
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<a href="/combination" aria-current={$page.url.pathname.startsWith('/combination') ? 'page' : undefined}>조합 탐색</a>
|
||||
{#if showEventTab}<a href="/event" aria-current={$page.url.pathname.startsWith('/event') ? 'page' : undefined}>이벤트 Pt</a>{/if}
|
||||
<a href="/data" aria-current={$page.url.pathname.startsWith('/data') ? 'page' : undefined}>사용자 데이터</a>
|
||||
<a href="/updates" aria-current={$page.url.pathname.startsWith('/updates') ? 'page' : undefined}>업데이트 내역</a>
|
||||
<a
|
||||
class="feedback-link"
|
||||
href="https://docs.google.com/forms/d/1z9c0c1crbCO6avXaYwrHZjhw2ib3zgAmW4ijJDTo4DM"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import updateHistoryMarkdown from '../../../UPDATES.md?raw';
|
||||
import { renderReleaseMarkdown } from '$lib/release-notes/markdown.ts';
|
||||
import { parseUpdateHistoryDocument } from '$lib/update-history-document.ts';
|
||||
|
||||
const updateHistoryDocument = parseUpdateHistoryDocument(updateHistoryMarkdown);
|
||||
const renderedUpdateHistory = renderReleaseMarkdown(updateHistoryDocument.body);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>업데이트 내역 · 홀로도리 계산기</title>
|
||||
<meta name="description" content="홀로도리 계산기에 수시로 반영된 기능, 개선, 수정, 데이터 업데이트 내역" />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<header class="page-header">
|
||||
<p class="eyebrow">CHANGELOG</p>
|
||||
<h1>업데이트 내역</h1>
|
||||
<p class="description">큰 버전 배포 단위가 아닌, 실제로 반영된 변경 사항을 순서대로 기록합니다.</p>
|
||||
</header>
|
||||
|
||||
<aside class="delivery-note">
|
||||
<span aria-hidden="true">●</span>
|
||||
<p><strong>온라인판</strong>은 변경 사항이 수시로 반영됩니다. <strong>오프라인판</strong>은 다시 내려받은 파일에 포함된 시점까지의 내역을 보여 줍니다.</p>
|
||||
</aside>
|
||||
|
||||
<article class="markdown-body">{@html renderedUpdateHistory}</article>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main { width: min(980px, calc(100% - var(--page-gutter))); margin: 0 auto; padding: var(--page-content-top) 0 88px; }
|
||||
.page-header { padding: 18px 0 var(--page-header-bottom); }
|
||||
.eyebrow { margin: 0 0 10px; color: #ed8bb5; font-size: 13px; font-weight: 900; letter-spacing: .12em; }
|
||||
h1 { margin: 0; color: #f3f5f8; font-size: var(--page-title-size); letter-spacing: -.045em; }
|
||||
.description { margin: 13px 0 0; color: #9ba5b4; font-size: var(--page-description-size); line-height: 1.65; }
|
||||
.delivery-note { display: flex; align-items: flex-start; gap: 11px; margin-bottom: 36px; padding: 15px 17px; border: 1px solid rgb(237 139 181 / 24%); border-radius: 12px; background: rgb(237 139 181 / 7%); }
|
||||
.delivery-note > span { margin-top: 7px; color: #ed8bb5; font-size: 7px; }
|
||||
.delivery-note p { margin: 0; color: #aeb6c2; font-size: 14px; line-height: 1.65; }
|
||||
.delivery-note strong { color: #e9edf2; }
|
||||
.markdown-body { position: relative; padding-left: 34px; }
|
||||
.markdown-body::before { position: absolute; top: 9px; bottom: 0; left: 6px; width: 1px; background: #303947; content: ''; }
|
||||
:global(.markdown-body h2) { position: relative; margin: 0 0 14px; padding-top: 2px; color: #cad0d9; font-size: 15px; font-weight: 850; }
|
||||
:global(.markdown-body h2:not(:first-child)) { margin-top: 38px; }
|
||||
:global(.markdown-body h2::before) { position: absolute; top: 6px; left: -34px; width: 13px; height: 13px; border: 3px solid #101319; border-radius: 50%; background: #ed8bb5; box-shadow: 0 0 0 1px rgb(237 139 181 / 45%); content: ''; }
|
||||
:global(.markdown-body h3) { margin: 10px 0 0; padding: 19px 20px 0; border: 1px solid #2b3441; border-bottom: 0; border-radius: 12px 12px 0 0; color: #edf0f4; background: #151a22; font-size: 17px; letter-spacing: -.02em; }
|
||||
:global(.markdown-body h3 + p) { margin: 0 0 10px; padding: 8px 20px 19px; border: 1px solid #2b3441; border-top: 0; border-radius: 0 0 12px 12px; color: #9ca6b4; background: #151a22; font-size: 14px; line-height: 1.65; }
|
||||
:global(.markdown-body code) { margin-right: 7px; padding: 3px 7px; border: 1px solid rgb(237 139 181 / 32%); border-radius: 999px; color: #efa0c1; background: rgb(237 139 181 / 9%); font-family: inherit; font-size: 10px; font-weight: 900; }
|
||||
:global(.markdown-body hr) { height: 1px; margin: 38px 0; border: 0; background: #2c3440; }
|
||||
:global(.markdown-body ul), :global(.markdown-body ol) { color: #9ca6b4; line-height: 1.7; }
|
||||
:global(.markdown-body a) { color: #f0a1c3; }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.page-header { padding-top: 12px; }
|
||||
.delivery-note { margin-bottom: 30px; }
|
||||
.markdown-body { padding-left: 27px; }
|
||||
:global(.markdown-body h2::before) { left: -27px; }
|
||||
:global(.markdown-body h3) { padding: 17px 17px 0; }
|
||||
:global(.markdown-body h3 + p) { padding: 8px 17px 17px; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user