본문으로 건너뛰기

Agent Board 도메인 모델

import { ZonedDateTime } from '@shared/utils';

데이터 저장 규칙

Agent Board는 Message Delivery & Tracking Layer로서 다음의 데이터 저장 규칙을 따릅니다:

스키마 분리 원칙

  • 개인정보 관련 데이터는 private 스키마에 저장되어야 합니다.
  • 비개인정보 데이터는 agent_board 스키마에 저장되어야 합니다.
  • 개인정보 포함 여부에 따라 위 규칙을 우선적으로 적용합니다.

Agent Board 도메인의 개인정보 분류

  • 개인정보 (private 스키마):
    • BoardMessage: userId와 연결된 메시지 데이터
    • BoardInteraction: userId와 연결된 상호작용 이력
    • BoardInteractionSummary: userId별 일일 통계 집계
  • 비개인정보 (agent_board 스키마):
    • MessageStatus, InteractionType: 도메인 열거형 타입
    • 향후 메시지 템플릿, 레이아웃 정의 등이 추가될 경우 agent_board 스키마에 저장

데이터 보관 정책

메시지 저장

  • 영구 저장: Agent Treatment Flow로부터 받은 모든 메시지는 PostgreSQL의 private 스키마에 저장됩니다.
  • 이유: Mobile 사용자는 언제든 비동기적으로 접속하므로, 메시지는 항상 전달 가능한 상태여야 합니다.
  • 보관 기간: 메시지별 TTL 또는 기본 30일 보관 후 아카이빙

상호작용 이력

  • 영구 저장: 모든 사용자 상호작용(view, click, dismiss 등)은 PostgreSQL의 private 스키마에 저장됩니다.
  • 이유: Agent Treatment Flow가 사용자의 과제 수행도를 분석하여 더 나은 치료 과제를 생성하기 위해 필요합니다.
  • 집계 데이터: 일별/주별 상호작용 통계를 미리 계산하여 저장

임시 상태 (Redis)

  • 연결 상태: 활성 SSE 연결 정보
  • 전송 대기 큐: 일시적인 네트워크 장애 시 재전송을 위한 버퍼
  • Rate Limiting 카운터: API 호출 제한용

1. 엔티티 관계도 (ERD)

1.1 메시지 전달 흐름 (Sequence Diagram)

1.2 메시지 생명주기 플로우 (Flowchart)

1.3 실시간 연결 관리 플로우 (State Diagram)

1.4 데이터 흐름 개요 (Data Flow Diagram)

이러한 플로우 다이어그램들은 Agent Board의 다음 핵심 기능을 시각화합니다:

  1. 메시지 전달 흐름: Treatment Flow로부터 메시지를 받아 사용자에게 전달하는 전체 과정
  2. 메시지 생명주기: 메시지가 생성되어 아카이빙되기까지의 상태 변화
  3. 실시간 연결 관리: SSE 연결의 상태 전이와 재연결 메커니즘
  4. 데이터 흐름: 메시지와 상호작용 데이터가 시스템을 통해 흐르는 경로

2. 엔티티

2.1 BoardMessage

/**
* Agent Treatment Flow로부터 완성된 Board 데이터를 받아 저장하고 전달할 메시지.
* Backend는 이미 렌더링된 완성된 Board를 저장하고 적절한 사용자에게 라우팅합니다.
* 템플릿 렌더링이나 메시지 생성은 Agent Board의 책임이 아닙니다.
*/
interface BoardMessage {
id: string; // Board 고유 식별자
userId: string; // 사용자 ID
userCycleId: string; // UserCycle ID (Agent Treatment Flow에서 전달)
boardType: AgentBoardType; // Board 타입
priority: AgentBoardPriority; // 우선순위
status: MessageStatus; // Board 상태
title: string; // 제목 (이미 렌더링된 텍스트)
content: string; // 내용 (이미 렌더링된 텍스트)
actions: ActionButton[]; // 액션 버튼 정의
displayStartTime: number; // 표시 시작 시간 (timestamp)
displayEndTime: number; // 표시 종료 시간 (timestamp)
displayOrder: number; // 표시 순서
metadata?: any; // 추가 메타데이터
product: 'dta' | 'dha'; // 제품 구분 (규제/비규제 분리)
sourceDomain: 'atf' | 'mao' | string; // 메시지 생성 도메인
createdAt: number; // 생성 시간 (timestamp)
deliveredAt?: number; // 전달 시간 (timestamp)
}

2.2 MessageQueue

/**
* 사용자별 메시지 큐.
* SSE 연결이 끊어진 동안의 메시지를 임시 보관합니다.
*/
interface MessageQueue {
userId: string; // 사용자 ID
messages: BoardMessage[]; // 대기 중인 메시지들
maxSize: number; // 최대 큐 크기
createdAt: Date; // 생성 시간
updatedAt: Date; // 마지막 업데이트 시간
}

2.3 SSEConnection

/**
* Server-Sent Events 연결 정보.
* 클라이언트와의 실시간 통신 연결을 관리합니다.
*/
interface SSEConnection {
connectionId: string; // 연결 ID
userId: string; // 사용자 ID
state: ConnectionState; // 연결 상태
connectedAt: Date; // 연결 시작 시간
lastHeartbeat: Date; // 마지막 하트비트 시간
clientInfo?: ClientInfo; // 클라이언트 정보
}

2.4 InteractionBatch

/**
* 클라이언트로부터 받은 상호작용 이벤트 배치.
* 클라이언트는 성능을 위해 이벤트를 배치로 전송합니다.
*/
interface InteractionBatch {
batchId: string; // 배치 ID
userId: string; // 사용자 ID
events: InteractionEvent[]; // 이벤트 목록
createdAt: Date; // 수신 시간
sentAt?: Date; // Treatment Flow로 전송한 시간
}

3. 값 객체

3.1 MessageContent

/**
* 메시지 콘텐츠 값 객체.
* 클라이언트가 렌더링할 구조화된 메시지 내용을 포함합니다.
*/
interface MessageContent {
title?: string; // 제목
body?: string; // 본문
sections?: MessageSection[]; // 구조화된 섹션
media?: MediaAttachment[]; // 미디어 콘텐츠
}

interface MessageSection {
type: string; // 섹션 타입
data: any; // 섹션 데이터
}

3.2 DisplayRules

/**
* 클라이언트 표시 규칙 값 객체.
* 클라이언트가 메시지를 어떻게 표시할지 정의합니다.
*/
interface DisplayRules {
priority: number; // 우선순위
requiresAction?: boolean; // 액션 필수 여부
}

3.3 ActionButton

/**
* 액션 버튼 정의 값 객체.
* 클라이언트가 렌더링하고 처리할 액션을 정의합니다.
*/
interface ActionButton {
id: string; // 액션 식별자
label: string; // 버튼 텍스트
action: {
type: 'navigate' | 'callback' | 'external' | 'dismiss';
target?: string;
params?: any;
};
}

3.4 InteractionEvent

/**
* 클라이언트로부터 받은 상호작용 이벤트.
* Backend는 이를 검증하고 Treatment Flow로 전달합니다.
*/
interface InteractionEvent {
boardId: string; // Board 메시지 ID
type: InteractionType; // 상호작용 타입
timestamp: number; // 이벤트 발생 시간 (Unix timestamp in milliseconds)
data?: InteractionData; // 상호작용 데이터
context?: InteractionContext; // 클라이언트 컨텍스트
}

3.5 ConnectionState

/**
* SSE 연결 상태 값 객체.
*/
enum ConnectionState {
CONNECTING = 'connecting', // 연결 중
CONNECTED = 'connected', // 연결됨
DISCONNECTED = 'disconnected', // 연결 끊김
RECONNECTING = 'reconnecting' // 재연결 중
}

3.6 ClientInfo

/**
* 클라이언트 정보 값 객체.
* SSE 연결 시 클라이언트가 제공하는 정보입니다.
*/
interface ClientInfo {
platform: 'ios' | 'android' | 'web'; // 플랫폼
appVersion: string; // 앱 버전
deviceId?: string; // 디바이스 ID
userAgent?: string; // User Agent
}

3.7 MessageMetadata

/**
* 메시지 메타데이터 값 객체.
*/
interface MessageMetadata {
phase?: string; // 치료 단계
messageType?: string; // 메시지 타입
generatedBy: string; // 생성 주체 (Agent Treatment Flow)
generatedAt: Date; // 생성 시간
version: string; // 메시지 버전
[key: string]: any; // 추가 메타데이터
}

3.8 InteractionContext

/**
* 상호작용 컨텍스트 값 객체.
* 클라이언트 환경 정보를 포함합니다.
*/
interface InteractionContext {
platform: string; // 플랫폼
deviceInfo?: any; // 디바이스 정보
screenSize?: { width: number; height: number }; // 화면 크기
orientation?: 'portrait' | 'landscape'; // 화면 방향
appState?: 'foreground' | 'background'; // 앱 상태
timestamp: number; // 컨텍스트 생성 시간 (Unix timestamp in milliseconds)
}

4. 집계 (Aggregates)

BoardMessage 집계

  • 루트: BoardMessage
  • 값 객체: MessageContent, DisplayRules, ActionButton, MessageMetadata
  • 불변식:
    • 메시지는 생성 후 변경될 수 없음
    • userId는 유효한 사용자여야 함
    • userCycleId는 유효한 UserCycle이어야 함
    • displayRules.priority는 0 이상의 정수여야 함
    • actions 필드는 항상 존재해야 함 (빈 배열 가능)
    • 만료된 메시지는 더 이상 전달되지 않음

SSEConnection 집계

  • 루트: SSEConnection
  • 값 객체: ConnectionState, ClientInfo
  • 불변식:
    • 사용자당 활성 연결은 플랫폼별로 하나만 허용
    • 하트비트 타임아웃 시 연결은 자동으로 정리됨
    • 연결 ID는 시스템 내에서 고유해야 함

MessageQueue 집계

  • 루트: MessageQueue
  • 불변식:
    • 큐 크기가 maxSize를 초과하면 오래된 메시지부터 제거
    • 사용자당 하나의 큐만 존재
    • TTL이 만료된 메시지는 자동 제거

InteractionBatch 집계

  • 루트: InteractionBatch
  • 값 객체: InteractionEvent, InteractionData, InteractionContext
  • 불변식:
    • 배치 내 모든 이벤트는 동일한 사용자로부터 와야 함
    • 이벤트 timestamp는 과거여야 함
    • 배치 크기는 설정된 최대값을 초과할 수 없음

5. 도메인 서비스

5.1 MessageDeliveryService

interface MessageDeliveryService {
/**
* Treatment Flow로부터 메시지를 받아 적절한 사용자에게 전달합니다.
*/
deliverMessage(message: BoardMessage): Promise<void>;

/**
* 특정 사용자의 대기 중인 메시지를 조회합니다.
*/
getPendingMessages(userId: string): Promise<BoardMessage[]>;

/**
* 메시지 큐를 정리합니다.
*/
cleanupMessageQueue(userId: string): Promise<void>;
}

5.2 SSEConnectionService

interface SSEConnectionService {
/**
* 새로운 SSE 연결을 등록합니다.
*/
registerConnection(userId: string, connectionId: string, clientInfo: ClientInfo): Promise<void>;

/**
* 연결을 종료합니다.
*/
closeConnection(connectionId: string): Promise<void>;

/**
* 사용자의 활성 연결을 조회합니다.
*/
getActiveConnection(userId: string): Promise<SSEConnection | null>;

/**
* 하트비트를 업데이트합니다.
*/
updateHeartbeat(connectionId: string): Promise<void>;

/**
* 타임아웃된 연결들을 정리합니다.
*/
cleanupStaleConnections(): Promise<void>;
}

5.3 InteractionProcessingService

interface InteractionProcessingService {
/**
* 클라이언트로부터 받은 상호작용 배치를 처리합니다.
*/
processBatch(batch: InteractionBatch): Promise<void>;

/**
* 상호작용 이벤트를 검증합니다.
*/
validateEvent(event: InteractionEvent): boolean;

/**
* Treatment Flow로 이벤트를 전달합니다.
*/
forwardToTreatmentFlow(events: InteractionEvent[]): Promise<void>;
}

5.4 MessageValidationService

interface MessageValidationService {
/**
* BoardMessage의 구조를 검증합니다.
*/
validateMessage(message: BoardMessage): boolean;

/**
* 사용자 권한을 확인합니다.
*/
checkUserAuthorization(userId: string, messageId: string): Promise<boolean>;

/**
* 메시지 크기 제한을 확인합니다.
*/
checkMessageSize(message: BoardMessage): boolean;
}

6. 도메인 이벤트

6.1 메시지 전달 이벤트

interface MessageReceivedFromTreatmentFlowEvent { messageId: string; userId: string; timestamp: Date; }
interface MessageQueuedEvent { messageId: string; userId: string; queueSize: number; timestamp: Date; }
interface MessageDeliveredToClientEvent { messageId: string; userId: string; connectionId: string; timestamp: Date; }
interface MessageDeliveryFailedEvent { messageId: string; userId: string; error: string; timestamp: Date; }
interface MessageExpiredEvent { messageId: string; userId: string; ttl: number; timestamp: Date; }

6.2 연결 관리 이벤트

interface ClientConnectedEvent { connectionId: string; userId: string; platform: string; timestamp: Date; }
interface ClientDisconnectedEvent { connectionId: string; userId: string; reason: string; timestamp: Date; }
interface HeartbeatReceivedEvent { connectionId: string; userId: string; timestamp: Date; }
interface ConnectionTimedOutEvent { connectionId: string; userId: string; lastHeartbeat: Date; timestamp: Date; }

6.3 상호작용 처리 이벤트

interface InteractionBatchReceivedEvent { batchId: string; userId: string; eventCount: number; timestamp: Date; }
interface InteractionValidatedEvent { batchId: string; validCount: number; invalidCount: number; timestamp: Date; }
interface InteractionForwardedEvent { batchId: string; userId: string; timestamp: Date; }
interface InteractionProcessingFailedEvent { batchId: string; error: string; timestamp: Date; }

7. 도메인 규칙

본 문서에서는 모델 구조에 대한 설명에 중점을 두고 있으며, 상세한 비즈니스 규칙은 Agent Board 도메인 비즈니스 규칙 문서를 참조하세요.

주요 규칙 카테고리는 다음과 같습니다:

  1. 메시지 전달 규칙: 라우팅, 큐 관리, TTL 처리
  2. 연결 관리 규칙: SSE 연결 유지, 하트비트, 재연결 정책
  3. 상호작용 처리 규칙: 배치 검증, 중복 제거, 전달 보장
  4. 보안 규칙: 인증, 권한 검증, Rate Limiting
  5. 성능 규칙: 큐 크기 제한, 연결 수 제한, 배치 크기

8. 데이터베이스 스키마 (Prisma)

generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema"]
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["private", "agent_board"]
}

// [Domain: Agent Board]

// 메시지 상태 열거형
enum MessageStatus {
PENDING // 전달 대기
DELIVERED // 전달됨
VIEWED // 조회됨
INTERACTED // 상호작용 발생
EXPIRED // 만료됨
ARCHIVED // 아카이빙됨

@@map("message_status_enum")
@@schema("agent_board")
}

// 상호작용 타입 열거형
enum InteractionType {
DELIVERED // SSE로 전달됨
VIEWED // 화면에 표시됨
CLICKED // 클릭됨
DISMISSED // 닫기/무시됨
ACTION // 특정 액션 실행
EXPIRED // 만료로 자동 제거됨

@@map("interaction_type_enum")
@@schema("agent_board")
}

// Board 메시지 모델 (private 스키마 - 개인정보 포함)
model BoardMessage {
id String @id @default(uuid())
userId String @map("user_id")
userCycleId String @map("user_cycle_id")
type String
content Json
displayRules Json @map("display_rules")
actions Json
metadata Json?
status MessageStatus @default(PENDING)
priority Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
expiresAt DateTime? @map("expires_at")
deliveredAt DateTime? @map("delivered_at")

// 관계
interactions BoardInteraction[]

@@index([userId, status])
@@index([userId, createdAt])
@@index([userCycleId, createdAt])
@@index([expiresAt])
// 성능 최적화: 우선순위 기반 메시지 조회용
@@index([userId, status, priority])
// 성능 최적화: 최신 메시지 조회용
@@index([createdAt])
// 성능 최적화: 상태별 메시지 관리용 (아카이빙, 만료 처리 등)
@@index([status, expiresAt])
@@map("board_messages")
@@schema("private")
}

// Board 상호작용 모델 (private 스키마 - 개인정보 포함)
model BoardInteraction {
id String @id @default(uuid())
messageId String @map("message_id")
userId String @map("user_id")
type InteractionType
timestamp DateTime @default(now())
data Json?
sessionId String? @map("session_id")
platform String?
appVersion String? @map("app_version")

// 관계
message BoardMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)

@@index([userId, timestamp])
@@index([messageId, type])
@@index([sessionId])
// 성능 최적화: 플랫폼별 분석용
@@index([platform, type])
// 성능 최적화: 최신 상호작용 조회용
@@index([timestamp])
@@map("board_interactions")
@@schema("private")
}

// Board 상호작용 요약 모델 (private 스키마 - 개인정보 포함)
model BoardInteractionSummary {
id String @id @default(uuid())
userId String @map("user_id")
date DateTime @db.Date
messagesSent Int @default(0) @map("messages_sent")
messagesViewed Int @default(0) @map("messages_viewed")
messagesClicked Int @default(0) @map("messages_clicked")
messagesDismissed Int @default(0) @map("messages_dismissed")
summaryByType Json @map("summary_by_type")
avgViewLatency Float? @map("avg_view_latency") @db.Real // 전달 ~ 조회까지 평균 시간(초)
avgClickLatency Float? @map("avg_click_latency") @db.Real // 조회 ~ 클릭까지 평균 시간(초)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([userId, date])
@@index([userId, date])
// 성능 최적화: 날짜 범위 조회용
@@index([date])
@@map("board_interaction_summaries")
@@schema("private")
}

데이터 생명주기

메시지 생명주기

  1. PENDING: Treatment Flow로부터 수신, private.board_messages 테이블에 저장
  2. DELIVERED: Mobile Client에 SSE로 전달
  3. VIEWED: 사용자가 실제로 화면에서 확인
  4. INTERACTED: 클릭 등 상호작용 발생
  5. EXPIRED/ARCHIVED: TTL 만료 또는 30일 경과

상호작용 데이터 활용

  • 실시간 분석: Treatment Flow가 API를 통해 조회
  • 일별 집계: 매일 자정에 private.board_interaction_summaries 생성
  • 장기 보관: 90일 이후 집계 데이터만 유지, 원본은 아카이빙

개인정보 보호 고려사항

데이터 접근 제어

  • private 스키마의 모든 테이블은 행 수준 보안(RLS) 적용
  • 사용자는 자신의 userId와 일치하는 데이터만 조회 가능
  • Treatment Flow는 분석을 위해 집계된 데이터만 접근

데이터 암호화

  • userId, sessionId 등 식별자는 해시 처리 고려
  • 민감한 content는 저장 시 암호화 적용
  • 전송 시 TLS 암호화 필수

GDPR 준수

  • 사용자 요청 시 개인 데이터 완전 삭제 기능
  • 데이터 이동성을 위한 내보내기 기능
  • 데이터 처리 목적 명시 및 동의 관리

9. 변경 이력

버전날짜작성자변경 내용
0.1.02025-06-23bok@weltcorp.com최초 작성