feat: 공정관리 규칙 UI 개선 및 품질인정심사시스템 경로 이동
- 자동 분류 규칙 리스트 UI 기획서대로 수정 - 번호, 개별 품목 지정 표시, 배지, 수정/삭제 버튼 - 규칙 수정 기능 추가 (기존 품목 체크 상태 유지) - 개별 품목 모달 UI 기획서대로 재구현 - 검색, 품목유형 필터, 체크박스 테이블 - 품질인정심사시스템 경로 이동 (dev → quality/qms) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { X, Save, Plus, Wrench, Trash2, Loader2 } from 'lucide-react';
|
||||
import { X, Save, Plus, Wrench, Trash2, Loader2, Pencil } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -74,25 +74,53 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
|
||||
// 규칙 모달 상태
|
||||
const [ruleModalOpen, setRuleModalOpen] = useState(false);
|
||||
const [editingRule, setEditingRule] = useState<ClassificationRule | undefined>(undefined);
|
||||
|
||||
// 규칙 추가
|
||||
const handleAddRule = useCallback(
|
||||
// 규칙 추가/수정
|
||||
const handleSaveRule = useCallback(
|
||||
(ruleData: Omit<ClassificationRule, 'id' | 'createdAt'>) => {
|
||||
const newRule: ClassificationRule = {
|
||||
...ruleData,
|
||||
id: `rule-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setClassificationRules((prev) => [...prev, newRule]);
|
||||
if (editingRule) {
|
||||
// 수정 모드
|
||||
setClassificationRules((prev) =>
|
||||
prev.map((r) =>
|
||||
r.id === editingRule.id
|
||||
? { ...r, ...ruleData }
|
||||
: r
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// 추가 모드
|
||||
const newRule: ClassificationRule = {
|
||||
...ruleData,
|
||||
id: `rule-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setClassificationRules((prev) => [...prev, newRule]);
|
||||
}
|
||||
setEditingRule(undefined);
|
||||
},
|
||||
[]
|
||||
[editingRule]
|
||||
);
|
||||
|
||||
// 규칙 수정 모달 열기
|
||||
const handleEditRule = useCallback((rule: ClassificationRule) => {
|
||||
setEditingRule(rule);
|
||||
setRuleModalOpen(true);
|
||||
}, []);
|
||||
|
||||
// 규칙 삭제
|
||||
const handleDeleteRule = useCallback((ruleId: string) => {
|
||||
setClassificationRules((prev) => prev.filter((r) => r.id !== ruleId));
|
||||
}, []);
|
||||
|
||||
// 모달 닫기
|
||||
const handleModalClose = useCallback((open: boolean) => {
|
||||
setRuleModalOpen(open);
|
||||
if (!open) {
|
||||
setEditingRule(undefined);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 제출
|
||||
const handleSubmit = async () => {
|
||||
if (!processName.trim()) {
|
||||
@@ -273,45 +301,83 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{classificationRules.map((rule) => (
|
||||
<div
|
||||
key={rule.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge variant={rule.isActive ? 'default' : 'secondary'}>
|
||||
{rule.isActive ? '활성' : '비활성'}
|
||||
</Badge>
|
||||
<div>
|
||||
<div className="font-medium">
|
||||
{rule.ruleType}{' '}
|
||||
{
|
||||
MATCHING_TYPE_OPTIONS.find(
|
||||
(o) => o.value === rule.matchingType
|
||||
)?.label
|
||||
}{' '}
|
||||
"{rule.conditionValue}"
|
||||
</div>
|
||||
{rule.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{rule.description}
|
||||
{classificationRules.map((rule, index) => {
|
||||
// 개별 품목인 경우 품목 개수 계산
|
||||
const isIndividual = rule.registrationType === 'individual';
|
||||
const itemCount = isIndividual
|
||||
? rule.conditionValue.split(',').filter(Boolean).length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={rule.id}
|
||||
className="flex items-start justify-between p-4 border rounded-lg"
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
{/* 번호 */}
|
||||
<span className="text-muted-foreground font-medium mt-0.5">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
{/* 제목 */}
|
||||
<div className="font-medium">
|
||||
{isIndividual ? (
|
||||
<>개별 품목 지정 - {itemCount}개 품목</>
|
||||
) : (
|
||||
<>
|
||||
{rule.ruleType}{' '}
|
||||
{
|
||||
MATCHING_TYPE_OPTIONS.find(
|
||||
(o) => o.value === rule.matchingType
|
||||
)?.label
|
||||
}{' '}
|
||||
"{rule.conditionValue}"
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 뱃지 + 우선순위 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{isIndividual
|
||||
? `${itemCount}개 품목 배정됨`
|
||||
: rule.isActive
|
||||
? '활성'
|
||||
: '비활성'}
|
||||
</Badge>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
우선순위: {rule.priority}
|
||||
</span>
|
||||
</div>
|
||||
{/* 설명 */}
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{isIndividual
|
||||
? `직접 선택한 품목 ${itemCount}개`
|
||||
: rule.description || ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 수정/삭제 버튼 */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleEditRule(rule)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteRule(rule.id)}
|
||||
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">우선순위: {rule.priority}</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteRule(rule.id)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -381,11 +447,12 @@ export function ProcessForm({ mode, initialData }: ProcessFormProps) {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 규칙 추가 모달 */}
|
||||
{/* 규칙 추가/수정 모달 */}
|
||||
<RuleModal
|
||||
open={ruleModalOpen}
|
||||
onOpenChange={setRuleModalOpen}
|
||||
onAdd={handleAddRule}
|
||||
onOpenChange={handleModalClose}
|
||||
onAdd={handleSaveRule}
|
||||
editRule={editingRule}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -13,6 +13,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -20,7 +21,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Search } from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Search, Package } from 'lucide-react';
|
||||
import type {
|
||||
ClassificationRule,
|
||||
RuleRegistrationType,
|
||||
@@ -29,6 +38,29 @@ import type {
|
||||
} from '@/types/process';
|
||||
import { RULE_TYPE_OPTIONS, MATCHING_TYPE_OPTIONS } from '@/types/process';
|
||||
|
||||
// 품목 유형 옵션
|
||||
const ITEM_TYPE_OPTIONS = [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '제품', label: '제품' },
|
||||
{ value: '반제품', label: '반제품' },
|
||||
{ value: '원자재', label: '원자재' },
|
||||
{ value: '부자재', label: '부자재' },
|
||||
];
|
||||
|
||||
// Mock 품목 데이터 (추후 API 연동)
|
||||
const MOCK_ITEMS = [
|
||||
{ code: '스크린원단-0.3T', name: '스크린원단', type: '부자재' },
|
||||
{ code: '스크린원단-0.5T', name: '스크린원단', type: '부자재' },
|
||||
{ code: '슬랫-75mm', name: '슬랫', type: '부자재' },
|
||||
{ code: '슬랫-100mm', name: '슬랫', type: '부자재' },
|
||||
{ code: '가이드레일-60×60', name: '가이드레일', type: '부자재' },
|
||||
{ code: '가이드레일-80×80', name: '가이드레일', type: '부자재' },
|
||||
{ code: 'SCR-001', name: '스크린A', type: '제품' },
|
||||
{ code: 'SCR-002', name: '스크린B', type: '제품' },
|
||||
{ code: 'STEEL-001', name: '철판A', type: '원자재' },
|
||||
{ code: 'STEEL-002', name: '철판B', type: '원자재' },
|
||||
];
|
||||
|
||||
interface RuleModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -37,54 +69,150 @@ interface RuleModalProps {
|
||||
}
|
||||
|
||||
export function RuleModal({ open, onOpenChange, onAdd, editRule }: 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 [description, setDescription] = useState(editRule?.description || '');
|
||||
const [isActive, setIsActive] = useState(editRule?.isActive ?? true);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!conditionValue.trim()) {
|
||||
alert('조건 값을 입력해주세요.');
|
||||
return;
|
||||
// 개별 품목용 상태
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [selectedItemType, setSelectedItemType] = useState('all');
|
||||
const [selectedItemCodes, setSelectedItemCodes] = useState<Set<string>>(new Set());
|
||||
|
||||
// 필터된 품목 목록
|
||||
const filteredItems = MOCK_ITEMS.filter((item) => {
|
||||
const matchesType = selectedItemType === 'all' || item.type === selectedItemType;
|
||||
const matchesSearch =
|
||||
!searchKeyword ||
|
||||
item.code.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||
item.name.toLowerCase().includes(searchKeyword.toLowerCase());
|
||||
return matchesType && matchesSearch;
|
||||
});
|
||||
|
||||
// 체크박스 토글
|
||||
const handleToggleItem = (code: string) => {
|
||||
setSelectedItemCodes((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(code)) {
|
||||
newSet.delete(code);
|
||||
} else {
|
||||
newSet.add(code);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// 전체 선택
|
||||
const handleSelectAll = () => {
|
||||
const allCodes = filteredItems.map((item) => item.code);
|
||||
setSelectedItemCodes(new Set(allCodes));
|
||||
};
|
||||
|
||||
// 초기화
|
||||
const handleResetSelection = () => {
|
||||
setSelectedItemCodes(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');
|
||||
|
||||
// 개별 품목인 경우 선택된 품목 코드 설정
|
||||
if (editRule.registrationType === 'individual') {
|
||||
const codes = editRule.conditionValue.split(',').filter(Boolean);
|
||||
setSelectedItemCodes(new Set(codes));
|
||||
} else {
|
||||
setSelectedItemCodes(new Set());
|
||||
}
|
||||
} else {
|
||||
// 추가 모드: 초기화
|
||||
setRegistrationType('pattern');
|
||||
setDescription('');
|
||||
setRuleType('품목코드');
|
||||
setMatchingType('startsWith');
|
||||
setConditionValue('');
|
||||
setPriority(10);
|
||||
setIsActive(true);
|
||||
setSearchKeyword('');
|
||||
setSelectedItemType('all');
|
||||
setSelectedItemCodes(new Set());
|
||||
}
|
||||
}
|
||||
}, [open, editRule]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (registrationType === 'pattern') {
|
||||
if (!conditionValue.trim()) {
|
||||
alert('조건 값을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (selectedItemCodes.size === 0) {
|
||||
alert('품목을 최소 1개 이상 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 개별 품목의 경우 conditionValue에 품목코드들을 저장
|
||||
const finalConditionValue =
|
||||
registrationType === 'individual'
|
||||
? Array.from(selectedItemCodes).join(',')
|
||||
: conditionValue.trim();
|
||||
|
||||
onAdd({
|
||||
registrationType,
|
||||
ruleType,
|
||||
matchingType,
|
||||
conditionValue: conditionValue.trim(),
|
||||
priority,
|
||||
ruleType: registrationType === 'individual' ? '품목코드' : ruleType,
|
||||
matchingType: registrationType === 'individual' ? 'equals' : matchingType,
|
||||
conditionValue: finalConditionValue,
|
||||
priority: registrationType === 'individual' ? 10 : priority,
|
||||
description: description.trim() || undefined,
|
||||
isActive,
|
||||
isActive: registrationType === 'individual' ? true : isActive,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setRegistrationType('pattern');
|
||||
setDescription('');
|
||||
setRuleType('품목코드');
|
||||
setMatchingType('startsWith');
|
||||
setConditionValue('');
|
||||
setPriority(10);
|
||||
setDescription('');
|
||||
setIsActive(true);
|
||||
setSearchKeyword('');
|
||||
setSelectedItemType('all');
|
||||
setSelectedItemCodes(new Set());
|
||||
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogContent className={registrationType === 'individual' ? 'max-w-xl' : 'max-w-md'}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>규칙 추가</DialogTitle>
|
||||
<DialogTitle>{editRule ? '규칙 수정' : '규칙 추가'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="space-y-4 py-4">
|
||||
{/* 등록 방식 */}
|
||||
<div className="space-y-3">
|
||||
<Label>등록 방식 *</Label>
|
||||
@@ -107,100 +235,228 @@ export function RuleModal({ open, onOpenChange, onAdd, editRule }: RuleModalProp
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* 규칙 유형 */}
|
||||
<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>
|
||||
{/* 패턴 규칙 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="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>
|
||||
<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>
|
||||
<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>
|
||||
<Input
|
||||
type="number"
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(Number(e.target.value))}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-24"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">낮을수록 먼저 적용됩니다</p>
|
||||
</div>
|
||||
{/* 우선순위 - 패턴 규칙에서만 표시 */}
|
||||
<div className="space-y-2">
|
||||
<Label>우선순위</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(Number(e.target.value))}
|
||||
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="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>
|
||||
{/* 활성 상태 - 패턴 규칙에서만 표시 */}
|
||||
<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>
|
||||
{ITEM_TYPE_OPTIONS.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>
|
||||
품목 목록 ({filteredItems.length}개) | 선택됨 ({selectedItemCodes.size}개)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
className="text-xs h-7"
|
||||
>
|
||||
전체 선택
|
||||
</Button>
|
||||
<span className="text-muted-foreground">|</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleResetSelection}
|
||||
className="text-xs h-7"
|
||||
>
|
||||
초기화
|
||||
</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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredItems.map((item) => (
|
||||
<TableRow
|
||||
key={item.code}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => handleToggleItem(item.code)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItemCodes.has(item.code)}
|
||||
onCheckedChange={() => handleToggleItem(item.code)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{item.code}</TableCell>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.type}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{filteredItems.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">
|
||||
검색 결과가 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 안내 문구 */}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
이 공정에 배정할 품목을 선택하세요. 다른 공정에 이미 배정된 품목은 표시되지 않습니다.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>추가</Button>
|
||||
<Button onClick={handleSubmit}>{editRule ? '수정' : '추가'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ export { QuoteManagementClient } from './QuoteManagementClient';
|
||||
|
||||
// 기존 컴포넌트
|
||||
export { default as QuoteDocument } from './QuoteDocument';
|
||||
export { default as QuoteRegistration } from './QuoteRegistration';
|
||||
export { default as QuoteCalculationReport } from './QuoteCalculationReport';
|
||||
export { default as PurchaseOrderDocument } from './PurchaseOrderDocument';
|
||||
export { QuoteRegistration, INITIAL_QUOTE_FORM } from './QuoteRegistration';
|
||||
export { QuoteCalculationReport } from './QuoteCalculationReport';
|
||||
export { PurchaseOrderDocument } from './PurchaseOrderDocument';
|
||||
|
||||
// 타입
|
||||
export type {
|
||||
|
||||
Reference in New Issue
Block a user