Skip to content

Commit 0b0128f

Browse files
authored
앨범 상세 페이지 구현 (#198)
* 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 제거
1 parent e97dbdd commit 0b0128f

File tree

17 files changed

+375
-68
lines changed

17 files changed

+375
-68
lines changed

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"build-storybook": "storybook build"
1111
},
1212
"dependencies": {
13+
"fast-average-color": "^9.4.0",
1314
"react": "^18.2.0",
1415
"react-dom": "^18.2.0",
1516
"socket.io-client": "^4.8.1"

client/src/App.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Routes, Route } from 'react-router-dom';
1+
import { Outlet, Route, Routes } from 'react-router-dom';
22
import { MainPage } from '@/pages/MainPage';
33
import { StreamingPage } from '@/pages/StreamingPage';
44
import { AdminPage } from '@/pages/AdminPage';
55
import { AlbumPage } from '@/pages/AlbumPage';
66
import { AdminLoginPage } from '@/pages/AdminLoginPage';
77
import { ProtectedRoute } from '@/components/ProtectedRoute';
8-
import { Outlet } from 'react-router-dom';
98
import { Sidebar } from './widgets/sidebar/ui/Sidebar';
109
import { GlobalBoundary } from './GlobalBoundary';
1110

client/src/app/router/routes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { MainPage } from '@/pages/MainPage';
22
import { StreamingPage } from '@/pages/StreamingPage';
33
import { Layout } from '@/Layout';
44
import { AdminPage } from '@/pages/AdminPage';
5-
import { AlbumPage } from '@/pages/AlbumPage';
65
import { AdminLoginPage } from '@/pages/AdminLoginPage';
76
import { ProtectedRoute } from '@/components/ProtectedRoute';
7+
import { AlbumPage } from '@/pages/AlbumPage';
88

99
export const routes = [
1010
{
Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,105 @@
1+
import { AlbumArtist, CommentList, Playlist } from '@/widgets/albums';
2+
import { publicAPI } from '@/shared/api/publicAPI.ts';
3+
import { useParams } from 'react-router-dom';
4+
import { useEffect, useState } from 'react';
5+
import { FastAverageColor } from 'fast-average-color';
6+
import { darken } from 'polished';
7+
8+
interface AlbumDetails {
9+
albumId: string;
10+
albumName: string;
11+
artist: string;
12+
jacketUrl: string;
13+
}
14+
115
export function AlbumPage() {
2-
return <div>AlbumPage</div>;
16+
const { albumId } = useParams<{ albumId: string }>();
17+
if (!albumId) return;
18+
19+
const [songDetails, setSongDetails] = useState<
20+
{ name: string; duration: string }[]
21+
>([]);
22+
const [albumJacketUrl, setAlbumJacketUrl] = useState<string>('LogoAlbum');
23+
const [albumDetails, setAlbumDetails] = useState<AlbumDetails>({});
24+
25+
useEffect(() => {
26+
(async () => {
27+
const albumResponse = await publicAPI
28+
.getAlbumInfo(albumId)
29+
.catch((err) => console.log(err));
30+
31+
setAlbumDetails(albumResponse.result.albumDetails);
32+
setSongDetails(albumResponse.result.songDetails);
33+
setAlbumJacketUrl(albumResponse.result.albumDetails.jacketUrl);
34+
})();
35+
}, [albumJacketUrl, albumId]);
36+
37+
const [backgroundColor, setBackgroundColor] = useState<string>('#222');
38+
39+
useEffect(() => {
40+
const fac = new FastAverageColor();
41+
const img = new Image();
42+
img.crossOrigin = 'anonymous';
43+
img.src = albumJacketUrl;
44+
45+
img.onload = () => {
46+
try {
47+
if (img.width === 0 || img.height === 0) {
48+
console.error('Image has no dimensions');
49+
return;
50+
}
51+
52+
const color = fac.getColor(img, {
53+
algorithm: 'dominant', // 주요 색상 추출
54+
mode: 'precision', // 더 정확한 색상 계산
55+
});
56+
57+
setBackgroundColor(darken(0.4, color.hex));
58+
} catch (e) {
59+
console.error('Color extraction failed:', e);
60+
}
61+
};
62+
63+
return () => {
64+
img.onload = null; // 클린업
65+
};
66+
}, [albumJacketUrl]); // albumJacketUrl이 변경될 때마다 실행
67+
68+
const totalDuration = songDetails.reduce(
69+
(total, acc) => total + Number(acc.songDuration),
70+
0,
71+
);
72+
73+
return (
74+
<div
75+
className={'px-80 pt-16 flex flex-col w-full'}
76+
style={{
77+
background: `linear-gradient(180deg, ${backgroundColor} 0%, rgba(0, 0, 0, 0) 20%)`,
78+
}}
79+
>
80+
<div className={'flex h-680 gap-20 mb-24 relative z-10'}>
81+
<article className={'w-[21.25rem] h-85 flex-shrink-0'}>
82+
<img
83+
id={'album-jacket'}
84+
src={albumJacketUrl}
85+
className={'w-[21.25rem] h-[21.25rem] select-none'}
86+
alt={`${albumDetails.albumName} 앨범 커버`}
87+
></img>
88+
<p
89+
className={`${albumDetails.albumName?.length >= 12 ? 'text-2xl' : albumDetails.albumName?.length >= 10 ? 'text-3xl' : 'text-4xl'} text-grayscale-50 mt-8 truncate`}
90+
style={{ fontWeight: 900 }}
91+
>
92+
{albumDetails.albumName}
93+
</p>
94+
<AlbumArtist
95+
artist={albumDetails.artist}
96+
songLength={songDetails.length}
97+
totalDuration={totalDuration}
98+
/>
99+
</article>
100+
<Playlist playlist={songDetails} />
101+
</div>
102+
<CommentList albumId={albumId} />
103+
</div>
104+
);
3105
}

client/src/shared/api/errorMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ export const ERROR_MESSAGE = {
1212
DEFAULT: {
1313
UNKNOWN_ERROR: '알 수 없는 에러가 발생했습니다.',
1414
},
15+
COMMENT: {
16+
COMMENT_MESSAGE_TO_LONG: '댓글은 200자 이내로 작성해주세요.',
17+
},
1518
};

client/src/shared/api/publicAPI.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,22 @@ export const publicAPI = {
5353
throw error;
5454
}
5555
},
56+
getComment: async (albumId: string) => {
57+
const { data } = await publicInstance.get(`/comment/album/${albumId}`);
58+
return data;
59+
},
60+
getAlbumInfo: async (albumId: string) => {
61+
const { data } = await publicInstance.get(`/album/${albumId}`);
62+
return data;
63+
},
64+
createComment: async (albumId: string, content: string) => {
65+
if (content.length === 0 || content.length > 200) {
66+
alert(ERROR_MESSAGE.COMMENT.COMMENT_MESSAGE_TO_LONG);
67+
throw new CustomError(ERROR_MESSAGE.COMMENT.COMMENT_MESSAGE_TO_LONG);
68+
}
69+
const { data } = await publicInstance.post(`/comment/album/${albumId}`, {
70+
content,
71+
});
72+
return data;
73+
},
5674
};

client/src/widgets/albums/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
export { AlbumList } from './ui/AlbumList';
2+
export { CommentList } from './ui/CommentList.tsx';
3+
export { Playlist } from './ui/Playlist';
4+
export { AlbumArtist } from './ui/AlbumArtist';
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
interface AlbumArtistProps {
2+
artist: string;
3+
songLength: number;
4+
totalDuration: number;
5+
}
6+
7+
export function AlbumArtist({
8+
artist,
9+
songLength,
10+
totalDuration,
11+
}: AlbumArtistProps) {
12+
const hour = Math.floor(Number(totalDuration) / 3600);
13+
const minute = Math.floor((Number(totalDuration) % 3600) / 60);
14+
const second = Math.floor(Number(totalDuration) % 60);
15+
16+
return (
17+
<section
18+
className={
19+
'text-lg text-grayscale-400 mt-4 flex justify-start overflow-visible whitespace-nowrap absolute max-w-[calc(100vw-340px)]'
20+
}
21+
>
22+
<span className={'truncate'}>{artist}</span>
23+
<p className={'flex-shrink-0 flex-grow-0 whitespace-nowrap'}>
24+
<span className={'mx-2'}></span>
25+
<span>{songLength}</span>
26+
</p>
27+
<p className={'flex-shrink-0 flex-grow-0 whitespace-nowrap'}>
28+
<span className={'mx-2'}></span>
29+
<span>
30+
{(hour > 0 ? String(hour).padStart(2, '0') + '시간 ' : '') +
31+
String(minute).padStart(2, '0') +
32+
'분 ' +
33+
String(second).padStart(2, '0') +
34+
'초'}
35+
</span>
36+
</p>
37+
</section>
38+
);
39+
}

client/src/widgets/albums/ui/AlbumList.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,18 @@ export function AlbumList() {
2626
<p className="mt-[70px] mb-7 text-3xl font-bold">최근 등록된 앨범</p>
2727
<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">
2828
{endedAlbumList.endedAlbums.slice(0, 7).map((album) => (
29-
<AlbumCard
30-
key={album.albumId}
31-
album={{
32-
albumId: album.albumId,
33-
albumName: album.albumName,
34-
artist: album.artist,
35-
albumTags: album.albumTags || '',
36-
jacketUrl: album.jacketUrl || LogoAlbum,
37-
}}
38-
/>
29+
<a href={`/album/${album.albumId}`}>
30+
<AlbumCard
31+
key={album.albumId}
32+
album={{
33+
albumId: album.albumId,
34+
albumName: album.albumName,
35+
artist: album.artist,
36+
albumTags: album.albumTags || '',
37+
jacketUrl: album.jacketUrl || LogoAlbum,
38+
}}
39+
/>
40+
</a>
3941
))}
4042
</ul>
4143
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
interface CommentProps {
2+
comment: { content: string; createdAt: Date };
3+
index: number;
4+
}
5+
6+
export function Comment({ comment, index }: CommentProps) {
7+
const date: Date = new Date(comment.createdAt);
8+
const dateFormat = `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}.`;
9+
return (
10+
<article className={'w-full flex justify-between mb-4 overflow-hidden'}>
11+
<p className={'w-[80px] mr-[24px] flex-shrink-0'}>댓글 #{index + 1}</p>
12+
<p className={'word-break break-all flex-grow'}>{comment.content}</p>
13+
<p className={'w-[90px] ml-[24px] flex-shrink-0'}>{dateFormat}</p>
14+
</article>
15+
);
16+
}

0 commit comments

Comments
 (0)