- 이미지/플레이스홀더 클릭시 이미지 등록 모달 열림 - URL 입력 탭: 이미지 URL로 등록 - 촬영 탭: 카메라로 직접 촬영 (1:1 크롭 가이드) - 기존 /api/admin/product-images API 재활용 - 저장 후 자동 새로고침
1354 lines
50 KiB
HTML
1354 lines
50 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>판매 내역 - 청춘약국 POS</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0f172a;
|
|
--bg-secondary: #1e293b;
|
|
--bg-card: #1e293b;
|
|
--bg-card-hover: #334155;
|
|
--border: #334155;
|
|
--text-primary: #f1f5f9;
|
|
--text-secondary: #94a3b8;
|
|
--text-muted: #64748b;
|
|
--accent-teal: #14b8a6;
|
|
--accent-blue: #3b82f6;
|
|
--accent-purple: #a855f7;
|
|
--accent-amber: #f59e0b;
|
|
--accent-emerald: #10b981;
|
|
--accent-rose: #f43f5e;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
-webkit-font-smoothing: antialiased;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* ══════════════════ 헤더 ══════════════════ */
|
|
.header {
|
|
background: linear-gradient(135deg, #0f766e 0%, #0d9488 50%, #14b8a6 100%);
|
|
padding: 20px 24px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
}
|
|
.header-inner {
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.header-left h1 {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.5px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.header-left p {
|
|
font-size: 13px;
|
|
opacity: 0.85;
|
|
margin-top: 4px;
|
|
}
|
|
.header-nav {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.header-nav a {
|
|
color: rgba(255,255,255,0.85);
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
background: rgba(255,255,255,0.1);
|
|
transition: all 0.2s;
|
|
}
|
|
.header-nav a:hover {
|
|
background: rgba(255,255,255,0.2);
|
|
color: #fff;
|
|
}
|
|
|
|
/* ══════════════════ 컨텐츠 ══════════════════ */
|
|
.content {
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
padding: 24px;
|
|
}
|
|
|
|
/* ══════════════════ 검색 영역 ══════════════════ */
|
|
.search-bar {
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
padding: 20px 24px;
|
|
margin-bottom: 20px;
|
|
border: 1px solid var(--border);
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-end;
|
|
}
|
|
.search-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
.search-group label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.search-group input, .search-group select {
|
|
padding: 10px 14px;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
color: var(--text-primary);
|
|
min-width: 140px;
|
|
transition: all 0.2s;
|
|
}
|
|
.search-group input:focus, .search-group select:focus {
|
|
outline: none;
|
|
border-color: var(--accent-teal);
|
|
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.2);
|
|
}
|
|
.search-group input::placeholder { color: var(--text-muted); }
|
|
.search-btn {
|
|
background: linear-gradient(135deg, var(--accent-teal), var(--accent-emerald));
|
|
color: #fff;
|
|
border: none;
|
|
padding: 10px 28px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.search-btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.4);
|
|
}
|
|
|
|
/* ══════════════════ 통계 카드 ══════════════════ */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.stat-card {
|
|
background: var(--bg-card);
|
|
border-radius: 14px;
|
|
padding: 20px;
|
|
border: 1px solid var(--border);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
}
|
|
.stat-card.teal::before { background: var(--accent-teal); }
|
|
.stat-card.blue::before { background: var(--accent-blue); }
|
|
.stat-card.purple::before { background: var(--accent-purple); }
|
|
.stat-card.amber::before { background: var(--accent-amber); }
|
|
.stat-card.emerald::before { background: var(--accent-emerald); }
|
|
|
|
.stat-icon {
|
|
font-size: 24px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
letter-spacing: -1px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.stat-card.teal .stat-value { color: var(--accent-teal); }
|
|
.stat-card.blue .stat-value { color: var(--accent-blue); }
|
|
.stat-card.purple .stat-value { color: var(--accent-purple); }
|
|
.stat-card.amber .stat-value { color: var(--accent-amber); }
|
|
.stat-card.emerald .stat-value { color: var(--accent-emerald); }
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
/* ══════════════════ 뷰 토글 ══════════════════ */
|
|
.view-controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
.code-toggle {
|
|
display: flex;
|
|
gap: 4px;
|
|
background: var(--bg-secondary);
|
|
padding: 4px;
|
|
border-radius: 10px;
|
|
}
|
|
.code-toggle button {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.code-toggle button.active {
|
|
background: var(--accent-teal);
|
|
color: #fff;
|
|
}
|
|
.code-toggle button:hover:not(.active) {
|
|
color: var(--text-primary);
|
|
}
|
|
.view-mode {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.view-btn {
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-card);
|
|
color: var(--text-secondary);
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.view-btn.active {
|
|
border-color: var(--accent-teal);
|
|
color: var(--accent-teal);
|
|
}
|
|
|
|
/* ══════════════════ 거래 카드 (그룹별) ══════════════════ */
|
|
.transactions-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
.tx-card {
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
transition: all 0.2s;
|
|
}
|
|
.tx-card:hover {
|
|
border-color: var(--accent-teal);
|
|
}
|
|
.tx-header {
|
|
padding: 16px 20px;
|
|
background: var(--bg-secondary);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.tx-header:hover {
|
|
background: var(--bg-card-hover);
|
|
}
|
|
.tx-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
}
|
|
.tx-id {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--accent-teal);
|
|
}
|
|
.tx-time {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
.tx-customer {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
background: var(--bg-primary);
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
}
|
|
.tx-summary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
.tx-count {
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
}
|
|
.tx-amount {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--accent-emerald);
|
|
}
|
|
.tx-toggle {
|
|
font-size: 16px;
|
|
color: var(--text-muted);
|
|
transition: transform 0.3s;
|
|
}
|
|
.tx-card.open .tx-toggle {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* 품목 테이블 */
|
|
.tx-items {
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease-out;
|
|
}
|
|
.tx-card.open .tx-items {
|
|
max-height: 2000px;
|
|
}
|
|
.items-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.items-table th {
|
|
padding: 12px 16px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
text-align: left;
|
|
background: rgba(0,0,0,0.2);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.items-table th:nth-child(4),
|
|
.items-table th:nth-child(5),
|
|
.items-table th:nth-child(6) {
|
|
text-align: right;
|
|
}
|
|
.items-table td {
|
|
padding: 14px 16px;
|
|
font-size: 13px;
|
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
vertical-align: middle;
|
|
}
|
|
.items-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.items-table tr:hover {
|
|
background: rgba(255,255,255,0.02);
|
|
}
|
|
|
|
/* 제품 셀 */
|
|
.product-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.product-thumb {
|
|
width: 36px;
|
|
height: 36px;
|
|
object-fit: cover;
|
|
border-radius: 6px;
|
|
background: var(--bg-secondary);
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
transition: transform 0.15s, box-shadow 0.15s;
|
|
}
|
|
.product-thumb:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 4px 12px rgba(139,92,246,0.3);
|
|
}
|
|
.product-thumb-placeholder {
|
|
width: 36px;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, #2a2a3e 0%, #1e1e2e 100%);
|
|
border-radius: 6px;
|
|
flex-shrink: 0;
|
|
border: 1px solid rgba(255,255,255,0.05);
|
|
cursor: pointer;
|
|
transition: transform 0.15s, border-color 0.15s;
|
|
}
|
|
.product-thumb-placeholder:hover {
|
|
transform: scale(1.1);
|
|
border-color: var(--accent-purple);
|
|
}
|
|
.product-thumb-placeholder svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
opacity: 0.3;
|
|
fill: #888;
|
|
}
|
|
|
|
/* 이미지 교체 모달 */
|
|
.image-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.8);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.image-modal.show { display: flex; }
|
|
|
|
.image-modal-content {
|
|
background: #1a1a3e;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
max-width: 450px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
border: 1px solid rgba(139,92,246,0.3);
|
|
}
|
|
.image-modal-content h3 {
|
|
margin: 0 0 16px 0;
|
|
color: var(--accent-purple);
|
|
font-size: 18px;
|
|
}
|
|
.image-modal-tabs {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.tab-btn {
|
|
flex: 1;
|
|
padding: 10px;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.tab-btn.active {
|
|
background: var(--accent-purple);
|
|
color: white;
|
|
border-color: var(--accent-purple);
|
|
}
|
|
.tab-content { display: none; }
|
|
.tab-content.active { display: block; }
|
|
|
|
.image-input {
|
|
width: 100%;
|
|
padding: 12px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
border-radius: 8px;
|
|
color: var(--text-primary);
|
|
margin-bottom: 12px;
|
|
}
|
|
.image-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent-purple);
|
|
}
|
|
|
|
.camera-container {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 1;
|
|
background: #000;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
margin-bottom: 12px;
|
|
}
|
|
.camera-container video,
|
|
.camera-container canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.camera-guide {
|
|
position: absolute;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.modal-btns {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
}
|
|
.btn-modal {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-modal.secondary {
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
}
|
|
.btn-modal.primary {
|
|
background: var(--accent-purple);
|
|
color: white;
|
|
}
|
|
.btn-modal:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
.product-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
}
|
|
.product-name {
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.product-supplier {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* 코드 뱃지 */
|
|
.code-badge {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
display: inline-block;
|
|
}
|
|
.code-drug {
|
|
background: rgba(59, 130, 246, 0.2);
|
|
color: #60a5fa;
|
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
}
|
|
.code-barcode {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
color: #34d399;
|
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
}
|
|
.code-standard {
|
|
background: rgba(245, 158, 11, 0.2);
|
|
color: #fbbf24;
|
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
|
}
|
|
.code-na {
|
|
background: rgba(148, 163, 184, 0.1);
|
|
color: var(--text-muted);
|
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
}
|
|
.code-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
/* 바코드 시각화 */
|
|
.barcode-visual {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.barcode-bars {
|
|
display: flex;
|
|
gap: 1px;
|
|
align-items: flex-end;
|
|
height: 20px;
|
|
}
|
|
.barcode-bars span {
|
|
width: 2px;
|
|
background: var(--accent-emerald);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* 숫자 정렬 */
|
|
.items-table td.qty,
|
|
.items-table td.price {
|
|
text-align: right;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 500;
|
|
}
|
|
.items-table td.price.total {
|
|
color: var(--accent-teal);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ══════════════════ 리스트 뷰 ══════════════════ */
|
|
.list-view {
|
|
display: none;
|
|
}
|
|
.list-view.active {
|
|
display: block;
|
|
}
|
|
.list-table-wrap {
|
|
background: var(--bg-card);
|
|
border-radius: 16px;
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
}
|
|
.list-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.list-table th {
|
|
padding: 14px 16px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
text-align: left;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
position: sticky;
|
|
top: 0;
|
|
}
|
|
.list-table td {
|
|
padding: 14px 16px;
|
|
font-size: 13px;
|
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
}
|
|
.list-table tr:hover {
|
|
background: rgba(255,255,255,0.02);
|
|
}
|
|
|
|
/* ══════════════════ 로딩/빈 상태 ══════════════════ */
|
|
.loading-state, .empty-state {
|
|
text-align: center;
|
|
padding: 80px 20px;
|
|
color: var(--text-muted);
|
|
}
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid var(--border);
|
|
border-top-color: var(--accent-teal);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto 16px;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.empty-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* ══════════════════ 반응형 ══════════════════ */
|
|
@media (max-width: 1200px) {
|
|
.stats-grid { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
@media (max-width: 768px) {
|
|
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.header-nav { display: none; }
|
|
.search-bar { flex-direction: column; }
|
|
.search-group { width: 100%; }
|
|
.search-group input, .search-group select { width: 100%; }
|
|
.tx-info { flex-wrap: wrap; gap: 8px; }
|
|
.view-controls { flex-direction: column; gap: 12px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<div class="header-inner">
|
|
<div class="header-left">
|
|
<h1>🧾 판매 내역</h1>
|
|
<p>POS 판매 데이터 · 바코드 · 표준코드 조회</p>
|
|
</div>
|
|
<nav class="header-nav">
|
|
<a href="/admin">📊 대시보드</a>
|
|
<a href="/admin/ai-crm">🤖 AI CRM</a>
|
|
<a href="/admin/alimtalk">📨 알림톡</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<!-- 검색 -->
|
|
<div class="search-bar">
|
|
<div class="search-group">
|
|
<label>조회 기간</label>
|
|
<select id="periodSelect">
|
|
<option value="1">오늘</option>
|
|
<option value="3" selected>최근 3일</option>
|
|
<option value="7">최근 7일</option>
|
|
<option value="30">최근 30일</option>
|
|
</select>
|
|
</div>
|
|
<div class="search-group">
|
|
<label>검색어</label>
|
|
<input type="text" id="searchInput" placeholder="상품명, 코드, 바코드...">
|
|
</div>
|
|
<div class="search-group">
|
|
<label>바코드</label>
|
|
<select id="barcodeFilter">
|
|
<option value="all">전체</option>
|
|
<option value="has">있음</option>
|
|
<option value="none">없음</option>
|
|
</select>
|
|
</div>
|
|
<button class="search-btn" onclick="loadSalesData()">🔍 조회</button>
|
|
</div>
|
|
|
|
<!-- 통계 -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card teal">
|
|
<div class="stat-icon">📅</div>
|
|
<div class="stat-value" id="statTxCount">-</div>
|
|
<div class="stat-label">조회 일수</div>
|
|
</div>
|
|
<div class="stat-card blue">
|
|
<div class="stat-icon">📦</div>
|
|
<div class="stat-value" id="statItemCount">-</div>
|
|
<div class="stat-label">총 판매 품목</div>
|
|
</div>
|
|
<div class="stat-card emerald">
|
|
<div class="stat-icon">💰</div>
|
|
<div class="stat-value" id="statAmount">-</div>
|
|
<div class="stat-label">총 매출액</div>
|
|
</div>
|
|
<div class="stat-card purple">
|
|
<div class="stat-icon">📊</div>
|
|
<div class="stat-value" id="statBarcode">-</div>
|
|
<div class="stat-label">바코드 매핑률</div>
|
|
</div>
|
|
<div class="stat-card amber">
|
|
<div class="stat-icon">🏷️</div>
|
|
<div class="stat-value" id="statProducts">-</div>
|
|
<div class="stat-label">고유 상품</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 뷰 컨트롤 -->
|
|
<div class="view-controls">
|
|
<div class="code-toggle">
|
|
<button class="active" data-code="drug" onclick="setCodeView('drug')">상품코드</button>
|
|
<button data-code="barcode" onclick="setCodeView('barcode')">바코드</button>
|
|
<button data-code="standard" onclick="setCodeView('standard')">표준코드</button>
|
|
<button data-code="all" onclick="setCodeView('all')">전체</button>
|
|
</div>
|
|
<div class="view-mode">
|
|
<button class="view-btn active" data-view="group" onclick="setViewMode('group')">📁 거래별</button>
|
|
<button class="view-btn" data-view="list" onclick="setViewMode('list')">📋 목록</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 거래별 뷰 -->
|
|
<div id="groupView" class="transactions-container">
|
|
<div class="loading-state">
|
|
<div class="loading-spinner"></div>
|
|
<div>데이터 로딩 중...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 리스트 뷰 -->
|
|
<div id="listView" class="list-view">
|
|
<div class="list-table-wrap">
|
|
<table class="list-table">
|
|
<thead>
|
|
<tr>
|
|
<th>판매일</th>
|
|
<th>상품명</th>
|
|
<th id="listCodeHeader">상품코드</th>
|
|
<th style="text-align:center">수량</th>
|
|
<th style="text-align:right">단가</th>
|
|
<th style="text-align:right">합계</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="listTableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let rawData = []; // API에서 받은 원본 데이터
|
|
let groupedData = []; // 거래별 그룹화된 데이터
|
|
let currentCodeView = 'drug';
|
|
let currentViewMode = 'group';
|
|
|
|
// ──────────────── 코드 뷰 전환 ────────────────
|
|
function setCodeView(view) {
|
|
currentCodeView = view;
|
|
document.querySelectorAll('.code-toggle button').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.code === view);
|
|
});
|
|
|
|
const headers = {
|
|
'drug': '상품코드',
|
|
'barcode': '바코드',
|
|
'standard': '표준코드',
|
|
'all': '코드 정보'
|
|
};
|
|
document.querySelectorAll('#codeHeader, #listCodeHeader').forEach(el => {
|
|
if (el) el.textContent = headers[view];
|
|
});
|
|
|
|
render();
|
|
}
|
|
|
|
// ──────────────── 뷰 모드 전환 ────────────────
|
|
function setViewMode(mode) {
|
|
currentViewMode = mode;
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.view === mode);
|
|
});
|
|
document.getElementById('groupView').style.display = mode === 'group' ? 'flex' : 'none';
|
|
document.getElementById('listView').classList.toggle('active', mode === 'list');
|
|
}
|
|
|
|
// ──────────────── 코드 렌더링 ────────────────
|
|
function renderCode(item) {
|
|
if (currentCodeView === 'drug') {
|
|
return `<span class="code-badge code-drug">${item.drug_code}</span>`;
|
|
} else if (currentCodeView === 'barcode') {
|
|
if (item.barcode) {
|
|
return `
|
|
<div class="barcode-visual">
|
|
<span class="code-badge code-barcode">${item.barcode}</span>
|
|
${renderBarcodeBars(item.barcode)}
|
|
</div>`;
|
|
}
|
|
return `<span class="code-badge code-na">—</span>`;
|
|
} else if (currentCodeView === 'standard') {
|
|
return item.standard_code
|
|
? `<span class="code-badge code-standard">${item.standard_code}</span>`
|
|
: `<span class="code-badge code-na">—</span>`;
|
|
} else {
|
|
return `
|
|
<div class="code-stack">
|
|
<span class="code-badge code-drug">${item.drug_code}</span>
|
|
${item.barcode
|
|
? `<span class="code-badge code-barcode">${item.barcode}</span>`
|
|
: `<span class="code-badge code-na">바코드 없음</span>`}
|
|
${item.standard_code
|
|
? `<span class="code-badge code-standard">${item.standard_code}</span>`
|
|
: ''}
|
|
</div>`;
|
|
}
|
|
}
|
|
|
|
// 바코드 시각화 바
|
|
function renderBarcodeBars(barcode) {
|
|
const bars = barcode.split('').map(c => {
|
|
const h = 8 + (parseInt(c) || c.charCodeAt(0) % 10) * 1.2;
|
|
return `<span style="height:${h}px"></span>`;
|
|
}).join('');
|
|
return `<div class="barcode-bars">${bars}</div>`;
|
|
}
|
|
|
|
// ──────────────── 포맷 ────────────────
|
|
function formatPrice(num) {
|
|
return new Intl.NumberFormat('ko-KR').format(num);
|
|
}
|
|
|
|
function formatDateTime(dt) {
|
|
if (!dt) return '-';
|
|
const d = new Date(dt);
|
|
return `${d.getMonth()+1}/${d.getDate()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return str.replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
|
}
|
|
|
|
// ──────────────── 데이터 그룹화 (날짜별) ────────────────
|
|
function groupByDate(items) {
|
|
const map = new Map();
|
|
items.forEach(item => {
|
|
const key = item.sale_date;
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
date: item.sale_date,
|
|
items: [],
|
|
total: 0
|
|
});
|
|
}
|
|
const group = map.get(key);
|
|
group.items.push(item);
|
|
group.total += item.total_price || 0;
|
|
});
|
|
return Array.from(map.values()).sort((a, b) =>
|
|
b.date.localeCompare(a.date)
|
|
);
|
|
}
|
|
|
|
// ──────────────── 렌더링 ────────────────
|
|
function render() {
|
|
renderGroupView();
|
|
renderListView();
|
|
}
|
|
|
|
function renderGroupView() {
|
|
const container = document.getElementById('groupView');
|
|
|
|
if (groupedData.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-icon">📭</div>
|
|
<div>판매 내역이 없습니다</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = groupedData.map((tx, idx) => `
|
|
<div class="tx-card" id="tx-${idx}">
|
|
<div class="tx-header" onclick="toggleTransaction(${idx})">
|
|
<div class="tx-info">
|
|
<span class="tx-id">📅 ${tx.date}</span>
|
|
</div>
|
|
<div class="tx-summary">
|
|
<span class="tx-count">${tx.items.length}개 품목</span>
|
|
<span class="tx-amount">${formatPrice(tx.total)}원</span>
|
|
<span class="tx-toggle">▼</span>
|
|
</div>
|
|
</div>
|
|
<div class="tx-items">
|
|
<table class="items-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40%">상품명</th>
|
|
<th id="codeHeader-${idx}">상품코드</th>
|
|
<th style="text-align:right;width:8%">수량</th>
|
|
<th style="text-align:right;width:12%">단가</th>
|
|
<th style="text-align:right;width:12%">합계</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${tx.items.map(item => `
|
|
<tr>
|
|
<td>
|
|
<div class="product-cell">
|
|
${item.thumbnail
|
|
? `<img src="data:image/jpeg;base64,${item.thumbnail}" class="product-thumb" onclick="openImageModal('${item.barcode || ''}', '${item.drug_code || ''}', '${escapeHtml(item.product_name)}')" alt="">`
|
|
: `<div class="product-thumb-placeholder" onclick="openImageModal('${item.barcode || ''}', '${item.drug_code || ''}', '${escapeHtml(item.product_name)}')"><svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h12l-4-5z"/></svg></div>`
|
|
}
|
|
<div class="product-info">
|
|
<span class="product-name">${escapeHtml(item.product_name)}</span>
|
|
${item.supplier ? `<span class="product-supplier">${escapeHtml(item.supplier)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${renderCode(item)}</td>
|
|
<td class="qty">${item.quantity}</td>
|
|
<td class="price">${formatPrice(item.unit_price)}원</td>
|
|
<td class="price total">${formatPrice(item.total_price)}원</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderListView() {
|
|
const tbody = document.getElementById('listTableBody');
|
|
|
|
if (rawData.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="6" class="empty-state">판매 내역이 없습니다</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = rawData.map(item => `
|
|
<tr>
|
|
<td style="color:var(--text-secondary);font-size:12px;">${item.sale_date}</td>
|
|
<td>
|
|
<div class="product-cell">
|
|
${item.thumbnail
|
|
? `<img src="data:image/jpeg;base64,${item.thumbnail}" class="product-thumb" onclick="openImageModal('${item.barcode || ''}', '${item.drug_code || ''}', '${escapeHtml(item.product_name)}')" alt="">`
|
|
: `<div class="product-thumb-placeholder" onclick="openImageModal('${item.barcode || ''}', '${item.drug_code || ''}', '${escapeHtml(item.product_name)}')"><svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h12l-4-5z"/></svg></div>`
|
|
}
|
|
<div class="product-info">
|
|
<span class="product-name">${escapeHtml(item.product_name)}</span>
|
|
${item.supplier ? `<span class="product-supplier">${escapeHtml(item.supplier)}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${renderCode(item)}</td>
|
|
<td style="text-align:center">${item.quantity}</td>
|
|
<td style="text-align:right;font-family:'JetBrains Mono',monospace;">${formatPrice(item.unit_price)}원</td>
|
|
<td style="text-align:right;font-family:'JetBrains Mono',monospace;color:var(--accent-teal);font-weight:600;">${formatPrice(item.total_price)}원</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function toggleTransaction(idx) {
|
|
const card = document.getElementById(`tx-${idx}`);
|
|
card.classList.toggle('open');
|
|
}
|
|
|
|
// ──────────────── 데이터 로드 ────────────────
|
|
function loadSalesData() {
|
|
const period = document.getElementById('periodSelect').value;
|
|
const search = document.getElementById('searchInput').value;
|
|
const barcodeFilter = document.getElementById('barcodeFilter').value;
|
|
|
|
document.getElementById('groupView').innerHTML = `
|
|
<div class="loading-state">
|
|
<div class="loading-spinner"></div>
|
|
<div>데이터 로딩 중...</div>
|
|
</div>`;
|
|
|
|
let url = `/api/sales-detail?days=${period}&barcode=${barcodeFilter}`;
|
|
if (search) url += `&search=${encodeURIComponent(search)}`;
|
|
|
|
fetch(url)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
rawData = data.items;
|
|
groupedData = groupByDate(rawData);
|
|
|
|
// 통계 업데이트
|
|
document.getElementById('statTxCount').textContent = groupedData.length.toLocaleString();
|
|
document.getElementById('statItemCount').textContent = data.stats.total_count.toLocaleString();
|
|
document.getElementById('statAmount').textContent = formatPrice(data.stats.total_amount);
|
|
document.getElementById('statBarcode').textContent = data.stats.barcode_rate + '%';
|
|
document.getElementById('statProducts').textContent = data.stats.unique_products.toLocaleString();
|
|
|
|
render();
|
|
} else {
|
|
document.getElementById('groupView').innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-icon">⚠️</div>
|
|
<div>오류: ${data.error}</div>
|
|
</div>`;
|
|
}
|
|
})
|
|
.catch(err => {
|
|
document.getElementById('groupView').innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-icon">❌</div>
|
|
<div>데이터 로드 실패</div>
|
|
</div>`;
|
|
});
|
|
}
|
|
|
|
// 엔터키 검색
|
|
document.getElementById('searchInput').addEventListener('keypress', e => {
|
|
if (e.key === 'Enter') loadSalesData();
|
|
});
|
|
|
|
// 초기 로드
|
|
loadSalesData();
|
|
|
|
// ──────────────── 이미지 교체 모달 ────────────────
|
|
let imgModalBarcode = null;
|
|
let imgModalDrugCode = null;
|
|
let imgModalName = null;
|
|
let cameraStream = null;
|
|
let capturedImageData = null;
|
|
|
|
function openImageModal(barcode, drugCode, productName) {
|
|
// 바코드나 drug_code 중 하나는 있어야 함
|
|
if (!barcode && !drugCode) {
|
|
showToast('제품 코드 정보가 없습니다', 'error');
|
|
return;
|
|
}
|
|
|
|
imgModalBarcode = barcode || null;
|
|
imgModalDrugCode = drugCode || null;
|
|
imgModalName = productName || (barcode || drugCode);
|
|
|
|
document.getElementById('imgModalProductName').textContent = imgModalName;
|
|
document.getElementById('imgModalCode').textContent = barcode || drugCode;
|
|
document.getElementById('imgUrlInput').value = '';
|
|
|
|
// URL 탭으로 초기화
|
|
switchImageTab('url');
|
|
|
|
document.getElementById('imageModal').classList.add('show');
|
|
document.getElementById('imgUrlInput').focus();
|
|
}
|
|
|
|
function closeImageModal() {
|
|
stopCamera();
|
|
document.getElementById('imageModal').classList.remove('show');
|
|
imgModalBarcode = null;
|
|
imgModalDrugCode = null;
|
|
imgModalName = null;
|
|
capturedImageData = null;
|
|
}
|
|
|
|
function switchImageTab(tab) {
|
|
document.querySelectorAll('.image-modal .tab-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.tab === tab);
|
|
});
|
|
document.querySelectorAll('.image-modal .tab-content').forEach(content => {
|
|
content.classList.toggle('active', content.id === 'tab' + tab.charAt(0).toUpperCase() + tab.slice(1));
|
|
});
|
|
|
|
if (tab === 'camera') {
|
|
startCamera();
|
|
} else {
|
|
stopCamera();
|
|
}
|
|
}
|
|
|
|
async function startCamera() {
|
|
try {
|
|
stopCamera();
|
|
|
|
const constraints = {
|
|
video: {
|
|
facingMode: { ideal: 'environment' },
|
|
width: { ideal: 1920 },
|
|
height: { ideal: 1920 }
|
|
},
|
|
audio: false
|
|
};
|
|
|
|
cameraStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
const video = document.getElementById('cameraVideo');
|
|
video.srcObject = cameraStream;
|
|
video.style.display = 'block';
|
|
|
|
document.getElementById('captureCanvas').style.display = 'none';
|
|
document.getElementById('cameraGuide').style.display = 'block';
|
|
document.getElementById('captureBtn').style.display = 'block';
|
|
document.getElementById('previewBtns').style.display = 'none';
|
|
capturedImageData = null;
|
|
|
|
} catch (err) {
|
|
console.error('카메라 오류:', err);
|
|
showToast('카메라에 접근할 수 없습니다', 'error');
|
|
}
|
|
}
|
|
|
|
function stopCamera() {
|
|
if (cameraStream) {
|
|
cameraStream.getTracks().forEach(track => track.stop());
|
|
cameraStream = null;
|
|
}
|
|
const video = document.getElementById('cameraVideo');
|
|
if (video) video.srcObject = null;
|
|
}
|
|
|
|
function capturePhoto() {
|
|
const video = document.getElementById('cameraVideo');
|
|
const canvas = document.getElementById('captureCanvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
const vw = video.videoWidth;
|
|
const vh = video.videoHeight;
|
|
const minDim = Math.min(vw, vh);
|
|
const cropSize = minDim * 0.8;
|
|
const sx = (vw - cropSize) / 2;
|
|
const sy = (vh - cropSize) / 2;
|
|
|
|
canvas.width = 800;
|
|
canvas.height = 800;
|
|
ctx.drawImage(video, sx, sy, cropSize, cropSize, 0, 0, 800, 800);
|
|
|
|
capturedImageData = canvas.toDataURL('image/jpeg', 0.92);
|
|
|
|
video.style.display = 'none';
|
|
canvas.style.display = 'block';
|
|
document.getElementById('cameraGuide').style.display = 'none';
|
|
document.getElementById('captureBtn').style.display = 'none';
|
|
document.getElementById('previewBtns').style.display = 'flex';
|
|
}
|
|
|
|
function retakePhoto() {
|
|
const video = document.getElementById('cameraVideo');
|
|
const canvas = document.getElementById('captureCanvas');
|
|
|
|
video.style.display = 'block';
|
|
canvas.style.display = 'none';
|
|
document.getElementById('cameraGuide').style.display = 'block';
|
|
document.getElementById('captureBtn').style.display = 'block';
|
|
document.getElementById('previewBtns').style.display = 'none';
|
|
capturedImageData = null;
|
|
}
|
|
|
|
async function submitCapturedImage() {
|
|
if (!capturedImageData) {
|
|
showToast('촬영된 이미지가 없습니다', 'error');
|
|
return;
|
|
}
|
|
|
|
const code = imgModalBarcode || imgModalDrugCode;
|
|
const name = imgModalName;
|
|
const imageData = capturedImageData;
|
|
|
|
closeImageModal();
|
|
showToast(`"${name}" 이미지 저장 중...`, 'info');
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/product-images/${code}/upload`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
image_data: imageData,
|
|
product_name: name,
|
|
drug_code: imgModalDrugCode
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
showToast('✅ 이미지 저장 완료!', 'success');
|
|
loadSalesData(); // 새로고침
|
|
} else {
|
|
showToast(data.error || '저장 실패', 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('오류: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function submitImageUrl() {
|
|
const imageUrl = document.getElementById('imgUrlInput').value.trim();
|
|
|
|
if (!imageUrl) {
|
|
showToast('이미지 URL을 입력하세요', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!imageUrl.startsWith('http')) {
|
|
showToast('올바른 URL을 입력하세요', 'error');
|
|
return;
|
|
}
|
|
|
|
const code = imgModalBarcode || imgModalDrugCode;
|
|
const name = imgModalName;
|
|
|
|
closeImageModal();
|
|
showToast(`"${name}" 이미지 다운로드 중...`, 'info');
|
|
|
|
try {
|
|
const res = await fetch(`/api/admin/product-images/${code}/replace`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
image_url: imageUrl,
|
|
product_name: name,
|
|
drug_code: imgModalDrugCode
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
showToast('✅ 이미지 등록 완료!', 'success');
|
|
loadSalesData(); // 새로고침
|
|
} else {
|
|
showToast(data.error || '등록 실패', 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('오류: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
bottom: 24px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 12px 24px;
|
|
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#6366f1'};
|
|
color: white;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
z-index: 2000;
|
|
animation: fadeIn 0.3s;
|
|
`;
|
|
toast.textContent = message;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 3000);
|
|
}
|
|
|
|
// 모달 외부 클릭시 닫기
|
|
document.getElementById('imageModal').addEventListener('click', e => {
|
|
if (e.target.id === 'imageModal') closeImageModal();
|
|
});
|
|
</script>
|
|
|
|
<!-- 이미지 교체 모달 -->
|
|
<div class="image-modal" id="imageModal">
|
|
<div class="image-modal-content">
|
|
<h3>📷 제품 이미지 등록</h3>
|
|
<div style="background: rgba(139,92,246,0.1); border-radius: 8px; padding: 12px; margin-bottom: 16px;">
|
|
<div style="font-weight: 600;" id="imgModalProductName">제품명</div>
|
|
<div style="font-size: 12px; color: var(--text-muted); font-family: monospace;" id="imgModalCode">코드</div>
|
|
</div>
|
|
|
|
<div class="image-modal-tabs">
|
|
<button class="tab-btn active" data-tab="url" onclick="switchImageTab('url')">🔗 URL 입력</button>
|
|
<button class="tab-btn" data-tab="camera" onclick="switchImageTab('camera')">📸 촬영</button>
|
|
</div>
|
|
|
|
<!-- URL 탭 -->
|
|
<div class="tab-content active" id="tabUrl">
|
|
<input type="text" class="image-input" id="imgUrlInput" placeholder="이미지 URL을 입력하세요...">
|
|
<div class="modal-btns">
|
|
<button class="btn-modal secondary" onclick="closeImageModal()">취소</button>
|
|
<button class="btn-modal primary" onclick="submitImageUrl()">등록하기</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 카메라 탭 -->
|
|
<div class="tab-content" id="tabCamera">
|
|
<div class="camera-container">
|
|
<video id="cameraVideo" autoplay playsinline></video>
|
|
<canvas id="captureCanvas" style="display:none;"></canvas>
|
|
<div class="camera-guide" id="cameraGuide">
|
|
<svg width="100%" height="100%" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
<rect x="10" y="10" width="80" height="80" fill="none" stroke="rgba(139,92,246,0.5)" stroke-width="0.5" stroke-dasharray="2,2"/>
|
|
<path d="M10,10 L20,10 M10,10 L10,20" fill="none" stroke="#a855f7" stroke-width="1"/>
|
|
<path d="M90,10 L80,10 M90,10 L90,20" fill="none" stroke="#a855f7" stroke-width="1"/>
|
|
<path d="M10,90 L20,90 M10,90 L10,80" fill="none" stroke="#a855f7" stroke-width="1"/>
|
|
<path d="M90,90 L80,90 M90,90 L90,80" fill="none" stroke="#a855f7" stroke-width="1"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="modal-btns" id="captureBtn">
|
|
<button class="btn-modal secondary" onclick="closeImageModal()">취소</button>
|
|
<button class="btn-modal primary" onclick="capturePhoto()">📸 촬영</button>
|
|
</div>
|
|
<div class="modal-btns" id="previewBtns" style="display:none;">
|
|
<button class="btn-modal secondary" onclick="retakePhoto()">다시 촬영</button>
|
|
<button class="btn-modal primary" onclick="submitCapturedImage()">저장하기</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|