feat: QR 자산 스캔 점검, 모바일 웹뷰 및 관리자 승인 시스템 구현 (DB 기반 맵 좌표 저장 단일화 포함)

This commit is contained in:
이태훈
2026-06-23 16:39:14 +09:00
parent 9f165faf13
commit f36e8e93e2
21 changed files with 2357 additions and 46 deletions

299
mobile.html Normal file
View File

@@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ITAM 모바일 실사 점검</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css" />
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js"></script>
<style>
:root {
--bg: #09090b;
--card: #18181b;
--card-border: #27272a;
--primary: #3b82f6;
--primary-hover: #2563eb;
--success: #10b981;
--danger: #ef4444;
--text: #f4f4f5;
--text-muted: #a1a1aa;
--font-family: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: var(--font-family);
}
body {
background-color: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
header {
background-color: var(--card);
border-bottom: 1px solid var(--card-border);
padding: 1rem;
text-align: center;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
header h1 {
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(135deg, #60a5fa, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--success);
box-shadow: 0 0 8px var(--success);
}
main {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
gap: 1rem;
overflow-y: auto;
align-items: center;
justify-content: center;
}
/* Scanner Viewport */
.scanner-container {
width: 100%;
max-width: 400px;
aspect-ratio: 1;
border-radius: 16px;
overflow: hidden;
border: 2px dashed var(--card-border);
position: relative;
background-color: #000;
}
#reader {
width: 100% !important;
height: 100% !important;
border: none !important;
}
#reader video {
object-fit: cover !important;
width: 100% !important;
height: 100% !important;
}
/* Scan Laser Line Animation */
.scan-laser {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(to right, transparent, var(--primary), transparent);
animation: scan 2s linear infinite;
z-index: 10;
pointer-events: none;
}
@keyframes scan {
0% { top: 0%; }
50% { top: 100%; }
100% { top: 0%; }
}
/* Bottom Info Card */
.info-panel {
width: 100%;
max-width: 400px;
background-color: var(--card);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
flex-shrink: 0;
}
.info-section {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.info-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.info-value {
font-size: 0.95rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 24px;
}
.badge-lock {
background-color: rgba(59, 130, 246, 0.15);
color: var(--primary);
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-size: 0.8rem;
border: 1px solid rgba(59, 130, 246, 0.3);
font-weight: 700;
}
.badge-empty {
color: var(--text-muted);
font-weight: 400;
font-style: italic;
}
.btn-action {
background-color: var(--primary);
color: var(--text);
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-action:hover {
background-color: var(--primary-hover);
}
.btn-action.btn-danger {
background-color: rgba(239, 68, 68, 0.15);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn-action.btn-danger:hover {
background-color: rgba(239, 68, 68, 0.25);
}
/* Manual Input Section */
.manual-toggle {
text-align: center;
font-size: 0.8rem;
color: var(--primary);
cursor: pointer;
text-decoration: underline;
}
.manual-form {
display: none;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
.input-field {
width: 100%;
background-color: var(--bg);
border: 1px solid var(--card-border);
border-radius: 8px;
color: var(--text);
padding: 0.5rem;
font-size: 0.9rem;
}
.input-field:focus {
outline: 1px solid var(--primary);
}
/* Feedbacks Overlay */
.feedback-message {
text-align: center;
padding: 0.5rem;
border-radius: 8px;
font-size: 0.85rem;
display: none;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.feedback-success {
background-color: rgba(16, 185, 129, 0.15);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.feedback-error {
background-color: rgba(239, 68, 68, 0.15);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
</style>
</head>
<body>
<header>
<h1>ITAM 모바일 실사</h1>
<div class="status-dot"></div>
</header>
<main>
<div class="scanner-container">
<div id="reader"></div>
<div class="scan-laser"></div>
</div>
<div class="info-panel">
<!-- 1. 위치 락 정보 -->
<div class="info-section">
<span class="info-label">현재 점검 위치 (Location)</span>
<div class="info-value">
<span id="loc-display" class="badge-empty">위치 QR 코드를 먼저 스캔하세요.</span>
<button id="btn-unlock-loc" class="btn-action btn-danger" style="display: none;">해제</button>
</div>
</div>
<hr style="border: 0; border-top: 1px solid var(--card-border); margin: 0.25rem 0;" />
<!-- 2. 자산 스캔 결과 및 피드백 -->
<div id="scan-feedback" class="feedback-message"></div>
<!-- 3. 수동 입력 토글 및 양식 -->
<div class="info-section">
<span id="btn-toggle-manual" class="manual-toggle">카메라가 안 되나요? 수동 코드로 입력</span>
<div id="manual-form" class="manual-form">
<input type="text" id="manual-code-input" class="input-field" placeholder="위치 또는 자산 코드 입력" />
<button id="btn-submit-manual" class="btn-action w-full">입력 확인</button>
</div>
</div>
</div>
</main>
<script type="module" src="/src/mobile-main.ts"></script>
</body>
</html>