From 526d310ac9589c534ba6aecce3a36f6ce866d6b1 Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 10 Feb 2026 11:30:45 +0900 Subject: [PATCH] =?UTF-8?q?-=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20-?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=A0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=EC=B9=B4=EB=93=9C=20=EB=B9=84=EC=9C=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=203:4=20>=204:5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 13 +-- scripts/config.js | 10 +-- scripts/main.js | 107 +++++++++++------------- scripts/productList.js | 67 ++++++++------- scripts/state.js | 2 +- style/tailwind.css | 180 ++++++++++++++++++++++++++++++++++++----- 6 files changed, 263 insertions(+), 116 deletions(-) diff --git a/index.html b/index.html index 193203d..6c8ba54 100644 --- a/index.html +++ b/index.html @@ -48,6 +48,9 @@ screens: { 'xs': '480px', }, + aspectRatio: { + 'card': '4 / 5', + }, }, }, } @@ -137,7 +140,7 @@ Total Results 0 - diff --git a/scripts/config.js b/scripts/config.js index 34b619c..75d32cd 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -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, // 상세 설명 배열 검색 -}; \ No newline at end of file +}; diff --git a/scripts/main.js b/scripts/main.js index 577f8d6..6b4e649 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -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); -}; \ No newline at end of file +}; diff --git a/scripts/productList.js b/scripts/productList.js index 778d6a1..2cf5ef3 100644 --- a/scripts/productList.js +++ b/scripts/productList.js @@ -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', + `
-
+
+ style="background-image: url('${product.images[0]}')">
${product.status} @@ -63,19 +77,21 @@ export function renderProducts(page = 1) {

${product.description}

- `); + `, + ); }); } 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 ` - + ${product.title} ${product.category} @@ -84,7 +100,8 @@ export function renderProducts(page = 1) { ${product.status} `; - }).join(''); + }) + .join(''); } // [중요] 전체 선택 체크박스 상태 동기화 @@ -105,23 +122,19 @@ export function renderPagination() { const totalPages = Math.ceil(state.visibleProducts.length / ITEMS_PER_PAGE); const { currentPage } = state; - let html = ``; + let html = ``; for (let i = 1; i <= totalPages; i++) { html += ``; } - html += ``; + html += ``; container.innerHTML = html; } diff --git a/scripts/state.js b/scripts/state.js index a1ca608..4991fa8 100644 --- a/scripts/state.js +++ b/scripts/state.js @@ -24,4 +24,4 @@ export const state = { // 선택 내역이 변경될 때마다 세션 스토리지에 저장하는 헬퍼 함수 export function saveSelection() { sessionStorage.setItem('selectedProductIds', JSON.stringify(Array.from(state.selectedIds))); -} \ No newline at end of file +} diff --git a/style/tailwind.css b/style/tailwind.css index 45dd428..67e0bfb 100644 --- a/style/tailwind.css +++ b/style/tailwind.css @@ -93,6 +93,8 @@ --text-3xl--line-height: calc(2.25 / 1.875); --text-4xl: 2.25rem; --text-4xl--line-height: calc(2.5 / 2.25); + --text-6xl: 3.75rem; + --text-6xl--line-height: 1; --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; @@ -321,9 +323,6 @@ .z-50 { z-index: 50; } - .z-\[60\] { - z-index: 60; - } .container { width: 100%; @media (width >= 40rem) { @@ -345,8 +344,11 @@ .mx-auto { margin-inline: auto; } - .mt-4 { - margin-top: calc(var(--spacing) * 4); + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .-mb-4 { + margin-bottom: calc(var(--spacing) * -4); } .mb-2 { margin-bottom: calc(var(--spacing) * 2); @@ -354,6 +356,9 @@ .mb-3 { margin-bottom: calc(var(--spacing) * 3); } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } .mb-6 { margin-bottom: calc(var(--spacing) * 6); } @@ -363,6 +368,15 @@ .ml-0 { margin-left: calc(var(--spacing) * 0); } + .ml-2 { + margin-left: calc(var(--spacing) * 2); + } + .line-clamp-1 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + } .block { display: block; } @@ -378,6 +392,9 @@ .inline-flex { display: inline-flex; } + .table { + display: table; + } .aspect-\[4\/5\] { aspect-ratio: 4/5; } @@ -400,6 +417,9 @@ .h-2 { height: calc(var(--spacing) * 2); } + .h-3 { + height: calc(var(--spacing) * 3); + } .h-4 { height: calc(var(--spacing) * 4); } @@ -448,12 +468,18 @@ .w-10 { width: calc(var(--spacing) * 10); } + .w-12 { + width: calc(var(--spacing) * 12); + } .w-auto { width: auto; } .w-full { width: 100%; } + .w-px { + width: 1px; + } .max-w-3xl { max-width: var(--container-3xl); } @@ -481,6 +507,9 @@ .grow { flex-grow: 1; } + .border-collapse { + border-collapse: collapse; + } .-translate-x-1\/2 { --tw-translate-x: calc(calc(1/2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -492,6 +521,9 @@ .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } + .cursor-not-allowed { + cursor: not-allowed; + } .cursor-pointer { cursor: pointer; } @@ -553,6 +585,20 @@ margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse))); } } + .divide-y { + :where(& > :not(:last-child)) { + --tw-divide-y-reverse: 0; + border-bottom-style: var(--tw-border-style); + border-top-style: var(--tw-border-style); + border-top-width: calc(1px * var(--tw-divide-y-reverse)); + border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + } + } + .divide-slate-100 { + :where(& > :not(:last-child)) { + border-color: var(--color-slate-100); + } + } .overflow-hidden { overflow: hidden; } @@ -622,6 +668,9 @@ .border-slate-200 { border-color: var(--color-slate-200); } + .border-slate-300 { + border-color: var(--color-slate-300); + } .border-slate-300\/30 { border-color: color-mix(in srgb, oklch(86.9% 0.022 252.894) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -640,6 +689,9 @@ .\!bg-green-600 { background-color: var(--color-green-600) !important; } + .bg-\[\#fae100\] { + background-color: #fae100; + } .bg-amber-100 { background-color: var(--color-amber-100); } @@ -718,12 +770,6 @@ .bg-slate-900 { background-color: var(--color-slate-900); } - .bg-slate-900\/10 { - background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 10%, transparent); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-slate-900) 10%, transparent); - } - } .bg-teal-100 { background-color: var(--color-teal-100); } @@ -760,6 +806,9 @@ .p-1 { padding: calc(var(--spacing) * 1); } + .p-1\.5 { + padding: calc(var(--spacing) * 1.5); + } .p-3 { padding: calc(var(--spacing) * 3); } @@ -802,6 +851,9 @@ .py-12 { padding-block: calc(var(--spacing) * 12); } + .py-20 { + padding-block: calc(var(--spacing) * 20); + } .pt-4 { padding-top: calc(var(--spacing) * 4); } @@ -820,9 +872,16 @@ .text-center { text-align: center; } + .text-left { + text-align: left; + } .text-right { text-align: right; } + .text-6xl { + font-size: var(--text-6xl); + line-height: var(--tw-leading, var(--text-6xl--line-height)); + } .text-base { font-size: var(--text-base); line-height: var(--tw-leading, var(--text-base--line-height)); @@ -843,6 +902,12 @@ font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); } + .\!text-\[14px\] { + font-size: 14px !important; + } + .\!text-\[20px\] { + font-size: 20px !important; + } .text-\[10px\] { font-size: 10px; } @@ -892,9 +957,6 @@ --tw-tracking: var(--tracking-widest); letter-spacing: var(--tracking-widest); } - .text-nowrap { - text-wrap: nowrap; - } .wrap-break-word { overflow-wrap: break-word; } @@ -908,6 +970,9 @@ .whitespace-nowrap { white-space: nowrap; } + .text-\[\#3c1e1e\] { + color: #3c1e1e; + } .text-\[\#111418\] { color: #111418; } @@ -959,6 +1024,9 @@ .text-sky-700 { color: var(--color-sky-700); } + .text-slate-300 { + color: var(--color-slate-300); + } .text-slate-400 { color: var(--color-slate-400); } @@ -968,6 +1036,9 @@ .text-slate-600 { color: var(--color-slate-600); } + .text-slate-800 { + color: var(--color-slate-800); + } .text-slate-900 { color: var(--color-slate-900); } @@ -983,6 +1054,9 @@ .line-through { text-decoration-line: line-through; } + .opacity-20 { + opacity: 20%; + } .opacity-30 { opacity: 30%; } @@ -1086,6 +1160,20 @@ color: var(--color-slate-400); } } + .hover\:bg-\[\#f7d600\] { + &:hover { + @media (hover: hover) { + background-color: #f7d600; + } + } + } + .hover\:bg-slate-50 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-50); + } + } + } .hover\:bg-slate-100 { &:hover { @media (hover: hover) { @@ -1100,6 +1188,13 @@ } } } + .hover\:text-slate-600 { + &:hover { + @media (hover: hover) { + color: var(--color-slate-600); + } + } + } .hover\:opacity-50 { &:hover { @media (hover: hover) { @@ -1161,11 +1256,6 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } } - .sm\:flex-row { - @media (width >= 40rem) { - flex-direction: row; - } - } .sm\:items-center { @media (width >= 40rem) { align-items: center; @@ -1386,6 +1476,13 @@ grid-template-columns: repeat(5, minmax(0, 1fr)); } } + .dark\:divide-slate-800 { + @media (prefers-color-scheme: dark) { + :where(& > :not(:last-child)) { + border-color: var(--color-slate-800); + } + } + } .dark\:border-gray-800 { @media (prefers-color-scheme: dark) { border-color: var(--color-gray-800); @@ -1509,6 +1606,14 @@ background-color: var(--color-slate-800); } } + .dark\:bg-slate-800\/50 { + @media (prefers-color-scheme: dark) { + background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-800) 50%, transparent); + } + } + } .dark\:bg-slate-900 { @media (prefers-color-scheme: dark) { background-color: var(--color-slate-900); @@ -1605,6 +1710,11 @@ color: var(--color-slate-100); } } + .dark\:text-slate-200 { + @media (prefers-color-scheme: dark) { + color: var(--color-slate-200); + } + } .dark\:text-slate-300 { @media (prefers-color-scheme: dark) { color: var(--color-slate-300); @@ -1620,6 +1730,11 @@ color: var(--color-slate-500); } } + .dark\:text-slate-700 { + @media (prefers-color-scheme: dark) { + color: var(--color-slate-700); + } + } .dark\:text-slate-900 { @media (prefers-color-scheme: dark) { color: var(--color-slate-900); @@ -1653,6 +1768,27 @@ } } } + .dark\:hover\:bg-slate-800\/50 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-800) 50%, transparent); + } + } + } + } + } + .dark\:hover\:text-slate-200 { + @media (prefers-color-scheme: dark) { + &:hover { + @media (hover: hover) { + color: var(--color-slate-200); + } + } + } + } } @property --tw-translate-x { syntax: "*"; @@ -1694,6 +1830,11 @@ inherits: false; initial-value: 0; } +@property --tw-divide-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-border-style { syntax: "*"; inherits: false; @@ -1901,6 +2042,7 @@ --tw-skew-x: initial; --tw-skew-y: initial; --tw-space-y-reverse: 0; + --tw-divide-y-reverse: 0; --tw-border-style: solid; --tw-leading: initial; --tw-font-weight: initial;