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
+49 -20
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { ANALYSIS_TRANSFER_KEY, parseAnalysisTransfer, transferredAnalysisKey } from '$lib/models/analysis-transfer.ts';
import { onMount, tick } from 'svelte';
import LeaderSongRanking from '$lib/components/LeaderSongRanking.svelte';
import { analytics } from '$lib/analytics.ts';
@@ -119,6 +120,8 @@
let pendingSimulationContext: SimulationContext | null = null;
let resultInputKey = '';
let pendingInputKey = '';
let transferredResult = false;
let transferredSource = '조합 탐색';
let simulationBase: ExactOptimizationResult | null = null;
let simulatedResult: ExactOptimizationResult | null = null;
let excludedActiveWindows: SkillWindow[] = [];
@@ -334,11 +337,46 @@
analysisReady = true;
}
if (mode === 'analysis') {
try {
const raw = sessionStorage.getItem(ANALYSIS_TRANSFER_KEY);
if (raw) {
const transfer = parseAnalysisTransfer(raw);
restoreAnalysisState(JSON.stringify({ ...transfer.capture.settings, schemaVersion: 2 }));
// Let hydration finish before mounting the result's reactive timeline.
void tick().then(async () => {
if (!mounted) return;
result = transfer.result;
resultCapture = transfer.capture;
resultInputKey = transferredAnalysisKey(transfer.capture);
achievementProbabilities = transfer.achievementProbabilities;
simulationContext = { cards: transfer.cards, chart: transfer.chart, options: {
perfectNoteScore: transfer.result.perfectNoteScore,
directSongScoreBonusPercent: transfer.directSongScoreBonusPercent
} };
transferredResult = true;
transferredSource = transfer.source === 'rating' ? '리더 레이팅' : '조합 탐색';
await tick();
requestAnimationFrame(() => {
if (!mounted) return;
const heading = document.getElementById('result-title');
heading?.focus({ preventScroll: true });
heading?.scrollIntoView({ behavior: 'instant', block: 'start' });
});
});
sessionStorage.removeItem(ANALYSIS_TRANSFER_KEY);
}
} catch (error) {
errorMessage = error instanceof Error ? error.message : '조합 탐색 결과를 읽지 못했습니다.';
}
}
worker = createScoreOptimizerWorker();
worker.onmessage = (event) => {
if (event.data.type === 'progress') progress = event.data.progress;
if (event.data.type === 'result') {
result = event.data.result;
transferredResult = false;
resultCapture = pendingCapture;
resultInputKey = pendingInputKey;
simulationContext = pendingSimulationContext;
@@ -568,24 +606,13 @@
throw new Error('서로 다른 멤버 5명과 성장 설정을 확인해 주세요.');
}
return { cards: context.cards, overallPower: context.unitPower.overallPower,
directSongScoreBonusPercent: context.boardEffects.directSongScoreBonusPercent };
}
async function showRatingResult(chart: NonNullable<typeof selectedChart>, rankingResult: ExactOptimizationResult, power: number, probabilities: ScoreAchievementProbabilities) {
ratingResultKey = ratingInputKey;
selectedSongId = chart.songId;
difficulty = chart.difficulty as DifficultyKey;
result = rankingResult;
achievementProbabilities = probabilities;
clearConnectionOptimizationProposal();
await tick();
resultCapture = structuredClone({
settings: presetSnapshot,
growth: { collection, memberBoards, boardCards, memoryCount, memberPowerUpPercent },
title: chart.title, durationSeconds: chart.durationSeconds, overallPower: power,
analyzedAt: new Date().toISOString()
});
document.getElementById('result-title')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
directSongScoreBonusPercent: context.boardEffects.directSongScoreBonusPercent,
capture: structuredClone({
settings: { ...presetSnapshot, songId: chart.songId, difficulty: chart.difficulty as DifficultyKey, songScoreBonusOverride: 10 },
growth: { collection, memberBoards, boardCards, memoryCount, memberPowerUpPercent },
title: chart.title, durationSeconds: chart.durationSeconds, overallPower: context.unitPower.overallPower,
analyzedAt: new Date().toISOString()
}) };
}
function connectionSocketName(memberId: string, socketId: string): string {
@@ -1068,12 +1095,13 @@
{#if mode === 'rating'}
<LeaderSongRanking {charts} leaderId={leaderCharacterId} leaderName={leaderMember?.name ?? ''}
now={releaseNow} inputKey={ratingInputKey} disabled={!analysisReady || running || connectionOptimizationRunning || candidateCount === 0}
getContext={ratingContext} onSelect={showRatingResult} />
getContext={ratingContext} />
{/if}
{#if mode === 'analysis' || result}
<section class="panel result" aria-labelledby="result-title">
<div class="section-title"><div><span>{mode === 'rating' ? '03' : '02'}</span><h2 id="result-title">분석 결과</h2></div></div>
<div class="section-title"><div><span>{mode === 'rating' ? '03' : '02'}</span><h2 id="result-title" tabindex="-1">분석 결과</h2></div></div>
{#if transferredResult}<p class="board-guide">{transferredSource} 당시의 계산 결과입니다. 다시 분석하면 현재 사용자 데이터를 적용합니다.</p>{/if}
{#if resultStale || pendingStale}<p class="stale-warning" role="status">입력 조건 또는 사용자 데이터가 변경되었습니다. {resultStale ? '아래는 이전 조건의 결과입니다. ' : ''}{pendingStale ? '진행 중인 분석은 시작 당시 조건으로 계속 계산합니다. ' : ''}최신 조건을 적용하려면 다시 분석해 주세요.</p>{/if}
{#if mode === 'analysis' && resultCapture}<p class="board-guide"><strong>{resultCapture.title} · {difficultyLabel(resultCapture.settings.difficulty)}</strong> · 분석 당시 종합력 {resultCapture.overallPower.toLocaleString('ko-KR')}</p>{/if}
{#if mode === 'rating' && result}<p class="board-guide"><strong>{selectedChart?.title} · {difficultyLabel(difficulty)}</strong> · 종합력 {overallPower.toLocaleString('ko-KR')} · 가창 보너스 10%</p>{/if}
@@ -1247,6 +1275,7 @@
</main>
<style>
#result-title { scroll-margin-top: 90px; }
.stale-warning { margin: 14px 0; padding: 11px 14px; border: 1px solid #665333; border-radius: 9px; color: #edcf92; background: #292319; font-size: 13px; line-height: 1.6; }
main { width: min(var(--page-max-width), calc(100% - var(--page-gutter))); margin: 0 auto; padding: var(--page-content-top) 0 80px; }
.page-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: var(--page-header-bottom); }