feat: 초기 프로젝트 설정 및 룰.md 파일 추가

This commit is contained in:
2025-07-28 09:53:31 +09:00
commit 09a4d38512
8165 changed files with 1021855 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
node_modules
npm-debug.log
Dockerfile
.dockerignore

59
api.hyungi.net/.env Normal file
View 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
View 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"]

View 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: '서버 오류' });
}
};

View 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) });
}
};

View 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) });
}
};

View 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 // 에러 유형 목록
};

View 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 // 에러 유형 목록
};

View 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) });
}
};

View 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) });
}
};

View 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) });
}
};

View 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
});
}
};

View 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: '스케줄 목록 조회 실패' });
}
};

View 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) });
}
};

View 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) });
}
};

View 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) });
}
};

View 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) });
}
};

View 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) });
}
};

View 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();

View 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) });
}
};

View 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
View 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
View 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 };

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

484
api.hyungi.net/index.js Normal file
View 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;

View 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: '존재하지 않는 경로입니다.' });
});

View 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();
};
};

View 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 };

View 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' });
}
};

View 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 });
}
};

View 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: '서버 오류가 발생했습니다.'
});
}
};

View 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;

View 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;

View 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 };

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View File

@@ -0,0 +1,12 @@
// models/pingModel.js
/**
* 단순 ping 비즈니스 로직 레이어
* 필요하다면 여기서 더 복잡한 처리나 다른 서비스 호출 등을 관리
*/
exports.ping = () => {
return {
message: 'pong',
timestamp: new Date().toISOString()
};
};

View 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 };

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
};

View 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
View 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
View 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
View 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')
}

View 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
View 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
View 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.
[![Node.js CI](https://github.com/Hexagon/base64/actions/workflows/node.js.yml/badge.svg)](https://github.com/Hexagon/base64/actions/workflows/node.js.yml) [![Deno CI](https://github.com/Hexagon/base64/actions/workflows/deno.yml/badge.svg)](https://github.com/Hexagon/base64/actions/workflows/deno.yml)
[![npm version](https://badge.fury.io/js/@hexagon%2Fbase64.svg)](https://badge.fury.io/js/@hexagon%2Fbase64) [![NPM Downloads](https://img.shields.io/npm/dm/@hexagon/base64.svg)](https://www.npmjs.org/package/@hexagon/base64) [![jsdelivr](https://data.jsdelivr.com/v1/package/npm/@hexagon/base64/badge?style=rounded)](https://www.jsdelivr.com/package/npm/@hexagon/base64) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/4978bdbf495941c087ecb32b120f28ff)](https://www.codacy.com/gh/Hexagon/base64/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Hexagon/base64&amp;utm_campaign=Badge_Grade)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](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

View 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.

View 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;
}));

View 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});

View 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"}

View 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};

View 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"}

View 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"
}

View 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 };

View File

@@ -0,0 +1,3 @@
import base64 from "./base64.js";
export default base64;

View 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 };
}

View File

@@ -0,0 +1,2 @@
export default base64;
import base64 from "./base64.js";

View 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.

View File

@@ -0,0 +1,81 @@
# Tiny CBOR
[![](https://img.shields.io/github/actions/workflow/status/levischuck/tiny-cbor/build.yaml?branch=main&style=flat-square)](https://github.com/LeviSchuck/tiny-cbor/actions)
[![](https://img.shields.io/codecov/c/gh/levischuck/tiny-cbor?style=flat-square)](https://codecov.io/gh/levischuck/tiny-cbor)
[![](https://img.shields.io/github/v/tag/levischuck/tiny-cbor?label=npm&logo=npm&style=flat-square)](https://www.npmjs.com/package/@levischuck/tiny-cbor)
[![](https://img.shields.io/jsr/v/%40levischuck/tiny-cbor?style=flat-square&logo=jsr&label=JSR)](https://jsr.io/@levischuck/tiny-cbor)
[![](https://img.shields.io/github/license/levischuck/tiny-cbor?style=flat-square)](https://github.com/LeviSchuck/tiny-cbor/blob/main/LICENSE.txt)
![](https://img.shields.io/bundlephobia/min/%40levischuck/tiny-cbor?style=flat-square)
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.

View 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;

View 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;
}

View 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[];

View 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,
];
}
}

View File

@@ -0,0 +1,2 @@
export { CBORTag, decodeCBOR, decodePartialCBOR, encodeCBOR, } from "./cbor/cbor.js";
export type { CBORType } from "./cbor/cbor.js";

View File

@@ -0,0 +1 @@
export { CBORTag, decodeCBOR, decodePartialCBOR, encodeCBOR, } from "./cbor/cbor.js";

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View 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"
}

View 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;

View 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;

View 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[];

View 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;

View File

@@ -0,0 +1,2 @@
export { CBORTag, decodeCBOR, decodePartialCBOR, encodeCBOR, } from "./cbor/cbor.js";
export type { CBORType } from "./cbor/cbor.js";

View 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; } });

View File

@@ -0,0 +1,3 @@
{
"type": "commonjs"
}

20
api.hyungi.net/node_modules/@npmcli/fs/LICENSE.md generated vendored Normal file
View 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
View 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()
```

View 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

View 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

View 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

View File

@@ -0,0 +1,9 @@
const semver = require('semver')
const satisfies = (range) => {
return semver.satisfies(process.version, range, { includePrerelease: true })
}
module.exports = {
satisfies,
}

View 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,
}

View 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
View 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
View 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

View 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
View 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