diff --git a/apps/web/public/sitemap-0.xml b/apps/web/public/sitemap-0.xml index c6d2ee01..bed39794 100644 --- a/apps/web/public/sitemap-0.xml +++ b/apps/web/public/sitemap-0.xml @@ -1,6 +1,6 @@ -https://www.singcode.kr/manifest.webmanifest2026-05-19T13:17:55.218Zweekly0.7 -https://www.singcode.kr2026-05-19T13:17:55.220Zweekly0.7 -https://www.singcode.kr/patch-notes2026-05-19T13:17:55.220Zweekly0.7 +https://www.singcode.kr/manifest.webmanifest2026-05-25T11:26:49.726Zweekly0.7 +https://www.singcode.kr/patch-notes2026-05-25T11:26:49.727Zweekly0.7 +https://www.singcode.kr2026-05-25T11:26:49.727Zweekly0.7 \ No newline at end of file diff --git a/apps/web/src/app/api/search/log/route.ts b/apps/web/src/app/api/search/log/route.ts index 27526586..c48d7392 100644 --- a/apps/web/src/app/api/search/log/route.ts +++ b/apps/web/src/app/api/search/log/route.ts @@ -1,3 +1,4 @@ +import { subDays } from 'date-fns'; import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; @@ -11,7 +12,13 @@ interface SearchLogCount { export async function GET(): Promise>> { try { const supabase = await createClient(); - const { data, error } = await supabase.from('search_logs').select('text'); + + // 최근 15일간의 검색 로그만 집계 + const fifteenDaysAgo = subDays(new Date(), 15).toISOString(); + const { data, error } = await supabase + .from('search_logs') + .select('text') + .gte('created_at', fifteenDaysAgo); if (error) throw error; diff --git a/apps/web/src/app/api/search/route.ts b/apps/web/src/app/api/search/route.ts index e0dd9fac..32b5e2bf 100644 --- a/apps/web/src/app/api/search/route.ts +++ b/apps/web/src/app/api/search/route.ts @@ -24,7 +24,18 @@ interface DBSong extends Song { function applyExactFilter(baseQuery: any, type: string, searchText: string) { if (type === 'all') { - return baseQuery.or(`title.ilike.${searchText},artist.ilike.${searchText}`); + return baseQuery.or( + `title.ilike.${searchText},title_ko.ilike.${searchText},artist.ilike.${searchText},artist_ko.ilike.${searchText}`, + ); + } + if (type === 'title') { + return baseQuery.or(`title.ilike.${searchText},title_ko.ilike.${searchText}`); + } + if (type === 'artist') { + return baseQuery.or(`artist.ilike.${searchText},artist_ko.ilike.${searchText}`); + } + if (type === 'number') { + return baseQuery.or(`num_tj.eq.${searchText},num_ky.eq.${searchText}`); } return baseQuery.ilike(type, searchText); } @@ -32,9 +43,25 @@ function applyExactFilter(baseQuery: any, type: string, searchText: string) { function applyPartialFilter(baseQuery: any, type: string, searchText: string) { if (type === 'all') { return baseQuery - .or(`title.ilike.%${searchText}%,artist.ilike.%${searchText}%`) + .or( + `title.ilike.%${searchText}%,title_ko.ilike.%${searchText}%,artist.ilike.%${searchText}%,artist_ko.ilike.%${searchText}%`, + ) .not('title', 'ilike', searchText) - .not('artist', 'ilike', searchText); + .not('title_ko', 'ilike', searchText) + .not('artist', 'ilike', searchText) + .not('artist_ko', 'ilike', searchText); + } + if (type === 'title') { + return baseQuery + .or(`title.ilike.%${searchText}%,title_ko.ilike.%${searchText}%`) + .not('title', 'ilike', searchText) + .not('title_ko', 'ilike', searchText); + } + if (type === 'artist') { + return baseQuery + .or(`artist.ilike.%${searchText}%,artist_ko.ilike.%${searchText}%`) + .not('artist', 'ilike', searchText) + .not('artist_ko', 'ilike', searchText); } return baseQuery.ilike(type, `%${searchText}%`).not(type, 'ilike', searchText); } @@ -50,6 +77,26 @@ async function executeSearchQueries( ): Promise<{ data: DBSong[]; hasNext: boolean } | { error: string }> { const size = to - from + 1; + // 번호 검색은 정확 매칭만 지원 (부분 일치 단계 스킵) + if (type === 'number') { + const exactCountResult = await applyExactFilter( + supabase.from('songs').select(selectClause, { count: 'exact', head: true }), + type, + query, + ); + if (exactCountResult.error) return { error: exactCountResult.error.message }; + const exactTotal = exactCountResult.count ?? 0; + + const exactQuery = applyExactFilter(supabase.from('songs').select(selectClause), type, query); + const { data, error } = await exactQuery.order(order).range(from, to); + if (error) return { error: error.message }; + + return { + data: (data as DBSong[]) ?? [], + hasNext: exactTotal > to + 1, + }; + } + // 1. 정확 일치 / 부분 일치 각각의 총 개수를 병렬로 조회 const exactCountQuery = applyExactFilter( supabase.from('songs').select(selectClause, { count: 'exact', head: true }), @@ -134,7 +181,7 @@ export async function GET(request: Request): Promise>> created_at: item.created_at, title: item.songs.title, artist: item.songs.artist, + title_ko: item.songs.title_ko, + artist_ko: item.songs.artist_ko, num_tj: item.songs.num_tj, num_ky: item.songs.num_ky, })); diff --git a/apps/web/src/app/api/songs/save/route.ts b/apps/web/src/app/api/songs/save/route.ts index 7688ffdc..4bd9500d 100644 --- a/apps/web/src/app/api/songs/save/route.ts +++ b/apps/web/src/app/api/songs/save/route.ts @@ -38,6 +38,8 @@ export async function GET(): Promise>> { updated_at: item.updated_at, title: item.songs.title, artist: item.songs.artist, + title_ko: item.songs.title_ko, + artist_ko: item.songs.artist_ko, num_tj: item.songs.num_tj, num_ky: item.songs.num_ky, })); diff --git a/apps/web/src/app/info/like/SongItem.tsx b/apps/web/src/app/info/like/SongItem.tsx index 6c5b8906..6d24d491 100644 --- a/apps/web/src/app/info/like/SongItem.tsx +++ b/apps/web/src/app/info/like/SongItem.tsx @@ -22,7 +22,13 @@ export default function SongItem({ />
{song.title} + {song.title_ko && song.title_ko !== song.title && ( + {song.title_ko} + )} {song.artist} + {song.artist_ko && song.artist_ko !== song.artist && ( + {song.artist_ko} + )}
); diff --git a/apps/web/src/app/info/save/FolderCard.tsx b/apps/web/src/app/info/save/FolderCard.tsx index 5217c4b2..993bb113 100644 --- a/apps/web/src/app/info/save/FolderCard.tsx +++ b/apps/web/src/app/info/save/FolderCard.tsx @@ -111,7 +111,13 @@ export default function FolderCard({

{song.title}

+ {song.title_ko && song.title_ko !== song.title && ( +

{song.title_ko}

+ )}

{song.artist}

+ {song.artist_ko && song.artist_ko !== song.artist && ( +

{song.artist_ko}

+ )}
diff --git a/apps/web/src/app/search/AddFolderModal.tsx b/apps/web/src/app/search/AddFolderModal.tsx index 6f6ad0ad..38159b35 100644 --- a/apps/web/src/app/search/AddFolderModal.tsx +++ b/apps/web/src/app/search/AddFolderModal.tsx @@ -46,7 +46,7 @@ export default function AddFolderModal({ const [folderName, setFolderName] = useState(''); const [isExistingPlaylist, setIsExistingPlaylist] = useState(false); - const { id: songId, title, artist } = song; + const { id: songId, title, artist, title_ko, artist_ko } = song; const LOGIC_TEXT = modalType === 'POST' ? '저장' : '수정'; @@ -114,7 +114,13 @@ export default function AddFolderModal({ {/* 곡 정보 */}
{title} + {title_ko && title_ko !== title && ( + {title_ko} + )} {artist} + {artist_ko && artist_ko !== artist && ( + {artist_ko} + )}
diff --git a/apps/web/src/app/search/HomePage.tsx b/apps/web/src/app/search/HomePage.tsx index 8b8e1769..383ceb57 100644 --- a/apps/web/src/app/search/HomePage.tsx +++ b/apps/web/src/app/search/HomePage.tsx @@ -120,6 +120,8 @@ export default function SearchPage() { return '노래 제목 검색'; case 'artist': return '가수 이름 검색'; + case 'number': + return '노래방 번호 검색 (TJ/KY)'; default: return '전체 키워드 검색'; } @@ -169,12 +171,13 @@ export default function SearchPage() {
- + {( [ ['all', '전체'], ['title', '제목'], ['artist', '가수'], + ['number', '번호'], ] as const ).map(([value, label]) => ( setOpen(false)} /> @@ -259,7 +261,7 @@ export default function SearchResultCard({ - +
{song.title} + {song.title_ko && song.title_ko !== song.title && ( + {song.title_ko} + )} {song.artist} + {song.artist_ko && song.artist_ko !== song.artist && ( + {song.artist_ko} + )}
); diff --git a/apps/web/src/components/PromotionBanner.tsx b/apps/web/src/components/PromotionBanner.tsx index 857bd204..b2568095 100644 --- a/apps/web/src/components/PromotionBanner.tsx +++ b/apps/web/src/components/PromotionBanner.tsx @@ -86,7 +86,7 @@ export default function PromotionBanner() { transition={{ duration: 0.25, ease: 'easeInOut' }} className="overflow-hidden" > -
+
-
+
{current.title} {hasKoTitle && ( - + {current.title_ko} )}
-
+
{current.artist} {hasKoArtist && ( - + {current.artist_ko} )} diff --git a/apps/web/src/components/RankingItem.tsx b/apps/web/src/components/RankingItem.tsx index 5ac27bb3..f1db5895 100644 --- a/apps/web/src/components/RankingItem.tsx +++ b/apps/web/src/components/RankingItem.tsx @@ -14,6 +14,8 @@ interface RankingItemProps { rank: number; title: string; artist: string; + title_ko?: string; + artist_ko?: string; num_tj: string; num_ky: string; value: number; @@ -25,6 +27,8 @@ export default function RankingItem({ rank, title, artist, + title_ko, + artist_ko, num_tj, num_ky, value, @@ -68,7 +72,13 @@ export default function RankingItem({
{title} + {title_ko && title_ko !== title && ( + {title_ko} + )} {artist} + {artist_ko && artist_ko !== artist && ( + {artist_ko} + )}
@@ -82,7 +92,7 @@ export default function RankingItem({
-
+