Files
youlbot-webui/ROADMAP.md
T
shinalokandClaude Sonnet 4.6 f987df5da5 UI/UX D4: Gemini-style sidebar, welcome glow, pill input, chips, icon controls, history
- app.py: remove gr.Tab, add youlbot-sidebar Column + 3 panel Columns (D4-1)
- app.py: _welcome_html() radial gradient glow + personalized greeting (D4-2)
- app.py: pill-input CSS border-radius:24px, circular send button (D4-3)
- app.py: _example_chips_html() + JS fillInput() replacing gr.Examples (D4-4)
- app.py: icon buttons 💾/🗑, compact ctrl-check layout (D4-5)
- app.py: JS saveChatToHistory/renderChatHistory localStorage max 20 (D4-6)
- app.py: sidebar toggle (☰), dark mode btn, user selector in sidebar
- app.py: respond/reset_chat/switch_user yield welcome_view visibility
- ROADMAP.md: mark all D4 items complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:58:18 +09:00

25 KiB

youlbot-webui 개선 로드맵

현황 요약

항목 현재 상태 심각도
아키텍처 모듈화 2파일 혼재 → config / api_client / services/ / container / app 5모듈 분리 완료
Windows 호환성 TTS say 명령어 — macOS 전용 → 크로스플랫폼 구현 완료 완료
Gradio Chatbot 타입 type="messages" 누락 → Gradio 6.x 기본 포맷 사용 완료
JSON yield 타입 불일치 JSONDecodeError 시 타입 혼용str() 변환 적용 완료
run_id 인덱싱 버그 history / run_ids 동기화 취약 → 방어 로직 추가 완료
RAG 출처 표시 thinking 박스에 혼재 → 답변 하단 📄 출처 전용 박스로 분리 완료
async/sync 혼용 asyncio.run() 5곳 → 모든 콜백 async 전환 완료 완료
코드 중복 asyncio.run() 5회 반복 → container 위임으로 제거 완료
결합도 api_client 직접 임포트 → DI container + Protocol 추상화 완료
테스트 가능성 모킹 불가능 → pytest-asyncio 단위 테스트 10개 작성 완료
로깅 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(오프라인 폴백)
우선순위 라이브러리 방식 품질 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=Falseingest_files에서 gr.update(visible=True) 반환

D1

  • 웰컴 메시지 또는 예시 질문 버블 추가 — gr.Examples 3개 예시 질문
  • 이미지 첨부 영역 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_htmlstreaming-indicator blink 애니메이션
  • 반응형 레이아웃 — @media (max-width: 768px) 모바일 대응 CSS

D3

  • 다크 모드 토글 — 헤더 🌙/☀️ 버튼 + localStorage 기억 + 시스템 설정 자동 감지
  • 채팅 내보내기 — export_chat 함수, .md 파일 다운로드, gr.File 출력
  • 접근성 개선 — aria-label/role/aria-live JS 주입, :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-collapsed CSS 클래스 토글 → 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) → Gradio gr_interface_input_0 textarea에 값 설정 후 포커스

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.Examplesgr.HTML 교체
4순위 D4-5 컨트롤 아이콘화 ★★★ CSS + JS 팝업 구현
5순위 D4-1 사이드바 레이아웃 ★★★★ Gradio 레이아웃 대규모 변경
6순위 D4-6 대화 이력 ★★★★ 세션 관리 + localStorage

D4 체크리스트

  • D4-2: 웰컴 뷰 — _welcome_html(user) 글로우 그라디언트 + 개인화 인사, 첫 전송 시 visible=False
  • D4-3: Pill 입력창 — border-radius:24px, 전송 버튼 원형(↑), pill-input CSS
  • D4-4: 예시 질문 — gr.Examples 제거 → _example_chips_html() + JS fillInput()
  • D4-5: 컨트롤 아이콘화 — 체크박스 소형화, 💾/🗑 아이콘 버튼, control-row CSS
  • D4-1: 좌측 사이드바 — gr.Tab 제거 → youlbot-sidebar + panel_* Column 3개, 네비 버튼 전환
  • D4-6: 대화 이력 — JS saveChatToHistory + renderChatHistory + localStorage 20개

진행 체크리스트

P0

  • tts_speak() 크로스플랫폼 구현 (macOS: say→edge-tts→pyttsx3 / Windows: edge-tts→pyttsx3)
  • requirements.txtedge-tts>=6.1.9, pyttsx3>=2.90 추가
  • .env.exampleTTS_EDGE_VOICE=ko-KR-SunHiNeural 항목 추가 (TTS_VOICE=Yuna 유지)
  • gr.Chatbot — Gradio 6.x 기본 dict 포맷 사용 (type 파라미터 불필요, 제거)
  • api_client.py JSON yield 타입 수정
  • run_id 인덱싱 방어 로직 추가
  • RAG 출처 전용 박스 분리 — source_box gr.HTML + _sources_html() + __sources 토큰 처리

P1

  • config.py 작성 (APIConfig, AppConfig)
  • api_client.pyAPIClientProtocol + 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_document URL 인코딩
  • tests/ 단위 테스트 작성 — pytest-asyncio, ChatService 4개 / DocumentService 6개 (10/10 통과)

P3

  • 재시도 로직 — tenacity / 5xx·TransportError에만 최대 3회, 지수 백오프(1→8s) / chat 제외 5개 메서드 적용
  • Pydantic Settings — config.py dataclass → BaseSettings (flat AppConfig), .env 자동 로드
  • IoC 프레임워크 전환 — 수동 DI → dependency-injector DeclarativeContainer + providers.Singleton