feat: 모바일 UX 대폭 개선 + PWA 구현 + 로그인 루프 수정
- 모바일 하단 네비: 메뉴 제거, 4개 핵심 기능(홈/TBM/작업보고/출근) SVG 아이콘 - 모바일 사이드바 스킵: 768px 이하에서 사이드바 미로드, 레이아웃 오프셋 해결 - 모바일 헤더: 햄버거 메뉴 숨김, 본문 margin/overflow 정리 - TBM 모바일: 풀스크린 모달, 저장 버튼 하단 고정, 터치 UX 개선 - PWA: manifest.json, sw.js(network-first), 앱 아이콘, iOS 메타태그, 킬스위치 - 로그인 무한루프 수정: 토큰 만료 검증, 쿠키 정리, loginPage 경로 수정 - 신고 메뉴 tkreport 리다이렉트: navbar + sidebar cross-system-link 적용 - TBM API: 작업장별 안전점검 체크리스트 조회 엔드포인트 추가 - 안전점검 체크리스트 관리 UI 개선 - tkuser: 이슈유형 관리 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -146,7 +146,7 @@ function populateWorkTypeSelects() {
|
||||
const modalSelect = document.getElementById('modalWorkType');
|
||||
|
||||
const options = workTypes.map(wt =>
|
||||
`<option value="${wt.work_type_id}">${wt.work_type_name}</option>`
|
||||
`<option value="${wt.id}">${wt.name}</option>`
|
||||
).join('');
|
||||
|
||||
if (filterSelect) {
|
||||
@@ -204,7 +204,7 @@ function renderBasicChecks() {
|
||||
console.log('기본 체크항목:', basicChecks.length, '개');
|
||||
|
||||
if (basicChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.');
|
||||
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.') + renderInlineAddStandalone('basic');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ function renderBasicChecks() {
|
||||
|
||||
container.innerHTML = Object.entries(grouped).map(([category, items]) =>
|
||||
renderChecklistGroup(category, items)
|
||||
).join('');
|
||||
).join('') + renderInlineAddStandalone('basic');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,8 +229,10 @@ function renderWeatherChecks() {
|
||||
weatherChecks = weatherChecks.filter(c => c.weather_condition === filterValue);
|
||||
}
|
||||
|
||||
const inlineRow = filterValue ? renderInlineAddStandalone('weather') : '';
|
||||
|
||||
if (weatherChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.');
|
||||
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.') + inlineRow;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,7 +245,7 @@ function renderWeatherChecks() {
|
||||
const name = conditionInfo?.condition_name || condition;
|
||||
|
||||
return renderChecklistGroup(`${icon} ${name}`, items, condition);
|
||||
}).join('');
|
||||
}).join('') + inlineRow;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,6 +256,12 @@ function renderTaskChecks() {
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
|
||||
// 공정 미선택 시 안내
|
||||
if (!workTypeId) {
|
||||
container.innerHTML = renderGuideState('공정을 먼저 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
let taskChecks = allChecks.filter(c => c.check_type === 'task');
|
||||
|
||||
if (taskId) {
|
||||
@@ -264,8 +272,10 @@ function renderTaskChecks() {
|
||||
taskChecks = taskChecks.filter(c => taskIds.includes(c.task_id));
|
||||
}
|
||||
|
||||
const inlineRow = taskId ? renderInlineAddStandalone('task') : '';
|
||||
|
||||
if (taskChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.');
|
||||
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.') + inlineRow;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,7 +287,7 @@ function renderTaskChecks() {
|
||||
const taskName = task?.task_name || `작업 ${taskId}`;
|
||||
|
||||
return renderChecklistGroup(`📋 ${taskName}`, items, null, taskId);
|
||||
}).join('');
|
||||
}).join('') + inlineRow;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,6 +394,18 @@ function renderEmptyState(message) {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 안내 상태 렌더링 (필터 미선택 시)
|
||||
*/
|
||||
function renderGuideState(message) {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">👆</div>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 필터 변경
|
||||
*/
|
||||
@@ -409,7 +431,7 @@ async function filterByWorkType() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
|
||||
const response = await apiCall(`/tasks/by-work-type/${workTypeId}`);
|
||||
if (response && response.success) {
|
||||
tasks = response.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
@@ -446,7 +468,7 @@ async function loadModalTasks() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
|
||||
const response = await apiCall(`/tasks/by-work-type/${workTypeId}`);
|
||||
if (response && response.success) {
|
||||
const modalTasks = response.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
@@ -486,6 +508,29 @@ function openAddModal() {
|
||||
}
|
||||
|
||||
toggleConditionalFields();
|
||||
|
||||
// 날씨별 탭: 현재 필터의 날씨 조건 반영
|
||||
if (currentTab === 'weather') {
|
||||
const weatherFilter = document.getElementById('weatherFilter')?.value;
|
||||
if (weatherFilter) {
|
||||
document.getElementById('weatherCondition').value = weatherFilter;
|
||||
}
|
||||
}
|
||||
|
||||
// 작업별 탭: 현재 필터의 공정/작업 반영
|
||||
if (currentTab === 'task') {
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
if (workTypeId) {
|
||||
document.getElementById('modalWorkType').value = workTypeId;
|
||||
loadModalTasks().then(() => {
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
if (taskId) {
|
||||
document.getElementById('modalTask').value = taskId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showModal();
|
||||
}
|
||||
|
||||
@@ -660,6 +705,132 @@ async function deleteCheck(checkId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 추가 행 렌더링
|
||||
*/
|
||||
function renderInlineAddRow(tabType) {
|
||||
if (tabType === 'basic') {
|
||||
const categoryOptions = Object.entries(CATEGORIES)
|
||||
.filter(([key]) => !['WEATHER', 'TASK'].includes(key))
|
||||
.map(([key, val]) => `<option value="${key}">${val.name}</option>`)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<select class="inline-add-select" id="inlineCategory">${categoryOptions}</select>
|
||||
<input type="text" class="inline-add-input" id="inlineBasicInput"
|
||||
placeholder="새 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('basic');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('basic')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tabType === 'weather') {
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<input type="text" class="inline-add-input" id="inlineWeatherInput"
|
||||
placeholder="새 날씨별 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('weather');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('weather')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tabType === 'task') {
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<input type="text" class="inline-add-input" id="inlineTaskInput"
|
||||
placeholder="새 작업별 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('task');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('task')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 추가 행을 standalone 컨테이너로 감싸기 (빈 상태용)
|
||||
*/
|
||||
function renderInlineAddStandalone(tabType) {
|
||||
return `<div class="inline-add-standalone">${renderInlineAddRow(tabType)}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인으로 체크 항목 추가
|
||||
*/
|
||||
async function addInlineCheck(tabType) {
|
||||
let checkItem, data;
|
||||
|
||||
if (tabType === 'basic') {
|
||||
const input = document.getElementById('inlineBasicInput');
|
||||
const categorySelect = document.getElementById('inlineCategory');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
data = {
|
||||
check_type: 'basic',
|
||||
check_item: checkItem,
|
||||
check_category: categorySelect?.value || 'PPE',
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else if (tabType === 'weather') {
|
||||
const input = document.getElementById('inlineWeatherInput');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
const weatherFilter = document.getElementById('weatherFilter')?.value;
|
||||
if (!weatherFilter) {
|
||||
showToast('날씨 조건을 먼저 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
check_type: 'weather',
|
||||
check_item: checkItem,
|
||||
check_category: 'WEATHER',
|
||||
weather_condition: weatherFilter,
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else if (tabType === 'task') {
|
||||
const input = document.getElementById('inlineTaskInput');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
if (!taskId) {
|
||||
showToast('작업을 먼저 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
check_type: 'task',
|
||||
check_item: checkItem,
|
||||
check_category: 'TASK',
|
||||
task_id: parseInt(taskId),
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall('/tbm/safety-checks', 'POST', data);
|
||||
if (response && response.success) {
|
||||
showToast('항목이 추가되었습니다.', 'success');
|
||||
await loadAllChecks();
|
||||
renderCurrentTab();
|
||||
} else {
|
||||
showToast(response?.message || '추가에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('인라인 추가 실패:', error);
|
||||
showToast('추가 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토스트 메시지 표시
|
||||
*/
|
||||
@@ -716,3 +887,4 @@ window.filterByWorkType = filterByWorkType;
|
||||
window.filterByTask = filterByTask;
|
||||
window.loadModalTasks = loadModalTasks;
|
||||
window.toggleConditionalFields = toggleConditionalFields;
|
||||
window.addInlineCheck = addInlineCheck;
|
||||
|
||||
Reference in New Issue
Block a user