267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { CustomerItemMappingConfig } from "./types";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { X } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface CustomerItemMappingComponentProps {
|
|
component: any;
|
|
isDesignMode?: boolean;
|
|
isSelected?: boolean;
|
|
isInteractive?: boolean;
|
|
config?: CustomerItemMappingConfig;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
onClick?: (e?: React.MouseEvent) => void;
|
|
onDragStart?: (e: React.DragEvent) => void;
|
|
onDragEnd?: (e: React.DragEvent) => void;
|
|
}
|
|
|
|
export const CustomerItemMappingComponent: React.FC<CustomerItemMappingComponentProps> = ({
|
|
component,
|
|
isDesignMode = false,
|
|
isSelected = false,
|
|
config,
|
|
className,
|
|
style,
|
|
onClick,
|
|
onDragStart,
|
|
onDragEnd,
|
|
}) => {
|
|
const finalConfig = {
|
|
...config,
|
|
...component.config,
|
|
} as CustomerItemMappingConfig;
|
|
|
|
const [data, setData] = useState<any[]>([]);
|
|
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
|
const [isAllSelected, setIsAllSelected] = useState(false);
|
|
|
|
// 데이터 로드 (실제 구현 시 API 호출)
|
|
useEffect(() => {
|
|
if (!isDesignMode && finalConfig.selectedTable) {
|
|
// TODO: API 호출로 데이터 로드
|
|
setData([]);
|
|
}
|
|
}, [finalConfig.selectedTable, isDesignMode]);
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
if (checked) {
|
|
const allIds = data.map((_, index) => `row-${index}`);
|
|
setSelectedRows(new Set(allIds));
|
|
setIsAllSelected(true);
|
|
} else {
|
|
setSelectedRows(new Set());
|
|
setIsAllSelected(false);
|
|
}
|
|
};
|
|
|
|
const handleRowSelection = (rowId: string, checked: boolean) => {
|
|
const newSelected = new Set(selectedRows);
|
|
if (checked) {
|
|
newSelected.add(rowId);
|
|
} else {
|
|
newSelected.delete(rowId);
|
|
}
|
|
setSelectedRows(newSelected);
|
|
setIsAllSelected(newSelected.size === data.length && data.length > 0);
|
|
};
|
|
|
|
const columns = finalConfig.columns || [];
|
|
const showCheckbox = finalConfig.checkbox?.enabled !== false;
|
|
|
|
// 스타일 계산
|
|
const componentStyle: React.CSSProperties = {
|
|
position: "relative",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
backgroundColor: "hsl(var(--background))",
|
|
overflow: "hidden",
|
|
boxSizing: "border-box",
|
|
width: "100%",
|
|
height: "100%",
|
|
minHeight: isDesignMode ? "300px" : "100%",
|
|
...style, // style prop이 위의 기본값들을 덮어씀
|
|
};
|
|
|
|
// 이벤트 핸들러
|
|
const handleClick = (e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
onClick?.();
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cn("w-full h-full", className)}
|
|
style={componentStyle}
|
|
onClick={handleClick}
|
|
onDragStart={isDesignMode ? onDragStart : undefined}
|
|
onDragEnd={isDesignMode ? onDragEnd : undefined}
|
|
draggable={isDesignMode}
|
|
>
|
|
{/* 헤더 */}
|
|
<div className="w-full border-border bg-muted flex h-12 flex-shrink-0 items-center justify-between border-b px-4 sm:h-14 sm:px-6">
|
|
<h3 className="text-sm font-semibold sm:text-base">
|
|
품목 추가 - {finalConfig.selectedTable || "[테이블 선택]"}
|
|
{finalConfig.showCompanyName && finalConfig.companyNameColumn && (
|
|
<span className="text-muted-foreground ml-2 text-xs font-normal sm:text-sm">
|
|
| {finalConfig.companyNameColumn}
|
|
</span>
|
|
)}
|
|
</h3>
|
|
<button className="hover:bg-muted-foreground/10 rounded p-1">
|
|
<X className="h-4 w-4 sm:h-5 sm:w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 검색/카테고리 영역 */}
|
|
{finalConfig.showSearchArea && (
|
|
<div className="w-full border-border bg-background flex-shrink-0 border-b p-3 sm:p-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
|
|
{/* 검색 입력 */}
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder={finalConfig.searchPlaceholder || "품목코드, 품목명, 규격 검색"}
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring h-9 w-full rounded-md border px-3 py-1 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm"
|
|
disabled={isDesignMode}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 카테고리 필터 */}
|
|
{finalConfig.enableCategoryFilter && (
|
|
<div className="w-full sm:w-auto sm:min-w-[160px]">
|
|
<select
|
|
className="border-input bg-background ring-offset-background focus-visible:ring-ring h-9 w-full rounded-md border px-3 py-1 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 sm:h-10 sm:text-sm"
|
|
disabled={isDesignMode}
|
|
>
|
|
{(finalConfig.categories || ["전체"]).map((category, idx) => (
|
|
<option key={idx} value={category}>
|
|
{category}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 목록 헤더 */}
|
|
<div className="w-full border-border bg-muted/50 flex h-10 flex-shrink-0 items-center justify-between border-b px-4 sm:h-12 sm:px-6">
|
|
<span className="text-xs font-semibold sm:text-sm">판매품목 목록</span>
|
|
<div className="flex items-center gap-3 sm:gap-6">
|
|
{showCheckbox && finalConfig.checkbox?.selectAll && (
|
|
<label className="flex cursor-pointer items-center gap-2">
|
|
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} />
|
|
<span className="text-xs sm:text-sm">전체 선택</span>
|
|
</label>
|
|
)}
|
|
<span className="text-muted-foreground text-xs font-medium sm:text-sm">
|
|
선택: {selectedRows.size}개
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 테이블 컨테이너 */}
|
|
<div className="flex w-full flex-1 flex-col overflow-hidden">
|
|
{/* 테이블 헤더 */}
|
|
{columns.length > 0 && (
|
|
<div className="border-border flex-shrink-0 border-b">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full" style={{ minWidth: "100%" }}>
|
|
<thead>
|
|
<tr className="bg-muted/30 h-10 sm:h-12">
|
|
{showCheckbox && (
|
|
<th className="border-border w-12 border-r px-2 text-center sm:w-16 sm:px-3"></th>
|
|
)}
|
|
{columns.map((col, index) => (
|
|
<th
|
|
key={col}
|
|
className={cn(
|
|
"border-border text-foreground px-3 text-left text-xs font-semibold sm:px-6 sm:text-sm",
|
|
index < columns.length - 1 && "border-r"
|
|
)}
|
|
>
|
|
{col}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 데이터 영역 */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
{data.length === 0 ? (
|
|
<div className="flex h-full flex-col items-center justify-center gap-3 p-8 text-center sm:gap-4 sm:p-12">
|
|
<div className="bg-muted/50 flex h-16 w-16 items-center justify-center rounded-full sm:h-20 sm:w-20">
|
|
<svg
|
|
className="text-muted-foreground h-8 w-8 sm:h-10 sm:w-10"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div className="space-y-1 sm:space-y-2">
|
|
<p className="text-foreground text-base font-semibold sm:text-lg">
|
|
{finalConfig.emptyMessage || "데이터가 없습니다"}
|
|
</p>
|
|
<p className="text-muted-foreground text-xs sm:text-sm">
|
|
{finalConfig.emptyDescription || "품목 데이터가 추가되면 여기에 표시됩니다"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full" style={{ minWidth: "100%" }}>
|
|
<tbody>
|
|
{data.map((row, index) => (
|
|
<tr key={index} className="hover:bg-muted/50 border-b transition-colors">
|
|
{showCheckbox && (
|
|
<td className="border-border w-12 border-r px-2 text-center sm:w-16 sm:px-3">
|
|
<Checkbox
|
|
checked={selectedRows.has(`row-${index}`)}
|
|
onCheckedChange={(checked) =>
|
|
handleRowSelection(`row-${index}`, checked as boolean)
|
|
}
|
|
/>
|
|
</td>
|
|
)}
|
|
{columns.map((col, colIndex) => (
|
|
<td
|
|
key={col}
|
|
className={cn(
|
|
"border-border px-3 py-2 text-xs sm:px-6 sm:py-3 sm:text-sm",
|
|
colIndex < columns.length - 1 && "border-r"
|
|
)}
|
|
>
|
|
{row[col] || "-"}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|