Feat: --port 옵션, VWorld 맵 타일, 구글 드라이브 업로드 추가

This commit is contained in:
user01
2026-05-16 03:12:42 +09:00
parent 43f23c52fc
commit e3fb8de5c5
2 changed files with 451 additions and 8 deletions

View File

@@ -16,9 +16,10 @@
</div>
<div class="header-right">
<form id="uploadForm" enctype="multipart/form-data">
<label for="fileInput" class="btn btn-primary"><i class="fas fa-upload"></i> 엑셀 업로드</label>
<label for="fileInput" class="btn btn-primary"><i class="fas fa-upload"></i> 로컬 업로드</label>
<input type="file" id="fileInput" accept=".xlsx,.xls" hidden />
</form>
<button class="btn btn-info" onclick="openGoogleDriveModal()"><i class="fab fa-google-drive"></i> 구글 드라이브</button>
<button class="btn btn-success" onclick="exportExcel()"><i class="fas fa-download"></i> 내보내기</button>
<button class="btn btn-danger" onclick="resetAssignments()"><i class="fas fa-undo"></i> 초기화</button>
<span id="statusBadge" class="status-badge"></span>
@@ -72,6 +73,30 @@
</div>
</div>
<div id="googleDriveModal" class="modal-overlay hidden">
<div class="modal-content" style="width: 500px;">
<div class="modal-header">
<h3><i class="fab fa-google-drive"></i> 구글 드라이브에서 불러오기</h3>
<button class="modal-close" onclick="closeGoogleDriveModal()" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #666;">&times;</button>
</div>
<div class="modal-body">
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; font-weight: bold;">파일 ID 또는 공유 링크</label>
<input type="text" id="googleDriveInput" placeholder="파일 ID 또는 https://drive.google.com/file/d/FILE_ID/view"
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box;" />
<div style="margin-top: 8px; font-size: 12px; color: #666;">
<p>• 구글 드라이브에서 공유된 파일의 링크를 붙여넣으세요</p>
<p>• 또는 파일 ID만 입력하세요 (예: 1a2b3c4d5e...)</p>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button onclick="closeGoogleDriveModal()" class="btn" style="background: #ddd; color: #333;">취소</button>
<button onclick="downloadFromGoogleDrive()" class="btn btn-primary">다운로드</button>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const YONGSAN = [37.5326, 126.9906];
@@ -226,12 +251,60 @@ function renderTabBar() {
// ── Map ──
function init() {
let tileLayer = null;
let mapSourceType = 'openstreetmap'; // 'vworld' 또는 'openstreetmap'
let vworldApiKey = null;
function setMapTileLayer(useVWorld = false) {
if (tileLayer) {
map.removeLayer(tileLayer);
}
if (useVWorld && vworldApiKey) {
tileLayer = L.tileLayer(
`https://api.vworld.kr/req/wmts/1.0.0/${vworldApiKey}/Base/{z}/{y}/{x}.png`,
{
maxZoom: 19,
minZoom: 1,
attribution: '&copy; <a href="http://www.vworld.kr" target="_blank">VWorld</a>',
errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
}
).addTo(map);
mapSourceType = 'vworld';
} else {
// OpenStreetMap (기본값)
tileLayer = L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
{
maxZoom: 19,
minZoom: 1,
attribution: '&copy; OpenStreetMap contributors',
}
).addTo(map);
mapSourceType = 'openstreetmap';
console.log('[지도] OpenStreetMap 로드됨');
}
}
async function init() {
map = L.map('map').setView(YONGSAN, 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '&copy; OpenStreetMap contributors',
}).addTo(map);
// VWorld API 상태 확인 및 맵 초기화
try {
const statusResp = await fetch('/api/vworld-api-status');
const statusData = await statusResp.json();
console.log('[지도] VWorld API 상태:', statusData);
if (statusData.valid && statusData.key) {
vworldApiKey = statusData.key;
setMapTileLayer(true); // VWorld 시도
} else {
setMapTileLayer(false); // OpenStreetMap 사용
}
} catch (e) {
console.warn('[지도] VWorld API 상태 확인 실패:', e);
setMapTileLayer(false); // 에러 시 OpenStreetMap 사용
}
const mc = document.getElementById('mapContainer');
mc.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; document.getElementById('dropOverlay').classList.add('active'); });
@@ -357,6 +430,95 @@ async function uploadExcel(e) {
e.target.value = '';
}
// ── Google Drive Integration ──
function openGoogleDriveModal() {
if (!activeTabId) {
showToast('먼저 탭을 선택하세요', 'warning');
return;
}
document.getElementById('googleDriveModal').classList.remove('hidden');
document.getElementById('googleDriveInput').value = '';
document.getElementById('googleDriveInput').focus();
}
function closeGoogleDriveModal() {
document.getElementById('googleDriveModal').classList.add('hidden');
}
function extractFileId(input) {
// 링크에서 파일 ID 추출: https://drive.google.com/file/d/FILE_ID/view
let match = input.match(/\/d\/([a-zA-Z0-9-_]+)/);
if (match) return match[1];
// 또는 링크에서: https://drive.google.com/open?id=FILE_ID
match = input.match(/id=([a-zA-Z0-9-_]+)/);
if (match) return match[1];
// 파일 ID 자체일 수도 있음
if (input.match(/^[a-zA-Z0-9-_]+$/)) return input;
return null;
}
async function downloadFromGoogleDrive() {
const input = document.getElementById('googleDriveInput').value.trim();
if (!input) {
showToast('파일 ID 또는 링크를 입력하세요', 'warning');
return;
}
const fileId = extractFileId(input);
if (!fileId) {
showToast('올바르지 않은 파일 ID 또는 링크입니다', 'error');
return;
}
closeGoogleDriveModal();
showLoadingModal();
updateLoadingModal('구글 드라이브에서 파일을 다운로드 중...');
let pollInterval = null;
try {
pollInterval = setInterval(async () => {
const active = await pollUploadProgress();
if (!active) {
clearInterval(pollInterval);
pollInterval = null;
}
}, 500);
const r = await api('/api/google-drive-upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileId: fileId, tab: activeTabId }),
});
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
await pollUploadProgress();
console.log('[구글 드라이브 업로드 결과] 근무자:', r.workers?.length || 0, '명, 근무처:', r.workplaces?.length || 0, '개');
tabDataCache[activeTabId] = r;
render();
await loadTabs();
updateLoadingModal('완료!');
setTimeout(hideLoadingModal, 500);
showToast(`파일 로드 완료: 근무자 ${r.workers?.length || 0}명, 근무처 ${r.workplaces?.length || 0}`, 'success');
} catch (err) {
if (pollInterval) {
clearInterval(pollInterval);
}
console.error('[구글 드라이브 다운로드 실패]', err);
hideLoadingModal();
showToast('다운로드 실패: ' + err.message, 'error');
}
}
// ── Rendering ──
function render() {