Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- Keep UI and analysis engine decoupled through shared contracts.
- Prefer minimal, test-first changes for production code.
- Prefer practical, friendly, rehearsal-first wording over academic or authority-heavy language.
- Before a song is selected, customer-facing copy must name the next rehearsal action (use your own song). Do not leave the empty workspace as a text-only prompt.
- Do not reduce the product to a chord analyzer when form, timing, player coordination, simplification, and setup cues are the real rehearsal blockers.
- Do not frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy.

Expand Down
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Last updated: 2026-03-11

- Product identity, UX tone, copy rules, and prioritization tie-breakers live in `docs/brand-story.md`.
- Future PRDs, TRDs, onboarding copy, empty states, error messages, and marketing copy should use that document as the single brand source of truth.
- Before local or YouTube source admission, the workspace empty card names using a local song as the next action and does not invent a bundled demo.

## Security source

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Before a song is chosen, the workspace names Use my own song as the next rehearsal action and keeps a licensed demo as an honest later slice.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

`AGENTS.md` is the canonical agent operating guide — read and follow it before making changes. It defines the security workflow (`Security Notes`), supply-chain workflow, cross-platform build rules, GitHub bootstrap rules, code style, and safety guardrails. This file complements it with commands and architecture; when in doubt, `AGENTS.md` and the docs it references win.

Before a song is selected, the workspace empty card must name Use my own song as the next action rather than leaving a text-only prompt.

Agent execution and delegation rules live in `docs/agents/README.md`. PR canonicalization rules live in `docs/workflow/pr-continuity.md`.

## Common commands
Expand Down
92 changes: 92 additions & 0 deletions apps/desktop/src/App.localSelectionConcurrency.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { App } from "./App";

const analysisMocks = vi.hoisted(() => ({
getAnalysisJobStatus: vi.fn(),
importYoutubeUrl: vi.fn(),
isSupportedYoutubeUrl: vi.fn(() => false),
loadProject: vi.fn(),
saveProject: vi.fn(),
selectLocalAudioSource: vi.fn(),
startAnalysisJob: vi.fn(),
subscribeToAnalysisJobUpdates: vi.fn(async () => () => undefined),
}));

vi.mock("./lib/analysis", () => ({
createDefaultAnalysisRequest: () => ({
sourceKind: "demo",
sourceLabel: "Late Night Set",
roleFocus: ["bass-guitar", "keys-right", "lead-vocal"],
}),
getAnalysisJobStatus: analysisMocks.getAnalysisJobStatus,
importYoutubeUrl: analysisMocks.importYoutubeUrl,
isSupportedYoutubeUrl: analysisMocks.isSupportedYoutubeUrl,
loadProject: analysisMocks.loadProject,
MAX_YOUTUBE_URL_LENGTH: 2048,
saveProject: analysisMocks.saveProject,
selectLocalAudioSource: analysisMocks.selectLocalAudioSource,
startAnalysisJob: analysisMocks.startAnalysisJob,
subscribeToAnalysisJobUpdates: analysisMocks.subscribeToAnalysisJobUpdates,
}));

vi.mock("./features/score/ScoreView", () => ({
ScoreView: () => <div>Score view</div>,
}));

const selectedBootstrap = {
projectId: "project-local-intake",
sourceMode: "reference",
projectRoot: "/tmp/bandscope/projects/project-local-intake",
cacheRoot: "/tmp/bandscope/cache/project-local-intake",
tempRoot: "/tmp/bandscope/temp/project-local-intake",
source: {
sourcePath: "/Users/test/Music/selected-song.wav",
fileName: "selected-song.wav",
extension: "wav",
fileSizeBytes: 1024,
},
};
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated

/**
* Security Notes:
* - The selected path is a synthetic test fixture and is not asserted as buyer-visible copy.
* - This test mocks the existing local-picker boundary and adds no filesystem, network, or IPC authority.
*/
describe("App local song intake concurrency", () => {
beforeEach(() => {
for (const mock of Object.values(analysisMocks)) {
mock.mockReset();
}
analysisMocks.isSupportedYoutubeUrl.mockReturnValue(false);
analysisMocks.subscribeToAnalysisJobUpdates.mockResolvedValue(() => undefined);
});

it("allows only one local picker while the first selection is pending", async () => {
let resolveSelection: ((value: { ok: true; bootstrap: typeof selectedBootstrap }) => void) | undefined;
analysisMocks.selectLocalAudioSource.mockImplementation(
() =>
new Promise((resolve) => {
resolveSelection = resolve;
}),
);

render(<App />);

const emptyAction = screen.getByRole("button", { name: "Use my own song" });
const headerAction = screen.getByRole("button", { name: "Choose local audio" });
fireEvent.click(emptyAction);

await waitFor(() => {
expect(emptyAction).toBeDisabled();

Check failure on line 81 in apps/desktop/src/App.localSelectionConcurrency.test.tsx

View workflow job for this annotation

GitHub Actions / ci / build-and-test

src/App.localSelectionConcurrency.test.tsx > App local song intake concurrency > allows only one local picker while the first selection is pending

Error: expect(element).toBeDisabled() Received element is not disabled: <button aria-label="Use my own song" class="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 hover:bg-primary/80 disabled:hover:bg-primary aria-disabled:hover:bg-primary h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 mt-8 min-h-11 bg-gradient-to-r from-cyan-400 to-violet-500 font-black text-slate-950 shadow-[0_14px_38px_rgba(34,211,238,0.28)] hover:from-cyan-300 hover:to-violet-400" data-slot="button" tabindex="0" type="button" /> Ignored nodes: comments, script, style <html> <head /> <body> <div> <div class="min-h-screen overflow-x-hidden bg-[var(--bandscope-bg)] text-slate-100 selection:bg-cyan-300/30" > <div class="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_18%_8%,rgba(15,120,255,0.22),transparent_28%),radial-gradient(circle_at_78%_0%,rgba(124,58,237,0.20),transparent_30%),linear-gradient(180deg,#07111f_0%,#020713_55%,#020611_100%)]" /> <div class="pointer-events-none fixed inset-0 opacity-[0.06] [background-image:linear-gradient(rgba(255,255,255,.8)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.8)_1px,transparent_1px)] [background-size:46px_46px]" /> <div class="relative flex min-h-screen" > <aside class="hidden w-64 shrink-0 border-r border-[color:var(--bandscope-border)] bg-[var(--bandscope-surface-strong)] px-5 py-5 shadow-[24px_0_80px_rgba(0,0,0,0.28)] backdrop-blur-2xl lg:flex lg:flex-col" > <div aria-hidden="true" class="mb-7 flex items-center gap-2" > <span class="size-3 rounded-full bg-red-400" /> <span class="size-3 rounded-full bg-amber-300" /> <span class="size-3 rounded-full bg-emerald-400" /> </div> <div class="mb-9 flex items-center gap-3" > <span aria-label="BandScope circular equalizer mark" class="relative grid size-11 shrink-0 place-items-center rounded-full border border-cyan-200/45 bg-cyan-200/10 shadow-[0_0_28px_rgba(103,232,249,0.34)]" role="img" > <span aria-hidden="true" class="absolute inset-1 rounded-full border border-teal-200/20" /> <span aria-hidden="true" class="flex h-8 items-end gap-0.5" > <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-3" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-5" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-7" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-4" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-6" /> </span>

Check failure on line 81 in apps/desktop/src/App.localSelectionConcurrency.test.tsx

View workflow job for this annotation

GitHub Actions / release-preflight

src/App.localSelectionConcurrency.test.tsx > App local song intake concurrency > allows only one local picker while the first selection is pending

Error: expect(element).toBeDisabled() Received element is not disabled: <button aria-label="Use my own song" class="group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 hover:bg-primary/80 disabled:hover:bg-primary aria-disabled:hover:bg-primary h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 mt-8 min-h-11 bg-gradient-to-r from-cyan-400 to-violet-500 font-black text-slate-950 shadow-[0_14px_38px_rgba(34,211,238,0.28)] hover:from-cyan-300 hover:to-violet-400" data-slot="button" tabindex="0" type="button" /> Ignored nodes: comments, script, style <html> <head /> <body> <div> <div class="min-h-screen overflow-x-hidden bg-[var(--bandscope-bg)] text-slate-100 selection:bg-cyan-300/30" > <div class="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_18%_8%,rgba(15,120,255,0.22),transparent_28%),radial-gradient(circle_at_78%_0%,rgba(124,58,237,0.20),transparent_30%),linear-gradient(180deg,#07111f_0%,#020713_55%,#020611_100%)]" /> <div class="pointer-events-none fixed inset-0 opacity-[0.06] [background-image:linear-gradient(rgba(255,255,255,.8)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,.8)_1px,transparent_1px)] [background-size:46px_46px]" /> <div class="relative flex min-h-screen" > <aside class="hidden w-64 shrink-0 border-r border-[color:var(--bandscope-border)] bg-[var(--bandscope-surface-strong)] px-5 py-5 shadow-[24px_0_80px_rgba(0,0,0,0.28)] backdrop-blur-2xl lg:flex lg:flex-col" > <div aria-hidden="true" class="mb-7 flex items-center gap-2" > <span class="size-3 rounded-full bg-red-400" /> <span class="size-3 rounded-full bg-amber-300" /> <span class="size-3 rounded-full bg-emerald-400" /> </div> <div class="mb-9 flex items-center gap-3" > <span aria-label="BandScope circular equalizer mark" class="relative grid size-11 shrink-0 place-items-center rounded-full border border-cyan-200/45 bg-cyan-200/10 shadow-[0_0_28px_rgba(103,232,249,0.34)]" role="img" > <span aria-hidden="true" class="absolute inset-1 rounded-full border border-teal-200/20" /> <span aria-hidden="true" class="flex h-8 items-end gap-0.5" > <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-3" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-5" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-7" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-4" /> <span class="w-1.5 rounded-full bg-gradient-to-t from-teal-300 via-cyan-200 to-violet-300 h-6" /> </span>
expect(headerAction).toBeDisabled();
});

fireEvent.click(emptyAction);
fireEvent.click(headerAction);
expect(analysisMocks.selectLocalAudioSource).toHaveBeenCalledTimes(1);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

resolveSelection?.({ ok: true, bootstrap: selectedBootstrap });
await waitFor(() => expect(screen.getByText("selected-song.wav")).toBeTruthy());
});
});
20 changes: 20 additions & 0 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,25 @@ describe("App", () => {
expect(screen.getByText(/YouTube only leaves the app when you choose import/i)).toBeTruthy();
});

it("names using a local song as the empty-workspace next action", () => {
render(<App />);

expect(screen.getByRole("heading", { name: "Start tonight's rehearsal" })).toBeTruthy();
expect(screen.getByRole("button", { name: "Use my own song" })).toBeTruthy();
expect(screen.getByText(/licensed demo is not bundled yet/i)).toBeTruthy();
});

it("starts local file intake from the empty-workspace next action", async () => {
tauriInvoke.mockResolvedValueOnce(bootstrapResponse());
render(<App />);

fireEvent.click(screen.getByRole("button", { name: "Use my own song" }));

await waitFor(() => {
expect(screen.getByText(/late-night-set\.wav/i)).toBeTruthy();
});
});

it("renders localized Korean shell copy for buyer-demo surfaces", () => {
const languageSpy = vi.spyOn(window.navigator, "language", "get").mockReturnValue("ko-KR");

Expand All @@ -260,6 +279,7 @@ describe("App", () => {
expect(screen.getByRole("button", { name: /^작업 공간$/i })).toBeTruthy();
expect(screen.getByRole("button", { name: /프로젝트 열기/i })).toBeTruthy();
expect(screen.getByRole("button", { name: /유튜브 가져오기/i })).toBeTruthy();
expect(screen.getByRole("button", { name: "내 곡 사용하기" })).toBeTruthy();
expect(screen.getByText(/로컬 우선/i)).toBeTruthy();
expect(screen.getByText(/합주 지도는 이 기기에 머뭅니다/i)).toBeTruthy();
expect(screen.getByText(/^템포$/i)).toBeTruthy();
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/App.tsx
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ export function App() {
if (jobResult) {
return <Workspace song={jobResult} sourceBootstrap={jobResultBootstrap} onSongUpdate={handleSongUpdate} />;
}
return <EmptyState />;
return <EmptyState onUseOwnSong={() => { void handleChooseLocalAudio(); }} chooseDisabled={analysisInFlight || isStarting || isImporting} />;
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
};

const currentView: RehearsalView = jobResult && activeView === "score" ? "score" : "workspace";
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/features/workspace/Workspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,10 @@ describe("Workspace", () => {

it("localizes empty and loading state titles", () => {
setNavigatorLanguage("ko-KR");
render(<EmptyState />);
render(<EmptyState onUseOwnSong={vi.fn()} />);
render(<LoadingState />);

expect(screen.getByRole("heading", { name: "분석 준비 완료" })).toBeTruthy();
expect(screen.getByRole("heading", { name: "오늘 합주를 시작하세요" })).toBeTruthy();
expect(screen.getByRole("heading", { name: "오디오 분석 중" })).toBeTruthy();
});

Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/features/workspace/WorkspaceStates.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { EmptyState, LoadingState } from "./WorkspaceStates";

const originalLanguage = window.navigator.language;

function setNavigatorLanguage(language: string) {
Object.defineProperty(window.navigator, "language", {
configurable: true,
value: language
});
}

describe("WorkspaceStates empty first-run card", () => {
afterEach(() => {
setNavigatorLanguage(originalLanguage);
});

it("names using a local song as the next rehearsal action", () => {
const onUseOwnSong = vi.fn();
render(<EmptyState onUseOwnSong={onUseOwnSong} />);

fireEvent.click(screen.getByRole("button", { name: "Use my own song" }));

expect(onUseOwnSong).toHaveBeenCalledTimes(1);
expect(screen.getByRole("heading", { name: "Start tonight's rehearsal" })).toBeTruthy();
expect(screen.getByText(/keeps the file local/i)).toBeTruthy();
expect(screen.queryByRole("button", { name: /try the demo/i })).toBeNull();
});

it("disables the next action while intake is already running", () => {
const onUseOwnSong = vi.fn();
render(<EmptyState onUseOwnSong={onUseOwnSong} chooseDisabled />);

expect(screen.getByRole("button", { name: "Use my own song" })).toBeDisabled();
});

it("localizes the empty next-action copy", () => {
setNavigatorLanguage("ko-KR");
render(
<>
<EmptyState onUseOwnSong={vi.fn()} />
<LoadingState />
</>
);

expect(screen.getByRole("heading", { name: "오늘 합주를 시작하세요" })).toBeTruthy();
expect(screen.getByRole("button", { name: "내 곡 사용하기" })).toBeTruthy();
expect(screen.getByRole("heading", { name: "오디오 분석 중" })).toBeTruthy();
});
});
29 changes: 24 additions & 5 deletions apps/desktop/src/features/workspace/WorkspaceStates.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import { createTranslator, detectPreferredLocale } from "../../i18n";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Loader2, Music, AlertCircle } from "lucide-react";
import { Loader2, Music, AlertCircle, Upload } from "lucide-react";

/** Documented. */
export function EmptyState() {
/** Next-action handlers for the empty rehearsal workspace. */
export interface EmptyStateProps {
onUseOwnSong: () => void;
chooseDisabled?: boolean;
}

/** Render the empty card that names choosing a local song as the next action. */
export function EmptyState({ onUseOwnSong, chooseDisabled = false }: EmptyStateProps) {
const t = createTranslator(detectPreferredLocale());
return (
<Card className="border-2 border-dashed border-cyan-300/20 bg-slate-950/50 shadow-[0_18px_70px_rgba(0,0,0,0.22)] backdrop-blur-xl">
<CardContent className="flex flex-col items-center justify-center py-24 text-center">
<div className="mb-6 rounded-full border border-cyan-300/30 bg-cyan-300/10 p-6 text-cyan-200 shadow-[0_0_38px_rgba(34,211,238,0.18)]">
<Music className="size-10" aria-hidden="true" />
</div>
<h3 className="mb-2 text-xl font-black text-white">{t("workspaceReadyToAnalyzeTitle")}</h3>
<p className="max-w-sm text-slate-400">{t("workspaceEmptyState")}</p>
<h3 className="mb-2 text-xl font-black text-white">{t("firstRunEmptyTitle")}</h3>
<p className="max-w-md text-slate-400">{t("firstRunEmptyGuidance")}</p>
<p className="mt-3 max-w-md text-sm text-slate-500">{t("firstRunEmptyLocalFirst")}</p>
<Button
type="button"
onClick={onUseOwnSong}
disabled={chooseDisabled}
className="mt-8 min-h-11 bg-gradient-to-r from-cyan-400 to-violet-500 font-black text-slate-950 shadow-[0_14px_38px_rgba(34,211,238,0.28)] hover:from-cyan-300 hover:to-violet-400"
aria-label={t("firstRunUseOwnSong")}
>
<Upload className="mr-2 size-4" aria-hidden="true" />
{t("firstRunUseOwnSong")}
</Button>
<p className="mt-4 max-w-md text-sm text-slate-500">{t("firstRunEmptyDemoNote")}</p>
Comment thread
seonghobae marked this conversation as resolved.
</CardContent>
</Card>
);
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"workspaceReadyToAnalyzeTitle": "Ready to Analyze",
"workspaceAnalyzingAudioTitle": "Analyzing Audio",
"workspaceEmptyState": "Choose an audio file to prepare for your rehearsal.",
Comment thread
github-actions[bot] marked this conversation as resolved.
Outdated
"firstRunEmptyTitle": "Start tonight's rehearsal",
"firstRunEmptyGuidance": "Choose a song on this device. BandScope keeps the file local and will not invent a rehearsal map until you analyze.",
"firstRunEmptyLocalFirst": "Your song stays on this device.",
"firstRunUseOwnSong": "Use my own song",
"firstRunEmptyDemoNote": "A licensed demo is not bundled yet. Use your own song to start tonight.",
"workspaceLoadingState": "Analyzing the song's form and instrument roles...",
"workspaceErrorState": "An error occurred during analysis. Please try again.",
"workspaceRehearsalMapLabel": "Tonight's rehearsal map",
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"workspaceReadyToAnalyzeTitle": "분석 준비 완료",
"workspaceAnalyzingAudioTitle": "오디오 분석 중",
"workspaceEmptyState": "합주할 곡의 오디오 파일을 선택해주세요.",
"firstRunEmptyTitle": "오늘 합주를 시작하세요",
"firstRunEmptyGuidance": "이 기기의 곡을 선택하세요. 파일은 기기에만 남고, 분석이 끝나기 전에 BandScope는 합주 지도를 만들지 않습니다.",
"firstRunEmptyLocalFirst": "선택한 곡은 이 기기에만 남습니다.",
"firstRunUseOwnSong": "내 곡 사용하기",
"firstRunEmptyDemoNote": "라이선스가 확인된 데모 곡은 아직 포함되지 않았습니다. 오늘 합주를 시작하려면 내 곡을 사용하세요.",
"workspaceLoadingState": "곡의 폼과 악기별 역할을 분석하고 있습니다...",
"workspaceErrorState": "분석 중 오류가 발생했습니다. 다시 시도해주세요.",
"workspaceRehearsalMapLabel": "오늘의 합주 지도",
Expand Down
Loading