diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index cbf6cf5..9d16cfc 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -380,10 +380,12 @@ async def upload_file( # 리비전 업로드인 경우 차이분 계산 materials_diff = [] + original_materials_to_classify = materials_to_classify.copy() # 원본 보존 + if parent_file_id is not None: # 새 파일의 자재들을 수량별로 그룹화 new_materials_grouped = {} - for material_data in materials_to_classify: + 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)) @@ -434,6 +436,9 @@ async def upload_file( # 차이분만 처리하도록 materials_to_classify 교체 materials_to_classify = materials_diff print(f"차이분 자재 개수: {len(materials_to_classify)}") + print(f"🔄 리비전 업로드: 차이분 {len(materials_diff)}개만 분류 처리") + else: + print(f"🆕 신규 업로드: 전체 {len(materials_to_classify)}개 분류 처리") # 분류가 필요한 자재 처리 print(f"분류할 자재 총 개수: {len(materials_to_classify)}") @@ -2701,4 +2706,205 @@ async def confirm_material_purchase_api( except Exception as e: db.rollback() print(f"구매수량 확정 실패: {str(e)}") - raise HTTPException(status_code=500, detail=f"구매수량 확정 실패: {str(e)}") \ No newline at end of file + raise HTTPException(status_code=500, detail=f"구매수량 확정 실패: {str(e)}") + + +@router.put("/{file_id}") +async def update_file_info( + file_id: int, + bom_name: Optional[str] = Body(None), + description: Optional[str] = Body(None), + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """파일 정보 수정""" + try: + # 파일 존재 확인 및 관련 정보 조회 + file_query = text(""" + SELECT id, bom_name, description, job_no, original_filename + FROM files + WHERE id = :file_id + """) + file_result = db.execute(file_query, {"file_id": file_id}).fetchone() + + if not file_result: + raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다") + + # 업데이트할 필드 준비 + update_fields = [] + params = {} + + if bom_name is not None: + update_fields.append("bom_name = :bom_name") + params["bom_name"] = bom_name + + if description is not None: + update_fields.append("description = :description") + params["description"] = description + + if not update_fields: + raise HTTPException(status_code=400, detail="수정할 정보가 없습니다") + + # BOM 이름 수정인 경우, 같은 job_no의 모든 리비전을 함께 업데이트 + if bom_name is not None and file_result.job_no: + # 같은 job_no의 모든 파일 업데이트 + update_query = text(f""" + UPDATE files + SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP + WHERE job_no = :job_no + """) + params["job_no"] = file_result.job_no + + else: + # 단일 파일만 업데이트 + update_query = text(f""" + UPDATE files + SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP + WHERE id = :file_id + """) + params["file_id"] = file_id + + db.execute(update_query, params) + db.commit() + + # 활동 로그 기록 - 간단하게 처리 + logger.info(f"파일 정보 수정 완료: 사용자={current_user['username']}, 파일ID={file_id}, BOM명={bom_name or 'N/A'}") + + return {"message": "파일 정보가 성공적으로 수정되었습니다"} + + except Exception as e: + logger.error(f"파일 정보 수정 실패: {str(e)}") + db.rollback() + raise HTTPException(status_code=500, detail=f"파일 정보 수정 실패: {str(e)}") + + +@router.get("/{file_id}/export-excel") +async def export_materials_to_excel( + file_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """자재 목록을 엑셀로 내보내기""" + try: + # 파일 정보 조회 + file_query = text(""" + SELECT f.id, f.original_filename, f.bom_name, f.job_no, f.revision, + p.project_name, p.official_project_code + FROM files f + LEFT JOIN projects p ON f.project_id = p.id + 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="파일을 찾을 수 없습니다") + + # 자재 목록 조회 + materials_query = text(""" + SELECT + m.line_number, + m.original_description, + m.quantity, + m.unit, + m.size_spec, + m.main_nom, + m.red_nom, + m.material_grade, + m.classified_category, + m.classification_confidence, + m.is_verified, + m.verified_by, + m.created_at + FROM materials m + WHERE m.file_id = :file_id + ORDER BY m.line_number ASC + """) + + materials_result = db.execute(materials_query, {"file_id": file_id}).fetchall() + + # 엑셀 데이터 준비 + excel_data = [] + for material in materials_result: + excel_data.append({ + "라인번호": material.line_number, + "품명": material.original_description, + "수량": material.quantity, + "단위": material.unit, + "사이즈": material.size_spec, + "주요NOM": material.main_nom, + "축소NOM": material.red_nom, + "재질등급": material.material_grade, + "분류": material.classified_category, + "신뢰도": material.classification_confidence, + "검증여부": "검증완료" if material.is_verified else "미검증", + "검증자": material.verified_by or "", + "등록일": material.created_at.strftime("%Y-%m-%d %H:%M:%S") if material.created_at else "" + }) + + # 활동 로그 기록 + activity_logger = ActivityLogger(db) + activity_logger.log_activity( + username=current_user["username"], + activity_type="엑셀 내보내기", + activity_description=f"자재 목록 엑셀 내보내기: {file_result.original_filename}", + target_type="file", + target_id=file_id, + metadata={ + "file_name": file_result.original_filename, + "bom_name": file_result.bom_name, + "job_no": file_result.job_no, + "revision": file_result.revision, + "materials_count": len(excel_data) + } + ) + + return { + "message": "엑셀 내보내기 준비 완료", + "file_info": { + "filename": file_result.original_filename, + "bom_name": file_result.bom_name, + "job_no": file_result.job_no, + "revision": file_result.revision, + "project_name": file_result.project_name + }, + "materials": excel_data, + "total_count": len(excel_data) + } + + except Exception as e: + logger.error(f"엑셀 내보내기 실패: {str(e)}") + raise HTTPException(status_code=500, detail=f"엑셀 내보내기 실패: {str(e)}") + + +@router.get("/{file_id}/materials/view-log") +async def log_materials_view( + file_id: int, + db: Session = Depends(get_db), + current_user: dict = Depends(get_current_user) +): + """자재 목록 조회 로그 기록""" + try: + # 파일 정보 조회 + file_query = text("SELECT original_filename, bom_name FROM files WHERE id = :file_id") + file_result = db.execute(file_query, {"file_id": file_id}).fetchone() + + if file_result: + # 활동 로그 기록 + activity_logger = ActivityLogger(db) + activity_logger.log_activity( + username=current_user["username"], + activity_type="자재 목록 조회", + activity_description=f"자재 목록 조회: {file_result.original_filename}", + target_type="file", + target_id=file_id, + metadata={ + "file_name": file_result.original_filename, + "bom_name": file_result.bom_name + } + ) + + return {"message": "조회 로그 기록 완료"} + + except Exception as e: + logger.error(f"조회 로그 기록 실패: {str(e)}") + return {"message": "조회 로그 기록 실패"} \ No newline at end of file diff --git a/backend/scripts/create_system_admin.py b/backend/scripts/create_system_admin.py index e3feb54..c495348 100755 --- a/backend/scripts/create_system_admin.py +++ b/backend/scripts/create_system_admin.py @@ -30,7 +30,7 @@ def create_system_admin(): # 데이터베이스 연결 try: - engine = create_engine(settings.database_url) + engine = create_engine(settings.get_database_url()) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) db = SessionLocal() diff --git a/docker-compose.proxy.yml b/docker-compose.proxy.yml new file mode 100644 index 0000000..b46bbf5 --- /dev/null +++ b/docker-compose.proxy.yml @@ -0,0 +1,15 @@ +services: + nginx-proxy: + image: nginx:alpine + container_name: tk-mp-nginx-proxy + restart: unless-stopped + ports: + - "80:80" + volumes: + - ./nginx-proxy.conf:/etc/nginx/conf.d/default.conf + networks: + - tk-mp-project_tk-mp-network + +networks: + tk-mp-project_tk-mp-network: + external: true \ No newline at end of file diff --git a/frontend/src/pages/LogMonitoringPage.jsx b/frontend/src/pages/LogMonitoringPage.jsx index 367bfaf..191c2dd 100644 --- a/frontend/src/pages/LogMonitoringPage.jsx +++ b/frontend/src/pages/LogMonitoringPage.jsx @@ -3,6 +3,7 @@ import api from '../api'; import { reportError, logUserActionError } from '../utils/errorLogger'; const LogMonitoringPage = ({ onNavigate, user }) => { + const [activeTab, setActiveTab] = useState('login-logs'); // 'login-logs', 'activity-logs', 'system-logs' const [stats, setStats] = useState({ totalUsers: 0, activeUsers: 0, @@ -11,6 +12,7 @@ const LogMonitoringPage = ({ onNavigate, user }) => { recentErrors: 0 }); const [recentActivity, setRecentActivity] = useState([]); + const [activityLogs, setActivityLogs] = useState([]); const [frontendErrors, setFrontendErrors] = useState([]); const [isLoading, setIsLoading] = useState(true); const [message, setMessage] = useState({ type: '', text: '' }); @@ -22,10 +24,26 @@ const LogMonitoringPage = ({ onNavigate, user }) => { return () => clearInterval(interval); }, []); + const loadActivityLogs = async () => { + try { + const response = await api.get('/auth/logs/system?limit=50'); + if (response.data.success) { + setActivityLogs(response.data.logs); + } + } catch (error) { + console.error('활동 로그 로딩 실패:', error); + } + }; + const loadDashboardData = async () => { try { setIsLoading(true); + // 활동 로그도 함께 로드 + if (activeTab === 'activity-logs') { + await loadActivityLogs(); + } + // 병렬로 데이터 로드 const [usersResponse, loginLogsResponse] = await Promise.all([ api.get('/auth/users'), @@ -158,9 +176,60 @@ const LogMonitoringPage = ({ onNavigate, user }) => {
- 실시간 시스템 활동 및 오류 모니터링 -
+ + + + {/* 탭 네비게이션 */} +통합 프로젝트 관리 시스템
+BOM 분류 시스템 v1.0
- 사용자 계정 관리 및 시스템 설정 + 사용자 계정 관리 및 시스템 로그 모니터링
| + 사용자명 + | ++ IP 주소 + | ++ 상태 + | ++ 로그인 시간 + | +
|---|---|---|---|
| + {log.username} + | ++ {log.ip_address} + | ++ + {log.status === 'success' ? '성공' : '실패'} + + | ++ {new Date(log.login_time).toLocaleString('ko-KR')} + | +
| + 사용자명 + | ++ 활동 유형 + | ++ 상세 내용 + | ++ 시간 + | +
|---|---|---|---|
| + {log.username} + | ++ + {log.activity_type} + + | ++ {log.activity_description} + | ++ {new Date(log.created_at).toLocaleString('ko-KR')} + | +