|
|
|
|
@ -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')) {
|
|
|
|
|
|
|
|
|
|
// 클릭된 버튼에서 가장 가까운 <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');
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
//<!--2025.11.12 15:56-->
|