me

[python/development] project Ti Manager2 개발
2025.06.05 / , , ,

project Ti Manager 란?

project Ti Manager2는 Ti 업무 의뢰, 업무 배정, 진행 상태 공유, 업체 별 통계 등 업무상 Ti 관련 전반에 쓰는 업무용 웹 사이트이다.

Ti Manager1은 당시 파이썬 2달 배우고 3달째에 바로 장고로 만들었던 툴이라 오류가 많았고, 6년째 데이터가 쌓이다 보니 사이트가 동작을 하지 못할 정도로 느려렸다.

이번에 로딩에 관련 DB 설계에 더욱 중점을 두고 일부 기능 변경을 하여 새로 만들기로 했다. 추가로 연도 별 데이터 엑셀 추출도 이번에 넣기로 했다.

프로젝트 기간: 06.05 ~


기본 환경 세팅

파이썬 장고에 기반을 두고 mariaDB로 진행 / 기존 ti1에서 DB 커스텀 이전(항목 일부 상이)

기본 설치

# 가상환경
D:\Python\python311\python.exe -m venv vTiManager2

# 장고
pip install django

# MySQL DB 
pip install pymysql

# 장고 CORS
pip install django-cors-headers

# iis 세팅용
pip install wfastcgi

# html 접근
pip install beautifulsoup4

# 엑셀 다운로드
pip install openpyxl

# 이미지 필드 사용을 위해서
pip install Pillow

# 메일링
pip install pywin32

# 기본 프로젝트
django-admin startproject back .

# 앱
python manage.py startapp org      # 사원 관련
python manage.py startapp web
python manage.py startapp setting
python manage.py startapp utils
python manage.py startapp report

back/setting

import os
import pymysql
from pathlib import Path
from django.contrib.messages import constants as messages

pymysql.install_as_MySQLdb()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-^t4f83^734b1jkn@gefq!=@u$n@lv)=t0u1ez+2=l$f%m%)s^k'
DEBUG = True
ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',  # 장고 CORS

    'org.apps.OrgConfig',
    'web.apps.WebConfig',
    'setting.apps.SettingConfig',
    'utils.apps.UtilsConfig',
]

AUTH_USER_MODEL = 'org.User'

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'back.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],     # 전역 템플릿 폴더 세팅
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'back.wsgi.application'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'ti2',
        'USER': 'root',              #root
        'PASSWORD': '0000',          #password
        'HOST': 'localhost',
        'PORT': '3306',
    }
}

# 쉬운 비밀번호 사용을 위해 비밀번호 유효성 검사기 제거
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 4,  # 최소 길이를 4로 설정
        }
    },
]

LANGUAGE_CODE = 'ko'
TIME_ZONE = 'Asia/Seoul'
USE_I18N = True
USE_TZ = True

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

MESSAGE_TAGS = {
    messages.DEBUG: 'debug',
    messages.INFO: 'info',
    messages.SUCCESS: 'success',
    messages.WARNING: 'warning',
    messages.ERROR: 'danger',
}

# 보안 설정 추가
SECURE_CONTENT_TYPE_NOSNIFF = True   # 브라우저가 Content-Type 헤더를 무시하지 못하도록 설정 - 콘텐츠 스니핑(콘텐츠 유형 추측)으로 인한 공격을 방지
SECURE_BROWSER_XSS_FILTER = True     # 브라우저의 XSS(크로스 사이트 스크립팅) 필터를 활성화 - 사용자가 입력한 데이터가 악의적으로 조작되어 XSS 공격에 악용되는 것을 방지
X_FRAME_OPTIONS = 'DENY'             # 페이지가 iframe으로 렌더링되는 것을 방지 - 클릭재킹(clickjacking) 공격을 방지
SECURE_CROSS_ORIGIN_OPENER_POLICY = None   # Django에서 기본으로 설정되는 Cross-Origin-Opener-Policy (COOP) 헤더를 비활성화



# 로그인 아웃 후 리다이렉션 경로
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/login/'

back/urls

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index, name='index'),
    path('', include('org.urls')),
    path('web/', include('web.urls')),
    path('setting/', include('setting.urls')),
]

# 개발 환경에서 미디어 파일 제공
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

org/admin.py

from django import forms
from django.db import models
from django.urls import path
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from org.models import Department, Team, User, Member
from org.views import member_update


# OUTLOOK_AVAILABLE 변수를 전역에서 정의
try:
    import win32com.client
    OUTLOOK_AVAILABLE = True
except ImportError:
    OUTLOOK_AVAILABLE = False


@admin.register(Department)
class AdminDepartment(admin.ModelAdmin):
    list_display = ['department_name']
    list_display_links = ['department_name']
    list_filter = ['department_name']

@admin.register(Team)
class AdminTeam(admin.ModelAdmin):
    list_display = ['team_name']
    list_display_links = ['team_name']
    list_filter = ['team_name']

class CustomUserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = '__all__'


class CustomUserAdmin(BaseUserAdmin):
    form = CustomUserForm
    formfield_overrides = {
        models.DecimalField: {'widget': forms.NumberInput(attrs={'step': '0.01'})},
    }

    # 관리자 페이지에 표시될 필드 설정
    list_display = ['user_name', 'email', 'user_phone', 'user_department', 'user_team', 'user_position']
    list_display_links = ['user_name', 'email']
    list_filter = ['user_name', 'email', 'user_team', 'user_department']
    search_fields = ['user_name', 'email', 'user_department', 'user_team']
    ordering = ['user_name']

    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('user_name', 'user_phone', 'user_img', 'user_department', 'user_team', 'user_position')}),
        ('Permissions', {'fields': ('is_staff', 'is_manager', 'is_superuser', 'groups', 'user_permissions')}),
        ('Important dates', {'fields': ('last_login',)}),
    )

    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2', 'user_name', 'user_phone', 'user_img', 'user_department', 'user_team', 'user_position', 'user_start')}
         ),
    )

# 커스텀 UserAdmin을 등록
admin.site.register(User, CustomUserAdmin)


@admin.register(Member)
class MemberAdmin(admin.ModelAdmin):
    change_list_template = "admin/member.html"
    list_display = ['member_name', 'member_email', 'member_team', 'member_department', 'member_position']
    ordering = ['member_department', 'member_team', 'member_name']

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path('member_update/', self.member_update, name='member_update'),
        ]
        return custom_urls + urls

    def member_update(self, request):
        return member_update(self, request)

org/forms

from django import forms
from django.forms.widgets import ClearableFileInput
from django.contrib.auth import password_validation
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import PasswordChangeForm as DjangoPasswordChangeForm
from django.core.exceptions import ValidationError
from .models import User


class LoginForm(AuthenticationForm):
    username = forms.EmailField(
        label="이메일",
        widget=forms.EmailInput(attrs={"autofocus": True, "value": ""})  # 기본값을 빈 값으로 설정
    )
    password = forms.CharField(label="비밀번호", widget=forms.PasswordInput)


# 프로필 변경에 기존 취소를 이미지 삭제로 텍스트 변경
class CustomClearableFileInput(ClearableFileInput):
    clear_checkbox_label = '이미지 삭제만 원할 경우 체크 후 SAVE'  # '취소'를 '이미지 삭제'로 변경


class ProfileUpdateForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['user_name', 'user_phone', 'user_department', 'user_team', 'user_position', 'user_img']
        widgets = {
            'user_img': CustomClearableFileInput,  # 사용자 정의 ClearableFileInput 적용
        }


class PasswordChangeForm(forms.Form):
    old_password = forms.CharField(widget=forms.PasswordInput(), label="기존 비밀번호")
    new_password = forms.CharField(widget=forms.PasswordInput(), label="새 비밀번호")
    confirm_password = forms.CharField(widget=forms.PasswordInput(), label="비밀번호 확인")

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user', None)  # 현재 사용자를 전달 받음
        super().__init__(*args, **kwargs)

    def clean_old_password(self):
        old_password = self.cleaned_data.get('old_password')
        if not self.user.check_password(old_password):
            raise forms.ValidationError("기존 비밀번호가 일치하지 않습니다.")
        return old_password

    def clean(self):
        cleaned_data = super().clean()
        new_password = cleaned_data.get('new_password')
        confirm_password = cleaned_data.get('confirm_password')

        if new_password != confirm_password:
            raise forms.ValidationError("새 비밀번호가 일치하지 않습니다.")

        # Django의 기본 비밀번호 유효성 검사를 사용
        try:
            password_validation.validate_password(new_password, self.user)
        except ValidationError as e:
            self.add_error('new_password', e)

        return cleaned_data

org/models

from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin


class Department(models.Model):
    department_code = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='본부 고유코드')
    department_name = models.CharField(verbose_name='본부 이름 ', max_length=50)

    def __str__(self):
        return self.department_name


class Team(models.Model):
    team_code = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='팀 고유코드')
    team_name = models.CharField(verbose_name='팀 이름 ', max_length=50)
    department = models.ForeignKey(Department, verbose_name='부서 선택', on_delete=models.CASCADE, null=True, blank=True)

    def __str__(self):
        return self.team_name


class UserManager(BaseUserManager):
    # 유저 생성 메서드
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError('이메일을 아이디로 쓰고 있습니다. 이메일은 필수입니다.')

        email = self.normalize_email(email)  # 이메일 형식을 표준화 (대소문자 구분 없이)
        user = self.model(email=email, **extra_fields)  # 유저 생성
        user.set_password(password)  # 비밀번호 설정
        user.save(using=self._db)  # 데이터베이스에 저장
        return user

    # 관리자(슈퍼유저) 생성 메서드
    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_manager', True)
        return self.create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    POSITION_CHOICES = [
        ('intern', '인턴'),
        ('staff', '사원'),
        ('associate', '주임'),
        ('assistant', '대리'),
        ('manager', '과장'),
        ('chief', '차장'),
        ('director', '부장'),
        ('leader', '팀장'),
        ('executive', '이사'),
        ('CEO', '대표이사')
    ]

    user_code = models.AutoField(primary_key=True)  # 자동 증가하는 고유 코드
    email = models.EmailField(unique=True)  # 로그인에 사용되는 이메일 (중복 불가)
    password = models.CharField(max_length=128)  # 기본 비밀번호 필드 (AbstractBaseUser에서 제공)
    user_name = models.CharField(max_length=100)  # 사용자 이름
    user_phone = models.CharField(max_length=15, null=True, blank=True)  # 전화번호 필드
    user_img = models.ImageField(upload_to='profiles/', null=True, blank=True)  # 프로필 이미지
    user_department = models.ForeignKey(Department, on_delete=models.CASCADE, null=True, blank=True)  # 소속 본부 정보
    user_team = models.ForeignKey(Team, on_delete=models.CASCADE, null=True, blank=True)
    user_position = models.CharField(max_length=50, choices=POSITION_CHOICES)  # 직급 정보 (인턴, 사원 등)
    is_manager = models.BooleanField(default=False)  # 관리자 여부
    is_staff = models.BooleanField(default=False)  # 관리자 페이지 접근 여부
    is_working = models.BooleanField(default=True)  # 근무 여부
    create_time = models.DateTimeField(auto_now_add=True)  # 계정 생성 시간
    update_time = models.DateTimeField(auto_now=True)  # 업데이트 시간
    objects = UserManager()  # 커스텀 매니저 설정

    # 이메일을 로그인 아이디로 사용하기 위한 설정
    USERNAME_FIELD = 'email'  # 로그인에 사용할 필드를 이메일로 설정

    def __str__(self):
        return self.user_name  # 객체 출력 시 유저 이름 반환


class Member(models.Model):
    POSITION_CHOICES = [
        ('etc', '고객사'),
        ('intern', '인턴'),
        ('staff', '사원'),
        ('associate', '주임'),
        ('assistant', '대리'),
        ('manager', '과장'),
        ('chief', '차장'),
        ('director', '부장'),
        ('leader', '팀장'),
        ('executive', '이사'),
        ('CEO', '대표이사')
    ]

    member_code = models.AutoField(primary_key=True)  # 자동 증가하는 고유 코드
    member_email = models.EmailField(unique=False)  # 유니크하지 않은 이메일 필드
    member_name = models.CharField(max_length=100)  # 이름
    member_phone = models.CharField(max_length=15, null=True, blank=True)  # 전화번호
    member_img = models.ImageField(upload_to='profiles/', null=True, blank=True)  # 프로필 이미지
    member_department = models.ForeignKey(Department, on_delete=models.CASCADE, null=True, blank=True)  # 소속 본부
    member_team = models.ForeignKey(Team, on_delete=models.CASCADE, null=True, blank=True)  # 소속 팀
    member_position = models.CharField(max_length=50, choices=POSITION_CHOICES)  # 직급 정보
    member_start = models.DateField(null=True, blank=True)  # 입사일
    is_working = models.BooleanField(default=True)  # 근무 여부
    create_time = models.DateTimeField(auto_now_add=True)  # 계정 생성 시간
    update_time = models.DateTimeField(auto_now=True)  # 업데이트 시간

    def __str__(self):
        return f"{self.member_team} - {self.member_name}"

    def save(self, *args, **kwargs):
        # 소속팀이 비어있는 경우 "한샘글로벌" 팀을 기본값으로 설정
        if self.member_team is None:
            hanssem_global_team, created = Team.objects.get_or_create(team_name="한샘글로벌")
            self.member_team = hanssem_global_team
        super().save(*args, **kwargs)

org/urls

from django.urls import path, include
from django.contrib.auth import views as auth_views
from . import views
from org.admin import MemberAdmin


# app_name = 'org'  # org만 네임스페이스 없이 사용

urlpatterns = [
    path('login/', views.login_view, name='login'),
    path('logout/', auth_views.LogoutView.as_view(next_page='/login/'), name='logout'),  # 로그아웃 URL 추가

    path('mypage/', views.mypage, name='mypage'),
    path('profile/', views.profile, name='profile'),
    path('password/', views.password, name='password'),
]

org/views

import os
import csv
from datetime import datetime
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash, authenticate, login
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from org.models import Department, Team, Member
from org.forms import LoginForm, PasswordChangeForm, ProfileUpdateForm


def login_view(request):
    if request.method == 'POST':
        form = LoginForm(request, data=request.POST)
        if form.is_valid():
            email = form.cleaned_data.get('username') or ''
            password = form.cleaned_data.get('password') or ''

            # 이메일을 사용해 사용자 인증
            user = authenticate(request, username=email, password=password)

            if user is not None:
                login(request, user)
                next_url = request.GET.get('next', '/mypage/')
                return redirect(next_url)
            else:
                # 로그인 실패 시 메시지 추가
                messages.error(request, '로그인 실패: 이메일 또는 비밀번호를 확인하세요.')
        else:
            # 폼 유효성 검사 실패 시 수동으로 오류 처리
            for field, errors in form.errors.items():
                for error in errors:
                    messages.error(request, error)  # 각 필드의 오류를 메시지로 전달
    else:
        form = LoginForm(initial={'username': ''})
    return render(request, 'login.html', {'form': form})


@login_required
def mypage(request):
    user = request.user
    return render(request, 'mypage.html', {
        'user': user,
    })


@login_required
def profile(request):
    user = request.user
    if request.method == 'POST':
        form = ProfileUpdateForm(request.POST, request.FILES, instance=user)  # instance로 기존 데이터 연결
        if form.is_valid():
            form.save()
            messages.success(request, '프로필이 성공적으로 업데이트되었습니다.')
            return redirect('mypage')  # 성공 시 마이페이지로 리디렉션
        else:
            messages.error(request, '프로필 업데이트에 실패했습니다.')
    else:
        form = ProfileUpdateForm(instance=user)  # 기존 데이터로 폼을 미리 채움

    return render(request, 'profile.html', {'form': form, 'user': user})


@login_required
def password(request):
    if request.method == 'POST':
        form = PasswordChangeForm(user=request.user, data=request.POST)
        if form.is_valid():
            user = request.user
            user.set_password(form.cleaned_data['new_password'])  # 비밀번호 변경
            user.save()
            messages.success(request, "비밀번호가 성공적으로 변경되었습니다.")
            update_session_auth_hash(request, user)  # 세션 갱신
            return redirect('mypage')  # 성공 시 마이페이지로 리디렉션
        else:
            messages.error(request, "비밀번호 변경에 실패했습니다. 다시 시도해주세요.")
            return render(request, 'password.html', {'form': form})
    else:
        form = PasswordChangeForm(user=request.user)
    return render(request, 'password.html', {'form': form})


# 부서 정보를 계산하는 함수 (공백 제거 추가)
def get_department(team_name):
    team_name_cleaned = team_name.replace(" ", "")  # 팀 이름의 모든 공백을 제거
    department_mapping = {
        '경영지원팀': None,
        '매뉴얼개발팀': '콘텐츠기획본부',
        '콘텐츠개발팀': '콘텐츠기획본부',
        '시스템개발팀': '콘텐츠기획본부',
        '현지화개발팀': '랭귀지서비스본부',
        '랭귀지서비스팀': '랭귀지서비스본부',  # 공백이 없는 팀 이름
        'CMD팀': 'CMD본부',
        '콘텐츠기획본부': '콘텐츠기획본부',
        '랭귀지서비스본부': '랭귀지서비스본부',
        'CMD본부': 'CMD본부',
        '세일즈&마케팅본부': '세일즈&마케팅본부',
        '미국지사': '미국지사',
        '베트남지사': '베트남지사'
    }
    return department_mapping.get(team_name_cleaned, None)


# 직급 정보를 매칭하는 함수 (공백 제거 추가)
def get_position(raw_position):
    if raw_position:
        position_cleaned = raw_position.split('(')[0].strip().replace(" ", "")  # 괄호 앞부분과 공백 제거
    else:
        position_cleaned = 'staff'

    POSITION_CHOICES = [
        ('intern', '인턴'),
        ('staff', '사원'),
        ('associate', '주임'),
        ('assistant', '대리'),
        ('manager', '과장'),
        ('chief', '차장'),
        ('director', '부장'),
        ('leader', '팀장'),
        ('executive', '이사'),
        ('CEO', '대표이사')
    ]

    for key, value in POSITION_CHOICES:
        if position_cleaned in value.replace(" ", ""):  # 직급도 공백을 제거하고 비교
            return key
    return None  # 매칭되지 않을 경우 기본값



# CSV 파일을 이용해 Member 데이터베이스를 업데이트하는 함수
def member_update(self, request):
    csv_file_path = os.path.join('static', 'file', 'outlook_out.csv')

    if not os.path.exists(csv_file_path):
        messages.error(request, "CSV 파일이 존재하지 않습니다.")
        return redirect('admin:org_member_changelist')

    with open(csv_file_path, mode='r', encoding='utf-8-sig') as file:
        reader = csv.DictReader(file)
        for row in reader:
            # 이메일이 존재하는지 확인
            member_email = row.get('email')
            if not member_email:
                continue  # 이메일이 없으면 처리하지 않음

            # 이름에서 + 또는 * 표시 앞의 값만 사용
            member_name = row.get('name').split('+')[0].split('*')[0].strip()

            # 직급 정보에서 괄호 앞 부분만 사용, 기본값 'staff' 설정
            member_position = get_position(row.get('position', '')) or 'staff'

            # 부서에 따른 본부 계산
            member_team_name = row.get('department', '')
            member_department_name = get_department(member_team_name)

            # 부서 이름으로 Department 인스턴스를 조회하거나 생성
            member_department = None
            if member_department_name:
                member_department, created = Department.objects.get_or_create(department_name=member_department_name)

            # 팀 이름으로 Team 인스턴스를 조회하거나 생성
            member_team = None
            if member_team_name:
                member_team, created = Team.objects.get_or_create(team_name=member_team_name)

            # 입사 날짜 추출 및 포맷 변환 (입사일이 없으면 None으로 처리)
            join_date = row.get('join_date')
            member_start = None
            if join_date and join_date != "입사일 없음":
                try:
                    # join_date가 YYYY.MM.DD 형식일 경우 YYYY-MM-DD로 변환
                    member_start = datetime.strptime(join_date, '%Y.%m.%d').date()
                except ValueError:
                    member_start = None  # 형식이 맞지 않으면 None으로 처리

            # 핸드폰 번호
            member_phone = row.get('phone')

            # 기존 이메일 값이 있는지 확인 후 업데이트 또는 새로 생성
            member, created = Member.objects.update_or_create(
                member_email=member_email,
                defaults={
                    'member_name': member_name,
                    'member_phone': member_phone,
                    'member_department': member_department,  # Department 인스턴스 할당
                    'member_team': member_team,  # Team 인스턴스 할당
                    'member_position': member_position,  # 기본값 할당
                    'member_start': member_start,  # 변환된 날짜 값 할당
                }
            )

            if created:
                messages.success(request, f"{member_name} 님이 새로 추가되었습니다.")
            else:
                messages.success(request, f"{member_name} 님의 정보가 업데이트되었습니다.")

    # 파일 처리 후 리디렉션
    return redirect('admin:org_member_changelist')

web/urls

from django.urls import path, re_path
from . import views


urlpatterns = [
    path('', views.index, name='index'),
]

web/views

from django.shortcuts import render

def index(request):
    return render(request, 'index.html')

setting/urls

from django.urls import path
from . import views

urlpatterns = [
    path('', views.setting_view, name='setting'),
]

setting/view

from django.shortcuts import render

def setting_view(request):
    return render(request, 'setting.html')

기타, urls – utils 앱 제외

from django.urls import path
from . import views

urlpatterns = []

파이참 세팅


DB 생성

마리아 DB 설치 방법: [python/setting] 파이썬 – Django 설치 + 셋팅

create database ti2;        # 데이터베이스 생성
use ti2;                    # ti2 DB 사용
show databases;             # 데이터베이스 생성 확인

최초 migrate

마이그레이션 하기 전 필수 주의 사항! 각, 앱마다 migrations/init.py – 파일들이 있어야 makemigrations 시 앱 인식이 제대로 된다. 이때 init.py는 내부 빈 파일이면 된다.

(보통은 자동 생성되는데 내가 일부 세팅 파일을 그냥 복사해서 쓰다 보니 생긴 문제)

python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser