Files
sam-react-prod/src/components/quality/InspectionManagement/OrderSelectModal.tsx
유병철 ca6247286a feat(WEB): 수입검사 관리 대폭 개선, 캘린더 DayTimeView 추가 및 출고 기능 보완
- 수입검사: InspectionCreate/Detail/List 대폭 개선, OrderSelectModal/문서 컴포넌트 신규 추가
- 수입검사: actions/types/mockData/inspectionConfig 전면 리팩토링
- QMS: InspectionModalV2/ImportInspectionDocument 개선
- 캘린더: DayTimeView 신규 추가, CalendarHeader/ScheduleCalendar/utils 확장
- 출고: ShipmentDetail/List/actions 개선, ShipmentOrderDocument/ShippingSlip 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 16:46:52 +09:00

221 lines
6.9 KiB
TypeScript

'use client';
/**
* 수주 선택 모달
*
* 기획서 기반 신규 생성:
* - 검색 입력
* - 체크박스 테이블 (수주번호, 현장명, 납품일, 개소)
* - 취소/선택 버튼
*/
import { useState, useCallback, useEffect } from 'react';
import { Search, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { toast } from 'sonner';
import { getOrderSelectList } from './actions';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import type { OrderSelectItem } from './types';
interface OrderSelectModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSelect: (items: OrderSelectItem[]) => void;
/** 이미 선택된 항목 ID 목록 (중복 선택 방지) */
excludeIds?: string[];
}
export function OrderSelectModal({
open,
onOpenChange,
onSelect,
excludeIds = [],
}: OrderSelectModalProps) {
const [searchTerm, setSearchTerm] = useState('');
const [items, setItems] = useState<OrderSelectItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
// 데이터 로드
const loadItems = useCallback(async (q?: string) => {
setIsLoading(true);
try {
const result = await getOrderSelectList({ q: q || undefined });
if (result.success) {
// 이미 선택된 항목 제외
const filtered = result.data.filter((item) => !excludeIds.includes(item.id));
setItems(filtered);
} else {
toast.error(result.error || '수주 목록을 불러오는데 실패했습니다.');
}
} catch (error) {
if (isNextRedirectError(error)) throw error;
console.error('[OrderSelectModal] loadItems error:', error);
toast.error('수주 목록 로드 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}, [excludeIds]);
// 모달 열릴 때 데이터 로드 & 상태 초기화
useEffect(() => {
if (open) {
setSearchTerm('');
setSelectedIds(new Set());
loadItems();
}
}, [open, loadItems]);
// 검색
const handleSearch = useCallback(() => {
loadItems(searchTerm);
}, [searchTerm, loadItems]);
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleSearch();
}
}, [handleSearch]);
// 체크박스 토글
const handleToggle = useCallback((id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
// 전체 선택/해제
const handleToggleAll = useCallback(() => {
setSelectedIds((prev) => {
if (prev.size === items.length) {
return new Set();
}
return new Set(items.map((item) => item.id));
});
}, [items]);
// 선택 확인
const handleConfirm = useCallback(() => {
const selectedItems = items.filter((item) => selectedIds.has(item.id));
onSelect(selectedItems);
onOpenChange(false);
}, [items, selectedIds, onSelect, onOpenChange]);
const isAllSelected = items.length > 0 && selectedIds.size === items.length;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
{/* 검색 */}
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleSearchKeyDown}
placeholder="수주번호, 현장명 검색..."
className="pl-9"
/>
</div>
<Button variant="outline" onClick={handleSearch}>
</Button>
</div>
{/* 테이블 */}
<div className="max-h-[400px] overflow-y-auto border rounded-md">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-10">
<Checkbox
checked={isAllSelected}
onCheckedChange={handleToggleAll}
/>
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow
key={item.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleToggle(item.id)}
>
<TableCell onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={selectedIds.has(item.id)}
onCheckedChange={() => handleToggle(item.id)}
/>
</TableCell>
<TableCell>{item.orderNumber}</TableCell>
<TableCell>{item.siteName}</TableCell>
<TableCell className="text-center">{item.deliveryDate}</TableCell>
<TableCell className="text-center">{item.locationCount}</TableCell>
</TableRow>
))}
{items.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground py-8">
{searchTerm ? '검색 결과가 없습니다.' : '수주 데이터가 없습니다.'}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button
onClick={handleConfirm}
disabled={selectedIds.size === 0}
>
({selectedIds.size})
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}