Skip to content

Commit f062777

Browse files
authored
Merge pull request #150 from GulSam00/feat/scrollText
Feat/scroll text
2 parents 80b28f6 + fa75ed2 commit f062777

25 files changed

Lines changed: 429 additions & 163 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ sing-code/
180180
- 2026.1.27 : 버전 2.0.0 배포. DB 재설계 및 로직 리펙토링. 출석 체크, 유저 별 포인트, 곡 추천 기능 추가.
181181
- 2026.2.8 : 버전 2.1.0 배포. 비회원 상태로 곡 부를곡 추가가 가능, Footer 애니메이션 추가.
182182
- 2026.2.20 : 버전 2.2.0 배포. 검색어 자동 완성 기능. es-hangul로 초성 검색 지원.
183+
- 2026.3.2 : 버전 2.3.0 배포. 곡 추천 페이지에서 UI 조정, 곡 추천 기능 추가. 글자 자동 스크롤 기능 추가.
183184

184185
## 📝 회고
185186

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web",
3-
"version": "2.2.0",
3+
"version": "2.3.0",
44
"type": "module",
55
"private": true,
66
"scripts": {

apps/web/public/changelog.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,12 @@
101101
"일본 가수를 한글로 검색할 수 있습니다. 초성으로도 가능합니다.",
102102
"유명 일본 가수를 한눈에 조회할 수 있습니다."
103103
]
104+
},
105+
"2.3.0": {
106+
"title": "버전 2.3.0",
107+
"message": [
108+
"인기곡 페이지의 UI를 개선했습니다. 이제 인기곡 페이지에서 곡을 추천할 수 있습니다.",
109+
"로그인 시 제공되는 코인을 30개로 변경했습니다."
110+
]
104111
}
105112
}

apps/web/public/sitemap-0.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
3-
<url><loc>https://www.singcode.kr</loc><lastmod>2026-02-19T15:16:29.251Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
3+
<url><loc>https://www.singcode.kr</loc><lastmod>2026-03-02T07:59:31.054Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
44
</urlset>

apps/web/src/app/api/search/route.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { SearchSong, Song } from '@/types/song';
66
import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser';
77

88
interface DBSong extends Song {
9+
total_stats: {
10+
total_thumb: number;
11+
};
912
tosings: {
1013
user_id: string;
1114
}[];
@@ -44,7 +47,14 @@ export async function GET(request: Request): Promise<NextResponse<ApiResponse<Se
4447
const supabase = await createClient();
4548

4649
if (!authenticated) {
47-
const baseQuery = supabase.from('songs').select('*', { count: 'exact' });
50+
const baseQuery = supabase.from('songs').select(
51+
`*,
52+
total_stats (
53+
*
54+
)
55+
`,
56+
{ count: 'exact' },
57+
);
4858

4959
if (type === 'all') {
5060
baseQuery.or(`title.ilike.%${query}%,artist.ilike.%${query}%`);
@@ -64,17 +74,16 @@ export async function GET(request: Request): Promise<NextResponse<ApiResponse<Se
6474
);
6575
}
6676

67-
const songs: SearchSong[] = data.map((song: Song) => ({
77+
const songs: SearchSong[] = data.map((song: DBSong) => ({
6878
id: song.id,
6979
title: song.title,
7080
artist: song.artist,
7181
num_tj: song.num_tj,
7282
num_ky: song.num_ky,
73-
// like_activities에서 현재 사용자의 데이터가 있는지 확인
7483
isLike: false,
75-
// tosings에서 현재 사용자의 데이터가 있는지 확인
7684
isToSing: false,
7785
isSave: false,
86+
thumb: song.total_stats?.total_thumb ?? 0,
7887
}));
7988

8089
return NextResponse.json({
@@ -90,6 +99,9 @@ export async function GET(request: Request): Promise<NextResponse<ApiResponse<Se
9099
const baseQuery = supabase.from('songs').select(
91100
`
92101
*,
102+
total_stats (
103+
*
104+
),
93105
tosings (
94106
user_id
95107
),
@@ -129,11 +141,10 @@ export async function GET(request: Request): Promise<NextResponse<ApiResponse<Se
129141
num_tj: song.num_tj,
130142
num_ky: song.num_ky,
131143

132-
// tosings에서 현재 사용자의 데이터가 있는지 확인
133144
isToSing: song.tosings?.some(tosing => tosing.user_id === userId) ?? false,
134-
// like_activities에서 현재 사용자의 데이터가 있는지 확인
135145
isLike: song.like_activities?.some(like => like.user_id === userId) ?? false,
136146
isSave: song.save_activities?.some(save => save.user_id === userId) ?? false,
147+
thumb: song.total_stats?.total_thumb ?? 0,
137148
}));
138149

139150
return NextResponse.json({

apps/web/src/app/api/songs/recent-add/route.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,37 @@
1+
import { endOfMonth, format, startOfMonth } from 'date-fns';
12
import { NextResponse } from 'next/server';
23

34
import createClient from '@/lib/supabase/server';
45
import { ApiResponse } from '@/types/apiRoute';
56
import { Song } from '@/types/song';
67

7-
interface ResponseRecentAddSong {
8-
songs: Song;
9-
}
10-
export async function GET(
11-
request: Request,
12-
): Promise<NextResponse<ApiResponse<ResponseRecentAddSong[]>>> {
8+
export async function GET(request: Request): Promise<NextResponse<ApiResponse<Song[]>>> {
139
try {
1410
const { searchParams } = new URL(request.url);
1511

16-
const year = Number(searchParams.get('year')) || 0;
17-
const month = Number(searchParams.get('month')) || 0;
12+
const now = new Date();
13+
const year = Number(searchParams.get('year'));
14+
const month = Number(searchParams.get('month'));
1815

19-
const startDate = new Date(year, month, 1);
20-
const endDate = new Date(year, month + 1, 1);
16+
// date-fns의 month는 0-indexed이므로 1을 빼줌 (사용자 입력은 1-12)
17+
const baseDate = new Date(year, month, 1);
18+
const startDate = format(startOfMonth(baseDate), 'yyyy-MM-01');
19+
const endDate = format(endOfMonth(baseDate), 'yyyy-MM-dd');
2120

2221
const supabase = await createClient();
2322

24-
// songs 테이블의 startDate, endDate 사이의 데이터를 가져옴
23+
// songs 테이블의 release 날짜가 해당 월의 시작일부터 마지막일 사이인 데이터를 가져옴
2524
const { data, error: recentError } = await supabase
26-
.from('songs') // songs 테이블에서 검색
27-
.select(`*`)
28-
.gte('created_at', startDate.toISOString())
29-
.lte('created_at', endDate.toISOString())
30-
.order('created_at', { ascending: false })
31-
.limit(100); // 단순히 songs의 created_at으로 정렬
25+
.from('songs')
26+
.select('*')
27+
.gte('release', startDate)
28+
.lte('release', endDate)
29+
.order('release', { ascending: false })
30+
.limit(100);
3231

3332
if (recentError) throw recentError;
3433

35-
return NextResponse.json({ success: true, data });
34+
return NextResponse.json({ success: true, data: data as Song[] });
3635
} catch (error) {
3736
if (error instanceof Error && error.cause === 'auth') {
3837
return NextResponse.json(

apps/web/src/app/api/user/check-in/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export async function PATCH(): Promise<NextResponse<ApiResponse<void>>> {
2121
.from('users')
2222
.update({
2323
last_check_in: new Date(),
24-
point: user.point + 10,
24+
point: user.point + 30,
2525
})
2626
.eq('id', userId);
2727

apps/web/src/app/info/like/SongItem.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import MarqueeText from '@/components/MarqueeText';
12
import { Checkbox } from '@/components/ui/checkbox';
23
import { AddListModalSong } from '@/types/song';
34
import { cn } from '@/utils/cn';
@@ -20,8 +21,8 @@ export default function SongItem({
2021
onCheckedChange={() => onToggleSelect(song.song_id)}
2122
/>
2223
<div className="min-w-0 flex-1">
23-
<h4 className="truncate text-sm font-medium">{song.title}</h4>
24-
<p className="text-muted-foreground truncate text-xs">{song.artist}</p>
24+
<MarqueeText className="text-sm font-medium">{song.title}</MarqueeText>
25+
<MarqueeText className="text-muted-foreground text-xs">{song.artist}</MarqueeText>
2526
</div>
2627
</div>
2728
);
Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client';
22

33
import { Construction } from 'lucide-react';
4-
// import IntervalProgress from '@/components/ui/IntervalProgress';
54
import { RotateCw } from 'lucide-react';
65

76
import RankingItem from '@/components/RankingItem';
87
import StaticLoading from '@/components/StaticLoading';
98
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
9+
import { ScrollArea } from '@/components/ui/scroll-area';
1010
import { useSongThumbQuery } from '@/queries/songThumbQuery';
1111

1212
export default function PopularRankingList() {
@@ -17,27 +17,32 @@ export default function PopularRankingList() {
1717
}
1818

1919
return (
20-
<Card>
20+
<Card className="relative">
2121
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
2222
<CardTitle className="text-xl">추천 곡 순위</CardTitle>
23-
{/* <IntervalProgress duration={5000} onComplete={() => refetch()} isLoading={isFetching} /> */}
2423

25-
<RotateCw onClick={() => refetch()} className="cursor-pointer hover:animate-spin" />
24+
<RotateCw
25+
onClick={() => refetch()}
26+
className="absolute top-6 right-6 cursor-pointer hover:animate-spin"
27+
/>
2628
</CardHeader>
27-
<CardContent className="pt-0">
28-
<div className="space-y-0">
29-
{data && data.length > 0 ? (
30-
data.map((item, index) => (
31-
<RankingItem key={index} {...item} rank={index + 1} value={item.total_thumb} />
32-
))
33-
) : (
34-
<div className="flex h-64 flex-col items-center justify-center gap-4">
35-
<Construction className="text-muted-foreground h-16 w-16" />
36-
<p className="text-muted-foreground text-xl">데이터를 준비중이에요</p>
37-
</div>
38-
)}
39-
</div>
40-
</CardContent>
29+
30+
<ScrollArea className="h-[calc(100vh-20rem)]">
31+
<CardContent className="pt-0">
32+
<div className="space-y-0">
33+
{data && data.length > 0 ? (
34+
data.map((item, index) => (
35+
<RankingItem key={index} {...item} rank={index + 1} value={item.total_thumb} />
36+
))
37+
) : (
38+
<div className="flex h-64 flex-col items-center justify-center gap-4">
39+
<Construction className="text-muted-foreground h-16 w-16" />
40+
<p className="text-muted-foreground text-xl">데이터를 준비중이에요</p>
41+
</div>
42+
)}
43+
</div>
44+
</CardContent>
45+
</ScrollArea>
4146
</Card>
4247
);
4348
}

apps/web/src/app/popular/page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { ScrollArea } from '@/components/ui/scroll-area';
2-
31
import PopularRankingList from './PopularRankingList';
42

53
export default function PopularPage() {
@@ -8,9 +6,8 @@ export default function PopularPage() {
86
<h1 className="text-2xl font-bold">인기 노래</h1>
97

108
{/* 추천 곡 순위 */}
11-
<ScrollArea className="h-[calc(100vh-20rem)]">
12-
<PopularRankingList />
13-
</ScrollArea>
9+
10+
<PopularRankingList />
1411
</div>
1512
);
1613
}

0 commit comments

Comments
 (0)