Rinda 모노레포 감사 & 개선 계획

B2B 글로벌 AI 세일즈 자동화 플랫폼 · v2.8.0.0 · principal-level 기술 감사

B+전체 건강도
성숙한 프로덕션, 견고한 가드
— 핵심 안전망 1개 구멍
admin · React 19 / ~2,138 파일 elysia-server · Bun / ~2,173 파일 5개 병렬 감사 에이전트 file:line 근거 분석 전용 · 코드 미수정
⚠️ 이 문서는 보안 취약점·내부 파일경로 등 민감정보를 포함합니다. 비공개(검색엔진 noindex) 링크이며 외부 공유를 삼가세요.

1. Executive Summary

전체 건강도: B+ — 성숙한 프로덕션 SaaS로 3-layer 분리·default-deny 라우트 인증·migration integrity gate·JWT 하드닝·중앙집중 DOMPurify·구조화 로거(pino) 등 동급 스타트업 대비 상위권 규율. 발견의 대부분은 "근본 결함"이 아니라 성장 부채(god 파일·fork 후 미수거 중복)정책 위반(자체 CLAUDE.md 규칙을 코드가 어김)으로, 팀이 옳은 패턴을 알면서 일부 경로에서 적용을 빠뜨린 형태다.

🔺 Top 3 리스크

  1. PR CI가 bun test를 전혀 실행 안 함 — 831개 테스트가 머지 게이트 밖, 회귀 무방비 Critical
  2. 1,276줄 단일 메서드 storeInboundEmailInDB — SendGrid 인바운드 전체가 테스트·리뷰 불가 단위 Critical
  3. 비만료·결정론적 디바이스 토큰 — 1개 유출 시 영구 쓰기 권한, per-token revoke 불가 High

🔻 Top 3 기회

  1. CI에 test:unit 추가(S) — 즉시 회귀 게이트 확보
  2. 3중 buyer-search 구현(~65k LOC) 중 canonical 1개만 — 최대 LOC 회수 + drift 제거
  3. 대용량 테이블 OFFSET→keyset 전환 — 이미 검증된 10,000× 개선 패턴 보유

2. Repo Map

목적수출 기업용 해외 바이어 발굴 + AI 이메일 영업 + 리드 관리 올인원(B2B SaaS)
성숙도프로덕션 (alpha/beta 이중 환경, docker-compose+nginx 배포, 정식 결제 Paddle/Toss)
스택FE: React 19 + Vite + TanStack Query + Jotai + Radix + Tailwind v4 (yarn4) / BE: Elysia + Bun + Drizzle/Postgres + BullMQ/Redis (bun) / E2E: Playwright(npm)
아키텍처BE 3-layer (routes→services→db) + 워커 플릿(BullMQ) + AI 에이전트(Mastra/LangGraph/Vercel ai). FE는 BE 미러, SSE 실시간 진행률

핵심 디렉토리

경로설명
elysia-server/src/{routes,services,db}백엔드 3-layer 코어 (services 829파일이 비즈니스 로직 중심)
elysia-server/src/{workers,lib/queue}BullMQ 워커 플릿 + 큐 레지스트리(SSOT)
services/buyer-search*바이어 탐색 — 3개 병렬 구현 공존(~65k LOC)
admin/src/{pages,components,lib/api}FE 페이지·UI·서버상태 훅
pages/lead-discovery{,-mastra}v1↔mastra fork 중복(~3.5k LOC drift)
.github/workflows, send-ci.shCI/CD + 로컬=CI 동일 검증 스크립트
docs/ (282파일) + 루트 4 MD문서 — ADR·인시던트·플랜·PDF 혼재, sprawl

놀란 점

3. Audit Report

심각도 순 · F=Fact, J=Judgment

Critical

C1PR CI가 bun test를 전혀 실행 안 함
  • 위치 .github/workflows/ci.yml:138-140; ci-cd-alpha.yml(test grep 0); 양 package.json ci:check
  • 영향 831개 테스트가 로컬·수동 전용. 회귀가 머지 게이트 없이 alpha 진입 — 과거 migration silent-skip 인시던트 계열과 동일 노출면 [FACT]
C2storeInboundEmailInDB 단일 메서드 1,276줄 (try/catch 23개)
  • 위치 services/webhook.service.ts:160
  • 영향 SendGrid 인바운드 전체(헤더파싱·첨부·답장매칭·DB쓰기·분류트리거)가 테스트·리뷰 불가 단위. 어떤 변경도 인바운드 전체 위험 [FACT]

High

ID발견위치영향F/J
H1비만료·결정론적 디바이스 토큰 wsId.HMAC(wsId,SECRET), per-token revoke 불가(글로벌 시크릿 회전=전체 로그아웃)workspace-device.service.ts:29-60; recordings-device.routes.ts:79,122,159(전부 public)토큰 1개 유출 시 해당 워크스페이스 recordings/contact-import에 영구 쓰기 권한J
H23중 buyer-search 구현 공존: base(57f/23k)·pro(5f/3k)·vercel-ai(144f/39k). mastra 큐 배선은 빈 디렉토리(dead)services/buyer-search*/; dead lib/queue/queues.ts:869스코어링/버그픽스를 2-3곳 중복 적용→drift. ~65k LOC 유지비J
H3OFFSET 페이지네이션 — 대용량 leads 위반(CLAUDE.md 명시 금지, 사례 1500ms→0.147ms)lead-search.service.ts:697,731깊은 offset에서 O(offset) 스캔, 대용량 워크스페이스 성능 저하F
H4OFFSET on emails/sequence_enrollments(둘 다 고성장)email-replies.service.ts:158; sequence-enrollment.service.ts:135,180,317동상 — 깊은 페이지 스캔F
H5Sentry DSN을 config에서 읽지만 @sentry 미설치·Sentry.init 없음 → 프로덕션 에러 트래킹 부재config.ts:398비-워커 경로(라우트/서비스) 미처리 에러 가시성 공백(Slack+pino에만 의존)F
H6핵심 비즈로직 테스트가 live-DB env 게이트로 기본 skip(env 없으면 조용히 통과). 24개 파일deals.service.test.ts:20-22 외 23CI에 없을 뿐 아니라 로컬에서도 env 없으면 거짓 greenF
H7서비스 26%(215/829)·워커 9%(12/129) 테스트. 발송 핵심 워커 거의 무테스트src/workers/, src/services/매출·발신 평판 직결 경로 회귀 무방비F
H8CI cost-gate denylist: 핵심 기여자 6명(+bot) push/PR은 CI 전체 skip.github/workflows/ci.yml:30-33가장 자주 머지하는 계정이 lint·typecheck·migration gate 우회J
H9root eslint:"*" dead dep; puppeteer 3곳 선언·24.37↔24.38 drift·src import 0건이며 full puppeteer가 deps에 위치package.json:23,24; elysia-server/package.json:83,165,176죽은/오배치 의존성, 설치 비대·비결정성F/J

Medium

ID발견위치F/J
M1lib/queue/queues.ts 4,636줄(87 Queue, 38개 near-identical addXxxJob 래퍼) + types.ts 2,410줄 — 머지충돌 자석lib/queue/queues.tsF
M2webhook.service.ts 2,578줄 단일 클래스에 3개 도메인(ingestion/engagement/enrollment) 혼재services/webhook.service.tsF
M3FE god 컴포넌트: ChatRoom.tsx 2,046줄(useEffect15/useState9/13 props), UpdateNoteForm.tsx 1,854줄lead-discovery-mastra/ChatRoom.tsxJ
M4FE lead-discovery v1↔mastra fork 중복, 이미 drift(ClarificationCards 292↔295 등 ~3.5k LOC)pages/lead-discovery{,-mastra}/; router/dashboard-routes.tsx:194J
M5FE OFFSET+keepPreviousData — CLAUDE.md 이중 금지 위반SequenceEnrollmentsTable.tsx:863,1277; hooks/sequences.ts:216-237F
M6COUNT(DISTINCT)가 OLTP db 풀에서 실행(analyticsDb import 0). lead-fit은 쓰기경로마다 호출ui-events.service.ts:175; lead-fit-feedback.service.ts:236F
M7subscription-utils.service.ts catch→return null — DB 에러를 "구독 없음"으로 마스킹, 티어 게이팅 오판 위험subscription-utils.service.ts:68,98,144J
M8미인증 Slack 알림 트리거(public) — 인터넷 누구나 내부 채널 flood 가능(글로벌 rate limit만 의존)routes/discover-email.routes.ts:135-167J
M9문서 sprawl: docs/ 282파일 + 루트 4 MD(CHANGELOG 141KB). ADR 2개만 번호, 형제 5개 미번호docs/, docs/architecture/F/J
M10@types/* 5개가 elysia dependencies에 위치; Biome 버전 drift; @typescript/native-preview"beta" 이동태그 → CI 비재현elysia-server/package.json:74,85,117-121F
M11integration 테스트가 실제 TCP 소켓 fetch(포트 경합 flaky 소지); E2E 자동 트리거 비활성(수동 dispatch만)routes/public/__tests__/*.test.ts:80; e2e-on-alpha.yml:3-6F

Low (요약)

auth.service.ts:220 JWT expiresIn as any(보안 인접) · webhook/auth 경계 formData as any(C2 입력원) · console.log 148건 pino와 혼재 · root yarn.lock gitignore된 8개월 orphan · CLAUDE.local.md git-tracked · FE @ts-ignore 1건 · exhaustive-deps suppress 77건(email-replies 집중) · device 토큰 시크릿이 JWT_SECRET로 fallback · trial-analytics.service.ts:214 수작업 quote-escape + sql.raw(현재 admin UUID만, 비exploit)

Strengths (보존 대상)

  • 3-layer 규율 유지(F): 214 라우트 파일 중 직접 db 접근 3건뿐, 실제 순환 import 없음
  • 에러 핸들링 규율(F): 실질 swallowed exception 0. 글로벌 핸들러 위임 + retry 정책 상수화
  • 타입 안전(F): 프로덕션 서비스 1,054파일 중 any 35개; FE는 2,138파일 중 escape hatch 20개·deprecated useMe() 0
  • 보안 성숙(F): JWT 하드닝(placeholder denylist·HS256 명시), 중앙 DOMPurify SSOT+CSS post-pass, timing-safe 비교 전역, CORS 비-와일드카드, nginx HSTS/CSP
  • FE 상태 규율(F): 글로벌 MutationCache.onError 토스트, 4xx no-retry, 무거운 테이블 useVirtualizer, 레이어드 에러바운더리
  • DB 가드(F): migration integrity F1–F8 gate, keyset 모범패턴, analyticsDb 분리풀 239곳 활용, N+1은 대부분 inArray 배치
  • 온보딩 실재(F): just setup 동작, README↔code↔version 동기(v2.8.0.0)

4. Improvement Strategy

5개 테마가 발견의 대부분을 설명한다.

T-A · 안전망 구멍 (C1,H6,H7,H8,M11)
옳은 테스트가 존재하나 CI가 안 돌리고 핵심 워커는 무테스트. → CI가 모든 머지에서 test:unit 실행·실패 시 차단. 원칙: 테스트는 실행돼야 자산이다.
T-B · fork 후 미수거 (H2,M4)
마이그레이션 중 구현을 병렬 유지, decommission 누락. → canonical 1개 확정·나머지 삭제. 원칙: 한 capability=한 구현(SSOT).
T-C · 자체 규칙 위반 (H3,H4,M5,M6)
CLAUDE.md가 금지한 OFFSET/keepPreviousData/OLTP COUNT(DISTINCT)를 코드가 어김. → keyset 전환·analyticsDb 라우팅·lint 게이트. 원칙: 문서화된 규칙은 강제돼야 산다.
T-D · god 파일 (C2,M1,M2,M3)
1,276줄 메서드·4,636줄 큐 레지스트리 등 blast radius 거대. → 책임 단위 분리. 원칙: 리뷰 가능한 크기로.
T-E · 관측·위생 부채 (H5,H9,M9,M10)
Sentry 죽은 설정, dead/오배치 의존성, 문서 sprawl. → 에러 트래킹 실연결·의존성 정리·문서 아카이빙. 원칙: 설정과 의존성은 실제와 일치.

고치지 않을 것 (명시적 trade-off)

  • AI SDK 3중 프레임워크(Vercel ai+Mastra+LangGraph) 통합 안 함 — 모두 실 import 있고, 통합은 XL 리스크 대비 ROI 낮음. 신규 코드만 1개로 수렴 권고.
  • console.log 148건 일괄 치환 안 함 — 저영향, 점진 교체.
  • TS6/tsgo 프리뷰 스택 롤백 안 함 — 실제 typecheck는 tsgo, 정상 동작. 단 "beta"→고정핀만.
  • worker.ts 2,182줄 — 위임형 레지스트리라 분리 우선순위 낮음.

"Done"의 측정 신호

  • CI가 test:unit 실패 시 fail (C1 닫힘)
  • 발송 워커(bulk-email/warmup/sequence-activation) 테스트 ≥1 happy+1 failure path
  • Critical 발견 0건, 대용량 OFFSET 경로 0건(grep gate)
  • buyer-search 디렉토리 1개, dead 큐 배선 0
  • Sentry 또는 대체 에러 트래커가 라우트 미처리 에러 캡처

5. Task Plan

⚡ Quick Wins

High impact / S effort — 즉시 가능

태스크위치검증
QW1 root eslint:"*" 제거 + puppeteer 3중선언 정리package.json:23,24build green
QW2 @types/* 5개 deps→devDeps, "beta"→고정핀, Biome 버전 정렬elysia-server/package.json:74,85,117-121CI 재현성
QW3 orphan root yarn.lock 정리, CLAUDE.local.md 추적 의도 확인.gitignoregit clean
QW4 dead buyer-search-mastra 큐 배선 제거lib/queue/queues.ts:869grep 0
QW5 subscription-utils null-on-error → 에러 구분(티어 오판 차단):68,98,144단위테스트 1건
Milestone 0

안전망 (리팩토링 전 필수)

ID태스크Accept노력리스크의존
M0-1CI에 test:unit 추가(양 워크스페이스 ci:check에 병합)일부러 깬 테스트가 PR failS낮음
M0-2live-DB 게이트 테스트를 CI에 ephemeral PG로 실행(24개 skip 해제)env 없이도 실행, greenMM0-1
M0-3발송 핵심 워커 happy+failure 테스트 추가각 워커 ≥2 테스트L낮음M0-1
M0-4CI denylist 재검토(lint+typecheck+migration gate는 우회 불가로)denylist 계정도 게이트 통과 강제S
Milestone 1

Critical / 보안·정확성 수정

ID태스크Accept노력리스크의존
M1-1storeInboundEmailInDB(1,276줄) 책임 단위 분리함수당 ≤120줄, 인바운드 테스트 greenL높음M0-1,M0-3
M1-2디바이스 토큰 DB기반 만료·per-token revoke 스킴토큰 개별 폐기 가능L높음
M1-3device 토큰 시크릿 가드+JWT 결합 분리; 미인증 Slack 트리거 per-route rate limit약한 시크릿 거부, rate limit 동작S낮음
M1-4M7 검증 후 billing-gate 정확성 회귀 테스트DB 에러≠"구독없음" 테스트S낮음QW5
Milestone 2

고레버리지 (미래 작업 가속)

ID태스크Accept노력리스크의존
M2-1buyer-search canonical 확정·나머지 2 삭제(~65k→~39k LOC)디렉토리 1개, E2E greenXL높음M0-1, OQ1
M2-2대용량 OFFSET→keyset 전환 + FE 무한스크롤해당 경로 .offset( 0, lint gateLM0-1
M2-3COUNT(DISTINCT) analyticsDb 라우팅(특히 쓰기경로 lead-fit)부하테스트 spill 0M낮음
M2-4Sentry(or 대체) 실연결 — 라우트/서비스 미처리 에러 캡처의도적 에러가 대시보드 기록M낮음
M2-5신규 OFFSET/keepPreviousData/OLTP COUNT(DISTINCT) 차단 lint/check 스크립트위반 PR failM낮음M2-2
Milestone 3

품질·정리

ID태스크노력
M3-1queues.ts 38 addXxxJob→typed enqueue() 헬퍼, 도메인별 파일 분리L
M3-2FE lead-discovery v1 retire(mastra 확정 후) — M4 중복 제거M
M3-3god 컴포넌트 ChatRoom/UpdateNoteForm 책임 분리L
M3-4webhook/auth 경계 as any 타입화, FE @ts-ignore@ts-expect-errorS
M3-5문서 아카이빙, ADR 번호 정합, CONTRIBUTING↔CLAUDE.md DB섹션 링크M
M3-6console.log 148건 점진 pino 교체, exhaustive-deps suppress 집중 리뷰M

Top 3 구현 스케치

① M0-1 (CI에 test:unit) — 최우선·최대 ROI
  • 접근ci:checkbun test:unit(BE)·bun test(FE) 추가. long-running/integration은 분리 유지(M0-2).
  • 단계 (1) test:unit baseline green 확인 → (2) ci:check에 합류 → (3) ci.yml이 ci:check 호출하는지 확인
  • Gotcha 24개 live-DB 테스트는 env 없으면 skip→거짓 green. M0-1은 unit만, DB게이트는 M0-2로 분리. denylist(M0-4) 미해결 시 핵심 기여자 PR 여전히 우회.
② M2-1 (buyer-search 통합) — 최대 LOC 회수
  • 접근 프로덕션 트래픽 받는 구현 확정(라우트→큐→워커 역추적, alpha 로그 grep). vercel-ai가 canonical 후보.
  • 단계 (1) 진입 라우트·큐 enqueue 매핑 → (2) 활성 1개 확정(OQ1) → (3) 나머지 dead 확인 후 삭제 → (4) E2E green
  • Gotcha pro가 특정 티어/실험에만 분기될 수 있음(삭제 전 feature flag grep 필수). 빈 mastra 배선(QW4)부터 제거가 안전한 첫걸음.
③ M1-1 (storeInboundEmailInDB 분리) — 최고 blast radius
  • 접근 순수 단계 추출 — parseInboundHeaders→persistAttachments→matchReplyToEnrollment→writeInboundEmail→triggerClassification. 오케스트레이터는 얇게.
  • 단계 (1) 선행: 인바운드 골든 테스트(SendGrid payload fixture→DB 검증) → (2) private 메서드로 기계적 추출 → (3) 테스트 green 유지 점진 → (4) 경계 as any 타입화
  • Gotcha 23개 try/catch가 부분 실패를 흡수 중 — 추출 시 "best-effort 첨부 실패 무시" 같은 의도된 silent가 깨질 수 있음. 추출 전 각 catch 의도 주석화 필수. 테스트 없이 착수 금지.

6. Open Questions (사람 결정 필요)

#질문막힌 태스크
OQ1buyer-search 3구현 중 canonical은? 트래픽이 vercel-ai로 전량 가는가, pro/구버전이 티어·실험으로 분기되는가?M2-1
OQ2lead-discovery v1을 retire해도 되는가? /lead-discovery-v1가 최근까지 편집됨 — 의도적 폴백인가?M3-2
OQ3디바이스 토큰: 코드 주석은 "beta 가면 DB 토큰 교체" — 지금 beta 운영 중인가? 그렇다면 H1은 즉시.M1-2
OQ4에러 트래킹: Sentry 도입 vs DSN config 제거(Slack+pino 유지) 중 어느 방향?M2-4
OQ5CI denylist(핵심 기여자 skip)의 비용 vs 안전 — migration/auth gate만이라도 강제해도 되는가?M0-4
OQ6성능 타깃: keyset 전환의 현재 통증 임계(워크스페이스당 row 수)는?M2-2
경량 검토 영역(시간 제약): AI 에이전트 내부 로직(Mastra/LangGraph 노드 정확성), e2e/ 704파일 테스트 품질 상세, FE 디자인시스템(frontend-design.md), i18n 파이프라인. 핵심 20%(서버 services·routes·queue, FE pages·hooks, 보안 경계, CI/DB)에 집중.