[feat] negodata/front: 상품·협력사 엑셀 업로드 양식 정비
업로드 모달/툴바를 드롭다운(일괄 업로드 + 양식 다운로드)으로 정리, UPLOAD_COLUMNS 단일 정의 공유, CSV 템플릿(헤더+예시행) 추가. dropdown-menu UI 추가. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c6a002e29
commit
21e9f71f53
55
negodata/front/src/components/ui/dropdown-menu.tsx
Normal file
55
negodata/front/src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// shadcn 스타일 DropdownMenu(@base-ui Menu 기반). 액션 묶음용 — Select 와 달리 값 선택이 아니라 명령 실행.
|
||||||
|
// 사용: <DropdownMenu><DropdownMenuTrigger render={<Button/>}>...</DropdownMenuTrigger>
|
||||||
|
// <DropdownMenuContent><DropdownMenuItem onClick={...}>...</DropdownMenuItem></DropdownMenuContent></DropdownMenu>
|
||||||
|
|
||||||
|
function DropdownMenu(props: MenuPrimitive.Root.Props) {
|
||||||
|
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger(props: MenuPrimitive.Trigger.Props) {
|
||||||
|
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: MenuPrimitive.Popup.Props) {
|
||||||
|
return (
|
||||||
|
<MenuPrimitive.Portal>
|
||||||
|
<MenuPrimitive.Positioner className="z-50 outline-none" sideOffset={4} align="end">
|
||||||
|
<MenuPrimitive.Popup
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground relative z-50 min-w-40 overflow-hidden rounded-lg border border-border p-1 text-xs shadow-md outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MenuPrimitive.Popup>
|
||||||
|
</MenuPrimitive.Positioner>
|
||||||
|
</MenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({ className, ...props }: MenuPrimitive.Item.Props) {
|
||||||
|
return (
|
||||||
|
<MenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-muted data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem }
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { Upload, X, FileSpreadsheet, CheckCircle2, Download, Trash2 } from 'lucide-react';
|
import { Upload, X, FileSpreadsheet, CheckCircle2, Trash2 } from 'lucide-react';
|
||||||
import type { ReqCreateSupplier as SupplierCreate } from '@/api/generated/model/reqCreateSupplier';
|
import type { ReqCreateSupplier as SupplierCreate } from '@/api/generated/model/reqCreateSupplier';
|
||||||
import { showToast } from '@/lib/notify';
|
import { showToast } from '@/lib/notify';
|
||||||
import { downloadExcel, parseCsv, type BulkFailure } from '@/lib/excel';
|
import { downloadExcel, parseCsv, type BulkFailure } from '@/lib/excel';
|
||||||
@ -72,6 +72,21 @@ function toSupplierCreate(row: RawRow): SupplierCreate {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 업로드 양식(.csv) 다운로드 — 채워 넣을 컬럼 헤더 + 예시 1행. 툴바·모달이 공유한다.
|
||||||
|
export function downloadPartnerTemplate() {
|
||||||
|
downloadExcel<TemplateRow>(
|
||||||
|
'협력사_업로드_양식',
|
||||||
|
[
|
||||||
|
{ header: '협력사명', value: (r) => r.name },
|
||||||
|
{ header: '식별코드', value: (r) => r.code },
|
||||||
|
{ header: '담당자명', value: (r) => r.managerName },
|
||||||
|
{ header: '담당자이메일', value: (r) => r.managerEmail },
|
||||||
|
{ header: '우선순위', value: (r) => r.priority },
|
||||||
|
],
|
||||||
|
[{ name: '예시) (주)한빛정밀', code: 'PART-EXAMPLE-001', managerName: '김철수 과장', managerEmail: 'cs.kim@example.com', priority: 'HIGH' }],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 협력사 엑셀 일괄 업로드 모달. 파일 파싱·원본 행 state는 이 컴포넌트가 소유하고,
|
// 협력사 엑셀 일괄 업로드 모달. 파일 파싱·원본 행 state는 이 컴포넌트가 소유하고,
|
||||||
// 검증은 렌더 시 validateRows로 파생한다. 실제 서버 등록은 onConfirm(검증된 행)으로 위임.
|
// 검증은 렌더 시 validateRows로 파생한다. 실제 서버 등록은 onConfirm(검증된 행)으로 위임.
|
||||||
export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUploadModalProps) {
|
export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUploadModalProps) {
|
||||||
@ -98,21 +113,6 @@ export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUp
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 업로드 양식(.csv) 다운로드 — 채워 넣을 컬럼 헤더 + 예시 1행 (lib/excel 재사용)
|
|
||||||
const handleDownloadTemplate = () => {
|
|
||||||
downloadExcel<TemplateRow>(
|
|
||||||
'협력사_업로드_양식',
|
|
||||||
[
|
|
||||||
{ header: '협력사명', value: (r) => r.name },
|
|
||||||
{ header: '식별코드', value: (r) => r.code },
|
|
||||||
{ header: '담당자명', value: (r) => r.managerName },
|
|
||||||
{ header: '담당자이메일', value: (r) => r.managerEmail },
|
|
||||||
{ header: '우선순위', value: (r) => r.priority },
|
|
||||||
],
|
|
||||||
[{ name: '예시) (주)한빛정밀', code: 'PART-EXAMPLE-001', managerName: '김철수 과장', managerEmail: 'cs.kim@example.com', priority: 'HIGH' }],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 업로드된 CSV를 파싱해 원본 행으로 적재(검증은 자동 파생). 헤더는 양식과 동일해야 함.
|
// 업로드된 CSV를 파싱해 원본 행으로 적재(검증은 자동 파생). 헤더는 양식과 동일해야 함.
|
||||||
const handleFile = async (file: File) => {
|
const handleFile = async (file: File) => {
|
||||||
const parsed = parseCsv(await file.text());
|
const parsed = parseCsv(await file.text());
|
||||||
@ -243,16 +243,6 @@ export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUp
|
|||||||
>
|
>
|
||||||
엑셀 파일 업로드하기
|
엑셀 파일 업로드하기
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="excel-partners-template-download-button"
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
className="mt-2.5 flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground underline underline-offset-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Download size={12} />
|
|
||||||
업로드 양식(.csv) 다운로드
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { Upload, X, FileSpreadsheet, CheckCircle2, Download, Trash2 } from 'lucide-react';
|
import { Upload, X, FileSpreadsheet, CheckCircle2, Trash2 } from 'lucide-react';
|
||||||
import type { ReqCreateItem as ItemCreate } from '@/api/generated/model/reqCreateItem';
|
import type { ReqCreateItem as ItemCreate } from '@/api/generated/model/reqCreateItem';
|
||||||
import { showToast } from '@/lib/notify';
|
import { showToast } from '@/lib/notify';
|
||||||
import { downloadExcel, parseCsv, type BulkFailure } from '@/lib/excel';
|
import { downloadExcel, parseCsv, type BulkFailure } from '@/lib/excel';
|
||||||
@ -22,6 +22,8 @@ type RawRow = {
|
|||||||
made_in: string;
|
made_in: string;
|
||||||
price: number;
|
price: number;
|
||||||
minPrice: number;
|
minPrice: number;
|
||||||
|
purchase_price: number;
|
||||||
|
selling_price: number;
|
||||||
image_url: string;
|
image_url: string;
|
||||||
moq: string;
|
moq: string;
|
||||||
lead_time: number;
|
lead_time: number;
|
||||||
@ -59,6 +61,8 @@ const UPLOAD_COLUMNS: { header: string; key: keyof RawRow }[] = [
|
|||||||
{ header: '원산지', key: 'made_in' },
|
{ header: '원산지', key: 'made_in' },
|
||||||
{ header: '상품 단가', key: 'price' },
|
{ header: '상품 단가', key: 'price' },
|
||||||
{ header: '최저한도', key: 'minPrice' },
|
{ header: '최저한도', key: 'minPrice' },
|
||||||
|
{ header: '매입가', key: 'purchase_price' },
|
||||||
|
{ header: '판매가', key: 'selling_price' },
|
||||||
{ header: '이미지URL', key: 'image_url' },
|
{ header: '이미지URL', key: 'image_url' },
|
||||||
{ header: '최소주문수량', key: 'moq' },
|
{ header: '최소주문수량', key: 'moq' },
|
||||||
{ header: '리드타임(일)', key: 'lead_time' },
|
{ header: '리드타임(일)', key: 'lead_time' },
|
||||||
@ -69,7 +73,7 @@ const UPLOAD_COLUMNS: { header: string; key: keyof RawRow }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 숫자 입력 컬럼 / 필수 컬럼(헤더에 * 표기)
|
// 숫자 입력 컬럼 / 필수 컬럼(헤더에 * 표기)
|
||||||
const NUMERIC_KEYS = new Set<keyof RawRow>(['price', 'minPrice', 'lead_time']);
|
const NUMERIC_KEYS = new Set<keyof RawRow>(['price', 'minPrice', 'purchase_price', 'selling_price', 'lead_time']);
|
||||||
const REQUIRED_KEYS = new Set<keyof RawRow>(['name', 'code', 'price']);
|
const REQUIRED_KEYS = new Set<keyof RawRow>(['name', 'code', 'price']);
|
||||||
|
|
||||||
// 양식에 채워 넣는 예시 행(시드 상품과 동일 셋). 다운로드 양식에 그대로 들어간다.
|
// 양식에 채워 넣는 예시 행(시드 상품과 동일 셋). 다운로드 양식에 그대로 들어간다.
|
||||||
@ -77,19 +81,28 @@ const EXAMPLE_ROWS: Record<string, string | number>[] = [
|
|||||||
{
|
{
|
||||||
name: '리튬인산철 배터리 모듈', code: 'BAT-LFP-100', model_name: 'LFP-100A',
|
name: '리튬인산철 배터리 모듈', code: 'BAT-LFP-100', model_name: 'LFP-100A',
|
||||||
category: '에너지/배터리', spec: '3.2V 100Ah', manufacturer: '한성에너지', made_in: '대한민국',
|
category: '에너지/배터리', spec: '3.2V 100Ah', manufacturer: '한성에너지', made_in: '대한민국',
|
||||||
price: 1250000, minPrice: 1037500, image_url: 'https://example.com/img/lfp-100a.jpg',
|
price: 1250000, minPrice: 1037500, purchase_price: 1000000, selling_price: 1250000, image_url: 'https://example.com/img/lfp-100a.jpg',
|
||||||
moq: '10 EA', lead_time: 14, quantity_unit: 'EA', delivery_type: '협력사배송',
|
moq: '10 EA', lead_time: 14, quantity_unit: 'EA', delivery_type: '협력사배송',
|
||||||
vat_yn: 'Y', delivery_fee_yn: 'N',
|
vat_yn: 'Y', delivery_fee_yn: 'N',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '산업용 6축 로봇암', code: 'ROB-6AX-22', model_name: 'RX-6A',
|
name: '산업용 6축 로봇암', code: 'ROB-6AX-22', model_name: 'RX-6A',
|
||||||
category: '자동화설비', spec: '가반하중 12kg', manufacturer: '오토메카', made_in: '일본',
|
category: '자동화설비', spec: '가반하중 12kg', manufacturer: '오토메카', made_in: '일본',
|
||||||
price: 18900000, minPrice: 16065000, image_url: 'https://example.com/img/rx-6a.jpg',
|
price: 18900000, minPrice: 16065000, purchase_price: 15000000, selling_price: 18900000, image_url: 'https://example.com/img/rx-6a.jpg',
|
||||||
moq: '1 EA', lead_time: 30, quantity_unit: 'EA', delivery_type: '지정택배배송',
|
moq: '1 EA', lead_time: 30, quantity_unit: 'EA', delivery_type: '지정택배배송',
|
||||||
vat_yn: 'Y', delivery_fee_yn: 'N',
|
vat_yn: 'Y', delivery_fee_yn: 'N',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 업로드 양식(.csv) 다운로드 — 전체 컬럼 헤더 + 예시 행(시드 상품 셋). UPLOAD_COLUMNS 단일 정의 공유. 툴바·모달이 공유한다.
|
||||||
|
export function downloadProductTemplate() {
|
||||||
|
downloadExcel<Record<string, string | number>>(
|
||||||
|
'상품_업로드_양식',
|
||||||
|
UPLOAD_COLUMNS.map((c) => ({ header: c.header, value: (r) => r[c.key] })),
|
||||||
|
EXAMPLE_ROWS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type ExcelUploadModalProps = {
|
type ExcelUploadModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
products: Product[]; // 코드 중복 검사용
|
products: Product[]; // 코드 중복 검사용
|
||||||
@ -149,6 +162,8 @@ function toItemCreate(row: RawRow): ItemCreate {
|
|||||||
manufacturer: row.manufacturer || undefined,
|
manufacturer: row.manufacturer || undefined,
|
||||||
made_in: row.made_in || undefined,
|
made_in: row.made_in || undefined,
|
||||||
price: row.price,
|
price: row.price,
|
||||||
|
purchase_price: row.purchase_price || undefined,
|
||||||
|
selling_price: row.selling_price || undefined,
|
||||||
image_url: row.image_url || undefined,
|
image_url: row.image_url || undefined,
|
||||||
moq: row.moq || undefined,
|
moq: row.moq || undefined,
|
||||||
lead_time: row.lead_time || undefined,
|
lead_time: row.lead_time || undefined,
|
||||||
@ -187,15 +202,6 @@ export function ExcelUploadModal({ open, products, onConfirm, onClose }: ExcelUp
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 업로드 양식(.csv) 다운로드 — 전체 컬럼 헤더 + 예시 행(시드 상품 셋). UPLOAD_COLUMNS 단일 정의 공유.
|
|
||||||
const handleDownloadTemplate = () => {
|
|
||||||
downloadExcel<Record<string, string | number>>(
|
|
||||||
'상품_업로드_양식',
|
|
||||||
UPLOAD_COLUMNS.map((c) => ({ header: c.header, value: (r) => r[c.key] })),
|
|
||||||
EXAMPLE_ROWS,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 업로드된 CSV를 파싱해 원본 행으로 적재(검증은 자동 파생). 헤더는 양식과 동일해야 함.
|
// 업로드된 CSV를 파싱해 원본 행으로 적재(검증은 자동 파생). 헤더는 양식과 동일해야 함.
|
||||||
const handleFile = async (file: File) => {
|
const handleFile = async (file: File) => {
|
||||||
const parsed = parseCsv(await file.text());
|
const parsed = parseCsv(await file.text());
|
||||||
@ -211,6 +217,8 @@ export function ExcelUploadModal({ open, products, onConfirm, onClose }: ExcelUp
|
|||||||
made_in: r['원산지'] ?? '',
|
made_in: r['원산지'] ?? '',
|
||||||
price: Number(r['상품 단가']) || 0,
|
price: Number(r['상품 단가']) || 0,
|
||||||
minPrice: Number(r['최저한도']) || 0,
|
minPrice: Number(r['최저한도']) || 0,
|
||||||
|
purchase_price: Number(r['매입가']) || 0,
|
||||||
|
selling_price: Number(r['판매가']) || 0,
|
||||||
image_url: r['이미지URL'] ?? '',
|
image_url: r['이미지URL'] ?? '',
|
||||||
moq: r['최소주문수량'] ?? '',
|
moq: r['최소주문수량'] ?? '',
|
||||||
lead_time: Number(r['리드타임(일)']) || 0,
|
lead_time: Number(r['리드타임(일)']) || 0,
|
||||||
@ -345,16 +353,6 @@ export function ExcelUploadModal({ open, products, onConfirm, onClose }: ExcelUp
|
|||||||
>
|
>
|
||||||
엑셀 파일 업로드하기
|
엑셀 파일 업로드하기
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="excel-template-download-button"
|
|
||||||
onClick={handleDownloadTemplate}
|
|
||||||
className="mt-2.5 flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground underline underline-offset-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Download size={12} />
|
|
||||||
업로드 양식(.csv) 다운로드
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Plus, Upload } from 'lucide-react';
|
import { Plus, Upload, Download, FileSpreadsheet, ChevronDown } from 'lucide-react';
|
||||||
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
||||||
import { showToast } from '@/lib/notify';
|
import { showToast } from '@/lib/notify';
|
||||||
import { confirm } from '@/lib/confirm';
|
import { confirm } from '@/lib/confirm';
|
||||||
@ -6,12 +6,13 @@ import { PageContainer } from '@/components/layout/PageContainer';
|
|||||||
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
import { useServerList } from '@/lib/useServerList';
|
import { useServerList } from '@/lib/useServerList';
|
||||||
import { usePartners } from '@/features/partners/hooks/usePartners';
|
import { usePartners } from '@/features/partners/hooks/usePartners';
|
||||||
import type { ListSuppliersParams } from '@/api/generated/model/listSuppliersParams';
|
import type { ListSuppliersParams } from '@/api/generated/model/listSuppliersParams';
|
||||||
import { PartnerTable } from '@/features/partners/components/PartnerTable';
|
import { PartnerTable } from '@/features/partners/components/PartnerTable';
|
||||||
import { PartnerFormSheet } from '@/features/partners/components/PartnerFormSheet';
|
import { PartnerFormSheet } from '@/features/partners/components/PartnerFormSheet';
|
||||||
import { ExcelUploadModal } from '@/features/partners/components/ExcelUploadModal';
|
import { ExcelUploadModal, downloadPartnerTemplate } from '@/features/partners/components/ExcelUploadModal';
|
||||||
import { prioritiesList, type Partner } from '@/features/partners/types';
|
import { prioritiesList, type Partner } from '@/features/partners/types';
|
||||||
|
|
||||||
export default function PartnersPage() {
|
export default function PartnersPage() {
|
||||||
@ -57,10 +58,23 @@ export default function PartnersPage() {
|
|||||||
<PageToolbar
|
<PageToolbar
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" onClick={() => overlay.open('modal', 'excel')}>
|
<DropdownMenu>
|
||||||
<Upload />
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||||
엑셀 일괄 업로드
|
<FileSpreadsheet />
|
||||||
</Button>
|
엑셀업로드
|
||||||
|
<ChevronDown />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onClick={() => overlay.open('modal', 'excel')}>
|
||||||
|
<Upload />
|
||||||
|
일괄 업로드
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={downloadPartnerTemplate}>
|
||||||
|
<Download />
|
||||||
|
양식 다운로드
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
<Button onClick={openCreate}>
|
<Button onClick={openCreate}>
|
||||||
<Plus />
|
<Plus />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Plus, Upload, TrendingDown } from 'lucide-react';
|
import { Plus, Upload, TrendingDown, Download, FileSpreadsheet, ChevronDown } from 'lucide-react';
|
||||||
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
import { useOverlayRouter } from '@/lib/useOverlayRouter';
|
||||||
import { showToast } from '@/lib/notify';
|
import { showToast } from '@/lib/notify';
|
||||||
import { confirm } from '@/lib/confirm';
|
import { confirm } from '@/lib/confirm';
|
||||||
@ -7,6 +7,7 @@ import { PageContainer } from '@/components/layout/PageContainer';
|
|||||||
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useServerList } from '@/lib/useServerList';
|
import { useServerList } from '@/lib/useServerList';
|
||||||
import { useProducts } from '@/features/products/hooks/useProducts';
|
import { useProducts } from '@/features/products/hooks/useProducts';
|
||||||
@ -14,7 +15,7 @@ import type { ListItemsParams } from '@/api/generated/model/listItemsParams';
|
|||||||
import { ProductTable } from '@/features/products/components/ProductTable';
|
import { ProductTable } from '@/features/products/components/ProductTable';
|
||||||
import { ProductFormSheet } from '@/features/products/components/ProductFormSheet';
|
import { ProductFormSheet } from '@/features/products/components/ProductFormSheet';
|
||||||
import { PriceUpdateModal } from '@/features/products/components/PriceUpdateModal';
|
import { PriceUpdateModal } from '@/features/products/components/PriceUpdateModal';
|
||||||
import { ExcelUploadModal } from '@/features/products/components/ExcelUploadModal';
|
import { ExcelUploadModal, downloadProductTemplate } from '@/features/products/components/ExcelUploadModal';
|
||||||
import { type Product } from '@/features/products/types';
|
import { type Product } from '@/features/products/types';
|
||||||
|
|
||||||
export default function ProductsPage() {
|
export default function ProductsPage() {
|
||||||
@ -82,10 +83,23 @@ export default function ProductsPage() {
|
|||||||
{selectedIds.length > 0 && <Badge variant="destructive">{selectedIds.length}</Badge>}
|
{selectedIds.length > 0 && <Badge variant="destructive">{selectedIds.length}</Badge>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button variant="outline" onClick={() => overlay.open('modal', 'excel')}>
|
<DropdownMenu>
|
||||||
<Upload />
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||||
엑셀 일괄 업로드
|
<FileSpreadsheet />
|
||||||
</Button>
|
엑셀업로드
|
||||||
|
<ChevronDown />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onClick={() => overlay.open('modal', 'excel')}>
|
||||||
|
<Upload />
|
||||||
|
일괄 업로드
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={downloadProductTemplate}>
|
||||||
|
<Download />
|
||||||
|
양식 다운로드
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
<Button onClick={openCreate}>
|
<Button onClick={openCreate}>
|
||||||
<Plus />
|
<Plus />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user