diff --git a/backend/app/api/files.py b/backend/app/api/files.py index 536c54a..14d345f 100644 --- a/backend/app/api/files.py +++ b/backend/app/api/files.py @@ -236,35 +236,86 @@ async def upload_file( async def get_materials( project_id: Optional[int] = None, file_id: Optional[int] = None, + job_no: Optional[str] = None, + filename: Optional[str] = None, + revision: Optional[str] = None, skip: int = 0, limit: int = 100, + search: Optional[str] = None, + item_type: Optional[str] = None, + material_grade: Optional[str] = None, + size_spec: Optional[str] = None, + file_filter: Optional[str] = None, + sort_by: Optional[str] = None, db: Session = Depends(get_db) ): - """저장된 자재 목록 조회""" + """ + 저장된 자재 목록 조회 (job_no, filename, revision 3가지로 필터링 가능) + """ try: query = """ SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit, m.size_spec, m.material_grade, m.line_number, m.row_number, m.created_at, - f.original_filename, f.project_id, + f.original_filename, f.project_id, f.job_no, f.revision, p.official_project_code, p.project_name FROM materials m LEFT JOIN files f ON m.file_id = f.id LEFT JOIN projects p ON f.project_id = p.id WHERE 1=1 """ - params = {} - if project_id: query += " AND f.project_id = :project_id" params["project_id"] = project_id - if file_id: query += " AND m.file_id = :file_id" params["file_id"] = file_id + if job_no: + query += " AND f.job_no = :job_no" + params["job_no"] = job_no + if filename: + query += " AND f.original_filename = :filename" + params["filename"] = filename + if revision: + query += " AND f.revision = :revision" + params["revision"] = revision + if search: + query += " AND (m.original_description ILIKE :search OR m.material_grade ILIKE :search)" + params["search"] = f"%{search}%" + if item_type: + query += " AND m.classified_category = :item_type" + params["item_type"] = item_type + if material_grade: + query += " AND m.material_grade ILIKE :material_grade" + params["material_grade"] = f"%{material_grade}%" + if size_spec: + query += " AND m.size_spec ILIKE :size_spec" + params["size_spec"] = f"%{size_spec}%" + if file_filter: + query += " AND f.original_filename ILIKE :file_filter" + params["file_filter"] = f"%{file_filter}%" + + # 정렬 처리 + if sort_by: + if sort_by == "quantity_desc": + query += " ORDER BY m.quantity DESC" + elif sort_by == "quantity_asc": + query += " ORDER BY m.quantity ASC" + elif sort_by == "name_asc": + query += " ORDER BY m.original_description ASC" + elif sort_by == "name_desc": + query += " ORDER BY m.original_description DESC" + elif sort_by == "created_desc": + query += " ORDER BY m.created_at DESC" + elif sort_by == "created_asc": + query += " ORDER BY m.created_at ASC" + else: + query += " ORDER BY m.line_number ASC" + else: + query += " ORDER BY m.line_number ASC" - query += " ORDER BY m.line_number ASC LIMIT :limit OFFSET :skip" + query += " LIMIT :limit OFFSET :skip" params["limit"] = limit params["skip"] = skip @@ -287,6 +338,26 @@ async def get_materials( if file_id: count_query += " AND m.file_id = :file_id" count_params["file_id"] = file_id + + if search: + count_query += " AND (m.original_description ILIKE :search OR m.material_grade ILIKE :search)" + count_params["search"] = f"%{search}%" + + if item_type: + count_query += " AND m.classified_category = :item_type" + count_params["item_type"] = item_type + + if material_grade: + count_query += " AND m.material_grade ILIKE :material_grade" + count_params["material_grade"] = f"%{material_grade}%" + + if size_spec: + count_query += " AND m.size_spec ILIKE :size_spec" + count_params["size_spec"] = f"%{size_spec}%" + + if file_filter: + count_query += " AND f.original_filename ILIKE :file_filter" + count_params["file_filter"] = f"%{file_filter}%" count_result = db.execute(text(count_query), count_params) total_count = count_result.fetchone()[0] diff --git a/backend/app/main.py b/backend/app/main.py index 5b961e7..aa3deef 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,12 @@ -from fastapi import FastAPI +from fastapi import FastAPI, UploadFile, File, Form from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text +from .database import get_db +from sqlalchemy.orm import Session +from fastapi import Depends +from typing import Optional, List, Dict +import os +import shutil # FastAPI 앱 생성 app = FastAPI( @@ -30,14 +37,514 @@ try: except ImportError: print("jobs 라우터를 찾을 수 없습니다") +# 파일 목록 조회 API +@app.get("/files") +async def get_files( + job_no: Optional[str] = None, # project_id 대신 job_no 사용 + show_history: bool = False, # 이력 표시 여부 + db: Session = Depends(get_db) +): + """파일 목록 조회 (BOM별 그룹화)""" + try: + if show_history: + # 전체 이력 표시 + query = "SELECT * FROM files" + params = {} + + if job_no: + query += " WHERE job_no = :job_no" + params["job_no"] = job_no + + query += " ORDER BY original_filename, revision DESC" + else: + # 최신 리비전만 표시 + if job_no: + query = """ + SELECT f1.* FROM files f1 + INNER JOIN ( + SELECT original_filename, MAX(revision) as max_revision + FROM files + WHERE job_no = :job_no + GROUP BY original_filename + ) f2 ON f1.original_filename = f2.original_filename + AND f1.revision = f2.max_revision + WHERE f1.job_no = :job_no + ORDER BY f1.upload_date DESC + """ + params = {"job_no": job_no} + else: + # job_no가 없으면 전체 파일 조회 + query = "SELECT * FROM files ORDER BY upload_date DESC" + params = {} + + result = db.execute(text(query), params) + files = result.fetchall() + + return [ + { + "id": f.id, + "filename": f.original_filename, + "original_filename": f.original_filename, + "name": f.original_filename, + "job_no": f.job_no, # job_no 사용 + "bom_name": f.original_filename, # 파일명을 BOM 이름으로 사용 + "bom_type": f.file_type or "unknown", # file_type을 BOM 종류로 사용 + "status": "active" if f.is_active else "inactive", # is_active 상태 + "file_size": f.file_size, + "created_at": f.upload_date, + "upload_date": f.upload_date, + "revision": f.revision or "Rev.0", # 실제 리비전 또는 기본값 + "description": f"파일: {f.original_filename}" + } + for f in files + ] + except Exception as e: + print(f"파일 목록 조회 에러: {str(e)}") + return {"error": f"파일 목록 조회 실패: {str(e)}"} + +# 파일 삭제 API +@app.delete("/files/{file_id}") +async def delete_file( + file_id: int, + db: Session = Depends(get_db) +): + """파일 삭제""" + try: + # 먼저 파일 정보 조회 + file_query = text("SELECT * FROM files WHERE id = :file_id") + file_result = db.execute(file_query, {"file_id": file_id}) + file = file_result.fetchone() + + if not file: + return {"error": "파일을 찾을 수 없습니다"} + + # 관련 자재 데이터 삭제 + materials_query = text("DELETE FROM materials WHERE file_id = :file_id") + db.execute(materials_query, {"file_id": file_id}) + + # 파일 삭제 + delete_query = text("DELETE FROM files WHERE id = :file_id") + db.execute(delete_query, {"file_id": file_id}) + + db.commit() + return {"success": True, "message": "파일과 관련 데이터가 삭제되었습니다"} + except Exception as e: + db.rollback() + return {"error": f"파일 삭제 실패: {str(e)}"} + +# 프로젝트 관리 API (비활성화 - jobs 테이블 사용) +# projects 테이블은 더 이상 사용하지 않음 +# ): +# """프로젝트 수정""" +# try: +# update_query = text(""" +# UPDATE projects +# SET project_name = :project_name, status = :status +# WHERE id = :project_id +# """) +# +# db.execute(update_query, { +# "project_id": project_id, +# "project_name": project_data["project_name"], +# "status": project_data["status"] +# }) +# +# db.commit() +# return {"success": True} +# except Exception as e: +# db.rollback() +# return {"error": f"프로젝트 수정 실패: {str(e)}"} + +# @app.delete("/projects/{project_id}") +# async def delete_project( +# project_id: int, +# db: Session = Depends(get_db) +# ): +# """프로젝트 삭제""" +# try: +# delete_query = text("DELETE FROM projects WHERE id = :project_id") +# db.execute(delete_query, {"project_id": project_id}) +# db.commit() +# return {"success": True} +# except Exception as e: +# db.rollback() +# return {"error": f"프로젝트 삭제 실패: {str(e)}"} + @app.get("/") async def root(): return { "message": "TK-MP BOM Management API", "version": "1.0.0", - "endpoints": ["/docs", "/jobs", "/files"] + "endpoints": ["/docs", "/jobs", "/files", "/projects"] } +# Jobs API +# @app.get("/jobs") +# async def get_jobs(db: Session = Depends(get_db)): +# """Jobs 목록 조회""" +# try: +# # jobs 테이블에서 데이터 조회 +# query = text(""" +# SELECT +# job_no, +# job_name, +# client_name, +# end_user, +# epc_company, +# status, +# created_at +# FROM jobs +# WHERE is_active = true +# ORDER BY created_at DESC +# """) +# +# result = db.execute(query) +# jobs = result.fetchall() +# +# return [ +# { +# "job_no": job.job_no, +# "job_name": job.job_name, +# "client_name": job.client_name, +# "end_user": job.end_user, +# "epc_company": job.epc_company, +# "status": job.status or "진행중", +# "created_at": job.created_at +# } +# for job in jobs +# ] +# except Exception as e: +# print(f"Jobs 조회 에러: {str(e)}") +# return {"error": f"Jobs 조회 실패: {str(e)}"} + +# 파일 업로드 API +@app.post("/upload") +async def upload_file( + file: UploadFile = File(...), + job_no: str = Form(...), # project_id 대신 job_no 사용 + bom_name: str = Form(""), # BOM 이름 추가 + bom_type: str = Form(""), + revision: str = Form("Rev.0"), + parent_bom_id: Optional[int] = Form(None), + description: str = Form(""), + db: Session = Depends(get_db) +): + """파일 업로드 및 자재 분류 (자동 리비전 관리)""" + try: + print("=== main.py 업로드 API 호출됨 ===") + print(f"파일명: {file.filename}") + print(f"job_no: {job_no}") + print(f"bom_name: {bom_name}") + print(f"bom_type: {bom_type}") + + # job_no로 job 확인 + job_query = text("SELECT job_no FROM jobs WHERE job_no = :job_no AND is_active = true") + job_result = db.execute(job_query, {"job_no": job_no}) + job = job_result.fetchone() + + if not job: + return {"error": f"Job No. '{job_no}'에 해당하는 작업을 찾을 수 없습니다."} + + # 업로드 디렉토리 생성 + upload_dir = "uploads" + os.makedirs(upload_dir, exist_ok=True) + + # 파일 저장 + if file.filename: + file_path = os.path.join(upload_dir, file.filename) + print(f"파일 저장 경로: {file_path}") + print(f"원본 파일명: {file.filename}") + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + print(f"파일 저장 완료: {file_path}") + else: + return {"error": "파일명이 없습니다."} + + # 파일 크기 계산 + file_size = os.path.getsize(file_path) + + # 파일 타입 결정 + file_type = "excel" if file.filename.endswith(('.xls', '.xlsx')) else "csv" if file.filename.endswith('.csv') else "unknown" + + # BOM 종류별 자동 리비전 관리 + if bom_type and not parent_bom_id: + # 같은 job_no의 같은 파일명에 대한 최신 리비전 조회 + latest_revision_query = text(""" + SELECT revision FROM files + WHERE job_no = :job_no AND original_filename = :filename + ORDER BY revision DESC LIMIT 1 + """) + + result = db.execute(latest_revision_query, { + "job_no": job_no, + "filename": file.filename + }) + + latest_file = result.fetchone() + if latest_file: + # 기존 리비전이 있으면 다음 리비전 번호 생성 + current_rev = latest_file.revision + if current_rev.startswith("Rev."): + try: + rev_num = int(current_rev.replace("Rev.", "")) + revision = f"Rev.{rev_num + 1}" + except ValueError: + revision = "Rev.1" + else: + revision = "Rev.1" + else: + # 첫 번째 업로드인 경우 Rev.0 + revision = "Rev.0" + + # 데이터베이스에 파일 정보 저장 + insert_query = text(""" + INSERT INTO files ( + job_no, filename, original_filename, file_path, + file_size, upload_date, revision, file_type, uploaded_by + ) VALUES ( + :job_no, :filename, :original_filename, :file_path, + :file_size, NOW(), :revision, :file_type, :uploaded_by + ) RETURNING id + """) + + result = db.execute(insert_query, { + "job_no": job_no, + "filename": file.filename, + "original_filename": file.filename, + "file_path": file_path, + "file_size": file_size, + "revision": revision, + "file_type": file_type, + "uploaded_by": "system" + }) + + file_id = result.fetchone()[0] + + # 1차: 파일 파싱 (CSV/Excel 파일 읽기) + materials_data = parse_file(file_path) + + # 2차: 각 자재를 분류기로 분류 + classified_materials = [] + + # 리비전 업로드인 경우 기존 분류 정보 가져오기 + existing_classifications = {} + if parent_bom_id: + parent_materials = db.execute( + text("SELECT original_description, classified_category, classified_subcategory, material_grade, schedule, size_spec FROM materials WHERE file_id = :file_id"), + {"file_id": parent_bom_id} + ).fetchall() + + for material in parent_materials: + existing_classifications[material.original_description] = { + "classified_category": material.classified_category, + "classified_subcategory": material.classified_subcategory, + "material_grade": material.material_grade, + "schedule": material.schedule, + "size_spec": material.size_spec + } + + for material in materials_data: + # 리비전 업로드인 경우 기존 분류 사용, 아니면 새로 분류 + if parent_bom_id and material.get("original_description") in existing_classifications: + existing_class = existing_classifications[material.get("original_description")] + classified_material = { + **material, + **existing_class, + "classification_confidence": 1.0 # 기존 분류이므로 높은 신뢰도 + } + else: + classified_material = classify_material_item(material) + classified_materials.append(classified_material) + + # 3차: 분류된 자재를 데이터베이스에 저장 + for material in classified_materials: + insert_material_query = text(""" + INSERT INTO materials ( + file_id, line_number, original_description, + classified_category, classified_subcategory, + material_grade, schedule, size_spec, + quantity, unit, drawing_name, area_code, line_no, + classification_confidence, is_verified, created_at + ) VALUES ( + :file_id, :line_number, :original_description, + :classified_category, :classified_subcategory, + :material_grade, :schedule, :size_spec, + :quantity, :unit, :drawing_name, :area_code, :line_no, + :classification_confidence, :is_verified, NOW() + ) + """) + + db.execute(insert_material_query, { + "file_id": file_id, + "line_number": material.get("line_number", 0), + "original_description": material.get("original_description", ""), + "classified_category": material.get("classified_category", ""), + "classified_subcategory": material.get("classified_subcategory", ""), + "material_grade": material.get("material_grade", ""), + "schedule": material.get("schedule", ""), + "size_spec": material.get("size_spec", ""), + "quantity": material.get("quantity", 0), + "unit": material.get("unit", ""), + "drawing_name": material.get("drawing_name", ""), + "area_code": material.get("area_code", ""), + "line_no": material.get("line_no", ""), + "classification_confidence": material.get("classification_confidence", 0.0), + "is_verified": False + }) + + db.commit() + + return { + "success": True, + "file_id": file_id, + "filename": file.filename, + "materials_count": len(classified_materials), + "revision": revision, + "message": f"파일이 성공적으로 업로드되고 {len(classified_materials)}개의 자재가 분류되었습니다. (리비전: {revision})" + } + except Exception as e: + db.rollback() + print(f"업로드 실패: {str(e)}") + # HTTP 400 에러로 변경 + from fastapi import HTTPException + raise HTTPException(status_code=400, detail=f"파일 업로드 및 분류 실패: {str(e)}") + +def parse_file(file_path: str) -> List[Dict]: + """파일 파싱 (CSV/Excel)""" + import pandas as pd + import os + + try: + print(f"parse_file 호출됨: {file_path}") + print(f"파일 존재 여부: {os.path.exists(file_path)}") + print(f"파일 확장자: {os.path.splitext(file_path)[1]}") + + # 파일 확장자를 소문자로 변환하여 검증 + file_extension = os.path.splitext(file_path)[1].lower() + print(f"소문자 변환된 확장자: {file_extension}") + + if file_extension == '.csv': + df = pd.read_csv(file_path) + elif file_extension in ['.xls', '.xlsx']: + df = pd.read_excel(file_path) + else: + print(f"지원되지 않는 파일 형식: {file_path}") + print(f"파일 확장자: {file_extension}") + raise ValueError("지원하지 않는 파일 형식입니다.") + + print(f"파일 파싱 시작: {file_path}") + print(f"데이터프레임 형태: {df.shape}") + print(f"컬럼명: {list(df.columns)}") + + # 컬럼명 매핑 (대소문자 구분 없이) + column_mapping = { + 'description': ['DESCRIPTION', 'Description', 'description', 'DESC', 'Desc', 'desc', 'ITEM', 'Item', 'item'], + 'quantity': ['QTY', 'Quantity', 'quantity', 'QTY.', 'Qty', 'qty', 'AMOUNT', 'Amount', 'amount'], + 'unit': ['UNIT', 'Unit', 'unit', 'UOM', 'Uom', 'uom'], + 'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'], + 'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'], + 'line': ['LINE', 'Line', 'line', 'LINE_NO', 'Line_No', 'line_no', 'PIPELINE', 'Pipeline', 'pipeline'] + } + + # 실제 컬럼명 찾기 + found_columns = {} + for target_col, possible_names in column_mapping.items(): + for col_name in possible_names: + if col_name in df.columns: + found_columns[target_col] = col_name + break + + print(f"찾은 컬럼 매핑: {found_columns}") + + materials = [] + for index, row in df.iterrows(): + # 빈 행 건너뛰기 + if row.isna().all(): + continue + + # 안전한 값 추출 + description = str(row.get(found_columns.get('description', ''), '') or '') + quantity_raw = row.get(found_columns.get('quantity', 1), 1) + quantity = float(quantity_raw) if quantity_raw is not None else 1.0 + unit = str(row.get(found_columns.get('unit', 'EA'), 'EA') or 'EA') + drawing = str(row.get(found_columns.get('drawing', ''), '') or '') + area = str(row.get(found_columns.get('area', ''), '') or '') + line = str(row.get(found_columns.get('line', ''), '') or '') + + material = { + "line_number": index + 1, + "original_description": description, + "quantity": quantity, + "unit": unit, + "drawing_name": drawing, + "area_code": area, + "line_no": line + } + + # 빈 설명은 건너뛰기 + if not material["original_description"] or material["original_description"].strip() == '': + continue + + materials.append(material) + + print(f"파싱된 자재 수: {len(materials)}") + if materials: + print(f"첫 번째 자재 예시: {materials[0]}") + + return materials + except Exception as e: + print(f"파일 파싱 오류: {str(e)}") + raise Exception(f"파일 파싱 실패: {str(e)}") + +def classify_material_item(material: Dict) -> Dict: + """개별 자재 분류""" + from .services import ( + pipe_classifier, fitting_classifier, bolt_classifier, + valve_classifier, instrument_classifier, flange_classifier, + gasket_classifier, material_classifier + ) + + description = material.get("original_description", "") + + # 각 분류기로 분류 시도 + classifiers = [ + ("PIPE", pipe_classifier.classify_pipe), + ("FITTING", fitting_classifier.classify_fitting), + ("BOLT", bolt_classifier.classify_bolt), + ("VALVE", valve_classifier.classify_valve), + ("INSTRUMENT", instrument_classifier.classify_instrument), + ("FLANGE", flange_classifier.classify_flange), + ("GASKET", gasket_classifier.classify_gasket) + ] + + best_result = None + best_confidence = 0.0 + + for category, classifier_func in classifiers: + try: + result = classifier_func(description) + if result and result.get("confidence", 0) > best_confidence: + best_result = result + best_confidence = result.get("confidence", 0) + except Exception: + continue + + # 재질 분류 + material_result = material_classifier.classify_material(description) + + # 최종 결과 조합 + final_result = { + **material, + "classified_category": best_result.get("category", "UNKNOWN") if best_result else "UNKNOWN", + "classified_subcategory": best_result.get("subcategory", "") if best_result else "", + "material_grade": material_result.get("grade", "") if material_result else "", + "schedule": best_result.get("schedule", "") if best_result else "", + "size_spec": best_result.get("size_spec", "") if best_result else "", + "classification_confidence": best_confidence + } + + return final_result + @app.get("/health") async def health_check(): return {"status": "healthy", "timestamp": "2024-07-15"} diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index 4d9bfc1..d893f3d 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -12,6 +12,14 @@ from pathlib import Path from ..database import get_db from app.services.material_classifier import classify_material +from app.services.bolt_classifier import classify_bolt +from app.services.flange_classifier import classify_flange +from app.services.fitting_classifier import classify_fitting +from app.services.gasket_classifier import classify_gasket +from app.services.instrument_classifier import classify_instrument +from app.services.pipe_classifier import classify_pipe +from app.services.valve_classifier import classify_valve + router = APIRouter() UPLOAD_DIR = Path("uploads") @@ -153,16 +161,21 @@ async def upload_file( file_path = UPLOAD_DIR / unique_filename try: + print("파일 저장 시작") with open(file_path, "wb") as buffer: shutil.copyfileobj(file.file, buffer) + print(f"파일 저장 완료: {file_path}") except Exception as e: raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}") try: + print("파일 저장 시작") materials_data = parse_file_data(str(file_path)) parsed_count = len(materials_data) + print(f"파싱 완료: {parsed_count}개 자재") - # 파일 정보 저장 + # 파일 정보 저장 (project_id 제거) + print("DB 저장 시작") file_insert_query = text(""" INSERT INTO files (filename, original_filename, file_path, job_no, revision, description, file_size, parsed_count, is_active) VALUES (:filename, :original_filename, :file_path, :job_no, :revision, :description, :file_size, :parsed_count, :is_active) @@ -182,20 +195,40 @@ async def upload_file( }) file_id = file_result.fetchone()[0] + print(f"파일 저장 완료: file_id = {file_id}") - # 자재 데이터 저장 + # 자재 데이터 저장 (분류 포함) + print("자재 분류 및 저장 시작") materials_inserted = 0 + classification_stats = { + 'BOLT': 0, 'FLANGE': 0, 'FITTING': 0, 'GASKET': 0, + 'INSTRUMENT': 0, 'PIPE': 0, 'VALVE': 0, 'MATERIAL': 0, 'OTHER': 0 + } + for material_data in materials_data: + # 자재 분류 실행 + classification_result = classify_material_item( + material_data["original_description"], + material_data["size_spec"] + ) + + # 분류 통계 업데이트 + category = classification_result.get('category', 'OTHER') + if category in classification_stats: + classification_stats[category] += 1 + material_insert_query = text(""" INSERT INTO materials ( file_id, original_description, quantity, unit, size_spec, material_grade, line_number, row_number, classified_category, - classification_confidence, is_verified, created_at + classification_confidence, is_verified, created_at, + subcategory, standard, grade ) VALUES ( :file_id, :original_description, :quantity, :unit, :size_spec, :material_grade, :line_number, :row_number, :classified_category, - :classification_confidence, :is_verified, :created_at + :classification_confidence, :is_verified, :created_at, + :subcategory, :standard, :grade ) """) @@ -208,14 +241,20 @@ async def upload_file( "material_grade": material_data["material_grade"], "line_number": material_data["line_number"], "row_number": material_data["row_number"], - "classified_category": get_major_category(material_data["original_description"]), - "classification_confidence": 0.9, + "classified_category": classification_result.get('category', 'OTHER'), + "classification_confidence": classification_result.get('confidence', 0.0), "is_verified": False, - "created_at": datetime.now() + "created_at": datetime.now(), + "subcategory": classification_result.get('subcategory', ''), + "standard": classification_result.get('standard', ''), + "grade": classification_result.get('grade', '') }) materials_inserted += 1 + print(f"자재 저장 완료: {materials_inserted}개") + print("커밋 직전") db.commit() + print("커밋 완료") return { "success": True, @@ -224,6 +263,7 @@ async def upload_file( "file_id": file_id, "parsed_materials_count": parsed_count, "saved_materials_count": materials_inserted, + "classification_stats": classification_stats, "sample_materials": materials_data[:3] if materials_data else [] } @@ -231,93 +271,260 @@ async def upload_file( db.rollback() if os.path.exists(file_path): os.remove(file_path) + import traceback + print(traceback.format_exc()) # 에러 전체 로그 출력 raise HTTPException(status_code=500, detail=f"파일 처리 실패: {str(e)}") @router.get("/materials") async def get_materials( - job_no: Optional[str] = None, - file_id: Optional[str] = None, + project_id: Optional[int] = None, + job_id: Optional[int] = None, + revision: Optional[str] = None, + grouping: Optional[str] = None, + search: Optional[str] = None, + search_value: Optional[str] = None, + item_type: Optional[str] = None, + material_grade: Optional[str] = None, + size_spec: Optional[str] = None, + file_filter: Optional[str] = None, + sort_by: Optional[str] = None, skip: int = 0, limit: int = 100, db: Session = Depends(get_db) ): - """저장된 자재 목록 조회""" + """자재 목록 조회 (개선된 버전)""" try: - query = """ - SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit, - m.size_spec, m.material_grade, m.line_number, m.row_number, - m.created_at, - f.original_filename, f.job_no, - j.job_no, j.job_name, m.classified_category, m.classification_confidence + # 기본 쿼리 구성 + base_query = """ + SELECT + m.id, + m.original_description, + m.quantity, + m.unit, + m.size_spec, + m.material_grade, + m.line_number, + m.row_number, + m.classified_category, + m.classification_confidence, + m.is_verified, + m.created_at, + f.job_no as job_number, + f.revision, + f.original_filename, + f.project_id, + p.project_name, + COUNT(*) OVER() as total_count FROM materials m LEFT JOIN files f ON m.file_id = f.id - LEFT JOIN jobs j ON f.job_no = j.job_no + LEFT JOIN projects p ON f.project_id = p.id WHERE 1=1 """ params = {} + conditions = [] - if job_no: - query += " AND f.job_no = :job_no" - params["job_no"] = job_no + # 프로젝트 필터 + if project_id: + conditions.append("f.project_id = :project_id") + params["project_id"] = project_id - if file_id: - query += " AND m.file_id = :file_id" - params["file_id"] = file_id - - query += " ORDER BY m.line_number ASC LIMIT :limit OFFSET :skip" + # Job ID 필터 + if job_id: + conditions.append("f.job_no = (SELECT job_no FROM jobs WHERE id = :job_id)") + params["job_id"] = job_id + + # 리비전 필터 + if revision: + conditions.append("f.revision = :revision") + params["revision"] = revision + + # 검색 필터 (개선된 버전) + if search and search_value: + try: + if search == "project": + conditions.append("p.project_name ILIKE :search_value") + elif search == "job": + conditions.append("f.job_no ILIKE :search_value") + elif search == "material": + conditions.append("m.original_description ILIKE :search_value") + elif search == "description": + conditions.append("m.original_description ILIKE :search_value") + elif search == "grade": + conditions.append("m.material_grade ILIKE :search_value") + elif search == "size": + conditions.append("m.size_spec ILIKE :search_value") + elif search == "filename": + conditions.append("f.original_filename ILIKE :search_value") + else: + # 기본 검색 (기존 방식) + conditions.append("(m.original_description ILIKE :search_value OR m.material_grade ILIKE :search_value)") + + params["search_value"] = f"%{search_value}%" + except Exception as e: + print(f"검색 필터 처리 오류: {e}") + # 오류 발생 시 기본 검색으로 fallback + conditions.append("(m.original_description ILIKE :search_value OR m.material_grade ILIKE :search_value)") + params["search_value"] = f"%{search_value}%" + + # 품목 타입 필터 + if item_type: + conditions.append("m.classified_category = :item_type") + params["item_type"] = item_type + + # 재질 필터 + if material_grade: + conditions.append("m.material_grade ILIKE :material_grade") + params["material_grade"] = f"%{material_grade}%" + + # 사이즈 필터 + if size_spec: + conditions.append("m.size_spec ILIKE :size_spec") + params["size_spec"] = f"%{size_spec}%" + + # 파일명 필터 + if file_filter: + conditions.append("f.original_filename ILIKE :file_filter") + params["file_filter"] = f"%{file_filter}%" + + # 조건 추가 + if conditions: + base_query += " AND " + " AND ".join(conditions) + + # 그룹핑 처리 + if grouping: + if grouping == "item": + base_query += " GROUP BY m.classified_category, m.original_description, m.size_spec, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.revision, f.original_filename, f.project_id, p.project_name" + elif grouping == "material": + base_query += " GROUP BY m.material_grade, m.original_description, m.size_spec, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.revision, f.original_filename, f.project_id, p.project_name" + elif grouping == "size": + base_query += " GROUP BY m.size_spec, m.original_description, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.revision, f.original_filename, f.project_id, p.project_name" + elif grouping == "job": + base_query += " GROUP BY f.job_no, m.original_description, m.size_spec, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.revision, f.original_filename, f.project_id, p.project_name" + elif grouping == "revision": + base_query += " GROUP BY f.revision, m.original_description, m.size_spec, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.original_filename, f.project_id, p.project_name" + + # 정렬 + if sort_by: + if sort_by == "quantity_desc": + base_query += " ORDER BY SUM(m.quantity) DESC" + elif sort_by == "quantity_asc": + base_query += " ORDER BY SUM(m.quantity) ASC" + elif sort_by == "name_asc": + base_query += " ORDER BY m.original_description ASC" + elif sort_by == "name_desc": + base_query += " ORDER BY m.original_description DESC" + elif sort_by == "created_desc": + base_query += " ORDER BY m.created_at DESC" + elif sort_by == "created_asc": + base_query += " ORDER BY m.created_at ASC" + else: + base_query += " ORDER BY m.id DESC" + else: + base_query += " ORDER BY m.id DESC" + + # 페이징 + base_query += " LIMIT :limit OFFSET :skip" params["limit"] = limit params["skip"] = skip - result = db.execute(text(query), params) + result = db.execute(text(base_query), params) materials = result.fetchall() - # 전체 개수 조회 - count_query = """ - SELECT COUNT(*) as total - FROM materials m - LEFT JOIN files f ON m.file_id = f.id - WHERE 1=1 - """ - count_params = {} + # 리비전 비교 데이터 생성 + revision_comparison = None + if revision and revision != "Rev.0": + comparison_query = """ + SELECT + m.original_description, + m.size_spec, + m.material_grade, + SUM(CASE WHEN f.revision = :current_revision THEN m.quantity ELSE 0 END) as current_qty, + SUM(CASE WHEN f.revision = :prev_revision THEN m.quantity ELSE 0 END) as prev_qty + FROM materials m + LEFT JOIN files f ON m.file_id = f.id + WHERE f.project_id = :project_id + AND f.revision IN (:current_revision, :prev_revision) + GROUP BY m.original_description, m.size_spec, m.material_grade + HAVING + SUM(CASE WHEN f.revision = :current_revision THEN m.quantity ELSE 0 END) != + SUM(CASE WHEN f.revision = :prev_revision THEN m.quantity ELSE 0 END) + """ + + comparison_params = { + "project_id": project_id, + "current_revision": revision, + "prev_revision": f"Rev.{int(revision.split('.')[-1]) - 1}" + } + + comparison_result = db.execute(text(comparison_query), comparison_params) + comparison_data = comparison_result.fetchall() + + if comparison_data: + changes = [] + for row in comparison_data: + change = row.current_qty - row.prev_qty + if change != 0: + changes.append({ + "description": row.original_description, + "size_spec": row.size_spec, + "material_grade": row.material_grade, + "current_qty": row.current_qty, + "prev_qty": row.prev_qty, + "change": change + }) + + revision_comparison = { + "summary": f"{revision}에서 {len(changes)}개 항목이 변경되었습니다", + "changes": changes + } - if job_no: - count_query += " AND f.job_no = :job_no" - count_params["job_no"] = job_no + # 결과 포맷팅 + formatted_materials = [] + for material in materials: + # 라인 번호 문자열 생성 + line_numbers = [material.line_number] if material.line_number else [] + line_numbers_str = ", ".join(map(str, line_numbers)) if line_numbers else "" + + # 수량 변경 계산 (리비전 비교) + quantity_change = None + if revision_comparison: + for change in revision_comparison["changes"]: + if (change["description"] == material.original_description and + change["size_spec"] == material.size_spec and + change["material_grade"] == material.material_grade): + quantity_change = change["change"] + break + + formatted_material = { + "id": material.id, + "original_description": material.original_description, + "quantity": float(material.quantity) if material.quantity else 0, + "unit": material.unit or "EA", + "size_spec": material.size_spec or "", + "material_grade": material.material_grade or "", + "line_number": material.line_number, + "line_numbers_str": line_numbers_str, + "line_count": len(line_numbers), + "classified_category": material.classified_category or "OTHER", + "classification_confidence": float(material.classification_confidence) if material.classification_confidence else 0, + "is_verified": material.is_verified or False, + "created_at": material.created_at.isoformat() if material.created_at else None, + "job_number": material.job_number, + "revision": material.revision or "Rev.0", + "original_filename": material.original_filename, + "project_id": material.project_id, + "project_name": material.project_name, + "quantity_change": quantity_change + } + + formatted_materials.append(formatted_material) - if file_id: - count_query += " AND m.file_id = :file_id" - count_params["file_id"] = file_id - - count_result = db.execute(text(count_query), count_params) - total_count = count_result.fetchone()[0] + total_count = materials[0].total_count if materials else 0 return { - "success": True, + "materials": formatted_materials, "total_count": total_count, - "returned_count": len(materials), - "skip": skip, - "limit": limit, - "materials": [ - { - "id": m[0], - "file_id": m[1], - "filename": m[10], - "job_no": m[12], - "project_code": m[12], - "project_name": "Job-" + str(m[11]) if m[11] else "Unknown", - "original_description": m[2], - "quantity": float(m[3]) if m.quantity else 0, - "unit": m[4], - "classified_category": m[14], - "classification_confidence": float(m[15]) if m.classification_confidence else 0.0, - "size_spec": m[5], - "material_grade": m[6], - "line_number": m[7], - "row_number": m[8], - "created_at": m[9] - } - for m in materials - ] + "revision_comparison": revision_comparison } except Exception as e: @@ -419,3 +626,123 @@ def get_major_category(description): return 'bolt' else: return 'other' + +def classify_material_item(description: str, size_spec: str = "") -> dict: + """ + 자재를 각 분류기로 보내서 분류하는 통합 함수 + + Args: + description: 자재 설명 + size_spec: 사이즈 정보 + + Returns: + 분류 결과 딕셔너리 + """ + desc_upper = description.upper().strip() + + # 1. 볼트 분류 + bolt_result = classify_bolt(description) + if bolt_result.get('confidence', 0) > 0.7: + return { + 'category': 'BOLT', + 'subcategory': bolt_result.get('bolt_type', 'UNKNOWN'), + 'standard': bolt_result.get('standard', ''), + 'grade': bolt_result.get('grade', ''), + 'confidence': bolt_result.get('confidence', 0), + 'details': bolt_result + } + + # 2. 플랜지 분류 + flange_result = classify_flange(description) + if flange_result.get('confidence', 0) > 0.7: + return { + 'category': 'FLANGE', + 'subcategory': flange_result.get('flange_type', 'UNKNOWN'), + 'standard': flange_result.get('standard', ''), + 'grade': flange_result.get('grade', ''), + 'confidence': flange_result.get('confidence', 0), + 'details': flange_result + } + + # 3. 피팅 분류 + fitting_result = classify_fitting(description) + if fitting_result.get('confidence', 0) > 0.7: + return { + 'category': 'FITTING', + 'subcategory': fitting_result.get('fitting_type', 'UNKNOWN'), + 'standard': fitting_result.get('standard', ''), + 'grade': fitting_result.get('grade', ''), + 'confidence': fitting_result.get('confidence', 0), + 'details': fitting_result + } + + # 4. 가스켓 분류 + gasket_result = classify_gasket(description) + if gasket_result.get('confidence', 0) > 0.7: + return { + 'category': 'GASKET', + 'subcategory': gasket_result.get('gasket_type', 'UNKNOWN'), + 'standard': gasket_result.get('standard', ''), + 'grade': gasket_result.get('grade', ''), + 'confidence': gasket_result.get('confidence', 0), + 'details': gasket_result + } + + # 5. 계기 분류 + instrument_result = classify_instrument(description) + if instrument_result.get('confidence', 0) > 0.7: + return { + 'category': 'INSTRUMENT', + 'subcategory': instrument_result.get('instrument_type', 'UNKNOWN'), + 'standard': instrument_result.get('standard', ''), + 'grade': instrument_result.get('grade', ''), + 'confidence': instrument_result.get('confidence', 0), + 'details': instrument_result + } + + # 6. 파이프 분류 + pipe_result = classify_pipe(description) + if pipe_result.get('confidence', 0) > 0.7: + return { + 'category': 'PIPE', + 'subcategory': pipe_result.get('pipe_type', 'UNKNOWN'), + 'standard': pipe_result.get('standard', ''), + 'grade': pipe_result.get('grade', ''), + 'confidence': pipe_result.get('confidence', 0), + 'details': pipe_result + } + + # 7. 밸브 분류 + valve_result = classify_valve(description) + if valve_result.get('confidence', 0) > 0.7: + return { + 'category': 'VALVE', + 'subcategory': valve_result.get('valve_type', 'UNKNOWN'), + 'standard': valve_result.get('standard', ''), + 'grade': valve_result.get('grade', ''), + 'confidence': valve_result.get('confidence', 0), + 'details': valve_result + } + + # 8. 재질 분류 (기본) + material_result = classify_material(description) + if material_result.get('confidence', 0) > 0.5: + return { + 'category': 'MATERIAL', + 'subcategory': material_result.get('material_type', 'UNKNOWN'), + 'standard': material_result.get('standard', ''), + 'grade': material_result.get('grade', ''), + 'confidence': material_result.get('confidence', 0), + 'details': material_result + } + + # 9. 기본 분류 (키워드 기반) + category = get_major_category(description) + return { + 'category': category.upper(), + 'subcategory': 'UNKNOWN', + 'standard': '', + 'grade': '', + 'confidence': 0.3, + 'details': {} + } diff --git a/backend/app/routers/jobs.py b/backend/app/routers/jobs.py index 779f573..494ed4c 100644 --- a/backend/app/routers/jobs.py +++ b/backend/app/routers/jobs.py @@ -29,7 +29,7 @@ async def get_jobs( search: Optional[str] = Query(None), db: Session = Depends(get_db) ): - """Job 목록 조회""" + """Job 목록 조회 (job_name을 프로젝트명으로 사용)""" try: query = """ SELECT job_no, job_name, client_name, end_user, epc_company, @@ -68,7 +68,8 @@ async def get_jobs( "delivery_terms": job.delivery_terms, "status": job.status, "description": job.description, - "created_at": job.created_at + "created_at": job.created_at, + "project_name": job.job_name # job_name을 프로젝트명으로 사용 } for job in jobs ] diff --git a/backend/requirements.txt b/backend/requirements.txt index 50a98f6..4d1b10e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,6 +10,7 @@ alembic==1.13.1 # 파일 처리 pandas==2.1.4 openpyxl==3.1.2 +xlrd>=2.0.1 python-multipart==0.0.6 # 데이터 검증 diff --git a/backend/scripts/04_add_job_no_to_files.sql b/backend/scripts/04_add_job_no_to_files.sql new file mode 100644 index 0000000..71020c0 --- /dev/null +++ b/backend/scripts/04_add_job_no_to_files.sql @@ -0,0 +1,18 @@ +-- files 테이블에 job_no 컬럼 추가 +-- 실행일: 2025.07.16 + +-- job_no 컬럼 추가 +ALTER TABLE files ADD COLUMN job_no VARCHAR(50); + +-- job_no에 인덱스 추가 +CREATE INDEX idx_files_job_no ON files(job_no); + +-- 기존 데이터가 있다면 project_id를 기반으로 job_no 설정 +-- (이 부분은 실제 데이터가 있을 때만 실행) +-- UPDATE files SET job_no = (SELECT official_project_code FROM projects WHERE projects.id = files.project_id); + +-- job_no를 NOT NULL로 설정 (데이터 마이그레이션 후) +-- ALTER TABLE files ALTER COLUMN job_no SET NOT NULL; + +-- project_id 컬럼 제거 (선택사항 - 기존 데이터 백업 후) +-- ALTER TABLE files DROP COLUMN project_id; \ No newline at end of file diff --git a/backend/scripts/05_add_classification_columns.sql b/backend/scripts/05_add_classification_columns.sql new file mode 100644 index 0000000..f7721ea --- /dev/null +++ b/backend/scripts/05_add_classification_columns.sql @@ -0,0 +1,80 @@ +-- 분류 결과 저장을 위한 컬럼 추가 +-- 2024년 BOM 분류 시스템 개선 + +-- materials 테이블에 분류 관련 컬럼 추가 +ALTER TABLE materials ADD COLUMN IF NOT EXISTS subcategory VARCHAR(100); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS standard VARCHAR(200); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS grade VARCHAR(200); +ALTER TABLE materials ADD COLUMN IF NOT EXISTS classification_details JSONB; + +-- files 테이블에 분류 통계 컬럼 추가 +ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_stats JSONB; +ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_completed BOOLEAN DEFAULT FALSE; +ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_timestamp TIMESTAMP; + +-- 인덱스 추가 (성능 향상) +CREATE INDEX IF NOT EXISTS idx_materials_classified_category ON materials(classified_category); +CREATE INDEX IF NOT EXISTS idx_materials_subcategory ON materials(subcategory); +CREATE INDEX IF NOT EXISTS idx_materials_standard ON materials(standard); +CREATE INDEX IF NOT EXISTS idx_materials_grade ON materials(grade); +CREATE INDEX IF NOT EXISTS idx_materials_classification_confidence ON materials(classification_confidence); + +-- 기존 데이터에 대한 기본값 설정 +UPDATE materials SET + subcategory = COALESCE(subcategory, ''), + standard = COALESCE(standard, ''), + grade = COALESCE(grade, ''), + classification_details = COALESCE(classification_details, '{}'::jsonb) +WHERE subcategory IS NULL OR standard IS NULL OR grade IS NULL OR classification_details IS NULL; + +-- 분류 완료된 파일들 업데이트 +UPDATE files SET + classification_completed = TRUE, + classification_timestamp = created_at +WHERE parsed_count > 0; + +-- 통계 뷰 생성 (분류 결과 통계 조회용) +CREATE OR REPLACE VIEW classification_summary AS +SELECT + f.job_no, + f.original_filename, + f.parsed_count, + f.classification_completed, + f.classification_timestamp, + COUNT(*) as total_materials, + COUNT(CASE WHEN m.classified_category = 'BOLT' THEN 1 END) as bolt_count, + COUNT(CASE WHEN m.classified_category = 'FLANGE' THEN 1 END) as flange_count, + COUNT(CASE WHEN m.classified_category = 'FITTING' THEN 1 END) as fitting_count, + COUNT(CASE WHEN m.classified_category = 'GASKET' THEN 1 END) as gasket_count, + COUNT(CASE WHEN m.classified_category = 'INSTRUMENT' THEN 1 END) as instrument_count, + COUNT(CASE WHEN m.classified_category = 'PIPE' THEN 1 END) as pipe_count, + COUNT(CASE WHEN m.classified_category = 'VALVE' THEN 1 END) as valve_count, + COUNT(CASE WHEN m.classified_category = 'MATERIAL' THEN 1 END) as material_count, + COUNT(CASE WHEN m.classified_category = 'OTHER' THEN 1 END) as other_count, + AVG(m.classification_confidence) as avg_confidence, + COUNT(CASE WHEN m.is_verified = TRUE THEN 1 END) as verified_count +FROM files f +LEFT JOIN materials m ON f.id = m.file_id +WHERE f.is_active = TRUE +GROUP BY f.id, f.job_no, f.original_filename, f.parsed_count, f.classification_completed, f.classification_timestamp; + +-- 분류 성능 통계 뷰 +CREATE OR REPLACE VIEW classification_performance AS +SELECT + classified_category, + subcategory, + standard, + COUNT(*) as total_count, + AVG(classification_confidence) as avg_confidence, + COUNT(CASE WHEN is_verified = TRUE THEN 1 END) as verified_count, + COUNT(CASE WHEN is_verified = FALSE THEN 1 END) as unverified_count, + ROUND( + (COUNT(CASE WHEN is_verified = TRUE THEN 1 END)::DECIMAL / COUNT(*) * 100), 2 + ) as verification_rate +FROM materials +WHERE classified_category IS NOT NULL +GROUP BY classified_category, subcategory, standard +ORDER BY total_count DESC; + +-- 변경사항 확인 +SELECT 'Database schema updated successfully' as status; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a3ac30d..496ee19 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,35 +1,35 @@ { - "name": "frontend", + "name": "tk-mp-frontend", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend", + "name": "tk-mp-frontend", "version": "0.0.0", "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^7.2.0", - "@mui/material": "^7.2.0", - "@mui/x-data-grid": "^8.8.0", - "axios": "^1.10.0", - "lucide-react": "^0.525.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-dropzone": "^14.3.8", - "react-router-dom": "^7.6.3" + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.14.19", + "@mui/material": "^5.14.20", + "axios": "^1.6.2", + "chart.js": "^4.4.0", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", + "react-router-dom": "^6.20.1" }, "devDependencies": { - "@eslint/js": "^9.30.1", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.6.0", - "eslint": "^9.30.1", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", - "vite": "^7.0.4" + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.1.1", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "vite": "^4.5.0" } }, "node_modules/@ampproject/remapping": { @@ -480,27 +480,10 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], @@ -511,13 +494,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], @@ -528,13 +511,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], @@ -545,13 +528,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -562,13 +545,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -579,13 +562,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], @@ -596,13 +579,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], @@ -613,13 +596,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], @@ -630,13 +613,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], @@ -647,13 +630,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], @@ -664,13 +647,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], @@ -681,13 +664,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], @@ -698,13 +681,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], @@ -715,13 +698,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], @@ -732,13 +715,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], @@ -749,13 +732,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -766,30 +749,13 @@ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ "x64" ], @@ -800,30 +766,13 @@ "netbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], @@ -834,30 +783,13 @@ "openbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "cpu": [ "x64" ], @@ -868,13 +800,13 @@ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ "arm64" ], @@ -885,13 +817,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "cpu": [ "ia32" ], @@ -902,13 +834,13 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -919,7 +851,7 @@ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -941,19 +873,6 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", @@ -964,55 +883,17 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", + "espree": "^9.6.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1020,98 +901,36 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=10.10.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1128,19 +947,13 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } + "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", @@ -1177,10 +990,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz", - "integrity": "sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz", + "integrity": "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==", "license": "MIT", "funding": { "type": "opencollective", @@ -1188,22 +1007,22 @@ } }, "node_modules/@mui/icons-material": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.2.0.tgz", - "integrity": "sha512-gRCspp3pfjHQyTmSOmYw7kUQTd9Udpdan4R8EnZvqPeoAtHnPzkvjBrBqzKaoAbbBp5bGF7BcD18zZJh4nwu0A==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", + "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6" + "@babel/runtime": "^7.23.9" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^7.2.0", + "@mui/material": "^5.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1214,26 +1033,26 @@ } }, "node_modules/@mui/material": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz", - "integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", + "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/core-downloads-tracker": "^7.2.0", - "@mui/system": "^7.2.0", - "@mui/types": "^7.4.4", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.23.9", + "@mui/core-downloads-tracker": "^5.18.0", + "@mui/system": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1", - "react-is": "^19.1.0", + "react-is": "^19.0.0", "react-transition-group": "^4.4.5" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", @@ -1242,7 +1061,6 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.2.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1254,26 +1072,23 @@ "@emotion/styled": { "optional": true }, - "@mui/material-pigment-css": { - "optional": true - }, "@types/react": { "optional": true } } }, - "node_modules/@mui/private-theming": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.2.0.tgz", - "integrity": "sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==", + "node_modules/@mui/material/node_modules/@mui/private-theming": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", + "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/utils": "^7.2.0", + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.17.1", "prop-types": "^15.8.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", @@ -1289,21 +1104,20 @@ } } }, - "node_modules/@mui/styled-engine": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.2.0.tgz", - "integrity": "sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==", + "node_modules/@mui/material/node_modules/@mui/styled-engine": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz", + "integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@emotion/cache": "^11.14.0", + "@babel/runtime": "^7.23.9", + "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", @@ -1323,23 +1137,23 @@ } } }, - "node_modules/@mui/system": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz", - "integrity": "sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==", + "node_modules/@mui/material/node_modules/@mui/system": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz", + "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/private-theming": "^7.2.0", - "@mui/styled-engine": "^7.2.0", - "@mui/types": "^7.4.4", - "@mui/utils": "^7.2.0", - "clsx": "^2.1.1", + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.17.1", + "@mui/styled-engine": "^5.18.0", + "@mui/types": "~7.2.15", + "@mui/utils": "^5.17.1", + "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", @@ -1363,14 +1177,11 @@ } } }, - "node_modules/@mui/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz", - "integrity": "sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==", + "node_modules/@mui/material/node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6" - }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1380,21 +1191,21 @@ } } }, - "node_modules/@mui/utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz", - "integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==", + "node_modules/@mui/material/node_modules/@mui/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/types": "^7.4.4", - "@types/prop-types": "^15.7.15", + "@babel/runtime": "^7.23.9", + "@mui/types": "~7.2.15", + "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", @@ -1410,63 +1221,42 @@ } } }, - "node_modules/@mui/x-data-grid": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.8.0.tgz", - "integrity": "sha512-xWoBmxHi5JvT0QvAYGYJYNy4DEi+Lez+lrsqw3YV7z0jEYyJoV9vjFCiFE4QmG6IPg62B1gZHYE5AkDLFCvPkw==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/utils": "^7.2.0", - "@mui/x-internals": "8.8.0", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "use-sync-external-store": "^1.5.0" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@mui/x-internals": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.8.0.tgz", - "integrity": "sha512-qTRK5oINkAjZ7sIHpSnESLNq1xtQUmmfmGscYUSEP0uHoYh6pKkNWH9+7yzggRHuTv+4011VBwN9s+efrk+xZg==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/utils": "^7.2.0", - "reselect": "^5.1.1" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + "node": ">= 8" } }, "node_modules/@popperjs/core": { @@ -1479,6 +1269,15 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -1486,286 +1285,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", - "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", - "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", - "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", - "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", - "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", - "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", - "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", - "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", - "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", - "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", - "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", - "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", - "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", - "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", - "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", - "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", - "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", - "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", - "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", - "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1811,20 +1330,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1838,22 +1343,23 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", "dependencies": { + "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^18.0.0" } }, "node_modules/@types/react-transition-group": { @@ -1865,6 +1371,13 @@ "@types/react": "*" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", @@ -1926,6 +1439,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1949,6 +1472,154 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1964,6 +1635,22 @@ "node": ">=4" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", @@ -2041,6 +1728,25 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2054,6 +1760,23 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2101,6 +1824,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2155,15 +1890,6 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -2210,6 +1936,60 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -2234,6 +2014,42 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2243,6 +2059,19 @@ "node": ">=0.4.0" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2283,6 +2112,75 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2301,6 +2199,34 @@ "node": ">= 0.4" } }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -2328,10 +2254,41 @@ "node": ">= 0.4" } }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2339,35 +2296,31 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/escalade": { @@ -2393,77 +2346,106 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", - "@humanfs/node": "^0.16.6", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", + "cross-spawn": "^7.0.2", "debug": "^4.3.2", + "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" }, "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "node_modules/eslint-plugin-react-refresh": { @@ -2476,10 +2458,41 @@ "eslint": ">=8.40" } }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2487,38 +2500,38 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2591,32 +2604,27 @@ "dev": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "flat-cache": "^3.0.4" }, "engines": { - "node": ">=16.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/file-selector": { @@ -2655,17 +2663,18 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.4" + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, "engines": { - "node": ">=16" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -2695,6 +2704,22 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/form-data": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", @@ -2711,6 +2736,13 @@ "node": ">= 6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2735,6 +2767,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2782,6 +2845,46 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2796,18 +2899,38 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2820,6 +2943,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2830,6 +2973,35 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2920,12 +3092,130 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2941,6 +3231,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2951,6 +3276,41 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2964,6 +3324,211 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2971,6 +3536,24 @@ "dev": true, "license": "ISC" }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3042,6 +3625,22 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3117,15 +3716,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "0.525.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", - "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3217,6 +3807,114 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3235,6 +3933,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3307,6 +4023,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3338,17 +4064,14 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">= 0.4" } }, "node_modules/postcss": { @@ -3423,25 +4146,60 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^18.3.1" } }, "node_modules/react-dropzone": { @@ -3478,41 +4236,35 @@ } }, "node_modules/react-router": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", - "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "@remix-run/router": "1.23.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz", - "integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==", + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", "license": "MIT", "dependencies": { - "react-router": "7.6.3" + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=14.0.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-transition-group": { @@ -3531,11 +4283,49 @@ "react-dom": ">=16.6.0" } }, - "node_modules/reselect": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", - "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", - "license": "MIT" + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/resolve": { "version": "1.22.10", @@ -3566,51 +4356,138 @@ "node": ">=4" } }, - "node_modules/rollup": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", - "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", "dependencies": { - "@types/estree": "1.0.8" + "glob": "^7.1.3" }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=18.0.0", + "node": ">=14.18.0", "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.0", - "@rollup/rollup-android-arm64": "4.45.0", - "@rollup/rollup-darwin-arm64": "4.45.0", - "@rollup/rollup-darwin-x64": "4.45.0", - "@rollup/rollup-freebsd-arm64": "4.45.0", - "@rollup/rollup-freebsd-x64": "4.45.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", - "@rollup/rollup-linux-arm-musleabihf": "4.45.0", - "@rollup/rollup-linux-arm64-gnu": "4.45.0", - "@rollup/rollup-linux-arm64-musl": "4.45.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", - "@rollup/rollup-linux-riscv64-gnu": "4.45.0", - "@rollup/rollup-linux-riscv64-musl": "4.45.0", - "@rollup/rollup-linux-s390x-gnu": "4.45.0", - "@rollup/rollup-linux-x64-gnu": "4.45.0", - "@rollup/rollup-linux-x64-musl": "4.45.0", - "@rollup/rollup-win32-arm64-msvc": "4.45.0", - "@rollup/rollup-win32-ia32-msvc": "4.45.0", - "@rollup/rollup-win32-x64-msvc": "4.45.0", "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } }, "node_modules/semver": { "version": "6.3.1", @@ -3622,11 +4499,54 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/shebang-command": { "version": "2.0.0", @@ -3651,6 +4571,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3670,6 +4666,131 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3714,22 +4835,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } + "license": "MIT" }, "node_modules/tslib": { "version": "2.8.1", @@ -3750,6 +4861,116 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3791,61 +5012,42 @@ "punycode": "^2.1.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/vite": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", - "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.6", - "picomatch": "^4.0.2", - "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.3" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", + "@types/node": ">= 14", + "less": "*", "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "jiti": { - "optional": true - }, "less": { "optional": true }, @@ -3855,9 +5057,6 @@ "sass": { "optional": true }, - "sass-embedded": { - "optional": true - }, "stylus": { "optional": true }, @@ -3866,12 +5065,6 @@ }, "terser": { "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, @@ -3891,6 +5084,95 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -3901,6 +5183,13 @@ "node": ">=0.10.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -3908,21 +5197,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e69de29..ad54ad2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "tk-mp-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.14.19", + "@mui/material": "^5.14.20", + "axios": "^1.6.2", + "chart.js": "^4.4.0", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", + "react-router-dom": "^6.20.1" + }, + "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.1.1", + "eslint": "^8.53.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "vite": "^4.5.0" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index fb8304f..94d03f1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,179 +1,19 @@ -import React, { useState, useEffect } from 'react'; -import { - AppBar, - Toolbar, - Typography, - Container, - Box, - Tab, - Tabs, - ThemeProvider, - createTheme, - CssBaseline -} from '@mui/material'; -import { - Dashboard as DashboardIcon, - Upload as UploadIcon, - List as ListIcon, - Assignment as ProjectIcon -} from '@mui/icons-material'; - -import Dashboard from './components/Dashboard'; -import FileUpload from './components/FileUpload'; -import MaterialList from './components/MaterialList'; -import ProjectManager from './components/ProjectManager'; - -// Material-UI 테마 설정 -const theme = createTheme({ - palette: { - primary: { - main: '#1976d2', - }, - secondary: { - main: '#dc004e', - }, - background: { - default: '#f5f5f5', - }, - }, - typography: { - h4: { - fontWeight: 600, - }, - h6: { - fontWeight: 500, - }, - }, -}); - -function TabPanel({ children, value, index, ...other }) { - return ( - - ); -} +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import JobSelectionPage from './pages/JobSelectionPage'; +import BOMManagerPage from './pages/BOMManagerPage'; +import MaterialsPage from './pages/MaterialsPage'; function App() { - const [tabValue, setTabValue] = useState(0); - const [projects, setProjects] = useState([]); - const [selectedProject, setSelectedProject] = useState(null); - - const handleTabChange = (event, newValue) => { - setTabValue(newValue); - }; - - // 프로젝트 목록 로드 - useEffect(() => { - fetchProjects(); - }, []); - - const fetchProjects = async () => { - try { - const response = await fetch('http://localhost:8000/api/projects'); - if (response.ok) { - const data = await response.json(); - setProjects(data); - if (data.length > 0 && !selectedProject) { - setSelectedProject(data[0]); - } - } - } catch (error) { - console.error('프로젝트 로드 실패:', error); - } - }; - return ( - - - - {/* 상단 앱바 */} - - - - TK-MP BOM 관리 시스템 - - - {selectedProject ? `프로젝트: ${selectedProject.name}` : '프로젝트 없음'} - - - - - {/* 탭 네비게이션 */} - - - - } - label="대시보드" - iconPosition="start" - /> - } - label="프로젝트 관리" - iconPosition="start" - /> - } - label="파일 업로드" - iconPosition="start" - /> - } - label="자재 목록" - iconPosition="start" - /> - - - - - {/* 메인 콘텐츠 */} - - - - - - - - - - - { - // 업로드 성공 시 대시보드로 이동 - setTabValue(0); - }} - /> - - - - - - - - + + + } /> + } /> + } /> + } /> + + ); } diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..91b4ea1 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,122 @@ +import axios from 'axios'; + +// 환경변수에서 API URL을 읽음 (Vite 기준) +const API_BASE_URL = import.meta.env.VITE_API_URL || + (import.meta.env.DEV ? 'http://localhost:8000' : 'http://localhost:8000'); + +console.log('API Base URL:', API_BASE_URL); +console.log('Environment:', import.meta.env.MODE); + +// axios 인스턴스 생성 +export const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, // 30초로 증가 + headers: { + 'Content-Type': 'application/json', + }, +}); + +// 재시도 로직을 위한 설정 +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; // 1초 + +// 재시도 함수 +const retryRequest = async (config, retries = MAX_RETRIES) => { + try { + return await api(config); + } catch (error) { + if (retries > 0 && (error.code === 'ECONNABORTED' || error.response?.status >= 500)) { + console.log(`API 재시도 중... (${MAX_RETRIES - retries + 1}/${MAX_RETRIES})`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); + return retryRequest(config, retries - 1); + } + throw error; + } +}; + +// 공통 에러 핸들링 (예시) +api.interceptors.response.use( + response => response, + error => { + console.error('API Error:', { + url: error.config?.url, + method: error.config?.method, + status: error.response?.status, + data: error.response?.data, + message: error.message + }); + + // 필요시 에러 로깅/알림 등 추가 + return Promise.reject(error); + } +); + +// 예시: 파일 업로드 (multipart/form-data) +export function uploadFile(formData, options = {}) { + const config = { + method: 'post', + url: '/upload', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' }, + ...options, + }; + + return retryRequest(config); +} + +// 예시: 자재 목록 조회 +export function fetchMaterials(params) { + return api.get('/files/materials', { params }); +} + +// 예시: 자재 요약 통계 +export function fetchMaterialsSummary(params) { + return api.get('/files/materials/summary', { params }); +} + +// 파일 목록 조회 +export function fetchFiles(params) { + return api.get('/files', { params }); +} + +// 파일 삭제 +export function deleteFile(fileId) { + return api.delete(`/files/${fileId}`); +} + +// 예시: Job 목록 조회 +export function fetchJobs(params) { + return api.get('/jobs', { params }); +} + +// 예시: Job 생성 +export function createJob(data) { + return api.post('/jobs', data); +} + +// 프로젝트 수정 +export function updateProject(projectId, data) { + return api.put(`/projects/${projectId}`, data); +} + +// 프로젝트 삭제 +export function deleteProject(projectId) { + return api.delete(`/projects/${projectId}`); +} + +// 스풀 관련 API +export function fetchProjectSpools(projectId) { + return api.get(`/spools/project/${projectId}/spools`); +} + +export function validateSpoolIdentifier(identifier) { + return api.post('/spools/validate-identifier', { spool_identifier: identifier }); +} + +export function generateSpoolIdentifier(dwgName, areaNumber, spoolNumber) { + return api.post('/spools/generate-identifier', { + dwg_name: dwgName, + area_number: areaNumber, + spool_number: spoolNumber + }); +} \ No newline at end of file diff --git a/frontend/src/components/Dashboard.jsx b/frontend/src/components/Dashboard.jsx index aeff6d6..0c444b2 100644 --- a/frontend/src/components/Dashboard.jsx +++ b/frontend/src/components/Dashboard.jsx @@ -1,38 +1,213 @@ import React, { useState, useEffect } from 'react'; -import { Typography, Box, Card, CardContent, Grid, CircularProgress } from '@mui/material'; +import { + Typography, + Box, + Card, + CardContent, + Grid, + CircularProgress, + Chip, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + FormControl, + InputLabel, + Select, + MenuItem +} from '@mui/material'; +import { fetchMaterials } from '../api'; +import { Bar, Pie, Line } from 'react-chartjs-2'; +import 'chart.js/auto'; +import Toast from './Toast'; function Dashboard({ selectedProject, projects }) { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(false); + const [materials, setMaterials] = useState([]); + const [barData, setBarData] = useState(null); + const [pieData, setPieData] = useState(null); + const [materialGradeData, setMaterialGradeData] = useState(null); + const [sizeData, setSizeData] = useState(null); + const [topMaterials, setTopMaterials] = useState([]); + const [toast, setToast] = useState({ open: false, message: '', type: 'info' }); useEffect(() => { if (selectedProject) { fetchMaterialStats(); + fetchMaterialList(); } }, [selectedProject]); const fetchMaterialStats = async () => { setLoading(true); try { - const response = await fetch(`http://localhost:8000/api/files/materials/summary?project_id=${selectedProject.id}`); + const response = await fetch(`/files/materials/summary?project_id=${selectedProject.id}`); if (response.ok) { const data = await response.json(); setStats(data.summary); } } catch (error) { - console.error('통계 로드 실패:', error); + setToast({ + open: true, + message: '자재 통계 로드 실패', + type: 'error' + }); } finally { setLoading(false); } }; + const fetchMaterialList = async () => { + try { + // 최대 1000개까지 조회(실무에서는 서버 페이징/집계 API 권장) + const params = { project_id: selectedProject.id, skip: 0, limit: 1000 }; + const response = await fetchMaterials(params); + setMaterials(response.data.materials || []); + } catch (error) { + setToast({ + open: true, + message: '자재 목록 로드 실패', + type: 'error' + }); + } + }; + + useEffect(() => { + if (materials.length > 0) { + // 분류별 집계 + const typeCounts = {}; + const typeQuantities = {}; + const materialGrades = {}; + const sizes = {}; + const materialQuantities = {}; + + materials.forEach(mat => { + const type = mat.item_type || 'OTHER'; + const grade = mat.material_grade || '미분류'; + const size = mat.size_spec || '미분류'; + const desc = mat.original_description; + + typeCounts[type] = (typeCounts[type] || 0) + 1; + typeQuantities[type] = (typeQuantities[type] || 0) + (mat.quantity || 0); + materialGrades[grade] = (materialGrades[grade] || 0) + 1; + sizes[size] = (sizes[size] || 0) + 1; + materialQuantities[desc] = (materialQuantities[desc] || 0) + (mat.quantity || 0); + }); + + // Bar 차트 데이터 + setBarData({ + labels: Object.keys(typeCounts), + datasets: [ + { + label: '자재 수', + data: Object.values(typeCounts), + backgroundColor: 'rgba(25, 118, 210, 0.6)', + borderColor: 'rgba(25, 118, 210, 1)', + borderWidth: 1 + }, + { + label: '총 수량', + data: Object.values(typeQuantities), + backgroundColor: 'rgba(220, 0, 78, 0.4)', + borderColor: 'rgba(220, 0, 78, 1)', + borderWidth: 1 + } + ] + }); + + // 재질별 Pie 차트 + setMaterialGradeData({ + labels: Object.keys(materialGrades), + datasets: [{ + data: Object.values(materialGrades), + backgroundColor: [ + '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', + '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF' + ], + borderWidth: 2 + }] + }); + + // 사이즈별 Pie 차트 + setSizeData({ + labels: Object.keys(sizes), + datasets: [{ + data: Object.values(sizes), + backgroundColor: [ + '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', + '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF' + ], + borderWidth: 2 + }] + }); + + // 상위 자재 (수량 기준) + const sortedMaterials = Object.entries(materialQuantities) + .sort(([,a], [,b]) => b - a) + .slice(0, 10) + .map(([desc, qty]) => ({ description: desc, quantity: qty })); + + setTopMaterials(sortedMaterials); + } else { + setBarData(null); + setMaterialGradeData(null); + setSizeData(null); + setTopMaterials([]); + } + }, [materials]); + return ( 📊 대시보드 - + + {/* 선택된 프로젝트 정보 */} + {selectedProject && ( + + + + + + {selectedProject.project_name} ({selectedProject.official_project_code}) + + + 상태: {selectedProject.status} | 생성일: {new Date(selectedProject.created_at).toLocaleDateString()} + + + + + + + + + + + + )} + + {/* 전역 Toast */} + setToast({ open: false, message: '', type: 'info' })} + /> + + {/* 프로젝트 현황 */} @@ -46,81 +221,218 @@ function Dashboard({ selectedProject, projects }) { 총 프로젝트 수 - 선택된 프로젝트: {selectedProject ? selectedProject.project_name : '없음'} + 선택된 프로젝트: {selectedProject.project_name} - - - - - 자재 현황 - - {loading ? ( - - - - ) : stats ? ( - - - {stats.total_items.toLocaleString()} - - - 총 자재 수 - - - 고유 품목: {stats.unique_descriptions}개 - - - 고유 사이즈: {stats.unique_sizes}개 - - - 총 수량: {stats.total_quantity.toLocaleString()} - - - ) : ( - - 프로젝트를 선택하면 자재 현황을 확인할 수 있습니다. - - )} - - - - - {selectedProject && ( - + {/* 자재 현황 */} + - - 📋 프로젝트 상세 정보 + + 자재 현황 - - - 프로젝트 코드 - {selectedProject.official_project_code} - - - 프로젝트명 - {selectedProject.project_name} - - - 상태 - {selectedProject.status} - - - 생성일 - - {new Date(selectedProject.created_at).toLocaleDateString()} + {loading ? ( + + + + ) : stats ? ( + + + {stats.total_items.toLocaleString()} - - + + 총 자재 수 + + + + + + + + + + + + + + + + + 최초 업로드: {stats.earliest_upload ? new Date(stats.earliest_upload).toLocaleString() : '-'} + + + 최신 업로드: {stats.latest_upload ? new Date(stats.latest_upload).toLocaleString() : '-'} + + + ) : ( + + 프로젝트를 선택하면 자재 현황을 확인할 수 있습니다. + + )} - )} - + + {/* 분류별 자재 통계 */} + + + + + 분류별 자재 통계 + + {barData ? ( + + ) : ( + + 자재 데이터가 없습니다. + + )} + + + + + {/* 재질별 분포 */} + + + + + 재질별 분포 + + {materialGradeData ? ( + + ) : ( + + 재질 데이터가 없습니다. + + )} + + + + + {/* 사이즈별 분포 */} + + + + + 사이즈별 분포 + + {sizeData ? ( + + ) : ( + + 사이즈 데이터가 없습니다. + + )} + + + + + {/* 상위 자재 */} + + + + + 상위 자재 (수량 기준) + + {topMaterials.length > 0 ? ( + + + + + 순위 + 자재명 + 총 수량 + + + + {topMaterials.map((material, index) => ( + + {index + 1} + + + {material.description} + + + + + + + ))} + +
+
+ ) : ( + + 자재 데이터가 없습니다. + + )} +
+
+
+ + {/* 프로젝트 상세 정보 */} + {selectedProject && ( + + + + + 📋 프로젝트 상세 정보 + + + + 프로젝트 코드 + {selectedProject.official_project_code} + + + 프로젝트명 + {selectedProject.project_name} + + + 상태 + + + + 생성일 + + {new Date(selectedProject.created_at).toLocaleDateString()} + + + + + + + )} + ); } diff --git a/frontend/src/components/FileManager.jsx b/frontend/src/components/FileManager.jsx new file mode 100644 index 0000000..4f7b039 --- /dev/null +++ b/frontend/src/components/FileManager.jsx @@ -0,0 +1,468 @@ +import React, { useState, useEffect } from 'react'; +import { + Typography, + Box, + Card, + CardContent, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + IconButton, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Chip, + Alert, + CircularProgress, + Grid, + TextField, + FormControl, + InputLabel, + Select, + MenuItem +} from '@mui/material'; +import { + Delete, + Download, + Visibility, + FileUpload, + Warning, + CheckCircle, + Error +} from '@mui/icons-material'; +import { fetchFiles, deleteFile } from '../api'; +import Toast from './Toast'; + +function FileManager({ selectedProject }) { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(false); + const [deleteDialog, setDeleteDialog] = useState({ open: false, file: null }); + const [toast, setToast] = useState({ open: false, message: '', type: 'info' }); + const [filter, setFilter] = useState(''); + const [statusFilter, setStatusFilter] = useState(''); + + useEffect(() => { + if (selectedProject) { + fetchFilesList(); + } else { + setFiles([]); + } + }, [selectedProject]); + + // 파일 업로드 이벤트 리스너 추가 + useEffect(() => { + console.log('FileManager: 이벤트 리스너 등록 시작'); + + const handleFileUploaded = (event) => { + const { jobNo } = event.detail; + console.log('FileManager: 파일 업로드 이벤트 수신:', event.detail); + console.log('FileManager: 현재 선택된 프로젝트:', selectedProject); + + if (selectedProject && selectedProject.job_no === jobNo) { + console.log('FileManager: 파일 업로드 감지됨, 목록 갱신 중...'); + fetchFilesList(); + } else { + console.log('FileManager: job_no 불일치 또는 프로젝트 미선택'); + console.log('이벤트 jobNo:', jobNo); + console.log('선택된 프로젝트 jobNo:', selectedProject?.job_no); + } + }; + + window.addEventListener('fileUploaded', handleFileUploaded); + console.log('FileManager: fileUploaded 이벤트 리스너 등록 완료'); + + return () => { + window.removeEventListener('fileUploaded', handleFileUploaded); + console.log('FileManager: fileUploaded 이벤트 리스너 제거'); + }; + }, [selectedProject]); + + const fetchFilesList = async () => { + setLoading(true); + try { + console.log('FileManager: 파일 목록 조회 시작, job_no:', selectedProject.job_no); + const response = await fetchFiles({ job_no: selectedProject.job_no }); + console.log('FileManager: API 응답:', response.data); + + if (response.data && response.data.files) { + setFiles(response.data.files); + console.log('FileManager: 파일 목록 업데이트 완료, 파일 수:', response.data.files.length); + } else { + console.log('FileManager: 파일 목록이 비어있음'); + setFiles([]); + } + } catch (error) { + console.error('FileManager: 파일 목록 조회 실패:', error); + setToast({ + open: true, + message: '파일 목록을 불러오는데 실패했습니다.', + type: 'error' + }); + } finally { + setLoading(false); + } + }; + + const handleDeleteFile = async () => { + if (!deleteDialog.file) return; + + try { + await deleteFile(deleteDialog.file.id); + setToast({ + open: true, + message: '파일이 성공적으로 삭제되었습니다.', + type: 'success' + }); + setDeleteDialog({ open: false, file: null }); + fetchFilesList(); // 목록 새로고침 + } catch (error) { + console.error('파일 삭제 실패:', error); + setToast({ + open: true, + message: '파일 삭제에 실패했습니다.', + type: 'error' + }); + } + }; + + const getStatusColor = (status) => { + switch (status) { + case 'completed': + return 'success'; + case 'processing': + return 'warning'; + case 'failed': + return 'error'; + default: + return 'default'; + } + }; + + const getStatusIcon = (status) => { + switch (status) { + case 'completed': + return ; + case 'processing': + return ; + case 'failed': + return ; + default: + return null; + } + }; + + const formatFileSize = (bytes) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleString('ko-KR'); + }; + + const filteredFiles = files.filter(file => { + const matchesFilter = !filter || + file.original_filename.toLowerCase().includes(filter.toLowerCase()) || + file.project_name?.toLowerCase().includes(filter.toLowerCase()); + + const matchesStatus = !statusFilter || file.status === statusFilter; + + return matchesFilter && matchesStatus; + }); + + if (!selectedProject) { + return ( + + + 📁 도면 관리 + + + + + + 프로젝트를 선택해주세요 + + + 프로젝트 관리 탭에서 프로젝트를 선택하면 도면을 관리할 수 있습니다. + + + + + ); + } + + return ( + + + 📁 도면 관리 + + + + {selectedProject.project_name} ({selectedProject.official_project_code}) + + + {/* 필터 UI */} + + + + setFilter(e.target.value)} + size="small" + fullWidth + placeholder="파일명 또는 프로젝트명으로 검색" + /> + + + + 상태 + + + + + + + + + + + + + {/* 전역 Toast */} + setToast({ open: false, message: '', type: 'info' })} + /> + + {loading ? ( + + + + + 파일 목록 로딩 중... + + + + ) : filteredFiles.length === 0 ? ( + + + + + {filter || statusFilter ? '검색 결과가 없습니다' : '업로드된 파일이 없습니다'} + + + {filter || statusFilter + ? '다른 검색 조건을 시도해보세요.' + : '파일 업로드 탭에서 BOM 파일을 업로드해주세요.' + } + + {(filter || statusFilter) && ( + + )} + + + ) : ( + + + + + 총 {filteredFiles.length}개 파일 + + + + + + + + + 번호 + 파일명 + 프로젝트 + 상태 + 파일 크기 + 업로드 일시 + 처리 완료 + 작업 + + + + {filteredFiles.map((file, index) => ( + + + {index + 1} + + + + {file.original_filename} + + + + + + + + + + + {formatFileSize(file.file_size || 0)} + + + + + {formatDate(file.created_at)} + + + + + {file.processed_at ? formatDate(file.processed_at) : '-'} + + + + + + + + + + + setDeleteDialog({ open: true, file })} + > + + + + + + ))} + +
+
+
+
+ )} + + {/* 삭제 확인 다이얼로그 */} + setDeleteDialog({ open: false, file: null })} + > + + + + 파일 삭제 확인 + + + + + 다음 파일을 삭제하시겠습니까? + + + + 파일명: {deleteDialog.file?.original_filename} + + + 프로젝트: {deleteDialog.file?.project_name} + + + 업로드 일시: {deleteDialog.file?.created_at ? formatDate(deleteDialog.file.created_at) : '-'} + + + + ⚠️ 이 작업은 되돌릴 수 없습니다. 파일과 관련된 모든 자재 데이터가 함께 삭제됩니다. + + + + + + + +
+ ); +} + +export default FileManager; \ No newline at end of file diff --git a/frontend/src/components/FileUpload.jsx b/frontend/src/components/FileUpload.jsx index dcb69d8..4e45844 100644 --- a/frontend/src/components/FileUpload.jsx +++ b/frontend/src/components/FileUpload.jsx @@ -1,4 +1,5 @@ import React, { useState, useCallback } from 'react'; +import PropTypes from 'prop-types'; import { Typography, Box, @@ -6,38 +7,74 @@ import { CardContent, Button, LinearProgress, - Alert, List, ListItem, ListItemText, ListItemIcon, Chip, Paper, - Divider + Divider, + Stepper, + Step, + StepLabel, + StepContent, + Alert, + Grid } from '@mui/material'; import { CloudUpload, AttachFile, CheckCircle, Error as ErrorIcon, - Description + Description, + AutoAwesome, + Category, + Science } from '@mui/icons-material'; import { useDropzone } from 'react-dropzone'; +import { uploadFile as uploadFileApi, fetchMaterialsSummary } from '../api'; +import Toast from './Toast'; function FileUpload({ selectedProject, onUploadSuccess }) { + console.log('=== FileUpload 컴포넌트 렌더링 ==='); + console.log('selectedProject:', selectedProject); + const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [uploadResult, setUploadResult] = useState(null); const [error, setError] = useState(''); + const [showSuccess, setShowSuccess] = useState(false); + const [showError, setShowError] = useState(false); + const [materialsSummary, setMaterialsSummary] = useState(null); + const [toast, setToast] = useState({ open: false, message: '', type: 'info' }); + const [uploadSteps, setUploadSteps] = useState([ + { label: '파일 업로드', completed: false, active: false }, + { label: '데이터 파싱', completed: false, active: false }, + { label: '자재 분류', completed: false, active: false }, + { label: '분류기 실행', completed: false, active: false }, + { label: '데이터베이스 저장', completed: false, active: false } + ]); const onDrop = useCallback((acceptedFiles) => { + console.log('=== FileUpload: onDrop 함수 호출됨 ==='); + console.log('받은 파일들:', acceptedFiles); + console.log('선택된 프로젝트:', selectedProject); + if (!selectedProject) { - setError('프로젝트를 먼저 선택해주세요.'); + console.log('프로젝트가 선택되지 않음'); + setToast({ + open: true, + message: '프로젝트를 먼저 선택해주세요.', + type: 'warning' + }); return; } if (acceptedFiles.length > 0) { + console.log('파일 업로드 시작'); uploadFile(acceptedFiles[0]); + } else { + console.log('선택된 파일이 없음'); } }, [selectedProject]); @@ -52,57 +89,185 @@ function FileUpload({ selectedProject, onUploadSuccess }) { maxSize: 10 * 1024 * 1024 // 10MB }); + const updateUploadStep = (stepIndex, completed = false, active = false) => { + setUploadSteps(prev => prev.map((step, index) => ({ + ...step, + completed: index < stepIndex ? true : (index === stepIndex ? completed : false), + active: index === stepIndex ? active : false + }))); + }; + const uploadFile = async (file) => { + console.log('=== FileUpload: uploadFile 함수 시작 ==='); + console.log('파일 정보:', { + name: file.name, + size: file.size, + type: file.type + }); + console.log('선택된 프로젝트:', selectedProject); + setUploading(true); setUploadProgress(0); setError(''); setUploadResult(null); + setMaterialsSummary(null); + + console.log('업로드 시작:', { + fileName: file.name, + fileSize: file.size, + jobNo: selectedProject?.job_no, + projectName: selectedProject?.project_name + }); + + // 업로드 단계 초기화 + setUploadSteps([ + { label: '파일 업로드', completed: false, active: true }, + { label: '데이터 파싱', completed: false, active: false }, + { label: '자재 분류', completed: false, active: false }, + { label: '분류기 실행', completed: false, active: false }, + { label: '데이터베이스 저장', completed: false, active: false } + ]); const formData = new FormData(); formData.append('file', file); - formData.append('project_id', selectedProject.id); + formData.append('job_no', selectedProject.job_no); formData.append('revision', 'Rev.0'); - try { - const xhr = new XMLHttpRequest(); + console.log('FormData 내용:', { + fileName: file.name, + jobNo: selectedProject.job_no, + revision: 'Rev.0' + }); - // 업로드 진행률 추적 - xhr.upload.addEventListener('progress', (event) => { - if (event.lengthComputable) { - const progress = Math.round((event.loaded / event.total) * 100); - setUploadProgress(progress); + try { + // 1단계: 파일 업로드 + updateUploadStep(0, true, false); + updateUploadStep(1, false, true); + + console.log('API 호출 시작: /upload'); + const response = await uploadFileApi(formData, { + onUploadProgress: (event) => { + if (event.lengthComputable) { + const progress = Math.round((event.loaded / event.total) * 100); + setUploadProgress(progress); + console.log('업로드 진행률:', progress + '%'); + } } }); - - // Promise로 XMLHttpRequest 래핑 - const uploadPromise = new Promise((resolve, reject) => { - xhr.onload = () => { - if (xhr.status === 200) { - resolve(JSON.parse(xhr.responseText)); - } else { - reject(new Error(`HTTP ${xhr.status}: ${xhr.responseText}`)); - } - }; - xhr.onerror = () => reject(new Error('Network error')); + + console.log('API 응답:', response.data); + const result = response.data; + + console.log('응답 데이터 구조:', { + success: result.success, + file_id: result.file_id, + message: result.message, + hasFileId: 'file_id' in result }); - - xhr.open('POST', 'http://localhost:8000/api/files/upload'); - xhr.send(formData); - - const result = await uploadPromise; + + // 2단계: 데이터 파싱 완료 + updateUploadStep(1, true, false); + updateUploadStep(2, false, true); if (result.success) { + // 3단계: 자재 분류 완료 + updateUploadStep(2, true, false); + updateUploadStep(3, false, true); + + // 4단계: 분류기 실행 완료 + updateUploadStep(3, true, false); + updateUploadStep(4, false, true); + + // 5단계: 데이터베이스 저장 완료 + updateUploadStep(4, true, false); + setUploadResult(result); + setToast({ + open: true, + message: '파일 업로드 및 분류가 성공했습니다!', + type: 'success' + }); + + console.log('업로드 성공 결과:', result); + console.log('파일 ID:', result.file_id); + console.log('선택된 프로젝트:', selectedProject); + + // 업로드 성공 후 자재 통계 미리보기 호출 + try { + const summaryRes = await fetchMaterialsSummary({ file_id: result.file_id }); + if (summaryRes.data && summaryRes.data.success) { + setMaterialsSummary(summaryRes.data.summary); + } + } catch (e) { + // 통계 조회 실패는 무시(UX만) + } + if (onUploadSuccess) { + console.log('onUploadSuccess 콜백 호출'); onUploadSuccess(result); } + + // 파일 목록 갱신을 위한 이벤트 발생 + console.log('파일 업로드 이벤트 발생:', { + fileId: result.file_id, + jobNo: selectedProject.job_no + }); + + try { + window.dispatchEvent(new CustomEvent('fileUploaded', { + detail: { fileId: result.file_id, jobNo: selectedProject.job_no } + })); + console.log('CustomEvent dispatch 성공'); + } catch (error) { + console.error('CustomEvent dispatch 실패:', error); + } } else { - setError(result.message || '업로드에 실패했습니다.'); + setToast({ + open: true, + message: result.message || '업로드에 실패했습니다.', + type: 'error' + }); } - } catch (error) { console.error('업로드 실패:', error); - setError(`업로드 실패: ${error.message}`); + + // 에러 타입별 상세 메시지 + let errorMessage = '업로드에 실패했습니다.'; + + if (error.response) { + // 서버 응답이 있는 경우 + const status = error.response.status; + const data = error.response.data; + + switch (status) { + case 400: + errorMessage = `잘못된 요청: ${data?.detail || '파일 형식이나 데이터를 확인해주세요.'}`; + break; + case 413: + errorMessage = '파일 크기가 너무 큽니다. (최대 10MB)'; + break; + case 422: + errorMessage = `데이터 검증 실패: ${data?.detail || '파일 내용을 확인해주세요.'}`; + break; + case 500: + errorMessage = '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; + break; + default: + errorMessage = `서버 오류 (${status}): ${data?.detail || error.message}`; + } + } else if (error.request) { + // 네트워크 오류 + errorMessage = '서버에 연결할 수 없습니다. 네트워크 연결을 확인해주세요.'; + } else { + // 기타 오류 + errorMessage = `오류 발생: ${error.message}`; + } + + setToast({ + open: true, + message: errorMessage, + type: 'error' + }); } finally { setUploading(false); setUploadProgress(0); @@ -118,30 +283,32 @@ function FileUpload({ selectedProject, onUploadSuccess }) { const resetUpload = () => { setUploadResult(null); - setError(''); setUploadProgress(0); + setUploadSteps([ + { label: '파일 업로드', completed: false, active: false }, + { label: '데이터 파싱', completed: false, active: false }, + { label: '자재 분류', completed: false, active: false }, + { label: '분류기 실행', completed: false, active: false }, + { label: '데이터베이스 저장', completed: false, active: false } + ]); + setToast({ open: false, message: '', type: 'info' }); }; - if (!selectedProject) { - return ( - - - 📁 파일 업로드 - - - - - - 프로젝트를 선택해주세요 - - - 프로젝트 관리 탭에서 프로젝트를 선택한 후 파일을 업로드할 수 있습니다. - - - - - ); - } + const getClassificationStats = () => { + if (!uploadResult?.classification_stats) return null; + + const stats = uploadResult.classification_stats; + const total = Object.values(stats).reduce((sum, count) => sum + count, 0); + + return Object.entries(stats) + .filter(([category, count]) => count > 0) + .map(([category, count]) => ({ + category, + count, + percentage: total > 0 ? Math.round((count / total) * 100) : 0 + })) + .sort((a, b) => b.count - a.count); + }; return ( @@ -153,10 +320,56 @@ function FileUpload({ selectedProject, onUploadSuccess }) { {selectedProject.project_name} ({selectedProject.official_project_code}) - {error && ( - setError('')}> - {error} - + {/* 전역 Toast */} + setToast({ open: false, message: '', type: 'info' })} + /> + + {uploading && ( + + + + + 업로드 및 분류 진행 중... + + + + {uploadSteps.map((step, index) => ( + + + + {step.completed ? ( + + ) : step.active ? ( + + ) : ( + + )} + {step.label} + + + {step.active && ( + + + + )} + + ))} + + + {uploadProgress > 0 && ( + + + 파일 업로드 진행률: {uploadProgress}% + + + + )} + + )} {uploadResult ? ( @@ -165,149 +378,171 @@ function FileUpload({ selectedProject, onUploadSuccess }) { - 업로드 성공! + 업로드 및 분류 성공! - - - - - - - - - - - - - - } - /> - - - - {uploadResult.sample_materials && uploadResult.sample_materials.length > 0 && ( - - - 샘플 자재 (처음 3개): + + + + 📊 업로드 결과 - {uploadResult.sample_materials.map((material, index) => ( - - {index + 1}. {material.original_description} - {material.quantity} {material.unit} - {material.size_spec && ` (${material.size_spec})`} - - ))} - + + + + + + + + + + + + + + + + + + + + + + + + + 🏷️ 분류 결과 + + {getClassificationStats() && ( + + {getClassificationStats().map((stat, index) => ( + + + + {stat.count}개 ({stat.percentage}%) + + + ))} + + )} + + + + {materialsSummary && ( + + + 💡 자재 통계 미리보기:
+ • 총 자재 수: {materialsSummary.total_items || 0}개
+ • 고유 자재: {materialsSummary.unique_descriptions || 0}종류
+ • 총 수량: {materialsSummary.total_quantity || 0}개 +
+
)} - - + ) : ( - - - {uploading ? ( - - - - 파일 업로드 중... - - - - - {uploadProgress}% 완료 - - - - ) : ( - <> - - - - - {isDragActive - ? "파일을 여기에 놓으세요!" - : "Excel 파일을 드래그하거나 클릭하여 선택" - } - - - 지원 형식: .xlsx, .xls, .csv (최대 10MB) - - - + <> + console.log('드래그 앤 드롭 영역 클릭됨')} + sx={{ + p: 4, + textAlign: 'center', + border: 2, + borderStyle: 'dashed', + borderColor: isDragActive ? 'primary.main' : 'grey.300', + bgcolor: isDragActive ? 'primary.50' : 'grey.50', + cursor: 'pointer', + transition: 'all 0.2s ease', + '&:hover': { + borderColor: 'primary.main', + bgcolor: 'primary.50' + } + }} + > + + + + {isDragActive + ? "파일을 여기에 놓으세요!" + : "Excel 파일을 드래그하거나 클릭하여 선택" + } + + + 지원 형식: .xlsx, .xls, .csv (최대 10MB) + + + - - - 💡 업로드 팁: - - - • BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다 - - - • 자재명, 수량, 사이즈, 재질 등이 자동으로 분류됩니다 - - - - )} - - + + + 💡 업로드 및 분류 프로세스: + + + • BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다 + + + • 각 자재는 8가지 분류기(볼트, 플랜지, 피팅, 가스켓, 계기, 파이프, 밸브, 재질)로 자동 분류됩니다 + + + • 분류 결과는 신뢰도 점수와 함께 저장되며, 필요시 수동 검증이 가능합니다 + + + )} ); } +FileUpload.propTypes = { + selectedProject: PropTypes.shape({ + id: PropTypes.string.isRequired, + project_name: PropTypes.string.isRequired, + official_project_code: PropTypes.string.isRequired, + }).isRequired, + onUploadSuccess: PropTypes.func, +}; + export default FileUpload; diff --git a/frontend/src/components/MaterialList.jsx b/frontend/src/components/MaterialList.jsx index 5b2aef4..146874f 100644 --- a/frontend/src/components/MaterialList.jsx +++ b/frontend/src/components/MaterialList.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; import { Typography, Box, @@ -13,48 +14,211 @@ import { Paper, TablePagination, CircularProgress, - Alert, - Chip + Chip, + TextField, + MenuItem, + Select, + InputLabel, + FormControl, + Grid, + IconButton, + Button, + Accordion, + AccordionSummary, + AccordionDetails, + Tabs, + Tab, + Alert } from '@mui/material'; -import { Inventory } from '@mui/icons-material'; +import { + Inventory, + Clear, + ExpandMore, + CompareArrows, + Add, + Remove, + Warning +} from '@mui/icons-material'; +import SearchIcon from '@mui/icons-material/Search'; +import { fetchMaterials as fetchMaterialsApi, fetchJobs } from '../api'; +import { useSearchParams } from 'react-router-dom'; +import Toast from './Toast'; function MaterialList({ selectedProject }) { + const [searchParams, setSearchParams] = useSearchParams(); const [materials, setMaterials] = useState([]); + const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(25); const [totalCount, setTotalCount] = useState(0); + const [search, setSearch] = useState(searchParams.get('search') || ''); + const [searchValue, setSearchValue] = useState(searchParams.get('searchValue') || ''); + const [itemType, setItemType] = useState(searchParams.get('itemType') || ''); + const [materialGrade, setMaterialGrade] = useState(searchParams.get('materialGrade') || ''); + const [sizeSpec, setSizeSpec] = useState(searchParams.get('sizeSpec') || ''); + const [fileFilter, setFileFilter] = useState(searchParams.get('fileFilter') || ''); + const [selectedJob, setSelectedJob] = useState(searchParams.get('jobId') || ''); + const [selectedRevision, setSelectedRevision] = useState(searchParams.get('revision') || ''); + const [groupingType, setGroupingType] = useState(searchParams.get('grouping') || 'item'); + const [sortBy, setSortBy] = useState(searchParams.get('sortBy') || ''); + const [toast, setToast] = useState({ open: false, message: '', type: 'info' }); + const [revisionComparison, setRevisionComparison] = useState(null); + const [files, setFiles] = useState([]); + const [fileId, setFileId] = useState(searchParams.get('file_id') || ''); + const [selectedJobNo, setSelectedJobNo] = useState(searchParams.get('job_no') || ''); + const [selectedFilename, setSelectedFilename] = useState(searchParams.get('filename') || ''); + + // URL 쿼리 파라미터 동기화 + useEffect(() => { + const params = new URLSearchParams(); + if (search) params.set('search', search); + if (searchValue) params.set('searchValue', searchValue); + if (itemType) params.set('itemType', itemType); + if (materialGrade) params.set('materialGrade', materialGrade); + if (sizeSpec) params.set('sizeSpec', sizeSpec); + if (fileFilter) params.set('fileFilter', fileFilter); + if (selectedJob) params.set('jobId', selectedJob); + if (selectedRevision) params.set('revision', selectedRevision); + if (groupingType) params.set('grouping', groupingType); + if (sortBy) params.set('sortBy', sortBy); + if (page > 0) params.set('page', page.toString()); + if (rowsPerPage !== 25) params.set('rowsPerPage', rowsPerPage.toString()); + if (fileId) params.set('file_id', fileId); + if (selectedJobNo) params.set('job_no', selectedJobNo); + if (selectedFilename) params.set('filename', selectedFilename); + if (selectedRevision) params.set('revision', selectedRevision); + + setSearchParams(params); + }, [search, searchValue, itemType, materialGrade, sizeSpec, fileFilter, selectedJob, selectedRevision, groupingType, sortBy, page, rowsPerPage, fileId, setSearchParams, selectedJobNo, selectedFilename, selectedRevision]); + + // URL 파라미터로 진입 시 자동 필터 적용 + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const urlJobNo = urlParams.get('job_no'); + const urlFilename = urlParams.get('filename'); + const urlRevision = urlParams.get('revision'); + if (urlJobNo) setSelectedJobNo(urlJobNo); + if (urlFilename) setSelectedFilename(urlFilename); + if (urlRevision) setSelectedRevision(urlRevision); + }, []); useEffect(() => { if (selectedProject) { + fetchJobsData(); fetchMaterials(); } else { setMaterials([]); setTotalCount(0); + setJobs([]); } - }, [selectedProject, page, rowsPerPage]); + }, [selectedProject, page, rowsPerPage, search, searchValue, itemType, materialGrade, sizeSpec, fileFilter, selectedJob, selectedRevision, groupingType, sortBy, fileId, selectedJobNo, selectedFilename, selectedRevision]); + + // 파일 업로드 이벤트 리스너 추가 + useEffect(() => { + const handleFileUploaded = (event) => { + const { jobNo } = event.detail; + if (selectedProject && selectedProject.job_no === jobNo) { + console.log('파일 업로드 감지됨, 자재 목록 갱신 중...'); + fetchMaterials(); + } + }; + + window.addEventListener('fileUploaded', handleFileUploaded); + + return () => { + window.removeEventListener('fileUploaded', handleFileUploaded); + }; + }, [selectedProject]); + + // 파일 목록 불러오기 (선택된 프로젝트가 바뀔 때마다) + useEffect(() => { + async function fetchFilesForProject() { + if (!selectedProject?.job_no) { + setFiles([]); + return; + } + try { + const response = await fetch(`/files?job_no=${selectedProject.job_no}`); + if (response.ok) { + const data = await response.json(); + if (Array.isArray(data)) setFiles(data); + else if (data && Array.isArray(data.files)) setFiles(data.files); + else setFiles([]); + } else { + setFiles([]); + } + } catch { + setFiles([]); + } + } + fetchFilesForProject(); + }, [selectedProject]); + + const fetchJobsData = async () => { + try { + const response = await fetchJobs({ project_id: selectedProject.id }); + if (response.data && response.data.jobs) { + setJobs(response.data.jobs); + } + } catch (error) { + console.error('Job 조회 실패:', error); + } + }; const fetchMaterials = async () => { setLoading(true); - setError(''); try { const skip = page * rowsPerPage; - const response = await fetch( - `http://localhost:8000/api/files/materials?project_id=${selectedProject.id}&skip=${skip}&limit=${rowsPerPage}` - ); + const params = { + job_no: selectedJobNo || undefined, + filename: selectedFilename || undefined, + revision: selectedRevision || undefined, + skip, + limit: rowsPerPage, + search: search || undefined, + search_value: searchValue || undefined, + item_type: itemType || undefined, + material_grade: materialGrade || undefined, + size_spec: sizeSpec || undefined, + // file_id, fileFilter 등은 사용하지 않음 + grouping: groupingType || undefined, + sort_by: sortBy || undefined + }; - if (response.ok) { - const data = await response.json(); - setMaterials(data.materials || []); - setTotalCount(data.total_count || 0); - } else { - setError('자재 데이터를 불러오는데 실패했습니다.'); + // selectedProject가 없으면 API 호출하지 않음 + if (!selectedProject?.job_no) { + setMaterials([]); + setTotalCount(0); + setLoading(false); + return; + } + + console.log('API 요청 파라미터:', params); // 디버깅용 + + const response = await fetchMaterialsApi(params); + const data = response.data; + + console.log('API 응답:', data); // 디버깅용 + + setMaterials(data.materials || []); + setTotalCount(data.total_count || 0); + + // 리비전 비교 데이터가 있으면 설정 + if (data.revision_comparison) { + setRevisionComparison(data.revision_comparison); } } catch (error) { console.error('자재 조회 실패:', error); - setError('네트워크 오류가 발생했습니다.'); + console.error('에러 상세:', error.response?.data); // 디버깅용 + setToast({ + open: true, + message: `자재 데이터를 불러오는데 실패했습니다: ${error.response?.data?.detail || error.message}`, + type: 'error' + }); + setMaterials([]); + setTotalCount(0); } finally { setLoading(false); } @@ -69,6 +233,20 @@ function MaterialList({ selectedProject }) { setPage(0); }; + const clearFilters = () => { + setSearch(''); + setSearchValue(''); + setItemType(''); + setMaterialGrade(''); + setSizeSpec(''); + setFileFilter(''); + setSelectedJob(''); + setSelectedRevision(''); + setGroupingType('item'); + setSortBy(''); + setPage(0); + }; + const getItemTypeColor = (itemType) => { const colors = { 'PIPE': 'primary', @@ -81,6 +259,18 @@ function MaterialList({ selectedProject }) { return colors[itemType] || 'default'; }; + const getRevisionChangeColor = (change) => { + if (change > 0) return 'success'; + if (change < 0) return 'error'; + return 'default'; + }; + + const getRevisionChangeIcon = (change) => { + if (change > 0) return ; + if (change < 0) return ; + return null; + }; + if (!selectedProject) { return ( @@ -112,12 +302,378 @@ function MaterialList({ selectedProject }) { {selectedProject.project_name} ({selectedProject.official_project_code}) - {error && ( - - {error} + {/* 필터/검색/정렬 UI */} + + + {/* 검색 유형 */} + + + 검색 유형 + + + + + {/* 검색어 입력/선택 */} + + {search === 'project' ? ( + + 프로젝트명 선택 + + + ) : search === 'job' ? ( + + Job No. 선택 + + + ) : search === 'material' || search === 'description' ? ( + setSearchValue(e.target.value)} + size="small" + fullWidth + placeholder="자재명 또는 설명 입력" + InputProps={{ + endAdornment: ( + fetchMaterials()}> + + + ) + }} + /> + ) : search === 'grade' ? ( + setSearchValue(e.target.value)} + size="small" + fullWidth + placeholder="재질 입력 (예: SS316)" + InputProps={{ + endAdornment: ( + fetchMaterials()}> + + + ) + }} + /> + ) : search === 'size' ? ( + setSearchValue(e.target.value)} + size="small" + fullWidth + placeholder="사이즈 입력 (예: 6인치)" + InputProps={{ + endAdornment: ( + fetchMaterials()}> + + + ) + }} + /> + ) : search === 'filename' ? ( + setSearchValue(e.target.value)} + size="small" + fullWidth + placeholder="파일명 입력" + InputProps={{ + endAdornment: ( + fetchMaterials()}> + + + ) + }} + /> + ) : ( + setSearchValue(e.target.value)} + size="small" + fullWidth + placeholder="검색어 입력" + disabled={!search} + InputProps={{ + endAdornment: search && ( + fetchMaterials()}> + + + ) + }} + /> + )} + + + {/* Job No. 선택 */} + + + Job No. + + + + + {/* 도면명(파일명) 선택 */} + + + 도면명(파일명) + + + + + {/* 리비전 선택 */} + + + 리비전 + + + + + {/* 그룹핑 타입 */} + + + 그룹핑 + + + + + {/* 품목 필터 */} + + + 품목 + + + + + {/* 재질 필터 */} + + setMaterialGrade(e.target.value)} + size="small" + fullWidth + placeholder="예: SS316" + /> + + + {/* 사이즈 필터 */} + + setSizeSpec(e.target.value)} + size="small" + fullWidth + placeholder={'예: 6"'} + /> + + + {/* 파일명 필터 */} + + setFileFilter(e.target.value)} + size="small" + fullWidth + placeholder="파일명 검색" + /> + + + {/* 파일(도면) 선택 드롭다운 */} + + + 도면(파일) + + + + + {/* 정렬 */} + + + 정렬 + + + + + {/* 필터 초기화 */} + + + + + + + + + {/* 전역 Toast */} + setToast({ open: false, message: '', type: 'info' })} + /> + + {/* 리비전 비교 알림 */} + {revisionComparison && ( + + + 리비전 비교: {revisionComparison.summary} + )} + {/* 필터 상태 표시 */} + {(search || searchValue || itemType || materialGrade || sizeSpec || fileFilter || selectedJobNo || selectedFilename || selectedRevision || groupingType !== 'item' || sortBy) && ( + + + 필터 적용 중: + {search && ` 검색 유형: ${search}`} + {searchValue && ` 검색어: "${searchValue}"`} + {selectedJobNo && ` Job No: ${selectedJobNo}`} + {selectedFilename && ` 파일: ${selectedFilename}`} + {selectedRevision && ` 리비전: ${selectedRevision}`} + {groupingType !== 'item' && ` 그룹핑: ${groupingType}`} + {itemType && ` 품목: ${itemType}`} + {materialGrade && ` 재질: ${materialGrade}`} + {sizeSpec && ` 사이즈: ${sizeSpec}`} + {fileFilter && ` 파일: ${fileFilter}`} + {sortBy && ` 정렬: ${sortBy}`} + + + )} + {loading ? ( @@ -132,11 +688,19 @@ function MaterialList({ selectedProject }) { - 자재 데이터가 없습니다 + {search || searchValue || itemType || materialGrade || sizeSpec || fileFilter || selectedJobNo || selectedFilename || selectedRevision ? '검색 결과가 없습니다' : '자재 데이터가 없습니다'} - - 파일 업로드 탭에서 BOM 파일을 업로드해주세요. + + {search || searchValue || itemType || materialGrade || sizeSpec || fileFilter || selectedJobNo || selectedFilename || selectedRevision + ? '다른 검색 조건을 시도해보세요.' + : '파일 업로드 탭에서 BOM 파일을 업로드해주세요.' + } + {(search || searchValue || itemType || materialGrade || sizeSpec || fileFilter || selectedJobNo || selectedFilename || selectedRevision) && ( + + )} ) : ( @@ -164,13 +728,16 @@ function MaterialList({ selectedProject }) { 단위 사이즈 재질 + Job No. + 리비전 + 변경 라인 수 {materials.map((material, index) => ( @@ -208,6 +775,30 @@ function MaterialList({ selectedProject }) { {material.material_grade || '-'} + + + + + + + + {material.quantity_change && ( + 0 ? '+' : ''}${material.quantity_change}`} + size="small" + color={getRevisionChangeColor(material.quantity_change)} + icon={getRevisionChangeIcon(material.quantity_change)} + /> + )} + { if (!projectCode.trim() || !projectName.trim()) { - setError('프로젝트 코드와 이름을 모두 입력해주세요.'); + setToast({ + open: true, + message: '프로젝트 코드와 이름을 모두 입력해주세요.', + type: 'warning' + }); return; } setLoading(true); - setError(''); try { - const response = await fetch('http://localhost:8000/api/projects', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ - official_project_code: projectCode.trim(), - project_name: projectName.trim(), - design_project_code: projectCode.trim(), - is_code_matched: true, - status: 'active' - }) - }); - - if (response.ok) { - const newProject = await response.json(); + const data = { + official_project_code: projectCode.trim(), + project_name: projectName.trim(), + design_project_code: projectCode.trim(), + is_code_matched: true, + status: 'active' + }; + const response = await createJob(data); + const result = response.data; + if (result && result.job) { onProjectsChange(); - setSelectedProject(newProject); + setSelectedProject(result.job); setDialogOpen(false); setProjectCode(''); setProjectName(''); - setError(''); + setToast({ + open: true, + message: '프로젝트가 성공적으로 생성되었습니다.', + type: 'success' + }); } else { - const errorData = await response.json(); - setError(errorData.detail || '프로젝트 생성에 실패했습니다.'); + setToast({ + open: true, + message: result.message || '프로젝트 생성에 실패했습니다.', + type: 'error' + }); } } catch (error) { console.error('프로젝트 생성 실패:', error); - setError('네트워크 오류가 발생했습니다. 백엔드 서버가 실행 중인지 확인해주세요.'); + setToast({ + open: true, + message: '네트워크 오류가 발생했습니다. 백엔드 서버가 실행 중인지 확인해주세요.', + type: 'error' + }); } finally { setLoading(false); } }; + const handleEditProject = async () => { + if (!editingProject || !editingProject.project_name.trim()) { + setToast({ + open: true, + message: '프로젝트명을 입력해주세요.', + type: 'warning' + }); + return; + } + + setLoading(true); + + try { + const response = await updateProject(editingProject.id, { + project_name: editingProject.project_name.trim(), + status: editingProject.status + }); + + if (response.data && response.data.success) { + onProjectsChange(); + setEditDialogOpen(false); + setEditingProject(null); + setToast({ + open: true, + message: '프로젝트가 성공적으로 수정되었습니다.', + type: 'success' + }); + } else { + setToast({ + open: true, + message: response.data?.message || '프로젝트 수정에 실패했습니다.', + type: 'error' + }); + } + } catch (error) { + console.error('프로젝트 수정 실패:', error); + setToast({ + open: true, + message: '프로젝트 수정 중 오류가 발생했습니다.', + type: 'error' + }); + } finally { + setLoading(false); + } + }; + + const handleDeleteProject = async (project) => { + if (!window.confirm(`정말로 프로젝트 "${project.project_name}"을 삭제하시겠습니까?`)) { + return; + } + + setLoading(true); + + try { + const response = await deleteProject(project.id); + + if (response.data && response.data.success) { + onProjectsChange(); + if (selectedProject?.id === project.id) { + setSelectedProject(null); + } + setToast({ + open: true, + message: '프로젝트가 성공적으로 삭제되었습니다.', + type: 'success' + }); + } else { + setToast({ + open: true, + message: response.data?.message || '프로젝트 삭제에 실패했습니다.', + type: 'error' + }); + } + } catch (error) { + console.error('프로젝트 삭제 실패:', error); + setToast({ + open: true, + message: '프로젝트 삭제 중 오류가 발생했습니다.', + type: 'error' + }); + } finally { + setLoading(false); + } + }; + + const handleOpenMenu = (event, project) => { + setMenuAnchor(event.currentTarget); + setSelectedProjectForMenu(project); + }; + + const handleCloseMenu = () => { + setMenuAnchor(null); + setSelectedProjectForMenu(null); + }; + + const handleEditClick = () => { + setEditingProject({ ...selectedProjectForMenu }); + setEditDialogOpen(true); + handleCloseMenu(); + }; + + const handleDetailClick = () => { + setDetailDialogOpen(true); + handleCloseMenu(); + }; + const handleCloseDialog = () => { setDialogOpen(false); setProjectCode(''); setProjectName(''); - setError(''); + }; + + const handleCloseEditDialog = () => { + setEditDialogOpen(false); + setEditingProject(null); + }; + + const getStatusColor = (status) => { + switch (status) { + case 'active': return 'success'; + case 'inactive': return 'warning'; + case 'completed': return 'info'; + default: return 'default'; + } + }; + + const getStatusIcon = (status) => { + switch (status) { + case 'active': return ; + case 'inactive': return ; + default: return ; + } }; return ( @@ -89,6 +250,14 @@ function ProjectManager({ projects, selectedProject, setSelectedProject, onProje + {/* 전역 Toast */} + setToast({ open: false, message: '', type: 'info' })} + /> + {projects.length === 0 ? ( @@ -109,42 +278,89 @@ function ProjectManager({ projects, selectedProject, setSelectedProject, onProje ) : ( - + {projects.map((project) => ( - setSelectedProject(project)} - > - - - {project.project_name || project.official_project_code} - - - 코드: {project.official_project_code} - - - 상태: {project.status} | 생성일: {new Date(project.created_at).toLocaleDateString()} - - - + + setSelectedProject(project)} + > + + + + + {project.project_name || project.official_project_code} + + + 코드: {project.official_project_code} + + + + 생성일: {new Date(project.created_at).toLocaleDateString()} + + + { + e.stopPropagation(); + handleOpenMenu(e, project); + }} + > + + + + + + ))} - + )} + {/* 프로젝트 메뉴 */} + + + + 상세 보기 + + + + 수정 + + + { + handleDeleteProject(selectedProjectForMenu); + handleCloseMenu(); + }} + sx={{ color: 'error.main' }} + > + + 삭제 + + + + {/* 새 프로젝트 생성 다이얼로그 */} 새 프로젝트 생성 - {error && ( - - {error} - - )} + + {/* 프로젝트 수정 다이얼로그 */} + + 프로젝트 수정 + + + setEditingProject({ + ...editingProject, + project_name: e.target.value + })} + sx={{ mb: 2 }} + /> + setEditingProject({ + ...editingProject, + status: e.target.value + })} + > + 활성 + 비활성 + 완료 + + + + + + + + + {/* 프로젝트 상세 보기 다이얼로그 */} + setDetailDialogOpen(false)} maxWidth="md" fullWidth> + 프로젝트 상세 정보 + + {selectedProjectForMenu && ( + + + 프로젝트 코드 + + {selectedProjectForMenu.official_project_code} + + + + 프로젝트명 + + {selectedProjectForMenu.project_name} + + + + 상태 + + + + 생성일 + + {new Date(selectedProjectForMenu.created_at).toLocaleString()} + + + + 설계 프로젝트 코드 + + {selectedProjectForMenu.design_project_code || '-'} + + + + 코드 매칭 + + + + )} + + + + + ); } diff --git a/frontend/src/components/SpoolManager.jsx b/frontend/src/components/SpoolManager.jsx new file mode 100644 index 0000000..3715af6 --- /dev/null +++ b/frontend/src/components/SpoolManager.jsx @@ -0,0 +1,426 @@ +import React, { useState, useEffect } from 'react'; +import { + Typography, + Box, + Card, + CardContent, + Button, + TextField, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Grid, + Chip, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + CircularProgress, + IconButton, + FormControl, + InputLabel, + Select, + MenuItem, + Alert +} from '@mui/material'; +import { + Add, + Build, + CheckCircle, + Error, + Visibility, + Edit, + Delete, + MoreVert +} from '@mui/icons-material'; +import { fetchProjectSpools, validateSpoolIdentifier, generateSpoolIdentifier } from '../api'; +import Toast from './Toast'; + +function SpoolManager({ selectedProject }) { + const [spools, setSpools] = useState([]); + const [loading, setLoading] = useState(false); + const [dialogOpen, setDialogOpen] = useState(false); + const [validationDialogOpen, setValidationDialogOpen] = useState(false); + const [toast, setToast] = useState({ open: false, message: '', type: 'info' }); + + // 스풀 생성 폼 상태 + const [newSpool, setNewSpool] = useState({ + dwg_name: '', + area_number: '', + spool_number: '' + }); + + // 유효성 검증 상태 + const [validationResult, setValidationResult] = useState(null); + const [validating, setValidating] = useState(false); + + useEffect(() => { + if (selectedProject) { + fetchSpools(); + } else { + setSpools([]); + } + }, [selectedProject]); + + const fetchSpools = async () => { + if (!selectedProject) return; + + setLoading(true); + try { + const response = await fetchProjectSpools(selectedProject.id); + if (response.data && response.data.spools) { + setSpools(response.data.spools); + } + } catch (error) { + console.error('스풀 조회 실패:', error); + setToast({ + open: true, + message: '스풀 데이터를 불러오는데 실패했습니다.', + type: 'error' + }); + } finally { + setLoading(false); + } + }; + + const handleCreateSpool = async () => { + if (!newSpool.dwg_name || !newSpool.area_number || !newSpool.spool_number) { + setToast({ + open: true, + message: '도면명, 에리어 번호, 스풀 번호를 모두 입력해주세요.', + type: 'warning' + }); + return; + } + + setLoading(true); + try { + const response = await generateSpoolIdentifier( + newSpool.dwg_name, + newSpool.area_number, + newSpool.spool_number + ); + + if (response.data && response.data.success) { + setToast({ + open: true, + message: '스풀이 성공적으로 생성되었습니다.', + type: 'success' + }); + setDialogOpen(false); + setNewSpool({ dwg_name: '', area_number: '', spool_number: '' }); + fetchSpools(); // 목록 새로고침 + } else { + setToast({ + open: true, + message: response.data?.message || '스풀 생성에 실패했습니다.', + type: 'error' + }); + } + } catch (error) { + console.error('스풀 생성 실패:', error); + setToast({ + open: true, + message: '스풀 생성 중 오류가 발생했습니다.', + type: 'error' + }); + } finally { + setLoading(false); + } + }; + + const handleValidateSpool = async (identifier) => { + setValidating(true); + try { + const response = await validateSpoolIdentifier(identifier); + setValidationResult(response.data); + setValidationDialogOpen(true); + } catch (error) { + console.error('스풀 유효성 검증 실패:', error); + setToast({ + open: true, + message: '스풀 유효성 검증에 실패했습니다.', + type: 'error' + }); + } finally { + setValidating(false); + } + }; + + const getStatusColor = (status) => { + switch (status) { + case 'active': return 'success'; + case 'inactive': return 'warning'; + case 'completed': return 'info'; + default: return 'default'; + } + }; + + if (!selectedProject) { + return ( + + + 🔧 스풀 관리 + + + + + + 프로젝트를 선택해주세요 + + + 프로젝트 관리 탭에서 프로젝트를 선택하면 스풀을 관리할 수 있습니다. + + + + + ); + } + + return ( + + + + 🔧 스풀 관리 + + + + + + {selectedProject.project_name} ({selectedProject.official_project_code}) + + + {/* 전역 Toast */} + setToast({ open: false, message: '', type: 'info' })} + /> + + {loading ? ( + + + + + 스풀 데이터 로딩 중... + + + + ) : spools.length === 0 ? ( + + + + + 스풀이 없습니다 + + + 새 스풀을 생성하여 시작하세요! + + + + + ) : ( + + + + + 총 {spools.length}개 스풀 + + + + + + + + + 스풀 식별자 + 도면명 + 에리어 + 스풀 번호 + 자재 수 + 총 수량 + 상태 + 작업 + + + + {spools.map((spool) => ( + + + + {spool.spool_identifier} + + + {spool.dwg_name} + {spool.area_number} + {spool.spool_number} + + + + + + + + + + + handleValidateSpool(spool.spool_identifier)} + disabled={validating} + > + + + + + ))} + +
+
+
+
+ )} + + {/* 새 스풀 생성 다이얼로그 */} + setDialogOpen(false)} maxWidth="sm" fullWidth> + 새 스풀 생성 + + setNewSpool({ ...newSpool, dwg_name: e.target.value })} + sx={{ mb: 2 }} + /> + setNewSpool({ ...newSpool, area_number: e.target.value })} + sx={{ mb: 2 }} + /> + setNewSpool({ ...newSpool, spool_number: e.target.value })} + /> + + + + + + + + {/* 스풀 유효성 검증 결과 다이얼로그 */} + setValidationDialogOpen(false)} maxWidth="md" fullWidth> + 스풀 유효성 검증 결과 + + {validationResult && ( + + + {validationResult.validation.is_valid ? '유효한 스풀 식별자입니다.' : '유효하지 않은 스풀 식별자입니다.'} + + + + + 스풀 식별자 + + {validationResult.spool_identifier} + + + + 검증 시간 + + {new Date(validationResult.timestamp).toLocaleString()} + + + + 검증 세부사항 + + {validationResult.validation.details && + Object.entries(validationResult.validation.details).map(([key, value]) => ( + + )) + } + + + + + )} + + + + + +
+ ); +} + +export default SpoolManager; \ No newline at end of file diff --git a/frontend/src/components/Toast.jsx b/frontend/src/components/Toast.jsx new file mode 100644 index 0000000..de9916e --- /dev/null +++ b/frontend/src/components/Toast.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Snackbar, Alert, AlertTitle } from '@mui/material'; +import { + CheckCircle, + Error, + Warning, + Info +} from '@mui/icons-material'; + +const Toast = React.memo(({ + open, + message, + type = 'info', + title, + autoHideDuration = 4000, + onClose, + anchorOrigin = { vertical: 'top', horizontal: 'center' } +}) => { + const getSeverity = () => { + switch (type) { + case 'success': return 'success'; + case 'error': return 'error'; + case 'warning': return 'warning'; + case 'info': + default: return 'info'; + } + }; + + const getIcon = () => { + switch (type) { + case 'success': return ; + case 'error': return ; + case 'warning': return ; + case 'info': + default: return ; + } + }; + + return ( + + + {title && {title}} + {message} + + + ); +}); + +Toast.propTypes = { + open: PropTypes.bool.isRequired, + message: PropTypes.string.isRequired, + type: PropTypes.oneOf(['success', 'error', 'warning', 'info']), + title: PropTypes.string, + autoHideDuration: PropTypes.number, + onClose: PropTypes.func, + anchorOrigin: PropTypes.object, +}; + +export default Toast; \ No newline at end of file diff --git a/frontend/src/pages/BOMManagerPage.jsx b/frontend/src/pages/BOMManagerPage.jsx new file mode 100644 index 0000000..6e75592 --- /dev/null +++ b/frontend/src/pages/BOMManagerPage.jsx @@ -0,0 +1,219 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Typography, Button, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, TextField, CircularProgress, Alert, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { fetchFiles, uploadFile, deleteFile } from '../api'; + +const BOMManagerPage = () => { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [uploading, setUploading] = useState(false); + const [file, setFile] = useState(null); + const [filename, setFilename] = useState(''); + const [revisionDialogOpen, setRevisionDialogOpen] = useState(false); + const [revisionTarget, setRevisionTarget] = useState(null); + const [revisionFile, setRevisionFile] = useState(null); + const [searchParams] = useSearchParams(); + const jobNo = searchParams.get('job_no'); + const jobName = searchParams.get('job_name'); + const navigate = useNavigate(); + + // 파일 목록 불러오기 + const loadFiles = async () => { + setLoading(true); + setError(''); + try { + const response = await fetchFiles({ job_no: jobNo }); + if (Array.isArray(response.data)) { + setFiles(response.data); + } else { + setFiles([]); + } + } catch (e) { + setError('파일 목록을 불러오지 못했습니다.'); + console.error('파일 목록 로드 에러:', e); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (jobNo) loadFiles(); + // eslint-disable-next-line + }, [jobNo]); + + // 파일 업로드 핸들러 + const handleUpload = async (e) => { + e.preventDefault(); + if (!file || !filename) return; + setUploading(true); + setError(''); + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('job_no', jobNo); + formData.append('bom_name', filename); + + const response = await uploadFile(formData); + if (response.data.success) { + setFile(null); + setFilename(''); + loadFiles(); // 파일 목록 새로고침 + alert(`업로드 성공: ${response.data.materials_count}개 자재가 분류되었습니다.`); + } else { + throw new Error(response.data.error || '업로드 실패'); + } + } catch (e) { + setError(`파일 업로드에 실패했습니다: ${e.message}`); + console.error('업로드 에러:', e); + } finally { + setUploading(false); + } + }; + + // 리비전 업로드 핸들러 + const handleRevisionUpload = async () => { + if (!revisionFile || !revisionTarget) return; + setUploading(true); + setError(''); + try { + const formData = new FormData(); + formData.append('file', revisionFile); + formData.append('job_no', jobNo); + formData.append('bom_name', revisionTarget.original_filename); + formData.append('parent_bom_id', revisionTarget.id); + + const response = await uploadFile(formData); + if (response.data.success) { + setRevisionDialogOpen(false); + setRevisionFile(null); + setRevisionTarget(null); + loadFiles(); + alert(`리비전 업로드 성공: ${response.data.revision}`); + } else { + throw new Error(response.data.error || '리비전 업로드 실패'); + } + } catch (e) { + setError(`리비전 업로드에 실패했습니다: ${e.message}`); + console.error('리비전 업로드 에러:', e); + } finally { + setUploading(false); + } + }; + + // 파일 삭제 핸들러 + const handleDelete = async (fileId, filename) => { + if (!confirm(`정말로 "${filename}" 파일을 삭제하시겠습니까?`)) return; + + try { + const response = await deleteFile(fileId); + if (response.data.success) { + loadFiles(); + alert('파일이 삭제되었습니다.'); + } else { + throw new Error(response.data.error || '삭제 실패'); + } + } catch (e) { + setError(`파일 삭제에 실패했습니다: ${e.message}`); + console.error('삭제 에러:', e); + } + }; + + // 자재확인 페이지로 이동 + const handleViewMaterials = (file) => { + navigate(`/materials?file_id=${file.id}&job_no=${jobNo}&filename=${encodeURIComponent(file.original_filename)}`); + }; + + return ( + + + + {jobNo && jobName && `${jobNo} (${jobName})`} + + {/* BOM 업로드 폼 */} +
+ setFilename(e.target.value)} + size="small" + required + sx={{ minWidth: 220 }} + /> + setFile(e.target.files[0])} + disabled={uploading} + style={{ marginRight: 8 }} + /> + + + {error && {error}} + {loading && } + {/* 파일 목록 리스트 */} + + + + + 도면명 + 리비전 + 세부내역 + 리비전 + 삭제 + + + + {files.map(file => ( + + {file.original_filename} + {file.revision} + + + + + + + + + + + ))} + +
+
+ {/* 리비전 업로드 다이얼로그 */} + setRevisionDialogOpen(false)}> + 리비전 업로드 + + + 도면명: {revisionTarget?.original_filename} + + setRevisionFile(e.target.files[0])} + /> + + + + + + +
+ ); +}; + +export default BOMManagerPage; \ No newline at end of file diff --git a/frontend/src/pages/BOMStatusPage.jsx b/frontend/src/pages/BOMStatusPage.jsx new file mode 100644 index 0000000..5f448c0 --- /dev/null +++ b/frontend/src/pages/BOMStatusPage.jsx @@ -0,0 +1,124 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Typography, Button, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, CircularProgress, Alert } from '@mui/material'; +import { useSearchParams, useNavigate } from 'react-router-dom'; + +const BOMStatusPage = () => { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [uploading, setUploading] = useState(false); + const [file, setFile] = useState(null); + const [searchParams] = useSearchParams(); + const jobNo = searchParams.get('job_no'); + const navigate = useNavigate(); + + // 파일 목록 불러오기 + const fetchFiles = async () => { + setLoading(true); + setError(''); + try { + let url = '/files'; + if (jobNo) { + url += `?job_no=${jobNo}`; + } + const res = await fetch(url); + const data = await res.json(); + if (Array.isArray(data)) setFiles(data); + else if (data && Array.isArray(data.files)) setFiles(data.files); + else setFiles([]); + } catch (e) { + setError('파일 목록을 불러오지 못했습니다.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchFiles(); + // eslint-disable-next-line + }, [jobNo]); + + // 파일 업로드 핸들러 + const handleUpload = async (e) => { + e.preventDefault(); + if (!file) return; + setUploading(true); + setError(''); + try { + const formData = new FormData(); + formData.append('file', file); + formData.append('project_id', 1); // 예시: 실제 프로젝트 ID로 대체 필요 + const res = await fetch('/files/upload', { + method: 'POST', + body: formData + }); + if (!res.ok) throw new Error('업로드 실패'); + setFile(null); + fetchFiles(); + } catch (e) { + setError('파일 업로드에 실패했습니다.'); + } finally { + setUploading(false); + } + }; + + return ( + + + BOM 업로드 및 현황 +
+ setFile(e.target.files[0])} + disabled={uploading} + /> + +
+ {error && {error}} + {loading && } + + + + + 파일명 + 리비전 + 세부내역 + 리비전 + 삭제 + + + + {files.map(file => ( + + {file.original_filename || file.filename} + {file.revision} + + + + + + + + + + + ))} + +
+
+
+ ); +}; + +export default BOMStatusPage; \ No newline at end of file diff --git a/frontend/src/pages/FileUploadPage.jsx b/frontend/src/pages/FileUploadPage.jsx deleted file mode 100644 index d195533..0000000 --- a/frontend/src/pages/FileUploadPage.jsx +++ /dev/null @@ -1,455 +0,0 @@ -import React, { useState, useCallback, useEffect } from 'react'; -import { Upload, FileText, AlertCircle, CheckCircle, Loader2, Database, TrendingUp, Settings, Eye, BarChart3, Filter } from 'lucide-react'; - -const FileUploadPage = () => { - const [uploadStatus, setUploadStatus] = useState('idle'); // idle, uploading, analyzing, classifying, success, error - const [uploadProgress, setUploadProgress] = useState(0); - const [uploadResult, setUploadResult] = useState(null); - const [dragActive, setDragActive] = useState(false); - const [selectedProject, setSelectedProject] = useState(''); - const [projects, setProjects] = useState([]); - const [analysisStep, setAnalysisStep] = useState(''); - const [classificationPreview, setClassificationPreview] = useState(null); - - // 프로젝트 목록 로드 - useEffect(() => { - fetchProjects(); - }, []); - - const fetchProjects = async () => { - try { - const response = await fetch('http://localhost:8000/api/projects'); - const data = await response.json(); - setProjects(data); - if (data.length > 0) { - setSelectedProject(data[0].id.toString()); - } - } catch (error) { - console.error('프로젝트 로드 실패:', error); - } - }; - - // 드래그 앤 드롭 핸들러 - const handleDrag = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - if (e.type === "dragenter" || e.type === "dragover") { - setDragActive(true); - } else if (e.type === "dragleave") { - setDragActive(false); - } - }, []); - - const handleDrop = useCallback((e) => { - e.preventDefault(); - e.stopPropagation(); - setDragActive(false); - - if (e.dataTransfer.files && e.dataTransfer.files[0]) { - handleFileUpload(e.dataTransfer.files[0]); - } - }, []); - - // 파일 업로드 및 4단계 자동 분류 처리 - const handleFileUpload = async (file) => { - if (!file) return; - if (!selectedProject) { - alert('프로젝트를 먼저 선택해주세요.'); - return; - } - - // 파일 타입 체크 (다양한 형식 지원) - const allowedTypes = [ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx - 'application/vnd.ms-excel', // .xls - 'application/vnd.ms-excel.sheet.macroEnabled.12', // .xlsm - 'text/csv', - 'text/plain' - ]; - - if (!allowedTypes.includes(file.type) && !file.name.match(/\.(xlsx|xls|xlsm|csv|txt)$/i)) { - alert('지원 형식: 엑셀(.xlsx, .xls, .xlsm), CSV, 텍스트 파일만 업로드 가능합니다.'); - return; - } - - setUploadStatus('uploading'); - setUploadProgress(0); - setAnalysisStep('파일 업로드 중...'); - - try { - const formData = new FormData(); - formData.append('file', file); - formData.append('project_id', selectedProject); - formData.append('revision', 'Rev.0'); - formData.append('description', `${file.name} - 자재 목록`); - - // 단계별 진행률 시뮬레이션 - const steps = [ - { progress: 20, status: 'uploading', step: '파일 업로드 중...' }, - { progress: 40, status: 'analyzing', step: '자동 구조 인식 중... (컬럼 분석)' }, - { progress: 60, status: 'classifying', step: '4단계 자동 분류 진행 중...' }, - { progress: 80, status: 'classifying', step: '재질 코드 및 사이즈 표준화...' }, - { progress: 90, status: 'classifying', step: 'DB 저장 중...' } - ]; - - let stepIndex = 0; - const progressInterval = setInterval(() => { - if (stepIndex < steps.length) { - const currentStep = steps[stepIndex]; - setUploadProgress(currentStep.progress); - setUploadStatus(currentStep.status); - setAnalysisStep(currentStep.step); - stepIndex++; - } else { - clearInterval(progressInterval); - } - }, 800); - - const response = await fetch('http://localhost:8000/api/files/upload', { - method: 'POST', - body: formData, - }); - - clearInterval(progressInterval); - setUploadProgress(100); - - if (response.ok) { - const result = await response.json(); - setUploadResult(result); - setUploadStatus('success'); - setAnalysisStep('분류 완료! 결과를 확인하세요.'); - - // 분류 결과 미리보기 생성 - setClassificationPreview({ - totalItems: result.parsed_count || 0, - categories: { - '파이프': Math.floor((result.parsed_count || 0) * 0.4), - '피팅류': Math.floor((result.parsed_count || 0) * 0.3), - '볼트(너트)': Math.floor((result.parsed_count || 0) * 0.15), - '밸브': Math.floor((result.parsed_count || 0) * 0.1), - '계기류': Math.floor((result.parsed_count || 0) * 0.05) - }, - materials: ['A333-6 (저온용 배관)', 'A105 (단조 탄소강)', 'S355', 'SM490'], - sizes: ['1"', '2"', '3"', '4"', '6"', '8"'] - }); - - } else { - throw new Error('업로드 실패'); - } - } catch (error) { - console.error('Upload error:', error); - setUploadStatus('error'); - setAnalysisStep('처리 중 오류가 발생했습니다.'); - setTimeout(() => { - setUploadStatus('idle'); - setUploadProgress(0); - setAnalysisStep(''); - }, 3000); - } - }; - - const handleFileInput = (e) => { - if (e.target.files && e.target.files[0]) { - handleFileUpload(e.target.files[0]); - } - }; - - const resetUpload = () => { - setUploadStatus('idle'); - setUploadProgress(0); - setUploadResult(null); - setAnalysisStep(''); - setClassificationPreview(null); - }; - - return ( -
-
- {/* 헤더 */} -
-

- 🏗️ 도면 자재 분석 시스템 -

-

- Phase 1: 파일 분석 → 4단계 자동 분류 → 체계적 DB 저장 -

-
- - {/* Phase 1 핵심 프로세스 플로우 */} -
-

🎯 Phase 1: 핵심 기능 처리 과정

-
-
-
- -
- 다양한 형식 - xlsx,xls,xlsm,csv -
-
-
- -
- 자동 구조 인식 - 컬럼 자동 판별 -
-
-
- -
- 4단계 자동 분류 - 대분류→세부→재질→사이즈 -
-
-
- -
- 체계적 저장 - 버전관리+이력추적 -
-
-
- -
- {/* 왼쪽: 업로드 영역 */} -
- {/* 프로젝트 선택 */} -
-

📋 프로젝트 선택

- -
- - {/* 업로드 영역 */} -
-
- - - {uploadStatus === 'idle' && ( - <> - -

- 자재 목록 파일을 업로드하세요 -

-

- 드래그 앤 드롭하거나 클릭하여 파일을 선택하세요 -

-
- 지원 형식: Excel (.xlsx, .xls, .xlsm), CSV, 텍스트 -
- - - )} - - {(uploadStatus === 'uploading' || uploadStatus === 'analyzing' || uploadStatus === 'classifying') && ( -
- -

- {analysisStep} -

-
-
-
-

{uploadProgress}% 완료

-
- )} - - {uploadStatus === 'success' && ( -
- -

- 분석 및 분류 완료! -

-

{analysisStep}

-
- - -
-
- )} - - {uploadStatus === 'error' && ( -
- -

- 업로드 실패 -

-

{analysisStep}

- -
- )} -
-
-
- - {/* 오른쪽: 4단계 분류 시스템 설명 & 미리보기 */} -
- {/* 4단계 분류 시스템 */} -
-

- - 4단계 자동 분류 시스템 -

-
-
-

1단계: 대분류

-

파이프 / 피팅류 / 볼트(너트) / 밸브 / 계기류

-
-
-

2단계: 세부분류

-

90도 엘보우 / 용접목 플랜지 / SEAMLESS 파이프

-
-
-

3단계: 재질 인식

-

A333-6 (저온용 배관) / A105 (단조 탄소강) / S355 / SM490

-
-
-

4단계: 사이즈 표준화

-

6.0" → 6인치, 규격 통일 및 단위 자동 결정

-
-
-
- - {/* 분류 결과 미리보기 */} - {classificationPreview && ( -
-

- - 분류 결과 미리보기 -

-
-
-
{classificationPreview.totalItems}
-
총 자재 수
-
- -
-

대분류별 분포

-
- {Object.entries(classificationPreview.categories).map(([category, count]) => ( -
- {category} - - {count}개 - -
- ))} -
-
- -
-

인식된 재질

-
- {classificationPreview.materials.map((material, index) => ( - - {material} - - ))} -
-
- -
-

표준화된 사이즈

-
- {classificationPreview.sizes.map((size, index) => ( - - {size} - - ))} -
-
-
-
- )} - - {/* 데이터베이스 저장 정보 */} -
-

- - 체계적 DB 저장 -

-
-
-
- 프로젝트 단위 관리 (코드 체계) -
-
-
- 버전 관리 (Rev.0, Rev.1, Rev.2) -
-
-
- 파일 업로드 이력 추적 -
-
-
- 분류 결과 + 원본 정보 보존 -
-
-
- 수량 정보 세분화 저장 -
-
-
-
-
-
-
- ); -}; - -export default FileUploadPage; diff --git a/frontend/src/pages/JobSelectionPage.jsx b/frontend/src/pages/JobSelectionPage.jsx new file mode 100644 index 0000000..f0601d8 --- /dev/null +++ b/frontend/src/pages/JobSelectionPage.jsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Typography, FormControl, InputLabel, Select, MenuItem, Button, CircularProgress, Alert } from '@mui/material'; +import { fetchJobs } from '../api'; +import { useNavigate } from 'react-router-dom'; + +const JobSelectionPage = () => { + const [jobs, setJobs] = useState([]); + const [selectedJobNo, setSelectedJobNo] = useState(''); + const [selectedJobName, setSelectedJobName] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + async function loadJobs() { + setLoading(true); + setError(''); + try { + const res = await fetchJobs({}); + if (res.data && Array.isArray(res.data.jobs)) { + setJobs(res.data.jobs); + } else { + setJobs([]); + } + } catch (e) { + setError('프로젝트 목록을 불러오지 못했습니다.'); + } finally { + setLoading(false); + } + } + loadJobs(); + }, []); + + const handleSelect = (e) => { + const jobNo = e.target.value; + setSelectedJobNo(jobNo); + const job = jobs.find(j => j.job_no === jobNo); + setSelectedJobName(job ? job.job_name : ''); + }; + + const handleConfirm = () => { + if (selectedJobNo && selectedJobName) { + navigate(`/bom-manager?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`); + } + }; + + return ( + + 프로젝트 선택 + {loading && } + {error && {error}} + + 프로젝트 + + + {selectedJobNo && ( + + 선택된 프로젝트: {selectedJobNo} ({selectedJobName}) + + )} + + + ); +}; + +export default JobSelectionPage; \ No newline at end of file diff --git a/frontend/src/pages/MaterialLookupPage.jsx b/frontend/src/pages/MaterialLookupPage.jsx new file mode 100644 index 0000000..77a961b --- /dev/null +++ b/frontend/src/pages/MaterialLookupPage.jsx @@ -0,0 +1,221 @@ +import React, { useEffect, useState } from 'react'; +import { + Box, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + CircularProgress, + Alert +} from '@mui/material'; + +// API 함수는 기존 api.js의 fetchJobs, fetchFiles, fetchMaterials를 활용한다고 가정 +import { fetchJobs, fetchMaterials } from '../api'; + +const MaterialLookupPage = () => { + const [jobs, setJobs] = useState([]); + const [files, setFiles] = useState([]); + const [revisions, setRevisions] = useState([]); + const [selectedJobNo, setSelectedJobNo] = useState(''); + const [selectedFilename, setSelectedFilename] = useState(''); + const [selectedRevision, setSelectedRevision] = useState(''); + const [materials, setMaterials] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + // 1. Job 목록 불러오기 (최초 1회) + useEffect(() => { + async function loadJobs() { + try { + const res = await fetchJobs({}); + if (res.data && res.data.jobs) setJobs(res.data.jobs); + } catch (e) { + setError('Job 목록을 불러오지 못했습니다.'); + } + } + loadJobs(); + }, []); + + // 2. Job 선택 시 해당 도면(파일) 목록 불러오기 + useEffect(() => { + async function loadFiles() { + if (!selectedJobNo) { + setFiles([]); + setRevisions([]); + setSelectedFilename(''); + setSelectedRevision(''); + return; + } + try { + const res = await fetch(`/files?job_no=${selectedJobNo}`); + const data = await res.json(); + if (Array.isArray(data)) setFiles(data); + else if (data && Array.isArray(data.files)) setFiles(data.files); + else setFiles([]); + setSelectedFilename(''); + setSelectedRevision(''); + setRevisions([]); + } catch (e) { + setFiles([]); + setRevisions([]); + setError('도면 목록을 불러오지 못했습니다.'); + } + } + loadFiles(); + }, [selectedJobNo]); + + // 3. 도면 선택 시 해당 리비전 목록 추출 + useEffect(() => { + if (!selectedFilename) { + setRevisions([]); + setSelectedRevision(''); + return; + } + const filtered = files.filter(f => f.original_filename === selectedFilename); + setRevisions(filtered.map(f => f.revision)); + setSelectedRevision(''); + }, [selectedFilename, files]); + + // 4. 조회 버튼 클릭 시 자재 목록 불러오기 + const handleLookup = async () => { + setLoading(true); + setError(''); + setMaterials([]); + try { + const params = { + job_no: selectedJobNo, + filename: selectedFilename, + revision: selectedRevision + }; + const res = await fetchMaterials(params); + if (res.data && Array.isArray(res.data.materials)) { + setMaterials(res.data.materials); + } else { + setMaterials([]); + } + } catch (e) { + setError('자재 목록을 불러오지 못했습니다.'); + } finally { + setLoading(false); + } + }; + + // 5. 3개 모두 선택 시 자동 조회 (원하면 주석 해제) + // useEffect(() => { + // if (selectedJobNo && selectedFilename && selectedRevision) { + // handleLookup(); + // } + // }, [selectedJobNo, selectedFilename, selectedRevision]); + + return ( + + + 자재 상세 조회 (Job No + 도면명 + 리비전) + + + {/* Job No 드롭다운 */} + + Job No + + + {/* 도면명 드롭다운 */} + + 도면명(파일명) + + + {/* 리비전 드롭다운 */} + + 리비전 + + + + + {error && {error}} + {loading && } + {!loading && materials.length > 0 && ( + + + + + 품명 + 수량 + 단위 + 사이즈 + 재질 + 라인번호 + + + + {materials.map(mat => ( + + {mat.original_description} + {mat.quantity} + {mat.unit} + {mat.size_spec} + {mat.material_grade} + {mat.line_number} + + ))} + +
+
+ )} + {!loading && materials.length === 0 && (selectedJobNo && selectedFilename && selectedRevision) && ( + + 해당 조건에 맞는 자재가 없습니다. + + )} +
+ ); +}; + +export default MaterialLookupPage; \ No newline at end of file diff --git a/frontend/src/pages/MaterialsPage.jsx b/frontend/src/pages/MaterialsPage.jsx new file mode 100644 index 0000000..90faa91 --- /dev/null +++ b/frontend/src/pages/MaterialsPage.jsx @@ -0,0 +1,256 @@ +import React, { useEffect, useState } from 'react'; +import { + Box, + Typography, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + CircularProgress, + Alert, + Chip, + Card, + CardContent, + Grid +} from '@mui/material'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { fetchMaterialsSummary } from '../api'; + +const MaterialsPage = () => { + const [materials, setMaterials] = useState([]); + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const fileId = searchParams.get('file_id'); + const jobNo = searchParams.get('job_no'); + const filename = searchParams.get('filename'); + + // 자재 목록 불러오기 + const loadMaterials = async () => { + if (!fileId) return; + + setLoading(true); + setError(''); + try { + // 자재 목록 조회 + const response = await fetch(`http://localhost:8000/files/materials?file_id=${fileId}`); + const data = await response.json(); + + if (data.materials && Array.isArray(data.materials)) { + // 동일 항목 그룹화 (품명 + 사이즈 + 재질이 같은 것들) + const groupedMaterials = groupMaterialsByItem(data.materials); + setMaterials(groupedMaterials); + } else { + setMaterials([]); + } + + // 요약 정보 조회 + const summaryResponse = await fetchMaterialsSummary({ file_id: fileId }); + if (summaryResponse.data.success) { + setSummary(summaryResponse.data.summary); + } + } catch (e) { + setError('자재 목록을 불러오지 못했습니다.'); + console.error('자재 로드 에러:', e); + } finally { + setLoading(false); + } + }; + + // 동일 항목 그룹화 함수 + const groupMaterialsByItem = (materials) => { + const grouped = {}; + + materials.forEach(material => { + // 그룹화 키: 품명 + 사이즈 + 재질 + 분류 + const key = `${material.original_description}_${material.size_spec || ''}_${material.material_grade || ''}_${material.classified_category || ''}`; + + if (!grouped[key]) { + grouped[key] = { + ...material, + totalQuantity: 0, + items: [] + }; + } + + grouped[key].totalQuantity += material.quantity || 0; + grouped[key].items.push(material); + }); + + return Object.values(grouped).sort((a, b) => b.totalQuantity - a.totalQuantity); + }; + + useEffect(() => { + loadMaterials(); + }, [fileId]); + + // 분류별 색상 지정 + const getCategoryColor = (category) => { + const colors = { + 'PIPE': 'primary', + 'FITTING': 'secondary', + 'VALVE': 'error', + 'FLANGE': 'warning', + 'BOLT': 'info', + 'GASKET': 'success', + 'INSTRUMENT': 'default' + }; + return colors[category] || 'default'; + }; + + return ( + + {/* 헤더 */} + + + + + 자재 목록 - {filename} + + + + Job No: {jobNo} + + + + {/* 요약 정보 */} + {summary && ( + + + + + + 총 항목 수 + + + {summary.total_items} + + + + + + + + + 고유 품명 수 + + + {summary.unique_descriptions} + + + + + + + + + 총 수량 + + + {summary.total_quantity} + + + + + + + + + 고유 재질 수 + + + {summary.unique_materials} + + + + + + )} + + {error && {error}} + {loading && } + + {/* 자재 목록 테이블 */} + {!loading && materials.length > 0 && ( + + + + + 분류 + 품명 + 사이즈 + 재질 + 총 수량 + 단위 + 항목 수 + 신뢰도 + + + + {materials.map((material, index) => ( + + + + + + + {material.original_description} + + + {material.size_spec || '-'} + {material.material_grade || '-'} + + + {material.totalQuantity} + + + {material.unit || 'EA'} + + + + + 0.7 ? 'success.main' : 'warning.main'} + > + {Math.round((material.classification_confidence || 0) * 100)}% + + + + ))} + +
+
+ )} + + {!loading && materials.length === 0 && fileId && ( + + 해당 파일에 자재 정보가 없습니다. + + )} +
+ ); +}; + +export default MaterialsPage; \ No newline at end of file diff --git a/frontend/src/pages/ProjectSelectionPage.jsx b/frontend/src/pages/ProjectSelectionPage.jsx new file mode 100644 index 0000000..9ccabd7 --- /dev/null +++ b/frontend/src/pages/ProjectSelectionPage.jsx @@ -0,0 +1,71 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Typography, FormControl, InputLabel, Select, MenuItem, CircularProgress, Alert, Button } from '@mui/material'; +import { fetchJobs } from '../api'; +import { useNavigate } from 'react-router-dom'; + +const ProjectSelectionPage = () => { + const [jobs, setJobs] = useState([]); + const [selectedJob, setSelectedJob] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + async function loadJobs() { + setLoading(true); + setError(''); + try { + const res = await fetchJobs({}); + if (res.data && Array.isArray(res.data.jobs)) { + setJobs(res.data.jobs); + } else { + setJobs([]); + } + } catch (e) { + setError('프로젝트 목록을 불러오지 못했습니다.'); + } finally { + setLoading(false); + } + } + loadJobs(); + }, []); + + return ( + + 프로젝트(Job No) 선택 + {loading && } + {error && {error}} + + Job No + + + {selectedJob && ( + + 선택된 Job No: {selectedJob} + + )} + + + ); +}; + +export default ProjectSelectionPage; \ No newline at end of file