From e87dd3b1f3f33f799ead1ffe8c6c2930ffae97eb Mon Sep 17 00:00:00 2001 From: dal4segno Date: Thu, 8 Oct 2026 19:07:35 +0900 Subject: [PATCH] feat: align random song output with jacket song lists --- UPDATES.md | 6 ++- src/routes/tools/+page.svelte | 89 ++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/UPDATES.md b/UPDATES.md index 88d885b..5785d6d 100644 --- a/UPDATES.md +++ b/UPDATES.md @@ -1,9 +1,13 @@ --- -updatedAt: 2026-10-08T09:15:32.000Z +updatedAt: 2026-10-08T10:07:05.000Z --- ## 2026년 10월 8일 +### 악곡 랜덤 결과 표시·복사 통일 + +`IMPROVED` 악곡 랜덤의 추첨 결과를 악곡 목록과 같은 간결한 번호·곡명 행으로 표시하고, 분홍색 배경과 난이도·레벨 표시를 제거했습니다. ‘목록 복사’는 번호·재킷·제목 순서이며, 재킷은 이미지로, 번호와 제목은 편집 가능한 텍스트로 제공합니다. 자동 복사를 사용할 수 없으면 복사용 목록을 직접 선택해 복사할 수 있습니다. 레벨 범위 조건과 악곡·난이도 조합별 추첨 확률은 유지합니다. + ### 카드·의상 목록 정렬 해제 `IMPROVED` 카드와 의상 효과 목록에 ‘기본 정렬’ 버튼을 추가했습니다. 정렬 가능한 열 제목을 반복해서 누르면 오름차순·내림차순·기본 순서로 전환합니다. 정렬을 해제해도 필터·개화 선택과 펼친 상세는 유지하며, 기본 정렬 상태는 다음 방문에도 유지합니다. diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte index 9ce007b..4bea04d 100644 --- a/src/routes/tools/+page.svelte +++ b/src/routes/tools/+page.svelte @@ -3,10 +3,11 @@ import ToolsNav from '$lib/components/ToolsNav.svelte'; import { page } from '$app/stores'; import exactChartCatalog from '$lib/data/exact-chart-catalog.json'; - import { difficultyLabel } from '$lib/data/difficulty.ts'; + import songJackets from '$lib/data/song-jackets.json'; import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts'; import { releasedSongCharts } from '$lib/data/song-release.ts'; - import { formatRandomSongResults, pickRandomSong, randomSongCandidates } from '$lib/engine/random-song.ts'; + import { pickRandomSong, randomSongCandidates } from '$lib/engine/random-song.ts'; + import { formatSongListClipboard } from '$lib/engine/song-list-copy.ts'; type Chart = (typeof exactChartCatalog.charts)[number]; let releaseClock = createInitialReleaseClock($page.data?.serverNow); @@ -18,10 +19,11 @@ let announcement = ''; let copying = false; let copyStatus = ''; - let showCopyText = false; - let copyTextArea: HTMLTextAreaElement | undefined; + let showCopyPreview = false; + let copyPreview: HTMLDivElement | undefined; + let copyOrigin = 'https://doricalc.cc'; - $: resultsText = formatRandomSongResults(results); + $: resultsClipboard = formatSongListClipboard(results, songJackets.jackets, copyOrigin); $: charts = releasedSongCharts(exactChartCatalog.charts, releaseNow); $: levels = [...new Set(charts.map((chart) => chart.difficultyLevel) @@ -37,41 +39,54 @@ if (!selected) return; results = [...results, selected]; copyStatus = ''; - showCopyText = false; - announcement = `${results.length}번째 추첨: ${selected.title}, ${difficultyLabel(selected.difficulty)}, ${selected.difficultyLevel == null ? '레벨 정보 없음' : `Lv. ${selected.difficultyLevel}`}`; + showCopyPreview = false; + announcement = `${results.length}번째 추첨: ${selected.title}`; } function clearResults() { results = []; copyStatus = ''; - showCopyText = false; + showCopyPreview = false; announcement = '추첨 목록을 초기화했습니다.'; } async function copyResults() { if (!results.length || copying) return; const snapshot = results; - const text = resultsText; + const content = resultsClipboard; copying = true; copyStatus = ''; - showCopyText = false; + showCopyPreview = false; try { - if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable'); - await navigator.clipboard.writeText(text); - if (results === snapshot) copyStatus = `${snapshot.length}개 결과를 복사했습니다.`; + if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') throw new Error('Rich clipboard unavailable'); + await navigator.clipboard.write([new ClipboardItem({ + 'text/html': new Blob([content.html], { type: 'text/html' }), + 'text/plain': new Blob([content.text], { type: 'text/plain' }) + })]); + if (results === snapshot) { + copyStatus = `${snapshot.length}개 악곡의 번호·재킷·제목을 추첨 순서대로 복사했습니다.`; + if (content.missingJackets) copyStatus += ` 재킷 정보가 없는 ${content.missingJackets}개는 번호·곡명만 포함했습니다.`; + } } catch { if (results !== snapshot) return; - copyStatus = '자동 복사를 사용할 수 없습니다. 아래 텍스트를 선택해 복사해 주세요.'; - showCopyText = true; + copyStatus = '자동 복사를 사용할 수 없습니다. 아래 선택된 목록을 직접 복사해 주세요.'; + showCopyPreview = true; await tick(); - copyTextArea?.focus(); - copyTextArea?.select(); + if (copyPreview) { + copyPreview.focus(); + const range = document.createRange(); + range.selectNodeContents(copyPreview); + const selection = window.getSelection(); + selection?.removeAllRanges(); + selection?.addRange(range); + } } finally { copying = false; } } onMount(() => { + copyOrigin = window.location.origin; let mounted = true; void synchronizeReleaseClock().then((clock) => { if (!mounted) return; @@ -137,25 +152,23 @@ -

먼저 뽑은 결과부터 표시합니다. 레벨 범위를 바꿔도 이전 결과는 유지됩니다.

{copyStatus}

- {#if showCopyText} - + {#if showCopyPreview} +
+ {@html resultsClipboard.html} +
{/if} {#if results.length}
    {#each results as result, index} -
  1. +
  2. {result.title} -
    {difficultyLabel(result.difficulty)}{result.difficultyLevel == null ? '레벨 정보 없음' : `Lv. ${result.difficultyLevel}`}
  3. {/each}
{:else} -

랜덤 선택을 누르면 악곡과 난이도를 함께 뽑습니다.

+

랜덤 선택을 누르면 추첨한 악곡이 차례대로 쌓입니다.

{/if} @@ -178,31 +191,29 @@ button { width: 100%; min-height: 48px; border: 0; border-radius: 10px; background: #ed8bb5; color: #21121a; font: inherit; font-weight: 850; cursor: pointer; } button:hover:not(:disabled) { background: #f4a8c9; } button:disabled { background: #343c49; color: #a4adba; cursor: not-allowed; } - button:focus-visible, select:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 3px; } - .result { border-color: #694254; background: rgb(237 139 181 / 6%); } + button:focus-visible, select:focus-visible, input:focus-visible, .copy-preview:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 3px; } + .result { padding: 20px; } .results-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .result-actions { display: flex; gap: 8px; flex-wrap: wrap; } .copy-status { margin: 12px 0 0; color: #f3b4d0; font-size: 13px; line-height: 1.6; } .copy-status:empty { margin: 0; } - .copy-text { display: grid; gap: 8px; margin-top: 12px; color: #a4adba; font-size: 13px; } - textarea { width: 100%; min-height: 88px; padding: 12px; resize: vertical; border: 1px solid #694254; border-radius: 8px; background: #101319; color: #e8ebf1; font: inherit; line-height: 1.7; } + .copy-preview { margin-top: 12px; max-height: 320px; overflow: auto; padding: 12px; border-radius: 9px; background: #fff; color: #222; font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; } h2 { margin: 0; font-size: 22px; } h2 span { margin-left: 6px; color: #a4adba; font-size: 14px; font-weight: 500; } - .clear-button { width: auto; min-height: 38px; padding: 8px 12px; border: 1px solid #694254; background: transparent; color: #f3b4d0; font-size: 13px; } - .clear-button:hover:not(:disabled) { background: #302331; } + .clear-button { width: auto; min-height: 36px; padding: 6px 10px; border: 1px solid #414b5b; background: transparent; color: #bec8d6; font-size: 13px; font-weight: 400; } + .clear-button:hover:not(:disabled) { border-color: #637083; background: #202733; } .clear-button:disabled { background: transparent; color: #87909e; border-color: #343c49; } - .results-list { margin: 20px 0 0; padding: 0; list-style: none; } - .results-list li { display: grid; grid-template-columns: minmax(28px, auto) minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 16px 12px; border-bottom: 1px solid #343c49; } - .results-list li.latest { border-radius: 9px; border-bottom-color: transparent; background: rgb(237 139 181 / 9%); } + .results-list { margin: 12px 0 0; padding: 0; list-style: none; } + .results-list li { display: grid; grid-template-columns: 24px minmax(0, 1fr); align-items: center; gap: 10px; min-height: 44px; padding: 6px 0; border-bottom: 1px solid #2c3441; } + .results-list li:last-child { border-bottom: 0; } .draw-number { color: #a4adba; font-size: 13px; font-variant-numeric: tabular-nums; } - .song-title { min-width: 0; overflow-wrap: anywhere; line-height: 1.6; } - .badges { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; font-size: 13px; } - .badges span, .badges strong { padding: 6px 10px; border-radius: 8px; background: #302331; color: #f3b4d0; } + .song-title { min-width: 0; overflow-wrap: anywhere; font-size: 14px; line-height: 1.4; } .placeholder { color: #a4adba; line-height: 1.7; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (max-width: 520px) { .panel, .result { padding: 20px; } - .results-list li { grid-template-columns: minmax(24px, auto) minmax(0, 1fr); gap: 8px; } - .badges { grid-column: 2; } + .result { padding: 16px; } + .results-list li { grid-template-columns: 18px minmax(0, 1fr); gap: 6px; } + .song-title { font-size: 13px; } }