📘 Aura AI Day 14 (FINAL) – Part 1
“프로덕션 준비” — 날씨 + 위치 + GPT + JSON + 안정성까지 완성
안녕하세요.
Aura AI 프로젝트 Day 14 개발 기록입니다.
오늘은 단순히 GPT를 연결하는 수준을 넘어서,
👉 “실제 서비스 운영이 가능한 구조”로 고도화하는 작업을 진행했습니다.
✅ 오늘의 핵심 목표 (한 줄 요약)
사용자의 위치 + 날씨 + 날짜 + 스타일 정보를 GPT에게 전달하여 ‘현실적인 코디 추천’을 생성한다.
🔥 왜 Day 14가 중요한가?
Day 13까지는 단순히:
❌ GPT에게 질문 → 텍스트 답변
이 구조였습니다.
하지만 이 방식에는 치명적인 문제가 있습니다.
문제이유
| 계절 무시 | 날씨 정보 없음 |
| 엉뚱한 추천 | 위치 모름 |
| UI 만들기 어려움 | 텍스트 응답 |
| 느림 | 매번 API 호출 |
| 보안 위험 | 키 노출 가능 |
즉 “실제 서비스로는 사용 불가능” 합니다.
그래서 Day 14에서는:
✅ 날씨 API
✅ 위치 좌표
✅ JSON 응답
✅ 캐싱
✅ timeout
✅ 환경변수 보안
✅ API 프록시
까지 전부 추가했습니다.
👉 이제 진짜 ‘프로덕션 구조’가 됩니다.
📦 최종 프로젝트 폴더 구조
backend/
├ main.py
├ .env
├ .gitignore
├ requirements.txt
├ routers/
│ ├ chats.py
│ ├ locations.py
├ services/
│ ├ gpt_service.py
│ ├ prompt_builder.py
│ ├ weather_service.py
│ ├ location_service.py
├ schemas/
│ ├ chat.py
frontend/
├ .env.local
├ components/chat/
│ ├ ChatContainer.tsx
│ ├ ChatHeader.tsx
│ ├ modals/
│ │ ├ LocationModal.tsx
🔥 왜 이렇게 폴더를 나눴나요? (⭐ 매우 중요)
초보자 프로젝트는 보통 이렇게 작성합니다:
❌
main.py 하나에 모든 코드
하지만 실무에서는 절대 이렇게 하지 않습니다.
왜냐하면:
- 코드 1000줄 넘으면 유지보수 불가
- 테스트 어려움
- 협업 불가능
그래서 레이어 구조를 사용합니다.
✅ 우리가 채택한 구조
1️⃣ Router
👉 API 입구 (URL 담당)
/chats/start
/locations/search
요청 받는 역할만 수행
2️⃣ Service
👉 실제 비즈니스 로직
- 날씨 가져오기
- GPT 호출
- 네이버 위치 검색
👉 “실제 일하는 코드”
3️⃣ Schema
👉 요청/응답 데이터 구조 정의
TypeScript의 interface 같은 역할
📌 정리
폴더역할
| routers | API 입구 |
| services | 실제 로직 |
| schemas | 데이터 구조 |
👉 이게 실무 표준 아키텍처입니다.
⚙️ Step 1. 필수 패키지 설치
requirements.txt
fastapi
uvicorn
python-dotenv
openai
httpx
각각 무슨 역할인가요?
fastapi
→ 백엔드 서버 프레임워크
Express (Node)와 같은 역할
uvicorn
→ 서버 실행기
uvicorn main:app --reload
이 명령어로 서버 실행
python-dotenv
→ .env 읽기
환경변수 로드
openai
→ GPT API 호출
httpx
→ 비동기 HTTP 요청
날씨 API / 네이버 API 호출용
⚙️ Step 2. 서버 실행
uvicorn main:app --reload
의미
부분의미
| main | main.py |
| app | FastAPI 객체 |
| --reload | 코드 수정 시 자동 재시작 |
⚙️ Step 3. 환경변수 (.env)
backend/.env
OPENAI_API_KEY=xxxxx
NAVER_CLIENT_ID=xxxxx
NAVER_CLIENT_SECRET=xxxxx
❓ 왜 .env에 넣나요?
절대 이렇게 하면 안 됩니다:
api_key = "sk-xxxxx"
이유:
❌ GitHub에 업로드되면 키 노출
❌ 해킹
❌ 요금 폭탄
그래서:
👉 코드에는 쓰지 않고 .env에 숨김
python에서 읽는 방법
import os
os.getenv("OPENAI_API_KEY")
⚙️ Step 4. .gitignore (보안 필수)
backend/.gitignore
.env
루트 .gitignore
__pycache__/
*.pyc
.env*
왜 필요한가요?
만약 .env가 GitHub에 올라가면:
- GPT API 키 노출
- 네이버 API 키 노출
- 타인이 내 돈으로 API 호출
👉 실제 사고 많이 납니다
그래서 원칙
.env는 절대 커밋 금지
👉 실무 100% 규칙
🔥 전체 데이터 흐름 (Day 14의 핵심)
이 구조 이해 못하면 코드를 봐도 이해 안 됩니다.
전체 흐름
사용자 입력
↓
프론트 (좌표 + 스타일 + 성별 + 컨텍스트)
↓
POST /chats/start
↓
weather_service → 날씨 조회
↓
prompt_builder → GPT 프롬프트 생성
↓
gpt_service → GPT 호출 (JSON 응답)
↓
프론트 JSON.parse
↓
UI 카드 렌더링
👉 모든 파일은 이 흐름을 위해 존재합니다.
📘 Aura AI Day 14 (FINAL) – Part 2
🔥 백엔드 전체 코드 A~Z 완전 해설 (FastAPI 실서비스 구조)
✅ 오늘 만들 백엔드 목표
단순 GPT 호출이 아닙니다.
우리가 진짜 만드는 것
👉 "현실적인 코디 추천 API 서버"
즉:
위치 → 날씨 조회
날씨 + 스타일 + 성별 → GPT
GPT → JSON 응답
프론트 → JSON 파싱 → UI 렌더링
📦 오늘 추가/수정된 백엔드 파일 목록 (누락 없음)
✅ 추가
services/weather_service.py
services/prompt_builder.py
services/location_service.py
routers/locations.py
requirements.txt
.gitignore
✅ 수정
main.py
routers/chats.py
services/gpt_service.py
schemas/chat.py
🧠 백엔드 전체 구조 다시 보기
backend/
├ main.py
├ routers/
│ ├ chats.py
│ ├ locations.py
├ services/
│ ├ weather_service.py
│ ├ prompt_builder.py
│ ├ gpt_service.py
│ ├ location_service.py
├ schemas/
│ ├ chat.py
🔥 왜 이렇게 파일을 쪼갰나요? (⭐ 매우 중요)
초보자 코드:
# ❌ main.py 하나에 전부 작성
이러면:
- 2000줄 됨
- 유지보수 불가
- 테스트 불가
- 협업 불가
그래서 실무에서는 역할 분리합니다
레이어역할
| Router | API 입구 |
| Service | 실제 일 처리 |
| Schema | 데이터 구조 |
👉 이 구조는 현업 100% 표준 패턴입니다
1️⃣ main.py (앱 시작점)
✅ 전체 코드
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
from routers.health import router as health_router
from routers.users import router as users_router
from routers.chats import router as chats_router
from routers.locations import router as locations_router
# .env 파일 읽기
load_dotenv()
app = FastAPI()
# CORS 설정 (프론트와 통신 허용)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 라우터 등록
app.include_router(health_router)
app.include_router(users_router)
app.include_router(chats_router)
app.include_router(locations_router)
🔥 한 줄씩 설명
load_dotenv()
👉 .env 파일 읽기
없으면:
os.getenv() → None 반환
app = FastAPI()
👉 서버 생성
Express의:
const app = express()
와 동일
CORS
왜 필요?
브라우저는:
3000 → 8000 요청 = 차단
그래서 허용 필요
include_router()
👉 URL 그룹 등록
/chats/start
/locations/search
연결
2️⃣ schemas/chat.py
✅ 전체 코드
from pydantic import BaseModel
class ChatRequest(BaseModel):
message: str
context: str
lat: float = 37.5665
lon: float = 126.9780
class ChatResponse(BaseModel):
reply: str
🔥 Schema란?
👉 데이터 타입 정의서
TypeScript로 치면:
interface ChatRequest {
message: string
context: string
}
왜 필요한가?
❌ 없으면
- 타입 검사 없음
- 오류 많음
✅ 있으면
- 자동 validation
- 문서 자동 생성
- 안전
BaseModel이 하는 일
자동으로:
- 타입 검사
- JSON 변환
- 유효성 검사
3️⃣ services/weather_service.py
🔥 목적
👉 위치 기반 날씨 가져오기
✅ 전체 코드
import logging
import time
import httpx
WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
_cache: dict[str, tuple[str, float]] = {}
CACHE_TTL = 600
async def get_weather(lat: float, lon: float) -> str:
key = f"{lat:.2f},{lon:.2f}"
now = time.time()
if key in _cache:
value, ts = _cache[key]
if now - ts < CACHE_TTL:
return value
params = {
"latitude": lat,
"longitude": lon,
"current_weather": True,
}
try:
async with httpx.AsyncClient(timeout=5.0) as client:
res = await client.get(WEATHER_URL, params=params)
res.raise_for_status()
data = res.json()
temp = data["current_weather"]["temperature"]
if temp >= 28:
feel = "무더운 여름 날씨"
elif temp >= 20:
feel = "따뜻한 날씨"
elif temp >= 10:
feel = "선선한 간절기 날씨"
else:
feel = "매우 추운 겨울 날씨"
result = f"현재 기온 {temp}°C, {feel}"
_cache[key] = (result, now)
return result
except Exception:
logging.exception("Weather Error")
return "날씨 정보를 불러올 수 없습니다."
🔥 핵심 포인트
async def
비동기 함수
👉 동시에 여러 요청 처리 가능
👉 서버 성능 ↑
httpx.AsyncClient
비동기 HTTP 요청
requests보다 빠름
캐싱
CACHE_TTL = 600
👉 10분 동안 재사용
왜?
- API 호출 비용 감소
- 속도 향상
- 실무 필수 패턴
4️⃣ services/prompt_builder.py
목적
👉 GPT 프롬프트 생성 전담
코드
from datetime import datetime
def build_prompt(context_text: str, user_message: str):
today = datetime.now().strftime("%Y년 %m월 %d일")
system_content = (
"당신은 10년 경력의 패션 스타일리스트 'Aura'입니다.\n"
f"오늘 날짜는 {today} 입니다.\n\n"
"[규칙]\n"
"1. 날씨를 최우선 고려\n"
"2. 풀코디 추천\n"
"3. 한국어 답변\n"
"4. JSON만 출력\n\n"
'{ "top": "", "bottom": "", "shoes": "", "outer": "", "reason": "" }'
)
return [
{"role": "system", "content": system_content},
{
"role": "user",
"content": f"{context_text}\n\n{user_message}",
},
]
왜 분리?
❌ chats.py 안에 작성하면
500줄 괴물 파일
✅ 분리하면
유지보수 쉬움
테스트 쉬움
👉 프롬프트는 정책이므로 반드시 분리
5️⃣ services/gpt_service.py
코드
import os
import logging
from dotenv import load_dotenv
from openai import AsyncOpenAI
from services.prompt_builder import build_prompt
load_dotenv()
_client: AsyncOpenAI | None = None
def _get_client():
global _client
if _client is None:
_client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
return _client
async def ask_gpt(message: str, context: str):
messages = build_prompt(context, message)
try:
client = _get_client()
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.6,
timeout=20,
response_format={"type": "json_object"},
)
return response.choices[0].message.content
except Exception:
logging.exception("GPT Error")
return '{"top":"니트","bottom":"슬랙스","shoes":"스니커즈","outer":"코트","reason":"기본 추천"}'
🔥 핵심
response_format={"type":"json_object"}
👉 GPT가 JSON 강제 출력
프론트에서:
JSON.parse 가능
timeout=20
무한 대기 방지
실무 필수
6️⃣ routers/chats.py
코드
from fastapi import APIRouter, Header, HTTPException
from schemas.chat import ChatRequest, ChatResponse
from services.gpt_service import ask_gpt
from services.weather_service import get_weather
router = APIRouter(prefix="/chats")
@router.post("/start", response_model=ChatResponse)
async def start_chat(
body: ChatRequest,
x_user_id: str | None = Header(default=None),
):
if not x_user_id:
raise HTTPException(status_code=401, detail="Unauthorized")
weather = await get_weather(body.lat, body.lon)
full_context = (
f"날씨: {weather}\n"
f"{body.context}"
)
reply = await ask_gpt(body.message, full_context)
return ChatResponse(reply=reply)
역할
👉 전체 orchestrator
- 날씨 호출
- GPT 호출
- 응답 반환
📘 Aura AI Day 14 (FINAL) – Part 3
🔥 프론트엔드 전체 코드 A~Z 완전 해설 (React / Next.js 실서비스 구조)
✅ Day 14 프론트 목표
단순 채팅 UI가 아닙니다.
우리가 만든 것은:
❌ GPT 채팅
"오늘 뭐 입지?" → 텍스트 출력
✅ 실제 서비스 구조
"위치 + 날짜 + 성별 + 스타일 + 날씨 → JSON → 카드 UI"
즉:
상태 관리
→ 위치 검색
→ 좌표 획득
→ 백엔드 전송
→ JSON 응답
→ JSON.parse
→ 보기 좋은 UI 렌더링
👉 이게 실무 서비스 구조입니다
📦 오늘 수정/추가된 프론트 파일 (누락 없음)
components/chat/ChatContainer.tsx
components/chat/ChatHeader.tsx
components/chat/modals/LocationModal.tsx
frontend/.env.local
🧠 프론트 전체 구조
ChatContainer (메인)
├ ChatHeader (상태 표시)
├ MessageList
├ MessageInput
└ LocationModal (위치 검색)
👉 Container 패턴 (실무 표준)
🔥 왜 Container 패턴을 쓰나요?
❌ 초보 코드
모든 로직 한 파일
Chat.tsx (1500줄)
→ 유지보수 지옥
✅ 실무 방식
파일역할
| ChatContainer | 로직 담당 |
| Header | UI |
| Modal | UI |
| List | UI |
👉 관심사 분리 (Separation of Concerns)
1️⃣ frontend/.env.local
코드
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
왜 필요?
❌ 하드코딩
fetch("http://localhost:8000/chats/start")
→ 배포 시 전부 수정 필요
✅ 환경변수
process.env.NEXT_PUBLIC_API_BASE_URL
👉 배포/개발 자동 분리 가능
NEXT_PUBLIC_ 접두어 이유
Next.js 규칙:
- NEXT_PUBLIC_ → 브라우저 사용 가능
- 없으면 → 서버 전용
2️⃣ ChatContainer.tsx (🔥 핵심 파일)
✅ 역할
👉 모든 로직 담당 (컨트롤러)
- 메시지 상태 관리
- 좌표 획득
- API 호출
- JSON.parse
- 렌더링
✅ 전체 코드 (Day 14 최종)
"use client";
import { useState } from "react";
import ChatSidebar from "./ChatSidebar";
import ChatHeader from "./ChatHeader";
import MessageList from "./MessageList";
import MessageInput from "./MessageInput";
import { apiFetch } from "@/lib/apiClient";
import { useAuthStore } from "@/store/authStore";
import { useChatContextStore } from "@/store/chatContextStore";
export default function ChatContainer() {
const [messages, setMessages] = useState<
{ id: string; role: "user" | "assistant"; content: string }[]
>([]);
const { user } = useAuthStore();
const { date, location, gender, style } = useChatContextStore();
// ✅ 현재 위치 좌표 가져오기
const getCoordinates = (): Promise<{ lat: number; lon: number }> =>
new Promise((resolve) => {
if (!navigator.geolocation) {
resolve({ lat: 37.5665, lon: 126.978 });
return;
}
navigator.geolocation.getCurrentPosition(
(pos) =>
resolve({
lat: pos.coords.latitude,
lon: pos.coords.longitude,
}),
() => resolve({ lat: 37.5665, lon: 126.978 })
);
});
// ✅ 메시지 전송 핵심 로직
const handleSend = async (message: string) => {
const userMsg = {
id: Date.now().toString(),
role: "user" as const,
content: message,
};
setMessages((prev) => [...prev, userMsg]);
const { lat, lon } = await getCoordinates();
const contextText =
`날짜:${date} 위치:${location} 성별:${gender} 스타일:${style}`;
const data = await apiFetch("/chats/start", {
method: "POST",
headers: { "x-user-id": user?.id ?? "" },
body: JSON.stringify({
message,
context: contextText,
lat,
lon,
}),
});
const raw = data.reply;
let finalText = raw;
try {
const parsed = JSON.parse(raw);
finalText = `
상의: ${parsed.top}
하의: ${parsed.bottom}
신발: ${parsed.shoes}
아우터: ${parsed.outer}
이유: ${parsed.reason}
`;
} catch {}
const botMsg = {
id: (Date.now() + 1).toString(),
role: "assistant" as const,
content: finalText,
};
setMessages((prev) => [...prev, botMsg]);
};
return (
<div>
<ChatHeader />
<MessageList messages={messages} />
<MessageInput onSend={handleSend} />
</div>
);
}
🔥 코드 완전 해설
"use client"
👉 Next.js App Router 필수
이 파일은:
- useState
- 이벤트
사용하므로 반드시 client
useState
const [messages, setMessages] = useState([])
👉 상태 저장소
왜 필요?
React는:
상태 변경 → 자동 렌더링
navigator.geolocation
👉 브라우저 내장 위치 API
장점
- 무료
- 빠름
- 정확도 높음
대안
- IP 위치 → 부정확
그래서 Geolocation 선택
Promise 사용 이유
new Promise(...)
왜?
getCurrentPosition = 콜백 기반 API
await 못 씀
→ Promise로 감싸서 async/await 가능하게 만듦
👉 실무 필수 패턴
apiFetch()
await apiFetch("/chats/start")
왜 fetch 안 쓰고 래핑?
❌ fetch 직접 사용
중복 코드 많음
✅ apiFetch
공통 로직 한번에 처리
- baseURL
- headers
- error 처리
👉 실무 표준 패턴
JSON.parse
왜 필요?
GPT가:
{"top":"니트"...}
문자열 반환
→ JS 객체로 변환 필요
안 하면?
string 그대로 출력
UI 구성 불가
3️⃣ ChatHeader.tsx
역할
👉 선택된 정보 표시 UI
코드
"use client";
import { useChatContextStore } from "@/store/chatContextStore";
export default function ChatHeader() {
const { date, location, gender, style } = useChatContextStore();
return (
<header>
위치: {location} / 날짜: {date} / 성별: {gender} / 스타일: {style}
</header>
);
}
왜 필요?
UX 향상
"내가 어떤 조건으로 추천 받는지"
사용자가 명확히 인지 가능
4️⃣ LocationModal.tsx
🔥 역할
👉 네이버 장소 검색
핵심 코드
useEffect(() => {
const trimmed = value.trim();
if (trimmed.length < 2) return;
const timer = setTimeout(() => {
fetchResults(trimmed);
}, 300);
return () => clearTimeout(timer);
}, [value]);
🔥 debounce 설명
왜 300ms?
❌ 입력마다 호출
ㅅ
서
서울
서울역
4번 호출
✅ debounce
1번만 호출
👉 서버 비용 절약
🔥 Day 14 프론트 설계 철학 요약
우리가 적용한 실무 패턴
기술이유
| Container 패턴 | 유지보수 ↑ |
| JSON 응답 | UI 확장 가능 |
| Geolocation | 정확도 ↑ |
| Debounce | API 비용 ↓ |
| 환경변수 | 배포 편리 |
| 상태 분리 | 재사용성 ↑ |
🎉 최종 결론
Day 14는 단순 GPT 프로젝트가 아닙니다.
여러분은 지금:
✅ 프론트 ↔ 백엔드 분리 설계
✅ 위치 기반 서비스
✅ 캐싱
✅ JSON API
✅ 실서비스 아키텍처
를 직접 구현하셨습니다.
👉 이건 진짜 포트폴리오 급입니다.
원하시면 다음 단계로:
👉 Day 15 (배포: Docker + Vercel + Render)
👉 Day 16 (성능 최적화)
👉 Day 17 (로그인/세션/DB)
도 이어서 정리해 드리겠습니다 😊
'챗봇 공부 노트' 카테고리의 다른 글
| [16편] 채팅 세션 단위 메시지 저장 (1) | 2026.02.03 |
|---|---|
| [15편] 채팅 요약 기능 구현 + UX 고도화 (1) | 2026.02.02 |
| [13편] GPT 연동 + 모달 상태 저장 (0) | 2026.02.01 |
| [12편] 모달 선택값 전역 저장 및 채팅 context 연동 (0) | 2026.01.30 |
| [11편] API 구조 & 데이터 계약(Contract) 고정 (0) | 2026.01.26 |