diff --git a/Front/client/src/components/SlideCanvas.tsx b/Front/client/src/components/SlideCanvas.tsx index 2b61e0e..10fed0e 100644 --- a/Front/client/src/components/SlideCanvas.tsx +++ b/Front/client/src/components/SlideCanvas.tsx @@ -1,17 +1,17 @@ -/** - * SlideCanvas ??Phase Z ?ㅼ젣 寃곌낵 (final.html) iframe ?쒖떆. +/** + * SlideCanvas — Phase Z 실제 결과 (final.html) iframe 표시. * - * ??븷 (Phase 1 ??MDX upload ???뚯씠?꾨씪???ㅽ뻾 ??寃곌낵 ?쒖떆): - * - MDX ?낅줈????server 媛€ Phase Z ?ㅽ뻾 - * - run_id ??final.html ??iframe ?쇰줈 ?쒖떆 - * - ?쒕??덉씠??/ mock ?щ씪?대뱶 layout ?뚮뜑 X + * 역할 (Phase 1 — MDX upload → 파이프라인 실행 → 결과 표시): + * - MDX 업로드 후 server 가 Phase Z 실행 + * - run_id 의 final.html 을 iframe 으로 표시 + * - 시뮬레이션 / mock 슬라이드 layout 렌더 X * - * ?댁쟾 simulation (FocusSplitLayout / QuadrantGridLayout / DEFAULT_GEOMETRIES / - * Phase Z Engine header / Summary footer) 紐⑤몢 ?쒓굅 ??frontend 媛€ ?щ씪?대뱶瑜?*留뚮뱾吏€ ?딆쓬*, - * backend 寃곌낵瑜?*?쎄린留???. + * 이전 simulation (FocusSplitLayout / QuadrantGridLayout / DEFAULT_GEOMETRIES / + * Phase Z Engine header / Summary footer) 모두 제거 — frontend 가 슬라이드를 *만들지 않음*, + * backend 결과를 *읽기만 함*. * - * Layout / zone / region / frame 蹂€寃?UI ??醫뚯슦 ?⑤꼸 (LayoutPanel / FramePanel) - * ?먯꽌 ?섑뻾. 蹂?而댄룷?뚰듃??*寃곌낵 viewer*. + * Layout / zone / region / frame 변경 UI 는 좌우 패널 (LayoutPanel / FramePanel) + * 에서 수행. 본 컴포넌트는 *결과 viewer*. */ import { useRef, useEffect, useState } from "react"; @@ -39,19 +39,19 @@ interface SlideCanvasProps { slidePlan: SlidePlan | null; normalizedContent: NormalizedContent | null; userSelection: UserSelection; - /** Phase Z 媛€ 留뚮뱺 final.html URL (iframe ?쇰줈 ?쒖떆). */ + /** Phase Z 가 만든 final.html URL (iframe 으로 표시). */ finalHtmlUrl?: string; - /** ?뚯씠?꾨씪???ㅽ뻾 以??쒖떆 (loading state). */ + /** 파이프라인 실행 중 표시 (loading state). */ isPipelineRunning?: boolean; - /** Phase 2 : pending layout 紐⑤뱶 ??final.html iframe ?④린怨?鍮?layout zone 留??쒖떆. */ + /** Phase 2 : pending layout 모드 — final.html iframe 숨기고 빈 layout zone 만 표시. */ isPendingLayout?: boolean; pendingLayoutId?: string | null; onCancelPendingLayout?: () => void; - /** ?몄쭛 紐⑤뱶 ?띿뒪??蹂€寃?諛쒖깮 ????Home.tsx 媛€ hasPendingChanges ?몃━嫄? */ + /** 편집 모드 텍스트 변경 발생 시 — Home.tsx 가 hasPendingChanges 트리거. */ onContentEdit?: () => void; - /** Zone overlay ?대┃ ???곗륫 Frame ??쑝濡??꾪솚 + 洹?zone ??frame ?꾨낫 ?쒖떆. */ + /** Zone overlay 클릭 — 우측 Frame 탭으로 전환 + 그 zone 의 frame 후보 표시. */ onZoneClick?: (zoneId: string) => void; - /** ?щ씪?대뱶 諛뺤뒪 (zone overlay ??鍮??곸뿭) ?대┃ ???곗륫 Layout ??쑝濡??꾪솚. */ + /** 슬라이드 박스 (zone overlay 외 빈 영역) 클릭 — 우측 Layout 탭으로 전환. */ onSlideClick?: () => void; onRegionClick?: (regionId: string) => void; onFrameSelect?: (frameId: string) => void; @@ -61,7 +61,7 @@ interface SlideCanvasProps { geometries: Record ) => void; /** IMP-51 (#79) u8 ??persisted slide-absolute image geometries - * (image_id ??{x,y,w,h} as percent of 1280횞720, range 0??00). Mirrors + * (image_id ??{x,y,w,h} as percent of 1280×720, range 0??00). Mirrors * the u3 typed-client `ImageOverride` contract and the u7 stamper that * emits CSS `left/top/width/height: {value}%`. Forward-compat optional; * u11 wires this from `userSelection.overrides.image_overrides`. When @@ -70,7 +70,7 @@ interface SlideCanvasProps { imageOverrides?: ImageOverridesOverride; /** IMP-51 (#79) u8 ??emitted when the user drags or resizes a stamped * user-content image. Geometry is slide-absolute percent (0??00 of - * 1280횞720), matching the persisted axis schema (u3 typed client) and + * 1280×720), matching the persisted axis schema (u3 typed client) and * the u7 CSS injection that writes the values directly into * `left/top/width/height: {value}%`. u10 wires this to a persistence * handler that updates `image_overrides` on user_overrides.json. */ @@ -205,26 +205,26 @@ export default function SlideCanvas({ }: SlideCanvasProps) { const containerRef = useRef(null); const [scale, setScale] = useState(1); - // iframe ??final.html ???ㅼ젣 .zone DOM boundingClientRect 痢≪젙媛?(?뺢퇋??0~1). - // key = data-zone-position (e.g., "top" / "bottom" / "primary"). ?닿쾶 SlidePlan - // ??zone.zone_id ?€ 1:1 ?쇱튂?섎?濡?overlay rendering ??留ㅼ묶 媛€?? + // iframe 안 final.html 의 실제 .zone DOM boundingClientRect 측정값 (정규화 0~1). + // key = data-zone-position (e.g., "top" / "bottom" / "primary"). 이게 SlidePlan + // 의 zone.zone_id 와 1:1 일치하므로 overlay rendering 시 매칭 가능. const [measuredZones, setMeasuredZones] = useState< Record >({}); - // iframe ??.slide-body ??bbox (?뺢퇋??0~1, 1280횞720 湲곗?). slide-base (title / - // divider / footer) ??洹??몃?. pendingLayout 紐⑤뱶 ?????곸뿭 ?덉뿉留?鍮?layout - // overlay 瑜?源붿븘??slide-base ??洹몃?濡??좎?. + // iframe 안 .slide-body 의 bbox (정규화 0~1, 1280×720 기준). slide-base (title / + // divider / footer) 는 그 외부. pendingLayout 모드 시 이 영역 안에만 빈 layout + // overlay 를 깔아서 slide-base 는 그대로 유지. const [measuredSlideBody, setMeasuredSlideBody] = useState<{ x: number; y: number; w: number; h: number; } | null>(null); - // Step B : section drag-drop drop target. ?ъ슜?먭? LeftMdxPanel ??section 移대뱶 - // 瑜?drag ?댁꽌 zone ??drop ??洹?zone ??section ?좊떦. dragOver ??媛뺤“ ?쒖떆. + // Step B : section drag-drop drop target. 사용자가 LeftMdxPanel 의 section 카드 + // 를 drag 해서 zone 에 drop 시 그 zone 에 section 할당. dragOver 시 강조 표시. const [dragOverZoneId, setDragOverZoneId] = useState(null); // IMP-51 (#79) u8 ??measured user-content image bboxes inside iframe - // (slide-absolute percent of 1280횞720, range 0??00). key = data-image-id + // (slide-absolute percent of 1280×720, range 0??00). key = data-image-id // stamped by u4 (`src/image_id_stamper.py`). Populated in the iframe // onLoad measure block alongside measuredZones / measuredSlideBody. // Units intentionally match the persisted `image_overrides` axis (u3 @@ -241,9 +241,9 @@ export default function SlideCanvas({ // Reset on finalHtmlUrl change and on edit-mode exit so stale ids never // leak across runs. const [selectedImageId, setSelectedImageId] = useState(null); - // HTML ?몄쭛 紐⑤뱶 ??湲€踰??⑦꽩 (designMode + contentEditable + outline CSS) 李⑥슜. - // ?쒖꽦 ??iframe ???띿뒪??element 吏곸젒 ?대┃?섏뿬 ?섏젙 媛€?? backend 諛섏쁺?€ 蹂??묒뾽. - // pendingLayout 怨?諛고???(異⑸룎 諛⑹?). + // HTML 편집 모드 — 글벗 패턴 (designMode + contentEditable + outline CSS) 차용. + // 활성 시 iframe 안 텍스트 element 직접 클릭하여 수정 가능. backend 반영은 별 작업. + // pendingLayout 과 배타적 (충돌 방지). // IMP-90 (#90) u11 ??`editMode` enum replaces the prior boolean. The // `isEditMode` shim is kept ONLY for the pendingLayout coupling + // zone-wrapper visual cues (border / hover / selected styling) that @@ -255,14 +255,14 @@ export default function SlideCanvas({ const editGates = computeEditModeGates(editMode, !!isPendingLayout); const iframeRef = useRef(null); - // ?몄쭛 紐⑤뱶 toggle ??iframe contentDocument ??湲€踰??⑦꽩 ?곸슜 / ?댁젣. + // 편집 모드 toggle 시 iframe contentDocument 에 글벗 패턴 적용 / 해제. useEffect(() => { const iframe = iframeRef.current; if (!iframe) return; const doc = iframe.contentDocument; if (!doc) return; - // ?몄쭛 紐⑤뱶 outline CSS ????踰덈쭔 二쇱엯 (id 濡?以묐났 諛⑹?) + // 편집 모드 outline CSS — 한 번만 주입 (id 로 중복 방지) let editStyle = doc.getElementById("phase-z-edit-style") as HTMLStyleElement | null; if (!editStyle) { editStyle = doc.createElement("style"); @@ -298,7 +298,7 @@ export default function SlideCanvas({ (el as HTMLElement).setAttribute("contenteditable", "true"); } }); - // ?몄쭛 諛쒖깮 ??hasPendingChanges ?몃━嫄? + // 편집 발생 시 hasPendingChanges 트리거. inputHandler = () => { onContentEdit?.(); }; @@ -363,7 +363,7 @@ export default function SlideCanvas({ }; }, [editGates.textEditing, editGates.imageSelection, finalHtmlUrl, onContentEdit, onTextEdit]); - // pendingLayout 吏꾩엯 ???몄쭛 紐⑤뱶 ?먮룞 OFF (異⑸룎 諛⑹?). + // pendingLayout 진입 시 편집 모드 자동 OFF (충돌 방지). useEffect(() => { if (isPendingLayout && isEditMode) setEditMode("off"); }, [isPendingLayout, isEditMode]); @@ -419,13 +419,13 @@ export default function SlideCanvas({ setEditMode("off"); }, [finalHtmlUrl, isPipelineRunning, isPendingLayout]); - // 16:9 鍮꾩쑉 ?좎??섎ʼn 而⑦뀒?대꼫???듭㎏濡?fit (?ㅽ겕濡?X). + // 16:9 비율 유지하며 컨테이너에 통째로 fit (스크롤 X). useEffect(() => { const el = containerRef.current; if (!el) return; const updateScale = () => { - const padding = 32; // 而⑦뀒?대꼫 ?묒そ ?щ갚 (1rem * 2) + const padding = 32; // 컨테이너 양쪽 여백 (1rem * 2) const cw = el.clientWidth - padding; const ch = el.clientHeight - padding; if (cw <= 0 || ch <= 0) return; @@ -444,11 +444,11 @@ export default function SlideCanvas({ }; }, []); - // Empty state ??MDX ?낅줈???? + // Empty state — MDX 업로드 전. const isEmpty = !finalHtmlUrl && !slidePlan && !isPipelineRunning && !isPendingLayout; const hasNoCurrentRender = !finalHtmlUrl && !!slidePlan && !isPipelineRunning && !isPendingLayout; - // ?щ씪?대뱶 諛뺤뒪 ?쒖떆 議곌굔 ??final.html ?덇굅??pendingLayout 紐⑤뱶. + // 슬라이드 박스 표시 조건 — final.html 있거나 pendingLayout 모드. // Task 13b: pending layout is a draft canvas, not a final.html preview. // Hide the iframe entirely while pending so the prior committed render // cannot visually overlap the pending zones. @@ -456,14 +456,14 @@ export default function SlideCanvas({ const showSlideBox = (renderIframe || isPendingLayout) && !isPipelineRunning; const pendingSlideBody = measuredSlideBody ?? { x: 0, y: 0, w: 1, h: 1 }; - // IMP-14 (Step 13 A-4) ??backend slide_base.html 媛€ embedded vs standalone CSS - // contract 瑜?`?embedded=1` query 濡??뚯쑀. 湲곗〈 query string 蹂댁〈?섎㈃??flag 留?異붽?. + // IMP-14 (Step 13 A-4) — backend slide_base.html 가 embedded vs standalone CSS + // contract 를 `?embedded=1` query 로 소유. 기존 query string 보존하면서 flag 만 추가. const embeddedSrc = renderIframe && finalHtmlUrl ? `${finalHtmlUrl}${finalHtmlUrl.includes("?") ? "&" : "?"}embedded=1` : undefined; - // wrapper ??scaled ?ш린瑜?媛€吏€誘€濡?layout ??fit. ?덉쓽 ?щ씪?대뱶??1280횞720 ?쇰줈 - // top-left origin scale ??wrapper ?덉뿉 ?뺥솗??留욎땄. + // wrapper 는 scaled 크기를 가지므로 layout 상 fit. 안의 슬라이드는 1280×720 으로 + // top-left origin scale 후 wrapper 안에 정확히 맞춤. const W_SCALED = SLIDE_W * scale; const H_SCALED = SLIDE_H * scale; @@ -506,8 +506,8 @@ export default function SlideCanvas({ className="relative overflow-hidden cursor-pointer bg-white shadow-[0_40px_100px_-20px_rgba(0,0,0,0.2)]" style={{ width: W_SCALED, height: H_SCALED }} onClick={(e) => { - // wrapper ?먯껜 ?대┃ (zone overlay 媛€ ?꾨땶 ?곸뿭) = slide ?꾩껜 = Layout ?? - // zone overlay ??stopPropagation ?쇰줈 ?ш린源뚯? ???? + // wrapper 자체 클릭 (zone overlay 가 아닌 영역) = slide 전체 = Layout 탭. + // zone overlay 는 stopPropagation 으로 여기까지 안 옴. if (e.target === e.currentTarget && onSlideClick) onSlideClick(); }} title={ @@ -516,7 +516,7 @@ export default function SlideCanvas({ : "Open layout panel" } > - {/* pendingLayout ???뚮쭔 ?몄텧?섎뒗 ?곗긽??痍⑥냼 踰꾪듉 (slide 諛뺤뒪 ?멸낸 ?좎?) */} + {/* pendingLayout 일 때만 노출되는 우상단 취소 버튼 (slide 박스 외곽 유지) */} {isPendingLayout && onCancelPendingLayout && ( )} @@ -593,7 +593,7 @@ export default function SlideCanvas({ key={embeddedSrc} ref={iframeRef} src={embeddedSrc} - title="Phase Z ?뚮뜑 寃곌낵" + title="Phase Z 렌더 결과" className="w-full h-full border-0 block" scrolling="no" sandbox="allow-same-origin allow-scripts" @@ -605,20 +605,20 @@ export default function SlideCanvas({ // in-iframe gesture (u14 will overlay React-side controls). style={{ pointerEvents: editGates.iframePointerAuto ? "auto" : "none" }} onLoad={(e) => { - // IMP-14 (Step 13 A-4) ??embedded vs standalone CSS reset ?€ backend - // slide_base.html 媛€ `?embedded=1` query 濡??뚯쑀. frontend 媛€ ???댁긽 - // reset CSS 瑜?contentDocument ??inject ?섏? ?딆쓬. embedded query 媛€ - // backend auto-mode detection script 瑜?trigger ?댁꽌 html.embedded - // class 瑜?遺숈씠怨?standalone-only body 洹쒖튃??reset. + // IMP-14 (Step 13 A-4) ??embedded vs standalone CSS reset ?? backend + // slide_base.html 가 `?embedded=1` query 로 소유. frontend 가 더 이상 + // reset CSS 를 contentDocument 에 inject 하지 않음. embedded query 가 + // backend auto-mode detection script 를 trigger 해서 html.embedded + // class 를 붙이고 standalone-only body 규칙을 reset. try { const doc = (e.currentTarget as HTMLIFrameElement).contentDocument; if (!doc) return; - // ?€?€ Zone DOM 痢≪젙 ?€?€ - // backend final.html ??.zone[data-zone-position="..."] ?붿냼瑜? - // 李얠븘??boundingClientRect 痢≪젙 ??1280횞720 湲곗? ?뺢퇋?? - // overlay 媛€ ??痢≪젙媛믪쑝濡?洹몃젮吏€誘€濡?backend ??layout (slide-base - // padding / dynamic ratios / grid) 洹몃?濡?諛섏쁺?? + // ???? Zone DOM 측정 ???? + // backend final.html 의 .zone[data-zone-position="..."] 요소를 + // 찾아서 boundingClientRect 측정 → 1280×720 기준 정규화. + // overlay 가 이 측정값으로 그려지므로 backend 의 layout (slide-base + // padding / dynamic ratios / grid) 그대로 반영됨. const zoneEls = doc.querySelectorAll( ".zone[data-zone-position]" ); @@ -639,8 +639,8 @@ export default function SlideCanvas({ }); setMeasuredZones(measured); - // .slide-body 痢≪젙 ??pendingLayout 紐⑤뱶 ?????곸뿭?먮쭔 鍮? - // overlay 瑜?源붿븘??slide-base (title / footer) ???좎?. + // .slide-body 측정 — pendingLayout 모드 시 이 영역에만 빈 + // overlay 를 깔아서 slide-base (title / footer) 는 유지. const bodyEl = doc.querySelector(".slide-body"); if (bodyEl) { const r = (bodyEl as HTMLElement).getBoundingClientRect(); @@ -652,12 +652,12 @@ export default function SlideCanvas({ }); } - // ?€?€ IMP-51 (#79) u8 ??user-content image bbox 痢≪젙 ?€?€ - // u4 stamper 媛€ 遺€李⑺븳 data-image-id 媛€ ?덈뒗 img 留??≪쓬 - // (decorative / frame img ?쒖쇅). 痢≪젙 寃곌낵??1280횞720 湲곗? - // ?щ씪?대뱶-?덈? percent (0??00) ??image_overrides axis (u3 - // ?€??+ u7 CSS `left/top/width/height: {value}%` 二쇱엯) ?€ - // ?숈씪??醫뚰몴怨꾨씪??痢≪젙 / ?곴뎄 ?€??/ emit 媛€ 1:1 留ㅼ묶?? + // ???? IMP-51 (#79) u8 ??user-content image bbox 측정 ???? + // u4 stamper 가 부착한 data-image-id 가 있는 img 만 잡음 + // (decorative / frame img 제외). 측정 결과는 1280×720 기준 + // 슬라이드-절대 percent (0–100) — image_overrides axis (u3 + // ????+ u7 CSS `left/top/width/height: {value}%` 주입) ?? + // 동일한 좌표계라서 측정 / 영구 저장 / emit 가 1:1 매칭됨. const imageEls = doc.querySelectorAll( '.slide img[data-image-role="user-content"][data-image-id]' ); @@ -679,17 +679,17 @@ export default function SlideCanvas({ }); setMeasuredImages(measuredImg); } catch (err) { - console.warn("[SlideCanvas] iframe inject/measure ?ㅽ뙣:", err); + console.warn("[SlideCanvas] iframe inject/measure 실패:", err); } }} /> )} - {/* ?€?€ pendingLayout ??slide-body ?곸뿭 overlay ?€?€ - slide-base (title / divider / footer) ??final.html 洹몃?濡??좎?. - slide-body ?곸뿭?먮쭔 ??dashed overlay 源붿븘??湲곗〈 .zone ??媛€由ш퀬 - ??layout zone ?덈궡 ?쒖떆. */} + {/* ── pendingLayout 시 slide-body 영역 overlay ── + slide-base (title / divider / footer) 는 final.html 그대로 유지. + slide-body 영역에만 흰 dashed overlay 깔아서 기존 .zone 들 가리고 + 새 layout zone 안내 표시. */} {isPendingLayout && (
- 1. section 移대뱶 ??zone drop  쨌  2. zone ?대┃ ??frame ?좏깮  쨌  3. ?ъ깮?? + 1. section 카드 → zone drop  ·  2. zone 클릭 → frame 선택  ·  3. 재생성
)} - {/* ?€?€ Zone overlay layer ?€?€ - normal mode : measuredZones (= iframe ??.zone DOM bbox) ?곗꽑. - pendingLayout : zone.position (computeZonePositions 寃곌낵 = slide-body - ?대? 0~1) ??measuredSlideBody ???덈? ?뺢퇋?붾줈 蹂€?? - isEditMode : zone overlay 諛섑닾紐?+ pointer-events: none ???띿뒪???몄쭛 - 諛⑺빐 X. resize handle 留?pointer-events: auto 濡??쒖꽦. */} + {/* ???? Zone overlay layer ???? + normal mode : measuredZones (= iframe 안 .zone DOM bbox) 우선. + pendingLayout : zone.position (computeZonePositions 결과 = slide-body + 내부 0~1) 을 measuredSlideBody 안 절대 정규화로 변환. + isEditMode : zone overlay 반투명 + pointer-events: none → 텍스트 편집 + 방해 X. resize handle 만 pointer-events: auto 로 활성. */} {slidePlan?.zones.map((zone) => { - // selectedZoneId ??zone.id ?먮뒗 zone.zone_id 以??대뒓 履쎌씠??留ㅼ묶 (utils - // ??getSelectedZone ???묒そ 蹂대?濡??쇨?). + // selectedZoneId 는 zone.id 또는 zone.zone_id 중 어느 쪽이든 매칭 (utils + // 의 getSelectedZone 도 양쪽 보므로 일관). const isSelected = userSelection.selectedZoneId === zone.id || userSelection.selectedZoneId === zone.zone_id; const isDragOver = dragOverZoneId === zone.id; - // Step B : zone overlay ?쇰꺼 = section_ids 湲곕컲 (S1, S1+S2 ??. - // backend zone position (top/bottom/primary) ?€ 遺€ ?쇰꺼 / tooltip ?쇰줈. - // pendingLayout 紐⑤뱶?먯꽌??override 留??ъ슜 (default 臾댁떆 ??鍮??곹깭濡??쒖옉). + // Step B : zone overlay 라벨 = section_ids 기반 (S1, S1+S2 등). + // backend zone position (top/bottom/primary) 은 부 라벨 / tooltip 으로. + // pendingLayout 모드에서는 override 만 사용 (default 무시 — 빈 상태로 시작). const sectionIds = isPendingLayout ? userSelection.overrides.zone_sections?.[zone.zone_id] ?? [] : userSelection.overrides.zone_sections?.[zone.zone_id] ?? @@ -743,10 +743,10 @@ export default function SlideCanvas({ .map((s) => `S${s.split("-").pop() ?? s}`) .join("+") : "(empty)"; - // 醫뚰몴 寃곗젙 (schema ?듭씪 ???ъ슜??override ????긽 slide-body ?대? 0~1) : - // override ?덉쓬 (zone_geometries) ??slide-body 湲곗? ??1280횞720 ?덈? 蹂€?? - // pending mode + override ?놁쓬 ??zone.position (slide-body 湲곗?) 蹂€?? - // normal mode + override ?놁쓬 ??measuredZones (1280횞720 ?덈?) 吏곸젒. + // 좌표 결정 (schema 통일 — 사용자 override 는 항상 slide-body 내부 0~1) : + // override 있음 (zone_geometries) → slide-body 기준 → 1280×720 절대 변환. + // pending mode + override 없음 → zone.position (slide-body 기준) 변환. + // normal mode + override 없음 → measuredZones (1280×720 절대) 직접. const overrideGeom = userSelection.overrides.zone_geometries?.[zone.zone_id] as | { x: number; y: number; w: number; h: number } @@ -759,7 +759,7 @@ export default function SlideCanvas({ }; let x: number, y: number, width: number, height: number; if (overrideGeom && (measuredSlideBody || isPendingLayout)) { - // ?ъ슜??override ??slide-body 湲곗? + // 사용자 override → slide-body 기준 x = pendingSlideBody.x + overrideGeom.x * pendingSlideBody.w; y = pendingSlideBody.y + overrideGeom.y * pendingSlideBody.h; width = overrideGeom.w * pendingSlideBody.w; @@ -777,10 +777,10 @@ export default function SlideCanvas({ height = measured?.h ?? zone.position.height; } - // Step C : pending mode ??zone overlay resize handles ??independent 紐⑤뜽. - // 媛?zone ???낅┰ 諛뺤뒪 ???몄젒 zone ?곹뼢 X. ?먯쑀 諛곗튂 (gap / 寃뱀묠 ?ъ슜??梨낆엫). - // boundary drag 紐⑤뜽 (?몄젒 ?먮룞 議곗젙) ?€ grid 媛숈? 臾띠쓬 layout ???곹빀?쒕뜲, - // ?ъ슜???섎룄 = zone position drag 源뚯? ?먯쑀濡?쾶 ??independent 媛€ ?먯뿰. + // Step C : pending mode ??zone overlay resize handles ??independent 모델. + // 각 zone 이 독립 박스 — 인접 zone 영향 X. 자유 배치 (gap / 겹침 사용자 책임). + // boundary drag 모델 (인접 자동 조정) 은 grid 같은 묶음 layout 에 적합한데, + // 사용자 의도 = zone position drag 까지 자유롭게 → independent 가 자연. type ResizeDir = | "top" | "bottom" @@ -793,10 +793,10 @@ export default function SlideCanvas({ const makeResizeHandler = ( direction: ResizeDir ) => (ev: React.MouseEvent) => { - // resize ??pendingLayout OR image-zone ?몄쭛 紐⑤뱶 ?쒖꽦. 2026-05-22 - // demo hot-fix ??frame partial ??@container aspect-ratio ?뚯쟾?? - // ?ㅼ뼱媛€??fixed px ?쒖빟 ?щ씪吏? IMP-90 u12: text/structure 紐⑤뱶 - // ?먯꽌??zone resize 鍮꾪솢??(mutually exclusive per editGates). + // resize 는 pendingLayout OR image-zone 편집 모드 활성. 2026-05-22 + // demo hot-fix — frame partial 에 @container aspect-ratio 회전이 + // 들어가서 fixed px 제약 사라짐. IMP-90 u12: text/structure 모드 + // 에서는 zone resize 비활성 (mutually exclusive per editGates). if ((!isPendingLayout && !editGates.zoneGestures) || !onZoneResize) return; ev.preventDefault(); ev.stopPropagation(); @@ -813,8 +813,8 @@ export default function SlideCanvas({ const affectsTop = direction === "top" || direction === "nw" || direction === "ne"; const affectsBottom = direction === "bottom" || direction === "sw" || direction === "se"; - // 2026-05-22 demo hot-fix ??iframe ??留덉슦??媛€濡쒖콈??mouseup leak ?쇱뼱?? - // (?몄쭛 紐⑤뱶?먯꽌 iframe pointerEvents=auto). drag ?숈븞 iframe 媛뺤젣 none. + // 2026-05-22 demo hot-fix — iframe 이 마우스 가로채서 mouseup leak 일어남 + // (편집 모드에서 iframe pointerEvents=auto). drag 동안 iframe 강제 none. const iframeEl = iframeRef.current; const prevIframePE = iframeEl ? iframeEl.style.pointerEvents : ""; if (iframeEl) iframeEl.style.pointerEvents = "none"; @@ -830,7 +830,7 @@ export default function SlideCanvas({ h = Math.max(minSize, Math.min(1 - startGeom.y, startGeom.h + dy)); } if (affectsLeft) { - // 醫뚯륫 寃쎄퀎 ?대룞 ??w 以꾩씠怨?x ?섎┝ (?먮뒗 諛섎?) + // 좌측 경계 이동 — w 줄이고 x 늘림 (또는 반대) const newW = Math.max(minSize, startGeom.w - dx); x = Math.max(0, startGeom.x + (startGeom.w - newW)); w = newW; @@ -859,10 +859,10 @@ export default function SlideCanvas({ const handleResizeSW = makeResizeHandler("sw"); const handleResizeSE = makeResizeHandler("se"); - // zone position drag ??pending mode ?먯꽌 zone body (handle ???곸뿭) - // mousedown drag 濡?x/y ?대룞. drag vs click 援щ텇 : - // 5px 誘몃쭔 ?대룞 = click ??onZoneClick - // 5px ?댁긽 = drag ??onZoneResize 濡?x/y 蹂€寃? + // zone position drag — pending mode 에서 zone body (handle 외 영역) + // mousedown drag 로 x/y 이동. drag vs click 구분 : + // 5px 미만 이동 = click → onZoneClick + // 5px 이상 = drag → onZoneResize 로 x/y 변경 const handleZoneMouseDown = ( ev: React.MouseEvent ) => { @@ -917,7 +917,7 @@ export default function SlideCanvas({ document.removeEventListener("mouseup", onUp); if (iframeEl) iframeEl.style.pointerEvents = prevIframePE; if (!dragged) { - // ?⑥닚 click ?쇰줈 泥섎━ ??onZoneClick. + // 단순 click 으로 처리 — onZoneClick. onZoneClick?.(zone.id); } }; @@ -925,9 +925,9 @@ export default function SlideCanvas({ document.addEventListener("mouseup", onUp); }; - // Step A : ?ъ슜?먭? ?ㅻⅨ frame ??怨⑤옄????洹?preview 瑜?zone ?덉뿉 - // 諛섑닾紐낆쑝濡?誘몃━蹂닿린. final.html (iframe) ??default frame 怨??ㅻ? ?뚮쭔. - // override ?녾굅??default ?€ 媛숈쑝硫?誘몃━蹂닿린 X (iframe 怨?以묐났). + // Step A : 사용자가 다른 frame 을 골랐을 때 그 preview 를 zone 안에 + // 반투명으로 미리보기. final.html (iframe) 의 default frame 과 다를 때만. + // override 없거나 default 와 같으면 미리보기 X (iframe 과 중복). const region = zone.internal_regions[0]; const defaultFrameId = region?.frame_match_strategy.frame_id; const overrideFrameId = region?.id @@ -973,19 +973,19 @@ export default function SlideCanvas({ onDragEnter={(ev) => { ev.preventDefault(); ev.stopPropagation(); - // 釉뚮씪?곗???custom dataTransfer type ??lowercase 媛뺤젣 ???묒そ ??check. + // 브라우저는 custom dataTransfer type 을 lowercase 강제 — 양쪽 다 check. const types = Array.from(ev.dataTransfer.types).map((t) => t.toLowerCase()); if (types.includes("sectionid") || types.includes("section-id")) { setDragOverZoneId(zone.id); } }} onDragOver={(ev) => { - // drop target ?대젮硫?onDragOver ?먯꽌 preventDefault ?꾩닔 ??type check ?놁씠 臾댁“嫄??덉슜. + // drop target 이려면 onDragOver 에서 preventDefault 필수 — type check 없이 무조건 허용. ev.preventDefault(); ev.dataTransfer.dropEffect = "move"; }} onDragLeave={(ev) => { - // ?먯떇?쇰줈 ?ㅼ뼱媛€硫댁꽌 諛쒖깮?섎뒗 leave 臾댁떆 ???ㅼ젣 leave 留?泥섎━. + // 자식으로 들어가면서 발생하는 leave 무시 — 실제 leave 만 처리. if (ev.currentTarget.contains(ev.relatedTarget as Node)) return; setDragOverZoneId((p) => (p === zone.id ? null : p)); }} @@ -993,7 +993,7 @@ export default function SlideCanvas({ ev.preventDefault(); ev.stopPropagation(); setDragOverZoneId(null); - // setData ?덈뜕 紐⑤뱺 type ???쒕룄 ??case 蹂€??/ ?ㅻⅨ ??fallback. + // setData 했던 모든 type 을 시도 — case 변환 / 다른 키 fallback. const sectionId = ev.dataTransfer.getData("sectionId") || ev.dataTransfer.getData("section-id") || @@ -1024,8 +1024,8 @@ export default function SlideCanvas({ top: `${y * 100}%`, width: `${width * 100}%`, height: `${height * 100}%`, - // ?몄쭛 紐⑤뱶 : zone overlay ?먯껜???대┃ ?듦낵 (?띿뒪???몄쭛 ?곗꽑), - // resize handle 留?洹??덉뿉??pointer-events: auto. + // 편집 모드 : zone overlay 자체는 클릭 통과 (텍스트 편집 우선), + // resize handle 만 그 안에서 pointer-events: auto. pointerEvents: isEditMode ? "none" : "auto", }} title={ @@ -1034,7 +1034,7 @@ export default function SlideCanvas({ : `${sectionLabel} (zone: ${zone.zone_id}) - open frame panel or drop a section` } > - {/* Step A : ?좏깮 frame preview 誘몃━蹂닿린 (諛섑닾紐? iframe 怨?以묐났 X) */} + {/* Step A : 선택 frame preview 미리보기 (반투명, iframe 과 중복 X) */} {previewUrl && ( <> {`${zone.zone_id} - {/* "preview" 諛곗? ???곗긽??*/} + {/* "preview" 배지 — 우상단 */} preview @@ -1061,8 +1061,8 @@ export default function SlideCanvas({ )} - {/* zone ?쇰꺼 ??醫뚯긽?? 二??쇰꺼 = section ids (S1, S1+S2), - 遺€ ?쇰꺼 = backend zone position (top, bottom, primary). */} + {/* zone 라벨 — 좌상단. 주 라벨 = section ids (S1, S1+S2), + 부 라벨 = backend zone position (top, bottom, primary). */}
- {/* drop hint ??drag over ??媛€?대뜲???덈궡 */} + {/* drop hint — drag over 시 가운데에 안내 */} {isDragOver && (
@@ -1089,13 +1089,13 @@ export default function SlideCanvas({
)} - {/* Step C : zone resize handles ??8 諛⑺뼢. pendingLayout OR image-zone - ?몄쭛 紐⑤뱶 ?쒖꽦. 2026-05-22 demo hot-fix ??frame partial ??@container - aspect-ratio ?뚯쟾 ?ㅼ뼱媛???fixed px ?쒖빟 ?щ씪??image-zone 紐⑤뱶 resize - ???섎? ?덉쓬. IMP-90 u12: text / structure 紐⑤뱶?먯꽌??zone resize - affordance 誘몃끂異?(editGates.zoneGestures = image-zone only). - edge handle (top/bottom/left/right) : ??boundary ?대룞 - corner handle (nw/ne/sw/se) : ??boundary ?숈떆. */} + {/* Step C : zone resize handles ??8 방향. pendingLayout OR image-zone + 편집 모드 활성. 2026-05-22 demo hot-fix — frame partial 에 @container + aspect-ratio 회전 들어간 후 fixed px 제약 사라져 image-zone 모드 resize + 도 의미 있음. IMP-90 u12: text / structure 모드에서는 zone resize + affordance 미노출 (editGates.zoneGestures = image-zone only). + edge handle (top/bottom/left/right) : 한 boundary 이동 + corner handle (nw/ne/sw/se) : 두 boundary 동시. */} {(isPendingLayout || editGates.zoneGestures) && onZoneResize && ( <> {/* top edge */} @@ -1104,7 +1104,7 @@ export default function SlideCanvas({ onClick={(ev) => ev.stopPropagation()} className="absolute -top-1 left-1/4 w-1/2 h-2 bg-blue-500/70 hover:bg-blue-500 rounded cursor-ns-resize z-30 transition shadow" style={{ pointerEvents: "auto" }} - title="?곷떒 boundary 議곗젅" + title="상단 boundary 조절" /> {/* bottom edge */}
ev.stopPropagation()} className="absolute -bottom-1 left-1/4 w-1/2 h-2 bg-blue-500/70 hover:bg-blue-500 rounded cursor-ns-resize z-30 transition shadow" style={{ pointerEvents: "auto" }} - title="?섎떒 boundary 議곗젅" + title="상단 boundary 조절" /> {/* left edge */}
ev.stopPropagation()} className="absolute top-1/4 -left-1 h-1/2 w-2 bg-blue-500/70 hover:bg-blue-500 rounded cursor-ew-resize z-30 transition shadow" style={{ pointerEvents: "auto" }} - title="醫뚯륫 boundary 議곗젅" + title="좌측 boundary 조절" /> {/* right edge */}
ev.stopPropagation()} className="absolute top-1/4 -right-1 h-1/2 w-2 bg-blue-500/70 hover:bg-blue-500 rounded cursor-ew-resize z-30 transition shadow" style={{ pointerEvents: "auto" }} - title="?곗륫 boundary 議곗젅" + title="좌측 boundary 조절" /> - {/* corner NW (醫뚯긽?? */} + {/* corner NW (좌상단) */}
ev.stopPropagation()} className="absolute -top-1 -left-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize z-30 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="醫뚯긽???숈떆 議곗젅" + title="좌상단 동시 조절" /> - {/* corner NE (?곗긽?? */} + {/* corner NE (우상단) */}
ev.stopPropagation()} className="absolute -top-1 -right-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize z-30 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="?곗긽???숈떆 議곗젅" + title="좌상단 동시 조절" /> - {/* corner SW (醫뚰븯?? */} + {/* corner SW (좌하단) */}
ev.stopPropagation()} className="absolute -bottom-1 -left-1 w-3 h-3 bg-white border-2 border-blue-500 rounded-sm cursor-nesw-resize z-30 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="醫뚰븯???숈떆 議곗젅" + title="좌하단 동시 조절" /> - {/* corner SE (?고븯?? */} + {/* corner SE (우하단) */}
ev.stopPropagation()} className="absolute -bottom-1 -right-1 w-4 h-4 bg-white border-2 border-blue-500 rounded-sm cursor-nwse-resize z-30 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="?고븯???숈떆 議곗젅" + title="좌하단 동시 조절" /> )} @@ -1177,8 +1177,8 @@ export default function SlideCanvas({ interior keeps text-edit reachability intact. pendingLayout mode already has wrapper pointerEvents:auto, so these surfaces are only needed in edit mode. - IMP-90 u12: image-zone-mode-only ??text / structure 紐⑤뱶?? - zone drag ????(editGates.zoneGestures = false ??紐⑤뱶 紐⑤몢). */} + IMP-90 u12: image-zone-mode-only — text / structure 모드는 + zone drag 안 함 (editGates.zoneGestures = false 두 모드 모두). */} {editGates.zoneGestures && !isPendingLayout && onZoneResize && ( <>
{ const persisted = imageOverrides?.[imageId]; const measured = measuredImages[imageId]; - // override ?곗꽑; ?놁쑝硫?measured baseline. ?????놁쑝硫?skip. + // override 우선; 없으면 measured baseline. 둘 다 없으면 skip. const geom = persisted ?? measured; if (!geom) return null; const isSelected = selectedImageId === imageId; @@ -1309,8 +1309,8 @@ export default function SlideCanvas({ const startMouseY = ev.clientY; const startGeom = { ...geom }; - // 2026-05-22 demo hot-fix parity ??iframe ??留덉슦??媛€濡? - // 梨꾩꽌 mouseup leak ?쇱뼱??(?몄쭛 紐⑤뱶?먯꽌 pe=auto). + // 2026-05-22 demo hot-fix parity — iframe 이 마우스 가로 + // 채서 mouseup leak 일어남 (편집 모드에서 pe=auto). const iframeEl = iframeRef.current; const prevIframePE = iframeEl ? iframeEl.style.pointerEvents : ""; if (iframeEl) iframeEl.style.pointerEvents = "none"; @@ -1375,8 +1375,8 @@ export default function SlideCanvas({ }} title={ isSelected - ? "?대?吏€ ?대룞 ???쒕옒洹?/ 紐⑥꽌由??몃뱾 = ?ш린 (Shift = 鍮꾩쑉 ?댁젣)" - : "?대┃?섏뿬 ?좏깮" + ? "이미지 이동 — 드래그 / 모서리 핸들 = 크기 (Shift = 비율 해제)" + : "클릭하여 선택" } > @@ -1391,28 +1391,28 @@ export default function SlideCanvas({ onClick={(ev) => ev.stopPropagation()} className="absolute -top-1 left-1/4 w-1/2 h-2 bg-emerald-500/70 hover:bg-emerald-500 rounded cursor-ns-resize z-40 transition shadow" style={{ pointerEvents: "auto" }} - title="?곷떒" + title="상단" />
beginDrag(ev, "bottom")} onClick={(ev) => ev.stopPropagation()} className="absolute -bottom-1 left-1/4 w-1/2 h-2 bg-emerald-500/70 hover:bg-emerald-500 rounded cursor-ns-resize z-40 transition shadow" style={{ pointerEvents: "auto" }} - title="?섎떒" + title="상단" />
beginDrag(ev, "left")} onClick={(ev) => ev.stopPropagation()} className="absolute top-1/4 -left-1 h-1/2 w-2 bg-emerald-500/70 hover:bg-emerald-500 rounded cursor-ew-resize z-40 transition shadow" style={{ pointerEvents: "auto" }} - title="醫뚯륫" + title="좌측" />
beginDrag(ev, "right")} onClick={(ev) => ev.stopPropagation()} className="absolute top-1/4 -right-1 h-1/2 w-2 bg-emerald-500/70 hover:bg-emerald-500 rounded cursor-ew-resize z-40 transition shadow" style={{ pointerEvents: "auto" }} - title="?곗륫" + title="좌측" /> {/* corners ??aspect locked by default, Shift unlocks */}
ev.stopPropagation()} className="absolute -top-1 -left-1 w-3 h-3 bg-white border-2 border-emerald-500 rounded-sm cursor-nwse-resize z-40 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="醫뚯긽??(Shift = 鍮꾩쑉 ?댁젣)" + title="좌상단 (Shift = 비율 해제)" />
beginDrag(ev, "ne")} onClick={(ev) => ev.stopPropagation()} className="absolute -top-1 -right-1 w-3 h-3 bg-white border-2 border-emerald-500 rounded-sm cursor-nesw-resize z-40 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="?곗긽??(Shift = 鍮꾩쑉 ?댁젣)" + title="좌상단 (Shift = 비율 해제)" />
beginDrag(ev, "sw")} onClick={(ev) => ev.stopPropagation()} className="absolute -bottom-1 -left-1 w-3 h-3 bg-white border-2 border-emerald-500 rounded-sm cursor-nesw-resize z-40 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="醫뚰븯??(Shift = 鍮꾩쑉 ?댁젣)" + title="좌하단 (Shift = 비율 해제)" />
beginDrag(ev, "se")} onClick={(ev) => ev.stopPropagation()} className="absolute -bottom-1 -right-1 w-4 h-4 bg-white border-2 border-emerald-500 rounded-sm cursor-nwse-resize z-40 hover:scale-125 transition shadow" style={{ pointerEvents: "auto" }} - title="?고븯??(Shift = 鍮꾩쑉 ?댁젣)" + title="좌하단 (Shift = 비율 해제)" /> )}