✏️ 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 from django.utils import timezone
import mongoengine as me import mongoengine as me
class Element(me.EmbeddedDocument): class NodeType(me.EmbeddedDocument):
element_id = me.IntField() """
element_type = me.StringField() "type": { "resolvedName": "CraftFrame" } 형태
content = me.StringField() """
css = me.DictField() 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): 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): 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) keyword = me.ListField(me.StringField(max_length=50), default=list)
description = me.StringField(default='', blank=True) description = me.StringField(default="")
object_type = me.StringField(choices=("portfolio", "project"))
object_id = me.StringField() object_type = me.StringField(choices=("portfolio", "project"), required=True)
object_id = me.StringField(required=True)
created_at = me.DateTimeField(default=timezone.now) created_at = me.DateTimeField(default=timezone.now)
updated_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): def save(self, *args, **kwargs):
self.updated_at = timezone.now() self.updated_at = timezone.now()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)

View File

@@ -1,30 +1,67 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Code, Page, Element
# from datetime import datetime, timezone
from django.utils import 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): class PageSerializer(serializers.Serializer):
cut = serializers.IntegerField() page_id = serializers.IntegerField()
elements = ElementSerializer(many=True) 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): class CodeSerializer(serializers.Serializer):
id = serializers.SerializerMethodField() id = serializers.SerializerMethodField()
pages = PageSerializer(many=True, required=False) keyword = serializers.ListField(
keyword = serializers.ListField(child=serializers.CharField(), required=False, allow_empty=True) child=serializers.CharField(max_length=50),
description = serializers.CharField(allow_blank=True, required=False) required=False,
created_at = serializers.SerializerMethodField() allow_empty=True,
updated_at = serializers.SerializerMethodField() 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): def get_created_at(self, obj):
return timezone.localtime(obj.created_at).isoformat(timespec="seconds") return timezone.localtime(obj.created_at).isoformat(timespec="seconds")
@@ -32,46 +69,77 @@ class CodeSerializer(serializers.Serializer):
def get_updated_at(self, obj): def get_updated_at(self, obj):
return timezone.localtime(obj.updated_at).isoformat(timespec="seconds") 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): def create(self, validated_data):
request = self.context['request'] request = self.context["request"]
validated_data['object_type'] = request.query_params.get('type')
validated_data['object_id'] = request.query_params.get('id') validated_data["object_type"] = request.query_params.get("type")
pages_data = validated_data.pop('pages') validated_data["object_id"] = request.query_params.get("id")
pages = [
Page( pages_data = validated_data.pop("pages", [])
cut=page['cut'], pages = [self._to_page(p) for p in pages_data]
elements=[Element(**el) for el in page['elements']]
) for page in pages_data
]
code = Code(pages=pages, **validated_data) code = Code(pages=pages, **validated_data)
code.save() code.save()
return code return code
def update(self, instance, validated_data): def update(self, instance, validated_data):
update_pages_data = validated_data.get('pages', []) # pages는 page_id 기준으로 upsert
existing = {p.cut: p for p in instance.pages} update_pages_data = validated_data.get("pages", [])
existing = {p.page_id: 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"]]
)
)
if 'keyword' in validated_data: for page in update_pages_data:
instance.keyword = validated_data['keyword'] pid = page.get("page_id")
if 'description' in validated_data: if pid is None:
instance.description = validated_data['description'] 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() instance.save()
return instance return instance

View File

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