From 15ef7c65e9f58637e4b4714afb9546c176ceaaa8 Mon Sep 17 00:00:00 2001 From: kyeongmin Date: Thu, 21 May 2026 14:56:56 +0900 Subject: [PATCH] fix(#75): IMP-47A mdx03 frontend execution stabilization (u1~u4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit u1: SlideCanvas iframe sandbox += allow-scripts (allow-same-origin preserved) → embedded-mode script in slide_base.html now applies html.embedded → standalone CSS reset deactivates inside iframe; no clipping u2: designAgentApi.loadRun merges candidate_evidence + v4_all_judgments + v4_candidates via Map dedup, LABEL_PRIORITY (use_as_is --- Front/client/src/components/SlideCanvas.tsx | 2 +- Front/client/src/pages/Home.tsx | 2 +- Front/client/src/services/designAgentApi.ts | 17 ++-- tests/manual/imp47a_e2e.md | 90 +++++++++++++++++++++ 4 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 tests/manual/imp47a_e2e.md diff --git a/Front/client/src/components/SlideCanvas.tsx b/Front/client/src/components/SlideCanvas.tsx index f1db0cd..f9412ac 100644 --- a/Front/client/src/components/SlideCanvas.tsx +++ b/Front/client/src/components/SlideCanvas.tsx @@ -293,7 +293,7 @@ export default function SlideCanvas({ title="Phase Z 렌더 결과" className="w-full h-full border-0 block" scrolling="no" - sandbox="allow-same-origin" + sandbox="allow-same-origin allow-scripts" style={{ pointerEvents: isEditMode ? "auto" : "none" }} onLoad={(e) => { // IMP-14 (Step 13 A-4) — embedded vs standalone CSS reset 은 backend diff --git a/Front/client/src/pages/Home.tsx b/Front/client/src/pages/Home.tsx index ea77145..00fa847 100644 --- a/Front/client/src/pages/Home.tsx +++ b/Front/client/src/pages/Home.tsx @@ -377,7 +377,7 @@ export default function Home() { ); setState((p) => ({ ...p, isLoading: false })); } - }, [state.uploadedFile]); + }, [state.uploadedFile, state.slidePlan, state.userSelection, pendingZones, pendingLayout]); // ── 섹션 드래그 앤 드롭 (Zone으로 재배치) ── const handleSectionDrop = useCallback((sectionId: string, zoneId: string) => { diff --git a/Front/client/src/services/designAgentApi.ts b/Front/client/src/services/designAgentApi.ts index c507f91..5db5ffa 100644 --- a/Front/client/src/services/designAgentApi.ts +++ b/Front/client/src/services/designAgentApi.ts @@ -511,12 +511,17 @@ export async function loadRun(runId: string): Promise { const candidateEvidence = Array.isArray(unit.candidate_evidence) ? unit.candidate_evidence : []; - const rawSource = - candidateEvidence.length > 0 - ? candidateEvidence - : (unit.v4_all_judgments?.length > 0 - ? unit.v4_all_judgments - : (unit.v4_candidates ?? [])); + const candidateMap = new Map(); + const pushCandidate = (c: any) => { + if (!c) return; + const key = c.template_id ?? c.id ?? c.frame_id; + if (!key) return; + if (!candidateMap.has(key)) candidateMap.set(key, c); + }; + candidateEvidence.forEach(pushCandidate); + (unit.v4_all_judgments ?? []).forEach(pushCandidate); + (unit.v4_candidates ?? []).forEach(pushCandidate); + const rawSource = Array.from(candidateMap.values()); const v4Source = [...rawSource].sort((a: any, b: any) => { const lp = (LABEL_PRIORITY[a.label] ?? 99) - (LABEL_PRIORITY[b.label] ?? 99); if (lp !== 0) return lp; diff --git a/tests/manual/imp47a_e2e.md b/tests/manual/imp47a_e2e.md new file mode 100644 index 0000000..727d587 --- /dev/null +++ b/tests/manual/imp47a_e2e.md @@ -0,0 +1,90 @@ +# IMP-47A — mdx03 frontend stabilization manual e2e + +Scope: frontend-only. Backend pipeline must NOT be modified during this test. +Path under test: `mdx=03` (default sample loaded on page open). + +## Preconditions + +- Backend running on `http://localhost:8001` (`uvicorn src.main:app --port 8001`). +- Frontend dev server running (`cd Front && npm run dev`). +- Working tree at IMP-47A Stage 3 HEAD (u1+u2+u3 applied to `Front/client/src/components/SlideCanvas.tsx`, `Front/client/src/services/designAgentApi.ts`, `Front/client/src/pages/Home.tsx`). +- Browser opens `http://localhost:5173/?mdx=03` (or default route, which auto-loads `mdx=03`). + +## Section 1 — iframe rendering (axis 1) + +Goal: verify u1 sandbox change lets `slide_base.html` script apply `html.embedded` class so the slide is not clipped inside the iframe. + +Steps: +1. Open the app; wait for `mdx=03` auto-load and initial `final.html` render. +2. Click the "슬라이드 플랜 생성하기" button; wait for `run "" 완료` toast. +3. Open browser DevTools → Elements; locate the iframe inside `SlideCanvas`; switch context to the iframe document. +4. Confirm `` is present (not just ``). +5. Confirm the rendered slide content fills the 1280×720 frame with no top padding offset and no clipping at the bottom. + +Pass: `html.embedded` class present AND no visible vertical shift/clipping. +Fail signal: iframe content pushed downward, footer cut off, or `html` lacks `embedded` class (means script never ran → sandbox regression). + +## Section 2 — multi-source frame candidates (axis 2) + +Goal: verify u2 3-source merge surfaces candidates from `candidate_evidence`, `v4_all_judgments`, and `v4_candidates` with deterministic dedup and cap. + +Steps: +1. After Section 1 success, click any zone in the canvas; right panel switches to the "frame" tab. +2. In the frame candidate list, count visible candidates. Expect ≤ `TOP_N_FRAMES` (=6). +3. Open DevTools → Network → reload `/api/run/`; inspect the JSON response and confirm at least two of `candidate_evidence`, `v4_all_judgments`, `v4_candidates` are non-empty. +4. Cross-check: union of `template_id ?? id ?? frame_id` keys from all three arrays (deduped, capped at 6) equals the UI list count and order. +5. Confirm the order respects LABEL_PRIORITY (`use_as_is` < `light_edit` < `restructure` < `reject`) then descending confidence. + +Pass: union/dedup/cap/order all match. +Fail signal: only candidates from a single source visible, duplicates by template_id, more than 6 items, or order violates LABEL_PRIORITY. + +## Section 3 — frame / layout override regeneration (axis 3) + +Goal: verify u3 5-dep `handleGenerate` callback delivers the latest override state to backend (no stale closure). + +Steps: +1. From Section 2, pick a non-default frame candidate (one whose label is not `use_as_is`); click "이 프레임 적용". +2. Confirm bottom-left button transforms to "선택대로 재생성하기" with amber pulse dot (hasPendingChanges = true). +3. Without any extra clicks, click "선택대로 재생성하기". +4. Open DevTools → Network → inspect the POST `/api/pipeline` body; confirm `overrides.frames` contains the chosen `{unit_id: frame_id}` mapping. +5. After success toast, confirm new `run_id` differs from the previous run, and the rendered iframe reflects the chosen frame (frame DOM class / id matches selection). +6. Repeat with a layout-card "적용하기" → pending overlay enters → "선택대로 재생성하기"; confirm POST body includes `overrides.layout` with the chosen preset id. + +Pass: every override (frames / layout / zoneSections / zoneGeometries when applicable) reaches backend on first click; new `run_id` returned. +Fail signal: POST body lacks the override, or backend re-renders with the previous selection (stale closure regression). + +## Section 4 — pending overlay enter / cancel / clear (axis 4) + +Goal: verify pendingLayout overlay enters on "적용하기", exits on "취소", and auto-clears on successful regenerate. + +Steps: +1. Click any non-current layout card "적용하기" button. +2. Confirm amber dashed overlay appears over `.slide-body` area with `PENDING BODY LAYOUT` label and chosen layout id. +3. Confirm "취소" button (top-right of canvas) is visible. +4. Click "취소"; confirm overlay disappears, `userSelection` resets, and `hasPendingChanges` indicator clears. +5. Re-enter pending mode (apply a layout again), this time click "선택대로 재생성하기"; confirm overlay disappears on success (Home.tsx clears `pendingLayout` + `hasPendingChanges` before pipeline call) and the new `final.html` renders inline (no overlay). + +Pass: enter → cancel → re-enter → regenerate cycle leaves no overlay, no stuck pending state, no leftover `hasPendingChanges` flag. +Fail signal: overlay persists after regenerate, button stays in amber state, or cancel does not restore the canvas. + +## Section 5 — mdx03 end-to-end pass (axis 5) + +Goal: smoke run combining Sections 1–4 in one session to validate mdx03 demo path. + +Steps: +1. Fresh reload `http://localhost:5173/?mdx=03`. +2. Click "슬라이드 플랜 생성하기" → wait for run completion → confirm iframe renders cleanly (Section 1 pass). +3. Click any zone → inspect 2+ candidates in frame list (Section 2 pass). +4. Apply a non-default frame → "선택대로 재생성하기" → new run id + iframe reflects override (Section 3 pass). +5. Apply a non-default layout → confirm overlay → "선택대로 재생성하기" → overlay clears (Section 4 pass). +6. Capture: run_id chain, final.html path under `data/runs//final.html`, and DOM screenshot of the rendered iframe content. + +Pass: all five sections green, no console errors, no toast errors, three distinct `run_id`s produced across the session. +Fail signal: any earlier section regression OR backend pipeline failure (out-of-scope for IMP-47A — log separately). + +## Out of scope (not tested here) + +- AI fallback activation in `Step 12` (`light_edit` / `restructure`) — IMP-47B. +- Frame cache (#62 / IMP-46). +- mdx04 / mdx05 path-specific axes. +- Automated Playwright e2e replacement — future work.