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}
+
{/* 버튼 영역 - 우측 하단에 고정 */}
-
+