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.

332 lines
14 KiB

// app.js
// DOM(HTML)이 모두 로드되었을 때 실행
document.addEventListener('DOMContentLoaded', () => {
// 탭 버튼과 콘텐츠 요소를 가져옵니다.
const tabVideo = document.getElementById('tab-video');
const tabModels = document.getElementById('tab-models');
const contentVideo = document.getElementById('content-video');
const contentModels = document.getElementById('content-models');
// 비디오 탭 클릭 시
if (tabVideo && tabModels) { // 탭 요소가 있는지 확인
tabVideo.addEventListener('click', () => {
// 버튼 활성화
tabVideo.classList.add('active');
tabModels.classList.remove('active');
// 콘텐츠 표시
contentVideo.classList.add('active');
contentModels.classList.remove('active');
});
}
// AI 모델 탭 클릭 시
if (tabVideo && tabModels) { // 탭 요소가 있는지 확인
tabModels.addEventListener('click', () => {
// 버튼 활성화
tabModels.classList.add('active');
tabVideo.classList.remove('active');
// 콘텐츠 표시
contentModels.classList.add('active');
contentVideo.classList.remove('active');
});
}
// ========== [수정됨] AI 모델 업로드 로직 시작 ==========
if (contentModels) {
// 전역 업로드 관련 요소 가져오기
const globalFileInput = document.getElementById('global-file-input');
const fileNameDisplay = document.getElementById('file-name-display');
const globalUploadButton = document.getElementById('global-upload-button');
// '찾아보기'로 파일 선택 시
if (globalFileInput && fileNameDisplay) {
globalFileInput.addEventListener('change', () => {
if (globalFileInput.files.length > 0) {
const file = globalFileInput.files[0];
// 파일 확장자 확인
if (!file.name.endsWith('.aiwbin')) {
alert('잘못된 파일 형식입니다. .aiwbin 파일만 선택할 수 있습니다.');
globalFileInput.value = ''; // 파일 선택 초기화
fileNameDisplay.textContent = '선택된 파일 없음';
} else {
// 파일명 표시
fileNameDisplay.textContent = file.name;
}
} else {
fileNameDisplay.textContent = '선택된 파일 없음';
}
});
}
// '업로드' 버튼 클릭 시
if (globalUploadButton && globalFileInput) {
globalUploadButton.addEventListener('click', () => {
// 파일이 선택되었는지 확인
if (globalFileInput.files.length === 0) {
alert('먼저 .aiwbin 파일을 선택해주세요.');
return;
}
const file = globalFileInput.files[0];
// (이중 확인) 파일 확장자 확인
if (!file.name.endsWith('.aiwbin')) {
alert('잘못된 파일 형식입니다. .aiwbin 파일만 업로드할 수 있습니다.');
globalFileInput.value = '';
fileNameDisplay.textContent = '선택된 파일 없음';
return;
}
// FormData 객체 생성
const formData = new FormData();
formData.append('modelFile', file); // 'modelFile'은 server.js와 일치해야 함
// [제거됨] modelId, modelRole 전송 로직
// 업로드 중 버튼 비활성화
globalUploadButton.disabled = true;
globalUploadButton.textContent = '업로드 중...';
// Fetch API를 사용하여 파일 업로드
fetch('/upload-model', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert(`업로드 성공!\n파일: ${file.name}`);
globalFileInput.value = ''; // 파일 선택 초기화
fileNameDisplay.textContent = '선택된 파일 없음';
} else {
alert(`업로드 실패: ${data.message}`);
}
})
.catch(error => {
console.error('Upload error:', error);
alert('업로드 중 오류가 발생했습니다.');
})
.finally(() => {
// 버튼 다시 활성화
globalUploadButton.disabled = false;
globalUploadButton.textContent = '업로드';
});
});
}
}
// ========== AI 모델 업로드 로직 끝 ==========
// ========== 추가된 부분 시작 ==========
// 로그아웃 버튼 처리
const logoutButton = document.getElementById('logout-button');
if (logoutButton) { // 로그아웃 버튼이 있는지 확인
logoutButton.addEventListener('click', () => {
console.log('Logout clicked');
// 로그인 페이지(루트 '/')로 이동
window.location.href = '/';
});
}
// ========== 추가된 부분 끝 ==========
// ========== video-test.html 스크립트 추가 시작 ==========
const uri = "ws://10.10.11.246:8765"; // 필요하면 여기만 수정
const imgEl = document.getElementById("frame");
const statusEl = document.getElementById("status");
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; // 선택된 라디오 버튼의 값
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" }
})
.then(response => response.json())
.then(data => {
console.log('Server response:', data);
// 서버 응답에 따라 상태 메시지 업데이트
if (data.status === 'success') {
// logStatus 함수가 아래에 정의되어 있으므로 사용 가능
logStatus(`모델 변경 완료: ${selectedModel}`);
} else {
logStatus(`모델 변경 실패: ${data.message}`, true);
}
})
.catch(error => {
console.error('Error setting model:', error);
logStatus('모델 변경 중 오류 발생', true);
});
}
});
// [수정] 모델 변경 리스너 끝
// [기존] 박스 컨테이너 위치/크기 조절 함수
function updateBboxContainerPosition() {
if (!imgEl || !bboxContainerEl) return;
// ... (기존 코드와 동일)
const top = imgEl.offsetTop;
const left = imgEl.offsetLeft;
const width = imgEl.offsetWidth;
const height = imgEl.offsetHeight;
bboxContainerEl.style.top = `${top}px`;
bboxContainerEl.style.left = `${left}px`;
bboxContainerEl.style.width = `${width}px`;
bboxContainerEl.style.height = `${height}px`;
}
function logStatus(msg, isError = false) {
statusEl.textContent = msg;
statusEl.className = "status" + (isError ? " err" : "");
}
function showFrameMeta(meta) {
frameInfoEl.textContent =
` | FRAME ch=${meta.ch} ts=${meta.ts_us} w=${meta.w} h=${meta.h}`;
}
// [기존] showDetections 함수
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}`);
bboxContainerEl.innerHTML = "";
if (!imgEl || !lastFrameMeta) {
items.forEach((it, i) => {
lines.push(
`#${i} cls=${it.cls} prob=${it.prob.toFixed(3)}`
+ ` x=${it.x.toFixed(3)} y=${it.y.toFixed(3)}`
+ ` w=${it.w.toFixed(3)} h=${it.h.toFixed(3)}`
+ ` resv=${it.resv}`
);
});
detListEl.textContent = lines.join("\n");
return;
}
updateBboxContainerPosition();
const imgWidth = imgEl.clientWidth;
const imgHeight = imgEl.clientHeight;
const frameWidth = lastFrameMeta.w;
const frameHeight = lastFrameMeta.h;
if (frameWidth === 0 || frameHeight === 0 || imgWidth === 0 || imgHeight === 0) {
items.forEach((it, i) => {
lines.push(
`#${i} cls=${it.cls} prob=${it.prob.toFixed(3)}`
+ ` x=${it.x.toFixed(3)} y=${it.y.toFixed(3)}`
+ ` w=${it.w.toFixed(3)} h=${it.h.toFixed(3)}`
+ ` resv=${it.resv}`
);
});
detListEl.textContent = lines.join("\n");
return;
}
const widthRatio = imgWidth / frameWidth;
const heightRatio = imgHeight / frameHeight;
const ratio = Math.min(widthRatio, heightRatio);
const videoDisplayWidth = frameWidth * ratio;
const videoDisplayHeight = frameHeight * ratio;
const offsetX = (imgWidth - videoDisplayWidth) / 2;
const offsetY = (imgHeight - videoDisplayHeight) / 2;
items.forEach((it, i) => {
lines.push(
`#${i} cls=${it.cls} prob=${it.prob.toFixed(3)}`
+ ` x=${it.x.toFixed(3)} y=${it.y.toFixed(3)}`
+ ` w=${it.w.toFixed(3)} h=${it.h.toFixed(3)}`
+ ` resv=${it.resv}`
);
const boxLeft = (it.x * ratio) + offsetX;
const boxTop = (it.y * ratio) + offsetY;
const boxWidth = it.w * ratio;
const boxHeight = it.h * ratio;
const bbox = document.createElement('div');
bbox.className = 'bbox';
bbox.style.left = `${boxLeft}px`;
bbox.style.top = `${boxTop}px`;
bbox.style.width = `${boxWidth}px`;
bbox.style.height = `${boxHeight}px`;
bboxContainerEl.appendChild(bbox);
});
detListEl.textContent = lines.join("\n");
}
// [기존] connect 함수
function connect() {
// ... (기존 코드와 동일) ...
const ws = new WebSocket(uri);
ws.binaryType = "arraybuffer";
ws.onopen = () => { logStatus(`연결됨: ${uri}`); };
ws.onclose = (ev) => {
logStatus(`연결 종료 (code=${ev.code}) 재접속 대기중...`, true);
setTimeout(connect, 2000);
};
ws.onerror = (err) => {
logStatus("WebSocket 오류 발생", true);
console.error(err);
};
ws.onmessage = (event) => {
const data = event.data;
if (typeof data === "string") {
try {
const meta = JSON.parse(data);
const t = meta.type;
if (t === "frame") {
lastFrameMeta = meta;
showFrameMeta(meta);
} else if (t === "det") {
showDetections(meta);
}
} catch (e) {
console.error("JSON parse error:", e, data);
}
return;
}
if (data instanceof ArrayBuffer && lastFrameMeta) {
const blob = new Blob([data], { type: "image/jpeg" });
const url = URL.createObjectURL(blob);
imgEl.onload = () => {
URL.revokeObjectURL(url);
updateBboxContainerPosition();
};
imgEl.src = url;
}
};
}
// WebSocket 연결 시작
connect();
// [기존] 창 크기 변경 시 이벤트
window.addEventListener('resize', updateBboxContainerPosition);
} // if (요소 확인) 끝
// ========== video-test.html 스크립트 추가 끝 ==========
});
//<!--2025.11.12 15:56-->