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 = $('