feat(pop-card-list): 장바구니 목록 모드 (cartListMode) 구현
- CartListModeConfig 타입 추가 (sourceScreenId, cartType, statusFilter) - 원본 화면 카드 설정 자동 상속 (screenApi.getLayoutPop) - cart_items 조회 + row_data JSON 파싱 + __cart_ 접두사 병합 - 체크박스 선택 (전체/개별) + selected_items 이벤트 발행 - 인라인 삭제 (확인 후 즉시 DB 반영) - 수량 수정 (NumberInputModal 재사용, 로컬 __cart_modified) - 설정 패널: 장바구니 모드 토글 + 원본 화면/컴포넌트 선택 - connectionMeta: selected_items, confirm_trigger 추가
This commit is contained in:
parent
7bf20bda14
commit
aa319a6bda
|
|
@ -14,6 +14,7 @@ import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
|
Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
|
||||||
ShoppingCart, X, Package, Truck, Box, Archive, Heart, Star,
|
ShoppingCart, X, Package, Truck, Box, Archive, Heart, Star,
|
||||||
|
Trash2,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -28,12 +29,14 @@ import type {
|
||||||
CardPresetSpec,
|
CardPresetSpec,
|
||||||
CartItem,
|
CartItem,
|
||||||
PackageEntry,
|
PackageEntry,
|
||||||
|
CartListModeConfig,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CARD_IMAGE,
|
DEFAULT_CARD_IMAGE,
|
||||||
CARD_PRESET_SPECS,
|
CARD_PRESET_SPECS,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { dataApi } from "@/lib/api/data";
|
import { dataApi } from "@/lib/api/data";
|
||||||
|
import { screenApi } from "@/lib/api/screen";
|
||||||
import { usePopEvent } from "@/hooks/pop/usePopEvent";
|
import { usePopEvent } from "@/hooks/pop/usePopEvent";
|
||||||
import { useCartSync } from "@/hooks/pop/useCartSync";
|
import { useCartSync } from "@/hooks/pop/useCartSync";
|
||||||
import { NumberInputModal } from "./NumberInputModal";
|
import { NumberInputModal } from "./NumberInputModal";
|
||||||
|
|
@ -121,6 +124,28 @@ function MarqueeText({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cart_items 행의 row_data JSON을 풀어서 __cart_ 접두사 메타데이터와 병합
|
||||||
|
function parseCartRow(dbRow: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
let rowData: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
const raw = dbRow.row_data;
|
||||||
|
if (typeof raw === "string" && raw.trim()) rowData = JSON.parse(raw);
|
||||||
|
else if (typeof raw === "object" && raw !== null) rowData = raw as Record<string, unknown>;
|
||||||
|
} catch { rowData = {}; }
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rowData,
|
||||||
|
__cart_id: dbRow.id,
|
||||||
|
__cart_quantity: Number(dbRow.quantity) || 0,
|
||||||
|
__cart_package_unit: dbRow.package_unit || "",
|
||||||
|
__cart_package_entries: dbRow.package_entries,
|
||||||
|
__cart_status: dbRow.status || "in_cart",
|
||||||
|
__cart_memo: dbRow.memo || "",
|
||||||
|
__cart_row_key: dbRow.row_key || "",
|
||||||
|
__cart_modified: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface PopCardListComponentProps {
|
interface PopCardListComponentProps {
|
||||||
config?: PopCardListConfig;
|
config?: PopCardListConfig;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -172,6 +197,14 @@ export function PopCardListComponent({
|
||||||
const cartType = config?.cartAction?.cartType;
|
const cartType = config?.cartAction?.cartType;
|
||||||
const cart = useCartSync(screenId || "", sourceTableName, cartType);
|
const cart = useCartSync(screenId || "", sourceTableName, cartType);
|
||||||
|
|
||||||
|
// 장바구니 목록 모드 플래그 및 상태
|
||||||
|
const isCartListMode = config?.cartListMode?.enabled === true;
|
||||||
|
const [inheritedTemplate, setInheritedTemplate] = useState<CardTemplateConfig | null>(null);
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 장바구니 목록 모드에서는 원본 화면의 템플릿을 사용, 폴백으로 자체 설정
|
||||||
|
const effectiveTemplate = (isCartListMode && inheritedTemplate) ? inheritedTemplate : template;
|
||||||
|
|
||||||
// 데이터 상태
|
// 데이터 상태
|
||||||
const [rows, setRows] = useState<RowData[]>([]);
|
const [rows, setRows] = useState<RowData[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -219,9 +252,9 @@ export function PopCardListComponent({
|
||||||
const cartRef = useRef(cart);
|
const cartRef = useRef(cart);
|
||||||
cartRef.current = cart;
|
cartRef.current = cart;
|
||||||
|
|
||||||
// "저장 요청" 이벤트 수신: 버튼 컴포넌트가 장바구니 저장을 요청하면 saveToDb 실행
|
// "저장 요청" 이벤트 수신: 버튼 컴포넌트가 장바구니 저장을 요청하면 saveToDb 실행 (장바구니 목록 모드 제외)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!componentId) return;
|
if (!componentId || isCartListMode) return;
|
||||||
const unsub = subscribe(
|
const unsub = subscribe(
|
||||||
`__comp_input__${componentId}__cart_save_trigger`,
|
`__comp_input__${componentId}__cart_save_trigger`,
|
||||||
async (payload: unknown) => {
|
async (payload: unknown) => {
|
||||||
|
|
@ -233,16 +266,16 @@ export function PopCardListComponent({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return unsub;
|
return unsub;
|
||||||
}, [componentId, subscribe, publish]);
|
}, [componentId, subscribe, publish, isCartListMode]);
|
||||||
|
|
||||||
// DB 로드 완료 후 초기 장바구니 상태를 버튼에 전달
|
// DB 로드 완료 후 초기 장바구니 상태를 버튼에 전달 (장바구니 목록 모드 제외)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!componentId || cart.loading) return;
|
if (!componentId || cart.loading || isCartListMode) return;
|
||||||
publish(`__comp_output__${componentId}__cart_updated`, {
|
publish(`__comp_output__${componentId}__cart_updated`, {
|
||||||
count: cart.cartCount,
|
count: cart.cartCount,
|
||||||
isDirty: cart.isDirty,
|
isDirty: cart.isDirty,
|
||||||
});
|
});
|
||||||
}, [componentId, cart.loading, cart.cartCount, cart.isDirty, publish]);
|
}, [componentId, cart.loading, cart.cartCount, cart.isDirty, publish, isCartListMode]);
|
||||||
|
|
||||||
// 카드 선택 시 selected_row 이벤트 발행
|
// 카드 선택 시 selected_row 이벤트 발행
|
||||||
const handleCardSelect = useCallback((row: RowData) => {
|
const handleCardSelect = useCallback((row: RowData) => {
|
||||||
|
|
@ -454,7 +487,70 @@ export function PopCardListComponent({
|
||||||
[dataSource]
|
[dataSource]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 장바구니 목록 모드 설정을 직렬화 (의존성 안정화)
|
||||||
|
const cartListModeKey = useMemo(
|
||||||
|
() => JSON.stringify(config?.cartListMode || null),
|
||||||
|
[config?.cartListMode]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// 장바구니 목록 모드: cart_items에서 직접 조회
|
||||||
|
if (isCartListMode) {
|
||||||
|
const cartListMode = config!.cartListMode!;
|
||||||
|
|
||||||
|
// 설정 미완료 시 데이터 조회하지 않음
|
||||||
|
if (!cartListMode.sourceScreenId || !cartListMode.cartType) {
|
||||||
|
setLoading(false);
|
||||||
|
setRows([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchCartData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
// 원본 화면 레이아웃에서 cardTemplate 상속
|
||||||
|
if (cartListMode.sourceScreenId) {
|
||||||
|
try {
|
||||||
|
const layoutJson = await screenApi.getLayoutPop(cartListMode.sourceScreenId);
|
||||||
|
const components = layoutJson?.components || [];
|
||||||
|
const matched = components.find(
|
||||||
|
(c: any) =>
|
||||||
|
c.type === "pop-card-list" &&
|
||||||
|
c.props?.cartAction?.cartType === cartListMode.cartType
|
||||||
|
);
|
||||||
|
if (matched?.props?.cardTemplate) {
|
||||||
|
setInheritedTemplate(matched.props.cardTemplate);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 레이아웃 로드 실패 시 config.cardTemplate 폴백
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cart_items 조회
|
||||||
|
const result = await dataApi.getTableData("cart_items", {
|
||||||
|
size: 500,
|
||||||
|
filters: {
|
||||||
|
cart_type: cartListMode.cartType,
|
||||||
|
status: cartListMode.statusFilter || "in_cart",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = (result.data || []).map(parseCartRow);
|
||||||
|
setRows(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "장바구니 데이터 조회 실패";
|
||||||
|
setError(message);
|
||||||
|
setRows([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchCartData();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 모드: 데이터 소스에서 조회
|
||||||
if (!dataSource?.tableName) {
|
if (!dataSource?.tableName) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRows([]);
|
setRows([]);
|
||||||
|
|
@ -510,16 +606,51 @@ export function PopCardListComponent({
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [dataSourceKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [dataSourceKey, isCartListMode, cartListModeKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// 이미지 URL 없는 항목 카운트 (내부 추적용, toast 미표시)
|
// 이미지 URL 없는 항목 카운트 (내부 추적용, toast 미표시)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && rows.length > 0 && template?.image?.enabled && template?.image?.imageColumn) {
|
if (!loading && rows.length > 0 && effectiveTemplate?.image?.enabled && effectiveTemplate?.image?.imageColumn) {
|
||||||
const imageColumn = template.image.imageColumn;
|
const imageColumn = effectiveTemplate.image.imageColumn;
|
||||||
missingImageCountRef.current = rows.filter((row) => !row[imageColumn]).length;
|
missingImageCountRef.current = rows.filter((row) => !row[imageColumn]).length;
|
||||||
}
|
}
|
||||||
}, [loading, rows, template?.image]);
|
}, [loading, rows, effectiveTemplate?.image]);
|
||||||
|
|
||||||
|
// 장바구니 목록 모드: 항목 삭제 콜백
|
||||||
|
const handleDeleteItem = useCallback((cartId: string) => {
|
||||||
|
setRows(prev => prev.filter(r => String(r.__cart_id) !== cartId));
|
||||||
|
setSelectedKeys(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(cartId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 장바구니 목록 모드: 수량 수정 콜백 (로컬만 업데이트, DB 미반영)
|
||||||
|
const handleUpdateQuantity = useCallback((
|
||||||
|
cartId: string,
|
||||||
|
quantity: number,
|
||||||
|
unit?: string,
|
||||||
|
entries?: PackageEntry[],
|
||||||
|
) => {
|
||||||
|
setRows(prev => prev.map(r => {
|
||||||
|
if (String(r.__cart_id) !== cartId) return r;
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
__cart_quantity: quantity,
|
||||||
|
__cart_package_unit: unit || r.__cart_package_unit,
|
||||||
|
__cart_package_entries: entries || r.__cart_package_entries,
|
||||||
|
__cart_modified: true,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 장바구니 목록 모드: 선택 항목 이벤트 발행
|
||||||
|
useEffect(() => {
|
||||||
|
if (!componentId || !isCartListMode) return;
|
||||||
|
const selectedItems = filteredRows.filter(r => selectedKeys.has(String(r.__cart_id)));
|
||||||
|
publish(`__comp_output__${componentId}__selected_items`, selectedItems);
|
||||||
|
}, [selectedKeys, filteredRows, componentId, isCartListMode, publish]);
|
||||||
|
|
||||||
// 카드 영역 스타일
|
// 카드 영역 스타일
|
||||||
const cardAreaStyle: React.CSSProperties = {
|
const cardAreaStyle: React.CSSProperties = {
|
||||||
|
|
@ -549,7 +680,13 @@ export function PopCardListComponent({
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={`flex h-full w-full flex-col ${className || ""}`}
|
className={`flex h-full w-full flex-col ${className || ""}`}
|
||||||
>
|
>
|
||||||
{!dataSource?.tableName ? (
|
{isCartListMode && (!config?.cartListMode?.sourceScreenId || !config?.cartListMode?.cartType) ? (
|
||||||
|
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
원본 화면과 장바구니 구분값을 설정해주세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : !isCartListMode && !dataSource?.tableName ? (
|
||||||
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
|
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
데이터 소스를 설정해주세요.
|
데이터 소스를 설정해주세요.
|
||||||
|
|
@ -569,6 +706,27 @@ export function PopCardListComponent({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{/* 장바구니 목록 모드: 선택 바 */}
|
||||||
|
{isCartListMode && (
|
||||||
|
<div className="flex shrink-0 items-center gap-3 border-b px-3 py-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedKeys.size === displayCards.length && displayCards.length > 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedKeys(new Set(displayCards.map(r => String(r.__cart_id))));
|
||||||
|
} else {
|
||||||
|
setSelectedKeys(new Set());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-4 w-4 rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{selectedKeys.size > 0 ? `${selectedKeys.size}개 선택됨` : "전체 선택"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 카드 영역 (스크롤 가능) */}
|
{/* 카드 영역 (스크롤 가능) */}
|
||||||
<div
|
<div
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
|
|
@ -580,15 +738,15 @@ export function PopCardListComponent({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{displayCards.map((row, index) => {
|
{displayCards.map((row, index) => {
|
||||||
const codeValue = template?.header?.codeField && row[template.header.codeField]
|
const codeValue = effectiveTemplate?.header?.codeField && row[effectiveTemplate.header.codeField]
|
||||||
? String(row[template.header.codeField])
|
? String(row[effectiveTemplate.header.codeField])
|
||||||
: null;
|
: null;
|
||||||
const rowKey = codeValue ? `${codeValue}-${index}` : `card-${index}`;
|
const rowKey = codeValue ? `${codeValue}-${index}` : `card-${index}`;
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={rowKey}
|
key={rowKey}
|
||||||
row={row}
|
row={row}
|
||||||
template={template}
|
template={effectiveTemplate}
|
||||||
scaled={scaled}
|
scaled={scaled}
|
||||||
inputField={config?.inputField}
|
inputField={config?.inputField}
|
||||||
packageConfig={config?.packageConfig}
|
packageConfig={config?.packageConfig}
|
||||||
|
|
@ -597,8 +755,21 @@ export function PopCardListComponent({
|
||||||
router={router}
|
router={router}
|
||||||
onSelect={handleCardSelect}
|
onSelect={handleCardSelect}
|
||||||
cart={cart}
|
cart={cart}
|
||||||
codeFieldName={template?.header?.codeField}
|
codeFieldName={effectiveTemplate?.header?.codeField}
|
||||||
parentComponentId={componentId}
|
parentComponentId={componentId}
|
||||||
|
isCartListMode={isCartListMode}
|
||||||
|
isSelected={selectedKeys.has(String(row.__cart_id))}
|
||||||
|
onToggleSelect={() => {
|
||||||
|
const cartId = String(row.__cart_id);
|
||||||
|
setSelectedKeys(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(cartId)) next.delete(cartId);
|
||||||
|
else next.add(cartId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDeleteItem={handleDeleteItem}
|
||||||
|
onUpdateQuantity={handleUpdateQuantity}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -681,6 +852,11 @@ function Card({
|
||||||
cart,
|
cart,
|
||||||
codeFieldName,
|
codeFieldName,
|
||||||
parentComponentId,
|
parentComponentId,
|
||||||
|
isCartListMode,
|
||||||
|
isSelected,
|
||||||
|
onToggleSelect,
|
||||||
|
onDeleteItem,
|
||||||
|
onUpdateQuantity,
|
||||||
}: {
|
}: {
|
||||||
row: RowData;
|
row: RowData;
|
||||||
template?: CardTemplateConfig;
|
template?: CardTemplateConfig;
|
||||||
|
|
@ -694,6 +870,11 @@ function Card({
|
||||||
cart: ReturnType<typeof useCartSync>;
|
cart: ReturnType<typeof useCartSync>;
|
||||||
codeFieldName?: string;
|
codeFieldName?: string;
|
||||||
parentComponentId?: string;
|
parentComponentId?: string;
|
||||||
|
isCartListMode?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onToggleSelect?: () => void;
|
||||||
|
onDeleteItem?: (cartId: string) => void;
|
||||||
|
onUpdateQuantity?: (cartId: string, quantity: number, unit?: string, entries?: PackageEntry[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const header = template?.header;
|
const header = template?.header;
|
||||||
const image = template?.image;
|
const image = template?.image;
|
||||||
|
|
@ -712,14 +893,24 @@ function Card({
|
||||||
const isCarted = cart.isItemInCart(rowKey);
|
const isCarted = cart.isItemInCart(rowKey);
|
||||||
const existingCartItem = cart.getCartItem(rowKey);
|
const existingCartItem = cart.getCartItem(rowKey);
|
||||||
|
|
||||||
// DB에서 로드된 장바구니 품목이면 입력값 복원
|
// DB에서 로드된 장바구니 품목이면 입력값 복원 (기본 모드)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isCartListMode) return;
|
||||||
if (existingCartItem && existingCartItem._origin === "db") {
|
if (existingCartItem && existingCartItem._origin === "db") {
|
||||||
setInputValue(existingCartItem.quantity);
|
setInputValue(existingCartItem.quantity);
|
||||||
setPackageUnit(existingCartItem.packageUnit);
|
setPackageUnit(existingCartItem.packageUnit);
|
||||||
setPackageEntries(existingCartItem.packageEntries || []);
|
setPackageEntries(existingCartItem.packageEntries || []);
|
||||||
}
|
}
|
||||||
}, [existingCartItem?._origin, existingCartItem?.quantity, existingCartItem?.packageUnit]);
|
}, [isCartListMode, existingCartItem?._origin, existingCartItem?.quantity, existingCartItem?.packageUnit]);
|
||||||
|
|
||||||
|
// 장바구니 목록 모드: __cart_quantity에서 초기값 복원
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCartListMode) return;
|
||||||
|
const cartQty = Number(row.__cart_quantity) || 0;
|
||||||
|
setInputValue(cartQty);
|
||||||
|
const cartUnit = row.__cart_package_unit ? String(row.__cart_package_unit) : undefined;
|
||||||
|
setPackageUnit(cartUnit);
|
||||||
|
}, [isCartListMode, row.__cart_quantity, row.__cart_package_unit]);
|
||||||
|
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
image?.enabled && image?.imageColumn && row[image.imageColumn]
|
image?.enabled && image?.imageColumn && row[image.imageColumn]
|
||||||
|
|
@ -771,6 +962,9 @@ function Card({
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
setPackageUnit(unit);
|
setPackageUnit(unit);
|
||||||
setPackageEntries(entries || []);
|
setPackageEntries(entries || []);
|
||||||
|
if (isCartListMode) {
|
||||||
|
onUpdateQuantity?.(String(row.__cart_id), value, unit, entries);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 담기: 로컬 상태에만 추가 + 연결 시스템으로 카운트 전달
|
// 담기: 로컬 상태에만 추가 + 연결 시스템으로 카운트 전달
|
||||||
|
|
@ -806,6 +1000,23 @@ function Card({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 장바구니 목록 모드: 개별 삭제
|
||||||
|
const handleCartDelete = async (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const cartId = String(row.__cart_id);
|
||||||
|
if (!cartId) return;
|
||||||
|
|
||||||
|
const ok = window.confirm("이 항목을 장바구니에서 삭제하시겠습니까?");
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dataApi.deleteRecord("cart_items", cartId);
|
||||||
|
onDeleteItem?.(cartId);
|
||||||
|
} catch {
|
||||||
|
toast.error("삭제에 실패했습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// pop-icon 스타일 담기/취소 버튼 아이콘 크기 (스케일 반영)
|
// pop-icon 스타일 담기/취소 버튼 아이콘 크기 (스케일 반영)
|
||||||
const iconSize = Math.max(14, Math.round(18 * (scaled.bodyTextSize / 11)));
|
const iconSize = Math.max(14, Math.round(18 * (scaled.bodyTextSize / 11)));
|
||||||
const cartLabel = cartAction?.label || "담기";
|
const cartLabel = cartAction?.label || "담기";
|
||||||
|
|
@ -815,22 +1026,43 @@ function Card({
|
||||||
onSelect?.(row);
|
onSelect?.(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 카드 테두리: 장바구니 목록 모드에서는 선택 상태, 기본 모드에서는 담기 상태 기준
|
||||||
|
const borderClass = isCartListMode
|
||||||
|
? isSelected
|
||||||
|
? "border-primary border-2 hover:border-primary/80"
|
||||||
|
: "hover:border-2 hover:border-blue-500"
|
||||||
|
: isCarted
|
||||||
|
? "border-emerald-500 border-2 hover:border-emerald-600"
|
||||||
|
: "hover:border-2 hover:border-blue-500";
|
||||||
|
|
||||||
|
// 카드 헤더 배경: 장바구니 목록 모드에서는 선택 상태, 기본 모드에서는 담기 상태 기준
|
||||||
|
const headerBgClass = isCartListMode
|
||||||
|
? isSelected ? "bg-primary/10 dark:bg-primary/20" : "bg-muted/30"
|
||||||
|
: isCarted ? "bg-emerald-50 dark:bg-emerald-950/30" : "bg-muted/30";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${
|
className={`relative cursor-pointer rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${borderClass}`}
|
||||||
isCarted
|
|
||||||
? "border-emerald-500 border-2 hover:border-emerald-600"
|
|
||||||
: "hover:border-2 hover:border-blue-500"
|
|
||||||
}`}
|
|
||||||
style={cardStyle}
|
style={cardStyle}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleCardClick(); }}
|
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleCardClick(); }}
|
||||||
>
|
>
|
||||||
|
{/* 장바구니 목록 모드: 체크박스 */}
|
||||||
|
{isCartListMode && (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={(e) => { e.stopPropagation(); onToggleSelect?.(); }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="absolute left-2 top-2 z-10 h-4 w-4 rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 헤더 영역 */}
|
{/* 헤더 영역 */}
|
||||||
{(codeValue !== null || titleValue !== null) && (
|
{(codeValue !== null || titleValue !== null) && (
|
||||||
<div className={`border-b ${isCarted ? "bg-emerald-50 dark:bg-emerald-950/30" : "bg-muted/30"}`} style={headerStyle}>
|
<div className={`border-b ${headerBgClass}`} style={headerStyle}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{codeValue !== null && (
|
{codeValue !== null && (
|
||||||
<span
|
<span
|
||||||
|
|
@ -892,8 +1124,8 @@ function Card({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 오른쪽: 수량 버튼 + 담기/취소 버튼 (각각 독립 ON/OFF) */}
|
{/* 오른쪽: 수량 버튼 + 담기/취소/삭제 버튼 */}
|
||||||
{(inputField?.enabled || cartAction) && (
|
{(inputField?.enabled || cartAction || isCartListMode) && (
|
||||||
<div
|
<div
|
||||||
className="ml-2 flex shrink-0 flex-col items-stretch justify-center gap-2"
|
className="ml-2 flex shrink-0 flex-col items-stretch justify-center gap-2"
|
||||||
style={{ minWidth: "100px" }}
|
style={{ minWidth: "100px" }}
|
||||||
|
|
@ -914,8 +1146,22 @@ function Card({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 담기/취소 버튼 (cartAction 존재 시 항상 표시) */}
|
{/* 장바구니 목록 모드: 삭제 버튼 */}
|
||||||
{cartAction && (
|
{isCartListMode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCartDelete}
|
||||||
|
className="flex flex-col items-center justify-center gap-0.5 rounded-xl bg-destructive px-2 py-1.5 text-destructive-foreground transition-colors duration-150 hover:bg-destructive/90 active:bg-destructive/80"
|
||||||
|
>
|
||||||
|
<Trash2 size={iconSize} />
|
||||||
|
<span style={{ fontSize: `${Math.max(9, scaled.bodyTextSize - 2)}px` }} className="font-semibold leading-tight">
|
||||||
|
삭제
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 기본 모드: 담기/취소 버튼 (cartAction 존재 시) */}
|
||||||
|
{!isCartListMode && cartAction && (
|
||||||
<>
|
<>
|
||||||
{isCarted ? (
|
{isCarted ? (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ import type {
|
||||||
CardCartActionConfig,
|
CardCartActionConfig,
|
||||||
CardResponsiveConfig,
|
CardResponsiveConfig,
|
||||||
ResponsiveDisplayMode,
|
ResponsiveDisplayMode,
|
||||||
|
CartListModeConfig,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { screenApi } from "@/lib/api/screen";
|
||||||
import {
|
import {
|
||||||
CARD_SCROLL_DIRECTION_LABELS,
|
CARD_SCROLL_DIRECTION_LABELS,
|
||||||
RESPONSIVE_DISPLAY_LABELS,
|
RESPONSIVE_DISPLAY_LABELS,
|
||||||
|
|
@ -139,6 +141,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
|
||||||
onUpdate({ ...cfg, ...partial });
|
onUpdate({ ...cfg, ...partial });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCartListMode = !!cfg.cartListMode?.enabled;
|
||||||
const hasTable = !!cfg.dataSource?.tableName;
|
const hasTable = !!cfg.dataSource?.tableName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -161,12 +164,12 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
|
||||||
className={`flex-1 px-2 py-2 text-xs font-medium transition-colors ${
|
className={`flex-1 px-2 py-2 text-xs font-medium transition-colors ${
|
||||||
activeTab === "template"
|
activeTab === "template"
|
||||||
? "border-b-2 border-primary text-primary"
|
? "border-b-2 border-primary text-primary"
|
||||||
: hasTable
|
: hasTable && !isCartListMode
|
||||||
? "text-muted-foreground hover:text-foreground"
|
? "text-muted-foreground hover:text-foreground"
|
||||||
: "text-muted-foreground/50 cursor-not-allowed"
|
: "text-muted-foreground/50 cursor-not-allowed"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => hasTable && setActiveTab("template")}
|
onClick={() => hasTable && !isCartListMode && setActiveTab("template")}
|
||||||
disabled={!hasTable}
|
disabled={!hasTable || isCartListMode}
|
||||||
>
|
>
|
||||||
카드 템플릿
|
카드 템플릿
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -183,7 +186,16 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === "template" && (
|
{activeTab === "template" && (
|
||||||
<CardTemplateTab config={cfg} onUpdate={updateConfig} />
|
isCartListMode ? (
|
||||||
|
<div className="flex h-40 items-center justify-center text-center">
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
<p className="text-sm">원본 화면의 카드 설정을 자동으로 사용합니다</p>
|
||||||
|
<p className="mt-1 text-xs">카드 디자인을 변경하려면 원본 화면에서 수정하세요</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CardTemplateTab config={cfg} onUpdate={updateConfig} />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -299,46 +311,58 @@ function BasicSettingsTab({
|
||||||
}
|
}
|
||||||
}, [currentMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [currentMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const isCartListMode = !!config.cartListMode?.enabled;
|
||||||
|
|
||||||
const updateDataSource = (partial: Partial<CardListDataSource>) => {
|
const updateDataSource = (partial: Partial<CardListDataSource>) => {
|
||||||
onUpdate({ dataSource: { ...dataSource, ...partial } });
|
onUpdate({ dataSource: { ...dataSource, ...partial } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 테이블 선택 */}
|
{/* 장바구니 목록 모드 */}
|
||||||
<CollapsibleSection title="테이블 선택" defaultOpen>
|
<CollapsibleSection title="장바구니 목록 모드" defaultOpen={isCartListMode}>
|
||||||
<div className="space-y-3">
|
<CartListModeSection
|
||||||
<div>
|
cartListMode={config.cartListMode}
|
||||||
<Label className="text-[10px] text-muted-foreground">데이터 테이블</Label>
|
onUpdate={(cartListMode) => onUpdate({ cartListMode })}
|
||||||
<TableCombobox
|
/>
|
||||||
tables={tables}
|
|
||||||
value={dataSource.tableName || ""}
|
|
||||||
onSelect={(val) => {
|
|
||||||
onUpdate({
|
|
||||||
dataSource: {
|
|
||||||
tableName: val,
|
|
||||||
joins: undefined,
|
|
||||||
filters: undefined,
|
|
||||||
sort: undefined,
|
|
||||||
limit: undefined,
|
|
||||||
},
|
|
||||||
cardTemplate: DEFAULT_TEMPLATE,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{dataSource.tableName && (
|
|
||||||
<div className="flex items-center gap-2 rounded-md border bg-muted/30 p-2">
|
|
||||||
<Database className="h-4 w-4 text-primary" />
|
|
||||||
<span className="text-xs font-medium">{dataSource.tableName}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* 조인 설정 (테이블 선택 시만 표시) */}
|
{/* 테이블 선택 (장바구니 모드 시 숨김) */}
|
||||||
{dataSource.tableName && (
|
{!isCartListMode && (
|
||||||
|
<CollapsibleSection title="테이블 선택" defaultOpen>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground">데이터 테이블</Label>
|
||||||
|
<TableCombobox
|
||||||
|
tables={tables}
|
||||||
|
value={dataSource.tableName || ""}
|
||||||
|
onSelect={(val) => {
|
||||||
|
onUpdate({
|
||||||
|
dataSource: {
|
||||||
|
tableName: val,
|
||||||
|
joins: undefined,
|
||||||
|
filters: undefined,
|
||||||
|
sort: undefined,
|
||||||
|
limit: undefined,
|
||||||
|
},
|
||||||
|
cardTemplate: DEFAULT_TEMPLATE,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dataSource.tableName && (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border bg-muted/30 p-2">
|
||||||
|
<Database className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-xs font-medium">{dataSource.tableName}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CollapsibleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 조인 설정 (장바구니 모드 시 숨김) */}
|
||||||
|
{!isCartListMode && dataSource.tableName && (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="조인 설정"
|
title="조인 설정"
|
||||||
badge={
|
badge={
|
||||||
|
|
@ -355,8 +379,8 @@ function BasicSettingsTab({
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 정렬 기준 (테이블 선택 시만 표시) */}
|
{/* 정렬 기준 (장바구니 모드 시 숨김) */}
|
||||||
{dataSource.tableName && (
|
{!isCartListMode && dataSource.tableName && (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="정렬 기준"
|
title="정렬 기준"
|
||||||
badge={
|
badge={
|
||||||
|
|
@ -838,6 +862,113 @@ function CollapsibleSection({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 장바구니 목록 모드 설정 =====
|
||||||
|
|
||||||
|
function CartListModeSection({
|
||||||
|
cartListMode,
|
||||||
|
onUpdate,
|
||||||
|
}: {
|
||||||
|
cartListMode?: CartListModeConfig;
|
||||||
|
onUpdate: (config: CartListModeConfig) => void;
|
||||||
|
}) {
|
||||||
|
const mode: CartListModeConfig = cartListMode || { enabled: false };
|
||||||
|
const [screens, setScreens] = useState<{ id: number; name: string }[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
screenApi
|
||||||
|
.getScreens({ size: 500 })
|
||||||
|
.then((res) => {
|
||||||
|
if (res?.data) {
|
||||||
|
setScreens(
|
||||||
|
res.data.map((s) => ({
|
||||||
|
id: s.screenId,
|
||||||
|
name: s.screenName || `화면 ${s.screenId}`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-[10px]">장바구니 목록 모드</Label>
|
||||||
|
<Switch
|
||||||
|
checked={mode.enabled}
|
||||||
|
onCheckedChange={(enabled) => onUpdate({ ...mode, enabled })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[9px] text-muted-foreground">
|
||||||
|
활성화하면 cart_items 테이블에서 데이터를 조회하고,
|
||||||
|
원본 화면의 카드 설정을 자동으로 상속합니다.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{mode.enabled && (
|
||||||
|
<>
|
||||||
|
{/* 원본 화면 선택 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground">원본 화면</Label>
|
||||||
|
<Select
|
||||||
|
value={mode.sourceScreenId ? String(mode.sourceScreenId) : "__none__"}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onUpdate({ ...mode, sourceScreenId: val === "__none__" ? undefined : Number(val) })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1 h-7 text-xs">
|
||||||
|
<SelectValue placeholder="화면 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">선택 안함</SelectItem>
|
||||||
|
{screens.map((s) => (
|
||||||
|
<SelectItem key={s.id} value={String(s.id)}>
|
||||||
|
{s.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="mt-1 text-[9px] text-muted-foreground">
|
||||||
|
품목을 담았던 화면을 선택하면 카드 디자인이 자동으로 적용됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 장바구니 구분값 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground">장바구니 구분값</Label>
|
||||||
|
<Input
|
||||||
|
value={mode.cartType || ""}
|
||||||
|
onChange={(e) => onUpdate({ ...mode, cartType: e.target.value })}
|
||||||
|
placeholder="예: purchase_inbound"
|
||||||
|
className="mt-1 h-7 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-[9px] text-muted-foreground">
|
||||||
|
원본 화면의 담기 버튼에 설정한 구분값과 동일하게 입력하세요.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 상태 필터 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-[10px] text-muted-foreground">상태 필터</Label>
|
||||||
|
<Select
|
||||||
|
value={mode.statusFilter || "in_cart"}
|
||||||
|
onValueChange={(val) => onUpdate({ ...mode, statusFilter: val })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1 h-7 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="in_cart">담긴 상태 (in_cart)</SelectItem>
|
||||||
|
<SelectItem value="confirmed">확정 완료 (confirmed)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 헤더 설정 섹션 =====
|
// ===== 헤더 설정 섹션 =====
|
||||||
|
|
||||||
function HeaderSettingsSection({
|
function HeaderSettingsSection({
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,12 @@ PopComponentRegistry.registerComponent({
|
||||||
{ key: "selected_row", label: "선택된 행", type: "selected_row", description: "사용자가 선택한 카드의 행 데이터를 전달" },
|
{ key: "selected_row", label: "선택된 행", type: "selected_row", description: "사용자가 선택한 카드의 행 데이터를 전달" },
|
||||||
{ key: "cart_updated", label: "장바구니 상태", type: "event", description: "장바구니 변경 시 count/isDirty 전달" },
|
{ key: "cart_updated", label: "장바구니 상태", type: "event", description: "장바구니 변경 시 count/isDirty 전달" },
|
||||||
{ key: "cart_save_completed", label: "저장 완료", type: "event", description: "장바구니 DB 저장 완료 후 결과 전달" },
|
{ key: "cart_save_completed", label: "저장 완료", type: "event", description: "장바구니 DB 저장 완료 후 결과 전달" },
|
||||||
|
{ key: "selected_items", label: "선택된 항목", type: "value", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" },
|
||||||
],
|
],
|
||||||
receivable: [
|
receivable: [
|
||||||
{ key: "filter_condition", label: "필터 조건", type: "filter_value", description: "외부 컴포넌트에서 받은 필터 조건으로 카드 목록 필터링" },
|
{ key: "filter_condition", label: "필터 조건", type: "filter_value", description: "외부 컴포넌트에서 받은 필터 조건으로 카드 목록 필터링" },
|
||||||
{ key: "cart_save_trigger", label: "저장 요청", type: "event", description: "장바구니 DB 일괄 저장 트리거 (버튼에서 수신)" },
|
{ key: "cart_save_trigger", label: "저장 요청", type: "event", description: "장바구니 DB 일괄 저장 트리거 (버튼에서 수신)" },
|
||||||
|
{ key: "confirm_trigger", label: "확정 트리거", type: "event", description: "입고 확정 시 수정된 수량 일괄 반영 + 선택 항목 전달" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
touchOptimized: true,
|
touchOptimized: true,
|
||||||
|
|
|
||||||
|
|
@ -608,6 +608,15 @@ export interface CardResponsiveConfig {
|
||||||
fields?: Record<string, ResponsiveDisplayMode>;
|
fields?: Record<string, ResponsiveDisplayMode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- 장바구니 목록 모드 설정 -----
|
||||||
|
|
||||||
|
export interface CartListModeConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
sourceScreenId?: number;
|
||||||
|
cartType?: string;
|
||||||
|
statusFilter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ----- pop-card-list 전체 설정 -----
|
// ----- pop-card-list 전체 설정 -----
|
||||||
|
|
||||||
export interface PopCardListConfig {
|
export interface PopCardListConfig {
|
||||||
|
|
@ -620,10 +629,11 @@ export interface PopCardListConfig {
|
||||||
gridColumns?: number;
|
gridColumns?: number;
|
||||||
gridRows?: number;
|
gridRows?: number;
|
||||||
|
|
||||||
// 반응형 표시 설정
|
|
||||||
responsiveDisplay?: CardResponsiveConfig;
|
responsiveDisplay?: CardResponsiveConfig;
|
||||||
|
|
||||||
inputField?: CardInputFieldConfig;
|
inputField?: CardInputFieldConfig;
|
||||||
packageConfig?: CardPackageConfig;
|
packageConfig?: CardPackageConfig;
|
||||||
cartAction?: CardCartActionConfig;
|
cartAction?: CardCartActionConfig;
|
||||||
|
|
||||||
|
cartListMode?: CartListModeConfig;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue