feat(pop-card-list): PopCardList 컴포넌트 구현

- PopCardList 컴포넌트 추가 (NumberInputModal, PackageUnitModal 포함)
- ComponentEditorPanel, PopRenderer 충돌 해결 (modals + onRequestResize 통합)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shin
2026-02-24 15:54:57 +09:00
parent 3336384434
commit 8cfd4024e1
19 changed files with 2099 additions and 372 deletions

View File

@@ -116,6 +116,8 @@ interface PopCanvasProps {
onLockLayout?: () => void;
onResetOverride?: (mode: GridMode) => void;
onChangeGapPreset?: (preset: GapPreset) => void;
/** 컴포넌트가 자신의 rowSpan/colSpan 변경을 요청 (CardList 확장 등) */
onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
/** 대시보드 페이지 미리보기 인덱스 (-1이면 기본 모드) */
previewPageIndex?: number;
/** 현재 활성 캔버스 ID ("main" 또는 모달 ID) */
@@ -147,6 +149,7 @@ export default function PopCanvas({
onLockLayout,
onResetOverride,
onChangeGapPreset,
onRequestResize,
previewPageIndex,
activeCanvasId = "main",
onActiveCanvasChange,
@@ -761,6 +764,7 @@ export default function PopCanvas({
onComponentMove={onMoveComponent}
onComponentResize={onResizeComponent}
onComponentResizeEnd={onResizeEnd}
onRequestResize={onRequestResize}
overrideGap={adjustedGap}
overridePadding={adjustedPadding}
previewPageIndex={previewPageIndex}

View File

@@ -55,6 +55,7 @@ export default function PopDesigner({
onBackToList,
onScreenUpdate,
}: PopDesignerProps) {
// ========================================
// 레이아웃 상태
// ========================================
@@ -489,6 +490,56 @@ export default function PopDesigner({
[layout, saveToHistory]
);
// 컴포넌트가 자신의 rowSpan/colSpan을 동적으로 변경 요청 (CardList 확장 등)
const handleRequestResize = useCallback(
(componentId: string, newRowSpan: number, newColSpan?: number) => {
const component = layout.components[componentId];
if (!component) return;
const newPosition = {
...component.position,
rowSpan: newRowSpan,
...(newColSpan !== undefined ? { colSpan: newColSpan } : {}),
};
// 기본 모드(tablet_landscape)인 경우: 원본 position 직접 수정
if (currentMode === "tablet_landscape") {
const newLayout = {
...layout,
components: {
...layout.components,
[componentId]: {
...component,
position: newPosition,
},
},
};
setLayout(newLayout);
saveToHistory(newLayout);
setHasChanges(true);
} else {
// 다른 모드인 경우: 오버라이드에 저장
const newLayout = {
...layout,
overrides: {
...layout.overrides,
[currentMode]: {
...layout.overrides?.[currentMode],
positions: {
...layout.overrides?.[currentMode]?.positions,
[componentId]: newPosition,
},
},
},
};
setLayout(newLayout);
saveToHistory(newLayout);
setHasChanges(true);
}
},
[layout, currentMode, saveToHistory]
);
// ========================================
// Gap 프리셋 관리
// ========================================
@@ -830,6 +881,7 @@ export default function PopDesigner({
onLockLayout={handleLockLayout}
onResetOverride={handleResetOverride}
onChangeGapPreset={handleChangeGapPreset}
onRequestResize={handleRequestResize}
previewPageIndex={previewPageIndex}
activeCanvasId={activeCanvasId}
onActiveCanvasChange={navigateToCanvas}

View File

@@ -209,6 +209,7 @@ export default function ComponentEditorPanel({
<ComponentSettingsForm
component={component}
onUpdate={onUpdateComponent}
currentMode={currentMode}
previewPageIndex={previewPageIndex}
onPreviewPage={onPreviewPage}
modals={modals}
@@ -399,12 +400,13 @@ function PositionForm({ component, currentMode, isDefaultMode, columns, onUpdate
interface ComponentSettingsFormProps {
component: PopComponentDefinitionV5;
onUpdate?: (updates: Partial<PopComponentDefinitionV5>) => void;
currentMode?: GridMode;
previewPageIndex?: number;
onPreviewPage?: (pageIndex: number) => void;
modals?: PopModalDefinition[];
}
function ComponentSettingsForm({ component, onUpdate, previewPageIndex, onPreviewPage, modals }: ComponentSettingsFormProps) {
function ComponentSettingsForm({ component, onUpdate, currentMode, previewPageIndex, onPreviewPage, modals }: ComponentSettingsFormProps) {
// PopComponentRegistry에서 configPanel 가져오기
const registeredComp = PopComponentRegistry.getComponent(component.type);
const ConfigPanel = registeredComp?.configPanel;
@@ -433,6 +435,8 @@ function ComponentSettingsForm({ component, onUpdate, previewPageIndex, onPrevie
<ConfigPanel
config={component.config || {}}
onUpdate={handleConfigUpdate}
currentMode={currentMode}
currentColSpan={component.position.colSpan}
onPreviewPage={onPreviewPage}
previewPageIndex={previewPageIndex}
modals={modals}

View File

@@ -48,6 +48,8 @@ interface PopRendererProps {
onComponentResize?: (componentId: string, newPosition: PopGridPosition) => void;
/** 컴포넌트 크기 조정 완료 (히스토리 저장용) */
onComponentResizeEnd?: (componentId: string) => void;
/** 컴포넌트가 자신의 rowSpan/colSpan 변경을 요청 (CardList 확장 등) */
onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
/** Gap 오버라이드 (Gap 프리셋 적용된 값) */
overrideGap?: number;
/** Padding 오버라이드 (Gap 프리셋 적용된 값) */
@@ -91,6 +93,7 @@ export default function PopRenderer({
onComponentMove,
onComponentResize,
onComponentResizeEnd,
onRequestResize,
overrideGap,
overridePadding,
className,
@@ -270,6 +273,7 @@ export default function PopRenderer({
onComponentMove={onComponentMove}
onComponentResize={onComponentResize}
onComponentResizeEnd={onComponentResizeEnd}
onRequestResize={onRequestResize}
previewPageIndex={previewPageIndex}
/>
);
@@ -279,7 +283,7 @@ export default function PopRenderer({
return (
<div
key={comp.id}
className="relative rounded-lg border-2 border-gray-200 bg-white transition-all z-10"
className="relative overflow-hidden rounded-lg border-2 border-gray-200 bg-white transition-all z-10"
style={positionStyle}
>
<ComponentContent
@@ -287,7 +291,8 @@ export default function PopRenderer({
effectivePosition={position}
isDesignMode={false}
isSelected={false}
screenId={String(currentScreenId || "")}
onRequestResize={onRequestResize}
screenId={currentScreenId ? String(currentScreenId) : undefined}
/>
</div>
);
@@ -315,6 +320,7 @@ interface DraggableComponentProps {
onComponentMove?: (componentId: string, newPosition: PopGridPosition) => void;
onComponentResize?: (componentId: string, newPosition: PopGridPosition) => void;
onComponentResizeEnd?: (componentId: string) => void;
onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
previewPageIndex?: number;
}
@@ -333,6 +339,7 @@ function DraggableComponent({
onComponentMove,
onComponentResize,
onComponentResizeEnd,
onRequestResize,
previewPageIndex,
}: DraggableComponentProps) {
const [{ isDragging }, drag] = useDrag(
@@ -373,7 +380,8 @@ function DraggableComponent({
isDesignMode={isDesignMode}
isSelected={isSelected}
previewPageIndex={previewPageIndex}
screenId=""
onRequestResize={onRequestResize}
screenId={undefined}
/>
{/* 리사이즈 핸들 (선택된 컴포넌트만) */}
@@ -525,11 +533,12 @@ interface ComponentContentProps {
isDesignMode: boolean;
isSelected: boolean;
previewPageIndex?: number;
onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
/** 화면 ID (이벤트 버스/액션 실행용) */
screenId?: string;
}
function ComponentContent({ component, effectivePosition, isDesignMode, isSelected, previewPageIndex, screenId }: ComponentContentProps) {
function ComponentContent({ component, effectivePosition, isDesignMode, isSelected, previewPageIndex, onRequestResize, screenId }: ComponentContentProps) {
const typeLabel = COMPONENT_TYPE_LABELS[component.type] || component.type;
// PopComponentRegistry에서 등록된 컴포넌트 가져오기
@@ -543,7 +552,8 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
// 실제 컴포넌트가 등록되어 있으면 실제 데이터로 렌더링 (대시보드 등)
if (ActualComp) {
// 아이콘 컴포넌트는 클릭 이벤트가 필요하므로 pointer-events 허용
const needsPointerEvents = component.type === "pop-icon";
// CardList 컴포넌트도 버튼 클릭이 필요하므로 pointer-events 허용
const needsPointerEvents = component.type === "pop-icon" || component.type === "pop-card-list";
return (
<div className={cn(
@@ -555,7 +565,11 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
label={component.label}
isDesignMode={isDesignMode}
previewPageIndex={previewPageIndex}
componentId={component.id}
screenId={screenId}
currentRowSpan={effectivePosition.rowSpan}
currentColSpan={effectivePosition.colSpan}
onRequestResize={onRequestResize}
/>
</div>
);
@@ -575,23 +589,36 @@ function ComponentContent({ component, effectivePosition, isDesignMode, isSelect
);
}
// 실제 모드: 컴포넌트 렌더링
return renderActualComponent(component, screenId);
// 실제 모드: 컴포넌트 렌더링 (뷰어 모드에서도 리사이즈 지원)
return renderActualComponent(component, effectivePosition, onRequestResize, screenId);
}
// ========================================
// 실제 컴포넌트 렌더링 (뷰어 모드)
// ========================================
function renderActualComponent(component: PopComponentDefinitionV5, screenId?: string): React.ReactNode {
function renderActualComponent(
component: PopComponentDefinitionV5,
effectivePosition?: PopGridPosition,
onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void,
screenId?: string,
): React.ReactNode {
// 레지스트리에서 등록된 실제 컴포넌트 조회
const registeredComp = PopComponentRegistry.getComponent(component.type);
const ActualComp = registeredComp?.component;
if (ActualComp) {
return (
<div className="w-full min-h-full">
<ActualComp config={component.config} label={component.label} screenId={screenId} componentId={component.id} />
<div className="h-full w-full overflow-hidden">
<ActualComp
config={component.config}
label={component.label}
componentId={component.id}
screenId={screenId}
currentRowSpan={effectivePosition?.rowSpan}
currentColSpan={effectivePosition?.colSpan}
onRequestResize={onRequestResize}
/>
</div>
);
}