feat: complete placement analysis workflow

This commit is contained in:
dal4segno committed 2026-08-18 14:22:04 +09:00
1 parent dd3ea911f6
commit 7b713bffe3
452 files changed
+97115 -24

No files matched your search

+102 -10
View File
@@ -1,5 +1,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import cardSummary from '$lib/data/cards-summary.json';
import type { CardListPreferences, UserData } from '$lib/models/user-data';
import { loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
type CardType = '큐트' | '퓨어' | '해피';
type HighestStat = '퍼포먼스' | '테크닉' | '센스';
@@ -58,6 +61,39 @@
let selectedGenerations = [...generationOptions];
let expandedCardId: string | null = null;
let selectedSkillLevel = 1;
let collection: UserData['collection'] = {};
let memberBoards: UserData['memberBoards'] = {};
let boardCards: UserData['boardCards'] = {};
let savedDecks: UserData['savedDecks'] = [];
let memoryCount = 0;
let memberPowerUpPercent = 0;
let storageReady = false;
let currentUserData: UserData;
$: currentUserData = {
schemaVersion: 1,
updatedAt: new Date().toISOString(),
memoryCount,
memberPowerUpPercent,
collection,
memberBoards,
boardCards,
savedDecks,
preferences: currentPreferences()
};
$: if (storageReady) {
try {
saveBrowserData(currentUserData);
} catch {}
}
onMount(() => {
try {
applyUserData(loadBrowserData(defaultPreferences()));
storageReady = true;
} catch {}
});
$: filteredCards = cards.filter((card) =>
selectedTypes.includes(card.type)
@@ -91,6 +127,46 @@
selectedGenerations = [...generationOptions];
}
function defaultPreferences(): CardListPreferences {
return {
types: [...typeOptions],
stats: [...statOptions],
activeConditions: [...skillConditionOptions],
specialConditions: [...skillConditionOptions],
activeProbabilities: [...activeProbabilityOptions],
cardVariants: [...cardVariantOptions],
generations: [...generationOptions]
};
}
function currentPreferences(): CardListPreferences {
return {
types: [...selectedTypes],
stats: [...selectedStats],
activeConditions: [...selectedActiveConditions],
specialConditions: [...selectedSpecialConditions],
activeProbabilities: [...selectedActiveProbabilities],
cardVariants: [...selectedCardVariants],
generations: [...selectedGenerations]
};
}
function applyUserData(data: UserData) {
selectedTypes = data.preferences.types.filter((value): value is CardType => typeOptions.includes(value as CardType));
selectedStats = data.preferences.stats.filter((value): value is HighestStat => statOptions.includes(value as HighestStat));
selectedActiveConditions = data.preferences.activeConditions.filter((value): value is SkillConditionStatus => skillConditionOptions.includes(value as SkillConditionStatus));
selectedSpecialConditions = data.preferences.specialConditions.filter((value): value is SkillConditionStatus => skillConditionOptions.includes(value as SkillConditionStatus));
selectedActiveProbabilities = data.preferences.activeProbabilities.filter((value): value is ActiveProbability => activeProbabilityOptions.includes(value as ActiveProbability));
selectedCardVariants = data.preferences.cardVariants.filter((value) => cardVariantOptions.includes(value));
selectedGenerations = data.preferences.generations.filter((value) => generationOptions.includes(value));
collection = data.collection;
memberBoards = data.memberBoards;
boardCards = data.boardCards;
savedDecks = data.savedDecks;
memoryCount = data.memoryCount;
memberPowerUpPercent = data.memberPowerUpPercent;
}
function cardVariantStatus(card: CardSummary): string {
return card.versionName || '일반';
}
@@ -192,14 +268,16 @@
<h1>카드 목록</h1>
<p class="description">카드의 기본 정보를 한눈에 확인하세요.</p>
</div>
<p class="total">
<strong>{filteredCards.length}</strong>
{#if filteredCards.length === cards.length}
장
{:else}
<span>/ {cards.length}장</span>
{/if}
</p>
<div class="header-side">
<p class="total">
<strong>{filteredCards.length}</strong>
{#if filteredCards.length === cards.length}
장
{:else}
<span>/ {cards.length}장</span>
{/if}
</p>
</div>
</header>
<section class="filters" aria-labelledby="filter-heading">
@@ -547,7 +625,7 @@
main {
width: min(1120px, calc(100% - 40px));
margin: 0 auto;
padding: 64px 0 80px;
padding: 36px 0 80px;
}
.page-header {
@@ -601,6 +679,13 @@
color: #8a92a2;
}
.header-side {
display: grid;
justify-items: end;
gap: 9px;
}
.filters {
margin-bottom: 20px;
padding: 22px 24px 24px;
@@ -1321,6 +1406,7 @@
color: #747e90;
}
.filters {
border-color: rgb(255 255 255 / 8%);
background: rgb(22 28 39 / 84%);
@@ -1511,6 +1597,7 @@
.page-header {
align-items: flex-start;
flex-direction: column;
}
.description {
@@ -1518,7 +1605,12 @@
}
.total {
margin-top: 4px;
margin: 0;
}
.header-side {
width: 100%;
justify-items: start;
}
.filters {