From 51685f1e5d9ca6734c2c07b7b9b644aacd5638b5 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 6 Mar 2026 00:26:19 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat=20:=20claude=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 74 +++++++++++++++++++++++ apps/web/CLAUDE.md | 145 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 CLAUDE.md create mode 100644 apps/web/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e7fe44f8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +**Singcode** is a Korean karaoke song management service (singcode.kr). This is a pnpm workspace monorepo managed with Turborepo. + +## Commands + +Run from the **repo root** to target all workspaces: + +```bash +pnpm dev # Start all dev servers via Turbo +pnpm dev-web # Start only the web app dev server +pnpm build # Build all packages +pnpm lint # Lint all packages +pnpm format # Prettier format all packages +pnpm check-types # TypeScript type-check all packages +``` + +Run from **`apps/web/`** for web-only work: + +```bash +pnpm dev # Next.js dev server with Turbopack (http://localhost:3000) +pnpm build # Production build + next-sitemap postbuild +pnpm lint # ESLint +pnpm format # Prettier format .ts, .tsx, .md +``` + +No test suite is configured. + +## Monorepo Structure + +``` +apps/ + web/ — Next.js 15 web app (primary app, see apps/web/CLAUDE.md) + mobile/ — Expo React Native app (early stage) +packages/ + open-api/ — Wrapper around the external karaoke open API (@repo/open-api) + query/ — Shared TanStack Query hooks for open-api (@repo/query) + api/ — Internal API utilities (@repo/api), built with tsup + ui/ — Shared UI components (@repo/ui) + eslint-config/ — Shared ESLint config (@repo/eslint-config) + format-config/ — Shared Prettier config (@repo/format-config) + typescript-config/ — Shared tsconfig bases + crawling/ — One-off data crawling scripts (not a published package) +``` + +## Web App Architecture + +See [apps/web/CLAUDE.md](apps/web/CLAUDE.md) for full detail. Key points: + +- **Next.js 15 App Router** + React 19, deployed on Vercel +- **BFF pattern**: client → internal API routes (`/api/*`) → Supabase / external karaoke API. Never call Supabase or external APIs directly from the browser. +- **Supabase** (`@supabase/ssr`) for auth and database; three client variants (browser, server/route handler, middleware) +- **TanStack Query** for server state; **Zustand** for client state +- **Tailwind CSS v4** + **shadcn/ui** in `src/components/ui/` (do not modify directly) +- Path alias `@/` → `src/` + +## Git Conventions + +Branch format: `/` — flow: `feat/*` → `develop` → `main` + +Commit format: ` : ` (space before and after colon) + +Types: `feat`, `fix`, `hotfix`, `chore`, `refactor`, `doc` + +Examples: +``` +feat : MarqueeText 자동 스크롤 텍스트 적용 +fix : SongCard css 수정 +chore : 버전 2.3.0 +``` diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md new file mode 100644 index 00000000..e39ab9db --- /dev/null +++ b/apps/web/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Singcode** is a Korean karaoke song management web app (singcode.kr). Users can search for songs by title/singer/number, save songs to folders, track liked songs, and manage their "to-sing" list. The app uses Supabase for auth and database, and an external karaoke open API via the `@repo/open-api` workspace package. + +## Commands + +All commands run from `apps/web/`: + +```bash +pnpm dev # Start dev server with Turbopack (http://localhost:3000) +pnpm build # Production build (also runs next-sitemap postbuild) +pnpm lint # ESLint +pnpm format # Prettier format all .ts, .tsx, .md files +pnpm start # Start production server +``` + +No test suite is configured. + +## Architecture + +### Monorepo Structure + +This is a pnpm workspace monorepo. The web app lives at `apps/web/`. Key workspace packages: +- `@repo/open-api` — wrapper around the external karaoke open API (used in API routes) +- `@repo/query` — shared TanStack Query setup +- `@repo/eslint-config`, `@repo/format-config` — shared tooling configs + +### Tech Stack + +- **Next.js 15** (App Router) with React 19 +- **Supabase** (`@supabase/ssr`) for auth and database +- **TanStack Query** for server state +- **Zustand** for client state +- **Tailwind CSS v4** + **shadcn/ui** components in `src/components/ui/` +- **Axios** with a base `/api` instance in `src/lib/api/client.ts` +- **Framer Motion / GSAP** for animations + +### Data Flow Pattern + +1. **API routes** (`src/app/api/`) act as a BFF layer — client code calls these via Axios, never directly to Supabase or external APIs from the browser. +2. **API route handlers** use `src/lib/supabase/server.ts` (for Next.js route handlers) to get an authenticated Supabase client, then call `src/utils/getAuthenticatedUser.ts` to extract `userId`. +3. **Client-side API functions** live in `src/lib/api/` (e.g., `likeSong.ts`, `saveSong.ts`). They call internal Next.js API routes via the Axios instance. +4. **TanStack Query hooks** live in `src/queries/` — they wrap the `src/lib/api/` functions. + +### Supabase Client Variants + +Three different Supabase clients for different contexts: +- `src/lib/supabase/client.ts` — browser client (`createBrowserClient`), uses `NEXT_PUBLIC_` env vars +- `src/lib/supabase/server.ts` — server/route handler client (`createServerClient`) +- `src/lib/supabase/api.ts` — legacy API routes client (Next.js Pages-style `req/res`) +- `src/lib/supabase/middleware.ts` — middleware client (`updateSession`) + +### Auth + +`src/auth.tsx` is a client-side `AuthProvider` wrapping the app. It checks auth on every route change via `useAuthStore`. Public routes (no auth required): `/`, `/popular`, `/login`, `/signup`, `/recent`, `/tosing`, `/update-password`. All other routes redirect to `/login?alert=login`. + +### State Management (Zustand) + +- `src/stores/useModalStore.ts` — global message dialog state (use `openMessage`/`closeMessage`) +- `src/stores/useSearchHistoryStore.ts` — search history persisted to `localStorage` +- `src/stores/middleware.ts` — shared Zustand middleware +- Auth state is in `useAuthStore` (referenced but not shown above) + +### Pages / Routes + +- `/` → Song search (main feature, `src/app/search/HomePage.tsx`) +- `/popular` → Popular songs +- `/recent` → Recently added songs +- `/tosing` → User's "to-sing" list +- `/info` → User profile/info +- `/info/like` → Liked songs +- `/info/save` → Saved song folders (supports drag-and-drop via `@dnd-kit`) +- `/login`, `/signup`, `/update-password`, `/withdrawal` → Auth flows + +### External Song Search API + +Song searches go through `GET /api/open_songs/[type]/[param]` which proxies to `@repo/open-api`. Search types: `title`, `singer`, `composer`, `lyricist`, `no`, `release`, `popular`. The `brand` query param selects the karaoke brand. + +### AI Chat + +`POST /api/chat` proxies to OpenAI (using the `openai` package). Client-side in `src/lib/api/openAIchat.ts`, UI in `src/app/search/ChatBot.tsx`. + +## Environment Variables + +Required in `.env` / `.env.development.local`: +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- `SUPABASE_URL` (server-only, used in legacy API client) +- `SUPABASE_ANON_KEY` (server-only) +- OpenAI key for chat feature + +## Git Conventions + +### Branch Naming + +Format: `/` + +| type | usage | +|------|-------| +| `feat` | new feature | +| `fix` | bug fix | +| `hotfix` | urgent fix | +| `chore` | maintenance, docs, config | +| `release` | release (e.g. `release/2.1.0`) | + +The part after the slash uses camelCase (e.g. `feat/scrollText`, `feat/FooterNavbar`, `fix/loginAuth`). + +Branch flow: `feat/*` → `develop` → `main` + +### Commit Messages + +Format: ` : ` — one space before and after the colon. + +| type | usage | +|------|-------| +| `feat` | new feature | +| `fix` | bug fix | +| `hotfix` | urgent bug fix | +| `chore` | version bump, config, format, cleanup | +| `refactor` | refactoring | +| `doc` | documentation | + +Examples: +``` +feat : MarqueeText 자동 스크롤 텍스트 적용 +fix : SongCard css 수정 +hotfix : 빌드 에러 수정. thumb 필드 옵셔널 타입 조정. +chore : 버전 2.3.0 +refactor : useSearchSong 훅 분리 - 곡 모달 저장 분리 +``` + +## Key Conventions + +- Path alias `@/` maps to `src/` +- `src/utils/cn.ts` — `clsx` + `tailwind-merge` utility for className merging +- `src/utils/isSuccessResponse.ts` — type guard for API responses +- `src/utils/getErrorMessage.ts` — standardized error message extraction +- shadcn/ui components are in `src/components/ui/` and should not be modified directly +- `src/components/reactBits/` — custom animation components (AnimatedContent, SplitText, GradientText, Shuffle) +- Global toast notifications use `sonner` (`` in layout) +- Global modal dialog uses `useModalStore` + `` in layout From 2d8e25aaeff39662614a7e9272a1a1e2b68ac3f3 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 6 Mar 2026 00:50:31 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat=20:=20ChatBot=20=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94/=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EB=B0=95=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?localStorage=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/search/HomePage.tsx | 43 ++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/search/HomePage.tsx b/apps/web/src/app/search/HomePage.tsx index bf930c83..73585688 100644 --- a/apps/web/src/app/search/HomePage.tsx +++ b/apps/web/src/app/search/HomePage.tsx @@ -6,7 +6,9 @@ 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 useSaveSongModal from '@/hooks/useSaveSongModal'; import useSearchSong from '@/hooks/useSearchSong'; @@ -55,6 +57,11 @@ export default function SearchPage() { const [isJpnArtistModalOpen, setIsJpnArtistModalOpen] = useState(false); const [isFocusAuto, setIsFocusAuto] = 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(null); const { ref, inView } = useInView({ @@ -64,6 +71,11 @@ export default function SearchPage() { const { guestToSingSongs } = useGuestToSingStore(); + const handleToggleChatBot = (checked: boolean) => { + setIsChatBotEnabled(checked); + localStorage.setItem('chatbot-enabled', String(checked)); + }; + const isToSing = (song: SearchSong, songId: string) => { if (!isAuthenticated) { return guestToSingSongs?.some(item => item.songs.id === songId); @@ -138,12 +150,27 @@ export default function SearchPage() { )} - handleSearchTypeChange('artist')} - /> +
+ handleSearchTypeChange('artist')} + /> +
+ + +
+
@@ -182,7 +209,7 @@ export default function SearchPage() { {/* 검색 기록 */} -
+
{searchSongs.length > 0 && (
{searchSongs.map((song, index) => ( @@ -239,7 +266,7 @@ export default function SearchPage() { )} {/* 챗봇 위젯 */} - + {isChatBotEnabled && }
); } From c322686f1954c5dd3095c12b679f27f7ea47bd31 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 6 Mar 2026 00:51:24 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat=20:=20ThumbUpModal=20=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=9E=85=EB=A0=A5=20Input=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/components/ThumbUpModal.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/web/src/components/ThumbUpModal.tsx b/apps/web/src/components/ThumbUpModal.tsx index 300625a1..21e9d2a6 100644 --- a/apps/web/src/components/ThumbUpModal.tsx +++ b/apps/web/src/components/ThumbUpModal.tsx @@ -7,6 +7,7 @@ import GradientText from '@/components/reactBits/GradientText'; import SplitText from '@/components/reactBits/SplitText'; import { Button } from '@/components/ui/button'; import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; import { Slider } from '@/components/ui/slider'; import { useSongThumbMutation } from '@/queries/songThumbQuery'; import { useUserQuery } from '@/queries/userQuery'; @@ -93,6 +94,23 @@ export default function ThumbUpModal({ className="z-50 cursor-pointer" /> +
+ { + const parsed = Number(e.target.value); + if (isNaN(parsed)) return; + const clamped = Math.min(Math.max(parsed, 0), point); + setValue([clamped]); + }} + className="text-center text-lg font-bold" + /> + P +
+
From 143a791700bf0bd25f7d7509f47e0c32bdbaaaed Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Fri, 6 Mar 2026 00:51:29 +0900 Subject: [PATCH 04/11] =?UTF-8?q?fix=20:=20MarqueeText=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EB=A3=A8=ED=94=84=20?= =?UTF-8?q?=EB=8D=9C=EC=BB=A5=EA=B1=B0=EB=A6=BC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/components/MarqueeText.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/MarqueeText.tsx b/apps/web/src/components/MarqueeText.tsx index a8abb1f1..f48e2775 100644 --- a/apps/web/src/components/MarqueeText.tsx +++ b/apps/web/src/components/MarqueeText.tsx @@ -13,6 +13,7 @@ interface MarqueeTextProps { export default function MarqueeText({ children, className, onClick }: MarqueeTextProps) { const containerRef = useRef(null); const [isOverflowing, setIsOverflowing] = useState(false); + const [isAnimationPaused, setIsAnimationPaused] = useState(false); const checkOverflow = () => { if (containerRef.current) { @@ -32,18 +33,22 @@ export default function MarqueeText({ children, className, onClick }: MarqueeTex
{ + if (isOverflowing) setIsAnimationPaused(p => !p); + onClick?.(); + }} >
{children} {/* 오버플로우 시에만 복제본을 렌더링 */} {isOverflowing && ( -