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/ProjectDeadlineCard.tsx b/src/entities/projects/ui/project-insert/ProjectDeadlineCard.tsx
index 3b7e846..2e7cc1d 100644
--- a/src/entities/projects/ui/project-insert/ProjectDeadlineCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectDeadlineCard.tsx
@@ -1,3 +1,5 @@
+import { TextField } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
import type { ChangeEvent, CSSProperties, JSX } from "react";
import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard";
@@ -15,6 +17,8 @@ const ProjectDeadlineCard = ({
large,
style,
}: ProjectDeadlineCardProps): JSX.Element => {
+ const theme = useTheme();
+
return (
-
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,
+ },
}}
/>
@@ -148,7 +146,11 @@ const ProjectPreferentialCard = ({
color: theme.palette.text.primary,
backgroundColor: "transparent",
cursor: "pointer",
- fontSize: "18px",
+ fontSize: {
+ xs: "16px",
+ sm: "17px",
+ md: "18px",
+ },
display: "flex",
alignItems: "center",
justifyContent: "center",
diff --git a/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx b/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx
index 18850c3..a1b2017 100644
--- a/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectRequirementsCard.tsx
@@ -1,7 +1,6 @@
import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/Delete";
import { Box, IconButton, TextField, Button } from "@mui/material";
-import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import type { ChangeEvent, CSSProperties, JSX } from "react";
@@ -21,7 +20,6 @@ const ProjectRequirementsCard = ({
style,
}: ProjectRequirementsCardProps): JSX.Element => {
const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const displayValue = value;
@@ -73,12 +71,12 @@ const ProjectRequirementsCard = ({
size="small"
sx={{
"& .MuiOutlinedInput-root": {
- height: 40,
- fontSize: isMobile
- ? theme.typography.body2.fontSize
- : large
- ? theme.typography.h5.fontSize
- : theme.typography.body1.fontSize,
+ height: { xs: 36, sm: 48 },
+ fontSize: {
+ xs: "16px",
+ sm: "17px",
+ md: "18px",
+ },
fontFamily: theme.typography.fontFamily,
background: "none",
border: "none",
@@ -94,7 +92,15 @@ const ProjectRequirementsCard = ({
},
},
"& .MuiOutlinedInput-input": {
- padding: large ? theme.spacing(2.2) : theme.spacing(1.7),
+ padding: large ? theme.spacing(1.5) : theme.spacing(1),
+ "&::placeholder": {
+ fontSize: {
+ xs: "16px",
+ sm: "17px",
+ md: "18px",
+ },
+ color: "#999",
+ },
},
}}
/>
diff --git a/src/entities/projects/ui/project-insert/ProjectSimpleDescCard.tsx b/src/entities/projects/ui/project-insert/ProjectSimpleDescCard.tsx
index 49441cd..3407e7e 100644
--- a/src/entities/projects/ui/project-insert/ProjectSimpleDescCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectSimpleDescCard.tsx
@@ -1,4 +1,5 @@
-import { TextField, useTheme } from "@mui/material";
+import { TextField } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
import type { ChangeEvent, CSSProperties, JSX } from "react";
import SimpleFormCard from "@shared/ui/project-insert/SimpleFormCard";
@@ -38,10 +39,15 @@ const ProjectSimpleDescCard = ({
size="small"
sx={{
"& .MuiOutlinedInput-root": {
- fontSize: 16,
- fontFamily: "inherit",
+ fontSize: {
+ xs: "16px",
+ sm: "17px",
+ md: "18px",
+ },
+ fontFamily: theme.typography.fontFamily,
background: "none",
-
+ border: "none",
+ minHeight: { md: 46, sm: 43, xs: 40 },
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.text.primary,
},
@@ -51,12 +57,13 @@ const ProjectSimpleDescCard = ({
},
},
"& .MuiOutlinedInput-input": {
- padding: large ? theme.spacing(2.2) : theme.spacing(1.7),
+ padding: large ? theme.spacing(1.5) : theme.spacing(1.2),
+ minHeight: { md: "46px", sm: "43px", xs: "40px" },
"&::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/ProjectTeamSizeCard.tsx b/src/entities/projects/ui/project-insert/ProjectTeamSizeCard.tsx
index 5d71a32..1536122 100644
--- a/src/entities/projects/ui/project-insert/ProjectTeamSizeCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectTeamSizeCard.tsx
@@ -31,23 +31,42 @@ const ProjectTeamSizeCard = ({
value={value ? value.toString() : ""}
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/ProjectTechStackCard.tsx b/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx
index 43666b7..2b839a1 100644
--- a/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectTechStackCard.tsx
@@ -61,26 +61,28 @@ const ProjectTechStackCard = ({
placeholder="React, Python, Figma... 뭐든 좋아요!"
sx={{
flex: 1,
- height: 40,
+ height: { xs: "40px !important", sm: "48px !important" },
borderRadius: "8px",
- border: `1px solid ${theme.palette.divider}`,
+ border: `1px solid #c9c9c9`,
fontSize: {
- xs: "14px",
- sm: "15px",
- md: "16px",
+ 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",
+ lineHeight: "normal",
+ minHeight: "unset",
"&::placeholder": {
fontSize: {
- xs: "14px", // 모바일
- sm: "15px", // 태블릿
- md: "16px", // 데스크톱
+ xs: "16px",
+ sm: "17px",
+ md: "18px",
},
color: "#999",
},
@@ -91,7 +93,7 @@ const ProjectTechStackCard = ({
},
"&:hover:not(:focus)": {
- borderColor: "#000000",
+ borderColor: theme.palette.text.primary,
},
}}
/>
@@ -101,7 +103,7 @@ const ProjectTechStackCard = ({
disabled={!newTech.trim()}
sx={{
minWidth: 48,
- height: 46,
+ height: { xs: "40px !important", sm: "48px !important" },
backgroundColor: "#2563EB",
"&:hover": { backgroundColor: "#1d4ed8" },
}}
@@ -125,9 +127,9 @@ const ProjectTechStackCard = ({
backgroundColor: theme.palette.grey[100],
borderRadius: 2,
fontSize: {
- xs: "14px",
- sm: "15px",
- md: "16px",
+ xs: "16px",
+ sm: "17px",
+ md: "18px",
},
color: theme.palette.text.primary,
}}
diff --git a/src/entities/projects/ui/project-insert/ProjectTitleCard.tsx b/src/entities/projects/ui/project-insert/ProjectTitleCard.tsx
index f597a70..068fd8d 100644
--- a/src/entities/projects/ui/project-insert/ProjectTitleCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectTitleCard.tsx
@@ -39,17 +39,17 @@ const ProjectTitleCard = ({
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",
@@ -59,9 +59,9 @@ const ProjectTitleCard = ({
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/ProjectWorkflowCard.tsx b/src/entities/projects/ui/project-insert/ProjectWorkflowCard.tsx
index d6a6321..2c7ab09 100644
--- a/src/entities/projects/ui/project-insert/ProjectWorkflowCard.tsx
+++ b/src/entities/projects/ui/project-insert/ProjectWorkflowCard.tsx
@@ -1,6 +1,5 @@
import type { SelectChangeEvent } from "@mui/material";
import { FormControl, Select, MenuItem } from "@mui/material";
-import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import type { CSSProperties, JSX } from "react";
@@ -44,7 +43,6 @@ export default function ProjectWorkflowCard({
style,
}: ProjectWorkflowCardProps): JSX.Element {
const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const handleChange = (event: SelectChangeEvent): void => {
onChange(event.target.value as Workflow);
@@ -61,23 +59,42 @@ export default function ProjectWorkflowCard({
value={value || ("" as Workflow)}
onChange={handleChange}
- size={large ? "medium" : "small"}
+ size="small"
displayEmpty
sx={{
- fontSize: isMobile
- ? theme.typography.body2.fontSize
- : large
- ? theme.typography.h5.fontSize
- : theme.typography.body1.fontSize,
- fontFamily: theme.typography.fontFamily,
- padding: large ? theme.spacing(2.2) : theme.spacing(1.7),
-
- height: 40,
+ height: { xs: 40, sm: 48 },
+ "& .MuiOutlinedInput-root": {
+ height: { xs: "36px !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",
+ },
+ },
"& .MuiSelect-select": {
- height: "40px",
+ padding: large ? theme.spacing(1.5) : theme.spacing(1),
+ 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/projects-card/ProjectCard.tsx b/src/entities/projects/ui/projects-card/ProjectCard.tsx
index 6ca6268..ab3f95c 100644
--- a/src/entities/projects/ui/projects-card/ProjectCard.tsx
+++ b/src/entities/projects/ui/projects-card/ProjectCard.tsx
@@ -57,7 +57,14 @@ const ProjectCard = ({
>
-
+
- {values.title}
+
+ {values.title}
{values.oneLineInfo}
- {values.simpleInfo}
-
-
+ {values.simpleInfo}
+
-
- >
+
+
);
};
export default ProjectInfo;
+const TextContainer = styled("div")({
+ display: "flex",
+ flexDirection: "column",
+ gap: "1.5rem",
+ marginTop: "1rem",
+});
+
+const Title = styled(Typography)({
+ marginBottom: "0.5rem",
+});
+
+const Description = styled(Typography)({
+ lineHeight: 1.7,
+ fontSize: "1rem",
+});
+
const OneLineInfo = styled("div")`
- margin: 1rem 0 1.5rem 0;
font-size: 18px;
+ color: #666;
+ font-weight: 500;
`;
+
+const InfoContainer = styled("div")(({ theme }) => ({
+ display: "grid",
+ gridTemplateColumns: "repeat(4, auto)",
+ gap: "4rem",
+ marginTop: "1rem",
+ justifyContent: "flex-start",
+ alignItems: "start",
+
+ [theme.breakpoints.down("md")]: {
+ gridTemplateColumns: "repeat(2, 1fr)",
+ gap: "1rem",
+ },
+
+ [theme.breakpoints.down("sm")]: {
+ gridTemplateColumns: "1fr",
+ gap: "0.75rem",
+ },
+}));
diff --git a/src/entities/projects/ui/projects-stats/HowToStart.tsx b/src/entities/projects/ui/projects-stats/HowToStart.tsx
index 764ee5a..e137ff8 100644
--- a/src/entities/projects/ui/projects-stats/HowToStart.tsx
+++ b/src/entities/projects/ui/projects-stats/HowToStart.tsx
@@ -41,8 +41,10 @@ export default HowToStart;
const ProjectStatsContainer = styled("div")(({ theme }) => ({
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
+ gridAutoRows: "1fr",
gap: "3.2rem",
marginTop: "3rem",
+ alignItems: "stretch",
[theme.breakpoints.down("sm")]: {
width: "100%",
diff --git a/src/entities/projects/ui/projects-stats/ProjectsStats.tsx b/src/entities/projects/ui/projects-stats/ProjectsStats.tsx
index 7bbbf07..1262197 100644
--- a/src/entities/projects/ui/projects-stats/ProjectsStats.tsx
+++ b/src/entities/projects/ui/projects-stats/ProjectsStats.tsx
@@ -4,8 +4,44 @@ import RocketLaunchIcon from "@mui/icons-material/RocketLaunch";
import { Card, CardContent, Stack, styled, Typography } from "@mui/material";
import type { JSX } from "react";
+import { useCountUp } from "@shared/hooks/useCountUp";
import FadeInUpOnView from "@shared/ui/animations/FadeInUpOnView";
+interface StatCardProps {
+ stat: {
+ id: string;
+ title: string;
+ value: number;
+ icon: JSX.Element;
+ color: string;
+ };
+ delay: number;
+}
+
+const StatCard = ({ stat, delay }: StatCardProps): JSX.Element => {
+ const { count } = useCountUp({
+ end: stat.value,
+ duration: 1500,
+ delay: delay * 1000,
+ });
+
+ return (
+
+
+
+
+ {stat.icon}
+
+ {`${count}+`}
+
+ {stat.title}
+
+
+
+
+ );
+};
+
const ProjectsStats = (): JSX.Element => {
const mock = [
{
@@ -18,7 +54,7 @@ const ProjectsStats = (): JSX.Element => {
{
id: "b",
title: "활성 사용자",
- value: 120,
+ value: 360,
icon: ,
color: "#16a34a",
},
@@ -33,27 +69,9 @@ const ProjectsStats = (): JSX.Element => {
return (
- {mock.map((stat, index) => {
- return (
-
-
-
-
-
- {stat.icon}
-
-
- {`${stat.value}+`}
-
-
- {stat.title}
-
-
-
-
-
- );
- })}
+ {mock.map((stat, index) => (
+
+ ))}
);
};
@@ -118,7 +136,7 @@ const ProjectStatsCount = styled(Typography)(({ theme }) => ({
fontWeight: "bold",
[theme.breakpoints.up("sm")]: {
- fontSize: "2.4rem",
+ fontSize: "3.2rem",
},
}));
diff --git a/src/entities/user/ui/UserInfoForm.tsx b/src/entities/user/ui/UserInfoForm.tsx
index 7342684..8e9f111 100644
--- a/src/entities/user/ui/UserInfoForm.tsx
+++ b/src/entities/user/ui/UserInfoForm.tsx
@@ -14,6 +14,8 @@ import { type JSX } from "react";
import { useSignUpForm } from "@entities/user/hooks/useSignUpForm";
import SubmitButton from "@entities/user/ui/SubmitButton";
+import { UserExperience } from "@shared/types/user";
+
// import { useAuthStore } from "@shared/stores/authStore";
const UserInfoForm = (): JSX.Element => {
@@ -71,9 +73,13 @@ const UserInfoForm = (): JSX.Element => {
displayEmpty
>
경력 선택
- 주니어 (3년 이하)
- 미들 (3년 이상 10년 이하)
- 시니어 (10년 이상)
+
+ {UserExperience.junior}
+
+ {UserExperience.mid}
+
+ {UserExperience.senior}
+
{errors.experience && 경력을 선택해주세요. }
diff --git a/src/features/README.md b/src/features/README.md
index 22f0859..2d5a495 100644
--- a/src/features/README.md
+++ b/src/features/README.md
@@ -1,253 +1,828 @@
# Features Layer
-**CUD (Create, Update, Delete) 로직**을 담당하는 기능 중심 계층입니다.
+> ⚡ **비즈니스 기능 계층** - 사용자 인터랙션과 비즈니스 로직을 담당
-## 🎯 핵심 개념
+## 📖 개요
-Features는 **사용자의 액션과 상태 변경**을 담당합니다:
-- ✅ **Create**: 새로운 데이터 생성
-- ✅ **Update**: 기존 데이터 수정
-- ✅ **Delete**: 데이터 삭제
-- ❌ **Read**: 읽기 전용 로직은 Entities에서 담당
+Features Layer는 **사용자의 액션을 처리하는 비즈니스 기능**들을 담당합니다. Create, Update, Delete 등의 변경 작업과 복잡한 사용자 인터랙션을 처리하며, 여러 entities를 조합하여 완전한 기능을 제공합니다.
-## 📁 구조
+## 📁 디렉토리 구조
```
features/
-├── auth/
-│ ├── api/
-│ │ └── authApi.ts
-│ ├── hooks/
-│ │ └── useLogin.ts
-│ ├── queries/
-│ │ └── authQueries.ts
-│ ├── types/
-│ │ └── Auth.ts
-│ ├── ui/
+├── auth/ # 🔐 인증 관련 기능
+│ ├── hooks/ # 커스텀 훅
+│ │ └── useSocialLogin.ts
+│ ├── ui/ # UI 컴포넌트
+│ │ ├── LoginButton.tsx
│ │ ├── LoginForm.tsx
-│ │ └── LogoutButton.tsx
-│ ├── libs/
-│ │ └── authValidation.ts
-│ └── index.ts
-├── post/
-│ ├── api/
-│ │ └── postApi.ts
-│ ├── hooks/
-│ │ └── useCreatePost.ts
-│ ├── ui/
-│ │ ├── CreatePostForm.tsx
-│ │ └── EditPostForm.tsx
-│ └── index.ts
-└── comment/
- ├── hooks/
- │ ├── useCreateComment.ts
- │ └── useDeleteComment.ts
- ├── ui/
- │ ├── CommentForm.tsx
- │ └── DeleteCommentButton.tsx
- └── index.ts
-```
-
-## 📄 폴더별 표준 구조
-
-각 Feature는 다음과 같은 표준 구조를 따릅니다:
-
-```
-feature-name/
-├── api/ # API 요청 로직 (mutation)
-├── hooks/ # 커스텀 훅 (액션 중심)
-├── queries/ # React Query Mutations
-├── types/ # TypeScript 타입 정의
-├── ui/ # UI 컴포넌트 (폼, 버튼 등)
-├── libs/ # 유틸리티 함수
-└── index.ts # 외부 노출 인터페이스
-```
-
-## 🔧 사용 예시
-
-### 1. 인증 Feature (auth)
-
-```typescript
-// features/auth/api/authApi.ts
-export const authApi = {
- login: async (credentials: LoginCredentials): Promise => {
- const response = await fetch('/api/auth/login', {
- method: 'POST',
- body: JSON.stringify(credentials),
- });
- return response.json();
- },
-
- logout: async (): Promise => {
- await fetch('/api/auth/logout', { method: 'POST' });
- },
-};
+│ │ ├── LoginTitle.tsx
+│ │ ├── LogoutButton.tsx
+│ │ └── SocialLoginButton.tsx
+│ └── index.ts # 외부 노출 인터페이스
+├── projects/ # 📝 프로젝트 관리 기능
+│ ├── api/ # API 요청 로직
+│ │ ├── createProjectApplicationsApi.ts
+│ │ ├── createProjectLikeApi.ts
+│ │ ├── projectsApi.ts
+│ │ └── userAPi.ts
+│ ├── hooks/ # 커스텀 훅
+│ │ ├── useApplyForm.ts
+│ │ ├── useInsertStep1.ts
+│ │ ├── useInsertStep2.ts
+│ │ ├── useInsertStep3.ts
+│ │ ├── useInsertStep4.ts
+│ │ ├── useOptimisticProjectLike.ts
+│ │ ├── useProjectInsertForm.ts
+│ │ └── useProjectPagination.ts
+│ ├── queries/ # React Query 뮤테이션
+│ │ ├── useCancelProjectApplication.ts
+│ │ ├── useCreateProjectApplications.ts
+│ │ ├── useCreateProjectLike.ts
+│ │ ├── useProjectApply.ts
+│ │ ├── useProjectDone.ts
+│ │ └── useProjectInsert.ts
+│ ├── types/ # 타입 정의
+│ │ └── project-update.ts
+│ └── ui/ # UI 컴포넌트
+│ ├── project-insert/
+│ │ ├── Step1.tsx
+│ │ ├── Step2.tsx
+│ │ ├── Step3.tsx
+│ │ └── Step4.tsx
+│ ├── ProjectApplyForm.tsx
+│ ├── ProjectDelete.tsx
+│ ├── ProjectLike.tsx
+│ └── ProjectModify.tsx
+├── email/ # 📧 이메일 발송 기능
+│ ├── api/ # API 로직
+│ │ └── emailApi.ts
+│ ├── hooks/ # 커스텀 훅
+│ │ └── useEmailForm.ts
+│ ├── queries/ # React Query 뮤테이션
+│ │ └── useSendEmail.ts
+│ ├── types/ # 타입 정의
+│ │ └── email.ts
+│ └── ui/ # UI 컴포넌트
+│ ├── EmailField.tsx
+│ ├── EmailModal.tsx
+│ ├── MessageField.tsx
+│ ├── PositionSelect.tsx
+│ └── SubjectField.tsx
+└── README.md # 이 파일
```
+## ⚡ 주요 Feature 모듈
+
+### 1. Auth Feature (인증)
+
+사용자 인증과 관련된 모든 기능을 담당합니다.
+
+#### 핵심 기능
+- **소셜 로그인**: Google, GitHub OAuth 인증
+- **로그아웃**: 세션 종료 및 토큰 정리
+- **인증 상태 관리**: Firebase Auth 연동
+
+#### 주요 컴포넌트
+
```typescript
-// features/auth/hooks/useLogin.ts
-import { useMutation } from '@tanstack/react-query';
-import { authApi } from '../api/authApi';
+// features/auth/ui/SocialLoginButton.tsx
+interface SocialLoginButtonProps {
+ provider: 'google' | 'github';
+ onSuccess?: (user: User) => void;
+ onError?: (error: Error) => void;
+}
+
+const SocialLoginButton = ({
+ provider,
+ onSuccess,
+ onError
+}: SocialLoginButtonProps): JSX.Element => {
+ const { mutate: socialLogin, isPending } = useSocialLogin();
+
+ const handleLogin = async () => {
+ try {
+ const user = await socialLogin(provider);
+ onSuccess?.(user);
+ } catch (error) {
+ onError?.(error as Error);
+ }
+ };
+
+ return (
+ : }
+ sx={{
+ py: 1.5,
+ textTransform: 'none',
+ fontSize: '1rem',
+ }}
+ >
+ {isPending ? (
+
+ ) : (
+ `${provider === 'google' ? 'Google' : 'GitHub'}로 계속하기`
+ )}
+
+ );
+};
+```
+
+#### 커스텀 훅
-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 (
+ toggleLike(projectId)}
+ disabled={isPending}
+ startIcon={
+ isPending ? (
+
+ ) : (
+
+ )
+ }
+ >
+ {isPending ? '처리 중...' : '좋아요'}
+
+ );
};
```
-### 액션 기반 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 (
+ <>
+
+
+
+ >
+ );
+};
-## 📄 폴더별 표준 구조
+export default HomePage;
+```
-각 페이지 폴더는 다음 구조를 따릅니다:
+### ProjectListPage 구조
+```typescript
+// pages/project-list/ui/ProjectListPage.tsx
+import SearchForm from "@entities/search/ui/SearchForm";
+import ProjectCard from "@entities/projects/ui/projects-card/ProjectCard";
+import SearchListResultHandler from "@entities/search/ui/SearchListResultHandler";
+import useProjectSearch from "@entities/search/hooks/useProjectSearch";
-```
-page-name/
-├── ui/ # 페이지 컴포넌트
-├── hooks/ # 페이지별 커스텀 훅 (선택적)
-├── types/ # 페이지별 타입 정의 (선택적)
-└── index.ts # 외부 노출 인터페이스
-```
+const ProjectListPage = (): JSX.Element => {
+ const { projects, handleSearch, handlePageChange, ... } = useProjectSearch(resultsRef);
-## 🔧 사용 예시
+ return (
+
+
+
+ {projects.map(project => (
+
+ ))}
+
+
+
+ );
+};
+```
+### ProjectInsertPage 구조
```typescript
-// pages/home/ui/HomePage.tsx
-import { UserWidget } from '@widgets/user';
-import { PostList } from '@features/post';
-import { Header } from '@shared/ui';
+// pages/project-insert/ui/ProjectInsertPage.tsx
+import Step1 from "@features/projects/ui/project-insert/Step1";
+import Step2 from "@features/projects/ui/project-insert/Step2";
+import StepBox from "./StepBox";
+import HoneyTipBox from "./HoneyTipBox";
+
+const ProjectInsertPage = (): JSX.Element => {
+ const { currentStep, ... } = useProjectInsertForm();
-const HomePage = (): JSX.Element => {
return (
-
+
+
+
+ {currentStep === 1 && }
+ {currentStep === 2 && }
+ {/* ... 다른 단계들 */}
+
+
+
);
};
-
-export { HomePage };
```
-```typescript
-// pages/home/index.ts
-export { HomePage } from './ui/HomePage';
+## 📋 개발 가이드라인
+
+### 1. 페이지 명명 규칙
+- **폴더명**: kebab-case (예: `project-detail`, `user-profile`)
+- **컴포넌트명**: PascalCase + Page 접미사 (예: `ProjectDetailPage`)
+- **파일명**: PascalCase (예: `ProjectInsertPage.tsx`)
+
+### 2. 페이지 책임 분리
+- **레이아웃 조합**: Widgets, Features, Entities 컴포넌트 조합
+- **라우팅 처리**: URL 파라미터 및 쿼리 스트링 처리
+- **전역 상태**: 페이지 레벨에서만 필요한 상태 관리
+- **에러 경계**: 페이지별 에러 처리
+
+### 3. 의존성 규칙
+```
+Pages 계층이 참조 가능한 계층:
+✅ Widgets - 복합 UI 컴포넌트
+✅ Features - 비즈니스 기능
+✅ Entities - 도메인 엔티티
+✅ Shared - 공통 유틸리티
+
+❌ App - 앱 설정 (역방향 의존성)
+❌ Pages - 다른 페이지 (순환 의존성 방지)
```
+### 4. 페이지별 특화 컴포넌트
+일부 페이지는 해당 페이지에서만 사용되는 UI 컴포넌트를 포함합니다:
+- `project-insert/`: 프로젝트 등록 관련 UI (HoneyTipBox, StepBox, TopTitle)
+- `user-profile/`: 사용자 프로필 관련 UI (UserNotFound)
+
+## 🛣️ 라우팅 연결
+
```typescript
// app/routes/App.tsx에서 사용
-import { HomePage } from '@pages/home';
+import HomePage from "@pages/home/ui/HomePage";
+import ProjectListPage from "@pages/project-list/ui/ProjectListPage";
+import ProjectDetailPage from "@pages/project-detail/ui/ProjectDetailPage";
const App = (): JSX.Element => {
return (
} />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
);
};
```
-## 📋 개발 가이드라인
-
-### 1. 페이지 명명 규칙
-- 폴더명: kebab-case (예: `user-profile`, `post-detail`)
-- 컴포넌트명: PascalCase + Page 접미사 (예: `UserProfilePage`)
+## ⚠️ 주의사항
-### 2. 페이지 책임
-- **조합만 담당**: 비즈니스 로직은 Features에 위임
-- **레이아웃 정의**: 페이지의 전체적인 구조만 관리
-- **라우팅 파라미터**: URL 파라미터 처리
+### 성능 최적화
+- **코드 스플리팅**: 각 페이지는 lazy loading 대상
+- **메모이제이션**: 불필요한 리렌더링 방지
+- **이미지 최적화**: 페이지별 이미지 lazy loading
-### 3. 의존성 규칙
-- Widgets, Features, Entities, Shared 계층 참조 가능
-- 다른 Pages는 직접 참조 금지
-- App 계층 참조 금지
+### SEO 고려사항
+- **메타 태그**: 페이지별 title, description 설정
+- **구조화된 데이터**: 프로젝트 상세 페이지 JSON-LD
+- **사이트맵**: 동적 페이지 URL 관리
-## ⚠️ 주의사항
+### 접근성
+- **키보드 네비게이션**: Tab 순서 관리
+- **스크린 리더**: ARIA 속성 설정
+- **포커스 관리**: 페이지 전환 시 포커스 초기화
-- 페이지는 단순히 컴포넌트를 조합하는 역할만 수행합니다
-- 복잡한 비즈니스 로직은 Features 계층으로 분리하세요
-- 페이지 간 상태 공유가 필요하면 Shared 계층을 활용하세요
\ No newline at end of file
+이 구조는 **Feature-Sliced Design** 아키텍처를 따르며, 각 페이지가 명확한 책임을 가지고 확장 가능하도록 설계되었습니다.
\ No newline at end of file
diff --git a/src/pages/home/ui/HomePage.tsx b/src/pages/home/ui/HomePage.tsx
index 43299eb..404a666 100644
--- a/src/pages/home/ui/HomePage.tsx
+++ b/src/pages/home/ui/HomePage.tsx
@@ -1,4 +1,4 @@
-import { Box, Container, styled } from "@mui/material";
+import { Box, Container, styled, Typography } from "@mui/material";
import type { JSX } from "react";
import Hero from "@widgets/hero/ui/Hero";
@@ -26,6 +26,17 @@ const HomePage = (): JSX.Element => {
+
+
+ 새로 올라온 프로젝트
+
+
+
+ 따끈따끈한 신규 프로젝트들을 만나보세요
+
+
+
+
{projects?.projects.map((project, index) => (
@@ -114,3 +125,46 @@ const ProjectSectionContainer = styled(Box)(({ theme }) => ({
padding: "6rem 0",
},
}));
+
+const SectionTitleContainer = styled(Box)(({ theme }) => ({
+ textAlign: "center",
+ marginBottom: theme.spacing(5),
+ position: "relative",
+ "&::after": {
+ content: '""',
+ display: "block",
+ width: "80px",
+ height: "3px",
+ background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.light} 100%)`,
+ margin: "16px auto 0",
+ borderRadius: "2px",
+ },
+}));
+
+const SectionTitle = styled(Typography)(({ theme }) => ({
+ fontSize: "2.5rem",
+ fontWeight: 700,
+ background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.light} 50%, ${theme.palette.primary.dark} 100%)`,
+ backgroundClip: "text",
+ WebkitBackgroundClip: "text",
+ WebkitTextFillColor: "transparent",
+ marginBottom: theme.spacing(1),
+ letterSpacing: "-0.02em",
+ [theme.breakpoints.down("md")]: {
+ fontSize: "2rem",
+ },
+ [theme.breakpoints.down("sm")]: {
+ fontSize: "1.75rem",
+ },
+}));
+
+const SectionSubtitle = styled(Typography)(({ theme }) => ({
+ fontSize: "1.1rem",
+ fontWeight: 400,
+ color: theme.palette.text.secondary,
+ opacity: 0.8,
+ lineHeight: 1.6,
+ [theme.breakpoints.down("sm")]: {
+ fontSize: "1rem",
+ },
+}));
diff --git a/src/pages/project-detail/ui/ProjectDetailPage.tsx b/src/pages/project-detail/ui/ProjectDetailPage.tsx
index 6bf585a..c3aa8ce 100644
--- a/src/pages/project-detail/ui/ProjectDetailPage.tsx
+++ b/src/pages/project-detail/ui/ProjectDetailPage.tsx
@@ -59,6 +59,7 @@ const ProjectDetailPage = (): JSX.Element | null => {
const projectLikeValues = {
status: (project?.status as RecruitmentStatus) || "모집중",
+ id: project?.id || "",
};
const descriptionlValues = {
@@ -113,7 +114,10 @@ const ProjectDetailPage = (): JSX.Element | null => {
-
+
diff --git a/src/shared/README.md b/src/shared/README.md
index 5cd6fe3..84deb13 100644
--- a/src/shared/README.md
+++ b/src/shared/README.md
@@ -1,399 +1,254 @@
# Shared Layer
-모든 계층에서 공통으로 사용되는 유틸리티와 기본 컴포넌트를 제공하는 최하위 계층입니다.
-
-## 🎯 핵심 개념
-
-Shared는 **재사용 가능한 공통 자원**을 제공합니다:
-- ✅ **공통 UI 컴포넌트**: Button, Input, Modal 등 기본 컴포넌트
-- ✅ **유틸리티 함수**: 날짜, 문자열, 배열 처리 등
-- ✅ **상수**: API URL, 설정값 등
-- ✅ **타입**: 공통으로 사용되는 TypeScript 타입
-- ✅ **훅**: 범용적으로 사용되는 커스텀 훅
-
-## 📁 구조
-
-```
-shared/
-├── ui/
-│ ├── button/
-│ │ ├── Button.tsx
-│ │ ├── Button.module.css
-│ │ └── index.ts
-│ ├── input/
-│ │ ├── Input.tsx
-│ │ └── index.ts
-│ ├── modal/
-│ │ ├── Modal.tsx
-│ │ └── index.ts
-│ └── index.ts
-├── libs/
-│ ├── utils/
-│ │ ├── dateUtils.ts
-│ │ ├── stringUtils.ts
-│ │ └── index.ts
-│ ├── validation/
-│ │ ├── validators.ts
-│ │ └── index.ts
-│ └── index.ts
-├── hooks/
-│ ├── useLocalStorage.ts
-│ ├── useDebounce.ts
-│ └── index.ts
-├── types/
-│ ├── common.ts
-│ ├── api.ts
-│ └── index.ts
-├── constants/
-│ ├── api.ts
-│ ├── config.ts
-│ └── index.ts
-└── index.ts
-```
-
-## 📄 폴더별 표준 구조
-
-### ui/ - 공통 UI 컴포넌트
-모든 기본 UI 컴포넌트를 포함합니다.
-
-```
-ui/
-├── component-name/
-│ ├── ComponentName.tsx # 메인 컴포넌트
-│ ├── ComponentName.module.css # 스타일 (선택적)
-│ ├── ComponentName.stories.tsx # Storybook (선택적)
-│ └── index.ts # Export
-└── index.ts # 전체 UI 컴포넌트 Export
-```
-
-### libs/ - 유틸리티 함수
-범용적인 헬퍼 함수들을 포함합니다.
-
-```
-libs/
-├── category-name/
-│ ├── functionName.ts
-│ └── index.ts
-└── index.ts
-```
-
-### hooks/ - 공통 커스텀 훅
-재사용 가능한 React 훅들을 포함합니다.
-
-### types/ - 공통 타입 정의
-여러 계층에서 사용되는 TypeScript 타입들을 정의합니다.
-
-### constants/ - 상수 정의
-앱 전체에서 사용되는 상수값들을 정의합니다.
-
-## 🔧 사용 예시
-
-### 1. UI 컴포넌트
-
-```typescript
-// shared/ui/button/Button.tsx
-interface ButtonProps {
- children: React.ReactNode;
- variant?: 'primary' | 'secondary' | 'danger';
- size?: 'small' | 'medium' | 'large';
- disabled?: boolean;
- onClick?: () => void;
-}
-
-const Button = ({
- children,
- variant = 'primary',
- size = 'medium',
- disabled = false,
- onClick
-}: ButtonProps): JSX.Element => {
- return (
-
- {children}
-
- );
-};
-
-export { Button };
-export type { ButtonProps };
-```
-
-```typescript
-// shared/ui/input/Input.tsx
-interface InputProps {
- type?: 'text' | 'email' | 'password' | 'number';
- placeholder?: string;
- value?: string;
- onChange?: (value: string) => void;
- error?: string;
- disabled?: boolean;
-}
-
-const Input = ({
- type = 'text',
- placeholder,
- value,
- onChange,
- error,
- disabled = false
-}: InputProps): JSX.Element => {
- return (
-
- onChange?.(e.target.value)}
- disabled={disabled}
- className={`input ${error ? 'input--error' : ''}`}
- />
- {error && {error} }
-
- );
-};
-
-export { Input };
-export type { InputProps };
-```
-
-### 2. 유틸리티 함수
-
-```typescript
-// shared/libs/utils/dateUtils.ts
-export const dateUtils = {
- formatDate: (date: Date, format: string = 'YYYY-MM-DD'): string => {
- // 날짜 포매팅 로직
- return date.toISOString().split('T')[0];
- },
-
- addDays: (date: Date, days: number): Date => {
- const result = new Date(date);
- result.setDate(result.getDate() + days);
- return result;
- },
-
- isToday: (date: Date): boolean => {
- const today = new Date();
- return date.toDateString() === today.toDateString();
- },
-};
-```
-
-```typescript
-// shared/libs/validation/validators.ts
-export const validators = {
- email: (email: string): boolean => {
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- return emailRegex.test(email);
- },
-
- password: (password: string): { isValid: boolean; message?: string } => {
- if (password.length < 8) {
- return { isValid: false, message: '비밀번호는 8자 이상이어야 합니다.' };
- }
- return { isValid: true };
- },
-
- required: (value: string): boolean => {
- return value.trim().length > 0;
- },
-};
-```
-
-### 3. 커스텀 훅
-
-```typescript
-// shared/hooks/useLocalStorage.ts
-import { useState, useEffect } from 'react';
-
-export const useLocalStorage = (
- key: string,
- initialValue: T
-): [T, (value: T) => void] => {
- const [storedValue, setStoredValue] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- console.error('Error reading localStorage:', error);
- return initialValue;
- }
- });
-
- const setValue = (value: T) => {
- try {
- setStoredValue(value);
- window.localStorage.setItem(key, JSON.stringify(value));
- } catch (error) {
- console.error('Error setting localStorage:', error);
- }
- };
-
- return [storedValue, setValue];
-};
-```
-
-```typescript
-// shared/hooks/useDebounce.ts
-import { useState, useEffect } from 'react';
-
-export const useDebounce = (value: T, delay: number): T => {
- const [debouncedValue, setDebouncedValue] = useState(value);
-
- useEffect(() => {
- const handler = setTimeout(() => {
- setDebouncedValue(value);
- }, delay);
-
- return () => {
- clearTimeout(handler);
- };
- }, [value, delay]);
-
- return debouncedValue;
-};
-```
-
-### 4. 공통 타입
-
-```typescript
-// shared/types/common.ts
-export interface ApiResponse {
- data: T;
- success: boolean;
- message?: string;
-}
-
-export interface PaginationParams {
- page: number;
- limit: number;
-}
-
-export interface PaginatedResponse {
- data: T[];
- pagination: {
- page: number;
- limit: number;
- total: number;
- totalPages: number;
- };
-}
-
-export type LoadingState = 'idle' | 'loading' | 'success' | 'error';
-```
-
-### 5. 상수
-
-```typescript
-// shared/constants/api.ts
-export const API_ENDPOINTS = {
- AUTH: {
- LOGIN: '/api/auth/login',
- LOGOUT: '/api/auth/logout',
- REFRESH: '/api/auth/refresh',
- },
- USERS: {
- LIST: '/api/users',
- DETAIL: (id: string) => `/api/users/${id}`,
- },
-} as const;
-
-export const HTTP_STATUS = {
- OK: 200,
- CREATED: 201,
- BAD_REQUEST: 400,
- UNAUTHORIZED: 401,
- FORBIDDEN: 403,
- NOT_FOUND: 404,
- INTERNAL_SERVER_ERROR: 500,
-} as const;
-```
-
-### 6. Export 구조
-
-```typescript
-// shared/ui/index.ts
-export { Button } from './button';
-export { Input } from './input';
-export { Modal } from './modal';
-export type { ButtonProps } from './button';
-export type { InputProps } from './input';
-```
-
-```typescript
-// shared/index.ts
-export * from './ui';
-export * from './libs';
-export * from './hooks';
-export * from './types';
-export * from './constants';
-```
-
-## 📋 개발 가이드라인
-
-### 1. 명명 규칙
-- 컴포넌트: PascalCase (예: `Button`, `Modal`)
-- 함수: camelCase (예: `formatDate`, `validateEmail`)
-- 상수: UPPER_SNAKE_CASE (예: `API_ENDPOINTS`, `HTTP_STATUS`)
-- 파일: kebab-case (예: `date-utils.ts`, `use-local-storage.ts`)
-
-### 2. 컴포넌트 설계 원칙
+> 🔧 **공통 자원 계층** - 모든 계층에서 사용되는 재사용 가능한 유틸리티와 컴포넌트
+
+## 📖 개요
+
+Shared Layer는 **FSD 아키텍처의 최하위 계층**으로, 애플리케이션 전체에서 공통으로 사용되는 재사용 가능한 자원들을 제공합니다. 비즈니스 로직과 독립적인 범용 도구들을 포함합니다.
+
+## 📁 디렉토리 구조
+
+```
+src/shared/
+├── api/ # 🌐 API 클라이언트
+│ └── userApi.ts # 공통 사용자 API
+├── firebase/ # 🔥 Firebase 설정
+│ └── firebase.ts # Firebase 초기화 및 설정
+├── hooks/ # 🎣 공통 커스텀 훅
+│ ├── useAuthObserver.ts # 인증 상태 관찰
+│ ├── useCountdown.ts # 카운트다운 타이머
+│ ├── useDraggable.ts # 드래그 앤 드롭
+│ ├── useIntersectionObserver.ts # 뷰포트 교차 관찰
+│ ├── useLoadingCursor.ts # 로딩 커서 관리
+│ └── usePagination.ts # 페이지네이션 로직
+├── libs/ # 📚 유틸리티 라이브러리
+│ └── utils/ # 범용 유틸리티 함수
+│ ├── experienceLabel.ts # 경험 레벨 레이블
+│ ├── measureHmr.ts # HMR 성능 측정
+│ ├── pagination.ts # 페이지네이션 계산
+│ ├── projectDetail.ts # 프로젝트 상세 유틸
+│ ├── projectInsert.ts # 프로젝트 등록 유틸
+│ └── scrollUtils.ts # 스크롤 관련 유틸
+├── queries/ # 🔄 공통 React Query
+│ └── useUserProfile.ts # 사용자 프로필 쿼리
+├── react-query/ # ⚛️ React Query 설정
+│ ├── queryClient.ts # Query Client 설정
+│ └── queryKey.ts # Query Key 상수
+├── stores/ # 🗄️ Zustand 스토어
+│ ├── applicationsStore.ts # 지원 상태 관리
+│ ├── authStore.ts # 인증 상태 관리
+│ ├── likeStore.ts # 좋아요 상태 관리
+│ ├── projectStore.ts # 프로젝트 상태 관리
+│ ├── searchStore.ts # 검색 상태 관리
+│ └── snackbarStore.ts # 알림 상태 관리
+├── types/ # 📝 공통 타입 정의
+│ ├── firebase.ts # Firebase 관련 타입
+│ ├── like.ts # 좋아요 타입
+│ ├── project.ts # 프로젝트 타입
+│ ├── schedule.ts # 일정 타입
+│ ├── search.ts # 검색 타입
+│ └── user.ts # 사용자 타입
+└── ui/ # 🎨 공통 UI 컴포넌트
+ ├── animations/ # 애니메이션 컴포넌트
+ │ ├── FadeInUp.tsx
+ │ └── FadeInUpOnView.tsx
+ ├── DeleteButton.tsx # 삭제 버튼
+ ├── DevelopersDropdown.tsx # 개발자 드롭다운
+ ├── DragScrollContainer.tsx # 드래그 스크롤 컨테이너
+ ├── GlobalSnackbar.tsx # 전역 스낵바
+ ├── home/ # 홈 관련 컴포넌트
+ │ └── WhiteInfoBox.tsx
+ ├── icons/ # 아이콘 컴포넌트
+ │ ├── CommonIcons.tsx
+ │ ├── GradientGitHubIcon.tsx
+ │ └── logo.svg
+ ├── loading-spinner/ # 로딩 스피너
+ │ ├── LoadingSpinner.tsx
+ │ ├── PageTransitionFallback.tsx
+ │ └── PageTransitionLoader.tsx
+ ├── LogoBox.tsx # 로고 박스
+ ├── NavigateButton.tsx # 네비게이션 버튼
+ ├── pagination/ # 페이지네이션
+ │ └── Pagination.tsx
+ ├── project-detail/ # 프로젝트 상세 공통 UI
+ │ ├── InfoRow.tsx
+ │ ├── InfoWithIcon.tsx
+ │ └── TitleWithIcon.tsx
+ ├── project-insert/ # 프로젝트 등록 공통 UI
+ │ ├── SimpleFormCard.tsx
+ │ └── StepWhiteBox.tsx
+ ├── ScrollToTop.tsx # 상단 스크롤 버튼
+ ├── SnackbarAlert.tsx # 스낵바 알림
+ └── user/ # 사용자 관련 공통 UI
+ ├── UserProfileAvatar.tsx
+ └── UserProfileWithNamePosition.tsx
+```
+
+## 🎯 주요 구성 요소
+
+### 1. UI 컴포넌트 (`ui/`)
+**역할**: 재사용 가능한 기본 UI 컴포넌트 제공
+
+#### 카테고리별 컴포넌트
+- **애니메이션**: 페이드인, 슬라이드업 등 공통 애니메이션 효과
+- **로딩**: 스피너, 페이지 전환 로더, 폴백 컴포넌트
+- **네비게이션**: 버튼, 페이지네이션, 스크롤 컨트롤
+- **사용자 인터페이스**: 아바타, 프로필 카드, 정보 표시
+- **아이콘**: 공통 아이콘, 브랜드 아이콘, SVG 컴포넌트
+
+#### 설계 원칙
- **범용성**: 특정 도메인에 종속되지 않는 범용 컴포넌트
-- **재사용성**: 다양한 상황에서 사용 가능한 유연한 API
-- **확장성**: variant, size 등의 props로 다양한 변형 지원
-- **접근성**: 웹 접근성 가이드라인 준수
-
-### 3. 유틸리티 함수 원칙
-- **순수 함수**: 사이드 이펙트 없는 순수 함수로 작성
-- **타입 안전성**: 명확한 입력/출력 타입 정의
-- **에러 처리**: 예외 상황에 대한 적절한 처리
-- **테스트 가능성**: 단위 테스트가 용이한 구조
-
-### 4. 의존성 규칙
-- **독립성**: 다른 모든 계층과 독립적
-- **외부 의존성**: 외부 라이브러리는 최소한으로 사용
-- **순환 참조 금지**: 다른 계층에서 Shared를 참조하므로 순환 참조 방지
-
-## 🎨 컴포넌트 예시
-
-### 기본 UI 컴포넌트
-```typescript
-// 단순하고 재사용 가능한 컴포넌트
-const Badge = ({ children, variant = 'default' }: BadgeProps): JSX.Element => (
- {children}
-);
-```
-
-### 조합 가능한 컴포넌트
-```typescript
-// 여러 부분으로 구성된 복합 컴포넌트
-const Card = ({ children }: CardProps): JSX.Element => (
- {children}
-);
-
-const CardHeader = ({ children }: CardHeaderProps): JSX.Element => (
- {children}
-);
-
-const CardBody = ({ children }: CardBodyProps): JSX.Element => (
- {children}
-);
-
-// 사용 예시
-
- 제목
- 내용
-
-```
-
-## ⚠️ 주의사항
-
-- **범용성 유지**: 특정 비즈니스 로직을 포함하지 마세요
-- **최소 기능 원칙**: 필요한 최소한의 기능만 제공하세요
-- **Breaking Changes 주의**: Shared 컴포넌트 변경은 전체 앱에 영향을 미칩니다
-- **문서화**: 모든 컴포넌트와 함수는 명확한 문서를 작성하세요
-- **테스트**: 공통 컴포넌트는 반드시 테스트 코드를 작성하세요
\ No newline at end of file
+- **재사용성**: 다양한 상황에서 활용 가능한 유연한 API
+- **일관성**: 전체 앱의 디자인 시스템 일관성 유지
+
+### 2. 커스텀 훅 (`hooks/`)
+**역할**: 재사용 가능한 비즈니스 로직 없는 순수 훅
+
+#### 주요 훅 카테고리
+- **상태 관리**: useAuthObserver, useLoadingCursor
+- **UI 인터랙션**: useDraggable, useIntersectionObserver
+- **데이터 처리**: usePagination, useCountdown
+- **성능 최적화**: 디바운싱, 스로틀링, 메모이제이션
+
+### 3. 상태 관리 (`stores/`)
+**역할**: Zustand를 통한 전역 클라이언트 상태 관리
+
+#### 스토어별 역할
+- **authStore**: 사용자 인증 상태, 토큰 관리
+- **projectStore**: 프로젝트 임시 데이터, 폼 상태
+- **searchStore**: 검색 필터, 정렬 옵션, 검색 이력
+- **likeStore**: 좋아요 상태의 낙관적 업데이트
+- **snackbarStore**: 전역 알림 메시지 관리
+
+### 4. 유틸리티 라이브러리 (`libs/`)
+**역할**: 순수 함수 기반의 범용 유틸리티
+
+#### 유틸리티 카테고리
+- **데이터 변환**: 날짜, 문자열, 숫자 형식 변환
+- **계산**: 페이지네이션, 통계, 수치 연산
+- **검증**: 폼 검증, 데이터 유효성 검사
+- **성능**: HMR 측정, 최적화 도구
+
+### 5. 타입 정의 (`types/`)
+**역할**: 애플리케이션 전체에서 공유하는 TypeScript 타입
+
+#### 타입 분류
+- **도메인 타입**: User, Project, Schedule 등 비즈니스 엔티티
+- **API 타입**: 요청/응답 인터페이스, 에러 타입
+- **UI 타입**: 컴포넌트 Props, 이벤트 핸들러
+- **유틸리티 타입**: 공통 유니온, 조건부 타입
+
+### 6. API 클라이언트 (`api/`)
+**역할**: 외부 서비스와의 통신 인터페이스
+
+#### 주요 기능
+- **HTTP 클라이언트**: axios 기반 API 호출
+- **에러 처리**: 공통 에러 핸들링 및 변환
+- **인터셉터**: 요청/응답 전처리
+- **타입 안전성**: API 응답 타입 보장
+
+## 🏗 아키텍처 역할
+
+### 계층 독립성
+Shared Layer는 **완전히 독립적**이며 다른 어떤 계층도 참조하지 않습니다.
+
+- **상위 계층**: App, Pages, Widgets, Features, Entities 모두 Shared 참조 가능
+- **Shared**: 다른 계층 참조 불가 (외부 라이브러리만 허용)
+- **순환 참조 방지**: 의존성 그래프의 최하위 노드
+
+### 재사용성 극대화
+- **도메인 무관성**: 특정 비즈니스 로직 포함 금지
+- **범용성**: 다양한 프로젝트에서 재사용 가능
+- **확장성**: 기존 기능을 깨뜨리지 않는 확장
+
+## ⚡ 성능 최적화
+
+### 1. 번들 최적화
+- **트리 쉐이킹**: 사용하지 않는 유틸리티 자동 제거
+- **코드 스플리팅**: 큰 유틸리티의 동적 임포트
+- **데드 코드 제거**: 사용되지 않는 함수 제거
+
+### 2. 렌더링 최적화
+- **React.memo**: 순수 UI 컴포넌트 메모이제이션
+- **useCallback/useMemo**: 훅 내 계산 최적화
+- **지연 초기화**: 무거운 계산의 lazy initialization
+
+### 3. 메모리 관리
+- **이벤트 리스너 정리**: cleanup 함수 적절한 구현
+- **타이머 정리**: setTimeout/setInterval 해제
+- **메모리 누수 방지**: 순환 참조 방지
+
+## 🎨 디자인 시스템 통합
+
+### 1. 테마 시스템
+- **MUI 테마**: Material-UI 테마 정의 및 확장
+- **CSS 변수**: 브라우저 네이티브 CSS 커스텀 프로퍼티
+- **다크 모드**: 테마 전환 시스템 지원
+
+### 2. 컴포넌트 변형
+- **Variant 시스템**: primary, secondary, danger 등
+- **크기 시스템**: small, medium, large
+- **상태 표현**: loading, disabled, error 상태
+
+### 3. 접근성 (A11y)
+- **ARIA 지원**: 적절한 ARIA 속성 적용
+- **키보드 네비게이션**: 키보드만으로 조작 가능
+- **스크린 리더**: 시각 장애인을 위한 텍스트 제공
+
+## 🧪 테스트 전략
+
+### 1. 단위 테스트
+- **순수 함수**: 유틸리티 함수의 입출력 검증
+- **커스텀 훅**: React Testing Library로 훅 동작 테스트
+- **컴포넌트**: 다양한 props 조합에 대한 렌더링 테스트
+
+### 2. 통합 테스트
+- **스토어 테스트**: 상태 변경 시나리오 검증
+- **API 클라이언트**: 모킹을 통한 네트워크 레이어 테스트
+- **UI 상호작용**: 사용자 이벤트에 대한 컴포넌트 반응
+
+### 3. 시각적 회귀 테스트
+- **Storybook**: 컴포넌트 변형들의 시각적 문서화
+- **스냅샷 테스트**: UI 변경 감지
+- **크로스 브라우저**: 주요 브라우저별 렌더링 검증
+
+## 🎯 개발 가이드라인
+
+### 새로운 공통 컴포넌트 생성 시
+1. **범용성 검증**: 특정 도메인에 종속되지 않는지 확인
+2. **API 설계**: 유연하고 확장 가능한 Props 인터페이스
+3. **접근성 준수**: ARIA, 키보드 네비게이션 고려
+4. **스토리북 작성**: 다양한 사용 예시 문서화
+5. **테스트 작성**: 컴포넌트 동작 검증
+
+### 유틸리티 함수 작성 시
+1. **순수 함수**: 사이드 이펙트 없는 함수 작성
+2. **타입 안전성**: 명확한 입력/출력 타입 정의
+3. **에러 처리**: 예외 상황에 대한 적절한 처리
+4. **성능 고려**: 시간/공간 복잡도 최적화
+5. **문서화**: JSDoc을 통한 명확한 설명
+
+### 커스텀 훅 설계 시
+1. **단일 책임**: 하나의 명확한 기능만 담당
+2. **재사용성**: 다양한 컴포넌트에서 활용 가능
+3. **의존성 최소화**: 외부 의존성 최소한으로 제한
+4. **cleanup**: 메모리 누수 방지를 위한 정리 로직
+5. **타입 추론**: TypeScript 타입 추론 최적화
+
+### 주의사항
+1. **브레이킹 체인지**: Shared 변경은 전체 앱에 영향
+2. **하위 호환성**: 기존 사용법 유지 또는 마이그레이션 가이드
+3. **성능 영향**: 공통 컴포넌트의 성능은 전체 성능에 직결
+4. **의존성 관리**: 외부 라이브러리 추가 시 신중한 검토
+
+---
+
+## 📚 관련 문서
+
+- [🏗 FSD 아키텍처 가이드](../../docs/FSD_ARCHITECTURE.md)
+- [🎨 디자인 시스템](../app/styles/README.md)
+- [📊 상태 관리 가이드](./stores/README.md)
+
+---
+
+💡 **개발 팁**: Shared는 **모든 계층의 기반**입니다. 변경 시 영향도가 크므로 신중하게 설계하고, 항상 하위 호환성을 고려하세요!
\ No newline at end of file
diff --git a/src/shared/hooks/useCountUp.ts b/src/shared/hooks/useCountUp.ts
new file mode 100644
index 0000000..211bbf4
--- /dev/null
+++ b/src/shared/hooks/useCountUp.ts
@@ -0,0 +1,65 @@
+import { useEffect, useState } from "react";
+
+interface UseCountUpProps {
+ end: number;
+ duration?: number;
+ start?: number;
+ delay?: number;
+ enabled?: boolean;
+}
+
+export const useCountUp = ({
+ end,
+ duration = 2000,
+ start = 0,
+ delay = 0,
+ enabled = true,
+}: UseCountUpProps): { count: number; isComplete: boolean } => {
+ const [count, setCount] = useState(start);
+ const [isComplete, setIsComplete] = useState(false);
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ const timer = setTimeout(() => {
+ let startTimestamp: number | null = null;
+ const startValue = start;
+ const endValue = end;
+
+ const step = (timestamp: number): void => {
+ if (!startTimestamp) startTimestamp = timestamp;
+ const progress = Math.min((timestamp - startTimestamp) / duration, 1);
+
+ const easeProgress =
+ progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
+
+ const currentValue =
+ startValue + (endValue - startValue) * easeProgress;
+
+ let currentCount: number;
+ if (progress >= 0.98) {
+ currentCount = endValue;
+ } else if (progress >= 0.9) {
+ currentCount = Math.round(currentValue);
+ } else {
+ currentCount = Math.floor(currentValue);
+ }
+
+ setCount(currentCount);
+
+ if (progress >= 0.98) {
+ setCount(endValue);
+ setIsComplete(true);
+ } else {
+ requestAnimationFrame(step);
+ }
+ };
+
+ requestAnimationFrame(step);
+ }, delay);
+
+ return () => clearTimeout(timer);
+ }, [end, duration, start, delay, enabled]);
+
+ return { count, isComplete };
+};
diff --git a/src/shared/ui/LogoBox.tsx b/src/shared/ui/LogoBox.tsx
index 4a679a8..8f16dd7 100644
--- a/src/shared/ui/LogoBox.tsx
+++ b/src/shared/ui/LogoBox.tsx
@@ -118,11 +118,11 @@ const LogoText = styled("span")<{ $size: "small" | "medium" | "large" }>(
[theme.breakpoints.down("md")]: {
fontSize:
- $size === "small" ? "1rem" : $size === "medium" ? "1.3rem" : "1.5rem",
+ $size === "small" ? "1.4rem" : $size === "medium" ? "1.6rem" : "1.8rem",
},
[theme.breakpoints.down("sm")]: {
fontSize:
- $size === "small" ? "0.9rem" : $size === "medium" ? "1.1rem" : "1.2rem",
+ $size === "small" ? "1.2rem" : $size === "medium" ? "1.4rem" : "1.6rem",
},
})
);
diff --git a/src/shared/ui/home/WhiteInfoBox.tsx b/src/shared/ui/home/WhiteInfoBox.tsx
index fd29c84..8f85271 100644
--- a/src/shared/ui/home/WhiteInfoBox.tsx
+++ b/src/shared/ui/home/WhiteInfoBox.tsx
@@ -30,7 +30,9 @@ const WhiteInfoBox = ({
return (
-
+
@@ -47,6 +49,9 @@ const WhiteInfoBox = ({
export default WhiteInfoBox;
const ItemCard = styled(Card)`
+ height: 100%;
+ display: flex;
+ flex-direction: column;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-1rem);
@@ -56,6 +61,8 @@ const ItemCard = styled(Card)`
const ProjectStatsStack = styled(Stack)`
text-align: center;
gap: 0.8rem;
+ height: 100%;
+ justify-content: center;
`;
const ProjectStatsIcon = styled("div")(
@@ -92,6 +99,7 @@ const ProjectStatsCount = styled(Typography)(({ theme }) => ({
const ProjectStatsTitle = styled(Typography)(({ theme }) => ({
fontSize: "1.4rem",
+ wordBreak: "keep-all",
[theme.breakpoints.up("sm")]: {
fontSize: "1.6rem",
diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx
index 1f3fec5..0047202 100644
--- a/src/widgets/Footer/Footer.tsx
+++ b/src/widgets/Footer/Footer.tsx
@@ -67,7 +67,8 @@ const Footer = (): JSX.Element => {
-
+ {/* 네비게이션 컨테이너 */}
+
{
>
프로젝트 등록
-
-
- {/* 디벨로퍼즈 드롭다운 메뉴 */}
-
-
+
@@ -144,7 +141,7 @@ const FooterContent = styled(Box)(({ theme }) => ({
alignItems: "flex-start",
justifyContent: "space-between",
gap: theme.spacing(2),
- [theme.breakpoints.down("sm")]: {
+ [theme.breakpoints.down(1200)]: {
flexDirection: "column",
alignItems: "flex-start",
gap: theme.spacing(1),
@@ -157,7 +154,7 @@ const LogoSection = styled(Box)(({ theme }) => ({
justifyContent: "space-between",
gap: 8,
height: "100%",
- [theme.breakpoints.down("sm")]: {
+ [theme.breakpoints.down(1200)]: {
alignItems: "center",
width: "100%",
flexDirection: "column",
@@ -169,31 +166,24 @@ const InfoSection = styled(Box)(({ theme }) => ({
minWidth: 200,
marginLeft: 16,
marginTop: 52,
- [theme.breakpoints.down("sm")]: {
+ [theme.breakpoints.down(1200)]: {
marginTop: 0,
marginLeft: 0,
textAlign: "center",
},
}));
-const NavSection = styled(Box)(({ theme }) => ({
+const NavigationContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
- gap: 16,
- marginTop: 16,
- [theme.breakpoints.down("sm")]: {
- width: "100%",
- justifyContent: "center",
- },
-}));
+ gap: "1rem",
+ marginTop: "1rem",
-const DevelopersSection = styled(Box)(({ theme }) => ({
- display: "flex",
- alignItems: "center",
- gap: 16,
- [theme.breakpoints.down("sm")]: {
+ [theme.breakpoints.down("md")]: {
+ flexDirection: "column",
+ alignItems: "center",
+ gap: "0.5rem",
width: "100%",
- justifyContent: "center",
},
}));
diff --git a/src/widgets/Header/Header.tsx b/src/widgets/Header/Header.tsx
index 1447051..ef8ba7e 100644
--- a/src/widgets/Header/Header.tsx
+++ b/src/widgets/Header/Header.tsx
@@ -71,7 +71,7 @@ const Header = (): JSX.Element => {
color="inherit"
aria-label="menu"
onClick={() => setDrawerOpen(true)}
- sx={{ ml: 1, mr: 1, p: 1.5 }}
+ sx={{ ml: 0.5, mr: 0, p: 0.5 }}
>
diff --git a/src/widgets/README.md b/src/widgets/README.md
index b9423a7..8f30299 100644
--- a/src/widgets/README.md
+++ b/src/widgets/README.md
@@ -1,159 +1,221 @@
# Widgets Layer
-여러 Features를 조합하여 완성된 기능 블록을 제공하는 계층입니다.
+> 🧩 **복합 UI 컴포넌트 계층** - 여러 entities와 features를 조합한 재사용 가능한 위젯
-## 📁 구조
+## 📖 개요
+
+Widgets Layer는 **복합 UI 컴포넌트**를 관리하는 계층입니다. 여러 entities와 features를 조합하여 완전한 기능을 가진 UI 블록을 제공하며, 여러 페이지에서 재사용 가능한 큰 단위의 컴포넌트들을 포함합니다.
+
+## 📁 디렉토리 구조
```
-widgets/
-├── header/
-│ ├── ui/
-│ │ └── Header.tsx
-│ ├── hooks/
-│ │ └── useHeaderData.ts
-│ └── index.ts
-├── sidebar/
-│ ├── ui/
-│ │ └── Sidebar.tsx
-│ └── index.ts
-└── user-dashboard/
- ├── ui/
- │ └── UserDashboard.tsx
- ├── types/
- │ └── Dashboard.ts
- └── index.ts
+src/widgets/
+├── Header/ # 🧭 네비게이션 헤더
+│ ├── Header.tsx # 메인 헤더 컴포넌트
+│ └── index.ts # 공개 API
+├── Footer/ # 🦶 페이지 푸터
+│ ├── Footer.tsx # 푸터 컴포넌트
+│ └── index.ts # 공개 API
+├── hero/ # 🎯 히어로 섹션
+│ └── ui/
+│ ├── Hero.tsx # 메인 히어로 영역
+│ └── HowToStartTitle.tsx # 시작 가이드 제목
+└── BackToHome/ # 🏠 홈 이동 버튼
+ └── BackToHome.tsx # 고정 위치 홈 버튼
```
-## 🎯 역할
+## 🎯 위젯별 상세 기능
-- **기능 조합**: 여러 Features와 Entities를 조합하여 완성된 UI 블록 생성
-- **독립적 위젯**: 재사용 가능한 복합 컴포넌트
-- **레이아웃 컴포넌트**: 헤더, 사이드바, 푸터 등 레이아웃 요소
-- **대시보드**: 여러 정보를 종합한 종합 화면
+### 1. Header 위젯 (`Header/`)
+**역할**: 전체 애플리케이션의 네비게이션 허브
-## 📄 폴더별 표준 구조
+#### 주요 기능
+- **로고 & 브랜딩**: 프로젝트 잼 로고 및 홈 링크
+- **주요 네비게이션**: 프로젝트 목록, 프로젝트 등록 링크
+- **사용자 메뉴**: 로그인/로그아웃, 프로필 드롭다운
+- **반응형 디자인**: 모바일 햄버거 메뉴
+
+#### 인증 상태별 동작
+- **비로그인**: 로그인/회원가입 버튼 표시
+- **로그인**: 사용자 아바타 및 드롭다운 메뉴
+- **관리자**: 추가 관리 메뉴 항목
-각 위젯 폴더는 다음 구조를 따릅니다:
+### 2. Footer 위젯 (`Footer/`)
+**역할**: 사이트 전체 정보 및 링크 제공
-```
-widget-name/
-├── header/
-├── aside/
-├── footer/
-├── layout/
-```
+#### 주요 구성
+- **회사 정보**: 프로젝트 설명 및 연락처
+- **서비스 링크**: 이용약관, 개인정보처리방침
+- **소셜 미디어**: GitHub, Discord 등 커뮤니티 링크
+- **개발자 정보**: 팀 소개 및 기여자 목록
-## 🔧 사용 예시
-
-```typescript
-// widgets/header/ui/Header.tsx
-import { UserProfile } from '@entities/user';
-import { NotificationButton } from '@features/notification';
-import { SearchBar } from '@features/search';
-import { Logo } from '@shared/ui';
-
-const Header = (): JSX.Element => {
- return (
-
- );
-};
-
-export { Header };
-```
+#### 반응형 레이아웃
+- **데스크톱**: 4컬럼 레이아웃
+- **태블릿**: 2컬럼 스택
+- **모바일**: 1컬럼 세로 배치
-```typescript
-// widgets/user-dashboard/ui/UserDashboard.tsx
-import { UserStats } from '@entities/user';
-import { RecentActivity } from '@entities/activity';
-import { QuickActions } from '@features/user-actions';
-
-const UserDashboard = (): JSX.Element => {
- return (
-
-
-
-
-
- );
-};
-
-export { UserDashboard };
-```
+### 3. Hero 위젯 (`hero/`)
+**역할**: 메인 페이지의 핵심 메시지 전달
-```typescript
-// widgets/header/index.ts
-export { Header } from './ui/Header';
-export type { HeaderProps } from './types/Header';
-```
+#### 핵심 컴포넌트
+- **Hero.tsx**: 메인 캐치프레이즈와 CTA 버튼
+- **HowToStartTitle.tsx**: 서비스 이용 가이드 섹션
-## 📋 개발 가이드라인
-
-### 1. 위젯 명명 규칙
-- 폴더명: kebab-case (예: `user-dashboard`, `navigation-menu`)
-- 컴포넌트명: PascalCase (예: `UserDashboard`, `NavigationMenu`)
-
-### 2. 위젯 특징
-- **독립성**: 다른 위젯에 의존하지 않고 독립적으로 동작
-- **재사용성**: 여러 페이지에서 재사용 가능
-- **완성도**: 사용자에게 완전한 기능을 제공
-
-### 3. 조합 원칙
-- Features의 액션과 Entities의 데이터를 조합
-- 복잡한 비즈니스 로직은 Features에 위임
-- UI 조합에만 집중
-
-### 4. 의존성 규칙
-- Features, Entities, Shared 계층 참조 가능
-- 다른 Widgets 직접 참조 금지
-- Pages, App 계층 참조 금지
-
-## 🎨 위젯 유형별 예시
-
-### 레이아웃 위젯
-```typescript
-// 헤더, 사이드바, 푸터 등
-const Header = (): JSX.Element => {
- return (
-
- {/* Features와 Entities 조합 */}
-
- );
-};
-```
+#### 주요 기능
+- **임팩트 메시지**: "함께 만들어가는 사이드 프로젝트"
+- **CTA 버튼**: 프로젝트 둘러보기, 프로젝트 등록
+- **애니메이션**: Fade-in, Slide-up 효과
+- **통계 정보**: 등록된 프로젝트 수, 활성 사용자 등
+
+### 4. BackToHome 위젯 (`BackToHome/`)
+**역할**: 어디서든 빠른 홈 이동 제공
-### 대시보드 위젯
-```typescript
-// 여러 정보를 종합한 대시보드
-const AdminDashboard = (): JSX.Element => {
- return (
-
- {/* 여러 통계와 차트 조합 */}
-
- );
-};
-```
+#### 주요 특징
+- **고정 위치**: 화면 우하단 플로팅 버튼
+- **스크롤 인식**: 일정 스크롤 시 표시
+- **부드러운 애니메이션**: 나타나기/사라지기 효과
+- **접근성**: 키보드 네비게이션 지원
+
+## 🏗 아키텍처 패턴
-### 기능 위젯
-```typescript
-// 완성된 기능 단위
-const CommentSection = (): JSX.Element => {
- return (
-
- {/* 댓글 목록 + 댓글 작성 기능 조합 */}
-
- );
-};
-```
+### 1. Compound Component Pattern
+복잡한 위젯의 내부 구조를 컴포넌트로 분리하여 유연성 확보
+
+### 2. Container/Presenter Pattern
+- **Container**: 비즈니스 로직과 상태 관리
+- **Presenter**: UI 렌더링에만 집중
+
+### 3. Hook Composition Pattern
+위젯별 커스텀 훅으로 재사용 가능한 로직 분리
+
+## 🎨 위젯 설계 원칙
+
+### 재사용성 (Reusability)
+- **Props Interface**: 유연한 설정 옵션 제공
+- **Theming**: MUI 테마 시스템 완전 지원
+- **반응형**: 모든 화면 크기에서 동작
+
+### 독립성 (Independence)
+- **외부 의존성 최소화**: 필수 props만 요구
+- **내부 상태 관리**: 위젯 내부에서 상태 완결
+- **에러 바운더리**: 위젯 오류가 전체에 영향 미치지 않음
+
+### 확장성 (Extensibility)
+- **슬롯 패턴**: 내부 컴포넌트 교체 가능
+- **이벤트 핸들러**: 외부에서 커스텀 동작 주입
+- **스타일 오버라이드**: 테마를 통한 스타일 커스터마이징
+
+## 🎯 반응형 디자인 전략
+
+### 브레이크포인트 시스템
+- **xs (0px)**: 모바일 세로
+- **sm (600px)**: 모바일 가로
+- **md (900px)**: 태블릿
+- **lg (1200px)**: 소형 데스크톱
+- **xl (1536px)**: 대형 데스크톱
+
+### 모바일 퍼스트 접근
+1. **기본**: 모바일 레이아웃으로 설계
+2. **확장**: 큰 화면을 위한 점진적 개선
+3. **터치**: 터치 친화적 인터랙션
+
+## 🎨 테마 및 스타일링
+
+### MUI 테마 통합
+- **팔레트**: 프로젝트 브랜드 컬러 시스템
+- **타이포그래피**: Pretendard 폰트 패밀리
+- **컴포넌트**: 일관된 스타일 오버라이드
+
+### CSS-in-JS 전략
+- **Emotion**: MUI의 기본 CSS-in-JS 엔진
+- **sx prop**: 인라인 스타일링
+- **styled**: 재사용 컴포넌트 스타일링
+
+### 다크 모드 지원
+- **테마 토글**: Header에서 다크/라이트 모드 전환
+- **자동 감지**: 시스템 설정 기반 초기 테마
+- **지속성**: localStorage를 통한 설정 유지
+
+## ⚡ 성능 최적화
+
+### 렌더링 최적화
+- **React.memo**: 불필요한 리렌더링 방지
+- **useMemo/useCallback**: 계산 비용이 큰 연산 메모이제이션
+- **지연 로딩**: 무거운 컴포넌트의 동적 임포트
+
+### 번들 최적화
+- **Tree Shaking**: 사용하지 않는 코드 제거
+- **Code Splitting**: 위젯별 청크 분할
+- **Dynamic Import**: 조건부 위젯 로딩
+
+### 이미지 최적화
+- **WebP 포맷**: 모던 브라우저 최적화
+- **Lazy Loading**: 뷰포트 진입 시 로딩
+- **적응형 이미지**: 디바이스별 최적 해상도
+
+## 🔧 접근성 (A11y) 지원
+
+### ARIA 표준 준수
+- **역할 정의**: role 속성을 통한 의미 전달
+- **상태 관리**: aria-expanded, aria-selected 등
+- **레이블링**: aria-label, aria-describedby 활용
+
+### 키보드 네비게이션
+- **Tab 순서**: 논리적인 포커스 흐름
+- **단축키**: 주요 액션에 대한 키보드 단축키
+- **포커스 표시**: 명확한 포커스 인디케이터
+
+### 스크린 리더 지원
+- **의미 있는 텍스트**: alt 텍스트, 버튼 레이블
+- **구조 정보**: 헤딩 계층, 랜드마크 역할
+- **상태 알림**: 동적 콘텐츠 변경 알림
+
+## 🧪 테스트 전략
+
+### 단위 테스트
+- **컴포넌트 렌더링**: 기본 렌더링 확인
+- **Props 전달**: 다양한 props 조합 테스트
+- **이벤트 핸들링**: 사용자 인터랙션 시뮬레이션
-## ⚠️ 주의사항
+### 통합 테스트
+- **위젯 간 상호작용**: Header와 다른 컴포넌트 연동
+- **라우팅 테스트**: 네비게이션 동작 확인
+- **반응형 테스트**: 다양한 화면 크기 검증
+
+### 시각적 회귀 테스트
+- **Storybook**: 위젯 스토리 작성
+- **스냅샷 테스트**: UI 변경 감지
+- **크로스 브라우저**: 주요 브라우저 호환성
+
+## 🎯 개발 가이드라인
+
+### 새로운 위젯 생성 시
+1. **디렉토리 구조**: `src/widgets/WidgetName/`
+2. **인덱스 파일**: 공개 API 정의
+3. **타입스크립트**: Props 인터페이스 정의
+4. **스토리북**: 개발 및 테스트용 스토리
+5. **문서화**: README 또는 JSDoc 주석
+
+### 기존 위젯 수정 시
+1. **영향도 분석**: 사용하는 페이지 확인
+2. **하위 호환성**: Props 변경 시 기존 사용법 유지
+3. **테스트 업데이트**: 변경사항에 맞는 테스트 수정
+4. **문서 갱신**: 변경된 API 문서화
+
+### 성능 고려사항
+1. **렌더링 최적화**: 불필요한 리렌더링 방지
+2. **메모리 누수**: 이벤트 리스너 정리
+3. **번들 크기**: 큰 의존성 동적 임포트
+4. **로딩 시간**: 초기 렌더링 최적화
+
+---
+
+## 📚 관련 문서
+
+- [🏗 FSD 아키텍처 가이드](../../docs/FSD_ARCHITECTURE.md)
+- [🎨 디자인 시스템](../app/styles/README.md)
+- [🧩 UI 컴포넌트](../shared/ui/README.md)
+
+---
-- 위젯은 완성된 기능 블록이므로 Pages에서 바로 사용 가능해야 합니다
-- 다른 위젯과의 직접적인 의존성은 피하세요
-- 위젯 내부에서 복잡한 상태 관리가 필요하면 Features로 분리를 고려하세요
\ No newline at end of file
+💡 **개발 팁**: 위젯은 **큰 단위의 UI 블록**입니다. 단순한 버튼이나 입력 필드보다는 Header, Footer 같은 완전한 기능을 가진 컴포넌트를 만드세요!
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index 269c5c8..64cba2b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -47,7 +47,7 @@ export default defineConfig(({ mode }) => ({
},
},
},
- publicDir: "src/app/public",
+ publicDir: "../../public",
server: {
port: 3000,
hmr: {