★★★★★
#앱별 실전 패턴 · claude-code
[buildit] 별점·리뷰 시스템 (시공 완료 후)
빌드잇: 시공 완료 후 상호 평가 및 리뷰.
#buildit#rating#review
빌드잇: 시공 완료 후 상호 평가 및 리뷰.
Firestore 스키마:
```
reviews/{reviewId}
- fromUserId
- toUserId
- projectId
- rating: 1~5
- text
- photos: ["url1", "url2", ...]
- createdAt
- helpful: 0
```
구현:
```dart
class ReviewService {
static Future<void> submitReview({
required String fromUserId,
required String toUserId,
required String projectId,
required double rating,
required String reviewText,
required List<String> photoUrls,
}) async {
final reviewDoc = await db.collection('reviews').add({
'fromUserId': fromUserId,
'toUserId': toUserId,
'projectId': projectId,
'rating': rating,
'text': reviewText,
'photos': photoUrls,
'createdAt': DateTime.now(),
'helpful': 0,
});
// 사용자의 평점 업데이트
await _updateUserRating(toUserId);
}
static Future<void> _updateUserRating(String userId) async {
final snap = await db.collection('reviews')
.where('toUserId', isEqualTo: userId)
.get();
if (snap.docs.isEmpty) return;
final avgRating = snap.docs
.map((doc) => doc['rating'] as double)
.reduce((a, b) => a + b) / snap.docs.length;
await db.collection('users').doc(userId).update({
'rating': avgRating,
'reviewCount': snap.size,
});
}
static Stream<List<Review>> getUserReviews(String userId) {
return db.collection('reviews')
.where('toUserId', isEqualTo: userId)
.orderBy('createdAt', descending: true)
.snapshots()
.map((snap) => snap.docs.map((doc) => Review.fromDoc(doc)).toList());
}
}
```
UI (리뷰 제출):
```dart
class ReviewSubmitScreen extends StatefulWidget {
final String toUserId;
final String projectId;
@override
State<ReviewSubmitScreen> createState() => _ReviewSubmitScreenState();
}
class _ReviewSubmitScreenState extends State<ReviewSubmitScreen> {
double _rating = 5;
late TextEditingController _reviewController;
List<String> _photoUrls = [];
@override
void initState() {
super.initState();
_reviewController = TextEditingController();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('평점'),
RatingBar.builder(
initialRating: _rating,
minRating: 1,
direction: Axis.horizontal,
allowHalfRating: false,
itemCount: 5,
onRatingUpdate: (value) => setState(() => _rating = value),
itemBuilder: (context, index) => Icon(Icons.star, color: Colors.amber),
),
TextField(
controller: _reviewController,
decoration: InputDecoration(labelText: '리뷰'),
maxLines: 5,
),
ElevatedButton(
onPressed: () async {
await ReviewService.submitReview(
fromUserId: currentUserId,
toUserId: widget.toUserId,
projectId: widget.projectId,
rating: _rating,
reviewText: _reviewController.text,
photoUrls: _photoUrls,
);
Navigator.pop(context);
},
child: Text('리뷰 제출'),
),
],
);
}
}
```
구현해줄까?
쓰는 법 — 위 칸에 본인 값을 채우면 아래 프롬프트에 바로 반영돼요. 복사 버튼을 누르면 채워진 그대로 클립보드에 담기니까, claude-code(또는 본인이 쓰는 AI)에 붙여넣기만 하면 됩니다. 변수가 없으면 그냥 복사해서 쓰세요.

