diff --git a/.gitignore b/.gitignore index 7037a53..10a301f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,6 @@ yarn-error.log* .gemini/ .cursorrules -.gitmessage.txt \ No newline at end of file +.gitmessage.txt + +temp/ diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..77f90e1 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,45 @@ +# SingCode Project Context + +## Project Overview + +SingCode is a Karaoke number search service built as a Monorepo. It aggregates song data from various sources and provides a web interface for users to search and manage songs. + +## Tech Stack (Global) + +- **Monorepo Manager:** TurboRepo +- **Package Manager:** pnpm (@9.0.0) +- **Language:** TypeScript (v5.8.2) +- **Core Framework:** React 19 +- **Engines:** Node.js >= 18 + +## Project Structure + +The project follows a standard pnpm workspace structure: + +- **`apps/web/`**: The main user-facing web application (Next.js). +- **`packages/`**: Shared libraries and configurations. + - **`crawling/`**: Scripts and logic for crawling song data (DB input). + - **`open-api/`**: Internal API module for providing karaoke numbers (Domestic songs). + - **`query/`**: Shared TanStack Query hooks and configurations. + - **`ui/`**: Shared UI components (Design System). + - **`eslint-config/`**: Shared ESLint configurations. + - **`typescript-config/`**: Shared `tsconfig` bases. + +## Development Workflow (TurboRepo) + +Use the following commands from the root directory: + +- **`pnpm dev`**: Starts the development server for all apps (runs `turbo run dev`). +- **`pnpm dev-web`**: Starts only the web application (`turbo run dev --filter=web`). +- **`pnpm build`**: Builds all apps and packages. +- **`pnpm lint`**: Runs linting across the workspace. +- **`pnpm format`**: Formats code using Prettier. +- **`pnpm check-types`**: Runs TypeScript type checking. + +## Key Conventions + +1. **Workspace Dependencies**: Packages utilize `workspace:*` to reference internal packages (e.g., `@repo/ui`). +2. **React 19**: All applications and UI packages are compatible with React 19. +3. **Strict Typing**: All code must be strictly typed via TypeScript. + +Context is in English, but please answer in Korean. diff --git a/README.md b/README.md index e131981..c990528 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ sing-code/ - 2025.6.17 : github action schedule 활용하여 매일마다 TJ 최신곡을 DB에 업데이트하는 프로세스 구축 - 2025.6.18 : 버전 1.6.0 배포. 회원탈퇴 기능 추가. mac 환경 이슈 해결. - 2025.10.26 : 버전 1.8.0 배포. 최근곡 기능 추가. 비동기 요청 포기, isPending으로 제어 +- 2026.1.4 : 버전 1.9.0 배포. OPENAI 활용 챗봇 기능 추가. +- 2026.1.27 : 버전 2.0.0 배포. DB 재설계 및 로직 리펙토링. 출석 체크, 유저 별 포인트, 곡 추천 기능 추가. ## 📝 회고 diff --git a/apps/web/GEMINI.md b/apps/web/GEMINI.md new file mode 100644 index 0000000..7919e9b --- /dev/null +++ b/apps/web/GEMINI.md @@ -0,0 +1,40 @@ +# Web Application Context (`apps/web`) + +## Overview + +This is the main Next.js web application for SingCode. It serves as the frontend client for searching songs, viewing lyrics, and user interaction. + +## Tech Stack + +- **Framework:** Next.js 15.2.7 (App Router) +- **Language:** TypeScript +- **Styling:** Tailwind CSS v4, `tailwind-merge`, `clsx`, `class-variance-authority` (CVA). +- **UI Components:** Radix UI Primitives, Lucide React (Icons). +- **State Management:** + - **Server State:** TanStack Query (`@repo/query`, v5). + - **Client Global State:** Zustand. + - **Local State:** React Hooks (`useState`, `useReducer`). +- **Backend & Auth:** Supabase (Auth, DB, SSR). +- **Animations:** GSAP, Motion (Framer Motion), Lottie, `tw-animate-css`. +- **Utilities:** `date-fns`, `immer`, `axios`. + +## Key Features & Libraries + +- **Drag & Drop:** `@dnd-kit` is used for interaction. +- **Physics Engine:** `matter-js` is used for specific visual effects. +- **AI Integration:** `openai` SDK is integrated for AI-related features. +- **Analytics:** PostHog, Vercel Analytics/Speed Insights. + +## Coding Conventions & Guidelines + +### 1. Component Structure + +- Use **Functional Components** with TypeScript interfaces for props. +- Use `shadcn/ui` patterns: Combine Radix UI primitives with Tailwind CSS. +- Use `cn()` utility (clsx + tailwind-merge) for conditional class names. + ```tsx + // Example +
...
+ ``` + +Context is in English, but please answer in Korean. diff --git a/apps/web/package.json b/apps/web/package.json index ee690ef..b7bcb60 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "1.9.1", + "version": "2.0.1", "type": "module", "private": true, "scripts": { diff --git a/apps/web/public/changelog.json b/apps/web/public/changelog.json index aa0ff78..edc5cc9 100644 --- a/apps/web/public/changelog.json +++ b/apps/web/public/changelog.json @@ -80,5 +80,12 @@ "포인트를 사용해 곡을 추천할 수 있습니다.", "인기곡 페이지에서는 추천곡 순위를 확인할 수 있습니다." ] + }, + "2.0.1": { + "title": "버전 2.0.1", + "message": [ + "로컬 스토리지 저장 기능을 개선했습니다.", + "검색 카드 디자인 및 기능을 개선했습니다." + ] } } diff --git a/apps/web/src/app/search/HomePage.tsx b/apps/web/src/app/search/HomePage.tsx index a36e1e2..25abbc1 100644 --- a/apps/web/src/app/search/HomePage.tsx +++ b/apps/web/src/app/search/HomePage.tsx @@ -9,9 +9,9 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { useSearchHistory } from '@/hooks/useSearchHistory'; import useSearchSong from '@/hooks/useSearchSong'; import { type ChatMessage } from '@/lib/api/openAIchat'; +import { useSearchHistoryStore } from '@/stores/useSearchHistoryStore'; import { SearchSong } from '@/types/song'; import { ChatResponseType } from '@/utils/safeParseJson'; @@ -57,7 +57,7 @@ export default function SearchPage() { searchSongs = searchResults.pages.flatMap(page => page.data); } - const { searchHistory, removeFromHistory } = useSearchHistory(); + const { searchHistory, removeFromHistory } = useSearchHistoryStore(); // 엔터 키 처리 const handleKeyUp = (e: React.KeyboardEvent) => { @@ -66,16 +66,6 @@ export default function SearchPage() { } }; - useEffect(() => { - const timeout = setTimeout(() => { - if (inView && hasNextPage && !isFetchingNextPage && !isError) { - fetchNextPage(); - } - }, 1000); // 1000ms 정도 지연 - - return () => clearTimeout(timeout); - }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isError]); - const handleSearchClick = () => { if (!search.trim()) { toast.error('검색어를 입력해주세요.'); @@ -100,6 +90,16 @@ export default function SearchPage() { } }; + useEffect(() => { + const timeout = setTimeout(() => { + if (inView && hasNextPage && !isFetchingNextPage && !isError) { + fetchNextPage(); + } + }, 1000); // 1000ms 정도 지연 + + return () => clearTimeout(timeout); + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isError]); + return (
@@ -157,9 +157,9 @@ export default function SearchPage() {
)}
- +
{searchSongs.length > 0 && ( -
+
{searchSongs.map((song, index) => ( handleToggleLike(song.id, song.isLike ? 'DELETE' : 'POST')} onClickSave={() => handleToggleSave(song, song.isSave ? 'PATCH' : 'POST')} + onClickArtist={() => setSearch(song.artist)} /> ))} {hasNextPage && !isFetchingNextPage && ( @@ -197,7 +198,7 @@ export default function SearchPage() {

노래 제목이나 가수를 검색해보세요

)} - +
{selectedSaveSong && ( void; onToggleLike: () => void; onClickSave: () => void; + onClickArtist: () => void; } export default function SearchResultCard({ @@ -21,6 +22,7 @@ export default function SearchResultCard({ onToggleToSing, onToggleLike, onClickSave, + onClickArtist, }: IProps) { const { id, title, artist, num_tj, num_ky, isToSing, isLike, isSave } = song; const { isAuthenticated } = useAuthStore(); @@ -36,16 +38,21 @@ export default function SearchResultCard({ }; return ( - + {/* 메인 콘텐츠 영역 */} -
+
{/* 노래 정보 */}
{/* 제목 및 가수 */} -
-
+
+

{title}

-

{artist}

+ + {artist} +
@@ -79,7 +86,7 @@ export default function SearchResultCard({
{/* 버튼 영역 - 우측 하단에 고정 */} -
+