Analyzed design mockup (Gemini-style) vs current UI and documented: - D4-1: left sidebar layout replacing top tabs - D4-2: welcome view with personalized greeting + glow gradient - D4-3: pill-shaped input with center/bottom position transition - D4-4: example question chip cards (gr.HTML replacing gr.Examples) - D4-5: control panel iconification with popup panel - D4-6: conversation history in sidebar via localStorage Includes comparison table, implementation difficulty ratings, and checklist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
24 KiB
24 KiB
youlbot-webui 개선 로드맵
현황 요약
| 항목 | 현재 상태 | 심각도 |
|---|---|---|
| 아키텍처 모듈화 | ✅ 완료 | |
| Windows 호환성 | say 명령어 — macOS 전용 |
✅ 완료 |
| Gradio Chatbot 타입 | type="messages" 누락 |
✅ 완료 |
| JSON yield 타입 불일치 | JSONDecodeError 시 타입 혼용str() 변환 적용 |
✅ 완료 |
| run_id 인덱싱 버그 | history / run_ids 동기화 취약 |
✅ 완료 |
| RAG 출처 표시 | 📄 출처 전용 박스로 분리 |
✅ 완료 |
| async/sync 혼용 | asyncio.run() 5곳 |
✅ 완료 |
| 코드 중복 | asyncio.run() 5회 반복 |
✅ 완료 |
| 결합도 | api_client 직접 임포트 |
✅ 완료 |
| 테스트 가능성 | ✅ 완료 | |
| 로깅 | print() 만 사용logging 모듈, LOG_LEVEL 환경변수 |
✅ 완료 |
IoC / 의존성 주입 전략
전용 IoC 프레임워크(dependency-injector 등)는 현재 규모에 과도한 복잡도를 유발합니다.
대신 수동 DI 패턴을 적용합니다.
Protocol기반 인터페이스로api_client추상화 → 테스트 모킹 가능Container클래스가 서비스 인스턴스 생성·생명주기 관리- 향후 규모가 커질 경우 프레임워크로 전환 용이
목표 아키텍처
┌──────────────────────────────────────────┐
│ app.py (Gradio UI) │
│ 콜백 함수는 service 메서드만 호출 │
└──────────────┬───────────────────────────┘
│ 주입받음
┌──────────────▼───────────────────────────┐
│ container.py (Container) │
│ chat_service, document_service 제공 │
└──────────────┬───────────────────────────┘
│ 생성
┌──────────────▼───────────────────────────┐
│ services/ │
│ chat.py / document.py / tts.py │
└──────────────┬───────────────────────────┘
│ 사용
┌──────────────▼───────────────────────────┐
│ api_client.py (APIClientProtocol) │
│ HTTPAPIClient (실구현) │
└──────────────┬───────────────────────────┘
│ 설정 읽음
┌──────────────▼───────────────────────────┐
│ config.py (AppConfig / APIConfig) │
│ 환경변수 일원화 │
└──────────────────────────────────────────┘
P0 — 즉시 수정 (버그·호환성)
현재 Windows 환경에서 실행 불가 또는 런타임 오류 유발 항목
1. 크로스플랫폼 TTS 구현 (app.py:47-61)
- 문제:
subprocess.run(["say", ...])는 macOS 전용 → Windows에서 동작 불가. - 플랫폼별 우선순위:
- macOS:
say(오프라인, 내장) →edge-tts(온라인 폴백) →pyttsx3(최종 폴백) - Windows:
edge-tts(온라인) →pyttsx3(오프라인 폴백)
- macOS:
| 우선순위 | 라이브러리 | 방식 | 품질 | Windows | macOS |
|---|---|---|---|---|---|
| macOS 1순위 | say |
오프라인(내장) | ⭐⭐⭐⭐ | ❌ | ✅ |
| macOS 2순위 / Windows 1순위 | edge-tts |
온라인(MS Edge) | ⭐⭐⭐⭐⭐ | ✅ | ✅ |
| 최종 폴백 | pyttsx3 |
오프라인(SAPI5/NSSpeech) | ⭐⭐⭐ | ✅ | ✅ |
async def tts_speak(text: str) -> str | None:
"""크로스플랫폼 TTS — 플랫폼별 우선순위 적용."""
if not text:
return None
# macOS: say 우선 (오프라인, 자연스러운 한국어)
if platform.system() == "Darwin":
try:
tmp = tempfile.NamedTemporaryFile(suffix=".aiff", delete=False)
tmp.close()
await asyncio.to_thread(
subprocess.run,
["say", "-v", _TTS_VOICE, "-o", tmp.name, text],
check=True, capture_output=True,
)
return tmp.name
except Exception:
pass
# Windows 1순위 / macOS say 실패 시: edge-tts (온라인)
try:
import edge_tts
tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
tmp.close()
await edge_tts.Communicate(text, _TTS_EDGE_VOICE).save(tmp.name)
return tmp.name
except Exception:
pass
# 최종 폴백: pyttsx3 (오프라인)
try:
import pyttsx3
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
tmp.close()
def _save():
engine = pyttsx3.init()
engine.save_to_file(text, tmp.name)
engine.runAndWait()
await asyncio.to_thread(_save)
return tmp.name
except Exception:
return None
- 전역 변수 (
app.py상단 추가):_TTS_VOICE = os.getenv("TTS_VOICE", "Yuna") # macOS say _TTS_EDGE_VOICE = os.getenv("TTS_EDGE_VOICE", "ko-KR-SunHiNeural") # edge-tts - 출력 포맷: macOS say →
.aiff(기존 유지), edge-tts →.mp3, pyttsx3 →.wav - requirements.txt 추가:
edge-tts>=6.1.9,pyttsx3>=2.90 - 주요 edge-tts 한국어 보이스:
ko-KR-SunHiNeural(여성),ko-KR-InJoonNeural(남성)
2. Gradio Chatbot type="messages" 누락 (app.py:186)
- 문제: Gradio 4.x에서
{"role": ..., "content": ...}딕셔너리 포맷 사용 시type="messages"를 명시하지 않으면 경고 또는 오류 발생. - 수정:
gr.Chatbot(type="messages", ...)추가.
3. JSON yield 타입 불일치 (api_client.py:45-46)
- 문제:
JSONDecodeError발생 시raw(bytes 또는 str)를 그대로 yield → 반환 타입tuple[str, str | None]불일치. - 수정:
yield str(raw), None으로 명시적 변환.
4. run_id 인덱싱 방어 로직 (app.py:107-108)
- 문제:
history길이와run_ids길이가 어긋나면 인덱스 오류 발생 가능. - 수정:
asst_turn계산 후 범위 초과 시None반환하는 방어 코드 보강.
P1 — 1주일 내 (구조 개선)
1. 설정 분리 → config.py 신규 생성
# config.py
from dataclasses import dataclass, field
import os
@dataclass
class APIConfig:
url: str = field(default_factory=lambda: os.getenv("YOULBOT_API_URL", "http://localhost:8000"))
token: str = field(default_factory=lambda: os.getenv("YOULBOT_API_TOKEN", ""))
timeout: int = 180
@dataclass
class AppConfig:
api: APIConfig = field(default_factory=APIConfig)
whisper_model_size: str = field(default_factory=lambda: os.getenv("WHISPER_MODEL_SIZE", "small"))
tts_voice: str = field(default_factory=lambda: os.getenv("TTS_VOICE", "Yuna"))
server_host: str = "0.0.0.0"
server_port: int = 7860
2. Protocol 인터페이스 + HTTPAPIClient 분리 (api_client.py 리팩터링)
# api_client.py
from typing import Protocol, AsyncIterator, runtime_checkable
@runtime_checkable
class APIClientProtocol(Protocol):
async def chat(self, message: str, user_id: str, show_thinking: bool) -> AsyncIterator[tuple[str, str | None]]: ...
async def reset(self, user_id: str) -> None: ...
async def ingest(self, file_path: str) -> dict: ...
async def list_documents(self) -> list[str]: ...
async def delete_document(self, source: str) -> None: ...
async def save_feedback(self, user_id: str, user_msg: str, asst_msg: str, rating: int, run_id: str | None) -> None: ...
class HTTPAPIClient:
def __init__(self, config: APIConfig): ...
# 기존 함수들을 메서드로 이전
3. 서비스 레이어 분리 → services/ 패키지 신규 생성
services/
├── __init__.py — ChatService, DocumentService, TTSService 재익스포트
├── chat.py — ChatService: chat, reset, save_feedback
├── document.py — DocumentService: ingest, list_documents, delete_document
└── tts.py — TTSService: speak (플랫폼 분기)
4. 수동 DI 컨테이너 → container.py 신규 생성
# container.py
class Container:
def __init__(self, config: AppConfig):
self._config = config
self._api_client: HTTPAPIClient | None = None
self._chat_service: ChatService | None = None
self._document_service: DocumentService | None = None
@property
def api_client(self) -> HTTPAPIClient: ...
@property
def chat_service(self) -> ChatService: ...
@property
def document_service(self) -> DocumentService: ...
5. Async 콜백 통일 (app.py)
handle_feedback,reset_chat,ingest_files,list_docs,delete_doc→ 모두async def로 전환asyncio.get_event_loop().run_until_complete()패턴 완전 제거- Gradio 4.x async 콜백 지원 활용
P2 — 2주일 내 (품질 개선)
| # | 항목 | 설명 |
|---|---|---|
| 1 | 로깅 시스템 | print() → logging 모듈, 구조적 로그 포맷 |
| 2 | httpx 연결 풀 공유 | HTTPAPIClient에서 AsyncClient 인스턴스 재사용 |
| 3 | 입력 검증 | 파일 경로 sanitize, URL path parameter 검증 |
| 4 | 단위 테스트 | tests/ 폴더 생성, ChatService / DocumentService Mock 테스트 |
P3 — 선택 사항 (장기)
| # | 항목 | 설명 |
|---|---|---|
| 1 | 재시도 로직 | tenacity 라이브러리 또는 수동 exponential backoff |
| 2 | Pydantic Settings | pydantic-settings 로 타입 안전 환경변수 관리 |
| 3 | IoC 프레임워크 전환 | 규모 확장 시 dependency-injector 도입 검토 |
최종 파일 구조 (목표)
youlbot-webui/
├── app.py # Gradio UI 전용 — 콜백만 존재, 비즈니스 로직 없음
├── container.py # 수동 DI 컨테이너
├── services/
│ ├── __init__.py # 재익스포트
│ ├── chat.py # ChatService
│ ├── document.py # DocumentService
│ └── tts.py # TTSService
├── api_client.py # APIClientProtocol + HTTPAPIClient
├── config.py # AppConfig, APIConfig dataclass
├── tests/
│ ├── test_chat_service.py
│ └── test_document_service.py
├── requirements.txt
├── .env.example
└── ROADMAP.md
UI/UX 디자인 개선
실제 UI 스크린샷 분석(2026-06-02) 기반 — 우선순위 순 정렬
D0 — 즉시 수정 (노출 결함)
| # | 위치 | 문제 | 개선 방안 |
|---|---|---|---|
| 1 | 대화 탭 · 입력창 | "Textbox" 레이블이 입력창 위에 그대로 노출 |
gr.Textbox(label="", show_label=False, ...) 또는 label=None |
| 2 | 대화 탭 · 음성 영역 | "마이크를 찾을 수 ..." 오류 메시지가 항상 노출 |
마이크 미사용 시 해당 텍스트 숨김 처리, 또는 visible=False 기본값 |
| 3 | 전체 탭 · 푸터 | "Gradio로 제작됨 🎉" Gradio 브랜딩 노출 |
css="footer { display: none; }" 추가 또는 gr.Blocks(show_footer=False) |
| 4 | 문서 등록 탭 · 결과 박스 | 업로드 전에도 빈 결과 박스가 항상 노출 |
초기 visible=False, 수집 완료 시에만 visible=True 반환 |
D1 — 1주일 내 (레이아웃 개선)
| # | 위치 | 문제 | 개선 방안 |
|---|---|---|---|
| 5 | 대화 탭 · 채팅 영역 | 초기 화면이 빈 흰 공간으로 시작 — 어색한 첫 인상 | 웰컴 메시지 또는 예시 질문 버블(gr.Examples) 추가 |
| 6 | 대화 탭 · 이미지 첨부 | 이미지 업로드 영역이 항상 전체 크기로 펼쳐짐 | gr.Accordion("이미지 첨부 (선택)", open=False) 로 접이식 변경 |
| 7 | 대화 탭 · 하단 컨트롤 | 사고 과정 표시, TTS, 대화 초기화 배치가 불규칙 |
gr.Row로 균등 3분할, 대화 초기화는 오른쪽 정렬 |
| 8 | 대화 탭 · 입력 영역 | 텍스트 입력창과 전송 버튼이 시각적으로 분리됨 |
입력창 높이를 lines=2로 통일, 버튼 높이 CSS로 맞춤 |
| 9 | 문서 관리 탭 · 삭제 UX | 경로를 테이블에서 복사해 입력 필드에 붙여넣기 해야 함 | 테이블 행 클릭 → 입력 필드 자동 채움 (select 이벤트 활용) |
| 10 | 문서 등록 탭 · 버튼 | 문서 수집 버튼이 전체 너비를 차지해 무게감 과도 |
scale=0 또는 min_width=200 으로 적정 크기 조절 |
D2 — 2주일 내 (시각 품질)
| # | 항목 | 설명 |
|---|---|---|
| 11 | 헤더 브랜딩 | 텍스트 전용 헤더 → 아이콘/로고 이미지 + 서브타이틀 레이아웃으로 개선 |
| 12 | 커스텀 테마 | Gradio 기본 보라색 → gr.themes.Soft() 또는 커스텀 primary_hue 색상 지정 |
| 13 | 사용자 선택 위치 | 사용자 드롭다운이 채팅 위에 있어 흐름 방해 → 헤더 우측 또는 사이드바로 이동 |
| 14 | 채팅 버블 스타일 | Gradio 기본 스타일 → CSS로 사용자/봇 버블 배경색·radius 차별화 |
| 15 | 응답 로딩 표시 | 스트리밍 중 시각적 피드백 없음 → 입력 비활성화 + 스피너 CSS 추가 |
| 16 | 반응형 레이아웃 | 좁은 뷰포트에서 요소 겹침 → gr.Column/gr.Row 비율 재조정 |
D3 — 선택 사항 (장기)
| # | 항목 | 설명 |
|---|---|---|
| 17 | 다크 모드 | gr.themes.Base() + CSS 변수로 다크/라이트 토글 지원 |
| 18 | 채팅 내보내기 | 대화 내용을 .txt/.md로 다운로드하는 버튼 추가 |
| 19 | 접근성 | aria-label 속성 추가, 키보드 포커스 표시 개선 |
| 20 | 온보딩 투어 | 첫 방문 사용자 대상 단계별 기능 안내 (JS 오버레이) |
D 체크리스트
D0
gr.Textbox(show_label=False)—"Textbox"레이블 제거- 음성 오류 메시지 기본 숨김 처리 —
gr.Accordion("🎤 음성으로 질문하기", open=False)로 기본 접힘 - Gradio 푸터 CSS 숨김 —
footer { display: none !important; }추가 결과박스 초기visible=False—ingest_files에서gr.update(visible=True)반환
D1
- 웰컴 메시지 또는 예시 질문 버블 추가 —
gr.Examples3개 예시 질문 - 이미지 첨부 영역
gr.Accordion으로 접이식 변경 —open=False기본 접힘 - 하단 컨트롤
gr.Row균등 배치 — 체크박스 좌측Column(scale=3), 초기화 버튼 우측Column(scale=1) - 입력창
lines=2+ 전송 버튼 높이 CSS 맞춤 —.send-btn { min-height: 80px } - 문서 관리 탭 — 테이블 행 클릭 → 삭제 경로 자동 채움 —
doc_table.select+select_doc_row 문서 수집버튼 크기 적정화 —scale=0, min_width=200
D2
- 헤더 아이콘/로고 추가 —
gr.HTML🤖 아이콘 + 타이틀/서브타이틀 레이아웃 - 커스텀 테마 적용 —
gr.themes.Soft(primary_hue="blue", secondary_hue="indigo", neutral_hue="slate") - 사용자 드롭다운 위치 이동 — 탭 내부 → 헤더 Row 우측 (
scale=0, min_width=160) - 채팅 버블 커스텀 CSS —
.message-wrap .user파란 배경,.message-wrap .bot회색 배경 - 스트리밍 로딩 표시 —
_live_html에streaming-indicatorblink 애니메이션 - 반응형 레이아웃 —
@media (max-width: 768px)모바일 대응 CSS
D3
- 다크 모드 토글 — 헤더 🌙/☀️ 버튼 +
localStorage기억 + 시스템 설정 자동 감지 - 채팅 내보내기 —
export_chat함수,.md파일 다운로드,gr.File출력 - 접근성 개선 —
aria-label/role/aria-liveJS 주입,:focus-visible파란 테두리 - 온보딩 투어 — 첫 방문 모달 (
localStorage체크, "시작하기" 버튼으로 닫기)
D4 — 시안 기반 UI 리뉴얼 (Gemini 스타일)
참고 시안: Google Gemini 스타일 — 좌측 사이드바 + 중앙 웰컴 화면 + 글로우 그라디언트
시안 vs 현재 비교
| 항목 | 현재 율봇 | 시안 (Gemini 스타일) |
|---|---|---|
| 네비게이션 | 상단 수평 탭 3개 | 좌측 접이식 사이드바 |
| 빈 화면 | 빈 흰 공간 + 예시 질문 하단 | 중앙 개인화 인사 + 글로우 배경 |
| 입력창 위치 | 항상 하단 고정 | 빈 상태: 중앙 / 대화 중: 하단 |
| 입력창 모양 | 직사각형 | Pill 형태(완전 둥근 테두리) |
| 예시 질문 | 레이블 있는 리스트 | 레이블 없는 수평 chip 카드 |
| 컨트롤 위치 | 항상 노출(체크박스·버튼 3개) | 아이콘 버튼으로 숨김 처리 |
| 사용자 표시 | 드롭다운(헤더 우측) | 사이드바 하단 프로필 영역 |
| 대화 이력 | 없음 | 사이드바에 최근 대화 목록 |
D4-1 — 좌측 사이드바 레이아웃
현재 탭 구조를 사이드바로 전환하는 핵심 레이아웃 변경
- 구조:
gr.Blocks내에gr.Column(scale=1)사이드바 +gr.Column(scale=5)메인 영역 - 사이드바 구성:
- 상단: 율봇 로고 + 사이드바 접기 버튼(☰)
- 중간: 네비게이션 (
💬 대화,📄 문서 등록,🗂 문서 관리) - 하단: 사용자 프로필(아이콘 + 이름)
- 사이드바 접기: JS로
sidebar-collapsedCSS 클래스 토글 →max-width: 0전환 - 현재 탭 →
gr.Tab제거, 메인 영역을gr.Column(visible=...)3개로 교체 - CSS:
.sidebar { width: 260px; transition: width .25s; }+.sidebar-collapsed { width: 64px; }
D4-2 — 빈 화면 웰컴 뷰 (중앙 인사 + 글로우)
대화가 없을 때 Gemini 스타일 중앙 화면 표시
- 개인화 인사:
"{user_name}님, 시작해 볼까요?"— 사용자 선택값 반영 - 글로우 그라디언트: 화면 중앙에 파란/하늘색 방사형 그라디언트 광원 효과
.welcome-glow { background: radial-gradient(ellipse 60% 50% at 50% 40%, rgba(99,179,237,.25) 0%, rgba(147,197,253,.12) 40%, transparent 70%); } - 전환 조건:
chatbot히스토리가 비어있으면 웰컴 뷰 표시, 첫 메시지 전송 시 채팅 뷰로 전환 - 구현:
gr.HTML(elem_id="welcome-view")+ JS로chatbot내 메시지 수 감지 → visibility 토글
D4-3 — Pill 형태 입력창 + 위치 전환
입력창을 완전 둥근 pill 스타일로 변경하고 빈 상태에서는 화면 중앙에 배치
- Pill CSS:
.pill-input textarea { border-radius: 999px !important; padding: 14px 24px !important; } .pill-input { border-radius: 999px !important; } - 중앙 배치 (빈 상태): 웰컴 뷰 안에 별도 입력 컨테이너 배치
position: relative; max-width: 680px; margin: 0 auto;
- 아이콘 통합: 입력창 좌측에
+(첨부), 우측에 전송 화살표 아이콘 - 전환: 첫 전송 후 하단 고정 입력창으로 자연스럽게 전환 (CSS transition)
D4-4 — 예시 질문 Chip 카드 스타일
현재 리스트형 예시 질문을 시안의 수평 카드 chip으로 교체
- 현재:
gr.Examples컴포넌트 (세로 리스트, 레이블 있음) - 변경:
gr.HTML로 직접 렌더링한 수평 chip 카드<div class="example-chips"> <button class="chip" onclick="fillInput('육아휴직 급여 신청 방법을 알려주세요')"> 육아휴직 급여 신청 방법 </button> ... </div> - CSS:
border-radius: 20px; border: 1px solid #e2e8f0; padding: 10px 18px; hover: background #f1f5f9 - JS:
fillInput(text)→ Gradiogr_interface_input_0textarea에 값 설정 후 포커스
D4-5 — 컨트롤 패널 아이콘화
항상 노출된 체크박스·버튼들을 아이콘 버튼으로 숨기고 필요 시 팝업
- 현재 노출 컨트롤: 사고 과정 표시, TTS, 내보내기, 대화 초기화
- 변경: 입력창 우측에
⚙아이콘 → 클릭 시 컨트롤 패널 팝업(드롭업).control-panel { position: absolute; bottom: 60px; right: 0; background: #fff; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,.12); } - 항상 보이는 것: 전송 버튼, 첨부 아이콘, 음성 아이콘
- 숨기는 것: 사고 과정 토글, TTS 토글, 내보내기, 초기화
D4-6 — 대화 이력 사이드바
사이드바에 최근 대화 목록 표시 (시안의 좌측 채팅 이력 섹션)
- 구현:
localStorage에 대화 세션 저장 (최대 20개) - 표시: 사이드바 중간 영역에 최근 대화 제목(첫 질문 앞 20자) 리스트
- 클릭: 해당 세션 복원 (현재는 메모리 기반이라 제목 표시만으로 시작)
- 세션 저장:
respond함수 완료 시localStorage업데이트 (JS)
D4 구현 난이도 및 순서
| 우선순위 | 항목 | 난이도 | 비고 |
|---|---|---|---|
| 1순위 | D4-2 웰컴 뷰 + 글로우 | ★★☆ | CSS + JS만으로 가능 |
| 2순위 | D4-3 Pill 입력창 | ★★☆ | CSS 위주, 중앙 배치는 JS |
| 3순위 | D4-4 Chip 예시 질문 | ★★☆ | gr.Examples → gr.HTML 교체 |
| 4순위 | D4-5 컨트롤 아이콘화 | ★★★ | CSS + JS 팝업 구현 |
| 5순위 | D4-1 사이드바 레이아웃 | ★★★★ | Gradio 레이아웃 대규모 변경 |
| 6순위 | D4-6 대화 이력 | ★★★★ | 세션 관리 + localStorage |
D4 체크리스트
- D4-2: 웰컴 뷰 — 개인화 인사 + 글로우 그라디언트 배경
- D4-3: Pill 입력창 CSS + 중앙/하단 위치 전환
- D4-4: 예시 질문 →
gr.HTMLchip 카드 스타일 - D4-5: 컨트롤 아이콘화 + ⚙ 팝업 패널
- D4-1: 좌측 사이드바 레이아웃 (탭 제거)
- D4-6: 대화 이력 localStorage 저장 + 사이드바 표시
진행 체크리스트
P0
tts_speak()크로스플랫폼 구현 (macOS: say→edge-tts→pyttsx3 / Windows: edge-tts→pyttsx3)requirements.txt—edge-tts>=6.1.9,pyttsx3>=2.90추가.env.example—TTS_EDGE_VOICE=ko-KR-SunHiNeural항목 추가 (TTS_VOICE=Yuna유지)gr.Chatbot— Gradio 6.x 기본 dict 포맷 사용 (type파라미터 불필요, 제거)api_client.pyJSON yield 타입 수정run_id인덱싱 방어 로직 추가- RAG 출처 전용 박스 분리 —
source_boxgr.HTML +_sources_html()+__sources토큰 처리
P1
config.py작성 (APIConfig, AppConfig)api_client.py—APIClientProtocol+HTTPAPIClient분리services/패키지 작성 (chat.py, document.py, tts.py + __init__.py 재익스포트)container.py작성 (lazy singleton 프로퍼티)app.py— 모든 콜백 async 전환 및 container 사용 (asyncio.run()완전 제거)
P2
logging모듈 도입 —basicConfig+LOG_LEVEL환경변수,print()제거- httpx
AsyncClient재사용 —HTTPAPIClient.__init__에서 공유 클라이언트 생성,aclose()추가 - 입력 검증 추가 —
DocumentService.ingest파일 존재·확장자 검증,delete_documentURL 인코딩 tests/단위 테스트 작성 —pytest-asyncio,ChatService4개 /DocumentService6개 (10/10 통과)
P3
- 재시도 로직 —
tenacity/ 5xx·TransportError에만 최대 3회, 지수 백오프(1→8s) /chat제외 5개 메서드 적용 - Pydantic Settings —
config.pydataclass →BaseSettings(flatAppConfig),.env자동 로드 - IoC 프레임워크 전환 — 수동 DI →
dependency-injectorDeclarativeContainer+providers.Singleton