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

# 기본 프로젝트
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

DB 생성

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

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

org/model

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)  # 직급 정보 (인턴, 사원 등)
    user_start = models.DateField(null=True, blank=True)  # 입사일
    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()  # 커스텀 매니저 설정
    etc_leave = models.DecimalField(
        max_digits=5,  # 최대 5자리까지 저장 가능 (정수부 + 소수부)
        decimal_places=2,  # 소수점 이하 2자리까지 저장 가능
        default=0,
        verbose_name='기존 사용 연차'
    )

    # 이메일을 로그인 아이디로 사용하기 위한 설정
    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/admin


web/urls

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


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

관리자 세팅

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

back/settings

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',
    'report.apps.ReportConfig',
]

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')]


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')),
    path('report/', include('report.urls')),
]

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

파이참 세팅