Add Chzzk campaign and reward models, import command, and schemas
Some checks failed
Deploy to Server / deploy (push) Failing after 19s
Some checks failed
Deploy to Server / deploy (push) Failing after 19s
This commit is contained in:
parent
c852134338
commit
677aedf42b
14 changed files with 650 additions and 9 deletions
0
chzzk/management/__init__.py
Normal file
0
chzzk/management/__init__.py
Normal file
0
chzzk/management/commands/__init__.py
Normal file
0
chzzk/management/commands/__init__.py
Normal file
137
chzzk/management/commands/import_chzzk_campaign.py
Normal file
137
chzzk/management/commands/import_chzzk_campaign.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
from chzzk.schemas import ChzzkCampaignV1
|
||||
from chzzk.schemas import ChzzkCampaignV2
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from chzzk.models import ChzzkCampaign
|
||||
from chzzk.models import ChzzkReward
|
||||
from chzzk.schemas import ChzzkApiResponseV1
|
||||
from chzzk.schemas import ChzzkApiResponseV2
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import argparse
|
||||
|
||||
CHZZK_API_URLS: list[tuple[str, str]] = [
|
||||
("v1", "https://api.chzzk.naver.com/service/v1/drops/campaigns/{campaign_no}"),
|
||||
("v2", "https://api.chzzk.naver.com/service/v2/drops/campaigns/{campaign_no}"),
|
||||
]
|
||||
|
||||
USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0"
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Django management command to scrape Chzzk drops campaigns from both v1 and v2 APIs and store them in the database."""
|
||||
|
||||
help = "Scrape Chzzk drops campaigns from both v1 and v2 APIs and store them."
|
||||
|
||||
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
||||
"""Add command-line arguments for the management command."""
|
||||
parser.add_argument("campaign_no", type=int, help="Campaign number to fetch")
|
||||
|
||||
def handle(self, **options) -> None:
|
||||
"""Main handler for the management command. Fetches campaign data from both API versions, validates, and stores them."""
|
||||
campaign_no: int = int(options["campaign_no"])
|
||||
for api_version, url_template in CHZZK_API_URLS:
|
||||
url: str = url_template.format(campaign_no=campaign_no)
|
||||
resp: requests.Response = requests.get(
|
||||
url,
|
||||
timeout=2,
|
||||
headers={
|
||||
"Accept": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data: dict[str, Any] = resp.json()
|
||||
|
||||
campaign_data: ChzzkCampaignV1 | ChzzkCampaignV2
|
||||
if api_version == "v1":
|
||||
campaign_data = ChzzkApiResponseV1.model_validate(data).content
|
||||
elif api_version == "v2":
|
||||
campaign_data = ChzzkApiResponseV2.model_validate(data).content
|
||||
else:
|
||||
msg: str = f"Unknown API version: {api_version}"
|
||||
self.stdout.write(self.style.ERROR(msg))
|
||||
continue
|
||||
|
||||
# Save campaign
|
||||
campaign_obj, created = ChzzkCampaign.objects.update_or_create(
|
||||
campaign_no=campaign_data.campaign_no,
|
||||
source_api=api_version,
|
||||
defaults={
|
||||
"title": campaign_data.title,
|
||||
"image_url": campaign_data.image_url,
|
||||
"description": campaign_data.description,
|
||||
"category_type": campaign_data.category_type,
|
||||
"category_id": campaign_data.category_id,
|
||||
"category_value": campaign_data.category_value,
|
||||
"pc_link_url": campaign_data.pc_link_url,
|
||||
"mobile_link_url": campaign_data.mobile_link_url,
|
||||
"service_id": campaign_data.service_id,
|
||||
"state": campaign_data.state,
|
||||
"start_date": campaign_data.start_date,
|
||||
"end_date": campaign_data.end_date,
|
||||
"has_ios_based_reward": campaign_data.has_ios_based_reward,
|
||||
"drops_campaign_not_started": campaign_data.drops_campaign_not_started,
|
||||
"campaign_reward_type": getattr(
|
||||
campaign_data,
|
||||
"campaign_reward_type",
|
||||
"",
|
||||
),
|
||||
"reward_type": getattr(campaign_data, "reward_type", ""),
|
||||
"account_link_url": campaign_data.account_link_url,
|
||||
"scraped_at": timezone.now(),
|
||||
"scrape_status": "success",
|
||||
"raw_json": data,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Created campaign {campaign_no} from {api_version}",
|
||||
),
|
||||
)
|
||||
for reward in campaign_data.reward_list:
|
||||
reward_, created = ChzzkReward.objects.update_or_create(
|
||||
campaign=campaign_obj,
|
||||
reward_no=reward.reward_no,
|
||||
defaults={
|
||||
"image_url": reward.image_url,
|
||||
"title": reward.title,
|
||||
"reward_type": reward.reward_type,
|
||||
"campaign_reward_type": getattr(
|
||||
reward,
|
||||
"campaign_reward_type",
|
||||
"",
|
||||
),
|
||||
"condition_type": reward.condition_type,
|
||||
"condition_for_minutes": reward.condition_for_minutes,
|
||||
"ios_based_reward": reward.ios_based_reward,
|
||||
"code_remaining_count": reward.code_remaining_count,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f" Created reward {reward_.reward_no} for campaign {campaign_no}",
|
||||
),
|
||||
)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Imported campaign {campaign_no} from {api_version}",
|
||||
),
|
||||
)
|
||||
100
chzzk/migrations/0001_initial.py
Normal file
100
chzzk/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Generated by Django 6.0.3 on 2026-03-31 19:33
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Initial migration for ChzzkCampaign and ChzzkReward models."""
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ChzzkCampaign",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("campaign_no", models.BigIntegerField(unique=True)),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("image_url", models.URLField()),
|
||||
("description", models.TextField()),
|
||||
("category_type", models.CharField(max_length=64)),
|
||||
("category_id", models.CharField(max_length=128)),
|
||||
("category_value", models.CharField(max_length=128)),
|
||||
("pc_link_url", models.URLField()),
|
||||
("mobile_link_url", models.URLField()),
|
||||
("service_id", models.CharField(max_length=128)),
|
||||
("state", models.CharField(max_length=64)),
|
||||
("start_date", models.DateTimeField()),
|
||||
("end_date", models.DateTimeField()),
|
||||
("has_ios_based_reward", models.BooleanField()),
|
||||
("drops_campaign_not_started", models.BooleanField()),
|
||||
(
|
||||
"campaign_reward_type",
|
||||
models.CharField(blank=True, default="", max_length=64),
|
||||
),
|
||||
(
|
||||
"reward_type",
|
||||
models.CharField(blank=True, default="", max_length=64),
|
||||
),
|
||||
("account_link_url", models.URLField()),
|
||||
("scraped_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||
("source_api", models.CharField(max_length=16)),
|
||||
("scrape_status", models.CharField(default="success", max_length=32)),
|
||||
("raw_json", models.JSONField()),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-start_date"],
|
||||
"unique_together": {("campaign_no", "source_api")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ChzzkReward",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("reward_no", models.BigIntegerField()),
|
||||
("image_url", models.URLField()),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("reward_type", models.CharField(max_length=64)),
|
||||
(
|
||||
"campaign_reward_type",
|
||||
models.CharField(blank=True, default="", max_length=64),
|
||||
),
|
||||
("condition_type", models.CharField(max_length=64)),
|
||||
("condition_for_minutes", models.IntegerField()),
|
||||
("ios_based_reward", models.BooleanField()),
|
||||
("code_remaining_count", models.IntegerField()),
|
||||
(
|
||||
"campaign",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="rewards",
|
||||
to="chzzk.chzzkcampaign",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("campaign", "reward_no")},
|
||||
},
|
||||
),
|
||||
]
|
||||
20
chzzk/migrations/0002_alter_chzzkcampaign_campaign_no.py
Normal file
20
chzzk/migrations/0002_alter_chzzkcampaign_campaign_no.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 6.0.3 on 2026-03-31 19:53
|
||||
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""Alter campaign_no field in ChzzkCampaign to remove unique constraint."""
|
||||
|
||||
dependencies = [
|
||||
("chzzk", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="chzzkcampaign",
|
||||
name="campaign_no",
|
||||
field=models.BigIntegerField(),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class ChzzkCampaign(models.Model):
|
||||
"""Chzzk campaign, including scraping metadata."""
|
||||
|
||||
campaign_no = models.BigIntegerField()
|
||||
title = models.CharField(max_length=255)
|
||||
image_url = models.URLField()
|
||||
description = models.TextField()
|
||||
category_type = models.CharField(max_length=64)
|
||||
category_id = models.CharField(max_length=128)
|
||||
category_value = models.CharField(max_length=128)
|
||||
pc_link_url = models.URLField()
|
||||
mobile_link_url = models.URLField()
|
||||
service_id = models.CharField(max_length=128)
|
||||
state = models.CharField(max_length=64)
|
||||
start_date = models.DateTimeField()
|
||||
end_date = models.DateTimeField()
|
||||
has_ios_based_reward = models.BooleanField()
|
||||
drops_campaign_not_started = models.BooleanField()
|
||||
campaign_reward_type = models.CharField(max_length=64, blank=True, default="")
|
||||
reward_type = models.CharField(max_length=64, blank=True, default="")
|
||||
account_link_url = models.URLField()
|
||||
|
||||
# Scraping metadata
|
||||
scraped_at = models.DateTimeField(default=timezone.now)
|
||||
source_api = models.CharField(max_length=16) # 'v1' or 'v2'
|
||||
scrape_status = models.CharField(max_length=32, default="success")
|
||||
raw_json = models.JSONField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("campaign_no", "source_api")
|
||||
ordering = ["-start_date"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.title} (#{self.campaign_no})"
|
||||
|
||||
|
||||
class ChzzkReward(models.Model):
|
||||
"""Chzzk reward belonging to a campaign."""
|
||||
|
||||
campaign = models.ForeignKey(
|
||||
ChzzkCampaign,
|
||||
related_name="rewards",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
reward_no = models.BigIntegerField()
|
||||
image_url = models.URLField()
|
||||
title = models.CharField(max_length=255)
|
||||
reward_type = models.CharField(max_length=64)
|
||||
campaign_reward_type = models.CharField(max_length=64, blank=True, default="")
|
||||
condition_type = models.CharField(max_length=64)
|
||||
condition_for_minutes = models.IntegerField()
|
||||
ios_based_reward = models.BooleanField()
|
||||
code_remaining_count = models.IntegerField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("campaign", "reward_no")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.title} (#{self.reward_no})"
|
||||
107
chzzk/schemas.py
Normal file
107
chzzk/schemas.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
class ChzzkRewardV1(BaseModel):
|
||||
"""Pydantic schema for Chzzk v1 reward object."""
|
||||
|
||||
title: str
|
||||
reward_no: int = Field(..., alias="rewardNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
reward_type: str = Field(..., alias="rewardType")
|
||||
condition_type: str = Field(..., alias="conditionType")
|
||||
condition_for_minutes: int = Field(..., alias="conditionForMinutes")
|
||||
ios_based_reward: bool = Field(..., alias="iosBasedReward")
|
||||
code_remaining_count: int = Field(..., alias="codeRemainingCount")
|
||||
|
||||
# Only in v1 API
|
||||
campaign_reward_type: str | None = Field(None, alias="campaignRewardType")
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkRewardV2(BaseModel):
|
||||
"""Pydantic schema for Chzzk v2 reward object."""
|
||||
|
||||
title: str
|
||||
reward_no: int = Field(..., alias="rewardNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
reward_type: str = Field(..., alias="rewardType")
|
||||
condition_type: str = Field(..., alias="conditionType")
|
||||
condition_for_minutes: int = Field(..., alias="conditionForMinutes")
|
||||
ios_based_reward: bool = Field(..., alias="iosBasedReward")
|
||||
code_remaining_count: int = Field(..., alias="codeRemainingCount")
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkCampaignV1(BaseModel):
|
||||
"""Pydantic schema for Chzzk v1 campaign object."""
|
||||
|
||||
title: str
|
||||
state: str
|
||||
description: str
|
||||
campaign_no: int = Field(..., alias="campaignNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
category_type: str = Field(..., alias="categoryType")
|
||||
category_id: str = Field(..., alias="categoryId")
|
||||
category_value: str = Field(..., alias="categoryValue")
|
||||
pc_link_url: str = Field(..., alias="pcLinkUrl")
|
||||
mobile_link_url: str = Field(..., alias="mobileLinkUrl")
|
||||
service_id: str = Field(..., alias="serviceId")
|
||||
start_date: str = Field(..., alias="startDate")
|
||||
end_date: str = Field(..., alias="endDate")
|
||||
reward_list: list[ChzzkRewardV1] = Field(..., alias="rewardList")
|
||||
has_ios_based_reward: bool = Field(..., alias="hasIosBasedReward")
|
||||
drops_campaign_not_started: bool = Field(..., alias="dropsCampaignNotStarted")
|
||||
campaign_reward_type: str | None = Field(None, alias="campaignRewardType")
|
||||
account_link_url: str = Field(..., alias="accountLinkUrl")
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkCampaignV2(BaseModel):
|
||||
"""Pydantic schema for Chzzk v2 campaign object."""
|
||||
|
||||
title: str
|
||||
state: str
|
||||
campaign_no: int = Field(..., alias="campaignNo")
|
||||
image_url: str = Field(..., alias="imageUrl")
|
||||
description: str
|
||||
category_type: str = Field(..., alias="categoryType")
|
||||
category_id: str = Field(..., alias="categoryId")
|
||||
category_value: str = Field(..., alias="categoryValue")
|
||||
pc_link_url: str = Field(..., alias="pcLinkUrl")
|
||||
mobile_link_url: str = Field(..., alias="mobileLinkUrl")
|
||||
service_id: str = Field(..., alias="serviceId")
|
||||
start_date: str = Field(..., alias="startDate")
|
||||
end_date: str = Field(..., alias="endDate")
|
||||
reward_list: list[ChzzkRewardV2] = Field(..., alias="rewardList")
|
||||
has_ios_based_reward: bool = Field(..., alias="hasIosBasedReward")
|
||||
drops_campaign_not_started: bool = Field(..., alias="dropsCampaignNotStarted")
|
||||
reward_type: str | None = Field(None, alias="rewardType")
|
||||
account_link_url: str = Field(..., alias="accountLinkUrl")
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkApiResponseV1(BaseModel):
|
||||
"""Pydantic schema for Chzzk v1 API response."""
|
||||
|
||||
code: int
|
||||
message: Any | None
|
||||
content: ChzzkCampaignV1
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
|
||||
|
||||
class ChzzkApiResponseV2(BaseModel):
|
||||
"""Pydantic schema for Chzzk v2 API response."""
|
||||
|
||||
code: int
|
||||
message: Any | None
|
||||
content: ChzzkCampaignV2
|
||||
|
||||
model_config = {"extra": "forbid"}
|
||||
40
chzzk/tests/test_schemas.py
Normal file
40
chzzk/tests/test_schemas.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pydantic
|
||||
import pytest
|
||||
|
||||
from chzzk.schemas import ChzzkApiResponseV1
|
||||
from chzzk.schemas import ChzzkApiResponseV2
|
||||
|
||||
TESTS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fname", "data_model"),
|
||||
[
|
||||
("v1_905.json", ChzzkApiResponseV1),
|
||||
("v2_905.json", ChzzkApiResponseV2),
|
||||
],
|
||||
)
|
||||
def test_chzzk_schema_strict(fname: str, data_model: type) -> None:
|
||||
"""Test that the schema strictly validates the given JSON file against the provided Pydantic model."""
|
||||
with Path(TESTS_DIR / fname).open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
# Should not raise
|
||||
data_model.model_validate(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fname", "data_model"),
|
||||
[
|
||||
("v1_905.json", ChzzkApiResponseV2),
|
||||
("v2_905.json", ChzzkApiResponseV1),
|
||||
],
|
||||
)
|
||||
def test_chzzk_schema_cross_fail(fname: str, data_model: type) -> None:
|
||||
"""Test that the schema fails when validating the wrong JSON file/model combination."""
|
||||
with Path(TESTS_DIR / fname).open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
with pytest.raises(pydantic.ValidationError):
|
||||
data_model.model_validate(data)
|
||||
58
chzzk/tests/v1_905.json
generated
Normal file
58
chzzk/tests/v1_905.json
generated
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"code": 200,
|
||||
"message": null,
|
||||
"content": {
|
||||
"campaignNo": 905,
|
||||
"title": "붉은사막 드롭스 #2",
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMzcg/MDAxNzc0MzU4Mzk3MjM0.JQAXkEYe2ntJ5gvzr5U5egn78DalY24mi1hodGyPYcog.tko3iBzm7dOqDKdjZAZX2ozWrj-tKjVcN3v3ieQaNzQg.JPEG/KR_260319_sony_masterimage_2160x2160.jpg",
|
||||
"description": "치지직에서 붉은사막 방송을 시청하고, 특별한 보상을 획득해 보세요.",
|
||||
"categoryType": "GAME",
|
||||
"categoryId": "CrimsonDesert",
|
||||
"categoryValue": "붉은사막",
|
||||
"pcLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
|
||||
"mobileLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
|
||||
"serviceId": "Pearl_Abyss_Event",
|
||||
"state": "PROMOTED",
|
||||
"startDate": "2026-03-26 09:00:00",
|
||||
"endDate": "2026-04-02 08:59:00",
|
||||
"rewardList": [
|
||||
{
|
||||
"rewardNo": 2521,
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTIg/MDAxNzc0MzU4NTgxMTY5.WFRtLuWw0qciEmMlnm79culfGW1lj-KwvXTaZLkbzMIg.PxoTwZ6v_i1N7Vkyj--OxmeuEJsRvQDWgjT1sOB6kfog.PNG/Week-2-60-min.png",
|
||||
"title": "푸른 정찰대 등자",
|
||||
"rewardType": "TIME_BASED",
|
||||
"campaignRewardType": "IN_APP",
|
||||
"conditionType": "TIME_BASED",
|
||||
"conditionForMinutes": 60,
|
||||
"iosBasedReward": false,
|
||||
"codeRemainingCount": 0
|
||||
},
|
||||
{
|
||||
"rewardNo": 2522,
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTYw/MDAxNzc0MzU4NjEwODkz.6SMLoYofJ6A2a9Nd1IlKn_CC3JLyR8ecxkt8cO-rzcAg.kQSP_H08VoS1qXGiB_3BBMYT_0IT07YPz09n0lNmD1cg.PNG/Week-2-120-min.png",
|
||||
"title": "푸른 정찰대 마면",
|
||||
"rewardType": "TIME_BASED",
|
||||
"campaignRewardType": "IN_APP",
|
||||
"conditionType": "TIME_BASED",
|
||||
"conditionForMinutes": 120,
|
||||
"iosBasedReward": false,
|
||||
"codeRemainingCount": 0
|
||||
},
|
||||
{
|
||||
"rewardNo": 2523,
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMjU3/MDAxNzc0MzU4NjM5MDgw.7u6E8ybLp7x9MNESI3od1Sf3AE_6AobSMFfb18MhsNog.M5ZvA1SSjaOObB2TB-P9Rw2rtMcV8zXipL0i7adILdwg.PNG/Week-2-180-min.png",
|
||||
"title": "푸른 정찰대 안장",
|
||||
"rewardType": "TIME_BASED",
|
||||
"campaignRewardType": "IN_APP",
|
||||
"conditionType": "TIME_BASED",
|
||||
"conditionForMinutes": 180,
|
||||
"iosBasedReward": false,
|
||||
"codeRemainingCount": 0
|
||||
}
|
||||
],
|
||||
"hasIosBasedReward": false,
|
||||
"dropsCampaignNotStarted": false,
|
||||
"campaignRewardType": "IN_APP",
|
||||
"accountLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops"
|
||||
}
|
||||
}
|
||||
55
chzzk/tests/v2_905.json
generated
Normal file
55
chzzk/tests/v2_905.json
generated
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"code": 200,
|
||||
"message": null,
|
||||
"content": {
|
||||
"campaignNo": 905,
|
||||
"title": "붉은사막 드롭스 #2",
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMzcg/MDAxNzc0MzU4Mzk3MjM0.JQAXkEYe2ntJ5gvzr5U5egn78DalY24mi1hodGyPYcog.tko3iBzm7dOqDKdjZAZX2ozWrj-tKjVcN3v3ieQaNzQg.JPEG/KR_260319_sony_masterimage_2160x2160.jpg",
|
||||
"description": "치지직에서 붉은사막 방송을 시청하고, 특별한 보상을 획득해 보세요.",
|
||||
"categoryType": "GAME",
|
||||
"categoryId": "CrimsonDesert",
|
||||
"categoryValue": "붉은사막",
|
||||
"pcLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
|
||||
"mobileLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops",
|
||||
"serviceId": "Pearl_Abyss_Event",
|
||||
"state": "PROMOTED",
|
||||
"startDate": "2026-03-26 09:00:00",
|
||||
"endDate": "2026-04-02 08:59:00",
|
||||
"rewardList": [
|
||||
{
|
||||
"rewardNo": 2521,
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTIg/MDAxNzc0MzU4NTgxMTY5.WFRtLuWw0qciEmMlnm79culfGW1lj-KwvXTaZLkbzMIg.PxoTwZ6v_i1N7Vkyj--OxmeuEJsRvQDWgjT1sOB6kfog.PNG/Week-2-60-min.png",
|
||||
"title": "푸른 정찰대 등자",
|
||||
"rewardType": "IN_APP",
|
||||
"conditionType": "TIME_BASED",
|
||||
"conditionForMinutes": 60,
|
||||
"iosBasedReward": false,
|
||||
"codeRemainingCount": 0
|
||||
},
|
||||
{
|
||||
"rewardNo": 2522,
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMTYw/MDAxNzc0MzU4NjEwODkz.6SMLoYofJ6A2a9Nd1IlKn_CC3JLyR8ecxkt8cO-rzcAg.kQSP_H08VoS1qXGiB_3BBMYT_0IT07YPz09n0lNmD1cg.PNG/Week-2-120-min.png",
|
||||
"title": "푸른 정찰대 마면",
|
||||
"rewardType": "IN_APP",
|
||||
"conditionType": "TIME_BASED",
|
||||
"conditionForMinutes": 120,
|
||||
"iosBasedReward": false,
|
||||
"codeRemainingCount": 0
|
||||
},
|
||||
{
|
||||
"rewardNo": 2523,
|
||||
"imageUrl": "https://nng-phinf.pstatic.net/MjAyNjAzMjRfMjU3/MDAxNzc0MzU4NjM5MDgw.7u6E8ybLp7x9MNESI3od1Sf3AE_6AobSMFfb18MhsNog.M5ZvA1SSjaOObB2TB-P9Rw2rtMcV8zXipL0i7adILdwg.PNG/Week-2-180-min.png",
|
||||
"title": "푸른 정찰대 안장",
|
||||
"rewardType": "IN_APP",
|
||||
"conditionType": "TIME_BASED",
|
||||
"conditionForMinutes": 180,
|
||||
"iosBasedReward": false,
|
||||
"codeRemainingCount": 0
|
||||
}
|
||||
],
|
||||
"hasIosBasedReward": false,
|
||||
"dropsCampaignNotStarted": false,
|
||||
"rewardType": "IN_APP",
|
||||
"accountLinkUrl": "https://event.pearlabyss.com/CrimsonDesert/Drops"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue