[Aight] AITool의 Categories 저장 시 순서를 위배하여 응답함으로써 대표카테고리별 뱃지 및 색상을 적용하기 힘든 문제

horiz.d·2023년 5월 5일
0

PJ: Aight

목록 보기
2/17

create_aiTool을 통해 aiTool을 생성요청할 때, categories에 [id1, id2, id3] 같은 모습의 value를 넣어서 저장하고 있었다.

하지만 실제 저장될 땐, ManyToMany 테이블의 관계로 저장되어 배열에 넣어준 순서가 유지된 채 저장되지 않았다.

아래는 기존 aiTool과 aiToolCategory 사이 N-M관계를 매핑하는 중간테이블이다. 보이다시피, 따로 카테고리 입력 순서가 정의돼있지 않다.


[N-M 매핑 테이블]

클라이언트의 메인페이지 코어섹션에서 대표 카테고리 하나씩을 식별하여 카테고리 색상, 뱃지를 스타일로 적용해주도록 설계했기 때문에, 대표 카테고리를 식별하기 위해 배열의 맨 앞에 들어갈 id와 카테고리 순서를 신경써서 넣어줘야 하며 이를 유지하여 저장해야할 필요가 있다.

아래는 한계를 맞이한 기존의 장고 core 서브앱의 핵심 코드임.

#models.py
from django.db import models
# from accounts.models import User


class AiToolCategory(models.Model):
    id = models.AutoField(primary_key=True)
    name_set =  models.JSONField(default=dict)  # name_set: {ko: string[], en: string[] }
    
    class Meta:
        managed = True #db에 테이블을 추가 및 삭제한다.
        db_table = 'core_aitool_category' 
    
class AiTool(models.Model):
    id = models.AutoField(primary_key=True)
    imgUrl = models.CharField(max_length=255)
    
    name_set = models.JSONField(default=dict) # name_set: {ko: string[], en: string[]}
    summary = models.TextField(default='Unknown')
    redirectUrl = models.CharField(max_length=255, default='https://cmd8.vercel.app/')
    categories = models.ManyToManyField(AiToolCategory, related_name='ai_tools') #N-M관계, 역참조이름 설정
    
    class Meta:
        managed = True #db에 테이블을 추가 및 삭제한다.
        db_table = 'core_aitool' 
# serializers.py
from rest_framework import serializers
from .models import AiTool, AiToolCategory

### Abstracts
class AbstractBinNameSerializer(serializers.Serializer):
    ko = serializers.ListField(child=serializers.CharField())
    en = serializers.ListField(child=serializers.CharField())

    class Meta:
        abstract = True


### Serializers
#
class ToolNameSerializer(AbstractBinNameSerializer):
    pass

class CategoryNameSerializer(AbstractBinNameSerializer):
    pass

class AiToolCategorySerializer(serializers.ModelSerializer):
    name_set = CategoryNameSerializer()
    class Meta:
        model = AiToolCategory
        fields = ('id', 'name_set')

class AiToolSerializer(serializers.ModelSerializer):
    name_set = ToolNameSerializer()
    categories = serializers.PrimaryKeyRelatedField(queryset=AiToolCategory.objects.all(), many=True)

    class Meta:
        model = AiTool
        fields = ('id', 'imgUrl', 'name_set', 'summary', 'redirectUrl', 'categories')
# views.py
@api_view(['POST'])
@csrf_exempt
def create_aiTool(request):
    try:
        data = json.loads(request.POST['data'])
        image = request.FILES['image']
        image_extension = os.path.splitext(image.name)[1]
        image_name = data['name_set']['en'][0] + image_extension

        # 이미지를 S3에 업로드하고 이미지 URL 가져오기
        default_storage.save(f'ai_tools/{image_name}', image)
        image_url = default_storage.url(f'ai_tools/{image_name}')

        # 이미지 URL을 요청 데이터에 추가하기
        data['imgUrl'] = image_url

        serializer = AiToolSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
        return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    except Exception as e:
       return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

@api_view(['GET'])
@csrf_exempt
def get_aiTool(request, pk):
    try:
        ai_tool = AiTool.objects.prefetch_related('categories').get(pk=pk)
        serializer = AiToolSerializer(ai_tool)
        return JsonResponse(serializer.data, status=status.HTTP_200_OK)
    except AiTool.DoesNotExist:
        return HttpResponse(status=status.HTTP_404_NOT_FOUND)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

#### safe=True(=default)였기에 딕셔너리가 아닌 직렬화 오류가 발생했었다.
#### err: In order to allow non-dict objects to be serialized set the safe parameter to False.
#### 해결법2 적용 해결했음 ( django 권장 해결법), 1번 대안은 safe=False 설정이었음
@api_view(['GET'])
@csrf_exempt
def get_all_aiTools(request):
    try:
        ai_tools = AiTool.objects.prefetch_related('categories').all() #prefetch사용해서 성능향상 도모: 어차피 메인페이지 항상 함께씀
        if not ai_tools:
            raise AiTool.DoesNotExist
        serializer = AiToolSerializer(ai_tools, many=True)#직렬화
        serialized_data = serializer.data
        return JsonResponse({'aiTools': serialized_data}, status=status.HTTP_200_OK)
    except AiTool.DoesNotExist:
        return HttpResponse(status=status.HTTP_404_NOT_FOUND)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(['GET'])
@csrf_exempt
def get_all_aiTools_combinedCats(request):
    try:
        ai_tools = AiTool.objects.prefetch_related('categories').all()
        if not ai_tools:
            raise AiTool.DoesNotExist
        serializer = AiToolSerializer(ai_tools, many=True)
        serialized_data = serializer.data
        
        # AiToolCategory를 한번에 가져와 ID를 기준으로 사전에 저장 : 성능개선용 (DB쿼리 수 감소)
        all_categories = AiToolCategory.objects.all()
        category_dict = {category.id: category for category in all_categories}

        # 프론트-메인코어 인터페이스 맞게 가공
        transformed_data = []
        for ai_tool in serialized_data:
            category_ids = ai_tool['categories']
            categories_ko = []
            categories_en = []
            
            for category_id in category_ids:
                category = category_dict[category_id]
                categories_ko.append(category.name_set['ko'][0])
                categories_en.append(category.name_set['en'][0])
            
            transformed_data.append({
                'id': ai_tool['id'],
                'imgUrl': ai_tool['imgUrl'],
                'ko': {
                    'name': ai_tool['name_set']['ko'],
                    'category': categories_ko,
                },
                'en': {
                    'name': ai_tool['name_set']['en'],
                    'category': categories_en,
                },
                'summary': ai_tool['summary'],
                'redirectUrl': ai_tool['redirectUrl'],
                'derived': {
                    'score': {
                        'avg': 4.5, # 여기에 실제 평균 점수를 적용
                        'cnt': 10, # 여기에 실제 평가 수를 적용
                    },
                    'favoriteCnt': 1000, # 여기에 실제 즐겨찾기 수를 적용
                },
            })
        
        return JsonResponse({'aiTools': transformed_data}, status=status.HTTP_200_OK)
    except AiTool.DoesNotExist:
        return HttpResponse(status=status.HTTP_404_NOT_FOUND)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
####################


##### CATEGORY #####

@api_view(['POST'])
@csrf_exempt
def create_aiTool_category(request):
    try:
        data = request.data
        serializer = AiToolCategorySerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=status.HTTP_201_CREATED)
        return JsonResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    
@api_view(['GET'])
@csrf_exempt
def get_aiTool_category(request, pk):
    try:
        ai_tool_category = AiToolCategory.objects.get(pk=pk)
        serializer = AiToolCategorySerializer(ai_tool_category)
        return JsonResponse(serializer.data, status=status.HTTP_200_OK)
    except AiToolCategory.DoesNotExist:
        return HttpResponse(status=status.HTTP_404_NOT_FOUND)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


@api_view(['GET'])
@csrf_exempt
def get_all_aiTool_categories(request):
    try:
        ai_tool_categories = AiToolCategory.objects.all()
        if not ai_tool_categories:
            raise AiToolCategory.DoesNotExist
        serializer = AiToolCategorySerializer(ai_tool_categories, many=True)
        serialized_data = serializer.data
        return JsonResponse({'aiToolCategories': serialized_data}, status=status.HTTP_200_OK)
    except AiToolCategory.DoesNotExist:
        return HttpResponse(status=status.HTTP_404_NOT_FOUND)
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

수정 및 해결

기존에 단순히 ManyToManyField를 사용해 단순한 다대다 매핑을 통해 자동생성했던 매핑테이블을 아래처럼 order를 함께 저장하는 테이중간매핑 테이블로 정의해 매핑해주었다.

그리고 아래는 그 중간테이블의 모습이며

실제로 관련 테이블을 활용한 get Query시, 입력 시 categories id 배열의 순서를 지켜서 응답을 받을 수 있음을 확인했다.

profile
가용한 시간은 한정적이고, 배울건 넘쳐난다.

0개의 댓글