Files
tk-factory-services/system1-factory/web/pages/admin/workers.html
Hyungi Ahn 4388628788 refactor: TBM/작업보고 코드 통합 및 API 쿼리 버그 수정
- 공통 유틸리티 추출 (common/utils.js, common/base-state.js)
- TBM 모바일 인라인 JS/CSS 외부 파일로 분리 (tbm-mobile.js, tbm-mobile.css)
- 미사용 코드 삭제 (index.js, work-report-*.js 등 5개 파일)
- TBM/작업보고 state.js, utils.js를 공통 모듈 기반으로 전환
- 작업보고서 SSO 인증 호환 수정 (token/user 함수)
- tbmModel.js: incomplete-reports 쿼리에서 users→sso_users 조인 수정, leader_name 조인 추가
- docker-compose.yml: system1-web 볼륨 마운트 추가
- 모바일 인계(handover) 기능 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:51:24 +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?v=2"></script>
<script src="/js/app-init.js?v=9" 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>