신규 3개 기능 수정 계획 — 알파 브랜치 (send-grid-test)

대상 — F1 시퀀스 종료 후 무답신 reminder · F2 OOO 자동응답 시 영업시간 재스케줄 · F3 "담당자 아님" 응답 시 forward · 작성 2026-05-29

TL;DR — 3개 기능 모두 hook 지점이 1~3곳에 집중되어 surgical. F2 는 reply-automation 파이프라인이 이미 완성되어 delay 계산만 동적으로 바꾸면 됨. F1 은 enrollment-progress.service.ts:319isLastStep 분기 1곳. F3 만 새 intent + 새 테이블 + 승인 UI 까지 필요.

1핵심 hook 지점 (가장 작게 건드릴 곳)

기능Primary hook
F1 reminderservices/enrollment-progress.service.ts:319isLastStep && status='completed' 분기 → BullMQ delayed job 등록
F2 OOO 재스케줄services/reply-automation-resume.service.ts:26 scheduleEnrollmentResume(delayDays) 호출 직전 — delayDays 를 fixed 7일 대신 OOO 본문 LLM 파싱 결과로 교체. executeEnrollmentResume() 에서 다음 step scheduledAtrecipient-send-time-recommendation 결과로 보정
F3 forwardservices/reply-automation.service.ts:79 processReplyAutomation() 의 intent 분기에 wrong_recipient 추가 + reply-tags-auto-classify.worker.ts:92 의 분류 enum 확장

2F1 — 시퀀스 종료 후 무답신 reminder

종류파일라인작업
수정services/enrollment-progress.service.ts250–326 (319)updateEnrollmentProgress()isLastStep 분기에서 finalReminderConfig.enabled 면 delayed job enqueue
수정db/schema/sequences.ts91–154sequencesfinalReminderConfig: { enabled, delayDays(=5), maxRetries(=1) } jsonb 추가
수정db/schema/sequences.ts193–234sequence_enrollmentslastReminderSentAt, reminderAttemptCount 컬럼 추가
수정services/personalized-email-generation.service.ts68+mode: 'final_reminder' 분기 — 본문 짧게(1–2문장), 원본 step 본문 reference
수정services/email-generation/prompt-builder.ts35–97reminder mode 일 때 system prompt 분기 ("정중하게 확인 요청, 8줄 이하")
수정services/email-generation/step-strategies.ts · step-templates-ko.tsfinalReminder 전략 추가
수정lib/queue/types.ts · queues.ts8–172 / 280SEQUENCE_FINAL_REMINDER 큐 등록
신규workers/bullmq/sequence-final-reminder.worker.tsrepliedAt IS NULL 재확인 ② recordUsage('email_send')emailService.sendEmail()lastReminderSentAt 갱신
수정workers/bullmq/index.ts272worker 등록
마이그레이션drizzle/0NNN_sequence_final_reminder.sqlbun db:generate

3F2 — OOO 자동응답 → 영업시간 재스케줄

종류파일라인작업
수정services/reply-automation.service.ts79–150intent='out_of_office' 시 reply 본문 + lead.timezone 을 resume 함수에 전달
수정services/reply-automation-resume.service.ts26–48scheduleEnrollmentResume() 시그니처 확장: (enrollmentId, sequenceId, { oooBody?, leadTimezone?, fallbackDelayDays }) → 본문 파싱하여 dynamic delay
신규services/ooo-parse.service.tsGemini Flash 로 본문에서 returnAt 추출 (regex 1차 — "return on YYYY-MM-DD" / "until …" — LLM 2차)
수정services/reply-automation-resume.service.ts53–114executeEnrollmentResume() 직후 pending step execution 의 scheduledAtrecipient-send-time-recommendation.recommend() 의 optimal hour 로 patch
수정db/schema/sequences.ts193–234sequence_enrollmentsoooParsedReturnAt, oooReplyEmailId 컬럼 추가
수정db/schema/sequences.ts237–289sequenceStepExecutionsrescheduleReason: enum('ooo_resume','recipient_optimal_time','manual') 추가
수정lib/queue/sequence-email-scheduler.tsapplySchedulingDelay()optional timezone-aware sendAt 보정 path 추가
마이그레이션drizzle/0NNN_ooo_reschedule.sqlbun db:generate

기존 fixed delay path 는 그대로 유지 — OOO 파싱 실패 시 폴백.

4F3 — "담당자 아님" 응답 → 신규 수신자 forward

종류파일라인작업
수정workers/bullmq/reply-tags-auto-classify.worker.ts92–177Gemini 분류 enum 에 wrong_recipient + 추출된 redirectEmail 필드 추가
수정services/reply-automation.service.ts105resolveIntentAction()wrong_recipient → 'forward_pending_approval' 매핑
수정services/webhook.service.ts2077–2090confidence ≥0.7 시 wrong-person-forward 큐에 enqueue (status=pending_approval)
수정db/schema/sequences.ts91–154replyAutomationConfigwrong_recipient: 'forward_manual'|'forward_auto'|'ignore' 추가 (default forward_manual)
신규db/schema/enrollment-redirects.ts테이블: id, workspaceId, fromEnrollmentId, fromLeadId, originalEmailId, replyEmailId, extractedEmail, toLeadId?, status, approvedBy, approvedAt, sentAt, createdAt
신규utils/email-extraction.util.tsextractEmailFromBody(text) — regex 1차 + LLM(Gemini) fallback
신규services/enrollment-redirect.service.tscreatePendingRedirect(), approveAndForward(redirectId, approverId), findOrCreateLeadByEmail()
신규workers/bullmq/wrong-person-forward.worker.tsapprove 후 진입 → findOrCreateLeadByEmail() → 원본 본문 + 새 lead 로 sendEmailWithAccount() 호출
재사용services/email-send.service.ts322–757변경 없음, metadata.forwardOf: replyEmailId 만 추가 권장
신규routes/inbox/redirects.routes.tsGET /redirects?status=pending_approval + POST /redirects/:id/approve|/reject (workspaceAuth)
신규admin/src/features/inbox/RedirectApprovalCard.tsx받은편지함 카드 + 1-click 승인
수정lib/queue/types.ts · queues.ts · workers/bullmq/index.tsWRONG_PERSON_FORWARD 큐 + worker 등록
마이그레이션drizzle/0NNN_enrollment_redirects.sqlbun db:generate

자동 단계까지 가지 말 것. workspace flag 로 점진 오픈.

5건드릴 필요 없는 부분 (재사용만)

시스템위치비고
Webhook 진입routes/webhook.routes.ts:24, webhook.service.ts:60수정 X — 기존 dedup·threading 그대로
Auto-reply 감지utils/auto-reply-detection.ts:43수정 X — F2 가 결과 그대로 사용
발송 entryservices/email.service.ts:543 + email-send.service.ts:322수정 X — F1/F3 가 그대로 호출
Billingusage.service.ts:98 recordUsage('email_send')수정 X — F1/F3 가 그대로 호출
수신자 timezonedb/schema/leads.ts:104,108수정 X — F2 가 그대로 활용
Optimal send timerecipient-send-time-recommendation.service.ts:68수정 X — F2 가 호출만 추가

6PR 분할 + Layered DAG

Layer 0 (schema/migration)              Layer 1 (병렬 구현)                       Layer 2 (UI/E2E)            Layer 3
┌─────────────────────────────┐         ┌────────────────────────────────┐        ┌──────────────────────┐   ┌─────────────┐
│N0.1* S 📦 schema-migrations════════N1.1* M ⚙ F2 reply-auto-resume═══════N2.1  S 🧪 e2e/playwright══N3.1* S 🚀  │
│  - 3개 컬럼/테이블 한꺼번에 │   ─────>│   OOO parse + dynamic delay    │        │   3 시나리오 추가    │   │  alpha 배포 │
│  - bun check:migrations PASS│   │ ───>│N1.2  M ⚙ F1 final-reminder     │───────>│                      │   │             │
│  - Slack 공지 후 머지       │   │ │   │   worker + lifecycle hook      │        └──────────────────────┘   └─────────────┘
└─────────────────────────────┘   │ │   └────────────────────────────────┘
                                  │ │   ┌────────────────────────────────┐        ┌──────────────────────┐
                                  │ └──>│N1.3  M ⚙ F3 BE worker+service═══════N2.2* M 🎨 F3 inbox FE════════════════>
                                  │     │   intent / extract / queue     │        │   ApprovalCard wire  │
                                  │     └────────────────────────────────┘        └──────────────────────┘
                                  └─────> (전 layer 가 schema 의존)

Status icons: done ▶run ⏸wait ✗fail   CP edges:    Non-CP: ─   * = Critical Path
Critical Path: N0.1 → N1.3 → N2.2 → N3.1 (F3 가 가장 길음 — FE 포함)

왜 schema 를 한 PR로 묶나check:migrations (F1–F8) + Slack 공지 + chain 무결성을 1번만 처리. 분리하면 3번 동기화 필요.

7결정 필요사항

① F1 default reminder delay

5일? 시퀀스별 설정 노출?

② F1 reminder 최대 횟수

1회만? (2–3회는 spam 위험)

③ F2 OOO 본문 파싱

Gemini Flash 1회 호출 OK? 비용 ≈ $0.0001/회

④ F3 자동화 수위

1차 forward_manual 만 출시 후 점진 자동화 동의?

⑤ F3 enrollment 복제

새 lead 를 같은 시퀀스 step 0 부터? 아니면 단발 forward 1통만?