Skip to content

Commit

Permalink
앨범 상세 페이지 구현 (#198)
Browse files Browse the repository at this point in the history
* refactor: 댓글 응답 값에 날짜 추가

* feat: 앨범 상세 페이지 구현

* build: fast average color 라이브러리 추가

* feat: 앨범 상세 페이지 배경 설정

* feat: 노래 제목 옆 숫자 출력 및 코멘트 익명 출력

* refactor: 프록시 설정 제거

* refactor: 배경 그라데이션 위치 수정 및 부모 영역 패딩 추가

* feat: 댓글 작성 기능 구현

* style: 콘텐츠 영역이 화면 밖으로 나가지 않도록 스타일 개선

* refactor: comment 데이터 서버 응답 시 최신순으로 정렬하여 반환

* refactor: comment 데이터 서버 응답 시 최신 10개만 반환 하도록 수정

* refactor: 댓글 저장 시 제한된 글자 수 이내로 작성했는지 검증

* feat: 앨범명 및 부가 정보 추가

* fix: 재생 시간 계산 문제 해결

* refactor: 아티스트 정보 출력 길이 수정 및 컴포넌트 분리

* style: 댓글 텍스트 수정

* style: 제목 및 아티스트명 출력 영역 확장

* style: 플레이리스트 출력 영역 확장

* refactor: 불필요한 then 메소드 사용 제거

* refactor: useEffect에서 albumId가 수정될 때 useEffect가 호출되도록 수정

* refactor: 이미지 alt 추가

* refactor: 정적 px 값 tailwind 방식으로 수정

* refactor: 스크롤 바 색 수정

* refactor: 불필요한 export 제거
  • Loading branch information
rdyjun authored Dec 3, 2024
1 parent e97dbdd commit 0b0128f
Show file tree
Hide file tree
Showing 17 changed files with 375 additions and 68 deletions.
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"fast-average-color": "^9.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"socket.io-client": "^4.8.1"
Expand Down
3 changes: 1 addition & 2 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Routes, Route } from 'react-router-dom';
import { Outlet, Route, Routes } from 'react-router-dom';
import { MainPage } from '@/pages/MainPage';
import { StreamingPage } from '@/pages/StreamingPage';
import { AdminPage } from '@/pages/AdminPage';
import { AlbumPage } from '@/pages/AlbumPage';
import { AdminLoginPage } from '@/pages/AdminLoginPage';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import { Outlet } from 'react-router-dom';
import { Sidebar } from './widgets/sidebar/ui/Sidebar';
import { GlobalBoundary } from './GlobalBoundary';

Expand Down
2 changes: 1 addition & 1 deletion client/src/app/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { MainPage } from '@/pages/MainPage';
import { StreamingPage } from '@/pages/StreamingPage';
import { Layout } from '@/Layout';
import { AdminPage } from '@/pages/AdminPage';
import { AlbumPage } from '@/pages/AlbumPage';
import { AdminLoginPage } from '@/pages/AdminLoginPage';
import { ProtectedRoute } from '@/components/ProtectedRoute';
import { AlbumPage } from '@/pages/AlbumPage';

export const routes = [
{
Expand Down
104 changes: 103 additions & 1 deletion client/src/pages/AlbumPage/ui/AlbumPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,105 @@
import { AlbumArtist, CommentList, Playlist } from '@/widgets/albums';
import { publicAPI } from '@/shared/api/publicAPI.ts';
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { FastAverageColor } from 'fast-average-color';
import { darken } from 'polished';

interface AlbumDetails {
albumId: string;
albumName: string;
artist: string;
jacketUrl: string;
}

export function AlbumPage() {
return <div>AlbumPage</div>;
const { albumId } = useParams<{ albumId: string }>();
if (!albumId) return;

const [songDetails, setSongDetails] = useState<
{ name: string; duration: string }[]
>([]);
const [albumJacketUrl, setAlbumJacketUrl] = useState<string>('LogoAlbum');
const [albumDetails, setAlbumDetails] = useState<AlbumDetails>({});

useEffect(() => {
(async () => {
const albumResponse = await publicAPI
.getAlbumInfo(albumId)
.catch((err) => console.log(err));

setAlbumDetails(albumResponse.result.albumDetails);
setSongDetails(albumResponse.result.songDetails);
setAlbumJacketUrl(albumResponse.result.albumDetails.jacketUrl);
})();
}, [albumJacketUrl, albumId]);

const [backgroundColor, setBackgroundColor] = useState<string>('#222');

useEffect(() => {
const fac = new FastAverageColor();
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = albumJacketUrl;

img.onload = () => {
try {
if (img.width === 0 || img.height === 0) {
console.error('Image has no dimensions');
return;
}

const color = fac.getColor(img, {
algorithm: 'dominant', // 주요 색상 추출
mode: 'precision', // 더 정확한 색상 계산
});

setBackgroundColor(darken(0.4, color.hex));
} catch (e) {
console.error('Color extraction failed:', e);
}
};

return () => {
img.onload = null; // 클린업
};
}, [albumJacketUrl]); // albumJacketUrl이 변경될 때마다 실행

const totalDuration = songDetails.reduce(
(total, acc) => total + Number(acc.songDuration),
0,
);

return (
<div
className={'px-80 pt-16 flex flex-col w-full'}
style={{
background: `linear-gradient(180deg, ${backgroundColor} 0%, rgba(0, 0, 0, 0) 20%)`,
}}
>
<div className={'flex h-680 gap-20 mb-24 relative z-10'}>
<article className={'w-[21.25rem] h-85 flex-shrink-0'}>
<img
id={'album-jacket'}
src={albumJacketUrl}
className={'w-[21.25rem] h-[21.25rem] select-none'}
alt={`${albumDetails.albumName} 앨범 커버`}
></img>
<p
className={`${albumDetails.albumName?.length >= 12 ? 'text-2xl' : albumDetails.albumName?.length >= 10 ? 'text-3xl' : 'text-4xl'} text-grayscale-50 mt-8 truncate`}
style={{ fontWeight: 900 }}
>
{albumDetails.albumName}
</p>
<AlbumArtist
artist={albumDetails.artist}
songLength={songDetails.length}
totalDuration={totalDuration}
/>
</article>
<Playlist playlist={songDetails} />
</div>
<CommentList albumId={albumId} />
</div>
);
}
3 changes: 3 additions & 0 deletions client/src/shared/api/errorMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export const ERROR_MESSAGE = {
DEFAULT: {
UNKNOWN_ERROR: '알 수 없는 에러가 발생했습니다.',
},
COMMENT: {
COMMENT_MESSAGE_TO_LONG: '댓글은 200자 이내로 작성해주세요.',
},
};
18 changes: 18 additions & 0 deletions client/src/shared/api/publicAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,22 @@ export const publicAPI = {
throw error;
}
},
getComment: async (albumId: string) => {
const { data } = await publicInstance.get(`/comment/album/${albumId}`);
return data;
},
getAlbumInfo: async (albumId: string) => {
const { data } = await publicInstance.get(`/album/${albumId}`);
return data;
},
createComment: async (albumId: string, content: string) => {
if (content.length === 0 || content.length > 200) {
alert(ERROR_MESSAGE.COMMENT.COMMENT_MESSAGE_TO_LONG);
throw new CustomError(ERROR_MESSAGE.COMMENT.COMMENT_MESSAGE_TO_LONG);
}
const { data } = await publicInstance.post(`/comment/album/${albumId}`, {
content,
});
return data;
},
};
3 changes: 3 additions & 0 deletions client/src/widgets/albums/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { AlbumList } from './ui/AlbumList';
export { CommentList } from './ui/CommentList.tsx';
export { Playlist } from './ui/Playlist';
export { AlbumArtist } from './ui/AlbumArtist';
39 changes: 39 additions & 0 deletions client/src/widgets/albums/ui/AlbumArtist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
interface AlbumArtistProps {
artist: string;
songLength: number;
totalDuration: number;
}

export function AlbumArtist({
artist,
songLength,
totalDuration,
}: AlbumArtistProps) {
const hour = Math.floor(Number(totalDuration) / 3600);
const minute = Math.floor((Number(totalDuration) % 3600) / 60);
const second = Math.floor(Number(totalDuration) % 60);

return (
<section
className={
'text-lg text-grayscale-400 mt-4 flex justify-start overflow-visible whitespace-nowrap absolute max-w-[calc(100vw-340px)]'
}
>
<span className={'truncate'}>{artist}</span>
<p className={'flex-shrink-0 flex-grow-0 whitespace-nowrap'}>
<span className={'mx-2'}></span>
<span>{songLength}</span>
</p>
<p className={'flex-shrink-0 flex-grow-0 whitespace-nowrap'}>
<span className={'mx-2'}></span>
<span>
{(hour > 0 ? String(hour).padStart(2, '0') + '시간 ' : '') +
String(minute).padStart(2, '0') +
'분 ' +
String(second).padStart(2, '0') +
'초'}
</span>
</p>
</section>
);
}
22 changes: 12 additions & 10 deletions client/src/widgets/albums/ui/AlbumList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ export function AlbumList() {
<p className="mt-[70px] mb-7 text-3xl font-bold">최근 등록된 앨범</p>
<ul className="w-full grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-9">
{endedAlbumList.endedAlbums.slice(0, 7).map((album) => (
<AlbumCard
key={album.albumId}
album={{
albumId: album.albumId,
albumName: album.albumName,
artist: album.artist,
albumTags: album.albumTags || '',
jacketUrl: album.jacketUrl || LogoAlbum,
}}
/>
<a href={`/album/${album.albumId}`}>
<AlbumCard
key={album.albumId}
album={{
albumId: album.albumId,
albumName: album.albumName,
artist: album.artist,
albumTags: album.albumTags || '',
jacketUrl: album.jacketUrl || LogoAlbum,
}}
/>
</a>
))}
</ul>
</div>
Expand Down
16 changes: 16 additions & 0 deletions client/src/widgets/albums/ui/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
interface CommentProps {
comment: { content: string; createdAt: Date };
index: number;
}

export function Comment({ comment, index }: CommentProps) {
const date: Date = new Date(comment.createdAt);
const dateFormat = `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}.`;
return (
<article className={'w-full flex justify-between mb-4 overflow-hidden'}>
<p className={'w-[80px] mr-[24px] flex-shrink-0'}>댓글 #{index + 1}</p>
<p className={'word-break break-all flex-grow'}>{comment.content}</p>
<p className={'w-[90px] ml-[24px] flex-shrink-0'}>{dateFormat}</p>
</article>
);
}
61 changes: 61 additions & 0 deletions client/src/widgets/albums/ui/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Comment } from './Comment.tsx';
import { publicAPI } from '@/shared/api/publicAPI.ts';
import { Button } from '@/shared/ui';
import { useEffect, useState } from 'react';

interface CommentListProps {
albumId: string;
}

export function CommentList({ albumId }: CommentListProps) {
const [commentList, setCommentList] = useState<{ albumName: string }[]>([]);

useEffect(() => {
(async () => {
const commentResponse = await publicAPI
.getComment(albumId)
.catch((err) => console.log(err));

setCommentList(commentResponse.result.albumComments);
})();
}, [commentList]);

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

const dom = e.currentTarget.previousElementSibling;
const response = publicAPI.createComment(albumId, dom.value);

response.then((res) => {
alert('댓글이 등록되었습니다.');
setCommentList([dom.value, ...commentList]);
dom.parentElement.reset();
});
};

return (
<div className="w-full text-grayscale-50 border-t border-grayscale-700 border-solid">
<p className={'text-4xl font-bold mb-8 mt-14'}>
코멘트
<span className={'ml-6 text-sm font-normal text-grayscale-400'}>
최신 10개의 댓글만 조회합니다.
</span>
</p>
<form className={'flex justify-between items-baseline mb-12'}>
<p className={'w-20 mr-6'}>댓글 작성</p>
<input
name={'content'}
className={
'bg-transparent border-b border-solid h-10 py-2 focus:outline-none flex-grow mr-16'
}
placeholder={'여기에 댓글을 입력해주세요.'}
maxLength={200}
/>
<Button type={'submit'} message={'등록'} onClick={handleSubmit} />
</form>
{commentList.map((comment, index) => (
<Comment key={index} comment={comment} index={index} />
))}
</div>
);
}
18 changes: 18 additions & 0 deletions client/src/widgets/albums/ui/Playlist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TrackItem } from './TrackItem';
import './Scrollbar.css';

export interface PlaylistComponentProps {
playlist: { songName: string; songDuration: string }[];
}

export function Playlist({ playlist }: PlaylistComponentProps) {
return (
<article className={'w-full overflow-y-scroll h-96 pr-4'}>
{playlist.map(
(item: { songName: string; songDuration: string }, index) => (
<TrackItem trackData={item} index={index} key={index} />
),
)}
</article>
);
}
3 changes: 3 additions & 0 deletions client/src/widgets/albums/ui/Scrollbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
::-webkit-scrollbar-thumb {
background: #fafafa !important;
}
30 changes: 30 additions & 0 deletions client/src/widgets/albums/ui/TrackItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
interface TrackItemProps {
trackData: { songName: string; songDuration: string };
index: number;
}

export function TrackItem({ trackData, index }: TrackItemProps) {
const hour = Math.floor(Number(trackData.songDuration) / 3600);
const minute = Math.floor((Number(trackData.songDuration) % 3600) / 60);
const second = Math.floor(Number(trackData.songDuration) % 60);

return (
<section
className={
'flex w-full h-[30px] text-grayscale-50 justify-between mb-[24px]'
}
>
<section className={'flex'}>
<section className={'mr-8'}>{index + 1}.</section>
<section>{trackData.songName}</section>
</section>

<section className={'text-grayscale-200 text-sm'}>
{(hour > 0 ? String(hour).padStart(2, '0') + ':' : '') +
String(minute).padStart(2, '0') +
':' +
String(second).padStart(2, '0')}
</section>
</section>
);
}
Loading

0 comments on commit 0b0128f

Please sign in to comment.