요약 코드는 가장 하단에 접어두었습니다. 내용이 궁금하지 않다면 요약본을 보길 추천드립니다.
🤔 코드를 작성하게 된 계기
2번째 프로젝트는 "음원" 경매 사이트이다. 음원을 경매한다면 당연하게도 음악을 듣고 경매를 해야하는데, 그 음악을 일일이 상세페이지에 들어가서 듣자니 상당히 수고로운 일이다. 그렇다고, 모든 음악을 메인에 깔아두자니 부담되는 상황.
그래서 경매가 가장 핫한것은 당연하게도 "돈"!
현재 경매에서 시간이 아직 마감기한을 지나지 않고, 그중에서 입찰금액이 가장 높은 5가지를 가져왔다.
(이것에 대한 쿼리문은 따로 다루겠다.)
그래서 그것을 가져와 재생목록화 하고 객체에 담긴 정보들을 이미지화, 재생할 수 있는 상태로 만드는것을 해보겠다.
✔ 필요한 제원
JAVA, HTML, Javascript, JPA 를 이용하겠다.
환경으로는 SpringBoot(Gradle)을 이용한다.
✔ 구현하고자 하는 기능
재생, 일시정지, 이전곡, 다음곡, 볼륨조절, 재생위치조절, 재생시간표시, 목록 클릭시 해당음원 재생
1. 뮤직 플레이어를 만들기 위한 공간 만들기
✔ HTML
<script src="/js/sidebar-audio-player.js"/><div class="music_player"> <!--body 최하단에 추가하여 js파일 따로 작성-->
<div class="current_track">
<img id="current_track_cover"> <!-- 노래에 따라 동적으로 변할 곳 -->
</div>
<div class="controls"> <!--컨트롤의 전체 박스-->
<audio id="audio" preload="metadata"> <!--음원 태그-->
<div class="song_silder_box"> <!--재생 슬라이더 박스-->
<div id="current_time"> <!--현재 시간을 나타내는 div-->
<input type="range" min="0" max="0" value="0" class="progress_slider" oninput"change_progress()">
</div>
<div class="song_controls"> <!--재생 컨트롤 박스-->
<!--재생, 일시정지, 이전곡, 다음곡은 이미지를 준비해 프로젝트에 넣어두고, 그 이미지를 가져오자.
아래에 이미지 경로에 적힌 이미지는 예시이다.-->
<button onclick="previous()"> <!--이전 곡 버튼-->
<img class="control_previous" id="control_previous" src="/pictures/previous.png">
</button>
<button onclick="play_pause()"> <!--재생, 일시정지 버튼-->
<img class="control_play_pause" id="control_play_pause" src="/pictures/play.png"> <!--default는 정지상태이기때문에 재생버튼을 기본으로 둔다.-->
</button>
<button onclick="next()"> <!--다음 곡 버튼-->
<img class="control_next" id="control_next" src="/pictures/next.png">
</button>
</div>
<div class="volume_slider_box"> <!--볼륨 슬라이더 박스-->
<input type="range" min="0" max="100" value="0" class="volume_slider" id="volume_slider" oninput="change_volume()">
</div>
</div>
<div class="playlist">
<!--append를 통해 재생 목록을 추가할 공간-->
</div>
</div>
<script src="/js/sidebar-audio-player.js"/> <!--body 최하단에 추가하여 js파일 따로 작성-->
2. 목록을 만들어내는데 필요한 객체를 불러오기
✔ JAVA
JSON형식으로 우리가 원하는 데이터를 return 하도록 한다.
쿼리문은 수동으로 직접 입력해 가져왔다.
굳이 top5가 아니어도 되지만, 중요한것은 이곳에 앨범이미지와, 앨범 음원에 대한 정보가 담겨있어야 한다는것.
이와 같이 서버의 C드라이브에 담긴 정보들을 바로 불러올 수 있도록 Configuration을 넣어준다.
✔ JAVA
package com.pits.auction.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class ResourceConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 이미지 리소스 핸들러 추가
registry.addResourceHandler("/images/**")
.addResourceLocations("file:///C:/Auction/Image/"); // 로컬 경로 설정
// 오디오 리소스 핸들러 추가
registry.addResourceHandler("/audios/**")
.addResourceLocations("file:///C:/Auction/Audio/"); // 로컬 경로 설정
}
}
이렇게 해줄 경우에 이미지와 오디오의 경로가 images나 audios를 최상위 경로로 잡을 경우 자동으로 C에서 찾아서 온다.
✔ JAVA
@ResponseBody
@RequestMapping("/top5Music")
public ResponseEntity<List<MusicAuctionProjection>> top5Music(){
Page<MusicAuctionProjection> top5Musics = musicAuctionService.findTop5ByEndTimeAfterCurrent();
if(top5Musics!=null){
List<MusicAuctionProjection> top5MusicList = top5Musics.getContent();
return ResponseEntity.ok(top5MusicList);
}else{
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
설정된 상태로 주소가 정확히 담긴 객체를 5개 가져온 뒤, ResponseEntity.ok의 파라미터에 담아 return 하여 String 형태로 return하자.
3. 불러온 객체를 fetch하여 목록에 담는 javascript(sidebar-audio-play.js)
✔ JAVASCRIPT
내용이 길기때문에 잘라서 작성하겠다.
1번. fetch하는 구간(fetchTracks)
/* 빈객체 생성 */
let tracks = [];
/* 비동기 프로그래밍을 뜻하는 async
await를 통해 기다리며, 그 앞줄에 있는 코드의 작업이 완료될때까지 await가 기다려준다. */
async function fetchTracks() {
/* 항상 불러와지는 것이 아니기때문에 try-catch절을 통해 error 발생시 consonle을 통해 확인 */
try {
/* 아까 만든 주소에서 fetch하여 response에 담는다. */
const response = await fetch('/main/top5Music');
/* 정상이 아니라면 throw를 통해 error 메시지 출력 */
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
/* console 창에 받은 response를 toString()을 이용해서 내용물이 잘 들었나 확인 */
console.log(response.toString());
/* response를 앞선 문장을 다 실행 한 후 json형태로 변환하여 아까 만들어둔 tracks에 담는다. */
tracks = await response.json();
/* 준비가 되었으니 initPlaylist라는 function에서 전역 변수로 선언된 tracks 객체를 쓰러가자 */
initPlaylist(); // 데이터 로딩 후 플레이리스트 초기화
} catch (error) {
/* 에러 상황이 나왔을때 fetch자체가 안될때가 있다. 이때 확인용 */
console.error('Error fetching the tracks:', error);
}
}
2번. tracks에 담긴 정보들을 HTML에 담아 재생할 준비하기(initPlaylist) / 재생 슬라이더와 오디오의 위치 일치 양방향( changeProgress)
/* 현재 재생되는 음원의 번호 (0~4)번을 쓴다. */
let trackIndex = 0;
/* 노래의 길이를 담기 위한 변수 */
let duration = 0;
/* 재생을 위해 audio라는 id를 가진 상수를 만들어주자 */
const audioElement = document.getElementById('audio');
/* tracks 객체에 담긴 정보들을 필드명에 맞춰 each문을 이용해 꺼내쓰자 */
function initPlaylist(){
/* 아까 HTML에서 만든 div를 고르자. */
const playlistDiv = document.querySelector('.playlist');
/* tracks에 대하여 track이라는 객체로 each문을 실행하며, 그때 각각 번호는 index에 있다. */
tracks.forEach((track,index)=>{
/* Entity를 확인하여 이미지 주소가 담긴 필드명을 꺼내서 추가하자. */
const coverImg = document.createElement('img');
coverImg.classList.add('ListImage');
coverImg.src = track.albumImage;
coverImg.alt = '${track.title}의 albumImage'; // 객체 필드값을 쓰고싶으면 EL로!
/* 이미지를 누르면 해당 이미지에 해당하는 index의 Track으로 넘어가는 함수를 실행!
selectTrack도 뒤에서 만들어주자. */
coverImg.addEventListener('click', ()=>selectTrack(index));
playlistDiv.appendChild(coverImg); // 다만들었으면 추가
/* Entity를 확인하여 제목이 담긴 필드명을 꺼내서 추가하자. */
const title = document.createElement('div');
title.classList.add('ListTitle');
title.textContent = track.title;
playlistDiv.appendChild(title); // 다만들었으면 추가
}); //forEach문 종료
/* 0번을 기본으로 두고 불러오겠다. 필드명 정확히 지켜서 쓰자. */
document.getElementById('current_track_cover').src = tracks[0].albumImage;
audioElement.src = tracks[0].albumMusic;
/* 메타 데이터가 로드되면 트랙의 전체 길이를 얻는다. */
audioElement.addEventListener('loadedmetadata', () =>{
/* audioElement에서 길이를 가져와 담고, 재생 길이에 맞게 재생위치 slider 조절. */
duration = audioElement.duration;
document.getElementById('progress_slider').max=duration;
}); // addEventListener 끝
/* audioElement의 재생위치가 바뀔때마다 재생위치 slider 의 위치도 업데이트한다.*/
audioElement.addEventLister('timeupdate', ()=>{
document.getElementById('progress_slider').value = audioElement.currentTime;
}); // addEventListener 끝
} // initPlaylist 끝
/* 재생위치 slider의 위치에 맞게 음악을 거꾸로 업데이트 하는 function */
function changeProgress(){
const progress_slider = document.getElementById('progress_slider');
/* 재생위치 slider의 위치값을 audioElement의 재생시간에 대입*/
audioElement.curretnTime = progress_slider.value;
}
/* DOM은 HTML과 CSS를 파싱하여 만들어진것인데, 이것의 로딩이 끝나고 실행하라는 뜻 */
document.addEventListener('DOMContentLoaded', fetchTracks);
document.addEventListener('DOMContentLoaded', initPlaylist);
3번. 노래 재생을 컨트롤 하는 function들
/* 재생과 정지에 관여하는 function */
function playPause() {
/* 재생과 정지상태를 판단하여 현재상황과 반대의 이미지로 돌려준다.
재생중일땐 일시정지버튼, 일시정지중일땐 재생버튼을 보여준다. */
const playPauseIcon = document.getElementById('control_play_pause');
if (audioElement.paused) {
/* 일시정지일 경우 재생시키고 icon의 이미지를 바꾼다.*/
audioElement.play();
playPauseIcon.src = "/pictures/pause.png";
} else {
/* 재생중일 경우 일시정지시키고 icon의 이미지를 바꾼다.*/
audioElement.pause();
playPauseIcon.src = "/pictures/play.png";
}
} // playPause() 끝
/* index에 맞는 곡을 불러와 current에 나타내는 function */
function selectTrack(index){
/* index번호를 받아 trackIndex로 저장 */
trackIndex = index;
/* 앨범이미지, 음원 바꾸고 음원을 재생하며, 통제 버튼을 일시정지 버튼으로 변환 */
document.getElementById('current_track_cover').src = tracks[trackIndex].albumImage;
audioElement.src = tracks[trackIndex].albumMusic;
playPauseIcon.src = "/pictures/pause.png";
audioElement.play();
} // selectTrack 끝
/* 곡을 앞뒤로 바꾸는 function */
function prevTrack(){
/* 0보다 작아지는 경우를 대비하여 곡의 개수만큼 더해준 후 남는 번호의 곡으로 간다.
예를들어 0번곡은 이전곡 버튼을 누르면 0 -1 +5 의 나머지인 4번곡으로 돌아간다.*/
trackIndex = (trackIndex - 1 + tracks.length) % tracks.length;
selectTrack(trackIndex);
}
function nextTrack(){
trackIndex = (trackIndex + 1 + tracks.length) % tracks.length;
selectTrack(trackIndex);
} // prevTrack, nextTrack 끝
4번. 볼륨과 음악 재생시간을 표기하는 function 들
/* 볼륨을 조절하는 function */
function changeVolume(){
const volume_slider = document.getElementById('volumeSlider');
/* volume 전체를 100으로 잡고, volume_slider의 값과 대입한다.*/
audioElement.volume = volume_slider.value / 100;
} // changeVolume 끝
/* 재생시간을 표기하는 addEventListener , 시간이 바뀔때마다 실행된다.*/
audioElement.addEventListener('timeupdate', ()=>{
/* audio의 현재 시간을 progress_slider에 나타낸다. */
document.getElementById('progress_slider').value = audioElement.currentTime;
/* 변수를 정하고 분과 초를 담아준다.
단위는 초를 나타내기때문에 60으로 나눈것이 분, 나머지가 초이다. */
let current_min = Math.floor(audioElement.currentTime / 60);
let current_sec = Math.floor(audioElement.currentTime % 60);
/* 10보다 작은 초의 경우엔 0을 붙여 항상 초가 2자리로 표현되게한다. */
if(current_sec<10){
current_sec = '0' + current_sec;
}
/* 이렇게 만든 분과 초를 current_time에 넣는다. */
document.getElementById('current_time').textContext = '${current_min}:${current_sec}';
}); //addEventListener 끝
😵모양이 이쁘지 않을 것이다. 이것을 이제 div로 잘 나누어 두었으니 css를 통해 이쁘게 만들어보자.
필자의 플레이어이다. css를 짜느라 한참 걸렸지만 그래도 괜찮게 만들어진거같아 뿌듯하다. ㅎㅎ
리스트가 어디갔나 하면 아래의 드롭다운 버튼을 누르면 애니메이션으로 커버만 등장하도록 해두었다.
이미지를 클릭하면 곡이 바뀐다.
Finally) 만들어보고 느낀점
javascript의 생소한 영역인 audio를 다루어 보았는데, 생각보다 객관적이고 단순한 구조로 이루어져있으며, 내가 수치로 시간을 구하거나 오히려 슬라이더의 위치를 숫자로 음원이 재생될 진행도를 정할 수 도 있어 신기했다. 이것을 통해 다른 것들을 더 만들어 보고싶다.
다음 목표는 유튜브 뮤직이나 다른 음원 사이트에서 보았던 이동해도 유지되는 재생목록이다. 인터넷을 찾아보니 여기에 필요한것은 React라고 하는데.. 백엔드를 지향하는 나에겐 너무 깊게 들어가는 영역같아 나중에 여유가 될때 다시 한번 건드려보기로 하고 여기까지만 했다.
😎필요한 사람들을 위한 요약
✔ HTML
<script src="/js/sidebar-audio-player.js"/><div class="music_player">
<div class="current_track">
<img id="current_track_cover">
</div>
<div class="controls">
<audio id="audio" preload="metadata">
<div class="song_silder_box">
<div id="current_time">
<input type="range" min="0" max="0" value="0" class="progress_slider" oninput"change_progress()">
</div>
<div class="song_controls">
<button onclick="previous()">
<img class="control_previous" id="control_previous" src="/pictures/previous.png">
</button>
<button onclick="play_pause()">
<img class="control_play_pause" id="control_play_pause" src="/pictures/play.png">
</button>
<button onclick="next()">
<img class="control_next" id="control_next" src="/pictures/next.png">
</button>
</div>
<div class="volume_slider_box">
<input type="range" min="0" max="100" value="0" class="volume_slider" id="volume_slider" oninput="change_volume()">
</div>
</div>
<div class="playlist">
</div>
</div>
<script src="/js/sidebar-audio-player.js"/>
✔ JAVA
Configuration
package com.pits.auction.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class ResourceConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations("file:///C:/Auction/Image/");
registry.addResourceHandler("/audios/**")
.addResourceLocations("file:///C:/Auction/Audio/");
}
}
Controller
@ResponseBody
@RequestMapping("/top5Music")
public ResponseEntity<List<MusicAuctionProjection>> top5Music(){
Page<MusicAuctionProjection> top5Musics = musicAuctionService.findTop5ByEndTimeAfterCurrent();
if(top5Musics!=null){
List<MusicAuctionProjection> top5MusicList = top5Musics.getContent();
return ResponseEntity.ok(top5MusicList);
}else{
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
✔ javascript
let tracks = [];
async function fetchTracks() {
try {
const response = await fetch('/main/top5Music');
if (!response.ok) {
throw new Error('Network response was not ok ' + response.statusText);
}
console.log(response.toString());
tracks = await response.json();
initPlaylist();
} catch (error) {
console.error('Error fetching the tracks:', error);
}
}
let trackIndex = 0;
let duration = 0;
const audioElement = document.getElementById('audio');
function initPlaylist(){
const playlistDiv = document.querySelector('.playlist');
tracks.forEach((track,index)=>{
const coverImg = document.createElement('img');
coverImg.classList.add('ListImage');
coverImg.src = track.albumImage;
coverImg.alt = `${track.title}의 albumImage`;
coverImg.addEventListener('click', ()=>selectTrack(index));
playlistDiv.appendChild(coverImg);
const title = document.createElement('div');
title.classList.add('ListTitle');
title.textContent = track.title;
playlistDiv.appendChild(title);
});
document.getElementById('current_track_cover').src = tracks[0].albumImage;
audioElement.src = tracks[0].albumMusic;
audioElement.addEventListener('loadedmetadata', () =>{
duration = audioElement.duration;
document.getElementById('progress_slider').max=duration;
});
audioElement.addEventListener('timeupdate', ()=>{
document.getElementById('progress_slider').value = audioElement.currentTime;
});
}
function changeProgress(){
const progress_slider = document.getElementById('progress_slider');
audioElement.currentTime = progress_slider.value;
}
document.addEventListener('DOMContentLoaded', fetchTracks);
document.addEventListener('DOMContentLoaded', initPlaylist);
function playPause() {
const playPauseIcon = document.getElementById('control_play_pause');
if (audioElement.paused) {
audioElement.play();
playPauseIcon.src = "/pictures/pause.png";
} else {
audioElement.pause();
playPauseIcon.src = "/pictures/play.png";
}
}
function selectTrack(index){
trackIndex = index;
document.getElementById('current_track_cover').src = tracks[trackIndex].albumImage;
audioElement.src = tracks[trackIndex].albumMusic;
playPauseIcon.src = "/pictures/pause.png";
audioElement.play();
}
function prevTrack(){
trackIndex = (trackIndex - 1 + tracks.length) % tracks.length;
selectTrack(trackIndex);
}
function nextTrack(){
trackIndex = (trackIndex + 1 + tracks.length) % tracks.length;
selectTrack(trackIndex);
}
function changeVolume(){
const volume_slider = document.getElementById('volumeSlider');
audioElement.volume = volume_slider.value / 100;
}
audioElement.addEventListener('timeupdate', ()=>{
document.getElementById('progress_slider').value = audioElement.currentTime;
let current_min = Math.floor(audioElement.currentTime / 60);
let current_sec = Math.floor(audioElement.currentTime % 60);
if(current_sec<10){
current_sec = '0' + current_sec;
}
document.getElementById('current_time').textContent = `${current_min}:${current_sec}`;
});
'프로젝트 > 중앙 정보처리학원 2차(음원 경매 사이트)' 카테고리의 다른 글
중앙정보처리학원 / 중앙정보기술인재개발원 4월말기수 2차 프로젝트 후기(23.08~09) (0) | 2023.09.13 |
---|---|
마감기한을 기준으로 DB 값을 변경하기(MYSQL) (0) | 2023.09.07 |
초보자를 위한 무한스크롤 HTML에서 구현하기 / 이해하기 (0) | 2023.09.05 |