- 파일 업로드 API에 field_key, file_id 파라미터 추가 - ItemMaster 타입에 files 필드 추가 (새 API 구조 지원) - DynamicItemForm에서 files 객체 파싱 로직 추가 - 시방서/인정서 파일 UI 개선: 파일명 표시 + 다운로드/수정/삭제 버튼 - 기존 API 구조와 새 API 구조 모두 지원 (폴백 처리) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
349 lines
16 KiB
TypeScript
349 lines
16 KiB
TypeScript
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Plus, Trash2 } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import type { ItemPage, ItemSection } from '@/contexts/ItemMasterContext';
|
|
|
|
export interface ConditionalFieldConfig {
|
|
fieldKey: string;
|
|
expectedValue: string;
|
|
targetFieldIds?: string[];
|
|
targetSectionIds?: string[];
|
|
}
|
|
|
|
interface ConditionalDisplayUIProps {
|
|
// States
|
|
newFieldConditionEnabled: boolean;
|
|
setNewFieldConditionEnabled: (value: boolean) => void;
|
|
newFieldConditionTargetType: 'field' | 'section';
|
|
setNewFieldConditionTargetType: (value: 'field' | 'section') => void;
|
|
newFieldConditionFields: ConditionalFieldConfig[];
|
|
setNewFieldConditionFields: (value: ConditionalFieldConfig[] | ((prev: ConditionalFieldConfig[]) => ConditionalFieldConfig[])) => void;
|
|
tempConditionValue: string;
|
|
setTempConditionValue: (value: string) => void;
|
|
|
|
// Context data
|
|
newFieldKey: string;
|
|
newFieldInputType: 'textbox' | 'dropdown' | 'checkbox' | 'number' | 'date' | 'textarea';
|
|
selectedPage: ItemPage | null;
|
|
selectedSectionForField: ItemSection | null;
|
|
editingFieldId: number | null;
|
|
|
|
// Constants
|
|
INPUT_TYPE_OPTIONS: Array<{value: string; label: string}>;
|
|
}
|
|
|
|
export function ConditionalDisplayUI({
|
|
newFieldConditionEnabled,
|
|
setNewFieldConditionEnabled,
|
|
newFieldConditionTargetType,
|
|
setNewFieldConditionTargetType,
|
|
newFieldConditionFields,
|
|
setNewFieldConditionFields,
|
|
tempConditionValue,
|
|
setTempConditionValue,
|
|
newFieldKey,
|
|
newFieldInputType,
|
|
selectedPage,
|
|
selectedSectionForField,
|
|
editingFieldId,
|
|
INPUT_TYPE_OPTIONS,
|
|
}: ConditionalDisplayUIProps) {
|
|
|
|
const getPlaceholderText = () => {
|
|
switch (newFieldInputType) {
|
|
case 'dropdown': return '드롭다운 옵션값을 입력하세요';
|
|
case 'checkbox': return '체크박스 상태값(true/false)을 입력하세요';
|
|
case 'textbox': return '텍스트 값을 입력하세요 (예: "제품", "부품")';
|
|
case 'number': return '숫자 값을 입력하세요 (예: 100, 200)';
|
|
case 'date': return '날짜 값을 입력하세요 (예: 2025-01-01)';
|
|
case 'textarea': return '텍스트 값을 입력하세요';
|
|
default: return '값을 입력하세요';
|
|
}
|
|
};
|
|
|
|
const handleAddCondition = () => {
|
|
if (!tempConditionValue) {
|
|
toast.error('조건값을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (newFieldConditionFields.find(f => f.expectedValue === tempConditionValue)) {
|
|
toast.error('이미 추가된 조건값입니다.');
|
|
return;
|
|
}
|
|
|
|
setNewFieldConditionFields(prev => [...prev, {
|
|
fieldKey: newFieldKey,
|
|
expectedValue: tempConditionValue,
|
|
targetFieldIds: newFieldConditionTargetType === 'field' ? [] : undefined,
|
|
targetSectionIds: newFieldConditionTargetType === 'section' ? [] : undefined,
|
|
}]);
|
|
setTempConditionValue('');
|
|
toast.success('조건값이 추가되었습니다. 표시할 대상을 선택하세요.');
|
|
};
|
|
|
|
const handleRemoveCondition = (index: number) => {
|
|
setNewFieldConditionFields(prev => prev.filter((_, i) => i !== index));
|
|
toast.success('조건이 제거되었습니다.');
|
|
};
|
|
|
|
// selectedSectionForField는 이미 ItemSection 객체이므로 바로 사용
|
|
// 신규 ItemField 타입: id는 number
|
|
const availableFields = selectedSectionForField?.fields?.filter(f => f.id !== editingFieldId) || [];
|
|
// 신규 ItemSection 타입: section_type은 'BASIC' | 'BOM' | 'CUSTOM'
|
|
// 2025-12-03: BOM 섹션도 조건부 표시 대상으로 포함 (체크박스 → BOM 섹션 연결용)
|
|
const availableSections = selectedPage?.sections || [];
|
|
|
|
return (
|
|
<div className="border-t pt-4 space-y-3">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Switch checked={newFieldConditionEnabled} onCheckedChange={setNewFieldConditionEnabled} />
|
|
<Label className="text-base">조건부 표시 설정</Label>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground pl-8">
|
|
이 항목의 값에 따라 다른 항목이나 섹션을 동적으로 표시/숨김 처리합니다
|
|
</p>
|
|
</div>
|
|
|
|
{newFieldConditionEnabled && selectedSectionForField && selectedPage && (
|
|
<div className="space-y-4 pl-6 pt-3 border-l-2 border-blue-200">
|
|
{/* 대상 타입 선택 */}
|
|
<div className="space-y-2 bg-blue-50 p-3 rounded">
|
|
<Label className="text-sm font-semibold">조건이 성립하면 무엇을 표시할까요?</Label>
|
|
<div className="flex gap-4 pl-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
checked={newFieldConditionTargetType === 'field'}
|
|
onChange={() => setNewFieldConditionTargetType('field')}
|
|
className="cursor-pointer"
|
|
/>
|
|
<span className="text-sm">추가 항목들 표시</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
checked={newFieldConditionTargetType === 'section'}
|
|
onChange={() => setNewFieldConditionTargetType('section')}
|
|
className="cursor-pointer"
|
|
/>
|
|
<span className="text-sm">전체 섹션 표시</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 일반항목용 조건 설정 */}
|
|
{newFieldConditionTargetType === 'field' && (
|
|
<div className="space-y-4">
|
|
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
|
|
<p className="text-xs text-yellow-800">
|
|
<strong>💡 사용 방법:</strong><br/>
|
|
1. 조건값을 추가하고 각 조건값마다 표시할 항목들을 선택합니다<br/>
|
|
2. 사용자가 이 항목에서 특정 값을 선택하면<br/>
|
|
3. 해당 조건값에 연결된 항목들만 동적으로 표시됩니다
|
|
</p>
|
|
</div>
|
|
|
|
{/* 추가된 조건 목록 */}
|
|
{newFieldConditionFields.length > 0 && (
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">등록된 조건 목록</Label>
|
|
{newFieldConditionFields.map((condition, conditionIndex) => (
|
|
<div key={conditionIndex} className="border border-blue-300 rounded-lg p-4 bg-blue-50 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<span className="text-sm font-bold text-blue-900">
|
|
조건값: "{condition.expectedValue}"
|
|
</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveCondition(conditionIndex)}
|
|
className="h-8 w-8 p-0 hover:bg-red-100"
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-600" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 이 조건값일 때 표시할 항목들 선택 */}
|
|
{availableFields.length > 0 ? (
|
|
<div className="space-y-2 pl-3 border-l-2 border-blue-300">
|
|
<Label className="text-xs font-semibold text-blue-800">
|
|
이 값일 때 표시할 항목들 ({condition.targetFieldIds?.length || 0}개 선택됨):
|
|
</Label>
|
|
<div className="space-y-1 max-h-40 overflow-y-auto bg-white rounded p-2">
|
|
{availableFields.map((field, fieldIdx) => {
|
|
const fieldIdStr = String(field.id);
|
|
return (
|
|
<label key={`condition-${conditionIndex}-field-${field.id}-${fieldIdx}`} className="flex items-center gap-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={condition.targetFieldIds?.includes(fieldIdStr) || false}
|
|
onChange={(e) => {
|
|
const newFields = [...newFieldConditionFields];
|
|
if (e.target.checked) {
|
|
newFields[conditionIndex].targetFieldIds = [
|
|
...(newFields[conditionIndex].targetFieldIds || []),
|
|
fieldIdStr
|
|
];
|
|
} else {
|
|
newFields[conditionIndex].targetFieldIds =
|
|
(newFields[conditionIndex].targetFieldIds || []).filter(id => id !== fieldIdStr);
|
|
}
|
|
setNewFieldConditionFields(newFields);
|
|
}}
|
|
className="cursor-pointer"
|
|
/>
|
|
<span className="text-xs flex-1">{field.field_name}</span>
|
|
<Badge variant="outline" className="text-xs">
|
|
{INPUT_TYPE_OPTIONS.find(o => o.value === field.field_type)?.label || field.field_type}
|
|
</Badge>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground pl-3">
|
|
현재 섹션에 표시할 수 있는 다른 항목이 없습니다.
|
|
</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 새 조건 추가 */}
|
|
<div className="space-y-2 p-3 bg-gray-50 rounded border border-gray-200">
|
|
<Label className="text-sm font-semibold">새 조건 추가</Label>
|
|
<p className="text-xs text-muted-foreground">{getPlaceholderText()}</p>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={tempConditionValue}
|
|
onChange={(e) => setTempConditionValue(e.target.value)}
|
|
placeholder="조건값 입력"
|
|
className="flex-1"
|
|
onKeyDown={(e) => e.key === 'Enter' && handleAddCondition()}
|
|
/>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={handleAddCondition}
|
|
>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
조건 추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 섹션용 조건 설정 */}
|
|
{newFieldConditionTargetType === 'section' && (
|
|
<div className="space-y-4">
|
|
<div className="bg-yellow-50 p-3 rounded border border-yellow-200">
|
|
<p className="text-xs text-yellow-800">
|
|
<strong>💡 사용 방법:</strong><br/>
|
|
1. 조건값을 추가하고 각 조건값마다 표시할 섹션들을 선택합니다<br/>
|
|
2. 사용자가 이 항목에서 특정 값을 선택하면<br/>
|
|
3. 해당 조건값에 연결된 섹션들만 동적으로 표시됩니다
|
|
</p>
|
|
</div>
|
|
|
|
{/* 추가된 조건 목록 */}
|
|
{newFieldConditionFields.length > 0 && (
|
|
<div className="space-y-3">
|
|
<Label className="text-sm font-semibold">등록된 조건 목록</Label>
|
|
{newFieldConditionFields.map((condition, conditionIndex) => (
|
|
<div key={conditionIndex} className="border border-blue-300 rounded-lg p-4 bg-blue-50 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<span className="text-sm font-bold text-blue-900">
|
|
조건값: "{condition.expectedValue}"
|
|
</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRemoveCondition(conditionIndex)}
|
|
className="h-8 w-8 p-0 hover:bg-red-100"
|
|
>
|
|
<Trash2 className="h-4 w-4 text-red-600" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 이 조건값일 때 표시할 섹션들 선택 */}
|
|
<div className="space-y-2 pl-3 border-l-2 border-blue-300">
|
|
<Label className="text-xs font-semibold text-blue-800">
|
|
이 값일 때 표시할 섹션들 ({condition.targetSectionIds?.length || 0}개 선택됨):
|
|
</Label>
|
|
<div className="space-y-1 max-h-40 overflow-y-auto bg-white rounded p-2">
|
|
{availableSections.map((section, sectionIdx) => {
|
|
const sectionIdStr = String(section.id);
|
|
return (
|
|
<label key={`condition-${conditionIndex}-section-${section.id}-${sectionIdx}`} className="flex items-center gap-2 p-2 hover:bg-gray-50 rounded cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={condition.targetSectionIds?.includes(sectionIdStr) || false}
|
|
onChange={(e) => {
|
|
const newFields = [...newFieldConditionFields];
|
|
if (e.target.checked) {
|
|
newFields[conditionIndex].targetSectionIds = [
|
|
...(newFields[conditionIndex].targetSectionIds || []),
|
|
sectionIdStr
|
|
];
|
|
} else {
|
|
newFields[conditionIndex].targetSectionIds =
|
|
(newFields[conditionIndex].targetSectionIds || []).filter(id => id !== sectionIdStr);
|
|
}
|
|
setNewFieldConditionFields(newFields);
|
|
}}
|
|
className="cursor-pointer"
|
|
/>
|
|
<span className="text-xs flex-1">{section.title}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 새 조건 추가 */}
|
|
<div className="space-y-2 p-3 bg-gray-50 rounded border border-gray-200">
|
|
<Label className="text-sm font-semibold">새 조건 추가</Label>
|
|
<p className="text-xs text-muted-foreground">{getPlaceholderText()}</p>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={tempConditionValue}
|
|
onChange={(e) => setTempConditionValue(e.target.value)}
|
|
placeholder="조건값 입력"
|
|
className="flex-1"
|
|
onKeyDown={(e) => e.key === 'Enter' && handleAddCondition()}
|
|
/>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
onClick={handleAddCondition}
|
|
>
|
|
<Plus className="h-4 w-4 mr-1" />
|
|
조건 추가
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|