Skip to content
13 changes: 10 additions & 3 deletions src/app/charts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ export default async function Charts() {
// const playlistId = "2fmFoUa7WNxIfvUg2jghxD";

const tracksList = await useTrackList();
const koraTracksList = await useTrackList({
playlistId: "1Gg5BI7b5xljyHnGXXrX0E",
});
const usaTracksList = await useTrackList({
playlistId: "0TyhU3nPbWY8BNObcPXt4u",
});
console.log("koraTracksList", koraTracksList);
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove or guard this console.log to avoid leaking debug output in production.

Suggested change
console.log("koraTracksList", koraTracksList);
if (process.env.NODE_ENV === "development") {
console.log("koraTracksList", koraTracksList);
}

Copilot uses AI. Check for mistakes.

return (
<div className="h-screen ">
<HeaderMain />
<div className="w-[1043px] mx-auto mt-[300px]">
<ChartTop tracksList={tracksList} />
<div className="flex items-center justify-between mt-10">
<ChartComponent tracksList={tracksList} />
<ChartComponent tracksList={tracksList} />
<div className="flex items-center justify-between w-full gap-10">
<ChartComponent tracksList={koraTracksList} title="한국 Top 50" />
<ChartComponent tracksList={usaTracksList} title="미국 Top 50" />
</div>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/app/homepage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default async function HomePage() {
<HeaderMain />
<div className="w-[1043px] mx-auto">
<div className="flex flex-col items-center justify-start h-full">
<main className="flex mt-[168px] gap-4 h-[617px]">
<main className="flex mt-[186px] gap-4 h-[617px]">
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider replacing the magic value mt-[186px] with a semantic utility or theme-defined spacing for consistency.

Suggested change
<main className="flex mt-[186px] gap-4 h-[617px]">
<main className="flex mt-custom-186 gap-4 h-[617px]">

Copilot uses AI. Check for mistakes.
<div className="flex items-center justify-center ">
<header className="w-[627px] h-[618px]">
<Image
Expand All @@ -27,10 +27,10 @@ export default async function HomePage() {
</header>
</div>
<div className="flex items-center justify-between flex-col w-[400px] h-[600px]">
<InterviewList />
<InterviewList className="mx-auto h-[800px]" slice={4} />
</div>
</main>
<div className="mx-auto mt-[100px] w-full">
<div className="mx-auto mt-10 w-full">
<ChartTop5 tracksList={tracksList} />
</div>
<YoutubePlaylist />
Expand Down
47 changes: 44 additions & 3 deletions src/features/chart/components/ChartComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,48 @@
export default function ChartComponent(tracksList: any) {
import Image from "next/image";
import { TrackItem } from "@/shared/types/SpotifyTrack";

interface ChartComponentProps {
tracksList: TrackItem[];
title: string;
className?: string;
}

export default function ChartComponent({
tracksList,
title,
className = "",
}: ChartComponentProps) {
return (
<div>
<div className="text-lg">차트 컴포넌트</div>
<div
className={`relative border-3 border-black p-5 mt-10 max-w-7xl bg-white w-full ${className}`}
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The border-3 class isn’t part of default Tailwind utilities; switch to an existing utility or define a custom one.

Suggested change
className={`relative border-3 border-black p-5 mt-10 max-w-7xl bg-white w-full ${className}`}
className={`relative border-2 border-black p-5 mt-10 max-w-7xl bg-white w-full ${className}`}

Copilot uses AI. Check for mistakes.
>
<div className="absolute -top-6 left-1/2 -translate-x-1/2 bg-black text-white px-6 py-2 border-2 border-black font-bold text-2xl">
{title}
</div>
{tracksList.map((item, index) => (
<div
key={index}
Comment on lines +22 to +24
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the array index as a React key can lead to rendering issues; prefer a stable, unique identifier if available.

Suggested change
{tracksList.map((item, index) => (
<div
key={index}
{tracksList.map((item) => (
<div
key={item.track.id}

Copilot uses AI. Check for mistakes.
className="flex items-center gap-4 mb-4 border-b-2 border-black pb-4 cursor-pointer hover:bg-gray-100 transition w-[450px] last:border-b-0 last:pb-0 last:mb-0"
>
<div className="flex items-center gap-4">
<div className="font-bold text-3xl w-[30px]">{index + 1}</div>
<Image
src={item.track.album.images[0].url}
alt={item.track.name}
width={100}
height={100}
/>
</div>
<div className="flex flex-col overflow-hidden w-[300px] ">
<div className="font-bold text-xl break-words max-w-xs">
{item.track.name}
</div>
<div className=" max-w-md text-gray-600 break-words">
{item.track.artists.map((artist) => artist.name).join(", ")}
</div>
</div>
</div>
))}
</div>
);
}
36 changes: 23 additions & 13 deletions src/features/chart/components/ChartTop.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Image from "next/image";

import Miniplayer from "@/features/chart/components/Miniplayer";
import InterviewList from "@/features/homepage/components/InterviewList";

import { getYoutubeTrackIdVideo } from "@/features/tracks/hooks/getYoutube";
import { TrackItem } from "@/shared/types/SpotifyTrack";

Expand All @@ -20,22 +22,26 @@ export default async function ChartTop({
return <div>트랙이 없습니다.</div>;
}
return (
<div className="relative border-2 border-black p-10 mt-10 max-w-7xl mx-auto bg-white">
<div className="relative border-3 border-black p-10 mt-10 max-w-7xl mx-auto bg-white">
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tailwind CSS does not include a border-3 utility by default; consider using border-2, border-4, or adding a custom border width.

Suggested change
<div className="relative border-3 border-black p-10 mt-10 max-w-7xl mx-auto bg-white">
<div className="relative border-2 border-black p-10 mt-10 max-w-7xl mx-auto bg-white">

Copilot uses AI. Check for mistakes.
<div className="absolute -top-6 left-1/2 -translate-x-1/2 bg-black text-white px-6 py-2 border-2 border-black font-bold text-2xl">
Top 1
Global Top 1
</div>

<div className="flex flex-col items-center justify-center w-full gap-10">
<iframe
width="100%"
height="500"
src={`https://www.youtube.com/embed/${musicVideo[0].id.videoId}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
<Miniplayer track={tracksList[0]?.track} />
{musicVideo && musicVideo.length > 0 ? (
<iframe
width="100%"
height="500"
src={`https://www.youtube.com/embed/${musicVideo[0].id.videoId}`}
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
) : (
<div>뮤직비디오를 찾을 수 없습니다.</div>
)}
{/* <Miniplayer track={tracksList[0]?.track} /> */}
<div className="flex justify-between items-center w-full">
{/*클릭시 해당 트랙 페이지로 이동*/}
<Image
Expand All @@ -44,7 +50,11 @@ export default async function ChartTop({
width={400}
height={400}
/>
<div>맵을 통한 3개 인터뷰</div>
<InterviewList
artistName={tracksList[0]?.track.artists[0].name}
className="w-[500px] mx-[0px]"
slice={3}
/>
</div>
</div>
</div>
Expand Down
20 changes: 15 additions & 5 deletions src/features/homepage/components/InterviewList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import getTrackIdInterview from "@/features/tracks/hooks/getTrackIdInterview";
import Link from "next/link";

import getTrackIdInterview from "@/features/tracks/hooks/getTrackIdInterview";

import { formatDate } from "@/lib/utils/date";

export default async function InterviewList() {
const LATEST_INTERVIEWS_QUERY = `artist interview site:rollingstone.com OR site:billboard.com OR site:pitchfork.com OR site:complex.com`;
export default async function InterviewList({
artistName,
className = "",
slice = 5,
}: {
artistName?: string;
className?: string;
slice?: number;
}) {
const LATEST_INTERVIEWS_QUERY = `${artistName} artist interview site:rollingstone.com OR site:billboard.com OR site:pitchfork.com OR site:complex.com`;
const interviews = await getTrackIdInterview(LATEST_INTERVIEWS_QUERY);

const sortedInterviews = interviews
Expand All @@ -21,12 +31,12 @@ export default async function InterviewList() {
return dateB - dateA;
});
return (
<div className=" pt-6 px-6 w-full max-w-2xl mx-auto border-2 border-black ">
<div className={`pt-6 px-6 border-2 border-black ${className}`}>
<h1 className="text-2xl font-semibold mb-6 text-slate-700 text-center">
Latest Interviews
</h1>
<ul>
{sortedInterviews.slice(0, 5).map((interview) => (
{sortedInterviews.slice(0, slice).map((interview) => (
<li key={interview.link} className="p-4 border-t border-black">
<Link
href={interview.link}
Expand Down
34 changes: 19 additions & 15 deletions src/features/tracks/hooks/getTrackIdInterview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@ import { CustomSearchResult } from "../types/custom-search";
export default async function getTrackIdInterview(
who: string
): Promise<CustomSearchResult[]> {
const API_KEY = process.env.GOOGLE_API_KEY!;
const CSE_ID = process.env.GOOGLE_CSE_ID!;
try {
const API_KEY = process.env.GOOGLE_API_KEY!;
const CSE_ID = process.env.GOOGLE_CSE_ID!;

const res = await fetch(
`https://www.googleapis.com/customsearch/v1?key=${API_KEY}&cx=${CSE_ID}&q=${encodeURIComponent(
who + " interview"
)}`,
{
next: { revalidate: 60 * 60 * 24 },
const res = await fetch(
`https://www.googleapis.com/customsearch/v1?key=${API_KEY}&cx=${CSE_ID}&q=${encodeURIComponent(
who
)}`,
{
next: { revalidate: 60 * 60 * 24 },
}
);

if (!res.ok) {
throw new Error("Failed to fetch YouTube videos");
}
);

if (!res.ok) {
throw new Error("Failed to fetch YouTube videos");
const data = await res.json();
return data.items || [];
} catch (error) {
console.error("getTrackIdInterview() 에러:", error);
return [];
}

const data = await res.json();
return data.items || [];
}

// 사용법: const interviews = await getTrackIdInterview(who);
82 changes: 44 additions & 38 deletions src/features/tracks/hooks/getYoutube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,51 @@ import { connectToDB } from "@/lib/mongo";
export async function getYoutubeTrackIdVideo(
trackName: string
): Promise<YoutubeVideo[]> {
const db = await connectToDB();
const collection = db.collection("musicVideos");

const cached = await collection.findOne({ trackName });

if (cached) {
return cached.videos;
}

const baseUrl = process.env.BASE_URL || "http://127.0.0.1:3000";

const res = await fetch(
`${baseUrl}/api/youtube-search?q=${encodeURIComponent(trackName)}`
);

if (!res.ok) {
throw new Error("유튜브 검색에 실패했습니다");
}

const data = await res.json();
const videos = data.items || [];

if (videos.length === 0) {
throw new Error("비디오를 찾을 수 없습니다");
}

await collection.updateOne(
{ trackName },
{
$set: {
trackName,
videos,
updatedAt: Date.now(),
try {
const db = await connectToDB();
const collection = db.collection("musicVideos");

const cached = await collection.findOne({ trackName });

if (cached) {
return cached.videos;
}

const baseUrl = process.env.BASE_URL || "http://127.0.0.1:3000";

const res = await fetch(
`${baseUrl}/api/youtube-search?q=${encodeURIComponent(trackName)}`
);

if (!res.ok) {
throw new Error("유튜브 검색에 실패했습니다");
return [];
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This return [] is unreachable because it follows a throw. Remove either the throw or the return to clarify the error path.

Suggested change
return [];

Copilot uses AI. Check for mistakes.
}

const data = await res.json();
const videos = data.items || [];

if (videos.length === 0) {
throw new Error("비디오를 찾을 수 없습니다");
return [];
Copy link

Copilot AI Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this return [] after a throw is unreachable and should be removed for clarity.

Suggested change
return [];

Copilot uses AI. Check for mistakes.
}

await collection.updateOne(
{ trackName },
{
$set: {
trackName,
videos,
updatedAt: Date.now(),
},
},
},
{ upsert: true }
);

return videos;
{ upsert: true }
);
return videos;
} catch (error) {
console.error("getYoutubeTrackIdVideo() 에러:", error);
return [];
}
}

// 유튜브 채널 가져오는 함수
Expand Down
8 changes: 5 additions & 3 deletions src/shared/hooks/useTrackList.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import getTopTrackPlaylist from "@/features/chart/hooks/getTopTrackPlaylist";

export default async function useTrackList() {
const playlistId = "2fmFoUa7WNxIfvUg2jghxD";
const tracksList = await getTopTrackPlaylist(playlistId);
export default async function useTrackList({
playlistId,
}: { playlistId?: string } = {}) {
const finalPlaylistId = playlistId || "2fmFoUa7WNxIfvUg2jghxD";
const tracksList = await getTopTrackPlaylist(finalPlaylistId);

return tracksList;
}
Expand Down