From 21e9f71f53525423aaf2f1c40e9418f25062b8e5 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Fri, 26 Jun 2026 17:08:43 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20negodata/front:=20=EC=83=81=ED=92=88?= =?UTF-8?q?=C2=B7=ED=98=91=EB=A0=A5=EC=82=AC=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=96=91=EC=8B=9D=20=EC=A0=95?= =?UTF-8?q?=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 업로드 모달/툴바를 드롭다운(일괄 업로드 + 양식 다운로드)으로 정리, UPLOAD_COLUMNS 단일 정의 공유, CSV 템플릿(헤더+예시행) 추가. dropdown-menu UI 추가. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../front/src/components/ui/dropdown-menu.tsx | 55 +++++++++++++++++++ .../partners/components/ExcelUploadModal.tsx | 42 ++++++-------- .../products/components/ExcelUploadModal.tsx | 44 +++++++-------- negodata/front/src/pages/partners.tsx | 26 +++++++-- negodata/front/src/pages/products.tsx | 26 +++++++-- 5 files changed, 132 insertions(+), 61 deletions(-) create mode 100644 negodata/front/src/components/ui/dropdown-menu.tsx diff --git a/negodata/front/src/components/ui/dropdown-menu.tsx b/negodata/front/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..9379dde --- /dev/null +++ b/negodata/front/src/components/ui/dropdown-menu.tsx @@ -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 와 달리 값 선택이 아니라 명령 실행. +// 사용: }>... +// ... + +function DropdownMenu(props: MenuPrimitive.Root.Props) { + return +} + +function DropdownMenuTrigger(props: MenuPrimitive.Trigger.Props) { + return +} + +function DropdownMenuContent({ + className, + children, + ...props +}: MenuPrimitive.Popup.Props) { + return ( + + + + {children} + + + + ) +} + +function DropdownMenuItem({ className, ...props }: MenuPrimitive.Item.Props) { + return ( + + ) +} + +export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } diff --git a/negodata/front/src/features/partners/components/ExcelUploadModal.tsx b/negodata/front/src/features/partners/components/ExcelUploadModal.tsx index 541d027..58b6b03 100644 --- a/negodata/front/src/features/partners/components/ExcelUploadModal.tsx +++ b/negodata/front/src/features/partners/components/ExcelUploadModal.tsx @@ -1,5 +1,5 @@ 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 { showToast } from '@/lib/notify'; import { downloadExcel, parseCsv, type BulkFailure } from '@/lib/excel'; @@ -72,6 +72,21 @@ function toSupplierCreate(row: RawRow): SupplierCreate { }; } +// 업로드 양식(.csv) 다운로드 — 채워 넣을 컬럼 헤더 + 예시 1행. 툴바·모달이 공유한다. +export function downloadPartnerTemplate() { + downloadExcel( + '협력사_업로드_양식', + [ + { 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는 이 컴포넌트가 소유하고, // 검증은 렌더 시 validateRows로 파생한다. 실제 서버 등록은 onConfirm(검증된 행)으로 위임. export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUploadModalProps) { @@ -98,21 +113,6 @@ export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUp onClose(); }; - // 업로드 양식(.csv) 다운로드 — 채워 넣을 컬럼 헤더 + 예시 1행 (lib/excel 재사용) - const handleDownloadTemplate = () => { - downloadExcel( - '협력사_업로드_양식', - [ - { 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를 파싱해 원본 행으로 적재(검증은 자동 파생). 헤더는 양식과 동일해야 함. const handleFile = async (file: File) => { const parsed = parseCsv(await file.text()); @@ -243,16 +243,6 @@ export function ExcelUploadModal({ open, partners, onConfirm, onClose }: ExcelUp > 엑셀 파일 업로드하기 - - ) : (
diff --git a/negodata/front/src/features/products/components/ExcelUploadModal.tsx b/negodata/front/src/features/products/components/ExcelUploadModal.tsx index de687cf..c208868 100644 --- a/negodata/front/src/features/products/components/ExcelUploadModal.tsx +++ b/negodata/front/src/features/products/components/ExcelUploadModal.tsx @@ -1,5 +1,5 @@ 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 { showToast } from '@/lib/notify'; import { downloadExcel, parseCsv, type BulkFailure } from '@/lib/excel'; @@ -22,6 +22,8 @@ type RawRow = { made_in: string; price: number; minPrice: number; + purchase_price: number; + selling_price: number; image_url: string; moq: string; lead_time: number; @@ -59,6 +61,8 @@ const UPLOAD_COLUMNS: { header: string; key: keyof RawRow }[] = [ { header: '원산지', key: 'made_in' }, { header: '상품 단가', key: 'price' }, { header: '최저한도', key: 'minPrice' }, + { header: '매입가', key: 'purchase_price' }, + { header: '판매가', key: 'selling_price' }, { header: '이미지URL', key: 'image_url' }, { header: '최소주문수량', key: 'moq' }, { header: '리드타임(일)', key: 'lead_time' }, @@ -69,7 +73,7 @@ const UPLOAD_COLUMNS: { header: string; key: keyof RawRow }[] = [ ]; // 숫자 입력 컬럼 / 필수 컬럼(헤더에 * 표기) -const NUMERIC_KEYS = new Set(['price', 'minPrice', 'lead_time']); +const NUMERIC_KEYS = new Set(['price', 'minPrice', 'purchase_price', 'selling_price', 'lead_time']); const REQUIRED_KEYS = new Set(['name', 'code', 'price']); // 양식에 채워 넣는 예시 행(시드 상품과 동일 셋). 다운로드 양식에 그대로 들어간다. @@ -77,19 +81,28 @@ const EXAMPLE_ROWS: Record[] = [ { name: '리튬인산철 배터리 모듈', code: 'BAT-LFP-100', model_name: 'LFP-100A', 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: '협력사배송', vat_yn: 'Y', delivery_fee_yn: 'N', }, { name: '산업용 6축 로봇암', code: 'ROB-6AX-22', model_name: 'RX-6A', 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: '지정택배배송', vat_yn: 'Y', delivery_fee_yn: 'N', }, ]; +// 업로드 양식(.csv) 다운로드 — 전체 컬럼 헤더 + 예시 행(시드 상품 셋). UPLOAD_COLUMNS 단일 정의 공유. 툴바·모달이 공유한다. +export function downloadProductTemplate() { + downloadExcel>( + '상품_업로드_양식', + UPLOAD_COLUMNS.map((c) => ({ header: c.header, value: (r) => r[c.key] })), + EXAMPLE_ROWS, + ); +} + type ExcelUploadModalProps = { open: boolean; products: Product[]; // 코드 중복 검사용 @@ -149,6 +162,8 @@ function toItemCreate(row: RawRow): ItemCreate { manufacturer: row.manufacturer || undefined, made_in: row.made_in || undefined, price: row.price, + purchase_price: row.purchase_price || undefined, + selling_price: row.selling_price || undefined, image_url: row.image_url || undefined, moq: row.moq || undefined, lead_time: row.lead_time || undefined, @@ -187,15 +202,6 @@ export function ExcelUploadModal({ open, products, onConfirm, onClose }: ExcelUp onClose(); }; - // 업로드 양식(.csv) 다운로드 — 전체 컬럼 헤더 + 예시 행(시드 상품 셋). UPLOAD_COLUMNS 단일 정의 공유. - const handleDownloadTemplate = () => { - downloadExcel>( - '상품_업로드_양식', - UPLOAD_COLUMNS.map((c) => ({ header: c.header, value: (r) => r[c.key] })), - EXAMPLE_ROWS, - ); - }; - // 업로드된 CSV를 파싱해 원본 행으로 적재(검증은 자동 파생). 헤더는 양식과 동일해야 함. const handleFile = async (file: File) => { const parsed = parseCsv(await file.text()); @@ -211,6 +217,8 @@ export function ExcelUploadModal({ open, products, onConfirm, onClose }: ExcelUp made_in: r['원산지'] ?? '', price: Number(r['상품 단가']) || 0, minPrice: Number(r['최저한도']) || 0, + purchase_price: Number(r['매입가']) || 0, + selling_price: Number(r['판매가']) || 0, image_url: r['이미지URL'] ?? '', moq: r['최소주문수량'] ?? '', lead_time: Number(r['리드타임(일)']) || 0, @@ -345,16 +353,6 @@ export function ExcelUploadModal({ open, products, onConfirm, onClose }: ExcelUp > 엑셀 파일 업로드하기 - -
) : (
diff --git a/negodata/front/src/pages/partners.tsx b/negodata/front/src/pages/partners.tsx index a2a8a80..013ddf1 100644 --- a/negodata/front/src/pages/partners.tsx +++ b/negodata/front/src/pages/partners.tsx @@ -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 { showToast } from '@/lib/notify'; import { confirm } from '@/lib/confirm'; @@ -6,12 +6,13 @@ import { PageContainer } from '@/components/layout/PageContainer'; import { PageToolbar, SearchInput } from '@/components/layout/PageToolbar'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Button } from '@/components/ui/button'; +import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'; import { useServerList } from '@/lib/useServerList'; import { usePartners } from '@/features/partners/hooks/usePartners'; import type { ListSuppliersParams } from '@/api/generated/model/listSuppliersParams'; import { PartnerTable } from '@/features/partners/components/PartnerTable'; 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'; export default function PartnersPage() { @@ -57,10 +58,23 @@ export default function PartnersPage() { - + + }> + + 엑셀업로드 + + + + overlay.open('modal', 'excel')}> + + 일괄 업로드 + + + + 양식 다운로드 + + + - + + }> + + 엑셀업로드 + + + + overlay.open('modal', 'excel')}> + + 일괄 업로드 + + + + 양식 다운로드 + + +