From b2ecd896f56e87e9f744c4dd9db6ab7e717239c6 Mon Sep 17 00:00:00 2001 From: king Date: Tue, 26 May 2026 21:50:19 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95:=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B6=8C=ED=95=9C=20DB=20?= =?UTF-8?q?=EB=B6=88=EC=9D=BC=EC=B9=98=20=EC=9A=B0=ED=9A=8C=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8B=A4=EC=A4=91=20=EC=97=91=EC=85=80=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth.py | 3 +- app/routers/orders.py | 163 +++++++++++++++++++++++------------------- static/index.html | 2 +- static/js/app.js | 13 ++-- 4 files changed, 99 insertions(+), 82 deletions(-) diff --git a/app/auth.py b/app/auth.py index 17b1c10..3b1913c 100644 --- a/app/auth.py +++ b/app/auth.py @@ -113,11 +113,12 @@ def get_current_user(request: Request, db: Session = Depends(get_db)): def get_admin_user(current_user: User = Depends(get_current_user)): - if not current_user.is_admin: + if not current_user.is_admin and current_user.email.lower() not in ADMIN_EMAILS: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return current_user + def _safe_next(raw: Optional[str]) -> str: """클라이언트가 넘긴 next 를 검증. 같은 호스트의 절대경로만 허용. diff --git a/app/routers/orders.py b/app/routers/orders.py index 184ed37..40c66fa 100644 --- a/app/routers/orders.py +++ b/app/routers/orders.py @@ -420,82 +420,87 @@ def orders_to_excel_response(orders, filename: str, sheet_name: str = "Orders"): ) -def run_upload_job(contents: bytes, filename: str, user_id: str, user_data: dict): +def run_upload_job(files_data: list[dict], user_id: str, user_data: dict): current_user = SimpleNamespace(**user_data) db = SessionLocal() + filenames_str = ", ".join([f["filename"] for f in files_data]) try: - set_upload_state( - user_id, - status="reading", - message="엑셀 파일을 읽는 중입니다...", - ) - - # read_excel requires xlrd for .xls and openpyxl for .xlsx - df = pd.read_excel(io.BytesIO(contents)) - - # Replace NaN with None for SQLAlchemy - df = df.where(pd.notnull(df), None) - - column_map = find_template_columns(list(df.columns)) - - total_rows = len(df) - set_upload_state( - user_id, - status="processing", - message=f"총 {total_rows}건의 엑셀 데이터를 정리하는 중입니다...", - total_rows=total_rows, - ) - db_records = [] - batch_size = 1000 + total_rows = 0 - for index, row in df.iterrows(): - rlist = list(row) - recipient_phone = row_value(rlist, column_map, "recipient_phone") - recipient_mobile = row_value(rlist, column_map, "recipient_mobile") - tracking_number = row_value(rlist, column_map, "tracking_number") - address = row_value(rlist, column_map, "address") + # 1단계: 모든 파일 읽기 및 행 데이터 정규화 + for f_idx, file_item in enumerate(files_data): + contents = file_item["contents"] + filename = file_item["filename"] - order = Order( - order_date=row_value(rlist, column_map, "order_date"), - sequence_num=row_value(rlist, column_map, "sequence_num"), - order_no=row_value(rlist, column_map, "order_no"), - order_no_mall=row_value(rlist, column_map, "order_no_mall"), - recipient_name=row_value(rlist, column_map, "recipient_name"), - product_code=row_value(rlist, column_map, "product_code"), - product_name=row_value(rlist, column_map, "product_name"), - order_quantity=row_value(rlist, column_map, "order_quantity"), - address=address, - address_normalized=normalize_address_key(address), - postal_code=row_value(rlist, column_map, "postal_code"), - recipient_phone=recipient_phone, - recipient_mobile=recipient_mobile, - recipient_phone_normalized=normalize_phone(recipient_phone), - recipient_mobile_normalized=normalize_phone(recipient_mobile), - delivery_memo=row_value(rlist, column_map, "delivery_memo"), - tracking_number=tracking_number, - tracking_number_normalized=normalize_tracking(tracking_number), - vendor=row_value(rlist, column_map, "vendor"), - order_list_1=row_value(rlist, column_map, "order_list_1"), - order_note=row_value(rlist, column_map, "order_note"), - upload_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S") + set_upload_state( + user_id, + status="reading", + message=f"[{f_idx + 1}/{len(files_data)}] 엑셀 파일을 읽는 중입니다 ({filename})...", ) - db_records.append(order) - # Update progress periodically - if index % 1000 == 0 and index > 0: - calc_progress = float(index) / total_rows if total_rows else 1 + df = pd.read_excel(io.BytesIO(contents)) + df = df.where(pd.notnull(df), None) + column_map = find_template_columns(list(df.columns)) - set_upload_state( - user_id, - status="processing", - message=f"{index}/{total_rows}건 엑셀 데이터 정리 중...", - upload_progress=calc_progress * 100, - total_rows=total_rows, - processed_rows=index, + file_rows = len(df) + total_rows += file_rows + + set_upload_state( + user_id, + status="processing", + message=f"[{f_idx + 1}/{len(files_data)}] {filename}의 엑셀 데이터를 정리하는 중...", + total_rows=total_rows, + ) + + for index, row in df.iterrows(): + rlist = list(row) + recipient_phone = row_value(rlist, column_map, "recipient_phone") + recipient_mobile = row_value(rlist, column_map, "recipient_mobile") + tracking_number = row_value(rlist, column_map, "tracking_number") + address = row_value(rlist, column_map, "address") + + order = Order( + order_date=row_value(rlist, column_map, "order_date"), + sequence_num=row_value(rlist, column_map, "sequence_num"), + order_no=row_value(rlist, column_map, "order_no"), + order_no_mall=row_value(rlist, column_map, "order_no_mall"), + recipient_name=row_value(rlist, column_map, "recipient_name"), + product_code=row_value(rlist, column_map, "product_code"), + product_name=row_value(rlist, column_map, "product_name"), + order_quantity=row_value(rlist, column_map, "order_quantity"), + address=address, + address_normalized=normalize_address_key(address), + postal_code=row_value(rlist, column_map, "postal_code"), + recipient_phone=recipient_phone, + recipient_mobile=recipient_mobile, + recipient_phone_normalized=normalize_phone(recipient_phone), + recipient_mobile_normalized=normalize_phone(recipient_mobile), + delivery_memo=row_value(rlist, column_map, "delivery_memo"), + tracking_number=tracking_number, + tracking_number_normalized=normalize_tracking(tracking_number), + vendor=row_value(rlist, column_map, "vendor"), + order_list_1=row_value(rlist, column_map, "order_list_1"), + order_note=row_value(rlist, column_map, "order_note"), + upload_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S") ) + db_records.append(order) + # 주기적으로 데이터 정리 진행률 갱신 (전체 누적 건수 기준) + current_processed = len(db_records) + if current_processed % 1000 == 0 and current_processed > 0: + calc_progress = float(current_processed) / total_rows if total_rows else 1 + set_upload_state( + user_id, + status="processing", + message=f"전체 {current_processed}건 엑셀 데이터 정리 중...", + upload_progress=calc_progress * 100, + total_rows=total_rows, + processed_rows=current_processed, + ) + + # 2단계: FTS 인덱스 및 DB 저장 단계 set_upload_state( user_id, status="indexing", @@ -506,6 +511,7 @@ def run_upload_job(contents: bytes, filename: str, user_id: str, user_data: dict processed_rows=total_rows, ) + batch_size = 1000 for i in range(0, len(db_records), batch_size): chunk = db_records[i:i + batch_size] db.bulk_save_objects(chunk) @@ -538,39 +544,48 @@ def run_upload_job(contents: bytes, filename: str, user_id: str, user_data: dict current_user, "파일 업로드 완료", "success", - {"filename": filename, "uploaded_count": len(db_records)}, + {"filenames": filenames_str, "uploaded_count": len(db_records)}, ) except ValueError as e: db.rollback() set_upload_error(user_id, str(e)) - log_event(current_user, "파일 업로드 실패", "error", {"filename": filename, "error": str(e)}) + log_event(current_user, "파일 업로드 실패", "error", {"filenames": filenames_str, "error": str(e)}) except Exception as e: db.rollback() set_upload_error(user_id, f"에러 발생: {str(e)}") - log_event(current_user, "파일 업로드 실패", "error", {"filename": filename, "error": str(e)}) + log_event(current_user, "파일 업로드 실패", "error", {"filenames": filenames_str, "error": str(e)}) finally: db.close() @router.post("/upload") def upload_file( - file: UploadFile = File(...), + files: list[UploadFile] = File(...), upload_token: str = Form(...), current_user = Depends(get_current_user) ): - filename = file.filename or "" - if not filename.endswith(".xls") and not filename.endswith(".xlsx"): - raise HTTPException(status_code=400, detail="Only XLS or XLSX files are allowed.") + for file in files: + filename = file.filename or "" + if not filename.endswith(".xls") and not filename.endswith(".xlsx"): + raise HTTPException(status_code=400, detail="XLS 또는 XLSX 파일만 업로드할 수 있습니다.") user_id = f"{current_user.id}:{upload_token}" - contents = file.file.read() + files_data = [] + for file in files: + contents = file.file.read() + files_data.append({ + "contents": contents, + "filename": file.filename or "" + }) + user_data = { "id": getattr(current_user, "id", ""), "email": getattr(current_user, "email", None), "name": getattr(current_user, "name", None), } - log_event(current_user, "파일 업로드 시작", "started", {"filename": filename}) + filenames_str = ", ".join([f["filename"] for f in files_data]) + log_event(current_user, "파일 업로드 시작", "started", {"filenames": filenames_str}) set_upload_state( user_id, status="queued", @@ -578,7 +593,7 @@ def upload_file( ) threading.Thread( target=run_upload_job, - args=(contents, filename, user_id, user_data), + args=(files_data, user_id, user_data), daemon=True, ).start() diff --git a/static/index.html b/static/index.html index 9c16589..7b5cd6b 100644 --- a/static/index.html +++ b/static/index.html @@ -90,7 +90,7 @@ (.xls, .xlsx) - + diff --git a/static/js/app.js b/static/js/app.js index 84ca5b9..2bd7705 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -261,17 +261,18 @@ document.addEventListener('DOMContentLoaded', () => { }); fileUpload.addEventListener('change', async (e) => { - const file = e.target.files[0]; - if (!file) return; + const files = e.target.files; + if (!files || files.length === 0) return; logUiEvent('파일 업로드 파일 선택', { - filename: file.name, - size: file.size, - type: file.type + count: files.length, + filenames: Array.from(files).map(f => f.name) }); const uploadToken = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const formData = new FormData(); - formData.append('file', file); + for (let i = 0; i < files.length; i++) { + formData.append('files', files[i]); + } formData.append('upload_token', uploadToken); resetUploadProgressModal();