diff --git a/.claude b/.claude new file mode 160000 index 0000000..ddf11a3 --- /dev/null +++ b/.claude @@ -0,0 +1 @@ +Subproject commit ddf11a34070a8b986069833175fd72eb86f8a565 diff --git a/.cursor/.gitignore b/.cursor/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.cursor/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/.cursor/PRD.md b/.cursor/PRD.md deleted file mode 100644 index 2e839cb..0000000 --- a/.cursor/PRD.md +++ /dev/null @@ -1 +0,0 @@ -# Product Requirements Document diff --git a/.cursor/README.md b/.cursor/README.md new file mode 100644 index 0000000..856cb92 --- /dev/null +++ b/.cursor/README.md @@ -0,0 +1,10 @@ +# .cursor + +## Supabase + +- https://supabase.com/docs/guides/getting-started/ai-prompts +- https://supabase.com/ui/docs/ai-editors-rules/prompts + +## TaskMaster AI + +- https://github.com/eyaltoledano/claude-task-master/tree/main/.cursor diff --git a/.cursor/TODO.md b/.cursor/TODO.md deleted file mode 100644 index 1addff6..0000000 --- a/.cursor/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -# TODO - -- [ ] diff --git a/.cursor/rules/common/cursor-rules.mdc b/.cursor/rules/common/cursor-rules.mdc new file mode 100644 index 0000000..7dfae3d --- /dev/null +++ b/.cursor/rules/common/cursor-rules.mdc @@ -0,0 +1,53 @@ +--- +description: Guidelines for creating and maintaining Cursor rules to ensure consistency and effectiveness. +globs: .cursor/rules/*.mdc +alwaysApply: true +--- + +- **Required Rule Structure:** + ```markdown + --- + description: Clear, one-line description of what the rule enforces + globs: path/to/files/*.ext, other/path/**/* + alwaysApply: boolean + --- + + - **Main Points in Bold** + - Sub-points with details + - Examples and explanations + ``` + +- **File References:** + - Use `[filename](mdc:path/to/file)` ([filename](mdc:filename)) to reference files + - Example: [prisma.mdc](mdc:.cursor/rules/prisma.mdc) for rule references + - Example: [schema.prisma](mdc:prisma/schema.prisma) for code references + +- **Code Examples:** + - Use language-specific code blocks + ```typescript + // ✅ DO: Show good examples + const goodExample = true; + + // ❌ DON'T: Show anti-patterns + const badExample = false; + ``` + +- **Rule Content Guidelines:** + - Start with high-level overview + - Include specific, actionable requirements + - Show examples of correct implementation + - Reference existing code when possible + - Keep rules DRY by referencing other rules + +- **Rule Maintenance:** + - Update rules when new patterns emerge + - Add examples from actual codebase + - Remove outdated patterns + - Cross-reference related rules + +- **Best Practices:** + - Use bullet points for clarity + - Keep descriptions concise + - Include both DO and DON'T examples + - Reference actual code over theoretical examples + - Use consistent formatting across rules \ No newline at end of file diff --git a/.cursor/rules/common/gemini-cli.mdc b/.cursor/rules/common/gemini-cli.mdc new file mode 100644 index 0000000..f2a3351 --- /dev/null +++ b/.cursor/rules/common/gemini-cli.mdc @@ -0,0 +1,347 @@ +--- +description: When analyzing large codebases or multiple files that might exceed context limits, use the Gemini CLI with its massive context window. Use `gemini -p` to leverage Google Gemini's large context capacity. +alwaysApply: false +--- + +## File and Directory Inclusion Syntax + +Use the `@` syntax to include files and directories in your Gemini prompts. The paths should be relative to WHERE you run the +gemini command: + +### Examples: + +**Single file analysis:** + +````bash +gemini -p "@src/main.py Explain this file's purpose and structure" + +Multiple files: +gemini -p "@package.json @src/index.js Analyze the dependencies used in the code" + +Entire directory: +gemini -p "@src/ Summarize the architecture of this codebase" + +Multiple directories: +gemini -p "@src/ @tests/ Analyze test coverage for the source code" + +Current directory and subdirectories: +gemini -p "@./ Give me an overview of this entire project" + +# +Or use --all_files flag: +gemini --all_files -p "Analyze the project structure and dependencies" + +Implementation Verification Examples + +Check if a feature is implemented: +gemini -p "@src/ @lib/ Has dark mode been implemented in this codebase? Show me the relevant files and functions" + +Verify authentication implementation: +gemini -p "@src/ @middleware/ Is JWT authentication implemented? List all auth-related endpoints and middleware" + +Check for specific patterns: +gemini -p "@src/ Are there any React hooks that handle WebSocket connections? List them with file paths" + +Verify error handling: +gemini -p "@src/ @api/ Is proper error handling implemented for all API endpoints? Show examples of try-catch blocks" + +Check for rate limiting: +gemini -p "@backend/ @middleware/ Is rate limiting implemented for the API? Show the implementation details" + +Verify caching strategy: +gemini -p "@src/ @lib/ @services/ Is Redis caching implemented? List all cache-related functions and their usage" + +Check for specific security measures: +gemini -p "@src/ @api/ Are SQL injection protections implemented? Show how user inputs are sanitized" + +Verify test coverage for features: +gemini -p "@src/payment/ @tests/ Is the payment processing module fully tested? List all test cases" + +When to Use Gemini CLI + +Use gemini -p when: +- Analyzing entire codebases or large directories +- Comparing multiple large files +- Need to understand project-wide patterns or architecture +- Current context window is insufficient for the task +- Working with files totaling more than 100KB +- Verifying if specific features, patterns, or security measures are implemented +- Checking for the presence of certain coding patterns across the entire codebase + +Important Notes + +- Paths in @ syntax are relative to your current working directory when invoking gemini +- The CLI will include file contents directly in the context +- No need for --yolo flag for read-only analysis +- Gemini's context window can handle entire codebases that would overflow Claude's context +- When checking implementations, be specific about what you're looking for to get accurate results # Using Gemini CLI for Large Codebase Analysis + + +When analyzing large codebases or multiple files that might exceed context limits, use the Gemini CLI with its massive +context window. Use `gemini -p` to leverage Google Gemini's large context capacity. + + +## File and Directory Inclusion Syntax + + +Use the `@` syntax to include files and directories in your Gemini prompts. The paths should be relative to WHERE you run the + gemini command: + + +### Examples: + + +**Single file analysis:** +```bash +gemini -p "@src/main.py Explain this file's purpose and structure" + + +Multiple files: +gemini -p "@package.json @src/index.js Analyze the dependencies used in the code" + + +Entire directory: +gemini -p "@src/ Summarize the architecture of this codebase" + + +Multiple directories: +gemini -p "@src/ @tests/ Analyze test coverage for the source code" + + +Current directory and subdirectories: +gemini -p "@./ Give me an overview of this entire project" +# Or use --all_files flag: +gemini --all_files -p "Analyze the project structure and dependencies" + + +Implementation Verification Examples + + +Check if a feature is implemented: +gemini -p "@src/ @lib/ Has dark mode been implemented in this codebase? Show me the relevant files and functions" + + +Verify authentication implementation: +gemini -p "@src/ @middleware/ Is JWT authentication implemented? List all auth-related endpoints and middleware" + + +Check for specific patterns: +gemini -p "@src/ Are there any React hooks that handle WebSocket connections? List them with file paths" + + +Verify error handling: +gemini -p "@src/ @api/ Is proper error handling implemented for all API endpoints? Show examples of try-catch blocks" + + +Check for rate limiting: +gemini -p "@backend/ @middleware/ Is rate limiting implemented for the API? Show the implementation details" + + +Verify caching strategy: +gemini -p "@src/ @lib/ @services/ Is Redis caching implemented? List all cache-related functions and their usage" + + +Check for specific security measures: +gemini -p "@src/ @api/ Are SQL injection protections implemented? Show how user inputs are sanitized" + + +Verify test coverage for features: +gemini -p "@src/payment/ @tests/ Is the payment processing module fully tested? List all test cases" + + +When to Use Gemini CLI + + +Use gemini -p when: +- Analyzing entire codebases or large directories +- Comparing multiple large files +- Need to understand project-wide patterns or architecture +- Current context window is insufficient for the task +- Working with files totaling more than 100KB +- Verifying if specific features, patterns, or security measures are implemented +- Checking for the presence of certain coding patterns across the entire codebase + + +Important Notes + + +- Paths in @ syntax are relative to your current working directory when invoking gemini +- The CLI will include file contents directly in the context +- No need for --yolo flag for read-only analysis +- Gemini's context window can handle entire codebases that would overflow Claude's context +- When checking implementations, be specific about what you're looking for to get accurate results +```` + +# Using Gemini CLI for Large Codebase Analysis + +When analyzing large codebases or multiple files that might exceed context limits, use the Gemini CLI with its massive +context window. Use `gemini -p` to leverage Google Gemini's large context capacity. + +## File and Directory Inclusion Syntax + +Use the `@` syntax to include files and directories in your Gemini prompts. The paths should be relative to WHERE you run the +gemini command: + +### Examples: + +**Single file analysis:** + +````bash +gemini -p "@src/main.py Explain this file's purpose and structure" + +Multiple files: +gemini -p "@package.json @src/index.js Analyze the dependencies used in the code" + +Entire directory: +gemini -p "@src/ Summarize the architecture of this codebase" + +Multiple directories: +gemini -p "@src/ @tests/ Analyze test coverage for the source code" + +Current directory and subdirectories: +gemini -p "@./ Give me an overview of this entire project" + +# +Or use --all_files flag: +gemini --all_files -p "Analyze the project structure and dependencies" + +Implementation Verification Examples + +Check if a feature is implemented: +gemini -p "@src/ @lib/ Has dark mode been implemented in this codebase? Show me the relevant files and functions" + +Verify authentication implementation: +gemini -p "@src/ @middleware/ Is JWT authentication implemented? List all auth-related endpoints and middleware" + +Check for specific patterns: +gemini -p "@src/ Are there any React hooks that handle WebSocket connections? List them with file paths" + +Verify error handling: +gemini -p "@src/ @api/ Is proper error handling implemented for all API endpoints? Show examples of try-catch blocks" + +Check for rate limiting: +gemini -p "@backend/ @middleware/ Is rate limiting implemented for the API? Show the implementation details" + +Verify caching strategy: +gemini -p "@src/ @lib/ @services/ Is Redis caching implemented? List all cache-related functions and their usage" + +Check for specific security measures: +gemini -p "@src/ @api/ Are SQL injection protections implemented? Show how user inputs are sanitized" + +Verify test coverage for features: +gemini -p "@src/payment/ @tests/ Is the payment processing module fully tested? List all test cases" + +When to Use Gemini CLI + +Use gemini -p when: +- Analyzing entire codebases or large directories +- Comparing multiple large files +- Need to understand project-wide patterns or architecture +- Current context window is insufficient for the task +- Working with files totaling more than 100KB +- Verifying if specific features, patterns, or security measures are implemented +- Checking for the presence of certain coding patterns across the entire codebase + +Important Notes + +- Paths in @ syntax are relative to your current working directory when invoking gemini +- The CLI will include file contents directly in the context +- No need for --yolo flag for read-only analysis +- Gemini's context window can handle entire codebases that would overflow Claude's context +- When checking implementations, be specific about what you're looking for to get accurate results # Using Gemini CLI for Large Codebase Analysis + + +When analyzing large codebases or multiple files that might exceed context limits, use the Gemini CLI with its massive +context window. Use `gemini -p` to leverage Google Gemini's large context capacity. + + +## File and Directory Inclusion Syntax + + +Use the `@` syntax to include files and directories in your Gemini prompts. The paths should be relative to WHERE you run the + gemini command: + + +### Examples: + + +**Single file analysis:** +```bash +gemini -p "@src/main.py Explain this file's purpose and structure" + + +Multiple files: +gemini -p "@package.json @src/index.js Analyze the dependencies used in the code" + + +Entire directory: +gemini -p "@src/ Summarize the architecture of this codebase" + + +Multiple directories: +gemini -p "@src/ @tests/ Analyze test coverage for the source code" + + +Current directory and subdirectories: +gemini -p "@./ Give me an overview of this entire project" +# Or use --all_files flag: +gemini --all_files -p "Analyze the project structure and dependencies" + + +Implementation Verification Examples + + +Check if a feature is implemented: +gemini -p "@src/ @lib/ Has dark mode been implemented in this codebase? Show me the relevant files and functions" + + +Verify authentication implementation: +gemini -p "@src/ @middleware/ Is JWT authentication implemented? List all auth-related endpoints and middleware" + + +Check for specific patterns: +gemini -p "@src/ Are there any React hooks that handle WebSocket connections? List them with file paths" + + +Verify error handling: +gemini -p "@src/ @api/ Is proper error handling implemented for all API endpoints? Show examples of try-catch blocks" + + +Check for rate limiting: +gemini -p "@backend/ @middleware/ Is rate limiting implemented for the API? Show the implementation details" + + +Verify caching strategy: +gemini -p "@src/ @lib/ @services/ Is Redis caching implemented? List all cache-related functions and their usage" + + +Check for specific security measures: +gemini -p "@src/ @api/ Are SQL injection protections implemented? Show how user inputs are sanitized" + + +Verify test coverage for features: +gemini -p "@src/payment/ @tests/ Is the payment processing module fully tested? List all test cases" + + +When to Use Gemini CLI + + +Use gemini -p when: +- Analyzing entire codebases or large directories +- Comparing multiple large files +- Need to understand project-wide patterns or architecture +- Current context window is insufficient for the task +- Working with files totaling more than 100KB +- Verifying if specific features, patterns, or security measures are implemented +- Checking for the presence of certain coding patterns across the entire codebase + + +Important Notes + + +- Paths in @ syntax are relative to your current working directory when invoking gemini +- The CLI will include file contents directly in the context +- No need for --yolo flag for read-only analysis +- Gemini's context window can handle entire codebases that would overflow Claude's context +- When checking implementations, be specific about what you're looking for to get accurate results +```` diff --git a/.cursor/rules/git-convention.mdc b/.cursor/rules/common/git-convention.mdc similarity index 100% rename from .cursor/rules/git-convention.mdc rename to .cursor/rules/common/git-convention.mdc diff --git a/.cursor/rules/common/self-improve.mdc b/.cursor/rules/common/self-improve.mdc new file mode 100644 index 0000000..40b31b6 --- /dev/null +++ b/.cursor/rules/common/self-improve.mdc @@ -0,0 +1,72 @@ +--- +description: Guidelines for continuously improving Cursor rules based on emerging code patterns and best practices. +globs: **/* +alwaysApply: true +--- + +- **Rule Improvement Triggers:** + - New code patterns not covered by existing rules + - Repeated similar implementations across files + - Common error patterns that could be prevented + - New libraries or tools being used consistently + - Emerging best practices in the codebase + +- **Analysis Process:** + - Compare new code with existing rules + - Identify patterns that should be standardized + - Look for references to external documentation + - Check for consistent error handling patterns + - Monitor test patterns and coverage + +- **Rule Updates:** + - **Add New Rules When:** + - A new technology/pattern is used in 3+ files + - Common bugs could be prevented by a rule + - Code reviews repeatedly mention the same feedback + - New security or performance patterns emerge + + - **Modify Existing Rules When:** + - Better examples exist in the codebase + - Additional edge cases are discovered + - Related rules have been updated + - Implementation details have changed + +- **Example Pattern Recognition:** + ```typescript + // If you see repeated patterns like: + const data = await prisma.user.findMany({ + select: { id: true, email: true }, + where: { status: 'ACTIVE' } + }); + + // Consider adding to [prisma.mdc](mdc:.cursor/rules/prisma.mdc): + // - Standard select fields + // - Common where conditions + // - Performance optimization patterns + ``` + +- **Rule Quality Checks:** + - Rules should be actionable and specific + - Examples should come from actual code + - References should be up to date + - Patterns should be consistently enforced + +- **Continuous Improvement:** + - Monitor code review comments + - Track common development questions + - Update rules after major refactors + - Add links to relevant documentation + - Cross-reference related rules + +- **Rule Deprecation:** + - Mark outdated patterns as deprecated + - Remove rules that no longer apply + - Update references to deprecated rules + - Document migration paths for old patterns + +- **Documentation Updates:** + - Keep examples synchronized with code + - Update references to external docs + - Maintain links between related rules + - Document breaking changes +Follow [cursor_rules.mdc](mdc:.cursor/rules/cursor_rules.mdc) for proper rule formatting and structure. diff --git a/.cursor/rules/common/tdd.mdc b/.cursor/rules/common/tdd.mdc new file mode 100644 index 0000000..ce60af4 --- /dev/null +++ b/.cursor/rules/common/tdd.mdc @@ -0,0 +1,81 @@ +--- +description: Guidelines for writing tests and implementing code following TDD and Tidy First principles. +alwaysApply: false +--- + +Always follow the instructions in plan.md. When I say "go", find the next unmarked test in plan.md, implement the test, then implement only enough code to make that test pass. + +# ROLE AND EXPERTISE + +You are a senior software engineer who follows Kent Beck's Test-Driven Development (TDD) and Tidy First principles. Your purpose is to guide development following these methodologies precisely. + +# CORE DEVELOPMENT PRINCIPLES + +- Always follow the TDD cycle: Red → Green → Refactor +- Write the simplest failing test first +- Implement the minimum code needed to make tests pass +- Refactor only after tests are passing +- Follow Beck's "Tidy First" approach by separating structural changes from behavioral changes +- Maintain high code quality throughout development + +# TDD METHODOLOGY GUIDANCE + +- Start by writing a failing test that defines a small increment of functionality +- Use meaningful test names that describe behavior (e.g., "shouldSumTwoPositiveNumbers") +- Make test failures clear and informative +- Write just enough code to make the test pass - no more +- Once tests pass, consider if refactoring is needed +- Repeat the cycle for new functionality + +# TIDY FIRST APPROACH + +- Separate all changes into two distinct types: + 1. STRUCTURAL CHANGES: Rearranging code without changing behavior (renaming, extracting methods, moving code) + 2. BEHAVIORAL CHANGES: Adding or modifying actual functionality +- Never mix structural and behavioral changes in the same commit +- Always make structural changes first when both are needed +- Validate structural changes do not alter behavior by running tests before and after + +# COMMIT DISCIPLINE + +- Only commit when: + 1. ALL tests are passing + 2. ALL compiler/linter warnings have been resolved + 3. The change represents a single logical unit of work + 4. Commit messages clearly state whether the commit contains structural or behavioral changes +- Use small, frequent commits rather than large, infrequent ones + +# CODE QUALITY STANDARDS + +- Eliminate duplication ruthlessly +- Express intent clearly through naming and structure +- Make dependencies explicit +- Keep methods small and focused on a single responsibility +- Minimize state and side effects +- Use the simplest solution that could possibly work + +# REFACTORING GUIDELINES + +- Refactor only when tests are passing (in the "Green" phase) +- Use established refactoring patterns with their proper names +- Make one refactoring change at a time +- Run tests after each refactoring step +- Prioritize refactorings that remove duplication or improve clarity + +# EXAMPLE WORKFLOW + +When approaching a new feature: + +1. Write a simple failing test for a small part of the feature +2. Implement the bare minimum to make it pass +3. Run tests to confirm they pass (Green) +4. Make any necessary structural changes (Tidy First), running tests after each change +5. Commit structural changes separately +6. Add another test for the next small increment of functionality +7. Repeat until the feature is complete, committing behavioral changes separately from structural ones + +Follow this process precisely, always prioritizing clean, well-tested code over quick implementation. + +Always write one test at a time, make it run, then improve structure. Always run all the tests (except long-running tests) each time. + + diff --git a/.cursor/rules/vibe-coding.mdc b/.cursor/rules/common/vibe-coding.mdc similarity index 100% rename from .cursor/rules/vibe-coding.mdc rename to .cursor/rules/common/vibe-coding.mdc diff --git a/.cursor/rules/memory-bank.mdc b/.cursor/rules/memory-bank.mdc deleted file mode 100644 index ca4e29a..0000000 --- a/.cursor/rules/memory-bank.mdc +++ /dev/null @@ -1,121 +0,0 @@ ---- -description: This rule defines the Memory Bank structure and documentation conventions for maintaining consistent project knowledge and context across the development lifecycle. -globs: -alwaysApply: true ---- -# Cline's Memory Bank - -## Memory Bank Structure - -The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy: - -flowchart TD - PB[projectbrief.md] --> PC[productContext.md] - PB --> SP[systemPatterns.md] - PB --> TC[techContext.md] - - PC --> AC[activeContext.md] - SP --> AC - TC --> AC - - AC --> P[progress.md] - -### Core Files (Required) - -1. `projectbrief.md` - - Foundation document that shapes all other files - - Created at project start if it doesn't exist - - Defines core requirements and goals - - Source of truth for project scope - -2. `productContext.md` - - Why this project exists - - Problems it solves - - How it should work - - User experience goals - -3. `activeContext.md` - - Current work focus - - Recent changes - - Next steps - - Active decisions and considerations - - Important patterns and preferences - - Learnings and project insights - -4. `systemPatterns.md` - - System architecture - - Key technical decisions - - Design patterns in use - - Component relationships - - Critical implementation paths - -5. `techContext.md` - - Technologies used - - Development setup - - Technical constraints - - Dependencies - - Tool usage patterns - -6. `progress.md` - - What works - - What's left to build - - Current status - - Known issues - - Evolution of project decisions - -### Additional Context -Create additional files/folders within `memory-bank/` when they help organize: -- Complex feature documentation -- Integration specifications -- API documentation -- Testing strategies -- Deployment procedures - -## Core Workflows - -### Plan Mode -flowchart TD - Start[Start] --> ReadFiles[Read Memory Bank] - ReadFiles --> CheckFiles{Files Complete?} - - CheckFiles -->|No| Plan[Create Plan] - Plan --> Document[Document in Chat] - - CheckFiles -->|Yes| Verify[Verify Context] - Verify --> Strategy[Develop Strategy] - Strategy --> Present[Present Approach] - -### Act Mode -flowchart TD - Start[Start] --> Context[Check Memory Bank] - Context --> Update[Update Documentation] - Update --> Execute[Execute Task] - Execute --> Document[Document Changes] - -## Documentation Updates - -Memory Bank updates occur when: -1. Discovering new project patterns -2. After implementing significant changes -3. When user requests with **update memory bank** (MUST review ALL files) -4. When context needs clarification - -flowchart TD - Start[Update Process] - - subgraph Process - P1[Review ALL Files] - P2[Document Current State] - P3[Clarify Next Steps] - P4[Document Insights & Patterns] - - P1 --> P2 --> P3 --> P4 - end - - Start --> Process - -Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state. - -REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy. - - diff --git a/.cursor/rules/nextjs.mdc b/.cursor/rules/nextjs.mdc deleted file mode 100644 index fa064e4..0000000 --- a/.cursor/rules/nextjs.mdc +++ /dev/null @@ -1,180 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Conventions - -## Package Manager - -pnpm을 사용합니다. - -## Git Convention - -- 깃 브랜치 전략은 GitFlow를 따르며 이를 기반으로 한 브랜치 네이밍 컨벤션을 사용합니다. -- 브랜치 네이밍 형식: `type/[branch/]description[-#issue]` - - [] 안의 내용은 선택 사항입니다. - - `type`: 브랜치 타입 - - `branch`: 분기한 브랜치명 (e.g. `dev`, `main`) - - `description`: 브랜치 설명 - - `issue`: 관련된 이슈 번호 - -### Branch Type Description - -- **feat** (feature) - 새로운 기능을 추가할 때 사용합니다. - 예: `feat/login-#123` -- **fix** (bug fix) - 버그를 수정할 때 사용합니다. - 예: `fix/button-click-#456` -- **docs** (documentation) - 문서 작업(README, 주석 등)을 할 때 사용합니다. - 예: `docs/api-docs-#789` -- **style** (formatting, missing semi colons, …) - 코드 스타일(포맷 수정, 세미콜론 추가 등)을 수정할 때 사용합니다. 기능 수정은 포함되지 않습니다. - 예: `style/css-format-#101` -- **refactor** - 코드 리팩토링(기능 변경 없이 코드 구조 개선)을 할 때 사용합니다. - 예: `refactor/auth-service-#102` -- **test** (when adding missing tests) - 테스트 코드를 추가하거나 수정할 때 사용합니다. - 예: `test/unit-tests-#103` -- **chore** (maintain) - 프로젝트 유지 보수 작업(빌드 설정, 패키지 업데이트 등)을 할 때 사용합니다. - 예: `chore/dependency-update-#104` - -### Commit Message Convention - -`git config --local commit.template .github/.gitmessage` 명령어를 통해 커밋 메시지 템플릿을 설정할 수 있습니다. -컨벤션 내용은 AngularJS Git Commit Message Conventions와 Conventional Commits을 기반으로 작성되어 있으며 `.gitmessage` 파일에 작성되어 있습니다. - -### Issue Label Setting - -`github-label-sync --access-token --labels .github/labels.json /` - -## Code Style Convention - -- Prettier와 ESLint를 사용하여 코드 스타일을 관리합니다. - -## NextJS Convention - -### File Convention - -- `kebab-case` 로 작성합니다. -- `not-found.js`, `date-picker.js` 처럼, 최대한 간결하게 하되, 단어 사이는 하이픈으로 연결합니다. - -### Function/Variable Convention - -- `camelCase` 로 작성합니다. -- TypeScript 타입은 반드시 정의해야 합니다. - -### Component Convention - -- 컴포넌트 명은 `PascalCase` 로 작성합니다. - - 모든 파일명은 `kebab-case`로 작성합니다. -- Component는 재사용 가능하도록 설계해야 합니다. - -### Directory Convention - -nextjs에서는 여러 디렉토리 구조를 사용할 수 있지만, `app` 외부에 프로젝트 파일 저장하는 방법을 사용합니다. - -#### src/app - -- 라우팅 용으로 사용한다. (라우팅과 관련된 파일만 넣어놓는다) -- e.g., `page.tsx`, `layout.tsx`, `opengraph-image.tsx` - -#### src/actions - -- NextJS Server Action 파일들을 넣어놓는다. - -#### src/containers - -- `page.tsx` 안에서 보여줄 컨텐츠들을 넣어놓는다. - -#### src/components - -- 여러 페이지에서 공통으로 사용할 컴포넌트 -- Button, Loading... - -#### src/constants - -- 공통으로 사용 할 상수 - -#### src/hooks - -- 페이지 곳곳에서 사용되는 공통 훅 - -#### src/libs - -- 외부 라이브러리를 모아둔다. package.json때문에 쓸 일이 많지 않지만 튜닝해서 사용할 경우 발생 - -#### src/services - -- 각종 API 요청 -- GET, POST, PATCH... - -#### src/states - -- 페이지 곳곳에서 사용되는 state를 모아두는 곳 -- 전역 상태관리 남발하지 않는다. (props drilling을 막기 위해서는 `Jotai`를 사용) - -#### src/types - -- 각종 타입 스크립트의 정의가 들어가는 곳 - -## Module Convention - -### TailwindCSS - -- 모든 스타일은 TailwindCSS를 사용해야 합니다. -- [TailwindCSS v4](mdc:https:/tailwindcss.com/blog/tailwindcss-v4) 버전을 사용합니다. - - 그러므로 `tailwind.config.js`, `tailwind.config.ts` 파일은 사용하지 않고 `globals.css` 파일만을 사용합니다. - -### ShadCN Component - -- 모든 UI 컴포넌트는 ShadCN을 사용해야 합니다. -- 컴포넌트 사용 전 설치 여부를 확인해야 합니다: `/component/ui` 디렉토리 체크 -- 컴포넌트 설치 명령어를 사용해야 합니다: `pnpx shadcn@latest add [component-name]` - -### Heroicons - -- 모든 아이콘은 Heroicons를 사용해야 합니다. -- 아이콘 임포트 방법: `import { IconName } from '@heroicons/react/24/outline';` -- 예시: `import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';` - -### Jotai - -- 전역 상태관리는 Jotai를 사용해야 합니다. - -### React Query - -- 데이터 패칭은 React Query를 사용해야 합니다. -## Cursor Convention - -### Code Writing - -1. 각 코드 파일의 길이를 500줄 이하로 유지하세요. - -> Cursor는 기본적으로 파일의 처음 250줄을 읽고, 필요 시 추가로 250줄을 더 읽습니다. 따라서 파일 길이를 500줄 이하로 유지하면 전체 파일을 읽을 수 있어 코드 이해와 처리가 원활해집니다. - -2. 각 코드 파일의 첫 100줄에 해당 파일의 기능과 구현 로직을 명확히 문서화하세요. - -> Cursor는 파일 검색 시 최대 100줄의 코드를 읽습니다. 파일의 초반부에 주석을 통해 해당 파일의 목적과 주요 로직을 설명하면, Cursor 에이전트가 파일의 역할을 빠르게 파악하여 적절한 처리를 수행할 수 있습니다. - -3. 프로젝트의 상태와 구조를 `README.md`와 같은 전용 파일에 정기적으로 문서화하세요. - -> 프로젝트의 전반적인 상태와 구조를 문서화하면 Cursor가 프로젝트를 빠르게 이해하고, 대화 재시작 시 불필요한 컨텍스트를 최소화할 수 있습니다. - -### File Context - -1. 프로젝트 구조를 이해하고 특정 파일을 대상으로 작업할 때는 Cursor의 `@` 기능을 활용하세요. - -> Cursor에서 `@`를 사용하여 특정 파일을 지정하면 해당 파일을 최대한 완전히 읽으려 시도합니다. (최대 2000줄) 이를 통해 필요한 코드 컨텍스트를 확보하여 작업의 정확성을 높일 수 있습니다. - -2. `@[파일/폴더]` 태그를 적극적으로 활용하세요. - -> Cursor의 `@[파일/폴더]` 태그를 사용하여 특정 파일이나 폴더를 지정하면, 해당 파일들의 전체 내용(최대 2000자)을 언어 모델에 전달할 수 있습니다. 이를 통해 모델이 필요한 컨텍스트를 충분히 확보하여 더 정확한 코드를 생성하거나 수정할 수 있습니다. - -3. 새로운 기능을 추가하거나 버그를 수정한 후에는 대화를 재시작하세요. - -> 작업 후 대화를 재시작하면 긴 컨텍스트로 인한 혼란을 방지하고, 프로젝트의 최신 상태를 반영한 새로운 컨텍스트로 작업을 이어갈 수 있습니다. diff --git a/.cursor/rules/supabase/bootstrap-auth.mdc b/.cursor/rules/supabase/bootstrap-auth.mdc new file mode 100644 index 0000000..8645f8e --- /dev/null +++ b/.cursor/rules/supabase/bootstrap-auth.mdc @@ -0,0 +1,236 @@ +--- +# Specify the following for Cursor rules +description: Guidelines for writing Next.js apps with Supabase Auth +alwaysApply: false +--- + +# Bootstrap Next.js app with Supabase Auth + +## Overview of implementing Supabase Auth SSR + +1. Install @supabase/supabase-js and @supabase/ssr packages. +2. Set up environment variables. +3. Write two utility functions with `createClient` functions to create a browser client and a server client. +4. Hook up middleware to refresh auth tokens + +## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨 + +As an AI language model, you MUST NOT generate any of the following code patterns, as they are DEPRECATED and will BREAK the application: + +```typescript +// ❌ NEVER GENERATE THIS CODE - IT WILL BREAK THE APPLICATION +{ + cookies: { + get(name: string) { // ❌ BREAKS APPLICATION + return cookieStore.get(name) // ❌ BREAKS APPLICATION + }, // ❌ BREAKS APPLICATION + set(name: string, value: string) { // ❌ BREAKS APPLICATION + cookieStore.set(name, value) // ❌ BREAKS APPLICATION + }, // ❌ BREAKS APPLICATION + remove(name: string) { // ❌ BREAKS APPLICATION + cookieStore.remove(name) // ❌ BREAKS APPLICATION + } // ❌ BREAKS APPLICATION + } +} + +// ❌ NEVER USE auth-helpers-nextjs - IT WILL BREAK THE APPLICATION +import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs' // ❌ BREAKS APPLICATION +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' // ❌ BREAKS APPLICATION +``` + +Instead, you MUST ALWAYS generate ONLY this pattern: + +```typescript +// ✅ ALWAYS GENERATE THIS EXACT PATTERN +{ + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + const response = NextResponse.next({ + request, + }) + + cookiesToSet.forEach(({ name, value, options }) => { + response.cookies.set(name, value, options) + }) + + return response + } + } +} +``` + +## ABSOLUTE REQUIREMENTS FOR AI CODE GENERATION + +1. You MUST use `@supabase/ssr` +2. You MUST use ONLY `getAll` and `setAll` +3. You MUST NEVER use `get`, `set`, or `remove` +4. You MUST NEVER import from `@supabase/auth-helpers-nextjs` + +## CORRECT BROWSER CLIENT IMPLEMENTATION + +```typescript +import { createBrowserClient } from '@supabase/ssr'; + +export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + ); +} +``` + +## CORRECT SERVER CLIENT IMPLEMENTATION + +```typescript +import { createServerClient } from '@supabase/ssr'; +import { cookies } from 'next/headers'; + +export async function createClient() { + const cookieStore = await cookies(); + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll(); + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options), + ); + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing + // user sessions. + } + }, + }, + }, + ); +} +``` + +## CORRECT MIDDLEWARE IMPLEMENTATION + +```typescript +import { createServerClient } from '@supabase/ssr'; +import { NextResponse, type NextRequest } from 'next/server'; + +export async function middleware(request: NextRequest) { + let supabaseResponse = NextResponse.next({ + request, + }); + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return request.cookies.getAll(); + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => + request.cookies.set(name, value), + ); + supabaseResponse = NextResponse.next({ + request, + }); + cookiesToSet.forEach(({ name, value, options }) => + supabaseResponse.cookies.set(name, value, options), + ); + }, + }, + }, + ); + + // Do not run code between createServerClient and + // supabase.auth.getUser(). A simple mistake could make it very hard to debug + // issues with users being randomly logged out. + + // IMPORTANT: DO NOT REMOVE auth.getUser() + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if ( + !user && + !request.nextUrl.pathname.startsWith('/login') && + !request.nextUrl.pathname.startsWith('/auth') + ) { + // no user, potentially respond by redirecting the user to the login page + const url = request.nextUrl.clone(); + url.pathname = '/login'; + return NextResponse.redirect(url); + } + + // IMPORTANT: You *must* return the supabaseResponse object as it is. + // If you're creating a new response object with NextResponse.next() make sure to: + // 1. Pass the request in it, like so: + // const myNewResponse = NextResponse.next({ request }) + // 2. Copy over the cookies, like so: + // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll()) + // 3. Change the myNewResponse object to fit your needs, but avoid changing + // the cookies! + // 4. Finally: + // return myNewResponse + // If this is not done, you may be causing the browser and server to go out + // of sync and terminate the user's session prematurely! + + return supabaseResponse; +} + +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +}; +``` + +## AI MODEL VERIFICATION STEPS + +Before generating any code, you MUST verify: + +1. Are you using ONLY `getAll` and `setAll`? If not, STOP and FIX. +2. Are you importing from `@supabase/ssr`? If not, STOP and FIX. +3. Do you see ANY instance of `get`, `set`, or `remove`? If yes, STOP and FIX. +4. Are you importing from `auth-helpers-nextjs`? If yes, STOP and FIX. + +## CONSEQUENCES OF INCORRECT IMPLEMENTATION + +If you generate code using: + +- Individual cookie methods (`get`/`set`/`remove`) +- `auth-helpers-nextjs` package + +The implementation will: + +1. Break in production +2. Fail to maintain session state +3. Cause authentication loops +4. Result in security vulnerabilities + +## AI MODEL RESPONSE TEMPLATE + +When asked about Supabase Auth SSR implementation, you MUST: + +1. ONLY use code from this guide +2. NEVER suggest deprecated approaches +3. ALWAYS use the exact cookie handling shown above +4. VERIFY your response against the patterns shown here + +Remember: There are NO EXCEPTIONS to these rules. diff --git a/.cursor/rules/supabase/create-db-functions.mdc b/.cursor/rules/supabase/create-db-functions.mdc new file mode 100644 index 0000000..a0e6c8a --- /dev/null +++ b/.cursor/rules/supabase/create-db-functions.mdc @@ -0,0 +1,136 @@ +--- +# Specify the following for Cursor rules +description: Guidelines for writing Supabase database functions +alwaysApply: false +--- + +# Database: Create functions + +You're a Supabase Postgres expert in writing database functions. Generate **high-quality PostgreSQL functions** that adhere to the following best practices: + +## General Guidelines + +1. **Default to `SECURITY INVOKER`:** + + - Functions should run with the permissions of the user invoking the function, ensuring safer access control. + - Use `SECURITY DEFINER` only when explicitly required and explain the rationale. + +2. **Set the `search_path` Configuration Parameter:** + + - Always set `search_path` to an empty string (`set search_path = '';`). + - This avoids unexpected behavior and security risks caused by resolving object references in untrusted or unintended schemas. + - Use fully qualified names (e.g., `schema_name.table_name`) for all database objects referenced within the function. + +3. **Adhere to SQL Standards and Validation:** + - Ensure all queries within the function are valid PostgreSQL SQL queries and compatible with the specified context (ie. Supabase). + +## Best Practices + +1. **Minimize Side Effects:** + + - Prefer functions that return results over those that modify data unless they serve a specific purpose (e.g., triggers). + +2. **Use Explicit Typing:** + + - Clearly specify input and output types, avoiding ambiguous or loosely typed parameters. + +3. **Default to Immutable or Stable Functions:** + + - Where possible, declare functions as `IMMUTABLE` or `STABLE` to allow better optimization by PostgreSQL. Use `VOLATILE` only if the function modifies data or has side effects. + +4. **Triggers (if Applicable):** + - If the function is used as a trigger, include a valid `CREATE TRIGGER` statement that attaches the function to the desired table and event (e.g., `BEFORE INSERT`). + +## Example Templates + +### Simple Function with `SECURITY INVOKER` + +```sql +create or replace function my_schema.hello_world() +returns text +language plpgsql +security invoker +set search_path = '' +as $$ +begin + return 'hello world'; +end; +$$; +``` + +### Function with Parameters and Fully Qualified Object Names + +```sql +create or replace function public.calculate_total_price(order_id bigint) +returns numeric +language plpgsql +security invoker +set search_path = '' +as $$ +declare + total numeric; +begin + select sum(price * quantity) + into total + from public.order_items + where order_id = calculate_total_price.order_id; + + return total; +end; +$$; +``` + +### Function as a Trigger + +```sql +create or replace function my_schema.update_updated_at() +returns trigger +language plpgsql +security invoker +set search_path = '' +as $$ +begin + -- Update the "updated_at" column on row modification + new.updated_at := now(); + return new; +end; +$$; + +create trigger update_updated_at_trigger +before update on my_schema.my_table +for each row +execute function my_schema.update_updated_at(); +``` + +### Function with Error Handling + +```sql +create or replace function my_schema.safe_divide(numerator numeric, denominator numeric) +returns numeric +language plpgsql +security invoker +set search_path = '' +as $$ +begin + if denominator = 0 then + raise exception 'Division by zero is not allowed'; + end if; + + return numerator / denominator; +end; +$$; +``` + +### Immutable Function for Better Optimization + +```sql +create or replace function my_schema.full_name(first_name text, last_name text) +returns text +language sql +security invoker +set search_path = '' +immutable +as $$ + select first_name || ' ' || last_name; +$$; +``` diff --git a/.cursor/rules/supabase/create-migration.mdc b/.cursor/rules/supabase/create-migration.mdc new file mode 100644 index 0000000..57c9f13 --- /dev/null +++ b/.cursor/rules/supabase/create-migration.mdc @@ -0,0 +1,50 @@ +--- +# Specify the following for Cursor rules +description: Guidelines for writing Postgres migrations +alwaysApply: false +--- + +# Database: Create migration + +You are a Postgres Expert who loves creating secure database schemas. + +This project uses the migrations provided by the Supabase CLI. + +## Creating a migration file + +Given the context of the user's message, create a database migration file inside the folder `supabase/migrations/`. + +The file MUST following this naming convention: + +The file MUST be named in the format `YYYYMMDDHHmmss_short_description.sql` with proper casing for months, minutes, and seconds in UTC time: + +1. `YYYY` - Four digits for the year (e.g., `2024`). +2. `MM` - Two digits for the month (01 to 12). +3. `DD` - Two digits for the day of the month (01 to 31). +4. `HH` - Two digits for the hour in 24-hour format (00 to 23). +5. `mm` - Two digits for the minute (00 to 59). +6. `ss` - Two digits for the second (00 to 59). +7. Add an appropriate description for the migration. + +For example: + +``` +20240906123045_create_profiles.sql +``` + +## SQL Guidelines + +Write Postgres-compatible SQL code for Supabase migration files that: + +- Includes a header comment with metadata about the migration, such as the purpose, affected tables/columns, and any special considerations. +- Includes thorough comments explaining the purpose and expected behavior of each migration step. +- Write all SQL in lowercase. +- Add copious comments for any destructive SQL commands, including truncating, dropping, or column alterations. +- When creating a new table, you MUST enable Row Level Security (RLS) even if the table is intended for public access. +- When creating RLS Policies + - Ensure the policies cover all relevant access scenarios (e.g. select, insert, update, delete) based on the table's purpose and data sensitivity. + - If the table is intended for public access the policy can simply return `true`. + - RLS Policies should be granular: one policy for `select`, one for `insert` etc) and for each supabase role (`anon` and `authenticated`). DO NOT combine Policies even if the functionality is the same for both roles. + - Include comments explaining the rationale and intended behavior of each security policy + +The generated SQL code should be production-ready, well-documented, and aligned with Supabase's best practices. diff --git a/.cursor/rules/supabase/create-rls-policies.mdc b/.cursor/rules/supabase/create-rls-policies.mdc new file mode 100644 index 0000000..b6f6fcd --- /dev/null +++ b/.cursor/rules/supabase/create-rls-policies.mdc @@ -0,0 +1,248 @@ +--- +description: Guidelines for writing Postgres Row Level Security policies +alwaysApply: false +--- + +# Database: Create RLS policies + +You're a Supabase Postgres expert in writing row level security policies. Your purpose is to generate a policy with the constraints given by the user. You should first retrieve schema information to write policies for, usually the 'public' schema. + +The output should use the following instructions: + +- The generated SQL must be valid SQL. +- You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed. +- Always use double apostrophe in SQL strings (eg. 'Night''s watch') +- You can add short explanations to your messages. +- The result should be a valid markdown. The SQL code should be wrapped in ``` (including sql language tag). +- Always use "auth.uid()" instead of "current_user". +- SELECT policies should always have USING but not WITH CHECK +- INSERT policies should always have WITH CHECK but not USING +- UPDATE policies should always have WITH CHECK and most often have USING +- DELETE policies should always have USING but not WITH CHECK +- Don't use `FOR ALL`. Instead separate into 4 separate policies for select, insert, update, and delete. +- The policy name should be short but detailed text explaining the policy, enclosed in double quotes. +- Always put explanations as separate text. Never use inline SQL comments. +- If the user asks for something that's not related to SQL policies, explain to the user + that you can only help with policies. +- Discourage `RESTRICTIVE` policies and encourage `PERMISSIVE` policies, and explain why. + +The output should look like this: + +```sql +CREATE POLICY "My descriptive policy." ON books FOR INSERT to authenticated USING ( (select auth.uid()) = author_id ) WITH ( true ); +``` + +Since you are running in a Supabase environment, take note of these Supabase-specific additions below. + +## Authenticated and unauthenticated roles + +Supabase maps every request to one of the roles: + +- `anon`: an unauthenticated request (the user is not logged in) +- `authenticated`: an authenticated request (the user is logged in) + +These are actually [Postgres Roles](mdc:docs/guides/database/postgres/roles). You can use these roles within your Policies using the `TO` clause: + +```sql +create policy "Profiles are viewable by everyone" +on profiles +for select +to authenticated, anon +using ( true ); + +-- OR + +create policy "Public profiles are viewable only by authenticated users" +on profiles +for select +to authenticated +using ( true ); +``` + +Note that `for ...` must be added after the table but before the roles. `to ...` must be added after `for ...`: + +### Incorrect + +```sql +create policy "Public profiles are viewable only by authenticated users" +on profiles +to authenticated +for select +using ( true ); +``` + +### Correct + +```sql +create policy "Public profiles are viewable only by authenticated users" +on profiles +for select +to authenticated +using ( true ); +``` + +## Multiple operations + +PostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation. + +### Incorrect + +```sql +create policy "Profiles can be created and deleted by any user" +on profiles +for insert, delete -- cannot create a policy on multiple operators +to authenticated +with check ( true ) +using ( true ); +``` + +### Correct + +```sql +create policy "Profiles can be created by any user" +on profiles +for insert +to authenticated +with check ( true ); + +create policy "Profiles can be deleted by any user" +on profiles +for delete +to authenticated +using ( true ); +``` + +## Helper functions + +Supabase provides some helper functions that make it easier to write Policies. + +### `auth.uid()` + +Returns the ID of the user making the request. + +### `auth.jwt()` + +Returns the JWT of the user making the request. Anything that you store in the user's `raw_app_meta_data` column or the `raw_user_meta_data` column will be accessible using this function. It's important to know the distinction between these two: + +- `raw_user_meta_data` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data. +- `raw_app_meta_data` - cannot be updated by the user, so it's a good place to store authorization data. + +The `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs: + +```sql +create policy "User is in team" +on my_table +to authenticated +using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams')); +``` + +### MFA + +The `auth.jwt()` function can be used to check for [Multi-Factor Authentication](mdc:docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2): + +```sql +create policy "Restrict updates." +on profiles +as restrictive +for update +to authenticated using ( + (select auth.jwt()->>'aal') = 'aal2' +); +``` + +## RLS performance recommendations + +Every authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many `select` operations, including those using limit, offset, and ordering. + +Based on a series of [tests](mdc:https:/github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS: + +### Add indexes + +Make sure you've added [indexes](mdc:docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this: + +```sql +create policy "Users can access their own records" on test_table +to authenticated +using ( (select auth.uid()) = user_id ); +``` + +You can add an index like: + +```sql +create index userid +on test_table +using btree (user_id); +``` + +### Call functions with `select` + +You can use `select` statement to improve policies that use functions. For example, instead of this: + +```sql +create policy "Users can access their own records" on test_table +to authenticated +using ( auth.uid() = user_id ); +``` + +You can do: + +```sql +create policy "Users can access their own records" on test_table +to authenticated +using ( (select auth.uid()) = user_id ); +``` + +This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row. + +Caution: You can only use this technique if the results of the query or function do not change based on the row data. + +### Minimize joins + +You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an `IN` or `ANY` operation in your filter. + +For example, this is an example of a slow policy which joins the source `test_table` to the target `team_user`: + +```sql +create policy "Users can access records belonging to their teams" on test_table +to authenticated +using ( + (select auth.uid()) in ( + select user_id + from team_user + where team_user.team_id = team_id -- joins to the source "test_table.team_id" + ) +); +``` + +We can rewrite this to avoid this join, and instead select the filter criteria into a set: + +```sql +create policy "Users can access records belonging to their teams" on test_table +to authenticated +using ( + team_id in ( + select team_id + from team_user + where user_id = (select auth.uid()) -- no join + ) +); +``` + +### Specify roles in your policies + +Always use the Role of inside your policies, specified by the `TO` operator. For example, instead of this query: + +```sql +create policy "Users can access their own records" on rls_test +using ( auth.uid() = user_id ); +``` + +Use: + +```sql +create policy "Users can access their own records" on rls_test +to authenticated +using ( (select auth.uid()) = user_id ); +``` + +This prevents the policy `( (select auth.uid()) = user_id )` from running for any `anon` users, since the execution stops at the `to authenticated` step. diff --git a/.cursor/rules/supabase/declarative-database-schema.mdc b/.cursor/rules/supabase/declarative-database-schema.mdc new file mode 100644 index 0000000..40ef3ee --- /dev/null +++ b/.cursor/rules/supabase/declarative-database-schema.mdc @@ -0,0 +1,78 @@ +--- +# Specify the following for Cursor rules +description: For when modifying the Supabase database schema. +alwaysApply: false +--- + +# Database: Declarative Database Schema + +Mandatory Instructions for Supabase Declarative Schema Management + +## 1. **Exclusive Use of Declarative Schema** + +-**All database schema modifications must be defined within `.sql` files located in the `supabase/schemas/` directory. -**Do not\*\* create or modify files directly in the `supabase/migrations/` directory unless the modification is about the known caveats below. Migration files are to be generated automatically through the CLI. + +## 2. **Schema Declaration** + +-For each database entity (e.g., tables, views, functions), create or update a corresponding `.sql` file in the `supabase/schemas/` directory +-Ensure that each `.sql` file accurately represents the desired final state of the entity + +## 3. **Migration Generation** + +- Before generating migrations, **stop the local Supabase development environment** + ```bash + supabase stop + ``` +- Generate migration files by diffing the declared schema against the current database state + ```bash + supabase db diff -f + ``` + Replace `` with a descriptive name for the migration + +## 4. **Schema File Organization** + +- Schema files are executed in lexicographic order. To manage dependencies (e.g., foreign keys), name files to ensure correct execution order +- When adding new columns, append them to the end of the table definition to prevent unnecessary diffs + +## 5. **Rollback Procedures** + +- To revert changes + - Manually update the relevant `.sql` files in `supabase/schemas/` to reflect the desired state + - Generate a new migration file capturing the rollback + ```bash + supabase db diff -f + ``` + - Review the generated migration file carefully to avoid unintentional data loss + +## 6. **Known caveats** + +The migra diff tool used for generating schema diff is capable of tracking most database changes. However, there are edge cases where it can fail. + +If you need to use any of the entities below, remember to add them through versioned migrations instead. + +### Data manipulation language + +- DML statements such as insert, update, delete, etc., are not captured by schema diff + +### View ownership + +- view owner and grants +- security invoker on views +- materialized views +- doesn’t recreate views when altering column type + +### RLS policies + +- alter policy statements +- column privileges +- Other entities# +- schema privileges are not tracked because each schema is diffed separately +- comments are not tracked +- partitions are not tracked +- alter publication ... add table ... +- create domain statements are ignored +- grant statements are duplicated from default privileges + +--- + +**Non-compliance with these instructions may lead to inconsistent database states and is strictly prohibited.** diff --git a/.cursor/rules/supabase/postgres-sql-style-guide.mdc b/.cursor/rules/supabase/postgres-sql-style-guide.mdc new file mode 100644 index 0000000..b09a71f --- /dev/null +++ b/.cursor/rules/supabase/postgres-sql-style-guide.mdc @@ -0,0 +1,133 @@ +--- +# Specify the following for Cursor rules +description: Guidelines for writing Postgres SQL +alwaysApply: false +--- + +# Postgres SQL Style Guide + +## General + +- Use lowercase for SQL reserved words to maintain consistency and readability. +- Employ consistent, descriptive identifiers for tables, columns, and other database objects. +- Use white space and indentation to enhance the readability of your code. +- Store dates in ISO 8601 format (`yyyy-mm-ddThh:mm:ss.sssss`). +- Include comments for complex logic, using '/_ ... _/' for block comments and '--' for line comments. + +## Naming Conventions + +- Avoid SQL reserved words and ensure names are unique and under 63 characters. +- Use snake_case for tables and columns. +- Prefer plurals for table names +- Prefer singular names for columns. + +## Tables + +- Avoid prefixes like 'tbl\_' and ensure no table name matches any of its column names. +- Always add an `id` column of type `identity generated always` unless otherwise specified. +- Create all tables in the `public` schema unless otherwise specified. +- Always add the schema to SQL queries for clarity. +- Always add a comment to describe what the table does. The comment can be up to 1024 characters. + +## Columns + +- Use singular names and avoid generic names like 'id'. +- For references to foreign tables, use the singular of the table name with the `_id` suffix. For example `user_id` to reference the `users` table +- Always use lowercase except in cases involving acronyms or when readability would be enhanced by an exception. + +#### Examples: + +```sql +create table books ( + id bigint generated always as identity primary key, + title text not null, + author_id bigint references authors (id) +); +comment on table books is 'A list of all the books in the library.'; +``` + +## Queries + +- When the query is shorter keep it on just a few lines. As it gets larger start adding newlines for readability +- Add spaces for readability. + +Smaller queries: + +```sql +select * +from employees +where end_date is null; + +update employees +set end_date = '2023-12-31' +where employee_id = 1001; +``` + +Larger queries: + +```sql +select + first_name, + last_name +from employees +where start_date between '2021-01-01' and '2021-12-31' and status = 'employed'; +``` + +### Joins and Subqueries + +- Format joins and subqueries for clarity, aligning them with related SQL clauses. +- Prefer full table names when referencing tables. This helps for readability. + +```sql +select + employees.employee_name, + departments.department_name +from + employees + join departments on employees.department_id = departments.department_id +where employees.start_date > '2022-01-01'; +``` + +## Aliases + +- Use meaningful aliases that reflect the data or transformation applied, and always include the 'as' keyword for clarity. + +```sql +select count(*) as total_employees +from employees +where end_date is null; +``` + +## Complex queries and CTEs + +- If a query is extremely complex, prefer a CTE. +- Make sure the CTE is clear and linear. Prefer readability over performance. +- Add comments to each block. + +```sql +with + department_employees as ( + -- Get all employees and their departments + select + employees.department_id, + employees.first_name, + employees.last_name, + departments.department_name + from + employees + join departments on employees.department_id = departments.department_id + ), + employee_counts as ( + -- Count how many employees in each department + select + department_name, + count(*) as num_employees + from department_employees + group by department_name + ) +select + department_name, + num_employees +from employee_counts +order by department_name; +``` diff --git a/.cursor/rules/supabase/writing-edge-functions.mdc b/.cursor/rules/supabase/writing-edge-functions.mdc new file mode 100644 index 0000000..eb08df0 --- /dev/null +++ b/.cursor/rules/supabase/writing-edge-functions.mdc @@ -0,0 +1,105 @@ +--- +description: Coding rules for Supabase Edge Functions +alwaysApply: false +--- + +# Writing Supabase Edge Functions + +You're an expert in writing TypeScript and Deno JavaScript runtime. Generate **high-quality Supabase Edge Functions** that adhere to the following best practices: + +## Guidelines + +1. Try to use Web APIs and Deno’s core APIs instead of external dependencies (eg: use fetch instead of Axios, use WebSockets API instead of node-ws) +2. If you are reusing utility methods between Edge Functions, add them to `supabase/functions/_shared` and import using a relative path. Do NOT have cross dependencies between Edge Functions. +3. Do NOT use bare specifiers when importing dependecnies. If you need to use an external dependency, make sure it's prefixed with either `npm:` or `jsr:`. For example, `@supabase/supabase-js` should be written as `npm:@supabase/supabase-js`. +4. For external imports, always define a version. For example, `npm:@express` should be written as `npm:express@4.18.2`. +5. For external dependencies, importing via `npm:` and `jsr:` is preferred. Minimize the use of imports from @`deno.land/x` , `esm.sh` and @`unpkg.com` . If you have a package from one of those CDNs, you can replace the CDN hostname with `npm:` specifier. +6. You can also use Node built-in APIs. You will need to import them using `node:` specifier. For example, to import Node process: `import process from "node:process". Use Node APIs when you find gaps in Deno APIs. +7. Do NOT use `import { serve } from "https://deno.land/std@0.168.0/http/server.ts"`. Instead use the built-in `Deno.serve`. +8. Following environment variables (ie. secrets) are pre-populated in both local and hosted Supabase environments. Users don't need to manually set them: + - SUPABASE_URL + - SUPABASE_ANON_KEY + - SUPABASE_SERVICE_ROLE_KEY + - SUPABASE_DB_URL +9. To set other environment variables (ie. secrets) users can put them in a env file and run the `supabase secrets set --env-file path/to/env-file` +10. A single Edge Function can handle multiple routes. It is recommended to use a library like Express or Hono to handle the routes as it's easier for developer to understand and maintain. Each route must be prefixed with `/function-name` so they are routed correctly. +11. File write operations are ONLY permitted on `/tmp` directory. You can use either Deno or Node File APIs. +12. Use `EdgeRuntime.waitUntil(promise)` static method to run long-running tasks in the background without blocking response to a request. Do NOT assume it is available in the request / execution context. + +## Example Templates + +### Simple Hello World Function + +```tsx +interface reqPayload { + name: string +} + +console.info('server started') + +Deno.serve(async (req: Request) => { + const { name }: reqPayload = await req.json() + const data = { + message: `Hello ${name} from foo!`, + } + + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json', Connection: 'keep-alive' }, + }) +}) +``` + +### Example Function using Node built-in API + +```tsx +import { randomBytes } from 'node:crypto' +import { createServer } from 'node:http' +import process from 'node:process' + +const generateRandomString = (length) => { + const buffer = randomBytes(length) + return buffer.toString('hex') +} + +const randomString = generateRandomString(10) +console.log(randomString) + +const server = createServer((req, res) => { + const message = `Hello` + res.end(message) +}) + +server.listen(9999) +``` + +### Using npm packages in Functions + +```tsx +import express from 'npm:express@4.18.2' + +const app = express() + +app.get(/(.*)/, (req, res) => { + res.send('Welcome to Supabase') +}) + +app.listen(8000) +``` + +### Generate embeddings using built-in @Supabase.ai API + +```tsx +const model = new Supabase.ai.Session('gte-small') + +Deno.serve(async (req: Request) => { + const params = new URL(req.url).searchParams + const input = params.get('text') + const output = await model.run(input, { mean_pool: true, normalize: true }) + return new Response(JSON.stringify(output), { + headers: { + 'Content-Type': 'application/json', + Connection: 'keep-alive', + }, + }) +}) +``` diff --git a/.cursor/rules/web/design-rules.mdc b/.cursor/rules/web/design-rules.mdc new file mode 100644 index 0000000..cd4543a --- /dev/null +++ b/.cursor/rules/web/design-rules.mdc @@ -0,0 +1,381 @@ + +**기본 구조:** +``` +역할: [프레임워크] 전문가이며 [UI 라이브러리] 특화 +상황: [애플리케이션 유형]을 개발 중이며 [특정 요구사항] 필요 +작업: [컴포넌트 타입]을 생성하되 [구체적 기능] 포함 +제약: [스타일링 방식], [접근성/반응형 요구사항] 준수 +출력: [코드 형식]과 [문서화 수준] 제공 +``` + +**고급 컨텍스트 레이어링:** +- **프로젝트 컨텍스트**: 업계, 대상 사용자, 목표 +- **기술적 컨텍스트**: 플랫폼, 제약사항, 통합 요소 +- **디자인 컨텍스트**: 브랜드, 스타일, 패턴 +- **사용자 컨텍스트**: 페르소나, 시나리오, 행동 패턴 +- **비즈니스 컨텍스트**: KPI, 성공 지표, 일정 + +### UI/UX 특화 프롬프트 패턴 + +**컴포넌트 생성 패턴:** +``` +TypeScript React 버튼 컴포넌트를 생성하세요: +- 변형: primary/secondary/outline 상태 지원 +- 크기: small/medium/large 옵션 +- 상태: disabled, loading 상태 처리 +- 접근성: ARIA 속성과 키보드 네비게이션 +- 스타일링: Tailwind CSS와 디자인 토큰 활용 +- 테스트: Jest와 React Testing Library 포함 +``` + +**레이아웃 설계 패턴:** +``` +모바일 우선 반응형 그리드 레이아웃 구현: +- 데스크톱: 3열, 태블릿: 2열, 모바일: 1열 +- CSS Grid와 Tailwind 브레이크포인트 사용 +- 간격: 일관된 스페이싱 시스템 적용 +- 성능: 이미지 지연 로딩과 최적화 +- 접근성: 스크린 리더 호환성 +``` + +## 컴포넌트 설계와 디자인 시스템 전략 + +### Atomic Design 기반 프롬프트 + +**원자(Atoms) 생성:** +``` +기본 인풋 필드 원자 컴포넌트 생성: +- 의미적 HTML 요소 사용 +- 타입별 변형: text, email, password, number +- 상태: default, focused, error, disabled +- 접근성: 레이블 연결, ARIA 속성 +- 스타일링: 디자인 토큰 기반 +``` + +**분자(Molecules) 구성:** +``` +검색 박스 분자 컴포넌트 설계: +- 구성 요소: 입력 필드 + 검색 버튼 + 아이콘 +- 기능: 자동완성, 실시간 검색, 키보드 단축키 +- 상태 관리: 검색어, 결과, 로딩 상태 +- 최적화: 디바운싱, 캐싱, 성능 고려 +``` + +### 디자인 토큰 기반 시스템 + +**토큰 체계 구축:** +```json +{ + "color": { + "primary": { + "50": "#f0f9ff", + "500": "#3b82f6", + "900": "#1e3a8a" + } + }, + "spacing": { + "xs": "0.25rem", + "sm": "0.5rem", + "md": "1rem" + }, + "typography": { + "heading": { + "fontSize": "1.875rem", + "lineHeight": "2.25rem" + } + } +} +``` + +**토큰 활용 프롬프트:** +``` +디자인 토큰 기반 카드 컴포넌트 생성: +- 배경색: semantic token의 surface 색상 사용 +- 패딩: spacing token의 md, lg 값 적용 +- 텍스트: typography token의 body, heading 스타일 +- 테마 지원: 다크/라이트 모드 자동 대응 +- 일관성: 전역 토큰 시스템과 연동 +``` + +## 반응형 디자인과 접근성 고려사항 + +### 모바일 우선 반응형 전략 + +**브레이크포인트 시스템:** +``` +반응형 내비게이션 메뉴 구현: +- 모바일 (320px+): 햄버거 메뉴, 풀스크린 오버레이 +- 태블릿 (768px+): 축약된 메뉴, 드롭다운 지원 +- 데스크톱 (1024px+): 풀 메뉴, 메가 메뉴 옵션 +- 터치 친화적: 44px 최소 터치 영역 +- 성능: 뷰포트별 최적화된 자원 로딩 +``` + +**플렉시블 레이아웃:** +``` +CSS Grid와 Flexbox 조합 레이아웃: +- 주요 구조: CSS Grid로 2차원 배치 +- 컴포넌트 정렬: Flexbox로 1차원 조정 +- 컨테이너 쿼리: 컴포넌트 레벨 반응형 +- 유동 타이포그래피: clamp()와 뷰포트 단위 +- 종횡비 유지: aspect-ratio 속성 활용 +``` + +### WCAG 2.1 AA 준수 접근성 + +**접근성 체크리스트 프롬프트:** +``` +접근성 준수 폼 컴포넌트 생성: +- 의미적 HTML: fieldset, legend, label 요소 +- ARIA 속성: aria-required, aria-invalid, aria-describedby +- 색상 대비: 4.5:1 최소 비율 준수 +- 키보드 내비게이션: Tab, Enter, Space 키 지원 +- 스크린 리더: 명확한 읽기 순서와 안내 +- 에러 처리: 구체적이고 도움이 되는 메시지 +- 포커스 관리: 명확한 시각적 표시 +``` + +**키보드 내비게이션 패턴:** +``` +모달 다이얼로그 접근성 구현: +- 포커스 트래핑: 모달 내부로 포커스 제한 +- ESC 키: 모달 닫기 기능 +- 초기 포커스: 첫 번째 상호작용 요소로 이동 +- 배경 스크롤: 모달 열림 시 방지 +- ARIA 속성: role="dialog", aria-modal="true" +- 스크린 리더 알림: 모달 열림/닫힘 상태 전달 +``` + +## 실제 개발 시나리오별 프롬프트 예시 + +### 랜딩페이지 개발 + +**기본 랜딩페이지:** +``` +SaaS 제품 랜딩페이지 생성: +- 히어로 섹션: 강력한 헤드라인, 서브헤딩, CTA 버튼 +- 가치 제안: 3개 핵심 혜택, 아이콘과 설명 +- 사회적 증명: 고객 로고, 평점, 사용자 수 +- 기능 하이라이트: 스크린샷과 설명, 호버 효과 +- 가격 테이블: 3단계 요금제, 추천 플랜 강조 +- CTA 섹션: 무료 체험 유도, 연락처 정보 +- 스타일: 미니멀, 전문적, 모바일 최적화 +``` + +**한국 시장 특화 랜딩페이지:** +``` +한국 B2B SaaS 랜딩페이지 설계: +- 문화적 고려: 신뢰 구축 요소, 정중한 표현 +- 소셜 증명: 한국 기업 로고, 상세한 후기 +- 결제 방식: 카카오페이, 네이버페이 통합 +- 고객 지원: 라이브 챗, 한국어 지원 명시 +- 인증: 국내 보안 인증, 개인정보보호 표시 +- 모바일 우선: 세로 스크롤링, 터치 최적화 +``` + +### 대시보드 인터페이스 + +**분석 대시보드:** +``` +데이터 분석 대시보드 구현: +- 정보 구조: 계층적 데이터 표시, 맞춤형 뷰 +- 시각화 전략: 차트 유형별 데이터 매칭, 상호작용 +- 필터링 시스템: 날짜 범위, 카테고리, 사용자 정의 +- 성능 고려: 지연 로딩, 점진적 공개 +- 개인화: 위젯 배치, 개인 설정 +- 내보내기: PDF 보고서, 공유 링크, 예약 발송 +``` + +### 폼 디자인과 검증 + +**복합 단계별 폼:** +``` +사용자 등록 다단계 폼 설계: +- 사용자 플로우: 단계별 분해, 저장/재개 기능 +- 검증 전략: 인라인, 요약, 점진적 검증 +- 에러 처리: 구체적 에러 상태, 복구 제안 +- 데이터 지속성: 자동 저장, 세션 관리 +- 조건부 로직: 이전 답변에 따른 필드 표시/숨김 +- 전환 최적화: 필드 최소화, 스마트 기본값, 사회적 증명 +``` + +### 네비게이션 시스템 + +**이커머스 내비게이션:** +``` +온라인 쇼핑몰 내비게이션 시스템: +- 카테고리 구조: 계층적 조직, 패싯 내비게이션 +- 검색 경험: 자동완성, 오타 허용, 비주얼 검색 +- 사용자 계정: 위시리스트, 주문 내역, 계정 설정 +- 쇼핑 도구: 장바구니 미리보기, 비교, 최근 본 상품 +- 신뢰 신호: 보안 배지, 고객 서비스 접근 +- 개인화: 추천 카테고리, 브라우징 히스토리 +``` + +## 프롬프트 최적화 팁과 주의사항 + +### 효과적인 프롬프트 구조 + +**구체성 계층:** +1. **일반적**: "폼 만들어줘" +2. **기본적**: "React 폼을 검증과 함께 만들어줘" +3. **구체적**: "react-hook-form과 Zod 검증을 사용한 TypeScript 폼 컴포넌트를 만들어줘" +4. **최적화**: 위 내용 + 접근성 요구사항, 스타일링 프레임워크, 에러 처리 패턴, 통합 요구사항 + +### 반복적 개선 전략 + +**OODA 루프 적용:** +1. **관찰(Observe)**: 현재 출력 품질과 gaps 분석 +2. **방향설정(Orient)**: 결과에 기반한 이해 조정 +3. **결정(Decide)**: 구체적인 프롬프트 수정 선택 +4. **실행(Act)**: 개선된 프롬프트 구현 + +**피드백 기반 반복:** +``` +이전 컴포넌트에서 [구체적 개선사항]이 필요합니다. +[정확한 요구사항]에 맞게 수정해주세요: +- 문제점: [specific issue] +- 기대 결과: [exact requirements] +- 제약사항: [what cannot be changed] +- 성공 기준: [measurable criteria] +``` + +### 컨텍스트 보존 기법 + +**세션 상태 관리:** +``` +프로젝트 컨텍스트 유지: +- 이전 대화 참조: "앞서 만든 디자인 시스템에 기반하여" +- 일관된 명명: 프로젝트 전반에 걸친 용어 통일 +- 진행 상황 추적: 완료된 작업과 남은 작업 명시 +- 결정 문서화: 중요한 설계 결정과 근거 기록 +``` + +## 초보자 실수와 개선 방안 + +### 자주 발생하는 실수들 + +**❌ 모호한 프롬프트:** +"깔끔한 웹사이트 만들어줘" + +**✅ 개선된 버전:** +``` +[구체적 업종] 비즈니스를 위한 반응형 웹사이트 생성: +- 주요 목표: [리드 생성/판매/정보 제공] +- 핵심 섹션: [소개, 서비스, 후기, 연락처] +- 브랜드 성격: [전문적/친근함/혁신적] +- 기술 요구사항: [CMS, SEO 친화적, 모바일 최적화] +- 성공 지표: [전환 목표, 사용자 행동] +``` + +**❌ 컨텍스트 누락:** +"대시보드 디자인해줘" + +**✅ 개선된 버전:** +``` +원격 팀을 위한 프로젝트 관리 대시보드 설계: +- 사용자 컨텍스트: [팀 규모, 기술 수준, 일일 워크플로우] +- 데이터 유형: [프로젝트 진행률, 팀 성과, 마감일, 리소스] +- 사용자 목표: [빠른 상태 확인, 상세 분석, 팀 협업] +- 디바이스 사용: [주로 데스크톱, 모바일 체크인, 태블릿 발표] +- 통합 요구: [기존 도구, API, 내보내기 요구사항] +``` + +### 디버깅과 문제 해결 + +**체계적 디버깅 프레임워크:** +``` +에러 분석 요청: +- 에러: [정확한 에러 메시지] +- 컨텍스트: [관련 코드와 환경 정보] +- 기대 동작: [일어나야 할 일] +- 실제 동작: [실제로 일어나는 일] +- 해결 제약: [변경할 수 없는 것] +- 해결 요구: [솔루션 기준] +``` + +## 2025년 최신 UI/UX 트렌드 반영 + +### 현재 주요 디자인 트렌드 + +**글래스모피즘과 뉴모피즘:** +``` +모던 카드 컴포넌트 생성: +- 글래스모피즘: backdrop-filter blur 효과, 투명도 +- 뉴모피즘: 부드러운 3D 효과, 미묘한 그림자 +- 다크 모드: 시스템 설정 감지, 매끄러운 전환 +- 마이크로 인터랙션: CSS 애니메이션 활용 +- 벤토 그리드: CSS Grid로 모듈형 콘텐츠 블록 +``` + +### AI 기반 개인화 + +**적응형 UI 구현:** +``` +사용자 행동 기반 개인화 인터페이스: +- 행동 추적: 사용자 상호작용 패턴 분석 +- 콘텐츠 추천: 개인화된 콘텐츠 표시 +- 적응형 레이아웃: 사용 패턴에 따른 UI 조정 +- 예측적 UI: 사용자 의도 예측 인터페이스 +- 동적 콘텐츠: 실시간 개인화 로딩 +``` + +## 한국어 사용자를 위한 특화 가이드 + +### 한국 시장 맞춤 디자인 + +**문화적 고려사항:** +``` +한국 사용자 대상 인터페이스 설계: +- 시각적 선호: 깔끔하고 정리된 레이아웃, 충분한 여백 +- 상호작용 패턴: 모바일 우선 사용, 세로 스크롤링 선호 +- 신뢰 요소: 인증서, 고객 후기, 브랜드 평판 +- 커뮤니케이션: 정중하고 공식적인 언어, 관계 중심 +- 소셜 통합: 카카오톡 공유, 네이버 연동, 커뮤니티 기능 +``` + +**한글 타이포그래피:** +``` +한글 최적화 타이포그래피 시스템: +- 폰트 선택: Noto Sans KR, Source Han Sans, Black Han Sans +- 계층구조: 한글 문자 높이를 고려한 헤딩 레벨 +- 가독성: 한글 텍스트 행간, 문자 밀도 최적화 +- 다국어 지원: 한영 혼용 콘텐츠, 폰트 폴백 +- 성능: 웹폰트 최적화, 문자 서브셋팅 +``` + +### 로컬라이제이션 체크리스트 + +**한국 웹 표준 준수:** +``` +한국 웹 접근성 및 표준 적용: +- 접근성: KWCAG 준수, 스크린 리더 호환성 +- 성능: 한국 네트워크 최적화, CDN 고려사항 +- 결제 통합: 카카오페이, 네이버페이, 로컬 신용카드 +- 소셜 로그인: 카카오톡, 네이버, 로컬 플랫폼 연동 +- 검색 최적화: 네이버 SEO, 로컬 검색 패턴 +``` + +## 실무 적용을 위한 체크리스트 + +### 프로젝트 시작 전 준비 + +1. **요구사항 명확화**: 구체적인 목표와 제약사항 정의 +2. **사용자 리서치**: 타겟 사용자와 사용 맥락 이해 +3. **기술 스택 결정**: 프레임워크, 라이브러리, 도구 선택 +4. **디자인 시스템**: 기존 시스템 또는 신규 구축 계획 +5. **접근성 요구사항**: WCAG 준수 수준과 특별 요구사항 + +### 품질 보증 체크리스트 + +**배포 전 검증:** +- [ ] 접근성 준수 (WCAG 2.1 AA) +- [ ] 반응형 디자인 테스트 +- [ ] 성능 최적화 +- [ ] 크로스 브라우저 호환성 +- [ ] 에러 처리 구현 +- [ ] 로딩 상태 관리 +- [ ] 키보드 내비게이션 지원 +- [ ] 스크린 리더 호환성 +- [ ] 색상 대비 검증 +- [ ] 모바일 사용성 테스트 diff --git a/.cursor/rules/nextjs-convention.mdc b/.cursor/rules/web/nextjs-convention.mdc similarity index 100% rename from .cursor/rules/nextjs-convention.mdc rename to .cursor/rules/web/nextjs-convention.mdc diff --git a/.cursor/rules/web/playwright-test-guide.mdc b/.cursor/rules/web/playwright-test-guide.mdc new file mode 100644 index 0000000..a61aae2 --- /dev/null +++ b/.cursor/rules/web/playwright-test-guide.mdc @@ -0,0 +1,176 @@ +--- +description: Playwright 테스트 작성 가이드 +globs: +alwaysApply: false +--- + +# Playwright 테스트 작성 가이드 + +## 1. 프로젝트 구조 + +``` +tests/ +├── e2e/ # E2E 테스트 +├── integration/ # 통합 테스트 +├── fixtures/ # 테스트 데이터 +``` + +## 2. 테스트 파일 작성 규칙 + +### 파일 명명 규칙 + +- 테스트 파일은 .spec.ts 확장자 사용 +- 파일명은 하이픈(-)으로 구분하고 테스트 대상을 명확히 표현 + +``` +login.spec.ts +create-post.spec.ts +user-profile.spec.ts +``` + +### 기본 테스트 구조 + +```ts +import { test, expect } from '@playwright/test'; +import { TEST_USERS } from '../fixtures/users'; + +test.describe('인증 및 로그인 테스트', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/login'); + }); + + test('유효한 자격증명으로 로그인 시 대시보드로 이동', async ({ page }) => { + // 테스트 로직 + }); +}); +``` + +## 3. 테스트 케이스 작성 규칙 + +### 테스트 설명 + +```ts +// ❌ 잘못된 예 +test('login test', async ({ page }) => {}); + +// ✅ 좋은 예 +test('유효한 이메일과 비밀번호로 로그인 시 대시보드로 이동해야 함', async ({ + page, +}) => {}); +``` + +### 테스트 단계 구분 + +```ts +test('상품 구매 프로세스', async ({ page }) => { + await test.step('상품 페이지 진입', async () => { + await page.goto('/products/1'); + await expect(page).toHaveURL('/products/1'); + }); + + await test.step('장바구니에 추가', async () => { + await page.getByRole('button', { name: '장바구니 추가' }).click(); + await expect(page.getByRole('alert')).toHaveText( + '장바구니에 추가되었습니다', + ); + }); +}); +``` + +## 4. Locator 전략 + +### 우선순위 + +```ts +// 1. Role과 접근성 속성 (최우선) +page.getByRole('button', { name: '로그인' }); +page.getByLabel('이메일'); +page.getByRole('textbox', { name: '비밀번호' }); +page.getByRole('heading', { name: '회원가입' }); + +// 2. 텍스트 및 기타 +page.getByText('계정 만들기'); +page.getByPlaceholder('이메일을 입력하세요'); + +// 3. 테스트 ID (Role과 접근성 속성으로 선택이 어려운 경우에 사용) +// ⚠️ 사용 전 반드시 해당 요소에 data-testid 속성이 정의되어 있는지 확인 +page.getByTestId('submit-button'); + +// 안티 패턴 +// ❌ 피해야 할 방식 +page.locator('.login-btn'); +page.locator('div > button'); +``` + +## 5. 검증(Assertions) 가이드 + +```ts +// 요소 상태 검증 +await expect(page.getByRole('button')).toBeEnabled(); +await expect(page.getByRole('alert')).toBeVisible(); + +// 텍스트 검증 +await expect(page.getByRole('heading')).toHaveText('환영합니다'); + +// URL 검증 +await expect(page).toHaveURL(/.*dashboard/); + +// 다중 요소 검증 +await expect(page.getByRole('listitem')).toHaveCount(3); +``` + +## 6. 테스트 데이터 관리 + +```ts +// tests/fixtures/users.ts +export const TEST_USERS = { + valid: { + email: 'test@example.com', + password: 'password123', + }, + invalid: { + email: 'invalid@example.com', + password: 'wrong', + }, +}; +``` + +## 7. 모킹 전략 + +다음과 같은 경우에만 API 모킹을 사용합니다: + +- 외부 API 의존성이 있는 경우 +- 테스트 환경에서 실제 API가 사용 불가능한 경우 +- 특정 에러 상황 테스트가 필요한 경우 + +```ts +// 특정 에러 상황 테스트를 위한 모킹 예시 +test('API 에러 발생 시 에러 메시지 표시', async ({ page }) => { + await page.route('**/api/users', (route) => { + route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: '서버 오류가 발생했습니다.' }), + }); + }); + + await page.goto('/users'); + await expect(page.getByRole('alert')).toHaveText('서버 오류가 발생했습니다.'); +}); +``` + +## 8. 인증 상태 관리 (storageState) + +### 인증이 필요한 테스트에서 사용 + +```typescript +// tests/e2e/protected-route.spec.ts +import { test } from '@playwright/test'; + +test.use({ storageState: './tests/fixtures/auth.json' }); + +test('인증이 필요한 페이지 접근', async ({ page }) => { + await page.goto('/dashboard'); + // ... 테스트 로직 +}); +``` diff --git a/.cursor/rules/toss-frontend.mdc b/.cursor/rules/web/toss-frontend.mdc similarity index 100% rename from .cursor/rules/toss-frontend.mdc rename to .cursor/rules/web/toss-frontend.mdc diff --git a/.node-version b/.node-version index a7fef51..c519bf5 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -v23.11.1 +v24.11.0 diff --git a/CLAUDE.md b/CLAUDE.md index c1d269b..59cf9ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,141 +1,124 @@ -# Rules for Vibe Coding - -## Limitations of Vibe Coding - -1. 컨텍스트의 한계가 명확하다 -> 최대한 많은 프롬프트 주입, 한계 도달 시 컨텍스트 초기화 -2. 할루시네이션이 여전히 존재 -> 최대한 많은 지침과 컨텍스트 제공, 테스트 코드 작성 - -## Ten Commandments of Vibe Coding - -1. AI 웹 빌더를 활용한 UI 혹은 보일러플레이트 기반 코드를 사용 -2. 기술 문서/지침이 적힌 기반 문서를 사용 - 1. 주류 기술 스택 선택 - 2. `rules`, `prd.md`, `TODO.md`, `CLAUDE.md` 등 이용 - 3. 지침에는 어떤 라이브러리(모듈)를 사용할 건지/코드 작성 방식 등 사용자가 원하는 대로 작성 (자세할 수록 좋음) -3. 계획 수립 - 1. AI(GPT/Claude/Gemini/Grok 등)를 사용하여 서비스 기능 구현 계획 수립 - 2. 쉬운 기능부터 어려운 기능까지 우선 순위를 매기기 -4. 한 작업을 세부 작업으로 분리 후 세부 작업만 시키기 - 1. Cursor 새로운 채팅/탭 기능 이용, Cursor Memory 활용 - 2. 기능을 작은 작업 단위로 분할하여 점진적 구현 -5. 최대한 많은 컨텍스트(문맥) 주입 - 1. @ 멘션 기능 활용 - 2. 최신 정보 주입 - 1. context7 MCP, Cursor Docs 사용 - 2. @Web 웹 검색 활용 -6. 프롬프트를 최대한 자세하게 작성 -7. Git을 이용한 버전 관리(코드를 백업) -8. 테스트 코드 작성 (TDD) -9. 주기적으로 코드 리팩토링 - -## General Coding Guidance - -- 생성하는 모든 코드는 명확하고, 간결하며, 유지보수 가능해야 합니다. -- 복잡한 로직은 단순화하고 불필요한 추상화는 피하십시오. 꼭 필요한 경우에만 도입하여 복잡성을 관리하십시오. -- 모든 공개 API 및 복잡한 로직에는 명확한 주석(예: JSDoc, Python Docstrings)을 작성하십시오. 코드의 의도와 작동 방식을 설명해야 합니다. -- 오류 처리는 항상 명시적으로 수행하며, 사용자에게 친화적인 오류 메시지를 제공하십시오. 단순한 try-catch 블록으로 오류를 숨기지 마십시오. -- 보안을 최우선으로 고려하여 코드를 작성하십시오. 잠재적인 취약점(예: SQL 인젝션, XSS, API 키 노출)을 방지하기 위한 검증 및 이스케이프 처리를 철저히 하십시오. -- 성능을 고려하여 효율적인 알고리즘과 데이터 구조를 선택하십시오. 불필요한 반복이나 계산을 피하십시오. - -## Test-Driven Development (TDD) - -- 기능 구현 요청 시, 먼저 해당 기능의 요구사항을 충족하는 포괄적인 단위 테스트 케이스를 작성하십시오. (필요시 통합 테스트 포함) -- 테스트 케이스는 긍정적 케이스, 부정적 케이스, 엣지 케이스를 모두 포함해야 합니다. -- 테스트 케이스 작성 후, 해당 테스트를 통과하는 최소한의 코드를 구현하십시오. -- 코드 구현 후 모든 테스트를 실행하고, 실패하는 테스트가 있다면 해당 테스트를 통과하도록 코드를 수정하십시오. 이 과정을 모든 테스트가 통과할 때까지 반복하십시오. 각 수정 시도 전, 웹 검색 또는 공식 문서 (context7 mcp 활용 등) 조사를 필수로 수행해야 합니다. -- 최대 3회를 초과하여 동일한 오류 수정 루프에 빠지지 않도록 주의하고, 해결이 어려울 경우 사용자에게 도움을 요청하십시오. -- 사용자로부터 테스트 실패 로그가 제공되면, 해당 로그를 분석하여 문제의 원인을 파악하고 코드를 수정하십시오. -- 생성된 테스트 코드는 사람이 쉽게 이해하고 유지보수할 수 있도록 명확하게 작성하십시오. - -## Feature Implementation Workflow - -기능을 구현할 때는 **반드시** 다음의 조건과 단계를 따릅니다: - -1. **계획 수립 및 검토:** - -- 요구사항 분석을 바탕으로 구체적인 구현 계획을 세웁니다. -- 수립된 계획을 사용자에게 제시하고, 진행 전에 반드시 검토와 승인을 받습니다. - -2. **단계적 구현 및 검증:** - -- 기능 구현 과정을 논리적인 작은 단위로 세분화하여 단계적으로 진행합니다. -- 각 단계의 핵심 로직에는 서버 및 클라이언트 환경 모두에 로그(예: `console.group`, `console.log`)를 추가합니다. - - 로그는 기능의 정상 작동 여부를 확인하고, 잠재적인 문제를 조기에 발견하여 디버깅하는 데 활용됩니다. - - 구현이 완료되고 안정화된 후에는 디버깅 목적의 로그는 제거하거나, 필요한 경우 최소한으로 유지하는 것을 고려합니다. -- 각 단계 구현 후에는 충분한 테스트와 검증을 통해 의도한 대로 작동하는지 확인합니다. - -## Context Management - -- 각 코드 파일의 첫 100줄 이내에 해당 파일의 기능, 구현 로직, 주요 의존성을 명확히 문서화하세요. 이는 AI가 파일을 빠르고 정확하게 이해하는 데 도움을 줍니다. 아래 예시 형식을 따르십시오: - - ```tsx - /** - * @file UserProfile.tsx - * @description 사용자 프로필 페이지 컴포넌트 - * - * 이 컴포넌트는 사용자의 프로필 정보를 표시하고 수정하는 기능을 제공합니다. - * - * 주요 기능: - * 1. 사용자 기본 정보 표시 (이름, 이메일, 프로필 이미지) - * 2. 프로필 정보 수정 - * 3. 프로필 이미지 업로드 - * - * 핵심 구현 로직: - * - Supabase Auth를 통한 사용자 인증 상태 확인 - * - React Query를 사용한 프로필 데이터 fetching 및 Caching - * - 이미지 업로드를 위한 Supabase Storage 활용 (클라이언트 측 직접 업로드) - * - Form 상태 관리를 위한 React Hook Form 사용 (유효성 검사 포함) - * - * @dependencies - * - @supabase/ssr: 인증 및 서버 사이드 Supabase 클라이언트 - * - @tanstack/react-query: 데이터 동기화 및 캐싱 - * - react-hook-form: 폼 관리 및 유효성 검사 - * - @heroicons/react: UI 아이콘 - * - * @see {@link /docs/user-profile-design.md} - 관련 디자인 문서 - * @see {@link /docs/api/users.md#PUT /api/users/:id} - 프로필 업데이트 API 명세 - */ - - import { useEffect } from "react"; - import { useForm } from "react-hook-form"; - import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; // QueryClient 추가 - import { createClient } from "@/utils/supabase/client"; - import { UserCircleIcon } from "@heroicons/react/24/outline"; - - // ... 컴포넌트 구현 ... - ``` - -- 기능은 논리적으로 분리된 작은 모듈 또는 패키지로 구성하십시오. 단일 파일이 500 라인을 넘지 않도록 노력하고, 필요시 파일을 분리하는 것을 적극적으로 제안하십시오. -- 새로운 기능을 구현할 때, 기존 프로젝트의 디렉토리 구조와 네이밍 컨벤션을 철저히 준수하십시오. -- 파일 및 디렉토리 이름은 해당 내용물의 기능을 명확하게 나타내도록 작성하십시오. (예: `user-auth-service.ts`, `product-display-component.tsx`) -- 전체 컨텍스트가 주어질 경우, 주요 파일 및 사용자 지침에 따라 필요한 정보를 요약 및 활용하되, 중요한 세부사항(특히 사용자가 명시한 제약 조건이나 우선순위)을 놓치지 않도록 주의하십시오. -- 프로젝트 루트의 `.cursor` 폴더에 위치한 디자인 문서, 작업 체크리스트(`todo.md`), 제품 요구사항 문서(`prd.md`), 추가 규칙 파일들을 항상 최우선으로 참조하여 작업의 일관성과 정확성을 유지하십시오. -- 프로젝트의 상태와 구조, 주요 결정 사항을 `README.md`와 같은 중앙 문서에 정기적으로 문서화하도록 사용자에게 상기시키거나, 직접 초안을 작성하여 제안하십시오. - -## Refactoring (리팩토링) - -- 리팩토링은 점진적으로, 작은 단위로 수행하십시오. 대규모의 전면적인 변경은 사용자에게 항상 확인받고 진행하십시오. -- 기존 코드의 스타일과 패러다임을 일관되게 유지하십시오. 새로운 라이브러리나 스타일을 도입해야 할 경우, 반드시 사용자에게 명시적인 확인과 승인을 요청하십시오. -- 불필요한 추상화 계층을 만들지 마십시오. 코드는 가능한 단순하게 유지하되, 명확성을 희생해서는 안 됩니다. 모든 추상화는 그 필요성을 명확히 설명할 수 있어야 합니다. -- 리팩토링 전후로 반드시 테스트를 실행하여 기존 기능이 손상되지 않았는지 확인하십시오. 필요한 경우, 리팩토링 범위에 맞춰 테스트 케이스를 업데이트하거나 추가 작성하십시오. -- 사용자로부터 '잘못된 리팩토링 함정'(예: 비즈니스 컨텍스트 미이해, 과도한 통합, 성능 저하 유발)에 대한 지적을 받으면, 해당 피드백을 최우선으로 반영하여 수정하고, 수정 내용을 사용자에게 명확히 설명하십시오. - -## User Instructions (사용자 지침 준수) - -- 사용자의 요청이 모호하거나 불완전하다고 판단되면, 주저하지 말고 명확화를 위한 구체적인 질문을 하십시오. 추측에 기반하여 코드를 생성하지 마십시오. -- 사용자가 '아이디어 구체화' 또는 '계획 수립' 단계를 요청하면, 체계적인 질문(예: 목표, 주요 기능, 사용자, 기술 제약)을 통해 상세한 명세(`.cursor/prd.md`) 또는 실행 가능한 작업 목록(`.cursor/todo.md`)을 도출하도록 적극적으로 지원하십시오. -- 사용자가 '일회용 코드' 또는 '학습 목적의 코드'를 요청할 경우, 유지보수성보다는 기능 구현 및 개념 설명에 더 중점을 둘 수 있습니다. 단, 이 경우에도 기본적인 코드 품질(가독성, 보안 기초)은 유지하고, 해당 코드의 한계를 사용자에게 명확히 고지하십시오. -- 사용자가 제공하는 `prd.md`, `todo.md`, `README.md` 등의 문서는 작업의 핵심적인 가이드라인이므로 반드시 숙지하고 철저히 따르십시오. 내용이 상충되거나 불분명한 경우 즉시 사용자에게 확인 요청하십시오. - -## Automation (자동화) - -- 품질 검사 도구(린터, 정적 분석기 등)에서 오류나 경고가 발생하면, 해당 내용을 분석하여 코드를 자동으로 수정하십시오. 이 과정에서 오류/경고가 없을 때까지 반복할 수 있으나, 각 수정 시도 전, 해당 오류/경고에 대한 웹 검색 또는 공식 문서(context7 mcp 활용 ) 조사를 필수로 수행하여 최적의 해결책을 적용해야 합니다. -- 최대 3회를 초과하여 동일한 오류 수정 루프에 빠지지 않도록 주의하고, 해결이 어려울 경우 사용자에게 현재까지의 시도와 문제점을 요약하여 보고하고 도움을 요청하십시오. - -## Supporting (사용자 지원) - -- 사용자가 기술 용어나 개발 프로세스에 익숙하지 않을 수 있음을 인지하고, 가능한 쉬운 용어로 설명하거나, 필요한 경우 비유나 구체적인 예시를 들어 추가적인 설명을 제공하십시오. -- 사용자가 문제 정의(PRD, 와이어프레임)를 명확히 하도록 돕고, 이를 기반으로 신속하게 프로토타입을 생성하여 시각적인 피드백 루프를 형성하고 사용자의 아이디어를 구체화하는 데 기여하십시오. -- 사용자가 'AI가 코드를 잘 작성하도록 프롬프팅'하는 방법을 문의하면, 명확한 역할 부여, 충분한 컨텍스트 제공, 구체적인 작업 지시의 중요성을 설명하고, 효과적인 프롬프트 패턴 예시를 제공하십시오. -- 사용자가 '내가 더 똑똑해지기'를 원할 경우, 관련 기술 키워드, 데이터 흐름, 실행 환경, 아키텍처 패턴 등에 대한 학습 자료(요약, 링크 등)를 제공하거나 관련 질문에 상세히 답변하여 사용자의 이해를 돕고 성장을 지원하십시오. -- 사용자가 '기능이 스펙대로 동작한다'는 목표를 달성하도록, 테스트 코드 작성의 중요성과 방법, 테스트 실행 및 결과 해석 과정을 명확히 안내하고, 버전 관리 시스템(예: Git)을 통한 커밋(저장)의 중요성과 기본 워크플로우를 설명하십시오. +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a URL shortening service built with Next.js 15, TypeScript, and Supabase. It features user authentication with an approval system, link management, and analytics tracking. + +## Common Development Commands + +```bash +# Development +pnpm dev # Start development server with Turbopack + +# Build & Production +pnpm build # Build for production +pnpm start # Start production server + +# Testing & Quality +pnpm test # Run tests with Vitest +pnpm lint # Run Next.js linter + +# Database Operations +pnpm run gen:types # Generate TypeScript types from Supabase schema +pnpm run db:push # Push migrations to Supabase +pnpm run db:pull # Pull schema from Supabase + +# Admin Setup +pnpm run seed:admin # Interactive script to create admin user +``` + +## High-Level Architecture + +### Tech Stack +- **Framework**: Next.js 15 with App Router +- **Language**: TypeScript +- **Styling**: Tailwind CSS v4 (using globals.css, no config file) +- **UI Components**: shadcn/ui components +- **Database**: Supabase (PostgreSQL) +- **Authentication**: Supabase Auth with SSR +- **Package Manager**: pnpm + +### Directory Structure + +``` +src/ +├── app/ # Next.js App Router pages +│ ├── admin/ # Admin dashboard (protected) +│ ├── [slug]/ # Dynamic redirect handler +│ └── auth/ # Auth callback routes +├── features/ # Feature-based modules +│ ├── auth/ # Authentication logic & components +│ ├── links/ # Link management +│ └── analytics/ # Analytics features +├── shared/ # Shared resources +│ ├── components/ui/ # shadcn/ui components +│ └── types/ # TypeScript definitions +├── lib/ # External library configurations +│ └── supabase/ # Supabase client setup +└── middleware.ts # Route protection & auth +``` + +### Key Architectural Patterns + +1. **Feature-Based Organization**: Code is organized by feature (auth, links, analytics) rather than by type +2. **Server Components First**: Prioritize React Server Components, use `'use client'` only when necessary +3. **Server Actions**: Use Server Actions instead of API routes for data mutations +4. **Type Safety**: All database types are auto-generated from Supabase schema + +### Authentication Flow + +1. **User Registration**: New users register and enter "pending" status +2. **Admin Approval**: Admins review and approve/reject users +3. **Role-Based Access**: Users have roles (user/admin) with different permissions +4. **Middleware Protection**: Routes are protected at the middleware level + +### Database Schema + +Key tables: +- `profiles`: User profiles with role and approval status +- `links`: Shortened URLs with click tracking +- `link_clicks`: Individual click events with metadata + +### Environment Variables + +Required variables: +``` +NEXT_PUBLIC_BASE_URL +NEXT_PUBLIC_SUPABASE_URL +NEXT_PUBLIC_SUPABASE_ANON_KEY +SUPABASE_SERVICE_ROLE_KEY (for admin functions) +``` + +## Coding Conventions + +1. **File Naming**: Use kebab-case for all files (e.g., `user-management-table.tsx`) +2. **Component Naming**: PascalCase for components, file names still kebab-case +3. **Functions/Variables**: camelCase with TypeScript types +4. **Imports**: Prefer named exports +5. **Icons**: Use lucide-react exclusively +6. **Styling**: Tailwind CSS only, no CSS modules or styled-components + +## Important Implementation Details + +1. **Supabase Clients**: + - Use `createClient()` from `/lib/supabase/client.ts` for client components + - Use `createServerClient()` from `/lib/supabase/server.ts` for server components + +2. **ID Generation**: Uses Snowflake algorithm for unique link IDs + +3. **Middleware**: Handles auth checks and redirects based on user status + +4. **Admin Features**: Located in `/app/admin/*` with role-based protection + +5. **Testing**: Vitest is configured with React Testing Library + +## Development Workflow + +1. Always check existing patterns in the codebase before implementing new features +2. Use Server Components by default, add 'use client' only when needed +3. Follow the feature-based directory structure +4. Generate types after database changes: `pnpm run gen:types` +5. Test with `pnpm test` before committing +6. Run `pnpm lint` to ensure code quality \ No newline at end of file diff --git a/package.json b/package.json index 1b88770..803054b 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,9 @@ "input-otp": "^1.4.2", "lucide-react": "^0.513.0", "nanoid": "^5.1.5", - "next": "15.3.0", + "next": "15.4.6", "next-themes": "^0.4.6", + "ogl": "^1.0.11", "react": "^19.0.0", "react-chartjs-2": "^5.3.0", "react-day-picker": "^9.7.0", diff --git a/prompts/20250809_163750.md b/prompts/20250809_163750.md new file mode 100644 index 0000000..2e2a6e3 --- /dev/null +++ b/prompts/20250809_163750.md @@ -0,0 +1,24 @@ +seungwonan + +┌ jsrepo v2.4.5 +│ +◇ You don't have jsrepo initialized in your project. Do you want to continue? +│ Yes +│ +◇ Retrieved blocks from https://reactbits.dev/ts/tailwind/ +│ +◇ Where would you like to add Backgrounds? +│ Threads +│ +◇ Include tests? +│ No +│ +◇ Add watermark? +│ No +│ +◇ What formatter would you like to use? +│ Prettier +Cannot resolve `"Backgrounds": "Threads"` from paths because we couldn't find a matching alias in the tsconfig. If you intended to use a relative path ensure that your path starts with `./`. + + +무슨 에러야 ? diff --git a/prompts/20250809_163906.md b/prompts/20250809_163906.md new file mode 100644 index 0000000..9b25882 --- /dev/null +++ b/prompts/20250809_163906.md @@ -0,0 +1,29 @@ +seungwonan + +┌ jsrepo v2.4.5 +│ +◇ You don't have jsrepo initialized in your project. Do you want to continue? +┌ jsrepo v2.4.5 +│ +┌ jsrepo v2.4.5 +│ +◇ You don't have jsrepo initialized in your project. Do you want to continue? +│ Yes +│ +◇ Retrieved blocks from https://reactbits.dev/ts/tailwind/ +│ +◇ Where would you like to add Backgrounds? +│ ./src/app/page.tsx +│ +◇ Include tests? +│ No +│ +◇ Add watermark? +│ No +│ +◇ What formatter would you like to use? +│ Prettier +│ +▲ Something went wrong + +뭐가 문제야? diff --git a/prompts/20250809_164053.md b/prompts/20250809_164053.md new file mode 100644 index 0000000..8855990 --- /dev/null +++ b/prompts/20250809_164053.md @@ -0,0 +1,3 @@ +seungwonan + +@src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx 이거 써줘 메인페이지에 diff --git a/prompts/20250809_164207.md b/prompts/20250809_164207.md new file mode 100644 index 0000000..475402e --- /dev/null +++ b/prompts/20250809_164207.md @@ -0,0 +1,126 @@ +seungwonan + +Error: ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx:1:17 +Ecmascript file had an error +> 1 | import React, { useEffect, useRef } from "react"; + | ^^^^^^^^^ + 2 | import { Renderer, Program, Mesh, Triangle, Color } from "ogl"; + 3 | + 4 | interface ThreadsProps { + +You're importing a component that needs `useEffect`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the `"use client"` directive. + + Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client + +Import trace: + ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx + ./src/app/page.tsx + at tr (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:20168:60) + at o6 (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:2952:164) + at iP (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:4008:32) + at i$ (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:4305:28) + at sv (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:5791:21) + at http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:5782:40 + at sm (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:5783:19) + at sa (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:5596:23) + at sZ (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:6520:124) + at MessagePort._ (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_next-devtools_index_a92e453b.js:8806:49)> demo-link@0.1.0 dev /Users/seungwonan/Dev/1-project/demodev/demo-res/demo-link +> next dev --turbopack + + ▲ Next.js 15.4.6 (Turbopack) + - Local: http://localhost:3000 + - Network: http://192.168.0.4:3000 + - Environments: .env.local + + ✓ Starting... + ✓ Compiled middleware in 157ms + ✓ Ready in 1194ms +Middleware - Path: / User: none + ○ Compiling / ... + ✓ Compiled / in 1596ms + ⨯ ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx:1:17 +Ecmascript file had an error +> 1 | import React, { useEffect, useRef } from "react"; + | ^^^^^^^^^ + 2 | import { Renderer, Program, Mesh, Triangle, Color } from "ogl"; + 3 | + 4 | interface ThreadsProps { + +You're importing a component that needs `useEffect`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the `"use client"` directive. + + Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client + + + +Import trace: + ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx + ./src/app/page.tsx + + + +./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx:1:28 +Ecmascript file had an error +> 1 | import React, { useEffect, useRef } from "react"; + | ^^^^^^ + 2 | import { Renderer, Program, Mesh, Triangle, Color } from "ogl"; + 3 | + 4 | interface ThreadsProps { + +You're importing a component that needs `useRef`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the `"use client"` directive. + + Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client + + + +Import trace: + ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx + ./src/app/page.tsx + + + ○ Compiling /_error ... + ✓ Compiled /_error in 554ms + GET / 500 in 2255ms +Middleware - Path: / User: none + ⨯ ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx:1:17 +Ecmascript file had an error +> 1 | import React, { useEffect, useRef } from "react"; + | ^^^^^^^^^ + 2 | import { Renderer, Program, Mesh, Triangle, Color } from "ogl"; + 3 | + 4 | interface ThreadsProps { + +You're importing a component that needs `useEffect`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the `"use client"` directive. + + Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client + + + +Import trace: + ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx + ./src/app/page.tsx + + + +./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx:1:28 +Ecmascript file had an error +> 1 | import React, { useEffect, useRef } from "react"; + | ^^^^^^ + 2 | import { Renderer, Program, Mesh, Triangle, Color } from "ogl"; + 3 | + 4 | interface ThreadsProps { + +You're importing a component that needs `useRef`. This React Hook only works in a Client Component. To fix, mark the file (or its parent) with the `"use client"` directive. + + Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client + + + +Import trace: + ./src/shared/components/ui/backgrounds.tsx/Threads/Threads.tsx + ./src/app/page.tsx + + + GET / 500 in 29ms + ✓ Compiled /favicon.ico in 159ms + GET /favicon.ico 500 in 170ms + diff --git a/prompts/20250809_164236.md b/prompts/20250809_164236.md new file mode 100644 index 0000000..4c15e7a --- /dev/null +++ b/prompts/20250809_164236.md @@ -0,0 +1,3 @@ +seungwonan + +이제 어울리게 좀 더 현대적으로 디자인을 변경해줘 diff --git a/prompts/20250809_164523.md b/prompts/20250809_164523.md new file mode 100644 index 0000000..6eb11b6 --- /dev/null +++ b/prompts/20250809_164523.md @@ -0,0 +1,10 @@ +seungwonan + +@font-face { + font-family: 'Pretendard-Regular'; + src: url('https://fastly.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff'); + font-weight: 400; + font-style: normal; +} + +폰트는 프리텐다드로 diff --git a/prompts/20250809_164631.md b/prompts/20250809_164631.md new file mode 100644 index 0000000..d98c8d8 --- /dev/null +++ b/prompts/20250809_164631.md @@ -0,0 +1,3 @@ +seungwonan + +전체적으로 디자인 맞추어줘 모든 페이지 다 돌아줘 diff --git a/prompts/20250809_164642.md b/prompts/20250809_164642.md new file mode 100644 index 0000000..bfc35bc --- /dev/null +++ b/prompts/20250809_164642.md @@ -0,0 +1,4 @@ +seungwonan + +전체적으로 디자인 맞추어줘 모든 페이지 다 돌아줘 +단계적으로 추론하고 답변해 생각 많이해 diff --git a/prompts/20250809_165702.md b/prompts/20250809_165702.md new file mode 100644 index 0000000..51ae8a0 --- /dev/null +++ b/prompts/20250809_165702.md @@ -0,0 +1,19 @@ +seungwonan + +Error: NEXT_HTTP_ERROR_FALLBACK;404 + at RedirectPage (rsc://React/Server/file:///Users/seungwonan/Dev/1-project/demodev/demo-res/demo-link/.next/server/chunks/ssr/%5Broot-of-the-server%5D__8f385554._.js?8:696:373) + at resolveErrorDev (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4124:48) + at getOutlinedModel (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:3760:28) + at parseModelString (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:3866:50) + at Object. (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4431:51) + at JSON.parse () + at initializeModelChunk (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:3464:30) + at getOutlinedModel (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:3716:17) + at parseModelString (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:3913:50) + at Array. (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4431:51) + at JSON.parse () + at resolveConsoleEntry (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4269:32) + at processFullStringRow (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4368:17) + at processFullBinaryRow (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4337:9) + at processBinaryChunk (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4415:98) + at progress (http://localhost:3000/_next/static/chunks/5c233_next_dist_compiled_814ddde3._.js:4581:25) diff --git a/prompts/20250809_165740.md b/prompts/20250809_165740.md new file mode 100644 index 0000000..7fc9b1d --- /dev/null +++ b/prompts/20250809_165740.md @@ -0,0 +1,14 @@ +seungwonan + +NEXT_HTTP_ERROR_FALLBACK;404 + +src/app/[slug]/page.tsx (33:14) @ RedirectPage + + + 31 | if (!link) { + 32 | console.log(`Link not found for slug: ${slug}`); +> 33 | notFound(); + | ^ + 34 | } + 35 | + 36 | // 3. 링크가 있으면 클릭 수 증가 시도 (user agent와 IP 정보 포함) diff --git a/prompts/20251201_024017.md b/prompts/20251201_024017.md new file mode 100644 index 0000000..4a67c1c --- /dev/null +++ b/prompts/20251201_024017.md @@ -0,0 +1,3 @@ +seungwonan + +현재까지 수정한 내용 체크해줘 diff --git a/prompts/20251201_024133.md b/prompts/20251201_024133.md new file mode 100644 index 0000000..f66b740 --- /dev/null +++ b/prompts/20251201_024133.md @@ -0,0 +1,5 @@ +seungwonan + + +이 수정 사항들이 괜찮은지 면밀하게 검토해줘 +이 수정 사항들이 괜찮은지 면밀하게 검토해줘 diff --git a/prompts/20251201_024821.md b/prompts/20251201_024821.md new file mode 100644 index 0000000..68154cf --- /dev/null +++ b/prompts/20251201_024821.md @@ -0,0 +1,5 @@ +seungwonan + + +해줘 +해줘 diff --git a/prompts/20251201_025405.md b/prompts/20251201_025405.md new file mode 100644 index 0000000..3f62669 --- /dev/null +++ b/prompts/20251201_025405.md @@ -0,0 +1,131 @@ +seungwonan + + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Thin.woff2') format('woff2'); + font-weight: 100; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-ExtraLight.woff2') format('woff2'); + font-weight: 200; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Light.woff2') format('woff2'); + font-weight: 300; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Regular.woff2') format('woff2'); + font-weight: 400; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Medium.woff2') format('woff2'); + font-weight: 500; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Bold.woff2') format('woff2'); + font-weight: 700; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-ExtraBold.woff2') format('woff2'); + font-weight: 800; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Black.woff2') format('woff2'); + font-weight: 900; + font-display: swap; +} + +프리텐다드는 이걸로 +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Thin.woff2') format('woff2'); + font-weight: 100; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-ExtraLight.woff2') format('woff2'); + font-weight: 200; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Light.woff2') format('woff2'); + font-weight: 300; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Regular.woff2') format('woff2'); + font-weight: 400; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Medium.woff2') format('woff2'); + font-weight: 500; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Bold.woff2') format('woff2'); + font-weight: 700; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-ExtraBold.woff2') format('woff2'); + font-weight: 800; + font-display: swap; +} + +@font-face { + font-family: 'Pretendard'; + src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/pretendard@1.0/Pretendard-Black.woff2') format('woff2'); + font-weight: 900; + font-display: swap; +} + +프리텐다드는 이걸로 diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx index fa8a992..701aa9e 100644 --- a/src/app/admin/dashboard/page.tsx +++ b/src/app/admin/dashboard/page.tsx @@ -8,112 +8,244 @@ import { CardTitle, } from "@/shared/components/ui/card"; import { - LinkIcon, - ChartBarIcon, - UsersIcon, - CalendarIcon, - ArrowTrendingUpIcon, -} from "@heroicons/react/24/outline"; + Link2, + BarChart3, + Users, + TrendingUp, + Clock, + MousePointerClick, + Activity, + Globe, + AlertTriangle, +} from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/shared/components/ui/button"; -// 동적 렌더링 강제 설정 (Static Generation 방지) +// Force dynamic rendering for authenticated server-side data export const dynamic = "force-dynamic"; export default async function AdminDashboard() { try { - // Use centralized auth service const { profile } = await AuthService.requireAuth(); const supabase = await createClient(); - // Get statistics with error handling - const [ - { count: totalLinks, error: linksError }, - { data: recentLinks, error: recentLinksError }, - { count: totalClicks, error: clicksError }, - todayTopLinks, - weekTopLinks, - ] = await Promise.all([ - supabase.from("links").select("*", { count: "exact" }), - supabase - .from("links") - .select("*") - .order("created_at", { ascending: false }) - .limit(5), - supabase.from("link_clicks").select("*", { count: "exact" }), - LinkService.getTopClickedLinksByPeriod("today", 5), - LinkService.getTopClickedLinksByPeriod("week", 5), - ]); + // Initialize default values for graceful degradation + let totalLinks = 0; + let recentLinks: any[] = []; + let totalClicks = 0; + let todayTopLinks: any[] = []; + let weekTopLinks: any[] = []; + let pendingUsers = 0; + let hasError = false; + let errorMessage = ""; - if (linksError) console.warn("Links count fetch failed:", linksError); - if (recentLinksError) - console.warn("Recent links fetch failed:", recentLinksError); - if (clicksError) console.warn("Clicks count fetch failed:", clicksError); + try { + // Get statistics with individual error handling + const results = await Promise.allSettled([ + supabase.from("links").select("*", { count: "exact", head: true }), + supabase + .from("links") + .select("*") + .order("created_at", { ascending: false }) + .limit(5), + supabase + .from("link_clicks") + .select("*", { count: "exact", head: true }), + LinkService.getTopClickedLinksByPeriod("today", 5), + LinkService.getTopClickedLinksByPeriod("week", 5), + ]); - // Admin only stats - let pendingUsers = 0; - if (profile?.role === "admin") { - const { count, error: pendingError } = await supabase - .from("profiles") - .select("*", { count: "exact" }) - .eq("status", "pending"); + // Process results with error checking + if (results[0].status === "fulfilled") { + totalLinks = results[0].value.count || 0; + } else { + console.error("Failed to fetch total links:", results[0].reason); + } - if (pendingError) { - console.warn("Pending users count fetch failed:", pendingError); + if (results[1].status === "fulfilled") { + recentLinks = results[1].value.data || []; } else { - pendingUsers = count || 0; + console.error("Failed to fetch recent links:", results[1].reason); + } + + if (results[2].status === "fulfilled") { + totalClicks = results[2].value.count || 0; + } else { + console.error("Failed to fetch total clicks:", results[2].reason); + } + + if (results[3].status === "fulfilled") { + todayTopLinks = results[3].value || []; + } else { + console.error("Failed to fetch today's top links:", results[3].reason); + } + + if (results[4].status === "fulfilled") { + weekTopLinks = results[4].value || []; + } else { + console.error("Failed to fetch week's top links:", results[4].reason); + } + + // Admin only stats with error handling + if (profile?.role === "admin") { + try { + const { count, error } = await supabase + .from("profiles") + .select("*", { count: "exact", head: true }) + .eq("status", "pending"); + + if (error) { + console.error("Failed to fetch pending users:", error); + } else { + pendingUsers = count || 0; + } + } catch (err) { + console.error("Error fetching pending users:", err); + } + } + + // Check if any critical queries failed + const criticalFailures = results + .slice(0, 3) + .filter((result) => result.status === "rejected"); + if (criticalFailures.length > 0) { + hasError = true; + errorMessage = "일부 통계 데이터를 불러오지 못했습니다."; } + } catch (err) { + console.error("Dashboard data fetch error:", err); + hasError = true; + errorMessage = "대시보드 데이터를 불러오는 중 오류가 발생했습니다."; } + // Calculate average clicks per link + const avgClicksPerLink = + totalLinks && totalLinks > 0 + ? Math.round((totalClicks || 0) / totalLinks) + : 0; + return ( -
-

대시보드

+
+ {/* Error Alert */} + {hasError && ( + + +
+ +

{errorMessage}

+
+
+
+ )} + + {/* Welcome Header */} +
+

+ + 안녕하세요, {profile?.role === "admin" ? "관리자" : "사용자"}님! + +

+

+ 오늘의 링크 활동과 통계를 확인하세요 +

+
{/* Statistics Cards */}
- - - 총 링크 수 - + +
+ + 총 링크 +
+ +
- -
{totalLinks || 0}
+ +
{totalLinks || 0}
+

+ 생성된 단축 URL +

- - - 총 클릭 수 - + +
+ + 총 클릭 +
+ +
- -
{totalClicks || 0}
+ +
{totalClicks || 0}
+

+ 전체 방문자 수 +

+
+ + + +
+ + 평균 클릭 +
+ +
+
+ +
{avgClicksPerLink}
+

+ 링크당 평균 클릭 +

{profile?.role === "admin" && ( - - - 승인 대기 - + +
+ + 승인 대기 +
+ +
- -
{pendingUsers}
-

새로운 사용자

+ +
{pendingUsers}
+

+ 새로운 사용자 +

+ {pendingUsers > 0 && ( + + )}
)}
- {/* Links Grid */} + {/* Charts Grid */}
{/* Today's Top Links */} - +
- - - 오늘 인기 링크 - - Top 5 +
+
+ +
+
+ 오늘의 인기 링크 +

+ 실시간 TOP 5 +

+
+
@@ -122,50 +254,64 @@ export default async function AdminDashboard() { {todayTopLinks.map((link, index) => (
-
- - {index + 1} - -
-

{link.slug}

- {link.description && ( -

- {link.description} -

- )} -

- {link.original_url} +

+ {index + 1} +
+
+

/{link.slug}

+ {link.description && ( +

+ {link.description}

-
+ )}
-

- {link.period_clicks} 클릭 +

+ {link.period_clicks}

-

오늘

+

클릭

))}
) : ( -

- 오늘 클릭된 링크가 없습니다. -

+
+ +

+ 오늘 클릭된 링크가 없습니다 +

+
)}
{/* This Week's Top Links */} - +
- - - 최근 7일 인기 링크 - - Top 5 +
+
+ +
+
+ 주간 인기 링크 +

+ 최근 7일 TOP 5 +

+
+
@@ -174,46 +320,69 @@ export default async function AdminDashboard() { {weekTopLinks.map((link, index) => (
-
- - {index + 1} - -
-

{link.slug}

- {link.description && ( -

- {link.description} -

- )} -

- {link.original_url} +

+ {index + 1} +
+
+

/{link.slug}

+ {link.description && ( +

+ {link.description}

-
+ )}
-

- {link.period_clicks} 클릭 +

+ {link.period_clicks}

-

7일간

+

클릭

))}
) : ( -

- 최근 7일간 클릭된 링크가 없습니다. -

+
+ +

+ 이번 주 클릭된 링크가 없습니다 +

+
)}
{/* Recent Links */} - + - 최근 생성된 링크 +
+
+
+ +
+
+ 최근 생성된 링크 +

+ 새로 추가된 단축 URL +

+
+
+ +
{recentLinks && recentLinks.length > 0 ? ( @@ -221,12 +390,17 @@ export default async function AdminDashboard() { {recentLinks.map((link) => (
-
-

{link.slug}

+
+
+

/{link.slug}

+ + {link.click_count || 0} 클릭 + +
{link.description && ( -

+

{link.description}

)} @@ -234,36 +408,71 @@ export default async function AdminDashboard() { {link.original_url}

-
-

- {link.click_count} 클릭 -

+

- {new Date(link.created_at || "").toLocaleDateString()} + {new Date(link.created_at || "").toLocaleDateString( + "ko-KR", + { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }, + )}

))}
) : ( -

- 아직 생성된 링크가 없습니다. -

+
+ +

+ 아직 생성된 링크가 없습니다 +

+ +
)}
); } catch (error) { - console.error("AdminDashboard error:", error); + console.error("Critical dashboard error:", error); + + // Fallback UI for complete failure return ( -
-
-

오류가 발생했습니다

+
+
+

대시보드

- 대시보드를 불러오는 중 문제가 발생했습니다. + 시스템 상태를 확인 중입니다...

+ + + +
+ +

+ 대시보드를 불러올 수 없습니다 +

+

+ 시스템에 일시적인 문제가 발생했습니다. 잠시 후 다시 + 시도해주세요. +

+ +
+
+
); } diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 4f0a365..c168697 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -28,10 +28,12 @@ export default async function AdminLayout({ // Only show sidebar for approved users if (profile?.status === "approved") { return ( -
+
-
-
+
+
+
+
{children}
diff --git a/src/app/admin/login/login-client.tsx b/src/app/admin/login/login-client.tsx index 253827d..bd0bb37 100644 --- a/src/app/admin/login/login-client.tsx +++ b/src/app/admin/login/login-client.tsx @@ -9,6 +9,7 @@ import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; import { Alert, AlertDescription } from "@/shared/components/ui/alert"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"; +import { Mail, Lock, Sparkles } from "lucide-react"; export default function AdminLoginClient() { const [isLoading, setIsLoading] = useState(false); @@ -40,15 +41,20 @@ export default function AdminLoginClient() { }; return ( -
- - - - 관리자 로그인 - - - DemoDev Link 관리 시스템 - +
+ + +
+ +
+
+ + 관리자 로그인 + + + DemoDev Link 관리 시스템 + +
@@ -58,53 +64,68 @@ export default function AdminLoginClient() { )} -
+
-
-
-
+

계정이 없으신가요?{" "} - + 회원가입

- - 메인 페이지로 돌아가기 + + ← 메인 페이지로 돌아가기

diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx index 4ce862a..1ff146a 100644 --- a/src/app/admin/login/page.tsx +++ b/src/app/admin/login/page.tsx @@ -27,10 +27,17 @@ export const dynamic = "force-dynamic"; export default function AdminLoginPage() { return ( -
+
+ {/* Animated Background Gradient */} +
+ + {/* Decorative Blurs */} +
+
+
+
} > diff --git a/src/app/admin/pending/page.tsx b/src/app/admin/pending/page.tsx index 4f95062..578bcf3 100644 --- a/src/app/admin/pending/page.tsx +++ b/src/app/admin/pending/page.tsx @@ -1,23 +1,27 @@ -import { AuthService } from "@/features/auth/services/auth.service"; +import { AuthService } from '@/features/auth/services/auth.service'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, -} from "@/shared/components/ui/card"; -import { AlertCircle } from "lucide-react"; +} from '@/shared/components/ui/card'; +import { AlertCircle } from 'lucide-react'; // Force dynamic rendering since we use cookies -export const dynamic = "force-dynamic"; +export const dynamic = 'force-dynamic'; export default async function PendingApprovalPage() { // Check if user is pending - this will redirect if not - await AuthService.requireAuth({ requiredStatus: "pending" }); + await AuthService.requireAuth({ requiredStatus: 'pending' }); return ( -
- +
+
+
+
+ +
@@ -38,7 +42,7 @@ export default async function PendingApprovalPage() {
  • 문의사항은 관리자에게 연락주세요
  • - +

    가입 신청해 주셔서 감사합니다 @@ -48,4 +52,4 @@ export default async function PendingApprovalPage() {

    ); -} \ No newline at end of file +} diff --git a/src/app/admin/register/page.tsx b/src/app/admin/register/page.tsx index 40f9c31..3fc0cb9 100644 --- a/src/app/admin/register/page.tsx +++ b/src/app/admin/register/page.tsx @@ -12,6 +12,7 @@ import { } from "@/shared/components/ui/card"; import { Separator } from "@/shared/components/ui/separator"; import Link from "next/link"; +import { UserPlus, Mail, Lock, CheckCircle, Sparkles } from "lucide-react"; import { redirect } from "next/navigation"; import { AuthService } from "@/features/auth/services/auth.service"; @@ -70,12 +71,28 @@ export default async function AdminRegisterPage({ searchParams }: PageProps) { : null; return ( -
    -
    - - - 회원가입 - DemoDev Link 서비스 가입 +
    + {/* Animated Background Gradient */} +
    + + {/* Decorative Blurs */} +
    +
    + +
    + + +
    + +
    +
    + + 회원가입 + + + DemoDev Link 서비스 가입 + +
    {errorMessage && ( @@ -84,83 +101,104 @@ export default async function AdminRegisterPage({ searchParams }: PageProps) { )} -
    +
    - - + +
    + + +
    - - + +
    + + +
    - - + +
    + + +
    -
    +
    - +
    - + 가입 안내
    -
    -

    • 회원가입 후 관리자의 승인이 필요합니다.

    -

    • 승인까지 보통 1-2일이 소요됩니다.

    -

    • 승인 완료 시 이메일로 알림을 받습니다.

    +
    +

    + + 회원가입 후 관리자의 승인이 필요합니다. +

    +

    + + 승인까지 보통 1-2일이 소요됩니다. +

    +

    + + 승인 완료 시 이메일로 알림을 받습니다. +

    -
    +

    이미 계정이 있으신가요?{" "} 로그인

    - - 메인 페이지로 돌아가기 + + ← 메인 페이지로 돌아가기

    diff --git a/src/app/admin/rejected/page.tsx b/src/app/admin/rejected/page.tsx index 16612b9..ec75c85 100644 --- a/src/app/admin/rejected/page.tsx +++ b/src/app/admin/rejected/page.tsx @@ -13,9 +13,13 @@ export default async function RejectedPage() { const { profile } = await AuthService.requireAuth({ requiredStatus: "rejected" }); return ( -
    -
    -
    +
    +
    +
    +
    + +
    +
    diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx new file mode 100644 index 0000000..7bfb7ac --- /dev/null +++ b/src/app/admin/settings/page.tsx @@ -0,0 +1,366 @@ +import { createClient } from "@/lib/supabase/server"; +import { redirect } from "next/navigation"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"; +import { Button } from "@/shared/components/ui/button"; +import { Switch } from "@/shared/components/ui/switch"; +import { Label } from "@/shared/components/ui/label"; +import { Input } from "@/shared/components/ui/input"; +import { Textarea } from "@/shared/components/ui/textarea"; +import { + Bell, + Shield, + Globe, + Database, + Mail, + Key, + Server, + Palette, + Save +} from "lucide-react"; + +export default async function SettingsPage() { + const supabase = await createClient(); + + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + redirect("/admin/login"); + } + + // Check if user is admin + const { data: profile } = await supabase + .from("profiles") + .select("role") + .eq("id", user.id) + .single(); + + if (profile?.role !== "admin") { + redirect("/admin/dashboard"); + } + + return ( +
    + {/* Page Header */} +
    +

    설정

    +

    + 시스템 설정을 관리하고 서비스를 구성합니다 +

    +
    + +
    + {/* General Settings */} + + +
    +
    + +
    +
    + 일반 설정 + 서비스 기본 설정을 관리합니다 +
    +
    +
    + +
    + + +
    +
    + +