me

[python/development] project Ameria2개발
2024.12.13 / , ,

project Amelia2 란?

project Amelia2 는 내부 팀에서 외부팀으로 전달할 자료나 프로그램 업데이트 관련 내용을 전달할 때 쓰는 게시판이다.

기존 장고로 2021년에 제작 했는데 이번에 사용하던 서버 교체로 데이터를 옮기다가 이참에 안 쓰는 게시판은 정리하고 파이썬 버전도 3.8.10에서 3.11.6으로 옮기고 mysqlclient도 PyMySQL로 변경해서 가져오기로 했다.

프로젝트 기간: 11.30 ~ 12.13


기본 환경 세팅

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

기본 설치

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

# 장고
pip install django

# MySQL DB 
pip install pymysql

# 장고 CORS
pip install django-cors-headers

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

# iis 세팅용
pip install wfastcgi

# 장고 글쓰기 에디터
pip install django-ckeditor-5

# html 접근
pip install beautifulsoup4

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

# 앱
python manage.py startapp org
python manage.py startapp web
python manage.py startapp history
python manage.py startapp handover
python manage.py startapp guide
python manage.py startapp setting
python manage.py startapp utils

DB생성

CREATE DATABASE amelia2 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

사용자 생성

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

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',
    'history.apps.HistoryConfig',
    'handover.apps.HandoverConfig',
    'guide.apps.GuideConfig',
    'setting.apps.SettingConfig',
    'utils.apps.UtilsConfig',

    'django_ckeditor_5',
]

AUTH_USER_MODEL = 'org.User'

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    ...
]

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


DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'amelia2',
        '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'


LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'verbose': {
            'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
            'style': '{',
        },
        'simple': {
            'format': '{levelname} {message}',
            'style': '{',
        },
    },
    'filters': {
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': 'error.log',
            'formatter': 'verbose'
        },
    },
    'loggers': {
        'django': {
            'handlers': ['console', 'file'],
            'level': 'INFO',
        },
        'task': {  # 'task'는 당신의 앱 이름입니다. 필요에 따라 변경하세요.
            'handlers': ['console', 'file'],
            'level': 'INFO',
            'propagate': False,
        },
    }
}

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

CKEDITOR_5_CONFIGS = {
    'default': {
        'toolbar': [
            'heading', '|',
            'highlight', 'fontColor', 'fontBackgroundColor', 'fontSize', '|',
            'insertTable', 'blockQuote', 'link',  'imageUpload', 'mediaEmbed', 'codeBlock',  '|',
            'bulletedList', 'numberedList', 'alignment', 'outdent', 'indent', 'horizontalLine', '|',
            'bold', 'italic', 'underline', 'strikethrough', '|',
            'undo', 'redo', '|',
        ],

        'extra_plugins': ['ImageUpload', 'Clipboard', 'Image', 'ImageToolbar', 'ImageStyle'],

        'indentBlock': {
            'classes': [
                'indent-1', 'indent-2', 'indent-3', 'indent-4'
            ],
            'offset': 40,
            'unit': 'px',
            'tags': ['p', 'ul', 'ol', 'blockquote', 'figure'],
        },


        'fontSize': {
            'options': [10, 12, 'default', 16, 18, 20, 24, 28, 32, 40]
        },

        'alignment': {
            'options': ['left', 'center', 'right', 'justify']
        },

        'image': {
            'toolbar': [
                'imageTextAlternative',
                'imageStyle:alignLeft',
                'imageStyle:alignCenter',
                'imageStyle:alignRight',
            ],
            'styles': [
                'alignLeft',
                'alignCenter',
                'alignRight'
            ],
        },

        'ckfinder': {
            'uploadUrl': '/ckeditor5/image_upload/',
        },


        'table': {
            'contentToolbar': ['tableColumn', 'tableRow', 'mergeTableCells']
        },
        'heading': {
            'options': [
                {'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph'},
                {'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1'},
                {'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2'},
                {'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3'}
            ]
        },
        'htmlSupport': {
            'allow': [
                {
                    'name': '*',  # 모든 태그 허용
                    'attributes': True,  # 모든 속성 허용
                    'classes': True,     # 모든 클래스 허용
                    'styles': True       # 모든 스타일 허용
                }
            ]
        },

        'fontColor': {
            'colors': [
                {
                    'color': '#000000',
                },
                {
                    'color': '#7F7F7F',
                },
                {
                    'color': '#BFBFBF',
                },
                {
                    'color': '#DFDFDF',
                },
                {
                    'color': '#FFFFFF',
                },
                {
                    'color': '#C00000',
                },
                {
                    'color': '#F79646',
                },
                {
                    'color': '#FFD966',
                },
                {
                    'color': '#9BBB59',
                },
                {
                    'color': '#00B050',
                },
                {
                    'color': '#4472C4',
                },

                {
                    'color': '#002060',
                },
                {
                    'color': '#7030A0',
                },
                {
                    'color': '#D58EBF',
                },
                {
                    'color': '#8B5E3C',
                },
            ],
            'columns': 5  # 색상을 한 줄에 몇 개씩 표시할지 설정
        },

        'fontBackgroundColor': {
            'colors': [
                {
                    'color': '#F2F2F2', #연한 회색(흰색에 가까운 배경)
                },
                {
                    'color': '#D9D9D9', # 밝은 회색
                },
                {
                    'color': '#BFBFBF', #중간 회색
                },
                {
                    'color': '#FDE9D9', #연한 주황빛
                },
                {
                    'color': '#FFF2CC', #연한 노랑
                },
                {
                    'color': '#E4F0E2', # 연한 연두
                },
                {
                    'color': '#D9EAD3', #부드러운 초록
                },
                {
                    'color': '#D9EAF3', # 연한 하늘색
                },
                {
                    'color': '#D9DFF4', # 연한 파란색
                },
                {
                    'color': '#EAD1DC', # 연한 핑크
                },
                {
                    'color': '#F4CCCC', #연한 빨강
                },
                {
                    'color': '#FCE4D6', # 연한 주황
                },
                {
                    'color': '#F9CB9C', # 중간 주황
                },
                {
                    'color': '#D9D2E9', # 연보라색
                },
                {
                    'color': '#F6E3DA', #부드러운 크림색
                },
            ],
            'columns': 5  # 색상을 한 줄에 몇 개씩 표시할지 설정
        },

        'language': 'ko',
        'removePlugins': ['WordCount'],
    }
}

# 보안 설정 추가
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) 헤더를 비활성화


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


# CKEditor 5 업로드 경로
CKEDITOR_5_UPLOAD_PATH = 'uploads/'


# 정적 파일 모아서 static에 넣어주기
# STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# python manage.py collectstatic


# 미디어 파일 경로 설정
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

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

back/utls.py

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('org/', include('org.urls')),
    path('web/', include('web.urls')),
    path('history/', include('history.urls')),
    path('handover/', include('handover.urls')),
    path('guide/', include('guide.urls')),
    path('setting/', include('setting.urls')),

    path('ckeditor5/', include('django_ckeditor_5.urls')),
    path('ckeditor5/image_upload/', views.upload_image, name='upload_image'),
]

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

Ameria2 DB 이전

HeidiSQL를 이용해서 원하는 데이터를 우클릭 후 [데이터베이스를 SQL로 내보내기] 후에 노트 패드 같은 에디터 프로그램 아무거나 열어서 데이터 수정 한 후

MySQL Client (MariaDB 10.6 (x64)) 프로그램 열어서~

source D:/Develop/D_Amelia2/db/p.sql;
source D:/Develop/D_Amelia2/db/m.sql;
source D:/Develop/D_Amelia2/db/x.sql;

이런 식으로 실행해서 DB 덮어주기~ 하면 됨


Ameria2 결과

이번 아멜리아2는 회사 내 외부 팀과의 정보 공유가 목적이라서 글쓰기 에디터와 디테일이 중점을 두었다.

특이점은 탭이랑 워드에 비해 한없이 기능이 적은 에디터를 어떻게 꾸리기 ㅠㅡㅠ~ 정도~

처음엔 탭 수를 사용자가 임의로 만들 수 있게 하려다 보니 에디터를 프론트 단에서 붙여야 하는데 그럴 때 에디터 내부에 지원되는 범위가 더 작아서 고정 탭 3개로 하고 3개에 전부 장고 쪽에서 에디터 붙여서 쓰기로 했다.

한 예로 이미지 복사 넣기로 넣은 다음에 이미지 크기 조정 하려니 프론트 쪽 에디터에선 잘 안되던.. 백에서 붙인 에디터에선 가능했다.