Merge pull request #100 from plers-org/sm/#99

Sm/#99
This commit is contained in:
NKEY
2025-12-29 21:32:52 +09:00
committed by GitHub
5 changed files with 247 additions and 104 deletions

View File

@@ -1,29 +1,85 @@
from django.db import models
# models.py
from django.utils import timezone
import mongoengine as me
class Element(me.EmbeddedDocument):
element_id = me.IntField()
element_type = me.StringField()
content = me.StringField()
css = me.DictField()
class NodeType(me.EmbeddedDocument):
"""
"type": { "resolvedName": "CraftFrame" } 형태
"""
resolvedName = me.StringField(required=True)
class CraftNode(me.EmbeddedDocument):
"""
CraftJS node 1개(값) 스키마
"""
type = me.EmbeddedDocumentField(NodeType, required=True)
isCanvas = me.BooleanField(default=False)
# props/custom/linkedNodes는 구조가 유동적이므로 dict로 저장
props = me.DictField(default=dict)
displayName = me.StringField()
custom = me.DictField(default=dict)
hidden = me.BooleanField(default=False)
# 자식 노드 id 리스트
nodes = me.ListField(me.StringField(), default=list)
# linkedNodes: { "ROOT-background": "mBIaZ3L5VU" } 같은 맵
linkedNodes = me.DictField(default=dict)
# 일부 노드에 존재 ("parent": "ROOT")
parent = me.StringField()
class Page(me.EmbeddedDocument):
cut = me.IntField()
elements = me.ListField(me.EmbeddedDocumentField(Element))
"""
pages[*] 구조:
{
"page_id": 1,
"title": "...",
"data": { "ROOT": {...}, "mBIaZ3L5VU": {...}, ... }
}
"""
page_id = me.IntField(required=True)
title = me.StringField(required=True)
# data는 nodeId -> CraftNode 맵
data = me.MapField(me.EmbeddedDocumentField(CraftNode), default=dict)
class Code(me.Document):
pages = me.ListField(me.EmbeddedDocumentField(Page))
"""
최상위 구조:
{
"keyword": [...],
"description": "...",
"pages": [...]
}
+ (기존에 쓰던 object_type/object_id 유지)
"""
pages = me.EmbeddedDocumentListField(Page, default=list)
keyword = me.ListField(me.StringField(max_length=50), default=list)
description = me.StringField(default='', blank=True)
object_type = me.StringField(choices=("portfolio", "project"))
object_id = me.StringField()
description = me.StringField(default="")
object_type = me.StringField(choices=("portfolio", "project"), required=True)
object_id = me.StringField(required=True)
created_at = me.DateTimeField(default=timezone.now)
updated_at = me.DateTimeField(default=timezone.now)
meta = {
"collection": "codes",
"indexes": [
("object_type", "object_id"),
"created_at",
"updated_at",
],
}
def save(self, *args, **kwargs):
self.updated_at = timezone.now()
return super().save(*args, **kwargs)

View File

@@ -1,30 +1,67 @@
from rest_framework import serializers
from .models import Code, Page, Element
# from datetime import datetime, timezone
from django.utils import timezone
# from zoneinfo import ZoneInfo
# KST = ZoneInfo("Asia/Seoul")
from .models import Code, Page, CraftNode, NodeType # (네 models.py에 맞춰 import)
class ElementSerializer(serializers.Serializer):
element_id = serializers.CharField(required=False)
element_type = serializers.CharField()
content = serializers.CharField(allow_blank=True)
css = serializers.DictField()
# -------------------------
# Node (CraftJS)
# -------------------------
class NodeTypeSerializer(serializers.Serializer):
resolvedName = serializers.CharField()
class CraftNodeSerializer(serializers.Serializer):
type = NodeTypeSerializer()
isCanvas = serializers.BooleanField(required=False, default=False)
props = serializers.DictField(required=False, default=dict)
displayName = serializers.CharField(required=False, allow_blank=True, allow_null=True)
custom = serializers.DictField(required=False, default=dict)
hidden = serializers.BooleanField(required=False, default=False)
nodes = serializers.ListField(child=serializers.CharField(), required=False, default=list)
linkedNodes = serializers.DictField(required=False, default=dict)
parent = serializers.CharField(required=False, allow_null=True, allow_blank=True)
# -------------------------
# Page
# -------------------------
class PageSerializer(serializers.Serializer):
cut = serializers.IntegerField()
elements = ElementSerializer(many=True)
page_id = serializers.IntegerField()
title = serializers.CharField()
# data: { "ROOT": {...}, "mBIaZ3L5VU": {...}, ... }
# -> key는 문자열(node id), value는 CraftNode
data = serializers.DictField(
child=CraftNodeSerializer(),
required=False,
default=dict,
)
# -------------------------
# Code
# -------------------------
class CodeSerializer(serializers.Serializer):
id = serializers.SerializerMethodField()
pages = PageSerializer(many=True, required=False)
keyword = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True)
description = serializers.CharField(allow_blank=True, required=False)
created_at = serializers.SerializerMethodField()
updated_at = serializers.SerializerMethodField()
keyword = serializers.ListField(
child=serializers.CharField(max_length=50),
required=False,
allow_empty=True,
default=list,
)
description = serializers.CharField(required=False, allow_blank=True, default="")
pages = PageSerializer(many=True, required=False, default=list)
created_at = serializers.SerializerMethodField()
updated_at = serializers.SerializerMethodField()
def get_id(self, obj):
return str(obj.id)
def get_created_at(self, obj):
return timezone.localtime(obj.created_at).isoformat(timespec="seconds")
@@ -32,46 +69,77 @@ class CodeSerializer(serializers.Serializer):
def get_updated_at(self, obj):
return timezone.localtime(obj.updated_at).isoformat(timespec="seconds")
def get_id(self, obj):
return str(obj.id)
# -------------------------
# helpers: dict -> EmbeddedDocument
# -------------------------
def _to_node_type(self, data: dict) -> NodeType:
return NodeType(resolvedName=data.get("resolvedName", ""))
def _to_craft_node(self, data: dict) -> CraftNode:
# type 필수
node_type = self._to_node_type(data["type"])
return CraftNode(
type=node_type,
isCanvas=data.get("isCanvas", False),
props=data.get("props", {}) or {},
displayName=data.get("displayName"),
custom=data.get("custom", {}) or {},
hidden=data.get("hidden", False),
nodes=data.get("nodes", []) or [],
linkedNodes=data.get("linkedNodes", {}) or {},
parent=data.get("parent"),
)
def _to_page(self, page: dict) -> Page:
raw_map = page.get("data", {}) or {}
node_map = {node_id: self._to_craft_node(node_dict) for node_id, node_dict in raw_map.items()}
return Page(
page_id=page["page_id"],
title=page["title"],
data=node_map,
)
# -------------------------
# create / update
# -------------------------
def create(self, validated_data):
request = self.context['request']
validated_data['object_type'] = request.query_params.get('type')
validated_data['object_id'] = request.query_params.get('id')
pages_data = validated_data.pop('pages')
pages = [
Page(
cut=page['cut'],
elements=[Element(**el) for el in page['elements']]
) for page in pages_data
]
request = self.context["request"]
validated_data["object_type"] = request.query_params.get("type")
validated_data["object_id"] = request.query_params.get("id")
pages_data = validated_data.pop("pages", [])
pages = [self._to_page(p) for p in pages_data]
code = Code(pages=pages, **validated_data)
code.save()
return code
def update(self, instance, validated_data):
update_pages_data = validated_data.get('pages', [])
existing = {p.cut: p for p in instance.pages}
for page in update_pages_data:
cut = page.get('cut')
if cut and cut in existing:
page_obj = existing[cut]
page_obj.elements = [Element(**el) for el in page["elements"]]
else:
instance.pages.append(
Page(
cut=cut,
elements=[Element(**el) for el in page["elements"]]
)
)
# pages는 page_id 기준으로 upsert
update_pages_data = validated_data.get("pages", [])
existing = {p.page_id: p for p in instance.pages}
if 'keyword' in validated_data:
instance.keyword = validated_data['keyword']
if 'description' in validated_data:
instance.description = validated_data['description']
for page in update_pages_data:
pid = page.get("page_id")
if pid is None:
continue
new_page_obj = self._to_page(page)
if pid in existing:
page_obj = existing[pid]
page_obj.title = new_page_obj.title
page_obj.data = new_page_obj.data
else:
instance.pages.append(new_page_obj)
if "keyword" in validated_data:
instance.keyword = validated_data["keyword"]
if "description" in validated_data:
instance.description = validated_data["description"]
instance.save()
return instance

View File

@@ -5,7 +5,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from django.db import transaction
from .models import Code, Page, Element
from .models import Code
from .serializers import CodeSerializer
from .permissions import IsOwnerOrMemberInCreateAndUpdateAndDelete, IsNotPublished
from .services import NocodetoolObjectMapService, NocodetoolHitService
@@ -24,25 +24,25 @@ class NoCodeToolAPIView(APIView):
def get(self, request):
related_type = request.query_params.get("type")
related_id = request.query_params.get("id")
code_id = None
if obj := NocodetoolObjectMapService.mapping_model_instance(related_type, related_id):
if obj.is_published:
NocodetoolHitService.hit_once(obj, request)
code_id = ObjectId(obj.code_id)
if not code_id:
obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id)
if not obj or not obj.code_id:
return Response({"message": "Not validated type or no object"}, status=status.HTTP_400_BAD_REQUEST)
if obj.is_published:
NocodetoolHitService.hit_once(obj, request)
code = Code.objects.filter(id=ObjectId(obj.code_id)).first()
if not code:
return Response({"message": "No code object"}, status=status.HTTP_400_BAD_REQUEST)
obj_serializer = NocodetoolObjectMapService.mapping_model_serializer(related_type)
code = Code.objects.filter(id=code_id).first()
if not code:
return Response({"message": "No code object"}, status=status.HTTP_400_BAD_REQUEST)
return Response({
"obj_info": obj_serializer(obj).data,
"codes": CodeSerializer(code).data
}, status=status.HTTP_200_OK)
"obj_info": obj_serializer(obj).data,
"codes": CodeSerializer(code).data
}, status=status.HTTP_200_OK)
@transaction.atomic
def post(self, request):
related_type = request.query_params.get("type")
@@ -50,32 +50,33 @@ class NoCodeToolAPIView(APIView):
obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id)
if not obj:
return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST)
if not obj.now_worker:
return Response({"message": "start edit first"}, status=status.HTTP_400_BAD_REQUEST)
if obj.now_worker != request.user.nickname:
return Response({"message": f"{obj.now_worker} is working now"}, status=status.HTTP_400_BAD_REQUEST)
data = request.data.copy()
thumbnail_file = data.pop("thumbnail", None)
obj_serializer = NocodetoolObjectMapService.mapping_model_serializer(related_type)
serializer = CodeSerializer(data=request.data, context={'request': request})
serializer = CodeSerializer(data=data, context={'request': request})
if serializer.is_valid():
code = serializer.save()
obj.code_id = str(code.id)
obj.save()
obj.save(update_fields=["code_id"])
if thumbnail_file:
obj.thumbnail = thumbnail_file
obj.save(update_fields=["thumbnail"])
return Response({
"obj_info": obj_serializer(obj).data,
"codes": CodeSerializer(code).data
}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@transaction.atomic
@@ -85,21 +86,25 @@ class NoCodeToolAPIView(APIView):
obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id)
if not obj:
return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST)
if not obj.now_worker:
return Response({"message": "start edit first"}, status=status.HTTP_400_BAD_REQUEST)
if obj.now_worker != request.user.nickname:
return Response({"message": f"{obj.now_worker} is working now"}, status=status.HTTP_400_BAD_REQUEST)
if not obj.code_id:
return Response({'message': 'No code object'}, status=status.HTTP_400_BAD_REQUEST)
code = Code.objects.filter(id=ObjectId(obj.code_id)).first()
if not code:
return Response({'message': 'No code object'}, status=status.HTTP_400_BAD_REQUEST)
data = request.data.copy()
thumbnail_file = data.pop("thumbnail", None)
obj_serializer = NocodetoolObjectMapService.mapping_model_serializer(related_type)
serializer = CodeSerializer(code, data=data, partial=True, context={'request': request})
if serializer.is_valid():
updated_code = serializer.save()
@@ -112,8 +117,9 @@ class NoCodeToolAPIView(APIView):
"obj_info": obj_serializer(obj).data,
"codes": CodeSerializer(updated_code).data
}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@transaction.atomic
def delete(self, request):
related_type = request.query_params.get("type")
@@ -121,20 +127,25 @@ class NoCodeToolAPIView(APIView):
obj = NocodetoolObjectMapService.mapping_model_instance(related_type, related_id)
if not obj:
return Response({"message": "No object"}, status=status.HTTP_400_BAD_REQUEST)
if not obj.now_worker:
return Response({"message": "start edit first"}, status=status.HTTP_400_BAD_REQUEST)
if obj.now_worker != request.user.nickname:
return Response({"message": f"{obj.now_worker} is working now"}, status=status.HTTP_400_BAD_REQUEST)
if not obj.code_id:
return Response({'message': 'No code object'}, status=status.HTTP_400_BAD_REQUEST)
code = Code.objects.filter(id=ObjectId(obj.code_id)).first()
if not code:
return Response({'message': 'No code object'}, status=status.HTTP_400_BAD_REQUEST)
code.delete()
obj.code_id = None
obj.thumbnail = None
obj.save()
obj.save(update_fields=["code_id", "thumbnail"])
return Response({"message": "delete code success"}, status=status.HTTP_200_OK)
class NocodeToolWorkingAPIView(APIView):

View File

@@ -54,12 +54,15 @@ class PortfolioSetRepresentAPIView(APIView):
portfolio = get_object_or_404(Portfolio, id=pk)
if user != portfolio.owner:
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
if before_represent := Portfolio.objects.filter(owner=user, is_represent=True).first():
before_represent.is_represent = False
before_represent.save()
portfolio.is_represent = True
is_representing = request.data.get("representing", False)
if is_representing:
if before_represent := Portfolio.objects.filter(owner=user, is_represent=True).first():
before_represent.is_represent = False
before_represent.save()
portfolio.is_represent = is_representing
portfolio.save()
return Response({"message": "change represent success"}, status=status.HTTP_200_OK)
return Response({"is_represented": is_representing}, status=status.HTTP_200_OK)
class PortfolioSetPublishAPIView(APIView):
@@ -69,9 +72,10 @@ class PortfolioSetPublishAPIView(APIView):
portfolio = get_object_or_404(Portfolio, id=pk)
if user != portfolio.owner:
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
portfolio.is_published = True
is_publishing = request.data.get("publishing", False)
portfolio.is_published = is_publishing
portfolio.save()
return Response({"message": "publish success"}, status=status.HTTP_200_OK)
return Response({"is_published": is_publishing}, status=status.HTTP_200_OK)
class PortfolioChangeState(APIView):
@transaction.atomic

View File

@@ -89,12 +89,15 @@ class ProjectSetRepresentAPIView(APIView):
project = get_object_or_404(Project, id=pk)
if user != project.owner:
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
if before_represent := Project.objects.filter(owner=user, is_represent=True).first():
before_represent.is_represent = False
before_represent.save()
project.is_represent = True
is_representing = request.data.get("representing", False)
if is_representing:
if before_represent := Project.objects.filter(owner=user, is_represent=True).first():
before_represent.is_represent = False
before_represent.save()
project.is_represent = is_representing
project.save()
return Response({"message": "change represent success"}, status=status.HTTP_200_OK)
return Response({"is_represented": is_representing}, status=status.HTTP_200_OK)
class ProjectSetPublishAPIView(APIView):
@@ -104,9 +107,10 @@ class ProjectSetPublishAPIView(APIView):
project = get_object_or_404(Project, id=pk)
if user != project.owner:
return Response({"message": "Not owner"}, status=status.HTTP_403_FORBIDDEN)
project.is_published = True
is_publishing = request.data.get("publishing", False)
project.is_published = is_publishing
project.save()
return Response({"message": "publish success"}, status=status.HTTP_200_OK)
return Response({"is_published": is_publishing}, status=status.HTTP_200_OK)
class ProjectChangeState(APIView):
@transaction.atomic