feat: 초기 프로젝트 설정 및 룰.md 파일 추가
This commit is contained in:
4
api.hyungi.net/.dockerignore
Normal file
4
api.hyungi.net/.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
59
api.hyungi.net/.env
Normal file
59
api.hyungi.net/.env
Normal file
@@ -0,0 +1,59 @@
|
||||
# .env
|
||||
PORT=3005
|
||||
|
||||
# MariaDB 컨테이너 초기화용 루트 패스워드
|
||||
DB_ROOT_PASSWORD=matxAc-jutty1-ruhsoc
|
||||
|
||||
# MariaDB 접속 정보
|
||||
DB_HOST=db_hyungi_net
|
||||
DB_PORT=3306
|
||||
DB_USER=hyungi
|
||||
DB_PASSWORD=tycdoq-Kawcug-8wesfa
|
||||
DB_NAME=hyungi
|
||||
|
||||
# =====================================================
|
||||
# JWT 설정 (보안 강화)
|
||||
# =====================================================
|
||||
JWT_SECRET=uHlUfU0nWEhkRFfOJSSr468lJzss9uC7
|
||||
JWT_REFRESH_SECRET=kL9mN3pQ5rT7vX1zA3dF5hJ7kM9nP2qR4sV6wY8 # 새로 추가
|
||||
JWT_EXPIRES_IN=15m
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
|
||||
# =====================================================
|
||||
# 보안 설정
|
||||
# =====================================================
|
||||
# 비밀번호 해싱 라운드
|
||||
BCRYPT_ROUNDS=10
|
||||
|
||||
# 로그인 실패 제한
|
||||
MAX_LOGIN_ATTEMPTS=5
|
||||
LOCK_TIME=15 # 분 단위
|
||||
|
||||
# =====================================================
|
||||
# CORS 설정
|
||||
# =====================================================
|
||||
ALLOWED_ORIGINS=http://192.168.0.3:3001,http://192.168.0.3,http://192.168.0.3:3000,http://192.168.0.3:25000
|
||||
|
||||
# =====================================================
|
||||
# API 속도 제한
|
||||
# =====================================================
|
||||
RATE_LIMIT_WINDOW=15 # 분 단위
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
LOGIN_RATE_LIMIT_MAX_REQUESTS=5
|
||||
|
||||
# =====================================================
|
||||
# 파일 업로드 설정
|
||||
# =====================================================
|
||||
MAX_FILE_SIZE=50 # MB 단위
|
||||
UPLOAD_PATH=./uploads
|
||||
|
||||
# =====================================================
|
||||
# 로깅 설정
|
||||
# =====================================================
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE_PATH=./logs
|
||||
|
||||
# =====================================================
|
||||
# 환경 설정
|
||||
# =====================================================
|
||||
NODE_ENV=production
|
||||
33
api.hyungi.net/Dockerfile
Normal file
33
api.hyungi.net/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
# Node.js 공식 이미지 사용
|
||||
FROM node:18-alpine
|
||||
|
||||
# 작업 디렉토리 설정
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# 패키지 파일 복사 (캐싱 최적화)
|
||||
COPY package*.json ./
|
||||
|
||||
# 프로덕션 의존성만 설치
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 앱 소스 복사
|
||||
COPY . .
|
||||
|
||||
# 로그 디렉토리 생성
|
||||
RUN mkdir -p logs uploads
|
||||
|
||||
# 실행 권한 설정
|
||||
RUN chown -R node:node /usr/src/app
|
||||
|
||||
# 보안을 위해 non-root 사용자로 실행
|
||||
USER node
|
||||
|
||||
# 포트 노출
|
||||
EXPOSE 3005
|
||||
|
||||
# 헬스체크 추가
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3005/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"
|
||||
|
||||
# 앱 시작
|
||||
CMD ["node", "index.js"]
|
||||
178
api.hyungi.net/controllers/authController.js
Normal file
178
api.hyungi.net/controllers/authController.js
Normal file
@@ -0,0 +1,178 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
exports.login = async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM Users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return res.status(401).json({ error: '존재하지 않는 사용자입니다.' });
|
||||
}
|
||||
|
||||
const user = rows[0];
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({ error: '비밀번호가 일치하지 않습니다.' });
|
||||
}
|
||||
|
||||
// JWT 토큰 생성
|
||||
const token = jwt.sign(
|
||||
{
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
access_level: user.access_level,
|
||||
worker_id: user.worker_id
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '1d' }
|
||||
);
|
||||
|
||||
// 토큰 포함 응답
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
token,
|
||||
user_id: user.user_id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[로그인 오류]', err);
|
||||
return res.status(500).json({
|
||||
error: '서버 내부 오류',
|
||||
detail: err.message || String(err)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 사용자 등록 기능 추가
|
||||
exports.register = async (req, res) => {
|
||||
try {
|
||||
const { username, password, name, access_level, worker_id } = req.body;
|
||||
const db = await getDb();
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!username || !password || !name || !access_level) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: '필수 정보가 누락되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 중복 아이디 확인
|
||||
const [existing] = await db.query(
|
||||
'SELECT user_id FROM Users WHERE username = ?',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
error: '이미 존재하는 아이디입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 비밀번호 해시화
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// role 설정 (access_level에 따라)
|
||||
const roleMap = {
|
||||
'admin': 'admin',
|
||||
'system': 'admin',
|
||||
'group_leader': 'leader',
|
||||
'support_team': 'support',
|
||||
'worker': 'user'
|
||||
};
|
||||
const role = roleMap[access_level] || 'user';
|
||||
|
||||
// 사용자 등록
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Users (username, password, name, role, access_level, worker_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[username, hashedPassword, name, role, access_level, worker_id]
|
||||
);
|
||||
|
||||
console.log('[사용자 등록 성공]', username);
|
||||
|
||||
return res.status(201).json({
|
||||
success: true,
|
||||
message: '사용자 등록이 완료되었습니다.',
|
||||
user_id: result.insertId
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[사용자 등록 오류]', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
detail: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 사용자 삭제 기능 추가
|
||||
exports.deleteUser = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const db = await getDb();
|
||||
|
||||
// 사용자 존재 확인
|
||||
const [user] = await db.query(
|
||||
'SELECT user_id FROM Users WHERE user_id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: '해당 사용자를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 삭제
|
||||
await db.query('DELETE FROM Users WHERE user_id = ?', [id]);
|
||||
|
||||
console.log('[사용자 삭제 성공] ID:', id);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: '사용자가 삭제되었습니다.'
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[사용자 삭제 오류]', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
detail: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 사용자 목록 조회
|
||||
exports.getAllUsers = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 비밀번호 제외하고 조회
|
||||
const [rows] = await db.query(
|
||||
`SELECT user_id, username, name, role, access_level, worker_id, created_at
|
||||
FROM Users
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
res.status(200).json(rows);
|
||||
} catch (err) {
|
||||
console.error('[사용자 목록 조회 실패]', err);
|
||||
res.status(500).json({ error: '서버 오류' });
|
||||
}
|
||||
};
|
||||
85
api.hyungi.net/controllers/cuttingPlanController.js
Normal file
85
api.hyungi.net/controllers/cuttingPlanController.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// controllers/cuttingPlanController.js
|
||||
const cuttingPlanModel = require('../models/cuttingPlanModel');
|
||||
|
||||
// 1. 생성
|
||||
exports.createCuttingPlan = async (req, res) => {
|
||||
try {
|
||||
const planData = req.body;
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.create(planData, (err, id) => {
|
||||
if (err) return reject(err);
|
||||
resolve(id);
|
||||
});
|
||||
});
|
||||
res.json({ success: true, cutting_plan_id: lastID });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllCuttingPlans = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.getAll((err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getCuttingPlanById = async (req, res) => {
|
||||
try {
|
||||
const cutting_plan_id = parseInt(req.params.cutting_plan_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.getById(cutting_plan_id, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'CuttingPlan not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateCuttingPlan = async (req, res) => {
|
||||
try {
|
||||
const cutting_plan_id = parseInt(req.params.cutting_plan_id, 10);
|
||||
const planData = { ...req.body, cutting_plan_id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.update(planData, (err, count) => {
|
||||
if (err) return reject(err);
|
||||
resolve(count);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeCuttingPlan = async (req, res) => {
|
||||
try {
|
||||
const cutting_plan_id = parseInt(req.params.cutting_plan_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.remove(cutting_plan_id, (err, count) => {
|
||||
if (err) return reject(err);
|
||||
resolve(count);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'CuttingPlan not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
110
api.hyungi.net/controllers/dailyIssueReportController.js
Normal file
110
api.hyungi.net/controllers/dailyIssueReportController.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const dailyIssueReportModel = require('../models/dailyIssueReportModel');
|
||||
|
||||
// 1. CREATE: 단일 또는 다중 등록 (worker_id 배열 지원)
|
||||
exports.createDailyIssueReport = async (req, res) => {
|
||||
try {
|
||||
const body = req.body;
|
||||
|
||||
// 기본 필드
|
||||
const base = {
|
||||
date: body.date,
|
||||
project_id: body.project_id,
|
||||
start_time: body.start_time,
|
||||
end_time: body.end_time,
|
||||
issue_type_id: body.issue_type_id
|
||||
};
|
||||
|
||||
if (!base.date || !base.project_id || !base.start_time || !base.end_time || !base.issue_type_id || !body.worker_id) {
|
||||
return res.status(400).json({ error: '필수 필드 누락' });
|
||||
}
|
||||
|
||||
// worker_id 배열화
|
||||
const workers = Array.isArray(body.worker_id) ? body.worker_id : [body.worker_id];
|
||||
const insertedIds = [];
|
||||
|
||||
for (const wid of workers) {
|
||||
const payload = { ...base, worker_id: wid };
|
||||
|
||||
const insertId = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.create(payload, (err, id) => {
|
||||
if (err) reject(err);
|
||||
else resolve(id);
|
||||
});
|
||||
});
|
||||
|
||||
insertedIds.push(insertId);
|
||||
}
|
||||
|
||||
res.json({ success: true, issue_report_ids: insertedIds });
|
||||
} catch (err) {
|
||||
console.error('🔥 createDailyIssueReport error:', err);
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. READ BY DATE
|
||||
exports.getDailyIssuesByDate = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.query;
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.getAllByDate(date, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. READ ONE
|
||||
exports.getDailyIssueById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.getById(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'DailyIssueReport not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. UPDATE
|
||||
exports.updateDailyIssue = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.update(id, req.body, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. DELETE
|
||||
exports.removeDailyIssue = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
dailyIssueReportModel.remove(id, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'DailyIssueReport not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
750
api.hyungi.net/controllers/dailyWorkReportController 이전.js
Normal file
750
api.hyungi.net/controllers/dailyWorkReportController 이전.js
Normal file
@@ -0,0 +1,750 @@
|
||||
// controllers/dailyWorkReportController.js - 누적입력 방식 + 모든 기존 기능 포함
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
|
||||
/**
|
||||
* 📝 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!)
|
||||
*/
|
||||
const createDailyWorkReport = (req, res) => {
|
||||
const { report_date, worker_id, work_entries } = req.body;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
const created_by_name = req.user?.name || req.user?.username || '알 수 없는 사용자';
|
||||
|
||||
// 1. 기본 유효성 검사
|
||||
if (!report_date || !worker_id || !work_entries) {
|
||||
return res.status(400).json({
|
||||
error: '필수 필드가 누락되었습니다.',
|
||||
required: ['report_date', 'worker_id', 'work_entries'],
|
||||
received: {
|
||||
report_date: !!report_date,
|
||||
worker_id: !!worker_id,
|
||||
work_entries: !!work_entries
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(work_entries) || work_entries.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: '최소 하나의 작업 항목이 필요합니다.',
|
||||
received_entries: work_entries?.length || 0
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 작업 항목 유효성 검사
|
||||
for (let i = 0; i < work_entries.length; i++) {
|
||||
const entry = work_entries[i];
|
||||
const requiredFields = ['project_id', 'work_type_id', 'work_status_id', 'work_hours'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 ${field}가 누락되었습니다.`,
|
||||
entry_index: i,
|
||||
missing_field: field
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 상태인 경우 에러 타입 필수
|
||||
if (entry.work_status_id === 2 && (!entry.error_type_id)) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}이 에러 상태인 경우 error_type_id가 필요합니다.`,
|
||||
entry_index: i
|
||||
});
|
||||
}
|
||||
|
||||
// 시간 유효성 검사
|
||||
const hours = parseFloat(entry.work_hours);
|
||||
if (isNaN(hours) || hours < 0 || hours > 24) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 작업시간이 유효하지 않습니다. (0-24시간)`,
|
||||
entry_index: i,
|
||||
received_hours: entry.work_hours
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 총 시간 계산
|
||||
const total_hours = work_entries.reduce((sum, entry) => sum + (parseFloat(entry.work_hours) || 0), 0);
|
||||
|
||||
// 4. 요청 데이터 구성
|
||||
const reportData = {
|
||||
report_date,
|
||||
worker_id: parseInt(worker_id),
|
||||
work_entries,
|
||||
created_by,
|
||||
created_by_name,
|
||||
total_hours,
|
||||
is_update: false
|
||||
};
|
||||
|
||||
console.log('📝 작업보고서 누적 추가 요청:', {
|
||||
date: report_date,
|
||||
worker: worker_id,
|
||||
creator: created_by_name,
|
||||
creator_id: created_by,
|
||||
entries: work_entries.length,
|
||||
total_hours
|
||||
});
|
||||
|
||||
// 5. 누적 추가 실행 (덮어쓰기 없음!)
|
||||
dailyWorkReportModel.createDailyReport(reportData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 생성 중 오류가 발생했습니다.',
|
||||
details: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 작업보고서 누적 추가 성공:', result);
|
||||
res.status(201).json({
|
||||
message: '작업보고서가 성공적으로 누적 추가되었습니다.',
|
||||
report_date,
|
||||
worker_id,
|
||||
created_by: created_by_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getAccumulatedReports = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
total_entries: data.length,
|
||||
accumulated_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
const getContributorsSummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('기여자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '기여자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
|
||||
|
||||
console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
contributors: data,
|
||||
total_contributors: data.length,
|
||||
grand_total_hours: totalHours,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 개인 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getMyAccumulatedData = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('개인 누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
created_by,
|
||||
my_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능)
|
||||
*/
|
||||
const removeMyEntry = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => {
|
||||
if (err) {
|
||||
console.error('개별 항목 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '항목 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 개별 항목 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '항목이 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 작업보고서 조회 (쿼리 파라미터 기반 - 작성자별 필터링 강화)
|
||||
*/
|
||||
const getDailyWorkReports = (req, res) => {
|
||||
const { date, worker_id, created_by: requested_created_by } = req.query;
|
||||
const current_user_id = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!current_user_id) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 일반 사용자는 자신이 작성한 것만 볼 수 있음
|
||||
const created_by = requested_created_by || current_user_id;
|
||||
|
||||
console.log('📊 작업보고서 조회 요청:', {
|
||||
date,
|
||||
worker_id,
|
||||
requested_created_by,
|
||||
current_user_id,
|
||||
final_created_by: created_by
|
||||
});
|
||||
|
||||
if (date && created_by) {
|
||||
// 날짜 + 작성자별 조회
|
||||
dailyWorkReportModel.getByDateAndCreator(date, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜+작성자별 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
} else if (date && worker_id) {
|
||||
// 기존 방식: 날짜 + 작업자별 (하지만 작성자 필터링 추가)
|
||||
dailyWorkReportModel.getByDateAndWorker(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 날짜+작업자별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
} else if (date) {
|
||||
// 날짜별 조회 (작성자 필터링)
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: '날짜(date) 파라미터가 필요합니다.',
|
||||
example: 'date=2024-06-16',
|
||||
optional: ['worker_id', 'created_by']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 날짜별 작업보고서 조회 (경로 파라미터)
|
||||
*/
|
||||
const getDailyWorkReportsByDate = (req, res) => {
|
||||
const { date } = req.params;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜별 조회 (경로): date=${date}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('날짜별 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === created_by);
|
||||
console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔍 작업보고서 검색 (페이지네이션 포함)
|
||||
*/
|
||||
const searchWorkReports = (req, res) => {
|
||||
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31',
|
||||
optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit']
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const searchParams = {
|
||||
start_date,
|
||||
end_date,
|
||||
worker_id: worker_id ? parseInt(worker_id) : null,
|
||||
project_id: project_id ? parseInt(project_id) : null,
|
||||
work_status_id: work_status_id ? parseInt(work_status_id) : null,
|
||||
created_by, // 작성자 필터링 추가
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
console.log('🔍 작업보고서 검색 요청:', searchParams);
|
||||
|
||||
dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 검색 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 검색 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 통계 조회 (작성자별 필터링)
|
||||
*/
|
||||
const getWorkReportStats = (req, res) => {
|
||||
const { start_date, end_date } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📈 통계 조회: ${start_date} ~ ${end_date}, 요청자: ${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getStatistics(start_date, end_date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '통계 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
...data,
|
||||
metadata: {
|
||||
note: '현재는 전체 통계입니다. 개인별 통계는 추후 구현 예정',
|
||||
requested_by: created_by,
|
||||
period: `${start_date} ~ ${end_date}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 일일 근무 요약 조회
|
||||
*/
|
||||
const getDailySummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (date) {
|
||||
console.log(`📊 일일 요약 조회: date=${date}`);
|
||||
dailyWorkReportModel.getSummaryByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('일일 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '일일 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else if (worker_id) {
|
||||
console.log(`📊 작업자별 요약 조회: worker_id=${worker_id}`);
|
||||
dailyWorkReportModel.getSummaryByWorker(worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: 'date 또는 worker_id 파라미터가 필요합니다.',
|
||||
examples: [
|
||||
'date=2024-06-16',
|
||||
'worker_id=1'
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📅 월간 요약 조회
|
||||
*/
|
||||
const getMonthlySummary = (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
error: 'year와 month가 필요합니다.',
|
||||
example: 'year=2024&month=01',
|
||||
note: 'month는 01, 02, ..., 12 형식으로 입력하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월간 요약 조회: ${year}-${month}`);
|
||||
|
||||
dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => {
|
||||
if (err) {
|
||||
console.error('월간 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '월간 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
summary: data,
|
||||
total_entries: data.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* ✏️ 작업보고서 수정
|
||||
*/
|
||||
const updateWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
const updated_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!updated_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
updateData.updated_by = updated_by;
|
||||
|
||||
console.log(`✏️ 작업보고서 수정 요청: id=${id}, 수정자=${updated_by}`);
|
||||
|
||||
dailyWorkReportModel.updateById(id, updateData, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 수정 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '수정할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 수정 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 수정되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
updated_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제
|
||||
*/
|
||||
const removeDailyWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 작업보고서 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeById(id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 작업자의 특정 날짜 전체 삭제
|
||||
*/
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
date: date,
|
||||
worker_id: worker_id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`);
|
||||
res.json({
|
||||
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
|
||||
date,
|
||||
worker_id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📋 마스터 데이터 조회 함수들
|
||||
*/
|
||||
const getWorkTypes = (req, res) => {
|
||||
console.log('📋 작업 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('작업 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 작업 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkStatusTypes = (req, res) => {
|
||||
console.log('📋 업무 상태 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkStatusTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('업무 상태 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '업무 상태 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getErrorTypes = (req, res) => {
|
||||
console.log('📋 에러 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllErrorTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('에러 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '에러 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 에러 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (기존 기능 + 누적 기능)
|
||||
module.exports = {
|
||||
// 📝 핵심 CRUD 함수들
|
||||
createDailyWorkReport, // 누적 추가 (덮어쓰기 없음)
|
||||
getDailyWorkReports, // 조회 (작성자별 필터링)
|
||||
getDailyWorkReportsByDate, // 날짜별 조회
|
||||
searchWorkReports, // 검색 (페이지네이션)
|
||||
updateWorkReport, // 수정
|
||||
removeDailyWorkReport, // 개별 삭제
|
||||
removeDailyWorkReportByDateAndWorker, // 전체 삭제
|
||||
|
||||
// 🔄 누적 관련 새로운 함수들
|
||||
getAccumulatedReports, // 누적 현황 조회
|
||||
getContributorsSummary, // 기여자별 요약
|
||||
getMyAccumulatedData, // 개인 누적 현황
|
||||
removeMyEntry, // 개별 항목 삭제 (본인 것만)
|
||||
|
||||
// 📊 요약 및 통계 함수들
|
||||
getDailySummary, // 일일 요약
|
||||
getMonthlySummary, // 월간 요약
|
||||
getWorkReportStats, // 통계
|
||||
|
||||
// 📋 마스터 데이터 함수들
|
||||
getWorkTypes, // 작업 유형 목록
|
||||
getWorkStatusTypes, // 업무 상태 유형 목록
|
||||
getErrorTypes // 에러 유형 목록
|
||||
};
|
||||
789
api.hyungi.net/controllers/dailyWorkReportController.js
Normal file
789
api.hyungi.net/controllers/dailyWorkReportController.js
Normal file
@@ -0,0 +1,789 @@
|
||||
// controllers/dailyWorkReportController.js - 권한별 전체 조회 지원 버전
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
|
||||
/**
|
||||
* 📝 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!)
|
||||
*/
|
||||
const createDailyWorkReport = (req, res) => {
|
||||
const { report_date, worker_id, work_entries } = req.body;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
const created_by_name = req.user?.name || req.user?.username || '알 수 없는 사용자';
|
||||
|
||||
// 1. 기본 유효성 검사
|
||||
if (!report_date || !worker_id || !work_entries) {
|
||||
return res.status(400).json({
|
||||
error: '필수 필드가 누락되었습니다.',
|
||||
required: ['report_date', 'worker_id', 'work_entries'],
|
||||
received: {
|
||||
report_date: !!report_date,
|
||||
worker_id: !!worker_id,
|
||||
work_entries: !!work_entries
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(work_entries) || work_entries.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: '최소 하나의 작업 항목이 필요합니다.',
|
||||
received_entries: work_entries?.length || 0
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 작업 항목 유효성 검사
|
||||
for (let i = 0; i < work_entries.length; i++) {
|
||||
const entry = work_entries[i];
|
||||
const requiredFields = ['project_id', 'work_type_id', 'work_status_id', 'work_hours'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 ${field}가 누락되었습니다.`,
|
||||
entry_index: i,
|
||||
missing_field: field
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 상태인 경우 에러 타입 필수
|
||||
if (entry.work_status_id === 2 && (!entry.error_type_id)) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}이 에러 상태인 경우 error_type_id가 필요합니다.`,
|
||||
entry_index: i
|
||||
});
|
||||
}
|
||||
|
||||
// 시간 유효성 검사
|
||||
const hours = parseFloat(entry.work_hours);
|
||||
if (isNaN(hours) || hours < 0 || hours > 24) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 작업시간이 유효하지 않습니다. (0-24시간)`,
|
||||
entry_index: i,
|
||||
received_hours: entry.work_hours
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 총 시간 계산
|
||||
const total_hours = work_entries.reduce((sum, entry) => sum + (parseFloat(entry.work_hours) || 0), 0);
|
||||
|
||||
// 4. 요청 데이터 구성
|
||||
const reportData = {
|
||||
report_date,
|
||||
worker_id: parseInt(worker_id),
|
||||
work_entries,
|
||||
created_by,
|
||||
created_by_name,
|
||||
total_hours,
|
||||
is_update: false
|
||||
};
|
||||
|
||||
console.log('📝 작업보고서 누적 추가 요청:', {
|
||||
date: report_date,
|
||||
worker: worker_id,
|
||||
creator: created_by_name,
|
||||
creator_id: created_by,
|
||||
entries: work_entries.length,
|
||||
total_hours
|
||||
});
|
||||
|
||||
// 5. 누적 추가 실행 (덮어쓰기 없음!)
|
||||
dailyWorkReportModel.createDailyReport(reportData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 생성 중 오류가 발생했습니다.',
|
||||
details: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 작업보고서 누적 추가 성공:', result);
|
||||
res.status(201).json({
|
||||
message: '작업보고서가 성공적으로 누적 추가되었습니다.',
|
||||
report_date,
|
||||
worker_id,
|
||||
created_by: created_by_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getAccumulatedReports = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
total_entries: data.length,
|
||||
accumulated_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
const getContributorsSummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('기여자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '기여자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
|
||||
|
||||
console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
contributors: data,
|
||||
total_contributors: data.length,
|
||||
grand_total_hours: totalHours,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 개인 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getMyAccumulatedData = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('개인 누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
created_by,
|
||||
my_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능)
|
||||
*/
|
||||
const removeMyEntry = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => {
|
||||
if (err) {
|
||||
console.error('개별 항목 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '항목 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 개별 항목 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '항목이 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 작업보고서 조회 (권한별 전체 조회 지원 - 핵심 수정!)
|
||||
*/
|
||||
const getDailyWorkReports = (req, res) => {
|
||||
const { date, worker_id, created_by: requested_created_by, view_all, admin, all, no_filter, ignore_created_by } = req.query;
|
||||
const current_user_id = req.user?.user_id || req.user?.id;
|
||||
const user_access_level = req.user?.access_level;
|
||||
|
||||
if (!current_user_id) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 🎯 권한별 필터링 로직 개선
|
||||
const isAdmin = user_access_level === 'system' || user_access_level === 'admin';
|
||||
const hasViewAllFlag = view_all === 'true' || admin === 'true' || all === 'true' ||
|
||||
no_filter === 'true' || ignore_created_by === 'true' ||
|
||||
requested_created_by === 'all' || requested_created_by === '';
|
||||
|
||||
const canViewAll = isAdmin || hasViewAllFlag;
|
||||
|
||||
// 관리자가 아니고 전체 조회 플래그도 없으면 본인 작성분으로 제한
|
||||
let final_created_by = null;
|
||||
if (!canViewAll) {
|
||||
final_created_by = requested_created_by || current_user_id;
|
||||
} else if (requested_created_by && requested_created_by !== 'all' && requested_created_by !== '') {
|
||||
final_created_by = requested_created_by;
|
||||
}
|
||||
|
||||
console.log('📊 작업보고서 조회 요청:', {
|
||||
date,
|
||||
worker_id,
|
||||
requested_created_by,
|
||||
current_user_id,
|
||||
user_access_level,
|
||||
isAdmin,
|
||||
hasViewAllFlag,
|
||||
canViewAll,
|
||||
final_created_by
|
||||
});
|
||||
|
||||
if (date && final_created_by) {
|
||||
// 날짜 + 작성자별 조회
|
||||
dailyWorkReportModel.getByDateAndCreator(date, final_created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜+작성자별 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
} else if (date && worker_id) {
|
||||
// 날짜 + 작업자별 조회
|
||||
dailyWorkReportModel.getByDateAndWorker(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 🎯 권한별 필터링
|
||||
let finalData = data;
|
||||
if (!canViewAll) {
|
||||
finalData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`);
|
||||
} else {
|
||||
console.log(`📊 관리자/전체 조회 권한: ${data.length}개 전체 반환`);
|
||||
}
|
||||
|
||||
res.json(finalData);
|
||||
});
|
||||
} else if (date) {
|
||||
// 날짜별 조회
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 🎯 권한별 필터링
|
||||
let finalData = data;
|
||||
if (!canViewAll) {
|
||||
finalData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`);
|
||||
} else {
|
||||
console.log(`📊 관리자/전체 조회 권한: ${data.length}개 전체 반환`);
|
||||
}
|
||||
|
||||
res.json(finalData);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: '날짜(date) 파라미터가 필요합니다.',
|
||||
example: 'date=2024-06-16',
|
||||
optional: ['worker_id', 'created_by', 'view_all', 'admin', 'all']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 날짜별 작업보고서 조회 (경로 파라미터 - 권한별 전체 조회 지원)
|
||||
*/
|
||||
const getDailyWorkReportsByDate = (req, res) => {
|
||||
const { date } = req.params;
|
||||
const current_user_id = req.user?.user_id || req.user?.id;
|
||||
const user_access_level = req.user?.access_level;
|
||||
|
||||
if (!current_user_id) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const isAdmin = user_access_level === 'system' || user_access_level === 'admin';
|
||||
|
||||
console.log(`📊 날짜별 조회 (경로): date=${date}, user=${current_user_id}, 권한=${user_access_level}, 관리자=${isAdmin}`);
|
||||
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('날짜별 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 🎯 권한별 필터링
|
||||
let finalData = data;
|
||||
if (!isAdmin) {
|
||||
finalData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`);
|
||||
} else {
|
||||
console.log(`📊 관리자 권한으로 전체 조회: ${data.length}개`);
|
||||
}
|
||||
|
||||
res.json(finalData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔍 작업보고서 검색 (페이지네이션 포함)
|
||||
*/
|
||||
const searchWorkReports = (req, res) => {
|
||||
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31',
|
||||
optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit']
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const searchParams = {
|
||||
start_date,
|
||||
end_date,
|
||||
worker_id: worker_id ? parseInt(worker_id) : null,
|
||||
project_id: project_id ? parseInt(project_id) : null,
|
||||
work_status_id: work_status_id ? parseInt(work_status_id) : null,
|
||||
created_by, // 작성자 필터링 추가
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
console.log('🔍 작업보고서 검색 요청:', searchParams);
|
||||
|
||||
dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 검색 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 검색 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 통계 조회 (작성자별 필터링)
|
||||
*/
|
||||
const getWorkReportStats = (req, res) => {
|
||||
const { start_date, end_date } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📈 통계 조회: ${start_date} ~ ${end_date}, 요청자: ${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getStatistics(start_date, end_date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '통계 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
...data,
|
||||
metadata: {
|
||||
note: '현재는 전체 통계입니다. 개인별 통계는 추후 구현 예정',
|
||||
requested_by: created_by,
|
||||
period: `${start_date} ~ ${end_date}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 일일 근무 요약 조회
|
||||
*/
|
||||
const getDailySummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (date) {
|
||||
console.log(`📊 일일 요약 조회: date=${date}`);
|
||||
dailyWorkReportModel.getSummaryByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('일일 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '일일 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else if (worker_id) {
|
||||
console.log(`📊 작업자별 요약 조회: worker_id=${worker_id}`);
|
||||
dailyWorkReportModel.getSummaryByWorker(worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: 'date 또는 worker_id 파라미터가 필요합니다.',
|
||||
examples: [
|
||||
'date=2024-06-16',
|
||||
'worker_id=1'
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📅 월간 요약 조회
|
||||
*/
|
||||
const getMonthlySummary = (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
error: 'year와 month가 필요합니다.',
|
||||
example: 'year=2024&month=01',
|
||||
note: 'month는 01, 02, ..., 12 형식으로 입력하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월간 요약 조회: ${year}-${month}`);
|
||||
|
||||
dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => {
|
||||
if (err) {
|
||||
console.error('월간 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '월간 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
summary: data,
|
||||
total_entries: data.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* ✏️ 작업보고서 수정
|
||||
*/
|
||||
const updateWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
const updated_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!updated_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
updateData.updated_by = updated_by;
|
||||
|
||||
console.log(`✏️ 작업보고서 수정 요청: id=${id}, 수정자=${updated_by}`);
|
||||
|
||||
dailyWorkReportModel.updateById(id, updateData, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 수정 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '수정할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 수정 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 수정되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
updated_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제
|
||||
*/
|
||||
const removeDailyWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 작업보고서 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeById(id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 작업자의 특정 날짜 전체 삭제
|
||||
*/
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
date: date,
|
||||
worker_id: worker_id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`);
|
||||
res.json({
|
||||
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
|
||||
date,
|
||||
worker_id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📋 마스터 데이터 조회 함수들
|
||||
*/
|
||||
const getWorkTypes = (req, res) => {
|
||||
console.log('📋 작업 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('작업 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 작업 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkStatusTypes = (req, res) => {
|
||||
console.log('📋 업무 상태 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkStatusTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('업무 상태 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '업무 상태 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getErrorTypes = (req, res) => {
|
||||
console.log('📋 에러 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllErrorTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('에러 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '에러 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 에러 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (권한별 조회 지원)
|
||||
module.exports = {
|
||||
// 📝 핵심 CRUD 함수들 (권한별 전체 조회 지원)
|
||||
createDailyWorkReport, // 누적 추가 (덮어쓰기 없음)
|
||||
getDailyWorkReports, // 조회 (권한별 필터링 개선)
|
||||
getDailyWorkReportsByDate, // 날짜별 조회 (권한별 필터링 개선)
|
||||
searchWorkReports, // 검색 (페이지네이션)
|
||||
updateWorkReport, // 수정
|
||||
removeDailyWorkReport, // 개별 삭제
|
||||
removeDailyWorkReportByDateAndWorker, // 전체 삭제
|
||||
|
||||
// 🔄 누적 관련 새로운 함수들
|
||||
getAccumulatedReports, // 누적 현황 조회
|
||||
getContributorsSummary, // 기여자별 요약
|
||||
getMyAccumulatedData, // 개인 누적 현황
|
||||
removeMyEntry, // 개별 항목 삭제 (본인 것만)
|
||||
|
||||
// 📊 요약 및 통계 함수들
|
||||
getDailySummary, // 일일 요약
|
||||
getMonthlySummary, // 월간 요약
|
||||
getWorkReportStats, // 통계
|
||||
|
||||
// 📋 마스터 데이터 함수들
|
||||
getWorkTypes, // 작업 유형 목록
|
||||
getWorkStatusTypes, // 업무 상태 유형 목록
|
||||
getErrorTypes // 에러 유형 목록
|
||||
};
|
||||
80
api.hyungi.net/controllers/equipmentListController.js
Normal file
80
api.hyungi.net/controllers/equipmentListController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// controllers/equipmentListController.js
|
||||
const equipmentListModel = require('../models/equipmentListModel');
|
||||
|
||||
// 1. 등록
|
||||
exports.createEquipment = async (req, res) => {
|
||||
try {
|
||||
const equipmentData = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.create(equipmentData, (err, insertId) =>
|
||||
err ? reject(err) : resolve(insertId)
|
||||
);
|
||||
});
|
||||
res.json({ success: true, equipment_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllEquipment = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.getAll((err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getEquipmentById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.equipment_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.getById(id, (err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Equipment not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateEquipment = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.equipment_id, 10);
|
||||
const data = { ...req.body, equipment_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.update(data, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeEquipment = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.equipment_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.remove(id, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Equipment not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
80
api.hyungi.net/controllers/factoryInfoController.js
Normal file
80
api.hyungi.net/controllers/factoryInfoController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// controllers/factoryInfoController.js
|
||||
const factoryInfoModel = require('../models/factoryInfoModel');
|
||||
|
||||
// 1. 공장 정보 생성
|
||||
exports.createFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const factoryData = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.create(factoryData, (err, insertId) =>
|
||||
err ? reject(err) : resolve(insertId)
|
||||
);
|
||||
});
|
||||
res.json({ success: true, factory_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 공장 정보 조회
|
||||
exports.getAllFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.getAll((err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getFactoryInfoById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.factory_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.getById(id, (err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'FactoryInfo not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.factory_id, 10);
|
||||
const data = { ...req.body, factory_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.update(data, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.factory_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.remove(id, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'FactoryInfo not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
55
api.hyungi.net/controllers/issueTypeController.js
Normal file
55
api.hyungi.net/controllers/issueTypeController.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const issueTypeModel = require('../models/issueTypeModel');
|
||||
|
||||
exports.createIssueType = async (req, res) => {
|
||||
try {
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.create(req.body, (err, insertId) =>
|
||||
err ? reject(err) : resolve(insertId)
|
||||
);
|
||||
});
|
||||
res.json({ success: true, issue_type_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
exports.getAllIssueTypes = async (_req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.getAll((err, data) => err ? reject(err) : resolve(data));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateIssueType = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.update(id, req.body, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Not found or no changes' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
exports.removeIssueType = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
issueTypeModel.remove(id, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
25
api.hyungi.net/controllers/pingController.js
Normal file
25
api.hyungi.net/controllers/pingController.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// controllers/pingController.js
|
||||
const { getDb } = require('../dbPool');
|
||||
const pingModel = require('../models/pingModel');
|
||||
|
||||
exports.ping = async (req, res) => {
|
||||
const data = pingModel.ping();
|
||||
try {
|
||||
// DB 연결 테스트
|
||||
const db = await getDb();
|
||||
await db.query('SELECT 1');
|
||||
return res.json({
|
||||
success: true,
|
||||
...data,
|
||||
db: 'ok'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[PING ERROR]', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'db error',
|
||||
timestamp: data.timestamp,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
127
api.hyungi.net/controllers/pipeSpecController.js
Normal file
127
api.hyungi.net/controllers/pipeSpecController.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ✅ 전체 스펙 목록 (프론트 드롭다운용 label 포함)
|
||||
exports.getAll = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT spec_id, material, diameter_in, schedule
|
||||
FROM PipeSpecs
|
||||
ORDER BY material, diameter_in
|
||||
`);
|
||||
|
||||
const result = rows.map(row => ({
|
||||
spec_id: row.spec_id,
|
||||
label: `${row.material} / ${row.diameter_in} / ${row.schedule}`
|
||||
}));
|
||||
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('[getAll 오류]', err);
|
||||
res.status(500).json({ error: '파이프 스펙 전체 조회 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 등록
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const { material, diameter_in, schedule } = req.body;
|
||||
if (!material || !diameter_in || !schedule) {
|
||||
return res.status(400).json({ error: '모든 항목이 필요합니다.' });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 중복 체크
|
||||
const [existing] = await db.query(
|
||||
`SELECT * FROM PipeSpecs WHERE material = ? AND diameter_in = ? AND schedule = ?`,
|
||||
[material.trim(), diameter_in.trim(), schedule.trim()]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(409).json({ error: '이미 등록된 스펙입니다.' });
|
||||
}
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO PipeSpecs (material, diameter_in, schedule) VALUES (?, ?, ?)`,
|
||||
[material.trim(), diameter_in.trim(), schedule.trim()]
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[create 오류]', err);
|
||||
res.status(500).json({ error: '파이프 스펙 등록 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 삭제
|
||||
exports.remove = async (req, res) => {
|
||||
const { spec_id } = req.params;
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM PipeSpecs WHERE spec_id = ?`,
|
||||
[spec_id]
|
||||
);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ error: '해당 스펙이 존재하지 않습니다.' });
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[remove 오류]', err);
|
||||
res.status(500).json({ error: '파이프 스펙 삭제 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 재질 목록
|
||||
exports.getMaterials = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT DISTINCT material FROM PipeSpecs ORDER BY material`
|
||||
);
|
||||
res.json(rows.map(row => row.material));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '재질 목록 조회 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 직경 목록 (material 기준)
|
||||
exports.getDiameters = async (req, res) => {
|
||||
const { material } = req.query;
|
||||
if (!material) {
|
||||
return res.status(400).json({ error: 'material 파라미터가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT DISTINCT diameter_in FROM PipeSpecs WHERE material = ? ORDER BY diameter_in`,
|
||||
[material]
|
||||
);
|
||||
res.json(rows.map(row => row.diameter_in));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '직경 목록 조회 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 스케줄 목록 (material + 직경 기준)
|
||||
exports.getSchedules = async (req, res) => {
|
||||
const { material, diameter_in } = req.query;
|
||||
if (!material || !diameter_in) {
|
||||
return res.status(400).json({ error: 'material과 diameter_in이 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT DISTINCT schedule FROM PipeSpecs
|
||||
WHERE material = ? AND diameter_in = ?
|
||||
ORDER BY schedule`,
|
||||
[material, diameter_in]
|
||||
);
|
||||
res.json(rows.map(row => row.schedule));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '스케줄 목록 조회 실패' });
|
||||
}
|
||||
};
|
||||
100
api.hyungi.net/controllers/processController.js
Normal file
100
api.hyungi.net/controllers/processController.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const processModel = require('../models/processModel');
|
||||
const projectModel = require('../models/projectModel');
|
||||
|
||||
// 1. 공정 등록
|
||||
exports.createProcess = async (req, res) => {
|
||||
try {
|
||||
const processData = req.body;
|
||||
|
||||
if (!processData.process_end) {
|
||||
const project = await new Promise((resolve, reject) => {
|
||||
projectModel.getById(processData.project_id, (err, row) => {
|
||||
if (err) return reject(err);
|
||||
if (!row) return reject({ status: 404, message: 'Project not found' });
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
processData.process_end = project.due_date;
|
||||
}
|
||||
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
processModel.create(processData, (err, id) => (err ? reject(err) : resolve(id)));
|
||||
});
|
||||
|
||||
res.json({ success: true, process_id: lastID });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
res.status(status).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllProcesses = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
processModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getProcessById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.process_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
processModel.getById(id, (err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Process not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateProcess = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.process_id, 10);
|
||||
const processData = { ...req.body, process_id: id };
|
||||
|
||||
if (!processData.process_end) {
|
||||
const project = await new Promise((resolve, reject) => {
|
||||
projectModel.getById(processData.project_id, (err, row) => {
|
||||
if (err) return reject(err);
|
||||
if (!row) return reject({ status: 404, message: 'Project not found' });
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
processData.process_end = project.due_date;
|
||||
}
|
||||
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
processModel.update(processData, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
res.status(status).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeProcess = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.process_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
processModel.remove(id, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Process not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
69
api.hyungi.net/controllers/projectController.js
Normal file
69
api.hyungi.net/controllers/projectController.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const projectModel = require('../models/projectModel');
|
||||
|
||||
// 1. 프로젝트 생성
|
||||
exports.createProject = async (req, res) => {
|
||||
try {
|
||||
const projectData = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
projectModel.create(projectData, (err, lastID) => (err ? reject(err) : resolve(lastID)));
|
||||
});
|
||||
res.json({ success: true, project_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllProjects = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
projectModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getProjectById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
projectModel.getById(id, (err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Project not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateProject = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
const data = { ...req.body, project_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
projectModel.update(data, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Project not found or no changes' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeProject = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.project_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
projectModel.remove(id, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Project not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
69
api.hyungi.net/controllers/taskController.js
Normal file
69
api.hyungi.net/controllers/taskController.js
Normal file
@@ -0,0 +1,69 @@
|
||||
const taskModel = require('../models/taskModel');
|
||||
|
||||
// 1. 생성
|
||||
exports.createTask = async (req, res) => {
|
||||
try {
|
||||
const taskData = req.body;
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
taskModel.create(taskData, (err, id) => (err ? reject(err) : resolve(id)));
|
||||
});
|
||||
res.json({ success: true, task_id: lastID });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllTasks = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
taskModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getTaskById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.task_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
taskModel.getById(id, (err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Task not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateTask = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.task_id, 10);
|
||||
const taskData = { ...req.body, task_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
taskModel.update(taskData, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Task not found or no change' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeTask = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.task_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
taskModel.remove(id, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Task not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
76
api.hyungi.net/controllers/toolsController.js
Normal file
76
api.hyungi.net/controllers/toolsController.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const Tools = require('../models/toolsModel');
|
||||
|
||||
// 1. 전체 도구 조회
|
||||
exports.getAll = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
Tools.getAllTools((err, data) => err ? reject(err) : resolve(data));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 단일 도구 조회
|
||||
exports.getById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
Tools.getToolById(id, (err, data) => err ? reject(err) : resolve(data));
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Tool not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 도구 생성
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const insertId = await new Promise((resolve, reject) => {
|
||||
Tools.createTool(req.body, (err, resultId) => {
|
||||
if (err) return reject(err);
|
||||
resolve(resultId);
|
||||
});
|
||||
});
|
||||
res.status(201).json({ success: true, id: insertId });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 도구 수정
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const changedRows = await new Promise((resolve, reject) => {
|
||||
Tools.updateTool(id, req.body, (err, affectedRows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changedRows === 0) return res.status(404).json({ error: 'Tool not found or no change' });
|
||||
res.json({ success: true, changes: changedRows });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 도구 삭제
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const deletedRows = await new Promise((resolve, reject) => {
|
||||
Tools.deleteTool(id, (err, affectedRows) => {
|
||||
if (err) return reject(err);
|
||||
resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (deletedRows === 0) return res.status(404).json({ error: 'Tool not found' });
|
||||
res.status(204).send();
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
26
api.hyungi.net/controllers/uploadController.js
Normal file
26
api.hyungi.net/controllers/uploadController.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const uploadModel = require('../models/uploadModel');
|
||||
|
||||
// 1. 문서 업로드
|
||||
exports.createUpload = async (req, res) => {
|
||||
try {
|
||||
const doc = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
uploadModel.create(doc, (err, insertId) => (err ? reject(err) : resolve(insertId)));
|
||||
});
|
||||
res.status(201).json({ success: true, id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 업로드 문서 조회
|
||||
exports.getUploads = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
uploadModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
372
api.hyungi.net/controllers/workAnalysisController.js
Normal file
372
api.hyungi.net/controllers/workAnalysisController.js
Normal file
@@ -0,0 +1,372 @@
|
||||
// controllers/workAnalysisController.js
|
||||
|
||||
const WorkAnalysis = require('../models/WorkAnalysis');
|
||||
const { getDb } = require('../dbPool'); // 기존 프로젝트의 DB 연결 방식 사용
|
||||
|
||||
class WorkAnalysisController {
|
||||
constructor() {
|
||||
// 메서드 바인딩
|
||||
this.getStats = this.getStats.bind(this);
|
||||
this.getDailyTrend = this.getDailyTrend.bind(this);
|
||||
this.getWorkerStats = this.getWorkerStats.bind(this);
|
||||
this.getProjectStats = this.getProjectStats.bind(this);
|
||||
this.getWorkTypeStats = this.getWorkTypeStats.bind(this);
|
||||
this.getRecentWork = this.getRecentWork.bind(this);
|
||||
this.getWeekdayPattern = this.getWeekdayPattern.bind(this);
|
||||
this.getErrorAnalysis = this.getErrorAnalysis.bind(this);
|
||||
this.getMonthlyComparison = this.getMonthlyComparison.bind(this);
|
||||
this.getWorkerSpecialization = this.getWorkerSpecialization.bind(this);
|
||||
}
|
||||
|
||||
// 날짜 유효성 검사
|
||||
validateDateRange(startDate, endDate) {
|
||||
if (!startDate || !endDate) {
|
||||
throw new Error('시작일과 종료일을 입력해주세요.');
|
||||
}
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
throw new Error('올바른 날짜 형식을 입력해주세요. (YYYY-MM-DD)');
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
throw new Error('시작일이 종료일보다 늦을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 너무 긴 기간 방지 (1년 제한)
|
||||
const diffTime = Math.abs(end - start);
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
if (diffDays > 365) {
|
||||
throw new Error('조회 기간은 1년을 초과할 수 없습니다.');
|
||||
}
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
// 기본 통계 조회
|
||||
async getStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const stats = await workAnalysis.getBasicStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: stats,
|
||||
message: '기본 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('기본 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 일별 작업시간 추이
|
||||
async getDailyTrend(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const trendData = await workAnalysis.getDailyTrend(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: trendData,
|
||||
message: '일별 추이 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('일별 추이 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 통계
|
||||
async getWorkerStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const workerStats = await workAnalysis.getWorkerStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: workerStats,
|
||||
message: '작업자별 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자별 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별 통계
|
||||
async getProjectStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const projectStats = await workAnalysis.getProjectStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: projectStats,
|
||||
message: '프로젝트별 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트별 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업유형별 통계
|
||||
async getWorkTypeStats(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const workTypeStats = await workAnalysis.getWorkTypeStats(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: workTypeStats,
|
||||
message: '작업유형별 통계 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업유형별 통계 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 작업 현황
|
||||
async getRecentWork(req, res) {
|
||||
try {
|
||||
const { start, end, limit = 10 } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
// limit 유효성 검사
|
||||
const limitNum = parseInt(limit);
|
||||
if (isNaN(limitNum) || limitNum < 1 || limitNum > 100) {
|
||||
throw new Error('limit은 1~100 사이의 숫자여야 합니다.');
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const recentWork = await workAnalysis.getRecentWork(start, end, limitNum);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: recentWork,
|
||||
message: '최근 작업 현황 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('최근 작업 현황 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 요일별 패턴 분석
|
||||
async getWeekdayPattern(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const weekdayPattern = await workAnalysis.getWeekdayPattern(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: weekdayPattern,
|
||||
message: '요일별 패턴 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('요일별 패턴 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 분석
|
||||
async getErrorAnalysis(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const errorAnalysis = await workAnalysis.getErrorAnalysis(start, end);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: errorAnalysis,
|
||||
message: '에러 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('에러 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 비교 분석
|
||||
async getMonthlyComparison(req, res) {
|
||||
try {
|
||||
const { year = new Date().getFullYear() } = req.query;
|
||||
|
||||
const yearNum = parseInt(year);
|
||||
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2050) {
|
||||
throw new Error('올바른 연도를 입력해주세요. (2000-2050)');
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const monthlyData = await workAnalysis.getMonthlyComparison(yearNum);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: monthlyData,
|
||||
message: '월별 비교 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 비교 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 전문분야 분석
|
||||
async getWorkerSpecialization(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
const specializationData = await workAnalysis.getWorkerSpecialization(start, end);
|
||||
|
||||
// 작업자별로 그룹화하여 정리
|
||||
const groupedData = specializationData.reduce((acc, item) => {
|
||||
if (!acc[item.worker_id]) {
|
||||
acc[item.worker_id] = [];
|
||||
}
|
||||
acc[item.worker_id].push({
|
||||
work_type_id: item.work_type_id,
|
||||
project_id: item.project_id,
|
||||
totalHours: item.totalHours,
|
||||
totalReports: item.totalReports,
|
||||
percentage: item.percentage
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: groupedData,
|
||||
message: '작업자별 전문분야 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자별 전문분야 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 대시보드용 종합 데이터
|
||||
async getDashboardData(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
const workAnalysis = new WorkAnalysis(db);
|
||||
|
||||
// 병렬로 여러 데이터 조회
|
||||
const [
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
] = await Promise.all([
|
||||
workAnalysis.getBasicStats(start, end),
|
||||
workAnalysis.getDailyTrend(start, end),
|
||||
workAnalysis.getWorkerStats(start, end),
|
||||
workAnalysis.getProjectStats(start, end),
|
||||
workAnalysis.getWorkTypeStats(start, end),
|
||||
workAnalysis.getRecentWork(start, end, 10)
|
||||
]);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
},
|
||||
message: '대시보드 데이터 조회 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('대시보드 데이터 조회 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WorkAnalysisController();
|
||||
134
api.hyungi.net/controllers/workReportController.js
Normal file
134
api.hyungi.net/controllers/workReportController.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// controllers/workReportController.js
|
||||
const workReportModel = require('../models/workReportModel');
|
||||
|
||||
// 1. CREATE: 단일 또는 다중 보고서 등록
|
||||
exports.createWorkReport = async (req, res) => {
|
||||
try {
|
||||
const reports = Array.isArray(req.body) ? req.body : [req.body];
|
||||
const workReport_ids = [];
|
||||
|
||||
for (const report of reports) {
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
workReportModel.create(report, (err, insertId) => {
|
||||
if (err) reject(err);
|
||||
else resolve(insertId);
|
||||
});
|
||||
});
|
||||
workReport_ids.push(id);
|
||||
}
|
||||
|
||||
res.json({ success: true, workReport_ids });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. READ BY DATE
|
||||
exports.getWorkReportsByDate = async (req, res) => {
|
||||
try {
|
||||
const { date } = req.params;
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workReportModel.getAllByDate(date, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. READ BY RANGE
|
||||
exports.getWorkReportsInRange = async (req, res) => {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workReportModel.getByRange(start, end, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. READ ONE
|
||||
exports.getWorkReportById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
workReportModel.getById(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'WorkReport not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. UPDATE
|
||||
exports.updateWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workReportModel.update(id, req.body, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 6. DELETE
|
||||
exports.removeWorkReport = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workReportModel.remove(id, (err, affectedRows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affectedRows);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'WorkReport not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 7. SUMMARY (월간)
|
||||
exports.getSummary = async (req, res) => {
|
||||
try {
|
||||
const { year, month } = req.query;
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({ error: '연도와 월이 필요합니다 (year, month)' });
|
||||
}
|
||||
|
||||
const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`;
|
||||
const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`;
|
||||
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workReportModel.getByRange(start, end, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!rows || rows.length === 0) {
|
||||
return res.status(404).json({ error: 'WorkReport not found' });
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
85
api.hyungi.net/controllers/workerController.js
Normal file
85
api.hyungi.net/controllers/workerController.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// controllers/workerController.js
|
||||
const workerModel = require('../models/workerModel');
|
||||
|
||||
// 1. 작업자 생성
|
||||
exports.createWorker = async (req, res) => {
|
||||
try {
|
||||
const workerData = req.body;
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
workerModel.create(workerData, (err, id) => {
|
||||
if (err) reject(err);
|
||||
else resolve(id);
|
||||
});
|
||||
});
|
||||
res.json({ success: true, worker_id: lastID });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 작업자 조회
|
||||
exports.getAllWorkers = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
workerModel.getAll((err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 작업자 조회
|
||||
exports.getWorkerById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
workerModel.getById(id, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Worker not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 작업자 수정
|
||||
exports.updateWorker = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
const workerData = { ...req.body, worker_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workerModel.update(workerData, (err, affected) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affected);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Worker not found or no change' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 작업자 삭제
|
||||
exports.removeWorker = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.worker_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
workerModel.remove(id, (err, affected) => {
|
||||
if (err) reject(err);
|
||||
else resolve(affected);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Worker not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
35
api.hyungi.net/db.js
Normal file
35
api.hyungi.net/db.js
Normal file
@@ -0,0 +1,35 @@
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
const retry = require('async-retry');
|
||||
|
||||
// 초기화된 pool을 export 하기 위한 변수
|
||||
let pool = null;
|
||||
|
||||
const initPool = async () => {
|
||||
if (pool) return pool; // 이미 초기화된 경우 재사용
|
||||
|
||||
await retry(async () => {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
const conn = await pool.getConnection();
|
||||
await conn.query('SET FOREIGN_KEY_CHECKS = 1');
|
||||
console.log(`✅ MariaDB 연결 성공: ${process.env.DB_HOST}:${process.env.DB_PORT || 3306}/${process.env.DB_NAME}`);
|
||||
conn.release();
|
||||
}, {
|
||||
retries: 10,
|
||||
minTimeout: 3000
|
||||
});
|
||||
|
||||
return pool;
|
||||
};
|
||||
|
||||
module.exports = initPool;
|
||||
62
api.hyungi.net/dbPool.js
Normal file
62
api.hyungi.net/dbPool.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// dbPool.js
|
||||
require('dotenv').config();
|
||||
const mysql = require('mysql2/promise');
|
||||
const retry = require('async-retry');
|
||||
|
||||
let pool = null;
|
||||
|
||||
async function initPool() {
|
||||
if (pool) return pool;
|
||||
|
||||
const {
|
||||
DB_HOST, DB_PORT, DB_USER,
|
||||
DB_PASSWORD, DB_NAME,
|
||||
DB_SOCKET, DB_CONN_LIMIT = '10'
|
||||
} = process.env;
|
||||
|
||||
if (!DB_USER || !DB_PASSWORD || !DB_NAME) {
|
||||
throw new Error('필수 환경변수(DB_USER, DB_PASSWORD, DB_NAME)가 없습니다.');
|
||||
}
|
||||
if (!DB_SOCKET && !DB_HOST) {
|
||||
throw new Error('DB_SOCKET이 없으면 DB_HOST가 반드시 필요합니다.');
|
||||
}
|
||||
|
||||
await retry(async () => {
|
||||
const config = {
|
||||
user: DB_USER,
|
||||
password: DB_PASSWORD,
|
||||
database: DB_NAME,
|
||||
waitForConnections: true,
|
||||
connectionLimit: parseInt(DB_CONN_LIMIT, 10),
|
||||
queueLimit: 0,
|
||||
charset: 'utf8mb4'
|
||||
};
|
||||
if (DB_SOCKET) {
|
||||
config.socketPath = DB_SOCKET;
|
||||
} else {
|
||||
config.host = DB_HOST;
|
||||
config.port = parseInt(DB_PORT, 10);
|
||||
}
|
||||
|
||||
pool = mysql.createPool(config);
|
||||
|
||||
// 첫 연결 검증
|
||||
const conn = await pool.getConnection();
|
||||
await conn.query('SET NAMES utf8mb4');
|
||||
conn.release();
|
||||
|
||||
console.log(`✅ MariaDB 연결 성공: ${DB_SOCKET ? `socket=${DB_SOCKET}` : `${DB_HOST}:${DB_PORT}`}`);
|
||||
}, {
|
||||
retries: 5,
|
||||
factor: 2,
|
||||
minTimeout: 1000
|
||||
});
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
async function getDb() {
|
||||
return initPool();
|
||||
}
|
||||
|
||||
module.exports = { getDb };
|
||||
103
api.hyungi.net/docker-compose.yml
Normal file
103
api.hyungi.net/docker-compose.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.9
|
||||
container_name: db_hyungi_net
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- MYSQL_DATABASE=${DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./migrations:/docker-entrypoint-initdb.d # SQL 마이그레이션 자동 실행
|
||||
ports:
|
||||
- "3306:3306" # 개발 시 외부 접속용 (운영 시 제거)
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: api_hyungi_net
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy # DB가 준비된 후 시작
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3005}:3005"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- ./public/img:/usr/src/app/public/img:ro
|
||||
- ./uploads:/usr/src/app/uploads
|
||||
- ./logs:/usr/src/app/logs # 로그 파일 저장
|
||||
- ./routes:/usr/src/app/routes
|
||||
- ./controllers:/usr/src/app/controllers
|
||||
- ./models:/usr/src/app/models
|
||||
- ./index.js:/usr/src/app/index.js
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin:latest
|
||||
container_name: pma_hyungi_net
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:80"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- PMA_HOST=${DB_HOST:-db}
|
||||
- PMA_USER=${DB_ROOT_USER:-root}
|
||||
- PMA_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- UPLOAD_LIMIT=50M
|
||||
|
||||
# Redis 캐시 서버 (선택사항 - 세션 관리 및 속도 제한용)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: redis_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-yourredispassword}
|
||||
|
||||
# Nginx 리버스 프록시 (선택사항 - HTTPS 및 로드밸런싱용)
|
||||
# nginx:
|
||||
# image: nginx:alpine
|
||||
# container_name: nginx_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# volumes:
|
||||
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
# - ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
# depends_on:
|
||||
# - api
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
external: true
|
||||
name: 7a5a13668b77b18bc1efaf1811d09560aa3be0e722d782e8460cb74f37328d81 # 기존 볼륨명으로 연결
|
||||
# redis_data: # Redis 사용 시 주석 해제
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hyungi_network
|
||||
99
api.hyungi.net/docker-compose.yml.backup
Normal file
99
api.hyungi.net/docker-compose.yml.backup
Normal file
@@ -0,0 +1,99 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.9
|
||||
container_name: db_hyungi_net
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- MYSQL_DATABASE=${DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./migrations:/docker-entrypoint-initdb.d # SQL 마이그레이션 자동 실행
|
||||
ports:
|
||||
- "3306:3306" # 개발 시 외부 접속용 (운영 시 제거)
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: api_hyungi_net
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy # DB가 준비된 후 시작
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3005}:3005"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- ./public/img:/usr/src/app/public/img:ro
|
||||
- ./uploads:/usr/src/app/uploads
|
||||
- ./logs:/usr/src/app/logs # 로그 파일 저장
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin:latest
|
||||
container_name: pma_hyungi_net
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:80"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- PMA_HOST=${DB_HOST:-db}
|
||||
- PMA_USER=${DB_ROOT_USER:-root}
|
||||
- PMA_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- UPLOAD_LIMIT=50M
|
||||
|
||||
# Redis 캐시 서버 (선택사항 - 세션 관리 및 속도 제한용)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: redis_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-yourredispassword}
|
||||
|
||||
# Nginx 리버스 프록시 (선택사항 - HTTPS 및 로드밸런싱용)
|
||||
# nginx:
|
||||
# image: nginx:alpine
|
||||
# container_name: nginx_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# volumes:
|
||||
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
# - ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
# depends_on:
|
||||
# - api
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
external: true
|
||||
name: 7a5a13668b77b18bc1efaf1811d09560aa3be0e722d782e8460cb74f37328d81 # 기존 볼륨명으로 연결
|
||||
# redis_data: # Redis 사용 시 주석 해제
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hyungi_network
|
||||
16
api.hyungi.net/ecosystem.config.js
Normal file
16
api.hyungi.net/ecosystem.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'hyungi-api',
|
||||
script: './index.js',
|
||||
env: {
|
||||
NODE_ENV: 'development'
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: 'production'
|
||||
},
|
||||
// 메모리 200MB 이상일 때 자동 재시작
|
||||
max_memory_restart: '200M'
|
||||
}
|
||||
]
|
||||
}
|
||||
2563
api.hyungi.net/hyungi.sql
Normal file
2563
api.hyungi.net/hyungi.sql
Normal file
File diff suppressed because it is too large
Load Diff
484
api.hyungi.net/index.js
Normal file
484
api.hyungi.net/index.js
Normal file
@@ -0,0 +1,484 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const app = express();
|
||||
|
||||
// ✅ Health check (맨 처음에 등록 - 모든 미들웨어보다 우선)
|
||||
app.get('/api/health', (req, res) => {
|
||||
console.log('🟢 Health check 호출됨!');
|
||||
res.status(200).json({
|
||||
status: 'healthy',
|
||||
service: 'Hyungi API',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ 보안 헤더 설정 (Helmet)
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||
connectSrc: ["'self'", "https://api.technicalkorea.com"],
|
||||
},
|
||||
},
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true
|
||||
}
|
||||
}));
|
||||
|
||||
// ✅ 요청 바디 용량 제한 확장
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
//개발용
|
||||
app.use(cors({
|
||||
origin: true, // 모든 origin 허용 (개발용)
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// ✅ CORS 설정: 허용 origin 명시 (수정된 버전)
|
||||
//app.use(cors({
|
||||
// origin: function (origin, callback) {
|
||||
// const allowedOrigins = process.env.ALLOWED_ORIGINS
|
||||
// ? process.env.ALLOWED_ORIGINS.split(',')
|
||||
// : [
|
||||
// 'http://localhost:3000',
|
||||
// 'http://localhost:3005',
|
||||
// 'http://web-ui',
|
||||
// 'http://web-ui:80',
|
||||
// 'http://web-ui:3001', // 실제 내부 포트
|
||||
// 'http://172.18.0.1',
|
||||
// 'http://172.18.0.1:3001',
|
||||
// 'http://172.18.0.2', // web-ui 컨테이너 IP
|
||||
// 'http://172.18.0.2:3001', // web-ui 컨테이너 IP:포트
|
||||
// 'http://192.168.0.3', // 나스 외부 IP (포트 없음)
|
||||
// 'http://192.168.0.3:80', // 나스 외부 접근
|
||||
// 'http://192.168.0.3:3001', // 나스 외부 접근 (실제 포트)
|
||||
// 'http://192.168.0.3:5000', // 시놀로지 기본 포트
|
||||
// 'http://192.168.0.3:5001', // 시놀로지 HTTPS 포트
|
||||
// // 추가: 더 유연한 허용
|
||||
// 'http://192.168.0.3:3000', // 다른 포트들도 허용
|
||||
// 'http://192.168.0.3:8080',
|
||||
// 'http://192.168.0.3:8000'
|
||||
// ];
|
||||
//
|
||||
// // 개발 환경에서는 모든 로컬 IP 허용
|
||||
// if (process.env.NODE_ENV === 'development' || !origin) {
|
||||
// return callback(null, true);
|
||||
// }
|
||||
//
|
||||
// // 192.168.x.x 대역 자동 허용 (시놀로지 환경)
|
||||
// if (origin && origin.match(/^http:\/\/192\.168\.\d+\.\d+(:\d+)?$/)) {
|
||||
// console.log('✅ 로컬 네트워크 IP 자동 허용:', origin);
|
||||
// return callback(null, true);
|
||||
// }
|
||||
//
|
||||
// if (allowedOrigins.includes(origin)) {
|
||||
// callback(null, true);
|
||||
// } else {
|
||||
// console.error('❌ CORS 차단됨:', origin);
|
||||
// console.log('허용된 Origins:', allowedOrigins);
|
||||
// callback(new Error('CORS 차단됨: ' + origin));
|
||||
// }
|
||||
// },
|
||||
// credentials: true
|
||||
// }));
|
||||
|
||||
// ✅ 신뢰할 수 있는 프록시 설정 (IP 주소 정확히 가져오기)
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// ✅ API 속도 제한 설정
|
||||
// 일반 API 속도 제한
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15분
|
||||
max: process.env.RATE_LIMIT_MAX_REQUESTS || 100,
|
||||
message: 'API 요청 한도를 초과했습니다. 잠시 후 다시 시도하세요.',
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// 로그인 API 속도 제한 (더 엄격하게)
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15분
|
||||
max: process.env.LOGIN_RATE_LIMIT_MAX_REQUESTS || 5,
|
||||
message: '너무 많은 로그인 시도입니다. 15분 후에 다시 시도하세요.',
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true, // 성공한 요청은 카운트하지 않음
|
||||
});
|
||||
|
||||
// ✅ 라우터 등록
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
const projectRoutes = require('./routes/projectRoutes');
|
||||
const workerRoutes = require('./routes/workerRoutes');
|
||||
const taskRoutes = require('./routes/taskRoutes');
|
||||
const processRoutes = require('./routes/processRoutes');
|
||||
const workReportRoutes = require('./routes/workReportRoutes');
|
||||
const cuttingPlanRoutes = require('./routes/cuttingPlanRoutes');
|
||||
const factoryInfoRoutes = require('./routes/factoryInfoRoutes');
|
||||
const equipmentListRoutes = require('./routes/equipmentListRoutes');
|
||||
const toolsRoute = require('./routes/toolsRoute');
|
||||
const uploadRoutes = require('./routes/uploadRoutes');
|
||||
const uploadBgRoutes = require('./routes/uploadBgRoutes');
|
||||
const dailyIssueReportRoutes = require('./routes/dailyIssueReportRoutes');
|
||||
const issueTypeRoutes = require('./routes/issueTypeRoutes');
|
||||
const healthRoutes = require('./routes/healthRoutes');
|
||||
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
||||
const dailyWorkReportRoutes = require('./routes/dailyWorkReportRoutes');
|
||||
const workAnalysisRoutes = require('./routes/workAnalysisRoutes');
|
||||
|
||||
// 🔒 인증 미들웨어 가져오기
|
||||
const { verifyToken } = require('./middlewares/authMiddleware');
|
||||
|
||||
// ahn.hyungi.net 배포용
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// ✅ 업로드된 파일 정적 라우팅 추가 (웹에서 이미지 접근 가능하게)
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// 🔒 활동 로깅 미들웨어
|
||||
const activityLogger = (req, res, next) => {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const logData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
status: res.statusCode,
|
||||
duration: duration + 'ms',
|
||||
ip: req.ip,
|
||||
user: req.user?.username || 'anonymous'
|
||||
};
|
||||
|
||||
// 성공/실패에 따른 로그 레벨 분기
|
||||
if (res.statusCode >= 400) {
|
||||
console.error('[API Error]', logData);
|
||||
} else if (res.statusCode >= 300) {
|
||||
console.warn('[API Redirect]', logData);
|
||||
} else {
|
||||
console.log('[API Access]', logData);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// ===== 📋 미들웨어 적용 순서 수정 (🔥 핵심 수정 부분) =====
|
||||
|
||||
// 모든 API 요청에 활동 로거 적용
|
||||
app.use('/api/*', activityLogger);
|
||||
|
||||
// 🔓 인증이 필요 없는 경로들을 먼저 등록 (순서 중요!)
|
||||
|
||||
// Health check는 이미 맨 위에서 등록됨
|
||||
|
||||
// 🔓 로그인 관련 경로들 (인증 없이 접근 가능)
|
||||
// 로그인 엔드포인트에 특별한 속도 제한 적용
|
||||
app.post('/api/auth/login', loginLimiter, (req, res, next) => {
|
||||
console.log('🔓 로그인 요청 받음:', req.body.username);
|
||||
authRoutes.handle(req, res, next);
|
||||
});
|
||||
|
||||
// 기타 공개 인증 엔드포인트들
|
||||
app.use('/api/auth/refresh-token', loginLimiter);
|
||||
app.use('/api/auth/check-password-strength', loginLimiter);
|
||||
|
||||
// 나머지 인증 라우트
|
||||
app.use('/api/auth', authRoutes);
|
||||
|
||||
// 🔒 일반 API 속도 제한 적용
|
||||
app.use('/api/', apiLimiter);
|
||||
|
||||
// 🔒 인증이 필요한 모든 API에 대해 토큰 검증 (수정된 버전)
|
||||
app.use('/api/*', (req, res, next) => {
|
||||
console.log(`🔍 API 요청: ${req.method} ${req.originalUrl}`);
|
||||
|
||||
// 🔓 인증이 필요 없는 경로들은 통과 (정확한 매칭)
|
||||
const publicPaths = [
|
||||
'/api/auth/login',
|
||||
'/api/auth/refresh-token',
|
||||
'/api/auth/check-password-strength',
|
||||
'/api/health'
|
||||
];
|
||||
|
||||
// 정확한 경로 매칭 확인
|
||||
const isPublicPath = publicPaths.some(path => {
|
||||
// 정확한 경로 또는 쿼리 파라미터가 있는 경우
|
||||
const isMatch = req.originalUrl === path ||
|
||||
req.originalUrl.startsWith(path + '?') ||
|
||||
req.originalUrl.startsWith(path + '/');
|
||||
if (isMatch) {
|
||||
console.log(`🔓 Public path 허용: ${req.originalUrl}`);
|
||||
}
|
||||
return isMatch;
|
||||
});
|
||||
|
||||
if (isPublicPath) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 나머지는 모두 인증 필요
|
||||
console.log(`🔒 인증 필요한 경로: ${req.originalUrl}`);
|
||||
verifyToken(req, res, next);
|
||||
});
|
||||
|
||||
// ===== 📊 모든 라우트 등록 (인증된 사용자만) =====
|
||||
|
||||
// 📝 일반 기능들
|
||||
app.use('/api/issue-reports', dailyIssueReportRoutes);
|
||||
app.use('/api/issue-types', issueTypeRoutes);
|
||||
|
||||
// 👥 기본 데이터들 (모든 인증된 사용자)
|
||||
app.use('/api/workers', workerRoutes);
|
||||
app.use('/api/daily-work-reports', dailyWorkReportRoutes);
|
||||
app.use('/api/work-analysis', workAnalysisRoutes);
|
||||
|
||||
// 📊 리포트 및 분석
|
||||
app.use('/api/workreports', workReportRoutes);
|
||||
app.use('/api/uploads', uploadRoutes);
|
||||
|
||||
// ⚙️ 시스템 데이터들 (모든 인증된 사용자)
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/processes', processRoutes);
|
||||
app.use('/api/cuttingplans', cuttingPlanRoutes);
|
||||
app.use('/api/factoryinfo', factoryInfoRoutes);
|
||||
app.use('/api/equipment', equipmentListRoutes);
|
||||
app.use('/api/tools', toolsRoute);
|
||||
app.use('/api/pipespecs', pipeSpecRoutes);
|
||||
|
||||
// 📤 파일 업로드
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
|
||||
// ===== 🔍 API 정보 엔드포인트 =====
|
||||
app.get('/api', (req, res) => {
|
||||
res.json({
|
||||
name: 'Technical Korea Work Management API',
|
||||
version: '2.1.0',
|
||||
description: '보안이 강화된 생산관리 시스템 API',
|
||||
timestamp: new Date().toISOString(),
|
||||
security: {
|
||||
authentication: 'JWT Bearer Token',
|
||||
rateLimit: {
|
||||
general: '100 requests per 15 minutes',
|
||||
login: '5 attempts per 15 minutes'
|
||||
},
|
||||
cors: 'Configured for specific origins',
|
||||
headers: 'Security headers enabled (Helmet)'
|
||||
},
|
||||
user: {
|
||||
username: req.user?.username || 'anonymous',
|
||||
access_level: req.user?.access_level || 'none',
|
||||
worker_id: req.user?.worker_id || null
|
||||
},
|
||||
endpoints: {
|
||||
auth: {
|
||||
login: 'POST /api/auth/login',
|
||||
logout: 'POST /api/auth/logout',
|
||||
refreshToken: 'POST /api/auth/refresh-token',
|
||||
changePassword: 'POST /api/auth/change-password',
|
||||
adminChangePassword: 'POST /api/auth/admin/change-password',
|
||||
checkPasswordStrength: 'POST /api/auth/check-password-strength',
|
||||
me: 'GET /api/auth/me',
|
||||
users: 'GET /api/auth/users',
|
||||
register: 'POST /api/auth/register',
|
||||
updateUser: 'PUT /api/auth/users/:id',
|
||||
deleteUser: 'DELETE /api/auth/users/:id',
|
||||
loginHistory: 'GET /api/auth/login-history'
|
||||
},
|
||||
dailyWorkReports: {
|
||||
workTypes: 'GET /api/daily-work-reports/work-types',
|
||||
workStatusTypes: 'GET /api/daily-work-reports/work-status-types',
|
||||
errorTypes: 'GET /api/daily-work-reports/error-types',
|
||||
create: 'POST /api/daily-work-reports',
|
||||
search: 'GET /api/daily-work-reports/search',
|
||||
summary: 'GET /api/daily-work-reports/summary',
|
||||
byDate: 'GET /api/daily-work-reports/date/:date',
|
||||
update: 'PUT /api/daily-work-reports/:id',
|
||||
delete: 'DELETE /api/daily-work-reports/:id'
|
||||
},
|
||||
workAnalysis: {
|
||||
stats: 'GET /api/work-analysis/stats?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
dailyTrend: 'GET /api/work-analysis/daily-trend?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
workerStats: 'GET /api/work-analysis/worker-stats?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
projectStats: 'GET /api/work-analysis/project-stats?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
workTypeStats: 'GET /api/work-analysis/work-type-stats?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
recentWork: 'GET /api/work-analysis/recent-work?start=YYYY-MM-DD&end=YYYY-MM-DD&limit=10',
|
||||
weekdayPattern: 'GET /api/work-analysis/weekday-pattern?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
errorAnalysis: 'GET /api/work-analysis/error-analysis?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
monthlyComparison: 'GET /api/work-analysis/monthly-comparison?year=YYYY',
|
||||
workerSpecialization: 'GET /api/work-analysis/worker-specialization?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
dashboard: 'GET /api/work-analysis/dashboard?start=YYYY-MM-DD&end=YYYY-MM-DD',
|
||||
health: 'GET /api/work-analysis/health'
|
||||
},
|
||||
workers: 'GET/POST/PUT/DELETE /api/workers',
|
||||
projects: 'GET/POST/PUT/DELETE /api/projects',
|
||||
issues: 'GET/POST/PUT/DELETE /api/issue-reports',
|
||||
reports: 'GET /api/workreports',
|
||||
uploads: 'POST /api/uploads'
|
||||
},
|
||||
note: '모든 API는 로그인 후 접근 가능합니다. 자세한 API 문서는 관리자에게 문의하세요.'
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 🏠 메인 페이지 라우트 =====
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
// ✅ 서버 실행
|
||||
const PORT = process.env.PORT || 3005;
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`
|
||||
🚀 Technical Korea Work Management System v2.1.0
|
||||
📍 서버가 포트 ${PORT}에서 실행 중입니다.
|
||||
🌐 접속 URL: http://localhost:${PORT}
|
||||
📊 API 문서: http://localhost:${PORT}/api
|
||||
|
||||
🔒 보안 기능:
|
||||
✅ JWT 토큰 인증
|
||||
✅ 로그인 실패 제한 (5회)
|
||||
✅ API 속도 제한
|
||||
✅ 보안 헤더 (Helmet)
|
||||
✅ CORS 설정 (192.168.0.3:3001 허용)
|
||||
✅ 활동 로깅
|
||||
|
||||
📋 새로운 기능:
|
||||
🔐 비밀번호 변경 (본인/관리자)
|
||||
🔄 토큰 갱신 (Refresh Token)
|
||||
📊 로그인 이력 조회
|
||||
💪 비밀번호 강도 체크
|
||||
`);
|
||||
}).on('error', (err) => {
|
||||
console.error('❌ 서버 실행 중 오류 발생:', err);
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`포트 ${PORT}이(가) 이미 사용 중입니다.`);
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 🚨 에러 핸들링 =====
|
||||
|
||||
// 404 핸들러
|
||||
app.use((req, res) => {
|
||||
console.log(`[404] ${req.method} ${req.originalUrl} - IP: ${req.ip}`);
|
||||
|
||||
if (req.originalUrl.startsWith('/api/')) {
|
||||
res.status(404).json({
|
||||
error: 'API 엔드포인트를 찾을 수 없습니다.',
|
||||
path: req.originalUrl,
|
||||
available: '/api',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
error: '요청하신 페이지를 찾을 수 없습니다.',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 전역 에러 핸들러
|
||||
app.use((err, req, res, next) => {
|
||||
const errorId = Date.now().toString(36);
|
||||
|
||||
console.error(`[ERROR ${errorId}] ${new Date().toISOString()}:`, {
|
||||
message: err.message,
|
||||
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
|
||||
url: req.originalUrl,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
user: req.user?.username || 'anonymous'
|
||||
});
|
||||
|
||||
// CORS 에러
|
||||
// if (err.message && err.message.includes('CORS 차단됨')) {
|
||||
// return res.status(403).json({
|
||||
// error: 'CORS 정책에 의해 차단되었습니다.',
|
||||
// message: 'API 접근이 허용되지 않은 도메인입니다.',
|
||||
// errorId
|
||||
// });
|
||||
// }
|
||||
|
||||
// JWT 에러
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
error: '유효하지 않은 토큰입니다.',
|
||||
errorId
|
||||
});
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
error: '토큰이 만료되었습니다. 다시 로그인해주세요.',
|
||||
errorId
|
||||
});
|
||||
}
|
||||
|
||||
// 요청 크기 초과
|
||||
if (err.type === 'entity.too.large') {
|
||||
return res.status(413).json({
|
||||
error: '요청 크기가 너무 큽니다. 50MB 이하로 줄여주세요.',
|
||||
errorId
|
||||
});
|
||||
}
|
||||
|
||||
// 일반 서버 에러
|
||||
res.status(err.status || 500).json({
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : '관리자에게 문의하세요.',
|
||||
errorId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 🔄 Graceful Shutdown =====
|
||||
const gracefulShutdown = () => {
|
||||
console.log('\n🛑 서버 종료 신호를 받았습니다...');
|
||||
|
||||
server.close(() => {
|
||||
console.log('✅ HTTP 서버가 정상적으로 종료되었습니다.');
|
||||
|
||||
// DB 연결 종료 등 추가 정리 작업
|
||||
// 예: db.end(), redis.quit() 등
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 30초 후 강제 종료
|
||||
setTimeout(() => {
|
||||
console.error('❌ 정상 종료 실패, 강제 종료합니다.');
|
||||
process.exit(1);
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
|
||||
// 처리되지 않은 Promise 거부
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('처리되지 않은 Promise 거부:', reason);
|
||||
// 개발 환경에서는 크래시, 프로덕션에서는 로그만
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// 처리되지 않은 예외
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('처리되지 않은 예외:', error);
|
||||
gracefulShutdown();
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
79
api.hyungi.net/index.js.backup
Normal file
79
api.hyungi.net/index.js.backup
Normal file
@@ -0,0 +1,79 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const app = express();
|
||||
|
||||
// ✅ 요청 바디 용량 제한 확장
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// ✅ CORS 설정: 허용 origin 명시
|
||||
app.use(cors({
|
||||
origin: function (origin, callback) {
|
||||
const allowedOrigins = [
|
||||
'https://ahn.hyungi.net',
|
||||
'https://tech.hyungi.net',
|
||||
'https://pdf.hyungi.net'
|
||||
];
|
||||
if (!origin || allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('CORS 차단됨: ' + origin));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// ✅ 라우터 등록
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
const projectRoutes = require('./routes/projectRoutes');
|
||||
const workerRoutes = require('./routes/workerRoutes');
|
||||
const taskRoutes = require('./routes/taskRoutes');
|
||||
const processRoutes = require('./routes/processRoutes');
|
||||
const workReportRoutes = require('./routes/workReportRoutes');
|
||||
const cuttingPlanRoutes = require('./routes/cuttingPlanRoutes');
|
||||
const factoryInfoRoutes = require('./routes/factoryInfoRoutes');
|
||||
const equipmentListRoutes = require('./routes/equipmentListRoutes');
|
||||
const toolsRoute = require('./routes/toolsRoute');
|
||||
const uploadRoutes = require('./routes/uploadRoutes');
|
||||
const uploadBgRoutes = require('./routes/uploadBgRoutes');
|
||||
const dailyIssueReportRoutes = require('./routes/dailyIssueReportRoutes');
|
||||
const issueTypeRoutes = require('./routes/issueTypeRoutes');
|
||||
const healthRoutes = require('./routes/healthRoutes');
|
||||
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
||||
|
||||
// ahn.hyungi.net 배포용
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// ✅ 업로드된 파일 정적 라우팅 추가 (웹에서 이미지 접근 가능하게)
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// ✅ 각 라우트 등록
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/workers', workerRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/processes', processRoutes);
|
||||
app.use('/api/workreports', workReportRoutes);
|
||||
app.use('/api/cuttingplans', cuttingPlanRoutes);
|
||||
app.use('/api/factoryinfo', factoryInfoRoutes);
|
||||
app.use('/api/equipment', equipmentListRoutes);
|
||||
app.use('/api/tools', toolsRoute);
|
||||
app.use('/api/uploads', uploadRoutes);
|
||||
app.use('/api', uploadBgRoutes); // ✅ upload-bg 경로용
|
||||
app.use('/api/issue-reports', dailyIssueReportRoutes);
|
||||
app.use('/api/issue-types', issueTypeRoutes);
|
||||
app.use('/api', healthRoutes);
|
||||
app.use('/api/pipespecs', pipeSpecRoutes);
|
||||
|
||||
// ✅ 서버 실행
|
||||
const PORT = process.env.PORT || 3005;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 서버가 ${PORT}번 포트에서 실행 중...`);
|
||||
}).on('error', (err) => {
|
||||
console.error('❌ 서버 실행 중 오류 발생:', err);
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: '존재하지 않는 경로입니다.' });
|
||||
});
|
||||
9
api.hyungi.net/middlewares/access.js
Normal file
9
api.hyungi.net/middlewares/access.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// utils/access.js
|
||||
exports.requireAccess = (...allowed) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user || !allowed.includes(req.user.access_level)) {
|
||||
return res.status(403).json({ error: '접근 권한이 없습니다' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
33
api.hyungi.net/middlewares/accessMiddleware.js
Normal file
33
api.hyungi.net/middlewares/accessMiddleware.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// middlewares/accessMiddleware.js
|
||||
|
||||
// 권한 레벨 정의
|
||||
const ACCESS_LEVELS = {
|
||||
worker: 1,
|
||||
group_leader: 2,
|
||||
support_team: 3,
|
||||
admin: 4,
|
||||
system: 5
|
||||
};
|
||||
|
||||
const requireAccess = (requiredLevel) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: '인증이 필요합니다.' });
|
||||
}
|
||||
|
||||
const userLevel = ACCESS_LEVELS[req.user.access_level] || 0;
|
||||
const required = ACCESS_LEVELS[requiredLevel] || 999;
|
||||
|
||||
if (userLevel < required) {
|
||||
return res.status(403).json({
|
||||
error: '접근 권한이 없습니다.',
|
||||
required: requiredLevel,
|
||||
current: req.user.access_level
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { requireAccess, ACCESS_LEVELS };
|
||||
20
api.hyungi.net/middlewares/auth.js
Normal file
20
api.hyungi.net/middlewares/auth.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// 📁 middlewares/auth.js
|
||||
const jwt = require('jsonwebtoken');
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader?.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded; // 다른 미들웨어에서 사용할 수 있게 설정
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(403).json({ message: 'Invalid token' });
|
||||
}
|
||||
};
|
||||
22
api.hyungi.net/middlewares/authMiddleware.js
Normal file
22
api.hyungi.net/middlewares/authMiddleware.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
exports.verifyToken = (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({ error: '토큰 없음' });
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: '토큰 누락' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next(); // ✅ 반드시 next 호출
|
||||
} catch (err) {
|
||||
console.error('[verifyToken 오류]', err.message);
|
||||
return res.status(403).json({ error: '토큰 검증 실패', detail: err.message });
|
||||
}
|
||||
};
|
||||
16
api.hyungi.net/middlewares/errorHandler.js
Normal file
16
api.hyungi.net/middlewares/errorHandler.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// middlewares/errorHandler.js
|
||||
exports.errorHandler = (err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
res.status(500).json({
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
details: err.message,
|
||||
stack: err.stack
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
};
|
||||
115
api.hyungi.net/migrations/002_add_master_tables.sql
Normal file
115
api.hyungi.net/migrations/002_add_master_tables.sql
Normal file
@@ -0,0 +1,115 @@
|
||||
-- migrations/002_add_master_tables.sql
|
||||
-- 기존 daily_work_reports 테이블을 유지하면서 필요한 마스터 테이블들만 추가
|
||||
|
||||
-- 1. 작업 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS work_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL COMMENT '작업 유형명',
|
||||
description TEXT COMMENT '작업 유형 설명',
|
||||
category VARCHAR(50) COMMENT '작업 카테고리',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. 업무 상태 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS work_status_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(50) NOT NULL COMMENT '상태명',
|
||||
description TEXT COMMENT '상태 설명',
|
||||
is_error BOOLEAN DEFAULT FALSE COMMENT '에러 상태 여부',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 3. 에러 유형 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS error_types (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL COMMENT '에러 유형명',
|
||||
description TEXT COMMENT '에러 설명',
|
||||
severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium' COMMENT '심각도',
|
||||
solution_guide TEXT COMMENT '해결 가이드',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 4. 감사 로그 테이블 생성 (누적입력 추적용)
|
||||
CREATE TABLE IF NOT EXISTS work_report_audit_log (
|
||||
log_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
action ENUM('ADD_ACCUMULATE', 'DELETE_SINGLE', 'UPDATE', 'DELETE', 'CREATE', 'DELETE_BATCH') NOT NULL COMMENT '작업 유형',
|
||||
report_id INT NULL COMMENT '관련 보고서 ID',
|
||||
old_values JSON NULL COMMENT '변경 전 값',
|
||||
new_values JSON NULL COMMENT '변경 후 값',
|
||||
changed_by INT NOT NULL COMMENT '변경자 ID',
|
||||
change_reason VARCHAR(500) COMMENT '변경 사유',
|
||||
ip_address VARCHAR(45) COMMENT 'IP 주소',
|
||||
user_agent TEXT COMMENT '사용자 에이전트',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '변경 시간',
|
||||
|
||||
INDEX idx_action_date (action, created_at),
|
||||
INDEX idx_changed_by (changed_by),
|
||||
INDEX idx_report_id (report_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 5. 기존 daily_work_reports 테이블에 누적입력을 위한 인덱스만 추가
|
||||
ALTER TABLE daily_work_reports
|
||||
ADD INDEX IF NOT EXISTS idx_created_by (created_by),
|
||||
ADD INDEX IF NOT EXISTS idx_date_worker_creator (report_date, worker_id, created_by);
|
||||
|
||||
-- 6. 기본 데이터 삽입 (기존 데이터와 호환되도록)
|
||||
|
||||
-- 업무 상태 유형 기본 데이터
|
||||
INSERT IGNORE INTO work_status_types (id, name, description, is_error) VALUES
|
||||
(1, '정규', '정상적으로 완료된 작업', FALSE),
|
||||
(2, '에러', '오류가 발생한 작업', TRUE);
|
||||
|
||||
-- 작업 유형 기본 데이터 (기존 데이터의 work_type_id=1과 호환)
|
||||
INSERT IGNORE INTO work_types (id, name, description, category) VALUES
|
||||
(1, '일반작업', '기본 작업 유형', '생산관리'),
|
||||
(2, '생산', '제품 생산 작업', '생산관리'),
|
||||
(3, '품질검사', '제품 품질 검사', '품질관리'),
|
||||
(4, '안전점검', '안전 상태 점검', '안전관리'),
|
||||
(5, '자재입고', '원자재 입고 작업', '구매관리'),
|
||||
(6, '설비점검', '생산 설비 점검', '설비관리'),
|
||||
(7, '재고관리', '재고 현황 관리', '창고관리'),
|
||||
(8, '포장', '제품 포장 작업', '생산관리'),
|
||||
(9, '출하', '제품 출하 작업', '물류관리');
|
||||
|
||||
-- 에러 유형 기본 데이터
|
||||
INSERT IGNORE INTO error_types (id, name, description, severity, solution_guide) VALUES
|
||||
(1, '설비고장', '생산 설비 고장', 'high', '즉시 설비팀에 연락하여 수리 요청'),
|
||||
(2, '자재부족', '필요 자재 부족', 'medium', '구매팀에 긴급 주문 요청'),
|
||||
(3, '품질불량', '제품 품질 기준 미달', 'high', '품질팀에 즉시 보고 및 생산 중단'),
|
||||
(4, '안전사고', '작업 중 안전사고 발생', 'critical', '즉시 작업 중단 및 안전팀 신고'),
|
||||
(5, '시스템오류', 'IT 시스템 오류', 'medium', 'IT팀에 장애 신고');
|
||||
|
||||
-- 7. 기존 데이터 호환성을 위한 뷰 생성
|
||||
CREATE OR REPLACE VIEW v_daily_reports_with_names AS
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.report_date,
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
dwr.work_type_id,
|
||||
COALESCE(wt.name, '일반작업') as work_type_name,
|
||||
COALESCE(wt.category, '생산관리') as work_category,
|
||||
dwr.work_status_id,
|
||||
COALESCE(wst.name, '정규') as work_status_name,
|
||||
COALESCE(wst.is_error, FALSE) as is_error,
|
||||
dwr.error_type_id,
|
||||
et.name as error_type_name,
|
||||
et.severity as error_severity,
|
||||
dwr.work_hours,
|
||||
dwr.created_by,
|
||||
u.name as created_by_name,
|
||||
dwr.created_at,
|
||||
dwr.updated_at
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id;
|
||||
|
||||
COMMIT;
|
||||
430
api.hyungi.net/models/WorkAnalysis.js
Normal file
430
api.hyungi.net/models/WorkAnalysis.js
Normal file
@@ -0,0 +1,430 @@
|
||||
// models/WorkAnalysis.js - 향상된 버전
|
||||
|
||||
class WorkAnalysis {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
// 기본 통계 조회
|
||||
async getBasicStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(SUM(work_hours), 0) as total_hours,
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(DISTINCT project_id) as active_projects,
|
||||
COUNT(DISTINCT worker_id) as active_workers,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_reports,
|
||||
ROUND(AVG(work_hours), 2) as avg_hours_per_report
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
const stats = results[0];
|
||||
|
||||
const errorRate = stats.total_reports > 0
|
||||
? (stats.error_reports / stats.total_reports) * 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalHours: parseFloat(stats.total_hours) || 0,
|
||||
totalReports: parseInt(stats.total_reports) || 0,
|
||||
activeProjects: parseInt(stats.active_projects) || 0,
|
||||
activeWorkers: parseInt(stats.active_workers) || 0,
|
||||
errorRate: parseFloat(errorRate.toFixed(2)) || 0,
|
||||
avgHoursPerReport: parseFloat(stats.avg_hours_per_report) || 0
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`기본 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 일별 작업시간 추이
|
||||
async getDailyTrend(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
report_date as date,
|
||||
SUM(work_hours) as hours,
|
||||
COUNT(*) as reports,
|
||||
COUNT(DISTINCT worker_id) as workers,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as errors
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
GROUP BY report_date
|
||||
ORDER BY report_date
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
date: row.date,
|
||||
hours: parseFloat(row.hours) || 0,
|
||||
reports: parseInt(row.reports) || 0,
|
||||
workers: parseInt(row.workers) || 0,
|
||||
errors: parseInt(row.errors) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`일별 추이 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 통계
|
||||
async getWorkerStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
ROUND(AVG(dwr.work_hours), 2) as avgHours,
|
||||
COUNT(DISTINCT dwr.project_id) as projectCount,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
|
||||
COUNT(DISTINCT dwr.report_date) as workingDays
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.worker_id, w.worker_name
|
||||
ORDER BY totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
worker_id: row.worker_id,
|
||||
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
avgHours: parseFloat(row.avgHours) || 0,
|
||||
projectCount: parseInt(row.projectCount) || 0,
|
||||
errorCount: parseInt(row.errorCount) || 0,
|
||||
workingDays: parseInt(row.workingDays) || 0,
|
||||
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`작업자별 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별 통계
|
||||
async getProjectStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
COUNT(DISTINCT dwr.worker_id) as workerCount,
|
||||
ROUND(AVG(dwr.work_hours), 2) as avgHours,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
|
||||
COUNT(DISTINCT dwr.report_date) as activeDays
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.project_id, p.project_name
|
||||
ORDER BY totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name || `프로젝트 ${row.project_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
workerCount: parseInt(row.workerCount) || 0,
|
||||
avgHours: parseFloat(row.avgHours) || 0,
|
||||
errorCount: parseInt(row.errorCount) || 0,
|
||||
activeDays: parseInt(row.activeDays) || 0,
|
||||
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`프로젝트별 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업유형별 통계
|
||||
async getWorkTypeStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.work_type_id,
|
||||
wt.name as work_type_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
ROUND(AVG(dwr.work_hours), 2) as avgHours,
|
||||
COUNT(DISTINCT dwr.worker_id) as workerCount,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
|
||||
COUNT(DISTINCT dwr.project_id) as projectCount
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.work_type_id, wt.name
|
||||
ORDER BY totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
avgHours: parseFloat(row.avgHours) || 0,
|
||||
workerCount: parseInt(row.workerCount) || 0,
|
||||
errorCount: parseInt(row.errorCount) || 0,
|
||||
projectCount: parseInt(row.projectCount) || 0,
|
||||
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`작업유형별 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 작업 현황
|
||||
async getRecentWork(startDate, endDate, limit = 50) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.report_date,
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
dwr.work_type_id,
|
||||
wt.name as work_type_name,
|
||||
dwr.work_status_id,
|
||||
wst.name as work_status_name,
|
||||
dwr.error_type_id,
|
||||
et.name as error_type_name,
|
||||
dwr.work_hours,
|
||||
dwr.created_by,
|
||||
u.name as created_by_name,
|
||||
dwr.created_at
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
ORDER BY dwr.created_at DESC
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate, parseInt(limit)]);
|
||||
return results.map(row => ({
|
||||
id: row.id,
|
||||
report_date: row.report_date,
|
||||
worker_id: row.worker_id,
|
||||
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name || `프로젝트 ${row.project_id}`,
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
||||
work_status_id: row.work_status_id,
|
||||
work_status_name: row.work_status_name || '정상',
|
||||
error_type_id: row.error_type_id,
|
||||
error_type_name: row.error_type_name || null,
|
||||
work_hours: parseFloat(row.work_hours) || 0,
|
||||
created_by: row.created_by,
|
||||
created_by_name: row.created_by_name || '미지정',
|
||||
created_at: row.created_at
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`최근 작업 현황 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 요일별 패턴 분석
|
||||
async getWeekdayPattern(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
DAYOFWEEK(report_date) as day_of_week,
|
||||
CASE DAYOFWEEK(report_date)
|
||||
WHEN 1 THEN '일요일'
|
||||
WHEN 2 THEN '월요일'
|
||||
WHEN 3 THEN '화요일'
|
||||
WHEN 4 THEN '수요일'
|
||||
WHEN 5 THEN '목요일'
|
||||
WHEN 6 THEN '금요일'
|
||||
WHEN 7 THEN '토요일'
|
||||
END as day_name,
|
||||
SUM(work_hours) as total_hours,
|
||||
COUNT(*) as total_reports,
|
||||
ROUND(AVG(work_hours), 2) as avg_hours,
|
||||
COUNT(DISTINCT worker_id) as active_workers
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
GROUP BY DAYOFWEEK(report_date)
|
||||
ORDER BY day_of_week
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
dayOfWeek: row.day_of_week,
|
||||
dayName: row.day_name,
|
||||
totalHours: parseFloat(row.total_hours) || 0,
|
||||
totalReports: parseInt(row.total_reports) || 0,
|
||||
avgHours: parseFloat(row.avg_hours) || 0,
|
||||
activeWorkers: parseInt(row.active_workers) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`요일별 패턴 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 분석
|
||||
async getErrorAnalysis(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.error_type_id,
|
||||
et.name as error_type_name,
|
||||
COUNT(*) as error_count,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.worker_id) as affected_workers,
|
||||
COUNT(DISTINCT dwr.project_id) as affected_projects
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
AND dwr.work_status_id = 2
|
||||
GROUP BY dwr.error_type_id, et.name
|
||||
ORDER BY error_count DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
error_type_id: row.error_type_id,
|
||||
error_type_name: row.error_type_name || `에러유형 ${row.error_type_id}`,
|
||||
errorCount: parseInt(row.error_count) || 0,
|
||||
totalHours: parseFloat(row.total_hours) || 0,
|
||||
affectedWorkers: parseInt(row.affected_workers) || 0,
|
||||
affectedProjects: parseInt(row.affected_projects) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`에러 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 비교 분석
|
||||
async getMonthlyComparison(year) {
|
||||
const query = `
|
||||
SELECT
|
||||
MONTH(report_date) as month,
|
||||
MONTHNAME(report_date) as month_name,
|
||||
SUM(work_hours) as total_hours,
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(DISTINCT worker_id) as active_workers,
|
||||
COUNT(DISTINCT project_id) as active_projects,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count
|
||||
FROM daily_work_reports
|
||||
WHERE YEAR(report_date) = ?
|
||||
GROUP BY MONTH(report_date), MONTHNAME(report_date)
|
||||
ORDER BY month
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [year]);
|
||||
return results.map(row => ({
|
||||
month: row.month,
|
||||
monthName: row.month_name,
|
||||
totalHours: parseFloat(row.total_hours) || 0,
|
||||
totalReports: parseInt(row.total_reports) || 0,
|
||||
activeWorkers: parseInt(row.active_workers) || 0,
|
||||
activeProjects: parseInt(row.active_projects) || 0,
|
||||
errorCount: parseInt(row.error_count) || 0,
|
||||
errorRate: row.total_reports > 0 ? parseFloat(((row.error_count / row.total_reports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`월별 비교 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 전문분야 분석
|
||||
async getWorkerSpecialization(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
dwr.work_type_id,
|
||||
wt.name as work_type_name,
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
ROUND((SUM(dwr.work_hours) / (
|
||||
SELECT SUM(work_hours)
|
||||
FROM daily_work_reports
|
||||
WHERE worker_id = dwr.worker_id
|
||||
AND report_date BETWEEN ? AND ?
|
||||
)) * 100, 2) as percentage
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.worker_id, w.worker_name, dwr.work_type_id, wt.name, dwr.project_id, p.project_name
|
||||
HAVING totalHours > 0
|
||||
ORDER BY dwr.worker_id, totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate, startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
worker_id: row.worker_id,
|
||||
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name || `프로젝트 ${row.project_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
percentage: parseFloat(row.percentage) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`작업자별 전문분야 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 대시보드용 종합 데이터
|
||||
async getDashboardData(startDate, endDate) {
|
||||
try {
|
||||
// 병렬로 모든 데이터 조회
|
||||
const [
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
] = await Promise.all([
|
||||
this.getBasicStats(startDate, endDate),
|
||||
this.getDailyTrend(startDate, endDate),
|
||||
this.getWorkerStats(startDate, endDate),
|
||||
this.getProjectStats(startDate, endDate),
|
||||
this.getWorkTypeStats(startDate, endDate),
|
||||
this.getRecentWork(startDate, endDate, 20)
|
||||
]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork,
|
||||
metadata: {
|
||||
period: `${startDate} ~ ${endDate}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`대시보드 데이터 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkAnalysis;
|
||||
89
api.hyungi.net/models/cuttingPlanModel.js
Normal file
89
api.hyungi.net/models/cuttingPlanModel.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const create = async (plan, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
project_id, drawing_name,
|
||||
pipe_spec, area_number,
|
||||
spool_number, length
|
||||
} = plan;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO CuttingPlan
|
||||
(project_id, drawing_name, pipe_spec, area_number, spool_number, length)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[project_id, drawing_name, pipe_spec, area_number, spool_number, length]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM CuttingPlan ORDER BY cutting_plan_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (cutting_plan_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM CuttingPlan WHERE cutting_plan_id = ?`,
|
||||
[cutting_plan_id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const update = async (plan, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
cutting_plan_id, project_id, drawing_name,
|
||||
pipe_spec, area_number, spool_number, length
|
||||
} = plan;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE CuttingPlan
|
||||
SET project_id = ?,
|
||||
drawing_name = ?,
|
||||
pipe_spec = ?,
|
||||
area_number = ?,
|
||||
spool_number = ?,
|
||||
length = ?
|
||||
WHERE cutting_plan_id = ?`,
|
||||
[project_id, drawing_name, pipe_spec, area_number, spool_number, length, cutting_plan_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (cutting_plan_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM CuttingPlan WHERE cutting_plan_id = ?`,
|
||||
[cutting_plan_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { create, getAll, getById, update, remove };
|
||||
123
api.hyungi.net/models/dailyIssueReportModel.js
Normal file
123
api.hyungi.net/models/dailyIssueReportModel.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 1. 등록 (단일 레코드)
|
||||
*/
|
||||
const create = async (report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
start_time,
|
||||
end_time,
|
||||
issue_type_id,
|
||||
description = null // 선택값 처리
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO DailyIssueReports
|
||||
(date, worker_id, project_id, start_time, end_time, issue_type_id, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[date, worker_id, project_id, start_time, end_time, issue_type_id, description]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. 특정 날짜의 전체 이슈 목록 조회
|
||||
*/
|
||||
const getAllByDate = async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
d.id,
|
||||
d.date,
|
||||
w.worker_name,
|
||||
p.project_name,
|
||||
d.start_time,
|
||||
d.end_time,
|
||||
t.category,
|
||||
t.subcategory,
|
||||
d.description
|
||||
FROM DailyIssueReports d
|
||||
LEFT JOIN Workers w ON d.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON d.project_id = p.project_id
|
||||
LEFT JOIN IssueTypes t ON d.issue_type_id = t.issue_type_id
|
||||
WHERE d.date = ?
|
||||
ORDER BY d.start_time ASC`,
|
||||
[date]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 3. 단일 조회 (선택사항: 컨트롤러에서 사용 중)
|
||||
*/
|
||||
const getById = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM DailyIssueReports WHERE id = ?`, [id]);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 4. 수정
|
||||
*/
|
||||
const update = async (id, data, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
for (const key in data) {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(data[key]);
|
||||
}
|
||||
|
||||
values.push(id); // 마지막에 id
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE DailyIssueReports SET ${fields.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 5. 삭제
|
||||
*/
|
||||
const remove = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`DELETE FROM DailyIssueReports WHERE id = ?`, [id]);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAllByDate,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
830
api.hyungi.net/models/dailyWorkReportModel.js
Normal file
830
api.hyungi.net/models/dailyWorkReportModel.js
Normal file
@@ -0,0 +1,830 @@
|
||||
// models/dailyWorkReportModel.js - 누적입력 방식 + 모든 기존 기능 포함
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 📋 마스터 데이터 조회 함수들
|
||||
*/
|
||||
const getAllWorkTypes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT * FROM work_types ORDER BY name ASC');
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('작업 유형 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAllWorkStatusTypes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT * FROM work_status_types ORDER BY id ASC');
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('업무 상태 유형 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAllErrorTypes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT * FROM error_types ORDER BY name ASC');
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('에러 유형 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔄 누적 추가 전용 함수 (createDailyReport 대체) - 절대 삭제 안함!
|
||||
*/
|
||||
const createDailyReport = async (reportData, callback) => {
|
||||
const { report_date, worker_id, work_entries, created_by, created_by_name, total_hours } = reportData;
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
console.log(`📝 ${created_by_name}이 ${report_date} ${worker_id}번 작업자에게 데이터 추가 중...`);
|
||||
|
||||
// ✅ 수정된 쿼리 (테이블 alias 추가):
|
||||
const [existingReports] = await conn.query(
|
||||
`SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id
|
||||
WHERE dwr.report_date = ? AND dwr.worker_id = ?
|
||||
GROUP BY dwr.created_by`,
|
||||
[report_date, worker_id]
|
||||
);
|
||||
|
||||
|
||||
console.log('기존 데이터 (삭제하지 않음):', existingReports);
|
||||
|
||||
// 2. ✅ 삭제 없이 새로운 데이터만 추가!
|
||||
const insertedIds = [];
|
||||
for (const entry of work_entries) {
|
||||
const { project_id, work_type_id, work_status_id, error_type_id, work_hours } = entry;
|
||||
|
||||
const [insertResult] = await conn.query(
|
||||
`INSERT INTO daily_work_reports
|
||||
(report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_by, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[report_date, worker_id, project_id, work_type_id, work_status_id || 1, error_type_id || null, work_hours, created_by]
|
||||
);
|
||||
|
||||
insertedIds.push(insertResult.insertId);
|
||||
}
|
||||
|
||||
// ✅ 수정된 쿼리:
|
||||
const [finalReports] = await conn.query(
|
||||
`SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id
|
||||
WHERE dwr.report_date = ? AND dwr.worker_id = ?
|
||||
GROUP BY dwr.created_by`,
|
||||
[report_date, worker_id]
|
||||
);
|
||||
|
||||
const grandTotal = finalReports.reduce((sum, report) => sum + parseFloat(report.total_hours || 0), 0);
|
||||
const myTotal = finalReports.find(r => r.created_by === created_by)?.total_hours || 0;
|
||||
|
||||
console.log('최종 결과:');
|
||||
finalReports.forEach(report => {
|
||||
console.log(` - ${report.created_by_name}: ${report.total_hours}시간 (${report.count}개 항목)`);
|
||||
});
|
||||
console.log(` 📊 총합: ${grandTotal}시간`);
|
||||
|
||||
// 4. 감사 로그 추가
|
||||
try {
|
||||
await conn.query(
|
||||
`INSERT INTO work_report_audit_log
|
||||
(action, report_id, new_values, changed_by, change_reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())`,
|
||||
[
|
||||
'ADD_ACCUMULATE',
|
||||
insertedIds[0] || null,
|
||||
JSON.stringify({
|
||||
report_date,
|
||||
worker_id,
|
||||
work_entries_count: work_entries.length,
|
||||
added_hours: total_hours,
|
||||
my_total: myTotal,
|
||||
grand_total: grandTotal,
|
||||
contributors: finalReports.map(r => ({ name: r.created_by_name, hours: r.total_hours }))
|
||||
}),
|
||||
created_by,
|
||||
`누적 추가 by ${created_by_name} - 삭제 없음`
|
||||
]
|
||||
);
|
||||
} catch (auditErr) {
|
||||
console.warn('감사 로그 추가 실패:', auditErr.message);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
|
||||
callback(null, {
|
||||
success: true,
|
||||
inserted_count: insertedIds.length,
|
||||
deleted_count: 0, // 항상 0 (삭제 안함)
|
||||
action: 'accumulated',
|
||||
message: `${created_by_name}이 ${total_hours}시간 추가했습니다. (개인 총 ${myTotal}시간, 전체 총 ${grandTotal}시간)`,
|
||||
final_summary: {
|
||||
my_total: parseFloat(myTotal),
|
||||
grand_total: grandTotal,
|
||||
total_contributors: finalReports.length,
|
||||
contributors: finalReports
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error('작업보고서 누적 추가 오류:', err);
|
||||
callback(err);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 특정 날짜 + 작업자 + 작성자의 누적 현황 조회
|
||||
*/
|
||||
const getMyAccumulatedHours = async (date, worker_id, created_by, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
SUM(work_hours) as my_total_hours,
|
||||
COUNT(*) as my_entry_count,
|
||||
GROUP_CONCAT(
|
||||
CONCAT(p.project_name, ':', work_hours, 'h')
|
||||
ORDER BY created_at
|
||||
) as my_entries
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date = ? AND dwr.worker_id = ? AND dwr.created_by = ?
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [date, worker_id, created_by]);
|
||||
callback(null, rows[0] || { my_total_hours: 0, my_entry_count: 0, my_entries: null });
|
||||
} catch (err) {
|
||||
console.error('개인 누적 현황 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 누적 현황 조회 - 날짜+작업자별 (모든 기여자)
|
||||
*/
|
||||
const getAccumulatedReportsByDate = async (date, worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = getSelectQuery() + `
|
||||
WHERE dwr.report_date = ? AND dwr.worker_id = ?
|
||||
ORDER BY dwr.created_by, dwr.created_at ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [date, worker_id]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회
|
||||
*/
|
||||
const getContributorsByDate = async (date, worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
dwr.created_by,
|
||||
u.name as created_by_name,
|
||||
COUNT(*) as entry_count,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
MIN(dwr.created_at) as first_entry,
|
||||
MAX(dwr.created_at) as last_entry,
|
||||
GROUP_CONCAT(
|
||||
CONCAT(p.project_name, ':', dwr.work_hours, 'h')
|
||||
ORDER BY dwr.created_at SEPARATOR ', '
|
||||
) as entry_details
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date = ? AND dwr.worker_id = ?
|
||||
GROUP BY dwr.created_by
|
||||
ORDER BY total_hours DESC, first_entry ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [date, worker_id]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('기여자별 요약 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업 항목만 삭제 (개별 삭제)
|
||||
*/
|
||||
const removeSpecificEntry = async (entry_id, deleted_by, callback) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// 삭제 전 정보 확인
|
||||
const [entryInfo] = await conn.query(
|
||||
`SELECT dwr.*, w.worker_name, p.project_name, u.name as created_by_name
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id
|
||||
WHERE dwr.id = ?`,
|
||||
[entry_id]
|
||||
);
|
||||
|
||||
if (entryInfo.length === 0) {
|
||||
await conn.rollback();
|
||||
return callback(new Error('삭제할 항목을 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
const entry = entryInfo[0];
|
||||
|
||||
// 권한 확인: 본인이 작성한 것만 삭제 가능
|
||||
if (entry.created_by !== deleted_by) {
|
||||
await conn.rollback();
|
||||
return callback(new Error('본인이 작성한 항목만 삭제할 수 있습니다.'));
|
||||
}
|
||||
|
||||
// 개별 항목 삭제
|
||||
const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [entry_id]);
|
||||
|
||||
// 감사 로그
|
||||
try {
|
||||
await conn.query(
|
||||
`INSERT INTO work_report_audit_log
|
||||
(action, report_id, old_values, changed_by, change_reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())`,
|
||||
[
|
||||
'DELETE_SINGLE',
|
||||
entry_id,
|
||||
JSON.stringify({
|
||||
worker_name: entry.worker_name,
|
||||
project_name: entry.project_name,
|
||||
work_hours: entry.work_hours,
|
||||
report_date: entry.report_date
|
||||
}),
|
||||
deleted_by,
|
||||
`개별 항목 삭제`
|
||||
]
|
||||
);
|
||||
} catch (auditErr) {
|
||||
console.warn('감사 로그 추가 실패:', auditErr.message);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
callback(null, {
|
||||
success: true,
|
||||
deleted_entry: {
|
||||
worker_name: entry.worker_name,
|
||||
project_name: entry.project_name,
|
||||
work_hours: entry.work_hours
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error('개별 항목 삭제 오류:', err);
|
||||
callback(err);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 공통 SELECT 쿼리 부분
|
||||
*/
|
||||
const getSelectQuery = () => `
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.report_date,
|
||||
dwr.worker_id,
|
||||
dwr.project_id,
|
||||
dwr.work_type_id,
|
||||
dwr.work_status_id,
|
||||
dwr.error_type_id,
|
||||
dwr.work_hours,
|
||||
dwr.created_by,
|
||||
w.worker_name,
|
||||
p.project_name,
|
||||
wt.name as work_type_name,
|
||||
wst.name as work_status_name,
|
||||
et.name as error_type_name,
|
||||
u.name as created_by_name,
|
||||
dwr.created_at,
|
||||
dwr.updated_at
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
|
||||
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
||||
LEFT JOIN Users u ON dwr.created_by = u.user_id
|
||||
`;
|
||||
|
||||
/**
|
||||
* 7. ID로 작업보고서 조회
|
||||
*/
|
||||
const getById = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = getSelectQuery() + 'WHERE dwr.id = ?';
|
||||
const [rows] = await db.query(sql, [id]);
|
||||
callback(null, rows[0] || null);
|
||||
} catch (err) {
|
||||
console.error('ID로 작업보고서 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 8. 일일 작업보고서 조회 (날짜별)
|
||||
*/
|
||||
const getByDate = async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = getSelectQuery() + `
|
||||
WHERE dwr.report_date = ?
|
||||
ORDER BY w.worker_name ASC, p.project_name ASC, dwr.id ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('날짜별 작업보고서 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 9. 일일 작업보고서 조회 (날짜 + 작성자별)
|
||||
*/
|
||||
const getByDateAndCreator = async (date, created_by, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = getSelectQuery() + `
|
||||
WHERE dwr.report_date = ? AND dwr.created_by = ?
|
||||
ORDER BY w.worker_name ASC, p.project_name ASC, dwr.id ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [date, created_by]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('날짜+작성자별 작업보고서 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 10. 일일 작업보고서 조회 (작업자별)
|
||||
*/
|
||||
const getByWorker = async (worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = getSelectQuery() + `
|
||||
WHERE dwr.worker_id = ?
|
||||
ORDER BY dwr.report_date DESC, dwr.id ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [worker_id]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('작업자별 작업보고서 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 11. 일일 작업보고서 조회 (날짜 + 작업자)
|
||||
*/
|
||||
const getByDateAndWorker = async (date, worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = getSelectQuery() + `
|
||||
WHERE dwr.report_date = ? AND dwr.worker_id = ?
|
||||
ORDER BY dwr.id ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [date, worker_id]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('날짜+작업자별 작업보고서 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 12. 기간별 조회
|
||||
*/
|
||||
const getByRange = async (start_date, end_date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = getSelectQuery() + `
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
ORDER BY dwr.report_date DESC, w.worker_name ASC, dwr.id ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [start_date, end_date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('기간별 작업보고서 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 13. 상세 검색 (페이지네이션 포함)
|
||||
*/
|
||||
const searchWithDetails = async (params, callback) => {
|
||||
const { start_date, end_date, worker_id, project_id, work_status_id, created_by, page, limit } = params;
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 조건 구성
|
||||
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
||||
let queryParams = [start_date, end_date];
|
||||
|
||||
if (worker_id) {
|
||||
whereConditions.push('dwr.worker_id = ?');
|
||||
queryParams.push(worker_id);
|
||||
}
|
||||
|
||||
if (project_id) {
|
||||
whereConditions.push('dwr.project_id = ?');
|
||||
queryParams.push(project_id);
|
||||
}
|
||||
|
||||
if (work_status_id) {
|
||||
whereConditions.push('dwr.work_status_id = ?');
|
||||
queryParams.push(work_status_id);
|
||||
}
|
||||
|
||||
if (created_by) {
|
||||
whereConditions.push('dwr.created_by = ?');
|
||||
queryParams.push(created_by);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 총 개수 조회
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM daily_work_reports dwr
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
const [countResult] = await db.query(countQuery, queryParams);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 데이터 조회 (JOIN 포함)
|
||||
const offset = (page - 1) * limit;
|
||||
const dataQuery = getSelectQuery() + `
|
||||
WHERE ${whereClause}
|
||||
ORDER BY dwr.report_date DESC, w.worker_name ASC, dwr.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const dataParams = [...queryParams, limit, offset];
|
||||
const [rows] = await db.query(dataQuery, dataParams);
|
||||
|
||||
callback(null, { reports: rows, total });
|
||||
} catch (err) {
|
||||
console.error('상세 검색 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 14. 일일 근무 요약 조회 (날짜별)
|
||||
*/
|
||||
const getSummaryByDate = async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
dwr.report_date,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(*) as work_entries_count,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.report_date = ?
|
||||
GROUP BY dwr.worker_id, dwr.report_date
|
||||
ORDER BY w.worker_name ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('일일 근무 요약 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 15. 일일 근무 요약 조회 (작업자별)
|
||||
*/
|
||||
const getSummaryByWorker = async (worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(*) as work_entries_count,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.worker_id = ?
|
||||
GROUP BY dwr.report_date, dwr.worker_id
|
||||
ORDER BY dwr.report_date DESC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [worker_id]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('작업자별 근무 요약 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 16. 월간 요약
|
||||
*/
|
||||
const getMonthlySummary = async (year, month, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`;
|
||||
const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`;
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
dwr.report_date,
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
SUM(dwr.work_hours) as total_work_hours,
|
||||
COUNT(DISTINCT dwr.project_id) as project_count,
|
||||
COUNT(*) as work_entries_count,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count,
|
||||
GROUP_CONCAT(DISTINCT p.project_name ORDER BY p.project_name) as projects,
|
||||
GROUP_CONCAT(DISTINCT wt.name ORDER BY wt.name) as work_types
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.report_date, dwr.worker_id
|
||||
ORDER BY dwr.report_date DESC, w.worker_name ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [start, end]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
console.error('월간 요약 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 17. 작업보고서 수정
|
||||
*/
|
||||
const updateById = async (id, updateData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const setFields = [];
|
||||
const values = [];
|
||||
|
||||
if (updateData.work_hours !== undefined) {
|
||||
setFields.push('work_hours = ?');
|
||||
values.push(updateData.work_hours);
|
||||
}
|
||||
|
||||
if (updateData.work_status_id !== undefined) {
|
||||
setFields.push('work_status_id = ?');
|
||||
values.push(updateData.work_status_id);
|
||||
}
|
||||
|
||||
if (updateData.error_type_id !== undefined) {
|
||||
setFields.push('error_type_id = ?');
|
||||
values.push(updateData.error_type_id);
|
||||
}
|
||||
|
||||
if (updateData.project_id !== undefined) {
|
||||
setFields.push('project_id = ?');
|
||||
values.push(updateData.project_id);
|
||||
}
|
||||
|
||||
if (updateData.work_type_id !== undefined) {
|
||||
setFields.push('work_type_id = ?');
|
||||
values.push(updateData.work_type_id);
|
||||
}
|
||||
|
||||
setFields.push('updated_at = NOW()');
|
||||
|
||||
if (updateData.updated_by) {
|
||||
setFields.push('updated_by = ?');
|
||||
values.push(updateData.updated_by);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
const sql = `UPDATE daily_work_reports SET ${setFields.join(', ')} WHERE id = ?`;
|
||||
const [result] = await db.query(sql, values);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
console.error('작업보고서 수정 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 18. 특정 작업보고서 삭제
|
||||
*/
|
||||
const removeById = async (id, deletedBy, callback) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// 삭제 전 정보 저장 (감사 로그용)
|
||||
const [reportInfo] = await conn.query('SELECT * FROM daily_work_reports WHERE id = ?', [id]);
|
||||
|
||||
// 작업보고서 삭제
|
||||
const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [id]);
|
||||
|
||||
// 감사 로그 추가
|
||||
if (reportInfo.length > 0 && deletedBy) {
|
||||
try {
|
||||
await conn.query(
|
||||
`INSERT INTO work_report_audit_log
|
||||
(action, report_id, old_values, changed_by, change_reason, created_at)
|
||||
VALUES ('DELETE', ?, ?, ?, 'Manual deletion', NOW())`,
|
||||
[id, JSON.stringify(reportInfo[0]), deletedBy]
|
||||
);
|
||||
} catch (auditErr) {
|
||||
console.warn('감사 로그 추가 실패:', auditErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error('작업보고서 삭제 오류:', err);
|
||||
callback(err);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 19. 작업자의 특정 날짜 전체 삭제
|
||||
*/
|
||||
const removeByDateAndWorker = async (date, worker_id, deletedBy, callback) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// 삭제 전 정보 저장 (감사 로그용)
|
||||
const [reportInfos] = await conn.query(
|
||||
'SELECT * FROM daily_work_reports WHERE report_date = ? AND worker_id = ?',
|
||||
[date, worker_id]
|
||||
);
|
||||
|
||||
// 작업보고서 삭제
|
||||
const [result] = await conn.query(
|
||||
'DELETE FROM daily_work_reports WHERE report_date = ? AND worker_id = ?',
|
||||
[date, worker_id]
|
||||
);
|
||||
|
||||
// 감사 로그 추가
|
||||
if (reportInfos.length > 0 && deletedBy) {
|
||||
try {
|
||||
await conn.query(
|
||||
`INSERT INTO work_report_audit_log
|
||||
(action, old_values, changed_by, change_reason, created_at)
|
||||
VALUES ('DELETE_BATCH', ?, ?, 'Batch deletion by date and worker', NOW())`,
|
||||
[JSON.stringify({ deleted_reports: reportInfos, count: reportInfos.length }), deletedBy]
|
||||
);
|
||||
} catch (auditErr) {
|
||||
console.warn('감사 로그 추가 실패:', auditErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error('작업보고서 전체 삭제 오류:', err);
|
||||
callback(err);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 20. 통계 조회
|
||||
*/
|
||||
const getStatistics = async (start_date, end_date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
COUNT(*) as total_reports,
|
||||
SUM(work_hours) as total_hours,
|
||||
COUNT(DISTINCT worker_id) as unique_workers,
|
||||
COUNT(DISTINCT project_id) as unique_projects,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count,
|
||||
AVG(work_hours) as avg_hours_per_entry,
|
||||
MIN(work_hours) as min_hours,
|
||||
MAX(work_hours) as max_hours
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [start_date, end_date]);
|
||||
|
||||
// 추가 통계 - 날짜별 집계
|
||||
const dailyStatsSql = `
|
||||
SELECT
|
||||
report_date,
|
||||
COUNT(*) as daily_reports,
|
||||
SUM(work_hours) as daily_hours,
|
||||
COUNT(DISTINCT worker_id) as daily_workers
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
GROUP BY report_date
|
||||
ORDER BY report_date DESC
|
||||
`;
|
||||
|
||||
const [dailyStats] = await db.query(dailyStatsSql, [start_date, end_date]);
|
||||
|
||||
const result = {
|
||||
overall: rows[0],
|
||||
daily_breakdown: dailyStats
|
||||
};
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 모든 함수 내보내기 (기존 기능 + 누적 기능)
|
||||
module.exports = {
|
||||
// 📋 마스터 데이터
|
||||
getAllWorkTypes,
|
||||
getAllWorkStatusTypes,
|
||||
getAllErrorTypes,
|
||||
|
||||
// 🔄 핵심 생성 함수 (누적 방식)
|
||||
createDailyReport, // 누적 추가 (덮어쓰기 없음)
|
||||
|
||||
// 📊 누적 관련 새로운 함수들
|
||||
getMyAccumulatedHours, // 개인 누적 현황
|
||||
getAccumulatedReportsByDate, // 날짜별 누적 현황
|
||||
getContributorsByDate, // 기여자별 요약
|
||||
removeSpecificEntry, // 개별 항목 삭제
|
||||
|
||||
// 📊 기존 조회 함수들 (모두 유지)
|
||||
getById,
|
||||
getByDate,
|
||||
getByDateAndCreator, // 날짜+작성자별 조회
|
||||
getByWorker,
|
||||
getByDateAndWorker,
|
||||
getByRange,
|
||||
searchWithDetails,
|
||||
getSummaryByDate,
|
||||
getSummaryByWorker,
|
||||
getMonthlySummary,
|
||||
|
||||
// ✏️ 수정/삭제 함수들 (기존 유지)
|
||||
updateById,
|
||||
removeById,
|
||||
removeByDateAndWorker,
|
||||
getStatistics
|
||||
};
|
||||
94
api.hyungi.net/models/equipmentListModel.js
Normal file
94
api.hyungi.net/models/equipmentListModel.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const create = async (equipment, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
factory_id, equipment_name,
|
||||
model, status, purchase_date, description
|
||||
} = equipment;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO EquipmentList
|
||||
(factory_id, equipment_name, model, status, purchase_date, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[factory_id, equipment_name, model, status, purchase_date, description]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM EquipmentList ORDER BY equipment_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (equipment_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM EquipmentList WHERE equipment_id = ?`,
|
||||
[equipment_id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const update = async (equipment, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
equipment_id, factory_id, equipment_name,
|
||||
model, status, purchase_date, description
|
||||
} = equipment;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE EquipmentList
|
||||
SET factory_id = ?,
|
||||
equipment_name = ?,
|
||||
model = ?,
|
||||
status = ?,
|
||||
purchase_date = ?,
|
||||
description = ?
|
||||
WHERE equipment_id = ?`,
|
||||
[factory_id, equipment_name, model, status, purchase_date, description, equipment_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (equipment_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM EquipmentList WHERE equipment_id = ?`,
|
||||
[equipment_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
86
api.hyungi.net/models/factoryInfoModel.js
Normal file
86
api.hyungi.net/models/factoryInfoModel.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const create = async (factory, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { factory_name, address, description, map_image_url } = factory;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO FactoryInfo
|
||||
(factory_name, address, description, map_image_url)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[factory_name, address, description, map_image_url]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM FactoryInfo ORDER BY factory_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (factory_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM FactoryInfo WHERE factory_id = ?`,
|
||||
[factory_id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const update = async (factory, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { factory_id, factory_name, address, description, map_image_url } = factory;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE FactoryInfo
|
||||
SET factory_name = ?,
|
||||
address = ?,
|
||||
description = ?,
|
||||
map_image_url = ?
|
||||
WHERE factory_id = ?`,
|
||||
[factory_name, address, description, map_image_url, factory_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (factory_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM FactoryInfo WHERE factory_id = ?`,
|
||||
[factory_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
58
api.hyungi.net/models/issueTypeModel.js
Normal file
58
api.hyungi.net/models/issueTypeModel.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// CREATE
|
||||
const create = async (type, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO IssueTypes (category, subcategory) VALUES (?, ?)`,
|
||||
[type.category, type.subcategory]
|
||||
);
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// READ ALL
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM IssueTypes ORDER BY category, subcategory`);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// UPDATE
|
||||
const update = async (id, type, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE IssueTypes SET category = ?, subcategory = ? WHERE id = ?`,
|
||||
[type.category, type.subcategory, id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE
|
||||
const remove = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`DELETE FROM IssueTypes WHERE id = ?`, [id]);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
12
api.hyungi.net/models/pingModel.js
Normal file
12
api.hyungi.net/models/pingModel.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// models/pingModel.js
|
||||
|
||||
/**
|
||||
* 단순 ping 비즈니스 로직 레이어
|
||||
* 필요하다면 여기서 더 복잡한 처리나 다른 서비스 호출 등을 관리
|
||||
*/
|
||||
exports.ping = () => {
|
||||
return {
|
||||
message: 'pong',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
};
|
||||
31
api.hyungi.net/models/pipeSpecModel.js
Normal file
31
api.hyungi.net/models/pipeSpecModel.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 전체 조회
|
||||
const getAll = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM PipeSpecs ORDER BY material, diameter_in`);
|
||||
return rows;
|
||||
};
|
||||
|
||||
// 등록
|
||||
const create = async ({ material, diameter_in, schedule }) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO PipeSpecs (material, diameter_in, schedule)
|
||||
VALUES (?, ?, ?)`,
|
||||
[material, diameter_in, schedule]
|
||||
);
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
// 삭제
|
||||
const remove = async (spec_id) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM PipeSpecs WHERE spec_id = ?`,
|
||||
[spec_id]
|
||||
);
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
module.exports = { getAll, create, remove };
|
||||
100
api.hyungi.net/models/processModel.js
Normal file
100
api.hyungi.net/models/processModel.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const create = async (processData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
project_id, process_name,
|
||||
process_start, process_end,
|
||||
planned_worker_count, process_description, note
|
||||
} = processData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Processes
|
||||
(project_id, process_name, process_start, process_end,
|
||||
planned_worker_count, process_description, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[project_id, process_name, process_start, process_end,
|
||||
planned_worker_count, process_description, note]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM Processes ORDER BY process_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (process_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM Processes WHERE process_id = ?`,
|
||||
[process_id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const update = async (processData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
process_id, project_id,
|
||||
process_name, process_start, process_end,
|
||||
planned_worker_count, process_description, note
|
||||
} = processData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE Processes
|
||||
SET project_id = ?,
|
||||
process_name = ?,
|
||||
process_start = ?,
|
||||
process_end = ?,
|
||||
planned_worker_count = ?,
|
||||
process_description = ?,
|
||||
note = ?
|
||||
WHERE process_id = ?`,
|
||||
[project_id, process_name, process_start, process_end,
|
||||
planned_worker_count, process_description, note, process_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (process_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM Processes WHERE process_id = ?`,
|
||||
[process_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
97
api.hyungi.net/models/projectModel.js
Normal file
97
api.hyungi.net/models/projectModel.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const create = async (project, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
job_no, project_name,
|
||||
contract_date, due_date,
|
||||
delivery_method, site, pm
|
||||
} = project;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Projects
|
||||
(job_no, project_name, contract_date, due_date, delivery_method, site, pm)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[job_no, project_name, contract_date, due_date, delivery_method, site, pm]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM Projects ORDER BY project_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getById = async (project_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM Projects WHERE project_id = ?`,
|
||||
[project_id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
const update = async (project, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
project_id, job_no, project_name,
|
||||
contract_date, due_date,
|
||||
delivery_method, site, pm
|
||||
} = project;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE Projects
|
||||
SET job_no = ?,
|
||||
project_name = ?,
|
||||
contract_date = ?,
|
||||
due_date = ?,
|
||||
delivery_method= ?,
|
||||
site = ?,
|
||||
pm = ?
|
||||
WHERE project_id = ?`,
|
||||
[job_no, project_name, contract_date, due_date, delivery_method, site, pm, project_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (project_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM Projects WHERE project_id = ?`,
|
||||
[project_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
90
api.hyungi.net/models/taskModel.js
Normal file
90
api.hyungi.net/models/taskModel.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 1. 생성
|
||||
const create = async (task, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category, subcategory, task_name, description } = task;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Tasks (category, subcategory, task_name, description)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[category, subcategory, task_name, description]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM Tasks ORDER BY task_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
const getById = async (task_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM Tasks WHERE task_id = ?`,
|
||||
[task_id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
const update = async (task, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { task_id, category, subcategory, task_name, description } = task;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE Tasks
|
||||
SET category = ?,
|
||||
subcategory = ?,
|
||||
task_name = ?,
|
||||
description = ?
|
||||
WHERE task_id = ?`,
|
||||
[category, subcategory, task_name, description, task_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
const remove = async (task_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM Tasks WHERE task_id = ?`,
|
||||
[task_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
89
api.hyungi.net/models/toolsModel.js
Normal file
89
api.hyungi.net/models/toolsModel.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 1. 전체 도구 조회
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT * FROM Tools');
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 단일 도구 조회
|
||||
const getById = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT * FROM Tools WHERE id = ?', [id]);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 도구 생성
|
||||
const create = async (tool, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note } = tool;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Tools
|
||||
(name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 도구 수정
|
||||
const update = async (id, tool, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note } = tool;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE Tools
|
||||
SET name = ?,
|
||||
location = ?,
|
||||
stock = ?,
|
||||
status = ?,
|
||||
factory_id = ?,
|
||||
map_x = ?,
|
||||
map_y = ?,
|
||||
map_zone = ?,
|
||||
map_note = ?
|
||||
WHERE id = ?`,
|
||||
[name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note, id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 도구 삭제
|
||||
const remove = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query('DELETE FROM Tools WHERE id = ?', [id]);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ export 정리
|
||||
module.exports = {
|
||||
getAll,
|
||||
getById,
|
||||
create,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
45
api.hyungi.net/models/uploadModel.js
Normal file
45
api.hyungi.net/models/uploadModel.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 1. 문서 업로드
|
||||
const create = async (doc, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO uploaded_documents
|
||||
(title, tags, description, original_name, stored_name, file_path, file_type, file_size, submitted_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const values = [
|
||||
doc.title,
|
||||
doc.tags,
|
||||
doc.description,
|
||||
doc.original_name,
|
||||
doc.stored_name,
|
||||
doc.file_path,
|
||||
doc.file_type,
|
||||
doc.file_size,
|
||||
doc.submitted_by
|
||||
];
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 문서 목록 조회
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM uploaded_documents ORDER BY created_at DESC`);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 내보내기
|
||||
module.exports = {
|
||||
create,
|
||||
getAll
|
||||
};
|
||||
20
api.hyungi.net/models/userModel.js
Normal file
20
api.hyungi.net/models/userModel.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 사용자 조회
|
||||
const findByUsername = async (username) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM Users WHERE username = ?', [username]
|
||||
);
|
||||
return rows[0];
|
||||
} catch (err) {
|
||||
console.error('DB 오류 - 사용자 조회 실패:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// 명확한 내보내기
|
||||
module.exports = {
|
||||
findByUsername
|
||||
};
|
||||
223
api.hyungi.net/models/workReportModel.js
Normal file
223
api.hyungi.net/models/workReportModel.js
Normal file
@@ -0,0 +1,223 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 1. 여러 건 등록 (트랜잭션 사용)
|
||||
*/
|
||||
const createBatch = async (reports, callback) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const sql = `
|
||||
INSERT INTO WorkReports
|
||||
(\`date\`, worker_id, project_id, task_id, overtime_hours, work_details, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
for (const rpt of reports) {
|
||||
const params = [
|
||||
rpt.date,
|
||||
rpt.worker_id,
|
||||
rpt.project_id,
|
||||
rpt.task_id || null,
|
||||
rpt.overtime_hours || null,
|
||||
rpt.work_details || null,
|
||||
rpt.memo || null
|
||||
];
|
||||
await conn.query(sql, params);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
callback(err);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. 단일 등록
|
||||
*/
|
||||
const create = async (report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date, worker_id, project_id,
|
||||
task_id, overtime_hours,
|
||||
work_details, memo
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO WorkReports
|
||||
(\`date\`, worker_id, project_id, task_id, overtime_hours, work_details, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
task_id || null,
|
||||
overtime_hours || null,
|
||||
work_details || null,
|
||||
memo || null
|
||||
]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 3. 날짜별 조회
|
||||
*/
|
||||
const getAllByDate = async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
wr.worker_id, -- 이 줄을 추가했습니다
|
||||
wr.id,
|
||||
wr.\`date\`,
|
||||
w.worker_name,
|
||||
p.project_name,
|
||||
CONCAT(t.category, ':', t.subcategory) AS task_name,
|
||||
wr.overtime_hours,
|
||||
wr.work_details,
|
||||
wr.memo
|
||||
FROM WorkReports wr
|
||||
LEFT JOIN Workers w ON wr.worker_id = w.worker_id
|
||||
LEFT JOIN Projects p ON wr.project_id = p.project_id
|
||||
LEFT JOIN Tasks t ON wr.task_id = t.task_id
|
||||
WHERE wr.\`date\` = ?
|
||||
ORDER BY w.worker_name ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 4. 기간 조회
|
||||
*/
|
||||
const getByRange = async (start, end, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM WorkReports
|
||||
WHERE \`date\` BETWEEN ? AND ?
|
||||
ORDER BY \`date\` ASC`,
|
||||
[start, end]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 5. ID로 조회
|
||||
*/
|
||||
const getById = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM WorkReports WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 6. 수정
|
||||
*/
|
||||
const update = async (id, report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date, worker_id, project_id,
|
||||
task_id, overtime_hours,
|
||||
work_details, memo
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE WorkReports
|
||||
SET \`date\` = ?,
|
||||
worker_id = ?,
|
||||
project_id = ?,
|
||||
task_id = ?,
|
||||
overtime_hours = ?,
|
||||
work_details = ?,
|
||||
memo = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
task_id || null,
|
||||
overtime_hours || null,
|
||||
work_details || null,
|
||||
memo || null,
|
||||
id
|
||||
]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 7. 삭제
|
||||
*/
|
||||
const remove = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM WorkReports WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 8. 중복 확인
|
||||
*/
|
||||
const existsByDateAndWorker = async (date, worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT 1 FROM WorkReports WHERE \`date\` = ? AND worker_id = ? LIMIT 1`,
|
||||
[date, worker_id]
|
||||
);
|
||||
callback(null, rows.length > 0);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 내보내기
|
||||
module.exports = {
|
||||
create,
|
||||
createBatch,
|
||||
getAllByDate,
|
||||
getByRange,
|
||||
getById,
|
||||
update,
|
||||
remove,
|
||||
existsByDateAndWorker
|
||||
};
|
||||
89
api.hyungi.net/models/workerModel.js
Normal file
89
api.hyungi.net/models/workerModel.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 1. 작업자 생성
|
||||
const create = async (worker, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { worker_name, join_date, job_type, salary, annual_leave, status } = worker;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Workers
|
||||
(worker_name, join_date, job_type, salary, annual_leave, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[worker_name, join_date, job_type, salary, annual_leave, status]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
const getAll = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM Workers ORDER BY worker_id DESC`);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
const getById = async (worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM Workers WHERE worker_id = ?`, [worker_id]);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 작업자 수정
|
||||
const update = async (worker, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { worker_id, worker_name, join_date, job_type, salary, annual_leave, status } = worker;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE Workers
|
||||
SET worker_name = ?,
|
||||
join_date = ?,
|
||||
job_type = ?,
|
||||
salary = ?,
|
||||
annual_leave = ?,
|
||||
status = ?
|
||||
WHERE worker_id = ?`,
|
||||
[worker_name, join_date, job_type, salary, annual_leave, status, worker_id]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
const remove = async (worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM Workers WHERE worker_id = ?`,
|
||||
[worker_id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 모듈 내보내기 (정상 구조)
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
0
api.hyungi.net/models/your_database.db
Normal file
0
api.hyungi.net/models/your_database.db
Normal file
4392
api.hyungi.net/node_modules/.package-lock.json
generated
vendored
Normal file
4392
api.hyungi.net/node_modules/.package-lock.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10
api.hyungi.net/node_modules/@gar/promisify/LICENSE.md
generated
vendored
Normal file
10
api.hyungi.net/node_modules/@gar/promisify/LICENSE.md
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2020-2022 Michael Garvin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
65
api.hyungi.net/node_modules/@gar/promisify/README.md
generated
vendored
Normal file
65
api.hyungi.net/node_modules/@gar/promisify/README.md
generated
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# @gar/promisify
|
||||
|
||||
### Promisify an entire object or class instance
|
||||
|
||||
This module leverages es6 Proxy and Reflect to promisify every function in an
|
||||
object or class instance.
|
||||
|
||||
It assumes the callback that the function is expecting is the last
|
||||
parameter, and that it is an error-first callback with only one value,
|
||||
i.e. `(err, value) => ...`. This mirrors node's `util.promisify` method.
|
||||
|
||||
In order that you can use it as a one-stop-shop for all your promisify
|
||||
needs, you can also pass it a function. That function will be
|
||||
promisified as normal using node's built-in `util.promisify` method.
|
||||
|
||||
[node's custom promisified
|
||||
functions](https://nodejs.org/api/util.html#util_custom_promisified_functions)
|
||||
will also be mirrored, further allowing this to be a drop-in replacement
|
||||
for the built-in `util.promisify`.
|
||||
|
||||
### Examples
|
||||
|
||||
Promisify an entire object
|
||||
|
||||
```javascript
|
||||
|
||||
const promisify = require('@gar/promisify')
|
||||
|
||||
class Foo {
|
||||
constructor (attr) {
|
||||
this.attr = attr
|
||||
}
|
||||
|
||||
double (input, cb) {
|
||||
cb(null, input * 2)
|
||||
}
|
||||
|
||||
const foo = new Foo('baz')
|
||||
const promisified = promisify(foo)
|
||||
|
||||
console.log(promisified.attr)
|
||||
console.log(await promisified.double(1024))
|
||||
```
|
||||
|
||||
Promisify a function
|
||||
|
||||
```javascript
|
||||
|
||||
const promisify = require('@gar/promisify')
|
||||
|
||||
function foo (a, cb) {
|
||||
if (a !== 'bad') {
|
||||
return cb(null, 'ok')
|
||||
}
|
||||
return cb('not ok')
|
||||
}
|
||||
|
||||
const promisified = promisify(foo)
|
||||
|
||||
// This will resolve to 'ok'
|
||||
promisified('good')
|
||||
|
||||
// this will reject
|
||||
promisified('bad')
|
||||
```
|
||||
36
api.hyungi.net/node_modules/@gar/promisify/index.js
generated
vendored
Normal file
36
api.hyungi.net/node_modules/@gar/promisify/index.js
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict'
|
||||
|
||||
const { promisify } = require('util')
|
||||
|
||||
const handler = {
|
||||
get: function (target, prop, receiver) {
|
||||
if (typeof target[prop] !== 'function') {
|
||||
return target[prop]
|
||||
}
|
||||
if (target[prop][promisify.custom]) {
|
||||
return function () {
|
||||
return Reflect.get(target, prop, receiver)[promisify.custom].apply(target, arguments)
|
||||
}
|
||||
}
|
||||
return function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
Reflect.get(target, prop, receiver).apply(target, [...arguments, function (err, result) {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(result)
|
||||
}])
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (thingToPromisify) {
|
||||
if (typeof thingToPromisify === 'function') {
|
||||
return promisify(thingToPromisify)
|
||||
}
|
||||
if (typeof thingToPromisify === 'object') {
|
||||
return new Proxy(thingToPromisify, handler)
|
||||
}
|
||||
throw new TypeError('Can only promisify functions or objects')
|
||||
}
|
||||
32
api.hyungi.net/node_modules/@gar/promisify/package.json
generated
vendored
Normal file
32
api.hyungi.net/node_modules/@gar/promisify/package.json
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@gar/promisify",
|
||||
"version": "1.1.3",
|
||||
"description": "Promisify an entire class or object",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wraithgar/gar-promisify.git"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix",
|
||||
"test": "lab -a @hapi/code -t 100",
|
||||
"posttest": "npm run lint"
|
||||
},
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"keywords": [
|
||||
"promisify",
|
||||
"all",
|
||||
"class",
|
||||
"object"
|
||||
],
|
||||
"author": "Gar <gar+npm@danger.computer>",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@hapi/code": "^8.0.1",
|
||||
"@hapi/lab": "^24.1.0",
|
||||
"standard": "^16.0.3"
|
||||
}
|
||||
}
|
||||
22
api.hyungi.net/node_modules/@hexagon/base64/LICENSE
generated
vendored
Normal file
22
api.hyungi.net/node_modules/@hexagon/base64/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021-2022 Hexagon <github.com/Hexagon>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
69
api.hyungi.net/node_modules/@hexagon/base64/README.md
generated
vendored
Normal file
69
api.hyungi.net/node_modules/@hexagon/base64/README.md
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
<p align="center">
|
||||
<img src="https://cdn.jsdelivr.net/gh/hexagon/base64@main/base64.png" alt="@hexagon/base64" width="200" height="200"><br>
|
||||
<br>Probably the only JavaScript base64 library you'll ever need!<br>
|
||||
</p>
|
||||
|
||||
# @hexagon/base64
|
||||
|
||||
Encode, decode and validate base64/base64url to string/arraybuffer and vice-versa. Works in Node, Deno and browser.
|
||||
|
||||
[](https://github.com/Hexagon/base64/actions/workflows/node.js.yml) [](https://github.com/Hexagon/base64/actions/workflows/deno.yml)
|
||||
[](https://badge.fury.io/js/@hexagon%2Fbase64) [](https://www.npmjs.org/package/@hexagon/base64) [](https://www.jsdelivr.com/package/npm/@hexagon/base64) [](https://www.codacy.com/gh/Hexagon/base64/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Hexagon/base64&utm_campaign=Badge_Grade)
|
||||
[](https://github.com/Hexagon/base64/blob/master/LICENSE)
|
||||
|
||||
* Supports regular base64 and base64url
|
||||
* Convert to/from string or arraybuffer
|
||||
* Validate / identify base64 and base64url
|
||||
* Works in Node.js >=4.0 (both require and import).
|
||||
* Works in Deno >=1.16.
|
||||
* Works in browsers as standalone, UMD or ES-module.
|
||||
* Includes [TypeScript](https://www.typescriptlang.org/) typings.
|
||||
|
||||
|
||||
```javascript
|
||||
// Encode string as regular base64
|
||||
const example1enc = base64.fromString("Hellö Wörld, how are you doing today?!");
|
||||
console.log(example1enc);
|
||||
// > SGVsbMO2IFfDtnJsZCwgaG93IGFyZSB5b3UgZG9pbmcgdG9kYXk/IQ==
|
||||
|
||||
// Decode string as regular base64
|
||||
const example1dec = base64.toString("SGVsbMO2IFfDtnJsZCwgaG93IGFyZSB5b3UgZG9pbmcgdG9kYXk/IQ==");
|
||||
console.log(example1dec);
|
||||
// > Hellö Wörld, how are you doing today?!
|
||||
```
|
||||
|
||||
Full documentation available at [base64.56k.guru](https://base64.56k.guru)
|
||||
|
||||
## Quick Installation
|
||||
|
||||
Node.js: `npm install @hexagon/base64 --save`
|
||||
|
||||
Deno: `import base64 from "https://deno.land/x/b64@1.1.28/src/base64.js";`
|
||||
|
||||
For browser/cdn usage, refer to the documentation.
|
||||
|
||||
### Quick API
|
||||
|
||||
- __fromArrayBuffer(buffer, urlMode)__ - Encodes `ArrayBuffer` into base64 or base64url if urlMode(optional) is true
|
||||
- __toArrayBuffer(str, urlMode)__ - Decodes base64url string (or base64url string if urlMode is true) to `ArrayBuffer`
|
||||
|
||||
- __fromString(str, urlMode)__ - Encodes `String` into base64 string(base64url string if urlMode is true)
|
||||
- __toString(str, urlMode)__ - Decodes base64 or base64url string to `String`
|
||||
|
||||
- __validate(str, urlMode)__ - Returns true if `String` str is valid base64/base64 dependending on urlMode
|
||||
|
||||
## Contributing
|
||||
|
||||
See [Contribution Guide](https://base64.56k.guru/contributing.html)
|
||||
|
||||
## Donations
|
||||
|
||||
If you found this library helpful and wish to support its development, consider making a donation through [Hexagon's GitHub Sponsors page](https://github.com/sponsors/hexagon). Your generosity ensures the library's continued development and maintenance.
|
||||
|
||||
### Contributors
|
||||
|
||||
The underlying code is loosely based on [github.com/niklasvh/base64-arraybuffer](https://github.com/niklasvh/base64-arraybuffer)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
12
api.hyungi.net/node_modules/@hexagon/base64/SECURITY.md
generated
vendored
Normal file
12
api.hyungi.net/node_modules/@hexagon/base64/SECURITY.md
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Email hexagon@56k.guru. Do NOT report an issue, we will have a look at it asap.
|
||||
198
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.cjs
generated
vendored
Normal file
198
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.cjs
generated
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.base64 = factory());
|
||||
})(this, (function () { 'use strict';
|
||||
|
||||
/* ------------------------------------------------------------------------------------
|
||||
|
||||
base64 - MIT License - Hexagon <hexagon@56k.guru>
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
License:
|
||||
|
||||
Copyright (c) 2021 Hexagon <hexagon@56k.guru>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
------------------------------------------------------------------------------------ */
|
||||
|
||||
const
|
||||
// Regular base64 characters
|
||||
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
|
||||
|
||||
// Base64url characters
|
||||
charsUrl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
||||
|
||||
genLookup = (target) => {
|
||||
const lookupTemp = typeof Uint8Array === "undefined" ? [] : new Uint8Array(256);
|
||||
const len = chars.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
lookupTemp[target.charCodeAt(i)] = i;
|
||||
}
|
||||
return lookupTemp;
|
||||
},
|
||||
|
||||
// Use a lookup table to find the index.
|
||||
lookup = genLookup(chars),
|
||||
lookupUrl = genLookup(charsUrl);
|
||||
|
||||
/**
|
||||
* Pre-calculated regexes for validating base64 and base64url
|
||||
*/
|
||||
const base64UrlPattern = /^[-A-Za-z0-9\-_]*$/;
|
||||
const base64Pattern = /^[-A-Za-z0-9+/]*={0,3}$/;
|
||||
|
||||
/**
|
||||
* @namespace base64
|
||||
*/
|
||||
const base64 = {};
|
||||
|
||||
/**
|
||||
* Convenience function for converting a base64 encoded string to an ArrayBuffer instance
|
||||
* @public
|
||||
*
|
||||
* @param {string} data - Base64 representation of data
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be expected
|
||||
* @returns {ArrayBuffer} - Decoded data
|
||||
*/
|
||||
base64.toArrayBuffer = (data, urlMode) => {
|
||||
const
|
||||
len = data.length;
|
||||
let bufferLength = data.length * 0.75,
|
||||
i,
|
||||
p = 0,
|
||||
encoded1,
|
||||
encoded2,
|
||||
encoded3,
|
||||
encoded4;
|
||||
|
||||
if (data[data.length - 1] === "=") {
|
||||
bufferLength--;
|
||||
if (data[data.length - 2] === "=") {
|
||||
bufferLength--;
|
||||
}
|
||||
}
|
||||
|
||||
const
|
||||
arraybuffer = new ArrayBuffer(bufferLength),
|
||||
bytes = new Uint8Array(arraybuffer),
|
||||
target = urlMode ? lookupUrl : lookup;
|
||||
|
||||
for (i = 0; i < len; i += 4) {
|
||||
encoded1 = target[data.charCodeAt(i)];
|
||||
encoded2 = target[data.charCodeAt(i + 1)];
|
||||
encoded3 = target[data.charCodeAt(i + 2)];
|
||||
encoded4 = target[data.charCodeAt(i + 3)];
|
||||
|
||||
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||
}
|
||||
|
||||
return arraybuffer;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function for creating a base64 encoded string from an ArrayBuffer instance
|
||||
* @public
|
||||
*
|
||||
* @param {ArrayBuffer} arrBuf - ArrayBuffer to be encoded
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
|
||||
* @returns {string} - Base64 representation of data
|
||||
*/
|
||||
base64.fromArrayBuffer = (arrBuf, urlMode) => {
|
||||
const bytes = new Uint8Array(arrBuf);
|
||||
let
|
||||
i,
|
||||
result = "";
|
||||
|
||||
const
|
||||
len = bytes.length,
|
||||
target = urlMode ? charsUrl : chars;
|
||||
|
||||
for (i = 0; i < len; i += 3) {
|
||||
result += target[bytes[i] >> 2];
|
||||
result += target[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||
result += target[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||
result += target[bytes[i + 2] & 63];
|
||||
}
|
||||
|
||||
const remainder = len % 3;
|
||||
if (remainder === 2) {
|
||||
result = result.substring(0, result.length - 1) + (urlMode ? "" : "=");
|
||||
} else if (remainder === 1) {
|
||||
result = result.substring(0, result.length - 2) + (urlMode ? "" : "==");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function for converting base64 to string
|
||||
* @public
|
||||
*
|
||||
* @param {string} str - Base64 encoded string to be decoded
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be expected
|
||||
* @returns {string} - Decoded string
|
||||
*/
|
||||
base64.toString = (str, urlMode) => {
|
||||
return new TextDecoder().decode(base64.toArrayBuffer(str, urlMode));
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function for converting a javascript string to base64
|
||||
* @public
|
||||
*
|
||||
* @param {string} str - String to be converted to base64
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
|
||||
* @returns {string} - Base64 encoded string
|
||||
*/
|
||||
base64.fromString = (str, urlMode) => {
|
||||
return base64.fromArrayBuffer(new TextEncoder().encode(str), urlMode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to validate base64
|
||||
* @public
|
||||
* @param {string} encoded - Base64 or Base64url encoded data
|
||||
* @param {boolean} [urlMode] - If set to true, base64url will be expected
|
||||
* @returns {boolean} - Valid base64/base64url?
|
||||
*/
|
||||
base64.validate = (encoded, urlMode) => {
|
||||
|
||||
// Bail out if not string
|
||||
if (!(typeof encoded === "string" || encoded instanceof String)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go on validate
|
||||
try {
|
||||
return urlMode ? base64UrlPattern.test(encoded) : base64Pattern.test(encoded);
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
base64.base64 = base64;
|
||||
|
||||
return base64;
|
||||
|
||||
}));
|
||||
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.js
generated
vendored
Normal file
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=typeof globalThis!=="undefined"?globalThis:global||self,global.base64=factory())})(this,function(){"use strict";const chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",charsUrl="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",genLookup=target=>{const lookupTemp=typeof Uint8Array==="undefined"?[]:new Uint8Array(256);const len=chars.length;for(let i=0;i<len;i++){lookupTemp[target.charCodeAt(i)]=i}return lookupTemp},lookup=genLookup(chars),lookupUrl=genLookup(charsUrl);const base64UrlPattern=/^[-A-Za-z0-9\-_]*$/;const base64Pattern=/^[-A-Za-z0-9+/]*={0,3}$/;const base64={};base64.toArrayBuffer=(data,urlMode)=>{const len=data.length;let bufferLength=data.length*.75,i,p=0,encoded1,encoded2,encoded3,encoded4;if(data[data.length-1]==="="){bufferLength--;if(data[data.length-2]==="="){bufferLength--}}const arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer),target=urlMode?lookupUrl:lookup;for(i=0;i<len;i+=4){encoded1=target[data.charCodeAt(i)];encoded2=target[data.charCodeAt(i+1)];encoded3=target[data.charCodeAt(i+2)];encoded4=target[data.charCodeAt(i+3)];bytes[p++]=encoded1<<2|encoded2>>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer};base64.fromArrayBuffer=(arrBuf,urlMode)=>{const bytes=new Uint8Array(arrBuf);let i,result="";const len=bytes.length,target=urlMode?charsUrl:chars;for(i=0;i<len;i+=3){result+=target[bytes[i]>>2];result+=target[(bytes[i]&3)<<4|bytes[i+1]>>4];result+=target[(bytes[i+1]&15)<<2|bytes[i+2]>>6];result+=target[bytes[i+2]&63]}const remainder=len%3;if(remainder===2){result=result.substring(0,result.length-1)+(urlMode?"":"=")}else if(remainder===1){result=result.substring(0,result.length-2)+(urlMode?"":"==")}return result};base64.toString=(str,urlMode)=>{return(new TextDecoder).decode(base64.toArrayBuffer(str,urlMode))};base64.fromString=(str,urlMode)=>{return base64.fromArrayBuffer((new TextEncoder).encode(str),urlMode)};base64.validate=(encoded,urlMode)=>{if(!(typeof encoded==="string"||encoded instanceof String)){return false}try{return urlMode?base64UrlPattern.test(encoded):base64Pattern.test(encoded)}catch(_e){return false}};base64.base64=base64;return base64});
|
||||
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.js.map
generated
vendored
Normal file
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.js.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["dist/base64.cjs"],"names":["global","factory","exports","module","define","amd","globalThis","self","base64","this","chars","charsUrl","genLookup","lookupTemp","Uint8Array","len","length","let","i","target","charCodeAt","lookup","lookupUrl","base64UrlPattern","base64Pattern","toArrayBuffer","data","urlMode","bufferLength","p","encoded1","encoded2","encoded3","encoded4","arraybuffer","ArrayBuffer","bytes","fromArrayBuffer","arrBuf","result","remainder","substring","toString","str","TextDecoder","decode","fromString","TextEncoder","encode","validate","encoded","String","test","_e"],"mappings":"CAAA,SAAWA,OAAQC,SAClB,OAAOC,UAAY,UAAY,OAAOC,SAAW,YAAcA,OAAOD,QAAUD,QAAQ,EACxF,OAAOG,SAAW,YAAcA,OAAOC,IAAMD,OAAOH,OAAO,GAC1DD,OAAS,OAAOM,aAAe,YAAcA,WAAaN,QAAUO,KAAMP,OAAOQ,OAASP,QAAQ,EACnG,GAAEQ,KAAM,WAAe,aA8BvB,MAECC,MAAQ,mEAGRC,SAAW,mEAEXC,UAAY,SACX,MAAMC,WAAa,OAAOC,aAAe,YAAc,GAAK,IAAIA,WAAW,GAAG,EAC9E,MAAMC,IAAML,MAAMM,OAClB,IAAKC,IAAIC,EAAI,EAAGA,EAAIH,IAAKG,CAAC,GAAI,CAC7BL,WAAWM,OAAOC,WAAWF,CAAC,GAAKA,CACpC,CACA,OAAOL,UACR,EAGAQ,OAAST,UAAUF,KAAK,EACxBY,UAAYV,UAAUD,QAAQ,EAK/B,MAAMY,iBAAmB,qBACzB,MAAMC,cAAgB,0BAKtB,MAAMhB,OAAS,GAUfA,OAAOiB,cAAgB,CAACC,KAAMC,WAC7B,MACCZ,IAAMW,KAAKV,OACZC,IAAIW,aAAeF,KAAKV,OAAS,IAChCE,EACAW,EAAI,EACJC,SACAC,SACAC,SACAC,SAED,GAAIP,KAAKA,KAAKV,OAAS,KAAO,IAAK,CAClCY,YAAY,GACZ,GAAIF,KAAKA,KAAKV,OAAS,KAAO,IAAK,CAClCY,YAAY,EACb,CACD,CAEA,MACCM,YAAc,IAAIC,YAAYP,YAAY,EAC1CQ,MAAQ,IAAItB,WAAWoB,WAAW,EAClCf,OAASQ,QAAUL,UAAYD,OAEhC,IAAKH,EAAI,EAAGA,EAAIH,IAAKG,GAAK,EAAG,CAC5BY,SAAWX,OAAOO,KAAKN,WAAWF,CAAC,GACnCa,SAAWZ,OAAOO,KAAKN,WAAWF,EAAI,CAAC,GACvCc,SAAWb,OAAOO,KAAKN,WAAWF,EAAI,CAAC,GACvCe,SAAWd,OAAOO,KAAKN,WAAWF,EAAI,CAAC,GAEvCkB,MAAMP,CAAC,IAAOC,UAAY,EAAMC,UAAY,EAC5CK,MAAMP,CAAC,KAAQE,SAAW,KAAO,EAAMC,UAAY,EACnDI,MAAMP,CAAC,KAAQG,SAAW,IAAM,EAAMC,SAAW,EAClD,CAEA,OAAOC,WAER,EAUA1B,OAAO6B,gBAAkB,CAACC,OAAQX,WACjC,MAAMS,MAAQ,IAAItB,WAAWwB,MAAM,EACnCrB,IACCC,EACAqB,OAAS,GAEV,MACCxB,IAAMqB,MAAMpB,OACZG,OAASQ,QAAUhB,SAAWD,MAE/B,IAAKQ,EAAI,EAAGA,EAAIH,IAAKG,GAAK,EAAG,CAC5BqB,QAAUpB,OAAOiB,MAAMlB,IAAM,GAC7BqB,QAAUpB,QAASiB,MAAMlB,GAAK,IAAM,EAAMkB,MAAMlB,EAAI,IAAM,GAC1DqB,QAAUpB,QAASiB,MAAMlB,EAAI,GAAK,KAAO,EAAMkB,MAAMlB,EAAI,IAAM,GAC/DqB,QAAUpB,OAAOiB,MAAMlB,EAAI,GAAK,GACjC,CAEA,MAAMsB,UAAYzB,IAAM,EACxB,GAAIyB,YAAc,EAAG,CACpBD,OAASA,OAAOE,UAAU,EAAGF,OAAOvB,OAAS,CAAC,GAAKW,QAAU,GAAK,IACnE,MAAO,GAAIa,YAAc,EAAG,CAC3BD,OAASA,OAAOE,UAAU,EAAGF,OAAOvB,OAAS,CAAC,GAAKW,QAAU,GAAK,KACnE,CAEA,OAAOY,MAER,EAUA/B,OAAOkC,SAAW,CAACC,IAAKhB,WACvB,OAAO,IAAIiB,aAAcC,OAAOrC,OAAOiB,cAAckB,IAAKhB,OAAO,CAAC,CACnE,EAUAnB,OAAOsC,WAAa,CAACH,IAAKhB,WACzB,OAAOnB,OAAO6B,iBAAgB,IAAIU,aAAcC,OAAOL,GAAG,EAAGhB,OAAO,CACrE,EASAnB,OAAOyC,SAAW,CAACC,QAASvB,WAG3B,GAAI,EAAE,OAAOuB,UAAY,UAAYA,mBAAmBC,QAAS,CAChE,OAAO,KACR,CAGA,IACC,OAAOxB,QAAUJ,iBAAiB6B,KAAKF,OAAO,EAAI1B,cAAc4B,KAAKF,OAAO,CAG7E,CAFE,MAAOG,IACR,OAAO,KACR,CACD,EAEA7C,OAAOA,OAASA,OAEhB,OAAOA,MAEP,CAAC"}
|
||||
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.mjs
generated
vendored
Normal file
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
const chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",charsUrl="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",genLookup=target=>{const lookupTemp=typeof Uint8Array==="undefined"?[]:new Uint8Array(256);const len=chars.length;for(let i=0;i<len;i++){lookupTemp[target.charCodeAt(i)]=i}return lookupTemp},lookup=genLookup(chars),lookupUrl=genLookup(charsUrl);const base64UrlPattern=/^[-A-Za-z0-9\-_]*$/;const base64Pattern=/^[-A-Za-z0-9+/]*={0,3}$/;const base64={};base64.toArrayBuffer=(data,urlMode)=>{const len=data.length;let bufferLength=data.length*.75,i,p=0,encoded1,encoded2,encoded3,encoded4;if(data[data.length-1]==="="){bufferLength--;if(data[data.length-2]==="="){bufferLength--}}const arraybuffer=new ArrayBuffer(bufferLength),bytes=new Uint8Array(arraybuffer),target=urlMode?lookupUrl:lookup;for(i=0;i<len;i+=4){encoded1=target[data.charCodeAt(i)];encoded2=target[data.charCodeAt(i+1)];encoded3=target[data.charCodeAt(i+2)];encoded4=target[data.charCodeAt(i+3)];bytes[p++]=encoded1<<2|encoded2>>4;bytes[p++]=(encoded2&15)<<4|encoded3>>2;bytes[p++]=(encoded3&3)<<6|encoded4&63}return arraybuffer};base64.fromArrayBuffer=(arrBuf,urlMode)=>{const bytes=new Uint8Array(arrBuf);let i,result="";const len=bytes.length,target=urlMode?charsUrl:chars;for(i=0;i<len;i+=3){result+=target[bytes[i]>>2];result+=target[(bytes[i]&3)<<4|bytes[i+1]>>4];result+=target[(bytes[i+1]&15)<<2|bytes[i+2]>>6];result+=target[bytes[i+2]&63]}const remainder=len%3;if(remainder===2){result=result.substring(0,result.length-1)+(urlMode?"":"=")}else if(remainder===1){result=result.substring(0,result.length-2)+(urlMode?"":"==")}return result};base64.toString=(str,urlMode)=>{return(new TextDecoder).decode(base64.toArrayBuffer(str,urlMode))};base64.fromString=(str,urlMode)=>{return base64.fromArrayBuffer((new TextEncoder).encode(str),urlMode)};base64.validate=(encoded,urlMode)=>{if(!(typeof encoded==="string"||encoded instanceof String)){return false}try{return urlMode?base64UrlPattern.test(encoded):base64Pattern.test(encoded)}catch(_e){return false}};base64.base64=base64;export{base64,base64 as default};
|
||||
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.mjs.map
generated
vendored
Normal file
1
api.hyungi.net/node_modules/@hexagon/base64/dist/base64.min.mjs.map
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["dist/base64.mjs"],"names":["chars","charsUrl","genLookup","lookupTemp","Uint8Array","len","length","let","i","target","charCodeAt","lookup","lookupUrl","base64UrlPattern","base64Pattern","base64","toArrayBuffer","data","urlMode","bufferLength","p","encoded1","encoded2","encoded3","encoded4","arraybuffer","ArrayBuffer","bytes","fromArrayBuffer","arrBuf","result","remainder","substring","toString","str","TextDecoder","decode","fromString","TextEncoder","encode","validate","encoded","String","test","_e"],"mappings":"AA4BA,MAECA,MAAQ,mEAGRC,SAAW,mEAEXC,UAAY,SACX,MAAMC,WAAa,OAAOC,aAAe,YAAc,GAAK,IAAIA,WAAW,GAAG,EAC9E,MAAMC,IAAML,MAAMM,OAClB,IAAKC,IAAIC,EAAI,EAAGA,EAAIH,IAAKG,CAAC,GAAI,CAC7BL,WAAWM,OAAOC,WAAWF,CAAC,GAAKA,CACpC,CACA,OAAOL,UACR,EAGAQ,OAAST,UAAUF,KAAK,EACxBY,UAAYV,UAAUD,QAAQ,EAK/B,MAAMY,iBAAmB,qBACzB,MAAMC,cAAgB,0BAKtB,MAAMC,OAAS,GAUfA,OAAOC,cAAgB,CAACC,KAAMC,WAC7B,MACCb,IAAMY,KAAKX,OACZC,IAAIY,aAAeF,KAAKX,OAAS,IAChCE,EACAY,EAAI,EACJC,SACAC,SACAC,SACAC,SAED,GAAIP,KAAKA,KAAKX,OAAS,KAAO,IAAK,CAClCa,YAAY,GACZ,GAAIF,KAAKA,KAAKX,OAAS,KAAO,IAAK,CAClCa,YAAY,EACb,CACD,CAEA,MACCM,YAAc,IAAIC,YAAYP,YAAY,EAC1CQ,MAAQ,IAAIvB,WAAWqB,WAAW,EAClChB,OAASS,QAAUN,UAAYD,OAEhC,IAAKH,EAAI,EAAGA,EAAIH,IAAKG,GAAK,EAAG,CAC5Ba,SAAWZ,OAAOQ,KAAKP,WAAWF,CAAC,GACnCc,SAAWb,OAAOQ,KAAKP,WAAWF,EAAI,CAAC,GACvCe,SAAWd,OAAOQ,KAAKP,WAAWF,EAAI,CAAC,GACvCgB,SAAWf,OAAOQ,KAAKP,WAAWF,EAAI,CAAC,GAEvCmB,MAAMP,CAAC,IAAOC,UAAY,EAAMC,UAAY,EAC5CK,MAAMP,CAAC,KAAQE,SAAW,KAAO,EAAMC,UAAY,EACnDI,MAAMP,CAAC,KAAQG,SAAW,IAAM,EAAMC,SAAW,EAClD,CAEA,OAAOC,WAER,EAUAV,OAAOa,gBAAkB,CAACC,OAAQX,WACjC,MAAMS,MAAQ,IAAIvB,WAAWyB,MAAM,EACnCtB,IACCC,EACAsB,OAAS,GAEV,MACCzB,IAAMsB,MAAMrB,OACZG,OAASS,QAAUjB,SAAWD,MAE/B,IAAKQ,EAAI,EAAGA,EAAIH,IAAKG,GAAK,EAAG,CAC5BsB,QAAUrB,OAAOkB,MAAMnB,IAAM,GAC7BsB,QAAUrB,QAASkB,MAAMnB,GAAK,IAAM,EAAMmB,MAAMnB,EAAI,IAAM,GAC1DsB,QAAUrB,QAASkB,MAAMnB,EAAI,GAAK,KAAO,EAAMmB,MAAMnB,EAAI,IAAM,GAC/DsB,QAAUrB,OAAOkB,MAAMnB,EAAI,GAAK,GACjC,CAEA,MAAMuB,UAAY1B,IAAM,EACxB,GAAI0B,YAAc,EAAG,CACpBD,OAASA,OAAOE,UAAU,EAAGF,OAAOxB,OAAS,CAAC,GAAKY,QAAU,GAAK,IACnE,MAAO,GAAIa,YAAc,EAAG,CAC3BD,OAASA,OAAOE,UAAU,EAAGF,OAAOxB,OAAS,CAAC,GAAKY,QAAU,GAAK,KACnE,CAEA,OAAOY,MAER,EAUAf,OAAOkB,SAAW,CAACC,IAAKhB,WACvB,OAAO,IAAIiB,aAAcC,OAAOrB,OAAOC,cAAckB,IAAKhB,OAAO,CAAC,CACnE,EAUAH,OAAOsB,WAAa,CAACH,IAAKhB,WACzB,OAAOH,OAAOa,iBAAgB,IAAIU,aAAcC,OAAOL,GAAG,EAAGhB,OAAO,CACrE,EASAH,OAAOyB,SAAW,CAACC,QAASvB,WAG3B,GAAI,EAAE,OAAOuB,UAAY,UAAYA,mBAAmBC,QAAS,CAChE,OAAO,KACR,CAGA,IACC,OAAOxB,QAAUL,iBAAiB8B,KAAKF,OAAO,EAAI3B,cAAc6B,KAAKF,OAAO,CAG7E,CAFE,MAAOG,IACR,OAAO,KACR,CACD,EAEA7B,OAAOA,OAASA,cAEPA,OAAQA,iBAAmB"}
|
||||
73
api.hyungi.net/node_modules/@hexagon/base64/package.json
generated
vendored
Normal file
73
api.hyungi.net/node_modules/@hexagon/base64/package.json
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "@hexagon/base64",
|
||||
"version": "1.1.28",
|
||||
"description": "Base64 and base64url to string or arraybuffer, and back. Node, Deno or browser.",
|
||||
"author": "Hexagon <github.com/hexagon>",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Niklas von Hertzen",
|
||||
"email": "niklasvh@gmail.com",
|
||||
"url": "https://hertzen.com"
|
||||
}
|
||||
],
|
||||
"homepage": "https://base64.56k.guru",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hexagon/base64"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/hexagon/base64/issues"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"src/*",
|
||||
"types/*",
|
||||
"SECURITY.md"
|
||||
],
|
||||
"keywords": [
|
||||
"base64",
|
||||
"base64url",
|
||||
"parser",
|
||||
"base64",
|
||||
"isomorphic",
|
||||
"arraybuffer",
|
||||
"string"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "uvu test test.base64.js",
|
||||
"test:dist": "uvu test/node/js && npm run test:ts",
|
||||
"test:coverage": "c8 --include=src npm test",
|
||||
"test:lint": "eslint ./**/*.js ./**/*.cjs",
|
||||
"test:lint:fix": "eslint --fix ./**/*.js ./**/*.cjs",
|
||||
"test:ts": "tsc --strict --noEmit ./test/node/ts/basics.ts",
|
||||
"build": "npm update && npm run build:precleanup && npm run test:lint && npm run build:typings && npm run build:dist && npm run build:minify && npm run build:cleanup && npm run test:coverage && npm run test:dist",
|
||||
"build:ci": "npm run test:lint && npm run build:typings && npm run build:dist && npm run build:minify && npm run build:cleanup && npm run test:coverage && npm run test:dist",
|
||||
"build:precleanup": "(rm -rf types/* || del /Q types\\*) && (rm -rf dist/* || del /Q dist\\*)",
|
||||
"build:dist": "rollup -c ./rollup.config.js",
|
||||
"build:minify": "uglifyjs dist/base64.cjs --source-map -o dist/base64.min.js && uglifyjs dist/base64.mjs --source-map -o dist/base64.min.mjs",
|
||||
"build:typings": "tsc",
|
||||
"build:cleanup": "(rm dist/base64.mjs || del dist\\base64.mjs)"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./dist/base64.cjs",
|
||||
"browser": "./dist/base64.min.js",
|
||||
"module": "./src/base64.js",
|
||||
"types": "types/base64.single.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/base64.js",
|
||||
"require": "./dist/base64.cjs",
|
||||
"browser": "./dist/base64.min.js"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"c8": "^8.0.1",
|
||||
"eslint": "^8.46.0",
|
||||
"jsdoc": "^4.0.2",
|
||||
"rollup": "^3.27.2",
|
||||
"typescript": "^5.2.2",
|
||||
"uglify-js": "^3.17.4",
|
||||
"uvu": "^0.5.6"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
190
api.hyungi.net/node_modules/@hexagon/base64/src/base64.js
generated
vendored
Normal file
190
api.hyungi.net/node_modules/@hexagon/base64/src/base64.js
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
/* ------------------------------------------------------------------------------------
|
||||
|
||||
base64 - MIT License - Hexagon <hexagon@56k.guru>
|
||||
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
License:
|
||||
|
||||
Copyright (c) 2021 Hexagon <hexagon@56k.guru>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
------------------------------------------------------------------------------------ */
|
||||
|
||||
const
|
||||
// Regular base64 characters
|
||||
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
|
||||
|
||||
// Base64url characters
|
||||
charsUrl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
||||
|
||||
genLookup = (target) => {
|
||||
const lookupTemp = typeof Uint8Array === "undefined" ? [] : new Uint8Array(256);
|
||||
const len = chars.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
lookupTemp[target.charCodeAt(i)] = i;
|
||||
}
|
||||
return lookupTemp;
|
||||
},
|
||||
|
||||
// Use a lookup table to find the index.
|
||||
lookup = genLookup(chars),
|
||||
lookupUrl = genLookup(charsUrl);
|
||||
|
||||
/**
|
||||
* Pre-calculated regexes for validating base64 and base64url
|
||||
*/
|
||||
const base64UrlPattern = /^[-A-Za-z0-9\-_]*$/;
|
||||
const base64Pattern = /^[-A-Za-z0-9+/]*={0,3}$/;
|
||||
|
||||
/**
|
||||
* @namespace base64
|
||||
*/
|
||||
const base64 = {};
|
||||
|
||||
/**
|
||||
* Convenience function for converting a base64 encoded string to an ArrayBuffer instance
|
||||
* @public
|
||||
*
|
||||
* @param {string} data - Base64 representation of data
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be expected
|
||||
* @returns {ArrayBuffer} - Decoded data
|
||||
*/
|
||||
base64.toArrayBuffer = (data, urlMode) => {
|
||||
const
|
||||
len = data.length;
|
||||
let bufferLength = data.length * 0.75,
|
||||
i,
|
||||
p = 0,
|
||||
encoded1,
|
||||
encoded2,
|
||||
encoded3,
|
||||
encoded4;
|
||||
|
||||
if (data[data.length - 1] === "=") {
|
||||
bufferLength--;
|
||||
if (data[data.length - 2] === "=") {
|
||||
bufferLength--;
|
||||
}
|
||||
}
|
||||
|
||||
const
|
||||
arraybuffer = new ArrayBuffer(bufferLength),
|
||||
bytes = new Uint8Array(arraybuffer),
|
||||
target = urlMode ? lookupUrl : lookup;
|
||||
|
||||
for (i = 0; i < len; i += 4) {
|
||||
encoded1 = target[data.charCodeAt(i)];
|
||||
encoded2 = target[data.charCodeAt(i + 1)];
|
||||
encoded3 = target[data.charCodeAt(i + 2)];
|
||||
encoded4 = target[data.charCodeAt(i + 3)];
|
||||
|
||||
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||
}
|
||||
|
||||
return arraybuffer;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function for creating a base64 encoded string from an ArrayBuffer instance
|
||||
* @public
|
||||
*
|
||||
* @param {ArrayBuffer} arrBuf - ArrayBuffer to be encoded
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
|
||||
* @returns {string} - Base64 representation of data
|
||||
*/
|
||||
base64.fromArrayBuffer = (arrBuf, urlMode) => {
|
||||
const bytes = new Uint8Array(arrBuf);
|
||||
let
|
||||
i,
|
||||
result = "";
|
||||
|
||||
const
|
||||
len = bytes.length,
|
||||
target = urlMode ? charsUrl : chars;
|
||||
|
||||
for (i = 0; i < len; i += 3) {
|
||||
result += target[bytes[i] >> 2];
|
||||
result += target[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||
result += target[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||
result += target[bytes[i + 2] & 63];
|
||||
}
|
||||
|
||||
const remainder = len % 3;
|
||||
if (remainder === 2) {
|
||||
result = result.substring(0, result.length - 1) + (urlMode ? "" : "=");
|
||||
} else if (remainder === 1) {
|
||||
result = result.substring(0, result.length - 2) + (urlMode ? "" : "==");
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function for converting base64 to string
|
||||
* @public
|
||||
*
|
||||
* @param {string} str - Base64 encoded string to be decoded
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be expected
|
||||
* @returns {string} - Decoded string
|
||||
*/
|
||||
base64.toString = (str, urlMode) => {
|
||||
return new TextDecoder().decode(base64.toArrayBuffer(str, urlMode));
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function for converting a javascript string to base64
|
||||
* @public
|
||||
*
|
||||
* @param {string} str - String to be converted to base64
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
|
||||
* @returns {string} - Base64 encoded string
|
||||
*/
|
||||
base64.fromString = (str, urlMode) => {
|
||||
return base64.fromArrayBuffer(new TextEncoder().encode(str), urlMode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to validate base64
|
||||
* @public
|
||||
* @param {string} encoded - Base64 or Base64url encoded data
|
||||
* @param {boolean} [urlMode] - If set to true, base64url will be expected
|
||||
* @returns {boolean} - Valid base64/base64url?
|
||||
*/
|
||||
base64.validate = (encoded, urlMode) => {
|
||||
|
||||
// Bail out if not string
|
||||
if (!(typeof encoded === "string" || encoded instanceof String)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go on validate
|
||||
try {
|
||||
return urlMode ? base64UrlPattern.test(encoded) : base64Pattern.test(encoded);
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
base64.base64 = base64;
|
||||
export default base64;
|
||||
export { base64 };
|
||||
3
api.hyungi.net/node_modules/@hexagon/base64/src/base64.single.js
generated
vendored
Normal file
3
api.hyungi.net/node_modules/@hexagon/base64/src/base64.single.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import base64 from "./base64.js";
|
||||
|
||||
export default base64;
|
||||
48
api.hyungi.net/node_modules/@hexagon/base64/types/base64.d.ts
generated
vendored
Normal file
48
api.hyungi.net/node_modules/@hexagon/base64/types/base64.d.ts
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
export default base64;
|
||||
export namespace base64 {
|
||||
/**
|
||||
* Convenience function for converting a base64 encoded string to an ArrayBuffer instance
|
||||
* @public
|
||||
*
|
||||
* @param {string} data - Base64 representation of data
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be expected
|
||||
* @returns {ArrayBuffer} - Decoded data
|
||||
*/
|
||||
export function toArrayBuffer(data: string, urlMode?: boolean): ArrayBuffer;
|
||||
/**
|
||||
* Convenience function for creating a base64 encoded string from an ArrayBuffer instance
|
||||
* @public
|
||||
*
|
||||
* @param {ArrayBuffer} arrBuf - ArrayBuffer to be encoded
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
|
||||
* @returns {string} - Base64 representation of data
|
||||
*/
|
||||
export function fromArrayBuffer(arrBuf: ArrayBuffer, urlMode?: boolean): string;
|
||||
/**
|
||||
* Convenience function for converting base64 to string
|
||||
* @public
|
||||
*
|
||||
* @param {string} str - Base64 encoded string to be decoded
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be expected
|
||||
* @returns {string} - Decoded string
|
||||
*/
|
||||
export function toString(str: string, urlMode?: boolean): string;
|
||||
/**
|
||||
* Convenience function for converting a javascript string to base64
|
||||
* @public
|
||||
*
|
||||
* @param {string} str - String to be converted to base64
|
||||
* @param {boolean} [urlMode] - If set to true, URL mode string will be returned
|
||||
* @returns {string} - Base64 encoded string
|
||||
*/
|
||||
export function fromString(str: string, urlMode?: boolean): string;
|
||||
/**
|
||||
* Function to validate base64
|
||||
* @public
|
||||
* @param {string} encoded - Base64 or Base64url encoded data
|
||||
* @param {boolean} [urlMode] - If set to true, base64url will be expected
|
||||
* @returns {boolean} - Valid base64/base64url?
|
||||
*/
|
||||
export function validate(encoded: string, urlMode?: boolean): boolean;
|
||||
export { base64 };
|
||||
}
|
||||
2
api.hyungi.net/node_modules/@hexagon/base64/types/base64.single.d.ts
generated
vendored
Normal file
2
api.hyungi.net/node_modules/@hexagon/base64/types/base64.single.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export default base64;
|
||||
import base64 from "./base64.js";
|
||||
21
api.hyungi.net/node_modules/@levischuck/tiny-cbor/LICENSE
generated
vendored
Normal file
21
api.hyungi.net/node_modules/@levischuck/tiny-cbor/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Levi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
81
api.hyungi.net/node_modules/@levischuck/tiny-cbor/README.md
generated
vendored
Normal file
81
api.hyungi.net/node_modules/@levischuck/tiny-cbor/README.md
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# Tiny CBOR
|
||||
|
||||
[](https://github.com/LeviSchuck/tiny-cbor/actions)
|
||||
[](https://codecov.io/gh/levischuck/tiny-cbor)
|
||||
[](https://www.npmjs.com/package/@levischuck/tiny-cbor)
|
||||
[](https://jsr.io/@levischuck/tiny-cbor)
|
||||
[](https://github.com/LeviSchuck/tiny-cbor/blob/main/LICENSE.txt)
|
||||

|
||||
|
||||
This minimal generic library decodes and encodes most useful CBOR structures
|
||||
into simple JavaScript structures:
|
||||
|
||||
- Maps with keys as `string`s or `number`s with `CBORType` values as a `Map`
|
||||
- Arrays of `CBORType` values
|
||||
- integers as `number`s
|
||||
- float32 and float64 as `number`s
|
||||
- float16 `NaN`, `Infinity`, `-Infinity`
|
||||
- `string`s
|
||||
- byte strings as `Uint8Array`
|
||||
- booleans
|
||||
- `null` and `undefined`
|
||||
- tags as `CBORTag(tag, value)`
|
||||
|
||||
## Limitations
|
||||
|
||||
This implementation does not support:
|
||||
|
||||
- indefinite length maps, arrays, text strings, or byte strings.
|
||||
- half precision floating point numbers
|
||||
- integers outside the range of `[-9007199254740991, 9007199254740991]`, see
|
||||
[Number.MAX_SAFE_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
|
||||
- native output to JSON
|
||||
- does not support generic objects, only `Map`s
|
||||
|
||||
This implementation has the following constraints:
|
||||
|
||||
- Map keys may only be strings or numbers
|
||||
- Tags are not interpreted
|
||||
|
||||
## Behavior
|
||||
|
||||
Maps that have duplicate keys will throw an error during decoding. Decoding data
|
||||
that is incomplete will throw an error during decoding.
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
// NPM
|
||||
// import { decodeCBOR } from "@levischuck/tiny-cbor";
|
||||
// or JSR
|
||||
// import { decodeCBOR } from "jsr:@levischuck/tiny-cbor";
|
||||
import { decodeCBOR } from "./index.ts";
|
||||
// Get your bytes somehow, directly or with decodeBase64 / decodeHex (available through @levischuck/tiny-encodings)
|
||||
const HELLO_WORLD_BYTES = new Uint8Array([
|
||||
107, // String wih length 11
|
||||
104, // h
|
||||
101, // e
|
||||
108, // l
|
||||
108, // l
|
||||
111, // o
|
||||
32, // Space
|
||||
119, // w
|
||||
111, // o
|
||||
114, // r
|
||||
108, // l
|
||||
100, // d
|
||||
]);
|
||||
const helloWorld = decodeCBOR(HELLO_WORLD_BYTES);
|
||||
if ("hello world" == helloWorld) {
|
||||
console.log("Success!");
|
||||
}
|
||||
```
|
||||
|
||||
## Where to get it
|
||||
|
||||
This library is available on
|
||||
[NPM](https://www.npmjs.com/package/@levischuck/tiny-cbor) and
|
||||
[JSR](https://jsr.io/@levischuck/tiny-cbor).
|
||||
|
||||
This library is no longer automatically published to Deno's Third Party Modules.
|
||||
Newer versions may appear on deno.land/x, but do not work.
|
||||
101
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor.d.ts
generated
vendored
Normal file
101
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor.d.ts
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* A value which is wrapped with a CBOR Tag.
|
||||
* Several tags are registered with defined meanings like 0 for a date string.
|
||||
* These meanings are **not interpreted** when decoded or encoded.
|
||||
*
|
||||
* This class is an immutable record.
|
||||
* If the tag number or value needs to change, then construct a new tag
|
||||
*/
|
||||
export declare class CBORTag {
|
||||
private tagId;
|
||||
private tagValue;
|
||||
/**
|
||||
* Wrap a value with a tag number.
|
||||
* When encoded, this tag will be attached to the value.
|
||||
*
|
||||
* @param tag Tag number
|
||||
* @param value Wrapped value
|
||||
*/
|
||||
constructor(tag: number, value: CBORType);
|
||||
/**
|
||||
* Read the tag number
|
||||
*/
|
||||
get tag(): number;
|
||||
/**
|
||||
* Read the value
|
||||
*/
|
||||
get value(): CBORType;
|
||||
}
|
||||
/**
|
||||
* Supported types which are encodable and decodable with tiny CBOR.
|
||||
* Note that plain javascript objects are omitted.
|
||||
*/
|
||||
export type CBORType = number | bigint | string | Uint8Array | boolean | null | undefined | CBORType[] | CBORTag | Map<string | number, CBORType>;
|
||||
/**
|
||||
* Like {decodeCBOR}, but the length of the data is unknown and there is likely
|
||||
* more -- possibly unrelated non-CBOR -- data afterwards.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodePartialCBOR} from './cbor.ts'
|
||||
* decodePartialCBOR(new Uint8Array([1, 2, 245, 3, 4]), 2)
|
||||
* // returns [true, 1]
|
||||
* // It did not decode the leading [1, 2] or trailing [3, 4]
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream to read data from
|
||||
* @param index where to start reading in the data stream
|
||||
* @returns a tuple of the value followed by bytes read.
|
||||
* @throws {Error}
|
||||
* When the data stream ends early or the CBOR data is not well formed
|
||||
*/
|
||||
export declare function decodePartialCBOR(data: DataView | Uint8Array | ArrayBuffer, index: number): [CBORType, number];
|
||||
/**
|
||||
* Decode CBOR data from a binary stream
|
||||
*
|
||||
* The entire data stream from [0, length) will be consumed.
|
||||
* If you require a partial decoding, see {decodePartialCBOR}.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodeCBOR, CBORTag, CBORType} from './cbor.ts'
|
||||
* decodeCBOR(new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32, 118, 97, 108, 117, 101]));
|
||||
* // returns new Map<string | number, CBORType>([
|
||||
* // ["key", "value"],
|
||||
* // [1, "another value"]
|
||||
* // ]);
|
||||
*
|
||||
* const taggedItem = new Uint8Array([217, 4, 210, 101, 104, 101, 108, 108, 111]);
|
||||
* decodeCBOR(new DataView(taggedItem.buffer))
|
||||
* // returns new CBORTag(1234, "hello")
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream, multiple types are supported
|
||||
* @returns
|
||||
*/
|
||||
export declare function decodeCBOR(data: DataView | Uint8Array | ArrayBuffer): CBORType;
|
||||
/**
|
||||
* Encode a supported structure to a CBOR byte string.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```ts
|
||||
* import {encodeCBOR, CBORType, CBORTag} from './cbor.ts'
|
||||
* encodeCBOR(new Map<string | number, CBORType>([
|
||||
* ["key", "value"],
|
||||
* [1, "another value"]
|
||||
* ]));
|
||||
* // returns new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32 118, 97, 108, 117, 101])
|
||||
*
|
||||
* encodeCBOR(new CBORTag(1234, "hello"))
|
||||
* // returns new UInt8Array([217, 4, 210, 101, 104, 101, 108, 108, 111])
|
||||
* ```
|
||||
*
|
||||
* @param data Data to encode
|
||||
* @returns A byte string as a Uint8Array
|
||||
* @throws Error
|
||||
* if unsupported data is found during encoding
|
||||
*/
|
||||
export declare function encodeCBOR(data: CBORType): Uint8Array;
|
||||
440
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor.js
generated
vendored
Normal file
440
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor.js
generated
vendored
Normal file
@@ -0,0 +1,440 @@
|
||||
import { decodeLength, encodeLength, MAJOR_TYPE_ARRAY, MAJOR_TYPE_BYTE_STRING, MAJOR_TYPE_MAP, MAJOR_TYPE_NEGATIVE_INTEGER, MAJOR_TYPE_SIMPLE_OR_FLOAT, MAJOR_TYPE_TAG, MAJOR_TYPE_TEXT_STRING, MAJOR_TYPE_UNSIGNED_INTEGER, } from "./cbor_internal.js";
|
||||
/**
|
||||
* A value which is wrapped with a CBOR Tag.
|
||||
* Several tags are registered with defined meanings like 0 for a date string.
|
||||
* These meanings are **not interpreted** when decoded or encoded.
|
||||
*
|
||||
* This class is an immutable record.
|
||||
* If the tag number or value needs to change, then construct a new tag
|
||||
*/
|
||||
export class CBORTag {
|
||||
/**
|
||||
* Wrap a value with a tag number.
|
||||
* When encoded, this tag will be attached to the value.
|
||||
*
|
||||
* @param tag Tag number
|
||||
* @param value Wrapped value
|
||||
*/
|
||||
constructor(tag, value) {
|
||||
Object.defineProperty(this, "tagId", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "tagValue", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
this.tagId = tag;
|
||||
this.tagValue = value;
|
||||
}
|
||||
/**
|
||||
* Read the tag number
|
||||
*/
|
||||
get tag() {
|
||||
return this.tagId;
|
||||
}
|
||||
/**
|
||||
* Read the value
|
||||
*/
|
||||
get value() {
|
||||
return this.tagValue;
|
||||
}
|
||||
}
|
||||
function decodeUnsignedInteger(data, argument, index) {
|
||||
return decodeLength(data, argument, index);
|
||||
}
|
||||
function decodeNegativeInteger(data, argument, index) {
|
||||
const [value, length] = decodeUnsignedInteger(data, argument, index);
|
||||
return [-value - 1, length];
|
||||
}
|
||||
function decodeByteString(data, argument, index) {
|
||||
const [lengthValue, lengthConsumed] = decodeLength(data, argument, index);
|
||||
const dataStartIndex = index + lengthConsumed;
|
||||
return [
|
||||
new Uint8Array(data.buffer.slice(dataStartIndex, dataStartIndex + lengthValue)),
|
||||
lengthConsumed + lengthValue,
|
||||
];
|
||||
}
|
||||
const TEXT_DECODER = new TextDecoder();
|
||||
function decodeString(data, argument, index) {
|
||||
const [value, length] = decodeByteString(data, argument, index);
|
||||
return [TEXT_DECODER.decode(value), length];
|
||||
}
|
||||
function decodeArray(data, argument, index) {
|
||||
if (argument === 0) {
|
||||
return [[], 1];
|
||||
}
|
||||
const [length, lengthConsumed] = decodeLength(data, argument, index);
|
||||
let consumedLength = lengthConsumed;
|
||||
const value = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
const remainingDataLength = data.byteLength - index - consumedLength;
|
||||
if (remainingDataLength <= 0) {
|
||||
throw new Error("array is not supported or well formed");
|
||||
}
|
||||
const [decodedValue, consumed] = decodeNext(data, index + consumedLength);
|
||||
value.push(decodedValue);
|
||||
consumedLength += consumed;
|
||||
}
|
||||
return [value, consumedLength];
|
||||
}
|
||||
const MAP_ERROR = "Map is not supported or well formed";
|
||||
function decodeMap(data, argument, index) {
|
||||
if (argument === 0) {
|
||||
return [new Map(), 1];
|
||||
}
|
||||
const [length, lengthConsumed] = decodeLength(data, argument, index);
|
||||
let consumedLength = lengthConsumed;
|
||||
const result = new Map();
|
||||
for (let i = 0; i < length; i++) {
|
||||
let remainingDataLength = data.byteLength - index - consumedLength;
|
||||
if (remainingDataLength <= 0) {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// Load key
|
||||
const [key, keyConsumed] = decodeNext(data, index + consumedLength);
|
||||
consumedLength += keyConsumed;
|
||||
remainingDataLength -= keyConsumed;
|
||||
// Check that there's enough to have a value
|
||||
if (remainingDataLength <= 0) {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// Technically CBOR maps can have any type as the key, and so can JS Maps
|
||||
// However, JS Maps can only reference such keys as references which would
|
||||
// require key iteration and pattern matching.
|
||||
// For simplicity, since such keys are not in use with WebAuthn, this
|
||||
// capability is not implemented and the types are restricted to strings
|
||||
// and numbers.
|
||||
if (typeof key !== "string" && typeof key !== "number") {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// CBOR Maps are not well formed if there are duplicate keys
|
||||
if (result.has(key)) {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// Load value
|
||||
const [value, valueConsumed] = decodeNext(data, index + consumedLength);
|
||||
consumedLength += valueConsumed;
|
||||
result.set(key, value);
|
||||
}
|
||||
return [result, consumedLength];
|
||||
}
|
||||
function decodeFloat16(data, index) {
|
||||
if (index + 3 > data.byteLength) {
|
||||
throw new Error("CBOR stream ended before end of Float 16");
|
||||
}
|
||||
// Skip the first byte
|
||||
const result = data.getUint16(index + 1, false);
|
||||
// A minimal selection of supported values
|
||||
if (result == 0x7c00) {
|
||||
return [Infinity, 3];
|
||||
}
|
||||
else if (result == 0x7e00) {
|
||||
return [NaN, 3];
|
||||
}
|
||||
else if (result == 0xfc00) {
|
||||
return [-Infinity, 3];
|
||||
}
|
||||
throw new Error("Float16 data is unsupported");
|
||||
}
|
||||
function decodeFloat32(data, index) {
|
||||
if (index + 5 > data.byteLength) {
|
||||
throw new Error("CBOR stream ended before end of Float 32");
|
||||
}
|
||||
// Skip the first byte
|
||||
const result = data.getFloat32(index + 1, false);
|
||||
// First byte + 4 byte float
|
||||
return [result, 5];
|
||||
}
|
||||
function decodeFloat64(data, index) {
|
||||
if (index + 9 > data.byteLength) {
|
||||
throw new Error("CBOR stream ended before end of Float 64");
|
||||
}
|
||||
// Skip the first byte
|
||||
const result = data.getFloat64(index + 1, false);
|
||||
// First byte + 8 byte float
|
||||
return [result, 9];
|
||||
}
|
||||
function decodeTag(data, argument, index) {
|
||||
const [tag, tagBytes] = decodeLength(data, argument, index);
|
||||
const [value, valueBytes] = decodeNext(data, index + tagBytes);
|
||||
return [new CBORTag(tag, value), tagBytes + valueBytes];
|
||||
}
|
||||
function decodeNext(data, index) {
|
||||
if (index >= data.byteLength) {
|
||||
throw new Error("CBOR stream ended before tag value");
|
||||
}
|
||||
const byte = data.getUint8(index);
|
||||
const majorType = byte >> 5;
|
||||
const argument = byte & 0x1f;
|
||||
switch (majorType) {
|
||||
case MAJOR_TYPE_UNSIGNED_INTEGER: {
|
||||
return decodeUnsignedInteger(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_NEGATIVE_INTEGER: {
|
||||
return decodeNegativeInteger(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_BYTE_STRING: {
|
||||
return decodeByteString(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_TEXT_STRING: {
|
||||
return decodeString(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_ARRAY: {
|
||||
return decodeArray(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_MAP: {
|
||||
return decodeMap(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_TAG: {
|
||||
return decodeTag(data, argument, index);
|
||||
}
|
||||
case MAJOR_TYPE_SIMPLE_OR_FLOAT: {
|
||||
switch (argument) {
|
||||
case 20:
|
||||
return [false, 1];
|
||||
case 21:
|
||||
return [true, 1];
|
||||
case 22:
|
||||
return [null, 1];
|
||||
case 23:
|
||||
return [undefined, 1];
|
||||
// 24: Simple value (value 32..255 in following byte)
|
||||
case 25: // IEEE 754 Half-Precision Float (16 bits follow)
|
||||
return decodeFloat16(data, index);
|
||||
case 26: // IEEE 754 Single-Precision Float (32 bits follow)
|
||||
return decodeFloat32(data, index);
|
||||
case 27: // IEEE 754 Double-Precision Float (64 bits follow)
|
||||
return decodeFloat64(data, index);
|
||||
// 28-30: Reserved, not well-formed in the present document
|
||||
// 31: "break" stop code for indefinite-length items
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Unsupported or not well formed at ${index}`);
|
||||
}
|
||||
function encodeSimple(data) {
|
||||
if (data === true) {
|
||||
return 0xf5;
|
||||
}
|
||||
else if (data === false) {
|
||||
return 0xf4;
|
||||
}
|
||||
else if (data === null) {
|
||||
return 0xf6;
|
||||
}
|
||||
// Else undefined
|
||||
return 0xf7;
|
||||
}
|
||||
function encodeFloat(data) {
|
||||
if (Math.fround(data) == data || !Number.isFinite(data) || Number.isNaN(data)) {
|
||||
// Float32
|
||||
const output = new Uint8Array(5);
|
||||
output[0] = 0xfa;
|
||||
const view = new DataView(output.buffer);
|
||||
view.setFloat32(1, data, false);
|
||||
return output;
|
||||
}
|
||||
else {
|
||||
// Float64
|
||||
const output = new Uint8Array(9);
|
||||
output[0] = 0xfb;
|
||||
const view = new DataView(output.buffer);
|
||||
view.setFloat64(1, data, false);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
function encodeNumber(data) {
|
||||
if (typeof data == "number") {
|
||||
if (Number.isSafeInteger(data)) {
|
||||
// Encode integer
|
||||
if (data < 0) {
|
||||
return encodeLength(MAJOR_TYPE_NEGATIVE_INTEGER, Math.abs(data));
|
||||
}
|
||||
else {
|
||||
return encodeLength(MAJOR_TYPE_UNSIGNED_INTEGER, data);
|
||||
}
|
||||
}
|
||||
return [encodeFloat(data)];
|
||||
}
|
||||
else {
|
||||
if (data < 0n) {
|
||||
return encodeLength(MAJOR_TYPE_NEGATIVE_INTEGER, data * -1n);
|
||||
}
|
||||
else {
|
||||
return encodeLength(MAJOR_TYPE_UNSIGNED_INTEGER, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
const ENCODER = new TextEncoder();
|
||||
function encodeString(data, output) {
|
||||
output.push(...encodeLength(MAJOR_TYPE_TEXT_STRING, data.length));
|
||||
output.push(ENCODER.encode(data));
|
||||
}
|
||||
function encodeBytes(data, output) {
|
||||
output.push(...encodeLength(MAJOR_TYPE_BYTE_STRING, data.length));
|
||||
output.push(data);
|
||||
}
|
||||
function encodeArray(data, output) {
|
||||
output.push(...encodeLength(MAJOR_TYPE_ARRAY, data.length));
|
||||
for (const element of data) {
|
||||
encodePartialCBOR(element, output);
|
||||
}
|
||||
}
|
||||
function encodeMap(data, output) {
|
||||
output.push(new Uint8Array(encodeLength(MAJOR_TYPE_MAP, data.size)));
|
||||
for (const [key, value] of data.entries()) {
|
||||
encodePartialCBOR(key, output);
|
||||
encodePartialCBOR(value, output);
|
||||
}
|
||||
}
|
||||
function encodeTag(tag, output) {
|
||||
output.push(...encodeLength(MAJOR_TYPE_TAG, tag.tag));
|
||||
encodePartialCBOR(tag.value, output);
|
||||
}
|
||||
function encodePartialCBOR(data, output) {
|
||||
if (typeof data == "boolean" || data === null || data == undefined) {
|
||||
output.push(encodeSimple(data));
|
||||
return;
|
||||
}
|
||||
if (typeof data == "number" || typeof data == "bigint") {
|
||||
output.push(...encodeNumber(data));
|
||||
return;
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
encodeString(data, output);
|
||||
return;
|
||||
}
|
||||
if (data instanceof Uint8Array) {
|
||||
encodeBytes(data, output);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
encodeArray(data, output);
|
||||
return;
|
||||
}
|
||||
if (data instanceof Map) {
|
||||
encodeMap(data, output);
|
||||
return;
|
||||
}
|
||||
if (data instanceof CBORTag) {
|
||||
encodeTag(data, output);
|
||||
return;
|
||||
}
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
/**
|
||||
* Like {decodeCBOR}, but the length of the data is unknown and there is likely
|
||||
* more -- possibly unrelated non-CBOR -- data afterwards.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodePartialCBOR} from './cbor.ts'
|
||||
* decodePartialCBOR(new Uint8Array([1, 2, 245, 3, 4]), 2)
|
||||
* // returns [true, 1]
|
||||
* // It did not decode the leading [1, 2] or trailing [3, 4]
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream to read data from
|
||||
* @param index where to start reading in the data stream
|
||||
* @returns a tuple of the value followed by bytes read.
|
||||
* @throws {Error}
|
||||
* When the data stream ends early or the CBOR data is not well formed
|
||||
*/
|
||||
export function decodePartialCBOR(data, index) {
|
||||
if (data.byteLength === 0 || data.byteLength <= index || index < 0) {
|
||||
throw new Error("No data");
|
||||
}
|
||||
if (data instanceof Uint8Array) {
|
||||
return decodeNext(new DataView(data.buffer), index);
|
||||
}
|
||||
else if (data instanceof ArrayBuffer) {
|
||||
return decodeNext(new DataView(data), index);
|
||||
}
|
||||
// otherwise, it is a data view
|
||||
return decodeNext(data, index);
|
||||
}
|
||||
/**
|
||||
* Decode CBOR data from a binary stream
|
||||
*
|
||||
* The entire data stream from [0, length) will be consumed.
|
||||
* If you require a partial decoding, see {decodePartialCBOR}.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodeCBOR, CBORTag, CBORType} from './cbor.ts'
|
||||
* decodeCBOR(new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32, 118, 97, 108, 117, 101]));
|
||||
* // returns new Map<string | number, CBORType>([
|
||||
* // ["key", "value"],
|
||||
* // [1, "another value"]
|
||||
* // ]);
|
||||
*
|
||||
* const taggedItem = new Uint8Array([217, 4, 210, 101, 104, 101, 108, 108, 111]);
|
||||
* decodeCBOR(new DataView(taggedItem.buffer))
|
||||
* // returns new CBORTag(1234, "hello")
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream, multiple types are supported
|
||||
* @returns
|
||||
*/
|
||||
export function decodeCBOR(data) {
|
||||
const [value, length] = decodePartialCBOR(data, 0);
|
||||
if (length !== data.byteLength) {
|
||||
throw new Error(`Data was decoded, but the whole stream was not processed ${length} != ${data.byteLength}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
/**
|
||||
* Encode a supported structure to a CBOR byte string.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```ts
|
||||
* import {encodeCBOR, CBORType, CBORTag} from './cbor.ts'
|
||||
* encodeCBOR(new Map<string | number, CBORType>([
|
||||
* ["key", "value"],
|
||||
* [1, "another value"]
|
||||
* ]));
|
||||
* // returns new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32 118, 97, 108, 117, 101])
|
||||
*
|
||||
* encodeCBOR(new CBORTag(1234, "hello"))
|
||||
* // returns new UInt8Array([217, 4, 210, 101, 104, 101, 108, 108, 111])
|
||||
* ```
|
||||
*
|
||||
* @param data Data to encode
|
||||
* @returns A byte string as a Uint8Array
|
||||
* @throws Error
|
||||
* if unsupported data is found during encoding
|
||||
*/
|
||||
export function encodeCBOR(data) {
|
||||
const results = [];
|
||||
encodePartialCBOR(data, results);
|
||||
let length = 0;
|
||||
for (const result of results) {
|
||||
if (typeof result == "number") {
|
||||
length += 1;
|
||||
}
|
||||
else {
|
||||
length += result.length;
|
||||
}
|
||||
}
|
||||
const output = new Uint8Array(length);
|
||||
let index = 0;
|
||||
for (const result of results) {
|
||||
if (typeof result == "number") {
|
||||
output[index] = result;
|
||||
index += 1;
|
||||
}
|
||||
else {
|
||||
output.set(result, index);
|
||||
index += result.length;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
11
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor_internal.d.ts
generated
vendored
Normal file
11
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor_internal.d.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export declare function decodeLength(data: DataView, argument: number, index: number): [number, number];
|
||||
export type MajorType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export declare const MAJOR_TYPE_UNSIGNED_INTEGER: MajorType;
|
||||
export declare const MAJOR_TYPE_NEGATIVE_INTEGER: MajorType;
|
||||
export declare const MAJOR_TYPE_BYTE_STRING: MajorType;
|
||||
export declare const MAJOR_TYPE_TEXT_STRING: MajorType;
|
||||
export declare const MAJOR_TYPE_ARRAY: MajorType;
|
||||
export declare const MAJOR_TYPE_MAP: MajorType;
|
||||
export declare const MAJOR_TYPE_TAG: MajorType;
|
||||
export declare const MAJOR_TYPE_SIMPLE_OR_FLOAT: MajorType;
|
||||
export declare function encodeLength(major: MajorType, argument: number | bigint): number[];
|
||||
111
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor_internal.js
generated
vendored
Normal file
111
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor_internal.js
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
export function decodeLength(data, argument, index) {
|
||||
if (argument < 24) {
|
||||
return [argument, 1];
|
||||
}
|
||||
const remainingDataLength = data.byteLength - index - 1;
|
||||
const view = new DataView(data.buffer, index + 1);
|
||||
let output;
|
||||
let bytes = 0;
|
||||
switch (argument) {
|
||||
case 24: {
|
||||
if (remainingDataLength > 0) {
|
||||
output = view.getUint8(0);
|
||||
bytes = 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 25: {
|
||||
if (remainingDataLength > 1) {
|
||||
output = view.getUint16(0, false);
|
||||
bytes = 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
if (remainingDataLength > 3) {
|
||||
output = view.getUint32(0, false);
|
||||
bytes = 5;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 27: {
|
||||
if (remainingDataLength > 7) {
|
||||
const bigOutput = view.getBigUint64(0, false);
|
||||
// Bound it to [24, MAX_SAFE_INTEGER], where it is safe
|
||||
// to encode as a javascript number
|
||||
if (bigOutput >= 24n && bigOutput <= Number.MAX_SAFE_INTEGER) {
|
||||
return [Number(bigOutput), 9];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (output && output >= 24) {
|
||||
return [output, bytes];
|
||||
}
|
||||
throw new Error("Length not supported or not well formed");
|
||||
}
|
||||
export const MAJOR_TYPE_UNSIGNED_INTEGER = 0;
|
||||
export const MAJOR_TYPE_NEGATIVE_INTEGER = 1;
|
||||
export const MAJOR_TYPE_BYTE_STRING = 2;
|
||||
export const MAJOR_TYPE_TEXT_STRING = 3;
|
||||
export const MAJOR_TYPE_ARRAY = 4;
|
||||
export const MAJOR_TYPE_MAP = 5;
|
||||
export const MAJOR_TYPE_TAG = 6;
|
||||
export const MAJOR_TYPE_SIMPLE_OR_FLOAT = 7;
|
||||
export function encodeLength(major, argument) {
|
||||
const majorEncoded = major << 5;
|
||||
if (argument < 0) {
|
||||
throw new Error("CBOR Data Item argument must not be negative");
|
||||
}
|
||||
// Convert to bigint first.
|
||||
// Encode integers around and above 32 bits in big endian / network byte order
|
||||
// is unreliable in javascript.
|
||||
// https://tc39.es/ecma262/#sec-bitwise-shift-operators
|
||||
// Bit shifting operations result in 32 bit signed numbers
|
||||
let bigintArgument;
|
||||
if (typeof argument == "number") {
|
||||
if (!Number.isInteger(argument)) {
|
||||
throw new Error("CBOR Data Item argument must be an integer");
|
||||
}
|
||||
bigintArgument = BigInt(argument);
|
||||
}
|
||||
else {
|
||||
bigintArgument = argument;
|
||||
}
|
||||
// Negative 0 is not a thing
|
||||
if (major == MAJOR_TYPE_NEGATIVE_INTEGER) {
|
||||
if (bigintArgument == 0n) {
|
||||
throw new Error("CBOR Data Item argument cannot be zero when negative");
|
||||
}
|
||||
bigintArgument = bigintArgument - 1n;
|
||||
}
|
||||
if (bigintArgument > 18446744073709551615n) {
|
||||
throw new Error("CBOR number out of range");
|
||||
}
|
||||
// Encode into 64 bits and extract the tail
|
||||
const buffer = new Uint8Array(8);
|
||||
const view = new DataView(buffer.buffer);
|
||||
view.setBigUint64(0, bigintArgument, false);
|
||||
if (bigintArgument <= 23) {
|
||||
return [majorEncoded | buffer[7]];
|
||||
}
|
||||
else if (bigintArgument <= 255) {
|
||||
return [majorEncoded | 24, buffer[7]];
|
||||
}
|
||||
else if (bigintArgument <= 65535) {
|
||||
return [majorEncoded | 25, ...buffer.slice(6)];
|
||||
}
|
||||
else if (bigintArgument <= 4294967295) {
|
||||
return [
|
||||
majorEncoded | 26,
|
||||
...buffer.slice(4),
|
||||
];
|
||||
}
|
||||
else {
|
||||
return [
|
||||
majorEncoded | 27,
|
||||
...buffer,
|
||||
];
|
||||
}
|
||||
}
|
||||
2
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/index.d.ts
generated
vendored
Normal file
2
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CBORTag, decodeCBOR, decodePartialCBOR, encodeCBOR, } from "./cbor/cbor.js";
|
||||
export type { CBORType } from "./cbor/cbor.js";
|
||||
1
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/index.js
generated
vendored
Normal file
1
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export { CBORTag, decodeCBOR, decodePartialCBOR, encodeCBOR, } from "./cbor/cbor.js";
|
||||
3
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/package.json
generated
vendored
Normal file
3
api.hyungi.net/node_modules/@levischuck/tiny-cbor/esm/package.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
25
api.hyungi.net/node_modules/@levischuck/tiny-cbor/package.json
generated
vendored
Normal file
25
api.hyungi.net/node_modules/@levischuck/tiny-cbor/package.json
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@levischuck/tiny-cbor",
|
||||
"version": "0.2.11",
|
||||
"description": "Tiny CBOR library",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/levischuck/tiny-cbor.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/levischuck/tiny-cbor/issues"
|
||||
},
|
||||
"main": "./script/index.js",
|
||||
"module": "./esm/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./esm/index.js",
|
||||
"require": "./script/index.js"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.9.0"
|
||||
},
|
||||
"_generatedBy": "dnt@0.40.0"
|
||||
}
|
||||
101
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor.d.ts
generated
vendored
Normal file
101
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor.d.ts
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* A value which is wrapped with a CBOR Tag.
|
||||
* Several tags are registered with defined meanings like 0 for a date string.
|
||||
* These meanings are **not interpreted** when decoded or encoded.
|
||||
*
|
||||
* This class is an immutable record.
|
||||
* If the tag number or value needs to change, then construct a new tag
|
||||
*/
|
||||
export declare class CBORTag {
|
||||
private tagId;
|
||||
private tagValue;
|
||||
/**
|
||||
* Wrap a value with a tag number.
|
||||
* When encoded, this tag will be attached to the value.
|
||||
*
|
||||
* @param tag Tag number
|
||||
* @param value Wrapped value
|
||||
*/
|
||||
constructor(tag: number, value: CBORType);
|
||||
/**
|
||||
* Read the tag number
|
||||
*/
|
||||
get tag(): number;
|
||||
/**
|
||||
* Read the value
|
||||
*/
|
||||
get value(): CBORType;
|
||||
}
|
||||
/**
|
||||
* Supported types which are encodable and decodable with tiny CBOR.
|
||||
* Note that plain javascript objects are omitted.
|
||||
*/
|
||||
export type CBORType = number | bigint | string | Uint8Array | boolean | null | undefined | CBORType[] | CBORTag | Map<string | number, CBORType>;
|
||||
/**
|
||||
* Like {decodeCBOR}, but the length of the data is unknown and there is likely
|
||||
* more -- possibly unrelated non-CBOR -- data afterwards.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodePartialCBOR} from './cbor.ts'
|
||||
* decodePartialCBOR(new Uint8Array([1, 2, 245, 3, 4]), 2)
|
||||
* // returns [true, 1]
|
||||
* // It did not decode the leading [1, 2] or trailing [3, 4]
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream to read data from
|
||||
* @param index where to start reading in the data stream
|
||||
* @returns a tuple of the value followed by bytes read.
|
||||
* @throws {Error}
|
||||
* When the data stream ends early or the CBOR data is not well formed
|
||||
*/
|
||||
export declare function decodePartialCBOR(data: DataView | Uint8Array | ArrayBuffer, index: number): [CBORType, number];
|
||||
/**
|
||||
* Decode CBOR data from a binary stream
|
||||
*
|
||||
* The entire data stream from [0, length) will be consumed.
|
||||
* If you require a partial decoding, see {decodePartialCBOR}.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodeCBOR, CBORTag, CBORType} from './cbor.ts'
|
||||
* decodeCBOR(new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32, 118, 97, 108, 117, 101]));
|
||||
* // returns new Map<string | number, CBORType>([
|
||||
* // ["key", "value"],
|
||||
* // [1, "another value"]
|
||||
* // ]);
|
||||
*
|
||||
* const taggedItem = new Uint8Array([217, 4, 210, 101, 104, 101, 108, 108, 111]);
|
||||
* decodeCBOR(new DataView(taggedItem.buffer))
|
||||
* // returns new CBORTag(1234, "hello")
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream, multiple types are supported
|
||||
* @returns
|
||||
*/
|
||||
export declare function decodeCBOR(data: DataView | Uint8Array | ArrayBuffer): CBORType;
|
||||
/**
|
||||
* Encode a supported structure to a CBOR byte string.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```ts
|
||||
* import {encodeCBOR, CBORType, CBORTag} from './cbor.ts'
|
||||
* encodeCBOR(new Map<string | number, CBORType>([
|
||||
* ["key", "value"],
|
||||
* [1, "another value"]
|
||||
* ]));
|
||||
* // returns new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32 118, 97, 108, 117, 101])
|
||||
*
|
||||
* encodeCBOR(new CBORTag(1234, "hello"))
|
||||
* // returns new UInt8Array([217, 4, 210, 101, 104, 101, 108, 108, 111])
|
||||
* ```
|
||||
*
|
||||
* @param data Data to encode
|
||||
* @returns A byte string as a Uint8Array
|
||||
* @throws Error
|
||||
* if unsupported data is found during encoding
|
||||
*/
|
||||
export declare function encodeCBOR(data: CBORType): Uint8Array;
|
||||
447
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor.js
generated
vendored
Normal file
447
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor.js
generated
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encodeCBOR = exports.decodeCBOR = exports.decodePartialCBOR = exports.CBORTag = void 0;
|
||||
const cbor_internal_js_1 = require("./cbor_internal.js");
|
||||
/**
|
||||
* A value which is wrapped with a CBOR Tag.
|
||||
* Several tags are registered with defined meanings like 0 for a date string.
|
||||
* These meanings are **not interpreted** when decoded or encoded.
|
||||
*
|
||||
* This class is an immutable record.
|
||||
* If the tag number or value needs to change, then construct a new tag
|
||||
*/
|
||||
class CBORTag {
|
||||
/**
|
||||
* Wrap a value with a tag number.
|
||||
* When encoded, this tag will be attached to the value.
|
||||
*
|
||||
* @param tag Tag number
|
||||
* @param value Wrapped value
|
||||
*/
|
||||
constructor(tag, value) {
|
||||
Object.defineProperty(this, "tagId", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
Object.defineProperty(this, "tagValue", {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: void 0
|
||||
});
|
||||
this.tagId = tag;
|
||||
this.tagValue = value;
|
||||
}
|
||||
/**
|
||||
* Read the tag number
|
||||
*/
|
||||
get tag() {
|
||||
return this.tagId;
|
||||
}
|
||||
/**
|
||||
* Read the value
|
||||
*/
|
||||
get value() {
|
||||
return this.tagValue;
|
||||
}
|
||||
}
|
||||
exports.CBORTag = CBORTag;
|
||||
function decodeUnsignedInteger(data, argument, index) {
|
||||
return (0, cbor_internal_js_1.decodeLength)(data, argument, index);
|
||||
}
|
||||
function decodeNegativeInteger(data, argument, index) {
|
||||
const [value, length] = decodeUnsignedInteger(data, argument, index);
|
||||
return [-value - 1, length];
|
||||
}
|
||||
function decodeByteString(data, argument, index) {
|
||||
const [lengthValue, lengthConsumed] = (0, cbor_internal_js_1.decodeLength)(data, argument, index);
|
||||
const dataStartIndex = index + lengthConsumed;
|
||||
return [
|
||||
new Uint8Array(data.buffer.slice(dataStartIndex, dataStartIndex + lengthValue)),
|
||||
lengthConsumed + lengthValue,
|
||||
];
|
||||
}
|
||||
const TEXT_DECODER = new TextDecoder();
|
||||
function decodeString(data, argument, index) {
|
||||
const [value, length] = decodeByteString(data, argument, index);
|
||||
return [TEXT_DECODER.decode(value), length];
|
||||
}
|
||||
function decodeArray(data, argument, index) {
|
||||
if (argument === 0) {
|
||||
return [[], 1];
|
||||
}
|
||||
const [length, lengthConsumed] = (0, cbor_internal_js_1.decodeLength)(data, argument, index);
|
||||
let consumedLength = lengthConsumed;
|
||||
const value = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
const remainingDataLength = data.byteLength - index - consumedLength;
|
||||
if (remainingDataLength <= 0) {
|
||||
throw new Error("array is not supported or well formed");
|
||||
}
|
||||
const [decodedValue, consumed] = decodeNext(data, index + consumedLength);
|
||||
value.push(decodedValue);
|
||||
consumedLength += consumed;
|
||||
}
|
||||
return [value, consumedLength];
|
||||
}
|
||||
const MAP_ERROR = "Map is not supported or well formed";
|
||||
function decodeMap(data, argument, index) {
|
||||
if (argument === 0) {
|
||||
return [new Map(), 1];
|
||||
}
|
||||
const [length, lengthConsumed] = (0, cbor_internal_js_1.decodeLength)(data, argument, index);
|
||||
let consumedLength = lengthConsumed;
|
||||
const result = new Map();
|
||||
for (let i = 0; i < length; i++) {
|
||||
let remainingDataLength = data.byteLength - index - consumedLength;
|
||||
if (remainingDataLength <= 0) {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// Load key
|
||||
const [key, keyConsumed] = decodeNext(data, index + consumedLength);
|
||||
consumedLength += keyConsumed;
|
||||
remainingDataLength -= keyConsumed;
|
||||
// Check that there's enough to have a value
|
||||
if (remainingDataLength <= 0) {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// Technically CBOR maps can have any type as the key, and so can JS Maps
|
||||
// However, JS Maps can only reference such keys as references which would
|
||||
// require key iteration and pattern matching.
|
||||
// For simplicity, since such keys are not in use with WebAuthn, this
|
||||
// capability is not implemented and the types are restricted to strings
|
||||
// and numbers.
|
||||
if (typeof key !== "string" && typeof key !== "number") {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// CBOR Maps are not well formed if there are duplicate keys
|
||||
if (result.has(key)) {
|
||||
throw new Error(MAP_ERROR);
|
||||
}
|
||||
// Load value
|
||||
const [value, valueConsumed] = decodeNext(data, index + consumedLength);
|
||||
consumedLength += valueConsumed;
|
||||
result.set(key, value);
|
||||
}
|
||||
return [result, consumedLength];
|
||||
}
|
||||
function decodeFloat16(data, index) {
|
||||
if (index + 3 > data.byteLength) {
|
||||
throw new Error("CBOR stream ended before end of Float 16");
|
||||
}
|
||||
// Skip the first byte
|
||||
const result = data.getUint16(index + 1, false);
|
||||
// A minimal selection of supported values
|
||||
if (result == 0x7c00) {
|
||||
return [Infinity, 3];
|
||||
}
|
||||
else if (result == 0x7e00) {
|
||||
return [NaN, 3];
|
||||
}
|
||||
else if (result == 0xfc00) {
|
||||
return [-Infinity, 3];
|
||||
}
|
||||
throw new Error("Float16 data is unsupported");
|
||||
}
|
||||
function decodeFloat32(data, index) {
|
||||
if (index + 5 > data.byteLength) {
|
||||
throw new Error("CBOR stream ended before end of Float 32");
|
||||
}
|
||||
// Skip the first byte
|
||||
const result = data.getFloat32(index + 1, false);
|
||||
// First byte + 4 byte float
|
||||
return [result, 5];
|
||||
}
|
||||
function decodeFloat64(data, index) {
|
||||
if (index + 9 > data.byteLength) {
|
||||
throw new Error("CBOR stream ended before end of Float 64");
|
||||
}
|
||||
// Skip the first byte
|
||||
const result = data.getFloat64(index + 1, false);
|
||||
// First byte + 8 byte float
|
||||
return [result, 9];
|
||||
}
|
||||
function decodeTag(data, argument, index) {
|
||||
const [tag, tagBytes] = (0, cbor_internal_js_1.decodeLength)(data, argument, index);
|
||||
const [value, valueBytes] = decodeNext(data, index + tagBytes);
|
||||
return [new CBORTag(tag, value), tagBytes + valueBytes];
|
||||
}
|
||||
function decodeNext(data, index) {
|
||||
if (index >= data.byteLength) {
|
||||
throw new Error("CBOR stream ended before tag value");
|
||||
}
|
||||
const byte = data.getUint8(index);
|
||||
const majorType = byte >> 5;
|
||||
const argument = byte & 0x1f;
|
||||
switch (majorType) {
|
||||
case cbor_internal_js_1.MAJOR_TYPE_UNSIGNED_INTEGER: {
|
||||
return decodeUnsignedInteger(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_NEGATIVE_INTEGER: {
|
||||
return decodeNegativeInteger(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_BYTE_STRING: {
|
||||
return decodeByteString(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_TEXT_STRING: {
|
||||
return decodeString(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_ARRAY: {
|
||||
return decodeArray(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_MAP: {
|
||||
return decodeMap(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_TAG: {
|
||||
return decodeTag(data, argument, index);
|
||||
}
|
||||
case cbor_internal_js_1.MAJOR_TYPE_SIMPLE_OR_FLOAT: {
|
||||
switch (argument) {
|
||||
case 20:
|
||||
return [false, 1];
|
||||
case 21:
|
||||
return [true, 1];
|
||||
case 22:
|
||||
return [null, 1];
|
||||
case 23:
|
||||
return [undefined, 1];
|
||||
// 24: Simple value (value 32..255 in following byte)
|
||||
case 25: // IEEE 754 Half-Precision Float (16 bits follow)
|
||||
return decodeFloat16(data, index);
|
||||
case 26: // IEEE 754 Single-Precision Float (32 bits follow)
|
||||
return decodeFloat32(data, index);
|
||||
case 27: // IEEE 754 Double-Precision Float (64 bits follow)
|
||||
return decodeFloat64(data, index);
|
||||
// 28-30: Reserved, not well-formed in the present document
|
||||
// 31: "break" stop code for indefinite-length items
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Unsupported or not well formed at ${index}`);
|
||||
}
|
||||
function encodeSimple(data) {
|
||||
if (data === true) {
|
||||
return 0xf5;
|
||||
}
|
||||
else if (data === false) {
|
||||
return 0xf4;
|
||||
}
|
||||
else if (data === null) {
|
||||
return 0xf6;
|
||||
}
|
||||
// Else undefined
|
||||
return 0xf7;
|
||||
}
|
||||
function encodeFloat(data) {
|
||||
if (Math.fround(data) == data || !Number.isFinite(data) || Number.isNaN(data)) {
|
||||
// Float32
|
||||
const output = new Uint8Array(5);
|
||||
output[0] = 0xfa;
|
||||
const view = new DataView(output.buffer);
|
||||
view.setFloat32(1, data, false);
|
||||
return output;
|
||||
}
|
||||
else {
|
||||
// Float64
|
||||
const output = new Uint8Array(9);
|
||||
output[0] = 0xfb;
|
||||
const view = new DataView(output.buffer);
|
||||
view.setFloat64(1, data, false);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
function encodeNumber(data) {
|
||||
if (typeof data == "number") {
|
||||
if (Number.isSafeInteger(data)) {
|
||||
// Encode integer
|
||||
if (data < 0) {
|
||||
return (0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_NEGATIVE_INTEGER, Math.abs(data));
|
||||
}
|
||||
else {
|
||||
return (0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_UNSIGNED_INTEGER, data);
|
||||
}
|
||||
}
|
||||
return [encodeFloat(data)];
|
||||
}
|
||||
else {
|
||||
if (data < 0n) {
|
||||
return (0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_NEGATIVE_INTEGER, data * -1n);
|
||||
}
|
||||
else {
|
||||
return (0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_UNSIGNED_INTEGER, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
const ENCODER = new TextEncoder();
|
||||
function encodeString(data, output) {
|
||||
output.push(...(0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_TEXT_STRING, data.length));
|
||||
output.push(ENCODER.encode(data));
|
||||
}
|
||||
function encodeBytes(data, output) {
|
||||
output.push(...(0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_BYTE_STRING, data.length));
|
||||
output.push(data);
|
||||
}
|
||||
function encodeArray(data, output) {
|
||||
output.push(...(0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_ARRAY, data.length));
|
||||
for (const element of data) {
|
||||
encodePartialCBOR(element, output);
|
||||
}
|
||||
}
|
||||
function encodeMap(data, output) {
|
||||
output.push(new Uint8Array((0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_MAP, data.size)));
|
||||
for (const [key, value] of data.entries()) {
|
||||
encodePartialCBOR(key, output);
|
||||
encodePartialCBOR(value, output);
|
||||
}
|
||||
}
|
||||
function encodeTag(tag, output) {
|
||||
output.push(...(0, cbor_internal_js_1.encodeLength)(cbor_internal_js_1.MAJOR_TYPE_TAG, tag.tag));
|
||||
encodePartialCBOR(tag.value, output);
|
||||
}
|
||||
function encodePartialCBOR(data, output) {
|
||||
if (typeof data == "boolean" || data === null || data == undefined) {
|
||||
output.push(encodeSimple(data));
|
||||
return;
|
||||
}
|
||||
if (typeof data == "number" || typeof data == "bigint") {
|
||||
output.push(...encodeNumber(data));
|
||||
return;
|
||||
}
|
||||
if (typeof data == "string") {
|
||||
encodeString(data, output);
|
||||
return;
|
||||
}
|
||||
if (data instanceof Uint8Array) {
|
||||
encodeBytes(data, output);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
encodeArray(data, output);
|
||||
return;
|
||||
}
|
||||
if (data instanceof Map) {
|
||||
encodeMap(data, output);
|
||||
return;
|
||||
}
|
||||
if (data instanceof CBORTag) {
|
||||
encodeTag(data, output);
|
||||
return;
|
||||
}
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
/**
|
||||
* Like {decodeCBOR}, but the length of the data is unknown and there is likely
|
||||
* more -- possibly unrelated non-CBOR -- data afterwards.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodePartialCBOR} from './cbor.ts'
|
||||
* decodePartialCBOR(new Uint8Array([1, 2, 245, 3, 4]), 2)
|
||||
* // returns [true, 1]
|
||||
* // It did not decode the leading [1, 2] or trailing [3, 4]
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream to read data from
|
||||
* @param index where to start reading in the data stream
|
||||
* @returns a tuple of the value followed by bytes read.
|
||||
* @throws {Error}
|
||||
* When the data stream ends early or the CBOR data is not well formed
|
||||
*/
|
||||
function decodePartialCBOR(data, index) {
|
||||
if (data.byteLength === 0 || data.byteLength <= index || index < 0) {
|
||||
throw new Error("No data");
|
||||
}
|
||||
if (data instanceof Uint8Array) {
|
||||
return decodeNext(new DataView(data.buffer), index);
|
||||
}
|
||||
else if (data instanceof ArrayBuffer) {
|
||||
return decodeNext(new DataView(data), index);
|
||||
}
|
||||
// otherwise, it is a data view
|
||||
return decodeNext(data, index);
|
||||
}
|
||||
exports.decodePartialCBOR = decodePartialCBOR;
|
||||
/**
|
||||
* Decode CBOR data from a binary stream
|
||||
*
|
||||
* The entire data stream from [0, length) will be consumed.
|
||||
* If you require a partial decoding, see {decodePartialCBOR}.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```ts
|
||||
* import {decodeCBOR, CBORTag, CBORType} from './cbor.ts'
|
||||
* decodeCBOR(new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32, 118, 97, 108, 117, 101]));
|
||||
* // returns new Map<string | number, CBORType>([
|
||||
* // ["key", "value"],
|
||||
* // [1, "another value"]
|
||||
* // ]);
|
||||
*
|
||||
* const taggedItem = new Uint8Array([217, 4, 210, 101, 104, 101, 108, 108, 111]);
|
||||
* decodeCBOR(new DataView(taggedItem.buffer))
|
||||
* // returns new CBORTag(1234, "hello")
|
||||
* ```
|
||||
*
|
||||
* @param data a data stream, multiple types are supported
|
||||
* @returns
|
||||
*/
|
||||
function decodeCBOR(data) {
|
||||
const [value, length] = decodePartialCBOR(data, 0);
|
||||
if (length !== data.byteLength) {
|
||||
throw new Error(`Data was decoded, but the whole stream was not processed ${length} != ${data.byteLength}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
exports.decodeCBOR = decodeCBOR;
|
||||
/**
|
||||
* Encode a supported structure to a CBOR byte string.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```ts
|
||||
* import {encodeCBOR, CBORType, CBORTag} from './cbor.ts'
|
||||
* encodeCBOR(new Map<string | number, CBORType>([
|
||||
* ["key", "value"],
|
||||
* [1, "another value"]
|
||||
* ]));
|
||||
* // returns new Uint8Array([162, 99, 107, 101, 121, 101, 118, 97, 108, 117, 101, 1, 109, 97, 110, 111, 116, 104, 101, 114, 32 118, 97, 108, 117, 101])
|
||||
*
|
||||
* encodeCBOR(new CBORTag(1234, "hello"))
|
||||
* // returns new UInt8Array([217, 4, 210, 101, 104, 101, 108, 108, 111])
|
||||
* ```
|
||||
*
|
||||
* @param data Data to encode
|
||||
* @returns A byte string as a Uint8Array
|
||||
* @throws Error
|
||||
* if unsupported data is found during encoding
|
||||
*/
|
||||
function encodeCBOR(data) {
|
||||
const results = [];
|
||||
encodePartialCBOR(data, results);
|
||||
let length = 0;
|
||||
for (const result of results) {
|
||||
if (typeof result == "number") {
|
||||
length += 1;
|
||||
}
|
||||
else {
|
||||
length += result.length;
|
||||
}
|
||||
}
|
||||
const output = new Uint8Array(length);
|
||||
let index = 0;
|
||||
for (const result of results) {
|
||||
if (typeof result == "number") {
|
||||
output[index] = result;
|
||||
index += 1;
|
||||
}
|
||||
else {
|
||||
output.set(result, index);
|
||||
index += result.length;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.encodeCBOR = encodeCBOR;
|
||||
11
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor_internal.d.ts
generated
vendored
Normal file
11
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor_internal.d.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export declare function decodeLength(data: DataView, argument: number, index: number): [number, number];
|
||||
export type MajorType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export declare const MAJOR_TYPE_UNSIGNED_INTEGER: MajorType;
|
||||
export declare const MAJOR_TYPE_NEGATIVE_INTEGER: MajorType;
|
||||
export declare const MAJOR_TYPE_BYTE_STRING: MajorType;
|
||||
export declare const MAJOR_TYPE_TEXT_STRING: MajorType;
|
||||
export declare const MAJOR_TYPE_ARRAY: MajorType;
|
||||
export declare const MAJOR_TYPE_MAP: MajorType;
|
||||
export declare const MAJOR_TYPE_TAG: MajorType;
|
||||
export declare const MAJOR_TYPE_SIMPLE_OR_FLOAT: MajorType;
|
||||
export declare function encodeLength(major: MajorType, argument: number | bigint): number[];
|
||||
116
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor_internal.js
generated
vendored
Normal file
116
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/cbor/cbor_internal.js
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encodeLength = exports.MAJOR_TYPE_SIMPLE_OR_FLOAT = exports.MAJOR_TYPE_TAG = exports.MAJOR_TYPE_MAP = exports.MAJOR_TYPE_ARRAY = exports.MAJOR_TYPE_TEXT_STRING = exports.MAJOR_TYPE_BYTE_STRING = exports.MAJOR_TYPE_NEGATIVE_INTEGER = exports.MAJOR_TYPE_UNSIGNED_INTEGER = exports.decodeLength = void 0;
|
||||
function decodeLength(data, argument, index) {
|
||||
if (argument < 24) {
|
||||
return [argument, 1];
|
||||
}
|
||||
const remainingDataLength = data.byteLength - index - 1;
|
||||
const view = new DataView(data.buffer, index + 1);
|
||||
let output;
|
||||
let bytes = 0;
|
||||
switch (argument) {
|
||||
case 24: {
|
||||
if (remainingDataLength > 0) {
|
||||
output = view.getUint8(0);
|
||||
bytes = 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 25: {
|
||||
if (remainingDataLength > 1) {
|
||||
output = view.getUint16(0, false);
|
||||
bytes = 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 26: {
|
||||
if (remainingDataLength > 3) {
|
||||
output = view.getUint32(0, false);
|
||||
bytes = 5;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 27: {
|
||||
if (remainingDataLength > 7) {
|
||||
const bigOutput = view.getBigUint64(0, false);
|
||||
// Bound it to [24, MAX_SAFE_INTEGER], where it is safe
|
||||
// to encode as a javascript number
|
||||
if (bigOutput >= 24n && bigOutput <= Number.MAX_SAFE_INTEGER) {
|
||||
return [Number(bigOutput), 9];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (output && output >= 24) {
|
||||
return [output, bytes];
|
||||
}
|
||||
throw new Error("Length not supported or not well formed");
|
||||
}
|
||||
exports.decodeLength = decodeLength;
|
||||
exports.MAJOR_TYPE_UNSIGNED_INTEGER = 0;
|
||||
exports.MAJOR_TYPE_NEGATIVE_INTEGER = 1;
|
||||
exports.MAJOR_TYPE_BYTE_STRING = 2;
|
||||
exports.MAJOR_TYPE_TEXT_STRING = 3;
|
||||
exports.MAJOR_TYPE_ARRAY = 4;
|
||||
exports.MAJOR_TYPE_MAP = 5;
|
||||
exports.MAJOR_TYPE_TAG = 6;
|
||||
exports.MAJOR_TYPE_SIMPLE_OR_FLOAT = 7;
|
||||
function encodeLength(major, argument) {
|
||||
const majorEncoded = major << 5;
|
||||
if (argument < 0) {
|
||||
throw new Error("CBOR Data Item argument must not be negative");
|
||||
}
|
||||
// Convert to bigint first.
|
||||
// Encode integers around and above 32 bits in big endian / network byte order
|
||||
// is unreliable in javascript.
|
||||
// https://tc39.es/ecma262/#sec-bitwise-shift-operators
|
||||
// Bit shifting operations result in 32 bit signed numbers
|
||||
let bigintArgument;
|
||||
if (typeof argument == "number") {
|
||||
if (!Number.isInteger(argument)) {
|
||||
throw new Error("CBOR Data Item argument must be an integer");
|
||||
}
|
||||
bigintArgument = BigInt(argument);
|
||||
}
|
||||
else {
|
||||
bigintArgument = argument;
|
||||
}
|
||||
// Negative 0 is not a thing
|
||||
if (major == exports.MAJOR_TYPE_NEGATIVE_INTEGER) {
|
||||
if (bigintArgument == 0n) {
|
||||
throw new Error("CBOR Data Item argument cannot be zero when negative");
|
||||
}
|
||||
bigintArgument = bigintArgument - 1n;
|
||||
}
|
||||
if (bigintArgument > 18446744073709551615n) {
|
||||
throw new Error("CBOR number out of range");
|
||||
}
|
||||
// Encode into 64 bits and extract the tail
|
||||
const buffer = new Uint8Array(8);
|
||||
const view = new DataView(buffer.buffer);
|
||||
view.setBigUint64(0, bigintArgument, false);
|
||||
if (bigintArgument <= 23) {
|
||||
return [majorEncoded | buffer[7]];
|
||||
}
|
||||
else if (bigintArgument <= 255) {
|
||||
return [majorEncoded | 24, buffer[7]];
|
||||
}
|
||||
else if (bigintArgument <= 65535) {
|
||||
return [majorEncoded | 25, ...buffer.slice(6)];
|
||||
}
|
||||
else if (bigintArgument <= 4294967295) {
|
||||
return [
|
||||
majorEncoded | 26,
|
||||
...buffer.slice(4),
|
||||
];
|
||||
}
|
||||
else {
|
||||
return [
|
||||
majorEncoded | 27,
|
||||
...buffer,
|
||||
];
|
||||
}
|
||||
}
|
||||
exports.encodeLength = encodeLength;
|
||||
2
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/index.d.ts
generated
vendored
Normal file
2
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CBORTag, decodeCBOR, decodePartialCBOR, encodeCBOR, } from "./cbor/cbor.js";
|
||||
export type { CBORType } from "./cbor/cbor.js";
|
||||
8
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/index.js
generated
vendored
Normal file
8
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/index.js
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.encodeCBOR = exports.decodePartialCBOR = exports.decodeCBOR = exports.CBORTag = void 0;
|
||||
var cbor_js_1 = require("./cbor/cbor.js");
|
||||
Object.defineProperty(exports, "CBORTag", { enumerable: true, get: function () { return cbor_js_1.CBORTag; } });
|
||||
Object.defineProperty(exports, "decodeCBOR", { enumerable: true, get: function () { return cbor_js_1.decodeCBOR; } });
|
||||
Object.defineProperty(exports, "decodePartialCBOR", { enumerable: true, get: function () { return cbor_js_1.decodePartialCBOR; } });
|
||||
Object.defineProperty(exports, "encodeCBOR", { enumerable: true, get: function () { return cbor_js_1.encodeCBOR; } });
|
||||
3
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/package.json
generated
vendored
Normal file
3
api.hyungi.net/node_modules/@levischuck/tiny-cbor/script/package.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
20
api.hyungi.net/node_modules/@npmcli/fs/LICENSE.md
generated
vendored
Normal file
20
api.hyungi.net/node_modules/@npmcli/fs/LICENSE.md
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<!-- This file is automatically added by @npmcli/template-oss. Do not edit. -->
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright npm, Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this
|
||||
software for any purpose with or without fee is hereby
|
||||
granted, provided that the above copyright notice and this
|
||||
permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND NPM DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
|
||||
EVENT SHALL NPM BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
|
||||
USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
60
api.hyungi.net/node_modules/@npmcli/fs/README.md
generated
vendored
Normal file
60
api.hyungi.net/node_modules/@npmcli/fs/README.md
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# @npmcli/fs
|
||||
|
||||
polyfills, and extensions, of the core `fs` module.
|
||||
|
||||
## Features
|
||||
|
||||
- all exposed functions return promises
|
||||
- `fs.rm` polyfill for node versions < 14.14.0
|
||||
- `fs.mkdir` polyfill adding support for the `recursive` and `force` options in node versions < 10.12.0
|
||||
- `fs.copyFile` extended to accept an `owner` option
|
||||
- `fs.mkdir` extended to accept an `owner` option
|
||||
- `fs.mkdtemp` extended to accept an `owner` option
|
||||
- `fs.writeFile` extended to accept an `owner` option
|
||||
- `fs.withTempDir` added
|
||||
- `fs.cp` polyfill for node < 16.7.0
|
||||
|
||||
## The `owner` option
|
||||
|
||||
The `copyFile`, `mkdir`, `mkdtemp`, `writeFile`, and `withTempDir` functions
|
||||
all accept a new `owner` property in their options. It can be used in two ways:
|
||||
|
||||
- `{ owner: { uid: 100, gid: 100 } }` - set the `uid` and `gid` explicitly
|
||||
- `{ owner: 100 }` - use one value, will set both `uid` and `gid` the same
|
||||
|
||||
The special string `'inherit'` may be passed instead of a number, which will
|
||||
cause this module to automatically determine the correct `uid` and/or `gid`
|
||||
from the nearest existing parent directory of the target.
|
||||
|
||||
## `fs.withTempDir(root, fn, options) -> Promise`
|
||||
|
||||
### Parameters
|
||||
|
||||
- `root`: the directory in which to create the temporary directory
|
||||
- `fn`: a function that will be called with the path to the temporary directory
|
||||
- `options`
|
||||
- `tmpPrefix`: a prefix to be used in the generated directory name
|
||||
|
||||
### Usage
|
||||
|
||||
The `withTempDir` function creates a temporary directory, runs the provided
|
||||
function (`fn`), then removes the temporary directory and resolves or rejects
|
||||
based on the result of `fn`.
|
||||
|
||||
```js
|
||||
const fs = require('@npmcli/fs')
|
||||
const os = require('os')
|
||||
|
||||
// this function will be called with the full path to the temporary directory
|
||||
// it is called with `await` behind the scenes, so can be async if desired.
|
||||
const myFunction = async (tempPath) => {
|
||||
return 'done!'
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const result = await fs.withTempDir(os.tmpdir(), myFunction)
|
||||
// result === 'done!'
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
17
api.hyungi.net/node_modules/@npmcli/fs/lib/common/file-url-to-path/index.js
generated
vendored
Normal file
17
api.hyungi.net/node_modules/@npmcli/fs/lib/common/file-url-to-path/index.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
const url = require('url')
|
||||
|
||||
const node = require('../node.js')
|
||||
const polyfill = require('./polyfill.js')
|
||||
|
||||
const useNative = node.satisfies('>=10.12.0')
|
||||
|
||||
const fileURLToPath = (path) => {
|
||||
// the polyfill is tested separately from this module, no need to hack
|
||||
// process.version to try to trigger it just for coverage
|
||||
// istanbul ignore next
|
||||
return useNative
|
||||
? url.fileURLToPath(path)
|
||||
: polyfill(path)
|
||||
}
|
||||
|
||||
module.exports = fileURLToPath
|
||||
121
api.hyungi.net/node_modules/@npmcli/fs/lib/common/file-url-to-path/polyfill.js
generated
vendored
Normal file
121
api.hyungi.net/node_modules/@npmcli/fs/lib/common/file-url-to-path/polyfill.js
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
const { URL, domainToUnicode } = require('url')
|
||||
|
||||
const CHAR_LOWERCASE_A = 97
|
||||
const CHAR_LOWERCASE_Z = 122
|
||||
|
||||
const isWindows = process.platform === 'win32'
|
||||
|
||||
class ERR_INVALID_FILE_URL_HOST extends TypeError {
|
||||
constructor (platform) {
|
||||
super(`File URL host must be "localhost" or empty on ${platform}`)
|
||||
this.code = 'ERR_INVALID_FILE_URL_HOST'
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${this.name} [${this.code}]: ${this.message}`
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_INVALID_FILE_URL_PATH extends TypeError {
|
||||
constructor (msg) {
|
||||
super(`File URL path ${msg}`)
|
||||
this.code = 'ERR_INVALID_FILE_URL_PATH'
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${this.name} [${this.code}]: ${this.message}`
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_INVALID_ARG_TYPE extends TypeError {
|
||||
constructor (name, actual) {
|
||||
super(`The "${name}" argument must be one of type string or an instance ` +
|
||||
`of URL. Received type ${typeof actual} ${actual}`)
|
||||
this.code = 'ERR_INVALID_ARG_TYPE'
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${this.name} [${this.code}]: ${this.message}`
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_INVALID_URL_SCHEME extends TypeError {
|
||||
constructor (expected) {
|
||||
super(`The URL must be of scheme ${expected}`)
|
||||
this.code = 'ERR_INVALID_URL_SCHEME'
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${this.name} [${this.code}]: ${this.message}`
|
||||
}
|
||||
}
|
||||
|
||||
const isURLInstance = (input) => {
|
||||
return input != null && input.href && input.origin
|
||||
}
|
||||
|
||||
const getPathFromURLWin32 = (url) => {
|
||||
const hostname = url.hostname
|
||||
let pathname = url.pathname
|
||||
for (let n = 0; n < pathname.length; n++) {
|
||||
if (pathname[n] === '%') {
|
||||
const third = pathname.codePointAt(n + 2) | 0x20
|
||||
if ((pathname[n + 1] === '2' && third === 102) ||
|
||||
(pathname[n + 1] === '5' && third === 99)) {
|
||||
throw new ERR_INVALID_FILE_URL_PATH('must not include encoded \\ or / characters')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pathname = pathname.replace(/\//g, '\\')
|
||||
pathname = decodeURIComponent(pathname)
|
||||
if (hostname !== '') {
|
||||
return `\\\\${domainToUnicode(hostname)}${pathname}`
|
||||
}
|
||||
|
||||
const letter = pathname.codePointAt(1) | 0x20
|
||||
const sep = pathname[2]
|
||||
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z ||
|
||||
(sep !== ':')) {
|
||||
throw new ERR_INVALID_FILE_URL_PATH('must be absolute')
|
||||
}
|
||||
|
||||
return pathname.slice(1)
|
||||
}
|
||||
|
||||
const getPathFromURLPosix = (url) => {
|
||||
if (url.hostname !== '') {
|
||||
throw new ERR_INVALID_FILE_URL_HOST(process.platform)
|
||||
}
|
||||
|
||||
const pathname = url.pathname
|
||||
|
||||
for (let n = 0; n < pathname.length; n++) {
|
||||
if (pathname[n] === '%') {
|
||||
const third = pathname.codePointAt(n + 2) | 0x20
|
||||
if (pathname[n + 1] === '2' && third === 102) {
|
||||
throw new ERR_INVALID_FILE_URL_PATH('must not include encoded / characters')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decodeURIComponent(pathname)
|
||||
}
|
||||
|
||||
const fileURLToPath = (path) => {
|
||||
if (typeof path === 'string') {
|
||||
path = new URL(path)
|
||||
} else if (!isURLInstance(path)) {
|
||||
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path)
|
||||
}
|
||||
|
||||
if (path.protocol !== 'file:') {
|
||||
throw new ERR_INVALID_URL_SCHEME('file')
|
||||
}
|
||||
|
||||
return isWindows
|
||||
? getPathFromURLWin32(path)
|
||||
: getPathFromURLPosix(path)
|
||||
}
|
||||
|
||||
module.exports = fileURLToPath
|
||||
20
api.hyungi.net/node_modules/@npmcli/fs/lib/common/get-options.js
generated
vendored
Normal file
20
api.hyungi.net/node_modules/@npmcli/fs/lib/common/get-options.js
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// given an input that may or may not be an object, return an object that has
|
||||
// a copy of every defined property listed in 'copy'. if the input is not an
|
||||
// object, assign it to the property named by 'wrap'
|
||||
const getOptions = (input, { copy, wrap }) => {
|
||||
const result = {}
|
||||
|
||||
if (input && typeof input === 'object') {
|
||||
for (const prop of copy) {
|
||||
if (input[prop] !== undefined) {
|
||||
result[prop] = input[prop]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[wrap] = input
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = getOptions
|
||||
9
api.hyungi.net/node_modules/@npmcli/fs/lib/common/node.js
generated
vendored
Normal file
9
api.hyungi.net/node_modules/@npmcli/fs/lib/common/node.js
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const semver = require('semver')
|
||||
|
||||
const satisfies = (range) => {
|
||||
return semver.satisfies(process.version, range, { includePrerelease: true })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
satisfies,
|
||||
}
|
||||
92
api.hyungi.net/node_modules/@npmcli/fs/lib/common/owner.js
generated
vendored
Normal file
92
api.hyungi.net/node_modules/@npmcli/fs/lib/common/owner.js
generated
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
const { dirname, resolve } = require('path')
|
||||
|
||||
const fileURLToPath = require('./file-url-to-path/index.js')
|
||||
const fs = require('../fs.js')
|
||||
|
||||
// given a path, find the owner of the nearest parent
|
||||
const find = async (path) => {
|
||||
// if we have no getuid, permissions are irrelevant on this platform
|
||||
if (!process.getuid) {
|
||||
return {}
|
||||
}
|
||||
|
||||
// fs methods accept URL objects with a scheme of file: so we need to unwrap
|
||||
// those into an actual path string before we can resolve it
|
||||
const resolved = path != null && path.href && path.origin
|
||||
? resolve(fileURLToPath(path))
|
||||
: resolve(path)
|
||||
|
||||
let stat
|
||||
|
||||
try {
|
||||
stat = await fs.lstat(resolved)
|
||||
} finally {
|
||||
// if we got a stat, return its contents
|
||||
if (stat) {
|
||||
return { uid: stat.uid, gid: stat.gid }
|
||||
}
|
||||
|
||||
// try the parent directory
|
||||
if (resolved !== dirname(resolved)) {
|
||||
return find(dirname(resolved))
|
||||
}
|
||||
|
||||
// no more parents, never got a stat, just return an empty object
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// given a path, uid, and gid update the ownership of the path if necessary
|
||||
const update = async (path, uid, gid) => {
|
||||
// nothing to update, just exit
|
||||
if (uid === undefined && gid === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// see if the permissions are already the same, if they are we don't
|
||||
// need to do anything, so return early
|
||||
const stat = await fs.stat(path)
|
||||
if (uid === stat.uid && gid === stat.gid) {
|
||||
return
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
try {
|
||||
await fs.chown(path, uid, gid)
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// accepts a `path` and the `owner` property of an options object and normalizes
|
||||
// it into an object with numerical `uid` and `gid`
|
||||
const validate = async (path, input) => {
|
||||
let uid
|
||||
let gid
|
||||
|
||||
if (typeof input === 'string' || typeof input === 'number') {
|
||||
uid = input
|
||||
gid = input
|
||||
} else if (input && typeof input === 'object') {
|
||||
uid = input.uid
|
||||
gid = input.gid
|
||||
}
|
||||
|
||||
if (uid === 'inherit' || gid === 'inherit') {
|
||||
const owner = await find(path)
|
||||
if (uid === 'inherit') {
|
||||
uid = owner.uid
|
||||
}
|
||||
|
||||
if (gid === 'inherit') {
|
||||
gid = owner.gid
|
||||
}
|
||||
}
|
||||
|
||||
return { uid, gid }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
find,
|
||||
update,
|
||||
validate,
|
||||
}
|
||||
22
api.hyungi.net/node_modules/@npmcli/fs/lib/copy-file.js
generated
vendored
Normal file
22
api.hyungi.net/node_modules/@npmcli/fs/lib/copy-file.js
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
const fs = require('./fs.js')
|
||||
const getOptions = require('./common/get-options.js')
|
||||
const owner = require('./common/owner.js')
|
||||
|
||||
const copyFile = async (src, dest, opts) => {
|
||||
const options = getOptions(opts, {
|
||||
copy: ['mode', 'owner'],
|
||||
wrap: 'mode',
|
||||
})
|
||||
|
||||
const { uid, gid } = await owner.validate(dest, options.owner)
|
||||
|
||||
// the node core method as of 16.5.0 does not support the mode being in an
|
||||
// object, so we have to pass the mode value directly
|
||||
const result = await fs.copyFile(src, dest, options.mode)
|
||||
|
||||
await owner.update(dest, uid, gid)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = copyFile
|
||||
15
api.hyungi.net/node_modules/@npmcli/fs/lib/cp/LICENSE
generated
vendored
Normal file
15
api.hyungi.net/node_modules/@npmcli/fs/lib/cp/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2011-2017 JP Richardson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
|
||||
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
22
api.hyungi.net/node_modules/@npmcli/fs/lib/cp/index.js
generated
vendored
Normal file
22
api.hyungi.net/node_modules/@npmcli/fs/lib/cp/index.js
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
const fs = require('../fs.js')
|
||||
const getOptions = require('../common/get-options.js')
|
||||
const node = require('../common/node.js')
|
||||
const polyfill = require('./polyfill.js')
|
||||
|
||||
// node 16.7.0 added fs.cp
|
||||
const useNative = node.satisfies('>=16.7.0')
|
||||
|
||||
const cp = async (src, dest, opts) => {
|
||||
const options = getOptions(opts, {
|
||||
copy: ['dereference', 'errorOnExist', 'filter', 'force', 'preserveTimestamps', 'recursive'],
|
||||
})
|
||||
|
||||
// the polyfill is tested separately from this module, no need to hack
|
||||
// process.version to try to trigger it just for coverage
|
||||
// istanbul ignore next
|
||||
return useNative
|
||||
? fs.cp(src, dest, options)
|
||||
: polyfill(src, dest, options)
|
||||
}
|
||||
|
||||
module.exports = cp
|
||||
428
api.hyungi.net/node_modules/@npmcli/fs/lib/cp/polyfill.js
generated
vendored
Normal file
428
api.hyungi.net/node_modules/@npmcli/fs/lib/cp/polyfill.js
generated
vendored
Normal file
@@ -0,0 +1,428 @@
|
||||
// this file is a modified version of the code in node 17.2.0
|
||||
// which is, in turn, a modified version of the fs-extra module on npm
|
||||
// node core changes:
|
||||
// - Use of the assert module has been replaced with core's error system.
|
||||
// - All code related to the glob dependency has been removed.
|
||||
// - Bring your own custom fs module is not currently supported.
|
||||
// - Some basic code cleanup.
|
||||
// changes here:
|
||||
// - remove all callback related code
|
||||
// - drop sync support
|
||||
// - change assertions back to non-internal methods (see options.js)
|
||||
// - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows
|
||||
'use strict'
|
||||
|
||||
const {
|
||||
ERR_FS_CP_DIR_TO_NON_DIR,
|
||||
ERR_FS_CP_EEXIST,
|
||||
ERR_FS_CP_EINVAL,
|
||||
ERR_FS_CP_FIFO_PIPE,
|
||||
ERR_FS_CP_NON_DIR_TO_DIR,
|
||||
ERR_FS_CP_SOCKET,
|
||||
ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
|
||||
ERR_FS_CP_UNKNOWN,
|
||||
ERR_FS_EISDIR,
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
} = require('../errors.js')
|
||||
const {
|
||||
constants: {
|
||||
errno: {
|
||||
EEXIST,
|
||||
EISDIR,
|
||||
EINVAL,
|
||||
ENOTDIR,
|
||||
},
|
||||
},
|
||||
} = require('os')
|
||||
const {
|
||||
chmod,
|
||||
copyFile,
|
||||
lstat,
|
||||
mkdir,
|
||||
readdir,
|
||||
readlink,
|
||||
stat,
|
||||
symlink,
|
||||
unlink,
|
||||
utimes,
|
||||
} = require('../fs.js')
|
||||
const {
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
parse,
|
||||
resolve,
|
||||
sep,
|
||||
toNamespacedPath,
|
||||
} = require('path')
|
||||
const { fileURLToPath } = require('url')
|
||||
|
||||
const defaultOptions = {
|
||||
dereference: false,
|
||||
errorOnExist: false,
|
||||
filter: undefined,
|
||||
force: true,
|
||||
preserveTimestamps: false,
|
||||
recursive: false,
|
||||
}
|
||||
|
||||
async function cp (src, dest, opts) {
|
||||
if (opts != null && typeof opts !== 'object') {
|
||||
throw new ERR_INVALID_ARG_TYPE('options', ['Object'], opts)
|
||||
}
|
||||
return cpFn(
|
||||
toNamespacedPath(getValidatedPath(src)),
|
||||
toNamespacedPath(getValidatedPath(dest)),
|
||||
{ ...defaultOptions, ...opts })
|
||||
}
|
||||
|
||||
function getValidatedPath (fileURLOrPath) {
|
||||
const path = fileURLOrPath != null && fileURLOrPath.href
|
||||
&& fileURLOrPath.origin
|
||||
? fileURLToPath(fileURLOrPath)
|
||||
: fileURLOrPath
|
||||
return path
|
||||
}
|
||||
|
||||
async function cpFn (src, dest, opts) {
|
||||
// Warn about using preserveTimestamps on 32-bit node
|
||||
// istanbul ignore next
|
||||
if (opts.preserveTimestamps && process.arch === 'ia32') {
|
||||
const warning = 'Using the preserveTimestamps option in 32-bit ' +
|
||||
'node is not recommended'
|
||||
process.emitWarning(warning, 'TimestampPrecisionWarning')
|
||||
}
|
||||
const stats = await checkPaths(src, dest, opts)
|
||||
const { srcStat, destStat } = stats
|
||||
await checkParentPaths(src, srcStat, dest)
|
||||
if (opts.filter) {
|
||||
return handleFilter(checkParentDir, destStat, src, dest, opts)
|
||||
}
|
||||
return checkParentDir(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
async function checkPaths (src, dest, opts) {
|
||||
const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts)
|
||||
if (destStat) {
|
||||
if (areIdentical(srcStat, destStat)) {
|
||||
throw new ERR_FS_CP_EINVAL({
|
||||
message: 'src and dest cannot be the same',
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
if (srcStat.isDirectory() && !destStat.isDirectory()) {
|
||||
throw new ERR_FS_CP_DIR_TO_NON_DIR({
|
||||
message: `cannot overwrite directory ${src} ` +
|
||||
`with non-directory ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EISDIR,
|
||||
})
|
||||
}
|
||||
if (!srcStat.isDirectory() && destStat.isDirectory()) {
|
||||
throw new ERR_FS_CP_NON_DIR_TO_DIR({
|
||||
message: `cannot overwrite non-directory ${src} ` +
|
||||
`with directory ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: ENOTDIR,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
|
||||
throw new ERR_FS_CP_EINVAL({
|
||||
message: `cannot copy ${src} to a subdirectory of self ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
return { srcStat, destStat }
|
||||
}
|
||||
|
||||
function areIdentical (srcStat, destStat) {
|
||||
return destStat.ino && destStat.dev && destStat.ino === srcStat.ino &&
|
||||
destStat.dev === srcStat.dev
|
||||
}
|
||||
|
||||
function getStats (src, dest, opts) {
|
||||
const statFunc = opts.dereference ?
|
||||
(file) => stat(file, { bigint: true }) :
|
||||
(file) => lstat(file, { bigint: true })
|
||||
return Promise.all([
|
||||
statFunc(src),
|
||||
statFunc(dest).catch((err) => {
|
||||
// istanbul ignore next: unsure how to cover.
|
||||
if (err.code === 'ENOENT') {
|
||||
return null
|
||||
}
|
||||
// istanbul ignore next: unsure how to cover.
|
||||
throw err
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
async function checkParentDir (destStat, src, dest, opts) {
|
||||
const destParent = dirname(dest)
|
||||
const dirExists = await pathExists(destParent)
|
||||
if (dirExists) {
|
||||
return getStatsForCopy(destStat, src, dest, opts)
|
||||
}
|
||||
await mkdir(destParent, { recursive: true })
|
||||
return getStatsForCopy(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
function pathExists (dest) {
|
||||
return stat(dest).then(
|
||||
() => true,
|
||||
// istanbul ignore next: not sure when this would occur
|
||||
(err) => (err.code === 'ENOENT' ? false : Promise.reject(err)))
|
||||
}
|
||||
|
||||
// Recursively check if dest parent is a subdirectory of src.
|
||||
// It works for all file types including symlinks since it
|
||||
// checks the src and dest inodes. It starts from the deepest
|
||||
// parent and stops once it reaches the src parent or the root path.
|
||||
async function checkParentPaths (src, srcStat, dest) {
|
||||
const srcParent = resolve(dirname(src))
|
||||
const destParent = resolve(dirname(dest))
|
||||
if (destParent === srcParent || destParent === parse(destParent).root) {
|
||||
return
|
||||
}
|
||||
let destStat
|
||||
try {
|
||||
destStat = await stat(destParent, { bigint: true })
|
||||
} catch (err) {
|
||||
// istanbul ignore else: not sure when this would occur
|
||||
if (err.code === 'ENOENT') {
|
||||
return
|
||||
}
|
||||
// istanbul ignore next: not sure when this would occur
|
||||
throw err
|
||||
}
|
||||
if (areIdentical(srcStat, destStat)) {
|
||||
throw new ERR_FS_CP_EINVAL({
|
||||
message: `cannot copy ${src} to a subdirectory of self ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
return checkParentPaths(src, srcStat, destParent)
|
||||
}
|
||||
|
||||
const normalizePathToArray = (path) =>
|
||||
resolve(path).split(sep).filter(Boolean)
|
||||
|
||||
// Return true if dest is a subdir of src, otherwise false.
|
||||
// It only checks the path strings.
|
||||
function isSrcSubdir (src, dest) {
|
||||
const srcArr = normalizePathToArray(src)
|
||||
const destArr = normalizePathToArray(dest)
|
||||
return srcArr.every((cur, i) => destArr[i] === cur)
|
||||
}
|
||||
|
||||
async function handleFilter (onInclude, destStat, src, dest, opts, cb) {
|
||||
const include = await opts.filter(src, dest)
|
||||
if (include) {
|
||||
return onInclude(destStat, src, dest, opts, cb)
|
||||
}
|
||||
}
|
||||
|
||||
function startCopy (destStat, src, dest, opts) {
|
||||
if (opts.filter) {
|
||||
return handleFilter(getStatsForCopy, destStat, src, dest, opts)
|
||||
}
|
||||
return getStatsForCopy(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
async function getStatsForCopy (destStat, src, dest, opts) {
|
||||
const statFn = opts.dereference ? stat : lstat
|
||||
const srcStat = await statFn(src)
|
||||
// istanbul ignore else: can't portably test FIFO
|
||||
if (srcStat.isDirectory() && opts.recursive) {
|
||||
return onDir(srcStat, destStat, src, dest, opts)
|
||||
} else if (srcStat.isDirectory()) {
|
||||
throw new ERR_FS_EISDIR({
|
||||
message: `${src} is a directory (not copied)`,
|
||||
path: src,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
} else if (srcStat.isFile() ||
|
||||
srcStat.isCharacterDevice() ||
|
||||
srcStat.isBlockDevice()) {
|
||||
return onFile(srcStat, destStat, src, dest, opts)
|
||||
} else if (srcStat.isSymbolicLink()) {
|
||||
return onLink(destStat, src, dest)
|
||||
} else if (srcStat.isSocket()) {
|
||||
throw new ERR_FS_CP_SOCKET({
|
||||
message: `cannot copy a socket file: ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
} else if (srcStat.isFIFO()) {
|
||||
throw new ERR_FS_CP_FIFO_PIPE({
|
||||
message: `cannot copy a FIFO pipe: ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
// istanbul ignore next: should be unreachable
|
||||
throw new ERR_FS_CP_UNKNOWN({
|
||||
message: `cannot copy an unknown file type: ${dest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
|
||||
function onFile (srcStat, destStat, src, dest, opts) {
|
||||
if (!destStat) {
|
||||
return _copyFile(srcStat, src, dest, opts)
|
||||
}
|
||||
return mayCopyFile(srcStat, src, dest, opts)
|
||||
}
|
||||
|
||||
async function mayCopyFile (srcStat, src, dest, opts) {
|
||||
if (opts.force) {
|
||||
await unlink(dest)
|
||||
return _copyFile(srcStat, src, dest, opts)
|
||||
} else if (opts.errorOnExist) {
|
||||
throw new ERR_FS_CP_EEXIST({
|
||||
message: `${dest} already exists`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EEXIST,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function _copyFile (srcStat, src, dest, opts) {
|
||||
await copyFile(src, dest)
|
||||
if (opts.preserveTimestamps) {
|
||||
return handleTimestampsAndMode(srcStat.mode, src, dest)
|
||||
}
|
||||
return setDestMode(dest, srcStat.mode)
|
||||
}
|
||||
|
||||
async function handleTimestampsAndMode (srcMode, src, dest) {
|
||||
// Make sure the file is writable before setting the timestamp
|
||||
// otherwise open fails with EPERM when invoked with 'r+'
|
||||
// (through utimes call)
|
||||
if (fileIsNotWritable(srcMode)) {
|
||||
await makeFileWritable(dest, srcMode)
|
||||
return setDestTimestampsAndMode(srcMode, src, dest)
|
||||
}
|
||||
return setDestTimestampsAndMode(srcMode, src, dest)
|
||||
}
|
||||
|
||||
function fileIsNotWritable (srcMode) {
|
||||
return (srcMode & 0o200) === 0
|
||||
}
|
||||
|
||||
function makeFileWritable (dest, srcMode) {
|
||||
return setDestMode(dest, srcMode | 0o200)
|
||||
}
|
||||
|
||||
async function setDestTimestampsAndMode (srcMode, src, dest) {
|
||||
await setDestTimestamps(src, dest)
|
||||
return setDestMode(dest, srcMode)
|
||||
}
|
||||
|
||||
function setDestMode (dest, srcMode) {
|
||||
return chmod(dest, srcMode)
|
||||
}
|
||||
|
||||
async function setDestTimestamps (src, dest) {
|
||||
// The initial srcStat.atime cannot be trusted
|
||||
// because it is modified by the read(2) system call
|
||||
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
|
||||
const updatedSrcStat = await stat(src)
|
||||
return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime)
|
||||
}
|
||||
|
||||
function onDir (srcStat, destStat, src, dest, opts) {
|
||||
if (!destStat) {
|
||||
return mkDirAndCopy(srcStat.mode, src, dest, opts)
|
||||
}
|
||||
return copyDir(src, dest, opts)
|
||||
}
|
||||
|
||||
async function mkDirAndCopy (srcMode, src, dest, opts) {
|
||||
await mkdir(dest)
|
||||
await copyDir(src, dest, opts)
|
||||
return setDestMode(dest, srcMode)
|
||||
}
|
||||
|
||||
async function copyDir (src, dest, opts) {
|
||||
const dir = await readdir(src)
|
||||
for (let i = 0; i < dir.length; i++) {
|
||||
const item = dir[i]
|
||||
const srcItem = join(src, item)
|
||||
const destItem = join(dest, item)
|
||||
const { destStat } = await checkPaths(srcItem, destItem, opts)
|
||||
await startCopy(destStat, srcItem, destItem, opts)
|
||||
}
|
||||
}
|
||||
|
||||
async function onLink (destStat, src, dest) {
|
||||
let resolvedSrc = await readlink(src)
|
||||
if (!isAbsolute(resolvedSrc)) {
|
||||
resolvedSrc = resolve(dirname(src), resolvedSrc)
|
||||
}
|
||||
if (!destStat) {
|
||||
return symlink(resolvedSrc, dest)
|
||||
}
|
||||
let resolvedDest
|
||||
try {
|
||||
resolvedDest = await readlink(dest)
|
||||
} catch (err) {
|
||||
// Dest exists and is a regular file or directory,
|
||||
// Windows may throw UNKNOWN error. If dest already exists,
|
||||
// fs throws error anyway, so no need to guard against it here.
|
||||
// istanbul ignore next: can only test on windows
|
||||
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') {
|
||||
return symlink(resolvedSrc, dest)
|
||||
}
|
||||
// istanbul ignore next: should not be possible
|
||||
throw err
|
||||
}
|
||||
if (!isAbsolute(resolvedDest)) {
|
||||
resolvedDest = resolve(dirname(dest), resolvedDest)
|
||||
}
|
||||
if (isSrcSubdir(resolvedSrc, resolvedDest)) {
|
||||
throw new ERR_FS_CP_EINVAL({
|
||||
message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
|
||||
`${resolvedDest}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
// Do not copy if src is a subdir of dest since unlinking
|
||||
// dest in this case would result in removing src contents
|
||||
// and therefore a broken symlink would be created.
|
||||
const srcStat = await stat(src)
|
||||
if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
|
||||
throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
|
||||
message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
|
||||
path: dest,
|
||||
syscall: 'cp',
|
||||
errno: EINVAL,
|
||||
})
|
||||
}
|
||||
return copyLink(resolvedSrc, dest)
|
||||
}
|
||||
|
||||
async function copyLink (resolvedSrc, dest) {
|
||||
await unlink(dest)
|
||||
return symlink(resolvedSrc, dest)
|
||||
}
|
||||
|
||||
module.exports = cp
|
||||
129
api.hyungi.net/node_modules/@npmcli/fs/lib/errors.js
generated
vendored
Normal file
129
api.hyungi.net/node_modules/@npmcli/fs/lib/errors.js
generated
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
'use strict'
|
||||
const { inspect } = require('util')
|
||||
|
||||
// adapted from node's internal/errors
|
||||
// https://github.com/nodejs/node/blob/c8a04049/lib/internal/errors.js
|
||||
|
||||
// close copy of node's internal SystemError class.
|
||||
class SystemError {
|
||||
constructor (code, prefix, context) {
|
||||
// XXX context.code is undefined in all constructors used in cp/polyfill
|
||||
// that may be a bug copied from node, maybe the constructor should use
|
||||
// `code` not `errno`? nodejs/node#41104
|
||||
let message = `${prefix}: ${context.syscall} returned ` +
|
||||
`${context.code} (${context.message})`
|
||||
|
||||
if (context.path !== undefined) {
|
||||
message += ` ${context.path}`
|
||||
}
|
||||
if (context.dest !== undefined) {
|
||||
message += ` => ${context.dest}`
|
||||
}
|
||||
|
||||
this.code = code
|
||||
Object.defineProperties(this, {
|
||||
name: {
|
||||
value: 'SystemError',
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
message: {
|
||||
value: message,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
},
|
||||
info: {
|
||||
value: context,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: false,
|
||||
},
|
||||
errno: {
|
||||
get () {
|
||||
return context.errno
|
||||
},
|
||||
set (value) {
|
||||
context.errno = value
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
syscall: {
|
||||
get () {
|
||||
return context.syscall
|
||||
},
|
||||
set (value) {
|
||||
context.syscall = value
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (context.path !== undefined) {
|
||||
Object.defineProperty(this, 'path', {
|
||||
get () {
|
||||
return context.path
|
||||
},
|
||||
set (value) {
|
||||
context.path = value
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
if (context.dest !== undefined) {
|
||||
Object.defineProperty(this, 'dest', {
|
||||
get () {
|
||||
return context.dest
|
||||
},
|
||||
set (value) {
|
||||
context.dest = value
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${this.name} [${this.code}]: ${this.message}`
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')] (_recurseTimes, ctx) {
|
||||
return inspect(this, {
|
||||
...ctx,
|
||||
getters: true,
|
||||
customInspect: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function E (code, message) {
|
||||
module.exports[code] = class NodeError extends SystemError {
|
||||
constructor (ctx) {
|
||||
super(code, message, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
E('ERR_FS_CP_DIR_TO_NON_DIR', 'Cannot overwrite directory with non-directory')
|
||||
E('ERR_FS_CP_EEXIST', 'Target already exists')
|
||||
E('ERR_FS_CP_EINVAL', 'Invalid src or dest')
|
||||
E('ERR_FS_CP_FIFO_PIPE', 'Cannot copy a FIFO pipe')
|
||||
E('ERR_FS_CP_NON_DIR_TO_DIR', 'Cannot overwrite non-directory with directory')
|
||||
E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file')
|
||||
E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY', 'Cannot overwrite symlink in subdirectory of self')
|
||||
E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type')
|
||||
E('ERR_FS_EISDIR', 'Path is a directory')
|
||||
|
||||
module.exports.ERR_INVALID_ARG_TYPE = class ERR_INVALID_ARG_TYPE extends Error {
|
||||
constructor (name, expected, actual) {
|
||||
super()
|
||||
this.code = 'ERR_INVALID_ARG_TYPE'
|
||||
this.message = `The ${name} argument must be ${expected}. Received ${typeof actual}`
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user