feat: apply feedback updates and streamline online delivery
Publish GitHub Pages output / publish (push) Successful in 34s

This commit is contained in:
2026-09-01 22:21:45 +09:00
parent 98d5575463
commit c0f4060dec
36 changed files with 447 additions and 471 deletions
+4 -1
View File
@@ -1,5 +1,6 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { assertValidScoreCalibrations } from './score-calibration-validation.mjs';
const calibrationPath = path.resolve('data/score-calibrations.json');
const catalogPath = path.resolve('src/lib/data/exact-chart-catalog.json');
@@ -9,6 +10,8 @@ const calibrations = JSON.parse(await readFile(calibrationPath, 'utf8'));
const catalog = JSON.parse(await readFile(catalogPath, 'utf8'));
const catalogRows = Array.isArray(catalog) ? catalog : catalog.charts;
await assertValidScoreCalibrations(calibrations, { chartDirectory });
for (const calibration of calibrations.charts) {
const difficulty = calibration.difficulty.toLowerCase();
const catalogRow = catalogRows.find((row) =>
@@ -25,7 +28,7 @@ for (const calibration of calibrations.charts) {
const chart = JSON.parse(await readFile(chartPath, 'utf8'));
chart.scoreRatio = calibration.scoreRatio;
chart.scoreCalibrationSource = 'measured';
await writeFile(chartPath, `${JSON.stringify(chart)}\n`);
await writeFile(chartPath, JSON.stringify(chart));
}
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
+1 -2
View File
@@ -53,8 +53,7 @@ try {
return
}
$package = Get-Content -LiteralPath (Join-Path $projectRoot 'package.json') -Raw | ConvertFrom-Json
& git commit -m ("deploy: HolodoriCalc v{0}" -f $package.version)
& git commit -m 'deploy: update HolodoriCalc'
if ($LASTEXITCODE -ne 0) { throw 'Pages commit failed.' }
& git push origin HEAD:main
if ($LASTEXITCODE -ne 0) { throw 'Pages push failed.' }
@@ -1,12 +1,10 @@
import { readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
const output = path.resolve(process.argv[2] ?? 'offline-dist');
const output = path.resolve('pages-dist');
const htmlPath = path.join(output, 'index.html');
const scriptPath = path.join(output, 'assets', 'app.js');
const stylePath = path.join(output, 'assets', 'app.css');
const packageJson = JSON.parse(await readFile(path.resolve('package.json'), 'utf8'));
const isPagesBuild = path.basename(output) === 'pages-dist';
let html = await readFile(htmlPath, 'utf8');
html = html.replace(/<script type="module" crossorigin src=/g, '<script defer src=');
@@ -20,7 +18,6 @@ await writeFile(scriptPath, script);
let style = await readFile(stylePath, 'utf8');
style = style.replaceAll('/icons/', '../icons/');
style += `
/* file://에서는 CSS 마스크 대신 읽기 쉬운 텍스트 뱃지를 사용한다. */
.description-stat-icon { display: none !important; }
.stat-badge .stat-icon,
.filter-options label .stat-icon {
@@ -44,22 +41,11 @@ await writeFile(stylePath, style);
// 채보는 app.js에 포함되므로 public 디렉터리에서 복사된 중복 파일은 제거한다.
await rm(path.join(output, 'data'), { recursive: true, force: true });
await writeFile(path.join(output, 'VERSION.txt'), `${packageJson.version}\n`);
await writeFile(path.join(output, 'README.txt'), isPagesBuild
? `홀로도리 계산기 ${packageJson.version}
await rm(path.join(output, 'VERSION.txt'), { force: true });
await writeFile(path.join(output, 'README.txt'), `홀로도리 계산기
서비스: https://doricalc-holo.live/
이 파일들은 GitHub Pages 배포용으로 자동 생성되었습니다.
계산과 사용자 데이터 저장은 사용자의 브라우저에서 처리됩니다.
`
: `홀로도리 계산기 ${packageJson.version}
1. 압축파일의 내용을 모두 한 폴더에 풀어 주세요.
2. index.html을 Chrome 또는 Edge로 열어 주세요.
3. assets와 icons 폴더는 index.html과 같은 위치에 그대로 두어야 합니다.
계산과 데이터 저장은 사용자의 브라우저에서 처리됩니다.
새 버전으로 교체하기 전에는 사용자 데이터 화면에서 백업 파일을 저장해 주세요.
`);
-19
View File
@@ -1,19 +0,0 @@
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
$package = Get-Content -LiteralPath (Join-Path $projectRoot 'package.json') -Raw | ConvertFrom-Json
$source = Join-Path $projectRoot 'offline-dist'
$releaseDirectory = Join-Path $projectRoot 'release'
$archive = Join-Path $releaseDirectory ("HolodoriCalc-offline-v{0}.zip" -f $package.version)
if (-not (Test-Path -LiteralPath $source)) {
throw '오프라인 빌드 결과가 없습니다.'
}
New-Item -ItemType Directory -Path $releaseDirectory -Force | Out-Null
if (Test-Path -LiteralPath $archive) {
Remove-Item -LiteralPath $archive -Force
}
Compress-Archive -Path (Join-Path $source '*') -DestinationPath $archive -CompressionLevel Optimal
Write-Output $archive
+2 -3
View File
@@ -1,4 +1,4 @@
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { cp, mkdtemp, rm, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
@@ -63,8 +63,7 @@ try {
if (diffCode === 0) {
process.stdout.write('GitHub Pages output is already up to date.\n');
} else if (diffCode === 1) {
const packageJson = JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8'));
await requireGit(['commit', '-m', `deploy: HolodoriCalc v${packageJson.version}`], { cwd: checkout, env: gitEnvironment });
await requireGit(['commit', '-m', 'deploy: update HolodoriCalc'], { cwd: checkout, env: gitEnvironment });
await requireGit(['push', 'origin', 'HEAD:main'], { cwd: checkout, env: gitEnvironment });
} else {
throw new Error(`git diff failed with exit code ${diffCode}.`);
+80
View File
@@ -0,0 +1,80 @@
import { access } from 'node:fs/promises';
import path from 'node:path';
import { finalNoteScore } from '../src/lib/engine/score.ts';
const DIFFICULTIES = new Set(['easy', 'normal', 'hard', 'expert']);
function assertFiniteNumber(value, label, { minimum = -Infinity, maximum = Infinity } = {}) {
if (typeof value !== 'number' || !Number.isFinite(value) || value < minimum || value > maximum) {
throw new Error(`${label} 값이 유효하지 않습니다: ${JSON.stringify(value)}`);
}
}
export async function assertValidScoreCalibrations(calibrations, options = {}) {
const chartDirectory = options.chartDirectory ?? path.resolve('static/data/charts');
if (calibrations?.schemaVersion !== 1 || !Array.isArray(calibrations.charts)) {
throw new Error('score-calibrations.json은 schemaVersion 1과 charts 배열이 필요합니다.');
}
const chartKeys = new Set();
for (const [chartIndex, calibration] of calibrations.charts.entries()) {
const prefix = `charts[${chartIndex}]`;
if (typeof calibration.songId !== 'string' || !/^m\d{4}$/.test(calibration.songId)) {
throw new Error(`${prefix}.songId 형식이 올바르지 않습니다: ${JSON.stringify(calibration.songId)}`);
}
const difficulty = typeof calibration.difficulty === 'string'
? calibration.difficulty.toLowerCase()
: '';
if (!DIFFICULTIES.has(difficulty)) {
throw new Error(`${prefix}.difficulty가 올바르지 않습니다: ${JSON.stringify(calibration.difficulty)}`);
}
const chartKey = `${calibration.songId}.${difficulty}`;
if (chartKeys.has(chartKey)) throw new Error(`중복 실측 보정 항목입니다: ${chartKey}`);
chartKeys.add(chartKey);
assertFiniteNumber(calibration.scoreRatio, `${prefix}.scoreRatio`, { minimum: Number.EPSILON });
if (!Array.isArray(calibration.measurements) || calibration.measurements.length === 0) {
throw new Error(`${prefix}.measurements에는 한 개 이상의 실측값이 필요합니다.`);
}
const measurementKeys = new Set();
for (const [measurementIndex, measurement] of calibration.measurements.entries()) {
const measurementPrefix = `${prefix}.measurements[${measurementIndex}]`;
assertFiniteNumber(measurement.overallPower, `${measurementPrefix}.overallPower`, { minimum: 1 });
assertFiniteNumber(
measurement.directSongScoreBonusPercent,
`${measurementPrefix}.directSongScoreBonusPercent`,
{ minimum: 0, maximum: 100 }
);
assertFiniteNumber(measurement.normalPerfectScore, `${measurementPrefix}.normalPerfectScore`, { minimum: 1 });
const measurementKey = [
measurement.overallPower,
measurement.directSongScoreBonusPercent,
measurement.normalPerfectScore
].join(':');
if (measurementKeys.has(measurementKey)) {
throw new Error(`${chartKey}에 중복 실측값이 있습니다: ${measurementKey}`);
}
measurementKeys.add(measurementKey);
const reproducedScore = finalNoteScore(
measurement.overallPower / calibration.scoreRatio,
measurement.directSongScoreBonusPercent
);
if (reproducedScore !== measurement.normalPerfectScore) {
throw new Error(
`${chartKey} 실측값을 재현하지 못했습니다: 기대 ${measurement.normalPerfectScore}, 계산 ${reproducedScore}`
);
}
}
if (difficulty === 'hard' || difficulty === 'expert') {
await access(path.join(chartDirectory, `${chartKey}.json`));
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { assertValidScoreCalibrations } from './score-calibration-validation.mjs';
const calibrationPath = path.resolve('data/score-calibrations.json');
const calibrations = JSON.parse(await readFile(calibrationPath, 'utf8'));
await assertValidScoreCalibrations(calibrations);
console.log(`${calibrations.charts.length}개 악곡 실측 보정과 원본 실측값을 검산했습니다.`);