feat: 안전 코드 tksafety 이관 + 사용자 관리 정리 + UI Tailwind 전환

Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
         미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 10:46:22 +09:00
parent 8373fe9e75
commit 9fda89a374
133 changed files with 5255 additions and 26181 deletions

View File

@@ -1,196 +1,162 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>일일 작업보고서 작성 | (주)테크니컬코리아</title>
<link rel="stylesheet" href="/css/design-system.css">
<link rel="stylesheet" href="/css/daily-work-report.css?v=13">
<link rel="stylesheet" href="/css/mobile.css?v=1">
<link rel="icon" type="image/png" href="/img/favicon.png">
<!-- api-base.js에서 SW 캐시 강제 해제 처리 -->
<script src="/js/api-base.js?v=3"></script>
<script src="/js/app-init.js?v=9" defer></script>
<script src="https://instant.page/5.2.0" type="module"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>작업보고서 작성 - TK 공장관리</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/tkfb.css">
<link rel="stylesheet" href="/css/daily-work-report.css?v=13">
</head>
<body>
<div class="work-report-container">
<!-- 네비게이션 바 -->
<div id="navbar-container"></div>
<!-- 메인 콘텐츠 -->
<main class="work-report-main">
<!-- 탭 메뉴 -->
<div class="tab-menu" style="margin-bottom: var(--space-6);">
<button class="tab-btn active" id="tbmReportTab" onclick="switchTab('tbm')">
작업보고서 작성
</button>
<button class="tab-btn" id="completedReportTab" onclick="switchTab('completed')">
작성 완료 보고서
</button>
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<!-- TBM 작업보고 섹션 -->
<div id="tbmReportSection" class="step-section active">
<!-- TBM 작업 목록 -->
<div id="tbmWorkList">
<!-- TBM 작업 항목들이 여기에 동적으로 추가됩니다 -->
<body class="bg-gray-50">
<header class="bg-orange-700 text-white sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-14">
<div class="flex items-center gap-3">
<button id="mobileMenuBtn" class="lg:hidden text-orange-200 hover:text-white">
<i class="fas fa-bars text-xl"></i>
</button>
<i class="fas fa-industry text-xl text-orange-200"></i>
<h1 class="text-lg font-semibold">TK 공장관리</h1>
</div>
<div class="flex items-center gap-4">
<span id="headerUserName" class="text-sm hidden sm:block">-</span>
<div id="headerUserAvatar" class="w-8 h-8 bg-orange-600 rounded-full flex items-center justify-center text-sm font-bold">-</div>
<button onclick="doLogout()" class="text-orange-200 hover:text-white" title="로그아웃">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</div>
</div>
</div>
</header>
<!-- 작성 완료 보고서 섹션 -->
<div id="completedReportSection" class="step-section" style="display: none;">
<!-- 날짜 선택 필터 -->
<div class="form-group" style="max-width: 300px; margin-bottom: var(--space-5);">
<label for="completedReportDate" class="form-label">조회 날짜</label>
<input type="date" id="completedReportDate" class="form-input" onchange="loadCompletedReports()">
</div>
<!-- Mobile overlay -->
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></div>
<!-- 완료된 보고서 목록 -->
<div id="completedReportsList">
<!-- 완료된 보고서들이 여기에 동적으로 추가됩니다 -->
</div>
</div>
</main>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 fade-in">
<div class="flex gap-6">
<!-- Sidebar Nav -->
<nav id="sideNav" class="hidden lg:flex flex-col gap-1 w-52 flex-shrink-0 pt-2 fixed lg:static z-40 bg-white lg:bg-transparent p-4 lg:p-0 rounded-lg lg:rounded-none shadow-lg lg:shadow-none top-14 left-0 bottom-0 overflow-y-auto"></nav>
<!-- 저장 결과 모달 -->
<div id="saveResultModal" class="modal-overlay" style="display: none;">
<div class="modal-container result-modal">
<div class="modal-header">
<h2 id="resultModalTitle">저장 결과</h2>
<button class="modal-close-btn" onclick="closeSaveResultModal()">×</button>
</div>
<div class="modal-body">
<div id="resultModalContent" class="result-content">
<!-- 결과 내용이 여기에 동적으로 추가됩니다 -->
<div class="flex-1 min-w-0">
<!-- 탭 메뉴 -->
<div class="tab-menu" style="margin-bottom: 1.5rem;">
<button class="tab-btn active" id="tbmReportTab" onclick="switchTab('tbm')">
작업보고서 작성
</button>
<button class="tab-btn" id="completedReportTab" onclick="switchTab('completed')">
작성 완료 보고서
</button>
</div>
<!-- 메시지 영역 -->
<div id="message-container"></div>
<!-- TBM 작업보고 섹션 -->
<div id="tbmReportSection" class="step-section active">
<div id="tbmWorkList"></div>
</div>
<!-- 작성 완료 보고서 섹션 -->
<div id="completedReportSection" class="step-section" style="display: none;">
<div class="form-group" style="max-width: 300px; margin-bottom: 1.25rem;">
<label for="completedReportDate" class="form-label">조회 날짜</label>
<input type="date" id="completedReportDate" class="form-input" onchange="loadCompletedReports()">
</div>
<div id="completedReportsList"></div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="closeSaveResultModal()">
확인
</button>
</div>
</div>
</div>
<!-- 작업장소 선택 모달 (지도 기반) -->
<div id="workplaceModal" class="modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1002; align-items: center; justify-content: center; overflow-y: auto; padding: 2rem 0;">
<div class="modal-container" style="background: white; border-radius: 8px; max-width: 1000px; width: 90%; max-height: none; margin: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column;">
<div class="modal-header" style="padding: 1.5rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;">
<h2 style="font-size: 1.25rem; font-weight: 600; color: #111827; margin: 0;">작업장소 선택</h2>
<button class="modal-close" onclick="closeWorkplaceModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #6b7280; padding: 0; width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center; border-radius: 4px;">&times;</button>
</div>
<div class="modal-body" style="padding: 1.5rem; flex: 1; overflow-y: visible;">
<!-- 1단계: 카테고리 선택 -->
<div id="categorySelectionArea">
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: #374151;">공장 선택</h3>
<div id="workplaceCategoryList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;">
<!-- 카테고리 버튼들 -->
</div>
<!-- 저장 결과 모달 -->
<div id="saveResultModal" class="modal-overlay" style="display: none;">
<div class="modal-container result-modal">
<div class="modal-header">
<h2 id="resultModalTitle">저장 결과</h2>
<button class="modal-close-btn" onclick="closeSaveResultModal()">×</button>
</div>
<div class="modal-body">
<div id="resultModalContent" class="result-content"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="closeSaveResultModal()">확인</button>
</div>
</div>
<!-- 2단계: 작업장 선택 (지도 + 리스트) -->
<div id="workplaceSelectionArea" style="display: none; margin-top: 1.5rem;">
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: #374151;">
<span id="selectedCategoryTitle">작업장 선택</span>
</h3>
<!-- 지도 기반 선택 영역 -->
<div id="layoutMapArea" style="display: none; margin-bottom: 1.5rem; padding: 1rem; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.5rem;">
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.75rem;">
지도에서 작업장을 클릭하여 선택하세요
</div>
<div style="text-align: center; position: relative; display: inline-block; max-width: 100%;">
<canvas id="workplaceMapCanvas" style="max-width: 100%; border-radius: 0.5rem; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"></canvas>
</div>
</div>
<!-- 리스트 선택 영역 -->
<div style="margin-bottom: 1rem;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem;">
<span style="font-size: 0.875rem; color: #6b7280;">리스트에서 선택</span>
</div>
<div id="workplaceListArea" style="display: flex; flex-direction: column; gap: 0.5rem; max-height: 200px; overflow-y: auto; padding: 0.75rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; background: white;">
<!-- 작업장소 목록 -->
</div>
</div>
<div style="display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;">
<button type="button" class="btn btn-secondary" onclick="closeWorkplaceModal()">취소</button>
<button type="button" class="btn btn-primary" id="confirmWorkplaceBtn" onclick="confirmWorkplaceSelection()" disabled>선택 완료</button>
</div>
</div>
</div>
</div>
</div>
<!-- 시간 선택 팝오버 -->
<div id="timePickerOverlay" class="time-picker-overlay" style="display: none;" onclick="closeTimePicker()">
<div class="time-picker-popup" onclick="event.stopPropagation()">
<div class="time-picker-header">
<h3 id="timePickerTitle">작업시간 선택</h3>
<button class="time-picker-close" onclick="closeTimePicker()">&times;</button>
</div>
<div class="quick-time-grid">
<button type="button" class="time-btn" onclick="setTimeValue(0.5)">
<span class="time-value">30분</span>
</button>
<button type="button" class="time-btn" onclick="setTimeValue(1)">
<span class="time-value">1시간</span>
</button>
<button type="button" class="time-btn" onclick="setTimeValue(2)">
<span class="time-value">2시간</span>
</button>
<button type="button" class="time-btn" onclick="setTimeValue(4)">
<span class="time-value">4시간</span>
</button>
<button type="button" class="time-btn" onclick="setTimeValue(8)">
<span class="time-value">8시간</span>
</button>
</div>
<div class="time-adjust-area">
<span class="current-time-label">현재:</span>
<strong id="currentTimeDisplay" class="current-time-value">0시간</strong>
<div class="adjust-buttons">
<button type="button" class="adjust-btn" onclick="adjustTime(-0.5)">-30분</button>
<button type="button" class="adjust-btn" onclick="adjustTime(0.5)">+30분</button>
<!-- 작업장소 선택 모달 (지도 기반) -->
<div id="workplaceModal" class="modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1002; align-items: center; justify-content: center; overflow-y: auto; padding: 2rem 0;">
<div class="modal-container" style="background: white; border-radius: 8px; max-width: 1000px; width: 90%; max-height: none; margin: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column;">
<div class="modal-header" style="padding: 1.5rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center;">
<h2 style="font-size: 1.25rem; font-weight: 600; color: #111827; margin: 0;">작업장소 선택</h2>
<button class="modal-close" onclick="closeWorkplaceModal()" style="background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #6b7280; padding: 0; width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center; border-radius: 4px;">&times;</button>
</div>
<div class="modal-body" style="padding: 1.5rem; flex: 1; overflow-y: visible;">
<div id="categorySelectionArea">
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: #374151;">공장 선택</h3>
<div id="workplaceCategoryList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;"></div>
</div>
<div id="workplaceSelectionArea" style="display: none; margin-top: 1.5rem;">
<h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: #374151;">
<span id="selectedCategoryTitle">작업장 선택</span>
</h3>
<div id="layoutMapArea" style="display: none; margin-bottom: 1.5rem; padding: 1rem; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 0.5rem;">
<div style="font-size: 0.875rem; color: #6b7280; margin-bottom: 0.75rem;">지도에서 작업장을 클릭하여 선택하세요</div>
<div style="text-align: center; position: relative; display: inline-block; max-width: 100%;">
<canvas id="workplaceMapCanvas" style="max-width: 100%; border-radius: 0.5rem; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"></canvas>
</div>
</div>
<div style="margin-bottom: 1rem;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.5rem;">
<span style="font-size: 0.875rem; color: #6b7280;">리스트에서 선택</span>
</div>
<div id="workplaceListArea" style="display: flex; flex-direction: column; gap: 0.5rem; max-height: 200px; overflow-y: auto; padding: 0.75rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; background: white;"></div>
</div>
<div style="display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;">
<button type="button" class="btn btn-secondary" onclick="closeWorkplaceModal()">취소</button>
<button type="button" class="btn btn-primary" id="confirmWorkplaceBtn" onclick="confirmWorkplaceSelection()" disabled>선택 완료</button>
</div>
</div>
</div>
</div>
</div>
<button type="button" class="confirm-btn" onclick="confirmTimeSelection()">확인</button>
</div>
</div>
<!-- 공통 모듈 -->
<script src="/js/common/utils.js?v=1"></script>
<script src="/js/common/base-state.js?v=1"></script>
<!-- 시간 선택 팝오버 -->
<div id="timePickerOverlay" class="time-picker-overlay" style="display: none;" onclick="closeTimePicker()">
<div class="time-picker-popup" onclick="event.stopPropagation()">
<div class="time-picker-header">
<h3 id="timePickerTitle">작업시간 선택</h3>
<button class="time-picker-close" onclick="closeTimePicker()">&times;</button>
</div>
<div class="quick-time-grid">
<button type="button" class="time-btn" onclick="setTimeValue(0.5)"><span class="time-value">30분</span></button>
<button type="button" class="time-btn" onclick="setTimeValue(1)"><span class="time-value">1시간</span></button>
<button type="button" class="time-btn" onclick="setTimeValue(2)"><span class="time-value">2시간</span></button>
<button type="button" class="time-btn" onclick="setTimeValue(4)"><span class="time-value">4시간</span></button>
<button type="button" class="time-btn" onclick="setTimeValue(8)"><span class="time-value">8시간</span></button>
</div>
<div class="time-adjust-area">
<span class="current-time-label">현재:</span>
<strong id="currentTimeDisplay" class="current-time-value">0시간</strong>
<div class="adjust-buttons">
<button type="button" class="adjust-btn" onclick="adjustTime(-0.5)">-30분</button>
<button type="button" class="adjust-btn" onclick="adjustTime(0.5)">+30분</button>
</div>
</div>
<button type="button" class="confirm-btn" onclick="confirmTimeSelection()">확인</button>
</div>
</div>
<!-- 작업보고서 모듈 (리팩토링된 구조) -->
<script src="/js/daily-work-report/state.js?v=2"></script>
<script src="/js/daily-work-report/utils.js?v=2"></script>
<script src="/js/daily-work-report/api.js?v=2"></script>
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
<script defer src="/js/daily-work-report.js?v=36"></script>
<!-- 모바일 하단 네비게이션 -->
<div id="mobile-nav-container"></div>
<script>
if (window.innerWidth <= 768) {
fetch('/components/mobile-nav.html')
.then(r => r.text())
.then(html => {
document.getElementById('mobile-nav-container').innerHTML = html;
const scripts = document.getElementById('mobile-nav-container').querySelectorAll('script');
scripts.forEach(s => eval(s.textContent));
});
}
</script>
<script src="/static/js/tkfb-core.js"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/common/utils.js?v=1"></script>
<script src="/js/common/base-state.js?v=1"></script>
<script src="/js/daily-work-report/state.js?v=2"></script>
<script src="/js/daily-work-report/utils.js?v=2"></script>
<script src="/js/daily-work-report/api.js?v=2"></script>
<script defer src="/js/daily-work-report.js?v=36"></script>
<script>initAuth();</script>
</body>
</html>
</html>