From 43a860809549b1b862c1bbd2f7ab6c47f8ff8f35 Mon Sep 17 00:00:00 2001 From: rudals252 Date: Mon, 22 Sep 2025 11:18:04 +0900 Subject: [PATCH] =?UTF-8?q?ip=EC=A3=BC=EC=86=8C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 63 +++- src/common/settings.py | 89 ++++++ src/config/app_config.py | 2 +- src/services/api_manager.py | 156 +++++++++ src/services/image_api_service.py | 73 +++++ src/static/js/const.js | 38 ++- src/static/js/init.js | 14 +- src/static/js/modules/apiClient.js | 159 +++++++++ src/static/js/modules/apiConfigManager.js | 373 ++++++++++++++++++++++ src/static/js/modules/imageGenerator.js | 213 ++++++++++++ src/static/js/modules/stateManager.js | 191 +++++++++++ src/static/js/modules/uiManager.js | 223 +++++++++++++ src/templates/index.html | 26 +- src/web_server.py | 6 +- 14 files changed, 1580 insertions(+), 46 deletions(-) create mode 100644 src/common/settings.py create mode 100644 src/services/api_manager.py create mode 100644 src/services/image_api_service.py create mode 100644 src/static/js/modules/apiClient.js create mode 100644 src/static/js/modules/apiConfigManager.js create mode 100644 src/static/js/modules/imageGenerator.js create mode 100644 src/static/js/modules/stateManager.js create mode 100644 src/static/js/modules/uiManager.js diff --git a/README.md b/README.md index a54c00c..ecbfefd 100755 --- a/README.md +++ b/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) 콘솔을 확인하거나 서버 로그를 점검해주세요. diff --git a/src/common/settings.py b/src/common/settings.py new file mode 100644 index 0000000..5fdcbed --- /dev/null +++ b/src/common/settings.py @@ -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 서버 연결 확인 +""" diff --git a/src/config/app_config.py b/src/config/app_config.py index 367210b..8cb43ea 100644 --- a/src/config/app_config.py +++ b/src/config/app_config.py @@ -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 ) diff --git a/src/services/api_manager.py b/src/services/api_manager.py new file mode 100644 index 0000000..5cf22fd --- /dev/null +++ b/src/services/api_manager.py @@ -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() \ No newline at end of file diff --git a/src/services/image_api_service.py b/src/services/image_api_service.py new file mode 100644 index 0000000..5f18556 --- /dev/null +++ b/src/services/image_api_service.py @@ -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 diff --git a/src/static/js/const.js b/src/static/js/const.js index 952561f..40083a6 100755 --- a/src/static/js/const.js +++ b/src/static/js/const.js @@ -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') \ No newline at end of file +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,'; diff --git a/src/static/js/init.js b/src/static/js/init.js index 2d6bc66..a689b8d 100644 --- a/src/static/js/init.js +++ b/src/static/js/init.js @@ -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가 로드되지 않았습니다.'); } // 이벤트 리스너 설정 diff --git a/src/static/js/modules/apiClient.js b/src/static/js/modules/apiClient.js new file mode 100644 index 0000000..8a0bfc5 --- /dev/null +++ b/src/static/js/modules/apiClient.js @@ -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; \ No newline at end of file diff --git a/src/static/js/modules/apiConfigManager.js b/src/static/js/modules/apiConfigManager.js new file mode 100644 index 0000000..93d74c9 --- /dev/null +++ b/src/static/js/modules/apiConfigManager.js @@ -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; \ No newline at end of file diff --git a/src/static/js/modules/imageGenerator.js b/src/static/js/modules/imageGenerator.js new file mode 100644 index 0000000..306e763 --- /dev/null +++ b/src/static/js/modules/imageGenerator.js @@ -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; \ No newline at end of file diff --git a/src/static/js/modules/stateManager.js b/src/static/js/modules/stateManager.js new file mode 100644 index 0000000..99edec5 --- /dev/null +++ b/src/static/js/modules/stateManager.js @@ -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; \ No newline at end of file diff --git a/src/static/js/modules/uiManager.js b/src/static/js/modules/uiManager.js new file mode 100644 index 0000000..8ab4750 --- /dev/null +++ b/src/static/js/modules/uiManager.js @@ -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 = $('', { + 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 = $('
'); + + const img = $('', { + src: BASE64_PREFIX + item.image, + class: 'result_image', + alt: `결과 이미지 ${index + 1}`, + loading: 'lazy' // 지연 로딩 + }); + + const 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; \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html index 8c6212d..8201a6f 100755 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -26,14 +26,14 @@
AI Image Generator
- +
API 설정
- @@ -101,14 +107,14 @@ - + - - - - - + + + + + diff --git a/src/web_server.py b/src/web_server.py index 4adbfdc..a656d75 100755 --- a/src/web_server.py +++ b/src/web_server.py @@ -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 # 로깅 설정