Skip to content

Commit 736f345

Browse files
committed
feat: 통계에 대한 기본 조회 기간 추가
1 parent ed158ff commit 736f345

8 files changed

Lines changed: 165 additions & 8 deletions

File tree

app/admin_api/dashboard/params.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from uuid import UUID
66

77
from core.const.datetime import Granularity
8-
from core.serializer.date_range_serializer import DateRangeSerializer
8+
from core.serializer.date_range_serializer import DateRangeSerializer, day_start, next_day_start
99
from django.db.models import QuerySet
10+
from event.models import Event
1011
from rest_framework import serializers
1112
from shop.order.models import OrderProductRelation
1213

@@ -42,7 +43,23 @@ class CounterParamsSerializer(serializers.Serializer):
4243

4344

4445
class _TimeSeriesParamsSerializer(CounterParamsSerializer):
45-
date_range = DateRangeSerializer(required=True, label="조회 기간")
46+
date_range = DateRangeSerializer(required=False, label="조회 기간")
47+
48+
def validate(self, attrs: dict) -> dict:
49+
if not attrs.get("date_range") and (event_id := attrs.get("event_id")):
50+
period = (
51+
Event.objects.filter_active()
52+
.filter(id=event_id)
53+
.values_list("stats_start_date", "stats_end_date")
54+
.first()
55+
)
56+
if period and all(period):
57+
start, end = period
58+
attrs["date_range"] = {"date_from": day_start(start), "date_to": next_day_start(end)}
59+
if not attrs.get("date_range"):
60+
err_msg = "조회 기간을 지정하거나, 통계 기간이 설정된 이벤트를 선택하세요."
61+
raise serializers.ValidationError({"date_range": err_msg})
62+
return attrs
4663

4764

4865
class SalesTrendParamsSerializer(_TimeSeriesParamsSerializer):

app/admin_api/serializers/event/event.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,15 @@ class Meta:
1414
"name_en",
1515
"event_start_at",
1616
"event_end_at",
17+
"stats_start_date",
18+
"stats_end_date",
1719
)
20+
21+
def validate(self, attrs: dict) -> dict:
22+
merged = {**attrs}
23+
for field in ("stats_start_date", "stats_end_date"):
24+
merged.setdefault(field, getattr(self.instance, field, None))
25+
start, end = merged["stats_start_date"], merged["stats_end_date"]
26+
if start and end and start > end:
27+
raise serializers.ValidationError({"stats_end_date": "통계 종료일은 시작일보다 이전일 수 없습니다."})
28+
return attrs
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from datetime import date
2+
3+
from admin_api.serializers.event.event import EventAdminSerializer
4+
from model_bakery import baker
5+
6+
7+
def test_admin_serializer_exposes_stats_period_fields():
8+
assert {"stats_start_date", "stats_end_date"} <= set(EventAdminSerializer().fields)
9+
10+
11+
def test_admin_serializer_accepts_valid_stats_period(db):
12+
event = baker.make("event.Event")
13+
serializer = EventAdminSerializer(
14+
instance=event,
15+
data={"stats_start_date": "2026-08-14", "stats_end_date": "2026-08-16"},
16+
partial=True,
17+
)
18+
assert serializer.is_valid(), serializer.errors
19+
20+
21+
def test_admin_serializer_rejects_inverted_stats_period(db):
22+
event = baker.make("event.Event", stats_start_date=date(2026, 8, 1))
23+
serializer = EventAdminSerializer(
24+
instance=event,
25+
data={"stats_start_date": "2026-08-16", "stats_end_date": "2026-08-14"},
26+
partial=True,
27+
)
28+
assert not serializer.is_valid()
29+
assert "stats_end_date" in serializer.errors

app/admin_api/test/shop/dashboard_test.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import date, datetime
22

33
import pytest
44
from core.const.datetime import KST
@@ -197,6 +197,51 @@ def test_sales_trend_missing_date_range_is_400(api_client):
197197
assert "date_range" in {e["attr"] for e in resp.json()["errors"]}
198198

199199

200+
# --- 이벤트별 통계 기간 프리셋 ---
201+
def _attach_event(ticket_product, **stats):
202+
event = baker.make("event.Event", name="파이콘 한국 2026", **stats)
203+
ticket_product.category.event = event
204+
ticket_product.category.save()
205+
return event
206+
207+
208+
def test_time_series_defaults_to_event_stats_period(api_client, ticket_product):
209+
"""date_range 미전송 + 통계 기간이 설정된 이벤트 선택 → 그 기간(inclusive)을 기본값으로 사용."""
210+
event = _attach_event(ticket_product, stats_start_date=date(2026, 8, 14), stats_end_date=date(2026, 8, 16))
211+
body = _chart_data(api_client, "line-sales-trend", {"event_id": str(event.id), "granularity": "day"}).json()
212+
assert [d["label"] for d in body["data"]] == ["2026-08-14", "2026-08-15", "2026-08-16"]
213+
214+
215+
def test_explicit_date_range_overrides_event_preset(api_client, ticket_product):
216+
event = _attach_event(ticket_product, stats_start_date=date(2026, 8, 1), stats_end_date=date(2026, 8, 31))
217+
body = _chart_data(
218+
api_client,
219+
"line-sales-trend",
220+
{
221+
"event_id": str(event.id),
222+
"date_range": {"date_from": "2026-08-14", "date_to": "2026-08-15"},
223+
"granularity": "day",
224+
},
225+
).json()
226+
assert [d["label"] for d in body["data"]] == ["2026-08-14", "2026-08-15"]
227+
228+
229+
def test_event_without_stats_period_still_requires_date_range(api_client, ticket_product):
230+
event = _attach_event(ticket_product) # 통계 기간 미설정
231+
resp = _chart_data(api_client, "line-sales-trend", {"event_id": str(event.id), "granularity": "day"})
232+
assert resp.status_code == status.HTTP_400_BAD_REQUEST
233+
assert "date_range" in {e["attr"] for e in resp.json()["errors"]}
234+
235+
236+
def test_event_option_carries_stats_period(api_client, ticket_product):
237+
"""프론트 프리필용: event 옵션이 통계 기본 기간을 동봉."""
238+
event = _attach_event(ticket_product, stats_start_date=date(2026, 8, 14), stats_end_date=date(2026, 8, 16))
239+
params = next(c for c in api_client.get(CHARTS_URL).json() if c["id"] == "line-sales-trend")["params"]
240+
opt = next(o for p in params if p["key"] == "event_id" for o in p["options"] if o["value"] == str(event.id))
241+
assert opt["date_from"] == "2026-08-14"
242+
assert opt["date_to"] == "2026-08-16"
243+
244+
200245
def test_sales_trend_invalid_granularity_is_400(api_client):
201246
resp = _chart_data(
202247
api_client,

app/admin_api/views/dashboard.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,31 @@ def list(self, request: Request) -> Response:
3535
.order_by("category__priority", "name")
3636
.values("id", "name", "category__event_id") # event_id: 프론트의 이벤트→티켓 종속 필터용
3737
)
38-
es = Event.objects.filter_active().filter(category__is_ticket=True).distinct().order_by("name")
38+
es = (
39+
Event.objects.filter_active()
40+
.filter(category__is_ticket=True)
41+
.values("id", "name", "stats_start_date", "stats_end_date")
42+
.distinct()
43+
.order_by("name")
44+
)
3945
dynamic_options = {
4046
"tickets": [
4147
{
4248
"value": str(t["id"]),
4349
"label": t["name"],
44-
"event_id": str(t["category__event_id"]) if t["category__event_id"] else None,
50+
"event_id": t["category__event_id"] and str(t["category__event_id"]),
4551
}
4652
for t in ts
4753
],
48-
"events": [{"value": str(e.id), "label": e.name} for e in es],
54+
"events": [
55+
{
56+
"value": str(e["id"]),
57+
"label": e["name"],
58+
"date_from": e["stats_start_date"] and e["stats_start_date"].isoformat(),
59+
"date_to": e["stats_end_date"] and e["stats_end_date"].isoformat(),
60+
}
61+
for e in es
62+
],
4963
}
5064
return Response([handler.to_dict(dynamic_options) for handler in CHART_REGISTRY.values()])
5165

app/core/serializer/date_range_serializer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@
44
from rest_framework import serializers
55

66

7+
def day_start(value: date) -> datetime:
8+
"""해당 날짜의 KST 자정 — 구간 포함 하한."""
9+
return datetime.combine(value, time.min, tzinfo=KST)
10+
11+
12+
def next_day_start(value: date) -> datetime:
13+
"""다음 날의 KST 자정 — 종료일 포함을 위한 반열린 상한(exclusive)."""
14+
return datetime.combine(value + timedelta(days=1), time.min, tzinfo=KST)
15+
16+
717
class DateRangeSerializer(serializers.Serializer):
818
date_from = serializers.DateField()
919
date_to = serializers.DateField()
1020

1121
def validate_date_from(self, value: date) -> datetime:
12-
return datetime.combine(value, time.min, tzinfo=KST)
22+
return day_start(value)
1323

1424
def validate_date_to(self, value: date) -> datetime:
15-
return datetime.combine(value + timedelta(days=1), time.min, tzinfo=KST)
25+
return next_day_start(value)
1626

1727
def validate(self, attrs: dict) -> dict:
1828
# date_to 는 exclusive end 로 +1d 보정된 값 — 정상 구간이면 from < to.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
dependencies = [("event", "0004_remove_event_uq__evt__name_name_ko_and_more")]
6+
operations = [
7+
migrations.AddField(
8+
model_name="event",
9+
name="stats_end_date",
10+
field=models.DateField(blank=True, null=True),
11+
),
12+
migrations.AddField(
13+
model_name="event",
14+
name="stats_start_date",
15+
field=models.DateField(blank=True, null=True),
16+
),
17+
migrations.AddField(
18+
model_name="historicalevent",
19+
name="stats_end_date",
20+
field=models.DateField(blank=True, null=True),
21+
),
22+
migrations.AddField(
23+
model_name="historicalevent",
24+
name="stats_start_date",
25+
field=models.DateField(blank=True, null=True),
26+
),
27+
]

app/event/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class Event(BaseAbstractModel):
1414
event_end_at = models.DateTimeField(null=True, blank=True)
1515
banner_display_start_at = models.DateTimeField(null=True, blank=True)
1616
banner_display_end_at = models.DateTimeField(null=True, blank=True)
17+
stats_start_date = models.DateField(null=True, blank=True)
18+
stats_end_date = models.DateField(null=True, blank=True)
1719

1820
class Meta:
1921
ordering = ["-event_start_at", "-event_end_at"]
@@ -34,3 +36,5 @@ def clean(self) -> None:
3436
and self.banner_display_start_at > self.banner_display_end_at
3537
):
3638
raise ValidationError("banner 전시 종료 날짜는 시작 날짜보다 이전일 수 없습니다.")
39+
if self.stats_start_date and self.stats_end_date and self.stats_start_date > self.stats_end_date:
40+
raise ValidationError("통계 종료일은 시작일보다 이전일 수 없습니다.")

0 commit comments

Comments
 (0)