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:
kmh
2026-03-30 11:52:03 +09:00
parent 348da95823
commit b97ca1a1c5
23 changed files with 1012 additions and 365 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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;

View File

@@ -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>

View 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>
);
}