보이스 클론 기반 cross-language 데이팅 앱 — 백엔드
Express 5 + Supabase + ElevenLabs + Vertex AI Gemini.
이 레포는 백엔드(Express + Supabase) 만 다룹니다. 모바일 클라이언트는 별도 레포 → perso-devrel/haru_FE
사용자가 본인 언어로 텍스트 메시지를 작성
→ 메시지를 자동으로 상대 언어로 번역
→ 사용자의 목소리를 입혀서 음성으로 전달
이 흐름이 메시지 송신 라우트의 비동기 파이프라인 6단계에 그대로 들어 있습니다.
번역된 메시지는 기계 음성이 아니라 송신자 본인의 클론 보이스로 재생됩니다. 말투·호흡·목소리 톤이 살아 있어, 사진이나 텍스트만으로는 만들 수 없는 "목소리만의 설렘" 을 의도합니다.
| 단계 | 일반 데이팅앱 | haru |
|---|---|---|
| 매칭 상대 탐색 | 사진 스와이프 | 블러된 사진 + 목소리 음성 청취 |
| 사진 공개 | 처음부터 전부 공개 / 결제·매칭으로 잠금 해제 | 채팅 5회 왕복 시 일부 공개, 10회 시 전체 공개 |
| 메시지 | 텍스트 즉시 노출 | 음성 1회 청취 후 텍스트 공개 |
이미 검증된 수요 위에 만듭니다.
- 한남-일녀 결혼 1,176건 (2024) — 전년 대비 +40.2%, 최근 10년 내 최고치
- 한남-일녀 결혼이 한녀-일남 결혼의 약 8배 (1,176건 vs 147건)
- 지리·문화적으로 근접 → 실제 만남으로 이어질 수 있는 조건이 갖춰져 있음
1차 출시 한국·일본, 확장 미국·태국·인도 순.
"Perso AI API로 이런 앱도 만들 수 있다" 는 레퍼런스 확보가 출발점.
현재 음성 클론 / TTS는 ElevenLabs API를 사용 중이며, 추후 Perso AI API로 전환 예정. 번역은 Vertex AI Gemini 2.5 Flash, 모더레이션은 OpenAI Moderation으로 구성.
코드는 Claude Code 와 함께 작성·아키텍처링했습니다. 변경 이력과 의사결정 회고가 CLAUDE.md 에 sprint 단위로 누적되어 있어, 외부에서 봐도 어떤 흐름으로 만들어졌는지 추적할 수 있습니다.
POST /api/matches/:id/messages ───┐
▼ │
[즉시 INSERT + 202 stub 응답] │ ◀── 송신자 클론 있으면 비동기 분기
│
┌───────────────────────────┘
▼
1) prepareTextForTTS() ㅋㅋㅋ → [laughs] (5개 언어 슬랭 + 이모티콘)
2) 모더레이션 사전 + OpenAI 노골 표현 즉시 차단
3) translateMessage() Gemini 2.5 Flash, audio tag 보존
4) synthesizeSpeech() ElevenLabs eleven_v3, 송신자 voice_id
5) Storage 업로드 voice-messages 버킷
6) DB UPDATE + Realtime 푸시 audio_status: processing → ready
▼
[수신자 채팅창: 번역문 + 송신자 목소리 재생 버튼]
수신자 측에서는 음성을 1회 청취해야 텍스트가 공개됩니다 (messages.listened_at + POST /api/matches/:matchId/messages/:messageId/listened + 채팅 목록 미리보기에서도 미청취 시 "새 메시지" 마스킹).
| 영역 | 스택 | 비고 |
|---|---|---|
| 런타임 | Express 5 + TypeScript 6 | tsx watch 로 핫리로드 |
| DB / Auth / Storage / Realtime | Supabase (@supabase/supabase-js 2.103+) |
service_role 로 RLS 우회, 서버 전용 |
| 음성 클론 / TTS | ElevenLabs (eleven_v3, stability=1.0) — 추후 Perso AI 전환 예정 |
inline audio tag ([laughs]/[sad]) |
| 번역 | Vertex AI Gemini 2.5 Flash | temperature=0.3, BLOCK_ONLY_HIGH |
| 모더레이션 2차 | OpenAI omni-moderation-latest |
사전 통과분만 호출, fail-open |
| 푸시 | Expo Push API | device_tokens + locale 본문 + DeviceNotRegistered cleanup |
| 입력 검증 | zod 4 | schemas/ + validateBody/validateQuery 미들웨어 |
| 테스트 | vitest 4 + supertest 7 | 20+ suite, 290+ cases |
# 1) BE — 이 레포
git clone https://github.com/perso-devrel/haru_BE
cd haru_BE
npm install
cp .env.example .env # 값 채우기 (아래 환경 변수 섹션)
# 2) Supabase 마이그레이션 — 002~025 를 Dashboard SQL Editor 에서 순서대로 실행
# Storage 버킷 3개 수동 생성: photos, voice-messages, voice-intro-audio
# 3) 개발 서버
npm run dev # http://localhost:3000 (Swagger: /docs)
# 4) FE 클론 + 띄우기 (별도 레포)
git clone https://github.com/perso-devrel/haru_FE
cd ../haru_FE
npm install --legacy-peer-deps
cp .env.example .env # 값 채우기 (FE 레포 README 참고)
npm run startPORT=3000
NODE_ENV=development
# Supabase
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<service_role> # 서버 전용, FE에 절대 노출 ❌
SUPABASE_ANON_KEY=<anon>
SUPABASE_JWT_SECRET=<jwt_secret>
# Google OAuth — Web/iOS/Android 3개 클라이언트 ID
GOOGLE_WEB_CLIENT_ID=...
GOOGLE_IOS_CLIENT_ID=...
GOOGLE_ANDROID_CLIENT_ID=...
# ElevenLabs
ELEVENLABS_API_KEY=...
# Vertex AI (Gemini 번역)
GOOGLE_APPLICATION_CREDENTIALS=credentials/gcp.json
GCP_PROJECT_ID=...
GCP_LOCATION=us-central1
# OpenAI Moderation
OPENAI_API_KEY=...
# 운영 정책
AUTO_FREEZE_REPORT_THRESHOLD=3 # 신고 누적 자동 freeze 임계치
ADMIN_DASHBOARD_ENABLED=true # 출시 시 false 로 반드시 차단src/
├── routes/ # /api/* 라우트
│ ├── auth.ts # Google OAuth, 토큰 갱신, 계정 삭제
│ ├── profile.ts # 프로필 CRUD, 사진 업로드
│ ├── voice.ts # ElevenLabs voice clone 관리
│ ├── discover.ts → swipe.ts # 추천 후보 + 스와이프 + 받은 좋아요 + quota
│ ├── match.ts # 매치 목록 + 메시지 + 파트너 상세 + 언매치
│ ├── message.ts # 메시지 송신 (즉시 + 비동기 파이프라인)
│ ├── block.ts # 차단 (양방향 가시성 차단)
│ ├── report.ts # 신고 → 누적 시 자동 freeze
│ ├── notifications.ts # 푸시 토큰 등록 + 선호 토글
│ ├── preference.ts # 매칭 선호도 (나이/성별/언어/국가)
│ └── admin.ts # dev/QA 전용 (ADMIN_DASHBOARD_ENABLED 게이트)
│
├── services/ # 외부 의존성 통합
│ ├── elevenlabs.ts # voice clone 생성 + eleven_v3 TTS
│ ├── translation.ts # Vertex AI Gemini 호출 + register-preserving 프롬프트
│ ├── voiceIntro.ts # 보이스 인트로 다국어 슬롯 합성 (ko/ja/en)
│ ├── pushNotifications.ts # Expo Push API + 차단/freeze/옵트아웃 가드
│ ├── openaiModeration.ts # OpenAI moderation 2차 layer
│ └── storage.ts # Supabase Storage 업로드 유틸 (UUID 파일명)
│
├── schemas/ # zod 입력 스키마
├── middleware/ # auth, validate, error, freezeGuard
├── constants/ # bioPhrasesCatalog, moderationDictionary
├── utils/ # textNormalization (audio tag 치환), errors
├── jobs/ # purgeExpiredAudio (30일 TTL sweep, in-process 24h interval)
└── index.ts # Express app + swagger + scheduler 부팅
supabase/migrations/ # 025개 forward-only 마이그
tests/ # vitest 20+ suite
scripts/ # seed-dev-accounts, cleanup-dev-accounts 등
| 경로 | 설명 |
|---|---|
/api/auth |
Google OAuth signInWithIdToken, refresh, 계정 삭제 (anonymize + 동기 cleanup) |
/api/profile |
프로필 CRUD, 사진 업로드 (JPEG/PNG/WebP, UUID 파일명) |
/api/voice |
ElevenLabs voice clone 등록 (재녹음 시 옛 voice 자동 cleanup) |
/api/discover |
4-단계 티어 추천 + 일일 50장 한도 + 받은 좋아요 + reciprocity boost |
/api/matches |
매치 목록 (RPC get_match_summaries_v3), 파트너 상세, 메시지 CRUD, 청취 / 언매치 |
/api/block, /api/report |
차단 + 신고 누적 자동 freeze |
/api/notifications |
Expo Push 토큰 register/unregister + 선호 GET/PATCH |
/api/preferences |
매칭 선호도 (나이/성별/언어/국가) |
/docs |
Swagger UI |
전체는 CLAUDE.md 에 있어요. 자주 부딪히는 것만:
- 모든 인증 필요 라우트는
authMiddleware를router.use()로 적용 - 입력 검증: zod 스키마 +
validateBody/validateQuery미들웨어 - 에러 처리:
AppError클래스 +errorMiddleware에서instanceof판별 - 매치 ID 는
user1_id < user2_id정렬 보장 (DB UNIQUE 제약) - 매치 삭제는 soft delete (
unmatched_at,unmatched_by) - 비동기 처리 (메시지 TTS, voice intro 합성): fire-and-forget +
.catch()로깅 - 신규 user-linked 테이블 추가 시
auth.ts:deleteAccount에 동기 cleanup 추가 필수 (GDPR/PIPA 데이터 삭제권) - 마이그레이션은 forward-only, 파일명은
NNN_<name>.sql패턴만
| 자산 | 정책 |
|---|---|
음성 학습 원본 (voice-samples) |
ElevenLabs 클론 생성 직후 즉시 폐기 — 버킷 자체 제거됨 (mig 023) |
합성된 음성 메시지 (voice-messages) |
청취 후 30일 자동 폐기 (in-process scheduler), 재청취 시 재합성 (mig 025) |
| ElevenLabs voice_id | 계정 탈퇴 시 ElevenLabs API 삭제 호출 (auth.ts:deleteAccount cleanup task) |
| 차단된 사용자 메시지 | 양방향 가시성 차단 (수신자 GET 필터 + Realtime 필터) |
| 모더레이션 audit | moderation_blocks 90일 보존, service_role 전용 RLS |
npm test # 전체 vitest
npx vitest run tests/message.test.ts # 단일 파일tests/setup.ts 에서 env / Supabase / ElevenLabs / Storage / Gemini 를 전역 모킹.
tests/helpers.ts 의 generateTestToken(userId?) + createMockSupabaseQuery() 로 빠르게 작성.
신규 외부 의존성 호출은 error destructure + 가시화 룰 — silent-success 회귀 차단.
- FE (Expo + React Native) →
perso-devrel/haru_FE
추가로:
CLAUDE.md— 전체 컨벤션 + 메시지 파이프라인 상세 + sprint 회고- Swagger UI —
npm run dev후http://localhost:3000/docs
MIT. 음성 클론 / 번역 / 실시간 채팅을 결합한 백엔드가 어떻게 구성될 수 있는지에 대한 레퍼런스로 자유롭게 참고하세요.