많은 사이클링 그룹에는 정기적인 라이드가 있습니다 - 월요일 아침 출퇴근 그룹, 매월 첫 번째 토요일 어드벤처 라이드, 또는 수요일 저녁 소셜 라이드 등. 지금까지 주최자들은 각 이벤트를 수동으로 만들어야 했습니다. 새로운 반복 파티 기능으로 일정을 한 번만 설정하면 Party Onbici가 자동으로 인스턴스를 생성합니다.
일회성 이벤트의 문제점
커뮤니티 사이클링 그룹은 일반적으로 예측 가능한 일정을 가지고 있습니다:
- “매주 화요일과 목요일 오전 6시 30분”
- “매월 첫 번째 일요일”
- “격주 토요일 아침”
이러한 이벤트를 수동으로 만드는 것은 지루하고 오류가 발생하기 쉬우며, 각 회차에서 일관된 경로와 설정을 유지하기 어렵습니다.
iCalendar RRULE 소개
우리는 iCalendar RRULE 표준(RFC 5545)을 기반으로 반복 이벤트 시스템을 구축했습니다. 이것은 Google Calendar, Apple Calendar, Outlook에서 사용하는 것과 동일한 형식으로, 더 넓은 캘린더 생태계와의 호환성을 보장합니다.
반복 패턴 예시
1
2
3
4
5
6
7
8
9
10
11
| RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR
→ 매주 월요일, 수요일, 금요일
RRULE:FREQ=MONTHLY;BYDAY=1SA
→ 매월 첫 번째 토요일
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=SU
→ 격주 일요일
RRULE:FREQ=DAILY;COUNT=10
→ 10회 동안 매일
|
데이터 모델
RecurringParty는 Party 인스턴스를 생성하는 템플릿 역할을 합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class RecurringParty(models.Model):
name = models.CharField(max_length=255)
# Schedule configuration
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
max_occurrences = models.PositiveIntegerField(null=True, blank=True)
recurrence = RecurrenceField() # RRULE pattern
# Template fields (copied to each instance)
departure_time = models.TimeField()
arrival_time = models.TimeField()
origin = PointField()
destination = PointField()
route = models.JSONField()
difficulty = models.CharField(choices=DIFFICULTY_CHOICES)
# ... other party attributes
is_active = models.BooleanField(default=True)
|
생성된 각 Party 인스턴스는 부모에 연결됩니다:
1
2
3
4
5
6
7
8
9
10
11
12
| class Party(models.Model):
# Existing fields...
# Recurring party support
scheduled_date = models.DateField(null=True, blank=True)
recurring_parent = models.ForeignKey(
RecurringParty,
on_delete=models.SET_NULL,
null=True,
related_name="instances"
)
is_cancelled = models.BooleanField(default=False)
|
인스턴스 생성
반복 파티가 생성되거나 일정이 변경되면 다가오는 기간에 대한 인스턴스를 생성합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| def generate_instances(self, lookahead_days: int = 30) -> list[Party]:
"""Generate party instances for upcoming occurrences."""
occurrences = self.recurrence.between(
datetime.now().date(),
datetime.now().date() + timedelta(days=lookahead_days),
)
created = []
for date in occurrences:
# Skip if instance already exists
if self.instances.filter(scheduled_date=date).exists():
continue
# Create new party instance from template
party = Party.objects.create(
name=f"{self.name} - {date.strftime('%B %d')}",
scheduled_date=date,
recurring_parent=self,
departure_time=self.departure_time,
arrival_time=self.arrival_time,
origin=self.origin,
destination=self.destination,
route=self.route,
# ... copy other template fields
)
created.append(party)
return created
|
Celery Beat를 통한 자동 생성
일일 Celery 작업이 인스턴스를 최신 상태로 유지합니다:
1
2
3
4
5
6
7
8
9
| @shared_task
def generate_recurring_party_instances():
"""Generate party instances for all active recurring parties."""
for recurring_party in RecurringParty.objects.filter(is_active=True):
created = recurring_party.generate_instances(lookahead_days=30)
if created:
logger.info(
f"Generated {len(created)} instances for {recurring_party.name}"
)
|
Celery Beat 스케줄은 자정에 이를 실행합니다:
1
2
3
4
5
6
| CELERY_BEAT_SCHEDULE = {
"generate-recurring-party-instances": {
"task": "party_onbici.apps.party.tasks.generate_recurring_party_instances",
"schedule": crontab(hour=0, minute=0), # Daily at midnight
},
}
|
사용자 인터페이스
반복 파티 만들기
폼은 django-recurrence 위젯을 사용하여 RRULE 패턴을 구축합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class RecurringPartyForm(forms.ModelForm):
recurrence = RecurrenceField(
label="Recurrence pattern",
help_text="Set up when this party should repeat.",
required=True,
)
class Meta:
model = RecurringParty
fields = [
"name", "start_date", "end_date", "recurrence",
"departure_time", "arrival_time",
"origin", "destination", "description",
"difficulty", "gender", "privacy",
]
|
위젯은 일반적인 패턴에 대한 직관적인 인터페이스를 제공합니다:
- 매일 / 매주 / 매월 / 매년
- 특정 요일
- 순서 패턴 (첫 번째, 두 번째, 마지막)
- 사용자 지정 간격 (2주마다)
- 종료 조건 (특정 날짜까지, N회 후, 또는 무기한)
인스턴스 관리
주최자는 생성된 모든 인스턴스를 보고 다음을 수행할 수 있습니다:
- 개별 인스턴스 편집 - 한 회차의 경로나 시간 변경
- 인스턴스 취소 - 특정 날짜를 취소됨으로 표시
- 인스턴스 재생성 - 다음 30일에 대한 생성을 수동으로 트리거
특수 케이스 처리
시간대 인식
모든 날짜는 주최자가 설정한 시간대에 저장되고 보는 사람의 현지 시간으로 표시됩니다:
1
2
3
| # Generate instances using timezone-aware dates
local_tz = get_current_timezone()
today = timezone.now().astimezone(local_tz).date()
|
휴일 및 예외 처리
사용자는 반복 패턴에 영향을 주지 않고 개별 인스턴스를 취소할 수 있습니다:
1
2
3
| party = recurring_party.instances.get(scheduled_date=holiday_date)
party.is_cancelled = True
party.save()
|
취소된 인스턴스는 시각적 표시기와 함께 목록에 계속 표시되어 혼란을 방지합니다.
고아 인스턴스
반복 파티가 삭제되면 생성된 인스턴스를 선택적으로 보존할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
| def form_valid(self, form):
delete_instances = request.POST.get("delete_instances") == "true"
if delete_instances:
# Soft-delete future instances
recurring_party.instances.filter(
scheduled_date__gte=date.today(),
deleted_at__isnull=True,
).update(deleted_at=timezone.now())
recurring_party.delete()
|
사용자 경험
주간 라이드 만들기는 몇 단계만으로 완료됩니다:
- 대시보드 → 반복 파티 → 만들기로 이동
- 파티 세부 정보 설정 (경로, 시간, 난이도)
- 반복 패턴 설정 (예: “매주 토요일 오전 8시”)
- 시작 날짜와 선택적 종료 날짜 설정
- 저장 - 인스턴스가 자동으로 생성됩니다!
시스템은 30일 앞까지 파티 인스턴스를 만들고, Celery가 시간이 지남에 따라 계속 새로운 인스턴스를 생성합니다.
사이클링 커뮤니티를 위한 이점
- 설정하고 잊어버리기 - 더 이상 매주 이벤트 만들기 필요 없음
- 일관성 - 같은 경로, 같은 시간, 같은 설정
- 유연성 - 개별 회차 편집 또는 취소
- 발견 - 라이더들이 정기 그룹 라이드를 쉽게 찾을 수 있음
- 캘린더 통합 - 캘린더 앱에서 반복 이벤트 구독
정기 라이드를 설정할 준비가 되셨나요? 반복 파티 만들기를 통해 일정 관리는 저희에게 맡기세요!