본문 바로가기
  • 실행력이 모든걸 결정한다
프로젝트 연습

[구현 완료] 간식 창고 (Django)

by 김코더 김주역 2023. 2. 20.
반응형

Github 링크

https://github.com/datamaker-kr/pilot-project

 

GitHub - datamaker-kr/pilot-project: 백엔드 신입 개발자 김주역님의 파일럿 프로젝트 입니다.

백엔드 신입 개발자 김주역님의 파일럿 프로젝트 입니다. . Contribute to datamaker-kr/pilot-project development by creating an account on GitHub.

github.com

 

 

 

 

프로젝트 소개

  • 프로젝트 이름 : 간식 창고
  • 프로젝트 설명 : 직원들은 간식을 신청할 수 있고, 관리자는 직원들이 신청한 간식들을 관리할 수 있는 서비스다.
  • 기술 스택 : Django
  • 진행 인원 및 작업 기간 : 1인, 2023.02.06~2023.02.10
  • 사용 에디터 : PyCharm
  • 버전 관리 툴 : Git

 

 

 

 

프로젝트 내용

<개요>

  • 요구 사항
  • 기본 설정
  • 요구 사항별 소스 코드 설명
  • 화면 구성

 

 

1. 요구 사항

1) 회원

- 회원가입할 때 이메일(아이디), 비밀번호, 이름만 받습니다.
- 회원은 일반 회원과 관리자로 나뉩니다.
- 관리자가 회원을 관리하는 목록이 존재하고, 회원을 관리자로 변경이 가능합니다.
- 회원 탈퇴가 가능하고, 가입할 때 탈퇴한 회원의 이메일로는 가입할 수 없습니다.

 

 

2) 간식 신청 게시판

- 이름, 이미지, 구매 URL, 설명을 입력할 수 있는 게시판을 만들어주세요.
- 목록만 봐도 입력한 모든 것과 상태를 확인할 수 있으면 좋겠습니다.
- 관리자가 신청 게시판에서 주문상태를 변경할 수 있습니다.
- 주문상태를 변경할 때 몇 월에 사용될 간식인지 지정할 수 있도록 해주세요.
- 주문된 간식은 월별로 볼 수 있도록 별도의 목록을 만들어주세요.

 

 

 

2. 기본 설정

1) settings.py

- 아래 포스팅의 [5. 기본 세팅(권장)]대로 진행했다.

https://kimcoder.tistory.com/339

 

[Django] Django 소개 / 프로젝트 생성

1. Django 소개 - Python 기반의 Web Framework들 중 하나로, MVT(Model-View-Template) 패턴을 따른다. 장고는 MVC 패턴의 View를 Template, Controller를 View라고 부른다. - 장고에서는 전체 프로그램을 프로젝트라 하고,

kimcoder.tistory.com

 

 

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)를 활용한 프로젝트를 진행해볼 것이다.

 

 

 

반응형

댓글