feat: BOM 관리 시스템 대폭 개선 및 Docker 배포 가이드 추가
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled

- 🎨 UI/UX 개선: 데본씽크 스타일 모던 디자인 적용
- 📁 컴포넌트 구조 개선: 폴더별 체계적 관리 (common/, bom/, materials/)
- 🔧 BOM 관리 페이지 리팩토링: NewMaterialsPage → BOMManagementPage + 카테고리별 컴포넌트 분리
- 💾 구매신청 기능 개선: 선택된 자재 비활성화, 제목 편집, 엑셀 다운로드
- 📊 자재 표시 개선: 타입/서브타입 컬럼 정리, 상세 정보 복원
- 🐛 CSS 빌드 오류 수정: NewMaterialsPage.css 문법 오류 해결
- 📚 문서화: PAGES_GUIDE.md 추가, README에 Docker 캐시 문제 해결 가이드 추가
- 🔄 API 개선: 구매신청 자재 조회, 제목 수정 엔드포인트 추가
This commit is contained in:
hyungi
2025-10-16 12:45:23 +09:00
parent 5aef867110
commit 64fd9ad3d2
31 changed files with 7450 additions and 1604 deletions

View File

@@ -176,6 +176,39 @@ async def get_signup_requests(
)
@router.get("/pending-signups/count")
async def get_pending_signups_count(
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
승인 대기 중인 회원가입 수 조회 (관리자 전용)
Returns:
dict: 승인 대기 중인 사용자 수
"""
try:
# 관리자 권한 확인
if current_user.get('role') not in ['admin', 'system']:
return {"count": 0} # 관리자가 아니면 0 반환
# 승인 대기 중인 사용자 수 조회
query = text("""
SELECT COUNT(*) as count
FROM users
WHERE status = 'pending'
""")
result = db.execute(query).fetchone()
count = result.count if result else 0
return {"count": count}
except Exception as e:
logger.error(f"승인 대기 회원가입 수 조회 실패: {str(e)}")
return {"count": 0} # 오류 시 0 반환
@router.post("/approve-signup/{user_id}")
async def approve_signup(
user_id: int,

View File

@@ -32,6 +32,13 @@ from app.services.revision_comparator import get_revision_comparison
router = APIRouter()
class ExcelSaveRequest(BaseModel):
file_id: int
category: str
materials: List[Dict]
filename: str
user_id: Optional[int] = None
def extract_enhanced_material_grade(description: str, original_grade: str, category: str) -> str:
"""
원본 설명에서 개선된 재질 정보를 추출
@@ -3039,9 +3046,9 @@ async def get_valve_details(
except Exception as e:
raise HTTPException(status_code=500, detail=f"VALVE 상세 정보 조회 실패: {str(e)}")
@router.get("/user-requirements")
@router.get("/{file_id}/user-requirements")
async def get_user_requirements(
file_id: Optional[int] = None,
file_id: int,
job_no: Optional[str] = None,
status: Optional[str] = None,
db: Session = Depends(get_db)
@@ -3729,4 +3736,174 @@ async def process_missing_drawings(
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"도면 처리 실패: {str(e)}")
raise HTTPException(status_code=500, detail=f"도면 처리 실패: {str(e)}")
@router.post("/save-excel")
async def save_excel_file(
request: ExcelSaveRequest,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
엑셀 파일을 서버에 저장하고 메타데이터를 기록
"""
try:
# 엑셀 저장 디렉토리 생성
excel_dir = Path("uploads/excel_exports")
excel_dir.mkdir(parents=True, exist_ok=True)
# 파일 경로 생성
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = f"{request.category}_{timestamp}_{request.filename}"
file_path = excel_dir / safe_filename
# 엑셀 파일 생성 (openpyxl 사용)
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
wb = openpyxl.Workbook()
ws = wb.active
ws.title = request.category
# 헤더 설정
headers = ['TAGNO', '품목명', '수량', '통화구분', '단가', '크기', '압력등급', '스케줄',
'재질', '상세내역', '사용자요구', '관리항목1', '관리항목2', '관리항목3',
'관리항목4', '납기일(YYYY-MM-DD)']
# 헤더 스타일
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
header_alignment = Alignment(horizontal="center", vertical="center")
# 헤더 작성
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
# 데이터 작성
for row_idx, material in enumerate(request.materials, 2):
# 기본 데이터
data = [
'', # TAGNO
request.category, # 품목명
material.get('quantity', 0), # 수량
'KRW', # 통화구분
1, # 단가
material.get('size_spec', '-'), # 크기
'-', # 압력등급
material.get('schedule', '-'), # 스케줄
material.get('full_material_grade', material.get('material_grade', '-')), # 재질
'-', # 상세내역
material.get('user_requirement', ''), # 사용자요구
'', '', '', '', # 관리항목들
datetime.now().strftime('%Y-%m-%d') # 납기일
]
# 데이터 입력
for col, value in enumerate(data, 1):
ws.cell(row=row_idx, column=col, value=value)
# 엑셀 파일 저장
wb.save(file_path)
# 데이터베이스에 메타데이터 저장 (테이블이 없으면 무시)
try:
save_query = text("""
INSERT INTO excel_exports (
file_id, category, filename, file_path,
material_count, created_by, created_at
) VALUES (
:file_id, :category, :filename, :file_path,
:material_count, :user_id, :created_at
)
""")
db.execute(save_query, {
"file_id": request.file_id,
"category": request.category,
"filename": safe_filename,
"file_path": str(file_path),
"material_count": len(request.materials),
"user_id": current_user.get('id'),
"created_at": datetime.now()
})
db.commit()
except Exception as db_error:
logger.warning(f"엑셀 메타데이터 저장 실패 (파일은 저장됨): {str(db_error)}")
# 메타데이터 저장 실패해도 파일은 저장되었으므로 계속 진행
logger.info(f"엑셀 파일 저장 완료: {safe_filename}")
return {
"success": True,
"message": "엑셀 파일이 성공적으로 저장되었습니다.",
"filename": safe_filename,
"file_path": str(file_path),
"material_count": len(request.materials)
}
except Exception as e:
logger.error(f"엑셀 파일 저장 실패: {str(e)}")
raise HTTPException(status_code=500, detail=f"엑셀 파일 저장 실패: {str(e)}")
@router.get("/excel-exports")
async def get_excel_exports(
file_id: Optional[int] = None,
category: Optional[str] = None,
current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
저장된 엑셀 파일 목록 조회
"""
try:
query = text("""
SELECT
id, file_id, category, filename, file_path,
material_count, created_by, created_at
FROM excel_exports
WHERE 1=1
""")
params = {}
if file_id:
query = text(str(query) + " AND file_id = :file_id")
params["file_id"] = file_id
if category:
query = text(str(query) + " AND category = :category")
params["category"] = category
query = text(str(query) + " ORDER BY created_at DESC")
result = db.execute(query, params).fetchall()
exports = []
for row in result:
exports.append({
"id": row.id,
"file_id": row.file_id,
"category": row.category,
"filename": row.filename,
"file_path": row.file_path,
"material_count": row.material_count,
"created_by": row.created_by,
"created_at": row.created_at.isoformat() if row.created_at else None
})
return {
"success": True,
"exports": exports
}
except Exception as e:
logger.error(f"엑셀 내보내기 목록 조회 실패: {str(e)}")
return {
"success": False,
"exports": [],
"message": "엑셀 내보내기 목록을 조회할 수 없습니다."
}

View File

@@ -1,7 +1,7 @@
"""
구매신청 관리 API
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Body
from fastapi.responses import FileResponse
from pydantic import BaseModel
from sqlalchemy import text
@@ -439,6 +439,101 @@ async def get_request_materials(
)
@router.get("/requested-materials")
async def get_requested_material_ids(
file_id: Optional[int] = None,
job_no: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
구매신청된 자재 ID 목록 조회 (BOM 페이지에서 비활성화용)
"""
try:
query = text("""
SELECT DISTINCT pri.material_id
FROM purchase_request_items pri
JOIN purchase_requests pr ON pri.request_id = pr.request_id
WHERE 1=1
AND (:file_id IS NULL OR pr.file_id = :file_id)
AND (:job_no IS NULL OR pr.job_no = :job_no)
""")
results = db.execute(query, {
"file_id": file_id,
"job_no": job_no
}).fetchall()
material_ids = [row.material_id for row in results]
return {
"success": True,
"requested_material_ids": material_ids,
"count": len(material_ids)
}
except Exception as e:
logger.error(f"Failed to get requested material IDs: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"구매신청 자재 ID 조회 실패: {str(e)}"
)
@router.patch("/{request_id}/title")
async def update_request_title(
request_id: int,
title: str = Body(..., embed=True),
# current_user: dict = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
구매신청 제목(request_no) 업데이트
"""
try:
# 구매신청 존재 확인
check_query = text("""
SELECT request_no FROM purchase_requests
WHERE request_id = :request_id
""")
existing = db.execute(check_query, {"request_id": request_id}).fetchone()
if not existing:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="구매신청을 찾을 수 없습니다"
)
# 제목 업데이트
update_query = text("""
UPDATE purchase_requests
SET request_no = :title
WHERE request_id = :request_id
""")
db.execute(update_query, {
"request_id": request_id,
"title": title
})
db.commit()
return {
"success": True,
"message": "구매신청 제목이 업데이트되었습니다",
"old_title": existing.request_no,
"new_title": title
}
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Failed to update request title: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"구매신청 제목 업데이트 실패: {str(e)}"
)
@router.get("/{request_id}/download-excel")
async def download_request_excel(
request_id: int,