- {/* 활성화 스위치 */}
- 계산 필드 사용
+ 포장등록 사용
updateField({ enabled })}
+ checked={config.enabled}
+ onCheckedChange={(enabled) => updateConfig({ enabled })}
/>
- {field.enabled && (
+ {config.enabled && (
<>
- {/* 라벨 */}
+ {/* 기본 포장 단위 체크박스 */}
- 라벨
- updateField({ label: e.target.value })}
- className="mt-1 h-7 text-xs"
- placeholder="미입고"
- />
-
-
- {/* 계산식 */}
-
-
계산식
-
updateField({ formula: e.target.value })}
- className="mt-1 h-7 text-xs font-mono"
- placeholder="$input - received_qty"
- />
-
- 사용 가능: 컬럼명, $input (입력값), +, -, *, /
-
-
-
- {/* 단위 */}
-
- 단위
- updateField({ unit: e.target.value })}
- className="mt-1 h-7 text-xs"
- placeholder="EA"
- />
-
-
- {/* 사용 가능한 컬럼 목록 */}
-
-
- 사용 가능한 컬럼
-
-
-
- {columns.map((col) => (
- {
- // 클릭 시 계산식에 컬럼명 추가
- const currentFormula = field.formula || "";
- updateField({ formula: currentFormula + col.name });
- }}
- >
- {col.name}
-
- ))}
-
+
사용할 포장 단위
+
+ {PACKAGE_UNITS.map((unit) => (
+ toggleUnit(unit.value)}
+ className={cn(
+ "flex items-center gap-1.5 rounded-md border px-2 py-1.5 text-[10px] transition-colors",
+ enabledSet.has(unit.value)
+ ? "border-primary bg-primary/5 text-primary"
+ : "border-muted text-muted-foreground hover:bg-muted/50"
+ )}
+ >
+ {unit.emoji}
+ {unit.label}
+
+ ))}
-
- 클릭하면 계산식에 추가됩니다
-
+
+ {/* 커스텀 단위 */}
+
+
커스텀 단위
+
+ {(config.customUnits || []).map((cu) => (
+
+ updateCustomUnit(cu.id, e.target.value)}
+ className="h-7 flex-1 text-xs"
+ placeholder="단위 이름 (예: 파렛트)"
+ />
+ removeCustomUnit(cu.id)}
+ >
+
+
+
+ ))}
+
+
+
+ 단위 추가
+
+
+
+ {/* 계산 결과 안내 메시지 */}
+
+ 계산 결과 안내 메시지
+ updateConfig({ showSummaryMessage: checked })}
+ />
+
+
+ 포장 등록 시 계산 결과를 안내 메시지로 표시합니다
+
>
)}
);
}
-// ===== 조인 설정 섹션 =====
+// ===== 조인 설정 섹션 (테이블 선택 -> 컬럼 자동 매칭) =====
+
+// 두 테이블 간 매칭 가능한 컬럼 쌍 찾기
+function findMatchingColumns(
+ sourceCols: ColumnInfo[],
+ targetCols: ColumnInfo[],
+): Array<{ source: string; target: string; confidence: "high" | "medium" }> {
+ const matches: Array<{ source: string; target: string; confidence: "high" | "medium" }> = [];
+ const sourceNames = sourceCols.map((c) => c.name);
+ const targetNames = targetCols.map((c) => c.name);
+
+ for (const src of sourceNames) {
+ for (const tgt of targetNames) {
+ // 정확히 같은 이름 (예: item_code = item_code)
+ if (src === tgt) {
+ matches.push({ source: src, target: tgt, confidence: "high" });
+ continue;
+ }
+ // 소스가 _id/_code/_no 로 끝나고, 타겟 테이블에 같은 이름이 있는 경우
+ // 예: source.customer_code -> target.customer_code (이미 위에서 처리됨)
+ // 소스 컬럼명이 타겟 컬럼명을 포함하거나, 타겟이 소스를 포함
+ const suffixes = ["_id", "_code", "_no", "_number", "_key"];
+ const srcBase = suffixes.reduce((name, s) => name.endsWith(s) ? name.slice(0, -s.length) : name, src);
+ const tgtBase = suffixes.reduce((name, s) => name.endsWith(s) ? name.slice(0, -s.length) : name, tgt);
+ if (srcBase && tgtBase && srcBase === tgtBase && src !== tgt) {
+ matches.push({ source: src, target: tgt, confidence: "medium" });
+ }
+ }
+ }
+
+ // confidence 높은 순 정렬
+ return matches.sort((a, b) => (a.confidence === "high" ? -1 : 1) - (b.confidence === "high" ? -1 : 1));
+}
function JoinSettingsSection({
dataSource,
@@ -1284,175 +1647,185 @@ function JoinSettingsSection({
}) {
const joins = dataSource.joins || [];
const [sourceColumns, setSourceColumns] = useState
([]);
- const [targetColumnsMap, setTargetColumnsMap] = useState<
- Record
- >({});
+ const [targetColumnsMap, setTargetColumnsMap] = useState>({});
- // 소스 테이블 컬럼 로드
useEffect(() => {
if (dataSource.tableName) {
fetchTableColumns(dataSource.tableName).then(setSourceColumns);
+ } else {
+ setSourceColumns([]);
}
}, [dataSource.tableName]);
- // 조인 추가
- const addJoin = () => {
- const newJoin: CardColumnJoin = {
- targetTable: "",
- joinType: "LEFT",
- sourceColumn: "",
- targetColumn: "",
- };
- onUpdate({ joins: [...joins, newJoin] });
+ const getTableLabel = (tableName: string) => {
+ const found = tables.find((t) => t.tableName === tableName);
+ return found?.displayName || tableName;
};
- // 조인 업데이트
- const updateJoin = (index: number, updated: CardColumnJoin) => {
- const newJoins = [...joins];
- newJoins[index] = updated;
- onUpdate({ joins: newJoins });
+ // 대상 테이블 컬럼 로드 + 자동 매칭
+ const loadTargetAndAutoMatch = (index: number, join: CardColumnJoin, targetTable: string) => {
+ const updated = { ...join, targetTable, sourceColumn: "", targetColumn: "" };
- // 대상 테이블 컬럼 로드
- if (updated.targetTable && !targetColumnsMap[updated.targetTable]) {
- fetchTableColumns(updated.targetTable).then((cols) => {
- setTargetColumnsMap((prev) => ({
- ...prev,
- [updated.targetTable]: cols,
- }));
+ const doMatch = (targetCols: ColumnInfo[]) => {
+ const matches = findMatchingColumns(sourceColumns, targetCols);
+ if (matches.length > 0) {
+ updated.sourceColumn = matches[0].source;
+ updated.targetColumn = matches[0].target;
+ }
+ const newJoins = [...joins];
+ newJoins[index] = updated;
+ onUpdate({ joins: newJoins });
+ };
+
+ if (targetColumnsMap[targetTable]) {
+ doMatch(targetColumnsMap[targetTable]);
+ } else {
+ fetchTableColumns(targetTable).then((cols) => {
+ setTargetColumnsMap((prev) => ({ ...prev, [targetTable]: cols }));
+ doMatch(cols);
});
}
};
- // 조인 삭제
+ const updateJoin = (index: number, updated: CardColumnJoin) => {
+ const newJoins = [...joins];
+ newJoins[index] = updated;
+ onUpdate({ joins: newJoins });
+ };
+
const deleteJoin = (index: number) => {
const newJoins = joins.filter((_, i) => i !== index);
onUpdate({ joins: newJoins.length > 0 ? newJoins : undefined });
};
+ const addJoin = () => {
+ onUpdate({ joins: [...joins, { targetTable: "", joinType: "LEFT", sourceColumn: "", targetColumn: "" }] });
+ };
+
return (
{joins.length === 0 ? (
- 다른 테이블과 조인하여 추가 컬럼을 사용할 수 있습니다
+ 다른 테이블을 연결하면 추가 정보를 카드에 표시할 수 있습니다
) : (
- {joins.map((join, index) => (
-
-
-
- 조인 {index + 1}
-
- deleteJoin(index)}
- >
-
-
-
+ {joins.map((join, index) => {
+ const targetCols = targetColumnsMap[join.targetTable] || [];
+ const matchingPairs = join.targetTable
+ ? findMatchingColumns(sourceColumns, targetCols)
+ : [];
+ const hasAutoMatch = join.sourceColumn && join.targetColumn;
- {/* 조인 타입 */}
-
- updateJoin(index, {
- ...join,
- joinType: val as "LEFT" | "INNER" | "RIGHT",
- })
- }
- >
-
-
-
-
- LEFT JOIN
- INNER JOIN
- RIGHT JOIN
-
-
-
- {/* 대상 테이블 */}
-
- updateJoin(index, {
- ...join,
- targetTable: val,
- targetColumn: "",
- })
- }
- >
-
-
-
-
- {tables
- .filter((t) => t.tableName !== dataSource.tableName)
- .map((table) => (
-
- {table.displayName || table.tableName}
-
- ))}
-
-
-
- {/* ON 조건 */}
- {join.targetTable && (
-
-
- updateJoin(index, { ...join, sourceColumn: val })
- }
- >
-
-
-
-
- {sourceColumns.map((col) => (
-
- {col.name}
-
- ))}
-
-
-
=
-
- updateJoin(index, { ...join, targetColumn: val })
- }
- >
-
-
-
-
- {(targetColumnsMap[join.targetTable] || []).map((col) => (
-
- {col.name}
-
- ))}
-
-
+ return (
+
+
+
+ {join.targetTable ? getTableLabel(join.targetTable) : "테이블 선택"}
+
+ deleteJoin(index)}>
+
+
- )}
-
- ))}
+
+ {/* 대상 테이블 선택 (검색 가능) */}
+
t.tableName !== dataSource.tableName)}
+ value={join.targetTable || ""}
+ onSelect={(val) => loadTargetAndAutoMatch(index, join, val)}
+ />
+
+ {/* 자동 매칭 결과 또는 수동 선택 */}
+ {join.targetTable && (
+
+ {/* 자동 매칭 성공: 결과 표시 */}
+ {hasAutoMatch ? (
+
+
+
+ 자동 연결됨: {join.sourceColumn} = {join.targetColumn}
+
+
+ ) : (
+
+
+ 자동 매칭되는 컬럼이 없습니다. 직접 선택하세요.
+
+
+ )}
+
+ {/* 다른 매칭 후보가 있으면 표시 */}
+ {matchingPairs.length > 1 && (
+
+ 다른 연결 기준:
+ {matchingPairs.map((pair) => {
+ const isActive = join.sourceColumn === pair.source && join.targetColumn === pair.target;
+ if (isActive) return null;
+ return (
+ updateJoin(index, { ...join, sourceColumn: pair.source, targetColumn: pair.target })}
+ >
+ {pair.source} = {pair.target}
+ {pair.confidence === "high" && (
+ 정확
+ )}
+
+ );
+ })}
+
+ )}
+
+ {/* 수동 선택 (펼치기) */}
+
+
+ 직접 컬럼 선택
+
+
+ updateJoin(index, { ...join, sourceColumn: val })}
+ >
+
+
+
+
+ {sourceColumns.map((col) => (
+ {col.name}
+ ))}
+
+
+ =
+ updateJoin(index, { ...join, targetColumn: val })}
+ >
+
+
+
+
+ {targetCols.map((col) => (
+ {col.name}
+ ))}
+
+
+
+
+
+ )}
+
+ );
+ })}
)}
-
+
- 조인 추가
+ 테이블 연결 추가
);
@@ -1593,101 +1966,108 @@ function FilterSettingsSection({
);
}
-// ===== 정렬 설정 섹션 =====
+// ===== 정렬 설정 섹션 (다중 정렬) =====
function SortSettingsSection({
dataSource,
- columns,
+ columnGroups,
onUpdate,
}: {
dataSource: CardListDataSource;
- columns: ColumnInfo[];
+ columnGroups: ColumnGroup[];
onUpdate: (partial: Partial
) => void;
}) {
- const sort = dataSource.sort;
+ // 하위 호환: 이전 형식(단일 객체)이 저장되어 있을 수 있음
+ const sorts: CardSortConfig[] = Array.isArray(dataSource.sort)
+ ? dataSource.sort
+ : dataSource.sort && typeof dataSource.sort === "object"
+ ? [dataSource.sort as CardSortConfig]
+ : [];
+
+ const addSort = () => {
+ onUpdate({ sort: [...sorts, { column: "", direction: "desc" }] });
+ };
+
+ const updateSort = (index: number, updated: CardSortConfig) => {
+ const newSorts = [...sorts];
+ newSorts[index] = updated;
+ onUpdate({ sort: newSorts });
+ };
+
+ const deleteSort = (index: number) => {
+ const newSorts = sorts.filter((_, i) => i !== index);
+ onUpdate({ sort: newSorts.length > 0 ? newSorts : undefined });
+ };
return (
- {/* 정렬 사용 여부 */}
-
- onUpdate({ sort: undefined })}
- >
- 정렬 없음
-
-
- onUpdate({ sort: { column: "", direction: "asc" } })
- }
- >
- 정렬 사용
-
-
+
+ 화면 로드 시 적용되는 기본 정렬 순서입니다. 위에 있는 항목이 우선 적용됩니다.
+
- {sort && (
+ {sorts.length === 0 ? (
+
+ ) : (
- {/* 정렬 컬럼 */}
-
- 정렬 컬럼
-
- onUpdate({ sort: { ...sort, column: val } })
- }
- >
-
-
-
-
- {columns.map((col) => (
-
- {col.name}
-
- ))}
-
-
-
-
- {/* 정렬 방향 */}
-
-
정렬 방향
-
+ {sorts.map((sort, index) => (
+
+
+ {index + 1}
+
+
+ updateSort(index, { ...sort, column: val || "" })}
+ placeholder="컬럼 선택"
+ />
+
onUpdate({ sort: { ...sort, direction: "asc" } })}
+ onClick={() => updateSort(index, { ...sort, direction: "asc" })}
>
- 오름차순
+ 오름
- onUpdate({ sort: { ...sort, direction: "desc" } })
- }
+ onClick={() => updateSort(index, { ...sort, direction: "desc" })}
>
- 내림차순
+ 내림
+
deleteSort(index)}
+ >
+
+
-
+ ))}
)}
+
+
+
+ 정렬 기준 추가
+
);
}
@@ -1872,3 +2252,142 @@ function CartActionSettingsSection({
);
}
+
+// ===== 반응형 표시 설정 섹션 =====
+
+const RESPONSIVE_MODES: ResponsiveDisplayMode[] = ["required", "shrink", "hidden"];
+
+function ResponsiveDisplaySection({
+ config,
+ onUpdate,
+}: {
+ config: PopCardListConfig;
+ onUpdate: (partial: Partial) => void;
+}) {
+ const template = config.cardTemplate || { header: {}, image: { enabled: false }, body: { fields: [] } };
+ const responsive = config.responsiveDisplay || {};
+ const bodyFields = template.body?.fields || [];
+
+ const hasHeader = !!template.header?.codeField || !!template.header?.titleField;
+ const hasImage = !!template.image?.enabled;
+ const hasFields = bodyFields.length > 0;
+
+ const updateResponsive = (partial: Partial) => {
+ onUpdate({ responsiveDisplay: { ...responsive, ...partial } });
+ };
+
+ const updateFieldMode = (fieldId: string, mode: ResponsiveDisplayMode) => {
+ updateResponsive({
+ fields: { ...(responsive.fields || {}), [fieldId]: mode },
+ });
+ };
+
+ if (!hasHeader && !hasImage && !hasFields) {
+ return (
+
+
+ 카드 템플릿에 항목을 먼저 추가하세요
+
+
+ );
+ }
+
+ return (
+
+
+ 화면이 좁아질 때 각 항목이 어떻게 표시될지 설정합니다.
+
+
+
+ {RESPONSIVE_MODES.map((mode) => (
+
+ {RESPONSIVE_DISPLAY_LABELS[mode]}
+ {mode === "required" && " = 항상 표시"}
+ {mode === "shrink" && " = 축소 가능"}
+ {mode === "hidden" && " = 숨김 가능"}
+
+ ))}
+
+
+
+ {template.header?.codeField && (
+ updateResponsive({ code: mode })}
+ />
+ )}
+ {template.header?.titleField && (
+ updateResponsive({ title: mode })}
+ />
+ )}
+ {hasImage && (
+ updateResponsive({ image: mode })}
+ />
+ )}
+ {bodyFields.map((field) => (
+ updateFieldMode(field.id, mode)}
+ />
+ ))}
+
+
+ );
+}
+
+function ResponsiveDisplayRow({
+ label,
+ sublabel,
+ value,
+ onChange,
+}: {
+ label: string;
+ sublabel?: string;
+ value: ResponsiveDisplayMode;
+ onChange: (mode: ResponsiveDisplayMode) => void;
+}) {
+ return (
+
+
+ {label}
+ {sublabel && (
+
+ {sublabel}
+
+ )}
+
+
+ {RESPONSIVE_MODES.map((mode) => (
+ onChange(mode)}
+ >
+ {RESPONSIVE_DISPLAY_LABELS[mode]}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/lib/registry/pop-components/pop-card-list/index.tsx b/frontend/lib/registry/pop-components/pop-card-list/index.tsx
index 6e417c1e..aea93555 100644
--- a/frontend/lib/registry/pop-components/pop-card-list/index.tsx
+++ b/frontend/lib/registry/pop-components/pop-card-list/index.tsx
@@ -60,6 +60,15 @@ PopComponentRegistry.registerComponent({
configPanel: PopCardListConfigPanel,
preview: PopCardListPreviewComponent,
defaultProps: defaultConfig,
+ connectionMeta: {
+ sendable: [
+ { key: "selected_row", label: "선택된 행", type: "selected_row", description: "사용자가 선택한 카드의 행 데이터를 전달" },
+ { key: "cart_item_added", label: "담기 완료", type: "event", description: "카드 담기 시 해당 행 + 수량 데이터 전달" },
+ ],
+ receivable: [
+ { key: "filter_condition", label: "필터 조건", type: "filter_value", description: "외부 컴포넌트에서 받은 필터 조건으로 카드 목록 필터링" },
+ ],
+ },
touchOptimized: true,
supportedDevices: ["mobile", "tablet"],
});
diff --git a/frontend/lib/registry/pop-components/types.ts b/frontend/lib/registry/pop-components/types.ts
index 632ae01a..a9a0a83a 100644
--- a/frontend/lib/registry/pop-components/types.ts
+++ b/frontend/lib/registry/pop-components/types.ts
@@ -364,11 +364,24 @@ export interface CardColumnFilter {
// ----- 본문 필드 바인딩 (라벨-값 쌍) -----
+export type FieldValueType = "column" | "formula";
+export type FormulaOperator = "+" | "-" | "*" | "/";
+export type FormulaRightType = "column" | "input";
+
export interface CardFieldBinding {
id: string;
- columnName: string; // DB 컬럼명
- label: string; // 표시 라벨 (예: "발주일")
- textColor?: string; // 텍스트 색상 (예: "#ef4444" 빨간색)
+ label: string;
+ valueType: FieldValueType;
+ columnName?: string; // valueType === "column"일 때 DB 컬럼명
+ // 구조화된 수식 (클릭형 빌더)
+ formulaLeft?: string; // 왼쪽: DB 컬럼명
+ formulaOperator?: FormulaOperator;
+ formulaRightType?: FormulaRightType; // "column" 또는 "input"($input)
+ formulaRight?: string; // rightType === "column"일 때 DB 컬럼명
+ /** @deprecated 구조화 수식 필드 사용, 하위 호환용 */
+ formula?: string;
+ unit?: string;
+ textColor?: string;
}
// ----- 카드 헤더 설정 (코드 + 제목) -----
@@ -406,11 +419,16 @@ export interface CardTemplateConfig {
// ----- 데이터 소스 (테이블 단위) -----
+export interface CardSortConfig {
+ column: string;
+ direction: "asc" | "desc";
+}
+
export interface CardListDataSource {
tableName: string;
joins?: CardColumnJoin[];
filters?: CardColumnFilter[];
- sort?: { column: string; direction: "asc" | "desc" };
+ sort?: CardSortConfig[];
limit?: { mode: "all" | "limited"; count?: number };
}
@@ -437,25 +455,40 @@ export const CARD_SCROLL_DIRECTION_LABELS: Record =
export interface CardInputFieldConfig {
enabled: boolean;
- columnName?: string; // 입력값이 저장될 컬럼
- label?: string; // 표시 라벨 (예: "발주 수량")
- unit?: string; // 단위 (예: "EA", "개")
- defaultValue?: number; // 기본값
- min?: number; // 최소값
- max?: number; // 최대값
- maxColumn?: string; // 최대값을 DB 컬럼에서 동적으로 가져올 컬럼명 (설정 시 row[maxColumn] 우선)
- step?: number; // 증감 단위
+ unit?: string; // 단위 (예: "EA")
+ limitColumn?: string; // 제한 기준 컬럼 (해당 행의 이 컬럼 값이 최대값)
+ saveTable?: string; // 저장 대상 테이블
+ saveColumn?: string; // 저장 대상 컬럼
+ /** @deprecated limitColumn 사용 */
+ maxColumn?: string;
+ /** @deprecated 미사용, 하위 호환용 */
+ label?: string;
+ /** @deprecated packageConfig로 이동, 하위 호환용 */
+ showPackageUnit?: boolean;
}
-// ----- 카드 내 계산 필드 설정 -----
+// ----- 포장등록 설정 -----
-export interface CardCalculatedFieldConfig {
- enabled: boolean;
- label?: string; // 표시 라벨 (예: "미입고")
- formula: string; // 계산식 (예: "order_qty - inbound_qty")
- sourceColumns: string[]; // 계산에 사용되는 컬럼들
- resultColumn?: string; // 결과를 저장할 컬럼 (선택)
- unit?: string; // 단위 (예: "EA")
+export interface CustomPackageUnit {
+ id: string;
+ label: string; // 표시명 (예: "파렛트")
+}
+
+export interface CardPackageConfig {
+ enabled: boolean; // 포장등록 기능 ON/OFF
+ enabledUnits?: string[]; // 활성화된 기본 단위 (예: ["box", "bag"]), undefined면 전체 표시
+ customUnits?: CustomPackageUnit[]; // 디자이너가 추가한 커스텀 단위
+ showSummaryMessage?: boolean; // 계산 결과 안내 메시지 표시 (기본 true)
+}
+
+// ----- 포장 내역 (2단계 계산 결과) -----
+
+export interface PackageEntry {
+ unitId: string; // 포장 단위 ID (예: "box")
+ unitLabel: string; // 포장 단위 표시명 (예: "박스")
+ packageCount: number; // 포장 수량 (예: 3)
+ quantityPerUnit: number; // 개당 수량 (예: 80)
+ totalQuantity: number; // 합계 = packageCount * quantityPerUnit
}
// ----- 담기 버튼 데이터 구조 (추후 API 연동 시 이 shape 그대로 사용) -----
@@ -464,6 +497,7 @@ export interface CartItem {
row: Record; // 카드 원본 행 데이터
quantity: number; // 입력 수량
packageUnit?: string; // 포장 단위 (box/bag/pack/bundle/roll/barrel)
+ packageEntries?: PackageEntry[]; // 포장 내역 (2단계 계산 시)
}
// ----- 담기 버튼 액션 설정 (pop-icon 스타일 + 장바구니 연동) -----
@@ -533,31 +567,39 @@ export const CARD_PRESET_SPECS: Record = {
},
};
+// ----- 반응형 표시 모드 -----
+
+export type ResponsiveDisplayMode = "required" | "shrink" | "hidden";
+
+export const RESPONSIVE_DISPLAY_LABELS: Record = {
+ required: "필수",
+ shrink: "축소",
+ hidden: "숨김",
+};
+
+export interface CardResponsiveConfig {
+ code?: ResponsiveDisplayMode;
+ title?: ResponsiveDisplayMode;
+ image?: ResponsiveDisplayMode;
+ fields?: Record;
+}
+
+// ----- pop-card-list 전체 설정 -----
+
export interface PopCardListConfig {
- // 데이터 소스 (테이블 단위)
dataSource: CardListDataSource;
-
- // 카드 템플릿 (헤더 + 이미지 + 본문)
cardTemplate: CardTemplateConfig;
-
- // 스크롤 방향
scrollDirection: CardScrollDirection;
cardsPerRow?: number; // deprecated, gridColumns 사용
- cardSize: CardSize; // 프리셋 크기 (small/medium/large)
+ cardSize: CardSize;
- // 그리드 배치 설정 (가로 x 세로)
- gridColumns?: number; // 가로 카드 수 (기본값: 3)
- gridRows?: number; // 세로 카드 수 (기본값: 2)
+ gridColumns?: number;
+ gridRows?: number;
- // 확장 설정 (더보기 클릭 시 그리드 rowSpan 변경)
- // expandedRowSpan 제거됨 - 항상 원래 크기의 2배로 확장
+ // 반응형 표시 설정
+ responsiveDisplay?: CardResponsiveConfig;
- // 입력 필드 설정 (수량 입력 등)
inputField?: CardInputFieldConfig;
-
- // 계산 필드 설정 (미입고 등 자동 계산)
- calculatedField?: CardCalculatedFieldConfig;
-
- // 담기 버튼 액션 설정 (pop-icon 스타일)
+ packageConfig?: CardPackageConfig;
cartAction?: CardCartActionConfig;
}