Rinda 개발자 온보딩 가이드
수출 기업 매니저를 위한 AI 영업 자동화 플랫폼. 바이어 발굴 → 이메일 캠페인 → 답장 관리까지 자동화하는 풀스택 모노레포입니다. 이 문서 하나로 전체 아키텍처와 각 용어를 이해할 수 있습니다.
routes → services → db로 처리하고, 오래 걸리는 일(이메일 발송·AI 검색)은 BullMQ 큐에 넣어 워커가 처리하며 진행률은 SSE로 실시간 전송한다.빠른 시작
사전 요구사항: Node 20+, Bun 1.2+, Yarn 4 (corepack), Docker, just (작업 러너).
git clone git@github.com:FINGU-GRINDA/send-grid-test.git
cd send-grid-test
just setup # 사전검증 + .env 생성 + Docker(PG18+Redis8) + 의존성 설치 + 스키마 push
just seed # 시드 데이터 생성
just dev # admin: http://localhost:5173 / API: http://localhost:3001
admin=yarn, elysia-server=bun, e2e=npm. 폴더를 헷갈리면 lockfile이 깨집니다.모노레포 구조
| 폴더 | 역할 | 패키지 매니저 |
|---|---|---|
| admin/ | 유저·관리자 대시보드 UI | yarn 4 |
| elysia-server/ | REST API + BullMQ 백그라운드 워커 | bun |
| e2e/ | Playwright 통합 테스트 | npm |
기술 스택
프론트엔드
백엔드
인프라
AI / LLM
이메일
결제 · 외부
SSOT 원칙 (Single Source of Truth)
같은 데이터는 한 곳에만 정의합니다. 중복되면 drift(불일치)가 생깁니다. 프론트는 백엔드의 미러이며 자체 SSOT를 만들지 않습니다.
| 데이터 성격 | 저장 위치 |
|---|---|
| 영속·다중 사용자 공유 (유저·결제·엔티티) | PostgreSQL (Drizzle schema) |
| 휘발·짧은 TTL (캐시·세션·rate limit·큐) | Redis / BullMQ |
| 서버 빌드 상수 (라우트·모델·티어) | elysia-server/src/** 모듈 export |
| 프론트 빌드 상수 (디자인 토큰·enum) | admin/src/** 모듈 export |
| 서버 상태의 클라 캐시 | TanStack Query (읽기 전용, mutate 금지) |
| 클라 ephemeral UI 상태 (모달·폼) | Jotai |
백엔드 · 3-Layer 아키텍처
모든 요청은 세 계층을 순서대로 통과합니다. 각 계층은 하나의 책임만 가집니다.
대표 흐름 예시 — 시퀀스 검색
// 1) routes/sequences.routes.ts — 요청 파싱 + 서비스 위임 + ok() 응답
.get("/search", async ({ query, workspace }) => {
const { items, cursor } = await listSequencesPaged({ ...filters })
return ok({ items, cursor }) // utils/reply.ts 헬퍼
}, { workspaceAuth: true }) // ← 인증 매크로 (필수 선언)
// 2) services/sequence-read.service.ts — DB 직접 접근
// 3) db/schema/sequences.ts — 테이블 스키마
AppError 서브클래스(NotFoundError, BadRequestError 등)를 throw → 글로벌 핸들러가 JSON으로 변환. 응답은 항상 ok(data) / created(data) 헬퍼 사용 ({ success: true } 직접 작성 금지).백엔드 · 인증 매크로
모든 라우트는 반드시 인증 매크로를 인라인 선언해야 합니다. 정적 게이트(check:route-auth)가 유일한 방어선이며, 런타임은 미선언 시 fail-open(public)이 됩니다. SSOT: plugins/auth.macro.ts
| 매크로 | 검증 | 주입 |
|---|---|---|
public: true | 없음 (공개) | — |
auth: true | JWT 토큰만 | { user } |
adminAuth: true | JWT + DB활성 + role=admin | { user } |
workspaceAuth: true | JWT + X-Workspace-Id + 멤버십 | { user, workspace } |
workspaceAuth: { resource, action } | 위 + IAM 정책 검사 | { user, workspace } |
--no-verify로 우회하지 마세요. 라우트 권한 누락은 보안 사고로 직결됩니다. drift 자동보정: sh send-ci.sh auth --fix. (ADR-0001: default-deny)백엔드 · DB · Drizzle
PostgreSQL 18 + Drizzle ORM. 스키마는 elysia-server/src/db/schema/, 마이그레이션은 drizzle/*.sql.
마이그레이션 워크플로
// 1) src/db/schema/*.ts 편집
// 2) 마이그레이션 자동 생성 (손으로 쓰지 말 것!)
bun db:generate // → 0NNN_*.sql + meta/0NNN_snapshot.json + _journal.json
// 3) 세 파일 함께 git add (하나라도 빠지면 게이트 실패)
git add drizzle/
// 4) 무결성 검증 (husky pre-commit + CI 에서 자동)
bun check:migrations // F1~F8 실패모드 차단
① 커밋된
drizzle/*.sql 수정 금지 (SHA-256 hash 깨짐) → hot-fix는 새 마이그레이션.② 신규 PK는 UUIDv7:
uuid().default(sql`uuidv7()`) — gen_random_uuid() 금지 (PG18 native).③ OFFSET 페이지네이션 금지 → keyset(cursor) 사용.
④ Enum 변경은 단독 마이그레이션으로 분리 (같은 트랜잭션 후속 DDL을 PG가 거부).
⑤ 무거운 집계(COUNT DISTINCT, DISTINCT JOIN)는
analyticsDb 사용 (work_mem 256MB).db(OLTP·모든 write, work_mem 32MB) vs analyticsDb(readonly 집계, 256MB). 무거운 쿼리를 기본 db로 돌리면 디스크 spill 발생.백엔드 · 큐 · 워커 (BullMQ)
오래 걸리는 작업은 API에서 직접 처리하지 않고 BullMQ 큐에 넣습니다. 워커는 별도 Bun 프로세스로 독립 배포됩니다.
| 구성 | 위치 |
|---|---|
| 큐 타입 정의 (60+ 큐) | lib/queue/types.ts |
| 큐 프로듀서 (작업 등록) | lib/queue/queues.ts |
| 워커 구현 | workers/bullmq/*.worker.ts |
| 워커 엔트리포인트 | src/worker.ts · worker-buyersearch.ts |
대표 큐
SEQUENCE_EMAIL— 시퀀스 이메일 발송 /SEQUENCE_EMAIL_LOADER— DB pending → BullMQ delayed 적재(30초 틱)LEAD_DISCOVERY_SEARCH / _ANALYZE— 리드 발굴 검색·분석BUYER_SEARCH_MASTRA— 바이어 탐색 AI 파이프라인 /AGENT_MISSION— AI 에이전트 실행
job-log.service.ts가 DB에 저장.백엔드 · 실시간 진행률 (SSE)
장시간 잡(시퀀스 발송, 리드 발굴)의 진행률은 SSE + Redis Pub/Sub로 실시간 전송. SSOT는 항상 DB. 위치: lib/sse/
Last-Event-ID로 놓친 이벤트만 복구(ring buffer). 도메인별 어댑터(isTerminal, filterEvent, dedupeKey)로 동작을 커스텀.백엔드 · AI 통합
기본 모델은 gemini-3-flash-preview. 스테이지별 모델 설정 SSOT: services/agent-model-settings.service.ts
모델 설정 아키텍처
AGENT_MODEL_CATALOG— 코드 기본값 (SSOT)db.agentModelSettings— 어드민 오버라이드 (없으면 기본값)resolveAgentModel(key)— 60초 TTL 캐시 조회, fail-open. 어드민 변경 시 즉시 캐시 무효화
| 스테이지 | 대표 키 | 모델 |
|---|---|---|
| 검색 전략 수립 | pre-search-plan | gemini-3.1-pro-preview |
| 검색어 추출 | query-extraction | gemini-3-flash-preview |
| 관련성 필터 | relevance-filter | gemini-3.5-flash |
| 바이어 탐색 본체 | supervisor 등 | BUYER_SEARCH_V2_MODEL |
프론트엔드 · 디렉토리 구조
프론트엔드 · 상태관리
두 가지 상태를 명확히 구분합니다. 잘못 쓰면 stale 캐시 보안 위험 또는 과도한 동기화 비용이 발생합니다.
TanStack Query 서버 상태
queryKey 기반 캐싱, staleTime, 조건부 enabled. 절대 직접 mutate 금지.useQuery({ queryKey:["leads",p],
queryFn:()=>leadsApi.search(p),
staleTime:120_000 })
Jotai 클라 UI 상태
atomWithStorage로 localStorage 동기화 가능.const wsAtom =
atomWithStorage("selectedWorkspace",null)
const [ws,setWs] = useAtom(wsAtom)
프론트엔드 · API 레이어 (3계층)
lib/api/generated/schema.ts 자동 생성(yarn openapi:generate). 신규 API는 service + hook을 분리해서 추가.프론트엔드 · 인증 · 권한
| 훅 | 역할 | 위치 |
|---|---|---|
useCurrentUser() | 현재 유저 (localStorage fast-path + 5분 캐싱) | lib/api/hooks/auth.ts |
useVerifyToken() | 쿠키 세션 유효성 확인 (마운트 시 1회) | lib/api/hooks/auth.ts |
usePermissions() | hasPermission(resource,action), isAdmin | lib/permission/ |
useBillingSubscription(wsId) | 구독 tier·trial 상태 | lib/api/hooks/billing-subscriptions.ts |
프론트엔드 · 디자인 시스템
SSOT는 admin/.claude/rules/frontend-design.md (희주님 관리 문서, 무단 수정 금지).
- 색상 토큰: 배경
bg-rd-n-200, 카드bg-rd-n-white, 제목text-rd-n-900, 액센트text-rd-blue-500 - 타이포: 최소
text-xs(12px) 이상. 한글은break-keep+[overflow-wrap:anywhere]조합 필수 - 아이콘: Material Symbols Rounded →
@/components/ui/material-symbolwrapper (lucide 직접 import 금지) - i18n: 모든 사용자 노출 텍스트는
t()키, 4개 언어(ko/en/id/ja) 모두 작성 - 라우팅: 신규 페이지는
router/lazy-imports.ts에 lazy import 등록 (번들 분할)
주요 도메인 기능
| 기능 | 설명 | FE 위치 | BE 위치 |
|---|---|---|---|
| Lead Discovery | AI 바이어 발굴·검색·분석 | pages/lead-discovery/ | services/lead-discovery/ (30+) |
| Buyer Search | 에이전트 기반 바이어 탐색 | pages/lead-discovery/ | services/buyer-search*/ + Mastra |
| Sequences | 이메일 캠페인 생성·실행 | pages/sequences/ | services/sequence*.ts |
| Email Replies | 받은 답장함·AI 회신·자동분류 | pages/email-replies/ | services/email-reply*.ts |
| LinkedIn SDR | LinkedIn 자동화 캠페인 | pages/linkedin-sdr/ | services/linkedin-sdr*.ts |
| Billing | 구독·결제 (Paddle/Toss) | pages/billing/ | services/billing/ · paddle/ |
CI / 배포
send-ci.sh — 통합 검증
sh send-ci.sh # full (기본, quiet) — lint + type-check + build
sh send-ci.sh fast # pre-commit용 (빠름)
sh send-ci.sh full --verbose # 전체 스트리밍 (디버깅)
sh send-ci.sh auth --fix # 라우트 권한 맵 + FE IAM 자동보정
환경 & 배포 파이프라인
| 환경 | 용도 | URL | 배포 트리거 |
|---|---|---|---|
| local | 개발 | localhost:5173 / :3001 | just dev |
| alpha | 스테이징 | alpha.rinda.ai | push origin alpha |
| beta | 프로덕션 | sendgrinda.cloud | merge → beta |
ssh alpha "docker logs ... | grep -E 'applying|completed'" (skip 시 다음 호출에서 42703 column does not exist).E2E 테스트 (헥사고날)
Playwright + 헥사고날 포트/어댑터 패턴. 셀렉터 SSOT는 어댑터 1곳뿐이라 디자인 변경 시 어댑터만 수정하면 모든 spec이 따라옵니다.
cd e2e
npm run bootstrap # 의존성 + storageState 준비
npm run test:local # 로컬 (just dev 선행 필요)
npm run test:local:ui # UI 모드 (브라우저 시각화)
waitForTimeout 금지 (web-first 대기 사용). 기능 변경 시 happy·edge·error·상태분기·게이트 케이스를 빠짐없이 추가. 체크리스트: docs/claude-rules/e2e-harness-workflow.md시크릿 (Infisical)
환경변수는 코드/.env에 하드코딩하지 않고 Infisical로 관리합니다. 환경: local · alpha · beta.
infisical login
infisical run -e local -- sh send-ci.sh
infisical export -e local -f .env > elysia-server/.env
# Claude Code 스킬 (자연어)
/infisical add GEMINI_API_KEY sk-...
/infisical sync
GEMINI/OPENAI/ANTHROPIC_API_KEY, SENDGRID_API_KEY, AWS_SES_*, DB_*, REDIS_*, JWT_SECRET, PADDLE_API_KEY, TOSS_SECRET_KEY.용어 사전
| 용어 | 뜻 |
|---|---|
| Lead / Buyer | 잠재 고객(바이어). 카피상 "Buyer List" 사용 ("Data List" ✕) |
| Sequence | 이메일 캠페인. 여러 step으로 구성되어 순차/조건 발송 |
| Enrollment | 특정 리드가 시퀀스에 편입된 상태 (발송 대상) |
| Workspace | 팀/조직 단위. 멤버·권한·구독·데이터의 격리 경계 |
| IAM | 권한 시스템. resource × action 매트릭스, default-deny (ADR-0001) |
| Tier | 구독 등급. trial/pro/team → ai:search 차단, enterprise → full |
| SSOT | Single Source of Truth — 데이터를 한 곳에만 정의 |
| SSE | Server-Sent Events — 서버→클라 단방향 실시간 스트림 (진행률) |
| Mastra / LangGraph | AI 에이전트 워크플로우 프레임워크 |
| drift | 같은 데이터가 여러 곳에 중복되어 값이 어긋나는 현상 |
| fail-open / fail-closed | 실패 시 허용(open) / 차단(closed). 라우트 권한은 정적 게이트로 막음 |
| keyset (cursor) | OFFSET 대신 마지막 값 기준 페이지네이션 (대용량 성능) |
첫 주 체크리스트
Day 1 — 환경 설정
- git clone +
just setup+just dev(admin:5173 / API:3001 확인) - admin 대시보드 로그인 / CLAUDE.md · CONTRIBUTING.md 읽기
Day 2–3 — 코드 구조 이해
just diagrams로 C4 아키텍처 다이어그램 보기- 한 기능을 routes → services → db로 따라 읽기 (예: sequences)
- FE는 page → hook → service → client 흐름 따라 읽기
Day 4–5 — 개발 & 배포 이해
sh send-ci.sh fast실행 / 작은 버그픽스 또는 기능 추가- E2E 테스트 작성 (e2e/tests/features 참고)
- 로컬 커밋 → pre-commit 훅(마이그레이션 무결성 등) 확인
주간 과제 — 첫 PR
- 작은 스코프 구현 →
sh send-ci.sh full통과 → E2E happy path 추가 → 리뷰 → 머지
Rinda 개발자 온보딩 가이드 · 자동 생성 문서 · 2026-06-25