Files
tk-factory-services/system1-factory/web/pages/admin/workers.html
Hyungi Ahn 7637be33f3 feat: TBM 모바일 시스템 + 작업 분할/이동 + 권한 통합
TBM 시스템:
- 4단계 워크플로우 (draft→세부편집→완료→작업보고)
- 모바일 전용 TBM 페이지 (tbm-mobile.html) + 3단계 생성 위자드
- 작업자 작업 분할 (work_hours + split_seq)
- 작업자 이동 보내기/빼오기 (tbm_transfers 테이블)
- 생성 시 중복 배정 방지 (당일 배정 현황 조회)
- 데스크탑 TBM 페이지 세부편집 기능 추가

작업보고서:
- 모바일 전용 작업보고서 페이지 (report-create-mobile.html)
- TBM에서 사전 등록된 work_hours 자동 반영

권한 시스템:
- tkuser user_page_permissions 테이블과 system1 페이지 접근 연동
- pageAccessRoutes를 userRoutes보다 먼저 등록 (라우트 우선순위 수정)
- TKUSER_DEFAULT_ACCESS 폴백 추가 (개인→부서→기본값 3단계)
- 권한 캐시키 갱신 (userPageAccess_v2)

기타:
- app-init.js 캐시 버스팅 (v=5)
- iOS Safari touch-action: manipulation 적용
- KST 타임존 날짜 버그 수정 (toISOString UTC 이슈)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:46:21 +09:00

496 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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/admin-pages.css?v=8">
<link rel="icon" type="image/png" href="/img/favicon.png">
<!-- 최적화된 로딩: API 설정 → 앱 초기화 (병렬 컴포넌트 로딩) -->
<script src="/js/api-base.js"></script>
<script src="/js/app-init.js?v=5" defer></script>
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
<script src="https://instant.page/5.2.0" type="module"></script>
<style>
.department-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 1.5rem;
min-height: calc(100vh - 200px);
}
/* 부서 패널 */
.department-panel {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.department-panel-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.department-panel-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #1f2937;
}
.department-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.department-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 0.25rem;
}
.department-item:hover {
background: #f3f4f6;
}
.department-item.active {
background: #dbeafe;
border-left: 3px solid #3b82f6;
}
.department-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.department-name {
font-weight: 500;
color: #1f2937;
}
.department-count {
font-size: 0.75rem;
color: #6b7280;
}
.department-actions {
display: flex;
gap: 0.25rem;
opacity: 0;
transition: opacity 0.2s;
}
.department-item:hover .department-actions {
opacity: 1;
}
.btn-icon {
padding: 0.375rem;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
color: #6b7280;
transition: all 0.2s;
}
.btn-icon:hover {
background: #e5e7eb;
color: #1f2937;
}
.btn-icon.danger:hover {
background: #fee2e2;
color: #dc2626;
}
/* 작업자 패널 */
.worker-panel {
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
.worker-panel-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.worker-panel-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: #1f2937;
}
.worker-toolbar {
padding: 1rem 1.25rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.worker-toolbar .search-input {
flex: 1;
min-width: 200px;
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
}
.worker-toolbar .filter-select {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
background: white;
}
.worker-list {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
/* 작업자 테이블 스타일 */
.workers-table {
width: 100%;
border-collapse: collapse;
}
.workers-table th,
.workers-table td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid #e5e7eb;
}
.workers-table th {
background: #f9fafb;
font-weight: 600;
font-size: 0.75rem;
text-transform: uppercase;
color: #6b7280;
}
.workers-table tr:hover {
background: #f9fafb;
}
.worker-name-cell {
display: flex;
align-items: center;
gap: 0.75rem;
}
.worker-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.status-badge.active {
background: #dcfce7;
color: #166534;
}
.status-badge.inactive {
background: #f3f4f6;
color: #6b7280;
}
.status-badge.resigned {
background: #fee2e2;
color: #dc2626;
}
.account-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
}
.account-badge.has-account {
background: #dbeafe;
color: #1d4ed8;
}
.account-badge.no-account {
background: #f3f4f6;
color: #9ca3af;
}
/* 빈 상태 */
.empty-state {
text-align: center;
padding: 3rem;
color: #6b7280;
}
.empty-state h4 {
margin: 0 0 0.5rem 0;
color: #374151;
}
/* 반응형 */
@media (max-width: 1024px) {
.department-layout {
grid-template-columns: 1fr;
}
.department-panel {
max-height: 300px;
}
}
/* 부서 모달 스타일 */
.form-check {
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-check input[type="checkbox"] {
width: 1rem;
height: 1rem;
}
</style>
</head>
<body>
<!-- 네비게이션 바 -->
<div id="navbar-container"></div>
<!-- 메인 레이아웃 -->
<div class="page-container">
<main class="main-content">
<div class="dashboard-main">
<div class="page-header">
<div class="page-title-section">
<h1 class="page-title">작업자 관리</h1>
<p class="page-description">부서별 작업자를 관리합니다. 부서를 선택하면 해당 부서의 작업자를 확인하고 관리할 수 있습니다.</p>
</div>
</div>
<!-- 부서 기반 레이아웃 -->
<div class="department-layout">
<!-- 왼쪽: 부서 목록 -->
<div class="department-panel">
<div class="department-panel-header">
<h3>부서 목록</h3>
<button class="btn btn-sm btn-primary" onclick="openDepartmentModal()">+ 부서 추가</button>
</div>
<div class="department-list" id="departmentList">
<!-- 부서 목록이 여기에 렌더링됩니다 -->
</div>
</div>
<!-- 오른쪽: 작업자 목록 -->
<div class="worker-panel">
<div class="worker-panel-header">
<h3 id="workerListTitle">부서를 선택하세요</h3>
<button class="btn btn-sm btn-primary" id="addWorkerBtn" onclick="openWorkerModal()" style="display: none;">+ 작업자 추가</button>
</div>
<div class="worker-toolbar" id="workerToolbar" style="display: none;">
<input type="text" class="search-input" id="workerSearch" placeholder="작업자 검색..." oninput="filterWorkers()">
<select class="filter-select" id="statusFilter" onchange="filterWorkers()">
<option value="">모든 상태</option>
<option value="active">활성</option>
<option value="inactive">비활성</option>
<option value="resigned">퇴사</option>
</select>
</div>
<div class="worker-list" id="workerList">
<div class="empty-state">
<h4>부서를 선택해주세요</h4>
<p>왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.</p>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 부서 추가/수정 모달 -->
<div id="departmentModal" class="modal-overlay" style="display: none;">
<div class="modal-container" style="max-width: 500px;">
<div class="modal-header">
<h2 id="departmentModalTitle">새 부서 등록</h2>
<button class="modal-close-btn" onclick="closeDepartmentModal()">×</button>
</div>
<div class="modal-body">
<form id="departmentForm" onsubmit="event.preventDefault(); saveDepartment();">
<input type="hidden" id="departmentId">
<div class="form-group">
<label class="form-label">부서명 *</label>
<input type="text" id="departmentName" class="form-control" placeholder="예: 생산팀, 품질관리팀" required>
</div>
<div class="form-group">
<label class="form-label">상위 부서</label>
<select id="parentDepartment" class="form-control">
<option value="">없음 (최상위 부서)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">설명</label>
<textarea id="departmentDescription" class="form-control" rows="2" placeholder="부서에 대한 설명"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">표시 순서</label>
<input type="number" id="displayOrder" class="form-control" value="0" min="0">
</div>
<div class="form-group">
<label class="form-label">&nbsp;</label>
<div class="form-check">
<input type="checkbox" id="isActiveDept" checked>
<label for="isActiveDept">활성화</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeDepartmentModal()">취소</button>
<button type="button" class="btn btn-danger" id="deleteDeptBtn" onclick="deleteDepartment()" style="display: none;">삭제</button>
<button type="button" class="btn btn-primary" onclick="saveDepartment()">저장</button>
</div>
</div>
</div>
<!-- 작업자 추가/수정 모달 -->
<div id="workerModal" class="modal-overlay" style="display: none;">
<div class="modal-container">
<div class="modal-header">
<h2 id="workerModalTitle">새 작업자 등록</h2>
<button class="modal-close-btn" onclick="closeWorkerModal()">×</button>
</div>
<div class="modal-body">
<form id="workerForm" onsubmit="event.preventDefault(); saveWorker();">
<input type="hidden" id="workerId">
<div class="form-row">
<div class="form-group">
<label class="form-label">작업자명 *</label>
<input type="text" id="workerName" class="form-control" placeholder="작업자 이름을 입력하세요" required>
</div>
<div class="form-group">
<label class="form-label">직책</label>
<select id="jobType" class="form-control">
<option value="worker">작업자</option>
<option value="leader">그룹장</option>
<option value="admin">관리자</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">입사일</label>
<input type="date" id="joinDate" class="form-control">
</div>
<div class="form-group">
<label class="form-label">급여</label>
<input type="number" id="salary" class="form-control" placeholder="월급여">
</div>
</div>
<div class="form-group">
<label class="form-label">연차</label>
<input type="number" id="annualLeave" class="form-control" placeholder="연차 일수" value="0">
</div>
<!-- 상태 관리 섹션 -->
<div class="form-group">
<label class="form-label" style="font-weight: 600; margin-bottom: 0.75rem; display: block;">상태 관리</label>
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
<!-- 계정 생성/연동 -->
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="checkbox" id="createAccount" style="margin: 0; cursor: pointer;">
<span>계정 생성/연동</span>
</label>
<small style="color: #6b7280; font-size: 0.75rem; margin-top: -0.5rem; margin-left: 1.5rem;">
체크 시 로그인 계정이 자동 생성됩니다 (ID: 이름 로마자 변환, 초기 비밀번호: 1234)
</small>
<!-- 현장직/사무직 구분 -->
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="checkbox" id="isActiveWorker" checked style="margin: 0; cursor: pointer;">
<span>현장직 (활성화)</span>
</label>
<small style="color: #6b7280; font-size: 0.75rem; margin-top: -0.5rem; margin-left: 1.5rem;">
체크: 현장직 (TBM, 작업보고서에 표시) / 체크 해제: 사무직
</small>
<!-- 퇴사 처리 -->
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
<input type="checkbox" id="isResigned" style="margin: 0; cursor: pointer;">
<span style="color: #ef4444;">퇴사 처리</span>
</label>
<small style="color: #ef4444; font-size: 0.75rem; margin-top: -0.5rem; margin-left: 1.5rem;">
퇴사한 작업자로 표시됩니다. TBM/작업 보고서에서 제외됩니다
</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeWorkerModal()">취소</button>
<button type="button" class="btn btn-danger" id="deleteWorkerBtn" onclick="deleteWorker()" style="display: none;">삭제</button>
<button type="button" class="btn btn-primary" onclick="saveWorker()">저장</button>
</div>
</div>
</div>
<!-- worker-management.js만 로드 (navbar/sidebar는 app-init.js에서 처리) -->
<script type="module" src="/js/worker-management.js?v=8"></script>
</body>
</html>