버그 수정: 관리자 권한 DB 불일치 우회 및 다중 엑셀 파일 업로드 기능 구현

This commit is contained in:
2026-05-26 21:50:19 +09:00
parent 274507e2af
commit b2ecd896f5
4 changed files with 99 additions and 82 deletions
+2 -1
View File
@@ -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)): 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") raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
return current_user return current_user
def _safe_next(raw: Optional[str]) -> str: def _safe_next(raw: Optional[str]) -> str:
"""클라이언트가 넘긴 next 를 검증. 같은 호스트의 절대경로만 허용. """클라이언트가 넘긴 next 를 검증. 같은 호스트의 절대경로만 허용.
+39 -24
View File
@@ -420,36 +420,40 @@ 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) current_user = SimpleNamespace(**user_data)
db = SessionLocal() db = SessionLocal()
filenames_str = ", ".join([f["filename"] for f in files_data])
try: try:
db_records = []
total_rows = 0
# 1단계: 모든 파일 읽기 및 행 데이터 정규화
for f_idx, file_item in enumerate(files_data):
contents = file_item["contents"]
filename = file_item["filename"]
set_upload_state( set_upload_state(
user_id, user_id,
status="reading", status="reading",
message="엑셀 파일을 읽는 중입니다...", message=f"[{f_idx + 1}/{len(files_data)}] 엑셀 파일을 읽는 중입니다 ({filename})...",
) )
# read_excel requires xlrd for .xls and openpyxl for .xlsx
df = pd.read_excel(io.BytesIO(contents)) df = pd.read_excel(io.BytesIO(contents))
# Replace NaN with None for SQLAlchemy
df = df.where(pd.notnull(df), None) df = df.where(pd.notnull(df), None)
column_map = find_template_columns(list(df.columns)) column_map = find_template_columns(list(df.columns))
total_rows = len(df) file_rows = len(df)
total_rows += file_rows
set_upload_state( set_upload_state(
user_id, user_id,
status="processing", status="processing",
message=f" {total_rows}의 엑셀 데이터를 정리하는 중입니다...", message=f"[{f_idx + 1}/{len(files_data)}] {filename}의 엑셀 데이터를 정리하는 중...",
total_rows=total_rows, total_rows=total_rows,
) )
db_records = []
batch_size = 1000
for index, row in df.iterrows(): for index, row in df.iterrows():
rlist = list(row) rlist = list(row)
recipient_phone = row_value(rlist, column_map, "recipient_phone") recipient_phone = row_value(rlist, column_map, "recipient_phone")
@@ -483,19 +487,20 @@ def run_upload_job(contents: bytes, filename: str, user_id: str, user_data: dict
) )
db_records.append(order) db_records.append(order)
# Update progress periodically # 주기적으로 데이터 정리 진행률 갱신 (전체 누적 건수 기준)
if index % 1000 == 0 and index > 0: current_processed = len(db_records)
calc_progress = float(index) / total_rows if total_rows else 1 if current_processed % 1000 == 0 and current_processed > 0:
calc_progress = float(current_processed) / total_rows if total_rows else 1
set_upload_state( set_upload_state(
user_id, user_id,
status="processing", status="processing",
message=f"{index}/{total_rows}건 엑셀 데이터 정리 중...", message=f"전체 {current_processed}건 엑셀 데이터 정리 중...",
upload_progress=calc_progress * 100, upload_progress=calc_progress * 100,
total_rows=total_rows, total_rows=total_rows,
processed_rows=index, processed_rows=current_processed,
) )
# 2단계: FTS 인덱스 및 DB 저장 단계
set_upload_state( set_upload_state(
user_id, user_id,
status="indexing", status="indexing",
@@ -506,6 +511,7 @@ def run_upload_job(contents: bytes, filename: str, user_id: str, user_data: dict
processed_rows=total_rows, processed_rows=total_rows,
) )
batch_size = 1000
for i in range(0, len(db_records), batch_size): for i in range(0, len(db_records), batch_size):
chunk = db_records[i:i + batch_size] chunk = db_records[i:i + batch_size]
db.bulk_save_objects(chunk) 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, current_user,
"파일 업로드 완료", "파일 업로드 완료",
"success", "success",
{"filename": filename, "uploaded_count": len(db_records)}, {"filenames": filenames_str, "uploaded_count": len(db_records)},
) )
except ValueError as e: except ValueError as e:
db.rollback() db.rollback()
set_upload_error(user_id, str(e)) 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: except Exception as e:
db.rollback() db.rollback()
set_upload_error(user_id, f"에러 발생: {str(e)}") 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: finally:
db.close() db.close()
@router.post("/upload") @router.post("/upload")
def upload_file( def upload_file(
file: UploadFile = File(...), files: list[UploadFile] = File(...),
upload_token: str = Form(...), upload_token: str = Form(...),
current_user = Depends(get_current_user) current_user = Depends(get_current_user)
): ):
for file in files:
filename = file.filename or "" filename = file.filename or ""
if not filename.endswith(".xls") and not filename.endswith(".xlsx"): if not filename.endswith(".xls") and not filename.endswith(".xlsx"):
raise HTTPException(status_code=400, detail="Only XLS or XLSX files are allowed.") raise HTTPException(status_code=400, detail="XLS 또는 XLSX 파일만 업로드할 수 있습니다.")
user_id = f"{current_user.id}:{upload_token}" user_id = f"{current_user.id}:{upload_token}"
files_data = []
for file in files:
contents = file.file.read() contents = file.file.read()
files_data.append({
"contents": contents,
"filename": file.filename or ""
})
user_data = { user_data = {
"id": getattr(current_user, "id", ""), "id": getattr(current_user, "id", ""),
"email": getattr(current_user, "email", None), "email": getattr(current_user, "email", None),
"name": getattr(current_user, "name", 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( set_upload_state(
user_id, user_id,
status="queued", status="queued",
@@ -578,7 +593,7 @@ def upload_file(
) )
threading.Thread( threading.Thread(
target=run_upload_job, target=run_upload_job,
args=(contents, filename, user_id, user_data), args=(files_data, user_id, user_data),
daemon=True, daemon=True,
).start() ).start()
+1 -1
View File
@@ -90,7 +90,7 @@
<span>(.xls, .xlsx)</span> <span>(.xls, .xlsx)</span>
</span> </span>
</label> </label>
<input id="file-upload" type="file" accept=".xls, .xlsx" /> <input id="file-upload" type="file" accept=".xls, .xlsx" multiple />
</div> </div>
</div> </div>
+7 -6
View File
@@ -261,17 +261,18 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
fileUpload.addEventListener('change', async (e) => { fileUpload.addEventListener('change', async (e) => {
const file = e.target.files[0]; const files = e.target.files;
if (!file) return; if (!files || files.length === 0) return;
logUiEvent('파일 업로드 파일 선택', { logUiEvent('파일 업로드 파일 선택', {
filename: file.name, count: files.length,
size: file.size, filenames: Array.from(files).map(f => f.name)
type: file.type
}); });
const uploadToken = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const uploadToken = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const formData = new FormData(); 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); formData.append('upload_token', uploadToken);
resetUploadProgressModal(); resetUploadProgressModal();