Rinda · 받은 답장함 (replied-emails)

자동응답 과다 유입 — 3대 개선 기획서

자동응답이 받은 답장함을 어지럽히는 문제를 발송 단계(예방) · 분류 단계(정확도) · 조회 단계(필터) 세 층위에서 끊어내는 설계안. 현행 코드 근거와 변경 지점, 우선순위까지 정리.

대상 화면 app.rinda.ai/replied-emails 작성 2026-06-10 범위 elysia-server · admin 상태 기획 (구현 전)

한눈에 보기

세 요청은 같은 증상("자동응답이 너무 많이 보인다")의 서로 다른 층위다. 하나만 고치면 나머지가 새어 나온다 — 세 층을 함께 막는다.

① 예방
국가별 영업일에만 발송 → 부재·휴가 자동응답 유입 자체를 감축
② 정확도
자동응답을 neutral로 흘리지 않게 분류·신호 융합
③ 필터
받은 답장함에서 자동응답 표시/숨김 토글 제공
#기능핵심 변경레이어난이도가치/리스크
1 국가별 영업일 발송 하드룰 Redis Lua + 공휴일 캘린더로 슬롯 예약 시 비영업일을 다음 영업일로 이동 발송 워커 L (높음) 가치 高 / 리스크 中 (발송 타이밍 회귀 주의)
2 자동응답 중립 오분류 개선 OOO 프롬프트 강화 + isAutoReply 휴리스틱 ↔ intent 융합 보정 AI 분류 S~M (낮음) 가치 中 / 리스크 低 (가장 빠른 ROI)
3 자동응답 표시/숨김 필터 autoReply 쿼리 파라미터 + 사이드바 토글 조회 (FE+BE) M (중간) 가치 中 / 리스크 低 (②와 결합 시 효과 극대)
설계 원칙
②의 분류 신호(isAutoReply + intent out_of_office)는 ③ 필터의 판정 기준이자 ①의 효과 측정 지표가 된다. "자동응답"의 정의를 한 곳에서 통일(SSOT)하고 세 기능이 이를 공유한다.

현행 구조 — 받은 답장함

FE는 URL params만 상태 SSOT(Jotai 미사용), BE는 스레드 기반 keyset 무한스크롤. 분류는 AI 8-intent → 화면은 3-bucket으로 축약.

근본 문제 — 두 자동응답 신호가 분리
헤더 휴리스틱(emails.isAutoReply)과 AI intent(out_of_office)가 서로 모른다. 휴리스틱이 자동응답으로 잡아도 LLM이 neutral을 주면 화면 bucket에서 사라지고(②), 필터로도 걸러낼 수 없다(③). ①로 유입을 줄여도 남은 건 여전히 새어 나온다.

1국가별 영업일 발송 하드룰 (Redis Lua)

발송 워커Redis Lua난이도 L

수신자 국가의 주말·공휴일에는 발송하지 않는다. 비영업일 발송이 부재중/휴가 자동응답을 대량 유발하므로, 유입을 원천에서 줄인다.

문제 정의

현재 발송 스케줄러는 provider별 간격(throttle)과 일/월 한도만 본다. 수신자 현지 시각이 토요일이든 공휴일이든 발송된다 → 부재중 자동응답이 즉시 회신되어 받은 답장함에 쌓인다.

현황 — 이미 깔려 있는 토대

자산위치상태
영업일/영업시간 유틸utils/timezone.ts:91 calculateScheduledTime · skipToNextBusinessDay · clampToBusinessHours존재 (주말만, 공휴일 미반영)
BusinessCalendar 스켈레톤modules/email-automation/domain.ts:80 isBusinessOpen · nextBusinessSlotholidays Set 비어있음
캘린더 어댑터services/business-calendar.adapter.ts공휴일 소스 미연결
수신자 국가/타임존db/schema/leads.ts:104 country · :108 timezone컬럼 존재
슬롯 예약 Lua (예약 시점)lib/queue/sequence-email-scheduler.ts:91 RESERVE_NEXT_SLOT_LUA원자적 예약 already
발송 직전 게이팅workers/.../steps/pre-check.ts:45 · send-email.ts:613가드 삽입 지점
핵심 인사이트
바닥부터 만들 필요 없다. timezone.ts의 영업일 계산 + BusinessCalendar 스켈레톤이 이미 있다. 빠진 건 ⓐ 공휴일 데이터 소스, ⓑ 예약 Lua에 비영업일 이동 로직, ⓒ Lead 국가→캘린더 연결 세 가지뿐.

제안 설계

의사 코드 — 비영업일 이동 Lua

-- KEYS[1]=throttle slot key  ARGV: baseMs, tz offset(min), holiday set key, openHour
local base = tonumber(ARGV[1])
local localDay = day_of_week(base, ARGV[2])      -- 0=일 .. 6=토
local dateStr = ymd(base, ARGV[2])              -- "2026-06-10"
while localDay == 0 or localDay == 6
      or redis.call('SISMEMBER', ARGV[3], dateStr) == 1 do
  base = next_day_at(base, ARGV[2], tonumber(ARGV[4]))  -- 익일 09:00 현지
  localDay = day_of_week(base, ARGV[2]); dateStr = ymd(base, ARGV[2])
end
redis.call('SET', KEYS[1], base)
return base

변경 파일

리스크 · 주의
  • 발송 타이밍 회귀 영역 — SES/SendGrid 상향 정책과 충돌 없는지 회귀 테스트 필수 (config.ts provider 정책).
  • country/timezone 결측 리드 다수 → 워크스페이스 기본 캘린더 fallback 정책 필요.
  • "발송 지연되는 것처럼 보임" 카피 가드 — copywriting-ux.md 금지어 회피("워밍업"·"보호 모드" X → "현지 영업일에 맞춰 예약됨" 류).

2자동응답 중립 오분류 개선

AI 분류프롬프트난이도 S~M · 최단 ROI

명백한 자동응답인데 neutral로 분류돼 화면 bucket·필터에서 누락되는 문제. 프롬프트 강화와 신호 융합으로 근본 해결(특정 케이스 패치 지양).

문제 정의 — 두 갈래

원인코드 근거증상
OOO 정의가 빈약ai-classification.service.ts:135 — "Automated out-of-office or vacation reply" 한 줄뿐한국어 부재중/연차/휴가 자동회신, 다국어 OOO를 놓침 → neutral
애매하면 neutral fallback:174 isReplyIntent(parsed.intent) ? … : "neutral"확신 낮은 자동응답이 전부 중립으로 흡수
휴리스틱과 미연결auto-reply-detection.ts → emails.isAutoReply (intent와 별개 저장)헤더로 자동응답 확정해도 intent는 보정 안 됨

제안 설계

// ai-classification.service.ts — parse 직후 신호 융합
if (email.isAutoReply && (intent === "neutral" || intent == null)) {
  intent = "out_of_office"          // 결정적 헤더 신호 우선
  reasoning += " [overridden by header heuristic]"
}

변경 파일

왜 가장 먼저 하나
프롬프트 + 후처리 수정만으로 위험이 낮고, ③ 필터가 걸러낼 "자동응답" 모집단의 정확도를 먼저 확보해야 ③이 의미 있게 동작한다. ②가 ③의 전제.

3자동응답 표시/숨김 필터

조회FE + BE난이도 M

받은 답장함에서 자동응답을 숨기고/보기/자동응답만 보기로 토글. 현재는 intent 양성 선택만 가능하고 "제외" 옵션이 없다.

문제 정의

필터는 intentFilter === bucket 양성 선택뿐(email-search.service.ts:1102). auto_reply bucket을 골라야만 out_of_office가 보이고, 반대로 "자동응답 빼고 보기"는 불가능. emails.isAutoReply 컬럼은 저장돼 있으나 쿼리 필터로 노출되지 않는다applyEmailRepliesFilterConditionsisAutoReply 분기 자체가 없음.

제안 설계 — 3-state 토글

의미SQL 조건 (자동응답 = OR 융합)
include전체 (기본)조건 없음
exclude자동응답 숨기기NOT (emails.isAutoReply OR intent = 'out_of_office')
only자동응답만emails.isAutoReply OR intent = 'out_of_office'
자동응답 정의 = 두 신호 OR (②와 공유)
헤더 휴리스틱(isAutoReply)과 AI intent(out_of_office)를 OR로 묶어 "자동응답"을 판정. ②의 융합 보정이 들어가면 두 신호가 대체로 수렴하지만, OR로 둬야 경계 케이스도 안전하게 잡힌다.

구현 포인트

주의 — 쿼리 경로 분기
email-search.service.ts:354needsEmailRepliesJoin은 intent/sentiment/tags 필터 시에만 emailReplies를 join한다. isAutoReply 필터는 join 없이도 가능하므로, 두 경로(join 있는 subquery · 없는 fast-path) 모두에서 조건이 적용되도록 배치해야 누락이 없다. intent='out_of_office' OR 절을 쓰면 join이 필요해지니, 성능을 위해 isAutoReply 단일 컬럼 우선 + intent는 ②의 융합으로 수렴시키는 방식을 권장.

우선순위 · 실행 순서

WSJF(가치/소요) 기준. ②를 먼저(저위험·③의 전제) → ③(②의 정확도 활용) → ①(별도 영역·고가치·고비용).

┌─ Stage 1 ──────────┐ ┌─ Stage 2 ──────────┐ ┌─ Stage 3 ───────────────┐ │ ② 분류 정확도 │ ══▶ │ ③ 조회 필터 │ │ ① 영업일 발송 하드룰 │ │ 프롬프트 + 신호융합 │ │ autoReply 파라미터 │ │ 공휴일소스+Lua+연결 │ │ [S~M] 저위험 │ │ +사이드바 토글 [M] │ │ [L] 독립 트랙·병렬 가능 │ └────────────────────┘ └────────────────────┘ └─────────────────────────┘ 선행(전제) ②의 정의 SSOT 공유 발송 워커 (별 영역)
순서기능성공 기준 (검증)의존
1② 분류 정확도다국어 OOO 테스트 통과 · isAutoReply인데 neutral인 케이스 0건(융합 후)없음
2③ 조회 필터"자동응답 숨기기" on → 자동응답 0건 노출 · 두 쿼리 경로 모두 적용② (자동응답 정의 SSOT)
3① 영업일 하드룰비영업일 예약 → 다음 영업일 09:00 이동 · provider throttle 회귀 없음 · 자동응답 유입률 ↓ 측정독립 (병렬 착수 가능)
측정 루프
①의 효과는 ②/③이 만든 "자동응답" 지표로 측정한다 — 영업일 룰 적용 전후 isAutoReply 유입률을 reply-classifier-analytics로 비교. 세 기능이 하나의 피드백 루프를 이룬다.

다음 결정 필요 사항