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 환경 권장 ![홀로도리 계산기 홈 화면](docs/images/home.png) @@ -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(/ -