feat: 모든 카테고리에 추가요청사항 저장 기능 및 엑셀 내보내기 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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:
@@ -119,6 +119,14 @@ try:
|
|||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.warning(f"purchase_request 라우터를 찾을 수 없습니다: {e}")
|
logger.warning(f"purchase_request 라우터를 찾을 수 없습니다: {e}")
|
||||||
|
|
||||||
|
# 자재 관리 라우터
|
||||||
|
try:
|
||||||
|
from .routers import materials
|
||||||
|
app.include_router(materials.router)
|
||||||
|
logger.info("materials 라우터 등록 완료")
|
||||||
|
except ImportError as e:
|
||||||
|
logger.warning(f"materials 라우터를 찾을 수 없습니다: {e}")
|
||||||
|
|
||||||
# 파일 관리 API 라우터 등록 (비활성화 - files 라우터와 충돌 방지)
|
# 파일 관리 API 라우터 등록 (비활성화 - files 라우터와 충돌 방지)
|
||||||
# try:
|
# try:
|
||||||
# from .api import file_management
|
# from .api import file_management
|
||||||
|
|||||||
@@ -1867,6 +1867,7 @@ async def get_materials(
|
|||||||
m.created_at, m.classified_category, m.classification_confidence,
|
m.created_at, m.classified_category, m.classification_confidence,
|
||||||
m.classification_details,
|
m.classification_details,
|
||||||
m.is_verified, m.verified_by, m.verified_at,
|
m.is_verified, m.verified_by, m.verified_at,
|
||||||
|
m.brand, m.user_requirement,
|
||||||
f.original_filename, f.project_id, f.job_no, f.revision,
|
f.original_filename, f.project_id, f.job_no, f.revision,
|
||||||
p.official_project_code, p.project_name,
|
p.official_project_code, p.project_name,
|
||||||
pd.outer_diameter, pd.schedule, pd.material_spec, pd.manufacturing_method,
|
pd.outer_diameter, pd.schedule, pd.material_spec, pd.manufacturing_method,
|
||||||
@@ -2087,7 +2088,10 @@ async def get_materials(
|
|||||||
"purchase_status": m.purchase_status,
|
"purchase_status": m.purchase_status,
|
||||||
"purchase_confirmed_by": m.confirmed_by,
|
"purchase_confirmed_by": m.confirmed_by,
|
||||||
"purchase_confirmed_at": m.confirmed_at,
|
"purchase_confirmed_at": m.confirmed_at,
|
||||||
"created_at": m.created_at
|
"created_at": m.created_at,
|
||||||
|
# 브랜드와 사용자 요구사항 필드 추가
|
||||||
|
"brand": m.brand,
|
||||||
|
"user_requirement": m.user_requirement
|
||||||
}
|
}
|
||||||
|
|
||||||
# 카테고리별 상세 정보 추가 (JOIN 결과 사용)
|
# 카테고리별 상세 정보 추가 (JOIN 결과 사용)
|
||||||
|
|||||||
161
backend/app/routers/materials.py
Normal file
161
backend/app/routers/materials.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import text
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from ..database import get_db
|
||||||
|
from ..auth.middleware import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/materials", tags=["materials"])
|
||||||
|
|
||||||
|
class BrandUpdate(BaseModel):
|
||||||
|
brand: str
|
||||||
|
|
||||||
|
class UserRequirementUpdate(BaseModel):
|
||||||
|
user_requirement: str
|
||||||
|
|
||||||
|
@router.patch("/{material_id}/brand")
|
||||||
|
async def update_material_brand(
|
||||||
|
material_id: int,
|
||||||
|
brand_data: BrandUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""자재의 브랜드 정보를 업데이트합니다."""
|
||||||
|
try:
|
||||||
|
# 자재 존재 여부 확인
|
||||||
|
result = db.execute(
|
||||||
|
text("SELECT id FROM materials WHERE id = :material_id"),
|
||||||
|
{"material_id": material_id}
|
||||||
|
)
|
||||||
|
material = result.fetchone()
|
||||||
|
|
||||||
|
if not material:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="자재를 찾을 수 없습니다."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 브랜드 업데이트
|
||||||
|
db.execute(
|
||||||
|
text("""
|
||||||
|
UPDATE materials
|
||||||
|
SET brand = :brand,
|
||||||
|
updated_by = :updated_by
|
||||||
|
WHERE id = :material_id
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"brand": brand_data.brand.strip(),
|
||||||
|
"updated_by": current_user.get("username", "unknown"),
|
||||||
|
"material_id": material_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "브랜드가 성공적으로 업데이트되었습니다.",
|
||||||
|
"material_id": material_id,
|
||||||
|
"brand": brand_data.brand.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"브랜드 업데이트 실패: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.patch("/{material_id}/user-requirement")
|
||||||
|
async def update_material_user_requirement(
|
||||||
|
material_id: int,
|
||||||
|
requirement_data: UserRequirementUpdate,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""자재의 사용자 요구사항을 업데이트합니다."""
|
||||||
|
try:
|
||||||
|
# 자재 존재 여부 확인
|
||||||
|
result = db.execute(
|
||||||
|
text("SELECT id FROM materials WHERE id = :material_id"),
|
||||||
|
{"material_id": material_id}
|
||||||
|
)
|
||||||
|
material = result.fetchone()
|
||||||
|
|
||||||
|
if not material:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="자재를 찾을 수 없습니다."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 사용자 요구사항 업데이트
|
||||||
|
db.execute(
|
||||||
|
text("""
|
||||||
|
UPDATE materials
|
||||||
|
SET user_requirement = :user_requirement,
|
||||||
|
updated_by = :updated_by
|
||||||
|
WHERE id = :material_id
|
||||||
|
"""),
|
||||||
|
{
|
||||||
|
"user_requirement": requirement_data.user_requirement.strip(),
|
||||||
|
"updated_by": current_user.get("username", "unknown"),
|
||||||
|
"material_id": material_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "사용자 요구사항이 성공적으로 업데이트되었습니다.",
|
||||||
|
"material_id": material_id,
|
||||||
|
"user_requirement": requirement_data.user_requirement.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"사용자 요구사항 업데이트 실패: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/{material_id}")
|
||||||
|
async def get_material(
|
||||||
|
material_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: dict = Depends(get_current_user)
|
||||||
|
):
|
||||||
|
"""자재 정보를 조회합니다."""
|
||||||
|
try:
|
||||||
|
result = db.execute(
|
||||||
|
text("""
|
||||||
|
SELECT id, original_description, classified_category,
|
||||||
|
brand, user_requirement, created_at, updated_by
|
||||||
|
FROM materials
|
||||||
|
WHERE id = :material_id
|
||||||
|
"""),
|
||||||
|
{"material_id": material_id}
|
||||||
|
)
|
||||||
|
material = result.fetchone()
|
||||||
|
|
||||||
|
if not material:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="자재를 찾을 수 없습니다."
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": material.id,
|
||||||
|
"original_description": material.original_description,
|
||||||
|
"classified_category": material.classified_category,
|
||||||
|
"brand": material.brand,
|
||||||
|
"user_requirement": material.user_requirement,
|
||||||
|
"created_at": material.created_at,
|
||||||
|
"updated_by": material.updated_by
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=f"자재 조회 실패: {str(e)}"
|
||||||
|
)
|
||||||
@@ -11,6 +11,7 @@ const BoltMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
jobNo,
|
jobNo,
|
||||||
fileId,
|
fileId,
|
||||||
user
|
user
|
||||||
@@ -18,6 +19,59 @@ const BoltMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
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 extractBoltAdditionalRequirements = (description) => {
|
||||||
const additionalReqs = [];
|
const additionalReqs = [];
|
||||||
@@ -252,7 +306,7 @@ const BoltMaterialsView = ({
|
|||||||
|
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
||||||
...material,
|
...material,
|
||||||
user_requirement: userRequirements[material.id] || ''
|
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -407,7 +461,7 @@ const BoltMaterialsView = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 150px 200px',
|
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 200px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -515,7 +569,7 @@ const BoltMaterialsView = ({
|
|||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 150px 200px',
|
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 200px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -580,23 +634,81 @@ const BoltMaterialsView = ({
|
|||||||
}}>
|
}}>
|
||||||
{info.userRequirements}
|
{info.userRequirements}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<input
|
{!editingRequest[material.id] && savedRequests[material.id] ? (
|
||||||
type="text"
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
value={userRequirements[material.id] || ''}
|
<>
|
||||||
onChange={(e) => setUserRequirements({
|
<div style={{
|
||||||
...userRequirements,
|
flex: 1,
|
||||||
[material.id]: e.target.value
|
padding: '6px 8px',
|
||||||
})}
|
border: '1px solid #e5e7eb',
|
||||||
placeholder="Enter additional request..."
|
borderRadius: '4px',
|
||||||
style={{
|
fontSize: '12px',
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
padding: '6px 8px',
|
background: '#f9fafb',
|
||||||
border: '1px solid #d1d5db',
|
color: '#374151'
|
||||||
borderRadius: '4px',
|
}}>
|
||||||
fontSize: '12px'
|
{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>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center' }}>
|
||||||
{info.purchaseQuantity}
|
{info.purchaseQuantity}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const FittingMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
fileId,
|
fileId,
|
||||||
jobNo,
|
jobNo,
|
||||||
user,
|
user,
|
||||||
@@ -19,6 +20,58 @@ const FittingMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
||||||
|
const [savingRequest, setSavingRequest] = useState({}); // 추가요구사항 저장 중 상태
|
||||||
|
const [savedRequests, setSavedRequests] = useState({}); // 저장된 추가요구사항 상태
|
||||||
|
const [editingRequest, setEditingRequest] = 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 extractNippleEndInfo = (description) => {
|
const extractNippleEndInfo = (description) => {
|
||||||
@@ -408,7 +461,7 @@ const FittingMaterialsView = ({
|
|||||||
|
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
||||||
...material,
|
...material,
|
||||||
user_requirement: userRequirements[material.id] || ''
|
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -627,7 +680,7 @@ const FittingMaterialsView = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 200px',
|
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 250px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -669,7 +722,7 @@ const FittingMaterialsView = ({
|
|||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 200px',
|
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 180px 120px 250px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -740,23 +793,81 @@ const FittingMaterialsView = ({
|
|||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
||||||
{info.quantity} {info.unit}
|
{info.quantity} {info.unit}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<input
|
{!editingRequest[material.id] && savedRequests[material.id] ? (
|
||||||
type="text"
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
value={userRequirements[material.id] || ''}
|
<>
|
||||||
onChange={(e) => setUserRequirements({
|
<div style={{
|
||||||
...userRequirements,
|
flex: 1,
|
||||||
[material.id]: e.target.value
|
padding: '6px 8px',
|
||||||
})}
|
border: '1px solid #e5e7eb',
|
||||||
placeholder="Enter additional request..."
|
borderRadius: '4px',
|
||||||
style={{
|
fontSize: '12px',
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
padding: '6px 8px',
|
background: '#f9fafb',
|
||||||
border: '1px solid #d1d5db',
|
color: '#374151'
|
||||||
borderRadius: '4px',
|
}}>
|
||||||
fontSize: '12px'
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const FlangeMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
fileId,
|
fileId,
|
||||||
jobNo,
|
jobNo,
|
||||||
user,
|
user,
|
||||||
@@ -19,6 +20,58 @@ const FlangeMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
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 parseFlangeInfo = (material) => {
|
const parseFlangeInfo = (material) => {
|
||||||
@@ -238,7 +291,7 @@ const FlangeMaterialsView = ({
|
|||||||
|
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
||||||
...material,
|
...material,
|
||||||
user_requirement: userRequirements[material.id] || ''
|
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -456,7 +509,7 @@ const FlangeMaterialsView = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 250px 150px 120px 120px 100px 150px 180px 200px',
|
gridTemplateColumns: '50px 250px 150px 120px 120px 100px 150px 180px 250px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -499,7 +552,7 @@ const FlangeMaterialsView = ({
|
|||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 250px 150px 120px 120px 100px 150px 180px 200px',
|
gridTemplateColumns: '50px 250px 150px 120px 120px 100px 150px 180px 250px',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -563,23 +616,81 @@ const FlangeMaterialsView = ({
|
|||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'center' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'center' }}>
|
||||||
{info.quantity} {info.unit}
|
{info.quantity} {info.unit}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<input
|
{!editingRequest[material.id] && savedRequests[material.id] ? (
|
||||||
type="text"
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
value={userRequirements[material.id] || ''}
|
<>
|
||||||
onChange={(e) => setUserRequirements({
|
<div style={{
|
||||||
...userRequirements,
|
flex: 1,
|
||||||
[material.id]: e.target.value
|
padding: '6px 8px',
|
||||||
})}
|
border: '1px solid #e5e7eb',
|
||||||
placeholder="Enter additional request..."
|
borderRadius: '4px',
|
||||||
style={{
|
fontSize: '12px',
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
padding: '6px 8px',
|
background: '#f9fafb',
|
||||||
border: '1px solid #d1d5db',
|
color: '#374151'
|
||||||
borderRadius: '4px',
|
}}>
|
||||||
fontSize: '12px'
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const GasketMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
fileId,
|
fileId,
|
||||||
jobNo,
|
jobNo,
|
||||||
user
|
user
|
||||||
@@ -18,6 +19,58 @@ const GasketMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
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 parseGasketInfo = (material) => {
|
const parseGasketInfo = (material) => {
|
||||||
const qty = Math.round(material.quantity || 0);
|
const qty = Math.round(material.quantity || 0);
|
||||||
@@ -189,7 +242,7 @@ const GasketMaterialsView = ({
|
|||||||
|
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
||||||
...material,
|
...material,
|
||||||
user_requirement: userRequirements[material.id] || ''
|
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -341,7 +394,7 @@ const GasketMaterialsView = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 250px 120px 100px 120px 200px 100px 180px 150px 120px',
|
gridTemplateColumns: '50px 250px 120px 100px 120px 200px 100px 180px 200px 120px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -461,7 +514,7 @@ const GasketMaterialsView = ({
|
|||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 250px 120px 100px 120px 200px 100px 180px 150px 120px',
|
gridTemplateColumns: '50px 250px 120px 100px 120px 200px 100px 180px 200px 120px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -532,23 +585,81 @@ const GasketMaterialsView = ({
|
|||||||
}}>
|
}}>
|
||||||
{info.userRequirements}
|
{info.userRequirements}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<input
|
{!editingRequest[material.id] && savedRequests[material.id] ? (
|
||||||
type="text"
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
value={userRequirements[material.id] || ''}
|
<>
|
||||||
onChange={(e) => setUserRequirements({
|
<div style={{
|
||||||
...userRequirements,
|
flex: 1,
|
||||||
[material.id]: e.target.value
|
padding: '6px 8px',
|
||||||
})}
|
border: '1px solid #e5e7eb',
|
||||||
placeholder="Enter additional request..."
|
borderRadius: '4px',
|
||||||
style={{
|
fontSize: '12px',
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
padding: '6px 8px',
|
background: '#f9fafb',
|
||||||
border: '1px solid #d1d5db',
|
color: '#374151'
|
||||||
borderRadius: '4px',
|
}}>
|
||||||
fontSize: '12px'
|
{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>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center', fontWeight: '500' }}>
|
<div style={{ fontSize: '14px', color: '#1f2937', textAlign: 'center', fontWeight: '500' }}>
|
||||||
{info.purchaseQuantity.toLocaleString()}
|
{info.purchaseQuantity.toLocaleString()}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const PipeMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
fileId,
|
fileId,
|
||||||
jobNo,
|
jobNo,
|
||||||
user,
|
user,
|
||||||
@@ -19,6 +20,97 @@ const PipeMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
||||||
|
const [brandInputs, setBrandInputs] = useState({}); // 브랜드 입력 상태
|
||||||
|
const [savingBrand, setSavingBrand] = useState({}); // 브랜드 저장 중 상태
|
||||||
|
const [savingRequest, setSavingRequest] = useState({}); // 추가요구사항 저장 중 상태
|
||||||
|
const [savedBrands, setSavedBrands] = useState({}); // 저장된 브랜드 상태
|
||||||
|
const [savedRequests, setSavedRequests] = useState({}); // 저장된 추가요구사항 상태
|
||||||
|
const [editingBrand, setEditingBrand] = useState({}); // 브랜드 편집 모드
|
||||||
|
const [editingRequest, setEditingRequest] = useState({}); // 추가요구사항 편집 모드
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 저장된 데이터 로드
|
||||||
|
React.useEffect(() => {
|
||||||
|
const loadSavedData = () => {
|
||||||
|
const savedBrandsData = {};
|
||||||
|
const savedRequestsData = {};
|
||||||
|
|
||||||
|
materials.forEach(material => {
|
||||||
|
// 백엔드에서 가져온 데이터가 있으면 저장된 상태로 설정
|
||||||
|
if (material.brand && material.brand.trim()) {
|
||||||
|
savedBrandsData[material.id] = material.brand.trim();
|
||||||
|
}
|
||||||
|
if (material.user_requirement && material.user_requirement.trim()) {
|
||||||
|
savedRequestsData[material.id] = material.user_requirement.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSavedBrands(savedBrandsData);
|
||||||
|
setSavedRequests(savedRequestsData);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (materials && materials.length > 0) {
|
||||||
|
loadSavedData();
|
||||||
|
} else {
|
||||||
|
setSavedBrands({});
|
||||||
|
setSavedRequests({});
|
||||||
|
}
|
||||||
|
}, [materials]);
|
||||||
|
|
||||||
|
// 브랜드 저장 함수
|
||||||
|
const handleSaveBrand = async (materialId, brand) => {
|
||||||
|
if (!brand.trim()) return;
|
||||||
|
|
||||||
|
setSavingBrand(prev => ({ ...prev, [materialId]: true }));
|
||||||
|
try {
|
||||||
|
await api.patch(`/materials/${materialId}/brand`, { brand: brand.trim() });
|
||||||
|
setSavedBrands(prev => ({ ...prev, [materialId]: brand.trim() }));
|
||||||
|
setEditingBrand(prev => ({ ...prev, [materialId]: false }));
|
||||||
|
setBrandInputs(prev => ({ ...prev, [materialId]: '' }));
|
||||||
|
|
||||||
|
if (updateMaterial) {
|
||||||
|
updateMaterial(materialId, { brand: brand.trim() });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('브랜드 저장 실패:', error);
|
||||||
|
alert('브랜드 저장에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
setSavingBrand(prev => ({ ...prev, [materialId]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 브랜드 편집 시작
|
||||||
|
const handleEditBrand = (materialId, currentBrand) => {
|
||||||
|
setEditingBrand(prev => ({ ...prev, [materialId]: true }));
|
||||||
|
setBrandInputs(prev => ({ ...prev, [materialId]: currentBrand || '' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 추가요구사항 저장 함수
|
||||||
|
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 calculatePipePurchase = (material) => {
|
const calculatePipePurchase = (material) => {
|
||||||
@@ -96,6 +188,7 @@ const PipeMaterialsView = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// 정렬 처리
|
// 정렬 처리
|
||||||
const handleSort = (key) => {
|
const handleSort = (key) => {
|
||||||
let direction = 'asc';
|
let direction = 'asc';
|
||||||
@@ -175,7 +268,7 @@ const PipeMaterialsView = ({
|
|||||||
// 사용자 요구사항 포함
|
// 사용자 요구사항 포함
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
||||||
...material,
|
...material,
|
||||||
user_requirement: userRequirements[material.id] || ''
|
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -397,7 +490,7 @@ const PipeMaterialsView = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 120px 120px 120px 150px 200px 120px 100px 120px 250px',
|
gridTemplateColumns: '50px 120px 120px 120px 150px 200px 120px 100px 120px 300px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -519,7 +612,7 @@ const PipeMaterialsView = ({
|
|||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 120px 120px 120px 150px 200px 120px 100px 120px 250px',
|
gridTemplateColumns: '50px 120px 120px 120px 150px 200px 120px 100px 120px 300px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -593,23 +686,81 @@ const PipeMaterialsView = ({
|
|||||||
<div style={{ fontSize: '14px', color: '#6b7280', fontWeight: '500' }}>
|
<div style={{ fontSize: '14px', color: '#6b7280', fontWeight: '500' }}>
|
||||||
{info.unit}
|
{info.unit}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<input
|
{!editingRequest[material.id] && savedRequests[material.id] ? (
|
||||||
type="text"
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
value={userRequirements[material.id] || ''}
|
<>
|
||||||
onChange={(e) => setUserRequirements({
|
<div style={{
|
||||||
...userRequirements,
|
flex: 1,
|
||||||
[material.id]: e.target.value
|
padding: '6px',
|
||||||
})}
|
border: '1px solid #e5e7eb',
|
||||||
placeholder="Enter additional request..."
|
borderRadius: '4px',
|
||||||
style={{
|
fontSize: '11px',
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
padding: '6px 8px',
|
background: '#f9fafb',
|
||||||
border: '1px solid #d1d5db',
|
color: '#374151'
|
||||||
borderRadius: '4px',
|
}}>
|
||||||
fontSize: '12px'
|
{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',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '11px',
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const SupportMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
jobNo,
|
jobNo,
|
||||||
fileId,
|
fileId,
|
||||||
user
|
user
|
||||||
@@ -18,6 +19,58 @@ const SupportMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
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 parseSupportInfo = (material) => {
|
const parseSupportInfo = (material) => {
|
||||||
const desc = material.original_description || '';
|
const desc = material.original_description || '';
|
||||||
@@ -29,7 +82,13 @@ const SupportMaterialsView = ({
|
|||||||
if (descUpper.includes('URETHANE') || descUpper.includes('BLOCK SHOE') || descUpper.includes('우레탄')) {
|
if (descUpper.includes('URETHANE') || descUpper.includes('BLOCK SHOE') || descUpper.includes('우레탄')) {
|
||||||
supportType = 'URETHANE BLOCK SHOE';
|
supportType = 'URETHANE BLOCK SHOE';
|
||||||
} else if (descUpper.includes('CLAMP') || descUpper.includes('클램프')) {
|
} else if (descUpper.includes('CLAMP') || descUpper.includes('클램프')) {
|
||||||
supportType = 'CLAMP';
|
// 클램프 타입 상세 분류 (CL-1, CL-2, CL-3 등)
|
||||||
|
const clampMatch = desc.match(/CL[-\s]*(\d+)/i);
|
||||||
|
if (clampMatch) {
|
||||||
|
supportType = `CLAMP CL-${clampMatch[1]}`;
|
||||||
|
} else {
|
||||||
|
supportType = 'CLAMP CL-1'; // 기본값
|
||||||
|
}
|
||||||
} else if (descUpper.includes('HANGER') || descUpper.includes('행거')) {
|
} else if (descUpper.includes('HANGER') || descUpper.includes('행거')) {
|
||||||
supportType = 'HANGER';
|
supportType = 'HANGER';
|
||||||
} else if (descUpper.includes('SPRING') || descUpper.includes('스프링')) {
|
} else if (descUpper.includes('SPRING') || descUpper.includes('스프링')) {
|
||||||
@@ -86,6 +145,9 @@ const SupportMaterialsView = ({
|
|||||||
if (!consolidated[key]) {
|
if (!consolidated[key]) {
|
||||||
consolidated[key] = {
|
consolidated[key] = {
|
||||||
...material,
|
...material,
|
||||||
|
// Material Grade 정보를 parsedInfo에서 가져와서 설정
|
||||||
|
material_grade: info.grade,
|
||||||
|
full_material_grade: info.grade,
|
||||||
consolidatedQuantity: info.originalQuantity,
|
consolidatedQuantity: info.originalQuantity,
|
||||||
consolidatedIds: [material.id],
|
consolidatedIds: [material.id],
|
||||||
parsedInfo: info
|
parsedInfo: info
|
||||||
@@ -208,8 +270,13 @@ const SupportMaterialsView = ({
|
|||||||
|
|
||||||
// 엑셀 내보내기
|
// 엑셀 내보내기
|
||||||
const handleExportToExcel = async () => {
|
const handleExportToExcel = async () => {
|
||||||
const selectedMaterialsData = materials.filter(m => selectedMaterials.has(m.id));
|
// 선택된 합산 자료 가져오기
|
||||||
if (selectedMaterialsData.length === 0) {
|
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||||
|
const selectedConsolidatedMaterials = filteredMaterials.filter(consolidatedMaterial =>
|
||||||
|
consolidatedMaterial.consolidatedIds.some(id => selectedMaterials.has(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedConsolidatedMaterials.length === 0) {
|
||||||
alert('내보낼 자재를 선택해주세요.');
|
alert('내보낼 자재를 선택해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -217,9 +284,13 @@ const SupportMaterialsView = ({
|
|||||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
|
||||||
const excelFileName = `SUPPORT_Materials_${timestamp}.xlsx`;
|
const excelFileName = `SUPPORT_Materials_${timestamp}.xlsx`;
|
||||||
|
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
// 합산된 자료를 엑셀 형태로 변환
|
||||||
...material,
|
const dataWithRequirements = selectedConsolidatedMaterials.map(consolidatedMaterial => ({
|
||||||
user_requirement: userRequirements[material.id] || ''
|
...consolidatedMaterial,
|
||||||
|
// 합산된 수량으로 덮어쓰기
|
||||||
|
quantity: consolidatedMaterial.consolidatedQuantity,
|
||||||
|
// 사용자 요구사항은 대표 ID 기준 (저장된 데이터 우선)
|
||||||
|
user_requirement: savedRequests[consolidatedMaterial.id] || userRequirements[consolidatedMaterial.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -235,7 +306,7 @@ const SupportMaterialsView = ({
|
|||||||
console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes');
|
console.log('✅ 엑셀 Blob 생성 완료:', excelBlob.size, 'bytes');
|
||||||
|
|
||||||
// 2. 구매신청 생성
|
// 2. 구매신청 생성
|
||||||
const allMaterialIds = selectedMaterialsData.map(m => m.id);
|
const allMaterialIds = selectedConsolidatedMaterials.flatMap(cm => cm.consolidatedIds);
|
||||||
const response = await api.post('/purchase-request/create', {
|
const response = await api.post('/purchase-request/create', {
|
||||||
file_id: fileId,
|
file_id: fileId,
|
||||||
job_no: jobNo,
|
job_no: jobNo,
|
||||||
@@ -248,7 +319,7 @@ const SupportMaterialsView = ({
|
|||||||
size: m.size_inch || m.size_spec,
|
size: m.size_inch || m.size_spec,
|
||||||
schedule: m.schedule,
|
schedule: m.schedule,
|
||||||
material_grade: m.material_grade || m.full_material_grade,
|
material_grade: m.material_grade || m.full_material_grade,
|
||||||
quantity: m.quantity,
|
quantity: m.quantity, // 이미 합산된 수량
|
||||||
unit: m.unit,
|
unit: m.unit,
|
||||||
user_requirement: userRequirements[m.id] || ''
|
user_requirement: userRequirements[m.id] || ''
|
||||||
}))
|
}))
|
||||||
@@ -374,7 +445,7 @@ const SupportMaterialsView = ({
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 150px 180px 150px 200px',
|
gridTemplateColumns: '50px 200px 120px 150px 180px 200px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
background: '#f8fafc',
|
background: '#f8fafc',
|
||||||
@@ -460,7 +531,7 @@ const SupportMaterialsView = ({
|
|||||||
key={`consolidated-${index}`}
|
key={`consolidated-${index}`}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 150px 180px 150px 200px',
|
gridTemplateColumns: '50px 200px 120px 150px 180px 200px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
@@ -510,23 +581,81 @@ const SupportMaterialsView = ({
|
|||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.size}</div>
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.size}</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.grade}</div>
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.grade}</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.userRequirements}</div>
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.userRequirements}</div>
|
||||||
<div>
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
<input
|
{!editingRequest[consolidatedMaterial.id] && savedRequests[consolidatedMaterial.id] ? (
|
||||||
type="text"
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
value={userRequirements[consolidatedMaterial.id] || ''}
|
<>
|
||||||
onChange={(e) => setUserRequirements({
|
<div style={{
|
||||||
...userRequirements,
|
flex: 1,
|
||||||
[consolidatedMaterial.id]: e.target.value
|
padding: '8px',
|
||||||
})}
|
border: '1px solid #e5e7eb',
|
||||||
placeholder="Enter additional request..."
|
borderRadius: '4px',
|
||||||
style={{
|
fontSize: '12px',
|
||||||
width: '100%',
|
textAlign: 'center',
|
||||||
padding: '8px',
|
background: '#f9fafb',
|
||||||
border: '1px solid #d1d5db',
|
color: '#374151'
|
||||||
borderRadius: '4px',
|
}}>
|
||||||
fontSize: '12px'
|
{savedRequests[consolidatedMaterial.id]}
|
||||||
}}
|
</div>
|
||||||
/>
|
<button
|
||||||
|
onClick={() => handleEditRequest(consolidatedMaterial.id, savedRequests[consolidatedMaterial.id])}
|
||||||
|
disabled={hasAnyPurchased}
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: hasAnyPurchased ? '#d1d5db' : '#f59e0b',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '10px',
|
||||||
|
cursor: hasAnyPurchased ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: hasAnyPurchased ? 0.5 : 1,
|
||||||
|
minWidth: '40px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// 편집 상태 - 입력 필드 + 저장 버튼
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={userRequirements[consolidatedMaterial.id] || ''}
|
||||||
|
onChange={(e) => setUserRequirements({
|
||||||
|
...userRequirements,
|
||||||
|
[consolidatedMaterial.id]: e.target.value
|
||||||
|
})}
|
||||||
|
placeholder="Enter additional request..."
|
||||||
|
disabled={hasAnyPurchased}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '8px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
opacity: hasAnyPurchased ? 0.5 : 1,
|
||||||
|
cursor: hasAnyPurchased ? 'not-allowed' : 'text'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSaveRequest(consolidatedMaterial.id, userRequirements[consolidatedMaterial.id] || '')}
|
||||||
|
disabled={hasAnyPurchased || savingRequest[consolidatedMaterial.id]}
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: hasAnyPurchased ? '#d1d5db' : '#10b981',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '10px',
|
||||||
|
cursor: hasAnyPurchased || savingRequest[consolidatedMaterial.id] ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: hasAnyPurchased ? 0.5 : 1,
|
||||||
|
minWidth: '40px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{savingRequest[consolidatedMaterial.id] ? '...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.purchaseQuantity}</div>
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.purchaseQuantity}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const ValveMaterialsView = ({
|
|||||||
setUserRequirements,
|
setUserRequirements,
|
||||||
purchasedMaterials,
|
purchasedMaterials,
|
||||||
onPurchasedMaterialsUpdate,
|
onPurchasedMaterialsUpdate,
|
||||||
|
updateMaterial, // 자재 업데이트 함수
|
||||||
fileId,
|
fileId,
|
||||||
jobNo,
|
jobNo,
|
||||||
user
|
user
|
||||||
@@ -18,58 +19,154 @@ const ValveMaterialsView = ({
|
|||||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
||||||
const [columnFilters, setColumnFilters] = useState({});
|
const [columnFilters, setColumnFilters] = useState({});
|
||||||
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
const [showFilterDropdown, setShowFilterDropdown] = useState(null);
|
||||||
|
const [brandInputs, setBrandInputs] = useState({}); // 브랜드 입력 상태
|
||||||
|
const [savingBrand, setSavingBrand] = useState({}); // 브랜드 저장 중 상태
|
||||||
|
const [savingRequest, setSavingRequest] = useState({}); // 추가요구사항 저장 중 상태
|
||||||
|
const [savedBrands, setSavedBrands] = useState({}); // 저장된 브랜드 상태
|
||||||
|
const [savedRequests, setSavedRequests] = useState({}); // 저장된 추가요구사항 상태
|
||||||
|
const [editingBrand, setEditingBrand] = useState({}); // 브랜드 편집 모드
|
||||||
|
const [editingRequest, setEditingRequest] = useState({}); // 추가요구사항 편집 모드
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 저장된 데이터 로드
|
||||||
|
React.useEffect(() => {
|
||||||
|
const loadSavedData = () => {
|
||||||
|
const savedBrandsData = {};
|
||||||
|
const savedRequestsData = {};
|
||||||
|
|
||||||
|
console.log('🔍 ValveMaterialsView useEffect 트리거됨:', materials.length, '개 자재');
|
||||||
|
console.log('🔍 현재 materials 배열:', materials.map(m => ({id: m.id, brand: m.brand, user_requirement: m.user_requirement})));
|
||||||
|
|
||||||
|
materials.forEach(material => {
|
||||||
|
// 백엔드에서 가져온 데이터가 있으면 저장된 상태로 설정
|
||||||
|
if (material.brand && material.brand.trim()) {
|
||||||
|
savedBrandsData[material.id] = material.brand.trim();
|
||||||
|
console.log('✅ 브랜드 로드됨:', material.id, '→', material.brand);
|
||||||
|
}
|
||||||
|
if (material.user_requirement && material.user_requirement.trim()) {
|
||||||
|
savedRequestsData[material.id] = material.user_requirement.trim();
|
||||||
|
console.log('✅ 요구사항 로드됨:', material.id, '→', material.user_requirement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('💾 최종 저장된 브랜드:', savedBrandsData);
|
||||||
|
console.log('💾 최종 저장된 요구사항:', savedRequestsData);
|
||||||
|
|
||||||
|
// 상태 업데이트를 즉시 반영하기 위해 setTimeout 사용
|
||||||
|
setSavedBrands(savedBrandsData);
|
||||||
|
setSavedRequests(savedRequestsData);
|
||||||
|
|
||||||
|
// 상태 업데이트 후 강제 리렌더링 확인
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('🔄 상태 업데이트 후 확인 - savedBrands:', savedBrandsData);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🔄 ValveMaterialsView useEffect 실행 - materials 길이:', materials?.length || 0);
|
||||||
|
|
||||||
|
if (materials && materials.length > 0) {
|
||||||
|
loadSavedData();
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ materials가 비어있거나 undefined');
|
||||||
|
// 빈 상태로 초기화
|
||||||
|
setSavedBrands({});
|
||||||
|
setSavedRequests({});
|
||||||
|
}
|
||||||
|
}, [materials]);
|
||||||
|
|
||||||
const parseValveInfo = (material) => {
|
const parseValveInfo = (material) => {
|
||||||
const valveDetails = material.valve_details || {};
|
const valveDetails = material.valve_details || {};
|
||||||
const description = material.original_description || '';
|
const description = material.original_description || '';
|
||||||
|
const descUpper = description.toUpperCase();
|
||||||
|
|
||||||
// 밸브 타입 파싱 (GATE, BALL, CHECK, GLOBE 등) - 기존 NewMaterialsPage와 동일
|
// 1. 벨브 타입 파싱 (한글명으로 표시)
|
||||||
let valveType = valveDetails.valve_type || '';
|
let valveType = '';
|
||||||
if (!valveType && description) {
|
if (descUpper.includes('SIGHT GLASS') || descUpper.includes('사이트글라스')) {
|
||||||
if (description.includes('GATE')) valveType = 'GATE';
|
valveType = 'SIGHT GLASS';
|
||||||
else if (description.includes('BALL')) valveType = 'BALL';
|
} else if (descUpper.includes('STRAINER') || descUpper.includes('스트레이너')) {
|
||||||
else if (description.includes('CHECK')) valveType = 'CHECK';
|
valveType = 'STRAINER';
|
||||||
else if (description.includes('GLOBE')) valveType = 'GLOBE';
|
} else if (descUpper.includes('GATE') || descUpper.includes('게이트')) {
|
||||||
else if (description.includes('BUTTERFLY')) valveType = 'BUTTERFLY';
|
valveType = 'GATE VALVE';
|
||||||
else if (description.includes('NEEDLE')) valveType = 'NEEDLE';
|
} else if (descUpper.includes('BALL') || descUpper.includes('볼')) {
|
||||||
else if (description.includes('RELIEF')) valveType = 'RELIEF';
|
valveType = 'BALL VALVE';
|
||||||
|
} else if (descUpper.includes('CHECK') || descUpper.includes('체크')) {
|
||||||
|
valveType = 'CHECK VALVE';
|
||||||
|
} else if (descUpper.includes('GLOBE') || descUpper.includes('글로브')) {
|
||||||
|
valveType = 'GLOBE VALVE';
|
||||||
|
} else if (descUpper.includes('BUTTERFLY') || descUpper.includes('버터플라이')) {
|
||||||
|
valveType = 'BUTTERFLY VALVE';
|
||||||
|
} else if (descUpper.includes('NEEDLE') || descUpper.includes('니들')) {
|
||||||
|
valveType = 'NEEDLE VALVE';
|
||||||
|
} else if (descUpper.includes('RELIEF') || descUpper.includes('릴리프')) {
|
||||||
|
valveType = 'RELIEF VALVE';
|
||||||
|
} else {
|
||||||
|
valveType = 'VALVE';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 연결 방식 파싱 (FLG, SW, THRD 등) - 기존 NewMaterialsPage와 동일
|
// 2. 사이즈 정보
|
||||||
|
const size = material.main_nom || material.size_inch || material.size_spec || '-';
|
||||||
|
|
||||||
|
// 3. 압력 등급
|
||||||
|
const pressure = material.pressure_rating ||
|
||||||
|
(descUpper.match(/(\d+)\s*LB/) ? descUpper.match(/(\d+)\s*LB/)[0] : '-');
|
||||||
|
|
||||||
|
// 4. 브랜드 (사용자 입력 가능)
|
||||||
|
const brand = '-'; // 기본값, 사용자가 입력할 수 있도록
|
||||||
|
|
||||||
|
// 5. 추가 정보 추출 (3-WAY, DOUL PLATE, DOUBLE DISC 등)
|
||||||
|
let additionalInfo = '';
|
||||||
|
const additionalPatterns = [
|
||||||
|
'3-WAY', '3WAY', 'THREE WAY',
|
||||||
|
'DOUL PLATE', 'DOUBLE PLATE', 'DUAL PLATE',
|
||||||
|
'DOUBLE DISC', 'DUAL DISC',
|
||||||
|
'SWING', 'LIFT', 'TILTING',
|
||||||
|
'WAFER', 'LUG', 'FLANGED',
|
||||||
|
'FULL BORE', 'REDUCED BORE',
|
||||||
|
'FIRE SAFE', 'ANTI STATIC'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of additionalPatterns) {
|
||||||
|
if (descUpper.includes(pattern)) {
|
||||||
|
if (additionalInfo) {
|
||||||
|
additionalInfo += ', ';
|
||||||
|
}
|
||||||
|
additionalInfo += pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!additionalInfo) {
|
||||||
|
additionalInfo = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 연결 방식 (투입구/Connection Type)
|
||||||
let connectionType = '';
|
let connectionType = '';
|
||||||
if (description.includes('FLG')) {
|
if (descUpper.includes('SW X THRD') || descUpper.includes('SW×THRD')) {
|
||||||
connectionType = 'FLG';
|
|
||||||
} else if (description.includes('SW X THRD')) {
|
|
||||||
connectionType = 'SW×THRD';
|
connectionType = 'SW×THRD';
|
||||||
} else if (description.includes('SW')) {
|
} else if (descUpper.includes('FLG') || descUpper.includes('FLANGE')) {
|
||||||
|
connectionType = 'FLG';
|
||||||
|
} else if (descUpper.includes('SW') || descUpper.includes('SOCKET')) {
|
||||||
connectionType = 'SW';
|
connectionType = 'SW';
|
||||||
} else if (description.includes('THRD')) {
|
} else if (descUpper.includes('THRD') || descUpper.includes('THREAD')) {
|
||||||
connectionType = 'THRD';
|
connectionType = 'THRD';
|
||||||
} else if (description.includes('BW')) {
|
} else if (descUpper.includes('BW') || descUpper.includes('BUTT WELD')) {
|
||||||
connectionType = 'BW';
|
connectionType = 'BW';
|
||||||
|
} else {
|
||||||
|
connectionType = '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 압력 등급 파싱
|
// 7. 구매 수량 계산 (기본 수량 그대로)
|
||||||
let pressure = '-';
|
const qty = Math.round(material.quantity || 0);
|
||||||
const pressureMatch = description.match(/(\d+)LB/i);
|
const purchaseQuantity = `${qty} EA`;
|
||||||
if (pressureMatch) {
|
|
||||||
pressure = `${pressureMatch[1]}LB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 스케줄은 밸브에는 일반적으로 없음 (기본값)
|
|
||||||
let schedule = '-';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'VALVE',
|
type: valveType, // 벨브 종류만 (GATE VALVE, BALL VALVE 등)
|
||||||
subtype: `${valveType} ${connectionType}`.trim() || 'VALVE', // 타입과 연결방식 결합
|
size: size,
|
||||||
valveType: valveType,
|
|
||||||
connectionType: connectionType,
|
|
||||||
size: material.size_spec || '-',
|
|
||||||
pressure: pressure,
|
pressure: pressure,
|
||||||
schedule: schedule,
|
brand: brand, // 브랜드 (사용자 입력 가능)
|
||||||
grade: material.material_grade || '-',
|
additionalInfo: additionalInfo, // 추가 정보 (3-WAY, DOUL PLATE 등)
|
||||||
quantity: Math.round(material.quantity || 0),
|
connection: connectionType, // 투입구/연결방식 (SW, FLG 등)
|
||||||
unit: '개',
|
additionalRequest: '-', // 추가 요구사항 (기존 User Requirement)
|
||||||
|
purchaseQuantity: purchaseQuantity,
|
||||||
|
originalQuantity: qty,
|
||||||
isValve: true
|
isValve: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -130,6 +227,68 @@ const ValveMaterialsView = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 전체 선택/해제 (구매신청된 자재 제외)
|
// 전체 선택/해제 (구매신청된 자재 제외)
|
||||||
|
// 브랜드 저장 함수
|
||||||
|
const handleSaveBrand = async (materialId, brand) => {
|
||||||
|
if (!brand.trim()) return;
|
||||||
|
|
||||||
|
setSavingBrand(prev => ({ ...prev, [materialId]: true }));
|
||||||
|
try {
|
||||||
|
await api.patch(`/materials/${materialId}/brand`, { brand: brand.trim() });
|
||||||
|
// 성공 시 저장된 상태로 전환
|
||||||
|
setSavedBrands(prev => ({ ...prev, [materialId]: brand.trim() }));
|
||||||
|
setEditingBrand(prev => ({ ...prev, [materialId]: false }));
|
||||||
|
setBrandInputs(prev => ({ ...prev, [materialId]: '' })); // 입력 필드 초기화
|
||||||
|
|
||||||
|
// materials 배열도 업데이트 (카테고리 변경 시 데이터 유지를 위해)
|
||||||
|
if (updateMaterial) {
|
||||||
|
updateMaterial(materialId, { brand: brand.trim() });
|
||||||
|
console.log('✅ materials 배열 업데이트 완료:', materialId, '→', brand.trim());
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('브랜드 저장 실패:', error);
|
||||||
|
alert('브랜드 저장에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
setSavingBrand(prev => ({ ...prev, [materialId]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 브랜드 편집 시작
|
||||||
|
const handleEditBrand = (materialId, currentBrand) => {
|
||||||
|
setEditingBrand(prev => ({ ...prev, [materialId]: true }));
|
||||||
|
setBrandInputs(prev => ({ ...prev, [materialId]: currentBrand || '' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 추가요구사항 저장 함수
|
||||||
|
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]: '' })); // 입력 필드 초기화
|
||||||
|
|
||||||
|
// materials 배열도 업데이트 (카테고리 변경 시 데이터 유지를 위해)
|
||||||
|
if (updateMaterial) {
|
||||||
|
updateMaterial(materialId, { user_requirement: request.trim() });
|
||||||
|
console.log('✅ materials 배열 업데이트 완료:', materialId, '→', 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 handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
const filteredMaterials = getFilteredAndSortedMaterials();
|
const filteredMaterials = getFilteredAndSortedMaterials();
|
||||||
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
|
const selectableMaterials = filteredMaterials.filter(m => !purchasedMaterials.has(m.id));
|
||||||
@@ -169,7 +328,7 @@ const ValveMaterialsView = ({
|
|||||||
|
|
||||||
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
const dataWithRequirements = selectedMaterialsData.map(material => ({
|
||||||
...material,
|
...material,
|
||||||
user_requirement: userRequirements[material.id] || ''
|
user_requirement: savedRequests[material.id] || userRequirements[material.id] || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -289,45 +448,48 @@ const ValveMaterialsView = ({
|
|||||||
>
|
>
|
||||||
{selectedMaterials.size === filteredMaterials.length ? 'Deselect All' : 'Select All'}
|
{selectedMaterials.size === filteredMaterials.length ? 'Deselect All' : 'Select All'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleExportToExcel}
|
onClick={handleExportToExcel}
|
||||||
disabled={selectedMaterials.size === 0}
|
disabled={selectedMaterials.size === 0}
|
||||||
style={{
|
style={{
|
||||||
background: selectedMaterials.size > 0 ? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' : '#e5e7eb',
|
background: selectedMaterials.size > 0 ? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' : '#e5e7eb',
|
||||||
color: selectedMaterials.size > 0 ? 'white' : '#9ca3af',
|
color: selectedMaterials.size > 0 ? 'white' : '#9ca3af',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '10px 16px',
|
padding: '10px 16px',
|
||||||
cursor: selectedMaterials.size > 0 ? 'pointer' : 'not-allowed',
|
cursor: selectedMaterials.size > 0 ? 'pointer' : 'not-allowed',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: '500'
|
fontWeight: '500'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Export to Excel ({selectedMaterials.size})
|
Export to Excel ({selectedMaterials.size})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'white',
|
background: 'white',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
overflow: 'hidden',
|
overflow: 'auto',
|
||||||
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)'
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
maxHeight: '600px'
|
||||||
}}>
|
}}>
|
||||||
{/* 헤더 */}
|
<div style={{ minWidth: '1600px' }}>
|
||||||
<div style={{
|
{/* 헤더 */}
|
||||||
display: 'grid',
|
<div style={{
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
display: 'grid',
|
||||||
gap: '16px',
|
gridTemplateColumns: '50px 200px 120px 120px 150px 180px 180px 150px 200px',
|
||||||
padding: '16px',
|
gap: '16px',
|
||||||
background: '#f8fafc',
|
padding: '16px',
|
||||||
borderBottom: '1px solid #e2e8f0',
|
background: '#f8fafc',
|
||||||
fontSize: '14px',
|
borderBottom: '1px solid #e2e8f0',
|
||||||
fontWeight: '600',
|
fontSize: '14px',
|
||||||
color: '#374151'
|
fontWeight: '600',
|
||||||
}}>
|
color: '#374151',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -339,19 +501,84 @@ const ValveMaterialsView = ({
|
|||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FilterableHeader sortKey="subtype" filterKey="subtype">Type</FilterableHeader>
|
<FilterableHeader
|
||||||
<FilterableHeader sortKey="size" filterKey="size">Size</FilterableHeader>
|
sortKey="type"
|
||||||
<FilterableHeader sortKey="pressure" filterKey="pressure">Pressure</FilterableHeader>
|
filterKey="type"
|
||||||
<FilterableHeader sortKey="schedule" filterKey="schedule">Schedule</FilterableHeader>
|
sortConfig={sortConfig}
|
||||||
<FilterableHeader sortKey="grade" filterKey="grade">Material Grade</FilterableHeader>
|
onSort={handleSort}
|
||||||
<FilterableHeader sortKey="quantity" filterKey="quantity">Quantity</FilterableHeader>
|
columnFilters={columnFilters}
|
||||||
<div>Unit</div>
|
onFilterChange={setColumnFilters}
|
||||||
<div>User Requirement</div>
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="size"
|
||||||
|
filterKey="size"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="pressure"
|
||||||
|
filterKey="pressure"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Pressure
|
||||||
|
</FilterableHeader>
|
||||||
|
<div>Brand</div>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="additionalInfo"
|
||||||
|
filterKey="additionalInfo"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Additional Info
|
||||||
|
</FilterableHeader>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="connection"
|
||||||
|
filterKey="connection"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Connection
|
||||||
|
</FilterableHeader>
|
||||||
|
<div>Additional Request</div>
|
||||||
|
<FilterableHeader
|
||||||
|
sortKey="purchaseQuantity"
|
||||||
|
filterKey="purchaseQuantity"
|
||||||
|
sortConfig={sortConfig}
|
||||||
|
onSort={handleSort}
|
||||||
|
columnFilters={columnFilters}
|
||||||
|
onFilterChange={setColumnFilters}
|
||||||
|
showFilterDropdown={showFilterDropdown}
|
||||||
|
setShowFilterDropdown={setShowFilterDropdown}
|
||||||
|
>
|
||||||
|
Purchase Quantity
|
||||||
|
</FilterableHeader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 데이터 행들 */}
|
{/* 데이터 행들 */}
|
||||||
<div style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
{filteredMaterials.map((material, index) => {
|
||||||
{filteredMaterials.map((material, index) => {
|
|
||||||
const info = parseValveInfo(material);
|
const info = parseValveInfo(material);
|
||||||
const isSelected = selectedMaterials.has(material.id);
|
const isSelected = selectedMaterials.has(material.id);
|
||||||
const isPurchased = purchasedMaterials.has(material.id);
|
const isPurchased = purchasedMaterials.has(material.id);
|
||||||
@@ -360,89 +587,230 @@ const ValveMaterialsView = ({
|
|||||||
<div
|
<div
|
||||||
key={material.id}
|
key={material.id}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: '50px 200px 120px 100px 120px 150px 80px 80px 200px',
|
gridTemplateColumns: '50px 200px 120px 120px 150px 180px 180px 150px 200px',
|
||||||
gap: '16px',
|
gap: '16px',
|
||||||
padding: '16px',
|
padding: '16px',
|
||||||
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
borderBottom: index < filteredMaterials.length - 1 ? '1px solid #f1f5f9' : 'none',
|
||||||
background: isSelected ? '#eff6ff' : (isPurchased ? '#fef3c7' : 'white'),
|
background: isSelected ? '#eff6ff' : (isPurchased ? '#fef3c7' : 'white'),
|
||||||
transition: 'background 0.15s ease'
|
transition: 'background 0.15s ease',
|
||||||
}}
|
textAlign: 'center'
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
if (!isSelected && !isPurchased) {
|
onMouseEnter={(e) => {
|
||||||
e.target.style.background = '#f8fafc';
|
if (!isSelected && !isPurchased) {
|
||||||
|
e.target.style.background = '#f8fafc';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (!isSelected && !isPurchased) {
|
||||||
|
e.target.style.background = 'white';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => handleMaterialSelect(material.id)}
|
||||||
|
disabled={isPurchased}
|
||||||
|
style={{
|
||||||
|
cursor: isPurchased ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: isPurchased ? 0.5 : 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
|
||||||
|
{info.type}
|
||||||
|
{isPurchased && (
|
||||||
|
<span style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
padding: '2px 6px',
|
||||||
|
background: '#fbbf24',
|
||||||
|
color: '#92400e',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}>
|
||||||
|
PURCHASED
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.size}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.pressure}</div>
|
||||||
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
|
{(() => {
|
||||||
|
// 디버깅: 렌더링 시점의 상태 확인
|
||||||
|
const hasEditingBrand = !!editingBrand[material.id];
|
||||||
|
const hasSavedBrand = !!savedBrands[material.id];
|
||||||
|
const shouldShowSaved = !hasEditingBrand && hasSavedBrand;
|
||||||
|
|
||||||
|
if (material.id === 11789) { // 테스트 자재만 로그
|
||||||
|
console.log(`🎨 UI 렌더링 - ID ${material.id}:`, {
|
||||||
|
editingBrand: hasEditingBrand,
|
||||||
|
savedBrandExists: hasSavedBrand,
|
||||||
|
savedBrandValue: savedBrands[material.id],
|
||||||
|
shouldShowSaved: shouldShowSaved,
|
||||||
|
allSavedBrands: Object.keys(savedBrands),
|
||||||
|
renderingMode: shouldShowSaved ? 'SAVED_VIEW' : 'INPUT_VIEW'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
// 명시적으로 boolean 반환
|
||||||
if (!isSelected && !isPurchased) {
|
return shouldShowSaved ? true : false;
|
||||||
e.target.style.background = 'white';
|
})() ? (
|
||||||
}
|
// 저장된 상태 - 브랜드 표시 + 수정 버튼
|
||||||
}}
|
<>
|
||||||
>
|
<div style={{
|
||||||
<div>
|
flex: 1,
|
||||||
<input
|
padding: '6px',
|
||||||
type="checkbox"
|
border: '1px solid #e5e7eb',
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => handleMaterialSelect(material.id)}
|
|
||||||
disabled={isPurchased}
|
|
||||||
style={{
|
|
||||||
cursor: isPurchased ? 'not-allowed' : 'pointer',
|
|
||||||
opacity: isPurchased ? 0.5 : 1
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '500' }}>
|
|
||||||
{info.subtype}
|
|
||||||
{isPurchased && (
|
|
||||||
<span style={{
|
|
||||||
marginLeft: '8px',
|
|
||||||
padding: '2px 6px',
|
|
||||||
background: '#fbbf24',
|
|
||||||
color: '#92400e',
|
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '10px',
|
fontSize: '11px',
|
||||||
fontWeight: '500'
|
textAlign: 'center',
|
||||||
|
background: '#f9fafb',
|
||||||
|
color: '#374151'
|
||||||
}}>
|
}}>
|
||||||
PURCHASED
|
{savedBrands[material.id]}
|
||||||
</span>
|
</div>
|
||||||
)}
|
<button
|
||||||
</div>
|
onClick={() => handleEditBrand(material.id, savedBrands[material.id])}
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
disabled={isPurchased}
|
||||||
{info.size}
|
style={{
|
||||||
</div>
|
padding: '6px 8px',
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
border: 'none',
|
||||||
{info.pressure}
|
borderRadius: '4px',
|
||||||
</div>
|
background: isPurchased ? '#d1d5db' : '#f59e0b',
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
color: 'white',
|
||||||
{info.schedule}
|
fontSize: '10px',
|
||||||
</div>
|
cursor: isPurchased ? 'not-allowed' : 'pointer',
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937' }}>
|
opacity: isPurchased ? 0.5 : 1,
|
||||||
{info.grade}
|
minWidth: '40px'
|
||||||
</div>
|
}}
|
||||||
<div style={{ fontSize: '14px', color: '#1f2937', fontWeight: '600', textAlign: 'right' }}>
|
>
|
||||||
{info.quantity}
|
Edit
|
||||||
</div>
|
</button>
|
||||||
<div style={{ fontSize: '14px', color: '#6b7280' }}>
|
</>
|
||||||
{info.unit}
|
) : (
|
||||||
</div>
|
// 편집 상태 - 입력 필드 + 저장 버튼
|
||||||
<div>
|
<>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={userRequirements[material.id] || ''}
|
value={brandInputs[material.id] || ''}
|
||||||
onChange={(e) => setUserRequirements({
|
onChange={(e) => setBrandInputs({
|
||||||
...userRequirements,
|
...brandInputs,
|
||||||
[material.id]: e.target.value
|
[material.id]: e.target.value
|
||||||
})}
|
})}
|
||||||
placeholder="Enter requirement..."
|
placeholder="Enter brand..."
|
||||||
style={{
|
disabled={isPurchased}
|
||||||
width: '100%',
|
style={{
|
||||||
padding: '6px 8px',
|
flex: 1,
|
||||||
border: '1px solid #d1d5db',
|
padding: '6px',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '11px',
|
||||||
|
textAlign: 'center',
|
||||||
|
opacity: isPurchased ? 0.5 : 1,
|
||||||
|
cursor: isPurchased ? 'not-allowed' : 'text'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => handleSaveBrand(material.id, brandInputs[material.id] || '')}
|
||||||
|
disabled={isPurchased || savingBrand[material.id] || !brandInputs[material.id]?.trim()}
|
||||||
|
style={{
|
||||||
|
padding: '6px 8px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: isPurchased ? '#d1d5db' : '#3b82f6',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '10px',
|
||||||
|
cursor: isPurchased || savingBrand[material.id] ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: isPurchased || !brandInputs[material.id]?.trim() ? 0.5 : 1,
|
||||||
|
minWidth: '40px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{savingBrand[material.id] ? '...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.additionalInfo}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#1f2937' }}>{info.connection}</div>
|
||||||
|
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
|
||||||
|
{!editingRequest[material.id] && savedRequests[material.id] ? (
|
||||||
|
// 저장된 상태 - 요구사항 표시 + 수정 버튼
|
||||||
|
<>
|
||||||
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '6px',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
fontSize: '12px'
|
fontSize: '11px',
|
||||||
}}
|
background: '#f9fafb',
|
||||||
/>
|
color: '#374151'
|
||||||
</div>
|
}}>
|
||||||
|
{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',
|
||||||
|
border: '1px solid #d1d5db',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '11px',
|
||||||
|
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' }}>{info.purchaseQuantity}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ const BOMManagementPage = ({
|
|||||||
const [purchasedMaterials, setPurchasedMaterials] = useState(new Set());
|
const [purchasedMaterials, setPurchasedMaterials] = useState(new Set());
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
// 자재 업데이트 함수 (브랜드, 사용자 요구사항 등)
|
||||||
|
const updateMaterial = (materialId, updates) => {
|
||||||
|
setMaterials(prevMaterials =>
|
||||||
|
prevMaterials.map(material =>
|
||||||
|
material.id === materialId
|
||||||
|
? { ...material, ...updates }
|
||||||
|
: material
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// 카테고리 정의
|
// 카테고리 정의
|
||||||
const categories = [
|
const categories = [
|
||||||
{ key: 'PIPE', label: 'Pipes', color: '#3b82f6' },
|
{ key: 'PIPE', label: 'Pipes', color: '#3b82f6' },
|
||||||
@@ -175,6 +186,7 @@ const BOMManagementPage = ({
|
|||||||
return newSet;
|
return newSet;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateMaterial, // 자재 업데이트 함수 추가
|
||||||
fileId,
|
fileId,
|
||||||
jobNo,
|
jobNo,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -115,6 +115,56 @@ const consolidateMaterials = (materials, isComparison = false) => {
|
|||||||
return Object.values(consolidated);
|
return Object.values(consolidated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 벨브 연결방식 추출 함수
|
||||||
|
*/
|
||||||
|
const extractValveConnectionType = (description) => {
|
||||||
|
const descUpper = description.toUpperCase();
|
||||||
|
|
||||||
|
if (descUpper.includes('SW X THRD') || descUpper.includes('SW×THRD')) {
|
||||||
|
return 'SW×THRD';
|
||||||
|
} else if (descUpper.includes('FLG') || descUpper.includes('FLANGE')) {
|
||||||
|
return 'FLG';
|
||||||
|
} else if (descUpper.includes('SW') || descUpper.includes('SOCKET')) {
|
||||||
|
return 'SW';
|
||||||
|
} else if (descUpper.includes('THRD') || descUpper.includes('THREAD')) {
|
||||||
|
return 'THRD';
|
||||||
|
} else if (descUpper.includes('BW') || descUpper.includes('BUTT WELD')) {
|
||||||
|
return 'BW';
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 벨브 추가 정보 추출 함수
|
||||||
|
*/
|
||||||
|
const extractValveAdditionalInfo = (description) => {
|
||||||
|
const descUpper = description.toUpperCase();
|
||||||
|
let additionalInfo = '';
|
||||||
|
|
||||||
|
const additionalPatterns = [
|
||||||
|
'3-WAY', '3WAY', 'THREE WAY',
|
||||||
|
'DOUL PLATE', 'DOUBLE PLATE', 'DUAL PLATE',
|
||||||
|
'DOUBLE DISC', 'DUAL DISC',
|
||||||
|
'SWING', 'LIFT', 'TILTING',
|
||||||
|
'WAFER', 'LUG', 'FLANGED',
|
||||||
|
'FULL BORE', 'REDUCED BORE',
|
||||||
|
'FIRE SAFE', 'ANTI STATIC'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of additionalPatterns) {
|
||||||
|
if (descUpper.includes(pattern)) {
|
||||||
|
if (additionalInfo) {
|
||||||
|
additionalInfo += ', ';
|
||||||
|
}
|
||||||
|
additionalInfo += pattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return additionalInfo || '-';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 자재 데이터를 엑셀용 형태로 변환
|
* 자재 데이터를 엑셀용 형태로 변환
|
||||||
*/
|
*/
|
||||||
@@ -401,24 +451,28 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
} else if (valveType === 'RELIEF') {
|
} else if (valveType === 'RELIEF') {
|
||||||
itemName = '릴리프 밸브';
|
itemName = '릴리프 밸브';
|
||||||
} else {
|
} else {
|
||||||
// description에서 추출
|
// description에서 추출 (BOM 페이지와 동일한 로직)
|
||||||
const desc = cleanDescription.toUpperCase();
|
const desc = cleanDescription.toUpperCase();
|
||||||
if (desc.includes('GATE')) {
|
if (desc.includes('SIGHT GLASS') || desc.includes('사이트글라스')) {
|
||||||
itemName = '게이트 밸브';
|
itemName = 'SIGHT GLASS';
|
||||||
} else if (desc.includes('BALL')) {
|
} else if (desc.includes('STRAINER') || desc.includes('스트레이너')) {
|
||||||
itemName = '볼 밸브';
|
itemName = 'STRAINER';
|
||||||
} else if (desc.includes('GLOBE')) {
|
} else if (desc.includes('GATE') || desc.includes('게이트')) {
|
||||||
itemName = '글로브 밸브';
|
itemName = 'GATE VALVE';
|
||||||
} else if (desc.includes('CHECK')) {
|
} else if (desc.includes('BALL') || desc.includes('볼')) {
|
||||||
itemName = '체크 밸브';
|
itemName = 'BALL VALVE';
|
||||||
} else if (desc.includes('BUTTERFLY')) {
|
} else if (desc.includes('CHECK') || desc.includes('체크')) {
|
||||||
itemName = '버터플라이 밸브';
|
itemName = 'CHECK VALVE';
|
||||||
} else if (desc.includes('NEEDLE')) {
|
} else if (desc.includes('GLOBE') || desc.includes('글로브')) {
|
||||||
itemName = '니들 밸브';
|
itemName = 'GLOBE VALVE';
|
||||||
} else if (desc.includes('RELIEF')) {
|
} else if (desc.includes('BUTTERFLY') || desc.includes('버터플라이')) {
|
||||||
itemName = '릴리프 밸브';
|
itemName = 'BUTTERFLY VALVE';
|
||||||
|
} else if (desc.includes('NEEDLE') || desc.includes('니들')) {
|
||||||
|
itemName = 'NEEDLE VALVE';
|
||||||
|
} else if (desc.includes('RELIEF') || desc.includes('릴리프')) {
|
||||||
|
itemName = 'RELIEF VALVE';
|
||||||
} else {
|
} else {
|
||||||
itemName = '밸브';
|
itemName = 'VALVE';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (category === 'GASKET') {
|
} else if (category === 'GASKET') {
|
||||||
@@ -489,13 +543,15 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
const desc = cleanDescription.toUpperCase();
|
const desc = cleanDescription.toUpperCase();
|
||||||
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
|
if (desc.includes('URETHANE') || desc.includes('BLOCK SHOE')) {
|
||||||
itemName = 'URETHANE BLOCK SHOE';
|
itemName = 'URETHANE BLOCK SHOE';
|
||||||
// 우레탄 블럭슈의 경우 두께 정보 추가
|
// 우레탄 블럭슈의 경우 두께 정보는 품목명에 포함하지 않음 (재질 열에서 처리)
|
||||||
const thicknessMatch = desc.match(/(\d+)\s*[tT](?![oO])/);
|
|
||||||
if (thicknessMatch) {
|
|
||||||
itemName += ` ${thicknessMatch[1]}t`;
|
|
||||||
}
|
|
||||||
} else if (desc.includes('CLAMP')) {
|
} else if (desc.includes('CLAMP')) {
|
||||||
itemName = 'CLAMP';
|
// 클램프 타입 상세 분류 (CL-1, CL-2, CL-3 등)
|
||||||
|
const clampMatch = desc.match(/CL[-\s]*(\d+)/i);
|
||||||
|
if (clampMatch) {
|
||||||
|
itemName = `CLAMP CL-${clampMatch[1]}`;
|
||||||
|
} else {
|
||||||
|
itemName = 'CLAMP CL-1'; // 기본값
|
||||||
|
}
|
||||||
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
} else if (desc.includes('U-BOLT') || desc.includes('U BOLT')) {
|
||||||
itemName = 'U-BOLT';
|
itemName = 'U-BOLT';
|
||||||
} else if (desc.includes('HANGER')) {
|
} else if (desc.includes('HANGER')) {
|
||||||
@@ -823,17 +879,17 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
base['관리항목3'] = ''; // N열
|
base['관리항목3'] = ''; // N열
|
||||||
base['관리항목4'] = ''; // O열
|
base['관리항목4'] = ''; // O열
|
||||||
} else if (category === 'VALVE') {
|
} else if (category === 'VALVE') {
|
||||||
// 밸브 전용 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
// 밸브 전용 컬럼 (F~O) - 새로운 구조
|
||||||
base['크기'] = material.size_spec || '-'; // F열
|
base['크기'] = material.size_spec || material.main_nom || '-'; // F열
|
||||||
base['압력등급'] = pressure; // G열
|
base['압력등급'] = pressure; // G열
|
||||||
base['재질'] = grade; // H열
|
base['브랜드'] = material.brand || '-'; // H열 (사용자 입력)
|
||||||
base['상세내역'] = detailInfo || '-'; // I열
|
base['추가정보'] = extractValveAdditionalInfo(cleanDescription); // I열 (3-WAY, DOUL PLATE 등)
|
||||||
base['사용자요구'] = material.user_requirement || ''; // J열
|
base['연결방식'] = material.connection_type || extractValveConnectionType(cleanDescription); // J열
|
||||||
base['관리항목1'] = ''; // K열
|
base['추가요청사항'] = material.user_requirement || ''; // K열
|
||||||
base['관리항목2'] = ''; // L열
|
base['관리항목1'] = ''; // L열
|
||||||
base['관리항목3'] = ''; // M열
|
base['관리항목2'] = ''; // M열
|
||||||
base['관리항목4'] = ''; // N열
|
base['관리항목3'] = ''; // N열
|
||||||
base['관리항목5'] = ''; // O열
|
base['관리항목4'] = ''; // O열
|
||||||
} else if (category === 'GASKET') {
|
} else if (category === 'GASKET') {
|
||||||
// 가스켓 전용 컬럼 (F~O) - 재질을 2개로 분리
|
// 가스켓 전용 컬럼 (F~O) - 재질을 2개로 분리
|
||||||
// 재질 분리 로직: SS304/GRAPHITE/SS304/SS304 → 재질1: SS304/GRAPHITE, 재질2: /SS304/SS304
|
// 재질 분리 로직: SS304/GRAPHITE/SS304/SS304 → 재질1: SS304/GRAPHITE, 재질2: /SS304/SS304
|
||||||
@@ -866,10 +922,10 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
base['재질1'] = material1; // I열: SS304/GRAPHITE
|
base['재질1'] = material1; // I열: SS304/GRAPHITE
|
||||||
base['재질2'] = material2; // J열: SS304/SS304
|
base['재질2'] = material2; // J열: SS304/SS304
|
||||||
base['두께'] = gasketThickness || '-'; // K열: 4.5mm
|
base['두께'] = gasketThickness || '-'; // K열: 4.5mm
|
||||||
base['사용자요구'] = material.user_requirement || ''; // L열
|
base['사용자요구'] = detailInfo; // L열: 분류기에서 추출된 요구사항
|
||||||
base['관리항목1'] = ''; // M열
|
base['추가요청사항'] = material.user_requirement || ''; // M열: 사용자 입력 요구사항
|
||||||
base['관리항목2'] = ''; // N열
|
base['관리항목1'] = ''; // N열
|
||||||
base['관리항목3'] = ''; // O열
|
base['관리항목2'] = ''; // O열
|
||||||
} else if (category === 'BOLT') {
|
} else if (category === 'BOLT') {
|
||||||
// 볼트 전용 컬럼 (F~O) - User Requirements와 Additional Request 분리
|
// 볼트 전용 컬럼 (F~O) - User Requirements와 Additional Request 분리
|
||||||
base['크기'] = material.size_spec || '-'; // F열
|
base['크기'] = material.size_spec || '-'; // F열
|
||||||
@@ -883,17 +939,17 @@ const formatMaterialForExcel = (material, includeComparison = false) => {
|
|||||||
base['관리항목3'] = ''; // N열
|
base['관리항목3'] = ''; // N열
|
||||||
base['관리항목4'] = ''; // O열
|
base['관리항목4'] = ''; // O열
|
||||||
} else if (category === 'SUPPORT') {
|
} else if (category === 'SUPPORT') {
|
||||||
// 서포트 전용 컬럼 (F~O)
|
// 서포트 전용 컬럼 (F~O) - 압력등급, 상세내역 제거
|
||||||
base['크기'] = material.size_spec || material.main_nom || '-'; // F열
|
base['크기'] = material.size_spec || material.main_nom || '-'; // F열
|
||||||
base['압력등급'] = pressure; // G열
|
base['재질'] = grade; // G열
|
||||||
base['재질'] = grade; // H열
|
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // H열 (분류기에서 추출)
|
||||||
base['상세내역'] = material.original_description || '-'; // I열
|
base['추가요청사항'] = material.user_requirement || ''; // I열 (사용자 입력)
|
||||||
base['사용자요구'] = material.user_requirements?.join(', ') || ''; // J열 (분류기에서 추출)
|
base['관리항목1'] = ''; // J열
|
||||||
base['추가요청사항'] = material.user_requirement || ''; // K열 (사용자 입력)
|
base['관리항목2'] = ''; // K열
|
||||||
base['관리항목1'] = ''; // L열
|
base['관리항목3'] = ''; // L열
|
||||||
base['관리항목2'] = ''; // M열
|
base['관리항목4'] = ''; // M열
|
||||||
base['관리항목3'] = ''; // N열
|
base['관리항목5'] = ''; // N열
|
||||||
base['관리항목4'] = ''; // O열
|
base['관리항목6'] = ''; // O열
|
||||||
} else {
|
} else {
|
||||||
// 기타 카테고리 기본 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
// 기타 카테고리 기본 컬럼 (F~O) - 타입 제거, 품목명에 포함됨
|
||||||
base['크기'] = material.size_spec || '-'; // F열
|
base['크기'] = material.size_spec || '-'; // F열
|
||||||
@@ -1153,10 +1209,10 @@ export const createExcelBlob = async (materials, filename, options = {}) => {
|
|||||||
'PIPE': ['크기', '스케줄', '재질', '제조방법', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'PIPE': ['크기', '스케줄', '재질', '제조방법', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'FITTING': ['크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'FITTING': ['크기', '압력등급', '스케줄', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'FLANGE': ['크기', '페이싱', '압력등급', '스케줄', '재질', '추가요구사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'VALVE': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'VALVE': ['크기', '압력등급', '브랜드', '추가정보', '연결방식', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '관리항목1', '관리항목2', '관리항목3'],
|
'GASKET': ['크기', '압력등급', '구조', '재질1', '재질2', '두께', '사용자요구', '추가요청사항', '관리항목1', '관리항목2'],
|
||||||
'BOLT': ['크기', '압력등급', '길이', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'BOLT': ['크기', '압력등급', '길이', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
||||||
'SUPPORT': ['크기', '압력등급', '재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3', '관리항목4'],
|
'SUPPORT': ['크기', '재질', '사용자요구', '추가요청사항', '관리항목1', '관리항목2', '관리항목3', '관리항목4', '관리항목5', '관리항목6'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const deliveryDateHeader = '납기일(YYYY-MM-DD)';
|
const deliveryDateHeader = '납기일(YYYY-MM-DD)';
|
||||||
|
|||||||
Reference in New Issue
Block a user