"use client";

/**
 * pop-card-list-v2 런타임 컴포넌트
 *
 * pop-card-list의 데이터 로딩/필터링/페이징/장바구니 로직을 재활용하되,
 * 카드 내부 렌더링은 CSS Grid + 셀 타입별 렌더러(cell-renderers.tsx)로 대체.
 */

import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import {
  Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight, Trash2, Check, X,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
  Dialog, DialogContent, DialogHeader, DialogTitle,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import type {
  PopCardListV2Config,
  CardGridConfigV2,
  CardCellDefinitionV2,
  CardInputFieldConfig,
  CardCartActionConfig,
  CardPackageConfig,
  CardPresetSpec,
  CartItem,
  PackageEntry,
  CollectDataRequest,
  CollectedDataResponse,
  TimelineProcessStep,
  TimelineDataSource,
  ActionButtonUpdate,
  ActionButtonClickAction,
  QuantityInputConfig,
  StatusValueMapping,
  SelectModeConfig,
  SelectModeButtonConfig,
} from "../types";
import {
  CARD_PRESET_SPECS, DEFAULT_CARD_IMAGE,
  VIRTUAL_SUB_STATUS, VIRTUAL_SUB_SEMANTIC, VIRTUAL_SUB_PROCESS, VIRTUAL_SUB_SEQ,
} from "../types";
import { dataApi } from "@/lib/api/data";
import { screenApi } from "@/lib/api/screen";
import { apiClient } from "@/lib/api/client";
import { usePopEvent } from "@/hooks/pop/usePopEvent";
import { useCartSync } from "@/hooks/pop/useCartSync";
import { useAuth } from "@/hooks/useAuth";
import { NumberInputModal } from "../pop-card-list/NumberInputModal";
import { renderCellV2 } from "./cell-renderers";
import type { PopLayoutDataV5 } from "@/components/pop/designer/types/pop-layout";
import { isV5Layout, detectGridMode } from "@/components/pop/designer/types/pop-layout";
import dynamic from "next/dynamic";
const PopViewerWithModals = dynamic(() => import("@/components/pop/viewer/PopViewerWithModals"), { ssr: false });

type RowData = Record<string, unknown>;

function calculateMaxQty(
  row: RowData,
  processId: string | number | undefined,
  cfg?: QuantityInputConfig,
): number {
  if (!cfg) return 999999;
  const maxVal = cfg.maxColumn ? Number(row[cfg.maxColumn]) || 999999 : 999999;
  if (!cfg.currentColumn) return maxVal;

  const processFlow = row.__processFlow__ as Array<{
    isCurrent: boolean;
    processId?: string | number;
    rawData?: Record<string, unknown>;
  }> | undefined;

  const currentProcess = processId
    ? processFlow?.find((p) => String(p.processId) === String(processId))
    : processFlow?.find((p) => p.isCurrent);

  if (currentProcess?.rawData) {
    const currentVal = Number(currentProcess.rawData[cfg.currentColumn]) || 0;
    return Math.max(0, maxVal - currentVal);
  }
  return maxVal;
}

// cart_items 행 파싱 (pop-card-list에서 그대로 차용)
function parseCartRow(dbRow: Record<string, unknown>): Record<string, unknown> {
  let rowData: Record<string, unknown> = {};
  try {
    const raw = dbRow.row_data;
    if (typeof raw === "string" && raw.trim()) rowData = JSON.parse(raw);
    else if (typeof raw === "object" && raw !== null) rowData = raw as Record<string, unknown>;
  } catch { rowData = {}; }

  return {
    ...rowData,
    __cart_id: dbRow.id,
    __cart_quantity: Number(dbRow.quantity) || 0,
    __cart_package_unit: dbRow.package_unit || "",
    __cart_package_entries: dbRow.package_entries,
    __cart_status: dbRow.status || "in_cart",
    __cart_memo: dbRow.memo || "",
    __cart_row_key: dbRow.row_key || "",
    __cart_modified: false,
  };
}

// 레거시 statusValues(고정 4키 객체) → statusMappings(동적 배열) 자동 변환
function resolveStatusMappings(src: TimelineDataSource): StatusValueMapping[] {
  if (src.statusMappings && src.statusMappings.length > 0) return src.statusMappings;

  // 레거시 호환: 기존 statusValues 객체가 있으면 변환
  const sv = (src as Record<string, unknown>).statusValues as Record<string, string> | undefined;
  return [
    { dbValue: sv?.waiting || "waiting", label: "대기", semantic: "pending" as const },
    { dbValue: sv?.accepted || "accepted", label: "접수", semantic: "active" as const },
    { dbValue: sv?.inProgress || "in_progress", label: "진행중", semantic: "active" as const },
    { dbValue: sv?.completed || "completed", label: "완료", semantic: "done" as const },
  ];
}

interface PopCardListV2ComponentProps {
  config?: PopCardListV2Config;
  className?: string;
  screenId?: string;
  componentId?: string;
  currentRowSpan?: number;
  currentColSpan?: number;
  onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
}

export function PopCardListV2Component({
  config,
  className,
  screenId,
  componentId,
  currentRowSpan,
  currentColSpan,
  onRequestResize,
}: PopCardListV2ComponentProps) {
  const { subscribe, publish } = usePopEvent(screenId || "default");
  const router = useRouter();
  const { userId: currentUserId } = useAuth();

  const isCartListMode = config?.cartListMode?.enabled === true;
  const [inheritedConfig, setInheritedConfig] = useState<Partial<PopCardListV2Config> | null>(null);
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());

  const effectiveConfig = useMemo<PopCardListV2Config | undefined>(() => {
    if (!isCartListMode || !inheritedConfig) return config;
    return {
      ...config,
      ...inheritedConfig,
      cartListMode: config?.cartListMode,
      dataSource: config?.dataSource,
    } as PopCardListV2Config;
  }, [config, inheritedConfig, isCartListMode]);

  const isHorizontalMode = (effectiveConfig?.scrollDirection || "vertical") === "horizontal";
  const maxGridColumns = effectiveConfig?.gridColumns || 2;
  const configGridRows = effectiveConfig?.gridRows || 3;
  const dataSource = effectiveConfig?.dataSource;
  const cardGrid = effectiveConfig?.cardGrid;

  const sourceTableName = (!isCartListMode && dataSource?.tableName) || "";
  const cart = useCartSync(screenId || "", sourceTableName);

  const [rows, setRows] = useState<RowData[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // 외부 필터
  const [externalFilters, setExternalFilters] = useState<
    Map<string, {
      fieldName: string;
      value: unknown;
      filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string; isSubTable?: boolean };
      _source?: string;
    }>
  >(new Map());

  useEffect(() => {
    if (!componentId) return;
    const unsub = subscribe(
      `__comp_input__${componentId}__filter_condition`,
      (payload: unknown) => {
        const data = payload as {
          value?: { fieldName?: string; value?: unknown; _source?: string };
          filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string; isSubTable?: boolean };
          _connectionId?: string;
        };
        const connId = data?._connectionId || "default";
        setExternalFilters((prev) => {
          const next = new Map(prev);
          if (data?.value?.value) {
            next.set(connId, {
              fieldName: data.value.fieldName || "",
              value: data.value.value,
              filterConfig: data.filterConfig,
              _source: data.value._source,
            });
          } else {
            next.delete(connId);
          }
          return next;
        });
      },
    );
    return unsub;
  }, [componentId, subscribe]);

  const cartRef = useRef(cart);
  cartRef.current = cart;

  // 저장 요청 수신
  useEffect(() => {
    if (!componentId || isCartListMode) return;
    const unsub = subscribe(
      `__comp_input__${componentId}__cart_save_trigger`,
      async (payload: unknown) => {
        const data = payload as { value?: { selectedColumns?: string[] } } | undefined;
        const ok = await cartRef.current.saveToDb(data?.value?.selectedColumns);
        publish(`__comp_output__${componentId}__cart_save_completed`, { success: ok });
      },
    );
    return unsub;
  }, [componentId, subscribe, publish, isCartListMode]);

  // 초기 장바구니 상태 전달
  useEffect(() => {
    if (!componentId || cart.loading || isCartListMode) return;
    publish(`__comp_output__${componentId}__cart_updated`, {
      count: cart.cartCount,
      isDirty: cart.isDirty,
    });
  }, [componentId, cart.loading, cart.cartCount, cart.isDirty, publish, isCartListMode]);

  // ===== 선택 모드 =====
  const [selectMode, setSelectMode] = useState(false);
  const [selectModeStatus, setSelectModeStatus] = useState<string>("");
  const [selectModeConfig, setSelectModeConfig] = useState<SelectModeConfig | null>(null);
  const [selectedRowIds, setSelectedRowIds] = useState<Set<string>>(new Set());
  const [selectProcessing, setSelectProcessing] = useState(false);

  // ===== 모달 열기 (POP 화면) =====
  const [popModalOpen, setPopModalOpen] = useState(false);
  const [popModalLayout, setPopModalLayout] = useState<PopLayoutDataV5 | null>(null);
  const [popModalScreenId, setPopModalScreenId] = useState<string>("");
  const [popModalRow, setPopModalRow] = useState<RowData | null>(null);

  const openPopModal = useCallback(async (screenIdStr: string, row: RowData) => {
    try {
      const sid = parseInt(screenIdStr, 10);
      if (isNaN(sid)) {
        toast.error("올바른 화면 ID가 아닙니다.");
        return;
      }
      const popLayout = await screenApi.getLayoutPop(sid);
      if (popLayout && isV5Layout(popLayout)) {
        setPopModalLayout(popLayout);
        setPopModalScreenId(String(sid));
        setPopModalRow(row);
        setPopModalOpen(true);
      } else {
        toast.error("해당 POP 화면을 찾을 수 없습니다.");
      }
    } catch {
      toast.error("POP 화면을 불러오는데 실패했습니다.");
    }
  }, []);

  const handleCardSelect = useCallback((row: RowData) => {

    if (effectiveConfig?.cardClickAction === "modal-open" && effectiveConfig?.cardClickModalConfig?.screenId) {
      const mc = effectiveConfig.cardClickModalConfig;
      if (mc.condition && mc.condition.type !== "always") {
        const processFlow = row.__processFlow__ as { isCurrent: boolean; status?: string }[] | undefined;
        const currentProcess = processFlow?.find((s) => s.isCurrent);
        if (mc.condition.type === "timeline-status") {
          if (currentProcess?.status !== mc.condition.value) return;
        } else if (mc.condition.type === "column-value") {
          if (String(row[mc.condition.column || ""] ?? "") !== mc.condition.value) return;
        }
      }
      openPopModal(mc.screenId, row);
      return;
    }
    if (!componentId) return;
    publish(`__comp_output__${componentId}__selected_row`, row);
  }, [componentId, publish, effectiveConfig, openPopModal]);

  const enterSelectMode = useCallback((whenStatus: string, buttonConfig: Record<string, unknown>) => {
    const smConfig = buttonConfig.selectModeConfig as SelectModeConfig | undefined;
    if (!smConfig) return;
    setSelectMode(true);
    setSelectModeStatus(smConfig.filterStatus || whenStatus);
    setSelectModeConfig(smConfig);
    setSelectedRowIds(new Set());
  }, []);

  const exitSelectMode = useCallback(() => {
    setSelectMode(false);
    setSelectModeStatus("");
    setSelectModeConfig(null);
    setSelectedRowIds(new Set());
  }, []);

  const toggleRowSelection = useCallback((row: RowData) => {
    const rowId = String(row.id ?? row.pk ?? "");
    if (!rowId) return;
    setSelectedRowIds((prev) => {
      const next = new Set(prev);
      if (next.has(rowId)) next.delete(rowId); else next.add(rowId);
      return next;
    });
  }, []);

  const isRowSelectable = useCallback((row: RowData) => {
    if (!selectMode) return false;
    const subStatus = row[VIRTUAL_SUB_STATUS];
    if (subStatus !== undefined) return String(subStatus) === selectModeStatus;
    return true;
  }, [selectMode, selectModeStatus]);

  // 확장/페이지네이션
  const [isExpanded, setIsExpanded] = useState(false);
  const [currentPage, setCurrentPage] = useState(1);
  const [originalRowSpan, setOriginalRowSpan] = useState<number | null>(null);

  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState(0);
  const [containerHeight, setContainerHeight] = useState(0);
  const baseContainerHeight = useRef(0);

  useEffect(() => {
    if (!containerRef.current) return;
    const observer = new ResizeObserver((entries) => {
      const entry = entries[0];
      if (!entry) return;
      const { width, height } = entry.contentRect;
      if (width > 0) setContainerWidth(width);
      if (height > 0) setContainerHeight(height);
    });
    observer.observe(containerRef.current);
    return () => observer.disconnect();
  }, []);

  const cardSizeKey = effectiveConfig?.cardSize || "large";
  const spec: CardPresetSpec = CARD_PRESET_SPECS[cardSizeKey] || CARD_PRESET_SPECS.large;

  const maxAllowedColumns = useMemo(() => {
    if (!currentColSpan) return maxGridColumns;
    if (currentColSpan >= 8) return maxGridColumns;
    return 1;
  }, [currentColSpan, maxGridColumns]);

  const minCardWidth = Math.round(spec.height * 1.6);
  const autoColumns = containerWidth > 0
    ? Math.max(1, Math.floor((containerWidth + spec.gap) / (minCardWidth + spec.gap)))
    : maxGridColumns;
  const gridColumns = Math.max(1, Math.min(autoColumns, maxGridColumns, maxAllowedColumns));
  const gridRows = configGridRows;

  // 셀 설정에서 timelineSource 탐색 (timeline/status-badge/action-buttons 중 하나에 설정됨)
  const timelineSource = useMemo<TimelineDataSource | undefined>(() => {
    const cells = cardGrid?.cells || [];
    for (const c of cells) {
      if ((c.type === "timeline" || c.type === "status-badge" || c.type === "action-buttons") && c.timelineSource?.processTable) {
        return c.timelineSource;
      }
    }
    return undefined;
  }, [cardGrid?.cells]);

  // 외부 필터 (메인 테이블 + 하위 테이블 분기)
  const filteredRows = useMemo(() => {
    if (externalFilters.size === 0) return rows;

    const allFilters = [...externalFilters.values()];
    const mainFilters = allFilters.filter((f) => !f.filterConfig?.isSubTable);
    const subFilters = allFilters.filter((f) => f.filterConfig?.isSubTable);

    // 1단계: 하위 테이블 필터 → __subStatus__ 주입
    const afterSubFilter = subFilters.length === 0
      ? rows
      : rows
          .map((row) => {
            const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
            if (!processFlow || processFlow.length === 0) return null;

            const matchingSteps = processFlow.filter((step) =>
              subFilters.every((filter) => {
                const searchValue = String(filter.value).toLowerCase();
                if (!searchValue) return true;
                const fc = filter.filterConfig;
                const col = fc?.targetColumn || filter.fieldName || "";
                if (!col) return true;
                const cellValue = String(step.rawData?.[col] ?? "").toLowerCase();
                const mode = fc?.filterMode || "contains";
                switch (mode) {
                  case "equals": return cellValue === searchValue;
                  case "starts_with": return cellValue.startsWith(searchValue);
                  default: return cellValue.includes(searchValue);
                }
              }),
            );

            if (matchingSteps.length === 0) return null;

            const matched = matchingSteps[0];
            // 매칭된 공정을 타임라인에서 강조
            const updatedFlow = processFlow.map((s) => ({
              ...s,
              isCurrent: s.seqNo === matched.seqNo,
            }));
            return {
              ...row,
              __processFlow__: updatedFlow,
              [VIRTUAL_SUB_STATUS]: matched.status,
              [VIRTUAL_SUB_SEMANTIC]: matched.semantic || "pending",
              [VIRTUAL_SUB_PROCESS]: matched.processName,
              [VIRTUAL_SUB_SEQ]: matched.seqNo,
            };
          })
          .filter((row): row is RowData => row !== null);

    // 2단계: 메인 테이블 필터 (__subStatus__ 주입된 데이터 기반)
    if (mainFilters.length === 0) return afterSubFilter;

    return afterSubFilter.filter((row) =>
      mainFilters.every((filter) => {
        const searchValue = String(filter.value).toLowerCase();
        if (!searchValue) return true;
        const fc = filter.filterConfig;
        const columns: string[] =
          fc?.targetColumns?.length ? fc.targetColumns
            : fc?.targetColumn ? [fc.targetColumn]
              : filter.fieldName ? [filter.fieldName] : [];
        if (columns.length === 0) return true;
        const mode = fc?.filterMode || "contains";

        // 하위 필터 활성 시: 상태 컬럼(status 등)을 __subStatus__로 대체
        const subCol = subFilters.length > 0 ? VIRTUAL_SUB_STATUS : null;
        const statusCol = timelineSource?.statusColumn || "status";
        const effectiveColumns = subCol
          ? columns.map((col) => col === statusCol || col === "status" ? subCol : col)
          : columns;

        return effectiveColumns.some((col) => {
          const cellValue = String(row[col] ?? "").toLowerCase();
          switch (mode) {
            case "equals": return cellValue === searchValue;
            case "starts_with": return cellValue.startsWith(searchValue);
            default: return cellValue.includes(searchValue);
          }
        });
      }),
    );
  }, [rows, externalFilters, timelineSource]);

  // 하위 필터 활성 여부
  const hasActiveSubFilter = useMemo(() => {
    if (externalFilters.size === 0) return false;
    return [...externalFilters.values()].some((f) => f.filterConfig?.isSubTable);
  }, [externalFilters]);

  // 선택 모드 일괄 처리
  const handleSelectModeAction = useCallback(async (btnConfig: SelectModeButtonConfig) => {
    if (btnConfig.clickMode === "cancel-select") {
      exitSelectMode();
      return;
    }

    if (btnConfig.clickMode === "status-change" && btnConfig.updates && btnConfig.targetTable) {
      if (selectedRowIds.size === 0) {
        toast.error("선택된 항목이 없습니다.");
        return;
      }
      if (btnConfig.confirmMessage && !window.confirm(btnConfig.confirmMessage)) return;

      setSelectProcessing(true);
      try {
        const selectedRows = filteredRows.filter((r) => {
          const rowId = String(r.id ?? r.pk ?? "");
          return selectedRowIds.has(rowId);
        });

        let successCount = 0;
        for (const row of selectedRows) {
          const processFlow = row.__processFlow__ as { isCurrent: boolean; processId?: string | number }[] | undefined;
          const currentProcess = processFlow?.find((s) => s.isCurrent);
          const targetId = currentProcess?.processId ?? row.id ?? row.pk;
          if (!targetId) continue;

          const tasks = btnConfig.updates.map((u, idx) => ({
            id: `sel-update-${idx}`,
            type: "data-update" as const,
            targetTable: btnConfig.targetTable!,
            targetColumn: u.column,
            operationType: (u.operationType || "assign") as "assign" | "add" | "subtract",
            valueSource: "fixed" as const,
            fixedValue: u.valueType === "static" ? (u.value ?? "") :
                        u.valueType === "currentUser" ? "__CURRENT_USER__" :
                        u.valueType === "currentTime" ? "__CURRENT_TIME__" :
                        (u.value ?? ""),
            lookupMode: "manual" as const,
            manualItemField: "id",
            manualPkColumn: "id",
          }));

          const result = await apiClient.post("/pop/execute-action", {
            tasks,
            data: { items: [{ ...row, id: targetId }], fieldValues: {} },
            mappings: {},
          });
          if (result.data?.success) successCount++;
        }

        if (successCount > 0) {
          toast.success(`${successCount}건 처리 완료`);
          exitSelectMode();
          fetchDataRef.current();
        } else {
          toast.error("처리에 실패했습니다.");
        }
      } catch (err: unknown) {
        toast.error(err instanceof Error ? err.message : "처리 중 오류 발생");
      } finally {
        setSelectProcessing(false);
      }
      return;
    }

    if (btnConfig.clickMode === "modal-open" && btnConfig.modalScreenId) {
      const selectedRows = filteredRows.filter((r) => {
        const rowId = String(r.id ?? r.pk ?? "");
        return selectedRowIds.has(rowId);
      });
      openPopModal(btnConfig.modalScreenId, selectedRows[0] || {});
      return;
    }
  }, [selectedRowIds, filteredRows, exitSelectMode]);

  // status-bar 필터를 제외한 rows (카운트 집계용)
  // status-bar에서 "접수가능" 등 선택해도 전체 카운트가 유지되어야 함
  const rowsForStatusCount = useMemo(() => {
    const hasStatusBarFilter = [...externalFilters.values()].some((f) => f._source === "status-bar");
    if (!hasStatusBarFilter) return filteredRows;

    // status-bar 필터를 제외한 필터만 적용
    const nonStatusFilters = new Map(
      [...externalFilters.entries()].filter(([, f]) => f._source !== "status-bar")
    );
    if (nonStatusFilters.size === 0) return rows;

    const allFilters = [...nonStatusFilters.values()];
    const mainFilters = allFilters.filter((f) => !f.filterConfig?.isSubTable);
    const subFilters = allFilters.filter((f) => f.filterConfig?.isSubTable);

    const afterSubFilter = subFilters.length === 0
      ? rows
      : rows
          .map((row) => {
            const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
            if (!processFlow || processFlow.length === 0) return null;
            const matchingSteps = processFlow.filter((step) =>
              subFilters.every((filter) => {
                const searchValue = String(filter.value).toLowerCase();
                if (!searchValue) return true;
                const fc = filter.filterConfig;
                const col = fc?.targetColumn || filter.fieldName || "";
                if (!col) return true;
                const cellValue = String(step.rawData?.[col] ?? "").toLowerCase();
                const mode = fc?.filterMode || "contains";
                switch (mode) {
                  case "equals": return cellValue === searchValue;
                  case "starts_with": return cellValue.startsWith(searchValue);
                  default: return cellValue.includes(searchValue);
                }
              }),
            );
            if (matchingSteps.length === 0) return null;
            const matched = matchingSteps[0];
            const updatedFlow = processFlow.map((s) => ({
              ...s,
              isCurrent: s.seqNo === matched.seqNo,
            }));
            return {
              ...row,
              __processFlow__: updatedFlow,
              [VIRTUAL_SUB_STATUS]: matched.status,
              [VIRTUAL_SUB_SEMANTIC]: matched.semantic || "pending",
              [VIRTUAL_SUB_PROCESS]: matched.processName,
              [VIRTUAL_SUB_SEQ]: matched.seqNo,
            };
          })
          .filter((row): row is RowData => row !== null);

    if (mainFilters.length === 0) return afterSubFilter;

    return afterSubFilter.filter((row) =>
      mainFilters.every((filter) => {
        const searchValue = String(filter.value).toLowerCase();
        if (!searchValue) return true;
        const fc = filter.filterConfig;
        const columns: string[] =
          fc?.targetColumns?.length ? fc.targetColumns
            : fc?.targetColumn ? [fc.targetColumn]
              : filter.fieldName ? [filter.fieldName] : [];
        if (columns.length === 0) return true;
        const mode = fc?.filterMode || "contains";
        const subCol = subFilters.length > 0 ? VIRTUAL_SUB_STATUS : null;
        const statusCol = timelineSource?.statusColumn || "status";
        const effectiveColumns = subCol
          ? columns.map((col) => col === statusCol || col === "status" ? subCol : col)
          : columns;
        return effectiveColumns.some((col) => {
          const cellValue = String(row[col] ?? "").toLowerCase();
          switch (mode) {
            case "equals": return cellValue === searchValue;
            case "starts_with": return cellValue.startsWith(searchValue);
            default: return cellValue.includes(searchValue);
          }
        });
      }),
    );
  }, [rows, filteredRows, externalFilters, timelineSource]);

  // 카운트 집계용 rows 발행 (status-bar 필터 제외)
  useEffect(() => {
    if (!componentId || loading) return;
    publish(`__comp_output__${componentId}__all_rows`, {
      rows: rowsForStatusCount,
      subStatusColumn: hasActiveSubFilter ? VIRTUAL_SUB_STATUS : null,
    });
  }, [componentId, rowsForStatusCount, loading, publish, hasActiveSubFilter]);

  const overflowCfg = effectiveConfig?.overflow;
  const baseVisibleCount = overflowCfg?.visibleCount ?? gridColumns * gridRows;
  const visibleCardCount = useMemo(() => Math.max(1, baseVisibleCount), [baseVisibleCount]);
  const hasMoreCards = filteredRows.length > visibleCardCount;
  const expandedCardsPerPage = useMemo(() => {
    if (overflowCfg?.mode === "pagination" && overflowCfg.pageSize) return overflowCfg.pageSize;
    if (overflowCfg?.mode === "loadMore" && overflowCfg.loadMoreCount) return overflowCfg.loadMoreCount + visibleCardCount;
    return Math.max(1, visibleCardCount * 2 + gridColumns);
  }, [visibleCardCount, gridColumns, overflowCfg]);

  const scrollAreaRef = useRef<HTMLDivElement>(null);

  const ownerSortColumn = config?.ownerSortColumn;

  const displayCards = useMemo(() => {
    let source = filteredRows;

    if (ownerSortColumn && currentUserId) {
      const mine: RowData[] = [];
      const others: RowData[] = [];
      for (const row of source) {
        if (String(row[ownerSortColumn] ?? "") === currentUserId) {
          mine.push(row);
        } else {
          others.push(row);
        }
      }
      source = [...mine, ...others];
    }

    if (!isExpanded) return source.slice(0, visibleCardCount);
    const start = (currentPage - 1) * expandedCardsPerPage;
    return source.slice(start, start + expandedCardsPerPage);
  }, [filteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage, ownerSortColumn, currentUserId]);

  const totalPages = isExpanded ? Math.ceil(filteredRows.length / expandedCardsPerPage) : 1;
  const needsPagination = isExpanded && totalPages > 1;

  const toggleExpand = () => {
    if (isExpanded) {
      if (!isHorizontalMode && originalRowSpan !== null && componentId && onRequestResize) {
        onRequestResize(componentId, originalRowSpan);
      }
      setCurrentPage(1);
      setOriginalRowSpan(null);
      baseContainerHeight.current = 0;
      setIsExpanded(false);
    } else {
      baseContainerHeight.current = containerHeight;
      if (!isHorizontalMode && componentId && onRequestResize && currentRowSpan !== undefined) {
        setOriginalRowSpan(currentRowSpan);
        onRequestResize(componentId, currentRowSpan * 2);
      }
      setIsExpanded(true);
    }
  };

  useEffect(() => {
    if (scrollAreaRef.current && isExpanded) {
      scrollAreaRef.current.scrollTop = 0;
      scrollAreaRef.current.scrollLeft = 0;
    }
  }, [currentPage, isExpanded]);

  // 데이터 조회
  const dataSourceKey = useMemo(() => JSON.stringify(dataSource || null), [dataSource]);
  const cartListModeKey = useMemo(() => JSON.stringify(config?.cartListMode || null), [config?.cartListMode]);

  // 하위 데이터 조회 + __processFlow__ 가상 컬럼 주입
  const injectProcessFlow = useCallback(async (
    fetchedRows: RowData[],
    src: TimelineDataSource,
  ): Promise<RowData[]> => {
    if (fetchedRows.length === 0) return fetchedRows;
    const rowIds = fetchedRows.map((r) => String(r.id)).filter(Boolean);
    if (rowIds.length === 0) return fetchedRows;

    // statusMappings 동적 배열 → dbValue-to-내부키 맵 구축
    // 레거시 statusValues 객체도 자동 변환
    const mappings = resolveStatusMappings(src);
    const dbToInternal = new Map<string, string>();
    const dbToSemantic = new Map<string, string>();
    for (const m of mappings) {
      dbToInternal.set(m.dbValue, m.dbValue);
      dbToSemantic.set(m.dbValue, m.semantic);
    }

    const processResult = await dataApi.getTableData(src.processTable, {
      page: 1,
      size: 1000,
      sortBy: src.seqColumn || "seq_no",
      sortOrder: "asc",
    });
    const allProcesses = processResult.data || [];

    // isDerived 매핑: DB에 없는 자동 판별 상태
    // 같은 시맨틱의 DB 원본 상태를 자동으로 찾아 변환 조건 구축
    const derivedRules: { sourceStatus: string; targetDbValue: string; targetSemantic: string }[] = [];
    for (const dm of mappings.filter((m) => m.isDerived)) {
      const source = mappings.find((m) => !m.isDerived && m.semantic === dm.semantic);
      if (source) {
        derivedRules.push({ sourceStatus: source.dbValue, targetDbValue: dm.dbValue, targetSemantic: dm.semantic });
      }
    }

    const processMap = new Map<string, TimelineProcessStep[]>();
    for (const p of allProcesses) {
      const fkValue = String(p[src.foreignKey] || "");
      if (!fkValue || !rowIds.includes(fkValue)) continue;
      if (!processMap.has(fkValue)) processMap.set(fkValue, []);

      const rawStatus = String(p[src.statusColumn] || "");
      const normalizedStatus = dbToInternal.get(rawStatus) || rawStatus;
      const semantic = dbToSemantic.get(rawStatus) || "pending";

      processMap.get(fkValue)!.push({
        seqNo: parseInt(String(p[src.seqColumn] || "0"), 10),
        processName: String(p[src.nameColumn] || ""),
        status: normalizedStatus,
        semantic: semantic as "pending" | "active" | "done",
        isCurrent: semantic === "active",
        processId: p.id as string | number | undefined,
        rawData: p as Record<string, unknown>,
      });
    }

    // 파생 상태 자동 변환: 이전 공정이 완료된 경우 변환
    if (derivedRules.length > 0) {
      for (const [, steps] of processMap) {
        steps.sort((a, b) => a.seqNo - b.seqNo);
        for (let i = 0; i < steps.length; i++) {
          const step = steps[i];
          const prevStep = i > 0 ? steps[i - 1] : null;
          for (const rule of derivedRules) {
            if (step.status !== rule.sourceStatus) continue;
            const prevIsDone = prevStep ? prevStep.semantic === "done" : true;
            if (prevIsDone) {
              step.status = rule.targetDbValue;
              step.semantic = rule.targetSemantic as "pending" | "active" | "done";
            }
          }
        }
      }
    }

    // isCurrent 결정: "기준" 체크된 상태와 일치하는 공정을 강조
    // 기준 상태가 없으면 기존 로직 (active → 첫 pending) 폴백
    const pivotDbValues = mappings.filter((m) => m.isDerived).map((m) => m.dbValue);
    for (const [, steps] of processMap) {
      steps.sort((a, b) => a.seqNo - b.seqNo);
      steps.forEach((s) => { s.isCurrent = false; });

      if (pivotDbValues.length > 0) {
        const pivotStep = steps.find((s) => pivotDbValues.includes(s.status));
        if (pivotStep) {
          pivotStep.isCurrent = true;
          continue;
        }
      }
      // 폴백: active가 있으면 첫 active, 없으면 첫 pending
      const firstActive = steps.find((s) => s.semantic === "active");
      if (firstActive) { firstActive.isCurrent = true; continue; }
      const firstPending = steps.find((s) => s.semantic === "pending");
      if (firstPending) { firstPending.isCurrent = true; }
    }

    return fetchedRows.map((row) => {
      const steps = processMap.get(String(row.id)) || [];
      const current = steps.find((s) => s.isCurrent);
      const processFields: Record<string, unknown> = {};
      if (current?.rawData) {
        for (const [key, val] of Object.entries(current.rawData)) {
          processFields[`__process_${key}`] = val;
        }
      }
      return { ...row, __processFlow__: steps, ...processFields };
    });
  }, []);

  const fetchData = useCallback(async () => {
    if (!dataSource?.tableName) { setLoading(false); setRows([]); return; }

    setLoading(true);
    setError(null);
    try {
      const filters: Record<string, unknown> = {};
      if (dataSource.filters?.length) {
        dataSource.filters.forEach((f) => {
          if (f.column && f.value && (!f.operator || f.operator === "=")) filters[f.column] = f.value;
        });
      }
      const sortArray = Array.isArray(dataSource.sort)
        ? dataSource.sort
        : dataSource.sort && typeof dataSource.sort === "object"
          ? [dataSource.sort as { column: string; direction: "asc" | "desc" }]
          : [];
      const primarySort = sortArray[0];
      const size = dataSource.limit?.mode === "limited" && dataSource.limit?.count ? dataSource.limit.count : 100;

      const result = await dataApi.getTableData(dataSource.tableName, {
        page: 1,
        size,
        sortBy: primarySort?.column || undefined,
        sortOrder: primarySort?.direction,
        filters: Object.keys(filters).length > 0 ? filters : undefined,
      });

      let fetchedRows = result.data || [];
      const clientFilters = (dataSource.filters || []).filter(
        (f) => f.column && f.value && f.operator && f.operator !== "=",
      );
      if (clientFilters.length > 0) {
        fetchedRows = fetchedRows.filter((row) =>
          clientFilters.every((f) => {
            const cellVal = row[f.column];
            const filterVal = f.value;
            switch (f.operator) {
              case "!=": return String(cellVal ?? "") !== filterVal;
              case ">":  return Number(cellVal) > Number(filterVal);
              case ">=": return Number(cellVal) >= Number(filterVal);
              case "<":  return Number(cellVal) < Number(filterVal);
              case "<=": return Number(cellVal) <= Number(filterVal);
              case "like": return String(cellVal ?? "").toLowerCase().includes(filterVal.toLowerCase());
              default: return true;
            }
          }),
        );
      }

      // timelineSource 설정이 있으면 공정 데이터 조회하여 __processFlow__ 주입
      if (timelineSource) {
        try {
          fetchedRows = await injectProcessFlow(fetchedRows, timelineSource);
        } catch {
          // 공정 데이터 조회 실패 시 무시 (메인 데이터는 정상 표시)
        }
      }

      setRows(fetchedRows);
    } catch (err) {
      setError(err instanceof Error ? err.message : "데이터 조회 실패");
      setRows([]);
    } finally { setLoading(false); }
  }, [dataSource, timelineSource, injectProcessFlow]);

  const fetchDataRef = useRef(fetchData);
  fetchDataRef.current = fetchData;

  useEffect(() => {
    if (isCartListMode) {
      const cartListMode = config!.cartListMode!;
      if (!cartListMode.sourceScreenId) { setLoading(false); setRows([]); return; }

      const fetchCartData = async () => {
        setLoading(true);
        setError(null);
        try {
          try {
            const layoutJson = await screenApi.getLayoutPop(cartListMode.sourceScreenId!);
            const componentsMap = layoutJson?.components || {};
            const componentList = Object.values(componentsMap) as any[];
            const matched = cartListMode.sourceComponentId
              ? componentList.find((c: any) => c.id === cartListMode.sourceComponentId)
              : componentList.find((c: any) => c.type === "pop-card-list-v2" || c.type === "pop-card-list");
            if (matched?.config) setInheritedConfig(matched.config);
          } catch { /* 레이아웃 로드 실패 시 자체 config 폴백 */ }

          const cartFilters: Record<string, unknown> = { status: cartListMode.statusFilter || "in_cart" };
          if (cartListMode.sourceScreenId) cartFilters.screen_id = String(cartListMode.sourceScreenId);
          const result = await dataApi.getTableData("cart_items", { size: 500, filters: cartFilters });
          setRows((result.data || []).map(parseCartRow));
        } catch (err) {
          setError(err instanceof Error ? err.message : "장바구니 데이터 조회 실패");
          setRows([]);
        } finally { setLoading(false); }
      };
      fetchCartData();
      return;
    }

    fetchData();
  }, [dataSourceKey, isCartListMode, cartListModeKey, fetchData]); // eslint-disable-line react-hooks/exhaustive-deps

  // 장바구니 목록 모드 콜백
  const handleDeleteItem = useCallback((cartId: string) => {
    setRows((prev) => prev.filter((r) => String(r.__cart_id) !== cartId));
    setSelectedKeys((prev) => { const next = new Set(prev); next.delete(cartId); return next; });
  }, []);

  const handleUpdateQuantity = useCallback((cartId: string, quantity: number, unit?: string, entries?: PackageEntry[]) => {
    setRows((prev) => prev.map((r) => {
      if (String(r.__cart_id) !== cartId) return r;
      return { ...r, __cart_quantity: quantity, __cart_package_unit: unit || r.__cart_package_unit, __cart_package_entries: entries || r.__cart_package_entries, __cart_modified: true };
    }));
  }, []);

  // 데이터 수집
  useEffect(() => {
    if (!componentId) return;
    const unsub = subscribe(
      `__comp_input__${componentId}__collect_data`,
      (payload: unknown) => {
        const request = (payload as Record<string, unknown>)?.value as CollectDataRequest | undefined;
        const selectedItems = isCartListMode
          ? filteredRows.filter((r) => selectedKeys.has(String(r.__cart_id ?? "")))
          : rows;
        const sm = config?.saveMapping;
        const mapping = sm?.targetTable && sm.mappings.length > 0
          ? { targetTable: sm.targetTable, columnMapping: Object.fromEntries(sm.mappings.filter((m) => m.sourceField && m.targetColumn).map((m) => [m.sourceField, m.targetColumn])) }
          : null;
        const cartChanges = cart.isDirty ? cart.getChanges() : undefined;
        const response: CollectedDataResponse = {
          requestId: request?.requestId ?? "",
          componentId: componentId,
          componentType: "pop-card-list-v2",
          data: { items: selectedItems, cartChanges } as any,
          mapping,
        };
        publish(`__comp_output__${componentId}__collected_data`, response);
      },
    );
    return unsub;
  }, [componentId, subscribe, publish, isCartListMode, filteredRows, rows, selectedKeys, cart]);

  // 선택 항목 이벤트
  useEffect(() => {
    if (!componentId || !isCartListMode) return;
    const selectedItems = filteredRows.filter((r) => selectedKeys.has(String(r.__cart_id ?? "")));
    publish(`__comp_output__${componentId}__selected_items`, selectedItems);
  }, [selectedKeys, filteredRows, componentId, isCartListMode, publish]);

  // 카드 영역 스타일
  const cardGap = effectiveConfig?.cardGap ?? spec.gap;
  const cardMinHeight = spec.height;
  const cardAreaStyle: React.CSSProperties = {
    gap: `${cardGap}px`,
    ...(isHorizontalMode
      ? {
          gridTemplateRows: `repeat(${gridRows}, minmax(${cardMinHeight}px, auto))`,
          gridAutoFlow: "column",
          gridAutoColumns: `${Math.round(cardMinHeight * 1.6)}px`,
        }
      : {
          gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
          gridAutoRows: `minmax(${cardMinHeight}px, auto)`,
        }),
  };

  const scrollClassName = isHorizontalMode
    ? "overflow-x-auto overflow-y-hidden"
    : isExpanded
      ? "overflow-y-auto overflow-x-hidden"
      : "overflow-hidden";

  return (
    <div ref={containerRef} className={`flex h-full w-full flex-col ${className || ""}`}>
      {isCartListMode && !config?.cartListMode?.sourceScreenId ? (
        <div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
          <p className="text-sm text-muted-foreground">원본 화면을 선택해주세요.</p>
        </div>
      ) : !isCartListMode && !dataSource?.tableName ? (
        <div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
          <p className="text-sm text-muted-foreground">데이터 소스를 설정해주세요.</p>
        </div>
      ) : effectiveConfig?.hideUntilFiltered && externalFilters.size === 0 ? (
        <div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
          <p className="text-sm text-muted-foreground">필터를 선택하면 데이터가 표시됩니다.</p>
        </div>
      ) : loading ? (
        <div className="flex flex-1 items-center justify-center rounded-md border bg-muted/30 p-4">
          <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
        </div>
      ) : error ? (
        <div className="flex flex-1 items-center justify-center rounded-md border border-destructive/50 bg-destructive/10 p-4">
          <p className="text-sm text-destructive">{error}</p>
        </div>
      ) : rows.length === 0 ? (
        <div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
          <p className="text-sm text-muted-foreground">데이터가 없습니다.</p>
        </div>
      ) : (
        <>
          {/* 선택 모드 상단 바 */}
          {selectMode && (
            <div className="flex shrink-0 items-center justify-between border-b bg-primary/5 px-3 py-2">
              <div className="flex items-center gap-2">
                <div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
                  <span className="text-xs font-bold">{selectedRowIds.size}</span>
                </div>
                <span className="text-sm font-medium">
                  {selectedRowIds.size > 0 ? `${selectedRowIds.size}개 선택됨` : "카드를 선택하세요"}
                </span>
              </div>
              <Button
                variant="ghost"
                size="sm"
                onClick={exitSelectMode}
                className="h-7 px-2 text-xs"
              >
                <X className="mr-1 h-3.5 w-3.5" />
                취소
              </Button>
            </div>
          )}

          {/* 장바구니 모드 상단 바 */}
          {!selectMode && isCartListMode && (
            <div className="flex shrink-0 items-center gap-3 border-b px-3 py-2">
              <input
                type="checkbox"
                checked={selectedKeys.size === filteredRows.length && filteredRows.length > 0}
                onChange={(e) => {
                  if (e.target.checked) {
                    setSelectedKeys(new Set(filteredRows.map((r) => String(r.__cart_id ?? ""))));
                  } else {
                    setSelectedKeys(new Set());
                  }
                }}
                className="h-4 w-4 rounded border-input"
              />
              <span className="text-sm text-muted-foreground">
                {selectedKeys.size > 0 ? `${selectedKeys.size}개 선택됨` : "전체 선택"}
              </span>
            </div>
          )}

          <div
            ref={scrollAreaRef}
            className={`min-h-0 flex-1 grid ${scrollClassName}`}
            style={{ ...cardAreaStyle, alignContent: "start", justifyContent: isHorizontalMode ? "start" : "center" }}
          >
            {displayCards.map((row, index) => {
              const locked = !!ownerSortColumn
                && !!String(row[ownerSortColumn] ?? "")
                && String(row[ownerSortColumn] ?? "") !== (currentUserId ?? "");
              return (
                <CardV2
                  key={`card-${index}`}
                  row={row}
                  cardGrid={cardGrid}
                  spec={spec}
                  config={effectiveConfig}
                  onSelect={handleCardSelect}
                  cart={cart}
                  publish={publish}
                  parentComponentId={componentId}
                  isCartListMode={isCartListMode}
                  isSelected={selectedKeys.has(String(row.__cart_id ?? ""))}
                  onToggleSelect={() => {
                    const cartId = row.__cart_id != null ? String(row.__cart_id) : "";
                    if (!cartId) return;
                    setSelectedKeys((prev) => { const next = new Set(prev); if (next.has(cartId)) next.delete(cartId); else next.add(cartId); return next; });
                  }}
                  onDeleteItem={handleDeleteItem}
                  onUpdateQuantity={handleUpdateQuantity}
                  onRefresh={fetchData}
                  selectMode={selectMode}
                  isSelectModeSelected={selectedRowIds.has(String(row.id ?? row.pk ?? ""))}
                  isSelectable={isRowSelectable(row)}
                  onToggleRowSelect={() => toggleRowSelection(row)}
                  onEnterSelectMode={enterSelectMode}
                  onOpenPopModal={openPopModal}
                  currentUserId={currentUserId}
                  isLockedByOther={locked}
                />
              );
            })}
          </div>

          {/* 선택 모드 하단 액션 바 */}
          {selectMode && selectModeConfig && (
            <div className="shrink-0 border-t bg-background px-3 py-2.5 shadow-[0_-2px_8px_rgba(0,0,0,0.08)]">
              <div className="flex items-center justify-end gap-2">
                {selectModeConfig.buttons.map((btn, idx) => (
                  <Button
                    key={idx}
                    variant={btn.variant || (btn.clickMode === "cancel-select" ? "outline" : "default")}
                    size="sm"
                    className="h-10 min-w-[80px] px-4 text-sm font-medium"
                    disabled={selectProcessing || (btn.clickMode !== "cancel-select" && selectedRowIds.size === 0)}
                    onClick={() => handleSelectModeAction(btn)}
                  >
                    {selectProcessing && btn.clickMode !== "cancel-select" && (
                      <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
                    )}
                    {btn.label}
                  </Button>
                ))}
              </div>
            </div>
          )}

          {/* 더보기/페이지네이션 */}
          {!selectMode && hasMoreCards && (
            <div className="shrink-0 border-t bg-background px-3 py-2">
              <div className="flex items-center justify-between gap-2">
                <div className="flex items-center gap-2">
                  <Button variant="outline" size="sm" onClick={toggleExpand} className="h-9 px-4 text-sm font-medium">
                    {isExpanded ? (<>접기 <ChevronUp className="ml-1 h-4 w-4" /></>) : (<>더보기 <ChevronDown className="ml-1 h-4 w-4" /></>)}
                  </Button>
                  <span className="text-xs text-muted-foreground">
                    {filteredRows.length}건{externalFilters.size > 0 && filteredRows.length !== rows.length ? ` / ${rows.length}건` : ""}
                  </span>
                </div>
                {isExpanded && needsPagination && (
                  <div className="flex items-center gap-1.5">
                    <Button variant="default" size="sm" onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={currentPage <= 1} className="h-9 w-9 p-0">
                      <ChevronLeft className="h-5 w-5" />
                    </Button>
                    <span className="min-w-[48px] text-center text-sm font-medium">{currentPage} / {totalPages}</span>
                    <Button variant="default" size="sm" onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={currentPage >= totalPages} className="h-9 w-9 p-0">
                      <ChevronRight className="h-5 w-5" />
                    </Button>
                  </div>
                )}
              </div>
            </div>
          )}
        </>
      )}

      {/* POP 화면 모달 (풀스크린) */}
      <Dialog open={popModalOpen} onOpenChange={(open) => {
        setPopModalOpen(open);
        if (!open) {
          setPopModalLayout(null);
          setPopModalRow(null);
        }
      }}>
        <DialogContent className="flex h-dvh w-screen max-w-none flex-col gap-0 rounded-none border-none p-0 [&>button]:z-50">
          <DialogHeader className="flex shrink-0 flex-row items-center justify-between border-b px-4 py-2">
            <DialogTitle className="text-base">{effectiveConfig?.cardClickModalConfig?.modalTitle || "상세 작업"}</DialogTitle>
          </DialogHeader>
          <div className="flex-1 overflow-auto">
            {popModalLayout && (
              <PopViewerWithModals
                layout={popModalLayout}
                viewportWidth={typeof window !== "undefined" ? window.innerWidth : 1024}
                screenId={popModalScreenId}
                currentMode={detectGridMode(typeof window !== "undefined" ? window.innerWidth : 1024)}
              />
            )}
          </div>
        </DialogContent>
      </Dialog>
    </div>
  );
}

// ===== 카드 V2 =====

interface CardV2Props {
  row: RowData;
  cardGrid?: CardGridConfigV2;
  spec: CardPresetSpec;
  config?: PopCardListV2Config;
  onSelect?: (row: RowData) => void;
  cart: ReturnType<typeof useCartSync>;
  publish: (eventName: string, payload?: unknown) => void;
  parentComponentId?: string;
  isCartListMode?: boolean;
  isSelected?: boolean;
  onToggleSelect?: () => void;
  onDeleteItem?: (cartId: string) => void;
  onUpdateQuantity?: (cartId: string, quantity: number, unit?: string, entries?: PackageEntry[]) => void;
  onRefresh?: () => void;
  selectMode?: boolean;
  isSelectModeSelected?: boolean;
  isSelectable?: boolean;
  onToggleRowSelect?: () => void;
  onEnterSelectMode?: (whenStatus: string, buttonConfig: Record<string, unknown>) => void;
  onOpenPopModal?: (screenId: string, row: RowData) => void;
  currentUserId?: string;
  isLockedByOther?: boolean;
}

function CardV2({
  row, cardGrid, spec, config, onSelect, cart, publish,
  parentComponentId, isCartListMode, isSelected, onToggleSelect,
  onDeleteItem, onUpdateQuantity, onRefresh,
  selectMode, isSelectModeSelected, isSelectable, onToggleRowSelect, onEnterSelectMode,
  onOpenPopModal, currentUserId, isLockedByOther,
}: CardV2Props) {
  const inputField = config?.inputField;
  const cartAction = config?.cartAction;
  const packageConfig = config?.packageConfig;
  const keyColumnName = cartAction?.keyColumn || "id";

  const [inputValue, setInputValue] = useState<number>(0);
  const [packageUnit, setPackageUnit] = useState<string | undefined>(undefined);
  const [packageEntries, setPackageEntries] = useState<PackageEntry[]>([]);
  const [isModalOpen, setIsModalOpen] = useState(false);

  const [qtyModalState, setQtyModalState] = useState<{
    open: boolean;
    row: RowData;
    processId?: string | number;
    action: ActionButtonClickAction;
  } | null>(null);

  const handleQtyConfirm = useCallback(async (value: number) => {
    if (!qtyModalState) return;
    const { row: actionRow, processId: qtyProcessId, action } = qtyModalState;
    setQtyModalState(null);
    if (!action.targetTable || !action.updates) return;

    const rowId = qtyProcessId ?? actionRow.id ?? actionRow.pk;
    if (!rowId) { toast.error("대상 레코드 ID를 찾을 수 없습니다."); return; }

    const lookupValue = action.joinConfig
      ? String(actionRow[action.joinConfig.sourceColumn] ?? rowId)
      : rowId;
    const lookupColumn = action.joinConfig?.targetColumn || "id";

    const tasks = action.updates.map((u, idx) => ({
      id: `qty-update-${idx}`,
      type: "data-update" as const,
      targetTable: action.targetTable!,
      targetColumn: u.column,
      operationType: (u.operationType || "assign") as "assign" | "add" | "subtract",
      valueSource: "fixed" as const,
      fixedValue: u.valueType === "userInput" ? String(value) :
                  u.valueType === "static" ? (u.value ?? "") :
                  u.valueType === "currentUser" ? "__CURRENT_USER__" :
                  u.valueType === "currentTime" ? "__CURRENT_TIME__" :
                  u.valueType === "columnRef" ? String(actionRow[u.value ?? ""] ?? "") :
                  (u.value ?? ""),
      lookupMode: "manual" as const,
      manualItemField: lookupColumn,
      manualPkColumn: lookupColumn,
      ...(idx === 0 && action.preCondition ? { preCondition: action.preCondition } : {}),
    }));

    const targetRow = action.joinConfig
      ? { ...actionRow, [lookupColumn]: lookupValue }
      : qtyProcessId ? { ...actionRow, id: qtyProcessId } : actionRow;

    try {
      const result = await apiClient.post("/pop/execute-action", {
        tasks,
        data: { items: [targetRow], fieldValues: {} },
        mappings: {},
      });
      if (result.data?.success) {
        toast.success(result.data.message || "처리 완료");
        onRefresh?.();
      } else {
        toast.error(result.data?.message || "처리 실패");
      }
    } catch (err: unknown) {
      if ((err as any)?.response?.status === 409) {
        toast.error((err as any).response?.data?.message || "이미 다른 사용자가 처리한 작업입니다.");
        onRefresh?.();
      } else {
        toast.error(err instanceof Error ? err.message : "처리 중 오류 발생");
      }
    }
  }, [qtyModalState, onRefresh]);

  const rowKey = keyColumnName && row[keyColumnName] ? String(row[keyColumnName]) : "";
  const isCarted = cart.isItemInCart(rowKey);
  const existingCartItem = cart.getCartItem(rowKey);

  // DB 장바구니 복원
  useEffect(() => {
    if (isCartListMode) return;
    if (existingCartItem && existingCartItem._origin === "db") {
      setInputValue(existingCartItem.quantity);
      setPackageUnit(existingCartItem.packageUnit);
      setPackageEntries(existingCartItem.packageEntries || []);
    }
  }, [isCartListMode, existingCartItem?._origin, existingCartItem?.quantity, existingCartItem?.packageUnit]);

  // 장바구니 목록 모드 초기값
  useEffect(() => {
    if (!isCartListMode) return;
    setInputValue(Number(row.__cart_quantity) || 0);
    setPackageUnit(row.__cart_package_unit ? String(row.__cart_package_unit) : undefined);
  }, [isCartListMode, row.__cart_quantity, row.__cart_package_unit]);

  // 제한 컬럼 자동 초기화
  const limitCol = inputField?.limitColumn || inputField?.maxColumn;
  const effectiveMax = useMemo(() => {
    if (limitCol) { const v = Number(row[limitCol]); if (!isNaN(v) && v > 0) return v; }
    return 999999;
  }, [limitCol, row]);

  useEffect(() => {
    if (isCartListMode) return;
    if (inputField?.enabled && limitCol && effectiveMax > 0 && effectiveMax < 999999) {
      setInputValue(effectiveMax);
    }
  }, [effectiveMax, inputField?.enabled, limitCol, isCartListMode]);

  const handleInputClick = (e: React.MouseEvent) => { e.stopPropagation(); setIsModalOpen(true); };
  const handleInputConfirm = (value: number, unit?: string, entries?: PackageEntry[]) => {
    setInputValue(value);
    setPackageUnit(unit);
    setPackageEntries(entries || []);
    if (isCartListMode) onUpdateQuantity?.(String(row.__cart_id), value, unit, entries);
  };

  const handleCartAdd = () => {
    if (!rowKey) return;
    cart.addItem({ row, quantity: inputValue, packageUnit, packageEntries: packageEntries.length > 0 ? packageEntries : undefined }, rowKey);
    if (parentComponentId) publish(`__comp_output__${parentComponentId}__cart_updated`, { count: cart.cartCount + 1, isDirty: true });
  };

  const handleCartCancel = () => {
    if (!rowKey) return;
    cart.removeItem(rowKey);
    if (parentComponentId) publish(`__comp_output__${parentComponentId}__cart_updated`, { count: Math.max(0, cart.cartCount - 1), isDirty: true });
  };

  const handleCartDelete = async (e: React.MouseEvent) => {
    e.stopPropagation();
    const cartId = row.__cart_id != null ? String(row.__cart_id) : "";
    if (!cartId) return;
    if (!window.confirm("이 항목을 장바구니에서 삭제하시겠습니까?")) return;
    try {
      await dataApi.updateRecord("cart_items", cartId, { status: "cancelled" });
      onDeleteItem?.(cartId);
    } catch { toast.error("삭제에 실패했습니다."); }
  };

  const borderClass = selectMode
    ? isSelectModeSelected
      ? "border-primary border-2 bg-primary/5"
      : isSelectable
        ? "hover:border-2 hover:border-primary/50"
        : "opacity-40 pointer-events-none"
    : isCartListMode
      ? isSelected ? "border-primary border-2 hover:border-primary/80" : "hover:border-2 hover:border-blue-500"
      : isCarted ? "border-emerald-500 border-2 hover:border-emerald-600" : "hover:border-2 hover:border-blue-500";

  if (!cardGrid || cardGrid.cells.length === 0) {
    return (
      <div className={`flex items-center justify-center rounded-lg border p-4 ${borderClass}`} style={{ minHeight: `${spec.height}px` }}>
        <span className="text-xs text-muted-foreground">카드 레이아웃을 설정하세요</span>
      </div>
    );
  }

  const gridStyle: React.CSSProperties = {
    display: "grid",
    gridTemplateColumns: cardGrid.colWidths.length > 0
      ? cardGrid.colWidths.map((w) => `minmax(30px, ${w || "1fr"})`).join(" ")
      : "1fr",
    gridTemplateRows: cardGrid.rowHeights?.length
      ? cardGrid.rowHeights.map((h) => {
          if (!h) return "minmax(24px, auto)";
          if (h.endsWith("px")) return `minmax(${h}, auto)`;
          const px = Math.round(parseFloat(h) * 24) || 24;
          return `minmax(${px}px, auto)`;
        }).join(" ")
      : `repeat(${cardGrid.rows || 1}, minmax(24px, auto))`,
    gap: `${cardGrid.gap || 0}px`,
  };

  const justifyMap = { left: "flex-start", center: "center", right: "flex-end" } as const;
  const alignItemsMap = { top: "flex-start", middle: "center", bottom: "flex-end" } as const;

  return (
    <div
      className={cn(
        "relative flex flex-col rounded-lg border bg-card shadow-sm transition-all duration-150",
        isLockedByOther
          ? "cursor-not-allowed opacity-50"
          : "cursor-pointer hover:shadow-md",
        borderClass,
      )}
      style={{ minHeight: `${spec.height}px` }}
      onClick={() => {
        if (isLockedByOther) return;
        if (selectMode && isSelectable) { onToggleRowSelect?.(); return; }
        if (!selectMode) onSelect?.(row);
      }}
      role="button"
      tabIndex={isLockedByOther ? -1 : 0}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") {
          if (isLockedByOther) return;
          if (selectMode && isSelectable) { onToggleRowSelect?.(); return; }
          if (!selectMode) onSelect?.(row);
        }
      }}
    >
      {/* 선택 모드: 체크 인디케이터 */}
      {selectMode && isSelectable && (
        <div className="absolute left-1.5 top-1.5 z-10">
          <div
            className={cn(
              "flex h-6 w-6 items-center justify-center rounded-full border-2 transition-colors duration-150",
              isSelectModeSelected
                ? "border-primary bg-primary text-primary-foreground"
                : "border-muted-foreground/40 bg-background"
            )}
            onClick={(e) => { e.stopPropagation(); onToggleRowSelect?.(); }}
          >
            {isSelectModeSelected && <Check className="h-3.5 w-3.5" />}
          </div>
        </div>
      )}

      {/* 장바구니 목록 모드: 체크박스 + 삭제 */}
      {!selectMode && isCartListMode && (
        <div className="absolute right-1 top-1 z-10 flex items-center gap-1">
          <input
            type="checkbox"
            checked={isSelected}
            onChange={(e) => { e.stopPropagation(); onToggleSelect?.(); }}
            onClick={(e) => e.stopPropagation()}
            className="h-4 w-4 rounded border-input"
          />
          <button type="button" onClick={handleCartDelete} className="rounded p-0.5 hover:bg-destructive/10">
            <Trash2 className="h-3.5 w-3.5 text-destructive" />
          </button>
        </div>
      )}

      {/* CSS Grid 기반 셀 렌더링 */}
      <div className="flex-1 overflow-hidden p-1" style={gridStyle}>
        {cardGrid.cells.map((cell) => (
          <div
            key={cell.id}
            className="overflow-hidden p-1"
            style={{
              display: "flex",
              flexDirection: "column",
              justifyContent: alignItemsMap[cell.verticalAlign || "top"],
              alignItems: justifyMap[cell.align || "left"],
              gridColumn: `${cell.col} / span ${cell.colSpan || 1}`,
              gridRow: `${cell.row} / span ${cell.rowSpan || 1}`,
              border: cardGrid.showCellBorder ? "1px solid hsl(var(--border))" : "none",
            }}
          >
            {renderCellV2({
              cell,
              row,
              inputValue,
              isCarted,
              onInputClick: handleInputClick,
              onCartAdd: handleCartAdd,
              onCartCancel: handleCartCancel,
              onEnterSelectMode,
              onActionButtonClick: async (taskPreset, actionRow, buttonConfig) => {
                const cfg = buttonConfig as Record<string, unknown> | undefined;
                const allActions = (cfg?.__allActions as ActionButtonClickAction[] | undefined) || [];
                const processId = cfg?.__processId as string | number | undefined;

                // 단일 액션 폴백 (기존 구조 호환)
                const actionsToRun = allActions.length > 0
                  ? allActions
                  : cfg?.type
                    ? [cfg as unknown as ActionButtonClickAction]
                    : [];

                if (actionsToRun.length === 0) {
                  if (parentComponentId) {
                    publish(`__comp_output__${parentComponentId}__action`, { taskPreset, row: actionRow });
                  }
                  return;
                }

                for (const action of actionsToRun) {
                  if (action.type === "quantity-input" && action.targetTable && action.updates) {
                    if (action.confirmMessage && !window.confirm(action.confirmMessage)) return;
                    setQtyModalState({ open: true, row: actionRow, processId, action });
                    return;
                  } else if (action.type === "immediate" && action.updates && action.updates.length > 0 && action.targetTable) {
                    if (action.confirmMessage) {
                      if (!window.confirm(action.confirmMessage)) return;
                    }
                    try {
                      const rowId = processId ?? actionRow.id ?? actionRow.pk;
                      if (!rowId) { toast.error("대상 레코드의 ID를 찾을 수 없습니다."); return; }
                      const lookupValue = action.joinConfig
                        ? String(actionRow[action.joinConfig.sourceColumn] ?? rowId)
                        : rowId;
                      const lookupColumn = action.joinConfig?.targetColumn || "id";
                      const tasks = action.updates.map((u, idx) => ({
                        id: `btn-update-${idx}`,
                        type: "data-update" as const,
                        targetTable: action.targetTable!,
                        targetColumn: u.column,
                        operationType: (u.operationType || "assign") as "assign" | "add" | "subtract",
                        valueSource: "fixed" as const,
                        fixedValue: u.valueType === "static" ? (u.value ?? "") :
                                    u.valueType === "currentUser" ? "__CURRENT_USER__" :
                                    u.valueType === "currentTime" ? "__CURRENT_TIME__" :
                                    u.valueType === "columnRef" ? String(actionRow[u.value ?? ""] ?? "") :
                                    (u.value ?? ""),
                        lookupMode: "manual" as const,
                        manualItemField: lookupColumn,
                        manualPkColumn: lookupColumn,
                        ...(idx === 0 && action.preCondition ? { preCondition: action.preCondition } : {}),
                      }));
                      const targetRow = action.joinConfig
                        ? { ...actionRow, [lookupColumn]: lookupValue }
                        : processId ? { ...actionRow, id: processId } : actionRow;
                      const result = await apiClient.post("/pop/execute-action", {
                        tasks,
                        data: { items: [targetRow], fieldValues: {} },
                        mappings: {},
                      });
                      if (result.data?.success) {
                        toast.success(result.data.message || "처리 완료");
                        onRefresh?.();
                      } else {
                        toast.error(result.data?.message || "처리 실패");
                        return;
                      }
                    } catch (err: unknown) {
                      if ((err as any)?.response?.status === 409) {
                        toast.error((err as any).response?.data?.message || "이미 다른 사용자가 처리한 작업입니다.");
                        onRefresh?.();
                      } else {
                        toast.error(err instanceof Error ? err.message : "처리 중 오류 발생");
                      }
                      return;
                    }
                  } else if (action.type === "modal-open" && action.modalScreenId) {
                    onOpenPopModal?.(action.modalScreenId, actionRow);
                  }
                }
              },
              packageEntries,
              inputUnit: inputField?.unit,
              currentUserId,
            })}
          </div>
        ))}
      </div>

      {inputField?.enabled && (
        <NumberInputModal
          open={isModalOpen}
          onOpenChange={setIsModalOpen}
          unit={inputField.unit || "EA"}
          initialValue={inputValue}
          initialPackageUnit={packageUnit}
          maxValue={effectiveMax}
          packageConfig={packageConfig}
          showPackageUnit={inputField.showPackageUnit}
          onConfirm={handleInputConfirm}
        />
      )}

      {qtyModalState?.open && (
        <NumberInputModal
          open={true}
          onOpenChange={(open) => { if (!open) setQtyModalState(null); }}
          unit={qtyModalState.action.quantityInput?.unit || "EA"}
          maxValue={calculateMaxQty(qtyModalState.row, qtyModalState.processId, qtyModalState.action.quantityInput)}
          showPackageUnit={qtyModalState.action.quantityInput?.enablePackage ?? false}
          onConfirm={(value) => handleQtyConfirm(value)}
        />
      )}

    </div>
  );
}
