[DRF] 인코딩된 이미지를 json으로 텍스트와 함께 받아와서 디코딩한 후 디비에 저장하기

김재연·2022년 3월 30일
0

결과

사전세팅

product를 생성하기 위한 다른 항목들을 추가해주고


요청(데이터) 보내기

주임님한테 받은 예시 데이터로 교체하고, 이미지 인코딩값은 여기서 용량이 작은 이미지를 base64로 인코딩해서 나온 결과 문자열을 복붙해서 요청을 보내면

url : 127.0.0.1:8000/products/car/decode

method : POST

json content :

{
    "brand": 1,
    "model": 1,
    "user": 1,
    "vehicle_group": 1,
    "used_yn": 1,
    "vehicle_age": 2019,
    "vin": "KMJWA37KBEU564147",
    "country": "KR",
    "size_l": 4410,
    "size_w": 1820,
    "size_h": 1685,
    "cost": 4860,
    "discount_amnt":300,
    "incoterms":"Inchoen",
    "comment":"상품에 대한 추가 코멘트",
    "carDetails":
    {
        "modeldetails": 1,
        "vehicle_type":"SUV",
        "grade":"1",
        "dsplc_amnt":3000,
        "passn_num":4,
        "handle_loc":"LHD",
        "transm_type":"오토",
        "color":"BR",
        "fuel_type":"가솔린",
        "drive_stm":"2륜 구동",
        "doors_num":3,
        "re_vin":false,
        "flood":false,
        "taxi":false,
        "police_car":false,
        "rental_car":false
    },
    "carStatus":
    {
        "corrossn": 50,
        "hole": true,
        "break_off": false,
        "brake": false,
        "engine": false,
        "transm": true,
        "aircnd": false,
        "heater": true,
        "audio": true,
        "window": false,
        "wheels_four": 3
    },
    "carOptsSafe":
    {  "drv_aribag":true,
        "passen_airbag":true,
        "front_sensor":true,
        "park_sensor":true,
        "rear_camera":true,
        "srnd_view":true,
        "abs":true,
        "esp":true,
        "ldws":true,
        "spare_tire":true
        },
    "carOptsInOut":{
        "heat_seat":false
    },
    "images":[
        "iVBORw0KGgoAAAANSUhEUgAAADkAAAAxCAIAAADvKZa/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxj
      wv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACWSURBVGhD7dBBCoAwDETR3v/SNZjiRkHR+ZHSeSt
      RST5pfR5uZbiV4VaGWxluZaza2q6Mbwri1vHEkE2XX/FMM52uTIIdBRdNX3fUVCa3MlZqDZFbUyzbU
      VAsnp7Fh/FWBL6EtJhtDW5luJXhVsY0rRH6c+vz9cLQ8HLWfq97428R8TiUWxluZbiV4VaGWxluZcz
      T2vsGgxkAwhVD4rIAAAAASUVORK5CYII=",
        "iVBORw0KGgoAAAANSUhEUgAAADkAAAAxCAIAAADvKZa/AAAAAXNSR0IArs4c6QAAAARnQU1BAACxj
      wv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADASURBVGhD7dLbDoMgEEVR//+nKcXRNLXGYZhdQ3v
      2g8ELsEJcyjzJyiQrk6xMsjL9k3XZsnuyBOt+pRvaox2orfAF7qjVRrK+Fd/giKO5wdU/siazotxM6
      xrHlZUpsu6lBuIi1hrB7V6xIpxWz2ddRaw2uupmazus0ynr29fsRVIR61n20dbxyWDdVhs5av5MLmi
      tTWZN5OLnJKsjWb39qDW2a52VxcWticnK5N3++dNNdK63JyuTrEyyMslKVMoDCIKtBy/oN+4AAAAAS
      UVORK5CYII="
    ]
}


확인하기

이렇게 정보가 등록이 된다.

주목해야할 점은 이 data를 form으로 보내지 않고 json content 로 보냈다는 점과 텍스트와 마찬가지로 이미지를 문자열형태로 바꿔서 json으로 텍스트랑 같이 보냈다는 점이다. 그리고 이미지는 딕셔너리가 아니라 리스트로 보낸다. 저렇게 인코딩돼서 전달한 값을 디코딩해서 filename1.pngfilename2.png라는 (임시이름) 이미지파일로 다시 바꾸었는데, 해당 링크로 들어가보면 아래와 같이 나온다.

둘다 제대로 올라갔다.. 근데 큰 파일은 인코딩된 문자열부터가 양이 개많아서 서버 오류가 난다. ㅈㄴ어쩌라고...

디비에는 잘 저장됐나 싶어서 pgAdmin에서 확인해보니까

잘 저장됐다.


코드 분석 & 알게된 것

views.py

차근차근 기억나는대로 끄적이자면 url은 할말이 없으니까 건너뛰고 views.py로 간다.

프로젝트 구조 분석하던 중에 가장 애먹었던게 'data를 어떻게 보낼까' 였다. 우선 위에 코드에 있는data=request.data 를 보자. 이거는 json 데이터를 request에 담아서 보내는 형식이기 때문에 thunderclient에서 json content에 바로 데이터를 보낸다.

근데 만약 데이터를 받는 코드가 data = request.POST['data] 이렇게 써져있다면? 이거는 form으로 data라는 필드에 내용을 채워 보낸 것을 받는거다.

그리고 serializer로 데이터를 보낼 때 context'images':data.get("images") 라는 부분을 붙여서 보냈는데, 이거는 serializer에서 images가 자꾸 validated_data에 포함이 안돼서 그랬다. 분명 json으로도 보냈고 이 view에서는 보이는데 serializer로 넘어가면 자꾸 images만 빠져서 넘어갔다. 그래서 아예 그냥 파싱을 해서 명시해서 보냈다.


models.py

그럼 이번에는 models.py. 처음에는 이미지 모델에 base64로 인코딩한 값을 저장했다가 인코딩값을 디비에 저장하면 안된다고 해서 필드에 포함이 안되면서 넘어가게 하느라 진짜 개힘들었다. 아무튼 인코딩값을 저장하는 필드 없이 ImageField 하나만을 가지고 여기에 이미지를 저장해야 했다.


Serializers.py

마지막 serializers.py. 일단 코드를 쭉 훑으면서 어떤 방식으로 코드를 작성했는지 설명하겠다.

우선 첫번째로 views.py 에서 serializer = DecodeSerializer(data=data, context={'request': request, 'images':data.get("images")}) 를 만나면 요청받은 data와 request, 그리고 data에서 images 부분을 파싱해서 DecodeSerializer로 보낸다. 위에서도 서술했듯이 Serializer로 data가 넘어오면 images가 제외된 채로 넘어와서 images를 따로 명시해서 보낸 것이다. (views에서는 data에 images가 포함되는 것이 확실하니까) 아마 image 모델에 따른 시리얼라이저가 없어서 그런가, 하고 추측하고 있다.

그리고 DecodeSerializer가 호출되면 자동으로 create 메소드가 실행이 된다. 텍스트를 저장하는 부분은 기존 코드에서 따왔다. 대충 설명하자면 carDetails, carStatus, carOptsSafe, carOptsInOut은 각각의 시리얼라이저에 의해 어떤 모델에 저장될지 정해지고, 어떤 필드에 정보를 저장할 것인지 결정된다. 그리고 validated_data에서 키값을 이용해 값들을 파싱한 후에, product를 foreign key로 잡아서 객체들을 생성한다.

예시로 CarStatusSerializer는 이렇게 생겼다.

class CarStatusSerializer(serializers.ModelSerializer) :
    class Meta:
        model = CarStatus # CarStatus 모델 사용
        fields = '__all__' # 모든 필드를 채울 것
        read_only_fields = ['product'] # product는 읽기만 하는 필드

serializer로 json 데이터를 직렬화해서.. 저장.. 뭐 그런 느낌인가? 필드를 하나하나 명시를 안했는데도 알아서 저장되는거 보니까 그른갑다.

사실 이미지도 이렇게 시리얼라이저로 저장하려고 정말 노력했는데.. 못했다. 야매로 한 방법을 써보자면 self.context.get("images") 로 view에서 보내준 이미지 인코딩값 문자열을 받아왔고, 간단하게 base64를 임포트시켜서 디코딩한 값을 새로운 배열 decoded에 저장했다. 그리고 그 배열을 돌면서 파일이름을 고유하게 정하고(지금은 일단 정신없어서 filename@.png) 그 파일 이름을 활용해 media_root 를 만든다. media_root는 생성할 이미지 파일의 경로를 최종적으로 가리키게 된다. 그럼 "media_root를 연다 = filename@.png 라는 파일을 열겠다, 없으면 생성하겠다!" 가 되겠지? 그럼 그 파일을 열어서 아까 디코딩한 값을 고대로 써주면 된다. 그리고 ProductImg 객체를 만들어서 image 필드에 그 경로를 써주고, foreign key로 앞서 받아온 product를 넣어주면 끝. 마지막으로 그렇게 생성된 전체 정보를 담은 객체인 product를 반환해준다. 진짜 마지막으로 맨 밑에 있는 class Meta는 이 시리얼라이저가 Product 라는 모델을 기본으로 사용한다는 뜻인거 같다. 아 생각해보니 이음 개발할 때도 시리얼라이저 밑에 사용할 모델을 썼던거 같다.

디코딩값을 이미지파일로 만드는거는 여기 도움을 많이 받았다.

원하는대로 되긴 했는데 response json이랑 디비에 저장되는거랑 자꾸 다르게 나와서 원리 이해하느라 진짜 힘들었다. 결국 response json에 image는 포함이 안됐지만 일단 디비에는 저장이 됐다. 아마 모델에 존재하지 않는 필드(이미지X, 이미지 인코딩값O)로 넘어와서 모델에 저장하려다 보니 많이 꼬인 것 같다. response json에 images가 뜨게 하려고 별 지랄을 다하다가 여기를 보고 따라하려고 해봤지만 결국 안됐다. 그래도 배운건 있으니까 정리를 하자면,

모델에 없는 필드인데 json으로 보내고 싶으면 serializers.SerializerMethodField() 를 쓰라는거였다. 이걸 쓰려면 get_필드이름 메소드를 꼭 만들어줘야했는데, 이름도 잘 맞춰줘야 했었고 모델도 이거 썼다 저거 썼다 진짜 개복잡했다. 심지어 images도 리스트로 받아왔어야 해서 딕셔너리로 달라고 별 지랄도 아니었다. 심지어 get_필드이름 얘는 반환하는 형태도 지 원하는대로 해줘야돼 진짜 상지랄 그러다가 어차피 json response보단 디비 저장이 우선이겠지 싶어서 버리고 텍스트만 저장하는 코드 기반으로 차근차근 했다.


전체코드

# urls.py
# 차 텍스트 + 이미지 같이 등록하는 새 url을 파서 작업했다.
path('products/car/decode', views.DecodeTest.as_view())
# views.py
# /products/car/decode
@permission_classes([AllowAny]) #모든 사용자 접근가능
class DecodeTest(APIView):

    @swagger_auto_schema(
        operation_summary="차 그룹의 상품 정보(이미지 포함) 등록",
        tags=["상품"],
        operation_description="차 그룹의 상품 정보(이미지 포함) 등록",
        request_body = DecodeSerializer
    )
      
    def post(self, request) :

        data = request.data
        #serializer 직렬화
        serializer = DecodeSerializer(data=data, context={'request': request, 'images':data.get("images")})

        # 데이터 유효성 검사
        if serializer.is_valid():
            # DB에 저장
            serializer.save()
            return Response(get_successCode(serializer.data), status=status.HTTP_201_CREATED)

        return Response(get_failCode(serializer.errors), status=status.HTTP_400_BAD_REQUEST)
# models.py
class ProductImg(TimeStampedModel):

    id = models.BigAutoField(primary_key=True)
    product = models.ForeignKey(Product,on_delete=models.CASCADE, related_name='images', help_text="상품 idx")
    image = models.ImageField(upload_to="", blank=True, help_text="이미지 파일 경로")

    def __str__(self):
        res = str(self.id) + " & " + str(self.image) + " / product : " + str(self.product) 
        return res
# serializers.py
class DecodeSerializer(serializers.ModelSerializer) :

    # 자식 테이블의 경우 serializer를 받는 변수가 related_name과 일치해야함
    # create할때는 read_only 쓰면 x
    carDetails = CarDetailSerializer()
    carStatus = CarStatusSerializer()
    carOptsSafe = CarOptsSafeSerializer()
    carOptsInOut = CarOptsInOutSerializer()

    def create(self, validated_data):
        # 텍스트 저장
        detail_data = validated_data.pop("carDetails")
        status_data = validated_data.pop("carStatus")
        optsSafe_data = validated_data.pop("carOptsSafe")
        optsInOut_data = validated_data.pop("carOptsInOut")

        product = Product.objects.create(**validated_data)
        # product fkey 참조해서
        CarDetails.objects.create(product=product, **detail_data)
        CarStatus.objects.create(product=product, **status_data)
        CarOptsSafe.objects.create(product=product, **optsSafe_data)
        CarOptsInOut.objects.create(product=product, **optsInOut_data)

        image_strings = self.context.get("images")
        decoded = []
        for image_string in image_strings:
            decoded.append(base64.b64decode(image_string))

        num = 1
        for img in decoded:
            filename = f'{img}'
            media_root = settings.MEDIA_ROOT + '\\' + "filename"+str(num)+".png"
            with open(media_root, 'wb') as f:
                f.write(img)
                ProductImg.objects.create(
                    image = f'/filename{num}.png',
                    product=product
                )
            num += 1

        return product

    class Meta:
        model = Product
        fields = '__all__'
        # exclude = ['model_id', 'brand_id'] #특정 칼럼만 제외
profile
일기장같은 공부기록📝

0개의 댓글