feat: 리비전 관리 시스템 완전 개편
변동이력 관리로 전환: - 도면번호 기준 변경 추적 - 리비전 업로드 시 전체 자재 저장 (차이분만 저장 방식 폐지) - 구매신청 정보 수량 기반 상속 리비전 변경 감지: - 수량/재질/크기/카테고리 변경 감지 - 변경 유형: specification_changed, quantity_changed, added, removed - 도면별 변경사항 추적 누락 도면 처리: - 리비전 업로드 시 누락된 도면 자동 감지 - 3가지 선택 옵션: 일부 업로드 / 도면 삭제 / 취소 - 구매신청 여부에 따라 다른 처리 (재고품 vs 숨김) 자재 상태 관리: - revision_status 컬럼 추가 (active/inventory/deleted_not_purchased/changed) - 재고품: 연노랑색 배경, '재고품' 배지 - 변경됨: 파란색 테두리, '변경됨' 배지 - 삭제됨: 자동 숨김 구매신청 정보 상속: - 수량 기반 상속 (그룹별 개수만큼만) - Rev.0에서 3개 구매 → Rev.1에서 처음 3개만 상속, 추가분은 미구매 - 도면번호 정확히 일치하는 경우에만 상속 기타 개선: - 구매신청 관리 페이지 수량 표시 개선 (3 EA, 소수점 제거) - 도면번호/라인번호 파싱 및 저장 (DWG_NAME, LINE_NUM 컬럼) - SPECIAL 카테고리 도면번호 표시 - 마이그레이션 스크립트 추가 (29_add_revision_status.sql)
This commit is contained in:
@@ -299,6 +299,72 @@ const BOMWorkspacePage = ({ project, onNavigate, onBack }) => {
|
||||
});
|
||||
|
||||
if (response.data?.success) {
|
||||
// 누락된 도면 확인
|
||||
if (response.data.missing_drawings && response.data.missing_drawings.requires_confirmation) {
|
||||
const missingDrawings = response.data.missing_drawings.drawings || [];
|
||||
const materialCount = response.data.missing_drawings.materials?.length || 0;
|
||||
const hasPreviousPurchase = response.data.missing_drawings.has_previous_purchase;
|
||||
const fileId = response.data.file_id;
|
||||
|
||||
// 사용자 선택을 위한 프롬프트 메시지
|
||||
let alertMessage = `⚠️ 리비전 업로드 확인\n\n` +
|
||||
`다음 도면이 새 파일에 없습니다:\n` +
|
||||
`${missingDrawings.slice(0, 5).join('\n')}` +
|
||||
`${missingDrawings.length > 5 ? `\n...외 ${missingDrawings.length - 5}개` : ''}\n\n` +
|
||||
`관련 자재: ${materialCount}개\n\n`;
|
||||
|
||||
if (hasPreviousPurchase) {
|
||||
// 케이스 1: 이미 구매신청된 경우
|
||||
alertMessage += `✅ 이전 리비전에서 구매신청이 진행되었습니다.\n\n` +
|
||||
`다음 중 선택하세요:\n\n` +
|
||||
`1️⃣ "일부만 업로드" - 변경된 도면만 업로드, 기존 도면 유지\n` +
|
||||
` → 누락된 도면의 자재는 "재고품"으로 표시 (연노랑색)\n\n` +
|
||||
`2️⃣ "도면 삭제됨" - 누락된 도면 완전 삭제\n` +
|
||||
` → 해당 자재 제거 및 재고품 처리\n\n` +
|
||||
`3️⃣ "취소" - 업로드 취소\n\n` +
|
||||
`숫자를 입력하세요 (1, 2, 3):`;
|
||||
} else {
|
||||
// 케이스 2: 구매신청 전인 경우
|
||||
alertMessage += `⚠️ 아직 구매신청이 진행되지 않았습니다.\n\n` +
|
||||
`다음 중 선택하세요:\n\n` +
|
||||
`1️⃣ "일부만 업로드" - 변경된 도면만 업로드, 기존 도면 유지\n` +
|
||||
` → 누락된 도면의 자재는 그대로 유지\n\n` +
|
||||
`2️⃣ "도면 삭제됨" - 누락된 도면 완전 삭제\n` +
|
||||
` → 해당 자재 완전 제거 (숨김 처리)\n\n` +
|
||||
`3️⃣ "취소" - 업로드 취소\n\n` +
|
||||
`숫자를 입력하세요 (1, 2, 3):`;
|
||||
}
|
||||
|
||||
const userChoice = prompt(alertMessage);
|
||||
|
||||
if (userChoice === '3' || userChoice === null) {
|
||||
// 취소 선택
|
||||
await api.delete(`/files/${fileId}`);
|
||||
alert('업로드가 취소되었습니다.');
|
||||
return;
|
||||
} else if (userChoice === '2') {
|
||||
// 도면 삭제됨 - 백엔드에 삭제 처리 요청
|
||||
try {
|
||||
await api.post(`/files/${fileId}/process-missing-drawings`, {
|
||||
action: 'delete',
|
||||
drawings: missingDrawings
|
||||
});
|
||||
alert(`✅ ${missingDrawings.length}개 도면이 삭제 처리되었습니다.`);
|
||||
} catch (err) {
|
||||
console.error('도면 삭제 처리 실패:', err);
|
||||
alert('도면 삭제 처리에 실패했습니다.');
|
||||
}
|
||||
} else if (userChoice === '1') {
|
||||
// 일부만 업로드 - 이미 처리됨 (기본 동작)
|
||||
alert(`✅ 일부 업로드로 처리되었습니다.\n누락된 ${missingDrawings.length}개 도면은 기존 상태를 유지합니다.`);
|
||||
} else {
|
||||
// 잘못된 입력
|
||||
await api.delete(`/files/${fileId}`);
|
||||
alert('잘못된 입력입니다. 업로드가 취소되었습니다.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
alert(`리비전 ${response.data.revision} 업로드 성공!\n신규 자재: ${response.data.new_materials_count || 0}개`);
|
||||
await loadFiles();
|
||||
}
|
||||
|
||||
@@ -1168,4 +1168,24 @@
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
리비전 상태별 스타일
|
||||
================================ */
|
||||
|
||||
/* 재고품 스타일 (구매신청 후 리비전에서 삭제됨) */
|
||||
.detailed-material-row.inventory {
|
||||
background-color: #fef3c7 !important; /* 연노랑색 배경 */
|
||||
border-left: 4px solid #f59e0b !important; /* 주황색 왼쪽 테두리 */
|
||||
}
|
||||
|
||||
/* 삭제된 자재 (구매신청 전) - 숨김 처리 */
|
||||
.detailed-material-row.deleted-not-purchased {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 변경된 자재 (추가 구매 필요) */
|
||||
.detailed-material-row.changed {
|
||||
border-left: 4px solid #3b82f6 !important; /* 파란색 왼쪽 테두리 */
|
||||
}
|
||||
@@ -1145,6 +1145,71 @@ const NewMaterialsPage = ({
|
||||
setSelectedMaterials(newSelection);
|
||||
};
|
||||
|
||||
// 자재 상태별 CSS 클래스 생성
|
||||
const getMaterialRowClasses = (material, baseClass) => {
|
||||
const classes = [baseClass];
|
||||
|
||||
if (selectedMaterials.has(material.id)) {
|
||||
classes.push('selected');
|
||||
}
|
||||
|
||||
// 구매신청 여부 확인
|
||||
const isPurchased = material.grouped_ids ?
|
||||
material.grouped_ids.some(id => purchasedMaterials.has(id)) :
|
||||
purchasedMaterials.has(material.id);
|
||||
|
||||
if (isPurchased) {
|
||||
classes.push('purchased');
|
||||
}
|
||||
|
||||
// 리비전 상태
|
||||
if (material.revision_status === 'inventory') {
|
||||
classes.push('inventory');
|
||||
} else if (material.revision_status === 'deleted_not_purchased') {
|
||||
classes.push('deleted-not-purchased');
|
||||
} else if (material.revision_status === 'changed') {
|
||||
classes.push('changed');
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
};
|
||||
|
||||
// 리비전 상태 배지 렌더링
|
||||
const renderRevisionBadge = (material) => {
|
||||
if (material.revision_status === 'inventory') {
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: '#f59e0b',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '4px',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
재고품
|
||||
</span>
|
||||
);
|
||||
} else if (material.revision_status === 'changed') {
|
||||
return (
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 6px',
|
||||
backgroundColor: '#3b82f6',
|
||||
color: 'white',
|
||||
fontSize: '10px',
|
||||
borderRadius: '3px',
|
||||
marginBottom: '4px',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
변경됨
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 필터 헤더 컴포넌트
|
||||
const FilterableHeader = ({ children, sortKey, filterKey, className = "" }) => {
|
||||
const uniqueValues = React.useMemo(() => {
|
||||
@@ -1856,7 +1921,7 @@ const NewMaterialsPage = ({
|
||||
return (
|
||||
<div
|
||||
key={material.id}
|
||||
className={`detailed-material-row pipe-row ${selectedMaterials.has(material.id) ? 'selected' : ''} ${isPurchased ? 'purchased' : ''}`}
|
||||
className={getMaterialRowClasses(material, 'detailed-material-row pipe-row')}
|
||||
style={isPurchased ? { opacity: 0.6, backgroundColor: '#f0f0f0' } : {}}
|
||||
>
|
||||
{/* 선택 */}
|
||||
|
||||
Reference in New Issue
Block a user