feat: BOM 관리 시스템 대폭 개선 및 Docker 배포 가이드 추가
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
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:
@@ -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,
|
||||
|
||||
@@ -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": "엑셀 내보내기 목록을 조회할 수 없습니다."
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user