TIL 05-04

김덕협·2026년 5월 4일

TIL

목록 보기
4/41

📅 2026-05-04 (월)


✍️ 공부한 내용

DB 中 N:1 (2) - User와 Article(게시글), User와 Comment(댓글) 간의 N:1 관계 정의하기


(참고)Django 시작 시 순서

  1. settings.py 읽음 <= AUTH_USER_MODEL 문자열은 여기서 사용 가능
  2. 모델들 로드 시작
  3. accounts.User 클래스 만들어짐 <= get_user_model() 가능 시점
  4. articles.Article 만들어짐
  5. 모든 모델 로드 완료
  6. View 함수들 실행 시작

1. 모델 관계 설정 (Article -user) (N:1)

class Article(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    # User 모델을 직접 import해서 사용하지 않는다.
    # 'accounts.User'라는 문자열만 들고 있다가 나중에 모든 모델이
    # 다 로드된 이후에 진짜 User 클래스를 찾아서 연결
    # ==> '지연 평가' (효율 및 최적화 굿)
    # ORM에서도 지연평가 쓰임. Article.objects.all() 이 코드 자체는 DB에 요청을 보내지 않음
    # list() 로 형변환 하거나 for로 반복하거나 실제 데이터 활용할 때 평가 진행 
    
    title = models.CharField(max_length=10)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
  • User 모델을 직접 import 하지 않는 이유
    • Aritcle 클래서 생성 시점이 User 클래스 생성 시점보다 빠른 경우 외래 키 설정에서 참고할 User 모델을 찾지 못해서 에러가 발생할 수도 있음.
    • models.py에서는 안정적인 참조가 필요하기에 settings.AUTH_USER_MODEL이라는 문자열을 참조하게 하였다가 모든 모델이 로드 된 이후에 진짜 User 클래스를 연결한다.

  • User 모델을 참조하는 2가지 방법
    • settings.AUTH_USER_MODEL : models.py에서만 사용 (반환 값 : account.User -> 이건 문자열)
    • get_user_model() : models.py를 제외한 다른 모든 위치에서 사용 (반환 값 : User Object -> 이건 객체)

  • 이렇게 설정한 후 Migration시 발생하는 에러
  $ python manage.py makemigrations

It is impossible to add a non-nullable field 'user' to article without specifying a default.
This is because the database needs something to populate existing rows.
Please select a fix: 
1) Provide a one-off default now (will be set on all existing rows with a null value for
this column)
2) Quit and manually define a default value in models.py.
Select an option:

=> 기존에 테이블이 있는 상황에서 필드를 추가하려 해서 발생하는 것, 모든 필드는 NOT NULL이 기본 제약 조건으로 설정되어 있기에 데이터 없이 새로운 필드를 추가하지 못해서 발생하는 에러다.

2. 게시글 CRUD 구현


게시글 CREATE

  • 새 게시글 작성 시 User 모델에 대한 사용자 입력 창이 나오지만, 나오지 않아야 할 입력이 나온다. (User, 다른 사람 선택 시 문제가 된다.)


  • 따라서 글 작성자는 보이지 않게 기존 ArticleForm에서 글 작성자를 선택할 수 없게(안 보이게) 수정해야 한다.
# articles/forms.py
class ArticleForm(forms.ModelForm):
	class Meta:
    	model = Article
        fields = ('title', 'content',)
        
        # exclude = ('user',) 이 방식으로도 가능

이대로 작성 시 user_id 필드에 데이터가 누락되어서 NOT NULL 제약 조건에 위배되기에 에러가 발생함.

  • 따라서 게시글 작성 시 작성자 정보가 함께 저장되도록 save의 commit 옵션을 활용해야 한다.
from django.contrib.auth.decorators import login_required

@login_required
def create(request):
	if request.method == 'POST':
    	form = ArticleForm(request.POST)
        if form.is_valid():
        	article = form.save(commit=False)
            article.user = request.user
            article.save()
            return redirect('articles:detail', article.pk)
        else:
        	. . . 

게시글 READ

  • index와 detail 페이지에 작성자 정보가 출력되도록 수정하자.
<!-->  articles/index.html  <-->
{% extends 'base.html' %}

{% block content %}
  <h1>INDEX</h1>
  {% if request.user.is_authenticated %}
    <p>Hello, {{ user.username }}</p>
    <a href="{% url "articles:create" %}">CREATE</a>
    <form action="{% url "accounts:logout" %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="로그아웃">
    </form>
    <form action="{% url "accounts:delete" %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="회원탈퇴">
    </form>
    <a href="{% url "accounts:update" %}">회원정보 수정</a>
  {% else %}
    <a href="{% url "accounts:login" %}">로그인</a> |
    <a href="{% url "accounts:signup" %}">회원가입</a>
  {% endif %}

  <hr>
  
  {% for article in articles %}
    <div>
      <div>작성자: {{article.user.username}} </div>
      <div>글 번호: {{ article.pk }}</div>
      <div>
        글 제목: <a href="{% url "articles:detail" article.pk %}">{{ article.title }}</a>
      </div>
    </div>
    <hr>
  {% endfor %}
{% endblock content %}

<!-->  articles/detail.html  <-->
{% extends 'base.html' %}

{% block content %}
  <h1>DETAIL</h1>
  <h2>{{ article.pk }} 번 글</h2>
  <hr>
  <p>제목: {{ article.title }}</p>
  <p>내용: {{ article.content }}</p>
  <p>작성일: {{ article.created_at }}</p>
  <p>수정일: {{ article.updated_at }}</p>
  <hr>
  {% if request.user == article.user %}
    <a href="{% url "articles:update" article.pk %}">EDIT</a>
    <form action="{% url "articles:delete" article.pk %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="DELETE">
    </form>
  {% endif %}

  {% comment %} 댓글 출력 {% endcomment %}
  <ul>
    {% for comment in comments %}
      <li>
        {{ comment.user.username }}- {{ comment.content }}
        {% if request.user == comment.user %}
        <form action="{% url "articles:comments_delete" article.pk comment.pk %}" method="POST">
          {% csrf_token %}
          <input type="submit" value="삭제">
        </form>
        {% endif %}
      </li>
    {% endfor %}
  </ul>

  <hr>
  {% comment %} 댓글 작성 form {% endcomment %}
  <form action="{% url "articles:comments_create" article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form }}
    <input type="submit">
  </form>

  <a href="{% url "articles:index" %}">메인 페이지로 돌아가기</a>
{% endblock content %}


게시글 UPDATE

  • 게시글 수정 요청자와 작성 사용자를 비교해서 본인 게시글만 수정 가능하도록 해야함.
# articles/views.py

@login_required
def update(request, article_id):
    article = Article.objects.get(pk=article_id)
    if request.user == article.user:
        if request.method == 'POST':
            form = ArticleForm(request.POST, instance=article)
            if form.is_valid():
                form.save()
                return redirect('articles:detail', article.pk)
        else:
            form = ArticleForm(instance=article)

    else:
        return redirect('articles:index')
    context = {
        'form': form,
        'article': article,
    }
    return render(request, 'articles/update.html', context)

<!--> articles/detail.html <!-->
{% extends 'base.html' %}

{% block content %}
  <h1>DETAIL</h1>
  <h2>{{ article.pk }} 번 글</h2>
  <hr>
  <p>제목: {{ article.title }}</p>
  <p>내용: {{ article.content }}</p>
  <p>작성일: {{ article.created_at }}</p>
  <p>수정일: {{ article.updated_at }}</p>
  <hr>
  {% if request.user == article.user %}
    <a href="{% url "articles:update" article.pk %}">EDIT</a>
    <form action="{% url "articles:delete" article.pk %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="DELETE">
    </form>
  {% endif %}

  {% comment %} 댓글 출력 {% endcomment %}
  <ul>
    {% for comment in comments %}
      <li>
        {{ comment.content }}
        <form action="{% url "articles:comments_delete" article.pk comment.pk %}" method="POST">
          {% csrf_token %}
          <input type="submit" value="삭제">
        </form>
      </li>
    {% endfor %}
  </ul>

  <hr>
  {% comment %} 댓글 작성 form {% endcomment %}
  <form action="{% url "articles:comments_create" article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form }}
    <input type="submit">
  </form>

  <a href="{% url "articles:index" %}">메인 페이지로 돌아가기</a>
{% endblock content %}


게시글 DELETE

  • 삭제 요청자와 게시글 작성자가 동일할 때 삭제하게 해야함.
@login_required
def delete(request, article_id):
    article = Article.objects.get(pk=article_id)
    if request.user == article.user:
        article.delete()    
    return redirect('articles:index')


3. 댓글 CRUD 구현

댓글 CREATE

# article/views.py
# 댓글 작성 view
def comments_create(request, article_id):
    # 1. 어떤 게시글에 작성되는 건지 게시글을 먼저 조회
    article = Article.objects.get(pk=article_id)

    # 2. 사용자로부터 입력받은 댓글 내용을 확인
    comment_form = CommentForm(request.POST)

    # 3. 유효성 검사
    if comment_form.is_valid():
        # 우리에게 필요한건 댓글이 실제 db에 저장되기 전에 외래 키 데이터가 채워져야 함
        # 그런데 외래 키 데이터를 넣으려면 댓글 객체가 필요함.
        # 하지만 댓글 객체는 save 이후에 생성됨. 그런데 우리는 save 전에 처리를 해야 함
        # 그래서 django modelform의 save메서드는 우리에게 옵션(commit)을 하나 줌
        # save의 commit옵션을 False로 변경하면
        #  ==> 우리에게 댓글 객체 인스턴스는 제공은 하되, 아직 db에 최종 저장 요청은 하지 않음
        comment = comment_form.save(commit=False)

        # 외래 키 데이터를 준비
        comment.article = article
        comment.user = request.user

        # 4. 저장
        comment.save()
        # 5. 저장 후 detail 페이지로 리다이렉트
        return redirect('articles:detail', article.pk)
    # 6. 유효성 검사가 실패했다면 에러메세지와 함께 상세 페이지를 다시 응답
    context = {
        'comment_form': comment_form,
        'article': article,
    }
    return render(request, 'articles/detail.html', context)

<!--> article/detail.html  <-->
{% extends 'base.html' %}

{% block content %}
  <h1>DETAIL</h1>
  <h2>{{ article.pk }} 번 글</h2>
  <hr>
  <p>제목: {{ article.title }}</p>
  <p>내용: {{ article.content }}</p>
  <p>작성일: {{ article.created_at }}</p>
  <p>수정일: {{ article.updated_at }}</p>
  <hr>
  {% if request.user == article.user %}
    <a href="{% url "articles:update" article.pk %}">EDIT</a>
    <form action="{% url "articles:delete" article.pk %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="DELETE">
    </form>
  {% endif %}

  {% comment %} 댓글 출력 {% endcomment %}
  <ul>
    {% for comment in comments %}
      <li>
        {{ comment.user.username }}- {{ comment.content }}
        <form action="{% url "articles:comments_delete" article.pk comment.pk %}" method="POST">
          {% csrf_token %}
          <input type="submit" value="삭제">
        </form>
      </li>
    {% endfor %}
  </ul>

  <hr>
  {% comment %} 댓글 작성 form {% endcomment %}
  <form action="{% url "articles:comments_create" article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form }}
    <input type="submit">
  </form>

  <a href="{% url "articles:index" %}">메인 페이지로 돌아가기</a>
{% endblock content %}


댓글 DELETE

  • 댓글 삭제 요청자와 댓글 작성자가 동일해야 삭제 가능하게 해야함.
# 댓글 삭제 view
def comments_delete(request, article_id, comment_id):
    # 1. 몇번 댓글 지우는지 조회
    comment = Comment.objects.get(pk=comment_id)

    if request.user == comment.user:
        # 2. 조회한 댓글 삭제
        comment.delete()

    # 3. 삭제 후 게시글 상세 페이지로 redirect
    return redirect('articles:detail', article_id)

<!-->   article/detail.html   <-->
{% extends 'base.html' %}

{% block content %}
  <h1>DETAIL</h1>
  <h2>{{ article.pk }} 번 글</h2>
  <hr>
  <p>제목: {{ article.title }}</p>
  <p>내용: {{ article.content }}</p>
  <p>작성일: {{ article.created_at }}</p>
  <p>수정일: {{ article.updated_at }}</p>
  <hr>
  {% if request.user == article.user %}
    <a href="{% url "articles:update" article.pk %}">EDIT</a>
    <form action="{% url "articles:delete" article.pk %}" method="POST">
      {% csrf_token %}
      <input type="submit" value="DELETE">
    </form>
  {% endif %}

  {% comment %} 댓글 출력 {% endcomment %}
  <ul>
    {% for comment in comments %}
      <li>
        {{ comment.user.username }}- {{ comment.content }}
        {% if request.user == comment.user %}
        <form action="{% url "articles:comments_delete" article.pk comment.pk %}" method="POST">
          {% csrf_token %}
          <input type="submit" value="삭제">
        </form>
        {% endif %}
      </li>
    {% endfor %}
  </ul>

  <hr>
  {% comment %} 댓글 작성 form {% endcomment %}
  <form action="{% url "articles:comments_create" article.pk %}" method="POST">
    {% csrf_token %}
    {{ comment_form }}
    <input type="submit">
  </form>

  <a href="{% url "articles:index" %}">메인 페이지로 돌아가기</a>
{% endblock content %}


4. View decorators

: View 함수의 동작을 수정하거나 추가 기능을 제공하는 데 사용되는 Python 데코레이터

  • 종류
    • Allowed HTTP methods : 뷰가 허용하는 HTTP 요청 방식 (GET, POST 등)을 제한
    • Conditional view processing
    • GZip compression

1) Allowed HTTP methods

  • require_http_methods()
from django.views.decorators.http import require_http_methods

@login_required
@require_http_methods(['GET', 'POST'])

def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save(commit=False)
            article.user = request.user
            article.save()
            return redirect('articles:detail', article.pk)
    else:
        form = ArticleForm()
    context = {
        'form': form,
    }
    return render(request, 'articles/create.html', context)

  • require_safe()
from django.views.decorators.http import require_http_methods, require_safe

@require_safe
def index(request):
    articles = Article.objects.all()
    context = {
        'articles': articles,
    }
    return render(request, 'articles/index.html', context)
    
    
@require_safe
def detail(request, article_id):
    article = Article.objects.get(pk=article_id)
    comment_form = CommentForm()
    # 현재 게시글에 작성된 모든 댓글을 조회 (역참조, 1 ==> N)
    comments = article.comment_set.all()
    context = {
        'article': article,
        'comment_form': comment_form,
        'comments': comments,
    }
    return render(request, 'articles/detail.html', context)
    


  • require_post
@login_required
@require_POST
def delete(request, article_id):
    article = Article.objects.get(pk=article_id)
    if request.user == article.user:
        article.delete()    
    return redirect('articles:index')
    
    
@require_POST
def comments_create(request, article_id):
    # 1. 어떤 게시글에 작성되는 건지 게시글을 먼저 조회
    article = Article.objects.get(pk=article_id)

    # 2. 사용자로부터 입력받은 댓글 내용을 확인
    comment_form = CommentForm(request.POST)

    # 3. 유효성 검사
    if comment_form.is_valid():
        # 우리에게 필요한건 댓글이 실제 db에 저장되기 전에 외래 키 데이터가 채워져야 함
        # 그런데 외래 키 데이터를 넣으려면 댓글 객체가 필요함.
        # 하지만 댓글 객체는 save 이후에 생성됨. 그런데 우리는 save 전에 처리를 해야 함
        # 그래서 django modelform의 save메서드는 우리에게 옵션(commit)을 하나 줌
        # save의 commit옵션을 False로 변경하면
        #  ==> 우리에게 댓글 객체 인스턴스는 제공은 하되, 아직 db에 최종 저장 요청은 하지 않음
        comment = comment_form.save(commit=False)

        # 외래 키 데이터를 준비
        comment.article = article
        comment.user = request.user

        # 4. 저장
        comment.save()
        # 5. 저장 후 detail 페이지로 리다이렉트
        return redirect('articles:detail', article.pk)
    # 6. 유효성 검사가 실패했다면 에러메세지와 함께 상세 페이지를 다시 응답
    context = {
        'comment_form': comment_form,
        'article': article,
    }
    return render(request, 'articles/detail.html', context)

# 댓글 삭제 view
@require_POST
def comments_delete(request, article_id, comment_id):
    # 1. 몇번 댓글 지우는지 조회
    comment = Comment.objects.get(pk=comment_id)

    if request.user == comment.user:
        # 2. 조회한 댓글 삭제
        comment.delete()

    # 3. 삭제 후 게시글 상세 페이지로 redirect
    return redirect('articles:detail', article_id)


5. ERD - Entity-Relationship Diagram

: 데이터베이스의 구조를 시각적으로 표현하는 도구

구성요소 : 엔티티 / 속성 / 관계


  • 개체와 속성 표현 방법


  • Cardinality : 한 엔티티와 다른 엔티티 간의 수적 관계를 나타내는 표현 (일대일 / 다대일 / 다대다)



profile
뭘봐

0개의 댓글