회원가입

[개발] 홈 화면에 자기소개 넣기

Beany 2024-10-09

ChatGPT 요약

테크 블로그에서는 Django Constance를 사용하여 자기 소개 관리 시스템을 개발하는 과정을 소개하고 있다. 이 시스템은 Redis를 사용하여 값을 저장하며, Markdown 코드를 입력하여 자기 소개를 표현하는 기능을 구현한다. 프로필 사진, 이름, 자기 소개 등의 정보를 저장하고 사용자에게 보여주는 방법까지 상세하게 설명하고 있다. 이를 위해 Django Constance 라이브러리를 활용하고 있으며, 프로젝트에 필요한 설정과 코드를 제공하고 있다.

 

개발을 시작하기 전에 자세한 내용을 어떻게 작성할지 고민 중입니다. 처음에는 새로운 테이블을 생성할까 생각했지만, Django Constance라는 라이브러리가 있어 이를 사용하기로 했습니다.
https://django-constance.readthedocs.io/en/latest/

Django Constance는 단순한 기록을 쉽게 관리할 수 있는 도구로, 기본적으로는 Redis를 저장소로 이용하여 값을 가져옵니다. 단점으로는 데이터베이스가 아닌 Redis를 사용해 캐싱하기 때문에, 과거에 백업했던 SQL에는 해당 데이터가 포함되지 않는다는 점입니다. 그러나 대부분의 자기소개 데이터는 기록보다는 단발성으로 보여주는 값이기에, 별도의 백업 필요성을 느끼지 않아 Redis 캐싱만으로도 충분히 가치 있다고 생각합니다.
(참고: 데이터 백업 게시글)

이를 이용해 아래와 같은 내용을 저장할 계획입니다:

  • 프로필 사진
  • 이름
  • 자세한 자기 소개

자세한 자기 소개 부분은 Markdown 코드를 입력하면 적용되도록 개발할 예정입니다. 여러 정보가 포함될 수 있고, 이미지 및 다양한 요소가 추가될 수 있어 Markdown을 사용하면 자기 소개를 유연하게 표현할 수 있을 것이라고 생각했습니다.

 

이제 Django Constance를 다운로드합시다.

pip install django-constance[redis]


 

코드


config.settings.py

THIRD_APPS = [
    ...
    'constance',
]


CONSTANCE_IGNORE_ADMIN_VERSION_CHECK = True
CONSTANCE_FILE_ROOT = 'constance'

CONSTANCE_ADDITIONAL_FIELDS = {
    'image_field': ['django.forms.ImageField', {}]
}

CONSTANCE_CONFIG = {
    'PROFILE_DESCRIPTION_MARKDOWN': (
        '',
        '홈 화면에 보이는 자기 소개 MARKDOWN 부분',
    ),
    'PROFILE_IMAGE_URL': (
        '',
        '홈 화면에 보이는 자기 소개 프로필 사진 URL',
        'image_field',
    ),
    'PROFILE_NAME': (
        '',
        '홈 화면에 보이는 이름',
    ),
    'PROFILE_SIMPLE_DESCRIPTION': (
        '',
        '홈 화면에 보이는 간단 소개',
    ),
}

ImageField 도 정의해서 프로필 사진을 위해 Media 파일을 올릴 수 있도록 작업했습니다.

 

Response DTO 에 새로 들어갈 정보 4개를 정의합니다. profile_descrptionMarkDown 코드가 들어가니 Any 로 설정합니다. str 로 설정하면 자동으로 xss 방지하는 것 같아 작동이 잘 안됐습니다.

board.dtos.response_dtos.py

class HomeResponse(BaseModel):
    ...
    profile_description: Optional[Any] = Field(
        None,
        description='홈 화면에 Profile 정보 MarkDown',
    )
    profile_image_url: Optional[str] = Field(
        None,
        description='홈 화면에 Profile 사진 URL',
    )
    profile_name: Optional[str] = Field(
        None,
        description='홈 화면에 Profile 이름',
    )
    profile_simple_description: Optional[str] = Field(
        None,
        description='홈 화면에 Profile 간단 소개',
    )

 

이제 정의한 constance config 를 사용합니다.

board.views.py

def home(request):
    ...
    return render(
        request,
        BOARD_HOME_PATH,
        HomeResponse(
            ...
            profile_description=mark_safe(config.PROFILE_DESCRIPTION_MARKDOWN),
            profile_image_url=settings.MEDIA_URL + config.PROFILE_IMAGE_URL,
            profile_name=config.PROFILE_NAME,
            profile_simple_description=config.PROFILE_SIMPLE_DESCRIPTION,
        ).model_dump(),
    )

HomeResponse 에 응답값을 같이 보내줍니다. MarkDown 코드여서 mark_safe 를 이용해서 전달합니다.

PROFILE_IMAGE_URLmedia 폴더 값이 없어 직접 MEDIA_URL 또한 넣어줘야합니다.

 

templates.base.html

<div class="row">
        {% block my_profile %}{% endblock %}
        <div class="col-12 mb-3">{% block lesson %}{% endblock %}</div>
        <div class="col-12 mb-3">{% block top %}{% endblock %}</div>
        ...
    </div>

lesson 부분 위에 my_profile 이라는 block 을 정의합니다.

 

board.templates.board.home.html

css, javascrptMarkDown 코드 재정의로 자기소개 부분을 디자인 합니다.

<style>
.profile-description {
    display: flex;
    overflow-x: auto;
    flex-direction: column;
    white-space: nowrap;
    padding: 15px;
    border-radius: 10px;
    background: #F5F5F5;
}

.profile-description::-webkit-scrollbar{
    height: 5px;
}

.profile-description::-webkit-scrollbar-thumb{
    background-color: rgb(2, 10, 10);
    border-radius: 10px;
}

.profile-description::-webkit-scrollbar-track{
    background-color: rgba(1, 6, 6, 0.33);
    border-radius: 10px;
}
</style>
...
<script>
    $(document).ready(function() {
        const replaceList = [
            {from: /&gt;/g, to: '>'},
            {from: /&nbsp;/g, to: ' '},
            {from: /&lt;/g, to: '<'},
            {from: /&amp;/g, to: '&'},
            {from: /&quot;/g, to: '"'},
            {from: /&#39;/g, to: "'"},
            {from: /&minus;/g, to: '-'},
        ];

        const lessonBodyNormalText = document.getElementById('lesson_body').innerHTML;
        const profileDescriptionNormalText = document.getElementById('profile-description').innerHTML;
        let lessonBodyMarkedText = marked.parse(lessonBodyNormalText);
        let profileDescriptionMarkedText = marked.parse(profileDescriptionNormalText);
        for (const replace of replaceList) {
            lessonBodyMarkedText = lessonBodyMarkedText.replace(replace.from, replace.to);
            profileDescriptionMarkedText = profileDescriptionMarkedText.replace(replace.from, replace.to);
        }
        document.getElementById('lesson_body').innerHTML = lessonBodyMarkedText;
        document.getElementById('profile-description').innerHTML = profileDescriptionMarkedText;

        $('#lesson_detail').on('click', function() {
            const $p = $(this).parent().parent().find('p');

            if ($p.hasClass('bottom_hidden_text')) {
                $p.removeClass('bottom_hidden_text')
                    .addClass('bottom_hidden_text-with-out-overflow')
                    .css('height', 'auto');

                let scrollHeight = $p.get(0).scrollHeight;

                $p.css('height', '150px');
                $p.stop().animate({ height: scrollHeight + "px" }, 500);

                $(this).text('접기');
            } else {
                $p.stop().animate({ height: "150px" }, 500, function() {
                    $p.removeClass('bottom_hidden_text-with-out-overflow')
                        .addClass('bottom_hidden_text');
                });

                $(this).text('자세히보기');
            }
        });
    });
</script>
...

{% block my_profile %}
<div class="col-sm-12 col-md-5 col-lg-4 col-xl-4 col-xxl-3">
    <div style="display: flex; flex-direction: column;">
        <div class="profile-image"
             style="width: 260px; height: 260px; border: solid #F5F5F5 2px; border-radius: 50%; background-image: url('{{ profile_image_url }}'); background-size: 100% auto; background-repeat: no-repeat; background-position: center; margin: 15px 0; align-self: center;"></div>
        <h4>{{ profile_name }}</h4>
    </div>
    <div class="profile-simple-description">
        <p>{{ profile_simple_description }}</p>
    </div>
</div>
<div class="col-sm-12 col-md-7 col-lg-8 col-xl-8 col-xxl-9" style="margin-bottom: 10px;">
<div id="profile-description" class="profile-description">
{{ profile_description }}
</div>
</div>
{% endblock %}

 

짠~ 완성입니다!!

 

그동안 블로그에 자기 소개가 없어서 조금 허전했던 것 같습니다~

0 0
블로그 일기
제 블로그의 고도화 과정을 설명합니다. 이는 코드 리팩토링과 추가된 기능들에 대해 기록하기 위한 게시판입니다. 어떤 기능이 추가되었는지, 무엇이 개선되었는지 등 고도화되는 과정을 자세히 다룰 예정입니다.
Yesterday: 456
Today: 105