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:
Hyungi Ahn
2026-02-27 08:40:46 +09:00
parent 5183e9ff85
commit af4bd26b06
3 changed files with 108 additions and 5 deletions

View File

@@ -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;
}
}

View File

@@ -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 -->

View File

@@ -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">&#9888;</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) {