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({
) : (