[멋사] 쇼핑몰 구현 -1

김지연·2023년 7월 15일
0

멋쟁이사자처럼_hywu

목록 보기
13/17
post-thumbnail

저번시간에는 간단하게 mysql 연결까지 해보았다.
사실 dbsqlite에서 mysql로 바뀐 것 뿐이지 그렇게 큰 차이는 없다.

일단 model 작성부터 빠르게 진행하겠다.

Shop - Model

from django.db import models
from django.urls import reverse


# Create your models here.

# 카테고리 모델
class Category(models.Model):
    name = models.CharField(max_length=30, db_index=True)
    # 검색엔진에서 찾기 쉽도록 사이트를 개선하는 프로세스
    meta_description = models.TextField(blank=True)
	
    # slug = 상품명을 이용해 url을 만드는 방식
    # 띄어쓰기 -> 하이픈 / 쉼표, 마침표 = 없애줌 등등으로 검색하기 편하게 끔 변환해 줌
    slug = models.SlugField(max_length=200, db_index=True, allow_unicode=True)

    class Meta:
        ordering = ['name']
        verbose_name = 'category'
        # plural = 복수형
        verbose_name_plural = 'categories'

    def __str__(self):
        return self.name

    def get_absolute_url(self):
    	# admin 페이지에서 view on site 버튼으로 slug를 이용해 해당하는 상풍의 카테고리로 이동
        # reverse = url 템플릿 태그랑 비슷하다고 보면 된다~ args는 인수를 넘겨주는 것
        return reverse('shop:product_in_category', args=[str(self.slug)])


# 상품 모델
class Product(models.Model):
	# foreignkey는 외래키로 Category와 Product를 연결해서 사용하겠다는 뜻이다.
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='products')
    name = models.CharField(max_length=100, db_index=True)
    slug = models.SlugField(max_length=100, db_index=True, unique=True, allow_unicode=True)

	# image는 upload to로 어디에 올릴 것인지 지정할 수 있다.
    # 사진의 양이 적다면 괜찮지만 많은 경우를 대비해 아래와 같이 세분화 하자
    image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
    baeminImage = models.ImageField(upload_to='productsQR/%Y/%m/%d', blank=True)
    description = models.TextField(blank=True)
    meta_description = models.TextField(blank=True)
    url = models.URLField(default='정보없음')
	
    
    price = models.DecimalField(max_digits=10, decimal_places=2)
    # 재고 관리는 필수이다.
    stock = models.PositiveIntegerField()

    available_display = models.BooleanField('Display', default=True)
    available_order = models.BooleanField('Order', default=True)

    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created']
        index_together = [['id', 'slug']]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('shop:product_detail', args=[self.id, self.slug])


이렇게 model을 작성한 후에 꼭 해야하는 것이 있다.

python manage.py makemigrations shop
python manage.py migrate shop

만약 오류 중에서 no module 이 뜬다면
pip install pillow 를 해주자 (없어서 그렇다)


Shop - View

from django.shortcuts import render, get_object_or_404

from .models import *

# Create your views here.

# 카테고리 페이지 뷰
def product_in_category(request, category_slug=None):
    current_category = None
    categories = Category.objects.all()
    products = Product.objects.filter(available_display=True)
	
    
    # category_slug를 찾아 현재어느 카테고리를 보여주는 지 판단
    if category_slug :
    	# 카테고리가 선택 될 경우 current_category 에 저장
        current_category = get_object_or_404(Category, slug=category_slug)
        # current_category에 속한 상품들만 필터링함
        products = products.filter(category=current_category)

    return render(request, 'shop/list.html', {'current_category':current_category, 'categories':categories, 'products':products})

# 상품 상세 페이지 뷰
def product_detail(request, id, product_slug=None) :
    product = get_object_or_404(Product, id=id, slug=product_slug)
    return render(request, 'shop/detail.html', {'product':product})

Shop - URL


shop/urls.py
from django.urls import path
from .views import *

app_name = 'shop'

urlpatterns = [
    path('', product_in_category, name='product_all'),
    path('<category_slug>/', product_in_category, name='product_in_category'),
    path('<int:id>/<product_slug>/', product_detail, name='product_detail'),
]

config/urls.py

from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('shop.urls')),
]

템플릿 확장

중복이 되는 템플릿이 있을 때 기준이 되는 템플릿을 만들고 이를 확장하는 형식
예시) 주로 웹사이트의 경우 navbar가 항상 상단에 고정되어있고 나머지 안의 부분만 바뀐다.

프로젝트 > templates(dir) > base.html 생성
bootstarp 사용 (minified 버전 == ajax 기능 제공)

base.html을 사용하기 위해서는

config/settings.py

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',
            ],
        },
    },
]

경로를 설정해주면 된다.

templates/base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>onlineShop</title>
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
    <link rel="icon" href="/favicon.ico" type="image/x-icon">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
          integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
            crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
            crossorigin="anonymous"></script>
    <!--    jQuery minified 버전-->
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"
            integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>

    {% block script %}
    {% endblock %}

    {% block style %}
    {% endblock %}

</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <a class="navbar-brand" href="/">배민 살짝 따라한 사이트</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse justify-content-end" id="navbarSupportedContent">
        <ul class="navbar-nav justify-content-end">
            {% if user.is_authenticated %}
            <li class="nav-item">
                <a class="nav-link btn btn-outline-info" href="{% url 'account_logout' %}">로그아웃</a>
                {% else %}
                <a class="nav-link btn btn-outline-info" href="{% url 'account_login' %}">로그인</a>
                    {% load socialaccount %}
                    {% providers_media_js %}

                    <a href="{% provider_login_url 'naver' %}">네이버로그인</a>
                    <a href="{% provider_login_url 'kakao' %}">카카오로그인</a>
                {% endif %}
            </li>
            &nbsp;&nbsp;
            <li class="nav-item">
                <a class="nav-link btn btn-outline-info" href="">장바구니
                    {% if cart|length > 0 %}
                        <span class="badge badge-pill badge-primary">{{cart|length}}</span>
                    {% else %}
                        : 비어있음
                    {% endif %}
                </a>
            </li>
        </ul>
        &nbsp;&nbsp;
        <button class="btn btn-outline-info my-2 my-sm-0" type="submit">검색</button>
    </div>
</nav>
<div class="container">
    {% block content %}
    {% endblock %}
</div>

</body>
</html>

Shop - Template

  1. shop/template/shop/list.html
<!-- base.html을 상속 받음 -->
{% extends 'base.html' %}
<!-- 해당 페이지의 title을 작성 -->
{% block title %}Category Page{% endblock %}
<!-- 여기서부터 확장 받은 것 -->
{% block content %}
<br>
<div class="row">
    <div class="col-2"> <!--col-2 : 2조각-->
        <div class="list-group">
            <a href="/" class="list-group-item list-group-item-action list-group-item-secondary
{% if not current_category%}active{% endif %}">All</a>
            {%for c in categories %}
            <a href="{{c.get_absolute_url}}" class="list-group-item list-group-item-action list-group-item-secondary
{% if current_category.slug == c.slug %}active{% endif %}">
                {{c.name}}</a>
            {% endfor %}
        </div>
    </div>
    <div class="col">
        <div class="alert alert-danger" role="alert">
            {% if current_category %}{{current_category.name}}를/을 판매하고 있는 스토어입니다.
            {% else %} 모든 스토어 {% endif %}
        </div>
        <div class="row">
            {% for product in products %}
            <div class="col-sm-4">
                <div class="card">
                    <img class="card-img-top" src="{{product.image.url}}" alt="Product image" width="466px" height="270">
                    <div class="card-body" style="text-align:center">
                        <h5 class="card-title">{{product.name}}</h5>
                      	<!-- 가격을 정수형으로 보이기 위해서 humanize를 사용  -->
                      	<!-- settings.py에다가 설치한 앱 django.contrib.humanize 등록하자 -->
                        {% load humanize %}
                        <img class="card-img" src="{{product.baeminImage.url}}" alt="QRcode image" style="width:70px; height:70px; text-align:center;"><p/>
                            <h4 class="badge badge-light">
                              <!-- 천 단위 구분 쉼표를 포함한 정수형 출력 -->
&#8361;{{product.price | floatformat:'0' | intcomma}}</h4><p/>
                        <a href="{{product.get_absolute_url}}" class="btn btn-outline-info">View Detail</a>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
    </div>
</div>
{% endblock %}

2. shop/templates/shop/detail.html
{% extends 'base.html' %}
{% block title %}Product Detail{% endblock %}
{% block content %}
<br>
<div class="container">
    <div class="row">
        <div class="col-4">
            <img src="{{product.image.url}}" width="300px" height="300px">
        </div>
        <div class="col">
            <h2 class="display-6">{{product.name}}</h2>
            <p><span class="badge badge-light">가격</span>
                {% load humanize %}
                &#8361;{{product.price | floatformat:'0' | intcomma}}</p>
            <form action="{% url 'cart:product_add' product.id %}" method="post">
                {{add_to_cart}}
                {% csrf_token %}
                <input type="submit" class="btn btn-outline-info btn-sm" value="장바구니에 담기">
            </form>
            <br>
            <h5><span class="badge badge-light"><a href="{{product.url}}">홈페이지 바로가기</a></span></h5>
            <br>
            <h5><span class="badge badge-secondary">설명</span>{{product.description|linebreaks}}</h5>
        </div>
    </div>
    <br><br>
            {% load disqus_tags %}
            {% disqus_show_comments %}
</div>
{% endblock %}

필자는 disqus를 사용하기 위해서 settings.py > installed_apps에 disqus를 추가하고

pip intall six 를 해준 후

C:\Users\djqtdj\venv\Lib\site-packages\disqus\__init__.py 경로에서

from six.moves.urllib_parse import urlencode
from six.moves.urllib_request import urlopen

이런식으로 바꿔줬다. 혹시나 안되는 사람들 (detail.html이 안된다면) 이렇게 해보자


Shop-Admin

shop/admin.py

from django.contrib import admin
from .models import *

# Register your models here.
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug']
    prepopulated_fields = {'slug': ('name',)}

class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug', 'category', 'price', 'stock', 'url',
                    'available_display',
                    'available_order', 'created', 'updated']
    prepopulated_fields = {'slug': ('name',)}
    list_editable = ['price', 'stock', 'available_display',
                     'available_order']

admin.site.register(Category, CategoryAdmin)
admin.site.register(Product, ProductAdmin)

Login - NAVER

소셜 로그인 -> 기존에 있는 유명한 앱을 다운로드해서 사용

pip install django-allauth

config/settings.py

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    # 나중에 더 등록하고 싶으면 뒤에 .facebook, .google 등등 넣어주면 된다
    'allauth.socialaccount.providers.naver',
    'allauth.socialaccount.providers.kakao',
    
# allauth 방식을 추가하기 위해서 쓰는 것    
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]

SOCIALACCOUNT_LOGIN_ON_GET = True
LOGIN_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
ACCOUNT_LOGOUT_ON_GET = True

ACCOUNT_AUTHENTICATION_METHOD = 'username'

ACCOUNT_EMAIL_VERIFICATION = 'none'

ACCOUNT_EMAIL_REQUIRED = False

필자는 site_id 에서 오류가 났기 때문에 따로 site_id는 지정하지 않았음
밑에 여러개 쓰여있는 것들 중 login redirect와 account logout redirect는 로그인/로그아웃하고 어디로 이동할 것인지 작성하는 것이다.

config/urls.py

path('accounts/', include('allauth.urls')),

새로운 앱을 추가 했으니 당연히
python manage.py migrate 를 해준다.

자! 이제 naverlogin 부터 해보자

https://developers.naver.com/apps/#/register?api=nvlogin

사이트에 들어가서

이렇게 되어있으니 잘 선택해주자 필자는 이름만 받았다.

우리는 pc에서 할 것이므로 pc 환경을 선택해주고

이렇게 url도 등록해주자

이렇게 되면

clicent id와 client secret이 발급되는데 메모장에 미리 적어두자

django admin 페이지에 들어가서

이렇게 설정해주면 된다.

나중에 로그인이 성공하게 되면

이렇게 social accounts 에 뜨게 된다.


Login - KAKAO

https://developers.kakao.com/console/app

해당 사이트에 방문해서 애플리케이션을 추가하자

이제 앱 정보를 들어가보면

이렇게 나와있다 우린 여기서 REST API 키를 사용할 것이다.

현재 탭(요약 정보) 에서 플랫폼으로 들어오면

이렇게 보이는데 Web을 위 사진과 같이 수정한다.

또한 하단에 있는 등록하러가기 링크를 눌러서 밑에 사진과 같이 설정해준다.
다른 설명/소개글을 보면 callback하고 /를 안넣는데 필자는 넣지않으면 실행이 안되어서 넣어주었다.

아래 사진과 같이 사이드 바에 이렇게 동의항목, 보안을 설정해 줄 것인데 동의항목은 네이버와 마찬가지로 자신이 원하는 항목을 선택해주면 됨으로 스킵하겠다.

보안탭에서

코드를 받을 수 있는데 이는 네이버와 마찬가지로 secret키 이다.
활성화 상태를 사용함으로 꼭 바꿔주고 admin 페이지로 넘어오자

아까와 마찬가지로 이렇게 등록을 해주면 이제 거의 다 끝났다.

로그인 버튼을 눌러서 들어갈 수도 있겠지만 제공해주는 템플릿이 이쁘지 않아서 따로 버튼을 만들어줬다. kakao, naver로그인을 각각 눌러보면 로그인 되는 것을 확인할 수 있는데
성공 여부를 확실하게 보려면 admin페이지에 자신이 등록되어있는지 확인하면 된다.


다음시간엔 장바구니를 구현하겠다
이만 끝

profile
천천히 꾸준히 하는 블로그

0개의 댓글