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 {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue