/`, ๋จ์ผ ์ง๊ณ ์ง์ ์ธ `api/router.py`, ๋ฐํ์ ๋ชจ๋์ ๊ฑฐ์ธ์ฒ๋ผ ๋ฐ๋ผ๊ฐ๋ `tests/`.
+- **๋๋ฉ์ธ ์ถ๊ฐ**: `items/` ๋ณต์ฌ โ ์ํฐํฐ / ์คํค๋ง / ํด๋์ค ์ด๋ฆ ๋ณ๊ฒฝ โ `__init__.py` ์ ์ฌexport ๊ฐฑ์ โ `src/app/api/router.py` ์ ๋ผ์ฐํฐ ๋ฑ๋ก โ ํ
์คํธ ๋ชจ๋ ์ถ๊ฐ. `main.py` ์์ ์ ์์ต๋๋ค.
diff --git a/docs/ko/tutorial/first-project.md b/docs/ko/tutorial/first-project.md
new file mode 100644
index 0000000..7fabfa5
--- /dev/null
+++ b/docs/ko/tutorial/first-project.md
@@ -0,0 +1,1252 @@
+# ์ฒซ ํ๋ก์ ํธ ๋ง๋ค๊ธฐ
+
+FastAPI-fastkit์ผ๋ก ์ฌ์ฉ์ ๊ด๋ฆฌ, ๊ฒ์๋ฌผ ์์ฑ, ๋๊ธ ์์คํ
์ ๊ฐ์ถ ์์ ํ ๋ธ๋ก๊ทธ API๋ฅผ ๊ตฌ์ถํฉ๋๋ค.
+
+## ํ๋ก์ ํธ ๊ฐ์
+
+์ด ํํ ๋ฆฌ์ผ์์๋ ๋ค์ ๊ธฐ๋ฅ์ ๊ฐ์ถ **๋ธ๋ก๊ทธ API** ๋ฅผ ๋ง๋ญ๋๋ค:
+
+- **์ฌ์ฉ์ ๊ด๋ฆฌ**: ํ์ ๊ฐ์
, ์ธ์ฆ, ์ฌ์ฉ์ ํ๋กํ
+- **๊ฒ์๋ฌผ ๊ด๋ฆฌ**: ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ ์์ฑ, ์กฐํ, ๊ฐฑ์ , ์ญ์
+- **๋๊ธ ์์คํ
**: ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ์ ๋๊ธ ์ถ๊ฐ
+- **๋ฐ์ดํฐ ๊ฒ์ฆ**: ๊ฒฌ๊ณ ํ ์
๋ ฅ ๊ฒ์ฆ ๋ฐ ์๋ฌ ์ฒ๋ฆฌ
+- **API ๋ฌธ์ํ**: ์๋ OpenAPI ๋ฌธ์
+- **ํ
์คํธ**: ์์ ํ ํ
์คํธ ์ค์ํธ
+
+### ๋ฐฐ์ฐ๋ ๋ด์ฉ
+
+์ด ํํ ๋ฆฌ์ผ์ด ๋๋ ๋๋ฉด ๋ค์์ ์ดํดํ๊ฒ ๋ฉ๋๋ค:
+
+- ๊ณ ๊ธ FastAPI-fastkit ํ๋ก์ ํธ ๊ตฌ์กฐ
+- SQLAlchemy์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ
+- ์ฌ์ฉ์ ์ธ์ฆ๊ณผ ๊ถํ ๋ถ์ฌ
+- ๋ณต์กํ ๋ฐ์ดํฐ ๊ด๊ณ
+- ์๋ฌ ์ฒ๋ฆฌ์ ๊ฒ์ฆ
+- ํ
์คํธ ๋ชจ๋ฒ ์ฌ๋ก
+
+## ์ฌ์ ์๊ตฌ ์ฌํญ
+
+์์ํ๊ธฐ ์ ์ ๋ค์์ ๊ฐ์ถฐ ๋์ธ์:
+
+- [์์ํ๊ธฐ](getting-started.md) ํํ ๋ฆฌ์ผ ์๋ฃ
+- REST API์ ๊ธฐ๋ณธ ์ดํด
+- Python 3.12+ ์ค์น
+- ํ
์คํธ ์๋ํฐ ๋๋ IDE ์ค๋น
+
+## 1๋จ๊ณ: ํ๋ก์ ํธ ์์ฑ
+
+๋ฐ์ดํฐ๋ฒ ์ด์ค ์ง์์ ์ํด **STANDARD** ์คํ์ผ๋ก ์ ํ๋ก์ ํธ๋ฅผ ์์ํฉ๋๋ค:
+
+
+
+```console
+$ fastkit init
+Enter the project name: blog-api
+Enter the author name: Your Name
+Enter the author email: your.email@example.com
+Enter the project description: A complete blog API with users, posts, and comments
+
+ Project Information
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Project Name โ blog-api โ
+โ Author โ Your Name โ
+โ Author Email โ your.email@example.com โ
+โ Description โ A complete blog API with users, posts, โ
+โ โ and comments โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Available Stacks and Dependencies:
+ MINIMAL Stack
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
+โ Dependency 1 โ fastapi โ
+โ Dependency 2 โ uvicorn โ
+โ Dependency 3 โ pydantic โ
+โ Dependency 4 โ pydantic-settings โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโ
+
+ STANDARD Stack
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
+โ Dependency 1 โ fastapi โ
+โ Dependency 2 โ uvicorn โ
+โ Dependency 3 โ sqlalchemy โ
+โ Dependency 4 โ alembic โ
+โ Dependency 5 โ pytest โ
+โ Dependency 6 โ pydantic โ
+โ Dependency 7 โ pydantic-settings โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโ
+
+Select stack (minimal, standard, full): standard
+
+Available Package Managers:
+ Package Managers
+โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ PIP โ Standard Python package manager โ
+โ UV โ Fast Python package manager โ
+โ PDM โ Modern Python dependency management โ
+โ POETRY โ Python dependency management and packaging โ
+โโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Select package manager (pip, uv, pdm, poetry) [uv]: uv
+Do you want to proceed with project creation? [y/N]: y
+
+โจ FastAPI project 'blog-api' has been created successfully!
+```
+
+
+
+## 2๋จ๊ณ: ํ๋ก์ ํธ ์ค์
+
+ํ๋ก์ ํธ๋ก ์ด๋ํด ๊ฐ์ ํ๊ฒฝ์ ํ์ฑํํฉ๋๋ค:
+
+
+
+```console
+$ cd blog-api
+$ source .venv/bin/activate
+```
+
+
+
+## 3๋จ๊ณ: ํ์ํ ๋ผ์ฐํธ ์ถ๊ฐ
+
+๋ธ๋ก๊ทธ API์ ์ฃผ์ ๋ฆฌ์์ค๋ฅผ ์ถ๊ฐํฉ๋๋ค:
+
+
+
+```console
+$ fastkit addroute blog-api users
+โจ Successfully added new route 'users' to project 'blog-api'
+
+$ fastkit addroute blog-api posts
+โจ Successfully added new route 'posts' to project 'blog-api'
+
+$ fastkit addroute blog-api comments
+โจ Successfully added new route 'comments' to project 'blog-api'
+```
+
+
+
+## 4๋จ๊ณ: ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ
+
+๋ฐ์ดํฐ ์คํค๋ง๋ฅผ ์ค๊ณํด ๋ด
์๋ค. ๋จผ์ ์ฌ์ฉ์ ์คํค๋ง๋ฅผ ์ข ๋ ํ์ค์ ์ผ๋ก ๊ฐฑ์ ํฉ๋๋ค.
+
+### User ์คํค๋ง ๊ฐฑ์
+
+`src/schemas/users.py` ๋ฅผ ์์ ํฉ๋๋ค:
+
+```python
+from typing import Optional, List
+from datetime import datetime
+from pydantic import BaseModel, EmailStr, Field
+
+class UserBase(BaseModel):
+ email: EmailStr
+ username: str = Field(..., min_length=3, max_length=50)
+ full_name: Optional[str] = None
+ bio: Optional[str] = Field(None, max_length=500)
+ is_active: bool = True
+
+class UserCreate(UserBase):
+ password: str = Field(..., min_length=8)
+
+class UserUpdate(BaseModel):
+ email: Optional[EmailStr] = None
+ username: Optional[str] = Field(None, min_length=3, max_length=50)
+ full_name: Optional[str] = None
+ bio: Optional[str] = Field(None, max_length=500)
+ is_active: Optional[bool] = None
+
+class User(UserBase):
+ id: int
+ created_at: datetime
+ posts_count: int = 0
+
+ class Config:
+ from_attributes = True
+
+class UserInDB(User):
+ hashed_password: str
+```
+
+### Post ์คํค๋ง ์์ฑ
+
+`src/schemas/posts.py` ๋ฅผ ์์ ํฉ๋๋ค:
+
+```python
+from typing import Optional, List
+from datetime import datetime
+from pydantic import BaseModel, Field
+
+class PostBase(BaseModel):
+ title: str = Field(..., min_length=1, max_length=200)
+ content: str = Field(..., min_length=1)
+ published: bool = True
+
+class PostCreate(PostBase):
+ pass
+
+class PostUpdate(BaseModel):
+ title: Optional[str] = Field(None, min_length=1, max_length=200)
+ content: Optional[str] = Field(None, min_length=1)
+ published: Optional[bool] = None
+
+class Post(PostBase):
+ id: int
+ author_id: int
+ created_at: datetime
+ updated_at: datetime
+ comments_count: int = 0
+
+ class Config:
+ from_attributes = True
+
+class PostWithAuthor(Post):
+ author: "User"
+
+class PostWithComments(Post):
+ comments: List["Comment"] = []
+
+# ์ํ import ํํผ๋ฅผ ์ํ import
+from src.schemas.users import User
+from src.schemas.comments import Comment
+PostWithAuthor.model_rebuild()
+PostWithComments.model_rebuild()
+```
+
+### Comment ์คํค๋ง ์์ฑ
+
+`src/schemas/comments.py` ๋ฅผ ์์ ํฉ๋๋ค:
+
+```python
+from typing import Optional
+from datetime import datetime
+from pydantic import BaseModel, Field
+
+class CommentBase(BaseModel):
+ content: str = Field(..., min_length=1, max_length=1000)
+
+class CommentCreate(CommentBase):
+ post_id: int
+
+class CommentUpdate(BaseModel):
+ content: Optional[str] = Field(None, min_length=1, max_length=1000)
+
+class Comment(CommentBase):
+ id: int
+ post_id: int
+ author_id: int
+ created_at: datetime
+ updated_at: datetime
+
+ class Config:
+ from_attributes = True
+
+class CommentWithAuthor(Comment):
+ author: "User"
+
+# ์ํ import ํํผ๋ฅผ ์ํ import
+from src.schemas.users import User
+CommentWithAuthor.model_rebuild()
+```
+
+## 5๋จ๊ณ: ๊ณ ๊ธ CRUD ์์
๊ตฌํ
+
+### ํฅ์๋ User CRUD
+
+`src/crud/users.py` ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค:
+
+```python
+from typing import List, Optional
+from datetime import datetime
+import hashlib
+from src.schemas.users import UserCreate, UserUpdate, UserInDB
+
+class UsersCRUD:
+ def __init__(self):
+ self._users: List[UserInDB] = []
+ self._next_id = 1
+
+ def _hash_password(self, password: str) -> str:
+ """Simple password hashing (use bcrypt in production)"""
+ return hashlib.sha256(password.encode()).hexdigest()
+
+ def _verify_password(self, plain_password: str, hashed_password: str) -> bool:
+ """Verify password against hash"""
+ return self._hash_password(plain_password) == hashed_password
+
+ def get_all(self) -> List[UserInDB]:
+ """Get all users"""
+ return [user for user in self._users if user.is_active]
+
+ def get_by_id(self, user_id: int) -> Optional[UserInDB]:
+ """Get user by ID"""
+ return next((user for user in self._users if user.id == user_id), None)
+
+ def get_by_email(self, email: str) -> Optional[UserInDB]:
+ """Get user by email"""
+ return next((user for user in self._users if user.email == email), None)
+
+ def get_by_username(self, username: str) -> Optional[UserInDB]:
+ """Get user by username"""
+ return next((user for user in self._users if user.username == username), None)
+
+ def create(self, user: UserCreate) -> UserInDB:
+ """Create a new user with validation"""
+ # Check for duplicates
+ if self.get_by_email(user.email):
+ raise ValueError("Email already registered")
+ if self.get_by_username(user.username):
+ raise ValueError("Username already taken")
+
+ new_user = UserInDB(
+ id=self._next_id,
+ email=user.email,
+ username=user.username,
+ full_name=user.full_name,
+ bio=user.bio,
+ is_active=user.is_active,
+ created_at=datetime.now(),
+ posts_count=0,
+ hashed_password=self._hash_password(user.password)
+ )
+ self._next_id += 1
+ self._users.append(new_user)
+ return new_user
+
+ def update(self, user_id: int, user_update: UserUpdate) -> Optional[UserInDB]:
+ """Update an existing user"""
+ user = self.get_by_id(user_id)
+ if not user:
+ return None
+
+ # Check for duplicates on email/username changes
+ update_data = user_update.dict(exclude_unset=True)
+ if "email" in update_data and update_data["email"] != user.email:
+ if self.get_by_email(update_data["email"]):
+ raise ValueError("Email already registered")
+
+ if "username" in update_data and update_data["username"] != user.username:
+ if self.get_by_username(update_data["username"]):
+ raise ValueError("Username already taken")
+
+ for field, value in update_data.items():
+ setattr(user, field, value)
+
+ return user
+
+ def delete(self, user_id: int) -> bool:
+ """Soft delete user (deactivate)"""
+ user = self.get_by_id(user_id)
+ if user:
+ user.is_active = False
+ return True
+ return False
+
+ def authenticate(self, email: str, password: str) -> Optional[UserInDB]:
+ """Authenticate user by email and password"""
+ user = self.get_by_email(email)
+ if user and self._verify_password(password, user.hashed_password):
+ return user
+ return None
+
+users_crud = UsersCRUD()
+```
+
+### Posts CRUD
+
+`src/crud/posts.py` ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค:
+
+```python
+from typing import List, Optional
+from datetime import datetime
+from src.schemas.posts import PostCreate, PostUpdate, Post
+
+class PostsCRUD:
+ def __init__(self):
+ self._posts: List[Post] = []
+ self._next_id = 1
+
+ def get_all(self, skip: int = 0, limit: int = 100, published_only: bool = True) -> List[Post]:
+ """Get all posts with pagination"""
+ posts = self._posts
+ if published_only:
+ posts = [post for post in posts if post.published]
+ return posts[skip:skip + limit]
+
+ def get_by_id(self, post_id: int) -> Optional[Post]:
+ """Get post by ID"""
+ return next((post for post in self._posts if post.id == post_id), None)
+
+ def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Post]:
+ """Get posts by author"""
+ author_posts = [post for post in self._posts if post.author_id == author_id]
+ return author_posts[skip:skip + limit]
+
+ def create(self, post: PostCreate, author_id: int) -> Post:
+ """Create a new post"""
+ now = datetime.now()
+ new_post = Post(
+ id=self._next_id,
+ title=post.title,
+ content=post.content,
+ published=post.published,
+ author_id=author_id,
+ created_at=now,
+ updated_at=now,
+ comments_count=0
+ )
+ self._next_id += 1
+ self._posts.append(new_post)
+
+ # Update author's post count
+ from src.crud.users import users_crud
+ author = users_crud.get_by_id(author_id)
+ if author:
+ author.posts_count += 1
+
+ return new_post
+
+ def update(self, post_id: int, post_update: PostUpdate, author_id: int) -> Optional[Post]:
+ """Update an existing post"""
+ post = self.get_by_id(post_id)
+ if not post or post.author_id != author_id:
+ return None
+
+ update_data = post_update.dict(exclude_unset=True)
+ for field, value in update_data.items():
+ setattr(post, field, value)
+
+ post.updated_at = datetime.now()
+ return post
+
+ def delete(self, post_id: int, author_id: int) -> bool:
+ """Delete a post"""
+ post = self.get_by_id(post_id)
+ if post and post.author_id == author_id:
+ self._posts.remove(post)
+
+ # Update author's post count
+ from src.crud.users import users_crud
+ author = users_crud.get_by_id(author_id)
+ if author:
+ author.posts_count = max(0, author.posts_count - 1)
+
+ return True
+ return False
+
+ def search(self, query: str, skip: int = 0, limit: int = 100) -> List[Post]:
+ """Search posts by title or content"""
+ query_lower = query.lower()
+ matching_posts = [
+ post for post in self._posts
+ if post.published and (
+ query_lower in post.title.lower() or
+ query_lower in post.content.lower()
+ )
+ ]
+ return matching_posts[skip:skip + limit]
+
+posts_crud = PostsCRUD()
+```
+
+### Comments CRUD
+
+`src/crud/comments.py` ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค:
+
+```python
+from typing import List, Optional
+from datetime import datetime
+from src.schemas.comments import CommentCreate, CommentUpdate, Comment
+
+class CommentsCRUD:
+ def __init__(self):
+ self._comments: List[Comment] = []
+ self._next_id = 1
+
+ def get_all(self) -> List[Comment]:
+ """Get all comments"""
+ return self._comments
+
+ def get_by_id(self, comment_id: int) -> Optional[Comment]:
+ """Get comment by ID"""
+ return next((comment for comment in self._comments if comment.id == comment_id), None)
+
+ def get_by_post(self, post_id: int, skip: int = 0, limit: int = 100) -> List[Comment]:
+ """Get comments for a specific post"""
+ post_comments = [comment for comment in self._comments if comment.post_id == post_id]
+ return post_comments[skip:skip + limit]
+
+ def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Comment]:
+ """Get comments by author"""
+ author_comments = [comment for comment in self._comments if comment.author_id == author_id]
+ return author_comments[skip:skip + limit]
+
+ def create(self, comment: CommentCreate, author_id: int) -> Comment:
+ """Create a new comment"""
+ # Verify post exists
+ from src.crud.posts import posts_crud
+ post = posts_crud.get_by_id(comment.post_id)
+ if not post:
+ raise ValueError("Post not found")
+
+ now = datetime.now()
+ new_comment = Comment(
+ id=self._next_id,
+ content=comment.content,
+ post_id=comment.post_id,
+ author_id=author_id,
+ created_at=now,
+ updated_at=now
+ )
+ self._next_id += 1
+ self._comments.append(new_comment)
+
+ # Update post's comment count
+ post.comments_count += 1
+
+ return new_comment
+
+ def update(self, comment_id: int, comment_update: CommentUpdate, author_id: int) -> Optional[Comment]:
+ """Update an existing comment"""
+ comment = self.get_by_id(comment_id)
+ if not comment or comment.author_id != author_id:
+ return None
+
+ update_data = comment_update.dict(exclude_unset=True)
+ for field, value in update_data.items():
+ setattr(comment, field, value)
+
+ comment.updated_at = datetime.now()
+ return comment
+
+ def delete(self, comment_id: int, author_id: int) -> bool:
+ """Delete a comment"""
+ comment = self.get_by_id(comment_id)
+ if comment and comment.author_id == author_id:
+ self._comments.remove(comment)
+
+ # Update post's comment count
+ from src.crud.posts import posts_crud
+ post = posts_crud.get_by_id(comment.post_id)
+ if post:
+ post.comments_count = max(0, post.comments_count - 1)
+
+ return True
+ return False
+
+comments_crud = CommentsCRUD()
+```
+
+## 6๋จ๊ณ: ๊ณ ๊ธ API ๋ผ์ฐํธ ๊ตฌํ
+
+### ํฅ์๋ User ๋ผ์ฐํธ
+
+`src/api/routes/users.py` ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค:
+
+```python
+from typing import List
+from fastapi import APIRouter, HTTPException, status, Depends, Query
+from src.schemas.users import User, UserCreate, UserUpdate
+from src.crud.users import users_crud
+
+router = APIRouter()
+
+# Helper function to get current user (simplified for tutorial)
+def get_current_user_id() -> int:
+ # In a real app, this would verify JWT token and return user ID
+ return 1 # For tutorial purposes
+
+@router.get("/", response_model=List[User])
+def read_users(
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=100)
+):
+ """Get all users with pagination"""
+ users = users_crud.get_all()[skip:skip + limit]
+ return [User(**user.dict()) for user in users]
+
+@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
+def create_user(user: UserCreate):
+ """Register a new user"""
+ try:
+ new_user = users_crud.create(user)
+ return User(**new_user.dict())
+ except ValueError as e:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e)
+ )
+
+@router.get("/{user_id}", response_model=User)
+def read_user(user_id: int):
+ """Get a specific user"""
+ user = users_crud.get_by_id(user_id)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"User with id {user_id} not found"
+ )
+ return User(**user.dict())
+
+@router.put("/{user_id}", response_model=User)
+def update_user(
+ user_id: int,
+ user_update: UserUpdate,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Update user profile"""
+ if user_id != current_user_id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="You can only update your own profile"
+ )
+
+ try:
+ updated_user = users_crud.update(user_id, user_update)
+ if not updated_user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User not found"
+ )
+ return User(**updated_user.dict())
+ except ValueError as e:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e)
+ )
+
+@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_user(
+ user_id: int,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Deactivate user account"""
+ if user_id != current_user_id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="You can only delete your own account"
+ )
+
+ success = users_crud.delete(user_id)
+ if not success:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User not found"
+ )
+
+@router.post("/login")
+def login(email: str, password: str):
+ """Authenticate user"""
+ user = users_crud.authenticate(email, password)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid email or password"
+ )
+
+ # In a real app, return JWT token
+ return {
+ "message": "Login successful",
+ "user_id": user.id,
+ "username": user.username
+ }
+```
+
+### ํฅ์๋ Posts ๋ผ์ฐํธ
+
+`src/api/routes/posts.py` ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค:
+
+```python
+from typing import List, Optional
+from fastapi import APIRouter, HTTPException, status, Depends, Query
+from src.schemas.posts import Post, PostCreate, PostUpdate
+from src.crud.posts import posts_crud
+
+router = APIRouter()
+
+def get_current_user_id() -> int:
+ return 1 # Simplified for tutorial
+
+@router.get("/", response_model=List[Post])
+def read_posts(
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=100),
+ search: Optional[str] = Query(None)
+):
+ """Get all posts with optional search"""
+ if search:
+ posts = posts_crud.search(search, skip, limit)
+ else:
+ posts = posts_crud.get_all(skip, limit)
+ return posts
+
+@router.post("/", response_model=Post, status_code=status.HTTP_201_CREATED)
+def create_post(
+ post: PostCreate,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Create a new blog post"""
+ new_post = posts_crud.create(post, current_user_id)
+ return new_post
+
+@router.get("/{post_id}", response_model=Post)
+def read_post(post_id: int):
+ """Get a specific post"""
+ post = posts_crud.get_by_id(post_id)
+ if not post:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Post not found"
+ )
+ return post
+
+@router.put("/{post_id}", response_model=Post)
+def update_post(
+ post_id: int,
+ post_update: PostUpdate,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Update a blog post"""
+ updated_post = posts_crud.update(post_id, post_update, current_user_id)
+ if not updated_post:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Post not found or you don't have permission to edit it"
+ )
+ return updated_post
+
+@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_post(
+ post_id: int,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Delete a blog post"""
+ success = posts_crud.delete(post_id, current_user_id)
+ if not success:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Post not found or you don't have permission to delete it"
+ )
+
+@router.get("/author/{author_id}", response_model=List[Post])
+def read_posts_by_author(
+ author_id: int,
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=100)
+):
+ """Get posts by a specific author"""
+ posts = posts_crud.get_by_author(author_id, skip, limit)
+ return posts
+```
+
+### ํฅ์๋ Comments ๋ผ์ฐํธ
+
+`src/api/routes/comments.py` ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค:
+
+```python
+from typing import List
+from fastapi import APIRouter, HTTPException, status, Depends, Query
+from src.schemas.comments import Comment, CommentCreate, CommentUpdate
+from src.crud.comments import comments_crud
+
+router = APIRouter()
+
+def get_current_user_id() -> int:
+ return 1 # Simplified for tutorial
+
+@router.get("/", response_model=List[Comment])
+def read_comments(
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=100)
+):
+ """Get all comments"""
+ comments = comments_crud.get_all()[skip:skip + limit]
+ return comments
+
+@router.post("/", response_model=Comment, status_code=status.HTTP_201_CREATED)
+def create_comment(
+ comment: CommentCreate,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Create a new comment"""
+ try:
+ new_comment = comments_crud.create(comment, current_user_id)
+ return new_comment
+ except ValueError as e:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e)
+ )
+
+@router.get("/{comment_id}", response_model=Comment)
+def read_comment(comment_id: int):
+ """Get a specific comment"""
+ comment = comments_crud.get_by_id(comment_id)
+ if not comment:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Comment not found"
+ )
+ return comment
+
+@router.put("/{comment_id}", response_model=Comment)
+def update_comment(
+ comment_id: int,
+ comment_update: CommentUpdate,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Update a comment"""
+ updated_comment = comments_crud.update(comment_id, comment_update, current_user_id)
+ if not updated_comment:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Comment not found or you don't have permission to edit it"
+ )
+ return updated_comment
+
+@router.delete("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_comment(
+ comment_id: int,
+ current_user_id: int = Depends(get_current_user_id)
+):
+ """Delete a comment"""
+ success = comments_crud.delete(comment_id, current_user_id)
+ if not success:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Comment not found or you don't have permission to delete it"
+ )
+
+@router.get("/post/{post_id}", response_model=List[Comment])
+def read_comments_by_post(
+ post_id: int,
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=100)
+):
+ """Get comments for a specific post"""
+ comments = comments_crud.get_by_post(post_id, skip, limit)
+ return comments
+
+@router.get("/author/{author_id}", response_model=List[Comment])
+def read_comments_by_author(
+ author_id: int,
+ skip: int = Query(0, ge=0),
+ limit: int = Query(100, ge=1, le=100)
+):
+ """Get comments by a specific author"""
+ comments = comments_crud.get_by_author(author_id, skip, limit)
+ return comments
+```
+
+## 7๋จ๊ณ: ๋ธ๋ก๊ทธ API ํ
์คํธ
+
+์๋ฒ๋ฅผ ์์ํ๊ณ ์์ฑ๋ ๋ธ๋ก๊ทธ API๋ฅผ ํ
์คํธํฉ๋๋ค:
+
+
+
+```console
+$ fastkit runserver
+INFO: Uvicorn running on http://127.0.0.1:8000
+```
+
+
+
+### ์ฌ์ฉ์ ํ์ ๊ฐ์
ํ
์คํธ
+
+
+
+```console
+$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "email": "john@example.com",
+ "username": "john_doe",
+ "full_name": "John Doe",
+ "bio": "Software developer and blogger",
+ "password": "securepassword123"
+ }'
+
+{
+ "id": 1,
+ "email": "john@example.com",
+ "username": "john_doe",
+ "full_name": "John Doe",
+ "bio": "Software developer and blogger",
+ "is_active": true,
+ "created_at": "2023-12-07T10:30:00",
+ "posts_count": 0
+}
+```
+
+
+
+### ์ฌ์ฉ์ ๋ก๊ทธ์ธ ํ
์คํธ
+
+
+
+```console
+$ curl -X POST "http://127.0.0.1:8000/api/v1/users/login" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "email": "john@example.com",
+ "password": "securepassword123"
+ }'
+
+{
+ "message": "Login successful",
+ "user_id": 1,
+ "username": "john_doe"
+}
+```
+
+
+
+### ๊ฒ์๋ฌผ ์์ฑ ํ
์คํธ
+
+
+
+```console
+$ curl -X POST "http://127.0.0.1:8000/api/v1/posts/" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "title": "My First Blog Post",
+ "content": "This is the content of my first blog post. It'\''s about learning FastAPI with FastAPI-fastkit!",
+ "published": true
+ }'
+
+{
+ "id": 1,
+ "title": "My First Blog Post",
+ "content": "This is the content of my first blog post. It's about learning FastAPI with FastAPI-fastkit!",
+ "published": true,
+ "author_id": 1,
+ "created_at": "2023-12-07T10:35:00",
+ "updated_at": "2023-12-07T10:35:00",
+ "comments_count": 0
+}
+```
+
+
+
+### ๋๊ธ ์์ฑ ํ
์คํธ
+
+
+
+```console
+$ curl -X POST "http://127.0.0.1:8000/api/v1/comments/" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "content": "Great post! I learned a lot from this.",
+ "post_id": 1
+ }'
+
+{
+ "id": 1,
+ "content": "Great post! I learned a lot from this.",
+ "post_id": 1,
+ "author_id": 1,
+ "created_at": "2023-12-07T10:40:00",
+ "updated_at": "2023-12-07T10:40:00"
+}
+```
+
+
+
+### ๊ฒ์ ๊ธฐ๋ฅ ํ
์คํธ
+
+
+
+```console
+$ curl "http://127.0.0.1:8000/api/v1/posts/?search=FastAPI"
+
+[
+ {
+ "id": 1,
+ "title": "My First Blog Post",
+ "content": "This is the content of my first blog post. It's about learning FastAPI with FastAPI-fastkit!",
+ "published": true,
+ "author_id": 1,
+ "created_at": "2023-12-07T10:35:00",
+ "updated_at": "2023-12-07T10:35:00",
+ "comments_count": 1
+ }
+]
+```
+
+
+
+## 8๋จ๊ณ: API ๋ฌธ์
+
+[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)์ ์ ์ํด ์์ฑ๋ API ๋ฌธ์๋ฅผ ํ์ธํ์ธ์. ๋ค์ ๋ด์ฉ์ ๋ณผ ์ ์์ต๋๋ค:
+
+- **Users**: ํ์ ๊ฐ์
, ๋ก๊ทธ์ธ, ํ๋กํ ๊ด๋ฆฌ
+- **Posts**: CRUD ์์
, ๊ฒ์, ์์ฑ์ ํํฐ๋ง
+- **Comments**: CRUD ์์
, ๊ฒ์๋ฌผ / ์์ฑ์ ํํฐ๋ง
+- **Items**: ์๋ ์์ ์๋ํฌ์ธํธ
+
+๋ฌธ์์์ ๋ณด์ฌ ์ฃผ๋ ๊ฒ:
+
+- ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ์๋ํฌ์ธํธ
+- ์์ฒญ / ์๋ต ์คํค๋ง
+- ๋ฐ์ดํฐ ๊ฒ์ฆ ๊ท์น
+- ์๋ฌ ์๋ต
+
+## 9๋จ๊ณ: ํ
์คํธ ์์ฑ
+
+๋ธ๋ก๊ทธ API๋ฅผ ์ํ ์ข
ํฉ ํ
์คํธ๋ฅผ ๋ง๋ค์ด ๋ด
์๋ค. `tests/test_blog_api.py`๋ฅผ ์์ฑํฉ๋๋ค:
+
+```python
+from fastapi.testclient import TestClient
+from src.main import app
+
+client = TestClient(app)
+
+class TestUserAPI:
+ def test_create_user(self):
+ user_data = {
+ "email": "test@example.com",
+ "username": "testuser",
+ "full_name": "Test User",
+ "bio": "Test bio",
+ "password": "testpassword123"
+ }
+ response = client.post("/api/v1/users/", json=user_data)
+ assert response.status_code == 201
+ data = response.json()
+ assert data["email"] == user_data["email"]
+ assert data["username"] == user_data["username"]
+ assert "id" in data
+ assert "hashed_password" not in data # Should not expose password
+
+ def test_duplicate_email(self):
+ # First user
+ user_data1 = {
+ "email": "duplicate@example.com",
+ "username": "user1",
+ "password": "password123"
+ }
+ response1 = client.post("/api/v1/users/", json=user_data1)
+ assert response1.status_code == 201
+
+ # Second user with same email
+ user_data2 = {
+ "email": "duplicate@example.com",
+ "username": "user2",
+ "password": "password123"
+ }
+ response2 = client.post("/api/v1/users/", json=user_data2)
+ assert response2.status_code == 400
+ assert "Email already registered" in response2.json()["detail"]
+
+ def test_login(self):
+ # Create user first
+ user_data = {
+ "email": "login@example.com",
+ "username": "loginuser",
+ "password": "loginpassword123"
+ }
+ client.post("/api/v1/users/", json=user_data)
+
+ # Test login
+ login_data = {
+ "email": "login@example.com",
+ "password": "loginpassword123"
+ }
+ response = client.post("/api/v1/users/login", json=login_data)
+ assert response.status_code == 200
+ data = response.json()
+ assert "user_id" in data
+ assert data["username"] == "loginuser"
+
+class TestPostAPI:
+ def test_create_post(self):
+ post_data = {
+ "title": "Test Post",
+ "content": "This is a test post content",
+ "published": True
+ }
+ response = client.post("/api/v1/posts/", json=post_data)
+ assert response.status_code == 201
+ data = response.json()
+ assert data["title"] == post_data["title"]
+ assert data["content"] == post_data["content"]
+ assert "id" in data
+ assert "author_id" in data
+
+ def test_read_posts(self):
+ response = client.get("/api/v1/posts/")
+ assert response.status_code == 200
+ data = response.json()
+ assert isinstance(data, list)
+
+ def test_search_posts(self):
+ # Create a post with specific content
+ post_data = {
+ "title": "FastAPI Tutorial",
+ "content": "Learn how to build APIs with FastAPI",
+ "published": True
+ }
+ client.post("/api/v1/posts/", json=post_data)
+
+ # Search for the post
+ response = client.get("/api/v1/posts/?search=FastAPI")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) > 0
+ assert any("FastAPI" in post["title"] or "FastAPI" in post["content"] for post in data)
+
+class TestCommentAPI:
+ def test_create_comment(self):
+ # Create a post first
+ post_data = {
+ "title": "Post for Comments",
+ "content": "This post will receive comments",
+ "published": True
+ }
+ post_response = client.post("/api/v1/posts/", json=post_data)
+ post_id = post_response.json()["id"]
+
+ # Create comment
+ comment_data = {
+ "content": "This is a test comment",
+ "post_id": post_id
+ }
+ response = client.post("/api/v1/comments/", json=comment_data)
+ assert response.status_code == 201
+ data = response.json()
+ assert data["content"] == comment_data["content"]
+ assert data["post_id"] == post_id
+
+ def test_get_comments_by_post(self):
+ # Create post and comment first
+ post_data = {
+ "title": "Post with Comments",
+ "content": "This post has comments",
+ "published": True
+ }
+ post_response = client.post("/api/v1/posts/", json=post_data)
+ post_id = post_response.json()["id"]
+
+ comment_data = {
+ "content": "Comment on post",
+ "post_id": post_id
+ }
+ client.post("/api/v1/comments/", json=comment_data)
+
+ # Get comments for the post
+ response = client.get(f"/api/v1/comments/post/{post_id}")
+ assert response.status_code == 200
+ data = response.json()
+ assert len(data) > 0
+ assert all(comment["post_id"] == post_id for comment in data)
+
+# Run the tests
+if __name__ == "__main__":
+ import pytest
+ pytest.main([__file__])
+```
+
+### ํ
์คํธ ์คํ
+
+
+
+```console
+$ python -m pytest tests/test_blog_api.py -v
+======================== test session starts ========================
+tests/test_blog_api.py::TestUserAPI::test_create_user PASSED
+tests/test_blog_api.py::TestUserAPI::test_duplicate_email PASSED
+tests/test_blog_api.py::TestUserAPI::test_login PASSED
+tests/test_blog_api.py::TestPostAPI::test_create_post PASSED
+tests/test_blog_api.py::TestPostAPI::test_read_posts PASSED
+tests/test_blog_api.py::TestPostAPI::test_search_posts PASSED
+tests/test_blog_api.py::TestCommentAPI::test_create_comment PASSED
+tests/test_blog_api.py::TestCommentAPI::test_get_comments_by_post PASSED
+======================== 8 passed in 1.23s ========================
+```
+
+
+
+## ๋ฌด์์ ๋ง๋ค์๋
+
+์ถํํฉ๋๋ค! ๋ค์ ๊ธฐ๋ฅ์ ๊ฐ์ถ ์์ ํ ๋ธ๋ก๊ทธ API๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๊ตฌ์ถํ์ต๋๋ค:
+
+### โ
๊ตฌํํ ๊ธฐ๋ฅ
+
+- **์ฌ์ฉ์ ๊ด๋ฆฌ**
+ - ๊ฒ์ฆ์ ๊ฐ์ถ ์ฌ์ฉ์ ํ์ ๊ฐ์
+ - ์ฌ์ฉ์ ์ธ์ฆ (๋ก๊ทธ์ธ)
+ - ํ๋กํ ๊ด๋ฆฌ
+ - ์ค๋ณต ๋ฐฉ์ง
+
+- **๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ**
+ - ๊ฒ์๋ฌผ ์์ฑ, ์กฐํ, ๊ฐฑ์ , ์ญ์
+ - ์์ฑ์ ๊ธฐ๋ฐ ํํฐ๋ง
+ - ๊ฒ์ ๊ธฐ๋ฅ
+ - ๊ฒ์ / ์์ ์ ์ฅ ์ํ
+
+- **๋๊ธ ์์คํ
**
+ - ๊ฒ์๋ฌผ์ ๋๊ธ ์ถ๊ฐ
+ - ๊ฒ์๋ฌผ๋ณ / ์์ฑ์๋ณ ๋๊ธ ์กฐํ
+ - ๋๊ธ ๊ด๋ฆฌ
+
+- **๋ฐ์ดํฐ ๊ฒ์ฆ**
+ - ์ด๋ฉ์ผ ๊ฒ์ฆ
+ - ํจ์ค์๋ ์๊ตฌ ์ฌํญ
+ - ์ฝํ
์ธ ๊ธธ์ด ์ ํ
+ - ํ์ ํ๋ ๊ฒ์ฆ
+
+- **์๋ฌ ์ฒ๋ฆฌ**
+ - ์ ์ ํ HTTP ์ํ ์ฝ๋
+ - ์ค๋ช
์ ์ธ ์๋ฌ ๋ฉ์์ง
+ - ์
๋ ฅ ๊ฒ์ฆ ์๋ฌ
+
+- **API ๋ฌธ์ํ**
+ - ์๋ OpenAPI ์์ฑ
+ - ์ธํฐ๋ํฐ๋ธ ํ
์คํธ ์ธํฐํ์ด์ค
+ - ์์ฒญ / ์๋ต ์คํค๋ง
+
+- **ํ
์คํธ**
+ - ์ข
ํฉ ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง
+ - ๋ชจ๋ ์๋ํฌ์ธํธ์ ๋จ์ ํ
์คํธ
+ - ์ฃ์ง ์ผ์ด์ค ํ
์คํธ
+
+## ๋ค์ ๋จ๊ณ
+
+### ๊ฐ๋ฅํ ๊ฐ์ ์ฌํญ
+
+1. **์ค์ ์ธ์ฆ**
+ - JWT ํ ํฐ ๊ตฌํ
+ - bcrypt ๋ก ํจ์ค์๋ ํด์ฑ ์ถ๊ฐ
+ - ์ญํ ๊ธฐ๋ฐ ๊ถํ
+
+2. **๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ**
+ - PostgreSQL ๋๋ MySQL ์ฌ์ฉ
+ - ์ ์ ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ชจ๋ธ ๊ตฌํ
+ - ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
์ถ๊ฐ
+
+3. **๊ณ ๊ธ ๊ธฐ๋ฅ**
+ - ์ด๋ฏธ์ง ํ์ผ ์
๋ก๋
+ - ์ด๋ฉ์ผ ์๋ฆผ
+ - ๊ฒ์๋ฌผ ์นดํ
๊ณ ๋ฆฌ / ํ๊ทธ
+ - ์ข์์ / ์ซ์ด์ ์์คํ
+
+4. **ํ๋ก๋์
์ค๋น**
+ - ๋ก๊น
์ถ๊ฐ
+ - ์บ์ฑ ๊ตฌํ
+ - ๋ ์ดํธ ์ ํ ์ถ๊ฐ
+ - ํ๊ฒฝ ์ค์
+
+### ํ์ต ์ด์ด๊ฐ๊ธฐ
+
+1. **[ํ
ํ๋ฆฟ ์ฌ์ฉํ๊ธฐ](../user-guide/using-templates.md)**: ๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ์ ์ํ `fastapi-psql-orm` ํ
ํ๋ฆฟ ์ดํด๋ณด๊ธฐ
+2. **[๋ผ์ฐํธ ์ถ๊ฐ](../user-guide/adding-routes.md)**: ๋ ๊ณ ๊ธ ๋ผ์ฐํ
ํจํด ํ์ต
+3. **[๊ธฐ์ฌ ์๋ด](../contributing/development-setup.md)**: FastAPI-fastkit์ ๊ธฐ์ฌํ๊ธฐ
+
+!!! tip "์ฌ๊ธฐ์ ๋ฐฐ์ด ๋ชจ๋ฒ ์ฌ๋ก"
+ - **๋ชจ๋ํ ์ํคํ
์ฒ**: ์คํค๋ง, CRUD, ๋ผ์ฐํธ๋ก ๊ด์ฌ์ฌ ๋ถ๋ฆฌ
+ - **๋ฐ์ดํฐ ๊ฒ์ฆ**: ๊ฒฌ๊ณ ํ ์
๋ ฅ ๊ฒ์ฆ์ ์ํ Pydantic ์ฌ์ฉ
+ - **์๋ฌ ์ฒ๋ฆฌ**: ์ ์ ํ HTTP ์ํ ์ฝ๋์ ์๋ฌ ๋ฉ์์ง
+ - **ํ
์คํธ**: ๋ชจ๋ ๊ธฐ๋ฅ์ ์์ฐ๋ฅด๋ ์ข
ํฉ ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง
+ - **๋ฌธ์ํ**: ์๋ API ๋ฌธ์ ์์ฑ ํ์ฉ
+
+์ด์ FastAPI-fastkit์ผ๋ก ์ค์๋น์ค ์์ค์ API๋ฅผ ๋ง๋ค ๊ธฐ๋ณธ๊ธฐ๋ฅผ ๊ฐ์ท์ต๋๋ค! ๐
diff --git a/docs/ko/tutorial/getting-started.md b/docs/ko/tutorial/getting-started.md
new file mode 100644
index 0000000..fabec3a
--- /dev/null
+++ b/docs/ko/tutorial/getting-started.md
@@ -0,0 +1,564 @@
+# ์์ํ๊ธฐ
+
+FastAPI-fastkit์ผ๋ก ์์ํ๋ ์ข
ํฉ ๋จ๊ณ๋ณ ํํ ๋ฆฌ์ผ์
๋๋ค. ์ด ๊ฐ์ด๋๋ ์ค์น๋ถํฐ ์ฒซ API ์คํ๊น์ง ์ฝ 15๋ถ ์์ ์ฐจ๊ทผ์ฐจ๊ทผ ์๋ดํฉ๋๋ค.
+
+## ์ฌ์ ์๊ตฌ ์ฌํญ
+
+์์ํ๊ธฐ ์ ์ ๋ค์์ ๊ฐ์ถฐ ๋์ธ์:
+
+- ์์คํ
์ ์ค์น๋ **Python 3.12 ์ด์**
+- **Python ๊ธฐ์ด ์ง์** (๋ณ์, ํจ์, ํด๋์ค)
+- **ํฐ๋ฏธ๋ / ์ปค๋งจ๋๋ผ์ธ** ์ฌ์ฉ ๊ฐ๋ฅ
+- **ํ
์คํธ ์๋ํฐ ๋๋ IDE** (VS Code, PyCharm ๋ฑ)
+
+## 1๋จ๊ณ: ์ค์น
+
+๋จผ์ FastAPI-fastkit์ ์ค์นํฉ๋๋ค. ํ๋ก์ ํธ๋ฅผ ๋ถ๋ฆฌํด์ ๊ด๋ฆฌํ ์ ์๋๋ก ๊ฐ์ ํ๊ฒฝ ์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.
+
+### ๋ฐฉ๋ฒ A: pip ์ฌ์ฉ (์ ํต์ )
+
+
+
+```console
+$ pip install fastapi-fastkit
+---> 100%
+Successfully installed fastapi-fastkit
+```
+
+
+
+### ๋ฐฉ๋ฒ B: UV ์ฌ์ฉ (๊ถ์ฅ โ ๋ ๋น ๋ฆ)
+
+UV๋ ๋น ๋ฅธ Python ํจํค์ง ๋งค๋์ ์
๋๋ค. ์์ง ์ค์นํ์ง ์์๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์งํํ์ธ์:
+
+
+
+```console
+# ๋จผ์ UV ์ค์น
+$ curl -LsSf https://astral.sh/uv/install.sh | sh
+
+# ์ด์ด์ FastAPI-fastkit ์ค์น
+$ uv pip install fastapi-fastkit
+---> 100%
+Successfully installed fastapi-fastkit
+```
+
+
+
+### ๋ฐฉ๋ฒ C: ๊ฐ์ ํ๊ฒฝ ์ฌ์ฉ
+
+
+
+```console
+$ python -m venv fastapi-env
+$ source fastapi-env/bin/activate # Windows: fastapi-env\Scripts\activate
+$ pip install fastapi-fastkit
+```
+
+
+
+### ์ค์น ํ์ธ
+
+FastAPI-fastkit์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ค์น๋๋์ง ํ์ธํฉ๋๋ค:
+
+
+
+```console
+$ fastkit --version
+FastAPI-fastkit version 1.0.0
+```
+
+
+
+## 2๋จ๊ณ: ์ฒซ ํ๋ก์ ํธ ์์ฑ
+
+์ด์ ๋ํํ `init` ๋ช
๋ น์ผ๋ก ์ฒซ FastAPI ํ๋ก์ ํธ๋ฅผ ๋ง๋ญ๋๋ค:
+
+
+
+```console
+$ fastkit init
+Enter the project name: my-first-api
+Enter the author name: Your Name
+Enter the author email: your.email@example.com
+Enter the project description: My first FastAPI project
+
+ Project Information
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Project Name โ my-first-api โ
+โ Author โ Your Name โ
+โ Author Email โ your.email@example.com โ
+โ Description โ My first FastAPI projectโ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Available Stacks and Dependencies:
+ MINIMAL Stack
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
+โ Dependency 1 โ fastapi โ
+โ Dependency 2 โ uvicorn โ
+โ Dependency 3 โ pydantic โ
+โ Dependency 4 โ pydantic-settings โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโ
+
+ STANDARD Stack
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
+โ Dependency 1 โ fastapi โ
+โ Dependency 2 โ uvicorn โ
+โ Dependency 3 โ sqlalchemy โ
+โ Dependency 4 โ alembic โ
+โ Dependency 5 โ pytest โ
+โ Dependency 6 โ pydantic โ
+โ Dependency 7 โ pydantic-settings โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโ
+
+Select stack (minimal, standard, full): minimal
+
+Available Package Managers:
+ Package Managers
+โโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ PIP โ Standard Python package manager โ
+โ UV โ Fast Python package manager โ
+โ PDM โ Modern Python dependency management โ
+โ POETRY โ Python dependency management and packaging โ
+โโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Select package manager (pip, uv, pdm, poetry) [uv]: uv
+Do you want to proceed with project creation? [y/N]: y
+
+Creating virtual environment...
+Installing dependencies...
+โจ FastAPI project 'my-first-api' has been created successfully!
+```
+
+
+
+!!! note "์คํ ์ ํ"
+ ์ด ํํ ๋ฆฌ์ผ์์๋ ๋จ์ํจ์ ์ํด **MINIMAL** ์ ๊ณจ๋์ต๋๋ค. ์ค์ ํ๋ก์ ํธ์์๋ **STANDARD** (๋ฐ์ดํฐ๋ฒ ์ด์ค ์ง์ ํฌํจ) ๋๋ **FULL** (๋ฐฑ๊ทธ๋ผ์ด๋ ์์
ํฌํจ) ์ ๊ณ ๋ คํ์ธ์.
+
+## 3๋จ๊ณ: ํ๋ก์ ํธ๋ก ์ด๋
+
+๋ฐฉ๊ธ ์์ฑํ ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ๋ก ์ด๋ํฉ๋๋ค:
+
+
+
+```console
+$ cd my-first-api
+$ ls -la
+total 32
+drwxr-xr-x 8 user user 256 Dec 7 10:30 .
+drwxr-xr-x 3 user user 96 Dec 7 10:30 ..
+drwxr-xr-x 5 user user 160 Dec 7 10:30 .venv
+-rw-r--r-- 1 user user 156 Dec 7 10:30 README.md
+-rw-r--r-- 1 user user 243 Dec 7 10:30 requirements.txt
+drwxr-xr-x 3 user user 96 Dec 7 10:30 scripts
+-rw-r--r-- 1 user user 1245 Dec 7 10:30 setup.py
+drwxr-xr-x 8 user user 256 Dec 7 10:30 src
+drwxr-xr-x 3 user user 96 Dec 7 10:30 tests
+```
+
+
+
+## 4๋จ๊ณ: ๊ฐ์ ํ๊ฒฝ ํ์ฑํ
+
+ํ๋ก์ ํธ์๋ ๋ฏธ๋ฆฌ ๊ตฌ์ฑ๋ ๊ฐ์ ํ๊ฒฝ์ด ํจ๊ป ์ต๋๋ค. ํ์ฑํํด ๋ด
์๋ค:
+
+
+
+```console
+$ source .venv/bin/activate # Windows: .venv\Scripts\activate
+(my-first-api) $
+```
+
+
+
+์ด์ ํฐ๋ฏธ๋ ํ๋กฌํํธ๊ฐ `(my-first-api)` ๋ก ํ์๋๋ฉฐ, ๊ฐ์ ํ๊ฒฝ์ด ํ์ฑํ๋์์ ์๋ ค ์ค๋๋ค.
+
+## 5๋จ๊ณ: ๊ฐ๋ฐ ์๋ฒ ์์
+
+์ด์ FastAPI ์๋ฒ๋ฅผ ์คํํด ๋ด
์๋ค:
+
+
+
+```console
+$ fastkit runserver
+INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
+INFO: Started reloader process [28720] using StatReload
+INFO: Started server process [28722]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+```
+
+
+
+๐ **์ถํํฉ๋๋ค!** FastAPI ์๋ฒ๊ฐ ๋์ ์ค์
๋๋ค.
+
+## 6๋จ๊ณ: API ํ
์คํธ
+
+์ฌ๋ฌ ๋ฐฉ๋ฒ์ผ๋ก API๋ฅผ ํ
์คํธํด ๋ด
์๋ค:
+
+### ๋ฐฉ๋ฒ 1: ๋ธ๋ผ์ฐ์
+
+์น ๋ธ๋ผ์ฐ์ ์์ ๋ค์ ์ฃผ์๋ฅผ ์ด์ด ๋ณด์ธ์:
+
+- **๋ฉ์ธ API ์๋ํฌ์ธํธ**: [http://127.0.0.1:8000](http://127.0.0.1:8000)
+
+๋ค์๊ณผ ๊ฐ์ด ๋ณด์ผ ๊ฒ์
๋๋ค:
+```json
+{"message": "Hello World"}
+```
+
+### ๋ฐฉ๋ฒ 2: ์ธํฐ๋ํฐ๋ธ API ๋ฌธ์
+
+์๋ ์์ฑ๋ API ๋ฌธ์๋ฅผ ์ด์ด ๋ด
๋๋ค:
+
+- **Swagger UI**: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)
+- **ReDoc**: [http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc)
+
+ํนํ Swagger UI ๊ฐ ์ ์ฉํฉ๋๋ค. ๋ค์์ ํ ์ ์์ต๋๋ค:
+
+- ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ์๋ํฌ์ธํธ ๋ณด๊ธฐ
+- ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์๋ํฌ์ธํธ ํ
์คํธ
+- ์์ฒญ / ์๋ต ์คํค๋ง ํ์ธ
+- OpenAPI ๋ช
์ธ ๋ค์ด๋ก๋
+
+### ๋ฐฉ๋ฒ 3: ์ปค๋งจ๋๋ผ์ธ
+
+์ ํฐ๋ฏธ๋์ ์ด๊ณ (์๋ฒ๋ฅผ ์คํํ๋ ํฐ๋ฏธ๋์ ๊ทธ๋๋ก ๋์ธ์) curl ๋ก ํ
์คํธํฉ๋๋ค:
+
+
+
+```console
+$ curl http://127.0.0.1:8000
+{"message":"Hello World"}
+
+$ curl http://127.0.0.1:8000/api/v1/items/
+[]
+
+$ curl -X POST "http://127.0.0.1:8000/api/v1/items/" \
+ -H "Content-Type: application/json" \
+ -d '{"title": "My First Item", "description": "This is a test item"}'
+{
+ "id": 1,
+ "title": "My First Item",
+ "description": "This is a test item"
+}
+```
+
+
+
+## 7๋จ๊ณ: ํ๋ก์ ํธ ๊ตฌ์กฐ ์ดํด
+
+FastAPI-fastkit์ด ๋ฌด์์ ์์ฑํ๋์ง ์ดํด๋ด
์๋ค:
+
+
+
+```console
+$ tree src
+src/
+โโโ __init__.py
+โโโ main.py # FastAPI ์ ํ๋ฆฌ์ผ์ด์
์ง์
์
+โโโ core/
+โ โโโ __init__.py
+โ โโโ config.py # ์ ํ๋ฆฌ์ผ์ด์
์ค์
+โโโ api/
+โ โโโ __init__.py
+โ โโโ api.py # ๋ฉ์ธ API ๋ผ์ฐํฐ
+โ โโโ routes/
+โ โโโ __init__.py
+โ โโโ items.py # Items API ์๋ํฌ์ธํธ
+โโโ crud/
+โ โโโ __init__.py
+โ โโโ items.py # items ์ ๋น์ฆ๋์ค ๋ก์ง
+โโโ schemas/
+โ โโโ __init__.py
+โ โโโ items.py # ๋ฐ์ดํฐ ๊ฒ์ฆ ์คํค๋ง
+โโโ mocks/
+ โโโ __init__.py
+ โโโ mock_items.json # ์ํ ๋ฐ์ดํฐ
+```
+
+
+
+### ์ฃผ์ ํ์ผ ์ค๋ช
+
+**`src/main.py`** โ ์ ํ๋ฆฌ์ผ์ด์
์ ํต์ฌ:
+```python
+from fastapi import FastAPI
+from src.api.api import api_router
+from src.core.config import settings
+
+app = FastAPI(
+ title=settings.PROJECT_NAME,
+ version=settings.VERSION,
+ openapi_url=f"{settings.API_V1_STR}/openapi.json"
+)
+
+app.include_router(api_router, prefix=settings.API_V1_STR)
+
+@app.get("/")
+def read_root():
+ return {"message": "Hello World"}
+```
+
+**`src/core/config.py`** โ ์ ํ๋ฆฌ์ผ์ด์
์ค์ :
+```python
+from pydantic_settings import BaseSettings
+
+class Settings(BaseSettings):
+ PROJECT_NAME: str = "my-first-api"
+ VERSION: str = "1.0.0"
+ API_V1_STR: str = "/api/v1"
+
+ class Config:
+ env_file = ".env"
+
+settings = Settings()
+```
+
+**`src/api/routes/items.py`** โ API ์๋ํฌ์ธํธ:
+```python
+from typing import List
+from fastapi import APIRouter, HTTPException
+from src.schemas.items import Item, ItemCreate, ItemUpdate
+from src.crud.items import items_crud
+
+router = APIRouter()
+
+@router.get("/", response_model=List[Item])
+def read_items():
+ """Get all items"""
+ return items_crud.get_all()
+
+@router.post("/", response_model=Item)
+def create_item(item: ItemCreate):
+ """Create a new item"""
+ return items_crud.create(item)
+```
+
+## 8๋จ๊ณ: ์ฒซ ์ปค์คํ
๋ผ์ฐํธ ์ถ๊ฐ
+
+๋ฐฐ์ด ๋ด์ฉ์ ์ฐ์ตํ ๊ฒธ ์ API ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด ๋ด
์๋ค:
+
+
+
+```console
+$ fastkit addroute my-first-api users
+ Adding New Route
+โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Project โ my-first-api โ
+โ Route Name โ users โ
+โ Target Directory โ ~/my-first-api โ
+โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Do you want to add route 'users' to project 'my-first-api'? [Y/n]: y
+
+โจ Successfully added new route 'users' to project 'my-first-api'
+```
+
+
+
+์๋ฒ๋ ์๋์ผ๋ก ์ฌ์์๋๊ณ , ์ด์ ์ ์๋ํฌ์ธํธ๋ค์ด ์๊น๋๋ค:
+
+- `GET /api/v1/users/` โ ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ
+- `POST /api/v1/users/` โ ์ ์ฌ์ฉ์ ์์ฑ
+- `GET /api/v1/users/{user_id}` โ ํน์ ์ฌ์ฉ์ ์กฐํ
+- ๊ทธ ์ธ ๋ค์...
+
+### ์ ๋ผ์ฐํธ ํ
์คํธ
+
+
+
+```console
+$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \
+ -H "Content-Type: application/json" \
+ -d '{"title": "John Doe", "description": "Software Developer"}'
+{
+ "id": 1,
+ "title": "John Doe",
+ "description": "Software Developer"
+}
+
+$ curl http://127.0.0.1:8000/api/v1/users/
+[
+ {
+ "id": 1,
+ "title": "John Doe",
+ "description": "Software Developer"
+ }
+]
+```
+
+
+
+## 9๋จ๊ณ: ์ฝ๋ ์ดํด๋ณด๊ณ ์์ ํ๊ธฐ
+
+์ฝ๋๊ฐ ์ด๋ป๊ฒ ๋์ํ๋์ง ์ดํดํ๊ธฐ ์ํด ์์ ์์ ์ ํด ๋ด
๋๋ค.
+
+### ํ์ ๋ฉ์์ง ์์
+
+ํ
์คํธ ์๋ํฐ์์ `src/main.py` ๋ฅผ ์ด๊ณ ๋ฃจํธ ์๋ํฌ์ธํธ๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค:
+
+```python
+@app.get("/")
+def read_root():
+ return {"message": "Welcome to my first FastAPI application!"}
+```
+
+ํ์ผ์ ์ ์ฅํฉ๋๋ค. ์๋ ๋ฆฌ๋ก๋ ๋๋ถ์ ์๋ฒ๊ฐ ์๋์ผ๋ก ์ฌ์์๋ฉ๋๋ค.
+
+### ๋ณ๊ฒฝ ์ฌํญ ํ
์คํธ
+
+
+
+```console
+$ curl http://127.0.0.1:8000
+{"message":"Welcome to my first FastAPI application!"}
+```
+
+
+
+### ์ ์๋ํฌ์ธํธ ์ถ๊ฐ
+
+`src/main.py` ์ ๋จ์ํ ์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํด ๋ด
์๋ค:
+
+```python
+@app.get("/hello/{name}")
+def say_hello(name: str):
+ return {"message": f"Hello, {name}!"}
+```
+
+### ์ ์๋ํฌ์ธํธ ํ
์คํธ
+
+
+
+```console
+$ curl http://127.0.0.1:8000/hello/World
+{"message":"Hello, World!"}
+
+$ curl http://127.0.0.1:8000/hello/FastAPI
+{"message":"Hello, FastAPI!"}
+```
+
+
+
+## 10๋จ๊ณ: ํ
์คํธ ์คํ
+
+ํ๋ก์ ํธ์๋ ๋ฏธ๋ฆฌ ๊ตฌ์ฑ๋ ํ
์คํธ๊ฐ ํจ๊ป ์ต๋๋ค. ์คํํด ๋ด
์๋ค:
+
+
+
+```console
+$ python -m pytest
+======================== test session starts ========================
+collected 5 items
+
+tests/test_items.py::test_create_item PASSED
+tests/test_items.py::test_read_items PASSED
+tests/test_items.py::test_read_item PASSED
+tests/test_items.py::test_update_item PASSED
+tests/test_items.py::test_delete_item PASSED
+
+======================== 5 passed in 0.45s ========================
+```
+
+
+
+## ํต์ฌ ๊ฐ๋
์ดํด
+
+### 1. FastAPI ์ ํ๋ฆฌ์ผ์ด์
๊ตฌ์กฐ
+
+FastAPI-fastkit์ **๋ชจ๋ํ ์ํคํ
์ฒ**๋ฅผ ๋ฐ๋ฆ
๋๋ค:
+
+- **`main.py`**: ์ ํ๋ฆฌ์ผ์ด์
์ง์
์ ๊ณผ ์ ์ญ ์๋ํฌ์ธํธ
+- **`api/`**: API ๋ผ์ฐํธ ๊ตฌ์ฑ
+- **`core/`**: ์ ํ๋ฆฌ์ผ์ด์
๊ตฌ์ฑ ๋ฐ ์ค์
+- **`crud/`**: ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ฐ์ดํฐ ์์
+- **`schemas/`**: ๋ฐ์ดํฐ ๊ฒ์ฆ ๋ฐ ์ง๋ ฌํ
+- **`tests/`**: ์๋ํ๋ ํ
์คํธ
+
+### 2. ์์กด์ฑ ๊ด๋ฆฌ
+
+ํ๋ก์ ํธ๋ ํ๋์ ์ธ Python ์์กด์ฑ ๊ด๋ฆฌ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค:
+
+- **๊ฐ์ ํ๊ฒฝ**: ๊ฒฉ๋ฆฌ๋ Python ํ๊ฒฝ
+- **requirements.txt**: ๋ชจ๋ ์์กด์ฑ์ ๋์ด
+- **์๋ ์ค์น**: ํ๋ก์ ํธ ์์ฑ ์ ์์กด์ฑ์ ์๋ ์ค์น
+
+### 3. ๊ฐ๋ฐ ์๋ฒ
+
+FastAPI-fastkit์ ASGI ์๋ฒ๋ก **Uvicorn**์ ์ฌ์ฉํฉ๋๋ค:
+
+- **์๋ ๋ฆฌ๋ก๋**: ์ฝ๋ ๋ณ๊ฒฝ ์ ์๋ ์ฌ์์
+- **๋น ๋ฅธ ์์**: ๋น ๋ฅธ ๊ฐ๋ฐ ๋ฐ๋ณต
+- **ํ๋ก๋์
๋๋น**: ํ๋ก๋์
์์๋ ๊ฐ์ ์๋ฒ ์ฌ์ฉ
+
+### 4. API ๋ฌธ์ํ
+
+FastAPI๋ ์๋์ผ๋ก ๋ค์ ํญ๋ชฉ์ ์์ฑํฉ๋๋ค:
+
+- **OpenAPI ๋ช
์ธ**: ์
๊ณ ํ์ค API ๋ฌธ์
+- **Swagger UI**: ์ธํฐ๋ํฐ๋ธ ํ
์คํธ ์ธํฐํ์ด์ค
+- **ReDoc**: ๋์ ๋ฌธ์ ๋ทฐ
+
+## ๋ค์ ๋จ๊ณ
+
+์ถํํฉ๋๋ค! ๋ค์์ ์ฑ๊ณต์ ์ผ๋ก ๋ง์ณค์ต๋๋ค:
+
+โ
FastAPI-fastkit ์ค์น
+โ
์ฒซ ํ๋ก์ ํธ ์์ฑ
+โ
๊ฐ๋ฐ ์๋ฒ ์์
+โ
API ์๋ํฌ์ธํธ ํ
์คํธ
+โ
์ ๋ผ์ฐํธ ์ถ๊ฐ
+โ
๊ธฐ์กด ์ฝ๋ ์์
+โ
ํ
์คํธ ์คํ
+
+### ํ์ต ์ด์ด๊ฐ๊ธฐ
+
+1. **[์ฒซ ํ๋ก์ ํธ ๋ง๋ค๊ธฐ](first-project.md)**: ๊ณ ๊ธ ๊ธฐ๋ฅ์ ๊ฐ์ถ ์์ ํ ๋ธ๋ก๊ทธ API ๊ตฌ์ถ
+2. **[๋ผ์ฐํธ ์ถ๊ฐ](../user-guide/adding-routes.md)**: ๋ณต์กํ API ์๋ํฌ์ธํธ ๋ง๋๋ ๋ฒ ํ์ต
+3. **[ํ
ํ๋ฆฟ ์ฌ์ฉํ๊ธฐ](../user-guide/using-templates.md)**: ์ฌ์ ๊ตฌ์ถ ํ๋ก์ ํธ ํ
ํ๋ฆฟ ์ดํด๋ณด๊ธฐ
+
+### ๋ ์คํํด ๋ณด๊ธฐ
+
+๋ค์ ๋์ ๊ณผ์ ๋ฅผ ์๋ํด ๋ณด์ธ์:
+
+1. **๊ฒ์ฆ ์ถ๊ฐ**: ์คํค๋ง๋ฅผ ์์ ํด ๋ฐ์ดํฐ ๊ฒ์ฆ ๊ท์น์ ์ถ๊ฐํด ๋ณด์ธ์
+2. **์ปค์คํ
์๋ต**: ๋ผ์ฐํธ์ ์๋ต ํ์์ ๋ฐ๊ฟ ๋ณด์ธ์
+3. **ํ๊ฒฝ ๋ณ์**: ์ค์ ์ `.env` ํ์ผ์ ์ฌ์ฉํด ๋ณด์ธ์
+4. **๋ฏธ๋ค์จ์ด ์ถ๊ฐ**: CORS ๋๋ ์ธ์ฆ์ ๊ตฌํํด ๋ณด์ธ์
+5. **๋ฐ์ดํฐ๋ฒ ์ด์ค ํตํฉ**: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ง์์ ์ํด STANDARD ์คํ์ผ๋ก ์
๊ทธ๋ ์ด๋ํ์ธ์
+
+### ์์ฃผ ๋ง์ฃผ์น๋ ๋ฌธ์ ์ ํด๊ฒฐ
+
+**์๋ฒ๊ฐ ์์๋์ง ์์ ๋:**
+
+- ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ์ ์๋์ง ํ์ธ
+- ๊ฐ์ ํ๊ฒฝ์ด ํ์ฑํ๋ผ ์๋์ง ํ์ธ
+- ์ฝ๋์ ๋ฌธ๋ฒ ์ค๋ฅ๊ฐ ์๋์ง ๊ฒ์ฆ
+
+**Import ์ค๋ฅ:**
+
+- ๋ชจ๋ `__init__.py` ํ์ผ์ด ์กด์ฌํ๋์ง ํ์ธ
+- import ๊ฒฝ๋ก๊ฐ ์ฌ๋ฐ๋ฅธ์ง ํ์ธ
+- ๊ฐ์ ํ๊ฒฝ์ ์ฌ์ฉํ๊ณ ์๋์ง ํ์ธ
+
+**ํฌํธ๊ฐ ์ด๋ฏธ ์ฌ์ฉ ์ค์ผ ๋:**
+```console
+$ fastkit runserver --port 8080
+```
+
+## ์ฌ๊ธฐ์ ๋ฐฐ์ด ๋ชจ๋ฒ ์ฌ๋ก
+
+1. **๊ฐ์ ํ๊ฒฝ**: ํญ์ ๊ฒฉ๋ฆฌ๋ ํ๊ฒฝ ์ฌ์ฉ
+2. **ํ๋ก์ ํธ ๊ตฌ์กฐ**: ์ ์ ๋ฆฌ๋ ๋ชจ๋ํ ์ํคํ
์ฒ ๋ฐ๋ฅด๊ธฐ
+3. **์๋ ๋ฆฌ๋ก๋**: ๋น ๋ฅธ ๋ฐ๋ณต์ ์ํด ๊ฐ๋ฐ ์๋ฒ ์ฌ์ฉ
+4. **API ๋ฌธ์ํ**: ์๋ ๋ฌธ์ ์์ฑ ํ์ฉ
+5. **ํ
์คํธ**: ๊ฐ๋ฐ ์ค์๋ ์ ๊ธฐ์ ์ผ๋ก ํ
์คํธ ์คํ
+
+!!! tip "๊ฐ๋ฐ ํ"
+ - ์ฝ๋ฉ ์ค์๋ ๊ฐ๋ฐ ์๋ฒ๋ฅผ ์ผ ๋์ธ์
+ - API ํ
์คํธ์๋ ์ธํฐ๋ํฐ๋ธ ๋ฌธ์ (`/docs`) ๋ฅผ ํ์ฉํ์ธ์
+ - ๋์์ด ๋๋ ์๋ฌ ๋ฉ์์ง๊ฐ ์๋์ง ํฐ๋ฏธ๋์ ํ์ธํ์ธ์
+ - ์ฝ๋๋ฅผ ์ ๊ธฐ์ ์ผ๋ก ๋ฒ์ ๊ด๋ฆฌ์ ์ปค๋ฐํ์ธ์
+
+์ด์ FastAPI-fastkit์ผ๋ก ๋ฉ์ง API๋ฅผ ๋ง๋ค ์ค๋น๊ฐ ๋์ต๋๋ค! ๐
diff --git a/docs/ko/tutorial/mcp-integration.md b/docs/ko/tutorial/mcp-integration.md
new file mode 100644
index 0000000..6d6d743
--- /dev/null
+++ b/docs/ko/tutorial/mcp-integration.md
@@ -0,0 +1,1730 @@
+# MCP (Model Context Protocol) ํตํฉ
+
+Model Context Protocol(MCP)์ FastAPI์ ํตํฉํด AI ๋ชจ๋ธ์ด API ์๋ํฌ์ธํธ๋ฅผ ๋๊ตฌ์ฒ๋ผ ํ์ฉํ ์ ์๋ ์์คํ
์ ๊ตฌ์ถํฉ๋๋ค. `fastapi-mcp` ํ
ํ๋ฆฟ์ผ๋ก ์ธ์ฆ, ๊ถํ ๊ด๋ฆฌ, MCP ์๋ฒ ๊ตฌํ์ด ํฌํจ๋ ์์ ํ AI ํตํฉ API๋ฅผ ๋ง๋ค์ด ๋ด
๋๋ค.
+
+## ์ด ํํ ๋ฆฌ์ผ์์ ๋ฐฐ์ฐ๋ ๋ด์ฉ
+
+- Model Context Protocol(MCP)์ ๊ฐ๋
๊ณผ ๊ตฌํ
+- JWT ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
๊ตฌ์ถ
+- ์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด (RBAC) ๊ตฌํ
+- MCP ๋๊ตฌ์ ๋
ธ์ถ๊ณผ ๊ด๋ฆฌ
+- AI ๋ชจ๋ธ๊ณผ์ ์์ ํ API ํต์
+- ์ฌ์ฉ์ ์ธ์
๊ณผ ์ปจํ
์คํธ ๊ด๋ฆฌ
+
+## ์ฌ์ ์๊ตฌ ์ฌํญ
+
+- [์ปค์คํ
์๋ต ์ฒ๋ฆฌ ํํ ๋ฆฌ์ผ](custom-response-handling.md) ์๋ฃ
+- JWT์ OAuth2์ ๊ธฐ๋ณธ ๊ฐ๋
์ดํด
+- AI / LLM ๋ชจ๋ธ๊ณผ์ API ํต์ ๊ฐ๋
+- MCP ํ๋กํ ์ฝ์ ๋ํ ๊ธฐ์ด ์ง์
+
+## Model Context Protocol (MCP) ์ด๋?
+
+MCP๋ AI ๋ชจ๋ธ์ด ์ธ๋ถ ์์คํ
๊ณผ ์ํธ ์์ฉํ ์ ์๋๋ก ๋ง๋ ํ์ค ํ๋กํ ์ฝ์
๋๋ค.
+
+### ๊ธฐ์กด ๋ฐฉ์ vs MCP ๋ฐฉ์
+
+**์ ํต์ ๋ฐฉ์ (์ง์ API ํธ์ถ):**
+```
+AI ๋ชจ๋ธ โ HTTP ์์ฒญ โ API ์๋ฒ โ ์๋ต
+```
+
+**MCP ๋ฐฉ์:**
+```
+AI ๋ชจ๋ธ โ MCP ํด๋ผ์ด์ธํธ โ MCP ์๋ฒ (FastAPI) โ ์์ ํ ๋๊ตฌ ์คํ โ ์๋ต
+```
+
+### MCP์ ์ฅ์
+
+- **๋ณด์**: ํตํฉ๋ ์ธ์ฆ๊ณผ ๊ถํ ๊ด๋ฆฌ
+- **ํ์คํ**: ์ผ๊ด๋ ์ธํฐํ์ด์ค ์ ๊ณต
+- **์ปจํ
์คํธ ๊ด๋ฆฌ**: ์ธ์
๊ธฐ๋ฐ ์ํ ์ ์ง
+- **๋๊ตฌ ์ถ์ํ**: ๋ณต์กํ API๋ฅผ ๋จ์ํ ๋๊ตฌ๋ก ๋
ธ์ถ
+
+## 1๋จ๊ณ: MCP ํตํฉ ํ๋ก์ ํธ ์์ฑ
+
+`fastapi-mcp` ํ
ํ๋ฆฟ์ผ๋ก ํ๋ก์ ํธ๋ฅผ ๋ง๋ญ๋๋ค:
+
+
+
+```console
+$ fastkit startdemo fastapi-mcp
+Enter the project name: ai-integrated-api
+Enter the author name: Developer Kim
+Enter the author email: developer@example.com
+Enter the project description: MCP-based API server integrated with AI models
+Deploying FastAPI project using 'fastapi-mcp' template
+
+ Project Information
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Project Name โ ai-integrated-api โ
+โ Author โ Developer Kim โ
+โ Author Email โ developer@example.com โ
+โ Description โ MCP-based API server integrated with AI models โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+ Template Dependencies
+โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโ
+โ Dependency 1 โ fastapi โ
+โ Dependency 2 โ uvicorn โ
+โ Dependency 3 โ pydantic โ
+โ Dependency 4 โ python-jose โ
+โ Dependency 5 โ passlib โ
+โ Dependency 6 โ python-multipartโ
+โ Dependency 7 โ mcp โ
+โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโ
+
+Select package manager (pip, uv, pdm, poetry) [uv]: uv
+Do you want to proceed with project creation? [y/N]: y
+
+โจ FastAPI project 'ai-integrated-api' from 'fastapi-mcp' has been created successfully!
+```
+
+
+
+## 2๋จ๊ณ: ํ๋ก์ ํธ ๊ตฌ์กฐ ๋ถ์
+
+์์ฑ๋ ํ๋ก์ ํธ์ ๊ตฌ์กฐ๋ฅผ ์ดํด๋ด
์๋ค:
+
+```
+ai-integrated-api/
+โโโ src/
+โ โโโ main.py # FastAPI ์ ํ๋ฆฌ์ผ์ด์
+โ โโโ auth/
+โ โ โโโ __init__.py
+โ โ โโโ models.py # ์ธ์ฆ ๊ด๋ จ ๋ฐ์ดํฐ ๋ชจ๋ธ
+โ โ โโโ jwt_handler.py # JWT ํ ํฐ ์ฒ๋ฆฌ
+โ โ โโโ dependencies.py # ์ธ์ฆ ์์กด์ฑ
+โ โ โโโ routes.py # ์ธ์ฆ ๋ผ์ฐํฐ
+โ โโโ mcp/
+โ โ โโโ __init__.py
+โ โ โโโ server.py # MCP ์๋ฒ ๊ตฌํ
+โ โ โโโ tools.py # MCP ๋๊ตฌ ์ ์
+โ โ โโโ client.py # MCP ํด๋ผ์ด์ธํธ (ํ
์คํธ์ฉ)
+โ โโโ api/
+โ โ โโโ __init__.py
+โ โ โโโ api.py # API ๋ผ์ฐํฐ ๋ชจ์
+โ โ โโโ routes/
+โ โ โโโ items.py # item ๊ด๋ฆฌ API
+โ โ โโโ users.py # ์ฌ์ฉ์ ๊ด๋ฆฌ API
+โ โ โโโ admin.py # ๊ด๋ฆฌ์ API
+โ โโโ schemas/
+โ โ โโโ __init__.py
+โ โ โโโ auth.py # ์ธ์ฆ ์คํค๋ง
+โ โ โโโ users.py # ์ฌ์ฉ์ ์คํค๋ง
+โ โ โโโ items.py # item ์คํค๋ง
+โ โโโ core/
+โ โโโ __init__.py
+โ โโโ config.py # ์ค์
+โ โโโ database.py # ๋ฐ์ดํฐ๋ฒ ์ด์ค (์ธ๋ฉ๋ชจ๋ฆฌ)
+โ โโโ security.py # ๋ณด์ ์ค์
+โโโ tests/
+ โโโ test_auth.py # ์ธ์ฆ ํ
์คํธ
+ โโโ test_mcp.py # MCP ํ
์คํธ
+ โโโ test_integration.py # ํตํฉ ํ
์คํธ
+```
+
+## 3๋จ๊ณ: ์ธ์ฆ ์์คํ
๊ตฌํ
+
+### JWT ํ ํฐ ์ฒ๋ฆฌ (`src/auth/jwt_handler.py`)
+
+```python
+from datetime import datetime, timedelta
+from typing import Optional, Dict, Any
+from jose import JWTError, jwt
+from passlib.context import CryptContext
+
+from src.core.config import settings
+
+# ๋น๋ฐ๋ฒํธ ํด์ฑ
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """Password verification"""
+ return pwd_context.verify(plain_password, hashed_password)
+
+def get_password_hash(password: str) -> str:
+ """Password hashing"""
+ return pwd_context.hash(password)
+
+def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
+ """Access token generation"""
+ to_encode = data.copy()
+
+ if expires_delta:
+ expire = datetime.utcnow() + expires_delta
+ else:
+ expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+
+ to_encode.update({"exp": expire, "iat": datetime.utcnow()})
+
+ encoded_jwt = jwt.encode(
+ to_encode,
+ settings.SECRET_KEY,
+ algorithm=settings.ALGORITHM
+ )
+
+ return encoded_jwt
+
+def create_refresh_token(user_id: str) -> str:
+ """Refresh token generation"""
+ data = {"sub": user_id, "type": "refresh"}
+ expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
+
+ to_encode = data.copy()
+ to_encode.update({"exp": expire, "iat": datetime.utcnow()})
+
+ return jwt.encode(
+ to_encode,
+ settings.SECRET_KEY,
+ algorithm=settings.ALGORITHM
+ )
+
+def decode_token(token: str) -> Optional[Dict[str, Any]]:
+ """Token decoding"""
+ try:
+ payload = jwt.decode(
+ token,
+ settings.SECRET_KEY,
+ algorithms=[settings.ALGORITHM]
+ )
+ return payload
+ except JWTError:
+ return None
+
+def verify_token(token: str, token_type: str = "access") -> Optional[str]:
+ """Token verification and user ID return"""
+ payload = decode_token(token)
+
+ if not payload:
+ return None
+
+ # ํ ํฐ ํ์
๊ฒ์ฆ
+ if token_type == "refresh" and payload.get("type") != "refresh":
+ return None
+
+ user_id = payload.get("sub")
+ if not user_id:
+ return None
+
+ return user_id
+
+class TokenManager:
+ """Token management class"""
+
+ def __init__(self):
+ self.blacklisted_tokens = set()
+
+ def blacklist_token(self, token: str):
+ """Add token to blacklist"""
+ self.blacklisted_tokens.add(token)
+
+ def is_blacklisted(self, token: str) -> bool:
+ """Check if token is blacklisted"""
+ return token in self.blacklisted_tokens
+
+ def create_token_pair(self, user_id: str, user_role: str) -> Dict[str, str]:
+ """Create access/refresh token pair"""
+ access_token_data = {
+ "sub": user_id,
+ "role": user_role,
+ "type": "access"
+ }
+
+ access_token = create_access_token(access_token_data)
+ refresh_token = create_refresh_token(user_id)
+
+ return {
+ "access_token": access_token,
+ "refresh_token": refresh_token,
+ "token_type": "bearer"
+ }
+
+# ์ ์ญ ํ ํฐ ๋งค๋์
+token_manager = TokenManager()
+```
+
+### ์ฌ์ฉ์ ๋ชจ๋ธ๊ณผ ๋ฐ์ดํฐ๋ฒ ์ด์ค (`src/auth/models.py`)
+
+```python
+from typing import List, Optional, Dict, Any
+from pydantic import BaseModel, EmailStr
+from enum import Enum
+from datetime import datetime
+
+class UserRole(str, Enum):
+ """User roles"""
+ ADMIN = "admin"
+ USER = "user"
+ AI_AGENT = "ai_agent"
+ READONLY = "readonly"
+
+class Permission(str, Enum):
+ """Permissions"""
+ READ_ITEMS = "read:items"
+ WRITE_ITEMS = "write:items"
+ DELETE_ITEMS = "delete:items"
+ MANAGE_USERS = "manage:users"
+ USE_MCP_TOOLS = "use:mcp_tools"
+ ADMIN_MCP = "admin:mcp"
+
+class User(BaseModel):
+ """User model"""
+ id: str
+ email: EmailStr
+ username: str
+ full_name: Optional[str] = None
+ role: UserRole
+ permissions: List[Permission]
+ is_active: bool = True
+ created_at: datetime
+ last_login: Optional[datetime] = None
+ api_key: Optional[str] = None # MCP ํด๋ผ์ด์ธํธ์ฉ
+
+class UserInDB(User):
+ """User model for database storage"""
+ hashed_password: str
+
+class UserCreate(BaseModel):
+ """User creation schema"""
+ email: EmailStr
+ username: str
+ password: str
+ full_name: Optional[str] = None
+ role: UserRole = UserRole.USER
+
+class UserUpdate(BaseModel):
+ """User update schema"""
+ email: Optional[EmailStr] = None
+ username: Optional[str] = None
+ full_name: Optional[str] = None
+ role: Optional[UserRole] = None
+ is_active: Optional[bool] = None
+
+class LoginRequest(BaseModel):
+ """Login request schema"""
+ username: str
+ password: str
+
+class TokenResponse(BaseModel):
+ """Token response schema"""
+ access_token: str
+ refresh_token: str
+ token_type: str = "bearer"
+ expires_in: int
+ user: User
+
+# ์ญํ ๋ณ ๊ธฐ๋ณธ ๊ถํ ๋งคํ
+ROLE_PERMISSIONS = {
+ UserRole.ADMIN: [
+ Permission.READ_ITEMS,
+ Permission.WRITE_ITEMS,
+ Permission.DELETE_ITEMS,
+ Permission.MANAGE_USERS,
+ Permission.USE_MCP_TOOLS,
+ Permission.ADMIN_MCP
+ ],
+ UserRole.USER: [
+ Permission.READ_ITEMS,
+ Permission.WRITE_ITEMS,
+ Permission.USE_MCP_TOOLS
+ ],
+ UserRole.AI_AGENT: [
+ Permission.READ_ITEMS,
+ Permission.WRITE_ITEMS,
+ Permission.USE_MCP_TOOLS
+ ],
+ UserRole.READONLY: [
+ Permission.READ_ITEMS
+ ]
+}
+
+class UserDatabase:
+ """Memory-based user database"""
+
+ def __init__(self):
+ self.users: Dict[str, UserInDB] = {}
+ self._init_default_users()
+
+ def _init_default_users(self):
+ """Create default users"""
+ from src.auth.jwt_handler import get_password_hash
+ import uuid
+
+ # ๊ด๋ฆฌ์ ๊ณ์
+ admin_id = str(uuid.uuid4())
+ self.users[admin_id] = UserInDB(
+ id=admin_id,
+ email="admin@example.com",
+ username="admin",
+ full_name="System Administrator",
+ role=UserRole.ADMIN,
+ permissions=ROLE_PERMISSIONS[UserRole.ADMIN],
+ hashed_password=get_password_hash("admin123"),
+ created_at=datetime.utcnow(),
+ api_key=str(uuid.uuid4())
+ )
+
+ # AI ์์ด์ ํธ ๊ณ์
+ ai_id = str(uuid.uuid4())
+ self.users[ai_id] = UserInDB(
+ id=ai_id,
+ email="ai@example.com",
+ username="ai_agent",
+ full_name="AI Assistant",
+ role=UserRole.AI_AGENT,
+ permissions=ROLE_PERMISSIONS[UserRole.AI_AGENT],
+ hashed_password=get_password_hash("ai123"),
+ created_at=datetime.utcnow(),
+ api_key=str(uuid.uuid4())
+ )
+
+ def get_user_by_username(self, username: str) -> Optional[UserInDB]:
+ """Get user by username"""
+ return next(
+ (user for user in self.users.values() if user.username == username),
+ None
+ )
+
+ def get_user_by_id(self, user_id: str) -> Optional[UserInDB]:
+ """Get user by ID"""
+ return self.users.get(user_id)
+
+ def get_user_by_api_key(self, api_key: str) -> Optional[UserInDB]:
+ """Get user by API key"""
+ return next(
+ (user for user in self.users.values() if user.api_key == api_key),
+ None
+ )
+
+ def create_user(self, user_create: UserCreate) -> UserInDB:
+ """Create user"""
+ import uuid
+ from src.auth.jwt_handler import get_password_hash
+
+ user_id = str(uuid.uuid4())
+ user = UserInDB(
+ id=user_id,
+ email=user_create.email,
+ username=user_create.username,
+ full_name=user_create.full_name,
+ role=user_create.role,
+ permissions=ROLE_PERMISSIONS[user_create.role],
+ hashed_password=get_password_hash(user_create.password),
+ created_at=datetime.utcnow(),
+ api_key=str(uuid.uuid4())
+ )
+
+ self.users[user_id] = user
+ return user
+
+ def update_user(self, user_id: str, user_update: UserUpdate) -> Optional[UserInDB]:
+ """Update user"""
+ if user_id not in self.users:
+ return None
+
+ user = self.users[user_id]
+ update_data = user_update.dict(exclude_unset=True)
+
+ for field, value in update_data.items():
+ setattr(user, field, value)
+
+ # ์ญํ ์ด ๋ณ๊ฒฝ๋๋ฉด ๊ถํ ๊ฐฑ์
+ if "role" in update_data:
+ user.permissions = ROLE_PERMISSIONS[user.role]
+
+ return user
+
+ def update_last_login(self, user_id: str):
+ """Update last login time"""
+ if user_id in self.users:
+ self.users[user_id].last_login = datetime.utcnow()
+
+# ์ ์ญ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ์คํด์ค
+user_db = UserDatabase()
+```
+
+## 4๋จ๊ณ: ์ธ์ฆ ์์กด์ฑ ๊ตฌํ
+
+### ์ธ์ฆ ์์กด์ฑ (`src/auth/dependencies.py`)
+
+```python
+from typing import Optional, List
+from fastapi import Depends, HTTPException, status, Security
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
+from jose import JWTError
+
+from src.auth.jwt_handler import decode_token, token_manager
+from src.auth.models import User, UserInDB, Permission, user_db
+
+# ๋ณด์ ์คํค๋ง
+security = HTTPBearer()
+api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
+
+async def get_current_user(
+ credentials: HTTPAuthorizationCredentials = Security(security)
+) -> User:
+ """Get current authenticated user"""
+ credentials_exception = HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ try:
+ token = credentials.credentials
+
+ # ๋ธ๋๋ฆฌ์คํธ ํ์ธ
+ if token_manager.is_blacklisted(token):
+ raise credentials_exception
+
+ payload = decode_token(token)
+ if payload is None:
+ raise credentials_exception
+
+ user_id: str = payload.get("sub")
+ if user_id is None:
+ raise credentials_exception
+
+ except JWTError:
+ raise credentials_exception
+
+ user = user_db.get_user_by_id(user_id)
+ if user is None:
+ raise credentials_exception
+
+ if not user.is_active:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Inactive user"
+ )
+
+ return User(**user.dict())
+
+async def get_current_user_by_api_key(
+ api_key: Optional[str] = Security(api_key_header)
+) -> Optional[User]:
+ """Authenticate user by API key"""
+ if not api_key:
+ return None
+
+ user = user_db.get_user_by_api_key(api_key)
+ if not user or not user.is_active:
+ return None
+
+ return User(**user.dict())
+
+async def get_current_user_flexible(
+ token_user: Optional[User] = Depends(get_current_user),
+ api_key_user: Optional[User] = Depends(get_current_user_by_api_key)
+) -> User:
+ """Authenticate user by token or API key (flexible authentication)"""
+ user = token_user or api_key_user
+
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Authentication required"
+ )
+
+ return user
+
+def require_permissions(*required_permissions: Permission):
+ """Dependency requiring specific permissions"""
+ def permission_checker(current_user: User = Depends(get_current_user_flexible)) -> User:
+ for permission in required_permissions:
+ if permission not in current_user.permissions:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail=f"Permission '{permission}' required"
+ )
+ return current_user
+
+ return permission_checker
+
+def require_roles(*required_roles):
+ """Dependency requiring specific roles"""
+ def role_checker(current_user: User = Depends(get_current_user_flexible)) -> User:
+ if current_user.role not in required_roles:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail=f"Role must be one of: {', '.join(required_roles)}"
+ )
+ return current_user
+
+ return role_checker
+
+# ์์ฃผ ์ฐ๋ ๊ถํ ์์กด์ฑ
+RequireAdmin = require_roles("admin")
+RequireReadItems = require_permissions(Permission.READ_ITEMS)
+RequireWriteItems = require_permissions(Permission.WRITE_ITEMS)
+RequireDeleteItems = require_permissions(Permission.DELETE_ITEMS)
+RequireMCPTools = require_permissions(Permission.USE_MCP_TOOLS)
+RequireAdminMCP = require_permissions(Permission.ADMIN_MCP)
+```
+
+### ์ธ์ฆ ๋ผ์ฐํฐ (`src/auth/routes.py`)
+
+```python
+from datetime import timedelta
+from fastapi import APIRouter, Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordRequestForm
+
+from src.auth.models import (
+ User, UserCreate, UserUpdate, LoginRequest, TokenResponse,
+ user_db, UserRole
+)
+from src.auth.jwt_handler import (
+ verify_password, token_manager, verify_token, create_access_token
+)
+from src.auth.dependencies import get_current_user, RequireAdmin
+from src.core.config import settings
+
+router = APIRouter(prefix="/auth", tags=["authentication"])
+
+@router.post("/register", response_model=User)
+async def register_user(user_create: UserCreate):
+ """Register user"""
+ # ์ค๋ณต ์ฌ์ฉ์๋ช
ํ์ธ
+ if user_db.get_user_by_username(user_create.username):
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Username already registered"
+ )
+
+ # ์ฒซ ์ฌ์ฉ์๋ ์๋์ผ๋ก ๊ด๋ฆฌ์๋ก ์ค์
+ if not user_db.users:
+ user_create.role = UserRole.ADMIN
+
+ user = user_db.create_user(user_create)
+ return User(**user.dict())
+
+@router.post("/login", response_model=TokenResponse)
+async def login_user(form_data: OAuth2PasswordRequestForm = Depends()):
+ """User login"""
+ user = user_db.get_user_by_username(form_data.username)
+
+ if not user or not verify_password(form_data.password, user.hashed_password):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Incorrect username or password",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+ if not user.is_active:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="Inactive user"
+ )
+
+ # ํ ํฐ ์์ฑ
+ tokens = token_manager.create_token_pair(user.id, user.role)
+
+ # ๋ง์ง๋ง ๋ก๊ทธ์ธ ์๊ฐ ๊ฐฑ์
+ user_db.update_last_login(user.id)
+
+ return TokenResponse(
+ access_token=tokens["access_token"],
+ refresh_token=tokens["refresh_token"],
+ token_type=tokens["token_type"],
+ expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
+ user=User(**user.dict())
+ )
+
+@router.post("/refresh", response_model=dict)
+async def refresh_token(refresh_token: str):
+ """Refresh token"""
+ user_id = verify_token(refresh_token, "refresh")
+
+ if not user_id:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Invalid refresh token"
+ )
+
+ user = user_db.get_user_by_id(user_id)
+ if not user or not user.is_active:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="User not found or inactive"
+ )
+
+ # ์ ํ ํฐ ์ ์์ฑ
+ tokens = token_manager.create_token_pair(user.id, user.role)
+
+ return {
+ "access_token": tokens["access_token"],
+ "refresh_token": tokens["refresh_token"],
+ "token_type": tokens["token_type"],
+ "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
+ }
+
+@router.post("/logout")
+async def logout_user(current_user: User = Depends(get_current_user)):
+ """User logout"""
+ # ์ค์ ๊ตฌํ์์๋ ํ ํฐ์ ๋ธ๋๋ฆฌ์คํธ์ ์ถ๊ฐ
+ return {"message": "Successfully logged out"}
+
+@router.get("/me", response_model=User)
+async def get_current_user_info(current_user: User = Depends(get_current_user)):
+ """Get current user information"""
+ return current_user
+
+@router.put("/me", response_model=User)
+async def update_current_user(
+ user_update: UserUpdate,
+ current_user: User = Depends(get_current_user)
+):
+ """Update current user information"""
+ # ์ผ๋ฐ ์ฌ์ฉ์๋ ์ญํ ์ ๋ณ๊ฒฝํ ์ ์์
+ if user_update.role and current_user.role != UserRole.ADMIN:
+ user_update.role = None
+
+ updated_user = user_db.update_user(current_user.id, user_update)
+ if not updated_user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User not found"
+ )
+
+ return User(**updated_user.dict())
+
+@router.get("/users", response_model=list[User])
+async def list_users(admin_user: User = Depends(RequireAdmin)):
+ """Get user list (admin only)"""
+ return [User(**user.dict()) for user in user_db.users.values()]
+
+@router.post("/users/{user_id}/generate-api-key")
+async def generate_api_key(
+ user_id: str,
+ admin_user: User = Depends(RequireAdmin)
+):
+ """Create user API key (admin only)"""
+ import uuid
+
+ user = user_db.get_user_by_id(user_id)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="User not found"
+ )
+
+ # ์ API ํค ์์ฑ
+ new_api_key = str(uuid.uuid4())
+ user.api_key = new_api_key
+
+ return {
+ "api_key": new_api_key,
+ "message": "API key generated successfully"
+ }
+```
+
+## 5๋จ๊ณ: MCP ์๋ฒ ๊ตฌํ
+
+### MCP ๋๊ตฌ ์ ์ (`src/mcp/tools.py`)
+
+```python
+from typing import Dict, Any, List, Optional
+from pydantic import BaseModel, Field
+from enum import Enum
+
+class ToolCategory(str, Enum):
+ """Tool category"""
+ DATA_MANAGEMENT = "data_management"
+ SEARCH = "search"
+ ANALYSIS = "analysis"
+ ADMIN = "admin"
+
+class MCPTool(BaseModel):
+ """MCP tool definition"""
+ name: str = Field(..., description="Tool name")
+ description: str = Field(..., description="Tool description")
+ category: ToolCategory = Field(..., description="Tool category")
+ parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameter schema")
+ required_permissions: List[str] = Field(default_factory=list, description="Required permissions")
+ examples: List[Dict[str, Any]] = Field(default_factory=list, description="Usage examples")
+
+class ToolRegistry:
+ """Tool registry"""
+
+ def __init__(self):
+ self.tools: Dict[str, MCPTool] = {}
+ self._register_default_tools()
+
+ def _register_default_tools(self):
+ """Register default tools"""
+
+ # item ์์ฑ ๋๊ตฌ
+ self.register_tool(MCPTool(
+ name="create_item",
+ description="Create a new item",
+ category=ToolCategory.DATA_MANAGEMENT,
+ parameters={
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Item name"
+ },
+ "description": {
+ "type": "string",
+ "description": "Item description"
+ },
+ "price": {
+ "type": "number",
+ "description": "Item price",
+ "minimum": 0
+ },
+ "category": {
+ "type": "string",
+ "description": "Item category"
+ }
+ },
+ "required": ["name", "price"]
+ },
+ required_permissions=["write:items"],
+ examples=[
+ {
+ "name": "Notebook",
+ "description": "High-performance gaming notebook",
+ "price": 1500000,
+ "category": "electronics"
+ }
+ ]
+ ))
+
+ # item ๊ฒ์ ๋๊ตฌ
+ self.register_tool(MCPTool(
+ name="search_items",
+ description="Search for items",
+ category=ToolCategory.SEARCH,
+ parameters={
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Search query"
+ },
+ "category": {
+ "type": "string",
+ "description": "Category filter"
+ },
+ "min_price": {
+ "type": "number",
+ "description": "Minimum price"
+ },
+ "max_price": {
+ "type": "number",
+ "description": "Maximum price"
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Result count limit",
+ "default": 10,
+ "maximum": 100
+ }
+ },
+ "required": ["query"]
+ },
+ required_permissions=["read:items"],
+ examples=[
+ {
+ "query": "Notebook",
+ "category": "electronics",
+ "max_price": 2000000,
+ "limit": 5
+ }
+ ]
+ ))
+
+ # item ๋ถ์ ๋๊ตฌ
+ self.register_tool(MCPTool(
+ name="analyze_items",
+ description="Analyze item data",
+ category=ToolCategory.ANALYSIS,
+ parameters={
+ "type": "object",
+ "properties": {
+ "analysis_type": {
+ "type": "string",
+ "enum": ["price_distribution", "category_breakdown", "trend_analysis"],
+ "description": "Analysis type"
+ },
+ "date_range": {
+ "type": "object",
+ "properties": {
+ "start_date": {"type": "string", "format": "date"},
+ "end_date": {"type": "string", "format": "date"}
+ },
+ "description": "Analysis period"
+ }
+ },
+ "required": ["analysis_type"]
+ },
+ required_permissions=["read:items"],
+ examples=[
+ {
+ "analysis_type": "price_distribution",
+ "date_range": {
+ "start_date": "2024-01-01",
+ "end_date": "2024-12-31"
+ }
+ }
+ ]
+ ))
+
+ # ์ฌ์ฉ์ ๊ด๋ฆฌ ๋๊ตฌ (๊ด๋ฆฌ์ ์ ์ฉ)
+ self.register_tool(MCPTool(
+ name="manage_users",
+ description="Manage users",
+ category=ToolCategory.ADMIN,
+ parameters={
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "enum": ["list", "create", "update", "deactivate"],
+ "description": "Action to perform"
+ },
+ "user_data": {
+ "type": "object",
+ "description": "User data (create/update)"
+ },
+ "user_id": {
+ "type": "string",
+ "description": "User ID (update/deactivate)"
+ }
+ },
+ "required": ["action"]
+ },
+ required_permissions=["manage:users"],
+ examples=[
+ {
+ "action": "list"
+ },
+ {
+ "action": "create",
+ "user_data": {
+ "username": "newuser",
+ "email": "newuser@example.com",
+ "role": "user"
+ }
+ }
+ ]
+ ))
+
+ def register_tool(self, tool: MCPTool):
+ """Register tool"""
+ self.tools[tool.name] = tool
+
+ def get_tool(self, tool_name: str) -> Optional[MCPTool]:
+ """Get tool"""
+ return self.tools.get(tool_name)
+
+ def list_tools(self, user_permissions: List[str] = None) -> List[MCPTool]:
+ """List tools by user permissions"""
+ if user_permissions is None:
+ return list(self.tools.values())
+
+ available_tools = []
+ for tool in self.tools.values():
+ # ๊ถํ ํ์ธ
+ if all(perm in user_permissions for perm in tool.required_permissions):
+ available_tools.append(tool)
+
+ return available_tools
+
+ def get_tools_by_category(self, category: ToolCategory, user_permissions: List[str] = None) -> List[MCPTool]:
+ """List tools by category"""
+ tools = self.list_tools(user_permissions)
+ return [tool for tool in tools if tool.category == category]
+
+# ์ ์ญ ๋๊ตฌ ๋ ์ง์คํธ๋ฆฌ
+tool_registry = ToolRegistry()
+```
+
+### MCP ์๋ฒ ๊ตฌํ (`src/mcp/server.py`)
+
+```python
+from typing import Dict, Any, List, Optional
+from fastapi import HTTPException, status
+import asyncio
+import json
+
+from src.mcp.tools import tool_registry, ToolCategory
+from src.auth.models import User, Permission
+from src.api.routes.items import ItemCRUD
+from src.auth.models import user_db
+
+class MCPServer:
+ """Model Context Protocol server"""
+
+ def __init__(self):
+ self.item_crud = ItemCRUD()
+ self.active_sessions: Dict[str, Dict[str, Any]] = {}
+
+ async def create_session(self, user: User) -> str:
+ """Create MCP session"""
+ import uuid
+
+ session_id = str(uuid.uuid4())
+ self.active_sessions[session_id] = {
+ "user_id": user.id,
+ "user": user,
+ "created_at": datetime.utcnow(),
+ "context": {},
+ "tool_usage_count": 0,
+ "last_activity": datetime.utcnow()
+ }
+
+ return session_id
+
+ async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
+ """Get session"""
+ session = self.active_sessions.get(session_id)
+ if session:
+ session["last_activity"] = datetime.utcnow()
+ return session
+
+ async def close_session(self, session_id: str):
+ """Close session"""
+ if session_id in self.active_sessions:
+ del self.active_sessions[session_id]
+
+ async def list_tools(self, user: User) -> List[Dict[str, Any]]:
+ """List tools available to user"""
+ user_permissions = [perm.value for perm in user.permissions]
+ tools = tool_registry.list_tools(user_permissions)
+
+ return [
+ {
+ "name": tool.name,
+ "description": tool.description,
+ "category": tool.category,
+ "parameters": tool.parameters,
+ "examples": tool.examples
+ }
+ for tool in tools
+ ]
+
+ async def execute_tool(
+ self,
+ tool_name: str,
+ parameters: Dict[str, Any],
+ user: User,
+ session_id: Optional[str] = None
+ ) -> Dict[str, Any]:
+ """Execute tool"""
+
+ # ๋๊ตฌ ์กด์ฌ ์ฌ๋ถ ํ์ธ
+ tool = tool_registry.get_tool(tool_name)
+ if not tool:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"Tool '{tool_name}' not found"
+ )
+
+ # ๊ถํ ํ์ธ
+ user_permissions = [perm.value for perm in user.permissions]
+ for required_perm in tool.required_permissions:
+ if required_perm not in user_permissions:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail=f"Permission '{required_perm}' required for tool '{tool_name}'"
+ )
+
+ # ์ธ์
๊ฐฑ์
+ if session_id:
+ session = await self.get_session(session_id)
+ if session:
+ session["tool_usage_count"] += 1
+
+ # ๋๊ตฌ ์คํ
+ try:
+ result = await self._execute_tool_logic(tool_name, parameters, user)
+
+ return {
+ "success": True,
+ "tool": tool_name,
+ "result": result,
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+ except Exception as e:
+ return {
+ "success": False,
+ "tool": tool_name,
+ "error": str(e),
+ "timestamp": datetime.utcnow().isoformat()
+ }
+
+ async def _execute_tool_logic(
+ self,
+ tool_name: str,
+ parameters: Dict[str, Any],
+ user: User
+ ) -> Any:
+ """Execute tool logic"""
+
+ if tool_name == "create_item":
+ return await self._create_item(parameters)
+
+ elif tool_name == "search_items":
+ return await self._search_items(parameters)
+
+ elif tool_name == "analyze_items":
+ return await self._analyze_items(parameters)
+
+ elif tool_name == "manage_users":
+ return await self._manage_users(parameters, user)
+
+ else:
+ raise ValueError(f"Tool '{tool_name}' implementation not found")
+
+ async def _create_item(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
+ """Create item tool implementation"""
+ from src.schemas.items import ItemCreate
+
+ try:
+ item_create = ItemCreate(**parameters)
+ created_item = await self.item_crud.create(item_create)
+
+ return {
+ "action": "create_item",
+ "item": created_item.dict(),
+ "message": f"Item '{created_item.name}' created successfully"
+ }
+ except Exception as e:
+ raise ValueError(f"Failed to create item: {str(e)}")
+
+ async def _search_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
+ """Search item tool implementation"""
+ query = parameters.get("query", "")
+ category = parameters.get("category")
+ min_price = parameters.get("min_price")
+ max_price = parameters.get("max_price")
+ limit = parameters.get("limit", 10)
+
+ # ๊ฒ์ ๋ก์ง ๊ตฌํ
+ all_items = await self.item_crud.get_all()
+ filtered_items = []
+
+ for item in all_items:
+ # ํ
์คํธ ๊ฒ์
+ if query.lower() not in item.name.lower() and query.lower() not in (item.description or "").lower():
+ continue
+
+ # ์นดํ
๊ณ ๋ฆฌ ํํฐ
+ if category and getattr(item, 'category', None) != category:
+ continue
+
+ # ๊ฐ๊ฒฉ ํํฐ
+ if min_price is not None and item.price < min_price:
+ continue
+ if max_price is not None and item.price > max_price:
+ continue
+
+ filtered_items.append(item)
+
+ # ๊ฒฐ๊ณผ ์ ํ
+ result_items = filtered_items[:limit]
+
+ return {
+ "action": "search_items",
+ "query": query,
+ "total_found": len(filtered_items),
+ "returned_count": len(result_items),
+ "items": [item.dict() for item in result_items]
+ }
+
+ async def _analyze_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
+ """Analyze item tool implementation"""
+ analysis_type = parameters.get("analysis_type")
+ date_range = parameters.get("date_range", {})
+
+ all_items = await self.item_crud.get_all()
+
+ if analysis_type == "price_distribution":
+ prices = [item.price for item in all_items]
+ if not prices:
+ return {"analysis": "price_distribution", "result": "No items found"}
+
+ return {
+ "analysis": "price_distribution",
+ "result": {
+ "total_items": len(prices),
+ "min_price": min(prices),
+ "max_price": max(prices),
+ "average_price": sum(prices) / len(prices),
+ "price_ranges": {
+ "under_100k": len([p for p in prices if p < 100000]),
+ "100k_to_500k": len([p for p in prices if 100000 <= p < 500000]),
+ "500k_to_1m": len([p for p in prices if 500000 <= p < 1000000]),
+ "over_1m": len([p for p in prices if p >= 1000000])
+ }
+ }
+ }
+
+ elif analysis_type == "category_breakdown":
+ categories = {}
+ for item in all_items:
+ category = getattr(item, 'category', 'uncategorized')
+ categories[category] = categories.get(category, 0) + 1
+
+ return {
+ "analysis": "category_breakdown",
+ "result": {
+ "total_categories": len(categories),
+ "categories": categories
+ }
+ }
+
+ else:
+ raise ValueError(f"Unknown analysis type: {analysis_type}")
+
+ async def _manage_users(self, parameters: Dict[str, Any], requesting_user: User) -> Dict[str, Any]:
+ """Manage user tool implementation"""
+ action = parameters.get("action")
+
+ # ๊ด๋ฆฌ์ ๊ถํ ํ์ธ
+ if Permission.MANAGE_USERS not in requesting_user.permissions:
+ raise ValueError("Insufficient permissions for user management")
+
+ if action == "list":
+ users = [User(**user.dict()) for user in user_db.users.values()]
+ return {
+ "action": "list_users",
+ "total_users": len(users),
+ "users": [user.dict() for user in users]
+ }
+
+ elif action == "create":
+ user_data = parameters.get("user_data", {})
+ from src.auth.models import UserCreate
+
+ user_create = UserCreate(**user_data)
+ created_user = user_db.create_user(user_create)
+
+ return {
+ "action": "create_user",
+ "user": User(**created_user.dict()).dict(),
+ "message": f"User '{created_user.username}' created successfully"
+ }
+
+ else:
+ raise ValueError(f"Unknown user management action: {action}")
+
+# ์ ์ญ MCP ์๋ฒ ์ธ์คํด์ค
+mcp_server = MCPServer()
+```
+
+## 6๋จ๊ณ: MCP API ์๋ํฌ์ธํธ ๊ตฌํ
+
+### MCP API ๋ผ์ฐํฐ (`src/api/routes/mcp.py`)
+
+```python
+from typing import Dict, Any, Optional
+from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
+from pydantic import BaseModel
+
+from src.auth.dependencies import get_current_user_flexible, RequireMCPTools
+from src.auth.models import User
+from src.mcp.server import mcp_server
+from src.mcp.tools import ToolCategory
+
+router = APIRouter(prefix="/mcp", tags=["MCP"])
+
+class ToolExecuteRequest(BaseModel):
+ """Tool execution request"""
+ tool_name: str
+ parameters: Dict[str, Any]
+ session_id: Optional[str] = None
+
+class SessionCreateResponse(BaseModel):
+ """Session creation response"""
+ session_id: str
+ message: str
+
+@router.post("/session", response_model=SessionCreateResponse)
+async def create_mcp_session(
+ current_user: User = Depends(RequireMCPTools)
+):
+ """Create MCP session"""
+ session_id = await mcp_server.create_session(current_user)
+
+ return SessionCreateResponse(
+ session_id=session_id,
+ message=f"MCP session created (User: {current_user.username})"
+ )
+
+@router.delete("/session/{session_id}")
+async def close_mcp_session(
+ session_id: str,
+ current_user: User = Depends(RequireMCPTools)
+):
+ """Close MCP session"""
+ session = await mcp_server.get_session(session_id)
+
+ if not session:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Session not found"
+ )
+
+ # ์ธ์
์์ ์ ํ์ธ
+ if session["user_id"] != current_user.id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Cannot close another user's session"
+ )
+
+ await mcp_server.close_session(session_id)
+
+ return {"message": "Session closed successfully"}
+
+@router.get("/tools")
+async def list_mcp_tools(
+ category: Optional[ToolCategory] = None,
+ current_user: User = Depends(RequireMCPTools)
+):
+ """List available MCP tools"""
+ tools = await mcp_server.list_tools(current_user)
+
+ if category:
+ tools = [tool for tool in tools if tool["category"] == category]
+
+ return {
+ "user": current_user.username,
+ "total_tools": len(tools),
+ "tools": tools
+ }
+
+@router.post("/execute")
+async def execute_mcp_tool(
+ request: ToolExecuteRequest,
+ background_tasks: BackgroundTasks,
+ current_user: User = Depends(RequireMCPTools)
+):
+ """Execute MCP tool"""
+
+ # ์ธ์
ํ์ธ (์ ํ)
+ if request.session_id:
+ session = await mcp_server.get_session(request.session_id)
+ if not session:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail="Session not found"
+ )
+
+ if session["user_id"] != current_user.id:
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Cannot use another user's session"
+ )
+
+ # ๋๊ตฌ ์คํ
+ result = await mcp_server.execute_tool(
+ tool_name=request.tool_name,
+ parameters=request.parameters,
+ user=current_user,
+ session_id=request.session_id
+ )
+
+ # ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋๊ตฌ ์ฌ์ฉ ๋ก๊ทธ ๊ธฐ๋ก
+ background_tasks.add_task(
+ log_tool_usage,
+ current_user.id,
+ request.tool_name,
+ result["success"]
+ )
+
+ return result
+
+@router.get("/sessions")
+async def list_user_sessions(
+ current_user: User = Depends(RequireMCPTools)
+):
+ """List active user sessions"""
+ user_sessions = []
+
+ for session_id, session_data in mcp_server.active_sessions.items():
+ if session_data["user_id"] == current_user.id:
+ user_sessions.append({
+ "session_id": session_id,
+ "created_at": session_data["created_at"],
+ "tool_usage_count": session_data["tool_usage_count"],
+ "last_activity": session_data["last_activity"]
+ })
+
+ return {
+ "user": current_user.username,
+ "active_sessions": len(user_sessions),
+ "sessions": user_sessions
+ }
+
+@router.get("/stats")
+async def get_mcp_stats(
+ current_user: User = Depends(RequireMCPTools)
+):
+ """MCP usage statistics"""
+ total_sessions = len(mcp_server.active_sessions)
+ user_sessions = len([
+ s for s in mcp_server.active_sessions.values()
+ if s["user_id"] == current_user.id
+ ])
+
+ return {
+ "user_stats": {
+ "username": current_user.username,
+ "active_sessions": user_sessions,
+ "permissions": [perm.value for perm in current_user.permissions]
+ },
+ "server_stats": {
+ "total_active_sessions": total_sessions,
+ "available_tools": len(await mcp_server.list_tools(current_user))
+ }
+ }
+
+async def log_tool_usage(user_id: str, tool_name: str, success: bool):
+ """Log tool usage (background job)"""
+ import logging
+
+ logger = logging.getLogger("mcp.usage")
+ logger.info(
+ f"Tool usage - User: {user_id}, Tool: {tool_name}, Success: {success}"
+ )
+```
+
+## 7๋จ๊ณ: ์ ํ๋ฆฌ์ผ์ด์
ํตํฉ๊ณผ ํ
์คํธ
+
+### ๋ฉ์ธ ์ ํ๋ฆฌ์ผ์ด์
(`src/main.py`)
+
+```python
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from src.auth.routes import router as auth_router
+from src.api.routes.items import router as items_router
+from src.api.routes.mcp import router as mcp_router
+from src.core.config import settings
+
+app = FastAPI(
+ title="AI Integrated API",
+ description="AI model integrated MCP-based API server",
+ version="1.0.0"
+)
+
+# CORS ์ค์
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.ALLOWED_HOSTS,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# ๋ผ์ฐํฐ ํฌํจ
+app.include_router(auth_router)
+app.include_router(items_router, prefix="/api/v1")
+app.include_router(mcp_router, prefix="/api/v1")
+
+@app.get("/")
+async def root():
+ return {
+ "message": "AI Integrated API with MCP Support",
+ "version": "1.0.0",
+ "endpoints": {
+ "authentication": "/auth",
+ "items": "/api/v1/items",
+ "mcp": "/api/v1/mcp",
+ "docs": "/docs"
+ }
+ }
+
+@app.get("/health")
+async def health_check():
+ """Health check endpoint"""
+ return {
+ "status": "healthy",
+ "version": "1.0.0",
+ "services": {
+ "auth": "operational",
+ "mcp": "operational",
+ "database": "operational"
+ }
+ }
+```
+
+### ์๋ฒ ์คํ๊ณผ ํ
์คํธ
+
+
+
+```console
+$ cd ai-integrated-api
+$ fastkit runserver
+Starting FastAPI server at 127.0.0.1:8000...
+
+# ์ฌ์ฉ์ ๋ก๊ทธ์ธ
+$ curl -X POST "http://localhost:8000/auth/login" \
+ -H "Content-Type: application/x-www-form-urlencoded" \
+ -d "username=admin&password=admin123"
+
+{
+ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "token_type": "bearer",
+ "expires_in": 1800,
+ "user": {
+ "id": "123e4567-e89b-12d3-a456-426614174000",
+ "email": "admin@example.com",
+ "username": "admin",
+ "role": "admin",
+ "permissions": ["read:items", "write:items", ...]
+ }
+}
+
+# MCP ์ธ์
์์ฑ
+$ curl -X POST "http://localhost:8000/api/v1/mcp/session" \
+ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
+
+{
+ "session_id": "abc123-def456-ghi789",
+ "message": "MCP session created (User: admin)"
+}
+
+# ์ฌ์ฉ ๊ฐ๋ฅํ ๋๊ตฌ ๋ชฉ๋ก
+$ curl "http://localhost:8000/api/v1/mcp/tools" \
+ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
+
+{
+ "user": "admin",
+ "total_tools": 4,
+ "tools": [
+ {
+ "name": "create_item",
+ "description": "Create a new item",
+ "category": "data_management",
+ "parameters": {...},
+ "examples": [...]
+ },
+ ...
+ ]
+}
+
+# MCP ๋๊ตฌ ์คํ (item ์์ฑ)
+$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \
+ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "tool_name": "create_item",
+ "parameters": {
+ "name": "AI generated item",
+ "description": "MCP through AI generated item",
+ "price": 500000,
+ "category": "ai_generated"
+ },
+ "session_id": "abc123-def456-ghi789"
+ }'
+
+{
+ "success": true,
+ "tool": "create_item",
+ "result": {
+ "action": "create_item",
+ "item": {
+ "id": 1,
+ "name": "AI generated item",
+ "description": "MCP through AI generated item",
+ "price": 500000,
+ "category": "ai_generated",
+ "created_at": "2024-01-01T12:00:00Z"
+ },
+ "message": "Item 'AI generated item' created successfully"
+ },
+ "timestamp": "2024-01-01T12:00:00.123456Z"
+}
+
+# MCP ๋๊ตฌ ์คํ (item ๊ฒ์)
+$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \
+ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "tool_name": "search_items",
+ "parameters": {
+ "query": "AI",
+ "limit": 5
+ }
+ }'
+```
+
+
+
+## 8๋จ๊ณ: AI ํด๋ผ์ด์ธํธ ์์
+
+### Python MCP ํด๋ผ์ด์ธํธ ์์
+
+```python
+# client_example.py
+import asyncio
+import aiohttp
+from typing import Dict, Any, List
+
+class MCPClient:
+ """MCP client example"""
+
+ def __init__(self, base_url: str, api_key: str):
+ self.base_url = base_url
+ self.api_key = api_key
+ self.session_id = None
+ self.session = None
+
+ async def __aenter__(self):
+ self.session = aiohttp.ClientSession(
+ headers={"X-API-Key": self.api_key}
+ )
+ return self
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ if self.session_id:
+ await self.close_session()
+ if self.session:
+ await self.session.close()
+
+ async def create_session(self) -> str:
+ """Create MCP session"""
+ async with self.session.post(f"{self.base_url}/api/v1/mcp/session") as resp:
+ data = await resp.json()
+ self.session_id = data["session_id"]
+ return self.session_id
+
+ async def close_session(self):
+ """Close MCP session"""
+ if self.session_id:
+ async with self.session.delete(f"{self.base_url}/api/v1/mcp/session/{self.session_id}"):
+ pass
+ self.session_id = None
+
+ async def list_tools(self) -> List[Dict[str, Any]]:
+ """List available tools"""
+ async with self.session.get(f"{self.base_url}/api/v1/mcp/tools") as resp:
+ data = await resp.json()
+ return data["tools"]
+
+ async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
+ """Execute tool"""
+ payload = {
+ "tool_name": tool_name,
+ "parameters": parameters,
+ "session_id": self.session_id
+ }
+
+ async with self.session.post(
+ f"{self.base_url}/api/v1/mcp/execute",
+ json=payload
+ ) as resp:
+ return await resp.json()
+
+ async def ai_assistant_workflow(self, user_request: str) -> str:
+ """AI assistant workflow simulation"""
+
+ # 1. ์ธ์
์์ฑ
+ await self.create_session()
+ print(f"Session created: {self.session_id}")
+
+ # 2. ์ฌ์ฉ์ ์์ฒญ์ ๋ถ์ํด ์ ์ ํ ๋๊ตฌ ์ ํ
+ if "Create item" in user_request or "Create" in user_request:
+ # item ์์ฑ ์์ฒญ
+ result = await self.execute_tool("create_item", {
+ "name": "AI recommended item",
+ "description": "AI generated item based on user request",
+ "price": 100000,
+ "category": "ai_recommended"
+ })
+
+ if result["success"]:
+ item_name = result["result"]["item"]["name"]
+ return f"โ
'{item_name}' item created successfully!"
+ else:
+ return f"โ Item creation failed: {result.get('error', 'Unknown error')}"
+
+ elif "Search" in user_request or "Find" in user_request:
+ # ๊ฒ์ ์์ฒญ
+ search_query = "Item" # ์ค์ ๋ก๋ NLP ์์ ์ถ์ถ
+ result = await self.execute_tool("search_items", {
+ "query": search_query,
+ "limit": 5
+ })
+
+ if result["success"]:
+ items = result["result"]["items"]
+ item_list = "\n".join([f"- {item['name']} (โฉ{item['price']:,})" for item in items])
+ return f"๐ Search results ({len(items)} items):\n{item_list}"
+ else:
+ return f"โ Search failed: {result.get('error', 'Unknown error')}"
+
+ elif "Analyze" in user_request:
+ # ๋ถ์ ์์ฒญ
+ result = await self.execute_tool("analyze_items", {
+ "analysis_type": "price_distribution"
+ })
+
+ if result["success"]:
+ analysis = result["result"]["result"]
+ return f"๐ Price analysis:\nAverage price: โฉ{analysis['average_price']:,.0f}\nMinimum: โฉ{analysis['min_price']:,} - Maximum: โฉ{analysis['max_price']:,}"
+ else:
+ return f"โ Analysis failed: {result.get('error', 'Unknown error')}"
+
+ else:
+ return "Sorry, I couldn't find a tool to handle that request."
+
+async def main():
+ """Client test"""
+ async with MCPClient("http://localhost:8000", "your-api-key-here") as client:
+
+ # ์ฌ์ฉ ๊ฐ๋ฅํ ๋๊ตฌ ๋ชฉ๋ก
+ tools = await client.list_tools()
+ print(f"Available tools: {len(tools)}")
+ for tool in tools:
+ print(f"- {tool['name']}: {tool['description']}")
+
+ print("\n" + "="*50 + "\n")
+
+ # AI ์ด์์คํดํธ ์๋ฎฌ๋ ์ด์
+ test_requests = [
+ "Create a new item",
+ "Search for items",
+ "Analyze price distribution"
+ ]
+
+ for request in test_requests:
+ print(f"User request: {request}")
+ response = await client.ai_assistant_workflow(request)
+ print(f"AI response: {response}")
+ print("-" * 30)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+
+
+
+
+## ์์ฝ
+
+์ด ํํ ๋ฆฌ์ผ์์๋ MCP(Model Context Protocol) ํตํฉ์ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํํ์ต๋๋ค:
+
+- โ
JWT ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ
๊ตฌ์ถ
+- โ
์ญํ ๊ธฐ๋ฐ ์ ๊ทผ ์ ์ด (RBAC) ๊ตฌํ
+- โ
MCP ์๋ฒ์ ๋๊ตฌ ์์คํ
๊ตฌํ
+- โ
์ธ์
๊ธฐ๋ฐ ์ปจํ
์คํธ ๊ด๋ฆฌ
+- โ
AI ๋ชจ๋ธ๊ณผ์ ์์ ํ API ํต์
+- โ
๋๊ตฌ ๊ถํ ๊ด๋ฆฌ์ ์ฌ์ฉ ์ถ์
+- โ
์ค์ AI ํด๋ผ์ด์ธํธ ์์ ๊ตฌํ
+
+์ด์ AI ๋ชจ๋ธ์ด API ๊ธฐ๋ฅ์ ์์ ํ๊ณ ํจ์จ์ ์ผ๋ก ํ์ฉํ ์ ์๋ ์์ ํ MCP ๊ธฐ๋ฐ ์์คํ
์ ์ง์ ๋ง๋ค ์ ์์ต๋๋ค!
diff --git a/docs/ko/user-guide/adding-routes.md b/docs/ko/user-guide/adding-routes.md
new file mode 100644
index 0000000..09a1970
--- /dev/null
+++ b/docs/ko/user-guide/adding-routes.md
@@ -0,0 +1,581 @@
+# ๋ผ์ฐํธ ์ถ๊ฐ
+
+๊ธฐ์กด FastAPI ํ๋ก์ ํธ์ ์ API ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ์ ์๋ดํฉ๋๋ค.
+
+## ๊ธฐ๋ณธ ๋ผ์ฐํธ ์ถ๊ฐ
+
+### `addroute` ๋ช
๋ น ์ฌ์ฉ
+
+FastAPI-fastkit์ `addroute` ๋ช
๋ น์ ์ฐ๋ฉด ์ ๋ผ์ฐํธ๋ฅผ ๊ฐํธํ๊ฒ ์ถ๊ฐํ ์ ์์ต๋๋ค:
+
+
+
+```console
+$ fastkit addroute my-awesome-api users
+ Adding New Route
+โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Project โ my-awesome-api โ
+โ Route Name โ users โ
+โ Target Directory โ ~/my-awesome-api โ
+โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Do you want to add route 'users' to project 'my-awesome-api'? [Y/n]: y
+
+โญโโโโโโโโโโโโโโโโโโโโโโโโ Info โโโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โน Updated main.py to include the API router โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
+โญโโโโโโโโโโโโโโโโโโโโโโโ Success โโโโโโโโโโโโโโโโโโโโโโโโฎ
+โ โจ Successfully added new route 'users' to project โ
+โ `my-awesome-api` โ
+โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
+```
+
+
+
+## ๋ฌด์์ด ๋ง๋ค์ด์ง๋
+
+๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๋ฉด FastAPI-fastkit์ด ๋ค์ ํญ๋ชฉ์ ์๋์ผ๋ก ๋ง๋ค์ด ์ค๋๋ค:
+
+### 1. ๋ผ์ฐํธ ํ์ผ: `src/api/routes/users.py`
+
+```python
+from typing import List
+from fastapi import APIRouter, HTTPException, status
+from src.schemas.users import User, UserCreate, UserUpdate
+from src.crud.users import users_crud
+
+router = APIRouter()
+
+@router.get("/", response_model=List[User])
+def read_users():
+ """Get all users"""
+ return users_crud.get_all()
+
+@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
+def create_user(user: UserCreate):
+ """Create a new user"""
+ return users_crud.create(user)
+
+@router.get("/{user_id}", response_model=User)
+def read_user(user_id: int):
+ """Get a specific user"""
+ user = users_crud.get_by_id(user_id)
+ if user is None:
+ raise HTTPException(status_code=404, detail="User not found")
+ return user
+
+@router.put("/{user_id}", response_model=User)
+def update_user(user_id: int, user: UserUpdate):
+ """Update a user"""
+ updated_user = users_crud.update(user_id, user)
+ if updated_user is None:
+ raise HTTPException(status_code=404, detail="User not found")
+ return updated_user
+
+@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_user(user_id: int):
+ """Delete a user"""
+ success = users_crud.delete(user_id)
+ if not success:
+ raise HTTPException(status_code=404, detail="User not found")
+```
+
+### 2. CRUD ์์
: `src/crud/users.py`
+
+```python
+from typing import List, Optional
+from src.schemas.users import User, UserCreate, UserUpdate
+
+class UsersCRUD:
+ def __init__(self):
+ self._users: List[User] = []
+ self._next_id = 1
+
+ def get_all(self) -> List[User]:
+ """Get all users"""
+ return self._users
+
+ def get_by_id(self, user_id: int) -> Optional[User]:
+ """Get user by ID"""
+ return next((user for user in self._users if user.id == user_id), None)
+
+ def create(self, user: UserCreate) -> User:
+ """Create a new user"""
+ new_user = User(
+ id=self._next_id,
+ title=user.title,
+ description=user.description
+ )
+ self._next_id += 1
+ self._users.append(new_user)
+ return new_user
+
+ def update(self, user_id: int, user: UserUpdate) -> Optional[User]:
+ """Update an existing user"""
+ existing_user = self.get_by_id(user_id)
+ if existing_user:
+ update_data = user.dict(exclude_unset=True)
+ for field, value in update_data.items():
+ setattr(existing_user, field, value)
+ return existing_user
+ return None
+
+ def delete(self, user_id: int) -> bool:
+ """Delete a user"""
+ user = self.get_by_id(user_id)
+ if user:
+ self._users.remove(user)
+ return True
+ return False
+
+users_crud = UsersCRUD()
+```
+
+### 3. Pydantic ์คํค๋ง: `src/schemas/users.py`
+
+```python
+from typing import Optional
+from pydantic import BaseModel
+
+class UserBase(BaseModel):
+ title: str
+ description: Optional[str] = None
+
+class UserCreate(UserBase):
+ pass
+
+class UserUpdate(BaseModel):
+ title: Optional[str] = None
+ description: Optional[str] = None
+
+class User(UserBase):
+ id: int
+
+ class Config:
+ from_attributes = True
+```
+
+### 4. ๋ผ์ฐํฐ ๋ฑ๋ก
+
+๋ช
๋ น์ `src/api/api.py` ๋ ์๋์ผ๋ก ๊ฐฑ์ ํด ์ ๋ผ์ฐํฐ๋ฅผ ํฌํจ์ํต๋๋ค:
+
+```python
+from fastapi import APIRouter
+from src.api.routes import items, users
+
+api_router = APIRouter()
+
+api_router.include_router(items.router, prefix="/items", tags=["items"])
+api_router.include_router(users.router, prefix="/users", tags=["users"])
+```
+
+## ์์ฑ๋๋ API ์๋ํฌ์ธํธ
+
+`users` ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํ๋ฉด ๋ค์ ์๋ํฌ์ธํธ๋ค์ด ๋ง๋ค์ด์ง๋๋ค:
+
+| ๋ฉ์๋ | ์๋ํฌ์ธํธ | ์ค๋ช
|
+|--------|----------|-------------|
+| `GET` | `/api/v1/users/` | ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ |
+| `POST` | `/api/v1/users/` | ์ ์ฌ์ฉ์ ์์ฑ |
+| `GET` | `/api/v1/users/{user_id}` | ํน์ ์ฌ์ฉ์ ์กฐํ |
+| `PUT` | `/api/v1/users/{user_id}` | ์ฌ์ฉ์ ๊ฐฑ์ |
+| `DELETE` | `/api/v1/users/{user_id}` | ์ฌ์ฉ์ ์ญ์ |
+
+## ์ ๋ผ์ฐํธ ํ
์คํธํ๊ธฐ
+
+### 1. ์๋ฒ ์์
+
+
+
+```console
+$ fastkit runserver
+INFO: Uvicorn running on http://127.0.0.1:8000
+```
+
+
+
+### 2. API ๋ฌธ์ ํ์ธ
+
+[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) ์ ์ ์ํด ์ธํฐ๋ํฐ๋ธ ๋ฌธ์์์ ์ ์๋ํฌ์ธํธ๋ค์ ํ์ธํ์ธ์.
+
+### 3. curl ๋ก ํ
์คํธ
+
+**์ฌ์ฉ์ ์์ฑ:**
+
+
+```console
+$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \
+ -H "Content-Type: application/json" \
+ -d '{"title": "John Doe", "description": "Software Developer"}'
+
+{
+ "id": 1,
+ "title": "John Doe",
+ "description": "Software Developer"
+}
+```
+
+
+
+**๋ชจ๋ ์ฌ์ฉ์ ์กฐํ:**
+
+
+```console
+$ curl http://127.0.0.1:8000/api/v1/users/
+
+[
+ {
+ "id": 1,
+ "title": "John Doe",
+ "description": "Software Developer"
+ }
+]
+```
+
+
+
+**ํน์ ์ฌ์ฉ์ ์กฐํ:**
+
+
+```console
+$ curl http://127.0.0.1:8000/api/v1/users/1
+
+{
+ "id": 1,
+ "title": "John Doe",
+ "description": "Software Developer"
+}
+```
+
+
+
+## ์์ฑ๋ ์ฝ๋ ์ปค์คํฐ๋ง์ด์ฆํ๊ธฐ
+
+์์ฑ๋ ์ฝ๋๋ ์์ ๋กญ๊ฒ ์์ ํ ์ ์์ต๋๋ค. ์์ฃผ ํ๋ ๋ณ๊ฒฝ๋ค์ ์๊ฐํฉ๋๋ค:
+
+### 1. ํฅ์๋ User ์คํค๋ง
+
+์ข ๋ ํ์ค์ ์ธ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ์ํด `src/schemas/users.py` ๋ฅผ ์์ ํ์ธ์:
+
+```python
+from typing import Optional
+from datetime import datetime
+from pydantic import BaseModel, EmailStr, Field
+
+class UserBase(BaseModel):
+ email: EmailStr
+ username: str = Field(..., min_length=3, max_length=50)
+ full_name: Optional[str] = None
+ is_active: bool = True
+
+class UserCreate(UserBase):
+ password: str = Field(..., min_length=8)
+
+class UserUpdate(BaseModel):
+ email: Optional[EmailStr] = None
+ username: Optional[str] = Field(None, min_length=3, max_length=50)
+ full_name: Optional[str] = None
+ is_active: Optional[bool] = None
+
+class User(UserBase):
+ id: int
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+class UserInDB(User):
+ hashed_password: str
+```
+
+### 2. ๊ฒ์ฆ์ ๊ฐํํ CRUD
+
+๊ฒ์ฆ ๋ก์ง์ ๋ ๊ฐ์ถ `src/crud/users.py` ๋ก ๊ฐฑ์ ํ์ธ์:
+
+```python
+from typing import List, Optional
+from datetime import datetime
+import hashlib
+from src.schemas.users import UserCreate, UserUpdate, UserInDB
+
+class UsersCRUD:
+ def __init__(self):
+ self._users: List[UserInDB] = []
+ self._next_id = 1
+
+ def _hash_password(self, password: str) -> str:
+ """Simple password hashing (use bcrypt in production)"""
+ return hashlib.sha256(password.encode()).hexdigest()
+
+ def get_by_email(self, email: str) -> Optional[UserInDB]:
+ """Get user by email"""
+ return next((user for user in self._users if user.email == email), None)
+
+ def get_by_username(self, username: str) -> Optional[UserInDB]:
+ """Get user by username"""
+ return next((user for user in self._users if user.username == username), None)
+
+ def create(self, user: UserCreate) -> UserInDB:
+ """Create a new user with validation"""
+ # Check for duplicates
+ if self.get_by_email(user.email):
+ raise ValueError("Email already registered")
+ if self.get_by_username(user.username):
+ raise ValueError("Username already taken")
+
+ new_user = UserInDB(
+ id=self._next_id,
+ email=user.email,
+ username=user.username,
+ full_name=user.full_name,
+ is_active=user.is_active,
+ created_at=datetime.now(),
+ hashed_password=self._hash_password(user.password)
+ )
+ self._next_id += 1
+ self._users.append(new_user)
+ return new_user
+
+users_crud = UsersCRUD()
+```
+
+### 3. ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํ ๋ผ์ฐํธ
+
+์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๋ ๊ฐ์ถ `src/api/routes/users.py` ๋ก ๊ฐฑ์ ํ์ธ์:
+
+```python
+from typing import List
+from fastapi import APIRouter, HTTPException, status
+from src.schemas.users import User, UserCreate, UserUpdate
+from src.crud.users import users_crud
+
+router = APIRouter()
+
+@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED)
+def create_user(user: UserCreate):
+ """Create a new user"""
+ try:
+ new_user = users_crud.create(user)
+ # Return user without password hash
+ return User(**new_user.dict())
+ except ValueError as e:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail=str(e)
+ )
+
+@router.get("/{user_id}", response_model=User)
+def read_user(user_id: int):
+ """Get a specific user"""
+ user = users_crud.get_by_id(user_id)
+ if not user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"User with id {user_id} not found"
+ )
+ return User(**user.dict())
+```
+
+## ์ฌ๋ฌ ๋ผ์ฐํธ ์ถ๊ฐํ๊ธฐ
+
+์ฌ๋ฌ ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํด ๊ตฌ์กฐ๊ฐ ๊ฐ์ถฐ์ง API๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค:
+
+
+
+```console
+# ์ถ๊ฐ ๋ฆฌ์์ค ๋ผ์ฐํธ ์์ฑ
+$ fastkit addroute my-awesome-api products
+$ fastkit addroute my-awesome-api orders
+$ fastkit addroute my-awesome-api categories
+
+# ๊ฐ๊ฐ์ด ์ ์ฒด CRUD ๊ตฌ์กฐ๋ฅผ ๋ง๋ค์ด ์ค๋๋ค
+```
+
+
+
+์ด๋ ๊ฒ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ข
ํฉ์ ์ธ API ๊ตฌ์ฑ์ด ์์ฑ๋ฉ๋๋ค:
+
+- `/api/v1/users/` - ์ฌ์ฉ์ ๊ด๋ฆฌ
+- `/api/v1/products/` - ์ํ ์นดํ๋ก๊ทธ
+- `/api/v1/orders/` - ์ฃผ๋ฌธ ์ฒ๋ฆฌ
+- `/api/v1/categories/` - ์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ
+
+## ๋ผ์ฐํธ ๊ตฌ์ฑ
+
+### ๊ด๋ จ ์๋ํฌ์ธํธ ๋ฌถ๊ธฐ
+
+๋ผ์ฐํธ๋ฅผ ๋๋ฉ์ธ ๋จ์๋ก ์ ๋ฆฌํ ์ ์์ต๋๋ค:
+
+```python
+# src/api/api.py
+from fastapi import APIRouter
+from src.api.routes import users, products, orders, categories
+
+api_router = APIRouter()
+
+# User management
+api_router.include_router(
+ users.router,
+ prefix="/users",
+ tags=["User Management"]
+)
+
+# E-commerce
+api_router.include_router(
+ products.router,
+ prefix="/products",
+ tags=["E-commerce"]
+)
+api_router.include_router(
+ orders.router,
+ prefix="/orders",
+ tags=["E-commerce"]
+)
+api_router.include_router(
+ categories.router,
+ prefix="/categories",
+ tags=["E-commerce"]
+)
+```
+
+### ๋ผ์ฐํธ ์์กด์ฑ ์ถ๊ฐ
+
+์ธ์ฆ ๋ฑ ์์กด์ฑ์ ์ถ๊ฐํ ์ ์์ต๋๋ค:
+
+```python
+from fastapi import APIRouter, Depends
+from src.core.auth import get_current_user
+
+router = APIRouter()
+
+@router.get("/profile", response_model=User)
+def get_user_profile(current_user: User = Depends(get_current_user)):
+ """Get current user's profile"""
+ return current_user
+
+@router.post("/", response_model=User)
+def create_user(
+ user: UserCreate,
+ current_user: User = Depends(get_current_user)
+):
+ """Create a new user (admin only)"""
+ if not current_user.is_admin:
+ raise HTTPException(status_code=403, detail="Admin access required")
+ return users_crud.create(user)
+```
+
+## ๋ชจ๋ฒ ์ฌ๋ก
+
+### 1. ์ผ๊ด๋ ๋ช
๋ช
+
+๋ช
๋ช
๊ท์น์ ์ผ๊ด๋๊ฒ ์ ์งํ์ธ์:
+
+- **๋ผ์ฐํธ ์ด๋ฆ**: ๋ณต์ ๋ช
์ฌ ์ฌ์ฉ (`users`, `products`, `orders`)
+- **์คํค๋ง ์ด๋ฆ**: ๋จ์ ์ฌ์ฉ (`User`, `Product`, `Order`)
+- **CRUD ํด๋์ค**: ๋์ `CRUD` ๋ถ์ด๊ธฐ (`UsersCRUD`, `ProductsCRUD`)
+
+### 2. ์๋ฌ ์ฒ๋ฆฌ
+
+ํญ์ ์๋ฌ๋ฅผ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ์ธ์:
+
+```python
+@router.post("/", response_model=User)
+def create_user(user: UserCreate):
+ try:
+ return users_crud.create(user)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ raise HTTPException(status_code=500, detail="Internal server error")
+```
+
+### 3. ๋ฌธ์ํ
+
+์์ธํ docstring ์ ์ถ๊ฐํ์ธ์:
+
+```python
+@router.get("/{user_id}", response_model=User)
+def read_user(user_id: int):
+ """
+ Get a specific user by ID.
+
+ Args:
+ user_id: The unique identifier for the user
+
+ Returns:
+ User: The user object with all details
+
+ Raises:
+ HTTPException: 404 if user not found
+ """
+ user = users_crud.get_by_id(user_id)
+ if not user:
+ raise HTTPException(status_code=404, detail="User not found")
+ return user
+```
+
+### 4. ํ
์คํธ
+
+์๋ก ์ถ๊ฐํ ๋ผ์ฐํธ๋ ํญ์ ํ
์คํธํ์ธ์:
+
+```python
+# tests/test_users.py
+from fastapi.testclient import TestClient
+from src.main import app
+
+client = TestClient(app)
+
+def test_create_user():
+ user_data = {
+ "email": "test@example.com",
+ "username": "testuser",
+ "password": "securepassword123"
+ }
+ response = client.post("/api/v1/users/", json=user_data)
+ assert response.status_code == 201
+ assert response.json()["email"] == user_data["email"]
+
+def test_get_user():
+ response = client.get("/api/v1/users/1")
+ assert response.status_code == 200
+```
+
+## ๋ฌธ์ ํด๊ฒฐ
+
+### ๋ผ์ฐํธ๊ฐ ๋ณด์ด์ง ์์ ๋
+
+API ๋ฌธ์์ ๋ผ์ฐํธ๊ฐ ๋ํ๋์ง ์๋๋ค๋ฉด:
+
+1. `src/api/api.py` ์์ **๋ผ์ฐํฐ ๋ฑ๋ก์ ํ์ธ**ํ์ธ์
+2. ๋ผ์ฐํธ ์ถ๊ฐ ํ **์๋ฒ๋ฅผ ์ฌ์์**ํ์ธ์
+3. ๋ผ์ฐํธ ํ์ผ์ **import ์ค๋ฅ๊ฐ ์๋์ง ํ์ธ**ํ์ธ์
+
+### Import ์ค๋ฅ
+
+import ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค๋ฉด:
+
+1. **ํ์ผ ๊ตฌ์กฐ**๊ฐ ๊ธฐ๋ ๋ ์ด์์๊ณผ ์ผ์นํ๋์ง ํ์ธํ์ธ์
+2. ๋ผ์ฐํธ์ CRUD ํ์ผ์ **์คํค๋ง import ๋ฅผ ๊ฒ์ฆ**ํ์ธ์
+3. **๋ชจ๋ `__init__.py` ํ์ผ์ด ์กด์ฌ**ํ๋์ง ํ์ธํ์ธ์
+
+### ์๋ฒ๊ฐ ์์๋์ง ์์ ๋
+
+๋ผ์ฐํธ ์ถ๊ฐ ํ ์๋ฒ๊ฐ ์์๋์ง ์๋๋ค๋ฉด:
+
+1. ์์ฑ๋ ํ์ผ์ **๋ฌธ๋ฒ ์ค๋ฅ๊ฐ ์๋์ง ํ์ธ**ํ์ธ์
+2. ํ์ผ ๊ฐ **์คํค๋ง ํธํ์ฑ์ ๊ฒ์ฆ**ํ์ธ์
+3. ๊ตฌ์ฒด์ ์ธ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ณด๋ ค๋ฉด **๋ก๊ทธ๋ฅผ ํ์ธ**ํ์ธ์
+
+## ๋ค์ ๋จ๊ณ
+
+์ด์ ๋ผ์ฐํธ ์ถ๊ฐ ๋ฐฉ๋ฒ์ ์์์ผ๋:
+
+1. **[์ฒซ ํ๋ก์ ํธ ๋ง๋ค๊ธฐ](../tutorial/first-project.md)**: ์์ ํ ๋ธ๋ก๊ทธ API ๊ตฌ์ถ
+2. **[CLI ๋ ํผ๋ฐ์ค](cli-reference.md)**: ์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ๋ช
๋ น์ด ํ์ต
+3. **[ํ
ํ๋ฆฟ ์ฌ์ฉํ๊ธฐ](using-templates.md)**: ์ฌ์ ๊ตฌ์ถ ํ๋ก์ ํธ ํ
ํ๋ฆฟ ์ดํด๋ณด๊ธฐ
+
+!!! tip "๋ผ์ฐํธ ๊ฐ๋ฐ ํ"
+ - ์ ๋ผ์ฐํธ๋ ํญ์ ์ธํฐ๋ํฐ๋ธ ๋ฌธ์ (`/docs`) ์์ ํ
์คํธํ์ธ์
+ - ์๋ฏธ ์๋ HTTP ์ํ ์ฝ๋๋ฅผ ์ฌ์ฉํ์ธ์
+ - ๋ชจ๋ ์๋ํฌ์ธํธ์ ์ ์ ํ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๊ตฌํํ์ธ์
+ - ๋ผ์ฐํธ ํธ๋ค๋ฌ๋ ๋จ์ํ๊ฒ ์ ์งํ๊ณ ๋น์ฆ๋์ค ๋ก์ง์ CRUD ํด๋์ค์ ์์ํ์ธ์
diff --git a/docs/ko/user-guide/choosing-a-starter.md b/docs/ko/user-guide/choosing-a-starter.md
index 8e3bcfd..f89fed6 100644
--- a/docs/ko/user-guide/choosing-a-starter.md
+++ b/docs/ko/user-guide/choosing-a-starter.md
@@ -1,10 +1,10 @@
# ์ด๋ค ์คํํฐ๋ฅผ ์ ํํด์ผ ํ ๊น?
-FastAPI-fastkit ์ ํ๋ก์ ํธ๋ฅผ ์์ํ๋ ์ฌ๋ฌ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์ด ํ์ด์ง๋ ์ ๊ท ์ฌ์ฉ์๋ฅผ ์ํ **๊ฒฐ์ ๊ฐ์ด๋**์
๋๋ค: ์ฌ๊ธฐ์ ๊ฒฝ๋ก๋ฅผ ์ ํ ๋ค์, ์ค์ ํ๋ก์ ํธ ์์ฑ์ [ํต ์คํํธ](quick-start.md) ๋ก ์ด๋ํด์ ์งํํ์ธ์.
+FastAPI-fastkit์ ํ๋ก์ ํธ๋ฅผ ์์ํ๋ ์ฌ๋ฌ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ์ด ํ์ด์ง๋ ์ฒ์ ์ฐ๋ ๋ถ์ ์ํ **์ ํ ๊ฐ์ด๋**์
๋๋ค. ์ฌ๊ธฐ์ ๋ฐฉํฅ์ ์ ํ ๋ค, ์ค์ ํ๋ก์ ํธ ์์ฑ์ [ํต ์คํํธ](quick-start.md)๋ก ๋์ด๊ฐ ์งํํ์ธ์.
ํ์ ์ด ์๋ค๋ฉด, ๋ต์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
-> **`fastkit init --interactive` ๋ก ์์ํด์ `domain-starter` ํ๋ฆฌ์
์ ์ ํํ์ธ์.** ๋ชจ๋ API ํ๋ก์ ํธ๋ฅผ ์ํ ๊ถ์ฅ ๊ธฐ๋ณธ๊ฐ์
๋๋ค.
+> **`fastkit init --interactive`๋ก ์์ํด์ `domain-starter` ํ๋ฆฌ์
์ ์ ํํ์ธ์.** ํ์ฌ ๊ถ์ฅ ๊ธฐ๋ณธ๊ฐ์
๋๋ค.
์ด ํ์ด์ง์ ๋๋จธ์ง ๋ถ๋ถ์ ๊ทธ ์ด์ ์, ๋ค๋ฅธ ์ ํ์ ํด์ผ ํ ๊ฒฝ์ฐ๋ฅผ ์ค๋ช
ํฉ๋๋ค.
@@ -12,20 +12,20 @@ FastAPI-fastkit ์ ํ๋ก์ ํธ๋ฅผ ์์ํ๋ ์ฌ๋ฌ ๊ฐ์ง ๋ฐฉ๋ฒ์ ์ ๊ณต
| ๋น์ ์ด... | ์์์ |
|---|---|
-| FastAPI ๊ฐ ์ฒ์์ด๊ณ ๊ฐ์ด๋๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉฐ ์์ํ๊ณ ์ถ๋ค | `fastkit init --interactive` (preset: **`domain-starter`**) |
+| FastAPI๊ฐ ์ฒ์์ด๊ณ ๊ฐ์ด๋๋ฅผ ๋ฐ๋ผ ์ฐจ๊ทผ์ฐจ๊ทผ ์์ํ๊ณ ์ถ๋ค | `fastkit init --interactive` (preset: **`domain-starter`**) |
| ๋์ํ๋ CRUD ๋ฐ๋ชจ๋ฅผ ์ฝ๊ณ ์์ ํ๋ฉด์ ๋ฐฐ์ฐ๊ณ ์ถ๋ค | `fastkit startdemo fastapi-default` |
| ๊ฐ๋ฅํ ๊ฐ์ฅ ์์ ์ค์บํด๋๋ฅผ ์ํ๋ค | `fastkit init --interactive` (preset: **`minimal`**) |
| ๋น ๋ฅธ ํ๋กํ ํ์
/ ๋จ์ผ ํ์ผ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ๋ค | `fastkit init --interactive` (preset: **`single-module`**) |
| ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ์ํ๋ค (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` |
-| ์ค๊ฐ ๊ท๋ชจ API ๋ฅผ ์ํ ํ๋ก๋์
์คํ์ผ ๋๋ฉ์ธ ๋ ์ด์์์ ์ํ๋ค | `fastkit init --interactive` (preset: **`domain-starter`**) |
+| ์ค๊ฐ ๊ท๋ชจ API์ ์ด์ธ๋ฆฌ๋ ์ค์ ํ ๋๋ฉ์ธ ๋ ์ด์์์ ์ํ๋ค | `fastkit init --interactive` (preset: **`domain-starter`**) |
-## `startdemo` vs `init --interactive` โ ๋ฌด์์ด ๋ค๋ฅธ๊ฐ?
+## `startdemo`์ `init --interactive`๋ ๋ฌด์์ด ๋ค๋ฅธ๊ฐ?
์ด ๋์ด ๋ฉ์ธ ์ง์
์ ์ด๋ฉฐ, ์๋ก ๋ค๋ฅธ ์ฉ๋๋ฅผ ๊ฐ์ต๋๋ค.
### `fastkit startdemo `
-๊ธฐ๋ณธ ์ ๊ณต๋๋ ํ
ํ๋ฆฟ(`fastapi-default`, `fastapi-async-crud`, `fastapi-psql-orm`, `fastapi-domain-starter`, ...) ์ค ํ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก **์์ฑ๋, ๋์ํ๋ ์์ ** ํ๋ก์ ํธ๋ฅผ ๋์คํฌ์ ์์ฑํฉ๋๋ค. ํ
ํ๋ฆฟ์ ์์ค ์ฝ๋๊ฐ ์๋ ๊ทธ๋๋ก ๋ณต์ฌ๋๋ฉฐ, ๋ฉํ๋ฐ์ดํฐ ํ๋ ์ด์คํ๋(`` ๋ฑ)๋ง ์นํ๋ฉ๋๋ค.
+๊ธฐ๋ณธ ์ ๊ณต ํ
ํ๋ฆฟ(`fastapi-default`, `fastapi-async-crud`, `fastapi-psql-orm`, `fastapi-domain-starter`, ...) ๊ฐ์ด๋ฐ ํ๋๋ฅผ ๋ฐํ์ผ๋ก **๋ฐ๋ก ์คํํด ๋ณผ ์ ์๋ ์์ฑํ ์์ ํ๋ก์ ํธ**๋ฅผ ๋์คํฌ์ ์์ฑํฉ๋๋ค. ํ
ํ๋ฆฟ์ ์์ค ์ฝ๋๋ ๊ฑฐ์ ๊ทธ๋๋ก ๋ณต์ฌ๋๊ณ , ๋ฉํ๋ฐ์ดํฐ ํ๋ ์ด์คํ๋(`` ๋ฑ)๋ง ์นํ๋ฉ๋๋ค.
- โ
์คํ ๊ฐ๋ฅํ ๋ฐ๋ชจ๊น์ง ๊ฐ์ฅ ๋น ๋ฅธ ๊ฒฝ๋ก.
- โ
๋ชจ๋ ์ฝ๋๊ฐ ์ค์ ๋ก ๋์ํ๊ณ ์ฝ๊ธฐ ์ฌ์, ์์ ๋ก ํ์ตํ๊ธฐ์ ์ข์.
@@ -38,11 +38,11 @@ $ fastkit startdemo fastapi-default # ๊ทธ์ค ํ๋๋ก ํ๋ก์ ํธ ์์ฑ
### `fastkit init --interactive`
-**๊ฐ์ด๋ํ ๋ง๋ฒ์ฌ**๋ฅผ ๋ฐ๋ผ ์งํํฉ๋๋ค: ํ๋ก์ ํธ ๋ฉํ๋ฐ์ดํฐ โ ์ํคํ
์ฒ ํ๋ฆฌ์
โ ๊ธฐ๋ฅ ์ ํ (๋ฐ์ดํฐ๋ฒ ์ด์ค, ์ธ์ฆ, ํ
์คํธ, ๋ฐฐํฌ, ...) โ ํจํค์ง ๋งค๋์ โ ํ์ธ. ์์ฑ๊ธฐ๋ ํ๋ฆฌ์
๋ณ๋ก ์ ์ ํ ๋ฒ ์ด์ค ํ
ํ๋ฆฟ์ ๊ณ ๋ฅด๊ณ , ๊ทธ ์์ ์ ํํ ๊ธฐ๋ฅ์ ๋ง์
ํ๋๋ค.
+**๋ํํ ๋ง๋ฒ์ฌ**๋ฅผ ๋ฐ๋ผ ์งํํฉ๋๋ค. ํ๋ก์ ํธ ๋ฉํ๋ฐ์ดํฐ โ ์ํคํ
์ฒ ํ๋ฆฌ์
โ ๊ธฐ๋ฅ ์ ํ(๋ฐ์ดํฐ๋ฒ ์ด์ค, ์ธ์ฆ, ํ
์คํธ, ๋ฐฐํฌ, ...) โ ํจํค์ง ๋งค๋์ โ ํ์ธ ์์๋ก ์ด์ด์ง๋๋ค. ์์ฑ๊ธฐ๋ ํ๋ฆฌ์
๋ณ๋ก ์ ์ ํ ๋ฒ ์ด์ค ํ
ํ๋ฆฟ์ ๊ณ ๋ฅด๊ณ , ๊ทธ ์์ ์ ํํ ๊ธฐ๋ฅ์ ๋ง์
ํ๋๋ค.
- โ
์ค์ ๋ก ์ํ๋ ์คํ์ ์ง์ ์กฐ๋ฆฝํ ์ ์์.
- โ
์ํคํ
์ฒ ํ๋ฆฌ์
์ด ํ๋ก์ ํธ ๋ ์ด์์(๋จ์ผ ํ์ผ, ๊ณ์ธตํ, ๋๋ฉ์ธ ์งํฅ, ...)์ ๊ฒฐ์ .
-- โ `main.py` ๋ณด์กด ํ๋ฆฌ์
(`classic-layered`, `domain-starter`)์ ์ค์ ๋ชจ๋์ ์์ฑํด ์ฃผ์ง๋ง, ๊ทธ ๋ชจ๋์ ๋ผ์ฐํฐ์ ์ฐ๊ฒฐํ๋ ์์
์ ์ฌ์ฉ์๊ฐ ์ง์ ํด์ผ ํจ. ํ๋ฆฌ์
๋ณ/๊ธฐ๋ฅ๋ณ ๊ณ์ฝ์ [์ํคํ
์ฒ ํ๋ฆฌ์
๋งคํธ๋ฆญ์ค](../reference/preset-feature-matrix.md) ๋ฅผ ์ฐธ๊ณ ํ์ธ์.
+- โ `main.py`๋ฅผ ๋ณด์กดํ๋ ํ๋ฆฌ์
(`classic-layered`, `domain-starter`)์ ์ค์ ๋ชจ๋๊น์ง๋ ๋ง๋ค์ด ์ฃผ์ง๋ง, ํด๋น ๋ชจ๋์ ๋ผ์ฐํฐ์ ์ฐ๊ฒฐํ๋ ์์
์ ์ฌ์ฉ์๊ฐ ์ง์ ํด์ผ ํจ. ํ๋ฆฌ์
๊ณผ ๊ธฐ๋ฅ ์กฐํฉ๋ณ ๋์์ [์ํคํ
์ฒ ํ๋ฆฌ์
๋งคํธ๋ฆญ์ค](../reference/preset-feature-matrix.md)๋ฅผ ์ฐธ๊ณ ํ์ธ์.
```console
$ fastkit init --interactive
@@ -56,9 +56,9 @@ $ fastkit init --interactive
๊ฐ์ฅ ์์ ๋์ ๊ฐ๋ฅํ FastAPI ์ฑ. ๋น ์ค์บํด๋ + ์ ํํ ๊ธฐ๋ฅ ํ๋๊ทธ๋ก๋ถํฐ ์ฌ์์ฑ๋ ๋จ์ผ `src/main.py` ์
๋๋ค. CORS, ๋ ์ดํธ ์ ํ, Prometheus ๊ณ์ธก์ ์ ํ ์ ์๋์ผ๋ก `main.py` ์ ์ฐ๊ฒฐ๋ฉ๋๋ค.
-- ๐ค **๋์**: ํ๋ก์ ํธ๊ฐ ์๋ผ๋จ์ ๋ฐ๋ผ ์ง์ ๊ตฌ์กฐ๋ฅผ ๋ง๋ค๊ณ ์ถ๊ฑฐ๋, ๋ ์ด์์์ ๋ํ ์ ์
๊ฒฌ ์์ด FastAPI ๋ฅผ ํ์ํ๊ณ ์ถ์ ์ฌ๋.
+- ๐ค **๋์**: ํ๋ก์ ํธ๊ฐ ์๋ผ๋ฉด์ ์ง์ ๊ตฌ์กฐ๋ฅผ ์ก๊ณ ์ถ๊ฑฐ๋, ํน์ ๋ ์ด์์์ ์ฝ๋งค์ด์ง ์๊ณ FastAPI๋ฅผ ํ์ํด ๋ณด๊ณ ์ถ์ ์ฌ๋.
- ๐ฆ **๋ฒ ์ด์ค ํ
ํ๋ฆฟ**: `fastapi-empty`.
-- ๐ง **๋ฉํ ๋ชจ๋ธ**: "FastAPI ๋ฅผ import ํ ๋จ์ผ ํ์ผ์ ๋ฐ๊ณ ๋๋จธ์ง๋ ๋ด๊ฐ ์์์."
+- ๐ง **์ด๋ ๊ฒ ์ดํดํ๋ฉด ์ฝ์ต๋๋ค**: "FastAPI๋ฅผ importํ ๋จ์ผ ํ์ผ ํ๋๋ง ๋ฐ๊ณ , ๋๋จธ์ง๋ ๋ด๊ฐ ์ฑ์ ๋ฃ๋๋ค."
### `single-module` โ ์คํฌ๋ฆฝํธํ ํ๋กํ ํ์
@@ -66,7 +66,7 @@ $ fastkit init --interactive
- ๐ค **๋์**: ๊ธ๋ฃจ ์คํฌ๋ฆฝํธ, ์์ webhook, ๋๋ ํจํค์ง ๊ฒฝ๊ณ๊ฐ ํ์ ์๋ ํ๋ฃจ์ง๋ฆฌ ํ๋กํ ํ์
.
- ๐ฆ **๋ฒ ์ด์ค ํ
ํ๋ฆฟ**: `fastapi-single-module`.
-- ๐ง **๋ฉํ ๋ชจ๋ธ**: "ํ ๋ฒ์ ์คํํ๊ณ ํ ๋ฒ์ ์ฝ์ ์ ์๋ Python ํ์ผ ํ๋๋ง ์์ผ๋ฉด ๋๋ค."
+- ๐ง **์ด๋ ๊ฒ ์ดํดํ๋ฉด ์ฝ์ต๋๋ค**: "ํ ๋ฒ์ ์คํํ๊ณ ํ ๋ฒ์ ๋ค ์ฝ์ ์ ์๋ Python ํ์ผ ํ๋๋ฉด ์ถฉ๋ถํ๋ค."
### `classic-layered` โ ๊ณ์ธตํ ๋ถํ (api / crud / schemas / core)
@@ -74,15 +74,15 @@ $ fastkit init --interactive
- ๐ค **๋์**: Django/Rails ํ ๋ ์ด์์์ ์ต์ํ ํ, ๊ณตํต CRUD ๋ฐฐ๊ด์ ๊ณต์ ํ๋ ์์ ์๋ํฌ์ธํธ๊ฐ ๋ง์ ํ๋ก์ ํธ.
- ๐ฆ **๋ฒ ์ด์ค ํ
ํ๋ฆฟ**: `fastapi-default`.
-- ๐ง **๋ฉํ ๋ชจ๋ธ**: "์ฝ๋๋ฅผ *๋ฌด์์ธ์ง*์ ๋ฐ๋ผ ๋๋๋ค."
+- ๐ง **์ด๋ ๊ฒ ์ดํดํ๋ฉด ์ฝ์ต๋๋ค**: "์ฝ๋๋ฅผ *๋ฌด์์ ๋ด๋นํ๋์ง* ๊ธฐ์ค์ผ๋ก ๋๋๋ค."
### `domain-starter` โ ๋๋ฉ์ธ ์งํฅ (๊ถ์ฅ ๊ธฐ๋ณธ๊ฐ)
์ฝ๋๋ฅผ **๋น์ฆ๋์ค ๊ฐ๋
๋ณ**๋ก ์์ง ๋ถํ ํฉ๋๋ค: ๊ฐ ๋๋ฉ์ธ์ ์์ ์ router, service, repository, schemas ๋ฅผ `src/app/domains//` ์๋์ ์์ ํฉ๋๋ค. `/health` ์๋ํฌ์ธํธ์, ์ ๊ฐ๋
๋ง๋ค ๋ณต์ฌํด ์ด๋ฆ๋ง ๋ฐ๊พธ๋ฉด ๋๋ `items` ์์ ๋๋ฉ์ธ์ด ํจ๊ป ์ ๊ณต๋ฉ๋๋ค. ๊ธฐ๋ณธ ์ ๊ณต๋ `main.py` (`src/app/` ์๋) ๋ ๋ณด์กด๋๋ฉฐ, ์์ฑ๋ ์ค์ ํ์ผ์ `src/app/core/` ์๋์ ๋ฐฐ์น๋ฉ๋๋ค.
-- ๐ค **๋์**: ์ฌ๋ฌ ๊ฐ์ ๋ณ๊ฐ ๊ฐ๋
(users, orders, billing, ...)์ด ์๋ผ๋ ์ค๊ฐ ๊ท๋ชจ API. ๊ถ์ฅ ๋ชจ๋ ๊ธฐ๋ณธ๊ฐ.
+- ๐ค **๋์**: users, orders, billing์ฒ๋ผ ์ฌ๋ฌ ๋น์ฆ๋์ค ๊ฐ๋
์ด ํจ๊ป ์๋ผ๋ ์ค๊ฐ ๊ท๋ชจ API. ํ์ฌ ๊ถ์ฅ ๊ธฐ๋ณธ๊ฐ.
- ๐ฆ **๋ฒ ์ด์ค ํ
ํ๋ฆฟ**: `fastapi-domain-starter`.
-- ๐ง **๋ฉํ ๋ชจ๋ธ**: "์ฝ๋๋ฅผ *๋น์ฆ๋์ค์ ์ผ๋ก ๋ฌด์์ ํ๋์ง*์ ๋ฐ๋ผ ๋๋๋ค."
+- ๐ง **์ด๋ ๊ฒ ์ดํดํ๋ฉด ์ฝ์ต๋๋ค**: "์ฝ๋๋ฅผ *๋น์ฆ๋์ค์ ์ผ๋ก ๋ฌด์์ ํ๋์ง* ๊ธฐ์ค์ผ๋ก ๋๋๋ค."
## ๋น๊ต ๋งคํธ๋ฆญ์ค
@@ -100,18 +100,18 @@ $ fastkit init --interactive
| pyproject ์ฐ์ | ์ ํ ์ฌํญ | ์ ํ ์ฌํญ | ์ ํ ์ฌํญ | โ
|
| ์ ํฉํ ๊ฒฝ์ฐ | "๊ตฌ์กฐ๋ ๋ด๊ฐ ํค์ด๋ค" | "ํ ํ์ผ ํ๋กํ ํ์
" | "๊ด์ฌ์ฌ๋ก ๋ถํ " | "๋น์ฆ๋์ค ๊ฐ๋
์ผ๋ก ๋ถํ " |
-๊ธฐ๋ฅ๋ณ ์ ์ฒด ๊ณ์ฝ (๋ฐ์ดํฐ๋ฒ ์ด์ค / ์ธ์ฆ ์ค์ ํ์ผ ๊ฒฝ๋ก, ์ด๋ค ์ ํ์ด ์๋ ์ฐ๊ฒฐ๋๊ณ ์ด๋ค ์ ํ์ ์๋ ์ฐ๊ฒฐ์ด ํ์ํ์ง, ๊ฒฝ๊ณ ๊ฐ ์ธ์ ๋ฐ์ํ๋์ง) ์ [์ํคํ
์ฒ ํ๋ฆฌ์
๋งคํธ๋ฆญ์ค](../reference/preset-feature-matrix.md) ๋ฅผ ์ฐธ๊ณ ํ์ธ์.
+๊ธฐ๋ฅ๋ณ ์ ์ฒด ๊ณ์ฝ(๋ฐ์ดํฐ๋ฒ ์ด์ค / ์ธ์ฆ ์ค์ ํ์ผ ๊ฒฝ๋ก, ์ด๋ค ์ ํ์ด ์๋ ์ฐ๊ฒฐ๋๊ณ ์ด๋ค ์ ํ์ ์๋ ์ฐ๊ฒฐ์ด ํ์ํ์ง, ๊ฒฝ๊ณ ๊ฐ ์ธ์ ๋ฐ์ํ๋์ง)์ [์ํคํ
์ฒ ํ๋ฆฌ์
๋งคํธ๋ฆญ์ค](../reference/preset-feature-matrix.md)๋ฅผ ์ฐธ๊ณ ํ์ธ์.
## `startdemo` ํ
ํ๋ฆฟ ์ ํํ๊ธฐ
-`fastkit startdemo ` ๋ ๊ฐ์ด๋ํ ์กฐ๋ฆฝ์ด ์๋ **์์ฑ๋, ์คํ ๊ฐ๋ฅํ ์์ **๋ฅผ ์ํ ๋ ๊ฐ์ฅ ์ ํฉํฉ๋๋ค. ๋๋ถ๋ถ์ ํ
ํ๋ฆฟ์ ์ ๋ค ๊ฐ์ง ํ๋ฆฌ์
์ค ํ๋์ ๋๋ต ๋์ํ์ง๋ง, ์ถ๊ฐ์ ์ธ ์์ ์ฝ๋(๋ชฉ ์คํ ์ด ์์ CRUD ์๋ํฌ์ธํธ, ์ปค์คํ
์๋ต ์ฒ๋ฆฌ, Docker ๋๊ตฌ ๋ฑ)์ ํจ๊ป ์ ๊ณต๋ฉ๋๋ค.
+`fastkit startdemo `๋ ๊ธฐ๋ฅ์ ์ง์ ์กฐํฉํ๋ ๋ฐฉ์๋ณด๋ค **์์ฑ๋ ์คํ ์์ **๊ฐ ํ์ํ ๋ ๊ฐ์ฅ ์ ๋ง์ต๋๋ค. ๋๋ถ๋ถ์ ํ
ํ๋ฆฟ์ ์ ๋ค ๊ฐ์ง ํ๋ฆฌ์
์ค ํ๋์ ๋๋ต ๋์ํ์ง๋ง, ๋ชฉ ์คํ ์ด ๊ธฐ๋ฐ CRUD ์๋ํฌ์ธํธ, ์๋ต ํฌ๋งคํ
์์ , Docker ๋๊ตฌ์ฒ๋ผ ์ถ๊ฐ ์์ ์ฝ๋๋ ํจ๊ป ์ ๊ณตํฉ๋๋ค.
| ํ
ํ๋ฆฟ | ๊ฐ์ฅ ๊ฐ๊น์ด ํ๋ฆฌ์
| ์ ํ ์์ |
|---|---|---|
| `fastapi-default` | `classic-layered` | ๊ณ์ธตํ ๋ ์ด์์ ์์ ๋์ํ๋ CRUD ๋ฐ๋ชจ. ์ข์ ์ฒซ ์ถ๋ฐ์ . |
| `fastapi-empty` | `minimal` | ๋น ์ค์บํด๋. `minimal` ์ด ๋๋ฌํ๋ ๋ชจ์๊ณผ ๋์ผ. |
| `fastapi-single-module` | `single-module` | ๋จ์ผ ํ์ผ ๋ฐ๋ชจ. |
-| `fastapi-domain-starter` | `domain-starter` | ๊ถ์ฅ ๋ชจ๋ ๊ธฐ๋ณธ๊ฐ. items ๋๋ฉ์ธ ์์ ํฌํจ. |
+| `fastapi-domain-starter` | `domain-starter` | ํ์ฌ ๊ถ์ฅ ๊ธฐ๋ณธ๊ฐ. items ๋๋ฉ์ธ ์์ ํฌํจ. |
| `fastapi-async-crud` | `classic-layered` | `fastapi-default` ์ ๋น๋๊ธฐ ๋ฒ์ . |
| `fastapi-custom-response` | `classic-layered` | ์ปค์คํ
์๋ต envelope / ํฌ๋งคํ
์ ๋ณด์ฌ์ค. |
| `fastapi-dockerized` | `classic-layered` | ๊ธฐ๋ณธ ๋ ์ด์์์ ํ๋ก๋์
์์ค์ Dockerfile ์ถ๊ฐ. |
@@ -125,14 +125,14 @@ $ fastkit init --interactive
**Q. ํ๋ฆฌ์
/ ํ
ํ๋ฆฟ์ ๋ฏธ๋ฆฌ ๊ฒฐ์ ํด์ผ ํ๋์?**
์๋๋๋ค โ ์์ฑ๋ ์ฝ๋๋ ๋์ค์ ์์ผ๋ก ์์ ๋กญ๊ฒ ์ฌ๊ตฌ์ฑํ ์ ์์ต๋๋ค. ํ๋ฆฌ์
์ ์ถ๋ฐ์ ์ผ ๋ฟ ๊ณ์ฝ์ด ์๋๋๋ค. ๋๋ฌด ๊น๊ฒ ๊ณ ๋ฏผํ์ง ๋ง์ธ์.
-**Q. "๋ชจ๋ํ" ์ ํ์ ๋ฌด์์ธ๊ฐ์?**
-`domain-starter`. pyproject ์ฐ์ ์ด๋ฉฐ, `/health` ์๋ํฌ์ธํธ๊ฐ ๋ด์ฅ๋์ด ์๊ณ , ์ ์ด์๋๋ ์ค๊ฐ ๊ท๋ชจ FastAPI ํ๋ก์ ํธ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์๋ ดํ๋ ๋ ์ด์์์ ์ฌ์ฉํฉ๋๋ค.
+**Q. ์ง๊ธ ์ถ์ฒํ๋ ๊ธฐ๋ณธ ์ ํ์ ๋ฌด์์ธ๊ฐ์?**
+`domain-starter`์
๋๋ค. pyproject๋ฅผ ์ฐ์ ์ผ๋ก ์ฌ์ฉํ๊ณ , `/health` ์๋ํฌ์ธํธ๊ฐ ๊ธฐ๋ณธ์ผ๋ก ๋ค์ด ์์ผ๋ฉฐ, ๊ด๋ฆฌ๊ฐ ์ฌ์ด ์ค๊ฐ ๊ท๋ชจ FastAPI ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๋ฐ๋ผ๊ฐ๋๋ค.
**Q. ๋์ค์ `classic-layered` ์์ `domain-starter` ๋ก ๋ฐ๊ฟ ์ ์๋์?**
๋ค, ๋จ ๋ง์ด๊ทธ๋ ์ด์
๋ช
๋ น์ ์์ผ๋ฉฐ ์๋ ๋ฆฌํฉํฐ๋ง์ด ํ์ํฉ๋๋ค. ํ๋ก์ ํธ๊ฐ ์๋ผ์ ๋๋ฉ์ธ ํด๋๊ฐ ํ์ํด์ง ๊ฑฐ๋ผ๊ณ ํ๋จ๋๋ฉด ์ฒ์๋ถํฐ `domain-starter` ๋ก ์์ํ์ธ์.
-**Q. ๊ทธ๋ฅ FastAPI ๋ฅผ ํ์ตํ๊ณ ์ถ๋ค๋ฉด์?**
-`fastkit startdemo fastapi-default` ๋ก ์์ํ์ธ์ โ ์ฝ๋๋ฅผ ์ฝ๊ณ , ํ
์คํธ๋ฅผ ์คํํ๊ณ , ์๋ํฌ์ธํธ ๋ช ๊ฐ๋ฅผ ๋ฐ๊ฟ ๋ณด์ธ์. ์ต์ํด์ง๋ฉด `fastkit init --interactive` ์ `domain-starter` ํ๋ฆฌ์
์ด ์์ฐ์ค๋ฌ์ด ๋ค์ ๋จ๊ณ์
๋๋ค.
+**Q. ๊ทธ๋ฅ FastAPI๋ฅผ ํ์ตํ๊ณ ์ถ๋ค๋ฉด์?**
+`fastkit startdemo fastapi-default`๋ก ์์ํ์ธ์. ์ฝ๋๋ฅผ ์ฝ๊ณ , ํ
์คํธ๋ฅผ ์คํํ๊ณ , ์๋ํฌ์ธํธ ๋ช ๊ฐ๋ฅผ ์ง์ ๋ฐ๊ฟ ๋ณด์ธ์. ์ต์ํด์ง๋ฉด `fastkit init --interactive`์ `domain-starter` ํ๋ฆฌ์
์ผ๋ก ๋์ด๊ฐ๋ ๊ฒ์ด ์์ฐ์ค๋ฌ์ด ๋ค์ ๋จ๊ณ์
๋๋ค.
**Q. ๊ฐ ํ๋ฆฌ์
์ด ์ ํํ ์ด๋ค ํ์ผ์ ์์ฑํ๋์ง๋ ์ด๋์ ๋ณด๋์?**
[์ํคํ
์ฒ ํ๋ฆฌ์
๋งคํธ๋ฆญ์ค](../reference/preset-feature-matrix.md) ๊ฐ ๊ทธ ๋ ํผ๋ฐ์ค ํ์ด์ง์
๋๋ค.
@@ -141,5 +141,5 @@ $ fastkit init --interactive
- [ํต ์คํํธ](quick-start.md) โ ์ค์ ๋ก ์ฒซ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด ๋ด
๋๋ค.
- [ํ๋ก์ ํธ ์์ฑ](creating-projects.md) โ CLI ํ๋๊ทธ๋ฅผ ๋ ๊น์ด ๋ค๋ฃจ๋ ์๋ด.
-- [๋๋ฉ์ธ ์งํฅ ํ๋ก์ ํธ ํํ ๋ฆฌ์ผ](../tutorial/domain-starter.md) โ `domain-starter` ๋ฅผ ๊ณจ๋๋ค๋ฉด, ์์ฑ๋ ํธ๋ฆฌ / ๋ฒ๋ค๋ `items` ์์ / ๋ค์ ๋๋ฉ์ธ์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ๊น์ง ๋ค๋ฃจ๋ ์๋ํฌ์๋ ์ํฌ์ค๋ฃจ.
+- [๋๋ฉ์ธ ์งํฅ ํ๋ก์ ํธ ํํ ๋ฆฌ์ผ](../tutorial/domain-starter.md) โ `domain-starter`๋ฅผ ๊ณจ๋๋ค๋ฉด, ์์ฑ๋ ํธ๋ฆฌ์ ๋ฒ๋ค๋ `items` ์์ , ๋ค์ ๋๋ฉ์ธ์ ์ถ๊ฐํ๋ ๋ฐฉ๋ฒ๊น์ง ์ ์ฒด ํ๋ฆ์ ๋ฐ๋ผ๊ฐ๋ฉฐ ์ค๋ช
ํฉ๋๋ค.
- [์ํคํ
์ฒ ํ๋ฆฌ์
๋งคํธ๋ฆญ์ค](../reference/preset-feature-matrix.md) โ ํ๋ฆฌ์
๋ณ/๊ธฐ๋ฅ๋ณ ์ ์ฒด ๊ณ์ฝ.
diff --git a/docs/ko/user-guide/cli-reference.md b/docs/ko/user-guide/cli-reference.md
new file mode 100644
index 0000000..fea6073
--- /dev/null
+++ b/docs/ko/user-guide/cli-reference.md
@@ -0,0 +1,821 @@
+# CLI ๋ ํผ๋ฐ์ค
+
+FastAPI-fastkit์ ๋ชจ๋ ์ปค๋งจ๋๋ผ์ธ ์ธํฐํ์ด์ค ๋ช
๋ น์ด๋ฅผ ํ๋์ ๋ณผ ์ ์๋ ๋ ํผ๋ฐ์ค์
๋๋ค.
+
+## ์ ์ญ ์ต์
+
+๋ชจ๋ ๋ช
๋ น์ ๋ค์์ ์ ์ญ ์ต์
์ ์ง์ํฉ๋๋ค:
+
+```console
+$ fastkit [GLOBAL_OPTIONS] COMMAND [COMMAND_OPTIONS]
+```
+
+### ์ ์ญ ์ต์
+
+| ์ต์
| ์ค๋ช
|
+|--------|-------------|
+| `--version` | FastAPI-fastkit ๋ฒ์ ํ์ |
+| `--help` | ๋์๋ง ํ์ |
+
+### ์์
+
+
+
+```console
+$ fastkit --version
+FastAPI-fastkit version 1.0.0
+
+$ fastkit --help
+Usage: fastkit [OPTIONS] COMMAND [ARGS]...
+
+ FastAPI-fastkit CLI
+
+Options:
+ --version Show the version and exit.
+ --help Show this message and exit.
+
+Commands:
+ addroute Add a new route to FastAPI project
+ init Create a new FastAPI project
+ list-templates List available FastAPI templates
+ runserver Start FastAPI development server
+ startdemo Create FastAPI project from template
+```
+
+
+
+## ๋ช
๋ น์ด
+
+### `init`
+
+๋ํํ ์ค์ ์ผ๋ก ์ FastAPI ํ๋ก์ ํธ๋ฅผ ์์ฑํฉ๋๋ค.
+
+#### ๋ฌธ๋ฒ
+
+```console
+$ fastkit init [OPTIONS]
+```
+
+#### ์ต์
+
+| ์ต์
| ์ค๋ช
| ๊ธฐ๋ณธ๊ฐ |
+|--------|-------------|---------|
+| `--package-manager` | ์ฌ์ฉํ ํจํค์ง ๋งค๋์ (pip, uv, pdm, poetry) | uv |
+| `--help` | ๋ช
๋ น ๋์๋ง ํ์ | - |
+
+#### ๋ํํ ํ๋กฌํํธ
+
+`init` ๋ช
๋ น์ ๋ค์์ ๋ฌป์ต๋๋ค:
+
+1. **ํ๋ก์ ํธ ์ด๋ฆ**: ๋๋ ํฐ๋ฆฌ ์ด๋ฆ์ด์ ํจํค์ง ์ด๋ฆ
+2. **์์ฑ์ ์ด๋ฆ**: ํจํค์ง ์์ฑ์ ์ ๋ณด
+3. **์์ฑ์ ์ด๋ฉ์ผ**: ํจํค์ง ์ฐ๋ฝ ์ด๋ฉ์ผ
+4. **ํ๋ก์ ํธ ์ค๋ช
**: ํ๋ก์ ํธ์ ๋ํ ์งง์ ์ค๋ช
+5. **์คํ ์ ํ**: minimal, standard, full ์ค ์ ํ
+6. **ํจํค์ง ๋งค๋์ ์ ํ**: pip, uv, pdm, poetry ์ค ์ ํ (`--package-manager` ๋ก ์ง์ ํ ๊ฒฝ์ฐ๋ ์ ์ธ)
+
+#### ์คํ ์ต์
+
+**MINIMAL ์คํ:**
+
+- `fastapi` - FastAPI ํ๋ ์์ํฌ
+- `uvicorn` - ASGI ์๋ฒ
+- `pydantic` - ๋ฐ์ดํฐ ๊ฒ์ฆ
+- `pydantic-settings` - ์ค์ ๊ด๋ฆฌ
+
+**STANDARD ์คํ:**
+
+- ๋ชจ๋ MINIMAL ์คํ ํจํค์ง
+- `sqlalchemy` - SQL ํดํท ๋ฐ ORM
+- `alembic` - ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ง์ด๊ทธ๋ ์ด์
๋๊ตฌ
+- `pytest` - ํ
์คํธ ํ๋ ์์ํฌ
+
+**FULL ์คํ:**
+
+- ๋ชจ๋ STANDARD ์คํ ํจํค์ง
+- `redis` - ์ธ๋ฉ๋ชจ๋ฆฌ ๋ฐ์ดํฐ ์ ์ฅ์
+- `celery` - ๋ถ์ฐ ์์
ํ
+
+#### ์์
+
+
+
+```console
+$ fastkit init
+Enter the project name: my-api
+Enter the author name: John Doe
+Enter the author email: john@example.com
+Enter the project description: My awesome API
+
+Select stack (minimal, standard, full): standard
+Select package manager (pip, uv, pdm, poetry) [uv]: uv
+Do you want to proceed with project creation? [y/N]: y
+
+โจ FastAPI project 'my-api' has been created successfully!
+```
+
+
+
+#### ์์ฑ๋๋ ๊ตฌ์กฐ
+
+๋ค์ ๊ตฌ์กฐ์ ํ๋ก์ ํธ๋ฅผ ์์ฑํฉ๋๋ค:
+
+```
+my-api/
+โโโ .venv/ # ๊ฐ์ ํ๊ฒฝ
+โโโ src/
+โ โโโ __init__.py
+โ โโโ main.py # FastAPI ์ ํ๋ฆฌ์ผ์ด์
+โ โโโ core/
+โ โ โโโ __init__.py
+โ โ โโโ config.py # ์ค์
+โ โโโ api/
+โ โ โโโ __init__.py
+โ โ โโโ api.py # API ๋ผ์ฐํฐ ๋ชจ์
+โ โ โโโ routes/
+โ โ โโโ __init__.py
+โ โ โโโ items.py # ์์ ๋ผ์ฐํธ
+โ โโโ crud/
+โ โ โโโ __init__.py
+โ โ โโโ items.py # CRUD ์์
+โ โโโ schemas/
+โ โ โโโ __init__.py
+โ โ โโโ items.py # Pydantic ์คํค๋ง
+โ โโโ mocks/
+โ โโโ __init__.py
+โ โโโ mock_items.json # ํ
์คํธ ๋ฐ์ดํฐ
+โโโ tests/
+โโโ scripts/
+โโโ requirements.txt
+โโโ setup.py
+โโโ README.md
+```
+
+### `addroute`
+
+๊ธฐ์กด FastAPI ํ๋ก์ ํธ์ ์ API ๋ผ์ฐํธ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
+
+#### ๋ฌธ๋ฒ
+
+```console
+$ fastkit addroute PROJECT_NAME ROUTE_NAME [OPTIONS]
+```
+
+#### ์ธ์
+
+| ์ธ์ | ์ค๋ช
| ํ์ |
+|----------|-------------|----------|
+| `PROJECT_NAME` | ๊ธฐ์กด ํ๋ก์ ํธ์ ์ด๋ฆ | ์ |
+| `ROUTE_NAME` | ์ ๋ผ์ฐํธ์ ์ด๋ฆ (๋ณต์ํ ๊ถ์ฅ) | ์ |
+
+#### ์ต์
+
+| ์ต์
| ์ค๋ช
| ๊ธฐ๋ณธ๊ฐ |
+|--------|-------------|---------|
+| `--help` | ๋ช
๋ น ๋์๋ง ํ์ | - |
+
+#### ์์
+
+
+
+```console
+$ fastkit addroute my-api users
+ Adding New Route
+โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Project โ my-api โ
+โ Route Name โ users โ
+โ Target Directory โ ~/my-api โ
+โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+Do you want to add route 'users' to project 'my-api'? [Y/n]: y
+
+โจ Successfully added new route 'users' to project 'my-api'
+```
+
+
+
+#### ์์ฑ๋๋ ํ์ผ
+
+ํ๋ก์ ํธ์ ๋ค์ ํ์ผ๋ค์ ์์ฑํฉ๋๋ค:
+
+- `src/api/routes/users.py` - ๋ผ์ฐํธ ํธ๋ค๋ฌ
+- `src/crud/users.py` - CRUD ์์
+- `src/schemas/users.py` - Pydantic ์คํค๋ง
+
+๋ํ `src/api/api.py` ๊ฐ ๊ฐฑ์ ๋์ด ์ ๋ผ์ฐํฐ๊ฐ ํฌํจ๋ฉ๋๋ค.
+
+#### ์์ฑ๋๋ ์๋ํฌ์ธํธ
+
+์ ์ฒด CRUD ์๋ํฌ์ธํธ๊ฐ ๋ง๋ค์ด์ง๋๋ค:
+
+| ๋ฉ์๋ | ์๋ํฌ์ธํธ | ์ค๋ช
|
+|--------|----------|-------------|
+| `GET` | `/api/v1/users/` | ๋ชจ๋ ์ฌ์ฉ์ ์กฐํ |
+| `POST` | `/api/v1/users/` | ์ ์ฌ์ฉ์ ์์ฑ |
+| `GET` | `/api/v1/users/{user_id}` | ํน์ ์ฌ์ฉ์ ์กฐํ |
+| `PUT` | `/api/v1/users/{user_id}` | ์ฌ์ฉ์ ๊ฐฑ์ |
+| `DELETE` | `/api/v1/users/{user_id}` | ์ฌ์ฉ์ ์ญ์ |
+
+### `startdemo`
+
+์ฌ์ ๊ตฌ์ถ๋ ํ
ํ๋ฆฟ์ผ๋ก๋ถํฐ FastAPI ํ๋ก์ ํธ๋ฅผ ์์ฑํฉ๋๋ค.
+
+#### ๋ฌธ๋ฒ
+
+```console
+$ fastkit startdemo [OPTIONS]
+```
+
+#### ์ต์
+
+| ์ต์
| ์ค๋ช
| ๊ธฐ๋ณธ๊ฐ |
+|--------|-------------|---------|
+| `--package-manager` | ์ฌ์ฉํ ํจํค์ง ๋งค๋์ (pip, uv, pdm, poetry) | uv |
+| `--help` | ๋ช
๋ น ๋์๋ง ํ์ | - |
+
+#### ๋ํํ ํ๋กฌํํธ
+
+`startdemo` ๋ช
๋ น์ ๋ค์์ ๋ฌป์ต๋๋ค:
+
+1. **ํ๋ก์ ํธ ์ด๋ฆ**: ์ ํ๋ก์ ํธ์ ๋๋ ํฐ๋ฆฌ ์ด๋ฆ
+2. **์์ฑ์ ์ด๋ฆ**: ํจํค์ง ์์ฑ์ ์ ๋ณด
+3. **์์ฑ์ ์ด๋ฉ์ผ**: ์ฐ๋ฝ ์ด๋ฉ์ผ
+4. **ํ๋ก์ ํธ ์ค๋ช
**: ์งง์ ์ค๋ช
+5. **ํจํค์ง ๋งค๋์ ์ ํ**: pip, uv, pdm, poetry ์ค ์ ํ (`--package-manager` ๋ก ์ง์ ํ ๊ฒฝ์ฐ๋ ์ ์ธ)
+
+#### ์ฌ์ฉ ๊ฐ๋ฅํ ํ
ํ๋ฆฟ
+
+| ํ
ํ๋ฆฟ | ์ค๋ช
| ๊ธฐ๋ฅ |
+|----------|-------------|----------|
+| `fastapi-default` | ๊ฐ๋จํ FastAPI ํ๋ก์ ํธ | ๊ธฐ๋ณธ CRUD, Mock ๋ฐ์ดํฐ |
+| `fastapi-async-crud` | ๋น๋๊ธฐ item ๊ด๋ฆฌ API | Async/await, ์ฑ๋ฅ |
+| `fastapi-custom-response` | ๋ง์ถคํ ์๋ต ์์คํ
| ๋ง์ถคํ ์๋ต, ํ์ด์ง๋ค์ด์
|
+| `fastapi-dockerized` | Docker ๊ธฐ๋ฐ FastAPI API | Docker, ํ๋ก๋์
์ค๋น |
+| `fastapi-psql-orm` | PostgreSQL์ฉ FastAPI API | PostgreSQL, SQLAlchemy, Alembic |
+| `fastapi-empty` | ์ต์ ๊ตฌ์ฑ FastAPI ํ๋ก์ ํธ | ์ต์ ์ค์ ๋ง ํฌํจ |
+
+#### ์์
+
+
+
+```console
+$ fastkit startdemo fastapi-psql-orm
+Enter the project name: my-blog
+Enter the author name: Jane Smith
+Enter the author email: jane@example.com
+Enter the project description: Blog API with PostgreSQL
+
+Select package manager (pip, uv, pdm, poetry) [uv]: poetry
+Do you want to proceed with project creation? [y/N]: y
+
+โจ FastAPI project 'my-blog' from 'fastapi-psql-orm' has been created!
+```
+
+
+
+### `runserver`
+
+FastAPI ๊ฐ๋ฐ ์๋ฒ๋ฅผ ์์ํฉ๋๋ค.
+
+#### ๋ฌธ๋ฒ
+
+```console
+$ fastkit runserver [OPTIONS]
+```
+
+#### ์ต์
+
+| ์ต์
| ๋จ์ถ | ์ค๋ช
| ๊ธฐ๋ณธ๊ฐ |
+|--------|-------|-------------|---------|
+| `--host` | `-h` | ๋ฐ์ธ๋ํ ํธ์คํธ | `127.0.0.1` |
+| `--port` | `-p` | ๋ฐ์ธ๋ํ ํฌํธ | `8000` |
+| `--reload` | `-r` | ์๋ ๋ฆฌ๋ก๋ ํ์ฑํ | `True` |
+| `--workers` | `-w` | ์์ปค ์ | `1` |
+| `--help` | | ๋ช
๋ น ๋์๋ง ํ์ | - |
+
+#### ์์
+
+
+
+```console
+# ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ (๊ธฐ๋ณธ ์ค์ )
+$ fastkit runserver
+INFO: Uvicorn running on http://127.0.0.1:8000
+
+# ์ปค์คํ
ํธ์คํธ์ ํฌํธ
+$ fastkit runserver --host 0.0.0.0 --port 8080
+INFO: Uvicorn running on http://0.0.0.0:8080
+
+# ์๋ ๋ฆฌ๋ก๋ ๋นํ์ฑํ
+$ fastkit runserver --no-reload
+INFO: Uvicorn running on http://127.0.0.1:8000
+
+# ๋ค์ค ์์ปค (ํ๋ก๋์
)
+$ fastkit runserver --workers 4
+INFO: Uvicorn running on http://127.0.0.1:8000
+```
+
+
+
+#### ์๊ตฌ ์ฌํญ
+
+- FastAPI ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ ์์์ ์คํํด์ผ ํฉ๋๋ค
+- ํ๋ก์ ํธ์ FastAPI ์ฑ์ด ์๋ `src/main.py` ๊ฐ ์์ด์ผ ํฉ๋๋ค
+- ๊ฐ์ ํ๊ฒฝ์ด ํ์ฑํ๋์ด ์์ด์ผ ํฉ๋๋ค
+
+### `list-templates`
+
+์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ FastAPI ํ๋ก์ ํธ ํ
ํ๋ฆฟ์ ๋์ดํฉ๋๋ค.
+
+#### ๋ฌธ๋ฒ
+
+```console
+$ fastkit list-templates [OPTIONS]
+```
+
+#### ์ต์
+
+| ์ต์
| ์ค๋ช
| ๊ธฐ๋ณธ๊ฐ |
+|--------|-------------|---------|
+| `--help` | ๋ช
๋ น ๋์๋ง ํ์ | - |
+
+#### ์์
+
+
+
+```console
+$ fastkit list-templates
+ Available Templates
+โโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ fastapi-custom-response โ Async Item Management API with โ
+โ โ Custom Response System โ
+โ fastapi-dockerized โ Dockerized FastAPI Item โ
+โ โ Management API โ
+โ fastapi-empty โ No description โ
+โ fastapi-async-crud โ Async Item Management API Server โ
+โ fastapi-psql-orm โ Dockerized FastAPI Item โ
+โ โ Management API with PostgreSQL โ
+โ fastapi-default โ Simple FastAPI Project โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+
+
+## ํ๊ฒฝ ๋ณ์
+
+FastAPI-fastkit์ ๋ค์ ํ๊ฒฝ ๋ณ์๋ฅผ ์ธ์ํฉ๋๋ค:
+
+| ๋ณ์ | ์ค๋ช
| ๊ธฐ๋ณธ๊ฐ |
+|----------|-------------|---------|
+| `FASTKIT_CONFIG_DIR` | ์ค์ ๋๋ ํฐ๋ฆฌ | `~/.fastkit` |
+| `FASTKIT_TEMPLATES_DIR` | ์ปค์คํ
ํ
ํ๋ฆฟ ๋๋ ํฐ๋ฆฌ | ๋ด์ฅ ํ
ํ๋ฆฟ |
+| `FASTKIT_LOG_LEVEL` | ๋ก๊น
๋ ๋ฒจ | `INFO` |
+
+### ์์
+
+
+
+```console
+# ์ปค์คํ
์ค์ ๋๋ ํฐ๋ฆฌ
+$ export FASTKIT_CONFIG_DIR=~/my-fastkit-config
+$ fastkit init
+
+# ์ปค์คํ
ํ
ํ๋ฆฟ ๋๋ ํฐ๋ฆฌ
+$ export FASTKIT_TEMPLATES_DIR=~/my-templates
+$ fastkit list-templates
+
+# ๋๋ฒ๊ทธ ๋ก๊น
+$ export FASTKIT_LOG_LEVEL=DEBUG
+$ fastkit init
+```
+
+
+
+## ์ค์ ํ์ผ
+
+FastAPI-fastkit์ ๊ธฐ๋ณธ ์ค์ ๊ฐ์ ์ํ ์ค์ ํ์ผ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
+
+### ์ค์ ํ์ผ ์์น
+
+1. `$FASTKIT_CONFIG_DIR/config.yaml` (`FASTKIT_CONFIG_DIR` ๊ฐ ์ค์ ๋ ๊ฒฝ์ฐ)
+2. `~/.fastkit/config.yaml` (๊ธฐ๋ณธ๊ฐ)
+3. `./fastkit.yaml` (ํ๋ก์ ํธ๋ณ)
+
+### ์ค์ ํ์ผ ํ์
+
+```yaml
+# ~/.fastkit/config.yaml
+default:
+ author:
+ name: "Your Name"
+ email: "your.email@example.com"
+
+ project:
+ stack: "standard"
+ create_venv: true
+ install_deps: true
+
+ server:
+ host: "127.0.0.1"
+ port: 8000
+ reload: true
+
+templates:
+ custom_dir: "~/my-templates"
+
+logging:
+ level: "INFO"
+ file: "~/.fastkit/logs/fastkit.log"
+```
+
+## ์์ฃผ ์ฐ๋ ์ํฌํ๋ก
+
+### 1. ์ ํ๋ก์ ํธ ์์ฑ
+
+
+
+```console
+# ์ ํ๋ก์ ํธ ์์ฑ
+$ fastkit init
+# ํ๋กฌํํธ๋ฅผ ๋ฐ๋ผ ์งํ...
+
+# ํ๋ก์ ํธ๋ก ์ด๋
+$ cd my-awesome-api
+
+# ๊ฐ์ ํ๊ฒฝ ํ์ฑํ
+$ source .venv/bin/activate
+
+# ๊ฐ๋ฐ ์๋ฒ ์์
+$ fastkit runserver
+```
+
+
+
+### 2. ๊ธฐ์กด ํ๋ก์ ํธ์ ๊ธฐ๋ฅ ์ถ๊ฐ
+
+
+
+```console
+# ์ฌ๋ฌ ๋ผ์ฐํธ ์ถ๊ฐ
+$ fastkit addroute my-api users
+$ fastkit addroute my-api products
+$ fastkit addroute my-api orders
+
+# API ํ
์คํธ
+$ fastkit runserver
+# http://127.0.0.1:8000/docs ์ ์
+```
+
+
+
+### 3. ๋ณต์กํ ํ๋ก์ ํธ์ ํ
ํ๋ฆฟ ์ฌ์ฉ
+
+
+
+```console
+# ์ฌ์ฉ ๊ฐ๋ฅํ ํ
ํ๋ฆฟ ๋ณด๊ธฐ
+$ fastkit list-templates
+
+# ํ
ํ๋ฆฟ์ผ๋ก๋ถํฐ ์์ฑ
+$ fastkit startdemo
+# ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๋ก์ ํธ๋ผ๋ฉด fastapi-psql-orm ์ ํ
+
+# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ (PostgreSQL ํ
ํ๋ฆฟ์ฉ)
+$ cd my-project
+$ docker-compose up -d postgres
+$ source .venv/bin/activate
+$ alembic upgrade head
+$ fastkit runserver
+```
+
+
+
+## ๋ฌธ์ ํด๊ฒฐ
+
+### ๋ช
๋ น์ ์ฐพ์ ์ ์์ ๋
+
+`fastkit` ๋ช
๋ น์ ์ฐพ์ ์ ์๋ค๋ฉด:
+
+1. **์ค์น ํ์ธ:**
+
+ ```console
+ $ pip show fastapi-fastkit
+ ```
+
+
+2. **ํ์ํ๋ฉด ์ฌ์ค์น:**
+
+ ```console
+ $ pip uninstall fastapi-fastkit
+ $ pip install fastapi-fastkit
+ ```
+
+
+3. **PATH ํ์ธ:**
+
+ ```console
+ $ which fastkit
+ ```
+
+
+### ๊ฐ์ ํ๊ฒฝ ๋ฌธ์
+
+๊ฐ์ ํ๊ฒฝ ์์ฑ์ ์คํจํ๋ค๋ฉด:
+
+1. **Python ๋ฒ์ ํ์ธ:**
+
+ ```console
+ $ python --version # 3.12+ ์ด์ด์ผ ํฉ๋๋ค
+ ```
+
+
+2. **venv ๋ชจ๋ ํ์ธ:**
+
+ ```console
+ $ python -m venv --help
+ ```
+
+
+3. **์๋ ๊ฐ์ ํ๊ฒฝ ์์ฑ:**
+
+ ```console
+ $ python -m venv .venv
+ $ source .venv/bin/activate
+ $ pip install -r requirements.txt
+ ```
+
+
+### ์๋ฒ๊ฐ ์์๋์ง ์์ ๋
+
+`fastkit runserver` ๊ฐ ์คํจํ๋ค๋ฉด:
+
+1. **ํ๋ก์ ํธ ๋๋ ํฐ๋ฆฌ์ ์๋์ง ํ์ธํ์ธ์**
+2. **`src/main.py` ๊ฐ ์๋์ง ๊ฒ์ฆํ์ธ์**
+3. **๊ฐ์ ํ๊ฒฝ์ ํ์ฑํํ์ธ์:**
+
+ ```console
+ $ source .venv/bin/activate
+ ```
+
+
+4. **๋ฌธ๋ฒ ์ค๋ฅ ํ์ธ:**
+
+ ```console
+ $ python -c "from src.main import app"
+ ```
+
+
+### ํฌํธ๊ฐ ์ด๋ฏธ ์ฌ์ฉ ์ค์ผ ๋
+
+8000 ๋ฒ ํฌํธ๊ฐ ์ฌ์ฉ ์ค์ด๋ผ๋ฉด:
+
+
+
+```console
+# ๋ค๋ฅธ ํฌํธ ์ฌ์ฉ
+$ fastkit runserver --port 8080
+
+# ๋๋ ๊ธฐ์กด ํ๋ก์ธ์ค ์ข
๋ฃ
+$ lsof -ti:8000 | xargs kill -9
+```
+
+
+
+## ๊ณ ๊ธ ์ฌ์ฉ๋ฒ
+
+### ์ปค์คํ
ํ
ํ๋ฆฟ
+
+๋ค์๊ณผ ๊ฐ์ด ์ปค์คํ
ํ
ํ๋ฆฟ์ ๋ง๋ค ์ ์์ต๋๋ค:
+
+1. **ํ
ํ๋ฆฟ ๋๋ ํฐ๋ฆฌ ์์ฑ:**
+ ```
+ my-template/
+ โโโ src/
+ โ โโโ main.py-tpl
+ โโโ requirements.txt-tpl
+ โโโ setup.py-tpl
+ ```
+
+2. **ํ๊ฒฝ ๋ณ์ ์ค์ :**
+
+ ```console
+ $ export FASTKIT_TEMPLATES_DIR=~/my-templates
+ ```
+
+
+3. **์ปค์คํ
ํ
ํ๋ฆฟ ์ฌ์ฉ:**
+
+ ```console
+ $ fastkit startdemo
+ # ์ปค์คํ
ํ
ํ๋ฆฟ์ด ๋ชฉ๋ก์ ๋ํ๋ฉ๋๋ค
+ ```
+
+
+### ์คํฌ๋ฆฝํธ์์ FastAPI-fastkit ํ์ฉํ๊ธฐ
+
+FastAPI-fastkit์ ์คํฌ๋ฆฝํธ ์์์ ํ์ฉํ ์๋ ์์ต๋๋ค:
+
+```bash
+#!/bin/bash
+# create-microservices.sh
+
+for service in users products orders; do
+ echo "Creating $service service..."
+ fastkit init <
+
+```console
+$ fastkit init --package-manager uv
+# UV ์ค์ ์ด ์ ์ฉ๋ pyproject.toml ์์ฑ
+```
+
+