Skip to content

Commit b866796

Browse files
authored
Merge pull request #245 from GulSam00/feat/244-searchKoreanAndNumber
[Feat] : 검색 페이지 한글 제목/가수 검색 및 번호 검색 필터 추가 (#244)
2 parents 49c8c0b + 200eeb2 commit b866796

18 files changed

Lines changed: 131 additions & 25 deletions

File tree

apps/web/public/sitemap-0.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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/manifest.webmanifest</loc><lastmod>2026-05-19T13:17:55.218Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
4-
<url><loc>https://www.singcode.kr</loc><lastmod>2026-05-19T13:17:55.220Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
5-
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-05-19T13:17:55.220Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
3+
<url><loc>https://www.singcode.kr/manifest.webmanifest</loc><lastmod>2026-05-25T11:26:49.726Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
4+
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-05-25T11:26:49.727Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
5+
<url><loc>https://www.singcode.kr</loc><lastmod>2026-05-25T11:26:49.727Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
66
</urlset>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { subDays } from 'date-fns';
12
import { NextResponse } from 'next/server';
23

34
import createClient from '@/lib/supabase/server';
@@ -11,7 +12,13 @@ interface SearchLogCount {
1112
export async function GET(): Promise<NextResponse<ApiResponse<SearchLogCount[]>>> {
1213
try {
1314
const supabase = await createClient();
14-
const { data, error } = await supabase.from('search_logs').select('text');
15+
16+
// 최근 15일간의 검색 로그만 집계
17+
const fifteenDaysAgo = subDays(new Date(), 15).toISOString();
18+
const { data, error } = await supabase
19+
.from('search_logs')
20+
.select('text')
21+
.gte('created_at', fifteenDaysAgo);
1522

1623
if (error) throw error;
1724

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

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,44 @@ interface DBSong extends Song {
2424

2525
function applyExactFilter(baseQuery: any, type: string, searchText: string) {
2626
if (type === 'all') {
27-
return baseQuery.or(`title.ilike.${searchText},artist.ilike.${searchText}`);
27+
return baseQuery.or(
28+
`title.ilike.${searchText},title_ko.ilike.${searchText},artist.ilike.${searchText},artist_ko.ilike.${searchText}`,
29+
);
30+
}
31+
if (type === 'title') {
32+
return baseQuery.or(`title.ilike.${searchText},title_ko.ilike.${searchText}`);
33+
}
34+
if (type === 'artist') {
35+
return baseQuery.or(`artist.ilike.${searchText},artist_ko.ilike.${searchText}`);
36+
}
37+
if (type === 'number') {
38+
return baseQuery.or(`num_tj.eq.${searchText},num_ky.eq.${searchText}`);
2839
}
2940
return baseQuery.ilike(type, searchText);
3041
}
3142

3243
function applyPartialFilter(baseQuery: any, type: string, searchText: string) {
3344
if (type === 'all') {
3445
return baseQuery
35-
.or(`title.ilike.%${searchText}%,artist.ilike.%${searchText}%`)
46+
.or(
47+
`title.ilike.%${searchText}%,title_ko.ilike.%${searchText}%,artist.ilike.%${searchText}%,artist_ko.ilike.%${searchText}%`,
48+
)
3649
.not('title', 'ilike', searchText)
37-
.not('artist', 'ilike', searchText);
50+
.not('title_ko', 'ilike', searchText)
51+
.not('artist', 'ilike', searchText)
52+
.not('artist_ko', 'ilike', searchText);
53+
}
54+
if (type === 'title') {
55+
return baseQuery
56+
.or(`title.ilike.%${searchText}%,title_ko.ilike.%${searchText}%`)
57+
.not('title', 'ilike', searchText)
58+
.not('title_ko', 'ilike', searchText);
59+
}
60+
if (type === 'artist') {
61+
return baseQuery
62+
.or(`artist.ilike.%${searchText}%,artist_ko.ilike.%${searchText}%`)
63+
.not('artist', 'ilike', searchText)
64+
.not('artist_ko', 'ilike', searchText);
3865
}
3966
return baseQuery.ilike(type, `%${searchText}%`).not(type, 'ilike', searchText);
4067
}
@@ -50,6 +77,26 @@ async function executeSearchQueries(
5077
): Promise<{ data: DBSong[]; hasNext: boolean } | { error: string }> {
5178
const size = to - from + 1;
5279

80+
// 번호 검색은 정확 매칭만 지원 (부분 일치 단계 스킵)
81+
if (type === 'number') {
82+
const exactCountResult = await applyExactFilter(
83+
supabase.from('songs').select(selectClause, { count: 'exact', head: true }),
84+
type,
85+
query,
86+
);
87+
if (exactCountResult.error) return { error: exactCountResult.error.message };
88+
const exactTotal = exactCountResult.count ?? 0;
89+
90+
const exactQuery = applyExactFilter(supabase.from('songs').select(selectClause), type, query);
91+
const { data, error } = await exactQuery.order(order).range(from, to);
92+
if (error) return { error: error.message };
93+
94+
return {
95+
data: (data as DBSong[]) ?? [],
96+
hasNext: exactTotal > to + 1,
97+
};
98+
}
99+
53100
// 1. 정확 일치 / 부분 일치 각각의 총 개수를 병렬로 조회
54101
const exactCountQuery = applyExactFilter(
55102
supabase.from('songs').select(selectClause, { count: 'exact', head: true }),
@@ -134,7 +181,7 @@ export async function GET(request: Request): Promise<NextResponse<ApiResponse<Se
134181
const { searchParams } = new URL(request.url);
135182
const query = searchParams.get('q');
136183
const type = searchParams.get('type') || 'title';
137-
const order = type === 'all' ? 'title' : type;
184+
const order = type === 'all' ? 'title' : type === 'number' ? 'num_tj' : type;
138185
const authenticated = searchParams.get('authenticated') === 'true';
139186

140187
const page = parseInt(searchParams.get('page') || '0', 10);

apps/web/src/app/api/songs/like/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export async function GET(): Promise<NextResponse<ApiResponse<PersonalSong[]>>>
3636
created_at: item.created_at,
3737
title: item.songs.title,
3838
artist: item.songs.artist,
39+
title_ko: item.songs.title_ko,
40+
artist_ko: item.songs.artist_ko,
3941
num_tj: item.songs.num_tj,
4042
num_ky: item.songs.num_ky,
4143
}));

apps/web/src/app/api/songs/save/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export async function GET(): Promise<NextResponse<ApiResponse<SaveSong[]>>> {
3838
updated_at: item.updated_at,
3939
title: item.songs.title,
4040
artist: item.songs.artist,
41+
title_ko: item.songs.title_ko,
42+
artist_ko: item.songs.artist_ko,
4143
num_tj: item.songs.num_tj,
4244
num_ky: item.songs.num_ky,
4345
}));

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ export default function SongItem({
2222
/>
2323
<div className="min-w-0 flex-1">
2424
<MarqueeText className="text-sm font-medium">{song.title}</MarqueeText>
25+
{song.title_ko && song.title_ko !== song.title && (
26+
<MarqueeText className="text-muted-foreground text-xs">{song.title_ko}</MarqueeText>
27+
)}
2528
<MarqueeText className="text-muted-foreground text-xs">{song.artist}</MarqueeText>
29+
{song.artist_ko && song.artist_ko !== song.artist && (
30+
<MarqueeText className="text-muted-foreground/70 text-xs">{song.artist_ko}</MarqueeText>
31+
)}
2632
</div>
2733
</div>
2834
);

apps/web/src/app/info/save/FolderCard.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,13 @@ export default function FolderCard({
111111
<Music className="text-muted-foreground h-4 w-4 shrink-0" />
112112
<div>
113113
<p className="text-sm font-medium">{song.title}</p>
114+
{song.title_ko && song.title_ko !== song.title && (
115+
<p className="text-muted-foreground text-xs">{song.title_ko}</p>
116+
)}
114117
<p className="text-muted-foreground text-xs">{song.artist}</p>
118+
{song.artist_ko && song.artist_ko !== song.artist && (
119+
<p className="text-muted-foreground/70 text-xs">{song.artist_ko}</p>
120+
)}
115121
</div>
116122
</div>
117123
</div>

apps/web/src/app/search/AddFolderModal.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function AddFolderModal({
4646
const [folderName, setFolderName] = useState<string>('');
4747
const [isExistingPlaylist, setIsExistingPlaylist] = useState(false);
4848

49-
const { id: songId, title, artist } = song;
49+
const { id: songId, title, artist, title_ko, artist_ko } = song;
5050

5151
const LOGIC_TEXT = modalType === 'POST' ? '저장' : '수정';
5252

@@ -114,7 +114,13 @@ export default function AddFolderModal({
114114
{/* 곡 정보 */}
115115
<div className="bg-muted mb-4 rounded-md p-3">
116116
<MarqueeText className="text-base font-medium">{title}</MarqueeText>
117+
{title_ko && title_ko !== title && (
118+
<MarqueeText className="text-muted-foreground text-xs">{title_ko}</MarqueeText>
119+
)}
117120
<MarqueeText className="text-muted-foreground text-sm">{artist}</MarqueeText>
121+
{artist_ko && artist_ko !== artist && (
122+
<MarqueeText className="text-muted-foreground/70 text-xs">{artist_ko}</MarqueeText>
123+
)}
118124
</div>
119125

120126
<div className="w-full space-y-4 py-2">

apps/web/src/app/search/HomePage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export default function SearchPage() {
120120
return '노래 제목 검색';
121121
case 'artist':
122122
return '가수 이름 검색';
123+
case 'number':
124+
return '노래방 번호 검색 (TJ/KY)';
123125
default:
124126
return '전체 키워드 검색';
125127
}
@@ -169,12 +171,13 @@ export default function SearchPage() {
169171
</div>
170172

171173
<Tabs defaultValue="all" value={searchType} onValueChange={handleSearchTypeChange}>
172-
<TabsList className="dark:bg-muted/50 grid w-full grid-cols-3 dark:border">
174+
<TabsList className="dark:bg-muted/50 grid w-full grid-cols-4 dark:border">
173175
{(
174176
[
175177
['all', '전체'],
176178
['title', '제목'],
177179
['artist', '가수'],
180+
['number', '번호'],
178181
] as const
179182
).map(([value, label]) => (
180183
<TabsTrigger

apps/web/src/app/search/SearchResultCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ export default function SearchResultCard({
143143
songId={id}
144144
title={title}
145145
artist={artist}
146+
title_ko={title_ko}
147+
artist_ko={artist_ko}
146148
thumb={thumb || 0}
147149
handleClose={() => setOpen(false)}
148150
/>
@@ -259,7 +261,7 @@ export default function SearchResultCard({
259261
</AnimatePresence>
260262

261263
<Dialog open={promotionOpen} onOpenChange={setPromotionOpen}>
262-
<DialogContent>
264+
<DialogContent className="h-[600px] max-h-[calc(100dvh-2rem)] overflow-y-auto">
263265
<SongPromotionModal
264266
songId={id}
265267
title={title}

0 commit comments

Comments
 (0)