diff --git a/frontend/lib/registry/pop-components/pop-button.tsx b/frontend/lib/registry/pop-components/pop-button.tsx index 923ece9e..a3be3e59 100644 --- a/frontend/lib/registry/pop-components/pop-button.tsx +++ b/frontend/lib/registry/pop-components/pop-button.tsx @@ -651,8 +651,12 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp const { subscribe, publish } = usePopEvent(screenId || "default"); - // 장바구니 모드 상태 - const isCartMode = config?.preset === "cart"; + // 장바구니 모드 상태 (v1 preset 또는 v2 tasks에 cart-save가 있으면 활성) + const v2Tasks = (config && "tasks" in config && Array.isArray((config as any).tasks)) + ? (config as any).tasks as PopButtonTask[] + : null; + const hasCartSaveTask = !!v2Tasks?.some((t) => t.type === "cart-save"); + const isCartMode = config?.preset === "cart" || hasCartSaveTask; const isInboundConfirmMode = config?.preset === "inbound-confirm"; const [cartCount, setCartCount] = useState(0); const [cartIsDirty, setCartIsDirty] = useState(false); @@ -746,8 +750,10 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp }, [isCartMode, componentId, subscribe]); // 저장 완료 수신 (카드 목록에서 saveToDb 완료 후 전달) - const cartScreenIdRef = React.useRef(config?.cart?.cartScreenId); - cartScreenIdRef.current = config?.cart?.cartScreenId; + const resolvedCartScreenId = config?.cart?.cartScreenId + || v2Tasks?.find((t) => t.type === "cart-save")?.cartScreenId; + const cartScreenIdRef = React.useRef(resolvedCartScreenId); + cartScreenIdRef.current = resolvedCartScreenId; useEffect(() => { if (!isCartMode || !componentId) return; @@ -990,7 +996,28 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp return; } - // v2 경로: tasks 배열이 있으면 새 실행 엔진 사용 + // 장바구니 모드 (v1 preset: "cart" 또는 v2 tasks에 cart-save 포함) + if (isCartMode) { + if (cartCount === 0 && !cartIsDirty) { + toast.info("장바구니가 비어 있습니다."); + return; + } + + if (cartIsDirty) { + setShowCartConfirm(true); + } else { + const targetScreenId = resolvedCartScreenId; + if (targetScreenId) { + const cleanId = String(targetScreenId).replace(/^.*\/(\d+)$/, "$1").trim(); + window.location.href = `/pop/screens/${cleanId}`; + } else { + toast.info("장바구니 화면이 설정되지 않았습니다."); + } + } + return; + } + + // v2 경로: tasks 배열이 있으면 새 실행 엔진 사용 (cart-save 제외) if (v2Config) { if (v2Config.confirm?.enabled) { setShowInboundConfirm(true); @@ -1012,27 +1039,6 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp return; } - // 장바구니 모드: isDirty 여부에 따라 분기 - if (isCartMode) { - if (cartCount === 0 && !cartIsDirty) { - toast.info("장바구니가 비어 있습니다."); - return; - } - - if (cartIsDirty) { - setShowCartConfirm(true); - } else { - const targetScreenId = config?.cart?.cartScreenId; - if (targetScreenId) { - const cleanId = targetScreenId.replace(/^.*\/(\d+)$/, "$1").trim(); - window.location.href = `/pop/screens/${cleanId}`; - } else { - toast.info("장바구니 화면이 설정되지 않았습니다."); - } - } - return; - } - const action = config?.action; if (!action) return; diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index ead6da6f..88375327 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState, useRef, useMemo, useCallback } from "react"; import { - Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight, Check, X, + Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight, Check, X, Search, } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -1161,8 +1161,11 @@ export function PopCardListV2Component({

데이터 소스를 설정해주세요.

) : config?.hideUntilFiltered && effectiveExternalFilters.size === 0 ? ( -
-

필터를 선택하면 데이터가 표시됩니다.

+
+ +

+ {config.hideUntilFilteredMessage || "필터를 먼저 선택해주세요."} +

) : loading ? (
diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx index 0b67dd82..f7e4c72d 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Config.tsx @@ -3250,9 +3250,20 @@ function TabActions({ />
{cfg.hideUntilFiltered && ( -

- 연결된 컴포넌트에서 필터 값이 전달되기 전까지 데이터를 표시하지 않습니다. -

+
+

+ 연결된 컴포넌트에서 필터 값이 전달되기 전까지 데이터를 표시하지 않습니다. +

+
+ + onUpdate({ hideUntilFilteredMessage: e.target.value })} + placeholder="필터를 먼저 선택해주세요." + className="h-7 text-[10px]" + /> +
+
)} {/* 기본 표시 수 */} diff --git a/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx b/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx index 60260693..aa94e851 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx @@ -14,7 +14,7 @@ import { useRouter } from "next/navigation"; import { Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight, ShoppingCart, X, Package, Truck, Box, Archive, Heart, Star, - Trash2, + Trash2, Search, type LucideIcon, } from "lucide-react"; import { toast } from "sonner"; @@ -770,6 +770,13 @@ export function PopCardListComponent({ 데이터 소스를 설정해주세요.

+ ) : !isCartListMode && config?.requireFilter && externalFilters.size === 0 ? ( +
+ +

+ {config.requireFilterMessage || "필터를 먼저 선택해주세요."} +

+
) : loading ? (
diff --git a/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx b/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx index f5d06036..793f6069 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/PopCardListConfig.tsx @@ -9,8 +9,11 @@ */ import React, { useState, useEffect, useMemo } from "react"; -import { ChevronDown, ChevronRight, Plus, Trash2, Database, Check } from "lucide-react"; +import { ChevronDown, ChevronRight, Plus, Trash2, Database, Check, ChevronsUpDown } from "lucide-react"; import { useCollapsibleSections } from "@/hooks/pop/useCollapsibleSections"; +import { useAuth } from "@/hooks/useAuth"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import type { GridMode } from "@/components/pop/designer/types/pop-layout"; import { GRID_BREAKPOINTS } from "@/components/pop/designer/types/pop-layout"; import { Button } from "@/components/ui/button"; @@ -431,6 +434,32 @@ function BasicSettingsTab({ )} + {/* 필터 필수 설정 (장바구니 모드 아닐 때만) */} + {!isCartListMode && dataSource.tableName && ( + +
+
+ + onUpdate({ requireFilter: checked })} + /> +
+ {config.requireFilter && ( +
+ + onUpdate({ requireFilterMessage: e.target.value })} + placeholder="필터를 먼저 선택해주세요." + className="h-7 text-[10px]" + /> +
+ )} +
+
+ )} + {/* 저장 매핑 (장바구니 모드일 때만) */} {isCartListMode && ( void; }) { const mode: CartListModeConfig = cartListMode || { enabled: false }; - const [screens, setScreens] = useState<{ id: number; name: string }[]>([]); + const [screens, setScreens] = useState<{ id: number; name: string; code: string }[]>([]); const [sourceCardLists, setSourceCardLists] = useState([]); const [loadingComponents, setLoadingComponents] = useState(false); + const [screenOpen, setScreenOpen] = useState(false); + const { companyCode } = useAuth(); - // 화면 목록 로드 useEffect(() => { screenApi - .getScreens({ size: 500 }) + .getScreens({ size: 500, companyCode: companyCode || undefined }) .then((res) => { if (res?.data) { setScreens( res.data.map((s) => ({ id: s.screenId, name: s.screenName || `화면 ${s.screenId}`, + code: s.screenCode || "", })) ); } }) .catch(() => {}); - }, []); + }, [companyCode]); - // 원본 화면 선택 시 -> 해당 화면의 pop-card-list 컴포넌트 목록 로드 useEffect(() => { if (!mode.sourceScreenId) { setSourceCardLists([]); @@ -889,22 +919,7 @@ function CartListModeSection({ .finally(() => setLoadingComponents(false)); }, [mode.sourceScreenId]); - const handleScreenChange = (val: string) => { - const screenId = val === "__none__" ? undefined : Number(val); - onUpdate({ ...mode, sourceScreenId: screenId }); - }; - - const handleComponentSelect = (val: string) => { - if (val === "__none__") { - onUpdate({ ...mode, sourceComponentId: undefined }); - return; - } - const compId = val.startsWith("__comp_") ? val.replace("__comp_", "") : val; - const found = sourceCardLists.find((c) => c.componentId === compId); - if (found) { - onUpdate({ ...mode, sourceComponentId: found.componentId }); - } - }; + const selectedScreen = screens.find((s) => s.id === mode.sourceScreenId); return (
@@ -923,28 +938,69 @@ function CartListModeSection({ {mode.enabled && ( <> - {/* 원본 화면 선택 */} + {/* 원본 화면 선택 (검색 가능 Combobox) */}
- + + + + + + + + + + 검색 결과가 없습니다. + + + {screens.map((s) => ( + { + onUpdate({ + ...mode, + sourceScreenId: mode.sourceScreenId === s.id ? undefined : s.id, + sourceComponentId: mode.sourceScreenId === s.id ? undefined : mode.sourceComponentId, + }); + setScreenOpen(false); + }} + className="text-xs" + > + +
+ {s.name} + ID: {s.id} +
+
+ ))} +
+
+
+
+
- {/* 원본 컴포넌트 선택 (원본 화면에서 자동 로드) */} + {/* 원본 컴포넌트 선택 */} {mode.sourceScreenId && (
@@ -959,7 +1015,14 @@ function CartListModeSection({ ) : (