From da9bce230157b65e85694f1ed9ffcd48d0235e2c Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 23 Mar 2026 10:26:06 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20POP=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=9E=85=EA=B3=A0=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1/UX=20=EA=B0=9C=EC=84=A0=20=EC=9E=A5=EB=B0=94?= =?UTF-8?q?=EA=B5=AC=EB=8B=88(cart)=20=EA=B8=B0=EB=B0=98=20=EA=B5=AC?= =?UTF-8?q?=EB=A7=A4=EC=9E=85=EA=B3=A0=20=ED=9D=90=EB=A6=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B2=84=ED=8A=BC=20=EB=8F=99=EC=9E=91,=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=A0=91=EA=B7=BC=EC=84=B1,=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=95=88=EB=82=B4=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=8B=A4=EC=A0=9C=20=EC=9A=B4=EC=98=81=20=EC=8B=9C?= =?UTF-8?q?=EB=82=98=EB=A6=AC=EC=98=A4=EC=97=90=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=EC=84=B1=EC=9D=84=20=ED=99=95=EB=B3=B4?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20[pop-button=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EB=AA=A8=EB=93=9C=20=ED=8C=90=EB=8B=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95]=20-=20isCartMode:=20v1=20preset=EB=BF=90=20=EC=95=84?= =?UTF-8?q?=EB=8B=88=EB=9D=BC=20v2=20tasks=EC=97=90=20cart-save=EA=B0=80?= =?UTF-8?q?=20=ED=8F=AC=ED=95=A8=EB=90=9C=20=20=20=EA=B2=BD=EC=9A=B0?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=EB=A1=9C=20=EC=9D=B8=EC=8B=9D=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0=20-=20resolvedCartScree?= =?UTF-8?q?nId:=20v2=20tasks=EC=9D=98=20cartScreenId=EB=8F=84=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20-=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EB=B6=84=EA=B8=B0=EB=A5=BC=20v2=20tasks?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=EB=B3=B4=EB=8B=A4=20=EB=A8=BC=EC=A0=80=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=ED=95=98=EC=97=AC=20=20=20cart-save=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=B4=20=EC=A0=95=EC=83=81=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20[pop-search=20=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=20=EC=A0=91=EA=B7=BC=EC=84=B1]=20-=20Icon?= =?UTF-8?q?View=EC=9D=98=20div=20=EC=B9=B4=EB=93=9C=EC=97=90=20role=3D"but?= =?UTF-8?q?ton",=20tabIndex=3D{0},=20=20=20onKeyDown(Enter/Space)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=ED=99=94=20=EB=B0=8F=20=ED=82=A4=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B9=B4=EB=93=9C=EB=A5=BC=20=20=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=EB=9E=99=ED=8B=B0=EB=B8=8C=20=EC=9A=94?= =?UTF-8?q?=EC=86=8C=EB=A1=9C=20=EC=9D=B8=EC=8B=9D=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?[=ED=95=84=ED=84=B0=20=ED=95=84=EC=88=98=20=EC=95=88=EB=82=B4?= =?UTF-8?q?=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EA=B8=B0=EB=8A=A5]=20-=20pop-?= =?UTF-8?q?card-list(=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D):=20requireFilter,=20requireFilterMessage=20=20=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80,=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=20=EB=AF=B8=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EC=95=88=EB=82=B4=20=EB=AC=B8=EA=B5=AC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20-=20pop-card-list-v2(MES=20=EA=B3=B5=EC=A0=95?= =?UTF-8?q?=ED=9D=90=EB=A6=84):=20hideUntilFilteredMessage=20=20=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80,=20=EA=B8=B0=EC=A1=B4?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EC=A0=84=20=EC=88=A8=EA=B9=80=EC=97=90?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EB=AC=B8=EA=B5=AC=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20-=20=EC=96=91=EC=AA=BD=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=EC=97=90=20=EC=95=88=EB=82=B4=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=20=EC=9E=85=EB=A0=A5=20UI=20=EC=B6=94=EA=B0=80=20[?= =?UTF-8?q?=EC=9B=90=EB=B3=B8=20=ED=99=94=EB=A9=B4=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?Combobox=20=EC=A0=84=ED=99=98]=20-=20PopCardListConfig=20?= =?UTF-8?q?=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20=EC=9B=90=EB=B3=B8=20=ED=99=94=EB=A9=B4=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=9D=84=20=20=20Select=EC=97=90=EC=84=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B0=80=EB=8A=A5=ED=95=9C=20Combobox=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EC=9D=98=20companyCode=EB=A1=9C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=AA=A9=EB=A1=9D=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../registry/pop-components/pop-button.tsx | 58 +++---- .../PopCardListV2Component.tsx | 9 +- .../pop-card-list-v2/PopCardListV2Config.tsx | 17 +- .../pop-card-list/PopCardListComponent.tsx | 9 +- .../pop-card-list/PopCardListConfig.tsx | 145 +++++++++++++----- .../pop-search/PopSearchComponent.tsx | 3 + frontend/lib/registry/pop-components/types.ts | 4 + 7 files changed, 171 insertions(+), 74 deletions(-) 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({ ) : (