- 공통 유틸리티 추출 (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>
496 lines
18 KiB
HTML
496 lines
18 KiB
HTML
<!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"> </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>
|