Compare commits
5
Commits
92bc93c01b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ea54699d | ||
|
|
8298f5af26 | ||
|
|
89f66c3bee | ||
|
|
3721d6296f | ||
|
|
d5d83c48f8 |
+35
-1
@@ -8,6 +8,38 @@
|
|||||||
- Gitea 저장소: `syskiller/ITOSMPiano-BachToBasics`
|
- Gitea 저장소: `syskiller/ITOSMPiano-BachToBasics`
|
||||||
- ITOSMGames와 `game.itosm.co.kr`에는 이 프로젝트 파일과 기록을 넣지 않는다.
|
- ITOSMGames와 `game.itosm.co.kr`에는 이 프로젝트 파일과 기록을 넣지 않는다.
|
||||||
|
|
||||||
|
## 2026-10-04 서비스 결정
|
||||||
|
|
||||||
|
- 피아노 학습 서비스의 최종 사용 프로그램은 Bach to Basics로 정했다.
|
||||||
|
- 포럼의 서비스 소개와 이용 안내는 `https://learn.itosm.co.kr`을 기준으로 구성한다.
|
||||||
|
- NoteRain은 최종 학습 서비스로 안내하지 않는다. 별도 종료 지시가 없으므로 기존 운영 상태 변경은 별도 작업으로 판단한다.
|
||||||
|
- Discourse 포럼 작업방에 소개와 용도별 게시 공간 구성을 요청했다. 실제 포럼 반영 결과는 해당 방에서 확인한다.
|
||||||
|
|
||||||
|
## 2026-10-09 초보자용 MIDI 연습곡
|
||||||
|
|
||||||
|
- 직접 만든 양손 피아노 연습곡 다섯 MIDI를 공개 곡 보관함에 추가했다. `별빛 계단`은 입문과 쉬움 두 버전을 같은 제목 아래에서 선택한다. 나머지는 `저녁빛 왈츠`, `해돋이 아르페지오`, `검은 건반 산책`이다.
|
||||||
|
- 각 MIDI는 약 64~76초 길이다. 왼손 근음·5도 교대와 분산 화음 및 3박자 왈츠와 오른손 아르페지오와 검은 건반 5음 음계를 연습한다.
|
||||||
|
- 영상의 곡을 복제하지 않았다. 분산 화음과 아르페지오 기법 참고 강좌의 제작자와 링크를 `docs/THIRD_PARTY_SAMPLES.md`에 기록했다.
|
||||||
|
- `python backend/tools/generate_beginner_midis.py`로 파일을 재생성하고 `node scripts/check_beginner_midis.mjs`로 두 손 트랙과 길이 및 박자를 검사한다.
|
||||||
|
- 운영 DB와 수정 대상 소스의 백업은 `/home/syskiller/piano/backups/bach-to-basics/20261009-beginner-midi`에 있다. DB 변경은 서버 시작 시 새 기본 공개 곡 5건을 등록한 것뿐이며 회원 자료 이전은 없었다.
|
||||||
|
- 개발 환경에서 MIDI 파서 검사와 백엔드 단위 테스트 5개를 통과했다. 운영 HTTPS 첫 화면과 비회원 공개 목록 및 MIDI 파일 5개 다운로드를 확인했다. 기존 회원 업로드 2개를 보존했고 백엔드만 갱신했다. 프런트엔드는 재시작하지 않았다.
|
||||||
|
- 실제 USB MIDI 장치로 다섯 곡의 손별 따라치기를 확인하는 작업은 남아 있다.
|
||||||
|
|
||||||
|
## 2026-10-09 연습곡 30개 교체
|
||||||
|
|
||||||
|
- 위 다섯 곡은 선율이 지나치게 비슷하다는 의견을 반영해 공개 목록에서 제거했다. 해당 기본 자료의 파일과 생성 스크립트도 교체했다. 회원 업로드에는 손대지 않았다.
|
||||||
|
- Mutopia Project에 원본과 권리 정보가 함께 있는 서로 다른 피아노곡 30개를 골랐다. 부르크뮐러 Op. 100에서 18곡과 슈만 Op. 68에서 12곡이다. 도입부에는 각 원곡의 첫 선율을 천천히 제시하고 마무리에는 마지막 선율과 긴 종지음을 넣었다. 각 편곡은 69~134초다.
|
||||||
|
- 오른손의 중심 선율과 왼손 저음을 양손 연습용으로 정리했다. 오른손 음표 밀도에 따라 입문과 쉬움과 중급으로 분류했다. 원본 MIDI와 RDF 및 곡별 출처와 라이선스를 `backend/tools/practice_sources`와 `backend/sample_midi/practice_collection.json`에 보관한다. 슈만 자료의 CC BY-SA 버전은 편곡본에도 적용한다.
|
||||||
|
- `node scripts/build_practice_collection.mjs`로 파일을 다시 만들고 `node scripts/check_practice_collection.mjs`로 30개의 서로 다른 선율과 양손 트랙 및 도입부와 마무리와 길이를 검사한다. 서버의 격리 폴더에서 백엔드 단위 테스트 5개를 통과했다.
|
||||||
|
- 운영 DB와 수정 대상 소스 및 기존 MIDI 파일의 백업은 `/home/syskiller/piano/backups/bach-to-basics/20261009-practice-30-intro-ending`에 있다. 백엔드만 다시 빌드했고 프런트엔드는 재시작하지 않았다. 공개 HTTPS 첫 화면과 비회원 목록의 새 곡 30개 및 각 파일의 MIDI 다운로드를 확인했다. 기존 회원 업로드 2건은 유지됐다.
|
||||||
|
- 30곡은 공개 도메인 또는 CC BY-SA로 표시된 고전 연습곡의 편곡이다. 특정 한국 쇼츠의 곡을 전사한 자료는 아니다. 실제 피아노 건반으로 각 곡의 연주감과 MIDI 입력을 확인하는 작업은 남아 있다.
|
||||||
|
|
||||||
|
## 2026-10-09 연습곡 30개 철회
|
||||||
|
|
||||||
|
- 위 30곡은 사용자 요청과 맞지 않아 운영 공개 목록과 MIDI 파일 및 생성 자료에서 모두 제거했다. 회원 업로드와 기존 기본 샘플 7개는 변경하지 않았다.
|
||||||
|
- 재시작 후에도 다시 등록되지 않도록 해당 30개 파일명을 제외 목록에 넣었다. Git 기록은 다시 쓰지 않고 삭제를 새 커밋으로 남긴다.
|
||||||
|
- 철회 직전 DB와 30개 MIDI 및 원본·생성 자료의 백업은 `/home/syskiller/piano/backups/bach-to-basics/20261009-withdraw-practice-30`에 있다. 백엔드만 다시 빌드했다. 재시작 후 HTTPS 공개 목록 0건과 DB 0건 및 MIDI 파일 0개를 확인했다. 회원 업로드 2건과 기존 기본 샘플 7개는 보존됐다.
|
||||||
|
|
||||||
## 2026-10-03 현재 상태
|
## 2026-10-03 현재 상태
|
||||||
|
|
||||||
- Docker의 `piano-bach-frontend`와 `piano-bach-backend`로 운영한다.
|
- Docker의 `piano-bach-frontend`와 `piano-bach-backend`로 운영한다.
|
||||||
@@ -28,13 +60,15 @@
|
|||||||
- 단계별 공개 샘플은 모두 오른손과 왼손을 포함한 피아노 독주다. 손별 연습은 연습 화면에서 선택한다.
|
- 단계별 공개 샘플은 모두 오른손과 왼손을 포함한 피아노 독주다. 손별 연습은 연습 화면에서 선택한다.
|
||||||
- 곡 목록 조회는 일시적인 연결 실패 때 한 번 재시도한다. 정상 응답을 받으면 남아 있던 연결 오류를 지운다.
|
- 곡 목록 조회는 일시적인 연결 실패 때 한 번 재시도한다. 정상 응답을 받으면 남아 있던 연결 오류를 지운다.
|
||||||
- 악보 커서는 변환된 악보의 시간 위치와 원본 템포 지도를 환산한 AlphaTab tick을 함께 사용한다. 음소거된 내부 플레이어가 위치 이벤트를 보내지 않는 경우에도 악보 렌더가 끝나면 커서를 직접 갱신한다. 확인한 AlphaTab 1.8.3을 고정해 커서 경로 변경으로 인한 재발을 막는다.
|
- 악보 커서는 변환된 악보의 시간 위치와 원본 템포 지도를 환산한 AlphaTab tick을 함께 사용한다. 음소거된 내부 플레이어가 위치 이벤트를 보내지 않는 경우에도 악보 렌더가 끝나면 커서를 직접 갱신한다. 확인한 AlphaTab 1.8.3을 고정해 커서 경로 변경으로 인한 재발을 막는다.
|
||||||
|
- 전체 화면의 악보 영역은 높이의 38%를 사용한다. 최소 높이는 화면에 따라 150px에서 220px 사이로 조정한다. 악보 렌더링이 끝난 뒤 실제 높이를 측정하며 양손 보표가 영역을 넘으면 배율을 자동으로 낮춰 위아래가 잘리지 않게 한다.
|
||||||
- 운영 브라우저에서 미뉴에트와 엘리제를 위하여 입문 악보의 오선과 음표 표시를 확인했다.
|
- 운영 브라우저에서 미뉴에트와 엘리제를 위하여 입문 악보의 오선과 음표 표시를 확인했다.
|
||||||
- 운영 백엔드에서 MIDI와 MusicXML과 MXL 및 PDF 검증기를 모두 통과했다.
|
- 운영 백엔드에서 MIDI와 MusicXML과 MXL 및 PDF 검증기를 모두 통과했다.
|
||||||
- 프런트 타입 검사와 67개 테스트와 운영 빌드를 통과했다.
|
- 프런트 타입 검사와 70개 테스트와 운영 빌드를 통과했다.
|
||||||
|
|
||||||
## 백업
|
## 백업
|
||||||
|
|
||||||
- 작업 전 DB와 수정 대상 소스: `/home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload`
|
- 작업 전 DB와 수정 대상 소스: `/home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload`
|
||||||
|
- 악보 영역 자동 맞춤 적용 전 소스: `/home/syskiller/piano/backups/bach-to-basics/20261003-1730-sheet-fit`
|
||||||
|
|
||||||
## 남은 확인
|
## 남은 확인
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ A browser-based piano practice tool. Drop in a **MIDI file**, a **MusicXML score
|
|||||||
|
|
||||||
이 저장소는 원본 Bach to Basics를 바탕으로 `learn.itosm.co.kr`에서 운영하는 한국어판이다. 독립 회원과 곡·악보 보관함을 제공한다. 비회원은 공개 곡을 연주할 수 있으며 회원은 MIDI와 MusicXML과 MXL 및 PDF 악보를 올려 공개 범위와 회원 등급을 지정할 수 있다.
|
이 저장소는 원본 Bach to Basics를 바탕으로 `learn.itosm.co.kr`에서 운영하는 한국어판이다. 독립 회원과 곡·악보 보관함을 제공한다. 비회원은 공개 곡을 연주할 수 있으며 회원은 MIDI와 MusicXML과 MXL 및 PDF 악보를 올려 공개 범위와 회원 등급을 지정할 수 있다.
|
||||||
|
|
||||||
기본 공개 자료는 Mutopia Project의 공개 도메인 MIDI와 그 원곡에서 만든 단계별 연습 편곡이다. `엘리제를 위하여 WoO 59`는 입문과 쉬움과 고급 버전을 같은 제목 아래에서 선택한다. 출처와 편곡 범위는 [기본 공개 샘플 출처](docs/THIRD_PARTY_SAMPLES.md)에 기록했다. 회원과 운영 구성은 [회원과 곡 보관함 운영 문서](docs/LIBRARY-AUTH-KO.md)를 따른다.
|
기본 공개 자료는 Mutopia Project의 MIDI와 그 원곡에서 만든 단계별 연습 편곡이다. `엘리제를 위하여 WoO 59`는 난이도가 다른 버전을 같은 제목 아래에서 선택한다. 출처와 편곡 범위 및 권리 표시는 [기본 공개 샘플 출처](docs/THIRD_PARTY_SAMPLES.md)에 기록했다. 회원과 운영 구성은 [회원과 곡 보관함 운영 문서](docs/LIBRARY-AUTH-KO.md)를 따른다.
|
||||||
|
|
||||||
[](https://raw.githubusercontent.com/gigliof/bach-to-basics/main/docs/demo.gif)
|
[](https://raw.githubusercontent.com/gigliof/bach-to-basics/main/docs/demo.gif)
|
||||||
|
|
||||||
|
|||||||
@@ -185,14 +185,21 @@ LEGACY_SAMPLE_KEYS = (
|
|||||||
"first-snow-accompaniment.mid",
|
"first-snow-accompaniment.mid",
|
||||||
"blue-breeze-walk.mid",
|
"blue-breeze-walk.mid",
|
||||||
"little-morning-waltz.mid",
|
"little-morning-waltz.mid",
|
||||||
|
"starlight-steps-beginner.mid",
|
||||||
|
"starlight-steps-easy.mid",
|
||||||
|
"evening-waltz-beginner.mid",
|
||||||
|
"sunrise-arpeggio-easy.mid",
|
||||||
|
"black-key-pentatonic-beginner.mid",
|
||||||
|
) + tuple(f"practice-burgmuller-{number:02}.mid" for number in range(1, 19)) + tuple(
|
||||||
|
f"practice-schumann-{number:02}.mid" for number in range(1, 13)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _seed_samples() -> None:
|
def _seed_samples() -> None:
|
||||||
source_root = Path(__file__).with_name("sample_midi")
|
source_root = Path(__file__).with_name("sample_midi")
|
||||||
with connect() as connection:
|
with connect() as connection:
|
||||||
# 초기 시험용으로 만든 짧은 음형은 실제 공개 도메인 연습곡으로 교체한다.
|
# 사용자 요청으로 철회한 기본 연습곡만 제거한다. 회원 업로드에는 영향을 주지 않는다.
|
||||||
# seed_key가 정확히 일치하는 기본 자료만 지우므로 회원 업로드에는 영향을 주지 않는다.
|
# 재시작 후에도 다시 등록되지 않도록 철회한 파일 이름을 정확히 지정한다.
|
||||||
placeholders = ",".join("?" for _ in LEGACY_SAMPLE_KEYS)
|
placeholders = ",".join("?" for _ in LEGACY_SAMPLE_KEYS)
|
||||||
connection.execute(
|
connection.execute(
|
||||||
f"DELETE FROM midi_items WHERE owner_id IS NULL AND seed_key IN ({placeholders})",
|
f"DELETE FROM midi_items WHERE owner_id IS NULL AND seed_key IN ({placeholders})",
|
||||||
|
|||||||
@@ -57,6 +57,18 @@ class LibraryDatabaseTest(unittest.TestCase):
|
|||||||
self.assertNotIn("오른손", sample["tags"])
|
self.assertNotIn("오른손", sample["tags"])
|
||||||
self.assertNotIn("쉬운 반주", sample["tags"])
|
self.assertNotIn("쉬운 반주", sample["tags"])
|
||||||
|
|
||||||
|
def test_withdrawn_practice_collection_is_not_seeded(self) -> None:
|
||||||
|
names = {f"practice-burgmuller-{number:02}.mid" for number in range(1, 19)}
|
||||||
|
names.update(f"practice-schumann-{number:02}.mid" for number in range(1, 13))
|
||||||
|
with self.db.connect() as connection:
|
||||||
|
rows = connection.execute(
|
||||||
|
"SELECT seed_key, visibility, min_tier, owner_id FROM midi_items WHERE seed_key IS NOT NULL"
|
||||||
|
).fetchall()
|
||||||
|
matching = {row["seed_key"]: row for row in rows if row["seed_key"] in names}
|
||||||
|
self.assertFalse(matching)
|
||||||
|
self.assertFalse(any(Path(self.temp.name, "midi", name).exists() for name in names))
|
||||||
|
self.assertFalse(any(row["seed_key"] in self.db.LEGACY_SAMPLE_KEYS for row in rows))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { fitHorizontalSheetScale } from "../components/SheetMusic/SheetMusicView";
|
||||||
|
|
||||||
|
describe("fitHorizontalSheetScale", () => {
|
||||||
|
it("악보 높이가 영역 안에 들어오면 현재 배율을 유지한다", () => {
|
||||||
|
expect(fitHorizontalSheetScale(0.62, 220, 240)).toBe(0.62);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("악보가 넘치면 아래 보표가 잘리지 않도록 배율을 낮춘다", () => {
|
||||||
|
expect(fitHorizontalSheetScale(0.62, 300, 240)).toBeCloseTo(0.4795, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("작은 화면에서도 최저 가독성 배율을 보장한다", () => {
|
||||||
|
expect(fitHorizontalSheetScale(0.62, 600, 180)).toBe(0.42);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,6 +16,30 @@ function sanitizeMusicXml(xml: string): string {
|
|||||||
return xml.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/\bon\w+\s*=/gi, "data-removed=");
|
return xml.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/\bon\w+\s*=/gi, "data-removed=");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HORIZONTAL_SHEET_BASE_SCALE = 0.62;
|
||||||
|
const HORIZONTAL_SHEET_MIN_SCALE = 0.42;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 화면의 악보는 곡마다 보표와 옥타브 표기의 높이가 달라진다.
|
||||||
|
* 고정 배율만 사용하면 복잡한 양손 악보의 아래 보표가 영역 밖으로 잘릴 수 있다.
|
||||||
|
* 실제 렌더링 높이가 가용 높이를 넘을 때만 배율을 낮추고 작은 화면에서도
|
||||||
|
* 음표를 알아볼 수 있도록 최저 배율을 보장한다.
|
||||||
|
*/
|
||||||
|
export function fitHorizontalSheetScale(
|
||||||
|
currentScale: number,
|
||||||
|
renderedHeight: number,
|
||||||
|
availableHeight: number,
|
||||||
|
): number {
|
||||||
|
if (renderedHeight <= 0 || availableHeight <= 0 || renderedHeight <= availableHeight) {
|
||||||
|
return currentScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safetyMargin = 8;
|
||||||
|
const usableHeight = Math.max(1, availableHeight - safetyMargin);
|
||||||
|
const fitted = currentScale * (usableHeight / renderedHeight);
|
||||||
|
return Math.max(HORIZONTAL_SHEET_MIN_SCALE, Math.min(currentScale, fitted));
|
||||||
|
}
|
||||||
|
|
||||||
export function SheetMusicView() {
|
export function SheetMusicView() {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null); // outer overflow-auto container
|
const scrollRef = useRef<HTMLDivElement>(null); // outer overflow-auto container
|
||||||
const containerRef = useRef<HTMLDivElement>(null); // inner AlphaTab host
|
const containerRef = useRef<HTMLDivElement>(null); // inner AlphaTab host
|
||||||
@@ -49,6 +73,7 @@ export function SheetMusicView() {
|
|||||||
// SheetMusicView mounts fresh per layout mode (not a singleton), so reading
|
// SheetMusicView mounts fresh per layout mode (not a singleton), so reading
|
||||||
// the mode once at init is safe for this instance's lifetime.
|
// the mode once at init is safe for this instance's lifetime.
|
||||||
const horizontal = useAppStore.getState().settings.layoutMode === "all";
|
const horizontal = useAppStore.getState().settings.layoutMode === "all";
|
||||||
|
let horizontalScale = HORIZONTAL_SHEET_BASE_SCALE;
|
||||||
|
|
||||||
const api = new AlphaTabApi(containerRef.current, {
|
const api = new AlphaTabApi(containerRef.current, {
|
||||||
core: {
|
core: {
|
||||||
@@ -70,7 +95,7 @@ export function SheetMusicView() {
|
|||||||
// Smaller scale in the horizontal "all" band so the full grand staff
|
// Smaller scale in the horizontal "all" band so the full grand staff
|
||||||
// (both clefs) fits the short band height without the bass clef being
|
// (both clefs) fits the short band height without the bass clef being
|
||||||
// clipped. Page mode keeps the larger, more readable 0.9.
|
// clipped. Page mode keeps the larger, more readable 0.9.
|
||||||
scale: horizontal ? 0.62 : 0.9,
|
scale: horizontal ? horizontalScale : 0.9,
|
||||||
// stretchForce 0 = notes keep their natural rhythmic proportions (like
|
// stretchForce 0 = notes keep their natural rhythmic proportions (like
|
||||||
// a printed score). A very small value (0.1) avoids a ragged right
|
// a printed score). A very small value (0.1) avoids a ragged right
|
||||||
// edge on the last system without visibly compressing the spacing.
|
// edge on the last system without visibly compressing the spacing.
|
||||||
@@ -97,6 +122,34 @@ export function SheetMusicView() {
|
|||||||
api.masterVolume = 0;
|
api.masterVolume = 0;
|
||||||
setApiReady(true);
|
setApiReady(true);
|
||||||
|
|
||||||
|
// 곡마다 보표와 8va·8vb 표기의 세로 높이가 달라진다. 렌더링이 끝난
|
||||||
|
// 뒤 실제 높이를 재서 악보 띠 안에 양손 보표가 모두 들어오도록 배율을
|
||||||
|
// 한 번 더 맞춘다. 계산 결과가 같으면 다시 렌더링하지 않아 반복을 막는다.
|
||||||
|
try {
|
||||||
|
api.postRenderFinished.on(() => {
|
||||||
|
if (!horizontal || disposed) return;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const scrollEl = scrollRef.current;
|
||||||
|
const scoreEl = containerRef.current;
|
||||||
|
if (!scrollEl || !scoreEl || disposed) return;
|
||||||
|
|
||||||
|
const nextScale = fitHorizontalSheetScale(
|
||||||
|
horizontalScale,
|
||||||
|
scoreEl.scrollHeight,
|
||||||
|
scrollEl.clientHeight - 16,
|
||||||
|
);
|
||||||
|
if (horizontalScale - nextScale < 0.01) return;
|
||||||
|
|
||||||
|
horizontalScale = nextScale;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(api.settings as any).display.scale = horizontalScale;
|
||||||
|
api.render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* 이전 AlphaTab 버전에서는 렌더 완료 이벤트가 없을 수 있다. */
|
||||||
|
}
|
||||||
|
|
||||||
// AlphaTab의 파일 해석과 렌더링 오류는 비동기로 전달된다. 빈 화면만
|
// AlphaTab의 파일 해석과 렌더링 오류는 비동기로 전달된다. 빈 화면만
|
||||||
// 남기지 않고 사용자가 원인을 알 수 있도록 화면에 오류를 표시한다.
|
// 남기지 않고 사용자가 원인을 알 수 있도록 화면에 오류를 표시한다.
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export function PracticeView() {
|
|||||||
<div
|
<div
|
||||||
key="sheet-band"
|
key="sheet-band"
|
||||||
className="flex flex-col min-h-0 overflow-hidden"
|
className="flex flex-col min-h-0 overflow-hidden"
|
||||||
style={{ flex: "0 0 32%", minHeight: 130 }}
|
style={{ flex: "0 0 38%", minHeight: "clamp(150px, 26vh, 220px)" }}
|
||||||
>
|
>
|
||||||
<SheetMusicView />
|
<SheetMusicView />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,3 +21,43 @@
|
|||||||
- 운영 백엔드에서 MIDI와 MusicXML과 MXL 및 PDF 검증을 통과했다.
|
- 운영 백엔드에서 MIDI와 MusicXML과 MXL 및 PDF 검증을 통과했다.
|
||||||
- 운영 브라우저에서 난이도별 묶음과 미뉴에트 및 엘리제를 위하여 악보 표시를 확인했다.
|
- 운영 브라우저에서 난이도별 묶음과 미뉴에트 및 엘리제를 위하여 악보 표시를 확인했다.
|
||||||
- 작업 전 백업은 /home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload에 저장했다.
|
- 작업 전 백업은 /home/syskiller/piano/backups/bach-to-basics/20261003-1545-sheet-upload에 저장했다.
|
||||||
|
- 전체 화면에서 악보 영역을 높이의 38%로 넓혔다. 최소 높이는 모바일 가로 화면에서 다른 연습 영역을 가리지 않도록 화면 높이에 따라 150px에서 220px 사이로 조정한다.
|
||||||
|
- 곡마다 다른 양손 보표와 8va 및 8vb 높이를 렌더링 뒤 측정한다. 영역을 넘으면 악보 배율을 자동으로 낮춰 아래 보표가 잘리지 않게 했다.
|
||||||
|
- 악보 자동 맞춤 계산의 정상 범위와 넘침 보정과 최저 배율을 검사하는 회귀 테스트 3개를 추가했다.
|
||||||
|
- 프런트 타입 검사와 전체 70개 테스트와 운영 빌드를 통과했다.
|
||||||
|
- 악보 영역 수정 전 백업은 /home/syskiller/piano/backups/bach-to-basics/20261003-1730-sheet-fit에 저장했다.
|
||||||
|
|
||||||
|
2026-10-04
|
||||||
|
|
||||||
|
- 피아노 학습 서비스의 최종 사용 프로그램을 Bach to Basics로 확정했다.
|
||||||
|
- Discourse 포럼 작업방에 서비스 소개와 사용법 및 곡 공유와 질문과 오류 신고 등의 게시 공간 구성을 요청했다.
|
||||||
|
- 현재 작업은 프로젝트 결정 기록만 변경했다. DB와 운영 컨테이너 및 이용자 데이터는 변경하지 않았다.
|
||||||
|
|
||||||
|
2026-10-09
|
||||||
|
|
||||||
|
- 초보자도 한 곡을 끝까지 연주할 수 있도록 약 64~76초 길이의 양손 MIDI 연습곡 5개를 직접 만들었다.
|
||||||
|
- 별빛 계단은 입문과 쉬움 두 버전으로 묶었다. 저녁빛 왈츠와 해돋이 아르페지오 및 검은 건반 산책도 공개 샘플로 등록했다.
|
||||||
|
- 분산 화음과 아르페지오 기법 참고 강좌의 제작자와 유튜브 주소를 샘플 출처 문서에 짧게 기록했다. 영상의 곡을 MIDI로 복제하지 않았다.
|
||||||
|
- 변경 파일은 backend/library_db.py와 backend/tools/generate_beginner_midis.py와 backend/sample_midi의 신규 파일 5개 및 검사 스크립트와 관련 문서다.
|
||||||
|
- MIDI 파서 검사에서 5개 파일의 길이와 박자 및 양손 트랙을 확인했다. 백엔드 단위 테스트 5개를 통과했다.
|
||||||
|
- 운영 백업은 /home/syskiller/piano/backups/bach-to-basics/20261009-beginner-midi 에 보관했다. 서버 시작 시 기본 공개 곡 5건이 DB에 추가됐고 회원 데이터 이전은 없었다.
|
||||||
|
- 백엔드 컨테이너만 갱신했다. HTTPS 첫 화면과 비회원 공개 목록 및 5개 파일 다운로드를 확인했다. 기존 회원 업로드 2개를 보존했고 프런트엔드는 재시작하지 않았다.
|
||||||
|
- 실제 USB MIDI 피아노를 연결한 손별 따라치기 검사는 남아 있다.
|
||||||
|
|
||||||
|
2026-10-09
|
||||||
|
|
||||||
|
- 선율이 비슷했던 기본 연습곡 5개를 공개 목록과 소스에서 교체했다. 회원이 업로드한 곡은 삭제하지 않았다.
|
||||||
|
- 부르크뮐러의 서로 다른 곡 18개와 슈만 어린이를 위한 앨범의 서로 다른 곡 12개를 양손 연습용으로 편곡했다. 각 곡의 첫 선율을 이용한 도입부와 마지막 선율을 이용한 마무리를 추가했다. 곡 길이는 69~134초다.
|
||||||
|
- 원본 MIDI와 RDF 및 곡별 권리 정보를 함께 보관했다. 슈만 MIDI의 CC BY-SA 2.5와 3.0 조건을 편곡에도 적용했다. 유튜브 쇼츠의 선율이나 MIDI를 복제하지 않았다.
|
||||||
|
- 변경 파일은 backend/library_db.py와 backend/tests/test_library_db.py와 backend/sample_midi 및 backend/tools/practice_sources와 scripts/build_practice_collection.mjs와 scripts/check_practice_collection.mjs 및 관련 안내 문서다.
|
||||||
|
- 새 MIDI 30개의 양손 트랙과 서로 다른 중심 선율과 도입부와 마무리와 재생 길이를 검사했다. 서버 격리 환경에서 백엔드 단위 테스트 5개를 통과했다.
|
||||||
|
- 운영 DB와 기존 MIDI 파일과 수정 대상 소스는 /home/syskiller/piano/backups/bach-to-basics/20261009-practice-30-intro-ending 에 백업했다. 백엔드 컨테이너만 다시 빌드했다.
|
||||||
|
- HTTPS 첫 화면과 비회원 목록에서 새 공개 곡 30개 및 각 MIDI 파일의 다운로드를 확인했다. 기존 회원 업로드 2건을 보존했다.
|
||||||
|
- 실제 건반을 연결해 곡별 연주감과 USB MIDI 입력을 확인하는 작업은 남아 있다.
|
||||||
|
|
||||||
|
2026-10-09
|
||||||
|
|
||||||
|
- 사용자 요청과 맞지 않는 부르크뮐러와 슈만 기본 연습곡 30개를 철회했다. 생성 MIDI와 원본 자료 및 생성·검사 스크립트를 제거했다.
|
||||||
|
- 기본 자료 제외 목록에 30곡 파일명을 정확히 지정해 서버 재시작 뒤에도 다시 등록되지 않게 했다. 회원 업로드와 기존 기본 샘플 7개는 유지했다.
|
||||||
|
- 철회 직전 DB와 MIDI 파일 및 원본 자료는 /home/syskiller/piano/backups/bach-to-basics/20261009-withdraw-practice-30 에 백업했다. 격리 환경 백엔드 테스트 5개를 통과했다. 운영 백엔드만 갱신했다.
|
||||||
|
- 재시작 뒤 HTTPS 비회원 목록에서 철회곡 0건을 확인했다. DB 철회곡 0건과 해당 MIDI 파일 0개 및 회원 업로드 2건과 기존 기본 샘플 7개를 확인했다.
|
||||||
|
|||||||
Reference in New Issue
Block a user