fix: preserve analysis bands across input workflows

This commit is contained in:
2026-08-29 08:05:15 +09:00
parent 122b6cf7be
commit 62971e07ec
10 changed files with 282 additions and 24 deletions
+5 -5
View File
@@ -34,24 +34,24 @@
<section class="workflow-card input-card" id="input-step" data-workflow-section="1">
<div class="step-heading">
<span class="step-number">1</span>
<div><h2>입력 준비</h2><p>기본 예제를 쓰거나 파일을 순서대로 교체하세요.</p></div>
<div><h2>입력 준비</h2><p>기본 예제를 쓰거나 파일을 교체하세요. Experiment부터 열어도 대역은 유지됩니다.</p></div>
</div>
<div class="workspace-tools input-file-grid" aria-label="입력 파일">
<label class="file-button file-choice" title="Scene JSON을 선택하세요."><span class="file-sequence">1</span><span><strong>Scene</strong><small>장면·음원</small></span><input id="scene-input" type="file" accept="application/json,.json" /></label>
<label class="file-button file-choice" title="사용자 배열이 있을 때 Array JSON을 선택하세요."><span class="file-sequence">2</span><span><strong>Array</strong><small>마이크 배열</small></span><input id="array-input" type="file" accept="application/json,.json" /></label>
<label class="file-button file-choice" title="현재 Scene과 Array를 참조하는 Experiment JSON을 선택하세요."><span class="file-sequence">3</span><span><strong>Experiment</strong><small>분석 설정</small></span><input id="experiment-input" type="file" accept="application/json,.json" /></label>
<label class="file-button file-choice" title="Experiment를 먼저 열 수 있습니다. 참조 ID가 다른 동안에는 실행만 비활성화됩니다."><span class="file-sequence">3</span><span><strong>Experiment</strong><small>분석 설정 · 선로드 가능</small></span><input id="experiment-input" type="file" accept="application/json,.json" /></label>
<label class="file-button file-choice" title="Scene에 사용한 원본 파일을 다시 선택하세요."><span class="file-sequence">4</span><span><strong>Assets</strong><small>오디오·이미지</small></span><input id="asset-input" type="file" accept="audio/*,image/*,.wav,.flac,.mp3,.ogg,.m4a,.png,.jpg,.jpeg,.webp,.svg" multiple /></label>
</div>
<div class="input-summary">
<div class="compact-section-heading"><h3>현재 장면</h3><a class="builder-link" href="/apps/dataset-builder/">Builder에서 편집 →</a></div>
<div class="compact-section-heading"><h3>현재 장면</h3><a class="builder-link" href="/apps/dataset-builder/" target="_blank" rel="noopener" title="현재 Camera의 Experiment를 유지한 채 새 탭에서 Scene을 편집합니다. 저장 후 이 Camera 탭으로 돌아와 Scene JSON을 여세요.">Builder 새 탭 ↗</a></div>
<div class="source-summary" id="source-summary"></div>
<div class="asset-list" id="asset-list"></div>
</div>
<div class="array-card">
<div class="array-heading"><div><span class="compact-label">MIC ARRAY</span><h3 id="array-title">배열</h3><a class="builder-link" href="/apps/array-builder/">Array Builder에서 만들기 →</a></div><strong id="microphone-count">— ch</strong></div>
<div class="array-heading"><div><span class="compact-label">MIC ARRAY</span><h3 id="array-title">배열</h3><a class="builder-link" href="/apps/array-builder/" target="_blank" rel="noopener" title="현재 Camera의 Experiment를 유지한 채 새 탭에서 배열을 편집합니다. 저장 후 이 Camera 탭으로 돌아와 Array JSON을 여세요.">Array Builder 새 탭 ↗</a></div><strong id="microphone-count">— ch</strong></div>
<label class="field">
<span>배열 선택</span>
<select id="array-preset">
@@ -200,7 +200,7 @@
</div>
<p id="live-status" aria-live="polite">trajectory가 있는 Scene을 불러오거나 이동 예제를 여세요.</p>
<div class="live-actions">
<button class="button button--ghost" id="load-moving-example-button" type="button">이동 예제 열기</button>
<button class="button button--ghost" id="load-moving-example-button" type="button" title="Scene·Array·Experiment와 분석 대역을 이동 예제 전체로 교체합니다.">이동 예제 전체 열기</button>
<button class="button button--primary" id="prepare-live-button" type="button" disabled>Map 준비</button>
<button class="button button--ghost" id="live-play-button" type="button" disabled>▶ 재생</button>
</div>
+34 -7
View File
@@ -981,7 +981,8 @@ function applyValidation(validation) {
if (!state.lastRun && document.documentElement.dataset.runState === "idle") {
setRunStatus(
`${state.arrayDefinition.microphones.length} ch · ` +
`${state.experiment.scan.columns}×${state.experiment.scan.rows} · 실행 준비 완료`,
`${state.experiment.scan.columns}×${state.experiment.scan.rows} · ` +
`분석 대역 ${state.experiment.frequencyBands.length}개 · 실행 준비 완료`,
);
}
}
@@ -1470,6 +1471,18 @@ function toggleLivePlayback() {
async function loadMovingExample() {
if (state.active) return;
const bandCount = state.experiment?.frequencyBands?.length ?? 0;
if (
(bandCount > 1 || hasUnsavedWork()) &&
!window.confirm(
"이동 예제는 현재 Scene·Array·Experiment 전체와 " +
String(bandCount) +
"개 분석 대역을 교체합니다. 계속할까요?",
)
) {
setRunStatus("이동 예제를 열지 않았습니다. 현재 입력과 분석 대역을 유지합니다.");
return;
}
beginInputOperation();
try {
setRunStatus("이동 음원 예제를 불러오고 있습니다…");
@@ -1494,7 +1507,7 @@ async function loadMovingExample() {
assetRuntime.reset();
markChanged({ syncControls: true, renderBands: true, dirty: "none" });
renderAssets();
setRunStatus("이동 음원 예제를 열었습니다. ‘라이브 준비’를 누르세요.");
setRunStatus("Scene·Array·Experiment를 이동 음원 예제 전체로 교체했습니다. ‘Map 준비’를 누르세요.");
} catch (error) {
setTransientError(error, "이동 예제 불러오기 실패");
} finally {
@@ -1561,10 +1574,10 @@ async function importDocument(kind, input) {
} else if (kind === "scene") {
issues = await validateScene(value);
} else {
issues = await validateExperiment(value, {
arrayDefinition: state.arrayDefinition,
scene: state.scene,
});
// An Experiment is a useful editable document even before its referenced
// Scene or Array has been loaded. Validate its own contract here, then let
// validateCurrent() keep Run disabled until the document IDs line up.
issues = await validateExperiment(value);
}
if (generation !== state.importGenerations[kind]) return;
if (issues.length > 0) throw new Error(issues.slice(0, 4).map(formatIssue).join(" · "));
@@ -1588,7 +1601,21 @@ async function importDocument(kind, input) {
dirty: kind === "experiment" ? "none" : "experiment",
});
renderAssets();
setRunStatus(`${file.name} ${kind} 문서를 불러왔습니다.`);
const bandStatus = kind === "scene"
? ` · 분석 대역 ${state.experiment.frequencyBands.length}개 유지`
: kind === "experiment"
? ` · 분석 대역 ${state.experiment.frequencyBands.length}개`
: "";
const pendingReferences = kind === "experiment"
? [
value.sceneId !== state.scene?.id ? `Scene ${value.sceneId}` : null,
value.arrayId !== state.arrayDefinition?.id ? `Array ${value.arrayId}` : null,
].filter(Boolean)
: [];
const referenceStatus = pendingReferences.length > 0
? ` · ${pendingReferences.join(" · ")} 연결 대기`
: "";
setRunStatus(`${file.name} ${kind} 문서를 불러왔습니다.${bandStatus}${referenceStatus}`);
} catch (error) {
setTransientError(error, `${file.name} 불러오기 실패`);
} finally {