본문 바로가기

Challenge/엘리스 AI 트랙 2기

엘리스 AI 트랙 2기 - 22, 23주차 모히또 레이서의 학습일지

728x90
반응형

 

프로젝트가 시작한 지 3주 차가 되어간다. 슬슬 정신이 혼미해져 간다.

 


 

3주 차는 모델 구현 결과를 웹서비스에 반영하고 웹서비스를 이용하는 사용자 정보를 수집할 수 있도록 데이터 베이스 환경을 구현에 집중했다. 

 

나는 이번 3주 차에 최대한 백 엔드 API를 구현해서 다른 팀원 분들을 도와드리자는 개인적인 목표를 두었다. 그래서 3주 차 이 한 주만에 4개의 API를 구현했다.

 

우선 알약의 이름과 모양, 색상의 값을 입력받으면 데이터베이스에서 조건에 맞는 알약을 찾아주는 직접 검색 API를 만들었다.

처음엔 장고의 쿼리셋에 익숙하지 않아서 ㅋㅋㅋ 진짜 그리디하게 알고리즘을 구현했다.

 

# 알약 직접 검색
@api_view(['GET'])
@permission_classes([AllowAny])
def search_direct(request):
    pill = InfoPill.objects.all()
    n = request.GET.get('n', "") # 약 이름
    s = request.GET.get('s', "") # 약 모양
    c_f = request.GET.get('c_f', "") # 약 앞면 색상
    # ?n= {약이름}만으로 검색 시 해당 이름에 해당 단어가 포함하면 반환해줌
    
    if n and s and c_f:
        pill = pill.filter(
            Q(item_name__contains=n) &
            Q(shape__exact=s) &
            Q(color_front__contains=c_f)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)

    elif n and s:
        pill = pill.filter(
            Q(item_name__contains=n) &
            Q(shape__exact=s)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)
    
    elif n and c_f:
        pill = pill.filter(
            Q(item_name__contains=n) &
            Q(color_front__contains=c_f)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)

    elif s and c_f:
        pill = pill.filter(
            Q(item_name__exact=s) &
            Q(color_front__contains=c_f)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)
    
    elif n:
        pill = pill.filter(
            Q(item_name__contains=n)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)

    elif s:
        pill = pill.filter(
            Q(shape__exact=s)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)

    elif c_f:
        pill = pill.filter(
            Q(color_front__contains=c_f)
            ).distinct()

        serializer = InfoPillSerializer(pill, many=True)
        return Response(serializer.data)

    else:
        return Response("해당하는 약 정보가 없습니다.")

 보이는 가.. 모든 경우의 수를 직접 대입해서 만든 그지 같은 직접 검색 API...

 

기능은 정상적으로 잘 작동되었지만 아무래도 이건 진짜 아닌 거 같아서 결국 리팩터링 작업에 들어갔다.

 

# 알약 직접 검색


@api_view(["GET"])
@permission_classes([AllowAny])
def search_direct(request):
    name = request.GET.get("name")  # 약 이름
    shape = request.GET.get("shape")  # 약 모양
    color_front = request.GET.get("color_front")  # 약 앞면 색상

    # 만약 ?name={약이름} 이랑 모양, 앞면 색상으로 검색 시 해당 이름과 모양, 색상이 포함된 값을 반환해줌
    if not name:
        name = ''
    if not color_front:
        color_front = ''

    if not shape:
        pill = InfoPill.objects.filter(item_name__contains=name) & InfoPill.objects.filter(
            color_front__contains=color_front)
    if shape:
        pill = InfoPill.objects.filter(shape__exact=shape) & InfoPill.objects.filter(
            item_name__contains=name) & InfoPill.objects.filter(color_front__contains=color_front)

    serializer = InfoPillSerializer(pill, many=True)

    page = int(request.GET.get('page', '1'))  # 페이지 params
    p = Paginator(serializer.data, 10)  # 페이지당 10개씩 보여 주기
    page_data = {"total_page": p.num_pages}, {"count": p.count}, {
        "page": page}, p.page(page).object_list

    return Response(page_data)

짜잔 이렇게나 간결해지고 이뻐졌다.. 헤헿   :)

 

 

두 번째 만든 API는 즐겨찾기 등록 및 조회 API다.

 

# 유저 즐겨찾기 API


@api_view(["GET", "POST", "DELETE"])
@permission_classes([IsAuthenticated])
def user_pill(request):
    content = {  # get으로 약 정보 확인하기 (지금은 유저로 돌림)
        "user": str(request.user.email),
    }

    user_email = request.user  # 유저 불러오기
    pill = InfoPill.objects.all()  # 약 정보 데이터 베이스 전부 가져오기
    pn = request.GET.get("pn", "")  # 약 넘버

    if request.method == "GET":
        if pn:
            # url 약 넘버 정확하게 일치한다면
            pill = pill.filter(Q(item_num__exact=pn)).distinct()
            if UserPill.objects.filter(Q(user_email=user_email.email) & Q(pill_num=pn)):
                return Response(True)
            else:
                return Response(False)

    if request.method == "POST":
        if pn:
            # url 약 넘버 정확하게 일치한다면
            pill = pill.filter(Q(item_num__exact=pn)).distinct()
            serializer = InfoPillSerializer(pill, many=True)
            pill_num = InfoPill.objects.get(
                item_num=pn)  # 입력한 약 넘버와 일치하는 약 번호 가져오기

            # UserPill 테이블에 user_email과 pill_num 저장
            userpillinfo = UserPill(user_email=user_email, pill_num=pill_num)
            userpillinfo.save()  # 저장 22
            return Response(f"{serializer.data}를 성공적으로 즐겨찾기에 추가했습니다.")
        else:
            return Response("올바른 요청 값이 아닙니다.")  # 정확한 약 넘버가 들어오지 않다면!

    if request.method == "DELETE":
        if pn:
            # url 약 넘버 정확하게 일치한다면
            pill = pill.filter(Q(item_num__exact=pn)).distinct()
            serializer = InfoPillSerializer(pill, many=True)
            pill_num = InfoPill.objects.get(
                item_num=pn)  # 입력한 약 넘버와 일치하는 약 번호 가져오기

            # test = UserPill(user_email=user_email, pill_num=pill_num)
            # UserPill 테이블에서 해당하는(pn) 값 삭제
            UserPill.objects.filter(
                user_email=user_email, pill_num=pill_num).delete()
            return Response("삭제 성공!")
        else:
            return Response("올바른 삭제 형식을 맞춰주세요.")


# 유저가 즐겨찾기 한 목록 보여주는 API


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def user_pill_list(request):
    """
    필요한 반환 리스트: PillInfo
    약 이름 = item_name
    사진 링크 = image
    성분/함량 = sungbun
    하루 복용량 = use_method_qesitm
    """

    user_email = request.user.email
    user_pill = UserPill.objects.filter(
        user_email=user_email).values_list("pill_num")
    pill = InfoPill.objects.filter(item_num__in=user_pill)

    serializer = UserPillListSerializer(pill, many=True)

    return Response(serializer.data)

알약의 고유 일련번호를 이용하여 해당하는 알약의 상세정보 페이지에서 즐겨찾기 버튼을 누르면 POST 메소드를 이용해 UserPill(유저 즐겨찾기)테이블에 자동으로 저장되고, 다시 이미 버튼이 활성화된 즐겨찾기 버튼을 누르면 DELETE 메소드를 이용해 해당하는 알약을 UserPill에서 삭제해주는 API를 구현했다. 그리고 맨 위에 있는 GET은 만약 UserPill에 지금 접속한 유저의 이메일과 상세 보기 페이지의 알약의 일련번호가 있으면 True를 아니면 False를 반환해 프론트 단에서 즐겨찾기 활성화 및 비활성화 기능을 용이하게 만들어줬당!

 

 

 

다음은 문제의 로그아웃 API다. 

 

우선 우리 서버의 유저 인증 방식은 JWT를 이용한 방식을 채택했는데, 나는 이걸 구현하면서도 멍청하게 JWT의 본질을 파악하지 못해 거의 이틀을 헤맸다.

 

우선 JWT 인증 방식에 사용되는 토큰은 Refresh 토큰과 Access 토큰이 있는데 Access 토큰은 인증 기간이 만료되기 전까지는 계속해서 사용이 가능하다는 특징이 있다. 하지만 댕청하게도 난 그것도 모르고 자꾸 서버단에서 액세스 토큰을 관리하려고 했고 당연히 될 리가 없었다. 그 결과 나는 액세스 토큰이 아니라 리프레쉬 토큰을 사용해 유저를 인증하는 방식을 썼지만 아무리 생각해도 이건 도저히 아닌 거 같아서 결국 액세스 토큰의 기간을 짧게 잡고 유저가 로그아웃 버튼을 누를 시 처음 로그인할 때 들고 온 리프레쉬 토큰을 끊어버리는 방식을 채택했다... (왜 난 이 쉬운 방법을 모르고 자꾸 Access 토큰을 서버단에서 관리하려고 했나...)

 

# refresh 토큰을 blacklist로 올리는 api

class LogoutView(GenericAPIView):
    serializer_class = RefreshTokenSerializer
    permission_classes = (permissions.IsAuthenticated,)

    def post(self, request, *args):
        sz = self.get_serializer(data=request.data)
        sz.is_valid(raise_exception=True)
        sz.save()
        return Response("로그아웃 성공", status=status.HTTP_204_NO_CONTENT)

라이브러리는 simple jwt를 사용했고 blacklist라는 기능을 사용했다.

 

 

 

 

 

마지막으로 가입한 회원이 비밀번호를 잊어버렸을 시에 비밀번호를 재설정해주는 API 기능을 추가했다.

 

사실 이 API는 구현이라기보다는 settings.py에 갖가지 설정 추가를 통해 기능을 넣은 것이다.

djoser라는 라이브러리에 Password Reset 기능이 있는데 내장되어 있는 url에 알맞은 형식을 담아 보내주면 

 

이런 식으로 비밀번호를 변경하라는 이메일을 보내주고, 링크에 접속하면

 

 

비밀번호를 설정해 달라는 페이지가 나온다! 호레이! 그리고 저기 폼에 알맞게 형식을 맞춰 다시 POST로 쏴주면

 

 

비밀번호가 성공적으로 설정되었다는 메일이 보내지면서 비밀번호가 변경된다.

 

 

4주 차에는 학습시킨 인공지능 모델 결과를 웹서비스에 반영하고 더 나은 사용자 경험을 위해 테스트를 진행하며 기능을 구체화하고 발전시켰다.

 

 

4주 차에는 3주 차 때 만든 API를 리팩터링 하고 axios를 이용해 비밀번호 재설정 페이지를 백단과 연결하고 다시 디자인도 다듬고 다른 분들 작업 도와주는 것에 집중했다.

 


후기

 

 

본격적인 개발에 들어간 주였다. 초반엔 매우 힘들었지만 하나하나 기능이 만들어지는 것을 보면 뿌듯함이 더 컸다. 그리고 장고가 익숙하지 않아 어떤 API는 함수형을 썼고 어떤 건 클래스형으로 구현했는데 다음에 만약 내가 장고를 또 만지는 일이 있다면 하나로 통일해서 해야겠다. 그리고 코치님이 되도록 클래스 형으로 하는 게 더 장점이 많다고 알려주셔서 아마 클래스 형으로 통일해서 기능들을 구현하지 않을까 싶다. 이제 마지막 한 주 남았다. 한 주 마무리를 잘해서 유종의 미를 거두고 싶다.

728x90
반응형