Implementation Plan · 앱린다(Rinda) Mobile

전화 → 액션플랜 자동화
AI 영업비서 최적 구현 계획

레퍼런스 리서치(2026)에서 확정한 기술 제약을 전제로, 기존 telinfo Flutter 앱 + 앱린다 백엔드(Elysia·BullMQ·Postgres) 위에 "받은 전화 녹음→STT→액션플랜" 과 "부재중 자동 SMS" 두 기능을 올리는 단계별 구현 설계.

작성 · 2026-06-29 스택 · Flutter + Elysia/Bun + 리턴제로 STT + Gemini 전략 · 갤럭시 우선 MVP → iOS fallback

🎯 설계 핵심 5

  • 두 기능을 분리해 각각 최적 경로로. ① 받은 전화 = 갤럭시 네이티브 녹음파일 자동수집 → 서버 STT/AI ② 부재중 = Android telephony 단말 직접 SMS(MVP) / 050·070 착신전환(제품화).
  • 최대 블로커는 "갤럭시 통화녹음 폴더 보호". 이미 telinfo가 부딪힌 문제(/Recordings/Call/ 일반 권한 read 불가). 해결 = SAF(Storage Access Framework) 디렉토리 영속 권한 — 사용자가 폴더를 1회 지정하면 재부팅 후에도 read 가능, Play 정책 안전.
  • 녹음은 새로 안 한다. OS가 만든 파일을 읽기만. 마이크·접근성 권한 미사용 → Play 통화녹음 금지(2022.5) 조항 비해당. 자체 녹음 시도는 금물.
  • 기존 앱린다 인프라 재사용. S3 업로드(s3.service)·BullMQ 큐·Drizzle/Postgres·SSE 진행률·Gemini(agent-model-settings)·디바이스 토큰 인증 — 신규 인프라 거의 0.
  • STT는 리턴제로(RTZR OpenAPI). 한국어 전화망 CER 3.56% 1위 + 2화자 diarization 배치. PIPA 민감 고객용 온프렘 Whisper 옵션은 후순위.
2~3주
갤럭시 MVP (받은전화+부재중)
~₩40
통화 1건 처리 추정 (STT+LLM)
0개
신규 인프라 (기존 재사용)

00 현재 상태 — 이미 있는 것

기존 mobile/telinfo (Flutter) 가 상당 부분을 구현해 둠. 이 위에 파이프라인을 얹는다.

✅ 구현 완료 (telinfo)

  • 통화로그 수집·일괄 업로드call_log, _bulkUploadCallLogs()
  • 녹음 파일 목록·재생·업로드RecordingInfo, recording_player_page, _bulkUploadRecordings()
  • 연락처·즐겨찾기flutter_contacts
  • SMS 권한·송수신 기반telephony 0.2.0
  • 서버 업로드 클라이언트RindaApiClient (디바이스 토큰, m4a MIME, s3.service 정합)

🚧 막힌 곳 / 미구현

  • 갤럭시 녹음폴더 보호/Recordings/Call/ 일반 권한 read 불가 (telinfo home_page:1127 안내문 존재) → §04에서 해결
  • 신규 녹음 자동 감지·자동 업로드 — 현재 수동 일괄만
  • STT → 액션플랜 백엔드 파이프라인 — 미구현
  • 부재중 자동 SMS 트리거 — telephony 기반만 있고 자동화 로직 없음
  • 액션플랜 모바일 표시 UI — 미구현

01 전체 아키텍처

단말 OS 제약을 우회하는 두 플로우. 받은 전화는 단말이 만든 녹음을 끌어오고, 부재중은 단말/서버에서 즉시 응답한다.

📞 받은 전화 → 액션플랜

📱갤럭시 자동녹음OS가 /Recordings/Call 에 .m4a 생성
👀Flutter 감지SAF 영속권한 + 주기 스캔으로 신규 파일
☁️S3 업로드RindaApiClient → s3.service
🎙리턴제로 STTBullMQ · 2화자 diarization
🧠Gemini 분석요약·니즈·딜단계·액션
📲모바일 표시SSE 푸시 + CRM 적재

📵 부재중 → 자동 SMS

MVP — Android 단말 직접 (telephony)
부재중 감지PhoneState / call_log
💬SmsManager 발송"출장중입니다" 즉시

무료·즉시. iOS 불가. SEND_SMS 권한.

제품화 — 050/070 착신전환 + 서버
☎️무응답 전환*61* → 050/070
🪝서버 webhook발신번호·시각
💬알림톡+대체문자크로스플랫폼

iOS 포함 전 단말. 운영비 발생.

기술 스택 결정

레이어선택근거 (기존 자산/제약)
모바일Flutter (telinfo 확장)이미 통화로그·녹음·SMS·업로드 구현됨. 재작성 불필요
녹음 수집SAF 영속권한 + 주기 스캔갤럭시 보호폴더 유일 정공법. Play 안전 (마이크/접근성 미사용)
백엔드Elysia/Bun routes→services→db앱린다 표준 3-layer
파일저장S3 (s3.service)RindaApiClient가 이미 업로드 중
비동기 처리BullMQ + RedisSTT·LLM은 수초~수십초 → 큐 필수. retry·backoff 내장
DBPostgres (Drizzle), UUIDv7영속·공유 엔티티 SSOT. keyset 페이지네이션
STT리턴제로 RTZR OpenAPI한국어 전화망 CER 3.56% 1위, 2화자 diarization 배치
LLMGemini (gemini-3-flash-preview)agent-model-settings.service SSOT, 비용·속도
실시간 알림SSE + Redis Pub/Sub앱린다 진행률 UI 표준. 액션플랜 완료 푸시
부재중 SMStelephony(MVP) → 050/070(확장)Android 즉시 무료 → iOS 포함 시 서버 경유

02 핵심 블로커 해결 — 갤럭시 녹음폴더 접근

telinfo가 이미 부딪힌 "/Recordings/Call/ 일반 권한 read 불가". 이게 받은 전화 기능의 사활을 가른다.

방법접근Play 정책UX판정
READ_MEDIA_AUDIO일반 미디어만안전무마찰 보호된 /Recordings/Call 미인덱싱
MANAGE_EXTERNAL_STORAGE전체 파일거부 위험무마찰 사이드로드/엔터프라이즈만
SAF 디렉토리 영속권한지정 폴더 read/write안전최초 1회 폴더 선택◎ 채택

✅ 채택: SAF (Storage Access Framework) 영속 권한

사용자가 최초 1회 ACTION_OPEN_DOCUMENT_TREE/Recordings/Call 폴더를 선택 → takePersistableUriPermission() 로 영속화 → 재부팅 후에도 DocumentFile/ContentResolver 로 read. 마이크·접근성 권한 불필요 → Play 통화녹음 금지 조항 비해당. Flutter는 saf_util/saf_stream 패키지 또는 플랫폼 채널로 구현.

// telinfo: SAF 영속권한 획득 + 신규 녹음 스캔 (의사코드)
final treeUri = await saf.openDocumentTree();           // 1회: /Recordings/Call 선택
await saf.takePersistableUriPermission(treeUri);        // 재부팅 내성
prefs.setString('callDirUri', treeUri.toString());

// WorkManager 주기 작업 (15분) — 업로드 안 된 새 파일만 diff
final files = await saf.listFiles(treeUri);
final fresh = files.where((f) => !uploaded.contains(f.id) && f.name.endsWith('.m4a'));
for (final f in fresh) {
  final callId = await api.uploadRecording(f, matchByTimestamp: callLog);  // 통화로그와 시각 매칭
  uploaded.add(f.id);
}

🔗 통화로그 ↔ 녹음파일 매칭

녹음 파일명/수정시각과 call_log 의 통화 시작시각·번호를 매칭해 "누구와의 통화인지"를 확정 → 연락처/딜에 연결. 파일명 규칙(Call recording_{상대}_{timestamp}.m4a)이 OEM별로 달라 시각 기반 매칭을 1차로, 파일명 파싱을 보조로.

03 데이터 모델 (Drizzle / Postgres)

영속·공유 엔티티는 Postgres SSOT. 신규 PK는 UUIDv7, 조회는 keyset.

// elysia-server/src/db/schema/calls.ts
export const calls = pgTable('calls', {
  id: uuid().default(sql`uuidv7()`).primaryKey(),
  workspaceId: uuid().notNull().references(() => workspaces.id),
  contactId: uuid().references(() => contacts.id),     // 매칭된 거래처
  direction: text({ enum: ['inbound','outbound','missed'] }).notNull(),
  peerNumber: text().notNull(),
  startedAt: timestamp().notNull(),
  durationSec: integer(),
  recordingS3Key: text(),                               // 녹음 있을 때만
  status: text({ enum: ['uploaded','transcribing','analyzing','done','failed'] }).notNull(),
  createdAt: timestamp().defaultNow(),
});

export const transcripts = pgTable('transcripts', {
  id: uuid().default(sql`uuidv7()`).primaryKey(),
  callId: uuid().notNull().references(() => calls.id),
  provider: text().default('rtzr'),
  fullText: text(),
  segments: jsonb(),        // [{speaker, start, end, text}] 화자라벨
  confidence: real(),
});

export const actionPlans = pgTable('action_plans', {
  id: uuid().default(sql`uuidv7()`).primaryKey(),
  callId: uuid().notNull().references(() => calls.id),
  summary: text(),
  customerNeeds: jsonb(),    // 추출된 고객 니즈
  dealStage: text(),         // 딜 단계 추론
  nextActions: jsonb(),      // [{action, dueDate, priority}]
  createdAt: timestamp().defaultNow(),
});

export const autoReplyLogs = pgTable('auto_reply_logs', {
  id: uuid().default(sql`uuidv7()`).primaryKey(),
  workspaceId: uuid().notNull(),
  peerNumber: text().notNull(),
  message: text().notNull(),
  channel: text({ enum: ['device_sms','alimtalk','sms_fallback'] }).notNull(),
  sentAt: timestamp().defaultNow(),
});

04 STT → 액션플랜 파이프라인 (BullMQ)

업로드 후 비동기 2단계 잡. 각 단계 실패는 BullMQ retry·backoff. 진행률은 SSE로 모바일에 푸시.

⬆️uploadS3 저장 + calls row(status=uploaded)
🎙job: transcribe리턴제로 배치 STT, 2화자 diarization → transcripts
🧠job: analyze화자라벨 전사문 → Gemini 구조화 출력 → action_plans
📡SSE notifystatus=done, 모바일 즉시 갱신
// elysia-server/src/services/call-analysis.service.ts (핵심만)
// 1) 리턴제로 배치 STT (화자분리)
const stt = await rtzr.transcribe({
  audioUrl: s3.signedUrl(call.recordingS3Key),
  config: { diarization: { enable: true, speakers: 2 }, language: 'ko' },
});

// 2) Gemini 액션플랜 (구조화 출력)
const plan = await gemini.generate({
  model: settings.get('call-analysis'),   // agent-model-settings SSOT
  systemPrompt: SALES_ANALYSIS_PROMPT,           // 영업 도메인: 니즈·딜단계·이의제기·next step
  input: formatSpeakerLabeled(stt.segments),     // "[상담원] … [고객] …"
  responseSchema: ActionPlanSchema,              // summary, customerNeeds, dealStage, nextActions[]
});

// 3) 저장 + SSE 푸시
await db.insert(actionPlans).values({ callId: call.id, ...plan });
redis.publish(`call:${call.id}`, { status: 'done' });

⚙️ 영업 특화 = 차별화 지점

단순 요약(에이닷·갤럭시AI 수준)이 아니라 영업 도메인 프롬프트로 ① 고객 니즈/페인포인트 ② 딜 단계 추론 ③ 이의제기 대응 ④ 구체적 next action(기한·우선순위)을 구조화 출력. 통화 N건을 거래처/딜 단위로 누적해 파이프라인을 만드는 것이 기성 통화요약 대비 핵심 우위.

05 부재중 자동 SMS

MVP는 Android 단말 직접 발송(무료·즉시), 제품화·iOS 대응은 050/070 착신전환 + 서버.

① MVP — telephony (Android)

// telinfo: 부재중 감지 → 자동 SMS
Telephony.instance.listenIncomingSms; // 권한
// PhoneState 부재중 이벤트 / call_log 폴링
if (call.type == CallType.missed &&
    settings.autoReplyEnabled) {
  await Telephony.instance.sendSms(
    to: call.number,
    message: settings.autoReplyText, // "현재 출장중…"
  );
  api.logAutoReply(call.number, 'device_sms');
}
  • 무료·즉시 단말에서 바로 발송
  • 제약 Android 전용, SEND_SMS 권한, Play 심사 유의
  • 발송 조건(업무시간·연락처별·중복방지) 설정 제공

② 제품화 — 050/070 + 서버

  • 무응답 조건부 착신전환 *61* → 050/070 가상번호(서버 종단)
  • 서버 webhook 발신번호·시각 수신 → 음성안내 재생
  • 카카오 알림톡 + 대체문자 발송 (실패 시 SMS fallback)
  • 크로스플랫폼 iOS 포함 전 단말, 단말 권한 불필요
  • 비용 가상번호 월정액 + 발송 건당

주의: 050 안심번호는 알림톡 발신번호로 못 씀 → 알림톡은 등록된 비즈 발신번호, 050은 수신 라우팅 전용으로 분리.

06 구현 단계 (DAG)

위상정렬 레이어. 같은 레이어는 병렬 가능, *=Critical Path, [S≤50 / M≤150 / L≤300 LOC].

L0 (기반) L1 (코어 파이프라인) L2 (자동화·UI) L3 ┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ │N0.1* M DB스키마│═══▶│N1.1* M STT/분석 │═══▶│N2.1* M 녹음 자동 │═══▶ N3.1* S │ +업로드 라우트 │ │ 파이프라인(BullMQ)│ │ 감지(SAF+스캔) │ 통합·번인 └──────────────┘ └──────────────────┘ └─────────────────┘ E2E ┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ │N0.2 S 리턴제로 │───▶│N1.2 S Gemini 영업 │───▶│N2.2 M 액션플랜 │ │ /Gemini 연동 │ │ 프롬프트·스키마 │ │ 모바일 UI(SSE) │ └──────────────┘ └──────────────────┘ └─────────────────┘ ┌──────────────┐ ┌─────────────────┐ │N0.3 S 부재중 │───────────────────────────▶│N2.3 S 부재중 설정 │ │ SMS(telephony) │ │ UI·조건·로그 │ └──────────────┘ └─────────────────┘
N0.1*[M]

DB 스키마 + 업로드 라우트

calls/transcripts/action_plans/auto_reply_logs 마이그레이션 + 녹음·통화로그 업로드 라우트(workspaceAuth 매크로).

  • bun db:generate 4개 테이블 (UUIDv7)
  • 업로드 라우트 → S3 + calls row 생성
  • RindaApiClient 정합 확인
verify: 녹음 업로드 시 calls row(status=uploaded) 생성 + S3 키 저장
N0.2[S]

리턴제로 / Gemini 연동 병렬 N0.1

RTZR OpenAPI 키·클라이언트, Gemini는 기존 agent-model-settings 재사용. Infisical에 시크릿 등록.

verify: 샘플 m4a → 전사 텍스트 / 샘플 텍스트 → 액션플랜 JSON
N0.3[S]

부재중 SMS (telephony) 병렬

telinfo에 부재중 감지 → 자동 SMS 발송 + 서버 로그. 가장 빨리 체감되는 기능.

verify: 부재중 발생 시 설정 문구가 발신자에게 발송 + auto_reply_logs 기록
N1.1*[M]

STT→분석 파이프라인 (BullMQ) N0.1·N0.2

call-processing 큐: transcribe → analyze 2 잡. retry/backoff. SSE 진행률.

  • transcribe 잡: 리턴제로 배치 + 2화자 diarization → transcripts
  • analyze 잡: Gemini 영업 프롬프트 → action_plans
  • Redis pub/sub → SSE 푸시
verify: 업로드 1건 → 수십초 내 action_plan 생성, status=done
N2.1*[M]

갤럭시 녹음 자동 감지 (SAF) N1.1

§02 SAF 영속권한 + WorkManager 주기 스캔 → 신규 .m4a diff 자동 업로드 + 통화로그 매칭.

verify: 통화 종료 후 무조작으로 액션플랜이 앱에 도착(15분 내)
N2.2[M]

액션플랜 모바일 UI (SSE) N1.1

통화별 요약·니즈·딜단계·next action 카드 + 거래처 단위 파이프라인 뷰. SSE 실시간 갱신.

verify: 액션플랜 완료 시 토스트/배지 + 카드 즉시 표시
N3.1*[S]

통합 E2E + 번인 전체

실기기(갤럭시) 통화→녹음→자동업로드→액션플랜→표시 happy/edge/error 케이스. --repeat-each 번인. send-ci.sh 통과.

verify: 전 케이스 green + 매칭 실패·STT 실패·SMS 중복 방지 검증

07 리스크 & 대응

리스크영향대응
갤럭시 자동녹음 미설정/지역차단입력 0온보딩에서 자동녹음 ON 가이드 + 미설정 감지 경고. 한국 타깃은 토글 노출됨
SAF 폴더 선택 UX 마찰도입 이탈스크린샷 가이드 + 1회만. 미부여 시 수동 업로드 fallback
OEM별 녹음 폴더·파일명 상이매칭 실패삼성 우선 지원 명시. 시각 기반 매칭 1차 + 파일명 파싱 보조
iOS 자동화 불가iOS 미지원share sheet 수동 업로드 + 부재중은 050/070 서버. 갤럭시 우선 포지셔닝
Play 심사 (SMS/저장소 권한)출시 지연READ_MEDIA_AUDIO 대신 SAF, 마이크/접근성 미요청. SEND_SMS는 핵심기능 정당성 문서화
통비법·녹음 고지법적일방 당사자 녹음(합법) 전제. 녹음 고지음은 OS 강제. 약관·동의 UX
STT/LLM 비용 증가마진배치 처리(실시간X), 통화당 ~₩40 추정. 무음/초단통화 스킵, 길이 기반 과금 모니터
부재중 SMS 중복·오발송고객 불만번호별 쿨다운·업무시간·연락처 화이트리스트. auto_reply_logs로 멱등

08 MVP 범위 & 일정

🎯 MVP (갤럭시, 2~3주)

  • 받은 전화 자동녹음 → SAF 자동수집 → 리턴제로 STT → Gemini 액션플랜 → 모바일 표시
  • 부재중 telephony 단말 직접 SMS (조건·로그)
  • 거래처 매칭 통화로그↔녹음 시각 매칭
  • 플랫폼 갤럭시(삼성) 전용

📈 v2 (제품화)

  • 거래처/딜 파이프라인 누적 뷰 + CRM 양방향
  • 부재중 050/070 착신전환 + 알림톡 (iOS 커버)
  • iOS share sheet 수동 + 부재중 서버
  • 후속 자동화 시퀀스 메일·리마인더 연동 (앱린다 기존 시퀀스 재사용)
  • 온프렘 STT PIPA 민감 고객용 Whisper 옵션

✅ 왜 이 계획이 최적인가

① 기존 telinfo·앱린다 인프라 재사용으로 신규 구축 최소 ② 단말 OS 제약을 정면 우회(녹음 안 만들고 읽기만 + 착신전환) ③ 한국어 전화망 최고 정확도(리턴제로) ④ 영업 도메인 액션플랜으로 기성 통화요약(에이닷·갤럭시AI) 대비 명확한 차별화 ⑤ 갤럭시 MVP로 2~3주 내 VITO 이탈 사용자 체감 가치 전달.