[260222]
필터 로직 개선 최종 스크립트 코드 정리
This commit is contained in:
465
scripts/main.js
465
scripts/main.js
@@ -1,36 +1,42 @@
|
||||
/** 진입점: 이벤트 바인딩·초기 렌더·URL 모달 처리 */
|
||||
/**
|
||||
* main.js
|
||||
* 앱 진입점: 전역 상태 초기화, 공통 이벤트 바인딩, 뷰 제어 및 유틸리티 기능
|
||||
*/
|
||||
|
||||
import { state, productsData, saveSelection } from './state.js';
|
||||
import { applyFilters, renderStatusChips, renderCategoryChips, bindCategoryFilter, renderTagChips } from './filters.js';
|
||||
import { applyFilters, renderStatusChips, renderCategoryChips, renderTagChips, setupTagToggle } from './filters.js';
|
||||
import { ITEMS_PER_PAGE, STATUS_META } from './config.js';
|
||||
import { renderProducts, changePage } from './productList.js';
|
||||
import { openModal, closeModal } from './modal.js';
|
||||
import { scrollToImage } from './carousel.js';
|
||||
|
||||
console.log('Total products loaded:', productsData.length);
|
||||
// ==========================================================================
|
||||
// 1. 전역 설정 및 윈도우 객체 등록 (Global Setup)
|
||||
// ==========================================================================
|
||||
|
||||
let lastThumbnailIndex = -1;
|
||||
|
||||
// HTML onclick에서 사용하기 위한 전역 등록
|
||||
// HTML 내 inline onclick 이벤트를 위한 전역 등록
|
||||
window.openModal = openModal;
|
||||
window.closeModal = closeModal;
|
||||
window.changePage = changePage;
|
||||
window.scrollToImage = scrollToImage;
|
||||
|
||||
let lastThumbnailIndex = -1;
|
||||
let currentHoverIndex = -1;
|
||||
let fadeTimers = {};
|
||||
let touchStartX = 0;
|
||||
let isDragging = false;
|
||||
|
||||
// 뷰 전환 이벤트
|
||||
// 뷰 전환 이벤트 바인딩
|
||||
document.getElementById('view-grid').onclick = () => {
|
||||
state.viewMode = 'grid';
|
||||
updateViewButtons();
|
||||
renderProducts(state.currentPage);
|
||||
};
|
||||
document.getElementById('view-table').onclick = () => {
|
||||
state.viewMode = 'table';
|
||||
updateViewButtons();
|
||||
renderProducts(state.currentPage);
|
||||
};
|
||||
// 로그용 유니크 ID 생성
|
||||
const sessionId = Math.random().toString(36).substring(2, 10);
|
||||
console.log(`%c[SESSION]: ${sessionId}`, 'color: #137fec; font-weight: bold; border: 1px solid #137fec; padding: 2px 5px; border-radius: 4px;');
|
||||
|
||||
// ==========================================================================
|
||||
// 2. 뷰(모드) 제어 로직 (View Mode)
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 뷰 전환 버튼 상태 업데이트 (Grid/Table)
|
||||
*/
|
||||
function updateViewButtons() {
|
||||
const isGrid = state.viewMode === 'grid';
|
||||
const gridBtn = document.getElementById('view-grid');
|
||||
@@ -54,15 +60,25 @@ function updateViewButtons() {
|
||||
}
|
||||
}
|
||||
|
||||
// [전역 함수] 체크박스 토글 및 합계 업데이트
|
||||
window.toggleSelectItem = (id) => {
|
||||
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
|
||||
else state.selectedIds.add(id);
|
||||
|
||||
updateSummary();
|
||||
document.getElementById('view-grid').onclick = () => {
|
||||
state.viewMode = 'grid';
|
||||
updateViewButtons();
|
||||
renderProducts(state.currentPage);
|
||||
};
|
||||
|
||||
/** 선택 요약 바 업데이트 (모바일 레이아웃 대응) */
|
||||
document.getElementById('view-table').onclick = () => {
|
||||
state.viewMode = 'table';
|
||||
updateViewButtons();
|
||||
renderProducts(state.currentPage);
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// 3. 선택 및 요약 로직 (Selection & Summary)
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 선택 요약 바 업데이트 (개수 및 총 가격)
|
||||
*/
|
||||
export function updateSummary() {
|
||||
const summaryBar = document.getElementById('selection-summary');
|
||||
const countEl = document.getElementById('selected-count');
|
||||
@@ -70,7 +86,7 @@ export function updateSummary() {
|
||||
|
||||
if (state.selectedIds.size > 0) {
|
||||
summaryBar.classList.remove('hidden');
|
||||
summaryBar.classList.add('flex'); // flex-wrap은 HTML에 이미 적용됨
|
||||
summaryBar.classList.add('flex');
|
||||
countEl.textContent = state.selectedIds.size;
|
||||
|
||||
const total = Array.from(state.selectedIds).reduce((sum, id) => {
|
||||
@@ -84,125 +100,23 @@ export function updateSummary() {
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 입력
|
||||
const searchInput = document.getElementById('search-input');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
state.searchKeyword = e.target.value.trim().toLowerCase();
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 개별 아이템 선택 토글
|
||||
*/
|
||||
window.toggleSelectItem = (id) => {
|
||||
const product = productsData.find((p) => p.id === id);
|
||||
if (!product || STATUS_META[product.status]?.selectable === false) return;
|
||||
|
||||
// Escape로 모달 닫기
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
const modal = document.getElementById('product-modal');
|
||||
if (!modal || modal.classList.contains('hidden')) return;
|
||||
closeModal();
|
||||
});
|
||||
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
|
||||
else state.activeStatuses.has(product.status) && state.selectedIds.add(id);
|
||||
|
||||
function checkUrlAndOpenModal() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const productId = params.get('id');
|
||||
if (productId) {
|
||||
const product = productsData.find((p) => String(p.id) === productId);
|
||||
if (product) {
|
||||
setTimeout(() => openModal(product.id), 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 초기 테마 설정 확인
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
// 1. 현재 테마 상태에 따라 아이콘 표시/숨김 처리
|
||||
function updateIcons() {
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
themeToggleDarkIcon.classList.add('hidden');
|
||||
} else {
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
themeToggleLightIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 초기 로드 시 설정 (localStorage 또는 시스템 설정 확인)
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
updateIcons();
|
||||
|
||||
// 3. 버튼 클릭 이벤트
|
||||
themeToggleBtn.addEventListener('click', function () {
|
||||
// 테마 토글
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
updateIcons();
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('Total products loaded:', productsData.length);
|
||||
|
||||
// 1. UI 컴포넌트 렌더링
|
||||
renderCategoryChips(productsData);
|
||||
bindCategoryFilter(productsData);
|
||||
renderStatusChips();
|
||||
renderTagChips();
|
||||
|
||||
// 2. 초기 데이터 계산 및 첫 페이지 렌더링 (순서 중요)
|
||||
applyFilters();
|
||||
renderProducts(state.currentPage);
|
||||
updateViewButtons();
|
||||
saveSelection();
|
||||
updateSummary();
|
||||
};
|
||||
|
||||
// 테마 설정 (기존 로직 유지)
|
||||
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
updateIcons();
|
||||
|
||||
// 3. 태그 토글 로직 (SVG 회전 포함)
|
||||
const toggleBtn = document.getElementById('toggle-tags');
|
||||
const tagContainer = document.getElementById('tag-container');
|
||||
if (toggleBtn && tagContainer) {
|
||||
toggleBtn.onclick = () => {
|
||||
const isExpanded = tagContainer.classList.contains('expanded');
|
||||
const svgIcon = toggleBtn.querySelector('svg');
|
||||
if (isExpanded) {
|
||||
tagContainer.style.maxHeight = tagContainer.scrollHeight + 'px';
|
||||
requestAnimationFrame(() => {
|
||||
tagContainer.style.maxHeight = '34px';
|
||||
tagContainer.classList.remove('expanded');
|
||||
if (svgIcon) svgIcon.style.transform = 'rotate(0deg)';
|
||||
});
|
||||
} else {
|
||||
tagContainer.style.maxHeight = tagContainer.scrollHeight + 'px';
|
||||
tagContainer.classList.add('expanded');
|
||||
if (svgIcon) svgIcon.style.transform = 'rotate(180deg)';
|
||||
setTimeout(() => {
|
||||
if (tagContainer.classList.contains('expanded')) tagContainer.style.maxHeight = 'none';
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 데이터용 새 ID 생성기
|
||||
const newId = Math.random().toString(36).substring(2, 10);
|
||||
console.log(`%c[NUMBER]: ${newId}`, 'color: #137fec; font-weight: bold; border: 1px solid #137fec; padding: 2px 5px; border-radius: 4px;');
|
||||
|
||||
/** 현재 페이지의 '선택 가능한' 항목들만 선택/해제 */
|
||||
/**
|
||||
* 현재 페이지 전체 선택/해제
|
||||
*/
|
||||
window.toggleSelectAll = (isChecked) => {
|
||||
const startIndex = (state.currentPage - 1) * ITEMS_PER_PAGE;
|
||||
const currentPageProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
||||
@@ -210,126 +124,87 @@ window.toggleSelectAll = (isChecked) => {
|
||||
currentPageProducts.forEach((p) => {
|
||||
const isSelectable = STATUS_META[p.status]?.selectable !== false;
|
||||
if (isSelectable) {
|
||||
if (isChecked) state.selectedIds.add(p.id);
|
||||
else state.selectedIds.delete(p.id);
|
||||
isChecked ? state.selectedIds.add(p.id) : state.selectedIds.delete(p.id);
|
||||
}
|
||||
});
|
||||
|
||||
saveSelection(); // 변경사항 저장
|
||||
saveSelection();
|
||||
updateSummary();
|
||||
renderProducts(state.currentPage);
|
||||
};
|
||||
|
||||
/** 선택 리셋 */
|
||||
// [수정] 기존 resetSelection을 모달 오픈으로 변경
|
||||
/**
|
||||
* 선택 초기화 모달 및 실행
|
||||
*/
|
||||
window.resetSelection = () => {
|
||||
const modal = document.getElementById('selection-reset-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
modal.classList.replace('hidden', 'flex');
|
||||
};
|
||||
|
||||
// [추가] 모달 닫기
|
||||
window.closeSelectionResetModal = () => {
|
||||
const modal = document.getElementById('selection-reset-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
modal.classList.replace('flex', 'hidden');
|
||||
};
|
||||
|
||||
// [추가] 실제 초기화 실행 (기존 로직 그대로)
|
||||
window.confirmSelectionReset = () => {
|
||||
state.selectedIds.clear();
|
||||
saveSelection(); // 스토리지 동기화
|
||||
saveSelection();
|
||||
updateSummary();
|
||||
renderProducts(state.currentPage);
|
||||
window.closeSelectionResetModal();
|
||||
};
|
||||
|
||||
/** 선택 토글 시 스토리지 저장 추가 */
|
||||
window.toggleSelectItem = (id) => {
|
||||
const product = productsData.find((p) => p.id === id);
|
||||
if (!product || STATUS_META[product.status]?.selectable === false) return;
|
||||
// ==========================================================================
|
||||
// 4. 테마 및 검색 이벤트 (Theme & Search)
|
||||
// ==========================================================================
|
||||
|
||||
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
|
||||
else state.selectedIds.add(id);
|
||||
/**
|
||||
* 다크/라이트 테마 초기화 및 버튼 바인딩
|
||||
*/
|
||||
export function initTheme() {
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
if (!themeToggleBtn) return;
|
||||
|
||||
saveSelection(); // 변경사항 저장
|
||||
updateSummary();
|
||||
};
|
||||
const isDark = localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
/** 선택된 항목들을 CSV 파일로 내보내기 */
|
||||
window.exportToExcel = () => {
|
||||
if (state.selectedIds.size === 0) {
|
||||
alert('내보낼 상품을 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
updateThemeIcons();
|
||||
|
||||
// 1. 선택된 데이터 추출 및 계산
|
||||
let totalCount = 0;
|
||||
let totalPrice = 0;
|
||||
themeToggleBtn.onclick = () => {
|
||||
const willBeDark = !document.documentElement.classList.contains('dark');
|
||||
document.documentElement.classList.toggle('dark', willBeDark);
|
||||
localStorage.setItem('color-theme', willBeDark ? 'dark' : 'light');
|
||||
updateThemeIcons();
|
||||
};
|
||||
}
|
||||
|
||||
const rows = Array.from(state.selectedIds)
|
||||
.map((id) => {
|
||||
const p = productsData.find((item) => item.id === id);
|
||||
if (p) {
|
||||
totalCount += 1;
|
||||
totalPrice += p.price;
|
||||
return [p.id, `"${p.title.replace(/"/g, '""')}"`, p.category, p.price, p.status, `"${p.description.replace(/"/g, '""')}"`];
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((row) => row !== null);
|
||||
function updateThemeIcons() {
|
||||
const darkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const lightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
if (!darkIcon || !lightIcon) return;
|
||||
|
||||
// 2. 헤더 및 푸터(합계) 설정
|
||||
const headers = ['상품 ID', '상품명', '카테고리', '가격', '상태', '상세설명'];
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
lightIcon.classList.toggle('hidden', !isDark);
|
||||
darkIcon.classList.toggle('hidden', isDark);
|
||||
}
|
||||
|
||||
// 영수증 느낌을 위한 하단 합계 줄
|
||||
const footerEmpty = ['', '', '', '', '', '']; // 빈 줄
|
||||
const footerTotal = ['TOTAL', `"총 ${totalCount}건의 항목"`, '', totalPrice, '', `"발행일: ${new Date().toLocaleString()}"`];
|
||||
// 검색어 입력 실시간 반영
|
||||
document.getElementById('search-input')?.addEventListener('input', (e) => {
|
||||
state.searchKeyword = e.target.value.trim().toLowerCase();
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// 3. CSV 포맷 생성 (한글 깨짐 방지 BOM 추가)
|
||||
const csvContent =
|
||||
'\uFEFF' +
|
||||
[
|
||||
headers.join(','),
|
||||
...rows.map((row) => row.join(',')),
|
||||
footerEmpty.join(','), // 간격 조절용 빈 줄
|
||||
footerTotal.join(','), // 합계 라인
|
||||
].join('\n');
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
|
||||
// 4. 다운로드 실행
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
// ==========================================================================
|
||||
// 5. 썸네일 애니메이션 및 인터랙션 (Thumbnail Logic)
|
||||
// ==========================================================================
|
||||
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `inventory_receipt_${timestamp}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
let currentHoverIndex = -1;
|
||||
|
||||
window.handleThumbnailHover = (e, productId) => {
|
||||
if (window.isDragging) return;
|
||||
|
||||
const product = productsData.find((p) => p.id === productId);
|
||||
if (!product || !product.images || product.images.length <= 1) return;
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
let index = Math.floor((x / rect.width) * product.images.length);
|
||||
index = Math.max(0, Math.min(product.images.length - 1, index));
|
||||
|
||||
if (currentHoverIndex !== index) {
|
||||
currentHoverIndex = index;
|
||||
updateThumbnailWithFade(productId, product.images[index], index);
|
||||
}
|
||||
};
|
||||
|
||||
/** * 썸네일을 즉시 업데이트하는 함수
|
||||
* 페이드 애니메이션을 제거하여 반응성을 높이고 깜빡임을 방지합니다.
|
||||
/**
|
||||
* 썸네일 이미지/인디케이터 즉시 업데이트 (마우스 호버용)
|
||||
*/
|
||||
function updateThumbnailWithFade(productId, newImageUrl, index) {
|
||||
const mainThumb = document.getElementById(`thumb-${productId}`);
|
||||
@@ -338,32 +213,22 @@ function updateThumbnailWithFade(productId, newImageUrl, index) {
|
||||
|
||||
if (!mainThumb) return;
|
||||
|
||||
// 1. 기존 페이드 타이머가 있다면 즉시 제거 (충돌 방지)
|
||||
if (fadeTimers[productId]) {
|
||||
clearTimeout(fadeTimers[productId]);
|
||||
delete fadeTimers[productId];
|
||||
}
|
||||
|
||||
// 2. 페이드 레이어(뒷배경)는 즉시 숨기고 메인 이미지만 즉시 교체
|
||||
// transition 없이 즉시 교체되도록 인라인 스타일로 제어합니다.
|
||||
if (fadeThumb) {
|
||||
fadeThumb.style.transition = 'none';
|
||||
fadeThumb.style.opacity = '0';
|
||||
}
|
||||
|
||||
mainThumb.style.backgroundImage = `url("${newImageUrl}")`;
|
||||
|
||||
// 3. 인디케이터 UI 업데이트
|
||||
mainThumb.style.backgroundImage = `url("${newImageUrl}")`;
|
||||
if (indicator) updateIndicatorUI(indicator, index);
|
||||
}
|
||||
|
||||
window.handleThumbnailLeave = (productId) => {
|
||||
currentHoverIndex = -1; // 인덱스 초기화
|
||||
|
||||
resetThumbnail(productId);
|
||||
};
|
||||
|
||||
/** * 마우스가 나갔을 때 썸네일을 첫 번째 이미지로 복구하는 함수
|
||||
/**
|
||||
* 썸네일 상태 복구
|
||||
*/
|
||||
function resetThumbnail(productId) {
|
||||
if (fadeTimers[productId]) {
|
||||
@@ -380,21 +245,17 @@ function resetThumbnail(productId) {
|
||||
|
||||
if (mainThumb) {
|
||||
const firstImgUrl = `url("${product.images[0]}")`;
|
||||
|
||||
// 즉시 첫 번째 이미지로 복구
|
||||
mainThumb.style.backgroundImage = firstImgUrl;
|
||||
|
||||
|
||||
if (fadeThumb) {
|
||||
fadeThumb.style.transition = 'none';
|
||||
fadeThumb.style.opacity = '0';
|
||||
fadeThumb.style.backgroundImage = firstImgUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (indicator) updateIndicatorUI(indicator, 0);
|
||||
}
|
||||
|
||||
// 중복 코드를 방지하기 위한 UI 업데이트 헬퍼
|
||||
function updateIndicatorUI(indicator, activeIndex) {
|
||||
Array.from(indicator.children).forEach((dot, i) => {
|
||||
const isActive = i === activeIndex;
|
||||
@@ -404,10 +265,29 @@ function updateIndicatorUI(indicator, activeIndex) {
|
||||
});
|
||||
}
|
||||
|
||||
// 터치 상태 관리를 위한 변수
|
||||
let touchStartX = 0;
|
||||
let isDragging = false;
|
||||
// 데스크탑 호버 이벤트
|
||||
window.handleThumbnailHover = (e, productId) => {
|
||||
if (window.isDragging) return;
|
||||
const product = productsData.find((p) => p.id === productId);
|
||||
if (!product || !product.images || product.images.length <= 1) return;
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
let index = Math.floor((x / rect.width) * product.images.length);
|
||||
index = Math.max(0, Math.min(product.images.length - 1, index));
|
||||
|
||||
if (currentHoverIndex !== index) {
|
||||
currentHoverIndex = index;
|
||||
updateThumbnailWithFade(productId, product.images[index], index);
|
||||
}
|
||||
};
|
||||
|
||||
window.handleThumbnailLeave = (productId) => {
|
||||
currentHoverIndex = -1;
|
||||
resetThumbnail(productId);
|
||||
};
|
||||
|
||||
// 모바일 터치 스와이프 이벤트
|
||||
window.handleTouchStart = (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
isDragging = false;
|
||||
@@ -417,10 +297,8 @@ window.handleTouchMove = (e, productId) => {
|
||||
const product = productsData.find((p) => p.id === productId);
|
||||
if (!product || !product.images || product.images.length <= 1) return;
|
||||
|
||||
const cardElement = e.currentTarget;
|
||||
const cardWidth = cardElement.offsetWidth;
|
||||
const touchCurrentX = e.touches[0].clientX;
|
||||
const diffX = touchStartX - touchCurrentX;
|
||||
const cardWidth = e.currentTarget.offsetWidth;
|
||||
const diffX = touchStartX - e.touches[0].clientX;
|
||||
|
||||
if (Math.abs(diffX) > 10) {
|
||||
isDragging = true;
|
||||
@@ -428,48 +306,87 @@ window.handleTouchMove = (e, productId) => {
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
const product = productsData.find((p) => p.id === productId);
|
||||
const step = cardWidth / product.images.length;
|
||||
let index = Math.floor(Math.abs(diffX) / step);
|
||||
index = Math.max(0, Math.min(product.images.length - 1, index));
|
||||
|
||||
// [수정 핵심] 인덱스가 이전과 같으면 함수를 종료하여 불필요한 리렌더링 방지
|
||||
if (lastThumbnailIndex === index) return;
|
||||
lastThumbnailIndex = index;
|
||||
|
||||
const mainThumb = document.getElementById(`thumb-${productId}`);
|
||||
const fadeThumb = document.getElementById(`thumb-fade-${productId}`);
|
||||
|
||||
// [수정 핵심] 인덱스가 실제로 변했을 때만 스타일을 바꿉니다.
|
||||
if (mainThumb && lastThumbnailIndex !== index) {
|
||||
lastThumbnailIndex = index; // 새 인덱스 저장
|
||||
|
||||
if (mainThumb) {
|
||||
mainThumb.style.backgroundImage = `url("${product.images[index]}")`;
|
||||
if (fadeThumb) fadeThumb.style.opacity = '0';
|
||||
updateIndicator(productId, index);
|
||||
updateIndicatorUI(document.getElementById(`indicator-${productId}`), index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.handleTouchEnd = (e, productId) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
|
||||
if (!isDragging) {
|
||||
window.openModal(productId);
|
||||
} else {
|
||||
resetThumbnail(productId); // 드래그 종료 시 확실한 리셋
|
||||
resetThumbnail(productId);
|
||||
}
|
||||
lastThumbnailIndex = -1; // 초기화
|
||||
lastThumbnailIndex = -1;
|
||||
isDragging = false;
|
||||
};
|
||||
|
||||
// 인디케이터 업데이트 헬퍼 함수
|
||||
function updateIndicator(productId, index) {
|
||||
const indicator = document.getElementById(`indicator-${productId}`);
|
||||
if (indicator) {
|
||||
Array.from(indicator.children).forEach((dot, i) => {
|
||||
dot.style.backgroundColor = i === index ? 'white' : 'rgba(255, 255, 255, 0.4)';
|
||||
dot.style.opacity = i === index ? '1' : '0.7';
|
||||
});
|
||||
// ==========================================================================
|
||||
// 6. 데이터 내보내기 (Export)
|
||||
// ==========================================================================
|
||||
|
||||
/**
|
||||
* 선택된 항목 CSV 다운로드
|
||||
*/
|
||||
window.exportToExcel = () => {
|
||||
if (state.selectedIds.size === 0) {
|
||||
alert('내보낼 상품을 선택해 주세요.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let totalCount = 0;
|
||||
let totalPrice = 0;
|
||||
|
||||
const rows = Array.from(state.selectedIds)
|
||||
.map((id) => {
|
||||
const p = productsData.find((item) => item.id === id);
|
||||
if (!p) return null;
|
||||
totalCount += 1;
|
||||
totalPrice += p.price;
|
||||
return [p.id, `"${p.title.replace(/"/g, '""')}"`, p.category, p.price, p.status, `"${p.description.replace(/"/g, '""')}"`];
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const headers = ['상품 ID', '상품명', '카테고리', '가격', '상태', '상세설명'];
|
||||
const footerTotal = ['TOTAL', `"총 ${totalCount}건"`, '', totalPrice, '', `"발행: ${new Date().toLocaleString()}"`];
|
||||
|
||||
const csvContent = '\uFEFF' + [headers.join(','), ...rows.map((row) => row.join(',')), ['', '', '', '', '', ''].join(','), footerTotal.join(',')].join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `inventory_receipt_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// ==========================================================================
|
||||
// 7. 초기화 (App Initialization)
|
||||
// ==========================================================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('App initialized. Products:', productsData.length);
|
||||
|
||||
initTheme();
|
||||
renderCategoryChips(productsData);
|
||||
renderStatusChips();
|
||||
renderTagChips();
|
||||
setupTagToggle();
|
||||
|
||||
applyFilters();
|
||||
updateViewButtons();
|
||||
updateSummary();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user