fix: keep Holo-work rows stable until sorting is reapplied
Publish GitHub Pages output / publish (push) Successful in 31s

This commit is contained in:
2026-09-14 11:26:02 +09:00
parent dd37f59565
commit 238b9a9913
4 changed files with 56 additions and 5 deletions
+23
View File
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { sortHoloWorkMembers } from '../models/holo-work-sort.ts';
const members = [{ id: 'a', order: 1 }, { id: 'b', order: 2 }, { id: 'c', order: 3 }];
const ids = (rows: typeof members) => rows.map((row) => row.id);
test('횟수 변경은 기존 순서를 유지하고 다시 정렬할 때 최신 횟수를 반영한다', () => {
const counts = { a: 1, b: 2, c: 3 };
const snapshot = sortHoloWorkMembers(members, counts, 'asc');
counts.a = 5;
assert.deepEqual(ids(snapshot), ['a', 'b', 'c']);
assert.deepEqual(ids(sortHoloWorkMembers(members, counts, 'asc')), ['b', 'c', 'a']);
assert.deepEqual(ids(sortHoloWorkMembers(members, counts, 'desc')), ['a', 'c', 'b']);
assert.deepEqual(ids(members), ['a', 'b', 'c']);
});
test('미등록 횟수는 0이며 동률은 멤버순, 기준 변경은 전체 목록에 적용한다', () => {
const counts = { a: 2, b: 2 };
assert.deepEqual(ids(sortHoloWorkMembers(members, counts, 'asc')), ['c', 'a', 'b']);
assert.deepEqual(ids(sortHoloWorkMembers(members, counts, 'desc')), ['a', 'b', 'c']);
assert.deepEqual(ids(sortHoloWorkMembers([...members].reverse(), counts, 'member')), ['a', 'b', 'c']);
});
+11
View File
@@ -0,0 +1,11 @@
export type HoloWorkSort = 'member' | 'asc' | 'desc';
/** A fresh ordering snapshot; subsequent count edits do not move its rows. */
export function sortHoloWorkMembers<T extends { id: string; order: number }>(
members: readonly T[], counts: Readonly<Record<string, number>>, sort: HoloWorkSort
): T[] {
return [...members].sort((a, b) => {
const difference = (counts[a.id] ?? 0) - (counts[b.id] ?? 0);
return (sort === 'asc' ? difference : sort === 'desc' ? -difference : 0) || a.order - b.order;
});
}
+15 -4
View File
@@ -8,6 +8,7 @@
import boardSummary from '$lib/data/boards-summary.json';
import { adjustWorkTeam, isWorkCount, parseWorkTeams, type HoloWorkCounts } from '$lib/storage/holo-work';
import type { HoloWorkTeam } from '$lib/models/user-data';
import { sortHoloWorkMembers, type HoloWorkSort } from '$lib/models/holo-work-sort';
import { USER_DATA_STORAGE_KEY, loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
import { defaultPreferences } from '$lib/models/default-preferences';
@@ -22,14 +23,20 @@
$: validTeam = Boolean(teamName.trim()) && selectedMembers.every((id) => members.some((member) => member.id === id)) && new Set(selectedMembers).size === 5;
let ready = false;
let search = '';
let sort = 'member';
let sort: HoloWorkSort = 'member';
let sortedMembers = [...members];
let status = '';
let error = '';
$: visibleMembers = members.filter((member) => member.name.replace(/\s/g, '').toLocaleLowerCase().includes(search.replace(/\s/g, '').toLocaleLowerCase()))
.sort((a, b) => (sort === 'asc' ? (counts[a.id] ?? 0) - (counts[b.id] ?? 0) : sort === 'desc' ? (counts[b.id] ?? 0) - (counts[a.id] ?? 0) : 0) || a.order - b.order);
$: visibleMembers = sortedMembers.filter((member) => member.name.replace(/\s/g, '').toLocaleLowerCase().includes(search.replace(/\s/g, '').toLocaleLowerCase()));
$: total = members.reduce((sum, member) => sum + BigInt(counts[member.id] ?? 0), 0n);
$: recorded = members.filter((member) => (counts[member.id] ?? 0) > 0).length;
function applySort(nextSort: HoloWorkSort = sort) {
sort = nextSort;
sortedMembers = sortHoloWorkMembers(members, counts, sort);
status = '현재 횟수로 정렬했습니다.';
}
function loadLatestData() {
const data = loadBrowserData(defaultPreferences());
bonuses = calculateHoloWorkBoardBonuses(boardSummary, data.memberBoards, {
@@ -224,8 +231,9 @@
<h2 class="all-members-title">멤버별 기록</h2>
<div class="filters">
<label>멤버 검색<input type="search" bind:value={search} placeholder="멤버 이름 입력" /></label>
<label>정렬<select bind:value={sort}><option value="member">멤버순</option><option value="asc">횟수 적은 순</option><option value="desc">횟수 많은 순</option></select></label>
<label>정렬<select value={sort} on:change={(event) => applySort(event.currentTarget.value as HoloWorkSort)}><option value="member">멤버순</option><option value="asc">횟수 적은 순</option><option value="desc">횟수 많은 순</option></select></label>
</div>
<div class="sort-actions"><p class="hint">횟수를 변경해도 위치는 유지됩니다. 정렬 기준을 바꾸거나 다시 정렬하면 최신 횟수를 반영합니다.</p><button type="button" on:click={() => applySort()} disabled={!ready}>다시 정렬</button></div>
<ul aria-label="멤버별 Holo워크 횟수">
{#each visibleMembers as member (member.id)}
<li>
@@ -253,6 +261,9 @@
.summary strong { display: block; margin-top: 10px; color: #f3b4d0; font-size: 26px; overflow-wrap: anywhere; }
small { margin-left: 7px; color: #a4adba; font-size: 13px; font-weight: 400; }
.filters { display: grid; grid-template-columns: 1fr 150px; gap: 12px; }
.sort-actions { display: flex; align-items: center; gap: 12px; justify-content: space-between; margin-top: 12px; }
.sort-actions .hint { margin: 0; }
.sort-actions button { flex-shrink: 0; padding: 8px 12px; font-size: 13px; }
label { display: grid; gap: 8px; color: #a4adba; font-size: 13px; }
input, select { box-sizing: border-box; min-width: 0; width: 100%; height: 44px; padding: 0 12px; border: 1px solid #414b5b; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; }
.status { min-height: 20px; color: #f3b4d0; font-size: 13px; }