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() {
|
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');
|
var saveBtn = document.getElementById('nextBtn');
|
||||||
if (saveBtn) {
|
if (saveBtn) {
|
||||||
@@ -538,10 +548,12 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('TBM 저장 오류:', error);
|
console.error('TBM 저장 오류:', error);
|
||||||
showToast('TBM 저장 중 오류가 발생했습니다: ' + error.message, 'error');
|
showToast('TBM 저장 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||||
|
if (overlay) overlay.style.display = 'none';
|
||||||
if (saveBtn) {
|
if (saveBtn) {
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
saveBtn.textContent = '저장';
|
saveBtn.textContent = '저장';
|
||||||
}
|
}
|
||||||
|
_saving = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
-webkit-font-smoothing: antialiased;
|
-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) {
|
@media (min-width: 480px) {
|
||||||
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
||||||
@@ -808,7 +813,7 @@
|
|||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div id="loadingOverlay" class="loading-overlay">
|
<div id="loadingOverlay" class="loading-overlay">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
<div class="loading-text">데이터를 불러오는 중...</div>
|
<div class="loading-text" id="loadingText">데이터를 불러오는 중...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
|
|||||||
@@ -15,6 +15,13 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
-webkit-font-smoothing: antialiased;
|
-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) {
|
@media (min-width: 480px) {
|
||||||
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
||||||
@@ -824,10 +831,43 @@
|
|||||||
from { opacity: 1; transform: translateY(0); }
|
from { opacity: 1; transform: translateY(0); }
|
||||||
to { opacity: 0; transform: translateY(-10px); }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="m-header">
|
<div class="m-header">
|
||||||
<div class="m-header-top">
|
<div class="m-header-top">
|
||||||
@@ -1077,6 +1117,24 @@
|
|||||||
var pickerWpStep = 'category'; // 'category' | 'place'
|
var pickerWpStep = 'category'; // 'category' | 'place'
|
||||||
var pickerSelectedCatId = null;
|
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() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
@@ -1088,10 +1146,12 @@
|
|||||||
String(now.getDate()).padStart(2,'0') + ' (' + days[now.getDay()] + ')';
|
String(now.getDate()).padStart(2,'0') + ' (' + days[now.getDay()] + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
var checks = 0;
|
try {
|
||||||
while (!window.apiCall && checks < 50) {
|
await window.waitForApi(8000);
|
||||||
await new Promise(function(r) { setTimeout(r, 100); });
|
} catch(e) {
|
||||||
checks++;
|
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') || '{}');
|
currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||||
@@ -1310,6 +1370,9 @@
|
|||||||
// ─── 세부 편집 바텀시트 ───
|
// ─── 세부 편집 바텀시트 ───
|
||||||
|
|
||||||
window.openDetailEditSheet = async function(sid) {
|
window.openDetailEditSheet = async function(sid) {
|
||||||
|
if (isBusy('detailEdit')) return;
|
||||||
|
setBusy('detailEdit');
|
||||||
|
showLoading('불러오는 중...');
|
||||||
deSessionId = sid;
|
deSessionId = sid;
|
||||||
deSelected = {};
|
deSelected = {};
|
||||||
try {
|
try {
|
||||||
@@ -1351,6 +1414,9 @@
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('세부 편집 로드 오류:', e);
|
console.error('세부 편집 로드 오류:', e);
|
||||||
window.showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
window.showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
clearBusy('detailEdit');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1655,6 +1721,7 @@
|
|||||||
window.closeDetailEditSheet = function() {
|
window.closeDetailEditSheet = function() {
|
||||||
document.getElementById('detailEditOverlay').style.display = 'none';
|
document.getElementById('detailEditOverlay').style.display = 'none';
|
||||||
document.getElementById('detailEditSheet').style.display = 'none';
|
document.getElementById('detailEditSheet').style.display = 'none';
|
||||||
|
clearBusy('detailEdit');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 저장 (부분 입력도 허용)
|
// 저장 (부분 입력도 허용)
|
||||||
@@ -1732,6 +1799,9 @@
|
|||||||
var completeTeamMembers = [];
|
var completeTeamMembers = [];
|
||||||
|
|
||||||
window.completeTbm = async function(sid) {
|
window.completeTbm = async function(sid) {
|
||||||
|
if (isBusy('complete')) return;
|
||||||
|
setBusy('complete');
|
||||||
|
showLoading('확인 중...');
|
||||||
completeSessionId = sid;
|
completeSessionId = sid;
|
||||||
try {
|
try {
|
||||||
var teamRes = await window.apiCall('/tbm/sessions/' + sid + '/team');
|
var teamRes = await window.apiCall('/tbm/sessions/' + sid + '/team');
|
||||||
@@ -1755,6 +1825,9 @@
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
window.showToast('팀원 조회 중 오류가 발생했습니다.', 'error');
|
window.showToast('팀원 조회 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
clearBusy('complete');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1901,6 +1974,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.openSplitSheet = async function(memberIdx) {
|
window.openSplitSheet = async function(memberIdx) {
|
||||||
|
if (isBusy('split')) return;
|
||||||
|
setBusy('split');
|
||||||
|
showLoading('불러오는 중...');
|
||||||
splitMemberIdx = memberIdx;
|
splitMemberIdx = memberIdx;
|
||||||
splitOption = 'keep';
|
splitOption = 'keep';
|
||||||
splitTargetSessionId = null;
|
splitTargetSessionId = null;
|
||||||
@@ -1936,6 +2012,8 @@
|
|||||||
|
|
||||||
document.getElementById('splitOverlay').style.display = 'block';
|
document.getElementById('splitOverlay').style.display = 'block';
|
||||||
document.getElementById('splitSheet').style.display = 'block';
|
document.getElementById('splitSheet').style.display = 'block';
|
||||||
|
hideLoading();
|
||||||
|
clearBusy('split');
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadSplitSessionList() {
|
async function loadSplitSessionList() {
|
||||||
@@ -1982,6 +2060,7 @@
|
|||||||
window.closeSplitSheet = function() {
|
window.closeSplitSheet = function() {
|
||||||
document.getElementById('splitOverlay').style.display = 'none';
|
document.getElementById('splitOverlay').style.display = 'none';
|
||||||
document.getElementById('splitSheet').style.display = 'none';
|
document.getElementById('splitSheet').style.display = 'none';
|
||||||
|
clearBusy('split');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.saveSplit = async function() {
|
window.saveSplit = async function() {
|
||||||
@@ -2082,6 +2161,9 @@
|
|||||||
var myDraftSession = null; // 내 draft TBM
|
var myDraftSession = null; // 내 draft TBM
|
||||||
|
|
||||||
window.openPullSheet = async function(sid) {
|
window.openPullSheet = async function(sid) {
|
||||||
|
if (isBusy('pull')) return;
|
||||||
|
setBusy('pull');
|
||||||
|
showLoading('불러오는 중...');
|
||||||
pullSessionId = sid;
|
pullSessionId = sid;
|
||||||
try {
|
try {
|
||||||
var res = await window.apiCall('/tbm/sessions/' + sid + '/team');
|
var res = await window.apiCall('/tbm/sessions/' + sid + '/team');
|
||||||
@@ -2134,12 +2216,16 @@
|
|||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('빼오기 로드 오류:', e);
|
console.error('빼오기 로드 오류:', e);
|
||||||
window.showToast('데이터를 불러올 수 없습니다.', 'error');
|
window.showToast('데이터를 불러올 수 없습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
clearBusy('pull');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.closePullSheet = function() {
|
window.closePullSheet = function() {
|
||||||
document.getElementById('pullOverlay').style.display = 'none';
|
document.getElementById('pullOverlay').style.display = 'none';
|
||||||
document.getElementById('pullSheet').style.display = 'none';
|
document.getElementById('pullSheet').style.display = 'none';
|
||||||
|
clearBusy('pull');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.startPull = async function(workerId, workerName, maxHours) {
|
window.startPull = async function(workerId, workerName, maxHours) {
|
||||||
|
|||||||
Reference in New Issue
Block a user