Skip to content

Likelion-Inha-11/-be-django-DRF-soohyun

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MBTI 페이지 과제 (홍진욱, 한수현)


❗️ERD

ERD [link][https://www.erdcloud.com/d/g3qNePNiy2dTTPBRq]


회원관련 및 인증 - 진욱 / 나머지 - 수현



❗️코드 별 기능


1. 회원가입 / 로그인 / 로그아웃

🫵 회원가입

🍎 model

class Profile(AbstractUser):
    nickname = models.CharField(max_length=15);
    user_mbti = models.CharField(max_length=4);

    def __str__(self):
        return self.nickname
  • 장고에서 기본으로 제공하는 AUTH를 이용해 User를 구성(기본 : username , password1 , password2 )
  • 다른 항목들을 추가하기 위해, AbstractUser를 이용함.
  • 한 user의 profile에 접근했을 때 닉네임으로 표기해 주기 위해 def str 을 사용했음.

🍊 url

path('signup/', views.signup, name='signup')\
  • 보이는 그대로.

🍉 view

def signup(request):
    if request.method == "POST":
        form = Signupform(request.POST)
        if form.is_valid():
                form.save()
                username = form.cleaned_data.get('username')
                raw_password = form.cleaned_data.get('password1')
                user = authenticate(username=username, password=raw_password)
                login(request, user)
                return redirect('MBTIAPP:main')
    else:
        form = Signupform()
    return render(request, 'MBTIAPP/signup.html', {'form' : form})
  • POST 방식의 요청을 받으면 , 미리 작성해둔 "Signupform"을 form으로 정의해줌 => 그 후, form을 검사하고 , 저장. 저장한 데이터들을 변수들에 담음(cleaned_data.get)

  • 그 후 회원가입이 완료되면 자동으로 로그인 되게 처리 후 main 페이지로 보냄

  • GET 방식일때는 (else문) signup.html을 그냥 form을 담아서 보여줌 (당연히 가입하려고 회원가입을 누른 상황이니까)

🍋 form

class Signupform(UserCreationForm):
    nickname = forms.CharField(max_length=15 ,label="닉네임", required=True)

    class Meta:
        model = Profile
        fields = ("username", "password1" , "password2", "nickname")
  • 여기서 AbstractUser로 구현했기에, 연결하느라 좀 애를 먹음.

  • nickname 은 기본 AUTH USER에 없기에 따로 추가해줬음.

  • form을 템플릿 상에서 보여줄 때, 어떤 필드를 보여줄지를 나열함.

🍏 templates

<form method="post" action="{% url 'MBTIAPP:signup' %}">
    {% csrf_token %}
    {% include "MBTIAPP/form_errors.html" %}
  • 템플릿은 너무 길어서 핵심만 첨부.

  • 회원가입을 하고 정보를 보내니, POST 방식으로, signup의 이름을 가진 url으로 보냄.

  • 보안을 위해 csrf_token , form_errors.html 만들어서 오류처리 (장고 내장기능) => 점프 투 장고 참고


🫵 로그인

🍊 url

path('login/', auth_views.LoginView.as_view(template_name='MBTIAPP/login.html'),name='login'),
  • 장고에 내장되어 있는, LoginView를 import해서 사용.

🍉 view

  • LoginView가 해줌..

🍏 templates

  • 뻔한 내용이라 생략

🫵 로그 아웃

🍊 url

path('logout/', auth_views.LogoutView.as_view(), name='logout'),
  • 로그인과 동일하게 LogoutView 가 해줌.

로그인과 유사해서 생략.

🫵 첫 페이지

🍏 templates

{% if user.is_authenticated %}
    {% csrf_token %}
    <a href="{% url 'MBTIAPP:logout' %}">로그아웃</a>
    <a href="{% url 'MBTIAPP:mbtitest' %}">MBTI테스트 하기</a>
    <a href="{% url 'MBTIAPP:blog' %}">MBTI 게시판으로</a>
    {% endif %}

    {% if not user.is_authenticated %}
    {% csrf_token %}
    <a href="{% url 'MBTIAPP:login' %}">로그인</a>
    <a href="{% url 'MBTIAPP:signup' %}">회원가입</a>
{% endif %}
  • User가 로그인이 되어있지 않은데, MBTI검사를 시킬 순 없으므로 "user.is_authenticated" 를 사용하여,

  • 로그인이 안된 상태라면, "로그인과 회원가입" 을.

  • 로그인이 된 상태라면, "로그아웃/ MBTI테스트url / MBTI게시판url" 을 보여주게 설정 하였다.


2. MBTITEST / TEST 결과 / MBTI를 Profile에 저장

🫵 MBTITEST + TEST결과

🍊 url

  • 뻔함

🍋 form

class MbtiForm(forms.Form):
    EI = forms.ChoiceField(choices=[('E','네'),('I','아니오')], widget=forms.RadioSelect)
    NS = forms.ChoiceField(choices=[('N','네'),('S','아니오')], widget=forms.RadioSelect)
    TF = forms.ChoiceField(choices=[('F','네'),('T','아니오')], widget=forms.RadioSelect)
    PJ = forms.ChoiceField(choices=[('J','네'),('P','아니오')], widget=forms.RadioSelect)
  • 어려웠음. MBTI 테스트를 하는 템플릿에서 Form형식으로 직접 코드를 짜려했지만, 그 결과를 조합해서 하나의 MBTI를 만들자니, 모든 경우를 if 문으로 처리하는 노가다를 해야할 것 같아서 하기 싫었음.

  • [링크][https://www.geeksforgeeks.org/choicefield-django-forms/]

  • Form의 사용법을 잘 몰랐기에 배울 겸 알아보았는데 위의 링크에서 ChoiceField를 form에서 지정해줄 수 있다고 알려줌.

🍉 view

def result(request):
    if request.method == 'POST':
        form = MbtiForm(request.POST)
        if form.is_valid():
            EI = form.cleaned_data['EI']
            NS = form.cleaned_data['NS']
            TF = form.cleaned_data['TF']
            PJ = form.cleaned_data['PJ']
            arr=[EI, NS, TF, PJ]
            my_mbti = ''.join(arr)
            user = request.user
            user.user_mbti = my_mbti
            user.save()
            context = {'mymbti':my_mbti}
            return render(request, 'MBTIAPP/result.html', context)
else:
    form = MbtiForm()
    context = {'form': form}
    return render(request, 'MBTIAPP/result.html',context)
  • 일단 POST 방식일때, if문 진입. 폼 검사 후, EI 등등의 변수( 이름 마음대로 만들어도 됨 ) 에 아까 회원가입할 때 배운 form.cleaned_data로 form에 입력된 값들을 집어 넣는다.

  • 그 다음 arr이란 리스트에 mbti들을 넣음, 그 후, join을 하여 my_mbti라는 변수를 만들어 거기에 리스트 요소들을 합쳐서 하나의 문자열로 만듬

  • 그 후 저장. 결과 창으로 정보(my_mbti) 담아서 이동

  • 뭔가 점프 투 장고같은 곳에 나오는 뻔한 내용이 아닌 내용을 내손으로 구현해보니 가장 재미있었던 부분.

🍏 templates

<h1>결과창임</h1>
{{ mymbti }}
<a href="{% url 'MBTIAPP:blog' %}">mbti 게시판으로 이동하기</a>
  • 그냥 아까 만든 mymbti를 보여줌..

🫵 MBTI를 Profile에 저장

🍉 view

def result(request):
    if request.method == 'POST':
        form = MbtiForm(request.POST)
        if form.is_valid():
            EI = form.cleaned_data['EI']
            NS = form.cleaned_data['NS']
            TF = form.cleaned_data['TF']
            PJ = form.cleaned_data['PJ']
            arr=[EI, NS, TF, PJ]
            my_mbti = ''.join(arr)
            user = request.user
            user.user_mbti = my_mbti
            user.save()
            context = {'mymbti':my_mbti}
            return render(request, 'MBTIAPP/result.html', context)
    else:
        form = MbtiForm()
        context = {'form': form}
        return render(request, 'MBTIAPP/result.html',context)
  • 보면 알겠지만, 아까 MBTI테스트의 view 코드이다.

  • 어떠한 기능을 구현하는데에 있어 중요한 내용 같아서 따로 씀.

  •   user = request.user
      user.user_mbti = my_mbti
      user.save()
    
  • ❗️ "user" 를 "request.user" 로 지정하여, 현재 이 동작을 수행한 "request.user" 정의 하기.

  • ❗️ 아까 Abstractuser로 만든 Profile이라는 모델에 user_mbti라는 필드를 추가했었다. 여기서 user.user_mbti라고 표기할 수 있는 이유는 Abstractuser는 AUTH로 만든 User를 상속하기 때문.

  • ❗️ 그 후 , 아직 빈칸인 user.user_mbti 에 my_mbti를 집어 넣어줌. (재검사 일시, 새로운 값으로 대체) => 저장함.


3. 게시글 CRUD

🍎 model

class Post(models.Model):
    title =models.CharField(max_length=50)
    poster = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE ,default=1)
    content = models.TextField()

    def __str__(self):
        return self.title
  • 중요한 부분 : Poster라는 필드는 아까 만든 Profile모델을 참조해야함. 그런데, 그냥 Profile을 참조한다고 하면, 어느 부분이 계속 작동을 안해서 ,

  • ❓settings.AUTH_USER_MODEL (AbstractUser를 사용할 때 setting.py에 지정해줌.) 을 가리키면 잘 작동함 (아직까지 이유 모름)

🍉 view

create

def new(request):
    return render(request, 'MBTIAPP/postcreate.html')

def postcreate(request):
    if(request.method == 'POST'):
        post = Post()
        post.title=request.POST['title']
        post.content=request.POST['content']
        post.poster = request.user
        post.save()     
    return redirect('MBTIAPP:blog')
  • 사실 보편적인 기능이라, 구글에 잘 나와있음.

  • postcreate 에서 "post" 를 Post(모델)을 가리키게 하고,

  • Post의 필드들에 POST방식으로 받은 정보들을 저장해준 후,

  • ❗️ Post모델의 poster는 아직 비어있으므로, "request.user" 을 불러와, 지금 이 동작을 수행한 user를 poster에 넣어준다

  • 저장하기!

read

def blog(request):
    posts = Post.objects.all()
    context = {'posts':posts}
    return render(request, 'MBTIAPP/blog.html', context)

def detail(request, pk):
    post_detail = get_object_or_404(Post, pk=pk)
    comments = post_detail.comment_set.all()
    context = {
        'post' : post_detail,
        'comments' : comments
    }
    return render(request, 'MBTIAPP/detail.html', context)
  • 게시글 목록을 보는 기능과, 상세페이지 구현.

  • 먼저, blog 함수 : "posts" 변수에 Post모델 담음. => blog.html로 정보 보냄, 끝.

  • 다음, detail 함수 : "post_detail" 변수 만들고 Post모델 가져옴

  • 여기엔 곧 설명할 댓글도 들어가므로, "comments" 라는 변수에 Comment 모델도 가져온다.

  • render로 context에 담은 정보들 보냄.

update

def edit(request, pk):
    post = Post.objects.get(pk=pk)
    if request.method == 'POST':
        post.title=request.POST['title']
        post.content=request.POST['content']
        post.save()  
        return redirect('MBTIAPP:detail', pk)
    else:
        context = {
            'post' : post
        }
        return render(request, 'MBTIAPP/edit.html', context)
  • "post" 라는 변수에 Post모델 가져옴.

  • POST 방식일 떄 (수정하기 버튼을 눌러 제출할 떄)는, post의 객체들을 그냥 바꿔주고 저장!!

  • GET 방식일 때 (수정하려고 들어왔을 때)는, 원래 쓰여있던 제목/글 들 (post) 를 보여줌.

delete

def delete(request, pk):
    post = Post.objects.get(pk=pk)
    post.delete()
    return redirect('MBTIAPP:blog')
  • 간단하다. post의 pk 를 받아와서, post 지정해주고 그냥 지워주면 됨..

🍏 templates

Create

<form action="{% url 'MBTIAPP:postcreate' %}" method="POST">
{% csrf_token %}
    <label for="title">제목</label><br>
    <input type="text" name="title", id="title"><br>
    <label for="content">글 내용</label><br>
    <textarea name="content" id="content" cols="40" rows="20"></textarea><br>
    <input type="submit" value="작성하기">
</form>
  • form 태그를 이용해 정보 입력 후 제출

  • ❗️ input의 name이 중요함. 이 name으로 view에서 POST방식으로 전달됨. ❗️

Read

{% for post in posts %}
<div class="posts">
    <h2>글 제목</h2>
    <a href="{% url 'MBTIAPP:detail' post.pk %}">{{post.title}}</a>
    <h2>작성자</h2>
    <p>{{post.poster.nickname}}<span>({{ post.poster.user_mbti }})</span></p>
</div>   
{% endfor %}
  • 게시글 리스트 페이지, for문을 돌려 post라는 변수를 이용해 posts안에 담긴 게시글의 수만큼 반복 됨.

      <h1>디테일페이지</h1>
      <div class="details">
          <a href="{% url 'MBTIAPP:blog' %}">게시판으로 돌아가기</a><br>
          <h2>제목</h2>
          <p>{{ post.title }}</p>
          <h2>내용</h2>
          <p>{{ post.content }}</p>
          <h2>작성자</h2>
          <span>{{ post.poster }}({{ post.poster.user_mbti }})</span><br><br><br>
          <a href="{% url 'MBTIAPP:edit' post.pk %}">수정하기</a>
          <a href="{% url 'MBTIAPP:delete' post.pk %}">삭제하기</a>
      </div>
    
  • Detail 페이지 인데, 게시글 제목, 내용 보여주고 작성자 닉네임, mbti를 보여줌.

Update는 유사하고 간단해서 생략.

Delete는 템플릿 필요없음.


4. 댓글 CRUD

🍎 model

class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
commenter = models.ForeignKey(settings.AUTH_USER_MODEL , on_delete=models.CASCADE)
content = models.CharField(max_length=150)

    def __str__(self):
        return self.content
  • Post와 비슷하다.

  • 다른점 : Comment에서는 어느 Post에 달린 댓글인지를 구분해야 하기 때문에, post라는 필드를 만들어 Post모델을 외래키로 참조한다.

🍋 form

class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        exclude = ('post','commenter')
  • 댓글을 작성할 때, 댓글 내용만 입력받으면 되니, Comment모델에서 post와 commenter는 exlude로 빼줌.
  • form태그로 html상에서 처리할 수 있을 것 같지만 Form.py를 이용하는 개념이 애매해서 공부할 겸 form으로 만듬.

🍊 url

path('<int:pk>/comment/', views.comment_create, name='comment_create'),
path('<int:post_pk>/comment/<int:comment_pk>/delete/', views.comment_delete, name='comment_delete'),
path('<int:post_pk>/comment/<int:comment_pk>/edit/', views.comment_edit, name='comment_edit'),
  • ❗️ 중요한듯 하다. 보면, comment_create의 url은 pk/comment 형식 . pk는 post의 pk이다.

  • ❗️ 핵심은 , delete와 edit은 댓글을 건드리는 url들 인데, 어느 게시물의 댓글인지 구분해야 하므로, post의 pk 와 comment의 pk를 둘다 url에 집어 넣어야 한다.

🍉 view

create

def comment_create(request, pk):
if request.user.is_authenticated:
    post = get_object_or_404(Post, pk=pk)
    form = CommentForm()
    if request.method == 'POST':
        comment_form = CommentForm(request.POST)
        if comment_form.is_valid():
            comment = comment_form.save(commit=False)
            comment.post = post
            comment.commenter = request.user
            comment.save()
            return redirect('MBTIAPP:detail', pk)
    else:
        form = CommentForm()
    return render(request, 'MBTIAPP/detail.html', {'post':post, 'form': form})
  • 헷갈려서 시간이 꽤나 걸림.. pk를 주고 받는 동작에 대한 공부가 잘 되었음.

  • 먼저 is_authenticated (로그인상태) 일때, "post"라는 변수에 Post모델 가져옴.

  • 처음 POST방식 으로 들어갔을 때, "comment_form"이라는 변수에, CommentForm을 가져옴. => 폼 검사 후,

  • comment의 필드들에 필요한 정보들을 담는다.

  • comment.post에는 이 글이 어떤 post인지 알기 위해 post를 담아줌

  • commenter 에는 이 댓글을 단 사람이 누구인지 DB에 넣어주기 위해 request.user를 저장함.

  • GET방식 일땐 빈폼을 보여줌.

  • ❓ 의문인점) 위에 있는 form = CommentForm() 을 쓰기전에는 오류가 떠서 알아보니, if문 (POST방식)으로 들어가기 전에 form을 정의해주어야 한다고 함.

  • ❓ 그건 else문안의 GET방식에서 form = CommentForm() 입력해 두었으니 GET으로 받고 입력하고 POST보내도 되는거 아닌가?...

delete

def comment_delete(request, post_pk, comment_pk):
    if request.user.is_authenticated:
        comment = get_object_or_404(Comment, pk=comment_pk)
        if request.user == comment.commenter:
            comment.delete()
    return redirect('MBTIAPP:detail', post_pk) 
  • post의 pk 와 comment의 pk 를 동시에 생각하느라 공부가 되었다.

  • 어느 게시물의 어느댓글을 지울건지 알아야 하기 때문에, post와 comment의 pk를 둘다 인자로 받음.

  • "comment"라는 변수에 Comment모델 받아옴, 당연히 이때 pk는 해당 댓글의 pk

  • if 문으로 들어가서, 지금 이 동작을 수행하는 user와 comment를 작성한 user가 같다면, 지우게 실행. (근데 이건 어차피 html상에서 본인이 쓴 댓글이 아니면 삭제버튼 자체가 안뜨게 구현해놔서 큰 의미는 없지만 그냥 작성했음)

  • 그리고 동작이 끝나면, detail페이지로 이동.

  • ❗️ detail페이지로 이동할 땐 post의 디테일 페이지 이므로, post_pk를 보내준다 ! comment_pk는 필요없음!

update는 유사해서 생략

🍏 templates

<h3>댓글들</h3>
{% if comments %}
<p>{{ comments|length }}개의 댓글이 있어요</p>
{% endif %}
<ul>
    {% for comment in comments %}
    <li>
        {{ comment.content }} - {{ comment.commenter }}<span>({{ comment.commenter.user_mbti }})</span>
        {% if user == comment.commenter %}
            <form action="{% url 'MBTIAPP:comment_delete' post.pk comment.pk %}">
            {% csrf_token %}
            <br><div class="buttons"><input type="submit" value="댓글삭제">
            </div>
            </form>
            <a href="{% url 'MBTIAPP:comment_edit' post.pk comment.pk %}">수정하기</a>
        {% endif %}
    </li>
    {% empty %}
        <p>No 댓글~</p>
    {% endfor %}
</ul>
  • 아까 comment라는 변수에 정보들을 담았으므로, if문을 이용해, comments에 접근.

  • length를 이용해 comments 안에 담긴 댓글 개수를 추출.

  • for문을 돌려, comments에 담긴 댓글 수 만큼 반복.

  • 댓글 내용 , 닉네임 , mbti 표기.

  • if문으로 들어가서, user가 댓글 작성자와 같을 경우, 댓글삭제와 수정하기 버튼이 보이게 함.

  • 구글링하다보니, empty 라는 것도 있길래 써봄.

  • ❗️ delete와 edit은 아까 말했듯이, post와 comment의 pk 둘다 필요로 하므로, 둘다 인자로 보내줘야함.


API 명세서

URL1 URL2 METHOD 설명
/main GET 메인 페이지
/signup POST 회원가입
/login POST 로그인
/logout POST 로그아웃
/form_errors GET 인증에러
/mbti_test POST mbti검사페이지
/result GET 검사결과페이지
/postcreate POST 게시글 작성
/blog GET 게시글 리스트
/new GET 게시글 작성 페이지로 이동
/int:pk GET 게시글 상세페이지
/int:pk /edit POST 게시글 수정페이지
/int:pk /delete POST 게시글 삭제
/int:pk /comment POST 댓글 작성
/int:post_pk/comment /int:comment_pk/delete POST 댓글 삭제
/int:post_pk/comment /int:comment_pk/edit POST 댓글 수정

❗️회고

-수현

그럴듯한 기능들을 직접 만들어보니, 내가 장고를 얼마나 얕게 알고 있었는지 깨닫게 되었고, 이 과제 하나로 엄청난 양의 공부를 하게 되었고, readme 적으면서 또 한번 정리하니 좋았다.

엉터리인 블로그들이 많다는 걸 다시 느낀다...

중요한 점들이나 의문점들은 위에 다 적어놨음.

-진욱

장고를 한번 짧게 건드려 보긴 했지만, 직접 코드를 짜보는 과정을 거치며, 많이 배웠고, 몰랐던 기능들이나 작동방식들이 흥미로웠다.


질의응답에 대한 답변

Q : csrf_token 말고도 다른 보안 기법에 대해 알아보세요

A : 꽤 어려운 주제라, 이러한 것들이 있다 정도만 알아 보았습니다.

  • XSS 보호
    => XSS공격이란, 공격자가 악성 스크립트를 다른 사용자의 브라우저에 삽입하는 것을 말합니다.
    그래서 , 장고의템플릿에는 대부분의 XSS공격으로 부터 보호할 수 있는 기능이 있기에, 특수한 상황을 제외하고는 안전하다고 합니다.

  • SQL 주입 보호
    => SQL주입 공격은, 악의적 사용자가 데이터베이스에서 임의로 sql코드를 실행할 수 있는 공격유형입니다.
    그래서, 장고는 쿼리 매개변수화를 사용해서 쿼리를 구성하여, SQL주입으로 부터 보호된다고 합니다.

  • 클릭재킹 보호
    => 클릭재킹 공격은, 악의적인 사이트가 다른 사이트를 프레임으로 감싸는 공격입니다. 이공격을 통해, 일반사용자가 대상 사이트에서 의도치않은 작업을 수행하도록 속일 수 잇다. 그래서 , 장고는 클릭재킹방지 기능을 브라우저에 제공합니다.

  • 여기까진 3개의 대표적인 장고의 내장된 기능들이고 , 개발을 하며 저희가 보안에 신경쓸 수 있는점은,
    파이썬 코드가 웹서버의 루트 외부에 있어야되므로 확인해야합니다.
    => 외부에 있어야 코드가 일반 텍스트로 제공되거나 실수로 실행되는 사고를 막을 수 있습니다

  • 또, 장고는 사용자 인증 요청을 조절하지 않기 때문에, 인증 시스템에 대한 공격으로 부터 보호하기 위해 다른 웹서버 인증 보안 모듈을 사용해야 한다고 합니다.
    그래서 그 예시를 찾아봤는데 , 아파치 보안 모듈 , nginx 보안 모듈, HAProxy 모듈등을 이용한 예시가 존재합니다.

Q : manage.py 뒤에 붙는 명령어 들을 나열해보세요

A : makemigrations , migrate , runserver, createsuperuser, startproject, startapp ...

  • 이 외에도 쉘로 DB를 조작할 수 있는 => python manage.py shell,
    정적 파일들을 하나의 경로에 모아주는 => python manage.py collectstatic,
    migration들을 보여주는 => python.manage.py showmigrations,

이외에도 더 존재하지만, 알아보니 좀 딥한 내용들이라 여기까지만 알아보았습니다.

Q : pk에 대해 알아보세요

A : 넵

  • 저번시간에 준혁/승환님 팀에서 설명해 주셨는데,

  • Pk 는 primary key의 줄임말로, 기본키라고 부릅니다. 모델에서 장고는 따로 pk를 정의하지 않아도 id필드를 기본키로 사용합니다. 물론 따로 지정해줄 수 도 있습니다.

  • pk는 주로 url매개변수에 사용되는데, 해당 기본키에 해당하는 페이지를 호출함으로써, 키별로 다른 페이지를 호출할 수 있습니다

  • 그래서 궁금해진게 결국 id랑 pk는 pk를 따로 바꿔주지 않는 이상 동일한데 왜 굳이 pk를 사용하여 url매개변수로 이용하나 싶어서 알아보았는데,

  • id를 사용해도 문제는 없지만, 장고 모델에 대한 일관성있는 코드를 위해 pk를 사용한다고 합니다.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages