의뢰인 포털 재설계 기획서
구현 상태: ✅ Phase 1~3 구현 완료 (2026-04-09) 변경사항: OTP → 카카오/네이버 OAuth 주 인증 + OTP 폴백. SOLAPI 자동 발송 → 변호사가 직접 링크 전달 (복사+카톡 공유).
구현 완료
Phase 1(인증 전환), Phase 2(메시징), Phase 3(레거시 정리)가 모두 구현되었습니다. OAuth 설정은 OAuth 설정 가이드를 참조하세요.
전체 플로우
1. 핵심 원칙
의뢰인 ≠ 서비스 유저
의뢰인 = 하나의 사건에 종속된 외부 열람자
- 의뢰인은 회원가입하지 않는다
- 의뢰인은 Firebase Auth 계정을 갖지 않는다
- 의뢰인은 사건 단위로 링크를 받아 접근한다
- 의뢰인 접근은 플랜(free/pro)과 무관하다
- 한 사건에 여러 의뢰인이 존재할 수 있다 (공동 원고 등)
2. 접근 플로우
[법무사/직원 — admin] [의뢰인 — 외부]
사건 등록 시 의뢰인 정보 입력
(이름, 이메일, 전화번호)
│
├─ 사건 상세 → "포털 공유" 클릭
│
├─ Server Action: createPortalTokenAction
│ ├─ portalTokens/{token} 문서 생성
│ ├─ 기존 토큰이 있으면 자동 revoke
│ └─ 링크 발송 (알림톡 > SMS > 이메일 순)
│
│ /portal/view?token=abc123
│ 링크 클릭
│ │
│ 1. 토큰 검증
│ ├─ 유효 → OTP 화면
│ └─ 만료/무효 → 안내 화면
│ │
│ 2. OTP 발송 (알림톡/SMS/이메일)
│ ├─ 6자리, SHA-256 해싱
│ └─ 5회 초과 시 차단
│ │
│ 3. OTP 일치 → 세션 쿠키 발급
│ ├─ 30일 만료
│ └─ 접속 시마다 자동 갱신
│ │
│ 4. 포털 화면
│ ├─ 사건 정보 (읽기)
│ ├─ 기일 일정 + D-day
│ ├─ 진행 경과 타임라인
│ ├─ 서류 확인/다운로드
│ └─ 대화 (질문/문의)
│
├─ admin 사건 상세 → 대화 탭 (NEW)
│ └─ 의뢰인과의 대화 확인 + 답변
│
└─ 사건 액티비티 로그에 의뢰인 접속 기록
3. 세션 관리
Firebase Auth를 사용하지 않는다. 서버에서 암호화된 세션 쿠키를 직접 관리한다.
세션 쿠키 (iron-session 또는 JWT 암호화)
┌──────────────────────────────┐
│ portal_session │
│ tenantId: string │
│ caseId: string │
│ clientName: string │
│ tokenId: string │ ← 원본 토큰 참조 (revoke 체크)
│ issuedAt: number │
│ expiresAt: number (30일) │
└──────────────────────────────┘
- 매 접속 시 expiresAt을 현재 +30일로 갱신 (sliding window)
- 토큰이 revoke되면 세션도 무효 (tokenId로 검증)
4. 데이터 모델
4.1 portalTokens (신규, portalInvites 대체)
tenants/{tenantId}/portalTokens/{token}
├ tenantId: string
├ caseId: string
├ clientName: string
├ contactType: "email" | "phone"
├ contact: string ← 이메일 또는 전화번호
├ status: "active" | "revoked"
├ otpHash?: string
├ otpExpiresAt?: Timestamp
├ otpAttempts?: number
├ createdAt: Timestamp
└ expiresAt: Timestamp ← 30일
- 같은 사건에 새 토큰 발급 시 기존 토큰 status → "revoked"
- 한 사건에 여러 의뢰인 = 의뢰인별 별도 토큰
4.2 messages (이동: tenant-level → case-level)
변경 전: tenants/{tenantId}/messages (clientPhone 기반)
변경 후: tenants/{tenantId}/cases/{caseId}/messages/{messageId}
├ from: "client" | "staff"
├ senderName: string
├ text: string
├ readAt?: Timestamp ← 상대방 읽은 시간
└ createdAt: Timestamp
4.3 cases (필드 변경)
제거: clientId (Firebase Auth UID — 더 이상 존재하지 않음)
유지: clientName, clientEmail, clientPhone (의뢰인 연락처)
추가: portalTokenId?: string (현재 활성 토큰 참조, 선택적)
4.4 activity logs (추가)
tenants/{tenantId}/cases/{caseId}/logs/{logId}
기존 타입에 추가:
├ type: "client_portal_access" ← 의뢰인 포털 접속
├ type: "client_view_document" ← 서류 열람
├ type: "client_download_document" ← 서류 다운로드
├ type: "client_send_message" ← 대화 전송
├ clientName: string
└ createdAt: Timestamp
5. UserRole 변경
변경 전: "owner" | "staff" | "client"
변경 후: "owner" | "staff"
파급 영향
| 파일 | 변경 |
|---|---|
types/auth.ts | UserRole에서 "client" 제거 |
types/tenant.ts | MemberRole = UserRole (자동 반영) |
lib/firebase/auth.ts | VALID_ROLES에서 "client" 제거 |
(admin)/layout.tsx | role === "client" redirect 제거 |
(portal)/layout.tsx | Firebase Auth → 포털 세션 쿠키 체크로 전환 |
requireStaffSession() | client 체크 불필요 (간소화) |
proxy.ts | /portal-invite 제거, /portal/view 추가 |
firestore.rules | isClientOfCase(), isClientOfParentCase() 제거 |
firestore.rules | messages client 규칙 제거 (서버 사이드 전용) |
components/layout/ | PLAN_LABEL에서 client 관련 제거 |
Storybook | client role 관련 스토리 업데이트 |
E2E | 포털 관련 테스트 전면 재작성 |
6. 알림 발송 모듈 (범용)
6.1 서비스 선정: SOLAPI (구 CoolSMS)
| 기준 | SOLAPI |
|---|---|
| SMS + 알림톡 | 단일 SDK 통합 |
| TypeScript | 네이티브 (solapi npm) |
| Cloud Functions | 호환 |
| 알림톡 실패 → SMS | 자동 fallback |
| 비용 (월 500건) | 약 6,500원 |
대안: 건당 단가 최우선 시 Aligo (REST API 직접 호출)
6.2 카카오 알림톡 설정 절차
- 카카오 비즈니스 채널 개설 — 사업자등록증 필요, 심사 1~3 영업일
- 채널 설정 — 검색 허용 ON, 홈 공개 ON, 고객센터 정보 입력
- SOLAPI에 채널 연동 — PFID 등록
- 템플릿 등록 + 심사 — 카카오 검수 1~3 영업일. 정보성 메시지만 허용
- 발송 — 승인된 템플릿 기반
6.3 범용 알림 모듈 설계
lib/messaging/
types.ts — MessageChannel, SendResult 타입
send.ts — sendMessage(channel, to, content) 통합 함수
templates.ts — 알림톡 템플릿 ID + 변수 매핑
providers/
solapi.ts — SOLAPI SDK 래퍼
email.ts — 기존 lib/email/send.ts 연동
발송 우선순위: 알림톡 → SMS → 이메일 (fallback chain)
6.4 필요한 알림톡 템플릿 (초기)
| 템플릿 | 내용 |
|---|---|
| 포털 링크 발송 | "#{사무소명}에서 사건 진행 현황을 확인하실 수 있습니다. 아래 링크를 눌러주세요." |
| OTP 인증 코드 | "인증 코드: #{OTP}. 10분 이내에 입력해주세요." |
| 기일 알림 (향후) | "#{사건번호} 다음 기일이 #{일수}일 후입니다." |
| 사건 상태 변경 (향후) | "#{사건번호} 사건 상태가 #{상태}로 변경되었습니다." |
7. admin 대화 UI (신규)
사건 상세 페이지에 대화 탭 추가:
cases/{caseId}/messages실시간 조회 (onSnapshot)- 의뢰인별 대화 스레드 표시 (clientName 기준)
- 답변 작성 (from: "staff", senderName: 현재 로그인 유저)
- 읽음 표시 (readAt 업데이트)
8. 삭제 대상
| 항목 | 이유 |
|---|---|
UserRole의 "client" | 의뢰인은 유저가 아님 |
CustomClaims client 관련 | Firebase Auth 불사용 |
portalInvites 컬렉션 | portalTokens로 대체 |
otp-actions.ts | portal-token-actions.ts로 대체 |
portal-invite 페이지 | /portal/view로 대체 |
Firestore rules isClientOfCase() | 서버 사이드 전용 |
Firestore rules isClientOfParentCase() | 서버 사이드 전용 |
| Firestore rules messages client 규칙 | 서버 사이드 전용 |
tenants/{tid}/messages 컬렉션 | cases/{cid}/messages로 이동 |
clientId 필드 (cases) | 토큰 기반 접근, UID 불필요 |
9. 구현 우선순위
Phase 1: 기반 (필수)
UserRole에서"client"제거 + 전체 파급 수정portalTokens데이터 모델 + Server Actions- 포털 세션 관리 (iron-session)
- OTP 인증 플로우 (이메일 먼저, SMS/알림톡은 Phase 2)
/portal/view신규 페이지 (토큰 → OTP → 열람)- 의뢰인 접속 액티비티 로그
Phase 2: 대화 + 알림
cases/{caseId}/messages서브컬렉션 + 마이그레이션- 포털 대화 UI (의뢰인 측)
- admin 대화 탭 (사건 상세)
- SOLAPI 연동 (SMS + 알림톡)
- 카카오 비즈니스 채널 + 템플릿 심사
Phase 3: 정리
- 기존
portalInvites, messages 마이그레이션/삭제 - Firestore rules 정리
- E2E 테스트 재작성
- Storybook 업데이트