Compare commits
10 Commits
f7f9486237
...
eb54875d35
Author | SHA1 | Date | |
---|---|---|---|
|
eb54875d35 | ||
60be2f9139 | |||
30a7f5cd3e | |||
f5913f2383 | |||
7cc2974dbf | |||
765379b8ab | |||
7a5a61f609 | |||
ef2d7c2340 | |||
48393c8f3c | |||
89c56cc1b6 |
@ -42,6 +42,7 @@ THIRD_PARTY_APPS = [
|
||||
|
||||
LOCAL_APPS = [
|
||||
"core.apps.CoreConfig",
|
||||
"crawl.apps.CrawlConfig",
|
||||
"graph.apps.GraphConfig",
|
||||
"market.apps.MarketConfig",
|
||||
"user.apps.UserConfig",
|
||||
|
@ -6,6 +6,11 @@ DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(" ")
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ALLOWED_ORIGINS").split(" ")
|
||||
|
||||
CSRF_COOKIE_DOMAIN = os.environ.get("CSRF_COOKIE_DOMAIN")
|
||||
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS").split(" ")
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
@ -1,3 +1 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
@ -37,17 +37,14 @@ def search_bunjang(keyword, page=1):
|
||||
result.append(
|
||||
{
|
||||
"title": item["name"],
|
||||
"price": item["price"],
|
||||
"price": int(item["price"]),
|
||||
"year": update_time.year,
|
||||
"month": update_time.month,
|
||||
"day": update_time.day,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
print("--------------------------------------------")
|
||||
print(url)
|
||||
print(data)
|
||||
print("--------------------------------------------")
|
||||
pass
|
||||
finally:
|
||||
return result
|
||||
|
||||
@ -56,14 +53,32 @@ def get_bunjang(keyword):
|
||||
result = []
|
||||
page = 1
|
||||
while True:
|
||||
print(f"page: {page}")
|
||||
print(f"b {keyword} p{page}")
|
||||
page_result = search_bunjang(keyword, page)
|
||||
if not page_result:
|
||||
break
|
||||
result += page_result
|
||||
filtered_result = []
|
||||
for item in page_result:
|
||||
price = int(item["price"])
|
||||
if not (
|
||||
"매입" in item["title"]
|
||||
or "삽니다" in item["title"]
|
||||
or "사요" in item["title"]
|
||||
or "케이스" in item["title"]
|
||||
or price % 10 != 0
|
||||
or price < 100000
|
||||
or price > 2000000
|
||||
):
|
||||
filtered_result.append(item)
|
||||
result += filtered_result
|
||||
page += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
# with open("bunjang.json", "w", encoding="utf-8") as file:
|
||||
# json.dump(result, file, ensure_ascii=False, indent=2)
|
||||
return result
|
||||
sum = 0
|
||||
for item in result:
|
||||
sum += item["price"]
|
||||
|
||||
if len(result) == 0:
|
||||
return 0
|
||||
avg = round(sum // len(result), -3)
|
||||
return avg
|
||||
|
@ -1,6 +1,7 @@
|
||||
import requests
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
# from bs4 import BeautifulSoup
|
||||
|
||||
@ -15,16 +16,15 @@ def get_api_id():
|
||||
js_url = base_url + re.findall(pattern, text)[0]
|
||||
response = requests.get(js_url)
|
||||
text = response.text
|
||||
index = text.find('iO.SENTRY_RELEASE={id:"') + 24
|
||||
id = text[index : index + 20]
|
||||
index = text.find('iO.SENTRY_RELEASE={id:"') + 23
|
||||
index_length = text[index:].find('"')
|
||||
id = text[index : index + index_length]
|
||||
return id
|
||||
|
||||
|
||||
def get_url(api_id, keyword, page=1):
|
||||
base = f"https://web.joongna.com/_next/data/{api_id}/search"
|
||||
return (
|
||||
f"{base}/{keyword}.json?page={page}&sort=RECENT_SORT&keyword={keyword}"
|
||||
)
|
||||
return f"{base}/{keyword}.json?page={page}&sort=RECENT_SORT&keyword={keyword}"
|
||||
|
||||
|
||||
def search_joongna(api_id, keyword, page):
|
||||
@ -36,9 +36,9 @@ def search_joongna(api_id, keyword, page):
|
||||
queries = data["pageProps"]["dehydratedState"]["queries"]
|
||||
if len(queries) == 0:
|
||||
return False
|
||||
items = data["pageProps"]["dehydratedState"]["queries"][0]["state"][
|
||||
items = data["pageProps"]["dehydratedState"]["queries"][0]["state"]["data"][
|
||||
"data"
|
||||
]["data"]["items"]
|
||||
]["items"]
|
||||
item_length = len(items)
|
||||
if item_length == 0:
|
||||
return False
|
||||
@ -73,14 +73,31 @@ def get_joongna(keyword):
|
||||
result = []
|
||||
page = 1
|
||||
while True:
|
||||
print(f"page: {page}")
|
||||
print(f"j {keyword} p{page}")
|
||||
page_result = search_joongna(api_id, keyword, page)
|
||||
if not page_result:
|
||||
break
|
||||
result += page_result
|
||||
filtered_result = []
|
||||
for item in page_result:
|
||||
if not (
|
||||
"매입" in item["title"]
|
||||
or "삽니다" in item["title"]
|
||||
or "사요" in item["title"]
|
||||
or "케이스" in item["title"]
|
||||
or item["price"] % 10 != 0
|
||||
or item["price"] < 100000
|
||||
or item["price"] > 2000000
|
||||
):
|
||||
filtered_result.append(item)
|
||||
result += filtered_result
|
||||
page += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
# with open("joongna.json", "w", encoding="utf-8") as file:
|
||||
# json.dump(result, file, ensure_ascii=False, indent=2)
|
||||
return result
|
||||
sum = 0
|
||||
for item in result:
|
||||
sum += item["price"]
|
||||
|
||||
if len(result) == 0:
|
||||
return 0
|
||||
avg = round(sum // len(result), -3)
|
||||
return avg
|
||||
|
30
crawl/methods.py
Normal file
30
crawl/methods.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.utils import timezone
|
||||
|
||||
from crawl.bunjang import get_bunjang
|
||||
from crawl.joongna import get_joongna
|
||||
from market.models import Product
|
||||
from graph.models import MonthlyTransaction
|
||||
|
||||
|
||||
def crawl():
|
||||
today = timezone.now()
|
||||
month = today.month - 1
|
||||
year = today.year
|
||||
if month == 0:
|
||||
month = 12
|
||||
year -= 1
|
||||
for product in Product.objects.all():
|
||||
name = product.name
|
||||
print(name)
|
||||
bunjang_result = get_bunjang(name)
|
||||
joongna_result = get_joongna(name)
|
||||
print("----------------")
|
||||
print(bunjang_result, joongna_result)
|
||||
print("----------------")
|
||||
avg = (bunjang_result + joongna_result) / 2
|
||||
MonthlyTransaction.objects.create(
|
||||
product=product,
|
||||
year=year,
|
||||
month=month,
|
||||
price=avg,
|
||||
)
|
@ -3,4 +3,10 @@ from django.contrib import admin
|
||||
from .models import Transaction, MonthlyTransaction
|
||||
|
||||
admin.site.register(Transaction)
|
||||
admin.site.register(MonthlyTransaction)
|
||||
|
||||
|
||||
@admin.register(MonthlyTransaction)
|
||||
class MonthlyTransactionAdmin(admin.ModelAdmin):
|
||||
list_display = ("product", "year", "month", "price")
|
||||
list_filter = ("year", "month")
|
||||
search_fields = ("product",)
|
||||
|
@ -1,11 +1,36 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Brand, Product, Post, Image
|
||||
from .models import Brand, Product, ItemIssues, Post, Image
|
||||
|
||||
|
||||
admin.site.register(Brand)
|
||||
admin.site.register(Product)
|
||||
admin.site.register(Image)
|
||||
|
||||
|
||||
@admin.register(ItemIssues)
|
||||
class ItemIssuesAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"display",
|
||||
"frame",
|
||||
"button",
|
||||
"biometric",
|
||||
"camera",
|
||||
"speaker",
|
||||
"others",
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Image)
|
||||
class ImageAdmin(admin.ModelAdmin):
|
||||
list_display = ("post", "image")
|
||||
raw_id_fields = ("post",)
|
||||
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "brand", "release_date")
|
||||
list_filter = ("brand", "release_date")
|
||||
search_fields = ("name",)
|
||||
date_hierarchy = "release_date"
|
||||
|
||||
|
||||
@admin.register(Post)
|
||||
@ -13,12 +38,15 @@ class PostAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"product",
|
||||
"price",
|
||||
"text",
|
||||
"author",
|
||||
"status",
|
||||
"item_issues",
|
||||
"done",
|
||||
"written_at",
|
||||
)
|
||||
list_filter = ("status", "written_at")
|
||||
list_filter = ("done", "written_at")
|
||||
search_fields = ("product", "text")
|
||||
date_hierarchy = "written_at"
|
||||
raw_id_fields = ("author",)
|
||||
raw_id_fields = (
|
||||
"author",
|
||||
"product",
|
||||
"item_issues",
|
||||
)
|
||||
|
@ -13,7 +13,9 @@ class Brand(models.Model):
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
brand = models.ForeignKey(
|
||||
Brand, on_delete=models.CASCADE, related_name="products"
|
||||
Brand,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="products",
|
||||
)
|
||||
release_date = models.DateField(blank=True, null=True)
|
||||
|
||||
@ -24,23 +26,31 @@ class Product(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class ItemIssues(models.Model):
|
||||
display = models.BooleanField(default=False)
|
||||
frame = models.BooleanField(default=False)
|
||||
button = models.BooleanField(default=False)
|
||||
biometric = models.BooleanField(default=False)
|
||||
camera = models.BooleanField(default=False)
|
||||
speaker = models.BooleanField(default=False)
|
||||
others = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
STATUS_CHOICES = (
|
||||
("s", "selling"),
|
||||
("r", "reserved"),
|
||||
("d", "done"),
|
||||
)
|
||||
product = models.ForeignKey(
|
||||
Product, on_delete=models.CASCADE, related_name="posts"
|
||||
Product,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="posts",
|
||||
)
|
||||
price = models.IntegerField()
|
||||
text = models.TextField()
|
||||
author = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, related_name="posts"
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=1, choices=STATUS_CHOICES, default="s"
|
||||
item_issues = models.OneToOneField(
|
||||
ItemIssues, on_delete=models.CASCADE, null=True, related_name="post"
|
||||
)
|
||||
done = models.BooleanField(default=False)
|
||||
written_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
@ -52,7 +62,9 @@ class Post(models.Model):
|
||||
|
||||
class Image(models.Model):
|
||||
post = models.ForeignKey(
|
||||
Post, on_delete=models.CASCADE, related_name="images"
|
||||
Post,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="images",
|
||||
)
|
||||
image = models.ImageField(upload_to="images", blank=True, null=True)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from market.models import (
|
||||
Product,
|
||||
Post,
|
||||
Image,
|
||||
ItemIssues,
|
||||
)
|
||||
|
||||
|
||||
@ -42,10 +43,31 @@ class ImageSerializer(ModelSerializer):
|
||||
fields = ("image",)
|
||||
|
||||
|
||||
class ImageCreateSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = ("post", "image")
|
||||
|
||||
|
||||
class ItemIssuesSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ItemIssues
|
||||
fields = (
|
||||
"display",
|
||||
"frame",
|
||||
"button",
|
||||
"biometric",
|
||||
"camera",
|
||||
"speaker",
|
||||
"others",
|
||||
)
|
||||
|
||||
|
||||
class PostListSerializer(ModelSerializer):
|
||||
product = ProductListSerializer(read_only=True)
|
||||
nickname = serializers.CharField(source="author.nickname")
|
||||
image = serializers.SerializerMethodField()
|
||||
item_issues = ItemIssuesSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
@ -55,7 +77,8 @@ class PostListSerializer(ModelSerializer):
|
||||
"price",
|
||||
"text",
|
||||
"nickname",
|
||||
"status",
|
||||
"item_issues",
|
||||
"done",
|
||||
"written_at",
|
||||
"image",
|
||||
)
|
||||
@ -63,30 +86,56 @@ class PostListSerializer(ModelSerializer):
|
||||
def get_image(self, obj):
|
||||
first_image = obj.images.first()
|
||||
return ImageSerializer(
|
||||
instance=first_image, context=self.context
|
||||
instance=first_image,
|
||||
context=self.context,
|
||||
).data["image"]
|
||||
|
||||
|
||||
class PostCreateSerializer(ModelSerializer):
|
||||
item_issues = ItemIssuesSerializer(write_only=True)
|
||||
# item_issues = serializers.JSONField(write_only=True)
|
||||
|
||||
photos = serializers.ListField(
|
||||
child=serializers.ImageField(),
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
def create(self, validated_data):
|
||||
item_issues_data = validated_data.pop("item_issues")
|
||||
item_issues = ItemIssues.objects.create(**item_issues_data)
|
||||
validated_data["item_issues"] = item_issues
|
||||
photos = validated_data.pop("photos", [])
|
||||
post = super().create(validated_data)
|
||||
for photo in photos:
|
||||
Image.objects.create(post=post, image=photo)
|
||||
return post
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = (
|
||||
"product",
|
||||
"price",
|
||||
"text",
|
||||
"item_issues",
|
||||
"photos",
|
||||
)
|
||||
|
||||
|
||||
class PostStatusUpdateSerializer(ModelSerializer):
|
||||
class PostUpdateSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = ("status",)
|
||||
fields = (
|
||||
"price",
|
||||
"text",
|
||||
"done",
|
||||
)
|
||||
|
||||
|
||||
class PostSerializer(ModelSerializer):
|
||||
product = ProductListSerializer(read_only=True)
|
||||
nickname = serializers.CharField(source="author.nickname")
|
||||
images = ImageSerializer(many=True, read_only=True)
|
||||
item_issues = ItemIssuesSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
@ -96,7 +145,8 @@ class PostSerializer(ModelSerializer):
|
||||
"price",
|
||||
"text",
|
||||
"nickname",
|
||||
"status",
|
||||
"item_issues",
|
||||
"done",
|
||||
"written_at",
|
||||
"images",
|
||||
)
|
||||
|
@ -5,14 +5,14 @@ from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from core.mixins import ActionBasedMixin
|
||||
from core.permissions import IsAuthorOrReadOnly, IsAdminUserOrReadOnly
|
||||
from market.models import Brand, Product, Post
|
||||
from market.models import Brand, Product, Post, ItemIssues, Image
|
||||
from market.serializers import (
|
||||
BrandSerializer,
|
||||
ProductSerializer,
|
||||
PostSerializer,
|
||||
PostCreateSerializer,
|
||||
PostListSerializer,
|
||||
PostStatusUpdateSerializer,
|
||||
PostUpdateSerializer,
|
||||
)
|
||||
from graph.models import Transaction
|
||||
from graph.methods import create_transaction
|
||||
@ -62,7 +62,7 @@ class PostViewset(ActionBasedMixin, ModelViewSet):
|
||||
serializer_class_map = {
|
||||
"list": PostListSerializer,
|
||||
"create": PostCreateSerializer,
|
||||
"update": PostStatusUpdateSerializer,
|
||||
"update": PostUpdateSerializer,
|
||||
}
|
||||
permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]
|
||||
permission_classes_map = {
|
||||
@ -85,19 +85,31 @@ class PostViewset(ActionBasedMixin, ModelViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
# item_issues = ItemIssues.objects.create()
|
||||
# serializer.save(author=self.request.user, item_issues=item_issues)
|
||||
serializer.save(author=self.request.user)
|
||||
# images = self.request.data.getlist("images")
|
||||
# for image in images:
|
||||
# Image.objects.create(post=serializer.instance, image=image)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
done = request.data.get("done")
|
||||
if not object.done and done:
|
||||
create_transaction(object)
|
||||
elif object.done and not done:
|
||||
Transaction.objects.filter(post=object).delete()
|
||||
if object.done != done:
|
||||
object.done = done
|
||||
object.save()
|
||||
return Response(status=200)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
object = self.get_object()
|
||||
object_unsold = object.status == "s" or object.status == "r"
|
||||
instance = serializer.instance
|
||||
if object_unsold and serializer.validated_data.get("status") == "d":
|
||||
done = serializer.validated_data.get("done")
|
||||
if not object.done and done:
|
||||
create_transaction(instance)
|
||||
elif (
|
||||
not object_unsold
|
||||
and serializer.validated_data.get("status") == "s"
|
||||
):
|
||||
Transaction.objects.filter(
|
||||
price=instance.price, product=instance.product
|
||||
).delete()
|
||||
elif object.done and not done:
|
||||
Transaction.objects.filter(post=object).delete()
|
||||
return super().perform_update(serializer)
|
||||
|
@ -5,10 +5,14 @@ Django==4.2
|
||||
django-cors-headers==3.14.0
|
||||
django-debug-toolbar==4.0.0
|
||||
djangorestframework==3.14.0
|
||||
gunicorn==21.2.0
|
||||
mypy-extensions==1.0.0
|
||||
packaging==23.0
|
||||
pathspec==0.11.1
|
||||
Pillow==9.5.0
|
||||
platformdirs==3.2.0
|
||||
psycopg2==2.9.9
|
||||
pytz==2022.7.1
|
||||
setuptools==68.2.2
|
||||
sqlparse==0.4.3
|
||||
wheel==0.41.3
|
||||
|
@ -1,4 +1,7 @@
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
|
||||
@ -77,6 +80,7 @@ class UserViewset(ActionBasedMixin, ModelViewSet):
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=["POST"])
|
||||
@method_decorator(csrf_exempt)
|
||||
def login(self, request):
|
||||
username = request.data["username"]
|
||||
password = request.data["password"]
|
||||
|
Loading…
Reference in New Issue
Block a user