diff --git a/AGENTS.md b/AGENTS.md
index bcd9414..a1cf33f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,8 +1,11 @@
# Repository instructions
-## Online and offline parity
+## Online-only release policy
-- Every user-facing feature, fix, data update, and behavior change must be included in both the online app and the offline app.
-- Keep shared behavior in `src/` whenever possible. If an online-only dependency is introduced, add or update the corresponding offline alias or adapter under `offline/`.
-- Treat `npm run build:offline` as a required verification step for every repository change, in addition to the relevant tests, checks, and online build.
-- Do not commit generated `offline-dist/` or `release/` artifacts unless the user explicitly requests packaged artifacts; these directories are ignored build outputs.
+- The supported product and release target is the online app, including the GitHub Pages deployment.
+- Do not run `npm run build:offline` or `npm run package:offline` during implementation, verification, commits, releases, or deployments.
+- Do not create, commit, upload, or publish standalone offline builds, `offline-dist/` output, or offline ZIP artifacts.
+- Online/offline feature parity is no longer required. Legacy offline-related source may remain only where the online Pages build still depends on shared adapters.
+- Verify repository changes with the relevant tests, `npm run check`, `npm run build`, and `npm run build:pages` when preparing a deployment.
+- Do not bump the package version, create version-specific release notes, create tags, or publish GitHub Releases for routine online commits and deployments unless the user explicitly requests them.
+- Use `UPDATES.md` as the single user-facing update history and include only changes that affect service users.
diff --git a/README.md b/README.md
index e9c2297..d63c053 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
홀로라이브 드림즈의 카드와 멤버 성장 상태를 관리하고, 악곡별 최적 배치와 카드 조합을 분석하는 비공식 도구입니다.
-[최신 버전 다운로드](https://github.com/DD4SN/HolodoriCalc/releases/latest) · 한국어 전용 · PC 환경 권장
+[온라인 서비스 이용](https://doricalc-holo.live/) · 한국어 전용 · PC 환경 권장

@@ -16,18 +16,12 @@
- 고정 멤버를 기준으로 남은 자리에 들어갈 카드 조합 탐색
- 액티브·스페셜 발동 구간과 노트 분포 시각화
- 사용자 데이터 백업과 복원
-- 인터넷 연결 없이 실행 가능한 오프라인 버전 제공
-## 다운로드 및 실행
+## 서비스 이용
-1. [최신 Release](https://github.com/DD4SN/HolodoriCalc/releases/latest)를 엽니다.
-2. `Assets`에서 `HolodoriCalc-offline-v버전.zip`을 내려받습니다.
-3. ZIP 파일을 원하는 폴더에 완전히 압축 해제합니다.
-4. 압축을 해제한 폴더의 `index.html`을 실행합니다.
+[홀로도리 계산기 온라인 서비스](https://doricalc-holo.live/)에 접속해 사용합니다. 별도의 설치나 다운로드는 필요하지 않습니다.
-> GitHub가 자동으로 제공하는 `Source code (zip)`은 실행용 배포 파일이 아닙니다. 반드시 `HolodoriCalc-offline-...zip`을 받아 주세요.
-
-Chrome 또는 Edge의 최신 버전을 권장합니다. 일부 브라우저는 로컬 HTML의 저장소나 네트워크 접근을 제한할 수 있습니다.
+Chrome 또는 Edge의 최신 버전을 권장합니다.
## 처음 사용하는 순서
@@ -117,9 +111,7 @@ Chrome 또는 Edge의 최신 버전을 권장합니다. 일부 브라우저는
## 업데이트
-홈 화면에서 현재 버전과 GitHub의 최신 Release를 비교합니다. 새 버전이 있으면 오프라인 ZIP 다운로드 버튼이 표시됩니다.
-
-업데이트는 자동으로 파일을 교체하지 않습니다. 새 ZIP을 내려받아 별도 폴더에 압축을 해제한 뒤 `index.html`을 실행해 주세요. 업데이트 전에 사용자 데이터 백업을 권장합니다.
+검증이 끝난 업데이트는 온라인 서비스에 배포됩니다. 중요한 변경 전에는 사용자 데이터 백업을 권장합니다.
## 참고
@@ -166,13 +158,7 @@ npm run dev
```bash
npm run check
npm run build
-npm run build:offline
-```
-
-릴리즈용 오프라인 ZIP 생성:
-
-```bash
-npm run package:offline
+npm run build:pages
```
### Production 실행
diff --git a/UPDATES.md b/UPDATES.md
index 9a0acea..fff565d 100644
--- a/UPDATES.md
+++ b/UPDATES.md
@@ -1,5 +1,37 @@
---
-updatedAt: 2026-08-31T18:26:57+09:00
+updatedAt: 2026-09-01T22:16:07+09:00
+---
+
+## 2026년 9월 1일
+
+### 실측 기반 점수 보정 확대
+
+`DATA` ALiCE&u, Ahoy!! We are Houshou Pirates, Alien Alien, Asuenokyokaisen, Boom! Boom! Tropica Vacation, UNDEAD, Zenryoku-Jumping!의 Expert 실측값을 반영했습니다.
+
+### 보드 재료 합계 표시
+
+`NEW` 활성화한 보드 노드에 필요한 노드 포인트와 색상별 큐브·코어 큐브 총합을 확인할 수 있습니다.
+
+### 모바일 보드 탐색 개선
+
+`IMPROVED` 모바일에서도 이전·다음 버튼으로 멤버 보드를 빠르게 이동할 수 있습니다.
+
+### 연결 카드 배치 확인 추가
+
+`IMPROVED` 이미 배치된 카드를 이동하거나 다른 카드를 덮어쓸 때 확인 메시지를 표시합니다.
+
+### 이벤트 Pt 조합 후보 확대
+
+`IMPROVED` 이벤트 Pt 최적화에서 모든 4성 카드를 조합 후보로 포함합니다.
+
+### 카드 한글 별명 검색 추가
+
+`IMPROVED` IRyS는 `아이리스`, AZKi는 `아즈키`로 검색할 수 있습니다.
+
+### 초록 노드 초기화 안내 개선
+
+`IMPROVED` 종합력 손실이 없는 노드를 `0 · 영향 없음`으로 표시하고, 기수 보너스 상한 초과 등 손실이 0이 되는 이유를 안내합니다.
+
---
## 2026년 8월 31일
diff --git a/data/score-calibrations.json b/data/score-calibrations.json
index 705dbe1..8b4cacc 100644
--- a/data/score-calibrations.json
+++ b/data/score-calibrations.json
@@ -81,6 +81,62 @@
"measurements": [
{ "overallPower": 348760, "directSongScoreBonusPercent": 4, "normalPerfectScore": 998 }
]
+ },
+ {
+ "songId": "m0047",
+ "difficulty": "expert",
+ "scoreRatio": 290.2132653061224,
+ "measurements": [
+ { "overallPower": 284409, "directSongScoreBonusPercent": 0, "normalPerfectScore": 980 }
+ ]
+ },
+ {
+ "songId": "m0010",
+ "difficulty": "expert",
+ "scoreRatio": 252.13563829787233,
+ "measurements": [
+ { "overallPower": 284409, "directSongScoreBonusPercent": 0, "normalPerfectScore": 1128 }
+ ]
+ },
+ {
+ "songId": "m0108",
+ "difficulty": "expert",
+ "scoreRatio": 373.73061760841,
+ "measurements": [
+ { "overallPower": 284409, "directSongScoreBonusPercent": 0, "normalPerfectScore": 761 }
+ ]
+ },
+ {
+ "songId": "m0040",
+ "difficulty": "expert",
+ "scoreRatio": 280.4822485207101,
+ "measurements": [
+ { "overallPower": 284409, "directSongScoreBonusPercent": 0, "normalPerfectScore": 1014 }
+ ]
+ },
+ {
+ "songId": "m0309",
+ "difficulty": "expert",
+ "scoreRatio": 316.33724653148346,
+ "measurements": [
+ { "overallPower": 296408, "directSongScoreBonusPercent": 0, "normalPerfectScore": 937 }
+ ]
+ },
+ {
+ "songId": "m0312",
+ "difficulty": "expert",
+ "scoreRatio": 292.9459119496856,
+ "measurements": [
+ { "overallPower": 296408, "directSongScoreBonusPercent": 10, "normalPerfectScore": 1113 }
+ ]
+ },
+ {
+ "songId": "m0035",
+ "difficulty": "expert",
+ "scoreRatio": 195.1219628964692,
+ "measurements": [
+ { "overallPower": 296408, "directSongScoreBonusPercent": 10, "normalPerfectScore": 1671 }
+ ]
}
]
}
diff --git a/docs/feedback-review-process.md b/docs/feedback-review-process.md
new file mode 100644
index 0000000..211a03b
--- /dev/null
+++ b/docs/feedback-review-process.md
@@ -0,0 +1,94 @@
+# 제보 응답 검토 프로세스
+
+홀로도리 계산기의 Google Form 세 종류를 매일 한 번 확인한다.
+
+- 실측값 제보: `https://docs.google.com/forms/d/1bShzsxNN-iTk0uU3RfMfmLkYXYmUTmIWMmCtQmfl_Ik`
+- 버그 제보: `https://docs.google.com/forms/d/1z9c0c1crbCO6avXaYwrHZjhw2ib3zgAmW4ijJDTo4DM`
+- 기능 건의: `https://docs.google.com/forms/d/16NcTst7Rpa9XzqYIfPa4t9JF2X0CMfYzEbZrT8PLqco`
+
+응답은 다음 Google Sheets에서 읽는다. 세 파일 모두 Google Drive 연결 계정에서 접근 가능해야 한다.
+
+- 실측값 응답: `https://docs.google.com/spreadsheets/d/16152xv_1vWnVLSdjE3dCg_jL8fwdwwCUpYMi6fWCg1Q/edit?gid=452258967#gid=452258967`
+- 버그 응답: `https://docs.google.com/spreadsheets/d/1XspUqVqHGJmaxQr4s8Uek9pIQMS-Vr3H71H3S82ZMvY/edit?gid=581733973#gid=581733973`
+- 기능 건의 응답: `https://docs.google.com/spreadsheets/d/1GroTH_5KtrMQqRt8zvtLcrop-hwD_FVo6w7ZxVS00Qw/edit?gid=1159328779#gid=1159328779`
+
+## 공통 처리
+
+1. `data/feedback-review-state.local.json`의 마지막 처리 시각 이후 응답만 가져온다. 이 파일은 `data/*` 규칙으로 Git에서 제외된다.
+2. Form 응답 시각과 행 번호 또는 안정적인 응답 식별자를 함께 기록해 재실행 때 중복 처리하지 않는다.
+3. 개인정보와 첨부 원본은 사용자에게 전달하는 요약에 넣지 않는다. 재현과 판단에 필요한 내용만 최소한으로 적는다.
+4. 접근 실패나 열 형식 변경이 있으면 데이터를 추정하지 않고 사용자에게 막힌 지점을 알린다.
+5. 처리가 끝난 응답까지만 상태 파일의 워터마크를 전진시킨다.
+
+## 실측값 제보
+
+다음 조건을 모두 만족한 제보만 반영 후보로 취급한다.
+
+- 악곡 ID와 난이도가 현재 카탈로그에 존재한다.
+- 종합력, 악곡 직접 스코어 보너스, 일반 PERFECT 1노트 점수가 양의 유한 숫자다.
+- 스크린샷 등 제보 근거에서 입력 숫자를 교차 확인할 수 있다.
+- 동일 조건의 기존 실측과 충돌하지 않는다. 충돌하면 어느 쪽도 자동 반영하지 않고 사용자에게 알린다.
+- 보정 비율로 모든 해당 악곡 실측값의 1노트 점수를 정확히 재현한다.
+
+한 실측값으로 새 보정 비율을 만드는 경우 기존 데이터와 같은 식을 사용한다.
+
+```text
+scoreRatio = overallPower × (1 + directSongScoreBonusPercent / 100) / normalPerfectScore
+```
+
+승인 가능한 제보는 `data/score-calibrations.json`에 측정값을 추가하거나 보정 항목을 만든 뒤 아래 순서로 검증한다.
+
+```powershell
+npm run data:validate-score-calibrations
+npm run data:apply-score-calibrations
+npm run test:engine
+npm run check
+npm run build
+```
+
+어느 단계든 실패하면 변경을 서비스 반영 완료로 표시하지 않는다. 검증이 모두 통과하면 변경 파일과 근거, 검산 결과를 사용자에게 요약한다. 자동 커밋·푸시·배포는 하지 않는다.
+
+## 버그 제보
+
+신규 버그를 재현 가능성, 영향 범위, 중복 여부로 묶는다. 재현할 수 있고 수정 범위가 명확한 버그는 원인을 확인한 뒤 최소 변경으로 직접 수정하고 회귀 테스트를 추가한다. 수정 후 공통 검증 절차를 모두 통과해야만 수정 완료로 보고한다.
+
+다음 경우에는 코드를 변경하지 않고 조사 결과와 필요한 판단을 사용자에게 알린다.
+
+- 재현 정보가 부족하거나 원인을 신뢰성 있게 특정할 수 없다.
+- 여러 기능의 동작 정책, 데이터 모델 또는 대규모 구조 변경이 필요하다.
+- 외부 서비스 변경, 데이터 마이그레이션, 보안·개인정보 판단이 필요하다.
+- 기존 사용자 변경과 충돌해 안전하게 분리할 수 없다.
+
+우선순위는 다음처럼 분류한다.
+
+- 긴급: 데이터 손실, 계산 결과의 중대한 오류, 서비스 사용 불가
+- 높음: 주요 기능 실패 또는 넓은 사용자 영향
+- 보통: 우회 가능한 기능 오류
+- 낮음: 표시 문제, 경미한 불편, 재현 정보 부족
+
+## 기능 건의
+
+유사 건의를 하나로 묶고 사용자 문제, 기대 효과, 예상 영향 범위를 판단한다. 요구사항이 명확하고 기존 설계 안에서 작고 되돌릴 수 있게 구현 가능한 기능은 직접 구현하고 회귀 테스트와 공통 검증 절차를 실행한다.
+
+UI 전면 변경, 새로운 제품 정책, 데이터 구조 변경, 외부 의존성 도입처럼 선택에 따라 결과가 크게 달라지는 기능은 구현안과 예상 변경 범위를 먼저 사용자에게 보고하고 승인을 기다린다.
+
+## 코드 변경 공통 검증
+
+버그 수정 또는 기능 구현 후 다음 명령을 순서대로 실행한다.
+
+```powershell
+npm run test:engine
+npm run check
+npm run build
+```
+
+변경과 직접 관련된 추가 테스트가 있으면 함께 실행한다. 어느 단계든 실패하면 완료로 보고하지 않으며, 자동으로 커밋·푸시·배포하지 않는다.
+
+## 매일 알림 형식
+
+- 신규 응답 수: 실측값 / 버그 / 기능
+- 실측값: 반영 완료, 보류, 충돌, 검증 실패 각각의 건수와 근거
+- 버그: 우선순위별 제목, 재현 결과, 수정·보류 상태와 검증 결과
+- 기능: 묶음별 제안, 자동 구현·승인 대기 상태와 검증 결과
+- 필요한 사용자 판단: 선택이 필요한 항목만 별도 표시
+- 신규 응답이 없으면 짧게 `신규 제보 없음`으로 보고
diff --git a/docs/github-pages-deployment.md b/docs/github-pages-deployment.md
index 6ddf25f..8b6550c 100644
--- a/docs/github-pages-deployment.md
+++ b/docs/github-pages-deployment.md
@@ -44,7 +44,7 @@ npm run deploy:pages
`.gitea/workflows/deploy-pages.yml`은 `main` 갱신, 수동 실행, 5분 간격 예약 실행에서 공개 가능 여부를 확인합니다. 공개 시각 전이면 정상적으로 배포를 건너뛰고, 승인 버전 불일치나 날짜 누락 같은 정책 오류는 실패로 처리합니다. 검증을 통과하면 정적 사이트를 만든 뒤 생성 파일만 GitHub Pages 저장소 최상위에 커밋합니다.
-이 워크플로는 더 이상 사용하지 않는 서버용 온라인 빌드를 만들지 않습니다. 오프라인 빌드도 로컬 회귀 테스트용으로만 유지하며 GitHub Pages 배포에서는 `build:pages`만 실행합니다.
+이 워크플로는 서버용 빌드나 별도의 오프라인 배포물을 만들지 않고 GitHub Pages용 `build:pages`만 실행합니다.
사전 준비:
diff --git a/package.json b/package.json
index 3c94a15..a81c399 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,10 @@
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 5173",
"build": "vite build",
- "build:offline": "svelte-kit sync && vite build --config vite.offline.config.ts && node scripts/finalize-offline-build.mjs",
"check:pages-data": "node scripts/verify-pages-public-data.mjs",
- "build:pages": "svelte-kit sync && npm run check:pages-data && vite build --config vite.pages.config.ts && node scripts/finalize-offline-build.mjs pages-dist",
+ "build:pages": "svelte-kit sync && npm run check:pages-data && vite build --config vite.pages.config.ts && node scripts/finalize-pages-build.mjs",
"deploy:pages": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/deploy-pages.ps1",
"publish:pages": "node scripts/publish-pages.mjs",
- "package:offline": "npm run build:offline && powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package-offline.ps1",
"preview": "vite preview --host 0.0.0.0",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"test:engine": "node --test src/lib/engine/*.test.ts src/lib/analytics/*.test.ts",
@@ -22,6 +20,7 @@
"data:sync": "node scripts/collect-holodoridb.mjs",
"data:import-songs": "node scripts/import-legacy-song-data.mjs",
"data:collect-charts": "node scripts/collect-exact-charts.mjs",
+ "data:validate-score-calibrations": "node scripts/validate-score-calibrations.mjs",
"data:apply-score-calibrations": "node scripts/apply-score-calibrations.mjs",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
diff --git a/scripts/apply-score-calibrations.mjs b/scripts/apply-score-calibrations.mjs
index b657e08..ee01d1f 100644
--- a/scripts/apply-score-calibrations.mjs
+++ b/scripts/apply-score-calibrations.mjs
@@ -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`);
diff --git a/scripts/deploy-pages.ps1 b/scripts/deploy-pages.ps1
index 4f34797..a32d4ba 100644
--- a/scripts/deploy-pages.ps1
+++ b/scripts/deploy-pages.ps1
@@ -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.' }
diff --git a/scripts/finalize-offline-build.mjs b/scripts/finalize-pages-build.mjs
similarity index 65%
rename from scripts/finalize-offline-build.mjs
rename to scripts/finalize-pages-build.mjs
index 1ef1c1b..eab90ad 100644
--- a/scripts/finalize-offline-build.mjs
+++ b/scripts/finalize-pages-build.mjs
@@ -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(/
-