You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
10 KiB

// server.js
// 1. 모듈 불러오기
const express = require('express');
const path = require('path');
const { spawn } = require('child_process');
const multer = require('multer');
const fs = require('fs');
// 2. Express 앱 생성
const app = express();
const port = 3000;
// ========================================================
// [설정] 경로 및 폴더 초기화
// ========================================================
// AI 모델용 업로드 경로
const uploadPath = '/mnt/user_data/applications/misc/networks/cuuva';
fs.mkdirSync(uploadPath, { recursive: true });
// 관심 인물(POI) 폴더 경로 (vip_tracks)
const vipTracksPath = '/mnt/user_data/applications/misc/vip_tracks';
fs.mkdirSync(vipTracksPath, { recursive: true });
// ========================================================
// [설정] Multer (파일 업로드 핸들러)
// ========================================================
// 1) AI 모델용 스토리지
const modelStorage = multer.diskStorage({
destination: (req, file, cb) => { cb(null, uploadPath); },
filename: (req, file, cb) => { cb(null, file.originalname); }
});
const modelFileFilter = (req, file, cb) => {
if (!file.originalname.endsWith('.aiwbin')) {
return cb(new Error('Invalid file type: Only .aiwbin files are allowed'), false);
}
cb(null, true);
};
const uploadModel = multer({ storage: modelStorage, fileFilter: modelFileFilter });
// 2) 관심 인물(POI) 이미지용 스토리지
const poiStorage = multer.diskStorage({
destination: (req, file, cb) => { cb(null, vipTracksPath); },
filename: (req, file, cb) => {
// 임시 저장을 위해 충돌 없는 이름을 생성
const ext = path.extname(file.originalname);
cb(null, `temp_${Date.now()}_${Math.round(Math.random() * 1E9)}${ext}`);
}
});
const poiFileFilter = (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
if (!['.jpg', '.jpeg', '.png'].includes(ext)) {
return cb(new Error('이미지 파일만 업로드 가능합니다 (.jpg, .png)'), false);
}
cb(null, true);
};
const uploadPoi = multer({ storage: poiStorage, fileFilter: poiFileFilter });
// ========================================================
// [미들웨어 및 라우트 설정]
// ========================================================
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
// POI 이미지 서빙
app.get('/poi-images/:folder/:filename', (req, res) => {
const { folder, filename } = req.params;
if (folder.includes('..') || filename.includes('..')) {
return res.status(400).send('Invalid path');
}
const filePath = path.join(vipTracksPath, folder, filename);
if (fs.existsSync(filePath)) {
res.sendFile(filePath);
} else {
res.status(404).send('Image not found');
}
});
// 페이지 라우팅
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/dashboard', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
});
// ========================================================
// [API] AI 모델 관련
// ========================================================
app.post('/set-model', (req, res) => {
const { model } = req.body;
if (!model) return res.status(400).json({ status: 'error', message: '모델 값이 없습니다.' });
const scriptCommand = '/mnt/user_data/feat_control/feat_on.sh';
const args = [model];
const scriptProcess = spawn(scriptCommand, args);
let stdoutData = '';
scriptProcess.stdout.on('data', (data) => { stdoutData += data.toString(); });
scriptProcess.on('close', (code) => {
if (code === 0) {
res.json({ status: 'success', message: `모델이 ${model}(으)로 변경됨`, output: stdoutData });
} else {
res.status(500).json({ status: 'error', message: '쉘 스크립트 실행 실패' });
}
});
});
app.post('/upload-model', uploadModel.single('modelFile'), (req, res) => {
if (!req.file) return res.status(400).json({ status: 'error', message: '파일 없음' });
res.json({ status: 'success', message: '업로드 성공', filePath: req.file.path });
});
function isVersionGreater(v1, v2) {
if (!v2) return true;
const parts1 = v1.replace('v', '').split('.').map(Number);
const parts2 = v2.replace('v', '').split('.').map(Number);
if (parts1[0] > parts2[0]) return true;
if (parts1[0] < parts2[0]) return false;
if (parts1[1] > parts2[1]) return true;
return false;
}
app.get('/list-models', (req, res) => {
const modelRegex = /^CUUVA_([A-Z]+)_v(\d+\.\d+)\.aiwbin$/;
const latestModels = {};
fs.readdir(uploadPath, (err, files) => {
if (err) return res.status(500).json({ status: 'error' });
files.forEach(file => {
const match = file.match(modelRegex);
if (match) {
const role = match[1];
const version = `v${match[2]}`;
const existing = latestModels[role];
if (!existing || isVersionGreater(version, existing.version)) {
latestModels[role] = { file: file, version: version };
}
}
});
res.json(latestModels);
});
});
app.post('/delete-model', (req, res) => {
const { filename } = req.body;
if (!filename) return res.status(400).json({ status: 'error', message: '파일명 누락' });
const safeFilename = path.basename(filename);
if (!safeFilename.endsWith('.aiwbin')) return res.status(400).json({ status: 'error', message: '잘못된 파일' });
const filePath = path.join(uploadPath, safeFilename);
fs.unlink(filePath, (err) => {
if (err && err.code !== 'ENOENT') return res.status(500).json({ status: 'error' });
res.json({ status: 'success', message: '삭제 완료' });
});
});
// ========================================================
// [API] 관심 인물(POI) 관리 관련
// ========================================================
app.get('/poi/list', (req, res) => {
fs.readdir(vipTracksPath, { withFileTypes: true }, (err, entries) => {
if (err) return res.json([]);
const result = entries
.filter(dirent => dirent.isDirectory())
.map(dirent => {
const name = dirent.name;
const dirPath = path.join(vipTracksPath, name);
let imageFiles = [];
try {
const files = fs.readdirSync(dirPath);
imageFiles = files.filter(file => /\.(jpg|jpeg|png)$/i.test(file));
} catch (e) {
imageFiles = [];
}
return { name, images: imageFiles };
});
res.json(result);
});
});
app.post('/poi/register', (req, res) => {
const { name } = req.body;
const nameRegex = /^[a-zA-Z0-9]+$/;
if (!name || !nameRegex.test(name)) {
return res.status(400).json({ status: 'error', message: '이름은 영문과 숫자만 가능합니다.' });
}
const targetDir = path.join(vipTracksPath, name);
if (fs.existsSync(targetDir)) {
return res.status(400).json({ status: 'error', message: '이미 등록된 이름입니다.' });
}
try {
fs.mkdirSync(targetDir);
res.json({ status: 'success' });
} catch (err) {
res.status(500).json({ status: 'error', message: '폴더 생성 실패' });
}
});
app.post('/poi/delete', (req, res) => {
const { name } = req.body;
const safeName = path.basename(name);
const targetDir = path.join(vipTracksPath, safeName);
try {
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true, force: true });
}
res.json({ status: 'success' });
} catch (err) {
res.status(500).json({ status: 'error', message: '삭제 실패' });
}
});
app.post('/poi/delete-image', (req, res) => {
const { name, imageName } = req.body;
if (!name || !imageName) return res.status(400).json({ status: 'error', message: '정보 누락' });
const safeName = path.basename(name);
const safeImageName = path.basename(imageName);
const targetFile = path.join(vipTracksPath, safeName, safeImageName);
if (fs.existsSync(targetFile)) {
try {
fs.unlinkSync(targetFile);
res.json({ status: 'success', message: '이미지가 삭제되었습니다.' });
} catch (err) {
res.status(500).json({ status: 'error', message: '파일 삭제 오류' });
}
} else {
res.status(404).json({ status: 'error', message: '이미지가 없습니다.' });
}
});
// [수정됨] 다중 파일 업로드 처리 (uploadPoi.array 사용)
app.post('/poi/upload-image', uploadPoi.array('poiFile'), (req, res) => {
// 다중 파일인 경우 req.files에 배열로 들어옴
if (!req.files || req.files.length === 0) {
return res.status(400).json({ status: 'error', message: '파일 업로드 실패 (jpg, png만 가능)' });
}
const poiName = req.body.poiName;
// poiName이 없으면 임시 파일 모두 삭제
if (!poiName) {
req.files.forEach(file => {
if(fs.existsSync(file.path)) fs.unlinkSync(file.path);
});
return res.status(400).json({ status: 'error', message: '대상 인물 정보 누락' });
}
const targetDir = path.join(vipTracksPath, poiName);
try {
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir);
}
// 업로드된 모든 파일을 순회하며 이동
req.files.forEach(file => {
const ext = path.extname(file.originalname).toLowerCase();
// 파일명 중복 방지를 위해 난수 추가
const newFileName = `img_${Date.now()}_${Math.floor(Math.random() * 100000)}${ext}`;
const targetPath = path.join(targetDir, newFileName);
fs.renameSync(file.path, targetPath);
});
res.json({ status: 'success', message: '이미지 등록 완료' });
} catch (err) {
console.error(err);
// 에러 발생 시 남아있는 임시 파일 정리
req.files.forEach(file => {
if(fs.existsSync(file.path)) try { fs.unlinkSync(file.path); } catch(e){}
});
res.status(500).json({ status: 'error', message: '파일 이동 실패' });
}
});
// 서버 실행
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});