🚀 초기 프로젝트 설정 완료
✨ 기능: - 기간제 근로자 작업관리 시스템 기본 구조 - 한국어 기반 프론트엔드 (로그인, 대시보드, 작업자 관리) - 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 설정
This commit is contained in:
78
.gitignore
vendored
Normal file
78
.gitignore
vendored
Normal file
@@ -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/
|
||||||
94
README.md
Normal file
94
README.md
Normal file
@@ -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
|
||||||
101
backend/models/database.js
Normal file
101
backend/models/database.js
Normal file
@@ -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
|
||||||
|
};
|
||||||
37
backend/package.json
Normal file
37
backend/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
102
backend/server.js
Normal file
102
backend/server.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
124
database/init.sql
Normal file
124
database/init.sql
Normal file
@@ -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);
|
||||||
292
frontend/css/dashboard.css
Normal file
292
frontend/css/dashboard.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
216
frontend/dashboard.html
Normal file
216
frontend/dashboard.html
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>작업관리 시스템 - 대시보드</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<link href="css/dashboard.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 상단 네비게이션 -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="#">
|
||||||
|
<i class="bi bi-tools me-2"></i>작업관리 시스템
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="dashboard.html">
|
||||||
|
<i class="bi bi-house-door me-1"></i>대시보드
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" id="workerManagementNav" style="display: none;">
|
||||||
|
<a class="nav-link" href="workers.html">
|
||||||
|
<i class="bi bi-people me-1"></i>작업자 관리
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="daily-work.html">
|
||||||
|
<i class="bi bi-calendar-check me-1"></i>일일 작업
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="errors.html">
|
||||||
|
<i class="bi bi-exclamation-triangle me-1"></i>에러사항
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="requests.html">
|
||||||
|
<i class="bi bi-cart me-1"></i>요청사항
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-person-circle me-1"></i><span id="userNameDisplay">사용자</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="logout()">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>로그아웃
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<!-- 메인 콘텐츠 -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2><i class="bi bi-speedometer2 me-2"></i>대시보드</h2>
|
||||||
|
<div class="text-muted">
|
||||||
|
<i class="bi bi-calendar3 me-1"></i><span id="currentDate"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 카드 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card bg-primary text-white h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title mb-0" id="totalWorkers">0</h4>
|
||||||
|
<p class="card-text">전체 작업자</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="bi bi-people fs-1"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card bg-success text-white h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title mb-0" id="todayWork">0</h4>
|
||||||
|
<p class="card-text">오늘 작업</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="bi bi-calendar-check fs-1"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card bg-warning text-white h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title mb-0" id="pendingErrors">0</h4>
|
||||||
|
<p class="card-text">미해결 에러</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="bi bi-exclamation-triangle fs-1"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card bg-info text-white h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h4 class="card-title mb-0" id="pendingRequests">0</h4>
|
||||||
|
<p class="card-text">대기 요청</p>
|
||||||
|
</div>
|
||||||
|
<div class="align-self-center">
|
||||||
|
<i class="bi bi-cart fs-1"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 최근 활동 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>최근 작업</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="recentWork">
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<i class="bi bi-hourglass-split fs-1"></i>
|
||||||
|
<p>데이터를 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-exclamation-circle me-2"></i>최근 에러</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="recentErrors">
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<i class="bi bi-hourglass-split fs-1"></i>
|
||||||
|
<p>데이터를 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 빠른 작업 버튼 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>빠른 작업</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-2" id="addWorkerBtn" style="display: none;">
|
||||||
|
<button class="btn btn-outline-primary w-100" onclick="location.href='workers.html'">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>작업자 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<button class="btn btn-outline-success w-100" onclick="location.href='daily-work.html'">
|
||||||
|
<i class="bi bi-plus-circle me-2"></i>작업 등록
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<button class="btn btn-outline-warning w-100" onclick="location.href='errors.html'">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2"></i>에러 신고
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<button class="btn btn-outline-info w-100" onclick="location.href='requests.html'">
|
||||||
|
<i class="bi bi-cart-plus me-2"></i>요청 등록
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
|
<script src="js/dashboard.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
140
frontend/index.html
Normal file
140
frontend/index.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>작업관리 시스템 - 로그인</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'Noto Sans KR', sans-serif;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.login-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.login-body {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.form-control {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
padding: 12px 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||||
|
}
|
||||||
|
.btn-login {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 100%;
|
||||||
|
color: white;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.btn-login:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.input-group-text {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
}
|
||||||
|
.input-group .form-control {
|
||||||
|
border-left: none;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2><i class="bi bi-tools"></i> 작업관리 시스템</h2>
|
||||||
|
<p class="mb-0">기간제 근로자 작업관리</p>
|
||||||
|
</div>
|
||||||
|
<div class="login-body">
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">아이디</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-person"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" id="username" name="username"
|
||||||
|
placeholder="아이디를 입력하세요" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password" class="form-label">비밀번호</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-lock"></i>
|
||||||
|
</span>
|
||||||
|
<input type="password" class="form-control" id="password" name="password"
|
||||||
|
placeholder="비밀번호를 입력하세요" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-login">
|
||||||
|
<i class="bi bi-box-arrow-in-right me-2"></i>로그인
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<small class="text-muted">
|
||||||
|
<strong>기본 계정:</strong><br>
|
||||||
|
관리자: admin / admin123<br>
|
||||||
|
사용자: user / user123
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 알림 모달 -->
|
||||||
|
<div class="modal fade" id="alertModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">알림</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="alertMessage">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">확인</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
266
frontend/js/auth.js
Normal file
266
frontend/js/auth.js
Normal file
@@ -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 = `<strong>${title}</strong><br>${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;
|
||||||
|
}
|
||||||
224
frontend/workers.html
Normal file
224
frontend/workers.html
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>작업관리 시스템 - 작업자 관리</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
|
<link href="css/dashboard.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 상단 네비게이션 -->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="dashboard.html">
|
||||||
|
<i class="bi bi-tools me-2"></i>작업관리 시스템
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav me-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="dashboard.html">
|
||||||
|
<i class="bi bi-house-door me-1"></i>대시보드
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="workers.html">
|
||||||
|
<i class="bi bi-people me-1"></i>작업자 관리
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="daily-work.html">
|
||||||
|
<i class="bi bi-calendar-check me-1"></i>일일 작업
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="errors.html">
|
||||||
|
<i class="bi bi-exclamation-triangle me-1"></i>에러사항
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="requests.html">
|
||||||
|
<i class="bi bi-cart me-1"></i>요청사항
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||||
|
<i class="bi bi-person-circle me-1"></i><span id="userNameDisplay">사용자</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="logout()">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>로그아웃
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h2><i class="bi bi-people me-2"></i>작업자 관리</h2>
|
||||||
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#workerModal" onclick="openWorkerModal()">
|
||||||
|
<i class="bi bi-person-plus me-2"></i>작업자 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 필터 -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="jobTypeFilter" class="form-label">직종 필터</label>
|
||||||
|
<select class="form-select" id="jobTypeFilter" onchange="filterWorkers()">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="welder">용접사</option>
|
||||||
|
<option value="plumber">배관사</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="statusFilter" class="form-label">상태 필터</label>
|
||||||
|
<select class="form-select" id="statusFilter" onchange="filterWorkers()">
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="active">활성</option>
|
||||||
|
<option value="inactive">비활성</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="searchInput" class="form-label">이름 검색</label>
|
||||||
|
<input type="text" class="form-control" id="searchInput" placeholder="작업자 이름 검색" oninput="filterWorkers()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 작업자 목록 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">작업자 목록</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>이름</th>
|
||||||
|
<th>직종</th>
|
||||||
|
<th>연락처</th>
|
||||||
|
<th>입사일</th>
|
||||||
|
<th>상태</th>
|
||||||
|
<th>등록일</th>
|
||||||
|
<th>작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="workersTableBody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">로딩 중...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">작업자 목록을 불러오는 중...</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 작업자 추가/수정 모달 -->
|
||||||
|
<div class="modal fade" id="workerModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="workerModalTitle">작업자 추가</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form id="workerForm">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="workerId" name="id">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="workerName" class="form-label">이름 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="workerName" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="jobType" class="form-label">직종 <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-select" id="jobType" name="job_type" required>
|
||||||
|
<option value="">직종을 선택하세요</option>
|
||||||
|
<option value="welder">용접사</option>
|
||||||
|
<option value="plumber">배관사</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="workerPhone" class="form-label">연락처</label>
|
||||||
|
<input type="tel" class="form-control" id="workerPhone" name="phone" placeholder="010-1234-5678">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="hireDate" class="form-label">입사일</label>
|
||||||
|
<input type="date" class="form-control" id="hireDate" name="hire_date">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="workerStatus" class="form-label">상태</label>
|
||||||
|
<select class="form-select" id="workerStatus" name="status">
|
||||||
|
<option value="active">활성</option>
|
||||||
|
<option value="inactive">비활성</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||||
|
<button type="submit" class="btn btn-primary">저장</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 삭제 확인 모달 -->
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">작업자 삭제</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>정말로 이 작업자를 삭제하시겠습니까?</p>
|
||||||
|
<p class="text-danger"><strong>주의:</strong> 삭제된 작업자의 모든 작업 기록도 함께 삭제됩니다.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">삭제</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 알림 토스트 -->
|
||||||
|
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||||
|
<div id="alertToast" class="toast" role="alert">
|
||||||
|
<div class="toast-header">
|
||||||
|
<strong class="me-auto">알림</strong>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body" id="toastMessage">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
|
<script src="js/workers.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user