본문으로 건너뛰기

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최초 작성