enum SenderMode { human, twin } // Product autonomy ladder names (PRD) — keep L0/L1/L2 as identifiers. // ignore: constant_identifier_names enum AutonomyLevel { L0, L1, L2 } /// 관계별 페르소나 (roadmap.md §2.7-B, PRD.md §2.1-②/§3.1) — minimum 2 tiers. enum RelationshipTier { close, formal; static RelationshipTier fromJson(String? raw) => RelationshipTier.values.firstWhere( (e) => e.name == raw, orElse: () => RelationshipTier.formal, ); String get label => this == RelationshipTier.close ? '가까운 사이' : '공식적인 사이'; } class User { User({required this.id, required this.displayName, required this.inviteCode}); final int id; final String displayName; final String inviteCode; factory User.fromJson(Map json) => User( id: json['id'] as int, displayName: json['display_name'] as String? ?? json['displayName'] as String? ?? '', inviteCode: json['invite_code'] as String? ?? json['inviteCode'] as String? ?? '', ); } class TwinSettings { TwinSettings({required this.autonomyLevel, required this.relationshipTier}); final AutonomyLevel autonomyLevel; final RelationshipTier relationshipTier; factory TwinSettings.fromJson(Map json) { final raw = (json['autonomy_level'] as String? ?? 'L0').toUpperCase(); return TwinSettings( autonomyLevel: AutonomyLevel.values.firstWhere( (e) => e.name == raw, orElse: () => AutonomyLevel.L0, ), relationshipTier: RelationshipTier.fromJson(json['relationship_tier'] as String?), ); } } class ConversationSummary { ConversationSummary({ required this.id, required this.isGroup, required this.userIds, required this.twinDisabledByPeer, this.twinDisabledByFlood = false, this.unreadCount = 0, this.createdAt, }); final int id; final bool isGroup; final List userIds; final bool twinDisabledByPeer; // roadmap.md §2.7-C 스팸/도배 감지: 자동으로 켜지지만(사람의 선택이 아님) // twinDisabledByPeer와 달리 되돌릴 수 있음 (POST /conversations/:id/flood-reset). final bool twinDisabledByFlood; final int unreadCount; final DateTime? createdAt; factory ConversationSummary.fromJson(Map json) { final rawIds = json['user_ids'] as List? ?? const []; return ConversationSummary( id: json['id'] as int, isGroup: json['is_group'] as bool? ?? false, userIds: rawIds.map((e) => e as int).toList(), twinDisabledByPeer: json['twin_disabled_by_peer'] as bool? ?? false, twinDisabledByFlood: json['twin_disabled_by_flood'] as bool? ?? false, unreadCount: json['unread_count'] as int? ?? 0, createdAt: DateTime.tryParse(json['created_at'] as String? ?? ''), ); } /// Title helper when peer display names are not yet loaded. String titleFor(int myUserId) { final peers = userIds.where((id) => id != myUserId).toList(); if (isGroup) return '그룹 #$id'; if (peers.isEmpty) return '나와의 대화 #$id'; return '대화 · 상대 #${peers.first}'; } } class Contact { Contact({ required this.id, required this.displayName, this.contactUserId, this.relationshipNote = '', this.relationshipTier, }); final int id; final String displayName; final int? contactUserId; final String relationshipNote; /// Per-contact override of the global relationship tier (roadmap.md /// §2.7-B). Null means "use the global default". final RelationshipTier? relationshipTier; factory Contact.fromJson(Map json) => Contact( id: json['id'] as int, displayName: json['display_name'] as String? ?? '', contactUserId: json['contact_user_id'] as int?, relationshipNote: json['relationship_note'] as String? ?? '', relationshipTier: json['relationship_tier'] == null ? null : RelationshipTier.fromJson(json['relationship_tier'] as String?), ); } class EscalationLogEntry { EscalationLogEntry({ required this.id, required this.conversationId, required this.reason, required this.messageSnippet, required this.resolved, required this.createdAt, }); final int id; final int conversationId; final String reason; final String messageSnippet; final bool resolved; final DateTime createdAt; factory EscalationLogEntry.fromJson(Map json) => EscalationLogEntry( id: json['id'] as int, conversationId: json['conversation_id'] as int? ?? 0, reason: json['reason'] as String? ?? '', messageSnippet: json['message_snippet'] as String? ?? '', resolved: json['resolved'] as bool? ?? false, createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? DateTime.now(), ); } class ChatMessage { ChatMessage({ required this.id, required this.conversationId, required this.senderId, required this.senderMode, required this.text, required this.retracted, required this.createdAt, }); final int id; final int conversationId; final int senderId; final SenderMode senderMode; final String text; final bool retracted; final DateTime createdAt; bool get isTwin => senderMode == SenderMode.twin; factory ChatMessage.fromJson(Map json) { final mode = (json['sender_mode'] as String? ?? 'human').toLowerCase(); return ChatMessage( id: json['id'] as int, conversationId: json['conversation_id'] as int? ?? json['conversationId'] as int? ?? 0, senderId: json['sender_id'] as int? ?? json['senderId'] as int? ?? 0, senderMode: mode == 'twin' ? SenderMode.twin : SenderMode.human, text: json['text'] as String? ?? '', retracted: json['retracted'] as bool? ?? false, createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? DateTime.now(), ); } } class WhitelistRule { WhitelistRule({required this.id, required this.topicKeyword, this.contactId}); final int id; final String topicKeyword; final int? contactId; factory WhitelistRule.fromJson(Map json) => WhitelistRule( id: json['id'] as int, topicKeyword: json['topic_keyword'] as String? ?? '', contactId: json['contact_id'] as int?, ); } /// 단톡 따라잡기(roadmap.md §2.7-A) — GET /conversations/:id/summary result. class GroupSummaryResult { GroupSummaryResult({ required this.status, required this.summary, required this.unreadCount, required this.needsReply, }); final String status; // empty | ok final String summary; final int unreadCount; final bool needsReply; factory GroupSummaryResult.fromJson(Map json) => GroupSummaryResult( status: json['status'] as String? ?? 'empty', summary: json['summary'] as String? ?? '', unreadCount: json['unread_count'] as int? ?? 0, needsReply: json['needs_reply'] as bool? ?? false, ); bool get isEmpty => status == 'empty' || unreadCount == 0; } class DraftResult { DraftResult({required this.status, required this.text}); final String status; // ok | escalate | no_key final String text; factory DraftResult.fromJson(Map json) => DraftResult( status: json['status'] as String? ?? 'ok', text: json['text'] as String? ?? '', ); bool get isEscalate => status == 'escalate'; }