✏️ Fix: [#99] 노코드툴 모델 대규모 수정

This commit is contained in:
sm4640
2025-12-29 17:46:26 +09:00
parent 85e285e737
commit 196498d879
3 changed files with 225 additions and 90 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):