14 KiB
CodeAssist 백엔드를 OpenCode + Django 로 — 분석과 인프라 제안
작성 2026-09-16. 코드 변경 전 분석 단계. 확정되면 /speckit-specify 로 specs/006-* 파고 여기 내용은 research 로 승계.
0. 한 줄 결론
프론트 계약은 그대로 두고, ABAP_OPENCODE 의 Django(web/BE) 안에 CodeAssist 전용 어댑터 앱을 하나 얹는다.
Django 가 OpenCode 서버를 호출하고, OpenCode 의 이벤트 스트림을 프론트가 이미 아는 POST /chat/stream SSE(token/title/usage/done/error)로 바꿔준다.
프론트 변경은 env 한 줄 + 토큰 저장 두 줄. 나머지는 전부 서버 쪽.
1. 지금 상태 — 프론트가 백엔드에 기대하는 것
출처: 2_frontend/src/lib/api/client.ts, lib/streaming/streamLLM.ts, features/snap/api/*, features/auth/api/auth.api.ts, types/api.ts, specs/005-*/contracts/chat-stream.md, docs/superpowers/specs/2026-07-18-snap-backend-connect-design.md.
현재 백엔드는 별도 repo 의 FastAPI(base-backend, localhost:8001). 이 머신엔 없음.
| 영역 | 프론트가 기대하는 것 |
|---|---|
| 베이스 | VITE_API_BASE_URL (기본 /api/v1, 풀 URL 도 됨). dev 는 Vite proxy 로 same-origin |
| 응답 모양 | 전부 envelope CommonResponse{success,statusCode,code,message,data,counts,errors,timestamp,meta}. 헬퍼가 .data 언랩, apiList 는 meta{currentPage,pageSize,totalItems,totalPages,hasNextPage,hasPreviousPage} 로 페이지네이션. success:false 면 ApiError |
| 인증 | httpOnly 쿠키(accessToken/refreshToken) + 401 → POST /auth/refresh 자동 재시도. Bearer seam 이미 있음 — lib/auth/tokenProvider.ts 에 토큰 넣으면 axios·SSE 둘 다 Authorization: Bearer 로 감 |
| 인증 API | POST /auth/login{email,password} → TokenResponse{token,refreshToken,…,user} (프론트는 .user 만 씀) · POST /auth/refresh · POST /auth/logout · GET /users/me · Entra POST /auth/entra/login, GET /auth/entra/config (501 이면 버튼 숨김) |
| 세션 | GET /chat/sessions?page&limit (page=3개 peek) · POST /chat/sessions · GET /chat/sessions/search?query&page&limit (메시지 본문 ILIKE) · GET /chat/sessions/{id}/messages (isGenerating true 면 1.5초 폴링) · POST /chat/sessions/{id}/cancel |
| DTO | SnapSession{id,title,titleLlm,isGenerating,createdAt,updatedAt} · SnapMessage{sessionId,role,content,createdAt,inputTokens?,outputTokens?,costUsd?,elapsedMs?} |
| 스트림 | POST /chat/stream body {sessionId,content,images?[{mediaType,data(dataURL)}],forcedSkill?,explain?}. 요청 하나 = SSE 하나. 이벤트 token{delta} → usage{used,limit,ratio,elapsed_ms} → done{traceId?}, 중간 title{title}, 실패는 error{message,code}. @microsoft/fetch-event-source 라 POST 가능 |
| 스니펫 | 백엔드 안 씀. Tauri 브릿지 → 로컬 SQLite |
패키징 gotcha (백엔드 뭘 쓰든 동일): Tauri 빌드는 dist 를 tauri:// 계열 origin 으로 띄움. VITE_API_BASE_URL=/api/v1 상대경로는 dev(Vite proxy)에서만 되고, 배포본은 풀 URL 이어야 함. 그러면 cross-origin 이라 쿠키는 SameSite=None; Secure = https 필수. 고객사 bare 배포가 http(10.196.81.34:8918)라 쿠키 인증은 거기서 깨짐 → Bearer 로 가는 게 안전.
2. 가져다 쓸 것 — ABAP_OPENCODE web/BE Django 가 이미 하는 것
출처: web/BE/code/README.md, common/opencode_service.py, apps/chat/sse.py, apps/accounts/authentication.py, deploy/docker-compose.yml, apps_ito/aaf/urls.py.
| 영역 | 있는 것 | CodeAssist 계약과 차이 |
|---|---|---|
| 런타임 | Django + DRF, uvicorn(ASGI) — SSE 버퍼링 없이 흐름 | 같음. 그대로 |
| 인증 | simplejwt Bearer 헤더 또는 ?token=, 14일 액세스, refresh 없음, 쿠키 없음, {"detail"} 에러 |
envelope 아님, 쿠키 아님, refresh 없음 |
| 세션 | OpenCode 가 세션·메시지 원본. Django 는 session_owner 매핑만 |
페이지네이션 없음, 검색 없음, isGenerating·usage 필드 없음 |
| 스트림 | GET /chat/events 전역 SSE 하나(OpenCode /event 를 owner 필터해 통째로 중계). 전송은 POST …/messages 동기(최대 10분 블록). FE 가 message.part.delta/session.idle 직접 파싱 |
요청-단위 SSE 아님, 이벤트 어휘 다름 |
| OpenCode | opencode serve :4096, workspace = AGENTS.md + skills/ + opencode.json(모델·MCP). .opencode/agent/*.md 로 에이전트 정의 |
AGENTS.md 가 "ABAP FS 브레인스토밍 자동 트리거" — CodeAssist 짧은 코드 답변과 성격 충돌 |
| LLM 연결 | opencode.json → OpenRouter GLM. 고객사는 render-opencode.py 가 FabriX 모델 목록으로 렌더하고 /api/ito passthrough 게이트웨이가 FabriX 로 중계(401 4조합 재시도까지 됨) |
그대로 재사용. 폐쇄망 대응 다 돼 있음 |
| SAP | sap-icf MCP(:3200), 고객사 ZAI 모드 자동 |
CodeAssist 챗도 "이 테이블 뭐야" 같은 질문에 쓸 수 있음 — 덤 |
| 배포 | compose(fe·db·be·be-watcher·opencode·mcp) + 고객사용 bare run.sh |
컨테이너/프로세스 하나 추가하면 됨 |
| DB | SQLite ↔ PostgreSQL 스위치, DB_HOST 로 자동 판단 |
그대로 |
3. 선택지
| A. Django 에 CodeAssist 어댑터 앱 추가 (추천) | B. 프론트를 ABAP_OPENCODE 계약으로 갈아타기 | C. OpenCode 안 거치고 /api/ito OpenAI 호환 중계만 |
|
|---|---|---|---|
| 프론트 변경 | env + 토큰 저장 2줄 | envelope·인증·스트림 모델 전부 (snap feature 대수술) | 스트림 어댑터 새로 |
| 서버 변경 | 앱 1개 + 어댑터 | 거의 없음 | 세션·히스토리·검색 전부 Django 가 직접 |
| OpenCode 장점(도구·MCP·스킬·세션 컨텍스트) | 씀 | 씀 | 못 씀 — "opencode + django" 요청과 어긋남 |
| 위험 | 이벤트 어댑터 품질 | 회귀 범위 큼(테스트 268개 건드림) | 기능은 단순하지만 요청 취지 밖 |
A 로 간다. 프론트는 이미 "seam 만 바꾸면 되게" 짜여 있고(2026-07-18 설계의 방침 그대로), Django 쪽은 owner 매핑·OpenCode 클라이언트·SSE 파서가 다 있어서 어댑터만 얹으면 됨.
4. 인프라 그림 (제안)
[Windows PC] [서버 VM — ABAP_OPENCODE 스택 + 1]
CodeAssist.exe (Tauri) ┌─ nginx(:80) 또는 Django 직접(bare :8080)
├─ WebView2: React dist │ /base-api/* → web/BE (기존 ABAP OpenCode 화면용)
│ VITE_API_BASE_URL=https://<서버>/ca-api/v1 │ /ca-api/v1/* → web/BE apps/codeassist ← 신규
│ Authorization: Bearer <JWT> ───────────┼──▶ Django(uvicorn ASGI, :8100)
│ │ ├─ apps/accounts JWT 발급 (공용)
│ │ ├─ apps/codeassist 세션·검색·스트림 어댑터 + 미러 DB
│ │ └─ apps_ito/aaf /api/ito OpenAI 호환 중계 (FabriX/GLM)
└─ Rust: 핫키·캡처·스니펫 SQLite(로컬, 서버 X) │
├─ opencode-ca(:4097) ← 신규 컨테이너. 같은 이미지, 다른 workspace
│ /workspace-ca/AGENTS.md (짧은 코드 답변 규칙)
│ /workspace-ca/opencode.json (모델 = /api/ito 게이트웨이 또는 OpenRouter, MCP = sap-icf)
│ /workspace-ca/.opencode/agent/codeassist.md
├─ opencode(:4096) 기존 ABAP OpenCode 용 (안 건드림)
├─ mcp(:3200) sap-icf 둘이 공유
└─ db SQLite 또는 PostgreSQL (공유, 테이블만 추가)
왜 OpenCode 를 따로 하나 더 띄우나: OpenCode 는 workspace 의 AGENTS.md 를 모든 세션에 적용함. ABAP OpenCode 것은 "리포트 만들어줘 → 코드 쓰지 말고 질문 시작" 규칙이라 CodeAssist(즉답 코드 도우미)랑 정면 충돌. 이미지·볼륨만 다르게 하면 비용 거의 0. 포트 하나(4097)만 더.
왜 Django 는 하나: 인증(accounts)·ITO 게이트웨이(FabriX)·DB·배포 스크립트를 두 번 안 만들려고. CodeAssist 라우트는 /ca-api/v1/ 프리픽스로 분리해서 기존 /api/v1/chat/* 랑 안 겹치게.
5. 어댑터 앱 apps/codeassist — 뭘 만드나
5.1 응답 모양
- DRF Renderer + exception handler 를 이 앱에만 걸어
CommonResponseenvelope 로 감쌈. 목록은meta채움. - 에러 코드는 프론트가 아는 것 유지:
CHAT_GENERATION_IN_PROGRESS(409),LLM_ERROR,REFRESH_TOKEN_MISSING등.
5.2 인증
POST /ca-api/v1/auth/login→ 기존 accounts 로그인 재사용, 응답만TokenResponse모양.refreshToken은 simplejwt refresh 토큰.POST /auth/refresh는 body 로 refreshToken 받는 걸로(쿠키 아님).GET /users/me는 accountsme감싸기.- Entra:
GET /auth/entra/config→ 501 로 시작 (프론트가 버튼 숨김). 필요해지면 2차. - 프론트 변경 지점 (유일): 로그인 응답의
token을setAccessToken()에 넣고 저장. 저장소는 Tauri 쪽(tauri-plugin-store또는 Windows Credential Manager) 이 정석, 1차는 localStorage 로 시작해도 됨. 401 인터셉터의 refresh 호출을 Bearer 모드에서도 돌게 하는 것까지 포함(지금은// TODO(host)).
5.3 세션·메시지 — 미러 테이블
OpenCode 는 페이지네이션·본문 검색·isGenerating·메시지별 usage 를 안 줌. 그래서 Django 에 미러를 둠(base-backend 도 DB 가 원본이었음):
ca_session(id=opencode session id, user, title, title_llm, is_generating, created, updated)ca_message(session, role, content, created, input_tokens, output_tokens, cost_usd, elapsed_ms)- 쓰는 시점: 스트림 시작 시 user 메시지 insert +
is_generating=true,session.idle받으면 OpenCodeGET /session/{id}/message로 마지막 assistant 메시지·tokens/cost가져와 insert,is_generating=false. - 검색은 미러에서
icontains. 목록은 미러에서-updated정렬 + page/limit. - 대화 컨텍스트 원본은 계속 OpenCode(세션 이어서 질문하면 OpenCode 가 히스토리 들고 있음). 미러는 조회·검색 전용.
5.4 스트림 어댑터 POST /chat/stream
순수 Django async 뷰(ASGI). 흐름:
- owner 확인,
is_generating이면 409CHAT_GENERATION_IN_PROGRESS. - 프로세스당 하나인 OpenCode
/event구독 태스크(기존iter_event_jsons재사용)에서 세션별asyncio.Queue로 fan-out. - OpenCode 에 메시지 전송은 블록 안 되게 스레드/태스크로 (
sync_to_async(send_message)또는 OpenCode 의 비동기 프롬프트 엔드포인트 — docs-lib 에서 버전 확인 필요).agent: "codeassist", 모델은 opencode.json 기본. - 큐에서
message.part.delta(text) →event: token {delta},session.updated의 title 바뀜 →event: title,session.idle→ usage 계산 →event: usage→event: done. 에러 이벤트 →event: error. - 이미지: OpenCode 메시지 parts 의 file 파트(mime + data URL)로 전달. 구현 전 docs-lib 로 파트 스키마 확인 (추측 금지).
- 취소
POST /chat/sessions/{id}/cancel→ OpenCodeabort+is_generating=false. - 프론트가 스트림을 잃어도
isGenerating폴링이 미러 DB 를 보니 답변 복구됨 — 기존 동작 그대로.
5.5 OpenCode workspace (web/opencode-ca/ 제안)
AGENTS.md: 한국어, ABAP/SAP 코드 즉답, 코드펜스 언어 태그, 불필요한 절차 질문 금지,explain플래그면 배경 설명.opencode.json: provider = 고객사면/api/ito게이트웨이(OpenAI 호환), 개발이면 OpenRouter.tools.question=false(웹과 같은 이유 — 답할 UI 없음). MCPsap-icf연결..opencode/agent/codeassist.md: 시스템 프롬프트.forcedSkill은 1차 무시.
5.6 배포
- compose:
opencode-ca서비스 추가(이미지Dockerfile.opencode재사용, 볼륨../opencode-ca:/workspace,oc_ca_data),be에OPENCODE_CA_BASE_URL=http://opencode-ca:4096. nginx 에/ca-api/location. barerun.sh에 프로세스 하나 추가. - CodeAssist 빌드:
2_frontend/.env.production에VITE_API_BASE_URL=https://<서버>/ca-api/v1. CORS 는 Tauri origin(tauri://localhost,http://tauri.localhost) 허용 — 서버CORS_ORIGINS에 추가.
6. 남는 결정 (사용자가 정할 것)
- Django 코드 위치 — ABAP_OPENCODE
web/BE에 앱 추가(추천, 인프라 공유) vs CodeAssist 용 Django 별도 repo. 별도면 accounts·ITO·배포를 복제해야 함. - 인증 저장 — 1차 localStorage(빠름) vs 처음부터 Tauri 저장소(안전). 토큰이 PC 에 남는 건 둘 다 같고, 보안 차이는 다른 프로세스가 읽기 쉬운가 정도.
- Entra 로그인 — 1차 501 로 끄기(추천) vs 바로 구현. 고객사 폐쇄망이면 어차피 못 씀.
- OpenCode 분리 — 컨테이너 분리(추천) vs 하나에 agent 만 나눔. 후자는 AGENTS.md 충돌을 프롬프트로 덮어야 해서 불안정.
- 스니펫 서버 동기화 — 지금은 로컬 SQLite. 팀 공유 원하면 2차 (
ca_snippet테이블 + 동기화).
7. 단계 (spec 파면 이 순서로 tasks 뽑을 것)
| 단계 | 내용 | 검증 |
|---|---|---|
| 1 | apps/codeassist 뼈대 + envelope renderer + /ca-api/v1 배선 + auth 4개(login/refresh/logout/me) + entra 501 |
프론트 로그인 화면이 실제로 통과 |
| 2 | 미러 테이블 + sessions list/create/messages/search | 목록·검색 화면 |
| 3 | opencode-ca 컨테이너 + workspace + Django OPENCODE_CA_BASE_URL |
curl 로 세션 생성·프롬프트 |
| 4 | 스트림 어댑터(token/title/usage/done/error) + cancel + isGenerating | Snap 채팅 스트리밍, 중단, 재진입 복구 |
| 5 | 이미지 파트 + explain |
캡처→질문 |
| 6 | 프론트 토큰 저장 2줄 + .env.production + CORS + 배포 스크립트 |
Tauri 빌드에서 end-to-end |
각 단계 전 docs-lib/ 에 OpenCode 서버 API 문서(세션·메시지 parts·event 스키마) 떨어뜨리고 보면서 짬. 지금 두 repo 다 그 문서 없음 — 1단계 준비물.