8 Commits

Author SHA1 Message Date
king b919f3ae30 결과 엑셀을 A열(사방넷 코드) 오름차순 정렬
- 문자열 비교 기준 정렬 (숫자/문자 혼재 대비)
- mergesort 로 안정 정렬 보장
- pandas to_excel 은 헤더를 별도로 다루므로 1행 헤더는 영향 없음

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:26:30 +09:00
king 52ce51aac7 결과 엑셀 확장자를 .xls → .xlsx 로 변경
파일 내용은 openpyxl 로 작성된 xlsx 포맷이지만
확장자만 .xls 였던 것을 실제 포맷과 맞춰 .xlsx 로 정정.

- 서버 다운로드 파일명 확장자 변경
- 프론트 fallback 파일명 변경

xls 파일을 "쓰는" 코드(xlwt 등)는 원래 없었음.
xlrd 는 입력 파일(.xls) 을 "읽는" 용도라 유지.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:22:16 +09:00
king da3ff199d8 결과 엑셀 A~C 열 너비 자동 조절
콘텐츠 길이에 맞춰 열 너비를 계산해서 적용한다.
한글은 ASCII 폭의 약 2배로 가중치를 두고,
최소 10 · 최대 60 으로 클램프해 너무 좁거나 넓지 않게 한다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:19:46 +09:00
king 9e6576a594 업로드 날짜 모달 UX 다듬기
- 기본 선택값을 오늘 날짜로 미리 지정 (업로드 실행 버튼도 즉시 활성)
- 안내 문구를 "발주 데이터의 주문 날짜로 날짜를 선택해주세요." 로 수정
- 선택된 날짜 박스와 하단 버튼 사이 1.5rem 간격 확보

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:02:03 +09:00
king 19a552452f 사방넷 코드가 없는 항목은 결과 엑셀에서 제외
itemcode_db 의 single_items.sabangnet_code 가 비어 있거나
마스터에 등록되지 않은 상품(미등록 코드 / 세트 구성 단품 미등록)은
결과 엑셀의 상품코드[필수] 컬럼에 들어갈 값이 없으므로 제외한다.

- 폴백 시 sabangnet_code 자리에 내부 코드 채우던 동작 제거
- 엑셀 빌드 단계에서 사방넷 코드가 빈 행을 스킵하고 warning 로그
- success 로그에 excel_row_count / skipped_no_sabang_count 노출

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:57:20 +09:00
king 995adb5cd8 업로드 데이터가 날짜 검색에 잡히지 않던 문제 수정
기존 데이터는 "YYYY-MM-DD HH:MM:SS" 형식으로 저장되고
검색 쿼리도 "YYYY-MM-DD 00:00:00" 으로 문자열 비교하는데,
새 업로드는 custom_date(YYYY-MM-DD)를 그대로 저장해서
문자열 비교상 날짜 필터에 잡히지 않아
"DB 업로드가 안 된 것처럼" 보이는 문제 발생.

달력에서 고른 날짜에 시간 00:00:00 을 붙여
"YYYY-MM-DD HH:MM:SS" 로 정규화 저장한다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:55:13 +09:00
king ea6a696903 낱개 출고 자동분석 업로드 예외 처리 보강
- 주문 날짜(custom_date) 형식 검증을 진입부로 이동 (400 응답)
- 엑셀 파싱/템플릿 컬럼 누락은 500이 아닌 400으로 분리
- 빈 행 자동 스킵으로 빈 Order 레코드 저장 방지
- safe_int_quantity 헬퍼 도입: 수량/세트 구성수량의 None·비숫자·
  '1.0' 같은 문자열도 견고하게 정수 변환
- itemcode_db 엔진 생성/조회 실패 시 단품 처리로 폴백하고
  치명적 오류 대신 warning 로그로 기록
- 세트 구성 단품이 single_items 에 없을 때 보강하여 결과 행 누락 방지
- 마스터 미등록 상품코드를 모아 warning 로그로 가시화
- 완료 시 단품/미등록 코드 수를 success 로그에 함께 기록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:36:48 +09:00
king ba07804d54 업로드 날짜 모달 CSS 클래스 버그 수정
modal-overlay 클래스에 CSS 정의가 없어 모달이 항상 표시되는
버그를 수정: 기존 .modal/.modal.show 패턴으로 교체하고,
배경 클릭 시 모달이 닫히는 동작도 추가.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 23:30:41 +09:00
4 changed files with 272 additions and 116 deletions
+213 -72
View File
@@ -83,6 +83,29 @@ def normalize_template_column(value) -> str:
return str(value).replace("\n", "").replace(" ", "").strip() return str(value).replace("\n", "").replace(" ", "").strip()
def safe_int_quantity(value, default: int = 0) -> int:
"""수량 값을 안전하게 정수로 변환. 비숫자/빈값/None 은 default 반환.
엑셀에서 1.0 같은 float, '1 '·'2개' 같은 공백 포함 문자열도 견고하게 처리."""
if value is None:
return default
if isinstance(value, bool):
return default
if isinstance(value, (int, float)):
try:
if pd.isna(value):
return default
return int(value)
except (ValueError, OverflowError, TypeError):
return default
text = str(value).strip()
if not text:
return default
try:
return int(float(text))
except (ValueError, TypeError):
return default
def clean_excel_value(value) -> str | None: def clean_excel_value(value) -> str | None:
if value is None or pd.isna(value): if value is None or pd.isna(value):
return None return None
@@ -566,10 +589,21 @@ def upload_file(
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
# 0. 주문 날짜(custom_date) 검증 — 잘못된 형식이면 400
try:
order_date_dt = datetime.strptime(custom_date, "%Y-%m-%d")
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="주문 날짜 형식이 올바르지 않습니다 (YYYY-MM-DD).")
# 기존 데이터/검색 쿼리가 "YYYY-MM-DD HH:MM:SS" 문자열 비교를 사용하므로
# 새 업로드 데이터도 동일하게 시간 부분(00:00:00)을 붙여 저장한다.
order_date_str = order_date_dt.strftime("%Y-%m-%d %H:%M:%S")
# 1. 파일 확장자 검증 # 1. 파일 확장자 검증
if not files:
raise HTTPException(status_code=400, detail="업로드할 파일이 없습니다.")
for file in files: 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") or filename.endswith(".xlsx")):
raise HTTPException(status_code=400, detail="XLS 또는 XLSX 파일만 업로드할 수 있습니다.") raise HTTPException(status_code=400, detail="XLS 또는 XLSX 파일만 업로드할 수 있습니다.")
filenames_str = ", ".join([f.filename for f in files]) filenames_str = ", ".join([f.filename for f in files])
@@ -580,24 +614,40 @@ def upload_file(
{"filenames": filenames_str, "custom_date": custom_date} {"filenames": filenames_str, "custom_date": custom_date}
) )
# 2. 파일 파싱 — 형식/템플릿 오류는 400 으로 분리
db_records = [] db_records = []
try: try:
# 2. 파일 파싱 및 데이터베이스 레코드 임시 적재
for file in files: for file in files:
contents = file.file.read() contents = file.file.read()
df = pd.read_excel(io.BytesIO(contents)) try:
df = pd.read_excel(io.BytesIO(contents))
except Exception as parse_err:
raise HTTPException(
status_code=400,
detail=f"'{file.filename}' 파일을 엑셀로 읽을 수 없습니다: {parse_err}"
)
df = df.where(pd.notnull(df), None) df = df.where(pd.notnull(df), None)
column_map = find_template_columns(list(df.columns)) try:
column_map = find_template_columns(list(df.columns))
except ValueError as ve:
raise HTTPException(
status_code=400,
detail=f"'{file.filename}' {ve}"
)
for index, row in df.iterrows(): for _index, row in df.iterrows():
rlist = list(row) rlist = list(row)
# 모든 셀이 비어있는 행은 스킵
if all(v is None or (isinstance(v, str) and not v.strip()) for v in rlist):
continue
recipient_phone = row_value(rlist, column_map, "recipient_phone") recipient_phone = row_value(rlist, column_map, "recipient_phone")
recipient_mobile = row_value(rlist, column_map, "recipient_mobile") recipient_mobile = row_value(rlist, column_map, "recipient_mobile")
tracking_number = row_value(rlist, column_map, "tracking_number") tracking_number = row_value(rlist, column_map, "tracking_number")
address = row_value(rlist, column_map, "address") address = row_value(rlist, column_map, "address")
order = Order( order = Order(
order_date=custom_date, # 사용자가 지정한 주문 날짜 강제 입력 order_date=order_date_str, # 달력에서 고른 날짜 + 00:00:00 (검색 호환)
sequence_num=row_value(rlist, column_map, "sequence_num"), sequence_num=row_value(rlist, column_map, "sequence_num"),
order_no=row_value(rlist, column_map, "order_no"), order_no=row_value(rlist, column_map, "order_no"),
order_no_mall=row_value(rlist, column_map, "order_no_mall"), order_no_mall=row_value(rlist, column_map, "order_no_mall"),
@@ -621,8 +671,14 @@ def upload_file(
upload_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S") upload_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
) )
db_records.append(order) db_records.append(order)
except HTTPException:
raise
except Exception as e:
log_event(current_user, "파일 파싱 실패", "error", {"filenames": filenames_str, "error": str(e)})
raise HTTPException(status_code=400, detail=f"엑셀 파싱 실패: {str(e)}")
# 3. 데이터베이스 벌크 저장 및 커밋 # 3. 데이터베이스 벌크 저장 — DB 오류는 500
try:
batch_size = 1000 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]
@@ -634,113 +690,198 @@ def upload_file(
"success", "success",
{"filenames": filenames_str, "uploaded_count": len(db_records)} {"filenames": filenames_str, "uploaded_count": len(db_records)}
) )
except Exception as e: except Exception as e:
db.rollback() db.rollback()
log_event(current_user, "파일 업로드 실패", "error", {"filenames": filenames_str, "error": str(e)}) log_event(current_user, "파일 업로드 DB 저장 실패", "error", {"filenames": filenames_str, "error": str(e)})
raise HTTPException(status_code=500, detail=f"데이터베이스 저장 실패: {str(e)}") raise HTTPException(status_code=500, detail=f"데이터베이스 저장 실패: {str(e)}")
# 4. 품목 마스터(itemcode_db) 연동 및 낱개 수량 환산 합계 계산 # 4. 품목 마스터(itemcode_db) 연동 및 낱개 수량 환산 합계 계산
try: try:
item_engine = get_secondary_engine("ITEM_DB_NAME") single_items: dict[str, dict] = {}
set_components: dict[str, list[dict]] = {}
single_items = {}
set_components = {} # itemcode_db 연결 실패는 치명적이지 않게 처리:
# 마스터를 못 읽으면 모든 상품을 단품으로 간주해서 그대로 합산한다.
try:
item_engine = get_secondary_engine("ITEM_DB_NAME")
except Exception as conn_err:
item_engine = None
log_event(
current_user,
"itemcode_db 엔진 생성 실패",
"warning",
{"error": str(conn_err)},
)
if item_engine: if item_engine:
with item_engine.connect() as conn: try:
# 1) 단품 마스터 맵 구성 with item_engine.connect() as conn:
rs_single = conn.execute(text("SELECT item_code, sabangnet_code, name FROM single_items")) # 1) 단품 마스터 맵 구성
for row in rs_single: rs_single = conn.execute(text("SELECT item_code, sabangnet_code, name FROM single_items"))
single_items[row[0]] = { for row in rs_single:
"sabangnet_code": row[1] or "", if not row[0]:
"name": row[2] or "" continue
} single_items[row[0]] = {
# 2) 세트 구성품 맵 구성 "sabangnet_code": row[1] or "",
rs_comp = conn.execute(text("SELECT set_code, single_code, quantity FROM set_components")) "name": row[2] or ""
for row in rs_comp: }
s_code = row[0] # 2) 세트 구성품 맵 구성
if s_code not in set_components: rs_comp = conn.execute(text("SELECT set_code, single_code, quantity FROM set_components"))
set_components[s_code] = [] for row in rs_comp:
set_components[s_code].append({ s_code = row[0]
"single_code": row[1], single_code = row[1]
"quantity": row[2] if not s_code or not single_code:
}) continue
set_components.setdefault(s_code, []).append({
# 3) 낱개 환산 처리 "single_code": single_code,
single_qty_sums = {} # 마스터의 수량 컬럼도 NULL/문자열 방어
"quantity": safe_int_quantity(row[2], default=1),
})
except Exception as db_err:
# 마스터 조회 실패해도 분석을 중단하지 않고 단품 처리로 폴백
log_event(
current_user,
"itemcode_db 조회 실패 — 단품 처리로 폴백",
"warning",
{"error": str(db_err)},
)
# 4-1) 낱개 환산 처리
single_qty_sums: dict[str, int] = {}
unknown_codes: set[str] = set()
for order in db_records: for order in db_records:
prod_code = (order.product_code or "").strip() prod_code = (order.product_code or "").strip()
if not prod_code: if not prod_code:
continue continue
qty = 1 # 수량은 비숫자/공백/None 모두 1로 폴백 (보존: 비어 있는 셀도 1건으로 본다)
try: qty = safe_int_quantity(order.order_quantity, default=1)
qty = int(order.order_quantity or 1)
except:
pass
if prod_code in single_items: if prod_code in single_items:
# 단품: 그대로 합산
single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty
elif prod_code in set_components: elif prod_code in set_components:
# 세트: 구성 단품별로 풀어서 합산
for comp in set_components[prod_code]: for comp in set_components[prod_code]:
s_code = comp["single_code"] s_code = comp["single_code"]
comp_qty = comp["quantity"] comp_qty = safe_int_quantity(comp["quantity"], default=1)
single_qty_sums[s_code] = single_qty_sums.get(s_code, 0) + (qty * comp_qty) single_qty_sums[s_code] = single_qty_sums.get(s_code, 0) + (qty * comp_qty)
# 세트 구성 단품 정보가 single_items 에 없을 수도 있으므로 placeholder 만 보강.
# 사방넷 코드는 비워서, 결과 엑셀 빌드 단계에서 제외 대상으로 인식되게 둔다.
if s_code not in single_items:
single_items[s_code] = {"sabangnet_code": "", "name": ""}
else: else:
# DB에 등록되어 있지 않은 코드는 단품으로 간주하고, 사방넷코드와 이름을 보존 # 마스터 미등록 코드: 사방넷 코드를 알 수 없으므로 placeholder 로만 추적.
# 결과 엑셀에서는 제외된다.
unknown_codes.add(prod_code)
single_items[prod_code] = { single_items[prod_code] = {
"sabangnet_code": prod_code, "sabangnet_code": "",
"name": order.product_name or "" "name": order.product_name or ""
} }
single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty
if unknown_codes:
log_event(
current_user,
"낱개 출고 분석 — 마스터 미등록 코드 감지",
"warning",
{"unknown_count": len(unknown_codes), "sample": list(unknown_codes)[:20]},
)
# 5. 결과 엑셀 데이터 빌드 # 5. 결과 엑셀 데이터 빌드
# A열=사방넷코드, B열=합계수량, C열=이름. 헤더는 요청 사양(상품코드[필수]/가용수량/불용수량).
# 사방넷 코드가 없는(마스터 미등록·구성품 미등록) 단품은 결과에서 제외한다.
excel_rows = [] excel_rows = []
skipped_no_sabang: list[str] = []
for s_code, total_qty in single_qty_sums.items(): for s_code, total_qty in single_qty_sums.items():
s_info = single_items.get(s_code, {"sabangnet_code": s_code, "name": ""}) s_info = single_items.get(s_code, {"sabangnet_code": "", "name": ""})
sabang_code = (s_info.get("sabangnet_code") or "").strip()
if not sabang_code:
skipped_no_sabang.append(s_code)
continue
excel_rows.append({ excel_rows.append({
"상품코드[필수]": s_info["sabangnet_code"] or s_code, "상품코드[필수]": sabang_code,
"가용수량": total_qty, "가용수량": total_qty,
"불용수량": s_info["name"] or "" # C열 헤더는 "불용수량", 실데이터는 이름! "불용수량": s_info.get("name") or ""
}) })
df_out = pd.DataFrame(excel_rows) if skipped_no_sabang:
if df_out.empty: log_event(
df_out = pd.DataFrame(columns=["상품코드[필수]", "가용수량", "불용수량"]) current_user,
else: "낱개 출고 분석 — 사방넷 코드 없는 항목 제외",
df_out = df_out[["상품코드[필수]", "가용수량", "불용수량"]] "warning",
{"skipped_count": len(skipped_no_sabang), "sample": skipped_no_sabang[:20]},
# 엑셀 파일 쓰기 )
df_out = pd.DataFrame(excel_rows, columns=["상품코드[필수]", "가용수량", "불용수량"])
# A열(사방넷 코드) 기준 오름차순 정렬 — 헤더는 영향 받지 않음
if not df_out.empty:
df_out = df_out.sort_values(
by="상품코드[필수]",
key=lambda col: col.astype(str),
kind="mergesort",
).reset_index(drop=True)
# 엑셀 파일 쓰기 (1행 헤더, 2행부터 데이터 — pandas to_excel 기본 동작)
# 작성 직후 A~C 열 너비를 콘텐츠 길이에 맞춰 자동 조절한다.
# (한글은 ASCII보다 폭이 약 2배이므로 가중치 적용)
def _excel_display_len(value) -> int:
s = "" if value is None else str(value)
width = 0
for ch in s:
width += 2 if ord(ch) > 127 else 1
return width
excel_io = io.BytesIO() excel_io = io.BytesIO()
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer: with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
df_out.to_excel(writer, index=False) df_out.to_excel(writer, index=False, sheet_name="발주")
worksheet = writer.sheets["발주"]
from openpyxl.utils import get_column_letter
for col_idx, col_name in enumerate(df_out.columns, start=1):
header_w = _excel_display_len(col_name)
data_w = max(
(_excel_display_len(v) for v in df_out[col_name].values),
default=0,
)
# 최소 10, 최대 60 으로 클램프 + 여백 2
worksheet.column_dimensions[get_column_letter(col_idx)].width = (
min(60, max(10, max(header_w, data_w) + 2))
)
excel_io.seek(0) excel_io.seek(0)
# 6. 다운로드 파일명 날짜 포맷 조합 # 6. 다운로드 파일명: "MM월 DD일 (요일)_발주_(낱개 상품 출고).xlsx"
try:
dt = datetime.strptime(custom_date, "%Y-%m-%d")
except:
dt = datetime.now()
weekdays = ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] weekdays = ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
weekday_str = weekdays[dt.weekday()] weekday_str = weekdays[order_date_dt.weekday()]
filename = f"{order_date_dt.strftime('%m')}{order_date_dt.strftime('%d')}일 ({weekday_str})_발주_(낱개 상품 출고).xlsx"
# 형식 예: 05월 10일 (일요일)_발주_(낱개 상품 출고).xls
filename = f"{dt.strftime('%m')}{dt.strftime('%d')}일 ({weekday_str})_발주_(낱개 상품 출고).xls"
encoded_filename = quote(filename) encoded_filename = quote(filename)
headers = { headers = {
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}", "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
"Access-Control-Expose-Headers": "Content-Disposition" "Access-Control-Expose-Headers": "Content-Disposition"
} }
log_event(
current_user,
"낱개 출고 자동분석 완료",
"success",
{
"filenames": filenames_str,
"uploaded_count": len(db_records),
"single_code_count": len(single_qty_sums),
"excel_row_count": len(excel_rows),
"unknown_code_count": len(unknown_codes),
"skipped_no_sabang_count": len(skipped_no_sabang),
},
)
return StreamingResponse( return StreamingResponse(
excel_io, excel_io,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers=headers headers=headers
) )
except HTTPException:
raise
except Exception as e: except Exception as e:
log_event(current_user, "낱개 출고 분석 오류", "error", {"error": str(e)}) log_event(current_user, "낱개 출고 분석 오류", "error", {"error": str(e)})
raise HTTPException(status_code=500, detail=f"낱개 출고 분석 및 엑셀 생성 실패: {str(e)}") raise HTTPException(status_code=500, detail=f"낱개 출고 분석 및 엑셀 생성 실패: {str(e)}")
+11 -4
View File
@@ -1839,12 +1839,18 @@ input[type="checkbox"] {
Upload Date Picker Custom Calendar Styles Upload Date Picker Custom Calendar Styles
========================================================================== */ ========================================================================== */
#upload-date-modal .modal-content { .upload-date-modal-content {
background: rgba(255, 255, 255, 0.95); max-width: 400px;
background: rgba(255, 255, 255, 0.95) !important;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.4) !important;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
}
.upload-date-modal-content h2 {
font-size: 1.2rem;
margin-bottom: 0.75rem;
} }
.modal-instruction-text { .modal-instruction-text {
@@ -1968,6 +1974,7 @@ input[type="checkbox"] {
border: 1px dashed #cbd5e1; border: 1px dashed #cbd5e1;
border-radius: 8px; border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
margin-bottom: 1.5rem;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
+30 -33
View File
@@ -407,42 +407,39 @@
</div> </div>
<!-- Upload Date Picker Modal --> <!-- Upload Date Picker Modal -->
<div id="upload-date-modal" class="modal-overlay"> <div id="upload-date-modal" class="modal">
<div class="modal-content glass-panel" style="max-width: 400px;"> <div class="modal-content glass-panel upload-date-modal-content">
<div class="modal-header"> <span class="close-btn" id="upload-date-close">&times;</span>
<h2><i class="fa-regular fa-calendar-check"></i> 업로드 날짜 지정</h2> <h2><i class="fa-regular fa-calendar-check"></i> 업로드 날짜 지정</h2>
<button type="button" class="modal-close" id="upload-date-close">&times;</button> <p class="modal-instruction-text">발주 데이터의 주문 날짜로 날짜를 선택해주세요.</p>
</div>
<div class="modal-body"> <!-- Custom Calendar Picker Container -->
<p class="modal-instruction-text">발주 데이터의 주문 날짜로 입력될 날짜를 선택해주세요.</p> <div class="custom-calendar-picker">
<div class="calendar-nav">
<!-- Custom Calendar Picker Container --> <button type="button" id="cal-prev-month" class="icon-btn"><i class="fa-solid fa-chevron-left"></i></button>
<div class="custom-calendar-picker"> <span id="cal-month-year-display" class="calendar-title">2026년 05월</span>
<div class="calendar-nav"> <button type="button" id="cal-next-month" class="icon-btn"><i class="fa-solid fa-chevron-right"></i></button>
<button type="button" id="cal-prev-month" class="icon-btn"><i class="fa-solid fa-chevron-left"></i></button>
<span id="cal-month-year-display" class="calendar-title">2026년 05월</span>
<button type="button" id="cal-next-month" class="icon-btn"><i class="fa-solid fa-chevron-right"></i></button>
</div>
<div class="calendar-weekdays">
<div class="weekday holiday"></div>
<div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday sat"></div>
</div>
<div id="calendar-days-grid" class="calendar-days">
<!-- Days will be rendered via JS -->
</div>
</div> </div>
<div class="calendar-weekdays">
<div class="selected-date-preview"> <div class="weekday holiday"></div>
<span>선택된 날짜:</span> <div class="weekday"></div>
<strong id="selected-date-text">선택 안 됨</strong> <div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday"></div>
<div class="weekday sat"></div>
</div>
<div id="calendar-days-grid" class="calendar-days">
<!-- Days will be rendered via JS -->
</div> </div>
</div> </div>
<div class="modal-footer button-row">
<div class="selected-date-preview">
<span>선택된 날짜:</span>
<strong id="selected-date-text">선택 안 됨</strong>
</div>
<div class="modal-actions">
<button id="btn-cancel-upload-date" class="btn btn-secondary">취소</button> <button id="btn-cancel-upload-date" class="btn btn-secondary">취소</button>
<button id="btn-submit-upload-date" class="btn btn-primary" disabled> <button id="btn-submit-upload-date" class="btn btn-primary" disabled>
<i class="fa-solid fa-cloud-arrow-up"></i> 업로드 실행 <i class="fa-solid fa-cloud-arrow-up"></i> 업로드 실행
+18 -7
View File
@@ -397,13 +397,21 @@ document.addEventListener('DOMContentLoaded', () => {
fileUpload.addEventListener('change', (e) => { fileUpload.addEventListener('change', (e) => {
const files = e.target.files; const files = e.target.files;
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
pendingFiles = files; pendingFiles = files;
calendarActiveDate = new Date();
calendarSelectedDate = null; // 기본 선택값: 오늘 날짜 (사용자가 명시적으로 다시 고를 수도 있음)
btnSubmitUploadDate.disabled = true; const today = new Date();
selectedDateText.textContent = '선택 안 됨'; const yyyy = today.getFullYear();
const mm = String(today.getMonth() + 1).padStart(2, '0');
const dd = String(today.getDate()).padStart(2, '0');
const weekdayNames = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
calendarActiveDate = new Date(yyyy, today.getMonth(), today.getDate());
calendarSelectedDate = `${yyyy}-${mm}-${dd}`;
selectedDateText.textContent = `${yyyy}${today.getMonth() + 1}${today.getDate()}일 (${weekdayNames[today.getDay()]})`;
btnSubmitUploadDate.disabled = false;
renderCalendar(); renderCalendar();
uploadDateModal.classList.add('show'); uploadDateModal.classList.add('show');
}); });
@@ -430,7 +438,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
if (response.ok) { if (response.ok) {
let downloadFilename = '낱개상품출고.xls'; let downloadFilename = '낱개상품출고.xlsx';
const disposition = response.headers.get('Content-Disposition'); const disposition = response.headers.get('Content-Disposition');
if (disposition && disposition.indexOf('filename*=') !== -1) { if (disposition && disposition.indexOf('filename*=') !== -1) {
const matches = disposition.split("filename*=UTF-8''"); const matches = disposition.split("filename*=UTF-8''");
@@ -1068,6 +1076,9 @@ document.addEventListener('DOMContentLoaded', () => {
if (e.target === returnRequestModal) { if (e.target === returnRequestModal) {
closeReturnRequestModal(); closeReturnRequestModal();
} }
if (e.target === uploadDateModal) {
closeUploadDateModal();
}
}); });
if (btnOptimizeDb) { if (btnOptimizeDb) {