Skip to content
Open
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
6 changes: 3 additions & 3 deletions apps/web/public/sitemap-0.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<url><loc>https://www.singcode.kr/manifest.webmanifest</loc><lastmod>2026-08-25T16:04:17.180Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-08-25T16:04:17.182Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr</loc><lastmod>2026-08-25T16:04:17.182Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/manifest.webmanifest</loc><lastmod>2026-08-28T16:24:56.791Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-08-28T16:24:56.793Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr</loc><lastmod>2026-08-28T16:24:56.794Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
</urlset>
202 changes: 101 additions & 101 deletions apps/web/src/app/popular/ArtistVotePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,118 +137,118 @@ export default function ArtistVotePanel() {
);
};

if (!isAuthenticated) {
return (
<div className="flex h-64 flex-col items-center justify-center gap-2">
<p className="text-muted-foreground text-xl">로그인하고 투표해보세요</p>
<p className="text-muted-foreground text-sm">
보유 포인트로 이달의 아티스트를 뽑을 수 있어요.
</p>
</div>
);
}

return (
<div className="flex min-h-0 flex-1 flex-col gap-3">
<div className="text-muted-foreground shrink-0 text-sm">
보유 포인트 <span className="text-foreground font-bold">{point}P</span>
{changedRows.length > 0 && (
<span className={isOverPoint ? 'text-destructive' : ''}>
{' '}
→ 저장 후 {remainingPoint}P
</span>
)}
</div>

<div className="relative shrink-0">
<Input
placeholder="아티스트 검색"
value={query}
onChange={event => handleChangeQuery(event.target.value)}
onFocus={() => setIsFocusAuto(true)}
onBlur={() => setIsFocusAuto(false)}
/>
{isFocusAuto && (
<SearchAutocomplete autoCompleteList={autoCompleteList} onSelect={handleSelectArtist} />
)}
</div>

{isSearchEmpty && (
<p className="text-muted-foreground shrink-0 text-sm">
&lsquo;{query.trim()}&rsquo; 검색 결과가 없어요.
</p>
)}
<div className="relative flex min-h-0 flex-1 flex-col">
{/* 비로그인이어도 투표 화면이 어떤 모습인지는 그대로 보여주고 안내 문구만 위에 덮는다.
inert는 아래 UI의 클릭·포커스·접근성 트리 노출을 한 번에 막아, 로그인 없이 조작되는 일이 없다. */}
<div className="flex min-h-0 flex-1 flex-col gap-3" inert={!isAuthenticated}>
<div className="text-muted-foreground shrink-0 text-sm">
보유 포인트 <span className="text-foreground font-bold">{point}P</span>
{changedRows.length > 0 && (
<span className={isOverPoint ? 'text-destructive' : ''}>
{' '}
→ 저장 후 {remainingPoint}P
</span>
)}
</div>

<ScrollArea className="min-h-0 flex-1">
<div className="flex flex-col gap-2 pr-2">
{rows.length === 0 ? (
<p className="text-muted-foreground py-8 text-center text-sm">
검색해서 투표할 아티스트를 담아보세요.
</p>
) : (
rows.map(row => (
<ArtistVoteRow
key={row.artist}
artist={row.artist}
artistKo={row.artistKo}
amount={row.amount}
savedAmount={row.savedAmount}
step={STEP}
disabled={isPending}
onChange={amount => handleChange(row.artist, amount)}
onSubmit={handleSubmit}
onDelete={() => handleDelete(row.artist)}
/>
))
<div className="relative shrink-0">
<Input
placeholder="아티스트 검색"
value={query}
onChange={event => handleChangeQuery(event.target.value)}
onFocus={() => setIsFocusAuto(true)}
onBlur={() => setIsFocusAuto(false)}
/>
{isFocusAuto && (
<SearchAutocomplete autoCompleteList={autoCompleteList} onSelect={handleSelectArtist} />
)}
</div>
</ScrollArea>

<div className="flex shrink-0 flex-col gap-2">
{isOverPoint && (
<p className="text-destructive text-sm">
포인트가 {-remainingPoint}P 부족해요. 투표값을 줄여주세요.
{isSearchEmpty && (
<p className="text-muted-foreground shrink-0 text-sm">
&lsquo;{query.trim()}&rsquo; 검색 결과가 없어요.
</p>
)}
<Button className="w-full" disabled={!canSave} onClick={handleSubmit}>
{changedRows.length > 0 ? `변경사항 ${changedRows.length}건 저장` : '변경사항 없음'}
</Button>
</div>

<Dialog open={isConfirmOpen} onOpenChange={setIsConfirmOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>투표를 반영할까요?</DialogTitle>
<DialogDescription>
저장하면 차액만큼 포인트가 차감되거나 환불돼요. 이번 달에는 언제든 다시 조정할 수
있어요.
</DialogDescription>
</DialogHeader>

<div className="flex max-h-[40vh] flex-col gap-1 overflow-y-auto">
{changedRows.map(row => (
<div key={row.artist} className="flex items-center justify-between gap-2 text-sm">
<ArtistName name={row.artist} artistKo={row.artistKo} className="flex-1" />
<span className="text-muted-foreground shrink-0 tabular-nums">
{row.savedAmount}P → {row.amount}P
</span>
</div>
))}
<ScrollArea className="min-h-0 flex-1">
<div className="flex flex-col gap-2 pr-2">
{rows.length === 0 ? (
<p className="text-muted-foreground py-8 text-center text-sm">
검색해서 투표할 아티스트를 담아보세요.
</p>
) : (
rows.map(row => (
<ArtistVoteRow
key={row.artist}
artist={row.artist}
artistKo={row.artistKo}
amount={row.amount}
savedAmount={row.savedAmount}
step={STEP}
disabled={isPending}
onChange={amount => handleChange(row.artist, amount)}
onSubmit={handleSubmit}
onDelete={() => handleDelete(row.artist)}
/>
))
)}
</div>
</ScrollArea>

<div className="flex items-center justify-between border-t pt-3 text-sm font-medium">
<span>{spending >= 0 ? '차감 포인트' : '환불 포인트'}</span>
<span className="tabular-nums">{Math.abs(spending)}P</span>
</div>
<div className="flex shrink-0 flex-col gap-2">
{isOverPoint && (
<p className="text-destructive text-sm">
포인트가 {-remainingPoint}P 부족해요. 투표값을 줄여주세요.
</p>
)}
<Button className="w-full" disabled={!canSave} onClick={handleSubmit}>
{changedRows.length > 0 ? `변경사항 ${changedRows.length}건 저장` : '변경사항 없음'}
</Button>
</div>

<Dialog open={isConfirmOpen} onOpenChange={setIsConfirmOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>투표를 반영할까요?</DialogTitle>
<DialogDescription>
저장하면 차액만큼 포인트가 차감되거나 환불돼요. 이번 달에는 언제든 다시 조정할 수
있어요.
</DialogDescription>
</DialogHeader>

<div className="flex max-h-[40vh] flex-col gap-1 overflow-y-auto">
{changedRows.map(row => (
<div key={row.artist} className="flex items-center justify-between gap-2 text-sm">
<ArtistName name={row.artist} artistKo={row.artistKo} className="flex-1" />
<span className="text-muted-foreground shrink-0 tabular-nums">
{row.savedAmount}P → {row.amount}P
</span>
</div>
))}
</div>

<div className="flex items-center justify-between border-t pt-3 text-sm font-medium">
<span>{spending >= 0 ? '차감 포인트' : '환불 포인트'}</span>
<span className="tabular-nums">{Math.abs(spending)}P</span>
</div>

<DialogFooter className="flex space-x-2">
<Button variant="outline" onClick={() => setIsConfirmOpen(false)}>
취소
</Button>
<Button onClick={handleConfirm}>확인</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>

<DialogFooter className="flex space-x-2">
<Button variant="outline" onClick={() => setIsConfirmOpen(false)}>
취소
</Button>
<Button onClick={handleConfirm}>확인</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{!isAuthenticated && (
<div className="bg-background/70 absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 rounded-lg backdrop-blur-[2px]">
<p className="text-foreground text-xl font-medium">로그인하면 참여할 수 있어요</p>
<p className="text-muted-foreground text-sm">이 달의 아티스트를 직접 뽑아주세요.</p>
</div>
)}
</div>
);
}
3 changes: 1 addition & 2 deletions apps/web/src/app/popular/ChartGenreFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useRef } from 'react';

import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { STR_TYPE_EMOJI, STR_TYPE_LABEL, StrType } from '@/types/tjChart';
import { STR_TYPE_LABEL, StrType } from '@/types/tjChart';

// 장르가 12종이라 줄바꿈 없이 한 줄로 두고 가로 스크롤로 훑게 한다.
// TabsTrigger 기본 스타일의 flex-1(균등 분할)은 flex-none으로 해제해야 칩 너비가 라벨에 맞는다.
Expand Down Expand Up @@ -57,7 +57,6 @@ export default function ChartGenreFilter({ value, onChange }: ChartGenreFilterPr
<TabsList ref={listRef} className={LIST_CLASSES}>
{Object.values(StrType).map(type => (
<TabsTrigger key={type} value={type} className={CHIP_CLASSES}>
<span aria-hidden="true">{STR_TYPE_EMOJI[type]}</span>
{STR_TYPE_LABEL[type]}
</TabsTrigger>
))}
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/app/search/SearchTour.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ export default function SearchTour({
zIndex: 10000,
showProgress: true,
buttons: TOOLTIP_BUTTONS,
// 라이브러리 기본값 'close'는 continuous 모드에서 X를 눌러도 다음 단계로 넘어갈 뿐 투어가 끝나지 않는다.
// 'skip'이어야 상태가 SKIPPED가 되어 투어가 종료되고 '다시 보지 않기'까지 저장된다.
closeButtonAction: 'skip' as const,
skipBeacon: true,
// 라이브러리 기본값(고정 380px)은 375px 이하 모바일 화면에서 좌우로 넘친다.
// 뷰포트보다 넓어지지 않되, 넓은 화면에서는 기존 380px 상한을 유지한다.
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/constants/tourDemoSong.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { SearchSong } from '@/types/song';
* 실제 검색 결과로 투어를 진행하면 곡 데이터가 바뀌거나 검색이 실패할 때 투어가 깨진다.
* 네트워크 없이 항상 같은 카드를 보여주도록 고정값을 쓴다.
*/
export const TOUR_DEMO_SEARCH_TERM = '밤이 깊었나';
export const TOUR_DEMO_SEARCH_TERM = '밤이 깊었네';

export const TOUR_DEMO_SONG: SearchSong = {
id: 'tour-demo-song',
title: '밤이 깊었나',
artist: '스판텍스',
title: '밤이 깊었네',
artist: '크라잉넛',
num_tj: '10101',
num_ky: '20202',
badges: [],
Expand Down
18 changes: 0 additions & 18 deletions apps/web/src/types/tjChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,6 @@ export const STR_TYPE_LABEL: Record<StrType, string> = {
[StrType.RnbUrban]: 'R&B/어반',
};

// 장르 뱃지 앞에 붙는 아이콘.
// Windows의 Segoe UI Emoji에 글리프가 없으면 두부(tofu)로 깨지므로,
// 국기(regional indicator)와 최신 이모지는 쓰지 않는다. 아코디언(U+1FA97)도 여기서 깨진다.
export const STR_TYPE_EMOJI: Record<StrType, string> = {
[StrType.All]: '🏆',
[StrType.Kpop]: '🎤',
[StrType.Pop]: '🌍',
[StrType.Jpop]: '🌸',
[StrType.Ballad]: '💗',
[StrType.Dance]: '💃',
[StrType.Trot]: '📻',
[StrType.Folk]: '🎸',
[StrType.Ost]: '🎬',
[StrType.RockMetal]: '🤘',
[StrType.RapHiphop]: '🔥',
[StrType.RnbUrban]: '🎷',
};

export interface TjChartRankingSong extends Song {
rank: number;
}
Expand Down