feat(tkuser): 연차/휴가 관리 프론트엔드 개편 (Sprint 001 Section B)

- workers 기반 → sso_users 기반 전환 (vacWorkers→vacUsers, /workers→/users)
- 휴가 탭: 부서 필터, 이름 검색, balance_type 뱃지, 장기근속 제외 체크박스
- 배정 모달: balance_type/expires_at 필드 추가, 사용자 부서별 optgroup
- 부서 탭: 팀장 표시/편집, 승인권한 CRUD
- 연차 설정 탭/JS 신규: 기본연차·장기근속·이월연차 설정 UI
- API 미완성 대응: 필드명 폴백(worker_id→user_id), 404 graceful degradation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-23 08:14:28 +09:00
parent c158da7832
commit a3f7a324b1
7 changed files with 527 additions and 67 deletions

View File

@@ -72,6 +72,9 @@
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="vacations" onclick="switchTab('vacations', event)">
<i class="fas fa-umbrella-beach mr-2"></i>휴가
</button>
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="vacationSettings" onclick="switchTab('vacationSettings', event)">
<i class="fas fa-sliders mr-2"></i>연차 설정
</button>
<span class="tab-divider"></span>
<!-- 거래/물품 -->
<span class="tab-group-label">거래</span>
@@ -513,6 +516,13 @@
<div id="deptMembersPanel" class="hidden mt-4 border-t pt-4">
<h3 class="text-sm font-semibold text-gray-700 mb-3"><i class="fas fa-users text-slate-400 mr-1.5"></i>소속 인원</h3>
<div id="deptMembersList" class="space-y-2"></div>
<div id="deptApprovalSection" class="hidden mt-4 border-t pt-4">
<div class="flex items-center justify-between mb-3">
<h4 class="text-sm font-semibold text-gray-700"><i class="fas fa-user-check text-slate-400 mr-1.5"></i>승인권한</h4>
<button onclick="openApprovalModal()" class="text-xs text-slate-500 hover:text-slate-700 px-1.5 py-0.5 rounded hover:bg-gray-100" title="승인권한 추가"><i class="fas fa-plus"></i></button>
</div>
<div id="deptApprovalList" class="space-y-2"></div>
</div>
</div>
</div>
</div>
@@ -732,7 +742,11 @@
<div class="bg-white rounded-xl shadow-sm p-5 flex flex-col flex-1 overflow-hidden">
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
<h2 class="text-base font-semibold text-gray-800"><i class="fas fa-calendar-check text-slate-400 mr-2"></i>연차 배정</h2>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 flex-wrap">
<select id="vacDeptFilter" class="input-field px-3 py-1.5 rounded-lg text-sm" onchange="filterVacBalances()">
<option value="">전체 부서</option>
</select>
<input type="text" id="vacSearch" class="input-field px-3 py-1.5 rounded-lg text-sm w-36" placeholder="이름 검색" oninput="filterVacBalances()">
<select id="vacYear" class="input-field px-3 py-1.5 rounded-lg text-sm" onchange="loadVacBalances()">
</select>
<button onclick="autoCalcVacation()" class="px-3 py-1.5 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 text-xs font-medium" title="입사일 기반 연차 자동 계산">
@@ -751,6 +765,15 @@
</div>
</div>
<!-- ============ 연차 설정 탭 ============ -->
<div id="tab-vacationSettings" class="hidden">
<div class="max-w-3xl mx-auto space-y-6">
<div id="vacSettingsContent">
<div class="text-gray-400 text-center py-8"><i class="fas fa-spinner fa-spin text-2xl"></i><p class="mt-2 text-sm">로딩 중...</p></div>
</div>
</div>
</div>
<!-- 휴가 유형 모달 -->
<div id="vacTypeModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-sm w-full p-6">
@@ -796,8 +819,8 @@
</div>
<!-- 개별 배정 모달 -->
<div id="vacBalanceModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-sm w-full p-6">
<div id="vacBalanceModal" class="modal-overlay fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-md w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 id="vacBalModalTitle" class="text-base font-semibold text-gray-900">연차 배정</h3>
<button onclick="closeVacBalanceModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
@@ -805,8 +828,8 @@
<form id="vacBalanceForm" class="space-y-3">
<input type="hidden" id="vbEditId">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">작업<span class="text-red-400">*</span></label>
<select id="vbWorker" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
<label class="block text-xs font-medium text-gray-600 mb-1">사용<span class="text-red-400">*</span></label>
<select id="vbUser" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
<option value="">선택</option>
</select>
</div>
@@ -816,6 +839,16 @@
<option value="">선택</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">배정 유형</label>
<select id="vbBalanceType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" onchange="onBalanceTypeChange()">
<option value="AUTO">기본연차</option>
<option value="MANUAL">추가부여</option>
<option value="CARRY_OVER">이월연차</option>
<option value="LONG_SERVICE">장기근속</option>
<option value="COMPANY_GRANT">회사부여</option>
</select>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">배정 일수</label>
@@ -826,6 +859,10 @@
<input type="number" id="vbUsedDays" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="0" step="0.5" min="0">
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">만료일</label>
<input type="date" id="vbExpiresAt" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">비고</label>
<input type="text" id="vbNotes" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" placeholder="메모">
@@ -1159,6 +1196,12 @@
<label class="block text-xs font-medium text-gray-600 mb-1">표시순서</label>
<input type="number" id="editDeptOrder" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" min="0">
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">팀장</label>
<select id="editDeptLeader" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
<option value="">미지정</option>
</select>
</div>
<div class="flex gap-3 pt-3">
<button type="button" onclick="closeDepartmentModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-sm">취소</button>
<button type="submit" class="flex-1 px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium"><i class="fas fa-save mr-1"></i>저장</button>
@@ -1167,6 +1210,42 @@
</div>
</div>
<!-- 승인권한 추가 모달 -->
<div id="approvalAuthorityModal" class="modal-overlay fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-sm w-full p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold text-gray-900">승인권한 추가</h3>
<button onclick="closeApprovalModal()" class="text-gray-400 hover:text-gray-600"><i class="fas fa-times"></i></button>
</div>
<form id="approvalAuthorityForm" class="space-y-3">
<input type="hidden" id="approvalDeptId">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">승인 유형 <span class="text-red-400">*</span></label>
<select id="approvalType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
<option value="">선택</option>
<option value="VACATION">휴가</option>
<option value="PURCHASE">구매</option>
<option value="DOCUMENT">문서</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">승인자 <span class="text-red-400">*</span></label>
<select id="approvalUserId" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
<option value="">선택</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">순서</label>
<input type="number" id="approvalOrder" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="1" min="1">
</div>
<div class="flex gap-3 pt-2">
<button type="button" onclick="closeApprovalModal()" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-sm">취소</button>
<button type="submit" class="flex-1 px-4 py-2 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium"><i class="fas fa-save mr-1"></i>저장</button>
</div>
</form>
</div>
</div>
<!-- 이슈 카테고리 수정 모달 -->
<div id="editIssueCategoryModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl max-w-md w-full p-6">
@@ -2239,17 +2318,18 @@
</div>
<!-- JS: Core (config, token, api, toast, helpers, init) -->
<script src="/static/js/tkuser-core.js?v=2026031602"></script>
<script src="/static/js/tkuser-core.js?v=2026032301"></script>
<!-- JS: Tabs -->
<script src="/static/js/tkuser-tabs.js?v=2026031602"></script>
<script src="/static/js/tkuser-tabs.js?v=2026032301"></script>
<!-- JS: Individual modules -->
<script src="/static/js/tkuser-users.js?v=2026031601"></script>
<script src="/static/js/tkuser-users.js?v=2026032301"></script>
<script src="/static/js/tkuser-projects.js?v=2026031401"></script>
<script src="/static/js/tkuser-departments.js?v=2026031401"></script>
<script src="/static/js/tkuser-departments.js?v=2026032301"></script>
<script src="/static/js/tkuser-issue-types.js?v=2026031401"></script>
<script src="/static/js/tkuser-workplaces.js?v=2026031401"></script>
<script src="/static/js/tkuser-tasks.js?v=2026031401"></script>
<script src="/static/js/tkuser-vacations.js?v=2026031401"></script>
<script src="/static/js/tkuser-vacations.js?v=2026032301"></script>
<script src="/static/js/tkuser-vacation-settings.js?v=2026032301"></script>
<script src="/static/js/tkuser-layout-map.js?v=2026031401"></script>
<script src="/static/js/tkuser-partners.js?v=2026031601"></script>
<script src="/static/js/tkuser-vendors.js?v=2026031401"></script>