10교시 · 웹 인터페이스 연결
2일차 11:00–12:00 · 이론/실습 선행: 9교시(전체 파이프라인 완성)
🎯 학습 목표
- Streamlit으로 업로드→리포트까지의 화면을 만든다.
- app.py가 조립 루트(composition root) 임을 이해한다 — 부품을 생성·연결하는 단 한 곳.
- 제외된 행, 차트, AI 요약을 사용자에게 보여 준다.
📖 개념 1 — 화면은 "가장 바깥의 얇은 껍데기"
9교시 마지막에 손으로 조립한 파이프라인을 기억하세요. 그 코드를 사용자의 클릭으로 실행해 주는 것이 화면(UI)의 전부입니다. 화면은 새로운 로직을 만들지 않습니다.
| 화면(app.py)이 하는 일 | 화면이 하지 않는 일 |
| 파일 업로드 받기 | 매출 계산(유스케이스가 함) |
| 부품 생성·연결(조립) | 데이터 검증(도메인이 함) |
| 결과를 보기 좋게 표시 | 차트 그리기(어댑터가 함) |
| 다운로드 버튼 제공 | LLM 호출 로직(어댑터가 함) |
🔍 관찰 포인트 — 화면을 통째로 바꿔도 안쪽은 그대로입니다. 나중에 Streamlit을 FastAPI(웹 API)나 CLI(명령줄)로 바꾼다고 해도, app.py만 새로 쓰면 됩니다. 도메인·유스케이스·어댑터는 한 줄도 바뀌지 않습니다. 화면이 "얇은 껍데기"일수록 이 교체가 쉬워집니다. 그래서 로직을 화면에 넣지 않는 것이 중요합니다.
📖 개념 2 — 조립 루트(Composition Root)
지금까지 각 부품(어댑터·유스케이스)은 서로의 구체 클래스 이름을 몰랐습니다. 포트(추상)에만 의존했죠. 그렇다면 "누가 진짜 CsvSalesRepository와 OpenAiInsightGateway를 만들어 끼워 넣는가?"
그 일을 하는 단 한 곳이 조립 루트이며, 이 프로젝트에서는 app.py입니다. 조립 루트는:
- 구체 어댑터를 생성하고(new),
- 유스케이스에 주입하고(생성자 인자로 전달),
- 실행 흐름을 시작합니다.
비유하면, 부품들은 "표준 규격(포트)"만 맞춘 채 따로 만들어졌고, 조립 루트가 마지막에 그것들을 한자리에서 결합합니다. 이 분리 덕분에 부품들은 독립적으로 테스트·교체될 수 있었습니다.
💻 실습 — app.py 작성
프로젝트 루트에 app.py를 만듭니다.
from __future__ import annotations
import tempfile # 업로드 파일을 임시 경로에 저장할 때 사용
from pathlib import Path
import streamlit as st
# 조립에 필요한 구체 어댑터·유스케이스·인프라를 모두 import 한다.
# (이 파일이 '조립 루트'이므로, 여기서만 구체 클래스를 알아도 된다.)
from sales_agent.adapters.csv_repository import CsvSalesRepository
from sales_agent.usecases.analyze_sales import AnalyzeSalesUseCase
from sales_agent.adapters.chart_renderer import MatplotlibChartRenderer
from sales_agent.adapters.llm_gateway import OpenAiInsightGateway
from sales_agent.usecases.generate_report import GenerateReportUseCase
from sales_agent.infra.openai_client import make_openai_client
# ── 조립 루트: 구체 부품을 생성·연결하는 '단 한 곳' ────────────────────
def build_pipeline():
"""구체 어댑터를 만들어 리포트 유스케이스에 주입하고, 그 유스케이스를 돌려준다.
모델 교체·차트 구현 교체 등 '무엇을 끼울지'에 대한 결정이 이 함수에 모인다.
"""
charts = MatplotlibChartRenderer() # 차트 어댑터 생성
insight = OpenAiInsightGateway(client=make_openai_client(), # LLM 어댑터 생성
model="gpt-4.1-mini") # (진짜 클라이언트 주입)
report_usecase = GenerateReportUseCase(charts, insight) # 두 어댑터를 유스케이스에 주입
return report_usecase
# ── 화면(가장 바깥의 얇은 껍데기) ─────────────────────────────────────
st.set_page_config(page_title="매출 인사이트 에이전트", layout="wide") # 페이지 기본 설정
st.title("📊 매출 인사이트 에이전트")
st.caption("매출 CSV를 올리면 지표·차트·AI 요약 리포트를 만들어 드립니다.")
uploaded = st.file_uploader("매출 CSV 업로드", type=["csv"]) # 파일 업로드 위젯(csv만 허용)
if uploaded is not None: # 사용자가 파일을 올렸다면
# 어댑터는 '파일 경로'를 입력으로 받으므로, 업로드된 바이트를 임시 파일로 저장한다.
# delete=False: with 블록을 빠져나가도 파일을 유지(이후 어댑터가 읽어야 하므로).
with tempfile.NamedTemporaryFile(delete=False, suffix=".csv") as tmp:
tmp.write(uploaded.getvalue()) # 업로드 내용을 임시 파일에 기록
tmp_path = tmp.name # 임시 파일 경로 확보
repo = CsvSalesRepository(tmp_path) # 그 경로로 CSV 어댑터 생성
if st.button("분석 실행", type="primary"): # 버튼을 누르면 분석 시작
with st.spinner("분석 중입니다..."): # 처리 중 스피너 표시
try:
result = AnalyzeSalesUseCase(repo).execute() # 적재+분석 실행
except ValueError as e:
# 도메인/유스케이스가 던진 오류(예: 유효 행 0개)를 사용자 메시지로 변환
st.error(f"분석할 수 없습니다: {e}")
st.stop() # 이후 코드 실행을 멈춰, 잘못된 상태로 진행하지 않게 한다
# 차트·리포트는 임시 폴더 아래에 생성한다.
out_dir = str(Path(tempfile.gettempdir()) / "sales_report")
report, md_path = build_pipeline().execute(result, out_dir) # 조립 루트로 리포트 생성
# Streamlit은 매 상호작용마다 스크립트를 처음부터 재실행한다.
# 결과를 session_state(세션 저장소)에 넣어야 다음 렌더링에서도 살아남는다.
st.session_state["report"] = report
st.session_state["md_path"] = md_path
st.session_state["skipped"] = repo.skipped # 제외된 행 보고도 함께 보관
# ── 결과 표시(세션에 결과가 있을 때만) ───────────────────────────────
if "report" in st.session_state:
report = st.session_state["report"]
r = report.result # 분석 결과(숫자) 묶음
# 핵심 지표 3개를 한 줄(3개 컬럼)로 표시
col1, col2, col3 = st.columns(3)
col1.metric("총매출", f"{r.total_revenue:,.0f}원")
col2.metric("총수량", f"{r.total_quantity:,}개")
col3.metric("기간", f"{r.period_start} ~ {r.period_end}")
st.subheader("🧠 AI 인사이트")
st.write(report.summary) # LLM이 만든 요약 문장
st.subheader("📈 차트")
for p in report.chart_paths:
st.image(p) # 생성된 차트 이미지를 화면에 표시
# 제외된 행이 있으면 사유와 함께 안내(6교시에서 기록해 둔 skipped)
skipped = st.session_state.get("skipped", [])
if skipped:
st.subheader("⚠️ 제외된 행")
st.write(f"{len(skipped)}건이 분석에서 제외되었습니다.")
for s in skipped:
st.text(f"- {s.line}행: {s.reason}")
# 리포트 마크다운 파일을 읽어 다운로드 버튼으로 제공
with open(st.session_state["md_path"], encoding="utf-8") as f:
st.download_button("📥 리포트(.md) 다운로드", f.read(),
file_name="sales_report.md")
🔍 관찰 포인트 1 — build_pipeline()이 유일한 조립 지점입니다. 구체 어댑터(MatplotlibChartRenderer, OpenAiInsightGateway)를 생성하는 코드가 이 함수 한 곳에만 있습니다. 모델을 바꾸거나 차트 구현을 교체하려면 여기만 손대면 됩니다. 화면 코드 곳곳에 new가 흩어지지 않게 모은 것입니다.
🔍 관찰 포인트 2 — st.session_state가 필요한 이유. Streamlit은 사용자가 버튼을 누를 때마다 스크립트 전체를 처음부터 다시 실행합니다. 그래서 분석 결과를 그냥 변수에 두면 다음 클릭에서 사라집니다. st.session_state(세션 저장소)에 넣어야 재실행 사이에도 결과가 유지됩니다. 이는 Streamlit의 실행 모델을 이해해야 보이는 함정입니다.
🔍 관찰 포인트 3 — 제외된 행을 사용자에게 보여 줍니다. 6교시에서 skipped에 기록해 둔 제외 사유를 화면에 표시합니다. 샘플 데이터의 6번째 행(수량 0)이 "7행: quantity must be > 0"으로 나타나면, 데이터 처리 → 보고까지의 흐름이 끝까지 이어진 것입니다.
💻 실습 — 실행
uv run streamlit run app.py
브라우저가 자동으로 열립니다(보통 http://localhost:8501). 열리지 않으면 터미널에 표시된 주소를 직접 입력하세요.
수동 확인 체크리스트
사용자 입장에서 다음을 직접 눌러 확인합니다.
- [ ] sample_data/sales_sample.csv를 업로드할 수 있다
- [ ] "분석 실행"을 누르면 총매출·총수량·기간이 표시된다
- [ ] AI 요약 문장이 나타난다
- [ ] 상위 상품·월별 추이 차트가 보인다(한글 안 깨짐)
- [ ] "제외된 행"에 7행(수량 0)이 사유와 함께 표시된다
- [ ] 리포트(.md)를 다운로드할 수 있다
🔍 관찰 포인트 — UI는 자동 테스트 대신 수동 확인으로 검증합니다. 화면의 시각적 동작은 자동 테스트로 일일이 검증하기 번거롭습니다. 대신 핵심 로직은 이미 6~9교시에서 단위 테스트로 검증되어 있으므로, 화면은 "조립과 표시가 맞는가"만 사람이 눈으로 확인하면 충분합니다. 테스트하기 어려운 부분(UI)을 최대한 얇게 유지한 설계가 여기서 빛을 발합니다.
✏️ 미니 실습 — 잘못된 파일로 동작 확인
수량이 전부 0인 CSV를 임시로 만들어 업로드해 보세요. "분석할 수 없습니다: 분석할 매출 레코드가 없습니다."라는 안내가 뜨고, 앱이 죽지 않으면 정상입니다.
🔍 관찰 포인트: 7교시에서 만든 ValueError(빈 레코드)가 화면에서 친절한 오류 메시지로 변환됩니다. 오류를 도메인에서 던지고, 화면에서 사용자 언어로 번역하는 흐름입니다.
🔒 진행 게이트
- 제출물(기록): 잘못된 행을 섞은 CSV 업로드 시 화면에 표시된 제외 행 수·사유(또는 빈 데이터의 안내 메시지).
- 이 결과가 있어야 다음 교시로 넘어갑니다.
- 정답·해설(별도 파일): solutions/미니실습정답_10.md
✅ 체크포인트
- [ ] 업로드→분석→표시→다운로드가 화면에서 동작한다
- [ ] build_pipeline()이 유일한 조립 지점이다
- [ ] st.session_state로 결과가 재실행 사이에 유지된다
- [ ] 제외된 행이 사유와 함께 표시된다
- [ ] 잘못된 입력에도 앱이 죽지 않고 안내한다
🛠️ 트러블슈팅
| 증상 | 원인 | 해결 |
| streamlit: 인식되지 않습니다 | 직접 실행 시도 | 반드시 uv run streamlit run app.py |
| 버튼 누르면 결과 사라짐 | session_state 미사용 | 결과를 st.session_state에 저장 |
| 차트 이미지 안 보임 | 경로 문제 | st.image(절대경로) 확인 |
| OpenAI 오류 | 키/크레딧 | 9교시 .env 점검 |
| 업로드 파일 읽기 실패 | 임시 저장 누락 | tempfile로 저장 후 경로 전달 |
🔑 핵심 정리
- 화면은 가장 바깥의 얇은 껍데기 — 새 로직 없이 조립·표시만 한다.
- app.py는 조립 루트로, 구체 어댑터를 만들어 유스케이스에 주입하는 유일한 곳이다.
- 핵심 로직은 이미 단위 테스트로 검증돼 있어, 화면은 수동 확인으로 충분하다.
다음 교시: 완성된 시스템의 구조를 점검하고, 아키텍처 규칙·커버리지·통합 테스트로 안정화합니다.
'AI System > 클린 아키텍처와 바이브 코딩을 활용한 AI Agent 시스템의 설계와 구' 카테고리의 다른 글
| d02 - 12. 바이브 코딩 도입과 빠른 재현 (0) | 2026.06.23 |
|---|---|
| d02 - 11. 구조안정화 (0) | 2026.06.23 |
| d02 - 9. 결과생성 파이프라인 (0) | 2026.06.23 |
| d02 - 8. 시각화와 출력 설계 (1) | 2026.06.23 |
| d01 - 클린 아키텍처 개념과 이점 (0) | 2026.06.22 |