본문 바로가기
HTML&CSS

HTML, CSS, JavaScript를 활용한 뮤직플레이어를 만들어보자. (2)

by 초콜렛시몽 2025. 1. 13.
반응형

지난번엔 기본틀만 가지고 있는 뮤직플레이어를 만들어봤는데요, 이번엔 파일추가, 폴더추가, 플레이리스트, 추가음악 제거 기능까지 가지고 있는 플레이어를 만들어볼까 합니다. 만드는 예제는 아래와 같으니 같이 만들어서 활용해 보죠~

.HTML

<div id="player">
        <div id="playlist">
            <h3>플레이리스트</h3>
            <ul id="trackList"></ul>
        </div>
        <div id="controls">
            <div id="now-playing">
                <h2>음악 플레이어</h2>
                <audio id="audio" autoplay></audio>
            </div>
            <div id="control-bar">
                <div class="control-buttons">
                    <button id="prev">이전</button>
                    <button id="play">재생</button>
                    <button id="pause">일시정지</button>
                    <button id="next">다음</button>
                </div>
                <div id="progress-container">
                    <span id="currentTime">0:00</span>
                    <input type="range" id="progressBar" value="0" min="0" max="100">
                    <span id="duration">0:00</span>
                </div>
                <div class="control-buttons">
                    <button id="addFile">파일 추가</button>
                    <button id="addFolder">폴더 추가</button>
                    <button id="removeFile">제거</button>
                </div>
                <div class="skin-selection">
                    <label for="skins">스킨:</label>
                    <select id="skins">
                        <option value="default">기본</option>
                        <option value="dark">다크</option>
                        <option value="light">라이트</option>
                    </select>
                </div>
                <div class="control-buttons">
                    <label for="volumeControl">볼륨:</label>
                    <input type="range" id="volumeControl" value="100" min="0" max="100">
                </div>
            </div>
        </div>
    </div>

CSS

:root {
            --bg-color: #f0f0f0;
            --text-color: #333;
            --button-bg: #007BFF;
            --button-hover-bg: #0056b3;
            --control-bg: #fff;
            --control-shadow: rgba(0, 0, 0, 0.1);
        }

        body {
            font-family: Arial, sans-serif;
            background-color: var(--bg-color);
            color: var(--text-color);
            margin: 0;
            padding: 0;
            display: flex;
            height: 100vh;
        }

        #player {
            display: flex;
            flex-direction: row;
            width: 100%;
            height: 100%;
        }

        #playlist {
            width: 250px;
            background-color: #fff;
            border-right: 1px solid #ccc;
            padding: 20px;
            box-sizing: border-box;
            overflow-y: auto;
        }

        #playlist h3 {
            margin-top: 0;
        }

        #trackList {
            list-style: none;
            padding: 0;
            margin: 0;
        }

        #trackList li {
            padding: 10px;
            cursor: pointer;
            border-bottom: 1px solid #eee;
        }

        #trackList li:hover {
            background-color: #f9f9f9;
        }

        #trackList li.active {
            background-color: #007BFF;
            color: #fff;
            font-weight: bold;
        }

        #controls {
            flex: 1;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }

        #now-playing {
            padding: 20px;
            text-align: center;
        }

        #control-bar {
            background-color: var(--control-bg);
            padding: 10px 20px;
            box-shadow: 0 -2px 5px var(--control-shadow);
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .control-buttons {
            display: flex;
            align-items: center;
        }

        .control-buttons button {
            margin-right: 10px;
            padding: 10px 15px;
            border: none;
            border-radius: 5px;
            background-color: var(--button-bg);
            color: white;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .control-buttons button:last-child {
            margin-right: 0;
        }

        .control-buttons button:hover {
            background-color: var(--button-hover-bg);
        }

        #progress-container {
            flex: 1;
            margin: 0 20px;
            display: flex;
            align-items: center;
        }

        #progressBar {
            width: 100%;
            margin: 0 10px;
        }

        #volumeControl {
            width: 100px;
        }

        .skin-selection {
            display: flex;
            align-items: center;
        }

        .skin-selection label {
            margin-right: 10px;
        }

        @media (max-width: 768px) {
            #playlist {
                width: 200px;
            }

            #volumeControl {
                width: 80px;
            }

            .control-buttons button {
                padding: 8px 12px;
                font-size: 14px;
            }
        }

JavaScript

const audio = document.getElementById('audio');
        const playButton = document.getElementById('play');
        const pauseButton = document.getElementById('pause');
        const nextButton = document.getElementById('next');
        const prevButton = document.getElementById('prev');
        const addFileButton = document.getElementById('addFile');
        const addFolderButton = document.getElementById('addFolder');
        const removeFileButton = document.getElementById('removeFile');
        const skins = document.getElementById('skins');
        const trackList = document.getElementById('trackList');
        const progressBar = document.getElementById('progressBar');
        const currentTimeEl = document.getElementById('currentTime');
        const durationEl = document.getElementById('duration');
        const volumeControl = document.getElementById('volumeControl');

        let playlist = [];
        let currentTrackIndex = 0;

        // 로컬 스토리지에서 플레이리스트 불러오기
        function loadPlaylist() {
            const storedPlaylist = JSON.parse(localStorage.getItem('playlist'));
            if (storedPlaylist) {
                playlist = storedPlaylist;
                playlist.forEach((track, index) => {
                    const listItem = document.createElement('li');
                    listItem.textContent = track.name;
                    listItem.addEventListener('click', () => {
                        currentTrackIndex = index;
                        loadTrack(currentTrackIndex);
                    });
                    trackList.appendChild(listItem);
                });
                if (playlist.length > 0) loadTrack(0);
            }
        }

        // 로컬 스토리지에 플레이리스트 저장
        function savePlaylist() {
            localStorage.setItem('playlist', JSON.stringify(playlist));
        }

        // 트랙 로드
        function loadTrack(index) {
            if (playlist[index]) {
                audio.src = playlist[index].url;
                audio.play();
                highlightCurrentTrack();
            }
        }

        // 현재 트랙 강조 표시
        function highlightCurrentTrack() {
            const items = trackList.querySelectorAll('li');
            items.forEach((item, idx) => {
                if (idx === currentTrackIndex) {
                    item.classList.add('active');
                } else {
                    item.classList.remove('active');
                }
            });
        }

        // 플레이 버튼 이벤트
        playButton.addEventListener('click', () => audio.play());

        // 일시정지 버튼 이벤트
        pauseButton.addEventListener('click', () => audio.pause());

        // 다음 버튼 이벤트
        nextButton.addEventListener('click', () => {
            if (playlist.length === 0) return;
            currentTrackIndex = (currentTrackIndex + 1) % playlist.length;
            loadTrack(currentTrackIndex);
            savePlaylist();
        });

        // 이전 버튼 이벤트
        prevButton.addEventListener('click', () => {
            if (playlist.length === 0) return;
            currentTrackIndex = (currentTrackIndex - 1 + playlist.length) % playlist.length;
            loadTrack(currentTrackIndex);
            savePlaylist();
        });

        // 파일 추가 버튼 이벤트
        addFileButton.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = 'audio/*';
            input.multiple = true;
            input.addEventListener('change', () => {
                Array.from(input.files).forEach(file => {
                    const url = URL.createObjectURL(file);
                    playlist.push({ name: file.name, url });
                    const listItem = document.createElement('li');
                    listItem.textContent = file.name;
                    listItem.addEventListener('click', () => {
                        currentTrackIndex = playlist.findIndex(track => track.name === file.name);
                        loadTrack(currentTrackIndex);
                        savePlaylist();
                    });
                    trackList.appendChild(listItem);
                });
                if (playlist.length === Array.from(input.files).length) loadTrack(0);
                savePlaylist();
            });
            input.click();
        });

        // 폴더 추가 버튼 이벤트
        addFolderButton.addEventListener('click', async () => {
            try {
                const dirHandle = await window.showDirectoryPicker();
                for await (const [name, handle] of dirHandle.entries()) {
                    if (handle.kind === 'file') {
                        const file = await handle.getFile();
                        const url = URL.createObjectURL(file);
                        playlist.push({ name: file.name, url });
                        const listItem = document.createElement('li');
                        listItem.textContent = file.name;
                        listItem.addEventListener('click', () => {
                            currentTrackIndex = playlist.findIndex(track => track.name === file.name);
                            loadTrack(currentTrackIndex);
                            savePlaylist();
                        });
                        trackList.appendChild(listItem);
                    }
                }
                if (playlist.length > 0 && !audio.src) loadTrack(currentTrackIndex);
                savePlaylist();
            } catch (err) {
                console.error(err);
                alert('폴더 추가에 실패했습니다. 브라우저가 이 기능을 지원하는지 확인하세요.');
            }
        });

        // 제거 버튼 이벤트
        removeFileButton.addEventListener('click', () => {
            if (playlist.length === 0) {
                alert('제거할 트랙이 없습니다.');
                return;
            }
            const selected = trackList.querySelector('li.active');
            if (selected) {
                const index = Array.from(trackList.children).indexOf(selected);
                URL.revokeObjectURL(playlist[index].url);
                playlist.splice(index, 1);
                trackList.removeChild(selected);
                if (currentTrackIndex >= playlist.length) {
                    currentTrackIndex = 0;
                }
                if (playlist.length > 0) {
                    loadTrack(currentTrackIndex);
                } else {
                    audio.src = '';
                }
                savePlaylist();
            } else {
                alert('선택된 트랙이 없습니다.');
            }
        });

        // 스킨 변경 이벤트
        skins.addEventListener('change', () => {
            document.body.classList.remove('default', 'dark', 'light');
            if (skins.value === 'dark') {
                document.body.style.backgroundColor = '#333';
                document.body.style.color = '#fff';
                document.querySelectorAll('.control-buttons button').forEach(btn => {
                    btn.style.backgroundColor = '#555';
                });
            } else if (skins.value === 'light') {
                document.body.style.backgroundColor = '#fff';
                document.body.style.color = '#333';
                document.querySelectorAll('.control-buttons button').forEach(btn => {
                    btn.style.backgroundColor = '#007BFF';
                });
            } else {
                document.body.style.backgroundColor = '#f0f0f0';
                document.body.style.color = '#333';
                document.querySelectorAll('.control-buttons button').forEach(btn => {
                    btn.style.backgroundColor = '#007BFF';
                });
            }
        });

        // 진행 바 업데이트
        audio.addEventListener('timeupdate', () => {
            if (audio.duration) {
                const progress = (audio.currentTime / audio.duration) * 100;
                progressBar.value = progress;
                currentTimeEl.textContent = formatTime(audio.currentTime);
                durationEl.textContent = formatTime(audio.duration);
            }
        });

        // 진행 바 변경 시 위치 이동
        progressBar.addEventListener('input', () => {
            if (audio.duration) {
                audio.currentTime = (progressBar.value / 100) * audio.duration;
            }
        });

        // 볼륨 조절
        volumeControl.addEventListener('input', () => {
            audio.volume = volumeControl.value / 100;
        });

        // 트랙 종료 시 다음 트랙 자동 재생
        audio.addEventListener('ended', () => {
            nextButton.click();
        });

        // 시간 형식 변환 함수
        function formatTime(seconds) {
            const minutes = Math.floor(seconds / 60);
            const secs = Math.floor(seconds % 60);
            return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
        }

        // 초기화
        window.addEventListener('DOMContentLoaded', loadPlaylist);

결과:

결과값이 1배율로 표기되서 글자가 밀렸으나, 0.25x로 변환하면 1920x1080사이즈로 변환되서 작지만 원래 상태로 잘 보여집니다. 가끔 집에서 음악 듣고 싶은데 따로 프로그램 설치하기 귀찮으신 분들은 이걸 활용해서 쓰셔도 무난할 정도의 코드인거 같네요.

그럼 모두 즐거운 한주 되시고 다음 포스팅에 뵈요 ^^

반응형