TL;DR — 3개 기능 모두 hook 지점이 1~3곳에 집중되어 surgical. F2 는 reply-automation 파이프라인이 이미 완성되어 delay 계산만 동적으로 바꾸면 됨. F1 은 enrollment-progress.service.ts:319 의 isLastStep 분기 1곳. F3 만 새 intent + 새 테이블 + 승인 UI 까지 필요.
| 기능 | Primary hook |
|---|---|
| F1 reminder | services/enrollment-progress.service.ts:319 의 isLastStep && status='completed' 분기 → BullMQ delayed job 등록 |
| F2 OOO 재스케줄 | services/reply-automation-resume.service.ts:26 scheduleEnrollmentResume(delayDays) 호출 직전 — delayDays 를 fixed 7일 대신 OOO 본문 LLM 파싱 결과로 교체. executeEnrollmentResume() 에서 다음 step scheduledAt 을 recipient-send-time-recommendation 결과로 보정 |
| F3 forward | services/reply-automation.service.ts:79 processReplyAutomation() 의 intent 분기에 wrong_recipient 추가 + reply-tags-auto-classify.worker.ts:92 의 분류 enum 확장 |
| 종류 | 파일 | 라인 | 작업 |
|---|---|---|---|
| 수정 | services/enrollment-progress.service.ts | 250–326 (319) | updateEnrollmentProgress() 의 isLastStep 분기에서 finalReminderConfig.enabled 면 delayed job enqueue |
| 수정 | db/schema/sequences.ts | 91–154 | sequences 에 finalReminderConfig: { enabled, delayDays(=5), maxRetries(=1) } jsonb 추가 |
| 수정 | db/schema/sequences.ts | 193–234 | sequence_enrollments 에 lastReminderSentAt, reminderAttemptCount 컬럼 추가 |
| 수정 | services/personalized-email-generation.service.ts | 68+ | mode: 'final_reminder' 분기 — 본문 짧게(1–2문장), 원본 step 본문 reference |
| 수정 | services/email-generation/prompt-builder.ts | 35–97 | reminder mode 일 때 system prompt 분기 ("정중하게 확인 요청, 8줄 이하") |
| 수정 | services/email-generation/step-strategies.ts · step-templates-ko.ts | — | finalReminder 전략 추가 |
| 수정 | lib/queue/types.ts · queues.ts | 8–172 / 280 | SEQUENCE_FINAL_REMINDER 큐 등록 |
| 신규 | workers/bullmq/sequence-final-reminder.worker.ts | — | ① repliedAt IS NULL 재확인 ② recordUsage('email_send') ③ emailService.sendEmail() ④ lastReminderSentAt 갱신 |
| 수정 | workers/bullmq/index.ts | 272 | worker 등록 |
| 마이그레이션 | drizzle/0NNN_sequence_final_reminder.sql | — | bun db:generate |
| 종류 | 파일 | 라인 | 작업 |
|---|---|---|---|
| 수정 | services/reply-automation.service.ts | 79–150 | intent='out_of_office' 시 reply 본문 + lead.timezone 을 resume 함수에 전달 |
| 수정 | services/reply-automation-resume.service.ts | 26–48 | scheduleEnrollmentResume() 시그니처 확장: (enrollmentId, sequenceId, { oooBody?, leadTimezone?, fallbackDelayDays }) → 본문 파싱하여 dynamic delay |
| 신규 | services/ooo-parse.service.ts | — | Gemini Flash 로 본문에서 returnAt 추출 (regex 1차 — "return on YYYY-MM-DD" / "until …" — LLM 2차) |
| 수정 | services/reply-automation-resume.service.ts | 53–114 | executeEnrollmentResume() 직후 pending step execution 의 scheduledAt 을 recipient-send-time-recommendation.recommend() 의 optimal hour 로 patch |
| 수정 | db/schema/sequences.ts | 193–234 | sequence_enrollments 에 oooParsedReturnAt, oooReplyEmailId 컬럼 추가 |
| 수정 | db/schema/sequences.ts | 237–289 | sequenceStepExecutions 에 rescheduleReason: enum('ooo_resume','recipient_optimal_time','manual') 추가 |
| 수정 | lib/queue/sequence-email-scheduler.ts | applySchedulingDelay() | optional timezone-aware sendAt 보정 path 추가 |
| 마이그레이션 | drizzle/0NNN_ooo_reschedule.sql | — | bun db:generate |
기존 fixed delay path 는 그대로 유지 — OOO 파싱 실패 시 폴백.
| 종류 | 파일 | 라인 | 작업 |
|---|---|---|---|
| 수정 | workers/bullmq/reply-tags-auto-classify.worker.ts | 92–177 | Gemini 분류 enum 에 wrong_recipient + 추출된 redirectEmail 필드 추가 |
| 수정 | services/reply-automation.service.ts | 105 | resolveIntentAction() 에 wrong_recipient → 'forward_pending_approval' 매핑 |
| 수정 | services/webhook.service.ts | 2077–2090 | confidence ≥0.7 시 wrong-person-forward 큐에 enqueue (status=pending_approval) |
| 수정 | db/schema/sequences.ts | 91–154 | replyAutomationConfig 에 wrong_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.ts | — | extractEmailFromBody(text) — regex 1차 + LLM(Gemini) fallback |
| 신규 | services/enrollment-redirect.service.ts | — | createPendingRedirect(), approveAndForward(redirectId, approverId), findOrCreateLeadByEmail() |
| 신규 | workers/bullmq/wrong-person-forward.worker.ts | — | approve 후 진입 → findOrCreateLeadByEmail() → 원본 본문 + 새 lead 로 sendEmailWithAccount() 호출 |
| 재사용 | services/email-send.service.ts | 322–757 | 변경 없음, metadata.forwardOf: replyEmailId 만 추가 권장 |
| 신규 | routes/inbox/redirects.routes.ts | — | GET /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.ts | — | WRONG_PERSON_FORWARD 큐 + worker 등록 |
| 마이그레이션 | drizzle/0NNN_enrollment_redirects.sql | — | bun db:generate |
자동 단계까지 가지 말 것. workspace flag 로 점진 오픈.
| 시스템 | 위치 | 비고 |
|---|---|---|
| Webhook 진입 | routes/webhook.routes.ts:24, webhook.service.ts:60 | 수정 X — 기존 dedup·threading 그대로 |
| Auto-reply 감지 | utils/auto-reply-detection.ts:43 | 수정 X — F2 가 결과 그대로 사용 |
| 발송 entry | services/email.service.ts:543 + email-send.service.ts:322 | 수정 X — F1/F3 가 그대로 호출 |
| Billing | usage.service.ts:98 recordUsage('email_send') | 수정 X — F1/F3 가 그대로 호출 |
| 수신자 timezone | db/schema/leads.ts:104,108 | 수정 X — F2 가 그대로 활용 |
| Optimal send time | recipient-send-time-recommendation.service.ts:68 | 수정 X — F2 가 호출만 추가 |
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번 동기화 필요.
5일? 시퀀스별 설정 노출?
1회만? (2–3회는 spam 위험)
Gemini Flash 1회 호출 OK? 비용 ≈ $0.0001/회
1차 forward_manual 만 출시 후 점진 자동화 동의?
새 lead 를 같은 시퀀스 step 0 부터? 아니면 단발 forward 1통만?