뉴스 LLM 프록시 — Azure Functions 셋업 기록

날짜 2026-07-27 · 작업 대상 kospi-avenger 게임의 뉴스 시스템 백엔드

목차

1. 왜 중계 서버가 필요한가

게임 클라(Godot Android)에서 Azure OpenAI 를 직접 호출하면 안 되는 3가지 이유:

  1. API 키 유출 = 결제 폭탄. APK 디컴파일로 키 추출 → 제3자가 무제한 사용. Azure OpenAI 는 요청당 과금이라 크레딧 소진 위험.
  2. 프롬프트 튜닝 사이클. "미친 무빙" 로직·프롬프트를 서버에 두면 앱 재배포 없이 실시간 튜닝. 클라에 두면 APK 새로 올려야 함.
  3. Rate limit·캐싱 제어. 유저별 quota, 유사 시장 상태 응답 재사용 → 비용 절감. 서버 없이는 통제 불가.

2. 최종 아키텍처

Game Client (Godot Android) │ POST /api/generateNews │ Headers: X-Game-Auth: <shared secret> │ Body: { holdings, cash, week, weekday, ticker_meta, count } │ ▼ Azure Function App (kospi-news-fn.azurewebsites.net) │ Node.js 20 TypeScript · Consumption Plan · Korea Central │ ├─ Auth: X-Game-Auth 헤더 검증 (shared secret 매칭) │ ├─ Zod 요청 스키마 검증 │ ├─ 프롬프트 조립 (system + user JSON payload) │ └─ Azure OpenAI 호출 │ ▼ Azure OpenAI (kospi-avenger-aoai.openai.azure.com) Deployment: gpt-5-nano (2025-08-07, GlobalStandard) ├─ reasoning_effort: minimal ├─ max_completion_tokens: 4000 └─ response_format: json_object │ ▼ 응답: { items: [ {kind, headline, body, impacts: [...]}, ... ] } │ ▼ Function App 후처리: ├─ Zod 응답 스키마 검증 ├─ impacts.code 를 ticker_meta 안에 있는 것만 필터 └─ 200 JSON 응답 → Game Client

3. 생성된 Azure 리소스 (전부 신규, 프로덕션 격리)

중요. 기존 zuzoo-prod-aoai·Zuzoo-RG 는 프로덕션 서비스라 절대 재사용 안 함. 게임 전용으로 완전 별도 격리.

리소스 종류이름리전용도
Resource Groupkospi-avenger-rgkoreacentral게임 전용 격리 컨테이너. 나중에 삭제 시 az group delete 한 번으로 정리.
Azure OpenAI 계정kospi-avenger-aoaikoreacentralLLM 호출 대상. Cognitive Services / kind=OpenAI / SKU S0.
Model Deploymentgpt-5-nano-gpt-5-nano 2025-08-07 GlobalStandard, capacity 30.
Storage AccountkospiavengerstorekoreacentralFunction App 필수 종속. 함수 코드 zip · 트리거 메타 · 런타임 락 저장. StandardV2 / LRS.
Function Appkospi-news-fnkoreacentralNode 24 / Windows / Consumption plan. 뉴스 프록시 코드 호스팅. (Linux 도 시도했으나 v4 model 과 조합 시 SCM 컨테이너 죽어 재생성)
App Insightskospi-news-fnkoreacentralFunction App 생성 시 자동 부속. 로그·메트릭 (무료 tier).

모델 선택 히스토리

처음에 gpt-4o-mini 를 시도했으나 "deprecating state" 로 신규 배포 불가. gpt-4.1-nano 역시 동일. 최신 세대인 gpt-5-nano 로 확정.

gpt-5-nano 특성:

4. 사용된 명령어 순서 (재현 가능)

4-1. 사전 확인

az --version           # 2.60+
node --version         # v20+
npm --version
az account show        # 현재 subscription
az cognitiveservices account list  # 기존 OpenAI 리소스 파악

4-2. Azure Functions Core Tools 설치

npm install -g azure-functions-core-tools@4 --unsafe-perm true
func --version         # 4.12+

4-3. Azure 리소스 생성

# Resource Group
az group create --name kospi-avenger-rg --location koreacentral

# Azure OpenAI 계정
az cognitiveservices account create \
  --name kospi-avenger-aoai \
  --resource-group kospi-avenger-rg \
  --location koreacentral \
  --kind OpenAI \
  --sku S0 \
  --yes

# 모델 배포 (gpt-5-nano)
az cognitiveservices account deployment create \
  --name kospi-avenger-aoai \
  --resource-group kospi-avenger-rg \
  --deployment-name gpt-5-nano \
  --model-name gpt-5-nano \
  --model-version "2025-08-07" \
  --model-format OpenAI \
  --sku-capacity 30 \
  --sku-name GlobalStandard

# Storage Account (Function App 필수 종속)
az storage account create \
  --name kospiavengerstore \
  --resource-group kospi-avenger-rg \
  --location koreacentral \
  --sku Standard_LRS \
  --kind StorageV2

# Function App — Windows Consumption 필수 (Linux + Node v4 조합 SCM 죽음 이슈)
az functionapp create \
  --name kospi-news-fn \
  --resource-group kospi-avenger-rg \
  --consumption-plan-location koreacentral \
  --runtime node \
  --runtime-version 24 \
  --functions-version 4 \
  --storage-account kospiavengerstore
  # ← --os-type 명시하지 않으면 Windows (Windows 사용 권장)

4-4. 프록시 프로젝트 스캐폴드 (로컬)

mkdir kospi_news_proxy && cd kospi_news_proxy
func init . --worker-runtime node --language typescript --model V4
func new --template "HTTP trigger" --name generateNews --authlevel function
npm install openai zod
git init

4-5. local.settings.json 작성 (개발용, 커밋 X)

KEY=$(az cognitiveservices account keys list \
  --name kospi-avenger-aoai --resource-group kospi-avenger-rg \
  --query "key1" -o tsv)
ENDPOINT=$(az cognitiveservices account show \
  --name kospi-avenger-aoai --resource-group kospi-avenger-rg \
  --query "properties.endpoint" -o tsv)

cat > local.settings.json <<EOF
{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AZURE_OPENAI_ENDPOINT": "${ENDPOINT}",
    "AZURE_OPENAI_KEY": "${KEY}",
    "AZURE_OPENAI_DEPLOYMENT": "gpt-5-nano",
    "AZURE_OPENAI_API_VERSION": "2025-01-01-preview",
    "GAME_SHARED_SECRET": "<32바이트 랜덤 base64>"
  },
  "Host": { "CORS": "*" }
}
EOF

절대 스크린으로 KEY 출력 금지. 셸 히스토리·transcript·로그에 남을 수 있음. 위처럼 파일로 파이프.

4-6. Shared Secret 생성

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

4-7. 로컬 빌드 + 실행

npm run build
func start
# → Functions: generateNews: [POST] http://localhost:7071/api/generateNews

4-8. Azure App Settings 등록 + 코드 배포

# App Settings (환경 변수) — 다시 stdout 노출 없이
KEY=$(az cognitiveservices account keys list --name kospi-avenger-aoai --resource-group kospi-avenger-rg --query "key1" -o tsv)
ENDPOINT=$(az cognitiveservices account show --name kospi-avenger-aoai --resource-group kospi-avenger-rg --query "properties.endpoint" -o tsv)

az functionapp config appsettings set \
  --name kospi-news-fn --resource-group kospi-avenger-rg \
  --settings \
    AZURE_OPENAI_ENDPOINT="$ENDPOINT" \
    AZURE_OPENAI_KEY="$KEY" \
    AZURE_OPENAI_DEPLOYMENT="gpt-5-nano" \
    AZURE_OPENAI_API_VERSION="2025-01-01-preview" \
    GAME_SHARED_SECRET="<프로덕션 새 시크릿>" \
  -o none

# 코드 배포
cd kospi_news_proxy
func azure functionapp publish kospi-news-fn --typescript

# Function key 조회 (배포 후 1-3분 대기 필요)
az functionapp function keys list \
  --resource-group kospi-avenger-rg \
  --name kospi-news-fn \
  --function-name generateNews \
  --query "default" -o tsv

5. 프록시 프로젝트 구조

kospi_news_proxy/
├── .gitignore                       # local.settings.json 자동 포함
├── package.json                     # openai, zod deps
├── tsconfig.json
├── host.json                        # Functions v4 런타임 설정
├── local.settings.json              # 로컬 env vars (커밋 X)
├── src/
│   └── functions/
│       └── generateNews.ts          # 유일한 함수 — HTTP POST 트리거
└── dist/                            # TypeScript 빌드 산출물 (커밋 X)

generateNews.ts 요약

6. 로컬 실행 방법

  1. cd kospi_news_proxy
  2. npm run build
  3. func start (백그라운드 실행 시 func start &)
  4. 테스트:
    curl -X POST http://localhost:7071/api/generateNews \
      -H "X-Game-Auth: <GAME_SHARED_SECRET>" \
      -H "Content-Type: application/json" \
      --data @sample_req.json
  5. 응답:
    {
      "items": [
        {
          "kind": "rumor",
          "headline": "조선양조간장, AI 발언 인연으로 급등 여부 주목",
          "body": "시장에 떠도는 루머성 소문이 확산되며...",
          "impacts": [
            { "code": "108860", "fair_value_delta_pct": 1.8,
              "reason": "이름 겹침 효과로 인한 단기 매수세 유입" }
          ]
        },
        ...
      ],
      "usage": { "prompt_tokens": 774, "completion_tokens": 486 }
    }

검증 완료. 로컬에서 "조선양조간장 이름 겹침 rumor" 같은 코스피 특유 무빙이 실제로 생성됨. 프롬프트 엔지니어링 유효.

7. Azure 배포 방법

  1. App Settings 등록 완료 (§4-8 참조).
  2. 코드 배포: func azure functionapp publish kospi-news-fn --typescript
  3. 초기화 대기 (Consumption plan 첫 배포 시 1-3분). 이 기간에 func list · keys list 명령이 400 리턴하는 건 정상.
  4. Function key 조회:
    az functionapp function keys list \
      --resource-group kospi-avenger-rg \
      --name kospi-news-fn \
      --function-name generateNews \
      --query "default" -o tsv
  5. 원격 엔드포인트:
    https://kospi-news-fn.azurewebsites.net/api/generateNews?code=<function-key>

8. 알려진 quirk 와 대처

gpt-4o-mini / gpt-4.1-* 신규 배포 불가: 오늘 (2026-07) 기준 "deprecating state" 로 신규 deployment 거부. gpt-5-nano/gpt-5-mini 등 최신 세대 사용.

reasoning 모델 API 차이: temperature · max_tokens 지원 안 함. 각각 제거 + max_completion_tokens 사용. reasoning_effort: minimal 안 주면 응답이 빈 문자열로 나옴.

Linux Consumption + Node v4 model = SCM 컨테이너 죽음: 처음 --os-type Linux 로 생성 후 배포하면 upload 성공 · sync-triggers BadRequest · 엔드포인트 503 무한 지속. 10분 이상 대기해도 안 살아남. Application Insights 로그도 못 붙임 (Kudu 자체가 죽음). 해결: Windows Consumption 으로 재생성--os-type 을 생략 (Windows 가 기본). 재배포 시 즉시 정상 작동.

sync triggers 지연 (Windows 에서도 가능): func azure functionapp publish 후 "Error calling sync triggers" 나오는 경우 있음. Windows 는 보통 1-2분 내 해결. Linux 는 §위 참조 (해결 안 됨, Windows 재생성 필요).

Node 24 필수: Node 20 은 2026-04-30 EOL 로 --runtime-version 20 불가. 24 사용. 로컬 개발도 24 로 통일 권장 (현재 v22 도 호환).

2026-07-27 최종 확인: Windows Consumption 재배포 후 원격 엔드포인트 HTTP 200, 6.9초 응답. 요청/응답 프롬프트 776/486 토큰 = 약 $0.0002 per call (gpt-5-nano 기준). 조선양조간장 이름 겹침 rumor 도 정상 생성.

Storage key 롤링 시 반드시 4단계: az storage account keys renew 만 하면 Function App 이 죽는다. Function App 이 storage 를 참조하는 곳이 3군데라 전부 갱신 + 재배포 필요.

  1. az storage account keys renew --account-name kospiavengerstore --resource-group kospi-avenger-rg --key key1
  2. AzureWebJobsStorage App Setting 을 새 key 로 재설정 (연결 문자열 형식):
    STORAGE_KEY=$(az storage account keys list --account-name kospiavengerstore --resource-group kospi-avenger-rg --query "[0].value" -o tsv)
    CONN="DefaultEndpointsProtocol=https;AccountName=kospiavengerstore;AccountKey=$STORAGE_KEY;EndpointSuffix=core.windows.net"
    az functionapp config appsettings set --name kospi-news-fn --resource-group kospi-avenger-rg \
      --settings AzureWebJobsStorage="$CONN" WEBSITE_CONTENTAZUREFILECONNECTIONSTRING="$CONN" -o none
  3. az functionapp restart --name kospi-news-fn --resource-group kospi-avenger-rg
  4. 재배포 필수WEBSITE_RUN_FROM_PACKAGE 는 예전 key 로 서명된 blob SAS URL 이라 무효화됨. 재배포로 새 SAS 생성:
    cd kospi_news_proxy && func azure functionapp publish kospi-news-fn --typescript

4번을 잊으면 HTTP 404 (endpoint 등록 안 됨) 무한 지속. 재배포 후 HTTP 200.

Shared secret 롤링: App Setting 갱신 후에는 Function App 이 자동으로 새 값 로드 (재시작 불필요). 하지만 로컬 local.settings.json 과 Godot 클라 embed 값도 동시 갱신 필수 (그렇지 않으면 401 unauthorized).

local.settings.json 은 반드시 .gitignore. func init 이 자동 추가하지만 확인 필수. Azure 키·shared secret 유출 방지.

9. Godot 클라이언트 연동 (완료)

2026-07-27 완료. 게임에서 실제로 kospi-news-fn 을 호출해 뉴스를 받아 시장에 반영하고 UI 에 표시하는 3피스가 배선되었다.

9-1. 파일 인벤토리

파일역할
scripts/sim/NewsClient.gdHTTPRequest 래퍼. .news-key 로드, sim 시간 기반 자동 요청 (게임 내 1시간마다 = 실시간 ~30초). 응답 items 를 Services.news_feed.inject() 에 전달.
scripts/sim/NewsFeed.gdinject(items) 메서드 추가. impacts[] 의 fair_value_delta_pctTicker.fair_value 에 즉시 적용. KIND_MAP 으로 rumor→찌라시 · disclosure→공시 · macro→거시 · sector→섹터 매핑. history 리스트 유지 (max 40).
scripts/trading/ui/NewsTicker.gd상단 40px Button. news_broadcast 구독, 헤드라인 마퀴 스크롤 (60px/sec), 탭 시 news_list_requested emit.
scripts/trading/ui/overlays/NewsListOverlay.gd전체화면 오버레이. NewsFeed.history 를 최신순 렌더. kind 필터 (전체/찌라시/공시/섹터/거시). 각 row 에 impact 종목·delta 태그 노출.
scenes/Main.tscnSimServices/NewsClient 노드 추가 (NewsFeed 옆).
scenes/menu/TradingOverlay.tscn상단 40px NewsTicker + NewsListOverlay 노드 추가. MainLayout offset_top=40 로 조정.
.news-key클라 자격증명. .gitignore 처리. 템플릿은 .news-key.example.

9-2. .news-key 셋업 (사용자 로컬)

# .news-key.example 을 복사한 뒤 값 채워넣기
cp .news-key.example .news-key

# .news-key 형식:
{
  "endpoint": "https://kospi-news-fn.azurewebsites.net/api/generateNews",
  "function_key": "<az functionapp function keys list ... 조회>",
  "shared_secret": "<App Setting GAME_SHARED_SECRET 과 동일>"
}

# function key 조회
az functionapp function keys list --name kospi-news-fn --resource-group kospi-avenger-rg --function-name generateNews

# shared secret 조회 (App Setting 에 저장된 값)
az functionapp config appsettings list --name kospi-news-fn --resource-group kospi-avenger-rg --query "[?name=='GAME_SHARED_SECRET'].value" -o tsv
보안 주의. .news-key.gitignore 에 등록되어 있지만 실수로 커밋되지 않는지 항상 git status 로 확인. 실기기 배포 시 (v0.5 β) 는 user:// 로 이관 예정.

9-3. 트리거 정책

9-4. 요청 / 응답 스키마

// 요청 (게임 → Azure Function)
{
  "holdings": { "005930": { "qty": 10, "avg_price": 70000 } },
  "cash": 9300000,
  "realized_pnl": 0,
  "week": 1,
  "weekday": 0,
  "ticker_meta": {
    "005930": {"display_name": "한성전자", "sector": "반도체/IT", "archetype": "large_cap", "reference_price": 70000},
    ... 20 종목
  },
  "count": 4
}

// 응답 (Azure Function → 게임)
{
  "items": [
    { "kind": "rumor" | "disclosure" | "macro" | "sector",
      "headline": "한줄 헤드라인",
      "body": "본문 요약",
      "impacts": [{"code": "005930", "fair_value_delta_pct": 3.5, "reason": "..."}] }
  ]
}

9-5. 시장 반영 로직

9-6. 운영 체크리스트


이 문서는 docs/NEWS_LLM_SETUP.html 에 저장. 프록시 저장소는 게임 리포와 분리 (C:\files\Dev\13.Godot\kospi_news_proxy).