★★★★★
#앱별 실전 패턴 · claude-code
[we_are_tonged] 채팅 시스템 (Firestore subcollection)
통했어요 앱: 매칭된 사용자끼리 1:1 채팅.
#we-are-tonged#messaging#chat#firestore
통했어요 앱: 매칭된 사용자끼리 1:1 채팅.
Firestore 스키마:
```
chats/{chatId}
- user1Id
- user2Id
- lastMessageAt
- lastMessage
chats/{chatId}/messages/{messageId}
- senderId
- text
- createdAt
- isRead
```
구현:
```dart
class ChatService {
static Future<String> getOrCreateChat(String userId1, String userId2) async {
// 기존 채팅방 확인
final snap = await db.collection('chats')
.where('user1Id', isEqualTo: userId1)
.where('user2Id', isEqualTo: userId2)
.get();
if (snap.docs.isNotEmpty) {
return snap.docs.first.id;
}
// 새 채팅방 생성
final chatDoc = await db.collection('chats').add({
'user1Id': userId1,
'user2Id': userId2,
'createdAt': DateTime.now(),
'lastMessageAt': DateTime.now(),
});
return chatDoc.id;
}
static Future<void> sendMessage({
required String chatId,
required String senderId,
required String text,
}) async {
// 메시지 추가
await db
.collection('chats')
.doc(chatId)
.collection('messages')
.add({
'senderId': senderId,
'text': text,
'createdAt': DateTime.now(),
'isRead': false,
});
// 채팅방 마지막 메시지 업데이트
await db.collection('chats').doc(chatId).update({
'lastMessageAt': DateTime.now(),
'lastMessage': text,
});
}
static Stream<List<ChatMessage>> getMessages(String chatId) {
return db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('createdAt', descending: true)
.snapshots()
.map((snap) => snap.docs
.map((doc) => ChatMessage.fromDoc(doc))
.toList());
}
}
```
UI:
```dart
class ChatScreen extends StatefulWidget {
final String chatId;
const ChatScreen({required this.chatId});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: StreamBuilder<List<ChatMessage>>(
stream: ChatService.getMessages(widget.chatId),
builder: (context, snapshot) {
final messages = snapshot.data ?? [];
return ListView.builder(
reverse: true,
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return ChatBubble(message: msg);
},
);
},
),
),
Padding(
padding: EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: '메시지'),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: () {
ChatService.sendMessage(
chatId: widget.chatId,
senderId: currentUserId,
text: _controller.text,
);
_controller.clear();
},
),
],
),
),
],
);
}
}
```
구현해줄까?
쓰는 법 — 위 칸에 본인 값을 채우면 아래 프롬프트에 바로 반영돼요. 복사 버튼을 누르면 채워진 그대로 클립보드에 담기니까, claude-code(또는 본인이 쓰는 AI)에 붙여넣기만 하면 됩니다. 변수가 없으면 그냥 복사해서 쓰세요.

