Rating 앱 - 별점 평가
동작하는 전체 코드: examples/rating/ —
make test가 함께 돌리고, 릴리스 게이트(CI)가 태그마다 다시 돌리는 예제다.
이 튜토리얼에서는 별점 평가 시스템을 만들며 키보드 접근성과 URL 상태 관리를 학습합니다.
학습 목표#
wire.paramsURL 상태 저장- 키보드 이벤트 (
.key.ArrowLeft,.key.ArrowRight) - 마우스 호버 이벤트
{% cond %}조건부 속성
완성 미리보기#
인터랙티브 별점 입력:
- 클릭으로 별점 선택
- 키보드 화살표로 조절
- 호버 시 미리보기
- URL에 상태 저장
1. 모델 정의#
rating/models.py:
from django.db import models
class Product(models.Model):
"""평가 대상 상품"""
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
@property
def average_rating(self):
"""평균 평점"""
ratings = self.ratings.all()
if not ratings:
return 0
return sum(r.score for r in ratings) / len(ratings)
class Rating(models.Model):
"""사용자 평점"""
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="ratings")
score = models.PositiveSmallIntegerField(choices=[(i, f"{i}점") for i in range(1, 6)])
session_key = models.CharField(max_length=40)
class Meta:
unique_together = ["product", "session_key"]
2. AUTO_BROADCAST 설정#
모델 구독을 위해 settings.py에 설정:
from wireview import AutoBroadcast
WIREVIEW = {
"AUTO_BROADCAST": AutoBroadcast(
model=True, # "rating.rating" 채널 활성화
model_pk=True, # "rating.rating.{pk}" 채널 활성화
senders={("rating", "Rating")}, # 알릴 모델. 비우면 아무것도 알리지 않는다
),
}
3. 컴포넌트 정의#
rating/live.py:
from wireview import Component, ModelAction
from .models import Product, Rating
class XStarRating(Component):
"""인터랙티브 별점 입력"""
class Meta:
template_name = "rating/star_rating.html"
subscriptions = {"rating.rating"} # rating 앱의 Rating 모델
product: Product
current_rating: int = 0 # 현재 평점
hover_rating: int = 0 # 호버 중인 평점
readonly: bool = False
@property
def visitor(self) -> str:
"""익명 평가자 식별자. self.session은 Django 세션의 읽기 전용 뷰다"""
return self.session.session_key or "anonymous"
async def joined(self):
"""마운트 시 기존 평점 로드"""
# URL에서 복원
if rating_param := self.wire.params.get("rating"):
self.current_rating = int(rating_param)
return
# DB에서 로드
existing = await Rating.objects.filter(
product=self.product, session_key=self.visitor
).afirst()
if existing:
self.current_rating = existing.score
async def set_hover(self, star: int):
"""호버 미리보기"""
if self.readonly:
self.skip_render()
return
self.hover_rating = star
async def clear_hover(self):
"""호버 해제"""
self.hover_rating = 0
async def rate(self, score: int):
"""평점 저장"""
if self.readonly or not 1 <= score <= 5:
self.skip_render()
return
await Rating.objects.aupdate_or_create(
product=self.product,
session_key=self.visitor,
defaults={"score": score},
)
self.current_rating = score
self.hover_rating = 0
self.wire.params["rating"] = str(score) # URL에 저장
async def adjust_rating(self, delta: int):
"""키보드로 조절"""
if self.readonly:
self.skip_render()
return
new_rating = max(1, min(5, (self.current_rating or 3) + delta))
await self.rate(new_rating)
세션 키는 뷰에서 만든다. 컴포넌트의
self.session은 Django 세션의 읽기 전용 뷰다. WebSocket에는Set-Cookie를 실을 응답이 없어 세션을 만들거나 쓰는 것은 뷰의 몫이다.def product_detail(request, product_id): if not request.session.session_key: request.session.create() ...자세한 내용은 세션 읽기.
4. 템플릿#
rating/templates/rating/star_rating.html:
{% load wireview %}
<div {% tag_header %}>
<div
class="star-rating {% if readonly %}readonly{% endif %}"
tabindex="{% if readonly %}-1{% else %}0{% endif %}"
role="slider"
aria-valuemin="1"
aria-valuemax="5"
aria-valuenow="{{ current_rating }}"
{% if not readonly %}
{% on 'keydown.key.ArrowRight' 'adjust_rating' delta=1 %}
{% on 'keydown.key.ArrowLeft' 'adjust_rating' delta=-1 %}
{% on 'mouseleave' 'clear_hover' %}
{% endif %}
>
{% for i in "12345" %}
{% with star_num=forloop.counter %}
<span
class="star {% if star_num <= hover_rating %}preview{% elif star_num <= current_rating %}filled{% endif %}"
{% if not readonly %}
{% on 'mouseenter' 'set_hover' star=star_num %}
{% on 'click' 'rate' score=star_num %}
{% endif %}
>★</span>
{% endwith %}
{% endfor %}
</div>
<div class="rating-text">
{% if current_rating %}
Your rating: <strong>{{ current_rating }}</strong> star{{ current_rating|pluralize }}
{% else %}
Click to rate
{% endif %}
</div>
{% if not readonly %}
<div class="keyboard-hint">
Use arrow keys to adjust rating
</div>
{% endif %}
</div>
5. 핵심 개념: 키보드 이벤트#
특정 키 감지#
{% on 'keydown.key.ArrowRight' 'adjust_rating' delta=1 %}
{% on 'keydown.key.ArrowLeft' 'adjust_rating' delta=-1 %}
지원되는 키 이벤트:
keydown.key.Enterkeydown.key.Escapekeydown.key.ArrowUp/Down/Left/Rightkeypress.enter(축약형)
마우스 이벤트#
{% on 'mouseenter' 'set_hover' star=star_num %}
{% on 'mouseleave' 'clear_hover' %}
6. wire.params - URL 상태 저장#
# 저장
self.wire.params["rating"] = str(score)
# 로드
if rating_param := self.wire.params.get("rating"):
self.current_rating = int(rating_param)
URL이 ?rating=4로 업데이트되어 새로고침해도 상태 유지됩니다.
7. 통계 컴포넌트#
평균과 분포를 보여주는 XRatingStats 컴포넌트:
class XRatingStats(Component):
class Meta:
template_name = "rating/rating_stats.html"
subscriptions = {"rating.rating"}
product: Product
async def mutation(self, channel, action, instance):
if instance.product_id == self.product.id:
self.force_render()
연습 문제#
- 반 별: 0.5 단위 평점 지원
- 평점 취소: 같은 별 클릭 시 평점 취소
- 애니메이션: 별 선택 시 펄스 애니메이션
다음 단계#
- Todo 앱 - CRUD, 모델 구독, 중첩 컴포넌트