Github 링크
https://github.com/datamaker-kr/pilot-project
프로젝트 소개
- 프로젝트 이름 : 간식 창고
- 프로젝트 설명 : 직원들은 간식을 신청할 수 있고, 관리자는 직원들이 신청한 간식들을 관리할 수 있는 서비스다.
- 기술 스택 : Django
- 진행 인원 및 작업 기간 : 1인, 2023.02.06~2023.02.10
- 사용 에디터 : PyCharm
- 버전 관리 툴 : Git
프로젝트 내용
<개요>
- 요구 사항
- 기본 설정
- 요구 사항별 소스 코드 설명
- 화면 구성
1. 요구 사항
1) 회원
- 회원가입할 때 이메일(아이디), 비밀번호, 이름만 받습니다.
- 회원은 일반 회원과 관리자로 나뉩니다.
- 관리자가 회원을 관리하는 목록이 존재하고, 회원을 관리자로 변경이 가능합니다.
- 회원 탈퇴가 가능하고, 가입할 때 탈퇴한 회원의 이메일로는 가입할 수 없습니다.
2) 간식 신청 게시판
- 이름, 이미지, 구매 URL, 설명을 입력할 수 있는 게시판을 만들어주세요.
- 목록만 봐도 입력한 모든 것과 상태를 확인할 수 있으면 좋겠습니다.
- 관리자가 신청 게시판에서 주문상태를 변경할 수 있습니다.
- 주문상태를 변경할 때 몇 월에 사용될 간식인지 지정할 수 있도록 해주세요.
- 주문된 간식은 월별로 볼 수 있도록 별도의 목록을 만들어주세요.
2. 기본 설정
1) settings.py
- 아래 포스팅의 [5. 기본 세팅(권장)]대로 진행했다.
https://kimcoder.tistory.com/339
2) media 설정 추가
- runserver 서버는 media 파일을 자동으로 가져와주지 않기 때문에, 직접 urls.py에 URL 패턴을 추가해줘야 한다.
# urls.py
urlpatterns = [
...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
이렇게 해두면, MEDIA_URL로 들어오는 요청에 대해 MEDIA_ROOT 경로를 탐색하게 된다.
3. 요구 사항별 소스 코드 설명
1) 회원가입할 때 이메일(아이디), 비밀번호, 이름만 받습니다.
- email 필드를 unique로 주기 위해 커스텀 User를 생성하고, 이를 인증용 User 클래스로 등록하였다.
class UserManager(BaseUserManager):
use_in_migrations = True
def create_user(self, email, username, password): # 재정의
if not username or not email or not password:
raise ValueError("fill all blanks")
user = self.model(
email = self.normalize_email(email),
username = username,
)
user.set_password(password)
user.save(using = self._db)
return user
def create_superuser(self, email, username, password): # 재정의
user = self.create_user(
email = self.normalize_email(email),
username = username,
password = password,
)
user.is_superuser = True
user.is_staff = True
user.save(using = self._db)
return user
class User(AbstractUser):
objects = UserManager()
email = models.EmailField('EMAIL', max_length=255, unique=True)
username = models.CharField('USERNAME', max_length=150, unique=True)
def __str__(self):
return self.email
# settings.py
AUTH_USER_MODEL = 'user.User'
- 폼에 User 클래스의 모든 속성을 받아서는 안되기 때문에, 이름, 이메일, 비밀번호(및 비밀번호 확인)만 받을 수 있도록 auth의 UserCreationForm을 상속 받아서 내부 Meta 클래스를 추가했다.
class SignupForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = User
fields = ['username', 'email', 'password1', 'password2']
- 다음과 같이 회원가입 관련 뷰들을 추가했다.
class UserCreateView(CreateView):
template_name = 'registration/register.html'
form_class = SignupForm
success_url = reverse_lazy('register_done')
class UserCreateDoneTV(TemplateView):
template_name = 'registration/register_done.html'
2) 회원은 일반 회원과 관리자로 나뉩니다.
- 장고에서는 Admin 페이지를 통해 테이블을 관리할 수 있도록 하는 관리자 페이지를 제공해준다.
# urls.py
urlpatterns = [
path('admin/', admin.site.urls),
...
] ...
개발자는 관리자가 테이블을 관리할 수 있도록 admin.py에 테이블과 각 테이블의 속성을 지정해주기만 하면 된다.
@admin.register(Snack)
class SnackAdmin(admin.ModelAdmin):
list_display = ['name', 'image', 'url', 'description', 'is_accepted', 'supply_year', 'supply_month']
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = ['email', 'username', 'password', 'is_active', 'is_staff', 'is_superuser']
3) 관리자가 회원을 관리하는 목록이 존재하고, 회원을 관리자로 변경이 가능합니다.
- 회원을 관리자로 변경할 수 있도록 is_staff, is_superuser을 UserAdmin 클래스의 list_display 목록에 추가했다. admin.py 소스코드는 2)에 첨부했다.
4) 회원 탈퇴가 가능하고, 가입할 때 탈퇴한 회원의 이메일로는 가입할 수 없습니다.
- 따로 회원의 이메일만을 저장하는 클래스를 만들 생각도 했었지만, 굳이 테이블을 하나 더 만들 필요는 없을 것 같았다. User의 email도 어차피 unique로 지정되어 있어서 탈퇴한 회원의 is_active만 False로 바꾸도록 했다.
class UserDeleteView(LoginRequiredMixin, RedirectView):
pattern_name = 'home'
def get(self, request, *args, **kwargs):
current_user = self.request.user
current_user.is_active = False
current_user.is_staff = False
current_user.is_superuser = False
current_user.save(using = 'default')
return super().get(request, *args, **kwargs)
5) 이름, 이미지, 구매 URL, 설명을 입력할 수 있는 게시판을 만들어주세요.
class Snack(models.Model):
name = models.CharField('NAME', max_length = 50)
image = models.ImageField('IMAGE', upload_to = 'snack/images/')
url = models.URLField('URL', max_length = 500)
description = models.CharField('DESCRIPTION', max_length = 300, blank = True, help_text = ' (개수 등 설명 추가)')
is_accepted = models.BooleanField('IS_ACCEPTED', default = False)
supply_year = models.PositiveSmallIntegerField('SUPPLY_YEAR', default = 2023, null = True)
supply_month = models.PositiveSmallIntegerField(
'SUPPLY_MONTH', null = True,
validators = [
MaxValueValidator(12),
MinValueValidator(1),
]
)
create_dt = models.DateField('CREATE_DT', auto_now_add = True) # 생성될 때 시각을 자동으로 기록
class Meta:
verbose_name = 'snack'
verbose_name_plural = 'snacks'
db_table = 'snack_snacks'
ordering = ('-create_dt',)
def __str__(self):
return self.name
- 이름, 이미지, 구매 URL, 설명 외에도 아래 속성들을 추가했다.
- is_accepted : 간식 신청 승인 여부
- supply_year : 비치 예정 연도
- supply_month : 비치 예정 월
- create_dt : 간식 신청 날짜
- 폼에 Snack 클래스의 모든 속성을 받아서는 안되기 때문에, 이름, 이미지, 구매 URL, 설명만 받을 수 있도록 ModelForm 클래스를 상속 받아서 내부 Meta 클래스를 추가했다.
class SnackForm(forms.ModelForm):
class Meta:
model = Snack
fields = ('name', 'image', 'url', 'description')
- 회원만 간식 신청이 가능하도록 CreateView를 상속 받은 클래스에 LoginRequiredMixin 클래스를 상속 받았다.
class SnackCV(LoginRequiredMixin, CreateView):
template_name = 'snack/enroll.html'
model = Snack
fields = ('name', 'image', 'url', 'description')
login_url = reverse_lazy('login')
success_url = reverse_lazy('home')
def get_context_data(self, **kwargs): # 템플릿 시스템으로 넘겨줄 컨텍스트 변수에 대한 작업
context = super().get_context_data(**kwargs)
if self.request.POST:
context['modelForm'] = SnackForm(self.request.POST, self.request.FILES)
else:
context['modelForm'] = SnackForm()
return context
6) 목록만 봐도 입력한 모든 것과 상태를 확인할 수 있으면 좋겠습니다.
- 간식 신청 날짜를 기준으로 내림차순(디폴트) 출력할 수 있게 했다. 간식 신청이 매우 많아질 것을 대비하여 페이징 기능도 추가했다.
class SnackAV(ArchiveIndexView):
model = Snack
date_field = 'create_dt'
paginate_by = 20
- ArchiveIndexView는 _archive.html로 끝나는 템플릿 파일을 자동으로 찾아주기 때문에 template_name은 따로 지정하지 않았다. snack_archive.html 파일을 다음과 같이 작성했다. 페이징 기능 역시 반영했다.
{% extends 'base.html' %}
{% load static %}
{% block title %}snack_list.html{% endblock %}
{% block content %}
<h5 class="my-3 border-bottom pb-2">{% block list-name %}간식 신청 목록(전체){% endblock %}</h5>
<table class="table table-bordered table-striped text-center align-middle">
<thead>
<tr>
<th scope="col" width="15%">간식 이름</th>
<th scope="col" width="15%">사진</th>
<th scope="col" width="20%">구매 URL</th>
<th scope="col" width="20%">설명</th>
<th scope="col" width="14%">신청 일자</th>
<th scope="col" width="16%">주문 상태</th>
</tr>
</thead>
<tbody>
{% for snack in object_list %}
<tr>
<th>{{snack.name}}</th>
{% if snack.image %}
<td><img src="{{ snack.image.url }}" width="100px" height="100px"></td>
{% else %}
<td><img src="{% static 'image/snack/default_snack_image.jpeg' %}" width="100px" height="100px"></td>
{% endif %}
<td><a href="{{ snack.url }}" style="font-size:12px">
{{ snack.url|slice:":70" }}...{% if snack.url|length > 70 %}(생략){% endif %}
</a></td>
<td>{{ snack.description }}</td>
<td>{{ snack.create_dt }}</td>
{% if snack.is_accepted %}
<td><span style="color:mediumseagreen">●</span> {{ snack.supply_year }}년 {{ snack.supply_month }}월 비치 예정</td>
{% else %}
<td><span style="color:orange">●</span> 주문 대기중</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page={{ page_obj.previous_page_number }}" class="page-link">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">Previous</a>
</li>
{% endif %}
<li class="ms-3 me-3 mt-1"><span class="align-middle">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>
{% if page_obj.has_next %}
<li class="page-item">
<a href="?page={{ page_obj.next_page_number }}" class="page-link">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endblock %}
7) 관리자가 신청 게시판에서 주문상태를 변경할 수 있습니다.
- 주문상태를 변경할 수 있도록 is_accepted 속성을 SnackAdmin 클래스의 list_display 목록에 추가했다. admin.py 소스코드는 2)에 첨부했다.
8) 주문상태를 변경할 때 몇 월에 사용될 간식인지 지정할 수 있도록 해주세요.
- 비치 예정 날짜를 변경할 수 있도록 supply_year, supply_month 속성을 SnackAdmin 클래스의 list_display 목록에 추가했다. admin.py 소스코드는 2)에 첨부했다.
9) 주문된 간식은 월별로 볼 수 있도록 별도의 목록을 만들어주세요.
- 최근 6개월, 현재 월, 추후 6개월 총 13달 동안의 간식을 확인할 수 있도록 구현하는 것이 좋을 것 같았다.
- 상단 nav의 월별 비치 예정 목록에서 연-월을 선택할 수 있도록, 연-월 리스트가 공통 context 사전에 추가되도록 했다. 아래와 같이 딕셔너리 타입 객체를 반환하는 함수를 context_processors 옵션에 추가한다.
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'utils.context_processors.get_timelist',
],
},
},
]
주변 13달에 대한 연-월 리스트를 담고 있는 딕셔너리 타입 객체를 반환한다.
# utils/context_processors.py
def get_timelist(self):
timelist = []
now = time.localtime(time.time())
now_year = now.tm_year
now_mon = now.tm_mon
for i in range(-6, 7):
if now_mon + i < 1:
timelist.append([now_year - 1, now_mon + i + 12])
elif now_mon + i > 12:
timelist.append([now_year + 1, now_mon + i - 12])
else:
timelist.append([now_year, now_mon + i])
return {'timelist': timelist}
- 월별 간식 리스트를 출력할 수 있도록 ListView를 상속 받았다. 특정 연-월에 해당하는 간식만 출력할 수 있도록 get_queryset() 함수를 오버라이딩했다. 참고로, 직접 날짜 필드를 사용하지 않으므로 MonthArchiveView는 사용하지 않았다.
class MonthlySnackLV(ListView):
model = Snack
paginate_by = 20
template_name = 'snack/monthly_list.html'
def get_context_data(self, **kwargs): # 템플릿 시스템으로 넘겨줄 컨텍스트 변수에 대한 작업
context = super().get_context_data(**kwargs)
context['time'] = [self.kwargs['year'], self.kwargs['month']]
return context
def get_queryset(self):
return Snack.objects.filter(supply_year__exact=self.kwargs['year']).filter(supply_month__exact=self.kwargs['month'])
4. 화면 구성
1) 회원가입
2) 관리 페이지
(1) 유저 목록
(2) 간식 상태 변경
3) 간식 신청
4) 월별 비치 예정 목록 nav (현재 2023/02)
5) 월별 비치 예정 목록
6) 홈 페이지
프로젝트를 마치며...
Django로 만든 첫 토이 프로젝트다. 목표 기간에 맞추지 못할까봐 오버페이스를 해서 그런지 생각보다 일찍 끝난 것 같다.
함수형 뷰보다는 확실히 클래스형 뷰가 편하다는 것을 느끼게 된 프로젝트였다. 장고에서 워낙 제네릭 뷰들을 잘 구현해놓아서 개발자는 일부 속성만 따로 지정해주기만 하면 기본적인 기능들은 손 쉽게 만들 수 있었다. 세부 로직을 변경해야될 때도 이미 구현된 메소드를 오버라이딩하기만 하면 된다는 점이 좋았다.
이 프로젝트를 통해 전반적인 Django의 요청 처리 흐름을 익히는데 많은 도움이 되었다. 다음에는 DRF(Django Rest Framework)를 활용한 프로젝트를 진행해볼 것이다.
'프로젝트 연습' 카테고리의 다른 글
[구현 완료] 간식 창고 (DRF) (0) | 2023.03.13 |
---|---|
[구현 완료] 주식 웹어플리케이션 (0) | 2021.05.19 |
[교내 수상작] 후방 충돌방지 자동차 (1) | 2020.08.29 |
[구현 완료] Java 기차표 예약 시스템 (5) | 2020.08.29 |
댓글