탭전환추가, 이미지 업로드 추가
This commit is contained in:
@@ -1,90 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: settings.py
|
||||
@Date: 2025-08-01
|
||||
@Author: SGM
|
||||
@Brief: 애플리케이션 전반의 상수 및 설정 관리
|
||||
@section MODIFYINFO 수정정보
|
||||
"""
|
||||
|
||||
# =============================================================================
|
||||
# 웹 서버 설정
|
||||
# =============================================================================
|
||||
|
||||
# WEB SERVER PORT
|
||||
# 다른 PC에서 접속할 때 포트 충돌이 있으면 변경하세요
|
||||
# 예: 51003, 51002, 8000 등
|
||||
SERVICE_PORT = 51001
|
||||
|
||||
# Uvicorn 서버 호스트 설정
|
||||
# "0.0.0.0": 모든 IP에서 접속 허용 (기본값, 권장)
|
||||
# "127.0.0.1": 로컬에서만 접속 허용
|
||||
# "192.168.x.x": 특정 IP에서만 접속 허용
|
||||
HOST = "0.0.0.0"
|
||||
|
||||
# =============================================================================
|
||||
# 외부 API 설정
|
||||
# =============================================================================
|
||||
|
||||
# AI 이미지 생성 API 서버 주소
|
||||
# 다른 PC나 서버에서 API 서버를 실행하는 경우 IP 주소를 변경하세요
|
||||
# 예: "http://192.168.1.100:51000/api/..."
|
||||
# "http://localhost:51000/api/..."
|
||||
#EXTERNAL_API_URL = "http://192.168.200.233:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data"
|
||||
EXTERNAL_API_URL = "http://210.222.143.78:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data"
|
||||
|
||||
# =============================================================================
|
||||
# 사전 정의된 API 엔드포인트 설정
|
||||
# =============================================================================
|
||||
|
||||
# 사용 가능한 API 엔드포인트 목록 (웹 UI에서 선택 가능)
|
||||
PREDEFINED_ENDPOINTS = [
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev2/Data)",
|
||||
"url": "http://192.168.200.232:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev3/Data)",
|
||||
"url": "http://192.168.200.233:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev3/Data)",
|
||||
"url": "http://210.222.143.78:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
]
|
||||
|
||||
# =============================================================================
|
||||
# 개발 편의 설정
|
||||
# =============================================================================
|
||||
|
||||
# 디버그 모드 (개발 중에는 True로 설정하면 더 자세한 로그 확인 가능)
|
||||
DEBUG_MODE = False
|
||||
|
||||
# API 타임아웃 설정 (초)
|
||||
API_TIMEOUT_SECONDS = 600
|
||||
|
||||
# =============================================================================
|
||||
# 설정 변경 가이드
|
||||
# =============================================================================
|
||||
"""
|
||||
🔧 다른 PC에서 사용할 때 확인할 것들:
|
||||
|
||||
1. SERVICE_PORT: 포트 충돌 시 변경
|
||||
- Windows: netstat -an | findstr :51003
|
||||
- 사용 중이면 51004, 51005 등으로 변경
|
||||
|
||||
2. EXTERNAL_API_URL: API 서버 주소 확인
|
||||
- API 서버가 실행 중인지 확인
|
||||
- IP 주소가 정확한지 확인
|
||||
- 브라우저에서 http://localhost:51003/api-status 로 연결 상태 확인
|
||||
|
||||
3. 방화벽 설정:
|
||||
- Windows Defender 방화벽에서 포트 허용 필요할 수 있음
|
||||
|
||||
4. 네트워크 확인:
|
||||
- ping 192.168.200.233 로 API 서버 연결 확인
|
||||
"""
|
||||
@@ -31,7 +31,7 @@ HOST = "0.0.0.0"
|
||||
# 다른 PC나 서버에서 API 서버를 실행하는 경우 IP 주소를 변경하세요
|
||||
# 예: "http://192.168.1.100:51000/api/..."
|
||||
# "http://localhost:51000/api/..."
|
||||
EXTERNAL_API_URL = "http://210.222.143.78:51000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data"
|
||||
EXTERNAL_API_URL = "http://210.222.143.78:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data"
|
||||
|
||||
# =============================================================================
|
||||
# 사전 정의된 API 엔드포인트 설정
|
||||
@@ -39,19 +39,19 @@ EXTERNAL_API_URL = "http://210.222.143.78:51000/api/services/vactorImageSearch/v
|
||||
|
||||
# 사용 가능한 API 엔드포인트 목록 (웹 UI에서 선택 가능)
|
||||
PREDEFINED_ENDPOINTS = [
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev2/Data)",
|
||||
"url": "http://192.168.200.232:51000/api/services/vectorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev3/Data)",
|
||||
"url": "http://192.168.200.233:52000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev2/Data)",
|
||||
"url": "http://192.168.200.232:51000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
{
|
||||
"name": "벡터이미지 검색(Daegu Center/Data)",
|
||||
"url": "http://210.222.143.78:51000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"name": "벡터이미지 검색(DataCenter/inputImage)",
|
||||
"url": "http://210.222.143.78:51000/api/services/vectorImageSearch/vit/inputImage/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
]
|
||||
|
||||
@@ -184,5 +184,58 @@ class AppConfig:
|
||||
|
||||
return validated
|
||||
|
||||
def validate_image_search_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
이미지 검색 요청 데이터 검증 및 정리
|
||||
|
||||
Args:
|
||||
data: 클라이언트에서 온 이미지 검색 요청 데이터
|
||||
|
||||
Returns:
|
||||
Dict: 검증된 데이터
|
||||
|
||||
Raises:
|
||||
ValueError: 유효하지 않은 데이터인 경우
|
||||
"""
|
||||
validated = {}
|
||||
|
||||
# 필수 필드 검증 - 이미지 데이터
|
||||
if not data.get('inputImage'):
|
||||
raise ValueError("이미지 데이터는 필수입니다")
|
||||
|
||||
# Base64 데이터 기본 검증
|
||||
image_data = data['inputImage']
|
||||
if not isinstance(image_data, str) or len(image_data) < 100:
|
||||
raise ValueError("올바르지 않은 이미지 데이터입니다")
|
||||
|
||||
validated['inputImage'] = image_data
|
||||
|
||||
# 선택적 필드 검증 및 기본값 설정
|
||||
validated['model_type'] = data.get('modelType', data.get('model_type', self.ui_config['default_settings']['model_type']))
|
||||
validated['index_type'] = data.get('indexType', data.get('index_type', self.ui_config['default_settings']['index_type']))
|
||||
|
||||
# search_num 검증
|
||||
search_num = data.get('searchNum', data.get('search_num', self.ui_config['default_settings']['search_num']))
|
||||
try:
|
||||
search_num = int(search_num)
|
||||
if not (self.ui_config['limits']['min_search_num'] <= search_num <= self.ui_config['limits']['max_search_num']):
|
||||
raise ValueError(f"Search Num은 {self.ui_config['limits']['min_search_num']}~{self.ui_config['limits']['max_search_num']} 사이여야 합니다")
|
||||
|
||||
logger.info(f"이미지 검색 search_num 검증 통과: {search_num} (타입: {type(search_num)})")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"이미지 검색 search_num 검증 실패: {search_num} (타입: {type(search_num)}), 오류: {e}")
|
||||
raise ValueError("Search Num은 숫자여야 합니다")
|
||||
|
||||
validated['search_num'] = search_num
|
||||
|
||||
# 외부 API는 camelCase를 기대함
|
||||
validated['searchNum'] = search_num
|
||||
validated['modelType'] = validated['model_type']
|
||||
validated['indexType'] = validated['index_type']
|
||||
|
||||
logger.info(f"이미지 검색 데이터 검증 완료: {dict(validated, inputImage='<base64_data>')}")
|
||||
|
||||
return validated
|
||||
|
||||
# 전역 설정 관리자 인스턴스
|
||||
app_config = AppConfig()
|
||||
11
src/response.txt
Normal file
11
src/response.txt
Normal file
File diff suppressed because one or more lines are too long
@@ -10,8 +10,8 @@
|
||||
|
||||
import httpx
|
||||
import logging
|
||||
from common.config import API_TIMEOUT_SECONDS
|
||||
import common.config as settings
|
||||
from common.settings import API_TIMEOUT_SECONDS
|
||||
import common.settings as settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -104,6 +104,22 @@ body {
|
||||
font-size: 15px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.clear_btn {
|
||||
background-color: #6c757d;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 25px;
|
||||
font-size: 15px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.clear_btn:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.error_zone {
|
||||
@@ -503,4 +519,206 @@ body {
|
||||
flex-basis: calc(20% - 16px); /* 5개씩 표시 */
|
||||
max-width: calc(20% - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 탭 네비게이션 스타일 ---- */
|
||||
|
||||
.tab_navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
margin: 20px auto;
|
||||
max-width: var(--max-width);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
overflow: hidden;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab_btn {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab_btn:not(:last-child) {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab_btn:hover {
|
||||
background-color: #e9ecef;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab_btn.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab_btn.active:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* ---- 탭 컨텐츠 스타일 ---- */
|
||||
|
||||
.tab_content {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab_content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ---- 이미지 업로드 관련 스타일 ---- */
|
||||
|
||||
.image_upload_zone {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: var(--border-radius-lg);
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.image_upload_zone:hover {
|
||||
border-color: var(--primary-color);
|
||||
background-color: #f0f8ff;
|
||||
}
|
||||
|
||||
.upload_label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload_label:hover {
|
||||
background-color: #0056b3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
.upload_icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file_info {
|
||||
margin-top: 15px;
|
||||
padding: 12px;
|
||||
background-color: #e8f4fd;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #bee5eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file_name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.file_path {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.remove_btn {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.remove_btn:hover {
|
||||
background-color: #c82333;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.image_preview {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image_preview img {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
|
||||
/* ---- 모바일 대응 ---- */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tab_navigation {
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.tab_btn {
|
||||
padding: 10px 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.image_upload_zone {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.upload_label {
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file_info {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file_name,
|
||||
.file_path {
|
||||
min-width: auto;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.image_preview img {
|
||||
max-width: 250px;
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ const SELECTORS = {
|
||||
};
|
||||
|
||||
const API_ENDPOINTS = {
|
||||
CREATE: '/create'
|
||||
CREATE: '/create',
|
||||
IMAGE_SEARCH: '/image-search'
|
||||
};
|
||||
|
||||
const BASE64_PREFIX = 'data:image/png;base64,';
|
||||
|
||||
@@ -15,6 +15,22 @@ $(document).ready(function() {
|
||||
|
||||
// 메인 컨트롤러 인스턴스 생성
|
||||
window.imageGenerator = new ImageGeneratorController();
|
||||
|
||||
// 탭 매니저 초기화
|
||||
if (typeof TabManager !== 'undefined') {
|
||||
window.tabManager = new TabManager();
|
||||
console.log('Tab Manager 초기화 완료');
|
||||
} else {
|
||||
console.warn('TabManager가 로드되지 않았습니다.');
|
||||
}
|
||||
|
||||
// 이미지 업로드 매니저 초기화
|
||||
if (typeof ImageUploadManager !== 'undefined') {
|
||||
window.imageUploadManager = new ImageUploadManager();
|
||||
console.log('Image Upload Manager 초기화 완료');
|
||||
} else {
|
||||
console.warn('ImageUploadManager가 로드되지 않았습니다.');
|
||||
}
|
||||
|
||||
// API 설정 관리자 초기화
|
||||
if (typeof ApiConfigManager !== 'undefined') {
|
||||
@@ -41,14 +57,24 @@ function setupEventListeners() {
|
||||
$(SELECTORS.GENERATOR_BTN).on('click', function() {
|
||||
window.imageGenerator.generateImage();
|
||||
});
|
||||
|
||||
|
||||
// 엔터키로 생성
|
||||
$(SELECTORS.PROMPT_INPUT).on('keypress', function(e) {
|
||||
if (e.which === 13) { // Enter key
|
||||
window.imageGenerator.generateImage();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Text Generator 결과 초기화 버튼
|
||||
$('#clear_results_btn').on('click', function() {
|
||||
clearTextGeneratorResults();
|
||||
});
|
||||
|
||||
// Image Upload 결과 초기화 버튼
|
||||
$('#image_clear_results_btn').on('click', function() {
|
||||
clearImageUploadResults();
|
||||
});
|
||||
|
||||
// 설정 변경 시 상태 관리자 업데이트 (선택사항)
|
||||
$(SELECTORS.MODEL_TYPE_SELECT + ', ' + SELECTORS.INDEX_TYPE_SELECT).on('change', function() {
|
||||
if (window.appState) {
|
||||
@@ -72,3 +98,46 @@ async function initializeApp() {
|
||||
$(SELECTORS.SEARCH_NUM_INPUT).val(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Generator 결과 초기화
|
||||
*/
|
||||
function clearTextGeneratorResults() {
|
||||
// 결과 영역 초기화
|
||||
$(SELECTORS.QUERY_IMAGE_ZONE).empty();
|
||||
$(SELECTORS.RESULT_IMAGE_ZONE).empty();
|
||||
$(SELECTORS.JSON_VIEWER).empty().hide();
|
||||
$(SELECTORS.ERROR_ZONE).empty();
|
||||
|
||||
// Clear 버튼 숨기기
|
||||
$('#clear_results_btn').hide();
|
||||
|
||||
console.log('Text Generator 결과 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* Image Upload 결과 초기화
|
||||
*/
|
||||
function clearImageUploadResults() {
|
||||
// 결과 영역 초기화
|
||||
$('#image_result_zone').empty();
|
||||
$('#image_json_viewer').empty().hide();
|
||||
$('#image_error_zone').empty();
|
||||
|
||||
// Clear 버튼 숨기기
|
||||
$('#image_clear_results_btn').hide();
|
||||
|
||||
// 업로드된 파일 정보는 유지 (사용자가 파일을 다시 선택할 필요 없도록)
|
||||
console.log('Image Upload 결과 초기화 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear 버튼 표시 (결과가 생성되었을 때 호출)
|
||||
*/
|
||||
function showClearButton(tabType) {
|
||||
if (tabType === 'text-generator') {
|
||||
$('#clear_results_btn').show();
|
||||
} else if (tabType === 'image-upload') {
|
||||
$('#image_clear_results_btn').show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,64 @@ class ApiClient {
|
||||
return processedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 파일로 유사 이미지 검색 API 호출
|
||||
*/
|
||||
async searchSimilarImages(file, options = {}) {
|
||||
console.log('ApiClient.searchSimilarImages 호출:', { file: file.name, options });
|
||||
|
||||
try {
|
||||
// 파일을 base64로 변환
|
||||
const base64Image = await this.fileToBase64(file);
|
||||
|
||||
// 요청 데이터 구성 (외부 API는 inputImage 필드를 기대함)
|
||||
const requestData = {
|
||||
inputImage: base64Image,
|
||||
model_type: options.modelType || 'l14',
|
||||
index_type: options.indexType || 'l2',
|
||||
search_num: options.searchNum || 4,
|
||||
modelType: options.modelType || 'l14', // 백엔드 호환성
|
||||
indexType: options.indexType || 'l2', // 백엔드 호환성
|
||||
searchNum: options.searchNum || 4 // 백엔드 호환성
|
||||
};
|
||||
|
||||
console.log('이미지 검색 요청 데이터:', {
|
||||
...requestData,
|
||||
inputImage: `base64 데이터 (${Math.round(base64Image.length / 1024)}KB)`
|
||||
});
|
||||
|
||||
const response = await this.makeRequest('POST', API_ENDPOINTS.IMAGE_SEARCH, requestData);
|
||||
console.log('이미지 검색 응답 성공:', response);
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('이미지 검색 ApiClient 오류:', error);
|
||||
throw this.handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일을 Base64로 변환
|
||||
*/
|
||||
fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
// data:image/jpeg;base64, 부분을 제거하고 base64 데이터만 추출
|
||||
const base64Data = e.target.result.split(',')[1];
|
||||
resolve(base64Data);
|
||||
};
|
||||
|
||||
reader.onerror = function(error) {
|
||||
console.error('파일 읽기 오류:', error);
|
||||
reject(new Error('파일을 읽을 수 없습니다.'));
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 타임아웃 설정
|
||||
*/
|
||||
|
||||
468
src/static/js/modules/imageUploadManager.js
Normal file
468
src/static/js/modules/imageUploadManager.js
Normal file
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* @File: imageUploadManager.js
|
||||
* @Date: 2025-09-22
|
||||
* @Author: SGM
|
||||
* @Brief: 이미지 업로드 및 검색 관리 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class ImageUploadManager {
|
||||
constructor() {
|
||||
this.selectedFile = null;
|
||||
this.isSearching = false;
|
||||
|
||||
// ApiClient 초기화 확인
|
||||
console.log('ApiClient 클래스 존재 여부:', typeof ApiClient);
|
||||
if (typeof ApiClient === 'undefined') {
|
||||
console.error('ApiClient 클래스가 정의되지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiClient = new ApiClient();
|
||||
console.log('ApiClient 인스턴스 생성됨:', this.apiClient);
|
||||
console.log('searchSimilarImages 메서드 존재 여부:', typeof this.apiClient.searchSimilarImages);
|
||||
|
||||
this.uiManager = new UiManager();
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 업로드 매니저 초기화
|
||||
*/
|
||||
init() {
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 바인딩
|
||||
*/
|
||||
bindEvents() {
|
||||
// 파일 선택 이벤트
|
||||
$('#image_file_input').on('change', (e) => {
|
||||
this.handleFileSelect(e);
|
||||
});
|
||||
|
||||
// 파일 제거 버튼 클릭
|
||||
$('#remove_image_btn').on('click', () => {
|
||||
this.removeSelectedFile();
|
||||
});
|
||||
|
||||
// 이미지 검색 버튼 클릭
|
||||
$('#image_search_btn').on('click', () => {
|
||||
this.searchSimilarImages();
|
||||
});
|
||||
|
||||
// 드래그 앤 드롭 지원
|
||||
$('.image_upload_zone').on({
|
||||
'dragover': (e) => {
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).addClass('dragover');
|
||||
},
|
||||
'dragleave': (e) => {
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).removeClass('dragover');
|
||||
},
|
||||
'drop': (e) => {
|
||||
e.preventDefault();
|
||||
$(e.currentTarget).removeClass('dragover');
|
||||
const files = e.originalEvent.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
this.handleFileSelect({ target: { files: files } });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 선택 처리
|
||||
* @param {Event} event - 파일 선택 이벤트
|
||||
*/
|
||||
handleFileSelect(event) {
|
||||
const files = event.target.files;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
|
||||
// 파일 유효성 검사
|
||||
if (!this.validateFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedFile = file;
|
||||
this.displayFileInfo(file);
|
||||
this.previewImage(file);
|
||||
|
||||
console.log('파일 선택됨:', file.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 유효성 검사
|
||||
* @param {File} file - 검사할 파일
|
||||
* @returns {boolean} 유효성 검사 결과
|
||||
*/
|
||||
validateFile(file) {
|
||||
// 파일 타입 검사
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.showError('이미지 파일만 업로드 가능합니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 파일 크기 검사 (10MB 제한)
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
this.showError('파일 크기는 10MB 이하여야 합니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 정보 표시
|
||||
* @param {File} file - 표시할 파일
|
||||
*/
|
||||
displayFileInfo(file) {
|
||||
const fileName = file.name;
|
||||
const filePath = file.webkitRelativePath || fileName;
|
||||
const fileSize = this.formatFileSize(file.size);
|
||||
|
||||
$('#image_file_name').text(`${fileName} (${fileSize})`);
|
||||
$('#image_file_path').text(filePath);
|
||||
$('#image_file_info').show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 미리보기 표시
|
||||
* @param {File} file - 미리보기할 파일
|
||||
*/
|
||||
previewImage(file) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
$('#preview_img').attr('src', e.target.result);
|
||||
$('#image_preview').show();
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 선택된 파일 제거
|
||||
*/
|
||||
removeSelectedFile() {
|
||||
this.selectedFile = null;
|
||||
$('#image_file_input').val('');
|
||||
$('#image_file_info').hide();
|
||||
$('#image_preview').hide();
|
||||
this.clearResults();
|
||||
|
||||
console.log('파일 선택 해제됨');
|
||||
}
|
||||
|
||||
/**
|
||||
* 유사 이미지 검색
|
||||
*/
|
||||
async searchSimilarImages() {
|
||||
if (this.isSearching) {
|
||||
console.warn('이미지 검색이 이미 진행 중입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일 선택 확인
|
||||
if (!this.selectedFile) {
|
||||
this.showError('이미지 파일을 먼저 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isSearching = true;
|
||||
this.clearErrors();
|
||||
this.showLoading();
|
||||
|
||||
// 검색 옵션 수집
|
||||
const searchOptions = this.collectSearchOptions();
|
||||
|
||||
// API 클라이언트 상태 재확인
|
||||
console.log('검색 시작 - ApiClient 상태 확인:', {
|
||||
apiClientExists: !!this.apiClient,
|
||||
apiClientType: typeof this.apiClient,
|
||||
searchMethodExists: !!(this.apiClient && this.apiClient.searchSimilarImages),
|
||||
searchMethodType: this.apiClient ? typeof this.apiClient.searchSimilarImages : 'N/A'
|
||||
});
|
||||
|
||||
if (!this.apiClient) {
|
||||
console.error('ApiClient 인스턴스가 없습니다. 재초기화 시도...');
|
||||
this.apiClient = new ApiClient();
|
||||
}
|
||||
|
||||
if (!this.apiClient.searchSimilarImages) {
|
||||
throw new Error('searchSimilarImages 메서드가 존재하지 않습니다.');
|
||||
}
|
||||
|
||||
// API 호출
|
||||
console.log('이미지 검색 시작:', this.selectedFile.name);
|
||||
const result = await this.apiClient.searchSimilarImages(this.selectedFile, searchOptions);
|
||||
console.log('이미지 검색 성공:', result);
|
||||
|
||||
// 결과 표시
|
||||
this.displaySearchResults(result);
|
||||
|
||||
// Clear 버튼 표시
|
||||
if (typeof showClearButton === 'function') {
|
||||
showClearButton('image-upload');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('이미지 검색 오류:', error);
|
||||
this.handleSearchError(error);
|
||||
} finally {
|
||||
this.isSearching = false;
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 옵션 수집
|
||||
* @returns {Object} 검색 옵션
|
||||
*/
|
||||
collectSearchOptions() {
|
||||
return {
|
||||
modelType: $('#image_model_type_select').val(),
|
||||
indexType: $('#image_index_type_select').val(),
|
||||
searchNum: parseInt($('#image_search_num_input').val(), 10)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 결과 표시
|
||||
* @param {Object} result - 검색 결과
|
||||
*/
|
||||
displaySearchResults(result) {
|
||||
if (!result || !result.vectorResult) {
|
||||
this.showError('검색 결과가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 결과 이미지들 표시
|
||||
if (result.vectorResult && result.vectorResult.length > 0) {
|
||||
this.displayResultImages(result.vectorResult);
|
||||
}
|
||||
|
||||
// JSON 뷰어 표시
|
||||
this.displayJson(result);
|
||||
|
||||
// 서버 제한 알림
|
||||
if (result._server_limitation) {
|
||||
this.showWarning(result._server_limitation.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 이미지들 표시
|
||||
* @param {Array} vectorResult - 벡터 검색 결과
|
||||
*/
|
||||
displayResultImages(vectorResult) {
|
||||
const $resultZone = $('#image_result_zone');
|
||||
$resultZone.empty();
|
||||
|
||||
if (!vectorResult || vectorResult.length === 0) {
|
||||
const $noResults = $('<div class="no_results">');
|
||||
$noResults.text('검색 결과가 없습니다.');
|
||||
$resultZone.append($noResults);
|
||||
return;
|
||||
}
|
||||
|
||||
vectorResult.forEach((item, index) => {
|
||||
if (item.image) {
|
||||
const $resultBlock = this.createResultBlock(item, index);
|
||||
$resultZone.append($resultBlock);
|
||||
}
|
||||
});
|
||||
|
||||
// 이미지 로드 완료 후 애니메이션 효과
|
||||
this.animateResults();
|
||||
console.log(`${vectorResult.length}개의 검색 결과 이미지 표시 완료`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 블록 생성 (기존 텍스트 생성기와 동일한 방식)
|
||||
* @param {Object} item - 이미지 결과 항목
|
||||
* @param {number} index - 인덱스
|
||||
* @returns {jQuery} 생성된 결과 블록
|
||||
*/
|
||||
createResultBlock(item, index) {
|
||||
const $block = $('<div class="result_block">');
|
||||
|
||||
// 애니메이션을 위해 초기에 숨김
|
||||
$block.css('display', 'none');
|
||||
|
||||
// 이미지 생성
|
||||
const $img = $('<img>', {
|
||||
src: `data:image/jpeg;base64,${item.image}`,
|
||||
class: 'result_image',
|
||||
alt: `검색 결과 ${index + 1}`,
|
||||
loading: 'lazy' // 지연 로딩
|
||||
});
|
||||
|
||||
// 이미지 로드 이벤트
|
||||
$img.on('load', function() {
|
||||
console.log(`이미지 ${index + 1} 로드 완료`);
|
||||
});
|
||||
|
||||
$img.on('error', function() {
|
||||
console.error(`이미지 ${index + 1} 로드 실패`);
|
||||
$(this).attr('alt', `이미지 로드 실패 ${index + 1}`);
|
||||
});
|
||||
|
||||
// 유사도/퍼센트 표시
|
||||
let $percent = null;
|
||||
if (item.percents !== undefined) {
|
||||
$percent = $('<div class="result_percent">');
|
||||
$percent.text(`유사도: ${item.percents.toFixed(1)}%`);
|
||||
} else if (item.similarity !== undefined) {
|
||||
$percent = $('<div class="result_percent">');
|
||||
$percent.text(`유사도: ${(item.similarity * 100).toFixed(1)}%`);
|
||||
} else if (item.percent !== undefined) {
|
||||
$percent = $('<div class="result_percent">');
|
||||
$percent.text(`${(item.percent * 100).toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// 블록에 이미지와 퍼센트 추가
|
||||
$block.append($img);
|
||||
if ($percent) {
|
||||
$block.append($percent);
|
||||
}
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 애니메이션 효과
|
||||
*/
|
||||
animateResults() {
|
||||
const $resultBlocks = $('#image_result_zone .result_block');
|
||||
$resultBlocks.each(function(index) {
|
||||
$(this).delay(index * 100).fadeIn(300);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 뷰어 표시 (AI generator와 동일한 방식)
|
||||
* @param {Object} data - 표시할 JSON 데이터
|
||||
*/
|
||||
displayJson(data) {
|
||||
const $jsonViewer = $('#image_json_viewer');
|
||||
|
||||
try {
|
||||
// JSON 뷰어 라이브러리 사용 (AI generator와 동일한 설정)
|
||||
$jsonViewer.jsonViewer(data, {
|
||||
collapsed: true, // 기본적으로 접힌 상태
|
||||
withQuotes: false,
|
||||
withLinks: false,
|
||||
rootCollapsable: true // 루트도 접을 수 있게
|
||||
}).show();
|
||||
|
||||
console.log('JSON 뷰어 표시 완료');
|
||||
} catch (error) {
|
||||
console.error('JSON 뷰어 오류:', error);
|
||||
// 라이브러리 실패 시 기본 JSON 표시
|
||||
$jsonViewer
|
||||
.text(JSON.stringify(data, null, 2))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 오류 처리
|
||||
* @param {Error} error - 발생한 오류
|
||||
*/
|
||||
handleSearchError(error) {
|
||||
let errorMessage = '이미지 검색 중 오류가 발생했습니다.';
|
||||
|
||||
if (error.response) {
|
||||
const data = error.response;
|
||||
switch (data.error_type) {
|
||||
case 'validation_error':
|
||||
errorMessage = data.detail || '입력값이 유효하지 않습니다.';
|
||||
break;
|
||||
case 'connection_error':
|
||||
errorMessage = '외부 API 서버에 연결할 수 없습니다. 네트워크를 확인해주세요.';
|
||||
break;
|
||||
case 'api_error':
|
||||
errorMessage = `외부 API 오류 (${data.status_code}): ${data.detail}`;
|
||||
break;
|
||||
default:
|
||||
errorMessage = data.detail || errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
this.showError(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 크기 포맷팅
|
||||
* @param {number} bytes - 바이트 크기
|
||||
* @returns {string} 포맷된 크기 문자열
|
||||
*/
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 메시지 표시
|
||||
* @param {string} message - 에러 메시지
|
||||
*/
|
||||
showError(message) {
|
||||
$('#image_error_zone').html(`<div style="color: red;">${message}</div>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 경고 메시지 표시
|
||||
* @param {string} message - 경고 메시지
|
||||
*/
|
||||
showWarning(message) {
|
||||
$('#image_error_zone').html(`<div style="color: orange;">${message}</div>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 메시지 제거
|
||||
*/
|
||||
clearErrors() {
|
||||
$('#image_error_zone').empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 표시
|
||||
*/
|
||||
showLoading() {
|
||||
$('#image_loading_zone').show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 숨김
|
||||
*/
|
||||
hideLoading() {
|
||||
$('#image_loading_zone').hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 초기화 (AI generator와 동일한 방식)
|
||||
*/
|
||||
clearResults() {
|
||||
$('#image_result_zone').empty();
|
||||
$('#image_json_viewer').empty().hide(); // 비우고 숨김
|
||||
this.clearErrors();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 노출
|
||||
window.ImageUploadManager = ImageUploadManager;
|
||||
177
src/static/js/modules/tabManager.js
Normal file
177
src/static/js/modules/tabManager.js
Normal file
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* @File: tabManager.js
|
||||
* @Date: 2025-09-22
|
||||
* @Author: SGM
|
||||
* @Brief: 탭 전환 관리 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class TabManager {
|
||||
constructor() {
|
||||
this.currentTab = 'text-generator';
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 매니저 초기화
|
||||
*/
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.setActiveTab(this.currentTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 바인딩
|
||||
*/
|
||||
bindEvents() {
|
||||
// 탭 버튼 클릭 이벤트
|
||||
$('.tab_btn').on('click', (e) => {
|
||||
const tabName = $(e.target).data('tab');
|
||||
this.setActiveTab(tabName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 탭 설정
|
||||
* @param {string} tabName - 활성화할 탭 이름
|
||||
*/
|
||||
setActiveTab(tabName) {
|
||||
// 이전 탭과 같으면 무시
|
||||
if (this.currentTab === tabName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 탭 버튼에서 active 클래스 제거
|
||||
$('.tab_btn').removeClass('active');
|
||||
|
||||
// 모든 탭 컨텐츠 숨기기
|
||||
$('.tab_content').removeClass('active');
|
||||
|
||||
// 선택된 탭 버튼에 active 클래스 추가
|
||||
$(`.tab_btn[data-tab="${tabName}"]`).addClass('active');
|
||||
|
||||
// 선택된 탭 컨텐츠 표시
|
||||
$(`#${tabName}-tab`).addClass('active');
|
||||
|
||||
// 현재 탭 업데이트
|
||||
this.currentTab = tabName;
|
||||
|
||||
console.log(`탭 전환됨: ${tabName}`);
|
||||
|
||||
// 탭 전환 이벤트 발생
|
||||
this.onTabChanged(tabName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환 시 호출되는 콜백
|
||||
* @param {string} tabName - 전환된 탭 이름
|
||||
*/
|
||||
onTabChanged(tabName) {
|
||||
// 에러 메시지 초기화
|
||||
this.clearErrors();
|
||||
|
||||
// 로딩 상태 초기화
|
||||
this.clearLoading();
|
||||
|
||||
// Clear 버튼 숨기기
|
||||
this.hideClearButtons();
|
||||
|
||||
// 상태 관리자에 탭 변경 알림
|
||||
if (window.appState) {
|
||||
window.appState.setCurrentTab(tabName);
|
||||
}
|
||||
|
||||
// 탭별 추가 초기화 작업
|
||||
if (tabName === 'image-upload') {
|
||||
this.initImageUploadTab();
|
||||
} else if (tabName === 'text-generator') {
|
||||
this.initTextGeneratorTab();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 활성 탭 반환
|
||||
* @returns {string} 현재 활성 탭 이름
|
||||
*/
|
||||
getCurrentTab() {
|
||||
return this.currentTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 메시지 초기화
|
||||
*/
|
||||
clearErrors() {
|
||||
$('#error_zone').empty();
|
||||
$('#image_error_zone').empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 상태 초기화
|
||||
*/
|
||||
clearLoading() {
|
||||
$('#loading_zone').hide();
|
||||
$('#image_loading_zone').hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear 버튼 숨기기
|
||||
*/
|
||||
hideClearButtons() {
|
||||
$('#clear_results_btn').hide();
|
||||
$('#image_clear_results_btn').hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Image Upload 탭 초기화
|
||||
*/
|
||||
initImageUploadTab() {
|
||||
console.log('Image Upload 탭 초기화');
|
||||
// 이미지 업로드 관련 초기화 작업
|
||||
this.clearImageUpload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Generator 탭 초기화
|
||||
*/
|
||||
initTextGeneratorTab() {
|
||||
console.log('Text Generator 탭 초기화');
|
||||
// 텍스트 생성기 관련 초기화 작업
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 업로드 상태 초기화
|
||||
*/
|
||||
clearImageUpload() {
|
||||
// 파일 입력 초기화
|
||||
$('#image_file_input').val('');
|
||||
|
||||
// 파일 정보 숨기기
|
||||
$('#image_file_info').hide();
|
||||
|
||||
// 미리보기 숨기기
|
||||
$('#image_preview').hide();
|
||||
|
||||
// 결과 초기화
|
||||
$('#image_result_zone').empty();
|
||||
$('#image_json_viewer').empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 비활성화 (필요시 사용)
|
||||
* @param {string} tabName - 비활성화할 탭 이름
|
||||
*/
|
||||
disableTab(tabName) {
|
||||
$(`.tab_btn[data-tab="${tabName}"]`).prop('disabled', true).addClass('disabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 활성화 (필요시 사용)
|
||||
* @param {string} tabName - 활성화할 탭 이름
|
||||
*/
|
||||
enableTab(tabName) {
|
||||
$(`.tab_btn[data-tab="${tabName}"]`).prop('disabled', false).removeClass('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 노출
|
||||
window.TabManager = TabManager;
|
||||
@@ -162,12 +162,22 @@ class UiManager {
|
||||
withQuotes: false,
|
||||
withLinks: false
|
||||
}).show();
|
||||
|
||||
// Clear 버튼 표시
|
||||
if (typeof showClearButton === 'function') {
|
||||
showClearButton('text-generator');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('JSON 뷰어 오류:', error);
|
||||
// 라이브러리 실패 시 기본 JSON 표시
|
||||
this.elements.jsonViewer
|
||||
.text(JSON.stringify(data, null, 2))
|
||||
.show();
|
||||
|
||||
// Clear 버튼 표시
|
||||
if (typeof showClearButton === 'function') {
|
||||
showClearButton('text-generator');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,40 +68,117 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img_generator_model_zone_1">
|
||||
<span class="model_name">AI Image Generator</span>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
id="input_txt_box"
|
||||
class="input_txt_box"
|
||||
placeholder="입력하세요." />
|
||||
<!-- 탭 네비게이션 -->
|
||||
<div class="tab_navigation">
|
||||
<button class="tab_btn active" data-tab="text-generator">
|
||||
Text Generator
|
||||
</button>
|
||||
<button class="tab_btn" data-tab="image-upload">Image Upload</button>
|
||||
</div>
|
||||
|
||||
<!-- Text Generator 탭 -->
|
||||
<div id="text-generator-tab" class="tab_content active">
|
||||
<div class="img_generator_model_zone_1">
|
||||
<span class="model_name">AI Image Generator</span>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
id="input_txt_box"
|
||||
class="input_txt_box"
|
||||
placeholder="입력하세요." />
|
||||
</div>
|
||||
<div class="select_zone">
|
||||
<label for="model_type_select">Model Type:</label>
|
||||
<select id="model_type_select">
|
||||
<option value="b32">b32</option>
|
||||
<option value="b16">b16</option>
|
||||
<option value="l14" selected>l14</option>
|
||||
<option value="l14_336">l14_336</option>
|
||||
</select>
|
||||
<label for="index_type_select">Index Type:</label>
|
||||
<select id="index_type_select">
|
||||
<option value="l2">l2</option>
|
||||
<option value="cos">cos</option>
|
||||
</select>
|
||||
<label for="search_num_input">Search Num:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="search_num_input"
|
||||
value="4"
|
||||
min="1"
|
||||
max="10" />
|
||||
</div>
|
||||
<div id="error_zone" class="error_zone"></div>
|
||||
<div class="generator_btn_zone">
|
||||
<button id="generator_btn" class="generator_btn">Request</button>
|
||||
<button id="clear_results_btn" class="clear_btn" style="display: none;">Clear Results</button>
|
||||
</div>
|
||||
<div id="loading_zone" style="display: none">Generating...</div>
|
||||
<div class="result_container">
|
||||
<div id="query_image_zone" class="query_image_zone"></div>
|
||||
<div id="result_image_zone" class="result_image_zone"></div>
|
||||
<pre id="json_viewer" class="json_viewer"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select_zone">
|
||||
<label for="model_type_select">Model Type:</label>
|
||||
<select id="model_type_select">
|
||||
<option value="b32">b32</option>
|
||||
<option value="b16">b16</option>
|
||||
<option value="l14" selected>l14</option>
|
||||
<option value="l14_336">l14_336</option>
|
||||
</select>
|
||||
<label for="index_type_select">Index Type:</label>
|
||||
<select id="index_type_select">
|
||||
<option value="l2">l2</option>
|
||||
<option value="cos">cos</option>
|
||||
</select>
|
||||
<label for="search_num_input">Search Num:</label>
|
||||
<input type="number" id="search_num_input" value="4" min="1" max="10" />
|
||||
</div>
|
||||
<div id="error_zone" class="error_zone"></div>
|
||||
<div class="generator_btn_zone">
|
||||
<button id="generator_btn" class="generator_btn">Request</button>
|
||||
</div>
|
||||
<div id="loading_zone" style="display: none">Generating...</div>
|
||||
<div class="result_container">
|
||||
<div id="query_image_zone" class="query_image_zone"></div>
|
||||
<div id="result_image_zone" class="result_image_zone"></div>
|
||||
<pre id="json_viewer" class="json_viewer"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload 탭 -->
|
||||
<div id="image-upload-tab" class="tab_content">
|
||||
<div class="img_generator_model_zone_1">
|
||||
<span class="model_name">Image Search</span>
|
||||
<div class="select_zone">
|
||||
<label for="image_model_type_select">Model Type:</label>
|
||||
<select id="image_model_type_select">
|
||||
<option value="b32">b32</option>
|
||||
<option value="b16">b16</option>
|
||||
<option value="l14" selected>l14</option>
|
||||
<option value="l14_336">l14_336</option>
|
||||
</select>
|
||||
<label for="image_index_type_select">Index Type:</label>
|
||||
<select id="image_index_type_select">
|
||||
<option value="l2" selected>l2</option>
|
||||
<option value="cos">cos</option>
|
||||
</select>
|
||||
<label for="image_search_num_input">Search Num:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="image_search_num_input"
|
||||
value="4"
|
||||
min="1"
|
||||
max="10" />
|
||||
</div>
|
||||
|
||||
<!-- 이미지 파일 업로드 영역 -->
|
||||
<div class="image_upload_zone">
|
||||
<label for="image_file_input" class="upload_label">
|
||||
<span class="upload_icon">📁</span>
|
||||
<span class="upload_text">이미지 파일 선택</span>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="image_file_input"
|
||||
accept="image/*"
|
||||
style="display: none" />
|
||||
<div id="image_file_info" class="file_info" style="display: none">
|
||||
<span id="image_file_name" class="file_name"></span>
|
||||
<span id="image_file_path" class="file_path"></span>
|
||||
<button id="remove_image_btn" class="remove_btn">×</button>
|
||||
</div>
|
||||
<div id="image_preview" class="image_preview" style="display: none">
|
||||
<img id="preview_img" alt="미리보기" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="image_error_zone" class="error_zone"></div>
|
||||
<div class="generator_btn_zone">
|
||||
<button id="image_search_btn" class="generator_btn">Request</button>
|
||||
<button id="image_clear_results_btn" class="clear_btn" style="display: none;">Clear Results</button>
|
||||
</div>
|
||||
<div id="image_loading_zone" style="display: none">Searching...</div>
|
||||
<div class="result_container">
|
||||
<div id="image_result_zone" class="result_image_zone"></div>
|
||||
<pre id="image_json_viewer" class="json_viewer"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="static/js/jquery.json-viewer.min.js"></script>
|
||||
@@ -115,6 +192,8 @@
|
||||
<script src="static/js/modules/uiManager.js"></script>
|
||||
<script src="static/js/modules/apiConfigManager.js"></script>
|
||||
<script src="static/js/modules/imageGenerator.js"></script>
|
||||
<script src="static/js/modules/tabManager.js"></script>
|
||||
<script src="static/js/modules/imageUploadManager.js"></script>
|
||||
|
||||
<!-- 초기화 -->
|
||||
<script src="static/js/init.js"></script>
|
||||
|
||||
@@ -141,6 +141,111 @@ async def get_api_endpoints():
|
||||
}
|
||||
|
||||
|
||||
@app.post("/image-search")
|
||||
async def image_search(request: Request):
|
||||
"""
|
||||
이미지 업로드를 통한 유사 이미지 검색
|
||||
"""
|
||||
try:
|
||||
raw_data = await request.json()
|
||||
logger.info(f"이미지 검색 요청 받음 - 모델: {raw_data.get('model_type', 'N/A')}, 검색 개수: {raw_data.get('search_num', 'N/A')}")
|
||||
|
||||
# 이미지 데이터 검증
|
||||
if not raw_data.get('inputImage'):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": "이미지 데이터가 제공되지 않았습니다.", "error_type": "validation_error"}
|
||||
)
|
||||
|
||||
# 요청 데이터 검증 및 정리
|
||||
validated_data = app_config.validate_image_search_data(raw_data)
|
||||
logger.info(f"검증된 이미지 검색 데이터: {dict(validated_data, inputImage='<base64_data>')}")
|
||||
|
||||
# 외부 API URL 설정 (inputImage API 사용)
|
||||
image_search_url = None
|
||||
for endpoint in PREDEFINED_ENDPOINTS:
|
||||
if 'inputImage' in endpoint['url']:
|
||||
image_search_url = endpoint['url']
|
||||
break
|
||||
|
||||
if not image_search_url:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "이미지 검색 API 엔드포인트를 찾을 수 없습니다.", "error_type": "config_error"}
|
||||
)
|
||||
|
||||
# 외부 API 호출
|
||||
logger.info(f"외부 이미지 검색 API 호출: {image_search_url}")
|
||||
timeout_config = httpx.Timeout(API_TIMEOUT_SECONDS, connect=10.0)
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout_config) as client:
|
||||
response = await client.post(image_search_url, json=validated_data)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
logger.info(f"이미지 검색 API 호출 성공: 응답 크기 {len(str(result))} 문자")
|
||||
|
||||
# vectorResult 배열 크기 확인
|
||||
if 'vectorResult' in result:
|
||||
vector_count = len(result['vectorResult']) if result['vectorResult'] else 0
|
||||
logger.info(f"요청한 이미지 개수: {validated_data.get('search_num', 'N/A')}")
|
||||
logger.info(f"실제 응답받은 이미지 개수: {vector_count}")
|
||||
|
||||
if vector_count != validated_data.get('search_num', 0):
|
||||
logger.warning(f"이미지 개수 불일치! 요청: {validated_data.get('search_num')}, 응답: {vector_count}")
|
||||
|
||||
# 응답에 제한 정보 추가
|
||||
result['_server_limitation'] = {
|
||||
'requested': validated_data.get('search_num', 0),
|
||||
'actual': vector_count,
|
||||
'message': f"외부 API 서버에서 최대 {vector_count}개까지만 반환합니다."
|
||||
}
|
||||
|
||||
return JSONResponse(status_code=200, content=result)
|
||||
|
||||
except ValueError as exc:
|
||||
logger.warning(f"이미지 검색 입력 검증 오류: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"detail": str(exc),
|
||||
"error_type": "validation_error"
|
||||
}
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
error_msg = f"외부 이미지 검색 API 오류: {exc.response.status_code}"
|
||||
logger.error(f"{error_msg} - {exc.response.text}")
|
||||
return JSONResponse(
|
||||
status_code=exc.response.status_code,
|
||||
content={
|
||||
"detail": error_msg,
|
||||
"error_type": "api_error",
|
||||
"status_code": exc.response.status_code
|
||||
}
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
error_msg = f"이미지 검색 API 연결 오류: {exc.__class__.__name__}"
|
||||
logger.error(f"{error_msg}: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={
|
||||
"detail": "외부 이미지 검색 API 서버에 연결할 수 없습니다. 네트워크 연결을 확인하세요.",
|
||||
"error_type": "connection_error",
|
||||
"technical_detail": str(exc)
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
error_msg = f"이미지 검색 예상치 못한 오류: {exc.__class__.__name__}"
|
||||
logger.error(f"{error_msg}: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "이미지 검색 중 서버 내부 오류가 발생했습니다.",
|
||||
"error_type": "internal_error"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/change-api-url")
|
||||
async def change_api_url(request: Request):
|
||||
"""
|
||||
@@ -149,26 +254,26 @@ async def change_api_url(request: Request):
|
||||
try:
|
||||
data = await request.json()
|
||||
new_url = data.get("url")
|
||||
|
||||
|
||||
if not new_url:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": "URL이 제공되지 않았습니다.", "error_type": "validation_error"}
|
||||
)
|
||||
|
||||
|
||||
# URL 형식 간단 검증
|
||||
if not (new_url.startswith("http://") or new_url.startswith("https://")):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": "올바른 URL 형식이 아닙니다. http:// 또는 https://로 시작해야 합니다.", "error_type": "validation_error"}
|
||||
)
|
||||
|
||||
|
||||
# 설정 변경
|
||||
old_url = settings.EXTERNAL_API_URL
|
||||
settings.EXTERNAL_API_URL = new_url
|
||||
|
||||
|
||||
logger.info(f"API URL 변경: {old_url} -> {new_url}")
|
||||
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"old_url": old_url,
|
||||
@@ -176,7 +281,7 @@ async def change_api_url(request: Request):
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"message": "API URL이 성공적으로 변경되었습니다."
|
||||
}
|
||||
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"API URL 변경 오류: {exc}")
|
||||
return JSONResponse(
|
||||
|
||||
Reference in New Issue
Block a user