Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,5 @@ yarn-error.log*
*.pem

# Crawling
log/*
crawlYoutubeSuccess.txt
crawlYoutubeFailed.txt
postByReleaseSuccess.txt
postByReleaseFailed.txt
errorLog.txt
transDataLog.txt
**/logs/*.txt

2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "0.1.0",
"version": "1.2.0",
"type": "module",
"private": true,
"scripts": {
Expand Down
18 changes: 18 additions & 0 deletions apps/web/public/changelog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"1.1.0": {
"title": "๋ฒ„์ „ 1.1.0",
"message": [
"๊ธฐ์กด ๋…ธ๋ž˜๋ฐฉ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋กœ ๊ต์ฒดํ•˜์˜€์Šต๋‹ˆ๋‹ค.",
"- TJ ๋…ธ๋ž˜๋ฐฉ์˜ 38519๊ฐœ์˜ ๊ณก ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ!",
"- ๊ธˆ์˜ ๋…ธ๋ž˜๋ฐฉ์˜ ๋ฐ์ดํ„ฐ๋„ ์ˆœ์ฐจ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ์ค‘!"
]
},
"1.2.0": {
"title": "๋ฒ„์ „ 1.2.0",
"message": [
"์ธ๊ธฐ๊ณก ํŽ˜์ด์ง€๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.",
"- ์ „์ฒด, ์—ฐ๋„๋ณ„, ์›”๋ณ„ ๋ถ€๋ฅธ ๊ณก์˜ ํ†ต๊ณ„์™€ ์ข‹์•„์š” ํ•œ ๊ณก์˜ ํ†ต๊ณ„๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
"์ข‹์•„์š” ํ•œ ๊ณก์ด ํ†ต๊ณ„์— ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค."
]
}
}
2 changes: 1 addition & 1 deletion apps/web/src/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function Footer() {
const pathname = usePathname();

return (
<footer className="bg-secondary fixed bottom-0 flex h-8 w-[360px] justify-between">
<footer className="bg-background fixed bottom-0 flex h-8 w-[360px] justify-between">
{navigation.map(item => {
const isActive = pathname === item.href;
return (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Header() {
const router = useRouter();

return (
<header className="bg-background sticky top-0 z-50 flex h-16 w-[360px] items-center justify-between p-4">
<header className="bg-background sticky top-0 z-50 flex h-16 w-[360px] items-center justify-between p-4 shadow-sm">
<Image src="/logo.png" alt="logo" width={64} height={64} onClick={() => router.push('/')} />

<Sidebar />
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import useVersionDialog from '@/hooks/useVersionDialog';
import useAuthStore from '@/stores/useAuthStore';

export default function Sidebar() {
Expand All @@ -25,6 +26,8 @@ export default function Sidebar() {
const [isEditing, setIsEditing] = useState(false);
const [newNickname, setNewNickname] = useState(user?.nickname || '');

const { version } = useVersionDialog();

const router = useRouter();

const handleEditStart = () => {
Expand Down Expand Up @@ -149,6 +152,7 @@ export default function Sidebar() {
>
<Image src="/github_mark.svg" alt="github" width={32} height={32} />
</Button>
<div>๋ฒ„์ „ {version}</div>
</div>
</div>
</SheetFooter>
Expand Down
55 changes: 55 additions & 0 deletions apps/web/src/app/api/total_stats/array/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextResponse } from 'next/server';

import createClient from '@/lib/supabase/server';
import { ApiResponse } from '@/types/apiRoute';
import { CountType } from '@/types/totalStat';

export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> {
try {
const supabase = await createClient();
const { songIds, countType, isMinus } = await request.json();

// countType ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
if (!['sing_count', 'like_count', 'saved_count'].includes(countType)) {
return NextResponse.json({ success: false, error: 'Invalid count type' }, { status: 400 });
}

songIds.forEach(async (songId: string) => {
const { data } = await supabase
.from('total_stats')
.select('*')
.eq('song_id', songId)
.single();

if (data) {
// ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ
const { error: updateError } = await supabase
.from('total_stats')
.update({
[countType]: data[countType as CountType] + (isMinus ? -1 : 1),
})
.eq('song_id', songId);

if (updateError) throw updateError;
} else {
// ์ƒˆ ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ
const { error: insertError } = await supabase.from('total_stats').insert({
song_id: songId,
[countType]: 1,
sing_count: countType === 'sing_count' ? 1 : 0,
like_count: countType === 'like_count' ? 1 : 0,
saved_count: countType === 'saved_count' ? 1 : 0,
});

if (insertError) throw insertError;
}
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error in total_stats API:', error);
return NextResponse.json(
{ success: false, error: 'Failed to update total_stats' },
{ status: 500 },
);
}
}
145 changes: 143 additions & 2 deletions apps/web/src/app/api/total_stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,150 @@ import { NextResponse } from 'next/server';

import createClient from '@/lib/supabase/server';
import { ApiResponse } from '@/types/apiRoute';
import { CountType, PeriodType, SongStat } from '@/types/totalStat';

// ์œ ํšจํ•œ ์นด์šดํŠธ ํƒ€์ž… ์ •์˜
type CountType = 'sing_count' | 'like_count' | 'saved_count';
// API KEY ๋…ธ์ถœ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋ฏธ๋“ค์›จ์–ด ์—ญํ• ์„ ํ•  API ROUTE ํ™œ์šฉ

export async function GET(request: Request): Promise<NextResponse<ApiResponse<SongStat[]>>> {
try {
const { searchParams } = new URL(request.url);
const countType = searchParams.get('countType') as CountType;
const periodType = searchParams.get('periodType') as PeriodType;

if (!countType || !periodType) {
return NextResponse.json(
{
success: false,
error: 'No query provided',
},
{ status: 400 },
);
}

const supabase = await createClient();
let resonse = [];
switch (countType) {
case 'sing_count': {
let startDate = new Date();
let endDate: Date;
switch (periodType) {
case 'all':
startDate = new Date(0); // 1970-01-01
endDate = new Date(); // ํ˜„์žฌ ๋‚ ์งœ
break;
case 'year':
startDate = new Date(startDate.getFullYear(), 0, 1); // ์˜ฌํ•ด์˜ ์ฒซ๋‚ 
endDate = new Date(startDate.getFullYear() + 1, 0, 1); // ๋‚ด๋…„์˜ ์ฒซ๋‚ 
break;
case 'month':
startDate = new Date(startDate.getFullYear(), startDate.getMonth(), 1); // ์ด๋ฒˆ ๋‹ฌ์˜ ์ฒซ๋‚ 
endDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1); // ๋‹ค์Œ ๋‹ฌ์˜ ์ฒซ๋‚ 
break;
default:
return NextResponse.json(
{
success: false,
error: 'Invalid period type',
},
{ status: 400 },
);
}
const { data: singCountData, error: singCountError } = await supabase
.from('total_stats')
.select('*, songs(*)')
.gte('created_at', startDate.toISOString())
.lt('created_at', endDate.toISOString())
.gt('sing_count', 0)
.order('sing_count', { ascending: false })
.limit(10);

if (singCountError) {
return NextResponse.json(
{
success: false,
error: singCountError?.message || 'Unknown error',
},
{ status: 500 },
);
}
resonse = singCountData.map(item => ({
value: item.sing_count,
song: item.songs,
}));
break;
}

case 'like_count': {
const { data: likeCountData, error: likeCountError } = await supabase
.from('total_stats')
.select('*, songs(*)')
.gt('like_count', 0)
.order('like_count', { ascending: false })
.limit(10);

if (likeCountError) {
return NextResponse.json(
{
success: false,
error: likeCountError?.message || 'Unknown error',
},
{ status: 500 },
);
}
resonse = likeCountData.map(item => ({
value: item.like_count,
song: item.songs,
}));
break;
}
case 'saved_count': {
const { data: savedCountData, error: savedCountError } = await supabase
.from('total_stats')
.select('*, songs(*)')
.gt('saved_count', 0)
.order('saved_count', { ascending: false })
.limit(10);

if (savedCountError) {
return NextResponse.json(
{
success: false,
error: savedCountError?.message || 'Unknown error',
},
{ status: 500 },
);
}
resonse = savedCountData.map(item => ({
value: item.saved_count,
song: item.songs,
}));
break;
}
default:
return NextResponse.json(
{
success: false,
error: 'Invalid count type',
},
{ status: 400 },
);
}

return NextResponse.json({
success: true,
data: resonse,
});
} catch (error) {
console.error('Error in search API:', error);
return NextResponse.json(
{
success: false,
error: 'Internal server error',
},
{ status: 500 },
);
}
}

export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> {
try {
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/api/version/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import pkg from '../../../../package.json';

export async function GET() {
return Response.json({ version: pkg.version });
}
2 changes: 1 addition & 1 deletion apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function RootLayout({
<AuthProvider>
<PostHogProvider>
<ErrorWrapper>
<div className="bg-secondary relative flex h-full w-[360px] flex-col">
<div className="relative flex h-full w-[360px] flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/library/liked/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ export default function LikedPage() {
const likedSongs = data ?? [];

return (
<div className="bg-background h-full px-4">
<div className="bg-background h-full">
{isLoading && <StaticLoading />}
<div className="mb-6 flex items-center">
<div className="mb-6 flex items-center px-2 py-4 shadow-sm">
<Button variant="ghost" size="icon" onClick={() => router.back()} className="mr-2">
<ArrowLeft className="h-5 w-5" />
</Button>
<h1 className="text-2xl font-bold">์ข‹์•„์š” ๊ณก ๊ด€๋ฆฌ</h1>
</div>

<div className="mb-4 flex items-center justify-between">
<div className="flex h-[48px] items-center justify-between p-2">
<p className="text-muted-foreground text-sm">
{deleteLikeSelected.length > 0
? `${deleteLikeSelected.length}๊ณก ์„ ํƒ๋จ`
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/app/library/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ export default function LibraryPage() {
const nickname = user?.nickname ?? '๊ทผ๋ฐ ๋ˆ„๊ตฌ์…จ๋”๋ผ...?';

return (
<div className="bg-background h-full space-y-4 px-4 py-8">
<h1 className="text-2xl font-bold">๋ฐ˜๊ฐ€์›Œ์š”, {nickname}</h1>
<div className="bg-background h-full space-y-4">
<div className="mb-6 flex items-center justify-between px-2 py-4 shadow-sm">
<h1 className="text-2xl font-bold">๋‚ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ</h1>
</div>

{/* <h1 className="text-xl font-bold">๋ฐ˜๊ฐ€์›Œ์š”, {nickname}</h1> */}

{menuItems.map(item => (
<Card
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/app/library/stats/UserRankingList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import RankingItem from '@/components/RankingItem';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { UserSongStat } from '@/types/userStat';
import { cn } from '@/utils/cn';

interface RankingListProps {
title: string;
items: UserSongStat[];

className?: string;
}
export default function UserRankingList({ title, items, className }: RankingListProps) {
return (
// <Card className={cn('w-[360px]', className)}>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xl">{title}</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="space-y-0">
{items.map((item, index) => (
<RankingItem key={index} {...item} rank={index + 1} value={item.singCount} />
))}
</div>
</CardContent>
</Card>
);
}
Loading