refactor: 품목관리 시스템 리팩토링 및 Sales 페이지 추가

DynamicItemForm 개선:
- 품목코드 자동생성 기능 추가
- 조건부 표시 로직 개선
- 불필요한 컴포넌트 정리 (DynamicField, DynamicSection 등)
- 타입 시스템 단순화

새로운 기능:
- Sales 페이지 마이그레이션 (견적관리, 거래처관리)
- 공통 컴포넌트 추가 (atoms, molecules, organisms, templates)

문서화:
- 구현 문서 및 참조 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-04 12:48:41 +09:00
parent 0552b02ba9
commit 3be5714805
73 changed files with 9318 additions and 4353 deletions

View File

@@ -95,7 +95,8 @@ export function ConditionalDisplayUI({
// 신규 ItemField 타입: id는 number
const availableFields = selectedSectionForField?.fields?.filter(f => f.id !== editingFieldId) || [];
// 신규 ItemSection 타입: section_type은 'BASIC' | 'BOM' | 'CUSTOM'
const availableSections = selectedPage?.sections.filter(s => s.section_type !== 'BOM') || [];
// 2025-12-03: BOM 섹션도 조건부 표시 대상으로 포함 (체크박스 → BOM 섹션 연결용)
const availableSections = selectedPage?.sections || [];
return (
<div className="border-t pt-4 space-y-3">

View File

@@ -21,7 +21,8 @@ const INPUT_TYPE_OPTIONS = [
interface DraggableFieldProps {
field: ItemField;
index: number;
moveField: (dragIndex: number, hoverIndex: number) => void;
// 2025-12-03: ID 기반으로 변경 (index는 stale 문제 발생)
moveField: (dragFieldId: number, hoverFieldId: number) => void;
onDelete: () => void;
onEdit?: () => void;
}
@@ -30,8 +31,10 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit }: Dr
const [isDragging, setIsDragging] = useState(false);
const handleDragStart = (e: React.DragEvent) => {
e.stopPropagation(); // 2025-12-03: 섹션 드래그 이벤트와 충돌 방지
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', JSON.stringify({ index, id: field.id }));
// 2025-12-03: 타입 구분 추가 (섹션/필드 드래그 구분)
e.dataTransfer.setData('application/json', JSON.stringify({ type: 'field', id: field.id }));
setIsDragging(true);
};
@@ -41,18 +44,25 @@ export function DraggableField({ field, index, moveField, onDelete, onEdit }: Dr
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation(); // 2025-12-03: 이벤트 버블링 방지
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation(); // 2025-12-03: 이벤트 버블링 방지
try {
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
if (data.index !== index) {
moveField(data.index, index);
const data = JSON.parse(e.dataTransfer.getData('application/json'));
// 2025-12-03: 타입 체크 - 필드 드래그만 처리
if (data.type !== 'field') {
console.log('[DraggableField] 필드 드래그가 아님, 무시:', data);
return;
}
if (data.id !== field.id) {
moveField(data.id, field.id);
}
} catch (err) {
// Ignore
// Ignore - 다른 타입의 드래그 데이터
}
};

View File

@@ -42,7 +42,8 @@ export function DraggableSection({
const handleDragStart = (e: React.DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', JSON.stringify({ index, id: section.id }));
// 2025-12-03: 타입 구분 추가 (섹션/필드 드래그 구분)
e.dataTransfer.setData('application/json', JSON.stringify({ type: 'section', index, id: section.id }));
setIsDragging(true);
};
@@ -58,12 +59,16 @@ export function DraggableSection({
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
try {
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
const data = JSON.parse(e.dataTransfer.getData('application/json'));
// 2025-12-03: 타입 체크 - 섹션 드래그만 처리
if (data.type !== 'section') {
return;
}
if (data.index !== index) {
moveSection(data.index, index);
}
} catch (err) {
// Ignore
// Ignore - 다른 타입의 드래그 데이터
}
};

View File

@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
const ITEM_TYPE_OPTIONS = [
{ value: 'FG', label: '제품 (FG)' },
{ value: 'PT', label: '부품 (PT)' },
{ value: 'SM', label: '반제품 (SM)' },
{ value: 'SM', label: '부자재 (SM)' },
{ value: 'RM', label: '원자재 (RM)' },
{ value: 'CS', label: '소모품 (CS)' },
];

View File

@@ -145,15 +145,15 @@ export function useFieldManagement(): UseFieldManagementReturn {
}
// 조건부 표시 설정
// 2025-12-02: ConditionalDisplayUI는 field/section 모두 newFieldConditionFields에 저장
// - field 모드: fieldConditions[].targetFieldIds에 필드 ID 저장
// - section 모드: fieldConditions[].targetSectionIds에 섹션 ID 저장
const displayCondition: FieldDisplayCondition | undefined = newFieldConditionEnabled
? {
targetType: newFieldConditionTargetType,
fieldConditions: newFieldConditionTargetType === 'field' && newFieldConditionFields.length > 0
fieldConditions: newFieldConditionFields.length > 0
? newFieldConditionFields
: undefined,
sectionIds: newFieldConditionTargetType === 'section' && newFieldConditionSections.length > 0
? newFieldConditionSections
: undefined
}
: undefined;

View File

@@ -59,7 +59,8 @@ interface HierarchyTabProps {
updateSection: (sectionId: number, updates: Partial<ItemSection>) => Promise<void>;
deleteField: (pageId: string, sectionId: string, fieldId: string) => void; // 2025-11-27: 연결 해제로 변경 (삭제 아님, 항목 탭에 유지)
handleEditField: (sectionId: string, field: any) => void;
moveField: (sectionId: number, dragIndex: number, hoverIndex: number) => void;
// 2025-12-03: ID 기반으로 변경 (index stale 문제 해결)
moveField: (sectionId: number, dragFieldId: number, hoverFieldId: number) => void | Promise<void>;
// 2025-11-26 추가: 섹션/필드 불러오기
setIsImportSectionDialogOpen?: (open: boolean) => void;
setIsImportFieldDialogOpen?: (open: boolean) => void;
@@ -441,10 +442,10 @@ export function HierarchyTab({
.sort((a, b) => (a.order_no ?? 0) - (b.order_no ?? 0))
.map((field, fieldIndex) => (
<DraggableField
key={`field-${field.id}-${fieldIndex}`}
key={field.id}
field={field}
index={fieldIndex}
moveField={(dragIndex, hoverIndex) => moveField(section.id, dragIndex, hoverIndex)}
moveField={(dragFieldId, hoverFieldId) => moveField(section.id, dragFieldId, hoverFieldId)}
onDelete={() => {
if (confirm('이 항목을 섹션에서 연결 해제하시겠습니까?\n(항목 데이터는 항목 탭에 유지됩니다)')) {
deleteField(String(selectedPage.id), String(section.id), String(field.id));