๐Ÿ“’ [ TIL ] 2022.07.29_72์ผ์ฐจ # ์ตœ์ข… ํ”„๋กœ์ ํŠธ (14)

๋ฌธ๋ช…์ฃผยท2022๋…„ 8์›” 1์ผ
1
post-thumbnail

[ 2022-07-29 (๊ธˆ) ์˜ค๋Š˜์˜ TIL ]

[ Today Project ]

drf ๋ฐฑ์—”๋“œ๊ฐœ๋ฐœ + ํ”„๋ก ํŠธ๊ฐœ๋ฐœ
: ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋Š” ํŒ€ํ”„๋กœ์ ํŠธ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

[ Today Learn ]

  • ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธฐ๋Šฅ๊ตฌํ˜„

โœ๏ธ ๋‚ด๊ฐ€ ๋ฐฐ์šด๊ฒƒ, ์–ป์€๊ฒƒ

serializers.py

from rest_framework import serializers
from article.models import (
    Article as ArticleModel,
    Comment as CommentModel,
    CommentLikeBridge,
    ArticleVoteBridge,
)
from user.models import User as User
from user.serializers import UserSerializer

class CommentSerializer(serializers.ModelSerializer):
    comments_related_article = serializers.SerializerMethodField()
    author = serializers.SerializerMethodField()
    count = serializers.SerializerMethodField()
    nickname = serializers.SerializerMethodField()

    def get_comments_related_article(self,obj):
        return obj.article.id

    def get_author(self,obj):
        return obj.comment_author.username

    def get_nickname(self,obj):
        return obj.comment_author.nickname

    def get_count(self,obj):
        like_count = CommentLikeBridge.objects.filter(comment_id=obj.id).count()
        return like_count

    # custum update
    def update(self, instance, validated_data):
        for key, value in validated_data.items():
            if key == "comment_author":
                instance.user(value)
                continue
            setattr(instance, key, value)
        instance.save()
        return instance
    class Meta :
        model = CommentModel
        fields = ['id', 'article', 'author','nickname', 'comment_created_at', 'comment_contents', 'comments_related_article', 'count']

class ArticleSerializer(serializers.ModelSerializer):
    comment_set = CommentSerializer(many=True)
    author = serializers.SerializerMethodField()
    vote = serializers.SerializerMethodField()
    nickname = serializers.SerializerMethodField()
    board = serializers.SerializerMethodField()


    def get_board(self,obj):
        return obj.board.name

    def get_author(self,obj):
        return obj.article_author.username

    def get_nickname(self,obj):
        return obj.article_author.nickname

    def get_vote(self,obj):
        votes = ArticleVoteBridge.objects.filter(article_id=obj.id)
        votes = list(votes.values())
        vote_count = dict(fox=0, green=0, miss=0)
        for vote in votes:
            if vote['category'] == 'ํญ์Šค์ž…๋‹ˆ๋‹ค':
                vote_count['fox'] += 1
            elif vote['category'] == '๊ทธ๋ฆฐ๋ผ์ดํŠธ':
                vote_count['green'] += 1
            else:
                vote_count['miss'] += 1
        vote_count['count'] = vote_count['fox'] + vote_count['green'] + vote_count['miss']
        return vote_count

    # custum update
    def update(self, instance, validated_data):
        for key, value in validated_data.items():
            if key == "article_author":
                instance.set_author(value)
                continue
            setattr(instance, key, value)
        instance.save()
        return instance
    class Meta:
        model = ArticleModel
        fields = ['id','author','nickname','article_title','article_image', 'board', 'vote',
        'article_contents','article_post_date',
        'article_exposure_date','comment_set', 
        ]

views.py

from unicodedata import category
from django.core.paginator import Paginator
from rest_framework.pagination import LimitOffsetPagination
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions
from urllib import parse

from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import permission_classes
from rest_framework_simplejwt.authentication import JWTAuthentication


from article.models import (
    Article as ArticleModel,
    Comment as CommentModel,
    Board as BoardModel,
    ArticleLike,
    CommentLike,
    CommentLikeBridge,
    ArticleLikeBridge,
    ArticleVoteBridge,
    Vote as VoteModel
)
from article.serializers import (
    ArticleSerializer,
    CommentSerializer,
)
# ๊ฒŒ์‹œํŒ ๋ณ„ ๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง€๋„ค์ด์…˜
class ArticlePagination(APIView, LimitOffsetPagination):
    def get(self, request, format=None):
        board = request.query_params.getlist('board')[0]
        page = int(request.query_params.getlist('page')[0])
        # if board == 'HOT':
        #     articles = ArticleModel.objects.filter(board__name=board)
        #     print(articles)
        articles = ArticleModel.objects.filter(board__name=board).order_by("-id")[((page-1)*10):(page*10)]
        if board == 'HOT':
            all = list(ArticleModel.objects.all().values())
            articles_id = []
            for article in all:
                articles_id.append(article['id'])
            vote_counts = []
            for id in articles_id:
                vote_count = ArticleVoteBridge.objects.filter(article_id=id).count()
                if vote_count >= 1:
                    num = 0
                    num = num + 1
                vote_counts.append(vote_count)
            print(num)
            count_list = {name:value for name, value in zip(articles_id, vote_counts)}
            vote_rank = sorted(count_list.items(), key=lambda x: x[1], reverse=True)[:num]
            ranking = []
            for i in range(num):
                ranking.append(vote_rank[i][0])
            article_rank = ArticleModel.objects.filter(id__in = ranking)
            results = self.paginate_queryset(article_rank, request, view=self)
            serializer = ArticleSerializer(results, many=True)
            return self.get_paginated_response(serializer.data)
        else:
            results = self.paginate_queryset(articles, request, view=self)
            serializer = ArticleSerializer(results, many=True)
            return self.get_paginated_response(serializer.data)

# ํˆฌํ‘œ์ˆœ ํƒ‘3 ๋ฆฌ์ŠคํŒ…
class MostVotedArticleView(APIView):
    def get(self, request):
        articles = list(ArticleModel.objects.all().values())
        articles_id = []
        for article in articles:
            articles_id.append(article['id'])
        vote_counts = []
        for id in articles_id:
            vote_count = ArticleVoteBridge.objects.filter(article_id=id).count()
            vote_counts.append(vote_count)
        count_list = { name:value for name, value in zip(articles_id, vote_counts)}
        vote_rank = sorted(count_list.items(), key=lambda x: x[1], reverse=True)[:3]
        first = vote_rank[0][0]
        second = vote_rank[1][0]
        third = vote_rank[2][0]
        ranking = [first, second, third]
        article_rank = ArticleModel.objects.filter(id__in = ranking)
        return Response(ArticleSerializer(article_rank, many=True).data)


# ๋Œ“๊ธ€ ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฆฌ์ŠคํŒ…
class CommentPagination(APIView, LimitOffsetPagination):
    def get(self, request, format=None):
        comments = CommentModel.objects.all()
        results = self.paginate_queryset(comments, request, view=self)
        serializer = CommentSerializer(results, many=True)
        return self.get_paginated_response(serializer.data)

class ArticleView(APIView):
    permission_classes = [permissions.AllowAny]

    # ๋ชจ๋“  ๊ฒŒ์‹œ๊ธ€ ๋ฆฌ์ŠคํŒ…
    def get(self, request):
        articles = list(ArticleModel.objects.all().order_by("-id"))
        result = ArticleSerializer(articles, many=True).data
        return Response(result) 

    # ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ
    def post(self, request):
        if request.user.is_anonymous:
            return Response({"error":"๊ธ€ ์ž‘์„ฑ์„ ์œ„ํ•ด ๋กœ๊ทธ์ธ์„ ํ•ด์ฃผ์„ธ์š”."})
        else:
            try:
                board = BoardModel.objects.get(name=request.data.get('board'))
                article = ArticleModel.objects.create(
                    article_author = request.user,
                    article_title = request.data.get('article_title',''),
                    article_contents = request.data.get('article_contents',''),
                    article_image = request.FILES['article_image'],
                    article_exposure_date = request.data.get('article_exposure_date',''),
                    board = board,
                )
            except:
                board = BoardModel.objects.get(name=request.data.get('board'))
                article = ArticleModel.objects.create(
                    article_author = request.user,
                    article_title = request.data.get('article_title',''),
                    article_contents = request.data.get('article_contents',''),
                    article_exposure_date = request.data.get('article_exposure_date',''),
                    board = board,
                )
            if len(request.data.get('article_title','')) <= 1 :
                return Response({"error":"title์ด 1์ž ์ดํ•˜๋ผ๋ฉด ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."})
            elif len(request.data.get('article_contents','')) <= 10 :
                return Response({"error":"contents๊ฐ€ 10์ž ์ดํ•˜๋ผ๋ฉด ๊ฒŒ์‹œ๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."}) 
            else:
                article.save()
                return Response({"message":"๊ฒŒ์‹œ๋ฌผ ์ž‘์„ฑ ์™„๋ฃŒ!!"})

    # ๊ฒŒ์‹œ๋ฌผ ์—…๋ฐ์ดํŠธ
    def put(self, request, obj_id):
        article = ArticleModel.objects.get(id=obj_id)

        if request.user == article.article_author or request.user.is_admin:
            article_serializer = ArticleSerializer(article, data=request.data, partial=True, context={"request": request})
            if article_serializer.is_valid():
                article_serializer.save()
                return Response(article_serializer.data, status=status.HTTP_200_OK)
            else:
                return Response(article_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        else:
            return Response({"message":"๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •์€ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋งŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."})

    # ๊ฒŒ์‹œ๋ฌผ ์‚ญ์ œ
    def delete(self, request, obj_id):
        article = ArticleModel.objects.get(id=obj_id)
        title = article.article_title 
        user = article.article_author
        if request.user == article.article_author or request.user.is_admin:
            ArticleModel.objects.get(id=obj_id).delete()
            return Response({'message': f'{user}๋‹˜์˜ {title} ๊ฒŒ์‹œ๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'})
        else:
            return Response({"message":"๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ๋Š” ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ์ž๋งŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."})

# article detail ํŽ˜์ด์ง€ article/<obj_id>/detail/
class ArticleDetailView(APIView):

    def get(self, request, obj_id):      
        article = ArticleModel.objects.get(id=obj_id)
        return Response(ArticleSerializer(article).data)
class CommentView(APIView):
    

    def get(self, request, obj_id):
        return Response(CommentSerializer(obj_id).data)

    # ๋Œ“๊ธ€ ์ž‘์„ฑ
    def post(self, request, obj_id):
        if request.user.is_anonymous:
            return Response({"error":"๋Œ“๊ธ€ ์ž‘์„ฑ์„ ์œ„ํ•ด ๋กœ๊ทธ์ธ์„ ํ•ด์ฃผ์„ธ์š”."})
        else:
            user = request.user
            request.data['article'] = ArticleModel.objects.get(id=obj_id)
            contents = request.data.get('comment_contents','')

            comment = CommentModel(
                article = request.data['article'],
                comment_author = user,
                comment_contents = contents,
            )
            comment.save()
            return Response({"message":"๋Œ“๊ธ€ ์ž‘์„ฑ ์™„๋ฃŒ!"})

    # ๋Œ“๊ธ€ ์—…๋ฐ์ดํŠธ
    def put(self, request, obj_id):
        data = request.data
        comment = CommentModel.objects.get(id=obj_id)

        if request.user == comment.comment_author or request.user.is_admin:
            comment_serializer = CommentSerializer(comment, data, partial=True, context={"request": request})
            if comment_serializer.is_valid():
                comment_serializer.save()
                return Response(comment_serializer.data, status=status.HTTP_200_OK)
        else:
            return Response({"error":"๋Œ“๊ธ€ ์ˆ˜์ •์€ ๋Œ“๊ธ€ ์ž‘์„ฑ์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."})

    # ๋Œ“๊ธ€ ์‚ญ์ œ
    def delete(self, request, obj_id):
        comment = CommentModel.objects.get(id=obj_id)
        comment_author = comment.comment_author
        article_author = comment.article.article_author
        if request.user == comment.comment_author or request.user.is_admin or request.user == article_author:
            CommentModel.objects.get(id=obj_id).delete()
            return Response({'message': f'{comment_author}๋‹˜์˜ ๋Œ“๊ธ€์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'})
        else:
            return Response({'error': '๋Œ“๊ธ€ ์‚ญ์ œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค'})

# ๊ฒŒ์‹œ๊ธ€ ๊ณต๊ฐ
class ArticleLikeView(APIView):
    authentication_classes = [JWTAuthentication]

    def post(self, request, article_id):
        like = ArticleLike.objects.create()
        article_title = ArticleModel.objects.get(id=article_id)
        all = list(ArticleLikeBridge.objects.all().values())
        print(all)
        all_id = []
        for obj in all:
            all_id.append(obj['user_id'])

        if request.user.id in all_id:
            article_like = ArticleLikeBridge.objects.get(user_id=request.user.id)
            article_like.delete()
            return Response({'message': f'{request.user}๋‹˜๊ป˜์„œ {article_title.article_title}์— ๊ณต๊ฐ์„ ์ทจ์†Œํ•˜์…จ์Šต๋‹ˆ๋‹ค.'})
        else:
            article_like = ArticleLikeBridge(
                article_id = article_id,
                user_id = request.user.id,
                like_id = like.id,
                category = request.data.get('category')
        )
            article_like.save()
            return Response({'message': f'{request.user}๋‹˜๊ป˜์„œ {article_title.article_title}์— {article_like.category}ํ•˜์…จ์Šต๋‹ˆ๋‹ค.'})

# ๊ฒŒ์‹œ๊ธ€ ํˆฌํ‘œ
class ArticleVoteBridgeView(APIView):
    authentication_classes = [JWTAuthentication]

    # ๊ฒŒ์‹œ๊ธ€ ํˆฌํ‘œ ์นด์šดํŠธ
    def get(self, request, article_id):
        votes = ArticleVoteBridge.objects.filter(article_id=article_id)
        votes = list(votes.values())
        count = dict(fox=0, green=0, miss=0)
        for vote in votes:
            if vote['category'] == 'ํญ์Šค์ž…๋‹ˆ๋‹ค':
                count['fox'] += 1
            elif vote['category'] == '๊ทธ๋ฆฐ๋ผ์ดํŠธ':
                count['green'] += 1
            else:
                count['miss'] += 1
        return Response(count)

    def post(self, request, article_id):
        vote = VoteModel.objects.create()
        article_title = ArticleModel.objects.get(id=article_id)
        all = list(ArticleVoteBridge.objects.all().values())
        article_vote = ArticleVoteBridge.objects.filter(user_id=request.user.id)
        vote_article = article_vote.filter(article_id=article_id)
        vote_article.delete()
        all_id = []
        for obj in all:
            all_id.append(obj['user_id'])
        try:
            article_vote = ArticleVoteBridge.objects.filter(user_id=request.user.id)
            if article_vote.category != request.data.get('category'):
                article_vote.delete()
                new_vote = ArticleVoteBridge(
                article_id = article_id,
                user_id = request.user.id,
                vote_id = vote.id,
                category = request.data.get('category',"")
            )
                new_vote.save()
                return Response({'message': f'{new_vote.category}๋กœ ์žฌํˆฌํ‘œ!'})
            elif article_vote.category == request.data.get('category'):
                return Response({'message': '์ด๋ฏธ ํˆฌํ‘œํ•˜์…จ์Šต๋‹ˆ๋‹ค.'})
            else:
                article_vote = ArticleVoteBridge(
                    article_id = article_id,
                    user_id = request.user.id,
                    vote_id = vote.id,
                    category = request.data.get('category',"")
            )
                article_vote.save()
                return Response({'message': f'{article_vote.category}์— ํ•œํ‘œ!'})
        except:
            article_vote = ArticleVoteBridge(
                article_id = article_id,
                user_id = request.user.id,
                vote_id = vote.id,
                category = request.data.get('category',"")
            )
            article_vote.save()
            return Response({'message': f'{article_vote.category}์— ํ•œํ‘œ!'})
# ๋Œ“๊ธ€ ๊ณต๊ฐ
class CommentLikeView(APIView):
    authentication_classes = [JWTAuthentication]

    # ๋Œ“๊ธ€ ๊ณต๊ฐ ์นด์šดํŠธ
    def get(self, request, comment_id):
        like_count = CommentLikeBridge.objects.filter(comment_id=comment_id).count()
        return Response(like_count)

    def post(self, request, comment_id):
        like = CommentLike.objects.create()
        comment = CommentModel.objects.get(id=comment_id)
        contents = comment.comment_contents
        all = list(CommentLikeBridge.objects.all().values())
        all_id = []
        for obj in all:
            all_id.append(obj['user_id'])
        

        if request.user.id in all_id:
            # for i in all:
            #     id = i['comment_id']                
            try:
                CommentLikeBridge.objects.get(comment_id=comment_id)
                comment_like = CommentLikeBridge.objects.get(comment_id=comment_id)
                if comment_like.user_id == request.user.id:
                    comment_like.delete()
                    return Response({'message': f'{request.user}๋‹˜๊ป˜์„œ {contents[0:10]}...๋Œ“๊ธ€์— ๊ณต๊ฐ์„ ์ทจ์†Œํ•˜์…จ์Šต๋‹ˆ๋‹ค.'})
            except:
                comment_like = CommentLikeBridge(
                    comment_id = comment_id,
                    user_id = request.user.id,
                    like_id = like.id,
                    category = request.data.get('category')
            )
                comment_like.save()
                return Response({'message': f'{request.user}๋‹˜๊ป˜์„œ {contents[0:10]}...๋Œ“๊ธ€์— {comment_like.category}ํ•˜์…จ์Šต๋‹ˆ๋‹ค.'})
        else:
            comment_like = CommentLikeBridge(
                comment_id = comment_id,
                user_id = request.user.id,
                like_id = like.id,
                category = request.data.get('category')
            )
            comment_like.save()
            return Response({'message': f'{request.user}๋‹˜๊ป˜์„œ {contents[0:10]}...๋Œ“๊ธ€์— {comment_like.category}ํ•˜์…จ์Šต๋‹ˆ๋‹ค.'})


# ๊ณต๊ฐ์ˆœ ๊ฒŒ์‹œ๊ธ€ ํƒ‘3 ๋ฆฌ์ŠคํŒ…
class MostLikedArticleView(APIView):
    def get(self, request):
        articles = list(ArticleModel.objects.all().values())
        articles_id = []
        for article in articles:
            articles_id.append(article['id'])
        like_counts = []
        for id in articles_id:
            like_count = ArticleLikeBridge.objects.filter(article_id=id).count()
            like_counts.append(like_count)
        count_list = { name:value for name, value in zip(articles_id, like_counts)}
        like_rank = sorted(count_list.items(), key=lambda x: x[1], reverse=True)[:3]
        ranking = []
        for rank in range(len(like_rank)):
            if like_rank[rank][0] is not None: 
                ranking.append(like_rank[rank][0])
            else:
                pass
        article_rank = ArticleModel.objects.filter(id__in = ranking)
        return Response(ArticleSerializer(article_rank, many=True).data)

# ๊ณต๊ฐ์ˆœ ๋Œ“๊ธ€ ํƒ‘3 ๋ฆฌ์ŠคํŒ…
class MostLikedCommentView(APIView):
    def get(self, request):
        comments = list(CommentModel.objects.all().values())
        comments_id = []
        for comment in comments:
            comments_id.append(comment['id'])
        like_counts = []
        for id in comments_id:
            like_count = CommentLikeBridge.objects.filter(comment_id=id).count()
            like_counts.append(like_count)
        count_list = { name:value for name, value in zip(comments_id, like_counts)}
        like_rank = sorted(count_list.items(), key=lambda x: x[1], reverse=True)[:3]
        first = like_rank[0][0]
        second = like_rank[1][0]
        third = like_rank[2][0]
        forth = like_rank[3][0]
        fifth = like_rank[4][0]
        sixth = like_rank[5][0]
        ranking = [first, second, third, forth, fifth, sixth]
        comment_rank = CommentModel.objects.filter(id__in = ranking)
        return Response(CommentSerializer(comment_rank, many=True).data)

# ํˆฌํ‘œ์ˆœ ํƒ‘3 ๋ฆฌ์ŠคํŒ…
class MostVotedArticleView(APIView):
    def get(self, request):
        articles = list(ArticleModel.objects.all().values())
        articles_id = []
        for article in articles:
            articles_id.append(article['id'])
        vote_counts = []
        for id in articles_id:
            vote_count = ArticleVoteBridge.objects.filter(article_id=id).count()
            vote_counts.append(vote_count)
        count_list = { name:value for name, value in zip(articles_id, vote_counts)}
        vote_rank = sorted(count_list.items(), key=lambda x: x[1], reverse=True)[:3]
        first = vote_rank[0][0]
        second = vote_rank[1][0]
        third = vote_rank[2][0]
        ranking = [first, second, third]
        article_rank = ArticleModel.objects.filter(id__in = ranking)
        return Response(ArticleSerializer(article_rank, many=True).data)

class SearchResult(APIView):
    permission_classes = [permissions.AllowAny]

    # ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŒ…
    def post(self, request):
        keywords = request.data.get('search')
        type= request.data.get('type')
        # ๋‚ด์šฉ ๊ฒ€์ƒ‰
        if type == '๋‚ด์šฉ':
            searched_contents = ArticleModel.objects.filter(article_contents__contains=keywords)
            result = ArticleSerializer(searched_contents, many=True).data
            return Response(result) 
        # ์ž‘์„ฑ์ž ๊ฒ€์ƒ‰
        elif type == '์ž‘์„ฑ์ž':
            searched_authors = ArticleModel.objects.filter(article_author__username=keywords)
            result = ArticleSerializer(searched_authors, many=True).data
            return Response(result) 
        # ์ œ๋ชฉ + ๋‚ด์šฉ ๊ฒ€์ƒ‰
        elif type == '์ œ๋ชฉ+๋‚ด์šฉ':
            searched_contents = ArticleModel.objects.filter(article_contents__contains=keywords)
            searched_titles = ArticleModel.objects.filter(article_title__contains=keywords)
            searched = searched_contents.union(searched_titles) 
            result = ArticleSerializer(searched, many=True).data
            return Response(result) 
        # ์ œ๋ชฉ ๊ฒ€์ƒ‰
        else:
            searched_titles = ArticleModel.objects.filter(article_title__contains=keywords)
            result = ArticleSerializer(searched_titles, many=True).data
            return Response(result) 


# ๋ฉ”์ธํŽ˜์ด์ง€ ๊ฒŒ์‹œํŒ๋ณ„ ์•„ํ‹ฐํด ๋ฆฌ์ŠคํŒ…
class ArticleByBoard(APIView):

    def get(self, request):
        boards = request.query_params.getlist('boards', '')
        results = []
        for board in boards:
            articles = ArticleModel.objects.filter(board__name=board).order_by("-id")[:5]
            result = ArticleSerializer(articles, many=True).data
            results_data = {
                f"{board}" : result
            }
            results.append(results_data)
        return Response(results)
profile
ํ•˜๋ฃจ ํ•œ๊ฑธ์Œ์”ฉ ๊พธ์ค€ํžˆ ๋‚˜์•„๊ฐ€๋Š” ๊ฐœ๋ฐœ์ž๐Ÿ™†โ€โ™€๏ธ https://github.com/Moonmooj

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด