AI Moel file management, login/ logout.

main
dongjin kim 7 months ago
parent 1083d0bee1
commit 2aa3fa1a0a

@ -33,16 +33,94 @@ document.addEventListener('DOMContentLoaded', () => {
// 콘텐츠 표시 // 콘텐츠 표시
contentModels.classList.add('active'); contentModels.classList.add('active');
contentVideo.classList.remove('active'); contentVideo.classList.remove('active');
// [추가] AI 모델 탭을 클릭할 때 목록 새로고침
loadModelList();
});
}
// ========== AI 모델 목록 로드 함수 ==========
function loadModelList() {
// 테이블 본문(tbody)을 선택
const tableBody = document.querySelector('.model-table tbody');
if (!tableBody) return; // 테이블이 없으면 중단
console.log('Loading model list...');
fetch('/list-models') // server.js에 추가한 엔드포인트
.then(response => response.json())
.then(models => {
// models = { "OBJDET": { "file": "...", "version": "..." }, ... }
// 테이블의 모든 'data-role' 행을 순회
tableBody.querySelectorAll('tr[data-role]').forEach(row => {
const role = row.dataset.role; // e.g., "OBJDET"
const modelData = models[role]; // 해당 역할의 모델 데이터 (없으면 undefined)
const fileCell = row.querySelector('.model-filename');
const versionCell = row.querySelector('.model-version');
if (modelData) {
// 일치하는 파일이 있으면, 파일명과 버전 업데이트
if (fileCell) fileCell.textContent = modelData.file;
if (versionCell) versionCell.textContent = modelData.version;
} else {
// 일치하는 파일이 없으면, 기본값 '-'으로 설정
// (HTML의 v1.0 플레이스홀더를 덮어씀)
if (fileCell) fileCell.textContent = '-';
if (versionCell) versionCell.textContent = '-';
}
});
})
.catch(error => {
console.error('모델 목록 로드 실패:', error);
alert('모델 목록을 불러오는 데 실패했습니다.');
});
}
// ========== AI 모델 목록 로드 함수 끝 ==========
// ========== [추가됨] 모델 삭제 요청 함수 ==========
function deleteModelFile(filename) {
console.log(`Requesting deletion of: ${filename}`);
fetch('/delete-model', { // server.js에 추가할 엔드포인트
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename: filename }) // { "filename": "CUUVA_..." }
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert(`파일이 성공적으로 삭제되었습니다: ${filename}`);
loadModelList(); // 삭제 성공 시 목록 새로고침
} else {
alert(`삭제 실패: ${data.message}`);
}
})
.catch(error => {
console.error('Delete error:', error);
alert('삭제 중 오류가 발생했습니다.');
}); });
} }
// ========== 모델 삭제 요청 함수 끝 ==========
// ========== [수정됨] AI 모델 업로드 로직 시작 ==========
// ========== AI 모델 업로드/관리 로직 시작 ==========
if (contentModels) { if (contentModels) {
// 전역 업로드 관련 요소 가져오기 // 전역 업로드 관련 요소 가져오기
const globalFileInput = document.getElementById('global-file-input'); const globalFileInput = document.getElementById('global-file-input');
const fileNameDisplay = document.getElementById('file-name-display'); const fileNameDisplay = document.getElementById('file-name-display');
const globalUploadButton = document.getElementById('global-upload-button'); const globalUploadButton = document.getElementById('global-upload-button');
// 새로고침 버튼 요소
const refreshButton = contentModels.querySelector('.refresh-button');
// [추가] 모델 테이블 본문 (이벤트 위임용)
const tableBody = contentModels.querySelector('.model-table tbody');
// '찾아보기'로 파일 선택 시 // '찾아보기'로 파일 선택 시
if (globalFileInput && fileNameDisplay) { if (globalFileInput && fileNameDisplay) {
globalFileInput.addEventListener('change', () => { globalFileInput.addEventListener('change', () => {
@ -86,7 +164,6 @@ document.addEventListener('DOMContentLoaded', () => {
// FormData 객체 생성 // FormData 객체 생성
const formData = new FormData(); const formData = new FormData();
formData.append('modelFile', file); // 'modelFile'은 server.js와 일치해야 함 formData.append('modelFile', file); // 'modelFile'은 server.js와 일치해야 함
// [제거됨] modelId, modelRole 전송 로직
// 업로드 중 버튼 비활성화 // 업로드 중 버튼 비활성화
globalUploadButton.disabled = true; globalUploadButton.disabled = true;
@ -103,6 +180,10 @@ document.addEventListener('DOMContentLoaded', () => {
alert(`업로드 성공!\n파일: ${file.name}`); alert(`업로드 성공!\n파일: ${file.name}`);
globalFileInput.value = ''; // 파일 선택 초기화 globalFileInput.value = ''; // 파일 선택 초기화
fileNameDisplay.textContent = '선택된 파일 없음'; fileNameDisplay.textContent = '선택된 파일 없음';
// 업로드 성공 시 모델 목록 새로고침
loadModelList();
} else { } else {
alert(`업로드 실패: ${data.message}`); alert(`업로드 실패: ${data.message}`);
} }
@ -118,12 +199,52 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
}); });
} }
// '새로고침' 버튼 클릭 시
if (refreshButton) {
refreshButton.addEventListener('click', () => {
loadModelList(); // 목록 새로고침 함수 호출
});
}
// ========== [추가됨] 삭제 버튼 이벤트 리스너 (이벤트 위임) ==========
if (tableBody) {
tableBody.addEventListener('click', (event) => {
// 클릭된 요소가 'btn-delete' 클래스를 가지고 있는지 확인
if (event.target.classList.contains('btn-delete')) {
// 클릭된 버튼에서 가장 가까운 <tr>(행)을 찾습니다.
const row = event.target.closest('tr');
if (!row) return;
// 행에서 파일명 셀(.model-filename)을 찾아 파일명을 가져옵니다.
const fileCell = row.querySelector('.model-filename');
const filename = fileCell ? fileCell.textContent : null;
// 파일명이 없거나 '-' 이면 (파일이 할당되지 않음) 중단
if (!filename || filename === '-') {
alert('삭제할 파일이 없습니다.');
return;
}
// 행의 두 번째 셀(<td>)에서 역할 텍스트를 가져옵니다.
const roleText = row.cells[1] ? row.cells[1].textContent : '알 수 없는 역할';
// 사용자에게 삭제 확인을 받습니다.
if (confirm(`정말로 이 모델 파일을 삭제하시겠습니까?\n\n역할: ${roleText}\n파일: ${filename}`)) {
// 확인 시, 삭제 함수 호출
deleteModelFile(filename);
}
}
});
}
// ========== 삭제 버튼 이벤트 리스너 끝 ==========
} }
// ========== AI 모델 업로드 로직 끝 ========== // ========== AI 모델 업로드/관리 로직 끝 ==========
// ========== 추가된 부분 시작 ========== // ========== 로그아웃 버튼 처리 ==========
// 로그아웃 버튼 처리
const logoutButton = document.getElementById('logout-button'); const logoutButton = document.getElementById('logout-button');
if (logoutButton) { // 로그아웃 버튼이 있는지 확인 if (logoutButton) { // 로그아웃 버튼이 있는지 확인
@ -133,7 +254,7 @@ document.addEventListener('DOMContentLoaded', () => {
window.location.href = '/'; window.location.href = '/';
}); });
} }
// ========== 추가된 부분 끝 ========== // ========== 로그아웃 버튼 처리 끝 ==========
// ========== video-test.html 스크립트 추가 시작 ========== // ========== video-test.html 스크립트 추가 시작 ==========
@ -143,36 +264,29 @@ document.addEventListener('DOMContentLoaded', () => {
const frameInfoEl = document.getElementById("frame-info"); const frameInfoEl = document.getElementById("frame-info");
const detListEl = document.getElementById("det-list"); const detListEl = document.getElementById("det-list");
const bboxContainerEl = document.getElementById("bbox-container"); const bboxContainerEl = document.getElementById("bbox-container");
// [수정] 1. 'current-model' select 대신 'current-model-container' div 요소를 가져옵니다.
const modelContainerEl = document.getElementById("current-model-container"); const modelContainerEl = document.getElementById("current-model-container");
let lastFrameMeta = null; let lastFrameMeta = null;
// [수정] 2. 비디오 관련 요소 확인 if문에 modelContainerEl을 추가합니다.
if (imgEl && statusEl && frameInfoEl && detListEl && bboxContainerEl && modelContainerEl) { if (imgEl && statusEl && frameInfoEl && detListEl && bboxContainerEl && modelContainerEl) {
// [수정] 3. 모델 변경 이벤트 리스너 추가 (이벤트 위임 사용) // 모델 변경 이벤트 리스너
modelContainerEl.addEventListener('change', (event) => { modelContainerEl.addEventListener('change', (event) => {
// 이벤트가 'name'이 'current-model'인 라디오 버튼에서 발생했는지 확인
if (event.target && event.target.name === 'current-model') { if (event.target && event.target.name === 'current-model') {
const selectedModel = event.target.value; // 선택된 라디오 버튼의 값 const selectedModel = event.target.value;
console.log(`Model changed to: ${selectedModel}`); console.log(`Model changed to: ${selectedModel}`);
// 서버에 /set-model 엔드포인트로 POST 요청 전송
fetch('/set-model', { fetch('/set-model', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ model: selectedModel }), // { "model": "OBJDET" } body: JSON.stringify({ model: selectedModel }),
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
console.log('Server response:', data); console.log('Server response:', data);
// 서버 응답에 따라 상태 메시지 업데이트
if (data.status === 'success') { if (data.status === 'success') {
// logStatus 함수가 아래에 정의되어 있으므로 사용 가능
logStatus(`모델 변경 완료: ${selectedModel}`); logStatus(`모델 변경 완료: ${selectedModel}`);
} else { } else {
logStatus(`모델 변경 실패: ${data.message}`, true); logStatus(`모델 변경 실패: ${data.message}`, true);
@ -184,12 +298,10 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
}); });
// [수정] 모델 변경 리스너 끝
// [기존] 박스 컨테이너 위치/크기 조절 함수 // 박스 컨테이너 위치/크기 조절 함수
function updateBboxContainerPosition() { function updateBboxContainerPosition() {
if (!imgEl || !bboxContainerEl) return; if (!imgEl || !bboxContainerEl) return;
// ... (기존 코드와 동일)
const top = imgEl.offsetTop; const top = imgEl.offsetTop;
const left = imgEl.offsetLeft; const left = imgEl.offsetLeft;
const width = imgEl.offsetWidth; const width = imgEl.offsetWidth;
@ -200,7 +312,6 @@ document.addEventListener('DOMContentLoaded', () => {
bboxContainerEl.style.height = `${height}px`; bboxContainerEl.style.height = `${height}px`;
} }
function logStatus(msg, isError = false) { function logStatus(msg, isError = false) {
statusEl.textContent = msg; statusEl.textContent = msg;
statusEl.className = "status" + (isError ? " err" : ""); statusEl.className = "status" + (isError ? " err" : "");
@ -211,9 +322,8 @@ document.addEventListener('DOMContentLoaded', () => {
` | FRAME ch=${meta.ch} ts=${meta.ts_us} w=${meta.w} h=${meta.h}`; ` | FRAME ch=${meta.ch} ts=${meta.ts_us} w=${meta.w} h=${meta.h}`;
} }
// [기존] showDetections 함수 // Detections 표시 함수
function showDetections(meta) { function showDetections(meta) {
// ... (기존 코드와 동일) ...
const items = meta.items || []; const items = meta.items || [];
let lines = []; let lines = [];
lines.push(`DET ch=${meta.ch} seq=${meta.seq} ts=${meta.ts_us} cnt=${items.length}`); lines.push(`DET ch=${meta.ch} seq=${meta.seq} ts=${meta.ts_us} cnt=${items.length}`);
@ -276,9 +386,8 @@ document.addEventListener('DOMContentLoaded', () => {
detListEl.textContent = lines.join("\n"); detListEl.textContent = lines.join("\n");
} }
// [기존] connect 함수 // WebSocket 연결 함수
function connect() { function connect() {
// ... (기존 코드와 동일) ...
const ws = new WebSocket(uri); const ws = new WebSocket(uri);
ws.binaryType = "arraybuffer"; ws.binaryType = "arraybuffer";
ws.onopen = () => { logStatus(`연결됨: ${uri}`); }; ws.onopen = () => { logStatus(`연결됨: ${uri}`); };
@ -322,11 +431,14 @@ document.addEventListener('DOMContentLoaded', () => {
// WebSocket 연결 시작 // WebSocket 연결 시작
connect(); connect();
// [기존] 창 크기 변경 시 이벤트 // 창 크기 변경 시 이벤트
window.addEventListener('resize', updateBboxContainerPosition); window.addEventListener('resize', updateBboxContainerPosition);
} // if (요소 확인) 끝 } // if (요소 확인) 끝
// ========== video-test.html 스크립트 추가 끝 ========== // ========== video-test.html 스크립트 추가 끝 ==========
// ========== 페이지 로드 시 모델 목록 즉시 로드 ==========
loadModelList();
}); });
//<!--2025.11.12 15:56-->

@ -71,8 +71,8 @@
<input type="file" id="global-file-input" class="hidden-file-input" accept=".aiwbin"> <input type="file" id="global-file-input" class="hidden-file-input" accept=".aiwbin">
<label for="global-file-input" class="btn-browse">찾아보기</label> <label for="global-file-input" class="btn-browse">찾아보기</label>
<span id="file-name-display">선택된 파일 없음</span> <span id="file-name-display">선택된 파일 없음</span>
<button id="global-upload-button" class="btn-action">업로드</button> <button id="global-upload-button" class="btn-action">⬆️ 업로드</button>
<button class="refresh-button">새로고침</button> <button class="refresh-button">🔄 새로고침</button>
</div> </div>
<table class="model-table"> <table class="model-table">
<thead> <thead>
@ -82,42 +82,47 @@
<th>파일명</th> <th>현재 버전</th> <th>파일명</th> <th>현재 버전</th>
<th>삭제</th> <th>삭제</th>
</tr> </tr>
<tr> <tbody>
<tr data-role="OBJDET">
<td>1</td> <td>1</td>
<td>객체 탐지/ 분류</td> <td>객체 탐지/ 분류</td>
<td>-</td> <td>v1.0</td> <td class="model-filename">-</td>
<td class="model-version">v1.0</td>
<td> <td>
<button class="btn-delete">삭제</button> <button class="btn-delete">삭제</button>
</td> </td>
</tr> </tr>
<tr> <tr data-role="FIRE">
<td>2</td> <td>2</td>
<td>이상행동(쓰러짐, 폭행) 감지</td> <td>화재(불꽃, 연기) 감지</td>
<td>-</td> <td>v1.0</td> <td class="model-filename">-</td>
<td class="model-version">v1.0</td>
<td> <td>
<button class="btn-delete">삭제</button> <button class="btn-delete">삭제</button>
</td> </td>
</tr> </tr>
<tr> <tr data-role="ABNORM"> <td>3</td>
<td>3</td> <td>이상행동(쓰러짐, 폭행) 감지</td>
<td>집합 군중 위험 인식</td> <td class="model-filename">-</td>
<td>-</td> <td>v1.0</td> <td class="model-version">v1.0</td>
<td> <td>
<button class="btn-delete">삭제</button> <button class="btn-delete">삭제</button>
</td> </td>
</tr> </tr>
<tr> <tr data-role="FACE">
<td>4</td> <td>4</td>
<td>화재(불꽃, 연기) 감지</td> <td>얼굴/ 인상착의 인식</td>
<td>-</td> <td>-</td> <td class="model-filename">-</td>
<td class="model-version">-</td>
<td> <td>
<button class="btn-delete">삭제</button> <button class="btn-delete">삭제</button>
</td> </td>
</tr> </tr>
<tr> <tr data-role="LPR">
<td>5</td> <td>5</td>
<td>차량 번호판/ 차종 인식</td> <td>차량 번호판/ 차종 인식</td>
<td>-</td> <td>-</td> <td class="model-filename">-</td>
<td class="model-version">-</td>
<td> <td>
<button class="btn-delete">삭제</button> <button class="btn-delete">삭제</button>
</td> </td>
@ -130,4 +135,3 @@
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>
<!--2025.11.12 15:56-->

@ -8,17 +8,58 @@
</head> </head>
<body class="login-page"> <body class="login-page">
<div class="login-container"> <div class="login-container">
<h1>Welcome to Al Drone System v1.0.875</h1> <h1>Al Drone Console v1.0</h1>
<form action="/dashboard" method="GET"> <div class="input-group">
<form id="login-form">
<div class="input-group">
<label for="id">id</label> <label for="id">id</label>
<input type="text" id="id" name="id"> <input type="text" id="id" name="id" required>
</div> </div>
<div class="input-group"> <div class="input-group">
<label for="password">password</label> <label for="password">password</label>
<input type="password" id="password" name="password"> <input type="password" id="password" name="password" required>
</div> </div>
<button type="submit" class="login-button">Login</button> <button type="submit" class="login-button">Login</button>
</form> </form>
</div> </div>
<script>
// DOM(HTML)이 모두 로드되었을 때 실행
document.addEventListener('DOMContentLoaded', () => {
// ID가 'login-form'인 폼 요소를 가져옵니다.
const loginForm = document.getElementById('login-form');
if (loginForm) {
// 폼에서 'submit' 이벤트(로그인 버튼 클릭)가 발생했을 때 실행될 함수를 정의
loginForm.addEventListener('submit', (event) => {
// 1. 폼의 기본 제출 동작(페이지 새로고침 또는 이동)을 막습니다.
event.preventDefault();
// 2. ID와 비밀번호 입력 필드에서 사용자가 입력한 값을 가져옵니다.
const idInput = document.getElementById('id');
const passwordInput = document.getElementById('password');
const id = idInput.value;
const password = passwordInput.value;
// 3. ID와 비밀번호가 'admin'인지 확인합니다.
if (id === 'admin' && password === 'admin') {
// 4. 일치하면: 'dashboard.html' 페이지로 이동합니다.
console.log('Login successful. Redirecting to dashboard.html...');
window.location.href = 'dashboard.html';
} else {
// 5. 일치하지 않으면: 사용자에게 알림 창을 표시합니다.
alert('ID 또는 비밀번호가 올바르지 않습니다.');
// (선택 사항) 틀린 비밀번호 필드를 비우고 다시 포커스합니다.
passwordInput.value = '';
passwordInput.focus();
}
});
}
});
</script>
</body> </body>
</html> </html>

@ -376,4 +376,4 @@ main {
border: 2px solid red; /* 요청된 빨간색 테두리 */ border: 2px solid red; /* 요청된 빨간색 테두리 */
box-sizing: border-box; /* 테두리 포함 크기 계산 */ box-sizing: border-box; /* 테두리 포함 크기 계산 */
} }
/*<!--2025.11.12 15:56-->*/ /*<!--2025.11.12 16:22-->*/

@ -1,36 +1,31 @@
// server.js
// 1. 모듈 불러오기 // 1. 모듈 불러오기
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const multer = require('multer'); // [추가] 파일 업로드 처리를 위한 multer const multer = require('multer'); // 파일 업로드 처리를 위한 multer
const fs = require('fs'); // [추가] 파일 시스템 접근을 위한 fs const fs = require('fs'); // 파일 시스템 접근을 위한 fs
// 2. Express 앱 생성 // 2. Express 앱 생성
const app = express(); const app = express();
const port = 3000; const port = 3000;
// [추가] 업로드 경로 설정 // 업로드 경로 설정
const uploadPath = '/mnt/user_data/applications/misc/networks/cuuva'; const uploadPath = '/mnt/user_data/applications/misc/networks/cuuva';
// [추가] 업로드 경로가 없으면 생성 // 업로드 경로가 없으면 생성
// (실제 운영 환경에서는 권한 문제가 없는지 확인 필요)
fs.mkdirSync(uploadPath, { recursive: true }); fs.mkdirSync(uploadPath, { recursive: true });
// [추가] Multer 저장소 설정 // Multer 저장소 설정
const storage = multer.diskStorage({ const storage = multer.diskStorage({
// 파일 저장 위치 지정
destination: (req, file, cb) => { destination: (req, file, cb) => {
cb(null, uploadPath); cb(null, uploadPath);
}, },
// 파일 이름 지정 (원본 파일 이름 사용)
filename: (req, file, cb) => { filename: (req, file, cb) => {
cb(null, file.originalname); cb(null, file.originalname);
} }
}); });
// [추가] Multer 파일 필터 설정 (.aiwbin 확장자만 허용) // Multer 파일 필터 설정 (.aiwbin 확장자만 허용)
const fileFilter = (req, file, cb) => { const fileFilter = (req, file, cb) => {
if (!file.originalname.endsWith('.aiwbin')) { if (!file.originalname.endsWith('.aiwbin')) {
// 허용되지 않는 파일 형식 // 허용되지 않는 파일 형식
@ -40,7 +35,7 @@ const fileFilter = (req, file, cb) => {
cb(null, true); cb(null, true);
}; };
// [추가] Multer 업로드 인스턴스 생성 // Multer 업로드 인스턴스 생성
const upload = multer({ const upload = multer({
storage: storage, storage: storage,
fileFilter: fileFilter fileFilter: fileFilter
@ -50,7 +45,7 @@ const upload = multer({
// 3. 'public' 폴더의 파일들을 정적 파일로 제공하도록 설정합니다. // 3. 'public' 폴더의 파일들을 정적 파일로 제공하도록 설정합니다.
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
// [추가] POST 요청의 JSON 본문(body)을 파싱하기 위한 미들웨어 // POST 요청의 JSON 본문(body)을 파싱하기 위한 미들웨어
app.use(express.json()); app.use(express.json());
// 4. 루트 경로('/')로 접속하면 'index.html'(로그인 페이지)를 보냅니다. // 4. 루트 경로('/')로 접속하면 'index.html'(로그인 페이지)를 보냅니다.
@ -63,7 +58,7 @@ app.get('/dashboard', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); res.sendFile(path.join(__dirname, 'public', 'dashboard.html'));
}); });
// [수정] 6. 모델 변경 API 엔드포인트 // 6. 모델 변경 API 엔드포인트
app.post('/set-model', (req, res) => { app.post('/set-model', (req, res) => {
const { model } = req.body; // app.js에서 보낸 { model: "..." } const { model } = req.body; // app.js에서 보낸 { model: "..." }
@ -73,49 +68,38 @@ app.post('/set-model', (req, res) => {
console.log(`[서버] 모델 변경 명령 수신: ${model}`); console.log(`[서버] 모델 변경 명령 수신: ${model}`);
// --- 요청하신 파이썬 스크립트 실행 --- // --- 요청하신 파이썬 스크립트 실행 ---
const pythonCommand = 'python3'; const pythonCommand = 'python3';
const scriptPath = '/mnt/user_data/ctrl_cli.py'; const scriptPath = '/mnt/user_data/ctrl_cli.py';
// [수정] 인수를 " [선택값]" 형태의 단일 문자열로 만듭니다.
const combinedArg = `"ON ${model}"`; const combinedArg = `"ON ${model}"`;
// [수정] 수정된 인수를 배열에 담아 전달합니다.
const args = [scriptPath, combinedArg]; const args = [scriptPath, combinedArg];
console.log(`[서버] 실행: ${pythonCommand} ${args.join(' ')}`); // 실행될 명령어 로그 console.log(`[서버] 실행: ${pythonCommand} ${args.join(' ')}`);
const py = spawn(pythonCommand, args); const py = spawn(pythonCommand, args);
let stdoutData = ''; let stdoutData = '';
let stderrData = ''; let stderrData = '';
// 파이썬 스크립트의 표준 출력
py.stdout.on('data', (data) => { py.stdout.on('data', (data) => {
console.log(`Python stdout: ${data}`); console.log(`Python stdout: ${data}`);
stdoutData += data.toString(); stdoutData += data.toString();
}); });
// 파이썬 스크립트의 표준 에러
py.stderr.on('data', (data) => { py.stderr.on('data', (data) => {
console.error(`Python stderr: ${data}`); console.error(`Python stderr: ${data}`);
stderrData += data.toString(); stderrData += data.toString();
}); });
// 파이썬 프로세스 종료
py.on('close', (code) => { py.on('close', (code) => {
console.log(`Python process exited with code ${code}`); console.log(`Python process exited with code ${code}`);
if (code === 0) { if (code === 0) {
// 성공 시 클라이언트에 응답
res.json({ status: 'success', message: `모델이 ${model}(으)로 변경됨`, output: stdoutData }); res.json({ status: 'success', message: `모델이 ${model}(으)로 변경됨`, output: stdoutData });
} else { } else {
// 실패 시 클라이언트에 에러 응답
res.status(500).json({ status: 'error', message: '파이썬 스크립트 실행 실패', error: stderrData }); res.status(500).json({ status: 'error', message: '파이썬 스크립트 실행 실패', error: stderrData });
} }
}); });
// 스폰 자체의 에러 (예: python3 명령어를 찾을 수 없음)
py.on('error', (err) => { py.on('error', (err) => {
console.error('[서버] 파이썬 프로세스 시작 실패:', err); console.error('[서버] 파이썬 프로세스 시작 실패:', err);
res.status(500).json({ status: 'error', message: '프로세스 시작 실패', error: err.message }); res.status(500).json({ status: 'error', message: '프로세스 시작 실패', error: err.message });
@ -123,39 +107,28 @@ app.post('/set-model', (req, res) => {
// --- 스크립트 실행 끝 --- // --- 스크립트 실행 끝 ---
}); });
// [수정됨] 7. 모델 파일 업로드 엔드포인트 // 7. 모델 파일 업로드 엔드포인트
// 'modelFile'은 app.js의 FormData.append('modelFile', ...)와 일치해야 합니다.
app.post('/upload-model', (req, res) => { app.post('/upload-model', (req, res) => {
// upload.single() 미들웨어를 수동으로 호출하여 오류를 상세히 처리
upload.single('modelFile')(req, res, function (err) { upload.single('modelFile')(req, res, function (err) {
// Multer 관련 오류 처리 (예: 파일 크기 제한, 저장소 오류 등)
if (err instanceof multer.MulterError) { if (err instanceof multer.MulterError) {
console.error('[Multer Error]:', err.message); console.error('[Multer Error]:', err.message);
return res.status(500).json({ status: 'error', message: `업로드 오류: ${err.message}` }); return res.status(500).json({ status: 'error', message: `업로드 오류: ${err.message}` });
} }
// 파일 필터에서 발생한 오류 처리 (예: .aiwbin이 아닌 경우)
else if (err) { else if (err) {
console.error('[File Filter Error]:', err.message); console.error('[File Filter Error]:', err.message);
return res.status(400).json({ status: 'error', message: err.message }); return res.status(400).json({ status: 'error', message: err.message });
} }
// 파일이 제대로 업로드되었는지 확인
if (!req.file) { if (!req.file) {
return res.status(400).json({ status: 'error', message: '업로드할 파일을 찾을 수 없습니다.' }); return res.status(400).json({ status: 'error', message: '업로드할 파일을 찾을 수 없습니다.' });
} }
// [제거됨] 폼 데이터로 함께 전송된 모델 정보 (modelId, modelRole)
// const modelId = req.body.modelId;
// const modelRole = req.body.modelRole;
console.log(`[서버] 파일 업로드 성공:`); console.log(`[서버] 파일 업로드 성공:`);
// [제거됨] console.log(` - 모델 ID: ${modelId} (${modelRole})`);
console.log(` - 원본 파일명: ${req.file.originalname}`); console.log(` - 원본 파일명: ${req.file.originalname}`);
console.log(` - 저장 경로: ${req.file.path}`); console.log(` - 저장 경로: ${req.file.path}`);
// 클라이언트에 성공 응답 전송
res.json({ res.json({
status: 'success', status: 'success',
message: `파일 '${req.file.originalname}'이(가) 성공적으로 업로드되었습니다.`, message: `파일 '${req.file.originalname}'이(가) 성공적으로 업로드되었습니다.`,
@ -164,9 +137,111 @@ app.post('/upload-model', (req, res) => {
}); });
}); });
// ========== 모델 목록 조회 로직 시작 ==========
/**
* 버전 문자열을 비교합니다. (: "v1.1" > "v1.0")
*/
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;
}
// 7-1. 모델 목록 조회 엔드포인트
app.get('/list-models', (req, res) => {
const modelRegex = /^CUUVA_([A-Z]+)_v(\d+\.\d+)\.aiwbin$/;
const latestModels = {};
fs.readdir(uploadPath, (err, files) => {
if (err) {
console.error('[서버] 모델 디렉토리 읽기 실패:', err);
return res.status(500).json({ status: 'error', message: '서버에서 파일 목록을 읽을 수 없습니다.' });
}
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
};
}
}
});
console.log('[서버] 최신 모델 목록 조회:', latestModels);
res.json(latestModels);
});
});
// ========== 모델 목록 조회 로직 끝 ==========
// ========== [추가됨] 모델 파일 삭제 엔드포인트 ==========
app.post('/delete-model', (req, res) => {
const { filename } = req.body; // { "filename": "CUUVA_..." }
if (!filename) {
return res.status(400).json({ status: 'error', message: '파일 이름이 제공되지 않았습니다.' });
}
// [보안] Directory Traversal 공격 방지
// path.basename은 파일명에서 상위 디렉토리(.., /) 등을 제거합니다.
const safeFilename = path.basename(filename);
// 만약 ".." 같은 것이 포함되어 safeFilename이 원래 filename과 다르다면, 비정상 요청
if (safeFilename !== filename) {
return res.status(400).json({ status: 'error', message: '잘못된 파일 이름 형식입니다.' });
}
// [보안] .aiwbin 파일만 삭제하도록 다시 한번 확인
if (!safeFilename.endsWith('.aiwbin')) {
return res.status(400).json({ status: 'error', message: '.aiwbin 파일만 삭제할 수 있습니다.' });
}
// 최종 삭제할 파일 경로
const filePath = path.join(uploadPath, safeFilename);
console.log(`[서버] 파일 삭제 시도: ${filePath}`);
// fs.unlink로 파일 삭제
fs.unlink(filePath, (err) => {
if (err) {
// 파일이 존재하지 않는 경우 (이미 삭제되었거나, 잘못된 요청)
if (err.code === 'ENOENT') {
console.error(`[서버] 삭제 실패: 파일 없음 ${filePath}`, err);
return res.status(404).json({ status: 'error', message: '삭제할 파일을 서버에서 찾을 수 없습니다.' });
}
// 기타 오류 (예: 권한 문제)
console.error(`[서버] 파일 삭제 오류 ${filePath}:`, err);
return res.status(500).json({ status: 'error', message: '파일 삭제 중 서버 오류가 발생했습니다.' });
}
// 삭제 성공
console.log(`[서버] 파일 삭제 성공: ${filePath}`);
res.json({ status: 'success', message: `파일 '${safeFilename}'(이)가 성공적으로 삭제되었습니다.` });
});
});
// ========== 모델 파일 삭제 엔드포인트 끝 ==========
// 8. (번호 수정) 설정한 3000번 포트에서 서버가 요청을 기다리도록 실행합니다. // 8. 설정한 3000번 포트에서 서버가 요청을 기다리도록 실행합니다.
app.listen(port, () => { app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`); console.log(`Server running on http://localhost:${port}`);
}); });
<!--2025.11.12 15:56--> //```
Loading…
Cancel
Save