"use client";

/**
 * pop-card-list 런타임 컴포넌트
 *
 * 테이블의 각 행이 하나의 카드로 표시됩니다.
 * 카드 구조:
 * - 헤더: 코드 + 제목
 * - 본문: 이미지(왼쪽) + 라벨-값 목록(오른쪽)
 */

import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import {
  Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight,
  ShoppingCart, X, Package, Truck, Box, Archive, Heart, Star,
  Trash2,
  type LucideIcon,
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import type {
  PopCardListConfig,
  CardTemplateConfig,
  CardFieldBinding,
  CardInputFieldConfig,
  CardCartActionConfig,
  CardPackageConfig,
  CardPresetSpec,
  CartItem,
  PackageEntry,
  CollectDataRequest,
  CollectedDataResponse,
} from "../types";
import { 
  DEFAULT_CARD_IMAGE,
  CARD_PRESET_SPECS,
} from "../types";
import { dataApi } from "@/lib/api/data";
import { screenApi } from "@/lib/api/screen";
import { usePopEvent } from "@/hooks/pop/usePopEvent";
import { useCartSync } from "@/hooks/pop/useCartSync";
import { NumberInputModal } from "./NumberInputModal";

const LUCIDE_ICON_MAP: Record<string, LucideIcon> = {
  ShoppingCart, Package, Truck, Box, Archive, Heart, Star,
};

function DynamicLucideIcon({ name, size = 20 }: { name?: string; size?: number }) {
  if (!name) return <ShoppingCart size={size} />;
  const IconComp = LUCIDE_ICON_MAP[name];
  if (!IconComp) return <ShoppingCart size={size} />;
  return <IconComp size={size} />;
}

// 마퀴 애니메이션 keyframes (한 번만 삽입)
const MARQUEE_STYLE_ID = "pop-card-marquee-style";
if (typeof document !== "undefined" && !document.getElementById(MARQUEE_STYLE_ID)) {
  const style = document.createElement("style");
  style.id = MARQUEE_STYLE_ID;
  style.textContent = `
    @keyframes pop-marquee {
      0%, 15% { transform: translateX(0); }
      85%, 100% { transform: translateX(var(--marquee-offset)); }
    }
  `;
  document.head.appendChild(style);
}

// 텍스트가 컨테이너보다 넓을 때 자동 슬라이딩하는 컴포넌트
function MarqueeText({
  children,
  className,
  style,
}: {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const textRef = useRef<HTMLSpanElement>(null);
  const [overflowPx, setOverflowPx] = useState(0);

  const measure = useCallback(() => {
    const container = containerRef.current;
    const text = textRef.current;
    if (!container || !text) return;
    const diff = text.scrollWidth - container.clientWidth;
    setOverflowPx(diff > 1 ? diff : 0);
  }, []);

  useEffect(() => {
    measure();
  }, [children, measure]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const ro = new ResizeObserver(() => measure());
    ro.observe(container);
    return () => ro.disconnect();
  }, [measure]);

  return (
    <div
      ref={containerRef}
      className={`overflow-hidden ${className || ""}`}
      style={style}
    >
      <span
        ref={textRef}
        className="inline-block whitespace-nowrap"
        style={
          overflowPx > 0
            ? {
                ["--marquee-offset" as string]: `-${overflowPx}px`,
                animation: "pop-marquee 5s ease-in-out infinite alternate",
              }
            : undefined
        }
      >
        {children}
      </span>
    </div>
  );
}

// cart_items 행의 row_data JSON을 풀어서 __cart_ 접두사 메타데이터와 병합
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,
  };
}

interface PopCardListComponentProps {
  config?: PopCardListConfig;
  className?: string;
  screenId?: string;
  // 동적 크기 변경을 위한 props (PopRenderer에서 전달)
  componentId?: string;
  currentRowSpan?: number;
  currentColSpan?: number;
  onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
}

// 테이블 행 데이터 타입
type RowData = Record<string, unknown>;

// 카드 내부 스타일 규격 (프리셋에서 매핑)
interface ScaledConfig {
  cardHeight: number;
  cardWidth: number;
  imageSize: number;
  padding: number;
  gap: number;
  headerPaddingX: number;
  headerPaddingY: number;
  codeTextSize: number;
  titleTextSize: number;
  bodyTextSize: number;
}

export function PopCardListComponent({
  config,
  className,
  screenId,
  componentId,
  currentRowSpan,
  currentColSpan,
  onRequestResize,
}: PopCardListComponentProps) {
  const { subscribe, publish } = usePopEvent(screenId || "default");
  const router = useRouter();

  // 장바구니 목록 모드 플래그 및 상태
  const isCartListMode = config?.cartListMode?.enabled === true;
  const [inheritedConfig, setInheritedConfig] = useState<Partial<PopCardListConfig> | null>(null);
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());

  // 장바구니 목록 모드: 원본 화면의 설정 전체를 병합 (cardTemplate, inputField, packageConfig, cardSize 등)
  const effectiveConfig = useMemo<PopCardListConfig | undefined>(() => {
    if (!isCartListMode || !inheritedConfig) return config;
    return {
      ...config,
      ...inheritedConfig,
      cartListMode: config?.cartListMode,
      dataSource: config?.dataSource,
    } as PopCardListConfig;
  }, [config, inheritedConfig, isCartListMode]);

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

  // 장바구니 DB 동기화 (장바구니 목록 모드에서는 비활성화)
  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);

  // 외부 필터 조건 (연결 시스템에서 수신, connectionId별 Map으로 복수 필터 AND 결합)
  const [externalFilters, setExternalFilters] = useState<
    Map<string, {
      fieldName: string;
      value: unknown;
      filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: 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 };
          filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string };
          _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,
            });
          } else {
            next.delete(connId);
          }
          return next;
        });
      }
    );
    return unsub;
  }, [componentId, subscribe]);

  // cart를 ref로 유지: 이벤트 콜백에서 항상 최신 참조를 사용
  const cartRef = useRef(cart);
  cartRef.current = cart;

  // "저장 요청" 이벤트 수신: 버튼 컴포넌트가 장바구니 저장을 요청하면 saveToDb 실행 (장바구니 목록 모드 제외)
  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]);

  // DB 로드 완료 후 초기 장바구니 상태를 버튼에 전달 (장바구니 목록 모드 제외)
  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]);

  // 카드 선택 시 selected_row 이벤트 발행
  const handleCardSelect = useCallback((row: RowData) => {
    if (!componentId) return;
    publish(`__comp_output__${componentId}__selected_row`, row);
  }, [componentId, publish]);

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

  // 컨테이너 ref + 크기 측정
  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();
  }, []);

  // 이미지 URL 없는 항목 카운트 (toast 중복 방지용)
  const missingImageCountRef = useRef(0);


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

  // 브레이크포인트별 열 제한: colSpan >= 8이면 config 최대값 허용, 그 외 1열
  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));

  // 행 수: 설정값 그대로 사용 (containerHeight 연동 시 피드백 루프 위험)
  const gridRows = configGridRows;

  // 카드 크기: 높이는 프리셋 고정, 너비만 컨테이너 기반 동적 계산
  // (높이를 containerHeight에 연동하면 뷰어 모드의 minmax(auto) 그리드와
  //  ResizeObserver 사이에서 피드백 루프가 발생해 무한 성장함)
  const scaled = useMemo((): ScaledConfig => {
    const gap = spec.gap;

    const cardHeight = spec.height;
    const minCardWidth = Math.round(spec.height * 1.6);

    const cardWidth = containerWidth > 0
      ? Math.max(minCardWidth,
          Math.floor((containerWidth - gap * (gridColumns - 1)) / gridColumns))
      : minCardWidth;

    return {
      cardHeight,
      cardWidth,
      imageSize: spec.imageSize,
      padding: spec.padding,
      gap,
      headerPaddingX: spec.headerPadX,
      headerPaddingY: spec.headerPadY,
      codeTextSize: spec.codeText,
      titleTextSize: spec.titleText,
      bodyTextSize: spec.bodyText,
    };
  }, [spec, containerWidth, gridColumns]);

  // 외부 필터 적용 (복수 필터 AND 결합)
  const filteredRows = useMemo(() => {
    if (externalFilters.size === 0) return rows;

    const matchSingleFilter = (
      row: RowData,
      filter: { fieldName: string; value: unknown; filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string } }
    ): boolean => {
      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 matchCell = (cellValue: string) => {
        switch (mode) {
          case "equals":
            return cellValue === searchValue;
          case "starts_with":
            return cellValue.startsWith(searchValue);
          case "contains":
          default:
            return cellValue.includes(searchValue);
        }
      };

      return columns.some((col) => matchCell(String(row[col] ?? "").toLowerCase()));
    };

    const allFilters = [...externalFilters.values()];
    return rows.filter((row) => allFilters.every((f) => matchSingleFilter(row, f)));
  }, [rows, externalFilters]);

  // 기본 상태에서 표시할 카드 수
  const visibleCardCount = useMemo(() => {
    return gridColumns * gridRows;
  }, [gridColumns, gridRows]);

  // 더보기 버튼 표시 여부
  const hasMoreCards = filteredRows.length > visibleCardCount;

  // 확장 상태에서 표시할 카드 수 계산
  const expandedCardsPerPage = useMemo(() => {
    // 가로/세로 모두: 기본 표시 수의 2배 + 스크롤 유도를 위해 1줄 추가
    // 가로: 컴포넌트 크기 변경 없이 카드 2배 → 가로 스크롤로 탐색
    // 세로: rowSpan 2배 → 2배 영역에 카드 채움 + 세로 스크롤
    return Math.max(1, visibleCardCount * 2 + gridColumns);
  }, [visibleCardCount, gridColumns]);

  // 스크롤 영역 ref
  const scrollAreaRef = useRef<HTMLDivElement>(null);

  // 현재 표시할 카드 결정
  const displayCards = useMemo(() => {
    if (!isExpanded) {
      return filteredRows.slice(0, visibleCardCount);
    } else {
      const start = (currentPage - 1) * expandedCardsPerPage;
      const end = start + expandedCardsPerPage;
      return filteredRows.slice(start, end);
    }
  }, [filteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage]);

  // 총 페이지 수
  const totalPages = isExpanded 
    ? Math.ceil(filteredRows.length / expandedCardsPerPage)
    : 1;
  // 페이지네이션 필요: 확장 상태에서 전체 카드가 한 페이지를 초과할 때
  const needsPagination = isExpanded && totalPages > 1;

  // 페이지 변경 핸들러
  const handlePrevPage = () => {
    if (currentPage > 1) {
      setCurrentPage(currentPage - 1);
    }
  };

  const handleNextPage = () => {
    if (currentPage < totalPages) {
      setCurrentPage(currentPage + 1);
    }
  };

  // 확장/접기 토글: 세로 모드에서만 rowSpan 2배 확장, 가로 모드에서는 크기 변경 없이 카드만 추가 표시
  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]);

  // dataSource를 직렬화해서 의존성 안정화 (객체 참조 변경에 의한 불필요한 재호출 방지)
  const dataSourceKey = useMemo(
    () => JSON.stringify(dataSource || null),
    [dataSource]
  );

  // 장바구니 목록 모드 설정을 직렬화 (의존성 안정화)
  const cartListModeKey = useMemo(
    () => JSON.stringify(config?.cartListMode || null),
    [config?.cartListMode]
  );

  useEffect(() => {
    // 장바구니 목록 모드: cart_items에서 직접 조회
    if (isCartListMode) {
      const cartListMode = config!.cartListMode!;

      // 원본 화면 미선택 시 데이터 조회하지 않음
      if (!cartListMode.sourceScreenId) {
        setLoading(false);
        setRows([]);
        return;
      }

      const fetchCartData = async () => {
        setLoading(true);
        setError(null);
        try {
          // 원본 화면 레이아웃에서 설정 전체 상속 (cardTemplate, inputField, packageConfig, cardSize 등)
          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");
            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,
          });

          const parsed = (result.data || []).map(parseCartRow);
          setRows(parsed);
        } catch (err) {
          const message = err instanceof Error ? err.message : "장바구니 데이터 조회 실패";
          setError(message);
          setRows([]);
        } finally {
          setLoading(false);
        }
      };
      fetchCartData();
      return;
    }

    // 기본 모드: 데이터 소스에서 조회
    if (!dataSource?.tableName) {
      setLoading(false);
      setRows([]);
      return;
    }

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      missingImageCountRef.current = 0;

      try {
        // 서버에는 = 연산자 필터만 전달, 나머지는 클라이언트 후처리
        const filters: Record<string, unknown> = {};
        if (dataSource.filters && dataSource.filters.length > 0) {
          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.length > 0 ? sortArray[0] : undefined;
        const sortBy = primarySort?.column;
        const sortOrder = primarySort?.direction;

        const size =
          dataSource.limit?.mode === "limited" && dataSource.limit?.count
            ? dataSource.limit.count
            : 100;

        const result = await dataApi.getTableData(dataSource.tableName, {
          page: 1,
          size,
          sortBy: sortBy || undefined,
          sortOrder,
          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;
              }
            })
          );
        }

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

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

  // 이미지 URL 없는 항목 카운트 (내부 추적용, toast 미표시)
  useEffect(() => {
    if (!loading && rows.length > 0 && effectiveTemplate?.image?.enabled && effectiveTemplate?.image?.imageColumn) {
      const imageColumn = effectiveTemplate.image.imageColumn;
      missingImageCountRef.current = rows.filter((row) => !row[imageColumn]).length;
    }
  }, [loading, rows, effectiveTemplate?.image]);

  // 장바구니 목록 모드: 항목 삭제 콜백
  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;
    });
  }, []);

  // 장바구니 목록 모드: 수량 수정 콜백 (로컬만 업데이트, DB 미반영)
  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,
      };
    }));
  }, []);

  // 데이터 수집 요청 수신: 버튼에서 collect_data 요청 → 선택 항목 + 매핑 응답
  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;

        // CardListSaveMapping → SaveMapping 변환
        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 response: CollectedDataResponse = {
          requestId: request?.requestId ?? "",
          componentId: componentId,
          componentType: "pop-card-list",
          data: { items: selectedItems },
          mapping,
        };

        publish(`__comp_output__${componentId}__collected_data`, response);
      }
    );
    return unsub;
  }, [componentId, subscribe, publish, isCartListMode, filteredRows, rows, selectedKeys]);

  // 장바구니 목록 모드: 선택 항목 이벤트 발행
  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 cardAreaStyle: React.CSSProperties = {
    gap: `${scaled.gap}px`,
    ...(isHorizontalMode
      ? {
          gridTemplateRows: `repeat(${gridRows}, ${scaled.cardHeight}px)`,
          gridAutoFlow: "column",
          gridAutoColumns: `${scaled.cardWidth}px`,
        }
      : {
          // 세로 모드: 1fr 비율 기반으로 컨테이너 너비 초과 방지
          gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
          gridAutoRows: `${scaled.cardHeight}px`,
        }),
  };

  // 세로 모드 스크롤 클래스: 비확장 시 overflow hidden, 확장 시에만 세로 스크롤 허용
  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>
      ) : 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>
      ) : (
        <>
          {/* 장바구니 목록 모드: 선택 바 */}
          {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 codeValue = effectiveTemplate?.header?.codeField && row[effectiveTemplate.header.codeField]
                ? String(row[effectiveTemplate.header.codeField])
                : null;
              const rowKey = codeValue ? `${codeValue}-${index}` : `card-${index}`;
              return (
              <Card
                key={rowKey}
                row={row}
                template={effectiveTemplate}
                scaled={scaled}
                inputField={effectiveConfig?.inputField}
                packageConfig={effectiveConfig?.packageConfig}
                cartAction={effectiveConfig?.cartAction}
                publish={publish}
                router={router}
                onSelect={handleCardSelect}
                cart={cart}
                keyColumnName={effectiveConfig?.cartAction?.keyColumn || "id"}
                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}
              />
              );
            })}
          </div>

          {/* 하단 컨트롤 영역 */}
          {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={handlePrevPage}
                      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={handleNextPage}
                      disabled={currentPage >= totalPages}
                      className="h-9 w-9 p-0"
                    >
                      <ChevronRight className="h-5 w-5" />
                    </Button>
                  </div>
                )}
              </div>
            </div>
          )}
        </>
      )}
    </div>
  );
}

// ===== 카드 컴포넌트 =====

function Card({
  row,
  template,
  scaled,
  inputField,
  packageConfig,
  cartAction,
  publish,
  router,
  onSelect,
  cart,
  keyColumnName,
  parentComponentId,
  isCartListMode,
  isSelected,
  onToggleSelect,
  onDeleteItem,
  onUpdateQuantity,
}: {
  row: RowData;
  template?: CardTemplateConfig;
  scaled: ScaledConfig;
  inputField?: CardInputFieldConfig;
  packageConfig?: CardPackageConfig;
  cartAction?: CardCartActionConfig;
  publish: (eventName: string, payload?: unknown) => void;
  router: ReturnType<typeof useRouter>;
  onSelect?: (row: RowData) => void;
  cart: ReturnType<typeof useCartSync>;
  keyColumnName?: string;
  parentComponentId?: string;
  isCartListMode?: boolean;
  isSelected?: boolean;
  onToggleSelect?: () => void;
  onDeleteItem?: (cartId: string) => void;
  onUpdateQuantity?: (cartId: string, quantity: number, unit?: string, entries?: PackageEntry[]) => void;
}) {
  const header = template?.header;
  const image = template?.image;
  const body = template?.body;

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

  const codeValue = header?.codeField ? row[header.codeField] : null;
  const titleValue = header?.titleField ? row[header.titleField] : null;

  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]);

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

  const imageUrl =
    image?.enabled && image?.imageColumn && row[image.imageColumn]
      ? String(row[image.imageColumn])
      : image?.defaultImage || DEFAULT_CARD_IMAGE;

  // limitColumn 우선, 하위 호환으로 maxColumn 폴백
  const limitCol = inputField?.limitColumn || inputField?.maxColumn;
  const effectiveMax = useMemo(() => {
    if (limitCol) {
      const colVal = Number(row[limitCol]);
      if (!isNaN(colVal) && colVal > 0) return colVal;
    }
    return 999999;
  }, [limitCol, row]);

  // 제한 컬럼이 있으면 최대값으로 자동 초기화
  useEffect(() => {
    if (inputField?.enabled && limitCol && effectiveMax > 0 && effectiveMax < 999999) {
      setInputValue(effectiveMax);
    }
  }, [effectiveMax, inputField?.enabled, limitCol]);

  const cardStyle: React.CSSProperties = {
    height: `${scaled.cardHeight}px`,
    overflow: "hidden",
  };

  const headerStyle: React.CSSProperties = {
    padding: `${scaled.headerPaddingY}px ${scaled.headerPaddingX}px`,
  };

  const bodyStyle: React.CSSProperties = {
    padding: `${scaled.padding}px`,
    gap: `${scaled.gap}px`,
  };

  const imageContainerStyle: React.CSSProperties = {
    width: `${scaled.imageSize}px`,
    height: `${scaled.imageSize}px`,
  };

  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;

    const cartItem: CartItem = {
      row,
      quantity: inputValue,
      packageUnit: packageUnit || undefined,
      packageEntries: packageEntries.length > 0 ? packageEntries : undefined,
    };

    cart.addItem(cartItem, 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;

    const ok = window.confirm("이 항목을 장바구니에서 삭제하시겠습니까?");
    if (!ok) return;

    try {
      await dataApi.updateRecord("cart_items", cartId, { status: "cancelled" });
      onDeleteItem?.(cartId);
    } catch {
      toast.error("삭제에 실패했습니다.");
    }
  };

  // pop-icon 스타일 담기/취소 버튼 아이콘 크기 (스케일 반영)
  const iconSize = Math.max(14, Math.round(18 * (scaled.bodyTextSize / 11)));
  const cartLabel = cartAction?.label || "담기";
  const cancelLabel = cartAction?.cancelLabel || "취소";

  const handleCardClick = () => {
    onSelect?.(row);
  };

  // 카드 테두리: 장바구니 목록 모드에서는 선택 상태, 기본 모드에서는 담기 상태 기준
  const borderClass = 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";

  // 카드 헤더 배경: 장바구니 목록 모드에서는 선택 상태, 기본 모드에서는 담기 상태 기준
  const headerBgClass = isCartListMode
    ? isSelected ? "bg-primary/10 dark:bg-primary/20" : "bg-muted/30"
    : isCarted ? "bg-emerald-50 dark:bg-emerald-950/30" : "bg-muted/30";

  return (
    <div
      className={`relative cursor-pointer rounded-lg border bg-card shadow-sm transition-all duration-150 hover:shadow-md ${borderClass}`}
      style={cardStyle}
      onClick={handleCardClick}
      role="button"
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleCardClick(); }}
    >
      {/* 헤더 영역 */}
      {(codeValue !== null || titleValue !== null || isCartListMode) && (
        <div className={`border-b ${headerBgClass}`} style={headerStyle}>
          <div className="flex items-center gap-2">
            {isCartListMode && (
              <input
                type="checkbox"
                checked={isSelected}
                onChange={(e) => { e.stopPropagation(); onToggleSelect?.(); }}
                onClick={(e) => e.stopPropagation()}
                className="h-4 w-4 shrink-0 rounded border-input"
              />
            )}
            {codeValue !== null && (
              <span
                className="shrink-0 font-medium text-muted-foreground"
                style={{ fontSize: `${scaled.codeTextSize}px` }}
              >
                {formatValue(codeValue)}
              </span>
            )}
            {titleValue !== null && (
              <MarqueeText
                className="font-bold text-foreground"
                style={{ fontSize: `${scaled.titleTextSize}px` }}
              >
                {formatValue(titleValue)}
              </MarqueeText>
            )}
          </div>
        </div>
      )}

      {/* 본문 영역 */}
      <div className="flex" style={bodyStyle}>
        {/* 이미지 (왼쪽) */}
        {image?.enabled && (
          <div className="shrink-0">
            <div
              className="flex items-center justify-center overflow-hidden rounded-md border bg-muted/30"
              style={imageContainerStyle}
            >
              <img
                src={imageUrl}
                alt=""
                className="h-full w-full object-contain p-1"
                onError={(e) => {
                  const target = e.target as HTMLImageElement;
                  if (target.src !== DEFAULT_CARD_IMAGE) target.src = DEFAULT_CARD_IMAGE;
                }}
              />
            </div>
          </div>
        )}

        {/* 필드 목록 (중간, flex-1) */}
        <div className="min-w-0 flex-1">
          <div style={{ display: "flex", flexDirection: "column", gap: `${Math.round(scaled.gap / 2)}px` }}>
            {body?.fields && body.fields.length > 0 ? (
              body.fields.map((field) => (
                <FieldRow key={field.id} field={field} row={row} scaled={scaled} inputValue={inputValue} />
              ))
            ) : (
              <div
                className="flex items-center justify-center text-muted-foreground"
                style={{ fontSize: `${scaled.bodyTextSize}px` }}
              >
                본문 필드를 추가하세요
              </div>
            )}
          </div>
        </div>

        {/* 오른쪽: 수량 버튼 + 담기/취소/삭제 버튼 */}
        {(inputField?.enabled || cartAction || isCartListMode) && (
          <div
            className="ml-2 flex shrink-0 flex-col items-stretch justify-center gap-2"
            style={{ minWidth: "100px" }}
          >
            {/* 수량 버튼 (입력 필드 ON일 때만) */}
            {inputField?.enabled && (
              <button
                type="button"
                onClick={handleInputClick}
                className="rounded-lg border-2 border-input bg-background px-2 py-1.5 text-center hover:border-primary active:bg-muted"
              >
                <span className="block text-lg font-bold leading-tight">
                  {inputValue.toLocaleString()}
                </span>
                <span className="text-muted-foreground block text-[12px]">
                  {inputField.unit || "EA"}
                </span>
              </button>
            )}

            {/* 장바구니 목록 모드: 삭제 버튼 */}
            {isCartListMode && (
              <button
                type="button"
                onClick={handleCartDelete}
                className="flex flex-col items-center justify-center gap-0.5 rounded-xl bg-destructive px-2 py-1.5 text-destructive-foreground transition-colors duration-150 hover:bg-destructive/90 active:bg-destructive/80"
              >
                <Trash2 size={iconSize} />
                <span style={{ fontSize: `${Math.max(9, scaled.bodyTextSize - 2)}px` }} className="font-semibold leading-tight">
                  삭제
                </span>
              </button>
            )}

            {/* 기본 모드: 담기/취소 버튼 (cartAction 존재 시) */}
            {!isCartListMode && cartAction && (
              <>
                {isCarted ? (
                  <button
                    type="button"
                    onClick={handleCartCancel}
                    className="flex flex-col items-center justify-center gap-0.5 rounded-xl bg-destructive px-2 py-1.5 text-destructive-foreground transition-colors duration-150 hover:bg-destructive/90 active:bg-destructive/80"
                  >
                    <X size={iconSize} />
                    <span style={{ fontSize: `${Math.max(9, scaled.bodyTextSize - 2)}px` }} className="font-semibold leading-tight">
                      {cancelLabel}
                    </span>
                  </button>
                ) : (
                  <button
                    type="button"
                    onClick={handleCartAdd}
                    className="flex flex-col items-center justify-center gap-0.5 rounded-xl bg-linear-to-br from-amber-400 to-orange-500 px-2 py-1.5 text-white transition-colors duration-150 hover:from-amber-500 hover:to-orange-600 active:from-amber-600 active:to-orange-700"
                  >
                    {cartAction?.iconType === "emoji" && cartAction?.iconValue ? (
                      <span style={{ fontSize: `${iconSize}px` }}>{cartAction.iconValue}</span>
                    ) : (
                      <DynamicLucideIcon name={cartAction?.iconValue} size={iconSize} />
                    )}
                    <span style={{ fontSize: `${Math.max(9, scaled.bodyTextSize - 2)}px` }} className="font-semibold leading-tight">
                      {cartLabel}
                    </span>
                  </button>
                )}
              </>
            )}
          </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}
        />
      )}
    </div>
  );
}

// ===== 필드 행 컴포넌트 =====

function FieldRow({
  field,
  row,
  scaled,
  inputValue,
}: {
  field: CardFieldBinding;
  row: RowData;
  scaled: ScaledConfig;
  inputValue?: number;
}) {
  const valueType = field.valueType || "column";

  const displayValue = useMemo(() => {
    if (valueType !== "formula") {
      return formatValue(field.columnName ? row[field.columnName] : undefined);
    }

    // 구조화된 수식 우선
    if (field.formulaLeft && field.formulaOperator) {
      const rightVal = field.formulaRightType === "input"
        ? (inputValue ?? 0)
        : Number(row[field.formulaRight || ""] ?? 0);
      const leftVal = Number(row[field.formulaLeft] ?? 0);

      let result: number | null = null;
      switch (field.formulaOperator) {
        case "+": result = leftVal + rightVal; break;
        case "-": result = leftVal - rightVal; break;
        case "*": result = leftVal * rightVal; break;
        case "/": result = rightVal !== 0 ? leftVal / rightVal : null; break;
      }

      if (result !== null && isFinite(result)) {
        const formatted = Math.round(result * 100) / 100;
        return field.unit ? `${formatted.toLocaleString()} ${field.unit}` : formatted.toLocaleString();
      }
      return "-";
    }

    // 하위 호환: 레거시 formula 문자열
    if (field.formula) {
      const result = evaluateFormula(field.formula, row, inputValue ?? 0);
      if (result !== null) {
        const formatted = result.toLocaleString();
        return field.unit ? `${formatted} ${field.unit}` : formatted;
      }
    }
    return "-";
  }, [valueType, field, row, inputValue]);

  const isFormula = valueType === "formula";
  const labelMinWidth = Math.round(50 * (scaled.bodyTextSize / 12));

  return (
    <div
      className="flex items-baseline"
      style={{ gap: `${Math.round(scaled.gap / 2)}px`, fontSize: `${scaled.bodyTextSize}px` }}
    >
      <span
        className="shrink-0 text-muted-foreground"
        style={{ minWidth: `${labelMinWidth}px` }}
      >
        {field.label}
      </span>
      <MarqueeText
        className="font-medium"
        style={{ color: field.textColor || (isFormula ? "#ea580c" : "#000000") }}
      >
        {displayValue}
      </MarqueeText>
    </div>
  );
}

// ===== 값 포맷팅 =====

function formatValue(value: unknown): string {
  if (value === null || value === undefined) {
    return "-";
  }
  if (typeof value === "number") {
    return value.toLocaleString();
  }
  if (typeof value === "boolean") {
    return value ? "예" : "아니오";
  }
  if (value instanceof Date) {
    return value.toLocaleDateString();
  }
  // ISO 날짜 문자열 감지 및 포맷
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
    const date = new Date(value);
    if (!isNaN(date.getTime())) {
      // MM-DD 형식으로 표시
      return `${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
    }
  }
  return String(value);
}

// ===== 계산식 평가 =====

/**
 * 간단한 계산식을 평가합니다.
 * 지원 연산: +, -, *, /
 * 특수 변수: $input (입력 필드 값)
 *
 * @param formula 계산식 (예: "order_qty - inbound_qty", "$input - received_qty")
 * @param row 데이터 행
 * @param inputValue 입력 필드 값
 * @returns 계산 결과 또는 null (계산 실패 시)
 */
function evaluateFormula(
  formula: string,
  row: RowData,
  inputValue: number
): number | null {
  try {
    // 수식에서 컬럼명과 $input을 실제 값으로 치환
    let expression = formula;

    // $input을 입력값으로 치환
    expression = expression.replace(/\$input/g, String(inputValue));

    // 컬럼명을 값으로 치환 (알파벳, 숫자, 언더스코어로 구성된 식별자)
    const columnPattern = /[a-zA-Z_][a-zA-Z0-9_]*/g;
    expression = expression.replace(columnPattern, (match) => {
      // 이미 숫자로 치환된 경우 스킵
      if (/^\d+$/.test(match)) return match;

      const value = row[match];
      if (value === null || value === undefined) return "0";
      if (typeof value === "number") return String(value);
      const parsed = parseFloat(String(value));
      return isNaN(parsed) ? "0" : String(parsed);
    });

    // 안전한 계산 (기본 산술 연산만 허용)
    // 허용: 숫자, +, -, *, /, (, ), 공백, 소수점
    if (!/^[\d\s+\-*/().]+$/.test(expression)) {
      return null;
    }

    // eval 대신 Function 사용 (더 안전)
    const result = new Function(`return (${expression})`)();

    if (typeof result !== "number" || isNaN(result) || !isFinite(result)) {
      return null;
    }

    return Math.round(result * 100) / 100; // 소수점 2자리까지
  } catch (error) {
    // 수식 평가 실패 시 null 반환
    return null;
  }
}
