날짜 2026-07-27 · 작업 대상 kospi-avenger 게임의 뉴스 시스템 백엔드
게임 클라(Godot Android)에서 Azure OpenAI 를 직접 호출하면 안 되는 3가지 이유:
중요. 기존 zuzoo-prod-aoai·Zuzoo-RG 는 프로덕션 서비스라 절대 재사용 안 함. 게임 전용으로 완전 별도 격리.
| 리소스 종류 | 이름 | 리전 | 용도 |
|---|---|---|---|
| Resource Group | kospi-avenger-rg | koreacentral | 게임 전용 격리 컨테이너. 나중에 삭제 시 az group delete 한 번으로 정리. |
| Azure OpenAI 계정 | kospi-avenger-aoai | koreacentral | LLM 호출 대상. Cognitive Services / kind=OpenAI / SKU S0. |
| Model Deployment | gpt-5-nano | - | gpt-5-nano 2025-08-07 GlobalStandard, capacity 30. |
| Storage Account | kospiavengerstore | koreacentral | Function App 필수 종속. 함수 코드 zip · 트리거 메타 · 런타임 락 저장. StandardV2 / LRS. |
| Function App | kospi-news-fn | koreacentral | Node 24 / Windows / Consumption plan. 뉴스 프록시 코드 호스팅. (Linux 도 시도했으나 v4 model 과 조합 시 SCM 컨테이너 죽어 재생성) |
| App Insights | kospi-news-fn | koreacentral | Function App 생성 시 자동 부속. 로그·메트릭 (무료 tier). |
처음에 gpt-4o-mini 를 시도했으나 "deprecating state" 로 신규 배포 불가. gpt-4.1-nano 역시 동일. 최신 세대인 gpt-5-nano 로 확정.
gpt-5-nano 특성:
reasoning_effort 로 내부 추론 강도 조절 (minimal/low/medium/high).temperature 지원 안 함 (제거 필요).max_tokens 대신 max_completion_tokens (reasoning tokens 포함).max_completion_tokens 가 너무 작으면 내부 추론에 다 소진되고 실제 응답이 빈 문자열로 나옴. reasoning_effort: minimal + 4000 이상 권장.az --version # 2.60+
node --version # v20+
npm --version
az account show # 현재 subscription
az cognitiveservices account list # 기존 OpenAI 리소스 파악
npm install -g azure-functions-core-tools@4 --unsafe-perm true
func --version # 4.12+
# 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 사용 권장)
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
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·로그에 남을 수 있음. 위처럼 파일로 파이프.
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
npm run build
func start
# → Functions: generateNews: [POST] http://localhost:7071/api/generateNews
# 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
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)
X-Game-Auth 헤더가 GAME_SHARED_SECRET 과 일치해야 통과. 아니면 401.{ holdings, cash, realized_pnl, week, weekday, ticker_meta, count }. 스키마 불일치 시 400.{ "items": [...] } 강제.reasoning_effort: minimal, max_completion_tokens: 4000, response_format: json_object.items[] 각 원소가 {kind, headline, body, impacts[]}. impacts.code 를 요청된 ticker_meta 안 종목으로만 필터.cd kospi_news_proxynpm run buildfunc start (백그라운드 실행 시 func start &)curl -X POST http://localhost:7071/api/generateNews \
-H "X-Game-Auth: <GAME_SHARED_SECRET>" \
-H "Content-Type: application/json" \
--data @sample_req.json
{
"items": [
{
"kind": "rumor",
"headline": "조선양조간장, AI 발언 인연으로 급등 여부 주목",
"body": "시장에 떠도는 루머성 소문이 확산되며...",
"impacts": [
{ "code": "108860", "fair_value_delta_pct": 1.8,
"reason": "이름 겹침 효과로 인한 단기 매수세 유입" }
]
},
...
],
"usage": { "prompt_tokens": 774, "completion_tokens": 486 }
}
검증 완료. 로컬에서 "조선양조간장 이름 겹침 rumor" 같은 코스피 특유 무빙이 실제로 생성됨. 프롬프트 엔지니어링 유효.
func azure functionapp publish kospi-news-fn --typescriptfunc list · keys list 명령이 400 리턴하는 건 정상.az functionapp function keys list \
--resource-group kospi-avenger-rg \
--name kospi-news-fn \
--function-name generateNews \
--query "default" -o tsv
https://kospi-news-fn.azurewebsites.net/api/generateNews?code=<function-key>
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군데라 전부 갱신 + 재배포 필요.
az storage account keys renew --account-name kospiavengerstore --resource-group kospi-avenger-rg --key key1AzureWebJobsStorage 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
az functionapp restart --name kospi-news-fn --resource-group kospi-avenger-rgWEBSITE_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 유출 방지.
2026-07-27 완료. 게임에서 실제로 kospi-news-fn 을 호출해 뉴스를 받아 시장에 반영하고 UI 에 표시하는 3피스가 배선되었다.
| 파일 | 역할 |
|---|---|
scripts/sim/NewsClient.gd | HTTPRequest 래퍼. .news-key 로드, sim 시간 기반 자동 요청 (게임 내 1시간마다 = 실시간 ~30초). 응답 items 를 Services.news_feed.inject() 에 전달. |
scripts/sim/NewsFeed.gd | inject(items) 메서드 추가. impacts[] 의 fair_value_delta_pct 를 Ticker.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.tscn | SimServices/NewsClient 노드 추가 (NewsFeed 옆). |
scenes/menu/TradingOverlay.tscn | 상단 40px NewsTicker + NewsListOverlay 노드 추가. MainLayout offset_top=40 로 조정. |
.news-key | 클라 자격증명. .gitignore 처리. 템플릿은 .news-key.example. |
.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:// 로 이관 예정.
NewsClient._on_tick(sim_delta) 가 sim_delta 를 누산해 REQUEST_EVERY_SIM_SECONDS=3600 (게임 내 1시간) 마다 request_news() 호출. 게임 하루 = 23400 sim sec 이므로 하루 6-7 배치, 배치당 3-5 뉴스 = 하루 20+ 이벤트 (SIM_SPEC 3-5개/일 기준 상회).Services.news_client.request_news(4) 직접 호출 가능 (디버그·강제 fetch)._inflight 플래그로 응답 대기 중엔 신규 요청 무시.// 요청 (게임 → 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": "..."}] }
]
}
Ticker.fair_value *= (1 + delta_pct/100).price_cap_hi/lo 로 clamp (상한가/하한가 ±30% 준수).Fundamentalist 봇이 매수/매도 → Momentum 봇이 추격 → 실제 가격 이동. SIM_SPEC §4.3 인과 사슬 유지.kospi-avenger-rg 월 $10 등).generateNews.ts 수정 → func azure functionapp publish.az functionapp function keys set 로 정기 교체 (분기 1회). 롤링 후 .news-key 값도 갱신.이 문서는 docs/NEWS_LLM_SETUP.html 에 저장. 프록시 저장소는 게임 리포와 분리 (C:\files\Dev\13.Godot\kospi_news_proxy).