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-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>
<url><loc>https://www.singcode.kr/manifest.webmanifest</loc><lastmod>2026-08-31T07:03:47.228Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-08-31T07:03:47.229Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr</loc><lastmod>2026-08-31T07:03:47.229Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
</urlset>
20 changes: 14 additions & 6 deletions apps/web/src/app/info/like/SongItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import MarqueeText from '@/components/MarqueeText';
import { Checkbox } from '@/components/ui/checkbox';
import { AddListModalSong } from '@/types/song';
import { cn } from '@/utils/cn';
import { splitDisplay } from '@/utils/songDisplay';

// 노래 항목 컴포넌트
export default function SongItem({
Expand All @@ -13,6 +14,9 @@ export default function SongItem({
isSelected: boolean;
onToggleSelect: (id: string) => void;
}) {
const titleParts = splitDisplay(song.title_ko, song.title);
const artistParts = splitDisplay(song.artist_ko, song.artist);

return (
<div className={cn('border-border flex items-center space-x-3 border-b py-2 last:border-0')}>
<Checkbox
Expand All @@ -21,13 +25,17 @@ export default function SongItem({
onCheckedChange={() => onToggleSelect(song.song_id)}
/>
<div className="min-w-0 flex-1">
<MarqueeText className="text-sm font-medium">{song.title}</MarqueeText>
{song.title_ko && song.title_ko !== song.title && (
<MarqueeText className="text-muted-foreground text-xs">{song.title_ko}</MarqueeText>
<MarqueeText className="text-sm font-medium">{titleParts.primary}</MarqueeText>
{titleParts.secondary && (
<MarqueeText className="text-muted-foreground text-xs">
{titleParts.secondary}
</MarqueeText>
)}
<MarqueeText className="text-muted-foreground text-xs">{song.artist}</MarqueeText>
{song.artist_ko && song.artist_ko !== song.artist && (
<MarqueeText className="text-muted-foreground/70 text-xs">{song.artist_ko}</MarqueeText>
<MarqueeText className="text-muted-foreground text-xs">{artistParts.primary}</MarqueeText>
{artistParts.secondary && (
<MarqueeText className="text-muted-foreground/70 text-xs">
{artistParts.secondary}
</MarqueeText>
)}
</div>
</div>
Expand Down
21 changes: 12 additions & 9 deletions apps/web/src/app/info/promotions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useDeleteUserPromotionMutation, useUserPromotionsQuery } from '@/querie
import useAuthStore from '@/stores/useAuthStore';
import { SongPromotion } from '@/types/promotion';
import { getTodayKST } from '@/utils/kst';
import { splitDisplay } from '@/utils/songDisplay';

function PromotionItem({
promotion,
Expand All @@ -34,8 +35,8 @@ function PromotionItem({
const todayKST = getTodayKST();
const canCancel = promotion.start_date > todayKST;
const { title, artist, title_ko, artist_ko } = promotion;
const hasKoTitle = title_ko && title_ko !== title;
const hasKoArtist = artist_ko && artist_ko !== artist;
const titleParts = splitDisplay(title_ko, title);
const artistParts = splitDisplay(artist_ko, artist);

const statusLabel = (() => {
if (promotion.end_date < todayKST)
Expand Down Expand Up @@ -70,11 +71,13 @@ function PromotionItem({
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="flex flex-col gap-0.5">
<p className="truncate text-base font-medium">{title}</p>
{hasKoTitle && <p className="text-muted-foreground truncate text-xs">{title_ko}</p>}
<p className="text-muted-foreground truncate text-sm">{artist}</p>
{hasKoArtist && (
<p className="text-muted-foreground/70 truncate text-xs">{artist_ko}</p>
<p className="truncate text-base font-medium">{titleParts.primary}</p>
{titleParts.secondary && (
<p className="text-muted-foreground truncate text-xs">{titleParts.secondary}</p>
)}
<p className="text-muted-foreground truncate text-sm">{artistParts.primary}</p>
{artistParts.secondary && (
<p className="text-muted-foreground/70 truncate text-xs">{artistParts.secondary}</p>
)}
</div>
<p className="bg-muted/50 text-foreground mt-2 rounded-md px-3 py-2 text-sm leading-relaxed whitespace-pre-line">
Expand Down Expand Up @@ -175,10 +178,10 @@ export default function MyPromotionsPage() {
{confirmTarget && (
<div className="space-y-2 py-2 text-sm">
<p className="font-medium">
{confirmTarget.title_ko ?? confirmTarget.title}
{splitDisplay(confirmTarget.title_ko, confirmTarget.title).primary}
<span className="text-muted-foreground font-normal">
{' · '}
{confirmTarget.artist_ko ?? confirmTarget.artist}
{splitDisplay(confirmTarget.artist_ko, confirmTarget.artist).primary}
</span>
</p>
<p className="text-muted-foreground">
Expand Down
58 changes: 34 additions & 24 deletions apps/web/src/app/info/save/FolderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Separator } from '@/components/ui/separator';
import { SaveSongFolder } from '@/types/song';
import { splitDisplay } from '@/utils/songDisplay';

interface IProps {
folder: SaveSongFolder;
Expand Down Expand Up @@ -96,33 +97,42 @@ export default function FolderCard({
<div className="px-4">
{folder.songList.length > 0 ? (
<div className="space-y-2">
{folder.songList.map(song => (
<div
key={song.song_id}
className="flex items-center gap-3 border-b py-2 last:border-0"
>
<Checkbox
id={`song-${song.song_id}`}
checked={!!selectedSongs[song.song_id]}
onCheckedChange={() => toggleSongSelection(song.song_id)}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Music className="text-muted-foreground h-4 w-4 shrink-0" />
<div>
<p className="text-sm font-medium">{song.title}</p>
{song.title_ko && song.title_ko !== song.title && (
<p className="text-muted-foreground text-xs">{song.title_ko}</p>
)}
<p className="text-muted-foreground text-xs">{song.artist}</p>
{song.artist_ko && song.artist_ko !== song.artist && (
<p className="text-muted-foreground/70 text-xs">{song.artist_ko}</p>
)}
{folder.songList.map(song => {
const titleParts = splitDisplay(song.title_ko, song.title);
const artistParts = splitDisplay(song.artist_ko, song.artist);

return (
<div
key={song.song_id}
className="flex items-center gap-3 border-b py-2 last:border-0"
>
<Checkbox
id={`song-${song.song_id}`}
checked={!!selectedSongs[song.song_id]}
onCheckedChange={() => toggleSongSelection(song.song_id)}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Music className="text-muted-foreground h-4 w-4 shrink-0" />
<div>
<p className="text-sm font-medium">{titleParts.primary}</p>
{titleParts.secondary && (
<p className="text-muted-foreground text-xs">
{titleParts.secondary}
</p>
)}
<p className="text-muted-foreground text-xs">{artistParts.primary}</p>
{artistParts.secondary && (
<p className="text-muted-foreground/70 text-xs">
{artistParts.secondary}
</p>
)}
</div>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
) : (
<div className="text-muted-foreground py-4 text-center">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/popular/ArtistVotePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export default function ArtistVotePanel() {
</div>

{!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]">
<div className="bg-background/10 absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 rounded-lg backdrop-blur-[1px]">
<p className="text-foreground text-xl font-medium">로그인하면 참여할 수 있어요</p>
<p className="text-muted-foreground text-sm">이 달의 아티스트를 직접 뽑아주세요.</p>
</div>
Expand Down
19 changes: 13 additions & 6 deletions apps/web/src/app/search/AddFolderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@/components/ui/select';
import { useSaveSongFolderQuery } from '@/queries/saveSongFolderQuery';
import { SaveSongFolderList, SearchSong } from '@/types/song';
import { splitDisplay } from '@/utils/songDisplay';

interface IProps {
modalType: '' | 'POST' | 'PATCH';
Expand All @@ -47,6 +48,8 @@ export default function AddFolderModal({
const [isExistingPlaylist, setIsExistingPlaylist] = useState(false);

const { id: songId, title, artist, title_ko, artist_ko } = song;
const titleParts = splitDisplay(title_ko, title);
const artistParts = splitDisplay(artist_ko, artist);

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

Expand Down Expand Up @@ -113,13 +116,17 @@ export default function AddFolderModal({

{/* 곡 정보 */}
<div className="bg-muted mb-4 rounded-md p-3">
<MarqueeText className="text-base font-medium">{title}</MarqueeText>
{title_ko && title_ko !== title && (
<MarqueeText className="text-muted-foreground text-xs">{title_ko}</MarqueeText>
<MarqueeText className="text-base font-medium">{titleParts.primary}</MarqueeText>
{titleParts.secondary && (
<MarqueeText className="text-muted-foreground text-xs">
{titleParts.secondary}
</MarqueeText>
)}
<MarqueeText className="text-muted-foreground text-sm">{artist}</MarqueeText>
{artist_ko && artist_ko !== artist && (
<MarqueeText className="text-muted-foreground/70 text-xs">{artist_ko}</MarqueeText>
<MarqueeText className="text-muted-foreground text-sm">{artistParts.primary}</MarqueeText>
{artistParts.secondary && (
<MarqueeText className="text-muted-foreground/70 text-xs">
{artistParts.secondary}
</MarqueeText>
)}
</div>

Expand Down
18 changes: 1 addition & 17 deletions apps/web/src/app/search/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import { useInView } from 'react-intersection-observer';
import { toast } from 'sonner';

import { Button } from '@/components/ui/button';
// import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
// import { Label } from '@/components/ui/label';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { TOUR_DEMO_SEARCH_TERM, TOUR_DEMO_SONG } from '@/constants/tourDemoSong';
import useSaveSongModal from '@/hooks/useSaveSongModal';
Expand All @@ -19,7 +17,6 @@ import { SearchSong, SearchType } from '@/types/song';
import { cn } from '@/utils/cn';

import AddFolderModal from './AddFolderModal';
// import ChatBot from './ChatBot';
import JpnArtistList from './JpnArtistList';
import NumberKeypad from './NumberKeypad';
import PopularSearchHistory from './PopularSearchHistory';
Expand Down Expand Up @@ -65,11 +62,6 @@ export default function SearchPage() {
const [isJpnArtistModalOpen, setIsJpnArtistModalOpen] = useState(false);
const [isFocusAuto, setIsFocusAuto] = useState(false);
const [isNumberKeypadOpen, setIsNumberKeypadOpen] = useState(false);
// const [isChatBotEnabled, setIsChatBotEnabled] = useState(() => {
// if (typeof window === 'undefined') return true;
// const stored = localStorage.getItem('chatbot-enabled');
// return stored === null ? true : stored === 'true';
// });

const [scrollRef, setScrollRef] = useState<HTMLDivElement | null>(null);
const { ref, inView } = useInView({
Expand All @@ -79,11 +71,6 @@ export default function SearchPage() {

const { guestToSingSongs } = useGuestToSingStore();

// const handleToggleChatBot = (checked: boolean) => {
// setIsChatBotEnabled(checked);
// localStorage.setItem('chatbot-enabled', String(checked));
// };

const guestToSingIds = useMemo(
() => new Set(guestToSingSongs?.map(item => item.songs.id)),
[guestToSingSongs],
Expand Down Expand Up @@ -398,7 +385,7 @@ export default function SearchPage() {
<div className="flex h-full flex-col gap-2">
<div className="text-muted-foreground flex items-center gap-2">
<Info className="h-4 w-4" />
<span className="m-2">전체 문장보다는 단어 단위로 검색해보세요</span>
<span className="m-2">단어 단위로 검색해주세요</span>
</div>

<div className="flex h-full flex-col justify-center gap-2">
Expand All @@ -419,9 +406,6 @@ export default function SearchPage() {
/>
)}

{/* 챗봇 위젯 */}
{/* {isChatBotEnabled && <ChatBot setInputSearch={setSearch} />} */}

<SearchTour
onPrepareExampleSearch={handlePrepareExampleSearch}
onTourNormalizeState={handleTourNormalizeState}
Expand Down
19 changes: 9 additions & 10 deletions apps/web/src/app/search/SearchResultCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Card } from '@/components/ui/card';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import useAuthStore from '@/stores/useAuthStore';
import { SearchSong } from '@/types/song';
import { splitDisplay } from '@/utils/songDisplay';

import SongActionButtons from './SongActionButtons';

Expand Down Expand Up @@ -41,10 +42,8 @@ function SearchResultCard({
onClickSave,
}: SearchResultCardProps) {
const { id, title, artist, title_ko, artist_ko, num_tj, num_ky, badges } = song;
const hasKoTitle = !!title_ko && title_ko !== title;
const hasKoArtist = !!artist_ko && artist_ko !== artist;
const displayTitle = hasKoTitle ? title_ko : title;
const displayArtist = hasKoArtist ? artist_ko : artist;
const titleParts = splitDisplay(title_ko, title);
const artistParts = splitDisplay(artist_ko, artist);

const { isAuthenticated } = useAuthStore();

Expand Down Expand Up @@ -89,11 +88,11 @@ function SearchResultCard({
<SongBadges badges={badges} className="mb-0.5" />
<MarqueeText
className="hover:text-accent cursor-pointer text-base font-medium hover:underline hover:underline-offset-4"
onClick={() => handleCopy(displayTitle)}
onClick={() => handleCopy(titleParts.primary)}
>
{displayTitle}
{titleParts.primary}
</MarqueeText>
{hasKoTitle && (
{titleParts.secondary && (
<MarqueeText
className="text-muted-foreground hover:text-accent cursor-pointer text-xs hover:underline hover:underline-offset-4"
onClick={() => handleCopy(title)}
Expand All @@ -103,11 +102,11 @@ function SearchResultCard({
)}
<MarqueeText
className="text-muted-foreground hover:text-accent mt-0.5 cursor-pointer text-sm hover:underline hover:underline-offset-4"
onClick={() => handleCopy(displayArtist)}
onClick={() => handleCopy(artistParts.primary)}
>
{displayArtist}
{artistParts.primary}
</MarqueeText>
{hasKoArtist && (
{artistParts.secondary && (
<MarqueeText
className="text-muted-foreground/70 hover:text-accent cursor-pointer text-xs hover:underline hover:underline-offset-4"
onClick={() => handleCopy(artist)}
Expand Down
20 changes: 14 additions & 6 deletions apps/web/src/app/tosing/ModalSongItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import MarqueeText from '@/components/MarqueeText';
import { Checkbox } from '@/components/ui/checkbox';
import { AddListModalSong } from '@/types/song';
import { cn } from '@/utils/cn';
import { splitDisplay } from '@/utils/songDisplay';

// 노래 항목 컴포넌트
export default function ModalSongItem({
Expand All @@ -13,6 +14,9 @@ export default function ModalSongItem({
isSelected: boolean;
onToggleSelect: (id: string) => void;
}) {
const titleParts = splitDisplay(song.title_ko, song.title);
const artistParts = splitDisplay(song.artist_ko, song.artist);

return (
<div
className={cn(
Expand All @@ -27,13 +31,17 @@ export default function ModalSongItem({
disabled={song.isInToSingList}
/>
<div className="min-w-0 flex-1">
<MarqueeText className="text-sm font-medium">{song.title}</MarqueeText>
{song.title_ko && song.title_ko !== song.title && (
<MarqueeText className="text-muted-foreground text-xs">{song.title_ko}</MarqueeText>
<MarqueeText className="text-sm font-medium">{titleParts.primary}</MarqueeText>
{titleParts.secondary && (
<MarqueeText className="text-muted-foreground text-xs">
{titleParts.secondary}
</MarqueeText>
)}
<MarqueeText className="text-muted-foreground text-xs">{song.artist}</MarqueeText>
{song.artist_ko && song.artist_ko !== song.artist && (
<MarqueeText className="text-muted-foreground/70 text-xs">{song.artist_ko}</MarqueeText>
<MarqueeText className="text-muted-foreground text-xs">{artistParts.primary}</MarqueeText>
{artistParts.secondary && (
<MarqueeText className="text-muted-foreground/70 text-xs">
{artistParts.secondary}
</MarqueeText>
)}
</div>
</div>
Expand Down
Loading