본문으로 건너뛰기

의뢰인 포털 재설계 기획서

구현 상태: ✅ 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.tsUserRole에서 "client" 제거
types/tenant.tsMemberRole = UserRole (자동 반영)
lib/firebase/auth.tsVALID_ROLES에서 "client" 제거
(admin)/layout.tsxrole === "client" redirect 제거
(portal)/layout.tsxFirebase Auth → 포털 세션 쿠키 체크로 전환
requireStaffSession()client 체크 불필요 (간소화)
proxy.ts/portal-invite 제거, /portal/view 추가
firestore.rulesisClientOfCase(), isClientOfParentCase() 제거
firestore.rulesmessages client 규칙 제거 (서버 사이드 전용)
components/layout/PLAN_LABEL에서 client 관련 제거
Storybookclient 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. 카카오 비즈니스 채널 개설 — 사업자등록증 필요, 심사 1~3 영업일
  2. 채널 설정 — 검색 허용 ON, 홈 공개 ON, 고객센터 정보 입력
  3. SOLAPI에 채널 연동 — PFID 등록
  4. 템플릿 등록 + 심사 — 카카오 검수 1~3 영업일. 정보성 메시지만 허용
  5. 발송 — 승인된 템플릿 기반

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.tsportal-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: 기반 (필수)

  1. UserRole에서 "client" 제거 + 전체 파급 수정
  2. portalTokens 데이터 모델 + Server Actions
  3. 포털 세션 관리 (iron-session)
  4. OTP 인증 플로우 (이메일 먼저, SMS/알림톡은 Phase 2)
  5. /portal/view 신규 페이지 (토큰 → OTP → 열람)
  6. 의뢰인 접속 액티비티 로그

Phase 2: 대화 + 알림

  1. cases/{caseId}/messages 서브컬렉션 + 마이그레이션
  2. 포털 대화 UI (의뢰인 측)
  3. admin 대화 탭 (사건 상세)
  4. SOLAPI 연동 (SMS + 알림톡)
  5. 카카오 비즈니스 채널 + 템플릿 심사

Phase 3: 정리

  1. 기존 portalInvites, messages 마이그레이션/삭제
  2. Firestore rules 정리
  3. E2E 테스트 재작성
  4. Storybook 업데이트