Enhance backend controllers, frontend pages, and V2 components
- Fix department, receiving, shippingOrder, shippingPlan controllers - Update admin pages (company management, disk usage) - Improve sales/logistics pages (order, shipping, outbound, receiving) - Enhance V2 components (file-upload, split-panel-layout, table-list) - Add SmartSelect common component - Update DataGrid, FullscreenDialog common components - Add gitignore rules for personal pipeline tools Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
|
||||
{
|
||||
key: "company_code",
|
||||
label: "회사코드",
|
||||
width: "150px",
|
||||
width: "12%",
|
||||
render: (value) => <span className="font-mono">{value}</span>,
|
||||
},
|
||||
{
|
||||
@@ -65,11 +65,12 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
|
||||
{
|
||||
key: "writer",
|
||||
label: "등록자",
|
||||
width: "200px",
|
||||
width: "15%",
|
||||
},
|
||||
{
|
||||
key: "diskUsage",
|
||||
label: "디스크 사용량",
|
||||
width: "15%",
|
||||
hideOnMobile: true,
|
||||
render: (_value, row) => formatDiskUsage(row),
|
||||
},
|
||||
@@ -99,7 +100,9 @@ export function CompanyTable({ companies, isLoading, onEdit, onDelete }: Company
|
||||
cardSubtitle={(c) => <span className="font-mono">{c.company_code}</span>}
|
||||
cardFields={cardFields}
|
||||
actionsLabel="작업"
|
||||
actionsWidth="180px"
|
||||
actionsWidth="12%"
|
||||
tableContainerClassName="!block"
|
||||
cardContainerClassName="!hidden"
|
||||
renderActions={(company) => (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -24,7 +24,7 @@ export function CompanyToolbar({ totalCount, onCreateClick }: CompanyToolbarProp
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 등록 버튼 */}
|
||||
<Button onClick={onCreateClick} className="h-10 gap-2 text-sm font-medium">
|
||||
<Button onClick={onCreateClick} className="h-10 w-full gap-2 text-sm font-medium lg:w-auto">
|
||||
<Plus className="h-4 w-4" />
|
||||
회사 등록
|
||||
</Button>
|
||||
|
||||
@@ -15,7 +15,7 @@ interface DiskUsageSummaryProps {
|
||||
export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUsageSummaryProps) {
|
||||
if (!diskUsageInfo) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 shadow-sm">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">디스크 사용량</h3>
|
||||
@@ -46,7 +46,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
|
||||
const lastCheckedDate = new Date(lastChecked);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 shadow-sm">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">디스크 사용량 현황</h3>
|
||||
@@ -64,7 +64,7 @@ export function DiskUsageSummary({ diskUsageInfo, isLoading, onRefresh }: DiskUs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4">
|
||||
{/* 총 회사 수 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Building2 className="h-4 w-4 text-primary" />
|
||||
|
||||
@@ -108,11 +108,11 @@ function SortableHeaderCell({
|
||||
style={style}
|
||||
className={cn(col.width, col.minWidth, "select-none relative")}
|
||||
>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="flex items-center gap-0.5 cursor-pointer flex-1 min-w-0"
|
||||
className="flex items-center gap-0.5 cursor-pointer min-w-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (col.sortable !== false) onSort(col.key);
|
||||
@@ -366,7 +366,6 @@ export function DataGrid({
|
||||
row[colKey] = editValue;
|
||||
toast.success("저장됨");
|
||||
} catch (err) {
|
||||
console.error("셀 저장 실패:", err);
|
||||
toast.error("저장에 실패했습니다.");
|
||||
setEditingCell(null);
|
||||
return;
|
||||
|
||||
@@ -31,6 +31,8 @@ interface FullscreenDialogProps {
|
||||
/** 기본 모달 너비 (기본: "w-[95vw]") */
|
||||
defaultWidth?: string;
|
||||
className?: string;
|
||||
/** children wrapper에 추가할 className (기본: "overflow-auto") — "overflow-hidden"으로 변경하면 내부 flex 레이아웃이 고정 높이 내에서 동작 */
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function FullscreenDialog({
|
||||
@@ -38,6 +40,7 @@ export function FullscreenDialog({
|
||||
defaultMaxWidth = "max-w-5xl",
|
||||
defaultWidth = "w-[95vw]",
|
||||
className,
|
||||
contentClassName,
|
||||
}: FullscreenDialogProps) {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
@@ -73,7 +76,7 @@ export function FullscreenDialog({
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className={cn("flex-1", contentClassName || "overflow-auto")}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
|
||||
122
frontend/components/common/SmartSelect.tsx
Normal file
122
frontend/components/common/SmartSelect.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* SmartSelect
|
||||
*
|
||||
* 옵션 개수에 따라 자동으로 검색 기능을 제공하는 셀렉트 컴포넌트.
|
||||
* - 옵션 5개 미만: 기본 Select (드롭다운)
|
||||
* - 옵션 5개 이상: Combobox (검색 + 드롭다운)
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SEARCH_THRESHOLD = 5;
|
||||
|
||||
export interface SmartSelectOption {
|
||||
code: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SmartSelectProps {
|
||||
options: SmartSelectOption[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SmartSelect({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder = "선택",
|
||||
disabled = false,
|
||||
className,
|
||||
}: SmartSelectProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const selectedLabel = useMemo(
|
||||
() => options.find((o) => o.code === value)?.label,
|
||||
[options, value],
|
||||
);
|
||||
|
||||
if (options.length < SEARCH_THRESHOLD) {
|
||||
return (
|
||||
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
|
||||
<SelectTrigger className={cn("h-9", className)}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
className={cn("h-9 w-full justify-between font-normal", className)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{selectedLabel || <span className="text-muted-foreground">{placeholder}</span>}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||
align="start"
|
||||
>
|
||||
<Command
|
||||
filter={(val, search) => {
|
||||
if (!search) return 1;
|
||||
return val.toLowerCase().includes(search.toLowerCase()) ? 1 : 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder="검색..." className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((o) => (
|
||||
<CommandItem
|
||||
key={o.code}
|
||||
value={o.label}
|
||||
onSelect={() => {
|
||||
onValueChange(o.code);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === o.code ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{o.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user