feat: streamline search results and open captured analyses
Publish GitHub Pages output / publish (push) Successful in 54s

This commit is contained in:
2026-09-09 19:15:25 +09:00
parent 28035110f9
commit d26b9ca884
8 changed files with 365 additions and 53 deletions
+27 -8
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { difficultyLabel } from '$lib/data/difficulty.ts';
import { ANALYSIS_TRANSFER_KEY, type AnalysisTransfer } from '$lib/models/analysis-transfer.ts';
import type { AnalysisCapture } from '$lib/models/analysis-history.ts';
import { onDestroy, onMount } from 'svelte';
import type catalog from '$lib/data/exact-chart-catalog.json';
import { loadExactSongChart } from '$lib/data/chart-loader.ts';
@@ -9,9 +11,9 @@
import { createScoreOptimizerWorker } from '$lib/workers/create-score-optimizer-worker.ts';
type Chart = (typeof catalog.charts)[number];
type Context = { cards: OptimizerCard[]; overallPower: number; directSongScoreBonusPercent: number };
type Context = { cards: OptimizerCard[]; overallPower: number; directSongScoreBonusPercent: number; capture: AnalysisCapture };
type Evaluation = { result: ExactOptimizationResult; achievementProbabilities: ScoreAchievementProbabilities };
type Row = Chart & Evaluation & { overallPower: number; theoreticalMaxScore: number; expectedScore: number };
type Row = Chart & Evaluation & { transfer: AnalysisTransfer; overallPower: number; theoreticalMaxScore: number; expectedScore: number };
export let charts: Chart[];
export let leaderId: string;
export let leaderName: string;
@@ -19,7 +21,6 @@
export let inputKey: string;
export let disabled = false;
export let getContext: (chart: Chart) => Context;
export let onSelect: (chart: Chart, result: ExactOptimizationResult, overallPower: number, probabilities: ScoreAchievementProbabilities) => void;
const difficulty = 'all';
let running = false;
@@ -30,6 +31,7 @@
let reportedCores = 1;
let activeJobs: Record<number, { title: string; fraction: number }> = {};
let message = '';
let transferError = '';
let failures: string[] = [];
let snapshotKey = '';
let snapshotLeader = '';
@@ -128,6 +130,8 @@
});
if (id !== runId) return;
rows = [...rows, { ...summary, result, achievementProbabilities, overallPower: context.overallPower,
transfer: { schemaVersion: 1, source: 'rating', capture: context.capture, cards: context.cards, chart,
result, achievementProbabilities, directSongScoreBonusPercent: context.directSongScoreBonusPercent },
theoreticalMaxScore: result.theoreticalMaxScore, expectedScore: result.expectedScore }];
} catch (error) {
if (id !== runId) return;
@@ -159,6 +163,20 @@
}
const format = (value: number) => Math.round(value).toLocaleString('ko-KR');
function openInAnalysis(event: MouseEvent, transfer: AnalysisTransfer): void {
event.preventDefault();
transferError = '';
try {
sessionStorage.setItem(ANALYSIS_TRANSFER_KEY, JSON.stringify(transfer));
const opened = window.open((event.currentTarget as HTMLAnchorElement).href, '_blank');
if (!opened) throw new Error('새 탭이 차단되었습니다. 팝업을 허용한 뒤 다시 열어 주세요.');
opened.opener = null;
} catch (error) {
transferError = error instanceof Error ? error.message : '배치 분석을 열지 못했습니다.';
} finally {
try { sessionStorage.removeItem(ANALYSIS_TRANSFER_KEY); } catch { /* Storage may be blocked. */ }
}
}
function probabilityLabel(value: number): string {
if (value <= 0) return '0%';
if (value < 0.000001) return '<0.0001%';
@@ -173,6 +191,7 @@
}
</script>
<section class="ranking" aria-labelledby="leader-ranking-title">
{#if transferError}<p role="alert">{transferError}</p>{/if}
<div class="heading">
<div class="section-heading"><span class="section-number">02</span><div><h2 id="leader-ranking-title">리더 레이팅 곡 추천</h2><p>지금 편성으로 도전할 곡을 찾아보세요.</p></div></div>
</div>
@@ -242,7 +261,7 @@
<div title={`${format(Math.ceil(row.theoreticalMaxScore * .99))}점 이상 달성 확률`}><span>99% 이상</span><strong>{probabilityLabel(row.achievementProbabilities.atLeast99Percent)}</strong></div>
<div title={`${format(Math.ceil(row.theoreticalMaxScore * .95))}점 이상 달성 확률`}><span>95% 이상</span><strong>{probabilityLabel(row.achievementProbabilities.atLeast95Percent)}</strong></div>
</div></td>
<td class="row-action"><button type="button" disabled={stale || disabled || running} on:click={() => onSelect(row, row.result, row.overallPower, row.achievementProbabilities)}>배치 보기 <span aria-hidden="true">→</span></button></td>
<td class="row-action"><a href="/analysis" on:click={(event) => openInAnalysis(event, row.transfer)}>배치 분석에서 보기 <span aria-hidden="true">↗</span></a></td>
</tr>{/each}</tbody>
</table>
</div>
@@ -376,8 +395,8 @@
.achievement-grid span { color: #9690a6; font-size: 10px; }
.achievement-grid strong { color: #cbb4db; font-size: 12px; font-weight: 650; }
.achievement-grid > div:first-child strong { color: #f1b0ce; }
.row-action button { padding: 9px 11px; background: transparent; white-space: nowrap; font-size: 11px; }
.row-action button > span { margin-left: 8px; color: #bd8ea8; }
.row-action a { display: inline-block; padding: 9px 11px; color: inherit; background: transparent; white-space: nowrap; font-size: 11px; }
.row-action a > span { margin-left: 8px; color: #bd8ea8; }
.mobile-label { display: none; }
.list-footer { justify-content: space-between; gap: 12px; margin-top: 15px; }
.range-label { color: #c4bdcd; font-size: 11px; }
@@ -452,8 +471,8 @@
.average-score { grid-column: 2; grid-row: 3; text-align: left; font-size: 11px; }
.mobile-label { display: inline-block; min-width: 48px; margin-right: 6px; color: #828e9f; font-size: 10px; }
.row-action { grid-column: 3; grid-row: 2 / 4; align-self: end; }
.row-action button { padding: 7px 8px; font-size: 10px; }
.row-action button > span { display: none; }
.row-action a { padding: 7px 8px; font-size: 10px; }
.row-action a > span { display: none; }
.probability-cell { grid-column: 1 / -1; grid-row: 4; padding-top: 11px; margin-top: 5px; border-top: 1px solid #3c3342; }
.achievement-grid { gap: 12px; text-align: center; }
.achievement-grid > div { gap: 5px; }