Update project files

This commit is contained in:
rudals252
2025-08-07 16:38:53 +09:00
parent e5f0928d0a
commit 5f608d1f23
21 changed files with 2845 additions and 77 deletions

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)"
],
"deny": []
}
}

140
README.md
View File

@@ -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 요청/응답 상세 분석

View File

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

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""
Config 패키지 초기화
"""

188
src/config/app_config.py Normal file
View 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
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.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
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 .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()

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

View File

@@ -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);
}
}

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

File diff suppressed because one or more lines are too long

View 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/'/g,"&apos;").replace(/"/g,"&quot;")}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(/&quot;/g,"\\&quot;"))+'"</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

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

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

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

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

View File

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

View File

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