feat: 리비전 관리 시스템 완전 개편

변동이력 관리로 전환:
- 도면번호 기준 변경 추적
- 리비전 업로드 시 전체 자재 저장 (차이분만 저장 방식 폐지)
- 구매신청 정보 수량 기반 상속

리비전 변경 감지:
- 수량/재질/크기/카테고리 변경 감지
- 변경 유형: specification_changed, quantity_changed, added, removed
- 도면별 변경사항 추적

누락 도면 처리:
- 리비전 업로드 시 누락된 도면 자동 감지
- 3가지 선택 옵션: 일부 업로드 / 도면 삭제 / 취소
- 구매신청 여부에 따라 다른 처리 (재고품 vs 숨김)

자재 상태 관리:
- revision_status 컬럼 추가 (active/inventory/deleted_not_purchased/changed)
- 재고품: 연노랑색 배경, '재고품' 배지
- 변경됨: 파란색 테두리, '변경됨' 배지
- 삭제됨: 자동 숨김

구매신청 정보 상속:
- 수량 기반 상속 (그룹별 개수만큼만)
- Rev.0에서 3개 구매 → Rev.1에서 처음 3개만 상속, 추가분은 미구매
- 도면번호 정확히 일치하는 경우에만 상속

기타 개선:
- 구매신청 관리 페이지 수량 표시 개선 (3 EA, 소수점 제거)
- 도면번호/라인번호 파싱 및 저장 (DWG_NAME, LINE_NUM 컬럼)
- SPECIAL 카테고리 도면번호 표시
- 마이그레이션 스크립트 추가 (29_add_revision_status.sql)
This commit is contained in:
Hyungi Ahn
2025-10-14 14:30:34 +09:00
parent e27020ae9b
commit 5a21ef8f6c
27 changed files with 5848 additions and 8503 deletions

View File

@@ -533,72 +533,67 @@ async def upload_file(
})
materials_inserted += 1
# 리비전 업로드 경우 차이분 계산
materials_diff = []
original_materials_to_classify = materials_to_classify.copy() # 원본 보존
# 리비전 업로드 경우 변경사항 추적
changed_materials_keys = set()
new_materials_keys = set()
if parent_file_id is not None:
# 새 파일의 자재들을 수량별로 그룹화
new_materials_grouped = {}
for material_data in original_materials_to_classify:
description = material_data["original_description"]
size_spec = material_data["size_spec"]
quantity = float(material_data.get("quantity", 0))
material_key = f"{description}|{size_spec or ''}"
if material_key in new_materials_grouped:
new_materials_grouped[material_key]["quantity"] += quantity
new_materials_grouped[material_key]["items"].append(material_data)
print(f"🔄 리비전 업로드: 전체 {len(materials_to_classify)}개 자재 저장 (변경사항 추적 포함)")
# 이전 리비전의 자재 조회 (도면번호 기준)
prev_materials_query = text("""
SELECT original_description, size_spec, material_grade, main_nom,
drawing_name, line_no, quantity
FROM materials
WHERE file_id = :parent_file_id
""")
prev_materials_result = db.execute(prev_materials_query, {"parent_file_id": parent_file_id}).fetchall()
# 이전 자재를 딕셔너리로 변환 (도면번호 + 설명 + 크기 + 재질로 키 생성)
prev_materials_dict = {}
for prev_mat in prev_materials_result:
if prev_mat.drawing_name:
key = f"{prev_mat.drawing_name}|{prev_mat.original_description}|{prev_mat.size_spec or ''}|{prev_mat.material_grade or ''}"
elif prev_mat.line_no:
key = f"{prev_mat.line_no}|{prev_mat.original_description}|{prev_mat.size_spec or ''}|{prev_mat.material_grade or ''}"
else:
new_materials_grouped[material_key] = {
"quantity": quantity,
"items": [material_data],
"description": description,
"size_spec": size_spec
}
# 차이분 계산
print(f"🔍 차이분 계산 시작: 신규 {len(new_materials_grouped)}개 vs 기존 {len(existing_materials_with_quantity)}")
for material_key, new_data in new_materials_grouped.items():
existing_quantity = existing_materials_with_quantity.get(material_key, 0)
new_quantity = new_data["quantity"]
key = f"{prev_mat.original_description}|{prev_mat.size_spec or ''}|{prev_mat.material_grade or ''}"
# 디버깅: 첫 10개 키 매칭 상태 출력
if len(materials_diff) < 10:
print(f"🔑 키 매칭: '{material_key}' → 기존:{existing_quantity}, 신규:{new_quantity}")
prev_materials_dict[key] = {
"quantity": float(prev_mat.quantity) if prev_mat.quantity else 0,
"description": prev_mat.original_description
}
# 새 자재와 비교하여 변경사항 감지
for material_data in materials_to_classify:
desc = material_data["original_description"]
size_spec = material_data.get("size_spec", "")
material_grade = material_data.get("material_grade", "")
dwg_name = material_data.get("dwg_name")
line_num = material_data.get("line_num")
if new_quantity > existing_quantity:
# 증가분이 있는 경우
diff_quantity = new_quantity - existing_quantity
print(f"차이분 발견: {new_data['description'][:50]}... (증가: {diff_quantity})")
if dwg_name:
new_key = f"{dwg_name}|{desc}|{size_spec}|{material_grade}"
elif line_num:
new_key = f"{line_num}|{desc}|{size_spec}|{material_grade}"
else:
new_key = f"{desc}|{size_spec}|{material_grade}"
if new_key in prev_materials_dict:
# 기존에 있던 자재 - 수량 변경 확인
prev_qty = prev_materials_dict[new_key]["quantity"]
new_qty = float(material_data.get("quantity", 0))
# 증가분만큼 자재 데이터 생성
for item in new_data["items"]:
if diff_quantity <= 0:
break
item_quantity = float(item.get("quantity", 0))
if item_quantity <= diff_quantity:
# 이 아이템 전체를 포함
materials_diff.append(item)
diff_quantity -= item_quantity
new_materials_count += 1
else:
# 이 아이템의 일부만 포함
item_copy = item.copy()
item_copy["quantity"] = diff_quantity
materials_diff.append(item_copy)
new_materials_count += 1
break
if abs(prev_qty - new_qty) > 0.001:
# 수량이 변경됨
changed_materials_keys.add(new_key)
print(f"🔄 변경 감지: {desc[:40]}... (수량: {prev_qty}{new_qty})")
else:
print(f"수량 감소/동일: {new_data['description'][:50]}... (기존:{existing_quantity} 새:{new_quantity})")
# 새로 추가된 자재
new_materials_keys.add(new_key)
print(f" 신규 감지: {desc[:40]}...")
# 차이분만 처리하도록 materials_to_classify 교체
materials_to_classify = materials_diff
print(f"차이분 자재 개수: {len(materials_to_classify)}")
print(f"🔄 리비전 업로드: 차이분 {len(materials_diff)}개 라인 아이템 분류 처리")
print(f"📊 차이분 요약: {len(new_materials_grouped)}개 자재 그룹 → {len(materials_diff)}개 라인 아이템")
print(f"📊 변경사항 요약: 변경 {len(changed_materials_keys)}개, 신규 {len(new_materials_keys)}")
else:
print(f"🆕 신규 업로드: 전체 {len(materials_to_classify)}개 분류 처리")
@@ -728,6 +723,8 @@ async def upload_file(
print(f" size_spec: '{material_data['size_spec']}'")
print(f" original_description: {material_data['original_description']}")
print(f" category: {classification_result.get('category', 'UNKNOWN')}")
print(f" drawing_name: {material_data.get('dwg_name')}")
print(f" line_no: {material_data.get('line_num')}")
material_result = db.execute(material_insert_query, {
"file_id": file_id,
@@ -752,7 +749,37 @@ async def upload_file(
material_id = material_result.fetchone()[0]
materials_inserted += 1
# 리비전 업로드인 경우 신규 자재 카운트는 이미 위에서 처리됨
# 리비전 업로드인 경우 변경/신규 자재 표시 및 구매신청 정보 상속
if parent_file_id is not None:
# 현재 자재의 키 생성
dwg_name = material_data.get("dwg_name")
line_num = material_data.get("line_num")
if dwg_name:
current_key = f"{dwg_name}|{description}|{size_spec}|{material_data.get('material_grade', '')}"
elif line_num:
current_key = f"{line_num}|{description}|{size_spec}|{material_data.get('material_grade', '')}"
else:
current_key = f"{description}|{size_spec}|{material_data.get('material_grade', '')}"
# 변경 또는 신규 자재에 상태 표시
if current_key in changed_materials_keys:
# 변경된 자재
update_status_query = text("""
UPDATE materials SET revision_status = 'changed'
WHERE id = :material_id
""")
db.execute(update_status_query, {"material_id": material_id})
elif current_key in new_materials_keys:
# 신규 자재 (추가됨)
update_status_query = text("""
UPDATE materials SET revision_status = 'active'
WHERE id = :material_id
""")
db.execute(update_status_query, {"material_id": material_id})
# 구매신청 정보 상속은 전체 자재 저장 후 일괄 처리하도록 변경
# (개별 처리는 비효율적이고 수량 계산이 복잡함)
# PIPE 분류 결과인 경우 상세 정보 저장
if classification_result.get("category") == "PIPE":
@@ -1432,6 +1459,83 @@ async def upload_file(
db.commit()
print(f"자재 저장 완료: {materials_inserted}")
# 리비전 업로드인 경우 구매신청 정보 수량 기반 상속
if parent_file_id is not None:
try:
print(f"🔄 구매신청 정보 상속 처리 시작...")
# 1. 이전 리비전에서 그룹별 구매신청 수량 집계
prev_purchase_summary = text("""
SELECT
m.original_description,
m.size_spec,
m.material_grade,
m.drawing_name,
COUNT(DISTINCT pri.material_id) as purchased_count,
SUM(pri.quantity) as total_purchased_qty,
MIN(pri.request_id) as request_id
FROM materials m
JOIN purchase_request_items pri ON m.id = pri.material_id
WHERE m.file_id = :parent_file_id
GROUP BY m.original_description, m.size_spec, m.material_grade, m.drawing_name
""")
prev_purchases = db.execute(prev_purchase_summary, {"parent_file_id": parent_file_id}).fetchall()
# 2. 새 리비전에서 같은 그룹의 자재에 수량만큼 구매신청 상속
for prev_purchase in prev_purchases:
purchased_count = prev_purchase.purchased_count
# 새 리비전에서 같은 그룹의 자재 조회 (순서대로)
new_group_materials = text("""
SELECT id, quantity
FROM materials
WHERE file_id = :file_id
AND original_description = :description
AND COALESCE(size_spec, '') = :size_spec
AND COALESCE(material_grade, '') = :material_grade
AND COALESCE(drawing_name, '') = :drawing_name
ORDER BY id
LIMIT :limit
""")
new_materials = db.execute(new_group_materials, {
"file_id": file_id,
"description": prev_purchase.original_description,
"size_spec": prev_purchase.size_spec or '',
"material_grade": prev_purchase.material_grade or '',
"drawing_name": prev_purchase.drawing_name or '',
"limit": purchased_count
}).fetchall()
# 구매신청 수량만큼만 상속
for new_mat in new_materials:
inherit_query = text("""
INSERT INTO purchase_request_items (
request_id, material_id, quantity, unit, user_requirement
) VALUES (
:request_id, :material_id, :quantity, 'EA', ''
)
ON CONFLICT DO NOTHING
""")
db.execute(inherit_query, {
"request_id": prev_purchase.request_id,
"material_id": new_mat.id,
"quantity": new_mat.quantity
})
inherited_count = len(new_materials)
if inherited_count > 0:
print(f"{prev_purchase.original_description[:30]}... (도면: {prev_purchase.drawing_name or 'N/A'}) → {inherited_count}/{purchased_count}개 상속")
db.commit()
print(f"✅ 구매신청 정보 상속 완료")
except Exception as e:
print(f"❌ 구매신청 정보 상속 실패: {str(e)}")
db.rollback()
# 상속 실패는 업로드 성공에 영향 없음
# 활동 로그 기록
try:
activity_logger = ActivityLogger(db)
@@ -1451,25 +1555,136 @@ async def upload_file(
print(f"활동 로그 기록 실패: {str(e)}")
# 로그 실패는 업로드 성공에 영향을 주지 않음
# 리비전 업로드인 경우 누락된 도면 감지 및 구매신청 여부 확인
missing_drawings_info = None
has_previous_purchase = False
if parent_file_id is not None:
try:
# 이전 리비전의 구매신청 여부 확인
purchase_check_query = text("""
SELECT COUNT(*) as purchase_count
FROM purchase_request_items pri
JOIN materials m ON pri.material_id = m.id
WHERE m.file_id = :parent_file_id
""")
purchase_result = db.execute(purchase_check_query, {"parent_file_id": parent_file_id}).fetchone()
has_previous_purchase = purchase_result.purchase_count > 0
print(f"📦 이전 리비전 구매신청 여부: {has_previous_purchase} ({purchase_result.purchase_count}개 자재)")
print(f"📂 parent_file_id: {parent_file_id}, new file_id: {file_id}")
# 이전 리비전의 도면 목록 조회
prev_drawings_query = text("""
SELECT DISTINCT drawing_name, line_no, COUNT(*) as material_count
FROM materials
WHERE file_id = :parent_file_id
AND (drawing_name IS NOT NULL OR line_no IS NOT NULL)
GROUP BY drawing_name, line_no
""")
prev_drawings_result = db.execute(prev_drawings_query, {"parent_file_id": parent_file_id}).fetchall()
print(f"📋 이전 리비전 도면 수: {len(prev_drawings_result)}")
# 새 리비전의 도면 목록 조회
new_drawings_query = text("""
SELECT DISTINCT drawing_name, line_no
FROM materials
WHERE file_id = :file_id
AND (drawing_name IS NOT NULL OR line_no IS NOT NULL)
""")
new_drawings_result = db.execute(new_drawings_query, {"file_id": file_id}).fetchall()
print(f"📋 새 리비전 도면 수: {len(new_drawings_result)}")
prev_drawings = set()
for row in prev_drawings_result:
if row.drawing_name:
prev_drawings.add(row.drawing_name)
elif row.line_no:
prev_drawings.add(row.line_no)
new_drawings = set()
for row in new_drawings_result:
if row.drawing_name:
new_drawings.add(row.drawing_name)
elif row.line_no:
new_drawings.add(row.line_no)
missing_drawings = prev_drawings - new_drawings
print(f"📊 이전 도면: {list(prev_drawings)[:5]}")
print(f"📊 새 도면: {list(new_drawings)[:5]}")
print(f"❌ 누락 도면: {len(missing_drawings)}")
if missing_drawings:
# 누락된 도면의 자재 상세 정보
missing_materials = []
for row in prev_drawings_result:
drawing = row.drawing_name or row.line_no
if drawing in missing_drawings:
missing_materials.append({
"drawing_name": drawing,
"material_count": row.material_count
})
missing_drawings_info = {
"drawings": list(missing_drawings),
"materials": missing_materials,
"count": len(missing_drawings),
"requires_confirmation": True,
"has_previous_purchase": has_previous_purchase,
"action": "mark_as_inventory" if has_previous_purchase else "hide_materials"
}
# 누락된 도면의 자재 상태 업데이트
if missing_drawings:
for drawing in missing_drawings:
status_to_set = 'inventory' if has_previous_purchase else 'deleted_not_purchased'
update_status_query = text("""
UPDATE materials
SET revision_status = :status
WHERE file_id = :parent_file_id
AND (drawing_name = :drawing OR line_no = :drawing)
""")
db.execute(update_status_query, {
"status": status_to_set,
"parent_file_id": parent_file_id,
"drawing": drawing
})
db.commit()
print(f"✅ 누락 도면 자재 상태 업데이트: {len(missing_drawings)}개 도면 → {status_to_set}")
except Exception as e:
print(f"누락 도면 감지 실패: {str(e)}")
db.rollback()
# 감지 실패는 업로드 성공에 영향 없음
# 리비전 업로드인 경우 메시지 다르게 표시
if parent_file_id is not None:
message = f"리비전 업로드 성공! {new_materials_count}개의 신규 자재가 추가되었습니다."
else:
message = f"업로드 성공! {materials_inserted}개 자재가 분류되었습니다."
return {
response_data = {
"success": True,
"message": message,
"original_filename": file.filename,
"file_id": file_id,
"materials_count": materials_inserted,
"saved_materials_count": materials_inserted,
"new_materials_count": new_materials_count if parent_file_id is not None else None, # 신규 자재 수
"revision": revision, # 생성된 리비전 정보 추가
"uploaded_by": username, # 업로드한 사용자 정보 추가
"new_materials_count": new_materials_count if parent_file_id is not None else None,
"revision": revision,
"uploaded_by": username,
"parsed_count": parsed_count
}
# 누락된 도면 정보 추가
if missing_drawings_info:
response_data["missing_drawings"] = missing_drawings_info
return response_data
except Exception as e:
db.rollback()
if os.path.exists(file_path):
@@ -1612,7 +1827,7 @@ async def get_materials(
query = """
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
m.size_spec, m.main_nom, m.red_nom, m.material_grade, m.full_material_grade, m.line_number, m.row_number,
m.drawing_name, m.line_no,
m.drawing_name, m.line_no, m.revision_status,
m.created_at, m.classified_category, m.classification_confidence,
m.classification_details,
m.is_verified, m.verified_by, m.verified_at,
@@ -1821,6 +2036,7 @@ async def get_materials(
"full_material_grade": m.full_material_grade,
"drawing_name": m.drawing_name,
"line_no": m.line_no,
"revision_status": m.revision_status or 'active',
"line_number": m.line_number,
"row_number": m.row_number,
# 구매수량 계산에서 분류된 정보를 우선 사용
@@ -2358,7 +2574,8 @@ async def compare_revisions(
# 기존 리비전 자재 조회
old_materials_query = text("""
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
m.material_grade, m.classified_category, m.classification_confidence
m.material_grade, m.classified_category, m.classification_confidence,
m.main_nom, m.red_nom, m.drawing_name, m.line_no
FROM materials m
JOIN files f ON m.file_id = f.id
WHERE f.job_no = :job_no
@@ -2376,7 +2593,8 @@ async def compare_revisions(
# 새 리비전 자재 조회
new_materials_query = text("""
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
m.material_grade, m.classified_category, m.classification_confidence
m.material_grade, m.classified_category, m.classification_confidence,
m.main_nom, m.red_nom, m.drawing_name, m.line_no
FROM materials m
JOIN files f ON m.file_id = f.id
WHERE f.job_no = :job_no
@@ -2391,9 +2609,17 @@ async def compare_revisions(
})
new_materials = new_result.fetchall()
# 자재 키 생성 함수 (전체 수량 기준)
# 자재 키 생성 함수 (도면번호 + 자재 정보)
def create_material_key(material):
return f"{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
# 도면번호가 있으면 도면번호 + 자재 설명으로 고유 키 생성
# (같은 도면에 여러 자재가 있을 수 있으므로)
if hasattr(material, 'drawing_name') and material.drawing_name:
return f"{material.drawing_name}|{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
elif hasattr(material, 'line_no') and material.line_no:
return f"{material.line_no}|{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
else:
# 도면번호 없으면 기존 방식 (설명 + 크기 + 재질)
return f"{material.original_description}|{material.size_spec or ''}|{material.material_grade or ''}"
# 기존 자재를 딕셔너리로 변환 (수량 합산)
old_materials_dict = {}
@@ -2410,7 +2636,11 @@ async def compare_revisions(
"size_spec": material.size_spec,
"material_grade": material.material_grade,
"classified_category": material.classified_category,
"classification_confidence": material.classification_confidence
"classification_confidence": material.classification_confidence,
"main_nom": material.main_nom,
"red_nom": material.red_nom,
"drawing_name": material.drawing_name,
"line_no": material.line_no
}
# 새 자재를 딕셔너리로 변환 (수량 합산)
@@ -2428,7 +2658,11 @@ async def compare_revisions(
"size_spec": material.size_spec,
"material_grade": material.material_grade,
"classified_category": material.classified_category,
"classification_confidence": material.classification_confidence
"classification_confidence": material.classification_confidence,
"main_nom": material.main_nom,
"red_nom": material.red_nom,
"drawing_name": material.drawing_name,
"line_no": material.line_no
}
# 변경 사항 분석
@@ -2457,22 +2691,68 @@ async def compare_revisions(
"change_type": "added"
})
elif old_item and new_item:
# 수량 변경 확인 (전체 수량 기준)
# 변경 사항 감지: 수량, 재질, 크기, 카테고리 등
changes_detected = []
old_qty = old_item["quantity"]
new_qty = new_item["quantity"]
qty_diff = new_qty - old_qty
# 수량 차이가 있으면 변경된 것으로 간주 (소수점 오차 고려)
# 1. 수량 변경 확인
if abs(qty_diff) > 0.001:
change_type = "quantity_increased" if qty_diff > 0 else "quantity_decreased"
changes_detected.append({
"type": "quantity",
"old_value": old_qty,
"new_value": new_qty,
"diff": qty_diff
})
# 2. 재질 변경 확인
if old_item.get("material_grade") != new_item.get("material_grade"):
changes_detected.append({
"type": "material",
"old_value": old_item.get("material_grade", "-"),
"new_value": new_item.get("material_grade", "-")
})
# 3. 크기 변경 확인
if old_item.get("main_nom") != new_item.get("main_nom") or old_item.get("size_spec") != new_item.get("size_spec"):
changes_detected.append({
"type": "size",
"old_value": old_item.get("main_nom") or old_item.get("size_spec", "-"),
"new_value": new_item.get("main_nom") or new_item.get("size_spec", "-")
})
# 4. 카테고리 변경 확인 (자재 종류가 바뀜)
if old_item.get("classified_category") != new_item.get("classified_category"):
changes_detected.append({
"type": "category",
"old_value": old_item.get("classified_category", "-"),
"new_value": new_item.get("classified_category", "-")
})
# 변경사항이 있으면 changed_items에 추가
if changes_detected:
# 변경 유형 결정
has_qty_change = any(c["type"] == "quantity" for c in changes_detected)
has_spec_change = any(c["type"] in ["material", "size", "category"] for c in changes_detected)
if has_spec_change:
change_type = "specification_changed" # 자재 사양 변경
elif has_qty_change:
change_type = "quantity_changed" # 수량만 변경
else:
change_type = "modified"
changed_items.append({
"key": key,
"old_item": old_item,
"new_item": new_item,
"quantity_change": qty_diff,
"quantity_change_abs": abs(qty_diff),
"changes": changes_detected,
"change_type": change_type,
"change_percentage": (qty_diff / old_qty * 100) if old_qty > 0 else 0
"quantity_change": qty_diff if has_qty_change else 0,
"drawing_name": new_item.get("drawing_name") or old_item.get("drawing_name"),
"line_no": new_item.get("line_no") or old_item.get("line_no")
})
# 분류별 통계
@@ -2490,6 +2770,40 @@ async def compare_revisions(
removed_stats = calculate_category_stats(removed_items)
changed_stats = calculate_category_stats(changed_items)
# 누락된 도면 감지
old_drawings = set()
new_drawings = set()
for material in old_materials:
if hasattr(material, 'drawing_name') and material.drawing_name:
old_drawings.add(material.drawing_name)
elif hasattr(material, 'line_no') and material.line_no:
old_drawings.add(material.line_no)
for material in new_materials:
if hasattr(material, 'drawing_name') and material.drawing_name:
new_drawings.add(material.drawing_name)
elif hasattr(material, 'line_no') and material.line_no:
new_drawings.add(material.line_no)
missing_drawings = old_drawings - new_drawings # 이전에는 있었는데 새 파일에 없는 도면
new_only_drawings = new_drawings - old_drawings # 새로 추가된 도면
# 누락된 도면의 자재 목록
missing_drawing_materials = []
if missing_drawings:
for material in old_materials:
drawing = getattr(material, 'drawing_name', None) or getattr(material, 'line_no', None)
if drawing in missing_drawings:
missing_drawing_materials.append({
"drawing_name": drawing,
"description": material.original_description,
"quantity": float(material.quantity) if material.quantity else 0,
"category": material.classified_category,
"size": material.main_nom or material.size_spec,
"material_grade": material.material_grade
})
return {
"success": True,
"comparison": {
@@ -2501,7 +2815,15 @@ async def compare_revisions(
"added_count": len(added_items),
"removed_count": len(removed_items),
"changed_count": len(changed_items),
"total_changes": len(added_items) + len(removed_items) + len(changed_items)
"total_changes": len(added_items) + len(removed_items) + len(changed_items),
"missing_drawings_count": len(missing_drawings),
"new_drawings_count": len(new_only_drawings)
},
"missing_drawings": {
"drawings": list(missing_drawings),
"materials": missing_drawing_materials,
"count": len(missing_drawings),
"requires_confirmation": len(missing_drawings) > 0
},
"changes": {
"added": added_items,
@@ -3291,4 +3613,91 @@ async def log_materials_view(
except Exception as e:
logger.error(f"조회 로그 기록 실패: {str(e)}")
return {"message": "조회 로그 기록 실패"}
return {"message": "조회 로그 기록 실패"}
@router.post("/{file_id}/process-missing-drawings")
async def process_missing_drawings(
file_id: int,
action: str = "delete",
drawings: List[str] = [],
db: Session = Depends(get_db)
):
"""
누락된 도면 처리
- action='delete': 누락된 도면의 이전 리비전 자재 삭제 처리
- action='keep': 누락된 도면의 이전 리비전 자재 유지
"""
try:
# 현재 파일 정보 확인
from pydantic import BaseModel
class MissingDrawingsRequest(BaseModel):
action: str
drawings: List[str]
# parent_file_id 조회는 files 테이블에 없을 수 있으므로 revision으로 판단
file_query = text("""
SELECT f.id, f.job_no, f.revision, f.original_filename
FROM files f
WHERE f.id = :file_id
""")
file_result = db.execute(file_query, {"file_id": file_id}).fetchone()
if not file_result:
raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다")
# 이전 리비전 파일 찾기
prev_revision_query = text("""
SELECT id FROM files
WHERE job_no = :job_no
AND original_filename = :filename
AND revision < :current_revision
ORDER BY revision DESC
LIMIT 1
""")
prev_result = db.execute(prev_revision_query, {
"job_no": file_result.job_no,
"filename": file_result.original_filename,
"current_revision": file_result.revision
}).fetchone()
if not prev_result:
raise HTTPException(status_code=400, detail="이전 리비전을 찾을 수 없습니다")
parent_file_id = prev_result.id
if action == "delete":
# 누락된 도면의 자재를 deleted_not_purchased 상태로 변경
for drawing in drawings:
update_query = text("""
UPDATE materials
SET revision_status = 'deleted_not_purchased'
WHERE file_id = :parent_file_id
AND (drawing_name = :drawing OR line_no = :drawing)
""")
db.execute(update_query, {
"parent_file_id": parent_file_id,
"drawing": drawing
})
db.commit()
return {
"success": True,
"message": f"{len(drawings)}개 도면의 자재가 삭제 처리되었습니다",
"action": "deleted",
"drawings_count": len(drawings)
}
else:
# keep - 이미 처리됨 (inventory 또는 active 상태 유지)
return {
"success": True,
"message": "누락된 도면의 자재가 유지됩니다",
"action": "kept",
"drawings_count": len(drawings)
}
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"도면 처리 실패: {str(e)}")

View File

@@ -290,6 +290,13 @@ async def get_request_materials(
materials = []
for row in results:
# quantity를 정수로 변환 (소수점 제거)
qty = row.requested_quantity or row.original_quantity
try:
qty_int = int(float(qty)) if qty else 0
except (ValueError, TypeError):
qty_int = 0
materials.append({
"item_id": row.item_id,
"material_id": row.material_id,
@@ -298,7 +305,7 @@ async def get_request_materials(
"size": row.size_spec,
"schedule": row.schedule,
"material_grade": row.material_grade,
"quantity": row.requested_quantity or row.original_quantity,
"quantity": qty_int, # 정수로 변환
"unit": row.requested_unit or row.original_unit,
"user_requirement": row.user_requirement,
"is_ordered": row.is_ordered,