feat(pop): 후속 액션 화면 이동 구현 + 입고확정 버튼 선택 상태 피드백

- PopViewerWithModals에 __pop_navigate__ 이벤트 구독 추가
  - targetScreenId가 있으면 해당 POP 화면으로 이동
  - "back"이면 router.back(), params는 쿼리스트링 전달
- 입고확정 버튼에 카드리스트 선택 상태 시각 피드백
  - 미선택: 기본 아이콘/색상
  - 선택됨: emerald-600 배경 + 선택 개수 뱃지
- selected_items connectionMeta category를 "event"로 변경하여 자동 매칭 대상 포함
This commit is contained in:
SeongHyun Kim 2026-03-03 17:13:01 +09:00
parent f12fca46be
commit 2e8300bbf5
3 changed files with 66 additions and 4 deletions

View File

@ -12,6 +12,7 @@
"use client"; "use client";
import { useState, useCallback, useEffect, useMemo } from "react"; import { useState, useCallback, useEffect, useMemo } from "react";
import { useRouter } from "next/navigation";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -61,6 +62,7 @@ export default function PopViewerWithModals({
overrideGap, overrideGap,
overridePadding, overridePadding,
}: PopViewerWithModalsProps) { }: PopViewerWithModalsProps) {
const router = useRouter();
const [modalStack, setModalStack] = useState<OpenModal[]>([]); const [modalStack, setModalStack] = useState<OpenModal[]>([]);
const { subscribe, publish } = usePopEvent(screenId); 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<string, string>;
};
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 () => { return () => {
unsubOpen(); unsubOpen();
unsubClose(); unsubClose();
unsubNavigate();
}; };
}, [subscribe, publish, layout.modals]); }, [subscribe, publish, layout.modals, router]);
// 최상위 모달만 닫기 (X 버튼, overlay 클릭, ESC) // 최상위 모달만 닫기 (X 버튼, overlay 클릭, ESC)
const handleCloseTopModal = useCallback(() => { const handleCloseTopModal = useCallback(() => {

View File

@ -420,6 +420,21 @@ export function PopButtonComponent({
const [showCartConfirm, setShowCartConfirm] = useState(false); const [showCartConfirm, setShowCartConfirm] = useState(false);
const [confirmProcessing, setConfirmProcessing] = useState(false); const [confirmProcessing, setConfirmProcessing] = useState(false);
const [showInboundConfirm, setShowInboundConfirm] = 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 전달) // 장바구니 상태 수신 (카드 목록에서 count/isDirty 전달)
useEffect(() => { useEffect(() => {
@ -673,6 +688,20 @@ export function PopButtonComponent({
return ""; return "";
}, [isCartMode, cartCount, cartIsDirty]); }, [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 ( return (
<> <>
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
@ -685,11 +714,12 @@ export function PopButtonComponent({
"transition-transform active:scale-95", "transition-transform active:scale-95",
isIconOnly && "px-2", isIconOnly && "px-2",
cartButtonClass, cartButtonClass,
inboundButtonClass,
)} )}
> >
{(isCartMode ? cartIconName : iconName) && ( {(isCartMode ? cartIconName : isInboundConfirmMode ? inboundIconName : iconName) && (
<DynamicLucideIcon <DynamicLucideIcon
name={isCartMode ? cartIconName : iconName} name={isCartMode ? cartIconName : isInboundConfirmMode ? inboundIconName : iconName}
size={16} size={16}
className={isIconOnly ? "" : "mr-1.5"} className={isIconOnly ? "" : "mr-1.5"}
/> />
@ -711,6 +741,16 @@ export function PopButtonComponent({
{cartCount} {cartCount}
</div> </div>
)} )}
{/* 입고 확정 선택 개수 배지 */}
{isInboundConfirmMode && inboundSelectedCount > 0 && (
<div
className="absolute -top-2 -right-2 flex items-center justify-center rounded-full bg-emerald-600 text-white text-[10px] font-bold"
style={{ minWidth: 18, height: 18, padding: "0 4px" }}
>
{inboundSelectedCount}
</div>
)}
</div> </div>
</div> </div>
@ -1990,6 +2030,7 @@ PopComponentRegistry.registerComponent({
{ key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "카드 목록에서 장바구니 변경 시 count/isDirty 수신" }, { key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "카드 목록에서 장바구니 변경 시 count/isDirty 수신" },
{ key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "카드 목록에서 DB 저장 완료 시 수신" }, { key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "카드 목록에서 DB 저장 완료 시 수신" },
{ key: "collected_data", label: "수집된 데이터", type: "event", category: "event", description: "컴포넌트에서 수집한 데이터+매핑 응답" }, { key: "collected_data", label: "수집된 데이터", type: "event", category: "event", description: "컴포넌트에서 수집한 데이터+매핑 응답" },
{ key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "카드 목록에서 체크박스로 선택된 항목 수 수신" },
], ],
}, },
touchOptimized: true, touchOptimized: true,

View File

@ -63,7 +63,7 @@ PopComponentRegistry.registerComponent({
{ key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" }, { key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" },
{ key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" }, { key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" },
{ key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "장바구니 DB 저장 완료 후 결과 전달" }, { 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: "데이터 수집 요청에 대한 응답 (선택 항목 + 매핑)" }, { key: "collected_data", label: "수집 응답", type: "event", category: "event", description: "데이터 수집 요청에 대한 응답 (선택 항목 + 매핑)" },
], ],
receivable: [ receivable: [