[HTML&CSS] UX Improvement (2)

seunghyun·2023년 12월 21일
0

Yougle

목록 보기
9/15
post-thumbnail

✔ 요구사항

관리자 페이지의 UX 를 더 개선해보자!

개선해보자!

  • 각 카드에 업로드 날짜도 표시하기 ✔
  • 로딩 애니메이션 가운데 정렬 ✔
  • 마우스 금지 표시 해제 ✔
  • 페이지네이션 ✔
  • 이미 MongoDB에 저장된 Subtitle 에 대하여 다운로드 버튼 비활성화 ✔
  • 유튜브 채널 이름 보여주기 ✔
  • 유튜브 채널 링크 바로가기 ✔ (Go To https://www.youtube.com/channel/[채널ID])

✔ 결과 화면

로딩 애니메이션페이지네이션, MongoDB 저장 여부에 따른 버튼 활성화/비활성화

✔ 페이지네이션

관리자 페이지라고 하더라도, 더 쉽게 볼 수 있고 사용자의 접근성을 높일 수 있도록 pagination 을 적용하기로 했습니다. 데이터베이스에 저장된 링크 수가 대량일지는 저희도 모르기 때문입니다!

  • 한 페이지에 페이지 링크는 30개로 보여준다.
  • 이전 버튼, 다음 버튼이 존재한다.
  • 첫 페이지로 가는 버튼, 마지막 페이지로 버튼이 존재한다.
  • 한 번에 총 5 페이지에 대한 버튼만 보여준다.

우리 프로젝트에서 페이지네이션을 구현하려면 서버 측 코드(app.py)와 클라이언트 측 코드(HTML 템플릿) 모두 수정해야 합니다. 서버 측에서는 데이터를 페이지 단위로 나눠서 전송해야 하고, 클라이언트 측에서는 사용자가 페이지를 넘길 수 있는 인터페이스를 제공해야 합니다.

또한 페이지네이션 로직을 POST (제출) 요청과 별개로 실행되게 해야 합니다. 그 이유는 페이지네이션은 일반적으로 데이터를 조회하는 목적의 기능 이며, HTTP 요청 메서드 (POST, GET 등)에 관계 없이 사용자가 웹 페이지에서 데이터를 탐색하고 검색할 수 있어야하기 때문입니다.

페이지네이션 : 서버 코드

사용자가 페이지를 선택하면 페이지 번호 및 관련된 데이터가 서버로 전달되고, 서버는 해당 페이지의 비디오 목록을 반환하여 페이지네이션을 제공합니다.

  • 초기 페이지 번호는 1로 설정됩니다.

  • 사용자가 채널 ID를 입력하고 검색을 요청하면, POST 메서드가 사용되어 서버로 요청이 전송됩니다. 서버는 사용자가 입력한 채널 ID를 가져옵니다.

  • 서버는 현재 페이지 번호를 GET (조회) 요청에서 가져옵니다.

    • page = request.args.get('page', 1, type=int) # 페이지 번호
  • 총 페이지 수(total_pages) = "총 동영상 수 / 페이지당 동영상 수 (per_page)"

    • total_pages = (total_videos + per_page - 1) // per_page
@app.route('/', methods=['GET', 'POST'])
def index():
    response_data = {'videos': [], 'channel_id': '', 'error': '',
                     'video_cnt': 0, 'published_at': '', 'channel_name': '',
                     'channel_link': '', 'total_pages': 0, 'current_page': 1} # 기본값으로 current_page를 1로 설정

    # GET 요청에서 채널 ID 가져오기
    channel_id = request.args.get('channel_id', '')

    if request.method == 'POST':
        channel_id = request.form['channel_id']
        # 채널 ID 유효성 검사
        if not youtube_data.validate_channel_id(channel_id):
            response_data['error'] = 'Invalid Channel ID. Enter again please!'
            return render_template('index.html', data=response_data)
     
        try:
            response_data['channel_id'] = channel_id
            exists_in_mongo = videos_db_query.check_channel_id_in_mongodb(channel_id)
            exists_in_sqlite = videos_db_query.check_channel_id_in_sqlite(channel_id)
            if not exists_in_mongo and not exists_in_sqlite:
                youtube_data.update_db(channel_id)
            elif exists_in_sqlite:

            response_data['videos'] = videos_db_query.innerjoin_by_channel_id(channel_id)
            response_data['video_cnt'] = videos_db_query.get_video_count(channel_id)
            response_data['channel_name'] = videos_db_query.get_channel_name(channel_id)
            response_data['channel_link'] = videos_db_query.get_channel_link(channel_id)
        except Exception as e:
            response_data['error'] = f'An error occurred: {str(e)}'
            return render_template('index.html', data=response_data)

    # 페이지네이션 처리는 POST와 무관하게 수행
    page = request.args.get('page', 1, type=int) # 페이지 번호 # HTTP GET
    per_page = 30  # 한 페이지당 비디오 수
    if channel_id:
        response_data['channel_id'] = channel_id
        total_videos = videos_db_query.get_video_count(channel_id) # 전체 동영상 개수
        total_pages = (total_videos + per_page - 1) // per_page # 총 페이지 수는 전체 동영상 수를 페이지당 동영상 수로 나누어 계산
        
        # 현재 페이지에 대한 정보와 함께, 채널 이름과 링크도 함께 업데이트하여 response_data에 저장
        response_data['videos'] = videos_db_query.get_videos_by_page(channel_id, page, per_page)
        response_data['video_cnt'] = videos_db_query.get_video_count(channel_id)
        response_data['total_pages'] = total_pages
        response_data['current_page'] = page
        channel_name = videos_db_query.get_channel_name(channel_id)
        if channel_name:
            response_data['channel_name'] = channel_name  # 채널 이름 설정
            response_data['channel_link'] = videos_db_query.get_channel_link(channel_id)

    return render_template('index.html', data=response_data) 

아래는 get_videos_by_page() 함수입니다.

  • LIMITOFFSET을 사용하여 특정 페이지의 비디오만 선택하도록 제한합니다.

  • LIMIT는 페이지당 비디오 수(per_page) 를 나타내며,

    • LIMIT 절은 쿼리 결과에서 반환할 행(row)의 최대 수를 지정하는 데 사용
    • 예를 들어, LIMIT 10은 쿼리 결과에서 최대 10개의 행만 반환
  • OFFSET은 페이지 번호(page) 에 따라 오프셋을 계산하여 해당 페이지의 비디오를 선택합니다.

    • OFFSET 절은 쿼리 결과에서 건너뛸 행의 수를 지정하는 데 사용
    • 예를 들어, OFFSET 20은 처음 20개의 결과를 건너뛰고 그 이후의 결과를 반환
# 특정 페이지의 비디오만 조회하는 쿼리
def get_videos_by_page(channel_id, page, per_page):
    with sqlite3.connect('videos.db') as connect:
        cursor = connect.cursor()
        offset = (page - 1) * per_page
        query = """
            SELECT video.vid, video.video_id, video.title, video.link, video.published_at
            FROM video
            INNER JOIN channel ON video.cid = channel.cid
            WHERE channel.channel_id = ?
            ORDER BY video.published_at DESC
            LIMIT ? OFFSET ?
        """
        cursor.execute(query, (channel_id, per_page, offset))
        rows = cursor.fetchall()
        return [{"vid": row[0], "video_id": row[1], "title": row[2], "link": row[3], "published_at": row[4]} for row in rows]

페이지네이션 : 클라이언트 코드

아래는 index.html 의 페이지네이션 관련 코드입니다.

  • First, Prev, 페이지 번호, Next, Last와 같은 페이지 이동 버튼을 제공합니다.

  • 현재 페이지 주변의 페이지 번호들 (최대 5개) 을 표시하고, 현재 페이지는 강조 표시됩니다.

  • 각 페이지 번호에는 해당 페이지로 이동할 수 있는 링크가 포함됩니다.

  1. "First" 버튼

    • 이 버튼은 첫 번째 페이지로 이동하는 버튼입니다.
    • href 속성을 통해 해당 버튼을 클릭하면 /?page=1로 이동하게 됩니다.
    • 또한, 현재 채널 ID가 존재하는 경우(data.channel_id), 해당 ID를 페이지로 전달하기 위해 &channel_id=와 함께 추가됩니다.
  2. "Prev" 버튼

    • 이 버튼은 이전 페이지로 이동하는 버튼입니다.
    • {% if data.current_page > 1 %} 구문을 통해 현재 페이지가 1보다 큰 경우에만 표시됩니다.
    • 클릭하면 현재 페이지에서 1을 빼서 이전 페이지로 이동하게 됩니다.
    • 마찬가지로 채널 ID도 함께 전달됩니다.
  3. 페이지 번호

    • 이 부분은 현재 페이지를 중심으로 주변 페이지 번호를 표시합니다.
    • {% set page_offset = 2 %}를 사용하여 현재 페이지 주변에 몇 개의 페이지 번호를 표시할 것인지 설정합니다.
    • start_pageend_page 변수를 계산하여 현재 페이지 주변의 페이지 번호 범위를 결정합니다.
    • range(start_page, end_page + 1)을 사용하여 해당 범위의 페이지 번호를 반복적으로 표시합니다.
    • 현재 페이지는 배경색이 파란색(#2980b9)으로 강조되며, 다른 페이지 번호는 클릭 가능한 링크로 표시됩니다.
    • 각 페이지 번호를 클릭하면 해당 페이지로 이동하게 됩니다.
  4. "Next" 버튼

    • 이 버튼은 다음 페이지로 이동하는 버튼입니다.
    • {% if data.current_page < data.total_pages %} 구문을 통해 현재 페이지가 총 페이지 수보다 작은 경우에만 표시됩니다.
    • 클릭하면 현재 페이지에서 1을 더해 다음 페이지로 이동하게 됩니다.
    • 채널 ID도 함께 전달됩니다.
  5. "Last" 버튼

    • 이 버튼은 마지막 페이지로 이동하는 버튼입니다.
    • href 속성을 통해 해당 버튼을 클릭하면 /?page= 뒤에 총 페이지 수가 추가되어 마지막 페이지로 이동하게 됩니다.
    • 채널 ID도 함께 전달됩니다.
<!-- 페이지네이션 컨트롤 -->
<div class="pagination">
	<!-- 첫 페이지 버튼 -->
	<a href="/?page=1{{ '&channel_id=' + data.channel_id if data.channel_id else '' }}">First</a>

	<!-- 이전 페이지 버튼 -->
	{% if data.current_page > 1 %}
	<a href="/?page={{ data.current_page - 1 }}{{ '&channel_id=' + data.channel_id if 	data.channel_id else '' }}">Prev</a>
	{% endif %}

	{% set page_offset = 2 %}
	{% set start_page = [data.current_page - page_offset, 1]|max %}
	{% set end_page = [start_page + 4, data.total_pages]|min %}

	<!-- 현재 페이지 주변의 페이지 번호들 (최대 5개) -->
	{% for page in range(start_page, end_page + 1) %}
		{% if page == data.current_page %}
			<a href="/?page={{ page }}&channel_id={{ data.channel_id }}" style="background-color: #2980b9; color: white;">{{ page }}</a>
		{% else %}
			<a href="/?page={{ page }}&channel_id={{ data.channel_id }}">{{ page }}</a>
		{% endif %}
	{% endfor %}

	<!-- 다음 페이지 버튼 -->
	{% if data.current_page < data.total_pages %}
	<a href="/?page={{ data.current_page + 1 }}{{ '&channel_id=' + data.channel_id if data.channel_id else '' }}">Next</a>
	{% endif %}

	<!-- 마지막 페이지 버튼 -->
	<a href="/?page={{ data.total_pages }}{{ '&channel_id=' + data.channel_id if data.channel_id else '' }}">Last</a>
</div>

✔ 버튼 비활성화

이미 MongoDB에 저장된 Subtitle 에 대하여 다운로드 버튼 비활성화해야 합니다.

즉 이미 MongoDB에 각 영상의 transcription이 존재하는 경우 "Get Videos" 버튼을 비활성화하고 "이미 DB에 있어요"라고 표시하는 기능을 구현하려면,
Flask 템플릿에서 조건부 렌더링을 사용하면 됩니다.

이 방법은 추가 라우트를 만들지 않고도 현재 구조 내에서 해결할 수 있습니다.

서버 코드

각 비디오의 ID를 사용하여 MongoDB에서 transcription의 존재 여부를 확인합니다.
이 정보는 결과 딕셔너리에 "transcription_exists" 키로 추가되며,
이후 HTML 템플릿에서 이 정보를 사용하여 사용자 인터페이스에 해당 정보를 표시할 수 있습니다.

# 특정 페이지의 비디오만 조회하는 쿼리
def get_videos_by_page(channel_id, page, per_page):
    with sqlite3.connect('videos.db') as connect:
        cursor = connect.cursor()
        offset = (page - 1) * per_page
        query = """
            SELECT video.vid, video.video_id, video.title, video.link, video.published_at
            FROM video
            INNER JOIN channel ON video.cid = channel.cid
            WHERE channel.channel_id = ?
            ORDER BY video.published_at DESC
            LIMIT ? OFFSET ?
        """
        cursor.execute(query, (channel_id, per_page, offset))
        rows = cursor.fetchall()
        # 비디오 정보에 transcription 존재 여부 추가
        video_info = []
        for row in rows:
            video_id = row[1]
            transcription_exists = check_transcription_none(channel_id, video_id) is not True
            video_info.append({
                "vid": row[0],
                "video_id": video_id,
                "title": row[2],
                "link": row[3],
                "published_at": row[4],
                "transcription_exists": transcription_exists
            })

        return video_info

클라이언트 코드

이렇게 하면 MongoDB에 transcription이 존재하는 영상에 대해서는 "이미 DB에 있어요"라는 메시지와 함께 비활성화된 버튼이 표시되고,
그렇지 않은 경우에는 정상적으로 "Download Subtitle" 링크가 활성화됩니다.
추가적인 라우트를 만들지 않고도 이 기능을 구현할 수 있습니다.

<div class="card">
	<a href="{{ video.link }}" class="video-link">{{ video.title }}</a>
	<p class="published-date">{{ video.published_at }}</p><!--업로드 날짜 표시-->
	<!-- transcription 존재 여부에 따라 버튼 상태와 메시지를 변경합니다. -->
	{% if video.transcription_exists %}
		<button disabled>이미 DB에 있어요</button>
	{% else %}
		<a href="/download/{{ data.channel_id }}/{{ video.video_id }}" class="download-link">Download Subtitle</a>
	{% endif %}
</div>

✔ CSS

로딩 애니메이션 가운데 정렬

/* 로딩 애니메이션 위치 조정 */
#loadingAnimation {
    text-align: center; /* 가운데 정렬 */
    padding-top: 20px; /* description-container와의 간격 */
}
/* 로딩 애니메이션을 위한 원형 스피너를 생성 */
.loader {
    margin: auto; /* 가운데 정렬 */
    border: 5px solid #f3f3f3; /* Light grey */
    border-top: 5px solid #3498db; /* Blue */
    border-radius: 50%;
    width: 50px;
    height: 50px;
    animation: spin 2s linear infinite;
}

마우스 금지 표시 해제

/* 마우스 금지 표시 해제 */
input[type="submit"]:disabled {
    background-color: #cccccc; /* 회색 */
    /*cursor: not-allowed; /* 커서 변경 */*/
    cursor: pointer; /* 커서 변경 */
}

🔗 Reference

0개의 댓글