Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
9.6 KiB
5_django_backend — CodeAssist 백엔드 (Django + OpenCode)
프론트(2_frontend)가 기대하는 base-backend 계약을 그대로 구현한 Django 서버. AI 답변은 CodeAssist 전용 OpenCode 서버(opencode/)가 만들고, 여기는 인증·세션 미러·SSE 어댑터를 맡음.
ABAP_OPENCODE web/BE 에서 뼈대(settings·JWT·OpenCode 클라)를 복사해 왔고, 그쪽 서버와는 독립.
CodeAssist(Tauri/React) ──/api/v1──▶ Django(uvicorn :8001) ──HTTP/SSE──▶ opencode serve(:4096) ──▶ LLM
Bearer JWT (localStorage) ├─ accounts 로그인·refresh
├─ chat 세션·검색 미러(SQLite/PG) + /chat/stream 어댑터
└─ opencode/ workspace (AGENTS.md·agent·opencode.json)
띄우기
# 1) OpenCode (터미널 1)
cd 5_django_backend/opencode
export OPENROUTER_API_KEY=sk-or-...
npx -y opencode-ai@1.18.6 serve --hostname 127.0.0.1 --port 4096
# 2) Django (터미널 2)
cd 5_django_backend
python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env # 필요하면 값 수정
.venv/bin/python manage.py migrate # 빈 DB 면 admin@codeassist.local / guest@codeassist.local (test1234) 시드
.venv/bin/uvicorn config.asgi:application --host 0.0.0.0 --port 8001 --reload
# 3) 프론트 (터미널 3) — .env 의 VITE_DEV_API_TARGET 기본이 :8001 이라 그대로
cd 2_frontend && npm run dev
runserver 말고 uvicorn(ASGI) 이어야 SSE 가 버퍼링 없이 흐름.
API (전부 envelope CommonResponse)
| 엔드포인트 | 설명 |
|---|---|
GET /api/v1/health |
상태 (무인증) |
POST /api/v1/auth/login {email,password} |
TokenResponse — access 60분 / refresh 14일 |
POST /api/v1/auth/refresh {refreshToken} |
access 재발급 (refresh 회전 안 함). 없으면 401 REFRESH_TOKEN_MISSING |
POST /api/v1/auth/logout |
null (클라가 토큰 버림) |
GET /api/v1/users/me |
UserResponse |
GET /api/v1/auth/entra/config |
항상 501 — 프론트가 Entra 버튼 숨김 |
GET /api/v1/chat/sessions?page&limit |
내 세션 목록, 최근순 + meta |
POST /api/v1/chat/sessions |
OpenCode 세션 생성 + 미러 |
GET /api/v1/chat/sessions/search?query&page&limit |
내 메시지 본문 검색 |
GET /api/v1/chat/sessions/{id}/messages |
세션 + 메시지 (isGenerating 폴링용) |
POST /api/v1/chat/sessions/{id}/cancel |
OpenCode abort + isGenerating 해제 |
POST /api/v1/chat/stream |
SSE — token{delta}… title{title} usage{used,limit,ratio,elapsed_ms} done{} / error{message,code} |
에러 code: INVALID_CREDENTIALS REFRESH_TOKEN_MISSING REFRESH_TOKEN_INVALID CHAT_GENERATION_IN_PROGRESS(409) UPSTREAM_UNAVAILABLE(503) LLM_ERROR LLM_ABORTED.
스트림이 도는 법 (apps/chat/stream.py)
- Bearer 검사 → 내 세션 →
is_generating이면 409. - user 메시지 미러 저장,
is_generating=true. apps/chat/events.py버스(프로세스당 하나가 OpenCode/event구독) 에 세션 큐 등록 →POST /session/{id}/prompt_async.message.part.updated(text, delta) →token,session.updated제목 →title,session.idle→ OpenCode 에서 최종 메시지·tokens·cost 가져와 assistant 미러 저장 →usage→done.session.error→error+ 부분 답변 저장.- 이 감시는 클라이언트와 별개 태스크라 창을 닫아도 idle 까지 돌고 DB 를 마무리함 → 프론트
isGenerating폴링이 답변 복구.
테스트
.venv/bin/python -m pytest -q # OpenCode 는 가짜(tests/test_stream.py FakeOpencode)
폴더
config/ settings · urls · asgi
common/ envelope.py(응답 포장·CodedError) · opencode_service.py(OpenCode HTTP/SSE)
apps/accounts User(email 로그인) · JWT · 시드
apps/chat models(ChatSession/ChatMessage) · views · events(버스) · stream(어댑터)
opencode/ OpenCode workspace — AGENTS.md · opencode.json · .opencode/agent/codeassist.md
docs-lib/ OpenCode SDK 1.18.6 타입 (API 진실원천)
tests/ pytest
고객사 = 사내 LLM(FabriX)
개발은 OpenRouter 직결이고, 고객사에선 ABAP_OPENCODE 와 같은 방식으로 FabriX 를 씀. OpenCode 는 LLM 을 직접 안 부르고 이 서버의 /api/ito(OpenAI 호환) 로 붙고, apps/gateway 가 FabriX 인증 헤더를 얹어 그대로 흘림.
OpenCode ──OpenAI 호환──▶ Django /api/ito/chat/completions ──x-llm-model-id / x-openapi-token / x-generative-ai-client──▶ FabriX
# 1) .env 에 AAF_FABRIX_* 채움 (.env.example 의 사내 LLM 블록. 키 이름은 ABAP_OPENCODE 와 같음)
# 2) OpenCode 설정을 FabriX 용으로 렌더 (opencode/opencode.json 덮어씀)
.venv/bin/python opencode/render_opencode.py
# 3) OpenCode 는 키 없이 그냥 띄움 (OPENROUTER_API_KEY 불필요)
cd opencode && opencode serve --hostname 127.0.0.1 --port 4096
# 4) 확인
curl http://localhost:8001/api/ito/healthcheck
curl http://localhost:8001/api/ito/models
AAF_FABRIX_MODELS="605:Gemma4,339:GaussO Flash,581:GaussO Think"처럼 두면 OpenCode 화면에서 모델을 고를 수 있고, 고른 id 가 그대로x-llm-model-id로 감.- 고객사 게이트웨이가 토큰 형식(Bearer/날것)·클라이언트 헤더 이름을 서버마다 다르게 받아서, 401 이면 다른 조합을 자동으로 더 시도하고 통과한 걸 기억함.
- 설정이 비어 있어도 서버는 뜨고,
/api/ito/chat/completions만 503 으로 뭐가 빠졌는지 알려줌. - ABAP_OPENCODE 의
apps_ito를 통째로 안 가져온 이유: 그쪽은 11k 줄 + DB 테이블 + langgraph 등 의존성 15개인데 여기 필요한 건 통과 중계뿐. 사용자별 키(IP 마스터)는 아직 없음 — 서비스 키 하나로 감. - 개발로 되돌리기:
git checkout opencode/opencode.json.
SAP 조회 MCP 붙이기 (고객사, -12 에 따로 띄움)
컨테이너끼리(-12↔-13) 통신은 금지라 -13 의 MCP 를 안 빌리고 -12 에 하나 더 띄움. 코드는 ABAP_OPENCODE 의 번들 한 파일(mcp.bundle.cjs) 그대로.
# PC: -13 에서 번들 꺼내 -12 로 (환경값은 -13 bare/.env 의 SAP_*·MCP_API_KEY 를 손으로 옮김)
scp -P 8916 root@10.196.81.34:/www/abap-ito/web/MCP/code/dist/mcp.bundle.cjs .
scp -P 8912 mcp.bundle.cjs root@10.196.81.34:/www/
# -12
cd /www/ABAP_CODE_ASSISTANT/5_django_backend
vi .env # MCP_PORT=8888, MCP_API_KEY, SAP_HOST_ICF/USERNAME/PASSWORD/CLIENT/VERIFY_SSL/API_BASE 채움 (.env.example 의 SAP 블록)
set -a; . ./.env; set +a
mkdir -p /www/mcp && cp /www/mcp.bundle.cjs /www/mcp/
(cd /www/mcp && nohup node mcp.bundle.cjs > /www/ABAP_CODE_ASSISTANT/5_django_backend/logs/mcp.log 2>&1 &)
.venv/bin/python opencode/render_opencode.py # MCP_API_KEY 있으면 sap-icf enabled:true 로 렌더
pkill -f "opencode-bin serve"
(cd opencode && nohup /www/opencode-bin serve --hostname 127.0.0.1 --port 4096 > ../logs/opencode.log 2>&1 &)
# 확인 — 도구 목록에 sap 도구 6개
curl -s -X POST http://127.0.0.1:8888/mcp -H "Authorization: Bearer $MCP_API_KEY" -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 400
- 로컬 개발에선
MCP_API_KEY를 비워두면sap-icf가enabled:false로 렌더돼 OpenCode 가 MCP 를 안 찾음. - MCP 가 zai 모드로 뜨는지는
logs/mcp.log에mode=zai찍히는 걸로. 고객사 SAP 은 zdict 가 아니라 ZAI_ICF 라SAP_API_BASE_ICF를 -13 값과 같게.
ABAP 소스 인덱스 붙이기 (출처 있는 코드)
ABAP_INDEXING 레포의 질의 서버(:8100)를 OpenCode 커스텀 툴로 붙임 — opencode/.opencode/tools/abap_index.ts (툴 3개: abap_index_search·abap_index_chunk·abap_index_source). 에이전트는 코드 질문에 인덱스를 먼저 찾고, 코드 위에 [출처: 프로그램 / unit L줄] 을 적음. 없으면 [출처: 없음 — 생성 코드].
# 인덱스 서버 (ABAP_INDEXING 레포, data/index.db 가 있어야 함)
cd ../ABAP_INDEXING && .venv/Scripts/python -m query.api # 127.0.0.1:8100
# CodeAssist 쪽: .env 의 ABAP_INDEX_URL (기본 그대로면 됨) → OpenCode 재시작. 툴 목록 확인:
curl -s http://127.0.0.1:4096/config 2>/dev/null | grep -o abap_index_[a-z]* | sort -u
- 인덱스 서버가 없으면 툴이 "인덱스 조회 실패"를 돌려주고 에이전트는 생성 코드로 답함. 앱은 안 죽음.
opencode/.opencode/package.json(@opencode-ai/plugin의존)은 그 폴더.gitignore에 걸려 git 에 없음. 고객사엔.opencode/폴더째(node_modules 포함, 약 수 MB) 같이 옮겨야 툴이 로드됨. 없으면 OpenCode 가 툴 파일을 import 못 해 조용히 빠짐 —/experimental/tool/ids로 확인.- 고객사(-12): ABAP_INDEXING 을 -12 에 같이 올리고(
data/index.db포함, 약 14MB) 8100 으로 띄움. 오프라인 wheels 는 fastapi·uvicorn 이 이미 있어 재활용 가능.
고객사 배포 한 방 — deploy.sh
고객사 GHE 저장소의 code-assistant/ 폴더로 관리(브랜치 아님 — -12·-13 이 각자 폴더만 씀). PC 에서 push → -12 에서:
cd /www/repo && git pull
cd code-assistant/5_django_backend && bash deploy.sh
deploy.sh = .env 새 키 채움(있으면 유지) → migrate → seed/snippets.db 있으면 import → OpenCode 설정 렌더 → OpenCode·Django 재시작 → health·모델·툴 확인.
처음 한 번은 .venv·.env·opencode/.opencode/node_modules 를 옛 폴더(/www/ABAP_CODE_ASSISTANT/5_django_backend)에서 복사해 온 뒤 실행.