Update project files
This commit is contained in:
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
140
README.md
140
README.md
@@ -1,3 +1,137 @@
|
||||
# AI IMAGE GENERATOR WEB MONITOR
|
||||
|
||||
AI 이미지 생성 웹 모니터
|
||||
# AI Image Generator Web Monitor
|
||||
|
||||
AI 이미지 생성 및 벡터 검색을 위한 웹 인터페이스
|
||||
|
||||
## 🛠️ 설치 및 실행
|
||||
|
||||
### 1. 환경 설정
|
||||
|
||||
```bash
|
||||
# Python 3.8+ 필요
|
||||
conda activate eyewear
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 서버 실행
|
||||
|
||||
```bash
|
||||
python src/web_server.py
|
||||
```
|
||||
|
||||
### 3. 웹 접속
|
||||
|
||||
```
|
||||
http://localhost:51003
|
||||
```
|
||||
|
||||
## ⚙️ 설정 관리
|
||||
|
||||
### API 엔드포인트 설정 (`src/common/config.py`)
|
||||
|
||||
#### 기본 API URL(DEV3)
|
||||
|
||||
```python
|
||||
EXTERNAL_API_URL = "http://192.168.200.233:52000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data"
|
||||
```
|
||||
|
||||
#### 사전 정의된 엔드포인트
|
||||
|
||||
```python
|
||||
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"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 서버 설정
|
||||
|
||||
- `SERVICE_PORT`: 웹 서버 포트 (기본: 51003)
|
||||
- `HOST`: 서버 바인딩 IP (기본: "0.0.0.0")
|
||||
- `API_TIMEOUT_SECONDS`: API 타임아웃 (기본: 600초)
|
||||
- `DEBUG_MODE`: 디버그 모드 (기본: False)
|
||||
|
||||
### 동적 API 변경 사용법
|
||||
|
||||
1. **웹페이지 상단의 "API 설정" 클릭**
|
||||
2. **사전 정의된 버튼 선택** 또는 **커스텀 URL 입력**
|
||||
3. **실시간으로 API 연결 변경됨**
|
||||
4. **현재 사용 중인 API가 표시됨**
|
||||
|
||||
## 🔧 API 엔드포인트
|
||||
|
||||
### 메인 엔드포인트
|
||||
|
||||
- `GET /` - 메인 웹 페이지
|
||||
- `POST /create` - 이미지 생성 요청
|
||||
- `GET /health` - 서버 상태 확인
|
||||
- `GET /api-status` - 외부 API 연결 상태
|
||||
|
||||
### API 설정 엔드포인트
|
||||
|
||||
- `GET /api-endpoints` - 사용 가능한 엔드포인트 목록
|
||||
- `POST /change-api-url` - API URL 동적 변경
|
||||
- `GET /config` - 프론트엔드 설정 정보
|
||||
|
||||
## 📁 프로젝트 구조
|
||||
|
||||
```
|
||||
src/
|
||||
├── common/
|
||||
│ └── config.py # 애플리케이션 설정 (구 settings.py)
|
||||
├── config/
|
||||
│ ├── __init__.py
|
||||
│ └── app_config.py # 통합 설정 관리
|
||||
├── services/
|
||||
│ ├── manager.py # API 통합 관리 (구 api_manager.py)
|
||||
│ └── client.py # 외부 API 호출 (구 image_api_service.py)
|
||||
├── static/
|
||||
│ ├── css/
|
||||
│ │ └── style.css # CSS 스타일 (CSS 변수 사용)
|
||||
│ └── js/
|
||||
│ ├── constants.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)
|
||||
├── templates/
|
||||
│ └── index.html # HTML 템플릿
|
||||
└── web_server.py # FastAPI 웹 서버
|
||||
```
|
||||
|
||||
## 🐛 문제 해결
|
||||
|
||||
### 포트 충돌
|
||||
|
||||
```bash
|
||||
# 포트 사용 확인
|
||||
netstat -an | findstr :51003
|
||||
|
||||
# config.py에서 SERVICE_PORT 변경
|
||||
SERVICE_PORT = 51004 # 다른 포트로 변경
|
||||
```
|
||||
|
||||
### API 연결 문제
|
||||
|
||||
```bash
|
||||
# API 상태 확인
|
||||
curl http://localhost:51003/api-status
|
||||
|
||||
# 또는 브라우저에서
|
||||
http://localhost:51003/api-status
|
||||
```
|
||||
|
||||
### 개발자 도구 디버깅
|
||||
|
||||
- **F12 → 콘솔 탭**: JavaScript 오류 및 API 호출 로그 확인
|
||||
- **네트워크 탭**: API 요청/응답 상세 분석
|
||||
|
||||
@@ -1,3 +1,71 @@
|
||||
fastapi==0.83.0
|
||||
uvicorn==0.16.0
|
||||
jinja2==3.0.3
|
||||
# AI Image Generator Web Server Dependencies
|
||||
#
|
||||
# 프로젝트: AI Image Generator Web Monitor v2.0
|
||||
# 개발 환경: Python 3.10.18
|
||||
# 지원 버전: Python 3.8+
|
||||
# 최소 요구: Python 3.8 (async/await, typing, f-string 지원)
|
||||
|
||||
# =============================================================================
|
||||
# 핵심 웹 프레임워크
|
||||
# =============================================================================
|
||||
|
||||
# FastAPI 웹 프레임워크 (async 지원, 자동 API 문서화)
|
||||
fastapi==0.104.1
|
||||
|
||||
# ASGI 서버 (production-ready, auto-reload 지원)
|
||||
uvicorn[standard]==0.24.0
|
||||
|
||||
# HTML 템플릿 엔진 (Jinja2)
|
||||
jinja2==3.1.2
|
||||
|
||||
# 파일 업로드 및 폼 데이터 처리
|
||||
python-multipart==0.0.6
|
||||
|
||||
# =============================================================================
|
||||
# HTTP 클라이언트 & 네트워킹
|
||||
# =============================================================================
|
||||
|
||||
# 비동기 HTTP 클라이언트 (외부 API 호출, requests의 async 버전)
|
||||
httpx==0.25.2
|
||||
|
||||
# =============================================================================
|
||||
# 개발 편의 패키지 (선택사항)
|
||||
# =============================================================================
|
||||
|
||||
# 환경변수 파일(.env) 지원 - 향후 배포시 사용 권장
|
||||
# python-dotenv==1.0.0
|
||||
|
||||
# 코드 품질 도구들 (개발 중 사용 권장)
|
||||
# flake8==6.0.0 # 코드 스타일 체크
|
||||
# black==23.0.0 # 코드 포매터
|
||||
# pytest==7.4.0 # 테스트 프레임워크
|
||||
|
||||
# =============================================================================
|
||||
# 설치 및 실행 가이드
|
||||
# =============================================================================
|
||||
#
|
||||
# 1. 환경 활성화:
|
||||
# conda activate eyewear
|
||||
#
|
||||
# 2. 패키지 설치:
|
||||
# pip install -r requirements.txt
|
||||
#
|
||||
# 3. 서버 실행:
|
||||
# python src/web_server.py
|
||||
#
|
||||
# 4. 웹 접속:
|
||||
# http://localhost:51003
|
||||
#
|
||||
# =============================================================================
|
||||
# 버전 호환성 정보
|
||||
# =============================================================================
|
||||
#
|
||||
# Python 3.8+ : 모든 기능 지원
|
||||
# Python 3.7 : typing 일부 제한, 권장하지 않음
|
||||
# Python 3.6- : 지원 안함 (f-string, async/await 제한)
|
||||
#
|
||||
# 테스트된 환경:
|
||||
# - Windows 10/11 + Python 3.10.18
|
||||
# - Conda 환경: eyewear
|
||||
#
|
||||
# =============================================================================
|
||||
85
src/common/config.py
Normal file
85
src/common/config.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: settings.py
|
||||
@Date: 2025-08-01
|
||||
@Author: SGM
|
||||
@Brief: 애플리케이션 전반의 상수 및 설정 관리
|
||||
@section MODIFYINFO 수정정보
|
||||
"""
|
||||
|
||||
# =============================================================================
|
||||
# 웹 서버 설정
|
||||
# =============================================================================
|
||||
|
||||
# WEB SERVER PORT
|
||||
# 다른 PC에서 접속할 때 포트 충돌이 있으면 변경하세요
|
||||
# 예: 51003, 51002, 8000 등
|
||||
SERVICE_PORT = 51001
|
||||
|
||||
# Uvicorn 서버 호스트 설정
|
||||
# "0.0.0.0": 모든 IP에서 접속 허용 (기본값, 권장)
|
||||
# "127.0.0.1": 로컬에서만 접속 허용
|
||||
# "192.168.x.x": 특정 IP에서만 접속 허용
|
||||
HOST = "0.0.0.0"
|
||||
|
||||
# =============================================================================
|
||||
# 외부 API 설정
|
||||
# =============================================================================
|
||||
|
||||
# AI 이미지 생성 API 서버 주소
|
||||
# 다른 PC나 서버에서 API 서버를 실행하는 경우 IP 주소를 변경하세요
|
||||
# 예: "http://192.168.1.100:51000/api/..."
|
||||
# "http://localhost:51000/api/..."
|
||||
#EXTERNAL_API_URL = "http://192.168.200.233:51000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data"
|
||||
EXTERNAL_API_URL = "http://192.168.200.232:51000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data"
|
||||
|
||||
# =============================================================================
|
||||
# 사전 정의된 API 엔드포인트 설정
|
||||
# =============================================================================
|
||||
|
||||
# 사용 가능한 API 엔드포인트 목록 (웹 UI에서 선택 가능)
|
||||
PREDEFINED_ENDPOINTS = [
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev2/Data)",
|
||||
"url": "http://192.168.200.232:51000/api/services/vactorImageSearch/vit/imageGenerate/imagen/data",
|
||||
"description": "Imagen 모델을 사용한 이미지 생성후 Vector 검색 그후 결과 이미지 데이터 return"
|
||||
},
|
||||
{
|
||||
"name": "벡터이미지 검색(Dev3/Data)",
|
||||
"url": "http://192.168.200.233:51000/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 서버 연결 확인
|
||||
"""
|
||||
5
src/config/__init__.py
Normal file
5
src/config/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Config 패키지 초기화
|
||||
"""
|
||||
188
src/config/app_config.py
Normal file
188
src/config/app_config.py
Normal file
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: app_config.py
|
||||
@Date: 2025-08-05
|
||||
@Author: SGM
|
||||
@Brief: 애플리케이션 전반의 설정을 통합 관리
|
||||
@section MODIFYINFO 수정정보
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from common.config import (
|
||||
SERVICE_PORT, HOST, EXTERNAL_API_URL,
|
||||
DEBUG_MODE, API_TIMEOUT_SECONDS
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AppConfig:
|
||||
"""
|
||||
애플리케이션 전반의 설정을 통합 관리하는 클래스
|
||||
|
||||
- 서버 설정
|
||||
- API 설정
|
||||
- UI 설정
|
||||
- 프론트엔드에서 필요한 설정 제공
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.server_config = self._load_server_config()
|
||||
self.api_config = self._load_api_config()
|
||||
self.ui_config = self._load_ui_config()
|
||||
logger.info("애플리케이션 설정 로드 완료")
|
||||
|
||||
def _load_server_config(self) -> Dict[str, Any]:
|
||||
"""서버 관련 설정"""
|
||||
return {
|
||||
'host': HOST,
|
||||
'port': SERVICE_PORT,
|
||||
'debug': DEBUG_MODE
|
||||
}
|
||||
|
||||
def _load_api_config(self) -> Dict[str, Any]:
|
||||
"""API 관련 설정"""
|
||||
return {
|
||||
'imagen': {
|
||||
'url': EXTERNAL_API_URL,
|
||||
'timeout': API_TIMEOUT_SECONDS,
|
||||
'name': 'Imagen API',
|
||||
'description': '구글 Imagen 기반 이미지 생성'
|
||||
}
|
||||
# 추후 추가될 API 설정들
|
||||
}
|
||||
|
||||
def _load_ui_config(self) -> Dict[str, Any]:
|
||||
"""UI 관련 설정"""
|
||||
return {
|
||||
'default_settings': {
|
||||
'model_type': 'l14',
|
||||
'index_type': 'cos',
|
||||
'search_num': 4
|
||||
},
|
||||
'limits': {
|
||||
'max_search_num': 10,
|
||||
'min_search_num': 1,
|
||||
'max_prompt_length': 500
|
||||
},
|
||||
'theme': {
|
||||
'primary_color': '#646464',
|
||||
'success_color': '#28a745',
|
||||
'error_color': '#dc3545'
|
||||
}
|
||||
}
|
||||
|
||||
def get_config_for_frontend(self) -> Dict[str, Any]:
|
||||
"""
|
||||
프론트엔드에서 사용할 설정만 반환
|
||||
(보안상 중요한 정보는 제외)
|
||||
|
||||
Returns:
|
||||
Dict: 프론트엔드용 설정 데이터
|
||||
"""
|
||||
return {
|
||||
'apis': {
|
||||
name: {
|
||||
'name': config['name'],
|
||||
'description': config['description']
|
||||
# URL이나 timeout 같은 민감한 정보는 제외
|
||||
}
|
||||
for name, config in self.api_config.items()
|
||||
},
|
||||
'ui': self.ui_config,
|
||||
'server': {
|
||||
'debug': self.server_config['debug']
|
||||
# host, port 같은 서버 정보는 제외
|
||||
}
|
||||
}
|
||||
|
||||
def get_api_config(self, api_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
특정 API의 설정 반환
|
||||
|
||||
Args:
|
||||
api_name: API 이름
|
||||
|
||||
Returns:
|
||||
Dict: API 설정 데이터
|
||||
|
||||
Raises:
|
||||
KeyError: 존재하지 않는 API인 경우
|
||||
"""
|
||||
if api_name not in self.api_config:
|
||||
available_apis = list(self.api_config.keys())
|
||||
raise KeyError(f"API '{api_name}' 설정을 찾을 수 없습니다. 사용 가능한 API: {available_apis}")
|
||||
|
||||
return self.api_config[api_name]
|
||||
|
||||
def update_api_config(self, api_name: str, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
API 설정 업데이트 (런타임에서 설정 변경 시 사용)
|
||||
|
||||
Args:
|
||||
api_name: API 이름
|
||||
config: 새로운 설정 데이터
|
||||
"""
|
||||
if api_name in self.api_config:
|
||||
self.api_config[api_name].update(config)
|
||||
logger.info(f"API '{api_name}' 설정 업데이트됨")
|
||||
else:
|
||||
self.api_config[api_name] = config
|
||||
logger.info(f"새로운 API '{api_name}' 설정 추가됨")
|
||||
|
||||
def validate_request_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
요청 데이터 검증 및 정리
|
||||
|
||||
Args:
|
||||
data: 클라이언트에서 온 요청 데이터
|
||||
|
||||
Returns:
|
||||
Dict: 검증된 데이터
|
||||
|
||||
Raises:
|
||||
ValueError: 유효하지 않은 데이터인 경우
|
||||
"""
|
||||
validated = {}
|
||||
|
||||
# 필수 필드 검증
|
||||
if not data.get('prompt'):
|
||||
raise ValueError("프롬프트는 필수입니다")
|
||||
|
||||
prompt = data['prompt'].strip()
|
||||
if len(prompt) > self.ui_config['limits']['max_prompt_length']:
|
||||
raise ValueError(f"프롬프트가 너무 깁니다 (최대 {self.ui_config['limits']['max_prompt_length']}자)")
|
||||
|
||||
validated['prompt'] = prompt
|
||||
|
||||
# 선택적 필드 검증 및 기본값 설정 (JavaScript camelCase -> Python snake_case 변환)
|
||||
validated['model_type'] = data.get('modelType', data.get('model_type', self.ui_config['default_settings']['model_type']))
|
||||
validated['index_type'] = data.get('indexType', data.get('index_type', self.ui_config['default_settings']['index_type']))
|
||||
|
||||
# search_num 검증 (JavaScript camelCase 지원)
|
||||
search_num = data.get('searchNum', data.get('search_num', self.ui_config['default_settings']['search_num']))
|
||||
try:
|
||||
search_num = int(search_num)
|
||||
if not (self.ui_config['limits']['min_search_num'] <= search_num <= self.ui_config['limits']['max_search_num']):
|
||||
raise ValueError(f"Search Num은 {self.ui_config['limits']['min_search_num']}~{self.ui_config['limits']['max_search_num']} 사이여야 합니다")
|
||||
|
||||
# 디버깅용 로그 추가
|
||||
logger.info(f"search_num 검증 통과: {search_num} (타입: {type(search_num)})")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"search_num 검증 실패: {search_num} (타입: {type(search_num)}), 오류: {e}")
|
||||
raise ValueError("Search Num은 숫자여야 합니다")
|
||||
|
||||
validated['search_num'] = search_num
|
||||
validated['querySend'] = data.get('querySend', True)
|
||||
|
||||
# *** 중요 수정: 외부 API는 camelCase를 기대함 ***
|
||||
# 내부적으로는 snake_case 사용하지만, 외부 API 전송 시에는 원본 형태 유지
|
||||
validated['searchNum'] = search_num # 외부 API용 camelCase 버전 추가
|
||||
validated['modelType'] = validated['model_type'] # 외부 API용
|
||||
validated['indexType'] = validated['index_type'] # 외부 API용
|
||||
|
||||
return validated
|
||||
|
||||
# 전역 설정 관리자 인스턴스
|
||||
app_config = AppConfig()
|
||||
73
src/services/client.py
Normal file
73
src/services/client.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: image_api_service.py
|
||||
@Date: 2025-08-01
|
||||
@Author: SGM
|
||||
@Brief: 외부 이미지 API 서비스
|
||||
@section MODIFYINFO 수정정보
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import logging
|
||||
from common.config import API_TIMEOUT_SECONDS
|
||||
import common.config 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
|
||||
156
src/services/manager.py
Normal file
156
src/services/manager.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
@File: api_manager.py
|
||||
@Date: 2025-08-05
|
||||
@Author: SGM
|
||||
@Brief: API 관리자 - 여러 이미지 생성 API를 통합 관리
|
||||
@section MODIFYINFO 수정정보
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from .client 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()
|
||||
57
src/static/css/jquery.json-viewer.css
Normal file
57
src/static/css/jquery.json-viewer.css
Normal file
@@ -0,0 +1,57 @@
|
||||
/* Root element */
|
||||
.json-document {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for JSON objects */
|
||||
ul.json-dict, ol.json-array {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 1px;
|
||||
border-left: 1px dotted #ccc;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.json-string {
|
||||
color: #0B7500;
|
||||
}
|
||||
.json-literal {
|
||||
color: #1A01CC;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Toggle button */
|
||||
a.json-toggle {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
a.json-toggle:before {
|
||||
font-size: 1.1em;
|
||||
color: #c0c0c0;
|
||||
content: "\25BC"; /* down arrow */
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
left: -1.2em;
|
||||
}
|
||||
a.json-toggle:hover:before {
|
||||
color: #aaa;
|
||||
}
|
||||
a.json-toggle.collapsed:before {
|
||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Collapsable placeholder links */
|
||||
a.json-placeholder {
|
||||
color: #aaa;
|
||||
padding: 0 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-placeholder:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,14 +1,36 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
/* static/css/style.css
|
||||
* 2025-08-04 수정 – .result_block 스타일 추가
|
||||
*/
|
||||
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
--success-color: #28a745;
|
||||
--error-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--border-color: #e0e0e0;
|
||||
--border-radius: 5px;
|
||||
--border-radius-lg: 10px;
|
||||
--max-width: 1200px;
|
||||
--box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
--box-shadow-lg: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: linear-gradient(135deg, #ece9e6, #ffffff);
|
||||
color: #444;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
align-items: center; /* 수평 중앙 정렬 */
|
||||
padding: 20px; /* 전체 여백 추가 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.title_zone {
|
||||
@@ -23,15 +45,17 @@ body {
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.img_generator_model_zone_1, .img_generator_model_zone_2 {
|
||||
.img_generator_model_zone_1 {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
margin: 30px;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||
width: 35%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 30px;
|
||||
margin: 20px auto; /* 중앙 정렬 */
|
||||
box-shadow: var(--box-shadow-lg);
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
text-align: center;
|
||||
box-sizing: border-box; /* 패딩이 너비에 포함되도록 */
|
||||
}
|
||||
|
||||
.model_name {
|
||||
@@ -50,6 +74,23 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.select_zone {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.select_zone label,
|
||||
.select_zone select,
|
||||
.select_zone input[type="number"] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.generator_btn_zone {
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
@@ -65,11 +106,401 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#input_txt_box_3, #input_txt_box_4 {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.error_zone {
|
||||
color: red;
|
||||
font-size: 15px;
|
||||
font-size: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.result_container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* ---- 이미지 출력 영역 ---- */
|
||||
|
||||
.query_image_zone {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.query_image {
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
border: 2px solid #007bff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.result_image_zone {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 이미지 + 매칭률 묶음 */
|
||||
.result_block {
|
||||
text-align: center;
|
||||
max-width: 200px;
|
||||
flex-grow: 1; /* 공간이 남으면 늘어나도록 */
|
||||
flex-shrink: 1; /* 공간이 부족하면 줄어들도록 */
|
||||
flex-basis: calc(20% - 16px); /* 5개씩 표시 (20% x 5 = 100%) */
|
||||
}
|
||||
|
||||
.result_image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.result_percent {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.json_viewer {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
text-align: left; /* JSON 뷰어 텍스트 왼쪽 정렬 */
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
/* ---- API 설정 영역 ---- */
|
||||
|
||||
.api_config_zone {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 15px;
|
||||
margin: 10px auto 20px auto;
|
||||
box-shadow: var(--box-shadow);
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.config_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.config_title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.toggle_btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.config_content {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.current_api_info {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.api_status_line {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.api_status_line span:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.connection_status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.status_indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status_dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--warning-color);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 연결 상태별 스타일 */
|
||||
.status_dot.connected {
|
||||
background-color: var(--success-color);
|
||||
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.status_dot.disconnected {
|
||||
background-color: var(--error-color);
|
||||
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.status_dot.checking {
|
||||
background-color: var(--warning-color);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* 펄스 애니메이션 */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 0 6px rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(255, 193, 7, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 성공/실패 애니메이션 */
|
||||
@keyframes success-flash {
|
||||
0% { background-color: var(--success-color); }
|
||||
50% { background-color: #20c997; }
|
||||
100% { background-color: var(--success-color); }
|
||||
}
|
||||
|
||||
@keyframes error-flash {
|
||||
0% { background-color: var(--error-color); }
|
||||
50% { background-color: #e74c3c; }
|
||||
100% { background-color: var(--error-color); }
|
||||
}
|
||||
|
||||
.status_dot.flash-success {
|
||||
animation: success-flash 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
.status_dot.flash-error {
|
||||
animation: error-flash 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
#connection_status_text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
#connection_status_text.connected {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
#connection_status_text.disconnected {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
#connection_status_text.checking {
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.refresh_btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius);
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.refresh_btn:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.refresh_btn.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.predefined_endpoints {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.endpoints_title {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.endpoint_buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.endpoint_btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.endpoint_btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.endpoint_btn.active {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.custom_endpoint {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.custom_endpoint span {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#custom_url_input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.config_btn {
|
||||
background-color: var(--success-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.config_btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* --- Media Queries --- */
|
||||
|
||||
/* Small screens (e.g., mobile phones) */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 10px; /* 모바일에서는 패딩 줄임 */
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.img_generator_model_zone_1 {
|
||||
margin: 10px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.api_config_zone {
|
||||
margin: 5px auto 15px auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.model_name {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.input_txt_box,
|
||||
.select_zone label,
|
||||
.select_zone select,
|
||||
.select_zone input[type="number"],
|
||||
.generator_btn,
|
||||
.error_zone,
|
||||
.result_percent {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.select_zone {
|
||||
flex-direction: column; /* 세로로 쌓이도록 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result_block {
|
||||
flex-basis: calc(50% - 16px); /* 두 개씩 표시 */
|
||||
max-width: calc(50% - 16px);
|
||||
}
|
||||
|
||||
.query_image {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium screens (e.g., tablets) */
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.img_generator_model_zone_1 {
|
||||
margin: 15px auto;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.api_config_zone {
|
||||
margin: 8px auto 18px auto;
|
||||
}
|
||||
|
||||
.result_block {
|
||||
flex-basis: calc(33.33% - 16px); /* 세 개씩 표시 */
|
||||
max-width: calc(33.33% - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Large screens (e.g., desktops, FHD) */
|
||||
@media (min-width: 1025px) {
|
||||
.result_block {
|
||||
flex-basis: calc(20% - 16px); /* 5개씩 표시 */
|
||||
max-width: calc(20% - 16px);
|
||||
}
|
||||
}
|
||||
26
src/static/js/constants.js
Normal file
26
src/static/js/constants.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* @File: const.js
|
||||
* @Date: 2025-08-01
|
||||
* @Author: SGM
|
||||
* @Brief: 상수 관리
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
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,';
|
||||
74
src/static/js/init.js
Normal file
74
src/static/js/init.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* @File: init.js
|
||||
* @Date: 2025-08-01
|
||||
* @Author: SGM
|
||||
* @Brief: 초기화 스크립트
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
$(document).ready(function() {
|
||||
// 모든 클래스가 로드되었는지 확인
|
||||
if (typeof MainController === 'undefined') {
|
||||
console.error('MainController가 로드되지 않았습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 메인 컨트롤러 인스턴스 생성
|
||||
window.imageGenerator = new MainController();
|
||||
|
||||
// API 설정 관리자 초기화
|
||||
if (typeof ApiSettings !== 'undefined') {
|
||||
ApiSettings.init();
|
||||
console.log('API Settings 초기화 완료');
|
||||
} else {
|
||||
console.warn('ApiSettings가 로드되지 않았습니다.');
|
||||
}
|
||||
|
||||
// 이벤트 리스너 설정
|
||||
setupEventListeners();
|
||||
|
||||
// 애플리케이션 초기화
|
||||
initializeApp();
|
||||
|
||||
console.log('AI Image Generator 모듈화 초기화 완료');
|
||||
});
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 설정
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
// 이미지 생성 버튼
|
||||
$(SELECTORS.GENERATOR_BTN).on('click', function() {
|
||||
window.imageGenerator.generateImage();
|
||||
});
|
||||
|
||||
// 엔터키로 생성
|
||||
$(SELECTORS.PROMPT_INPUT).on('keypress', function(e) {
|
||||
if (e.which === 13) { // Enter key
|
||||
window.imageGenerator.generateImage();
|
||||
}
|
||||
});
|
||||
|
||||
// 설정 변경 시 상태 관리자 업데이트 (선택사항)
|
||||
$(SELECTORS.MODEL_TYPE_SELECT + ', ' + SELECTORS.INDEX_TYPE_SELECT).on('change', function() {
|
||||
if (window.appState) {
|
||||
const settings = window.imageGenerator.uiManager.getCurrentSettings();
|
||||
window.appState.updateSettings(settings);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 애플리케이션 초기화
|
||||
*/
|
||||
async function initializeApp() {
|
||||
// 서버 설정 로드
|
||||
if (window.appState && window.imageGenerator.apiClient) {
|
||||
await window.appState.loadConfig();
|
||||
}
|
||||
|
||||
// 개발 편의를 위한 초기값 설정
|
||||
$(SELECTORS.MODEL_TYPE_SELECT).val('l14');
|
||||
$(SELECTORS.SEARCH_NUM_INPUT).val(4);
|
||||
}
|
||||
|
||||
2
src/static/js/jquery-3.7.1.min.js
vendored
Normal file
2
src/static/js/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
src/static/js/jquery.json-viewer.min.js
vendored
Normal file
8
src/static/js/jquery.json-viewer.min.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Minified by jsDelivr using Terser v5.37.0.
|
||||
* Original file: /npm/jquery.json-viewer@1.5.0/json-viewer/jquery.json-viewer.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
!function(s){function e(s){return s instanceof Object&&Object.keys(s).length>0}function l(s){return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/'/g,"'").replace(/"/g,""")}function t(s,n){var a="";if("string"==typeof s)s=l(s),n.withLinks&&function(s){for(var e=["http","https","ftp","ftps"],l=0;l<e.length;++l)if(s.startsWith(e[l]+"://"))return!0;return!1}(s)?a+='<a href="'+s+'" class="json-string" target="_blank">'+s+"</a>":a+='<span class="json-string">"'+(s=s.replace(/"/g,"\\""))+'"</span>';else if("number"==typeof s||"bigint"==typeof s)a+='<span class="json-literal">'+s+"</span>";else if("boolean"==typeof s)a+='<span class="json-literal">'+s+"</span>";else if(null===s)a+='<span class="json-literal">null</span>';else if(s instanceof Array)if(s.length>0){a+='[<ol class="json-array">';for(var o=0;o<s.length;++o)a+="<li>",e(s[o])&&(a+='<a href class="json-toggle"></a>'),a+=t(s[o],n),o<s.length-1&&(a+=","),a+="</li>";a+="</ol>]"}else a+="[]";else if("object"==typeof s)if(n.bigNumbers&&("function"==typeof s.toExponential||s.isLosslessNumber))a+='<span class="json-literal">'+s.toString()+"</span>";else{var i=Object.keys(s).length;if(i>0){for(var r in a+='{<ul class="json-dict">',s)if(Object.prototype.hasOwnProperty.call(s,r)){r=l(r);var c=n.withQuotes?'<span class="json-string">"'+r+'"</span>':r;a+="<li>",e(s[r])?a+='<a href class="json-toggle">'+c+"</a>":a+=c,a+=": "+t(s[r],n),--i>0&&(a+=","),a+="</li>"}a+="</ul>}"}else a+="{}"}return a}s.fn.jsonViewer=function(l,n){return n=Object.assign({},{collapsed:!1,rootCollapsable:!0,withQuotes:!1,withLinks:!0,bigNumbers:!1},n),this.each((function(){var a=t(l,n);n.rootCollapsable&&e(l)&&(a='<a href class="json-toggle"></a>'+a),s(this).html(a),s(this).addClass("json-document"),s(this).off("click"),s(this).on("click","a.json-toggle",(function(){var e=s(this).toggleClass("collapsed").siblings("ul.json-dict, ol.json-array");if(e.toggle(),e.is(":visible"))e.siblings(".json-placeholder").remove();else{var l=e.children("li").length,t=l+(l>1?" items":" item");e.after('<a href class="json-placeholder">'+t+"</a>")}return!1})),s(this).on("click","a.json-placeholder",(function(){return s(this).siblings("a.json-toggle").click(),!1})),1==n.collapsed&&s(this).find("a.json-toggle").click()}))}}(jQuery);
|
||||
//# sourceMappingURL=/sm/465958409b0102294d491b30ce64ca72a25b1f2a2eeb08f5c4d0e06c26fea020.map
|
||||
373
src/static/js/modules/config.js
Normal file
373
src/static/js/modules/config.js
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* @File: apiConfigManager.js
|
||||
* @Date: 2025-08-06
|
||||
* @Brief: API 엔드포인트 설정 관리 모듈
|
||||
*/
|
||||
|
||||
const ApiSettings = {
|
||||
endpoints: [],
|
||||
currentUrl: '',
|
||||
connectionStatus: 'checking', // 'connected', 'disconnected', 'checking'
|
||||
statusCheckInterval: null,
|
||||
|
||||
async init() {
|
||||
console.log('ApiSettings 초기화 시작...');
|
||||
try {
|
||||
await this.loadEndpoints();
|
||||
this.setupEventListeners();
|
||||
this.updateUI();
|
||||
this.startConnectionMonitoring();
|
||||
console.log('ApiSettings 초기화 완료');
|
||||
} 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.ApiSettings) {
|
||||
window.ApiSettings.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
// 전역에서 사용할 수 있도록 window 객체에 추가
|
||||
window.ApiSettings = ApiSettings;
|
||||
159
src/static/js/modules/http.js
Normal file
159
src/static/js/modules/http.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* @File: apiClient.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: SGM
|
||||
* @Brief: API 통신 클라이언트 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class HttpClient {
|
||||
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.HttpClient = HttpClient;
|
||||
213
src/static/js/modules/main.js
Normal file
213
src/static/js/modules/main.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* @File: imageGenerator.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: SGM
|
||||
* @Brief: 이미지 생성 메인 컨트롤러 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class MainController {
|
||||
constructor() {
|
||||
this.isGenerating = false;
|
||||
this.apiClient = new HttpClient();
|
||||
this.uiManager = new UiUpdater();
|
||||
|
||||
// 상태 관리자 연결
|
||||
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.MainController = MainController;
|
||||
191
src/static/js/modules/state.js
Normal file
191
src/static/js/modules/state.js
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* @File: stateManager.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: SGM
|
||||
* @Brief: 애플리케이션 상태 관리자
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class AppState {
|
||||
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 AppState();
|
||||
|
||||
// 페이지 로드 시 설정 자동 로드
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
appState.loadConfig();
|
||||
});
|
||||
|
||||
// 전역으로 노출 (다른 스크립트에서 사용할 수 있도록)
|
||||
window.appState = appState;
|
||||
223
src/static/js/modules/ui.js
Normal file
223
src/static/js/modules/ui.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* @File: uiManager.js
|
||||
* @Date: 2025-08-05
|
||||
* @Author: Claude
|
||||
* @Brief: UI 관리 모듈
|
||||
* @section MODIFYINFO 수정정보
|
||||
*/
|
||||
|
||||
class UiUpdater {
|
||||
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.UiUpdater = UiUpdater;
|
||||
@@ -1,58 +1,116 @@
|
||||
<!---
|
||||
@File: index.html
|
||||
@Date: 2025-01-16
|
||||
@author: A2TEC
|
||||
@brief: G 웹 서버
|
||||
@Date: 2025-08-01
|
||||
@Author: SGM
|
||||
@Brief: Eye 웹 서버
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2025-01-16/ksy : base
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<!-- <head> -->
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
content="no-cache, no-store, must-revalidate" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ImageGenerator</title>
|
||||
<script src="static/js/jquery.min.js"></script>
|
||||
<script src="static/js/config.js"></script>
|
||||
<link rel="stylesheet" href="static/css/style.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="static/js/jquery-3.7.1.min.js"></script>
|
||||
<link rel="stylesheet" href="static/css/style.css" type="text/css" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="static/css/jquery.json-viewer.css"
|
||||
type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="title_zone">
|
||||
<span class="title">AI Image Generator</span>
|
||||
<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 class="current_api_info">
|
||||
<div class="api_status_line">
|
||||
<span>현재 API: </span>
|
||||
<span id="current_api_url">로딩중...</span>
|
||||
</div>
|
||||
<div class="connection_status">
|
||||
<div class="status_indicator">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="predefined_endpoints">
|
||||
<span class="endpoints_title">사전 정의된 엔드포인트:</span>
|
||||
<div id="endpoint_buttons" class="endpoint_buttons">
|
||||
<!-- 버튼들이 동적으로 추가될 예정 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="custom_endpoint">
|
||||
<span>커스텀 URL:</span>
|
||||
<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>
|
||||
</div>
|
||||
<div class="img_generator_model_zone_1">
|
||||
<span class="model_name">Bing Art</span>
|
||||
<div>
|
||||
<input type="text" id="input_txt_box_1" class="input_txt_box" placeholder="입력하세요.">
|
||||
</div>
|
||||
<div>
|
||||
<span class="download_count">Download Count:</span>
|
||||
<input type="text" id="input_txt_box_3" class="input_txt_box" value="1">
|
||||
</div>
|
||||
<div id="error_zone_1" class="error_zone"></div>
|
||||
<div class="generator_btn_zone">
|
||||
<button onclick="generatorBtn1()" id="generator_1_btn" class="generator_btn">Create</button>
|
||||
</div>
|
||||
<span class="model_name">AI Image Generator</span>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
id="input_txt_box"
|
||||
class="input_txt_box"
|
||||
placeholder="입력하세요." />
|
||||
</div>
|
||||
<div class="select_zone">
|
||||
<label for="model_type_select">Model Type:</label>
|
||||
<select id="model_type_select">
|
||||
<option value="b32">b32</option>
|
||||
<option value="b16">b16</option>
|
||||
<option value="l14" selected>l14</option>
|
||||
<option value="l14_336">l14_336</option>
|
||||
</select>
|
||||
<label for="index_type_select">Index Type:</label>
|
||||
<select id="index_type_select">
|
||||
<option value="l2">l2</option>
|
||||
<option value="cos">cos</option>
|
||||
</select>
|
||||
<label for="search_num_input">Search Num:</label>
|
||||
<input type="number" id="search_num_input" value="4" min="1" max="10" />
|
||||
</div>
|
||||
<div id="error_zone" class="error_zone"></div>
|
||||
<div class="generator_btn_zone">
|
||||
<button id="generator_btn" class="generator_btn">Request</button>
|
||||
</div>
|
||||
<div id="loading_zone" style="display: none">Generating...</div>
|
||||
<div class="result_container">
|
||||
<div id="query_image_zone" class="query_image_zone"></div>
|
||||
<div id="result_image_zone" class="result_image_zone"></div>
|
||||
<pre id="json_viewer" class="json_viewer"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img_generator_model_zone_2">
|
||||
<span class="model_name">Imagen</span>
|
||||
<div>
|
||||
<input type="text" id="input_txt_box_2" class="input_txt_box" placeholder="입력하세요.">
|
||||
</div>
|
||||
<div>
|
||||
<span class="download_count">Download Count:</span>
|
||||
<input type="text" id="input_txt_box_4" class="input_txt_box" value="1">
|
||||
</div>
|
||||
<div id="error_zone_2" class="error_zone"></div>
|
||||
<div class="generator_btn_zone">
|
||||
<button onclick="generatorBtn2()" id="generator_1_btn" class="generator_btn">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="static/js/const.js"></script>
|
||||
<script src="static/js/api.js"></script>
|
||||
<script src="static/js/utility.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<script src="static/js/jquery.json-viewer.min.js"></script>
|
||||
|
||||
<!-- 상수 및 설정 -->
|
||||
<script src="static/js/constants.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/init.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,21 +2,32 @@
|
||||
|
||||
"""
|
||||
@File: web_server.py
|
||||
@Date: 2025-01-16
|
||||
@author: DaoolDNS
|
||||
@brief: AI 이미지 생성 웹 서버
|
||||
@Date: 2025-08-01
|
||||
@Author: SGM
|
||||
@Brief: AI 이미지 생성 웹 서버
|
||||
@section MODIFYINFO 수정정보
|
||||
- 수정자/수정일 : 수정내역
|
||||
- 2025-01-16/ksy : base
|
||||
"""
|
||||
|
||||
|
||||
import uvicorn
|
||||
import httpx
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from common.const import SERVICE_PORT
|
||||
from common.config import SERVICE_PORT, HOST, API_TIMEOUT_SECONDS, PREDEFINED_ENDPOINTS
|
||||
import common.config as settings
|
||||
from services.manager import api_manager
|
||||
from config.app_config import app_config
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@@ -24,6 +35,8 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request):
|
||||
"""
|
||||
@@ -33,5 +46,226 @@ async def home(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""
|
||||
서버 상태 확인 (헬스체크)
|
||||
개발 시 서버가 정상 동작하는지 빠르게 확인용
|
||||
"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"service": "AI Image Generator Web Server"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api-status")
|
||||
async def api_status_check():
|
||||
"""
|
||||
외부 API 연결 상태 확인
|
||||
개발 시 외부 API 서버 연결 상태를 미리 확인할 수 있음
|
||||
"""
|
||||
try:
|
||||
# 간단한 연결 테스트 (실제 API 호출 없이 연결만 확인)
|
||||
async with httpx.AsyncClient(timeout=API_TIMEOUT_SECONDS) as client:
|
||||
test_url = settings.EXTERNAL_API_URL.split('/api/')[0] if '/api/' in settings.EXTERNAL_API_URL else settings.EXTERNAL_API_URL
|
||||
response = await client.get(test_url, timeout=API_TIMEOUT_SECONDS)
|
||||
|
||||
return {
|
||||
"status": "connected",
|
||||
"api_url": settings.EXTERNAL_API_URL,
|
||||
"connection_test": "success",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.warning(f"API 연결 테스트 실패: {exc}")
|
||||
return {
|
||||
"status": "disconnected",
|
||||
"api_url": settings.EXTERNAL_API_URL,
|
||||
"connection_test": "failed",
|
||||
"error": str(exc),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"note": "외부 API 서버가 실행되지 않았거나 네트워크 문제일 수 있습니다."
|
||||
}
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_frontend_config():
|
||||
"""
|
||||
프론트엔드에서 필요한 설정 정보 반환
|
||||
"""
|
||||
try:
|
||||
config_data = app_config.get_config_for_frontend()
|
||||
available_apis = api_manager.get_available_apis()
|
||||
|
||||
return JSONResponse(status_code=200, content={
|
||||
"config": config_data,
|
||||
"apis": available_apis
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.error(f"설정 로드 오류: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "설정을 로드할 수 없습니다.", "error_type": "config_error"}
|
||||
)
|
||||
|
||||
|
||||
@app.post("/create")
|
||||
async def create_item(request: Request):
|
||||
"""
|
||||
이미지 생성 요청 처리 (기본 API 사용)
|
||||
"""
|
||||
return await _create_with_api(request, api_name=None)
|
||||
|
||||
|
||||
@app.post("/create/{api_name}")
|
||||
async def create_with_specific_api(api_name: str, request: Request):
|
||||
"""
|
||||
특정 API로 이미지 생성 요청 처리
|
||||
|
||||
Args:
|
||||
api_name: 사용할 API 이름 (예: 'imagen', 'dalle')
|
||||
"""
|
||||
return await _create_with_api(request, api_name=api_name)
|
||||
|
||||
|
||||
@app.get("/api-endpoints")
|
||||
async def get_api_endpoints():
|
||||
"""
|
||||
사용 가능한 API 엔드포인트 목록 반환
|
||||
"""
|
||||
return {
|
||||
"current_url": settings.EXTERNAL_API_URL,
|
||||
"predefined_endpoints": PREDEFINED_ENDPOINTS,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
@app.post("/change-api-url")
|
||||
async def change_api_url(request: Request):
|
||||
"""
|
||||
API URL 동적 변경
|
||||
"""
|
||||
try:
|
||||
data = await request.json()
|
||||
new_url = data.get("url")
|
||||
|
||||
if not new_url:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": "URL이 제공되지 않았습니다.", "error_type": "validation_error"}
|
||||
)
|
||||
|
||||
# URL 형식 간단 검증
|
||||
if not (new_url.startswith("http://") or new_url.startswith("https://")):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"detail": "올바른 URL 형식이 아닙니다. http:// 또는 https://로 시작해야 합니다.", "error_type": "validation_error"}
|
||||
)
|
||||
|
||||
# 설정 변경
|
||||
old_url = settings.EXTERNAL_API_URL
|
||||
settings.EXTERNAL_API_URL = new_url
|
||||
|
||||
logger.info(f"API URL 변경: {old_url} -> {new_url}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"old_url": old_url,
|
||||
"new_url": new_url,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"message": "API URL이 성공적으로 변경되었습니다."
|
||||
}
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(f"API URL 변경 오류: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "API URL 변경 중 오류가 발생했습니다.",
|
||||
"error_type": "internal_error"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _create_with_api(request: Request, api_name: str = None):
|
||||
"""
|
||||
API를 사용한 이미지 생성 공통 로직
|
||||
"""
|
||||
try:
|
||||
raw_data = await request.json()
|
||||
logger.info(f"이미지 생성 요청 받음: {raw_data.get('prompt', 'N/A')[:50]}... (API: {api_name or 'default'})")
|
||||
logger.info(f"요청 데이터 전체: {raw_data}") # 디버깅용 로그 추가
|
||||
|
||||
# 데이터 검증 및 정리
|
||||
validated_data = app_config.validate_request_data(raw_data)
|
||||
logger.info(f"검증된 데이터: {validated_data}") # 디버깅용 로그 추가
|
||||
|
||||
# API 설정 검증
|
||||
if not api_manager.validate_settings(validated_data, api_name):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"detail": "유효하지 않은 설정값입니다.",
|
||||
"error_type": "validation_error"
|
||||
}
|
||||
)
|
||||
|
||||
# 이미지 생성 요청
|
||||
response_data = await api_manager.generate_image(validated_data, api_name)
|
||||
logger.info(f"이미지 생성 성공 (API: {api_name or 'default'})")
|
||||
|
||||
return JSONResponse(status_code=200, content=response_data)
|
||||
|
||||
except ValueError as exc:
|
||||
# 입력 데이터 검증 오류
|
||||
logger.warning(f"입력 검증 오류: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"detail": str(exc),
|
||||
"error_type": "validation_error"
|
||||
}
|
||||
)
|
||||
except httpx.HTTPStatusError as exc:
|
||||
error_msg = f"외부 API 오류: {exc.response.status_code}"
|
||||
logger.error(f"{error_msg} - {exc.response.text}")
|
||||
return JSONResponse(
|
||||
status_code=exc.response.status_code,
|
||||
content={
|
||||
"detail": error_msg,
|
||||
"error_type": "api_error",
|
||||
"status_code": exc.response.status_code
|
||||
}
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
error_msg = f"API 연결 오류: {exc.__class__.__name__}"
|
||||
logger.error(f"{error_msg}: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={
|
||||
"detail": "외부 API 서버에 연결할 수 없습니다. 네트워크 연결을 확인하세요.",
|
||||
"error_type": "connection_error",
|
||||
"technical_detail": str(exc)
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
error_msg = f"예상치 못한 오류: {exc.__class__.__name__}"
|
||||
logger.error(f"{error_msg}: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "서버 내부 오류가 발생했습니다.",
|
||||
"error_type": "internal_error"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run("web_server:app", host='0.0.0.0', port=SERVICE_PORT)
|
||||
logger.info(f"AI Image Generator 웹 서버를 시작합니다...")
|
||||
logger.info(f"서버 주소: http://{HOST}:{SERVICE_PORT}")
|
||||
logger.info(f"외부 API: {settings.EXTERNAL_API_URL}")
|
||||
logger.info(f"헬스체크: http://localhost:{SERVICE_PORT}/health")
|
||||
logger.info(f"API 상태 확인: http://localhost:{SERVICE_PORT}/api-status")
|
||||
|
||||
uvicorn.run("web_server:app", host=HOST, port=SERVICE_PORT)
|
||||
Reference in New Issue
Block a user