From 2e8300bbf5efba7ed1bc43589d003b3229890a4a Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 3 Mar 2026 17:13:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(pop):=20=ED=9B=84=EC=86=8D=20=EC=95=A1?= =?UTF-8?q?=EC=85=98=20=ED=99=94=EB=A9=B4=20=EC=9D=B4=EB=8F=99=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20+=20=EC=9E=85=EA=B3=A0=ED=99=95=EC=A0=95=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=84=A0=ED=83=9D=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20-=20PopViewerWithModals=EC=97=90?= =?UTF-8?q?=20=5F=5Fpop=5Fnavigate=5F=5F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EC=B6=94=EA=B0=80=20=20=20-=20targetScree?= =?UTF-8?q?nId=EA=B0=80=20=EC=9E=88=EC=9C=BC=EB=A9=B4=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=20POP=20=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=20=20-=20"back"=EC=9D=B4=EB=A9=B4=20router.back(),?= =?UTF-8?q?=20params=EB=8A=94=20=EC=BF=BC=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A7=81=20=EC=A0=84=EB=8B=AC=20-=20=EC=9E=85=EA=B3=A0?= =?UTF-8?q?=ED=99=95=EC=A0=95=20=EB=B2=84=ED=8A=BC=EC=97=90=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=8B=9C=EA=B0=81=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=20=20-=20=EB=AF=B8=EC=84=A0=ED=83=9D:=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=95=84=EC=9D=B4=EC=BD=98/=EC=83=89=EC=83=81=20?= =?UTF-8?q?=20=20-=20=EC=84=A0=ED=83=9D=EB=90=A8:=20emerald-600=20?= =?UTF-8?q?=EB=B0=B0=EA=B2=BD=20+=20=EC=84=A0=ED=83=9D=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EB=B1=83=EC=A7=80=20-=20selected=5Fitems=20connect?= =?UTF-8?q?ionMeta=20category=EB=A5=BC=20"event"=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EC=97=AC=20=EC=9E=90=EB=8F=99=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=20=EB=8C=80=EC=83=81=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pop/viewer/PopViewerWithModals.tsx | 23 +++++++++- .../registry/pop-components/pop-button.tsx | 45 ++++++++++++++++++- .../pop-components/pop-card-list/index.tsx | 2 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/frontend/components/pop/viewer/PopViewerWithModals.tsx b/frontend/components/pop/viewer/PopViewerWithModals.tsx index 78d1b647..f322d4c0 100644 --- a/frontend/components/pop/viewer/PopViewerWithModals.tsx +++ b/frontend/components/pop/viewer/PopViewerWithModals.tsx @@ -12,6 +12,7 @@ "use client"; import { useState, useCallback, useEffect, useMemo } from "react"; +import { useRouter } from "next/navigation"; import { Dialog, DialogContent, @@ -61,6 +62,7 @@ export default function PopViewerWithModals({ overrideGap, overridePadding, }: PopViewerWithModalsProps) { + const router = useRouter(); const [modalStack, setModalStack] = useState([]); const { subscribe, publish } = usePopEvent(screenId); @@ -126,11 +128,30 @@ export default function PopViewerWithModals({ }); }); + const unsubNavigate = subscribe("__pop_navigate__", (payload: unknown) => { + const data = payload as { + screenId?: string; + params?: Record; + }; + + if (!data?.screenId) return; + + if (data.screenId === "back") { + router.back(); + } else { + const query = data.params + ? "?" + new URLSearchParams(data.params).toString() + : ""; + window.location.href = `/pop/screens/${data.screenId}${query}`; + } + }); + return () => { unsubOpen(); unsubClose(); + unsubNavigate(); }; - }, [subscribe, publish, layout.modals]); + }, [subscribe, publish, layout.modals, router]); // 최상위 모달만 닫기 (X 버튼, overlay 클릭, ESC) const handleCloseTopModal = useCallback(() => { diff --git a/frontend/lib/registry/pop-components/pop-button.tsx b/frontend/lib/registry/pop-components/pop-button.tsx index a3d7f4b9..0dc5aa50 100644 --- a/frontend/lib/registry/pop-components/pop-button.tsx +++ b/frontend/lib/registry/pop-components/pop-button.tsx @@ -420,6 +420,21 @@ export function PopButtonComponent({ const [showCartConfirm, setShowCartConfirm] = useState(false); const [confirmProcessing, setConfirmProcessing] = useState(false); const [showInboundConfirm, setShowInboundConfirm] = useState(false); + const [inboundSelectedCount, setInboundSelectedCount] = useState(0); + + // 입고 확정 모드: 선택 항목 수 수신 + useEffect(() => { + if (!isInboundConfirmMode || !componentId) return; + const unsub = subscribe( + `__comp_input__${componentId}__selected_items`, + (payload: unknown) => { + const data = payload as { value?: unknown[] } | undefined; + const items = Array.isArray(data?.value) ? data.value : []; + setInboundSelectedCount(items.length); + } + ); + return unsub; + }, [isInboundConfirmMode, componentId, subscribe]); // 장바구니 상태 수신 (카드 목록에서 count/isDirty 전달) useEffect(() => { @@ -673,6 +688,20 @@ export function PopButtonComponent({ return ""; }, [isCartMode, cartCount, cartIsDirty]); + // 입고 확정 2상태 아이콘: 미선택(기본 아이콘) / 선택됨(체크 아이콘) + const inboundIconName = useMemo(() => { + if (!isInboundConfirmMode) return iconName; + return inboundSelectedCount > 0 ? (config?.icon || "PackageCheck") : (config?.icon || "PackageCheck"); + }, [isInboundConfirmMode, inboundSelectedCount, config?.icon, iconName]); + + // 입고 확정 2상태 버튼 색상: 미선택(기본) / 선택됨(초록) + const inboundButtonClass = useMemo(() => { + if (!isInboundConfirmMode) return ""; + return inboundSelectedCount > 0 + ? "bg-emerald-600 hover:bg-emerald-700 text-white border-emerald-600" + : ""; + }, [isInboundConfirmMode, inboundSelectedCount]); + return ( <>
@@ -685,11 +714,12 @@ export function PopButtonComponent({ "transition-transform active:scale-95", isIconOnly && "px-2", cartButtonClass, + inboundButtonClass, )} > - {(isCartMode ? cartIconName : iconName) && ( + {(isCartMode ? cartIconName : isInboundConfirmMode ? inboundIconName : iconName) && ( @@ -711,6 +741,16 @@ export function PopButtonComponent({ {cartCount}
)} + + {/* 입고 확정 선택 개수 배지 */} + {isInboundConfirmMode && inboundSelectedCount > 0 && ( +
+ {inboundSelectedCount} +
+ )} @@ -1990,6 +2030,7 @@ PopComponentRegistry.registerComponent({ { key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "카드 목록에서 장바구니 변경 시 count/isDirty 수신" }, { key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "카드 목록에서 DB 저장 완료 시 수신" }, { key: "collected_data", label: "수집된 데이터", type: "event", category: "event", description: "컴포넌트에서 수집한 데이터+매핑 응답" }, + { key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "카드 목록에서 체크박스로 선택된 항목 수 수신" }, ], }, touchOptimized: true, diff --git a/frontend/lib/registry/pop-components/pop-card-list/index.tsx b/frontend/lib/registry/pop-components/pop-card-list/index.tsx index 01b9bf64..b9b769af 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/index.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/index.tsx @@ -63,7 +63,7 @@ PopComponentRegistry.registerComponent({ { key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" }, { key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" }, { key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "장바구니 DB 저장 완료 후 결과 전달" }, - { key: "selected_items", label: "선택된 항목", type: "value", category: "data", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" }, + { key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" }, { key: "collected_data", label: "수집 응답", type: "event", category: "event", description: "데이터 수집 요청에 대한 응답 (선택 항목 + 매핑)" }, ], receivable: [