feat: complete placement analysis workflow

This commit is contained in:
2026-08-18 14:22:04 +09:00
parent dd3ea911f6
commit 7b713bffe3
452 changed files with 97115 additions and 24 deletions
+100
View File
@@ -0,0 +1,100 @@
import type { CardListPreferences, CollectionCardState, UserData } from '$lib/models/user-data';
import { createUserData } from '$lib/models/user-data';
export const USER_DATA_STORAGE_KEY = 'holodori:user-data:v1';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function stringArray(value: unknown, fallback: string[]): string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string')
? [...value]
: [...fallback];
}
function finiteNumber(value: unknown, fallback = 0): number {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}
function parseCollection(value: Record<string, unknown>): UserData['collection'] {
return Object.fromEntries(
Object.entries(value).flatMap(([cardId, state]) => {
if (!isRecord(state)) return [];
const parsed: CollectionCardState = {
owned: state.owned === true,
awakening: Math.max(0, Math.min(5, Math.round(finiteNumber(state.awakening))))
};
return [[cardId, parsed]];
})
);
}
function parseMemberBoards(value: unknown): UserData['memberBoards'] {
if (!isRecord(value)) return {};
return Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, string[]] =>
Array.isArray(entry[1]) && entry[1].every((nodeId) => typeof nodeId === 'string')
)
);
}
function parseBoardCards(value: unknown): UserData['boardCards'] {
if (!isRecord(value)) return {};
return Object.fromEntries(Object.entries(value).flatMap(([memberId, placements]) => {
if (!isRecord(placements)) return [];
return [[memberId, Object.fromEntries(
Object.entries(placements).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
)]];
}));
}
export function parseUserData(text: string, fallbackPreferences: CardListPreferences): UserData {
const value: unknown = JSON.parse(text);
if (!isRecord(value) || value.schemaVersion !== 1) {
throw new Error('지원하지 않는 백업 파일입니다.');
}
if (!isRecord(value.preferences) || !isRecord(value.collection) || !Array.isArray(value.savedDecks)) {
throw new Error('올바른 홀로도리 백업 파일이 아닙니다.');
}
const preferences = value.preferences;
const defaults = createUserData(fallbackPreferences);
return {
schemaVersion: 1,
updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : defaults.updatedAt,
memoryCount: Math.max(0, Math.round(finiteNumber(value.memoryCount))),
memberPowerUpPercent: Math.max(0, Math.min(50, finiteNumber(value.memberPowerUpPercent))),
collection: parseCollection(value.collection),
memberBoards: parseMemberBoards(value.memberBoards),
boardCards: parseBoardCards(value.boardCards),
savedDecks: value.savedDecks as UserData['savedDecks'],
preferences: {
types: stringArray(preferences.types, fallbackPreferences.types),
stats: stringArray(preferences.stats, fallbackPreferences.stats),
activeConditions: stringArray(preferences.activeConditions, fallbackPreferences.activeConditions),
specialConditions: stringArray(preferences.specialConditions, fallbackPreferences.specialConditions),
activeProbabilities: stringArray(preferences.activeProbabilities, fallbackPreferences.activeProbabilities),
cardVariants: stringArray(preferences.cardVariants, fallbackPreferences.cardVariants),
generations: stringArray(preferences.generations, fallbackPreferences.generations)
}
};
}
export function loadBrowserData(fallbackPreferences: CardListPreferences): UserData {
const stored = localStorage.getItem(USER_DATA_STORAGE_KEY);
return stored ? parseUserData(stored, fallbackPreferences) : createUserData(fallbackPreferences);
}
export function saveBrowserData(data: UserData): UserData {
const saved = { ...data, updatedAt: new Date().toISOString() } satisfies UserData;
localStorage.setItem(USER_DATA_STORAGE_KEY, JSON.stringify(saved));
return saved;
}
export function clearBrowserData(): void {
localStorage.removeItem(USER_DATA_STORAGE_KEY);
}