b836e79스냅샷에서 UTF-8→CP949 오변환으로 한글 주석·UI 문자열 156줄 손상. 복구 방법: - CP949 재인코딩→UTF-8 역변환 (ASCII 코드 줄 불변 보장) - 소실 바이트('?' 잔여)가 있는 줄만4da22ad정상본에서 회수 (134줄 + 수작업 9줄) - 순수 ASCII 줄 변조 0 검증 — b836e79의 기능 변경(pendingSlideBody 등) 전부 보존 - BOM 제거 검증: tsc --noEmit 0 에러, vitest 425/425 통과 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, { x: number; y: number; w: number; h: number }>
|
||||
) => 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<HTMLDivElement>(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<string, { x: number; y: number; w: number; h: number }>
|
||||
>({});
|
||||
// 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<string | null>(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<string | null>(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<HTMLIFrameElement>(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 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -526,7 +526,7 @@ export default function SlideCanvas({
|
||||
className="absolute top-2 right-2 z-30 text-[10px] font-bold uppercase tracking-tighter px-2 py-1 rounded bg-slate-800 text-white hover:bg-slate-700 shadow"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
>
|
||||
痍⑥냼
|
||||
취소
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -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<HTMLImageElement>(
|
||||
'.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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ?? 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 && (
|
||||
<div
|
||||
className="absolute bg-white border-2 border-dashed border-amber-400 z-10"
|
||||
@@ -710,29 +710,29 @@ export default function SlideCanvas({
|
||||
{pendingLayoutId}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400 block mt-1">
|
||||
1. section 移대뱶 ??zone drop 쨌 2. zone ?대┃ ??frame ?좏깮 쨌 3. ?ъ깮??
|
||||
1. section 카드 → zone drop · 2. zone 클릭 → frame 선택 · 3. 재생성
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ?? 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<HTMLDivElement>) => {
|
||||
// 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<HTMLDivElement>
|
||||
) => {
|
||||
@@ -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 && (
|
||||
<>
|
||||
<img
|
||||
@@ -1042,7 +1042,7 @@ export default function SlideCanvas({
|
||||
alt={`${zone.zone_id} preview`}
|
||||
className="absolute inset-0 w-full h-full object-contain bg-white/60 opacity-70 pointer-events-none"
|
||||
/>
|
||||
{/* "preview" 諛곗? ???곗긽??*/}
|
||||
{/* "preview" 배지 — 우상단 */}
|
||||
<span className="absolute top-1 right-1 text-[9px] font-black uppercase tracking-tighter px-1.5 py-0.5 rounded bg-amber-500 text-white shadow">
|
||||
preview
|
||||
</span>
|
||||
@@ -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). */}
|
||||
<div className="absolute top-1 left-1 flex items-center gap-1 pointer-events-none">
|
||||
<span
|
||||
className={`text-[10px] font-black uppercase tracking-tighter px-1.5 py-0.5 rounded ${
|
||||
@@ -1080,7 +1080,7 @@ export default function SlideCanvas({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* drop hint ??drag over ??媛?대뜲???덈궡 */}
|
||||
{/* drop hint — drag over 시 가운데에 안내 */}
|
||||
{isDragOver && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<span className="text-[12px] font-black uppercase tracking-wider px-3 py-1.5 rounded-full bg-emerald-500 text-white shadow-xl">
|
||||
@@ -1089,13 +1089,13 @@ export default function SlideCanvas({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 */}
|
||||
<div
|
||||
@@ -1112,7 +1112,7 @@ export default function SlideCanvas({
|
||||
onClick={(ev) => 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 */}
|
||||
<div
|
||||
@@ -1120,7 +1120,7 @@ export default function SlideCanvas({
|
||||
onClick={(ev) => 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 */}
|
||||
<div
|
||||
@@ -1128,39 +1128,39 @@ export default function SlideCanvas({
|
||||
onClick={(ev) => 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 (좌상단) */}
|
||||
<div
|
||||
onMouseDown={handleResizeNW}
|
||||
onClick={(ev) => 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 (우상단) */}
|
||||
<div
|
||||
onMouseDown={handleResizeNE}
|
||||
onClick={(ev) => 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 (좌하단) */}
|
||||
<div
|
||||
onMouseDown={handleResizeSW}
|
||||
onClick={(ev) => 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 (우하단) */}
|
||||
<div
|
||||
onMouseDown={handleResizeSE}
|
||||
onClick={(ev) => 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 && (
|
||||
<>
|
||||
<div
|
||||
@@ -1257,7 +1257,7 @@ export default function SlideCanvas({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* ?? IMP-51 (#79) u8 ??user-content image edit overlay ??
|
||||
{/* ???? IMP-51 (#79) u8 ??user-content image edit overlay ????
|
||||
Activates only in edit mode when an image_id appears in either
|
||||
`imageOverrides` (u11-fed persisted axis) or `measuredImages`
|
||||
(iframe-measured baseline). pendingLayout suppresses the image
|
||||
@@ -1285,15 +1285,15 @@ export default function SlideCanvas({
|
||||
verified is exercised directly by vitest (mirror of how IMP-54
|
||||
u3 split the zone math out of SlideCanvas). */}
|
||||
{/* IMP-90 u12: image overlay is image-zone-mode-only. text /
|
||||
structure 紐⑤뱶?먯꽌??image drag/resize affordance 誘몃끂異?
|
||||
(editGates.imageOverlay = false). pendingLayout ???숈씪?섍쾶
|
||||
suppress (computeEditModeGates 媛 紐⑤몢 false 諛섑솚). */}
|
||||
structure 모드에서는 image drag/resize affordance 미노출
|
||||
(editGates.imageOverlay = false). pendingLayout 도 동일하게
|
||||
suppress (computeEditModeGates 가 모두 false 반환). */}
|
||||
{!isPendingLayout && editGates.imageOverlay && finalHtmlUrl && onImageResize &&
|
||||
Object.entries({ ...measuredImages, ...(imageOverrides ?? {}) }).map(
|
||||
([imageId]) => {
|
||||
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 = 비율 해제)"
|
||||
: "클릭하여 선택"
|
||||
}
|
||||
>
|
||||
<span className="absolute top-1 left-1 text-[9px] font-black uppercase tracking-tighter px-1.5 py-0.5 rounded bg-emerald-600/90 text-white shadow pointer-events-none">
|
||||
@@ -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="상단"
|
||||
/>
|
||||
<div
|
||||
onMouseDown={(ev) => 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="상단"
|
||||
/>
|
||||
<div
|
||||
onMouseDown={(ev) => 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="좌측"
|
||||
/>
|
||||
<div
|
||||
onMouseDown={(ev) => 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 */}
|
||||
<div
|
||||
@@ -1420,28 +1420,28 @@ export default function SlideCanvas({
|
||||
onClick={(ev) => 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 = 비율 해제)"
|
||||
/>
|
||||
<div
|
||||
onMouseDown={(ev) => 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 = 비율 해제)"
|
||||
/>
|
||||
<div
|
||||
onMouseDown={(ev) => 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 = 비율 해제)"
|
||||
/>
|
||||
<div
|
||||
onMouseDown={(ev) => 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 = 비율 해제)"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user