ip주소 추가 및 파일 변경

This commit is contained in:
rudals252
2025-09-22 11:18:04 +09:00
parent 4b5a170e45
commit 43a8608095
14 changed files with 1580 additions and 46 deletions

View File

@@ -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
View 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 서버 연결 확인
"""

View File

@@ -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
View 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()

View 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

View File

@@ -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,';

View File

@@ -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가 로드되지 않았습니다.');
}
// 이벤트 리스너 설정

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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>

View File

@@ -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
# 로깅 설정