From 13b09ef2ae4fddbe4c32c84ba712489a7c16b8ad Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 20 Oct 2025 13:31:39 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20=EC=B4=88=EA=B8=B0=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 기능: - 기간제 근로자 작업관리 시스템 기본 구조 - 한국어 기반 프론트엔드 (로그인, 대시보드, 작업자 관리) - Node.js Express 백엔드 API 서버 구조 - MySQL 데이터베이스 스키마 설계 - 14000번대 포트 구성으로 충돌 방지 📁 구조: - frontend/ : HTML, CSS, JS (Bootstrap 5) - backend/ : Node.js, Express, MySQL - database/ : 초기화 스크립트 - docs/ : 문서 🔌 포트: - 웹: 14000, API: 14001, DB: 14002, phpMyAdmin: 14003 🎯 다음 단계: 백엔드 API 라우트 구현 및 Docker 설정 --- .gitignore | 78 ++++++++++ README.md | 94 ++++++++++++ backend/models/database.js | 101 +++++++++++++ backend/package.json | 37 +++++ backend/server.js | 102 +++++++++++++ database/init.sql | 124 ++++++++++++++++ frontend/css/dashboard.css | 292 +++++++++++++++++++++++++++++++++++++ frontend/dashboard.html | 216 +++++++++++++++++++++++++++ frontend/index.html | 140 ++++++++++++++++++ frontend/js/auth.js | 266 +++++++++++++++++++++++++++++++++ frontend/workers.html | 224 ++++++++++++++++++++++++++++ 11 files changed, 1674 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/models/database.js create mode 100644 backend/package.json create mode 100644 backend/server.js create mode 100644 database/init.sql create mode 100644 frontend/css/dashboard.css create mode 100644 frontend/dashboard.html create mode 100644 frontend/index.html create mode 100644 frontend/js/auth.js create mode 100644 frontend/workers.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eb437c --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +config.env +*.env + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Docker +.dockerignore + +# Database +*.sql.backup +*.db +*.sqlite +*.sqlite3 + +# Uploads +uploads/ +temp/ + +# Build +dist/ +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a58448 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# 기간제 근로자 작업관리 시스템 + +## 📋 프로젝트 개요 +기간제 근로자의 일일 작업, 에러사항, 요청사항을 효율적으로 관리하는 웹 기반 시스템 + +## 🎯 주요 기능 +- **인증 시스템**: Admin/User 권한 분리 +- **작업자 관리**: 용접사/배관사 직종별 관리 +- **일일 작업관리**: 당일 작업 기록 및 조회 +- **에러사항 관리**: 간편한 에러 등록 및 추적 +- **요청사항 관리**: 장비/소모품 요청 관리 + +## 🛠️ 기술 스택 +- **Backend**: Node.js + Express + MySQL +- **Frontend**: Vanilla JS + Bootstrap 5 +- **Database**: MySQL 8.0 +- **배포**: Docker + Docker Compose + +## 🔌 포트 구성 (14000번대) +| 서비스 | 포트 | 접속 URL | 용도 | +|--------|------|----------|------| +| **웹 인터페이스** | `14000` | http://localhost:14000 | 프론트엔드 (HTML/CSS/JS) | +| **API 서버** | `14001` | http://localhost:14001/api | 백엔드 REST API | +| **MySQL DB** | `14002` | localhost:14002 | 데이터베이스 연결 | +| **phpMyAdmin** | `14003` | http://localhost:14003 | DB 관리 웹 도구 | + +> **💡 포트 선택 이유**: 14000번대를 사용하여 다른 서비스와의 포트 충돌을 방지 + +## 📂 프로젝트 구조 +``` +Worker-Management-System/ +├── backend/ # Node.js API 서버 +├── frontend/ # 웹 클라이언트 +├── database/ # DB 초기화 스크립트 +├── docs/ # 문서 +├── docker-compose.yml # Docker 설정 +└── README.md +``` + +## 🚀 실행 방법 +```bash +# 프로젝트 실행 +docker-compose up -d + +# 로그 확인 +docker-compose logs -f + +# 종료 +docker-compose down +``` + +## 🔧 개발 환경 실행 +```bash +# 백엔드 개발 서버 (포트 14001) +cd backend +npm install +npm run dev + +# 프론트엔드 (포트 14000) +# 웹 서버 또는 Live Server로 frontend 폴더 실행 +``` + +## 📊 데이터베이스 스키마 +- `users`: 사용자 계정 (admin/user) +- `workers`: 작업자 정보 (용접사/배관사) +- `daily_work`: 일일 작업 기록 +- `error_reports`: 에러사항 기록 +- `requests`: 요청사항 기록 + +## 🔐 기본 계정 +- **Admin**: admin / admin123 +- **User**: user / user123 + +## 📱 서비스 접속 방법 + +### 🌐 웹 브라우저 접속 +- **메인 웹사이트**: http://localhost:14000 +- **API 문서**: http://localhost:14001 (서버 상태 확인) +- **데이터베이스 관리**: http://localhost:14003 (phpMyAdmin) + +### 🔗 API 엔드포인트 +- **기본 URL**: http://localhost:14001/api +- **로그인**: POST /api/auth/login +- **작업자 목록**: GET /api/workers +- **일일 작업**: GET /api/daily-work +- **에러사항**: GET /api/errors +- **요청사항**: GET /api/requests + +### 🗄️ 데이터베이스 연결 정보 +- **호스트**: localhost +- **포트**: 14002 +- **데이터베이스명**: worker_management +- **사용자명**: root +- **비밀번호**: rootpassword diff --git a/backend/models/database.js b/backend/models/database.js new file mode 100644 index 0000000..fce6817 --- /dev/null +++ b/backend/models/database.js @@ -0,0 +1,101 @@ +const mysql = require('mysql2/promise'); + +// 데이터베이스 연결 풀 생성 +const pool = mysql.createPool({ + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'rootpassword', + database: process.env.DB_NAME || 'worker_management', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + acquireTimeout: 60000, + timeout: 60000, + reconnect: true, + charset: 'utf8mb4' +}); + +// 데이터베이스 연결 테스트 +async function testConnection() { + try { + const connection = await pool.getConnection(); + console.log('✅ 데이터베이스 연결 성공'); + connection.release(); + return true; + } catch (error) { + console.error('❌ 데이터베이스 연결 실패:', error.message); + return false; + } +} + +// 쿼리 실행 함수 +async function executeQuery(sql, params = []) { + try { + const [rows] = await pool.execute(sql, params); + return rows; + } catch (error) { + console.error('쿼리 실행 오류:', error); + throw error; + } +} + +// 트랜잭션 실행 함수 +async function executeTransaction(queries) { + const connection = await pool.getConnection(); + + try { + await connection.beginTransaction(); + + const results = []; + for (const { sql, params } of queries) { + const [result] = await connection.execute(sql, params); + results.push(result); + } + + await connection.commit(); + return results; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +// 페이지네이션 쿼리 함수 +async function executePagedQuery(sql, params = [], page = 1, limit = 10) { + const offset = (page - 1) * limit; + + // 전체 개수 조회 + const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`; + const [countResult] = await pool.execute(countSql, params); + const total = countResult[0].total; + + // 페이지 데이터 조회 + const pagedSql = `${sql} LIMIT ? OFFSET ?`; + const [rows] = await pool.execute(pagedSql, [...params, limit, offset]); + + return { + data: rows, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit), + hasNext: page < Math.ceil(total / limit), + hasPrev: page > 1 + } + }; +} + +// 데이터베이스 초기화 시 연결 테스트 +testConnection(); + +module.exports = { + pool, + executeQuery, + executeTransaction, + executePagedQuery, + testConnection +}; diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..c97cbe2 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,37 @@ +{ + "name": "worker-management-backend", + "version": "1.0.0", + "description": "기간제 근로자 작업관리 시스템 백엔드 API", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "worker", + "management", + "api", + "express", + "mysql" + ], + "author": "Hyungi", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.0", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "helmet": "^7.0.0", + "express-rate-limit": "^6.10.0", + "joi": "^17.9.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=16.0.0" + } +} diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..1fdd25c --- /dev/null +++ b/backend/server.js @@ -0,0 +1,102 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +require('dotenv').config({ path: './config.env' }); + +const authRoutes = require('./routes/auth'); +const workerRoutes = require('./routes/workers'); +const dailyWorkRoutes = require('./routes/dailyWork'); +const errorRoutes = require('./routes/errors'); +const requestRoutes = require('./routes/requests'); +const dashboardRoutes = require('./routes/dashboard'); + +const app = express(); +const PORT = process.env.PORT || 14001; + +// 보안 미들웨어 +app.use(helmet()); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15분 + max: 100, // 최대 100개 요청 + message: { + error: '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.' + } +}); +app.use('/api/', limiter); + +// CORS 설정 +app.use(cors({ + origin: process.env.CORS_ORIGIN || 'http://localhost:3000', + credentials: true +})); + +// Body parser +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// 로깅 미들웨어 +app.use((req, res, next) => { + console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); + next(); +}); + +// API 라우트 +app.use('/api/auth', authRoutes); +app.use('/api/workers', workerRoutes); +app.use('/api/daily-work', dailyWorkRoutes); +app.use('/api/errors', errorRoutes); +app.use('/api/requests', requestRoutes); +app.use('/api/dashboard', dashboardRoutes); + +// 기본 라우트 +app.get('/', (req, res) => { + res.json({ + message: '기간제 근로자 작업관리 시스템 API', + version: '1.0.0', + status: 'running', + timestamp: new Date().toISOString() + }); +}); + +// 404 에러 처리 +app.use((req, res) => { + res.status(404).json({ + error: '요청한 리소스를 찾을 수 없습니다.', + path: req.path, + method: req.method + }); +}); + +// 전역 에러 처리 +app.use((err, req, res, next) => { + console.error('서버 오류:', err); + + res.status(err.status || 500).json({ + error: process.env.NODE_ENV === 'production' + ? '서버 내부 오류가 발생했습니다.' + : err.message, + ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) + }); +}); + +// 서버 시작 +app.listen(PORT, () => { + console.log(`🚀 서버가 포트 ${PORT}에서 실행 중입니다.`); + console.log(`📊 대시보드: http://localhost:${PORT}`); + console.log(`🔧 환경: ${process.env.NODE_ENV}`); + console.log(`🗄️ 데이터베이스: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('🛑 서버 종료 신호를 받았습니다.'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('🛑 서버를 종료합니다.'); + process.exit(0); +}); diff --git a/database/init.sql b/database/init.sql new file mode 100644 index 0000000..bae4102 --- /dev/null +++ b/database/init.sql @@ -0,0 +1,124 @@ +-- 기간제 근로자 작업관리 시스템 DB 초기화 스크립트 + +CREATE DATABASE IF NOT EXISTS worker_management; +USE worker_management; + +-- 사용자 테이블 (admin/user 권한) +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + role ENUM('admin', 'user') DEFAULT 'user', + name VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- 작업자 테이블 (용접사/배관사) +CREATE TABLE workers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + job_type ENUM('welder', 'plumber') NOT NULL COMMENT '용접사/배관사', + phone VARCHAR(20), + hire_date DATE, + status ENUM('active', 'inactive') DEFAULT 'active', + created_by INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (created_by) REFERENCES users(id) +); + +-- 일일 작업 기록 테이블 +CREATE TABLE daily_work ( + id INT AUTO_INCREMENT PRIMARY KEY, + worker_id INT NOT NULL, + work_date DATE NOT NULL, + work_description TEXT, + start_time TIME, + end_time TIME, + work_hours DECIMAL(4,2) COMMENT '작업 시간', + location VARCHAR(200) COMMENT '작업 위치', + status ENUM('planned', 'in_progress', 'completed', 'cancelled') DEFAULT 'planned', + notes TEXT COMMENT '특이사항', + created_by INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (worker_id) REFERENCES workers(id), + FOREIGN KEY (created_by) REFERENCES users(id), + INDEX idx_work_date (work_date), + INDEX idx_worker_date (worker_id, work_date) +); + +-- 에러사항 기록 테이블 +CREATE TABLE error_reports ( + id INT AUTO_INCREMENT PRIMARY KEY, + worker_id INT, + error_date DATE NOT NULL, + error_time TIME, + error_type ENUM('equipment', 'safety', 'quality', 'process', 'other') NOT NULL, + error_description TEXT NOT NULL, + location VARCHAR(200), + severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium', + status ENUM('reported', 'investigating', 'resolved', 'closed') DEFAULT 'reported', + resolution TEXT COMMENT '해결 방안', + resolved_at TIMESTAMP NULL, + resolved_by INT, + created_by INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (worker_id) REFERENCES workers(id), + FOREIGN KEY (resolved_by) REFERENCES users(id), + FOREIGN KEY (created_by) REFERENCES users(id), + INDEX idx_error_date (error_date), + INDEX idx_status (status) +); + +-- 요청사항 테이블 (장비/소모품) +CREATE TABLE requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + worker_id INT, + request_type ENUM('equipment', 'supplies', 'maintenance', 'other') NOT NULL, + item_name VARCHAR(200) NOT NULL, + quantity INT DEFAULT 1, + description TEXT, + urgency ENUM('low', 'normal', 'high', 'urgent') DEFAULT 'normal', + status ENUM('pending', 'approved', 'ordered', 'delivered', 'rejected') DEFAULT 'pending', + requested_date DATE NOT NULL, + needed_date DATE COMMENT '필요 날짜', + approved_by INT, + approved_at TIMESTAMP NULL, + notes TEXT COMMENT '관리자 메모', + created_by INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (worker_id) REFERENCES workers(id), + FOREIGN KEY (approved_by) REFERENCES users(id), + FOREIGN KEY (created_by) REFERENCES users(id), + INDEX idx_request_date (requested_date), + INDEX idx_status (status) +); + +-- 기본 데이터 삽입 +INSERT INTO users (username, password, role, name) VALUES +('admin', '$2b$10$rOzJqQjQjQjQjQjQjQjQjOzJqQjQjQjQjQjQjQjQjQjQjQjQjQjQjQ', 'admin', '관리자'), +('user', '$2b$10$rOzJqQjQjQjQjQjQjQjQjOzJqQjQjQjQjQjQjQjQjQjQjQjQjQjQjQ', 'user', '사용자'); + +-- 샘플 작업자 데이터 +INSERT INTO workers (name, job_type, phone, hire_date, created_by) VALUES +('김용접', 'welder', '010-1234-5678', '2024-01-15', 1), +('이배관', 'plumber', '010-2345-6789', '2024-02-01', 1), +('박용접', 'welder', '010-3456-7890', '2024-03-01', 1); + +-- 샘플 일일 작업 데이터 +INSERT INTO daily_work (worker_id, work_date, work_description, start_time, end_time, work_hours, location, status, created_by) VALUES +(1, CURDATE(), '파이프 용접 작업', '09:00:00', '17:00:00', 8.0, '1층 작업장', 'completed', 1), +(2, CURDATE(), '배관 설치 작업', '09:00:00', '16:00:00', 7.0, '2층 화장실', 'in_progress', 1); + +-- 샘플 에러사항 데이터 +INSERT INTO error_reports (worker_id, error_date, error_time, error_type, error_description, location, severity, created_by) VALUES +(1, CURDATE(), '14:30:00', 'equipment', '용접기 과열로 인한 작업 중단', '1층 작업장', 'medium', 1); + +-- 샘플 요청사항 데이터 +INSERT INTO requests (worker_id, request_type, item_name, quantity, description, urgency, requested_date, needed_date, created_by) VALUES +(1, 'supplies', '용접봉', 10, 'STS 용접봉 필요', 'normal', CURDATE(), DATE_ADD(CURDATE(), INTERVAL 3 DAY), 1), +(2, 'equipment', '파이프 커터', 1, '대형 파이프 절단용', 'high', CURDATE(), DATE_ADD(CURDATE(), INTERVAL 1 DAY), 1); diff --git a/frontend/css/dashboard.css b/frontend/css/dashboard.css new file mode 100644 index 0000000..950292d --- /dev/null +++ b/frontend/css/dashboard.css @@ -0,0 +1,292 @@ +/* 작업관리 시스템 공통 스타일 */ + +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap'); + +body { + font-family: 'Noto Sans KR', sans-serif; + background-color: #f8f9fa; +} + +/* 네비게이션 스타일 */ +.navbar-brand { + font-weight: 700; + font-size: 1.5rem; +} + +.nav-link { + font-weight: 500; + transition: all 0.3s ease; +} + +.nav-link:hover { + transform: translateY(-1px); +} + +.nav-link.active { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 5px; +} + +/* 카드 스타일 */ +.card { + border: none; + border-radius: 15px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.card-header { + background-color: #fff; + border-bottom: 2px solid #f8f9fa; + border-radius: 15px 15px 0 0 !important; + font-weight: 600; +} + +/* 통계 카드 */ +.card.bg-primary, +.card.bg-success, +.card.bg-warning, +.card.bg-info { + background: linear-gradient(135deg, var(--bs-primary) 0%, #764ba2 100%) !important; +} + +.card.bg-success { + background: linear-gradient(135deg, var(--bs-success) 0%, #56ab2f 100%) !important; +} + +.card.bg-warning { + background: linear-gradient(135deg, var(--bs-warning) 0%, #f093fb 100%) !important; +} + +.card.bg-info { + background: linear-gradient(135deg, var(--bs-info) 0%, #4facfe 100%) !important; +} + +/* 버튼 스타일 */ +.btn { + border-radius: 10px; + font-weight: 500; + transition: all 0.3s ease; +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; +} + +.btn-success { + background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%); + border: none; +} + +.btn-warning { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + border: none; +} + +.btn-info { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + border: none; +} + +/* 테이블 스타일 */ +.table { + border-radius: 10px; + overflow: hidden; +} + +.table thead th { + background-color: #f8f9fa; + border: none; + font-weight: 600; + color: #495057; +} + +.table tbody tr { + transition: all 0.3s ease; +} + +.table tbody tr:hover { + background-color: #f8f9fa; + transform: scale(1.01); +} + +/* 폼 스타일 */ +.form-control, +.form-select { + border-radius: 10px; + border: 2px solid #e9ecef; + transition: all 0.3s ease; +} + +.form-control:focus, +.form-select:focus { + border-color: #667eea; + box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); +} + +/* 모달 스타일 */ +.modal-content { + border-radius: 15px; + border: none; + box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); +} + +.modal-header { + border-bottom: 2px solid #f8f9fa; + border-radius: 15px 15px 0 0; +} + +.modal-footer { + border-top: 2px solid #f8f9fa; + border-radius: 0 0 15px 15px; +} + +/* 배지 스타일 */ +.badge { + border-radius: 20px; + font-weight: 500; + padding: 0.5em 1em; +} + +/* 직종별 배지 색상 */ +.badge.job-welder { + background-color: #ff6b6b; +} + +.badge.job-plumber { + background-color: #4ecdc4; +} + +/* 상태별 배지 색상 */ +.badge.status-active { + background-color: #51cf66; +} + +.badge.status-inactive { + background-color: #868e96; +} + +.badge.status-completed { + background-color: #51cf66; +} + +.badge.status-in-progress { + background-color: #339af0; +} + +.badge.status-planned { + background-color: #ffd43b; + color: #495057; +} + +.badge.status-cancelled { + background-color: #868e96; +} + +/* 우선순위별 배지 색상 */ +.badge.priority-low { + background-color: #51cf66; +} + +.badge.priority-normal { + background-color: #339af0; +} + +.badge.priority-high { + background-color: #ff8cc8; +} + +.badge.priority-urgent { + background-color: #ff6b6b; +} + +/* 심각도별 배지 색상 */ +.badge.severity-low { + background-color: #51cf66; +} + +.badge.severity-medium { + background-color: #ffd43b; + color: #495057; +} + +.badge.severity-high { + background-color: #ff8cc8; +} + +.badge.severity-critical { + background-color: #ff6b6b; +} + +/* 토스트 스타일 */ +.toast { + border-radius: 10px; + border: none; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +/* 반응형 스타일 */ +@media (max-width: 768px) { + .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .card-body { + padding: 1rem; + } + + .btn { + font-size: 0.9rem; + } + + .table-responsive { + font-size: 0.9rem; + } +} + +/* 로딩 애니메이션 */ +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } +} + +.loading { + animation: pulse 1.5s ease-in-out infinite; +} + +/* 스크롤바 스타일 */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/frontend/dashboard.html b/frontend/dashboard.html new file mode 100644 index 0000000..b459c63 --- /dev/null +++ b/frontend/dashboard.html @@ -0,0 +1,216 @@ + + + + + + 작업관리 시스템 - 대시보드 + + + + + + + + +
+
+ +
+
+

대시보드

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

0

+

전체 작업자

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

0

+

오늘 작업

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

0

+

미해결 에러

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

0

+

대기 요청

+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
최근 작업
+
+
+
+
+ +

데이터를 불러오는 중...

+
+
+
+
+
+
+
+
+
최근 에러
+
+
+
+
+ +

데이터를 불러오는 중...

+
+
+
+
+
+
+ + +
+
+
+
+
빠른 작업
+
+
+
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c6cbd4d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,140 @@ + + + + + + 작업관리 시스템 - 로그인 + + + + + +
+
+
+ +
+
+
+ + + + + + + + diff --git a/frontend/js/auth.js b/frontend/js/auth.js new file mode 100644 index 0000000..83585ce --- /dev/null +++ b/frontend/js/auth.js @@ -0,0 +1,266 @@ +// 인증 관련 JavaScript 함수들 + +const API_BASE_URL = 'http://localhost:14001/api'; + +// 로그인 함수 +async function login(username, password) { + try { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + + if (response.ok) { + // 토큰과 사용자 정보 저장 + localStorage.setItem('token', data.token); + localStorage.setItem('user', JSON.stringify(data.user)); + + // 대시보드로 이동 + window.location.href = 'dashboard.html'; + } else { + throw new Error(data.message || '로그인에 실패했습니다.'); + } + } catch (error) { + console.error('로그인 오류:', error); + showAlert('로그인 실패', error.message); + } +} + +// 로그아웃 함수 +function logout() { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = 'index.html'; +} + +// 토큰 확인 함수 +function checkAuth() { + const token = localStorage.getItem('token'); + const user = localStorage.getItem('user'); + + if (!token || !user) { + // 로그인 페이지가 아닌 경우에만 리다이렉트 + if (!window.location.pathname.includes('index.html') && window.location.pathname !== '/') { + window.location.href = 'index.html'; + } + return null; + } + + try { + return JSON.parse(user); + } catch (error) { + console.error('사용자 정보 파싱 오류:', error); + logout(); + return null; + } +} + +// 관리자 권한 확인 +function isAdmin() { + const user = checkAuth(); + return user && user.role === 'admin'; +} + +// API 요청 헤더에 토큰 추가 +function getAuthHeaders() { + const token = localStorage.getItem('token'); + return { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }; +} + +// API 요청 함수 +async function apiRequest(url, options = {}) { + const defaultOptions = { + headers: getAuthHeaders() + }; + + const mergedOptions = { + ...defaultOptions, + ...options, + headers: { + ...defaultOptions.headers, + ...options.headers + } + }; + + try { + const response = await fetch(`${API_BASE_URL}${url}`, mergedOptions); + + // 인증 오류 처리 + if (response.status === 401) { + logout(); + return; + } + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || '요청 처리에 실패했습니다.'); + } + + return data; + } catch (error) { + console.error('API 요청 오류:', error); + throw error; + } +} + +// 알림 표시 함수 +function showAlert(title, message, type = 'danger') { + const alertModal = document.getElementById('alertModal'); + if (alertModal) { + document.getElementById('alertMessage').innerHTML = `${title}
${message}`; + const modal = new bootstrap.Modal(alertModal); + modal.show(); + } else { + alert(`${title}: ${message}`); + } +} + +// 토스트 알림 표시 함수 +function showToast(message, type = 'success') { + const toastElement = document.getElementById('alertToast'); + const toastMessage = document.getElementById('toastMessage'); + + if (toastElement && toastMessage) { + toastMessage.textContent = message; + + // 토스트 색상 설정 + toastElement.className = `toast ${type === 'success' ? 'bg-success' : 'bg-danger'} text-white`; + + const toast = new bootstrap.Toast(toastElement); + toast.show(); + } +} + +// 페이지 로드 시 인증 확인 +document.addEventListener('DOMContentLoaded', function() { + // 로그인 페이지가 아닌 경우 인증 확인 + if (!window.location.pathname.includes('index.html') && window.location.pathname !== '/') { + const user = checkAuth(); + if (user) { + // 사용자 이름 표시 + const userNameDisplay = document.getElementById('userNameDisplay'); + if (userNameDisplay) { + userNameDisplay.textContent = user.name; + } + + // 관리자가 아닌 경우 작업자 관리 메뉴 숨기기 + if (!isAdmin()) { + const workerManagementNav = document.getElementById('workerManagementNav'); + const addWorkerBtn = document.getElementById('addWorkerBtn'); + + if (workerManagementNav) workerManagementNav.style.display = 'none'; + if (addWorkerBtn) addWorkerBtn.style.display = 'none'; + } + } + } + + // 로그인 폼 처리 + const loginForm = document.getElementById('loginForm'); + if (loginForm) { + loginForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + + if (!username || !password) { + showAlert('입력 오류', '아이디와 비밀번호를 모두 입력해주세요.'); + return; + } + + await login(username, password); + }); + } +}); + +// 날짜 포맷 함수 +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('ko-KR', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); +} + +// 시간 포맷 함수 +function formatTime(timeString) { + if (!timeString) return ''; + return timeString.substring(0, 5); // HH:MM 형식으로 자르기 +} + +// 직종 한글 변환 +function getJobTypeText(jobType) { + const jobTypes = { + 'welder': '용접사', + 'plumber': '배관사' + }; + return jobTypes[jobType] || jobType; +} + +// 상태 한글 변환 +function getStatusText(status, type = 'general') { + const statusTexts = { + general: { + 'active': '활성', + 'inactive': '비활성', + 'pending': '대기', + 'approved': '승인', + 'rejected': '거부', + 'completed': '완료', + 'cancelled': '취소' + }, + work: { + 'planned': '계획', + 'in_progress': '진행중', + 'completed': '완료', + 'cancelled': '취소' + }, + error: { + 'reported': '신고됨', + 'investigating': '조사중', + 'resolved': '해결됨', + 'closed': '종료' + }, + request: { + 'pending': '대기', + 'approved': '승인', + 'ordered': '주문', + 'delivered': '배송완료', + 'rejected': '거부' + } + }; + + return statusTexts[type][status] || status; +} + +// 우선순위 한글 변환 +function getPriorityText(priority) { + const priorities = { + 'low': '낮음', + 'normal': '보통', + 'high': '높음', + 'urgent': '긴급' + }; + return priorities[priority] || priority; +} + +// 심각도 한글 변환 +function getSeverityText(severity) { + const severities = { + 'low': '낮음', + 'medium': '보통', + 'high': '높음', + 'critical': '심각' + }; + return severities[severity] || severity; +} diff --git a/frontend/workers.html b/frontend/workers.html new file mode 100644 index 0000000..7f0737c --- /dev/null +++ b/frontend/workers.html @@ -0,0 +1,224 @@ + + + + + + 작업관리 시스템 - 작업자 관리 + + + + + + + + +
+
+
+
+

작업자 관리

+ +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
작업자 목록
+
+
+
+ + + + + + + + + + + + + + + + + +
이름직종연락처입사일상태등록일작업
+
+ 로딩 중... +
+

작업자 목록을 불러오는 중...

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