feat(pop): 액션 아키텍처 + 모달 시스템 구현 (STEP 0~7)
- executePopAction / usePopAction 훅 신규 생성 - pop-button을 usePopAction 기반으로 리팩토링 - PopModalDefinition 타입 + MODAL_SIZE_PRESETS 정의 - PopDesignerContext 신규 생성 (모달 탭 상태 공유) - PopDesigner에 모달 탭 UI 추가 (메인 캔버스 / 모달 캔버스 전환) - PopCanvas에 접이식 ModalSizeSettingsPanel + ModalThumbnailPreview 구현 - PopViewerWithModals 신규 생성 (뷰어 모달 렌더링 + 스택 관리) - FULL 모달 전체화면 지원 (h-dvh, w-screen, rounded-none) - pop-string-list 카드 버튼 액션 연동 - pop-icon / SelectedItemsDetailInput lucide import 최적화 - tsconfig skipLibCheck 설정 추가 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* 오버플로우: visibleRows 제한 + "전체보기" 확장
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { ChevronDown, ChevronUp, Loader2, AlertCircle, ChevronsUpDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { executePopAction } from "@/hooks/pop/executePopAction";
|
||||
import { usePopEvent } from "@/hooks/pop/usePopEvent";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
PopStringListConfig,
|
||||
CardGridConfig,
|
||||
@@ -43,6 +46,7 @@ function resolveColumnName(name: string): string {
|
||||
interface PopStringListComponentProps {
|
||||
config?: PopStringListConfig;
|
||||
className?: string;
|
||||
screenId?: string;
|
||||
}
|
||||
|
||||
// 테이블 행 데이터 타입
|
||||
@@ -53,6 +57,7 @@ type RowData = Record<string, unknown>;
|
||||
export function PopStringListComponent({
|
||||
config,
|
||||
className,
|
||||
screenId,
|
||||
}: PopStringListComponentProps) {
|
||||
const displayMode = config?.displayMode || "list";
|
||||
const header = config?.header;
|
||||
@@ -67,6 +72,46 @@ export function PopStringListComponent({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
// 카드 버튼 행 단위 로딩 인덱스 (-1 = 로딩 없음)
|
||||
const [loadingRowIdx, setLoadingRowIdx] = useState<number>(-1);
|
||||
|
||||
// 이벤트 발행 (카드 버튼 액션에서 사용)
|
||||
const { publish } = usePopEvent(screenId || "");
|
||||
|
||||
// 카드 버튼 클릭 핸들러
|
||||
const handleCardButtonClick = useCallback(
|
||||
async (cell: CardCellDefinition, row: RowData) => {
|
||||
if (!cell.buttonAction) return;
|
||||
|
||||
// 확인 다이얼로그 (간단 구현: window.confirm)
|
||||
if (cell.buttonConfirm?.enabled) {
|
||||
const msg = cell.buttonConfirm.message || "이 작업을 실행하시겠습니까?";
|
||||
if (!window.confirm(msg)) return;
|
||||
}
|
||||
|
||||
const rowIndex = rows.indexOf(row);
|
||||
setLoadingRowIdx(rowIndex);
|
||||
|
||||
try {
|
||||
const result = await executePopAction(cell.buttonAction, row as Record<string, unknown>, {
|
||||
publish,
|
||||
screenId,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success("작업이 완료되었습니다.");
|
||||
} else {
|
||||
toast.error(result.error || "작업에 실패했습니다.");
|
||||
}
|
||||
} catch {
|
||||
toast.error("알 수 없는 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setLoadingRowIdx(-1);
|
||||
}
|
||||
},
|
||||
[rows, publish, screenId]
|
||||
);
|
||||
|
||||
// 오버플로우 계산 (JSON 복원 시 string 유입 방어)
|
||||
const visibleRows = Number(overflow?.visibleRows) || 5;
|
||||
const maxExpandRows = Number(overflow?.maxExpandRows) || 20;
|
||||
@@ -83,9 +128,20 @@ export function PopStringListComponent({
|
||||
setExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// dataSource 원시값 추출 (객체 참조 대신 안정적인 의존성 사용)
|
||||
const dsTableName = dataSource?.tableName;
|
||||
const dsSortColumn = dataSource?.sort?.column;
|
||||
const dsSortDirection = dataSource?.sort?.direction;
|
||||
const dsLimitMode = dataSource?.limit?.mode;
|
||||
const dsLimitCount = dataSource?.limit?.count;
|
||||
const dsFiltersKey = useMemo(
|
||||
() => JSON.stringify(dataSource?.filters || []),
|
||||
[dataSource?.filters]
|
||||
);
|
||||
|
||||
// 데이터 조회
|
||||
useEffect(() => {
|
||||
if (!dataSource?.tableName) {
|
||||
if (!dsTableName) {
|
||||
setLoading(false);
|
||||
setRows([]);
|
||||
return;
|
||||
@@ -98,8 +154,9 @@ export function PopStringListComponent({
|
||||
try {
|
||||
// 필터 조건 구성
|
||||
const filters: Record<string, unknown> = {};
|
||||
if (dataSource.filters && dataSource.filters.length > 0) {
|
||||
dataSource.filters.forEach((f) => {
|
||||
const parsedFilters = JSON.parse(dsFiltersKey) as Array<{ column?: string; value?: string }>;
|
||||
if (parsedFilters.length > 0) {
|
||||
parsedFilters.forEach((f) => {
|
||||
if (f.column && f.value) {
|
||||
filters[f.column] = f.value;
|
||||
}
|
||||
@@ -107,16 +164,16 @@ export function PopStringListComponent({
|
||||
}
|
||||
|
||||
// 정렬 조건
|
||||
const sortBy = dataSource.sort?.column;
|
||||
const sortOrder = dataSource.sort?.direction;
|
||||
const sortBy = dsSortColumn;
|
||||
const sortOrder = dsSortDirection;
|
||||
|
||||
// 개수 제한 (string 유입 방어: Number 캐스팅)
|
||||
const size =
|
||||
dataSource.limit?.mode === "limited" && dataSource.limit?.count
|
||||
? Number(dataSource.limit.count)
|
||||
dsLimitMode === "limited" && dsLimitCount
|
||||
? Number(dsLimitCount)
|
||||
: maxExpandRows;
|
||||
|
||||
const result = await dataApi.getTableData(dataSource.tableName, {
|
||||
const result = await dataApi.getTableData(dsTableName, {
|
||||
page: 1,
|
||||
size,
|
||||
sortBy: sortOrder ? sortBy : undefined,
|
||||
@@ -136,7 +193,7 @@ export function PopStringListComponent({
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [dataSource, maxExpandRows]);
|
||||
}, [dsTableName, dsSortColumn, dsSortDirection, dsLimitMode, dsLimitCount, dsFiltersKey, maxExpandRows]);
|
||||
|
||||
// 로딩 상태
|
||||
if (loading) {
|
||||
@@ -199,6 +256,8 @@ export function PopStringListComponent({
|
||||
<CardModeView
|
||||
cardGrid={cardGrid}
|
||||
data={visibleData}
|
||||
handleCardButtonClick={handleCardButtonClick}
|
||||
loadingRowId={loadingRowIdx}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -376,9 +435,11 @@ function ListModeView({ columns, data }: ListModeViewProps) {
|
||||
interface CardModeViewProps {
|
||||
cardGrid?: CardGridConfig;
|
||||
data: RowData[];
|
||||
handleCardButtonClick?: (cell: CardCellDefinition, row: RowData) => void;
|
||||
loadingRowId?: number;
|
||||
}
|
||||
|
||||
function CardModeView({ cardGrid, data }: CardModeViewProps) {
|
||||
function CardModeView({ cardGrid, data, handleCardButtonClick, loadingRowId }: CardModeViewProps) {
|
||||
if (!cardGrid || (cardGrid.cells || []).length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-2">
|
||||
@@ -439,7 +500,7 @@ function CardModeView({ cardGrid, data }: CardModeViewProps) {
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
{renderCellContent(cell, row)}
|
||||
{renderCellContent(cell, row, handleCardButtonClick, loadingRowId === i)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -451,7 +512,12 @@ function CardModeView({ cardGrid, data }: CardModeViewProps) {
|
||||
|
||||
// ===== 셀 컨텐츠 렌더링 =====
|
||||
|
||||
function renderCellContent(cell: CardCellDefinition, row: RowData): React.ReactNode {
|
||||
function renderCellContent(
|
||||
cell: CardCellDefinition,
|
||||
row: RowData,
|
||||
onButtonClick?: (cell: CardCellDefinition, row: RowData) => void,
|
||||
isButtonLoading?: boolean,
|
||||
): React.ReactNode {
|
||||
const value = row[cell.columnName];
|
||||
const displayValue = value != null ? String(value) : "";
|
||||
|
||||
@@ -478,7 +544,16 @@ function renderCellContent(cell: CardCellDefinition, row: RowData): React.ReactN
|
||||
|
||||
case "button":
|
||||
return (
|
||||
<Button variant="outline" size="sm" className="h-6 text-[10px]">
|
||||
<Button
|
||||
variant={cell.buttonVariant || "outline"}
|
||||
size="sm"
|
||||
className="h-6 text-[10px]"
|
||||
disabled={isButtonLoading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onButtonClick?.(cell, row);
|
||||
}}
|
||||
>
|
||||
{cell.label || displayValue}
|
||||
</Button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user