Files
youlbot-webui/ROADMAP.md
T
shinalok e08b43c785 UI/UX D0: fix Textbox label, hide audio accordion, hide footer, hide ingest result box
- app.py: show_label=False on msg_box to remove "Textbox" label
- app.py: wrap audio input in gr.Accordion(open=False) to hide microphone error on load
- app.py: add CSS to hide Gradio footer branding
- app.py: ingest_status starts visible=False; ingest_files returns gr.update(visible=True)
- ROADMAP.md: add UI/UX design improvement section (D0–D3) and mark D0 complete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 14:25:54 +09:00

18 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.Accordion으로 접이식 변경
  • 하단 컨트롤 gr.Row 균등 배치
  • 문서 관리 탭 — 테이블 행 클릭 → 삭제 경로 자동 채움
  • 문서 수집 버튼 크기 적정화

D2

  • 헤더 아이콘/로고 추가
  • gr.themes.Soft() 또는 커스텀 테마 적용
  • 사용자 드롭다운 위치 이동
  • 채팅 버블 커스텀 CSS
  • 스트리밍 로딩 표시

D3

  • 다크 모드 토글
  • 채팅 내보내기 버튼
  • 접근성 개선

진행 체크리스트

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