feat: apply feedback updates and streamline online delivery
Publish GitHub Pages output / publish (push) Successful in 34s
Publish GitHub Pages output / publish (push) Successful in 34s
This commit is contained in:
@@ -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`);
|
||||
|
||||
@@ -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과 같은 위치에 그대로 두어야 합니다.
|
||||
|
||||
계산과 데이터 저장은 사용자의 브라우저에서 처리됩니다.
|
||||
새 버전으로 교체하기 전에는 사용자 데이터 화면에서 백업 파일을 저장해 주세요.
|
||||
`);
|
||||
@@ -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
|
||||
@@ -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}.`);
|
||||
|
||||
@@ -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`));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}개 악곡 실측 보정과 원본 실측값을 검산했습니다.`);
|
||||
Reference in New Issue
Block a user