231 lines
7.0 KiB
Dart
231 lines
7.0 KiB
Dart
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<String, dynamic> 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<String, dynamic> 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.unreadCount = 0,
|
|
this.createdAt,
|
|
});
|
|
|
|
final int id;
|
|
final bool isGroup;
|
|
final List<int> userIds;
|
|
final bool twinDisabledByPeer;
|
|
final int unreadCount;
|
|
final DateTime? createdAt;
|
|
|
|
factory ConversationSummary.fromJson(Map<String, dynamic> json) {
|
|
final rawIds = json['user_ids'] as List<dynamic>? ?? 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,
|
|
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) => DraftResult(
|
|
status: json['status'] as String? ?? 'ok',
|
|
text: json['text'] as String? ?? '',
|
|
);
|
|
|
|
bool get isEscalate => status == 'escalate';
|
|
}
|