여러 엑셀 파일 업로드 시 업로드 날짜 지정 기능 및 낱개 출고 자동분석 다운로드 기능 구현
This commit is contained in:
+179
-59
@@ -270,82 +270,202 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
uploadProgressModal.style.display = 'none';
|
||||
});
|
||||
|
||||
fileUpload.addEventListener('change', async (e) => {
|
||||
// --- Upload Date picker calendar modal elements ---
|
||||
const uploadDateModal = document.getElementById('upload-date-modal');
|
||||
const uploadDateClose = document.getElementById('upload-date-close');
|
||||
const btnCancelUploadDate = document.getElementById('btn-cancel-upload-date');
|
||||
const btnSubmitUploadDate = document.getElementById('btn-submit-upload-date');
|
||||
const calPrevMonth = document.getElementById('cal-prev-month');
|
||||
const calNextMonth = document.getElementById('cal-next-month');
|
||||
const calMonthYearDisplay = document.getElementById('cal-month-year-display');
|
||||
const calendarDaysGrid = document.getElementById('calendar-days-grid');
|
||||
const selectedDateText = document.getElementById('selected-date-text');
|
||||
|
||||
let pendingFiles = null;
|
||||
let calendarActiveDate = new Date();
|
||||
let calendarSelectedDate = null;
|
||||
|
||||
function isKoreanHoliday(year, month, date) {
|
||||
const mm = String(month + 1).padStart(2, '0');
|
||||
const dd = String(date).padStart(2, '0');
|
||||
const mmdd = `${mm}-${dd}`;
|
||||
|
||||
const solarHolidays = [
|
||||
"01-01", "03-01", "05-05", "06-06", "08-15", "10-03", "10-09", "12-25"
|
||||
];
|
||||
if (solarHolidays.includes(mmdd)) return true;
|
||||
|
||||
if (year === 2026) {
|
||||
const holidays2026 = [
|
||||
"02-16", "02-17", "02-18", "02-19",
|
||||
"05-25",
|
||||
"09-24", "09-25", "09-26", "09-28"
|
||||
];
|
||||
return holidays2026.includes(mmdd);
|
||||
}
|
||||
if (year === 2025) {
|
||||
const holidays2025 = [
|
||||
"01-27", "01-28", "01-29", "01-30",
|
||||
"05-06",
|
||||
"10-05", "10-06", "10-07", "10-08"
|
||||
];
|
||||
return holidays2025.includes(mmdd);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderCalendar() {
|
||||
const year = calendarActiveDate.getFullYear();
|
||||
const month = calendarActiveDate.getMonth();
|
||||
|
||||
calMonthYearDisplay.textContent = `${year}년 ${String(month + 1).padStart(2, '0')}월`;
|
||||
calendarDaysGrid.innerHTML = '';
|
||||
|
||||
const firstDayIndex = new Date(year, month, 1).getDay();
|
||||
const lastDay = new Date(year, month + 1, 0).getDate();
|
||||
const prevLastDay = new Date(year, month, 0).getDate();
|
||||
|
||||
for (let i = firstDayIndex - 1; i >= 0; i--) {
|
||||
const dayDiv = document.createElement('div');
|
||||
dayDiv.className = 'calendar-day other-month';
|
||||
dayDiv.textContent = prevLastDay - i;
|
||||
calendarDaysGrid.appendChild(dayDiv);
|
||||
}
|
||||
|
||||
const weekdays = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
|
||||
for (let day = 1; day <= lastDay; day++) {
|
||||
const dayDiv = document.createElement('div');
|
||||
dayDiv.className = 'calendar-day';
|
||||
dayDiv.textContent = day;
|
||||
|
||||
const currentDate = new Date(year, month, day);
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
|
||||
if (dayOfWeek === 0 || isKoreanHoliday(year, month, day)) {
|
||||
dayDiv.classList.add('holiday');
|
||||
} else if (dayOfWeek === 6) {
|
||||
dayDiv.classList.add('sat');
|
||||
}
|
||||
|
||||
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
if (calendarSelectedDate === dateStr) {
|
||||
dayDiv.classList.add('selected');
|
||||
}
|
||||
|
||||
dayDiv.addEventListener('click', () => {
|
||||
calendarSelectedDate = dateStr;
|
||||
calendarDaysGrid.querySelectorAll('.calendar-day.selected').forEach(el => {
|
||||
el.classList.remove('selected');
|
||||
});
|
||||
dayDiv.classList.add('selected');
|
||||
selectedDateText.textContent = `${year}년 ${month + 1}월 ${day}일 (${weekdays[dayOfWeek]})`;
|
||||
btnSubmitUploadDate.disabled = false;
|
||||
});
|
||||
|
||||
calendarDaysGrid.appendChild(dayDiv);
|
||||
}
|
||||
|
||||
const totalCells = firstDayIndex + lastDay;
|
||||
const nextDaysToFill = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
|
||||
for (let day = 1; day <= nextDaysToFill; day++) {
|
||||
const dayDiv = document.createElement('div');
|
||||
dayDiv.className = 'calendar-day other-month';
|
||||
dayDiv.textContent = day;
|
||||
calendarDaysGrid.appendChild(dayDiv);
|
||||
}
|
||||
}
|
||||
|
||||
calPrevMonth.addEventListener('click', () => {
|
||||
calendarActiveDate.setMonth(calendarActiveDate.getMonth() - 1);
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
calNextMonth.addEventListener('click', () => {
|
||||
calendarActiveDate.setMonth(calendarActiveDate.getMonth() + 1);
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
const closeUploadDateModal = () => {
|
||||
uploadDateModal.classList.remove('show');
|
||||
pendingFiles = null;
|
||||
fileUpload.value = '';
|
||||
};
|
||||
|
||||
if (uploadDateClose) uploadDateClose.addEventListener('click', closeUploadDateModal);
|
||||
if (btnCancelUploadDate) btnCancelUploadDate.addEventListener('click', closeUploadDateModal);
|
||||
|
||||
fileUpload.addEventListener('change', (e) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
logUiEvent('파일 업로드 파일 선택', {
|
||||
count: files.length,
|
||||
filenames: Array.from(files).map(f => f.name)
|
||||
});
|
||||
|
||||
pendingFiles = files;
|
||||
calendarActiveDate = new Date();
|
||||
calendarSelectedDate = null;
|
||||
btnSubmitUploadDate.disabled = true;
|
||||
selectedDateText.textContent = '선택 안 됨';
|
||||
|
||||
renderCalendar();
|
||||
uploadDateModal.classList.add('show');
|
||||
});
|
||||
|
||||
btnSubmitUploadDate.addEventListener('click', async () => {
|
||||
if (!pendingFiles || !calendarSelectedDate) return;
|
||||
|
||||
btnSubmitUploadDate.disabled = true;
|
||||
const originalHtml = btnSubmitUploadDate.innerHTML;
|
||||
btnSubmitUploadDate.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> 업로드 및 분석 중...';
|
||||
|
||||
const uploadToken = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formData.append('files', files[i]);
|
||||
for (let i = 0; i < pendingFiles.length; i++) {
|
||||
formData.append('files', pendingFiles[i]);
|
||||
}
|
||||
formData.append('upload_token', uploadToken);
|
||||
|
||||
resetUploadProgressModal();
|
||||
uploadProgressModal.style.display = 'flex';
|
||||
fileUpload.disabled = true;
|
||||
|
||||
const eventSource = new EventSource(appUrl(`/api/orders/progress?upload_token=${encodeURIComponent(uploadToken)}`));
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.status !== 'idle') {
|
||||
setUploadProgress(data);
|
||||
}
|
||||
if (data.status === 'completed' || data.status === 'error') {
|
||||
eventSource.close();
|
||||
fileUpload.disabled = false;
|
||||
fileUpload.value = '';
|
||||
if (data.status === 'completed') {
|
||||
showToast(data.message || '업로드와 검색 인덱스 반영이 완료되었습니다.', 'success');
|
||||
} else {
|
||||
showToast(data.message || '업로드 실패', 'error');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload progress parse error', err);
|
||||
}
|
||||
};
|
||||
|
||||
formData.append('custom_date', calendarSelectedDate);
|
||||
|
||||
try {
|
||||
const res = await fetch(appUrl(`/api/orders/upload`), {
|
||||
const response = await fetch(appUrl(`/api/orders/upload`), {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
if (!['completed', 'error'].includes(latestUploadProgress?.status)) {
|
||||
setUploadProgress({
|
||||
...(latestUploadProgress || {}),
|
||||
status: data.status || 'queued',
|
||||
message: data.message || '업로드 작업이 시작되었습니다.'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let downloadFilename = '낱개상품출고.xls';
|
||||
const disposition = response.headers.get('Content-Disposition');
|
||||
if (disposition && disposition.indexOf('filename*=') !== -1) {
|
||||
const matches = disposition.split("filename*=UTF-8''");
|
||||
if (matches.length > 1) {
|
||||
downloadFilename = decodeURIComponent(matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
const fileBlob = await response.blob();
|
||||
const downloadUrl = window.URL.createObjectURL(fileBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = downloadUrl;
|
||||
a.download = downloadFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
|
||||
showToast('업로드 완료 및 출고 파일 다운로드 완료', 'success');
|
||||
uploadDateModal.classList.remove('show');
|
||||
} else {
|
||||
showToast(data.detail || 'Upload failed', 'error');
|
||||
setUploadProgress({
|
||||
status: 'error',
|
||||
message: data.detail || '업로드 실패'
|
||||
});
|
||||
eventSource.close();
|
||||
fileUpload.disabled = false;
|
||||
fileUpload.value = '';
|
||||
let errMsg = '업로드 분석 실패';
|
||||
try {
|
||||
const data = await response.json();
|
||||
errMsg = data.detail || errMsg;
|
||||
} catch(_) {}
|
||||
showToast(errMsg, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('네트워크 오류가 발생했습니다.', 'error');
|
||||
setUploadProgress({
|
||||
status: 'error',
|
||||
message: '네트워크 오류가 발생했습니다.'
|
||||
});
|
||||
eventSource.close();
|
||||
fileUpload.disabled = false;
|
||||
} finally {
|
||||
btnSubmitUploadDate.disabled = false;
|
||||
btnSubmitUploadDate.innerHTML = originalHtml;
|
||||
fileUpload.value = '';
|
||||
pendingFiles = null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user