document.addEventListener('DOMContentLoaded', () => {
const APP_BASE_PATH = (window.APP_BASE_PATH || '').replace(/\/$/, '');
const appUrl = (path) => {
const rawUrl = `${APP_BASE_PATH}${path.startsWith('/') ? path : `/${path}`}`;
return rawUrl.replace(/\/+/g, '/');
};
// --- Elements ---
// loginScreen / user-name 표시는 더 이상 존재하지 않음.
// (서버에서 미인증 사용자를 dbx-main 으로 리다이렉트하고, 이름 표시는 제거됨)
const searchInputs = {
name: document.getElementById('search-name'),
address: document.getElementById('search-address'),
phone: document.getElementById('search-phone'),
tracking: document.getElementById('search-tracking')
};
const searchStartDate = document.getElementById('search-start-date');
const searchEndDate = document.getElementById('search-end-date');
const btnSearch = document.getElementById('btn-search');
const btnResetSearch = document.getElementById('btn-reset-search');
const btnDownloadSearchResults = document.getElementById('btn-download-search-results');
const btnDataManagement = document.getElementById('btn-data-management');
const fileUpload = document.getElementById('file-upload');
const resultsBody = document.getElementById('results-body');
const resultCount = document.getElementById('result-count');
const dataManagementModal = document.getElementById('data-management-modal');
const dbSizeBadge = document.getElementById('db-size-badge');
const orderEditModal = document.getElementById('order-edit-modal');
const returnRequestModal = document.getElementById('return-request-modal');
const uploadProgressModal = document.getElementById('upload-progress-modal');
const btnCloseDataManagement = document.getElementById('data-management-close');
const btnCloseOrderEdit = document.getElementById('order-edit-close');
const btnCancelOrderEdit = document.getElementById('btn-cancel-order-edit');
const btnSaveOrderEdit = document.getElementById('btn-save-order-edit');
const btnDeleteOrderEdit = document.getElementById('btn-delete-order-edit');
const btnOpenReturnRequest = document.getElementById('btn-open-return-request');
const btnCloseReturnRequest = document.getElementById('return-request-close');
const btnCancelReturnRequest = document.getElementById('btn-cancel-return-request');
const btnSendReturnRequest = document.getElementById('btn-send-return-request');
const btnCloseUploadProgress = document.getElementById('btn-close-upload-progress');
const editRecipientName = document.getElementById('edit-recipient-name');
const editAddress = document.getElementById('edit-address');
const editRecipientMobile = document.getElementById('edit-recipient-mobile');
const editCustomerNote = document.getElementById('edit-customer-note');
const editOrderNote = document.getElementById('edit-order-note');
const returnRequestDate = document.getElementById('return-request-date');
const returnRequestType = document.getElementById('return-request-type');
const returnRequestTracking = document.getElementById('return-request-tracking');
const returnRequestRemarks = document.getElementById('return-request-remarks');
const uploadModalMessage = document.getElementById('upload-modal-message');
const uploadDataProgressText = document.getElementById('upload-data-progress-text');
const uploadDataProgressFill = document.getElementById('upload-data-progress-fill');
const uploadDataProgressCount = document.getElementById('upload-data-progress-count');
const uploadIndexProgressText = document.getElementById('upload-index-progress-text');
const uploadIndexProgressFill = document.getElementById('upload-index-progress-fill');
const uploadIndexProgressCount = document.getElementById('upload-index-progress-count');
const btnOptimizeDb = document.getElementById('btn-optimize-db');
const dbProgressContainer = document.getElementById('optimize-progress-container');
const dbProgressText = document.getElementById('optimize-progress-text');
const dbProgressFill = document.getElementById('optimize-progress-fill');
// DB Export Elements
const btnExportDb = document.getElementById('btn-export-db');
const exportStartDate = document.getElementById('export-start-date');
const exportEndDate = document.getElementById('export-end-date');
const exportAllDatesChk = document.getElementById('export-all-dates-chk');
const exportDateRangeContainer = document.getElementById('export-date-range-container');
const exportProgressContainer = document.getElementById('export-progress-container');
const exportProgressMessage = document.getElementById('export-progress-message');
const exportProgressText = document.getElementById('export-progress-text');
const exportProgressFill = document.getElementById('export-progress-fill');
const exportProgressCount = document.getElementById('export-progress-count');
const btnBackupDb = document.getElementById('btn-backup-db');
const btnDownloadEventLog = document.getElementById('btn-download-event-log');
const restoreDbFile = document.getElementById('restore-db-file');
const repeatStartDate = document.getElementById('repeat-start-date');
const repeatEndDate = document.getElementById('repeat-end-date');
const repeatMinCount = document.getElementById('repeat-min-count');
const btnDownloadRepeatPurchase = document.getElementById('btn-download-repeat-purchase');
const deleteStartDate = document.getElementById('delete-start-date');
const deleteEndDate = document.getElementById('delete-end-date');
const btnDeleteDateRangeOrders = document.getElementById('btn-delete-date-range-orders');
const deleteDateProgressContainer = document.getElementById('delete-date-progress-container');
const deleteDateProgressMessage = document.getElementById('delete-date-progress-message');
const deleteDateProgressText = document.getElementById('delete-date-progress-text');
const deleteDateProgressFill = document.getElementById('delete-date-progress-fill');
const deleteDateProgressCount = document.getElementById('delete-date-progress-count');
const deleteCleanupProgressMessage = document.getElementById('delete-cleanup-progress-message');
const deleteCleanupProgressText = document.getElementById('delete-cleanup-progress-text');
const deleteCleanupProgressFill = document.getElementById('delete-cleanup-progress-fill');
const btnDeleteAllOrders = document.getElementById('btn-delete-all-orders');
// State
let currentUser = null;
let latestUploadProgress = null;
let currentSearchPage = 1;
let editingOrderId = null;
let editingOrderData = null;
let highlightDateGroups = false;
let latestSearchParams = null;
let latestSearchTotal = 0;
// --- Initialization ---
checkAuth();
function logUiEvent(action, details = {}) {
fetch(appUrl(`/api/settings/log-ui-event`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, details })
}).catch(() => {});
}
async function loadDatabaseSize() {
if (!dbSizeBadge) return;
dbSizeBadge.textContent = '용량 확인 중';
const targetUrl = appUrl(`/api/settings/database-size`);
try {
const response = await fetch(targetUrl);
if (!response.ok) {
let errMsg = `HTTP ${response.status} ${response.statusText}`;
try {
const errData = await response.json();
errMsg += ` - ${errData.detail || ''}`;
} catch (_) {}
throw new Error(errMsg);
}
const data = await response.json();
dbSizeBadge.textContent = `DB ${data.formatted || '-'}`;
dbSizeBadge.title = data.database ? `DB 종류: ${data.database}` : 'DB 용량';
} catch (error) {
dbSizeBadge.textContent = '용량 확인 실패';
dbSizeBadge.title = error.message || 'DB 용량 조회 실패';
alert(`[디버그 정보]\n대상 URL: ${targetUrl}\nwindow.APP_BASE_PATH: ${window.APP_BASE_PATH}\n에러 내용: ${error.message}`);
}
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function todayKstDateString() {
try {
return new Intl.DateTimeFormat('sv-SE', {
timeZone: 'Asia/Seoul',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(new Date());
} catch (error) {
const now = new Date();
const localDate = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
return localDate.toISOString().slice(0, 10);
}
}
function trackingNumberOptions(value) {
const trackingText = String(value || '').trim();
if (!trackingText) return [];
const parts = trackingText
.split(/[\/,\n\r]+/)
.map((item) => item.trim())
.filter(Boolean);
return [...new Set(parts)];
}
async function copyTextToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
// --- Authentication ---
// 미인증 사용자는 서버(read_root) 가 이미 dbx-main 로그인으로 리다이렉트하므로
// 여기서는 is_admin 확인 + 세션 만료 시 다시 dbx-main 으로 보내는 역할만 한다.
async function checkAuth() {
try {
const res = await fetch(appUrl(`/api/users/me`));
if (res.ok) {
currentUser = await res.json();
if (btnDataManagement) {
btnDataManagement.style.display = currentUser.is_admin ? 'inline-flex' : 'none';
}
} else if (res.status === 401) {
// 세션 만료 — dbx-main 로그인으로 위임 (next 로 현재 위치 보존)
const next = encodeURIComponent((APP_BASE_PATH || '') + '/');
window.location.href = appUrl(`/login`) + '?next=' + next;
}
} catch (error) {
console.error('Auth check error', error);
}
}
// --- Upload Data ---
function resetUploadProgressModal() {
latestUploadProgress = null;
uploadModalMessage.textContent = '업로드를 준비하는 중입니다...';
uploadModalMessage.classList.remove('error', 'success');
uploadDataProgressFill.style.width = '0%';
uploadIndexProgressFill.style.width = '0%';
uploadDataProgressText.textContent = '0%';
uploadIndexProgressText.textContent = '0%';
uploadDataProgressCount.textContent = '0 / 0건';
uploadIndexProgressCount.textContent = '0 / 0건';
btnCloseUploadProgress.disabled = true;
}
function setUploadProgress(data) {
latestUploadProgress = {
...(latestUploadProgress || {}),
...data
};
data = latestUploadProgress;
const uploadPercent = Math.max(0, Math.min(100, Number(data.upload_progress || 0)));
const indexPercent = Math.max(0, Math.min(100, Number(data.index_progress || 0)));
const totalRows = Number(data.total_rows || 0);
const processedRows = Number(data.processed_rows || 0);
const indexedRows = Number(data.indexed_rows || 0);
uploadDataProgressFill.style.width = `${uploadPercent}%`;
uploadIndexProgressFill.style.width = `${indexPercent}%`;
uploadDataProgressText.textContent = `${Math.round(uploadPercent)}%`;
uploadIndexProgressText.textContent = `${Math.round(indexPercent)}%`;
uploadDataProgressCount.textContent = `${processedRows.toLocaleString()} / ${totalRows.toLocaleString()}건`;
uploadIndexProgressCount.textContent = `${indexedRows.toLocaleString()} / ${totalRows.toLocaleString()}건`;
uploadModalMessage.textContent = data.message || '처리 중입니다...';
if (data.status === 'completed') {
uploadModalMessage.classList.add('success');
uploadModalMessage.classList.remove('error');
btnCloseUploadProgress.disabled = false;
} else if (data.status === 'error') {
uploadModalMessage.classList.add('error');
uploadModalMessage.classList.remove('success');
btnCloseUploadProgress.disabled = false;
} else {
uploadModalMessage.classList.remove('error', 'success');
}
}
btnCloseUploadProgress.addEventListener('click', () => {
logUiEvent('업로드 진행 창 닫기', {
status: latestUploadProgress?.status || '',
total_rows: latestUploadProgress?.total_rows || 0
});
uploadProgressModal.style.display = 'none';
});
// --- 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;
pendingFiles = files;
// 기본 선택값: 오늘 날짜 (사용자가 명시적으로 다시 고를 수도 있음)
const today = new Date();
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();
uploadDateModal.classList.add('show');
});
btnSubmitUploadDate.addEventListener('click', async () => {
if (!pendingFiles || !calendarSelectedDate) return;
btnSubmitUploadDate.disabled = true;
const originalHtml = btnSubmitUploadDate.innerHTML;
btnSubmitUploadDate.innerHTML = ' 업로드 및 분석 중...';
const uploadToken = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
const formData = new FormData();
for (let i = 0; i < pendingFiles.length; i++) {
formData.append('files', pendingFiles[i]);
}
formData.append('upload_token', uploadToken);
formData.append('custom_date', calendarSelectedDate);
try {
const response = await fetch(appUrl(`/api/orders/upload`), {
method: 'POST',
body: formData
});
if (response.ok) {
let downloadFilename = '낱개상품출고.xlsx';
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 {
let errMsg = '업로드 분석 실패';
try {
const data = await response.json();
errMsg = data.detail || errMsg;
} catch(_) {}
showToast(errMsg, 'error');
}
} catch (error) {
showToast('네트워크 오류가 발생했습니다.', 'error');
} finally {
btnSubmitUploadDate.disabled = false;
btnSubmitUploadDate.innerHTML = originalHtml;
fileUpload.value = '';
pendingFiles = null;
}
});
// --- Search ---
btnSearch.addEventListener('click', () => {
highlightDateGroups = false;
performSearch(1);
});
btnResetSearch.addEventListener('click', () => {
highlightDateGroups = false;
Object.values(searchInputs).forEach(input => {
input.value = '';
});
if (searchStartDate) searchStartDate.value = '';
if (searchEndDate) searchEndDate.value = '';
latestSearchParams = null;
latestSearchTotal = 0;
if (btnDownloadSearchResults) {
btnDownloadSearchResults.disabled = true;
}
});
// Allow enter key to search
Object.values(searchInputs).forEach(input => {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
highlightDateGroups = false;
performSearch();
}
});
});
function getCurrentSearchParams() {
const params = new URLSearchParams();
const nameVal = searchInputs.name.value.trim();
const addressVal = searchInputs.address.value.trim();
const phoneVal = searchInputs.phone.value.trim();
const trackingVal = searchInputs.tracking.value.trim();
const startVal = searchStartDate ? searchStartDate.value : "";
const endVal = searchEndDate ? searchEndDate.value : "";
if (nameVal) params.append('name', nameVal);
if (addressVal) params.append('address', addressVal);
if (phoneVal) params.append('phone', phoneVal);
if (trackingVal) params.append('tracking', trackingVal);
if (startVal) params.append('order_start_date', startVal);
if (endVal) params.append('order_end_date', endVal);
return {
params,
hasFilters: Boolean(nameVal || addressVal || phoneVal || trackingVal || startVal || endVal)
};
}
async function performSearch(page = 1) {
currentSearchPage = page;
const searchState = getCurrentSearchParams();
if (!searchState.hasFilters) {
showToast('검색 조건을 하나 이상 입력해주세요.', 'error');
return;
}
try {
btnSearch.innerHTML = ' 검색중';
btnSearch.disabled = true;
const params = new URLSearchParams(searchState.params);
params.set('page', page);
params.set('size', 27);
const queryUrl = appUrl(`/api/orders/search?${params.toString()}`);
const res = await fetch(queryUrl);
if (res.ok) {
const data = await res.json();
latestSearchParams = new URLSearchParams(searchState.params);
latestSearchTotal = Number(data.total || 0);
if (btnDownloadSearchResults) {
btnDownloadSearchResults.disabled = latestSearchTotal === 0;
}
renderTable(data.items, data.total);
renderPagination(data.page, data.total_pages);
if (page === 1) {
showToast(`${data.total} 건이 검색되었습니다.`, 'success');
}
} else {
showToast('검색 실패', 'error');
}
} catch (error) {
showToast('검색 중 오류 발생', 'error');
} finally {
btnSearch.innerHTML = ' 검색';
btnSearch.disabled = false;
}
}
if (btnDownloadSearchResults) {
btnDownloadSearchResults.addEventListener('click', async () => {
if (!latestSearchParams || latestSearchTotal <= 0) {
showToast('다운로드할 검색 결과가 없습니다.', 'error');
return;
}
const originalText = btnDownloadSearchResults.innerHTML;
btnDownloadSearchResults.disabled = true;
btnDownloadSearchResults.innerHTML = ' 생성 중';
try {
const res = await fetch(appUrl(`/api/orders/export-search?${latestSearchParams.toString()}`));
if (res.ok) {
await downloadFileFromResponse(res, '검색결과.xlsx');
showToast('검색 결과 다운로드가 완료되었습니다.', 'success');
} else {
const data = await res.json();
showToast(data.detail || '검색 결과 다운로드 실패', 'error');
}
} catch (error) {
showToast('검색 결과 다운로드 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnDownloadSearchResults.disabled = latestSearchTotal === 0;
btnDownloadSearchResults.innerHTML = originalText;
}
});
}
function renderTable(orders, totalCount) {
resultCount.textContent = totalCount || orders.length;
if (orders.length === 0) {
resultsBody.innerHTML = `
| 검색 결과가 없습니다. |
`;
return;
}
resultsBody.innerHTML = '';
let previousDateKey = null;
let useAlternateDateShade = false;
orders.forEach(order => {
const tr = document.createElement('tr');
const pCode = order.product_code || '';
const pName = order.product_name || '';
// Date formatting logic "yy년 mm월 dd일"
let formattedDate = order.order_date || '';
if (formattedDate) {
// Try parsing YYYY-MM-DD
const parts = formattedDate.split('-');
if (parts.length >= 3) {
const year = parts[0].substring(2); // yy
const month = parts[1];
const day = parts[2].substring(0, 2); // in case there's time component
formattedDate = `${year}년 ${month}월 ${day}일`;
}
}
const dateKey = (order.order_date || '').slice(0, 10);
if (highlightDateGroups && dateKey !== previousDateKey) {
useAlternateDateShade = previousDateKey === null ? false : !useAlternateDateShade;
previousDateKey = dateKey;
}
if (highlightDateGroups && useAlternateDateShade) {
tr.classList.add('date-group-alt');
}
// Create a clickable tracking link if tracking number exists
let trackingCell = ` | `;
if (order.tracking_number) {
// Handle multiple tracking numbers separated by '/'
const trackingNumbers = order.tracking_number.split('/').map(t => t.trim()).filter(t => t);
const links = trackingNumbers.map(num => {
const trackingUrl = `https://www.cjlogistics.com/ko/tool/parcel/tracking?gnbInvcNo=${encodeURIComponent(num)}`;
return `${escapeHtml(num)}`;
});
// Join multiple links with a visual separator, e.g. ' / ' or '
'
trackingCell = `${links.join(' / ')} | `;
}
const orderNote = order.order_note || '';
const orderNoteIcon = orderNote
? `📝`
: '';
const orderNoLabel = `${orderNoteIcon}${escapeHtml(order.order_no_mall || '')}`;
let orderNoCell = `${orderNoLabel} | `;
if (order.vendor === '자사몰' && order.order_no_mall) {
const url = `https://miraskitchen.cafe24.com/admin/php/shop1/s_new/order_detail.php?order_id=${encodeURIComponent(order.order_no_mall)}`;
orderNoCell = `${orderNoteIcon}${escapeHtml(order.order_no_mall)} | `;
} else if (order.vendor === '스마트스토어' && order.order_no_mall) {
const url = `https://sell.smartstore.naver.com/#/naverpay/manage/order?detailSearch.type=ORDER_NO&detailSearch.keyword=${encodeURIComponent(order.order_no_mall)}`;
orderNoCell = `${orderNoteIcon}${escapeHtml(order.order_no_mall)} | `;
}
const vendorText = order.vendor || '';
const normalizedVendor = vendorText.replace(/\s/g, '');
let vendorCell = `${escapeHtml(vendorText)} | `;
if (normalizedVendor === '자사몰' || normalizedVendor === '가사몰') {
vendorCell = '자사몰 | ';
} else if (normalizedVendor === '스마트스토어') {
vendorCell = '스마트스토어 | ';
}
const countBadge = (count, title, searchType) => {
const numericCount = Number(count || 0);
if (numericCount <= 1) return '';
let levelClass = 'count-badge-neutral';
if (numericCount >= 10) {
levelClass = 'count-badge-danger';
} else if (numericCount >= 5) {
levelClass = 'count-badge-warning';
}
return `${numericCount}`;
};
const addressToCopy = order.address || '';
const addressToSearch = addressToCopy.trim();
const phoneToCopy = order.recipient_mobile || order.recipient_phone || '';
const phoneToSearch = phoneToCopy.trim();
const addressCountBadge = countBadge(
order.address_order_count,
'전체 데이터에서 같은 주소의 주문날짜',
'address'
);
const phoneCountBadge = countBadge(
order.phone_order_count,
'전체 데이터에서 같은 전화번호의 주문날짜',
'phone'
);
const customerNote = order.customer_note || '';
const noteIcon = customerNote
? `📝`
: '';
tr.innerHTML = `
${escapeHtml(formattedDate)} |
${noteIcon}${escapeHtml(order.recipient_name || '')} |
${escapeHtml(pCode)} |
${escapeHtml(pName)} |
${escapeHtml(order.order_quantity || '')} |
${escapeHtml(addressToCopy)}${addressCountBadge} |
${escapeHtml(phoneToCopy)}${phoneCountBadge} |
${trackingCell}
${vendorCell}
${orderNoCell}
${escapeHtml(order.order_list_1 || '')} |
`;
tr.querySelector('[data-edit-field="name"]').dataset.copy = order.recipient_name || '';
tr.querySelector('[data-edit-field="address"]').dataset.copy = addressToCopy;
tr.querySelector('[data-edit-field="phone"]').dataset.copy = phoneToCopy;
let copyTimer = null;
tr.addEventListener('dblclick', (e) => {
const editableCell = e.target.closest('.editable-contact');
if (!editableCell) return;
if (copyTimer) {
clearTimeout(copyTimer);
copyTimer = null;
}
e.preventDefault();
e.stopPropagation();
openOrderEditModal(order, editableCell.dataset.editField);
});
tr.addEventListener('click', (e) => {
if (e.target.tagName === 'A') return;
const countBadgeEl = e.target.closest('.count-badge');
if (countBadgeEl) {
e.preventDefault();
e.stopPropagation();
searchInputs.tracking.value = '';
if (searchStartDate) searchStartDate.value = '';
if (searchEndDate) searchEndDate.value = '';
if (countBadgeEl.dataset.searchType === 'address') {
searchInputs.name.value = order.recipient_name || '';
searchInputs.address.value = addressToSearch;
searchInputs.phone.value = '';
} else if (countBadgeEl.dataset.searchType === 'phone') {
searchInputs.name.value = '';
searchInputs.phone.value = phoneToSearch;
searchInputs.address.value = '';
}
highlightDateGroups = true;
performSearch(1);
return;
}
const copyableCell = e.target.closest('.copyable');
if (copyableCell) {
const textToCopy = copyableCell.getAttribute('data-copy');
if (textToCopy) {
if (copyTimer) clearTimeout(copyTimer);
copyTimer = setTimeout(() => {
copyTextToClipboard(textToCopy).then(() => {
showToast('클립보드에 복사되었습니다.', 'success');
}).catch(err => {
console.error('복사 실패:', err);
showToast('클립보드 복사에 실패했습니다.', 'error');
});
copyTimer = null;
}, 220);
}
}
});
resultsBody.appendChild(tr);
});
}
function closeOrderEditModal() {
if (!orderEditModal) return;
orderEditModal.classList.remove('show');
if (returnRequestModal) returnRequestModal.classList.remove('show');
editingOrderId = null;
editingOrderData = null;
}
function closeReturnRequestModal() {
if (!returnRequestModal) return;
returnRequestModal.classList.remove('show');
}
function openOrderEditModal(order, focusField) {
if (!orderEditModal) return;
editingOrderId = order.id;
editingOrderData = order;
editRecipientName.value = order.recipient_name || '';
editAddress.value = order.address || '';
editRecipientMobile.value = order.recipient_mobile || order.recipient_phone || '';
if (editCustomerNote) {
editCustomerNote.value = order.customer_note || '';
}
if (editOrderNote) {
editOrderNote.value = order.order_note || '';
}
orderEditModal.classList.add('show');
const focusTarget = {
name: editRecipientName,
address: editAddress,
phone: editRecipientMobile,
}[focusField] || editRecipientName;
setTimeout(() => {
focusTarget.focus();
focusTarget.select();
}, 0);
}
function openReturnRequestModal() {
if (!returnRequestModal || !editingOrderId) return;
if (returnRequestDate) returnRequestDate.value = todayKstDateString();
if (returnRequestType) returnRequestType.value = '단순변심';
if (returnRequestTracking) {
const trackingOptions = trackingNumberOptions(editingOrderData ? editingOrderData.tracking_number : '');
returnRequestTracking.innerHTML = '';
if (trackingOptions.length) {
trackingOptions.forEach((trackingNo) => {
const option = document.createElement('option');
option.value = trackingNo;
option.textContent = trackingNo;
returnRequestTracking.appendChild(option);
});
returnRequestTracking.disabled = trackingOptions.length <= 1;
} else {
const option = document.createElement('option');
option.value = '';
option.textContent = '송장번호 없음';
returnRequestTracking.appendChild(option);
returnRequestTracking.disabled = true;
}
}
if (returnRequestRemarks) returnRequestRemarks.value = '';
returnRequestModal.classList.add('show');
setTimeout(() => {
if (returnRequestRemarks) returnRequestRemarks.focus();
}, 0);
logUiEvent('반품 신청 팝업 열기', { order_id: editingOrderId });
}
async function sendReturnRequest() {
if (!editingOrderId || !btnSendReturnRequest) return;
const originalText = btnSendReturnRequest.innerHTML;
btnSendReturnRequest.disabled = true;
btnSendReturnRequest.innerHTML = ' 전송 중...';
const payload = {
request_date: returnRequestDate ? returnRequestDate.value : '',
request_type: returnRequestType ? returnRequestType.value : '단순변심',
receiver_name: editRecipientName ? editRecipientName.value : '',
address: editAddress ? editAddress.value : '',
phone: editRecipientMobile ? editRecipientMobile.value : '',
tracking_no: returnRequestTracking ? returnRequestTracking.value : '',
remarks: returnRequestRemarks ? returnRequestRemarks.value : '',
};
logUiEvent('반품 신청 전송 버튼 클릭', {
order_id: editingOrderId,
request_date: payload.request_date,
request_type: payload.request_type,
has_remarks: !!payload.remarks,
});
try {
const res = await fetch(appUrl(`/api/orders/${editingOrderId}/return-request`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
if (res.ok) {
showToast(data.message || '반품 신청 데이터가 전송되었습니다.', 'success');
closeOrderEditModal();
} else {
showToast(data.detail || '반품 신청 전송 실패', 'error');
}
} catch (error) {
showToast('반품 신청 전송 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnSendReturnRequest.disabled = false;
btnSendReturnRequest.innerHTML = originalText;
}
}
async function saveOrderEdit() {
if (!editingOrderId) return;
const originalText = btnSaveOrderEdit.innerHTML;
btnSaveOrderEdit.disabled = true;
btnSaveOrderEdit.innerHTML = ' 저장 중...';
try {
const res = await fetch(appUrl(`/api/orders/${editingOrderId}/contact`), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_name: editRecipientName.value,
address: editAddress.value,
recipient_mobile: editRecipientMobile.value,
customer_note: editCustomerNote ? editCustomerNote.value : '',
order_note: editOrderNote ? editOrderNote.value : ''
})
});
const data = await res.json();
if (res.ok) {
showToast(data.message || '주문 정보가 수정되었습니다.', 'success');
closeOrderEditModal();
await performSearch(currentSearchPage);
} else {
showToast(data.detail || '주문 정보 수정 실패', 'error');
}
} catch (error) {
showToast('주문 정보 수정 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnSaveOrderEdit.disabled = false;
btnSaveOrderEdit.innerHTML = originalText;
}
}
async function deleteCurrentOrder() {
if (!editingOrderId || !btnDeleteOrderEdit) return;
const deletePassword = prompt('이 주문을 삭제하려면 비밀번호를 입력하세요.');
if (deletePassword === null) {
logUiEvent('주문 단건 삭제 취소', { order_id: editingOrderId, step: 'password_prompt' });
return;
}
if (deletePassword !== '1225') {
logUiEvent('주문 단건 삭제 암호 오류', { order_id: editingOrderId });
showToast('비밀번호가 올바르지 않습니다.', 'error');
return;
}
if (!confirm('이 주문을 삭제하시겠습니까? 삭제 후 되돌릴 수 없습니다.')) {
logUiEvent('주문 단건 삭제 취소', { order_id: editingOrderId, step: 'confirm' });
return;
}
const orderIdToDelete = editingOrderId;
const originalText = btnDeleteOrderEdit.innerHTML;
btnDeleteOrderEdit.disabled = true;
btnDeleteOrderEdit.innerHTML = ' 삭제 중...';
logUiEvent('주문 단건 삭제 요청', { order_id: orderIdToDelete });
try {
const res = await fetch(appUrl(`/api/orders/${orderIdToDelete}/delete`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: deletePassword })
});
const data = await res.json();
if (res.ok) {
showToast(data.message || '주문이 삭제되었습니다.', 'success');
closeOrderEditModal();
await performSearch(currentSearchPage);
} else {
showToast(data.detail || '주문 삭제 실패', 'error');
}
} catch (error) {
showToast('주문 삭제 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnDeleteOrderEdit.disabled = false;
btnDeleteOrderEdit.innerHTML = originalText;
}
}
function renderPagination(currentPage, totalPages) {
const container = document.getElementById('pagination-container');
if (!container) return;
container.innerHTML = '';
if (totalPages <= 1) return;
const pageGroupSize = 50;
const groupStart = Math.floor((currentPage - 1) / pageGroupSize) * pageGroupSize + 1;
const groupEnd = Math.min(groupStart + pageGroupSize - 1, totalPages);
if (groupStart > 1) {
const prevBtn = document.createElement('button');
prevBtn.className = 'btn-page btn-page-nav';
prevBtn.textContent = '<<이전 페이지';
prevBtn.addEventListener('click', () => {
performSearch(groupStart - pageGroupSize);
});
container.appendChild(prevBtn);
}
for (let i = groupStart; i <= groupEnd; i++) {
const btn = document.createElement('button');
btn.className = `btn-page ${i === currentPage ? 'active' : ''}`;
btn.textContent = i;
btn.addEventListener('click', () => {
performSearch(i);
});
container.appendChild(btn);
}
if (groupEnd < totalPages) {
const nextBtn = document.createElement('button');
nextBtn.className = 'btn-page btn-page-nav';
nextBtn.textContent = '다음 페이지>>';
nextBtn.addEventListener('click', () => {
performSearch(groupEnd + 1);
});
container.appendChild(nextBtn);
}
}
if (btnDataManagement) {
btnDataManagement.addEventListener('click', () => {
dataManagementModal.classList.add('show');
loadDatabaseSize();
logUiEvent('데이터 관리 탭 열기');
});
}
if (btnCloseDataManagement) {
btnCloseDataManagement.addEventListener('click', () => {
dataManagementModal.classList.remove('show');
logUiEvent('데이터 관리 탭 닫기', { close_type: 'button' });
});
}
if (btnCloseOrderEdit) {
btnCloseOrderEdit.addEventListener('click', closeOrderEditModal);
}
if (btnCancelOrderEdit) {
btnCancelOrderEdit.addEventListener('click', closeOrderEditModal);
}
if (btnSaveOrderEdit) {
btnSaveOrderEdit.addEventListener('click', saveOrderEdit);
}
if (btnDeleteOrderEdit) {
btnDeleteOrderEdit.addEventListener('click', deleteCurrentOrder);
}
if (btnOpenReturnRequest) {
btnOpenReturnRequest.addEventListener('click', openReturnRequestModal);
}
if (btnCloseReturnRequest) {
btnCloseReturnRequest.addEventListener('click', closeReturnRequestModal);
}
if (btnCancelReturnRequest) {
btnCancelReturnRequest.addEventListener('click', closeReturnRequestModal);
}
if (btnSendReturnRequest) {
btnSendReturnRequest.addEventListener('click', sendReturnRequest);
}
window.addEventListener('click', (e) => {
if (e.target === dataManagementModal) {
dataManagementModal.classList.remove('show');
logUiEvent('데이터 관리 탭 닫기', { close_type: 'backdrop' });
}
if (e.target === orderEditModal) {
closeOrderEditModal();
}
if (e.target === returnRequestModal) {
closeReturnRequestModal();
}
if (e.target === uploadDateModal) {
closeUploadDateModal();
}
});
if (btnOptimizeDb) {
btnOptimizeDb.addEventListener('click', async () => {
if (!confirm('데이터베이스 최적화를 시작하시겠습니까?\n이 작업은 데이터 용량에 따라 몇 초에서 수십 초가 소요될 수 있습니다.')) return;
logUiEvent('데이터베이스 최적화 요청');
btnOptimizeDb.disabled = true;
btnOptimizeDb.innerHTML = ' 최적화 중...';
dbProgressContainer.style.display = 'block';
dbProgressFill.style.width = '0%';
dbProgressText.textContent = '0%';
let progress = 0;
// Fake progress that approaches 95%
const progressInterval = setInterval(() => {
progress += (95 - progress) * 0.15;
dbProgressFill.style.width = `${progress}%`;
dbProgressText.textContent = `${Math.round(progress)}%`;
}, 500);
try {
const res = await fetch(appUrl(`/api/settings/optimize`), {
method: 'POST'
});
const data = await res.json();
clearInterval(progressInterval);
if (res.ok) {
dbProgressFill.style.width = '100%';
dbProgressText.textContent = '100%';
showToast(data.message || '최적화가 완료되었습니다.', 'success');
loadDatabaseSize();
} else {
showToast(data.detail || '최적화 실패', 'error');
}
} catch (error) {
clearInterval(progressInterval);
showToast('네트워크 오류', 'error');
} finally {
btnOptimizeDb.disabled = false;
btnOptimizeDb.innerHTML = ' 데이터베이스 최적화 (용량 줄이기)';
setTimeout(() => {
dbProgressContainer.style.display = 'none';
dbProgressFill.style.width = '0%';
dbProgressText.textContent = '0%';
}, 3000);
}
});
}
async function downloadFileFromResponse(res, fallbackFilename) {
let filename = fallbackFilename;
const disposition = res.headers.get('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/);
const plainMatch = disposition.match(/filename="?([^";]+)"?/);
if (utf8Match && utf8Match[1]) {
filename = decodeURIComponent(utf8Match[1]);
} else if (plainMatch && plainMatch[1]) {
filename = plainMatch[1];
}
}
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}, 1000);
}
async function getErrorMessageFromResponse(res, fallbackMessage) {
try {
const contentType = res.headers.get('Content-Type') || '';
if (contentType.includes('application/json')) {
const data = await res.json();
return data.detail || data.message || fallbackMessage;
}
const text = await res.text();
return text || fallbackMessage;
} catch (error) {
return fallbackMessage;
}
}
if (btnBackupDb) {
btnBackupDb.addEventListener('click', async () => {
logUiEvent('DB 백업 다운로드 요청');
const originalText = btnBackupDb.innerHTML;
btnBackupDb.disabled = true;
btnBackupDb.innerHTML = ' 백업 생성 중...';
try {
const res = await fetch(appUrl(`/api/settings/backup-db`));
if (res.ok) {
await downloadFileFromResponse(res, 'OMS_DB_backup.db');
showToast('DB 백업 다운로드가 완료되었습니다.', 'success');
} else {
const data = await res.json();
showToast(data.detail || 'DB 백업 실패', 'error');
}
} catch (error) {
showToast('DB 백업 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnBackupDb.disabled = false;
btnBackupDb.innerHTML = originalText;
}
});
}
if (btnDownloadEventLog) {
btnDownloadEventLog.addEventListener('click', async () => {
logUiEvent('이벤트 로그 다운로드 요청');
const originalText = btnDownloadEventLog.innerHTML;
btnDownloadEventLog.disabled = true;
btnDownloadEventLog.innerHTML = ' 로그 생성 중...';
try {
const res = await fetch(appUrl(`/api/settings/download-log`));
if (res.ok) {
await downloadFileFromResponse(res, 'OMS_event_log.csv');
showToast('이벤트 로그 다운로드가 완료되었습니다.', 'success');
} else {
const message = await getErrorMessageFromResponse(res, '로그 다운로드 실패');
showToast(message, 'error');
}
} catch (error) {
showToast(`로그 다운로드 오류: ${error.message || '응답 처리 실패'}`, 'error');
} finally {
btnDownloadEventLog.disabled = false;
btnDownloadEventLog.innerHTML = originalText;
}
});
}
if (btnDownloadRepeatPurchase) {
btnDownloadRepeatPurchase.addEventListener('click', async () => {
const startDate = repeatStartDate ? repeatStartDate.value : '';
const endDate = repeatEndDate ? repeatEndDate.value : '';
const minCount = Number(repeatMinCount ? repeatMinCount.value : 3);
if (!startDate || !endDate) {
showToast('반복 구매 조회 시작일과 종료일을 모두 선택해 주세요.', 'error');
return;
}
if (startDate > endDate) {
showToast('시작일이 종료일보다 늦을 수 없습니다.', 'error');
return;
}
if (!Number.isInteger(minCount) || minCount < 1) {
showToast('구매 회수는 1 이상의 숫자로 입력해 주세요.', 'error');
return;
}
logUiEvent('반복 구매 리스트 다운로드 요청', {
start_date: startDate,
end_date: endDate,
min_count: minCount
});
const originalText = btnDownloadRepeatPurchase.innerHTML;
btnDownloadRepeatPurchase.disabled = true;
btnDownloadRepeatPurchase.innerHTML = ' 파일 생성 중...';
try {
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate,
min_count: String(minCount)
});
const res = await fetch(appUrl(`/api/settings/repeat-purchase-export?${params.toString()}`));
if (res.ok) {
await downloadFileFromResponse(res, '반복구매.xlsx');
showToast('반복 구매 리스트 다운로드가 완료되었습니다.', 'success');
} else {
const data = await res.json();
showToast(data.detail || '반복 구매 리스트 다운로드 실패', 'error');
}
} catch (error) {
showToast('반복 구매 리스트 다운로드 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnDownloadRepeatPurchase.disabled = false;
btnDownloadRepeatPurchase.innerHTML = originalText;
}
});
}
if (restoreDbFile) {
restoreDbFile.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
logUiEvent('DB 복구 파일 선택', {
filename: file.name,
size: file.size
});
if (!confirm(`백업 파일 "${file.name}"로 DB를 복구하시겠습니까?\n현재 DB는 자동 안전백업 후 교체됩니다.`)) {
logUiEvent('DB 복구 취소', { filename: file.name, step: 'first_confirm' });
restoreDbFile.value = '';
return;
}
if (!confirm('복구가 완료되면 현재 검색 결과는 초기화됩니다. 계속하시겠습니까?')) {
logUiEvent('DB 복구 취소', { filename: file.name, step: 'second_confirm' });
restoreDbFile.value = '';
return;
}
logUiEvent('DB 복구 요청', { filename: file.name });
const formData = new FormData();
formData.append('file', file);
const restoreLabel = document.querySelector('label[for="restore-db-file"]');
const originalText = restoreLabel ? restoreLabel.innerHTML : '';
restoreDbFile.disabled = true;
if (restoreLabel) {
restoreLabel.innerHTML = ' 복구 중...';
}
try {
const res = await fetch(appUrl(`/api/settings/restore-db`), {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok) {
showToast(`${data.restored_count}건의 주문 데이터가 복구되었습니다.`, 'success');
resultsBody.innerHTML = `| DB가 복구되었습니다. 다시 검색해주세요. |
`;
resultCount.textContent = '0';
dataManagementModal.classList.remove('show');
} else {
showToast(data.detail || 'DB 복구 실패', 'error');
}
} catch (error) {
showToast('DB 복구 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
restoreDbFile.disabled = false;
restoreDbFile.value = '';
if (restoreLabel) {
restoreLabel.innerHTML = originalText;
}
}
});
}
function resetDeleteDateProgress() {
if (!deleteDateProgressContainer) return;
deleteDateProgressContainer.style.display = 'block';
deleteDateProgressMessage.textContent = '삭제 준비 중...';
deleteDateProgressText.textContent = '0%';
deleteDateProgressFill.style.width = '0%';
deleteDateProgressCount.textContent = '0 / 0건';
if (deleteCleanupProgressMessage) deleteCleanupProgressMessage.textContent = '정리 대기';
if (deleteCleanupProgressText) deleteCleanupProgressText.textContent = '0%';
if (deleteCleanupProgressFill) deleteCleanupProgressFill.style.width = '0%';
}
function setDeleteDateProgress(data) {
if (!deleteDateProgressContainer) return;
const progress = Math.max(0, Math.min(100, Number(data.progress || 0)));
const cleanupProgress = Math.max(0, Math.min(100, Number(data.cleanup_progress || 0)));
const totalRows = Number(data.total_rows || 0);
const deletedRows = Number(data.deleted_rows || 0);
deleteDateProgressContainer.style.display = 'block';
deleteDateProgressMessage.textContent = progress < 100 ? (data.message || '삭제 중입니다...') : '삭제 완료';
deleteDateProgressText.textContent = `${Math.round(progress)}%`;
deleteDateProgressFill.style.width = `${progress}%`;
deleteDateProgressCount.textContent = `${deletedRows.toLocaleString()} / ${totalRows.toLocaleString()}건`;
if (deleteCleanupProgressMessage) {
deleteCleanupProgressMessage.textContent = data.cleanup_step || (cleanupProgress > 0 ? data.message : '정리 대기');
}
if (deleteCleanupProgressText) {
deleteCleanupProgressText.textContent = `${Math.round(cleanupProgress)}%`;
}
if (deleteCleanupProgressFill) {
deleteCleanupProgressFill.style.width = `${cleanupProgress}%`;
}
}
if (btnDeleteDateRangeOrders) {
btnDeleteDateRangeOrders.addEventListener('click', async () => {
const startDate = deleteStartDate ? deleteStartDate.value : '';
const endDate = deleteEndDate ? deleteEndDate.value : '';
if (!startDate || !endDate) {
showToast('삭제할 시작일과 종료일을 모두 선택해 주세요.', 'error');
return;
}
if (startDate > endDate) {
showToast('시작일이 종료일보다 늦을 수 없습니다.', 'error');
return;
}
const deletePassword = prompt('지정한 날짜 범위의 주문 데이터를 삭제하려면 비밀번호를 입력하세요.');
if (deletePassword === null) return;
if (deletePassword !== '1225') {
showToast('비밀번호가 올바르지 않습니다.', 'error');
logUiEvent('날짜 범위 주문 데이터 삭제 비밀번호 오류', {
start_date: startDate,
end_date: endDate
});
return;
}
if (!confirm(`비밀번호가 확인되었습니다.\n${startDate} ~ ${endDate} 주문 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) {
logUiEvent('날짜 범위 주문 데이터 삭제 취소', {
start_date: startDate,
end_date: endDate
});
return;
}
logUiEvent('날짜 범위 주문 데이터 삭제 요청', {
start_date: startDate,
end_date: endDate
});
const originalText = btnDeleteDateRangeOrders.innerHTML;
const deleteToken = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
let deleteEventSource = null;
resetDeleteDateProgress();
btnDeleteDateRangeOrders.disabled = true;
btnDeleteDateRangeOrders.innerHTML = ' 범위 삭제 중...';
try {
deleteEventSource = new EventSource(appUrl(`/api/settings/delete-progress?delete_token=${encodeURIComponent(deleteToken)}`));
deleteEventSource.onmessage = (event) => {
try {
const progressData = JSON.parse(event.data);
if (progressData.status !== 'idle') {
setDeleteDateProgress(progressData);
}
if (['completed', 'error'].includes(progressData.status)) {
deleteEventSource.close();
}
} catch (err) {
console.error('Delete progress parse error', err);
}
};
deleteEventSource.onerror = () => {
if (deleteEventSource) deleteEventSource.close();
};
const res = await fetch(appUrl(`/api/settings/delete-orders-by-date-range`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
password: deletePassword,
start_date: startDate,
end_date: endDate,
delete_token: deleteToken
})
});
const data = await res.json();
if (res.ok) {
setDeleteDateProgress({
status: 'completed',
message: '삭제가 완료되었습니다.',
progress: 100,
cleanup_progress: 100,
total_rows: data.deleted_count,
deleted_rows: data.deleted_count,
cleanup_step: '정리 완료'
});
showToast(`${data.deleted_count}건의 주문 데이터가 삭제되었습니다.`, 'success');
resultsBody.innerHTML = `| ${startDate} ~ ${endDate} 주문 데이터가 삭제되었습니다. 다시 검색해 주세요. |
`;
resultCount.textContent = '0';
dataManagementModal.classList.remove('show');
} else {
showToast(data.detail || '날짜 범위 삭제 실패', 'error');
}
} catch (error) {
showToast('날짜 범위 삭제 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
if (deleteEventSource) deleteEventSource.close();
btnDeleteDateRangeOrders.disabled = false;
btnDeleteDateRangeOrders.innerHTML = originalText;
}
});
}
if (btnDeleteAllOrders) {
btnDeleteAllOrders.addEventListener('click', async () => {
const deletePassword = prompt('전체 주문 데이터를 삭제하려면 비밀번호를 입력하세요.');
if (deletePassword === null) return;
if (deletePassword !== '1225') {
showToast('비밀번호가 올바르지 않습니다.', 'error');
logUiEvent('전체 주문 데이터 삭제 비밀번호 오류');
return;
}
if (!confirm('비밀번호가 확인되었습니다.\n전체 주문 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.')) {
logUiEvent('전체 주문 데이터 삭제 취소');
return;
}
logUiEvent('전체 주문 데이터 삭제 요청');
const originalText = btnDeleteAllOrders.innerHTML;
btnDeleteAllOrders.disabled = true;
btnDeleteAllOrders.innerHTML = ' 전체 삭제 중...';
try {
const res = await fetch(appUrl(`/api/settings/delete-all-orders`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: deletePassword })
});
const data = await res.json();
if (res.ok) {
showToast(`${data.deleted_count}건의 전체 주문 데이터가 삭제되었습니다.`, 'success');
resultsBody.innerHTML = `| 전체 주문 데이터가 삭제되었습니다. |
`;
resultCount.textContent = '0';
dataManagementModal.classList.remove('show');
} else {
showToast(data.detail || '전체 삭제 실패', 'error');
}
} catch (error) {
showToast('전체 삭제 중 네트워크 오류가 발생했습니다.', 'error');
} finally {
btnDeleteAllOrders.disabled = false;
btnDeleteAllOrders.innerHTML = originalText;
}
});
}
if (exportAllDatesChk) {
exportAllDatesChk.addEventListener('change', (e) => {
logUiEvent('DB 데이터 엑셀 다운로드 전체 날짜 옵션 변경', {
checked: e.target.checked
});
if (e.target.checked) {
exportDateRangeContainer.style.opacity = '0.4';
exportStartDate.disabled = true;
exportEndDate.disabled = true;
} else {
exportDateRangeContainer.style.opacity = '1';
exportStartDate.disabled = false;
exportEndDate.disabled = false;
}
});
}
function resetExportProgress() {
if (!exportProgressContainer) return;
exportProgressContainer.style.display = 'block';
exportProgressMessage.textContent = '파일 생성 준비 중...';
exportProgressText.textContent = '0%';
exportProgressFill.style.width = '0%';
exportProgressCount.textContent = '0 / 0건';
}
function setExportProgress(data) {
if (!exportProgressContainer) return;
const processedRows = Number(data.processed_rows || 0);
const totalRows = Number(data.total_rows || 0);
const progress = Math.max(0, Math.min(100, Number(data.progress || 0)));
exportProgressContainer.style.display = 'block';
exportProgressMessage.textContent = data.message || '파일 생성 중...';
exportProgressText.textContent = `${Math.round(progress)}%`;
exportProgressFill.style.width = `${progress}%`;
exportProgressCount.textContent = `${processedRows.toLocaleString()} / ${totalRows.toLocaleString()}건`;
}
if (btnExportDb) {
btnExportDb.addEventListener('click', async () => {
const isAllDates = exportAllDatesChk.checked;
const startDate = exportStartDate.value;
const endDate = exportEndDate.value;
if (!isAllDates && (!startDate || !endDate)) {
showToast('시작일과 종료일을 모두 선택해주세요.', 'error');
return;
}
if (!isAllDates && startDate > endDate) {
showToast('시작일이 종료일보다 클 수 없습니다.', 'error');
return;
}
logUiEvent('DB 데이터 엑셀 다운로드 요청', {
all_dates: isAllDates,
start_date: isAllDates ? '' : startDate,
end_date: isAllDates ? '' : endDate
});
btnExportDb.disabled = true;
const originalText = btnExportDb.innerHTML;
btnExportDb.innerHTML = ' 파일 생성 및 다운로드 중...';
resetExportProgress();
try {
// Determine query parameters
const params = new URLSearchParams();
if (!isAllDates) {
params.append('start_date', startDate);
params.append('end_date', endDate);
}
const startRes = await fetch(appUrl(`/api/settings/export-db/start?${params.toString()}`), {
method: 'POST'
});
if (!startRes.ok) {
const message = await getErrorMessageFromResponse(startRes, '엑셀 다운로드 작업 시작 실패');
showToast(message, 'error');
return;
}
const startData = await startRes.json();
const exportToken = startData.export_token;
if (!exportToken) {
throw new Error('다운로드 작업 번호를 받지 못했습니다.');
}
let statusData = startData;
for (;;) {
await new Promise(resolve => setTimeout(resolve, 2000));
const statusRes = await fetch(appUrl(`/api/settings/export-db/status?export_token=${encodeURIComponent(exportToken)}`));
if (!statusRes.ok) {
const message = await getErrorMessageFromResponse(statusRes, '엑셀 다운로드 상태 확인 실패');
showToast(message, 'error');
return;
}
statusData = await statusRes.json();
setExportProgress(statusData);
const processedRows = Number(statusData.processed_rows || 0);
const processedText = processedRows > 0 ? ` (${processedRows.toLocaleString()}건)` : '';
btnExportDb.innerHTML = ` ${statusData.message || '파일 생성 중...'}${processedText}`;
if (statusData.status === 'completed') {
break;
}
if (statusData.status === 'error') {
showToast(statusData.message || '엑셀 파일 생성 실패', 'error');
return;
}
}
const downloadRes = await fetch(appUrl(`/api/settings/export-db/download?export_token=${encodeURIComponent(exportToken)}`));
if (downloadRes.ok) {
await downloadFileFromResponse(downloadRes, 'DB_Export.xlsx');
setExportProgress({
...statusData,
status: 'completed',
message: '다운로드가 완료되었습니다.',
progress: 100
});
showToast('데이터베이스 다운로드가 완료되었습니다.', 'success');
} else {
const message = await getErrorMessageFromResponse(downloadRes, '엑셀 파일 다운로드 실패');
showToast(message, 'error');
}
} catch (error) {
console.error(error);
showToast(`엑셀 다운로드 오류: ${error.message || '응답 처리 실패'}`, 'error');
} finally {
btnExportDb.disabled = false;
btnExportDb.innerHTML = originalText;
}
});
}
// --- Toast Notifications ---
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
let icon = type === 'success' ? '' : '';
toast.innerHTML = `${icon} ${message}`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(100%)';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// --- Column Resizing ---
function initResizers() {
const table = document.getElementById('results-table');
const cols = table.querySelectorAll('th');
[].forEach.call(cols, function (th) {
const resizer = th.querySelector('.resizer');
if (!resizer) return;
// Make the th position sticky if needed, and relative for its children
// The CSS already handles position: sticky and position: relative
let x = 0;
let w = 0;
const mouseDownHandler = function (e) {
// Get the current mouse position
x = e.clientX;
// Calculate the current width of column
const styles = window.getComputedStyle(th);
w = parseInt(styles.width, 10);
// Attach listeners for document's events
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
resizer.classList.add('resizing');
};
const mouseMoveHandler = function (e) {
// Determine how far the mouse has been moved
const dx = e.clientX - x;
// Update the width of column
th.style.width = `${w + dx}px`;
th.style.minWidth = `${w + dx}px`;
th.style.maxWidth = `${w + dx}px`;
};
const mouseUpHandler = function () {
// Remove the handlers of `mousemove` and `mouseup`
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
resizer.classList.remove('resizing');
};
resizer.addEventListener('mousedown', mouseDownHandler);
});
}
// Initialize resizers once when DOM is loaded
initResizers();
});