챗봇 공부 노트

[14편] 날씨 기반 GPT 추천/위치 검색 연동 및 JSON 응답 고도화

frontend-diary-log 2026. 2. 1. 21:44

 

 

📘 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)

도 이어서 정리해 드리겠습니다 😊