fix(mobile): TBM 모바일 버튼 반응성 개선 + 로딩 오버레이 추가
touch-action: manipulation으로 더블탭 줌 방지, busy guard로 중복 호출 차단, waitForApi 전환, CSS 스피너 로딩 오버레이로 비동기 작업 피드백 제공 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -480,7 +480,17 @@
|
||||
|
||||
// ==================== 저장 ====================
|
||||
|
||||
var _saving = false;
|
||||
async function saveWizard() {
|
||||
if (_saving) return;
|
||||
_saving = true;
|
||||
// 로딩 오버레이 표시
|
||||
var overlay = document.getElementById('loadingOverlay');
|
||||
var loadingText = document.getElementById('loadingText');
|
||||
if (overlay) {
|
||||
if (loadingText) loadingText.textContent = '저장 중...';
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
// 저장 버튼 비활성화
|
||||
var saveBtn = document.getElementById('nextBtn');
|
||||
if (saveBtn) {
|
||||
@@ -538,10 +548,12 @@
|
||||
} catch (error) {
|
||||
console.error('TBM 저장 오류:', error);
|
||||
showToast('TBM 저장 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
if (saveBtn) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '저장';
|
||||
}
|
||||
_saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
padding: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
button, .worker-card, .list-item, .list-item-skip, .pill-btn, .pill-btn-add,
|
||||
.nav-btn, .select-all-btn, [onclick] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@media (min-width: 480px) {
|
||||
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
||||
@@ -808,7 +813,7 @@
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loadingOverlay" class="loading-overlay">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">데이터를 불러오는 중...</div>
|
||||
<div class="loading-text" id="loadingText">데이터를 불러오는 중...</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
|
||||
@@ -15,6 +15,13 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
button, .m-tbm-row, .m-tab, .m-new-btn, .m-detail-btn, .m-load-more,
|
||||
.picker-item, .split-radio-item, .split-session-item, .pull-btn,
|
||||
.de-save-btn, .de-group-btn, .de-split-btn, .pill-btn, .worker-card,
|
||||
[onclick] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@media (min-width: 480px) {
|
||||
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
||||
@@ -824,10 +831,43 @@
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
/* Loading overlay */
|
||||
.m-loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(255,255,255,0.75);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.m-loading-overlay.active { display: flex; }
|
||||
.m-loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.m-loading-text {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loadingOverlay" class="m-loading-overlay">
|
||||
<div class="m-loading-spinner"></div>
|
||||
<div class="m-loading-text" id="loadingText">불러오는 중...</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="m-header">
|
||||
<div class="m-header-top">
|
||||
@@ -1077,6 +1117,24 @@
|
||||
var pickerWpStep = 'category'; // 'category' | 'place'
|
||||
var pickerSelectedCatId = null;
|
||||
|
||||
// busy guard - 비동기 함수 중복 호출 방지
|
||||
var _busy = {};
|
||||
function isBusy(key) { return !!_busy[key]; }
|
||||
function setBusy(key) { _busy[key] = true; }
|
||||
function clearBusy(key) { delete _busy[key]; }
|
||||
|
||||
function showLoading(msg) {
|
||||
var el = document.getElementById('loadingOverlay');
|
||||
if (el) {
|
||||
document.getElementById('loadingText').textContent = msg || '불러오는 중...';
|
||||
el.classList.add('active');
|
||||
}
|
||||
}
|
||||
function hideLoading() {
|
||||
var el = document.getElementById('loadingOverlay');
|
||||
if (el) el.classList.remove('active');
|
||||
}
|
||||
|
||||
// 초기화
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
var now = new Date();
|
||||
@@ -1088,10 +1146,12 @@
|
||||
String(now.getDate()).padStart(2,'0') + ' (' + days[now.getDay()] + ')';
|
||||
}
|
||||
|
||||
var checks = 0;
|
||||
while (!window.apiCall && checks < 50) {
|
||||
await new Promise(function(r) { setTimeout(r, 100); });
|
||||
checks++;
|
||||
try {
|
||||
await window.waitForApi(8000);
|
||||
} catch(e) {
|
||||
document.getElementById('tbmContent').innerHTML =
|
||||
'<div class="m-empty"><div class="m-empty-icon">⚠</div><div class="m-empty-text">서버 연결에 실패했습니다</div><div class="m-empty-sub">페이지를 새로고침해 주세요</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
@@ -1310,6 +1370,9 @@
|
||||
// ─── 세부 편집 바텀시트 ───
|
||||
|
||||
window.openDetailEditSheet = async function(sid) {
|
||||
if (isBusy('detailEdit')) return;
|
||||
setBusy('detailEdit');
|
||||
showLoading('불러오는 중...');
|
||||
deSessionId = sid;
|
||||
deSelected = {};
|
||||
try {
|
||||
@@ -1351,6 +1414,9 @@
|
||||
} catch(e) {
|
||||
console.error('세부 편집 로드 오류:', e);
|
||||
window.showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
clearBusy('detailEdit');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1655,6 +1721,7 @@
|
||||
window.closeDetailEditSheet = function() {
|
||||
document.getElementById('detailEditOverlay').style.display = 'none';
|
||||
document.getElementById('detailEditSheet').style.display = 'none';
|
||||
clearBusy('detailEdit');
|
||||
};
|
||||
|
||||
// 저장 (부분 입력도 허용)
|
||||
@@ -1732,6 +1799,9 @@
|
||||
var completeTeamMembers = [];
|
||||
|
||||
window.completeTbm = async function(sid) {
|
||||
if (isBusy('complete')) return;
|
||||
setBusy('complete');
|
||||
showLoading('확인 중...');
|
||||
completeSessionId = sid;
|
||||
try {
|
||||
var teamRes = await window.apiCall('/tbm/sessions/' + sid + '/team');
|
||||
@@ -1755,6 +1825,9 @@
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
window.showToast('팀원 조회 중 오류가 발생했습니다.', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
clearBusy('complete');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1901,6 +1974,9 @@
|
||||
}
|
||||
|
||||
window.openSplitSheet = async function(memberIdx) {
|
||||
if (isBusy('split')) return;
|
||||
setBusy('split');
|
||||
showLoading('불러오는 중...');
|
||||
splitMemberIdx = memberIdx;
|
||||
splitOption = 'keep';
|
||||
splitTargetSessionId = null;
|
||||
@@ -1936,6 +2012,8 @@
|
||||
|
||||
document.getElementById('splitOverlay').style.display = 'block';
|
||||
document.getElementById('splitSheet').style.display = 'block';
|
||||
hideLoading();
|
||||
clearBusy('split');
|
||||
};
|
||||
|
||||
async function loadSplitSessionList() {
|
||||
@@ -1982,6 +2060,7 @@
|
||||
window.closeSplitSheet = function() {
|
||||
document.getElementById('splitOverlay').style.display = 'none';
|
||||
document.getElementById('splitSheet').style.display = 'none';
|
||||
clearBusy('split');
|
||||
};
|
||||
|
||||
window.saveSplit = async function() {
|
||||
@@ -2082,6 +2161,9 @@
|
||||
var myDraftSession = null; // 내 draft TBM
|
||||
|
||||
window.openPullSheet = async function(sid) {
|
||||
if (isBusy('pull')) return;
|
||||
setBusy('pull');
|
||||
showLoading('불러오는 중...');
|
||||
pullSessionId = sid;
|
||||
try {
|
||||
var res = await window.apiCall('/tbm/sessions/' + sid + '/team');
|
||||
@@ -2134,12 +2216,16 @@
|
||||
} catch(e) {
|
||||
console.error('빼오기 로드 오류:', e);
|
||||
window.showToast('데이터를 불러올 수 없습니다.', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
clearBusy('pull');
|
||||
}
|
||||
};
|
||||
|
||||
window.closePullSheet = function() {
|
||||
document.getElementById('pullOverlay').style.display = 'none';
|
||||
document.getElementById('pullSheet').style.display = 'none';
|
||||
clearBusy('pull');
|
||||
};
|
||||
|
||||
window.startPull = async function(workerId, workerName, maxHours) {
|
||||
|
||||
Reference in New Issue
Block a user