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:
SeongHyun Kim 2026-02-27 14:57:24 +09:00
parent 7bf20bda14
commit aa319a6bda
4 changed files with 455 additions and 66 deletions

View File

@ -14,6 +14,7 @@ import { useRouter } from "next/navigation";
import {
Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
ShoppingCart, X, Package, Truck, Box, Archive, Heart, Star,
Trash2,
type LucideIcon,
} from "lucide-react";
import { toast } from "sonner";
@ -28,12 +29,14 @@ import type {
CardPresetSpec,
CartItem,
PackageEntry,
CartListModeConfig,
} from "../types";
import {
DEFAULT_CARD_IMAGE,
CARD_PRESET_SPECS,
} from "../types";
import { dataApi } from "@/lib/api/data";
import { screenApi } from "@/lib/api/screen";
import { usePopEvent } from "@/hooks/pop/usePopEvent";
import { useCartSync } from "@/hooks/pop/useCartSync";
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 {
config?: PopCardListConfig;
className?: string;
@ -172,6 +197,14 @@ export function PopCardListComponent({
const cartType = config?.cartAction?.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 [loading, setLoading] = useState(true);
@ -219,9 +252,9 @@ export function PopCardListComponent({
const cartRef = useRef(cart);
cartRef.current = cart;
// "저장 요청" 이벤트 수신: 버튼 컴포넌트가 장바구니 저장을 요청하면 saveToDb 실행
// "저장 요청" 이벤트 수신: 버튼 컴포넌트가 장바구니 저장을 요청하면 saveToDb 실행 (장바구니 목록 모드 제외)
useEffect(() => {
if (!componentId) return;
if (!componentId || isCartListMode) return;
const unsub = subscribe(
`__comp_input__${componentId}__cart_save_trigger`,
async (payload: unknown) => {
@ -233,16 +266,16 @@ export function PopCardListComponent({
}
);
return unsub;
}, [componentId, subscribe, publish]);
}, [componentId, subscribe, publish, isCartListMode]);
// DB 로드 완료 후 초기 장바구니 상태를 버튼에 전달
// DB 로드 완료 후 초기 장바구니 상태를 버튼에 전달 (장바구니 목록 모드 제외)
useEffect(() => {
if (!componentId || cart.loading) return;
if (!componentId || cart.loading || isCartListMode) return;
publish(`__comp_output__${componentId}__cart_updated`, {
count: cart.cartCount,
isDirty: cart.isDirty,
});
}, [componentId, cart.loading, cart.cartCount, cart.isDirty, publish]);
}, [componentId, cart.loading, cart.cartCount, cart.isDirty, publish, isCartListMode]);
// 카드 선택 시 selected_row 이벤트 발행
const handleCardSelect = useCallback((row: RowData) => {
@ -454,7 +487,70 @@ export function PopCardListComponent({
[dataSource]
);
// 장바구니 목록 모드 설정을 직렬화 (의존성 안정화)
const cartListModeKey = useMemo(
() => JSON.stringify(config?.cartListMode || null),
[config?.cartListMode]
);
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) {
setLoading(false);
setRows([]);
@ -510,16 +606,51 @@ export function PopCardListComponent({
};
fetchData();
}, [dataSourceKey]); // eslint-disable-line react-hooks/exhaustive-deps
}, [dataSourceKey, isCartListMode, cartListModeKey]); // eslint-disable-line react-hooks/exhaustive-deps
// 이미지 URL 없는 항목 카운트 (내부 추적용, toast 미표시)
useEffect(() => {
if (!loading && rows.length > 0 && template?.image?.enabled && template?.image?.imageColumn) {
const imageColumn = template.image.imageColumn;
if (!loading && rows.length > 0 && effectiveTemplate?.image?.enabled && effectiveTemplate?.image?.imageColumn) {
const imageColumn = effectiveTemplate.image.imageColumn;
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 = {
@ -549,7 +680,13 @@ export function PopCardListComponent({
ref={containerRef}
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">
<p className="text-sm text-muted-foreground">
.
@ -569,6 +706,27 @@ export function PopCardListComponent({
</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
ref={scrollAreaRef}
@ -580,15 +738,15 @@ export function PopCardListComponent({
}}
>
{displayCards.map((row, index) => {
const codeValue = template?.header?.codeField && row[template.header.codeField]
? String(row[template.header.codeField])
const codeValue = effectiveTemplate?.header?.codeField && row[effectiveTemplate.header.codeField]
? String(row[effectiveTemplate.header.codeField])
: null;
const rowKey = codeValue ? `${codeValue}-${index}` : `card-${index}`;
return (
<Card
key={rowKey}
row={row}
template={template}
template={effectiveTemplate}
scaled={scaled}
inputField={config?.inputField}
packageConfig={config?.packageConfig}
@ -597,8 +755,21 @@ export function PopCardListComponent({
router={router}
onSelect={handleCardSelect}
cart={cart}
codeFieldName={template?.header?.codeField}
codeFieldName={effectiveTemplate?.header?.codeField}
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,
codeFieldName,
parentComponentId,
isCartListMode,
isSelected,
onToggleSelect,
onDeleteItem,
onUpdateQuantity,
}: {
row: RowData;
template?: CardTemplateConfig;
@ -694,6 +870,11 @@ function Card({
cart: ReturnType<typeof useCartSync>;
codeFieldName?: 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 image = template?.image;
@ -712,14 +893,24 @@ function Card({
const isCarted = cart.isItemInCart(rowKey);
const existingCartItem = cart.getCartItem(rowKey);
// DB에서 로드된 장바구니 품목이면 입력값 복원
// DB에서 로드된 장바구니 품목이면 입력값 복원 (기본 모드)
useEffect(() => {
if (isCartListMode) return;
if (existingCartItem && existingCartItem._origin === "db") {
setInputValue(existingCartItem.quantity);
setPackageUnit(existingCartItem.packageUnit);
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 =
image?.enabled && image?.imageColumn && row[image.imageColumn]
@ -771,6 +962,9 @@ function Card({
setInputValue(value);
setPackageUnit(unit);
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 스타일 담기/취소 버튼 아이콘 크기 (스케일 반영)
const iconSize = Math.max(14, Math.round(18 * (scaled.bodyTextSize / 11)));
const cartLabel = cartAction?.label || "담기";
@ -815,22 +1026,43 @@ function Card({
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 (
<div
className={`cursor-pointer rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${
isCarted
? "border-emerald-500 border-2 hover:border-emerald-600"
: "hover:border-2 hover:border-blue-500"
}`}
className={`relative cursor-pointer rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${borderClass}`}
style={cardStyle}
onClick={handleCardClick}
role="button"
tabIndex={0}
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) && (
<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">
{codeValue !== null && (
<span
@ -892,8 +1124,8 @@ function Card({
</div>
</div>
{/* 오른쪽: 수량 버튼 + 담기/취소 버튼 (각각 독립 ON/OFF) */}
{(inputField?.enabled || cartAction) && (
{/* 오른쪽: 수량 버튼 + 담기/취소/삭제 버튼 */}
{(inputField?.enabled || cartAction || isCartListMode) && (
<div
className="ml-2 flex shrink-0 flex-col items-stretch justify-center gap-2"
style={{ minWidth: "100px" }}
@ -914,8 +1146,22 @@ function Card({
</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 ? (
<button

View File

@ -49,7 +49,9 @@ import type {
CardCartActionConfig,
CardResponsiveConfig,
ResponsiveDisplayMode,
CartListModeConfig,
} from "../types";
import { screenApi } from "@/lib/api/screen";
import {
CARD_SCROLL_DIRECTION_LABELS,
RESPONSIVE_DISPLAY_LABELS,
@ -139,6 +141,7 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
onUpdate({ ...cfg, ...partial });
};
const isCartListMode = !!cfg.cartListMode?.enabled;
const hasTable = !!cfg.dataSource?.tableName;
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 ${
activeTab === "template"
? "border-b-2 border-primary text-primary"
: hasTable
: hasTable && !isCartListMode
? "text-muted-foreground hover:text-foreground"
: "text-muted-foreground/50 cursor-not-allowed"
}`}
onClick={() => hasTable && setActiveTab("template")}
disabled={!hasTable}
onClick={() => hasTable && !isCartListMode && setActiveTab("template")}
disabled={!hasTable || isCartListMode}
>
릿
</button>
@ -183,7 +186,16 @@ export function PopCardListConfigPanel({ config, onUpdate, currentMode, currentC
/>
)}
{activeTab === "template" && (
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>
@ -299,13 +311,24 @@ function BasicSettingsTab({
}
}, [currentMode]); // eslint-disable-line react-hooks/exhaustive-deps
const isCartListMode = !!config.cartListMode?.enabled;
const updateDataSource = (partial: Partial<CardListDataSource>) => {
onUpdate({ dataSource: { ...dataSource, ...partial } });
};
return (
<div className="space-y-4">
{/* 테이블 선택 */}
{/* 장바구니 목록 모드 */}
<CollapsibleSection title="장바구니 목록 모드" defaultOpen={isCartListMode}>
<CartListModeSection
cartListMode={config.cartListMode}
onUpdate={(cartListMode) => onUpdate({ cartListMode })}
/>
</CollapsibleSection>
{/* 테이블 선택 (장바구니 모드 시 숨김) */}
{!isCartListMode && (
<CollapsibleSection title="테이블 선택" defaultOpen>
<div className="space-y-3">
<div>
@ -336,9 +359,10 @@ function BasicSettingsTab({
)}
</div>
</CollapsibleSection>
)}
{/* 조인 설정 (테이블 선택 시만 표시) */}
{dataSource.tableName && (
{/* 조인 설정 (장바구니 모드 시 숨김) */}
{!isCartListMode && dataSource.tableName && (
<CollapsibleSection
title="조인 설정"
badge={
@ -355,8 +379,8 @@ function BasicSettingsTab({
</CollapsibleSection>
)}
{/* 정렬 기준 (테이블 선택 시만 표시) */}
{dataSource.tableName && (
{/* 정렬 기준 (장바구니 모드 시 숨김) */}
{!isCartListMode && dataSource.tableName && (
<CollapsibleSection
title="정렬 기준"
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({

View File

@ -63,10 +63,12 @@ PopComponentRegistry.registerComponent({
{ key: "selected_row", label: "선택된 행", type: "selected_row", description: "사용자가 선택한 카드의 행 데이터를 전달" },
{ key: "cart_updated", label: "장바구니 상태", type: "event", description: "장바구니 변경 시 count/isDirty 전달" },
{ key: "cart_save_completed", label: "저장 완료", type: "event", description: "장바구니 DB 저장 완료 후 결과 전달" },
{ key: "selected_items", label: "선택된 항목", type: "value", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" },
],
receivable: [
{ key: "filter_condition", label: "필터 조건", type: "filter_value", description: "외부 컴포넌트에서 받은 필터 조건으로 카드 목록 필터링" },
{ key: "cart_save_trigger", label: "저장 요청", type: "event", description: "장바구니 DB 일괄 저장 트리거 (버튼에서 수신)" },
{ key: "confirm_trigger", label: "확정 트리거", type: "event", description: "입고 확정 시 수정된 수량 일괄 반영 + 선택 항목 전달" },
],
},
touchOptimized: true,

View File

@ -608,6 +608,15 @@ export interface CardResponsiveConfig {
fields?: Record<string, ResponsiveDisplayMode>;
}
// ----- 장바구니 목록 모드 설정 -----
export interface CartListModeConfig {
enabled: boolean;
sourceScreenId?: number;
cartType?: string;
statusFilter?: string;
}
// ----- pop-card-list 전체 설정 -----
export interface PopCardListConfig {
@ -620,10 +629,11 @@ export interface PopCardListConfig {
gridColumns?: number;
gridRows?: number;
// 반응형 표시 설정
responsiveDisplay?: CardResponsiveConfig;
inputField?: CardInputFieldConfig;
packageConfig?: CardPackageConfig;
cartAction?: CardCartActionConfig;
cartListMode?: CartListModeConfig;
}