[JS] UX Improvement (3)

seunghyun·2023년 12월 21일
0

Yougle

목록 보기
10/15
post-thumbnail

✔ 요구사항

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

개선할 것

  • 동적 인터페이스: Download Subtitle 버튼을 누르면
    • Downloading... 표시 ✔
    • 로딩 애니메이션 표시 ✔
  • 제목을 눌러서 새로고침 ✔
  • 영상 제목 위에 영상 썸네일 이미지도 작게 추가하기 ✔
  • 다운로드가 완료되면
    • 알아서 이미 MongoDB에 저장된 Subtitle 에 대하여 다운로드 버튼 비활성화
  • Downloaded 버튼을 누르면, 새로운 화면에서 "[타임스탬프] 텍스트" 형식으로 모든 자막을 보여준다.

✔ 결과 화면



✔ Downloading... 새로고침 🔄

JavaScript 🔄

자바스크립트를 사용하여 Download Subtitle 버튼 클릭 시 AJAX 요청을 서버로 보내고, 응답을 받은 후 페이지를 새로고침합니다.

로딩 애니메이션 추가

  • 새로운 <div> 요소를 생성하여 로딩 애니메이션(스피너)을 나타내는 'loader-downloading' 클래스를 적용합니다.
  • 이 요소를 버튼 바로 다음에 페이지에 삽입합니다.

서버 요청 (Fetch API 사용)

  • fetch 함수를 사용하여 서버에 자막 파일 다운로드 요청을 보냅니다.
  • 요청 URL은 /download/${channelId}/${videoId} 형식으로 구성됩니다.

응답 처리

  • 서버로부터 응답을 받으면, 페이지를 새로고침합니다 (window.location.reload()).
  • 오류가 발생하면 콘솔에 오류를 기록하고, 버튼을 원래 상태로 되돌립니다.
// 자막 다운로드 함수
function downloadSubtitle(channelId, videoId, button) {
    // 버튼 비활성화 및 스타일 변경
    button.disabled = true;
    button.className = 'downloading-button';
    button.innerHTML = 'Downloading...';

    // 로딩 애니메이션 요소 생성 및 추가
    var loader = document.createElement('div');
    loader.className = 'loader-downloading';
    button.parentNode.insertBefore(loader, button.nextSibling);

    fetch(`/download/${channelId}/${videoId}`)
        .then(response => {
            // 다운로드 후 처리

            // 페이지 새로고침
            window.location.reload();
        })
        .catch(error => {
            console.error('Error:', error);
            button.disabled = false;
            button.className = 'download-link';
            button.innerHTML = 'Download Subtitle';
            loader.remove();
        });
}

HTML 🔄

HTML에 해당 자바스크립트 함수를 호출하는 버튼 링크를 추가합니다. onclick 속성을 통해 버튼 클릭 시 downloadSubtitle 함수를 호출합니다.

<button onclick="downloadSubtitle('{{ data.channel_id }}', '{{ video.video_id }}', this)" class="download-link">Download Subtitle</button>

CSS 🔄

Downloading... 과 로딩 애니메이션의 CSS style 은 다음과 같습니다.

/* Downloading... */
.downloading-button {
    display: inline-block;
    padding: 10px 15px;
    margin-top: 10px;
    background-color: #90EE90; /* 초록 배경 */
    color: white;
    text-decoration: none;
    border-radius: 5px;
    border: none;
    transition: background-color 0.3s, transform 0.3s;
    cursor: pointer;
}

.loader-downloading {
    margin: auto; /* 가운데 정렬 */
    border: 5px solid #f3f3f3; /* Light grey */
    border-top: 5px solid #90EE90; /* Blue */
    border-radius: 50%;
    width: 15px;
    height: 15px;
    animation: spin 2s linear infinite;
}
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

✔ 제목을 눌러서 새로고침

Get Whisper Subtitle by Channel ID 을 누르면 모든 정보가 초기화되고 첫 주소로 돌아가도록 하고 싶었습니다.

html 수정을 아래와 같이,

<h1 onclick="redirectToHome()" style="cursor: pointer;">Get Whisper Subtitle by Channel ID</h1>

script.js 수정을 아래와 같이 했습니다.

// 새로고침
function redirectToHome() {
	window.location.href = '/'; // 홈페이지 URL로 리다이렉트
}

✔ 영상 제목 위에 썸네일 이미지도 작게 추가하기

영상의 썸네일 이미지를 구하는 법은 생각보다 간단합니다.

# 비디오 정보에서 썸네일 URL 추출 및 출력
for video in video_info:
    video_id = video['video_id']
    thumbnail_url = f'https://img.youtube.com/vi/{video_id}/maxresdefault.jpg'
    print(f"비디오 ID: {video_id}, 썸네일 URL: {thumbnail_url}")

    # 이 URL을 웹페이지에서 사용할 수 있습니다.
    # 예: <img src="{thumbnail_url}" alt="썸네일">

유튜브 썸네일은 5가지의 해상도로 제공됩니다.

  1. 기본 (Default) 해상도

    • 크기: 120x90 픽셀
    • URL 형식: https://img.youtube.com/vi/[VideoID]/default.jpg
  2. 중간 (Medium) 해상도

    • 크기: 320x180 픽셀
    • URL 형식: https://img.youtube.com/vi/[VideoID]/mqdefault.jpg
  3. 고화질 (High) 해상도

    • 크기: 480x360 픽셀
    • URL 형식: https://img.youtube.com/vi/[VideoID]/hqdefault.jpg
  4. 표준 정의 (Standard Definition) 해상도

    • 크기: 640x480 픽셀
    • URL 형식: https://img.youtube.com/vi/[VideoID]/sddefault.jpg
  5. 최대 해상도 (Max Resolution) 해상도

    • 크기: 1280x720 픽셀 또는 그 이상
    • URL 형식: https://img.youtube.com/vi/[VideoID]/maxresdefault.jpg

저희 프로젝트에선 간단히 기본 해상도를 사용하기로 했습니다.

그래서 이렇게 수정해줬습니다.

        {% if data.videos %}
            <div class="grid-container">
                {% for video in data.videos %}
                <div class="card">
                    <!-- 썸네일 이미지 추가 -->
                    {% set thumbnail_url = 'https://img.youtube.com/vi/' ~ video.video_id ~ '/default.jpg' %}
                    <img src="{{ thumbnail_url }}" alt="{{ video.title }}">
                    <!-- 비디오 제목 링크 -->
                    <a href="{{ video.link }}" class="video-link">{{ video.title }}</a>
                    <p class="published-date">{{ video.published_at }}</p><!--업로드 날짜 표시-->
                    <!-- transcription 존재 여부에 따라 버튼 상태와 메시지를 변경합니다. -->
                    {% if video.transcription_exists %}
                        <button class="exists-in-db" disabled>Downloaded</button>
                    {% else %}
                        <button onclick="downloadSubtitle('{{ data.channel_id }}', '{{ video.video_id }}', this)" class="download-link">Download Subtitle</button>
                    {% endif %}
                    <!--mongodb 버전이었다. 제목을 누르면 바로 다운로드가 되는 코드-->
                    <!--<a href="/download/{{ video.id }}">{{ video.title }}</a>-->
                </div>

썸네일이 보이니 영상을 더 구별하기 쉬워졌습니다. ㅎㅎ


✔ Downloaded 버튼, "[타임스탬프] 텍스트" 출력

라우트를 만들어줬습니다.

@app.route('/view-transcription/<channel_id>/<video_id>')
def view_transcription(channel_id, video_id):
    transcription = videos_db_query.find_mongodb_trans(channel_id, video_id)
    if transcription is None:
        return "Transcription not found", 404
    return render_template('transcription.html', transcription=transcription, video_id=video_id)

transcription.html 은 아래와 같습니다. "[분:초] 텍스트" 형식으로 나오도록 곧 바꿔줘야겠습니다.

<!DOCTYPE html>
<html>
<head>
    <title>View Transcription</title>
</head>
<body>
    {% if transcription.segments %}
        <ul>
            {% for segment in transcription.segments %}
                <li>
                    <strong>Time:</strong> {{ segment.start }}s
                    <strong>Text:</strong> {{ segment.text }}
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No transcription segments found.</p>
    {% endif %}
    <a href="/">Back to Home</a>
</body>
</html>

index.html 은 아래와 같이 변경해주었습니다.

{% if video.transcription_exists %}
                        <!--<button class="exists-in-db" disabled>Downloaded</button>-->
                        <button onclick="viewDownload('{{ data.channel_id }}', '{{ video.video_id }}')" class="view-downloaded">View Downloaded</button>
                    {% else %}

아래의 코드는 이에 따른 js, css 입니다.

// 다운로드한 자막 펼쳐보기
function viewDownload(channelId, videoId) {
    console.log(`Viewing transcription for Channel ID: ${channelId}, Video ID: ${videoId}`);
    window.location.href = `/view-transcription/${channelId}/${videoId}`;
}
/* View Downloaded 이미 다운로드된 자막 펼쳐보기 */
.view-downloaded {
    display: inline-block;
    padding: 10px 15px;
    margin-top: 10px;
    background-color: #90EE90; /* 밝은 초록 */
    color: white;
    text-decoration: none;
    border-radius: 5px;
    border: none;
    transition: background-color 0.3s, transform 0.3s;
}
.view-downloaded:hover {
    background-color: #66de66; /* 진한 초록 */
    transform: translateY(-2px);
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

타임스탬프를 [분:초] 형식으로 바꿔주기

이제, 유튜브의 형식대로 [분:초] 형식으로 타임스탬프를 수정해주겠습니다.

# 자막 정보의 타임스탬프를 [초] 형식에서 [분:초] 형식으로 변환합니다.
def format_seconds_to_min_sec(seconds):
    minutes = int(seconds // 60)
    seconds = int(seconds % 60)
    return f"{minutes:02d}:{seconds:02d}"
    
@app.route('/view-transcription/<channel_id>/<video_id>')
def view_transcription(channel_id, video_id):
    transcription = videos_db_query.find_mongodb_trans(channel_id, video_id)
    if transcription is None:
        return "Transcription not found", 404

    # 각 segment 의 시간을 분:초 형식으로 변환한다.
    for segment in transcription['segments']:
        segment['formatted_start'] = youtube_data.format_seconds_to_min_sec(segment['start'])
    return render_template('transcription.html', transcription=transcription, video_id=video_id)

그리고 transcription.html 을 아래와 같이 변경해줍니다.

<!DOCTYPE html>
<html>
<head>
    <title>View Transcription</title>
</head>
<body>
    {% if transcription.segments %}
        <ul>
            {% for segment in transcription.segments %}
                <li>
                    [{{ segment.formatted_start }}] {{ segment.text }}
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No transcription segments found.</p>
    {% endif %}
    <a href="/">Back to Home</a>
</body>
</html>

🔗 Reference

0개의 댓글