diff --git a/system1-factory/api/config/routes.js b/system1-factory/api/config/routes.js index 6bc8d7e..bbe6d0e 100644 --- a/system1-factory/api/config/routes.js +++ b/system1-factory/api/config/routes.js @@ -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 문서 diff --git a/system1-factory/api/routes.js b/system1-factory/api/routes.js index 6bc8d7e..bbe6d0e 100644 --- a/system1-factory/api/routes.js +++ b/system1-factory/api/routes.js @@ -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 문서 diff --git a/system1-factory/api/routes/notificationRecipientRoutes.js b/system1-factory/api/routes/notificationRecipientRoutes.js deleted file mode 100644 index 12f91bb..0000000 --- a/system1-factory/api/routes/notificationRecipientRoutes.js +++ /dev/null @@ -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; diff --git a/system1-factory/api/routes/systemRoutes.js b/system1-factory/api/routes/systemRoutes.js index e2d5243..b2d4a98 100644 --- a/system1-factory/api/routes/systemRoutes.js +++ b/system1-factory/api/routes/systemRoutes.js @@ -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); - // ===== 시스템 로그 관련 ===== /** diff --git a/system1-factory/api/routes/visitRequestRoutes.js b/system1-factory/api/routes/visitRequestRoutes.js deleted file mode 100644 index 9adf676..0000000 --- a/system1-factory/api/routes/visitRequestRoutes.js +++ /dev/null @@ -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; diff --git a/system1-factory/web/components/mobile-nav.html b/system1-factory/web/components/mobile-nav.html deleted file mode 100644 index 7d72119..0000000 --- a/system1-factory/web/components/mobile-nav.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - diff --git a/system1-factory/web/components/navbar.html b/system1-factory/web/components/navbar.html deleted file mode 100644 index a656d69..0000000 --- a/system1-factory/web/components/navbar.html +++ /dev/null @@ -1,764 +0,0 @@ - - -
-
-
- - -
- -
-

테크니컬코리아

-

생산팀 포털

-
-
-
- -
-
-
- --월 --일 (--) - --시 --분 --초 -
-
- 🌤️ - --°C - 날씨 로딩중 -
-
-
- -
- -
- -
-
-

알림

- 모두 보기 -
-
-
새 알림이 없습니다.
-
-
-
- - - 📊 - 대시보드 - - - - - 신고 - - - -
-
-
- - \ No newline at end of file diff --git a/system1-factory/web/components/sidebar-nav.html b/system1-factory/web/components/sidebar-nav.html deleted file mode 100644 index 0a555f5..0000000 --- a/system1-factory/web/components/sidebar-nav.html +++ /dev/null @@ -1,482 +0,0 @@ - - - - - - - - diff --git a/system1-factory/web/components/sidebar.html b/system1-factory/web/components/sidebar.html deleted file mode 100644 index 532fd6a..0000000 --- a/system1-factory/web/components/sidebar.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - \ No newline at end of file diff --git a/system1-factory/web/css/admin-pages.css b/system1-factory/web/css/admin-pages.css deleted file mode 100644 index fb42bc8..0000000 --- a/system1-factory/web/css/admin-pages.css +++ /dev/null @@ -1,1555 +0,0 @@ -/** - * 관리자 페이지 공통 스타일 - * 사무적이고 전문적인 디자인 시스템 - * - * 적용 페이지: - * - projects.html (프로젝트 관리) - * - workers.html (작업자 관리) - * - codes.html (코드 관리) - */ - -/* ============================================ - 1. 전역 변수 및 기본 설정 - ============================================ */ -:root { - /* 색상 시스템 - 사무적 느낌의 중성 색상 */ - --color-primary: #2563eb; - --color-primary-dark: #1e40af; - --color-primary-light: #dbeafe; - - --color-secondary: #64748b; - --color-secondary-dark: #475569; - --color-secondary-light: #f1f5f9; - - --color-success: #10b981; - --color-warning: #f59e0b; - --color-error: #ef4444; - --color-info: #3b82f6; - - /* 배경 색상 */ - --bg-body: #f8fafc; - --bg-card: #ffffff; - --bg-hover: #f1f5f9; - - /* 텍스트 색상 */ - --text-primary: #0f172a; - --text-secondary: #64748b; - --text-muted: #94a3b8; - - /* 테두리 색상 */ - --border-color: #e2e8f0; - --border-focus: #2563eb; - - /* 그림자 */ - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - - /* 간격 */ - --space-xs: 0.25rem; - --space-sm: 0.5rem; - --space-md: 1rem; - --space-lg: 1.5rem; - --space-xl: 2rem; - --space-2xl: 3rem; - - /* 폰트 크기 */ - --font-xs: 0.75rem; - --font-sm: 0.875rem; - --font-base: 1rem; - --font-lg: 1.125rem; - --font-xl: 1.25rem; - --font-2xl: 1.5rem; - --font-3xl: 1.875rem; - - /* Border Radius */ - --radius-sm: 0.25rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; -} - -/* ============================================ - 2. 레이아웃 구조 (2단 레이아웃: 사이드바 + 메인) - ============================================ */ -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans KR', Roboto, sans-serif; - background: var(--bg-body); - color: var(--text-primary); - line-height: 1.6; - margin: 0; - padding: 0; -} - -/* 페이지 컨테이너: 사이드바 + 메인 콘텐츠 */ -.page-container { - display: flex; - min-height: calc(100vh - 80px); /* 네비바 높이 제외 */ - background: var(--bg-body); -} - -/* 사이드바 */ -.sidebar { - width: 240px; - min-width: 240px; - flex-shrink: 0; - background: #ffffff; - border-right: 1px solid var(--border-color); - box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05); - overflow-y: auto; -} - -.sidebar-nav { - padding: var(--space-lg) 0; -} - -.sidebar-header { - padding: 0 var(--space-lg) var(--space-md); - border-bottom: 1px solid var(--border-color); - margin-bottom: var(--space-md); -} - -.sidebar-title { - font-size: var(--font-base); - font-weight: 700; - color: var(--text-primary); - margin: 0; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.sidebar-menu { - list-style: none; - padding: 0; - margin: 0; -} - -.menu-item a { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-sm) var(--space-lg); - color: var(--text-secondary); - text-decoration: none; - transition: all 0.2s ease; - white-space: nowrap; -} - -.menu-item a:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.menu-item.active a { - background: var(--color-primary-light); - color: var(--color-primary); - font-weight: 600; - border-right: 3px solid var(--color-primary); -} - -.menu-icon { - font-size: var(--font-lg); - flex-shrink: 0; -} - -.menu-text { - font-size: var(--font-sm); -} - -.menu-divider { - height: 1px; - background: var(--border-color); - margin: var(--space-md) var(--space-lg); -} - -/* 메인 콘텐츠 영역 */ -.main-content { - flex: 1; - padding: var(--space-lg); - padding-left: var(--space-md); - overflow-x: hidden; - max-width: 100%; - box-sizing: border-box; -} - -.dashboard-main { - width: 100%; - max-width: 1400px; - margin: 0 auto; -} - -/* 뒤로가기 버튼 - 사이드바로 대체되어 숨김 */ -.back-button { - display: none; -} - -/* ============================================ - 3. 페이지 헤더 - ============================================ */ -.page-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: var(--space-xl); - padding: var(--space-lg); - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); -} - -.page-title-section { - flex: 1; -} - -.page-title { - display: flex; - align-items: center; - gap: var(--space-md); - font-size: var(--font-3xl); - font-weight: 700; - color: var(--text-primary); - margin: 0 0 var(--space-sm) 0; - white-space: nowrap; /* 텍스트 줄바꿈 방지 */ -} - -.title-icon { - font-size: var(--font-2xl); - filter: grayscale(30%); -} - -.page-description { - font-size: var(--font-sm); - color: var(--text-secondary); - margin: 0; - line-height: 1.5; - max-width: 100%; - word-wrap: break-word; -} - -.page-actions { - display: flex; - gap: var(--space-sm); - flex-shrink: 0; -} - -/* ============================================ - 4. 버튼 스타일 - ============================================ */ -.btn { - display: inline-flex; - align-items: center; - gap: var(--space-sm); - padding: 0.625rem 1.25rem; - font-size: var(--font-sm); - font-weight: 600; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} - -.btn-primary { - background: var(--color-primary); - color: white; - box-shadow: var(--shadow-sm); -} - -.btn-primary:hover { - background: var(--color-primary-dark); - box-shadow: var(--shadow-md); - transform: translateY(-1px); -} - -.btn-secondary { - background: var(--bg-card); - color: var(--text-secondary); - border: 1px solid var(--border-color); -} - -.btn-secondary:hover { - background: var(--bg-hover); - color: var(--text-primary); - border-color: var(--color-secondary); -} - -.btn-icon { - font-size: var(--font-base); - filter: grayscale(20%); -} - -/* ============================================ - 5. 검색 섹션 - ============================================ */ -.search-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--space-lg); - margin-bottom: var(--space-xl); - box-shadow: var(--shadow-sm); -} - -.search-bar { - display: flex; - gap: var(--space-md); - margin-bottom: var(--space-md); -} - -.search-input { - flex: 1; - padding: 0.625rem 1rem; - font-size: var(--font-sm); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-body); - transition: all 0.2s ease; -} - -.search-input:focus { - outline: none; - border-color: var(--border-focus); - background: white; - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); -} - -.search-btn { - padding: 0.625rem 1.25rem; - background: var(--color-primary); - color: white; - border: none; - border-radius: var(--radius-md); - font-size: var(--font-sm); - font-weight: 600; - cursor: pointer; - white-space: nowrap; - transition: all 0.2s ease; -} - -.search-btn:hover { - background: var(--color-primary-dark); - transform: translateY(-1px); -} - -/* 필터 영역 */ -.filter-group { - display: flex; - flex-wrap: wrap; - gap: var(--space-md); - align-items: center; -} - -.filter-label { - font-size: var(--font-sm); - font-weight: 600; - color: var(--text-secondary); - margin-right: var(--space-sm); -} - -.filter-btn { - padding: 0.5rem 1rem; - font-size: var(--font-sm); - background: var(--bg-body); - color: var(--text-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s ease; -} - -.filter-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.filter-btn.active { - background: var(--color-primary); - color: white; - border-color: var(--color-primary); -} - -/* ============================================ - 6. 프로젝트 섹션 - ============================================ */ -.projects-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--space-lg); - box-shadow: var(--shadow-sm); - width: 100%; - max-width: 100%; - overflow: hidden; - box-sizing: border-box; -} - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: var(--space-md); - margin-bottom: var(--space-lg); - padding-bottom: var(--space-md); - border-bottom: 1px solid var(--border-color); -} - -.section-title { - font-size: var(--font-xl); - font-weight: 700; - color: var(--text-primary); - margin: 0; -} - -/* 통계 카드 */ -.project-stats { - display: flex; - gap: var(--space-sm); - flex-wrap: wrap; -} - -.stat-item { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: 0.625rem 1rem; - background: var(--bg-card); - border: 2px solid var(--border-color); - border-radius: var(--radius-md); - font-size: var(--font-sm); - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} - -.stat-item:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); -} - -.stat-item.active { - border-width: 2px; - box-shadow: var(--shadow-md); - transform: scale(1.02); -} - -.stat-icon { - font-size: var(--font-lg); -} - -/* 상태별 통계 색상 */ -.active-stat { - background: rgba(16, 185, 129, 0.05); - color: var(--color-success); - border-color: var(--color-success); -} - -.inactive-stat { - background: rgba(239, 68, 68, 0.05); - color: var(--color-error); - border-color: var(--color-error); -} - -.total-stat { - background: rgba(37, 99, 235, 0.05); - color: var(--color-primary); - border-color: var(--color-primary); -} - -/* ============================================ - 7. 그리드 레이아웃 (중앙 정렬) - ============================================ */ -.projects-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--space-lg); - width: 100%; - max-width: 100%; -} - -/* ============================================ - 8. 카드 스타일 - ============================================ */ -.project-card, -.worker-card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--space-lg); - transition: all 0.2s ease; - cursor: pointer; - position: relative; - display: flex; - flex-direction: column; - height: 420px; - min-height: 420px; - max-height: 420px; - max-width: 100%; - overflow: hidden; - box-sizing: border-box; - word-wrap: break-word; -} - -.project-card:hover, -.worker-card:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-lg); - transform: translateY(-4px); -} - -.project-card.inactive, -.worker-card.inactive { - opacity: 0.75; - background: var(--bg-body); -} - -/* 카드 헤더 */ -.project-header { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; -} - -.project-info { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-sm); - margin-bottom: var(--space-md); -} - -.project-job-no { - font-size: var(--font-xs); - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.project-name { - font-size: var(--font-lg); - font-weight: 700; - color: var(--text-primary); - margin: 0; - line-height: 1.3; -} - -.project-badge { - padding: 0.25rem 0.75rem; - font-size: var(--font-xs); - font-weight: 600; - border-radius: var(--radius-sm); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.badge-active { - background: rgba(16, 185, 129, 0.1); - color: var(--color-success); -} - -.badge-inactive { - background: rgba(239, 68, 68, 0.1); - color: var(--color-error); -} - -/* 카드 메타 정보 */ -.project-meta { - display: grid; - grid-template-columns: 1fr; - gap: var(--space-xs); - font-size: var(--font-sm); - color: var(--text-secondary); - padding: var(--space-md); - background: var(--bg-body); - border-radius: var(--radius-md); - margin-bottom: var(--space-md); -} - -.project-meta > span { - display: flex; - align-items: center; - gap: var(--space-xs); - padding: var(--space-xs) 0; - line-height: 1.5; -} - -.project-meta > span:empty::after { - content: '정보 없음'; - color: var(--text-muted); - font-style: italic; -} - -.meta-row { - display: grid; - grid-template-columns: 100px 1fr; - align-items: center; - gap: var(--space-sm); - font-size: var(--font-sm); - padding: var(--space-xs) 0; -} - -.meta-label { - font-weight: 600; - color: var(--text-muted); - font-size: var(--font-xs); - text-transform: uppercase; - letter-spacing: 0.3px; -} - -.meta-value { - color: var(--text-primary); - font-weight: 500; -} - -.meta-value:empty::after { - content: '-'; - color: var(--text-muted); -} - -/* 카드 액션 */ -.project-actions, -.worker-actions { - display: flex; - gap: var(--space-sm); - margin-top: auto; - padding-top: var(--space-md); - border-top: 1px solid var(--border-color); -} - -.btn-edit, -.btn-delete, -.action-btn { - flex: 1; - padding: 0.5rem 1rem; - font-size: var(--font-sm); - font-weight: 600; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: white; - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - gap: var(--space-xs); -} - -.btn-edit:hover { - background: var(--color-primary); - color: white; - border-color: var(--color-primary); - transform: translateY(-1px); -} - -.btn-delete:hover, -.action-btn.danger:hover { - background: var(--color-error); - color: white; - border-color: var(--color-error); - transform: translateY(-1px); -} - -/* 작업자 카드 특별 스타일 */ -.worker-card .project-info { - display: flex; - align-items: flex-start; - gap: var(--space-md); - margin-bottom: var(--space-md); -} - -.worker-avatar { - width: 60px; - height: 60px; - border-radius: 50%; - background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark)); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - box-shadow: var(--shadow-md); -} - -.avatar-initial { - color: white; - font-size: var(--font-xl); - font-weight: 700; -} - -.worker-card.inactive .worker-avatar { - background: linear-gradient(135deg, var(--color-secondary), var(--color-secondary-dark)); - opacity: 0.7; -} - -.worker-details { - flex: 1; - min-width: 0; -} - -/* 비활성화 오버레이 및 라벨 */ -.inactive-overlay { - position: absolute; - top: var(--space-md); - right: var(--space-md); - z-index: 10; -} - -.inactive-badge { - display: inline-flex; - align-items: center; - gap: var(--space-xs); - padding: 0.375rem 0.75rem; - background: rgba(239, 68, 68, 0.9); - color: white; - border-radius: var(--radius-md); - font-size: var(--font-xs); - font-weight: 700; - box-shadow: var(--shadow-md); -} - -.inactive-label { - color: var(--color-error); - font-size: var(--font-sm); - font-weight: 600; - margin-left: var(--space-xs); -} - -.inactive-notice { - color: var(--color-warning) !important; - font-weight: 600 !important; -} - -/* ============================================ - 9. 코드 관리 탭 - ============================================ */ -.code-tabs { - display: flex; - gap: var(--space-sm); - margin-bottom: var(--space-xl); - padding: var(--space-sm); - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - overflow-x: auto; -} - -.tab-btn { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: 0.75rem 1.25rem; - font-size: var(--font-sm); - font-weight: 600; - color: var(--text-secondary); - background: transparent; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} - -.tab-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.tab-btn.active { - color: var(--color-primary); - background: var(--color-primary-light); - border-bottom: 3px solid var(--color-primary); - font-weight: 600; -} - -.tab-icon { - font-size: var(--font-base); -} - -/* ============================================ - 10. 테이블 스타일 - ============================================ */ -.table-container { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - overflow: hidden; -} - -table { - width: 100%; - border-collapse: collapse; -} - -thead { - background: var(--bg-body); - border-bottom: 2px solid var(--border-color); -} - -th { - padding: 1rem; - font-size: var(--font-sm); - font-weight: 700; - color: var(--text-secondary); - text-align: left; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -td { - padding: 1rem; - font-size: var(--font-sm); - color: var(--text-primary); - border-bottom: 1px solid var(--border-color); -} - -tr:hover { - background: var(--bg-hover); -} - -tr:last-child td { - border-bottom: none; -} - -/* 테이블 내 버튼 스타일 */ -.data-table .btn-icon { - padding: 0.375rem 0.5rem; - background: transparent; - border: none; - cursor: pointer; - font-size: 1rem; - transition: all 0.2s ease; - border-radius: var(--radius-sm); -} - -.data-table .btn-icon:hover { - background: var(--bg-hover); - transform: scale(1.1); -} - -/* ============================================ - 11. 모달 - ============================================ */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - backdrop-filter: blur(4px); -} - -.modal-container { - background: var(--bg-card); - border-radius: var(--radius-xl); - width: 90%; - max-width: 600px; - max-height: 90vh; - overflow-y: auto; - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--space-lg); - border-bottom: 1px solid var(--border-color); -} - -.modal-title { - font-size: var(--font-xl); - font-weight: 700; - color: var(--text-primary); - margin: 0; -} - -.modal-close { - padding: 0.5rem; - background: none; - border: none; - font-size: var(--font-xl); - color: var(--text-secondary); - cursor: pointer; - border-radius: var(--radius-md); - transition: all 0.2s ease; -} - -.modal-close:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.modal-body { - padding: var(--space-lg); -} - -.modal-footer { - display: flex; - gap: var(--space-md); - justify-content: flex-end; - padding: var(--space-lg); - border-top: 1px solid var(--border-color); -} - -/* ============================================ - 12. 폼 요소 - ============================================ */ -.form-group { - margin-bottom: var(--space-lg); -} - -.form-label { - display: block; - margin-bottom: var(--space-sm); - font-size: var(--font-sm); - font-weight: 600; - color: var(--text-primary); -} - -.form-label.required::after { - content: '*'; - color: var(--color-error); - margin-left: var(--space-xs); -} - -.form-input, -.form-select, -.form-textarea { - width: 100%; - padding: 0.625rem 1rem; - font-size: var(--font-sm); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: white; - transition: all 0.2s ease; - box-sizing: border-box; -} - -.form-input:focus, -.form-select:focus, -.form-textarea:focus { - outline: none; - border-color: var(--border-focus); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); -} - -.form-textarea { - resize: vertical; - min-height: 100px; -} - -.form-help { - margin-top: var(--space-xs); - font-size: var(--font-xs); - color: var(--text-muted); -} - -/* ============================================ - 13. 반응형 디자인 - ============================================ */ - -/* 대형 데스크탑 (1400px 이상) */ -@media (min-width: 1400px) { - .projects-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -/* 일반 데스크탑 (1200px ~ 1399px) */ -@media (max-width: 1399px) and (min-width: 1200px) { - .projects-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -/* 중간 화면 (1024px ~ 1199px) - 2열 */ -@media (max-width: 1199px) and (min-width: 1024px) { - .projects-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -/* 태블릿 (768px ~ 1023px) */ -@media (max-width: 1023px) and (min-width: 768px) { - .sidebar { - width: 200px; - min-width: 200px; - } - - .main-content { - padding: var(--space-md); - } - - .page-header { - flex-direction: column; - align-items: flex-start; - gap: var(--space-md); - } - - .page-actions { - width: 100%; - justify-content: flex-start; - } - - .projects-grid { - grid-template-columns: repeat(2, 1fr); - } - - .search-bar { - flex-direction: column; - } - - .project-stats { - flex-wrap: wrap; - gap: var(--space-sm); - } - - .stat-item { - font-size: var(--font-xs); - padding: 0.375rem 0.625rem; - } -} - -/* 모바일 (767px 이하) */ -@media (max-width: 767px) { - .page-container { - flex-direction: column; - } - - .sidebar { - display: none; /* 모바일에서는 사이드바 숨김 */ - } - - .main-content { - padding: var(--space-sm); - } - - .page-title { - font-size: var(--font-xl); - flex-direction: column; - align-items: flex-start; - gap: var(--space-xs); - } - - .title-icon { - font-size: var(--font-xl); - } - - .page-actions { - width: 100%; - flex-direction: column; - } - - .btn { - width: 100%; - justify-content: center; - } - - .search-section, - .projects-section { - padding: var(--space-md); - } - - .search-bar { - flex-direction: column; - } - - .projects-grid { - grid-template-columns: 1fr; - gap: var(--space-md); - } - - .project-card { - min-height: auto; - } - - .project-stats { - flex-direction: column; - width: 100%; - } - - .stat-item { - width: 100%; - justify-content: space-between; - } - - .code-tabs { - flex-direction: column; - } - - .tab-btn { - justify-content: center; - } - - .meta-row { - grid-template-columns: 80px 1fr; - gap: var(--space-xs); - } - - .meta-label { - font-size: 0.625rem; - } - - .modal-container { - width: 95%; - margin: var(--space-sm); - max-height: 95vh; - } -} - -/* 초소형 모바일 (480px 이하) */ -@media (max-width: 480px) { - .work-report-main { - padding: var(--space-xs); - } - - .page-title { - font-size: var(--font-lg); - } - - .page-description { - font-size: var(--font-xs); - } - - .search-section, - .projects-section { - padding: var(--space-sm); - } - - .project-card { - padding: var(--space-md); - } - - .btn { - padding: 0.5rem 0.875rem; - font-size: var(--font-xs); - } -} - -/* ============================================ - 14. Empty State - ============================================ */ -.empty-state { - text-align: center; - padding: var(--space-2xl) var(--space-lg); - color: var(--text-secondary); -} - -.empty-icon { - font-size: 4rem; - margin-bottom: var(--space-lg); - opacity: 0.5; -} - -.empty-state h3 { - font-size: var(--font-xl); - font-weight: 600; - color: var(--text-primary); - margin: 0 0 var(--space-sm) 0; -} - -.empty-state p { - font-size: var(--font-sm); - color: var(--text-secondary); - margin-bottom: var(--space-lg); -} - -/* ============================================ - 15. 로딩 및 스켈레톤 - ============================================ */ -.skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: skeleton-loading 1.5s ease-in-out infinite; - border-radius: var(--radius-md); -} - -@keyframes skeleton-loading { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -.skeleton-card { - height: 280px; -} - -.skeleton-text { - height: 1rem; - margin-bottom: var(--space-sm); -} - -/* ============================================ - 16. 유틸리티 클래스 - ============================================ */ -.text-center { - text-align: center; -} - -.text-muted { - color: var(--text-muted); -} - -.text-success { - color: var(--color-success); -} - -.text-error { - color: var(--color-error); -} - -.mt-0 { margin-top: 0; } -.mt-1 { margin-top: var(--space-sm); } -.mt-2 { margin-top: var(--space-md); } -.mt-3 { margin-top: var(--space-lg); } - -.mb-0 { margin-bottom: 0; } -.mb-1 { margin-bottom: var(--space-sm); } -.mb-2 { margin-bottom: var(--space-md); } -.mb-3 { margin-bottom: var(--space-lg); } - -.hidden { - display: none !important; -} - -.loading { - opacity: 0.6; - pointer-events: none; -} - -/* 코드 상세 정보 */ -.code-detail { - display: flex; - flex-direction: column; - gap: var(--space-xs); -} - -.code-name { - font-size: var(--font-base); - font-weight: 600; - color: var(--text-primary); -} - -.code-description { - font-size: var(--font-sm); - color: var(--text-secondary); - line-height: 1.5; -} - -/* ============================================ - 17. 코드 관리 페이지 전용 스타일 - ============================================ */ - -/* 코드 탭 스타일 */ -.code-tabs { - display: flex; - gap: var(--space-sm); - margin-bottom: var(--space-xl); - border-bottom: 2px solid var(--border-color); - padding-bottom: 0; -} - -.tab-btn { - display: flex; - align-items: center; - gap: var(--space-sm); - padding: var(--space-md) var(--space-lg); - background: transparent; - border: none; - border-bottom: 3px solid transparent; - color: var(--text-secondary); - font-size: var(--font-base); - font-weight: 600; - cursor: pointer; - transition: all 0.2s ease; - position: relative; - margin-bottom: -2px; -} - -.tab-btn:hover { - color: var(--color-primary); - background: var(--color-primary-light); -} - -.tab-btn.active { - color: var(--color-primary); - border-bottom-color: var(--color-primary); -} - -.tab-icon { - font-size: var(--font-lg); -} - -/* 탭 콘텐츠 */ -.code-tab-content { - display: none; -} - -.code-tab-content.active { - display: block; - animation: fadeIn 0.3s ease; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* 코드 섹션 */ -.code-section { - margin-bottom: var(--space-xl); -} - -/* 코드 통계 */ -.code-stats { - display: flex; - gap: var(--space-sm); - flex-wrap: wrap; - margin-bottom: var(--space-lg); -} - -.code-stats .stat-item { - padding: 0.5rem 1rem; - font-size: var(--font-sm); -} - -/* 코드 그리드 */ -.code-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--space-lg); - width: 100%; -} - -/* 코드 카드 */ -.code-card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: var(--space-lg); - transition: all 0.2s ease; - cursor: pointer; - position: relative; - display: flex; - flex-direction: column; - min-height: 200px; -} - -.code-card:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-lg); - transform: translateY(-2px); -} - -/* 코드 카드 헤더 */ -.code-header { - display: flex; - align-items: flex-start; - gap: var(--space-md); - margin-bottom: var(--space-md); -} - -.code-icon { - font-size: 2rem; - flex-shrink: 0; - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-body); - border-radius: var(--radius-md); -} - -.code-info { - flex: 1; - min-width: 0; -} - -.code-name { - font-size: var(--font-lg); - font-weight: 700; - color: var(--text-primary); - margin: 0 0 var(--space-xs) 0; - word-break: break-word; -} - -.code-label { - display: inline-block; - padding: 0.25rem 0.75rem; - background: var(--bg-body); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - font-size: var(--font-xs); - font-weight: 600; - color: var(--text-secondary); -} - -.code-actions { - display: flex; - gap: var(--space-xs); - flex-shrink: 0; -} - -.btn-small { - padding: 0.375rem 0.625rem; - font-size: var(--font-sm); - background: white; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.2s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.btn-small:hover { - transform: scale(1.1); -} - -.btn-small.btn-edit:hover { - background: var(--color-primary); - border-color: var(--color-primary); - color: white; -} - -.btn-small.btn-delete:hover { - background: var(--color-error); - border-color: var(--color-error); - color: white; -} - -/* 코드 설명 */ -.code-description { - font-size: var(--font-sm); - color: var(--text-secondary); - line-height: 1.6; - margin-bottom: var(--space-md); - flex: 1; -} - -/* 해결 가이드 */ -.solution-guide { - background: #fef3c7; - border-left: 3px solid #f59e0b; - padding: var(--space-md); - border-radius: var(--radius-sm); - font-size: var(--font-sm); - margin-bottom: var(--space-md); - line-height: 1.6; -} - -.solution-guide strong { - color: #92400e; - display: block; - margin-bottom: var(--space-xs); -} - -/* 코드 메타 정보 */ -.code-meta { - display: flex; - flex-wrap: wrap; - gap: var(--space-md); - padding-top: var(--space-md); - border-top: 1px solid var(--border-color); - margin-top: auto; -} - -.code-date { - font-size: var(--font-xs); - color: var(--text-muted); -} - -/* 상태별 카드 스타일 */ -.normal-status { - border-left: 4px solid #10b981; -} - -.error-status { - border-left: 4px solid #ef4444; -} - -/* 심각도별 카드 스타일 */ -.severity-low { - border-left: 4px solid #10b981; -} - -.severity-low .code-icon { - background: #d1fae5; -} - -.severity-medium { - border-left: 4px solid #f59e0b; -} - -.severity-medium .code-icon { - background: #fef3c7; -} - -.severity-high { - border-left: 4px solid #f97316; -} - -.severity-high .code-icon { - background: #ffedd5; -} - -.severity-critical { - border-left: 4px solid #ef4444; -} - -.severity-critical .code-icon { - background: #fee2e2; -} - -/* 작업 유형 카드 */ -.work-type-card { - border-left: 4px solid #3b82f6; -} - -.work-type-card .code-icon { - background: #dbeafe; -} - -/* 반응형 - 코드 그리드 */ -@media (max-width: 1199px) { - .code-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 767px) { - .code-grid { - grid-template-columns: 1fr; - } - - .code-tabs { - overflow-x: auto; - flex-wrap: nowrap; - } - - .tab-btn { - white-space: nowrap; - padding: var(--space-sm) var(--space-md); - font-size: var(--font-sm); - } -} diff --git a/system1-factory/web/css/admin-settings.css b/system1-factory/web/css/admin-settings.css deleted file mode 100644 index 8013870..0000000 --- a/system1-factory/web/css/admin-settings.css +++ /dev/null @@ -1,1320 +0,0 @@ -/* admin-settings.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: 3rem 2rem; - margin-bottom: 0; -} - -.work-report-header h1 { - font-size: 2.5rem; - font-weight: 700; - margin: 0 0 1rem 0; - text-shadow: 0 2px 4px rgba(0,0,0,0.3); -} - -.work-report-header .subtitle { - font-size: 1.1rem; - opacity: 0.9; - margin: 0; - font-weight: 300; -} - -.work-report-main { - background: #f8f9fa; - min-height: calc(100vh - 200px); - 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: 8px; - font-weight: 500; - margin: 0 2rem 2rem 2rem; - transition: all 0.3s ease; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.back-button:hover { - background: white; - color: #007bff; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0,0,0,0.15); -} - -.dashboard-main { - padding: 0 2rem 2rem 2rem; - max-width: 1400px; - margin: 0 auto; -} - -.page-header { - margin-bottom: 2rem; -} - -.page-title-section { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.page-title { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 2rem; - font-weight: 700; - color: #1a1a1a; - margin: 0; -} - -.title-icon { - font-size: 2.25rem; -} - -.page-description { - font-size: 1rem; - color: #666; - margin: 0; -} - -/* 설정 섹션 */ -.settings-section { - background: white; - border-radius: 12px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); - overflow: hidden; -} - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem 2rem; - background: #f8f9fa; - border-bottom: 1px solid #e9ecef; -} - -.section-title { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1.25rem; - font-weight: 600; - color: #1a1a1a; - margin: 0; -} - -.section-icon { - font-size: 1.5rem; -} - -/* 사용자 컨테이너 */ -.users-container { - padding: 2rem; -} - -.users-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - gap: 1rem; -} - -/* 검색 박스 */ -.search-box { - position: relative; - flex: 1; - max-width: 300px; -} - -.search-input { - width: 100%; - padding: 0.75rem 1rem 0.75rem 2.5rem; - border: 2px solid #e9ecef; - border-radius: 8px; - font-size: 0.9rem; - transition: all 0.2s ease; -} - -.search-input:focus { - outline: none; - border-color: #007bff; - box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); -} - -.search-icon { - position: absolute; - left: 0.75rem; - top: 50%; - transform: translateY(-50%); - font-size: 1rem; - color: #666; -} - -/* 필터 버튼 */ -.filter-buttons { - display: flex; - gap: 0.5rem; -} - -.filter-btn { - padding: 0.5rem 1rem; - border: 2px solid #e9ecef; - background: white; - border-radius: 6px; - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.filter-btn:hover { - border-color: #007bff; - color: #007bff; -} - -.filter-btn.active { - background: #007bff; - border-color: #007bff; - color: white; -} - -/* 사용자 테이블 */ -.users-table-container { - border: 1px solid #e9ecef; - border-radius: 8px; - overflow: hidden; -} - -.users-table { - width: 100%; - border-collapse: collapse; - font-size: 0.9rem; -} - -.users-table th { - background: #f8f9fa; - padding: 1rem; - text-align: left; - font-weight: 600; - color: #495057; - border-bottom: 2px solid #e9ecef; -} - -.users-table td { - padding: 1rem; - border-bottom: 1px solid #e9ecef; - vertical-align: middle; -} - -.users-table tbody tr:hover { - background: #f8f9fa; -} - -/* 사용자 정보 */ -.user-info { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.user-avatar-small { - width: 36px; - height: 36px; - background: #007bff; - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 0.9rem; -} - -.user-details h4 { - margin: 0; - font-size: 0.9rem; - font-weight: 600; - color: #1a1a1a; -} - -.user-details p { - margin: 0; - font-size: 0.8rem; - color: #666; -} - -/* 역할 배지 */ -.role-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.75rem; - font-weight: 600; -} - -.role-badge.admin { - background: #dc3545; - color: white; -} - -.role-badge.leader { - background: #fd7e14; - color: white; -} - -.role-badge.user { - background: #28a745; - color: white; -} - -/* 상태 배지 */ -.status-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.75rem; - border-radius: 12px; - font-size: 0.75rem; - font-weight: 600; -} - -.status-badge.active { - background: #d4edda; - color: #155724; -} - -.status-badge.inactive { - background: #f8d7da; - color: #721c24; -} - -/* 액션 버튼 */ -.action-buttons { - display: flex; - gap: 0.5rem; -} - -.action-btn { - padding: 0.4rem 0.8rem; - border: none; - border-radius: 4px; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.action-btn.edit { - background: #007bff; - color: white; -} - -.action-btn.edit:hover { - background: #0056b3; -} - -.action-btn.delete { - background: #dc3545; - color: white; -} - -.action-btn.delete:hover { - background: #c82333; -} - -.action-btn.toggle { - background: #6c757d; - color: white; -} - -.action-btn.toggle:hover { - background: #545b62; -} - -/* 빈 상태 */ -.empty-state { - text-align: center; - padding: 3rem 2rem; - color: #666; -} - -.empty-icon { - font-size: 4rem; - margin-bottom: 1rem; -} - -.empty-state h3 { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 0.5rem; - color: #1a1a1a; -} - -.empty-state p { - font-size: 0.9rem; - margin: 0; -} - -/* 모달 스타일 */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-container { - background: white; - border-radius: 12px; - box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1); - width: 90%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; -} - -.modal-container.small { - max-width: 400px; -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem 2rem; - border-bottom: 1px solid #e9ecef; -} - -.modal-header h2 { - margin: 0; - font-size: 1.25rem; - font-weight: 600; - color: #1a1a1a; -} - -.modal-close-btn { - background: none; - border: none; - font-size: 1.5rem; - color: #666; - cursor: pointer; - padding: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: all 0.2s ease; -} - -.modal-close-btn:hover { - background: #f8f9fa; - color: #1a1a1a; -} - -.modal-body { - padding: 2rem; -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 1rem; - padding: 1.5rem 2rem; - border-top: 1px solid #e9ecef; - background: #f8f9fa; -} - -/* 폼 스타일 */ -.form-group { - margin-bottom: 1.5rem; -} - -.form-label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: #1a1a1a; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem; - border: 2px solid #e9ecef; - border-radius: 6px; - font-size: 0.9rem; - transition: all 0.2s ease; -} - -.form-control:focus { - outline: none; - border-color: #007bff; - box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); -} - -.form-help { - display: block; - margin-top: 0.25rem; - font-size: 0.8rem; - color: #666; -} - -/* 삭제 경고 */ -.delete-warning { - text-align: center; - padding: 1rem 0; -} - -.warning-icon { - font-size: 3rem; - margin-bottom: 1rem; -} - -.delete-warning p { - margin-bottom: 0.5rem; - font-size: 1rem; - color: #1a1a1a; -} - -.warning-text { - font-size: 0.9rem; - color: #dc3545; - font-weight: 500; -} - -/* 버튼 스타일 */ -.btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border: none; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 500; - text-decoration: none; - cursor: pointer; - transition: all 0.2s ease; -} - -.btn-primary { - background: #007bff; - color: white; -} - -.btn-primary:hover { - background: #0056b3; - transform: translateY(-1px); -} - -.btn-secondary { - background: #6c757d; - color: white; -} - -.btn-secondary:hover { - background: #545b62; -} - -.btn-danger { - background: #dc3545; - color: white; -} - -.btn-danger:hover { - background: #c82333; -} - -.btn-icon { - font-size: 1rem; -} - -/* 토스트 알림 */ -.toast-container { - position: fixed; - top: 20px; - right: 20px; - z-index: 1100; -} - -.toast { - background: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - padding: 1rem 1.5rem; - margin-bottom: 0.5rem; - display: flex; - align-items: center; - gap: 0.75rem; - min-width: 300px; - animation: slideIn 0.3s ease; -} - -.toast.success { - border-left: 4px solid #28a745; -} - -.toast.error { - border-left: 4px solid #dc3545; -} - -.toast.warning { - border-left: 4px solid #ffc107; -} - -.toast-icon { - font-size: 1.25rem; -} - -.toast-message { - flex: 1; - font-size: 0.9rem; - color: #1a1a1a; -} - -.toast-close { - background: none; - border: none; - font-size: 1.25rem; - color: #666; - cursor: pointer; - padding: 0; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* 반응형 */ -@media (max-width: 1024px) { - .dashboard-main { - padding: 1.5rem; - } - - .users-header { - flex-direction: column; - align-items: stretch; - gap: 1rem; - } - - .search-box { - max-width: none; - } -} - -@media (max-width: 768px) { - .dashboard-main { - padding: 1rem; - } - - .section-header { - flex-direction: column; - align-items: stretch; - gap: 1rem; - } - - .users-table-container { - overflow-x: auto; - } - - .users-table { - min-width: 600px; - } - - .modal-container { - width: 95%; - margin: 1rem; - } - - .modal-body { - padding: 1.5rem; - } -} - -@media (max-width: 480px) { - .filter-buttons { - flex-wrap: wrap; - } - - .action-buttons { - flex-direction: column; - } -} - -/* 페이지 권한 관리 스타일 */ -.page-access-list { - max-height: 300px; - overflow-y: auto; - border: 1px solid var(--border-light); - border-radius: var(--radius-md); - padding: var(--space-3); - background: var(--bg-secondary); -} - -.page-access-category { - margin-bottom: var(--space-4); -} - -.page-access-category:last-child { - margin-bottom: 0; -} - -.page-access-category-title { - font-size: var(--text-sm); - font-weight: var(--font-semibold); - color: var(--text-secondary); - margin-bottom: var(--space-2); - padding-bottom: var(--space-2); - border-bottom: 1px solid var(--border-light); - text-transform: uppercase; -} - -.page-access-item { - display: flex; - align-items: center; - padding: var(--space-2); - border-radius: var(--radius-sm); - transition: var(--transition-normal); -} - -.page-access-item:hover { - background: var(--bg-hover); -} - -.page-access-item label { - display: flex; - align-items: center; - cursor: pointer; - flex: 1; - margin: 0; -} - -.page-access-item input[type="checkbox"] { - margin-right: var(--space-2); - width: 18px; - height: 18px; - cursor: pointer; -} - -.page-access-item .page-name { - font-size: var(--text-sm); - color: var(--text-primary); - font-weight: var(--font-medium); -} - -/* 권한 관리 버튼 스타일 */ -.action-btn.permissions { - background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); - color: white; -} - -.action-btn.permissions:hover { - background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); -} - -/* 비밀번호 초기화 버튼 스타일 */ -.action-btn.reset-pw { - background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); - color: white; -} - -.action-btn.reset-pw:hover { - background: linear-gradient(135deg, #d97706 0%, #b45309 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3); -} - -/* 페이지 권한 모달 사용자 정보 */ -.page-access-user-info { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-4); - background: var(--bg-secondary); - border-radius: var(--radius-lg); - margin-bottom: var(--space-4); -} - -.page-access-user-info h3 { - margin: 0; - font-size: var(--text-lg); - font-weight: var(--font-semibold); - color: var(--text-primary); -} - -.page-access-user-info p { - margin: 0; - font-size: var(--text-sm); - color: var(--text-secondary); -} - -/* 폴더 트리 스타일 */ -.folder-tree { - padding: 0; -} - -.folder-group { - margin-bottom: 0.5rem; - border: 1px solid #e9ecef; - border-radius: 8px; - overflow: hidden; -} - -.folder-header { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - cursor: pointer; - transition: all 0.2s ease; - user-select: none; -} - -.folder-header:hover { - background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%); -} - -.folder-icon { - font-size: 1.25rem; -} - -.folder-name { - font-weight: 600; - color: #1a1a1a; - flex: 1; -} - -.folder-count { - font-size: 0.75rem; - color: #6c757d; - background: white; - padding: 0.125rem 0.5rem; - border-radius: 10px; -} - -.folder-toggle { - font-size: 0.75rem; - color: #6c757d; - transition: transform 0.2s ease; -} - -.folder-content { - padding: 0.5rem; - background: white; -} - -.page-item { - padding: 0.5rem 0.75rem; - margin-left: 1rem; - border-radius: 6px; - transition: all 0.2s ease; -} - -.page-item:hover { - background: #f8f9fa; -} - -.page-label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - margin: 0; - width: 100%; -} - -.page-label input[type="checkbox"] { - width: 18px; - height: 18px; - cursor: pointer; - accent-color: #007bff; -} - -.page-label input[type="checkbox"]:disabled { - cursor: not-allowed; - opacity: 0.6; -} - -.file-icon { - font-size: 1rem; - opacity: 0.7; -} - -.page-label .page-name { - flex: 1; - font-size: 0.875rem; - color: #495057; -} - -.always-access-badge { - font-size: 0.65rem; - background: linear-gradient(135deg, #28a745 0%, #20c997 100%); - color: white; - padding: 0.125rem 0.5rem; - border-radius: 10px; - font-weight: 500; -} - -/* 활성화/비활성화 버튼 스타일 */ -.action-btn.activate { - background: linear-gradient(135deg, #28a745 0%, #20c997 100%); - color: white; -} - -.action-btn.activate:hover { - background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); -} - -.action-btn.deactivate { - background: linear-gradient(135deg, #6c757d 0%, #495057 100%); - color: white; -} - -.action-btn.deactivate:hover { - background: linear-gradient(135deg, #495057 0%, #343a40 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(108, 117, 125, 0.3); -} - -.action-btn.danger { - background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); - color: white; -} - -.action-btn.danger:hover { - background: linear-gradient(135deg, #c82333 0%, #a71d2a 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3); -} - -/* 작업자 연결 스타일 */ -.worker-link-container { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem; - background: #f8f9fa; - border-radius: 8px; - border: 1px solid #e9ecef; -} - -.linked-worker-info { - flex: 1; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.linked-worker-info .no-worker { - color: #6c757d; - font-style: italic; -} - -.linked-worker-info .worker-badge { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.375rem 0.75rem; - background: linear-gradient(135deg, #28a745 0%, #20c997 100%); - color: white; - border-radius: 20px; - font-size: 0.875rem; - font-weight: 500; -} - -.linked-worker-info .worker-badge .dept-name { - opacity: 0.9; - font-size: 0.75rem; -} - -/* 작업자 선택 모달 */ -.worker-select-layout { - display: grid; - grid-template-columns: 200px 1fr; - gap: 1rem; - min-height: 400px; -} - -.department-list-panel, -.worker-list-panel { - border: 1px solid #e9ecef; - border-radius: 8px; - overflow: hidden; -} - -.panel-title { - margin: 0; - padding: 0.75rem 1rem; - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - font-size: 0.875rem; - font-weight: 600; - color: #495057; - border-bottom: 1px solid #e9ecef; -} - -.department-list { - max-height: 350px; - overflow-y: auto; -} - -.department-item { - padding: 0.75rem 1rem; - cursor: pointer; - border-bottom: 1px solid #f0f0f0; - transition: all 0.2s ease; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.department-item:hover { - background: #f8f9fa; -} - -.department-item.active { - background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); - color: white; -} - -.department-item .dept-icon { - font-size: 1rem; -} - -.department-item .dept-name { - flex: 1; - font-size: 0.875rem; -} - -.department-item .dept-count { - font-size: 0.75rem; - background: rgba(0,0,0,0.1); - padding: 0.125rem 0.5rem; - border-radius: 10px; -} - -.department-item.active .dept-count { - background: rgba(255,255,255,0.2); -} - -.worker-list { - max-height: 350px; - overflow-y: auto; - padding: 0.5rem; -} - -.worker-list .empty-message { - text-align: center; - padding: 2rem; - color: #6c757d; - font-style: italic; -} - -.worker-select-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease; - border: 2px solid transparent; -} - -.worker-select-item:hover { - background: #f8f9fa; - border-color: #e9ecef; -} - -.worker-select-item.selected { - background: #e7f3ff; - border-color: #007bff; -} - -.worker-select-item .worker-avatar { - width: 40px; - height: 40px; - background: linear-gradient(135deg, #007bff 0%, #6610f2 100%); - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: 1rem; -} - -.worker-select-item .worker-info { - flex: 1; -} - -.worker-select-item .worker-name { - font-weight: 600; - color: #1a1a1a; - font-size: 0.9rem; -} - -.worker-select-item .worker-role { - font-size: 0.75rem; - color: #6c757d; -} - -.worker-select-item .select-indicator { - width: 24px; - height: 24px; - border: 2px solid #dee2e6; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; -} - -.worker-select-item.selected .select-indicator { - background: #007bff; - border-color: #007bff; - color: white; -} - -.worker-select-item .already-linked { - font-size: 0.7rem; - background: #ffc107; - color: #000; - padding: 0.125rem 0.5rem; - border-radius: 10px; -} - -/* 버튼 크기 조정 */ -.btn-sm { - padding: 0.375rem 0.75rem; - font-size: 0.8rem; -} - -/* ======================================== - 알림 수신자 설정 섹션 - ======================================== */ -#notificationRecipientsSection { - margin-top: 2rem; -} - -#notificationRecipientsSection .section-header { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; -} - -.section-description { - font-size: 0.875rem; - color: #6c757d; - margin: 0; -} - -.notification-recipients-container { - padding: 1.5rem 2rem; -} - -.notification-type-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; -} - -.notification-type-card { - background: #f8f9fa; - border: 2px solid #e9ecef; - border-radius: 12px; - padding: 1.25rem; - transition: all 0.2s ease; -} - -.notification-type-card:hover { - border-color: #007bff; - box-shadow: 0 4px 12px rgba(0, 123, 255, 0.1); -} - -.notification-type-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.notification-type-title { - display: flex; - align-items: center; - gap: 0.5rem; - font-weight: 600; - font-size: 1rem; - color: #1a1a1a; -} - -.notification-type-icon { - font-size: 1.25rem; -} - -.notification-type-card.repair .notification-type-icon { color: #fd7e14; } -.notification-type-card.safety .notification-type-icon { color: #dc3545; } -.notification-type-card.nonconformity .notification-type-icon { color: #6f42c1; } -.notification-type-card.equipment .notification-type-icon { color: #17a2b8; } -.notification-type-card.maintenance .notification-type-icon { color: #28a745; } -.notification-type-card.system .notification-type-icon { color: #6c757d; } - -.edit-recipients-btn { - padding: 0.375rem 0.75rem; - background: white; - border: 1px solid #dee2e6; - border-radius: 6px; - font-size: 0.75rem; - color: #495057; - cursor: pointer; - transition: all 0.2s ease; -} - -.edit-recipients-btn:hover { - background: #007bff; - border-color: #007bff; - color: white; -} - -.recipient-list { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - min-height: 32px; -} - -.recipient-tag { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.625rem; - background: white; - border: 1px solid #dee2e6; - border-radius: 16px; - font-size: 0.75rem; - color: #495057; -} - -.recipient-tag .tag-icon { - font-size: 0.875rem; -} - -.no-recipients { - font-size: 0.8rem; - color: #adb5bd; - font-style: italic; -} - -/* 알림 수신자 편집 모달 */ -.modal-description { - font-size: 0.875rem; - color: #6c757d; - margin-bottom: 1rem; -} - -.recipient-search-box { - margin-bottom: 1rem; -} - -.recipient-user-list { - max-height: 350px; - overflow-y: auto; - border: 1px solid #e9ecef; - border-radius: 8px; - background: #f8f9fa; -} - -.recipient-user-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - border-bottom: 1px solid #e9ecef; - cursor: pointer; - transition: all 0.15s ease; -} - -.recipient-user-item:last-child { - border-bottom: none; -} - -.recipient-user-item:hover { - background: #e9ecef; -} - -.recipient-user-item.selected { - background: #e7f3ff; -} - -.recipient-user-item input[type="checkbox"] { - width: 18px; - height: 18px; - cursor: pointer; - accent-color: #007bff; -} - -.recipient-user-item .user-avatar-small { - width: 32px; - height: 32px; - font-size: 0.8rem; -} - -.recipient-user-info { - flex: 1; -} - -.recipient-user-name { - font-weight: 500; - font-size: 0.875rem; - color: #1a1a1a; -} - -.recipient-user-role { - font-size: 0.75rem; - color: #6c757d; -} - -@media (max-width: 768px) { - .notification-type-cards { - grid-template-columns: 1fr; - } -} diff --git a/system1-factory/web/css/admin.css b/system1-factory/web/css/admin.css deleted file mode 100644 index 145f0a6..0000000 --- a/system1-factory/web/css/admin.css +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/system1-factory/web/css/annual-vacation-overview.css b/system1-factory/web/css/annual-vacation-overview.css deleted file mode 100644 index 3d1730f..0000000 --- a/system1-factory/web/css/annual-vacation-overview.css +++ /dev/null @@ -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; - } -} diff --git a/system1-factory/web/css/attendance-validation.css b/system1-factory/web/css/attendance-validation.css deleted file mode 100644 index 15184a3..0000000 --- a/system1-factory/web/css/attendance-validation.css +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/system1-factory/web/css/attendance.css b/system1-factory/web/css/attendance.css deleted file mode 100644 index 0b77e15..0000000 --- a/system1-factory/web/css/attendance.css +++ /dev/null @@ -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; } \ No newline at end of file diff --git a/system1-factory/web/css/common.css b/system1-factory/web/css/common.css deleted file mode 100644 index eb1a24c..0000000 --- a/system1-factory/web/css/common.css +++ /dev/null @@ -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; } -} diff --git a/system1-factory/web/css/daily-issue.css b/system1-factory/web/css/daily-issue.css deleted file mode 100644 index 827e7c7..0000000 --- a/system1-factory/web/css/daily-issue.css +++ /dev/null @@ -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; -} diff --git a/system1-factory/web/css/design-system.css b/system1-factory/web/css/design-system.css deleted file mode 100644 index ee1b15c..0000000 --- a/system1-factory/web/css/design-system.css +++ /dev/null @@ -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); } -} diff --git a/system1-factory/web/css/factory.css b/system1-factory/web/css/factory.css deleted file mode 100644 index e7dcd68..0000000 --- a/system1-factory/web/css/factory.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/system1-factory/web/css/login.css b/system1-factory/web/css/login.css deleted file mode 100644 index 3f9061e..0000000 --- a/system1-factory/web/css/login.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/system1-factory/web/css/main-layout.css b/system1-factory/web/css/main-layout.css deleted file mode 100644 index 6467328..0000000 --- a/system1-factory/web/css/main-layout.css +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/system1-factory/web/css/management-dashboard.css b/system1-factory/web/css/management-dashboard.css deleted file mode 100644 index 08524ef..0000000 --- a/system1-factory/web/css/management-dashboard.css +++ /dev/null @@ -1,1005 +0,0 @@ -/* management-dashboard.css - 관리자 대시보드 전용 스타일 */ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Segoe UI', 'Noto Sans KR', Tahoma, Geneva, Verdana, sans-serif; - background-color: #f5f7fa; - color: #333; - line-height: 1.6; -} - -/* 메인 레이아웃 */ -.main-layout-with-navbar { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.content-wrapper { - flex: 1; - padding: 20px; -} - -.dashboard-container { - max-width: 1400px; - margin: 0 auto; -} - -/* 페이지 헤더 */ -.page-header { - text-align: center; - margin-bottom: 2.5rem; - padding: 3rem 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); - position: relative; -} - -.page-header h1 { - font-size: 2.5rem; - margin-bottom: 0.8rem; - font-weight: 700; -} - -.subtitle { - font-size: 1.1rem; - opacity: 0.9; - margin-bottom: 1rem; -} - -.permission-badge { - display: inline-block; - background: rgba(255,255,255,0.2); - color: white; - padding: 8px 20px; - border-radius: 25px; - font-size: 14px; - font-weight: 600; - border: 2px solid rgba(255,255,255,0.3); -} - -/* 뒤로가기 버튼 */ -.back-btn { - background: rgba(255,255,255,0.95); - color: #667eea; - border: 3px solid #667eea; - padding: 16px 32px; - border-radius: 12px; - text-decoration: none; - font-weight: 700; - font-size: 18px; - display: inline-flex; - align-items: center; - gap: 12px; - 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); -} - -/* 메시지 스타일 */ -.message { - padding: 20px 32px; - border-radius: 12px; - margin-bottom: 32px; - font-weight: 600; - font-size: 18px; - box-shadow: 0 3px 12px rgba(0,0,0,0.1); -} - -.message.warning { - background: #fff3cd; - color: #856404; - border: 2px solid #ffeaa7; -} - -.message.error { - background: #f8d7da; - color: #721c24; - border: 2px solid #f5c6cb; -} - -.message.success { - background: #d4edda; - color: #155724; - border: 2px solid #c3e6cb; -} - -.message.loading { - background: #cce5ff; - color: #0066cc; - border: 2px solid #99d6ff; -} - -/* 날짜 선택 카드 */ -.date-selection-card { - background: white; - border-radius: 16px; - padding: 2rem; - box-shadow: 0 6px 24px rgba(0,0,0,0.08); - margin-bottom: 2rem; -} - -.date-selection-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 2px solid #f0f0f0; -} - -.date-selection-header h3 { - margin: 0; - color: #333; - font-size: 1.4rem; -} - -.refresh-btn { - background: #28a745; - color: white; - border: none; - border-radius: 8px; - padding: 12px 20px; - cursor: pointer; - font-size: 14px; - font-weight: 600; - transition: all 0.3s; - display: flex; - align-items: center; - gap: 8px; -} - -.refresh-btn:hover { - background: #1e7e34; - transform: translateY(-1px); -} - -.date-selection-body { - display: flex; - gap: 20px; - align-items: center; - justify-content: center; -} - -.date-input { - padding: 15px 20px; - font-size: 18px; - border: 3px solid #e1e5e9; - border-radius: 12px; - background: white; - transition: border-color 0.3s; - min-width: 200px; -} - -.date-input:focus { - outline: none; - border-color: #007bff; - box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.15); -} - -/* 버튼 스타일 */ -.btn { - padding: 15px 30px; - border: none; - border-radius: 12px; - font-size: 16px; - font-weight: 700; - cursor: pointer; - transition: all 0.3s; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - text-decoration: none; - box-shadow: 0 3px 12px rgba(0,0,0,0.1); -} - -.btn-primary { - background: #007bff; - color: white; -} - -.btn-primary:hover { - background: #0056b3; - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(0, 123, 255, 0.3); -} - -.btn-secondary { - background: #6c757d; - color: white; -} - -.btn-secondary:hover { - background: #545b62; - transform: translateY(-2px); -} - -/* 요약 섹션 */ -.summary-section { - background: white; - border-radius: 16px; - padding: 2rem; - box-shadow: 0 6px 24px rgba(0,0,0,0.08); - margin-bottom: 2rem; -} - -.summary-section h3 { - margin-bottom: 1.5rem; - color: #333; - font-size: 1.4rem; -} - -.summary-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 20px; -} - -.summary-card { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - border-radius: 12px; - padding: 1.5rem; - display: flex; - align-items: center; - gap: 15px; - border: 2px solid #dee2e6; - transition: all 0.3s ease; -} - -.summary-card:hover { - transform: translateY(-3px); - box-shadow: 0 8px 25px rgba(0,0,0,0.1); -} - -.summary-icon { - font-size: 2rem; - width: 60px; - height: 60px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: rgba(255,255,255,0.8); -} - -.summary-content { - flex: 1; -} - -.summary-number { - font-size: 2rem; - font-weight: 700; - color: #333; - line-height: 1; -} - -.summary-label { - font-size: 0.9rem; - color: #666; - margin-top: 4px; -} - -/* 개별 요약 카드 색상 */ -.summary-card.total-workers { - background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); - border-color: #2196f3; -} - -.summary-card.completed-workers { - background: linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%); - border-color: #4caf50; -} - -.summary-card.missing-workers { - background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%); - border-color: #f44336; -} - -.summary-card.total-hours { - background: linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%); - border-color: #9c27b0; -} - -.summary-card.total-entries { - background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%); - border-color: #ff9800; -} - -.summary-card.error-count { - background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%); - border-color: #e91e63; -} - -/* 액션 바 */ -.action-bar { - background: white; - border-radius: 12px; - padding: 1.5rem; - box-shadow: 0 4px 16px rgba(0,0,0,0.06); - margin-bottom: 2rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.filter-section { - display: flex; - gap: 20px; - align-items: center; -} - -.filter-checkbox { - display: flex; - align-items: center; - gap: 10px; - cursor: pointer; - font-weight: 600; - font-size: 16px; - color: #333; -} - -.filter-checkbox input[type="checkbox"] { - display: none; -} - -.checkmark { - width: 20px; - height: 20px; - border: 2px solid #007bff; - border-radius: 4px; - position: relative; - transition: all 0.3s ease; -} - -.filter-checkbox input[type="checkbox"]:checked + .checkmark { - background: #007bff; -} - -.filter-checkbox input[type="checkbox"]:checked + .checkmark::after { - content: "✓"; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: white; - font-weight: bold; - font-size: 12px; -} - -/* 작업자 섹션 */ -.workers-section { - background: white; - border-radius: 16px; - padding: 2rem; - box-shadow: 0 6px 24px rgba(0,0,0,0.08); - margin-bottom: 2rem; -} - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 2px solid #f0f0f0; -} - -.section-header h3 { - margin: 0; - color: #333; - font-size: 1.4rem; -} - -.legend { - display: flex; - gap: 15px; -} - -.legend-item { - font-size: 14px; - font-weight: 600; - padding: 6px 12px; - border-radius: 20px; - border: 2px solid; -} - -.legend-item.completed { - color: #28a745; - border-color: #28a745; - background: rgba(40, 167, 69, 0.1); -} - -.legend-item.missing { - color: #dc3545; - border-color: #dc3545; - background: rgba(220, 53, 69, 0.1); -} - -.legend-item.partial { - color: #ffc107; - border-color: #ffc107; - background: rgba(255, 193, 7, 0.1); -} - -/* 작업자 테이블 스타일 */ -.table-container { - background: white; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 4px 16px rgba(0,0,0,0.06); -} - -.workers-table { - width: 100%; - border-collapse: collapse; - font-size: 14px; -} - -.workers-table thead { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; -} - -.workers-table th { - padding: 16px 12px; - text-align: left; - font-weight: 700; - font-size: 14px; - border-bottom: 2px solid rgba(255,255,255,0.2); -} - -.workers-table th:first-child { - padding-left: 20px; -} - -.workers-table th:last-child { - padding-right: 20px; -} - -.workers-table tbody tr { - border-bottom: 1px solid #e9ecef; - transition: all 0.3s ease; -} - -.workers-table tbody tr:hover { - background: #f8f9fa; - transform: scale(1.01); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -.workers-table tbody tr:last-child { - border-bottom: none; -} - -.workers-table td { - padding: 16px 12px; - vertical-align: middle; - line-height: 1.4; -} - -.workers-table td:first-child { - padding-left: 20px; -} - -.workers-table td:last-child { - padding-right: 20px; -} - -/* 작업자 이름 스타일 */ -.worker-name-cell { - font-weight: 700; - color: #333; - display: flex; - align-items: center; - gap: 8px; -} - -/* 상태 배지 스타일 */ -.status-badge { - padding: 6px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 700; - color: white; - white-space: nowrap; - text-align: center; - min-width: 70px; -} - -.status-badge.completed { - background: linear-gradient(135deg, #28a745 0%, #20c997 100%); -} - -.status-badge.missing { - background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); -} - -.status-badge.partial { - background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%); - color: #333; -} - -/* 시간 표시 스타일 */ -.hours-cell { - font-weight: 700; - font-size: 16px; - color: #495057; -} - -.hours-cell.zero { - color: #dc3545; - opacity: 0.7; -} - -.hours-cell.partial { - color: #ffc107; -} - -.hours-cell.full { - color: #28a745; -} - -/* 작업 유형 태그 */ -.work-types-container { - display: flex; - flex-wrap: wrap; - gap: 4px; - max-width: 120px; -} - -.work-type-tag { - background: #e3f2fd; - color: #1565c0; - padding: 4px 8px; - border-radius: 12px; - font-size: 11px; - font-weight: 600; - white-space: nowrap; - border: 1px solid #bbdefb; -} - -/* 프로젝트 태그 */ -.projects-container { - display: flex; - flex-wrap: wrap; - gap: 4px; - max-width: 150px; -} - -.project-tag { - background: #f3e5f5; - color: #7b1fa2; - padding: 4px 8px; - border-radius: 12px; - font-size: 11px; - font-weight: 600; - white-space: nowrap; - border: 1px solid #e1bee7; -} - -/* 기여자 태그 */ -.contributors-container { - display: flex; - flex-wrap: wrap; - gap: 4px; - max-width: 120px; -} - -.contributor-tag { - background: #e8f5e8; - color: #2e7d32; - padding: 4px 8px; - border-radius: 12px; - font-size: 11px; - font-weight: 600; - white-space: nowrap; - border: 1px solid #c8e6c9; -} - -/* 업데이트 시간 스타일 */ -.update-time { - font-size: 12px; - color: #666; - white-space: nowrap; -} - -.update-time.recent { - color: #28a745; - font-weight: 600; -} - -.update-time.old { - color: #dc3545; -} - -/* 상세 버튼 */ -.detail-btn { - background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); - color: white; - border: none; - border-radius: 20px; - padding: 8px 16px; - cursor: pointer; - font-size: 12px; - font-weight: 600; - transition: all 0.3s; - white-space: nowrap; -} - -.detail-btn:hover { - background: linear-gradient(135deg, #0056b3 0%, #004085 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3); -} - -/* 데이터 없음 행 */ -.no-data-row { - text-align: center; - padding: 40px 20px; - color: #666; - font-style: italic; -} - -/* 테이블 반응형 */ -@media (max-width: 1200px) { - .workers-table { - font-size: 13px; - } - - .workers-table th, - .workers-table td { - padding: 12px 8px; - } - - .work-types-container, - .projects-container, - .contributors-container { - max-width: 100px; - } -} - -@media (max-width: 992px) { - .table-container { - overflow-x: auto; - } - - .workers-table { - min-width: 800px; - font-size: 12px; - } - - .workers-table th, - .workers-table td { - padding: 10px 6px; - } - - .work-types-container, - .projects-container, - .contributors-container { - max-width: 80px; - } - - .work-type-tag, - .project-tag, - .contributor-tag { - font-size: 10px; - padding: 3px 6px; - } -} - -@media (max-width: 768px) { - .workers-table { - min-width: 700px; - font-size: 11px; - } - - .workers-table th, - .workers-table td { - padding: 8px 4px; - } - - .status-badge { - font-size: 10px; - padding: 4px 8px; - min-width: 60px; - } - - .detail-btn { - font-size: 10px; - padding: 6px 12px; - } -} - -/* 로딩 스피너 */ -.loading-spinner { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 4rem 2rem; - background: white; - border-radius: 16px; - box-shadow: 0 6px 24px rgba(0,0,0,0.08); -} - -.spinner { - width: 50px; - height: 50px; - border: 4px solid #f3f3f3; - border-top: 4px solid #007bff; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 1rem; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.loading-spinner p { - color: #666; - font-size: 1.1rem; - font-weight: 600; -} - -/* 데이터 없음 메시지 */ -.no-data-message { - text-align: center; - padding: 4rem 2rem; - background: white; - border-radius: 16px; - box-shadow: 0 6px 24px rgba(0,0,0,0.08); -} - -.no-data-icon { - font-size: 4rem; - margin-bottom: 1rem; -} - -.no-data-message h3 { - color: #333; - margin-bottom: 1rem; - font-size: 1.5rem; -} - -.no-data-message p { - color: #666; - font-size: 1.1rem; - line-height: 1.6; -} - -/* 작업자 상세 모달 */ -.worker-detail-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: 1000; - animation: fadeIn 0.3s ease; -} - -.modal-content { - background: white; - border-radius: 16px; - width: 90%; - max-width: 800px; - max-height: 90vh; - overflow-y: auto; - box-shadow: 0 10px 40px rgba(0,0,0,0.3); - animation: slideIn 0.3s ease; -} - -.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; -} - -.modal-header h3 { - margin: 0; - font-size: 20px; - font-weight: 700; -} - -.close-modal-btn { - background: rgba(255,255,255,0.2); - color: white; - border: none; - border-radius: 50%; - width: 32px; - height: 32px; - cursor: pointer; - font-size: 18px; - 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); -} - -.modal-body { - padding: 24px; -} - -/* 사용법 안내 */ -.guide-section { - background: white; - border-radius: 16px; - padding: 2rem; - box-shadow: 0 6px 24px rgba(0,0,0,0.08); - margin-top: 2rem; -} - -.guide-section h3 { - margin-bottom: 1.5rem; - color: #333; - font-size: 1.4rem; - text-align: center; -} - -.guide-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 20px; -} - -.guide-item { - text-align: center; - padding: 20px; - background: #f8f9fa; - border-radius: 12px; - border: 2px solid #e9ecef; - transition: all 0.3s ease; -} - -.guide-item:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(0,0,0,0.1); - border-color: #667eea; -} - -.guide-icon { - font-size: 28px; - margin-bottom: 12px; -} - -.guide-item strong { - display: block; - font-size: 16px; - font-weight: 700; - margin-bottom: 8px; - color: #333; -} - -/* 애니메이션 */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateY(-50px) scale(0.9); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -/* 수정 모달 스타일 */ -.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: 600px; - max-height: 90vh; - overflow-y: auto; - box-shadow: 0 10px 40px rgba(0,0,0,0.3); - animation: slideIn 0.3s ease; -} - -.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: 20px; - font-weight: 700; -} - -.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: 16px; - 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; -} \ No newline at end of file diff --git a/system1-factory/web/css/my-attendance.css b/system1-factory/web/css/my-attendance.css deleted file mode 100644 index a984fb8..0000000 --- a/system1-factory/web/css/my-attendance.css +++ /dev/null @@ -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; - } -} diff --git a/system1-factory/web/css/my-dashboard.css b/system1-factory/web/css/my-dashboard.css deleted file mode 100644 index 89e545d..0000000 --- a/system1-factory/web/css/my-dashboard.css +++ /dev/null @@ -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; - } -} diff --git a/system1-factory/web/css/project-management.css b/system1-factory/web/css/project-management.css deleted file mode 100644 index a0ee6a5..0000000 --- a/system1-factory/web/css/project-management.css +++ /dev/null @@ -1,1570 +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 컴포넌트가 관리 */ -/* 필요한 경우 navbar.html에서 수정하세요 */ - -.logo { - height: 40px; - width: auto; -} - -.company-info { - display: flex; - flex-direction: column; -} - -.company-name { - font-size: 1.25rem; - font-weight: 700; - color: #1f2937; - margin: 0; - line-height: 1.2; -} - -.company-subtitle { - font-size: 0.875rem; - color: #6b7280; - font-weight: 500; -} - -.header-center { - display: flex; - align-items: center; -} - -.current-time { - display: flex; - flex-direction: column; - align-items: center; - padding: 0.5rem 1rem; - background: rgba(59, 130, 246, 0.1); - border-radius: 0.5rem; - border: 1px solid rgba(59, 130, 246, 0.2); -} - -.time-label { - font-size: 0.75rem; - color: #6b7280; - margin-bottom: 0.125rem; -} - -.time-value { - font-size: 1rem; - font-weight: 600; - color: #1f2937; - font-family: 'Courier New', monospace; -} - -.header-right { - display: flex; - align-items: center; - gap: 1rem; -} - -.header-actions { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.back-btn, .dashboard-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - background: rgba(255, 255, 255, 0.15); - color: #374151; - text-decoration: none; - border-radius: 1.25rem; - font-size: 0.85rem; - font-weight: 500; - transition: all 0.3s ease; - border: 1px solid rgba(255, 255, 255, 0.3); - backdrop-filter: blur(10px); -} - -.back-btn:hover, .dashboard-btn:hover { - background: rgba(255, 255, 255, 0.25); - transform: translateY(-1px); - text-decoration: none; - color: #1f2937; -} - -.user-profile { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.5rem 1rem; - background: rgba(255, 255, 255, 0.1); - border-radius: 2rem; - cursor: pointer; - transition: all 0.3s ease; - position: relative; -} - -.user-profile:hover { - background: rgba(255, 255, 255, 0.2); -} - -.user-avatar { - width: 2.5rem; - height: 2.5rem; - background: linear-gradient(135deg, #3b82f6, #1d4ed8); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-weight: 600; - font-size: 1rem; -} - -.user-info { - display: flex; - flex-direction: column; -} - -.user-name { - font-size: 0.875rem; - font-weight: 600; - color: #1f2937; - line-height: 1.2; -} - -.user-role { - font-size: 0.75rem; - color: #6b7280; -} - -/* 메인 콘텐츠 */ -.dashboard-main { - flex: 1; - padding: 2rem; - min-height: calc(100vh - 80px); - max-width: 1600px; - margin: 0 auto; - width: 100%; -} - -.page-header { - display: flex; - justify-content: space-between; - align-items: flex-end; - margin-bottom: 2rem; -} - -.page-title-section { - flex: 1; -} - -.page-title { - display: flex; - align-items: 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; -} - -.page-actions { - display: flex; - gap: 1rem; -} - -.btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border: none; - border-radius: 0.75rem; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; - text-decoration: none; -} - -.btn-primary { - background: #3b82f6; - color: white; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); -} - -.btn-primary:hover { - background: #2563eb; - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4); -} - -.btn-secondary { - background: rgba(255, 255, 255, 0.9); - color: #374151; - border: 1px solid rgba(255, 255, 255, 0.3); -} - -.btn-secondary:hover { - background: white; - transform: translateY(-2px); -} - -.btn-danger { - background: #ef4444; - color: white; -} - -.btn-danger:hover { - background: #dc2626; - transform: translateY(-2px); -} - -/* 검색 및 필터 섹션 */ -.search-section { - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border-radius: 1rem; - padding: 1.5rem; - margin-bottom: 2rem; - border: 1px solid rgba(255, 255, 255, 0.2); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); -} - -.search-bar { - display: flex; - gap: 1rem; - margin-bottom: 1rem; -} - -.search-input { - flex: 1; - padding: 0.75rem 1rem; - border: 1px solid #d1d5db; - border-radius: 0.5rem; - font-size: 0.875rem; - transition: all 0.3s ease; -} - -.search-input:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -.search-btn { - padding: 0.75rem 1rem; - background: #3b82f6; - color: white; - border: none; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.3s ease; -} - -.search-btn:hover { - background: #2563eb; -} - -.filter-options { - display: flex; - gap: 1rem; -} - -.filter-select { - padding: 0.5rem 1rem; - border: 1px solid #d1d5db; - border-radius: 0.5rem; - font-size: 0.875rem; - background: white; - cursor: pointer; -} - -/* 프로젝트 섹션 */ -.projects-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; -} - -.project-stats { - display: flex; - gap: 1.5rem; - font-size: 0.875rem; - align-items: center; -} - -.stat-item { - display: flex; - align-items: center; - gap: 0.25rem; - padding: 0.5rem 0.75rem; - border-radius: 0.5rem; - font-weight: 500; - transition: all 0.3s ease; - cursor: pointer; - position: relative; -} - -.stat-item:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -.stat-item.active { - transform: scale(1.05); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2); - z-index: 10; -} - -.stat-item .stat-icon { - font-size: 1rem; -} - -.stat-item span:not(.stat-icon) { - font-weight: 600; -} - -/* 활성 프로젝트 통계 */ -.active-stat { - background: rgba(16, 185, 129, 0.1); - color: #065f46; - border: 1px solid rgba(16, 185, 129, 0.2); -} - -.active-stat span:not(.stat-icon) { - color: #10b981; -} - -.active-stat.active { - background: rgba(16, 185, 129, 0.2); - border: 2px solid #10b981; -} - -.active-stat:hover { - background: rgba(16, 185, 129, 0.15); -} - -/* 비활성 프로젝트 통계 */ -.inactive-stat { - background: rgba(239, 68, 68, 0.1); - color: #7f1d1d; - border: 1px solid rgba(239, 68, 68, 0.2); -} - -.inactive-stat span:not(.stat-icon) { - color: #ef4444; -} - -.inactive-stat.active { - background: rgba(239, 68, 68, 0.2); - border: 2px solid #ef4444; -} - -.inactive-stat:hover { - background: rgba(239, 68, 68, 0.15); -} - -/* 전체 프로젝트 통계 */ -.total-stat { - background: rgba(59, 130, 246, 0.1); - color: #1e3a8a; - border: 1px solid rgba(59, 130, 246, 0.2); -} - -.total-stat span:not(.stat-icon) { - color: #3b82f6; -} - -.total-stat.active { - background: rgba(59, 130, 246, 0.2); - border: 2px solid #3b82f6; -} - -.total-stat:hover { - background: rgba(59, 130, 246, 0.15); -} - -/* 프로젝트 그리드 */ -.projects-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - gap: 1.5rem; - justify-content: center; -} - -/* 작업자 카드 전용 스타일 */ -.worker-card .project-info { - display: flex; - align-items: flex-start; - gap: 1rem; -} - -.worker-avatar { - width: 60px; - height: 60px; - border-radius: 50%; - background: linear-gradient(135deg, #3b82f6, #1d4ed8); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); -} - -.avatar-initial { - color: white; - font-size: 1.5rem; - font-weight: 700; -} - -.worker-card .project-name { - margin-top: 0.25rem; -} - -.worker-card .project-meta { - margin-top: 0.5rem; -} - -.worker-card.inactive .worker-avatar { - background: linear-gradient(135deg, #9ca3af, #6b7280); - box-shadow: 0 4px 12px rgba(156, 163, 175, 0.3); -} - -/* 작업 유형 트리 뷰 스타일 */ -.task-tree-container { - 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); -} - -.tree-header { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 1px solid #e5e7eb; -} - -.btn-outline { - background: transparent; - border: 1px solid #d1d5db; - color: #374151; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - font-size: 0.875rem; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.btn-outline:hover { - background: #f3f4f6; - border-color: #9ca3af; -} - -.task-tree { - max-height: 600px; - overflow-y: auto; -} - -/* 카테고리 (대분류) 스타일 */ -.tree-category { - margin-bottom: 1rem; - border: 1px solid #e5e7eb; - border-radius: 0.75rem; - overflow: hidden; -} - -.category-header { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem 1.25rem; - background: linear-gradient(135deg, #f8fafc, #e2e8f0); - cursor: pointer; - transition: all 0.3s ease; - border-bottom: 1px solid #e5e7eb; -} - -.category-header:hover { - background: linear-gradient(135deg, #f1f5f9, #cbd5e1); -} - -.category-toggle { - font-size: 0.875rem; - color: #6b7280; - transition: transform 0.3s ease; -} - -.category-icon { - font-size: 1.25rem; -} - -.category-name { - font-size: 1.125rem; - font-weight: 600; - color: #1f2937; - flex: 1; -} - -.category-count { - font-size: 0.875rem; - color: #6b7280; - background: rgba(107, 114, 128, 0.1); - padding: 0.25rem 0.5rem; - border-radius: 0.375rem; -} - -.category-actions { - display: flex; - gap: 0.5rem; -} - -.category-content { - background: #ffffff; -} - -/* 서브카테고리 (중분류) 스타일 */ -.tree-subcategory { - border-bottom: 1px solid #f3f4f6; -} - -.tree-subcategory:last-child { - border-bottom: none; -} - -.subcategory-header { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.875rem 1.25rem; - background: #f8fafc; - cursor: pointer; - transition: all 0.3s ease; -} - -.subcategory-header:hover { - background: #f1f5f9; -} - -.subcategory-toggle { - font-size: 0.75rem; - color: #6b7280; - margin-left: 1rem; -} - -.subcategory-icon { - font-size: 1rem; -} - -.subcategory-name { - font-size: 1rem; - font-weight: 500; - color: #374151; - flex: 1; -} - -.subcategory-count { - font-size: 0.75rem; - color: #6b7280; - background: rgba(107, 114, 128, 0.1); - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; -} - -.subcategory-actions { - display: flex; - gap: 0.25rem; -} - -.subcategory-content { - background: #ffffff; -} - -/* 작업 (상세) 스타일 */ -.tree-task { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1.25rem 0.75rem 2.5rem; - border-bottom: 1px solid #f9fafb; - cursor: pointer; - transition: all 0.3s ease; -} - -.tree-task:hover { - background: #f9fafb; -} - -.tree-task:last-child { - border-bottom: none; -} - -.task-info { - display: flex; - align-items: center; - gap: 0.75rem; - flex: 1; -} - -.task-icon { - font-size: 0.875rem; - color: #6b7280; -} - -.task-name { - font-size: 0.875rem; - font-weight: 500; - color: #1f2937; -} - -.task-description { - font-size: 0.75rem; - color: #6b7280; - margin-left: 0.5rem; - font-style: italic; -} - -.task-actions { - display: flex; - gap: 0.25rem; - opacity: 0; - transition: opacity 0.3s ease; -} - -.tree-task:hover .task-actions { - opacity: 1; -} - -/* 작은 버튼 스타일 */ -.btn-small { - padding: 0.25rem 0.5rem; - border: none; - border-radius: 0.25rem; - font-size: 0.75rem; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - justify-content: center; -} - -.btn-small.btn-primary { - background: #3b82f6; - color: white; -} - -.btn-small.btn-primary:hover { - background: #2563eb; -} - -.btn-small.btn-secondary { - background: #6b7280; - color: white; -} - -.btn-small.btn-secondary:hover { - background: #4b5563; -} - -.btn-small.btn-edit { - background: #f59e0b; - color: white; -} - -.btn-small.btn-edit:hover { - background: #d97706; -} - -.btn-small.btn-delete { - background: #ef4444; - color: white; -} - -.btn-small.btn-delete:hover { - background: #dc2626; -} - -/* 코드 관리 전용 스타일 */ -.code-tabs { - display: flex; - gap: 0.5rem; - margin-bottom: 2rem; - border-bottom: 3px solid #d1d5db; - padding-bottom: 0; - background: rgba(255, 255, 255, 0.9); - border-radius: 1rem 1rem 0 0; - padding: 0.5rem 0.5rem 0 0.5rem; -} - -.tab-btn { - background: rgba(255, 255, 255, 0.7); - border: 2px solid #e5e7eb; - padding: 1rem 1.5rem; - border-radius: 0.75rem; - cursor: pointer; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.95rem; - font-weight: 600; - color: #4b5563; - border-bottom: 3px solid transparent; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.tab-btn:hover { - background: rgba(59, 130, 246, 0.1); - color: #1e40af; - border-color: #3b82f6; - transform: translateY(-1px); -} - -.tab-btn.active { - background: linear-gradient(135deg, #3b82f6, #1d4ed8); - color: #ffffff; - border-color: #1d4ed8; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); - transform: translateY(-2px); -} - -.tab-icon { - font-size: 1rem; -} - -.code-tab-content { - display: none; -} - -.code-tab-content.active { - display: block; -} - -.code-section { - background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(248, 250, 252, 0.95)); - backdrop-filter: blur(20px); - border-radius: 1rem; - padding: 2rem; - border: 2px solid rgba(59, 130, 246, 0.2); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); -} - -.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 { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1.25rem; - font-weight: 600; - color: #1f2937; - margin: 0; -} - -.section-icon { - font-size: 1.5rem; -} - -.section-actions { - display: flex; - gap: 0.75rem; -} - -.code-stats { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - flex-wrap: wrap; -} - -.code-stats .stat-item { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.05)); - border: 2px solid rgba(59, 130, 246, 0.3); - color: #1e40af; - padding: 0.75rem 1.25rem; - border-radius: 0.75rem; - font-size: 0.9rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 0.5rem; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); -} - -.code-stats .critical-stat { - background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1)); - border-color: rgba(239, 68, 68, 0.4); - color: #dc2626; - box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); -} - -.code-stats .high-stat { - background: linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(249, 115, 22, 0.1)); - border-color: rgba(249, 115, 22, 0.4); - color: #ea580c; - box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3); -} - -.code-stats .medium-stat { - background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(245, 158, 11, 0.1)); - border-color: rgba(245, 158, 11, 0.4); - color: #d97706; - box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3); -} - -.code-stats .low-stat { - background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1)); - border-color: rgba(16, 185, 129, 0.4); - color: #059669; - box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); -} - -.code-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 1.5rem; -} - -.code-card { - background: linear-gradient(135deg, #ffffff, #f8fafc); - border: 2px solid #e5e7eb; - border-radius: 1rem; - padding: 1.5rem; - cursor: pointer; - transition: all 0.3s ease; - position: relative; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.code-card:hover { - transform: translateY(-4px); - box-shadow: 0 12px 35px rgba(0, 0, 0, 0.2); - border-color: #3b82f6; - background: linear-gradient(135deg, #ffffff, #f0f9ff); -} - -.code-card.normal-status { - border-left: 6px solid #10b981; - background: linear-gradient(135deg, #ffffff, #f0fdf4); -} - -.code-card.normal-status:hover { - background: linear-gradient(135deg, #f0fdf4, #dcfce7); - box-shadow: 0 12px 35px rgba(16, 185, 129, 0.3); -} - -.code-card.error-status { - border-left: 6px solid #ef4444; - background: linear-gradient(135deg, #ffffff, #fef2f2); -} - -.code-card.error-status:hover { - background: linear-gradient(135deg, #fef2f2, #fee2e2); - box-shadow: 0 12px 35px rgba(239, 68, 68, 0.3); -} - -.code-card.error-type-card.severity-low { - border-left: 6px solid #10b981; - background: linear-gradient(135deg, #ffffff, #f0fdf4); -} - -.code-card.error-type-card.severity-low:hover { - background: linear-gradient(135deg, #f0fdf4, #dcfce7); - box-shadow: 0 12px 35px rgba(16, 185, 129, 0.3); -} - -.code-card.error-type-card.severity-medium { - border-left: 6px solid #f59e0b; - background: linear-gradient(135deg, #ffffff, #fffbeb); -} - -.code-card.error-type-card.severity-medium:hover { - background: linear-gradient(135deg, #fffbeb, #fef3c7); - box-shadow: 0 12px 35px rgba(245, 158, 11, 0.3); -} - -.code-card.error-type-card.severity-high { - border-left: 6px solid #f97316; - background: linear-gradient(135deg, #ffffff, #fff7ed); -} - -.code-card.error-type-card.severity-high:hover { - background: linear-gradient(135deg, #fff7ed, #fed7aa); - box-shadow: 0 12px 35px rgba(249, 115, 22, 0.3); -} - -.code-card.error-type-card.severity-critical { - border-left: 6px solid #ef4444; - background: linear-gradient(135deg, #ffffff, #fef2f2); -} - -.code-card.error-type-card.severity-critical:hover { - background: linear-gradient(135deg, #fef2f2, #fee2e2); - box-shadow: 0 12px 35px rgba(239, 68, 68, 0.3); -} - -.code-card.work-type-card { - border-left: 6px solid #6366f1; - background: linear-gradient(135deg, #ffffff, #faf5ff); -} - -.code-card.work-type-card:hover { - background: linear-gradient(135deg, #faf5ff, #f3e8ff); - box-shadow: 0 12px 35px rgba(99, 102, 241, 0.3); -} - -.code-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: 1rem; -} - -.code-icon { - font-size: 1.5rem; - margin-right: 0.75rem; -} - -.code-info { - flex: 1; -} - -.code-name { - font-size: 1.25rem; - font-weight: 700; - color: #111827; - margin: 0 0 0.5rem 0; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); -} - -.code-label { - font-size: 0.8rem; - font-weight: 600; - color: #374151; - background: linear-gradient(135deg, #f3f4f6, #e5e7eb); - padding: 0.25rem 0.75rem; - border-radius: 0.5rem; - border: 1px solid #d1d5db; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.code-description { - color: #6b7280; - font-size: 0.875rem; - line-height: 1.5; - margin: 0 0 1rem 0; -} - -.solution-guide { - background: #f0f9ff; - border: 1px solid #bae6fd; - border-radius: 0.5rem; - padding: 0.75rem; - margin: 1rem 0; - font-size: 0.875rem; - color: #0c4a6e; -} - -.code-meta { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.75rem; - color: #9ca3af; - margin-top: 1rem; - padding-top: 0.75rem; - border-top: 1px solid #f3f4f6; -} - -.code-date { - font-size: 0.75rem; - color: #9ca3af; -} - -.code-actions { - display: flex; - gap: 0.25rem; - opacity: 0; - transition: opacity 0.3s ease; -} - -.code-card:hover .code-actions { - opacity: 1; -} - -.form-checkbox { - margin-right: 0.5rem; -} - -.form-help { - display: block; - margin-top: 0.25rem; - font-size: 0.75rem; - color: #6b7280; -} - -/* 프로필 드롭다운 스타일 개선 */ -.profile-dropdown { - position: relative; -} - -.profile-dropdown .dropdown-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - color: #374151; - text-decoration: none; - transition: all 0.3s ease; - border: none; - background: transparent; - width: 100%; - text-align: left; - font-size: 0.875rem; - cursor: pointer; - font-family: inherit; - border-radius: 0.5rem; - margin: 0.25rem; -} - -.profile-dropdown .dropdown-item:hover { - background: linear-gradient(135deg, #f3f4f6, #e5e7eb); - color: #1f2937; - transform: translateX(2px); -} - -.profile-dropdown .dropdown-item.logout-btn { - color: #dc2626; - border-top: 1px solid #e5e7eb; - margin-top: 0.5rem; - padding-top: 0.75rem; -} - -.profile-dropdown .dropdown-item.logout-btn:hover { - background: linear-gradient(135deg, #fef2f2, #fee2e2); - color: #b91c1c; -} - -.profile-dropdown .dropdown-icon { - font-size: 1.1rem; - width: 1.5rem; - text-align: center; - opacity: 0.8; -} - -.profile-dropdown .dropdown-item:hover .dropdown-icon { - opacity: 1; -} - -/* 헤더 사용자 프로필 스타일 개선 */ -.user-profile { - position: relative; - cursor: pointer; -} - -.user-profile .profile-dropdown { - position: absolute; - top: calc(100% + 0.5rem); - right: 0; - background: linear-gradient(135deg, #ffffff, #f8fafc); - border: 2px solid rgba(59, 130, 246, 0.2); - border-radius: 1rem; - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); - min-width: 200px; - opacity: 0; - visibility: hidden; - transform: translateY(-10px); - transition: all 0.3s ease; - overflow: hidden; - z-index: 1000; - backdrop-filter: blur(20px); -} - -.user-profile .profile-dropdown[style*="block"] { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -/* 반응형 디자인 */ -@media (max-width: 768px) { - .code-tabs { - flex-direction: column; - gap: 0; - } - - .tab-btn { - border-radius: 0; - border-bottom: 1px solid #e5e7eb; - } - - .tab-btn.active { - border-bottom-color: #3b82f6; - } - - .section-header { - flex-direction: column; - align-items: flex-start; - gap: 1rem; - } - - .code-stats { - justify-content: center; - } - - .code-grid { - grid-template-columns: 1fr; - } - - .user-profile .profile-dropdown { - right: -1rem; - min-width: 180px; - } -} - -.project-card { - background: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.75rem; - padding: 1.5rem; - transition: all 0.3s ease; - cursor: pointer; -} - -.project-card:hover { - background: #f1f5f9; - border-color: #cbd5e1; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.project-card.inactive { - opacity: 0.8; - background: #f8f9fa; - border-color: #e9ecef; - border-left: 4px solid #ef4444; - position: relative; -} - -.project-card.inactive:hover { - background: #f1f3f4; - border-color: #dee2e6; -} - -/* 비활성화 오버레이 */ -.inactive-overlay { - position: absolute; - top: 0; - right: 0; - z-index: 10; -} - -.inactive-badge { - background: linear-gradient(135deg, #ef4444, #dc2626); - color: white; - padding: 0.25rem 0.75rem; - border-radius: 0 0.75rem 0 0.5rem; - font-size: 0.75rem; - font-weight: 600; - box-shadow: 0 2px 4px rgba(239, 68, 68, 0.3); -} - -/* 비활성 라벨 */ -.inactive-label { - color: #ef4444; - font-size: 0.8rem; - font-weight: 600; - margin-left: 0.5rem; - background: rgba(239, 68, 68, 0.1); - padding: 0.125rem 0.5rem; - border-radius: 0.25rem; -} - -/* 비활성 안내 */ -.inactive-notice { - color: #f59e0b; - font-size: 0.75rem; - font-weight: 500; - background: rgba(245, 158, 11, 0.1); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - border: 1px solid rgba(245, 158, 11, 0.2); - display: inline-block; - margin-top: 0.25rem; -} - -.project-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1rem; -} - -.project-info { - flex: 1; -} - -.project-job-no { - font-size: 0.75rem; - color: #6b7280; - font-weight: 500; - margin-bottom: 0.25rem; -} - -.project-name { - font-size: 1rem; - font-weight: 600; - color: #1f2937; - margin: 0 0 0.5rem 0; - line-height: 1.4; -} - -.project-meta { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.75rem; - color: #6b7280; -} - -.project-actions { - display: flex; - gap: 0.5rem; -} - -.btn-edit, .btn-delete { - padding: 0.375rem 0.75rem; - border: none; - border-radius: 0.375rem; - font-size: 0.75rem; - font-weight: 500; - cursor: pointer; - transition: all 0.3s ease; -} - -.btn-edit { - background: #3b82f6; - color: white; -} - -.btn-edit:hover { - background: #2563eb; -} - -.btn-delete { - background: #ef4444; - color: white; -} - -.btn-delete:hover { - background: #dc2626; -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: 3rem 2rem; - color: #6b7280; -} - -.empty-state .empty-icon { - font-size: 4rem; - margin-bottom: 1rem; - opacity: 0.5; -} - -.empty-state h3 { - margin: 0 0 0.5rem 0; - color: #374151; - font-size: 1.25rem; -} - -.empty-state p { - margin: 0 0 1.5rem 0; - font-size: 0.875rem; -} - -/* 모달 스타일 */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - padding: 1rem; -} - -.modal-container { - background: white; - border-radius: 1rem; - max-width: 600px; - width: 100%; - max-height: 90vh; - overflow-y: auto; - box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem; - border-bottom: 1px solid #e5e7eb; -} - -.modal-header h2 { - margin: 0; - font-size: 1.25rem; - font-weight: 600; - color: #1f2937; -} - -.modal-close-btn { - background: none; - border: none; - font-size: 1.5rem; - color: #6b7280; - cursor: pointer; - padding: 0.25rem; - border-radius: 0.25rem; - transition: all 0.3s ease; -} - -.modal-close-btn:hover { - background: #f3f4f6; - color: #374151; -} - -.modal-body { - padding: 1.5rem; -} - -.modal-footer { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1.5rem; - border-top: 1px solid #e5e7eb; - gap: 1rem; -} - -/* 폼 스타일 */ -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - margin-bottom: 1rem; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.form-label { - font-size: 0.875rem; - font-weight: 500; - color: #374151; -} - -.form-control { - padding: 0.75rem; - border: 1px solid #d1d5db; - border-radius: 0.5rem; - font-size: 0.875rem; - transition: all 0.3s ease; -} - -.form-control:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -/* 반응형 디자인 */ -@media (max-width: 1024px) { - .dashboard-header { - padding: 1rem; - } - - .dashboard-main { - padding: 1.5rem; - } - - .page-header { - flex-direction: column; - align-items: flex-start; - gap: 1rem; - } - - .projects-grid { - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - } -} - -@media (max-width: 768px) { - .header-center { - display: none; - } - - .company-info { - display: none; - } - - .page-title { - font-size: 2rem; - } - - .page-actions { - flex-direction: column; - } - - .search-bar { - flex-direction: column; - } - - .filter-options { - flex-direction: column; - } - - .projects-grid { - grid-template-columns: 1fr; - } - - .form-row { - grid-template-columns: 1fr; - } - - .project-stats { - flex-direction: column; - gap: 0.75rem; - align-items: stretch; - } - - .stat-item { - justify-content: center; - } -} - -@media (max-width: 480px) { - .dashboard-header { - padding: 0.75rem; - } - - .dashboard-main { - padding: 1rem; - } - - .page-title { - font-size: 1.75rem; - } - - .search-section, - .projects-section { - padding: 1rem; - } - - .modal-container { - margin: 0.5rem; - max-height: 95vh; - } -} - -/* 작업자 상태 토글 버튼 스타일 */ -.btn-toggle { - background: none; - border: 2px solid; - border-radius: 50%; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - font-size: 1rem; - transition: all 0.2s ease; - margin-right: 0.25rem; -} - -.btn-deactivate { - border-color: #ef4444; - color: #ef4444; - background: rgba(239, 68, 68, 0.1); -} - -.btn-deactivate:hover { - background: #ef4444; - color: white; - transform: scale(1.05); -} - -.btn-activate { - border-color: #10b981; - color: #10b981; - background: rgba(16, 185, 129, 0.1); -} - -.btn-activate:hover { - background: #10b981; - color: white; - transform: scale(1.05); -} diff --git a/system1-factory/web/css/system-dashboard.css b/system1-factory/web/css/system-dashboard.css deleted file mode 100644 index 9b8a723..0000000 --- a/system1-factory/web/css/system-dashboard.css +++ /dev/null @@ -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,'); - 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); -} diff --git a/system1-factory/web/css/user.css b/system1-factory/web/css/user.css deleted file mode 100644 index c6a4562..0000000 --- a/system1-factory/web/css/user.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/system1-factory/web/css/work-management.css b/system1-factory/web/css/work-management.css deleted file mode 100644 index 5261e5f..0000000 --- a/system1-factory/web/css/work-management.css +++ /dev/null @@ -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; -} diff --git a/system1-factory/web/css/work-review.css b/system1-factory/web/css/work-review.css deleted file mode 100644 index daaec2c..0000000 --- a/system1-factory/web/css/work-review.css +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/system1-factory/web/index.html b/system1-factory/web/index.html index 7b93a9a..af33734 100644 --- a/system1-factory/web/index.html +++ b/system1-factory/web/index.html @@ -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')); } diff --git a/system1-factory/web/js/admin-settings.js b/system1-factory/web/js/admin-settings.js deleted file mode 100644 index 4c87cce..0000000 --- a/system1-factory/web/js/admin-settings.js +++ /dev/null @@ -1,1224 +0,0 @@ -// admin-settings.js - 관리자 설정 페이지 - -// 전역 변수 -let currentUser = null; -let users = []; -let filteredUsers = []; -let currentEditingUser = null; - -// DOM 요소 -const elements = { - // 시간 - timeValue: document.getElementById('timeValue'), - - // 사용자 정보 - userName: document.getElementById('userName'), - userRole: document.getElementById('userRole'), - userInitial: document.getElementById('userInitial'), - - // 검색 및 필터 - userSearch: document.getElementById('userSearch'), - filterButtons: document.querySelectorAll('.filter-btn'), - - // 테이블 - usersTableBody: document.getElementById('usersTableBody'), - emptyState: document.getElementById('emptyState'), - - // 버튼 - addUserBtn: document.getElementById('addUserBtn'), - saveUserBtn: document.getElementById('saveUserBtn'), - confirmDeleteBtn: document.getElementById('confirmDeleteBtn'), - - // 모달 - userModal: document.getElementById('userModal'), - deleteModal: document.getElementById('deleteModal'), - modalTitle: document.getElementById('modalTitle'), - - // 폼 - userForm: document.getElementById('userForm'), - userNameInput: document.getElementById('userName'), - userIdInput: document.getElementById('userId'), - userPasswordInput: document.getElementById('userPassword'), - userRoleSelect: document.getElementById('userRole'), - userEmailInput: document.getElementById('userEmail'), - userPhoneInput: document.getElementById('userPhone'), - passwordGroup: document.getElementById('passwordGroup'), - - // 토스트 - toastContainer: document.getElementById('toastContainer') -}; - -// ========== 초기화 ========== // -document.addEventListener('DOMContentLoaded', async () => { - - try { - await initializePage(); - } catch (error) { - console.error(' 페이지 초기화 오류:', error); - showToast('페이지를 불러오는 중 오류가 발생했습니다.', 'error'); - } -}); - -async function initializePage() { - // 이벤트 리스너 설정 - setupEventListeners(); - - // 사용자 목록 로드 - await loadUsers(); -} - -// ========== 사용자 정보 설정 ========== // -// navbar/sidebar는 app-init.js에서 공통 처리 -function setupUserInfo() { - const authData = getAuthData(); - if (authData && authData.user) { - currentUser = authData.user; - } -} - -function getAuthData() { - const token = localStorage.getItem('sso_token'); - const user = localStorage.getItem('sso_user'); - return { - token, - user: user ? JSON.parse(user) : null - }; -} - -// ========== 시간 업데이트 ========== // -function updateCurrentTime() { - const now = new Date(); - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const seconds = String(now.getSeconds()).padStart(2, '0'); - if (elements.timeValue) { - elements.timeValue.textContent = `${hours}시 ${minutes}분 ${seconds}초`; - } -} - -// ========== 이벤트 리스너 ========== // -function setupEventListeners() { - // 검색 - if (elements.userSearch) { - elements.userSearch.addEventListener('input', handleSearch); - } - - // 필터 버튼 - elements.filterButtons.forEach(btn => { - btn.addEventListener('click', handleFilter); - }); - - // 사용자 추가 버튼 - if (elements.addUserBtn) { - elements.addUserBtn.addEventListener('click', openAddUserModal); - } - - // 사용자 저장 버튼 - if (elements.saveUserBtn) { - elements.saveUserBtn.addEventListener('click', saveUser); - } - - // 삭제 확인 버튼 - if (elements.confirmDeleteBtn) { - elements.confirmDeleteBtn.addEventListener('click', confirmDeleteUser); - } - - // 로그아웃 버튼 - const logoutBtn = document.getElementById('logoutBtn'); - if (logoutBtn) { - logoutBtn.addEventListener('click', handleLogout); - } - - // 프로필 드롭다운 - const userProfile = document.getElementById('userProfile'); - const profileMenu = document.getElementById('profileMenu'); - if (userProfile && profileMenu) { - userProfile.addEventListener('click', (e) => { - e.stopPropagation(); - profileMenu.style.display = profileMenu.style.display === 'block' ? 'none' : 'block'; - }); - - document.addEventListener('click', () => { - profileMenu.style.display = 'none'; - }); - } -} - -// ========== 사용자 관리 ========== // -async function loadUsers() { - try { - - // 실제 API에서 사용자 데이터 가져오기 - const response = await window.apiCall('/users'); - users = Array.isArray(response) ? response : (response.data || []); - - - // 필터링된 사용자 목록 초기화 - filteredUsers = [...users]; - - // 테이블 렌더링 - renderUsersTable(); - - } catch (error) { - console.error(' 사용자 목록 로딩 오류:', error); - showToast('사용자 목록을 불러오는 중 오류가 발생했습니다.', 'error'); - users = []; - filteredUsers = []; - renderUsersTable(); - } -} - -function renderUsersTable() { - if (!elements.usersTableBody) return; - - if (filteredUsers.length === 0) { - elements.usersTableBody.innerHTML = ''; - if (elements.emptyState) { - elements.emptyState.style.display = 'block'; - } - return; - } - - if (elements.emptyState) { - elements.emptyState.style.display = 'none'; - } - - elements.usersTableBody.innerHTML = filteredUsers.map(user => ` - - -
-
${(user.name || user.username).charAt(0)}
-
-

${user.name || user.username}

-

${user.email || '이메일 없음'}

-
-
- - ${user.username} - - - ${getRoleIcon(user.role)} ${getRoleName(user.role)} - - - - - ${user.is_active ? '활성' : '비활성'} - - - ${formatDate(user.last_login) || '로그인 기록 없음'} - -
- - ${user.role !== 'Admin' && user.role !== 'admin' ? ` - - ` : ''} - - - -
- - - `).join(''); -} - -function getRoleIcon(role) { - const icons = { - admin: '👑', - leader: '👨‍💼', - user: '👤' - }; - return icons[role] || '👤'; -} - -function getRoleName(role) { - const names = { - admin: '관리자', - leader: '그룹장', - user: '작업자' - }; - return names[role] || '작업자'; -} - -function formatDate(dateString) { - if (!dateString) return null; - - const date = new Date(dateString); - return date.toLocaleDateString('ko-KR', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); -} - -// ========== 검색 및 필터링 ========== // -function handleSearch(e) { - const searchTerm = e.target.value.toLowerCase(); - - filteredUsers = users.filter(user => { - return (user.name && user.name.toLowerCase().includes(searchTerm)) || - (user.username && user.username.toLowerCase().includes(searchTerm)) || - (user.email && user.email.toLowerCase().includes(searchTerm)); - }); - - renderUsersTable(); -} - -function handleFilter(e) { - const filterType = e.target.dataset.filter; - - // 활성 버튼 변경 - elements.filterButtons.forEach(btn => btn.classList.remove('active')); - e.target.classList.add('active'); - - // 필터링 - if (filterType === 'all') { - filteredUsers = [...users]; - } else { - filteredUsers = users.filter(user => user.role === filterType); - } - - renderUsersTable(); -} - -// ========== 모달 관리 ========== // -function openAddUserModal() { - currentEditingUser = null; - - if (elements.modalTitle) { - elements.modalTitle.textContent = '새 사용자 추가'; - } - - // 폼 초기화 - if (elements.userForm) { - elements.userForm.reset(); - } - - // 비밀번호 필드 표시 - if (elements.passwordGroup) { - elements.passwordGroup.style.display = 'block'; - } - - if (elements.userPasswordInput) { - elements.userPasswordInput.required = true; - } - - // 작업자 연결 섹션 숨기기 (새 사용자 추가 시) - const workerLinkGroup = document.getElementById('workerLinkGroup'); - if (workerLinkGroup) { - workerLinkGroup.style.display = 'none'; - } - - if (elements.userModal) { - elements.userModal.style.display = 'flex'; - } -} - -function editUser(userId) { - const user = users.find(u => u.user_id === userId); - if (!user) return; - - currentEditingUser = user; - - if (elements.modalTitle) { - elements.modalTitle.textContent = '사용자 정보 수정'; - } - - // 역할 이름을 HTML select option value로 변환 - const roleToValueMap = { - 'Admin': 'admin', - 'System Admin': 'admin', - 'User': 'user', - 'Guest': 'user' - }; - - // 폼에 데이터 채우기 - if (elements.userNameInput) elements.userNameInput.value = user.name || ''; - if (elements.userIdInput) elements.userIdInput.value = user.username || ''; - if (elements.userRoleSelect) elements.userRoleSelect.value = roleToValueMap[user.role] || 'user'; - if (elements.userEmailInput) elements.userEmailInput.value = user.email || ''; - if (elements.userPhoneInput) elements.userPhoneInput.value = user.phone || ''; - - // 비밀번호 필드 숨기기 (수정 시에는 선택사항) - if (elements.passwordGroup) { - elements.passwordGroup.style.display = 'none'; - } - - if (elements.userPasswordInput) { - elements.userPasswordInput.required = false; - } - - // 작업자 연결 섹션 표시 (수정 시에만) - const workerLinkGroup = document.getElementById('workerLinkGroup'); - if (workerLinkGroup) { - workerLinkGroup.style.display = 'block'; - updateLinkedWorkerDisplay(user); - } - - if (elements.userModal) { - elements.userModal.style.display = 'flex'; - } -} - -function closeUserModal() { - if (elements.userModal) { - elements.userModal.style.display = 'none'; - } - currentEditingUser = null; -} - -// 영구 삭제 (Hard Delete) -async function permanentDeleteUser(userId, username) { - if (!confirm(`⚠️ 경고: "${username}" 사용자를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!\n관련된 모든 데이터(로그인 기록, 권한 설정 등)도 함께 삭제됩니다.`)) { - return; - } - - // 이중 확인 - if (!confirm(`정말로 "${username}"을(를) 영구 삭제하시겠습니까?\n\n[확인]을 누르면 즉시 삭제됩니다.`)) { - return; - } - - try { - const response = await window.apiCall(`/users/${userId}/permanent`, 'DELETE'); - - if (response.success) { - showToast(`"${username}" 사용자가 영구 삭제되었습니다.`, 'success'); - await loadUsers(); - } else { - throw new Error(response.message || '사용자 삭제에 실패했습니다.'); - } - } catch (error) { - console.error('사용자 영구 삭제 오류:', error); - showToast(`사용자 삭제 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} - -function closeDeleteModal() { - if (elements.deleteModal) { - elements.deleteModal.style.display = 'none'; - } - currentEditingUser = null; -} - -// ========== 비밀번호 초기화 ========== // -async function resetPassword(userId, username) { - if (!confirm(`${username} 사용자의 비밀번호를 000000으로 초기화하시겠습니까?`)) { - return; - } - - try { - const response = await window.apiCall(`/users/${userId}/reset-password`, 'POST'); - - if (response.success) { - showToast(`${username}의 비밀번호가 000000으로 초기화되었습니다.`, 'success'); - } else { - showToast(response.message || '비밀번호 초기화에 실패했습니다.', 'error'); - } - } catch (error) { - console.error('비밀번호 초기화 오류:', error); - showToast('비밀번호 초기화 중 오류가 발생했습니다.', 'error'); - } -} -window.resetPassword = resetPassword; - -// ========== 사용자 CRUD ========== // -async function saveUser() { - try { - const formData = { - name: elements.userNameInput?.value, - username: elements.userIdInput?.value, - role: elements.userRoleSelect?.value, // HTML select value는 이미 'admin' 또는 'user' - email: elements.userEmailInput?.value - }; - - console.log('저장할 데이터:', formData); - - // 유효성 검사 - if (!formData.name || !formData.username || !formData.role) { - showToast('필수 항목을 모두 입력해주세요.', 'error'); - return; - } - - // 비밀번호 처리 - if (!currentEditingUser && elements.userPasswordInput?.value) { - formData.password = elements.userPasswordInput.value; - } else if (currentEditingUser && elements.userPasswordInput?.value) { - formData.password = elements.userPasswordInput.value; - } - - let response; - if (currentEditingUser) { - // 수정 - response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', formData); - } else { - // 생성 - response = await window.apiCall('/users', 'POST', formData); - } - - if (response.success || response.user_id) { - const action = currentEditingUser ? '수정' : '생성'; - showToast(`사용자가 성공적으로 ${action}되었습니다.`, 'success'); - - closeUserModal(); - await loadUsers(); - } else { - throw new Error(response.message || '사용자 저장에 실패했습니다.'); - } - - } catch (error) { - console.error('사용자 저장 오류:', error); - showToast(`사용자 저장 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} - -async function confirmDeleteUser() { - if (!currentEditingUser) return; - - try { - const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'DELETE'); - - if (response.success) { - showToast('사용자가 성공적으로 삭제되었습니다.', 'success'); - closeDeleteModal(); - await loadUsers(); - } else { - throw new Error(response.message || '사용자 삭제에 실패했습니다.'); - } - - } catch (error) { - console.error('사용자 삭제 오류:', error); - showToast(`사용자 삭제 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} - -async function toggleUserStatus(userId) { - try { - const user = users.find(u => u.user_id === userId); - if (!user) return; - - const newStatus = !user.is_active; - const response = await window.apiCall(`/users/${userId}/status`, 'PUT', { is_active: newStatus }); - - if (response.success) { - const action = newStatus ? '활성화' : '비활성화'; - showToast(`사용자가 성공적으로 ${action}되었습니다.`, 'success'); - await loadUsers(); - } else { - throw new Error(response.message || '사용자 상태 변경에 실패했습니다.'); - } - - } catch (error) { - console.error('사용자 상태 변경 오류:', error); - showToast(`사용자 상태 변경 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} - -// ========== 로그아웃 ========== // -function handleLogout() { - if (confirm('로그아웃하시겠습니까?')) { - localStorage.clear(); - if (window.clearSSOAuth) window.clearSSOAuth(); - window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login'; - } -} - -// showToast → api-base.js 전역 사용 - -// ========== 전역 함수 (HTML에서 호출) ========== // -window.editUser = editUser; -window.toggleUserStatus = toggleUserStatus; -window.closeUserModal = closeUserModal; -window.closeDeleteModal = closeDeleteModal; -window.permanentDeleteUser = permanentDeleteUser; - -// ========== 페이지 권한 관리 ========== // -let allPages = []; -let userPageAccess = []; - -// 모든 페이지 목록 로드 -async function loadAllPages() { - try { - const response = await apiCall('/pages'); - allPages = response.data || response || []; - } catch (error) { - console.error(' 페이지 목록 로드 오류:', error); - allPages = []; - } -} - -// 사용자의 페이지 권한 로드 -async function loadUserPageAccess(userId) { - try { - const response = await apiCall(`/users/${userId}/page-access`); - userPageAccess = response.data?.pageAccess || []; - } catch (error) { - console.error(' 사용자 페이지 권한 로드 오류:', error); - userPageAccess = []; - } -} - - -// 페이지 권한 저장 -async function savePageAccess(userId, containerId = null) { - try { - // 특정 컨테이너가 지정되면 그 안에서만 체크박스 선택 - const container = containerId ? document.getElementById(containerId) : document; - const checkboxes = container.querySelectorAll('.page-access-checkbox:not([disabled])'); - - // 중복 page_id 제거 (Map 사용) - const pageAccessMap = new Map(); - checkboxes.forEach(checkbox => { - const pageId = parseInt(checkbox.dataset.pageId); - pageAccessMap.set(pageId, { - page_id: pageId, - can_access: checkbox.checked ? 1 : 0 - }); - }); - - const pageAccessData = Array.from(pageAccessMap.values()); - - - await apiCall(`/users/${userId}/page-access`, 'PUT', { - pageAccess: pageAccessData - }); - - } catch (error) { - console.error(' 페이지 권한 저장 오류:', error); - throw error; - } -} - - - - -// ========== 페이지 권한 관리 모달 ========== // -let currentPageAccessUser = null; - -// 페이지 권한 관리 모달 열기 -async function managePageAccess(userId) { - try { - // 페이지 목록이 없으면 로드 - if (allPages.length === 0) { - await loadAllPages(); - } - - // 사용자 정보 가져오기 - const user = users.find(u => u.user_id === userId); - if (!user) { - showToast('사용자를 찾을 수 없습니다.', 'error'); - return; - } - - currentPageAccessUser = user; - - // 사용자의 페이지 권한 로드 - await loadUserPageAccess(userId); - - // 모달 정보 업데이트 - const userName = user.name || user.username; - document.getElementById('pageAccessModalTitle').textContent = userName + ' - 페이지 권한 관리'; - document.getElementById('pageAccessUserName').textContent = userName; - document.getElementById('pageAccessUserRole').textContent = getRoleName(user.role); - document.getElementById('pageAccessUserAvatar').textContent = userName.charAt(0); - - // 페이지 권한 체크박스 렌더링 - renderPageAccessModalList(); - - // 모달 표시 - document.getElementById('pageAccessModal').style.display = 'flex'; - } catch (error) { - console.error(' 페이지 권한 관리 모달 오류:', error); - showToast('페이지 권한 관리를 열 수 없습니다.', 'error'); - } -} - -// 페이지 권한 모달 닫기 -function closePageAccessModal() { - document.getElementById('pageAccessModal').style.display = 'none'; - currentPageAccessUser = null; -} - -// 페이지 권한 체크박스 렌더링 (모달용) - 폴더 구조 형태 -function renderPageAccessModalList() { - const pageAccessList = document.getElementById('pageAccessModalList'); - if (!pageAccessList) return; - - // 폴더 구조 정의 (page_key 패턴 기준) - const folderStructure = { - 'dashboard': { name: '대시보드', icon: '📊', pages: [] }, - 'work': { name: '작업 관리', icon: '📋', pages: [] }, - 'safety': { name: '안전 관리', icon: '🛡️', pages: [] }, - 'attendance': { name: '근태 관리', icon: '📅', pages: [] }, - 'admin': { name: '시스템 관리', icon: '⚙️', pages: [] }, - 'profile': { name: '내 정보', icon: '👤', pages: [] } - }; - - // 페이지를 폴더별로 분류 - allPages.forEach(page => { - const pageKey = page.page_key || ''; - - if (pageKey === 'dashboard') { - folderStructure['dashboard'].pages.push(page); - } else if (pageKey.startsWith('work.')) { - folderStructure['work'].pages.push(page); - } else if (pageKey.startsWith('safety.')) { - folderStructure['safety'].pages.push(page); - } else if (pageKey.startsWith('attendance.')) { - folderStructure['attendance'].pages.push(page); - } else if (pageKey.startsWith('admin.')) { - folderStructure['admin'].pages.push(page); - } else if (pageKey.startsWith('profile.')) { - folderStructure['profile'].pages.push(page); - } - }); - - // HTML 생성 - 폴더 트리 형태 - let html = '
'; - - Object.keys(folderStructure).forEach(folderKey => { - const folder = folderStructure[folderKey]; - if (folder.pages.length === 0) return; - - const folderId = 'folder-' + folderKey; - - html += '
'; - html += '
'; - html += '' + folder.icon + ''; - html += '' + folder.name + ''; - html += '(' + folder.pages.length + ')'; - html += ''; - html += '
'; - - html += '
'; - - folder.pages.forEach(page => { - // 프로필과 대시보드는 모든 사용자가 접근 가능 - const isAlwaysAccessible = page.page_key === 'dashboard' || page.page_key.startsWith('profile.'); - const isChecked = userPageAccess.find(p => p.page_id === page.id && p.can_access === 1) || isAlwaysAccessible; - - // 파일명만 추출 (page_key에서) - const fileName = page.page_key.split('.').pop() || page.page_key; - - html += '
'; - html += ''; - html += '
'; - }); - - html += '
'; // folder-content - html += '
'; // folder-group - }); - - html += '
'; // folder-tree - - pageAccessList.innerHTML = html; -} - -// 폴더 접기/펼치기 -function toggleFolder(folderId) { - const content = document.getElementById(folderId); - const toggle = document.getElementById('toggle-' + folderId); - - if (content && toggle) { - const isExpanded = content.style.display !== 'none'; - content.style.display = isExpanded ? 'none' : 'block'; - toggle.textContent = isExpanded ? '▶' : '▼'; - } -} -window.toggleFolder = toggleFolder; - -// 페이지 권한 저장 (모달용) -async function savePageAccessFromModal() { - if (!currentPageAccessUser) { - showToast('사용자 정보가 없습니다.', 'error'); - return; - } - - try { - // 모달 컨테이너 지정 - await savePageAccess(currentPageAccessUser.user_id, 'pageAccessModalList'); - showToast('페이지 권한이 저장되었습니다.', 'success'); - - // 캐시 삭제 (사용자가 다시 로그인하거나 페이지 새로고침 필요) - localStorage.removeItem('userPageAccess'); - - closePageAccessModal(); - } catch (error) { - console.error(' 페이지 권한 저장 오류:', error); - showToast('페이지 권한 저장에 실패했습니다.', 'error'); - } -} - -// 전역 함수로 등록 -window.managePageAccess = managePageAccess; -window.closePageAccessModal = closePageAccessModal; - -// 저장 버튼 이벤트 리스너 -document.addEventListener('DOMContentLoaded', () => { - const saveBtn = document.getElementById('savePageAccessBtn'); - if (saveBtn) { - saveBtn.addEventListener('click', savePageAccessFromModal); - } -}); - -// ========== 작업자 연결 기능 ========== // -let departments = []; -let selectedUserId = null; - -// 연결된 작업자 정보 표시 업데이트 -function updateLinkedWorkerDisplay(user) { - const linkedWorkerInfo = document.getElementById('linkedWorkerInfo'); - if (!linkedWorkerInfo) return; - - if (user.user_id && user.worker_name) { - linkedWorkerInfo.innerHTML = ` - - 👤 ${user.worker_name} - ${user.department_name ? `(${user.department_name})` : ''} - - `; - } else { - linkedWorkerInfo.innerHTML = '연결된 작업자 없음'; - } -} - -// 작업자 선택 모달 열기 -async function openWorkerSelectModal() { - if (!currentEditingUser) { - showToast('사용자 정보가 없습니다.', 'error'); - return; - } - - selectedUserId = currentEditingUser.user_id || null; - - // 부서 목록 로드 - await loadDepartmentsForSelect(); - - // 모달 표시 - document.getElementById('workerSelectModal').style.display = 'flex'; -} -window.openWorkerSelectModal = openWorkerSelectModal; - -// 작업자 선택 모달 닫기 -function closeWorkerSelectModal() { - document.getElementById('workerSelectModal').style.display = 'none'; - selectedUserId = null; -} -window.closeWorkerSelectModal = closeWorkerSelectModal; - -// 부서 목록 로드 -async function loadDepartmentsForSelect() { - try { - const response = await window.apiCall('/departments'); - departments = response.data || response || []; - - renderDepartmentList(); - } catch (error) { - console.error('부서 목록 로드 실패:', error); - showToast('부서 목록을 불러오는데 실패했습니다.', 'error'); - } -} - -// 부서 목록 렌더링 -function renderDepartmentList() { - const container = document.getElementById('departmentList'); - if (!container) return; - - if (departments.length === 0) { - container.innerHTML = '
등록된 부서가 없습니다
'; - return; - } - - container.innerHTML = departments.map(dept => ` -
- 📁 - ${dept.department_name} - ${dept.worker_count || 0}명 -
- `).join(''); -} - -// 부서 선택 -async function selectDepartment(departmentId) { - // 활성 상태 업데이트 - document.querySelectorAll('.department-item').forEach(item => { - item.classList.remove('active'); - }); - document.querySelector(`.department-item[data-dept-id="${departmentId}"]`)?.classList.add('active'); - - // 해당 부서의 작업자 목록 로드 - await loadWorkersForSelect(departmentId); -} -window.selectDepartment = selectDepartment; - -// 부서별 작업자 목록 로드 -async function loadWorkersForSelect(departmentId) { - try { - const response = await window.apiCall(`/departments/${departmentId}/workers`); - const workers = response.data || response || []; - - renderWorkerListForSelect(workers); - } catch (error) { - console.error('작업자 목록 로드 실패:', error); - showToast('작업자 목록을 불러오는데 실패했습니다.', 'error'); - } -} - -// 작업자 목록 렌더링 (선택용) -function renderWorkerListForSelect(workers) { - const container = document.getElementById('workerListForSelect'); - if (!container) return; - - if (workers.length === 0) { - container.innerHTML = '
이 부서에 작업자가 없습니다
'; - return; - } - - // 이미 다른 계정에 연결된 작업자 확인을 위해 users 배열 사용 - const linkedUserIds = users - .filter(u => u.user_id && u.user_id !== currentEditingUser?.user_id) - .map(u => u.user_id); - - container.innerHTML = workers.map(worker => { - const isSelected = selectedUserId === worker.user_id; - const isLinkedToOther = linkedUserIds.includes(worker.user_id); - const linkedUser = isLinkedToOther ? users.find(u => u.user_id === worker.user_id) : null; - - return ` -
-
${worker.worker_name.charAt(0)}
-
-
${worker.worker_name}
-
${getJobTypeName(worker.job_type)}
-
- ${isLinkedToOther ? `${linkedUser?.username} 연결됨` : ''} -
${isSelected ? '✓' : ''}
-
- `; - }).join(''); -} - -// 직책 한글 변환 -function getJobTypeName(jobType) { - const names = { - leader: '그룹장', - worker: '작업자', - admin: '관리자' - }; - return names[jobType] || jobType || '-'; -} - -// 작업자 선택 -async function selectWorker(userId, workerName) { - selectedUserId = userId; - - // UI 업데이트 - document.querySelectorAll('.worker-select-item').forEach(item => { - item.classList.remove('selected'); - item.querySelector('.select-indicator').textContent = ''; - }); - - const selectedItem = document.querySelector(`.worker-select-item[onclick*="${userId}"]`); - if (selectedItem) { - selectedItem.classList.add('selected'); - selectedItem.querySelector('.select-indicator').textContent = '✓'; - } - - // 서버에 저장 - try { - const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', { - user_id: userId - }); - - if (response.success) { - // currentEditingUser 업데이트 - currentEditingUser.user_id = userId; - currentEditingUser.worker_name = workerName; - - // 부서 정보도 업데이트 - const dept = departments.find(d => - document.querySelector(`.department-item.active`)?.dataset.deptId == d.department_id - ); - if (dept) { - currentEditingUser.department_name = dept.department_name; - } - - // users 배열 업데이트 - const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id); - if (userIndex !== -1) { - users[userIndex] = { ...users[userIndex], ...currentEditingUser }; - } - - // 표시 업데이트 - updateLinkedWorkerDisplay(currentEditingUser); - - showToast(`${workerName} 작업자가 연결되었습니다.`, 'success'); - closeWorkerSelectModal(); - } else { - throw new Error(response.message || '작업자 연결에 실패했습니다.'); - } - } catch (error) { - console.error('작업자 연결 오류:', error); - showToast(`작업자 연결 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} -window.selectWorker = selectWorker; - -// 작업자 연결 해제 -async function unlinkWorker() { - if (!currentEditingUser) { - showToast('사용자 정보가 없습니다.', 'error'); - return; - } - - if (!currentEditingUser.user_id) { - showToast('연결된 작업자가 없습니다.', 'warning'); - closeWorkerSelectModal(); - return; - } - - if (!confirm('작업자 연결을 해제하시겠습니까?')) { - return; - } - - try { - const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', { - user_id: null - }); - - if (response.success) { - // currentEditingUser 업데이트 - currentEditingUser.user_id = null; - currentEditingUser.worker_name = null; - currentEditingUser.department_name = null; - - // users 배열 업데이트 - const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id); - if (userIndex !== -1) { - users[userIndex] = { ...users[userIndex], user_id: null, worker_name: null, department_name: null }; - } - - // 표시 업데이트 - updateLinkedWorkerDisplay(currentEditingUser); - - showToast('작업자 연결이 해제되었습니다.', 'success'); - closeWorkerSelectModal(); - } else { - throw new Error(response.message || '연결 해제에 실패했습니다.'); - } - } catch (error) { - console.error('작업자 연결 해제 오류:', error); - showToast(`연결 해제 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} -window.unlinkWorker = unlinkWorker; - -// ========== 알림 수신자 관리 ========== // -let notificationRecipients = {}; -let allUsersForRecipient = []; -let currentNotificationType = null; - -const NOTIFICATION_TYPE_CONFIG = { - repair: { name: '설비 수리', icon: '🔧', description: '설비 수리 신청 시 알림을 받을 사용자' }, - safety: { name: '안전 신고', icon: '⚠️', description: '안전 관련 신고 시 알림을 받을 사용자' }, - nonconformity: { name: '부적합 신고', icon: '🚫', description: '부적합 사항 신고 시 알림을 받을 사용자' }, - equipment: { name: '설비 관련', icon: '🔩', description: '설비 관련 알림을 받을 사용자' }, - maintenance: { name: '정기점검', icon: '🛠️', description: '정기점검 알림을 받을 사용자' }, - system: { name: '시스템', icon: '📢', description: '시스템 알림을 받을 사용자' } -}; - -// 알림 수신자 목록 로드 -async function loadNotificationRecipients() { - try { - const response = await window.apiCall('/notification-recipients'); - if (response.success) { - notificationRecipients = response.data || {}; - renderNotificationTypeCards(); - } - } catch (error) { - console.error('알림 수신자 로드 오류:', error); - } -} - -// 알림 유형 카드 렌더링 -function renderNotificationTypeCards() { - const container = document.getElementById('notificationTypeCards'); - if (!container) return; - - let html = ''; - - Object.keys(NOTIFICATION_TYPE_CONFIG).forEach(type => { - const config = NOTIFICATION_TYPE_CONFIG[type]; - const recipients = notificationRecipients[type]?.recipients || []; - - html += ` -
-
-
- ${config.icon} - ${config.name} -
- -
-
- ${recipients.length > 0 - ? recipients.map(r => ` - - 👤 - ${r.user_name || r.username} - - `).join('') - : '지정된 수신자 없음' - } -
-
- `; - }); - - container.innerHTML = html; -} - -// 수신자 편집 모달 열기 -async function openRecipientModal(notificationType) { - currentNotificationType = notificationType; - const config = NOTIFICATION_TYPE_CONFIG[notificationType]; - - // 모달 정보 업데이트 - document.getElementById('recipientModalTitle').textContent = config.name + ' 알림 수신자'; - document.getElementById('recipientModalDesc').textContent = config.description; - - // 사용자 목록 로드 (users가 이미 로드되어 있으면 사용) - if (users.length === 0) { - await loadUsers(); - } - allUsersForRecipient = users.filter(u => u.is_active); - - // 현재 수신자 목록 - const currentRecipients = notificationRecipients[notificationType]?.recipients || []; - const currentRecipientIds = currentRecipients.map(r => r.user_id); - - // 사용자 목록 렌더링 - renderRecipientUserList(currentRecipientIds); - - // 검색 이벤트 - const searchInput = document.getElementById('recipientSearchInput'); - searchInput.value = ''; - searchInput.oninput = (e) => { - renderRecipientUserList(currentRecipientIds, e.target.value); - }; - - // 모달 표시 - document.getElementById('notificationRecipientModal').style.display = 'flex'; -} -window.openRecipientModal = openRecipientModal; - -// 수신자 사용자 목록 렌더링 -function renderRecipientUserList(selectedIds, searchTerm = '') { - const container = document.getElementById('recipientUserList'); - if (!container) return; - - let filteredUsers = allUsersForRecipient; - - if (searchTerm) { - const term = searchTerm.toLowerCase(); - filteredUsers = filteredUsers.filter(u => - (u.name && u.name.toLowerCase().includes(term)) || - (u.username && u.username.toLowerCase().includes(term)) - ); - } - - if (filteredUsers.length === 0) { - container.innerHTML = '
사용자가 없습니다
'; - return; - } - - container.innerHTML = filteredUsers.map(user => { - const isSelected = selectedIds.includes(user.user_id); - return ` -
- -
${(user.name || user.username).charAt(0)}
-
-
${user.name || user.username}
-
${getRoleName(user.role)}
-
-
- `; - }).join(''); -} - -// 수신자 토글 -function toggleRecipientUser(userId, element) { - const checkbox = element.querySelector('input[type="checkbox"]'); - checkbox.checked = !checkbox.checked; - element.classList.toggle('selected', checkbox.checked); -} -window.toggleRecipientUser = toggleRecipientUser; - -// 수신자 모달 닫기 -function closeRecipientModal() { - document.getElementById('notificationRecipientModal').style.display = 'none'; - currentNotificationType = null; -} -window.closeRecipientModal = closeRecipientModal; - -// 알림 수신자 저장 -async function saveNotificationRecipients() { - if (!currentNotificationType) { - showToast('알림 유형이 선택되지 않았습니다.', 'error'); - return; - } - - try { - const checkboxes = document.querySelectorAll('#recipientUserList input[type="checkbox"]:checked'); - const userIds = Array.from(checkboxes).map(cb => parseInt(cb.dataset.userId)); - - const response = await window.apiCall(`/notification-recipients/${currentNotificationType}`, 'PUT', { - user_ids: userIds - }); - - if (response.success) { - showToast('알림 수신자가 저장되었습니다.', 'success'); - closeRecipientModal(); - await loadNotificationRecipients(); - } else { - throw new Error(response.message || '저장에 실패했습니다.'); - } - } catch (error) { - console.error('알림 수신자 저장 오류:', error); - showToast(`저장 중 오류가 발생했습니다: ${error.message}`, 'error'); - } -} -window.saveNotificationRecipients = saveNotificationRecipients; - -// 초기화 시 알림 수신자 로드 -const originalInitializePage = initializePage; -initializePage = async function() { - await originalInitializePage(); - await loadNotificationRecipients(); -}; diff --git a/system1-factory/web/js/admin.js b/system1-factory/web/js/admin.js deleted file mode 100644 index 1a6a5c5..0000000 --- a/system1-factory/web/js/admin.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/system1-factory/web/js/api-helper.js b/system1-factory/web/js/api-helper.js deleted file mode 100644 index b5acf6f..0000000 --- a/system1-factory/web/js/api-helper.js +++ /dev/null @@ -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} - 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} - 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; diff --git a/system1-factory/web/js/app-init.js b/system1-factory/web/js/app-init.js deleted file mode 100644 index caf7b6f..0000000 --- a/system1-factory/web/js/app-init.js +++ /dev/null @@ -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 = '
새 알림이 없습니다.
'; - 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 => ` -
-
${icons[n.type] || '\ud83d\udd14'}
-
-
${escapeHtml(n.title)}
-
${escapeHtml(n.message || '')}
-
-
${formatTimeAgo(n.created_at)}
-
- `).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 }; -})(); diff --git a/system1-factory/web/js/auth-check.js b/system1-factory/web/js/auth-check.js deleted file mode 100644 index 76b145b..0000000 --- a/system1-factory/web/js/auth-check.js +++ /dev/null @@ -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) 제거. -})(); \ No newline at end of file diff --git a/system1-factory/web/js/component-loader.js b/system1-factory/web/js/component-loader.js deleted file mode 100644 index 6d63146..0000000 --- a/system1-factory/web/js/component-loader.js +++ /dev/null @@ -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} 로딩에 실패했습니다. 관리자에게 문의하세요.`; - } -} \ No newline at end of file diff --git a/system1-factory/web/js/load-navbar.js b/system1-factory/web/js/load-navbar.js deleted file mode 100644 index ec8ec1a..0000000 --- a/system1-factory/web/js/load-navbar.js +++ /dev/null @@ -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 = '
새 알림이 없습니다.
'; - return; - } - - const NOTIF_ICONS = { - repair: '🔧', - safety: '⚠️', - system: '📢', - equipment: '🔩', - maintenance: '🛠️' - }; - - list.innerHTML = notifications.slice(0, 5).map(n => ` -
-
- ${NOTIF_ICONS[n.type] || '🔔'} -
-
-
${escapeHtml(n.title)}
-
${escapeHtml(n.message || '')}
-
-
${formatTimeAgo(n.created_at)}
-
- `).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); - } -}); \ No newline at end of file diff --git a/system1-factory/web/js/load-sections.js b/system1-factory/web/js/load-sections.js deleted file mode 100644 index 7c32854..0000000 --- a/system1-factory/web/js/load-sections.js +++ /dev/null @@ -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} 통계 데이터 또는 에러 시 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 = ` -

📝 오늘 등록된 작업: ${stats.today_reports_count}건

-

👥 참여 작업자: ${stats.today_workers_count}명

- `; - } -} - -/** - * 메인 로직: 페이지에 역할별 섹션을 로드하고 내용을 채웁니다. - */ -async function initializeSections() { - const mainContainer = document.querySelector('main[id$="-sections"]'); - if (!mainContainer) { - console.error('섹션을 담을 메인 컨테이너를 찾을 수 없습니다.'); - return; - } - mainContainer.innerHTML = '
콘텐츠를 불러오는 중...
'; - - const currentUser = getUser(); - if (!currentUser) { - mainContainer.innerHTML = '
사용자 정보를 찾을 수 없습니다.
'; - 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); \ No newline at end of file diff --git a/system1-factory/web/js/load-sidebar.js b/system1-factory/web/js/load-sidebar.js deleted file mode 100644 index 0b2e9c8..0000000 --- a/system1-factory/web/js/load-sidebar.js +++ /dev/null @@ -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 }; \ No newline at end of file diff --git a/system1-factory/web/js/manage-user.js b/system1-factory/web/js/manage-user.js deleted file mode 100644 index bd8c97a..0000000 --- a/system1-factory/web/js/manage-user.js +++ /dev/null @@ -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 = '불러오는 중...'; - - 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 = ''; - 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 = '데이터 형식 오류'; - } - } catch (error) { - console.error('Load users error:', error); - tbody.innerHTML = '로드 실패: ' + error.message + ''; - } -} - -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(); -}); \ No newline at end of file diff --git a/system1-factory/web/js/mobile-dashboard.js b/system1-factory/web/js/mobile-dashboard.js index 840bfec..1162c46 100644 --- a/system1-factory/web/js/mobile-dashboard.js +++ b/system1-factory/web/js/mobile-dashboard.js @@ -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 += '
'; @@ -187,14 +167,6 @@ '
'; } - // 방문 - if (visitors) { - html += '
' + - '🚪' + - '방문 ' + visitors.visitCount + '건 · ' + visitors.totalVisitors + '명' + - '
'; - } - // 신고 (미완료만) if (issues && issues.activeCount > 0) { html += '
' + @@ -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 += '
'; } - // 방문 - if (visitors && visitors.requests.length > 0) { - html += '
'; - html += '
▶ 방문
'; - visitors.requests.forEach(function(r) { - var company = r.visitor_company || '업체 미지정'; - var count = parseInt(r.visitor_count) || 0; - var purpose = r.purpose_name || ''; - html += '
'; - html += '
' + escapeHtml(company) + ' · ' + count + '명'; - if (purpose) html += ' · ' + escapeHtml(purpose); - html += '
'; - html += '
'; - }); - html += '
'; - } - // 신고 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); } diff --git a/system1-factory/web/js/navigation.js b/system1-factory/web/js/navigation.js deleted file mode 100644 index 700e0ff..0000000 --- a/system1-factory/web/js/navigation.js +++ /dev/null @@ -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() { ... } \ No newline at end of file diff --git a/system1-factory/web/js/page-access-cache.js b/system1-factory/web/js/page-access-cache.js deleted file mode 100644 index 84f2e9a..0000000 --- a/system1-factory/web/js/page-access-cache.js +++ /dev/null @@ -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} 접근 가능한 페이지 목록 - */ -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} - */ -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} - */ -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; -} diff --git a/system1-factory/web/js/safety-checklist-manage.js b/system1-factory/web/js/safety-checklist-manage.js deleted file mode 100644 index 8554462..0000000 --- a/system1-factory/web/js/safety-checklist-manage.js +++ /dev/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 => - `` - ).join(''); - - if (filterSelect) { - filterSelect.innerHTML = `${options}`; - } - - if (modalSelect) { - modalSelect.innerHTML = options || ''; - } -} - -/** - * 공정 셀렉트 박스 채우기 - */ -function populateWorkTypeSelects() { - const filterSelect = document.getElementById('workTypeFilter'); - const modalSelect = document.getElementById('modalWorkType'); - - const options = workTypes.map(wt => - `` - ).join(''); - - if (filterSelect) { - filterSelect.innerHTML = `${options}`; - } - - if (modalSelect) { - modalSelect.innerHTML = `${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 ` -
-
-
- ${icon} - ${displayTitle} -
- ${items.length}개 -
-
- ${items.map(item => renderChecklistItem(item)).join('')} -
-
- `; -} - -/** - * 체크리스트 항목 렌더링 - */ -function renderChecklistItem(item) { - const requiredBadge = item.is_required - ? '필수' - : '선택'; - - return ` -
-
-
${item.check_item}
-
- ${requiredBadge} - ${item.description ? `${item.description}` : ''} -
-
-
- - -
-
- `; -} - -/** - * 빈 상태 렌더링 - */ -function renderEmptyState(message) { - return ` -
-
📋
-

${message}

-
- `; -} - -/** - * 안내 상태 렌더링 (필터 미선택 시) - */ -function renderGuideState(message) { - return ` -
-
👆
-

${message}

-
- `; -} - -/** - * 날씨 필터 변경 - */ -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 = ''; - } - tasks = []; - renderTaskChecks(); - return; - } - - try { - const response = await apiCall(`/tasks/by-work-type/${workTypeId}`); - if (response && response.success) { - tasks = response.data || []; - taskSelect.innerHTML = '' + - tasks.map(t => ``).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 = ''; - } - return; - } - - try { - const response = await apiCall(`/tasks/by-work-type/${workTypeId}`); - if (response && response.success) { - const modalTasks = response.data || []; - taskSelect.innerHTML = '' + - modalTasks.map(t => ``).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]) => ``) - .join(''); - - return ` -
- - - -
- `; - } - - if (tabType === 'weather') { - return ` -
- - -
- `; - } - - if (tabType === 'task') { - return ` -
- - -
- `; - } - - return ''; -} - -/** - * 인라인 추가 행을 standalone 컨테이너로 감싸기 (빈 상태용) - */ -function renderInlineAddStandalone(tabType) { - return `
${renderInlineAddRow(tabType)}
`; -} - -/** - * 인라인으로 체크 항목 추가 - */ -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; diff --git a/system1-factory/web/js/safety-management.js b/system1-factory/web/js/safety-management.js deleted file mode 100644 index 5a30a8e..0000000 --- a/system1-factory/web/js/safety-management.js +++ /dev/null @@ -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 = ` -
-
📭
-

출입 신청이 없습니다

-

현재 ${getStatusText(currentStatus)} 상태의 신청이 없습니다.

-
- `; - return; - } - - let html = ` - - - - - - - - - - - - - - - - `; - - requests.forEach(req => { - const statusText = { - 'pending': '승인 대기', - 'approved': '승인됨', - 'rejected': '반려됨', - 'training_completed': '교육 완료' - }[req.status] || req.status; - - html += ` - - - - - - - - - - - - `; - }); - - html += ` - -
신청일신청자방문자인원방문 작업장방문 일시목적상태작업
${new Date(req.created_at).toLocaleDateString()}${req.requester_full_name || req.requester_name}${req.visitor_company}${req.visitor_count}명${req.category_name} - ${req.workplace_name}${req.visit_date} ${req.visit_time}${req.purpose_name}${statusText} -
- - ${req.status === 'pending' ? ` - - - ` : ''} - ${req.status === 'approved' ? ` - - ` : ''} -
-
- `; - - 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 = ` -
-
신청 번호
-
#${req.request_id}
- -
신청일
-
${new Date(req.created_at).toLocaleString()}
- -
신청자
-
${req.requester_full_name || req.requester_name}
- -
방문자 소속
-
${req.visitor_company}
- -
방문 인원
-
${req.visitor_count}명
- -
방문 구역
-
${req.category_name}
- -
방문 작업장
-
${req.workplace_name}
- -
방문 날짜
-
${req.visit_date}
- -
방문 시간
-
${req.visit_time}
- -
방문 목적
-
${req.purpose_name}
- -
상태
-
${statusText}
-
- `; - - if (req.notes) { - html += ` -
- 비고:
- ${req.notes} -
- `; - } - - if (req.rejection_reason) { - html += ` -
- 반려 사유:
- ${req.rejection_reason} -
- `; - } - - if (req.approved_by) { - html += ` -
- 처리 정보:
- 처리자: ${req.approver_name || 'Unknown'}
- 처리 시간: ${new Date(req.approved_at).toLocaleString()} -
- `; - } - - 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; diff --git a/system1-factory/web/js/safety-report-list.js b/system1-factory/web/js/safety-report-list.js deleted file mode 100644 index 313a777..0000000 --- a/system1-factory/web/js/safety-report-list.js +++ /dev/null @@ -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 = ` -
-
목록을 불러올 수 없습니다
-

잠시 후 다시 시도해주세요.

-
- `; - } -} - -/** - * 안전신고 목록 렌더링 - */ -function renderIssues(issues) { - if (issues.length === 0) { - issueList.innerHTML = ` -
-
등록된 안전 신고가 없습니다
-

새로운 안전 문제를 신고하려면 '안전 신고' 버튼을 클릭하세요.

-
- `; - 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 ` -
-
- #${safeReportId} - ${STATUS_LABELS[issue.status] || escapeHtml(issue.status || '-')} -
- -
- ${categoryName} - ${title} -
- -
- - - - - - ${reporterName} - - - - - - - - - ${reportDate} - - ${location ? ` - - - - - - ${location} - - ` : ''} - ${assignedName ? ` - - - - - - - - 담당: ${assignedName} - - ` : ''} -
- - ${photos.length > 0 ? ` -
- ${photos.slice(0, 3).map(p => ` - 신고 사진 - `).join('')} - ${photos.length > 3 ? `+${photos.length - 3}` : ''} -
- ` : ''} -
- `; - }).join(''); -} - -/** - * 상세 보기 - */ -function viewIssue(reportId) { - window.location.href = `/pages/safety/issue-detail.html?id=${reportId}&from=safety`; -} diff --git a/system1-factory/web/js/safety-training-conduct.js b/system1-factory/web/js/safety-training-conduct.js deleted file mode 100644 index 746c7a7..0000000 --- a/system1-factory/web/js/safety-training-conduct.js +++ /dev/null @@ -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 = ` -
-
신청 번호
-
#${requestData.request_id}
-
-
-
신청자
-
${requestData.requester_full_name || requestData.requester_name}
-
-
-
방문자 소속
-
${requestData.visitor_company}
-
-
-
방문 인원
-
${requestData.visitor_count}명
-
-
-
방문 작업장
-
${requestData.category_name} - ${requestData.workplace_name}
-
-
-
방문 일시
-
${formattedDate} ${requestData.visit_time}
-
-
-
방문 목적
-
${requestData.purpose_name}
-
- `; - - 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 = '

저장된 서명 목록

'; - - savedSignatures.forEach((sig, index) => { - html += ` -
- 서명 ${index + 1} -
-
방문자 ${index + 1}
-
저장 시간: ${sig.timestamp}
-
-
- -
-
- `; - }); - - 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; diff --git a/system1-factory/web/js/visit-request.js b/system1-factory/web/js/visit-request.js deleted file mode 100644 index 752625d..0000000 --- a/system1-factory/web/js/visit-request.js +++ /dev/null @@ -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 = ''; - - 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 = ''; - - 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 = '

신청 내역이 없습니다

'; - return; - } - - let html = ''; - requests.forEach(req => { - const statusText = { - 'pending': '승인 대기', - 'approved': '승인됨', - 'rejected': '반려됨', - 'training_completed': '교육 완료' - }[req.status] || req.status; - - html += ` -
-
-

${req.visitor_company} (${req.visitor_count}명)

- ${statusText} -
-
-
- 방문 작업장 - ${req.category_name} - ${req.workplace_name} -
-
- 방문 일시 - ${req.visit_date} ${req.visit_time} -
-
- 방문 목적 - ${req.purpose_name} -
-
- 신청일 - ${new Date(req.created_at).toLocaleDateString()} -
-
- ${req.rejection_reason ? `

반려 사유: ${req.rejection_reason}

` : ''} - ${req.notes ? `

비고: ${req.notes}

` : ''} -
- `; - }); - - 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 = ` -
-
${selectedCategory.category_name} - ${region.workplace_name}
- `; - - // 상세 정보 카드 표시 - const infoDiv = document.getElementById('selectedWorkplaceInfo'); - infoDiv.style.display = 'block'; - infoDiv.innerHTML = ` -
-
📍
-
-
${region.workplace_name}
-
${selectedCategory.category_name}
-
- -
- `; - - // 모달 닫기 - closeMapModal(); - - window.showToast(`${region.workplace_name} 작업장이 선택되었습니다.`, 'success'); -} - -/** - * 작업장 선택 초기화 - */ -function clearWorkplaceSelection() { - selectedWorkplace = null; - - const selectionDiv = document.getElementById('workplaceSelection'); - selectionDiv.classList.remove('selected'); - selectionDiv.innerHTML = ` -
📍
-
지도에서 작업장을 선택하세요
- `; - - 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; diff --git a/system1-factory/web/js/worker-management.js b/system1-factory/web/js/worker-management.js deleted file mode 100644 index 686e784..0000000 --- a/system1-factory/web/js/worker-management.js +++ /dev/null @@ -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 = ` -
- 등록된 부서가 없습니다.
- -
- `; - 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 ` -
-
- ${safeDeptName} - ${workerCount}명 -
-
- - -
-
- `; - }).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 = '' + - departments - .filter(d => d.department_id !== parseInt(currentId)) - .map(d => { - const safeDeptId = parseInt(d.department_id) || 0; - return ``; - }) - .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 = ` -
-

부서를 선택해주세요

-

왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.

-
- `; - } - - 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 = ` -
-

작업자가 없습니다

-

"+ 작업자 추가" 버튼을 눌러 작업자를 등록하세요.

-
- `; - return; - } - - const tableHtml = ` - - - - - - - - - - - - - ${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 ` - - - - - - - - - `; - }).join('')} - -
이름직책상태입사일계정관리
-
-
${firstChar}
- ${safeWorkerName} -
-
${jobType}${statusText}${worker.join_date ? formatDate(worker.join_date) : '-'} - - -
- - -
-
- `; - - 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; diff --git a/system1-factory/web/js/workplace-status.js b/system1-factory/web/js/workplace-status.js index 3d468ce..45558c0 100644 --- a/system1-factory/web/js/workplace-status.js +++ b/system1-factory/web/js/workplace-status.js @@ -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 = '

금일 방문 예정 인원이 없습니다.

'; - return; - } - - let html = ''; - visitors.forEach(visitor => { - const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨'; - - html += ` -
-
-

${escapeHtml(visitor.visitor_company)}

- ${parseInt(visitor.visitor_count) || 0}명 • ${statusText} -
-

⏰ ${escapeHtml(visitor.visit_time)}

-

📋 ${escapeHtml(visitor.purpose_name)}

-
- `; - }); - - container.innerHTML = html; -} // 상세 지도 초기화 async function initDetailMap(workplace) { diff --git a/system1-factory/web/nginx.conf b/system1-factory/web/nginx.conf index 490c53d..8e64979 100644 --- a/system1-factory/web/nginx.conf +++ b/system1-factory/web/nginx.conf @@ -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; diff --git a/system1-factory/web/pages/admin/accounts.html b/system1-factory/web/pages/admin/accounts.html deleted file mode 100644 index c0f0bcd..0000000 --- a/system1-factory/web/pages/admin/accounts.html +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - 관리자 설정 | (주)테크니컬코리아 - - - - - - -
- - - - -
-
- - - -
-
-

사용자 계정 관리

- -
- -
-
- -
- - - - -
-
- -
- - - - - - - - - - - - - - -
사용자명아이디역할상태최종 로그인관리
-
- - -
-
- - -
-
-

알림 수신자 설정

-

알림 유형별 수신자를 지정합니다. 지정된 사용자에게만 알림이 전송됩니다.

-
- -
-
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - - -
- - - - - - - - diff --git a/system1-factory/web/pages/admin/attendance-report.html b/system1-factory/web/pages/admin/attendance-report.html index 28a1568..844a3b0 100644 --- a/system1-factory/web/pages/admin/attendance-report.html +++ b/system1-factory/web/pages/admin/attendance-report.html @@ -3,13 +3,10 @@ - 출퇴근-작업보고서 대조 | (주)테크니컬코리아 - - - - - - + 출퇴근-작업보고서 대조 - TK 공장관리 + + + - - - - - -
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
+
+
-
+ + + diff --git a/system1-factory/web/pages/admin/departments.html b/system1-factory/web/pages/admin/departments.html index 9f489b4..cf75ee2 100644 --- a/system1-factory/web/pages/admin/departments.html +++ b/system1-factory/web/pages/admin/departments.html @@ -3,13 +3,10 @@ - 부서 관리 | (주)테크니컬코리아 - - - - - - + 부서 관리 - TK 공장관리 + + + - - - -
-
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-
-
+ + + + + + diff --git a/system1-factory/web/pages/admin/equipment-detail.html b/system1-factory/web/pages/admin/equipment-detail.html index 6bd998a..c881a10 100644 --- a/system1-factory/web/pages/admin/equipment-detail.html +++ b/system1-factory/web/pages/admin/equipment-detail.html @@ -3,23 +3,34 @@ - 설비 상세 | (주)테크니컬코리아 - - + 설비 상세 - TK 공장관리 + + + - - - - - - - - -
- -
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-
-
+ + + + + + diff --git a/system1-factory/web/pages/admin/equipments.html b/system1-factory/web/pages/admin/equipments.html index 6bd788b..8ce01fc 100644 --- a/system1-factory/web/pages/admin/equipments.html +++ b/system1-factory/web/pages/admin/equipments.html @@ -3,23 +3,34 @@ - 설비 관리 | (주)테크니컬코리아 - - + 설비 관리 - TK 공장관리 + + + - - - - - - - - -
- -
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
+ + + + + + diff --git a/system1-factory/web/pages/admin/issue-categories.html b/system1-factory/web/pages/admin/issue-categories.html index bdd3a2b..886cdba 100644 --- a/system1-factory/web/pages/admin/issue-categories.html +++ b/system1-factory/web/pages/admin/issue-categories.html @@ -3,13 +3,10 @@ - 신고 카테고리 관리 | (주)테크니컬코리아 - - - - - - + 신고 카테고리 관리 - TK 공장관리 + + + - - - -
-
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
+ + + + + + diff --git a/system1-factory/web/pages/admin/notifications.html b/system1-factory/web/pages/admin/notifications.html index b47db6f..be29eeb 100644 --- a/system1-factory/web/pages/admin/notifications.html +++ b/system1-factory/web/pages/admin/notifications.html @@ -3,18 +3,14 @@ - 알림 관리 | (주)테크니컬코리아 - - - - - - + 알림 관리 - TK 공장관리 + + + - - - - -
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +

알림 관리

@@ -358,9 +371,12 @@
-
+ + + - + + + diff --git a/system1-factory/web/pages/admin/projects.html b/system1-factory/web/pages/admin/projects.html index eb36f68..189df7f 100644 --- a/system1-factory/web/pages/admin/projects.html +++ b/system1-factory/web/pages/admin/projects.html @@ -3,13 +3,12 @@ - 프로젝트 관리 | (주)테크니컬코리아 - - - + 프로젝트 관리 - TK 공장관리 + + + - - - - -
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
+ + + + - + diff --git a/system1-factory/web/pages/admin/repair-management.html b/system1-factory/web/pages/admin/repair-management.html index 87db8a5..98c072b 100644 --- a/system1-factory/web/pages/admin/repair-management.html +++ b/system1-factory/web/pages/admin/repair-management.html @@ -3,17 +3,14 @@ - 시설설비 관리 | (주)테크니컬코리아 - - - - - + 시설설비 관리 - TK 공장관리 + + + - - - - -
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
+ + + + + + diff --git a/system1-factory/web/pages/admin/tasks.html b/system1-factory/web/pages/admin/tasks.html index caf2252..5ca27cf 100644 --- a/system1-factory/web/pages/admin/tasks.html +++ b/system1-factory/web/pages/admin/tasks.html @@ -3,14 +3,12 @@ - 작업 관리 | (주)테크니컬코리아 - - - - - + 작업 관리 - TK 공장관리 + + + - - - - -
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
+ + + + + + diff --git a/system1-factory/web/pages/admin/workers.html b/system1-factory/web/pages/admin/workers.html deleted file mode 100644 index e6a9275..0000000 --- a/system1-factory/web/pages/admin/workers.html +++ /dev/null @@ -1,495 +0,0 @@ - - - - - - 작업자 관리 | (주)테크니컬코리아 - - - - - - - - - - - - - - - -
-
-
- - - -
- -
-
-

부서 목록

- -
-
- -
-
- - -
-
-

부서를 선택하세요

- -
- -
-
-

부서를 선택해주세요

-

왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.

-
-
-
-
-
-
-
- - - - - - - - - - - diff --git a/system1-factory/web/pages/admin/workplaces.html b/system1-factory/web/pages/admin/workplaces.html index c207938..4784246 100644 --- a/system1-factory/web/pages/admin/workplaces.html +++ b/system1-factory/web/pages/admin/workplaces.html @@ -3,22 +3,34 @@ - 작업장 관리 | (주)테크니컬코리아 - - + 작업장 관리 - TK 공장관리 + + + - - - - - - - - - -
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
@@ -128,8 +140,9 @@
-
-
+ + + + + @@ -425,5 +440,6 @@ + diff --git a/system1-factory/web/pages/attendance/annual-overview.html b/system1-factory/web/pages/attendance/annual-overview.html index a29e851..bb16840 100644 --- a/system1-factory/web/pages/attendance/annual-overview.html +++ b/system1-factory/web/pages/attendance/annual-overview.html @@ -3,12 +3,10 @@ - 연간 연차 현황 | 테크니컬코리아 - - - - - + 연간 연차 현황 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-
+
-
+ + + diff --git a/system1-factory/web/pages/attendance/checkin.html b/system1-factory/web/pages/attendance/checkin.html index 1d40db0..e3d3ccb 100644 --- a/system1-factory/web/pages/attendance/checkin.html +++ b/system1-factory/web/pages/attendance/checkin.html @@ -3,14 +3,10 @@ - 출근 체크 | (주)테크니컬코리아 - - - - - - - + 출근 체크 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-

출근 체크

클릭하여 출근/결근 상태를 변경하세요

@@ -204,12 +217,14 @@
-
- +
+
+
+ + + - - - -
- + diff --git a/system1-factory/web/pages/attendance/daily.html b/system1-factory/web/pages/attendance/daily.html index 57d4b48..40c4293 100644 --- a/system1-factory/web/pages/attendance/daily.html +++ b/system1-factory/web/pages/attendance/daily.html @@ -3,21 +3,34 @@ - 일일 출퇴근 입력 | (주)테크니컬코리아 - - - - - - + 일일 출퇴근 입력 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
- -
-
-
- +
+
+ + + + - + diff --git a/system1-factory/web/pages/attendance/monthly.html b/system1-factory/web/pages/attendance/monthly.html index 576e4f5..44bdfdc 100644 --- a/system1-factory/web/pages/attendance/monthly.html +++ b/system1-factory/web/pages/attendance/monthly.html @@ -3,13 +3,10 @@ - 월별 출근부 | (주)테크니컬코리아 - - - - - - + 월별 출근부 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
- -
-
+ +
+
- + + - + diff --git a/system1-factory/web/pages/attendance/my-vacation-info.html b/system1-factory/web/pages/attendance/my-vacation-info.html index 105df28..2e9d280 100644 --- a/system1-factory/web/pages/attendance/my-vacation-info.html +++ b/system1-factory/web/pages/attendance/my-vacation-info.html @@ -3,12 +3,10 @@ - 내 연차 정보 | 테크니컬코리아 - - - - - + 내 연차 정보 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-
-
- +
+
+ + + + + diff --git a/system1-factory/web/pages/attendance/vacation-allocation.html b/system1-factory/web/pages/attendance/vacation-allocation.html index 2edb317..2600c5b 100644 --- a/system1-factory/web/pages/attendance/vacation-allocation.html +++ b/system1-factory/web/pages/attendance/vacation-allocation.html @@ -4,30 +4,35 @@ - 휴가 발생 입력 | 테크니컬코리아 - - - + 휴가 발생 입력 - TK 공장관리 + + + - - - - - - - - - -
- - - - - -
-
+ +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-
- -
+ + +
@@ -349,6 +353,10 @@ + + + + diff --git a/system1-factory/web/pages/attendance/vacation-approval.html b/system1-factory/web/pages/attendance/vacation-approval.html index 173645d..e58ceb3 100644 --- a/system1-factory/web/pages/attendance/vacation-approval.html +++ b/system1-factory/web/pages/attendance/vacation-approval.html @@ -3,13 +3,10 @@ - 휴가 승인 관리 | (주)테크니컬코리아 - - - - - - + 휴가 승인 관리 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
- -
-
-
- +
+
+ + + + - + diff --git a/system1-factory/web/pages/attendance/vacation-input.html b/system1-factory/web/pages/attendance/vacation-input.html index c9a2f03..45a648f 100644 --- a/system1-factory/web/pages/attendance/vacation-input.html +++ b/system1-factory/web/pages/attendance/vacation-input.html @@ -3,21 +3,34 @@ - 휴가 직접 입력 | (주)테크니컬코리아 - - - - - - + 휴가 직접 입력 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
- -
-
-
- +
+
+ + + + - + diff --git a/system1-factory/web/pages/attendance/vacation-management.html b/system1-factory/web/pages/attendance/vacation-management.html index d38afef..b03a0d4 100644 --- a/system1-factory/web/pages/attendance/vacation-management.html +++ b/system1-factory/web/pages/attendance/vacation-management.html @@ -3,13 +3,10 @@ - 휴가 관리 | (주)테크니컬코리아 - - - - - - + 휴가 관리 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
- -
-
-
- +
+
+ + + + - + diff --git a/system1-factory/web/pages/attendance/vacation-request.html b/system1-factory/web/pages/attendance/vacation-request.html index 56459e8..b83181c 100644 --- a/system1-factory/web/pages/attendance/vacation-request.html +++ b/system1-factory/web/pages/attendance/vacation-request.html @@ -3,21 +3,34 @@ - 휴가 신청 | (주)테크니컬코리아 - - - - - - + 휴가 신청 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
- -
-
-
- +
+
+ + + + - + diff --git a/system1-factory/web/pages/attendance/work-status.html b/system1-factory/web/pages/attendance/work-status.html index b167d35..01de510 100644 --- a/system1-factory/web/pages/attendance/work-status.html +++ b/system1-factory/web/pages/attendance/work-status.html @@ -3,13 +3,10 @@ - 근무 현황 | (주)테크니컬코리아 - - - - - - + 근무 현황 - TK 공장관리 + + + - - - + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ +
+
+ +
-
-
-
+
+
+
+ + + - - -
- + diff --git a/system1-factory/web/pages/dashboard-new.html b/system1-factory/web/pages/dashboard-new.html new file mode 100644 index 0000000..5a9c8b8 --- /dev/null +++ b/system1-factory/web/pages/dashboard-new.html @@ -0,0 +1,144 @@ + + + + + + 대시보드 - TK 공장관리 + + + + + +
+
+
+
+ + +

TK 공장관리

+
+
+ +
-
+ +
+
+
+
+ + + + +
+
+ + + +
+ +
+
+

대시보드

+

-

+
+ +
+ + +
+
+
-
+
금일 TBM
+
+
+
-
+
출근 인원
+
+
+
-
+
수리 요청
+
+
+
-
+
미확인 알림
+
+
+ +
+ +
+

+ 금일 TBM +

+
+

로딩 중...

+
+
+ + +
+

+ 최근 알림 +

+
+

로딩 중...

+
+
+ + +
+

+ 수리 요청 현황 +

+
+

로딩 중...

+
+
+ + + +
+
+
+
+ + + + + diff --git a/system1-factory/web/pages/dashboard.html b/system1-factory/web/pages/dashboard.html index cd00541..3940a68 100644 --- a/system1-factory/web/pages/dashboard.html +++ b/system1-factory/web/pages/dashboard.html @@ -1,546 +1,334 @@ - - - - 작업 현황판 | 테크니컬코리아 - - - - - - - - - - - - - - - - - - - - - - + + + 작업장 현황 - TK 공장관리 + + + + - - - -
- - - - - -
- - - - - -
-
-
-
-

작업장 현황

-
- - -
-
-
-
- -
- - -
-
-
-
-

🚚 임시 이동된 설비

- -
-
-
-
- -
- -
-
-
- -
- - - - -
- - -
- - -
-
- -
-
-

-

-
- -
- - -
- -
- - - - - -
- - -
- -
- -
-
-
👷
-
- 0 - 작업자 +
+ +
-
+
-
-
-
🚪
-
- 0 - 방문자 +
+
+ + + + + +
+
+ + + +
+ + + + +
+
+
+
+

작업장 현황

+
+ + +
+
+
+
+ +
+
🏭
+

공장을 선택하세요

+

위에서 공장을 선택하면 작업장 현황을 확인할 수 있습니다.

+
+
+
+
+ + +
+
+
+
+

🚚 임시 이동된 설비

+ +
+
+
+
+ +
+
+
+
+
+
+ + +
+ + +
+
+
+
+

+

-
-
-
📋
-
- 0 - 작업 수 + +
+
+
+ + + + +
-
-
- - -
-

- 🔧 - 진행 중인 작업 -

-
-

진행 중인 작업이 없습니다.

-
-
- - -
-

- ⚙️ - 설비 현황 -

-
-

설비 정보를 불러오는 중...

-
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- 🗺️ -

상세 지도를 불러오는 중...

-
-
-
-
- - -
-
- -
-

- 📥 - 이 작업장으로 이동해 온 설비 -

-
-

없음

+
+
+
+
+
👷
+
0작업자
+
+
+
🚪
+
0방문자
+
+
+
📋
+
0작업 수
+
+
+
+

🔧 진행 중인 작업

+

진행 중인 작업이 없습니다.

+
+
+

⚙️ 설비 현황

+

설비 정보를 불러오는 중...

+
+
+
+
+
+
+
🗺️

상세 지도를 불러오는 중...

+
+
+
+
+
+
+

📥 이 작업장으로 이동해 온 설비

+

없음

+
+
+

📤 다른 곳으로 이동한 설비

+

없음

+
+
+
-
- - -
-

- 📤 - 다른 곳으로 이동한 설비 -

-
-

없음

+
+ + +
+
+ +
+

+ +
+
+
+
+
+

설비 사진

+
등록된 사진이 없습니다
+
+
+ + + +
+

수리 이력

수리 이력이 없습니다
+

외부반출 이력

외부반출 이력이 없습니다
-
-
-
+
- - - - -
-
- -
-

- -
-
- -
- -
-
-
- - -
-
-

설비 사진

- + + - -
- - - -
- - -
-

수리 이력

-
-
수리 이력이 없습니다
+ +
-
- -