본문 바로가기
SK Shieldus Rookies 19th/인프라 활용을 위한 파이썬

[SK shieldus Rookies 19기][Django] - Django 3

by En_Geon 2024. 3. 24.

 

1. 스타일 적용

 

웹 페이지를 만들 때 세 가지 요소가 있다.

 

구분 HTML CSS JavaScript
역할 DATA Style Action

 

HTML은 데이터를 나타내는 데 구조적으로 표현하기 위한 것이다. 데이터는 태그를 둘러싸여 있는데 기계가 해석하기에는 편한데 사람이 보기에는 어렵다. 그래서 이것을 구조와 형식을 만들어주는 CSS로 스타일을 만드는 것이다.

 

CSS는 해당하는 문서의 구조, 형식, 색깔, 배치, 모양을 다루는 것이다. 데이터들을 시각화해서 사람이 쉽게 해석할 수 있도록 하는 것이 CSS에서 스타일을 만드는 것이다. 기계는 태그를 통해 해석하는 반면에, 사람은 시각적인 데이터를 가지고 해석한다. 그래서 CSS는 사람을 위한 것이다.

 

Java Script는 동작을 처리하기 위해 사용한다. 모든 것들을 서버 사이드에서 다 만들어서 전달 해도 되지만 그렇게 하면 너무 많은 것들을 서버에서 만들어야 하므로 일정 수준을 브라우저에 위임시키는 것이다.

 

여기서는 스타일 적용을 위해 CSS를 살펴본다.

 

1) style.css 적용

C:\python\project\mysite\satic\style.css

 

 

textarea{
    width: 100%;
}
input[type=submit] {
    margin-top: 10px;
}

 

{ }(중괄호) 앞에 나오는 것을 선택자라고 한다. CSS 형태는 "선택자 {적용할 스타일; 적용할 스타일; ...}"으로 되어 있다.

CSS와 선택자에 대한 내용은 이곳에 자세히 설명되어 있으니 꼭 읽어보는 것을 추천한다.

 

textarea는 요소(태그) 선택자인데, textarea 태그에 적용할 스타일을 지정하는 것이다.

 

input은 속성 선택자이다. input 태그 중 type 값이 submit인 태그에 적용할 스타일을 지정하는 것이다.

 

2) config\settings.py에 스태틱 디렉터리 등록

스태틱 파일이 위치하는 곳을 설정 파일에 등록

 

STATIC_URL = '/static/'

STATICFILES_DIRS = [
	BASE_DIR / 'static', 
]

 

 

3) 질문 상세 페이지(question_detail.html)에 스타일 적용

 

{% load static %}			<!-- 추가 -->
<link rel="stylesheet" type="text/css" href="{%static 'style.css'}">

<h1>{{ question.subject }}</h1>

<div>
    {{ question.content }}
</div>

<!-- 답변 내용 출력 -->
<h5>{{ question.answer_set.count }}개의 답변이 있습니다.</h5>
<div>
    <ul>
        {% for answer in question.answer_set.all %}
            <li>{{ answer.content }}</li>
        {% endfor %}
    </ul>
</div>

<form action="{% url 'pybo:answer_create' question.id %}" method="post">
    {% csrf_token %}
    <textarea name="content" id="content" rows="15"></textarea>
    <input type="submit" value="답변 등록"/>
</form>

 

스태틱 디렉터리에 있는 style.css를 사용하겠다는 것을 정의한 것이다.

 

4) 브라우저 확인

 

스타일 확인

 

답변 내용을 입력하는 창(textarea 태그)의 넓이가 화면의 가로 100%를 차지하고 답변 등록 버튼(submit 버튼)의 위쪽 바깥 여백이 10px 추가된 것을 확인할 수 있다.

 

 

2. 부트스트랩

부트스트랩 다운로드

 

위와 같이 태그를 하나하나 입력할 수도 있지만, 페이지가 커지면 태그 하나하나 스타일을 정하기는 쉽지 않다. 그래서 잘 정리되어 있는 스타일 파일을 가져와 사용하는 것이 효율적이다.

 

미리 정리되어 있는 속성들을 이용해서 세팅만 하면 원하는 형태의 화면이 쉽게 만들어질 수 있다.

 

다운받은 압축파일에서 bootstrap.min.css 파일을 static 디렉터리에 복사한다.

 

1) 질문 목록 템플릿(question_list.html)에 부트스트랩 스타일 적용

 

{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}" >

<div class="container my-3">
    <table class="table">
        <thead>
            <tr class="thead-dark">
                <th>번호</th>
                <th>제목</th>
                <th>작성일시</th>
            </tr>
        </thead>
        <tbody>
            {% if question_list %}
                {% for question in question_list %}
                    <tr>
                        <td>{{ forloop.counter }}</td>
                        <td>
                            <a href="{% url 'pybo:detail' question.id %}">
                            {{ question.subject }}
                            </a>
                        </td>
                        <td>{{ question.create_date }}</td>
                    </tr>
                {% endfor %}
            {% else %}
                <tr>
                    <td colspan="3">질문이 없습니다.</td>
                </tr>
            {% endif %}
        </tbody>
    </table>
</div>

 

 

class들을 일정한 규칙으로 미리 정의 되어 있는 것을 적용하면 그 클래스에 맞는 스타일이 자동으로 입혀지도록 만들어져 있다.

 

container는 화면 전체를 감싼다는 의미다. m은 margin(마진)이다. margin은 콘텐츠와 박스 요소 테두리 선 바깥쪽의 간격을 뜻한다. 이때 박스 요소의 바깥쪽 테두리에는 Top, Bottom, left, right가 있는데, m만 사용하면 네 방향, mx는 왼쪽, 오른쪽, my는 위, 아래를 말한다. 그래서 my-3을 사용하면 3px만큼 여백을 준다는 의미가 된다.

 

table은 도표가 나오는 것이다. 도표에는 항상 타이틀이 나오고 내용이 나오는데 이를 지정하는 부분이 thead가 된다.

 

tr은 테이블의 row 데이터 가로줄을 나타내고, th는 테이블의 column 데이터 세로줄을 나타내고, td는 테이블의 내용(데이터)을 나타낸다. tr은 table row의 약자, th는 table head의 약자, td는 table data의 약자다.

 

thead-dark는 헤더의 배경색을 어두운색으로 준다는 의미다.

색깔 같은 경우에는 색깔 코드를 직접 입력하지 않고 부트스트랩에서 지정한 단어를 사용하면 그 단어에 지정된 색깔을 넣어준다. 이 색깔의 정의는 부트스트랩에서 볼 수 있다.

 

tbody는 실제 데이터가 들어가는 것이다. 

 

(1) 브라우저 확인

 

브라우저 확인

 

 

2) 질문 상세 페이지(question_detail.html) 부트스트랩 스타일 적용

 

{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">

<div class="container my-3">
	<h2 class="border-bottom py-2">{{ question.subject }}</h2>

	<div class="card my-3">
		<div class="card-body">
			<div class="card-text" style="white-space: pre-line;">
				{{ question.content }}
			</div>
			<div class="d-flex justify-content-end">
				<div class="badge badge-light p-2">
					{{ question.create_date }}
				</div>
			</div>
		</div>
	</div>

	<h5 class="border-bottom my-3 py-2">
		{{ question.answer_set.count }}개의 답변이 있습니다.
	</h5>
	{% for answer in question.answer_set.all %}
	<div class="card my-3">
		<div class="card-body">
			<div class="card-text" style="white-space: pre-line;">
				{{ answer.content }}
			</div>
			<div class="d-flex justify-content-end">
				<div class="badge badge-light p-2">
					{{ answer.create_date }}
				</div>
			</div>
		</div>
	</div>
	{% endfor %}

	<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
		{% csrf_token %}
		<div class="form-group">
			<textarea name="content" id="content" rows="15" class="form-control"></textarea>
		</div>
		<input type="submit" value="답변 등록" class="btn btn-primary"/>
	</form>
</div>

 

my는 박스 요소의 바깥 위, 아래 여백이고, py는 박스 요소의 안쪽 여백을 의미한다.

 

card라는 것은 카드 형식의 출력을 나타낼 때 사용한다. card의 설명은 부트스트랩 Components Card에서 볼 수 있다.

 

pre-line은 연속된 공백 문자를 하나로 합친다는 의미다.

 

d-flex justify-content-end는 오른쪽 정렬을 의미한다.

 

badge는 작성된 날짜가 회색 바탕으로 나오는 것을 볼 수 있다.

 

form-group은 form element(요소)들을 묶어 주는 역할을 한다. form element는 form 태그 안에 있는 사용자 입력을 받을 수 있는 태그들을 말한다.

 

(1) 브라우저 확인

 

브라우저 확인

 

 

 

3. HTML5 표준에 맞춰서 변경

HTML5에 대한 자세한 설명은 링크로 대체

 

HTML5 표준은 개발 생산성을 향상, 시맨틱 태그 등장, 멀티미디어 강화 등을 테마로 해서 등장한 표준이다.

HTML5의 input Types의 예제와 실습 링크를 첨부한다. 링크에서 나오는 예제들이 개발 편의성을 향상했다.

 

1) templates\base.html 생성

모든 화면에 공통으로 적용되는 내용 (문서의 틀), 템플릿 파일을 구조화

 

 

{% load static %}
<!doctype html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shring-to-fit=no">
    <link rel="stylesheet" type="text/css" href="{% static 'bootstrap.min.css' %}">
    <link rel="stylesheet" type="text/css" href="{% static 'style.css' %}">
    <title>Hello, pybo!</title>
</head>
<body>
    {% block content %}
    {% endblock %}
</body>
</html>

 

 

2) question_list.html 질문 목록 템플릿 수정

 

{% extends 'base.html' %}	<!-- 추가 -->

{% block content %}		<!-- 추가 -->
<div class="container my-3">
    <table class="table">
        <thead>
            <tr class="thead-dark">
                <th>번호</th>
                <th>제목</th>
                <th>작성일시</th>
            </tr>
        </thead>
        <tbody>
            {% if question_list %}
                {% for question in question_list %}
                    <tr>
                        <td>{{ forloop.counter }}</td>
                        <td>
                            <a href="{% url 'pybo:detail' question.id %}">
                            {{ question.subject }}
                            </a>
                        </td>
                        <td>{{ question.create_date }}</td>
                    </tr>
                {% endfor %}
            {% else %}
                <tr>
                    <td colspan="3">질문이 없습니다.</td>
                </tr>
            {% endif %}
        </tbody>
    </table>
</div>
{% endblock %}			<!-- 추가 -->

 

 

extends 'base.html'은 base.html을 가져와서 block으로 지정해 놓은 부분을 base.html body에 정의한 block content에 넣어주는 것이다.

 

3) question_detail.html 질문 상세 템플릿 수정

 

{% extends 'base.html' %}

{% block content %}
<div class="container my-3">
	<h2 class="border-bottom py-2">{{ question.subject }}</h2>

	<div class="card my-3">
		<div class="card-body">
			<div class="card-text" style="white-space: pre-line;">
				{{ question.content }}
			</div>
			<div class="d-flex justify-content-end">
				<div class="badge badge-light p-2">
					{{ question.create_date }}
				</div>
			</div>
		</div>
	</div>

	<h5 class="border-bottom my-3 py-2">
		{{ question.answer_set.count }}개의 답변이 있습니다.
	</h5>
	{% for answer in question.answer_set.all %}
	<div class="card my-3">
		<div class="card-body">
			<div class="card-text" style="white-space: pre-line;">
				{{ answer.content }}
			</div>
			<div class="d-flex justify-content-end">
				<div class="badge badge-light p-2">
					{{ answer.create_date }}
				</div>
			</div>
		</div>
	</div>
	{% endfor %}

	<form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
		{% csrf_token %}
		<div clas="form-group">
			<textarea name="content" id="content" rows="15" class="form-control"></textarea>
		</div>
		<input type="submit" value="답변 등록" class="btn btn-primary"/>
	</form>
</div>
{% endblock %}

 

위 질문 목록 템플릿 코드 수정과 같은 방식으로 추가한다.

 

 

4. 질문 등록 기능 추가

 

1) 질문 목록 페이지(question_list.html)에 질문 동록 버튼 추가

 

{% extends  'base.html' %}

{% block content %}
<div class="container my-3">
    <table class="table">
        <thead>
            <tr class="thead-dark">
                <th>번호</th>
                <th>제목</th>
                <th>작성일시</th>
            </tr>
        </thead>
        <tbody>
            {% if question_list %}
                {% for question in question_list %}
                    <tr>
                        <td>{{ forloop.counter }}</td>
                        <td>
                            <a href="{% url 'pybo:detail' question.id %}">
                            {{ question.subject }}
                            </a>
                        </td>
                        <td>{{ question.create_date }}</td>
                    </tr>
                {% endfor %}
            {% else %}
                <tr>
                    <td colspan="3">질문이 없습니다.</td>
                </tr>
            {% endif %}
        </tbody>
    </table>
    <a href="{% url 'pybo:question_create' %}" class="btn btn-primary">
        질문 등록하기
    </a>
</div>
{% endblock%}

 

새로운 별칭을 사용했으므로 URL 맵핑해야 한다.

 

 

2) pybo\urls.py에 URL 맵핑

 

from django.urls import path
from . import views         # 현재 패키지에서 views 모듈을 가져옴

app_name = 'pybo'

urlpatterns = [
    path('', views.index, name = 'index'),
    path('<int:question_id>/', views.detail, name = 'detail'),
    path('answer/create/<int:question_id>', views.answer_create, name='answer_create'),
    path('question/create', views.question_create, name='question_create') # 추가
]

 

 

 

3) pyob\views.py에 question_create 메서드 추가

 

from django.shortcuts import render, get_object_or_404, redirect
from .models import Question
from .forms import QuestionForm		#추가
from django.utils import timezone

def index(request):
    question_list = Question.objects.order_by('-create_date')
    context = { 'question_list': question_list }
    
    return render(request, 'pybo/question_list.html', context)


def detail(request, question_id): 
    # question = Question.objects.get(id=question_id)
    question = get_object_or_404(Question, pk=question_id)
    context = { 'question': question }

    return render(request, 'pybo/question_detail.html', context)


def answer_create(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    question.answer_set.create(content=request.POST.get('content'), create_date=timezone.now())
    
    return redirect('pybo:detail', question_id=question.id)


def question_create(request):	# 추가
    form = QuestionForm()
    return render(request, 'pybo/question_form.html', { 'form': form })

 

새로운 패키지를 추가했고, 아직 정의가 되어 있지 않기 때문에 정의를 해주어야 한다.

 

 

4) pybo\forms.py 생성

QuestionForm 클래스 추가

 

from django import forms
from pybo.models import Question

class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question
        fields = ['subject', 'content']

 

  • ModelForm을 상속받아 모델 폼을 정의
  • 모델 폼은 반드시 Meta라는 내부 클래스를 정의해야 함
  • 모델 폼에서 사용할 모델과 모델의 필드를 지정

model은 보여질 폼( QuestionForm) 하고 연결되는 모델이다.

 

fields는 그 모델이 가지고 있는 변수들을 의미하는데, 그 변수 중 어떤 것을 입력받게 할 것인가를 정의해 주는 것이다.

 

 

5) templates\pybo\question_form.html 추가

 

{% extends 'base.html' %}

{% block content %}
<div class="container">
    <h5 class="my-3 border-bottom pb-2">질문 등록</h5>
    <form method="post" class="post-form my-3">
        {% csrf_token %}
        {{ form.as_p }}				
        <button type="submit" class="btn btn-primary">저장하기</button> 
    </form>
</div>
{% endblock %}

 

  • 모델 폼과 연결된 입력 항목(Mata 클래스의 field로 지정된 항목)에 값을 입력할 수 있는 코드를 자동 생성

 

6) 브라우저 화인

 

브라우저 확인

 

 

 

브라우저 확인

 

 

 

5. pybo\views.py에 question_create 메서드 수정

요청 방식에 따라 GET 방식인 경우 입력 화면을 반환, POST 방식인 경우 내용을 저장 후 리다이렉트

 

주소를 입력하거나, 링크를 눌러서 이동하는 것들은 GET 방식을 사용한다.

question_list.html에 정의한 주소로 가게 되어 있다. 그래서 질문 등록을 누르면 링크를 통해 이동하는 것이므로 GET 방식이 된다.

question_create에 요청은 GET 방식으로 들어오는 것이라고 할 수 있다.

 

그러나 저장하기를 누르면 question_form.html에 정의한 것처럼 POST 방식으로 전달한다. 

question_create 메서드가 GET 방식과 POST 방식 두 개가 공존한다.

그래서 GET 방식으로 들어왔을 때와 POST 방식으로 들어왔을 때를 구분해야 한다.

 

from django.shortcuts import render, get_object_or_404, redirect
from .models import Question
from .forms import QuestionForm
from django.utils import timezone

def index(request):
    question_list = Question.objects.order_by('-create_date')
    context = { 'question_list' : question_list }
    
    return render(request, 'pybo/question_list.html', context)

def detail(request, question_id) :
    # question = Question.objects.get(id=question_id)
    question = get_object_or_404(Question, pk=question_id)
    context = { 'question' : question }

    return render(request, 'pybo/question_detail.html', context)

def answer_create(request, question_id) :
    question = get_object_or_404(Question, pk=question_id)
    question.answer_set.create(content=request.POST.get('content'), create_date=timezone.now())

    return redirect('pybo:detail', question_id=question_id)

def question_create(request):	# 수정
    if request.method == 'GET':
        form = QuestionForm()
        return render(request, 'pybo/question_form.html', { 'form': form})
    elif request.method == 'POST' :
        # POST 방식으로 전달된 요청인 경우, 요청 본문을 통해 전달된 입력값을 저장 
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')

 

  • is_valid는 모델에 속성들이 정의되어 있는데 그 조건에 맞는지를 확인한다. 맞으면 save한다.
  • form.save를 하면 QuestionForm에 있는 필드 값을 세팅해 준다.

commit=False는 저장할 때 세 가지 값 subject, content, datetime이 들어가야 하는데 사용자는 두 가지 값만 입력했기 때문에 시간을 추가 해주기 위해 DB에 반영하지 않고 임시저장을 하는 것이다.  

 

create_date를 추가해 주면서 네 가지 값이 모두 들어가 save를 하면 DB에 저장이 된다.

 

 

1) 브라우저 확인

 

브라우저 확인

 

 

 

브라우저 확인

 

 

 

6. 질문 입력 화면 스타일 적용

 

question_form.html에는 {{form.as_p}} 코드를 통해 폼 엘리먼트와 입력 항목을 자동으로 생성해 주기 때문에 스타일을 적용할 수 없다.

 

스타일을 적용하기 위해서는 모델 폼 파일(forms.py)에 내부 클래스 Meta에서 widgets와 labels 속성을 이용해 스타일을 적용할 수 있다.

 

from django import forms
from pybo.models import Question

class QuestionForm(forms.ModelForm):
    class Meta:
        model = Question
        fields = ['subject', 'content']
        widgets = {
            # <input type="text" name="subject" maxlength="200" required id="id_subject" class="form-control">
            'subject': forms.TextInput(attrs={'class': 'form-control'}), 
            # <textarea name="content" cols="40" rows="10" required id="id_content" class="form-control">
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
        }
        labels = {
            'subject': '제목', 
            'content': '내용',
        }

 

 

subject와 content는 주석과 같이 설정된다.

 

저장 후 브라우저 실행하면 바뀐 것을 확인할 수 있다.

 

댓글