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:
@@ -47,12 +47,10 @@ function setupRoutes(app) {
|
||||
const vacationRequestRoutes = require('../routes/vacationRequestRoutes');
|
||||
const vacationTypeRoutes = require('../routes/vacationTypeRoutes');
|
||||
const vacationBalanceRoutes = require('../routes/vacationBalanceRoutes');
|
||||
const visitRequestRoutes = require('../routes/visitRequestRoutes');
|
||||
const workIssueRoutes = require('../routes/workIssueRoutes');
|
||||
const departmentRoutes = require('../routes/departmentRoutes');
|
||||
const patrolRoutes = require('../routes/patrolRoutes');
|
||||
const notificationRoutes = require('../routes/notificationRoutes');
|
||||
const notificationRecipientRoutes = require('../routes/notificationRecipientRoutes');
|
||||
|
||||
// Rate Limiters 설정
|
||||
const rateLimit = require('express-rate-limit');
|
||||
@@ -154,13 +152,11 @@ function setupRoutes(app) {
|
||||
app.use('/api/vacation-requests', vacationRequestRoutes); // 휴가 신청 관리
|
||||
app.use('/api/vacation-types', vacationTypeRoutes); // 휴가 유형 관리
|
||||
app.use('/api/vacation-balances', vacationBalanceRoutes); // 휴가 잔액 관리
|
||||
app.use('/api/workplace-visits', visitRequestRoutes); // 출입 신청 및 안전교육 관리
|
||||
app.use('/api/tbm', tbmRoutes); // TBM 시스템
|
||||
app.use('/api/work-issues', workIssueRoutes); // 카테고리/아이템 + 신고 조회 (같은 MariaDB 공유)
|
||||
app.use('/api/departments', departmentRoutes); // 부서 관리
|
||||
app.use('/api/patrol', patrolRoutes); // 일일순회점검 시스템
|
||||
app.use('/api/notifications', notificationRoutes); // 알림 시스템
|
||||
app.use('/api/notification-recipients', notificationRecipientRoutes); // 알림 수신자 설정
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
// Swagger API 문서
|
||||
|
||||
@@ -47,12 +47,10 @@ function setupRoutes(app) {
|
||||
const vacationRequestRoutes = require('../routes/vacationRequestRoutes');
|
||||
const vacationTypeRoutes = require('../routes/vacationTypeRoutes');
|
||||
const vacationBalanceRoutes = require('../routes/vacationBalanceRoutes');
|
||||
const visitRequestRoutes = require('../routes/visitRequestRoutes');
|
||||
const workIssueRoutes = require('../routes/workIssueRoutes');
|
||||
const departmentRoutes = require('../routes/departmentRoutes');
|
||||
const patrolRoutes = require('../routes/patrolRoutes');
|
||||
const notificationRoutes = require('../routes/notificationRoutes');
|
||||
const notificationRecipientRoutes = require('../routes/notificationRecipientRoutes');
|
||||
|
||||
// Rate Limiters 설정
|
||||
const rateLimit = require('express-rate-limit');
|
||||
@@ -154,13 +152,11 @@ function setupRoutes(app) {
|
||||
app.use('/api/vacation-requests', vacationRequestRoutes); // 휴가 신청 관리
|
||||
app.use('/api/vacation-types', vacationTypeRoutes); // 휴가 유형 관리
|
||||
app.use('/api/vacation-balances', vacationBalanceRoutes); // 휴가 잔액 관리
|
||||
app.use('/api/workplace-visits', visitRequestRoutes); // 출입 신청 및 안전교육 관리
|
||||
app.use('/api/tbm', tbmRoutes); // TBM 시스템
|
||||
app.use('/api/work-issues', workIssueRoutes); // 카테고리/아이템 + 신고 조회 (같은 MariaDB 공유)
|
||||
app.use('/api/departments', departmentRoutes); // 부서 관리
|
||||
app.use('/api/patrol', patrolRoutes); // 일일순회점검 시스템
|
||||
app.use('/api/notifications', notificationRoutes); // 알림 시스템
|
||||
app.use('/api/notification-recipients', notificationRecipientRoutes); // 알림 수신자 설정
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
// Swagger API 문서
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// routes/notificationRecipientRoutes.js
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const notificationRecipientController = require('../controllers/notificationRecipientController');
|
||||
const { verifyToken, requireMinLevel } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 필요
|
||||
router.use(verifyToken);
|
||||
|
||||
// 알림 유형 목록
|
||||
router.get('/types', notificationRecipientController.getTypes);
|
||||
|
||||
// 전체 수신자 목록 (유형별 그룹화)
|
||||
router.get('/', notificationRecipientController.getAll);
|
||||
|
||||
// 유형별 수신자 조회
|
||||
router.get('/:type', notificationRecipientController.getByType);
|
||||
|
||||
// 수신자 추가 (관리자만)
|
||||
router.post('/', requireMinLevel('admin'), notificationRecipientController.add);
|
||||
|
||||
// 유형별 수신자 일괄 설정 (관리자만)
|
||||
router.put('/:type', requireMinLevel('admin'), notificationRecipientController.setRecipients);
|
||||
|
||||
// 수신자 제거 (관리자만)
|
||||
router.delete('/:type/:userId', requireMinLevel('admin'), notificationRecipientController.remove);
|
||||
|
||||
module.exports = router;
|
||||
@@ -2,7 +2,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const systemController = require('../controllers/systemController');
|
||||
const userController = require('../controllers/userController');
|
||||
const { requireAuth, requireRole } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 및 시스템 권한 확인 적용
|
||||
@@ -35,44 +34,6 @@ router.get('/alerts', systemController.getSystemAlerts);
|
||||
*/
|
||||
router.get('/recent-activities', systemController.getRecentActivities);
|
||||
|
||||
// ===== 사용자 관리 관련 =====
|
||||
|
||||
/**
|
||||
* GET /api/system/users/stats
|
||||
* 사용자 통계 조회
|
||||
*/
|
||||
router.get('/users/stats', systemController.getUserStats);
|
||||
|
||||
/**
|
||||
* GET /api/system/users
|
||||
* 모든 사용자 목록 조회
|
||||
*/
|
||||
router.get('/users', userController.getAllUsers);
|
||||
|
||||
/**
|
||||
* POST /api/system/users
|
||||
* 새 사용자 생성
|
||||
*/
|
||||
router.post('/users', userController.createUser);
|
||||
|
||||
/**
|
||||
* PUT /api/system/users/:id
|
||||
* 사용자 정보 수정
|
||||
*/
|
||||
router.put('/users/:id', userController.updateUser);
|
||||
|
||||
/**
|
||||
* DELETE /api/system/users/:id
|
||||
* 사용자 삭제
|
||||
*/
|
||||
router.delete('/users/:id', userController.deleteUser);
|
||||
|
||||
/**
|
||||
* POST /api/system/users/:id/reset-password
|
||||
* 사용자 비밀번호 재설정
|
||||
*/
|
||||
router.post('/users/:id/reset-password', userController.resetUserPassword);
|
||||
|
||||
// ===== 시스템 로그 관련 =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const visitRequestController = require('../controllers/visitRequestController');
|
||||
const { verifyToken } = require('../middlewares/auth');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(verifyToken);
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
// 출입 신청 생성
|
||||
router.post('/requests', visitRequestController.createVisitRequest);
|
||||
|
||||
// 출입 신청 목록 조회 (필터: status, visit_date, start_date, end_date, requester_id, category_id)
|
||||
router.get('/requests', visitRequestController.getAllVisitRequests);
|
||||
|
||||
// 출입 신청 상세 조회
|
||||
router.get('/requests/:id', visitRequestController.getVisitRequestById);
|
||||
|
||||
// 출입 신청 수정
|
||||
router.put('/requests/:id', visitRequestController.updateVisitRequest);
|
||||
|
||||
// 출입 신청 삭제
|
||||
router.delete('/requests/:id', visitRequestController.deleteVisitRequest);
|
||||
|
||||
// 출입 신청 승인
|
||||
router.put('/requests/:id/approve', visitRequestController.approveVisitRequest);
|
||||
|
||||
// 출입 신청 반려
|
||||
router.put('/requests/:id/reject', visitRequestController.rejectVisitRequest);
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
// 모든 방문 목적 조회
|
||||
router.get('/purposes', visitRequestController.getAllVisitPurposes);
|
||||
|
||||
// 활성 방문 목적만 조회
|
||||
router.get('/purposes/active', visitRequestController.getActiveVisitPurposes);
|
||||
|
||||
// 방문 목적 추가
|
||||
router.post('/purposes', visitRequestController.createVisitPurpose);
|
||||
|
||||
// 방문 목적 수정
|
||||
router.put('/purposes/:id', visitRequestController.updateVisitPurpose);
|
||||
|
||||
// 방문 목적 삭제
|
||||
router.delete('/purposes/:id', visitRequestController.deleteVisitPurpose);
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
// 안전교육 기록 생성
|
||||
router.post('/training', visitRequestController.createTrainingRecord);
|
||||
|
||||
// 안전교육 기록 목록 조회 (필터: training_date, start_date, end_date, trainer_id)
|
||||
router.get('/training', visitRequestController.getTrainingRecords);
|
||||
|
||||
// 특정 출입 신청의 안전교육 기록 조회
|
||||
router.get('/training/request/:requestId', visitRequestController.getTrainingRecordByRequestId);
|
||||
|
||||
// 안전교육 기록 수정
|
||||
router.put('/training/:id', visitRequestController.updateTrainingRecord);
|
||||
|
||||
// 안전교육 완료 (서명 포함)
|
||||
router.post('/training/:id/complete', visitRequestController.completeTraining);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,147 +0,0 @@
|
||||
<!-- components/mobile-nav.html -->
|
||||
<!-- 모바일 하단 네비게이션 (4개 핵심 기능) -->
|
||||
<nav class="mobile-bottom-nav" id="mobileBottomNav">
|
||||
<a href="/pages/dashboard.html" class="mobile-nav-item" data-page="dashboard">
|
||||
<svg class="mobile-nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
<span class="mobile-nav-label">홈</span>
|
||||
</a>
|
||||
<a href="/pages/work/tbm-mobile.html" class="mobile-nav-item" data-page="tbm">
|
||||
<svg class="mobile-nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 11l3 3L22 4"/>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>
|
||||
<span class="mobile-nav-label">TBM</span>
|
||||
</a>
|
||||
<a href="/pages/work/report-create-mobile.html" class="mobile-nav-item" data-page="report">
|
||||
<svg class="mobile-nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
<span class="mobile-nav-label">작업보고</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/checkin.html" class="mobile-nav-item" data-page="checkin">
|
||||
<svg class="mobile-nav-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
<span class="mobile-nav-label">출근</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* 모바일 하단 네비게이션 */
|
||||
.mobile-bottom-nav {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 68px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
box-shadow: 0 -2px 12px rgba(0,0,0,0.08);
|
||||
z-index: 1000;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-bottom-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: calc(68px + env(safe-area-inset-bottom)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
text-decoration: none;
|
||||
color: #9ca3af;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0.25rem;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
position: relative;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.mobile-nav-item:active {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* SVG 아이콘 */
|
||||
.mobile-nav-svg {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.mobile-nav-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* 활성 상태 */
|
||||
.mobile-nav-item.active {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.mobile-nav-item.active .mobile-nav-svg {
|
||||
transform: scale(1.08);
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
.mobile-nav-item.active .mobile-nav-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 활성 인디케이터 점 */
|
||||
.mobile-nav-item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: #2563eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var currentPath = window.location.pathname;
|
||||
var navItems = document.querySelectorAll('.mobile-nav-item[data-page]');
|
||||
|
||||
navItems.forEach(function(item) {
|
||||
var href = item.getAttribute('href');
|
||||
if (href && currentPath.includes(href.replace('/pages/', '').replace('.html', ''))) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
if (currentPath.includes('dashboard')) {
|
||||
var dashItem = document.querySelector('[data-page="dashboard"]');
|
||||
if (dashItem) dashItem.classList.add('active');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -1,764 +0,0 @@
|
||||
<!-- components/navbar.html -->
|
||||
<!-- 최신 대시보드 헤더 -->
|
||||
<header class="dashboard-header">
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<!-- 모바일 메뉴 버튼 -->
|
||||
<button class="mobile-menu-btn" id="mobileMenuBtn" aria-label="메뉴 열기">
|
||||
☰
|
||||
</button>
|
||||
<div class="brand">
|
||||
<img src="/img/logo.png" alt="테크니컬코리아" class="brand-logo">
|
||||
<div class="brand-text">
|
||||
<h1 class="brand-title">테크니컬코리아</h1>
|
||||
<p class="brand-subtitle">생산팀 포털</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<div class="datetime-weather-box">
|
||||
<div class="date-time-section">
|
||||
<span class="date-value" id="dateValue">--월 --일 (--)</span>
|
||||
<span class="time-value" id="timeValue">--시 --분 --초</span>
|
||||
</div>
|
||||
<div class="weather-section" id="weatherSection">
|
||||
<span class="weather-icon" id="weatherIcon">🌤️</span>
|
||||
<span class="weather-temp" id="weatherTemp">--°C</span>
|
||||
<span class="weather-desc" id="weatherDesc">날씨 로딩중</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- 알림 버튼 -->
|
||||
<div class="notification-wrapper" id="notificationWrapper">
|
||||
<button class="notification-btn" id="notificationBtn">
|
||||
<svg class="notification-icon-svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
</svg>
|
||||
<span class="notification-badge" id="notificationBadge" style="display:none;">0</span>
|
||||
</button>
|
||||
<div class="notification-dropdown" id="notificationDropdown">
|
||||
<div class="notification-header">
|
||||
<h4>알림</h4>
|
||||
<a href="/pages/admin/notifications.html" class="view-all-link">모두 보기</a>
|
||||
</div>
|
||||
<div class="notification-list" id="notificationList">
|
||||
<div class="notification-empty">새 알림이 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/pages/dashboard.html" id="dashboardBtn" class="dashboard-btn">
|
||||
<span class="btn-icon">📊</span>
|
||||
<span class="btn-text">대시보드</span>
|
||||
</a>
|
||||
|
||||
<a href="https://tkreport.technicalkorea.net" class="report-btn">
|
||||
<span class="btn-icon">⚠</span>
|
||||
<span class="btn-text">신고</span>
|
||||
</a>
|
||||
|
||||
<div class="user-profile" id="userProfile">
|
||||
<div class="user-avatar">
|
||||
<span class="avatar-text" id="userInitial">사</span>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name" id="userName">사용자</span>
|
||||
<span class="user-role" id="userRole">작업자</span>
|
||||
</div>
|
||||
<div class="profile-menu" id="profileMenu">
|
||||
<a href="/pages/profile/info.html" class="menu-item">
|
||||
<span class="menu-icon">👤</span>
|
||||
내 프로필
|
||||
</a>
|
||||
<a href="/pages/profile/password.html" class="menu-item">
|
||||
<span class="menu-icon">🔐</span>
|
||||
비밀번호 변경
|
||||
</a>
|
||||
<a href="/pages/admin/accounts.html" class="menu-item admin-only">
|
||||
<span class="menu-icon">⚙️</span>
|
||||
관리자 설정
|
||||
</a>
|
||||
<button class="menu-item logout-btn" id="logoutBtn">
|
||||
<span class="menu-icon">🚪</span>
|
||||
로그아웃
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
/* 최신 대시보드 헤더 스타일 */
|
||||
.dashboard-header {
|
||||
background: var(--header-gradient);
|
||||
color: var(--text-inverse);
|
||||
padding: var(--space-4) var(--space-6);
|
||||
box-shadow: var(--shadow-lg);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 헤더 높이만큼 본문 여백 추가 */
|
||||
body {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 var(--space-4);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.header-left .brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-bold);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: var(--text-sm);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
font-weight: var(--font-normal);
|
||||
}
|
||||
|
||||
/* 날짜/시간/날씨 박스 */
|
||||
.datetime-weather-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
}
|
||||
|
||||
.date-time-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-right: var(--space-4);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.date-value {
|
||||
font-size: var(--text-sm);
|
||||
opacity: 0.9;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.weather-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.weather-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.weather-temp {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.weather-desc {
|
||||
font-size: var(--text-sm);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.header-right .user-profile {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--radius-full);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-normal);
|
||||
}
|
||||
|
||||
.user-profile:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--primary-200);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--primary-900);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: var(--text-xs);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.profile-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: var(--space-2);
|
||||
background: linear-gradient(135deg, var(--bg-primary), var(--bg-secondary));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border: 2px solid rgba(14, 165, 233, 0.2);
|
||||
min-width: 220px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition-slow);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.user-profile:hover .profile-menu,
|
||||
.profile-menu:hover {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-5);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-slow);
|
||||
border-radius: var(--radius-md);
|
||||
margin: var(--space-1);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: linear-gradient(135deg, var(--gray-100), var(--gray-200));
|
||||
color: var(--text-primary);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.menu-item:first-child {
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: var(--text-lg);
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: var(--error-500) !important;
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin-top: var(--space-2);
|
||||
padding-top: var(--space-3);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: linear-gradient(135deg, var(--error-50), #fee2e2) !important;
|
||||
color: var(--error-700) !important;
|
||||
}
|
||||
|
||||
/* 대시보드 버튼 */
|
||||
.dashboard-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--text-sm);
|
||||
transition: var(--transition-slow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dashboard-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.dashboard-btn .btn-icon {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.dashboard-btn .btn-text {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 신고 버튼 */
|
||||
.report-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-5);
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--text-sm);
|
||||
transition: var(--transition-slow);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.report-btn:hover {
|
||||
background: rgba(220, 38, 38, 1);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.report-btn .btn-icon {
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.report-btn .btn-text {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 알림 버튼 스타일 */
|
||||
.notification-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-normal);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notification-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.notification-icon-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification-btn.has-notifications {
|
||||
animation: pulse-btn 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-btn {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
||||
50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
|
||||
}
|
||||
|
||||
.notification-badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
background: var(--error-500);
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-bold);
|
||||
border-radius: var(--radius-full);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: pulse-badge 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-badge {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: var(--space-2);
|
||||
width: 320px;
|
||||
max-height: 400px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-xl);
|
||||
border: 1px solid var(--border-light);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition-normal);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notification-dropdown.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.notification-header h4 {
|
||||
margin: 0;
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.view-all-link {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--primary-500);
|
||||
text-decoration: none;
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
.view-all-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
}
|
||||
|
||||
.notification-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background: rgba(14, 165, 233, 0.05);
|
||||
}
|
||||
|
||||
.notification-item.unread::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--primary-500);
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notification-item-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--warning-100);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notification-item-icon.repair {
|
||||
background: var(--warning-100);
|
||||
}
|
||||
|
||||
.notification-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notification-item-title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.notification-item-desc {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.notification-item-time {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notification-empty {
|
||||
padding: var(--space-6);
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* 모바일 메뉴 버튼 */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
margin-right: var(--space-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-btn:hover,
|
||||
.mobile-menu-btn:active {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 1024px) {
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-header {
|
||||
padding: 0 0.5rem;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-left .brand {
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.dashboard-btn .btn-text,
|
||||
.report-btn .btn-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dashboard-btn,
|
||||
.report-btn {
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.dashboard-btn .btn-icon,
|
||||
.report-btn .btn-icon {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
position: fixed;
|
||||
top: 52px;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
padding: 0.125rem 0.25rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.profile-menu {
|
||||
position: fixed;
|
||||
top: 52px;
|
||||
right: 0.5rem;
|
||||
left: auto;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 모바일 사이드바 열릴 때 바디 스크롤 방지 */
|
||||
body.sidebar-mobile-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,482 +0,0 @@
|
||||
<!-- components/sidebar-nav.html -->
|
||||
<!-- 카테고리별 사이드 네비게이션 메뉴 -->
|
||||
<aside class="sidebar-nav" id="sidebarNav">
|
||||
<div class="sidebar-toggle" id="sidebarToggle">
|
||||
<span class="toggle-icon">☰</span>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-menu">
|
||||
<!-- 대시보드 -->
|
||||
<a href="/pages/dashboard.html" class="nav-item" data-page-key="dashboard">
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span class="nav-text">대시보드</span>
|
||||
</a>
|
||||
|
||||
<!-- 작업 관리 -->
|
||||
<div class="nav-category" data-category="work">
|
||||
<button class="nav-category-header">
|
||||
<span class="nav-icon">📝</span>
|
||||
<span class="nav-text">작업 관리</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</button>
|
||||
<div class="nav-category-items">
|
||||
<a href="/pages/work/tbm.html" class="nav-item" data-page-key="work.tbm">
|
||||
<span class="nav-text">TBM 관리</span>
|
||||
</a>
|
||||
<a href="/pages/work/report-create.html" class="nav-item" data-page-key="work.report_create">
|
||||
<span class="nav-text">작업보고서 작성</span>
|
||||
</a>
|
||||
<a href="/pages/work/analysis.html" class="nav-item admin-only" data-page-key="work.analysis">
|
||||
<span class="nav-text">작업 분석</span>
|
||||
</a>
|
||||
<a href="/pages/work/nonconformity.html" class="nav-item" data-page-key="work.nonconformity">
|
||||
<span class="nav-text">부적합 현황</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공장 관리 -->
|
||||
<div class="nav-category" data-category="factory">
|
||||
<button class="nav-category-header">
|
||||
<span class="nav-icon">🏭</span>
|
||||
<span class="nav-text">공장 관리</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</button>
|
||||
<div class="nav-category-items">
|
||||
<a href="/pages/admin/repair-management.html" class="nav-item" data-page-key="factory.repair_management">
|
||||
<span class="nav-text">시설설비 관리</span>
|
||||
</a>
|
||||
<a href="/pages/inspection/daily-patrol.html" class="nav-item" data-page-key="inspection.daily_patrol">
|
||||
<span class="nav-text">일일순회점검</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/checkin.html" class="nav-item" data-page-key="inspection.checkin">
|
||||
<span class="nav-text">출근 체크</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/work-status.html" class="nav-item" data-page-key="inspection.work_status">
|
||||
<span class="nav-text">근무 현황</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 안전 관리 -->
|
||||
<div class="nav-category" data-category="safety">
|
||||
<button class="nav-category-header">
|
||||
<span class="nav-icon">🛡</span>
|
||||
<span class="nav-text">안전 관리</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</button>
|
||||
<div class="nav-category-items">
|
||||
<a href="#" class="nav-item cross-system-link" data-system="report" data-path="/pages/safety/my-reports.html" data-page-key="safety.my_reports">
|
||||
<span class="nav-text">내 신고 현황</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item cross-system-link admin-only" data-system="report" data-path="/pages/safety/report-status.html" data-page-key="safety.report_status">
|
||||
<span class="nav-text">전체 신고 현황</span>
|
||||
</a>
|
||||
<a href="/pages/safety/visit-request.html" class="nav-item" data-page-key="safety.visit_request">
|
||||
<span class="nav-text">출입 신청</span>
|
||||
</a>
|
||||
<a href="/pages/safety/management.html" class="nav-item admin-only" data-page-key="safety.management">
|
||||
<span class="nav-text">안전 관리</span>
|
||||
</a>
|
||||
<a href="/pages/safety/checklist-manage.html" class="nav-item admin-only" data-page-key="safety.checklist_manage">
|
||||
<span class="nav-text">체크리스트 관리</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 근태 관리 -->
|
||||
<div class="nav-category" data-category="attendance">
|
||||
<button class="nav-category-header">
|
||||
<span class="nav-icon">📅</span>
|
||||
<span class="nav-text">근태 관리</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</button>
|
||||
<div class="nav-category-items">
|
||||
<a href="/pages/attendance/my-vacation-info.html" class="nav-item" data-page-key="attendance.my_vacation_info">
|
||||
<span class="nav-text">내 연차 정보</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/monthly.html" class="nav-item" data-page-key="attendance.monthly">
|
||||
<span class="nav-text">월간 근태</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/vacation-request.html" class="nav-item" data-page-key="attendance.vacation_request">
|
||||
<span class="nav-text">휴가 신청</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/vacation-management.html" class="nav-item admin-only" data-page-key="attendance.vacation_management">
|
||||
<span class="nav-text">휴가 관리</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/vacation-allocation.html" class="nav-item admin-only" data-page-key="attendance.vacation_allocation">
|
||||
<span class="nav-text">휴가 발생 입력</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/annual-overview.html" class="nav-item admin-only" data-page-key="attendance.annual_overview">
|
||||
<span class="nav-text">연간 휴가 현황</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시스템 관리 (관리자 전용) -->
|
||||
<div class="nav-category admin-only" data-category="admin">
|
||||
<button class="nav-category-header">
|
||||
<span class="nav-icon">⚙</span>
|
||||
<span class="nav-text">시스템 관리</span>
|
||||
<span class="nav-arrow">▾</span>
|
||||
</button>
|
||||
<div class="nav-category-items">
|
||||
<a href="/pages/admin/accounts.html" class="nav-item" data-page-key="admin.accounts">
|
||||
<span class="nav-text">계정 관리</span>
|
||||
</a>
|
||||
<a href="/pages/admin/workers.html" class="nav-item" data-page-key="admin.workers">
|
||||
<span class="nav-text">작업자 관리</span>
|
||||
</a>
|
||||
<a href="/pages/admin/projects.html" class="nav-item" data-page-key="admin.projects">
|
||||
<span class="nav-text">프로젝트 관리</span>
|
||||
</a>
|
||||
<a href="/pages/admin/tasks.html" class="nav-item" data-page-key="admin.tasks">
|
||||
<span class="nav-text">작업 관리</span>
|
||||
</a>
|
||||
<a href="/pages/admin/workplaces.html" class="nav-item" data-page-key="admin.workplaces">
|
||||
<span class="nav-text">작업장 관리</span>
|
||||
</a>
|
||||
<a href="/pages/admin/equipments.html" class="nav-item" data-page-key="admin.equipments">
|
||||
<span class="nav-text">설비 관리</span>
|
||||
</a>
|
||||
<a href="#" class="nav-item cross-system-link admin-only" data-system="report" data-path="/pages/admin/issue-categories.html" data-page-key="admin.issue_categories">
|
||||
<span class="nav-text">신고 카테고리 관리</span>
|
||||
</a>
|
||||
<a href="/pages/admin/attendance-report.html" class="nav-item" data-page-key="admin.attendance_report">
|
||||
<span class="nav-text">출퇴근-보고서 대조</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
/* 사이드바 기본 스타일 */
|
||||
.sidebar-nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 80px; /* 헤더 높이만큼 아래로 */
|
||||
height: calc(100vh - 80px);
|
||||
width: 260px;
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
color: #e2e8f0;
|
||||
z-index: 99; /* 헤더보다 낮게 */
|
||||
transition: transform 0.3s ease, width 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.sidebar-nav.collapsed {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.sidebar-nav.collapsed .nav-text,
|
||||
.sidebar-nav.collapsed .nav-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-nav.collapsed .nav-category-items {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 토글 버튼 */
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 1.5rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 메뉴 스타일 */
|
||||
.sidebar-menu {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
/* 네비게이션 아이템 */
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
color: #cbd5e1;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(14, 165, 233, 0.15);
|
||||
color: #38bdf8;
|
||||
border-left-color: #38bdf8;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.25rem;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 카테고리 헤더 */
|
||||
.nav-category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.nav-category-header:hover {
|
||||
color: #e2e8f0;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.nav-category.expanded .nav-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 카테고리 아이템 */
|
||||
.nav-category-items {
|
||||
display: none;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-category.expanded .nav-category-items {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-category-items .nav-item {
|
||||
padding-left: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* 관리자 전용 숨김 */
|
||||
.admin-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-only.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-category.admin-only.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일 */
|
||||
.sidebar-menu::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar-menu::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 메인 콘텐츠 여백 */
|
||||
body.has-sidebar .dashboard-container,
|
||||
body.has-sidebar .work-report-container,
|
||||
body.has-sidebar .analysis-container,
|
||||
body.has-sidebar > .dashboard-main {
|
||||
margin-left: 260px;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
/* page-container 사용 시: page-container에만 margin 적용 (main-content 중복 방지) */
|
||||
body.has-sidebar .page-container {
|
||||
margin-left: 260px;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
/* page-container 없이 main-content만 있는 경우 */
|
||||
body.has-sidebar > .main-content {
|
||||
margin-left: 260px;
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
|
||||
body.has-sidebar.sidebar-collapsed .dashboard-container,
|
||||
body.has-sidebar.sidebar-collapsed .work-report-container,
|
||||
body.has-sidebar.sidebar-collapsed .analysis-container,
|
||||
body.has-sidebar.sidebar-collapsed > .dashboard-main {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
body.has-sidebar.sidebar-collapsed .page-container {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
body.has-sidebar.sidebar-collapsed > .main-content {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
/* 반응형 - 모바일 */
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar-nav {
|
||||
transform: translateX(-100%);
|
||||
width: 280px;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.sidebar-nav.mobile-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 모바일 헤더 */
|
||||
.sidebar-toggle {
|
||||
justify-content: space-between;
|
||||
padding: 0 1rem;
|
||||
height: 64px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-toggle::after {
|
||||
content: '메뉴';
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.sidebar-nav .toggle-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav.mobile-open .toggle-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-nav.mobile-open .toggle-icon::before {
|
||||
content: '✕';
|
||||
}
|
||||
|
||||
.sidebar-nav.mobile-open .toggle-icon span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 오버레이 배경 */
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.sidebar-overlay.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body.has-sidebar .dashboard-container,
|
||||
body.has-sidebar .page-container,
|
||||
body.has-sidebar > .main-content,
|
||||
body.has-sidebar .work-report-container,
|
||||
body.has-sidebar .analysis-container,
|
||||
body.has-sidebar > .dashboard-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 메뉴 아이템 모바일 최적화 */
|
||||
.nav-item {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.nav-category-items .nav-item {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
.nav-category-header {
|
||||
padding: 1rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 모바일 메뉴 버튼 (헤더용) */
|
||||
.mobile-menu-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.mobile-menu-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.mobile-menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 모바일 오버레이 -->
|
||||
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
||||
@@ -1,136 +0,0 @@
|
||||
<!-- ✅ /components/sidebar.html -->
|
||||
<aside class="sidebar">
|
||||
<nav class="sidebar-nav">
|
||||
<!-- 일반 작업자 메뉴 -->
|
||||
<div class="menu-section worker-only">
|
||||
<h3 class="menu-title">👷 작업 메뉴</h3>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/pages/work-reports/create.html">📝 작업 일보 작성</a></li>
|
||||
<li><a href="/pages/common/attendance.html">📋 출근부 확인</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 그룹장 메뉴 -->
|
||||
<div class="menu-section group-leader-only">
|
||||
<h3 class="menu-title">👨🏫 그룹장 메뉴</h3>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/pages/issue-reports/daily-issue.html">📋 일일 이슈 보고</a></li>
|
||||
<li><a href="/pages/work-reports/team-reports.html">👥 팀 작업 관리</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 지원팀 메뉴 -->
|
||||
<div class="menu-section support-only">
|
||||
<h3 class="menu-title">🧑💼 지원팀 메뉴</h3>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/pages/work-reports/create.html">📥 작업보고서 입력</a></li>
|
||||
<li><a href="/pages/work-reports/manage.html">🛠 작업보고서 관리</a></li>
|
||||
<li><a href="/pages/common/attendance.html">📊 전체 출근부</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 관리자 메뉴 -->
|
||||
<div class="menu-section admin-only">
|
||||
<h3 class="menu-title">🏢 관리자 메뉴</h3>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/pages/admin/reports-dashboard.html">📈 리포트 대시보드</a></li>
|
||||
<li><a href="/pages/admin/system-logs.html">📋 시스템 로그</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 시스템 관리자 메뉴 -->
|
||||
<div class="menu-section system-only">
|
||||
<h3 class="menu-title">⚙️ 시스템 관리</h3>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/pages/admin/manage-user.html">👤 사용자 관리</a></li>
|
||||
<li><a href="/pages/admin/manage-worker.html">👷 작업자 관리</a></li>
|
||||
<li><a href="/pages/admin/manage-project.html">📁 프로젝트 관리</a></li>
|
||||
<li><a href="/pages/admin/manage-task.html">📋 작업 유형 관리</a></li>
|
||||
<li><a href="/pages/admin/manage-issue.html">🚨 이슈 유형 관리</a></li>
|
||||
<li><a href="/pages/admin/manage-pipespec.html">🔧 배관 스펙 관리</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 공통 메뉴 (모든 사용자) -->
|
||||
<div class="menu-section">
|
||||
<h3 class="menu-title">📌 공통 메뉴</h3>
|
||||
<ul class="menu-list">
|
||||
<li><a href="/pages/common/factory-list.html">🏭 공장 정보</a></li>
|
||||
<li><a href="/pages/common/emergency-contacts.html">📞 비상 연락망</a></li>
|
||||
<li><a href="/pages/common/help.html">❓ 도움말</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: #1a237e;
|
||||
color: white;
|
||||
min-height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
box-shadow: 2px 0 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.menu-section:not(:last-child) {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 0 0 12px 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.menu-list a {
|
||||
display: block;
|
||||
color: rgba(255,255,255,0.9);
|
||||
text-decoration: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.menu-list a:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.menu-list a:active {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
/* 모바일 대응 */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
body {
|
||||
font-family: 'Malgun Gothic', sans-serif;
|
||||
margin: 0;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
.main-layout {
|
||||
display: flex;
|
||||
}
|
||||
#sidebar-container {
|
||||
width: 250px;
|
||||
background: #1a237e;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
}
|
||||
#content-container {
|
||||
flex: 1;
|
||||
padding: 30px;
|
||||
}
|
||||
h1, h2 {
|
||||
color: #1976d2;
|
||||
}
|
||||
a {
|
||||
color: #1976d2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Admin 권한 배지 스타일 */
|
||||
.admin-badge {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(255,107,53,0.3);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-only-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.admin-only-link:hover .admin-badge {
|
||||
background: linear-gradient(135deg, #ff5722 0%, #ff9800 100%);
|
||||
box-shadow: 0 3px 6px rgba(255,107,53,0.4);
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
/**
|
||||
* annual-vacation-overview.css
|
||||
* 연간 연차 현황 페이지 스타일
|
||||
*/
|
||||
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* 페이지 헤더 */
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 필터 섹션 */
|
||||
.filter-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-select {
|
||||
padding: 0.625rem 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 탭 네비게이션 */
|
||||
.tabs-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 1rem 2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--color-primary);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--color-primary);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 탭 컨텐츠 */
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 월 선택 컨트롤 */
|
||||
.month-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.month-controls .form-select {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* 차트 섹션 */
|
||||
.chart-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn-outline.active {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* 테이블 섹션 */
|
||||
.table-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.data-table thead {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table tbody tr {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 3rem 1rem !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-state .spinner {
|
||||
margin: 0 auto 1rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 사용률 프로그레스 바 */
|
||||
.usage-rate-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-fill.low {
|
||||
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
.progress-fill.medium {
|
||||
background: linear-gradient(90deg, #f59e0b 0%, #d97706 100%);
|
||||
}
|
||||
|
||||
.progress-fill.high {
|
||||
background: linear-gradient(90deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
|
||||
.usage-rate-text {
|
||||
font-weight: 600;
|
||||
min-width: 45px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.content-wrapper {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.tabs-nav {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 100%;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-controls button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,883 +0,0 @@
|
||||
/* 근태 검증 관리 시스템 - 개선된 스타일 */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 뒤로가기 버튼 */
|
||||
.back-btn {
|
||||
background: rgba(255,255,255,0.95);
|
||||
color: #667eea;
|
||||
border: 3px solid #667eea;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 3px 12px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 페이지 헤더 */
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 메인 카드 */
|
||||
.main-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.main-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 3rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 캘린더 헤더 */
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: between;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow: 0 6px 24px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* 월간 요약 */
|
||||
.summary-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
text-align: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.summary-card.normal {
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.summary-card.warning {
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.summary-card.error {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.summary-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-card.normal .summary-number {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.summary-card.warning .summary-number {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.summary-card.error .summary-number {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 요일 헤더 */
|
||||
.weekday-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.weekday.sunday {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.weekday.saturday {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* 캘린더 그리드 */
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
position: relative;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
border: 2px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.calendar-day.hover-enabled:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.calendar-day.selected {
|
||||
transform: scale(1.05);
|
||||
z-index: 10;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.calendar-day.loading-state {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
border-color: #3b82f6;
|
||||
animation: loading-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.calendar-day.error-state {
|
||||
background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%);
|
||||
border-color: #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.calendar-day.normal {
|
||||
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
||||
border-color: #10b981;
|
||||
color: #064e3b;
|
||||
}
|
||||
|
||||
.calendar-day.needs-review {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
border-color: #f59e0b;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.calendar-day.missing {
|
||||
background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%);
|
||||
border-color: #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.calendar-day.no-data {
|
||||
background: #f9fafb;
|
||||
border-color: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calendar-day.no-data::after {
|
||||
content: "클릭하여 확인";
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
font-size: 10px;
|
||||
color: #6b7280;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.calendar-day.no-data.hover-enabled:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-dot.pulse {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.status-dot.normal {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.status-dot.warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
/* 범례 */
|
||||
.legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.legend-dot.normal {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.legend-dot.warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.legend-dot.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
/* 빈 상태 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
color: #6b7280;
|
||||
font-size: 1rem;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 작업자 카드 */
|
||||
.worker-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.worker-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.worker-card.normal {
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.worker-card.needs-review {
|
||||
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.worker-card.missing {
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fecaca 100%);
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.worker-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.worker-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.worker-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
color: #374151;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.worker-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.worker-id {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 1.5rem;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
/* 데이터 행 */
|
||||
.data-section {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.data-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.data-label {
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.data-value {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.difference-positive {
|
||||
color: #dc2626;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.difference-negative {
|
||||
color: #2563eb;
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 버튼 */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 24px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(107, 114, 128, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.875rem;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.75rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
/* 필터 */
|
||||
.filter-container {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 모달 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 20px 20px 0 0;
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-top: 2px solid #f0f0f0;
|
||||
background: #f8fafc;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
/* 폼 요소 */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 메시지 */
|
||||
.message {
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
||||
color: #065f46;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%);
|
||||
color: #991b1b;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
color: #92400e;
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.message.loading {
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
||||
color: #1e40af;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
/* 로딩 스피너 */
|
||||
.loading-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 애니메이션 */
|
||||
.fade-in {
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 768px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
min-height: 60px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.worker-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.legend {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.calendar-day {
|
||||
min-height: 50px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.summary-number {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.worker-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-body, .modal-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
h2 {
|
||||
text-align: center;
|
||||
color: #343a40;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.controls label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.controls select,
|
||||
.controls button {
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
button#loadAttendance {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
button#downloadPdf {
|
||||
background-color: #007BFF;
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
#attendanceTableContainer {
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
background: white;
|
||||
}
|
||||
th {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
.divider {
|
||||
border-left: 3px solid #333 !important;
|
||||
}
|
||||
tr.separator td {
|
||||
border-bottom: 2px solid #999;
|
||||
padding: 0;
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
.overtime-cell { background: #e9e5ff !important; }
|
||||
.leave { background: #f5e0d6 !important; }
|
||||
.holiday { background: #ffd6d6 !important; }
|
||||
.paid-leave { background: #d6f0ff !important; }
|
||||
.no-data { background: #ddd !important; }
|
||||
.overtime-sum { background: #e9e5ff !important; }
|
||||
@@ -1,300 +0,0 @@
|
||||
/* Common CSS - 공통 스타일 */
|
||||
|
||||
/* ========== 통일된 헤더 스타일 ========== */
|
||||
.work-report-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.work-report-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 2rem 1.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.work-report-header h1 {
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.75rem 0;
|
||||
text-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.3);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.work-report-header .subtitle {
|
||||
font-size: clamp(0.875rem, 2vw, 1.1rem);
|
||||
opacity: 0.9;
|
||||
margin: 0;
|
||||
font-weight: 300;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 90%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.work-report-main {
|
||||
background: #f8f9fa;
|
||||
min-height: calc(100vh - 12rem);
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
margin: 0 1.5rem 1.5rem 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: white;
|
||||
color: #007bff;
|
||||
transform: translateY(-0.0625rem);
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* 반응형 헤더 */
|
||||
@media (max-width: 768px) {
|
||||
.work-report-header {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.work-report-header h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin: 0 1rem 1rem 1rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.work-report-header {
|
||||
padding: 1.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.work-report-header .subtitle {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin: 0 0.75rem 0.75rem 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reset and Base Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.5rem; }
|
||||
h3 { font-size: 1.25rem; }
|
||||
h4 { font-size: 1.125rem; }
|
||||
h5 { font-size: 1rem; }
|
||||
h6 { font-size: 0.875rem; }
|
||||
|
||||
/* ========== 헤더 액션 버튼 ========== */
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.dashboard-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.dashboard-btn .btn-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.mb-1 { margin-bottom: 0.25rem; }
|
||||
.mb-2 { margin-bottom: 0.5rem; }
|
||||
.mb-3 { margin-bottom: 0.75rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-5 { margin-bottom: 1.25rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
|
||||
.mt-1 { margin-top: 0.25rem; }
|
||||
.mt-2 { margin-top: 0.5rem; }
|
||||
.mt-3 { margin-top: 0.75rem; }
|
||||
.mt-4 { margin-top: 1rem; }
|
||||
.mt-5 { margin-top: 1.25rem; }
|
||||
.mt-6 { margin-top: 1.5rem; }
|
||||
|
||||
.p-1 { padding: 0.25rem; }
|
||||
.p-2 { padding: 0.5rem; }
|
||||
.p-3 { padding: 0.75rem; }
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-5 { padding: 1.25rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; }
|
||||
h2 { font-size: 1.25rem; }
|
||||
h3 { font-size: 1.125rem; }
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
/* /css/daily-issue.css */
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f5f7fa;
|
||||
margin: 0;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
select, input[type="date"], button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 6px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
button#submitBtn {
|
||||
margin-top: 30px;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button#submitBtn:hover {
|
||||
background: #125cb1;
|
||||
}
|
||||
|
||||
.multi-select-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.multi-select-box .btn {
|
||||
flex: 1 0 30%;
|
||||
padding: 8px;
|
||||
border: 1px solid #1976d2;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #1976d2;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.multi-select-box .btn.selected {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.time-range {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.time-range select {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
/* ✅ design-system.css - 한글 기반 모던 디자인 시스템 */
|
||||
|
||||
/* ========== 색상 시스템 ========== */
|
||||
:root {
|
||||
/* 주요 브랜드 색상 (하늘색 계열) */
|
||||
--primary-50: #f0f9ff;
|
||||
--primary-100: #e0f2fe;
|
||||
--primary-200: #bae6fd;
|
||||
--primary-300: #7dd3fc;
|
||||
--primary-400: #38bdf8;
|
||||
--primary-500: #0ea5e9;
|
||||
--primary-600: #0284c7;
|
||||
--primary-700: #0369a1;
|
||||
--primary-800: #075985;
|
||||
--primary-900: #0c4a6e;
|
||||
|
||||
/* 헤더 그라디언트 */
|
||||
--header-gradient: linear-gradient(135deg, #0ea5e9 0%, #38bdf8 50%, #7dd3fc 100%);
|
||||
|
||||
/* 보조 색상 */
|
||||
--secondary-50: #f3e5f5;
|
||||
--secondary-100: #e1bee7;
|
||||
--secondary-200: #ce93d8;
|
||||
--secondary-300: #ba68c8;
|
||||
--secondary-400: #ab47bc;
|
||||
--secondary-500: #9c27b0;
|
||||
--secondary-600: #8e24aa;
|
||||
--secondary-700: #7b1fa2;
|
||||
--secondary-800: #6a1b9a;
|
||||
--secondary-900: #4a148c;
|
||||
|
||||
/* 그레이 스케일 */
|
||||
--gray-50: #fafafa;
|
||||
--gray-100: #f5f5f5;
|
||||
--gray-200: #eeeeee;
|
||||
--gray-300: #e0e0e0;
|
||||
--gray-400: #bdbdbd;
|
||||
--gray-500: #9e9e9e;
|
||||
--gray-600: #757575;
|
||||
--gray-700: #616161;
|
||||
--gray-800: #424242;
|
||||
--gray-900: #212121;
|
||||
|
||||
/* 상태 색상 */
|
||||
--success-50: #e8f5e8;
|
||||
--success-500: #4caf50;
|
||||
--success-700: #388e3c;
|
||||
|
||||
--warning-50: #fff8e1;
|
||||
--warning-500: #ff9800;
|
||||
--warning-700: #f57c00;
|
||||
|
||||
--error-50: #ffebee;
|
||||
--error-500: #f44336;
|
||||
--error-700: #d32f2f;
|
||||
|
||||
--info-50: #e1f5fe;
|
||||
--info-500: #03a9f4;
|
||||
--info-700: #0288d1;
|
||||
|
||||
/* 따뜻한 중성 색상 (베이지/크림) */
|
||||
--warm-50: #fafaf9; /* 매우 밝은 크림 */
|
||||
--warm-100: #f5f5f4; /* 밝은 크림 */
|
||||
--warm-200: #e7e5e4; /* 베이지 */
|
||||
--warm-300: #d6d3d1; /* 중간 베이지 */
|
||||
--warm-400: #a8a29e; /* 진한 베이지 */
|
||||
--warm-500: #78716c; /* 그레이 베이지 */
|
||||
|
||||
/* 부드러운 작업 상태 색상 (눈이 편한 톤) */
|
||||
--status-success-bg: #dcfce7; /* 부드러운 초록 배경 */
|
||||
--status-success-text: #16a34a; /* 부드러운 초록 텍스트 */
|
||||
--status-info-bg: #e0f2fe; /* 부드러운 하늘색 배경 */
|
||||
--status-info-text: #0284c7; /* 부드러운 하늘색 텍스트 */
|
||||
--status-warning-bg: #fef3c7; /* 부드러운 노랑 배경 */
|
||||
--status-warning-text: #ca8a04; /* 부드러운 노랑 텍스트 */
|
||||
--status-error-bg: #fee2e2; /* 부드러운 빨강 배경 */
|
||||
--status-error-text: #dc2626; /* 부드러운 빨강 텍스트 */
|
||||
--status-critical-bg: #fecaca; /* 진한 빨강 배경 */
|
||||
--status-critical-text: #b91c1c; /* 진한 빨강 텍스트 */
|
||||
--status-vacation-bg: #fed7aa; /* 부드러운 주황 배경 */
|
||||
--status-vacation-text: #ea580c; /* 부드러운 주황 텍스트 */
|
||||
|
||||
/* 배경 색상 */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-tertiary: #f1f5f9;
|
||||
--bg-overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* 텍스트 색상 */
|
||||
--text-primary: #1a202c;
|
||||
--text-secondary: #4a5568;
|
||||
--text-tertiary: #718096;
|
||||
--text-inverse: #ffffff;
|
||||
|
||||
/* 경계선 */
|
||||
--border-light: #e2e8f0;
|
||||
--border-medium: #cbd5e0;
|
||||
--border-dark: #a0aec0;
|
||||
|
||||
/* 그림자 */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
|
||||
/* 반경 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 간격 */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
--space-20: 80px;
|
||||
--space-24: 96px;
|
||||
|
||||
/* 폰트 크기 */
|
||||
--text-xs: 12px;
|
||||
--text-sm: 14px;
|
||||
--text-base: 16px;
|
||||
--text-lg: 18px;
|
||||
--text-xl: 20px;
|
||||
--text-2xl: 24px;
|
||||
--text-3xl: 30px;
|
||||
--text-4xl: 36px;
|
||||
--text-5xl: 48px;
|
||||
|
||||
/* 폰트 두께 */
|
||||
--font-light: 300;
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
--font-extrabold: 800;
|
||||
|
||||
/* 애니메이션 */
|
||||
--transition-fast: 150ms ease-in-out;
|
||||
--transition-normal: 250ms ease-in-out;
|
||||
--transition-slow: 350ms ease-in-out;
|
||||
}
|
||||
|
||||
/* ========== 기본 리셋 ========== */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', system-ui, sans-serif;
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ========== 타이포그래피 ========== */
|
||||
.text-xs { font-size: var(--text-xs); }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
.text-base { font-size: var(--text-base); }
|
||||
.text-lg { font-size: var(--text-lg); }
|
||||
.text-xl { font-size: var(--text-xl); }
|
||||
.text-2xl { font-size: var(--text-2xl); }
|
||||
.text-3xl { font-size: var(--text-3xl); }
|
||||
.text-4xl { font-size: var(--text-4xl); }
|
||||
.text-5xl { font-size: var(--text-5xl); }
|
||||
|
||||
.font-light { font-weight: var(--font-light); }
|
||||
.font-normal { font-weight: var(--font-normal); }
|
||||
.font-medium { font-weight: var(--font-medium); }
|
||||
.font-semibold { font-weight: var(--font-semibold); }
|
||||
.font-bold { font-weight: var(--font-bold); }
|
||||
.font-extrabold { font-weight: var(--font-extrabold); }
|
||||
|
||||
.text-primary { color: var(--text-primary); }
|
||||
.text-secondary { color: var(--text-secondary); }
|
||||
.text-tertiary { color: var(--text-tertiary); }
|
||||
.text-inverse { color: var(--text-inverse); }
|
||||
|
||||
/* ========== 카드 컴포넌트 ========== */
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
transition: var(--transition-normal);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-6);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: var(--space-6);
|
||||
border-top: 1px solid var(--border-light);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
||||
}
|
||||
|
||||
/* ========== 버튼 컴포넌트 ========== */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-500);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--primary-600);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--gray-100);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-medium);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success-500);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: var(--success-700);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: var(--warning-500);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: var(--warning-700);
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
background: var(--error-500);
|
||||
color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.btn-error:hover:not(:disabled) {
|
||||
background: var(--error-700);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* ========== 배지 컴포넌트 ========== */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
border-radius: var(--radius-full);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: var(--primary-100);
|
||||
color: var(--primary-800);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: var(--success-50);
|
||||
color: var(--success-700);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: var(--warning-50);
|
||||
color: var(--warning-700);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background: var(--error-50);
|
||||
color: var(--error-700);
|
||||
}
|
||||
|
||||
.badge-gray {
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
/* ========== 상태 표시기 ========== */
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
margin-right: var(--space-2);
|
||||
}
|
||||
|
||||
.status-dot.active {
|
||||
background: var(--success-500);
|
||||
box-shadow: 0 0 0 2px var(--success-100);
|
||||
}
|
||||
|
||||
.status-dot.inactive {
|
||||
background: var(--gray-400);
|
||||
}
|
||||
|
||||
.status-dot.warning {
|
||||
background: var(--warning-500);
|
||||
box-shadow: 0 0 0 2px var(--warning-100);
|
||||
}
|
||||
|
||||
.status-dot.error {
|
||||
background: var(--error-500);
|
||||
box-shadow: 0 0 0 2px var(--error-100);
|
||||
}
|
||||
|
||||
/* ========== 그리드 시스템 ========== */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, 1fr); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-cols-2,
|
||||
.grid-cols-3,
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 플렉스 유틸리티 ========== */
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.items-start { align-items: flex-start; }
|
||||
.items-end { align-items: flex-end; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-start { justify-content: flex-start; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.gap-1 { gap: var(--space-1); }
|
||||
.gap-2 { gap: var(--space-2); }
|
||||
.gap-3 { gap: var(--space-3); }
|
||||
.gap-4 { gap: var(--space-4); }
|
||||
.gap-6 { gap: var(--space-6); }
|
||||
|
||||
/* ========== 간격 유틸리티 ========== */
|
||||
.p-1 { padding: var(--space-1); }
|
||||
.p-2 { padding: var(--space-2); }
|
||||
.p-3 { padding: var(--space-3); }
|
||||
.p-4 { padding: var(--space-4); }
|
||||
.p-6 { padding: var(--space-6); }
|
||||
.p-8 { padding: var(--space-8); }
|
||||
|
||||
.m-1 { margin: var(--space-1); }
|
||||
.m-2 { margin: var(--space-2); }
|
||||
.m-3 { margin: var(--space-3); }
|
||||
.m-4 { margin: var(--space-4); }
|
||||
.m-6 { margin: var(--space-6); }
|
||||
.m-8 { margin: var(--space-8); }
|
||||
|
||||
.mb-2 { margin-bottom: var(--space-2); }
|
||||
.mb-4 { margin-bottom: var(--space-4); }
|
||||
.mb-6 { margin-bottom: var(--space-6); }
|
||||
.mt-4 { margin-top: var(--space-4); }
|
||||
.mt-6 { margin-top: var(--space-6); }
|
||||
|
||||
/* ========== 반응형 유틸리티 ========== */
|
||||
@media (max-width: 640px) {
|
||||
.sm\:hidden { display: none; }
|
||||
.sm\:text-sm { font-size: var(--text-sm); }
|
||||
.sm\:p-4 { padding: var(--space-4); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.md\:hidden { display: none; }
|
||||
.md\:flex-col { flex-direction: column; }
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.lg\:hidden { display: none; }
|
||||
}
|
||||
|
||||
/* ========== 애니메이션 ========== */
|
||||
.fade-in {
|
||||
animation: fadeIn var(--transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp var(--transition-normal) ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 로딩 스피너 ========== */
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--gray-200);
|
||||
border-top: 2px solid var(--primary-500);
|
||||
border-radius: var(--radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
body {
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
form label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: bold;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
form input[type="text"],
|
||||
form input[type="file"],
|
||||
form textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: url('/img/login-bg.jpeg') no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
font-family: 'Malgun Gothic', sans-serif;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
margin: 100px auto;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 200px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 15px 0;
|
||||
padding: 12px;
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 20px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #1565c0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 10px;
|
||||
color: #ff6b6b;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
/* ✅ /css/main-layout.css - 공통 레이아웃 스타일 */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 메인 레이아웃 구조 */
|
||||
.main-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#navbar-container {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#sidebar-container {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#content-container,
|
||||
#sections-container,
|
||||
#admin-sections,
|
||||
#user-sections {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 카드 스타일 */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* 섹션 스타일 */
|
||||
section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #1976d2;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
section li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
section a {
|
||||
color: #1976d2;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 4px 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
section a:hover {
|
||||
color: #0d47a1;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* 로딩 상태 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '.';
|
||||
animation: dots 1.5s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% { content: '.'; }
|
||||
40% { content: '..'; }
|
||||
60%, 100% { content: '...'; }
|
||||
}
|
||||
|
||||
/* 에러 상태 */
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.error-state button {
|
||||
margin-top: 16px;
|
||||
padding: 8px 24px;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.error-state button:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 1024px) {
|
||||
#content-container,
|
||||
#sections-container {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#sidebar-container {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,583 +0,0 @@
|
||||
/**
|
||||
* 나의 출근 현황 페이지 스타일
|
||||
*/
|
||||
|
||||
/* 페이지 헤더 */
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 통계 카드 섹션 */
|
||||
.stats-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 36px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 탭 컨테이너 */
|
||||
.tab-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #495057;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 탭 컨텐츠 */
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 테이블 스타일 */
|
||||
#attendanceTable {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
#attendanceTable thead {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
#attendanceTable th {
|
||||
padding: 16px 12px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
#attendanceTable td {
|
||||
padding: 14px 12px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
}
|
||||
|
||||
#attendanceTable tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#attendanceTable tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.loading-cell,
|
||||
.empty-cell,
|
||||
.error-cell {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-cell {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.notes-cell {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
color: #6c757d;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 상태 배지 */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.status-badge.normal {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-badge.late {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-badge.early {
|
||||
background: #ffe5b5;
|
||||
color: #a56200;
|
||||
}
|
||||
|
||||
.status-badge.absent {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-badge.vacation {
|
||||
background: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
/* 달력 스타일 */
|
||||
#calendarContainer {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.calendar-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.calendar-nav-btn {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.calendar-nav-btn:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.calendar-day-header {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
padding: 12px 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.calendar-day.empty {
|
||||
background: #f8f9fa;
|
||||
border-color: #f1f3f5;
|
||||
}
|
||||
|
||||
.calendar-day.has-record {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.calendar-day.has-record:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 달력 날짜 상태별 색상 */
|
||||
.calendar-day.normal {
|
||||
background: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.calendar-day.late {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.calendar-day.early {
|
||||
background: #ffe5b5;
|
||||
border-color: #ffd98a;
|
||||
color: #a56200;
|
||||
}
|
||||
|
||||
.calendar-day.absent {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.calendar-day.vacation {
|
||||
background: #cce5ff;
|
||||
border-color: #b8daff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.calendar-day-number {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.calendar-day-status {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 달력 범례 */
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.legend-dot.normal {
|
||||
background: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
}
|
||||
|
||||
.legend-dot.late {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
}
|
||||
|
||||
.legend-dot.early {
|
||||
background: #ffe5b5;
|
||||
border-color: #ffd98a;
|
||||
}
|
||||
|
||||
.legend-dot.absent {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.legend-dot.vacation {
|
||||
background: #cce5ff;
|
||||
border-color: #b8daff;
|
||||
}
|
||||
|
||||
/* 모달 스타일 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
color: #6c757d;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: #f8f9fa;
|
||||
color: #343a40;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 상세 정보 그리드 */
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-item.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.detail-item div {
|
||||
font-size: 15px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* 버튼 스타일 */
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.calendar-day-number {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.calendar-day-status {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#attendanceTable {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#attendanceTable th,
|
||||
#attendanceTable td {
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.notes-cell {
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
/* My Dashboard CSS */
|
||||
|
||||
.dashboard-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
background: #f8f9fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: white;
|
||||
color: #007bff;
|
||||
transform: translateY(-0.0625rem);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2rem;
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 사용자 정보 카드 */
|
||||
.user-info-card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* 연차 정보 위젯 */
|
||||
.vacation-widget {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.vacation-widget h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.vacation-summary {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.vacation-summary .stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vacation-summary .label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.vacation-summary .value {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 1.5rem;
|
||||
background: rgba(255,255,255,0.2);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: rgba(255,255,255,0.8);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
/* 캘린더 섹션 */
|
||||
.calendar-section {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.calendar-section h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.calendar-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.calendar-controls button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.calendar-controls button:hover {
|
||||
background: #764ba2;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.calendar-day:hover:not(.empty) {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.calendar-day.empty {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.calendar-day.normal {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.calendar-day.late {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.calendar-day.vacation {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.calendar-day.absent {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.calendar-legend span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot.normal {
|
||||
background: #d4edda;
|
||||
}
|
||||
|
||||
.dot.late {
|
||||
background: #fff3cd;
|
||||
}
|
||||
|
||||
.dot.vacation {
|
||||
background: #d1ecf1;
|
||||
}
|
||||
|
||||
.dot.absent {
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
/* 근무 시간 통계 */
|
||||
.work-hours-stats {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 최근 작업 보고서 */
|
||||
.recent-reports {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.recent-reports h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.report-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.report-item .date {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.report-item .project {
|
||||
flex: 1;
|
||||
margin: 0 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.report-item .hours {
|
||||
color: #667eea;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.vacation-summary {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,953 +0,0 @@
|
||||
/* 시스템 대시보드 전용 스타일 */
|
||||
|
||||
/* 시스템 대시보드 배경 - 깔끔한 흰색 */
|
||||
.main-layout .content-wrapper {
|
||||
background: #ffffff;
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 0;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 시스템 관리자 배너 */
|
||||
.system-banner {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.system-banner::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.banner-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.system-icon {
|
||||
font-size: 3rem;
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 1rem;
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.banner-text h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.banner-text p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.banner-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.system-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(255,255,255,0.15);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.quick-btn:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background: #2ecc71;
|
||||
box-shadow: 0 0 10px rgba(46,204,113,0.5);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 메인 컨텐츠 패딩 조정 */
|
||||
.main-content {
|
||||
padding: 0 2rem 2rem 2rem;
|
||||
}
|
||||
|
||||
/* 반응형 배너 */
|
||||
@media (max-width: 768px) {
|
||||
.system-banner {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.banner-left {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.system-icon {
|
||||
font-size: 2.5rem;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.banner-text h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.banner-text p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.banner-right {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 0 1rem 2rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* 시스템 배지 */
|
||||
|
||||
.system-badge {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid #c0392b;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #c0392b;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
/* 메인 컨텐츠 */
|
||||
.main-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 시스템 상태 개요 */
|
||||
.system-overview {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.system-overview h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.status-value.online {
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.status-value.warning {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.status-value.error {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.status-info small {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* 관리 섹션 */
|
||||
.management-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.management-section h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.management-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.management-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.management-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.management-card.primary {
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
font-size: 1.2rem;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-content p {
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.9rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border-color: #2980b9;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
border-color: #7f8c8d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
/* 최근 활동 */
|
||||
.recent-activity {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.recent-activity h2 {
|
||||
color: white;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.activity-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: rgba(52, 152, 219, 0.05);
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.activity-info h4 {
|
||||
margin: 0 0 0.3rem 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-info p {
|
||||
margin: 0;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
margin-left: auto;
|
||||
color: #95a5a6;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* 모달 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
margin: 5% auto;
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 2rem;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 1200px) {
|
||||
.status-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
.management-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-layout .content-wrapper {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.status-grid,
|
||||
.management-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.management-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 5% auto;
|
||||
}
|
||||
|
||||
.system-overview h2,
|
||||
.management-section h2,
|
||||
.recent-activity h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.main-layout .content-wrapper {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.status-card,
|
||||
.management-card {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 0.8rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 계정 관리 스타일 */
|
||||
.account-management {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.account-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #ecf0f1;
|
||||
}
|
||||
|
||||
.account-header h4 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.account-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.account-filters input,
|
||||
.account-filters select {
|
||||
padding: 0.7rem;
|
||||
border: 2px solid #ecf0f1;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.account-filters input:focus,
|
||||
.account-filters select:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
overflow-x: auto;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.users-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.users-table th,
|
||||
.users-table td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
}
|
||||
|
||||
.users-table th {
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.users-table tr:hover {
|
||||
background: rgba(52, 152, 219, 0.05);
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.role-badge.role-system {
|
||||
background: linear-gradient(45deg, #e74c3c, #c0392b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.role-badge.role-admin {
|
||||
background: linear-gradient(45deg, #f39c12, #e67e22);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.role-badge.role-leader {
|
||||
background: linear-gradient(45deg, #9b59b6, #8e44ad);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.role-badge.role-user {
|
||||
background: linear-gradient(45deg, #95a5a6, #7f8c8d);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: linear-gradient(45deg, #27ae60, #2ecc71);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: linear-gradient(45deg, #e74c3c, #c0392b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: linear-gradient(45deg, #e74c3c, #c0392b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.error-message i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message p {
|
||||
margin: 1rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 알림 스타일 */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 3000;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
background: linear-gradient(45deg, #27ae60, #2ecc71);
|
||||
}
|
||||
|
||||
.notification-warning {
|
||||
background: linear-gradient(45deg, #f39c12, #e67e22);
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
background: linear-gradient(45deg, #e74c3c, #c0392b);
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 사용자 폼 스타일 */
|
||||
.user-edit-form,
|
||||
.user-create-form {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.user-edit-form h4,
|
||||
.user-create-form h4 {
|
||||
margin: 0 0 2rem 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 2px solid #ecf0f1;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 2px solid #ecf0f1;
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일링 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(45deg, #3498db, #2980b9);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(45deg, #2980b9, #3498db);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
body {
|
||||
font-family: 'Malgun Gothic', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 30px;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.quick-menu {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #e8f5e9;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
/* 작업 관리 페이지 스타일 */
|
||||
|
||||
/* 기본 레이아웃 */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 헤더 스타일은 navbar.html에서 관리됨 */
|
||||
|
||||
/* 메인 콘텐츠 */
|
||||
.dashboard-main {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
min-height: calc(100vh - 80px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title-section {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 0.5rem 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 1.125rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 관리 메뉴 그리드 */
|
||||
.management-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.management-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.management-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.management-card:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.management-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f3f4f6, #e5e7eb);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.card-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
font-size: 0.875rem;
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.management-card:hover .card-action {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
/* 최근 활동 섹션 */
|
||||
.recent-activity-section {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: 1.25rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: white;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.25rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.activity-meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.activity-user {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-main {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.management-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.management-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.dashboard-main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.management-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.recent-activity-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 빠른 액세스 섹션 ========== */
|
||||
.quick-access-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border: 2px solid rgba(59, 130, 246, 0.1);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
min-height: 120px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.quick-text {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ========== 관리 섹션 ========== */
|
||||
.management-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* ========== 시스템 상태 섹션 ========== */
|
||||
.system-status-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
@@ -1,839 +0,0 @@
|
||||
/* work-review.css - 작업 검토 페이지 전용 스타일 (개선된 버전) */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.main-layout-with-navbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.review-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 페이지 헤더 */
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 24px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 뒤로가기 버튼 */
|
||||
.back-btn {
|
||||
background: rgba(255,255,255,0.95);
|
||||
color: #667eea;
|
||||
border: 3px solid #667eea;
|
||||
padding: 12px 24px;
|
||||
border-radius: 12px;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 컨트롤 패널 */
|
||||
.control-panel {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.month-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-btn, .today-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.nav-btn:hover, .today-btn:hover {
|
||||
background: #0056b3;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.today-btn {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.today-btn:hover {
|
||||
background: #1e7e34;
|
||||
}
|
||||
|
||||
.current-month {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 사용법 안내 */
|
||||
.usage-guide {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.usage-guide h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
font-size: 1.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border: 2px solid #dee2e6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.guide-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.guide-icon {
|
||||
font-size: 2rem;
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guide-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.guide-text strong {
|
||||
color: #007bff;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* 캘린더 */
|
||||
.calendar-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.calendar-container h3 {
|
||||
color: #333;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
border: 3px solid #dee2e6;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
min-height: 80px;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.day-cell:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.day-cell.other-month {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.day-cell.today {
|
||||
border: 3px solid #007bff;
|
||||
background: #e7f3ff;
|
||||
}
|
||||
|
||||
.day-cell.selected {
|
||||
background: #d4edda;
|
||||
border: 3px solid #28a745;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.day-cell.selected .day-number {
|
||||
color: #155724;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* 선택된 날짜 정보 패널 */
|
||||
.day-info-panel {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.day-info-placeholder {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.day-info-placeholder h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.day-info-content {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.day-info-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 3px solid #dee2e6;
|
||||
}
|
||||
|
||||
.day-info-header h3 {
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.day-info-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.review-toggle, .refresh-day-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.review-toggle {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.review-toggle.reviewed {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.review-toggle:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.refresh-day-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.refresh-day-btn:hover {
|
||||
background: #545b62;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 일별 요약 정보 */
|
||||
.day-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 12px;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.summary-value.normal-work {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.summary-value.overtime {
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.summary-value.vacation {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.summary-value.reviewed {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.summary-value.unreviewed {
|
||||
color: #fd7e14;
|
||||
}
|
||||
|
||||
/* 작업자별 상세 섹션 */
|
||||
.workers-detail-container h4 {
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.worker-detail-section {
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.worker-header-detail {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.delete-worker-btn {
|
||||
background: rgba(220, 53, 69, 0.9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.delete-worker-btn:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.worker-work-items {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.work-item-detail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #007bff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.work-item-detail:hover {
|
||||
background: #e9ecef;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.work-item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.work-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-work-btn, .delete-work-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-work-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.edit-work-btn:hover {
|
||||
background: #0056b3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.delete-work-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-work-btn:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 메시지 */
|
||||
.message {
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 600;
|
||||
animation: slideInDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message.loading {
|
||||
background: #cce5ff;
|
||||
color: #0066cc;
|
||||
border: 2px solid #99d6ff;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 2px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 2px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 2px solid #ffeaa7;
|
||||
}
|
||||
|
||||
/* 수정 모달 스타일 */
|
||||
.edit-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1001;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.edit-modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
animation: slideInUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(50px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.edit-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.close-modal-btn {
|
||||
background: rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.close-modal-btn:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.edit-modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.edit-form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.edit-form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.edit-select, .edit-input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.edit-select:focus, .edit-input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.edit-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
border-top: 2px solid #f0f0f0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
|
||||
/* 확인 모달 */
|
||||
.confirm-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1002;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.confirm-modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||
animation: slideInUp 0.3s ease;
|
||||
}
|
||||
|
||||
.confirm-modal-header {
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.confirm-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.confirm-modal-body {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.confirm-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 16px 24px 24px;
|
||||
border-top: 2px solid #f0f0f0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0 0 16px 16px;
|
||||
}
|
||||
|
||||
/* 버튼 스타일 */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #1e7e34;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.content-wrapper {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.month-navigation {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
min-height: 60px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.day-summary {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.day-info-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day-info-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.review-toggle, .refresh-day-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.work-item-detail {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.work-item-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.worker-header-detail {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.edit-modal-content {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.edit-modal-footer, .confirm-modal-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-work-btn, .delete-work-btn {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,10 @@
|
||||
var token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
|
||||
if (token && token !== 'undefined' && token !== 'null') {
|
||||
// 이미 로그인된 경우 대시보드로 이동
|
||||
window.location.replace('/pages/dashboard.html');
|
||||
window.location.replace('/pages/dashboard-new.html');
|
||||
} else {
|
||||
// SSO 로그인 페이지로 리다이렉트 (gateway의 /login)
|
||||
window.location.replace('/login?redirect=' + encodeURIComponent('/pages/dashboard.html'));
|
||||
window.location.replace('/login?redirect=' + encodeURIComponent('/pages/dashboard-new.html'));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,34 +0,0 @@
|
||||
// ✅ /js/admin.js (수정됨 - 중복 로딩 제거)
|
||||
async function initDashboard() {
|
||||
// 로그인 토큰 확인
|
||||
const token = localStorage.getItem('sso_token') || (window.getSSOToken ? window.getSSOToken() : null);
|
||||
if (!token) {
|
||||
location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ navbar, sidebar는 각각의 모듈에서 처리하도록 변경
|
||||
// load-navbar.js, load-sidebar.js가 자동으로 처리함
|
||||
|
||||
// ✅ 콘텐츠만 직접 로딩 (admin-sections.html이 자동 로딩됨)
|
||||
console.log('관리자 대시보드 초기화 완료');
|
||||
}
|
||||
|
||||
// ✅ 보조 함수 - 필요시 수동 컴포넌트 로딩용
|
||||
async function loadComponent(id, url) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const html = await res.text();
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.innerHTML = html;
|
||||
} else {
|
||||
console.warn(`요소를 찾을 수 없습니다: ${id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`컴포넌트 로딩 실패 (${url}):`, err);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initDashboard);
|
||||
@@ -1,139 +0,0 @@
|
||||
// /public/js/api-helper.js
|
||||
// ES6 모듈 의존성 제거 - 브라우저 호환성 개선
|
||||
|
||||
// API 설정 (window 객체에서 가져오기)
|
||||
const API_BASE_URL = window.API_BASE_URL || 'http://localhost:30005/api';
|
||||
|
||||
// 인증 관련 함수들 (직접 구현)
|
||||
function getToken() {
|
||||
// SSO 토큰 우선, 기존 token 폴백
|
||||
if (window.getSSOToken) return window.getSSOToken();
|
||||
const token = localStorage.getItem('sso_token');
|
||||
return token && token !== 'undefined' && token !== 'null' ? token : null;
|
||||
}
|
||||
|
||||
function clearAuthData() {
|
||||
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 API를 호출합니다. (인증이 필요 없는 public 요청)
|
||||
* @param {string} username - 사용자 아이디
|
||||
* @param {string} password - 사용자 비밀번호
|
||||
* @returns {Promise<object>} - API 응답 결과
|
||||
*/
|
||||
async function login(username, password) {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
// API 에러 응답을 그대로 에러 객체로 던져서 호출부에서 처리하도록 함
|
||||
throw new Error(result.error || '로그인에 실패했습니다.');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 인증이 필요한 API 요청을 위한 fetch 래퍼 함수
|
||||
* @param {string} endpoint - /로 시작하는 API 엔드포인트
|
||||
* @param {object} options - fetch 함수에 전달할 옵션
|
||||
* @returns {Promise<Response>} - fetch 응답 객체
|
||||
*/
|
||||
async function authFetch(endpoint, options = {}) {
|
||||
const token = getToken();
|
||||
|
||||
if (!token) {
|
||||
console.error('토큰이 없습니다. 로그인이 필요합니다.');
|
||||
clearAuthData(); // 인증 정보 정리
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||
// 에러를 던져서 후속 실행을 중단
|
||||
throw new Error('인증 토큰이 없습니다.');
|
||||
}
|
||||
|
||||
const defaultHeaders = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
// 401 Unauthorized 에러 발생 시, 토큰이 유효하지 않다는 의미
|
||||
if (response.status === 401) {
|
||||
console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.');
|
||||
clearAuthData(); // 만료된 인증 정보 정리
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||
throw new Error('인증에 실패했습니다.');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// 공통 API 요청 함수들
|
||||
|
||||
/**
|
||||
* GET 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
*/
|
||||
async function apiGet(endpoint) {
|
||||
const response = await authFetch(endpoint);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
* @param {object} data - 전송할 데이터
|
||||
*/
|
||||
async function apiPost(endpoint, data) {
|
||||
const response = await authFetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
* @param {object} data - 전송할 데이터
|
||||
*/
|
||||
async function apiPut(endpoint, data) {
|
||||
const response = await authFetch(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 요청 헬퍼
|
||||
* @param {string} endpoint - API 엔드포인트
|
||||
*/
|
||||
async function apiDelete(endpoint) {
|
||||
const response = await authFetch(endpoint, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 전역 함수로 설정
|
||||
window.login = login;
|
||||
window.apiGet = apiGet;
|
||||
window.apiPost = apiPost;
|
||||
window.apiPut = apiPut;
|
||||
window.apiDelete = apiDelete;
|
||||
window.getToken = getToken;
|
||||
window.clearAuthData = clearAuthData;
|
||||
@@ -1,589 +0,0 @@
|
||||
// /js/app-init.js
|
||||
// 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드
|
||||
// 모든 페이지에서 이 하나의 스크립트만 로드하면 됨
|
||||
// api-base.js가 먼저 로드되어야 함 (getSSOToken, getSSOUser, clearSSOAuth 등)
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ===== 캐시 설정 =====
|
||||
const CACHE_DURATION = 10 * 60 * 1000; // 10분
|
||||
const COMPONENT_CACHE_PREFIX = 'component_v3_';
|
||||
|
||||
// ===== 인증 함수 (api-base.js의 SSO 함수 활용) =====
|
||||
function isLoggedIn() {
|
||||
const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
||||
return token && token !== 'undefined' && token !== 'null';
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
if (window.getSSOUser) return window.getSSOUser();
|
||||
const user = localStorage.getItem('sso_user');
|
||||
try { return user ? JSON.parse(user) : null; } catch(e) { return null; }
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
return window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
||||
}
|
||||
|
||||
function clearAuthData() {
|
||||
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
localStorage.removeItem('userPageAccess');
|
||||
}
|
||||
|
||||
// ===== 페이지 권한 캐시 =====
|
||||
let pageAccessPromise = null;
|
||||
|
||||
async function getPageAccess(currentUser) {
|
||||
if (!currentUser || !currentUser.user_id) return null;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = localStorage.getItem('userPageAccess');
|
||||
if (cached) {
|
||||
try {
|
||||
const cacheData = JSON.parse(cached);
|
||||
if (Date.now() - cacheData.timestamp < CACHE_DURATION) {
|
||||
return cacheData.pages;
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem('userPageAccess');
|
||||
}
|
||||
}
|
||||
|
||||
// 이미 로딩 중이면 기존 Promise 반환
|
||||
if (pageAccessPromise) return pageAccessPromise;
|
||||
|
||||
// 새로운 API 호출
|
||||
pageAccessPromise = (async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = await response.json();
|
||||
const pages = data.data.pageAccess || [];
|
||||
|
||||
localStorage.setItem('userPageAccess', JSON.stringify({
|
||||
pages: pages,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
return pages;
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 조회 오류:', error);
|
||||
return null;
|
||||
} finally {
|
||||
pageAccessPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return pageAccessPromise;
|
||||
}
|
||||
|
||||
async function getAccessiblePageKeys(currentUser) {
|
||||
const pages = await getPageAccess(currentUser);
|
||||
if (!pages) return [];
|
||||
return pages.filter(p => p.can_access == 1).map(p => p.page_key);
|
||||
}
|
||||
|
||||
// ===== 현재 페이지 키 추출 =====
|
||||
// 하위 페이지 → 부모 페이지 키 매핑 (동일 권한 공유)
|
||||
const PAGE_KEY_ALIASES = {
|
||||
'work.tbm-mobile': 'work.tbm',
|
||||
'work.tbm-create': 'work.tbm',
|
||||
'work.report-create-mobile': 'work.report-create',
|
||||
'admin.equipment-detail': 'admin.equipments',
|
||||
'safety.issue-detail': 'safety.issue-report'
|
||||
};
|
||||
|
||||
function getCurrentPageKey() {
|
||||
const path = window.location.pathname;
|
||||
if (!path.startsWith('/pages/')) return null;
|
||||
const pagePath = path.substring(7).replace('.html', '');
|
||||
const rawKey = pagePath.replace(/\//g, '.');
|
||||
return PAGE_KEY_ALIASES[rawKey] || rawKey;
|
||||
}
|
||||
|
||||
// ===== 컴포넌트 로더 =====
|
||||
async function loadComponent(name, selector, processor) {
|
||||
const container = document.querySelector(selector);
|
||||
if (!container) return;
|
||||
|
||||
const paths = {
|
||||
'navbar': '/components/navbar.html',
|
||||
'sidebar-nav': '/components/sidebar-nav.html'
|
||||
};
|
||||
|
||||
const componentPath = paths[name];
|
||||
if (!componentPath) return;
|
||||
|
||||
try {
|
||||
const cacheKey = COMPONENT_CACHE_PREFIX + name;
|
||||
let html = sessionStorage.getItem(cacheKey);
|
||||
|
||||
if (!html) {
|
||||
const response = await fetch(componentPath);
|
||||
if (!response.ok) throw new Error('컴포넌트 로드 실패');
|
||||
html = await response.text();
|
||||
try { sessionStorage.setItem(cacheKey, html); } catch (e) {}
|
||||
}
|
||||
|
||||
if (processor) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
await processor(doc);
|
||||
container.innerHTML = doc.body.innerHTML;
|
||||
} else {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`컴포넌트 로드 오류 (${name}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 네비바 처리 =====
|
||||
const ROLE_NAMES = {
|
||||
'system admin': '시스템 관리자',
|
||||
'admin': '관리자',
|
||||
'leader': '그룹장',
|
||||
'user': '작업자',
|
||||
'support': '지원팀',
|
||||
'default': '사용자'
|
||||
};
|
||||
|
||||
async function processNavbar(doc, currentUser, accessiblePageKeys) {
|
||||
const userRole = (currentUser.role || '').toLowerCase();
|
||||
const isAdmin = userRole === 'admin' || userRole === 'system admin';
|
||||
|
||||
if (isAdmin) {
|
||||
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
|
||||
} else {
|
||||
doc.querySelectorAll('[data-page-key]').forEach(item => {
|
||||
const pageKey = item.getAttribute('data-page-key');
|
||||
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return;
|
||||
if (!accessiblePageKeys.includes(pageKey)) item.remove();
|
||||
});
|
||||
doc.querySelectorAll('.admin-only').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 사용자 정보 표시
|
||||
const displayName = currentUser.name || currentUser.username;
|
||||
const roleName = ROLE_NAMES[userRole] || ROLE_NAMES.default;
|
||||
|
||||
const setElementText = (id, text) => {
|
||||
const el = doc.getElementById(id);
|
||||
if (el) el.textContent = text;
|
||||
};
|
||||
|
||||
setElementText('userName', displayName);
|
||||
setElementText('userRole', roleName);
|
||||
setElementText('userInitial', displayName.charAt(0));
|
||||
}
|
||||
|
||||
// ===== 사이드바 처리 =====
|
||||
async function processSidebar(doc, currentUser, accessiblePageKeys) {
|
||||
const userRole = (currentUser.role || '').toLowerCase();
|
||||
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
||||
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
||||
accessLevel === 'admin' || accessLevel === 'system';
|
||||
|
||||
if (isAdmin) {
|
||||
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
|
||||
} else {
|
||||
doc.querySelectorAll('[data-page-key]').forEach(item => {
|
||||
const pageKey = item.getAttribute('data-page-key');
|
||||
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return;
|
||||
if (!accessiblePageKeys.includes(pageKey)) item.style.display = 'none';
|
||||
});
|
||||
doc.querySelectorAll('.nav-category.admin-only').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
// 현재 페이지 하이라이트
|
||||
const currentPath = window.location.pathname;
|
||||
doc.querySelectorAll('.nav-item').forEach(item => {
|
||||
const href = item.getAttribute('href');
|
||||
if (href && currentPath.includes(href.replace(/^\//, ''))) {
|
||||
item.classList.add('active');
|
||||
const category = item.closest('.nav-category');
|
||||
if (category) category.classList.add('expanded');
|
||||
}
|
||||
});
|
||||
|
||||
// 저장된 상태 복원 (기본값: 접힌 상태)
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
||||
const sidebar = doc.querySelector('.sidebar-nav');
|
||||
if (isCollapsed && sidebar) {
|
||||
sidebar.classList.add('collapsed');
|
||||
document.body.classList.add('sidebar-collapsed');
|
||||
}
|
||||
|
||||
const expandedCategories = JSON.parse(localStorage.getItem('sidebarExpanded') || '[]');
|
||||
expandedCategories.forEach(category => {
|
||||
const el = doc.querySelector(`[data-category="${category}"]`);
|
||||
if (el) el.classList.add('expanded');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 사이드바 이벤트 설정 =====
|
||||
function setupSidebarEvents() {
|
||||
const sidebar = document.getElementById('sidebarNav');
|
||||
const toggle = document.getElementById('sidebarToggle');
|
||||
if (!sidebar || !toggle) return;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
document.body.classList.toggle('sidebar-collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
|
||||
});
|
||||
|
||||
sidebar.querySelectorAll('.nav-category-header').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const category = header.closest('.nav-category');
|
||||
category.classList.toggle('expanded');
|
||||
|
||||
const expanded = [];
|
||||
sidebar.querySelectorAll('.nav-category.expanded').forEach(cat => {
|
||||
const name = cat.getAttribute('data-category');
|
||||
if (name) expanded.push(name);
|
||||
});
|
||||
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 네비바 이벤트 설정 =====
|
||||
function setupNavbarEvents() {
|
||||
const logoutButton = document.getElementById('logoutBtn');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
if (confirm('로그아웃 하시겠습니까?')) {
|
||||
clearAuthData();
|
||||
if (window.clearSSOAuth) window.clearSSOAuth();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent('/pages/dashboard.html');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 알림 버튼 이벤트
|
||||
const notificationBtn = document.getElementById('notificationBtn');
|
||||
const notificationDropdown = document.getElementById('notificationDropdown');
|
||||
const notificationWrapper = document.getElementById('notificationWrapper');
|
||||
|
||||
if (notificationBtn && notificationDropdown) {
|
||||
notificationBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
notificationDropdown.classList.toggle('show');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (notificationWrapper && !notificationWrapper.contains(e.target)) {
|
||||
notificationDropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 알림 로드 =====
|
||||
async function loadNotifications() {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) return;
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const notifications = result.data || [];
|
||||
updateNotificationBadge(notifications.length);
|
||||
renderNotificationList(notifications);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('알림 로드 오류:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateNotificationBadge(count) {
|
||||
const badge = document.getElementById('notificationBadge');
|
||||
const btn = document.getElementById('notificationBtn');
|
||||
if (!badge) return;
|
||||
|
||||
if (count > 0) {
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
badge.style.display = 'flex';
|
||||
btn?.classList.add('has-notifications');
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
btn?.classList.remove('has-notifications');
|
||||
}
|
||||
}
|
||||
|
||||
function renderNotificationList(notifications) {
|
||||
const list = document.getElementById('notificationList');
|
||||
if (!list) return;
|
||||
|
||||
if (notifications.length === 0) {
|
||||
list.innerHTML = '<div class="notification-empty">새 알림이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const icons = { repair: '\ud83d\udd27', safety: '\u26a0\ufe0f', system: '\ud83d\udce2', equipment: '\ud83d\udea9', maintenance: '\ud83d\udee0\ufe0f' };
|
||||
|
||||
list.innerHTML = notifications.slice(0, 5).map(n => `
|
||||
<div class="notification-item ${n.is_read ? '' : 'unread'}" data-id="${n.notification_id}" data-url="${n.link_url || ''}">
|
||||
<div class="notification-item-icon ${n.type || 'repair'}">${icons[n.type] || '\ud83d\udd14'}</div>
|
||||
<div class="notification-item-content">
|
||||
<div class="notification-item-title">${escapeHtml(n.title)}</div>
|
||||
<div class="notification-item-desc">${escapeHtml(n.message || '')}</div>
|
||||
</div>
|
||||
<div class="notification-item-time">${formatTimeAgo(n.created_at)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
list.querySelectorAll('.notification-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const url = item.dataset.url;
|
||||
window.location.href = url || '/pages/admin/notifications.html';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatTimeAgo(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return '방금 전';
|
||||
if (diffMins < 60) return `${diffMins}분 전`;
|
||||
if (diffHours < 24) return `${diffHours}시간 전`;
|
||||
if (diffDays < 7) return `${diffDays}일 전`;
|
||||
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ===== 날짜/시간 업데이트 =====
|
||||
function updateDateTime() {
|
||||
const now = new Date();
|
||||
const timeEl = document.getElementById('timeValue');
|
||||
if (timeEl) {
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
timeEl.textContent = `${hours}시 ${minutes}분 ${seconds}초`;
|
||||
}
|
||||
|
||||
const dateEl = document.getElementById('dateValue');
|
||||
if (dateEl) {
|
||||
const days = ['일', '월', '화', '수', '목', '토'];
|
||||
dateEl.textContent = `${now.getMonth() + 1}월 ${now.getDate()}일 (${days[now.getDay()]})`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 날씨 업데이트 =====
|
||||
const WEATHER_ICONS = { clear: '\u2600\ufe0f', rain: '\ud83c\udf27\ufe0f', snow: '\u2744\ufe0f', heat: '\ud83e\udd75', cold: '\ud83e\udd76', wind: '\ud83c\udf2c\ufe0f', fog: '\ud83c\udf2b\ufe0f', dust: '\ud83d\ude37', cloudy: '\u26c5', overcast: '\u2601\ufe0f' };
|
||||
const WEATHER_NAMES = { clear: '맑음', rain: '비', snow: '눈', heat: '폭염', cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지', cloudy: '구름많음', overcast: '흐림' };
|
||||
|
||||
async function updateWeather() {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
|
||||
// 캐시 확인
|
||||
const cached = sessionStorage.getItem('weatherCache');
|
||||
let result;
|
||||
if (cached) {
|
||||
const cacheData = JSON.parse(cached);
|
||||
if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) {
|
||||
result = cacheData.data;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
const response = await fetch(`${window.API_BASE_URL}/tbm/weather/current`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!response.ok) return;
|
||||
result = await response.json();
|
||||
sessionStorage.setItem('weatherCache', JSON.stringify({ data: result, timestamp: Date.now() }));
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
const { temperature, conditions } = result.data;
|
||||
const tempEl = document.getElementById('weatherTemp');
|
||||
if (tempEl && temperature != null) tempEl.textContent = `${Math.round(temperature)}°C`;
|
||||
|
||||
const iconEl = document.getElementById('weatherIcon');
|
||||
const descEl = document.getElementById('weatherDesc');
|
||||
if (conditions && conditions.length > 0) {
|
||||
const primary = conditions[0];
|
||||
if (iconEl) iconEl.textContent = WEATHER_ICONS[primary] || '\ud83c\udf24\ufe0f';
|
||||
if (descEl) descEl.textContent = WEATHER_NAMES[primary] || '맑음';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('날씨 정보 로드 실패');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 메인 초기화 =====
|
||||
async function init() {
|
||||
// 1. 인증 확인
|
||||
if (!isLoggedIn()) {
|
||||
clearAuthData();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUser = getUser();
|
||||
if (!currentUser || !currentUser.username) {
|
||||
clearAuthData();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||
return;
|
||||
}
|
||||
|
||||
const userRole = (currentUser.role || '').toLowerCase();
|
||||
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
||||
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
||||
accessLevel === 'admin' || accessLevel === 'system';
|
||||
|
||||
// 2. 페이지 접근 권한 체크 (Admin은 건너뛰기, API 실패시 허용)
|
||||
let accessiblePageKeys = [];
|
||||
const pageKey = getCurrentPageKey();
|
||||
if (!isAdmin) {
|
||||
const pages = await getPageAccess(currentUser);
|
||||
if (pages) {
|
||||
accessiblePageKeys = pages.filter(p => p.can_access == 1).map(p => p.page_key);
|
||||
if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) {
|
||||
if (!accessiblePageKeys.includes(pageKey)) {
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
window.location.href = '/pages/dashboard.html';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 사이드바 컨테이너 생성 (없으면)
|
||||
let sidebarContainer = document.getElementById('sidebar-container');
|
||||
if (!sidebarContainer) {
|
||||
sidebarContainer = document.createElement('div');
|
||||
sidebarContainer.id = 'sidebar-container';
|
||||
document.body.prepend(sidebarContainer);
|
||||
}
|
||||
|
||||
// 4. 네비바와 사이드바 동시 로드
|
||||
await Promise.all([
|
||||
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
|
||||
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
|
||||
]);
|
||||
|
||||
// 5. 이벤트 설정
|
||||
setupNavbarEvents();
|
||||
setupSidebarEvents();
|
||||
document.body.classList.add('has-sidebar');
|
||||
|
||||
// 6. 페이지 전환 로딩 인디케이터 설정
|
||||
setupPageTransitionLoader();
|
||||
|
||||
// 7. 날짜/시간 (비동기)
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 1000);
|
||||
|
||||
// 8. 날씨 (백그라운드)
|
||||
setTimeout(updateWeather, 100);
|
||||
|
||||
// 9. 알림 로드 (30초마다 갱신)
|
||||
setTimeout(loadNotifications, 200);
|
||||
setInterval(loadNotifications, 30000);
|
||||
}
|
||||
|
||||
// ===== 페이지 전환 로딩 인디케이터 =====
|
||||
function setupPageTransitionLoader() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#page-loader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
||||
z-index: 99999;
|
||||
transition: width 0.3s ease;
|
||||
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
#page-loader.loading {
|
||||
width: 70%;
|
||||
}
|
||||
#page-loader.done {
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: width 0.2s ease, opacity 0.3s ease 0.2s;
|
||||
}
|
||||
body.page-loading {
|
||||
cursor: wait;
|
||||
}
|
||||
body.page-loading * {
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const loader = document.createElement('div');
|
||||
loader.id = 'page-loader';
|
||||
document.body.appendChild(loader);
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a');
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return;
|
||||
if (link.target === '_blank') return;
|
||||
|
||||
loader.classList.remove('done');
|
||||
loader.classList.add('loading');
|
||||
document.body.classList.add('page-loading');
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const loader = document.getElementById('page-loader');
|
||||
if (loader) {
|
||||
loader.classList.remove('loading');
|
||||
loader.classList.add('done');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// DOMContentLoaded 시 실행
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// 전역 노출 (필요시)
|
||||
window.appInit = { getUser, getToken, clearAuthData, isLoggedIn };
|
||||
})();
|
||||
@@ -1,178 +0,0 @@
|
||||
// /js/auth-check.js
|
||||
// auth.js의 함수들을 직접 구현 (모듈 의존성 제거)
|
||||
|
||||
function isLoggedIn() {
|
||||
var token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
||||
return token && token !== 'undefined' && token !== 'null';
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
return window.getSSOUser ? window.getSSOUser() : (function() {
|
||||
var u = localStorage.getItem('sso_user');
|
||||
return u ? JSON.parse(u) : null;
|
||||
})();
|
||||
}
|
||||
|
||||
function clearAuthData() {
|
||||
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||
localStorage.removeItem('sso_token');
|
||||
localStorage.removeItem('sso_user');
|
||||
localStorage.removeItem('userPageAccess');
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 페이지의 page_key를 URL 경로로부터 추출
|
||||
* 예: /pages/work/tbm.html -> work.tbm
|
||||
* /pages/admin/accounts.html -> admin.accounts
|
||||
* /pages/dashboard.html -> dashboard
|
||||
*/
|
||||
// 하위 페이지 → 부모 페이지 키 매핑 (동일 권한 공유)
|
||||
var PAGE_KEY_ALIASES = {
|
||||
'work.tbm-create': 'work.tbm',
|
||||
'work.tbm-mobile': 'work.tbm'
|
||||
};
|
||||
|
||||
function getCurrentPageKey() {
|
||||
const path = window.location.pathname;
|
||||
|
||||
// /pages/로 시작하는지 확인
|
||||
if (!path.startsWith('/pages/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// /pages/ 이후 경로 추출
|
||||
const pagePath = path.substring(7); // '/pages/' 제거
|
||||
|
||||
// .html 제거
|
||||
const withoutExt = pagePath.replace('.html', '');
|
||||
|
||||
// 슬래시를 점으로 변환
|
||||
const rawKey = withoutExt.replace(/\//g, '.');
|
||||
|
||||
return PAGE_KEY_ALIASES[rawKey] || rawKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한 확인 (캐시 활용)
|
||||
*/
|
||||
async function checkPageAccess(pageKey) {
|
||||
const currentUser = getUser();
|
||||
|
||||
// Admin은 모든 페이지 접근 가능
|
||||
if (currentUser.role === 'Admin' || currentUser.role === 'System Admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 프로필 페이지는 모든 사용자 접근 가능
|
||||
if (pageKey && pageKey.startsWith('profile.')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 대시보드는 모든 사용자 접근 가능
|
||||
if (pageKey === 'dashboard') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 캐시된 권한 확인
|
||||
const cached = localStorage.getItem('userPageAccess');
|
||||
let accessiblePages = null;
|
||||
|
||||
if (cached) {
|
||||
const cacheData = JSON.parse(cached);
|
||||
// 캐시가 5분 이내인 경우 사용
|
||||
if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) {
|
||||
accessiblePages = cacheData.pages;
|
||||
}
|
||||
}
|
||||
|
||||
// 캐시가 없으면 API 호출
|
||||
if (!accessiblePages) {
|
||||
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + (window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('페이지 권한 조회 실패:', response.status);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
accessiblePages = data.data.pageAccess || [];
|
||||
|
||||
// 캐시 저장
|
||||
localStorage.setItem('userPageAccess', JSON.stringify({
|
||||
pages: accessiblePages,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
// 해당 페이지에 대한 접근 권한 확인
|
||||
const pageAccess = accessiblePages.find(p => p.page_key === pageKey);
|
||||
return pageAccess && pageAccess.can_access === 1;
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 체크 오류:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 쿠키 직접 읽기 (api-base.js의 cookieGet은 IIFE 내부 함수이므로 접근 불가)
|
||||
function _authCookieGet(name) {
|
||||
var match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
|
||||
// 즉시 실행 함수로 스코프를 보호하고 로직을 실행
|
||||
(async function() {
|
||||
// 쿠키 우선 검증: 쿠키 없고 localStorage에만 토큰이 있으면 정리
|
||||
var cookieToken = _authCookieGet('sso_token');
|
||||
var localToken = localStorage.getItem('sso_token');
|
||||
if (!cookieToken && localToken) {
|
||||
['sso_token','sso_user','sso_refresh_token','token','user','access_token',
|
||||
'currentUser','current_user','userInfo','userPageAccess'].forEach(function(k) { localStorage.removeItem(k); });
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
clearAuthData(); // 만약을 위해 한번 더 정리
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
return; // 이후 코드 실행 방지
|
||||
}
|
||||
|
||||
const currentUser = getUser();
|
||||
|
||||
// 사용자 정보가 유효한지 확인 (토큰은 있지만 유저 정보가 깨졌을 경우)
|
||||
if (!currentUser || !currentUser.username) {
|
||||
console.error(' 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
|
||||
clearAuthData();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
const userRole = currentUser.role || currentUser.access_level || '사용자';
|
||||
|
||||
// 페이지 접근 권한 체크 (Admin은 건너뛰기)
|
||||
if (currentUser.role !== 'Admin' && currentUser.role !== 'System Admin') {
|
||||
const pageKey = getCurrentPageKey();
|
||||
|
||||
if (pageKey) {
|
||||
const hasAccess = await checkPageAccess(pageKey);
|
||||
|
||||
if (!hasAccess) {
|
||||
console.error(` 페이지 접근 권한이 없습니다: ${pageKey}`);
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
window.location.href = '/pages/dashboard.html';
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 역할 기반 메뉴 제어 로직은 각 컴포넌트 로더(load-navbar.js 등)로 이전함.
|
||||
// 전역 변수 할당(window.currentUser) 제거.
|
||||
})();
|
||||
@@ -1,80 +0,0 @@
|
||||
// /js/component-loader.js
|
||||
import { config } from './config.js';
|
||||
|
||||
// 캐시 버전 (컴포넌트 변경 시 증가)
|
||||
const CACHE_VERSION = 'v4';
|
||||
|
||||
/**
|
||||
* 컴포넌트 HTML을 캐시에서 가져오거나 fetch
|
||||
*/
|
||||
async function getComponentHtml(componentName, componentPath) {
|
||||
const cacheKey = `component_${componentName}_${CACHE_VERSION}`;
|
||||
|
||||
// 캐시에서 먼저 확인
|
||||
const cached = sessionStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 캐시 없으면 fetch
|
||||
const response = await fetch(componentPath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`컴포넌트 파일을 불러올 수 없습니다: ${response.statusText}`);
|
||||
}
|
||||
const htmlText = await response.text();
|
||||
|
||||
// 캐시에 저장
|
||||
try {
|
||||
sessionStorage.setItem(cacheKey, htmlText);
|
||||
} catch (e) {
|
||||
// sessionStorage 용량 초과 시 무시
|
||||
}
|
||||
|
||||
return htmlText;
|
||||
}
|
||||
|
||||
/**
|
||||
* 공용 HTML 컴포넌트를 페이지의 특정 위치에 동적으로 로드합니다.
|
||||
* @param {string} componentName - 로드할 컴포넌트의 이름 (e.g., 'sidebar', 'navbar'). config.js의 components 객체에 정의된 키와 일치해야 합니다.
|
||||
* @param {string} containerSelector - 컴포넌트가 삽입될 DOM 요소의 CSS 선택자 (e.g., '#sidebar-container').
|
||||
* @param {function(Document): void} [domProcessor=null] - DOM에 삽입하기 전에 로드된 HTML(Document)을 조작하는 선택적 함수.
|
||||
* (e.g., 역할 기반 메뉴 필터링)
|
||||
*/
|
||||
export async function loadComponent(componentName, containerSelector, domProcessor = null) {
|
||||
const container = document.querySelector(containerSelector);
|
||||
if (!container) {
|
||||
console.warn(` 컴포넌트를 삽입할 컨테이너를 찾을 수 없습니다: ${containerSelector} (선택사항일 수 있음)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const componentPath = config.components[componentName];
|
||||
if (!componentPath) {
|
||||
console.error(` 설정 파일(config.js)에서 '${componentName}' 컴포넌트의 경로를 찾을 수 없습니다.`);
|
||||
container.textContent = `${componentName} 로딩 실패`;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const htmlText = await getComponentHtml(componentName, componentPath);
|
||||
|
||||
if (domProcessor) {
|
||||
// 1. 텍스트를 가상 DOM으로 파싱
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 2. DOM 프로세서(콜백)를 실행하여 DOM 조작
|
||||
await domProcessor(doc);
|
||||
|
||||
// 3. 조작된 HTML을 실제 DOM에 삽입
|
||||
container.innerHTML = doc.body.innerHTML;
|
||||
} else {
|
||||
// DOM 조작이 필요 없는 경우, 바로 삽입
|
||||
container.innerHTML = htmlText;
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(` '${componentName}' 컴포넌트 로딩 실패:`, error);
|
||||
container.textContent = `${componentName} 로딩에 실패했습니다. 관리자에게 문의하세요.`;
|
||||
}
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
// /js/load-navbar.js
|
||||
import { getUser, clearAuthData } from './auth.js';
|
||||
import { loadComponent } from './component-loader.js';
|
||||
import { config } from './config.js';
|
||||
|
||||
// 역할 이름을 한글로 변환하는 맵
|
||||
const ROLE_NAMES = {
|
||||
'system admin': '시스템 관리자',
|
||||
'admin': '관리자',
|
||||
'system': '시스템 관리자',
|
||||
'leader': '그룹장',
|
||||
'user': '작업자',
|
||||
'support': '지원팀',
|
||||
'default': '사용자',
|
||||
};
|
||||
|
||||
/**
|
||||
* 네비게이션 바 DOM을 사용자 정보와 역할에 맞게 수정하는 프로세서입니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
*/
|
||||
async function processNavbarDom(doc) {
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) return;
|
||||
|
||||
// 1. 역할 및 페이지 권한 기반 메뉴 필터링
|
||||
await filterMenuByPageAccess(doc, currentUser);
|
||||
|
||||
// 2. 사용자 정보 채우기
|
||||
populateUserInfo(doc, currentUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한에 따라 메뉴 항목을 필터링합니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {object} currentUser - 현재 사용자 객체
|
||||
*/
|
||||
async function filterMenuByPageAccess(doc, currentUser) {
|
||||
const userRole = (currentUser.role || '').toLowerCase();
|
||||
|
||||
// Admin은 모든 메뉴 표시 + .admin-only 요소 활성화
|
||||
if (userRole === 'admin' || userRole === 'system admin') {
|
||||
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 사용자의 페이지 접근 권한 조회
|
||||
const cached = localStorage.getItem('userPageAccess');
|
||||
let accessiblePages = null;
|
||||
|
||||
if (cached) {
|
||||
const cacheData = JSON.parse(cached);
|
||||
// 캐시가 5분 이내인 경우 사용
|
||||
if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) {
|
||||
accessiblePages = cacheData.pages;
|
||||
}
|
||||
}
|
||||
|
||||
// 캐시가 없으면 API 호출
|
||||
if (!accessiblePages) {
|
||||
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('sso_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('페이지 권한 조회 실패:', response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
accessiblePages = data.data.pageAccess || [];
|
||||
|
||||
// 캐시 저장
|
||||
localStorage.setItem('userPageAccess', JSON.stringify({
|
||||
pages: accessiblePages,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
// 접근 가능한 페이지 키 목록
|
||||
const accessiblePageKeys = accessiblePages
|
||||
.filter(p => p.can_access === 1)
|
||||
.map(p => p.page_key);
|
||||
|
||||
// 메뉴 항목에 data-page-key 속성이 있으면 해당 권한 체크
|
||||
const menuItems = doc.querySelectorAll('[data-page-key]');
|
||||
menuItems.forEach(item => {
|
||||
const pageKey = item.getAttribute('data-page-key');
|
||||
|
||||
// 대시보드와 프로필 페이지는 모든 사용자 접근 가능
|
||||
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 권한이 없으면 메뉴 항목 제거
|
||||
if (!accessiblePageKeys.includes(pageKey)) {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Admin 전용 메뉴는 무조건 제거
|
||||
doc.querySelectorAll('.admin-only').forEach(el => el.remove());
|
||||
|
||||
} catch (error) {
|
||||
console.error('메뉴 필터링 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 바에 사용자 정보를 채웁니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {object} user - 현재 사용자 객체
|
||||
*/
|
||||
function populateUserInfo(doc, user) {
|
||||
const displayName = user.name || user.username;
|
||||
// 대소문자 구분 없이 처리
|
||||
const roleLower = (user.role || '').toLowerCase();
|
||||
const roleName = ROLE_NAMES[roleLower] || ROLE_NAMES.default;
|
||||
|
||||
const elements = {
|
||||
'userName': displayName,
|
||||
'userRole': roleName,
|
||||
'userInitial': displayName.charAt(0),
|
||||
};
|
||||
|
||||
for (const id in elements) {
|
||||
const el = doc.getElementById(id);
|
||||
if (el) el.textContent = elements[id];
|
||||
}
|
||||
|
||||
// 메인 대시보드 URL 설정
|
||||
const dashboardBtn = doc.getElementById('dashboardBtn');
|
||||
if (dashboardBtn) {
|
||||
dashboardBtn.href = '/pages/dashboard.html';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 바와 관련된 모든 이벤트를 설정합니다.
|
||||
*/
|
||||
function setupNavbarEvents() {
|
||||
const logoutButton = document.getElementById('logoutBtn');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
if (confirm('로그아웃 하시겠습니까?')) {
|
||||
clearAuthData();
|
||||
if (window.clearSSOAuth) window.clearSSOAuth();
|
||||
window.location.href = config.paths.loginPage + '?redirect=' + encodeURIComponent('/pages/dashboard.html');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 모바일 메뉴 버튼
|
||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||
const sidebar = document.getElementById('sidebarNav');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
|
||||
if (mobileMenuBtn && sidebar) {
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('mobile-open');
|
||||
overlay?.classList.toggle('show');
|
||||
document.body.classList.toggle('sidebar-mobile-open');
|
||||
});
|
||||
}
|
||||
|
||||
// 오버레이 클릭시 닫기
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', () => {
|
||||
sidebar?.classList.remove('mobile-open');
|
||||
overlay.classList.remove('show');
|
||||
document.body.classList.remove('sidebar-mobile-open');
|
||||
});
|
||||
}
|
||||
|
||||
// 사이드바 토글 버튼 (모바일에서 닫기)
|
||||
const sidebarToggle = document.getElementById('sidebarToggle');
|
||||
if (sidebarToggle && sidebar) {
|
||||
sidebarToggle.addEventListener('click', () => {
|
||||
if (window.innerWidth <= 1024) {
|
||||
sidebar.classList.remove('mobile-open');
|
||||
overlay?.classList.remove('show');
|
||||
document.body.classList.remove('sidebar-mobile-open');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 날짜와 시간을 업데이트하는 함수
|
||||
*/
|
||||
function updateDateTime() {
|
||||
const now = new Date();
|
||||
|
||||
// 시간 업데이트 (시 분 초 형식으로 고정)
|
||||
const timeElement = document.getElementById('timeValue');
|
||||
if (timeElement) {
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
timeElement.textContent = `${hours}시 ${minutes}분 ${seconds}초`;
|
||||
}
|
||||
|
||||
// 날짜 업데이트
|
||||
const dateElement = document.getElementById('dateValue');
|
||||
if (dateElement) {
|
||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const month = now.getMonth() + 1;
|
||||
const date = now.getDate();
|
||||
const day = days[now.getDay()];
|
||||
dateElement.textContent = `${month}월 ${date}일 (${day})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 날씨 아이콘 매핑
|
||||
const WEATHER_ICONS = {
|
||||
clear: '☀️',
|
||||
rain: '🌧️',
|
||||
snow: '❄️',
|
||||
heat: '🔥',
|
||||
cold: '🥶',
|
||||
wind: '💨',
|
||||
fog: '🌫️',
|
||||
dust: '😷',
|
||||
cloudy: '⛅',
|
||||
overcast: '☁️'
|
||||
};
|
||||
|
||||
// 날씨 조건명
|
||||
const WEATHER_NAMES = {
|
||||
clear: '맑음',
|
||||
rain: '비',
|
||||
snow: '눈',
|
||||
heat: '폭염',
|
||||
cold: '한파',
|
||||
wind: '강풍',
|
||||
fog: '안개',
|
||||
dust: '미세먼지',
|
||||
cloudy: '구름많음',
|
||||
overcast: '흐림'
|
||||
};
|
||||
|
||||
/**
|
||||
* 날씨 정보를 가져와서 업데이트하는 함수
|
||||
*/
|
||||
async function updateWeather() {
|
||||
try {
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return;
|
||||
|
||||
const response = await fetch(`${window.API_BASE_URL}/tbm/weather/current`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('날씨 API 호출 실패');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
const { temperature, conditions, weatherData } = result.data;
|
||||
|
||||
// 온도 표시
|
||||
const tempElement = document.getElementById('weatherTemp');
|
||||
if (tempElement && temperature !== null && temperature !== undefined) {
|
||||
tempElement.textContent = `${Math.round(temperature)}°C`;
|
||||
}
|
||||
|
||||
// 날씨 아이콘 및 설명
|
||||
const iconElement = document.getElementById('weatherIcon');
|
||||
const descElement = document.getElementById('weatherDesc');
|
||||
|
||||
if (conditions && conditions.length > 0) {
|
||||
const primaryCondition = conditions[0];
|
||||
if (iconElement) {
|
||||
iconElement.textContent = WEATHER_ICONS[primaryCondition] || '🌤️';
|
||||
}
|
||||
if (descElement) {
|
||||
descElement.textContent = WEATHER_NAMES[primaryCondition] || '맑음';
|
||||
}
|
||||
} else {
|
||||
if (iconElement) iconElement.textContent = '☀️';
|
||||
if (descElement) descElement.textContent = '맑음';
|
||||
}
|
||||
|
||||
// 날씨 섹션 표시
|
||||
const weatherSection = document.getElementById('weatherSection');
|
||||
if (weatherSection) {
|
||||
weatherSection.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('날씨 정보 로드 실패:', error.message);
|
||||
// 실패해도 기본값 표시
|
||||
const descElement = document.getElementById('weatherDesc');
|
||||
if (descElement) {
|
||||
descElement.textContent = '날씨 정보 없음';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 알림 시스템
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* 알림 관련 이벤트 설정
|
||||
*/
|
||||
function setupNotificationEvents() {
|
||||
const notificationBtn = document.getElementById('notificationBtn');
|
||||
const notificationDropdown = document.getElementById('notificationDropdown');
|
||||
const notificationWrapper = document.getElementById('notificationWrapper');
|
||||
|
||||
if (notificationBtn) {
|
||||
notificationBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
notificationDropdown?.classList.toggle('show');
|
||||
});
|
||||
}
|
||||
|
||||
// 외부 클릭시 드롭다운 닫기
|
||||
document.addEventListener('click', (e) => {
|
||||
if (notificationWrapper && notificationDropdown && !notificationWrapper.contains(e.target)) {
|
||||
notificationDropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 목록 로드
|
||||
*/
|
||||
async function loadNotifications() {
|
||||
try {
|
||||
const token = localStorage.getItem('sso_token');
|
||||
if (!token) return;
|
||||
|
||||
const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const notifications = result.data || [];
|
||||
updateNotificationBadge(notifications.length);
|
||||
renderNotificationList(notifications);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('알림 로드 오류:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배지 업데이트
|
||||
*/
|
||||
function updateNotificationBadge(count) {
|
||||
const badge = document.getElementById('notificationBadge');
|
||||
const btn = document.getElementById('notificationBtn');
|
||||
if (!badge) return;
|
||||
|
||||
if (count > 0) {
|
||||
badge.textContent = count > 99 ? '99+' : count;
|
||||
badge.style.display = 'flex';
|
||||
btn?.classList.add('has-notifications');
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
btn?.classList.remove('has-notifications');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 목록 렌더링
|
||||
*/
|
||||
function renderNotificationList(notifications) {
|
||||
const list = document.getElementById('notificationList');
|
||||
if (!list) return;
|
||||
|
||||
if (notifications.length === 0) {
|
||||
list.innerHTML = '<div class="notification-empty">새 알림이 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const NOTIF_ICONS = {
|
||||
repair: '🔧',
|
||||
safety: '⚠️',
|
||||
system: '📢',
|
||||
equipment: '🔩',
|
||||
maintenance: '🛠️'
|
||||
};
|
||||
|
||||
list.innerHTML = notifications.slice(0, 5).map(n => `
|
||||
<div class="notification-item ${n.is_read ? '' : 'unread'}" data-id="${n.notification_id}" data-url="${n.link_url || ''}">
|
||||
<div class="notification-item-icon ${n.type || 'repair'}">
|
||||
${NOTIF_ICONS[n.type] || '🔔'}
|
||||
</div>
|
||||
<div class="notification-item-content">
|
||||
<div class="notification-item-title">${escapeHtml(n.title)}</div>
|
||||
<div class="notification-item-desc">${escapeHtml(n.message || '')}</div>
|
||||
</div>
|
||||
<div class="notification-item-time">${formatTimeAgo(n.created_at)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 클릭 이벤트 추가
|
||||
list.querySelectorAll('.notification-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const linkUrl = item.dataset.url;
|
||||
// 수리 알림은 클릭해도 읽음 처리 안함 (수리 처리 페이지에서 확인 처리)
|
||||
window.location.href = linkUrl || '/pages/admin/notifications.html';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 시간 포맷팅
|
||||
*/
|
||||
function formatTimeAgo(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return '방금 전';
|
||||
if (diffMins < 60) return `${diffMins}분 전`;
|
||||
if (diffHours < 24) return `${diffHours}시간 전`;
|
||||
if (diffDays < 7) return `${diffDays}일 전`;
|
||||
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
|
||||
|
||||
// 메인 로직: DOMContentLoaded 시 실행
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (getUser()) {
|
||||
// 1. 컴포넌트 로드 및 DOM 수정
|
||||
await loadComponent('navbar', '#navbar-container', processNavbarDom);
|
||||
|
||||
// 2. DOM에 삽입된 후에 이벤트 리스너 설정
|
||||
setupNavbarEvents();
|
||||
|
||||
// 3. 실시간 날짜/시간 업데이트 시작
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 1000);
|
||||
|
||||
// 4. 날씨 정보 로드 (10분마다 갱신)
|
||||
updateWeather();
|
||||
setInterval(updateWeather, 10 * 60 * 1000);
|
||||
|
||||
// 5. 알림 이벤트 설정 및 로드 (30초마다 갱신)
|
||||
setupNotificationEvents();
|
||||
loadNotifications();
|
||||
setInterval(loadNotifications, 30000);
|
||||
}
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
// /js/load-sections.js
|
||||
import { getUser } from './auth.js';
|
||||
import { apiGet } from './api-helper.js';
|
||||
|
||||
// 역할에 따라 불러올 섹션 HTML 파일을 매핑합니다.
|
||||
const SECTION_MAP = {
|
||||
admin: '/components/sections/admin-sections.html',
|
||||
system: '/components/sections/admin-sections.html', // system도 admin과 동일한 섹션을 사용
|
||||
leader: '/components/sections/leader-sections.html',
|
||||
user: '/components/sections/user-sections.html',
|
||||
default: '/components/sections/user-sections.html', // 역할이 없는 경우 기본값
|
||||
};
|
||||
|
||||
/**
|
||||
* API를 통해 대시보드 통계 데이터를 가져옵니다.
|
||||
* @returns {Promise<object|null>} 통계 데이터 또는 에러 시 null
|
||||
*/
|
||||
async function fetchDashboardStats() {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
// 실제 백엔드 엔드포인트는 /api/dashboard/stats 와 같은 형태로 구현될 수 있습니다.
|
||||
const stats = await apiGet(`/workreports?start=${today}&end=${today}`);
|
||||
// 필요한 데이터 형태로 가공 (예시)
|
||||
return {
|
||||
today_reports_count: stats.length,
|
||||
today_workers_count: new Set(stats.map(d => d.user_id)).size,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('대시보드 통계 데이터 로드 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 가상 DOM에 통계 데이터를 채워 넣습니다.
|
||||
* @param {Document} doc - 파싱된 HTML 문서 객체
|
||||
* @param {object} stats - 통계 데이터
|
||||
*/
|
||||
function populateStatsData(doc, stats) {
|
||||
if (!stats) return;
|
||||
|
||||
const todayStatsEl = doc.getElementById('today-stats');
|
||||
if (todayStatsEl) {
|
||||
todayStatsEl.innerHTML = `
|
||||
<p>📝 오늘 등록된 작업: ${stats.today_reports_count}건</p>
|
||||
<p>👥 참여 작업자: ${stats.today_workers_count}명</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 로직: 페이지에 역할별 섹션을 로드하고 내용을 채웁니다.
|
||||
*/
|
||||
async function initializeSections() {
|
||||
const mainContainer = document.querySelector('main[id$="-sections"]');
|
||||
if (!mainContainer) {
|
||||
console.error('섹션을 담을 메인 컨테이너를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
mainContainer.innerHTML = '<div class="loading">콘텐츠를 불러오는 중...</div>';
|
||||
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) {
|
||||
mainContainer.innerHTML = '<div class="error-state">사용자 정보를 찾을 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionFile = SECTION_MAP[currentUser.role] || SECTION_MAP.default;
|
||||
|
||||
try {
|
||||
// 1. 역할에 맞는 HTML 템플릿과 동적 데이터를 동시에 로드 (Promise.all 활용)
|
||||
const [htmlResponse, statsData] = await Promise.all([
|
||||
fetch(sectionFile),
|
||||
fetchDashboardStats()
|
||||
]);
|
||||
|
||||
if (!htmlResponse.ok) {
|
||||
throw new Error(`섹션 파일(${sectionFile})을 불러오는 데 실패했습니다.`);
|
||||
}
|
||||
const htmlText = await htmlResponse.text();
|
||||
|
||||
// 2. 텍스트를 가상 DOM으로 파싱
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 3. (필요 시) 역할 기반으로 가상 DOM 필터링 - 현재는 파일 자체가 역할별로 나뉘어 불필요
|
||||
// filterByRole(doc, currentUser.role);
|
||||
|
||||
// 4. 가상 DOM에 동적 데이터 채우기
|
||||
populateStatsData(doc, statsData);
|
||||
|
||||
// 5. 모든 수정이 완료된 HTML을 실제 DOM에 한 번에 삽입
|
||||
mainContainer.innerHTML = doc.body.innerHTML;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('섹션 로딩 중 오류 발생:', error);
|
||||
mainContainer.textContent = '콘텐츠 로딩에 실패했습니다. 페이지를 새로고침해 주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// DOM이 로드되면 섹션 초기화를 시작합니다.
|
||||
document.addEventListener('DOMContentLoaded', initializeSections);
|
||||
@@ -1,209 +0,0 @@
|
||||
// /js/load-sidebar.js
|
||||
// 사이드바 네비게이션 로더 및 컨트롤러
|
||||
|
||||
import { getUser } from './auth.js';
|
||||
import { loadComponent } from './component-loader.js';
|
||||
|
||||
/**
|
||||
* 사이드바 DOM을 사용자 권한에 맞게 처리
|
||||
*/
|
||||
async function processSidebarDom(doc) {
|
||||
const currentUser = getUser();
|
||||
if (!currentUser) return;
|
||||
|
||||
const userRole = (currentUser.role || '').toLowerCase();
|
||||
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
||||
// role 또는 access_level로 관리자 확인
|
||||
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
||||
accessLevel === 'admin' || accessLevel === 'system';
|
||||
|
||||
// 1. 관리자 전용 메뉴 표시/숨김
|
||||
if (isAdmin) {
|
||||
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
|
||||
} else {
|
||||
// 비관리자: 페이지 접근 권한에 따라 메뉴 필터링
|
||||
await filterMenuByPageAccess(doc, currentUser);
|
||||
}
|
||||
|
||||
// 2. 현재 페이지 활성화
|
||||
highlightCurrentPage(doc);
|
||||
|
||||
// 3. 저장된 상태 복원
|
||||
restoreSidebarState(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한에 따라 메뉴 필터링
|
||||
*/
|
||||
async function filterMenuByPageAccess(doc, currentUser) {
|
||||
try {
|
||||
const cached = localStorage.getItem('userPageAccess');
|
||||
let accessiblePages = null;
|
||||
|
||||
if (cached) {
|
||||
const cacheData = JSON.parse(cached);
|
||||
if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) {
|
||||
accessiblePages = cacheData.pages;
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessiblePages) {
|
||||
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('sso_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
accessiblePages = data.data.pageAccess || [];
|
||||
|
||||
localStorage.setItem('userPageAccess', JSON.stringify({
|
||||
pages: accessiblePages,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
const accessiblePageKeys = accessiblePages
|
||||
.filter(p => p.can_access === 1)
|
||||
.map(p => p.page_key);
|
||||
|
||||
// 메뉴 항목 필터링
|
||||
const menuItems = doc.querySelectorAll('[data-page-key]');
|
||||
menuItems.forEach(item => {
|
||||
const pageKey = item.getAttribute('data-page-key');
|
||||
|
||||
// 대시보드와 프로필은 항상 표시
|
||||
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 권한 없으면 숨김
|
||||
if (!accessiblePageKeys.includes(pageKey)) {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 관리자 전용 카테고리 제거
|
||||
doc.querySelectorAll('.nav-category.admin-only').forEach(el => el.remove());
|
||||
|
||||
} catch (error) {
|
||||
console.error('사이드바 메뉴 필터링 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 페이지 하이라이트
|
||||
*/
|
||||
function highlightCurrentPage(doc) {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
doc.querySelectorAll('.nav-item').forEach(item => {
|
||||
const href = item.getAttribute('href');
|
||||
if (href && currentPath.includes(href.replace(/^\//, ''))) {
|
||||
item.classList.add('active');
|
||||
|
||||
// 부모 카테고리 열기
|
||||
const category = item.closest('.nav-category');
|
||||
if (category) {
|
||||
category.classList.add('expanded');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이드바 상태 복원 (기본값: 접힌 상태)
|
||||
*/
|
||||
function restoreSidebarState(doc) {
|
||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
||||
const sidebar = doc.querySelector('.sidebar-nav');
|
||||
|
||||
if (isCollapsed && sidebar) {
|
||||
sidebar.classList.add('collapsed');
|
||||
document.body.classList.add('sidebar-collapsed');
|
||||
}
|
||||
|
||||
// 확장된 카테고리 복원
|
||||
const expandedCategories = JSON.parse(localStorage.getItem('sidebarExpanded') || '[]');
|
||||
expandedCategories.forEach(category => {
|
||||
const el = doc.querySelector(`[data-category="${category}"]`);
|
||||
if (el) el.classList.add('expanded');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이드바 이벤트 설정
|
||||
*/
|
||||
function setupSidebarEvents() {
|
||||
const sidebar = document.getElementById('sidebarNav');
|
||||
const toggle = document.getElementById('sidebarToggle');
|
||||
|
||||
if (!sidebar || !toggle) return;
|
||||
|
||||
// 토글 버튼 클릭
|
||||
toggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
document.body.classList.toggle('sidebar-collapsed');
|
||||
|
||||
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
|
||||
});
|
||||
|
||||
// 카테고리 헤더 클릭
|
||||
sidebar.querySelectorAll('.nav-category-header').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const category = header.closest('.nav-category');
|
||||
category.classList.toggle('expanded');
|
||||
|
||||
// 상태 저장
|
||||
const expanded = [];
|
||||
sidebar.querySelectorAll('.nav-category.expanded').forEach(cat => {
|
||||
const categoryName = cat.getAttribute('data-category');
|
||||
if (categoryName) expanded.push(categoryName);
|
||||
});
|
||||
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded));
|
||||
});
|
||||
});
|
||||
|
||||
// 링크 프리페치 - 마우스 올리면 미리 로드
|
||||
const prefetchedUrls = new Set();
|
||||
sidebar.querySelectorAll('a.nav-item').forEach(link => {
|
||||
link.addEventListener('mouseenter', () => {
|
||||
const href = link.getAttribute('href');
|
||||
if (href && !prefetchedUrls.has(href) && !href.startsWith('#')) {
|
||||
prefetchedUrls.add(href);
|
||||
const prefetchLink = document.createElement('link');
|
||||
prefetchLink.rel = 'prefetch';
|
||||
prefetchLink.href = href;
|
||||
document.head.appendChild(prefetchLink);
|
||||
}
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사이드바 초기화
|
||||
*/
|
||||
async function initSidebar() {
|
||||
// 사이드바 컨테이너가 없으면 생성
|
||||
let container = document.getElementById('sidebar-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'sidebar-container';
|
||||
document.body.prepend(container);
|
||||
}
|
||||
|
||||
if (getUser()) {
|
||||
await loadComponent('sidebar-nav', '#sidebar-container', processSidebarDom);
|
||||
document.body.classList.add('has-sidebar');
|
||||
setupSidebarEvents();
|
||||
}
|
||||
}
|
||||
|
||||
// DOMContentLoaded 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', initSidebar);
|
||||
|
||||
export { initSidebar };
|
||||
@@ -1,496 +0,0 @@
|
||||
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
|
||||
|
||||
// 인증 확인
|
||||
const token = ensureAuthenticated();
|
||||
|
||||
const accessLabels = {
|
||||
worker: '작업자',
|
||||
group_leader: '그룹장',
|
||||
support_team: '지원팀',
|
||||
admin: '관리자',
|
||||
system: '시스템'
|
||||
};
|
||||
|
||||
// 현재 사용자 정보 가져오기
|
||||
const currentUser = JSON.parse(localStorage.getItem('sso_user') || '{}');
|
||||
const isSystemUser = currentUser.access_level === 'system';
|
||||
|
||||
function createRow(item, cols, delHandler) {
|
||||
const tr = document.createElement('tr');
|
||||
cols.forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = item[key] || '-';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.textContent = '삭제';
|
||||
delBtn.className = 'btn-delete';
|
||||
delBtn.onclick = () => delHandler(item);
|
||||
const td = document.createElement('td');
|
||||
td.appendChild(delBtn);
|
||||
tr.appendChild(td);
|
||||
return tr;
|
||||
}
|
||||
|
||||
// 내 비밀번호 변경
|
||||
const myPasswordForm = document.getElementById('myPasswordForm');
|
||||
myPasswordForm?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const currentPassword = document.getElementById('currentPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
// 새 비밀번호 확인
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('❌ 새 비밀번호가 일치하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 비밀번호 강도 검사
|
||||
if (newPassword.length < 6) {
|
||||
alert('❌ 비밀번호는 최소 6자 이상이어야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/auth/change-password`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
currentPassword,
|
||||
newPassword
|
||||
})
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok && result.success) {
|
||||
showToast('✅ 비밀번호가 변경되었습니다.');
|
||||
myPasswordForm.reset();
|
||||
|
||||
// 3초 후 로그인 페이지로 이동
|
||||
setTimeout(() => {
|
||||
alert('비밀번호가 변경되어 다시 로그인해주세요.');
|
||||
if (window.clearSSOAuth) window.clearSSOAuth();
|
||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('❌ 비밀번호 변경 실패: ' + (result.error || '현재 비밀번호가 올바르지 않습니다.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Password change error:', error);
|
||||
alert('🚨 서버 오류: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// 시스템 권한자만 볼 수 있는 사용자 비밀번호 변경 섹션
|
||||
if (isSystemUser) {
|
||||
const systemCard = document.getElementById('systemPasswordChangeCard');
|
||||
if (systemCard) {
|
||||
systemCard.style.display = 'block';
|
||||
}
|
||||
|
||||
// 사용자 비밀번호 변경 (시스템 권한자)
|
||||
const userPasswordForm = document.getElementById('userPasswordForm');
|
||||
userPasswordForm?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const targetUserId = document.getElementById('targetUserId').value;
|
||||
const newPassword = document.getElementById('targetNewPassword').value;
|
||||
|
||||
if (!targetUserId) {
|
||||
alert('❌ 사용자를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
alert('❌ 비밀번호는 최소 6자 이상이어야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('정말로 이 사용자의 비밀번호를 변경하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/auth/admin/change-password`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
userId: targetUserId,
|
||||
newPassword
|
||||
})
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (res.ok && result.success) {
|
||||
showToast('✅ 사용자 비밀번호가 변경되었습니다.');
|
||||
userPasswordForm.reset();
|
||||
} else {
|
||||
alert('❌ 비밀번호 변경 실패: ' + (result.error || '권한이 없습니다.'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Admin password change error:', error);
|
||||
alert('🚨 서버 오류: ' + error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 등록
|
||||
const userForm = document.getElementById('userForm');
|
||||
userForm?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
username: document.getElementById('username').value.trim(),
|
||||
password: document.getElementById('password').value.trim(),
|
||||
name: document.getElementById('name').value.trim(),
|
||||
access_level: document.getElementById('access_level').value,
|
||||
user_id: document.getElementById('user_id').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.success) {
|
||||
showToast('✅ 등록 완료');
|
||||
userForm.reset();
|
||||
loadUsers();
|
||||
} else {
|
||||
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
alert('🚨 서버 오류: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadUsers() {
|
||||
const tbody = document.getElementById('userTableBody');
|
||||
tbody.innerHTML = '<tr><td colspan="6">불러오는 중...</td></tr>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/auth/users`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
const list = await res.json();
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (Array.isArray(list)) {
|
||||
// 시스템 권한자용 사용자 선택 옵션도 업데이트
|
||||
if (isSystemUser) {
|
||||
const targetUserSelect = document.getElementById('targetUserId');
|
||||
if (targetUserSelect) {
|
||||
targetUserSelect.innerHTML = '<option value="">사용자 선택</option>';
|
||||
list.forEach(user => {
|
||||
// 본인은 제외
|
||||
if (user.user_id !== currentUser.user_id) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = user.user_id;
|
||||
opt.textContent = `${user.name} (${user.username})`;
|
||||
targetUserSelect.appendChild(opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
list.forEach(item => {
|
||||
item.access_level = accessLabels[item.access_level] || item.access_level;
|
||||
item.user_id = item.user_id || '-';
|
||||
|
||||
// 행 생성
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// 데이터 컬럼
|
||||
['user_id', 'username', 'name', 'access_level', 'user_id'].forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = item[key] || '-';
|
||||
tr.appendChild(td);
|
||||
});
|
||||
|
||||
// 작업 컬럼 (페이지 권한 버튼 + 삭제 버튼)
|
||||
const actionTd = document.createElement('td');
|
||||
|
||||
// 페이지 권한 버튼 (Admin/System이 아닌 경우에만)
|
||||
if (item.access_level !== '관리자' && item.access_level !== '시스템') {
|
||||
const pageAccessBtn = document.createElement('button');
|
||||
pageAccessBtn.textContent = '페이지 권한';
|
||||
pageAccessBtn.className = 'btn btn-info btn-sm';
|
||||
pageAccessBtn.style.marginRight = '5px';
|
||||
pageAccessBtn.onclick = () => openPageAccessModal(item.user_id, item.username, item.name);
|
||||
actionTd.appendChild(pageAccessBtn);
|
||||
}
|
||||
|
||||
// 삭제 버튼
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.textContent = '삭제';
|
||||
delBtn.className = 'btn-delete';
|
||||
delBtn.onclick = async () => {
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const delRes = await fetch(`${API}/auth/users/${item.user_id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (delRes.ok) {
|
||||
showToast('✅ 삭제 완료');
|
||||
loadUsers();
|
||||
} else {
|
||||
alert('❌ 삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('🚨 삭제 중 오류 발생');
|
||||
}
|
||||
};
|
||||
actionTd.appendChild(delBtn);
|
||||
|
||||
tr.appendChild(actionTd);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="6">데이터 형식 오류</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load users error:', error);
|
||||
tbody.innerHTML = '<tr><td colspan="6">로드 실패: ' + error.message + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkerOptions() {
|
||||
const select = document.getElementById('user_id');
|
||||
if (!select) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/workers`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
const allWorkers = await res.json();
|
||||
|
||||
// 활성화된 작업자만 필터링
|
||||
const workers = allWorkers.filter(worker => {
|
||||
return worker.status === 'active' || worker.is_active === 1 || worker.is_active === true;
|
||||
});
|
||||
|
||||
if (Array.isArray(workers)) {
|
||||
workers.forEach(w => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = w.user_id;
|
||||
opt.textContent = `${w.worker_name} (${w.user_id})`;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('작업자 목록 불러오기 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// showToast → api-base.js 전역 사용
|
||||
|
||||
// ========== 페이지 접근 권한 관리 ==========
|
||||
|
||||
let currentEditingUserId = null;
|
||||
let currentUserPageAccess = [];
|
||||
|
||||
/**
|
||||
* 페이지 권한 관리 모달 열기
|
||||
*/
|
||||
async function openPageAccessModal(userId, username, name) {
|
||||
currentEditingUserId = userId;
|
||||
|
||||
const modal = document.getElementById('pageAccessModal');
|
||||
const modalUserInfo = document.getElementById('modalUserInfo');
|
||||
const modalUserRole = document.getElementById('modalUserRole');
|
||||
|
||||
modalUserInfo.textContent = `${name} (${username})`;
|
||||
modalUserRole.textContent = `사용자 ID: ${userId}`;
|
||||
|
||||
try {
|
||||
// 사용자의 페이지 접근 권한 조회
|
||||
const res = await fetch(`${API}/users/${userId}/page-access`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('페이지 접근 권한을 불러오는데 실패했습니다.');
|
||||
}
|
||||
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
currentUserPageAccess = result.data.pageAccess;
|
||||
renderPageAccessList(result.data.pageAccess);
|
||||
modal.style.display = 'block';
|
||||
} else {
|
||||
throw new Error(result.error || '데이터 로드 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 로드 오류:', error);
|
||||
alert('❌ 페이지 권한을 불러오는데 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 접근 권한 목록 렌더링
|
||||
*/
|
||||
function renderPageAccessList(pageAccess) {
|
||||
const categories = {
|
||||
dashboard: document.getElementById('dashboardPageList'),
|
||||
management: document.getElementById('managementPageList'),
|
||||
common: document.getElementById('commonPageList')
|
||||
};
|
||||
|
||||
// 카테고리별로 초기화
|
||||
Object.values(categories).forEach(el => {
|
||||
if (el) el.innerHTML = '';
|
||||
});
|
||||
|
||||
// 카테고리별로 그룹화
|
||||
const grouped = pageAccess.reduce((acc, page) => {
|
||||
if (!acc[page.category]) acc[page.category] = [];
|
||||
acc[page.category].push(page);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 각 카테고리별로 렌더링
|
||||
Object.keys(grouped).forEach(category => {
|
||||
const container = categories[category];
|
||||
if (!container) return;
|
||||
|
||||
grouped[category].forEach(page => {
|
||||
const pageItem = document.createElement('div');
|
||||
pageItem.className = 'page-item';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.id = `page_${page.page_id}`;
|
||||
checkbox.checked = page.can_access === 1 || page.can_access === true;
|
||||
checkbox.dataset.pageId = page.page_id;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `page_${page.page_id}`;
|
||||
label.textContent = page.page_name;
|
||||
|
||||
const pathSpan = document.createElement('span');
|
||||
pathSpan.className = 'page-path';
|
||||
pathSpan.textContent = page.page_path;
|
||||
|
||||
pageItem.appendChild(checkbox);
|
||||
pageItem.appendChild(label);
|
||||
pageItem.appendChild(pathSpan);
|
||||
|
||||
container.appendChild(pageItem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 권한 변경 사항 저장
|
||||
*/
|
||||
async function savePageAccessChanges() {
|
||||
if (!currentEditingUserId) {
|
||||
alert('사용자 정보가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 체크박스 상태 가져오기
|
||||
const checkboxes = document.querySelectorAll('.page-item input[type="checkbox"]');
|
||||
const pageAccessUpdates = {};
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
const pageId = parseInt(checkbox.dataset.pageId);
|
||||
const canAccess = checkbox.checked;
|
||||
pageAccessUpdates[pageId] = canAccess;
|
||||
});
|
||||
|
||||
try {
|
||||
// 변경된 페이지 권한을 서버로 전송
|
||||
const pageIds = Object.keys(pageAccessUpdates).map(id => parseInt(id));
|
||||
const canAccessValues = pageIds.map(id => pageAccessUpdates[id]);
|
||||
|
||||
// 접근 가능한 페이지
|
||||
const accessiblePages = pageIds.filter((id, index) => canAccessValues[index]);
|
||||
// 접근 불가능한 페이지
|
||||
const inaccessiblePages = pageIds.filter((id, index) => !canAccessValues[index]);
|
||||
|
||||
// 접근 가능 페이지 업데이트
|
||||
if (accessiblePages.length > 0) {
|
||||
await fetch(`${API}/users/${currentEditingUserId}/page-access`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
pageIds: accessiblePages,
|
||||
canAccess: true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// 접근 불가능 페이지 업데이트
|
||||
if (inaccessiblePages.length > 0) {
|
||||
await fetch(`${API}/users/${currentEditingUserId}/page-access`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
pageIds: inaccessiblePages,
|
||||
canAccess: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
showToast('✅ 페이지 접근 권한이 저장되었습니다.');
|
||||
closePageAccessModal();
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 저장 오류:', error);
|
||||
alert('❌ 페이지 권한 저장에 실패했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 권한 관리 모달 닫기
|
||||
*/
|
||||
function closePageAccessModal() {
|
||||
const modal = document.getElementById('pageAccessModal');
|
||||
modal.style.display = 'none';
|
||||
currentEditingUserId = null;
|
||||
currentUserPageAccess = [];
|
||||
}
|
||||
|
||||
// 모달 닫기 버튼 이벤트
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const modal = document.getElementById('pageAccessModal');
|
||||
const closeBtn = modal?.querySelector('.close');
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = closePageAccessModal;
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
window.onclick = (event) => {
|
||||
if (event.target === modal) {
|
||||
closePageAccessModal();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 전역 함수로 노출
|
||||
window.openPageAccessModal = openPageAccessModal;
|
||||
window.closePageAccessModal = closePageAccessModal;
|
||||
window.savePageAccessChanges = savePageAccessChanges;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
loadUsers();
|
||||
loadWorkerOptions();
|
||||
});
|
||||
@@ -12,7 +12,6 @@
|
||||
var categories = [];
|
||||
var allWorkplaces = [];
|
||||
var tbmByWorkplace = {};
|
||||
var visitorsByWorkplace = {};
|
||||
var movedByWorkplace = {};
|
||||
var issuesByWorkplace = {};
|
||||
var workplacesByCategory = {};
|
||||
@@ -37,24 +36,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
function groupVisitorsByWorkplace(requests) {
|
||||
visitorsByWorkplace = {};
|
||||
if (!Array.isArray(requests)) return;
|
||||
requests.forEach(function(r) {
|
||||
// 오늘 날짜 + 승인된 건만
|
||||
if (r.visit_date !== today) return;
|
||||
if (r.status !== 'approved') return;
|
||||
var wpId = r.workplace_id;
|
||||
if (!wpId) return;
|
||||
if (!visitorsByWorkplace[wpId]) {
|
||||
visitorsByWorkplace[wpId] = { visitCount: 0, totalVisitors: 0, requests: [] };
|
||||
}
|
||||
visitorsByWorkplace[wpId].visitCount++;
|
||||
visitorsByWorkplace[wpId].totalVisitors += parseInt(r.visitor_count) || 0;
|
||||
visitorsByWorkplace[wpId].requests.push(r);
|
||||
});
|
||||
}
|
||||
|
||||
function groupMovedByWorkplace(items) {
|
||||
movedByWorkplace = {};
|
||||
if (!Array.isArray(items)) return;
|
||||
@@ -158,11 +139,10 @@
|
||||
workplaces.forEach(function(wp) {
|
||||
var wpId = wp.workplace_id;
|
||||
var tbm = tbmByWorkplace[wpId];
|
||||
var visitors = visitorsByWorkplace[wpId];
|
||||
var moved = movedByWorkplace[wpId];
|
||||
var issues = issuesByWorkplace[wpId];
|
||||
|
||||
var hasAny = tbm || visitors || moved || issues;
|
||||
var hasAny = tbm || moved || issues;
|
||||
|
||||
html += '<div class="md-wp-card" data-wp-id="' + wpId + '">';
|
||||
|
||||
@@ -187,14 +167,6 @@
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// 방문
|
||||
if (visitors) {
|
||||
html += '<div class="md-wp-stat-row">' +
|
||||
'<span class="md-wp-stat-icon">🚪</span>' +
|
||||
'<span class="md-wp-stat-text">방문 ' + visitors.visitCount + '건 · ' + visitors.totalVisitors + '명</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// 신고 (미완료만)
|
||||
if (issues && issues.activeCount > 0) {
|
||||
html += '<div class="md-wp-stat-row md-wp-stat--warning">' +
|
||||
@@ -230,7 +202,7 @@
|
||||
var cards = container.querySelectorAll('.md-wp-card[data-wp-id]');
|
||||
cards.forEach(function(card) {
|
||||
var wpId = card.getAttribute('data-wp-id');
|
||||
var hasActivity = tbmByWorkplace[wpId] || visitorsByWorkplace[wpId] ||
|
||||
var hasActivity = tbmByWorkplace[wpId] ||
|
||||
movedByWorkplace[wpId] || issuesByWorkplace[wpId];
|
||||
if (!hasActivity) return;
|
||||
card.querySelector('.md-wp-header').addEventListener('click', function() {
|
||||
@@ -262,7 +234,6 @@
|
||||
function renderCardDetail(wpId) {
|
||||
var html = '';
|
||||
var tbm = tbmByWorkplace[wpId];
|
||||
var visitors = visitorsByWorkplace[wpId];
|
||||
var issues = issuesByWorkplace[wpId];
|
||||
var moved = movedByWorkplace[wpId];
|
||||
|
||||
@@ -282,23 +253,6 @@
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// 방문
|
||||
if (visitors && visitors.requests.length > 0) {
|
||||
html += '<div class="md-wp-detail-section">';
|
||||
html += '<div class="md-wp-detail-title">▶ 방문</div>';
|
||||
visitors.requests.forEach(function(r) {
|
||||
var company = r.visitor_company || '업체 미지정';
|
||||
var count = parseInt(r.visitor_count) || 0;
|
||||
var purpose = r.purpose_name || '';
|
||||
html += '<div class="md-wp-detail-item">';
|
||||
html += '<div class="md-wp-detail-main">' + escapeHtml(company) + ' · ' + count + '명';
|
||||
if (purpose) html += ' · ' + escapeHtml(purpose);
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// 신고
|
||||
if (issues && issues.items.length > 0) {
|
||||
var statusMap = { reported: '신고', received: '접수', in_progress: '처리중' };
|
||||
@@ -380,7 +334,6 @@
|
||||
var results = await Promise.allSettled([
|
||||
window.apiCall('/workplaces/categories'),
|
||||
window.apiCall('/tbm/sessions/date/' + today),
|
||||
window.apiCall('/workplace-visits/requests?visit_date=' + today + '&status=approved'),
|
||||
window.apiCall('/equipments/moved/list'),
|
||||
window.apiCall('/work-issues?start_date=' + today + '&end_date=' + today),
|
||||
window.apiCall('/workplaces')
|
||||
@@ -396,24 +349,19 @@
|
||||
groupTbmByWorkplace(results[1].value.data || []);
|
||||
}
|
||||
|
||||
// 방문
|
||||
if (results[2].status === 'fulfilled' && results[2].value && results[2].value.success) {
|
||||
groupVisitorsByWorkplace(results[2].value.data || []);
|
||||
}
|
||||
|
||||
// 이동설비
|
||||
if (results[3].status === 'fulfilled' && results[3].value && results[3].value.success) {
|
||||
groupMovedByWorkplace(results[3].value.data || []);
|
||||
if (results[2].status === 'fulfilled' && results[2].value && results[2].value.success) {
|
||||
groupMovedByWorkplace(results[2].value.data || []);
|
||||
}
|
||||
|
||||
// 신고
|
||||
if (results[4].status === 'fulfilled' && results[4].value && results[4].value.success) {
|
||||
groupIssuesByWorkplace(results[4].value.data || []);
|
||||
if (results[3].status === 'fulfilled' && results[3].value && results[3].value.success) {
|
||||
groupIssuesByWorkplace(results[3].value.data || []);
|
||||
}
|
||||
|
||||
// 작업장 전체 (카테고리별 그룹핑)
|
||||
if (results[5].status === 'fulfilled' && results[5].value && results[5].value.success) {
|
||||
allWorkplaces = results[5].value.data || [];
|
||||
if (results[4].status === 'fulfilled' && results[4].value && results[4].value.success) {
|
||||
allWorkplaces = results[4].value.data || [];
|
||||
groupWorkplacesByCategory(allWorkplaces);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// /js/navigation.js
|
||||
import { config } from './config.js';
|
||||
|
||||
/**
|
||||
* 지정된 URL로 페이지를 리디렉션합니다.
|
||||
* @param {string} url - 이동할 URL
|
||||
*/
|
||||
function redirect(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 페이지로 리디렉션합니다.
|
||||
*/
|
||||
export function redirectToLogin() {
|
||||
const loginUrl = config.paths.loginPage + '?redirect=' + encodeURIComponent(window.location.href);
|
||||
redirect(loginUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자의 기본 대시보드 페이지로 리디렉션합니다.
|
||||
* 백엔드가 지정한 URL이 있으면 그곳으로, 없으면 기본 URL로 이동합니다.
|
||||
* @param {string} [backendRedirectUrl=null] - 백엔드에서 전달받은 리디렉션 URL
|
||||
*/
|
||||
export function redirectToDefaultDashboard(backendRedirectUrl = null) {
|
||||
const destination = backendRedirectUrl || config.paths.defaultDashboard;
|
||||
|
||||
// 부드러운 화면 전환 효과
|
||||
document.body.style.transition = 'opacity 0.3s ease-out';
|
||||
document.body.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
redirect(destination);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 대시보드 페이지로 리디렉션합니다.
|
||||
*/
|
||||
export function redirectToSystemDashboard() {
|
||||
redirect(config.paths.systemDashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그룹 리더 대시보드 페이지로 리디렉션합니다.
|
||||
*/
|
||||
export function redirectToGroupLeaderDashboard() {
|
||||
redirect(config.paths.groupLeaderDashboard);
|
||||
}
|
||||
|
||||
// 필요에 따라 더 많은 리디렉션 함수를 추가할 수 있습니다.
|
||||
// export function redirectToUserProfile() { ... }
|
||||
@@ -1,119 +0,0 @@
|
||||
// /js/page-access-cache.js
|
||||
// 페이지 권한 캐시 - 중복 API 호출 방지
|
||||
|
||||
const CACHE_KEY = 'userPageAccess';
|
||||
const CACHE_DURATION = 10 * 60 * 1000; // 10분
|
||||
|
||||
// 진행 중인 API 호출 Promise (중복 방지)
|
||||
let fetchPromise = null;
|
||||
|
||||
/**
|
||||
* 페이지 접근 권한 데이터 가져오기 (캐시 우선)
|
||||
* @param {object} currentUser - 현재 사용자 객체
|
||||
* @returns {Promise<Array>} 접근 가능한 페이지 목록
|
||||
*/
|
||||
export async function getPageAccess(currentUser) {
|
||||
if (!currentUser || !currentUser.user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 캐시 확인
|
||||
const cached = localStorage.getItem(CACHE_KEY);
|
||||
if (cached) {
|
||||
try {
|
||||
const cacheData = JSON.parse(cached);
|
||||
if (Date.now() - cacheData.timestamp < CACHE_DURATION) {
|
||||
return cacheData.pages;
|
||||
}
|
||||
} catch (e) {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 이미 API 호출 중이면 기존 Promise 반환
|
||||
if (fetchPromise) {
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
// 3. 새로운 API 호출
|
||||
fetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('sso_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('페이지 권한 조회 실패:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const accessiblePages = data.data.pageAccess || [];
|
||||
|
||||
// 캐시 저장
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify({
|
||||
pages: accessiblePages,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
|
||||
return accessiblePages;
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 조회 오류:', error);
|
||||
return null;
|
||||
} finally {
|
||||
fetchPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 페이지에 대한 접근 권한 확인
|
||||
* @param {string} pageKey - 페이지 키
|
||||
* @param {object} currentUser - 현재 사용자 객체
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function hasPageAccess(pageKey, currentUser) {
|
||||
// Admin은 모든 페이지 접근 가능
|
||||
if (currentUser.role === 'Admin' || currentUser.role === 'System Admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 대시보드, 프로필은 모든 사용자 접근 가능
|
||||
if (pageKey === 'dashboard' || (pageKey && pageKey.startsWith('profile.'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pages = await getPageAccess(currentUser);
|
||||
if (!pages) return false;
|
||||
|
||||
const pageAccess = pages.find(p => p.page_key === pageKey);
|
||||
return pageAccess && pageAccess.can_access === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 가능한 페이지 키 목록 반환
|
||||
* @param {object} currentUser
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function getAccessiblePageKeys(currentUser) {
|
||||
const pages = await getPageAccess(currentUser);
|
||||
if (!pages) return [];
|
||||
|
||||
return pages
|
||||
.filter(p => p.can_access === 1)
|
||||
.map(p => p.page_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 초기화
|
||||
*/
|
||||
export function clearPageAccessCache() {
|
||||
localStorage.removeItem(CACHE_KEY);
|
||||
fetchPromise = null;
|
||||
}
|
||||
@@ -1,850 +0,0 @@
|
||||
/**
|
||||
* 안전 체크리스트 관리 페이지 스크립트
|
||||
*
|
||||
* 3가지 유형의 체크리스트 항목을 관리:
|
||||
* 1. 기본 사항 - 항상 표시
|
||||
* 2. 날씨별 - 날씨 조건에 따라 표시
|
||||
* 3. 작업별 - 선택한 작업에 따라 표시
|
||||
*
|
||||
* @since 2026-02-02
|
||||
*/
|
||||
|
||||
import { apiCall } from './api-config.js';
|
||||
|
||||
// 전역 상태
|
||||
let allChecks = [];
|
||||
let weatherConditions = [];
|
||||
let workTypes = [];
|
||||
let tasks = [];
|
||||
let currentTab = 'basic';
|
||||
let editingCheckId = null;
|
||||
|
||||
// 카테고리 정보
|
||||
const CATEGORIES = {
|
||||
PPE: { name: 'PPE (개인보호장비)', icon: '🦺' },
|
||||
EQUIPMENT: { name: 'EQUIPMENT (장비점검)', icon: '🔧' },
|
||||
ENVIRONMENT: { name: 'ENVIRONMENT (작업환경)', icon: '🏗️' },
|
||||
EMERGENCY: { name: 'EMERGENCY (비상대응)', icon: '🚨' },
|
||||
WEATHER: { name: 'WEATHER (날씨)', icon: '🌤️' },
|
||||
TASK: { name: 'TASK (작업)', icon: '📋' }
|
||||
};
|
||||
|
||||
// 날씨 아이콘 매핑
|
||||
const WEATHER_ICONS = {
|
||||
clear: '☀️',
|
||||
rain: '🌧️',
|
||||
snow: '❄️',
|
||||
heat: '🔥',
|
||||
cold: '🥶',
|
||||
wind: '💨',
|
||||
fog: '🌫️',
|
||||
dust: '😷'
|
||||
};
|
||||
|
||||
/**
|
||||
* 페이지 초기화
|
||||
*/
|
||||
async function initPage() {
|
||||
try {
|
||||
|
||||
await Promise.all([
|
||||
loadAllChecks(),
|
||||
loadWeatherConditions(),
|
||||
loadWorkTypes()
|
||||
]);
|
||||
|
||||
renderCurrentTab();
|
||||
} catch (error) {
|
||||
console.error('초기화 실패:', error);
|
||||
showToast('데이터를 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// DOMContentLoaded 이벤트
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
|
||||
/**
|
||||
* 모든 안전 체크 항목 로드
|
||||
*/
|
||||
async function loadAllChecks() {
|
||||
try {
|
||||
const response = await apiCall('/tbm/safety-checks');
|
||||
if (response && response.success) {
|
||||
allChecks = response.data || [];
|
||||
} else {
|
||||
console.warn('체크 항목 응답 실패:', response);
|
||||
allChecks = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('체크 항목 로드 실패:', error);
|
||||
allChecks = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 조건 목록 로드
|
||||
*/
|
||||
async function loadWeatherConditions() {
|
||||
try {
|
||||
const response = await apiCall('/tbm/weather/conditions');
|
||||
if (response && response.success) {
|
||||
weatherConditions = response.data || [];
|
||||
populateWeatherSelects();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('날씨 조건 로드 실패:', error);
|
||||
weatherConditions = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정(작업 유형) 목록 로드
|
||||
*/
|
||||
async function loadWorkTypes() {
|
||||
try {
|
||||
const response = await apiCall('/daily-work-reports/work-types');
|
||||
if (response && response.success) {
|
||||
workTypes = response.data || [];
|
||||
populateWorkTypeSelects();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('공정 목록 로드 실패:', error);
|
||||
workTypes = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 조건 셀렉트 박스 채우기
|
||||
*/
|
||||
function populateWeatherSelects() {
|
||||
const filterSelect = document.getElementById('weatherFilter');
|
||||
const modalSelect = document.getElementById('weatherCondition');
|
||||
|
||||
const options = weatherConditions.map(wc =>
|
||||
`<option value="${wc.condition_code}">${WEATHER_ICONS[wc.condition_code] || ''} ${wc.condition_name}</option>`
|
||||
).join('');
|
||||
|
||||
if (filterSelect) {
|
||||
filterSelect.innerHTML = `<option value="">모든 날씨 조건</option>${options}`;
|
||||
}
|
||||
|
||||
if (modalSelect) {
|
||||
modalSelect.innerHTML = options || '<option value="">날씨 조건 없음</option>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 셀렉트 박스 채우기
|
||||
*/
|
||||
function populateWorkTypeSelects() {
|
||||
const filterSelect = document.getElementById('workTypeFilter');
|
||||
const modalSelect = document.getElementById('modalWorkType');
|
||||
|
||||
const options = workTypes.map(wt =>
|
||||
`<option value="${wt.id}">${wt.name}</option>`
|
||||
).join('');
|
||||
|
||||
if (filterSelect) {
|
||||
filterSelect.innerHTML = `<option value="">공정 선택</option>${options}`;
|
||||
}
|
||||
|
||||
if (modalSelect) {
|
||||
modalSelect.innerHTML = `<option value="">공정 선택</option>${options}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
function switchTab(tabName) {
|
||||
currentTab = tabName;
|
||||
|
||||
// 탭 버튼 상태 업데이트
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// 탭 콘텐츠 표시/숨김
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.toggle('active', content.id === `${tabName}Tab`);
|
||||
});
|
||||
|
||||
renderCurrentTab();
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 탭 렌더링
|
||||
*/
|
||||
function renderCurrentTab() {
|
||||
switch (currentTab) {
|
||||
case 'basic':
|
||||
renderBasicChecks();
|
||||
break;
|
||||
case 'weather':
|
||||
renderWeatherChecks();
|
||||
break;
|
||||
case 'task':
|
||||
renderTaskChecks();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 체크 항목 렌더링
|
||||
*/
|
||||
function renderBasicChecks() {
|
||||
const container = document.getElementById('basicChecklistContainer');
|
||||
const basicChecks = allChecks.filter(c => c.check_type === 'basic');
|
||||
|
||||
console.log('기본 체크항목:', basicChecks.length, '개');
|
||||
|
||||
if (basicChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.') + renderInlineAddStandalone('basic');
|
||||
return;
|
||||
}
|
||||
|
||||
// 카테고리별로 그룹화
|
||||
const grouped = groupByCategory(basicChecks);
|
||||
|
||||
container.innerHTML = Object.entries(grouped).map(([category, items]) =>
|
||||
renderChecklistGroup(category, items)
|
||||
).join('') + renderInlineAddStandalone('basic');
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨별 체크 항목 렌더링
|
||||
*/
|
||||
function renderWeatherChecks() {
|
||||
const container = document.getElementById('weatherChecklistContainer');
|
||||
const filterValue = document.getElementById('weatherFilter')?.value;
|
||||
|
||||
let weatherChecks = allChecks.filter(c => c.check_type === 'weather');
|
||||
|
||||
if (filterValue) {
|
||||
weatherChecks = weatherChecks.filter(c => c.weather_condition === filterValue);
|
||||
}
|
||||
|
||||
const inlineRow = filterValue ? renderInlineAddStandalone('weather') : '';
|
||||
|
||||
if (weatherChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.') + inlineRow;
|
||||
return;
|
||||
}
|
||||
|
||||
// 날씨 조건별로 그룹화
|
||||
const grouped = groupByWeather(weatherChecks);
|
||||
|
||||
container.innerHTML = Object.entries(grouped).map(([condition, items]) => {
|
||||
const conditionInfo = weatherConditions.find(wc => wc.condition_code === condition);
|
||||
const icon = WEATHER_ICONS[condition] || '🌤️';
|
||||
const name = conditionInfo?.condition_name || condition;
|
||||
|
||||
return renderChecklistGroup(`${icon} ${name}`, items, condition);
|
||||
}).join('') + inlineRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업별 체크 항목 렌더링
|
||||
*/
|
||||
function renderTaskChecks() {
|
||||
const container = document.getElementById('taskChecklistContainer');
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
|
||||
// 공정 미선택 시 안내
|
||||
if (!workTypeId) {
|
||||
container.innerHTML = renderGuideState('공정을 먼저 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
let taskChecks = allChecks.filter(c => c.check_type === 'task');
|
||||
|
||||
if (taskId) {
|
||||
taskChecks = taskChecks.filter(c => c.task_id == taskId);
|
||||
} else if (workTypeId && tasks.length > 0) {
|
||||
const workTypeTasks = tasks.filter(t => t.work_type_id == workTypeId);
|
||||
const taskIds = workTypeTasks.map(t => t.task_id);
|
||||
taskChecks = taskChecks.filter(c => taskIds.includes(c.task_id));
|
||||
}
|
||||
|
||||
const inlineRow = taskId ? renderInlineAddStandalone('task') : '';
|
||||
|
||||
if (taskChecks.length === 0) {
|
||||
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.') + inlineRow;
|
||||
return;
|
||||
}
|
||||
|
||||
// 작업별로 그룹화
|
||||
const grouped = groupByTask(taskChecks);
|
||||
|
||||
container.innerHTML = Object.entries(grouped).map(([taskId, items]) => {
|
||||
const task = tasks.find(t => t.task_id == taskId);
|
||||
const taskName = task?.task_name || `작업 ${taskId}`;
|
||||
|
||||
return renderChecklistGroup(`📋 ${taskName}`, items, null, taskId);
|
||||
}).join('') + inlineRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리별 그룹화
|
||||
*/
|
||||
function groupByCategory(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const category = check.check_category || 'OTHER';
|
||||
if (!acc[category]) acc[category] = [];
|
||||
acc[category].push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 조건별 그룹화
|
||||
*/
|
||||
function groupByWeather(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const condition = check.weather_condition || 'other';
|
||||
if (!acc[condition]) acc[condition] = [];
|
||||
acc[condition].push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업별 그룹화
|
||||
*/
|
||||
function groupByTask(checks) {
|
||||
return checks.reduce((acc, check) => {
|
||||
const taskId = check.task_id || 0;
|
||||
if (!acc[taskId]) acc[taskId] = [];
|
||||
acc[taskId].push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 체크리스트 그룹 렌더링
|
||||
*/
|
||||
function renderChecklistGroup(title, items, weatherCondition = null, taskId = null) {
|
||||
const categoryInfo = CATEGORIES[title] || { name: title, icon: '' };
|
||||
const displayTitle = categoryInfo.name !== title ? categoryInfo.name : title;
|
||||
const icon = categoryInfo.icon || '';
|
||||
|
||||
// 표시 순서로 정렬
|
||||
items.sort((a, b) => (a.display_order || 0) - (b.display_order || 0));
|
||||
|
||||
return `
|
||||
<div class="checklist-group">
|
||||
<div class="group-header">
|
||||
<div class="group-title">
|
||||
<span class="group-icon">${icon}</span>
|
||||
<span>${displayTitle}</span>
|
||||
</div>
|
||||
<span class="group-count">${items.length}개</span>
|
||||
</div>
|
||||
<div class="checklist-items">
|
||||
${items.map(item => renderChecklistItem(item)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 체크리스트 항목 렌더링
|
||||
*/
|
||||
function renderChecklistItem(item) {
|
||||
const requiredBadge = item.is_required
|
||||
? '<span class="item-badge badge-required">필수</span>'
|
||||
: '<span class="item-badge badge-optional">선택</span>';
|
||||
|
||||
return `
|
||||
<div class="checklist-item" data-check-id="${item.check_id}">
|
||||
<div class="item-info">
|
||||
<div class="item-name">${item.check_item}</div>
|
||||
<div class="item-meta">
|
||||
${requiredBadge}
|
||||
${item.description ? `<span>${item.description}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="btn-icon btn-edit" onclick="openEditModal(${item.check_id})" title="수정">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-icon btn-delete" onclick="confirmDelete(${item.check_id})" title="삭제">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 상태 렌더링
|
||||
*/
|
||||
function renderEmptyState(message) {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📋</div>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 안내 상태 렌더링 (필터 미선택 시)
|
||||
*/
|
||||
function renderGuideState(message) {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">👆</div>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날씨 필터 변경
|
||||
*/
|
||||
function filterByWeather() {
|
||||
renderWeatherChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 공정 필터 변경
|
||||
*/
|
||||
async function filterByWorkType() {
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
const taskSelect = document.getElementById('taskFilter');
|
||||
|
||||
// workTypeId가 없거나 빈 문자열이면 early return
|
||||
if (!workTypeId || workTypeId === '' || workTypeId === 'undefined') {
|
||||
if (taskSelect) {
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>';
|
||||
}
|
||||
tasks = [];
|
||||
renderTaskChecks();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/tasks/by-work-type/${workTypeId}`);
|
||||
if (response && response.success) {
|
||||
tasks = response.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
tasks.map(t => `<option value="${t.task_id}">${t.task_name}</option>`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업 목록 로드 실패:', error);
|
||||
tasks = [];
|
||||
}
|
||||
|
||||
renderTaskChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업 필터 변경
|
||||
*/
|
||||
function filterByTask() {
|
||||
renderTaskChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달의 작업 목록 로드
|
||||
*/
|
||||
async function loadModalTasks() {
|
||||
const workTypeId = document.getElementById('modalWorkType')?.value;
|
||||
const taskSelect = document.getElementById('modalTask');
|
||||
|
||||
// workTypeId가 없거나 빈 문자열이면 early return
|
||||
if (!workTypeId || workTypeId === '' || workTypeId === 'undefined') {
|
||||
if (taskSelect) {
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall(`/tasks/by-work-type/${workTypeId}`);
|
||||
if (response && response.success) {
|
||||
const modalTasks = response.data || [];
|
||||
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
||||
modalTasks.map(t => `<option value="${t.task_id}">${t.task_name}</option>`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업 목록 로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건부 필드 토글
|
||||
*/
|
||||
function toggleConditionalFields() {
|
||||
const checkType = document.querySelector('input[name="checkType"]:checked')?.value;
|
||||
|
||||
document.getElementById('basicFields').classList.toggle('show', checkType === 'basic');
|
||||
document.getElementById('weatherFields').classList.toggle('show', checkType === 'weather');
|
||||
document.getElementById('taskFields').classList.toggle('show', checkType === 'task');
|
||||
}
|
||||
|
||||
/**
|
||||
* 추가 모달 열기
|
||||
*/
|
||||
function openAddModal() {
|
||||
editingCheckId = null;
|
||||
document.getElementById('modalTitle').textContent = '체크 항목 추가';
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('checkForm').reset();
|
||||
document.getElementById('checkId').value = '';
|
||||
|
||||
// 현재 탭에 맞는 유형 선택
|
||||
const typeRadio = document.querySelector(`input[name="checkType"][value="${currentTab}"]`);
|
||||
if (typeRadio) {
|
||||
typeRadio.checked = true;
|
||||
}
|
||||
|
||||
toggleConditionalFields();
|
||||
|
||||
// 날씨별 탭: 현재 필터의 날씨 조건 반영
|
||||
if (currentTab === 'weather') {
|
||||
const weatherFilter = document.getElementById('weatherFilter')?.value;
|
||||
if (weatherFilter) {
|
||||
document.getElementById('weatherCondition').value = weatherFilter;
|
||||
}
|
||||
}
|
||||
|
||||
// 작업별 탭: 현재 필터의 공정/작업 반영
|
||||
if (currentTab === 'task') {
|
||||
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
||||
if (workTypeId) {
|
||||
document.getElementById('modalWorkType').value = workTypeId;
|
||||
loadModalTasks().then(() => {
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
if (taskId) {
|
||||
document.getElementById('modalTask').value = taskId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 모달 열기
|
||||
*/
|
||||
async function openEditModal(checkId) {
|
||||
editingCheckId = checkId;
|
||||
const check = allChecks.find(c => c.check_id === checkId);
|
||||
|
||||
if (!check) {
|
||||
showToast('항목을 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('modalTitle').textContent = '체크 항목 수정';
|
||||
document.getElementById('checkId').value = checkId;
|
||||
|
||||
// 유형 선택
|
||||
const typeRadio = document.querySelector(`input[name="checkType"][value="${check.check_type}"]`);
|
||||
if (typeRadio) {
|
||||
typeRadio.checked = true;
|
||||
}
|
||||
|
||||
toggleConditionalFields();
|
||||
|
||||
// 카테고리
|
||||
if (check.check_type === 'basic') {
|
||||
document.getElementById('checkCategory').value = check.check_category || 'PPE';
|
||||
}
|
||||
|
||||
// 날씨 조건
|
||||
if (check.check_type === 'weather') {
|
||||
document.getElementById('weatherCondition').value = check.weather_condition || '';
|
||||
}
|
||||
|
||||
// 작업
|
||||
if (check.check_type === 'task' && check.task_id) {
|
||||
// 먼저 공정 찾기 (task를 통해)
|
||||
const task = tasks.find(t => t.task_id === check.task_id);
|
||||
if (task) {
|
||||
document.getElementById('modalWorkType').value = task.work_type_id;
|
||||
await loadModalTasks();
|
||||
document.getElementById('modalTask').value = check.task_id;
|
||||
}
|
||||
}
|
||||
|
||||
// 공통 필드
|
||||
document.getElementById('checkItem').value = check.check_item || '';
|
||||
document.getElementById('checkDescription').value = check.description || '';
|
||||
document.getElementById('isRequired').checked = check.is_required === 1 || check.is_required === true;
|
||||
document.getElementById('displayOrder').value = check.display_order || 0;
|
||||
|
||||
showModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 표시
|
||||
*/
|
||||
function showModal() {
|
||||
document.getElementById('checkModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 닫기
|
||||
*/
|
||||
function closeModal() {
|
||||
document.getElementById('checkModal').style.display = 'none';
|
||||
editingCheckId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 체크 항목 저장
|
||||
*/
|
||||
async function saveCheck() {
|
||||
const checkType = document.querySelector('input[name="checkType"]:checked')?.value;
|
||||
const checkItem = document.getElementById('checkItem').value.trim();
|
||||
|
||||
if (!checkItem) {
|
||||
showToast('체크 항목을 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
check_type: checkType,
|
||||
check_item: checkItem,
|
||||
description: document.getElementById('checkDescription').value.trim() || null,
|
||||
is_required: document.getElementById('isRequired').checked,
|
||||
display_order: parseInt(document.getElementById('displayOrder').value) || 0
|
||||
};
|
||||
|
||||
// 유형별 추가 데이터
|
||||
switch (checkType) {
|
||||
case 'basic':
|
||||
data.check_category = document.getElementById('checkCategory').value;
|
||||
break;
|
||||
case 'weather':
|
||||
data.check_category = 'WEATHER';
|
||||
data.weather_condition = document.getElementById('weatherCondition').value;
|
||||
if (!data.weather_condition) {
|
||||
showToast('날씨 조건을 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'task':
|
||||
data.check_category = 'TASK';
|
||||
data.task_id = document.getElementById('modalTask').value;
|
||||
if (!data.task_id) {
|
||||
showToast('작업을 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (editingCheckId) {
|
||||
// 수정
|
||||
response = await apiCall(`/tbm/safety-checks/${editingCheckId}`, 'PUT', data);
|
||||
} else {
|
||||
// 추가
|
||||
response = await apiCall('/tbm/safety-checks', 'POST', data);
|
||||
}
|
||||
|
||||
if (response && response.success) {
|
||||
showToast(editingCheckId ? '항목이 수정되었습니다.' : '항목이 추가되었습니다.', 'success');
|
||||
closeModal();
|
||||
await loadAllChecks();
|
||||
renderCurrentTab();
|
||||
} else {
|
||||
showToast(response?.message || '저장에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 실패:', error);
|
||||
showToast('저장 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 확인
|
||||
*/
|
||||
function confirmDelete(checkId) {
|
||||
const check = allChecks.find(c => c.check_id === checkId);
|
||||
|
||||
if (!check) {
|
||||
showToast('항목을 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`"${check.check_item}" 항목을 삭제하시겠습니까?`)) {
|
||||
deleteCheck(checkId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 체크 항목 삭제
|
||||
*/
|
||||
async function deleteCheck(checkId) {
|
||||
try {
|
||||
const response = await apiCall(`/tbm/safety-checks/${checkId}`, 'DELETE');
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('항목이 삭제되었습니다.', 'success');
|
||||
await loadAllChecks();
|
||||
renderCurrentTab();
|
||||
} else {
|
||||
showToast(response?.message || '삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 실패:', error);
|
||||
showToast('삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 추가 행 렌더링
|
||||
*/
|
||||
function renderInlineAddRow(tabType) {
|
||||
if (tabType === 'basic') {
|
||||
const categoryOptions = Object.entries(CATEGORIES)
|
||||
.filter(([key]) => !['WEATHER', 'TASK'].includes(key))
|
||||
.map(([key, val]) => `<option value="${key}">${val.name}</option>`)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<select class="inline-add-select" id="inlineCategory">${categoryOptions}</select>
|
||||
<input type="text" class="inline-add-input" id="inlineBasicInput"
|
||||
placeholder="새 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('basic');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('basic')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tabType === 'weather') {
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<input type="text" class="inline-add-input" id="inlineWeatherInput"
|
||||
placeholder="새 날씨별 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('weather');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('weather')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (tabType === 'task') {
|
||||
return `
|
||||
<div class="inline-add-row">
|
||||
<input type="text" class="inline-add-input" id="inlineTaskInput"
|
||||
placeholder="새 작업별 체크 항목 입력..." onkeydown="if(event.key==='Enter'){event.preventDefault();addInlineCheck('task');}">
|
||||
<button class="inline-add-btn" onclick="addInlineCheck('task')">추가</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 추가 행을 standalone 컨테이너로 감싸기 (빈 상태용)
|
||||
*/
|
||||
function renderInlineAddStandalone(tabType) {
|
||||
return `<div class="inline-add-standalone">${renderInlineAddRow(tabType)}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인으로 체크 항목 추가
|
||||
*/
|
||||
async function addInlineCheck(tabType) {
|
||||
let checkItem, data;
|
||||
|
||||
if (tabType === 'basic') {
|
||||
const input = document.getElementById('inlineBasicInput');
|
||||
const categorySelect = document.getElementById('inlineCategory');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
data = {
|
||||
check_type: 'basic',
|
||||
check_item: checkItem,
|
||||
check_category: categorySelect?.value || 'PPE',
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else if (tabType === 'weather') {
|
||||
const input = document.getElementById('inlineWeatherInput');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
const weatherFilter = document.getElementById('weatherFilter')?.value;
|
||||
if (!weatherFilter) {
|
||||
showToast('날씨 조건을 먼저 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
check_type: 'weather',
|
||||
check_item: checkItem,
|
||||
check_category: 'WEATHER',
|
||||
weather_condition: weatherFilter,
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else if (tabType === 'task') {
|
||||
const input = document.getElementById('inlineTaskInput');
|
||||
checkItem = input?.value.trim();
|
||||
if (!checkItem) { input?.focus(); return; }
|
||||
|
||||
const taskId = document.getElementById('taskFilter')?.value;
|
||||
if (!taskId) {
|
||||
showToast('작업을 먼저 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data = {
|
||||
check_type: 'task',
|
||||
check_item: checkItem,
|
||||
check_category: 'TASK',
|
||||
task_id: parseInt(taskId),
|
||||
is_required: true,
|
||||
display_order: 0
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall('/tbm/safety-checks', 'POST', data);
|
||||
if (response && response.success) {
|
||||
showToast('항목이 추가되었습니다.', 'success');
|
||||
await loadAllChecks();
|
||||
renderCurrentTab();
|
||||
} else {
|
||||
showToast(response?.message || '추가에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('인라인 추가 실패:', error);
|
||||
showToast('추가 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// showToast → api-base.js 전역 사용
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
document.getElementById('checkModal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// HTML onclick에서 호출할 수 있도록 전역에 노출
|
||||
window.switchTab = switchTab;
|
||||
window.openAddModal = openAddModal;
|
||||
window.openEditModal = openEditModal;
|
||||
window.closeModal = closeModal;
|
||||
window.saveCheck = saveCheck;
|
||||
window.confirmDelete = confirmDelete;
|
||||
window.filterByWeather = filterByWeather;
|
||||
window.filterByWorkType = filterByWorkType;
|
||||
window.filterByTask = filterByTask;
|
||||
window.loadModalTasks = loadModalTasks;
|
||||
window.toggleConditionalFields = toggleConditionalFields;
|
||||
window.addInlineCheck = addInlineCheck;
|
||||
@@ -1,368 +0,0 @@
|
||||
// 안전관리 대시보드 JavaScript
|
||||
|
||||
let currentStatus = 'pending';
|
||||
let requests = [];
|
||||
let currentRejectRequestId = null;
|
||||
|
||||
// showToast → api-base.js 전역 사용
|
||||
|
||||
// ==================== 초기화 ====================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadRequests();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// ==================== 데이터 로드 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 목록 로드
|
||||
*/
|
||||
async function loadRequests() {
|
||||
try {
|
||||
const filters = currentStatus === 'all' ? {} : { status: currentStatus };
|
||||
const queryString = new URLSearchParams(filters).toString();
|
||||
|
||||
const response = await window.apiCall(`/workplace-visits/requests?${queryString}`, 'GET');
|
||||
|
||||
if (response && response.success) {
|
||||
requests = response.data || [];
|
||||
renderRequestTable();
|
||||
updateStats();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('출입 신청 목록 로드 오류:', error);
|
||||
showToast('출입 신청 목록을 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 업데이트
|
||||
*/
|
||||
async function updateStats() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplace-visits/requests', 'GET');
|
||||
|
||||
if (response && response.success) {
|
||||
const allRequests = response.data || [];
|
||||
|
||||
const stats = {
|
||||
pending: allRequests.filter(r => r.status === 'pending').length,
|
||||
approved: allRequests.filter(r => r.status === 'approved').length,
|
||||
training_completed: allRequests.filter(r => r.status === 'training_completed').length,
|
||||
rejected: allRequests.filter(r => r.status === 'rejected').length
|
||||
};
|
||||
|
||||
document.getElementById('statPending').textContent = stats.pending;
|
||||
document.getElementById('statApproved').textContent = stats.approved;
|
||||
document.getElementById('statTrainingCompleted').textContent = stats.training_completed;
|
||||
document.getElementById('statRejected').textContent = stats.rejected;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('통계 업데이트 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 렌더링
|
||||
*/
|
||||
function renderRequestTable() {
|
||||
const container = document.getElementById('requestTableContainer');
|
||||
|
||||
if (requests.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📭</div>
|
||||
<h3>출입 신청이 없습니다</h3>
|
||||
<p>현재 ${getStatusText(currentStatus)} 상태의 신청이 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<table class="request-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>신청일</th>
|
||||
<th>신청자</th>
|
||||
<th>방문자</th>
|
||||
<th>인원</th>
|
||||
<th>방문 작업장</th>
|
||||
<th>방문 일시</th>
|
||||
<th>목적</th>
|
||||
<th>상태</th>
|
||||
<th>작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
requests.forEach(req => {
|
||||
const statusText = {
|
||||
'pending': '승인 대기',
|
||||
'approved': '승인됨',
|
||||
'rejected': '반려됨',
|
||||
'training_completed': '교육 완료'
|
||||
}[req.status] || req.status;
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${new Date(req.created_at).toLocaleDateString()}</td>
|
||||
<td>${req.requester_full_name || req.requester_name}</td>
|
||||
<td>${req.visitor_company}</td>
|
||||
<td>${req.visitor_count}명</td>
|
||||
<td>${req.category_name} - ${req.workplace_name}</td>
|
||||
<td>${req.visit_date} ${req.visit_time}</td>
|
||||
<td>${req.purpose_name}</td>
|
||||
<td><span class="status-badge ${req.status}">${statusText}</span></td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-sm btn-secondary" onclick="viewDetail(${req.request_id})">상세</button>
|
||||
${req.status === 'pending' ? `
|
||||
<button class="btn btn-sm btn-primary" onclick="approveRequest(${req.request_id})">승인</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="openRejectModal(${req.request_id})">반려</button>
|
||||
` : ''}
|
||||
${req.status === 'approved' ? `
|
||||
<button class="btn btn-sm btn-primary" onclick="startTraining(${req.request_id})">교육 진행</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 텍스트 변환
|
||||
*/
|
||||
function getStatusText(status) {
|
||||
const map = {
|
||||
'pending': '승인 대기',
|
||||
'approved': '승인 완료',
|
||||
'rejected': '반려',
|
||||
'training_completed': '교육 완료',
|
||||
'all': '전체'
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
|
||||
// ==================== 탭 전환 ====================
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
async function switchTab(status) {
|
||||
currentStatus = status;
|
||||
|
||||
// 탭 활성화 상태 변경
|
||||
document.querySelectorAll('.status-tab').forEach(tab => {
|
||||
if (tab.dataset.status === status) {
|
||||
tab.classList.add('active');
|
||||
} else {
|
||||
tab.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
await loadRequests();
|
||||
}
|
||||
|
||||
// ==================== 상세보기 ====================
|
||||
|
||||
/**
|
||||
* 상세보기 모달 열기
|
||||
*/
|
||||
async function viewDetail(requestId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplace-visits/requests/${requestId}`, 'GET');
|
||||
|
||||
if (response && response.success) {
|
||||
const req = response.data;
|
||||
const statusText = {
|
||||
'pending': '승인 대기',
|
||||
'approved': '승인됨',
|
||||
'rejected': '반려됨',
|
||||
'training_completed': '교육 완료'
|
||||
}[req.status] || req.status;
|
||||
|
||||
let html = `
|
||||
<div class="detail-grid">
|
||||
<div class="detail-label">신청 번호</div>
|
||||
<div class="detail-value">#${req.request_id}</div>
|
||||
|
||||
<div class="detail-label">신청일</div>
|
||||
<div class="detail-value">${new Date(req.created_at).toLocaleString()}</div>
|
||||
|
||||
<div class="detail-label">신청자</div>
|
||||
<div class="detail-value">${req.requester_full_name || req.requester_name}</div>
|
||||
|
||||
<div class="detail-label">방문자 소속</div>
|
||||
<div class="detail-value">${req.visitor_company}</div>
|
||||
|
||||
<div class="detail-label">방문 인원</div>
|
||||
<div class="detail-value">${req.visitor_count}명</div>
|
||||
|
||||
<div class="detail-label">방문 구역</div>
|
||||
<div class="detail-value">${req.category_name}</div>
|
||||
|
||||
<div class="detail-label">방문 작업장</div>
|
||||
<div class="detail-value">${req.workplace_name}</div>
|
||||
|
||||
<div class="detail-label">방문 날짜</div>
|
||||
<div class="detail-value">${req.visit_date}</div>
|
||||
|
||||
<div class="detail-label">방문 시간</div>
|
||||
<div class="detail-value">${req.visit_time}</div>
|
||||
|
||||
<div class="detail-label">방문 목적</div>
|
||||
<div class="detail-value">${req.purpose_name}</div>
|
||||
|
||||
<div class="detail-label">상태</div>
|
||||
<div class="detail-value"><span class="status-badge ${req.status}">${statusText}</span></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (req.notes) {
|
||||
html += `
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--gray-50); border-radius: var(--radius-md);">
|
||||
<strong>비고:</strong><br>
|
||||
${req.notes}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (req.rejection_reason) {
|
||||
html += `
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--red-50); border-radius: var(--radius-md); color: var(--red-700);">
|
||||
<strong>반려 사유:</strong><br>
|
||||
${req.rejection_reason}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (req.approved_by) {
|
||||
html += `
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--blue-50); border-radius: var(--radius-md);">
|
||||
<strong>처리 정보:</strong><br>
|
||||
처리자: ${req.approver_name || 'Unknown'}<br>
|
||||
처리 시간: ${new Date(req.approved_at).toLocaleString()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
document.getElementById('detailContent').innerHTML = html;
|
||||
document.getElementById('detailModal').style.display = 'flex';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('상세 정보 로드 오류:', error);
|
||||
showToast('상세 정보를 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상세보기 모달 닫기
|
||||
*/
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detailModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// ==================== 승인/반려 ====================
|
||||
|
||||
/**
|
||||
* 승인 처리
|
||||
*/
|
||||
async function approveRequest(requestId) {
|
||||
if (!confirm('이 출입 신청을 승인하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/workplace-visits/requests/${requestId}/approve`, 'PUT');
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('출입 신청이 승인되었습니다.', 'success');
|
||||
await loadRequests();
|
||||
updateStats();
|
||||
} else {
|
||||
throw new Error(response?.message || '승인 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('승인 처리 오류:', error);
|
||||
showToast(error.message || '승인 처리 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려 모달 열기
|
||||
*/
|
||||
function openRejectModal(requestId) {
|
||||
currentRejectRequestId = requestId;
|
||||
document.getElementById('rejectionReason').value = '';
|
||||
document.getElementById('rejectModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려 모달 닫기
|
||||
*/
|
||||
function closeRejectModal() {
|
||||
currentRejectRequestId = null;
|
||||
document.getElementById('rejectModal').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 반려 확정
|
||||
*/
|
||||
async function confirmReject() {
|
||||
const reason = document.getElementById('rejectionReason').value.trim();
|
||||
|
||||
if (!reason) {
|
||||
showToast('반려 사유를 입력해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(
|
||||
`/workplace-visits/requests/${currentRejectRequestId}/reject`,
|
||||
'PUT',
|
||||
{ rejection_reason: reason }
|
||||
);
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('출입 신청이 반려되었습니다.', 'success');
|
||||
closeRejectModal();
|
||||
await loadRequests();
|
||||
updateStats();
|
||||
} else {
|
||||
throw new Error(response?.message || '반려 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('반려 처리 오류:', error);
|
||||
showToast(error.message || '반려 처리 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 안전교육 진행 ====================
|
||||
|
||||
/**
|
||||
* 안전교육 진행 페이지로 이동
|
||||
*/
|
||||
function startTraining(requestId) {
|
||||
window.location.href = `/pages/safety/training-conduct.html?request_id=${requestId}`;
|
||||
}
|
||||
|
||||
// 전역 함수로 노출
|
||||
window.switchTab = switchTab;
|
||||
window.viewDetail = viewDetail;
|
||||
window.closeDetailModal = closeDetailModal;
|
||||
window.approveRequest = approveRequest;
|
||||
window.openRejectModal = openRejectModal;
|
||||
window.closeRejectModal = closeRejectModal;
|
||||
window.confirmReject = confirmReject;
|
||||
window.startTraining = startTraining;
|
||||
@@ -1,222 +0,0 @@
|
||||
/**
|
||||
* 안전신고 현황 페이지 JavaScript
|
||||
* category_type=safety 고정 필터
|
||||
*/
|
||||
|
||||
const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api';
|
||||
const CATEGORY_TYPE = 'safety';
|
||||
|
||||
// 상태 한글 변환
|
||||
const STATUS_LABELS = {
|
||||
reported: '신고',
|
||||
received: '접수',
|
||||
in_progress: '처리중',
|
||||
completed: '완료',
|
||||
closed: '종료'
|
||||
};
|
||||
|
||||
// DOM 요소
|
||||
let issueList;
|
||||
let filterStatus, filterStartDate, filterEndDate;
|
||||
|
||||
// 초기화
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
issueList = document.getElementById('issueList');
|
||||
filterStatus = document.getElementById('filterStatus');
|
||||
filterStartDate = document.getElementById('filterStartDate');
|
||||
filterEndDate = document.getElementById('filterEndDate');
|
||||
|
||||
// 필터 이벤트 리스너
|
||||
filterStatus.addEventListener('change', loadIssues);
|
||||
filterStartDate.addEventListener('change', loadIssues);
|
||||
filterEndDate.addEventListener('change', loadIssues);
|
||||
|
||||
// 데이터 로드
|
||||
await Promise.all([loadStats(), loadIssues()]);
|
||||
});
|
||||
|
||||
/**
|
||||
* 통계 로드 (안전만)
|
||||
*/
|
||||
async function loadStats() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/work-issues/stats/summary?category_type=${CATEGORY_TYPE}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
document.getElementById('statsGrid').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
document.getElementById('statReported').textContent = data.data.reported || 0;
|
||||
document.getElementById('statReceived').textContent = data.data.received || 0;
|
||||
document.getElementById('statProgress').textContent = data.data.in_progress || 0;
|
||||
document.getElementById('statCompleted').textContent = data.data.completed || 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('통계 로드 실패:', error);
|
||||
document.getElementById('statsGrid').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전신고 목록 로드
|
||||
*/
|
||||
async function loadIssues() {
|
||||
try {
|
||||
// 필터 파라미터 구성 (category_type 고정)
|
||||
const params = new URLSearchParams();
|
||||
params.append('category_type', CATEGORY_TYPE);
|
||||
|
||||
if (filterStatus.value) params.append('status', filterStatus.value);
|
||||
if (filterStartDate.value) params.append('start_date', filterStartDate.value);
|
||||
if (filterEndDate.value) params.append('end_date', filterEndDate.value);
|
||||
|
||||
const response = await fetch(`${API_BASE}/work-issues?${params.toString()}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('sso_token')}` }
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('목록 조회 실패');
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
renderIssues(data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('안전신고 목록 로드 실패:', error);
|
||||
issueList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-title">목록을 불러올 수 없습니다</div>
|
||||
<p>잠시 후 다시 시도해주세요.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전신고 목록 렌더링
|
||||
*/
|
||||
function renderIssues(issues) {
|
||||
if (issues.length === 0) {
|
||||
issueList.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-title">등록된 안전 신고가 없습니다</div>
|
||||
<p>새로운 안전 문제를 신고하려면 '안전 신고' 버튼을 클릭하세요.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = (window.API_BASE_URL || 'http://localhost:30005').replace('/api', '');
|
||||
|
||||
issueList.innerHTML = issues.map(issue => {
|
||||
const reportDate = new Date(issue.report_date).toLocaleString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
// 위치 정보 (escaped)
|
||||
let location = escapeHtml(issue.custom_location || '');
|
||||
if (issue.factory_name) {
|
||||
location = escapeHtml(issue.factory_name);
|
||||
if (issue.workplace_name) {
|
||||
location += ` - ${escapeHtml(issue.workplace_name)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 신고 제목 (항목명 또는 카테고리명)
|
||||
const title = escapeHtml(issue.issue_item_name || issue.issue_category_name || '안전 신고');
|
||||
const categoryName = escapeHtml(issue.issue_category_name || '안전');
|
||||
|
||||
// 사진 목록
|
||||
const photos = [
|
||||
issue.photo_path1,
|
||||
issue.photo_path2,
|
||||
issue.photo_path3,
|
||||
issue.photo_path4,
|
||||
issue.photo_path5
|
||||
].filter(Boolean);
|
||||
|
||||
// 안전한 값들
|
||||
const safeReportId = parseInt(issue.report_id) || 0;
|
||||
const validStatuses = ['reported', 'received', 'in_progress', 'completed', 'closed'];
|
||||
const safeStatus = validStatuses.includes(issue.status) ? issue.status : 'reported';
|
||||
const reporterName = escapeHtml(issue.reporter_full_name || issue.reporter_name || '-');
|
||||
const assignedName = issue.assigned_full_name ? escapeHtml(issue.assigned_full_name) : '';
|
||||
|
||||
return `
|
||||
<div class="issue-card" onclick="viewIssue(${safeReportId})">
|
||||
<div class="issue-header">
|
||||
<span class="issue-id">#${safeReportId}</span>
|
||||
<span class="issue-status ${safeStatus}">${STATUS_LABELS[issue.status] || escapeHtml(issue.status || '-')}</span>
|
||||
</div>
|
||||
|
||||
<div class="issue-title">
|
||||
<span class="issue-category-badge">${categoryName}</span>
|
||||
${title}
|
||||
</div>
|
||||
|
||||
<div class="issue-meta">
|
||||
<span class="issue-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
${reporterName}
|
||||
</span>
|
||||
<span class="issue-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
${reportDate}
|
||||
</span>
|
||||
${location ? `
|
||||
<span class="issue-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
${location}
|
||||
</span>
|
||||
` : ''}
|
||||
${assignedName ? `
|
||||
<span class="issue-meta-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
담당: ${assignedName}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${photos.length > 0 ? `
|
||||
<div class="issue-photos">
|
||||
${photos.slice(0, 3).map(p => `
|
||||
<img src="${baseUrl}${encodeURI(p)}" alt="신고 사진" loading="lazy">
|
||||
`).join('')}
|
||||
${photos.length > 3 ? `<span style="display: flex; align-items: center; color: var(--gray-500);">+${photos.length - 3}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상세 보기
|
||||
*/
|
||||
function viewIssue(reportId) {
|
||||
window.location.href = `/pages/safety/issue-detail.html?id=${reportId}&from=safety`;
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
// 안전교육 진행 페이지 JavaScript
|
||||
|
||||
let requestId = null;
|
||||
let requestData = null;
|
||||
let canvas = null;
|
||||
let ctx = null;
|
||||
let isDrawing = false;
|
||||
let hasSignature = false;
|
||||
let savedSignatures = []; // 저장된 서명 목록
|
||||
|
||||
// showToast → api-base.js 전역 사용
|
||||
|
||||
// ==================== 초기화 ====================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// URL 파라미터에서 request_id 가져오기
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
requestId = urlParams.get('request_id');
|
||||
|
||||
if (!requestId) {
|
||||
showToast('출입 신청 ID가 없습니다.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/pages/safety/management.html';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 서명 캔버스 초기화
|
||||
initSignatureCanvas();
|
||||
|
||||
// 현재 날짜 표시
|
||||
const today = new Date().toLocaleDateString('ko-KR');
|
||||
document.getElementById('signatureDate').textContent = today;
|
||||
|
||||
// 출입 신청 정보 로드
|
||||
await loadRequestInfo();
|
||||
});
|
||||
|
||||
// ==================== 출입 신청 정보 로드 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 정보 로드
|
||||
*/
|
||||
async function loadRequestInfo() {
|
||||
try {
|
||||
const response = await window.apiCall(`/workplace-visits/requests/${requestId}`, 'GET');
|
||||
|
||||
if (response && response.success) {
|
||||
requestData = response.data;
|
||||
|
||||
// 상태 확인 - 승인됨 상태만 진행 가능
|
||||
if (requestData.status !== 'approved') {
|
||||
showToast('이미 처리되었거나 승인되지 않은 신청입니다.', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/pages/safety/management.html';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
renderRequestInfo();
|
||||
} else {
|
||||
throw new Error(response?.message || '정보를 불러올 수 없습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('출입 신청 정보 로드 오류:', error);
|
||||
showToast('출입 신청 정보를 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 출입 신청 정보 렌더링
|
||||
*/
|
||||
function renderRequestInfo() {
|
||||
const container = document.getElementById('requestInfo');
|
||||
|
||||
// 날짜 포맷 변환
|
||||
const visitDate = new Date(requestData.visit_date);
|
||||
const formattedDate = visitDate.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short'
|
||||
});
|
||||
|
||||
const html = `
|
||||
<div class="info-item">
|
||||
<div class="info-label">신청 번호</div>
|
||||
<div class="info-value">#${requestData.request_id}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">신청자</div>
|
||||
<div class="info-value">${requestData.requester_full_name || requestData.requester_name}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">방문자 소속</div>
|
||||
<div class="info-value">${requestData.visitor_company}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">방문 인원</div>
|
||||
<div class="info-value">${requestData.visitor_count}명</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">방문 작업장</div>
|
||||
<div class="info-value">${requestData.category_name} - ${requestData.workplace_name}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">방문 일시</div>
|
||||
<div class="info-value">${formattedDate} ${requestData.visit_time}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">방문 목적</div>
|
||||
<div class="info-value">${requestData.purpose_name}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ==================== 서명 캔버스 ====================
|
||||
|
||||
/**
|
||||
* 서명 캔버스 초기화
|
||||
*/
|
||||
function initSignatureCanvas() {
|
||||
canvas = document.getElementById('signatureCanvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
|
||||
// 캔버스 크기 설정
|
||||
const container = canvas.parentElement;
|
||||
canvas.width = container.clientWidth - 4; // border 제외
|
||||
canvas.height = 300;
|
||||
|
||||
// 그리기 설정
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// 마우스 이벤트
|
||||
canvas.addEventListener('mousedown', startDrawing);
|
||||
canvas.addEventListener('mousemove', draw);
|
||||
canvas.addEventListener('mouseup', stopDrawing);
|
||||
canvas.addEventListener('mouseout', stopDrawing);
|
||||
|
||||
// 터치 이벤트 (모바일, Apple Pencil)
|
||||
canvas.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
canvas.addEventListener('touchend', stopDrawing);
|
||||
canvas.addEventListener('touchcancel', stopDrawing);
|
||||
|
||||
// Pointer Events (Apple Pencil 최적화)
|
||||
if (window.PointerEvent) {
|
||||
canvas.addEventListener('pointerdown', handlePointerDown);
|
||||
canvas.addEventListener('pointermove', handlePointerMove);
|
||||
canvas.addEventListener('pointerup', stopDrawing);
|
||||
canvas.addEventListener('pointercancel', stopDrawing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 그리기 시작 (마우스)
|
||||
*/
|
||||
function startDrawing(e) {
|
||||
isDrawing = true;
|
||||
hasSignature = true;
|
||||
document.getElementById('signaturePlaceholder').style.display = 'none';
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* 그리기 (마우스)
|
||||
*/
|
||||
function draw(e) {
|
||||
if (!isDrawing) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 그리기 중지
|
||||
*/
|
||||
function stopDrawing() {
|
||||
isDrawing = false;
|
||||
ctx.beginPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 터치 시작 처리
|
||||
*/
|
||||
function handleTouchStart(e) {
|
||||
e.preventDefault();
|
||||
isDrawing = true;
|
||||
hasSignature = true;
|
||||
document.getElementById('signaturePlaceholder').style.display = 'none';
|
||||
|
||||
const touch = e.touches[0];
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(touch.clientX - rect.left, touch.clientY - rect.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* 터치 이동 처리
|
||||
*/
|
||||
function handleTouchMove(e) {
|
||||
if (!isDrawing) return;
|
||||
e.preventDefault();
|
||||
|
||||
const touch = e.touches[0];
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.lineTo(touch.clientX - rect.left, touch.clientY - rect.top);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer 시작 처리 (Apple Pencil)
|
||||
*/
|
||||
function handlePointerDown(e) {
|
||||
isDrawing = true;
|
||||
hasSignature = true;
|
||||
document.getElementById('signaturePlaceholder').style.display = 'none';
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointer 이동 처리 (Apple Pencil)
|
||||
*/
|
||||
function handlePointerMove(e) {
|
||||
if (!isDrawing) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 서명 지우기
|
||||
*/
|
||||
function clearSignature() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
hasSignature = false;
|
||||
document.getElementById('signaturePlaceholder').style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 서명을 Base64로 변환
|
||||
*/
|
||||
function getSignatureBase64() {
|
||||
if (!hasSignature) {
|
||||
return null;
|
||||
}
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 서명 저장
|
||||
*/
|
||||
function saveSignature() {
|
||||
if (!hasSignature) {
|
||||
showToast('서명이 없습니다. 이름과 서명을 작성해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const signatureImage = getSignatureBase64();
|
||||
const now = new Date();
|
||||
|
||||
savedSignatures.push({
|
||||
id: Date.now(),
|
||||
image: signatureImage,
|
||||
timestamp: now.toLocaleString('ko-KR')
|
||||
});
|
||||
|
||||
// 서명 카운트 업데이트
|
||||
document.getElementById('signatureCount').textContent = savedSignatures.length;
|
||||
|
||||
// 캔버스 초기화
|
||||
clearSignature();
|
||||
|
||||
// 저장된 서명 목록 렌더링
|
||||
renderSavedSignatures();
|
||||
|
||||
// 교육 완료 버튼 활성화
|
||||
updateCompleteButton();
|
||||
|
||||
showToast('서명이 저장되었습니다.', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 서명 목록 렌더링
|
||||
*/
|
||||
function renderSavedSignatures() {
|
||||
const container = document.getElementById('savedSignatures');
|
||||
|
||||
if (savedSignatures.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h3 style="font-size: var(--text-lg); font-weight: 600; margin-bottom: 16px; color: var(--gray-700);">저장된 서명 목록</h3>';
|
||||
|
||||
savedSignatures.forEach((sig, index) => {
|
||||
html += `
|
||||
<div class="saved-signature-card">
|
||||
<img src="${sig.image}" alt="서명 ${index + 1}">
|
||||
<div class="saved-signature-info">
|
||||
<div class="saved-signature-number">방문자 ${index + 1}</div>
|
||||
<div class="saved-signature-date">저장 시간: ${sig.timestamp}</div>
|
||||
</div>
|
||||
<div class="saved-signature-actions">
|
||||
<button type="button" class="btn btn-sm btn-danger" onclick="deleteSignature(${sig.id})">
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 서명 삭제
|
||||
*/
|
||||
function deleteSignature(signatureId) {
|
||||
if (!confirm('이 서명을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
savedSignatures = savedSignatures.filter(sig => sig.id !== signatureId);
|
||||
|
||||
// 서명 카운트 업데이트
|
||||
document.getElementById('signatureCount').textContent = savedSignatures.length;
|
||||
|
||||
// 목록 다시 렌더링
|
||||
renderSavedSignatures();
|
||||
|
||||
// 교육 완료 버튼 상태 업데이트
|
||||
updateCompleteButton();
|
||||
|
||||
showToast('서명이 삭제되었습니다.', 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 교육 완료 버튼 활성화/비활성화
|
||||
*/
|
||||
function updateCompleteButton() {
|
||||
const completeBtn = document.getElementById('completeBtn');
|
||||
|
||||
// 체크리스트와 서명이 모두 있어야 활성화
|
||||
const checkboxes = document.querySelectorAll('input[name="safety-check"]');
|
||||
const checkedItems = Array.from(checkboxes).filter(cb => cb.checked);
|
||||
const allChecked = checkedItems.length === checkboxes.length;
|
||||
const hasSignatures = savedSignatures.length > 0;
|
||||
|
||||
completeBtn.disabled = !(allChecked && hasSignatures);
|
||||
}
|
||||
|
||||
// ==================== 교육 완료 처리 ====================
|
||||
|
||||
/**
|
||||
* 교육 완료 처리
|
||||
*/
|
||||
async function completeTraining() {
|
||||
// 체크리스트 검증
|
||||
const checkboxes = document.querySelectorAll('input[name="safety-check"]');
|
||||
const checkedItems = Array.from(checkboxes).filter(cb => cb.checked);
|
||||
|
||||
if (checkedItems.length !== checkboxes.length) {
|
||||
showToast('모든 안전교육 항목을 체크해주세요.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 서명 검증
|
||||
if (savedSignatures.length === 0) {
|
||||
showToast('최소 1명 이상의 서명이 필요합니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 확인
|
||||
if (!confirm(`${savedSignatures.length}명의 방문자 안전교육을 완료하시겠습니까?\n완료 후에는 수정할 수 없습니다.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 교육 항목 수집
|
||||
const trainingItems = checkedItems.map(cb => cb.value).join(', ');
|
||||
|
||||
// API 호출
|
||||
const userData = localStorage.getItem('sso_user');
|
||||
const currentUser = userData ? JSON.parse(userData) : null;
|
||||
|
||||
if (!currentUser) {
|
||||
showToast('로그인 정보를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 시간
|
||||
const now = new Date();
|
||||
const currentTime = now.toTimeString().split(' ')[0]; // HH:MM:SS
|
||||
const trainingDate = now.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
// 각 서명에 대해 개별적으로 API 호출
|
||||
let successCount = 0;
|
||||
for (let i = 0; i < savedSignatures.length; i++) {
|
||||
const sig = savedSignatures[i];
|
||||
|
||||
const payload = {
|
||||
request_id: requestId,
|
||||
conducted_by: currentUser.user_id,
|
||||
training_date: trainingDate,
|
||||
training_start_time: currentTime,
|
||||
training_end_time: currentTime,
|
||||
training_items: trainingItems,
|
||||
visitor_name: `방문자 ${i + 1}`, // 순번으로 구분
|
||||
signature_image: sig.image,
|
||||
notes: `교육 완료 - ${checkedItems.length}개 항목 (${i + 1}/${savedSignatures.length})`
|
||||
};
|
||||
|
||||
const response = await window.apiCall(
|
||||
'/workplace-visits/training',
|
||||
'POST',
|
||||
payload
|
||||
);
|
||||
|
||||
if (response && response.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
console.error(`서명 ${i + 1} 저장 실패:`, response);
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount === savedSignatures.length) {
|
||||
showToast(`${successCount}명의 안전교육이 완료되었습니다.`, 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/pages/safety/management.html';
|
||||
}, 1500);
|
||||
} else if (successCount > 0) {
|
||||
showToast(`${successCount}/${savedSignatures.length}명의 교육만 저장되었습니다.`, 'warning');
|
||||
} else {
|
||||
throw new Error('교육 완료 처리 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('교육 완료 처리 오류:', error);
|
||||
showToast(error.message || '교육 완료 처리 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 뒤로 가기
|
||||
*/
|
||||
function goBack() {
|
||||
if (hasSignature || document.querySelector('input[name="safety-check"]:checked')) {
|
||||
if (!confirm('작성 중인 내용이 있습니다. 정말 나가시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
window.location.href = '/pages/safety/management.html';
|
||||
}
|
||||
|
||||
// 전역 함수로 노출
|
||||
window.clearSignature = clearSignature;
|
||||
window.saveSignature = saveSignature;
|
||||
window.deleteSignature = deleteSignature;
|
||||
window.updateCompleteButton = updateCompleteButton;
|
||||
window.completeTraining = completeTraining;
|
||||
window.goBack = goBack;
|
||||
@@ -1,443 +0,0 @@
|
||||
// 출입 신청 페이지 JavaScript
|
||||
|
||||
let categories = [];
|
||||
let workplaces = [];
|
||||
let mapRegions = [];
|
||||
let visitPurposes = [];
|
||||
let selectedWorkplace = null;
|
||||
let selectedCategory = null;
|
||||
let canvas = null;
|
||||
let ctx = null;
|
||||
let layoutImage = null;
|
||||
|
||||
// showToast, createToastContainer → api-base.js 전역 사용
|
||||
|
||||
// ==================== 초기화 ====================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// 오늘 날짜 기본값 설정
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('visitDate').value = today;
|
||||
document.getElementById('visitDate').min = today;
|
||||
|
||||
// 현재 시간 + 1시간 기본값 설정
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + 1);
|
||||
const timeString = now.toTimeString().slice(0, 5);
|
||||
document.getElementById('visitTime').value = timeString;
|
||||
|
||||
// 데이터 로드
|
||||
await loadCategories();
|
||||
await loadVisitPurposes();
|
||||
await loadMyRequests();
|
||||
|
||||
// 폼 제출 이벤트
|
||||
document.getElementById('visitRequestForm').addEventListener('submit', handleSubmit);
|
||||
|
||||
// 캔버스 초기화
|
||||
canvas = document.getElementById('workplaceMapCanvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
});
|
||||
|
||||
// ==================== 데이터 로드 ====================
|
||||
|
||||
/**
|
||||
* 카테고리(공장) 목록 로드
|
||||
*/
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplaces/categories', 'GET');
|
||||
if (response && response.success) {
|
||||
categories = response.data || [];
|
||||
|
||||
const categorySelect = document.getElementById('categorySelect');
|
||||
categorySelect.innerHTML = '<option value="">구역을 선택하세요</option>';
|
||||
|
||||
categories.forEach(cat => {
|
||||
if (cat.is_active) {
|
||||
const option = document.createElement('option');
|
||||
option.value = cat.category_id;
|
||||
option.textContent = cat.category_name;
|
||||
categorySelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('카테고리 로드 오류:', error);
|
||||
window.showToast('카테고리 로드 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 방문 목적 목록 로드
|
||||
*/
|
||||
async function loadVisitPurposes() {
|
||||
try {
|
||||
const response = await window.apiCall('/workplace-visits/purposes/active', 'GET');
|
||||
if (response && response.success) {
|
||||
visitPurposes = response.data || [];
|
||||
|
||||
const purposeSelect = document.getElementById('visitPurpose');
|
||||
purposeSelect.innerHTML = '<option value="">선택하세요</option>';
|
||||
|
||||
visitPurposes.forEach(purpose => {
|
||||
const option = document.createElement('option');
|
||||
option.value = purpose.purpose_id;
|
||||
option.textContent = purpose.purpose_name;
|
||||
purposeSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('방문 목적 로드 오류:', error);
|
||||
window.showToast('방문 목적 로드 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 내 출입 신청 목록 로드
|
||||
*/
|
||||
async function loadMyRequests() {
|
||||
try {
|
||||
// localStorage에서 사용자 정보 가져오기
|
||||
const userData = localStorage.getItem('sso_user');
|
||||
const currentUser = userData ? JSON.parse(userData) : null;
|
||||
|
||||
if (!currentUser || !currentUser.user_id) {
|
||||
console.log('사용자 정보 없음');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await window.apiCall(`/workplace-visits/requests?requester_id=${currentUser.user_id}`, 'GET');
|
||||
|
||||
if (response && response.success) {
|
||||
const requests = response.data || [];
|
||||
renderMyRequests(requests);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('내 신청 목록 로드 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 내 신청 목록 렌더링
|
||||
*/
|
||||
function renderMyRequests(requests) {
|
||||
const listDiv = document.getElementById('myRequestsList');
|
||||
|
||||
if (requests.length === 0) {
|
||||
listDiv.innerHTML = '<p style="text-align: center; color: var(--gray-500); padding: 32px;">신청 내역이 없습니다</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
requests.forEach(req => {
|
||||
const statusText = {
|
||||
'pending': '승인 대기',
|
||||
'approved': '승인됨',
|
||||
'rejected': '반려됨',
|
||||
'training_completed': '교육 완료'
|
||||
}[req.status] || req.status;
|
||||
|
||||
html += `
|
||||
<div class="request-card">
|
||||
<div class="request-card-header">
|
||||
<h3 style="margin: 0; font-size: var(--text-lg);">${req.visitor_company} (${req.visitor_count}명)</h3>
|
||||
<span class="request-status ${req.status}">${statusText}</span>
|
||||
</div>
|
||||
<div class="request-info">
|
||||
<div class="info-item">
|
||||
<span class="info-label">방문 작업장</span>
|
||||
<span class="info-value">${req.category_name} - ${req.workplace_name}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">방문 일시</span>
|
||||
<span class="info-value">${req.visit_date} ${req.visit_time}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">방문 목적</span>
|
||||
<span class="info-value">${req.purpose_name}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">신청일</span>
|
||||
<span class="info-value">${new Date(req.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
${req.rejection_reason ? `<p style="margin-top: 12px; padding: 12px; background: var(--red-50); color: var(--red-700); border-radius: var(--radius-md); font-size: var(--text-sm);"><strong>반려 사유:</strong> ${req.rejection_reason}</p>` : ''}
|
||||
${req.notes ? `<p style="margin-top: 12px; color: var(--gray-600); font-size: var(--text-sm);"><strong>비고:</strong> ${req.notes}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
listDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
// ==================== 작업장 지도 모달 ====================
|
||||
|
||||
/**
|
||||
* 지도 모달 열기
|
||||
*/
|
||||
function openMapModal() {
|
||||
document.getElementById('mapModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* 지도 모달 닫기
|
||||
*/
|
||||
function closeMapModal() {
|
||||
document.getElementById('mapModal').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 지도 로드
|
||||
*/
|
||||
async function loadWorkplaceMap() {
|
||||
const categoryId = document.getElementById('categorySelect').value;
|
||||
if (!categoryId) {
|
||||
document.getElementById('mapCanvasContainer').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
selectedCategory = categories.find(c => c.category_id == categoryId);
|
||||
|
||||
try {
|
||||
// 작업장 목록 로드
|
||||
const workplacesResponse = await window.apiCall(`/workplaces/categories/${categoryId}`, 'GET');
|
||||
if (workplacesResponse && workplacesResponse.success) {
|
||||
workplaces = workplacesResponse.data || [];
|
||||
}
|
||||
|
||||
// 지도 영역 로드
|
||||
const regionsResponse = await window.apiCall(`/workplaces/categories/${categoryId}/map-regions`, 'GET');
|
||||
if (regionsResponse && regionsResponse.success) {
|
||||
mapRegions = regionsResponse.data || [];
|
||||
}
|
||||
|
||||
// 레이아웃 이미지가 있으면 표시
|
||||
if (selectedCategory && selectedCategory.layout_image) {
|
||||
// API_BASE_URL에서 /api 제거하고 이미지 경로 생성
|
||||
const baseUrl = (window.API_BASE_URL || 'http://localhost:30005').replace('/api', '');
|
||||
const fullImageUrl = selectedCategory.layout_image.startsWith('http')
|
||||
? selectedCategory.layout_image
|
||||
: `${baseUrl}${selectedCategory.layout_image}`;
|
||||
|
||||
console.log('이미지 URL:', fullImageUrl);
|
||||
loadImageToCanvas(fullImageUrl);
|
||||
document.getElementById('mapCanvasContainer').style.display = 'block';
|
||||
} else {
|
||||
window.showToast('선택한 구역에 레이아웃 지도가 없습니다.', 'warning');
|
||||
document.getElementById('mapCanvasContainer').style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업장 지도 로드 오류:', error);
|
||||
window.showToast('작업장 지도 로드 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지를 캔버스에 로드
|
||||
*/
|
||||
function loadImageToCanvas(imagePath) {
|
||||
const img = new Image();
|
||||
// crossOrigin 제거 - 같은 도메인이므로 불필요
|
||||
img.onload = function() {
|
||||
// 캔버스 크기 설정
|
||||
const maxWidth = 800;
|
||||
const scale = img.width > maxWidth ? maxWidth / img.width : 1;
|
||||
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
|
||||
// 이미지 그리기
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
layoutImage = img;
|
||||
|
||||
// 영역 표시
|
||||
drawRegions();
|
||||
|
||||
// 클릭 이벤트 등록
|
||||
canvas.onclick = handleCanvasClick;
|
||||
};
|
||||
img.onerror = function() {
|
||||
window.showToast('지도 이미지를 불러올 수 없습니다.', 'error');
|
||||
};
|
||||
img.src = imagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 지도 영역 그리기
|
||||
*/
|
||||
function drawRegions() {
|
||||
mapRegions.forEach(region => {
|
||||
const x1 = (region.x_start / 100) * canvas.width;
|
||||
const y1 = (region.y_start / 100) * canvas.height;
|
||||
const x2 = (region.x_end / 100) * canvas.width;
|
||||
const y2 = (region.y_end / 100) * canvas.height;
|
||||
|
||||
// 영역 박스
|
||||
ctx.strokeStyle = '#10b981';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
|
||||
|
||||
ctx.fillStyle = 'rgba(16, 185, 129, 0.1)';
|
||||
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
|
||||
|
||||
// 작업장 이름
|
||||
ctx.fillStyle = '#10b981';
|
||||
ctx.font = 'bold 14px sans-serif';
|
||||
ctx.fillText(region.workplace_name || '', x1 + 5, y1 + 20);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 캔버스 클릭 핸들러
|
||||
*/
|
||||
function handleCanvasClick(event) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// 클릭한 위치의 영역 찾기
|
||||
for (const region of mapRegions) {
|
||||
const x1 = (region.x_start / 100) * canvas.width;
|
||||
const y1 = (region.y_start / 100) * canvas.height;
|
||||
const x2 = (region.x_end / 100) * canvas.width;
|
||||
const y2 = (region.y_end / 100) * canvas.height;
|
||||
|
||||
if (x >= x1 && x <= x2 && y >= y1 && y <= y2) {
|
||||
// 작업장 선택
|
||||
selectWorkplace(region);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.showToast('작업장 영역을 클릭해주세요.', 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 선택
|
||||
*/
|
||||
function selectWorkplace(region) {
|
||||
selectedWorkplace = {
|
||||
workplace_id: region.workplace_id,
|
||||
workplace_name: region.workplace_name,
|
||||
category_id: selectedCategory.category_id,
|
||||
category_name: selectedCategory.category_name
|
||||
};
|
||||
|
||||
// 선택 표시
|
||||
const selectionDiv = document.getElementById('workplaceSelection');
|
||||
selectionDiv.classList.add('selected');
|
||||
selectionDiv.innerHTML = `
|
||||
<div class="icon">✅</div>
|
||||
<div class="text">${selectedCategory.category_name} - ${region.workplace_name}</div>
|
||||
`;
|
||||
|
||||
// 상세 정보 카드 표시
|
||||
const infoDiv = document.getElementById('selectedWorkplaceInfo');
|
||||
infoDiv.style.display = 'block';
|
||||
infoDiv.innerHTML = `
|
||||
<div class="workplace-info-card">
|
||||
<div class="icon">📍</div>
|
||||
<div class="details">
|
||||
<div class="name">${region.workplace_name}</div>
|
||||
<div class="category">${selectedCategory.category_name}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="clearWorkplaceSelection()">변경</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 모달 닫기
|
||||
closeMapModal();
|
||||
|
||||
window.showToast(`${region.workplace_name} 작업장이 선택되었습니다.`, 'success');
|
||||
}
|
||||
|
||||
/**
|
||||
* 작업장 선택 초기화
|
||||
*/
|
||||
function clearWorkplaceSelection() {
|
||||
selectedWorkplace = null;
|
||||
|
||||
const selectionDiv = document.getElementById('workplaceSelection');
|
||||
selectionDiv.classList.remove('selected');
|
||||
selectionDiv.innerHTML = `
|
||||
<div class="icon">📍</div>
|
||||
<div class="text">지도에서 작업장을 선택하세요</div>
|
||||
`;
|
||||
|
||||
document.getElementById('selectedWorkplaceInfo').style.display = 'none';
|
||||
}
|
||||
|
||||
// ==================== 폼 제출 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 제출
|
||||
*/
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!selectedWorkplace) {
|
||||
window.showToast('작업장을 선택해주세요.', 'warning');
|
||||
openMapModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
visitor_company: document.getElementById('visitorCompany').value.trim(),
|
||||
visitor_count: parseInt(document.getElementById('visitorCount').value),
|
||||
category_id: selectedWorkplace.category_id,
|
||||
workplace_id: selectedWorkplace.workplace_id,
|
||||
visit_date: document.getElementById('visitDate').value,
|
||||
visit_time: document.getElementById('visitTime').value,
|
||||
purpose_id: parseInt(document.getElementById('visitPurpose').value),
|
||||
notes: document.getElementById('notes').value.trim() || null
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await window.apiCall('/workplace-visits/requests', 'POST', formData);
|
||||
|
||||
if (response && response.success) {
|
||||
window.showToast('출입 신청 및 안전교육 신청이 완료되었습니다. 안전관리자의 승인을 기다려주세요.', 'success');
|
||||
|
||||
// 폼 초기화
|
||||
resetForm();
|
||||
|
||||
// 내 신청 목록 새로고침
|
||||
await loadMyRequests();
|
||||
} else {
|
||||
throw new Error(response?.message || '신청 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('출입 신청 오류:', error);
|
||||
window.showToast(error.message || '출입 신청 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 초기화
|
||||
*/
|
||||
function resetForm() {
|
||||
document.getElementById('visitRequestForm').reset();
|
||||
clearWorkplaceSelection();
|
||||
|
||||
// 오늘 날짜와 시간 다시 설정
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('visitDate').value = today;
|
||||
|
||||
const now = new Date();
|
||||
now.setHours(now.getHours() + 1);
|
||||
const timeString = now.toTimeString().slice(0, 5);
|
||||
document.getElementById('visitTime').value = timeString;
|
||||
|
||||
document.getElementById('visitorCount').value = 1;
|
||||
}
|
||||
|
||||
// 전역 함수로 노출
|
||||
window.openMapModal = openMapModal;
|
||||
window.closeMapModal = closeMapModal;
|
||||
window.loadWorkplaceMap = loadWorkplaceMap;
|
||||
window.clearWorkplaceSelection = clearWorkplaceSelection;
|
||||
window.resetForm = resetForm;
|
||||
@@ -1,582 +0,0 @@
|
||||
// 작업자 관리 페이지 JavaScript (부서 기반)
|
||||
|
||||
// 전역 변수
|
||||
let departments = [];
|
||||
let currentDepartmentId = null;
|
||||
let allWorkers = [];
|
||||
let filteredWorkers = [];
|
||||
let currentEditingWorker = null;
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await waitForApi();
|
||||
await loadDepartments();
|
||||
});
|
||||
|
||||
// waitForApi → api-base.js 전역 사용
|
||||
|
||||
// ============================================
|
||||
// 부서 관련 함수
|
||||
// ============================================
|
||||
|
||||
// 부서 목록 로드
|
||||
async function loadDepartments() {
|
||||
try {
|
||||
const response = await window.apiCall('/departments');
|
||||
const result = response;
|
||||
|
||||
if (result && result.success) {
|
||||
departments = result.data;
|
||||
renderDepartmentList();
|
||||
updateParentDepartmentSelect();
|
||||
} else if (Array.isArray(result)) {
|
||||
departments = result;
|
||||
renderDepartmentList();
|
||||
updateParentDepartmentSelect();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('부서 목록 로드 실패:', error);
|
||||
showToast('부서 목록을 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 부서 목록 렌더링
|
||||
function renderDepartmentList() {
|
||||
const container = document.getElementById('departmentList');
|
||||
if (!container) return;
|
||||
|
||||
if (departments.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div style="text-align: center; padding: 2rem; color: #9ca3af;">
|
||||
등록된 부서가 없습니다.<br>
|
||||
<button class="btn btn-primary btn-sm" style="margin-top: 1rem;" onclick="openDepartmentModal()">
|
||||
첫 부서 등록하기
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = departments.map(dept => {
|
||||
const safeDeptId = parseInt(dept.department_id) || 0;
|
||||
const safeDeptName = escapeHtml(dept.department_name || '-');
|
||||
const workerCount = parseInt(dept.worker_count) || 0;
|
||||
return `
|
||||
<div class="department-item ${currentDepartmentId === dept.department_id ? 'active' : ''}"
|
||||
onclick="selectDepartment(${safeDeptId})">
|
||||
<div class="department-info">
|
||||
<span class="department-name">${safeDeptName}</span>
|
||||
<span class="department-count">${workerCount}명</span>
|
||||
</div>
|
||||
<div class="department-actions" onclick="event.stopPropagation()">
|
||||
<button class="btn-icon" onclick="editDepartment(${safeDeptId})" title="수정">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon danger" onclick="confirmDeleteDepartment(${safeDeptId})" title="삭제">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 부서 선택
|
||||
async function selectDepartment(departmentId) {
|
||||
currentDepartmentId = departmentId;
|
||||
renderDepartmentList();
|
||||
|
||||
const dept = departments.find(d => d.department_id === departmentId);
|
||||
document.getElementById('workerListTitle').textContent = `${dept.department_name} 작업자`;
|
||||
document.getElementById('addWorkerBtn').style.display = 'inline-flex';
|
||||
document.getElementById('workerToolbar').style.display = 'flex';
|
||||
|
||||
await loadWorkersByDepartment(departmentId);
|
||||
}
|
||||
|
||||
// 상위 부서 선택 옵션 업데이트
|
||||
function updateParentDepartmentSelect() {
|
||||
const select = document.getElementById('parentDepartment');
|
||||
if (!select) return;
|
||||
|
||||
const currentId = document.getElementById('departmentId')?.value;
|
||||
|
||||
select.innerHTML = '<option value="">없음 (최상위 부서)</option>' +
|
||||
departments
|
||||
.filter(d => d.department_id !== parseInt(currentId))
|
||||
.map(d => {
|
||||
const safeDeptId = parseInt(d.department_id) || 0;
|
||||
return `<option value="${safeDeptId}">${escapeHtml(d.department_name || '-')}</option>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 부서 모달 열기
|
||||
function openDepartmentModal(departmentId = null) {
|
||||
const modal = document.getElementById('departmentModal');
|
||||
const title = document.getElementById('departmentModalTitle');
|
||||
const form = document.getElementById('departmentForm');
|
||||
const deleteBtn = document.getElementById('deleteDeptBtn');
|
||||
|
||||
updateParentDepartmentSelect();
|
||||
|
||||
if (departmentId) {
|
||||
const dept = departments.find(d => d.department_id === departmentId);
|
||||
title.textContent = '부서 수정';
|
||||
deleteBtn.style.display = 'inline-flex';
|
||||
document.getElementById('departmentId').value = dept.department_id;
|
||||
document.getElementById('departmentName').value = dept.department_name;
|
||||
document.getElementById('parentDepartment').value = dept.parent_id || '';
|
||||
document.getElementById('departmentDescription').value = dept.description || '';
|
||||
document.getElementById('displayOrder').value = dept.display_order || 0;
|
||||
document.getElementById('isActiveDept').checked = dept.is_active !== 0 && dept.is_active !== false;
|
||||
} else {
|
||||
title.textContent = '새 부서 등록';
|
||||
deleteBtn.style.display = 'none';
|
||||
form.reset();
|
||||
document.getElementById('departmentId').value = '';
|
||||
document.getElementById('isActiveDept').checked = true;
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('departmentName').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 부서 모달 닫기
|
||||
function closeDepartmentModal() {
|
||||
const modal = document.getElementById('departmentModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 부서 저장
|
||||
async function saveDepartment() {
|
||||
const departmentId = document.getElementById('departmentId').value;
|
||||
const data = {
|
||||
department_name: document.getElementById('departmentName').value.trim(),
|
||||
parent_id: document.getElementById('parentDepartment').value || null,
|
||||
description: document.getElementById('departmentDescription').value.trim(),
|
||||
display_order: parseInt(document.getElementById('displayOrder').value) || 0,
|
||||
is_active: document.getElementById('isActiveDept').checked
|
||||
};
|
||||
|
||||
if (!data.department_name) {
|
||||
showToast('부서명은 필수 입력 항목입니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = departmentId ? `/departments/${departmentId}` : '/departments';
|
||||
const method = departmentId ? 'PUT' : 'POST';
|
||||
|
||||
const response = await window.apiCall(url, method, data);
|
||||
|
||||
if (response && response.success) {
|
||||
showToast(response.message || '부서가 저장되었습니다.', 'success');
|
||||
closeDepartmentModal();
|
||||
await loadDepartments();
|
||||
} else {
|
||||
throw new Error(response?.error || '저장 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('부서 저장 실패:', error);
|
||||
showToast('부서 저장에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 부서 수정
|
||||
function editDepartment(departmentId) {
|
||||
openDepartmentModal(departmentId);
|
||||
}
|
||||
|
||||
// 부서 삭제 확인
|
||||
function confirmDeleteDepartment(departmentId) {
|
||||
const dept = departments.find(d => d.department_id === departmentId);
|
||||
if (!dept) return;
|
||||
|
||||
const workerCount = dept.worker_count || 0;
|
||||
let message = `"${dept.department_name}" 부서를 삭제하시겠습니까?`;
|
||||
|
||||
if (workerCount > 0) {
|
||||
message += `\n\n⚠️ 이 부서에는 ${workerCount}명의 작업자가 있습니다.\n삭제하면 작업자들의 부서 정보가 제거됩니다.`;
|
||||
}
|
||||
|
||||
if (confirm(message)) {
|
||||
deleteDepartment(departmentId);
|
||||
}
|
||||
}
|
||||
|
||||
// 부서 삭제
|
||||
async function deleteDepartment(departmentId = null) {
|
||||
const id = departmentId || document.getElementById('departmentId').value;
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
const response = await window.apiCall(`/departments/${id}`, 'DELETE');
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('부서가 삭제되었습니다.', 'success');
|
||||
closeDepartmentModal();
|
||||
|
||||
if (currentDepartmentId === parseInt(id)) {
|
||||
currentDepartmentId = null;
|
||||
document.getElementById('workerListTitle').textContent = '부서를 선택하세요';
|
||||
document.getElementById('addWorkerBtn').style.display = 'none';
|
||||
document.getElementById('workerToolbar').style.display = 'none';
|
||||
document.getElementById('workerList').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h4>부서를 선택해주세요</h4>
|
||||
<p>왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
await loadDepartments();
|
||||
} else {
|
||||
throw new Error(response?.error || '삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('부서 삭제 실패:', error);
|
||||
showToast(error.message || '부서 삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 작업자 관련 함수
|
||||
// ============================================
|
||||
|
||||
// 부서별 작업자 로드
|
||||
async function loadWorkersByDepartment(departmentId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/departments/${departmentId}/workers`);
|
||||
|
||||
if (response && response.success) {
|
||||
allWorkers = response.data;
|
||||
filteredWorkers = [...allWorkers];
|
||||
renderWorkerList();
|
||||
} else if (Array.isArray(response)) {
|
||||
allWorkers = response;
|
||||
filteredWorkers = [...allWorkers];
|
||||
renderWorkerList();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업자 목록 로드 실패:', error);
|
||||
showToast('작업자 목록을 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 목록 렌더링
|
||||
function renderWorkerList() {
|
||||
const container = document.getElementById('workerList');
|
||||
if (!container) return;
|
||||
|
||||
if (filteredWorkers.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<h4>작업자가 없습니다</h4>
|
||||
<p>"+ 작업자 추가" 버튼을 눌러 작업자를 등록하세요.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const tableHtml = `
|
||||
<table class="workers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>이름</th>
|
||||
<th>직책</th>
|
||||
<th>상태</th>
|
||||
<th>입사일</th>
|
||||
<th>계정</th>
|
||||
<th style="width: 100px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${filteredWorkers.map(worker => {
|
||||
const jobTypeMap = {
|
||||
'worker': '작업자',
|
||||
'leader': '그룹장',
|
||||
'admin': '관리자'
|
||||
};
|
||||
const safeJobType = ['worker', 'leader', 'admin'].includes(worker.job_type) ? worker.job_type : '';
|
||||
const jobType = jobTypeMap[safeJobType] || escapeHtml(worker.job_type || '-');
|
||||
|
||||
const isInactive = worker.status === 'inactive';
|
||||
const isResigned = worker.employment_status === 'resigned';
|
||||
const hasAccount = worker.user_id !== null && worker.user_id !== undefined;
|
||||
|
||||
let statusClass = 'active';
|
||||
let statusText = '현장직';
|
||||
if (isResigned) {
|
||||
statusClass = 'resigned';
|
||||
statusText = '퇴사';
|
||||
} else if (isInactive) {
|
||||
statusClass = 'inactive';
|
||||
statusText = '사무직';
|
||||
}
|
||||
|
||||
const safeWorkerId = parseInt(worker.user_id) || 0;
|
||||
const safeWorkerName = escapeHtml(worker.worker_name || '');
|
||||
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
|
||||
|
||||
return `
|
||||
<tr style="${isResigned ? 'opacity: 0.6;' : ''}">
|
||||
<td>
|
||||
<div class="worker-name-cell">
|
||||
<div class="worker-avatar">${firstChar}</div>
|
||||
<span style="font-weight: 500;">${safeWorkerName}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>${jobType}</td>
|
||||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||||
<td>${worker.join_date ? formatDate(worker.join_date) : '-'}</td>
|
||||
<td>
|
||||
<span class="account-badge ${hasAccount ? 'has-account' : 'no-account'}">
|
||||
${hasAccount ? '🔐 연동' : '⚪ 없음'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 0.25rem; justify-content: center;">
|
||||
<button class="btn-icon" onclick="editWorker(${safeWorkerId})" title="수정">✏️</button>
|
||||
<button class="btn-icon danger" onclick="confirmDeleteWorker(${safeWorkerId})" title="삭제">🗑️</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
container.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
// 작업자 필터링
|
||||
function filterWorkers() {
|
||||
const searchTerm = document.getElementById('workerSearch')?.value.toLowerCase().trim() || '';
|
||||
const statusValue = document.getElementById('statusFilter')?.value || '';
|
||||
|
||||
filteredWorkers = allWorkers.filter(worker => {
|
||||
// 검색 필터
|
||||
const matchesSearch = !searchTerm ||
|
||||
worker.worker_name.toLowerCase().includes(searchTerm) ||
|
||||
(worker.job_type && worker.job_type.toLowerCase().includes(searchTerm));
|
||||
|
||||
// 상태 필터
|
||||
let matchesStatus = true;
|
||||
if (statusValue === 'active') {
|
||||
matchesStatus = worker.status !== 'inactive' && worker.employment_status !== 'resigned';
|
||||
} else if (statusValue === 'inactive') {
|
||||
matchesStatus = worker.status === 'inactive';
|
||||
} else if (statusValue === 'resigned') {
|
||||
matchesStatus = worker.employment_status === 'resigned';
|
||||
}
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
renderWorkerList();
|
||||
}
|
||||
|
||||
// 작업자 모달 열기
|
||||
function openWorkerModal(workerId = null) {
|
||||
const modal = document.getElementById('workerModal');
|
||||
const title = document.getElementById('workerModalTitle');
|
||||
const deleteBtn = document.getElementById('deleteWorkerBtn');
|
||||
|
||||
if (!currentDepartmentId) {
|
||||
showToast('먼저 부서를 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (workerId) {
|
||||
const worker = allWorkers.find(w => w.user_id === workerId);
|
||||
if (!worker) {
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
currentEditingWorker = worker;
|
||||
title.textContent = '작업자 정보 수정';
|
||||
deleteBtn.style.display = 'inline-flex';
|
||||
|
||||
document.getElementById('workerId').value = worker.user_id;
|
||||
document.getElementById('workerName').value = worker.worker_name || '';
|
||||
document.getElementById('jobType').value = worker.job_type || 'worker';
|
||||
document.getElementById('joinDate').value = worker.join_date ? worker.join_date.split('T')[0] : '';
|
||||
document.getElementById('salary').value = worker.salary || '';
|
||||
document.getElementById('annualLeave').value = worker.annual_leave || 0;
|
||||
document.getElementById('isActiveWorker').checked = worker.status !== 'inactive';
|
||||
document.getElementById('createAccount').checked = worker.user_id !== null && worker.user_id !== undefined;
|
||||
document.getElementById('isResigned').checked = worker.employment_status === 'resigned';
|
||||
} else {
|
||||
currentEditingWorker = null;
|
||||
title.textContent = '새 작업자 등록';
|
||||
deleteBtn.style.display = 'none';
|
||||
|
||||
document.getElementById('workerForm').reset();
|
||||
document.getElementById('workerId').value = '';
|
||||
document.getElementById('isActiveWorker').checked = true;
|
||||
document.getElementById('createAccount').checked = false;
|
||||
document.getElementById('isResigned').checked = false;
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('workerName').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 작업자 모달 닫기
|
||||
function closeWorkerModal() {
|
||||
const modal = document.getElementById('workerModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
currentEditingWorker = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 편집
|
||||
function editWorker(workerId) {
|
||||
openWorkerModal(workerId);
|
||||
}
|
||||
|
||||
// 작업자 저장
|
||||
async function saveWorker() {
|
||||
const workerId = document.getElementById('workerId').value;
|
||||
|
||||
const workerData = {
|
||||
worker_name: document.getElementById('workerName').value.trim(),
|
||||
job_type: document.getElementById('jobType').value || 'worker',
|
||||
join_date: document.getElementById('joinDate').value || null,
|
||||
salary: document.getElementById('salary').value || null,
|
||||
annual_leave: document.getElementById('annualLeave').value || 0,
|
||||
status: document.getElementById('isActiveWorker').checked ? 'active' : 'inactive',
|
||||
employment_status: document.getElementById('isResigned').checked ? 'resigned' : 'employed',
|
||||
create_account: document.getElementById('createAccount').checked,
|
||||
department_id: currentDepartmentId
|
||||
};
|
||||
|
||||
if (!workerData.worker_name) {
|
||||
showToast('작업자명은 필수 입력 항목입니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (workerId) {
|
||||
response = await window.apiCall(`/workers/${workerId}`, 'PUT', workerData);
|
||||
} else {
|
||||
response = await window.apiCall('/workers', 'POST', workerData);
|
||||
}
|
||||
|
||||
if (response && (response.success || response.data)) {
|
||||
const action = workerId ? '수정' : '등록';
|
||||
showToast(`작업자가 성공적으로 ${action}되었습니다.`, 'success');
|
||||
|
||||
closeWorkerModal();
|
||||
await loadDepartments();
|
||||
await loadWorkersByDepartment(currentDepartmentId);
|
||||
} else {
|
||||
throw new Error(response?.message || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업자 저장 오류:', error);
|
||||
showToast(error.message || '작업자 저장 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 삭제 확인
|
||||
function confirmDeleteWorker(workerId) {
|
||||
const worker = allWorkers.find(w => w.user_id === workerId);
|
||||
if (!worker) {
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessage = `"${worker.worker_name}" 작업자를 삭제하시겠습니까?\n\n⚠️ 관련된 모든 데이터(작업보고서, 이슈 등)가 함께 삭제됩니다.`;
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
deleteWorkerById(workerId);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 삭제 (모달에서)
|
||||
function deleteWorker() {
|
||||
if (currentEditingWorker) {
|
||||
confirmDeleteWorker(currentEditingWorker.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 삭제 실행
|
||||
async function deleteWorkerById(workerId) {
|
||||
try {
|
||||
const response = await window.apiCall(`/workers/${workerId}`, 'DELETE');
|
||||
|
||||
if (response && (response.success || response.message)) {
|
||||
showToast('작업자가 삭제되었습니다.', 'success');
|
||||
|
||||
closeWorkerModal();
|
||||
await loadDepartments();
|
||||
await loadWorkersByDepartment(currentDepartmentId);
|
||||
} else {
|
||||
throw new Error(response?.error || '삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업자 삭제 오류:', error);
|
||||
showToast(error.message || '작업자 삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 유틸리티 함수
|
||||
// ============================================
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
// showToast → api-base.js 전역 사용
|
||||
|
||||
// ============================================
|
||||
// 전역 함수 노출
|
||||
// ============================================
|
||||
window.openDepartmentModal = openDepartmentModal;
|
||||
window.closeDepartmentModal = closeDepartmentModal;
|
||||
window.saveDepartment = saveDepartment;
|
||||
window.editDepartment = editDepartment;
|
||||
window.deleteDepartment = deleteDepartment;
|
||||
window.confirmDeleteDepartment = confirmDeleteDepartment;
|
||||
window.selectDepartment = selectDepartment;
|
||||
|
||||
window.openWorkerModal = openWorkerModal;
|
||||
window.closeWorkerModal = closeWorkerModal;
|
||||
window.saveWorker = saveWorker;
|
||||
window.editWorker = editWorker;
|
||||
window.deleteWorker = deleteWorker;
|
||||
window.confirmDeleteWorker = confirmDeleteWorker;
|
||||
window.filterWorkers = filterWorkers;
|
||||
@@ -10,9 +10,6 @@ let canvasImage = null;
|
||||
// 금일 TBM 작업자 데이터
|
||||
let todayWorkers = [];
|
||||
|
||||
// 금일 출입 신청 데이터
|
||||
let todayVisitors = [];
|
||||
|
||||
// ==================== 초기화 ====================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
@@ -175,8 +172,6 @@ async function loadTodayData() {
|
||||
// TBM 작업자 데이터 로드
|
||||
await loadTodayWorkers(today);
|
||||
|
||||
// 출입 신청 데이터 로드
|
||||
await loadTodayVisitors(today);
|
||||
}
|
||||
|
||||
async function loadTodayWorkers(date) {
|
||||
@@ -212,43 +207,6 @@ async function loadTodayWorkers(date) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTodayVisitors(date) {
|
||||
try {
|
||||
// 날짜 형식 확인 (YYYY-MM-DD)
|
||||
const formattedDate = date.split('T')[0];
|
||||
|
||||
const response = await window.apiCall(`/workplace-visits/requests`, 'GET');
|
||||
|
||||
if (response && response.success) {
|
||||
const requests = response.data || [];
|
||||
|
||||
// 금일 날짜와 승인된 요청 필터링
|
||||
todayVisitors = requests.filter(req => {
|
||||
// UTC 변환 없이 로컬 날짜로 비교
|
||||
const visitDateObj = new Date(req.visit_date);
|
||||
const visitYear = visitDateObj.getFullYear();
|
||||
const visitMonth = String(visitDateObj.getMonth() + 1).padStart(2, '0');
|
||||
const visitDay = String(visitDateObj.getDate()).padStart(2, '0');
|
||||
const visitDate = `${visitYear}-${visitMonth}-${visitDay}`;
|
||||
|
||||
return visitDate === formattedDate &&
|
||||
(req.status === 'approved' || req.status === 'training_completed');
|
||||
}).map(req => ({
|
||||
workplace_id: req.workplace_id,
|
||||
visitor_company: req.visitor_company,
|
||||
visitor_count: req.visitor_count,
|
||||
visit_time: req.visit_time,
|
||||
purpose_name: req.purpose_name,
|
||||
status: req.status
|
||||
}));
|
||||
|
||||
console.log('로드된 방문자:', todayVisitors);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('출입 신청 데이터 로드 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 지도 렌더링 ====================
|
||||
|
||||
function renderMap() {
|
||||
@@ -260,19 +218,17 @@ function renderMap() {
|
||||
|
||||
// 모든 작업장 영역 표시
|
||||
mapRegions.forEach(region => {
|
||||
// 해당 작업장의 작업자/방문자 인원 계산
|
||||
// 해당 작업장의 작업자 인원 계산
|
||||
const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id);
|
||||
const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id);
|
||||
|
||||
const totalWorkerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0);
|
||||
const totalVisitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0);
|
||||
|
||||
// 영역 그리기
|
||||
drawWorkplaceRegion(region, totalWorkerCount, totalVisitorCount);
|
||||
drawWorkplaceRegion(region, totalWorkerCount);
|
||||
});
|
||||
}
|
||||
|
||||
function drawWorkplaceRegion(region, workerCount, visitorCount) {
|
||||
function drawWorkplaceRegion(region, workerCount) {
|
||||
// 사각형 좌표 변환
|
||||
const x1 = (region.x_start / 100) * canvas.width;
|
||||
const y1 = (region.y_start / 100) * canvas.height;
|
||||
@@ -286,20 +242,12 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) {
|
||||
|
||||
// 색상 결정
|
||||
let fillColor, strokeColor;
|
||||
const hasActivity = workerCount > 0 || visitorCount > 0;
|
||||
const hasActivity = workerCount > 0;
|
||||
|
||||
if (workerCount > 0 && visitorCount > 0) {
|
||||
// 둘 다 있음 - 초록색
|
||||
fillColor = 'rgba(34, 197, 94, 0.3)';
|
||||
strokeColor = 'rgb(34, 197, 94)';
|
||||
} else if (workerCount > 0) {
|
||||
// 내부 작업자만 - 파란색
|
||||
if (workerCount > 0) {
|
||||
// 작업자 있음 - 파란색
|
||||
fillColor = 'rgba(59, 130, 246, 0.3)';
|
||||
strokeColor = 'rgb(59, 130, 246)';
|
||||
} else if (visitorCount > 0) {
|
||||
// 외부 방문자만 - 보라색
|
||||
fillColor = 'rgba(168, 85, 247, 0.3)';
|
||||
strokeColor = 'rgb(168, 85, 247)';
|
||||
} else {
|
||||
// 인원 없음 - 회색 테두리만
|
||||
fillColor = 'rgba(0, 0, 0, 0)'; // 투명
|
||||
@@ -332,9 +280,8 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) {
|
||||
ctx.stroke();
|
||||
|
||||
// 텍스트
|
||||
const totalCount = workerCount + visitorCount;
|
||||
ctx.fillStyle = strokeColor;
|
||||
ctx.fillText(totalCount.toString(), centerX, centerY);
|
||||
ctx.fillText(workerCount.toString(), centerX, centerY);
|
||||
ctx.restore();
|
||||
} else {
|
||||
// 인원이 없을 때는 작업장 이름만 표시
|
||||
@@ -389,7 +336,6 @@ let currentModalWorkplace = null;
|
||||
function showWorkplaceDetail(workplace) {
|
||||
currentModalWorkplace = workplace;
|
||||
const workers = todayWorkers.filter(w => w.workplace_id === workplace.workplace_id);
|
||||
const visitors = todayVisitors.filter(v => v.workplace_id === workplace.workplace_id);
|
||||
|
||||
// 모달 제목
|
||||
document.getElementById('modalWorkplaceName').textContent = workplace.workplace_name;
|
||||
@@ -397,15 +343,16 @@ function showWorkplaceDetail(workplace) {
|
||||
|
||||
// 요약 카드 업데이트
|
||||
const totalWorkers = workers.reduce((sum, w) => sum + (w.member_count || 0), 0);
|
||||
const totalVisitors = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0);
|
||||
|
||||
document.getElementById('summaryWorkerCount').textContent = totalWorkers;
|
||||
document.getElementById('summaryVisitorCount').textContent = totalVisitors;
|
||||
const summaryVisitorEl = document.getElementById('summaryVisitorCount');
|
||||
if (summaryVisitorEl) summaryVisitorEl.textContent = '0';
|
||||
document.getElementById('summaryTaskCount').textContent = workers.length;
|
||||
|
||||
// 배지 업데이트
|
||||
document.getElementById('workerCountBadge').textContent = totalWorkers;
|
||||
document.getElementById('visitorCountBadge').textContent = totalVisitors;
|
||||
const visitorBadgeEl = document.getElementById('visitorCountBadge');
|
||||
if (visitorBadgeEl) visitorBadgeEl.textContent = '0';
|
||||
|
||||
// 현황 개요 탭 - 현재 작업 목록
|
||||
renderCurrentTasks(workers);
|
||||
@@ -416,9 +363,6 @@ function showWorkplaceDetail(workplace) {
|
||||
// 작업자 탭
|
||||
renderWorkersTab(workers);
|
||||
|
||||
// 방문자 탭
|
||||
renderVisitorsTab(visitors);
|
||||
|
||||
// 상세 지도 초기화
|
||||
initDetailMap(workplace);
|
||||
|
||||
@@ -529,33 +473,6 @@ function renderWorkersTab(workers) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 방문자 탭 렌더링
|
||||
function renderVisitorsTab(visitors) {
|
||||
const container = document.getElementById('externalVisitorsList');
|
||||
|
||||
if (visitors.length === 0) {
|
||||
container.innerHTML = '<p class="empty-message">금일 방문 예정 인원이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
visitors.forEach(visitor => {
|
||||
const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨';
|
||||
|
||||
html += `
|
||||
<div class="visitor-item">
|
||||
<div class="visitor-item-header">
|
||||
<p class="visitor-item-title">${escapeHtml(visitor.visitor_company)}</p>
|
||||
<span class="visitor-item-badge">${parseInt(visitor.visitor_count) || 0}명 • ${statusText}</span>
|
||||
</div>
|
||||
<p class="visitor-item-detail">⏰ ${escapeHtml(visitor.visit_time)}</p>
|
||||
<p class="visitor-item-detail">📋 ${escapeHtml(visitor.purpose_name)}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 상세 지도 초기화
|
||||
async function initDetailMap(workplace) {
|
||||
|
||||
@@ -46,6 +46,12 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Static files (new Tailwind UI)
|
||||
location /static/ {
|
||||
expires 1h;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
<!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/common.css?v=1">
|
||||
<link rel="stylesheet" href="/css/admin-settings.css?v=2">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<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="settings-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">사용자 계정 관리</h2>
|
||||
<button class="btn btn-primary" id="addUserBtn">새 사용자 추가</button>
|
||||
</div>
|
||||
|
||||
<div class="users-container">
|
||||
<div class="users-header">
|
||||
<div class="search-box">
|
||||
<input type="text" id="userSearch" placeholder="사용자 검색..." class="search-input">
|
||||
<span class="search-icon"></span>
|
||||
</div>
|
||||
<div class="filter-buttons">
|
||||
<button class="filter-btn active" data-filter="all">전체</button>
|
||||
<button class="filter-btn" data-filter="admin">관리자</button>
|
||||
<button class="filter-btn" data-filter="leader">그룹장</button>
|
||||
<button class="filter-btn" data-filter="user">작업자</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="users-table-container">
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>사용자명</th>
|
||||
<th>아이디</th>
|
||||
<th>역할</th>
|
||||
<th>상태</th>
|
||||
<th>최종 로그인</th>
|
||||
<th>관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usersTableBody">
|
||||
<!-- 사용자 목록이 여기에 동적으로 생성됩니다 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" id="emptyState" style="display: none;">
|
||||
<h3>등록된 사용자가 없습니다</h3>
|
||||
<p>새 사용자를 추가해보세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 알림 수신자 설정 섹션 -->
|
||||
<div class="settings-section" id="notificationRecipientsSection">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">알림 수신자 설정</h2>
|
||||
<p class="section-description">알림 유형별 수신자를 지정합니다. 지정된 사용자에게만 알림이 전송됩니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="notification-recipients-container">
|
||||
<div class="notification-type-cards" id="notificationTypeCards">
|
||||
<!-- 동적으로 생성됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 추가/수정 모달 -->
|
||||
<div id="userModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="modalTitle">새 사용자 추가</h2>
|
||||
<button class="modal-close-btn" onclick="closeUserModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form id="userForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">사용자명 *</label>
|
||||
<input type="text" id="userName" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">아이디 *</label>
|
||||
<input type="text" id="userId" class="form-control" required>
|
||||
<small class="form-help">영문, 숫자, 한글, 특수문자(._-) 사용 가능 (3-20자)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="passwordGroup">
|
||||
<label class="form-label">비밀번호 *</label>
|
||||
<input type="password" id="userPassword" class="form-control" required>
|
||||
<small class="form-help">최소 6자 이상</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">역할 *</label>
|
||||
<select id="userRole" class="form-control" required>
|
||||
<option value="">역할 선택</option>
|
||||
<option value="admin">관리자</option>
|
||||
<option value="user">사용자</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">이메일</label>
|
||||
<input type="email" id="userEmail" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">전화번호</label>
|
||||
<input type="tel" id="userPhone" class="form-control">
|
||||
</div>
|
||||
|
||||
<!-- 작업자 연결 (수정 시에만 표시) -->
|
||||
<div class="form-group" id="workerLinkGroup" style="display: none;">
|
||||
<label class="form-label">작업자 연결</label>
|
||||
<div class="worker-link-container">
|
||||
<div class="linked-worker-info" id="linkedWorkerInfo">
|
||||
<span class="no-worker">연결된 작업자 없음</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="openWorkerSelectModal()">
|
||||
작업자 선택
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-help">계정과 작업자를 연결하면 출퇴근, 작업보고서 등의 기록이 연동됩니다</small>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeUserModal()">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="saveUserBtn">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사용자 삭제 확인 모달 -->
|
||||
<div id="deleteModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container small">
|
||||
<div class="modal-header">
|
||||
<h2>사용자 삭제</h2>
|
||||
<button class="modal-close-btn" onclick="closeDeleteModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="delete-warning">
|
||||
<div class="warning-icon"></div>
|
||||
<p>정말로 이 사용자를 삭제하시겠습니까?</p>
|
||||
<p class="warning-text">삭제된 사용자는 복구할 수 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">취소</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 페이지 권한 관리 모달 -->
|
||||
<div id="pageAccessModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="pageAccessModalTitle">페이지 권한 관리</h2>
|
||||
<button class="modal-close-btn" onclick="closePageAccessModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="page-access-user-info">
|
||||
<div class="user-avatar-small" id="pageAccessUserAvatar">U</div>
|
||||
<div>
|
||||
<h3 id="pageAccessUserName">사용자</h3>
|
||||
<p id="pageAccessUserRole">역할</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">접근 가능한 페이지</label>
|
||||
<small class="form-help">체크된 페이지에만 접근할 수 있습니다</small>
|
||||
<div id="pageAccessModalList" class="page-access-list">
|
||||
<!-- 페이지 체크박스 목록 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closePageAccessModal()">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="savePageAccessBtn">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 선택 모달 -->
|
||||
<div id="workerSelectModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2>작업자 선택</h2>
|
||||
<button class="modal-close-btn" onclick="closeWorkerSelectModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="worker-select-layout">
|
||||
<!-- 부서 목록 -->
|
||||
<div class="department-list-panel">
|
||||
<h3 class="panel-title">부서</h3>
|
||||
<div class="department-list" id="departmentList">
|
||||
<!-- 부서 목록이 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 목록 -->
|
||||
<div class="worker-list-panel">
|
||||
<h3 class="panel-title">작업자</h3>
|
||||
<div class="worker-list" id="workerListForSelect">
|
||||
<div class="empty-message">부서를 선택하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeWorkerSelectModal()">취소</button>
|
||||
<button type="button" class="btn btn-danger" onclick="unlinkWorker()">연결 해제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 알림 수신자 편집 모달 -->
|
||||
<div id="notificationRecipientModal" class="modal-overlay" style="display: none;">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 id="recipientModalTitle">알림 수신자 설정</h2>
|
||||
<button class="modal-close-btn" onclick="closeRecipientModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p class="modal-description" id="recipientModalDesc">이 알림을 받을 사용자를 선택하세요.</p>
|
||||
|
||||
<div class="recipient-search-box">
|
||||
<input type="text" id="recipientSearchInput" placeholder="사용자 검색..." class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="recipient-user-list" id="recipientUserList">
|
||||
<!-- 사용자 체크박스 목록 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeRecipientModal()">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveNotificationRecipients()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 토스트 알림 -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<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>
|
||||
<script src="/js/admin-settings.js?v=9"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,13 +3,10 @@
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
@@ -98,13 +95,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">출퇴근-작업보고서 대조</h1>
|
||||
@@ -175,8 +187,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
@@ -482,5 +497,6 @@
|
||||
container.innerHTML = tableHTML;
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.department-grid {
|
||||
display: grid;
|
||||
@@ -215,12 +212,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="page-container">
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">부서 관리</h1>
|
||||
@@ -267,7 +280,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 부서 등록/수정 모달 -->
|
||||
@@ -311,6 +324,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/department-management.js"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,23 +3,34 @@
|
||||
<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">
|
||||
<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/equipment-detail.css?v=1">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 레이아웃 -->
|
||||
<div class="page-container">
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<!-- 뒤로가기 & 제목 -->
|
||||
<div class="page-header eq-detail-header">
|
||||
<div class="page-title-section">
|
||||
@@ -119,7 +130,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 추가 모달 -->
|
||||
@@ -303,6 +314,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
@@ -340,5 +353,6 @@
|
||||
})();
|
||||
</script>
|
||||
<script src="/js/equipment-detail.js?v=1"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,23 +3,34 @@
|
||||
<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">
|
||||
<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/equipment-management.css?v=1">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 레이아웃 -->
|
||||
<div class="page-container">
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">설비 관리</h1>
|
||||
@@ -72,7 +83,7 @@
|
||||
<!-- 설비 목록이 여기에 동적으로 렌더링됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 설비 추가/수정 모달 -->
|
||||
@@ -179,6 +190,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
@@ -216,5 +229,6 @@
|
||||
})();
|
||||
</script>
|
||||
<script src="/js/equipment-management.js?v=8"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
@@ -211,12 +208,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="page-container">
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">신고 카테고리 관리</h1>
|
||||
@@ -238,7 +251,7 @@
|
||||
<div class="empty-state">카테고리를 불러오는 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 카테고리 추가/수정 모달 -->
|
||||
@@ -316,6 +329,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script type="module" src="/js/issue-category-manage.js"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,18 +3,14 @@
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.notification-page-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
@@ -308,11 +304,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-placeholder"></div>
|
||||
<div id="sidebar-placeholder"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="notification-page-container">
|
||||
<div class="notification-header">
|
||||
<h1>알림 관리</h1>
|
||||
@@ -358,9 +371,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/load-sidebar.js"></script>
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
@@ -550,5 +566,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
<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">
|
||||
<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">
|
||||
<style>
|
||||
.page-wrapper {
|
||||
padding: 1rem 1.5rem;
|
||||
max-width: 1600px;
|
||||
}
|
||||
.page-header {
|
||||
@@ -226,11 +225,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">프로젝트 관리</h1>
|
||||
@@ -280,7 +296,9 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 프로젝트 모달 -->
|
||||
<div id="projectModal" class="modal-overlay" style="display: none;">
|
||||
@@ -366,8 +384,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<script>
|
||||
let allProjects = [];
|
||||
let filteredProjects = [];
|
||||
@@ -683,5 +701,6 @@
|
||||
window.refreshProjectList = refreshProjectList;
|
||||
window.filterByStatus = filterByStatus;
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,17 +3,14 @@
|
||||
<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">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
<style>
|
||||
.repair-page {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@@ -358,11 +355,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="repair-page">
|
||||
<div class="page-header">
|
||||
<h1>시설설비 관리</h1>
|
||||
@@ -408,7 +422,9 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 접수 모달 -->
|
||||
<div class="modal-overlay" id="receiveModal">
|
||||
@@ -471,6 +487,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script>
|
||||
let currentReportId = null;
|
||||
let allRepairs = [];
|
||||
@@ -848,5 +866,6 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
<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">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
<style>
|
||||
.page-wrapper { padding: 1rem 1.5rem; max-width: 1400px; }
|
||||
.page-wrapper { max-width: 1400px; }
|
||||
.page-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
@@ -140,11 +138,28 @@
|
||||
.form-hint { font-size: 0.7rem; color: #6b7280; margin-top: 0.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">작업 관리</h1>
|
||||
@@ -193,7 +208,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업 모달 -->
|
||||
<div id="taskModal" class="modal-overlay" style="display:none;">
|
||||
@@ -268,6 +285,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script>
|
||||
let workTypes = [];
|
||||
let tasks = [];
|
||||
@@ -592,5 +611,6 @@
|
||||
window.deleteWorkType = deleteWorkType;
|
||||
window.refreshData = refreshData;
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,495 +0,0 @@
|
||||
<!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=3"></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>
|
||||
@@ -3,22 +3,34 @@
|
||||
<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">
|
||||
<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/workplace-management.css?v=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 레이아웃 (기존 admin 레이아웃과 호환) -->
|
||||
<div class="page-container">
|
||||
<main class="main-content">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="wp-content">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="wp-page-header">
|
||||
@@ -128,7 +140,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공장(카테고리) 추가/수정 모달 -->
|
||||
@@ -418,6 +431,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 작업장 관리 모듈 (리팩토링된 구조) -->
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/workplace-management/state.js?v=1"></script>
|
||||
<script src="/js/workplace-management/utils.js?v=1"></script>
|
||||
<script src="/js/workplace-management/api.js?v=1"></script>
|
||||
@@ -425,5 +440,6 @@
|
||||
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
|
||||
<script type="module" src="/js/workplace-management.js?v=9"></script>
|
||||
<script type="module" src="/js/workplace-layout-map.js?v=2"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
<style>
|
||||
.page-wrapper {
|
||||
padding: 1.5rem;
|
||||
@@ -234,12 +232,29 @@
|
||||
.loading { text-align: center; padding: 2rem; color: #6b7280; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<main class="main-content">
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">연간 연차 현황</h1>
|
||||
@@ -288,9 +303,11 @@
|
||||
<div class="save-bar">
|
||||
<span class="save-status" id="saveStatus">변경사항이 있으면 저장 버튼을 눌러주세요</span>
|
||||
<button class="btn btn-success" onclick="saveAll()">저장</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 경조사 모달 -->
|
||||
<div class="modal-overlay" id="specialModal">
|
||||
@@ -311,6 +328,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
// axios 설정
|
||||
@@ -758,5 +777,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="stylesheet" href="/css/mobile.css?v=1">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.page-wrapper {
|
||||
padding: 1.5rem;
|
||||
@@ -162,12 +158,29 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<main class="main-content">
|
||||
<div class="page-wrapper">
|
||||
<h1 class="page-title">출근 체크</h1>
|
||||
<p class="page-desc">클릭하여 출근/결근 상태를 변경하세요</p>
|
||||
|
||||
@@ -204,12 +217,14 @@
|
||||
<div id="saveStatus" style="margin-bottom: 1rem;"></div>
|
||||
<button id="saveBtn" class="btn-save" onclick="saveCheckin()">출근 체크 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="module">
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
const checkApiConfig = setInterval(() => {
|
||||
@@ -395,19 +410,6 @@
|
||||
}
|
||||
}
|
||||
</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>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,21 +3,34 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">일일 출퇴근 입력</h1>
|
||||
@@ -52,13 +65,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
</script>
|
||||
<script>
|
||||
// axios 기본 설정
|
||||
(function() {
|
||||
@@ -388,5 +402,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
/* 테이블 컨테이너 */
|
||||
.table-container {
|
||||
@@ -354,13 +351,29 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">월별 출근부</h1>
|
||||
@@ -432,9 +445,11 @@
|
||||
<span>미입사</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 휴무일 설정 모달 -->
|
||||
<div class="modal-overlay" id="holidayModal">
|
||||
@@ -459,10 +474,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
</script>
|
||||
<script>
|
||||
// axios 기본 설정
|
||||
(function() {
|
||||
@@ -1157,5 +1171,6 @@
|
||||
link.click();
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
<style>
|
||||
.page-wrapper {
|
||||
padding: 1.5rem;
|
||||
@@ -197,12 +195,29 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<main class="main-content">
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">내 연차 정보</h1>
|
||||
@@ -245,9 +260,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
// axios 설정
|
||||
@@ -550,5 +569,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,30 +4,35 @@
|
||||
<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">
|
||||
<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/vacation-allocation.css">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<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>
|
||||
<script type="module" src="/js/vacation-allocation.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 메인 컨테이너 -->
|
||||
<div class="page-container">
|
||||
|
||||
<!-- 네비게이션 헤더 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="content-wrapper">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="page-header">
|
||||
@@ -256,8 +261,7 @@
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 알림 토스트 -->
|
||||
@@ -349,6 +353,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script type="module" src="/js/vacation-allocation.js" defer></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.tabs {
|
||||
display: flex;
|
||||
@@ -42,13 +39,29 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">휴가 승인 관리</h1>
|
||||
@@ -105,14 +118,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="/js/vacation-common.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
</script>
|
||||
<script>
|
||||
// axios 기본 설정
|
||||
(function() {
|
||||
@@ -260,5 +274,6 @@
|
||||
renderVacationRequests(allRequestsData, 'allRequestsList', false);
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,21 +3,34 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">휴가 직접 입력</h1>
|
||||
@@ -105,14 +118,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="/js/vacation-common.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
</script>
|
||||
<script>
|
||||
// axios 기본 설정
|
||||
(function() {
|
||||
@@ -287,5 +301,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
<style>
|
||||
.tabs {
|
||||
display: flex;
|
||||
@@ -42,13 +39,29 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">휴가 관리</h1>
|
||||
@@ -187,14 +200,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="/js/vacation-common.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
</script>
|
||||
<script>
|
||||
// axios 기본 설정
|
||||
(function() {
|
||||
@@ -454,5 +468,6 @@
|
||||
renderVacationRequests(allRequestsData, 'allRequestsList', false);
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,21 +3,34 @@
|
||||
<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=7">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">휴가 신청</h1>
|
||||
@@ -99,14 +112,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script src="/js/vacation-common.js"></script>
|
||||
<script type="module">
|
||||
import '/js/api-config.js?v=3';
|
||||
</script>
|
||||
<script>
|
||||
// axios 기본 설정
|
||||
(function() {
|
||||
@@ -265,5 +279,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
<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=7">
|
||||
<link rel="stylesheet" href="/css/mobile.css?v=1">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
<style>
|
||||
.page-wrapper {
|
||||
padding: 1.5rem;
|
||||
@@ -204,12 +201,33 @@
|
||||
.warning-box a { color: #92400e; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="has-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
<div id="sidebar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<main class="main-content">
|
||||
<div class="page-wrapper">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">근무 현황</h1>
|
||||
<div class="controls">
|
||||
@@ -253,9 +271,13 @@
|
||||
<span id="saveStatus" class="save-status"></span>
|
||||
<button id="saveBtn" class="btn-save" onclick="saveWorkStatus()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
@@ -695,19 +717,6 @@
|
||||
}
|
||||
}
|
||||
</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>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
144
system1-factory/web/pages/dashboard-new.html
Normal file
144
system1-factory/web/pages/dashboard-new.html
Normal file
@@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<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">
|
||||
</head>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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 class="flex-1 min-w-0">
|
||||
<!-- 날짜/시간 헤더 -->
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-800">대시보드</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5" id="dateTimeDisplay">-</p>
|
||||
</div>
|
||||
<button onclick="loadDashboard()" class="text-sm text-gray-500 hover:text-orange-600 border border-gray-200 px-3 py-1.5 rounded-lg hover:bg-orange-50">
|
||||
<i class="fas fa-sync-alt mr-1"></i>새로고침
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 요약 카드 -->
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-orange-600" id="statTbm">-</div>
|
||||
<div class="stat-label">금일 TBM</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-blue-600" id="statWorkers">-</div>
|
||||
<div class="stat-label">출근 인원</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-red-600" id="statRepairs">-</div>
|
||||
<div class="stat-label">수리 요청</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-purple-600" id="statNotifications">-</div>
|
||||
<div class="stat-label">미확인 알림</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<!-- 금일 TBM 현황 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-5">
|
||||
<h3 class="text-base font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-clipboard-list text-orange-500 mr-2"></i>금일 TBM
|
||||
</h3>
|
||||
<div id="tbmList" class="space-y-2">
|
||||
<p class="text-gray-400 text-sm text-center py-4">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 알림 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-5">
|
||||
<h3 class="text-base font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-bell text-orange-500 mr-2"></i>최근 알림
|
||||
</h3>
|
||||
<div id="notificationList" class="space-y-2">
|
||||
<p class="text-gray-400 text-sm text-center py-4">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 미완료 수리 요청 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-5">
|
||||
<h3 class="text-base font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-tools text-orange-500 mr-2"></i>수리 요청 현황
|
||||
</h3>
|
||||
<div id="repairList" class="space-y-2">
|
||||
<p class="text-gray-400 text-sm text-center py-4">로딩 중...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 빠른 이동 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-5">
|
||||
<h3 class="text-base font-semibold text-gray-800 mb-4">
|
||||
<i class="fas fa-rocket text-orange-500 mr-2"></i>빠른 이동
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<a href="/pages/work/tbm.html" class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:border-orange-200 hover:bg-orange-50 transition-colors">
|
||||
<i class="fas fa-clipboard-list text-orange-500 w-5 text-center"></i>
|
||||
<span class="text-sm text-gray-700">TBM 관리</span>
|
||||
</a>
|
||||
<a href="/pages/work/report-create.html" class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:border-orange-200 hover:bg-orange-50 transition-colors">
|
||||
<i class="fas fa-file-alt text-orange-500 w-5 text-center"></i>
|
||||
<span class="text-sm text-gray-700">작업보고서</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/checkin.html" class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:border-orange-200 hover:bg-orange-50 transition-colors">
|
||||
<i class="fas fa-user-check text-orange-500 w-5 text-center"></i>
|
||||
<span class="text-sm text-gray-700">출근 체크</span>
|
||||
</a>
|
||||
<a href="/pages/admin/repair-management.html" class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:border-orange-200 hover:bg-orange-50 transition-colors">
|
||||
<i class="fas fa-tools text-orange-500 w-5 text-center"></i>
|
||||
<span class="text-sm text-gray-700">시설설비 관리</span>
|
||||
</a>
|
||||
<a href="/pages/attendance/vacation-request.html" class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:border-orange-200 hover:bg-orange-50 transition-colors">
|
||||
<i class="fas fa-paper-plane text-orange-500 w-5 text-center"></i>
|
||||
<span class="text-sm text-gray-700">휴가 신청</span>
|
||||
</a>
|
||||
<a href="/pages/dashboard.html" class="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:border-orange-200 hover:bg-orange-50 transition-colors">
|
||||
<i class="fas fa-map text-orange-500 w-5 text-center"></i>
|
||||
<span class="text-sm text-gray-700">작업장 현황</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/static/js/tkfb-dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>작업 현황판 | 테크니컬코리아</title>
|
||||
|
||||
<!-- 리소스 프리로딩 -->
|
||||
<!-- preconnect는 Gateway 프록시 사용 시 불필요 -->
|
||||
<link rel="preload" href="/css/design-system.css" as="style">
|
||||
<link rel="preload" href="/js/api-base.js?v=3" as="script">
|
||||
<link rel="preload" href="/js/app-init.js?v=9" as="script">
|
||||
|
||||
<!-- 모던 디자인 시스템 적용 -->
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<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/modern-dashboard.css?v=3">
|
||||
<link rel="stylesheet" href="/css/mobile.css?v=4">
|
||||
<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 type="module" src="/js/modern-dashboard.js?v=10" defer></script>
|
||||
<script type="module" src="/js/group-leader-dashboard.js?v=1" defer></script>
|
||||
<script src="/js/workplace-status.js?v=3" defer></script>
|
||||
<script src="/js/mobile-dashboard.js?v=4" defer></script>
|
||||
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
|
||||
<script src="https://instant.page/5.2.0" type="module"></script>
|
||||
</head>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<body>
|
||||
<!-- 메인 컨테이너 -->
|
||||
<div class="dashboard-container">
|
||||
<!-- Mobile overlay -->
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></div>
|
||||
|
||||
<!-- 네비게이션 헤더 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="dashboard-main">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<!-- 모바일 대시보드 (작업장 리스트 뷰) -->
|
||||
<section id="mobileDashboardView" class="mobile-dashboard-view" style="display:none;">
|
||||
<div class="md-date-header">
|
||||
<span class="md-date-label">금일 현황</span>
|
||||
<time class="md-date-value" id="mDateValue"></time>
|
||||
</div>
|
||||
|
||||
<!-- 카테고리 탭 -->
|
||||
<div class="md-category-tabs" id="mCategoryTabs"></div>
|
||||
|
||||
<!-- 작업장 리스트 -->
|
||||
<div class="md-workplace-list" id="mWorkplaceList"></div>
|
||||
</section>
|
||||
|
||||
@@ -63,40 +60,35 @@
|
||||
<select id="categorySelect" class="form-select" style="width: 200px;">
|
||||
<option value="">공장을 선택하세요</option>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" id="refreshMapBtn">
|
||||
새로고침
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" id="refreshMapBtn">새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- 지도 영역 -->
|
||||
<div id="workplaceMapContainer" style="position: relative; display: none;">
|
||||
<canvas id="workplaceMapCanvas" style="width: 100%; max-width: 100%; cursor: pointer; border: 2px solid var(--gray-300); border-radius: var(--radius-md);"></canvas>
|
||||
<div id="mapLegend" style="position: absolute; top: 16px; right: 16px; background: white; padding: 16px; border-radius: var(--radius-md); box-shadow: var(--shadow-md);">
|
||||
<h4 style="font-size: var(--text-sm); font-weight: 700; margin-bottom: 12px;">범례</h4>
|
||||
<canvas id="workplaceMapCanvas" style="width: 100%; max-width: 100%; cursor: pointer; border: 2px solid #d1d5db; border-radius: 0.5rem;"></canvas>
|
||||
<div id="mapLegend" style="position: absolute; top: 16px; right: 16px; background: white; padding: 16px; border-radius: 0.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||||
<h4 style="font-size: 0.875rem; font-weight: 700; margin-bottom: 12px;">범례</h4>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="width: 16px; height: 16px; background: rgba(59, 130, 246, 0.3); border: 2px solid rgb(59, 130, 246); border-radius: 4px;"></div>
|
||||
<span style="font-size: var(--text-sm);">작업 중 (내부 작업자)</span>
|
||||
<span style="font-size: 0.875rem;">작업 중 (내부 작업자)</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="width: 16px; height: 16px; background: rgba(168, 85, 247, 0.3); border: 2px solid rgb(168, 85, 247); border-radius: 4px;"></div>
|
||||
<span style="font-size: var(--text-sm);">방문 예정 (외부 인원)</span>
|
||||
<span style="font-size: 0.875rem;">방문 예정 (외부 인원)</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="width: 16px; height: 16px; background: rgba(34, 197, 94, 0.3); border: 2px solid rgb(34, 197, 94); border-radius: 4px;"></div>
|
||||
<span style="font-size: var(--text-sm);">작업 + 방문</span>
|
||||
<span style="font-size: 0.875rem;">작업 + 방문</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 안내 메시지 -->
|
||||
<div id="mapPlaceholder" style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px; color: var(--gray-500);">
|
||||
<div id="mapPlaceholder" style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px; color: #6b7280;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">🏭</div>
|
||||
<h3 style="margin-bottom: 8px;">공장을 선택하세요</h3>
|
||||
<p style="font-size: var(--text-sm);">위에서 공장을 선택하면 작업장 현황을 확인할 수 있습니다.</p>
|
||||
<p style="font-size: 0.875rem;">위에서 공장을 선택하면 작업장 현황을 확인할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,42 +104,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="movedEquipmentList" class="moved-equipment-grid">
|
||||
<!-- 동적 로드 -->
|
||||
</div>
|
||||
<div id="noMovedEquipment" style="display: none; text-align: center; padding: 40px; color: var(--gray-500);">
|
||||
<div id="movedEquipmentList" class="moved-equipment-grid"></div>
|
||||
<div id="noMovedEquipment" style="display: none; text-align: center; padding: 40px; color: #6b7280;">
|
||||
<div style="font-size: 48px; margin-bottom: 12px;">✅</div>
|
||||
<p>임시 이동된 설비가 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- 푸터 -->
|
||||
<footer class="dashboard-footer">
|
||||
<div class="footer-content">
|
||||
<p class="footer-text">
|
||||
© 2025 (주)테크니컬코리아. 모든 권리 보유.
|
||||
</p>
|
||||
<div class="footer-links">
|
||||
<a href="#" class="footer-link">도움말</a>
|
||||
<a href="#" class="footer-link">문의하기</a>
|
||||
<a href="#" class="footer-link">개인정보처리방침</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 알림 토스트 -->
|
||||
<!-- 토스트 -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- 작업장 상세 정보 모달 -->
|
||||
<div id="workplaceDetailModal" class="workplace-modal-overlay">
|
||||
<div class="workplace-modal-container">
|
||||
<!-- 모달 헤더 -->
|
||||
<div class="workplace-modal-header">
|
||||
<div class="workplace-modal-title-section">
|
||||
<h2 id="modalWorkplaceName" class="workplace-modal-title"></h2>
|
||||
@@ -155,146 +129,78 @@
|
||||
</div>
|
||||
<button class="workplace-modal-close" onclick="closeWorkplaceModal()">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 모달 바디 -->
|
||||
<div class="workplace-modal-body">
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="workplace-modal-tabs">
|
||||
<button class="workplace-tab active" data-tab="overview" onclick="switchWorkplaceTab('overview')">
|
||||
<span class="tab-icon">📊</span>
|
||||
<span class="tab-text">현황 개요</span>
|
||||
<span class="tab-icon">📊</span><span class="tab-text">현황 개요</span>
|
||||
</button>
|
||||
<button class="workplace-tab" data-tab="workers" onclick="switchWorkplaceTab('workers')">
|
||||
<span class="tab-icon">👷</span>
|
||||
<span class="tab-text">작업자</span>
|
||||
<span class="tab-icon">👷</span><span class="tab-text">작업자</span>
|
||||
<span id="workerCountBadge" class="tab-badge">0</span>
|
||||
</button>
|
||||
<button class="workplace-tab" data-tab="visitors" onclick="switchWorkplaceTab('visitors')">
|
||||
<span class="tab-icon">🚪</span>
|
||||
<span class="tab-text">방문자</span>
|
||||
<span class="tab-icon">🚪</span><span class="tab-text">방문자</span>
|
||||
<span id="visitorCountBadge" class="tab-badge">0</span>
|
||||
</button>
|
||||
<button class="workplace-tab" data-tab="detail-map" onclick="switchWorkplaceTab('detail-map')">
|
||||
<span class="tab-icon">🗺️</span>
|
||||
<span class="tab-text">상세 지도</span>
|
||||
<span class="tab-icon">🗺️</span><span class="tab-text">상세 지도</span>
|
||||
</button>
|
||||
<button class="workplace-tab" data-tab="moved-eq" onclick="switchWorkplaceTab('moved-eq')">
|
||||
<span class="tab-icon">🚚</span>
|
||||
<span class="tab-text">이동 설비</span>
|
||||
<span class="tab-icon">🚚</span><span class="tab-text">이동 설비</span>
|
||||
<span id="movedEqCountBadge" class="tab-badge" style="display:none;">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 탭 콘텐츠 -->
|
||||
<div class="workplace-tab-contents">
|
||||
<!-- 현황 개요 탭 -->
|
||||
<div id="tab-overview" class="workplace-tab-content active">
|
||||
<!-- 요약 카드 -->
|
||||
<div class="workplace-summary-cards">
|
||||
<div class="summary-card workers">
|
||||
<div class="summary-icon">👷</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-value" id="summaryWorkerCount">0</span>
|
||||
<span class="summary-label">작업자</span>
|
||||
</div>
|
||||
<div class="summary-info"><span class="summary-value" id="summaryWorkerCount">0</span><span class="summary-label">작업자</span></div>
|
||||
</div>
|
||||
<div class="summary-card visitors">
|
||||
<div class="summary-icon">🚪</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-value" id="summaryVisitorCount">0</span>
|
||||
<span class="summary-label">방문자</span>
|
||||
</div>
|
||||
<div class="summary-info"><span class="summary-value" id="summaryVisitorCount">0</span><span class="summary-label">방문자</span></div>
|
||||
</div>
|
||||
<div class="summary-card tasks">
|
||||
<div class="summary-icon">📋</div>
|
||||
<div class="summary-info">
|
||||
<span class="summary-value" id="summaryTaskCount">0</span>
|
||||
<span class="summary-label">작업 수</span>
|
||||
<div class="summary-info"><span class="summary-value" id="summaryTaskCount">0</span><span class="summary-label">작업 수</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 현재 작업 목록 -->
|
||||
<div class="workplace-section">
|
||||
<h4 class="section-title">
|
||||
<span class="section-icon">🔧</span>
|
||||
진행 중인 작업
|
||||
</h4>
|
||||
<div id="currentTasksList" class="current-tasks-list">
|
||||
<p class="empty-message">진행 중인 작업이 없습니다.</p>
|
||||
<h4 class="section-title"><span class="section-icon">🔧</span> 진행 중인 작업</h4>
|
||||
<div id="currentTasksList" class="current-tasks-list"><p class="empty-message">진행 중인 작업이 없습니다.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 설비 현황 (간략) -->
|
||||
<div class="workplace-section">
|
||||
<h4 class="section-title">
|
||||
<span class="section-icon">⚙️</span>
|
||||
설비 현황
|
||||
</h4>
|
||||
<div id="equipmentSummary" class="equipment-summary">
|
||||
<p class="empty-message">설비 정보를 불러오는 중...</p>
|
||||
<h4 class="section-title"><span class="section-icon">⚙️</span> 설비 현황</h4>
|
||||
<div id="equipmentSummary" class="equipment-summary"><p class="empty-message">설비 정보를 불러오는 중...</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 탭 -->
|
||||
<div id="tab-workers" class="workplace-tab-content">
|
||||
<div id="internalWorkersList" class="workers-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- 방문자 탭 -->
|
||||
<div id="tab-visitors" class="workplace-tab-content">
|
||||
<div id="externalVisitorsList" class="visitors-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 지도 탭 -->
|
||||
<div id="tab-workers" class="workplace-tab-content"><div id="internalWorkersList" class="workers-list"></div></div>
|
||||
<div id="tab-visitors" class="workplace-tab-content"><div id="externalVisitorsList" class="visitors-list"></div></div>
|
||||
<div id="tab-detail-map" class="workplace-tab-content">
|
||||
<div id="detailMapContainer" class="detail-map-container">
|
||||
<div class="detail-map-placeholder">
|
||||
<span class="placeholder-icon">🗺️</span>
|
||||
<p>상세 지도를 불러오는 중...</p>
|
||||
</div>
|
||||
<div class="detail-map-placeholder"><span class="placeholder-icon">🗺️</span><p>상세 지도를 불러오는 중...</p></div>
|
||||
</div>
|
||||
<div id="detailMapLegend" class="detail-map-legend"></div>
|
||||
</div>
|
||||
|
||||
<!-- 이동 설비 탭 -->
|
||||
<div id="tab-moved-eq" class="workplace-tab-content">
|
||||
<div class="moved-eq-tab-content">
|
||||
<!-- 이 작업장으로 이동해 온 설비 -->
|
||||
<div class="workplace-section">
|
||||
<h4 class="section-title">
|
||||
<span class="section-icon">📥</span>
|
||||
이 작업장으로 이동해 온 설비
|
||||
</h4>
|
||||
<div id="movedInEquipmentList" class="moved-eq-list">
|
||||
<p class="empty-message">없음</p>
|
||||
<h4 class="section-title"><span class="section-icon">📥</span> 이 작업장으로 이동해 온 설비</h4>
|
||||
<div id="movedInEquipmentList" class="moved-eq-list"><p class="empty-message">없음</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 이 작업장에서 다른 곳으로 이동한 설비 -->
|
||||
<div class="workplace-section">
|
||||
<h4 class="section-title">
|
||||
<span class="section-icon">📤</span>
|
||||
다른 곳으로 이동한 설비
|
||||
</h4>
|
||||
<div id="movedOutEquipmentList" class="moved-eq-list">
|
||||
<p class="empty-message">없음</p>
|
||||
<h4 class="section-title"><span class="section-icon">📤</span> 다른 곳으로 이동한 설비</h4>
|
||||
<div id="movedOutEquipmentList" class="moved-eq-list"><p class="empty-message">없음</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모달 푸터 -->
|
||||
<div class="workplace-modal-footer">
|
||||
<button class="btn btn-outline" onclick="openPatrolPage()">
|
||||
<span>🔍</span> 순회점검
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="openPatrolPage()"><span>🔍</span> 순회점검</button>
|
||||
<button class="btn btn-primary" onclick="closeWorkplaceModal()">닫기</button>
|
||||
</div>
|
||||
|
||||
<!-- 설비 상세 슬라이드 패널 -->
|
||||
<div id="equipmentSlidePanel" class="equipment-slide-panel">
|
||||
<div class="slide-panel-header">
|
||||
@@ -304,52 +210,19 @@
|
||||
<span id="panelEquipmentStatus" class="slide-panel-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="slide-panel-body">
|
||||
<!-- 기본 정보 -->
|
||||
<div class="panel-section"><div class="panel-info-grid" id="panelEquipmentInfo"></div></div>
|
||||
<div class="panel-section">
|
||||
<div class="panel-info-grid" id="panelEquipmentInfo"></div>
|
||||
<div class="panel-section-header"><h4>설비 사진</h4><button class="btn-icon-sm" onclick="openPanelPhotoUpload()">+</button></div>
|
||||
<div class="panel-photo-grid" id="panelPhotoGrid"><div class="panel-empty">등록된 사진이 없습니다</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 -->
|
||||
<div class="panel-section">
|
||||
<div class="panel-section-header">
|
||||
<h4>설비 사진</h4>
|
||||
<button class="btn-icon-sm" onclick="openPanelPhotoUpload()">+</button>
|
||||
</div>
|
||||
<div class="panel-photo-grid" id="panelPhotoGrid">
|
||||
<div class="panel-empty">등록된 사진이 없습니다</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 액션 버튼 -->
|
||||
<div class="panel-actions">
|
||||
<button class="panel-action-btn move" onclick="openPanelMoveModal()">
|
||||
<span>↔</span> 임시이동
|
||||
</button>
|
||||
<button class="panel-action-btn repair" onclick="openPanelRepairModal()">
|
||||
<span>🔧</span> 수리신청
|
||||
</button>
|
||||
<button class="panel-action-btn export" onclick="openPanelExportModal()">
|
||||
<span>🚚</span> 외부반출
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 수리 이력 -->
|
||||
<div class="panel-section">
|
||||
<h4 class="panel-section-title">수리 이력</h4>
|
||||
<div id="panelRepairHistory" class="panel-history-list">
|
||||
<div class="panel-empty">수리 이력이 없습니다</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 외부반출 이력 -->
|
||||
<div class="panel-section">
|
||||
<h4 class="panel-section-title">외부반출 이력</h4>
|
||||
<div id="panelExternalHistory" class="panel-history-list">
|
||||
<div class="panel-empty">외부반출 이력이 없습니다</div>
|
||||
</div>
|
||||
<button class="panel-action-btn move" onclick="openPanelMoveModal()"><span>↔</span> 임시이동</button>
|
||||
<button class="panel-action-btn repair" onclick="openPanelRepairModal()"><span>🔧</span> 수리신청</button>
|
||||
<button class="panel-action-btn export" onclick="openPanelExportModal()"><span>🚚</span> 외부반출</button>
|
||||
</div>
|
||||
<div class="panel-section"><h4 class="panel-section-title">수리 이력</h4><div id="panelRepairHistory" class="panel-history-list"><div class="panel-empty">수리 이력이 없습니다</div></div></div>
|
||||
<div class="panel-section"><h4 class="panel-section-title">외부반출 이력</h4><div id="panelExternalHistory" class="panel-history-list"><div class="panel-empty">외부반출 이력이 없습니다</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,10 +231,7 @@
|
||||
<!-- 설비 사진 업로드 모달 -->
|
||||
<div id="panelPhotoModal" class="mini-modal-overlay" style="display:none;">
|
||||
<div class="mini-modal">
|
||||
<div class="mini-modal-header">
|
||||
<h4>사진 추가</h4>
|
||||
<button onclick="closePanelPhotoModal()">×</button>
|
||||
</div>
|
||||
<div class="mini-modal-header"><h4>사진 추가</h4><button onclick="closePanelPhotoModal()">×</button></div>
|
||||
<div class="mini-modal-body">
|
||||
<input type="file" id="panelPhotoInput" accept="image/*" onchange="previewPanelPhoto(event)">
|
||||
<div id="panelPhotoPreview" class="mini-photo-preview"></div>
|
||||
@@ -377,45 +247,22 @@
|
||||
<!-- 설비 임시이동 모달 -->
|
||||
<div id="panelMoveModal" class="mini-modal-overlay" style="display:none;">
|
||||
<div class="mini-modal" style="max-width:700px;">
|
||||
<div class="mini-modal-header">
|
||||
<h4 id="panelMoveTitle">설비 임시 이동</h4>
|
||||
<button onclick="closePanelMoveModal()">×</button>
|
||||
</div>
|
||||
<div class="mini-modal-header"><h4 id="panelMoveTitle">설비 임시 이동</h4><button onclick="closePanelMoveModal()">×</button></div>
|
||||
<div class="mini-modal-body" style="padding:0;">
|
||||
<!-- Step 1: 공장 선택 (대분류 지도) -->
|
||||
<div id="moveStep1" class="move-step-content">
|
||||
<div class="move-step-header">
|
||||
<span class="step-badge">1</span>
|
||||
<span>공장 선택</span>
|
||||
<div class="move-step-header"><span class="step-badge">1</span><span>공장 선택</span></div>
|
||||
<div class="move-factory-grid" id="moveFactoryGrid"></div>
|
||||
</div>
|
||||
<div class="move-factory-grid" id="moveFactoryGrid">
|
||||
<!-- 공장 카드들 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: 작업장 선택 (공장 레이아웃 지도) -->
|
||||
<div id="moveStep2" class="move-step-content" style="display:none;">
|
||||
<div class="move-step-header">
|
||||
<button class="btn-step-back" onclick="moveBackToStep1()">←</button>
|
||||
<span class="step-badge">2</span>
|
||||
<span id="moveStep2Title">작업장 선택</span>
|
||||
</div>
|
||||
<div class="move-step-header"><button class="btn-step-back" onclick="moveBackToStep1()">←</button><span class="step-badge">2</span><span id="moveStep2Title">작업장 선택</span></div>
|
||||
<p class="move-help-text">지도에서 이동할 작업장을 클릭하세요</p>
|
||||
<div class="move-layout-map" id="moveLayoutMapContainer"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 위치 선택 (상세 지도) -->
|
||||
<div id="moveStep3" class="move-step-content" style="display:none;">
|
||||
<div class="move-step-header">
|
||||
<button class="btn-step-back" onclick="moveBackToStep2()">←</button>
|
||||
<span class="step-badge">3</span>
|
||||
<span id="moveStep3Title">위치 선택</span>
|
||||
</div>
|
||||
<div class="move-step-header"><button class="btn-step-back" onclick="moveBackToStep2()">←</button><span class="step-badge">3</span><span id="moveStep3Title">위치 선택</span></div>
|
||||
<p class="move-help-text">지도에서 설비를 배치할 위치를 클릭하세요</p>
|
||||
<div class="move-detail-map" id="moveDetailMapContainer"></div>
|
||||
<div class="form-group" style="padding:12px;">
|
||||
<input type="text" id="panelMoveReason" class="form-control" placeholder="이동 사유 (선택)">
|
||||
</div>
|
||||
<div class="form-group" style="padding:12px;"><input type="text" id="panelMoveReason" class="form-control" placeholder="이동 사유 (선택)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-modal-footer">
|
||||
@@ -428,29 +275,12 @@
|
||||
<!-- 설비 수리신청 모달 -->
|
||||
<div id="panelRepairModal" class="mini-modal-overlay" style="display:none;">
|
||||
<div class="mini-modal">
|
||||
<div class="mini-modal-header">
|
||||
<h4>수리 신청</h4>
|
||||
<button onclick="closePanelRepairModal()">×</button>
|
||||
</div>
|
||||
<div class="mini-modal-header"><h4>수리 신청</h4><button onclick="closePanelRepairModal()">×</button></div>
|
||||
<div class="mini-modal-body">
|
||||
<div class="form-group">
|
||||
<label>수리 유형</label>
|
||||
<select id="panelRepairItem" class="form-control" onchange="onRepairTypeChange()">
|
||||
<option value="">선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="newRepairTypeGroup" style="display:none;">
|
||||
<label>새 유형 이름</label>
|
||||
<input type="text" id="newRepairTypeName" class="form-control" placeholder="새로운 수리 유형 입력">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>상세 내용</label>
|
||||
<textarea id="panelRepairDesc" class="form-control" rows="3" placeholder="수리 필요 내용"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>사진 첨부</label>
|
||||
<input type="file" id="panelRepairPhotoInput" accept="image/*" multiple>
|
||||
</div>
|
||||
<div class="form-group"><label>수리 유형</label><select id="panelRepairItem" class="form-control" onchange="onRepairTypeChange()"><option value="">선택하세요</option></select></div>
|
||||
<div class="form-group" id="newRepairTypeGroup" style="display:none;"><label>새 유형 이름</label><input type="text" id="newRepairTypeName" class="form-control" placeholder="새로운 수리 유형 입력"></div>
|
||||
<div class="form-group"><label>상세 내용</label><textarea id="panelRepairDesc" class="form-control" rows="3" placeholder="수리 필요 내용"></textarea></div>
|
||||
<div class="form-group"><label>사진 첨부</label><input type="file" id="panelRepairPhotoInput" accept="image/*" multiple></div>
|
||||
</div>
|
||||
<div class="mini-modal-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="closePanelRepairModal()">취소</button>
|
||||
@@ -462,32 +292,13 @@
|
||||
<!-- 설비 외부반출 모달 -->
|
||||
<div id="panelExportModal" class="mini-modal-overlay" style="display:none;">
|
||||
<div class="mini-modal">
|
||||
<div class="mini-modal-header">
|
||||
<h4>외부 반출</h4>
|
||||
<button onclick="closePanelExportModal()">×</button>
|
||||
</div>
|
||||
<div class="mini-modal-header"><h4>외부 반출</h4><button onclick="closePanelExportModal()">×</button></div>
|
||||
<div class="mini-modal-body">
|
||||
<div class="form-group">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="panelIsRepairExport"> 수리 외주
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>반출일</label>
|
||||
<input type="date" id="panelExportDate" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>반입 예정일</label>
|
||||
<input type="date" id="panelExpectedReturn" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>반출처</label>
|
||||
<input type="text" id="panelExportDest" class="form-control" placeholder="업체명">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>반출 사유</label>
|
||||
<textarea id="panelExportReason" class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="form-group"><label class="checkbox-inline"><input type="checkbox" id="panelIsRepairExport"> 수리 외주</label></div>
|
||||
<div class="form-group"><label>반출일</label><input type="date" id="panelExportDate" class="form-control"></div>
|
||||
<div class="form-group"><label>반입 예정일</label><input type="date" id="panelExpectedReturn" class="form-control"></div>
|
||||
<div class="form-group"><label>반출처</label><input type="text" id="panelExportDest" class="form-control" placeholder="업체명"></div>
|
||||
<div class="form-group"><label>반출 사유</label><textarea id="panelExportReason" class="form-control" rows="2"></textarea></div>
|
||||
</div>
|
||||
<div class="mini-modal-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="closePanelExportModal()">취소</button>
|
||||
@@ -499,24 +310,11 @@
|
||||
<!-- 설비 반입 모달 -->
|
||||
<div id="panelReturnModal" class="mini-modal-overlay" style="display:none;">
|
||||
<div class="mini-modal" style="max-width:350px;">
|
||||
<div class="mini-modal-header">
|
||||
<h4>설비 반입</h4>
|
||||
<button onclick="closePanelReturnModal()">×</button>
|
||||
</div>
|
||||
<div class="mini-modal-header"><h4>설비 반입</h4><button onclick="closePanelReturnModal()">×</button></div>
|
||||
<div class="mini-modal-body">
|
||||
<input type="hidden" id="panelReturnLogId">
|
||||
<div class="form-group">
|
||||
<label>반입일</label>
|
||||
<input type="date" id="panelReturnDate" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>반입 후 상태</label>
|
||||
<select id="panelReturnStatus" class="form-control">
|
||||
<option value="active">정상 가동</option>
|
||||
<option value="maintenance">점검 필요</option>
|
||||
<option value="repair_needed">추가 수리 필요</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>반입일</label><input type="date" id="panelReturnDate" class="form-control"></div>
|
||||
<div class="form-group"><label>반입 후 상태</label><select id="panelReturnStatus" class="form-control"><option value="active">정상 가동</option><option value="maintenance">점검 필요</option><option value="repair_needed">추가 수리 필요</option></select></div>
|
||||
</div>
|
||||
<div class="mini-modal-footer">
|
||||
<button class="btn btn-secondary btn-sm" onclick="closePanelReturnModal()">취소</button>
|
||||
@@ -525,22 +323,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 하단 네비게이션 -->
|
||||
<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 type="module" src="/js/modern-dashboard.js?v=10"></script>
|
||||
<script type="module" src="/js/group-leader-dashboard.js?v=1"></script>
|
||||
<script src="/js/workplace-status.js?v=3"></script>
|
||||
<script src="/js/mobile-dashboard.js?v=4"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3,22 +3,34 @@
|
||||
<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">
|
||||
<title>일일순회점검 - TK 공장관리</title>
|
||||
<link rel="stylesheet" href="/css/daily-patrol.css?v=4">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 레이아웃 -->
|
||||
<div class="page-container">
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
@@ -124,7 +136,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 물품 추가/수정 모달 -->
|
||||
@@ -197,6 +209,9 @@
|
||||
}, 50);
|
||||
})();
|
||||
</script>
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/daily-patrol.js?v=6"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,22 +3,34 @@
|
||||
<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">
|
||||
<title>구역 상세 - TK 공장관리</title>
|
||||
<link rel="stylesheet" href="/css/zone-detail.css?v=4">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 레이아웃 -->
|
||||
<div class="page-container">
|
||||
<main class="main-content">
|
||||
<div class="dashboard-main">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="zone-header">
|
||||
<div class="zone-header-left">
|
||||
@@ -143,7 +155,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 현황 등록/수정 모달 -->
|
||||
@@ -292,6 +304,9 @@
|
||||
}, 50);
|
||||
})();
|
||||
</script>
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/zone-detail.js?v=6"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>내 프로필 | (주)테크니컬코리아</title>
|
||||
<link rel="stylesheet" href="/css/main-layout.css">
|
||||
<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">
|
||||
|
||||
<style>
|
||||
.profile-page {
|
||||
@@ -201,10 +203,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout-no-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="profile-page">
|
||||
<div class="profile-header">
|
||||
<div class="profile-avatar" id="profileAvatar"></div>
|
||||
@@ -297,7 +317,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script type="module" src="/js/my-profile.js"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,8 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>비밀번호 변경 | (주)테크니컬코리아</title>
|
||||
<link rel="stylesheet" href="/css/main-layout.css">
|
||||
<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">
|
||||
|
||||
<style>
|
||||
/* 페이지 전용 스타일 */
|
||||
@@ -269,10 +271,28 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-layout-no-sidebar">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<div class="password-page">
|
||||
<div class="page-title">
|
||||
<h1>비밀번호 변경</h1>
|
||||
@@ -367,7 +387,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script type="module" src="/js/change-password.js"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,635 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>안전 체크리스트 관리 - TK-FB</title>
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
<style>
|
||||
.page-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #111827);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 탭 메뉴 */
|
||||
.tab-menu {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #3b82f6;
|
||||
border-bottom-color: #3b82f6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 체크리스트 카드 */
|
||||
.checklist-group {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #f9fafb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.group-icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.group-count {
|
||||
background: #e5e7eb;
|
||||
color: #6b7280;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.checklist-items {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.875rem 1.25rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.checklist-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.item-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-required {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.badge-optional {
|
||||
background: #e5e7eb;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.btn-edit:hover {
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
/* 필터/검색 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* 모달 */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-radio-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-radio input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-checkbox input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conditional-fields {
|
||||
display: none;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.conditional-fields.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #d1d5db;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 인라인 추가 행 */
|
||||
.inline-add-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: #f9fafb;
|
||||
border-top: 1px dashed #e5e7eb;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inline-add-input {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.inline-add-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.inline-add-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-add-btn:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.inline-add-select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.inline-add-standalone {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inline-add-standalone .inline-add-row {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* 날씨 아이콘 */
|
||||
.weather-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.weather-icon.rain::before { content: '[rain]'; }
|
||||
.weather-icon.snow::before { content: '[snow]'; }
|
||||
.weather-icon.heat::before { content: '[heat]'; }
|
||||
.weather-icon.cold::before { content: '[cold]'; }
|
||||
.weather-icon.wind::before { content: '[wind]'; }
|
||||
.weather-icon.fog::before { content: '[fog]'; }
|
||||
.weather-icon.dust::before { content: '[dust]'; }
|
||||
.weather-icon.clear::before { content: '[clear]'; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.checklist-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">안전 체크리스트 관리</h1>
|
||||
</div>
|
||||
|
||||
<!-- 탭 메뉴 -->
|
||||
<div class="tab-menu">
|
||||
<button class="tab-btn active" data-tab="basic" onclick="switchTab('basic')">
|
||||
기본 사항
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="weather" onclick="switchTab('weather')">
|
||||
날씨별
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="task" onclick="switchTab('task')">
|
||||
작업별
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 기본 사항 탭 -->
|
||||
<div id="basicTab" class="tab-content active">
|
||||
<div id="basicChecklistContainer">
|
||||
<!-- 동적 생성 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 날씨별 탭 -->
|
||||
<div id="weatherTab" class="tab-content">
|
||||
<div class="filter-bar">
|
||||
<select id="weatherFilter" class="filter-select" onchange="filterByWeather()">
|
||||
<option value="">모든 날씨 조건</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="weatherChecklistContainer">
|
||||
<!-- 동적 생성 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업별 탭 -->
|
||||
<div id="taskTab" class="tab-content">
|
||||
<div class="filter-bar">
|
||||
<select id="workTypeFilter" class="filter-select" onchange="filterByWorkType()">
|
||||
<option value="">공정 선택</option>
|
||||
</select>
|
||||
<select id="taskFilter" class="filter-select" onchange="filterByTask()">
|
||||
<option value="">작업 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="taskChecklistContainer">
|
||||
<!-- 동적 생성 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 추가/수정 모달 -->
|
||||
<div id="checkModal" class="modal-overlay">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="modalTitle">체크 항목 추가</h2>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="checkForm">
|
||||
<input type="hidden" id="checkId">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">유형</label>
|
||||
<div class="form-radio-group">
|
||||
<label class="form-radio">
|
||||
<input type="radio" name="checkType" value="basic" checked onchange="toggleConditionalFields()">
|
||||
<span>기본</span>
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input type="radio" name="checkType" value="weather" onchange="toggleConditionalFields()">
|
||||
<span>날씨별</span>
|
||||
</label>
|
||||
<label class="form-radio">
|
||||
<input type="radio" name="checkType" value="task" onchange="toggleConditionalFields()">
|
||||
<span>작업별</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 기본 유형: 카테고리 선택 -->
|
||||
<div id="basicFields" class="conditional-fields show">
|
||||
<div class="form-group">
|
||||
<label class="form-label">카테고리</label>
|
||||
<select id="checkCategory" class="form-select">
|
||||
<option value="PPE">PPE (개인보호장비)</option>
|
||||
<option value="EQUIPMENT">EQUIPMENT (장비점검)</option>
|
||||
<option value="ENVIRONMENT">ENVIRONMENT (작업환경)</option>
|
||||
<option value="EMERGENCY">EMERGENCY (비상대응)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 날씨별 유형: 날씨 조건 선택 -->
|
||||
<div id="weatherFields" class="conditional-fields">
|
||||
<div class="form-group">
|
||||
<label class="form-label">날씨 조건</label>
|
||||
<select id="weatherCondition" class="form-select">
|
||||
<!-- 동적 로드 -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업별 유형: 공정/작업 선택 -->
|
||||
<div id="taskFields" class="conditional-fields">
|
||||
<div class="form-group">
|
||||
<label class="form-label">공정</label>
|
||||
<select id="modalWorkType" class="form-select" onchange="loadModalTasks()">
|
||||
<option value="">공정 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">작업</label>
|
||||
<select id="modalTask" class="form-select">
|
||||
<option value="">작업 선택</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">체크 항목</label>
|
||||
<input type="text" id="checkItem" class="form-input" placeholder="예: 안전모 착용 확인" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">설명 (선택)</label>
|
||||
<textarea id="checkDescription" class="form-textarea" placeholder="항목에 대한 상세 설명"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" id="isRequired" checked>
|
||||
<span>필수 체크 항목</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">표시 순서</label>
|
||||
<input type="number" id="displayOrder" class="form-input" value="0" min="0">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="closeModal()">취소</button>
|
||||
<button type="button" class="btn-primary" onclick="saveCheck()">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<script type="module" src="/js/safety-checklist-manage.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>리다이렉트 중...</title>
|
||||
<script>
|
||||
(function() {
|
||||
var hostname = window.location.hostname;
|
||||
var protocol = window.location.protocol;
|
||||
var target;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
target = protocol + '//tkreport.technicalkorea.net/pages/safety/issue-detail.html';
|
||||
} else {
|
||||
target = protocol + '//' + hostname + ':30180/pages/safety/issue-detail.html';
|
||||
}
|
||||
window.location.replace(target + window.location.search);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>신고 시스템으로 이동 중...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>리다이렉트 중...</title>
|
||||
<script>
|
||||
(function() {
|
||||
var hostname = window.location.hostname;
|
||||
var protocol = window.location.protocol;
|
||||
var target;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
target = protocol + '//tkreport.technicalkorea.net/pages/safety/issue-report.html';
|
||||
} else {
|
||||
target = protocol + '//' + hostname + ':30180/pages/safety/issue-report.html';
|
||||
}
|
||||
window.location.replace(target + window.location.search);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>신고 시스템으로 이동 중...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,291 +0,0 @@
|
||||
<!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/common.css?v=2">
|
||||
<link rel="stylesheet" href="/css/project-management.css?v=3">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<style>
|
||||
.status-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.status-tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.status-tab:hover {
|
||||
color: var(--primary-600);
|
||||
}
|
||||
|
||||
.status-tab.active {
|
||||
color: var(--primary-600);
|
||||
border-bottom-color: var(--primary-600);
|
||||
}
|
||||
|
||||
.request-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.request-table th {
|
||||
background: var(--gray-50);
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.request-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.request-table tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: var(--yellow-100);
|
||||
color: var(--yellow-700);
|
||||
}
|
||||
|
||||
.status-badge.approved {
|
||||
background: var(--green-100);
|
||||
color: var(--green-700);
|
||||
}
|
||||
|
||||
.status-badge.rejected {
|
||||
background: var(--red-100);
|
||||
color: var(--red-700);
|
||||
}
|
||||
|
||||
.status-badge.training_completed {
|
||||
background: var(--blue-100);
|
||||
color: var(--blue-700);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 64px 32px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-left: 4px solid var(--primary-500);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<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="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">승인 대기</div>
|
||||
<div class="stat-value" id="statPending">0</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: var(--green-500);">
|
||||
<div class="stat-label">승인 완료</div>
|
||||
<div class="stat-value" id="statApproved">0</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: var(--blue-500);">
|
||||
<div class="stat-label">교육 완료</div>
|
||||
<div class="stat-value" id="statTrainingCompleted">0</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: var(--red-500);">
|
||||
<div class="stat-label">반려</div>
|
||||
<div class="stat-value" id="statRejected">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 탭 -->
|
||||
<div class="code-section">
|
||||
<div class="status-tabs">
|
||||
<button class="status-tab active" data-status="pending" onclick="switchTab('pending')">
|
||||
승인 대기
|
||||
</button>
|
||||
<button class="status-tab" data-status="approved" onclick="switchTab('approved')">
|
||||
승인 완료
|
||||
</button>
|
||||
<button class="status-tab" data-status="training_completed" onclick="switchTab('training_completed')">
|
||||
교육 완료
|
||||
</button>
|
||||
<button class="status-tab" data-status="rejected" onclick="switchTab('rejected')">
|
||||
반려
|
||||
</button>
|
||||
<button class="status-tab" data-status="all" onclick="switchTab('all')">
|
||||
전체
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 테이블 -->
|
||||
<div id="requestTableContainer">
|
||||
<!-- 동적으로 로드됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 상세보기 모달 -->
|
||||
<div id="detailModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>출입 신청 상세</h2>
|
||||
<button class="btn btn-secondary btn-sm" onclick="closeDetailModal()">닫기</button>
|
||||
</div>
|
||||
<div id="detailContent">
|
||||
<!-- 동적으로 로드됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 반려 사유 입력 모달 -->
|
||||
<div id="rejectModal" class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>반려 사유 입력</h2>
|
||||
<button class="btn btn-secondary btn-sm" onclick="closeRejectModal()">취소</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<label for="rejectionReason">반려 사유 *</label>
|
||||
<textarea id="rejectionReason" rows="4" style="width: 100%; padding: 12px; border: 1px solid var(--gray-300); border-radius: var(--radius-md);" placeholder="반려 사유를 입력하세요"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px;">
|
||||
<button class="btn btn-secondary" onclick="closeRejectModal()">취소</button>
|
||||
<button class="btn btn-danger" onclick="confirmReject()">반려 확정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/safety-management.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>리다이렉트 중...</title>
|
||||
<script>
|
||||
// 안전신고 현황 → System 2 (신고 시스템)로 리다이렉트
|
||||
(function() {
|
||||
var hostname = window.location.hostname;
|
||||
var protocol = window.location.protocol;
|
||||
var target;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
target = protocol + '//tkreport.technicalkorea.net/pages/safety/report-status.html';
|
||||
} else {
|
||||
target = protocol + '//' + hostname + ':30180/pages/safety/report-status.html';
|
||||
}
|
||||
window.location.replace(target + window.location.search);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>신고 시스템으로 이동 중...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,778 +0,0 @@
|
||||
<!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/common.css?v=2">
|
||||
<link rel="stylesheet" href="/css/project-management.css?v=3">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<style>
|
||||
/* 스텝 인디케이터 */
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #9ca3af;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: #e5e7eb;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.step.active .step-connector,
|
||||
.step.completed .step-connector {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.step.active {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.step.completed {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #f3f4f6;
|
||||
border: 2px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.step.active .step-number {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step.completed .step-number {
|
||||
background: #10b981;
|
||||
border-color: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 폼 섹션 */
|
||||
.form-section {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-section-title .section-number {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 지도 컨테이너 */
|
||||
.map-container {
|
||||
position: relative;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#issueMapCanvas {
|
||||
display: block;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.selected-location-info {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 0.5rem;
|
||||
color: #1e40af;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selected-location-info.empty {
|
||||
background: #f9fafb;
|
||||
border-color: #e5e7eb;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-location-toggle {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-location-toggle input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #3b82f6;
|
||||
}
|
||||
|
||||
.custom-location-input {
|
||||
margin-top: 0.75rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-location-input.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-location-input input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.custom-location-input input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 유형 선택 버튼 */
|
||||
.type-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
padding: 1.5rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 0.75rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
border-color: #d1d5db;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.type-btn.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.type-btn.nonconformity.selected {
|
||||
border-color: #f97316;
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
.type-btn.safety.selected {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.type-btn-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.type-btn-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.type-btn-desc {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 카테고리 선택 */
|
||||
#categoryContainer {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.category-btn:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.category-btn.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
/* 항목 선택 */
|
||||
.item-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-btn {
|
||||
padding: 0.625rem 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 9999px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.item-btn:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.item-btn.selected {
|
||||
border-color: #3b82f6;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.item-btn[data-severity="critical"].selected {
|
||||
background: #ef4444;
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.item-btn[data-severity="high"].selected {
|
||||
background: #f97316;
|
||||
border-color: #f97316;
|
||||
}
|
||||
|
||||
.item-btn.custom-input-btn {
|
||||
border-style: dashed;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.item-btn.custom-input-btn:hover {
|
||||
border-color: #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.item-btn.custom-input-btn.selected {
|
||||
border-style: solid;
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
/* 직접 입력 영역 */
|
||||
.custom-item-input {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-item-input input {
|
||||
flex: 1;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.custom-item-input input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.btn-confirm-custom {
|
||||
padding: 0.625rem 1rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-confirm-custom:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-cancel-custom {
|
||||
padding: 0.625rem 1rem;
|
||||
background: white;
|
||||
color: #4b5563;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-cancel-custom:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* 사진 업로드 */
|
||||
.photo-upload-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.photo-slot {
|
||||
aspect-ratio: 1;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.photo-slot:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.photo-slot.has-photo {
|
||||
border-style: solid;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.photo-slot img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.photo-slot .add-icon {
|
||||
font-size: 1.5rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.photo-slot .remove-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.photo-slot.has-photo .remove-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.photo-slot.has-photo .add-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 추가 설명 */
|
||||
.additional-textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.additional-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* 제출 버튼 */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
padding: 0.875rem 2rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-submit:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: 0.875rem 1.5rem;
|
||||
background: white;
|
||||
color: #4b5563;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* 작업 선택 모달 */
|
||||
.work-selection-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.work-selection-modal.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.work-selection-content {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.work-selection-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.work-option {
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.work-option:hover {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.work-option-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.work-option-desc {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 반응형 */
|
||||
@media (max-width: 768px) {
|
||||
.type-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.photo-upload-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<main class="work-report-main">
|
||||
<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="step-indicator">
|
||||
<div class="step active" data-step="1">
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-text">위치 선택</span>
|
||||
</div>
|
||||
<div class="step-connector"></div>
|
||||
<div class="step" data-step="2">
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-text">유형 선택</span>
|
||||
</div>
|
||||
<div class="step-connector"></div>
|
||||
<div class="step" data-step="3">
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-text">항목 선택</span>
|
||||
</div>
|
||||
<div class="step-connector"></div>
|
||||
<div class="step" data-step="4">
|
||||
<span class="step-number">4</span>
|
||||
<span class="step-text">사진/설명</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: 위치 선택 -->
|
||||
<div class="form-section" id="step1Section">
|
||||
<h2 class="form-section-title">
|
||||
<span class="section-number">1</span>
|
||||
발생 위치 선택
|
||||
</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">공장 선택</label>
|
||||
<select id="factorySelect" class="form-control">
|
||||
<option value="">공장을 선택하세요</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="map-container">
|
||||
<canvas id="issueMapCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="selected-location-info empty" id="selectedLocationInfo">
|
||||
지도에서 작업장을 클릭하여 위치를 선택하세요
|
||||
</div>
|
||||
|
||||
<div class="custom-location-toggle">
|
||||
<input type="checkbox" id="useCustomLocation">
|
||||
<label for="useCustomLocation">지도에 없는 위치 직접 입력</label>
|
||||
</div>
|
||||
|
||||
<div class="custom-location-input" id="customLocationInput">
|
||||
<input type="text" id="customLocation" placeholder="위치를 입력하세요 (예: 야적장 입구, 주차장 등)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: 문제 유형 선택 -->
|
||||
<div class="form-section" id="step2Section">
|
||||
<h2 class="form-section-title">
|
||||
<span class="section-number">2</span>
|
||||
문제 유형 선택
|
||||
</h2>
|
||||
|
||||
<div class="type-buttons">
|
||||
<div class="type-btn nonconformity" data-type="nonconformity">
|
||||
<div class="type-btn-icon">📋</div>
|
||||
<div class="type-btn-title">부적합 사항</div>
|
||||
<div class="type-btn-desc">자재, 설계, 검사 관련 문제</div>
|
||||
</div>
|
||||
<div class="type-btn safety" data-type="safety">
|
||||
<div class="type-btn-icon">⚠</div>
|
||||
<div class="type-btn-title">안전 문제</div>
|
||||
<div class="type-btn-desc">보호구, 위험구역, 안전수칙 관련</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="categoryContainer" style="display: none;">
|
||||
<label class="form-label">세부 카테고리</label>
|
||||
<div class="category-grid" id="categoryGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 신고 항목 선택 -->
|
||||
<div class="form-section" id="step3Section">
|
||||
<h2 class="form-section-title">
|
||||
<span class="section-number">3</span>
|
||||
신고 항목 선택
|
||||
</h2>
|
||||
<p style="color: #6b7280; margin-bottom: 1rem; font-size: 0.875rem;">해당하는 항목을 선택하거나 직접 입력하세요.</p>
|
||||
|
||||
<div class="item-grid" id="itemGrid">
|
||||
<p style="color: #9ca3af;">먼저 카테고리를 선택하세요</p>
|
||||
</div>
|
||||
|
||||
<!-- 직접 입력 영역 -->
|
||||
<div class="custom-item-input" id="customItemInput" style="display: none;">
|
||||
<input type="text" id="customItemName" placeholder="신고 항목을 직접 입력하세요..." maxlength="100">
|
||||
<button type="button" class="btn-confirm-custom" onclick="confirmCustomItem()">확인</button>
|
||||
<button type="button" class="btn-cancel-custom" onclick="cancelCustomItem()">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: 사진 및 추가 설명 -->
|
||||
<div class="form-section" id="step4Section">
|
||||
<h2 class="form-section-title">
|
||||
<span class="section-number">4</span>
|
||||
사진 및 추가 설명
|
||||
</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">사진 첨부 (최대 5장)</label>
|
||||
<div class="photo-upload-grid">
|
||||
<div class="photo-slot" data-index="0">
|
||||
<span class="add-icon">+</span>
|
||||
<button class="remove-btn" type="button">×</button>
|
||||
</div>
|
||||
<div class="photo-slot" data-index="1">
|
||||
<span class="add-icon">+</span>
|
||||
<button class="remove-btn" type="button">×</button>
|
||||
</div>
|
||||
<div class="photo-slot" data-index="2">
|
||||
<span class="add-icon">+</span>
|
||||
<button class="remove-btn" type="button">×</button>
|
||||
</div>
|
||||
<div class="photo-slot" data-index="3">
|
||||
<span class="add-icon">+</span>
|
||||
<button class="remove-btn" type="button">×</button>
|
||||
</div>
|
||||
<div class="photo-slot" data-index="4">
|
||||
<span class="add-icon">+</span>
|
||||
<button class="remove-btn" type="button">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="photoInput" accept="image/*" capture="environment" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="additionalDescription">추가 설명 (선택)</label>
|
||||
<textarea id="additionalDescription" class="additional-textarea" placeholder="추가로 설명이 필요한 내용을 입력하세요..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제출 버튼 -->
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn-cancel" onclick="history.back()">취소</button>
|
||||
<button type="button" class="btn-submit" id="submitBtn" onclick="submitReport()">신고 제출</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 작업 선택 모달 -->
|
||||
<div class="work-selection-modal" id="workSelectionModal">
|
||||
<div class="work-selection-content">
|
||||
<h3 class="work-selection-title">작업 선택</h3>
|
||||
<p style="margin-bottom: 1rem; color: #6b7280; font-size: 0.875rem;">이 위치에 등록된 작업이 있습니다. 연결할 작업을 선택하세요.</p>
|
||||
<div id="workOptionsList"></div>
|
||||
<button type="button" onclick="closeWorkModal()" style="width: 100%; padding: 0.75rem; margin-top: 0.5rem; background: #f3f4f6; border: none; border-radius: 0.5rem; cursor: pointer; font-size: 0.875rem;">
|
||||
작업 연결 없이 진행
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/issue-report.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,327 +0,0 @@
|
||||
<!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/common.css?v=2">
|
||||
<link rel="stylesheet" href="/css/project-management.css?v=3">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<style>
|
||||
.training-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.request-info-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-left: 4px solid var(--primary-500);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.checklist-section {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px solid transparent;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
border-color: var(--primary-300);
|
||||
background: var(--primary-50);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checklist-item label {
|
||||
flex: 1;
|
||||
font-size: var(--text-base);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked + label {
|
||||
color: var(--gray-500);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.signature-section {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.signature-canvas-container {
|
||||
border: 2px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
background: white;
|
||||
margin-top: 16px;
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.signature-canvas {
|
||||
display: block;
|
||||
cursor: crosshair;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.signature-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.signature-placeholder {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--gray-400);
|
||||
font-size: var(--text-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: var(--yellow-50);
|
||||
border: 2px solid var(--yellow-300);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
flex: 1;
|
||||
color: var(--yellow-800);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.saved-signature-card {
|
||||
background: var(--gray-50);
|
||||
border: 2px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.saved-signature-card img {
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-sm);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.saved-signature-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.saved-signature-number {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
color: var(--primary-600);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.saved-signature-date {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.saved-signature-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<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="training-container">
|
||||
<!-- 출입 신청 정보 -->
|
||||
<div class="request-info-card">
|
||||
<h2 class="section-title" style="margin-bottom: 16px;">출입 신청 정보</h2>
|
||||
<div id="requestInfo" class="info-grid">
|
||||
<!-- 동적으로 로드됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 안전교육 체크리스트 -->
|
||||
<div class="checklist-section">
|
||||
<h2 class="section-title" style="margin-bottom: 16px;">안전교육 체크리스트</h2>
|
||||
<p style="color: var(--gray-600); margin-bottom: 20px;">
|
||||
방문자에게 다음 안전 사항을 교육하고 체크해주세요.
|
||||
</p>
|
||||
|
||||
<div id="checklistContainer">
|
||||
<div class="checklist-item">
|
||||
<input type="checkbox" id="check1" name="safety-check" value="개인보호구 착용" onchange="updateCompleteButton()">
|
||||
<label for="check1">개인보호구(안전모, 안전화, 안전복) 착용 방법 교육</label>
|
||||
</div>
|
||||
<div class="checklist-item">
|
||||
<input type="checkbox" id="check2" name="safety-check" value="작업장 위험요소" onchange="updateCompleteButton()">
|
||||
<label for="check2">작업장 내 위험요소 및 주의사항 안내</label>
|
||||
</div>
|
||||
<div class="checklist-item">
|
||||
<input type="checkbox" id="check3" name="safety-check" value="비상대피로" onchange="updateCompleteButton()">
|
||||
<label for="check3">비상대피로 및 비상연락망 안내</label>
|
||||
</div>
|
||||
<div class="checklist-item">
|
||||
<input type="checkbox" id="check4" name="safety-check" value="출입통제구역" onchange="updateCompleteButton()">
|
||||
<label for="check4">출입통제구역 및 금지사항 안내</label>
|
||||
</div>
|
||||
<div class="checklist-item">
|
||||
<input type="checkbox" id="check5" name="safety-check" value="사고발생시 대응" onchange="updateCompleteButton()">
|
||||
<label for="check5">사고 발생 시 대응 절차 교육</label>
|
||||
</div>
|
||||
<div class="checklist-item">
|
||||
<input type="checkbox" id="check6" name="safety-check" value="안전수칙 준수" onchange="updateCompleteButton()">
|
||||
<label for="check6">현장 안전수칙 준수 서약</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 경고 -->
|
||||
<div class="warning-box">
|
||||
<div class="warning-icon"></div>
|
||||
<div class="warning-text">
|
||||
<strong>중요:</strong> 모든 체크리스트 항목을 완료하고 방문자의 서명을 받은 후 교육 완료 처리를 해주세요.
|
||||
교육 완료 후에는 수정할 수 없습니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 서명 섹션 -->
|
||||
<div class="signature-section">
|
||||
<h2 class="section-title" style="margin-bottom: 16px;">방문자 서명 (<span id="signatureCount">0</span>명)</h2>
|
||||
<p style="color: var(--gray-600); margin-bottom: 20px;">
|
||||
각 방문자가 왼쪽에 이름을 쓰고 오른쪽에 서명한 후 "저장" 버튼을 눌러주세요.
|
||||
</p>
|
||||
|
||||
<div class="signature-canvas-container" style="position: relative;">
|
||||
<!-- 이름과 서명 구분선 및 라벨 -->
|
||||
<div style="position: absolute; top: 10px; left: 10px; right: 10px; display: flex; justify-content: space-between; z-index: 1; pointer-events: none;">
|
||||
<span style="font-size: var(--text-sm); color: var(--gray-500); font-weight: 600;">이름</span>
|
||||
<span style="position: absolute; left: 250px; top: 0; bottom: 0; width: 2px; background: var(--gray-300);"></span>
|
||||
<span style="font-size: var(--text-sm); color: var(--gray-500); font-weight: 600); margin-left: auto;">서명</span>
|
||||
</div>
|
||||
<canvas id="signatureCanvas" class="signature-canvas" width="800" height="300"></canvas>
|
||||
<div id="signaturePlaceholder" class="signature-placeholder" style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
|
||||
<div>왼쪽에 이름을 쓰고, 오른쪽에 서명해주세요</div>
|
||||
<div style="font-size: var(--text-sm); color: var(--gray-400);">(마우스, 터치, 또는 Apple Pencil 사용)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="signature-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="clearSignature()">
|
||||
서명 지우기
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveSignature()">
|
||||
서명 저장
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="font-size: var(--text-sm); color: var(--gray-600); margin-top: 12px;">
|
||||
서명 날짜: <span id="signatureDate"></span>
|
||||
</div>
|
||||
|
||||
<!-- 저장된 서명 목록 -->
|
||||
<div id="savedSignatures" style="margin-top: 24px;">
|
||||
<!-- 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 제출 버튼 -->
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="goBack()">
|
||||
취소
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="completeTraining()" id="completeBtn" disabled>
|
||||
교육 완료 처리
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/safety-training-conduct.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,371 +0,0 @@
|
||||
<!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/common.css?v=2">
|
||||
<link rel="stylesheet" href="/css/project-management.css?v=3">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<style>
|
||||
.visit-form-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-base);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workplace-selection {
|
||||
padding: 20px;
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius-md);
|
||||
border: 2px dashed var(--gray-300);
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.workplace-selection:hover {
|
||||
border-color: var(--primary-500);
|
||||
background: var(--primary-50);
|
||||
}
|
||||
|
||||
.workplace-selection.selected {
|
||||
border-color: var(--primary-500);
|
||||
border-style: solid;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.workplace-selection .icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.workplace-selection .text {
|
||||
font-size: var(--text-base);
|
||||
color: var(--gray-600);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.workplace-selection.selected .text {
|
||||
color: var(--primary-600);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
/* 지도 모달 스타일 */
|
||||
.map-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.map-modal-content {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
padding: 32px;
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
.map-canvas-container {
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
border: 2px solid var(--gray-300);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.map-canvas {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.workplace-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--primary-50);
|
||||
border: 2px solid var(--primary-200);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.workplace-info-card .icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.workplace-info-card .details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.workplace-info-card .name {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
color: var(--primary-700);
|
||||
}
|
||||
|
||||
.workplace-info-card .category {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--gray-600);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.my-requests-section {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.request-card {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.request-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.request-status {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.request-status.pending {
|
||||
background: var(--yellow-100);
|
||||
color: var(--yellow-700);
|
||||
}
|
||||
|
||||
.request-status.approved {
|
||||
background: var(--green-100);
|
||||
color: var(--green-700);
|
||||
}
|
||||
|
||||
.request-status.rejected {
|
||||
background: var(--red-100);
|
||||
color: var(--red-700);
|
||||
}
|
||||
|
||||
.request-status.training_completed {
|
||||
background: var(--blue-100);
|
||||
color: var(--blue-700);
|
||||
}
|
||||
|
||||
.request-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--gray-500);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<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="visit-form-container">
|
||||
<div class="code-section">
|
||||
<h2 class="section-title">출입 정보 입력</h2>
|
||||
|
||||
<form id="visitRequestForm">
|
||||
<!-- 방문자 정보 -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="visitorCompany">방문자 소속 *</label>
|
||||
<input type="text" id="visitorCompany" placeholder="예: (주)협력업체, 일용직 등" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="visitorCount">방문 인원 *</label>
|
||||
<input type="number" id="visitorCount" value="1" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업장 선택 -->
|
||||
<div class="form-group">
|
||||
<label>방문 작업장 *</label>
|
||||
<div id="workplaceSelection" class="workplace-selection" onclick="openMapModal()">
|
||||
<div class="icon"></div>
|
||||
<div class="text">지도에서 작업장을 선택하세요</div>
|
||||
</div>
|
||||
<div id="selectedWorkplaceInfo" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- 방문 일시 -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="visitDate">방문 날짜 *</label>
|
||||
<input type="date" id="visitDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="visitTime">방문 시간 *</label>
|
||||
<input type="time" id="visitTime" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 방문 목적 -->
|
||||
<div class="form-group">
|
||||
<label for="visitPurpose">방문 목적 *</label>
|
||||
<select id="visitPurpose" required>
|
||||
<option value="">선택하세요</option>
|
||||
<!-- 동적으로 로드됨 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 비고 -->
|
||||
<div class="form-group">
|
||||
<label for="notes">비고 (선택)</label>
|
||||
<textarea id="notes" placeholder="추가 전달 사항이 있다면 입력하세요"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 -->
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="resetForm()">초기화</button>
|
||||
<button type="submit" class="btn btn-primary">출입 신청 및 안전교육 신청</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 내 신청 목록 -->
|
||||
<div class="my-requests-section">
|
||||
<div class="code-section">
|
||||
<h2 class="section-title">내 출입 신청 현황</h2>
|
||||
<div id="myRequestsList">
|
||||
<!-- 동적으로 로드됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 작업장 지도 모달 -->
|
||||
<div id="mapModal" class="map-modal">
|
||||
<div class="map-modal-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
||||
<h2 style="margin: 0;">작업장 선택</h2>
|
||||
<button class="btn btn-secondary" onclick="closeMapModal()">닫기</button>
|
||||
</div>
|
||||
|
||||
<!-- 구역(공장) 선택 -->
|
||||
<div class="form-group">
|
||||
<label for="categorySelect">구역(공장) 선택</label>
|
||||
<select id="categorySelect" onchange="loadWorkplaceMap()">
|
||||
<option value="">구역을 선택하세요</option>
|
||||
<!-- 동적으로 로드됨 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 지도 캔버스 -->
|
||||
<div id="mapCanvasContainer" style="display: none;">
|
||||
<div class="map-canvas-container">
|
||||
<canvas id="workplaceMapCanvas" class="map-canvas"></canvas>
|
||||
</div>
|
||||
<p style="margin-top: 12px; color: var(--gray-600); font-size: var(--text-sm);">
|
||||
지도에서 방문할 작업장을 클릭하세요
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/visit-request.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,23 +3,35 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>작업 분석 | (주)테크니컬코리아</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<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/work-analysis.css?v=41">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<div class="analysis-container">
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
<!-- 뒤로가기 버튼 -->
|
||||
<a href="javascript:history.back()" class="back-button" style="margin: 1rem 0;">
|
||||
← 뒤로가기
|
||||
@@ -262,9 +274,11 @@
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script type="module" src="/js/work-analysis.js?v=5"></script>
|
||||
|
||||
<script>
|
||||
@@ -2855,5 +2869,6 @@
|
||||
// 초기 모드 설정
|
||||
window.currentAnalysisMode = 'period';
|
||||
</script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,278 +3,71 @@
|
||||
<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/common.css?v=2">
|
||||
<link rel="stylesheet" href="/css/project-management.css?v=3">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<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>
|
||||
<style>
|
||||
/* 통계 카드 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.25rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.stat-card.reported .stat-number { color: #3b82f6; }
|
||||
.stat-card.received .stat-number { color: #f97316; }
|
||||
.stat-card.in_progress .stat-number { color: #8b5cf6; }
|
||||
.stat-card.completed .stat-number { color: #10b981; }
|
||||
|
||||
/* 필터 바 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.filter-bar select,
|
||||
.filter-bar input {
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.filter-bar select:focus,
|
||||
.filter-bar input:focus {
|
||||
outline: none;
|
||||
border-color: #f97316;
|
||||
box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.1);
|
||||
}
|
||||
|
||||
.btn-new-report {
|
||||
margin-left: auto;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: #f97316;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-new-report:hover {
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
/* 신고 목록 */
|
||||
.issue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.issue-card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.issue-card:hover {
|
||||
border-color: #fed7aa;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.issue-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.issue-id {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.issue-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.issue-status.reported {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.issue-status.received {
|
||||
background: #fed7aa;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.issue-status.in_progress {
|
||||
background: #e9d5ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.issue-status.completed {
|
||||
background: #d1fae5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.issue-status.closed {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.issue-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.issue-category-badge {
|
||||
display: inline-block;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.issue-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.issue-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.issue-photos {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.issue-photos img {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 빈 상태 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 1.5rem;
|
||||
color: #6b7280;
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.btn-new-report {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<div id="navbar-container"></div>
|
||||
|
||||
<main class="work-report-main">
|
||||
<div class="dashboard-main">
|
||||
<div class="page-header">
|
||||
<div class="page-title-section">
|
||||
<h1 class="page-title">부적합 현황</h1>
|
||||
<p class="page-description">자재, 설계, 검사 등 작업 관련 부적합 신고 현황입니다.</p>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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 class="flex-1 min-w-0">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="mb-5">
|
||||
<h2 class="text-xl font-bold text-gray-800">부적합 현황</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">자재, 설계, 검사 등 작업 관련 부적합 신고 현황입니다.</p>
|
||||
</div>
|
||||
|
||||
<!-- 통계 카드 -->
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card reported">
|
||||
<div class="stat-number" id="statReported">-</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5" id="statsGrid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-blue-600" id="statReported">-</div>
|
||||
<div class="stat-label">신고</div>
|
||||
</div>
|
||||
<div class="stat-card received">
|
||||
<div class="stat-number" id="statReceived">-</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-orange-600" id="statReceived">-</div>
|
||||
<div class="stat-label">접수</div>
|
||||
</div>
|
||||
<div class="stat-card in_progress">
|
||||
<div class="stat-number" id="statProgress">-</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-purple-600" id="statProgress">-</div>
|
||||
<div class="stat-label">처리중</div>
|
||||
</div>
|
||||
<div class="stat-card completed">
|
||||
<div class="stat-number" id="statCompleted">-</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value text-green-600" id="statCompleted">-</div>
|
||||
<div class="stat-label">완료</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 바 -->
|
||||
<div class="filter-bar">
|
||||
<select id="filterStatus">
|
||||
<div class="bg-white rounded-xl shadow-sm p-4 mb-5 flex flex-wrap items-center gap-3">
|
||||
<select id="filterStatus" class="input-field px-3 py-2 rounded-lg text-sm">
|
||||
<option value="">전체 상태</option>
|
||||
<option value="reported">신고</option>
|
||||
<option value="received">접수</option>
|
||||
@@ -282,25 +75,22 @@
|
||||
<option value="completed">완료</option>
|
||||
<option value="closed">종료</option>
|
||||
</select>
|
||||
|
||||
<input type="date" id="filterStartDate" title="시작일">
|
||||
<input type="date" id="filterEndDate" title="종료일">
|
||||
|
||||
<a href="/pages/safety/report.html?type=nonconformity" class="btn-new-report">
|
||||
+ 부적합 신고
|
||||
<input type="date" id="filterStartDate" class="input-field px-3 py-2 rounded-lg text-sm" title="시작일">
|
||||
<input type="date" id="filterEndDate" class="input-field px-3 py-2 rounded-lg text-sm" title="종료일">
|
||||
<a id="btnNewReport" href="#" class="ml-auto inline-flex items-center gap-2 px-4 py-2 bg-orange-600 text-white rounded-lg text-sm font-semibold hover:bg-orange-700 transition-colors">
|
||||
<i class="fas fa-plus"></i>부적합 신고
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 신고 목록 -->
|
||||
<div class="issue-list" id="issueList">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-title">로딩 중...</div>
|
||||
<div id="issueList" class="space-y-3">
|
||||
<div class="bg-white rounded-xl shadow-sm p-8 text-center text-gray-400 text-sm">로딩 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/nonconformity-list.js?v=2"></script>
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/static/js/tkfb-nonconformity.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>작업보고서 | (주)테크니컬코리아</title>
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<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-mobile.css?v=1">
|
||||
<link rel="stylesheet" href="/css/mobile.css?v=1">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<style>
|
||||
/* 데스크탑이면 리다이렉트 */
|
||||
@media (min-width: 769px) {
|
||||
@@ -26,11 +24,29 @@
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 네비게이션 바 (app-init에서 로드) -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<div class="m-container">
|
||||
<!-- Sticky 헤더 -->
|
||||
<div class="m-header">
|
||||
<h1 class="m-header-title">작업보고서</h1>
|
||||
@@ -85,7 +101,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 시간 선택 오버레이 -->
|
||||
<div class="m-time-overlay" id="mTimeOverlay" onclick="MobileReport.closeTimePicker()">
|
||||
@@ -169,7 +184,13 @@
|
||||
<!-- 토스트 -->
|
||||
<div class="m-toast" id="mToast"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공통 모듈 -->
|
||||
<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>
|
||||
|
||||
@@ -180,19 +201,6 @@
|
||||
|
||||
<!-- 모바일 전용 UI 로직 -->
|
||||
<script src="/js/daily-work-report-mobile.js?v=4"></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>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<!-- Mobile overlay -->
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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 class="flex-1 min-w-0">
|
||||
<!-- 탭 메뉴 -->
|
||||
<div class="tab-menu" style="margin-bottom: var(--space-6);">
|
||||
<div class="tab-menu" style="margin-bottom: 1.5rem;">
|
||||
<button class="tab-btn active" id="tbmReportTab" onclick="switchTab('tbm')">
|
||||
작업보고서 작성
|
||||
</button>
|
||||
@@ -35,26 +55,19 @@
|
||||
|
||||
<!-- TBM 작업보고 섹션 -->
|
||||
<div id="tbmReportSection" class="step-section active">
|
||||
<!-- TBM 작업 목록 -->
|
||||
<div id="tbmWorkList">
|
||||
<!-- TBM 작업 항목들이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
<div id="tbmWorkList"></div>
|
||||
</div>
|
||||
|
||||
<!-- 작성 완료 보고서 섹션 -->
|
||||
<div id="completedReportSection" class="step-section" style="display: none;">
|
||||
<!-- 날짜 선택 필터 -->
|
||||
<div class="form-group" style="max-width: 300px; margin-bottom: var(--space-5);">
|
||||
<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 id="completedReportsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- 저장 결과 모달 -->
|
||||
@@ -65,14 +78,10 @@
|
||||
<button class="modal-close-btn" onclick="closeSaveResultModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="resultModalContent" class="result-content">
|
||||
<!-- 결과 내용이 여기에 동적으로 추가됩니다 -->
|
||||
</div>
|
||||
<div id="resultModalContent" class="result-content"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="closeSaveResultModal()">
|
||||
확인
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="closeSaveResultModal()">확인</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,40 +94,26 @@
|
||||
<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;">×</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 id="workplaceCategoryList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;"></div>
|
||||
</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="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 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>
|
||||
|
||||
<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>
|
||||
@@ -135,25 +130,13 @@
|
||||
<h3 id="timePickerTitle">작업시간 선택</h3>
|
||||
<button class="time-picker-close" onclick="closeTimePicker()">×</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>
|
||||
<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>
|
||||
@@ -162,35 +145,18 @@
|
||||
<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="/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>
|
||||
|
||||
<!-- 기존 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>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,27 +3,18 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>TBM 시작 | (주)테크니컬코리아</title>
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<title>TBM 등록 - 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">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;
|
||||
background: #f3f4f6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
button, .worker-card, .list-item, .list-item-skip, .pill-btn, .pill-btn-add,
|
||||
.nav-btn, .select-all-btn, [onclick] {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@media (min-width: 480px) {
|
||||
body { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
||||
.tbm-create-wrap { max-width: 480px; margin: 0 auto; min-height: 100vh; }
|
||||
}
|
||||
|
||||
/* Fixed header */
|
||||
@@ -781,7 +772,34 @@
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<div class="tbm-create-wrap">
|
||||
<!-- Fixed Header -->
|
||||
<div class="wizard-header">
|
||||
<button type="button" class="back-btn" onclick="goBack()">←</button>
|
||||
@@ -818,15 +836,22 @@
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<!-- 공통 모듈 -->
|
||||
<script src="/js/common/utils.js?v=2"></script>
|
||||
<script src="/js/common/base-state.js?v=2"></script>
|
||||
|
||||
<script src="/js/tbm/state.js?v=3"></script>
|
||||
<script src="/js/tbm/utils.js?v=3"></script>
|
||||
<script src="/js/tbm/api.js?v=4"></script>
|
||||
<script src="/js/tbm-create.js?v=14"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,14 +3,35 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>TBM | (주)테크니컬코리아</title>
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<title>TBM - 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/tbm-mobile.css?v=1">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
</header>
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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">
|
||||
<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 class="flex-1 min-w-0">
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loadingOverlay" class="m-loading-overlay">
|
||||
@@ -270,7 +291,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 공통 모듈 -->
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/common/utils.js?v=2"></script>
|
||||
<script src="/js/common/base-state.js?v=2"></script>
|
||||
|
||||
@@ -278,5 +305,6 @@
|
||||
<script src="/js/tbm/utils.js?v=3"></script>
|
||||
<script src="/js/tbm/api.js?v=4"></script>
|
||||
<script src="/js/tbm-mobile.js?v=3"></script>
|
||||
<script>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,25 +3,43 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TBM 관리 | (주)테크니컬코리아</title>
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
<link rel="stylesheet" href="/css/common.css?v=2">
|
||||
<title>TBM 관리 - 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/tbm.css?v=5">
|
||||
<link rel="stylesheet" href="/css/mobile.css?v=1">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<!-- 최적화된 로딩: API 설정 → 앱 초기화 (병렬 컴포넌트 로딩) -->
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/app-init.js?v=9" defer></script>
|
||||
<!-- instant.page: 링크 호버 시 페이지 프리로딩 -->
|
||||
<script src="https://instant.page/5.2.0" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="work-report-container">
|
||||
<!-- 네비게이션 바 -->
|
||||
<div id="navbar-container"></div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<!-- 메인 콘텐츠 -->
|
||||
<main class="work-report-main">
|
||||
<!-- Mobile overlay -->
|
||||
<div id="mobileOverlay" class="hidden fixed inset-0 bg-black/50 z-30 lg:hidden"></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 class="flex-1 min-w-0">
|
||||
<div class="tbm-container">
|
||||
<!-- 페이지 헤더 -->
|
||||
<div class="tbm-page-header">
|
||||
@@ -80,11 +98,8 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="tbm-card-grid" id="todayTbmGrid">
|
||||
<!-- 오늘의 TBM 세션 카드들이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
<div class="tbm-card-grid" id="todayTbmGrid"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="tbm-empty-state" id="todayEmptyState" style="display: none;">
|
||||
<div class="tbm-empty-icon">📋</div>
|
||||
<h3 class="tbm-empty-title">오늘 등록된 TBM이 없습니다</h3>
|
||||
@@ -128,12 +143,8 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 날짜별 그룹 컨테이너 -->
|
||||
<div class="tbm-section-body" id="tbmDateGroupsContainer">
|
||||
<!-- 날짜별 TBM 그룹이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
<div class="tbm-section-body" id="tbmDateGroupsContainer"></div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div class="tbm-empty-state" id="emptyState" style="display: none;">
|
||||
<div class="tbm-empty-icon">📚</div>
|
||||
<h3 class="tbm-empty-title">등록된 TBM 세션이 없습니다</h3>
|
||||
@@ -142,29 +153,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TBM 생성 모달 (간소화) -->
|
||||
<!-- TBM 생성 모달 -->
|
||||
<div id="tbmModal" class="tbm-modal-overlay" style="display: none;">
|
||||
<div class="tbm-modal" style="max-width: 800px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title" id="modalTitle">
|
||||
<span>📝</span>
|
||||
새 TBM 시작
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title" id="modalTitle"><span>📝</span> 새 TBM 시작</h2>
|
||||
<button class="tbm-modal-close" onclick="closeTbmModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<form id="tbmForm" onsubmit="event.preventDefault(); saveTbmSession();">
|
||||
<input type="hidden" id="sessionId">
|
||||
|
||||
<!-- 기본 정보 섹션 -->
|
||||
<div class="tbm-form-section">
|
||||
<h3 class="tbm-form-section-title">
|
||||
<span>📅</span>
|
||||
기본 정보
|
||||
</h3>
|
||||
<h3 class="tbm-form-section-title"><span>📅</span> 기본 정보</h3>
|
||||
<div class="tbm-form-row">
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">TBM 날짜<span class="tbm-form-required">*</span></label>
|
||||
@@ -180,25 +184,18 @@
|
||||
<div class="tbm-form-row">
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">프로젝트</label>
|
||||
<select id="newTbmProjectId" class="tbm-form-input">
|
||||
<option value="">선택 안함</option>
|
||||
</select>
|
||||
<select id="newTbmProjectId" class="tbm-form-input"><option value="">선택 안함</option></select>
|
||||
</div>
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">공정<span class="tbm-form-required">*</span></label>
|
||||
<select id="newTbmWorkTypeId" class="tbm-form-input" required>
|
||||
<option value="">공정 선택...</option>
|
||||
</select>
|
||||
<select id="newTbmWorkTypeId" class="tbm-form-input" required><option value="">공정 선택...</option></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 선택 섹션 -->
|
||||
<div class="tbm-form-section">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 class="tbm-form-section-title" style="margin: 0; border: 0; padding: 0;">
|
||||
<span>👥</span>
|
||||
작업자 선택
|
||||
<span>👥</span> 작업자 선택
|
||||
<span id="newTbmWorkerCount" style="color: #3b82f6; font-size: 0.875rem;">(0명)</span>
|
||||
</h3>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
@@ -206,11 +203,7 @@
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="deselectAllNewTbmWorkers()">전체 해제</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="newTbmWorkerGrid" class="tbm-worker-select-grid">
|
||||
<!-- 작업자 체크박스 그리드가 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="newTbmWorkerGrid" class="tbm-worker-select-grid"></div>
|
||||
<div class="tbm-alert tbm-alert-info" style="margin-top: 1rem;">
|
||||
<span class="tbm-alert-icon">💡</span>
|
||||
<div class="tbm-alert-content">
|
||||
@@ -220,13 +213,9 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeTbmModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="saveTbmSession()">
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
저장하기
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="saveTbmSession()"><span class="tbm-btn-icon">✓</span> 저장하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,13 +224,9 @@
|
||||
<div id="bulkSettingModal" class="tbm-modal-overlay" style="display: none; z-index: 1101;">
|
||||
<div class="tbm-modal" style="max-width: 700px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>⚙</span>
|
||||
일괄 설정
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>⚙</span> 일괄 설정</h2>
|
||||
<button class="tbm-modal-close" onclick="closeBulkSettingModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<div class="tbm-alert tbm-alert-info">
|
||||
<span class="tbm-alert-icon">💡</span>
|
||||
@@ -250,8 +235,6 @@
|
||||
<div class="tbm-alert-text">선택한 작업자들의 <strong>첫 번째 작업 라인</strong>에 동일한 정보가 적용됩니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 선택 -->
|
||||
<div class="tbm-form-section">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem;">
|
||||
<label class="tbm-form-label">적용할 작업자 선택<span class="tbm-form-required">*</span></label>
|
||||
@@ -260,63 +243,38 @@
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="deselectAllForBulk()">해제</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bulkWorkerSelection" class="tbm-worker-select-grid" style="max-height: 180px;">
|
||||
<!-- 작업자 체크박스들이 여기에 생성됩니다 -->
|
||||
<div id="bulkWorkerSelection" class="tbm-worker-select-grid" style="max-height: 180px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-section" style="border-top: 1px solid #e2e8f0; padding-top: 1.5rem;">
|
||||
<h3 class="tbm-form-section-title" style="border: 0; padding: 0; margin-bottom: 1rem;">
|
||||
<span>🛠</span>
|
||||
적용할 작업 정보
|
||||
</h3>
|
||||
|
||||
<h3 class="tbm-form-section-title" style="border: 0; padding: 0; margin-bottom: 1rem;"><span>🛠</span> 적용할 작업 정보</h3>
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">프로젝트</label>
|
||||
<button type="button" id="bulkProjectBtn" onclick="openBulkItemSelect('project')" class="tbm-select-btn">
|
||||
프로젝트 선택
|
||||
<span class="tbm-select-arrow">▼</span>
|
||||
</button>
|
||||
<button type="button" id="bulkProjectBtn" onclick="openBulkItemSelect('project')" class="tbm-select-btn">프로젝트 선택 <span class="tbm-select-arrow">▼</span></button>
|
||||
<input type="hidden" id="bulkProjectId">
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-row">
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">공정<span class="tbm-form-required">*</span></label>
|
||||
<button type="button" id="bulkWorkTypeBtn" onclick="openBulkItemSelect('workType')" class="tbm-select-btn">
|
||||
공정 선택
|
||||
<span class="tbm-select-arrow">▼</span>
|
||||
</button>
|
||||
<button type="button" id="bulkWorkTypeBtn" onclick="openBulkItemSelect('workType')" class="tbm-select-btn">공정 선택 <span class="tbm-select-arrow">▼</span></button>
|
||||
<input type="hidden" id="bulkWorkTypeId">
|
||||
</div>
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">작업<span class="tbm-form-required">*</span></label>
|
||||
<button type="button" id="bulkTaskBtn" onclick="openBulkItemSelect('task')" class="tbm-select-btn" disabled>
|
||||
작업 선택
|
||||
<span class="tbm-select-arrow">▼</span>
|
||||
</button>
|
||||
<button type="button" id="bulkTaskBtn" onclick="openBulkItemSelect('task')" class="tbm-select-btn" disabled>작업 선택 <span class="tbm-select-arrow">▼</span></button>
|
||||
<input type="hidden" id="bulkTaskId">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">작업장<span class="tbm-form-required">*</span></label>
|
||||
<button type="button" id="bulkWorkplaceBtn" onclick="openBulkWorkplaceSelect()" class="tbm-select-btn">
|
||||
작업장 선택
|
||||
<span class="tbm-select-arrow">▼</span>
|
||||
</button>
|
||||
<button type="button" id="bulkWorkplaceBtn" onclick="openBulkWorkplaceSelect()" class="tbm-select-btn">작업장 선택 <span class="tbm-select-arrow">▼</span></button>
|
||||
<input type="hidden" id="bulkWorkplaceCategoryId">
|
||||
<input type="hidden" id="bulkWorkplaceId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeBulkSettingModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="applyBulkSettings()">
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
선택한 작업자에 적용
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="applyBulkSettings()"><span class="tbm-btn-icon">✓</span> 선택한 작업자에 적용</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -325,122 +283,81 @@
|
||||
<div id="workerSelectionModal" class="tbm-modal-overlay" style="display: none; z-index: 1101;">
|
||||
<div class="tbm-modal" style="max-width: 800px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>👥</span>
|
||||
작업자 선택
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>👥</span> 작업자 선택</h2>
|
||||
<button class="tbm-modal-close" onclick="closeWorkerSelectionModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<div style="margin-bottom: 1rem; display: flex; gap: 0.5rem;">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="selectAllWorkersInModal()">전체 선택</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="deselectAllWorkersInModal()">전체 해제</button>
|
||||
</div>
|
||||
|
||||
<div id="workerCardGrid" class="tbm-worker-select-grid">
|
||||
<!-- 작업자 카드들이 여기에 생성됩니다 -->
|
||||
<div id="workerCardGrid" class="tbm-worker-select-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeWorkerSelectionModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="confirmWorkerSelection()">
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
선택 완료
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="confirmWorkerSelection()"><span class="tbm-btn-icon">✓</span> 선택 완료</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 항목 선택 모달 (프로젝트/공정/작업 선택용) -->
|
||||
<!-- 항목 선택 모달 -->
|
||||
<div id="itemSelectModal" class="tbm-modal-overlay" style="display: none; z-index: 1102;">
|
||||
<div class="tbm-modal" style="max-width: 600px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title" id="itemSelectModalTitle">항목 선택</h2>
|
||||
<button class="tbm-modal-close" onclick="closeItemSelectModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<div id="itemSelectList" class="tbm-item-list">
|
||||
<!-- 선택 항목들이 여기에 생성됩니다 -->
|
||||
<div id="itemSelectList" class="tbm-item-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeItemSelectModal()">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업장 선택 모달 (2단계: 공장 → 작업장) -->
|
||||
<!-- 작업장 선택 모달 -->
|
||||
<div id="workplaceSelectModal" class="tbm-modal-overlay" style="display: none; z-index: 1102;">
|
||||
<div class="tbm-modal" style="max-width: 1000px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>🏭</span>
|
||||
작업장 선택
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>🏭</span> 작업장 선택</h2>
|
||||
<button class="tbm-modal-close" onclick="closeWorkplaceSelectModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<!-- 1단계: 공장 선택 -->
|
||||
<div class="tbm-form-section">
|
||||
<h3 class="tbm-form-section-title">
|
||||
<span style="background: #3b82f6; color: white; width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 0.8rem;">1</span>
|
||||
공장 선택
|
||||
</h3>
|
||||
<div id="categoryList" style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<!-- 공장 카테고리 버튼들이 여기에 생성됩니다 -->
|
||||
<div id="categoryList" style="display: flex; flex-wrap: wrap; gap: 0.5rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2단계: 작업장 선택 (지도 기본 + 리스트 토글) -->
|
||||
<div id="workplaceSelectionArea" style="display: none;">
|
||||
<div class="tbm-form-section">
|
||||
<h3 class="tbm-form-section-title">
|
||||
<span style="background: #3b82f6; color: white; width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 0.8rem;">2</span>
|
||||
작업장 선택
|
||||
</h3>
|
||||
|
||||
<!-- 지도 기반 선택 (기본 표시) -->
|
||||
<div id="layoutMapArea" style="display: none; padding: 1rem; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px;">
|
||||
<div style="font-size: 0.875rem; color: #64748b; margin-bottom: 0.75rem;">
|
||||
지도에서 작업장을 클릭하여 선택하세요
|
||||
</div>
|
||||
<div style="font-size: 0.875rem; color: #64748b; margin-bottom: 0.75rem;">지도에서 작업장을 클릭하여 선택하세요</div>
|
||||
<div class="tbm-workplace-map-container">
|
||||
<canvas id="workplaceMapCanvas"></canvas>
|
||||
</div>
|
||||
<button type="button" class="landscape-trigger-btn" id="landscapeTriggerBtn" onclick="openLandscapeMap()" style="display:none;">
|
||||
📺 전체화면 지도
|
||||
</button>
|
||||
<button type="button" class="landscape-trigger-btn" id="landscapeTriggerBtn" onclick="openLandscapeMap()" style="display:none;">📺 전체화면 지도</button>
|
||||
</div>
|
||||
|
||||
<!-- 리스트 기반 선택 (모바일에서 토글) -->
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm"
|
||||
onclick="toggleWorkplaceList()" id="toggleListBtn" style="display: none;">
|
||||
리스트로 선택
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="toggleWorkplaceList()" id="toggleListBtn" style="display: none;">리스트로 선택</button>
|
||||
<div id="workplaceListSection">
|
||||
<div id="workplaceList" class="tbm-item-list">
|
||||
<div style="color: #94a3b8; text-align: center; padding: 2rem;">
|
||||
공장을 먼저 선택해주세요
|
||||
<div style="color: #94a3b8; text-align: center; padding: 2rem;">공장을 먼저 선택해주세요</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeWorkplaceSelectModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="confirmWorkplaceSelection()" id="confirmWorkplaceBtn" disabled>
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
선택 완료
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="confirmWorkplaceSelection()" id="confirmWorkplaceBtn" disabled><span class="tbm-btn-icon">✓</span> 선택 완료</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -449,13 +366,9 @@
|
||||
<div id="teamModal" class="tbm-modal-overlay" style="display: none;">
|
||||
<div class="tbm-modal" style="max-width: 900px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>👥</span>
|
||||
팀 구성
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>👥</span> 팀 구성</h2>
|
||||
<button class="tbm-modal-close" onclick="closeTeamModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 class="tbm-form-section-title" style="margin: 0; border: 0; padding: 0;">작업자 선택</h3>
|
||||
@@ -464,27 +377,17 @@
|
||||
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="deselectAllWorkers()">전체 해제</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workerSelectionGrid" class="tbm-worker-select-grid">
|
||||
<!-- 작업자 체크박스 목록이 여기에 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<div id="workerSelectionGrid" class="tbm-worker-select-grid"></div>
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<h3 class="tbm-form-section-title">
|
||||
선택된 팀원 <span id="selectedCount" style="color: #3b82f6;">0</span>명
|
||||
</h3>
|
||||
<h3 class="tbm-form-section-title">선택된 팀원 <span id="selectedCount" style="color: #3b82f6;">0</span>명</h3>
|
||||
<div id="selectedWorkersList" style="display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 50px; padding: 0.75rem; background: #f8fafc; border-radius: 10px; border: 1px solid #e2e8f0;">
|
||||
<p style="margin: 0; color: #94a3b8; font-size: 0.875rem;">작업자를 선택해주세요</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeTeamModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="saveTeamComposition()">
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
팀 구성 완료
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="saveTeamComposition()"><span class="tbm-btn-icon">✓</span> 팀 구성 완료</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -493,25 +396,15 @@
|
||||
<div id="safetyModal" class="tbm-modal-overlay" style="display: none;">
|
||||
<div class="tbm-modal" style="max-width: 700px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>🛡</span>
|
||||
안전 체크리스트
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>🛡</span> 안전 체크리스트</h2>
|
||||
<button class="tbm-modal-close" onclick="closeSafetyModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<div id="safetyChecklistContainer" class="tbm-safety-list">
|
||||
<!-- 안전 체크리스트가 여기에 생성됩니다 -->
|
||||
<div id="safetyChecklistContainer" class="tbm-safety-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeSafetyModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-success" onclick="saveSafetyChecklist()">
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
안전 체크 완료
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-success" onclick="saveSafetyChecklist()"><span class="tbm-btn-icon">✓</span> 안전 체크 완료</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -520,13 +413,9 @@
|
||||
<div id="completeModal" class="tbm-modal-overlay" style="display: none;">
|
||||
<div class="tbm-modal" style="max-width: 500px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>✓</span>
|
||||
TBM 완료
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>✓</span> TBM 완료</h2>
|
||||
<button class="tbm-modal-close" onclick="closeCompleteModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<div class="tbm-alert tbm-alert-warning">
|
||||
<span class="tbm-alert-icon">⚠</span>
|
||||
@@ -535,12 +424,10 @@
|
||||
<div class="tbm-alert-text">완료 후에는 수정할 수 없습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-group" style="margin-top: 1.5rem;">
|
||||
<label class="tbm-form-label">종료 시간</label>
|
||||
<input type="time" id="endTime" class="tbm-form-input">
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-group" style="margin-top: 1rem;">
|
||||
<label class="tbm-form-label">작업자 근태</label>
|
||||
<div id="completeAttendanceList" style="max-height: 300px; overflow-y: auto; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 0.5rem;">
|
||||
@@ -548,13 +435,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeCompleteModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-success" id="completeModalBtn" onclick="completeTbmSession()">
|
||||
<span class="tbm-btn-icon">✓</span>
|
||||
완료
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-success" id="completeModalBtn" onclick="completeTbmSession()"><span class="tbm-btn-icon">✓</span> 완료</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -563,17 +446,12 @@
|
||||
<div id="handoverModal" class="tbm-modal-overlay" style="display: none;">
|
||||
<div class="tbm-modal" style="max-width: 600px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>👉</span>
|
||||
작업 인계
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>👉</span> 작업 인계</h2>
|
||||
<button class="tbm-modal-close" onclick="closeHandoverModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<form id="handoverForm">
|
||||
<input type="hidden" id="handoverSessionId">
|
||||
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">인계 사유<span class="tbm-form-required">*</span></label>
|
||||
<select id="handoverReason" class="tbm-form-input" required>
|
||||
@@ -584,14 +462,10 @@
|
||||
<option value="other">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">인수자 (다음 팀장)<span class="tbm-form-required">*</span></label>
|
||||
<select id="toLeaderId" class="tbm-form-input" required>
|
||||
<option value="">인수자 선택...</option>
|
||||
</select>
|
||||
<select id="toLeaderId" class="tbm-form-input" required><option value="">인수자 선택...</option></select>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-row">
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">인계 날짜<span class="tbm-form-required">*</span></label>
|
||||
@@ -602,27 +476,19 @@
|
||||
<input type="time" id="handoverTime" class="tbm-form-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label">인계 내용</label>
|
||||
<textarea id="handoverNotes" class="tbm-form-input" rows="4" placeholder="인수자에게 전달할 내용을 입력하세요" style="resize: vertical;"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="tbm-form-group">
|
||||
<label class="tbm-form-label" style="margin-bottom: 0.75rem;">인계할 팀원 선택</label>
|
||||
<div id="handoverTeamList" style="max-height: 200px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 10px; padding: 0.75rem; background: #f8fafc;">
|
||||
<!-- 팀원 체크박스 목록 -->
|
||||
</div>
|
||||
<div id="handoverTeamList" style="max-height: 200px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 10px; padding: 0.75rem; background: #f8fafc;"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeHandoverModal()">취소</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="saveHandover()">
|
||||
<span class="tbm-btn-icon">👉</span>
|
||||
인계 요청
|
||||
</button>
|
||||
<button type="button" class="tbm-btn tbm-btn-primary" onclick="saveHandover()"><span class="tbm-btn-icon">👉</span> 인계 요청</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -631,55 +497,30 @@
|
||||
<div id="detailModal" class="tbm-modal-overlay" style="display: none;">
|
||||
<div class="tbm-modal" style="max-width: 900px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2 class="tbm-modal-title">
|
||||
<span>📋</span>
|
||||
TBM 상세 정보
|
||||
</h2>
|
||||
<h2 class="tbm-modal-title"><span>📋</span> TBM 상세 정보</h2>
|
||||
<button class="tbm-modal-close" onclick="closeDetailModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-body">
|
||||
<!-- 세션 기본 정보 -->
|
||||
<div class="tbm-form-section">
|
||||
<h3 class="tbm-form-section-title">
|
||||
<span>📅</span>
|
||||
기본 정보
|
||||
</h3>
|
||||
<div id="detailBasicInfo" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;">
|
||||
<!-- 동적 생성 -->
|
||||
<h3 class="tbm-form-section-title"><span>📅</span> 기본 정보</h3>
|
||||
<div id="detailBasicInfo" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 팀 구성 -->
|
||||
<div class="tbm-form-section">
|
||||
<h3 class="tbm-form-section-title">
|
||||
<span>👥</span>
|
||||
팀 구성
|
||||
</h3>
|
||||
<div id="detailTeamMembers" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
<!-- 동적 생성 -->
|
||||
<h3 class="tbm-form-section-title"><span>👥</span> 팀 구성</h3>
|
||||
<div id="detailTeamMembers" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.75rem;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 안전 체크 -->
|
||||
<div class="tbm-form-section">
|
||||
<h3 class="tbm-form-section-title">
|
||||
<span>🛡</span>
|
||||
안전 체크리스트
|
||||
</h3>
|
||||
<div id="detailSafetyChecks">
|
||||
<!-- 동적 생성 -->
|
||||
<h3 class="tbm-form-section-title"><span>🛡</span> 안전 체크리스트</h3>
|
||||
<div id="detailSafetyChecks"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tbm-modal-footer" id="detailModalFooter">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeDetailModal()">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 가로모드 전체화면 지도 오버레이 -->
|
||||
<!-- 가로모드 전체화면 지도 -->
|
||||
<div id="landscapeOverlay" class="landscape-overlay" style="display:none;">
|
||||
<div id="landscapeInner" class="landscape-inner">
|
||||
<div class="landscape-header">
|
||||
@@ -695,65 +536,38 @@
|
||||
<!-- 분할 모달 -->
|
||||
<div id="splitModal" class="tbm-modal-overlay" style="display:none;">
|
||||
<div class="tbm-modal-container" style="max-width:500px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2>작업 분할</h2>
|
||||
<button type="button" class="tbm-modal-close" onclick="closeSplitModal()">×</button>
|
||||
</div>
|
||||
<div class="tbm-modal-header"><h2>작업 분할</h2><button type="button" class="tbm-modal-close" onclick="closeSplitModal()">×</button></div>
|
||||
<div class="tbm-modal-body">
|
||||
<p style="font-size:0.8125rem; color:#6b7280; margin-bottom:0.75rem;">작업자의 배정 시간을 분할합니다.</p>
|
||||
<div id="splitMemberList" style="display:flex; flex-direction:column; gap:0.5rem;"></div>
|
||||
</div>
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeSplitModal()">닫기</button>
|
||||
</div>
|
||||
<div class="tbm-modal-footer"><button type="button" class="tbm-btn tbm-btn-secondary" onclick="closeSplitModal()">닫기</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 빼오기 모달 -->
|
||||
<div id="pullModal" class="tbm-modal-overlay" style="display:none;">
|
||||
<div class="tbm-modal-container" style="max-width:500px;">
|
||||
<div class="tbm-modal-header">
|
||||
<h2>빼오기</h2>
|
||||
<button type="button" class="tbm-modal-close" onclick="closePullModal()">×</button>
|
||||
</div>
|
||||
<div class="tbm-modal-header"><h2>빼오기</h2><button type="button" class="tbm-modal-close" onclick="closePullModal()">×</button></div>
|
||||
<div class="tbm-modal-body">
|
||||
<p style="font-size:0.8125rem; color:#6b7280; margin-bottom:0.75rem;">다른 반장의 TBM에서 작업자를 빼옵니다.</p>
|
||||
<div id="pullSessionList"></div>
|
||||
</div>
|
||||
<div class="tbm-modal-footer">
|
||||
<button type="button" class="tbm-btn tbm-btn-secondary" onclick="closePullModal()">닫기</button>
|
||||
</div>
|
||||
<div class="tbm-modal-footer"><button type="button" class="tbm-btn tbm-btn-secondary" onclick="closePullModal()">닫기</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 토스트 알림 -->
|
||||
<!-- 토스트 -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
</div>
|
||||
|
||||
<!-- 공통 모듈 -->
|
||||
<script src="/static/js/tkfb-core.js"></script>
|
||||
<script src="/js/api-base.js?v=3"></script>
|
||||
<script src="/js/common/utils.js?v=2"></script>
|
||||
<script src="/js/common/base-state.js?v=2"></script>
|
||||
|
||||
<!-- TBM 모듈 (리팩토링된 구조) -->
|
||||
<script src="/js/tbm/state.js?v=3"></script>
|
||||
<script src="/js/tbm/utils.js?v=3"></script>
|
||||
<script src="/js/tbm/api.js?v=4"></script>
|
||||
|
||||
<!-- 기존 UI 로직 (점진적 마이그레이션) -->
|
||||
<script defer src="/js/tbm.js?v=13"></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>initAuth();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
59
system1-factory/web/static/css/tkfb.css
Normal file
59
system1-factory/web/static/css/tkfb.css
Normal file
@@ -0,0 +1,59 @@
|
||||
/* tkfb global styles — orange theme */
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; margin: 0; }
|
||||
.fade-in { opacity: 0; transition: opacity 0.3s; }
|
||||
.fade-in.visible { opacity: 1; }
|
||||
|
||||
/* Input */
|
||||
.input-field { border: 1px solid #e2e8f0; transition: border-color 0.15s; outline: none; }
|
||||
.input-field:focus { border-color: #ea580c; box-shadow: 0 0 0 3px rgba(234,88,12,0.1); }
|
||||
|
||||
/* Toast */
|
||||
.toast-message { transition: opacity 0.3s; }
|
||||
|
||||
/* Nav active */
|
||||
.nav-link.active { background: rgba(234,88,12,0.12); color: #c2410c; font-weight: 600; }
|
||||
|
||||
/* Nav category */
|
||||
.nav-category-header { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.5rem 1rem; background: none; border: none; cursor: pointer; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: #9ca3af; transition: color 0.15s; }
|
||||
.nav-category-header:hover { color: #6b7280; }
|
||||
.nav-category-header .nav-arrow { margin-left: auto; font-size: 0.6rem; transition: transform 0.2s; }
|
||||
.nav-category.expanded .nav-arrow { transform: rotate(180deg); }
|
||||
.nav-category-items { display: none; }
|
||||
.nav-category.expanded .nav-category-items { display: flex; flex-direction: column; gap: 1px; }
|
||||
|
||||
/* Stat card */
|
||||
.stat-card { background: white; border-radius: 0.75rem; padding: 1.25rem; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.stat-card .stat-value { font-size: 1.75rem; font-weight: 700; line-height: 1.2; }
|
||||
.stat-card .stat-label { font-size: 0.8rem; color: #6b7280; margin-top: 0.25rem; }
|
||||
|
||||
/* Table */
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
||||
.data-table th { background: #f1f5f9; padding: 0.625rem 0.75rem; text-align: left; font-weight: 600; color: #475569; white-space: nowrap; border-bottom: 2px solid #e2e8f0; }
|
||||
.data-table td { padding: 0.625rem 0.75rem; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
|
||||
.data-table tr:hover { background: #f8fafc; }
|
||||
|
||||
/* Badge */
|
||||
.badge { display: inline-flex; align-items: center; padding: 0.125rem 0.5rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; }
|
||||
.badge-green { background: #ecfdf5; color: #059669; }
|
||||
.badge-blue { background: #eff6ff; color: #2563eb; }
|
||||
.badge-amber { background: #fffbeb; color: #d97706; }
|
||||
.badge-red { background: #fef2f2; color: #dc2626; }
|
||||
.badge-gray { background: #f3f4f6; color: #6b7280; }
|
||||
.badge-orange { background: #fff7ed; color: #c2410c; }
|
||||
.badge-purple { background: #faf5ff; color: #7c3aed; }
|
||||
|
||||
/* Collapsible */
|
||||
.collapsible-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease; }
|
||||
.collapsible-content.open { max-height: 500px; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 50; padding: 1rem; }
|
||||
.modal-content { background: white; border-radius: 0.75rem; max-width: 40rem; width: 100%; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.2); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.stat-card .stat-value { font-size: 1.25rem; }
|
||||
.data-table { font-size: 0.8rem; }
|
||||
.data-table th, .data-table td { padding: 0.5rem; }
|
||||
.hide-mobile { display: none; }
|
||||
}
|
||||
281
system1-factory/web/static/js/tkfb-core.js
Normal file
281
system1-factory/web/static/js/tkfb-core.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/* ===== 서비스 워커 해제 ===== */
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(r) { r.unregister(); }); });
|
||||
if (typeof caches !== 'undefined') { caches.keys().then(function(ns) { ns.forEach(function(n) { caches.delete(n); }); }); }
|
||||
}
|
||||
|
||||
/* ===== Config ===== */
|
||||
const API_BASE = '/api';
|
||||
|
||||
/* ===== Token ===== */
|
||||
function _cookieGet(n) { const m = document.cookie.match(new RegExp('(?:^|; )' + n + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : null; }
|
||||
function _cookieRemove(n) { let c = n + '=; path=/; max-age=0'; if (location.hostname.includes('technicalkorea.net')) c += '; domain=.technicalkorea.net; secure; samesite=lax'; document.cookie = c; }
|
||||
function getToken() { return _cookieGet('sso_token') || localStorage.getItem('sso_token'); }
|
||||
function getLoginUrl() {
|
||||
const h = location.hostname;
|
||||
const t = Date.now();
|
||||
if (h.includes('technicalkorea.net')) return location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(location.href) + '&_t=' + t;
|
||||
return location.protocol + '//' + h + ':30000/login?redirect=' + encodeURIComponent(location.href) + '&_t=' + t;
|
||||
}
|
||||
function decodeToken(t) { try { const b = atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')); return JSON.parse(new TextDecoder().decode(Uint8Array.from(b, c => c.charCodeAt(0)))); } catch { return null; } }
|
||||
|
||||
/* ===== 리다이렉트 루프 방지 ===== */
|
||||
const _REDIRECT_KEY = '_sso_redirect_ts';
|
||||
function _safeRedirect() {
|
||||
const last = parseInt(sessionStorage.getItem(_REDIRECT_KEY) || '0', 10);
|
||||
if (Date.now() - last < 5000) { console.warn('[tkfb] 리다이렉트 루프 감지'); return; }
|
||||
sessionStorage.setItem(_REDIRECT_KEY, String(Date.now()));
|
||||
location.href = getLoginUrl();
|
||||
}
|
||||
|
||||
/* ===== API ===== */
|
||||
async function api(path, opts = {}) {
|
||||
const token = getToken();
|
||||
const headers = { 'Authorization': token ? `Bearer ${token}` : '', ...(opts.headers||{}) };
|
||||
if (!(opts.body instanceof FormData)) headers['Content-Type'] = 'application/json';
|
||||
const res = await fetch(API_BASE + path, { ...opts, headers });
|
||||
if (res.status === 401) { _safeRedirect(); throw new Error('인증 만료'); }
|
||||
if (res.headers.get('content-type')?.includes('text/csv')) return res;
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || data.message || '요청 실패');
|
||||
return data;
|
||||
}
|
||||
|
||||
/* ===== Toast ===== */
|
||||
function showToast(msg, type = 'success') {
|
||||
document.querySelector('.toast-message')?.remove();
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast-message fixed bottom-4 right-4 px-4 py-3 rounded-lg text-white z-[10000] shadow-lg ${type==='success'?'bg-orange-500':'bg-red-500'}`;
|
||||
el.innerHTML = `<i class="fas ${type==='success'?'fa-check-circle':'fa-exclamation-circle'} mr-2"></i>${escapeHtml(msg)}`;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => { el.classList.add('opacity-0'); setTimeout(() => el.remove(), 300); }, 3000);
|
||||
}
|
||||
|
||||
/* ===== Escape ===== */
|
||||
function escapeHtml(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
|
||||
|
||||
/* ===== Helpers ===== */
|
||||
function formatDate(d) { if (!d) return ''; return String(d).substring(0, 10); }
|
||||
function formatTime(d) { if (!d) return ''; return String(d).substring(11, 16); }
|
||||
function formatDateTime(d) { if (!d) return ''; return String(d).substring(0, 16).replace('T', ' '); }
|
||||
function debounce(fn, ms) { let t; return function(...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), ms); }; }
|
||||
|
||||
/* ===== Logout ===== */
|
||||
function doLogout() {
|
||||
if (!confirm('로그아웃?')) return;
|
||||
_cookieRemove('sso_token'); _cookieRemove('sso_user'); _cookieRemove('sso_refresh_token');
|
||||
['sso_token','sso_user','sso_refresh_token','token','user','access_token','currentUser','current_user','userInfo','userPageAccess'].forEach(k => localStorage.removeItem(k));
|
||||
location.href = getLoginUrl() + '&logout=1';
|
||||
}
|
||||
|
||||
/* ===== Page Access ===== */
|
||||
const _PA_CACHE_KEY = 'userPageAccess';
|
||||
const _PA_CACHE_DURATION = 5 * 60 * 1000; // 5분
|
||||
let _paPromise = null;
|
||||
|
||||
async function _fetchPageAccess(userId) {
|
||||
const cached = localStorage.getItem(_PA_CACHE_KEY);
|
||||
if (cached) {
|
||||
try {
|
||||
const c = JSON.parse(cached);
|
||||
if (Date.now() - c.timestamp < _PA_CACHE_DURATION) return c.keys;
|
||||
} catch { localStorage.removeItem(_PA_CACHE_KEY); }
|
||||
}
|
||||
if (_paPromise) return _paPromise;
|
||||
_paPromise = (async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/users/${userId}/page-access`, {
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
const pages = data.data?.pageAccess || [];
|
||||
const keys = pages.filter(p => p.can_access == 1).map(p => p.page_key);
|
||||
localStorage.setItem(_PA_CACHE_KEY, JSON.stringify({ keys, timestamp: Date.now() }));
|
||||
return keys;
|
||||
} catch (e) {
|
||||
console.error('페이지 권한 조회 오류:', e);
|
||||
return [];
|
||||
} finally { _paPromise = null; }
|
||||
})();
|
||||
return _paPromise;
|
||||
}
|
||||
|
||||
/* ===== Navbar ===== */
|
||||
const NAV_MENU = [
|
||||
{ cat: null, href: '/pages/dashboard-new.html', icon: 'fa-home', label: '대시보드', key: 'dashboard' },
|
||||
{ cat: '작업 관리', items: [
|
||||
{ href: '/pages/work/tbm.html', icon: 'fa-clipboard-list', label: 'TBM 관리', key: 'work.tbm' },
|
||||
{ href: '/pages/work/report-create.html', icon: 'fa-file-alt', label: '작업보고서 작성', key: 'work.report_create' },
|
||||
{ href: '/pages/work/analysis.html', icon: 'fa-chart-bar', label: '작업 분석', key: 'work.analysis', admin: true },
|
||||
{ href: '/pages/work/nonconformity.html', icon: 'fa-exclamation-triangle', label: '부적합 현황', key: 'work.nonconformity' },
|
||||
]},
|
||||
{ cat: '공장 관리', items: [
|
||||
{ href: '/pages/admin/repair-management.html', icon: 'fa-tools', label: '시설설비 관리', key: 'factory.repair_management' },
|
||||
{ href: '/pages/inspection/daily-patrol.html', icon: 'fa-route', label: '일일순회점검', key: 'inspection.daily_patrol' },
|
||||
{ href: '/pages/attendance/checkin.html', icon: 'fa-user-check', label: '출근 체크', key: 'inspection.checkin' },
|
||||
{ href: '/pages/attendance/work-status.html', icon: 'fa-briefcase', label: '근무 현황', key: 'inspection.work_status' },
|
||||
]},
|
||||
{ cat: '근태 관리', items: [
|
||||
{ href: '/pages/attendance/my-vacation-info.html', icon: 'fa-info-circle', label: '내 연차 정보', key: 'attendance.my_vacation_info' },
|
||||
{ href: '/pages/attendance/monthly.html', icon: 'fa-calendar', label: '월간 근태', key: 'attendance.monthly' },
|
||||
{ href: '/pages/attendance/vacation-request.html', icon: 'fa-paper-plane', label: '휴가 신청', key: 'attendance.vacation_request' },
|
||||
{ href: '/pages/attendance/vacation-management.html', icon: 'fa-cog', label: '휴가 관리', key: 'attendance.vacation_management', admin: true },
|
||||
{ href: '/pages/attendance/vacation-allocation.html', icon: 'fa-plus-circle', label: '휴가 발생 입력', key: 'attendance.vacation_allocation', admin: true },
|
||||
{ href: '/pages/attendance/annual-overview.html', icon: 'fa-chart-pie', label: '연간 휴가 현황', key: 'attendance.annual_overview', admin: true },
|
||||
]},
|
||||
{ cat: '시스템 관리', admin: true, items: [
|
||||
{ href: 'https://tkuser.technicalkorea.net', icon: 'fa-users-cog', label: '사용자 관리', key: 'admin.user_management', external: true },
|
||||
{ href: '/pages/admin/projects.html', icon: 'fa-project-diagram', label: '프로젝트 관리', key: 'admin.projects' },
|
||||
{ href: '/pages/admin/tasks.html', icon: 'fa-tasks', label: '작업 관리', key: 'admin.tasks' },
|
||||
{ href: '/pages/admin/workplaces.html', icon: 'fa-building', label: '작업장 관리', key: 'admin.workplaces' },
|
||||
{ href: '/pages/admin/equipments.html', icon: 'fa-cogs', label: '설비 관리', key: 'admin.equipments' },
|
||||
{ href: '/pages/admin/departments.html', icon: 'fa-sitemap', label: '부서 관리', key: 'admin.departments' },
|
||||
{ href: '/pages/admin/notifications.html', icon: 'fa-bell', label: '알림 관리', key: 'admin.notifications' },
|
||||
{ href: '/pages/admin/attendance-report.html', icon: 'fa-clipboard-check', label: '출퇴근-보고서 대조', key: 'admin.attendance_report' },
|
||||
]},
|
||||
];
|
||||
|
||||
// 하위 페이지 → 부모 페이지 키 매핑
|
||||
const PAGE_KEY_ALIASES = {
|
||||
'dashboard_new': 'dashboard',
|
||||
'work.tbm_mobile': 'work.tbm',
|
||||
'work.tbm_create': 'work.tbm',
|
||||
'work.report_create_mobile': 'work.report_create',
|
||||
'admin.equipment_detail': 'admin.equipments',
|
||||
'admin.repair_management': 'factory.repair_management',
|
||||
'attendance.checkin': 'inspection.checkin',
|
||||
'attendance.work_status': 'inspection.work_status',
|
||||
};
|
||||
|
||||
function _getCurrentPageKey() {
|
||||
const path = location.pathname;
|
||||
if (!path.startsWith('/pages/')) return 'dashboard';
|
||||
const raw = path.substring(7).replace('.html', '').replace(/\//g, '.').replace(/-/g, '_');
|
||||
return PAGE_KEY_ALIASES[raw] || raw;
|
||||
}
|
||||
|
||||
function renderNavbar(accessibleKeys) {
|
||||
const nav = document.getElementById('sideNav');
|
||||
if (!nav) return;
|
||||
|
||||
const currentKey = _getCurrentPageKey();
|
||||
const isAdmin = currentUser && ['admin', 'system', 'system admin'].includes(currentUser.role);
|
||||
|
||||
let html = '';
|
||||
|
||||
for (const entry of NAV_MENU) {
|
||||
if (!entry.cat) {
|
||||
// Top-level link (dashboard)
|
||||
const active = currentKey === entry.key;
|
||||
html += `<a href="${entry.href}" class="nav-link flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm transition-colors ${active ? 'active' : 'text-gray-600 hover:bg-gray-100'}">
|
||||
<i class="fas ${entry.icon} w-5 text-center"></i><span>${entry.label}</span></a>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Category
|
||||
if (entry.admin && !isAdmin) continue;
|
||||
|
||||
const visibleItems = entry.items.filter(item => {
|
||||
if (item.admin && !isAdmin) return false;
|
||||
if (isAdmin) return true;
|
||||
return accessibleKeys.includes(item.key);
|
||||
});
|
||||
|
||||
if (visibleItems.length === 0) continue;
|
||||
|
||||
const hasActive = visibleItems.some(item => currentKey === item.key);
|
||||
|
||||
html += `<div class="nav-category${hasActive ? ' expanded' : ''}">
|
||||
<button class="nav-category-header" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<span>${entry.cat}</span><span class="nav-arrow">▾</span>
|
||||
</button>
|
||||
<div class="nav-category-items">`;
|
||||
|
||||
for (const item of visibleItems) {
|
||||
const active = currentKey === item.key;
|
||||
const target = item.external ? ' target="_blank"' : '';
|
||||
const arrow = item.external ? ' ↗' : '';
|
||||
html += `<a href="${item.href}"${target} class="nav-link flex items-center gap-3 px-4 py-2 rounded-lg text-sm transition-colors ${active ? 'active' : 'text-gray-600 hover:bg-gray-100'}" data-page-key="${item.key}">
|
||||
<i class="fas ${item.icon} w-5 text-center text-xs"></i><span>${item.label}${arrow}</span></a>`;
|
||||
}
|
||||
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
nav.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ===== Mobile Menu ===== */
|
||||
function toggleMobileMenu() {
|
||||
const nav = document.getElementById('sideNav');
|
||||
const overlay = document.getElementById('mobileOverlay');
|
||||
if (!nav) return;
|
||||
const isOpen = nav.classList.contains('mobile-open');
|
||||
nav.classList.toggle('mobile-open');
|
||||
if (overlay) overlay.classList.toggle('hidden', isOpen);
|
||||
}
|
||||
|
||||
/* ===== State ===== */
|
||||
let currentUser = null;
|
||||
|
||||
/* ===== Init ===== */
|
||||
async function initAuth() {
|
||||
// 쿠키 우선 검증
|
||||
const cookieToken = _cookieGet('sso_token');
|
||||
const localToken = localStorage.getItem('sso_token');
|
||||
if (!cookieToken && localToken) {
|
||||
['sso_token','sso_user','sso_refresh_token','token','user','access_token',
|
||||
'currentUser','current_user','userInfo','userPageAccess'].forEach(k => localStorage.removeItem(k));
|
||||
_safeRedirect();
|
||||
return false;
|
||||
}
|
||||
|
||||
const token = getToken();
|
||||
if (!token) { _safeRedirect(); return false; }
|
||||
const decoded = decodeToken(token);
|
||||
if (!decoded) { _safeRedirect(); return false; }
|
||||
sessionStorage.removeItem(_REDIRECT_KEY);
|
||||
if (!localStorage.getItem('sso_token')) localStorage.setItem('sso_token', token);
|
||||
|
||||
currentUser = {
|
||||
id: decoded.user_id || decoded.id,
|
||||
username: decoded.username || decoded.sub,
|
||||
name: decoded.name || decoded.full_name,
|
||||
role: (decoded.role || decoded.access_level || '').toLowerCase()
|
||||
};
|
||||
|
||||
const dn = currentUser.name || currentUser.username;
|
||||
const nameEl = document.getElementById('headerUserName');
|
||||
const avatarEl = document.getElementById('headerUserAvatar');
|
||||
if (nameEl) nameEl.textContent = dn;
|
||||
if (avatarEl) avatarEl.textContent = dn.charAt(0).toUpperCase();
|
||||
|
||||
// Page access 기반 네비게이션
|
||||
const isAdmin = ['admin', 'system', 'system admin'].includes(currentUser.role);
|
||||
let accessibleKeys = [];
|
||||
if (!isAdmin) {
|
||||
accessibleKeys = await _fetchPageAccess(currentUser.id);
|
||||
// 현재 페이지 접근 권한 확인
|
||||
const pageKey = _getCurrentPageKey();
|
||||
if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) {
|
||||
if (!accessibleKeys.includes(pageKey)) {
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
location.href = '/pages/dashboard-new.html';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderNavbar(accessibleKeys);
|
||||
|
||||
// Mobile menu overlay
|
||||
const mobileBtn = document.getElementById('mobileMenuBtn');
|
||||
if (mobileBtn) mobileBtn.addEventListener('click', toggleMobileMenu);
|
||||
const overlay = document.getElementById('mobileOverlay');
|
||||
if (overlay) overlay.addEventListener('click', toggleMobileMenu);
|
||||
|
||||
setTimeout(() => document.querySelector('.fade-in')?.classList.add('visible'), 50);
|
||||
return true;
|
||||
}
|
||||
127
system1-factory/web/static/js/tkfb-dashboard.js
Normal file
127
system1-factory/web/static/js/tkfb-dashboard.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* ===== Dashboard (대시보드) ===== */
|
||||
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
|
||||
function updateDateTime() {
|
||||
const now = new Date();
|
||||
const days = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const h = String(now.getHours()).padStart(2, '0');
|
||||
const m = String(now.getMinutes()).padStart(2, '0');
|
||||
const el = document.getElementById('dateTimeDisplay');
|
||||
if (el) el.textContent = `${now.getFullYear()}년 ${now.getMonth()+1}월 ${now.getDate()}일 (${days[now.getDay()]}) ${h}:${m}`;
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
updateDateTime();
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
api('/tbm/sessions/date/' + today).catch(() => ({ data: [] })),
|
||||
api('/notifications/unread').catch(() => ({ data: [] })),
|
||||
api('/equipments/repair-requests?status=pending').catch(() => ({ data: [] })),
|
||||
api('/attendance/today-summary').catch(() => ({ data: {} })),
|
||||
]);
|
||||
|
||||
const tbmData = results[0].status === 'fulfilled' ? results[0].value : { data: [] };
|
||||
const notifData = results[1].status === 'fulfilled' ? results[1].value : { data: [] };
|
||||
const repairData = results[2].status === 'fulfilled' ? results[2].value : { data: [] };
|
||||
const attendData = results[3].status === 'fulfilled' ? results[3].value : { data: {} };
|
||||
|
||||
const tbmSessions = tbmData.data || [];
|
||||
const notifications = notifData.data || [];
|
||||
const repairs = repairData.data || [];
|
||||
const attendance = attendData.data || {};
|
||||
|
||||
// Stats
|
||||
document.getElementById('statTbm').textContent = tbmSessions.length;
|
||||
document.getElementById('statWorkers').textContent = attendance.checked_in_count || 0;
|
||||
document.getElementById('statRepairs').textContent = repairs.length;
|
||||
document.getElementById('statNotifications').textContent = notifications.length;
|
||||
|
||||
// TBM list
|
||||
renderTbmList(tbmSessions);
|
||||
renderNotificationList(notifications);
|
||||
renderRepairList(repairs);
|
||||
}
|
||||
|
||||
function renderTbmList(sessions) {
|
||||
const el = document.getElementById('tbmList');
|
||||
if (!sessions.length) {
|
||||
el.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">금일 TBM이 없습니다</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = sessions.slice(0, 5).map(s => {
|
||||
const workers = s.team_member_count || 0;
|
||||
return `<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-800">${escapeHtml(s.workplace_name || s.session_title || 'TBM')}</div>
|
||||
<div class="text-xs text-gray-500">${escapeHtml(s.leader_name || '-')} · ${workers}명</div>
|
||||
</div>
|
||||
<span class="badge ${s.status === 'completed' ? 'badge-green' : 'badge-amber'}">${s.status === 'completed' ? '완료' : '진행중'}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (sessions.length > 5) {
|
||||
el.innerHTML += `<a href="/pages/work/tbm.html" class="block text-center text-xs text-orange-600 hover:text-orange-700 mt-2">전체 보기 (${sessions.length}건)</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNotificationList(notifications) {
|
||||
const el = document.getElementById('notificationList');
|
||||
if (!notifications.length) {
|
||||
el.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">새 알림이 없습니다</p>';
|
||||
return;
|
||||
}
|
||||
const icons = { repair: 'fa-wrench text-amber-500', safety: 'fa-shield-alt text-red-500', system: 'fa-bell text-blue-500', equipment: 'fa-cog text-gray-500', maintenance: 'fa-tools text-green-500' };
|
||||
el.innerHTML = notifications.slice(0, 5).map(n => {
|
||||
const iconClass = icons[n.type] || 'fa-bell text-gray-400';
|
||||
return `<div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100" onclick="location.href='/pages/admin/notifications.html'">
|
||||
<i class="fas ${iconClass} mt-0.5"></i>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-gray-800 truncate">${escapeHtml(n.title)}</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5">${formatTimeAgo(n.created_at)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderRepairList(repairs) {
|
||||
const el = document.getElementById('repairList');
|
||||
if (!repairs.length) {
|
||||
el.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">대기 중인 수리 요청이 없습니다</p>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = repairs.slice(0, 5).map(r => {
|
||||
return `<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-800">${escapeHtml(r.equipment_name || r.title || '수리 요청')}</div>
|
||||
<div class="text-xs text-gray-500">${formatDate(r.created_at)}</div>
|
||||
</div>
|
||||
<span class="badge badge-red">대기</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (repairs.length > 5) {
|
||||
el.innerHTML += `<a href="/pages/admin/repair-management.html" class="block text-center text-xs text-orange-600 hover:text-orange-700 mt-2">전체 보기 (${repairs.length}건)</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeAgo(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now - d;
|
||||
const mins = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
if (mins < 1) return '방금 전';
|
||||
if (mins < 60) return `${mins}분 전`;
|
||||
if (hours < 24) return `${hours}시간 전`;
|
||||
if (days < 7) return `${days}일 전`;
|
||||
return formatDate(dateStr);
|
||||
}
|
||||
|
||||
/* ===== Init ===== */
|
||||
(async function() {
|
||||
if (!await initAuth()) return;
|
||||
updateDateTime();
|
||||
setInterval(updateDateTime, 60000);
|
||||
loadDashboard();
|
||||
})();
|
||||
119
system1-factory/web/static/js/tkfb-nonconformity.js
Normal file
119
system1-factory/web/static/js/tkfb-nonconformity.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/* ===== 부적합 현황 (Nonconformity List) ===== */
|
||||
|
||||
const CATEGORY_TYPE = 'nonconformity';
|
||||
|
||||
const STATUS_LABELS = {
|
||||
reported: '신고', received: '접수', in_progress: '처리중',
|
||||
completed: '완료', closed: '종료'
|
||||
};
|
||||
|
||||
const STATUS_BADGE = {
|
||||
reported: 'badge-blue', received: 'badge-orange', in_progress: 'badge-purple',
|
||||
completed: 'badge-green', closed: 'badge-gray'
|
||||
};
|
||||
|
||||
function getReportUrl() {
|
||||
const h = location.hostname;
|
||||
if (h.includes('technicalkorea.net')) return 'https://tkreport.technicalkorea.net/pages/safety/issue-report.html?type=nonconformity';
|
||||
return location.protocol + '//' + h + ':30180/pages/safety/issue-report.html?type=nonconformity';
|
||||
}
|
||||
|
||||
function getIssueDetailUrl(reportId) {
|
||||
const h = location.hostname;
|
||||
if (h.includes('technicalkorea.net')) return `https://tkreport.technicalkorea.net/pages/safety/issue-detail.html?id=${reportId}&from=nonconformity`;
|
||||
return `${location.protocol}//${h}:30180/pages/safety/issue-detail.html?id=${reportId}&from=nonconformity`;
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const data = await api(`/work-issues/stats/summary?category_type=${CATEGORY_TYPE}`);
|
||||
if (data.success && data.data) {
|
||||
document.getElementById('statReported').textContent = data.data.reported || 0;
|
||||
document.getElementById('statReceived').textContent = data.data.received || 0;
|
||||
document.getElementById('statProgress').textContent = data.data.in_progress || 0;
|
||||
document.getElementById('statCompleted').textContent = data.data.completed || 0;
|
||||
}
|
||||
} catch {
|
||||
document.getElementById('statsGrid').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadIssues() {
|
||||
const params = new URLSearchParams();
|
||||
params.append('category_type', CATEGORY_TYPE);
|
||||
|
||||
const status = document.getElementById('filterStatus').value;
|
||||
const startDate = document.getElementById('filterStartDate').value;
|
||||
const endDate = document.getElementById('filterEndDate').value;
|
||||
|
||||
if (status) params.append('status', status);
|
||||
if (startDate) params.append('start_date', startDate);
|
||||
if (endDate) params.append('end_date', endDate);
|
||||
|
||||
try {
|
||||
const data = await api(`/work-issues?${params.toString()}`);
|
||||
if (data.success) renderIssues(data.data || []);
|
||||
} catch {
|
||||
document.getElementById('issueList').innerHTML =
|
||||
'<div class="bg-white rounded-xl shadow-sm p-8 text-center text-gray-400 text-sm">목록을 불러올 수 없습니다. 잠시 후 다시 시도해주세요.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderIssues(issues) {
|
||||
const el = document.getElementById('issueList');
|
||||
if (!issues.length) {
|
||||
el.innerHTML = '<div class="bg-white rounded-xl shadow-sm p-8 text-center"><p class="font-semibold text-gray-700 mb-1">등록된 부적합 신고가 없습니다</p><p class="text-sm text-gray-400">새로운 부적합을 신고하려면 \'부적합 신고\' 버튼을 클릭하세요.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = issues.map(issue => {
|
||||
const reportDate = formatDateTime(issue.report_date);
|
||||
let loc = escapeHtml(issue.custom_location || '');
|
||||
if (issue.factory_name) {
|
||||
loc = escapeHtml(issue.factory_name);
|
||||
if (issue.workplace_name) loc += ` - ${escapeHtml(issue.workplace_name)}`;
|
||||
}
|
||||
const title = escapeHtml(issue.issue_item_name || issue.issue_category_name || '부적합 신고');
|
||||
const categoryName = escapeHtml(issue.issue_category_name || '부적합');
|
||||
const reportId = parseInt(issue.report_id) || 0;
|
||||
const validStatuses = ['reported', 'received', 'in_progress', 'completed', 'closed'];
|
||||
const safeStatus = validStatuses.includes(issue.status) ? issue.status : 'reported';
|
||||
const reporter = escapeHtml(issue.reporter_full_name || issue.reporter_name || '-');
|
||||
const assigned = issue.assigned_full_name ? escapeHtml(issue.assigned_full_name) : '';
|
||||
|
||||
const photos = [issue.photo_path1, issue.photo_path2, issue.photo_path3, issue.photo_path4, issue.photo_path5].filter(Boolean);
|
||||
|
||||
return `<div class="bg-white rounded-xl shadow-sm p-4 border border-transparent hover:border-orange-200 hover:shadow-md transition-all cursor-pointer" onclick="location.href='${getIssueDetailUrl(reportId)}'">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-sm text-gray-400">#${reportId}</span>
|
||||
<span class="badge ${STATUS_BADGE[safeStatus] || 'badge-gray'}">${STATUS_LABELS[issue.status] || escapeHtml(issue.status || '-')}</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="badge badge-orange mr-1 text-xs">${categoryName}</span>
|
||||
<span class="font-semibold text-gray-800">${title}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 text-sm text-gray-500">
|
||||
<span class="flex items-center gap-1"><i class="fas fa-user text-xs"></i>${reporter}</span>
|
||||
<span class="flex items-center gap-1"><i class="fas fa-calendar text-xs"></i>${reportDate}</span>
|
||||
${loc ? `<span class="flex items-center gap-1"><i class="fas fa-map-marker-alt text-xs"></i>${loc}</span>` : ''}
|
||||
${assigned ? `<span class="flex items-center gap-1"><i class="fas fa-user-cog text-xs"></i>담당: ${assigned}</span>` : ''}
|
||||
</div>
|
||||
${photos.length > 0 ? `<div class="flex gap-2 mt-3">${photos.slice(0, 3).map(p => `<img src="${encodeURI(p)}" alt="사진" loading="lazy" class="w-14 h-14 object-cover rounded border border-gray-200">`).join('')}${photos.length > 3 ? `<span class="flex items-center text-sm text-gray-400">+${photos.length - 3}</span>` : ''}</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ===== Init ===== */
|
||||
(async function() {
|
||||
if (!await initAuth()) return;
|
||||
|
||||
// 신고 버튼 URL 설정
|
||||
document.getElementById('btnNewReport').href = getReportUrl();
|
||||
|
||||
// 필터 이벤트
|
||||
document.getElementById('filterStatus').addEventListener('change', loadIssues);
|
||||
document.getElementById('filterStartDate').addEventListener('change', loadIssues);
|
||||
document.getElementById('filterEndDate').addEventListener('change', loadIssues);
|
||||
|
||||
await Promise.all([loadStats(), loadIssues()]);
|
||||
})();
|
||||
@@ -1,207 +0,0 @@
|
||||
# 페이지 레이아웃 템플릿 가이드
|
||||
|
||||
이 디렉토리에는 프로젝트의 표준 페이지 레이아웃 템플릿이 포함되어 있습니다.
|
||||
|
||||
## 📋 사용 가능한 템플릿
|
||||
|
||||
### 1. `dashboard-layout.html`
|
||||
**용도**: 메인 대시보드 페이지
|
||||
**특징**:
|
||||
- 빠른 작업 섹션 포함
|
||||
- 여러 콘텐츠 섹션 지원
|
||||
- 푸터 포함
|
||||
- 토스트 알림 지원
|
||||
|
||||
**적합한 페이지**:
|
||||
- `dashboard.html` (메인 대시보드)
|
||||
- 통계/분석 대시보드
|
||||
|
||||
---
|
||||
|
||||
### 2. `work-layout.html`
|
||||
**용도**: 작업 관련 페이지
|
||||
**특징**:
|
||||
- 페이지 제목 및 설명 섹션
|
||||
- 검색/필터 영역
|
||||
- 작업 콘텐츠 카드
|
||||
- 로딩 스피너
|
||||
- 모달 지원
|
||||
|
||||
**적합한 페이지**:
|
||||
- `work/report-create.html` (작업 보고서 작성)
|
||||
- `work/report-view.html` (작업 현황 확인)
|
||||
- `work/analysis.html` (작업 분석)
|
||||
|
||||
---
|
||||
|
||||
### 3. `admin-layout.html`
|
||||
**용도**: 관리자 기능 페이지
|
||||
**특징**:
|
||||
- 뒤로가기 버튼이 있는 헤더
|
||||
- 탭 네비게이션 지원
|
||||
- 데이터 테이블 레이아웃
|
||||
- 페이지네이션
|
||||
- 일괄 선택 기능
|
||||
- 확인 모달
|
||||
|
||||
**적합한 페이지**:
|
||||
- `admin/projects.html` (프로젝트 관리)
|
||||
- `admin/workers.html` (작업자 관리)
|
||||
- `admin/codes.html` (코드 관리)
|
||||
- `admin/accounts.html` (계정 관리)
|
||||
|
||||
---
|
||||
|
||||
### 4. `simple-layout.html`
|
||||
**용도**: 단순한 폼/프로필 페이지
|
||||
**특징**:
|
||||
- 좁은 중앙 컨테이너 (최대 800px)
|
||||
- 폼 요소 스타일링
|
||||
- 뒤로가기 버튼
|
||||
- 정보 표시 리스트
|
||||
- 반응형 폼 레이아웃
|
||||
|
||||
**적합한 페이지**:
|
||||
- `profile/info.html` (내 프로필)
|
||||
- `profile/password.html` (비밀번호 변경)
|
||||
- 설정 페이지
|
||||
|
||||
---
|
||||
|
||||
## 🎨 공통 요소
|
||||
|
||||
모든 템플릿에는 다음이 포함됩니다:
|
||||
|
||||
### CSS
|
||||
```html
|
||||
<link rel="stylesheet" href="/css/design-system.css">
|
||||
```
|
||||
- 색상, 타이포그래피, 간격 등 CSS 변수
|
||||
- 공통 컴포넌트 스타일 (버튼, 카드, 배지 등)
|
||||
|
||||
### 상단 네비게이션 (Navbar)
|
||||
```html
|
||||
<div id="navbar-container"></div>
|
||||
<script type="module" src="/js/load-navbar.js"></script>
|
||||
```
|
||||
- 자동으로 로드되는 표준 네비게이션 헤더
|
||||
- 사용자 프로필 메뉴, 로그아웃 기능 포함
|
||||
- 현재 시각 표시
|
||||
|
||||
### 사이드바 네비게이션 (Sidebar)
|
||||
```html
|
||||
<div id="sidebar-container"></div>
|
||||
<script type="module" src="/js/load-sidebar.js"></script>
|
||||
```
|
||||
- 카테고리별 접이식 메뉴 (작업관리, 안전관리, 근태관리, 시스템관리)
|
||||
- 사용자 권한에 따른 메뉴 자동 필터링
|
||||
- 접기/펼치기 상태 자동 저장 (localStorage)
|
||||
- 현재 페이지 자동 하이라이트
|
||||
|
||||
### 인증
|
||||
```html
|
||||
<script type="module" src="/js/auth-check.js"></script>
|
||||
```
|
||||
- 페이지 접근 시 자동 로그인 확인
|
||||
- 미인증 시 로그인 페이지로 리다이렉트
|
||||
|
||||
---
|
||||
|
||||
## 🚀 템플릿 사용 방법
|
||||
|
||||
### 1단계: 템플릿 복사
|
||||
```bash
|
||||
cp web-ui/templates/work-layout.html web-ui/pages/work/new-page.html
|
||||
```
|
||||
|
||||
### 2단계: 메타데이터 수정
|
||||
```html
|
||||
<title>새 페이지 | 테크니컬코리아</title>
|
||||
```
|
||||
|
||||
### 3단계: CSS/JS 추가
|
||||
```html
|
||||
<!-- 페이지별 CSS -->
|
||||
<link rel="stylesheet" href="/css/new-page.css">
|
||||
|
||||
<!-- 페이지별 JS -->
|
||||
<script type="module" src="/js/new-page.js" defer></script>
|
||||
```
|
||||
|
||||
### 4단계: 콘텐츠 작성
|
||||
템플릿의 주석 처리된 예시 코드를 참고하여 실제 콘텐츠로 교체합니다.
|
||||
|
||||
---
|
||||
|
||||
## 📐 CSS 변수 활용
|
||||
|
||||
모든 템플릿은 `design-system.css`의 CSS 변수를 사용합니다:
|
||||
|
||||
### 색상
|
||||
```css
|
||||
var(--primary-500) /* 하늘색 (기본) */
|
||||
var(--success-500) /* 성공 (녹색) */
|
||||
var(--error-500) /* 오류 (빨간색) */
|
||||
var(--text-primary) /* 주요 텍스트 */
|
||||
```
|
||||
|
||||
### 간격
|
||||
```css
|
||||
var(--space-2) /* 8px */
|
||||
var(--space-4) /* 16px */
|
||||
var(--space-6) /* 24px */
|
||||
```
|
||||
|
||||
### 타이포그래피
|
||||
```css
|
||||
var(--text-sm) /* 14px */
|
||||
var(--text-base) /* 16px */
|
||||
var(--font-medium) /* 500 */
|
||||
var(--font-bold) /* 700 */
|
||||
```
|
||||
|
||||
### 기타
|
||||
```css
|
||||
var(--radius-md) /* 8px 둥근 모서리 */
|
||||
var(--shadow-md) /* 중간 그림자 */
|
||||
var(--transition-fast) /* 150ms 전환 */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 모범 사례
|
||||
|
||||
### DO ✅
|
||||
- 템플릿을 복사하여 시작
|
||||
- CSS 변수 사용
|
||||
- 의미있는 클래스명 사용
|
||||
- 주석으로 섹션 구분
|
||||
- 반응형 디자인 고려
|
||||
|
||||
### DON'T ❌
|
||||
- 하드코딩된 색상 사용 (`#0ea5e9` 대신 `var(--primary-500)`)
|
||||
- 인라인 스타일 남발
|
||||
- 중복 코드 작성
|
||||
- 템플릿 구조 크게 변경
|
||||
|
||||
---
|
||||
|
||||
## 🔄 템플릿 업데이트
|
||||
|
||||
템플릿이 업데이트되면:
|
||||
1. 기존 페이지를 즉시 수정할 필요는 없음
|
||||
2. 새 기능이 필요한 페이지만 선택적으로 적용
|
||||
3. 주요 변경사항은 `개발 log/` 디렉토리에 문서화
|
||||
|
||||
---
|
||||
|
||||
## 📞 문의
|
||||
|
||||
템플릿 사용에 대한 질문이나 개선 제안이 있으면:
|
||||
- `CODING_GUIDE.md` 참조
|
||||
- 개발팀에 문의
|
||||
|
||||
---
|
||||
|
||||
**마지막 업데이트**: 2026-01-20
|
||||
**버전**: 1.0.0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user