feat(pop-designer): 반응형 그리드 시스템 고도화
- 브레이크포인트 재설계: 실제 기기 CSS 뷰포트 기반 (479/767/1023px) - 자동 줄바꿈 시스템: col > maxCol 컴포넌트 자동 재배치, 검토 필요 알림 - Gap 프리셋: 좁게/보통/넓게 3단계 간격 조절 - 셀 크기 강제 고정: gridTemplateRows + overflow-hidden - 세로 자동 확장: 동적 캔버스 높이 계산 (최소 600px) - 뷰어 모드 일관성: detectGridMode() 직접 사용 - 컴포넌트 ID 충돌 방지: 로드 시 idCounter 자동 설정 - popdocs 문서 정비: ADR 2건, 레거시 문서 archive 이동 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
726f6ac395
commit
40219fed08
|
|
@ -2701,4 +2701,3 @@ export const ensurePopRootGroup = async (req: AuthenticatedRequest, res: Respons
|
|||
res.status(500).json({ success: false, message: "POP 루트 그룹 확보에 실패했습니다.", error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ import {
|
|||
GridMode,
|
||||
isV5Layout,
|
||||
createEmptyPopLayoutV5,
|
||||
GAP_PRESETS,
|
||||
GRID_BREAKPOINTS,
|
||||
detectGridMode,
|
||||
} from "@/components/pop/designer/types/pop-layout";
|
||||
import PopRenderer from "@/components/pop/designer/renderers/PopRenderer";
|
||||
import {
|
||||
|
|
@ -27,15 +30,15 @@ import {
|
|||
type DeviceType,
|
||||
} from "@/hooks/useDeviceOrientation";
|
||||
|
||||
// 디바이스별 크기 (프리뷰 모드용)
|
||||
const DEVICE_SIZES: Record<DeviceType, Record<"landscape" | "portrait", { width: number; height: number; label: string }>> = {
|
||||
// 디바이스별 크기 (너비만, 높이는 콘텐츠 기반)
|
||||
const DEVICE_SIZES: Record<DeviceType, Record<"landscape" | "portrait", { width: number; label: string }>> = {
|
||||
mobile: {
|
||||
landscape: { width: 667, height: 375, label: "모바일 가로" },
|
||||
portrait: { width: 375, height: 667, label: "모바일 세로" },
|
||||
landscape: { width: 600, label: "모바일 가로" },
|
||||
portrait: { width: 375, label: "모바일 세로" },
|
||||
},
|
||||
tablet: {
|
||||
landscape: { width: 1024, height: 768, label: "태블릿 가로" },
|
||||
portrait: { width: 768, height: 1024, label: "태블릿 세로" },
|
||||
landscape: { width: 1024, label: "태블릿 가로" },
|
||||
portrait: { width: 820, label: "태블릿 세로" },
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -69,7 +72,6 @@ function PopScreenViewPage() {
|
|||
// 현재 모드 정보
|
||||
const deviceType = mode.device;
|
||||
const isLandscape = mode.isLandscape;
|
||||
const currentModeKey = getModeKey(deviceType, isLandscape);
|
||||
|
||||
const { user } = useAuth();
|
||||
|
||||
|
|
@ -81,6 +83,13 @@ function PopScreenViewPage() {
|
|||
// 뷰포트 너비 (클라이언트 사이드에서만 계산, 최대 1366px)
|
||||
const [viewportWidth, setViewportWidth] = useState(1024); // 기본값: 태블릿 가로
|
||||
|
||||
// 모드 결정:
|
||||
// - 프리뷰 모드: 수동 선택한 device/orientation 사용
|
||||
// - 일반 모드: 화면 너비 기준으로 자동 결정 (GRID_BREAKPOINTS와 일치)
|
||||
const currentModeKey = isPreviewMode
|
||||
? getModeKey(deviceType, isLandscape)
|
||||
: detectGridMode(viewportWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const updateViewportWidth = () => {
|
||||
setViewportWidth(Math.min(window.innerWidth, 1366));
|
||||
|
|
@ -261,25 +270,38 @@ function PopScreenViewPage() {
|
|||
)}
|
||||
|
||||
<div
|
||||
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-hidden border-8 border-gray-800" : "w-full h-full"}`}
|
||||
className={`bg-white transition-all duration-300 ${isPreviewMode ? "shadow-2xl rounded-3xl overflow-auto border-8 border-gray-800" : "w-full"}`}
|
||||
style={isPreviewMode ? {
|
||||
width: currentDevice.width,
|
||||
height: currentDevice.height,
|
||||
maxHeight: "80vh",
|
||||
flexShrink: 0,
|
||||
} : undefined}
|
||||
>
|
||||
{/* v5 그리드 렌더러 */}
|
||||
{hasComponents ? (
|
||||
<div
|
||||
className="mx-auto h-full"
|
||||
className="mx-auto min-h-full"
|
||||
style={{ maxWidth: 1366 }}
|
||||
>
|
||||
<PopRenderer
|
||||
layout={layout}
|
||||
viewportWidth={isPreviewMode ? currentDevice.width : viewportWidth}
|
||||
currentMode={currentModeKey}
|
||||
isDesignMode={false}
|
||||
/>
|
||||
{(() => {
|
||||
// Gap 프리셋 계산
|
||||
const currentGapPreset = layout.settings.gapPreset || "medium";
|
||||
const gapMultiplier = GAP_PRESETS[currentGapPreset]?.multiplier || 1.0;
|
||||
const breakpoint = GRID_BREAKPOINTS[currentModeKey];
|
||||
const adjustedGap = Math.round(breakpoint.gap * gapMultiplier);
|
||||
const adjustedPadding = Math.max(8, Math.round(breakpoint.padding * gapMultiplier));
|
||||
|
||||
return (
|
||||
<PopRenderer
|
||||
layout={layout}
|
||||
viewportWidth={isPreviewMode ? currentDevice.width : viewportWidth}
|
||||
currentMode={currentModeKey}
|
||||
isDesignMode={false}
|
||||
overrideGap={adjustedGap}
|
||||
overridePadding={adjustedPadding}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
// 빈 화면
|
||||
|
|
|
|||
|
|
@ -9,15 +9,24 @@ import {
|
|||
PopComponentType,
|
||||
PopGridPosition,
|
||||
GridMode,
|
||||
GapPreset,
|
||||
GAP_PRESETS,
|
||||
GRID_BREAKPOINTS,
|
||||
DEFAULT_COMPONENT_GRID_SIZE,
|
||||
} from "./types/pop-layout";
|
||||
import { ZoomIn, ZoomOut, Maximize2, Smartphone, Tablet, Lock, RotateCcw, AlertTriangle, EyeOff } from "lucide-react";
|
||||
import { useDrag } from "react-dnd";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import PopRenderer from "./renderers/PopRenderer";
|
||||
import { findNextEmptyPosition, isOverlapping, getAllEffectivePositions, isOutOfBounds } from "./utils/gridUtils";
|
||||
import { findNextEmptyPosition, isOverlapping, getAllEffectivePositions, needsReview } from "./utils/gridUtils";
|
||||
import { DND_ITEM_TYPES } from "./constants";
|
||||
|
||||
/**
|
||||
|
|
@ -65,13 +74,13 @@ interface DragItemMoveComponent {
|
|||
}
|
||||
|
||||
// ========================================
|
||||
// 프리셋 해상도 (4개 모드)
|
||||
// 프리셋 해상도 (4개 모드) - 너비만 정의
|
||||
// ========================================
|
||||
const VIEWPORT_PRESETS = [
|
||||
{ id: "mobile_portrait", label: "모바일 세로", shortLabel: "모바일↕ (4칸)", width: 375, height: 667, icon: Smartphone },
|
||||
{ id: "mobile_landscape", label: "모바일 가로", shortLabel: "모바일↔ (6칸)", width: 667, height: 375, icon: Smartphone },
|
||||
{ id: "tablet_portrait", label: "태블릿 세로", shortLabel: "태블릿↕ (8칸)", width: 768, height: 1024, icon: Tablet },
|
||||
{ id: "tablet_landscape", label: "태블릿 가로", shortLabel: "태블릿↔ (12칸)", width: 1024, height: 768, icon: Tablet },
|
||||
{ id: "mobile_portrait", label: "모바일 세로", shortLabel: "모바일↕ (4칸)", width: 375, icon: Smartphone },
|
||||
{ id: "mobile_landscape", label: "모바일 가로", shortLabel: "모바일↔ (6칸)", width: 600, icon: Smartphone },
|
||||
{ id: "tablet_portrait", label: "태블릿 세로", shortLabel: "태블릿↕ (8칸)", width: 820, icon: Tablet },
|
||||
{ id: "tablet_landscape", label: "태블릿 가로", shortLabel: "태블릿↔ (12칸)", width: 1024, icon: Tablet },
|
||||
] as const;
|
||||
|
||||
type ViewportPreset = GridMode;
|
||||
|
|
@ -79,6 +88,10 @@ type ViewportPreset = GridMode;
|
|||
// 기본 프리셋 (태블릿 가로)
|
||||
const DEFAULT_PRESET: ViewportPreset = "tablet_landscape";
|
||||
|
||||
// 캔버스 세로 자동 확장 설정
|
||||
const MIN_CANVAS_HEIGHT = 600; // 최소 캔버스 높이 (px)
|
||||
const CANVAS_EXTRA_ROWS = 3; // 여유 행 수
|
||||
|
||||
// ========================================
|
||||
// Props
|
||||
// ========================================
|
||||
|
|
@ -98,6 +111,7 @@ interface PopCanvasProps {
|
|||
onUnhideComponent?: (componentId: string) => void;
|
||||
onLockLayout?: () => void;
|
||||
onResetOverride?: (mode: GridMode) => void;
|
||||
onChangeGapPreset?: (preset: GapPreset) => void;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
|
|
@ -120,13 +134,13 @@ export default function PopCanvas({
|
|||
onUnhideComponent,
|
||||
onLockLayout,
|
||||
onResetOverride,
|
||||
onChangeGapPreset,
|
||||
}: PopCanvasProps) {
|
||||
// 줌 상태
|
||||
const [canvasScale, setCanvasScale] = useState(0.8);
|
||||
|
||||
// 커스텀 뷰포트 크기
|
||||
// 커스텀 뷰포트 너비
|
||||
const [customWidth, setCustomWidth] = useState(1024);
|
||||
const [customHeight, setCustomHeight] = useState(768);
|
||||
|
||||
// 그리드 가이드 표시 여부
|
||||
const [showGridGuide, setShowGridGuide] = useState(true);
|
||||
|
|
@ -142,12 +156,48 @@ export default function PopCanvas({
|
|||
const currentPreset = VIEWPORT_PRESETS.find((p) => p.id === currentMode)!;
|
||||
const breakpoint = GRID_BREAKPOINTS[currentMode];
|
||||
|
||||
// 그리드 라벨 계산
|
||||
// Gap 프리셋 적용
|
||||
const currentGapPreset = layout.settings.gapPreset || "medium";
|
||||
const gapMultiplier = GAP_PRESETS[currentGapPreset]?.multiplier || 1.0;
|
||||
const adjustedGap = Math.round(breakpoint.gap * gapMultiplier);
|
||||
const adjustedPadding = Math.max(8, Math.round(breakpoint.padding * gapMultiplier));
|
||||
|
||||
// 숨김 컴포넌트 ID 목록
|
||||
const hiddenComponentIds = layout.overrides?.[currentMode]?.hidden || [];
|
||||
|
||||
// 동적 캔버스 높이 계산 (컴포넌트 배치 기반)
|
||||
const dynamicCanvasHeight = useMemo(() => {
|
||||
const visibleComps = Object.values(layout.components).filter(
|
||||
comp => !hiddenComponentIds.includes(comp.id)
|
||||
);
|
||||
|
||||
if (visibleComps.length === 0) return MIN_CANVAS_HEIGHT;
|
||||
|
||||
// 최대 row + rowSpan 찾기
|
||||
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
||||
const overridePos = layout.overrides?.[currentMode]?.positions?.[comp.id];
|
||||
const pos = overridePos ? { ...comp.position, ...overridePos } : comp.position;
|
||||
const rowEnd = pos.row + pos.rowSpan;
|
||||
return Math.max(max, rowEnd);
|
||||
}, 1);
|
||||
|
||||
// 높이 계산: (행 수 + 여유) * (행높이 + gap) + padding
|
||||
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS;
|
||||
const height = totalRows * (breakpoint.rowHeight + adjustedGap) + adjustedPadding * 2;
|
||||
|
||||
return Math.max(MIN_CANVAS_HEIGHT, height);
|
||||
}, [layout.components, layout.overrides, currentMode, hiddenComponentIds, breakpoint.rowHeight, adjustedGap, adjustedPadding]);
|
||||
|
||||
// 그리드 라벨 계산 (동적 행 수)
|
||||
const gridLabels = useMemo(() => {
|
||||
const columnLabels = Array.from({ length: breakpoint.columns }, (_, i) => i + 1);
|
||||
const rowLabels = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||
|
||||
// 동적 행 수 계산
|
||||
const rowCount = Math.ceil(dynamicCanvasHeight / (breakpoint.rowHeight + adjustedGap));
|
||||
const rowLabels = Array.from({ length: rowCount }, (_, i) => i + 1);
|
||||
|
||||
return { columnLabels, rowLabels };
|
||||
}, [breakpoint.columns]);
|
||||
}, [breakpoint.columns, breakpoint.rowHeight, dynamicCanvasHeight, adjustedGap]);
|
||||
|
||||
// 줌 컨트롤
|
||||
const handleZoomIn = () => setCanvasScale((prev) => Math.min(1.5, prev + 0.1));
|
||||
|
|
@ -159,7 +209,7 @@ export default function PopCanvas({
|
|||
onModeChange(mode);
|
||||
const presetData = VIEWPORT_PRESETS.find((p) => p.id === mode)!;
|
||||
setCustomWidth(presetData.width);
|
||||
setCustomHeight(presetData.height);
|
||||
// customHeight는 dynamicCanvasHeight로 자동 계산됨
|
||||
};
|
||||
|
||||
// 패닝
|
||||
|
|
@ -235,8 +285,8 @@ export default function PopCanvas({
|
|||
customWidth,
|
||||
breakpoint.columns,
|
||||
breakpoint.rowHeight,
|
||||
breakpoint.gap,
|
||||
breakpoint.padding
|
||||
adjustedGap,
|
||||
adjustedPadding
|
||||
);
|
||||
|
||||
const dragItem = item as DragItemComponent;
|
||||
|
|
@ -289,8 +339,8 @@ export default function PopCanvas({
|
|||
customWidth,
|
||||
breakpoint.columns,
|
||||
breakpoint.rowHeight,
|
||||
breakpoint.gap,
|
||||
breakpoint.padding
|
||||
adjustedGap,
|
||||
adjustedPadding
|
||||
);
|
||||
|
||||
const dragItem = item as DragItemMoveComponent & { fromHidden?: boolean };
|
||||
|
|
@ -299,7 +349,7 @@ export default function PopCanvas({
|
|||
const effectivePositions = getAllEffectivePositions(layout, currentMode);
|
||||
|
||||
// 이동 중인 컴포넌트의 현재 유효 위치에서 colSpan/rowSpan 가져오기
|
||||
// 초과 컴포넌트(OutOfBoundsPanel에서 드래그)나 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용
|
||||
// 검토 필요(ReviewPanel에서 클릭)나 숨김 컴포넌트는 effectivePositions에 없으므로 원본 사용
|
||||
const currentEffectivePos = effectivePositions.get(dragItem.componentId);
|
||||
const componentData = layout.components[dragItem.componentId];
|
||||
|
||||
|
|
@ -348,7 +398,7 @@ export default function PopCanvas({
|
|||
canDrop: monitor.canDrop(),
|
||||
}),
|
||||
}),
|
||||
[onDropComponent, onMoveComponent, onUnhideComponent, breakpoint, layout, currentMode, canvasScale, customWidth, customHeight]
|
||||
[onDropComponent, onMoveComponent, onUnhideComponent, breakpoint, layout, currentMode, canvasScale, customWidth, adjustedGap, adjustedPadding]
|
||||
);
|
||||
|
||||
drop(canvasRef);
|
||||
|
|
@ -356,43 +406,36 @@ export default function PopCanvas({
|
|||
// 빈 상태 체크
|
||||
const isEmpty = Object.keys(layout.components).length === 0;
|
||||
|
||||
// 숨김 처리된 컴포넌트 목록
|
||||
const hiddenComponentIds = useMemo(() => {
|
||||
return layout.overrides?.[currentMode]?.hidden || [];
|
||||
}, [layout.overrides, currentMode]);
|
||||
|
||||
// 숨김 처리된 컴포넌트 객체 목록
|
||||
// 숨김 처리된 컴포넌트 객체 목록 (hiddenComponentIds는 라인 166에서 정의됨)
|
||||
const hiddenComponents = useMemo(() => {
|
||||
return hiddenComponentIds
|
||||
.map(id => layout.components[id])
|
||||
.filter(Boolean);
|
||||
}, [hiddenComponentIds, layout.components]);
|
||||
|
||||
// 초과 컴포넌트 목록 (오른쪽 영역에 표시)
|
||||
// 오버라이드가 있는 컴포넌트는 오버라이드 위치로 판단
|
||||
// 숨김 처리된 컴포넌트는 제외
|
||||
const outOfBoundsComponents = useMemo(() => {
|
||||
return Object.values(layout.components).filter(comp => {
|
||||
// 숨김 처리된 컴포넌트는 초과 목록에서 제외
|
||||
if (hiddenComponentIds.includes(comp.id)) return false;
|
||||
// 표시되는 컴포넌트 목록 (숨김 제외)
|
||||
const visibleComponents = useMemo(() => {
|
||||
return Object.values(layout.components).filter(
|
||||
comp => !hiddenComponentIds.includes(comp.id)
|
||||
);
|
||||
}, [layout.components, hiddenComponentIds]);
|
||||
|
||||
// 오버라이드 위치 확인
|
||||
const overridePos = layout.overrides?.[currentMode]?.positions?.[comp.id];
|
||||
const overridePosition = overridePos
|
||||
? { ...comp.position, ...overridePos }
|
||||
: null;
|
||||
|
||||
return isOutOfBounds(comp.position, currentMode, overridePosition);
|
||||
// 검토 필요 컴포넌트 목록
|
||||
const reviewComponents = useMemo(() => {
|
||||
return visibleComponents.filter(comp => {
|
||||
const hasOverride = !!layout.overrides?.[currentMode]?.positions?.[comp.id];
|
||||
return needsReview(currentMode, hasOverride);
|
||||
});
|
||||
}, [layout.components, layout.overrides, currentMode, hiddenComponentIds]);
|
||||
}, [visibleComponents, layout.overrides, currentMode]);
|
||||
|
||||
// 검토 패널 표시 여부 (12칸 모드가 아니고, 검토 필요 컴포넌트가 있을 때)
|
||||
const showReviewPanel = currentMode !== "tablet_landscape" && reviewComponents.length > 0;
|
||||
|
||||
// 12칸 모드가 아닐 때만 패널 표시
|
||||
// 초과 컴포넌트: 있을 때만 표시
|
||||
// 숨김 패널: 숨김 컴포넌트가 있거나, 그리드에 컴포넌트가 있을 때 드롭 영역으로 표시
|
||||
const showOutOfBoundsPanel = currentMode !== "tablet_landscape" && outOfBoundsComponents.length > 0;
|
||||
const hasGridComponents = Object.keys(layout.components).length > 0;
|
||||
const showHiddenPanel = currentMode !== "tablet_landscape" && (hiddenComponents.length > 0 || hasGridComponents);
|
||||
const showRightPanel = showOutOfBoundsPanel || showHiddenPanel;
|
||||
const showRightPanel = showReviewPanel || showHiddenPanel;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-gray-50">
|
||||
|
|
@ -457,7 +500,29 @@ export default function PopCanvas({
|
|||
|
||||
{/* 해상도 표시 */}
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{customWidth} × {customHeight}
|
||||
{customWidth} × {Math.round(dynamicCanvasHeight)}
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-gray-300" />
|
||||
|
||||
{/* Gap 프리셋 선택 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">간격:</span>
|
||||
<Select
|
||||
value={currentGapPreset}
|
||||
onValueChange={(value) => onChangeGapPreset?.(value as GapPreset)}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-20 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(GAP_PRESETS) as GapPreset[]).map((preset) => (
|
||||
<SelectItem key={preset} value={preset} className="text-xs">
|
||||
{GAP_PRESETS[preset].label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
|
@ -528,7 +593,7 @@ export default function PopCanvas({
|
|||
width: showRightPanel
|
||||
? `${customWidth + 32 + 220}px` // 오른쪽 패널 공간 추가
|
||||
: `${customWidth + 32}px`,
|
||||
minHeight: `${customHeight + 32}px`,
|
||||
minHeight: `${dynamicCanvasHeight + 32}px`,
|
||||
transform: `scale(${canvasScale})`,
|
||||
}}
|
||||
>
|
||||
|
|
@ -541,8 +606,8 @@ export default function PopCanvas({
|
|||
<div
|
||||
className="flex absolute top-0 left-8"
|
||||
style={{
|
||||
gap: `${breakpoint.gap}px`,
|
||||
paddingLeft: `${breakpoint.padding}px`,
|
||||
gap: `${adjustedGap}px`,
|
||||
paddingLeft: `${adjustedPadding}px`,
|
||||
}}
|
||||
>
|
||||
{gridLabels.columnLabels.map((num) => (
|
||||
|
|
@ -550,7 +615,7 @@ export default function PopCanvas({
|
|||
key={`col-${num}`}
|
||||
className="flex items-center justify-center text-xs font-semibold text-blue-500"
|
||||
style={{
|
||||
width: `calc((${customWidth}px - ${breakpoint.padding * 2}px - ${breakpoint.gap * (breakpoint.columns - 1)}px) / ${breakpoint.columns})`,
|
||||
width: `calc((${customWidth}px - ${adjustedPadding * 2}px - ${adjustedGap * (breakpoint.columns - 1)}px) / ${breakpoint.columns})`,
|
||||
height: "24px",
|
||||
}}
|
||||
>
|
||||
|
|
@ -563,8 +628,8 @@ export default function PopCanvas({
|
|||
<div
|
||||
className="flex flex-col absolute top-8 left-0"
|
||||
style={{
|
||||
gap: `${breakpoint.gap}px`,
|
||||
paddingTop: `${breakpoint.padding}px`,
|
||||
gap: `${adjustedGap}px`,
|
||||
paddingTop: `${adjustedPadding}px`,
|
||||
}}
|
||||
>
|
||||
{gridLabels.rowLabels.map((num) => (
|
||||
|
|
@ -592,7 +657,7 @@ export default function PopCanvas({
|
|||
)}
|
||||
style={{
|
||||
width: `${customWidth}px`,
|
||||
minHeight: `${customHeight}px`,
|
||||
minHeight: `${dynamicCanvasHeight}px`,
|
||||
marginLeft: "32px",
|
||||
marginTop: "32px",
|
||||
}}
|
||||
|
|
@ -623,6 +688,8 @@ export default function PopCanvas({
|
|||
onComponentMove={onMoveComponent}
|
||||
onComponentResize={onResizeComponent}
|
||||
onComponentResizeEnd={onResizeEnd}
|
||||
overrideGap={adjustedGap}
|
||||
overridePadding={adjustedPadding}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -634,13 +701,12 @@ export default function PopCanvas({
|
|||
className="flex flex-col gap-3"
|
||||
style={{ marginTop: "32px" }}
|
||||
>
|
||||
{/* 초과 컴포넌트 패널 */}
|
||||
{showOutOfBoundsPanel && (
|
||||
<OutOfBoundsPanel
|
||||
components={outOfBoundsComponents}
|
||||
{/* 검토 필요 패널 */}
|
||||
{showReviewPanel && (
|
||||
<ReviewPanel
|
||||
components={reviewComponents}
|
||||
selectedComponentId={selectedComponentId}
|
||||
onSelectComponent={onSelectComponent}
|
||||
onHideComponent={onHideComponent}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -672,61 +738,96 @@ export default function PopCanvas({
|
|||
}
|
||||
|
||||
// ========================================
|
||||
// 초과 컴포넌트 영역 (오른쪽 패널)
|
||||
// 검토 필요 영역 (오른쪽 패널)
|
||||
// ========================================
|
||||
|
||||
interface OutOfBoundsPanelProps {
|
||||
interface ReviewPanelProps {
|
||||
components: PopComponentDefinitionV5[];
|
||||
selectedComponentId: string | null;
|
||||
onSelectComponent: (id: string | null) => void;
|
||||
onHideComponent?: (componentId: string) => void;
|
||||
}
|
||||
|
||||
function OutOfBoundsPanel({
|
||||
function ReviewPanel({
|
||||
components,
|
||||
selectedComponentId,
|
||||
onSelectComponent,
|
||||
onHideComponent,
|
||||
}: OutOfBoundsPanelProps) {
|
||||
}: ReviewPanelProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col rounded-lg border-2 border-dashed border-orange-300 bg-orange-50/50"
|
||||
className="flex flex-col rounded-lg border-2 border-dashed border-blue-300 bg-blue-50/50"
|
||||
style={{
|
||||
width: "200px",
|
||||
maxHeight: "300px",
|
||||
}}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-2 border-b border-orange-200 bg-orange-100/50 px-3 py-2 rounded-t-lg">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600" />
|
||||
<span className="text-xs font-semibold text-orange-700">
|
||||
화면 밖 ({components.length}개)
|
||||
<div className="flex items-center gap-2 border-b border-blue-200 bg-blue-100/50 px-3 py-2 rounded-t-lg">
|
||||
<AlertTriangle className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-xs font-semibold text-blue-700">
|
||||
검토 필요 ({components.length}개)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 컴포넌트 목록 */}
|
||||
<div className="flex-1 overflow-auto p-2 space-y-2">
|
||||
{components.map((comp) => (
|
||||
<OutOfBoundsItem
|
||||
<ReviewItem
|
||||
key={comp.id}
|
||||
component={comp}
|
||||
isSelected={selectedComponentId === comp.id}
|
||||
onSelect={() => onSelectComponent(comp.id)}
|
||||
onHide={() => onHideComponent?.(comp.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 안내 문구 */}
|
||||
<div className="border-t border-orange-200 px-3 py-2 bg-orange-50/80 rounded-b-lg">
|
||||
<p className="text-[10px] text-orange-600 leading-tight">
|
||||
드래그로 그리드 배치 / 클릭하면 숨김 처리
|
||||
<div className="border-t border-blue-200 px-3 py-2 bg-blue-50/80 rounded-b-lg">
|
||||
<p className="text-[10px] text-blue-600 leading-tight">
|
||||
자동 배치됨. 클릭하여 확인 후 편집 가능
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 검토 필요 아이템 (ReviewPanel 내부)
|
||||
// ========================================
|
||||
|
||||
interface ReviewItemProps {
|
||||
component: PopComponentDefinitionV5;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
function ReviewItem({
|
||||
component,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: ReviewItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-md border-2 p-2 cursor-pointer transition-all",
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-100 shadow-sm"
|
||||
: "border-blue-200 bg-white hover:border-blue-400 hover:bg-blue-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect();
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-medium text-blue-800 line-clamp-1">
|
||||
{component.label || component.id}
|
||||
</span>
|
||||
<span className="text-[10px] text-blue-600 bg-blue-50 rounded px-1.5 py-0.5 self-start">
|
||||
자동 배치됨
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 숨김 컴포넌트 영역 (오른쪽 패널)
|
||||
// ========================================
|
||||
|
|
@ -812,68 +913,6 @@ function HiddenPanel({
|
|||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 초과 컴포넌트 아이템 (드래그 가능)
|
||||
// ========================================
|
||||
|
||||
interface OutOfBoundsItemProps {
|
||||
component: PopComponentDefinitionV5;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
function OutOfBoundsItem({
|
||||
component,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onHide,
|
||||
}: OutOfBoundsItemProps) {
|
||||
const [{ isDragging }, drag] = useDrag(
|
||||
() => ({
|
||||
type: DND_ITEM_TYPES.MOVE_COMPONENT,
|
||||
item: {
|
||||
componentId: component.id,
|
||||
originalPosition: component.position,
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}),
|
||||
[component.id, component.position]
|
||||
);
|
||||
|
||||
// 클릭 시 숨김 처리
|
||||
const handleClick = () => {
|
||||
onSelect();
|
||||
onHide();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drag}
|
||||
className={cn(
|
||||
"rounded-md border-2 bg-white p-2 cursor-move transition-all",
|
||||
isSelected
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-gray-300 hover:border-gray-400",
|
||||
isDragging && "opacity-50"
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{/* 컴포넌트 이름 */}
|
||||
<div className="text-xs font-medium text-gray-700 truncate">
|
||||
{component.label || component.type}
|
||||
</div>
|
||||
|
||||
{/* 원본 위치 정보 */}
|
||||
<div className="text-[10px] text-gray-500 mt-1">
|
||||
원본: {component.position.col}열, {component.position.row}행
|
||||
({component.position.colSpan}×{component.position.rowSpan})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 숨김 컴포넌트 아이템 (드래그 가능)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
PopComponentDefinitionV5,
|
||||
PopGridPosition,
|
||||
GridMode,
|
||||
GapPreset,
|
||||
createEmptyPopLayoutV5,
|
||||
isV5Layout,
|
||||
addComponentToV5Layout,
|
||||
|
|
@ -132,10 +133,27 @@ export default function PopDesigner({
|
|||
|
||||
if (loadedLayout && isV5Layout(loadedLayout) && Object.keys(loadedLayout.components).length > 0) {
|
||||
// v5 레이아웃 로드
|
||||
// 기존 레이아웃 호환성: gapPreset이 없으면 기본값 추가
|
||||
if (!loadedLayout.settings.gapPreset) {
|
||||
loadedLayout.settings.gapPreset = "medium";
|
||||
}
|
||||
setLayout(loadedLayout);
|
||||
setHistory([loadedLayout]);
|
||||
setHistoryIndex(0);
|
||||
console.log(`POP 레이아웃 로드: ${Object.keys(loadedLayout.components).length}개 컴포넌트`);
|
||||
|
||||
// 기존 컴포넌트 ID에서 최대 숫자 추출하여 idCounter 설정 (중복 방지)
|
||||
const existingIds = Object.keys(loadedLayout.components);
|
||||
const maxId = existingIds.reduce((max, id) => {
|
||||
const match = id.match(/comp_(\d+)/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1], 10);
|
||||
return num > max ? num : max;
|
||||
}
|
||||
return max;
|
||||
}, 0);
|
||||
setIdCounter(maxId + 1);
|
||||
|
||||
console.log(`POP 레이아웃 로드: ${existingIds.length}개 컴포넌트, idCounter: ${maxId + 1}`);
|
||||
} else {
|
||||
// 새 화면 또는 빈 레이아웃
|
||||
const emptyLayout = createEmptyPopLayoutV5();
|
||||
|
|
@ -336,6 +354,23 @@ export default function PopDesigner({
|
|||
[layout, saveToHistory]
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// Gap 프리셋 관리
|
||||
// ========================================
|
||||
|
||||
const handleChangeGapPreset = useCallback((preset: GapPreset) => {
|
||||
const newLayout = {
|
||||
...layout,
|
||||
settings: {
|
||||
...layout.settings,
|
||||
gapPreset: preset,
|
||||
},
|
||||
};
|
||||
setLayout(newLayout);
|
||||
saveToHistory(newLayout);
|
||||
setHasChanges(true);
|
||||
}, [layout, saveToHistory]);
|
||||
|
||||
// ========================================
|
||||
// 모드별 오버라이드 관리
|
||||
// ========================================
|
||||
|
|
@ -598,6 +633,7 @@ export default function PopDesigner({
|
|||
onUnhideComponent={handleUnhideComponent}
|
||||
onLockLayout={handleLockLayout}
|
||||
onResetOverride={handleResetOverride}
|
||||
onChangeGapPreset={handleChangeGapPreset}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export default function ComponentEditorPanel({
|
|||
// 선택된 컴포넌트 없음
|
||||
if (!component) {
|
||||
return (
|
||||
<div className={cn("flex h-full flex-col", className)}>
|
||||
<div className={cn("flex h-full flex-col bg-white", className)}>
|
||||
<div className="border-b px-4 py-3">
|
||||
<h3 className="text-sm font-medium">속성</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
} from "../types/pop-layout";
|
||||
import {
|
||||
convertAndResolvePositions,
|
||||
isOutOfBounds,
|
||||
isOverlapping,
|
||||
getAllEffectivePositions,
|
||||
} from "../utils/gridUtils";
|
||||
|
|
@ -48,6 +47,10 @@ interface PopRendererProps {
|
|||
onComponentResize?: (componentId: string, newPosition: PopGridPosition) => void;
|
||||
/** 컴포넌트 크기 조정 완료 (히스토리 저장용) */
|
||||
onComponentResizeEnd?: (componentId: string) => void;
|
||||
/** Gap 오버라이드 (Gap 프리셋 적용된 값) */
|
||||
overrideGap?: number;
|
||||
/** Padding 오버라이드 (Gap 프리셋 적용된 값) */
|
||||
overridePadding?: number;
|
||||
/** 추가 className */
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -76,6 +79,8 @@ export default function PopRenderer({
|
|||
onComponentMove,
|
||||
onComponentResize,
|
||||
onComponentResizeEnd,
|
||||
overrideGap,
|
||||
overridePadding,
|
||||
className,
|
||||
}: PopRendererProps) {
|
||||
const { gridConfig, components, overrides } = layout;
|
||||
|
|
@ -84,26 +89,45 @@ export default function PopRenderer({
|
|||
const mode = currentMode || detectGridMode(viewportWidth);
|
||||
const breakpoint = GRID_BREAKPOINTS[mode];
|
||||
|
||||
// CSS Grid 스타일
|
||||
// Gap/Padding: 오버라이드 우선, 없으면 기본값 사용
|
||||
const finalGap = overrideGap !== undefined ? overrideGap : breakpoint.gap;
|
||||
const finalPadding = overridePadding !== undefined ? overridePadding : breakpoint.padding;
|
||||
|
||||
// 숨김 컴포넌트 ID 목록
|
||||
const hiddenIds = overrides?.[mode]?.hidden || [];
|
||||
|
||||
// 동적 행 수 계산 (가이드 셀 + Grid 스타일 공유, 숨김 컴포넌트 제외)
|
||||
const dynamicRowCount = useMemo(() => {
|
||||
const visibleComps = Object.values(components).filter(
|
||||
comp => !hiddenIds.includes(comp.id)
|
||||
);
|
||||
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
||||
const override = overrides?.[mode]?.positions?.[comp.id];
|
||||
const pos = override ? { ...comp.position, ...override } : comp.position;
|
||||
return Math.max(max, pos.row + pos.rowSpan);
|
||||
}, 1);
|
||||
return Math.max(10, maxRowEnd + 3);
|
||||
}, [components, overrides, mode, hiddenIds]);
|
||||
|
||||
// CSS Grid 스타일 (행 높이 강제 고정: 셀 크기 = 컴포넌트 크기의 기준)
|
||||
const gridStyle = useMemo((): React.CSSProperties => ({
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${breakpoint.columns}, 1fr)`,
|
||||
gridTemplateRows: `repeat(${dynamicRowCount}, ${breakpoint.rowHeight}px)`,
|
||||
gridAutoRows: `${breakpoint.rowHeight}px`,
|
||||
gap: `${breakpoint.gap}px`,
|
||||
padding: `${breakpoint.padding}px`,
|
||||
gap: `${finalGap}px`,
|
||||
padding: `${finalPadding}px`,
|
||||
minHeight: "100%",
|
||||
backgroundColor: "#ffffff",
|
||||
position: "relative",
|
||||
}), [breakpoint]);
|
||||
}), [breakpoint, finalGap, finalPadding, dynamicRowCount]);
|
||||
|
||||
// 그리드 가이드 셀 생성
|
||||
// 그리드 가이드 셀 생성 (동적 행 수)
|
||||
const gridCells = useMemo(() => {
|
||||
if (!isDesignMode || !showGridGuide) return [];
|
||||
|
||||
const cells = [];
|
||||
const rowCount = 20; // 충분한 행 수
|
||||
|
||||
for (let row = 1; row <= rowCount; row++) {
|
||||
for (let row = 1; row <= dynamicRowCount; row++) {
|
||||
for (let col = 1; col <= breakpoint.columns; col++) {
|
||||
cells.push({
|
||||
id: `cell-${col}-${row}`,
|
||||
|
|
@ -113,7 +137,7 @@ export default function PopRenderer({
|
|||
}
|
||||
}
|
||||
return cells;
|
||||
}, [isDesignMode, showGridGuide, breakpoint.columns]);
|
||||
}, [isDesignMode, showGridGuide, breakpoint.columns, dynamicRowCount]);
|
||||
|
||||
// visibility 체크
|
||||
const isVisible = (comp: PopComponentDefinitionV5): boolean => {
|
||||
|
|
@ -192,7 +216,7 @@ export default function PopRenderer({
|
|||
))}
|
||||
|
||||
{/* 컴포넌트 렌더링 (z-index로 위에 표시) */}
|
||||
{/* 디자인 모드에서는 초과 컴포넌트를 그리드에서 제외 (오른쪽 별도 영역에 표시) */}
|
||||
{/* v5.1: 자동 줄바꿈으로 모든 컴포넌트가 그리드 안에 배치됨 */}
|
||||
{Object.values(components).map((comp) => {
|
||||
// visibility 체크
|
||||
if (!isVisible(comp)) return null;
|
||||
|
|
@ -200,40 +224,47 @@ export default function PopRenderer({
|
|||
// 오버라이드 숨김 체크
|
||||
if (isHiddenByOverride(comp)) return null;
|
||||
|
||||
// 오버라이드 위치 가져오기 (있으면)
|
||||
const overridePos = overrides?.[mode]?.positions?.[comp.id];
|
||||
const overridePosition = overridePos
|
||||
? { ...comp.position, ...overridePos }
|
||||
: null;
|
||||
|
||||
// 초과 컴포넌트 체크 (오버라이드 고려)
|
||||
const outOfBounds = isOutOfBounds(comp.position, mode, overridePosition);
|
||||
|
||||
// 디자인 모드에서 초과 컴포넌트는 그리드에 렌더링하지 않음
|
||||
// (PopCanvas의 OutOfBoundsPanel에서 별도로 렌더링)
|
||||
if (isDesignMode && outOfBounds) return null;
|
||||
|
||||
const position = getEffectivePosition(comp);
|
||||
const positionStyle = convertPosition(position);
|
||||
const isSelected = selectedComponentId === comp.id;
|
||||
|
||||
// 디자인 모드에서는 드래그 가능한 컴포넌트, 뷰어 모드에서는 일반 컴포넌트
|
||||
if (isDesignMode) {
|
||||
return (
|
||||
<DraggableComponent
|
||||
key={comp.id}
|
||||
component={comp}
|
||||
position={position}
|
||||
positionStyle={positionStyle}
|
||||
isSelected={isSelected}
|
||||
isDesignMode={isDesignMode}
|
||||
breakpoint={breakpoint}
|
||||
viewportWidth={viewportWidth}
|
||||
allEffectivePositions={effectivePositionsMap}
|
||||
effectiveGap={finalGap}
|
||||
effectivePadding={finalPadding}
|
||||
onComponentClick={onComponentClick}
|
||||
onComponentMove={onComponentMove}
|
||||
onComponentResize={onComponentResize}
|
||||
onComponentResizeEnd={onComponentResizeEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 뷰어 모드: 드래그 없는 일반 렌더링
|
||||
return (
|
||||
<DraggableComponent
|
||||
<div
|
||||
key={comp.id}
|
||||
component={comp}
|
||||
position={position}
|
||||
positionStyle={positionStyle}
|
||||
isSelected={isSelected}
|
||||
isDesignMode={isDesignMode}
|
||||
isOutOfBounds={false}
|
||||
breakpoint={breakpoint}
|
||||
viewportWidth={viewportWidth}
|
||||
allEffectivePositions={effectivePositionsMap}
|
||||
onComponentClick={onComponentClick}
|
||||
onComponentMove={onComponentMove}
|
||||
onComponentResize={onComponentResize}
|
||||
onComponentResizeEnd={onComponentResizeEnd}
|
||||
/>
|
||||
className="relative rounded-lg border-2 border-gray-200 bg-white transition-all overflow-hidden z-10"
|
||||
style={positionStyle}
|
||||
>
|
||||
<ComponentContent
|
||||
component={comp}
|
||||
effectivePosition={position}
|
||||
isDesignMode={false}
|
||||
isSelected={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -250,10 +281,11 @@ interface DraggableComponentProps {
|
|||
positionStyle: React.CSSProperties;
|
||||
isSelected: boolean;
|
||||
isDesignMode: boolean;
|
||||
isOutOfBounds: boolean;
|
||||
breakpoint: GridBreakpoint;
|
||||
viewportWidth: number;
|
||||
allEffectivePositions: Map<string, PopGridPosition>;
|
||||
effectiveGap: number;
|
||||
effectivePadding: number;
|
||||
onComponentClick?: (componentId: string) => void;
|
||||
onComponentMove?: (componentId: string, newPosition: PopGridPosition) => void;
|
||||
onComponentResize?: (componentId: string, newPosition: PopGridPosition) => void;
|
||||
|
|
@ -266,10 +298,11 @@ function DraggableComponent({
|
|||
positionStyle,
|
||||
isSelected,
|
||||
isDesignMode,
|
||||
isOutOfBounds,
|
||||
breakpoint,
|
||||
viewportWidth,
|
||||
allEffectivePositions,
|
||||
effectiveGap,
|
||||
effectivePadding,
|
||||
onComponentClick,
|
||||
onComponentMove,
|
||||
onComponentResize,
|
||||
|
|
@ -294,10 +327,7 @@ function DraggableComponent({
|
|||
<div
|
||||
ref={isDesignMode ? drag : null}
|
||||
className={cn(
|
||||
"relative rounded-lg border-2 transition-all overflow-visible z-10",
|
||||
// 초과 컴포넌트 스타일 (디자인 모드에서만)
|
||||
isDesignMode && isOutOfBounds && "opacity-40 bg-gray-400/30",
|
||||
!isOutOfBounds && "bg-white",
|
||||
"relative rounded-lg border-2 transition-all overflow-hidden z-10 bg-white",
|
||||
isSelected
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-gray-200",
|
||||
|
|
@ -309,28 +339,24 @@ function DraggableComponent({
|
|||
e.stopPropagation();
|
||||
onComponentClick?.(component.id);
|
||||
}}
|
||||
title={
|
||||
isDesignMode && isOutOfBounds
|
||||
? `이 컴포넌트는 ${breakpoint.label}에서 표시되지 않습니다`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ComponentContent
|
||||
component={component}
|
||||
effectivePosition={position}
|
||||
isDesignMode={isDesignMode}
|
||||
isSelected={isSelected}
|
||||
isOutOfBounds={isOutOfBounds}
|
||||
/>
|
||||
|
||||
{/* 리사이즈 핸들 (선택된 컴포넌트만, 초과 아닐 때만) */}
|
||||
{isDesignMode && isSelected && !isOutOfBounds && onComponentResize && (
|
||||
{/* 리사이즈 핸들 (선택된 컴포넌트만) */}
|
||||
{isDesignMode && isSelected && onComponentResize && (
|
||||
<ResizeHandles
|
||||
component={component}
|
||||
position={position}
|
||||
breakpoint={breakpoint}
|
||||
viewportWidth={viewportWidth}
|
||||
allEffectivePositions={allEffectivePositions}
|
||||
effectiveGap={effectiveGap}
|
||||
effectivePadding={effectivePadding}
|
||||
onResize={onComponentResize}
|
||||
onResizeEnd={onComponentResizeEnd}
|
||||
/>
|
||||
|
|
@ -349,6 +375,8 @@ interface ResizeHandlesProps {
|
|||
breakpoint: GridBreakpoint;
|
||||
viewportWidth: number;
|
||||
allEffectivePositions: Map<string, PopGridPosition>;
|
||||
effectiveGap: number;
|
||||
effectivePadding: number;
|
||||
onResize: (componentId: string, newPosition: PopGridPosition) => void;
|
||||
onResizeEnd?: (componentId: string) => void;
|
||||
}
|
||||
|
|
@ -359,6 +387,8 @@ function ResizeHandles({
|
|||
breakpoint,
|
||||
viewportWidth,
|
||||
allEffectivePositions,
|
||||
effectiveGap,
|
||||
effectivePadding,
|
||||
onResize,
|
||||
onResizeEnd,
|
||||
}: ResizeHandlesProps) {
|
||||
|
|
@ -371,11 +401,11 @@ function ResizeHandles({
|
|||
const startColSpan = position.colSpan;
|
||||
const startRowSpan = position.rowSpan;
|
||||
|
||||
// 그리드 셀 크기 동적 계산
|
||||
// 그리드 셀 크기 동적 계산 (Gap 프리셋 적용된 값 사용)
|
||||
// 사용 가능한 너비 = 뷰포트 너비 - 양쪽 패딩 - gap*(칸수-1)
|
||||
const availableWidth = viewportWidth - breakpoint.padding * 2 - breakpoint.gap * (breakpoint.columns - 1);
|
||||
const cellWidth = availableWidth / breakpoint.columns + breakpoint.gap; // 셀 너비 + gap 단위
|
||||
const cellHeight = breakpoint.rowHeight + breakpoint.gap;
|
||||
const availableWidth = viewportWidth - effectivePadding * 2 - effectiveGap * (breakpoint.columns - 1);
|
||||
const cellWidth = availableWidth / breakpoint.columns + effectiveGap; // 셀 너비 + gap 단위
|
||||
const cellHeight = breakpoint.rowHeight + effectiveGap;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
|
|
@ -465,10 +495,9 @@ interface ComponentContentProps {
|
|||
effectivePosition: PopGridPosition;
|
||||
isDesignMode: boolean;
|
||||
isSelected: boolean;
|
||||
isOutOfBounds: boolean;
|
||||
}
|
||||
|
||||
function ComponentContent({ component, effectivePosition, isDesignMode, isSelected, isOutOfBounds }: ComponentContentProps) {
|
||||
function ComponentContent({ component, effectivePosition, isDesignMode, isSelected }: ComponentContentProps) {
|
||||
const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type;
|
||||
|
||||
// 디자인 모드: 플레이스홀더 표시
|
||||
|
|
@ -488,21 +517,11 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
|
|||
)}>
|
||||
{component.label || typeLabel}
|
||||
</span>
|
||||
|
||||
{/* 초과 표시 */}
|
||||
{isOutOfBounds && (
|
||||
<span className="ml-1 text-[9px] text-orange-600 font-semibold">
|
||||
밖
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="flex flex-1 items-center justify-center p-2">
|
||||
<span className={cn(
|
||||
"text-xs",
|
||||
isOutOfBounds ? "text-gray-500" : "text-gray-400"
|
||||
)}>
|
||||
<span className="text-xs text-gray-400">
|
||||
{typeLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -119,11 +119,12 @@ export interface GridBreakpoint {
|
|||
|
||||
/**
|
||||
* 브레이크포인트 상수
|
||||
* 업계 표준 (768px, 1024px) + 실제 기기 커버리지 기반
|
||||
*/
|
||||
export const GRID_BREAKPOINTS: Record<GridMode, GridBreakpoint> = {
|
||||
// 4~6인치 모바일 세로
|
||||
// 스마트폰 세로 (iPhone SE ~ Galaxy S25 Ultra)
|
||||
mobile_portrait: {
|
||||
maxWidth: 599,
|
||||
maxWidth: 479,
|
||||
columns: 4,
|
||||
rowHeight: 40,
|
||||
gap: 8,
|
||||
|
|
@ -131,10 +132,10 @@ export const GRID_BREAKPOINTS: Record<GridMode, GridBreakpoint> = {
|
|||
label: "모바일 세로 (4칸)",
|
||||
},
|
||||
|
||||
// 6~8인치 모바일 가로 / 작은 태블릿
|
||||
// 스마트폰 가로 + 소형 태블릿
|
||||
mobile_landscape: {
|
||||
minWidth: 600,
|
||||
maxWidth: 839,
|
||||
minWidth: 480,
|
||||
maxWidth: 767,
|
||||
columns: 6,
|
||||
rowHeight: 44,
|
||||
gap: 8,
|
||||
|
|
@ -142,9 +143,9 @@ export const GRID_BREAKPOINTS: Record<GridMode, GridBreakpoint> = {
|
|||
label: "모바일 가로 (6칸)",
|
||||
},
|
||||
|
||||
// 8~10인치 태블릿 세로
|
||||
// 태블릿 세로 (iPad Mini ~ iPad Pro)
|
||||
tablet_portrait: {
|
||||
minWidth: 840,
|
||||
minWidth: 768,
|
||||
maxWidth: 1023,
|
||||
columns: 8,
|
||||
rowHeight: 48,
|
||||
|
|
@ -153,7 +154,7 @@ export const GRID_BREAKPOINTS: Record<GridMode, GridBreakpoint> = {
|
|||
label: "태블릿 세로 (8칸)",
|
||||
},
|
||||
|
||||
// 10~14인치 태블릿 가로 (기본)
|
||||
// 태블릿 가로 + 데스크톱 (기본)
|
||||
tablet_landscape: {
|
||||
minWidth: 1024,
|
||||
columns: 12,
|
||||
|
|
@ -171,10 +172,11 @@ export const DEFAULT_GRID_MODE: GridMode = "tablet_landscape";
|
|||
|
||||
/**
|
||||
* 뷰포트 너비로 모드 감지
|
||||
* GRID_BREAKPOINTS와 일치하는 브레이크포인트 사용
|
||||
*/
|
||||
export function detectGridMode(viewportWidth: number): GridMode {
|
||||
if (viewportWidth < 600) return "mobile_portrait";
|
||||
if (viewportWidth < 840) return "mobile_landscape";
|
||||
if (viewportWidth < 480) return "mobile_portrait";
|
||||
if (viewportWidth < 768) return "mobile_landscape";
|
||||
if (viewportWidth < 1024) return "tablet_portrait";
|
||||
return "tablet_landscape";
|
||||
}
|
||||
|
|
@ -257,6 +259,28 @@ export interface PopComponentDefinitionV5 {
|
|||
config?: PopComponentConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gap 프리셋 타입
|
||||
*/
|
||||
export type GapPreset = "narrow" | "medium" | "wide";
|
||||
|
||||
/**
|
||||
* Gap 프리셋 설정
|
||||
*/
|
||||
export interface GapPresetConfig {
|
||||
multiplier: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gap 프리셋 상수
|
||||
*/
|
||||
export const GAP_PRESETS: Record<GapPreset, GapPresetConfig> = {
|
||||
narrow: { multiplier: 0.5, label: "좁게" },
|
||||
medium: { multiplier: 1.0, label: "보통" },
|
||||
wide: { multiplier: 1.5, label: "넓게" },
|
||||
};
|
||||
|
||||
/**
|
||||
* v5 전역 설정
|
||||
*/
|
||||
|
|
@ -266,6 +290,9 @@ export interface PopGlobalSettingsV5 {
|
|||
|
||||
// 모드
|
||||
mode: "normal" | "industrial";
|
||||
|
||||
// Gap 프리셋
|
||||
gapPreset: GapPreset; // 기본 "medium"
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -298,6 +325,7 @@ export const createEmptyPopLayoutV5 = (): PopLayoutDataV5 => ({
|
|||
settings: {
|
||||
touchTargetMin: 48,
|
||||
mode: "normal",
|
||||
gapPreset: "medium",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,37 @@ import {
|
|||
PopGridPosition,
|
||||
GridMode,
|
||||
GRID_BREAKPOINTS,
|
||||
GridBreakpoint,
|
||||
GapPreset,
|
||||
GAP_PRESETS,
|
||||
PopLayoutDataV5,
|
||||
PopComponentDefinitionV5,
|
||||
} from "../types/pop-layout";
|
||||
|
||||
// ========================================
|
||||
// Gap/Padding 조정
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Gap 프리셋에 따라 breakpoint의 gap/padding 조정
|
||||
*
|
||||
* @param base 기본 breakpoint 설정
|
||||
* @param preset Gap 프리셋 ("narrow" | "medium" | "wide")
|
||||
* @returns 조정된 breakpoint (gap, padding 계산됨)
|
||||
*/
|
||||
export function getAdjustedBreakpoint(
|
||||
base: GridBreakpoint,
|
||||
preset: GapPreset
|
||||
): GridBreakpoint {
|
||||
const multiplier = GAP_PRESETS[preset]?.multiplier || 1.0;
|
||||
|
||||
return {
|
||||
...base,
|
||||
gap: Math.round(base.gap * multiplier),
|
||||
padding: Math.max(8, Math.round(base.padding * multiplier)), // 최소 8px
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 그리드 위치 변환
|
||||
// ========================================
|
||||
|
|
@ -49,37 +76,106 @@ export function convertPositionToMode(
|
|||
|
||||
/**
|
||||
* 여러 컴포넌트를 모드별로 변환하고 겹침 해결
|
||||
*
|
||||
* v5.1 자동 줄바꿈:
|
||||
* - 원본 col > targetColumns인 컴포넌트는 자동으로 맨 아래에 배치
|
||||
* - 정보 손실 방지: 모든 컴포넌트가 그리드 안에 배치됨
|
||||
*/
|
||||
export function convertAndResolvePositions(
|
||||
components: Array<{ id: string; position: PopGridPosition }>,
|
||||
targetMode: GridMode
|
||||
): Array<{ id: string; position: PopGridPosition }> {
|
||||
// 엣지 케이스: 빈 배열
|
||||
if (components.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetColumns = GRID_BREAKPOINTS[targetMode].columns;
|
||||
|
||||
// 1단계: 각 컴포넌트를 비율로 변환
|
||||
// 1단계: 각 컴포넌트를 비율로 변환 (원본 col 보존)
|
||||
const converted = components.map(comp => ({
|
||||
id: comp.id,
|
||||
position: convertPositionToMode(comp.position, targetMode),
|
||||
originalCol: comp.position.col, // 원본 col 보존
|
||||
}));
|
||||
|
||||
// 2단계: 겹침 해결 (아래로 밀기)
|
||||
return resolveOverlaps(converted, targetColumns);
|
||||
// 2단계: 정상 컴포넌트 vs 초과 컴포넌트 분리
|
||||
const normalComponents = converted.filter(c => c.originalCol <= targetColumns);
|
||||
const overflowComponents = converted.filter(c => c.originalCol > targetColumns);
|
||||
|
||||
// 3단계: 정상 컴포넌트의 최대 row 계산
|
||||
const maxRow = normalComponents.length > 0
|
||||
? Math.max(...normalComponents.map(c => c.position.row + c.position.rowSpan - 1))
|
||||
: 0;
|
||||
|
||||
// 4단계: 초과 컴포넌트들을 맨 아래에 순차 배치
|
||||
let currentRow = maxRow + 1;
|
||||
const wrappedComponents = overflowComponents.map(comp => {
|
||||
const wrappedPosition: PopGridPosition = {
|
||||
col: 1, // 왼쪽 끝부터 시작
|
||||
row: currentRow,
|
||||
colSpan: Math.min(comp.position.colSpan, targetColumns), // 최대 칸 수 제한
|
||||
rowSpan: comp.position.rowSpan,
|
||||
};
|
||||
currentRow += comp.position.rowSpan; // 다음 행으로 이동
|
||||
|
||||
return {
|
||||
id: comp.id,
|
||||
position: wrappedPosition,
|
||||
};
|
||||
});
|
||||
|
||||
// 5단계: 정상 + 줄바꿈 컴포넌트 병합
|
||||
const adjusted = [
|
||||
...normalComponents.map(c => ({ id: c.id, position: c.position })),
|
||||
...wrappedComponents,
|
||||
];
|
||||
|
||||
// 6단계: 겹침 해결 (아래로 밀기)
|
||||
return resolveOverlaps(adjusted, targetColumns);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 초과 컴포넌트 감지
|
||||
// 검토 필요 판별
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* 컴포넌트가 현재 모드에서 화면 밖으로 초과하는지 확인
|
||||
* 컴포넌트가 현재 모드에서 "검토 필요" 상태인지 확인
|
||||
*
|
||||
* 판단 우선순위:
|
||||
* 1. 오버라이드 위치가 있으면 오버라이드 위치로 판단
|
||||
* 2. 오버라이드 없으면 원본 위치로 판단
|
||||
* v5.1 검토 필요 기준:
|
||||
* - 12칸 모드(기본 모드)가 아님
|
||||
* - 해당 모드에서 오버라이드가 없음 (아직 편집 안 함)
|
||||
*
|
||||
* @param originalPosition 원본 위치 (12칸 기준)
|
||||
* @param currentMode 현재 그리드 모드
|
||||
* @param overridePosition 오버라이드 위치 (있으면)
|
||||
* @param hasOverride 해당 모드에서 오버라이드 존재 여부
|
||||
* @returns true = 검토 필요, false = 검토 완료 또는 불필요
|
||||
*/
|
||||
export function needsReview(
|
||||
currentMode: GridMode,
|
||||
hasOverride: boolean
|
||||
): boolean {
|
||||
const targetColumns = GRID_BREAKPOINTS[currentMode].columns;
|
||||
|
||||
// 12칸 모드는 기본 모드이므로 검토 불필요
|
||||
if (targetColumns === 12) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 오버라이드가 있으면 이미 편집함 → 검토 완료
|
||||
if (hasOverride) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 오버라이드 없으면 → 검토 필요
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated v5.1부터 needsReview() 사용 권장
|
||||
*
|
||||
* 기존 isOutOfBounds는 "화면 밖" 개념이었으나,
|
||||
* v5.1 자동 줄바꿈으로 인해 모든 컴포넌트가 그리드 안에 배치됩니다.
|
||||
* 대신 needsReview()로 "검토 필요" 여부를 판별하세요.
|
||||
*/
|
||||
export function isOutOfBounds(
|
||||
originalPosition: PopGridPosition,
|
||||
|
|
@ -95,11 +191,10 @@ export function isOutOfBounds(
|
|||
|
||||
// 오버라이드가 있으면 오버라이드 위치로 판단
|
||||
if (overridePosition) {
|
||||
// 오버라이드 시작 열이 범위 내면 "초과 아님"
|
||||
return overridePosition.col > targetColumns;
|
||||
}
|
||||
|
||||
// 오버라이드 없으면 원본 시작 열이 현재 모드 칸 수를 초과하면 "화면 밖"
|
||||
// 오버라이드 없으면 원본 col로 판단
|
||||
return originalPosition.col > targetColumns;
|
||||
}
|
||||
|
||||
|
|
@ -421,7 +516,10 @@ export function getEffectiveComponentPosition(
|
|||
|
||||
/**
|
||||
* 모든 컴포넌트의 유효 위치를 일괄 계산합니다.
|
||||
* 숨김 처리된 컴포넌트와 화면 밖 컴포넌트는 제외됩니다.
|
||||
* 숨김 처리된 컴포넌트는 제외됩니다.
|
||||
*
|
||||
* v5.1: 자동 줄바꿈 시스템으로 인해 모든 컴포넌트가 그리드 안에 배치되므로
|
||||
* "화면 밖" 개념이 제거되었습니다.
|
||||
*/
|
||||
export function getAllEffectivePositions(
|
||||
layout: PopLayoutDataV5,
|
||||
|
|
@ -453,16 +551,10 @@ export function getAllEffectivePositions(
|
|||
autoResolvedPositions
|
||||
);
|
||||
|
||||
// v5.1: 자동 줄바꿈으로 인해 모든 컴포넌트가 그리드 안에 있음
|
||||
// 따라서 추가 필터링 불필요
|
||||
if (position) {
|
||||
// 화면 밖 컴포넌트도 제외 (오버라이드 위치 고려)
|
||||
const overridePos = layout.overrides?.[mode]?.positions?.[componentId];
|
||||
const overridePosition = overridePos
|
||||
? { ...layout.components[componentId].position, ...overridePos }
|
||||
: null;
|
||||
|
||||
if (!isOutOfBounds(layout.components[componentId].position, mode, overridePosition)) {
|
||||
result.set(componentId, position);
|
||||
}
|
||||
result.set(componentId, position);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@ export interface ResponsiveMode {
|
|||
|
||||
// ========================================
|
||||
// 브레이크포인트 (화면 너비 기준)
|
||||
// GRID_BREAKPOINTS와 일치해야 함!
|
||||
// ========================================
|
||||
const BREAKPOINTS = {
|
||||
// 모바일: 0 ~ 767px
|
||||
// 태블릿: 768px 이상
|
||||
TABLET_MIN: 768,
|
||||
// mobile_portrait: ~479px (4칸)
|
||||
// mobile_landscape: 480~767px (6칸)
|
||||
// tablet_portrait: 768~1023px (8칸)
|
||||
// tablet_landscape: 1024px~ (12칸)
|
||||
TABLET_MIN: 768, // 768px 이상이면 tablet
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# POP 화면 시스템 아키텍처
|
||||
|
||||
**최종 업데이트: 2026-02-05 (v5 그리드 시스템)**
|
||||
**최종 업데이트: 2026-02-06 (v5.2 브레이크포인트 재설계 + 세로 자동 확장)**
|
||||
|
||||
POP(Point of Production) 화면은 모바일/태블릿 환경에 최적화된 터치 기반 화면 시스템입니다.
|
||||
|
||||
|
|
@ -79,14 +79,19 @@ handleUndo() / handleRedo() // 히스토리
|
|||
// DnD 설정
|
||||
const DND_ITEM_TYPES = { COMPONENT: "component" };
|
||||
|
||||
// 뷰포트 프리셋 (4개 모드)
|
||||
// 뷰포트 프리셋 (4개 모드) - height 제거됨 (세로 무한 스크롤)
|
||||
const VIEWPORT_PRESETS = [
|
||||
{ id: "mobile_portrait", width: 375, columns: 4 },
|
||||
{ id: "mobile_landscape", width: 667, columns: 6 },
|
||||
{ id: "tablet_portrait", width: 768, columns: 8 },
|
||||
{ id: "mobile_landscape", width: 600, columns: 6 },
|
||||
{ id: "tablet_portrait", width: 834, columns: 8 },
|
||||
{ id: "tablet_landscape", width: 1024, columns: 12 },
|
||||
];
|
||||
|
||||
// 세로 자동 확장
|
||||
const MIN_CANVAS_HEIGHT = 600; // 최소 캔버스 높이
|
||||
const CANVAS_EXTRA_ROWS = 3; // 항상 유지되는 여유 행 수
|
||||
const dynamicCanvasHeight = useMemo(() => { ... }, []);
|
||||
|
||||
// 기능
|
||||
- useDrop(): 팔레트에서 컴포넌트 드롭
|
||||
- handleWheel(): 줌 (30%~150%)
|
||||
|
|
@ -149,14 +154,22 @@ const convertPosition = (pos: PopGridPosition, targetMode: GridMode) => {
|
|||
// 그리드 모드
|
||||
type GridMode = "mobile_portrait" | "mobile_landscape" | "tablet_portrait" | "tablet_landscape";
|
||||
|
||||
// 브레이크포인트 설정
|
||||
// 브레이크포인트 설정 (2026-02-06 재설계)
|
||||
const GRID_BREAKPOINTS = {
|
||||
mobile_portrait: { columns: 4, rowHeight: 48, gap: 8, padding: 12 },
|
||||
mobile_landscape: { columns: 6, rowHeight: 44, gap: 8, padding: 16 },
|
||||
tablet_portrait: { columns: 8, rowHeight: 52, gap: 12, padding: 20 },
|
||||
tablet_landscape: { columns: 12, rowHeight: 56, gap: 12, padding: 24 },
|
||||
mobile_portrait: { columns: 4, maxWidth: 479, gap: 8, padding: 12 },
|
||||
mobile_landscape: { columns: 6, minWidth: 480, maxWidth: 767, gap: 8, padding: 16 },
|
||||
tablet_portrait: { columns: 8, minWidth: 768, maxWidth: 1023, gap: 12, padding: 20 },
|
||||
tablet_landscape: { columns: 12, minWidth: 1024, gap: 12, padding: 24 },
|
||||
};
|
||||
|
||||
// 모드 감지 (순수 너비 기반)
|
||||
function detectGridMode(viewportWidth: number): GridMode {
|
||||
if (viewportWidth < 480) return "mobile_portrait";
|
||||
if (viewportWidth < 768) return "mobile_landscape";
|
||||
if (viewportWidth < 1024) return "tablet_portrait";
|
||||
return "tablet_landscape";
|
||||
}
|
||||
|
||||
// 레이아웃 데이터
|
||||
interface PopLayoutDataV5 {
|
||||
version: "pop-5.0";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,266 @@
|
|||
|
||||
---
|
||||
|
||||
## [2026-02-06] v5.2.1 그리드 셀 크기 강제 고정
|
||||
|
||||
### 배경 (왜 이 작업이 필요했는가)
|
||||
|
||||
**문제 상황**:
|
||||
- 4칸 모드에서 특정 행의 가이드 셀이 다른 행보다 작게 표시됨
|
||||
- `gridAutoRows`는 최소 높이만 보장하여, 컴포넌트 콘텐츠가 행 높이를 밀어내면 인접 빈 셀도 영향받음
|
||||
- "셀의 크기 = 컴포넌트의 크기"라는 핵심 설계 원칙이 시각적으로 깨짐
|
||||
|
||||
### Changed
|
||||
|
||||
- **gridAutoRows → gridTemplateRows** (PopRenderer.tsx)
|
||||
```typescript
|
||||
// 변경 전: 최소 높이만 보장 (콘텐츠에 따라 늘어남)
|
||||
gridAutoRows: `${breakpoint.rowHeight}px`
|
||||
|
||||
// 변경 후: 행 높이 강제 고정
|
||||
gridTemplateRows: `repeat(${dynamicRowCount}, ${breakpoint.rowHeight}px)`
|
||||
gridAutoRows: `${breakpoint.rowHeight}px` // 동적 추가행 대비 유지
|
||||
```
|
||||
|
||||
- **dynamicRowCount 분리** (PopRenderer.tsx)
|
||||
- gridCells 내부 → 독립 useMemo로 분리
|
||||
- gridStyle과 gridCells에서 공유
|
||||
|
||||
- **컴포넌트 overflow 변경** (PopRenderer.tsx)
|
||||
- `overflow-visible` → `overflow-hidden`
|
||||
- 컴포넌트 콘텐츠가 셀 경계를 벗어나지 않도록 강제
|
||||
|
||||
### Fixed
|
||||
|
||||
- **PopRenderer dynamicRowCount에서 숨김 컴포넌트 포함 문제**
|
||||
- PopCanvas는 숨김 제외하여 높이 계산, PopRenderer는 포함하여 계산 → 기준 불일치
|
||||
- PopRenderer에도 숨김 필터 추가, 여유행 +5 → +3으로 통일
|
||||
|
||||
- **디버깅 console.log 잔존** (PopCanvas.tsx)
|
||||
- reviewComponents useMemo 내 console.log 2개 삭제
|
||||
|
||||
- **뷰어 viewportWidth 선언 순서** (page.tsx)
|
||||
- currentModeKey보다 뒤에 선언되어 있던 viewportWidth를 앞으로 이동
|
||||
|
||||
### 수정 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `PopRenderer.tsx` | gridTemplateRows 강제 고정, dynamicRowCount 분리, overflow-hidden, 숨김 필터 추가 |
|
||||
| `PopCanvas.tsx` | 디버깅 console.log 삭제 |
|
||||
| `page.tsx (뷰어)` | viewportWidth 선언 순서 수정 |
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-06] v5.2 브레이크포인트 재설계 + 세로 자동 확장
|
||||
|
||||
### 배경 (왜 이 작업이 필요했는가)
|
||||
|
||||
**문제 상황**:
|
||||
- 뷰어에서 브라우저 수동 리사이즈 시 768~839px 구간에서 모드 불일치
|
||||
- useResponsiveMode 훅과 GRID_BREAKPOINTS 상수 간 기준 불일치
|
||||
- 기존 브레이크포인트가 실제 기기 뷰포트와 맞지 않음
|
||||
|
||||
**사용자 요구사항**:
|
||||
- "현장 모바일 기기 8~14인치, 핸드폰은 아이폰 미니 ~ 갤럭시 울트라"
|
||||
- "세로는 신경쓸 필요 없고 무한 스크롤 가능해야 함"
|
||||
|
||||
### Changed
|
||||
|
||||
- **브레이크포인트 재설계** (pop-layout.ts)
|
||||
| 모드 | 변경 전 | 변경 후 | 근거 |
|
||||
|------|--------|--------|------|
|
||||
| mobile_portrait | ~599px | ~479px | 스마트폰 세로 최대 440px |
|
||||
| mobile_landscape | 600~839px | 480~767px | 스마트폰 가로 |
|
||||
| tablet_portrait | 840~1023px | 768~1023px | iPad Mini 768px 포함 |
|
||||
| tablet_landscape | 1024px+ | 동일 | - |
|
||||
|
||||
- **detectGridMode() 조건 수정** (pop-layout.ts)
|
||||
```typescript
|
||||
if (viewportWidth < 480) return "mobile_portrait"; // was 600
|
||||
if (viewportWidth < 768) return "mobile_landscape"; // was 840
|
||||
if (viewportWidth < 1024) return "tablet_portrait";
|
||||
```
|
||||
|
||||
- **BREAKPOINTS.TABLET_MIN 변경** (useDeviceOrientation.ts)
|
||||
- 768 (was 840)
|
||||
|
||||
- **VIEWPORT_PRESETS에서 height 제거** (PopCanvas.tsx)
|
||||
- width만 유지, 세로는 무한 스크롤
|
||||
|
||||
### Added
|
||||
|
||||
- **세로 자동 확장** (PopCanvas.tsx)
|
||||
- `MIN_CANVAS_HEIGHT = 600`: 최소 캔버스 높이
|
||||
- `CANVAS_EXTRA_ROWS = 3`: 항상 유지되는 여유 행 수
|
||||
- `dynamicCanvasHeight`: 컴포넌트 배치 기반 동적 계산
|
||||
|
||||
- **격자 셀 동적 계산** (PopRenderer.tsx)
|
||||
- 고정 20행 → maxRowEnd + 5 동적 계산
|
||||
|
||||
- **뷰어 일관성 확보** (page.tsx)
|
||||
- 프리뷰 모드: useResponsiveModeWithOverride 유지
|
||||
- 일반 모드: detectGridMode(viewportWidth) 직접 사용
|
||||
|
||||
### Fixed
|
||||
|
||||
- **뷰어 반응형 모드 불일치**
|
||||
- 768~839px 구간에서 6칸/8칸 모드 불일치 해결
|
||||
|
||||
- **hiddenComponentIds 중복 정의 에러**
|
||||
- 라인 410-412 중복 useMemo 제거
|
||||
|
||||
### Technical Details
|
||||
|
||||
```
|
||||
브레이크포인트 재설계 근거 (실제 기기 CSS 뷰포트):
|
||||
|
||||
| 기기 | CSS 뷰포트 너비 |
|
||||
|------|----------------|
|
||||
| iPhone SE | 375px |
|
||||
| iPhone 16 Pro | 402px |
|
||||
| Galaxy S25 Ultra | 440px |
|
||||
| iPad Mini 7 | 768px |
|
||||
| iPad Pro 11 | 834px (세로), 1194px (가로) |
|
||||
| iPad Pro 13 | 1024px (세로), 1366px (가로) |
|
||||
|
||||
→ 768px, 1024px가 업계 표준 (Tailwind, Bootstrap 동일)
|
||||
```
|
||||
|
||||
```
|
||||
세로 자동 확장 로직:
|
||||
|
||||
const dynamicCanvasHeight = useMemo(() => {
|
||||
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
||||
return Math.max(max, comp.row + comp.rowSpan);
|
||||
}, 1);
|
||||
|
||||
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS; // +3행 여유
|
||||
const height = totalRows * (rowHeight + gap) + padding * 2;
|
||||
|
||||
return Math.max(MIN_CANVAS_HEIGHT, height); // 최소 600px
|
||||
}, [...]);
|
||||
```
|
||||
|
||||
### 수정 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `pop-layout.ts` | GRID_BREAKPOINTS 값 수정, detectGridMode() 조건 수정 |
|
||||
| `useDeviceOrientation.ts` | BREAKPOINTS.TABLET_MIN = 768 |
|
||||
| `PopCanvas.tsx` | VIEWPORT_PRESETS height 제거, dynamicCanvasHeight 추가 |
|
||||
| `PopRenderer.tsx` | gridCells 동적 행 수 계산 |
|
||||
| `page.tsx (뷰어)` | detectGridMode() 사용 |
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-06] v5.1 자동 줄바꿈 + 검토 필요 시스템
|
||||
|
||||
### 배경 (왜 이 작업이 필요했는가)
|
||||
|
||||
**문제 상황**:
|
||||
- 12칸에서 배치한 컴포넌트가 4칸 모드로 전환하면 "화면 밖" 패널로 이동하여 뷰어에서 안 보임
|
||||
- 사용자가 모든 모드를 수동으로 편집해야 하는 부담
|
||||
- "화면 밖" 개념이 실제로는 "검토 필요" 알림 역할이었음
|
||||
|
||||
**해결 방향**:
|
||||
- 자동 줄바꿈: col > maxCol인 컴포넌트를 자동으로 맨 아래에 배치
|
||||
- 정보 손실 방지: 모든 컴포넌트가 항상 그리드 안에 표시됨
|
||||
- 검토 필요 알림: 오버라이드 없으면 "검토 필요" 표시 (자동 배치 상태)
|
||||
|
||||
### Added
|
||||
|
||||
- **자동 줄바꿈 로직** (gridUtils.ts)
|
||||
- `convertAndResolvePositions()` 수정
|
||||
- 원본 col 보존 로직 추가
|
||||
- 정상 컴포넌트 vs 초과 컴포넌트 분리
|
||||
- 초과 컴포넌트를 맨 아래에 순차 배치 (col=1, row=맨아래+1)
|
||||
- colSpan 자동 축소 (targetColumns 초과 방지)
|
||||
|
||||
- **검토 필요 판별 함수** (gridUtils.ts)
|
||||
- `needsReview()` 신규 함수
|
||||
- 기준: 12칸 아니고 + 오버라이드 없으면 → 검토 필요
|
||||
- 간단한 로직: "이 모드에서 편집했냐 안 했냐"
|
||||
|
||||
- **검토 필요 패널** (PopCanvas.tsx)
|
||||
- `ReviewPanel`: "화면 밖" → "검토 필요"로 이름 변경
|
||||
- `ReviewItem`: 클릭 시 해당 컴포넌트 선택 (드래그 없음)
|
||||
- 자동 배치 뱃지 표시
|
||||
- 파란색 테마 (경고 아닌 안내 느낌)
|
||||
|
||||
### Changed
|
||||
|
||||
- **isOutOfBounds() Deprecated** (gridUtils.ts)
|
||||
- `@deprecated` 주석 추가
|
||||
- needsReview()로 대체 권장
|
||||
- 하위 호환을 위해 함수는 유지
|
||||
|
||||
- **"화면 밖" 패널 역할 변경** (PopCanvas.tsx)
|
||||
- 기존: col > maxCol → 화면 밖 (드래그로 복원)
|
||||
- 변경: 오버라이드 없음 → 검토 필요 (클릭으로 선택)
|
||||
- 숨김 기능과 완전히 분리 (별도 유지)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **정보 손실 문제 해결**
|
||||
- 모든 컴포넌트가 항상 그리드 안에 배치됨
|
||||
- 뷰어에서도 자동 배치가 적용되어 모두 표시됨
|
||||
|
||||
### Technical Details
|
||||
|
||||
```
|
||||
자동 줄바꿈 로직:
|
||||
|
||||
1. convertAndResolvePositions() 호출
|
||||
components: [ {id: "A", position: {col:1, ...}}, {id: "B", position: {col:5, ...}} ]
|
||||
targetMode: "mobile_portrait" (4칸)
|
||||
|
||||
2. 비율 변환 + 원본 col 보존
|
||||
converted: [
|
||||
{id: "A", position: {col:1, ...}, originalCol: 1},
|
||||
{id: "B", position: {col:2, ...}, originalCol: 5} // col은 변환됨, 원본은 5
|
||||
]
|
||||
|
||||
3. 정상 vs 초과 분리
|
||||
normalComponents: [A] // originalCol ≤ 4
|
||||
overflowComponents: [B] // originalCol > 4
|
||||
|
||||
4. 맨 아래 배치
|
||||
maxRow = A의 (row + rowSpan - 1) = 1
|
||||
B: col=1, row=2 (맨 아래에 자동 배치)
|
||||
|
||||
5. 겹침 해결
|
||||
resolveOverlaps([A, B], 4) // 최종 위치 확정
|
||||
|
||||
6. 검토 필요 판별
|
||||
needsReview("mobile_portrait", false) // 오버라이드 없음 → true
|
||||
→ ReviewPanel에 B 표시
|
||||
```
|
||||
|
||||
```
|
||||
검토 필요 vs 숨김:
|
||||
|
||||
구분 | 검토 필요 | 숨김
|
||||
------------- | ---------------------- | -------------------
|
||||
역할 | 자동 배치 알림 | 의도적 숨김
|
||||
뷰어에서 | 보임 (자동 배치) | 안 보임
|
||||
디자이너에서 | ReviewPanel 표시 | HiddenPanel 표시
|
||||
판단 기준 | 오버라이드 없음 | hidden 배열에 ID
|
||||
색상 테마 | 파란색 (안내) | 회색 (제외)
|
||||
```
|
||||
|
||||
### 수정 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `gridUtils.ts` | convertAndResolvePositions 자동 줄바꿈, needsReview 추가, isOutOfBounds deprecated |
|
||||
| `PopCanvas.tsx` | OutOfBoundsPanel → ReviewPanel 변경, needsReview 필터링 |
|
||||
| `PopRenderer.tsx` | isOutOfBounds import 제거 (사용 안 함) |
|
||||
| `README.md` | v5.1 버전 표시, 최신 기능 요약 |
|
||||
| `CHANGELOG.md` | v5.1 항목 추가 |
|
||||
|
||||
---
|
||||
|
||||
## [2026-02-05 심야] 반응형 레이아웃 + 숨김 기능 완성
|
||||
|
||||
### 배경 (왜 이 작업이 필요했는가)
|
||||
|
|
@ -71,6 +331,10 @@
|
|||
- **getAllEffectivePositions에 숨김 컴포넌트 포함**
|
||||
- 해결: 숨김 및 화면밖 컴포넌트를 결과에서 제외
|
||||
|
||||
- **Expected drag drop context 에러 (뷰어 페이지)**
|
||||
- 원인: `DraggableComponent`에서 `useDrag` 훅이 `DndProvider` 없이 호출됨
|
||||
- 해결: `isDesignMode=false`일 때 `DraggableComponent` 대신 일반 `div`로 렌더링
|
||||
|
||||
### Changed
|
||||
|
||||
- **PopModeOverrideV5 타입 확장**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# POP 파일 상세 목록
|
||||
|
||||
**최종 업데이트: 2026-02-05 저녁 (드래그앤드롭 수정)**
|
||||
**최종 업데이트: 2026-02-06 (v5.2 브레이크포인트 재설계 + 세로 자동 확장)**
|
||||
|
||||
이 문서는 POP 화면 시스템과 관련된 모든 파일을 나열하고 각 파일의 역할을 설명합니다.
|
||||
|
||||
|
|
@ -172,15 +172,20 @@ interface PopCanvasProps {
|
|||
}
|
||||
```
|
||||
|
||||
**뷰포트 프리셋**:
|
||||
**뷰포트 프리셋** (v5.2 - height 제거됨, 세로 자동 확장):
|
||||
|
||||
```typescript
|
||||
const VIEWPORT_PRESETS = [
|
||||
{ id: "mobile_portrait", label: "모바일 세로", width: 375, height: 667 }, // 4칸
|
||||
{ id: "mobile_landscape", label: "모바일 가로", width: 667, height: 375 }, // 6칸
|
||||
{ id: "tablet_portrait", label: "태블릿 세로", width: 768, height: 1024 }, // 8칸
|
||||
{ id: "tablet_landscape", label: "태블릿 가로", width: 1024, height: 768 }, // 12칸
|
||||
{ id: "mobile_portrait", label: "모바일 세로", width: 375, columns: 4 },
|
||||
{ id: "mobile_landscape", label: "모바일 가로", width: 600, columns: 6 },
|
||||
{ id: "tablet_portrait", label: "태블릿 세로", width: 834, columns: 8 },
|
||||
{ id: "tablet_landscape", label: "태블릿 가로", width: 1024, columns: 12 },
|
||||
];
|
||||
|
||||
// 세로 자동 확장
|
||||
const MIN_CANVAS_HEIGHT = 600;
|
||||
const CANVAS_EXTRA_ROWS = 3;
|
||||
const dynamicCanvasHeight = useMemo(() => { ... }, []);
|
||||
```
|
||||
|
||||
**제공 기능**:
|
||||
|
|
@ -251,7 +256,7 @@ export { default as ComponentEditorPanel, default } from "./ComponentEditorPanel
|
|||
|------|------|
|
||||
| 역할 | v5 레이아웃 CSS Grid 렌더러 + 격자 셀 |
|
||||
| 입력 | PopLayoutDataV5, viewportWidth, currentMode, showGridGuide |
|
||||
| 격자 | 12x20 = 240개 실제 DOM 셀 (CSS Grid 좌표계) |
|
||||
| 격자 | 동적 행 수 (컴포넌트 배치에 따라 자동 계산, CSS Grid 좌표계) |
|
||||
|
||||
**핵심 Props**:
|
||||
|
||||
|
|
@ -272,16 +277,22 @@ interface PopRendererProps {
|
|||
**격자 셀 렌더링**:
|
||||
|
||||
```typescript
|
||||
// 12x20 = 240개 셀 생성
|
||||
// 동적 행 수 계산 (컴포넌트 배치 기반)
|
||||
const gridCells = useMemo(() => {
|
||||
const maxRowEnd = Object.values(components).reduce((max, comp) => {
|
||||
const pos = getEffectivePosition(comp);
|
||||
return Math.max(max, pos.row + pos.rowSpan);
|
||||
}, 1);
|
||||
const rowCount = Math.max(10, maxRowEnd + 5);
|
||||
|
||||
const cells = [];
|
||||
for (let row = 1; row <= 20; row++) {
|
||||
for (let col = 1; col <= 12; col++) {
|
||||
cells.push({ id: `${col}-${row}`, col, row });
|
||||
for (let row = 1; row <= rowCount; row++) {
|
||||
for (let col = 1; col <= breakpoint.columns; col++) {
|
||||
cells.push({ id: `cell-${col}-${row}`, col, row });
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}, []);
|
||||
}, [components, overrides, mode, breakpoint.columns]);
|
||||
|
||||
// 컴포넌트와 동일한 CSS Grid 좌표계로 렌더링
|
||||
{showGridGuide && gridCells.map(cell => (
|
||||
|
|
|
|||
|
|
@ -19,11 +19,20 @@
|
|||
|
||||
| 기능 | 파일 | 함수/컴포넌트 | 설명 |
|
||||
|------|------|--------------|------|
|
||||
| 격자 셀 | PopRenderer.tsx | `gridCells` | CSS Grid 기반 격자선 |
|
||||
| 격자 셀 | PopRenderer.tsx | `gridCells` | CSS Grid 기반 격자선 (동적 행 수) |
|
||||
| 열 라벨 | PopCanvas.tsx | `gridLabels.columns` | 1~12 표시 |
|
||||
| 행 라벨 | PopCanvas.tsx | `gridLabels.rows` | 1~20 표시 |
|
||||
| 행 라벨 | PopCanvas.tsx | `gridLabels.rows` | 동적 계산 (dynamicCanvasHeight 기반) |
|
||||
| 토글 | PopCanvas.tsx | `showGridGuide` 상태 | 격자 ON/OFF |
|
||||
|
||||
## 세로 자동 확장
|
||||
|
||||
| 기능 | 파일 | 함수/컴포넌트 | 설명 |
|
||||
|------|------|--------------|------|
|
||||
| 동적 높이 | PopCanvas.tsx | `dynamicCanvasHeight` | 컴포넌트 배치 기반 자동 계산 |
|
||||
| 최소 높이 | PopCanvas.tsx | `MIN_CANVAS_HEIGHT` | 600px 보장 |
|
||||
| 여유 행 | PopCanvas.tsx | `CANVAS_EXTRA_ROWS` | 항상 3행 추가 |
|
||||
| 격자 행 수 | PopRenderer.tsx | `gridCells` | maxRowEnd + 5 동적 계산 |
|
||||
|
||||
## 드래그 앤 드롭
|
||||
|
||||
| 기능 | 파일 | 함수/컴포넌트 | 설명 |
|
||||
|
|
@ -71,9 +80,26 @@
|
|||
|
||||
| 기능 | 파일 | 함수/컴포넌트 | 설명 |
|
||||
|------|------|--------------|------|
|
||||
| 프리셋 전환 | PopCanvas.tsx | `VIEWPORT_PRESETS` | 4개 모드 |
|
||||
| 프리셋 전환 | PopCanvas.tsx | `VIEWPORT_PRESETS` | 4개 모드 (width만, height 제거) |
|
||||
| 줌 컨트롤 | PopCanvas.tsx | `canvasScale` | 30%~150% |
|
||||
| 패닝 | PopCanvas.tsx | Space + 드래그 | 캔버스 이동 |
|
||||
| 모드 감지 | pop-layout.ts | `detectGridMode()` | 너비 기반 모드 판별 |
|
||||
|
||||
## 브레이크포인트
|
||||
|
||||
| 기능 | 파일 | 함수/컴포넌트 | 설명 |
|
||||
|------|------|--------------|------|
|
||||
| 그리드 설정 | pop-layout.ts | `GRID_BREAKPOINTS` | 모드별 칸 수, gap, padding |
|
||||
| 모드 감지 | pop-layout.ts | `detectGridMode()` | viewportWidth → GridMode |
|
||||
| 훅 연동 | useDeviceOrientation.ts | `BREAKPOINTS.TABLET_MIN` | 768px (태블릿 경계) |
|
||||
|
||||
## 자동 줄바꿈/검토
|
||||
|
||||
| 기능 | 파일 | 함수/컴포넌트 | 설명 |
|
||||
|------|------|--------------|------|
|
||||
| 자동 배치 | gridUtils.ts | `convertAndResolvePositions()` | col > maxCol → 맨 아래 배치 |
|
||||
| 검토 필요 판별 | gridUtils.ts | `needsReview()` | 오버라이드 없으면 true |
|
||||
| 검토 패널 | PopCanvas.tsx | `ReviewPanel` | 검토 필요 컴포넌트 목록 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
327
popdocs/PLAN.md
327
popdocs/PLAN.md
|
|
@ -2,130 +2,80 @@
|
|||
|
||||
---
|
||||
|
||||
## 현재 상태 (2026-02-04)
|
||||
## 현재 상태 (2026-02-06)
|
||||
|
||||
**v4 통합 설계 모드 Phase 1.6 완료 (비율 스케일링 시스템)**
|
||||
|
||||
### 완료된 작업
|
||||
|
||||
1. **v4 기본 구조** - 완료
|
||||
2. **v4 렌더러** - 완료 (PopFlexRenderer)
|
||||
3. **v4 디자이너 통합** - 완료
|
||||
4. **새 화면 v4 기본 적용** - 완료
|
||||
5. **Undo/Redo** - 완료 (데스크탑 모드와 동일 방식)
|
||||
6. **드래그 리사이즈** - 완료
|
||||
7. **Flexbox 가로 배치** - 완료 (업계 표준 방식)
|
||||
8. **Spacer 컴포넌트** - 완료
|
||||
9. **컴포넌트 순서 변경** - 완료 (드래그 앤 드롭)
|
||||
10. **비율 스케일링** - 완료 (업계 표준 Scale with Fixed Aspect Ratio)
|
||||
|
||||
### 현재 UI 상태
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ← 목록 화면명 *변경됨 [↶][↷] 자동 레이아웃 (v4) [저장] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 미리보기: [모바일↕] [모바일↔] [태블릿↕] [태블릿↔(기본)] │
|
||||
│ 너비: [========●====] 1024 x 768 70% [-][+] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │ │ │
|
||||
│ 컴포넌트 │ [필드1] [필드2] [필드3] [필드4] │ 속성 패널 │
|
||||
│ 팔레트 │ [필드5] [Spacer] [Spacer] │ │
|
||||
│ (20%) │ (가로 배치 + 줄바꿈) │ (20%) │
|
||||
│ │ │ │
|
||||
│ - 필드 │ 디바이스 스크린 (스크롤 가능) │ │
|
||||
│ - 버튼 │ │ │
|
||||
│ - 리스트 │ │ │
|
||||
│ - 인디케이터│ │ │
|
||||
│ - 스캐너 │ │ │
|
||||
│ - 숫자패드 │ │ │
|
||||
│ - 스페이서 │ │ │
|
||||
└──────────┴────────────────────────────────────┴─────────────────┘
|
||||
```
|
||||
**v5.2 그리드 시스템 완성 (브레이크포인트 재설계 + 세로 자동 확장)**
|
||||
|
||||
---
|
||||
|
||||
## 작업 순서
|
||||
|
||||
```
|
||||
[Phase 1~3] [Phase 4] [Phase 5]
|
||||
v4 Flexbox → 실제 컴포넌트 → 그리드 시스템 (v5)
|
||||
완료 다음 계획 승인
|
||||
[Phase 1~3] [Phase 5] [Phase 4]
|
||||
v4 Flexbox → v5 CSS Grid → 실제 컴포넌트 구현
|
||||
완료 완료 (v5.2) 다음
|
||||
```
|
||||
|
||||
### Phase 5: 그리드 시스템 (v5) - 신규 계획
|
||||
|
||||
```
|
||||
[Phase 5.1] [Phase 5.2] [Phase 5.3] [Phase 5.4]
|
||||
그리드 타입 정의 → 그리드 렌더러 → 디자이너 UI → 반응형 자동화
|
||||
```
|
||||
|
||||
**상세 계획**: [GRID_SYSTEM_PLAN.md](./GRID_SYSTEM_PLAN.md)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 기본 구조 (완료)
|
||||
## 완료된 Phase
|
||||
|
||||
- [x] v3/v4 탭 제거 (자동 판별)
|
||||
- [x] 새 화면 → v4로 시작
|
||||
- [x] 기존 v3 화면 → v3로 로드 (하위 호환)
|
||||
- [x] 4개 프리셋 버튼 (모바일↕, 모바일↔, 태블릿↕, 태블릿↔)
|
||||
- [x] 슬라이더 유지 (320~1200px)
|
||||
- [x] 기본 프리셋: 태블릿 가로 (1024x768)
|
||||
- [x] ComponentPaletteV4 생성 (v4 전용 팔레트)
|
||||
- [x] 빈 레이아웃도 v4로 시작하도록 로직 수정
|
||||
### Phase 1~3: v4 Flexbox 시스템 (완료, 레거시 삭제됨)
|
||||
|
||||
## Phase 1.5: Flexbox 가로 배치 + 기본 기능 (완료)
|
||||
v4 Flexbox 기반 시스템은 v5 CSS Grid로 완전히 대체되었습니다.
|
||||
v4 관련 파일은 모두 삭제되었습니다.
|
||||
|
||||
- [x] Undo/Redo (Ctrl+Z / Ctrl+Shift+Z)
|
||||
- [x] 드래그 리사이즈 핸들 (오른쪽, 아래, 오른쪽아래)
|
||||
- [x] Flexbox 가로 배치 (`direction: horizontal`, `wrap: true`)
|
||||
- [x] 컴포넌트 타입별 기본 크기 설정
|
||||
- [x] Spacer 컴포넌트 (`pop-spacer`) - 정렬용 빈 공간
|
||||
- [x] 컴포넌트 순서 변경 (드래그 앤 드롭)
|
||||
- [x] 디바이스 스크린 스크롤 (무한 스크롤)
|
||||
- [x] v4 기본 구조, 렌더러, 디자이너 통합
|
||||
- [x] Undo/Redo, 드래그 리사이즈, Flexbox 가로 배치
|
||||
- [x] 비율 스케일링 시스템
|
||||
- [x] 오버라이드 기능 (모드별 배치 고정)
|
||||
- [x] 컴포넌트 표시/숨김, 줄바꿈
|
||||
|
||||
## Phase 1.6: 비율 스케일링 시스템 (완료)
|
||||
### Phase 5: v5 CSS Grid 시스템 (완료)
|
||||
|
||||
- [x] 기준 너비 설정 (BASE_VIEWPORT_WIDTH = 1024px)
|
||||
- [x] 최대 너비 제한 (1366px, 12인치)
|
||||
- [x] 뷰포트 감지 (resize 이벤트 리스너)
|
||||
- [x] 컴포넌트 크기 스케일 적용 (fixedWidth, fixedHeight)
|
||||
- [x] 컨테이너 스케일 적용 (gap, padding)
|
||||
- [x] 디자인 모드 분리 (scale=1, 원본 유지)
|
||||
- [x] DndProvider 에러 수정 (뷰어에서 useDrag/useDrop 방지)
|
||||
#### Phase 5.1: 타입 정의 (완료)
|
||||
- [x] `PopLayoutDataV5` 인터페이스
|
||||
- [x] `PopGridConfig`, `PopGridPosition` 타입
|
||||
- [x] `GridMode`, `GRID_BREAKPOINTS` 상수
|
||||
- [x] `createEmptyPopLayoutV5()`, `isV5Layout()`, `detectGridMode()`
|
||||
|
||||
## Phase 2: 오버라이드 기능 (완료) ✅
|
||||
#### Phase 5.2: 그리드 렌더러 (완료)
|
||||
- [x] `PopRenderer.tsx` - CSS Grid 기반 렌더링
|
||||
- [x] 격자 셀 렌더링 (CSS Grid 동일 좌표계)
|
||||
- [x] 위치 변환 (12칸 -> 4/6/8칸)
|
||||
|
||||
### Phase 2.1: 배치 고정 (완료)
|
||||
- [x] 현재 모드 추적 (PopDesigner)
|
||||
- [x] 고정 버튼 UI 및 로직
|
||||
- [x] 오버라이드 저장 (배치만)
|
||||
- [x] 오버라이드 초기화 로직
|
||||
- [x] **버그 수정**: tempLayout 도입 (root 오염 방지)
|
||||
- [x] **속성 패널**: 다른 모드에서 비활성화
|
||||
#### Phase 5.3: 디자이너 UI (완료)
|
||||
- [x] `PopCanvas.tsx` - 그리드 캔버스 + 행/열 라벨
|
||||
- [x] 드래그 스냅 (칸에 맞춤)
|
||||
- [x] `ComponentEditorPanel.tsx` - 위치 편집
|
||||
|
||||
### Phase 2.2: 렌더러 오버라이드 적용 (완료) ✅
|
||||
- [x] PopFlexRenderer에서 오버라이드 병합 (getMergedRoot)
|
||||
- [x] 컨테이너 속성 오버라이드 적용 (direction, wrap, gap, alignItems, justifyContent, padding, children)
|
||||
- [x] tempLayout 우선 표시 (고정 전 미리보기)
|
||||
- [x] 테스트 (모드별 다른 배치)
|
||||
#### Phase 5.4: 반응형 자동화 (완료)
|
||||
- [x] 자동 변환 알고리즘 (12칸 -> 4칸)
|
||||
- [x] 겹침 감지 및 재배치
|
||||
- [x] 모드별 오버라이드 저장
|
||||
|
||||
### Phase 2.3: 편집 자동 감지 (완료) ✅
|
||||
- [x] 순서 변경 시 자동 tempLayout 저장
|
||||
- [x] "고정" 버튼으로 정식 오버라이드 전환
|
||||
- [x] 속성 변경은 기본 모드에서만 가능 (다른 모드 차단)
|
||||
#### v5.1 추가 기능 (완료)
|
||||
- [x] 자동 줄바꿈 (col > maxCol -> 맨 아래 배치)
|
||||
- [x] "검토 필요" 알림 시스템
|
||||
- [x] Gap 프리셋 (좁게/보통/넓게)
|
||||
- [x] 숨김 기능 (모드별)
|
||||
|
||||
## Phase 3: 컴포넌트 표시/숨김 + 줄바꿈 (완료) ✅
|
||||
#### v5.2 브레이크포인트 재설계 + 세로 자동 확장 (완료)
|
||||
- [x] 기기 기반 브레이크포인트 (479/767/1023px)
|
||||
- [x] 세로 자동 확장 (dynamicCanvasHeight)
|
||||
- [x] 뷰어 반응형 일관성 (detectGridMode 사용)
|
||||
- [x] VIEWPORT_PRESETS에서 height 제거
|
||||
|
||||
- [x] visibility 속성 추가 (모드별 true/false)
|
||||
- [x] 속성 패널 "표시" 탭 추가 (체크박스 UI)
|
||||
- [x] 렌더러에서 visibility 처리
|
||||
- [x] pop-break 컴포넌트 추가 (강제 줄바꿈)
|
||||
- [x] 컴포넌트 오버라이드 병합 로직
|
||||
- [x] 삭제 시 오버라이드 정리 로직
|
||||
---
|
||||
|
||||
## Phase 4: 실제 컴포넌트 구현 (다음)
|
||||
## 다음 작업
|
||||
|
||||
### Phase 4: 실제 컴포넌트 구현
|
||||
|
||||
현재 모든 컴포넌트는 `pop-sample` (샘플 박스)로 렌더링됩니다.
|
||||
실제 컴포넌트를 구현하여 데이터 바인딩까지 연결해야 합니다.
|
||||
|
||||
**컴포넌트 구현 목록**:
|
||||
|
||||
- [ ] pop-field: 입력/표시 필드
|
||||
- [ ] pop-button: 액션 버튼
|
||||
|
|
@ -134,172 +84,37 @@ v4 Flexbox → 실제 컴포넌트 → 그리드 시스템 (v5)
|
|||
- [ ] pop-scanner: 바코드/QR 스캔
|
||||
- [ ] pop-numpad: 숫자 입력 패드
|
||||
|
||||
---
|
||||
**참고 문서**: [components-spec.md](./components-spec.md)
|
||||
|
||||
## Phase 5: 그리드 시스템 (v5) - 계획 승인
|
||||
### 후속 작업
|
||||
|
||||
> 상세 계획: [GRID_SYSTEM_PLAN.md](./GRID_SYSTEM_PLAN.md)
|
||||
|
||||
### 개요
|
||||
|
||||
Flexbox 흐름 기반 → **CSS Grid 위치 지정** 방식으로 전환
|
||||
|
||||
| 항목 | v4 (현재) | v5 (그리드) |
|
||||
|------|----------|-------------|
|
||||
| 배치 | Flexbox 흐름 | Grid 좌표 (열/행) |
|
||||
| 크기 | 픽셀 (200px) | 칸 (colSpan, rowSpan) |
|
||||
| 줄바꿈 | 자동 | 명시적 |
|
||||
|
||||
### Phase 5.1: 그리드 타입 정의
|
||||
|
||||
- [ ] `PopLayoutDataV5` 인터페이스
|
||||
- [ ] `PopGridConfig` (칸 수, 행 높이, 간격)
|
||||
- [ ] `PopComponentPositionV5` (col, row, colSpan, rowSpan)
|
||||
- [ ] 브레이크포인트 상수 (4칸/6칸/8칸/12칸)
|
||||
|
||||
### Phase 5.2: 그리드 렌더러
|
||||
|
||||
- [ ] `PopGridRenderer.tsx` 생성
|
||||
- [ ] CSS Grid 스타일 계산
|
||||
- [ ] 브레이크포인트 감지 및 칸 수 변경
|
||||
- [ ] 위치 변환 (12칸 → 4칸)
|
||||
|
||||
### Phase 5.3: 디자이너 UI
|
||||
|
||||
- [ ] `PopCanvasV5.tsx` (그리드 캔버스)
|
||||
- [ ] 바둑판 배경 표시
|
||||
- [ ] 드래그 스냅 (칸에 맞춤)
|
||||
- [ ] 위치 편집 패널
|
||||
|
||||
### Phase 5.4: 반응형 자동화
|
||||
|
||||
- [ ] 자동 변환 알고리즘 (12칸 → 4칸)
|
||||
- [ ] 겹침 감지 및 재배치
|
||||
- [ ] 모드별 오버라이드
|
||||
|
||||
### 브레이크포인트
|
||||
|
||||
| 모드 | 화면 범위 | 그리드 칸 수 |
|
||||
|------|----------|-------------|
|
||||
| 모바일 세로 | ~599px (4~6인치) | 4칸 |
|
||||
| 모바일 가로 | 600~839px (6~8인치) | 6칸 |
|
||||
| 태블릿 세로 | 840~1023px (8~10인치) | 8칸 |
|
||||
| 태블릿 가로 | 1024px~ (10~14인치) | 12칸 |
|
||||
- [ ] 워크플로우 연동 (버튼 액션, 화면 전환)
|
||||
- [ ] 데이터 바인딩 연결
|
||||
- [ ] 실기기 테스트 (아이폰 SE, iPad Mini 등)
|
||||
|
||||
---
|
||||
|
||||
## 완료된 기능 목록
|
||||
## 브레이크포인트 (v5.2 현재)
|
||||
|
||||
### v4 타입 정의
|
||||
|
||||
- [x] `PopLayoutDataV4` - 단일 소스 레이아웃
|
||||
- [x] `PopContainerV4` - 스택 컨테이너
|
||||
- [x] `PopSizeConstraintV4` - 크기 규칙 (fixed/fill/hug)
|
||||
- [x] `PopResponsiveRuleV4` - 반응형 규칙
|
||||
- [x] `createEmptyPopLayoutV4()` - 생성 함수
|
||||
- [x] `isV4Layout()` - 타입 가드
|
||||
- [x] `addComponentToV4Layout()` - 컴포넌트 추가
|
||||
- [x] `removeComponentFromV4Layout()` - 컴포넌트 삭제
|
||||
- [x] `updateComponentInV4Layout()` - 컴포넌트 수정
|
||||
- [x] `updateContainerV4()` - 컨테이너 수정
|
||||
- [x] `findContainerV4()` - 컨테이너 찾기
|
||||
|
||||
### v4 렌더러
|
||||
|
||||
- [x] `PopFlexRenderer` - Flexbox 기반 렌더링
|
||||
- [x] 컨테이너 재귀 렌더링 (`ContainerRenderer`)
|
||||
- [x] 반응형 규칙 적용 (`applyResponsiveRules`)
|
||||
- [x] 컴포넌트 숨김 처리 (`hideBelow`)
|
||||
- [x] 크기 제약 → CSS 변환 (`calculateSizeStyle`)
|
||||
- [x] 드래그 리사이즈 핸들 (`ComponentRendererV4`)
|
||||
- [x] 드래그 앤 드롭 순서 변경 (`DraggableComponentWrapper`)
|
||||
- [x] 비율 스케일링 (`BASE_VIEWPORT_WIDTH`, scale 계산)
|
||||
|
||||
### v4 캔버스
|
||||
|
||||
- [x] `PopCanvasV4` - v4 전용 캔버스
|
||||
- [x] 뷰포트 프리셋 (4개 모드)
|
||||
- [x] 너비 슬라이더 (320~1200px)
|
||||
- [x] 줌 컨트롤 (30%~150%)
|
||||
- [x] 패닝 (Space + 드래그)
|
||||
- [x] 드래그 앤 드롭
|
||||
|
||||
### v4 속성 패널
|
||||
|
||||
- [x] `ComponentEditorPanelV4` - 속성 편집 패널
|
||||
- [x] 크기 제약 편집 (fixed/fill/hug)
|
||||
- [x] 컨테이너 설정 (방향, 정렬, 간격)
|
||||
|
||||
### 디자이너 통합
|
||||
|
||||
- [x] `PopDesigner` v3/v4 자동 판별
|
||||
- [x] 새 화면 v4 기본 적용
|
||||
- [x] 기존 v3 화면 하위 호환
|
||||
- [x] `ComponentPaletteV4` v4 전용 팔레트 (Spacer 포함)
|
||||
- [x] Undo/Redo 버튼 및 단축키
|
||||
- [x] 컴포넌트 순서 변경 핸들러 (`handleReorderComponentV4`)
|
||||
| 모드 | 화면 너비 | 칸 수 | 대상 기기 |
|
||||
|------|----------|-------|----------|
|
||||
| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S |
|
||||
| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로 |
|
||||
| tablet_portrait | 768~1023px | 8칸 | 8~10인치 태블릿 세로 |
|
||||
| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 |
|
||||
|
||||
---
|
||||
|
||||
## v3 vs v4 비교
|
||||
## 관련 문서
|
||||
|
||||
| 항목 | v3 (기존) | v4 (새로운) |
|
||||
|------|-----------|-------------|
|
||||
| 설계 | 4모드 각각 | 1번만 |
|
||||
| 데이터 | col, row 위치 | 규칙 (fill/fixed/hug) |
|
||||
| 렌더링 | CSS Grid | Flexbox |
|
||||
| 반응형 | 수동 | 자동 + 규칙 |
|
||||
| 새 화면 | - | 기본 적용 |
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 역할 |
|
||||
| 문서 | 내용 |
|
||||
|------|------|
|
||||
| `PopDesigner.tsx` | v3/v4 통합 디자이너 |
|
||||
| `PopCanvasV4.tsx` | v4 캔버스 (4개 프리셋 + 슬라이더) |
|
||||
| `PopFlexRenderer.tsx` | v4 Flexbox 렌더러 + 비율 스케일링 |
|
||||
| `ComponentPaletteV4.tsx` | v4 컴포넌트 팔레트 |
|
||||
| `ComponentEditorPanelV4.tsx` | v4 속성 편집 패널 |
|
||||
| `pop-layout.ts` | v3/v4 타입 정의 |
|
||||
| `page.tsx` (뷰어) | v4 레이아웃 뷰어 + viewportWidth 감지 |
|
||||
| [STATUS.md](./STATUS.md) | 현재 진행 상태 |
|
||||
| [SPEC.md](./SPEC.md) | 기술 스펙 |
|
||||
| [ARCHITECTURE.md](./ARCHITECTURE.md) | 코드 구조 |
|
||||
| [components-spec.md](./components-spec.md) | 컴포넌트 상세 설계 |
|
||||
| [decisions/005](./decisions/005-breakpoint-redesign.md) | 브레이크포인트 재설계 ADR |
|
||||
|
||||
---
|
||||
|
||||
## 비율 스케일링 시스템
|
||||
|
||||
### 개념
|
||||
10인치(1024px) 기준으로 디자인하면, 8~12인치 화면에서 배치는 유지하고 크기만 비례 조정
|
||||
|
||||
### 계산 공식
|
||||
```
|
||||
scale = viewportWidth / BASE_VIEWPORT_WIDTH (1024)
|
||||
scaledSize = originalSize * scale
|
||||
```
|
||||
|
||||
### 적용 범위
|
||||
|
||||
| 항목 | 스케일 적용 |
|
||||
|------|------------|
|
||||
| fixedWidth | O |
|
||||
| fixedHeight | O |
|
||||
| minWidth/maxWidth | O |
|
||||
| minHeight | O |
|
||||
| gap | O |
|
||||
| padding | O |
|
||||
| flex (fill) | X (비율 기반) |
|
||||
| hug | X (내용 기반) |
|
||||
|
||||
### 화면별 스케일
|
||||
|
||||
| 화면 | 너비 | 스케일 | 200px → |
|
||||
|------|------|--------|---------|
|
||||
| 8인치 | 800px | 0.78 | 156px |
|
||||
| 10인치 | 1024px | 1.00 | 200px |
|
||||
| 12인치 | 1366px | 1.33 | 266px |
|
||||
| 14인치+ | 1366px (max) | 1.33 | 266px + 여백 |
|
||||
|
||||
---
|
||||
|
||||
*최종 업데이트: 2026-02-05 (Phase 5 그리드 시스템 계획 추가)*
|
||||
*최종 업데이트: 2026-02-06 (v5.2 완료, Phase 4 대기)*
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@
|
|||
|------|------|------|--------|
|
||||
| rowSpan이 적용 안됨 | gridTemplateRows를 `1fr`로 변경 | 2026-02-02 | grid, rowSpan, CSS |
|
||||
| 컴포넌트 크기 스케일 안됨 | viewportWidth 기반 scale 계산 추가 | 2026-02-04 | scale, viewport, 반응형 |
|
||||
| **그리드 가이드 셀 크기 불균일** | gridAutoRows → gridTemplateRows로 행 높이 강제 고정 | 2026-02-06 | gridAutoRows, gridTemplateRows, 셀 크기, CSS Grid |
|
||||
| **컴포넌트 콘텐츠가 셀 경계 벗어남** | overflow-visible → overflow-hidden 변경 | 2026-02-06 | overflow, 셀 크기, 콘텐츠 |
|
||||
|
||||
## DnD (드래그앤드롭) 관련
|
||||
|
||||
|
|
@ -18,6 +20,7 @@
|
|||
|------|------|------|--------|
|
||||
| useDrag 에러 (뷰어에서) | isDesignMode 체크 후 early return | 2026-02-04 | DnD, useDrag, 뷰어 |
|
||||
| DndProvider 중복 에러 | 최상위에서만 Provider 사용 | 2026-02-04 | DndProvider, react-dnd |
|
||||
| **Expected drag drop context (뷰어)** | isDesignMode=false일 때 DraggableComponent 대신 일반 div 렌더링 | 2026-02-05 | DndProvider, useDrag, 뷰어, context |
|
||||
| **컴포넌트 중첩(겹침)** | toast import 누락 → `sonner`에서 import | 2026-02-05 | 겹침, overlap, toast |
|
||||
| **리사이즈 핸들 작동 안됨** | useDrop 2개 중복 → 단일 useDrop으로 통합 | 2026-02-05 | resize, 핸들, useDrop |
|
||||
| **드래그 좌표 완전 틀림 (Row 92)** | 캔버스 scale 보정 누락 → `(offset - rect.left) / scale` | 2026-02-05 | scale, 좌표, transform |
|
||||
|
|
@ -35,10 +38,19 @@
|
|||
|
||||
| 문제 | 해결 | 날짜 | 키워드 |
|
||||
|------|------|------|--------|
|
||||
| **화면 밖 컴포넌트 정보 손실** | 자동 줄바꿈 로직 추가 (col > maxCol → col=1, row=맨아래+1) | 2026-02-06 | 자동배치, 줄바꿈, 정보손실 |
|
||||
| Flexbox 배치 예측 불가 | CSS Grid로 전환 (v5) | 2026-02-05 | Flexbox, Grid, 반응형 |
|
||||
| 4모드 각각 배치 힘듦 | 제약조건 기반 시스템 (v4) | 2026-02-03 | 모드, 반응형, 제약조건 |
|
||||
| 4모드 자동 전환 안됨 | useResponsiveMode 훅 추가 | 2026-02-01 | 모드, 훅, 반응형 |
|
||||
|
||||
## 브레이크포인트/반응형 관련
|
||||
|
||||
| 문제 | 해결 | 날짜 | 키워드 |
|
||||
|------|------|------|--------|
|
||||
| **뷰어 반응형 모드 불일치** | detectGridMode() 사용으로 일관성 확보 | 2026-02-06 | 반응형, 뷰어, 모드 |
|
||||
| **768~839px 모드 불일치** | TABLET_MIN 768로 변경, 브레이크포인트 재설계 | 2026-02-06 | 브레이크포인트, 768px |
|
||||
| **useResponsiveMode vs GRID_BREAKPOINTS 불일치** | 뷰어에서 detectGridMode(viewportWidth) 사용 | 2026-02-06 | 훅, 상수, 일관성 |
|
||||
|
||||
## 저장/로드 관련
|
||||
|
||||
| 문제 | 해결 | 날짜 | 키워드 |
|
||||
|
|
@ -77,6 +89,14 @@
|
|||
| 컴포넌트 이동 안됨 | **해결** | useDrop/useDrag 타입 통일 |
|
||||
| 컴포넌트 중첩(겹침) | **해결** | toast import 추가 → 겹침 감지 로직 정상 작동 |
|
||||
| 리사이즈 핸들 작동 안됨 | **해결** | useDrop 통합 (2개 → 1개) |
|
||||
| 숨김 컴포넌트 드래그 안됨 | **해결** | handleMoveComponent에서 숨김 해제 + 위치 저장 단일 상태 업데이트 |
|
||||
| 그리드 범위 초과 에러 | **해결** | adjustedCol 계산으로 드롭 위치 자동 조정 |
|
||||
| Expected drag drop context (뷰어) | **해결** | isDesignMode=false일 때 일반 div 렌더링 |
|
||||
| hiddenComponentIds 중복 정의 | **해결** | 중복 useMemo 제거 (라인 410-412) |
|
||||
| 뷰어 반응형 모드 불일치 | **해결** | detectGridMode() 사용 |
|
||||
| 그리드 가이드 셀 크기 불균일 | **해결** | gridTemplateRows로 행 높이 강제 고정 |
|
||||
| Canvas vs Renderer 행 수 불일치 | **해결** | 숨김 필터 통일, 여유행 +3으로 통일 |
|
||||
| 디버깅 console.log 잔존 | **해결** | reviewComponents 내 console.log 삭제 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -114,4 +134,111 @@ calcGridPosition(relX, relY, customWidth, ...);
|
|||
|
||||
---
|
||||
|
||||
## Expected drag drop context 에러 상세 (2026-02-05 심야)
|
||||
|
||||
### 증상
|
||||
```
|
||||
Invariant Violation: Expected drag drop context
|
||||
at useDrag (...)
|
||||
at DraggableComponent (...)
|
||||
```
|
||||
뷰어 페이지(`/pop/viewer/[screenId]`)에서 POP 화면 조회 시 에러 발생
|
||||
|
||||
### 원인
|
||||
```
|
||||
PopRenderer의 DraggableComponent에서 useDrag 훅을 무조건 호출
|
||||
→ 뷰어 페이지에는 DndProvider가 없음
|
||||
→ React 훅은 조건부 호출 불가 (Rules of Hooks)
|
||||
→ DndProvider 없이 useDrag 호출 시 context 에러
|
||||
```
|
||||
|
||||
### 해결
|
||||
```typescript
|
||||
// PopRenderer.tsx - 컴포넌트 렌더링 부분
|
||||
if (isDesignMode) {
|
||||
return (
|
||||
<DraggableComponent ... /> // useDrag 사용
|
||||
);
|
||||
}
|
||||
|
||||
// 뷰어 모드: 드래그 없는 일반 렌더링
|
||||
return (
|
||||
<div className="..." style={positionStyle}>
|
||||
<ComponentContent ... />
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### 교훈
|
||||
> React DnD의 `useDrag`/`useDrop` 훅은 반드시 `DndProvider` 내부에서만 호출해야 함.
|
||||
> 디자인 모드와 뷰어 모드를 분기할 때, 훅이 포함된 컴포넌트 자체를 조건부 렌더링해야 함.
|
||||
> 훅 내부에서 `canDrag: false`로 설정해도 훅 자체는 호출되므로 context 에러 발생.
|
||||
|
||||
### 관련 파일
|
||||
- `gridUtils.ts`: convertAndResolvePositions(), needsReview()
|
||||
- `PopCanvas.tsx`: ReviewPanel, ReviewItem
|
||||
- `PopRenderer.tsx`: 자동 배치 위치 렌더링
|
||||
|
||||
---
|
||||
|
||||
## 뷰어 반응형 모드 불일치 상세 (2026-02-06)
|
||||
|
||||
### 증상
|
||||
```
|
||||
- 아이폰 SE, iPad Pro 프리셋은 정상 작동
|
||||
- 브라우저 수동 리사이즈 시 6칸 모드(mobile_landscape)가 적용 안 됨
|
||||
- 768~839px 구간에서 8칸으로 표시됨 (예상: 6칸)
|
||||
```
|
||||
|
||||
### 원인
|
||||
```
|
||||
useResponsiveMode 훅:
|
||||
- deviceType: width/height 비율로 "mobile"/"tablet" 판정
|
||||
- isLandscape: width > height로 판정
|
||||
- BREAKPOINTS.TABLET_MIN = 840 (당시)
|
||||
|
||||
GRID_BREAKPOINTS:
|
||||
- mobile_landscape: 600~839px (6칸)
|
||||
- tablet_portrait: 840~1023px (8칸)
|
||||
|
||||
결과:
|
||||
- 768px 화면 → useResponsiveMode: "tablet" (768 < 840이지만 비율 판정)
|
||||
- 768px 화면 → GRID_BREAKPOINTS: "mobile_landscape" (6칸)
|
||||
- → 모드 불일치!
|
||||
```
|
||||
|
||||
### 해결
|
||||
|
||||
**1단계: 브레이크포인트 재설계**
|
||||
```typescript
|
||||
// 기존
|
||||
mobile_landscape: { minWidth: 600, maxWidth: 839 }
|
||||
tablet_portrait: { minWidth: 840, maxWidth: 1023 }
|
||||
|
||||
// 변경 후
|
||||
mobile_landscape: { minWidth: 480, maxWidth: 767 }
|
||||
tablet_portrait: { minWidth: 768, maxWidth: 1023 }
|
||||
```
|
||||
|
||||
**2단계: 훅 연동**
|
||||
```typescript
|
||||
// useDeviceOrientation.ts
|
||||
BREAKPOINTS.TABLET_MIN: 768 // was 840
|
||||
```
|
||||
|
||||
**3단계: 뷰어 모드 감지 방식 변경**
|
||||
```typescript
|
||||
// page.tsx (뷰어)
|
||||
const currentModeKey = isPreviewMode
|
||||
? getModeKey(deviceType, isLandscape) // 프리뷰: 수동 선택
|
||||
: detectGridMode(viewportWidth); // 일반: 너비 기반 (일관성 확보)
|
||||
```
|
||||
|
||||
### 교훈
|
||||
> 반응형 모드 판정은 **단일 소스(GRID_BREAKPOINTS)**를 기준으로 해야 함.
|
||||
> 훅과 상수가 각각 다른 기준을 사용하면 구간별 불일치 발생.
|
||||
> 뷰어에서는 `detectGridMode(viewportWidth)` 직접 사용으로 일관성 확보.
|
||||
|
||||
---
|
||||
|
||||
*새 문제-해결 추가 시 해당 카테고리 테이블에 행 추가*
|
||||
|
|
|
|||
|
|
@ -9,22 +9,20 @@
|
|||
|
||||
| 항목 | 값 |
|
||||
|------|-----|
|
||||
| 버전 | **v5** (CSS Grid 기반) |
|
||||
| 상태 | **반응형 레이아웃 + 숨김 기능 완료** |
|
||||
| 버전 | **v5.2** (브레이크포인트 재설계 + 세로 자동 확장) |
|
||||
| 상태 | **반응형 시스템 완성** |
|
||||
| 다음 | Phase 4 (실제 컴포넌트 구현) |
|
||||
|
||||
**마지막 업데이트**: 2026-02-05 심야
|
||||
**마지막 업데이트**: 2026-02-06
|
||||
|
||||
---
|
||||
|
||||
## 마지막 대화 요약
|
||||
|
||||
> **반응형 레이아웃 시스템 완성**:
|
||||
> - 모드별 컴포넌트 재배치 (오버라이드) 시스템 구현
|
||||
> - 화면 밖 컴포넌트 오른쪽 패널 배치 기능
|
||||
> - 컴포넌트 숨김/숨김해제 기능 (모드별)
|
||||
> - 리사이즈 겹침 검사 추가
|
||||
> - H키 단축키로 숨김 처리
|
||||
> **v5.2.1 그리드 셀 크기 강제 고정**:
|
||||
> - gridAutoRows → gridTemplateRows로 행 높이 강제 고정
|
||||
> - "셀의 크기 = 컴포넌트의 크기" 원칙을 코드 수준에서 강제
|
||||
> - Canvas/Renderer 간 행 수 계산 기준 통일 (숨김 필터, 여유행 +3)
|
||||
>
|
||||
> 다음: Phase 4 (실제 컴포넌트 구현)
|
||||
|
||||
|
|
@ -38,6 +36,10 @@
|
|||
| 저장/조회 규칙 | [SAVE_RULES.md](./SAVE_RULES.md) |
|
||||
| 왜 v5로 바꿨어? | [decisions/003-v5-grid-system.md](./decisions/003-v5-grid-system.md) |
|
||||
| 그리드 가이드 설계 | [decisions/004-grid-guide-integration.md](./decisions/004-grid-guide-integration.md) |
|
||||
| 브레이크포인트 재설계 | [decisions/005-breakpoint-redesign.md](./decisions/005-breakpoint-redesign.md) |
|
||||
| 자동 줄바꿈 시스템 | [decisions/006-auto-wrap-review-system.md](./decisions/006-auto-wrap-review-system.md) |
|
||||
| 개발 계획/로드맵 | [PLAN.md](./PLAN.md) |
|
||||
| 컴포넌트 상세 설계 | [components-spec.md](./components-spec.md) |
|
||||
| 이전 문제 해결 | [PROBLEMS.md](./PROBLEMS.md) |
|
||||
| 코드 어디 있어? | [FILES.md](./FILES.md) |
|
||||
| 기능별 색인 | [INDEX.md](./INDEX.md) |
|
||||
|
|
@ -88,15 +90,17 @@ decisions/, sessions/, archive/
|
|||
|
||||
## v5 그리드 시스템 (현재)
|
||||
|
||||
| 모드 | 화면 너비 | 칸 수 |
|
||||
|------|----------|-------|
|
||||
| mobile_portrait | ~599px | 4칸 |
|
||||
| mobile_landscape | 600~839px | 6칸 |
|
||||
| tablet_portrait | 840~1023px | 8칸 |
|
||||
| tablet_landscape | 1024px~ | 12칸 |
|
||||
| 모드 | 화면 너비 | 칸 수 | 대상 기기 |
|
||||
|------|----------|-------|----------|
|
||||
| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S |
|
||||
| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로 |
|
||||
| tablet_portrait | 768~1023px | 8칸 | 8~10인치 태블릿 세로 |
|
||||
| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 |
|
||||
|
||||
**핵심**: 컴포넌트를 칸 단위로 배치 (col, row, colSpan, rowSpan)
|
||||
|
||||
**세로 무한 스크롤**: 캔버스 높이 자동 확장 (컴포넌트 배치에 따라)
|
||||
|
||||
**그리드 가이드**: CSS Grid 기반 격자 셀 + 행/열 라벨 (ON/OFF 토글)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@
|
|||
|
||||
### 1. 그리드 시스템
|
||||
|
||||
| 모드 | 화면 너비 | 칸 수 | 대상 |
|
||||
|------|----------|-------|------|
|
||||
| mobile_portrait | ~599px | 4칸 | 4~6인치 |
|
||||
| mobile_landscape | 600~839px | 6칸 | 7인치 |
|
||||
| tablet_portrait | 840~1023px | 8칸 | 8~10인치 |
|
||||
| tablet_landscape | 1024px~ | 12칸 | 10~14인치 (기본) |
|
||||
| 모드 | 화면 너비 | 칸 수 | 대상 기기 |
|
||||
|------|----------|-------|----------|
|
||||
| mobile_portrait | ~479px | 4칸 | 아이폰 SE ~ 갤럭시 S (세로) |
|
||||
| mobile_landscape | 480~767px | 6칸 | 스마트폰 가로, 작은 태블릿 |
|
||||
| tablet_portrait | 768~1023px | 8칸 | iPad Mini ~ iPad Pro (세로) |
|
||||
| tablet_landscape | 1024px~ | 12칸 | 10~14인치 태블릿 가로 (기본) |
|
||||
|
||||
> **브레이크포인트 기준**: 실제 기기 CSS 뷰포트 너비 기반 (2026-02-06 재설계)
|
||||
|
||||
### 2. 위치 지정
|
||||
|
||||
|
|
@ -34,29 +36,59 @@ const GRID_BREAKPOINTS = {
|
|||
columns: 4,
|
||||
rowHeight: 48,
|
||||
gap: 8,
|
||||
padding: 12
|
||||
padding: 12,
|
||||
maxWidth: 479, // 아이폰 SE (375px) ~ 갤럭시 S (360px)
|
||||
},
|
||||
mobile_landscape: {
|
||||
columns: 6,
|
||||
rowHeight: 44,
|
||||
gap: 8,
|
||||
padding: 16
|
||||
padding: 16,
|
||||
minWidth: 480,
|
||||
maxWidth: 767, // 스마트폰 가로
|
||||
},
|
||||
tablet_portrait: {
|
||||
columns: 8,
|
||||
rowHeight: 52,
|
||||
gap: 12,
|
||||
padding: 20
|
||||
padding: 20,
|
||||
minWidth: 768, // iPad Mini 세로 (768px)
|
||||
maxWidth: 1023,
|
||||
},
|
||||
tablet_landscape: {
|
||||
columns: 12,
|
||||
rowHeight: 56,
|
||||
gap: 12,
|
||||
padding: 24
|
||||
padding: 24,
|
||||
minWidth: 1024, // iPad Pro 11 가로 (1194px), 12.9 가로 (1366px)
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 세로 자동 확장
|
||||
|
||||
```typescript
|
||||
// 캔버스 높이 동적 계산
|
||||
const MIN_CANVAS_HEIGHT = 600; // 최소 높이 (px)
|
||||
const CANVAS_EXTRA_ROWS = 3; // 항상 유지되는 여유 행 수
|
||||
|
||||
const dynamicCanvasHeight = useMemo(() => {
|
||||
// 가장 아래 컴포넌트 위치 계산
|
||||
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
||||
const rowEnd = pos.row + pos.rowSpan;
|
||||
return Math.max(max, rowEnd);
|
||||
}, 1);
|
||||
|
||||
// 여유 행 추가하여 높이 계산
|
||||
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS;
|
||||
return Math.max(MIN_CANVAS_HEIGHT, totalRows * rowHeight + padding);
|
||||
}, [layout.components, ...]);
|
||||
```
|
||||
|
||||
**특징**:
|
||||
- 디자이너: 세로 무한 확장 (컴포넌트 추가에 제한 없음)
|
||||
- 뷰어: 터치 스크롤로 아래 컴포넌트 접근 가능
|
||||
|
||||
---
|
||||
|
||||
## 데이터 구조
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 현재 상태
|
||||
|
||||
> **마지막 업데이트**: 2026-02-05 심야
|
||||
> **마지막 업데이트**: 2026-02-06
|
||||
> **담당**: POP 화면 디자이너
|
||||
|
||||
---
|
||||
|
|
@ -19,10 +19,16 @@
|
|||
| 컴포넌트 팔레트 | 완료 | `ComponentPalette.tsx` |
|
||||
| 드래그앤드롭 | 완료 | 스케일 보정, DND 상수 통합 |
|
||||
| 그리드 가이드 재설계 | 완료 | CSS Grid 기반 통합 |
|
||||
| **모드별 오버라이드** | **완료** | 위치/크기 모드별 저장 |
|
||||
| **화면 밖 컴포넌트** | **완료** | 오른쪽 패널 배치, 드래그로 복원 |
|
||||
| **숨김 기능** | **완료** | 모드별 숨김/숨김해제 |
|
||||
| **리사이즈 겹침 검사** | **완료** | 실시간 겹침 방지 |
|
||||
| 모드별 오버라이드 | 완료 | 위치/크기 모드별 저장 |
|
||||
| 화면 밖 컴포넌트 | 완료 | 오른쪽 패널 배치, 드래그로 복원 |
|
||||
| 숨김 기능 | 완료 | 모드별 숨김/숨김해제 |
|
||||
| 리사이즈 겹침 검사 | 완료 | 실시간 겹침 방지 |
|
||||
| Gap 프리셋 | 완료 | 좁게/보통/넓게 간격 조정 |
|
||||
| **자동 줄바꿈** | **완료** | col > maxCol → 맨 아래 배치 |
|
||||
| **검토 필요 시스템** | **완료** | 오버라이드 없으면 검토 알림 |
|
||||
| **브레이크포인트 재설계** | **완료** | 기기 기반 (479/767/1023px) |
|
||||
| **세로 자동 확장** | **완료** | 캔버스 높이 동적 계산 |
|
||||
| **그리드 셀 크기 강제 고정** | **완료** | gridTemplateRows로 행 높이 고정, overflow-hidden |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -38,29 +44,39 @@
|
|||
|
||||
---
|
||||
|
||||
## 최근 주요 변경 (2026-02-05 심야)
|
||||
## 최근 주요 변경 (2026-02-06)
|
||||
|
||||
### 반응형 레이아웃 시스템
|
||||
### 브레이크포인트 재설계
|
||||
| 모드 | 변경 전 | 변경 후 | 근거 |
|
||||
|------|--------|--------|------|
|
||||
| mobile_portrait | ~599px | ~479px | 스마트폰 세로 |
|
||||
| mobile_landscape | 600~839px | 480~767px | 스마트폰 가로 |
|
||||
| tablet_portrait | 840~1023px | 768~1023px | iPad Mini 포함 |
|
||||
| tablet_landscape | 1024px+ | 동일 | - |
|
||||
|
||||
### 세로 자동 확장
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 동적 캔버스 높이 | 컴포넌트 배치에 따라 자동 계산 |
|
||||
| 최소 높이 | 600px 보장 |
|
||||
| 여유 행 | 항상 3행 추가 |
|
||||
| 뷰어 스크롤 | 터치 스크롤로 아래 컴포넌트 접근 |
|
||||
|
||||
### v5.1 자동 줄바꿈 시스템
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 자동 줄바꿈 | col > maxCol인 컴포넌트를 맨 아래에 자동 배치 |
|
||||
| 정보 손실 방지 | 모든 컴포넌트가 항상 그리드 안에 표시됨 |
|
||||
| 검토 필요 알림 | 오버라이드 없으면 "검토 필요" 패널 표시 |
|
||||
| 검토 완료 | 편집하면 오버라이드 저장, 검토 필요에서 제거 |
|
||||
|
||||
### 기존 기능 유지 (2026-02-05 심야)
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 모드별 재배치 | 4/6/8/12칸 모드별로 컴포넌트 위치/크기 개별 저장 |
|
||||
| 자동 레이아웃 고정 | 드래그/리사이즈 시 자동으로 오버라이드 저장 |
|
||||
| 원본으로 되돌리기 | 오버라이드 삭제하여 자동 재배치로 복원 |
|
||||
|
||||
### 화면 밖 컴포넌트 처리
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 오른쪽 패널 표시 | 현재 모드에서 초과하는 컴포넌트 별도 표시 |
|
||||
| 드래그로 복원 | 패널에서 그리드로 드래그하여 재배치 |
|
||||
| 위치 자동 조정 | 그리드 범위 초과 시 자동으로 왼쪽으로 밀어서 배치 |
|
||||
|
||||
### 숨김 기능
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 모드별 숨김 | 특정 모드에서만 컴포넌트 숨김 가능 |
|
||||
| 숨김 방법 | 드래그→숨김패널 / H키 / 화면밖 컴포넌트 클릭 |
|
||||
| 숨김 해제 | 숨김패널에서 그리드로 드래그 |
|
||||
| 12칸 모드 제한 | 기본 모드(12칸)에서는 숨김 기능 비활성화 |
|
||||
| 숨김 기능 | 특정 모드에서 컴포넌트 의도적 숨김 (검토와 별개) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -74,6 +90,14 @@
|
|||
| DND 타입 상수 불일치 | 해결됨 | constants/dnd.ts로 통합 |
|
||||
| 숨김 컴포넌트 드래그 안됨 | 해결됨 | 상태 업데이트 순서 수정 |
|
||||
| 그리드 범위 초과 에러 | 해결됨 | 드롭 위치 자동 조정 |
|
||||
| Expected drag drop context | 해결됨 | 뷰어 모드에서 일반 div 렌더링 |
|
||||
| Gap 프리셋 UI 안 보임 | 해결됨 | 그리드 라벨에 adjustedGap 적용 |
|
||||
| 화면 밖 컴포넌트 정보 손실 | 해결됨 | 자동 줄바꿈으로 항상 그리드 안에 배치 |
|
||||
| 뷰어 반응형 모드 불일치 | 해결됨 | detectGridMode() 사용으로 일관성 확보 |
|
||||
| hiddenComponentIds 중복 정의 | 해결됨 | 중복 useMemo 제거 |
|
||||
| 그리드 가이드 셀 크기 불균일 | 해결됨 | gridTemplateRows로 행 높이 강제 고정 |
|
||||
| Canvas/Renderer 행 수 불일치 | 해결됨 | 숨김 필터 통일, 여유행 +3 |
|
||||
| 디버깅 console.log 잔존 | 해결됨 | reviewComponents 내 삭제 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -81,7 +105,8 @@
|
|||
|
||||
| 날짜 | 요약 | 상세 |
|
||||
|------|------|------|
|
||||
| 2026-02-05 심야 | 반응형 레이아웃, 숨김 기능, 겹침 검사 | 이 세션 |
|
||||
| 2026-02-06 | 브레이크포인트 재설계, 세로 자동 확장, v5.1 자동 줄바꿈 | [sessions/2026-02-06.md](./sessions/2026-02-06.md) |
|
||||
| 2026-02-05 심야 | 반응형 레이아웃, 숨김 기능, 겹침 검사 | [sessions/2026-02-05.md](./sessions/2026-02-05.md) |
|
||||
| 2026-02-05 저녁 | v5 통합, 그리드 가이드 재설계 | [sessions/2026-02-05.md](./sessions/2026-02-05.md) |
|
||||
|
||||
---
|
||||
|
|
@ -90,7 +115,8 @@
|
|||
|
||||
| ADR | 제목 | 날짜 |
|
||||
|-----|------|------|
|
||||
| 005 | 반응형 레이아웃 및 숨김 기능 | 2026-02-05 |
|
||||
| 006 | v5.1 자동 줄바꿈 + 검토 필요 시스템 | 2026-02-06 |
|
||||
| 005 | 브레이크포인트 재설계 (기기 기반) + 세로 자동 확장 | 2026-02-06 |
|
||||
| 004 | 그리드 가이드 CSS Grid 통합 | 2026-02-05 |
|
||||
| 003 | v5 CSS Grid 채택 | 2026-02-05 |
|
||||
| 001 | v4 제약조건 기반 | 2026-02-03 |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
# ADR 005: 브레이크포인트 재설계 (기기 기반)
|
||||
|
||||
**날짜**: 2026-02-06
|
||||
**상태**: 채택
|
||||
**의사결정자**: 시스템 아키텍트
|
||||
|
||||
---
|
||||
|
||||
## 상황 (Context)
|
||||
|
||||
### 문제 1: 뷰어에서 모드 전환 불일치
|
||||
|
||||
```
|
||||
브라우저 수동 리사이즈 시:
|
||||
- useResponsiveMode 훅: 768px 이상 → "tablet" 판정
|
||||
- GRID_BREAKPOINTS: 768~839px → "mobile_landscape" (6칸)
|
||||
|
||||
결과: 768~839px 구간에서 모드 불일치 발생
|
||||
```
|
||||
|
||||
### 문제 2: 기존 브레이크포인트 근거 부족
|
||||
|
||||
```
|
||||
기존 설정:
|
||||
- mobile_portrait: ~599px
|
||||
- mobile_landscape: 600~839px
|
||||
- tablet_portrait: 840~1023px
|
||||
|
||||
문제: 실제 기기 뷰포트와 맞지 않음
|
||||
- iPad Mini 세로: 768px (mobile_landscape로 분류됨)
|
||||
```
|
||||
|
||||
### 사용자 요구사항
|
||||
|
||||
> "현장 모바일 기기가 최소 8인치 ~ 최대 14인치,
|
||||
> 핸드폰은 아이폰 미니 ~ 갤럭시 울트라 사이즈"
|
||||
|
||||
---
|
||||
|
||||
## 연구 (Research)
|
||||
|
||||
### 실제 기기 CSS 뷰포트 조사 (2026년 기준)
|
||||
|
||||
| 기기 | 화면 크기 | CSS 뷰포트 너비 |
|
||||
|------|----------|----------------|
|
||||
| iPhone SE | 4.7" | 375px |
|
||||
| iPhone 16 Pro | 6.3" | 402px |
|
||||
| Galaxy S25 Ultra | 6.9" | 440px |
|
||||
| iPad Mini 7 | 8.3" | 768px |
|
||||
| iPad Pro 11 | 11" | 834px (세로), 1194px (가로) |
|
||||
| iPad Pro 13 | 13" | 1024px (세로), 1366px (가로) |
|
||||
|
||||
### 업계 표준 브레이크포인트
|
||||
|
||||
| 프레임워크 | 모바일/태블릿 경계 | 태블릿/데스크톱 경계 |
|
||||
|-----------|------------------|-------------------|
|
||||
| Tailwind CSS | 768px | 1024px |
|
||||
| Bootstrap 5 | 768px | 992px |
|
||||
| Material Design 3 | 600px | 840px |
|
||||
|
||||
**공통점**: 768px, 1024px가 거의 표준
|
||||
|
||||
---
|
||||
|
||||
## 결정 (Decision)
|
||||
|
||||
### 채택: 기기 기반 브레이크포인트
|
||||
|
||||
| 모드 | 너비 범위 | 변경 전 | 근거 |
|
||||
|------|----------|--------|------|
|
||||
| mobile_portrait | 0~479px | 0~599px | 스마트폰 세로 최대 440px |
|
||||
| mobile_landscape | 480~767px | 600~839px | 스마트폰 가로, 767px까지 |
|
||||
| tablet_portrait | 768~1023px | 840~1023px | iPad Mini 768px 포함 |
|
||||
| tablet_landscape | 1024px+ | 동일 | 대형 태블릿 가로 |
|
||||
|
||||
### 핵심 변경
|
||||
|
||||
```typescript
|
||||
// pop-layout.ts - GRID_BREAKPOINTS
|
||||
mobile_portrait: { maxWidth: 479 } // was 599
|
||||
mobile_landscape: { minWidth: 480, maxWidth: 767 } // was 600, 839
|
||||
tablet_portrait: { minWidth: 768, maxWidth: 1023 } // was 840, 1023
|
||||
tablet_landscape: { minWidth: 1024 } // 동일
|
||||
|
||||
// pop-layout.ts - detectGridMode()
|
||||
if (viewportWidth < 480) return "mobile_portrait"; // was 600
|
||||
if (viewportWidth < 768) return "mobile_landscape"; // was 840
|
||||
if (viewportWidth < 1024) return "tablet_portrait";
|
||||
|
||||
// useDeviceOrientation.ts - BREAKPOINTS
|
||||
TABLET_MIN: 768 // was 840
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 구현 (Implementation)
|
||||
|
||||
### 수정 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `pop-layout.ts` | GRID_BREAKPOINTS 값 수정, detectGridMode() 조건 수정 |
|
||||
| `useDeviceOrientation.ts` | BREAKPOINTS.TABLET_MIN = 768 |
|
||||
| `PopCanvas.tsx` | VIEWPORT_PRESETS width 값 조정 |
|
||||
| `page.tsx (뷰어)` | detectGridMode() 사용으로 일관성 확보 |
|
||||
|
||||
### 뷰어 모드 감지 방식 변경
|
||||
|
||||
```typescript
|
||||
// 변경 전: useResponsiveModeWithOverride만 사용
|
||||
const currentModeKey = getModeKey(deviceType, isLandscape);
|
||||
|
||||
// 변경 후: 프리뷰 모드와 일반 모드 분리
|
||||
const currentModeKey = isPreviewMode
|
||||
? getModeKey(deviceType, isLandscape) // 프리뷰: 수동 선택
|
||||
: detectGridMode(viewportWidth); // 일반: 너비 기반
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 결과 (Consequences)
|
||||
|
||||
### 긍정적 효과
|
||||
|
||||
| 효과 | 설명 |
|
||||
|------|------|
|
||||
| **기기 커버리지** | 아이폰 SE ~ 갤럭시 울트라, 8~14인치 태블릿 모두 포함 |
|
||||
| **업계 표준 호환** | 768px, 1024px는 거의 모든 프레임워크 기준점 |
|
||||
| **일관성 확보** | GRID_BREAKPOINTS와 detectGridMode() 완전 일치 |
|
||||
| **직관적 매핑** | 스마트폰 세로/가로, 태블릿 세로/가로 자연스럽게 분류 |
|
||||
|
||||
### 트레이드오프
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| **기존 데이터 영향** | 600~767px 구간이 6칸→6칸 (영향 없음) |
|
||||
| **768~839px 변경** | 기존 6칸→8칸 (태블릿으로 재분류) |
|
||||
|
||||
---
|
||||
|
||||
## 세로 자동 확장 (추가 결정)
|
||||
|
||||
### 배경
|
||||
|
||||
> "세로는 신경쓸 필요가 없는 것 맞지?
|
||||
> 그렇다면 캔버스도 세로 무한 스크롤이 가능해야겠네?"
|
||||
|
||||
### 결정
|
||||
|
||||
1. **뷰포트 프리셋에서 height 제거** (width만 유지)
|
||||
2. **캔버스 높이 동적 계산** (컴포넌트 배치 기준)
|
||||
3. **항상 여유 행 3개 유지** (추가 배치 공간)
|
||||
4. **뷰어에서 터치 스크롤** 지원
|
||||
|
||||
### 구현
|
||||
|
||||
```typescript
|
||||
// PopCanvas.tsx
|
||||
const MIN_CANVAS_HEIGHT = 600;
|
||||
const CANVAS_EXTRA_ROWS = 3;
|
||||
|
||||
const dynamicCanvasHeight = useMemo(() => {
|
||||
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
||||
return Math.max(max, comp.row + comp.rowSpan);
|
||||
}, 1);
|
||||
|
||||
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS;
|
||||
return Math.max(MIN_CANVAS_HEIGHT, totalRows * rowHeight);
|
||||
}, [layout.components, ...]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [003-v5-grid-system.md](./003-v5-grid-system.md) - v5 그리드 시스템 채택
|
||||
- [006-auto-wrap-review-system.md](./006-auto-wrap-review-system.md) - 자동 줄바꿈
|
||||
|
||||
---
|
||||
|
||||
**결론**: 실제 기기 뷰포트 기반 브레이크포인트로 일관성 확보 + 세로 무한 스크롤로 UX 개선
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
# ADR 006: v5.1 자동 줄바꿈 + 검토 필요 시스템
|
||||
|
||||
**날짜**: 2026-02-06
|
||||
**상태**: 채택
|
||||
**의사결정자**: 시스템 아키텍트
|
||||
|
||||
---
|
||||
|
||||
## 상황 (Context)
|
||||
|
||||
v5 반응형 레이아웃에서 "화면 밖" 개념으로 컴포넌트를 처리했으나, 다음 문제가 발생했습니다:
|
||||
|
||||
### 문제 1: 정보 손실
|
||||
```
|
||||
12칸 모드:
|
||||
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
|
||||
│ A │ B (col=5, 6칸) │
|
||||
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
|
||||
|
||||
4칸 모드 (기존):
|
||||
┌────┬────┬────┬────┐ 화면 밖:
|
||||
│ A │ │ - B
|
||||
└────┴────┴────┴────┘
|
||||
↑ A만 보임 ↑ 뷰어에서 안 보임!
|
||||
```
|
||||
|
||||
### 문제 2: 사용자 의도 불일치
|
||||
사용자가 기대한 "화면 밖" 역할:
|
||||
- ❌ 컴포넌트 숨김 (현재 동작)
|
||||
- ✅ "이 컴포넌트 검토 필요" 알림
|
||||
|
||||
---
|
||||
|
||||
## 결정 (Decision)
|
||||
|
||||
### 채택: 자동 줄바꿈 + 검토 필요 시스템
|
||||
|
||||
```
|
||||
col > maxCol → 자동으로 맨 아래에 배치 (줄바꿈)
|
||||
오버라이드 없음 → "검토 필요" 알림
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 구현 (Implementation)
|
||||
|
||||
### 1. 자동 줄바꿈 로직
|
||||
|
||||
**파일**: `gridUtils.ts` - `convertAndResolvePositions()`
|
||||
|
||||
```typescript
|
||||
// 단계별 처리:
|
||||
1. 비율 변환 + 원본 col 보존
|
||||
converted = components.map(comp => ({
|
||||
id: comp.id,
|
||||
position: convertPositionToMode(comp.position, targetMode),
|
||||
originalCol: comp.position.col, // ⭐ 원본 보존
|
||||
}))
|
||||
|
||||
2. 정상 vs 초과 분리
|
||||
normalComponents = originalCol ≤ targetColumns
|
||||
overflowComponents = originalCol > targetColumns
|
||||
|
||||
3. 초과 컴포넌트 자동 배치
|
||||
maxRow = normalComponents의 최대 row
|
||||
overflowComponents → col=1, row=맨아래+1
|
||||
|
||||
4. 겹침 해결
|
||||
resolveOverlaps([...normalComponents, ...wrappedComponents])
|
||||
```
|
||||
|
||||
### 2. 검토 필요 판별
|
||||
|
||||
**파일**: `gridUtils.ts` - `needsReview()`
|
||||
|
||||
```typescript
|
||||
function needsReview(
|
||||
currentMode: GridMode,
|
||||
hasOverride: boolean
|
||||
): boolean {
|
||||
// 12칸 모드는 기본 모드이므로 검토 불필요
|
||||
if (GRID_BREAKPOINTS[currentMode].columns === 12) return false;
|
||||
|
||||
// 오버라이드가 있으면 이미 편집함 → 검토 완료
|
||||
if (hasOverride) return false;
|
||||
|
||||
// 오버라이드 없으면 → 검토 필요
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**판단 기준 (최종)**: "이 모드에서 편집했냐 안 했냐"
|
||||
|
||||
### 3. 검토 필요 패널
|
||||
|
||||
**파일**: `PopCanvas.tsx` - `ReviewPanel`
|
||||
|
||||
```typescript
|
||||
// 필터링
|
||||
const reviewComponents = visibleComponents.filter(comp => {
|
||||
const hasOverride = !!layout.overrides?.[currentMode]?.positions?.[comp.id];
|
||||
return needsReview(currentMode, hasOverride);
|
||||
});
|
||||
|
||||
// UI
|
||||
<ReviewPanel
|
||||
components={reviewComponents}
|
||||
onSelectComponent={onSelectComponent} // 클릭 시 선택
|
||||
/>
|
||||
```
|
||||
|
||||
**변경 사항**:
|
||||
- 기존: `OutOfBoundsPanel` (주황색, 드래그로 복원)
|
||||
- 변경: `ReviewPanel` (파란색, 클릭으로 선택)
|
||||
|
||||
---
|
||||
|
||||
## 결과 (Consequences)
|
||||
|
||||
### 긍정적 효과
|
||||
|
||||
| 효과 | 설명 |
|
||||
|------|------|
|
||||
| **정보 손실 방지** | 모든 컴포넌트가 항상 그리드 안에 표시됨 |
|
||||
| **사용자 부담 감소** | 자동 배치를 먼저 제공, 필요시에만 편집 |
|
||||
| **의도 명확화** | "숨김" ≠ "검토 필요" (기능 분리) |
|
||||
| **뷰어 호환** | 자동 배치가 뷰어에도 적용됨 |
|
||||
|
||||
### 트레이드오프
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| **스크롤 증가** | 아래로 자동 배치되면 페이지가 길어질 수 있음 |
|
||||
| **자동 배치 품질** | 사용자가 원하지 않는 위치에 배치될 수 있음 |
|
||||
|
||||
---
|
||||
|
||||
## 사용자 시나리오
|
||||
|
||||
### 시나리오 1: 수용 (자동 배치 그대로)
|
||||
```
|
||||
1. 12칸에서 컴포넌트 A, B, C 배치
|
||||
2. 4칸 모드로 전환
|
||||
3. 시스템: 자동 배치 + "검토 필요 (3개)" 알림
|
||||
4. 사용자: 확인 → "괜찮네" → 아무것도 안 함
|
||||
5. 결과: 자동 배치 유지 (오버라이드 없음)
|
||||
```
|
||||
|
||||
### 시나리오 2: 편집 (오버라이드 저장)
|
||||
```
|
||||
1. 12칸에서 컴포넌트 A, B, C 배치
|
||||
2. 4칸 모드로 전환
|
||||
3. 시스템: 자동 배치 + "검토 필요 (3개)" 알림
|
||||
4. 사용자: A 클릭 → 드래그/리사이즈
|
||||
5. 결과: A 오버라이드 저장 → A 검토 완료
|
||||
6. "검토 필요 (2개)" (B, C만 남음)
|
||||
```
|
||||
|
||||
### 시나리오 3: 보류 (나중에)
|
||||
```
|
||||
1. 12칸에서 컴포넌트 A, B, C 배치
|
||||
2. 4칸 모드로 전환
|
||||
3. 시스템: 자동 배치 + "검토 필요 (3개)" 알림
|
||||
4. 사용자: 다른 모드로 전환 또는 저장
|
||||
5. 결과: 자동 배치 유지, 나중에도 "검토 필요" 표시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 기능 비교
|
||||
|
||||
| 구분 | 역할 | 뷰어에서 | 판단 기준 |
|
||||
|------|------|---------|----------|
|
||||
| **검토 필요** | 자동 배치 알림 | **보임** | 오버라이드 없음 |
|
||||
| **숨김** | 의도적 숨김 | **안 보임** | hidden 배열에 ID |
|
||||
|
||||
---
|
||||
|
||||
## 대안 (Alternatives Considered)
|
||||
|
||||
### A안: 완전 자동 (채택 ✅)
|
||||
- 모든 초과 컴포넌트 자동 배치
|
||||
- "검토 필요" 알림으로 확인 유도
|
||||
- 업계 표준 (Webflow, Retool)
|
||||
|
||||
### B안: 선택적 자동 (미채택)
|
||||
- 첫 전환 시만 자동 배치
|
||||
- 사용자가 원하면 "화면 밖"으로 드래그
|
||||
- 복잡성 증가
|
||||
|
||||
### C안: 수동 배치 유지 (미채택)
|
||||
- 기존 "화면 밖" 패널 유지
|
||||
- 사용자가 모든 모드 수동 편집
|
||||
- 사용자 부담 과다
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
### 업계 표준 (2026년 기준)
|
||||
- **Grafana, Tableau**: Masonry Layout (조적식)
|
||||
- **Retool, PowerApps**: Vertical Stacking (수직 스택)
|
||||
- **Webflow, Framer**: CSS Grid Auto-Placement
|
||||
|
||||
**공통점**: "Fluid Reflow (유동적 재배치)" - 정보 손실 방지
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
|------|----------|
|
||||
| `gridUtils.ts` | convertAndResolvePositions, needsReview 추가 |
|
||||
| `PopCanvas.tsx` | ReviewPanel로 변경 |
|
||||
| `PopRenderer.tsx` | isOutOfBounds import 제거 |
|
||||
| `pop-layout.ts` | 타입 변경 없음 (기존 구조 유지) |
|
||||
|
||||
---
|
||||
|
||||
**결론**: 자동 줄바꿈 + 검토 필요 시스템으로 정보 손실 방지 및 사용자 부담 최소화
|
||||
|
|
@ -52,7 +52,7 @@ v5 그리드 시스템 통합 완료, 그리드 가이드 재설계, **드래그
|
|||
## 미완료
|
||||
|
||||
- [x] 실제 화면 테스트 (디자이너 페이지) → 완료, 정상 작동
|
||||
- [ ] 간격 조정 규칙 결정 (전역 고정 vs 화면별 vs 컴포넌트별)
|
||||
- [x] 간격 조정 규칙 결정 → 2026-02-06 Gap 프리셋으로 해결 (좁게/보통/넓게)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
# 2026-02-06 작업 기록
|
||||
|
||||
## 요약
|
||||
v5.1 자동 줄바꿈 + 검토 필요 시스템 완성, 브레이크포인트 재설계, 세로 자동 확장 구현
|
||||
|
||||
---
|
||||
|
||||
## 완료
|
||||
|
||||
### 브레이크포인트 재설계
|
||||
- [x] GRID_BREAKPOINTS 값 수정 (기기 기반)
|
||||
- [x] detectGridMode() 조건 수정
|
||||
- [x] useDeviceOrientation.ts TABLET_MIN 768로 변경
|
||||
- [x] 뷰어에서 detectGridMode() 사용하여 일관성 확보
|
||||
|
||||
### 세로 자동 확장
|
||||
- [x] VIEWPORT_PRESETS에서 height 속성 제거
|
||||
- [x] dynamicCanvasHeight useMemo 추가
|
||||
- [x] MIN_CANVAS_HEIGHT, CANVAS_EXTRA_ROWS 상수 추가
|
||||
- [x] gridLabels 동적 계산 (행 수 자동 조정)
|
||||
- [x] gridCells 동적 계산 (PopRenderer)
|
||||
- [x] 뷰어 프리뷰 모드 스크롤 지원
|
||||
|
||||
### 자동 줄바꿈 시스템 (v5.1)
|
||||
- [x] convertAndResolvePositions() 자동 줄바꿈 로직
|
||||
- [x] 원본 col 보존 로직
|
||||
- [x] 초과 컴포넌트 맨 아래 배치
|
||||
- [x] colSpan 자동 축소
|
||||
|
||||
### 검토 필요 시스템
|
||||
- [x] needsReview() 함수 추가
|
||||
- [x] OutOfBoundsPanel → ReviewPanel 변경
|
||||
- [x] 파란색 테마 (안내 느낌)
|
||||
- [x] 클릭 시 컴포넌트 선택
|
||||
|
||||
### 버그 수정
|
||||
- [x] hiddenComponentIds 중복 정의 에러 수정
|
||||
- [x] useDrop 의존성 배열 수정
|
||||
- [x] 검토 필요 패널 모드별 표시 불일치 수정
|
||||
|
||||
### 그리드 셀 크기 강제 고정 (v5.2.1)
|
||||
- [x] gridAutoRows → gridTemplateRows 변경 (행 높이 강제 고정)
|
||||
- [x] dynamicRowCount를 gridStyle과 gridCells에서 공유
|
||||
- [x] 컴포넌트 overflow: visible → overflow: hidden 변경
|
||||
- [x] PopRenderer dynamicRowCount에서 숨김 컴포넌트 제외
|
||||
- [x] PopCanvas와 PopRenderer의 여유행 기준 통일 (+3)
|
||||
- [x] 디버깅용 console.log 2개 삭제
|
||||
- [x] 뷰어 page.tsx viewportWidth 선언 순서 수정
|
||||
|
||||
---
|
||||
|
||||
## 브레이크포인트 변경 상세
|
||||
|
||||
### 변경 전 → 변경 후
|
||||
|
||||
| 모드 | 변경 전 | 변경 후 | 근거 |
|
||||
|------|--------|--------|------|
|
||||
| mobile_portrait | ~599px | ~479px | 스마트폰 세로 최대 440px |
|
||||
| mobile_landscape | 600~839px | 480~767px | 767px까지 스마트폰 |
|
||||
| tablet_portrait | 840~1023px | 768~1023px | iPad Mini 768px 포함 |
|
||||
| tablet_landscape | 1024px+ | 동일 | 변경 없음 |
|
||||
|
||||
### 연구 결과 (기기별 CSS 뷰포트)
|
||||
|
||||
| 기기 | CSS 뷰포트 너비 |
|
||||
|------|----------------|
|
||||
| iPhone SE | 375px |
|
||||
| iPhone 16 Pro | 402px |
|
||||
| Galaxy S25 Ultra | 440px |
|
||||
| iPad Mini 7 | 768px |
|
||||
| iPad Pro 11 | 834px (세로), 1194px (가로) |
|
||||
| iPad Pro 13 | 1024px (세로), 1366px (가로) |
|
||||
|
||||
---
|
||||
|
||||
## 세로 자동 확장 상세
|
||||
|
||||
### 핵심 상수
|
||||
|
||||
```typescript
|
||||
const MIN_CANVAS_HEIGHT = 600; // 최소 캔버스 높이 (px)
|
||||
const CANVAS_EXTRA_ROWS = 3; // 항상 유지되는 여유 행 수
|
||||
```
|
||||
|
||||
### 동적 높이 계산 로직
|
||||
|
||||
```typescript
|
||||
const dynamicCanvasHeight = useMemo(() => {
|
||||
const visibleComps = Object.values(layout.components)
|
||||
.filter(comp => !hiddenComponentIds.includes(comp.id));
|
||||
|
||||
if (visibleComps.length === 0) return MIN_CANVAS_HEIGHT;
|
||||
|
||||
const maxRowEnd = visibleComps.reduce((max, comp) => {
|
||||
const pos = getEffectivePosition(comp);
|
||||
return Math.max(max, pos.row + pos.rowSpan);
|
||||
}, 1);
|
||||
|
||||
const totalRows = maxRowEnd + CANVAS_EXTRA_ROWS;
|
||||
const height = totalRows * (rowHeight + gap) + padding * 2;
|
||||
|
||||
return Math.max(MIN_CANVAS_HEIGHT, height);
|
||||
}, [dependencies]);
|
||||
```
|
||||
|
||||
### 영향받는 영역
|
||||
|
||||
| 영역 | 변경 |
|
||||
|------|------|
|
||||
| 캔버스 컨테이너 | minHeight: dynamicCanvasHeight |
|
||||
| 디바이스 스크린 | minHeight: dynamicCanvasHeight |
|
||||
| 행 라벨 | 동적 행 수 계산 |
|
||||
| 격자 셀 | 동적 행 수 계산 |
|
||||
|
||||
---
|
||||
|
||||
## 자동 줄바꿈 로직 상세
|
||||
|
||||
### 처리 단계
|
||||
|
||||
```
|
||||
1. 비율 변환 + 원본 col 보존
|
||||
converted = map(comp => ({
|
||||
position: convertPositionToMode(comp.position),
|
||||
originalCol: comp.position.col, // 원본 보존
|
||||
}))
|
||||
|
||||
2. 정상 vs 초과 분리
|
||||
normalComponents = filter(originalCol <= targetColumns)
|
||||
overflowComponents = filter(originalCol > targetColumns)
|
||||
|
||||
3. 초과 컴포넌트 맨 아래 배치
|
||||
maxRow = normalComponents의 최대 (row + rowSpan - 1)
|
||||
overflowComponents → col=1, row=maxRow+1
|
||||
|
||||
4. colSpan 자동 축소
|
||||
if (colSpan > targetColumns) colSpan = targetColumns
|
||||
|
||||
5. 겹침 해결
|
||||
resolveOverlaps([...normalComponents, ...wrappedComponents])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 대화 핵심
|
||||
|
||||
### 반응형 불일치 문제
|
||||
|
||||
**사용자 리포트**:
|
||||
> "아이폰 SE, iPad Pro 프리셋은 잘 되는데,
|
||||
> 브라우저 수동 리사이즈 시 6칸 모드가 적용 안 되는 것 같아"
|
||||
|
||||
**원인 분석**:
|
||||
- useResponsiveMode: width/height 비율로 landscape/portrait 판정
|
||||
- GRID_BREAKPOINTS: 순수 너비 기반
|
||||
- 768~839px 구간에서 불일치 발생
|
||||
|
||||
**해결**:
|
||||
- 뷰어에서 detectGridMode(viewportWidth) 사용
|
||||
- 프리뷰 모드만 useResponsiveModeWithOverride 유지
|
||||
|
||||
### 세로 무한 스크롤 결정
|
||||
|
||||
**사용자 질문**:
|
||||
> "우리 화면 모드는 너비만 신경쓰면 되잖아?
|
||||
> 세로는 무한 스크롤이 가능해야 하겠네?"
|
||||
|
||||
**확인 사항**:
|
||||
1. 너비만 신경쓰면 됨 ✅
|
||||
2. 캔버스 세로 무한 스크롤 필요 ✅
|
||||
3. 뷰어에서 터치 스크롤 지원 ✅
|
||||
|
||||
**구현 방식 선택**:
|
||||
- 수동 행 추가 방식 vs **자동 확장 방식 (채택)**
|
||||
- 이유: 여유 공간 3행 자동 유지, 사용자 부담 최소화
|
||||
|
||||
---
|
||||
|
||||
## 빌드 결과
|
||||
|
||||
```
|
||||
exit_code: 0
|
||||
주요 변경 파일: 6개
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 관련 링크
|
||||
|
||||
- ADR: [decisions/005-breakpoint-redesign.md](../decisions/005-breakpoint-redesign.md)
|
||||
- ADR: [decisions/006-auto-wrap-review-system.md](../decisions/006-auto-wrap-review-system.md)
|
||||
- 이전 세션: [sessions/2026-02-05.md](./2026-02-05.md)
|
||||
|
||||
---
|
||||
|
||||
## 이번 작업에서 배운 것
|
||||
|
||||
### 새로 알게 된 기술 개념
|
||||
|
||||
- **gridAutoRows vs gridTemplateRows**: `gridAutoRows`는 행의 *최소* 높이만 보장하고 콘텐츠에 따라 늘어날 수 있음. `gridTemplateRows`는 행 높이를 *강제 고정*함. 가이드 셀과 컴포넌트가 같은 Grid 컨테이너에 있을 때, 컴포넌트 콘텐츠가 행 높이를 밀어내면 인접한 빈 가이드 셀 크기도 함께 변해 시각적 불일치가 발생함.
|
||||
|
||||
### 발생했던 에러와 원인 패턴
|
||||
|
||||
| 에러 | 원인 패턴 |
|
||||
|------|-----------|
|
||||
| 그리드 셀 크기 불균일 | 같은 CSS Grid에서 gridAutoRows(최소값)를 사용하면 콘텐츠가 행 높이를 변형시킴 |
|
||||
| Canvas vs Renderer 행 수 불일치 | 같은 데이터(행 수)를 두 곳에서 계산하면서 필터 조건(숨김 제외)이 달랐음 |
|
||||
| 디버깅 console.log 잔존 | 기능 완료 후 정리 단계를 생략함 |
|
||||
| viewportWidth 참조 순서 | 변수 사용 코드가 선언 코드보다 위에 위치 (JS 호이스팅으로 동작은 하지만 가독성 저하) |
|
||||
|
||||
### 다음에 비슷한 작업할 때 주의할 점
|
||||
|
||||
1. **CSS Grid에서 "고정 크기" 셀이 필요하면 `gridTemplateRows`를 사용**하고, `gridAutoRows`는 동적 추가행 대비용으로만 유지
|
||||
2. **같은 데이터를 여러 곳에서 계산할 때, 필터 조건이 동일한지 반드시 비교** (숨김 제외 등)
|
||||
3. **기능 완료 후 `console.log`를 Grep으로 검색하여 디버깅 로그 정리**
|
||||
4. **변수 선언 순서는 의존 관계 순서와 일치**시켜야 가독성과 유지보수성 확보
|
||||
|
||||
---
|
||||
|
||||
## 중단점
|
||||
|
||||
> **다음 작업**: Phase 4 실제 컴포넌트 구현
|
||||
> - pop-label, pop-button 등 실제 렌더링 구현
|
||||
> - 데이터 바인딩 연결
|
||||
> - STATUS.md의 "다음 작업" 섹션 참조
|
||||
|
||||
---
|
||||
|
||||
## 다음 작업자 참고
|
||||
|
||||
1. **테스트 필요**
|
||||
- 아이폰 SE 실기기 테스트
|
||||
- iPad Mini 세로 모드 확인
|
||||
- 브라우저 리사이즈로 모드 전환 확인
|
||||
|
||||
2. **향후 작업**
|
||||
- Phase 4: 실제 컴포넌트 구현 (pop-label, pop-button 등)
|
||||
- 데이터 바인딩 연결
|
||||
- 워크플로우 연동
|
||||
Loading…
Reference in New Issue