118 lines
4.9 KiB
TypeScript
118 lines
4.9 KiB
TypeScript
import type { CardListPreferences, CardListSortKey, CardSkillLevel, CollectionCardState, SortDirection, 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 cardListSortKey(value: unknown, fallback: CardListSortKey | null): CardListSortKey | null {
|
|
return value === 'coolTime' || value === 'probability' || value === 'duration' || value === 'effect'
|
|
? value
|
|
: fallback;
|
|
}
|
|
|
|
function sortDirection(value: unknown, fallback: SortDirection): SortDirection {
|
|
return value === 'asc' || value === 'desc' ? value : fallback;
|
|
}
|
|
|
|
function cardSkillLevel(value: unknown, fallback: CardSkillLevel): CardSkillLevel {
|
|
return value === 1 || value === 2 ? 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),
|
|
sortKey: cardListSortKey(preferences.sortKey, fallbackPreferences.sortKey),
|
|
sortDirection: sortDirection(preferences.sortDirection, fallbackPreferences.sortDirection),
|
|
skillLevel: cardSkillLevel(preferences.skillLevel, fallbackPreferences.skillLevel)
|
|
}
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|