본문 바로가기

내일 배움 캠프/TIL

조회수 기능 만들기(2)_IP기반


조회수 기능 만들기(2) - IP 기반

먼저 블로그 앱의 models에 사용자 IP, 날짜, 블로그ID, 게시글ID를 저장할 모델을 만들어준다.

# blogs/models.py

class ArticleHits(models.Model):
    client_ip = models.GenericIPAddressField(protocol='both', unpack_ipv4=True, null=True, verbose_name='사용자 IP주소')
    date = models.DateField(auto_now_add=True, verbose_name='조회 날짜')
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)

    def __str__(self):
        return str(self.article.id)

 

 

모델을 만들어 주었다면 함수와 그안에 사용될 serialize를 만들어 주자. 처음에는 http 처리방식get으로 해볼려했는데 모델에 따로 저장해줄 방법이 없어서 post로 진행했다.

serializer안에 사용자의 IP를 불러오는 함수를 따로 설정해 주었다. 'HTTP_X_FORWARDED_FOR' 와  'REMOTE_ADDR' 을 이용했는데 먼저 코드를 살펴보자.

# blogs/serializers.py

class ArticleHitSerializer(serializers.ModelSerializer):
    def get_client_ip(request):
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            ip = x_forwarded_for.split(',')[0]
        else:
            ip = request.META.get('REMOTE_ADDR')
        return ip
    
    class Meta:
        model = ArticleHits
        fields = ""

request.META에서 XFF가 있는지 찾는다. 존재한다면 client IP를 가져오고, 존재하지 않는다면 REMOTE_ADDR의 값을 사용해 IP를 가져온다.

 

 

HTTP_X_FORWARDED_FOR (XFF) 와 REMOTE_ADDR

XFF는 HTTP Header중 하나로 HTTP Server에 요청한 사용자의 IP를 식별할때 쓴다.

 

실제 IP는 REMOTE_ADDR에 가지고 있는데, Proxy 서버를 거치게 되면서 해당값만 으로는 정확한 IP를 알수없다. 그래서 XFF와 함께 쓰이는 것이다.  XFF에 저장되는 값들은 다음과 같다.

X-Forwarded-For: client, proxy1, proxy2

우리는 사용자 IP만 필요하기 때문에  첫번째 값(client)만 가져오면 된다. 

ip = x_forwarded_for.split(',')[0]

 

 

자, 사용자 IP를 불러올 준비는 다했다. 이제 post를 이용해 해당 게시물을 누를때 조회수(hits)가 올라가는 함수를 만들어야 한다. 

# blogs/views.py

"""조회수 기능(ip기반)"""
def post(self, request, blog_name, article_id):
    blog = Blog.objects.filter(blog_name=blog_name)
    article = get_object_or_404(Article, blog_id=blog[0].id, id=article_id)
    hit = ArticleHitSerializer.get_client_ip(request)
    articlehit = ArticleHits.objects.filter(article_id=article_id).values()
    serializer = ArticleHitSerializer(data=request.data)

 

blog : 사용자가 만든 블로그를 쿼리셋 형태로 저장된다

article : 사용자가 만든 블로그 내에서 작성한 특정 게시글이 저장된다

hit : 현재 블로그를 이용하는 사용자의 IP를 불러온다

articlehit : 게시글을 눌렀을 경우 사용자의 IP가 DB에 저장된다

 

 

조회수 중복을 방지하기 위해 현재 블로그를 이용하는 사용자의 IP의 존재 유무를 알아야한다.

a = 0
for n in range(len(articlehit)):
    if hit == articlehit[n]['client_ip']:
        a += 1

 

 

articlehit의 값이 빈값일 경우 bool을 해주면 False가 나온다. 따라서 bool(articlehit) = False 또는 a=0일 경우에 조회수를 증가 시켜준다.

즉, articlehit의 값이 빈값이라는 것은 DB에 해당 사용자의 IP가 저장된 상태가 아니라는것이고, a=0 이라는 것은 현재 사용자의 IP와 articlehit안에 저장된 IP값이 같은게 없다는 뜻이다.

if bool(articlehit) is False or a == 0:
    if serializer.is_valid():
            article.hits += 1
            article.save()
            serializer.save(client_ip=hit ,article_id=article_id, blog_id=blog[0].id)
            threading.Timer(20, ArticleDetailView.timer_delete, args=(article_id, hit)).start()
            return Response(serializer.data, status=status.HTTP_200_OK)
    else:
        return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
else:
    return Response("동일한 ip주소가 존재합니다.", status=status.HTTP_205_RESET_CONTENT)

 threading.Timer()는 일정시간이 지나면 자동으로  DB에 있는 값을 지워주어 다시 조회수를 증가시킬수 있게 하기위해 사용했다. 

 

여기서 정말 중요한것!

threading.Timer를 쓸때는 threading.Timer(시간, 함수, 함수안에 들어가는 인자) 형태로 써야하는데 args=() 형태로 쓰지않으면 무한 오류가 나기때문에 매우 주의해야 한다.

 

 

일정시간이 지나면 IP를 자동으로 삭제하는 함수도 작성해야 한다. (위 함수들과 같은 클래스내에 있다)

"""일정 시간 지나면 ip 자동 삭제하는 함수"""
def timer_delete(article_id, hit):
    articlehit = ArticleHits.objects.filter(article_id=article_id, client_ip=hit)
    print(articlehit)
    articlehit.delete()

 

결과

조회하기 전

 

조회 후

 

일정 시간이 지나기 전에 다시 조회 했을때