ADR 0025 — 의뢰인 포털 메시지 알림
- Status: Implemented (PR-1 머지, 2026-04-26 / PR-2~ Phase 3 후속 별도 ADR 예정)
- Date: 2026-04-26
- Decision Drivers: Pack 3 (부동산) + Phase 2 V2 검색 품질 완료 후 사용자 명시 우선순위 — "의뢰인 포털 메시징·알림". 메시징 자체는 portal-redesign Phase 2 (#94) 에서 완성됐으나 알림 인프라 부재 — 변호사가 의뢰인 메시지를 놓치는 사고 위험.
- Anti-Goal Compliance: 4개 모두 미위반. 변호사 ↔ 의뢰인 1:1 사건 단위 메시징의 알림 보강이며 B2C 매칭·광고와 무관.
결정
tenants/{tid}/cases/{cid}/messages/{mid} Firestore onCreate trigger 로
의뢰인 메시지 도착 시 변호사 (tenant owner) 에게 자동 알림:
- 이메일 (Resend) — 사건 상세 링크 + 메시지 미리보기
- 인앱 알림 (
tenants/{tid}/notifications) — 변호사 측 NotificationBell 통합
변호사 → 의뢰인 외부 알림 (SMS/카카오톡) 은 SOLAPI 인프라 가입 필요로 별도 ADR.
배경
- portal-redesign.md Phase 2 (구현 완료 2026-04-09) 가 메시징 컬렉션·UI 모두 완성:
- Firestore:
tenants/{tid}/cases/{cid}/messages/{mid} - 필드:
from("client" | "staff"),text,isRead,senderName,createdAt - UI: PortalMessagesTab (의뢰인) + CaseMessagesTab (변호사)
- Firestore:
- 이미 unread badge 도 사건 상세 탭에 있음 (CaseDetailClient.unreadMsgCount)
- 결정적 갭: 변호사가 대시보드를 방문하지 않으면 의뢰인 메시지 도착을 모름. 의뢰인은 답을 기다리고 있으나 변호사는 인지 못 함 → 신뢰 손상.
대안 검토
| 대안 | 평가 |
|---|---|
| (1) Cloud Scheduler 매시간 polling | latency 60분 — 의뢰인 체감 너무 느림 |
| (2) Firestore onCreate trigger (선택) | 즉시 (수초) · 비용 무시할 수준 · 기존 reminders.ts 패턴 재사용 |
| (3) 클라이언트 측 onSnapshot 유지 | 변호사 브라우저 닫혀있으면 무의미 |
| (4) FCM push notification | 모바일 앱 부재 (현재 PWA 만) — 우선순위 낮음 |
알림 채널 우선순위 (Phase 3 시리즈)
| 채널 | 대상 | 인프라 | 상태 |
|---|---|---|---|
| 이메일 (Resend) | 변호사 | 기존 email.ts 재사용 | Phase 3 PR-1 (이번) |
| 인앱 (notifications) | 변호사 | 기존 NotificationBell 통합 | Phase 3 PR-1 (이번) |
| SMS (SOLAPI) | 의뢰인 | 신규 인프라 가입 | 별도 ADR 예정 |
| 카카오톡 알림톡 | 의뢰인 | SOLAPI + 템플릿 승인 | 별도 ADR 예정 |
| FCM push | 변호사 모바일 | PWA + service worker | 모바일 앱 진입 시 |
트리거 동작
trigger: tenants/{tenantId}/cases/{caseId}/messages/{messageId} onCreate
if (from !== "client") return; // 변호사 본인 발신 skip
if (!text.trim()) return; // 빈 본문 skip
const [caseSnap, tenantSnap] = await Promise.all([read case, read tenant]);
const ownerUid = tenant.ownerUid;
const ownerEmail = members/{ownerUid}.email;
// 1. 인앱 알림 (이메일과 무관하게 항상)
notifications.add({
type: "client_message_received",
caseId, caseNumber, clientName, preview, messageId,
recipientUid: ownerUid, isRead: false, createdAt: serverTimestamp,
});
// 2. 이메일 (이메일 주소 있을 때만)
if (ownerEmail) {
Resend.send({
to: ownerEmail,
subject: "[새 메시지] {clientName} — 사건 {caseNumber}",
html: 인라인 스타일 (HTML/URL escape),
});
}
실패 정책: 알림 실패가 메시지 자체 손실을 야기해선 안 됨. 모든 단계 try/catch + silent log.
보안
- 의뢰인 입력 (
clientName,messageText,senderName) 모두 HTML escape (5종 문자) - URL 의 큰따옴표 percent-encode (href 속성 깨짐 방어)
- PII 보호: 메시지 미리보기 160자 cap (전문 노출 X — 이메일 본문에서)
Anti-Goal 검증
- ✅ 1번 "민사 외 형사·행정·IP" 미위반 — 메시징은 사건 도메인 무관.
- ✅ 2번 "법률 조언 AI" 미위반 — 단순 알림.
- ✅ 3번 "의뢰인 B2C 매칭" 미위반 — 이미 1:1 사건 단위.
- ✅ 4번 "변호사 매칭·광고" 미위반.
한계
- 이메일 도달률: Resend 무료 티어 + RESEND_API_KEY 미설정 시 silent skip. 운영 환경 전환 시 환경변수 점검 필수.
- 인앱 알림 mark-as-read: 사용자가 NotificationBell 클릭 시 isRead 갱신 (별도 PR — 기존 NotificationBell 패턴 재사용).
- 의뢰인 측 외부 알림 미구현: 변호사 답신을 의뢰인이 즉시 인지하려면 SMS/카카오톡 필요 → SOLAPI 가입 후 별도 ADR.
- NotificationType union 미동기화: functions 패키지가 web 패키지 type 을 import 못 함. 단순 string
"client_message_received"로 두고 web 측이 알아서 라우팅. 타입 drift 위험 있으나 현실적 절충.
Minority Report
- P5 (개발자) 우려: trigger 가 모든 메시지 write 마다 발화 — 의뢰인이 다량 메시지 보내면 비용. 현실적으로 1 의뢰인 ↔ 1 변호사 채널이라 분당 수십 건은 어려움. 사후 모니터링 필요.
- P4 (디자이너) 우려: 이메일 디자인이 너무 단순. brand 일관성 보강 후속 PR — 현재는 기능 우선.
- P3 (PM) 우려: 의뢰인이 답변 못 받으면 변호사 신뢰 손상. 변호사→의뢰인 SMS 가 더 시급할 수도. 사용자 명시: 둘 다 하지만 변호사 측 인프라 (Resend) 가 이미 있으므로 이걸 먼저.
- P1 (변호사) 양보 불가: 의뢰인 메시지 누락은 사고. 알림 trigger 는 무조건 신뢰성 (Cloud Functions onCreate) 으로 가야 함 — 충족.
후속 과제 (Phase 3 시리즈)
- ADR 0026: SOLAPI SMS/카카오톡 알림톡 (의뢰인 측 외부 알림)
- PR-3: 변호사 측 NotificationBell 에 client_message_received 카드 노출 (mark-as-read)
- PR-4: 인앱 알림 deeplink → 사건 메시지 탭 자동 활성
- ADR 0027: PWA + FCM (변호사 모바일 push)
- 운영 매뉴얼: 알림 실패 모니터링 (
apps/docs/content/operations/portal-message-notification-runbook.md)