이 저장소에서 작업하는 AI 코딩 에이전트를 위한 안내다.
패키지 매니저는 pnpm 10.29.3 고정, Node 22 (.nvmrc).
pnpm dev # tsx watch src/index.ts
pnpm typecheck # tsc --noEmit (CI가 build 전에 실행)
pnpm build # tsc → dist/
pnpm start # node dist/index.jstest 스크립트는 없다. vitest를 직접 호출한다:
pnpm vitest run # 전체
pnpm vitest run src/modules/team-chat/team-chat.repository.test.ts # 단일 파일
pnpm vitest run -t "테스트 이름" # 이름으로 필터테스트는 실제 PostgreSQL에 붙는다(postgresql://postgres:postgres@localhost:5432/test_db, vitest.config.ts에 하드코딩). beforeAll에서 initializeCoreSchema로 스키마를 만들고 beforeEach에서 TRUNCATE ... CASCADE로 초기화한다. DB 없이는 리포지토리 테스트가 실패한다.
API 문서는 서버 기동 후 /docs (Swagger UI), 헬스체크는 /health.
브로커 주소는 KAFKA_BROKERS(기본 127.0.0.1:9092, 쉼표 구분)로 지정한다.
initSocketServer(src/lib/socket/socket.server.ts)가 producer/consumer 연결을 try/catch로 감싸므로
브로커가 없어도 서버는 뜬다. 대신 kafkaReady=false로 남아 team:message:send가 전부
team:message:error로 되돌아온다. 팀 채팅이 조용히 안 되면 기동 로그의 "Kafka 연결 실패" 경고부터 본다.
개인 AI 채팅(message:send)은 Kafka를 거치지 않는다.
Kafka 4.x는 KRaft 단독이라 ZooKeeper를 따로 띄우지 않는다.
brew install kafka
brew services start kafka # 로그인 시 자동 기동까지 걸린다
# 확인
brew services list | grep kafka # started 여야 한다
lsof -nP -iTCP:9092 -sTCP:LISTEN # java가 9092를 잡고 있어야 한다토픽은 num.partitions=1 + 자동 생성이라 보통 손댈 게 없다. 직접 만들거나 확인하려면
(Homebrew판 CLI는 .sh가 붙지 않는다):
kafka-topics --list --bootstrap-server localhost:9092
kafka-topics --create --topic team-chat-message \
--bootstrap-server localhost:9092 --partitions 1 --replication-factor 1함정 — 브로커는 멀쩡한데 CLI만 죽는 경우. brew services는 자기 openjdk로 브로커를 띄우지만,
kafka-topics 같은 CLI는 셸의 JAVA_HOME을 따라간다. ~/.zshrc에 옛 JDK 경로가 박혀 있으면
브로커는 잘 돌면서 CLI만 bin/java: No such file or directory로 죽는다:
export JAVA_HOME=$(brew --prefix)/opt/openjdkdocs/windows-setup.md의 「카프카 설치」·「카프카 토픽 생성」 절을 따른다. macOS에서도 같은 명령이 동작한다.
루트의 docker-compose.yml은 0바이트 빈 파일이라 docker compose up은 아무것도 안 띄운다.
Fastify 5 + 순수 pg (ORM·마이그레이션 도구 없음) + TypeScript ESM.
"type": "module" + moduleResolution: NodeNext. 모든 상대 import는 .js 확장자를 붙여야 한다 (./foo.service.js). 소스가 .ts여도 마찬가지.
src/modules/<domain>/에 route / schema / service / repository / types 5파일.
route: HTTP 입출력. Fastify JSON Schema를 인라인으로 선언(Swagger 문서 + 응답 직렬화용)하고, 실제 검증은 zod로 다시 한다 —createXBodySchema.parse(request.body). 두 스키마가 공존하는 구조이므로 필드 추가 시 양쪽을 함께 고쳐야 한다.service: 도메인 규칙,HttpError로 상태코드 결정.repository:interface XRepository+PgXRepository implements로 분리. snake_case 행 → camelCase 객체 변환은 리포지토리 안의toX()매퍼가 담당.
응답 형식은 성공 { ok: true, data }, 실패는 registerErrorHandler가 { status, ok: false, error, message, details }로 정규화(ZodError → 400 자동).
DI 프레임워크가 없다. src/index.ts가 유일한 composition root로, Pool → 리포지토리 → 서비스 → registerXRoutes(app, deps) 순서로 손으로 엮는다. 새 모듈은 여기에 등록해야 살아난다.
선택 의존성은 env 유무에 따라 null로 떨어지고 기능이 통째로 비활성화된다:
OPENAI_API_KEY없음 →projectAnalysisService = null→/api/project-analysis/*라우트 자체가 등록되지 않음ANALYSIS_SERVER_URL/ANALYSIS_SERVER_INTERNAL_TOKEN없음 → 동기화 완료 후 인덱싱 콜백이 분석 서버 대신 로컬 분석 서비스로 폴백
마이그레이션 파일이 없다. src/modules/core-schema/core-schema.repository.ts의 initializeCoreSchema(pool)이 부팅 시마다 CREATE TABLE IF NOT EXISTS / ALTER TABLE ... ADD COLUMN IF NOT EXISTS로 전체 스키마를 멱등하게 만든다. 테이블·컬럼 추가는 이 파일에 추가하는 것이 유일한 경로이고, 기존 컬럼 변경/삭제는 이 방식으로 표현되지 않으니 주의.
Fastify 플러그인이나 preHandler 데코레이터를 쓰지 않는다. 각 route 파일이 로컬 getAccessToken(authorization) 헬퍼로 Bearer 토큰을 꺼내 authService.getMe(token)을 핸들러마다 직접 호출한다. 보호된 엔드포인트를 추가할 때 기존 route 파일의 이 패턴을 그대로 따라간다.
JWT는 access + refresh 2종. users.token_version으로 전역 무효화, user_refresh_tokens에 해시 저장.
- Python RAG/분석 서버 (
RAG_SERVICE_URL,ANALYSIS_SERVER_URL):RagSearchClient.searchChunks로 청크 검색 →trimContext→buildRagMessages→OpenAiClient.streamChat스트리밍. Python 응답은normalizeSearchResult()로 내부 타입 변환 후 사용한다. 파이프라인 조립도src/index.ts에 있고 각 단계가 langsmithtraceable로 감싸져 있다(LANGSMITH_*환경변수는config/env.ts스키마에 없고 SDK가process.env에서 직접 읽는다). - GitHub OAuth Device Flow + 레포 동기화:
ProjectSyncCoordinator가SYNC_REPO_BASE_DIR아래로 클론/풀하고, 완료 시 인덱싱 잡을 트리거. - 실시간:
src/lib/socket/socket.server.ts(socket.io). 팀 채팅 메시지는 Kafka(team-chat-message토픽)를 거쳐 컨슈머가 DB 저장 + 브로드캐스트한다.
src/config/load-env.ts를 가장 먼저 import해야 한다(.env → 비프로덕션이면 .env.local 순으로 override). src/config/env.ts가 zod로 엄격 파싱하며 실패 시 부팅이 죽는다. 새 환경변수는 envSchema와 .env.example 양쪽에 추가한다.
- 이 서버는 Qode-python(RAG 검색·인덱싱)과 짝이다.
ANALYSIS_SERVER_INTERNAL_TOKEN과SYNC_REPO_BASE_DIR은 Qode-python.env의QODE_INTERNAL_TOKEN·저장소 루트와 일치해야 한다. 값이 어긋나면 동기화는 성공하는데 인덱싱만 401/경로 오류로 죽는다. - auth 라우트만
/api접두어가 없다(/auth/login,/auth/me…). 나머지는 전부/api/*. .env.local은 비프로덕션에서.env를 덮어쓴다. "설정을 바꿨는데 안 먹는다"면.env.local을 먼저 본다.- 사용자용 개요는
README.md, 컨벤션·함정은 이 파일. 둘을 고칠 때 서로 어긋나지 않게 한다.
main 푸시 → GitHub Actions(.github/workflows/deploy.yml) → typecheck/build 후 EC2에 SSH로 git pull + pm2 reload qode-server. 작업 브랜치는 develop.
docs/crud-convention.md(CRUD 견본), docs/windows-setup.md·docs/macos-setup.md(초기 설치), docs/aws-server.md.