Django Member Board MVT 프로젝트 코드 분석
처음에 mysql을 세팅한다.
-- root 또는 CREATE USER/GRANT 권한이 있는 MySQL 계정으로 실행합니다.
-- 실습용 데이터베이스가 이미 존재하면 삭제하지 않고 그대로 사용합니다.
CREATE DATABASE IF NOT EXISTS django_member_board
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- localhost에서 접속하는 Django 전용 계정을 생성합니다.
CREATE USER IF NOT EXISTS 'django_user'@'localhost' IDENTIFIED BY 'django1234!';
-- 127.0.0.1로 접속하는 환경도 고려하여 동일 사용자 계정을 생성합니다.
CREATE USER IF NOT EXISTS 'django_user'@'127.0.0.1' IDENTIFIED BY 'django1234!';
-- 프로젝트 DB의 모든 테이블에 대한 권한을 localhost 계정에 부여합니다.
GRANT ALL PRIVILEGES ON django_member_board.* TO 'django_user'@'localhost';
-- 프로젝트 DB의 모든 테이블에 대한 권한을 127.0.0.1 계정에 부여합니다.
GRANT ALL PRIVILEGES ON django_member_board.* TO 'django_user'@'127.0.0.1';
-- 권한 변경 내용을 즉시 반영합니다.
FLUSH PRIVILEGES;
env
django 개발여부, 운영환경에서는 false로 변경해야한다.
django_secret_key 암호화, 서명에 사용하는 비밀키. 실제 환경에서는 충분히 길게 세팅.
django_allow_hosts, 허용할 호스트 목록, 쉼표로 구분한다.
db 세팅.
# Django 개발 모드 여부입니다. 운영 환경에서는 False로 변경합니다.
DJANGO_DEBUG=True
# Django 암호화/서명에 사용하는 비밀키입니다. 실제 환경에서는 충분히 긴 임의 문자열로 변경합니다.
DJANGO_SECRET_KEY=change-this-secret-key
# 접속을 허용할 호스트 목록입니다. 쉼표로 구분합니다.
DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost
# MySQL 데이터베이스 이름입니다.
DB_NAME=django_member_board
# MySQL 사용자 이름입니다.
DB_USER=django_user
# MySQL 사용자 비밀번호입니다.
DB_PASSWORD=django1234!
# MySQL 서버 주소입니다.
DB_HOST=127.0.0.1
# MySQL 기본 포트입니다.
DB_PORT=3306
boards.models.py
settings, models import
author를 models.foreignkey로 변환
각 field 변수 지정.
meta 설정 ordering, verbose_name, verbose_name_plural
can_edit, can_delete시 true의 조건 설정
"""Spring MVC의 Board DTO/BOARD 테이블을 Django ORM 모델로 변환한 파일입니다."""
# settings.AUTH_USER_MODEL을 참조하기 위해 settings를 가져옵니다.
from django.conf import settings
# ORM 필드 및 모델 기반 클래스를 사용하기 위해 models를 가져옵니다.
from django.db import models
class Board(models.Model):
"""로그인 회원이 작성하고 소유권 기반으로 수정/삭제하는 게시글 모델입니다."""
# 원본 BOARD_WRITER를 회원 테이블에 대한 외래키로 변환합니다.
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="boards", verbose_name="작성자")
# 원본 BOARD_TITLE에 대응합니다.
title = models.CharField("제목", max_length=100)
# 원본 BOARD_CONTENT에 대응합니다.
content = models.TextField("내용")
# 원본 첨부파일 관련 두 필드를 Django FileField 하나로 단순화하여 실제 저장 경로까지 관리합니다.
attachment = models.FileField("첨부파일", upload_to="board_files/%Y/%m/%d/", blank=True, null=True)
# 원본 BOARD_READCOUNT에 대응하는 조회수입니다.
read_count = models.PositiveIntegerField("조회수", default=0)
# 원본 BOARD_DATE에 대응하며 최초 작성 시각을 자동 저장합니다.
created_at = models.DateTimeField("작성일", auto_now_add=True)
# 게시글 수정 시각을 자동 저장합니다.
updated_at = models.DateTimeField("수정일", auto_now=True)
class Meta:
# 최신 글이 먼저 나오도록 기본 정렬합니다.
ordering = ["-id"]
# 관리자 화면에서 표시할 단수 이름입니다.
verbose_name = "게시글"
# 관리자 화면에서 표시할 복수 이름입니다.
verbose_name_plural = "게시글"
def __str__(self):
"""게시글 객체를 제목으로 표시합니다."""
# 관리자 화면 등에서 제목이 보이도록 반환합니다.
return self.title
def can_edit(self, user):
"""수정 권한은 작성자 본인에게만 허용합니다."""
# 로그인한 사용자의 PK와 작성자 PK가 같은 경우에만 True입니다.
return user.is_authenticated and self.author_id == user.id
def can_delete(self, user):
"""삭제 권한은 작성자 본인 또는 관리자에게 허용합니다."""
# 작성자 본인이거나 staff/superuser 관리자이면 삭제할 수 있습니다.
return user.is_authenticated and (self.author_id == user.id or user.is_staff or user.is_superuser)
models.py
import django abstractUser 기본 사용자 모델
django.db models 데이터베이스 필드 선언하기 위한 model 모듈
member 회원 테이블 구조 정의 ,컬럼정의. abstractuser를 상속했기때문에 username, password, email, is_staff, is_active, date_joined 같은 기보 ㄴ사용자 관련 필드를 함께 가져온다 . 굳이 다시 선언할 필요가 없다. 기본값을 바꿀경우 일부러 다시 선언.
Mata, django 모델 을 어떻게 취급할지 메타데이터 넣는곳. __str__ 정의
@property 매서드를 변수처럼 사용할 수 있게해주는 python 기능. admin여부, login 여부를 변수처럼 활용할 수 있다.
"""Spring MVC의 Member DTO/테이블을 Django 사용자 모델로 변환한 파일입니다."""
# Django가 제공하는 확장 가능한 기본 사용자 모델 AbstractUser를 가져옵니다.
from django.contrib.auth.models import AbstractUser
# 데이터베이스 필드를 선언하기 위한 models 모듈을 가져옵니다.
from django.db import models
class Member(AbstractUser):
"""회원가입, 인증, 세션, 관리자 권한을 함께 처리하는 사용자 정의 회원 모델입니다."""
# 성별 선택값을 상수로 정의하여 DB에는 짧은 값, 화면에는 한글 설명을 사용합니다.
GENDER_CHOICES = [("M", "남성"), ("F", "여성"), ("N", "선택 안 함")]
# 가입방식 선택값을 정의합니다. 이번 프로젝트의 일반 회원가입은 direct를 사용합니다.
SIGN_TYPE_CHOICES = [("direct", "직접 가입"), ("kakao", "카카오"), ("naver", "네이버"), ("google", "구글")]
# AbstractUser.username 필드를 USERID처럼 사용하며 최대 길이를 원본 프로젝트보다 넉넉한 50자로 확장합니다.
username = models.CharField("회원 아이디", max_length=50, unique=True)
# 원본 Member.userName에 대응하는 화면 표시용 이름입니다.
display_name = models.CharField("회원 이름", max_length=30)
# 원본 Member.gender에 대응하며 미선택 값도 허용합니다.
gender = models.CharField("성별", max_length=1, choices=GENDER_CHOICES, default="N")
# 원본 Member.age에 대응하며 입력하지 않을 수도 있도록 null/blank를 허용합니다.
age = models.PositiveIntegerField("나이", null=True, blank=True)
# 원본 Member.phone에 대응하는 전화번호 문자열 필드입니다.
phone = models.CharField("전화번호", max_length=20, blank=True)
# AbstractUser.email을 재선언해 이메일 입력을 필수값으로 설정합니다.
email = models.EmailField("이메일", unique=True)
# 원본 SIGNTYPE 필드에 대응합니다.
sign_type = models.CharField("가입 방식", max_length=10, choices=SIGN_TYPE_CHOICES, default="direct")
# 원본 PHOTO_FILENAME에 대응하며 실제 파일을 media/member_photos/에 저장합니다.
photo = models.ImageField("프로필 사진", upload_to="member_photos/%Y/%m/", blank=True, null=True)
# 원본 LASTMODIFIED 역할을 하는 마지막 회원정보 수정 시각입니다.
updated_at = models.DateTimeField("마지막 수정일", auto_now=True)
class Meta:
# Django 관리자 화면과 디버깅 시 표시할 단수 모델 이름입니다.
verbose_name = "회원"
# 복수 모델 이름입니다.
verbose_name_plural = "회원"
# 회원 목록 기본 정렬을 최근 가입 순으로 지정합니다.
ordering = ["-date_joined"]
def __str__(self):
"""관리자 화면 등에서 회원 객체를 읽기 쉽게 표시합니다."""
# 아이디와 회원 이름을 함께 문자열로 반환합니다.
return f"{self.username} ({self.display_name})"
@property
def admin_yn(self):
"""원본 프로젝트의 ADMIN_YN 값을 Django 권한 필드에 맞춰 표현합니다."""
# staff 또는 superuser이면 관리자 Y, 아니면 N을 반환합니다.
return "Y" if self.is_staff or self.is_superuser else "N"
@property
def login_ok(self):
"""원본 프로젝트의 LOGIN_OK 값을 Django is_active와 대응시킵니다."""
# 활성 사용자이면 Y, 로그인 제한 사용자이면 N을 반환합니다.
return "Y" if self.is_active else "N"
members.forms.py
django.contrib.auth.forms 사용자 생성, 수정 기반 폼 클래스 가져오기
django form
models 가져오기
SignUpForm, ProfileUpdateForm, LoginForm Meta에 field 설정, init시 쵝화 로직. css 적용
"""회원가입, 로그인, 회원정보 수정에 사용할 Django Form을 정의합니다."""
# Django의 사용자 생성/수정 폼 기반 클래스를 가져옵니다.
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
# 일반 ModelForm을 만들기 위한 forms 모듈을 가져옵니다.
from django import forms
# 현재 앱의 사용자 정의 회원 모델을 가져옵니다.
from .models import Member
class SignUpForm(UserCreationForm):
"""비밀번호 해시 저장까지 자동 처리하는 회원가입 폼입니다."""
class Meta:
# 저장 대상 모델을 사용자 정의 Member로 지정합니다.
model = Member
# 회원가입 화면에서 입력받을 필드를 순서대로 지정합니다.
fields = ("username", "display_name", "email", "gender", "age", "phone", "photo", "password1", "password2")
def __init__(self, *args, **kwargs):
"""Bootstrap 스타일과 한글 placeholder를 각 입력 위젯에 적용합니다."""
# 부모 UserCreationForm의 초기화 로직을 먼저 실행합니다.
super().__init__(*args, **kwargs)
# 모든 폼 필드를 순회하면서 공통 CSS 클래스를 적용합니다.
for field in self.fields.values():
field.widget.attrs.setdefault("class", "form-control")
class LoginForm(AuthenticationForm):
"""Django authenticate()를 사용하는 세션 로그인 폼입니다."""
def __init__(self, request=None, *args, **kwargs):
"""로그인 입력창에 공통 CSS 클래스를 적용합니다."""
# 부모 AuthenticationForm 초기화를 수행합니다.
super().__init__(request=request, *args, **kwargs)
# 아이디 입력 필드 스타일을 지정합니다.
self.fields["username"].widget.attrs.update({"class": "form-control", "placeholder": "회원 아이디"})
# 비밀번호 입력 필드 스타일을 지정합니다.
self.fields["password"].widget.attrs.update({"class": "form-control", "placeholder": "비밀번호"})
class ProfileUpdateForm(forms.ModelForm):
"""로그인 사용자가 본인의 회원 정보를 수정할 때 사용하는 폼입니다."""
class Meta:
# 수정 대상 모델을 Member로 지정합니다.
model = Member
# 아이디와 권한은 임의 수정하지 못하게 제외하고 일반 프로필 정보만 허용합니다.
fields = ("display_name", "email", "gender", "age", "phone", "photo")
def __init__(self, *args, **kwargs):
"""프로필 수정 입력 요소에 공통 CSS 클래스를 적용합니다."""
# ModelForm 기본 초기화를 수행합니다.
super().__init__(*args, **kwargs)
# 모든 입력 위젯에 form-control 클래스를 부여합니다.
for field in self.fields.values():
field.widget.attrs.setdefault("class", "form-control")
forms.py
import django의 form, authenticationform, usercreationform 사용자 생성, 수정 폼
현 패키지의 . models member 가져오기
meta 설정. modelform 상속해서 board 전용 form 만들기. meta 설정 정보 정의. model은 board 대상으로. 사용자가 폼중에서 입력할 수 있는 필드 지정. widgets, content를 textarea 형태로 보여준다. rows 10줄 높이로.
init시 boardform을 자동을 ㅗ초기화한다. for문으로 모든 fields에 class 형 css 지정하기
"""게시글 등록과 수정에 공통으로 사용할 ModelForm입니다."""
# Django 폼 모듈을 가져옵니다.
from django import forms
# Board ORM 모델을 가져옵니다.
from .models import Board
class BoardForm(forms.ModelForm):
"""작성자는 서버의 request.user로 지정하고 제목/내용/파일만 입력받습니다."""
class Meta:
# 저장 대상 모델을 Board로 지정합니다.
model = Board
# 사용자가 직접 수정 가능한 필드만 폼에 노출합니다.
fields = ("title", "content", "attachment")
# 내용 입력칸은 여러 줄 textarea로 표시합니다.
widgets = {"content": forms.Textarea(attrs={"rows": 10})}
def __init__(self, *args, **kwargs):
"""각 입력 요소에 공통 CSS 클래스를 적용합니다."""
# ModelForm 기본 초기화를 수행합니다.
super().__init__(*args, **kwargs)
# 모든 폼 필드 위젯에 form-control 스타일을 적용합니다.
for field in self.fields.values():
field.widget.attrs.setdefault("class", "form-control")
views.py
django에서 지원하는 처리함수를 가져오기 login, logout, login_reauired, user_passes_test, message, get_object_or_404, redirect, render
Board.models에서 Board 객체 가져오기
현재 폴더의 forms와 models 가져오기
home 함수만들어 board.objects.select_related author와 관련되있는것 에서 :5 잘라 return
signup_view() if문으로 request.user.is_authenticated 로그인할경우 redirect, post 요청이면 signupform에 넣어 is_valid() 검증 완료시 form.save(), login() 해 세션을 생성하고 success 메세지 보여주기 return redirect boards.list else일경우 signupform() 회원가입으로 넘기기, 그 외의 경우 render signup.html출력.
앞선 settings.py가 자동으로 로그인하지않을경우 로그인했을경우 redirect해주지만, 회원가입 등의 경우에는업무로직으로 views.py에서 직접 정의한다.
"""회원가입, 로그인 세션, 회원정보, 관리자 회원관리 View를 정의합니다."""
# 사용자 인증/로그인/로그아웃 처리를 위한 함수를 가져옵니다.
from django.contrib.auth import login, logout
# 로그인한 사용자만 접근하도록 제한하는 데코레이터를 가져옵니다.
from django.contrib.auth.decorators import login_required, user_passes_test
# 처리 결과 메시지를 화면으로 전달하기 위한 messages 기능을 가져옵니다.
from django.contrib import messages
# HTTP 요청에 따라 페이지 렌더링, 이동, 객체 조회를 하기 위한 단축 함수를 가져옵니다.
from django.shortcuts import get_object_or_404, redirect, render
# 게시판 메인 화면에서 최근 게시글을 보여주기 위해 Board 모델을 가져옵니다.
from boards.models import Board
# 회원 관련 폼을 가져옵니다.
from .forms import LoginForm, ProfileUpdateForm, SignUpForm
# 회원 모델을 가져옵니다.
from .models import Member
def home(request):
"""로그인 여부와 관계없이 접근 가능한 첫 화면을 출력합니다."""
# ORM을 이용해 최근 게시글 5개만 조회합니다.
recent_posts = Board.objects.select_related("author").all()[:5]
# 조회 결과를 home.html 템플릿에 전달합니다.
return render(request, "home.html", {"recent_posts": recent_posts})
def signup_view(request):
"""GET이면 회원가입 화면, POST이면 새 회원을 저장합니다."""
# 이미 로그인한 사용자는 중복 회원가입을 막고 메인으로 이동시킵니다.
if request.user.is_authenticated:
return redirect("home")
# POST 요청이면 전송된 일반 값과 업로드 파일을 폼에 바인딩합니다.
if request.method == "POST":
form = SignUpForm(request.POST, request.FILES)
# Django의 필드 검증과 비밀번호 일치/강도 검사를 모두 통과했는지 확인합니다.
if form.is_valid():
# UserCreationForm.save()가 비밀번호를 평문이 아닌 안전한 해시로 저장합니다.
member = form.save()
# 회원가입 직후 자동 로그인하여 세션을 생성합니다.
login(request, member)
# 다음 화면에서 보여줄 성공 메시지를 세션에 저장합니다.
messages.success(request, "회원가입과 로그인이 완료되었습니다.")
# 게시판 목록으로 이동합니다.
return redirect("boards:list")
else:
# GET 요청이면 빈 회원가입 폼을 생성합니다.
form = SignUpForm()
# 오류가 있거나 GET 요청이면 폼 화면을 출력합니다.
return render(request, "members/signup.html", {"form": form})
def login_view(request):
"""아이디/비밀번호를 검증하고 성공 시 Django 로그인 세션을 생성합니다."""
# 이미 로그인한 사용자는 게시판으로 이동시킵니다.
if request.user.is_authenticated:
return redirect("boards:list")
# AuthenticationForm에 현재 request와 POST 데이터를 전달합니다.
form = LoginForm(request=request, data=request.POST or None)
# POST 요청이며 아이디/비밀번호 검증에 성공한 경우 처리합니다.
if request.method == "POST" and form.is_valid():
# 검증 완료된 사용자 객체를 가져옵니다.
member = form.get_user()
# login()이 세션에 사용자 PK와 인증 백엔드 정보를 안전하게 저장합니다.
login(request, member)
# 로그인 성공 메시지를 저장합니다.
messages.success(request, f"{member.display_name}님, 로그인되었습니다.")
# 로그인 전에 접근하려던 URL(next)이 있으면 그곳으로, 없으면 게시판으로 이동합니다.
return redirect(request.GET.get("next") or "boards:list")
# 로그인 화면을 출력합니다.
return render(request, "members/login.html", {"form": form})
@login_required
def logout_view(request):
"""현재 로그인 세션을 제거하여 로그아웃합니다."""
# 상태 변경 작업은 POST 요청에서만 처리합니다.
if request.method == "POST":
# logout()이 현재 세션의 인증 데이터를 제거하고 세션을 안전하게 정리합니다.
logout(request)
# 로그아웃 안내 메시지를 저장합니다.
messages.success(request, "로그아웃되었습니다.")
# 메인 화면으로 이동합니다.
return redirect("home")
@login_required
def profile_view(request):
"""로그인한 본인의 회원 정보를 조회합니다."""
# request.user는 AuthenticationMiddleware가 세션을 기준으로 구성한 Member 객체입니다.
return render(request, "members/profile.html", {"member": request.user})
@login_required
def profile_update_view(request):
"""로그인 사용자가 본인의 회원 정보를 수정합니다."""
# POST이면 입력값/파일을 기존 회원 객체에 바인딩하고, GET이면 기존 값이 채워진 폼을 만듭니다.
form = ProfileUpdateForm(request.POST or None, request.FILES or None, instance=request.user)
# POST 요청이고 유효성 검사를 통과하면 DB에 UPDATE SQL에 해당하는 ORM 작업을 수행합니다.
if request.method == "POST" and form.is_valid():
form.save()
messages.success(request, "회원 정보가 수정되었습니다.")
return redirect("members:profile")
# 수정 폼을 출력합니다.
return render(request, "members/profile_form.html", {"form": form})
@login_required
def withdraw_view(request):
"""로그인한 사용자가 본인 계정을 탈퇴합니다."""
# 실수로 GET 요청만으로 삭제되지 않도록 POST에서만 실제 삭제합니다.
if request.method == "POST":
# 현재 로그인한 회원 객체를 보관합니다.
member = request.user
# 먼저 로그아웃하여 세션에서 사용자 인증 정보를 제거합니다.
logout(request)
# 회원 삭제 시 Board.author의 CASCADE 설정에 따라 해당 사용자의 글도 함께 삭제됩니다.
member.delete()
messages.success(request, "회원 탈퇴가 완료되었습니다.")
return redirect("home")
# 확인 화면을 출력합니다.
return render(request, "members/withdraw_confirm.html")
def _is_admin(user):
"""staff 또는 superuser인지 검사하여 관리자 전용 View에서 재사용합니다."""
# 인증된 사용자이면서 Django 관리자 권한 플래그가 하나라도 있으면 True를 반환합니다.
return user.is_authenticated and (user.is_staff or user.is_superuser)
@user_passes_test(_is_admin)
def member_list_view(request):
"""관리자가 일반 회원 목록을 조회합니다."""
# superuser를 제외한 회원을 최근 가입순으로 조회합니다.
members = Member.objects.filter(is_superuser=False).order_by("-date_joined")
# 관리자 회원 목록 화면을 출력합니다.
return render(request, "members/member_list.html", {"members": members})
@user_passes_test(_is_admin)
def toggle_active_view(request, pk):
"""관리자가 일반 회원의 로그인 허용/제한 상태를 변경합니다."""
# URL의 PK로 대상 회원을 조회하고 없으면 404를 반환합니다.
member = get_object_or_404(Member, pk=pk, is_superuser=False)
# GET으로 상태 변경이 되지 않도록 POST에서만 처리합니다.
if request.method == "POST":
# 현재 활성 상태를 반대로 변경하여 원본 LOGIN_OK 토글 기능을 구현합니다.
member.is_active = not member.is_active
# 변경한 is_active 필드만 UPDATE하도록 저장합니다.
member.save(update_fields=["is_active"])
messages.success(request, f"{member.username}의 로그인 상태를 {'허용' if member.is_active else '제한'}으로 변경했습니다.")
# 회원 목록으로 이동합니다.
return redirect("members:member_list")
members.urls.py
path, 같은 앱의 view 가져오기
url에 이름 붙이는 설정 공간. 프로젝트에 members/login, boards/login, admin/login 등 앱이 여러개 있으면 login 이름이 겹칠 수 있기 때문에 app_name을 이용해서 앱 이름을 앞에 붙여 구분할 수 있도록 한다. <a href="{% url 'members:login' %}">
urlpatterens 정의
"""회원 앱의 URL과 View를 연결합니다."""
# URL 패턴을 작성하기 위해 path를 가져옵니다.
from django.urls import path
# 같은 앱의 views 모듈을 가져옵니다.
from . import views
# 템플릿에서 members:login처럼 이름공간을 사용할 수 있도록 app_name을 지정합니다.
app_name = "members"
# 회원 관련 URL 패턴을 정의합니다.
urlpatterns = [
path("signup/", views.signup_view, name="signup"),
path("login/", views.login_view, name="login"),
path("logout/", views.logout_view, name="logout"),
path("profile/", views.profile_view, name="profile"),
path("profile/edit/", views.profile_update_view, name="profile_edit"),
path("withdraw/", views.withdraw_view, name="withdraw"),
path("admin-list/", views.member_list_view, name="member_list"),
path("<int:pk>/toggle-active/", views.toggle_active_view, name="toggle_active"),
]
boards.urls.py
마찬가지.
int:pk는 url에 들어오는 숫자를 pk이름으로 받아서 view에 전달한다는 뜻.
"""게시판 앱의 URL과 CRUD View를 연결합니다."""
# URL 패턴 선언 함수를 가져옵니다.
from django.urls import path
# 같은 앱의 views를 가져옵니다.
from . import views
# 템플릿 reverse URL에서 boards:detail 형태로 사용할 이름공간입니다.
app_name = "boards"
# 게시판 CRUD URL을 정의합니다.
urlpatterns = [
path("", views.board_list, name="list"),
path("create/", views.board_create, name="create"),
path("<int:pk>/", views.board_detail, name="detail"),
path("<int:pk>/edit/", views.board_update, name="update"),
path("<int:pk>/delete/", views.board_delete, name="delete"),
]
config.urls
django.comtrib admin 관리자 사이트 객체
django.urls inclue, path 앱 url을 연결하고 path로 url 패턴을 정의
django.conf.urls.static static 정적 파일을 url로 제공하기 위한 static 함수
django.conf settings django가 제공하는 설정 객체.
members.views home home view 함수 가져오기
urlpatterns 정하기. admin, members, boards 에 각 요청을 url 파일 설정.
if settings.DEBUG debug 가 true일때 static 파일이 있는 url을 접근할 수 있도록 url을 추가 .
"""프로젝트 최상위 URL 라우팅 설정입니다."""
# Django 관리자 사이트 객체를 가져옵니다.
from django.contrib import admin
# include()로 앱 URL을 연결하고 path()로 URL 패턴을 정의합니다.
from django.urls import include, path
# 개발 중 업로드 파일을 Django 개발 서버로 제공하기 위한 함수를 가져옵니다.
from django.conf.urls.static import static
# settings의 DEBUG, MEDIA_URL, MEDIA_ROOT 값을 참조합니다.
from django.conf import settings
# 메인 화면 View 함수를 가져옵니다.
from members.views import home
# 브라우저 요청 URL을 각 View 또는 앱 URL 설정으로 연결합니다.
urlpatterns = [
# Django 기본 관리자 사이트입니다.
path("admin/", admin.site.urls),
# 사이트 첫 화면입니다.
path("", home, name="home"),
# /members/ 이하 요청을 회원 앱 URL 설정으로 전달합니다.
path("members/", include("members.urls")),
# /boards/ 이하 요청을 게시판 앱 URL 설정으로 전달합니다.
path("boards/", include("boards.urls")),
]
# DEBUG=True인 개발 환경에서는 /media/ URL로 업로드 파일을 직접 확인할 수 있게 합니다.
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
config.wsgi.application
import os
django.core.wsgi get_wsgi_application web server gateway interface 웹서버와 django 애플리케이션을 연결해주는 규칙, 인터페이스. django 애플리케이션을 웹서버가 실행할 수 있도록 연결해준다.
os.environ.setdefault() settings 모듈 정의.
application 객체만들기
"""WSGI 서버가 Django 애플리케이션을 구동할 때 사용하는 설정 파일입니다."""
# 환경변수 설정을 위해 os 모듈을 가져옵니다.
import os
# WSGI application 객체를 생성하는 Django 함수를 가져옵니다.
from django.core.wsgi import get_wsgi_application
# 기본 settings 모듈을 지정합니다.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
# 웹 서버가 호출할 WSGI 애플리케이션 객체를 생성합니다.
application = get_wsgi_application()
config.settings
import path, os, load_dotenv 불러와 읽어 변수에 할당.
INSTALLED_APPS 사이트 기능들 명시하기django. 붙은것들은 django에서 지원하는 기본앱들이고 외 프로젝트에서 만든 앱을 지정할 수 있다.
MIDDLEWARE http 요청, 응답 과정에 적용할 기본헤더, 세션, 정규화, 위조방지, user 구현, 메세지기능, 클릭 재킹 공격 완화 미들웨어를 순서대로 등록한다. securityMiddleware, sessionMiddleware, commonmiddleware, csrfviewmiddleware, authenticationmiddleware, messagemiddleware, xframeoptionsmiddleware.
최상위 url 설정 모듈 지정 ROOT_URLCONF
BACKEND는 django.template.backends.django.DjangoTemplates 기본 템플릿 백엔드 사용
DIRS 설정.
APP_DIRS True django가 앱의 tempates 폴더를 자동으로 찾도록 설정한다.
OPTIONS django 템플릿에서 사용할 수 있는 공통변수를 자동으로 넣어주는 설정 request를 템플릿에 자동으로 전달해 html에서 {{ request.user }}로 사용할 수 있고
auth 로그인한 사용자 정보를 템플릿에 자동으로 전달해 {{ user }} {{ user.is_authenticated }} 등을 사용할 수 있다.
messages는 프레임워크에서 발생한 메세지를 템플릿에서 사용할 수 있게 해준다 .
messages.success(request, "로그인 성공!")
{% for message in messages %}
{{ message }}
{% endfor %}
WSGI_APPLICATION 웹서버가 사용할 애플리케이션 객체 위치 지정
DATABASES 데이터베이스 연결정보를 환경변수 기반으로 설정
AUTH_PASSWORD_VALIDATORS django 기본 비밀번호 검증기 사용
userattributessimilarityvalidator는 사용자 정보와 너무 비슷한 비밀번호 막기 예를 들어 이름과 비슷하게 금지.
Minimumlengthvalidator 비밀번호 최소 길이 검사. django에서는 기본 보통 최소 8자리를 요구한다.
commonpasswordvalidator 사람들이 흔히 사용하는 뻔한 비밀번호를 막는다. 1234...
NumbericPasswordValidator 숫자로만 이루어진 비밀번호막기
AUTH_USER_MODEL 기본 auth.User 대신 members.mebmer 모델을 사용자 모델로 사용한다.
LOGIN_URL 로그인하지 않았을경우 member:login 로 이동, 로그인시 LOGIN_REDIRECT_URL board로, 로그아웃 LOGOUT_REDIRECT_URL 시 home으로 이동.
SESSION_COOKIE_AGE 비활성 시간이 30분을 넘으면 세션 만료
SESSION_SAVE_EVERY_REQUEST 요청 발생할때마다 세션 만료시간을 30분으로 연장한다.
사이트 기본 언어 지정 LANGUAGE_CODE, 표준시간대지정, django 번역기능 활성화 USE_I18N, 시간을 타임존 정보와 함께 관리한다. USE_TZ
STATIC_URL 정적 파일명
STATICFILES_DIRS 공통 static 디렉터리
MEDIA_URL 업로드한 사진.. 첨부파일등의 url 접두사 지정
MEDIA_ROOT 실제 저장할 미디어 파일 디렉터리 지정
DEFAULT_AUTO_FIELD django에서 db 테이블의 기본 pk를 어떤 자료형으로 자동 생성할지 정하는 설정. bigautofield는 autofield와 bigautofield 모두 자동으로 증가하는 정수형 id를 만든다. 데이터가 엄청 많이 쓰이는 서비스에서는 일반 정수형 id의 범위를 넘어살 가능성이 있음으로 더 큰 범위의 정수형 pk를 사용하는 것.
"""Django 프로젝트의 전역 환경설정 파일입니다."""
# 파일 시스템 경로 계산을 위해 Path 클래스를 가져옵니다.
from pathlib import Path
# 환경변수 값을 읽기 위해 os 모듈을 가져옵니다.
import os
# 프로젝트 루트의 .env 파일을 읽기 위해 python-dotenv 함수를 가져옵니다.
from dotenv import load_dotenv
# settings.py 기준 두 단계 위 디렉터리를 프로젝트 루트로 지정합니다.
BASE_DIR = Path(__file__).resolve().parent.parent
# 프로젝트 루트의 .env 파일이 존재하면 환경변수로 로딩합니다.
load_dotenv(BASE_DIR / ".env")
# 세션, CSRF 토큰, 비밀번호 재설정 등에 사용하는 비밀키를 환경변수에서 읽습니다.
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-secret-key-change-me")
# 문자열 환경변수를 비교하여 개발 디버그 모드를 결정합니다.
DEBUG = os.getenv("DJANGO_DEBUG", "True").lower() == "true"
# 쉼표로 구분된 호스트 문자열을 리스트로 변환합니다.
ALLOWED_HOSTS = [host.strip() for host in os.getenv("DJANGO_ALLOWED_HOSTS", "127.0.0.1,localhost").split(",") if host.strip()]
# Django 기본 앱과 프로젝트에서 만든 앱을 등록합니다.
INSTALLED_APPS = [
# Django 관리자 사이트 기능을 제공합니다.
"django.contrib.admin",
# 로그인, 로그아웃, 사용자/권한 모델 기능을 제공합니다.
"django.contrib.auth",
# 모델 ContentType 정보를 관리합니다.
"django.contrib.contenttypes",
# 세션 데이터를 DB에 저장하고 로그인 상태를 유지합니다.
"django.contrib.sessions",
# 일회성 성공/오류 메시지를 화면에 출력하도록 지원합니다.
"django.contrib.messages",
# CSS, JavaScript, 이미지 등의 정적 파일을 관리합니다.
"django.contrib.staticfiles",
# 사용자 정의 회원 모델과 회원 기능을 제공하는 앱입니다.
"members",
# 게시판 CRUD 기능을 제공하는 앱입니다.
"boards",
]
# HTTP 요청/응답 과정에 적용할 미들웨어를 순서대로 등록합니다.
MIDDLEWARE = [
# 보안 관련 기본 헤더와 설정을 적용합니다.
"django.middleware.security.SecurityMiddleware",
# 세션 객체 request.session을 사용할 수 있게 합니다.
"django.contrib.sessions.middleware.SessionMiddleware",
# URL 처리 전 공통적인 요청 정규화를 수행합니다.
"django.middleware.common.CommonMiddleware",
# POST 요청 위조를 방지하기 위해 CSRF 검증을 수행합니다.
"django.middleware.csrf.CsrfViewMiddleware",
# 세션의 사용자 ID를 바탕으로 request.user를 구성합니다.
"django.contrib.auth.middleware.AuthenticationMiddleware",
# messages.success(), messages.error() 같은 메시지 기능을 제공합니다.
"django.contrib.messages.middleware.MessageMiddleware",
# 클릭재킹 공격을 완화하기 위한 X-Frame-Options 헤더를 적용합니다.
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# 프로젝트의 최상위 URL 설정 모듈을 지정합니다.
ROOT_URLCONF = "config.urls"
# Django 템플릿 엔진과 공통 템플릿 경로를 설정합니다.
TEMPLATES = [
{
# Django 기본 템플릿 백엔드를 사용합니다.
"BACKEND": "django.template.backends.django.DjangoTemplates",
# 프로젝트 루트/templates 폴더에서도 템플릿을 찾도록 합니다.
"DIRS": [BASE_DIR / "templates"],
# 각 앱 내부의 templates 폴더를 자동 검색합니다.
"APP_DIRS": True,
# 템플릿에서 request, user, messages 등을 사용하기 위한 Context Processor를 설정합니다.
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# WSGI 웹 서버가 사용할 애플리케이션 객체 위치를 지정합니다.
WSGI_APPLICATION = "config.wsgi.application"
# MySQL 데이터베이스 연결 정보를 환경변수 기반으로 설정합니다.
DATABASES = {
"default": {
# Django가 mysqlclient 드라이버를 이용해 MySQL과 통신하도록 지정합니다.
"ENGINE": "django.db.backends.mysql",
# 사용할 MySQL 데이터베이스 이름을 지정합니다.
"NAME": os.getenv("DB_NAME", "django_member_board"),
# MySQL 접속 계정을 지정합니다.
"USER": os.getenv("DB_USER", "django_user"),
# MySQL 접속 비밀번호를 지정합니다.
"PASSWORD": os.getenv("DB_PASSWORD", "django1234!"),
# MySQL 서버 주소를 지정합니다.
"HOST": os.getenv("DB_HOST", "127.0.0.1"),
# MySQL 서버 포트를 지정합니다.
"PORT": os.getenv("DB_PORT", "3306"),
# 한글과 이모지를 안정적으로 저장하기 위해 utf8mb4 문자셋을 사용합니다.
"OPTIONS": {"charset": "utf8mb4"},
}
}
# Django 기본 비밀번호 검증기를 사용하여 지나치게 단순한 비밀번호를 제한합니다.
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# 프로젝트에서 기본 auth.User 대신 members.Member 모델을 사용자 모델로 사용합니다.
AUTH_USER_MODEL = "members.Member"
# @login_required에서 로그인하지 않은 사용자를 이동시킬 URL 이름입니다.
LOGIN_URL = "members:login"
# 로그인 성공 후 next 값이 없을 때 이동할 기본 URL입니다.
LOGIN_REDIRECT_URL = "boards:list"
# 로그아웃 후 이동할 기본 URL입니다.
LOGOUT_REDIRECT_URL = "home"
# 사용자의 비활성 시간이 30분을 넘으면 세션을 만료시킵니다.
SESSION_COOKIE_AGE = 30 * 60
# 요청이 발생할 때마다 세션 만료 시간을 다시 30분으로 연장합니다.
SESSION_SAVE_EVERY_REQUEST = True
# 사이트의 기본 언어를 한국어로 지정합니다.
LANGUAGE_CODE = "ko-kr"
# 서버와 템플릿에서 사용하는 표준 시간대를 서울로 지정합니다.
TIME_ZONE = "Asia/Seoul"
# Django 번역 기능을 활성화합니다.
USE_I18N = True
# timezone-aware datetime을 사용합니다.
USE_TZ = True
# /static/ URL이 정적 파일을 가리키도록 설정합니다.
STATIC_URL = "static/"
# 프로젝트 공통 static 디렉터리를 추가합니다.
STATICFILES_DIRS = [BASE_DIR / "static"]
# 업로드한 프로필 사진과 게시글 첨부파일의 URL 접두사를 지정합니다.
MEDIA_URL = "media/"
# 업로드 파일을 실제 저장할 디렉터리를 지정합니다.
MEDIA_ROOT = BASE_DIR / "media"
# AutoField보다 큰 정수형 기본 PK 타입을 사용합니다.
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
manage.py
os.environ.setdefault DJANGO_SETTINS_MODULE이 지정되지않았다면 config.settings 파일을 기본값으로 사용한다 .
Django 프로젝트를 실행하고 관리하기 위한 진입점.
Django가 제공하는 관리명령 실행함수
django.core.management execute_from_command_line, python manage.py runserver, migrate, makemigrations, createsyperuser 명령들을 처리해주는 핵심 함수
django 설치가 안되어 importerror가 날경우 raise error, dkssoansrn.
#!/usr/bin/env python
"""Django 관리 명령을 실행하기 위한 프로젝트 진입 파일입니다."""
# 운영체제 환경변수를 다루기 위해 os 모듈을 가져옵니다.
import os
# 명령행 인자를 Django에 전달하기 위해 sys 모듈을 가져옵니다.
import sys
def main():
"""settings 모듈을 지정한 뒤 Django 관리 명령을 실행합니다."""
# DJANGO_SETTINGS_MODULE이 별도로 지정되지 않았다면 프로젝트 설정 파일을 기본값으로 사용합니다.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
try:
# Django의 manage.py 명령 처리 함수를 필요한 시점에 가져옵니다.
from django.core.management import execute_from_command_line
except ImportError as exc:
# Django가 설치되지 않았을 때 원인을 알기 쉬운 메시지로 다시 발생시킵니다.
raise ImportError("Django가 설치되지 않았습니다. pip install -r requirements.txt 를 실행하세요.") from exc
# python manage.py 뒤에 입력한 migrate, runserver 등의 명령을 Django에 전달합니다.
execute_from_command_line(sys.argv)
# 이 파일이 직접 실행될 때만 main() 함수를 호출합니다.
if __name__ == "__main__":
main()
각 파일(서비스) 내에 app.py는 urls.py처럼 직접 import해서 호출하는 코드가 보이지않지만 django가 앱을 시작할때 자동으로 읽고 등록한다.
app.py
django 설정 기반 클래스 가져오기 app.cinfig
models pk를 big autofield 로 지정하기
사용할 앱 패키지 이름 name을 members로 지정하기
# Django 앱 설정 기반 클래스를 가져옵니다.
from django.apps import AppConfig
class MembersConfig(AppConfig):
"""회원 앱의 기본 설정을 정의합니다."""
# 기본 PK 타입을 BigAutoField로 지정합니다.
default_auto_field = "django.db.models.BigAutoField"
# INSTALLED_APPS에서 사용할 앱 패키지 이름입니다.
name = "members"
wsgi.py도 wagi 서버가 이 파일을 진입점으로 해 불러온다.
settings 모듈을 지정하고, application 객체 생성하기
"""WSGI 서버가 Django 애플리케이션을 구동할 때 사용하는 설정 파일입니다."""
# 환경변수 설정을 위해 os 모듈을 가져옵니다.
import os
# WSGI application 객체를 생성하는 Django 함수를 가져옵니다.
from django.core.wsgi import get_wsgi_application
# 기본 settings 모듈을 지정합니다.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
# 웹 서버가 호출할 WSGI 애플리케이션 객체를 생성합니다.
application = get_wsgi_application()
settings.py에 INSTALLED_APPS 에 members, boards가 명시되어 django가 앱들을 초기화하면서 admin 앱 의 자동 등록과정을 거친다.
django.contrib adimin, auth.admin, 현 패키지에 정의된 memeber rkwudhrl
@admin.register(Member) django admin에 member 모델을 등록.
관리자 목록 화면에서 확인할 필드 지정
검색 기능 지정
관리자 화면 오른쪽 등에 필터 기능 만들기
fieldsets 기본 django의 useradmin 의 회원 수정 화면은 그대로 사용하되 추가로 만든 memeber 전용 필드를 추가한다.
"""Django 관리자 사이트에서 게시글을 관리하기 위한 설정입니다."""
# Django admin 기능을 가져옵니다.
from django.contrib import admin
# 게시글 모델을 가져옵니다.
from .models import Board
@admin.register(Board)
class BoardAdmin(admin.ModelAdmin):
"""게시글 목록/검색/필터 UI를 설정합니다."""
# 관리자 목록에서 주요 게시글 정보를 표시합니다.
list_display = ("id", "title", "author", "read_count", "created_at")
# 제목, 내용, 작성자 아이디로 검색할 수 있게 합니다.
search_fields = ("title", "content", "author__username")
# 작성일을 기준으로 날짜 필터를 제공합니다.
list_filter = ("created_at",)
# 작성자 FK 선택 시 검색 UI를 제공합니다.
autocomplete_fields = ("author",)
각 화면 html을 확인해보자.
base.html
html 태그 정의
header, nav 정의
{% url 'boards:list' %} 등으로 링크를 연결하고 {% if user.is_authenticated %} 로그인된 사용자에게만 보일 메뉴 설정
main 태그안에 block content를 넣어 고유 콘텐츠가 삽이보디도록 한다.
{% load static %}
<!DOCTYPE html>
<html lang="ko">
<head>
<!-- 한글이 깨지지 않도록 UTF-8 문자셋을 지정합니다. -->
<meta charset="UTF-8">
<!-- 모바일 화면에서도 반응형 레이아웃이 적용되도록 viewport를 지정합니다. -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 각 페이지가 전달한 title 값이 있으면 사용하고 없으면 기본 제목을 사용합니다. -->
<title>{% block title %}Django Member Board{% endblock %}</title>
<!-- 프로젝트 공통 CSS 파일을 Django static 태그로 연결합니다. -->
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<!-- 모든 페이지에서 공통으로 사용하는 상단 내비게이션입니다. -->
<header class="site-header">
<nav class="nav container">
<!-- 서비스 로고를 메인 화면에 연결합니다. -->
<a class="brand" href="{% url 'home' %}">Django MVT 실습</a>
<div class="nav-links">
<!-- 게시판은 로그인 여부와 상관없이 목록 조회가 가능합니다. -->
<a href="{% url 'boards:list' %}">게시판</a>
{% if user.is_authenticated %}
<!-- 로그인한 사용자에게만 글쓰기와 회원정보 메뉴를 표시합니다. -->
<a href="{% url 'boards:create' %}">글쓰기</a>
<a href="{% url 'members:profile' %}">{{ user.display_name }}님</a>
{% if user.is_staff or user.is_superuser %}
<!-- 관리자에게만 회원 관리 메뉴를 표시합니다. -->
<a href="{% url 'members:member_list' %}">회원관리</a>
<a href="/admin/">Django Admin</a>
{% endif %}
<!-- 로그아웃은 상태 변경이므로 POST form으로 전송합니다. -->
<form class="inline-form" method="post" action="{% url 'members:logout' %}">
{% csrf_token %}
<button class="link-button" type="submit">로그아웃</button>
</form>
{% else %}
<!-- 비로그인 사용자에게 로그인/회원가입 링크를 표시합니다. -->
<a href="{% url 'members:login' %}">로그인</a>
<a href="{% url 'members:signup' %}">회원가입</a>
{% endif %}
</div>
</nav>
</header>
<!-- Django messages 프레임워크에 저장된 성공/오류 메시지를 출력합니다. -->
<div class="container message-area">
{% for message in messages %}
<div class="message {{ message.tags }}">{{ message }}</div>
{% endfor %}
</div>
<!-- 각 페이지 고유 콘텐츠가 삽입되는 영역입니다. -->
<main class="container main-content">
{% block content %}{% endblock %}
</main>
<!-- 공통 하단 영역입니다. -->
<footer class="site-footer">
<div class="container">Spring MVC → Django MVT 변환 실습 프로젝트</div>
</footer>
</body>
</html>
home.html
뼈대 구성. extends base.html
contents 에 들어갈 html을 정의
{% if not user.is_authenticated %}일때만 회원가입 a가 보인다.
{% for post in recent_posts %} 게시글을 출력할 수 있다.
{% extends 'base.html' %}
{% block title %}메인 | Django Member Board{% endblock %}
{% block content %}
<!-- 첫 화면의 서비스 소개 영역입니다. -->
<section class="hero card">
<h1>Django 회원 관리 + 게시판 서비스</h1>
<p>MySQL, Django ORM, 세션 인증, 회원 권한, 게시글 CRUD를 한 프로젝트에서 실습합니다.</p>
<div class="actions">
<a class="btn" href="{% url 'boards:list' %}">게시판 보기</a>
{% if not user.is_authenticated %}
<a class="btn secondary" href="{% url 'members:signup' %}">회원가입</a>
{% endif %}
</div>
</section>
<!-- ORM으로 조회한 최근 게시글을 간단히 출력합니다. -->
<section class="card">
<h2>최근 게시글</h2>
<div class="list-table">
{% for post in recent_posts %}
<a class="list-row" href="{% url 'boards:detail' post.pk %}">
<span>{{ post.title }}</span>
<span>{{ post.author.display_name }} · {{ post.created_at|date:'Y-m-d H:i' }}</span>
</a>
{% empty %}
<p>등록된 게시글이 없습니다.</p>
{% endfor %}
</div>
</section>
{% endblock %}
login.html
로그인. form안에 {% csrf_token %} post요청보낼때 다른 사이트가 사용자 몰래 요청하는 공격 csrf를 막기위한 보안 토큰
for문으로 form을 출력, error시 안내
{% extends 'base.html' %}
{% block title %}로그인{% endblock %}
{% block content %}
<section class="card narrow">
<h1>로그인</h1>
<!-- 인증 정보는 POST 방식으로 전송합니다. -->
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}<div class="field-error">{{ error }}</div>{% endfor %}
</div>
{% endfor %}
{% for error in form.non_field_errors %}<div class="field-error">{{ error }}</div>{% endfor %}
<button class="btn" type="submit">로그인</button>
</form>
</section>
{% endblock %}
회원가입도 마찬가지.
{% extends 'base.html' %}
{% block title %}회원가입{% endblock %}
{% block content %}
<!-- 파일 업로드가 있으므로 multipart/form-data 인코딩을 사용합니다. -->
<section class="card narrow">
<h1>회원가입</h1>
<form method="post" enctype="multipart/form-data">
<!-- Django CSRF 미들웨어 검증을 통과하기 위한 토큰입니다. -->
{% csrf_token %}
<!-- 폼의 각 필드와 오류 메시지를 출력합니다. -->
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}<small>{{ field.help_text|safe }}</small>{% endif %}
{% for error in field.errors %}<div class="field-error">{{ error }}</div>{% endfor %}
</div>
{% endfor %}
{% for error in form.non_field_errors %}<div class="field-error">{{ error }}</div>{% endfor %}
<button class="btn" type="submit">가입하기</button>
</form>
</section>
{% endblock %}
회원관리
for문으로 list 출력.
empty일경우 설정 추가
{% extends 'base.html' %}
{% block title %}회원 관리{% endblock %}
{% block content %}
<section class="card">
<h1>관리자 회원 관리</h1>
<div class="table-wrap">
<table>
<thead><tr><th>아이디</th><th>이름</th><th>이메일</th><th>가입일</th><th>로그인</th><th>처리</th></tr></thead>
<tbody>
{% for member in members %}
<tr>
<td>{{ member.username }}</td><td>{{ member.display_name }}</td><td>{{ member.email }}</td>
<td>{{ member.date_joined|date:'Y-m-d' }}</td>
<td>{% if member.is_active %}허용{% else %}제한{% endif %}</td>
<td>
<!-- 회원의 로그인 허용 상태 변경도 POST 방식으로 처리합니다. -->
<form method="post" action="{% url 'members:toggle_active' member.pk %}">
{% csrf_token %}
<button class="btn small" type="submit">{% if member.is_active %}로그인 제한{% else %}로그인 허용{% endif %}</button>
</form>
</td>
</tr>
{% empty %}<tr><td colspan="6">회원이 없습니다.</td></tr>{% endfor %}
</tbody>
</table>
</div>
</section>
{% endblock %}
회원정보 상세
if photo가 있을 경우 출력, member 정보 출력.
{% extends 'base.html' %}
{% block title %}회원정보{% endblock %}
{% block content %}
<section class="card narrow">
<h1>회원정보</h1>
<!-- 로그인 세션에서 얻은 현재 사용자 정보를 출력합니다. -->
{% if member.photo %}<img class="profile-photo" src="{{ member.photo.url }}" alt="프로필 사진">{% endif %}
<dl class="detail-grid">
<dt>아이디</dt><dd>{{ member.username }}</dd>
<dt>이름</dt><dd>{{ member.display_name }}</dd>
<dt>이메일</dt><dd>{{ member.email }}</dd>
<dt>성별</dt><dd>{{ member.get_gender_display }}</dd>
<dt>나이</dt><dd>{{ member.age|default:'-' }}</dd>
<dt>전화번호</dt><dd>{{ member.phone|default:'-' }}</dd>
<dt>가입일</dt><dd>{{ member.date_joined|date:'Y-m-d H:i' }}</dd>
<dt>로그인 허용</dt><dd>{{ member.login_ok }}</dd>
<dt>관리자</dt><dd>{{ member.admin_yn }}</dd>
</dl>
<div class="actions">
<a class="btn" href="{% url 'members:profile_edit' %}">정보 수정</a>
<a class="btn danger" href="{% url 'members:withdraw' %}">회원 탈퇴</a>
</div>
</section>
{% endblock %}
회원 수정
form enctype은 media가 있을 수 있으니 multipart로 .
for문으로 출력.
{% extends 'base.html' %}
{% block title %}회원정보 수정{% endblock %}
{% block content %}
<section class="card narrow">
<h1>회원정보 수정</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}<div class="field-error">{{ error }}</div>{% endfor %}
</div>
{% endfor %}
<button class="btn" type="submit">저장</button>
<a class="btn secondary" href="{% url 'members:profile' %}">취소</a>
</form>
</section>
{% endblock %}
회원탈퇴
해당 폴더 url을 보면withdraw가 있고, withdraw_view 함수를 보면 logout 후에 memver.delete를 수행한다.
{% extends 'base.html' %}
{% block title %}회원 탈퇴{% endblock %}
{% block content %}
<section class="card narrow">
<h1>회원 탈퇴</h1>
<p>회원 계정과 회원이 작성한 게시글이 함께 삭제됩니다. 계속하시겠습니까?</p>
<form method="post">
{% csrf_token %}
<button class="btn danger" type="submit">탈퇴 확정</button>
<a class="btn secondary" href="{% url 'members:profile' %}">취소</a>
</form>
</section>
{% endblock %}
"""회원가입, 로그인 세션, 회원정보, 관리자 회원관리 View를 정의합니다."""
# 사용자 인증/로그인/로그아웃 처리를 위한 함수를 가져옵니다.
from django.contrib.auth import login, logout
# 로그인한 사용자만 접근하도록 제한하는 데코레이터를 가져옵니다.
from django.contrib.auth.decorators import login_required, user_passes_test
# 처리 결과 메시지를 화면으로 전달하기 위한 messages 기능을 가져옵니다.
from django.contrib import messages
# HTTP 요청에 따라 페이지 렌더링, 이동, 객체 조회를 하기 위한 단축 함수를 가져옵니다.
from django.shortcuts import get_object_or_404, redirect, render
# 게시판 메인 화면에서 최근 게시글을 보여주기 위해 Board 모델을 가져옵니다.
from boards.models import Board
# 회원 관련 폼을 가져옵니다.
from .forms import LoginForm, ProfileUpdateForm, SignUpForm
# 회원 모델을 가져옵니다.
from .models import Member
def home(request):
"""로그인 여부와 관계없이 접근 가능한 첫 화면을 출력합니다."""
# ORM을 이용해 최근 게시글 5개만 조회합니다.
recent_posts = Board.objects.select_related("author").all()[:5]
# 조회 결과를 home.html 템플릿에 전달합니다.
return render(request, "home.html", {"recent_posts": recent_posts})
def signup_view(request):
"""GET이면 회원가입 화면, POST이면 새 회원을 저장합니다."""
# 이미 로그인한 사용자는 중복 회원가입을 막고 메인으로 이동시킵니다.
if request.user.is_authenticated:
return redirect("home")
# POST 요청이면 전송된 일반 값과 업로드 파일을 폼에 바인딩합니다.
if request.method == "POST":
form = SignUpForm(request.POST, request.FILES)
# Django의 필드 검증과 비밀번호 일치/강도 검사를 모두 통과했는지 확인합니다.
if form.is_valid():
# UserCreationForm.save()가 비밀번호를 평문이 아닌 안전한 해시로 저장합니다.
member = form.save()
# 회원가입 직후 자동 로그인하여 세션을 생성합니다.
login(request, member)
# 다음 화면에서 보여줄 성공 메시지를 세션에 저장합니다.
messages.success(request, "회원가입과 로그인이 완료되었습니다.")
# 게시판 목록으로 이동합니다.
return redirect("boards:list")
else:
# GET 요청이면 빈 회원가입 폼을 생성합니다.
form = SignUpForm()
# 오류가 있거나 GET 요청이면 폼 화면을 출력합니다.
return render(request, "members/signup.html", {"form": form})
def login_view(request):
"""아이디/비밀번호를 검증하고 성공 시 Django 로그인 세션을 생성합니다."""
# 이미 로그인한 사용자는 게시판으로 이동시킵니다.
if request.user.is_authenticated:
return redirect("boards:list")
# AuthenticationForm에 현재 request와 POST 데이터를 전달합니다.
form = LoginForm(request=request, data=request.POST or None)
# POST 요청이며 아이디/비밀번호 검증에 성공한 경우 처리합니다.
if request.method == "POST" and form.is_valid():
# 검증 완료된 사용자 객체를 가져옵니다.
member = form.get_user()
# login()이 세션에 사용자 PK와 인증 백엔드 정보를 안전하게 저장합니다.
login(request, member)
# 로그인 성공 메시지를 저장합니다.
messages.success(request, f"{member.display_name}님, 로그인되었습니다.")
# 로그인 전에 접근하려던 URL(next)이 있으면 그곳으로, 없으면 게시판으로 이동합니다.
return redirect(request.GET.get("next") or "boards:list")
# 로그인 화면을 출력합니다.
return render(request, "members/login.html", {"form": form})
@login_required
def logout_view(request):
"""현재 로그인 세션을 제거하여 로그아웃합니다."""
# 상태 변경 작업은 POST 요청에서만 처리합니다.
if request.method == "POST":
# logout()이 현재 세션의 인증 데이터를 제거하고 세션을 안전하게 정리합니다.
logout(request)
# 로그아웃 안내 메시지를 저장합니다.
messages.success(request, "로그아웃되었습니다.")
# 메인 화면으로 이동합니다.
return redirect("home")
@login_required
def profile_view(request):
"""로그인한 본인의 회원 정보를 조회합니다."""
# request.user는 AuthenticationMiddleware가 세션을 기준으로 구성한 Member 객체입니다.
return render(request, "members/profile.html", {"member": request.user})
@login_required
def profile_update_view(request):
"""로그인 사용자가 본인의 회원 정보를 수정합니다."""
# POST이면 입력값/파일을 기존 회원 객체에 바인딩하고, GET이면 기존 값이 채워진 폼을 만듭니다.
form = ProfileUpdateForm(request.POST or None, request.FILES or None, instance=request.user)
# POST 요청이고 유효성 검사를 통과하면 DB에 UPDATE SQL에 해당하는 ORM 작업을 수행합니다.
if request.method == "POST" and form.is_valid():
form.save()
messages.success(request, "회원 정보가 수정되었습니다.")
return redirect("members:profile")
# 수정 폼을 출력합니다.
return render(request, "members/profile_form.html", {"form": form})
@login_required
def withdraw_view(request):
"""로그인한 사용자가 본인 계정을 탈퇴합니다."""
# 실수로 GET 요청만으로 삭제되지 않도록 POST에서만 실제 삭제합니다.
if request.method == "POST":
# 현재 로그인한 회원 객체를 보관합니다.
member = request.user
# 먼저 로그아웃하여 세션에서 사용자 인증 정보를 제거합니다.
logout(request)
# 회원 삭제 시 Board.author의 CASCADE 설정에 따라 해당 사용자의 글도 함께 삭제됩니다.
member.delete()
messages.success(request, "회원 탈퇴가 완료되었습니다.")
return redirect("home")
# 확인 화면을 출력합니다.
return render(request, "members/withdraw_confirm.html")
def _is_admin(user):
"""staff 또는 superuser인지 검사하여 관리자 전용 View에서 재사용합니다."""
# 인증된 사용자이면서 Django 관리자 권한 플래그가 하나라도 있으면 True를 반환합니다.
return user.is_authenticated and (user.is_staff or user.is_superuser)
@user_passes_test(_is_admin)
def member_list_view(request):
"""관리자가 일반 회원 목록을 조회합니다."""
# superuser를 제외한 회원을 최근 가입순으로 조회합니다.
members = Member.objects.filter(is_superuser=False).order_by("-date_joined")
# 관리자 회원 목록 화면을 출력합니다.
return render(request, "members/member_list.html", {"members": members})
@user_passes_test(_is_admin)
def toggle_active_view(request, pk):
"""관리자가 일반 회원의 로그인 허용/제한 상태를 변경합니다."""
# URL의 PK로 대상 회원을 조회하고 없으면 404를 반환합니다.
member = get_object_or_404(Member, pk=pk, is_superuser=False)
# GET으로 상태 변경이 되지 않도록 POST에서만 처리합니다.
if request.method == "POST":
# 현재 활성 상태를 반대로 변경하여 원본 LOGIN_OK 토글 기능을 구현합니다.
member.is_active = not member.is_active
# 변경한 is_active 필드만 UPDATE하도록 저장합니다.
member.save(update_fields=["is_active"])
messages.success(request, f"{member.username}의 로그인 상태를 {'허용' if member.is_active else '제한'}으로 변경했습니다.")
# 회원 목록으로 이동합니다.
return redirect("members:member_list")
'Personal > SK 네트웍스 AI 캠프' 카테고리의 다른 글
| [SK네트웍스 Family AI 캠프] 32기 16주차 회고: Day57~Day60 (0) | 2026.08.13 |
|---|---|
| SK 네트웍스 AI 캠프 - 4_AI 활용 애플리케이션 개발 - Day59_django_member_board_openai_chromadb_rag_final (0) | 2026.08.13 |
| SK 네트웍스 AI 캠프 - 4_AI 활용 애플리케이션 개발 - Day57 Django 기반 웹 서버 개발 기초 (0) | 2026.08.10 |
| [SK네트웍스 Family AI 캠프] 32기 15주차 회고: Day56 (0) | 2026.08.05 |
| SK 네트웍스 AI 캠프 - 4_AI 활용 애플리케이션 개발 - Day56_HTML5 (0) | 2026.08.05 |