ip주소 추가 및 파일 변경
This commit is contained in:
63
README.md
63
README.md
@@ -26,7 +26,7 @@ http://localhost:51003
|
||||
|
||||
## ⚙️ 설정 관리
|
||||
|
||||
### API 엔드포인트 설정 (`src/common/config.py`)
|
||||
### API 엔드포인트 설정 (`src/common/settings.py`)
|
||||
|
||||
#### 기본 API URL(DEV3)
|
||||
|
||||
@@ -85,30 +85,49 @@ PREDEFINED_ENDPOINTS = [
|
||||
```
|
||||
src/
|
||||
├── common/
|
||||
│ └── config.py # 애플리케이션 설정 (구 settings.py)
|
||||
│ └── settings.py # 애플리케이션 설정
|
||||
├── config/
|
||||
│ ├── __init__.py
|
||||
│ └── app_config.py # 통합 설정 관리
|
||||
├── services/
|
||||
│ ├── manager.py # API 통합 관리 (구 api_manager.py)
|
||||
│ └── client.py # 외부 API 호출 (구 image_api_service.py)
|
||||
│ ├── api_manager.py # API 통합 관리
|
||||
│ └── image_api_service.py # 외부 API 호출
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ └── style.css # CSS 스타일 (CSS 변수 사용)
|
||||
│ └── js/
|
||||
│ ├── constants.js # 상수 정의 (구 const.js)
|
||||
│ ├── const.js # 상수 정의
|
||||
│ ├── init.js # 초기화 스크립트
|
||||
│ └── modules/
|
||||
│ ├── http.js # HTTP 통신 (구 apiClient.js)
|
||||
│ ├── config.js # API 설정 관리 (구 apiConfigManager.js) ⭐
|
||||
│ ├── main.js # 메인 컨트롤러 (구 imageGenerator.js)
|
||||
│ ├── state.js # 상태 관리 (구 stateManager.js)
|
||||
│ └── ui.js # UI 업데이트 (구 uiManager.js)
|
||||
│ ├── apiClient.js # API 통신
|
||||
│ ├── apiConfigManager.js # API 설정 관리 ⭐ NEW
|
||||
│ ├── imageGenerator.js # 이미지 생성 로직
|
||||
│ ├── stateManager.js # 상태 관리
|
||||
│ └── uiManager.js # UI 관리
|
||||
├── templates/
|
||||
│ └── index.html # HTML 템플릿
|
||||
└── web_server.py # FastAPI 웹 서버
|
||||
```
|
||||
|
||||
## 🎨 UI/UX 개선사항
|
||||
|
||||
### 반응형 레이아웃
|
||||
|
||||
- **데스크톱 (1025px+)**: 이미지 5개/행, 여백 20px
|
||||
- **태블릿 (769-1024px)**: 이미지 3개/행, 여백 15px
|
||||
- **모바일 (~768px)**: 이미지 2개/행, 여백 10px
|
||||
|
||||
### CSS 최적화
|
||||
|
||||
- CSS 변수 도입으로 통일된 디자인 시스템
|
||||
- 공통 색상, 크기, 그림자 값 중앙 관리
|
||||
|
||||
### 사용성 개선
|
||||
|
||||
- 접을 수 있는 API 설정 패널
|
||||
- 실시간 API 상태 표시
|
||||
- 호버 효과 및 활성 상태 표시
|
||||
|
||||
## 🐛 문제 해결
|
||||
|
||||
### 포트 충돌
|
||||
@@ -117,7 +136,7 @@ src/
|
||||
# 포트 사용 확인
|
||||
netstat -an | findstr :51003
|
||||
|
||||
# config.py에서 SERVICE_PORT 변경
|
||||
# settings.py에서 SERVICE_PORT 변경
|
||||
SERVICE_PORT = 51004 # 다른 포트로 변경
|
||||
```
|
||||
|
||||
@@ -135,3 +154,25 @@ http://localhost:51003/api-status
|
||||
|
||||
- **F12 → 콘솔 탭**: JavaScript 오류 및 API 호출 로그 확인
|
||||
- **네트워크 탭**: API 요청/응답 상세 분석
|
||||
|
||||
## 📝 개발 로그
|
||||
|
||||
### v2.0 (2025-08-06)
|
||||
|
||||
- ✅ 동적 API 엔드포인트 설정 기능 추가
|
||||
- ✅ 사전 정의된 서버 버튼들
|
||||
- ✅ 반응형 이미지 레이아웃 개선 (5/3/2개)
|
||||
- ✅ CSS 변수 도입 및 최적화
|
||||
- ✅ Legacy 코드 제거 및 클린업
|
||||
- ✅ API 설정 패널 UI 추가
|
||||
|
||||
### 주요 변경사항
|
||||
|
||||
- `apiConfigManager.js` 모듈 추가
|
||||
- API URL 실시간 변경 기능
|
||||
- 설정 패널 토글 기능
|
||||
- CSS 변수 기반 디자인 시스템
|
||||
|
||||
## 📞 지원
|
||||
|
||||
문제가 발생하면 개발자 도구(F12) 콘솔을 확인하거나 서버 로그를 점검해주세요.
|
||||
|
||||
89
src/common/settings.py
Normal file
89
src/common/settings.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- 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://210.222.143.78:51001/api/services/vactorImageSearch/vit/imageGenerate/imagen/data"
|
||||
|
||||
# =============================================================================
|
||||
# 사전 정의된 API 엔드포인트 설정
|
||||
# =============================================================================
|
||||
|
||||
# 사용 가능한 API 엔드포인트 목록 (웹 UI에서 선택 가능)
|
||||
PREDEFINED_ENDPOINTS = [
|
||||
{
|
||||
"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:51001/api/services/vactorImageSearch/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 서버 연결 확인
|
||||
"""
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from common.config import (
|
||||
from common.settings import (
|
||||
SERVICE_PORT, HOST, EXTERNAL_API_URL,
|
||||
DEBUG_MODE, API_TIMEOUT_SECONDS
|
||||
)
|
||||
|
||||
156
src/services/api_manager.py
Normal file
156
src/services/api_manager.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: api_manager.py
|
||||
@Date: 2025-08-05
|
||||
@Author: SGM
|
||||
@Brief: API 관리자 - 여러 이미지 생성 API를 통합 관리
|
||||
@section MODIFYINFO 수정정보
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from .image_api_service import call_image_generation_api
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ApiManager:
|
||||
"""
|
||||
이미지 생성 API들을 통합 관리하는 클래스
|
||||
|
||||
현재는 Imagen API만 지원하지만, 나중에 DALL-E, Midjourney 등을
|
||||
쉽게 추가할 수 있도록 설계됨
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.apis = {
|
||||
'imagen': {
|
||||
'name': 'Imagen API',
|
||||
'description': '구글 Imagen 기반 이미지 생성',
|
||||
'supported_settings': {
|
||||
'model_type': ['b32', 'b16', 'l14', 'l14_336'],
|
||||
'index_type': ['l2', 'cos'],
|
||||
'search_num': {'min': 1, 'max': 10}
|
||||
},
|
||||
'handler': self._call_imagen_api
|
||||
}
|
||||
# 추후 추가될 API들:
|
||||
# 'dalle': {...},
|
||||
# 'midjourney': {...}
|
||||
}
|
||||
self.default_api = 'imagen'
|
||||
logger.info(f"API 관리자 초기화 완료: {list(self.apis.keys())}")
|
||||
|
||||
async def generate_image(self, data: Dict[str, Any], api_name: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
이미지 생성 요청 처리
|
||||
|
||||
Args:
|
||||
data: 이미지 생성 요청 데이터
|
||||
api_name: 사용할 API 이름 (None이면 기본 API 사용)
|
||||
|
||||
Returns:
|
||||
Dict: API 응답 데이터
|
||||
|
||||
Raises:
|
||||
ValueError: 지원하지 않는 API인 경우
|
||||
Exception: API 호출 실패 시
|
||||
"""
|
||||
if api_name is None:
|
||||
api_name = self.default_api
|
||||
|
||||
if api_name not in self.apis:
|
||||
available_apis = list(self.apis.keys())
|
||||
raise ValueError(f"지원하지 않는 API: {api_name}. 사용 가능한 API: {available_apis}")
|
||||
|
||||
api_info = self.apis[api_name]
|
||||
logger.info(f"이미지 생성 요청 - API: {api_info['name']}")
|
||||
|
||||
try:
|
||||
# API별 핸들러 호출
|
||||
result = await api_info['handler'](data)
|
||||
logger.info(f"이미지 생성 성공 - API: {api_name}")
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"이미지 생성 실패 - API: {api_name}, 오류: {exc}")
|
||||
raise
|
||||
|
||||
async def _call_imagen_api(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Imagen API 호출 (기존 로직 재사용)
|
||||
"""
|
||||
return await call_image_generation_api(data)
|
||||
|
||||
def get_available_apis(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
사용 가능한 API 목록과 각 API의 정보 반환
|
||||
|
||||
Returns:
|
||||
Dict: API 이름을 키로 하는 API 정보 딕셔너리
|
||||
"""
|
||||
return {
|
||||
name: {
|
||||
'name': info['name'],
|
||||
'description': info['description'],
|
||||
'supported_settings': info['supported_settings']
|
||||
}
|
||||
for name, info in self.apis.items()
|
||||
}
|
||||
|
||||
def validate_settings(self, settings: Dict[str, Any], api_name: Optional[str] = None) -> bool:
|
||||
"""
|
||||
API별 설정값 검증
|
||||
|
||||
Args:
|
||||
settings: 검증할 설정값들
|
||||
api_name: 대상 API 이름
|
||||
|
||||
Returns:
|
||||
bool: 설정값이 유효한지 여부
|
||||
"""
|
||||
if api_name is None:
|
||||
api_name = self.default_api
|
||||
|
||||
if api_name not in self.apis:
|
||||
return False
|
||||
|
||||
supported = self.apis[api_name]['supported_settings']
|
||||
|
||||
# model_type 검증
|
||||
if 'model_type' in settings:
|
||||
if settings['model_type'] not in supported['model_type']:
|
||||
logger.warning(f"지원하지 않는 model_type: {settings['model_type']}")
|
||||
return False
|
||||
|
||||
# index_type 검증
|
||||
if 'index_type' in settings:
|
||||
if settings['index_type'] not in supported['index_type']:
|
||||
logger.warning(f"지원하지 않는 index_type: {settings['index_type']}")
|
||||
return False
|
||||
|
||||
# search_num 검증
|
||||
if 'search_num' in settings:
|
||||
num = settings['search_num']
|
||||
if not (supported['search_num']['min'] <= num <= supported['search_num']['max']):
|
||||
logger.warning(f"search_num 범위 초과: {num}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def set_default_api(self, api_name: str) -> None:
|
||||
"""
|
||||
기본 사용 API 설정
|
||||
|
||||
Args:
|
||||
api_name: 기본으로 사용할 API 이름
|
||||
"""
|
||||
if api_name in self.apis:
|
||||
self.default_api = api_name
|
||||
logger.info(f"기본 API 변경: {api_name}")
|
||||
else:
|
||||
raise ValueError(f"존재하지 않는 API: {api_name}")
|
||||
|
||||
# 전역 API 관리자 인스턴스
|
||||
api_manager = ApiManager()
|
||||
73
src/services/image_api_service.py
Normal file
73
src/services/image_api_service.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: image_api_service.py
|
||||
@Date: 2025-08-01
|
||||
@Author: SGM
|
||||
@Brief: 외부 이미지 API 서비스
|
||||
@section MODIFYINFO 수정정보
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import logging
|
||||
from common.settings import API_TIMEOUT_SECONDS
|
||||
import common.settings as settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def call_image_generation_api(data: dict):
|
||||
"""
|
||||
외부 이미지 생성 API 호출
|
||||
|
||||
Args:
|
||||
data (dict): API 요청 데이터
|
||||
|
||||
Returns:
|
||||
dict: API 응답 데이터
|
||||
|
||||
Raises:
|
||||
httpx.HTTPStatusError: HTTP 에러 발생 시
|
||||
httpx.RequestError: 네트워크 연결 오류 시
|
||||
"""
|
||||
timeout_config = httpx.Timeout(API_TIMEOUT_SECONDS, connect=10.0) # 설정 파일에서 타임아웃 시간 가져옴
|
||||
|
||||
logger.info(f"외부 API 호출 시작: {settings.EXTERNAL_API_URL}")
|
||||
logger.info(f"외부 API로 전송할 데이터: {data}")
|
||||
logger.info(f"search_num 전달 확인: {data.get('search_num', 'NOT_FOUND')}")
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout_config) as client:
|
||||
try:
|
||||
response = await client.post(settings.EXTERNAL_API_URL, json=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"요청한 이미지 개수: {data.get('search_num', 'N/A')}")
|
||||
logger.info(f"실제 응답받은 이미지 개수: {vector_count}")
|
||||
|
||||
if vector_count != data.get('search_num', 0):
|
||||
logger.warning(f"이미지 개수 불일치! 요청: {data.get('search_num')}, 응답: {vector_count}")
|
||||
|
||||
# 응답에 제한 정보 추가 (프론트엔드에서 사용자에게 알림)
|
||||
result['_server_limitation'] = {
|
||||
'requested': data.get('search_num', 0),
|
||||
'actual': vector_count,
|
||||
'message': f"외부 API 서버에서 최대 {vector_count}개까지만 반환합니다."
|
||||
}
|
||||
|
||||
# 응답 구조 상세 분석
|
||||
if result['vectorResult']:
|
||||
logger.info(f"첫 번째 이미지 구조: {list(result['vectorResult'][0].keys()) if result['vectorResult'][0] else 'N/A'}")
|
||||
else:
|
||||
logger.warning("응답에 vectorResult가 없습니다")
|
||||
logger.info(f"전체 응답 구조: {list(result.keys())}")
|
||||
|
||||
return result
|
||||
|
||||
except httpx.TimeoutException as exc:
|
||||
logger.error(f"API 타임아웃: {exc}")
|
||||
raise httpx.RequestError(f"API 응답 시간 초과 ({API_TIMEOUT_SECONDS}초)") from exc
|
||||
@@ -1,16 +1,26 @@
|
||||
/*
|
||||
@File: const.js
|
||||
@Date: 2025-01-16
|
||||
@author: A2TEC
|
||||
@brief: G 웹 서버
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2025-01-16/ksy : base
|
||||
*/
|
||||
* @File: const.js
|
||||
* @Date: 2025-08-01
|
||||
* @Author: SGM
|
||||
* @Brief: 상수 관리
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
const inputTxtBox1 = $('#input_txt_box_1')
|
||||
const inputTxtBox2 = $('#input_txt_box_2')
|
||||
const inputTxtBox3 = $('#input_txt_box_3')
|
||||
const inputTxtBox4 = $('#input_txt_box_4')
|
||||
const errorZone1 = $('#error_zone_1')
|
||||
const errorZone2 = $('#error_zone_2')
|
||||
const SELECTORS = {
|
||||
PROMPT_INPUT: '#input_txt_box',
|
||||
MODEL_TYPE_SELECT: '#model_type_select',
|
||||
INDEX_TYPE_SELECT: '#index_type_select',
|
||||
SEARCH_NUM_INPUT: '#search_num_input',
|
||||
ERROR_ZONE: '#error_zone',
|
||||
LOADING_ZONE: '#loading_zone',
|
||||
QUERY_IMAGE_ZONE: '#query_image_zone',
|
||||
RESULT_IMAGE_ZONE: '#result_image_zone',
|
||||
JSON_VIEWER: '#json_viewer',
|
||||
GENERATOR_BTN: '#generator_btn',
|
||||
};
|
||||
|
||||
const API_ENDPOINTS = {
|
||||
CREATE: '/create'
|
||||
};
|
||||
|
||||
const BASE64_PREFIX = 'data:image/png;base64,';
|
||||
|
||||
@@ -8,20 +8,20 @@
|
||||
|
||||
$(document).ready(function() {
|
||||
// 모든 클래스가 로드되었는지 확인
|
||||
if (typeof MainController === 'undefined') {
|
||||
console.error('MainController가 로드되지 않았습니다.');
|
||||
if (typeof ImageGeneratorController === 'undefined') {
|
||||
console.error('ImageGeneratorController가 로드되지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 메인 컨트롤러 인스턴스 생성
|
||||
window.imageGenerator = new MainController();
|
||||
window.imageGenerator = new ImageGeneratorController();
|
||||
|
||||
// API 설정 관리자 초기화
|
||||
if (typeof ApiSettings !== 'undefined') {
|
||||
ApiSettings.init();
|
||||
console.log('API Settings 초기화 완료');
|
||||
if (typeof ApiConfigManager !== 'undefined') {
|
||||
ApiConfigManager.init();
|
||||
console.log('API Config Manager 초기화 완료');
|
||||
} else {
|
||||
console.warn('ApiSettings가 로드되지 않았습니다.');
|
||||
console.warn('ApiConfigManager가 로드되지 않았습니다.');
|
||||
}
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
|
||||
159
src/static/js/modules/apiClient.js
Normal file
159
src/static/js/modules/apiClient.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* @File: apiClient.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: SGM
|
||||
* @Brief: API 통신 클라이언트 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class ApiClient {
|
||||
constructor() {
|
||||
this.baseUrl = ''; // 현재 호스트 사용
|
||||
this.defaultTimeout = 600000; // 600초 (10분) - 백엔드와 동일하게 설정
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 생성 API 호출
|
||||
*/
|
||||
async generateImage(data, apiName = null) {
|
||||
const endpoint = apiName ? `/create/${apiName}` : API_ENDPOINTS.CREATE;
|
||||
|
||||
console.log('ApiClient.generateImage 호출:', { data, apiName, endpoint });
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest('POST', endpoint, data);
|
||||
console.log('ApiClient 응답 성공:', response);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('ApiClient 오류:', error);
|
||||
throw this.handleApiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 설정 정보 가져오기
|
||||
*/
|
||||
async getConfig() {
|
||||
try {
|
||||
const response = await this.makeRequest('GET', '/config');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('설정 로드 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 상태 확인
|
||||
*/
|
||||
async checkHealth() {
|
||||
try {
|
||||
const response = await this.makeRequest('GET', '/health');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('헬스체크 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 연결 상태 확인
|
||||
*/
|
||||
async checkApiStatus() {
|
||||
try {
|
||||
const response = await this.makeRequest('GET', '/api-status');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('API 상태 확인 실패:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 요청 실행
|
||||
*/
|
||||
async makeRequest(method, endpoint, data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ajaxOptions = {
|
||||
url: this.baseUrl + endpoint,
|
||||
type: method,
|
||||
timeout: this.defaultTimeout,
|
||||
success: (response, textStatus, jqXHR) => {
|
||||
resolve(response);
|
||||
},
|
||||
error: (jqXHR, textStatus, errorThrown) => {
|
||||
const error = this.parseError(jqXHR, textStatus, errorThrown);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT')) {
|
||||
ajaxOptions.contentType = 'application/json';
|
||||
ajaxOptions.data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
$.ajax(ajaxOptions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 응답 파싱
|
||||
*/
|
||||
parseError(jqXHR, textStatus, errorThrown) {
|
||||
let errorInfo = {
|
||||
status: jqXHR.status,
|
||||
statusText: jqXHR.statusText,
|
||||
textStatus: textStatus,
|
||||
errorThrown: errorThrown,
|
||||
response: null
|
||||
};
|
||||
|
||||
try {
|
||||
if (jqXHR.responseText) {
|
||||
errorInfo.response = JSON.parse(jqXHR.responseText);
|
||||
}
|
||||
} catch (e) {
|
||||
errorInfo.response = { detail: jqXHR.responseText };
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 오류 처리
|
||||
*/
|
||||
handleApiError(error) {
|
||||
// 구체적인 오류 정보 생성
|
||||
const processedError = {
|
||||
message: '알 수 없는 오류가 발생했습니다.',
|
||||
type: 'unknown',
|
||||
status: error.status,
|
||||
response: error.response
|
||||
};
|
||||
|
||||
if (error.textStatus === 'timeout') {
|
||||
processedError.message = '요청 시간이 초과되었습니다. 잠시 후 다시 시도해주세요.';
|
||||
processedError.type = 'timeout';
|
||||
} else if (error.textStatus === 'error') {
|
||||
if (error.status === 0) {
|
||||
processedError.message = '서버에 연결할 수 없습니다. 네트워크 연결을 확인해주세요.';
|
||||
processedError.type = 'network';
|
||||
} else if (error.response && error.response.detail) {
|
||||
processedError.message = error.response.detail;
|
||||
processedError.type = error.response.error_type || 'server_error';
|
||||
}
|
||||
}
|
||||
|
||||
return processedError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 타임아웃 설정
|
||||
*/
|
||||
setTimeout(timeout) {
|
||||
this.defaultTimeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 노출
|
||||
window.ApiClient = ApiClient;
|
||||
373
src/static/js/modules/apiConfigManager.js
Normal file
373
src/static/js/modules/apiConfigManager.js
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* @File: apiConfigManager.js
|
||||
* @Date: 2025-08-06
|
||||
* @Brief: API 엔드포인트 설정 관리 모듈
|
||||
*/
|
||||
|
||||
const ApiConfigManager = {
|
||||
endpoints: [],
|
||||
currentUrl: '',
|
||||
connectionStatus: 'checking', // 'connected', 'disconnected', 'checking'
|
||||
statusCheckInterval: null,
|
||||
|
||||
async init() {
|
||||
console.log('ApiConfigManager 초기화 시작...');
|
||||
try {
|
||||
await this.loadEndpoints();
|
||||
this.setupEventListeners();
|
||||
this.updateUI();
|
||||
this.startConnectionMonitoring();
|
||||
console.log('ApiConfigManager 초기화 완료');
|
||||
} catch (error) {
|
||||
console.error('API Config Manager 초기화 실패:', error);
|
||||
this.showMessage('API 설정 로드 실패: ' + error.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadEndpoints() {
|
||||
console.log('API 엔드포인트 로드 시작...');
|
||||
try {
|
||||
const response = await fetch('/api-endpoints');
|
||||
console.log('API 엔드포인트 응답 상태:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('API 엔드포인트 응답 데이터:', data);
|
||||
|
||||
this.endpoints = data.predefined_endpoints || [];
|
||||
this.currentUrl = data.current_url || '';
|
||||
|
||||
console.log('로드된 엔드포인트 개수:', this.endpoints.length);
|
||||
console.log('현재 URL:', this.currentUrl);
|
||||
} catch (error) {
|
||||
console.error('API 엔드포인트 로드 실패:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setupEventListeners() {
|
||||
console.log('이벤트 리스너 설정 시작...');
|
||||
|
||||
// 설정 토글 (전체 헤더 클릭 가능)
|
||||
const configHeader = document.querySelector('.config_header');
|
||||
const toggleBtn = document.getElementById('toggle_config_btn');
|
||||
const configContent = document.getElementById('config_content');
|
||||
|
||||
console.log('설정 헤더:', configHeader);
|
||||
console.log('토글 버튼:', toggleBtn);
|
||||
console.log('설정 컨텐츠:', configContent);
|
||||
|
||||
if (configHeader && configContent && toggleBtn) {
|
||||
const toggleConfig = () => {
|
||||
console.log('설정 패널 토글됨');
|
||||
const isVisible = configContent.style.display !== 'none';
|
||||
configContent.style.display = isVisible ? 'none' : 'block';
|
||||
toggleBtn.textContent = isVisible ? '▼' : '▲';
|
||||
console.log('패널 상태 변경:', isVisible ? '숨김' : '표시');
|
||||
};
|
||||
|
||||
configHeader.addEventListener('click', toggleConfig);
|
||||
console.log('설정 헤더 클릭 이벤트 리스너 등록 완료');
|
||||
} else {
|
||||
console.error('설정 헤더, 토글 버튼 또는 설정 컨텐츠를 찾을 수 없음');
|
||||
}
|
||||
|
||||
// 커스텀 URL 설정 버튼
|
||||
const setCustomBtn = document.getElementById('set_custom_url_btn');
|
||||
const customInput = document.getElementById('custom_url_input');
|
||||
|
||||
console.log('커스텀 버튼:', setCustomBtn);
|
||||
console.log('커스텀 입력:', customInput);
|
||||
|
||||
if (setCustomBtn && customInput) {
|
||||
setCustomBtn.addEventListener('click', () => {
|
||||
console.log('커스텀 URL 버튼 클릭됨');
|
||||
const customUrl = customInput.value.trim();
|
||||
if (customUrl) {
|
||||
this.changeApiUrl(customUrl);
|
||||
}
|
||||
});
|
||||
|
||||
// Enter 키로도 설정 가능
|
||||
customInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
console.log('Enter 키 눌림');
|
||||
const customUrl = customInput.value.trim();
|
||||
if (customUrl) {
|
||||
this.changeApiUrl(customUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('커스텀 URL 이벤트 리스너 등록 완료');
|
||||
} else {
|
||||
console.error('커스텀 URL 요소들을 찾을 수 없음');
|
||||
}
|
||||
|
||||
// 새로고침 버튼
|
||||
const refreshBtn = document.getElementById('refresh_connection_btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
console.log('연결 상태 새로고침 버튼 클릭됨');
|
||||
this.checkConnectionStatus(true);
|
||||
});
|
||||
console.log('새로고침 버튼 이벤트 리스너 등록 완료');
|
||||
} else {
|
||||
console.error('새로고침 버튼을 찾을 수 없음');
|
||||
}
|
||||
},
|
||||
|
||||
updateUI() {
|
||||
this.updateCurrentUrlDisplay();
|
||||
this.createEndpointButtons();
|
||||
this.updateConnectionStatusUI();
|
||||
},
|
||||
|
||||
updateCurrentUrlDisplay() {
|
||||
const currentUrlElement = document.getElementById('current_api_url');
|
||||
if (currentUrlElement) {
|
||||
currentUrlElement.textContent = this.currentUrl || '알 수 없음';
|
||||
}
|
||||
},
|
||||
|
||||
createEndpointButtons() {
|
||||
const buttonsContainer = document.getElementById('endpoint_buttons');
|
||||
if (!buttonsContainer) return;
|
||||
|
||||
buttonsContainer.innerHTML = '';
|
||||
|
||||
this.endpoints.forEach((endpoint, index) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'endpoint_btn';
|
||||
button.textContent = endpoint.name;
|
||||
button.title = endpoint.description;
|
||||
|
||||
// 현재 URL과 일치하면 active 클래스 추가
|
||||
if (endpoint.url === this.currentUrl) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.changeApiUrl(endpoint.url);
|
||||
});
|
||||
|
||||
buttonsContainer.appendChild(button);
|
||||
});
|
||||
},
|
||||
|
||||
async changeApiUrl(newUrl) {
|
||||
if (!newUrl) {
|
||||
alert('URL을 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) {
|
||||
alert('올바른 URL 형식이 아닙니다. http:// 또는 https://로 시작해야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/change-api-url', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ url: newUrl })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.currentUrl = newUrl;
|
||||
this.updateCurrentUrlDisplay();
|
||||
this.updateButtonStates();
|
||||
|
||||
// 커스텀 URL 입력 필드 초기화
|
||||
const customInput = document.getElementById('custom_url_input');
|
||||
if (customInput) {
|
||||
customInput.value = '';
|
||||
}
|
||||
|
||||
// 연결 상태 즉시 확인
|
||||
this.checkConnectionStatus(true);
|
||||
|
||||
// 성공 메시지 표시 (선택적)
|
||||
this.showMessage(`API URL이 변경되었습니다: ${newUrl}`, 'success');
|
||||
|
||||
console.log('API URL 변경 성공:', data);
|
||||
} else {
|
||||
throw new Error(data.detail || 'API URL 변경 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API URL 변경 오류:', error);
|
||||
this.showMessage(`API URL 변경 실패: ${error.message}`, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
updateButtonStates() {
|
||||
const buttons = document.querySelectorAll('.endpoint_btn');
|
||||
buttons.forEach(button => {
|
||||
const endpoint = this.endpoints.find(ep => ep.name === button.textContent);
|
||||
if (endpoint && endpoint.url === this.currentUrl) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showMessage(message, type = 'info') {
|
||||
// 간단한 메시지 표시 (기존 error_zone 활용)
|
||||
const errorZone = document.getElementById('error_zone');
|
||||
if (errorZone) {
|
||||
errorZone.textContent = message;
|
||||
errorZone.style.color = type === 'error' ? 'red' :
|
||||
type === 'success' ? 'green' : 'blue';
|
||||
|
||||
// 3초 후 메시지 제거
|
||||
setTimeout(() => {
|
||||
errorZone.textContent = '';
|
||||
}, 3000);
|
||||
} else {
|
||||
// error_zone이 없으면 alert 사용
|
||||
alert(message);
|
||||
}
|
||||
},
|
||||
|
||||
// 연결 상태 모니터링 시작
|
||||
startConnectionMonitoring() {
|
||||
console.log('연결 상태 모니터링 시작');
|
||||
// 초기 상태 확인
|
||||
this.checkConnectionStatus();
|
||||
|
||||
// 30초마다 상태 확인
|
||||
this.statusCheckInterval = setInterval(() => {
|
||||
this.checkConnectionStatus();
|
||||
}, 30000);
|
||||
},
|
||||
|
||||
// 연결 상태 확인
|
||||
async checkConnectionStatus(showAnimation = false) {
|
||||
console.log('API 연결 상태 확인 중...');
|
||||
|
||||
// 확인 중 상태로 설정
|
||||
this.setConnectionStatus('checking');
|
||||
|
||||
// 새로고침 버튼 애니메이션
|
||||
if (showAnimation) {
|
||||
const refreshBtn = document.getElementById('refresh_connection_btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.classList.add('spinning');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api-status', {
|
||||
method: 'GET',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.status === 'connected') {
|
||||
this.setConnectionStatus('connected');
|
||||
console.log('API 연결 상태: 정상');
|
||||
} else {
|
||||
this.setConnectionStatus('disconnected');
|
||||
console.log('API 연결 상태: 실패', data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('연결 상태 확인 실패:', error);
|
||||
this.setConnectionStatus('disconnected');
|
||||
} finally {
|
||||
// 새로고침 버튼 애니메이션 제거
|
||||
if (showAnimation) {
|
||||
setTimeout(() => {
|
||||
const refreshBtn = document.getElementById('refresh_connection_btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.classList.remove('spinning');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 연결 상태 설정
|
||||
setConnectionStatus(status) {
|
||||
const previousStatus = this.connectionStatus;
|
||||
this.connectionStatus = status;
|
||||
|
||||
// UI 업데이트
|
||||
this.updateConnectionStatusUI();
|
||||
|
||||
// 상태 변경 시 플래시 효과
|
||||
if (previousStatus !== status && previousStatus !== 'checking') {
|
||||
this.flashStatusChange(status);
|
||||
}
|
||||
},
|
||||
|
||||
// 연결 상태 UI 업데이트
|
||||
updateConnectionStatusUI() {
|
||||
const statusDot = document.getElementById('status_dot');
|
||||
const statusText = document.getElementById('connection_status_text');
|
||||
|
||||
if (!statusDot || !statusText) return;
|
||||
|
||||
// 기존 클래스 제거
|
||||
statusDot.className = 'status_dot';
|
||||
statusText.className = '';
|
||||
|
||||
// 상태별 클래스 및 텍스트 설정
|
||||
switch (this.connectionStatus) {
|
||||
case 'connected':
|
||||
statusDot.classList.add('connected');
|
||||
statusText.classList.add('connected');
|
||||
statusText.textContent = '연결됨';
|
||||
break;
|
||||
case 'disconnected':
|
||||
statusDot.classList.add('disconnected');
|
||||
statusText.classList.add('disconnected');
|
||||
statusText.textContent = '연결 실패';
|
||||
break;
|
||||
case 'checking':
|
||||
default:
|
||||
statusDot.classList.add('checking');
|
||||
statusText.classList.add('checking');
|
||||
statusText.textContent = '연결 확인 중...';
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// 상태 변경 플래시 효과
|
||||
flashStatusChange(newStatus) {
|
||||
const statusDot = document.getElementById('status_dot');
|
||||
if (!statusDot) return;
|
||||
|
||||
const flashClass = newStatus === 'connected' ? 'flash-success' : 'flash-error';
|
||||
|
||||
statusDot.classList.add(flashClass);
|
||||
setTimeout(() => {
|
||||
statusDot.classList.remove(flashClass);
|
||||
}, 600);
|
||||
},
|
||||
|
||||
// 정리 함수 (페이지 언로드시 호출)
|
||||
cleanup() {
|
||||
if (this.statusCheckInterval) {
|
||||
clearInterval(this.statusCheckInterval);
|
||||
this.statusCheckInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 페이지 언로드시 정리
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (window.ApiConfigManager) {
|
||||
window.ApiConfigManager.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
// 전역에서 사용할 수 있도록 window 객체에 추가
|
||||
window.ApiConfigManager = ApiConfigManager;
|
||||
213
src/static/js/modules/imageGenerator.js
Normal file
213
src/static/js/modules/imageGenerator.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* @File: imageGenerator.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: SGM
|
||||
* @Brief: 이미지 생성 메인 컨트롤러 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class ImageGeneratorController {
|
||||
constructor() {
|
||||
this.isGenerating = false;
|
||||
this.apiClient = new ApiClient();
|
||||
this.uiManager = new UiManager();
|
||||
|
||||
// 상태 관리자 연결
|
||||
if (window.appState) {
|
||||
this.stateManager = window.appState;
|
||||
this.setupStateSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경 구독 설정
|
||||
*/
|
||||
setupStateSubscription() {
|
||||
this.stateManager.subscribe((newState, prevState) => {
|
||||
// 로딩 상태 변경 시 UI 업데이트
|
||||
if (newState.isLoading !== prevState.isLoading) {
|
||||
this.uiManager.toggleLoading(newState.isLoading);
|
||||
}
|
||||
|
||||
// 결과 상태 변경 시 UI 업데이트
|
||||
if (newState.lastResult !== prevState.lastResult) {
|
||||
this.displayResults(newState.lastResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 생성 메인 함수
|
||||
*/
|
||||
async generateImage() {
|
||||
console.log('=== generateImage 시작 ===');
|
||||
|
||||
if (this.isGenerating) {
|
||||
console.warn('이미지 생성이 이미 진행 중입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 입력값 수집 및 검증
|
||||
const inputData = this.collectInputData();
|
||||
if (!this.validateInput(inputData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isGenerating = true;
|
||||
|
||||
// 상태 관리자 업데이트
|
||||
if (this.stateManager) {
|
||||
this.stateManager.setLoading(true);
|
||||
this.stateManager.clearResults();
|
||||
this.stateManager.updateSettings(inputData);
|
||||
}
|
||||
|
||||
// UI 초기화
|
||||
this.uiManager.clearResults();
|
||||
this.uiManager.showLoading();
|
||||
|
||||
// API 호출
|
||||
console.log('API 호출 시작:', inputData);
|
||||
const result = await this.apiClient.generateImage(inputData);
|
||||
console.log('API 호출 성공:', result);
|
||||
|
||||
// 결과 처리
|
||||
this.handleSuccess(result);
|
||||
|
||||
} catch (error) {
|
||||
console.error('generateImage 오류:', error);
|
||||
this.handleError(error);
|
||||
} finally {
|
||||
this.isGenerating = false;
|
||||
this.uiManager.hideLoading();
|
||||
|
||||
if (this.stateManager) {
|
||||
this.stateManager.setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 데이터 수집
|
||||
*/
|
||||
collectInputData() {
|
||||
return {
|
||||
prompt: $(SELECTORS.PROMPT_INPUT).val().trim(),
|
||||
modelType: $(SELECTORS.MODEL_TYPE_SELECT).val(),
|
||||
indexType: $(SELECTORS.INDEX_TYPE_SELECT).val(),
|
||||
searchNum: parseInt($(SELECTORS.SEARCH_NUM_INPUT).val(), 10),
|
||||
querySend: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력값 검증
|
||||
*/
|
||||
validateInput(data) {
|
||||
if (!data.prompt) {
|
||||
this.uiManager.displayError('프롬프트를 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNaN(data.searchNum) || data.searchNum < 1 || data.searchNum > 10) {
|
||||
this.uiManager.displayError('검색 개수는 1-10 사이의 숫자여야 합니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 응답 처리
|
||||
*/
|
||||
handleSuccess(result) {
|
||||
console.log('이미지 생성 성공:', result);
|
||||
|
||||
// 상태 관리자 업데이트
|
||||
if (this.stateManager) {
|
||||
this.stateManager.setResult(result);
|
||||
}
|
||||
|
||||
// UI 업데이트
|
||||
this.displayResults(result);
|
||||
|
||||
// 서버 제한 알림 (백엔드에서 추가한 제한 정보)
|
||||
if (result._server_limitation) {
|
||||
this.uiManager.displayWarning(result._server_limitation.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 처리
|
||||
*/
|
||||
handleError(error) {
|
||||
console.error('이미지 생성 오류:', error);
|
||||
|
||||
let errorMessage = '이미지 생성 중 오류가 발생했습니다.';
|
||||
|
||||
if (error.response) {
|
||||
// HTTP 오류
|
||||
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.uiManager.displayError(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 표시
|
||||
*/
|
||||
displayResults(result) {
|
||||
if (!result || !result.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 쿼리 이미지 표시
|
||||
if (result.queryImage) {
|
||||
this.uiManager.displayQueryImage(result.queryImage);
|
||||
}
|
||||
|
||||
// 결과 이미지들 표시
|
||||
if (result.vectorResult && result.vectorResult.length > 0) {
|
||||
this.uiManager.displayResultImages(result.vectorResult);
|
||||
}
|
||||
|
||||
// JSON 뷰어 표시
|
||||
this.uiManager.displayJson(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 프리셋 적용 (향후 확장용)
|
||||
*/
|
||||
applyPreset(presetName) {
|
||||
console.log(`프리셋 적용: ${presetName}`);
|
||||
// TODO: 프리셋 로직 구현
|
||||
}
|
||||
|
||||
/**
|
||||
* API 변경 (향후 확장용)
|
||||
*/
|
||||
changeApi(apiName) {
|
||||
console.log(`API 변경: ${apiName}`);
|
||||
if (this.stateManager) {
|
||||
this.stateManager.setCurrentApi(apiName);
|
||||
}
|
||||
// TODO: API 변경 로직 구현
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 노출
|
||||
window.ImageGeneratorController = ImageGeneratorController;
|
||||
191
src/static/js/modules/stateManager.js
Normal file
191
src/static/js/modules/stateManager.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* @File: stateManager.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: SGM
|
||||
* @Brief: 애플리케이션 상태 관리자
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class StateManager {
|
||||
constructor() {
|
||||
this.state = {
|
||||
// API 관련 상태
|
||||
currentApi: 'imagen',
|
||||
availableApis: {},
|
||||
|
||||
// UI 상태
|
||||
isLoading: false,
|
||||
currentPreset: null,
|
||||
|
||||
// 설정 상태
|
||||
settings: {
|
||||
modelType: 'l14',
|
||||
indexType: 'cos',
|
||||
searchNum: 4
|
||||
},
|
||||
|
||||
// 애플리케이션 설정
|
||||
config: null,
|
||||
|
||||
// 결과 상태
|
||||
lastResult: null,
|
||||
queryImage: null,
|
||||
resultImages: []
|
||||
};
|
||||
|
||||
this.subscribers = [];
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
* @param {Object} newState - 업데이트할 상태
|
||||
*/
|
||||
setState(newState) {
|
||||
const prevState = { ...this.state };
|
||||
this.state = { ...this.state, ...newState };
|
||||
|
||||
// 상태 변경을 구독자들에게 알림
|
||||
this.notifySubscribers(prevState, this.state);
|
||||
|
||||
console.log('State updated:', newState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 상태 반환
|
||||
* @param {string} key - 특정 상태 키 (선택사항)
|
||||
* @returns {*} 상태 값
|
||||
*/
|
||||
getState(key = null) {
|
||||
if (key) {
|
||||
return this.state[key];
|
||||
}
|
||||
return { ...this.state };
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 변경 구독
|
||||
* @param {Function} callback - 상태 변경 시 호출될 콜백
|
||||
*/
|
||||
subscribe(callback) {
|
||||
this.subscribers.push(callback);
|
||||
|
||||
// 구독 해제 함수 반환
|
||||
return () => {
|
||||
const index = this.subscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.subscribers.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 구독자들에게 상태 변경 알림
|
||||
* @param {Object} prevState - 이전 상태
|
||||
* @param {Object} newState - 새로운 상태
|
||||
*/
|
||||
notifySubscribers(prevState, newState) {
|
||||
this.subscribers.forEach(callback => {
|
||||
try {
|
||||
callback(newState, prevState);
|
||||
} catch (error) {
|
||||
console.error('Error in state subscriber:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버에서 설정 로드
|
||||
*/
|
||||
async loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/config');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.setState({
|
||||
config: data.config,
|
||||
availableApis: data.apis
|
||||
});
|
||||
this.initialized = true;
|
||||
console.log('애플리케이션 설정 로드 완료');
|
||||
} else {
|
||||
console.error('설정 로드 실패:', response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('설정 로드 중 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 변경
|
||||
* @param {string} apiName - 새로운 API 이름
|
||||
*/
|
||||
setCurrentApi(apiName) {
|
||||
if (this.state.availableApis[apiName]) {
|
||||
this.setState({ currentApi: apiName });
|
||||
console.log(`API 변경: ${apiName}`);
|
||||
} else {
|
||||
console.error(`알 수 없는 API: ${apiName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 업데이트
|
||||
* @param {Object} newSettings - 새로운 설정
|
||||
*/
|
||||
updateSettings(newSettings) {
|
||||
this.setState({
|
||||
settings: { ...this.state.settings, ...newSettings }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 상태 변경
|
||||
* @param {boolean} isLoading - 로딩 여부
|
||||
*/
|
||||
setLoading(isLoading) {
|
||||
this.setState({ isLoading });
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 데이터 설정
|
||||
* @param {Object} result - API 응답 결과
|
||||
*/
|
||||
setResult(result) {
|
||||
this.setState({
|
||||
lastResult: result,
|
||||
queryImage: result.queryImage || null,
|
||||
resultImages: result.vectorResult || []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 초기화
|
||||
*/
|
||||
clearResults() {
|
||||
this.setState({
|
||||
lastResult: null,
|
||||
queryImage: null,
|
||||
resultImages: []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 초기화 여부 확인
|
||||
* @returns {boolean} 초기화 완료 여부
|
||||
*/
|
||||
isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 상태 관리자 인스턴스
|
||||
const appState = new StateManager();
|
||||
|
||||
// 페이지 로드 시 설정 자동 로드
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
appState.loadConfig();
|
||||
});
|
||||
|
||||
// 전역으로 노출 (다른 스크립트에서 사용할 수 있도록)
|
||||
window.appState = appState;
|
||||
223
src/static/js/modules/uiManager.js
Normal file
223
src/static/js/modules/uiManager.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* @File: uiManager.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: Claude
|
||||
* @Brief: UI 관리 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class UiManager {
|
||||
constructor() {
|
||||
this.elements = this.cacheElements();
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM 요소 캐싱
|
||||
*/
|
||||
cacheElements() {
|
||||
return {
|
||||
promptInput: $(SELECTORS.PROMPT_INPUT),
|
||||
modelTypeSelect: $(SELECTORS.MODEL_TYPE_SELECT),
|
||||
indexTypeSelect: $(SELECTORS.INDEX_TYPE_SELECT),
|
||||
searchNumInput: $(SELECTORS.SEARCH_NUM_INPUT),
|
||||
errorZone: $(SELECTORS.ERROR_ZONE),
|
||||
loadingZone: $(SELECTORS.LOADING_ZONE),
|
||||
queryImageZone: $(SELECTORS.QUERY_IMAGE_ZONE),
|
||||
resultImageZone: $(SELECTORS.RESULT_IMAGE_ZONE),
|
||||
jsonViewer: $(SELECTORS.JSON_VIEWER),
|
||||
generatorBtn: $(SELECTORS.GENERATOR_BTN)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 상태 표시
|
||||
*/
|
||||
showLoading() {
|
||||
this.elements.loadingZone.show();
|
||||
this.elements.generatorBtn.prop('disabled', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 상태 숨김
|
||||
*/
|
||||
hideLoading() {
|
||||
this.elements.loadingZone.hide();
|
||||
this.elements.generatorBtn.prop('disabled', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 상태 토글
|
||||
*/
|
||||
toggleLoading(isLoading) {
|
||||
if (isLoading) {
|
||||
this.showLoading();
|
||||
} else {
|
||||
this.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 영역 초기화
|
||||
*/
|
||||
clearResults() {
|
||||
this.elements.queryImageZone.empty();
|
||||
this.elements.resultImageZone.empty();
|
||||
this.elements.jsonViewer.hide();
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 메시지 표시
|
||||
*/
|
||||
displayError(message) {
|
||||
this.elements.errorZone.text(message).show();
|
||||
console.error('UI Error:', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 경고 메시지 표시
|
||||
*/
|
||||
displayWarning(message) {
|
||||
// 경고는 에러보다 덜 강조되도록 스타일 구분
|
||||
this.elements.errorZone
|
||||
.text(message)
|
||||
.removeClass('error')
|
||||
.addClass('warning')
|
||||
.show();
|
||||
console.warn('UI Warning:', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류 메시지 제거
|
||||
*/
|
||||
clearError() {
|
||||
this.elements.errorZone.text('').hide().removeClass('warning error');
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿼리 이미지 표시
|
||||
*/
|
||||
displayQueryImage(base64Image) {
|
||||
if (!base64Image) return;
|
||||
|
||||
const img = $('<img>', {
|
||||
src: BASE64_PREFIX + base64Image,
|
||||
class: 'query_image',
|
||||
alt: '생성된 쿼리 이미지'
|
||||
});
|
||||
|
||||
this.elements.queryImageZone.empty().append(img);
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 이미지들 표시
|
||||
*/
|
||||
displayResultImages(vectorResult) {
|
||||
if (!vectorResult || vectorResult.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.elements.resultImageZone.empty();
|
||||
|
||||
vectorResult.forEach((item, index) => {
|
||||
if (item.image && item.percents !== undefined) {
|
||||
const resultBlock = this.createResultBlock(item, index);
|
||||
this.elements.resultImageZone.append(resultBlock);
|
||||
}
|
||||
});
|
||||
|
||||
// 이미지 로드 완료 후 애니메이션 효과 (선택사항)
|
||||
this.animateResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 블록 생성
|
||||
*/
|
||||
createResultBlock(item, index) {
|
||||
const block = $('<div class="result_block">');
|
||||
|
||||
const img = $('<img>', {
|
||||
src: BASE64_PREFIX + item.image,
|
||||
class: 'result_image',
|
||||
alt: `결과 이미지 ${index + 1}`,
|
||||
loading: 'lazy' // 지연 로딩
|
||||
});
|
||||
|
||||
const percent = $('<div class="result_percent">').text(
|
||||
`유사도: ${item.percents.toFixed(1)}%`
|
||||
);
|
||||
|
||||
block.append(img, percent);
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 뷰어 표시
|
||||
*/
|
||||
displayJson(data) {
|
||||
try {
|
||||
// JSON 뷰어 라이브러리 사용
|
||||
this.elements.jsonViewer.jsonViewer(data, {
|
||||
collapsed: true, // 기본적으로 접힌 상태
|
||||
withQuotes: false,
|
||||
withLinks: false
|
||||
}).show();
|
||||
} catch (error) {
|
||||
console.error('JSON 뷰어 오류:', error);
|
||||
// 라이브러리 실패 시 기본 JSON 표시
|
||||
this.elements.jsonViewer
|
||||
.text(JSON.stringify(data, null, 2))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결과 애니메이션 효과
|
||||
*/
|
||||
animateResults() {
|
||||
this.elements.resultImageZone.find('.result_block').each(function(index) {
|
||||
$(this).css('opacity', '0').delay(index * 100).animate({
|
||||
opacity: 1
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력값 초기화
|
||||
*/
|
||||
resetInputs() {
|
||||
this.elements.promptInput.val('');
|
||||
this.elements.searchNumInput.val(DEFAULT_VALUES.SEARCH_NUM);
|
||||
this.clearResults();
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정값 적용 (프리셋 등에서 사용)
|
||||
*/
|
||||
applySettings(settings) {
|
||||
if (settings.modelType) {
|
||||
this.elements.modelTypeSelect.val(settings.modelType);
|
||||
}
|
||||
if (settings.indexType) {
|
||||
this.elements.indexTypeSelect.val(settings.indexType);
|
||||
}
|
||||
if (settings.searchNum) {
|
||||
this.elements.searchNumInput.val(settings.searchNum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 설정값 가져오기
|
||||
*/
|
||||
getCurrentSettings() {
|
||||
return {
|
||||
prompt: this.elements.promptInput.val().trim(),
|
||||
modelType: this.elements.modelTypeSelect.val(),
|
||||
indexType: this.elements.indexTypeSelect.val(),
|
||||
searchNum: parseInt(this.elements.searchNumInput.val(), 10)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 전역으로 노출
|
||||
window.UiManager = UiManager;
|
||||
@@ -26,14 +26,14 @@
|
||||
<div class="title_zone">
|
||||
<span class="title">AI Image Generator</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- API 엔드포인트 설정 영역 -->
|
||||
<div class="api_config_zone">
|
||||
<div class="config_header">
|
||||
<span class="config_title">API 설정</span>
|
||||
<button id="toggle_config_btn" class="toggle_btn">▼</button>
|
||||
</div>
|
||||
<div id="config_content" class="config_content" style="display: none;">
|
||||
<div id="config_content" class="config_content" style="display: none">
|
||||
<div class="current_api_info">
|
||||
<div class="api_status_line">
|
||||
<span>현재 API: </span>
|
||||
@@ -44,7 +44,10 @@
|
||||
<div class="status_dot" id="status_dot"></div>
|
||||
<span id="connection_status_text">연결 확인 중...</span>
|
||||
</div>
|
||||
<button id="refresh_connection_btn" class="refresh_btn" title="연결 상태 새로고침">
|
||||
<button
|
||||
id="refresh_connection_btn"
|
||||
class="refresh_btn"
|
||||
title="연결 상태 새로고침">
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
@@ -57,7 +60,10 @@
|
||||
</div>
|
||||
<div class="custom_endpoint">
|
||||
<span>커스텀 URL:</span>
|
||||
<input type="text" id="custom_url_input" placeholder="http://192.168.x.x:port/api/..." />
|
||||
<input
|
||||
type="text"
|
||||
id="custom_url_input"
|
||||
placeholder="http://192.168.x.x:port/api/..." />
|
||||
<button id="set_custom_url_btn" class="config_btn">적용</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,14 +107,14 @@
|
||||
<script src="static/js/jquery.json-viewer.min.js"></script>
|
||||
|
||||
<!-- 상수 및 설정 -->
|
||||
<script src="static/js/constants.js"></script>
|
||||
<script src="static/js/const.js"></script>
|
||||
|
||||
<!-- 모듈들 (순서 중요) -->
|
||||
<script src="static/js/modules/state.js"></script>
|
||||
<script src="static/js/modules/http.js"></script>
|
||||
<script src="static/js/modules/ui.js"></script>
|
||||
<script src="static/js/modules/config.js"></script>
|
||||
<script src="static/js/modules/main.js"></script>
|
||||
<script src="static/js/modules/stateManager.js"></script>
|
||||
<script src="static/js/modules/apiClient.js"></script>
|
||||
<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/init.js"></script>
|
||||
|
||||
@@ -17,9 +17,9 @@ from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from common.config import SERVICE_PORT, HOST, API_TIMEOUT_SECONDS, PREDEFINED_ENDPOINTS
|
||||
import common.config as settings
|
||||
from services.manager import api_manager
|
||||
from common.settings import SERVICE_PORT, HOST, API_TIMEOUT_SECONDS, PREDEFINED_ENDPOINTS
|
||||
import common.settings as settings
|
||||
from services.api_manager import api_manager
|
||||
from config.app_config import app_config
|
||||
|
||||
# 로깅 설정
|
||||
|
||||
Reference in New Issue
Block a user