diff --git a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx index d0ef53f4..96413443 100644 --- a/frontend/app/(pop)/pop/screens/[screenId]/page.tsx +++ b/frontend/app/(pop)/pop/screens/[screenId]/page.tsx @@ -291,10 +291,10 @@ function PopScreenViewPage() { {/* 일반 모드 네비게이션 바 제거 (프로필 컴포넌트에서 PC 모드 전환 가능) */} {/* POP 화면 컨텐츠 */} -
+
{(() => { const adjustedGap = BLOCK_GAP; diff --git a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx index d79883ad..96901be1 100644 --- a/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx +++ b/frontend/components/pop/designer/panels/ComponentEditorPanel.tsx @@ -70,8 +70,8 @@ const COMPONENT_TYPE_LABELS: Record = { "pop-text": "텍스트", "pop-icon": "아이콘", "pop-dashboard": "대시보드", - "pop-card-list": "카드 목록", - "pop-card-list-v2": "카드 목록 V2", + "pop-card-list": "장바구니 목록", + "pop-card-list-v2": "MES 공정흐름", "pop-field": "필드", "pop-button": "버튼", "pop-string-list": "리스트 목록", diff --git a/frontend/components/pop/designer/panels/ComponentPalette.tsx b/frontend/components/pop/designer/panels/ComponentPalette.tsx index ddedc7d0..f4de9053 100644 --- a/frontend/components/pop/designer/panels/ComponentPalette.tsx +++ b/frontend/components/pop/designer/panels/ComponentPalette.tsx @@ -41,13 +41,13 @@ const PALETTE_ITEMS: PaletteItem[] = [ }, { type: "pop-card-list", - label: "카드 목록", + label: "장바구니 목록", icon: LayoutGrid, description: "테이블 데이터를 카드 형태로 표시", }, { type: "pop-card-list-v2", - label: "카드 목록 V2", + label: "MES 공정흐름", icon: LayoutGrid, description: "슬롯 기반 카드 (CSS Grid + 셀 타입별 렌더링)", }, diff --git a/frontend/components/pop/designer/renderers/PopRenderer.tsx b/frontend/components/pop/designer/renderers/PopRenderer.tsx index 3af031b4..f802dfc8 100644 --- a/frontend/components/pop/designer/renderers/PopRenderer.tsx +++ b/frontend/components/pop/designer/renderers/PopRenderer.tsx @@ -75,8 +75,8 @@ const COMPONENT_TYPE_LABELS: Record = { "pop-text": "텍스트", "pop-icon": "아이콘", "pop-dashboard": "대시보드", - "pop-card-list": "카드 목록", - "pop-card-list-v2": "카드 목록 V2", + "pop-card-list": "장바구니 목록", + "pop-card-list-v2": "MES 공정흐름", "pop-button": "버튼", "pop-string-list": "리스트 목록", "pop-search": "검색", @@ -145,13 +145,9 @@ export default function PopRenderer({ return Math.max(10, maxRowEnd + 3); }, [components, overrides, mode, hiddenIds]); - // V6: CSS Grid - 열은 1fr(뷰포트 꽉 채움), 행은 고정 BLOCK_SIZE - const rowTemplate = isDesignMode - ? `repeat(${dynamicRowCount}, ${BLOCK_SIZE}px)` - : `repeat(${dynamicRowCount}, minmax(${BLOCK_SIZE}px, auto))`; - const autoRowHeight = isDesignMode - ? `${BLOCK_SIZE}px` - : `minmax(${BLOCK_SIZE}px, auto)`; + // V6: CSS Grid - 열은 1fr(뷰포트 꽉 채움), 행은 고정 BLOCK_SIZE (디자이너/뷰어 동일 = WYSIWYG) + const rowTemplate = `repeat(${dynamicRowCount}, ${BLOCK_SIZE}px)`; + const autoRowHeight = `${BLOCK_SIZE}px`; const gridStyle = useMemo((): React.CSSProperties => ({ display: "grid", @@ -161,7 +157,7 @@ export default function PopRenderer({ gap: `${finalGap}px`, padding: `${finalPadding}px`, minHeight: "100%", - backgroundColor: "#ffffff", + backgroundColor: "hsl(var(--background))", position: "relative", }), [columns, finalGap, finalPadding, dynamicRowCount, rowTemplate, autoRowHeight]); @@ -296,11 +292,20 @@ export default function PopRenderer({ ); } - // 뷰어 모드: 드래그 없는 일반 렌더링 (overflow visible로 컨텐츠 확장 허용) + // 콘텐츠 영역 컴포넌트는 라운드 테두리 표시 + const contentTypes = new Set([ + "pop-dashboard", "pop-card-list", "pop-card-list-v2", + "pop-string-list", "pop-work-detail", "pop-sample", + ]); + const needsBorder = contentTypes.has(comp.type); + return (
{ if (!isCartMode) return ""; if (cartCount > 0 && !cartIsDirty) { - return "bg-emerald-600 hover:bg-emerald-700 text-white border-emerald-600"; + return "bg-primary hover:bg-primary/90 text-primary-foreground border-primary"; } if (cartIsDirty) { - return "bg-amber-500 hover:bg-orange-600 text-white border-orange-500 animate-pulse"; + return "bg-warning hover:bg-warning/90 text-warning-foreground border-warning animate-pulse"; } return ""; }, [isCartMode, cartCount, cartIsDirty]); @@ -1089,19 +1089,19 @@ export function PopButtonComponent({ config, label, isDesignMode, screenId, comp // 데이터 작업 버튼 2상태 색상: 미선택(기본) / 선택됨(초록) const inboundButtonClass = useMemo(() => { if (isCartMode) return ""; - return inboundSelectedCount > 0 ? "bg-emerald-600 hover:bg-emerald-700 text-white border-emerald-600" : ""; + return inboundSelectedCount > 0 ? "bg-primary hover:bg-primary/90 text-primary-foreground border-primary" : ""; }, [isCartMode, inboundSelectedCount]); return ( <> -
-
+
+
-
- )} - {/* CSS Grid 기반 셀 렌더링 */}
{cardGrid.cells.map((cell) => ( @@ -1880,10 +1646,7 @@ function CardV2({ cell, row, inputValue, - isCarted, onInputClick: handleInputClick, - onCartAdd: handleCartAdd, - onCartCancel: handleCartCancel, onEnterSelectMode, onActionButtonClick: async (taskPreset, actionRow, buttonConfig) => { const cfg = buttonConfig as Record | undefined; 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 73863b85..0b67dd82 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 @@ -1556,15 +1556,6 @@ function CellDetailEditor({
)} - {cell.type === "cart-button" && ( -
- 담기 버튼 설정 -
- onUpdate({ cartLabel: e.target.value })} placeholder="담기" className="h-7 flex-1 text-[10px]" /> - onUpdate({ cartCancelLabel: e.target.value })} placeholder="취소" className="h-7 flex-1 text-[10px]" /> -
-
- )}
); } diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Preview.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Preview.tsx index 8ebaf913..2f692dd7 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Preview.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Preview.tsx @@ -29,7 +29,7 @@ export function PopCardListV2PreviewComponent({ config }: PopCardListV2PreviewPr
- 카드 목록 V2 + MES 공정흐름
diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx index 9c24d382..deb573fc 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/cell-renderers.tsx @@ -9,7 +9,7 @@ import React, { useMemo, useState } from "react"; import { - ShoppingCart, X, Package, Truck, Box, Archive, Heart, Star, + X, Package, Truck, Box, Archive, Heart, Star, Loader2, CheckCircle2, CircleDot, Clock, Check, ChevronRight, type LucideIcon, } from "lucide-react"; @@ -27,16 +27,9 @@ type RowData = Record; // ===== 공통 유틸 ===== const LUCIDE_ICON_MAP: Record = { - ShoppingCart, Package, Truck, Box, Archive, Heart, Star, + Package, Truck, Box, Archive, Heart, Star, }; -function DynamicLucideIcon({ name, size = 20 }: { name?: string; size?: number }) { - if (!name) return ; - const IconComp = LUCIDE_ICON_MAP[name]; - if (!IconComp) return ; - return ; -} - function formatValue(value: unknown): string { if (value === null || value === undefined) return "-"; if (typeof value === "number") return value.toLocaleString(); @@ -60,11 +53,8 @@ export interface CellRendererProps { cell: CardCellDefinitionV2; row: RowData; inputValue?: number; - isCarted?: boolean; isButtonLoading?: boolean; onInputClick?: (e: React.MouseEvent) => void; - onCartAdd?: () => void; - onCartCancel?: () => void; onButtonClick?: (cell: CardCellDefinitionV2, row: RowData) => void; onActionButtonClick?: (taskPreset: string, row: RowData, buttonConfig?: Record) => void; onEnterSelectMode?: (whenStatus: string, buttonConfig: Record) => void; @@ -89,8 +79,6 @@ export function renderCellV2(props: CellRendererProps): React.ReactNode { return ; case "number-input": return ; - case "cart-button": - return ; case "package-summary": return ; case "status-badge": @@ -262,43 +250,7 @@ function NumberInputCell({ cell, row, inputValue, onInputClick }: CellRendererPr ); } -// ===== 7. cart-button ===== - -function CartButtonCell({ cell, row, isCarted, onCartAdd, onCartCancel }: CellRendererProps) { - const iconSize = 18; - const label = cell.cartLabel || "담기"; - const cancelLabel = cell.cartCancelLabel || "취소"; - - if (isCarted) { - return ( - - ); - } - - return ( - - ); -} - -// ===== 8. package-summary ===== +// ===== 7. package-summary ===== function PackageSummaryCell({ cell, packageEntries, inputUnit }: CellRendererProps) { if (!packageEntries || packageEntries.length === 0) return null; diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx index 138ab941..8bfb91e0 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx @@ -32,8 +32,8 @@ const defaultConfig: PopCardListV2Config = { PopComponentRegistry.registerComponent({ id: "pop-card-list-v2", - name: "카드 목록 V2", - description: "슬롯 기반 카드 레이아웃 (CSS Grid + 셀 타입별 렌더링)", + name: "MES 공정흐름", + description: "MES 생산실적 카드 레이아웃 (공정 흐름 + 상태 관리)", category: "display", icon: "LayoutGrid", component: PopCardListV2Component, @@ -44,15 +44,10 @@ PopComponentRegistry.registerComponent({ sendable: [ { key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" }, { key: "all_rows", label: "전체 데이터", type: "all_rows", 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: "event", category: "event", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" }, - { key: "collected_data", label: "수집 응답", type: "event", category: "event", description: "데이터 수집 요청에 대한 응답 (선택 항목 + 매핑)" }, + { key: "collected_data", label: "수집 응답", type: "event", category: "event", description: "데이터 수집 요청에 대한 응답 (항목 + 매핑)" }, ], receivable: [ { key: "filter_condition", label: "필터 조건", type: "filter_value", category: "filter", description: "외부 컴포넌트에서 받은 필터 조건으로 카드 목록 필터링" }, - { key: "cart_save_trigger", label: "저장 요청", type: "event", category: "event", description: "장바구니 DB 일괄 저장 트리거 (버튼에서 수신)" }, - { key: "confirm_trigger", label: "확정 트리거", type: "event", category: "event", description: "입고 확정 시 수정된 수량 일괄 반영 + 선택 항목 전달" }, { key: "collect_data", label: "수집 요청", type: "event", category: "event", description: "버튼에서 데이터+매핑 수집 요청 수신" }, ], }, diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/migrate.ts b/frontend/lib/registry/pop-components/pop-card-list-v2/migrate.ts index e4bfed8f..267d1501 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/migrate.ts +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/migrate.ts @@ -66,7 +66,7 @@ export function migrateCardListConfig(old: PopCardListConfig): PopCardListV2Conf // 3. 본문 필드들 (이미지 오른쪽) const fieldStartCol = old.cardTemplate?.image?.enabled ? 2 : 1; const fieldColSpan = old.cardTemplate?.image?.enabled ? 2 : 3; - const hasRightActions = !!(old.inputField?.enabled || old.cartAction); + const hasRightActions = !!old.inputField?.enabled; (old.cardTemplate?.body?.fields || []).forEach((field, i) => { cells.push({ @@ -102,20 +102,7 @@ export function migrateCardListConfig(old: PopCardListConfig): PopCardListV2Conf limitColumn: old.inputField.limitColumn || old.inputField.maxColumn, }); } - if (old.cartAction) { - cells.push({ - id: "cart", - row: nextRow + Math.ceil(bodyRowSpan / 2), - col: rightCol, - rowSpan: Math.floor(bodyRowSpan / 2) || 1, - colSpan: 1, - type: "cart-button", - cartLabel: old.cartAction.label, - cartCancelLabel: old.cartAction.cancelLabel, - cartIconType: old.cartAction.iconType, - cartIconValue: old.cartAction.iconValue, - }); - } + // 5. 포장 요약 (마지막 행, full-width) if (old.packageConfig?.enabled) { @@ -156,8 +143,6 @@ export function migrateCardListConfig(old: PopCardListConfig): PopCardListV2Conf responsiveDisplay: old.responsiveDisplay, inputField: old.inputField, packageConfig: old.packageConfig, - cartAction: old.cartAction, - cartListMode: old.cartListMode, saveMapping: old.saveMapping, }; } diff --git a/frontend/lib/registry/pop-components/pop-card-list/PopCardListPreview.tsx b/frontend/lib/registry/pop-components/pop-card-list/PopCardListPreview.tsx index 312567b9..78fbee7f 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/PopCardListPreview.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/PopCardListPreview.tsx @@ -42,7 +42,7 @@ export function PopCardListPreviewComponent({
- 카드 목록 + 장바구니 목록
{/* 설정 배지 */} 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 fe6a43df..62a1af57 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/index.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/index.tsx @@ -50,8 +50,8 @@ const defaultConfig: PopCardListConfig = { // 레지스트리 등록 PopComponentRegistry.registerComponent({ id: "pop-card-list", - name: "카드 목록", - description: "테이블 데이터를 카드 형태로 표시 (헤더 + 이미지 + 필드 목록)", + name: "장바구니 목록", + description: "장바구니 담기/확정 카드 목록 (입고, 출고, 수주 등)", category: "display", icon: "LayoutGrid", component: PopCardListComponent, diff --git a/frontend/lib/registry/pop-components/pop-icon.tsx b/frontend/lib/registry/pop-components/pop-icon.tsx index ff04ab7b..93cbe9ae 100644 --- a/frontend/lib/registry/pop-components/pop-icon.tsx +++ b/frontend/lib/registry/pop-components/pop-icon.tsx @@ -103,6 +103,7 @@ export interface PopIconConfig { labelColor?: string; labelFontSize?: number; backgroundColor?: string; + iconColor?: string; gradient?: GradientConfig; borderRadiusPercent?: number; sizeMode: IconSizeMode; @@ -337,12 +338,14 @@ export function PopIconComponent({ setPendingNavigate(null); }; - // 배경 스타일 (이미지 타입일 때는 배경 없음) + // 배경 스타일: transparent 설정이 최우선 const backgroundStyle: React.CSSProperties = iconType === "image" ? { backgroundColor: "transparent" } - : config?.gradient - ? buildGradientStyle(config.gradient) - : { backgroundColor: config?.backgroundColor || "#e0e0e0" }; + : config?.backgroundColor === "transparent" + ? { backgroundColor: "transparent" } + : config?.gradient + ? buildGradientStyle(config.gradient) + : { backgroundColor: config?.backgroundColor || "hsl(var(--muted))" }; // 테두리 반경 (0% = 사각형, 100% = 원형) const radiusPercent = config?.borderRadiusPercent ?? 20; @@ -352,6 +355,8 @@ export function PopIconComponent({ const isLabelRight = config?.labelPosition === "right"; const showLabel = config?.labelPosition !== "none" && (config?.label || label); + const effectiveIconColor = config?.iconColor || "#ffffff"; + // 아이콘 렌더링 const renderIcon = () => { // 빠른 선택 @@ -361,7 +366,7 @@ export function PopIconComponent({ ); } else if (config?.quickSelectType === "emoji" && config?.quickSelectValue) { @@ -398,36 +403,40 @@ export function PopIconComponent({ return 📦; }; + const hasLabel = showLabel && (config?.label || label); + const labelFontSize = config?.labelFontSize || 12; + return (
- {/* 아이콘 컨테이너 */} + {/* 아이콘 컨테이너: 라벨이 있으면 라벨 공간만큼 축소 */}
{renderIcon()}
{/* 라벨 */} - {showLabel && ( + {hasLabel && ( {config?.label || label} @@ -453,8 +462,6 @@ export function PopIconComponent({ 확인 후 이동 @@ -853,23 +860,69 @@ function LabelSettings({ config, onUpdate }: PopIconConfigPanelProps) { // 스타일 설정 function StyleSettings({ config, onUpdate }: PopIconConfigPanelProps) { + const bgColor = config?.backgroundColor || ""; + const iconColor = config?.iconColor || "#ffffff"; + const isTransparent = bgColor === "transparent"; + return ( -
- - onUpdate({ - ...config, - borderRadiusPercent: Number(e.target.value) - })} - className="w-full" - /> +
+ {/* 배경색 */} +
+ +
+ + {!isTransparent && ( + onUpdate({ ...config, backgroundColor: e.target.value })} + className="h-8 w-12 cursor-pointer p-0.5" + /> + )} +
+
+ + {/* 아이콘 색상 */} +
+ + onUpdate({ ...config, iconColor: e.target.value })} + className="h-8 w-12 cursor-pointer p-0.5" + /> +
+ + {/* 모서리 */} +
+ + onUpdate({ + ...config, + borderRadiusPercent: Number(e.target.value) + })} + className="w-full" + /> +
); } diff --git a/frontend/lib/registry/pop-components/pop-profile.tsx b/frontend/lib/registry/pop-components/pop-profile.tsx index 49aaa10c..325b3ea3 100644 --- a/frontend/lib/registry/pop-components/pop-profile.tsx +++ b/frontend/lib/registry/pop-components/pop-profile.tsx @@ -111,7 +111,7 @@ function PopProfileComponent({ config: rawConfig }: PopProfileComponentProps) { sizeInfo.container, sizeInfo.text, )} - style={{ minWidth: sizeInfo.px, minHeight: sizeInfo.px }} + style={{ width: sizeInfo.px, height: sizeInfo.px, maxWidth: "100%", maxHeight: "100%" }} > {user?.photo && user.photo.trim() !== "" && user.photo !== "null" ? ( = { @@ -223,6 +227,16 @@ function DesignModePreview({ ); default: // 일반 텍스트 미리보기 + if (config?.marquee) { + return ( +
+ [마키] + + {config?.content || label || "텍스트"} + +
+ ); + } return (
; + } - // 정렬 래퍼 클래스 const alignWrapperClass = cn( "flex w-full h-full", VERTICAL_ALIGN_CLASSES[config?.verticalAlign || "center"], @@ -380,12 +398,56 @@ function TextDisplay({ return (
- {config?.content || label || "텍스트"} + {text}
); } +function MarqueeDisplay({ + config, + text, + sizeClass, +}: { + config?: PopTextConfig; + text: string; + sizeClass: string; +}) { + const speed = config?.marqueeSpeed || 15; + const iconName = config?.marqueeIcon; + const weightClass = FONT_WEIGHT_CLASSES[config?.fontWeight || "normal"]; + const uniqueId = React.useId().replace(/:/g, ""); + + return ( +
+ {iconName && (() => { + const pascalName = iconName.replace(/(^|-)(\w)/g, (_: string, __: string, c: string) => c.toUpperCase()); + const LucideIcon = (lucideIcons as Record>)[pascalName]; + return LucideIcon ? ( +
+ +
+ ) : null; + })()} +
+
+ {text} + {text} +
+ +
+
+ ); +} + // ======================================== // 설정 패널 // ======================================== @@ -450,6 +512,44 @@ export function PopTextConfigPanel({ className="text-xs resize-none" />
+ + {/* 마키(흐르는 텍스트) 설정 */} + +
+
+ + onUpdate({ ...config, marquee: v })} + /> +
+ {config?.marquee && ( + <> +
+ + onUpdate({ ...config, marqueeSpeed: Number(e.target.value) })} + className="w-full" + /> +
+
+ + onUpdate({ ...config, marqueeIcon: e.target.value })} + placeholder="예: flag, megaphone, info" + className="h-8 text-xs" + /> +
+ + )} +
+ diff --git a/frontend/lib/registry/pop-components/types.ts b/frontend/lib/registry/pop-components/types.ts index 8a1341df..f2aa0c4c 100644 --- a/frontend/lib/registry/pop-components/types.ts +++ b/frontend/lib/registry/pop-components/types.ts @@ -736,7 +736,6 @@ export type CardCellType = | "badge" | "button" | "number-input" - | "cart-button" | "package-summary" | "status-badge" | "timeline" @@ -822,12 +821,6 @@ export interface CardCellDefinitionV2 { limitColumn?: string; autoInitMax?: boolean; - // cart-button 타입 전용 - cartLabel?: string; - cartCancelLabel?: string; - cartIconType?: "lucide" | "emoji"; - cartIconValue?: string; - // status-badge 타입 전용 statusColumn?: string; statusMap?: Array<{ value: string; label: string; color: string }>; @@ -1000,8 +993,6 @@ export interface PopCardListV2Config { responsiveDisplay?: CardResponsiveConfig; inputField?: CardInputFieldConfig; packageConfig?: CardPackageConfig; - cartAction?: CardCartActionConfig; - cartListMode?: CartListModeConfig; saveMapping?: CardListSaveMapping; ownerSortColumn?: string; ownerFilterMode?: "priority" | "only";