diff --git a/README.md b/README.md index aaff1ee..042c7b9 100644 --- a/README.md +++ b/README.md @@ -1,169 +1,236 @@ -# Project +# 프로젝트 잼 (Project Jam) -프로젝트 잼 - 함께 만들어가는 사이드 프로젝트 +함께 만들어가는 사이드 프로젝트 매칭 플랫폼 🚀 + +> **"아이디어는 있지만 팀원이 없다?"** 프로젝트 잼에서 완벽한 팀원을 찾아보세요! ## 📋 목차 +- [프로젝트 소개](#-프로젝트-소개) +- [주요 기능](#-주요-기능) - [기술 스택](#-기술-스택) - [프로젝트 구조](#-프로젝트-구조) - [FSD 아키텍처](#-fsd-아키텍처) -- [코드 품질 관리](#-코드-품질-관리) - [개발 환경 설정](#-개발-환경-설정) - [브랜치 전략 & CI/CD](#-브랜치-전략--cicd) - [커밋 컨벤션](#-커밋-컨벤션) - [개발 가이드라인](#-개발-가이드라인) -## 🛠 기술 스택 +## 🎯 프로젝트 소개 + +프로젝트 잼은 **사이드 프로젝트를 함께 진행할 팀원을 찾는 플랫폼**입니다. + +### 💡 핵심 가치 +- **매칭**: 기술 스택과 관심사가 맞는 개발자 연결 +- **협업**: 체계적인 프로젝트 관리와 소통 지원 +- **성장**: 실무 경험을 통한 개발 역량 향상 + +### 🎯 타겟 사용자 +- 사이드 프로젝트 아이디어는 있지만 팀원이 필요한 개발자 +- 새로운 기술을 배우며 실무 경험을 쌓고 싶은 개발자 +- 포트폴리오 프로젝트를 함께 만들고 싶은 개발자 + +## ✨ 주요 기능 + +### 🔍 프로젝트 탐색 +- **스마트 검색**: 기술 스택, 포지션, 카테고리별 필터링 +- **실시간 검색**: 즉석 결과 표시 및 검색 기록 관리 +- **페이지네이션**: 부드러운 스크롤과 함께하는 페이지 이동 -### Core -- **React 19** - UI 라이브러리 -- **TypeScript 5** - 타입 시스템 -- **Vite** - 빌드 도구 +### 📝 프로젝트 관리 +- **4단계 등록**: 직관적인 프로젝트 생성 프로세스 +- **상세 정보**: 기술 스택, 일정, 팀 구성, 요구사항 등 +- **실시간 수정**: 프로젝트 정보 동적 업데이트 -### Code Quality -- **ESLint** - 코드 품질 검사 -- **Prettier** - 코드 포매팅 -- **Husky** - Git Hooks 관리 -- **lint-staged** - 스테이지된 파일만 검사 -- **Commitlint** - 커밋 메시지 컨벤션 +### 👥 팀원 매칭 +- **지원 시스템**: 원클릭 프로젝트 지원 및 관리 +- **이메일 연동**: EmailJS를 통한 직접 소통 지원 +- **프로필 시스템**: 개발자 경력 및 관심사 표시 -### Architecture -- **FSD (Feature-Sliced Design)** - 프로젝트 아키텍처 -- **Path Alias** - 절대 경로 import +### 🎨 사용자 경험 +- **반응형 디자인**: 모바일부터 데스크톱까지 완벽 지원 +- **다크/라이트 테마**: 사용자 선호에 따른 테마 전환 +- **접근성**: ARIA 표준 준수 및 키보드 네비게이션 + +## 🛠 기술 스택 + +### Frontend Core +- **React 19** - 최신 UI 라이브러리 + Concurrent Features +- **TypeScript 5** - 강타입 시스템으로 안정성 확보 +- **Vite** - 초고속 빌드 및 HMR 지원 +- **Material-UI (MUI)** - 일관된 디자인 시스템 + +### State Management & Data Fetching +- **Zustand** - 가볍고 직관적인 상태 관리 +- **TanStack Query (React Query)** - 서버 상태 관리 및 캐싱 +- **React Hook Form** - 고성능 폼 상태 관리 + +### Backend & Infrastructure +- **Firebase** - 실시간 데이터베이스 및 인증 + - Authentication (Google, GitHub) + - Firestore Database + - Storage +- **EmailJS** - 클라이언트 사이드 이메일 발송 +- **Vercel** - 자동 배포 및 호스팅 + +### Code Quality & DX +- **ESLint + Prettier** - 코드 품질 및 스타일 일관성 +- **Husky + lint-staged** - Git Hooks를 통한 자동 검증 +- **Commitlint** - 커밋 메시지 컨벤션 강제 +- **TypeScript Strict Mode** - 엄격한 타입 검사 + +### Architecture & Patterns +- **FSD (Feature-Sliced Design)** - 확장 가능한 프로젝트 구조 +- **Compound Component Pattern** - 재사용 가능한 UI 컴포넌트 +- **Custom Hooks Pattern** - 비즈니스 로직 분리 ## 📁 프로젝트 구조 ``` src/ -├── app/ # 앱 전체 설정 및 진입점 -│ ├── configs/ # 환경 설정 -│ ├── entry/ # 앱 진입점 -│ ├── routes/ # 라우팅 설정 -│ └── styles/ # 글로벌 스타일 -├── entities/ # 도메인 엔티티 (Read 로직) -├── features/ # 기능 단위 (CUD 로직) -├── pages/ # 페이지 컴포넌트 -├── shared/ # 공통 유틸리티 -└── widgets/ # 복합 컴포넌트 +├── app/ # 🏛️ 앱 전체 설정 및 진입점 +│ ├── configs/ # 환경 설정 (Vite, Firebase) +│ ├── entry/ # 앱 진입점 (main.tsx) +│ ├── routes/ # 라우팅 설정 (Auth, Layout) +│ └── styles/ # 글로벌 스타일 & 테마 +├── widgets/ # 🧩 복합 UI 컴포넌트 +│ ├── Header/ # 네비게이션 헤더 +│ ├── Footer/ # 푸터 +│ ├── hero/ # 메인 히어로 섹션 +│ └── BackToHome/ # 홈 이동 버튼 +├── pages/ # 📄 페이지 컴포넌트 (8개) +│ ├── home/ # 메인 홈페이지 +│ ├── project-list/ # 프로젝트 목록 & 검색 +│ ├── project-detail/ # 프로젝트 상세보기 +│ ├── project-insert/ # 프로젝트 등록 (4단계) +│ ├── user-profile/ # 사용자 프로필 +│ ├── login/ # 로그인 +│ ├── signup/ # 회원가입 +│ └── not-found/ # 404 페이지 +├── features/ # ⚡ 비즈니스 기능 (CUD 로직) +│ ├── auth/ # 소셜 로그인 & 로그아웃 +│ ├── projects/ # 프로젝트 CRUD & 좋아요 +│ └── email/ # 이메일 발송 시스템 +├── entities/ # 📊 도메인 엔티티 (Read 로직) +│ ├── projects/ # 프로젝트 조회 & 표시 +│ ├── search/ # 검색 & 필터링 +│ └── user/ # 사용자 정보 표시 +└── shared/ # 🔧 공통 유틸리티 + ├── api/ # API 클라이언트 + ├── hooks/ # 공통 커스텀 훅 + ├── stores/ # Zustand 스토어 + ├── types/ # 공통 타입 정의 + ├── ui/ # 재사용 UI 컴포넌트 + └── libs/ # 유틸리티 함수 ``` -### 폴더별 표준 구조 +### 📊 주요 도메인 모델 -각 도메인 폴더(`entities`, `features`)는 다음과 같은 표준 구조를 따릅니다: - -``` -domain-name/ -├── api/ # API 요청 로직 -├── hooks/ # 커스텀 훅 -├── queries/ # React Query 설정 -├── types/ # TypeScript 타입 정의 -├── ui/ # UI 컴포넌트 -├── libs/ # 유틸리티 함수 -└── index.ts # 외부 노출 인터페이스 -``` +- **프로젝트 도메인**: 제목, 설명, 기술스택, 카테고리, 포지션, 일정 등 +- **사용자 도메인**: 이름, 이메일, 역할, 경험, 좋아요/지원 프로젝트 등 +- **검색 도메인**: 카테고리, 상태, 워크플로우, 포지션, 정렬 기준 등 ## 🏗 FSD 아키텍처 -### 계층별 역할 +### 계층별 역할 및 실제 구현 1. **App Layer** (`src/app/`) - - 앱 전체 설정 및 초기화 - - 글로벌 프로바이더, 라우터 설정 + - 🔧 **설정**: Firebase, React Query, MUI Theme + - 🛣️ **라우팅**: Private Routes, Auth Guards + - 🎨 **글로벌 스타일**: CSS Variables, 폰트 설정 2. **Pages Layer** (`src/pages/`) - - 각 페이지별 컴포넌트 - - 라우팅과 연결되는 최상위 페이지 + - 📄 **페이지 조합**: 8개 주요 페이지 + - 🔗 **라우팅 연결**: URL 파라미터 처리 + - 📱 **레이아웃**: 페이지별 고유 레이아웃 3. **Widgets Layer** (`src/widgets/`) - - 여러 features를 조합한 복합 컴포넌트 - - 페이지의 큰 섹션 단위 + - 🧩 **복합 컴포넌트**: Header, Footer, Hero + - 📦 **재사용 섹션**: 여러 페이지에서 공통 사용 4. **Features Layer** (`src/features/`) - - **CUD 로직** (Create, Update, Delete) - - 사용자 인터랙션이 포함된 기능 - - 상태 변경을 담당 + - ⚡ **CUD 로직**: 프로젝트 생성/수정/삭제, 좋아요 + - 🔐 **인증**: 소셜 로그인, 로그아웃 + - 📧 **이메일**: EmailJS 연동 메시지 발송 5. **Entities Layer** (`src/entities/`) - - **Read 로직** (조회, 표시) - - 도메인 모델과 읽기 전용 로직 - - 비즈니스 엔티티 표현 + - 📊 **Read 로직**: 프로젝트 조회, 검색, 사용자 표시 + - 🎨 **UI 컴포넌트**: 카드, 리스트, 상세 정보 + - 🔍 **검색 시스템**: 필터링, 페이지네이션 6. **Shared Layer** (`src/shared/`) - - 공통 유틸리티, UI 컴포넌트 - - 모든 계층에서 사용 가능 + - 🔧 **공통 도구**: API 클라이언트, 유틸리티 + - 💾 **상태 관리**: Zustand 스토어 + - 🎨 **UI 킷**: 버튼, 모달, 스피너 등 -### 의존성 규칙 +### 의존성 규칙 시각화 ``` -App ← Pages ← Widgets ← Features ← Entities ← Shared -``` - -- 하위 계층은 상위 계층을 참조할 수 없습니다 -- 같은 계층 내 다른 모듈 간 직접 참조는 금지됩니다 -- 같은 모듈 내에서는 자유롭게 참조 가능합니다 - -## 🔍 코드 품질 관리 - -### ESLint 규칙 - -- **FSD 아키텍처 강제**: 계층별 참조 제한 -- **TypeScript 엄격 모드**: 명시적 함수 반환 타입 필수 -- **React 모범 사례**: Hooks 규칙, JSX 접근성 -- **Import 정리**: 절대/상대 경로 규칙 - -### Prettier 설정 - -- **세미콜론**: 필수 -- **따옴표**: 큰따옴표 사용 -- **Trailing Comma**: 항상 추가 -- **들여쓰기**: 2칸 스페이스 - -### Path Alias - -```typescript -// tsconfig.app.json, vite.config.ts, eslint.config.mjs에 설정 -"@app/*": ["./src/app/*"] -"@pages/*": ["./src/pages/*"] -"@widgets/*": ["./src/widgets/*"] -"@features/*": ["./src/features/*"] -"@entities/*": ["./src/entities/*"] -"@shared/*": ["./src/shared/*"] +┌─────────────────────────────────────────┐ +│ App Layer (설정, 라우팅, 글로벌 스타일) │ +└─────────────────┬───────────────────────┘ + │ ✅ 참조 가능 +┌─────────────────▼───────────────────────┐ +│ Pages (홈, 프로젝트, 프로필, 로그인 등) │ +└─────────────────┬───────────────────────┘ + │ ✅ 참조 가능 +┌─────────────────▼───────────────────────┐ +│ Widgets (헤더, 푸터, 히어로 섹션) │ +└─────────────────┬───────────────────────┘ + │ ✅ 참조 가능 +┌─────────────────▼───────────────────────┐ +│ Features (인증, 프로젝트 CUD, 이메일) │ +└─────────────────┬───────────────────────┘ + │ ✅ 참조 가능 +┌─────────────────▼───────────────────────┐ +│ Entities (프로젝트 조회, 검색, 사용자) │ +└─────────────────┬───────────────────────┘ + │ ✅ 참조 가능 +┌─────────────────▼───────────────────────┐ +│ Shared (API, 훅, 스토어, UI 컴포넌트) │ +└─────────────────────────────────────────┘ ``` ## 🚀 개발 환경 설정 -### 설치 +### 필수 요구사항 +- **Node.js** 18+ +- **pnpm** (패키지 매니저) +- **Firebase** 프로젝트 설정 +- **EmailJS** 계정 설정 + +### 설치 및 실행 ```bash +# 의존성 설치 pnpm install -``` ### 개발 서버 실행 -```bash +# 개발 서버 실행 (http://localhost:5173) pnpm dev -``` - -### 빌드 -```bash +# 빌드 pnpm build -``` -### 미리보기 - -```bash +# 빌드 미리보기 pnpm preview ``` -### 코드 검사 +### 코드 품질 관리 ```bash -# ESLint 검사 +# ESLint 검사 (FSD 아키텍처 규칙 포함) pnpm lint # Prettier 포매팅 pnpm format + +# TypeScript 타입 검사 +pnpm type-check + +# 전체 품질 검사 +pnpm lint && pnpm type-check ``` ## 🔀 브랜치 전략 & CI/CD @@ -171,34 +238,32 @@ pnpm format ### 브랜치 구조 ``` -main (Production) -├── develop (Staging) -│ ├── feat/auth/tkyoun0421 -│ ├── feat/user-profile/developer2 -│ └── fix/login-bug/tkyoun0421 +main (🚀 Production) +├── develop (🚧 Staging) +│ ├── feat/search +│ ├── feat/user +│ ├── fix/email +│ └── refactor/auth ``` ### 워크플로우 -1. **기능 개발**: `feat/기능명/깃허브아이디` 브랜치에서 개발 -2. **스테이징**: `develop` 브랜치로 PR → Vercel Preview 자동 배포 -3. **프로덕션**: `develop` → `main` PR → Vercel Production 자동 배포 +1. **기능 개발**: `feat/기능명` 브랜치 +2. **Staging**: `develop` 브랜치 → Vercel Preview 자동 배포 +3. **Production**: `main` 브랜치 → Vercel Production 자동 배포 ### CI/CD 파이프라인 -- **CI**: 타입체크, 린트, 포맷, 빌드, 보안 검사 -- **CD**: Vercel 자동 배포 (develop: Preview, main: Production) - -### 관련 문서 - -- [브랜치 전략 상세 가이드](./docs/BRANCH_STRATEGY.md) -- [GitHub Actions CI/CD 설정](./.github/workflows/README.md) +- 📋 타입체크, 린트, 포맷팅 검사 +- 🏗️ 프로덕션 빌드 테스트 +- 🔒 보안 취약점 스캔 +- 🚀 Vercel 자동 배포 ## 📝 커밋 컨벤션 -[Conventional Commits](https://www.conventionalcommits.org/) 규칙을 따릅니다. +[Conventional Commits](https://www.conventionalcommits.org/) 규칙을 엄격히 준수합니다. -### 허용되는 타입 +### 커밋 타입 - `feat`: 새로운 기능 - `fix`: 버그 수정 @@ -214,7 +279,7 @@ main (Production) ### 커밋 메시지 형식 -``` +```bash type(scope): subject body @@ -222,71 +287,95 @@ body footer ``` -### 예시 +## 🎯 개발 가이드라인 -```bash -git commit -m "feat(user): 사용자 프로필 조회 기능 추가" -git commit -m "fix(auth): 로그인 실패 시 에러 처리 개선" -git commit -m "docs: README에 FSD 아키텍처 설명 추가" -``` +### React 컴포넌트 작성 규칙 -## 🎯 개발 가이드라인 +- **명시적 반환 타입**: 모든 컴포넌트는 `JSX.Element` 반환 타입 명시 +- **React.FC 금지**: 함수형 컴포넌트 직접 선언 방식 사용 +- **단일 책임 원칙**: 하나의 컴포넌트는 하나의 역할만 담당 -### 컴포넌트 작성 규칙 +### Features vs Entities 구분 기준 -1. **명시적 반환 타입**: 모든 컴포넌트는 `JSX.Element` 반환 타입을 명시 -2. **React.FC 금지**: 함수형 컴포넌트 직접 선언 방식 사용 -3. **단일 책임 원칙**: 하나의 컴포넌트는 하나의 역할만 담당 +| 구분 | Features | Entities | +|------|----------|----------| +| **역할** | 사용자 액션 처리 | 데이터 표시 | +| **예시** | 로그인 폼, 프로젝트 생성 | 프로젝트 카드, 사용자 정보 | +| **상태** | 변경 가능 (CUD) | 읽기 전용 (Read) | +| **의존성** | Entities 참조 가능 | Features 참조 불가 | -```typescript -// ✅ 올바른 예시 -const UserProfile = (): JSX.Element => { - return
User Profile
; -}; +### 디렉토리 네이밍 규칙 -// ❌ 잘못된 예시 -const UserProfile: React.FC = () => { - return
User Profile
; -}; -``` +- **kebab-case 사용**: `project-list/`, `user-profile/`, `email-modal/` +- **다른 케이스 금지**: `ProjectList/`, `userProfile/`, `Email_Modal/` -### Features vs Entities 구분 +### Import 순서 규칙 -- **Features**: 사용자 액션, 폼 제출, 데이터 변경 -- **Entities**: 데이터 표시, 읽기 전용 컴포넌트 +1. 외부 라이브러리 (React, MUI 등) +2. 내부 모듈 (절대 경로) +3. 상대 경로 -### 모듈 구조 예시 +## 🛡️ 코드 품질 자동화 -```typescript -// src/entities/user/index.ts -export { UserCard } from './ui/UserCard'; -export { useUserQuery } from './queries/useUserQuery'; -export type { User } from './types/User'; +### Pre-commit Hooks -// src/features/auth/index.ts -export { LoginForm } from './ui/LoginForm'; -export { useLoginMutation } from './hooks/useLoginMutation'; -``` +1. TypeScript 타입 체크 +2. ESLint 자동 수정 +3. Prettier 포매팅 +4. 커밋 메시지 검증 + +### VSCode 설정 권장사항 -## 🔧 Git Hooks +- 저장 시 자동 포맷팅 +- Prettier 기본 포맷터 설정 +- 절대 경로 import 선호 +- ESLint 검증 활성화 -### Pre-commit +## 🚀 배포 및 성능 -- ESLint 자동 수정 -- Prettier 포매팅 -- 타입 체크 +### 빌드 최적화 +- **Tree Shaking**: 사용하지 않는 코드 제거 +- **Code Splitting**: 페이지별 청크 분할 +- **Dynamic Import**: 지연 로딩 구현 -### Commit-msg +### 성능 모니터링 +- **Core Web Vitals**: LCP, FID, CLS 추적 +- **Bundle Analyzer**: 번들 크기 분석 +- **Lighthouse**: 성능 점수 측정 -- 커밋 메시지 컨벤션 검사 -- 제목 길이 제한 (50자) -- 헤더 길이 제한 (72자) +### 배포 환경 +- **Production**: `main` 브랜치 → `project-jam.vercel.app` +- **Staging**: `develop` 브랜치 → `project-jam-dev.vercel.app` +- **Preview**: PR 브랜치 → 고유 미리보기 URL + +## 📚 관련 문서 + +- 📖 [FSD 아키텍처 상세 가이드](./docs/FSD_ARCHITECTURE.md) +- 🌿 [브랜치 전략 가이드](./docs/BRANCH_STRATEGY.md) +- 🏗️ [각 계층별 상세 문서](./src/) + - [App Layer](./src/app/README.md) + - [Pages Layer](./src/pages/README.md) + - [Widgets Layer](./src/widgets/README.md) + - [Features Layer](./src/features/README.md) + - [Entities Layer](./src/entities/README.md) + - [Shared Layer](./src/shared/README.md) + +## 🤝 기여하기 + +1. **이슈 등록**: 버그 리포트나 기능 제안 +2. **브랜치 생성**: `feat/기능명` +3. **개발**: FSD 규칙 준수하여 개발 +4. **품질 검사**: `pnpm lint && pnpm type-check` +5. **PR 생성**: `develop` 브랜치로 Pull Request + +--- -## 📚 추가 문서 +## 📞 문의 및 지원 -- [FSD 아키텍처 상세 가이드](./docs/FSD_ARCHITECTURE.md) -- [각 계층별 README](./src/) +- **GitHub Issues**: 버그 리포트 및 기능 제안 +- **Discussion**: 질문 및 아이디어 공유 +- **Email**: project-jam@example.com --- -💡 **개발 시 주의사항**: FSD 아키텍처 규칙을 위반하면 ESLint 에러가 발생합니다. 의존성 규칙을 준수하여 개발해 주세요! +💡 **개발 팁**: FSD 아키텍처를 준수하면 ESLint가 실시간으로 도와드립니다. 의존성 규칙을 위반하면 빨간 밑줄로 알려드려요! 🔴 diff --git a/src/app/README.md b/src/app/README.md index 5d8d911..3f3ae6d 100644 --- a/src/app/README.md +++ b/src/app/README.md @@ -1,215 +1,146 @@ -# 🚀 App Layer +# App Layer -**앱 전체 설정 및 초기화를 담당하는 레이어** +> 🏛️ **애플리케이션의 최상위 계층** - 전체 앱의 설정과 초기화를 담당 -App 레이어는 애플리케이션의 진입점과 전역 설정을 관리합니다. +## 📖 개요 -## 📁 폴더 구조 +App Layer는 **FSD 아키텍처의 최상위 계층**으로, 애플리케이션 전체의 설정과 초기화를 담당합니다. 다른 모든 계층들이 올바르게 작동할 수 있는 환경을 제공합니다. -``` -app/ -├── entry/ # 앱 진입점 -│ └── main.tsx # React 애플리케이션 마운트 -├── routes/ # 라우팅 설정 -│ └── App.tsx # 메인 앱 컴포넌트, 라우터 설정 -├── styles/ # 전역 스타일 -│ └── App.css # 글로벌 CSS -├── configs/ # 앱 설정 -│ └── vite-env.d.ts # Vite 환경 타입 정의 -├── index.html # HTML 템플릿 -└── README.md # 이 파일 -``` +## 📁 디렉토리 구조 -## 🛣️ 라우터 설정 (React Router v7) - -### 레이지 로딩 구현 - -성능 최적화를 위해 모든 페이지 컴포넌트에 레이지 로딩을 적용했습니다: - -```typescript -// app/routes/App.tsx -import { Suspense, lazy } from "react"; -import { LoadingSpinner } from "@shared/ui"; - -// 동적 임포트로 번들 분할 -const HomePage = lazy(() => import("@pages/home/ui/HomePage")); -const ProjectDetailPage = lazy(() => import("@pages/project-detail/ui/ProjectDetailPage")); -const NotFoundPage = lazy(() => import("@pages/not-found/ui/NotFoundPage")); - -function App(): JSX.Element { - return ( - - }> - - } /> - } /> - } /> - - - - ); -} ``` - -### 📈 레이지 로딩의 장점 - -1. **초기 번들 크기 감소**: 첫 페이지 로드 시 필요한 코드만 다운로드 -2. **빠른 초기 로딩**: 사용자가 접근하지 않는 페이지는 로드하지 않음 -3. **자동 코드 스플리팅**: Vite가 자동으로 청크를 분할 -4. **네트워크 효율성**: 필요할 때만 리소스 다운로드 - -### 🔧 라우트 설정 규칙 - -#### 1. 페이지 컴포넌트 요구사항 -```typescript -// ✅ 올바른 방법 - default export 사용 -const HomePage = (): JSX.Element => { - return
홈 페이지
; -}; - -export default HomePage; - -// ❌ 잘못된 방법 - named export는 레이지 로딩에서 사용 불가 -export { HomePage }; +src/app/ +├── configs/ # 🔧 앱 전체 설정 +│ └── vite-env.d.ts # Vite 환경 변수 타입 정의 +├── entry/ # 🚀 앱 진입점 +│ └── main.tsx # React 앱 최초 마운트 +├── routes/ # 🛣️ 라우팅 설정 +│ ├── App.tsx # 최상위 앱 컴포넌트 +│ ├── AuthLayout.tsx # 인증 관련 레이아웃 +│ ├── MainLayout.tsx # 메인 앱 레이아웃 +│ └── PrivateRoute.tsx # 인증 가드 +└── styles/ # 🎨 글로벌 스타일 + ├── fonts/ # 웹폰트 (Pretendard) + ├── global.css # CSS 리셋 & 전역 스타일 + └── theme.ts # MUI 테마 설정 ``` -#### 2. 라우트 패턴 -```typescript -// 기본 라우트 -} /> - -// 동적 라우트 -} /> +## 🎯 주요 책임 -// 중첩 라우트 -} /> - -// 404 페이지 (반드시 마지막에 위치) -} /> -``` - -#### 3. 네비게이션 가드 -```typescript -// 인증이 필요한 페이지의 경우 -const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { - const isAuthenticated = useAuth(); - return isAuthenticated ? children : ; -}; - - - - - } -/> -``` - -## 🎨 로딩 상태 관리 - -### LoadingSpinner 컴포넌트 -레이지 로딩 중 표시되는 로딩 스피너: - -```typescript -// shared/ui/LoadingSpinner.tsx -const LoadingSpinner = (): JSX.Element => { - return ( -
-
- 페이지를 로딩 중입니다... -
- ); -}; -``` - -### 에러 바운더리 -추후 추가 예정: -```typescript -// app/components/ErrorBoundary.tsx -class ErrorBoundary extends Component { - // 레이지 로딩 실패 시 에러 처리 -} -``` +### 1. 앱 진입점 관리 (`entry/`) +- **React 앱 초기화**: DOM 마운트 및 최초 렌더링 +- **StrictMode 설정**: 개발 환경에서 잠재적 문제 감지 +- **Global Provider 설정**: React Query, Router, Theme Provider + +### 2. 라우팅 시스템 (`routes/`) +- **라우트 정의**: 8개 주요 페이지 라우팅 +- **인증 가드**: 로그인 필요 페이지 보호 +- **레이아웃 관리**: 인증/비인증 사용자별 레이아웃 +- **중첩 라우팅**: 계층적 URL 구조 + +### 3. 글로벌 스타일 (`styles/`) +- **CSS 변수**: 색상, 폰트, 간격 등 디자인 토큰 +- **웹폰트 관리**: Pretendard 폰트 최적화 로딩 +- **MUI 테마**: Material-UI 커스텀 테마 정의 +- **반응형 브레이크포인트**: 모바일 퍼스트 디자인 + +### 4. 환경 설정 (`configs/`) +- **환경 변수**: Firebase, EmailJS 등 외부 서비스 설정 +- **타입 정의**: Vite 관련 환경 변수 타입 안전성 +- **빌드 설정**: 배포 환경별 설정 분리 + +## 🏗 아키텍처 역할 + +### 계층 의존성 관리 +App Layer는 **모든 하위 계층을 통합**하여 완전한 애플리케이션을 구성합니다. + +- **Pages** → UI 라우팅 연결 +- **Widgets** → 공통 레이아웃 컴포넌트 배치 +- **Features** → 전역 기능 초기화 +- **Entities** → 데이터 계층 설정 +- **Shared** → 공통 설정 및 유틸리티 + +### 전역 상태 관리 +- **React Query**: 서버 상태 관리 및 캐싱 설정 +- **Zustand**: 클라이언트 상태 관리 초기화 +- **Firebase**: 인증 및 데이터베이스 연결 설정 + +### 라우팅 전략 +- **React Router v6**: 선언적 라우팅 +- **Lazy Loading**: 페이지별 코드 스플리팅 +- **Protected Routes**: 인증 기반 접근 제어 +- **Error Boundaries**: 라우트 레벨 에러 처리 + +## ⚡ 성능 최적화 + +### 초기 로딩 최적화 +- **폰트 preload**: 중요 폰트 우선 로딩 +- **Critical CSS**: 중요 스타일 인라인 처리 +- **Resource Hints**: DNS prefetch, preconnect 설정 + +### 번들 최적화 +- **Tree Shaking**: 사용하지 않는 코드 제거 +- **Code Splitting**: 라우트별 청크 분할 +- **Dynamic Import**: 필요 시점 모듈 로딩 + +## 🔒 보안 고려사항 + +### 환경 변수 관리 +- **민감 정보 보호**: API 키 등 환경 변수 분리 +- **런타임 검증**: 필수 환경 변수 존재 여부 확인 +- **타입 안전성**: 환경 변수 타입 정의 및 검증 + +### 인증 보안 +- **토큰 관리**: Firebase Auth 토큰 자동 갱신 +- **세션 보안**: 안전한 세션 관리 +- **CSRF 보호**: 크로스 사이트 요청 위조 방지 + +## 📊 성능 모니터링 + +### Core Web Vitals +- **LCP (Largest Contentful Paint)**: 최대 콘텐츠 렌더링 시간 +- **FID (First Input Delay)**: 첫 번째 입력 지연 시간 +- **CLS (Cumulative Layout Shift)**: 누적 레이아웃 이동 + +### 사용자 경험 메트릭 +- **TTFB (Time to First Byte)**: 첫 바이트까지의 시간 +- **FCP (First Contentful Paint)**: 첫 콘텐츠 렌더링 시간 +- **TTI (Time to Interactive)**: 상호작용 가능 시점 + +## 🚀 배포 설정 + +### Vercel 최적화 +- **Build Settings**: 빌드 명령어 및 출력 디렉토리 +- **Environment Variables**: 배포 환경별 변수 설정 +- **Headers Configuration**: 보안 헤더 및 캐싱 정책 + +### 환경별 배포 +- **Production**: `main` 브랜치 → 프로덕션 도메인 +- **Staging**: `develop` 브랜치 → 스테이징 도메인 +- **Preview**: PR 브랜치 → 미리보기 URL + +## 🎯 개발 가이드라인 + +### App Layer 수정 시 주의사항 +1. **전역 영향도**: 모든 페이지에 영향을 미침 +2. **성능 고려**: 초기 로딩 성능에 직접 영향 +3. **의존성 순환**: 하위 계층 참조 금지 +4. **브레이킹 체인지**: 다른 개발자와 사전 논의 필요 + +### 새로운 글로벌 설정 추가 시 +1. **configs/** 디렉토리에 설정 파일 생성 +2. **main.tsx**에서 Provider 래핑 +3. **타입 정의** 추가 (vite-env.d.ts) +4. **문서화** 및 팀 공유 -## 🔗 의존성 규칙 - -App 레이어는 **모든 레이어**를 참조할 수 있습니다: - -```typescript -// ✅ 허용 - 모든 레이어 참조 가능 -import { HomePage } from '@pages/home'; // Pages -import { Navigation } from '@widgets/navigation'; // Widgets -import { LoginForm } from '@features/auth'; // Features -import { UserCard } from '@entities/user'; // Entities -import { Button } from '@shared/ui'; // Shared -``` - -## 📝 진입점 설정 - -### main.tsx -React 애플리케이션의 진입점: - -```typescript -// app/entry/main.tsx -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import App from '@app/routes/App'; - -createRoot(document.getElementById('root')!).render( - - - -); -``` - -### 전역 프로바이더 설정 -추후 추가될 프로바이더들: - -```typescript -// React Query, 테마, 다국어 등 -function App(): JSX.Element { - return ( - - - - {/* 라우터 설정 */} - - - - ); -} -``` - -## 🚀 성능 최적화 - -### 1. 프리로딩 -중요한 페이지는 미리 로드: -```typescript -// 마우스 오버 시 프리로드 -const handleMouseEnter = () => { - import('@pages/about/ui/AboutPage'); -}; -``` +--- -### 2. 번들 분석 -빌드 후 번들 크기 확인: -```bash -pnpm build -pnpm preview -``` +## 📚 관련 문서 -### 3. 라우트 우선순위 -자주 사용되는 라우트를 먼저 정의: -```typescript - - } /> {/* 가장 많이 사용 */} - } /> {/* 두 번째로 많이 사용 */} - } /> {/* 가끔 사용 */} - } /> {/* 404는 항상 마지막 */} - -``` +- [🏗 FSD 아키텍처 가이드](../../docs/FSD_ARCHITECTURE.md) +- [🎨 디자인 시스템](./styles/README.md) +- [🛣️ 라우팅 가이드](./routes/README.md) --- -💡 **참고**: 새로운 페이지를 추가할 때는 반드시 레이지 로딩을 적용하고 default export를 사용해 주세요! \ No newline at end of file +💡 **개발 팁**: App Layer는 **앱의 뼈대**입니다. 변경 시 모든 하위 계층에 영향을 미치므로 신중하게 접근하세요! \ No newline at end of file diff --git a/src/entities/README.md b/src/entities/README.md index 725b96c..079a653 100644 --- a/src/entities/README.md +++ b/src/entities/README.md @@ -1,253 +1,271 @@ -# 📊 Entities Layer +# Entities Layer -**데이터 조회와 표시를 담당하는 레이어** +> 📊 **도메인 엔티티 계층** - 비즈니스 도메인 데이터의 조회와 표시를 담당 -Entities 레이어는 **Read 로직**에 특화된 계층으로, 데이터를 조회하고 표시하는 역할을 담당합니다. +## 📖 개요 -## 🎯 역할과 책임 +Entities Layer는 **비즈니스 도메인의 핵심 데이터를 관리**하는 계층입니다. 주로 Read 작업을 담당하며, 데이터를 조회하고 사용자에게 표시하는 UI 컴포넌트와 로직을 포함합니다. -### 주요 기능 -- 📋 **데이터 조회**: API로부터 데이터 패칭 -- 🎨 **데이터 표시**: UI 컴포넌트로 데이터 렌더링 -- 🔄 **데이터 캐싱**: React Query를 통한 효율적인 캐싱 -- 📊 **상태 관리**: 읽기 전용 상태 관리 +## 📁 디렉토리 구조 -### Features vs Entities 구분 -| **Features (CUD)** | **Entities (Read)** | -|-------------------|---------------------| -| 데이터 변경 | 데이터 표시 | -| `useMutation` | `useQuery` | -| 폼 제출, 버튼 클릭 | 데이터 조회, 렌더링 | -| LoginForm, DeleteButton | UserCard, PostList | +``` +src/entities/ +├── projects/ # 📋 프로젝트 도메인 +│ ├── api/ # 프로젝트 조회 API +│ │ ├── getProjectApplicationsApi.ts +│ │ ├── getProjectLikeApi.ts +│ │ └── projectsApi.ts +│ ├── hooks/ # 프로젝트 관련 훅 +│ │ ├── useDeleteProjectsMutation.ts +│ │ ├── useGetProjects.ts +│ │ └── useProjectsByIds.ts +│ ├── queries/ # React Query 쿼리 +│ │ ├── useGetProjectApplications.ts +│ │ ├── useGetProjectLike.ts +│ │ ├── useProjectList.ts +│ │ ├── useProjectsItem.ts +│ │ └── useProjectsTotalCount.ts +│ ├── types/ # 프로젝트 타입 정의 +│ │ └── firebase.ts +│ └── ui/ # 프로젝트 표시 UI +│ ├── liked-projects/ +│ ├── post-info/ +│ ├── profile-page-projects-card/ +│ ├── project-collection-tab/ +│ ├── project-insert/ +│ ├── projects-card/ +│ ├── projects-detail/ +│ └── projects-stats/ +├── search/ # 🔍 검색 도메인 +│ ├── api/ # 검색 API +│ │ └── projectSearchApi.ts +│ ├── hooks/ # 검색 관련 훅 +│ │ ├── useFilteredProjects.ts +│ │ ├── useProjectSearch.ts +│ │ ├── useSearchHistory.ts +│ │ └── useSearchInput.ts +│ ├── model/ # 검색 비즈니스 로직 +│ │ ├── searchConstants.ts +│ │ ├── searchFormConfig.ts +│ │ └── searchQueryBuilder.ts +│ ├── queries/ # 검색 쿼리 +│ │ └── useProjectSearchQueries.ts +│ └── ui/ # 검색 UI 컴포넌트 +│ ├── SearchActions.tsx +│ ├── SearchFilters.tsx +│ ├── SearchForm.tsx +│ ├── SearchInput.tsx +│ ├── SearchInputHistory.tsx +│ ├── SearchInputHistoryToggle.tsx +│ ├── SearchLabels.tsx +│ ├── SearchListResultHandler.tsx +│ ├── SearchLoadingSpinner.tsx +│ ├── SearchPagination.tsx +│ ├── SearchSelectBox.tsx +│ ├── SearchStatusField.tsx +│ └── SelectBox.tsx +└── user/ # 👤 사용자 도메인 + ├── hooks/ # 사용자 관련 훅 + │ ├── useSignUp.ts + │ ├── useSignUpForm.ts + │ ├── useUpdateUser.ts + │ └── useUpdateUserForm.ts + └── ui/ # 사용자 표시 UI + ├── SubmitButton.tsx + ├── UpdateUserForm.tsx + ├── user-profile/ + │ ├── TapWithBadge.tsx + │ ├── UserProfileCard.tsx + │ └── UserProfileHeader.tsx + └── UserInfoForm.tsx +``` -## 📁 폴더 구조 +## 🎯 도메인별 상세 기능 -각 도메인 엔티티는 다음 구조를 따릅니다: +### 1. Projects Entity (`projects/`) +**역할**: 프로젝트 정보의 조회와 표시 -``` -entities/ -├── user/ # 사용자 도메인 -│ ├── api/ # API 요청 로직 -│ │ └── userApi.ts -│ ├── hooks/ # 커스텀 훅 -│ │ └── useUser.ts -│ ├── queries/ # React Query 설정 -│ │ └── userQueries.ts -│ ├── types/ # TypeScript 타입 -│ │ └── User.ts -│ ├── ui/ # UI 컴포넌트 -│ │ ├── UserCard.tsx -│ │ ├── UserProfile.tsx -│ │ └── UserList.tsx -│ ├── libs/ # 유틸리티 함수 -│ │ └── userUtils.ts -│ └── index.ts # 외부 노출 인터페이스 -├── post/ # 게시물 도메인 -│ ├── api/ -│ ├── hooks/ -│ ├── queries/ -│ ├── types/ -│ ├── ui/ -│ ├── libs/ -│ └── index.ts -└── README.md # 이 파일 -``` +#### 주요 컴포넌트 그룹 +- **liked-projects/**: 좋아요한 프로젝트 목록 및 빈 상태 +- **post-info/**: 프로젝트 상세 정보 (지원, 리더 정보, 메타데이터) +- **project-collection-tab/**: 프로젝트 컬렉션 탭 네비게이션 +- **project-insert/**: 프로젝트 등록 폼의 각 카드 컴포넌트들 +- **projects-card/**: 프로젝트 목록용 카드 및 빈 상태 +- **projects-detail/**: 프로젝트 상세 페이지 섹션들 +- **projects-stats/**: 프로젝트 통계 및 안내 정보 -## 🔧 개발 가이드 +#### 데이터 관리 +- **조회 최적화**: React Query를 통한 데이터 캐싱 +- **상태 동기화**: 실시간 프로젝트 상태 반영 +- **페이지네이션**: 대용량 프로젝트 목록 처리 -### 1. API 요청 로직 (`api/`) +### 2. Search Entity (`search/`) +**역할**: 프로젝트 검색 및 필터링 시스템 -```typescript -// entities/user/api/userApi.ts -import { apiClient } from '@shared/api'; -import type { User } from '../types/User'; +#### 핵심 기능 +- **다중 필터**: 카테고리, 기술스택, 포지션, 상태별 필터링 +- **검색 기록**: 사용자별 검색 이력 관리 +- **실시간 검색**: 타이핑과 동시에 결과 업데이트 +- **고급 검색**: 복합 조건을 통한 정확한 매칭 -export const userApi = { - getUser: async (id: string): Promise => { - const response = await apiClient.get(`/users/${id}`); - return response.data; - }, +#### 검색 엔진 구조 +- **Query Builder**: 동적 검색 쿼리 생성 +- **Filter Chain**: 연쇄적 필터 적용 +- **Result Ranking**: 관련도 기반 결과 정렬 - getUsers: async (): Promise => { - const response = await apiClient.get('/users'); - return response.data; - }, -}; -``` +### 3. User Entity (`user/`) +**역할**: 사용자 정보 표시 및 프로필 관리 -### 2. React Query 설정 (`queries/`) - -```typescript -// entities/user/queries/userQueries.ts -import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { userApi } from '../api/userApi'; -import type { User } from '../types/User'; - -export const useUser = (id: string): UseQueryResult => { - return useQuery({ - queryKey: ['user', id], - queryFn: () => userApi.getUser(id), - enabled: !!id, - }); -}; - -export const useUsers = (): UseQueryResult => { - return useQuery({ - queryKey: ['users'], - queryFn: userApi.getUsers, - }); -}; -``` +#### 사용자 표시 컴포넌트 +- **ProfileCard**: 사용자 기본 정보 카드 +- **ProfileHeader**: 프로필 페이지 헤더 +- **TapWithBadge**: 배지가 있는 탭 컴포넌트 -### 3. 커스텀 훅 (`hooks/`) - -```typescript -// entities/user/hooks/useUser.ts -import { useUser as useUserQuery } from '../queries/userQueries'; -import type { User } from '../types/User'; - -export const useUser = (id: string) => { - const query = useUserQuery(id); - - return { - user: query.data, - isLoading: query.isLoading, - isError: query.isError, - error: query.error, - refetch: query.refetch, - }; -}; -``` +#### 사용자 정보 관리 +- **회원가입**: 새 사용자 등록 폼 +- **프로필 수정**: 기존 사용자 정보 업데이트 +- **표시 최적화**: 사용자 데이터 효율적 렌더링 -### 4. UI 컴포넌트 (`ui/`) +## 🏗 아키텍처 패턴 -```typescript -// entities/user/ui/UserCard.tsx -import { useUser } from '../hooks/useUser'; -import type { User } from '../types/User'; +### 1. Repository Pattern +API 호출을 추상화하여 데이터 접근 로직과 UI를 분리 -interface UserCardProps { - userId: string; -} +### 2. Query Object Pattern +복잡한 검색 조건을 객체로 캡슐화하여 관리 -const UserCard = ({ userId }: UserCardProps): JSX.Element => { - const { user, isLoading, isError } = useUser(userId); +### 3. Observer Pattern +데이터 변경 시 관련 컴포넌트들에게 자동 알림 - if (isLoading) return
로딩 중...
; - if (isError) return
에러가 발생했습니다
; - if (!user) return
사용자를 찾을 수 없습니다
; +## 🎨 UI 컴포넌트 설계 원칙 - return ( -
-

{user.name}

-

{user.email}

-
- ); -}; +### 1. 단일 책임 원칙 +각 컴포넌트는 하나의 명확한 데이터 표시 역할만 담당 -export { UserCard }; -``` +### 2. 조합 가능성 +작은 컴포넌트들을 조합하여 복잡한 UI 구성 -### 5. 타입 정의 (`types/`) - -```typescript -// entities/user/types/User.ts -export interface User { - id: string; - name: string; - email: string; - avatar?: string; - createdAt: string; - updatedAt: string; -} - -export interface UserPreview { - id: string; - name: string; - avatar?: string; -} -``` +### 3. 재사용성 +다양한 맥락에서 사용 가능한 유연한 컴포넌트 설계 -### 6. 외부 노출 인터페이스 (`index.ts`) +## 📊 데이터 흐름 관리 -```typescript -// entities/user/index.ts -// UI 컴포넌트 -export { UserCard } from './ui/UserCard'; -export { UserProfile } from './ui/UserProfile'; -export { UserList } from './ui/UserList'; +### 1. 서버 상태 캐싱 +- **React Query**: 서버 데이터 자동 캐싱 및 무효화 +- **Stale-While-Revalidate**: 캐시된 데이터 우선 표시 후 백그라운드 업데이트 +- **옵티미스틱 업데이트**: 사용자 액션 즉시 반영 + +### 2. 클라이언트 상태 관리 +- **지역 상태**: 컴포넌트 내부 UI 상태 +- **전역 상태**: 여러 컴포넌트가 공유하는 상태 +- **동기화**: 서버 상태와 클라이언트 상태 일치 -// 훅 -export { useUser } from './hooks/useUser'; +### 3. 에러 상태 처리 +- **로딩 상태**: 데이터 fetching 중 로딩 표시 +- **에러 상태**: 네트워크 에러 시 친화적 메시지 +- **빈 상태**: 데이터가 없을 때 적절한 안내 -// 타입 -export type { User, UserPreview } from './types/User'; +## ⚡ 성능 최적화 -// API (필요한 경우에만) -export { userApi } from './api/userApi'; -``` +### 1. 렌더링 최적화 +- **React.memo**: 불필요한 리렌더링 방지 +- **가상화**: 대용량 리스트 가상 스크롤 +- **지연 로딩**: 뷰포트 진입 시 컴포넌트 로딩 -## 🎯 베스트 프랙티스 +### 2. 데이터 최적화 +- **선택적 필드**: 필요한 데이터만 요청 +- **페이지네이션**: 대용량 데이터 청크 단위 로딩 +- **프리페칭**: 사용자 행동 예측하여 미리 데이터 로딩 -### 1. 데이터 조회 최적화 -- React Query의 캐싱 활용 -- 적절한 `staleTime` 설정 -- 데이터 의존성 관리 (`enabled` 옵션) +### 3. 번들 최적화 +- **트리 쉐이킹**: 사용하지 않는 코드 제거 +- **코드 스플리팅**: 라우트/컴포넌트별 청크 분할 +- **동적 임포트**: 조건부 컴포넌트 로딩 + +## 🔍 검색 시스템 고도화 + +### 1. 검색 알고리즘 +- **풀텍스트 검색**: 제목, 설명 내 키워드 매칭 +- **가중치 기반 스코어링**: 필드별 중요도 차등 적용 +- **퍼지 매칭**: 오타 허용 검색 + +### 2. 사용자 경험 개선 +- **자동완성**: 입력 중 검색어 제안 +- **검색 하이라이트**: 결과에서 검색어 강조 +- **최근 검색어**: 사용자별 검색 이력 -### 2. 컴포넌트 설계 -- 단일 책임 원칙 준수 -- 로딩/에러 상태 처리 -- 명시적 반환 타입 ([JSX.Element 사용][[memory:7559751984028653409]]) +### 3. 고급 필터링 +- **다중 선택**: 여러 카테고리 동시 선택 +- **범위 필터**: 날짜, 팀 크기 등 범위 조건 +- **정렬 옵션**: 최신순, 인기순, 관련도순 -### 3. 타입 안전성 -- 모든 API 응답에 타입 정의 -- 선택적 프로퍼티 명시 -- 유니온 타입 활용 +## 🎯 사용자 경험 (UX) 고려사항 -### 4. 성능 고려사항 -- 불필요한 리렌더링 방지 -- 메모이제이션 적절히 활용 -- 지연 로딩 고려 +### 1. 로딩 상태 관리 +- **스켈레톤 UI**: 데이터 로딩 중 레이아웃 미리보기 +- **프로그레시브 로딩**: 중요한 내용부터 우선 표시 +- **백그라운드 업데이트**: 사용자 인터랙션 방해 없이 데이터 갱신 -## 🔗 의존성 규칙 +### 2. 반응형 디자인 +- **모바일 최적화**: 터치 친화적 인터페이스 +- **적응형 레이아웃**: 화면 크기별 최적 배치 +- **성능 고려**: 모바일 환경에서의 빠른 로딩 -Entities는 **Shared 레이어만** 참조할 수 있습니다: +### 3. 접근성 (Accessibility) +- **키보드 네비게이션**: 마우스 없이도 완전한 사용 +- **스크린 리더**: 시각 장애인을 위한 음성 지원 +- **색상 대비**: WCAG 가이드라인 준수 -```typescript -// ✅ 허용 -import { Button } from '@shared/ui'; -import { formatDate } from '@shared/libs'; -import { apiClient } from '@shared/api'; +## 🧪 테스트 전략 -// ❌ 금지 - 상위 레이어 참조 -import { LoginForm } from '@features/auth'; // Features -import { Header } from '@widgets/header'; // Widgets -import { HomePage } from '@pages/home'; // Pages +### 1. 컴포넌트 테스트 +- **렌더링 테스트**: 다양한 props에 대한 올바른 렌더링 +- **상호작용 테스트**: 사용자 클릭, 입력에 대한 반응 +- **상태 변경 테스트**: 데이터 변경 시 UI 업데이트 -// ❌ 금지 - 같은 레이어 다른 모듈 참조 -import { PostCard } from '@entities/post'; // 다른 Entity -``` +### 2. 통합 테스트 +- **API 연동 테스트**: 실제 API와의 데이터 흐름 +- **검색 시나리오**: 복합 검색 조건의 정확한 결과 +- **페이지네이션 테스트**: 대용량 데이터 처리 -## 📝 명명 규칙 +### 3. 성능 테스트 +- **렌더링 성능**: 컴포넌트 렌더링 시간 측정 +- **메모리 사용량**: 메모리 누수 검사 +- **번들 크기**: 최적화 효과 측정 -- **폴더명**: kebab-case (`user-profile`, `product-list`) -- **파일명**: PascalCase (`UserCard.tsx`, `ProductList.tsx`) -- **컴포넌트명**: PascalCase + 명시적 반환 타입 -- **훅명**: `use` 접두사 + camelCase (`useUser`, `useProductList`) -- **타입명**: PascalCase (`User`, `Product`) +## 🎯 개발 가이드라인 + +### Entities vs Features 구분 기준 +- **Entities**: 사용자가 **보는** 데이터 (프로젝트 카드, 사용자 프로필) +- **Features**: 사용자가 **하는** 액션 (프로젝트 생성, 로그인) + +### 새로운 Entity 생성 시 +1. **도메인 분석**: 비즈니스 도메인의 명확한 경계 정의 +2. **API 설계**: RESTful한 읽기 전용 엔드포인트 +3. **타입 정의**: TypeScript 인터페이스로 데이터 구조 명시 +4. **컴포넌트 분리**: 작은 단위로 재사용 가능한 컴포넌트 +5. **쿼리 최적화**: React Query 캐싱 전략 수립 + +### 기존 Entity 수정 시 +1. **영향도 분석**: 해당 Entity를 사용하는 모든 위치 파악 +2. **하위 호환성**: 기존 인터페이스 유지 또는 점진적 마이그레이션 +3. **성능 영향**: 변경으로 인한 성능 저하 여부 확인 +4. **테스트 업데이트**: 변경된 로직에 맞는 테스트 케이스 + +### 성능 고려사항 +1. **리렌더링 최소화**: 불필요한 상태 변경 방지 +2. **메모리 효율**: 대용량 데이터 처리 시 가상화 고려 +3. **네트워크 최적화**: 필요한 데이터만 선택적으로 로딩 +4. **캐싱 전략**: 적절한 캐시 TTL 설정 -## 🚀 시작하기 +--- -새로운 엔티티를 추가할 때: +## 📚 관련 문서 -1. **도메인 폴더 생성**: `entities/domain-name/` -2. **기본 구조 설정**: `api/`, `hooks/`, `queries/`, `types/`, `ui/`, `index.ts` -3. **타입 정의**: 먼저 타입부터 정의 -4. **API 레이어**: API 요청 함수 작성 -5. **Query 레이어**: React Query 훅 작성 -6. **UI 컴포넌트**: 데이터 표시 컴포넌트 작성 -7. **Public API**: `index.ts`에서 외부 노출 인터페이스 정의 +- [🏗 FSD 아키텍처 가이드](../../docs/FSD_ARCHITECTURE.md) +- [⚡ Features Layer](../features/README.md) +- [🔧 Shared Layer](../shared/README.md) --- -💡 **참고**: Entities는 **읽기 전용**입니다. 데이터 변경이 필요한 경우 Features 레이어를 사용해 주세요! \ No newline at end of file +💡 **개발 팁**: Entities는 **데이터의 표현**에 집중하세요. 사용자가 정보를 쉽게 이해하고 탐색할 수 있는 직관적인 UI를 만드는 것이 핵심입니다! \ No newline at end of file diff --git a/src/entities/projects/ui/project-insert/ProjectCategoryCard.tsx b/src/entities/projects/ui/project-insert/ProjectCategoryCard.tsx index e9f71ed..c986952 100644 --- a/src/entities/projects/ui/project-insert/ProjectCategoryCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectCategoryCard.tsx @@ -32,22 +32,42 @@ const ProjectCategoryCard = ({ diff --git a/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx b/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx index 866f9cf..0be16ec 100644 --- a/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectDetailDescriptionCard.tsx @@ -1,5 +1,4 @@ import { TextField } from "@mui/material"; -import { useMediaQuery } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import type { ChangeEvent, CSSProperties, JSX } from "react"; @@ -19,7 +18,6 @@ const ProjectDetailDescriptionCard = ({ style, }: ProjectDetailDescriptionCardProps): JSX.Element => { const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); const handleChange = (e: ChangeEvent): void => { onChange(e.target.value); @@ -47,18 +45,18 @@ AI 기술을 활용하여 개인별 학습 패턴을 분석하고, 최적화된 ## 🚀 기대 효과 - 개인화된 학습으로 학습 효율성 30% 향상 - 학습 동기 부여 및 지속성 증대`} - multiline // 여러 줄 입력 가능 + multiline minRows={large ? 12 : 8} maxRows={large ? 20 : 15} fullWidth variant="outlined" sx={{ "& .MuiOutlinedInput-root": { - fontSize: isMobile - ? theme.typography.body2.fontSize - : large - ? theme.typography.h5.fontSize - : theme.typography.body1.fontSize, + fontSize: { + xs: "15px", + sm: "16px", + md: "17px", + }, fontFamily: "monospace", lineHeight: 1.6, padding: 0, @@ -73,6 +71,19 @@ AI 기술을 활용하여 개인별 학습 패턴을 분석하고, 최적화된 "& .MuiOutlinedInput-input": { padding: large ? theme.spacing(2.5) : theme.spacing(2), resize: "vertical", + fontSize: { + xs: "15px", + sm: "16px", + md: "17px", + }, + "&::placeholder": { + fontSize: { + xs: "15px", + sm: "16px", + md: "17px", + }, + color: "#999", + }, }, }} /> diff --git a/src/entities/projects/ui/project-insert/ProjectExpectedPeriodCard.tsx b/src/entities/projects/ui/project-insert/ProjectExpectedPeriodCard.tsx index 8633851..1764db9 100644 --- a/src/entities/projects/ui/project-insert/ProjectExpectedPeriodCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectExpectedPeriodCard.tsx @@ -32,22 +32,42 @@ export default function ProjectScheduleCard({ value={value || ("" as ExpectedPeriod)} onChange={onChange} - size={large ? "medium" : "small"} + size="small" displayEmpty sx={{ - fontSize: { - xs: large ? "14px" : "14px", - sm: large ? "15px" : "15px", - md: large ? "16px" : "16px", + height: { xs: 40, sm: 48 }, + "& .MuiOutlinedInput-root": { + height: { xs: "40px !important", sm: "48px !important" }, + fontSize: { + xs: "16px", + sm: "17px", + md: "18px", + }, + fontFamily: theme.typography.fontFamily, + background: "none", + border: "none", + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.text.primary, + }, + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, }, - fontFamily: theme.typography.fontFamily, - padding: large ? theme.spacing(2.2) : theme.spacing(1.7), - height: 40, "& .MuiSelect-select": { - height: "40px", + padding: large ? theme.spacing(2.2) : theme.spacing(1.7), + height: "auto !important", + minHeight: "unset !important", display: "flex", alignItems: "center", - padding: 0, + fontSize: { + xs: "16px", + sm: "17px", + md: "18px", + }, + "&[aria-expanded='false']": { + color: value ? "inherit" : "#999", + }, }, }} > diff --git a/src/entities/projects/ui/project-insert/ProjectOneLineCard.tsx b/src/entities/projects/ui/project-insert/ProjectOneLineCard.tsx index 9829ca5..346d587 100644 --- a/src/entities/projects/ui/project-insert/ProjectOneLineCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectOneLineCard.tsx @@ -39,18 +39,18 @@ const ProjectOneLineCard = ({ sx={{ "& .MuiOutlinedInput-root": { height: 40, - fontSize: large - ? theme.typography.h5.fontSize - : theme.typography.body1.fontSize, + fontSize: { + xs: "16px", + sm: "17px", + md: "18px", + }, fontFamily: theme.typography.fontFamily, background: "none", border: "none", - // 호버 시 테두리 검정색 "&:hover .MuiOutlinedInput-notchedOutline": { borderColor: theme.palette.text.primary, }, - // 포커스 시 테두리 primary 색상 "&.Mui-focused .MuiOutlinedInput-notchedOutline": { borderColor: theme.palette.primary.main, borderWidth: "2px", @@ -60,9 +60,9 @@ const ProjectOneLineCard = ({ padding: large ? theme.spacing(2.2) : theme.spacing(1.7), "&::placeholder": { fontSize: { - xs: "14px", - sm: "15px", - md: "16px", + xs: "16px", + sm: "17px", + md: "18px", }, color: "#999", }, diff --git a/src/entities/projects/ui/project-insert/ProjectPositionsCard.tsx b/src/entities/projects/ui/project-insert/ProjectPositionsCard.tsx index 047909b..c7c9b29 100644 --- a/src/entities/projects/ui/project-insert/ProjectPositionsCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectPositionsCard.tsx @@ -26,17 +26,17 @@ interface ProjectPositionsCardProps { } const USER_ROLES = [ - { value: "frontend", label: "프론트엔드 개발자" }, - { value: "backend", label: "백엔드 개발자" }, - { value: "fullstack", label: "풀스택 개발자" }, - { value: "designer", label: "디자이너" }, - { value: "pm", label: "프로덕트 매니저" }, + { value: "프론트엔드 개발자", label: "프론트엔드 개발자" }, + { value: "백엔드 개발자", label: "백엔드 개발자" }, + { value: "풀스택 개발자", label: "풀스택 개발자" }, + { value: "디자이너", label: "디자이너" }, + { value: "프로덕트 매니저", label: "프로덕트 매니저" }, ]; const EXPERIENCE_OPTIONS = [ - { value: "junior", label: "주니어 (3년 이하)" }, - { value: "mid", label: "미들 (3년 이상 10년 이하)" }, - { value: "senior", label: "시니어 (10년 이상)" }, + { value: "주니어 (3년 이하)", label: "주니어 (3년 이하)" }, + { value: "미들 (3년 이상 10년 이하)", label: "미들 (3년 이상 10년 이하)" }, + { value: "시니어 (10년 이상)", label: "시니어 (10년 이상)" }, ]; const ProjectPositionsCard = ({ diff --git a/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx b/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx index b82b744..81d2b98 100644 --- a/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx +++ b/src/entities/projects/ui/project-insert/ProjectPreferentialCard.tsx @@ -1,15 +1,7 @@ import AddIcon from "@mui/icons-material/Add"; import { Box, Button } from "@mui/material"; -import { useMediaQuery } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import type { - ChangeEvent, - CSSProperties, - JSX, - FocusEvent, - MouseEvent, - KeyboardEvent, -} from "react"; +import type { ChangeEvent, CSSProperties, JSX, KeyboardEvent } from "react"; import { useState } from "react"; import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard"; @@ -28,7 +20,6 @@ const ProjectPreferentialCard = ({ style, }: ProjectPreferentialCardProps): JSX.Element => { const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); const [newPreferential, setNewPreferential] = useState(""); const addPreferential = (): void => { @@ -59,7 +50,8 @@ const ProjectPreferentialCard = ({ > {/* 입력 + 추가 버튼 */} - ) => @@ -67,40 +59,42 @@ const ProjectPreferentialCard = ({ } onKeyUp={handleKeyPress} placeholder="예: AWS, Docker, 스타트업 경험..." - style={{ + sx={{ flex: 1, - height: 40, - borderRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - fontSize: isMobile - ? theme.typography.body2.fontSize - : large - ? theme.typography.h5.fontSize - : theme.typography.body1.fontSize, + height: { xs: "40px !important", sm: "48px !important" }, + borderRadius: "8px", + border: `1px solid #c9c9c9`, + fontSize: { + xs: "16px", + sm: "17px", + md: "18px", + }, fontFamily: theme.typography.fontFamily, - background: theme.palette.background.paper, - padding: large ? theme.spacing(2.2) : theme.spacing(1.7), + background: "none", + padding: large ? theme.spacing(1.5) : theme.spacing(1), boxSizing: "border-box", outline: "none", transition: "border-color 0.2s ease-in-out", - }} - onFocus={(e) => { - e.target.style.borderColor = theme.palette.primary.main; - e.target.style.borderWidth = "2px"; - }} - onBlur={(e: FocusEvent) => { - e.currentTarget.style.borderColor = theme.palette.divider; - e.currentTarget.style.borderWidth = "1px"; - }} - onMouseEnter={(e: MouseEvent) => { - if (e.currentTarget !== document.activeElement) { - e.currentTarget.style.borderColor = "#000000"; - } - }} - onMouseLeave={(e: MouseEvent) => { - if (e.currentTarget !== document.activeElement) { - e.currentTarget.style.borderColor = theme.palette.divider; - } + lineHeight: "normal", + minHeight: "unset", + + "&::placeholder": { + fontSize: { + xs: "16px", + sm: "17px", + md: "18px", + }, + color: "#999", + }, + + "&:focus": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, + + "&:hover:not(:focus)": { + borderColor: theme.palette.text.primary, + }, }} /> + ); +}; +``` + +#### 커스텀 훅 -export const useLogin = () => { +```typescript +// features/auth/hooks/useSocialLogin.ts +export const useSocialLogin = () => { return useMutation({ - mutationFn: authApi.login, + mutationFn: async (provider: 'google' | 'github') => { + const authProvider = provider === 'google' + ? new GoogleAuthProvider() + : new GithubAuthProvider(); + + const result = await signInWithPopup(auth, authProvider); + return result.user; + }, onSuccess: (user) => { - // 로그인 성공 처리 - localStorage.setItem('token', user.token); + // 성공 시 사용자 정보 저장 + console.log('로그인 성공:', user); + // 홈페이지로 리다이렉트 또는 상태 업데이트 }, onError: (error) => { - // 에러 처리 - console.error('Login failed:', error); + console.error('로그인 실패:', error); + // 에러 처리 (토스트 메시지 등) + }, + }); +}; +``` + +### 2. Projects Feature (프로젝트 관리) + +프로젝트의 생성, 수정, 삭제, 좋아요 등 모든 상태 변경을 담당합니다. + +#### 핵심 기능 +- **프로젝트 등록**: 4단계 위저드 폼 +- **프로젝트 지원**: 프로젝트 신청 및 취소 +- **좋아요 시스템**: 관심 프로젝트 북마크 +- **프로젝트 수정/삭제**: 소유자 권한 관리 + +#### 프로젝트 등록 플로우 + +```typescript +// features/projects/ui/project-insert/Step1.tsx +const Step1 = (): JSX.Element => { + const { + register, + formState: { errors }, + watch, + } = useFormContext(); + + const { nextStep } = useInsertStep1(); + + return ( + + + + 프로젝트 기본 정보 + + + 프로젝트의 기본적인 정보를 입력해주세요. + + + + + + + + + + + + ); +}; +``` + +#### 좋아요 시스템 (낙관적 업데이트) + +```typescript +// features/projects/hooks/useOptimisticProjectLike.ts +export const useOptimisticProjectLike = (projectId: string) => { + const queryClient = useQueryClient(); + const { user } = useAuthObserver(); + + const { mutate: toggleLike } = useMutation({ + mutationFn: createProjectLikeApi, + + // 낙관적 업데이트 + onMutate: async ({ projectId, userId, isLiked }) => { + // 진행 중인 쿼리 취소 + await queryClient.cancelQueries({ queryKey: ['project', projectId] }); + + // 이전 데이터 백업 + const previousData = queryClient.getQueryData(['project', projectId]); + + // 낙관적으로 UI 업데이트 + queryClient.setQueryData(['project', projectId], (old: any) => ({ + ...old, + isLiked: !isLiked, + likeCount: isLiked ? old.likeCount - 1 : old.likeCount + 1, + })); + + return { previousData }; + }, + + // 실패 시 롤백 + onError: (error, variables, context) => { + if (context?.previousData) { + queryClient.setQueryData(['project', projectId], context.previousData); + } + }, + + // 성공/실패 관계없이 데이터 재검증 + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['project', projectId] }); }, }); + + return { toggleLike }; }; ``` +#### 프로젝트 지원 시스템 + ```typescript -// features/auth/ui/LoginForm.tsx -import { useLogin } from '../hooks/useLogin'; +// features/projects/ui/ProjectApplyForm.tsx +const ProjectApplyForm = ({ projectId, onClose }: Props): JSX.Element => { + const { register, handleSubmit, formState: { errors } } = useApplyForm(); + const { mutate: applyProject, isPending } = useCreateProjectApplications(); + + const onSubmit = (data: ApplyFormData) => { + applyProject({ + projectId, + message: data.message, + position: data.position, + experience: data.experience, + }, { + onSuccess: () => { + onClose(); + // 성공 알림 + }, + }); + }; + + return ( + + 프로젝트 지원하기 + +
+ + + + + + + + + + + + +
+
+ ); +}; +``` + +### 3. Email Feature (이메일 발송) + +EmailJS를 통한 프로젝트 문의 및 소통 기능을 담당합니다. + +#### 핵심 기능 +- **프로젝트 문의**: 프로젝트 리더에게 직접 연락 +- **이메일 템플릿**: 구조화된 메시지 포맷 +- **실시간 발송**: EmailJS 클라이언트 사이드 발송 + +#### 이메일 모달 컴포넌트 -const LoginForm = (): JSX.Element => { - const loginMutation = useLogin(); +```typescript +// features/email/ui/EmailModal.tsx +interface EmailModalProps { + open: boolean; + onClose: () => void; + recipient: { + name: string; + email: string; + }; + project: { + id: string; + title: string; + }; +} + +const EmailModal = ({ + open, + onClose, + recipient, + project +}: EmailModalProps): JSX.Element => { + const { + register, + handleSubmit, + reset, + formState: { errors, isValid } + } = useEmailForm(); - const handleSubmit = (formData: LoginCredentials) => { - loginMutation.mutate(formData); + const { mutate: sendEmail, isPending } = useSendEmail(); + + const onSubmit = (data: EmailFormData) => { + sendEmail({ + to_name: recipient.name, + to_email: recipient.email, + from_name: data.senderName, + from_email: data.senderEmail, + project_title: project.title, + position: data.position, + subject: data.subject, + message: data.message, + }, { + onSuccess: () => { + reset(); + onClose(); + // 성공 알림 + }, + }); }; return ( -
- {/* 폼 구현 */} - -
+ + + + {recipient.name}님에게 연락하기 + + + 프로젝트: {project.title} + + + +
+ + + + + + + + + + + + + +
+
); }; +``` -export { LoginForm }; +#### EmailJS API 연동 + +```typescript +// features/email/api/emailApi.ts +import emailjs from '@emailjs/browser'; + +interface EmailData { + to_name: string; + to_email: string; + from_name: string; + from_email: string; + project_title: string; + position: string; + subject: string; + message: string; +} + +export const sendEmailApi = async (emailData: EmailData): Promise => { + const serviceId = import.meta.env.VITE_EMAIL_SERVICE_ID; + const templateId = import.meta.env.VITE_EMAIL_TEMPLATE_ID; + const publicKey = import.meta.env.VITE_EMAIL_PUBLIC_KEY; + + if (!serviceId || !templateId || !publicKey) { + throw new Error('이메일 설정이 완료되지 않았습니다.'); + } + + try { + console.log('📧 이메일 발송 시작:', { + service: serviceId, + template: templateId, + to: emailData.to_email, + }); + + const result = await emailjs.send( + serviceId, + templateId, + emailData, + publicKey + ); + + console.log('✅ 이메일 발송 성공:', result); + + if (result.status !== 200) { + throw new Error(`이메일 발송 실패: ${result.text}`); + } + } catch (error) { + console.error('❌ 이메일 발송 실패:', error); + throw new Error('이메일 발송에 실패했습니다. 잠시 후 다시 시도해주세요.'); + } +}; ``` -### 2. 게시물 생성 Feature (post) +## 🏗️ 아키텍처 패턴 + +### 1. Command Pattern (명령 패턴) + +사용자 액션을 명령 객체로 캡슐화합니다. ```typescript -// features/post/hooks/useCreatePost.ts -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { postApi } from '../api/postApi'; +// 좋아요 토글 명령 +const toggleLikeCommand = { + execute: async (projectId: string, userId: string) => { + const isCurrentlyLiked = await checkIsLiked(projectId, userId); + return isCurrentlyLiked + ? await removeLike(projectId, userId) + : await addLike(projectId, userId); + }, + undo: async (projectId: string, userId: string) => { + // 되돌리기 로직 + }, +}; +``` + +### 2. Repository Pattern (저장소 패턴) + +데이터 접근 로직을 추상화합니다. + +```typescript +// features/projects/api/projectsApi.ts +export const projectRepository = { + create: (data: CreateProjectData) => + addDoc(collection(db, 'projects'), data), + + update: (id: string, data: UpdateProjectData) => + updateDoc(doc(db, 'projects', id), data), + + delete: (id: string) => + deleteDoc(doc(db, 'projects', id)), + + like: (projectId: string, userId: string) => + setDoc(doc(db, 'likes', `${projectId}_${userId}`), { + projectId, + userId, + createdAt: serverTimestamp(), + }), +}; +``` + +### 3. Form State Management + +React Hook Form과 Zod를 활용한 폼 상태 관리입니다. -export const useCreatePost = () => { +```typescript +// features/projects/hooks/useProjectInsertForm.ts +export const useProjectInsertForm = () => { + const form = useForm({ + resolver: zodResolver(projectInsertSchema), + defaultValues: { + title: '', + oneline: '', + category: '', + techStack: [], + positions: [], + // ... 기타 필드 + }, + mode: 'onChange', // 실시간 유효성 검사 + }); + + const { data: stepData, setStepData } = useProjectStore(); + + // 단계별 데이터 동기화 + useEffect(() => { + if (stepData) { + form.reset(stepData); + } + }, [stepData, form]); + + return { + ...form, + saveStepData: (data: Partial) => { + setStepData({ ...stepData, ...data }); + }, + }; +}; +``` + +## 🔗 의존성 관계 + +### 허용되는 참조 +```typescript +// ✅ Entities Layer 참조 (데이터 표시용) +import { ProjectCard } from '@entities/projects'; +import { UserProfile } from '@entities/user'; + +// ✅ Shared Layer 참조 (공통 유틸리티) +import { Button, Modal } from '@shared/ui'; +import { useApi, useForm } from '@shared/hooks'; +import { validateEmail } from '@shared/libs/utils'; +``` + +### 금지되는 참조 +```typescript +// ❌ Pages Layer 참조 금지 +import { ProjectListPage } from '@pages/project-list'; + +// ❌ Widgets Layer 참조 금지 +import { Header } from '@widgets/Header'; + +// ❌ 다른 Features 직접 참조 금지 +import { LoginForm } from '@features/auth'; + +// ❌ App Layer 참조 금지 +import { App } from '@app/routes/App'; +``` + +## 🚀 비즈니스 로직 패턴 + +### 1. 낙관적 업데이트 + +사용자 경험 향상을 위한 즉시 UI 반영입니다. + +```typescript +const useOptimisticUpdate = (queryKey: string[]) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: postApi.create, - onSuccess: () => { - // 캐시 무효화하여 목록 새로고침 - queryClient.invalidateQueries({ queryKey: ['posts'] }); + onMutate: async (newData) => { + await queryClient.cancelQueries({ queryKey }); + const previousData = queryClient.getQueryData(queryKey); + + queryClient.setQueryData(queryKey, newData); + return { previousData }; + }, + onError: (err, newData, context) => { + queryClient.setQueryData(queryKey, context?.previousData); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey }); }, }); }; ``` -```typescript -// features/post/ui/CreatePostForm.tsx -import { useCreatePost } from '../hooks/useCreatePost'; +### 2. 단계별 폼 상태 관리 -const CreatePostForm = (): JSX.Element => { - const createPostMutation = useCreatePost(); +복잡한 폼을 단계별로 관리합니다. + +```typescript +// Zustand 스토어로 단계 간 데이터 공유 +export const useProjectInsertStore = create((set, get) => ({ + currentStep: 1, + formData: {}, - const handleSubmit = (postData: CreatePostRequest) => { - createPostMutation.mutate(postData); - }; + nextStep: () => set((state) => ({ + currentStep: Math.min(state.currentStep + 1, 4) + })), + + prevStep: () => set((state) => ({ + currentStep: Math.max(state.currentStep - 1, 1) + })), + + updateFormData: (data) => set((state) => ({ + formData: { ...state.formData, ...data } + })), + + resetForm: () => set({ currentStep: 1, formData: {} }), +})); +``` +### 3. 에러 처리 및 재시도 + +견고한 에러 처리 메커니즘입니다. + +```typescript +export const useResilientMutation = ( + mutationFn: (data: T) => Promise, + options?: { + retries?: number; + retryDelay?: number; + } +) => { + return useMutation({ + mutationFn, + retry: options?.retries ?? 3, + retryDelay: (attemptIndex) => + Math.min(1000 * 2 ** attemptIndex, 30000), // 지수 백오프 + + onError: (error) => { + // 전역 에러 처리 + if (error.message.includes('auth')) { + // 인증 에러 처리 + redirectToLogin(); + } else if (error.message.includes('network')) { + // 네트워크 에러 처리 + showNetworkErrorToast(); + } else { + // 일반 에러 처리 + showErrorToast(error.message); + } + }, + }); +}; +``` + +## 📊 성능 최적화 + +### 1. 지연 로딩 + +무거운 컴포넌트의 지연 로딩입니다. + +```typescript +// 프로젝트 등록 폼 지연 로딩 +const ProjectInsertForm = lazy(() => + import('./project-insert/ProjectInsertForm') +); + +const ProjectInsertPage = (): JSX.Element => { return ( -
- {/* 게시물 생성 폼 */} -
+ }> + + ); }; - -export { CreatePostForm }; ``` -### 3. Export 구조 +### 2. 메모이제이션 + +비싼 계산의 캐싱입니다. ```typescript -// features/auth/index.ts -export { LoginForm } from './ui/LoginForm'; -export { LogoutButton } from './ui/LogoutButton'; -export { useLogin } from './hooks/useLogin'; -export { useLogout } from './hooks/useLogout'; -export type { LoginCredentials } from './types/Auth'; -``` +const ProjectApplyForm = ({ project }: Props): JSX.Element => { + // 프로젝트 분석 결과 캐싱 + const analysis = useMemo(() => { + return analyzeProjectRequirements(project); + }, [project.id, project.requirements]); -## 📋 개발 가이드라인 + // 폼 유효성 검사 결과 캐싱 + const isFormValid = useMemo(() => { + return validateProjectForm(formData); + }, [formData]); -### 1. Features vs Entities 구분 + return ( + // 폼 렌더링 + ); +}; +``` -| 구분 | Features (CUD) | Entities (R) | -|------|----------------|--------------| -| **목적** | 데이터 변경 | 데이터 표시 | -| **액션** | 폼 제출, 버튼 클릭 | 데이터 조회, 렌더링 | -| **상태** | 로딩, 에러, 성공 | 읽기 전용 | -| **예시** | 로그인 폼, 삭제 버튰 | 사용자 카드, 목록 | +### 3. 디바운싱 -### 2. 명명 규칙 -- 폴더명: kebab-case (예: `user-management`, `post-editor`) -- 컴포넌트명: 액션 중심 (예: `CreateUserForm`, `DeleteButton`) -- 훅명: `use` + 액션 (예: `useCreateUser`, `useDeletePost`) +실시간 검색 및 자동 저장입니다. -### 3. React Query 패턴 ```typescript -// Mutation 중심 (상태 변경) -const createMutation = useMutation({ - mutationFn: api.create, - onSuccess: () => { - queryClient.invalidateQueries(['entities']); - }, -}); +export const useAutosave = ( + data: ProjectInsertFormData, + saveFunction: (data: ProjectInsertFormData) => void +) => { + const debouncedSave = useMemo( + () => debounce(saveFunction, 2000), + [saveFunction] + ); + + useEffect(() => { + if (data && Object.keys(data).length > 0) { + debouncedSave(data); + } + }, [data, debouncedSave]); + + return debouncedSave; +}; ``` -### 4. 의존성 규칙 -- Entities, Shared 계층만 참조 가능 -- 다른 Features 직접 참조 금지 -- Pages, Widgets, App 계층 참조 금지 +## 📱 사용자 경험 (UX) + +### 1. 로딩 상태 관리 -## 🚀 Feature 유형별 예시 +모든 액션에 적절한 로딩 표시를 제공합니다. -### 폼 기반 Feature ```typescript -// 사용자 입력을 받는 기능 -const UserRegistrationForm = (): JSX.Element => { - const registerMutation = useRegister(); - // 폼 로직 +const ProjectLikeButton = ({ projectId }: Props): JSX.Element => { + const { mutate: toggleLike, isPending } = useOptimisticProjectLike(projectId); + + return ( + + ); }; ``` -### 액션 기반 Feature +### 2. 실시간 피드백 + +사용자 액션에 즉시 피드백을 제공합니다. + ```typescript -// 버튼 클릭으로 실행되는 기능 -const DeletePostButton = ({ postId }: Props): JSX.Element => { - const deleteMutation = useDeletePost(); - // 삭제 로직 +const useToastNotification = () => { + const { showSnackbar } = useSnackbarStore(); + + return { + success: (message: string) => + showSnackbar({ message, severity: 'success' }), + error: (message: string) => + showSnackbar({ message, severity: 'error' }), + info: (message: string) => + showSnackbar({ message, severity: 'info' }), + }; }; ``` -### 복합 Feature +### 3. 접근성 지원 + +키보드 네비게이션과 스크린 리더를 지원합니다. + ```typescript -// 여러 액션을 포함하는 기능 -const PostEditor = (): JSX.Element => { - const saveMutation = useSavePost(); - const publishMutation = usePublishPost(); - // 편집기 로직 +const ProjectApplyButton = ({ project }: Props): JSX.Element => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleApply(); + } + }; + + return ( + + ); }; ``` -## ⚠️ 주의사항 +--- + +## 📝 개발 가이드라인 + +1. **단일 책임 원칙**: 각 feature는 하나의 비즈니스 도메인만 담당 +2. **상태 변경 중심**: CUD 로직만 포함, Read 로직은 entities에서 처리 +3. **사용자 중심 설계**: 모든 인터랙션에 적절한 피드백 제공 +4. **에러 처리**: 모든 비동기 작업에 에러 처리 구현 +5. **성능 고려**: 불필요한 리렌더링 방지 및 최적화 적용 +6. **접근성**: ARIA 표준 준수 및 키보드 네비게이션 지원 -- **읽기 전용 로직은 Entities로**: 데이터 조회/표시는 Features에 포함하지 마세요 -- **단일 책임 원칙**: 하나의 Feature는 하나의 주요 액션을 담당해야 합니다 -- **상태 관리**: 복잡한 상태는 React Query나 Context를 활용하세요 -- **에러 처리**: 모든 mutation에 적절한 에러 처리를 포함하세요 \ No newline at end of file +💡 **팁**: Feature가 복잡해지면 여러 개의 작은 feature로 분리하고, entities와의 경계를 명확히 유지하세요! \ No newline at end of file diff --git a/src/features/email/api/emailApi.ts b/src/features/email/api/emailApi.ts index 7733797..384c61e 100644 --- a/src/features/email/api/emailApi.ts +++ b/src/features/email/api/emailApi.ts @@ -12,7 +12,7 @@ const EMAIL_PUBLIC_KEY = import.meta.env.VITE_EMAIL_PUBLIC_KEY || ""; emailjs.init(EMAIL_PUBLIC_KEY); export const sendEmailApi = async ({ - actualSenderEmail, + senderEmail, receiverEmail, projectId, projectTitle, @@ -23,8 +23,8 @@ export const sendEmailApi = async ({ to_name: receiverEmail.split("@")[0], to_email: receiverEmail, - from_name: actualSenderEmail.split("@")[0], - from_email: actualSenderEmail, + from_name: senderEmail.split("@")[0], + from_email: senderEmail, subject: emailData.subject, message: emailData.message, @@ -32,7 +32,7 @@ export const sendEmailApi = async ({ project_title: projectTitle, project_id: projectId, - reply_to: actualSenderEmail, + reply_to: senderEmail, }; const result = await emailjs.send( @@ -40,21 +40,32 @@ export const sendEmailApi = async ({ EMAIL_TEMPLATE_ID, templateParams ); - return { success: true, message: result.text, }; } catch (error) { + console.error("❌ 이메일 전송 실패:", error); + let errorMessage = "이메일 전송에 실패했습니다. 다시 시도해주세요."; if (error instanceof Error) { + console.error("에러 상세:", error.message); + if (error.message.includes("template")) { errorMessage = "이메일 템플릿 설정에 문제가 있습니다."; } else if (error.message.includes("service")) { errorMessage = "이메일 서비스 설정에 문제가 있습니다."; - } else if (error.message.includes("user")) { + } else if ( + error.message.includes("user") || + error.message.includes("public_key") + ) { errorMessage = "이메일 서비스 인증에 실패했습니다."; + } else if ( + error.message.includes("network") || + error.message.includes("fetch") + ) { + errorMessage = "네트워크 연결을 확인해주세요."; } } diff --git a/src/features/email/hooks/useAutoFocus.ts b/src/features/email/hooks/useAutoFocus.ts new file mode 100644 index 0000000..0bb05f9 --- /dev/null +++ b/src/features/email/hooks/useAutoFocus.ts @@ -0,0 +1,36 @@ +import { useRef, useCallback } from "react"; + +interface UseAutoFocusOptions { + isOpen: boolean; +} + +interface UseAutoFocusReturn { + elementRef: React.RefObject; + handleTransitionEnd: () => void; +} + +export const useAutoFocus = ({ + isOpen, +}: UseAutoFocusOptions): UseAutoFocusReturn => { + const elementRef = useRef(null); + + const focusElement = useCallback((): void => { + const input = elementRef.current?.querySelector("input"); + if (input) { + input.focus(); + } + }, []); + + const handleTransitionEnd = useCallback((): void => { + if (isOpen) { + focusElement(); + } + }, [isOpen, focusElement]); + + return { + elementRef, + handleTransitionEnd, + }; +}; + +export default useAutoFocus; diff --git a/src/features/email/hooks/useEmailForm.ts b/src/features/email/hooks/useEmailForm.ts index da9ddd1..8d0982a 100644 --- a/src/features/email/hooks/useEmailForm.ts +++ b/src/features/email/hooks/useEmailForm.ts @@ -55,7 +55,7 @@ const useEmailForm = ({ sendEmailMutation.mutate( { - actualSenderEmail: senderEmail, + senderEmail, receiverEmail, projectId, projectTitle, diff --git a/src/features/email/hooks/useFormValidation.ts b/src/features/email/hooks/useFormValidation.ts new file mode 100644 index 0000000..509e16b --- /dev/null +++ b/src/features/email/hooks/useFormValidation.ts @@ -0,0 +1,37 @@ +import { useMemo } from "react"; + +interface UseFormValidationOptions { + subject: string; + message: string; +} + +interface UseFormValidationReturn { + isFormValid: boolean; + isSubjectValid: boolean; + isMessageValid: boolean; +} + +export const useFormValidation = ({ + subject, + message, +}: UseFormValidationOptions): UseFormValidationReturn => { + const isSubjectValid = useMemo(() => { + return subject.trim().length > 0; + }, [subject]); + + const isMessageValid = useMemo(() => { + return message.trim().length > 0; + }, [message]); + + const isFormValid = useMemo(() => { + return isSubjectValid && isMessageValid; + }, [isSubjectValid, isMessageValid]); + + return { + isFormValid, + isSubjectValid, + isMessageValid, + }; +}; + +export default useFormValidation; diff --git a/src/features/email/types/email.ts b/src/features/email/types/email.ts index 2455bc3..dc7a849 100644 --- a/src/features/email/types/email.ts +++ b/src/features/email/types/email.ts @@ -29,7 +29,7 @@ export interface UseEmailFormReturn { } export interface SendEmailRequest { - actualSenderEmail: string; + senderEmail: string; receiverEmail: string; projectId: string; projectTitle: string; diff --git a/src/features/email/ui/EmailField.tsx b/src/features/email/ui/EmailField.tsx index fa04a61..80011c0 100644 --- a/src/features/email/ui/EmailField.tsx +++ b/src/features/email/ui/EmailField.tsx @@ -1,4 +1,5 @@ import { TextField } from "@mui/material"; +import { styled } from "@mui/material/styles"; import { memo, type JSX } from "react"; interface EmailFieldProps { @@ -10,7 +11,7 @@ const EmailFieldComponent = ({ label, value, }: EmailFieldProps): JSX.Element => ( - ({ + "& .MuiOutlinedInput-root": { + backgroundColor: theme.palette.grey[50], + borderStyle: "dashed", + + "& fieldset": { + borderColor: theme.palette.grey[300], + borderStyle: "dashed", + borderWidth: "1px", + }, + + "&:hover fieldset": { + borderColor: theme.palette.grey[400], + borderStyle: "dashed", + }, + + "&.Mui-disabled": { + "& fieldset": { + borderStyle: "dashed", + borderColor: theme.palette.grey[300], + }, + }, + }, + + "& .MuiInputBase-input.Mui-disabled": { + color: theme.palette.text.secondary, + WebkitTextFillColor: theme.palette.text.secondary, + opacity: 0.8, + }, + + "& .MuiInputLabel-root.Mui-disabled": { + color: theme.palette.text.secondary, + opacity: 0.7, + }, +})); diff --git a/src/features/email/ui/EmailModal.tsx b/src/features/email/ui/EmailModal.tsx index caa8cf9..a55cb04 100644 --- a/src/features/email/ui/EmailModal.tsx +++ b/src/features/email/ui/EmailModal.tsx @@ -10,7 +10,9 @@ import { import { styled } from "@mui/material/styles"; import { type JSX } from "react"; +import useAutoFocus from "@features/email/hooks/useAutoFocus"; import useEmailForm from "@features/email/hooks/useEmailForm"; +import useFormValidation from "@features/email/hooks/useFormValidation"; import EmailField from "@features/email/ui/EmailField"; import MessageField from "@features/email/ui/MessageField"; import SubjectField from "@features/email/ui/SubjectField"; @@ -47,10 +49,23 @@ const EmailModal = ({ onClose, }); - const isFormValid = subject.trim().length > 0 && message.trim().length > 0; + const { elementRef, handleTransitionEnd } = useAutoFocus({ + isOpen: open, + }); + + const { isFormValid } = useFormValidation({ + subject, + message, + }); return ( - + 📧 이메일 보내기 @@ -58,7 +73,11 @@ const EmailModal = ({ - + diff --git a/src/features/email/ui/MessageField.tsx b/src/features/email/ui/MessageField.tsx index f1107d6..6c19e94 100644 --- a/src/features/email/ui/MessageField.tsx +++ b/src/features/email/ui/MessageField.tsx @@ -1,4 +1,5 @@ import { TextField } from "@mui/material"; +import { styled } from "@mui/material/styles"; import { memo, type JSX, type ChangeEvent } from "react"; interface MessageFieldProps { @@ -10,7 +11,7 @@ const MessageFieldComponent = ({ value, onChange, }: MessageFieldProps): JSX.Element => ( - ({ + "& .MuiOutlinedInput-root": { + backgroundColor: theme.palette.background.paper, + transition: "all 0.2s ease-in-out", + + "& fieldset": { + borderColor: theme.palette.grey[400], + borderWidth: "2px", + }, + + "&:hover fieldset": { + borderColor: theme.palette.primary.main, + }, + + "&.Mui-focused": { + backgroundColor: theme.palette.primary.light + "08", + + "& fieldset": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, + }, + }, + + "& .MuiInputLabel-root": { + fontWeight: 500, + + "&.Mui-focused": { + color: theme.palette.primary.main, + fontWeight: 600, + }, + + "&.Mui-error": { + color: theme.palette.error.main, + }, + }, + + "& .MuiInputBase-input": { + fontSize: "1.4rem", + lineHeight: 1.6, + + "&::placeholder": { + color: theme.palette.text.secondary, + opacity: 0.7, + }, + }, +})); diff --git a/src/features/email/ui/PositionSelect.tsx b/src/features/email/ui/PositionSelect.tsx deleted file mode 100644 index 3b61ea7..0000000 --- a/src/features/email/ui/PositionSelect.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { FormControl, InputLabel, Select, MenuItem } from "@mui/material"; -import { styled } from "@mui/material/styles"; -import { memo, type JSX } from "react"; - -import { RecruitmentStatus, type Positions } from "@shared/types/project"; -import type { UserRole } from "@shared/types/user"; - -interface PositionSelectProps { - value: string; - onChange: (value: string) => void; - projectPositions: Positions[]; -} - -// UserRole에 따른 한글 라벨 매핑 -const POSITION_LABELS: Record = { - frontend: "프론트엔드 개발자", - backend: "백엔드 개발자", - fullstack: "풀스택 개발자", - designer: "UI/UX 디자이너", - pm: "프로젝트 매니저", -}; - -const PositionSelectComponent = ({ - value, - onChange, - projectPositions, -}: PositionSelectProps): JSX.Element => { - // 모집중인 포지션들만 필터링 (status가 undefined이거나 recruiting인 것들) - const availablePositions = (projectPositions || []).filter( - (pos) => !pos.status || pos.status === RecruitmentStatus.recruiting - ); - - return ( - - 💼 지원 포지션 - onChange(e.target.value as UserRole | "")} - label="💼 지원 포지션" - disabled={availablePositions.length === 0} - > - {availablePositions.length === 0 ? ( - - 현재 모집중인 포지션이 없습니다 - - ) : ( - availablePositions.map((position, index) => { - const uniqueValue = `${position.position}-${index}`; - return ( - - {POSITION_LABELS[position.position]} ({position.count}명 모집) - {position.experience && ` - ${position.experience}`} - - ); - }) - )} - - - ); -}; - -const PositionSelect = memo(PositionSelectComponent); - -export default PositionSelect; - -const StyledSelect = styled(Select)({ - fontSize: "1.4rem", - fontFamily: "inherit", - "&:hover .MuiOutlinedInput-notchedOutline": { - borderColor: "#222", - }, - "&.Mui-focused .MuiOutlinedInput-notchedOutline": { - borderColor: "#1976d2", - borderWidth: "2px", - }, -}); diff --git a/src/features/email/ui/SubjectField.tsx b/src/features/email/ui/SubjectField.tsx index 4f811a2..ed764f6 100644 --- a/src/features/email/ui/SubjectField.tsx +++ b/src/features/email/ui/SubjectField.tsx @@ -1,27 +1,77 @@ import { TextField } from "@mui/material"; -import { memo, type JSX, type ChangeEvent } from "react"; +import { styled } from "@mui/material/styles"; +import { memo, type ChangeEvent, forwardRef } from "react"; interface SubjectFieldProps { value: string; onChange: (e: ChangeEvent) => void; } -const SubjectFieldComponent = ({ - value, - onChange, -}: SubjectFieldProps): JSX.Element => ( - +const SubjectFieldComponent = forwardRef( + ({ value, onChange }, ref) => ( + + ) ); +SubjectFieldComponent.displayName = "SubjectFieldComponent"; + const SubjectField = memo(SubjectFieldComponent); export default SubjectField; + +const EditableTextField = styled(TextField)(({ theme }) => ({ + "& .MuiOutlinedInput-root": { + backgroundColor: theme.palette.background.paper, + transition: "all 0.2s ease-in-out", + + "& fieldset": { + borderColor: theme.palette.grey[400], + borderWidth: "2px", + }, + + "&:hover fieldset": { + borderColor: theme.palette.primary.main, + }, + + "&.Mui-focused": { + backgroundColor: theme.palette.primary.light + "08", + + "& fieldset": { + borderColor: theme.palette.primary.main, + borderWidth: "2px", + }, + }, + }, + + "& .MuiInputLabel-root": { + fontWeight: 500, + + "&.Mui-focused": { + color: theme.palette.primary.main, + fontWeight: 600, + }, + + "&.Mui-error": { + color: theme.palette.error.main, + }, + }, + + "& .MuiInputBase-input": { + fontSize: "1.4rem", + + "&::placeholder": { + color: theme.palette.text.secondary, + opacity: 0.7, + }, + }, +})); diff --git a/src/features/projects/hooks/useOptimisticProjectLike.ts b/src/features/projects/hooks/useOptimisticProjectLike.ts index fdce5c4..32a868b 100644 --- a/src/features/projects/hooks/useOptimisticProjectLike.ts +++ b/src/features/projects/hooks/useOptimisticProjectLike.ts @@ -42,7 +42,6 @@ export const useOptimisticProjectLike = (): UseOptimisticProjectLikeProps => { // 의존성 [] 이어도 될 같습니다 ... 만 나중에 천천히 알아보며 리팩토링 하기로하고 남겨두겟습니다 return () => { if (debounceTimerRef.current) { - console.log("???//sDFsdfsdf"); clearTimeout(debounceTimerRef.current); } pendingServerSync.current = false; diff --git a/src/features/projects/ui/ProjectLike.tsx b/src/features/projects/ui/ProjectLike.tsx index 4712a96..c7399e5 100644 --- a/src/features/projects/ui/ProjectLike.tsx +++ b/src/features/projects/ui/ProjectLike.tsx @@ -1,8 +1,11 @@ -import { Box, styled } from "@mui/material"; +import { Box, Chip, styled } from "@mui/material"; import type { JSX } from "react"; import { useOptimisticProjectLike } from "@features/projects/hooks/useOptimisticProjectLike"; +import { useGetProjectApplicationUsers } from "@entities/projects/queries/useGetProjectApplications"; +import { useGetProjectLikedUsers } from "@entities/projects/queries/useGetProjectLike"; + import { getStatusClassname, shareProjectUrl, @@ -19,11 +22,17 @@ type ProjectLikeType = Pick; interface ProjectLikeProps { values: ProjectLikeType; + projectId: string; } -const ProjectLike = ({ values }: ProjectLikeProps): JSX.Element => { +const ProjectLike = ({ values, projectId }: ProjectLikeProps): JSX.Element => { const { isLiked, toggleLike } = useOptimisticProjectLike(); const { showError, showSuccess } = useSnackbarStore(); + const { data: likedUsers } = useGetProjectLikedUsers(projectId); + const { data: appliedUsers } = useGetProjectApplicationUsers(projectId); + + const likedUserCnt = likedUsers?.length || 0; + const appliedUsersCnt = appliedUsers?.length || 0; const sharelink = (): void => { shareProjectUrl() @@ -33,9 +42,24 @@ const ProjectLike = ({ values }: ProjectLikeProps): JSX.Element => { return ( - - {values.status} - + + + + {likedUserCnt + appliedUsersCnt > 3 && ( + + )} + @@ -73,12 +97,9 @@ const HeadIconBox = styled(Box)` } `; -const StatusBox = styled("div")` - padding: 0.5rem 1.2rem; - font-size: 12px; +const StatusChip = styled(Chip)` font-weight: 600; letter-spacing: 0.025em; - border-radius: 4px; &.ing { color: white; @@ -88,4 +109,12 @@ const StatusBox = styled("div")` color: #303030; background-color: #f0f0f0; } + &.black { + color: white; + background-color: #1d1d1d; + } + &.red { + color: white; + background: linear-gradient(to bottom right, #ff8b5d, #ff2c25); + } `; diff --git a/src/pages/README.md b/src/pages/README.md index 92515b7..65ed915 100644 --- a/src/pages/README.md +++ b/src/pages/README.md @@ -1,103 +1,223 @@ # Pages Layer -라우팅과 연결되는 페이지 컴포넌트를 관리하는 계층입니다. +프로젝트 매칭 플랫폼의 라우팅과 연결되는 페이지 컴포넌트를 관리하는 계층입니다. -## 📁 구조 +## 📁 실제 구조 ``` pages/ -├── home/ -│ ├── ui/ -│ │ └── HomePage.tsx -│ └── index.ts -├── login/ -│ ├── ui/ -│ │ └── LoginPage.tsx -│ └── index.ts -└── profile/ - ├── ui/ - │ └── ProfilePage.tsx - └── index.ts +├── home/ # 메인 홈페이지 +│ └── ui/ +│ └── HomePage.tsx +├── login/ # 로그인 페이지 +│ └── ui/ +│ └── LoginPage.tsx +├── signup/ # 회원가입 페이지 +│ └── ui/ +│ └── SignUpPage.tsx +├── project-list/ # 프로젝트 목록 및 검색 +│ └── ui/ +│ └── ProjectListPage.tsx +├── project-detail/ # 프로젝트 상세보기 +│ └── ui/ +│ └── ProjectDetailPage.tsx +├── project-insert/ # 프로젝트 등록 +│ └── ui/ +│ ├── ProjectInsertPage.tsx +│ ├── HoneyTipBox.tsx +│ ├── StepBox.tsx +│ └── TopTitle.tsx +├── user-profile/ # 사용자 프로필 +│ └── ui/ +│ ├── UserProfilePage.tsx +│ └── UserNotFound.tsx +└── not-found/ # 404 에러 페이지 + └── ui/ + └── NotFoundPage.tsx ``` -## 🎯 역할 +## 🎯 각 페이지 역할 + +### 🏠 HomePage +- 프로젝트 통계 및 시작 가이드 표시 +- Hero 섹션과 주요 기능 소개 +- 메인 CTA (Call To Action) 버튼 배치 + +### 🔐 LoginPage / SignUpPage +- 소셜 로그인 (GitHub, Google) 지원 +- 로그인 후 리다이렉트 처리 +- 회원가입 폼 및 유효성 검사 + +### 📋 ProjectListPage +- 프로젝트 검색 및 필터링 +- 페이지네이션 및 무한 스크롤 +- 검색 기록 관리 +- 프로젝트 카드 목록 표시 + +### 📄 ProjectDetailPage +- 프로젝트 상세 정보 표시 +- 프로젝트 지원 및 좋아요 기능 +- 프로젝트 리더 정보 및 연락하기 +- 이메일 모달 연동 + +### ✏️ ProjectInsertPage +- 4단계 프로젝트 등록 폼 +- 단계별 입력 검증 +- 프로젝트 생성 및 수정 +- 꿀팁 박스 및 가이드 제공 + +### 👤 UserProfilePage +- 사용자 프로필 정보 표시 +- 등록한 프로젝트 및 좋아요한 프로젝트 +- 지원한 프로젝트 현황 +- 프로필 수정 기능 + +## 🔧 페이지 구현 예시 + +### HomePage 구조 +```typescript +// pages/home/ui/HomePage.tsx +import Hero from "@widgets/hero/ui/Hero"; +import ProjectsStats from "@entities/projects/ui/projects-stats/ProjectsStats"; +import Footer from "@widgets/Footer"; -- **페이지 조합**: Widgets, Features, Entities를 조합하여 완전한 페이지 구성 -- **라우팅 연결**: React Router와 연결되는 최상위 컴포넌트 -- **페이지별 레이아웃**: 각 페이지의 고유한 레이아웃 정의 -- **SEO 설정**: 페이지별 메타데이터 관리 +const HomePage = (): JSX.Element => { + return ( + <> + + +