
메인 유저 시나리오
1. 유저는 이미지를 1개 이상 선택해 업로드한다.
2. 유저는 앱을 통해 이미지로부터 추출된 텍스트 정보를 받는다.
3. 유저는 원하는 ppt 템플릿을 고르고, 텍스트 내용, 위치, 폰트, 크기 등을 수정한 후 ppt 생성 요청을 보낸다.
4. 이후 유저는 원할 때마다 생성된 ppt 자료를 다운로드 받을 수 있다.
자료메이커의 메인 유저 시나리오는 위와 같고, 이 시나리오를 바탕으로 API 서버가 어떤식으로 구현되었는지 정리해 볼 예정이다.
이번 포스팅은 1번, 이미지 업로드에 대한 부분을 정리한다.
서버에서는 이미지 업로드 기능을 다음과 같은 과정으로 처리한다.
이미지 업로드 기능
1. 업로드 request를 보낸 유저의 정보를 얻는다.
2. 업로드 request에 대한 정보를 DB에 저장한다. (Upload)
3. request에 담긴 이미지 파일들을 서버 storage에 저장한다.
4. 서버 storage에 저장되어 있는 이미지 파일들을 S3 storage로 업로드하고, DB에 각 이미지 정보를 저장한다. (Image)
5. 텍스트 추출모델(AI 모델)과 연결하여 이미지로부터 텍스트를 추출하고, 결과를 response한다.
6. 업로드 후 일정 시간 이상 경과된 파일들은 서버에서 일괄 삭제한다.
우선 이미지 업로드에 대한 요청을 처리할 함수를 만들어준다. 이미지 업로드 요청 처리는 "생성"에 해당하는 작업을 수행하므로 POST로 받도록 한다.
처음 시작할 틀은 아래와 같다.
# files/views.py
class ImageView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, ]
def post(self, request):
# todo
return JsonResponse()
이제 위에서 정리한 '이미지 업로드 기능'의 각 task에 대해 detail하게 정리해보도록 하자.
task 1, 2번에 해당한다.
이미지 파일들이 저장될 경로 설정을 위해 user_id와 upload_id가 필요하다.
이 때 user_id가 사용자 아이디 정보이고, upload_id가 업로드 아이디 정보에 해당한다.
참고로 upload_id는 사용자가 이미지 업로드 요청을 보낼 때, 그 한 번의 요청에 대한 정보를 'Upload' 단위로 저장하는데, 이 때 각 Upload에 대해 부여되는 식별자를 의미한다.
예를 들어, 사용자가 여러 장의 이미지 파일들을 담아 이미지 업로드 요청을 했다고 하자. 매번 올라오는 업로드 요청마다 담겨있는 이미지 파일들의 개수가 다르며, DB에서는 이렇게 개수가 변화되는 이미지들을 한번에 동적으로 저장할 수 있는 방법이 없다.
따라서 DB에 이미지 파일들을 저장할 때는, 각 이미지에 대한 정보를 DB에 저장하고, 이 이미지들이 어느 request로부터 업로드된 파일인지를 식별하기 위해 한 번 더 묶어줄 필요가 있다.
이 때, 각 이미지 정보를 저장하는 객체를 Image (DB의 images 테이블과 매핑), 이미지 정보들을 request 단위로 묶어주는 객체를 Upload (DB의 uploads 테이블과 매핑)라고 지정하였다.
본론으로 돌아와서, user_id와 upload_id가 필요한 이유는, 이미지 파일들을 스토리지로 저장할 때 이미지 경로에 user_id와 upload_id가 포함되기 때문이다.
이미지 경로: ~/media/{user_id}/upload{upload_id}/images/{image file name}
예를 들어, user1이라는 아이디를 가진 사용자가 sample1.png, sample2.png라는 이미지들에 대해 업로드 요청을 했다고 하자. 이 때 user1의 업로드 request에 부여된 upload_id가 13이라고 하면, 각 이미지들의 저장 경로는 ~/media/user1/upload13/images/sample1.png과 ~/media/user1/upload13/images/sample2.png가 된다.
참고로 user1은 본인 아이디로 된 폴더(user1/)으로만 접근할 수 있으며, 이를 위해서는 request의 header에 있는 토큰 정보를 이용해 현재 접근한 유저가 누구인지 식별할 필요가 있다. (자료메이커 프로젝트에서는 세션이 아니라 JWT 토큰 인증으로 구현해둔 상태이므로, request header에 담겨오는 토큰의 payload 부분에서 user pk 정보를 추출해 올 수 있다.)
# files/views.py
class ImageView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, ]
def post(self, request):
user = request.user
# Create Upload
upload = Upload.objects.create(user=user)
upload.upload_path = get_path(user.user_id, upload.id)
upload.save()
return JsonResponse()
Upload는 다음과 같은 구성정보를 포함한다.
Upload
- id : primary key, bigInt, automatically increasing, 업로드 아이디(식별자)
- user : foreign key, string, 기존 User 객체와 매핑, 업로드 요청을 한 유저의 아이디
- upload_path : string, 업로드한 이미지 파일들이 저장되는 폴더 경로
Image는 다음과 같은 구성정보를 포함한다.
Image
- id : primary key, bigInt, automatically increasing, 이미지 아이디(식별자)
- upload : foreign key, bigInt, 기존 Upload 객체와 매핑, 이미지가 딸려온 업로드의 아이디
- image_path : string, 이미지 파일이 저장되는 폴더 경로
- image_name : string, 스토리지에 저장된 이미지 파일의 이름
- image_type : string, 해당 이미지에 주로 포함된 내용이 영어인지 한글인지, 필기체인지 인쇄체인지에 대한 타입 지정
+) Upload와 Image는 files/models.py에 구현되어 있다.
task3, 4에 해당한다.
request.FILES로부터 업로드 요청된 이미지들의 리스트를 얻을 수 있다.
이미지들은 key-value형태로 넘어오게 되는데, key에는 image0, image1, image2와 같이 image + 인덱스(0부터 시작)의 형태로 되어있고, value는 이미지 파일 자체가 들어있다.
이미지 파일은 FileSystemStorage를 이용해 로컬 서버에 저장하고, 이후 boto3 모듈을 이용해 s3로도 업로드되도록 하였다. s3 업로드에 대한 자세한 내용은 추후에 정리하고, 우선은 s3_upload라는 함수는 s3 버킷으로 이미지 파일들을 업로드할 수 있도록 구현해 둔 함수라는 점만 알고 넘어가자.
+) s3_upload 함수와 boto3 모듈 이용에 대한 구체적인 구현 내용은 여기에 정리해두었다.
# files/views.py
class ImageView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, ]
def post(self, request):
user = request.user
# Create Upload
upload = Upload.objects.create(user=user)
upload.upload_path = get_path(user.user_id, upload.id)
upload.save()
# Get image data
images = request.FILES
image_type = request.POST['image_type']
s3_path = get_path(user.user_id, upload.id) + "images/"
local_path = MEDIA_DIR + s3_path
# Save images into storage
fs = FileSystemStorage(location=local_path, base_url=local_path)
for i in range(len(images)):
image = images['image'+str(i)] # Get image file
save_image = fs.save(image.name, image) # save image into local storage
image_name = fs.url(save_image).split('/')[-1]
s3_upload(local_path+image_name, s3_path+image_name) # save image into S3 storage
return JsonResponse()
task4에 해당한다.
앞서 언급한 바와 같이, 여러 이미지 정보를 한 번에 DB에 저장할 수는 없고, 개별 이미지에 대해서 저장해주어야 한다. Image에 하나씩 저장해두도록 한다.
+) serializer를 이용해서 깔끔하게 구현하고 싶었으나, 우선 반환하려는 정보가 Image에 대한 json 정보가 아니고, create() 함수에 validated_data를 넘길 때, 원하는 값들만 넘겨주는 것이 안돼서 우선은 views.py에 전부 구현해두었다. 이건 뭔가 다른 방법을 찾아봐야겠다.
# files/views.py
class ImageView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, ]
# Save images, Return text
def post(self, request):
user = request.user
# Create Upload
upload = Upload.objects.create(user=user)
upload.upload_path = get_path(user.user_id, upload.id)
upload.save()
# Get image data
images = request.FILES
image_type = request.POST['image_type']
s3_path = get_path(user.user_id, upload.id) + "images/"
local_path = MEDIA_DIR + s3_path
# Save images into storage & database
fs = FileSystemStorage(location=local_path, base_url=local_path)
for i in range(len(images)):
image = images['image'+str(i)] # Get image file
save_image = fs.save(image.name, image) # save image into local storage
image_name = fs.url(save_image).split('/')[-1]
s3_upload(local_path+image_name, s3_path+image_name) # save image into S3 storage
Image.objects.create( # save image into database
upload = upload,
image_path = s3_path,
image_name = image_name,
image_type = image_type,
)
return JsonResponse()
task5에 해당한다.
# files/views.py
class ImageView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, ]
def post(self, request):
user = request.user
# Create Upload
upload = Upload.objects.create(user=user)
upload.upload_path = get_path(user.user_id, upload.id)
upload.save()
# Get image data
images = request.FILES
image_type = request.POST['image_type']
s3_path = get_path(user.user_id, upload.id) + "images/"
local_path = MEDIA_DIR + s3_path
# Save images into storage & database
fs = FileSystemStorage(location=local_path, base_url=local_path)
for i in range(len(images)):
image = images['image'+str(i)] # Get image file
save_image = fs.save(image.name, image) # save image into local storage
image_name = fs.url(save_image).split('/')[-1]
s3_upload(local_path+image_name, s3_path+image_name) # save image into S3 storage
Image.objects.create( # save image into database
upload = upload,
image_path = s3_path,
image_name = image_name,
image_type = image_type,
)
# Extract text from images
result = detect_recognition(local_path, image_type)
# convert result into json format, and return it
data = get_json(result, upload.id)
return JsonResponse(data=data, status=status.HTTP_200_OK)
detect_recognition
텍스트 추출 모델과 연결해주는 함수이다. models/views.py에 구현되어있다.
이미지들이 저장된 스토리지 경로(image_path)와 해당 이미지들의 타입(image_type)을 parameter로 넘겨주면, 해당 이미지들로부터 추출한 텍스트 정보를 bytes 객체로 반환한다.
따라서 result에는 추출된 텍스트 정보가 bytes으로 저장되어있다.
JsonFormat
bytes 객체를 string type으로 변환하고, 여기서 적절한 데이터들을 split하여 dict 형태의 반환 값을 만들어준다. 즉, 앱에 response할 json 데이터 형태를 만들고 반환해주는 함수이다. JsonFormat은 함수 구현 내용은 여기에 정리해 두었다.
따라서