feat(WEB): DatePicker 공통화 및 공정관리/작업자화면 대폭 개선
DatePicker 공통화: - date-picker.tsx 공통 컴포넌트 신규 추가 - 전체 폼 컴포넌트 DatePicker 통일 적용 (50+ 파일) - DateRangeSelector 개선 공정관리: - RuleModal 대폭 리팩토링 (-592줄 → 간소화) - ProcessForm, StepForm 개선 - ProcessDetail 수정, actions 확장 작업자화면: - WorkerScreen 기능 대폭 확장 (+543줄) - WorkItemCard 개선 - types 확장 회계/인사/영업/품질: - BadDebtDetail, BillDetail, DepositDetail, SalesDetail 등 DatePicker 적용 - EmployeeForm, VacationDialog 등 DatePicker 적용 - OrderRegistration, QuoteRegistration DatePicker 적용 - InspectionCreate, InspectionDetail DatePicker 적용 공사관리/CEO대시보드: - BiddingDetail, ContractDetail, HandoverReport 등 DatePicker 적용 - ScheduleDetailModal, TodayIssueSection 개선 기타: - WorkOrderCreate/Edit/Detail/List 개선 - ShipmentCreate/Edit, ReceivingDetail 개선 - calendar, calendarEvents 수정 - datepicker 마이그레이션 체크리스트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,10 +11,6 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { QuantityInput } from '@/components/ui/quantity-input';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
@@ -31,17 +27,33 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Search, Package } from 'lucide-react';
|
||||
import type {
|
||||
ClassificationRule,
|
||||
RuleRegistrationType,
|
||||
RuleType,
|
||||
MatchingType,
|
||||
} from '@/types/process';
|
||||
import { RULE_TYPE_OPTIONS, MATCHING_TYPE_OPTIONS } from '@/types/process';
|
||||
import { Search } from 'lucide-react';
|
||||
import type { ClassificationRule } from '@/types/process';
|
||||
import { PROCESS_CATEGORY_OPTIONS } from '@/types/process';
|
||||
|
||||
// 품목 유형 기본 옵션 (전체)
|
||||
const DEFAULT_ITEM_TYPE_OPTION = { value: 'all', label: '전체' };
|
||||
// 공정 필터 옵션
|
||||
const PROCESS_FILTER_OPTIONS = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '스크린', label: '스크린' },
|
||||
{ value: '슬릿', label: '슬릿' },
|
||||
{ value: '절곡', label: '절곡' },
|
||||
];
|
||||
|
||||
// 공정 필터에 따른 구분 필터 옵션
|
||||
function getCategoryFilterOptions(processFilter: string): { value: string; label: string }[] {
|
||||
if (processFilter === 'all') {
|
||||
return [{ value: 'all', label: '전체' }];
|
||||
}
|
||||
const categories = PROCESS_CATEGORY_OPTIONS[processFilter];
|
||||
if (!categories || categories.length === 0) {
|
||||
return [{ value: 'all', label: '전체' }];
|
||||
}
|
||||
// 스크린의 경우 '없음'만 있으므로 전체만 표시
|
||||
if (categories.length === 1 && categories[0].value === '없음') {
|
||||
return [{ value: 'all', label: '전체' }];
|
||||
}
|
||||
return [{ value: 'all', label: '전체' }, ...categories];
|
||||
}
|
||||
|
||||
interface RuleModalProps {
|
||||
open: boolean;
|
||||
@@ -50,44 +62,40 @@ interface RuleModalProps {
|
||||
editRule?: ClassificationRule;
|
||||
/** 현재 공정 ID (다른 공정에 이미 배정된 품목 제외용) */
|
||||
processId?: string;
|
||||
/** 현재 공정명 (하단 안내 문구용) */
|
||||
processName?: string;
|
||||
}
|
||||
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule, processId }: RuleModalProps) {
|
||||
// 공통 상태
|
||||
const [registrationType, setRegistrationType] = useState<RuleRegistrationType>(
|
||||
editRule?.registrationType || 'pattern'
|
||||
);
|
||||
const [description, setDescription] = useState(editRule?.description || '');
|
||||
|
||||
// 패턴 규칙용 상태
|
||||
const [ruleType, setRuleType] = useState<RuleType>(editRule?.ruleType || '품목코드');
|
||||
const [matchingType, setMatchingType] = useState<MatchingType>(
|
||||
editRule?.matchingType || 'startsWith'
|
||||
);
|
||||
const [conditionValue, setConditionValue] = useState(editRule?.conditionValue || '');
|
||||
const [priority, setPriority] = useState(editRule?.priority || 10);
|
||||
const [isActive, setIsActive] = useState(editRule?.isActive ?? true);
|
||||
|
||||
// 개별 품목용 상태
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule, processId, processName }: RuleModalProps) {
|
||||
// 검색/필터 상태
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [selectedItemType, setSelectedItemType] = useState('all');
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// 공정/구분 필터 상태
|
||||
const [processFilter, setProcessFilter] = useState('all');
|
||||
const [categoryFilter, setCategoryFilter] = useState('all');
|
||||
|
||||
// 품목 목록 API 상태
|
||||
const [itemList, setItemList] = useState<ItemOption[]>([]);
|
||||
const [isItemsLoading, setIsItemsLoading] = useState(false);
|
||||
|
||||
// 품목 유형 옵션 (common_codes에서 동적 조회)
|
||||
const [itemTypeOptions, setItemTypeOptions] = useState<Array<{ value: string; label: string }>>([DEFAULT_ITEM_TYPE_OPTION]);
|
||||
const [itemTypeOptions, setItemTypeOptions] = useState<Array<{ value: string; label: string }>>([
|
||||
{ value: 'all', label: '전체' },
|
||||
]);
|
||||
|
||||
// 품목 목록 로드 (debounced)
|
||||
// 구분 필터 옵션 (공정 필터에 따라 변경)
|
||||
const categoryFilterOptions = getCategoryFilterOptions(processFilter);
|
||||
|
||||
// 품목 목록 로드
|
||||
const loadItems = useCallback(async (q?: string, itemType?: string) => {
|
||||
setIsItemsLoading(true);
|
||||
const items = await getItemList({
|
||||
q: q || undefined,
|
||||
itemType: itemType === 'all' ? undefined : itemType,
|
||||
size: 1000, // 전체 품목 조회
|
||||
excludeProcessId: processId, // 다른 공정에 이미 배정된 품목 제외
|
||||
size: 1000,
|
||||
excludeProcessId: processId,
|
||||
});
|
||||
setItemList(items);
|
||||
setIsItemsLoading(false);
|
||||
@@ -96,21 +104,14 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId }: Ru
|
||||
// 검색어 유효성 검사 함수
|
||||
const isValidSearchKeyword = (keyword: string): boolean => {
|
||||
if (!keyword || keyword.trim() === '') return false;
|
||||
|
||||
const trimmed = keyword.trim();
|
||||
// 한글이 포함되어 있으면 1자 이상
|
||||
const hasKorean = /[가-힣]/.test(trimmed);
|
||||
if (hasKorean) return trimmed.length >= 1;
|
||||
|
||||
// 영어/숫자만 있으면 2자 이상
|
||||
return trimmed.length >= 2;
|
||||
};
|
||||
|
||||
// 검색어/품목유형 변경 시 API 호출 (debounce)
|
||||
// 검색어 변경 시 API 호출 (debounce)
|
||||
useEffect(() => {
|
||||
if (registrationType !== 'individual') return;
|
||||
|
||||
// 검색어 유효성 검사 - 유효하지 않으면 빈 목록
|
||||
if (!isValidSearchKeyword(searchKeyword)) {
|
||||
setItemList([]);
|
||||
return;
|
||||
@@ -121,30 +122,40 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId }: Ru
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchKeyword, selectedItemType, registrationType, loadItems]);
|
||||
}, [searchKeyword, selectedItemType, loadItems]);
|
||||
|
||||
// 품목유형 변경 시 검색어가 유효하면 재검색
|
||||
useEffect(() => {
|
||||
if (registrationType !== 'individual') return;
|
||||
if (!isValidSearchKeyword(searchKeyword)) return;
|
||||
|
||||
loadItems(searchKeyword, selectedItemType);
|
||||
}, [selectedItemType]);
|
||||
|
||||
// 모달 열릴 때 품목 목록 초기화 + 품목유형 옵션 로드
|
||||
// 모달 열릴 때 초기화
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// 품목유형 옵션 로드 (common_codes에서 동적 조회)
|
||||
// 품목유형 옵션 로드
|
||||
getItemTypeOptions().then((options) => {
|
||||
setItemTypeOptions([DEFAULT_ITEM_TYPE_OPTION, ...options]);
|
||||
setItemTypeOptions([{ value: 'all', label: '전체' }, ...options]);
|
||||
});
|
||||
|
||||
if (registrationType === 'individual') {
|
||||
setItemList([]);
|
||||
setSearchKeyword('');
|
||||
if (editRule) {
|
||||
// 수정 모드: 기존 선택된 품목 ID 설정
|
||||
if (editRule.registrationType === 'individual') {
|
||||
const ids = editRule.conditionValue.split(',').filter(Boolean);
|
||||
setSelectedItemIds(new Set(ids));
|
||||
} else {
|
||||
setSelectedItemIds(new Set());
|
||||
}
|
||||
} else {
|
||||
setSelectedItemIds(new Set());
|
||||
}
|
||||
|
||||
setSearchKeyword('');
|
||||
setSelectedItemType('all');
|
||||
setProcessFilter('all');
|
||||
setCategoryFilter('all');
|
||||
setItemList([]);
|
||||
}
|
||||
}, [open, registrationType]);
|
||||
}, [open, editRule]);
|
||||
|
||||
// 공정 필터 변경 시 구분 필터 리셋
|
||||
useEffect(() => {
|
||||
setCategoryFilter('all');
|
||||
}, [processFilter]);
|
||||
|
||||
// 체크박스 토글
|
||||
const handleToggleItem = (id: string) => {
|
||||
@@ -159,363 +170,174 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule, processId }: Ru
|
||||
});
|
||||
};
|
||||
|
||||
// 전체 선택
|
||||
const handleSelectAll = () => {
|
||||
const allIds = itemList.map((item) => item.id);
|
||||
setSelectedItemIds(new Set(allIds));
|
||||
};
|
||||
|
||||
// 초기화
|
||||
const handleResetSelection = () => {
|
||||
setSelectedItemIds(new Set());
|
||||
};
|
||||
|
||||
// 모달 열릴 때 초기화 또는 수정 데이터 로드
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (editRule) {
|
||||
// 수정 모드: 기존 데이터 로드
|
||||
setRegistrationType(editRule.registrationType);
|
||||
setDescription(editRule.description || '');
|
||||
setRuleType(editRule.ruleType);
|
||||
setMatchingType(editRule.matchingType);
|
||||
setConditionValue(editRule.conditionValue);
|
||||
setPriority(editRule.priority);
|
||||
setIsActive(editRule.isActive);
|
||||
setSearchKeyword('');
|
||||
setSelectedItemType('all');
|
||||
|
||||
// 개별 품목인 경우 선택된 품목 ID 설정
|
||||
if (editRule.registrationType === 'individual') {
|
||||
const ids = editRule.conditionValue.split(',').filter(Boolean);
|
||||
setSelectedItemIds(new Set(ids));
|
||||
} else {
|
||||
setSelectedItemIds(new Set());
|
||||
}
|
||||
} else {
|
||||
// 추가 모드: 초기화 (개별 품목을 디폴트로)
|
||||
setRegistrationType('individual');
|
||||
setDescription('');
|
||||
setRuleType('품목코드');
|
||||
setMatchingType('startsWith');
|
||||
setConditionValue('');
|
||||
setPriority(10);
|
||||
setIsActive(true);
|
||||
setSearchKeyword('');
|
||||
setSelectedItemType('all');
|
||||
setSelectedItemIds(new Set());
|
||||
}
|
||||
}
|
||||
}, [open, editRule]);
|
||||
|
||||
// 저장
|
||||
const handleSubmit = () => {
|
||||
if (registrationType === 'pattern') {
|
||||
if (!conditionValue.trim()) {
|
||||
alert('조건 값을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (selectedItemIds.size === 0) {
|
||||
alert('품목을 최소 1개 이상 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (selectedItemIds.size === 0) {
|
||||
alert('품목을 최소 1개 이상 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 개별 품목의 경우 conditionValue에 품목코드들을 저장
|
||||
const finalConditionValue =
|
||||
registrationType === 'individual'
|
||||
? Array.from(selectedItemIds).join(',')
|
||||
: conditionValue.trim();
|
||||
const finalConditionValue = Array.from(selectedItemIds).join(',');
|
||||
|
||||
onAdd({
|
||||
registrationType,
|
||||
ruleType: registrationType === 'individual' ? '품목코드' : ruleType,
|
||||
matchingType: registrationType === 'individual' ? 'equals' : matchingType,
|
||||
registrationType: 'individual',
|
||||
ruleType: '품목코드',
|
||||
matchingType: 'equals',
|
||||
conditionValue: finalConditionValue,
|
||||
priority: registrationType === 'individual' ? 10 : priority,
|
||||
description: description.trim() || undefined,
|
||||
isActive: registrationType === 'individual' ? true : isActive,
|
||||
priority: 10,
|
||||
description: undefined,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setRegistrationType('pattern');
|
||||
setDescription('');
|
||||
setRuleType('품목코드');
|
||||
setMatchingType('startsWith');
|
||||
setConditionValue('');
|
||||
setPriority(10);
|
||||
setIsActive(true);
|
||||
// Reset
|
||||
setSearchKeyword('');
|
||||
setSelectedItemType('all');
|
||||
setSelectedItemIds(new Set());
|
||||
setProcessFilter('all');
|
||||
setCategoryFilter('all');
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={registrationType === 'individual' ? 'max-w-xl max-h-[90vh] flex flex-col' : 'max-w-md'}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editRule ? '규칙 수정' : '규칙 추가'}</DialogTitle>
|
||||
<DialogTitle>공정 품목 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4 overflow-y-auto flex-1">
|
||||
{/* 등록 방식 */}
|
||||
<div className="space-y-3">
|
||||
<Label>등록 방식 *</Label>
|
||||
<RadioGroup
|
||||
value={registrationType}
|
||||
onValueChange={(v) => setRegistrationType(v as RuleRegistrationType)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="pattern" id="pattern" />
|
||||
<Label htmlFor="pattern" className="font-normal">
|
||||
패턴 규칙 (코드/명칭 기반 자동 분류)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="individual" id="individual" />
|
||||
<Label htmlFor="individual" className="font-normal">
|
||||
개별 품목 (특정 품목 직접 지정)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{/* 검색 입력 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
placeholder="품목코드 또는 품목명으로 검색..."
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 패턴 규칙 UI */}
|
||||
{registrationType === 'pattern' && (
|
||||
<>
|
||||
{/* 규칙 유형 */}
|
||||
<div className="space-y-2">
|
||||
<Label>규칙 유형 *</Label>
|
||||
<Select value={ruleType} onValueChange={(v) => setRuleType(v as RuleType)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{RULE_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* 카운트 + 필터 행 */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{isItemsLoading ? (
|
||||
'로딩 중...'
|
||||
) : (
|
||||
<>
|
||||
총 {itemList.length}건{' '}
|
||||
{selectedItemIds.size > 0 && (
|
||||
<span className="text-primary font-medium">
|
||||
{selectedItemIds.size}건 선택됨
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={processFilter} onValueChange={setProcessFilter}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectValue placeholder="공정" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROCESS_FILTER_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
key={`category-${processFilter}`}
|
||||
value={categoryFilter}
|
||||
onValueChange={setCategoryFilter}
|
||||
disabled={categoryFilterOptions.length <= 1}
|
||||
>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectValue placeholder="구분" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryFilterOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 매칭 방식 */}
|
||||
<div className="space-y-2">
|
||||
<Label>매칭 방식 *</Label>
|
||||
<Select
|
||||
value={matchingType}
|
||||
onValueChange={(v) => setMatchingType(v as MatchingType)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MATCHING_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 조건 값 */}
|
||||
<div className="space-y-2">
|
||||
<Label>조건 값 *</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={conditionValue}
|
||||
onChange={(e) => setConditionValue(e.target.value)}
|
||||
placeholder="예: SCR-, E-, STEEL-"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button variant="outline" size="icon">
|
||||
<Search className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter 키를 누르거나 검색 버튼을 클릭하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 우선순위 - 패턴 규칙에서만 표시 */}
|
||||
<div className="space-y-2">
|
||||
<Label>우선순위</Label>
|
||||
<QuantityInput
|
||||
value={priority}
|
||||
onChange={(value) => setPriority(value ?? 1)}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">낮을수록 먼저 적용됩니다</p>
|
||||
</div>
|
||||
|
||||
{/* 설명 - 패턴 규칙 */}
|
||||
<div className="space-y-2">
|
||||
<Label>설명</Label>
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="규칙에 대한 설명"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 활성 상태 - 패턴 규칙에서만 표시 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>활성 상태</Label>
|
||||
<Switch checked={isActive} onCheckedChange={setIsActive} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 개별 품목 UI - 기획서 기준 */}
|
||||
{registrationType === 'individual' && (
|
||||
<>
|
||||
{/* 설명 (선택) */}
|
||||
<div className="space-y-2">
|
||||
<Label>설명 (선택)</Label>
|
||||
<Input
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="이 품목 그룹에 대한 설명"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 품목 검색 + 품목 유형 필터 */}
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<Label className="text-sm">품목 검색</Label>
|
||||
<div className="relative mt-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
placeholder="품목코드 또는 품목명으로 검색..."
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Label className="text-sm">품목 유형</Label>
|
||||
<Select value={selectedItemType} onValueChange={setSelectedItemType}>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{itemTypeOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품목 목록 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Package className="h-4 w-4" />
|
||||
<span>
|
||||
{isItemsLoading ? (
|
||||
'로딩 중...'
|
||||
) : (
|
||||
<>품목 목록 ({itemList.length}개) | 선택됨 ({selectedItemIds.size}개)</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
className="text-xs h-7"
|
||||
disabled={isItemsLoading || itemList.length === 0}
|
||||
>
|
||||
전체 선택
|
||||
</Button>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleResetSelection}
|
||||
className="text-xs h-7"
|
||||
disabled={selectedItemIds.size === 0}
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품목 테이블 */}
|
||||
<div className="border rounded-lg max-h-[280px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[40px]"></TableHead>
|
||||
<TableHead>품목코드</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
<TableHead className="w-[80px]">품목유형</TableHead>
|
||||
{/* 품목 테이블 */}
|
||||
<div className="border rounded-lg max-h-[340px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[40px]"></TableHead>
|
||||
<TableHead className="w-[80px]">품목유형</TableHead>
|
||||
<TableHead>품목코드</TableHead>
|
||||
<TableHead>품목명</TableHead>
|
||||
{/* TODO: API에서 process_name, process_category 필드 지원 후 실제 데이터 표시 */}
|
||||
<TableHead className="w-[80px]">공정</TableHead>
|
||||
<TableHead className="w-[80px]">구분</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isItemsLoading ? (
|
||||
<TableRow key="loading">
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
품목 목록을 불러오는 중...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : itemList.length === 0 ? (
|
||||
<TableRow key="empty">
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground py-8">
|
||||
{searchKeyword.trim() === ''
|
||||
? '품목을 검색해주세요 (한글 1자 이상, 영문 2자 이상)'
|
||||
: '검색 결과가 없습니다'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
itemList.map((item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleToggleItem(item.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItemIds.has(item.id)}
|
||||
onCheckedChange={() => handleToggleItem(item.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
<TableCell className="font-medium">{item.code}</TableCell>
|
||||
<TableCell>{item.fullName}</TableCell>
|
||||
{/* TODO: API 지원 후 item.processName / item.processCategory 표시 */}
|
||||
<TableCell className="text-muted-foreground">-</TableCell>
|
||||
<TableCell className="text-muted-foreground">-</TableCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isItemsLoading ? (
|
||||
<TableRow key="loading">
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
품목 목록을 불러오는 중...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : itemList.length === 0 ? (
|
||||
<TableRow key="empty">
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
{searchKeyword.trim() === ''
|
||||
? '품목을 검색해주세요 (한글 1자 이상, 영문 2자 이상)'
|
||||
: '검색 결과가 없습니다'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
itemList.map((item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleToggleItem(item.id)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItemIds.has(item.id)}
|
||||
onCheckedChange={() => handleToggleItem(item.id)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{item.code}</TableCell>
|
||||
<TableCell>{item.fullName}</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 안내 문구 */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
이 공정에 배정할 품목을 선택하세요. 다른 공정에 이미 배정된 품목은 표시되지 않습니다.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{/* 안내 문구 */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
선택 후 저장하시면 선택한 품목들이{' '}
|
||||
<span className="font-medium text-foreground">
|
||||
{processName || '해당'}
|
||||
</span>{' '}
|
||||
공정으로 변경됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>{editRule ? '수정' : '추가'}</Button>
|
||||
<Button onClick={handleSubmit}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
Reference in New Issue
Block a user