버그 수정: 관리자 권한 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)):
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 를 검증. 같은 호스트의 절대경로만 허용.
+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)
db = SessionLocal()
filenames_str = ", ".join([f["filename"] for f in files_data])
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(
user_id,
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))
# 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)
file_rows = len(df)
total_rows += file_rows
set_upload_state(
user_id,
status="processing",
message=f" {total_rows}의 엑셀 데이터를 정리하는 중입니다...",
message=f"[{f_idx + 1}/{len(files_data)}] {filename}의 엑셀 데이터를 정리하는 중...",
total_rows=total_rows,
)
db_records = []
batch_size = 1000
for index, row in df.iterrows():
rlist = list(row)
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)
# Update progress periodically
if index % 1000 == 0 and index > 0:
calc_progress = float(index) / total_rows if total_rows else 1
# 주기적으로 데이터 정리 진행률 갱신 (전체 누적 건수 기준)
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"{index}/{total_rows}건 엑셀 데이터 정리 중...",
message=f"전체 {current_processed}건 엑셀 데이터 정리 중...",
upload_progress=calc_progress * 100,
total_rows=total_rows,
processed_rows=index,
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)
):
for file in files:
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.")
raise HTTPException(status_code=400, detail="XLS 또는 XLSX 파일만 업로드할 수 있습니다.")
user_id = f"{current_user.id}:{upload_token}"
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()
+1 -1
View File
@@ -90,7 +90,7 @@
<span>(.xls, .xlsx)</span>
</span>
</label>
<input id="file-upload" type="file" accept=".xls, .xlsx" />
<input id="file-upload" type="file" accept=".xls, .xlsx" multiple />
</div>
</div>
+7 -6
View File
@@ -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();