From 196498d87960f188678d1abc8143bf924d5a0adf Mon Sep 17 00:00:00 2001 From: sm4640 Date: Mon, 29 Dec 2025 17:46:26 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix:=20[#99]=20=EB=85=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=ED=88=B4=20=EB=AA=A8=EB=8D=B8=20=EB=8C=80?= =?UTF-8?q?=EA=B7=9C=EB=AA=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nocodetools/models.py | 84 +++++++++++++++---- nocodetools/serializers.py | 168 ++++++++++++++++++++++++++----------- nocodetools/views.py | 63 ++++++++------ 3 files changed, 225 insertions(+), 90 deletions(-) diff --git a/nocodetools/models.py b/nocodetools/models.py index ce44b8e..bb747d4 100644 --- a/nocodetools/models.py +++ b/nocodetools/models.py @@ -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) - \ No newline at end of file diff --git a/nocodetools/serializers.py b/nocodetools/serializers.py index 613a718..18deecc 100644 --- a/nocodetools/serializers.py +++ b/nocodetools/serializers.py @@ -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 diff --git a/nocodetools/views.py b/nocodetools/views.py index 9f38de4..092f48a 100644 --- a/nocodetools/views.py +++ b/nocodetools/views.py @@ -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):