- 코드 정리

- 경고 클래스 수정
- 카드 비율 수정 3:4 > 4:5
This commit is contained in:
2026-02-10 11:30:45 +09:00
parent ddc491dec1
commit 526d310ac9
6 changed files with 263 additions and 116 deletions

View File

@@ -66,9 +66,9 @@ export const TAG_STYLES = {
export const TAG_DEFAULT_STYLE = 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400';
export const SEARCH_CONFIG = {
USE_TITLE: true, // 상품명 검색
USE_CUSTOM_TAG: true, // 커스텀 태그 검색
USE_TAGS: true, // 태그 배열 검색
USE_DESCRIPTION: true, // 요약 설명 검색
USE_TITLE: true, // 상품명 검색
USE_CUSTOM_TAG: true, // 커스텀 태그 검색
USE_TAGS: true, // 태그 배열 검색
USE_DESCRIPTION: true, // 요약 설명 검색
USE_FULL_DESCRIPTION: false, // 상세 설명 배열 검색
};
};

View File

@@ -1,11 +1,6 @@
/** 진입점: 이벤트 바인딩·초기 렌더·URL 모달 처리 */
import { state, productsData, saveSelection } from './state.js';
import {
applyFilters,
renderStatusChips,
renderCategoryChips,
bindCategoryFilter,
} from './filters.js';
import { applyFilters, renderStatusChips, renderCategoryChips, bindCategoryFilter } from './filters.js';
import { ITEMS_PER_PAGE, STATUS_META } from './config.js';
import { renderProducts, changePage } from './productList.js';
import { openModal, closeModal } from './modal.js';
@@ -44,7 +39,7 @@ function updateViewButtons() {
window.toggleSelectItem = (id) => {
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
else state.selectedIds.add(id);
updateSummary();
};
@@ -52,22 +47,26 @@ export function updateSummary() {
const summary = document.getElementById('selection-summary');
const countEl = document.getElementById('selected-count');
const priceEl = document.getElementById('selected-total-price');
if (!summary) return;
// 테이블 모드이면서 선택된 항목이 있을 때만 노출
if (state.viewMode === 'table' && state.selectedIds.size > 0) {
// [수정] flex를 추가할 때 hidden은 확실히 제거
summary.classList.remove('hidden');
summary.classList.add('flex');
const total = Array.from(state.selectedIds).reduce((sum, id) => {
const p = productsData.find(item => item.id === id);
const p = productsData.find((item) => item.id === id);
return sum + (p ? p.price : 0);
}, 0);
countEl.textContent = state.selectedIds.size;
priceEl.textContent = `${total.toLocaleString()}`;
} else {
summary.classList.add('hidden');
// [수정] hidden을 추가할 때 flex는 확실히 제거
summary.classList.remove('flex');
summary.classList.add('hidden');
}
}
@@ -124,7 +123,7 @@ if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localS
updateIcons();
// 3. 버튼 클릭 이벤트
themeToggleBtn.addEventListener('click', function() {
themeToggleBtn.addEventListener('click', function () {
// 테마 토글
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
@@ -155,15 +154,15 @@ console.log(`%c[NUMBER]: ${newId}`, 'color: #137fec; font-weight: bold; border:
window.toggleSelectAll = (isChecked) => {
const startIndex = (state.currentPage - 1) * ITEMS_PER_PAGE;
const currentPageProducts = state.visibleProducts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
currentPageProducts.forEach(p => {
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);
}
});
saveSelection(); // 변경사항 저장
updateSummary();
renderProducts(state.currentPage);
@@ -171,7 +170,7 @@ window.toggleSelectAll = (isChecked) => {
/** 선택 리셋 */
window.resetSelection = () => {
if (!confirm("선택된 내역을 모두 초기화할까요?")) return;
if (!confirm('선택된 내역을 모두 초기화할까요?')) return;
state.selectedIds.clear();
saveSelection(); // 스토리지 동기화
updateSummary();
@@ -180,12 +179,12 @@ window.resetSelection = () => {
/** 선택 토글 시 스토리지 저장 추가 */
window.toggleSelectItem = (id) => {
const product = productsData.find(p => p.id === id);
const product = productsData.find((p) => p.id === id);
if (!product || STATUS_META[product.status]?.selectable === false) return;
if (state.selectedIds.has(id)) state.selectedIds.delete(id);
else state.selectedIds.add(id);
saveSelection(); // 변경사항 저장
updateSummary();
};
@@ -193,62 +192,52 @@ window.toggleSelectItem = (id) => {
/** 선택된 항목들을 CSV 파일로 내보내기 */
window.exportToExcel = () => {
if (state.selectedIds.size === 0) {
alert("내보낼 상품을 선택해 주세요.");
alert('내보낼 상품을 선택해 주세요.');
return;
}
// 1. 선택된 데이터 추출 및 계산
let totalCount = 0;
let totalPrice = 0;
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);
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);
// 2. 헤더 및 푸터(합계) 설정
const headers = ["상품 ID", "상품명", "카테고리", "가격", "상태", "상세설명"];
const headers = ['상품 ID', '상품명', '카테고리', '가격', '상태', '상세설명'];
// 영수증 느낌을 위한 하단 합계 줄
const footerEmpty = ["", "", "", "", "", ""]; // 빈 줄
const footerTotal = [
"TOTAL",
`"총 ${totalCount}건의 항목"`,
"",
totalPrice,
"",
`"발행일: ${new Date().toLocaleString()}"`
];
const footerEmpty = ['', '', '', '', '', '']; // 빈 줄
const footerTotal = ['TOTAL', `"총 ${totalCount}건의 항목"`, '', totalPrice, '', `"발행일: ${new Date().toLocaleString()}"`];
// 3. CSV 포맷 생성 (한글 깨짐 방지 BOM 추가)
const csvContent = "\uFEFF" + [
headers.join(","),
...rows.map(row => row.join(",")),
footerEmpty.join(","), // 간격 조절용 빈 줄
footerTotal.join(",") // 합계 라인
].join("\n");
const csvContent =
'\uFEFF' +
[
headers.join(','),
...rows.map((row) => row.join(',')),
footerEmpty.join(','), // 간격 조절용 빈 줄
footerTotal.join(','), // 합계 라인
].join('\n');
// 4. 다운로드 실행
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
const link = document.createElement('a');
const timestamp = new Date().toISOString().split('T')[0];
link.setAttribute("href", url);
link.setAttribute("download", `inventory_receipt_${timestamp}.csv`);
link.setAttribute('href', url);
link.setAttribute('download', `inventory_receipt_${timestamp}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
};

View File

@@ -29,13 +29,25 @@ export function renderProducts(page = 1) {
// 2. 뷰 모드에 따른 컨테이너 노출 설정
if (state.viewMode === 'grid') {
// 그리드 활성화
grid.classList.remove('hidden');
grid.classList.add('grid');
// 테이블 및 요약바 비활성화
tableWrapper.classList.add('hidden');
summaryBar.classList.add('hidden');
if (summaryBar) {
summaryBar.classList.remove('flex'); // flex 제거
summaryBar.classList.add('hidden');
}
} else {
// 그리드 비활성화
grid.classList.remove('grid');
grid.classList.add('hidden');
// 테이블 활성화
tableWrapper.classList.remove('hidden');
// 요약바 노출 여부는 데이터 상태에 따라 updateSummary에서 결정
updateSummary();
}
@@ -47,11 +59,13 @@ export function renderProducts(page = 1) {
grid.innerHTML = '';
pagedProducts.forEach((product) => {
const isSold = STATUS_META[product.status]?.soldOut === true;
grid.insertAdjacentHTML('beforeend', `
grid.insertAdjacentHTML(
'beforeend',
`
<div class="group flex flex-col gap-4 cursor-pointer" onclick="openModal('${product.id}')">
<div class="relative w-full aspect-[4/5] bg-slate-50 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm group-hover:shadow-md transition-shadow">
<div class="relative w-full aspect-card bg-slate-50 dark:bg-slate-800 rounded-xl overflow-hidden shadow-sm group-hover:shadow-md transition-shadow">
<div class="w-full h-full bg-center bg-no-repeat bg-cover transform ${isSold ? 'grayscale opacity-80' : 'group-hover:scale-105'} transition-transform duration-500"
style="background-image: url('${product.images[0]}')"></div>
style="background-image: url('${product.images[0]}')"></div>
<div class="absolute top-3 left-3">
<span class="px-2 py-1 text-[10px] uppercase tracking-wider font-bold rounded ${STATUS_COLOR[product.status]} backdrop-blur-md border">
${product.status}
@@ -63,19 +77,21 @@ export function renderProducts(page = 1) {
<p class="text-slate-500 dark:text-slate-400 text-sm font-normal line-clamp-1">${product.description}</p>
</div>
</div>
`);
`,
);
});
} else {
// 테이블 렌더링 (이전과 동일, 가격 포함)
tableBody.innerHTML = pagedProducts.map(product => {
const isSelectable = STATUS_META[product.status]?.selectable !== false;
return `
tableBody.innerHTML = pagedProducts
.map((product) => {
const isSelectable = STATUS_META[product.status]?.selectable !== false;
return `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors cursor-pointer" onclick="if(event.target.type !== 'checkbox') openModal('${product.id}')">
<td class="py-4 px-4 text-center" onclick="event.stopPropagation()">
<input type="checkbox" class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer' : 'opacity-20 cursor-not-allowed'}"
${state.selectedIds.has(product.id) ? 'checked' : ''}
${isSelectable ? '' : 'disabled'}
onchange="window.toggleSelectItem('${product.id}')">
<input type="checkbox" class="product-check rounded border-slate-300 w-4 h-4 ${isSelectable ? 'cursor-pointer' : 'opacity-20 cursor-not-allowed'}"
${state.selectedIds.has(product.id) ? 'checked' : ''}
${isSelectable ? '' : 'disabled'}
onchange="window.toggleSelectItem('${product.id}')">
</td>
<td class="py-4 px-4 font-semibold text-slate-900 dark:text-white">${product.title}</td>
<td class="py-4 px-4 text-slate-500 text-xs">${product.category}</td>
@@ -84,7 +100,8 @@ export function renderProducts(page = 1) {
<span class="px-2 py-0.5 rounded text-[10px] font-bold border ${STATUS_COLOR[product.status]}">${product.status}</span>
</td>
</tr>`;
}).join('');
})
.join('');
}
// [중요] 전체 선택 체크박스 상태 동기화
@@ -105,23 +122,19 @@ export function renderPagination() {
const totalPages = Math.ceil(state.visibleProducts.length / ITEMS_PER_PAGE);
const { currentPage } = state;
let html = `<button onclick="changePage(${currentPage - 1})" class="size-10 flex items-center justify-center ${currentPage === 1 ? 'invisible' : ''}"><svg viewBox="0 0 24 24" fill="none"
stroke="#64748B" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round"
class="w-5 h-5">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>`;
let html = `<button onclick="changePage(${currentPage - 1})" class="size-10 flex items-center justify-center ${currentPage === 1 ? 'invisible' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>`;
for (let i = 1; i <= totalPages; i++) {
html += `<button onclick="changePage(${i})" class="size-10 font-bold rounded-lg ${i === currentPage ? 'bg-primary text-white' : 'text-slate-500'}">${i}</button>`;
}
html += `<button onclick="changePage(${currentPage + 1})" class="size-10 flex items-center justify-center ${currentPage === totalPages ? 'invisible' : ''}"><svg viewBox="0 0 24 24" fill="none"
stroke="#64748B" stroke-width="3"
stroke-linecap="round" stroke-linejoin="round"
class="w-5 h-5">
<path d="M9 18l6-6-6-6" />
</svg>
</button>`;
html += `<button onclick="changePage(${currentPage + 1})" class="size-10 flex items-center justify-center ${currentPage === totalPages ? 'invisible' : ''}">
<svg viewBox="0 0 24 24" fill="none" stroke="#64748B" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
<path d="M9 18l6-6-6-6" />
</svg>
</button>`;
container.innerHTML = html;
}

View File

@@ -24,4 +24,4 @@ export const state = {
// 선택 내역이 변경될 때마다 세션 스토리지에 저장하는 헬퍼 함수
export function saveSelection() {
sessionStorage.setItem('selectedProductIds', JSON.stringify(Array.from(state.selectedIds)));
}
}