diff --git a/public/app.js b/public/app.js
index a01a49f..807de34 100644
--- a/public/app.js
+++ b/public/app.js
@@ -33,16 +33,94 @@ document.addEventListener('DOMContentLoaded', () => {
// 콘텐츠 표시
contentModels.classList.add('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) {
// 전역 업로드 관련 요소 가져오기
const globalFileInput = document.getElementById('global-file-input');
const fileNameDisplay = document.getElementById('file-name-display');
const globalUploadButton = document.getElementById('global-upload-button');
+ // 새로고침 버튼 요소
+ const refreshButton = contentModels.querySelector('.refresh-button');
+
+ // [추가] 모델 테이블 본문 (이벤트 위임용)
+ const tableBody = contentModels.querySelector('.model-table tbody');
+
// '찾아보기'로 파일 선택 시
if (globalFileInput && fileNameDisplay) {
globalFileInput.addEventListener('change', () => {
@@ -86,7 +164,6 @@ document.addEventListener('DOMContentLoaded', () => {
// FormData 객체 생성
const formData = new FormData();
formData.append('modelFile', file); // 'modelFile'은 server.js와 일치해야 함
- // [제거됨] modelId, modelRole 전송 로직
// 업로드 중 버튼 비활성화
globalUploadButton.disabled = true;
@@ -103,6 +180,10 @@ document.addEventListener('DOMContentLoaded', () => {
alert(`업로드 성공!\n파일: ${file.name}`);
globalFileInput.value = ''; // 파일 선택 초기화
fileNameDisplay.textContent = '선택된 파일 없음';
+
+ // 업로드 성공 시 모델 목록 새로고침
+ loadModelList();
+
} else {
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')) {
+
+ // 클릭된 버튼에서 가장 가까운
(행)을 찾습니다.
+ 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;
+ }
+
+ // 행의 두 번째 셀()에서 역할 텍스트를 가져옵니다.
+ 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');
if (logoutButton) { // 로그아웃 버튼이 있는지 확인
@@ -133,7 +254,7 @@ document.addEventListener('DOMContentLoaded', () => {
window.location.href = '/';
});
}
- // ========== 추가된 부분 끝 ==========
+ // ========== 로그아웃 버튼 처리 끝 ==========
// ========== video-test.html 스크립트 추가 시작 ==========
@@ -143,36 +264,29 @@ document.addEventListener('DOMContentLoaded', () => {
const frameInfoEl = document.getElementById("frame-info");
const detListEl = document.getElementById("det-list");
const bboxContainerEl = document.getElementById("bbox-container");
-
- // [수정] 1. 'current-model' select 대신 'current-model-container' div 요소를 가져옵니다.
const modelContainerEl = document.getElementById("current-model-container");
let lastFrameMeta = null;
- // [수정] 2. 비디오 관련 요소 확인 if문에 modelContainerEl을 추가합니다.
if (imgEl && statusEl && frameInfoEl && detListEl && bboxContainerEl && modelContainerEl) {
- // [수정] 3. 모델 변경 이벤트 리스너 추가 (이벤트 위임 사용)
+ // 모델 변경 이벤트 리스너
modelContainerEl.addEventListener('change', (event) => {
- // 이벤트가 '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}`);
- // 서버에 /set-model 엔드포인트로 POST 요청 전송
fetch('/set-model', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
- body: JSON.stringify({ model: selectedModel }), // { "model": "OBJDET" }
+ body: JSON.stringify({ model: selectedModel }),
})
.then(response => response.json())
.then(data => {
console.log('Server response:', data);
- // 서버 응답에 따라 상태 메시지 업데이트
if (data.status === 'success') {
- // logStatus 함수가 아래에 정의되어 있으므로 사용 가능
logStatus(`모델 변경 완료: ${selectedModel}`);
} else {
logStatus(`모델 변경 실패: ${data.message}`, true);
@@ -184,12 +298,10 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
});
- // [수정] 모델 변경 리스너 끝
- // [기존] 박스 컨테이너 위치/크기 조절 함수
+ // 박스 컨테이너 위치/크기 조절 함수
function updateBboxContainerPosition() {
if (!imgEl || !bboxContainerEl) return;
- // ... (기존 코드와 동일)
const top = imgEl.offsetTop;
const left = imgEl.offsetLeft;
const width = imgEl.offsetWidth;
@@ -200,7 +312,6 @@ document.addEventListener('DOMContentLoaded', () => {
bboxContainerEl.style.height = `${height}px`;
}
-
function logStatus(msg, isError = false) {
statusEl.textContent = msg;
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}`;
}
- // [기존] showDetections 함수
+ // Detections 표시 함수
function showDetections(meta) {
- // ... (기존 코드와 동일) ...
const items = meta.items || [];
let lines = [];
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");
}
- // [기존] connect 함수
+ // WebSocket 연결 함수
function connect() {
- // ... (기존 코드와 동일) ...
const ws = new WebSocket(uri);
ws.binaryType = "arraybuffer";
ws.onopen = () => { logStatus(`연결됨: ${uri}`); };
@@ -322,11 +431,14 @@ document.addEventListener('DOMContentLoaded', () => {
// WebSocket 연결 시작
connect();
- // [기존] 창 크기 변경 시 이벤트
+ // 창 크기 변경 시 이벤트
window.addEventListener('resize', updateBboxContainerPosition);
} // if (요소 확인) 끝
// ========== video-test.html 스크립트 추가 끝 ==========
+
+ // ========== 페이지 로드 시 모델 목록 즉시 로드 ==========
+ loadModelList();
+
});
-//
\ No newline at end of file
diff --git a/public/dashboard.html b/public/dashboard.html
index d3aa821..e9a139a 100644
--- a/public/dashboard.html
+++ b/public/dashboard.html
@@ -71,8 +71,8 @@
선택된 파일 없음
-
-
+
+
@@ -82,42 +82,47 @@
| 파일명 | 현재 버전 |
삭제 |
-
+
+
| 1 |
객체 탐지/ 분류 |
- - | v1.0 |
+ - |
+ v1.0 |
|
-
+
| 2 |
- 이상행동(쓰러짐, 폭행) 감지 |
- - | v1.0 |
+ 화재(불꽃, 연기) 감지 |
+ - |
+ v1.0 |
|
-
- | 3 |
- 집합 군중 위험 인식 |
- - | v1.0 |
+ | 3 |
+ 이상행동(쓰러짐, 폭행) 감지 |
+ - |
+ v1.0 |
|
-
+
| 4 |
- 화재(불꽃, 연기) 감지 |
- - | - |
+ 얼굴/ 인상착의 인식 |
+ - |
+ - |
|
-
+
| 5 |
차량 번호판/ 차종 인식 |
- - | - |
+ - |
+ - |
|
@@ -129,5 +134,4 @@
|