feat: 모든 카테고리에 추가요청사항 저장 기능 및 엑셀 내보내기 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 모든 BOM 카테고리(Pipe, Fitting, Flange, Gasket, Bolt, Support)에 추가요청사항 저장/편집 기능 추가
- 저장된 데이터의 카테고리 변경 및 페이지 새로고침 시 지속성 보장
- 백엔드 materials 테이블에 brand, user_requirement 컬럼 추가
- 새로운 /materials/{id}/brand, /materials/{id}/user-requirement PATCH API 엔드포인트 추가
- 모든 카테고리에서 Additional Request 컬럼 너비 확장 (UI 겹침 방지)
- GASKET 카테고리 엑셀 내보내기에 누락된 '추가요청사항' 컬럼 추가
- 엑셀 내보내기 시 저장된 추가요청사항이 우선 반영되도록 개선
- P열 납기일 규칙 유지하며 관리항목 개수 조정
This commit is contained in:
hyungi
2025-10-17 12:54:17 +09:00
parent 6b6360ecd5
commit f336b5a4a8
12 changed files with 1667 additions and 333 deletions

View File

@@ -11,6 +11,7 @@ const BoltMaterialsView = ({
setUserRequirements,
purchasedMaterials,
onPurchasedMaterialsUpdate,
updateMaterial, // 자재 업데이트 함수
jobNo,
fileId,
user
@@ -18,6 +19,59 @@ const BoltMaterialsView = ({
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
const [columnFilters, setColumnFilters] = useState({});
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
const [savedRequests, setSavedRequests] = useState({}); // 저장된 추가요구사항 상태
const [editingRequest, setEditingRequest] = useState({}); // 추가요구사항 편집 모드
const [savingRequest, setSavingRequest] = useState({}); // 추가요구사항 저장 중 상태
// 컴포넌트 마운트 시 저장된 데이터 로드
React.useEffect(() => {
const loadSavedData = () => {
const savedRequestsData = {};
materials.forEach(material => {
if (material.user_requirement && material.user_requirement.trim()) {
savedRequestsData[material.id] = material.user_requirement.trim();
}
});
setSavedRequests(savedRequestsData);
};
if (materials && materials.length > 0) {
loadSavedData();
} else {
setSavedRequests({});
}
}, [materials]);
// 추가요구사항 저장 함수
const handleSaveRequest = async (materialId, request) => {
setSavingRequest(prev => ({ ...prev, [materialId]: true }));
try {
await api.patch(`/materials/${materialId}/user-requirement`, {
user_requirement: request.trim()
});
setSavedRequests(prev => ({ ...prev, [materialId]: request.trim() }));
setEditingRequest(prev => ({ ...prev, [materialId]: false }));
setUserRequirements(prev => ({ ...prev, [materialId]: '' }));
if (updateMaterial) {
updateMaterial(materialId, { user_requirement: request.trim() });
}
} catch (error) {
console.error('추가요구사항 저장 실패:', error);
alert('추가요구사항 저장에 실패했습니다.');
} finally {
setSavingRequest(prev => ({ ...prev, [materialId]: false }));
}
};
// 추가요구사항 편집 시작
const handleEditRequest = (materialId, currentRequest) => {
setEditingRequest(prev => ({ ...prev, [materialId]: true }));
setUserRequirements(prev => ({ ...prev, [materialId]: currentRequest || '' }));
};
// 볼트 추가요구사항 추출 함수
const extractBoltAdditionalRequirements = (description) => {
const additionalReqs = [];
@@ -252,7 +306,7 @@ const BoltMaterialsView = ({
const dataWithRequirements = selectedMaterialsData.map(material => ({
...material,
user_requirement: userRequirements[material.id] || ''
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
}));
try {
@@ -407,7 +461,7 @@ const BoltMaterialsView = ({
{/* 헤더 */}
<div style={{
display: 'grid',
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 150px 200px',
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 200px 200px',
gap: '16px',
padding: '16px',
background: '#f8fafc',
@@ -515,7 +569,7 @@ const BoltMaterialsView = ({
key={material.id}
style={{
display: 'grid',
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 150px 200px',
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 200px 200px',
gap: '16px',
padding: '16px',
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
@@ -580,23 +634,81 @@ const BoltMaterialsView = ({
}}>
{info.userRequirements}
</div>
<div>
<input
type="text"
value={userRequirements[material.id] || ''}
onChange={(e) => setUserRequirements({
...userRequirements,
[material.id]: e.target.value
})}
placeholder="Enter additional request..."
style={{
width: '100%',
padding: '6px 8px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '12px'
}}
/>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
{!editingRequest[material.id] && savedRequests[material.id] ? (
// 저장된 상태 - 요구사항 표시 + 수정 버튼
<>
<div style={{
flex: 1,
padding: '6px 8px',
border: '1px solid #e5e7eb',
borderRadius: '4px',
fontSize: '12px',
textAlign: 'center',
background: '#f9fafb',
color: '#374151'
}}>
{savedRequests[material.id]}
</div>
<button
onClick={() => handleEditRequest(material.id, savedRequests[material.id])}
disabled={isPurchased}
style={{
padding: '6px 8px',
border: 'none',
borderRadius: '4px',
background: isPurchased ? '#d1d5db' : '#f59e0b',
color: 'white',
fontSize: '10px',
cursor: isPurchased ? 'not-allowed' : 'pointer',
opacity: isPurchased ? 0.5 : 1,
minWidth: '40px'
}}
>
Edit
</button>
</>
) : (
// 편집 상태 - 입력 필드 + 저장 버튼
<>
<input
type="text"
value={userRequirements[material.id] || ''}
onChange={(e) => setUserRequirements({
...userRequirements,
[material.id]: e.target.value
})}
placeholder="Enter additional request..."
disabled={isPurchased}
style={{
flex: 1,
padding: '6px 8px',
border: '1px solid #d1d5db',
borderRadius: '4px',
fontSize: '12px',
opacity: isPurchased ? 0.5 : 1,
cursor: isPurchased ? 'not-allowed' : 'text'
}}
/>
<button
onClick={() => handleSaveRequest(material.id, userRequirements[material.id] || '')}
disabled={isPurchased || savingRequest[material.id]}
style={{
padding: '6px 8px',
border: 'none',
borderRadius: '4px',
background: isPurchased ? '#d1d5db' : '#10b981',
color: 'white',
fontSize: '10px',
cursor: isPurchased || savingRequest[material.id] ? 'not-allowed' : 'pointer',
opacity: isPurchased ? 0.5 : 1,
minWidth: '40px'
}}
>
{savingRequest[material.id] ? '...' : 'Save'}
</button>
</>
)}
</div>
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
{info.purchaseQuantity}