언리얼엔진/노노그램

[UE5] 사용자 제작 퍼즐 - 퍼즐 업로드

mstone8370 2023. 11. 13. 17:30

 

다음으로 사용자가 퍼즐을 만들면 서버에서 검증하고, 검증에 통과하면 데이터베이스에 저장해야한다.

 

먼저 기존에 있던 퍼즐을 푸는 기능을 조금만 수정해서 아래와 같이 퍼즐을 만드는 기능을 추가했다.

 

퍼즐의 전체 크기가 바뀔 때마다 크기를 화면에 맞추는 작업을 하다보니 힌트가 늘어나면 퍼즐이 흔들리는 문제가 있어서 고쳐야 할 듯 하다.

그래도 일단 기능은 제대로 작동하므로 이대로 진행한다.

 

 

 


 

 

 

그 다음 서버를 만들어서 제작된 퍼즐을 검증해야한다.

저번 글에서 장고를 써서 서버를 만들기로 했다.

사실 웹 애플리케이션을 만드는 것도 처음이고, 장고를 써보는 것도 처음이어서 작동만이라도 되게 하는 것을 목표로 했다.

 

서버의 역할은 크게 두가지가 있다.

하나는 사용자가 제작한 퍼즐을 검증하는 것.

다른 하나는 사용자가 제작한 퍼즐을 관리하는 것이다.

그래서 장고 프로젝트에 퍼즐을 검증하는 validation 앱과 사용자의 퍼즐을 관리하는 userpuzzle 앱을 추가했다.

 


 

먼저 퍼즐 검증 과정은 사용자가 제작한 퍼즐의 힌트 정보를 인코딩해서 POST방식으로 전달하고, 퍼즐 검증 결과를 JSON으로 받게 된다.

JSON에는 퍼즐의 정답 개수와 퍼즐의 답이 여러개인 경우 모호한 칸들의 좌표가 포함된다.

모호한 칸들은 No2g처럼 각 칸에 표시를 할 것이다.

 

먼저 저번 글에서 다뤘던 검증 코드를 조금 수정해서 답이 여러개인 경우는 항상 2를 리턴하고, 모호한 칸들도 같이 리턴하게 했다.

# 검증 코드는 생략

answer_count = 0
if any(0 in row for row in can_do):
    print("Something's wrong")
elif all(can_do[i][j] in (1, 2) for j in range(width) for i in range(height)):
    print("Solution would be unique")  # but could be incorrect!
    answer_count = 1
else:
    print("Solution may not be unique")
    answer_count = 2

return (answer_count, [str(x) + "|" + str(y) for x in range(len(can_do[0])) for y in range(len(can_do)) if can_do[y][x] == 3])

저번 글에서 다뤘듯이 can_do 2차원 리스트에는 모호한 칸들은 3으로 나타내고 있으니 각 칸들을 순회해서 3이 있으면 그 칸의 좌표를 문자열 형태로 리스트에 넣었다.

 

그 다음 장고에서는 해당하는 URL로 요청을 보내면 결과를 보내주게 했다.

def validate(request):
    if request.method == 'POST':
        data = request.body.decode('utf-8')
        return JsonResponse(solve(data, True))
    return HttpResponseForbidden()

 

언리얼 엔진에서는 업로드 용으로 만든 버튼을 누르면 아래와 같이 작동하게 했다.

 

ShowServerResponse 함수에서 검증 결과를 보여주는 작업을 한다.

 

만약 정답이 여러개 존재하는 경우에는 아래처럼 작동한다.

 

 


 

다음으로 퍼즐의 정답이 하나인 경우에는 업로드를 위해 추가 정보를 더 입력하게 해야한다.

필요한 추가정보는 No2g처럼 퍼즐 이름, 퍼즐 설명, 사용자 이름이다.

퍼즐을 풀기 전에는 퍼즐 이름을 보여주지 않고, 퍼즐 설명과 사용자 이름만 보여준다.

No2g에 있는 퍼즐 설명을 굳이 따라서 넣은 이유는 다른 사용자들에게 힌트를 주거나 호기심을 유발시켜서 퍼즐을 풀게 만드는 역할을 하는 것이 필요하다고 생각했고, 퍼즐 설명이 그 역할을 잘 한다고 생각하기 때문이다.

 

따라서 퍼즐을 정상적으로 풀 수 있는 경우에는 아래와 같이 추가 정보를 입력하는 위젯을 띄우게 했다.

 

 

그리고 서버에서도 해당 정보를 받아서 데이터베이스에 저장해야한다.

장고의 userpuzzle 앱에서 해당 정보를 저장하는 모델을 추가했다.

def upload_path(instance, filename):
    return f'{instance.user_name}/{filename}'

class UserPuzzle(models.Model):
    puzzle_name = models.CharField(verbose_name="puzzle_name", name="puzzle_name", max_length=20)
    puzzle_description = models.CharField(verbose_name="puzzle_description", name="puzzle_description", max_length=30)
    user_name = models.CharField(verbose_name="user_name", name="user_name", max_length=16)
    upload_date = models.DateTimeField(verbose_name="upload_date", name="upload_date", auto_now_add=True)
    puzzle_image = models.ImageField(verbose_name="puzzle_image", name="puzzle_image", upload_to=upload_path)
    encoded_hint = models.CharField(verbose_name="encoded_hint", name="encoded_hint", max_length=960)
    hint_count = models.IntegerField(verbose_name="hint_count", name="hint_count")
    puzzle_hash = models.CharField(verbose_name="puzzle_hash", name="puzzle_hash", max_length=12)

    def save(self, *args, **kwargs):
        self.hint_count = 0
        for char in self.encoded_hint:
            if char != '|' and char != ';':
                self.hint_count += 1

        super().save(*args, **kwargs)

퍼즐 이름, 퍼즐 설명, 사용자 이름 외에도 다른 정보들이 필요하다.

새로 업로드 된 퍼즐을 상단에 표시하기 위해 날짜 정보와, 퍼즐을 완료했을 때의 이미지, 퍼즐의 힌트 정보, 힌트의 개수, 해시된 문자열이 필요하다.

해시된 문자열의 경우 퍼즐의 이미지를 저장할 때 사용된다.

해시 함수는 FarmHash를 사용했다.

 

그리고 필요한 정보들을 전달받으면 다음의 과정을 거친다.

required_keys = [
    'puzzle_name',
    'puzzle_description',
    'user_name',
    'encoded_hint',
    'encoded_puzzle'
]

@csrf_exempt
def upload_request(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        if all(req_key in data.keys() for req_key in required_keys):
            current_date = timezone.now()
            puzzle = UserPuzzle(
                puzzle_name=data['puzzle_name'],
                puzzle_description=data['puzzle_description'],
                user_name=data['user_name'],
                upload_date=current_date,
                encoded_hint=data['encoded_hint']
            )
            hash_str = [
                str(data['puzzle_name']),
                str(data['user_name']),
                str(current_date),
                str(data['encoded_hint']),
            ]
            hashed_str = str(Fingerprint32("_".join(hash_str)))
            puzzle.puzzle_hash = hashed_str

            temp_img_dir = save_img_with_encoded_puzzle(data['encoded_puzzle'], hashed_str)
            if temp_img_dir != "":
                img_file = File(open(temp_img_dir, 'rb'))
                puzzle.puzzle_image.save(os.path.basename(temp_img_dir), img_file)
                puzzle.save()
                return HttpResponse("Success")
        return HttpResponse("Failed")
    return HttpResponseForbidden()

모델을 생성한 뒤에 퍼즐의 정보들로 해시 문자열을 생성하고, 퍼즐의 상태를 통해 이미지를 생성한 뒤에 저장한다.

장고의 ImageField는 원래 사용자가 전달한 이미지를 저장하는 용도로 알고있다.

여기에서는 서버에서 이미지를 생성한 다음에 저장해야하므로, 임시 폴더에 이미지를 먼저 생성한 뒤에 ImageField에 저장하는 방법을 썼다.

이미지를 생성하는 것은 다른 글에서 설명한것과 동일하게 OpenCV를 사용했다.

전에는 언리얼엔진에서 작동해야하므로 C++으로 작성했지만 여기에서는 장고에서 작동해야하므로 Python으로 작성됐다.

 

이렇게 해서 사용자 퍼즐을 업로드한 결과는 아래와 같다.

 

데이터베이스
프로젝트 폴더 내부의 이미지