diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d07939e..83f43db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: rev: v6.0.0 hooks: - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] - id: end-of-file-fixer - id: check-yaml - id: check-toml diff --git a/docs/en/contributing/development-setup.md b/docs/en/contributing/development-setup.md index 60777d7..3f6c30b 100644 --- a/docs/en/contributing/development-setup.md +++ b/docs/en/contributing/development-setup.md @@ -31,6 +31,7 @@ Installing pre-commit hooks... This single command: + - Installs the package in editable mode with dev dependencies - Sets up pre-commit hooks - Configures development tools diff --git a/docs/en/contributing/template-creation-guide.md b/docs/en/contributing/template-creation-guide.md index 9a0b1b4..c6ee7ef 100644 --- a/docs/en/contributing/template-creation-guide.md +++ b/docs/en/contributing/template-creation-guide.md @@ -30,6 +30,7 @@ fastapi-{purpose}-{stack} ``` Examples: + - `fastapi-microservice` (Microservice template) - `fastapi-graphql` (GraphQL integration template) - `fastapi-auth-jwt` (JWT authentication template) @@ -380,9 +381,9 @@ The inspector automatically validates the following items: #### ✅ Dependencies Validation - [ ] `fastapi` is declared in at least one of: - - [ ] `pyproject.toml-tpl` under `[project].dependencies` (preferred) - - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` under `install_requires` + - [ ] `pyproject.toml-tpl` under `[project].dependencies` (preferred) + - [ ] `requirements.txt-tpl` + - [ ] `setup.py-tpl` under `install_requires` #### ✅ FastAPI Implementation Validation @@ -400,6 +401,7 @@ The inspector automatically validates the following items: FastAPI-fastkit includes **automated template testing** that runs comprehensive tests for all templates: **Test Coverage:** + - ✅ Template creation process - ✅ Project metadata injection - ✅ Virtual environment setup @@ -425,12 +427,14 @@ New templates are **automatically discovered** and tested without manual configu 4. ✅ **Comprehensive Validation**: Structure, metadata, and functionality checks **What This Means for You:** + - 🚀 **No Additional Test Files at `FastAPI-fastkit`'s main source testcases**: Your template is tested automatically - ⚡ **Faster Development**: Focus on template content, not test setup - 🛡️ **Quality Assurance**: Consistent testing across all templates - 🔄 **CI/CD Integration**: Automatic testing in pull requests **Manual Testing Still Required:** + - 🧪 **Template-specific functionality**: Business logic and custom features - 🔧 **Integration testing**: External services and complex workflows - 📱 **End-to-end scenarios**: Complete user workflows @@ -541,8 +545,8 @@ Closes #issue-number ### Review Process 1. **Automated Validation**: GitHub Actions automatically validates the template - - **Template PR Inspection**: Runs `inspect-changed-templates.py` on PRs modifying templates - - **Weekly Inspection**: Full template validation every Wednesday + - **Template PR Inspection**: Runs `inspect-changed-templates.py` on PRs modifying templates + - **Weekly Inspection**: Full template validation every Wednesday 2. **Code Review**: Maintainers and community review the code 3. **Testing**: Template is tested in various environments 4. **Documentation Review**: Review documentation accuracy and completeness diff --git a/docs/en/contributing/translation-guide.md b/docs/en/contributing/translation-guide.md index d80fcdd..87defb7 100644 --- a/docs/en/contributing/translation-guide.md +++ b/docs/en/contributing/translation-guide.md @@ -18,6 +18,12 @@ The user-facing companion to this policy is the lists each locale's actual completeness and how MkDocs renders pages that haven't been translated yet (TL;DR: they fall back to English). +The repository-root `CHANGELOG.md` also remains in English as the +canonical release history. If a locale exposes a `changelog.md` page, +that page should link to or include the canonical English changelog +rather than maintain a separate translated changelog unless project +policy changes later. + When you contribute a translation, also update the status page's table so users can tell what's available without guessing from the language switcher. @@ -213,6 +219,14 @@ The script will create a Pull Request with the translations. Review the PR: 3. Ensure code examples remain unchanged 4. Check for language-specific issues +### Changelog policy + +- Keep the repository-root `CHANGELOG.md` in English. +- Do not open translation PRs whose goal is to rewrite release history + into another language inside the root changelog. +- If a locale needs a changelog page, treat `docs//changelog.md` + as a wrapper or entry point for the canonical English changelog. + ### 4. Approve and Merge (for maintainers) Once the translation is verified: diff --git a/docs/en/index.md b/docs/en/index.md index f1f75b1..853a309 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -345,6 +345,7 @@ Installing dependencies... The interactive mode provides: + - **Architecture preset selection** (`minimal` / `single-module` / `classic-layered` / `domain-starter`) that picks the right base template and project layout - **Guided selection** for databases, authentication, background tasks, caching, monitoring, and more - **Auto-generated code** for selected features — varies by preset (regenerated `main.py` for `minimal` / `single-module`; preserve template-shipped `main.py` and overlay config modules for `classic-layered` / `domain-starter`) @@ -362,7 +363,7 @@ Add a new route endpoint to your FastAPI project with:
```console -$ fastkit addroute my-awesome-project user +$ fastkit addroute user my-awesome-project Adding New Route ┌──────────────────┬──────────────────────────────────────────┐ │ Project │ my-awesome-project │ @@ -500,6 +501,7 @@ Learn FastAPI development through practical use cases with our pre-built templat - **[Integrating with MCP](tutorial/mcp-integration.md)** - Create an API server integrated with AI models using the `fastapi-mcp` template Each tutorial provides: + - ✅ **Practical Examples** - Code you can use directly in real projects - ✅ **Step-by-Step Guides** - Detailed explanations for beginners to follow easily - ✅ **Best Practices** - Industry-standard patterns and security considerations diff --git a/docs/en/reference/template-quality-assurance.md b/docs/en/reference/template-quality-assurance.md index 9bc051a..fec2db7 100644 --- a/docs/en/reference/template-quality-assurance.md +++ b/docs/en/reference/template-quality-assurance.md @@ -101,17 +101,20 @@ The automated testing system runs in **CI/CD pipelines**: ### Benefits for Contributors **Zero Configuration Testing:** + - 🚀 Add new template → automatic testing - ⚡ No manual test file creation required - 🛡️ Consistent quality standards **Comprehensive Coverage:** + - 🔍 End-to-end project creation testing - 📦 Multi package manager validation - 🏗️ Complete dependency resolution testing - ✅ Real-world usage simulation **Developer Experience:** + - 🎯 **Focus on Template Content**: Testing is automatic - 🔄 **Immediate Feedback**: Fast test execution - 📊 **Clear Results**: Detailed test reporting @@ -190,8 +193,8 @@ For a template to pass inspection, it must meet these requirements: - Python files must use `.py-tpl` extension - Must include a `tests/` directory and a `README.md-tpl` file - Must include **at least one** metadata file: - - `pyproject.toml-tpl` (preferred, PEP 621), or - - `setup.py-tpl` (legacy, still accepted) + - `pyproject.toml-tpl` (preferred, PEP 621), or + - `setup.py-tpl` (legacy, still accepted) - `requirements.txt-tpl` is optional when `pyproject.toml-tpl` declares `[project].dependencies` diff --git a/docs/en/reference/translation-status.md b/docs/en/reference/translation-status.md index e5b044d..2611b38 100644 --- a/docs/en/reference/translation-status.md +++ b/docs/en/reference/translation-status.md @@ -19,6 +19,11 @@ The English files live under [`docs/en/`](https://github.com/bnbong/FastAPI-fast Every other locale (`docs/ko/`, `docs/ja/`, ...) is a translation target. +The repository-root `CHANGELOG.md` is part of that English source of +truth. Locale-specific `changelog.md` pages may exist as wrappers or +entry points, but they intentionally reuse the canonical English +release history instead of maintaining translated copies. + ## Per-locale completeness The numbers below count Markdown pages in each locale's directory tree @@ -29,14 +34,14 @@ next section explains). | Locale | Status | Markdown pages | Notes | |---|---|---:|---| | 🇬🇧 English (`en`) | ✅ Source of truth | 26 / 26 | Authoritative. | -| 🇰🇷 Korean (`ko`) | 🟡 Partial | 2 / 26 | `index.md`, `changelog.md`. Other pages fall back to English. | +| 🇰🇷 Korean (`ko`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/ko/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇯🇵 Japanese (`ja`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇨🇳 Chinese (`zh`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇪🇸 Spanish (`es`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇫🇷 French (`fr`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇩🇪 German (`de`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | -*Snapshot verified 2026-05-06.* These counts are maintained by hand; +*Snapshot verified 2026-05-07; ko row recounted for the current branch after Phase 3 (contributing + reference) landed. Korean now has all locale pages present, while `docs/ko/changelog.md` intentionally points to the canonical English changelog.* These counts are maintained by hand; to recount the current state from the repo root, run: ```console @@ -88,19 +93,38 @@ underlying content actually is. ## How to help -If you'd like to translate a page or fix an existing translation: +The current rollout is **one tracking issue per locale**, with the work +broken into **phases** — for example `ko` is being landed across +Phase 1 (top-level + core user-guide), Phase 2 (remaining user-guide + +all tutorials), Phase 3 (contributing + reference). Each phase ships +as its own PR so reviewers can sign off on a coherent slice without +waiting for the entire locale to be finished. + +If you'd like to contribute: 1. Read the [Translation Guide](../contributing/translation-guide.md) for the workflow, tooling, and style conventions. -2. Translate one page at a time — partial translations are welcome and - merged incrementally. -3. Open a PR adding the new file(s) under `docs//`. - Keep filenames identical to the English source so MkDocs picks them - up automatically. -4. Update this page's table to reflect the new completeness count +2. **Check or open the locale tracking issue first.** If a locale + already has an open tracking issue, claim a phase (or a specific + page within a phase) there so the work doesn't double up. If no + tracking issue exists for the locale you want to work on, open one + that lists which pages belong to which phase, then start with + Phase 1. +3. **One PR per phase is the preferred shape.** Smaller "fix this + single page" PRs are still welcome — especially for correcting an + out-of-sync translation — but for net-new locale work, batching by + phase keeps glossary decisions and cross-link wording consistent + across the slice. +4. Open the PR adding files under `docs//`. Keep + filenames identical to the English source so MkDocs picks them up + automatically. +5. Treat localized changelog pages as wrappers around the canonical + English `CHANGELOG.md` unless project policy explicitly changes. +6. Update this page's table to reflect the new completeness count (use the recount snippet at the top of this page) and bump the "Snapshot verified" date so reviewers can see when it was last - reconciled. + reconciled. Note in the "Notes" column which phase has landed if + the locale is still partial. Bug reports about translated pages going out of sync with the English source are welcome — please link the English page and the translated diff --git a/docs/en/tutorial/first-project.md b/docs/en/tutorial/first-project.md index f245c42..1a1b4ad 100644 --- a/docs/en/tutorial/first-project.md +++ b/docs/en/tutorial/first-project.md @@ -114,13 +114,13 @@ Let's add the main resources for our blog API:
```console -$ fastkit addroute blog-api users +$ fastkit addroute users blog-api ✨ Successfully added new route 'users' to project 'blog-api' -$ fastkit addroute blog-api posts +$ fastkit addroute posts blog-api ✨ Successfully added new route 'posts' to project 'blog-api' -$ fastkit addroute blog-api comments +$ fastkit addroute comments blog-api ✨ Successfully added new route 'comments' to project 'blog-api' ``` @@ -985,6 +985,7 @@ Visit [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) to see your compl - **Items**: Original example endpoints The documentation shows: + - All available endpoints - Request/response schemas - Data validation rules @@ -1172,68 +1173,68 @@ Congratulations! You've successfully built a complete blog API with: ### ✅ Features Implemented - **User Management** - - User registration with validation - - User authentication (login) - - Profile management - - Duplicate prevention + - User registration with validation + - User authentication (login) + - Profile management + - Duplicate prevention - **Blog Posts** - - Create, read, update, delete posts - - Author-based filtering - - Search functionality - - Publish/draft status + - Create, read, update, delete posts + - Author-based filtering + - Search functionality + - Publish/draft status - **Comment System** - - Add comments to posts - - View comments by post or author - - Comment management + - Add comments to posts + - View comments by post or author + - Comment management - **Data Validation** - - Email validation - - Password requirements - - Content length limits - - Required field validation + - Email validation + - Password requirements + - Content length limits + - Required field validation - **Error Handling** - - Proper HTTP status codes - - Descriptive error messages - - Input validation errors + - Proper HTTP status codes + - Descriptive error messages + - Input validation errors - **API Documentation** - - Automatic OpenAPI generation - - Interactive testing interface - - Request/response schemas + - Automatic OpenAPI generation + - Interactive testing interface + - Request/response schemas - **Testing** - - Comprehensive test coverage - - Unit tests for all endpoints - - Edge case testing + - Comprehensive test coverage + - Unit tests for all endpoints + - Edge case testing ## Next Steps ### Potential Enhancements 1. **Real Authentication** - - Implement JWT tokens - - Add password hashing with bcrypt - - Role-based permissions + - Implement JWT tokens + - Add password hashing with bcrypt + - Role-based permissions 2. **Database Integration** - - Use PostgreSQL or MySQL - - Implement proper database models - - Add database migrations + - Use PostgreSQL or MySQL + - Implement proper database models + - Add database migrations 3. **Advanced Features** - - File uploads for images - - Email notifications - - Post categories/tags - - Like/dislike system + - File uploads for images + - Email notifications + - Post categories/tags + - Like/dislike system 4. **Production Readiness** - - Add logging - - Implement caching - - Add rate limiting - - Environment configuration + - Add logging + - Implement caching + - Add rate limiting + - Environment configuration ### Continue Learning diff --git a/docs/en/tutorial/getting-started.md b/docs/en/tutorial/getting-started.md index 1fb41c3..1b147dd 100644 --- a/docs/en/tutorial/getting-started.md +++ b/docs/en/tutorial/getting-started.md @@ -215,6 +215,7 @@ Visit the automatically generated API documentation: - **ReDoc**: [http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc) The Swagger UI is particularly useful - you can: + - See all available endpoints - Test endpoints directly in your browser - View request/response schemas @@ -341,7 +342,7 @@ Let's add a new API route to practice what you've learned:
```console -$ fastkit addroute my-first-api users +$ fastkit addroute users my-first-api Adding New Route ┌──────────────────┬──────────────────────────────────────────┐ │ Project │ my-first-api │ @@ -357,6 +358,7 @@ Do you want to add route 'users' to project 'my-first-api'? [Y/n]: y
The server will automatically restart and now you have new endpoints: + - `GET /api/v1/users/` - Get all users - `POST /api/v1/users/` - Create a new user - `GET /api/v1/users/{user_id}` - Get a specific user @@ -529,11 +531,13 @@ Try these challenges: ### Common Issues and Solutions **Server won't start:** + - Check you're in the project directory - Ensure virtual environment is activated - Verify no syntax errors in your code **Import errors:** + - Make sure all `__init__.py` files exist - Check your import paths are correct - Verify you're using the virtual environment diff --git a/docs/en/user-guide/adding-routes.md b/docs/en/user-guide/adding-routes.md index 921cee6..ddb521e 100644 --- a/docs/en/user-guide/adding-routes.md +++ b/docs/en/user-guide/adding-routes.md @@ -11,7 +11,7 @@ FastAPI-fastkit's `addroute` command makes it easy to add new routes:
```console -$ fastkit addroute my-awesome-api users +$ fastkit addroute users my-awesome-api Adding New Route ┌──────────────────┬──────────────────────────────────────────┐ │ Project │ my-awesome-api │ @@ -383,10 +383,10 @@ You can add multiple routes to build a complete API:
```console -# Add more resource routes -$ fastkit addroute my-awesome-api products -$ fastkit addroute my-awesome-api orders -$ fastkit addroute my-awesome-api categories +# Add more resource routes (route name first, project dir second) +$ fastkit addroute products my-awesome-api +$ fastkit addroute orders my-awesome-api +$ fastkit addroute categories my-awesome-api # Each creates the full CRUD structure ``` @@ -394,6 +394,7 @@ $ fastkit addroute my-awesome-api categories
This creates a comprehensive API with: + - `/api/v1/users/` - User management - `/api/v1/products/` - Product catalog - `/api/v1/orders/` - Order processing @@ -468,6 +469,7 @@ def create_user( ### 1. Consistent Naming Follow consistent naming conventions: + - **Route names**: Use plural nouns (`users`, `products`, `orders`) - **Schema names**: Use singular (`User`, `Product`, `Order`) - **CRUD classes**: End with `CRUD` (`UsersCRUD`, `ProductsCRUD`) diff --git a/docs/en/user-guide/cli-reference.md b/docs/en/user-guide/cli-reference.md index 370b981..7f68074 100644 --- a/docs/en/user-guide/cli-reference.md +++ b/docs/en/user-guide/cli-reference.md @@ -158,15 +158,15 @@ Add a new API route to an existing FastAPI project. #### Syntax ```console -$ fastkit addroute PROJECT_NAME ROUTE_NAME [OPTIONS] +$ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] ``` #### Arguments | Argument | Description | Required | |----------|-------------|----------| -| `PROJECT_NAME` | Name of the existing project | Yes | | `ROUTE_NAME` | Name of the new route (plural recommended) | Yes | +| `PROJECT_DIR` | Project directory under your workspace (defaults to `.`, the current directory) | No | #### Options @@ -179,7 +179,8 @@ $ fastkit addroute PROJECT_NAME ROUTE_NAME [OPTIONS]
```console -$ fastkit addroute my-api users +$ cd my-api +$ fastkit addroute users Adding New Route ┌──────────────────┬──────────────────────────────────────────┐ │ Project │ my-api │ @@ -194,6 +195,16 @@ Do you want to add route 'users' to project 'my-api'? [Y/n]: y
+You can also target a project under your workspace by name without `cd`-ing into it: + +
+ +```console +$ fastkit addroute users my-api +``` + +
+ #### Generated Files Creates these files in the project: @@ -456,10 +467,10 @@ $ fastkit runserver
```console -# Add multiple routes -$ fastkit addroute my-api users -$ fastkit addroute my-api products -$ fastkit addroute my-api orders +# Add multiple routes (project name as second positional arg = workspace project) +$ fastkit addroute users my-api +$ fastkit addroute products my-api +$ fastkit addroute orders my-api # Test the API $ fastkit runserver @@ -631,7 +642,7 @@ y EOF cd "$service-service" - fastkit addroute "$service-service" "$service" + fastkit addroute "$service" cd .. done ``` diff --git a/docs/en/user-guide/creating-projects.md b/docs/en/user-guide/creating-projects.md index 05df6e9..0859362 100644 --- a/docs/en/user-guide/creating-projects.md +++ b/docs/en/user-guide/creating-projects.md @@ -177,6 +177,7 @@ Each package manager has its advantages: - 🛠️ **Reliable**: Deterministic resolution **Generated files:** + - `pyproject.toml` (PEP 621 format) - `uv.lock` (lockfile) @@ -198,6 +199,7 @@ uv run pytest # Run tests - 📊 **Analytics**: Dependency analysis tools **Generated files:** + - `pyproject.toml` (PEP 621 format) - `pdm.lock` (lockfile) @@ -219,6 +221,7 @@ pdm run pytest # Run tests - 🏗️ **Complete**: Full project lifecycle management **Generated files:** + - `pyproject.toml` (Poetry format) - `poetry.lock` (lockfile) @@ -240,6 +243,7 @@ poetry run pytest # Run tests - 🔧 **Simple**: Straightforward workflow **Generated files:** + - `requirements.txt` **Usage after creation:** diff --git a/docs/en/user-guide/quick-start.md b/docs/en/user-guide/quick-start.md index 3c1ebe2..fbe882a 100644 --- a/docs/en/user-guide/quick-start.md +++ b/docs/en/user-guide/quick-start.md @@ -135,6 +135,7 @@ You'll see: Visit [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) This is the automatically generated **Swagger UI** documentation where you can: + - See all your API endpoints - Test endpoints directly in the browser - View request/response schemas @@ -152,7 +153,7 @@ Let's add a new API route to your project:
```console -$ fastkit addroute my-first-app users +$ fastkit addroute users my-first-app Adding New Route ┌──────────────────┬──────────────────────────────────────────┐ │ Project │ my-first-app │ @@ -357,9 +358,9 @@ $ fastkit list-templates # Create a project from a template $ fastkit startdemo -# Add more routes -$ fastkit addroute my-first-app products -$ fastkit addroute my-first-app orders +# Add more routes (route name first, project dir second) +$ fastkit addroute products my-first-app +$ fastkit addroute orders my-first-app ```
diff --git a/docs/en/user-guide/using-templates.md b/docs/en/user-guide/using-templates.md index 98de5cd..16b01dd 100644 --- a/docs/en/user-guide/using-templates.md +++ b/docs/en/user-guide/using-templates.md @@ -381,9 +381,9 @@ After creating a project from a template, you can customize it:
```console -$ fastkit addroute my-blog-api posts -$ fastkit addroute my-blog-api users -$ fastkit addroute my-blog-api comments +$ fastkit addroute posts my-blog-api +$ fastkit addroute users my-blog-api +$ fastkit addroute comments my-blog-api ```
@@ -492,7 +492,7 @@ $ fastkit runserver $ python -m pytest # Add new features -$ fastkit addroute your-project new-resource +$ fastkit addroute new-resource your-project ```
diff --git a/docs/js/custom.js b/docs/js/custom.js index ef64c61..0a02f4e 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -67,15 +67,23 @@ function setupTermynal() { saveBuffer(); const promptStart = line.indexOf(promptLiteralStart); if (promptStart === -1) { - console.error("Custom prompt found but no end delimiter", line) + const value = "💬 " + line.replace(customPromptLiteralStart, "").trimEnd(); + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0 + }); + } else { + const prompt = line + .slice(0, promptStart) + .replace(customPromptLiteralStart, ""); + const value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt + }); } - const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") - let value = line.slice(promptStart + promptLiteralStart.length); - useLines.push({ - type: "input", - value: value, - prompt: prompt - }); } else { buffer.push(line); } diff --git a/docs/ko/contributing/code-guidelines.md b/docs/ko/contributing/code-guidelines.md new file mode 100644 index 0000000..a8872a8 --- /dev/null +++ b/docs/ko/contributing/code-guidelines.md @@ -0,0 +1,748 @@ +# 코드 가이드라인 + +FastAPI-fastkit에 기여하는 모든 분을 위한 종합 코딩 표준과 모범 사례입니다. + +## 개요 + +이 가이드라인은 FastAPI-fastkit 프로젝트 전반에서 코드 품질과 일관성, 유지보수성을 지키기 위해 마련됐습니다. 이 기준을 따르면 읽기 쉽고, 유지보수하기 편하며, 확장에도 유리한 코드베이스를 만드는 데 도움이 됩니다. + +## Python 코드 스타일 + +### PEP 8 준수 + +[PEP 8](https://www.python.org/dev/peps/pep-0008/)을 기본으로 하되, 아래 규칙도 함께 따라 주세요: + +- **줄 길이**: 88자 (Black 기본값) +- **들여쓰기**: 공백 4칸 (탭 사용 금지) +- **후행 쉼표(trailing comma)**: 여러 줄 구조에서는 필수 +- **문자열 따옴표**: 큰따옴표 권장 + +### 코드 포매팅 + +자동 코드 포매터로 **Black**을 사용합니다: + +```python +# Good ✅ +def create_project( + name: str, + template: str, + options: Dict[str, Any], +) -> ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name, template=template) + +# Bad ❌ +def create_project(name: str, template: str, options: Dict[str,Any])->ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name,template=template) +``` + +### Import 정리 + +import 정리는 **isort**로 처리합니다: + +```python +# Standard library imports +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Union + +# Third-party imports +import click +import pydantic +from fastapi import FastAPI + +# Local imports +from fastapi_fastkit.commands import BaseCommand +from fastapi_fastkit.utils import validation +from fastapi_fastkit.templates.manager import TemplateManager +``` + +## 타입 힌트 + +### 타입 힌트 필수 + +모든 공개 함수와 메서드에는 타입 힌트가 들어가야 합니다: + +```python +# Good ✅ +def validate_project_name(name: str) -> bool: + """Validate project name format.""" + return name.isidentifier() and not name.startswith('_') + +def create_files( + files: List[Path], + template_data: Dict[str, Any] +) -> List[Path]: + """Create files from template data.""" + created_files = [] + for file_path in files: + # Implementation... + created_files.append(file_path) + return created_files + +# Bad ❌ +def validate_project_name(name): + return name.isidentifier() and not name.startswith('_') +``` + +### 복잡한 타입 어노테이션 + +복잡한 구조에는 적절한 타입 어노테이션을 사용하세요: + +```python +from typing import Dict, List, Optional, Union, Tuple, Any +from pathlib import Path + +# 복잡한 타입에 대한 타입 별칭 +ProjectConfig = Dict[str, Union[str, bool, List[str]]] +FileMapping = Dict[Path, str] +ValidationResult = Tuple[bool, Optional[str]] + +def process_template( + template_path: Path, + config: ProjectConfig, + output_dir: Optional[Path] = None, +) -> ValidationResult: + """Process template with configuration.""" + # Implementation... + return True, None +``` + +## 명명 규칙 + +### 변수와 함수 + +- 변수와 함수는 **snake_case** +- 의도를 설명하는 **서술적인 이름** +- 흔히 통용되는 약어가 아니라면 **약어 회피** + +```python +# Good ✅ +project_name = "my-api" +template_directory = Path("templates") +user_input_data = get_user_input() + +def validate_email_address(email: str) -> bool: + """Validate email address format.""" + return "@" in email and "." in email + +# Bad ❌ +proj_nm = "my-api" +temp_dir = Path("templates") +usr_data = get_input() + +def validate_email(e): + return "@" in e and "." in e +``` + +### 클래스 + +- 클래스명은 **PascalCase** +- **서술적이고 구체적인** 이름 + +```python +# Good ✅ +class SomeClass: + """Represents example class of FastAPI-fastkit.""" + pass + +class SomeClassValidationError(Exception): + """Raised when example class validation fails.""" + pass + +class UserInputHandler: + """Handles user input validation and processing.""" + pass + +# Bad ❌ +class Class: + pass + +class Error(Exception): + pass + +class Handler: + pass +``` + +### 상수 + +- 언더스코어로 구분된 **UPPER_CASE** +- **모듈 수준** 상수만 허용 + +```python +# Good ✅ +DEFAULT_TEMPLATE_NAME = "fastapi-default" +MAX_PROJECT_NAME_LENGTH = 50 +SUPPORTED_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +# Bad ❌ +default_template = "fastapi-default" +maxLength = 50 +versions = ["3.8", "3.9", "3.10", "3.11", "3.12"] +``` + +## 문서 표준 + +### Docstring + +모든 공개 API 에는 **Google 스타일 docstring** 을 사용하세요: + +```python +def create_project_structure( + project_name: str, + template_path: Path, + output_directory: Optional[Path] = None, + overwrite: bool = False, +) -> List[Path]: + """Create project structure from template. + + Creates a new FastAPI project structure by copying and processing + template files. Supports variable substitution and file customization. + + Args: + project_name: Name of the project to create. Must be a valid + Python identifier. + template_path: Path to the template directory containing + source files and configuration. + output_directory: Directory where project will be created. + Defaults to current working directory. + overwrite: Whether to overwrite existing files. If False, + raises error when files exist. + + Returns: + List of created file paths in order of creation. + + Raises: + ValueError: If project_name is invalid or empty. + FileExistsError: If output directory exists and overwrite is False. + TemplateNotFoundError: If template_path doesn't exist. + PermissionError: If insufficient permissions to create files. + + Example: + ```python + template_path = Path("templates/fastapi-default") + created_files = create_project_structure( + project_name="my-api", + template_path=template_path, + output_directory=Path("./projects"), + overwrite=False + ) + print(f"Created {len(created_files)} files") + ``` + """ + # Implementation here... + pass +``` + +### 주석 + +- **무엇이 아니라 왜** 를 설명 +- **꼭 필요할 때만 사용** — 코드는 자체로 설명적이어야 함 +- 코드가 바뀌면 **주석도 함께 갱신** + +```python +# Good ✅ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # 개발 모드에서는 실험적 패키지를 허용하기 위해 검증을 건너뜀 + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # 알려진 보안 취약점이 있는지 각 의존성을 확인 + for requirement in requirements: + if is_vulnerable_package(requirement): + return False + + return True + +# Bad ❌ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # dev 모드인지 확인 + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # requirements 를 순회 + for requirement in requirements: + # 취약한지 확인 + if is_vulnerable_package(requirement): + return False + + # true 반환 + return True +``` + +## 에러 처리 + +### 예외 처리 + +- 가능하면 **구체적인 예외를 잡기** +- **의미 있는 에러 메시지** 제공 +- 에러를 **적절히 로깅** + +```python +# Good ✅ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except FileNotFoundError: + raise TemplateNotFoundError( + f"Template configuration not found: {config_file}" + ) + except yaml.YAMLError as e: + raise TemplateConfigError( + f"Invalid YAML syntax in {config_file}: {e}" + ) + except PermissionError: + raise TemplateAccessError( + f"Permission denied reading {config_file}" + ) + +# Bad ❌ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except Exception as e: + raise Exception(f"Error loading config: {e}") +``` + +### 커스텀 예외 + +서로 다른 에러 조건에 대한 구체적인 예외를 정의하세요: + +```python +class FastKitError(Exception): + """Base exception for FastAPI-fastkit errors.""" + pass + +class ProjectCreationError(FastKitError): + """Raised when project creation fails.""" + pass + +class TemplateNotFoundError(FastKitError): + """Raised when template is not found.""" + pass + +class ValidationError(FastKitError): + """Raised when input validation fails.""" + + def __init__(self, message: str, field: str = None): + super().__init__(message) + self.field = field +``` + +## 테스트 표준 + +### 테스트 구조 + +테스트를 명확한 구조와 명명으로 정리하세요: + +```python +class TestProjectCreation: + """Test project creation functionality.""" + + def test_create_project_with_valid_name(self, tmp_path): + """Test project creation with valid project name.""" + project_name = "test-project" + result = create_project(project_name, template="minimal", output=tmp_path) + + assert result.success is True + assert (tmp_path / project_name).exists() + assert (tmp_path / project_name / "src" / "main.py").exists() + + def test_create_project_with_invalid_name(self): + """Test project creation fails with invalid name.""" + with pytest.raises(ValueError, match="Invalid project name"): + create_project("invalid-project-name!", template="minimal") + + def test_create_project_overwrites_existing(self, tmp_path): + """Test project creation overwrites existing directory when forced.""" + project_name = "existing-project" + project_dir = tmp_path / project_name + project_dir.mkdir() + + result = create_project( + project_name, + template="minimal", + output=tmp_path, + overwrite=True + ) + + assert result.success is True + assert project_dir.exists() +``` + +### 테스트 커버리지 + +- 새 코드는 **90% 이상의 커버리지** 목표 +- **엣지 케이스**와 에러 상황을 테스트 +- **외부 의존성을 모킹** + +```python +def test_template_download_with_network_error(mock_requests): + """Test template download handles network errors gracefully.""" + mock_requests.get.side_effect = requests.ConnectionError("Network unreachable") + + with pytest.raises(TemplateDownloadError, match="Network error"): + download_template("https://example.com/template.zip") + +def test_file_creation_with_permission_error(mock_open): + """Test file creation handles permission errors.""" + mock_open.side_effect = PermissionError("Permission denied") + + with pytest.raises(FileCreationError, match="Permission denied"): + create_file(Path("/restricted/file.py"), content="test") +``` + +## Import 가이드라인 + +### Import 정리 + +!!! note + + `isort` 포매터가 import 를 자동으로 정리해 주므로, `bash scripts/format.sh` 만 실행하면 import 정리는 손쉽게 끝납니다. + +1. **표준 라이브러리** import 먼저 +2. **서드파티** import 그 다음 +3. **로컬 애플리케이션** import 마지막 +4. 각 그룹 사이에 **빈 줄** 하나 + +```python +# Standard library +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional + +# Third-party +import click +import pydantic +import yaml +from fastapi import FastAPI + +# Local application +from fastapi_fastkit.commands.base import BaseCommand +from fastapi_fastkit.utils.validation import validate_project_name +from fastapi_fastkit.templates import TemplateManager +``` + +### Import 모범 사례 + +- **wildcard import 금지** (`from module import *`) +- 명확성을 위해 **절대 경로 import 사용** +- 항목을 많이 가져올 때는 **개별 항목이 아닌 모듈을 import** + +```python +# Good ✅ +from fastapi_fastkit.utils import validation, files, formatting + +# Good ✅ (적은 수의 항목을 가져올 때) +from fastapi_fastkit.utils.validation import validate_email, validate_project_name + +# Bad ❌ +from fastapi_fastkit.utils.validation import * + +# Bad ❌ (많은 항목을 가져올 때) +from fastapi_fastkit.utils.validation import ( + validate_email, validate_project_name, validate_template_name, + validate_dependencies, validate_python_version, validate_directory +) +``` + +## 보안 가이드라인 + +### 입력 검증 + +사용자 입력은 항상 검증하고 정화하세요: + +```python +def validate_project_name(name: str) -> str: + """Validate and sanitize project name.""" + if not name: + raise ValueError("Project name cannot be empty") + + if not name.isidentifier(): + raise ValueError("Project name must be a valid Python identifier") + + if name.startswith('_'): + raise ValueError("Project name cannot start with underscore") + + if len(name) > 50: + raise ValueError("Project name too long (max 50 characters)") + + # 위험한 문자를 제거해 정화 + sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', name) + + return sanitized +``` + +### 파일 작업 + +파일 경로와 작업은 신중히 다루세요: + +```python +def create_file_safely(file_path: Path, content: str, base_dir: Path) -> None: + """Create file safely within base directory.""" + # 디렉터리 traversal 공격을 방지하기 위해 resolve + resolved_path = file_path.resolve() + resolved_base = base_dir.resolve() + + # 파일이 base 디렉터리 안에 있는지 확인 + try: + resolved_path.relative_to(resolved_base) + except ValueError: + raise SecurityError(f"File path outside base directory: {file_path}") + + # 부모 디렉터리를 안전하게 생성 + resolved_path.parent.mkdir(parents=True, exist_ok=True) + + # 적절한 권한으로 파일 작성 + resolved_path.write_text(content, encoding='utf-8') + resolved_path.chmod(0o644) # 소유자는 읽기/쓰기, 그 외에는 읽기만 +``` + +## 성능 가이드라인 + +### 효율적인 코드 작성 + +- 큰 데이터셋에는 **제너레이터 사용** +- **조기 최적화 금지** +- **최적화 전에 프로파일링** + +```python +# Good ✅ — 메모리 효율을 위한 제너레이터 +def process_large_template(template_files: List[Path]) -> Iterator[ProcessedFile]: + """Process template files efficiently.""" + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + yield ProcessedFile(path=file_path, content=processed_content) + +# Bad ❌ — 모든 것을 메모리에 적재 +def process_large_template(template_files: List[Path]) -> List[ProcessedFile]: + """Process template files.""" + results = [] + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + results.append(ProcessedFile(path=file_path, content=processed_content)) + return results +``` + +### 캐싱 + +비싼 작업에는 캐싱을 사용하세요: + +```python +from functools import lru_cache + +@lru_cache(maxsize=128) +def get_template_metadata(template_path: Path) -> TemplateMetadata: + """Get template metadata with caching.""" + config_file = template_path / "template.yaml" + + if not config_file.exists(): + return TemplateMetadata(name=template_path.name) + + config = yaml.safe_load(config_file.read_text()) + return TemplateMetadata.from_config(config) +``` + +## Git 커밋 가이드라인 + +### 커밋 메시지 형식 + +conventional commit 형식을 사용하세요: + +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +### 커밋 타입 + +- **feat**: 새 기능 +- **fix**: 버그 수정 +- **docs**: 문서 변경 +- **style**: 코드 스타일 변경 (포매팅 등) +- **refactor**: 코드 리팩터링 +- **test**: 테스트 추가 또는 갱신 +- **chore**: 유지보수 작업 + +### 예시 + +```bash +# Good ✅ +feat(cli): add template validation command + +Add new command to validate template structure and configuration. +The command checks for required files, validates YAML syntax, +and ensures template follows conventions. + +Closes #123 + +# Good ✅ +fix(templates): handle missing dependency files gracefully + +When a template references a requirements file that doesn't exist, +show a clear error message instead of crashing. + +# Bad ❌ +update stuff + +# Bad ❌ +Fixed bug +``` + +## 코드 리뷰 가이드라인 + +### 작성자에게 + +코드 리뷰를 요청하기 전에: + +1. **모든 테스트 실행**하고 통과 확인 +2. **코드 커버리지 유지** 확인 +3. 필요 시 **문서 갱신** +4. **커밋 메시지** 컨벤션 준수 +5. PR은 **작고 목적이 분명하게** 유지 + +### 리뷰어에게 + +코드를 리뷰할 때: + +1. **동작 확인** — 의도한 대로 작동하는가? +2. **테스트 검토** — 엣지 케이스가 다뤄졌는가? +3. **문서 검증** — 명확하고 최신 상태인가? +4. **코드 스타일 점검** — 프로젝트 컨벤션을 따르는가? +5. **보안 고려** — 잠재적 취약점은 없는가? + +### 리뷰 체크리스트 + +- [ ] 코드가 스타일 가이드라인을 따름 +- [ ] 테스트가 종합적이고 통과함 +- [ ] 문서가 갱신됨 +- [ ] 보안 취약점 없음 +- [ ] 성능 측면이 고려됨 +- [ ] 에러 처리가 적절함 +- [ ] 커밋 메시지가 컨벤션을 따름 + +## 도구와 자동화 + +### Pre-commit 훅 + +표준을 강제하기 위해 pre-commit 훅을 사용합니다: + +```yaml +# .pre-commit-config.yaml +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + +- repo: local + hooks: + - id: format + name: format + entry: black --config pyproject.toml --check . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: isort-check + name: isort check + entry: isort --sp pyproject.toml --check-only --diff . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: isort-fix + name: isort fix + entry: isort --sp pyproject.toml . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: black-fix + name: black fix + entry: black --config pyproject.toml . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: mypy + name: mypy + entry: mypy --config-file pyproject.toml src + language: python + types: [python] + additional_dependencies: + - mypy>=1.12.0 + - rich>=13.9.2 + - click>=8.1.7 + - pyyaml>=6.0.0 + - types-PyYAML>=6.0.12 + pass_filenames: false + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate +``` + +!!! note + + Pre-commit 훅은 격리된 Python 환경 (`language: python`) 에서 실행됩니다. + +### IDE 설정 + +권장 VS Code 설정: + +```json +{ + "python.linting.enabled": true, + "python.linting.mypyEnabled": true, + "python.formatting.provider": "black", + "python.sortImports.path": "isort", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +} +``` + +## 다음 단계 + +이 가이드라인을 읽은 뒤에는 다음 순서로 진행해 보세요: + +1. [개발 환경 설정](development-setup.md)을 따라 **개발 환경 준비** +2. 익숙해질 수 있도록 **작은 기여부터 연습** +3. 불명확한 부분은 GitHub Discussions에서 **질문하기** +4. **기존 코드를 살펴보며** 가이드라인이 실제로 어떻게 적용됐는지 확인 + +!!! tip "빠른 참조" + - `make check-all` 로 코드가 모든 가이드라인을 따르는지 검증 + - 이슈를 조기에 잡기 위해 pre-commit 훅을 설정 + - 의문이 있으면 기존 코드 예시 참고 + - 코드 리뷰에서 도움이 필요하면 주저하지 말고 요청 + +이 가이드라인을 따르면 FastAPI-fastkit의 높은 코드 품질을 유지하면서, 모두가 더 편하게 협업할 수 있습니다! 🚀 diff --git a/docs/ko/contributing/development-setup.md b/docs/ko/contributing/development-setup.md new file mode 100644 index 0000000..eae05ff --- /dev/null +++ b/docs/ko/contributing/development-setup.md @@ -0,0 +1,817 @@ +# 개발 환경 설정 + +FastAPI-fastkit에 기여하기 위한 개발 환경 준비 가이드입니다. + +## 사전 요구 사항 + +시작하기 전에 다음을 갖춰 두세요: + +- **Python 3.12 이상** 설치 +- **Git** 설치 및 설정 완료 +- Python과 FastAPI에 대한 **기초 지식** +- **텍스트 에디터 또는 IDE** (VS Code, PyCharm 등) + +## Makefile로 빠르게 설정하기 + +FastAPI-fastkit은 개발 환경을 손쉽게 준비할 수 있도록 Makefile을 제공합니다: + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make install-dev +Setting up development environment... +Creating virtual environment... +Installing dependencies... +Installing pre-commit hooks... +✅ Development environment ready! +``` + +
+ +이 명령 하나로 다음 작업이 함께 이뤄집니다: + +- 개발 의존성과 함께 패키지를 `editable` 모드로 설치 +- pre-commit 훅 설정 +- 개발 도구 구성 + +!!! note + + 이 명령을 실행하기 전에 가상 환경을 만들어 활성화해 두세요. + +## 수동 설정 + +직접 설정하고 싶거나 사용 중인 환경에서 Makefile이 동작하지 않는다면: + +### 1. 저장소 clone + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +``` + +
+ +### 2. 가상 환경 생성 + +
+ +```console +$ python -m venv .venv +$ source .venv/bin/activate # Windows: .venv\Scripts\activate +``` + +
+ +### 3. 의존성 설치 + +
+ +```console +# 개발 의존성과 함께 editable 모드로 패키지 설치 +$ pip install -e ".[dev]" + +# 또는 requirements 파일로 설치 +$ pip install -r requirements.txt +$ pip install -r requirements-dev.txt +``` + +
+ +### 4. Pre-commit 훅 설정 + +
+ +```console +$ pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +
+ +### 5. 설치 확인 + +
+ +```console +$ fastkit --version +fastapi-fastkit, version 1.2.1 + +$ python -m pytest tests/ +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +tests/test_templates.py::test_template_listing PASSED +... +======================== 45 passed in 2.34s ======================== +``` + +
+ +## 개발 도구 + +개발 환경에는 코드 품질을 유지하기 위한 여러 도구가 포함돼 있습니다: + +### 한 번에 실행하는 명령 + +Makefile 사용: + +```console +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! +``` + +제공되는 스크립트 사용: + +```console +$ ./scripts/format.sh +$ ./scripts/lint.sh +``` + +### 코드 포매팅 + +**Black** — 코드 포매터: + +
+ +```console +$ black src/ tests/ +reformatted src/main.py +reformatted tests/test_cli.py +All done! ✨ 🍰 ✨ +``` + +
+ +**isort** — import 정렬: + +
+ +```console +$ isort src/ tests/ +Fixing import order in src/main.py +``` + +
+ +### 코드 린팅 + +**mypy** — 타입 체크: + +
+ +```console +$ mypy src/ +Success: no issues found in 12 source files +``` + +
+ +## 사용 가능한 Make 명령 + +프로젝트 Makefile 은 일반적인 개발 작업을 위한 편의 명령들을 제공합니다: + +### 설정 명령 + +| 명령 | 설명 | +|---------|-------------| +| `make install` | 프로덕션 모드로 패키지 설치 | +| `make install-dev` | 개발 의존성과 함께 패키지 설치 | +| `make install-test` | 테스트용 패키지 설치 (제거 후 재설치) | +| `make uninstall` | 패키지 제거 | +| `make clean` | 빌드 아티팩트와 캐시 파일 정리 | + +### 코드 품질 명령 + +| 명령 | 설명 | +|---------|-------------| +| `make format` | black 과 isort 로 코드 포매팅 | +| `make format-check` | 변경 없이 코드 포맷 검증만 | +| `make lint` | 모든 린트 검사 실행 (isort, black, mypy) | + +### 테스트 명령 + +| 명령 | 설명 | +|---------|-------------| +| `make test` | 모든 테스트 실행 | +| `make test-verbose` | verbose 출력으로 테스트 실행 | +| `make test-coverage` | 커버리지 리포트와 함께 테스트 실행 | +| `make coverage-report` | 상세 커버리지 리포트 생성 (FORMAT=html/xml/json/all) | + +### 템플릿 검사 명령 + +| 명령 | 설명 | +|---------|-------------| +| `make inspect-templates` | 모든 템플릿에 대해 검사 실행 | +| `make inspect-templates-verbose` | verbose 출력으로 템플릿 검사 | +| `make inspect-template` | 특정 템플릿 검사 (TEMPLATES 파라미터) | + +### 문서 명령 + +| 명령 | 설명 | +|---------|-------------| +| `make serve-docs` | 문서를 로컬에서 서빙 | +| `make build-docs` | 문서 빌드 | + +### 번역 명령 + +| 명령 | 설명 | +|---------|-----------| +| `make translate` | 문서 번역 (LANG, PROVIDER, MODEL 파라미터) | + +### 예시 + +
+ +```console +# 코드 포맷 후 모든 검사 실행 +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! + +# 커버리지와 함께 테스트 실행 +$ make test-coverage +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +... +======================== 45 passed in 2.34s ======================== + +---------- coverage: platform darwin, python 3.12.1-final-0 ---------- +Name Stmts Miss Cover +-------------------------------------------- +src/main.py 45 2 96% +src/cli.py 89 5 94% +src/templates.py 67 3 96% +-------------------------------------------- +TOTAL 201 10 95% + +# HTML 커버리지 리포트 생성 +$ make coverage-report FORMAT=html +🌐 Opening HTML coverage report in browser... + +# 한국어로 문서 번역 +$ make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +Starting translation... +Running: python scripts/translate.py --target-lang ko --api-provider github --model gpt-4o-mini +``` + +
+ +## 프로젝트 구조 + +개발을 위해 프로젝트 구조를 이해해 두는 것이 중요합니다: + +```bash +FastAPI-fastkit/ +├── src/ +│ ├── fastapi_fastkit/ +│ │ ├── __main__.py # 애플리케이션 진입점 +│ │ ├── backend/ +│ │ │ ├── inspector.py # FastAPI-fastkit 템플릿 inspector +│ │ │ ├── interactive/ +│ │ │ │ ├── config_builder.py # 대화형 모드용 설정 빌더 +│ │ │ │ ├── prompts.py # 대화형 모드용 프롬프트 +│ │ │ │ ├── selectors.py # 대화형 모드용 selector 로직 +│ │ │ │ └── validators.py # 대화형 모드용 사용자 입력 validator +│ │ │ ├── main.py # 백엔드 로직 진입점 +│ │ │ ├── package_managers/ +│ │ │ │ ├── base.py # 패키지 매니저 base 클래스 +│ │ │ │ ├── factory.py # 패키지 매니저 factory +│ │ │ │ ├── pdm_manager.py # PDM 패키지 매니저 +│ │ │ │ ├── pip_manager.py # pip 패키지 매니저 +│ │ │ │ ├── poetry_manager.py # Poetry 패키지 매니저 +│ │ │ │ └── uv_manager.py # uv 패키지 매니저 +│ │ │ ├── project_builder/ +│ │ │ │ ├── config_generator.py # 프로젝트 빌더용 설정 생성기 +│ │ │ │ └── dependency_collector.py # 프로젝트 빌더용 의존성 수집기 +│ │ │ └── transducer.py # 프로젝트 빌더용 transducer +│ │ ├── cli.py # FastAPI-fastkit 메인 CLI 진입점 +│ │ ├── core/ +│ │ │ ├── exceptions.py # 예외 처리 +│ │ │ └── settings.py # 설정 구성 +│ │ ├── fastapi_project_template/ +│ │ │ ├── PROJECT_README_TEMPLATE.md # fastkit 템플릿 프로젝트의 base README 파일 +│ │ │ ├── README.md # fastkit 템플릿 README +│ │ │ ├── fastapi-async-crud/ +│ │ │ ├── fastapi-custom-response/ +│ │ │ ├── fastapi-default/ +│ │ │ ├── fastapi-dockerized/ +│ │ │ ├── fastapi-empty/ +│ │ │ ├── fastapi-mcp/ +│ │ │ ├── fastapi-psql-orm/ +│ │ │ ├── fastapi-single-module/ +│ │ │ └── modules/ +│ │ │ ├── api/ +│ │ │ │ └── routes/ +│ │ │ ├── crud/ +│ │ │ └── schemas/ +│ │ ├── py.typed +│ │ └── utils/ +│ │ ├── logging.py # 로깅 설정 +│ │ └── main.py # FastAPI-fastkit 메인 진입점 +│ └── logs +├── tests +│ ├── conftest.py # pytest 설정 +│ ├── test_backends/ +│ ├── test_cli_operations/ +│ ├── test_core.py +│ ├── test_rich/ +│ ├── test_templates/ +│ └── test_utils.py +├── uv.lock +├── docs/ # 문서 +├── scripts/ # 개발 스크립트 +├── mkdocs.yml +├── overrides/ # mkdocs 오버라이드 +├── pdm.lock +├── pyproject.toml +├── requirements-docs.txt # 문서용 requirements +├── requirements.txt # 개발용 requirements +├── CHANGELOG.md +├── CITATION.cff +├── CODE_OF_CONDUCT.md +├── CONTRIBUTING.md +├── LICENSE +├── MANIFEST.in +├── Makefile +├── README.md +├── SECURITY.md +└── env.example # 환경 변수 예시 (번역 AI 모델 환경 변수 포함) +``` + +### 핵심 디렉터리 + +- **`src/fastapi_fastkit/`** — 메인 패키지 소스 코드 + - **`cli.py`** — 메인 CLI 진입점 + - **`backend/`** — 핵심 백엔드 로직 + - **`inspector.py`** — 템플릿 inspector + - **`interactive/`** — 대화형 모드 컴포넌트 (prompts, selectors, validators) + - **`package_managers/`** — 패키지 매니저 구현 (pip, uv, pdm, poetry) + - **`project_builder/`** — 프로젝트 빌드 유틸리티 + - **`transducer.py`** — 템플릿 transducer + - **`core/`** — 핵심 설정과 예외 + - **`fastapi_project_template/`** — 프로젝트 템플릿 (fastapi-default, fastapi-async-crud 등) + - **`utils/`** — 공유 유틸리티 함수 +- **`tests/`** — 테스트 스위트 + - **`test_backends/`** — 백엔드 관련 테스트 + - **`test_cli_operations/`** — CLI 동작 테스트 + - **`test_templates/`** — 템플릿 시스템 테스트 +- **`docs/`** — 문서 (MkDocs) + - 사용자 가이드, 튜토리얼, API 레퍼런스 + +## 개발 워크플로 + +### 1. 기능 브랜치 생성 + +
+ +```console +$ git checkout -b feature/add-new-template +Switched to a new branch 'feature/add-new-template' +``` + +
+ +### 2. 변경 사항 작성 + +코드 편집, 기능 추가, 버그 수정... + +### 3. 테스트와 검사 실행 + +
+ +```console +$ make dev-check +Running all quality checks... +Running all tests... +✅ All tests passed! +``` + +
+ +### 4. 변경 사항 커밋 + +Pre-commit 훅이 자동으로 실행됩니다: + +
+ +```console +$ git add . +$ git commit -m "Add new FastAPI template with authentication" +format...................................................................Passed +isort-check..............................................................Passed +black-fix................................................................Passed +mypy.....................................................................Passed +[feature/add-new-template abc1234] Add new FastAPI template with authentication +``` + +
+ +### 5. Push 후 Pull Request 생성 + +
+ +```console +$ git push origin feature/add-new-template +$ gh pr create --title "Add new FastAPI template with authentication" +``` + +
+ +## 테스트 + +### 테스트 실행 + +**모든 테스트:** + +
+ +```console +$ make test +# 또는 +$ python -m pytest +``` + +
+ +**특정 테스트 파일:** + +
+ +```console +$ python -m pytest tests/test_cli.py -v +``` + +
+ +**커버리지와 함께:** + +
+ +```console +$ make test-coverage +# 또는 +$ python -m pytest --cov=src --cov-report=html +``` + +
+ +### 테스트 작성 + +새 기능을 추가할 때는 항상 테스트를 함께 작성하세요: + +```python +# tests/test_commands/test_new_feature.py +import pytest +from fastapi_fastkit.commands.new_feature import NewFeatureCommand + +class TestNewFeatureCommand: + def test_command_success(self): + """Test successful command execution""" + command = NewFeatureCommand() + result = command.execute(valid_args) + assert result.success is True + assert result.message == "Feature executed successfully" + + def test_command_validation_error(self): + """Test command with invalid arguments""" + command = NewFeatureCommand() + with pytest.raises(ValueError, match="Invalid argument"): + command.execute(invalid_args) + + def test_command_edge_case(self): + """Test edge case handling""" + command = NewFeatureCommand() + result = command.execute(edge_case_args) + assert result.success is True + assert "warning" in result.message.lower() +``` + +### 테스트 카테고리 + +**단위 테스트** — 개별 함수와 클래스 테스트: + +```python +def test_validate_project_name(): + assert validate_project_name("valid-name") is True + assert validate_project_name("invalid name!") is False +``` + +**통합 테스트** — 명령 간 상호 작용 테스트: + +```python +def test_init_command_creates_project(tmp_path): + result = runner.invoke(cli, ['init'], input='test-project\n...') + assert result.exit_code == 0 + assert (tmp_path / "test-project").exists() +``` + +**종단간 테스트** — 전체 워크플로를 검증하는 테스트: + +```python +def test_full_project_creation_workflow(tmp_path): + # 프로젝트 생성 + result = runner.invoke(cli, ['init'], input='...') + assert result.exit_code == 0 + + # 라우트 추가 + result = runner.invoke(cli, ['addroute', 'test-project', 'users']) + assert result.exit_code == 0 + + # 파일 존재 확인 + assert (tmp_path / "test-project" / "src" / "api" / "routes" / "users.py").exists() +``` + +## 문서 + +### 문서 로컬 서빙 + +
+ +```console +$ make serve-docs +INFO - Building documentation... +INFO - Cleaning site directory +INFO - Documentation built in 0.43 seconds +INFO - [14:30:00] Serving on http://127.0.0.1:8000/ +``` + +
+ +### 문서 빌드 + +
+ +```console +$ make build-docs +INFO - Building documentation... +INFO - Documentation built in 0.43 seconds +``` + +
+ +### 문서 작성 + +문서는 Markdown으로 작성하며 MkDocs로 빌드합니다. 예시 구조는 다음과 같습니다: + +**기능 가이드 템플릿:** + +````markdown +# New Feature Guide + +This guide explains how to use the new feature. + +## Prerequisites + +- FastAPI-fastkit installed +- Basic Python knowledge + +## Usage + +
+ +```console +$ fastkit new-feature --option value +✅ Feature executed successfully! +``` + +
+ +!!! tip "Pro Tip" + Use `--help` to see all available options. +```` + +`mkdocs-material` 사용법이 더 궁금하다면 [mkdocs-material 문서](https://squidfunk.github.io/mkdocs-material/reference/admonitions/)를 참고하세요. + +## 코드 스타일 가이드라인 + +### Python 코드 스타일 + +[PEP 8](https://www.python.org/dev/peps/pep-0008/)을 기본으로 하되, 아래 규칙도 함께 따라 주세요: + +- **줄 길이**: 88자 (Black 기본값) +- **Import**: isort 로 정리 +- **타입 힌트**: 모든 공개 함수에 필수 +- **Docstring**: 모든 공개 API 에 Google 스타일 + +### 예시 + +```python +from typing import List, Optional +from pathlib import Path + +def create_project_structure( + project_name: str, + template_path: Path, + output_dir: Optional[Path] = None, +) -> List[Path]: + """Create project structure from template. + + Args: + project_name: Name of the project to create + template_path: Path to the template directory + output_dir: Output directory, defaults to current directory + + Returns: + List of created file paths + + Raises: + ValueError: If project_name is invalid + FileNotFoundError: If template_path doesn't exist + """ + if not project_name.isidentifier(): + raise ValueError(f"Invalid project name: {project_name}") + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + # Implementation here... + return created_files +``` + +## 환경 변수 + +개발할 때는 다음 환경 변수를 설정해 활용할 수 있습니다: + +| 변수 | 설명 | 기본값 | +|----------|-------------|---------| +| `FASTKIT_DEBUG` | 디버그 로깅 활성화 | `False` | +| `FASTKIT_DEV_MODE` | 개발 기능 활성화 | `False` | +| `FASTKIT_TEMPLATE_DIR` | 커스텀 템플릿 디렉터리 | 내장 템플릿 | +| `FASTKIT_CONFIG_DIR` | 설정 디렉터리 | `~/.fastkit` | +| `TRANSLATION_API_KEY` | 번역 API 키 ([Github AI 모델 제공자](https://github.com/marketplace/models/azure-openai) 사용 시 Github PAT 입력) | `None` | + +
+ +```console +$ export FASTKIT_DEBUG=true +$ export FASTKIT_DEV_MODE=true +$ fastkit init +DEBUG: Loading configuration from /home/user/.fastkit/ +DEBUG: Available templates: ['fastapi-default', ...] +``` + +
+ +다른 환경 변수 설정은 [settings.py](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/core/settings.py) 모듈을 참고하세요. + +## 문제 해결 + +### 자주 발생하는 문제 + +**1. Pre-commit 훅 실패:** + +
+ +```console +$ git commit -m "Fix bug" +black....................................................................Failed +hookid: black + +Files were modified by this hook. Additional output: + +would reformat src/cli.py +``` + +
+ +**해결책:** 포매터를 실행하고 다시 커밋하세요: + +
+ +```console +$ make format +$ git add . +$ git commit -m "Fix bug" +``` + +
+ +**2. 다른 Python 버전에서 테스트가 실패할 때:** + +**해결 방법:** `tox`로 여러 Python 버전을 함께 테스트하세요: + +
+ +```console +$ pip install tox +$ tox +py38: commands succeeded +py39: commands succeeded +py310: commands succeeded +py311: commands succeeded +py312: commands succeeded +``` + +
+ +**3. 개발 중 import 오류:** + +**해결책:** 패키지를 editable 모드로 설치하세요: + +
+ +```console +$ pip install -e . +``` + +
+ +### 도움 받기 + +- **[GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues)**: 버그 신고와 기능 요청 +- **[GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)**: 질문과 아이디어 공유 +- **문서**: [사용자 가이드](../user-guide/installation.md) 확인 + +## 기여 가이드라인 + +### PR 제출 전 + +1. **모든 검사 실행:** `make dev-check` +2. 필요 시 **문서 갱신** +3. 새 기능에는 **테스트 추가** +4. **커밋 메시지 컨벤션 준수** + +### 커밋 메시지 형식 + +``` +type(scope): brief description + +Longer description if needed + +Fixes #123 +``` + +**타입:** + +- `feat`: 새 기능 +- `fix`: 버그 수정 +- `docs`: 문서 변경 +- `style`: 코드 스타일 변경 +- `refactor`: 코드 리팩터링 +- `test`: 테스트 추가/변경 +- `chore`: 유지보수 작업 + +**예시:** + +``` +feat(cli): add new template command + +Add support for creating projects from custom templates. +The command accepts a template path and creates a new +project with the specified configuration. + +Fixes #45 + +fix(templates): handle missing template files gracefully + +When a template file is missing, show a clear error message +instead of crashing with a stack trace. + +Fixes #67 +``` + +## 릴리스 프로세스 + +메인테이너용 릴리스 프로세스: + +1. `setup.py` 와 `__init__.py` 의 **버전 갱신** +2. **CHANGELOG.md 갱신** +3. **릴리스 PR 생성** +4. 머지 후 **태그 생성** +5. **GitHub Actions**가 자동으로 빌드와 배포를 수행 + +
+ +```console +$ git tag v1.2.0 +$ git push origin v1.2.0 +``` + +
+ +## 다음 단계 + +이제 개발 환경이 준비됐으니: + +1. 아키텍처를 이해하기 위해 **[코드베이스 살펴보기](https://github.com/bnbong/FastAPI-fastkit/tree/main/src/fastapi_fastkit)** +2. 모든 것이 정상 동작하는지 **테스트 스위트 실행** +3. GitHub에서 작업할 **[이슈 고르기](https://github.com/bnbong/FastAPI-fastkit/issues)** +4. 다른 기여자들과 소통하고 싶다면 **[Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) 참여** + +즐거운 코딩 되세요! 🚀 + +!!! tip "개발 팁" + - 커밋 전에 `make dev-check` 사용 + - 테스트를 먼저 작성 (TDD 접근) + - 커밋은 작고 집중되게 유지 + - 새 기능과 함께 문서도 갱신 diff --git a/docs/ko/contributing/template-creation-guide.md b/docs/ko/contributing/template-creation-guide.md new file mode 100644 index 0000000..8e8bcbb --- /dev/null +++ b/docs/ko/contributing/template-creation-guide.md @@ -0,0 +1,577 @@ +# FastAPI 템플릿 작성 가이드 + +FastAPI-fastkit에 새 FastAPI 프로젝트 템플릿을 추가하는 방법을 정리한 종합 가이드입니다. + +## 🎯 개요 + +새 템플릿을 추가하는 작업은 5단계 프로세스로 진행됩니다: + +1. **📋 계획과 설계** — 템플릿의 목적과 구조 정의 +2. **🏗️ 템플릿 구현** — 필수 구조와 파일 생성 +3. **🔍 로컬 검증** — inspector 로 템플릿 검증 +4. **📚 문서화** — README 및 사용 가이드 작성 +5. **🚀 제출과 검토** — PR 생성 및 커뮤니티 검토 + +## 📋 1단계: 계획과 설계 + +### 템플릿 목적 정의 + +새 템플릿을 만들기 전에 다음 질문에 답해 보세요: + +- **이 템플릿이 제공하는 고유한 가치는 무엇인가?** +- **기존 템플릿들과 어떻게 차별화되는가?** +- **어떤 사용자 그룹이 대상 사용자인가?** +- **어떤 기술 스택을 포함할 것인가?** + +### 템플릿 명명 규칙 + +``` +fastapi-{purpose}-{stack} +``` + +예: + +- `fastapi-microservice` (마이크로서비스 템플릿) +- `fastapi-graphql` (GraphQL 통합 템플릿) +- `fastapi-auth-jwt` (JWT 인증 템플릿) + +### 기술 스택 계획 + +포함할 주요 기술을 미리 정의하세요: + +```yaml +# 예시: fastapi-microservice 템플릿 +core_dependencies: + - fastapi + - uvicorn + - pydantic + - pydantic-settings + +additional_features: + - sqlalchemy (ORM) + - alembic (migrations) + - redis (caching) + - celery (background tasks) + - pytest (testing) + +development_tools: + - black (code formatting) + - isort (import sorting) + - mypy (type checking) + - pre-commit (Git hooks) +``` + +## 🏗️ 2단계: 템플릿 구현 + +### 필수 디렉터리 구조 + +``` +fastapi-{template-name}/ +├── src/ # 애플리케이션 소스 코드 +│ ├── main.py-tpl # ✅ FastAPI 앱 진입점 (필수) +│ ├── __init__.py-tpl +│ ├── api/ # API 라우터 +│ │ ├── __init__.py-tpl +│ │ ├── api.py-tpl # 메인 API 라우터 +│ │ └── routes/ # 개별 라우트 +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl # 예제 라우트 +│ ├── core/ # 코어 설정 +│ │ ├── __init__.py-tpl +│ │ └── config.py-tpl # 설정 관리 +│ ├── crud/ # CRUD 로직 +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ ├── schemas/ # Pydantic 모델 +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ └── utils/ # 유틸리티 함수 +│ ├── __init__.py-tpl +│ └── helpers.py-tpl +├── tests/ # ✅ 테스트 (필수) +│ ├── __init__.py-tpl +│ ├── conftest.py-tpl # pytest 설정 +│ └── test_items.py-tpl # 예제 테스트 +├── scripts/ # 스크립트 +│ ├── format.sh-tpl # 코드 포매팅 +│ ├── lint.sh-tpl # 린팅 +│ ├── run-server.sh-tpl # 서버 실행 +│ └── test.sh-tpl # 테스트 실행 +├── pyproject.toml-tpl # ✅ 주요 메타데이터 (PEP 621, 권장) +├── setup.py-tpl # 🟡 Legacy 메타데이터 (하위 호환을 위해 허용) +├── requirements.txt-tpl # 🟡 pyproject 가 의존성을 선언하면 선택 사항 +├── setup.cfg-tpl # 개발 도구 설정 +├── README.md-tpl # ✅ 프로젝트 문서 (필수) +├── .env-tpl # 환경 변수 템플릿 +└── .gitignore-tpl # Git ignore 파일 +``` + +**최소 필수 파일.** 템플릿은 다음을 반드시 제공해야 합니다: + +- `tests/` 디렉터리 +- `README.md-tpl` +- 메타데이터 파일 최소 하나: `pyproject.toml-tpl` (권장, PEP 621) 또는 `setup.py-tpl` (레거시, 여전히 허용) +- 다음 중 최소 한 곳에 `fastapi` 의존성 선언: `pyproject.toml-tpl` 의 `[project].dependencies`, `requirements.txt-tpl`, 또는 `setup.py-tpl` 의 `install_requires` + +`pyproject.toml-tpl`이 `[project].dependencies`를 선언한다면 `requirements.txt-tpl`은 더 이상 필수가 아닙니다. 최신 템플릿이라면 `pyproject.toml-tpl`을 주요 메타데이터 파일로 사용하는 편을 권장합니다. + +### 파일 작성 가이드 + +#### 1. main.py-tpl 작성 + +```python +""" +FastAPI application entry point + +This file is the main application for the project created with FastAPI-fastkit. +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.api import api_router +from core.config import settings + +# FastAPI 앱 생성 (inspector 검증을 위해 필수) +app = FastAPI( + title="", + description="Project created with FastAPI-fastkit", + version="1.0.0", +) + +# CORS 미들웨어 구성 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# API 라우터 등록 +app.include_router(api_router, prefix="/api/v1") + +@app.get("/") +async def root(): + """Root endpoint""" + return {"message": "Hello from !"} + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +#### 2. pyproject.toml-tpl 작성 (권장) + +최신 템플릿은 PEP 621 형식의 `pyproject.toml-tpl`로 메타데이터와 의존성을 선언해야 합니다. 최소한 이 파일은 `[project]` 섹션에 `name`, `version`, `description`, 그리고 `fastapi`가 포함된 `dependencies` 리스트를 가져야 합니다. 또한 사용자 워크스페이스 안의 다른 FastAPI 프로젝트와 생성된 프로젝트를 구분할 수 있도록 `is_fastkit_project()`가 인식하는 두 가지 FastAPI-fastkit 식별 마커도 함께 담아야 합니다: + +- `description` 의 `[FastAPI-fastkit templated]` 접두사 +- `managed = true` 를 가진 별도의 `[tool.fastapi-fastkit]` 테이블 + +검출은 둘 중 하나만 있어도 인식합니다 (대소문자 구분 없음). 메타데이터 주입 단계가 템플릿이 마커를 빠뜨려도 프로젝트 생성 시점에 추가해 주지만, 작성자는 명시적으로 포함해 두는 것이 좋습니다. + +```toml +[project] +name = "" +version = "0.1.0" +description = "[FastAPI-fastkit templated] " +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.0", + "pydantic-settings>=2.7.0", + "python-dotenv>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "httpx>=0.28.0", +] + +[tool.fastapi-fastkit] +managed = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +``` + +#### 3. requirements.txt-tpl 작성 (선택) + +`pyproject.toml-tpl` 이 `[project].dependencies` 를 선언한다면 선택 사항입니다. `pip` 단독 워크플로를 선호하는 템플릿에는 여전히 유용합니다. + +```txt +# FastAPI 핵심 의존성 (필수) +fastapi==0.104.1 +uvicorn[standard]==0.24.0 + +# 데이터 검증 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# 환경 변수 관리 +python-dotenv==1.0.0 + +# 데이터베이스 (필요한 경우) +sqlalchemy==2.0.23 +alembic==1.13.0 + +# 개발 도구 +pytest==7.4.3 +pytest-asyncio==0.21.1 +httpx==0.25.2 + +# 코드 품질 +black==23.11.0 +isort==5.12.0 +mypy==1.7.1 +``` + +#### 4. setup.py-tpl 작성 (레거시 — `pyproject`가 있다면 선택 사항) + +Legacy 템플릿을 위해 유지됩니다. 새 템플릿은 `pyproject.toml-tpl` 을 제공한다면 이 파일이 필요 없습니다. + +```python +""" + package setup + +Project created with FastAPI-fastkit. +""" +from setuptools import find_packages, setup + +# 의존성 목록 (타입 어노테이션 필수) +install_requires: list[str] = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "python-dotenv>=1.0.0", +] + +setup( + name="", + version="1.0.0", + description="[FastAPI-fastkit templated] ", # is_fastkit_project()가 사용하는 식별 마커 + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="", + author_email="", + packages=find_packages(), + install_requires=install_requires, + python_requires=">=3.8", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], +) +``` + +#### 5. 테스트 파일 작성 + +```python +# tests/test_items.py-tpl +""" +Items API test module +""" +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_read_root(): + """Test root endpoint""" + response = client.get("/") + assert response.status_code == 200 + assert "message" in response.json() + +def test_health_check(): + """Test health check""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + +def test_create_item(): + """Test item creation""" + item_data = { + "name": "Test Item", + "description": "Test Description" + } + response = client.post("/api/v1/items/", json=item_data) + assert response.status_code == 200 + data = response.json() + assert data["name"] == item_data["name"] + assert data["description"] == item_data["description"] + +def test_read_items(): + """Test reading items list""" + response = client.get("/api/v1/items/") + assert response.status_code == 200 + assert isinstance(response.json(), list) +``` + +## 🔍 3단계: 로컬 검증 + +### 자동 검증 스크립트 실행 + +새 템플릿이 준비되면 다음 명령으로 검증하세요: + +```bash +# 모든 템플릿 검증 +make inspect-templates + +# 특정 템플릿만 검증 +make inspect-template TEMPLATES="fastapi-your-template" + +# verbose 출력으로 검증 +python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose +``` + +!!! note + + PR을 제출하면 **Template PR Inspection** 워크플로가 자동으로 실행되어 템플릿 변경 사항을 검증합니다. 검증 결과는 PR에 직접 피드백으로 남습니다. + +### 검증 체크리스트 + +inspector 가 자동으로 검증하는 항목들입니다: + +#### ✅ 파일 구조 검증 + +- [ ] `tests/` 디렉터리 존재 +- [ ] `README.md-tpl` 파일 존재 +- [ ] `pyproject.toml-tpl` (권장) 또는 `setup.py-tpl` (레거시) 중 최소 하나 존재 + +#### ✅ 파일 확장자 검증 + +- [ ] 모든 Python 파일이 `.py-tpl` 확장자 사용 +- [ ] `.py` 확장자 파일이 존재하지 않음 + +#### ✅ 의존성 검증 + +- [ ] 다음 중 최소 한 곳에 `fastapi` 가 선언됨: + - [ ] `pyproject.toml-tpl` 의 `[project].dependencies` (권장) + - [ ] `requirements.txt-tpl` + - [ ] `setup.py-tpl` 의 `install_requires` + +#### ✅ FastAPI 구현 검증 + +- [ ] `main.py-tpl` 에 `FastAPI` import 존재 +- [ ] `main.py-tpl` 에 `app = FastAPI()` 같은 앱 생성 코드 존재 + +#### ✅ 테스트 실행 검증 + +- [ ] 가상 환경 생성 성공 +- [ ] 의존성 설치 성공 +- [ ] 모든 pytest 테스트 통과 + +#### ✅ 자동 템플릿 테스트 + +FastAPI-fastkit 은 모든 템플릿에 대해 종합 테스트를 실행하는 **자동 템플릿 테스트** 시스템을 포함합니다: + +**테스트 커버리지:** + +- ✅ 템플릿 생성 과정 +- ✅ 프로젝트 메타데이터 주입 +- ✅ 가상 환경 설정 +- ✅ 의존성 설치 (모든 패키지 매니저) +- ✅ 기본 프로젝트 구조 검증 +- ✅ FastAPI 프로젝트 식별 + +**테스트 실행:** + +```console +# 모든 템플릿을 자동으로 테스트 +$ pytest tests/test_templates/test_all_templates.py -v + +# 특정 템플릿만 테스트 +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v +``` + +**템플릿 테스트 자동 발견:** +새 템플릿은 수동 설정 없이 **자동으로 발견**돼 테스트됩니다: + +1. ✅ **추가 설정 없음**: 템플릿 추가 → 자동 테스트 +2. ✅ **일관된 테스트**: 모든 템플릿에 동일한 품질 기준 +3. ✅ **다중 패키지 매니저**: UV, PDM, Poetry, PIP 로 테스트 +4. ✅ **종합 검증**: 구조, 메타데이터, 동작 검증 + +**기여자에게 의미하는 것:** + +- 🚀 **`FastAPI-fastkit` 메인 소스 testcase 에 별도 테스트 파일 불필요**: 템플릿이 자동으로 테스트됨 +- ⚡ **빠른 개발**: 테스트 설정이 아닌 템플릿 콘텐츠에 집중 +- 🛡️ **품질 보증**: 모든 템플릿에 일관된 테스트 +- 🔄 **CI/CD 통합**: PR 에서 자동 테스트 + +**여전히 직접 테스트가 필요한 영역:** + +- 🧪 **템플릿 고유 동작**: 비즈니스 로직과 커스텀 기능 +- 🔧 **통합 테스트**: 외부 서비스와 복잡한 워크플로 +- 📱 **엔드투엔드 시나리오**: 사용자의 전체 워크플로 + +**테스트 모범 사례:** + +```console +# 1. 로컬에서 템플릿 테스트 +$ fastkit startdemo your-template-name + +# 2. 자동 테스트 실행 +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v + +# 3. 다양한 패키지 매니저로 테스트 +$ fastkit startdemo your-template-name --package-manager poetry +$ fastkit startdemo your-template-name --package-manager pdm +$ fastkit startdemo your-template-name --package-manager uv +``` + +### 수동 검증 체크리스트 + +자동 검증 외에도 다음 항목을 직접 점검하세요: + +#### 🔧 코드 품질 + +- [ ] 코드가 PEP 8 스타일 가이드를 따름 +- [ ] 적절한 타입 힌트 사용 +- [ ] 의미 있는 변수명과 함수명 +- [ ] 적절한 주석과 docstring + +#### 🏗️ 아키텍처 + +- [ ] 관심사 분리 (API, 비즈니스 로직, 데이터 접근의 분리) +- [ ] 재사용 가능한 컴포넌트 설계 +- [ ] 확장 가능한 구조 +- [ ] 보안 모범 사례 적용 + +#### 📚 문서 + +- [ ] README.md-tpl 이 PROJECT_README_TEMPLATE.md 형식을 따름 +- [ ] 설치와 실행 방법이 명시됨 +- [ ] API 문서 (OpenAPI/Swagger) +- [ ] 환경 변수 설명 + +## 📚 4단계: 문서화 + +### README.md-tpl 작성 + +[PROJECT_README_TEMPLATE.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md) 가이드를 기준으로 작성하세요. + +### 템플릿 설명 문서 작성 + +`src/fastapi_fastkit/fastapi_project_template/README.md` 에 새 템플릿에 대한 설명을 추가하세요: + +```markdown +## fastapi-your-template + +여기에 새 템플릿에 대한 짧은 설명과 사용 사례를 작성하세요. + +### Features: +- 기능 1 +- 기능 2 +- 기능 3 + +### Use Cases: +- 사용 사례 1 +- 사용 사례 2 +``` + +## 🚀 5단계: 제출과 검토 + +### PR 생성 전 체크리스트 + +- [ ] 모든 자동 검증 통과 (`make inspect-templates`) +- [ ] 코드 포매팅 완료 (`make format`) +- [ ] 린트 검사 통과 (`make lint`) +- [ ] 모든 테스트 통과 (`make test`) +- [ ] 문서 작성 완료 +- [ ] CONTRIBUTING.md 가이드라인 준수 + +### PR 제목과 설명 + +``` +[TEMPLATE] Add fastapi-{template-name} template + +## Overview +새 {purpose} 템플릿을 추가합니다. + +## Key Features +- 기능 1 +- 기능 2 +- 기능 3 + +## Validation Results +- [ ] Inspector 검증 통과 +- [ ] 모든 테스트 통과 +- [ ] 문서 작성 완료 + +## Usage Example +\```bash +fastkit startdemo +# 템플릿 선택: fastapi-{template-name} +\``` + +## Related Issues +Closes #issue-number +``` + +### 검토 프로세스 + +1. **자동 검증**: GitHub Actions 가 템플릿을 자동으로 검증합니다 + - **Template PR Inspection**: 템플릿을 수정하는 PR 에서 `inspect-changed-templates.py` 실행 + - **Weekly Inspection**: 매주 수요일 전체 템플릿 검증 +2. **코드 검토**: 메인테이너와 커뮤니티가 코드를 검토합니다 +3. **테스트**: 다양한 환경에서 템플릿이 테스트됩니다 +4. **문서 검토**: 문서의 정확성과 완전성을 검토합니다 +5. **승인과 머지**: 모든 요구 사항을 만족하면 main 브랜치에 머지 + +!!! note + + 검증 결과가 자동 PR 코멘트로 달립니다. 리뷰 요청 전에 이 코멘트들을 먼저 확인해 주세요! + +## 🎯 모범 사례 + +### 보안 고려 사항 + +- 민감한 정보는 환경 변수로 관리 +- 적절한 CORS 설정 +- 입력 데이터 검증 +- SQL 인젝션 방지 + +### 성능 최적화 + +- 비동기 처리 활용 +- 데이터베이스 쿼리 최적화 +- 적절한 캐싱 전략 +- 응답 압축 설정 + +### 유지보수성 + +- 명확한 코드 구조 +- 포괄적인 테스트 커버리지 +- 자세한 문서 +- 로깅과 모니터링 설정 + +## 🆘 도움이 필요하신가요? + +- 📖 [개발 환경 설정 가이드](development-setup.md) +- 📋 [코드 가이드라인](code-guidelines.md) +- 💬 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) +- 📧 [메인테이너에게 연락](mailto:bbbong9@gmail.com) + +새 템플릿을 추가하는 일은 FastAPI-fastkit 커뮤니티에 큰 도움이 됩니다. +당신의 아이디어와 노력이 다른 개발자들에게 큰 도움이 될 것입니다! 🚀 diff --git a/docs/ko/contributing/translation-guide.md b/docs/ko/contributing/translation-guide.md new file mode 100644 index 0000000..379ba08 --- /dev/null +++ b/docs/ko/contributing/translation-guide.md @@ -0,0 +1,371 @@ +# 번역 가이드 + +이 가이드는 FastAPI-fastkit 문서 번역에 기여하는 방법을 설명합니다. + +## 원본과 번역 정책 + +> **영어 (`en`)가 FastAPI-fastkit 문서의 기준이 되는 원문입니다.** 다른 모든 언어는 번역 대상이며, 릴리스 단위로든 개별 페이지 단위로든 영어 원문보다 뒤처질 수 있습니다. +> +> 번역된 페이지가 영어 페이지와 다르면, 번역이 따라잡을 때까지 **영어 페이지를 신뢰하세요**. 번역은 기여자들이 도달한 완성도 그대로 배포됩니다 — 부분 번역이 정상이고 자연스러운 상태입니다. + +이 정책을 사용자 관점에서 설명하는 페이지는 [번역 현황](../reference/translation-status.md)입니다. 그 페이지에는 각 언어의 실제 완성도와, 아직 번역되지 않은 페이지가 어떻게 표시되는지(요약하면 영어 원문으로 대체됨)가 정리돼 있습니다. + +리포지토리 루트의 `CHANGELOG.md` 역시 영어를 기준으로 유지합니다. 어떤 언어에서 `changelog.md` 페이지를 제공하더라도, 별도의 번역본 changelog를 유지하기보다는 이 영문 기준 changelog를 링크하거나 포함하는 방식으로 다루는 것이 현재 정책입니다. + +번역에 기여할 때는 사용자가 언어 선택기에서 짐작하지 않아도 무엇이 가능한지 알 수 있도록, 현황 페이지의 표도 함께 갱신해 주세요. + +## 개요 + +FastAPI-fastkit은 AI 기반 자동 번역 시스템을 사용해 문서를 여러 언어로 번역합니다. 이 시스템은 다음과 같은 방식으로 동작합니다: + +- 영어로 작성된 원본 문서를 읽고 +- AI API(OpenAI 또는 Anthropic)를 사용해 콘텐츠를 번역하며 +- 번역 결과를 언어별 디렉터리에 저장하고 +- 검토용 GitHub Pull Request를 생성합니다 + +자동화는 초안을 만들어 주는 역할일 뿐이며, 머지 전에는 여전히 사람의 검토가 필요합니다. AI가 생성한 번역은 PR에서 `draft` 상태로 표시하고, 머지 전에 해당 언어에 능숙한 사용자가 검토해야 합니다. + +## 지원 언어 + +아래는 현재 문서 사이트에서 **빌드 대상으로 설정된** 언어 목록입니다. 빌드 대상으로 등록되어 있다고 해서 그 언어의 페이지가 모두 번역되었다는 뜻은 **아닙니다**. 실제 완성도는 [번역 현황](../reference/translation-status.md) 페이지를 참고하세요. + +- 🇰🇷 한국어 (ko) +- 🇯🇵 일본어 (ja) +- 🇨🇳 중국어 (zh) +- 🇪🇸 스페인어 (es) +- 🇫🇷 프랑스어 (fr) +- 🇩🇪 독일어 (de) + +## 사전 요구 사항 + +### 1. 번역 의존성 설치 + +```bash +# pip 로 설치 +pip install openai anthropic + +# 또는 pdm 사용 +pdm install -G translation +``` + +### 2. API 키 설정 + +OpenAI 또는 Anthropic 의 API 키가 필요합니다: + +```bash +# OpenAI +export TRANSLATION_API_KEY="sk-..." + +# 또는 Anthropic +export TRANSLATION_API_KEY="sk-ant-..." +``` + +### 3. GitHub CLI 설치 (선택) + +자동 PR 생성을 위해: + +```bash +# macOS +brew install gh + +# Linux +curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null +sudo apt update +sudo apt install gh + +# 인증 +gh auth login +``` + +## 사용법 + +### Make 명령 사용 (권장) + +번역을 실행하는 가장 쉬운 방법은 다음과 같습니다: + +```bash +# 모든 언어로 모든 문서 번역 +make translate + +# 특정 언어로만 번역 +make translate LANG=ko + +# API 제공자와 모델 지정 +make translate LANG=ko PROVIDER=openai MODEL=gpt-4 +make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +``` + +### 스크립트 직접 실행 + +#### 모든 문서 번역 + +지원하는 모든 언어로 모든 문서를 번역: + +```bash +python scripts/translate.py --api-provider openai +``` + +### 특정 언어만 번역 + +한국어로만 번역: + +```bash +python scripts/translate.py --target-lang ko --api-provider openai +``` + +### 특정 파일만 번역 + +특정 문서 파일만 번역: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/installation.md user-guide/quick-start.md \ + --api-provider openai +``` + +### PR 생성 건너뛰기 + +GitHub PR을 만들지 않고 번역만 수행하려면: + +```bash +python scripts/translate.py --target-lang ko --no-pr --api-provider openai +``` + +### Anthropic Claude 사용 + +OpenAI 대신 Anthropic Claude 사용: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --api-provider anthropic \ + --api-key "sk-ant-..." +``` + +## 디렉터리 구조 + +번역이 끝난 뒤 문서 구조는 다음과 같습니다: + +``` +docs/ +├── en/ # 영어 (원본) +│ ├── index.md +│ ├── user-guide/ +│ │ ├── installation.md +│ │ ├── quick-start.md +│ │ └── ... +│ ├── tutorial/ +│ └── ... +├── ko/ # 한국어 +│ ├── index.md +│ ├── user-guide/ +│ └── ... +├── ja/ # 일본어 +├── zh/ # 중국어 +├── es/ # 스페인어 +├── fr/ # 프랑스어 +├── de/ # 독일어 +├── css/ # 공유 자산 +├── js/ # 공유 자산 +└── img/ # 공유 자산 +``` + +## 번역 워크플로 + +### 1. 영어로 문서 작성 + +모든 문서는 먼저 `docs/` 디렉터리에 영어로 작성해야 합니다: + +```bash +# 새 문서 작성 +vim docs/user-guide/new-feature.md +``` + +### 2. 번역 실행 + +영어 문서 작성이 끝나면 번역 스크립트를 실행하세요: + +```bash +python scripts/translate.py --target-lang ko +``` + +### 3. Pull Request 검토 + +스크립트가 번역 결과로 Pull Request를 생성합니다. PR을 검토할 때는 다음 사항을 확인하세요: + +1. 마크다운 형식이 보존됐는지 확인 +2. 기술 용어가 적절히 처리됐는지 검증 +3. 코드 예제가 그대로 유지됐는지 확인 +4. 언어별 특수 이슈가 없는지 점검 + +### changelog 정책 + +- 리포지토리 루트의 `CHANGELOG.md` 는 영어로 유지합니다. +- 루트 changelog 자체를 다른 언어로 다시 작성하는 것을 목표로 한 번역 PR은 열지 마세요. +- 어떤 언어에서 changelog 페이지가 필요하다면, `docs//changelog.md` 는 영문 기준 changelog로 들어가는 래퍼나 진입점으로 취급하세요. + +### 4. 승인과 머지 (메인테이너용) + +번역이 검증되면: + +```bash +gh pr review --approve +gh pr merge +``` + +### 5. 문서 배포 + +문서 사이트가 자동으로 새 번역을 반영해 다시 빌드됩니다. + +## 번역 설정 + +`scripts/translation_config.json`을 편집해 동작을 조정할 수 있습니다: + +```json +{ + "source_language": "en", + "target_languages": [ + { + "code": "ko", + "name": "Korean", + "native_name": "한국어", + "enabled": true + } + ], + "translation_settings": { + "default_api_provider": "openai", + "batch_size": 5, + "preserve_formatting": true + }, + "github_settings": { + "create_pr_by_default": true, + "branch_prefix": "translation" + } +} +``` + +## 모범 사례 + +### 원본 문서 작성 시 + +1. **명확한 표현**: 번역이 잘 되도록 명확하고 단순한 영어로 작성 +2. **일관된 용어**: 기술 용어는 일관되게 사용 +3. **올바른 코드 블록**: 코드 블록에 항상 언어를 지정 +4. **링크 검증**: 내부 링크는 모두 상대 경로 사용 + +### 번역 검토 시 + +1. **기술 용어**: 기술 용어가 대상 언어에 적절한지 검증 +2. **문화적 맥락**: 예제가 현지화되어야 하는지 확인 +3. **형식**: 모든 마크다운 형식이 보존됐는지 확인 +4. **코드 무결성**: 코드 블록이 그대로 유지됐는지 검증 + +## 문제 해결 + +### API 레이트 제한 + +API 레이트 제한에 걸리면, 더 작은 단위로 번역하세요: + +```bash +# user guide 만 번역 +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/*.md +``` + +### 번역 품질 문제 + +번역 품질이 떨어진다면: + +1. API 키가 유효한지 확인 +2. 다른 AI 제공자 시도 +3. 복잡한 문서를 더 작은 섹션으로 나누기 +4. 직접 검토하고 수정 + +### GitHub PR 실패 + +PR 생성이 실패한다면: + +```bash +# PR 없이 번역만 +python scripts/translate.py --target-lang ko --no-pr + +# 직접 PR 생성 +git checkout -b translation/ko +git add docs/ko/ +git commit -m "Add Korean translations" +git push -u origin translation/ko +gh pr create --title "Add Korean translations" +``` + +## 수동 번역 + +직접 손으로 번역할 수도 있습니다: + +1. 영어 파일을 대상 언어 디렉터리에 복사: + +```bash +mkdir -p docs/ko/user-guide +cp docs/en/user-guide/installation.md docs/ko/user-guide/installation.md +``` + +2. 선호하는 에디터로 파일을 편집 +3. 커밋 후 PR 생성 + +## 언어 전환 + +문서 사이트 상단에는 언어 선택기가 있습니다. 사용자는 다음과 같은 순서로 활용할 수 있습니다: + +1. 언어 선택기 클릭 +2. 선호하는 언어 선택 +3. 번역된 문서 탐색 + +## 새 언어 추가하기 + +새 언어를 추가하려면: + +1. `scripts/translation_config.json` 편집: + +```json +{ + "code": "pt", + "name": "Portuguese", + "native_name": "Português", + "enabled": true +} +``` + +2. `mkdocs.yml` 갱신: + +```yaml +- locale: pt + name: Português + build: true +``` + +3. 번역 실행: + +```bash +python scripts/translate.py --target-lang pt +``` + +## 도움이 필요하신가요? + +- **Issues**: 번역 관련 이슈는 [GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues)에 등록 +- **Discussions**: 질문은 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)에서 논의 +- **기여 안내**: [CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md) 참고 + +## 번역 품질 기준 + +모든 번역은 다음 기준을 충족해야 합니다: + +- ✅ 모든 마크다운 형식을 보존 +- ✅ 코드 블록을 그대로 유지 +- ✅ 기술 용어를 적절하게 다룸 +- ✅ 올바른 문법과 맞춤법 사용 +- ✅ 언어별 컨벤션을 따름 +- ✅ 모든 링크가 정상 동작하는지 테스트 + +FastAPI-fastkit 번역에 기여해 주셔서 감사합니다! 🌍 diff --git a/docs/ko/index.md b/docs/ko/index.md index f2821ef..39ac1b7 100644 --- a/docs/ko/index.md +++ b/docs/ko/index.md @@ -2,7 +2,7 @@ FastAPI-fastkit

-FastAPI-fastkit: Python과 FastAPI 신규 사용자용 빠르고 사용하기 쉬운 스타터 키트 +FastAPI-fastkit: Python과 FastAPI를 처음 접하는 분들을 위한 빠르고 쓰기 쉬운 스타터 키트

@@ -18,31 +18,26 @@ --- -이 프로젝트는 Python과 [FastAPI](https://github.com/fastapi/fastapi) 신규 사용자가 Python 기반 웹 앱을 개발하는 데 필요한 개발 환경 구성을 더욱 빠르게 할 수 있도록 만들었습니다. +이 프로젝트는 Python과 [FastAPI](https://github.com/fastapi/fastapi)를 처음 접하는 사용자가 Python 기반 웹 앱 개발에 필요한 환경을 더 빠르게 갖출 수 있도록 만들어졌습니다. -이 프로젝트는 `SpringBoot initializer` 및 Python Django의 `django-admin` CLI 동작에서 영감을 받았습니다. +이 프로젝트는 `SpringBoot initializer`와 Python Django의 `django-admin` CLI에서 영감을 받았습니다. !!! info "번역 상태" - 이 문서의 **원본은 영어 (`en`)** 입니다. 한국어 번역은 일부 페이지에만 - 제공되며, 번역되지 않은 페이지는 영어 원문으로 표시됩니다. 각 언어의 - 실제 번역 진행 상황은 - [Translation Status](reference/translation-status.md) 페이지를 - 확인해 주세요. 영어 페이지와 번역 페이지의 내용이 다르다면 영어 - 페이지를 기준으로 삼으세요. + 이 문서의 **원본은 영어 (`en`)**입니다. 언어 선택기에 표시되는 다른 언어는 부분 번역 상태이거나 페이지별로 영어 원문이 대신 표시될 수 있습니다. 각 언어의 실제 번역 진행 상황은 [번역 현황](reference/translation-status.md) 페이지를 참고하세요. ## 주요 기능 -- **⚡ Immediate FastAPI project creation** : [Python Django](https://github.com/django/django)의 `django-admin` 기능에서 영감을 받은 CLI를 통해 초고속 FastAPI 워크스페이스 및 프로젝트 생성 -- **✨ 대화형 프로젝트 빌더**: 데이터베이스, 인증, 캐싱, 모니터링 등 기능을 단계별로 안내하여 선택하고 코드를 자동 생성 -- **🎨 Prettier CLI outputs** : [rich library](https://github.com/Textualize/rich) 기반의 아름다운 CLI 경험 -- **📋 Standards-based FastAPI project templates** : 모든 FastAPI-fastkit 템플릿은 Python 표준과 FastAPI의 일반적 사용 패턴을 기반으로 구성 -- **🔍 Automated template quality assurance** : 주간 자동화 테스트로 모든 템플릿이 정상 동작하며 최신 상태로 유지됨을 보장 -- **🚀 Multiple project templates** : 다양한 사용 사례에 맞춘 사전 구성 템플릿 선택 가능(async CRUD, Docker, PostgreSQL 등) -- **📦 Multiple package manager support** : 의존성 관리를 위해 선호하는 Python 패키지 관리자(pip, uv, pdm, poetry)를 선택 가능 +- **⚡ 즉시 FastAPI 프로젝트 생성**: [Python Django](https://github.com/django/django)의 `django-admin` 기능에서 영감을 받아 FastAPI 워크스페이스와 프로젝트를 빠르게 생성 +- **✨ 대화형 프로젝트 빌더**: 데이터베이스, 인증, 캐싱, 모니터링 등을 단계별로 안내하고, 선택한 구성을 바탕으로 코드를 자동 생성 +- **🎨 보기 좋은 CLI 출력**: [rich library](https://github.com/Textualize/rich) 기반의 깔끔한 CLI 경험 +- **📋 표준 기반 FastAPI 프로젝트 템플릿**: 모든 FastAPI-fastkit 템플릿은 Python 표준과 FastAPI의 일반적인 사용 패턴을 바탕으로 구성 +- **🔍 자동화된 템플릿 품질 보증**: 주간 자동 테스트로 모든 템플릿이 정상 동작하고 최신 상태를 유지하도록 보장 +- **🚀 다양한 프로젝트 템플릿**: async CRUD, Docker, PostgreSQL 등 다양한 사용 사례에 맞춘 사전 구성 템플릿 제공 +- **📦 다중 패키지 매니저 지원**: 선호하는 Python 패키지 매니저(pip, uv, pdm, poetry)를 선택 가능 ## 설치 -Python 환경에 `FastAPI-fastkit`를 설치하세요. +Python 환경에 `FastAPI-fastkit`을 설치하세요.

@@ -56,11 +51,11 @@ $ pip install FastAPI-fastkit ## 사용법 -### FastAPI 프로젝트 워크스페이스 환경을 즉시 생성 +### 새 FastAPI 프로젝트 워크스페이스 환경을 즉시 생성 -이제 FastAPI-fastkit으로 매우 빠르게 새 FastAPI 프로젝트를 시작할 수 있습니다! +이제 FastAPI-fastkit으로 새 FastAPI 프로젝트를 아주 빠르게 시작할 수 있습니다. -다음으로 즉시 새 FastAPI 프로젝트 워크스페이스를 생성하세요: +다음 명령으로 새 FastAPI 프로젝트 워크스페이스를 즉시 생성하세요:
@@ -177,11 +172,11 @@ Installing dependencies...
-이 명령은 Python 가상 환경과 함께 새로운 FastAPI 프로젝트 워크스페이스 환경을 생성합니다. +이 명령은 Python 가상 환경까지 포함된 새 FastAPI 프로젝트 작업 공간을 만들어 줍니다. ### 대화형 모드로 프로젝트 생성 ✨ NEW! -보다 복잡한 프로젝트의 경우, 지능형 기능 선택을 통해 대화형 모드로 FastAPI 애플리케이션을 단계별로 구성하세요: +보다 복잡한 프로젝트의 경우, **대화형 모드**를 사용해 지능형 기능 선택과 함께 단계별로 FastAPI 애플리케이션을 구성하세요:
@@ -197,11 +192,11 @@ Enter the author email: john@example.com Enter the project description: Full-stack FastAPI project with PostgreSQL and JWT 🧱 Architecture Preset -프로젝트 레이아웃을 선택합니다. Enter를 누르면 추천 기본값이 적용됩니다. - 1. minimal - 가장 작은 FastAPI 앱 - 2. single-module - 단일 모듈 구성 (프로토타입 / 스크립트용) - 3. classic-layered - api/routes + crud + schemas + core (fastapi-default 형태) - 4. domain-starter - 도메인 지향 src/app/domains// (recommended) +Pick a project layout. Press Enter to accept the recommended default. + 1. minimal - Smallest viable FastAPI app + 2. single-module - Everything in one module (prototypes / scripts) + 3. classic-layered - api/routes + crud + schemas + core (à la fastapi-default) + 4. domain-starter - Domain-oriented src/app/domains// (recommended) Select architecture preset: [4] @@ -251,38 +246,37 @@ Select monitoring (Loguru, OpenTelemetry, Prometheus, None): Select monitoring: 3 +🧪 Testing Framework Selection +Select testing framework (Basic, Coverage, Advanced, None): + 1. Basic - pytest + httpx for API testing + 2. Coverage - Basic + code coverage + 3. Advanced - Coverage + faker + factory-boy for fixtures + 4. None - No testing framework -🧪 테스트 프레임워크 선택 -테스트 프레임워크를 선택하세요 (Basic, Coverage, Advanced, None): - 1. Basic - API 테스트를 위한 pytest + httpx - 2. Coverage - Basic + 코드 커버리지 - 3. Advanced - Coverage + 픽스처를 위한 faker + factory-boy - 4. None - 테스트 프레임워크 없음 +Select testing framework: 2 -테스트 프레임워크 선택: 2 - -🛠️ 추가 유틸리티 -유틸리티를 선택하세요 (쉼표로 구분된 숫자, 예: 1,3,4): +🛠️ Additional Utilities +Select utilities (comma-separated numbers, e.g., 1,3,4): 1. CORS - Cross-Origin Resource Sharing - 2. Rate-Limiting - 요청 속도 제한 - 3. Pagination - 페이지네이션 지원 - 4. WebSocket - WebSocket 지원 + 2. Rate-Limiting - Request rate limiting + 3. Pagination - Pagination support + 4. WebSocket - WebSocket support -유틸리티 선택: 1 +Select utilities: 1 -🚀 배포 구성 -배포 옵션을 선택하세요: - 1. Docker - Dockerfile 생성 - 2. docker-compose - docker-compose.yml 생성(Docker 포함) - 3. None - 배포 구성 없음 +🚀 Deployment Configuration +Select deployment option: + 1. Docker - Generate Dockerfile + 2. docker-compose - Generate docker-compose.yml (includes Docker) + 3. None - No deployment configuration -배포 옵션 선택: 2 +Select deployment option: 2 -📦 패키지 매니저 선택 -패키지 매니저를 선택하세요 (pip, uv, pdm, poetry): uv +📦 Package Manager Selection +Select package manager (pip, uv, pdm, poetry): uv -📝 사용자 정의 패키지(선택 사항) -사용자 정의 패키지 이름을 입력하세요(쉼표로 구분, 건너뛰려면 Enter): +📝 Custom Packages (optional) +Enter custom package names (comma-separated, press Enter to skip): 📋 Project Configuration Summary ┌─────────────────────┬───────────────────────────────────────────────────────────────────────────┐ @@ -334,63 +328,64 @@ Proceed with project creation? [Y/n]: y │ ✨ Generated configuration files for selected stack │ ╰───────────────────────────────────────────────────────╯ -가상 환경을 생성하는 중... -종속성을 설치하는 중... +Creating virtual environment... +Installing dependencies... ----> 100% -╭─────────────────────── 성공 ───────────────────────╮ -│ ✨ FastAPI 프로젝트 'my-fullstack-project'가 │ -│ 'fastapi-domain-starter' 템플릿에서 생성되었습니다│ -╰────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-fullstack-project' from │ +│ 'fastapi-domain-starter' has been created! │ +╰───────────────────────────────────────────────────────╯ ```
대화형 모드가 제공하는 기능: -- **아키텍처 프리셋 선택** (`minimal` / `single-module` / `classic-layered` / `domain-starter`) — 베이스 템플릿과 프로젝트 레이아웃을 선택 -- 데이터베이스, 인증, 백그라운드 작업, 캐싱, 모니터링 등 기능에 대한 가이드형 선택 -- 선택한 기능에 대한 자동 코드 생성 — 프리셋에 따라 동작 방식 차이 (`minimal` / `single-module`은 `main.py` 재생성, `classic-layered` / `domain-starter`는 템플릿 제공 `main.py` 보존하며 구성 모듈만 추가) -- 프리셋에 맞춘 Docker 생성 — 생성된 `Dockerfile`의 `CMD`가 해당 프리셋의 실제 진입점(`src.main:app` 또는 `src.app.main:app`)을 가리킴 -- 자동 pip 호환성의 스마트 종속성 관리 -- 호환되지 않는 조합을 방지하는 기능 검증과 수동 와이어링 경고 -- 생성된 `pyproject.toml`에 식별 마커 주입 (`description` 마커 + `[tool.fastapi-fastkit]` 테이블) — 이후 `is_fastkit_project()`가 생성된 프로젝트를 식별 가능 -### FastAPI 프로젝트에 새 라우트를 추가하기 +- **아키텍처 프리셋 선택** (`minimal` / `single-module` / `classic-layered` / `domain-starter`) — 적절한 베이스 템플릿과 프로젝트 레이아웃을 결정 +- 데이터베이스, 인증, 백그라운드 작업, 캐싱, 모니터링 등에 대한 **가이드형 선택** +- 선택한 기능에 대한 **자동 코드 생성** — 프리셋에 따라 동작 방식이 다름 (`minimal` / `single-module` 은 `main.py` 재생성, `classic-layered` / `domain-starter` 는 템플릿 제공 `main.py` 보존하며 설정 모듈만 추가) +- **프리셋 인지형 Docker 생성** — 생성된 `Dockerfile` 의 `CMD` 가 해당 프리셋의 실제 진입점 (`src.main:app` 또는 `src.app.main:app`) 을 가리킴 +- 자동 pip 호환성을 갖춘 **스마트 의존성 관리** +- 프리셋이 자동 연결할 수 없는 선택에 대해 수동 연결 안내를 출력하는 **기능 검증** +- 생성된 `pyproject.toml` 에 **식별 마커** 주입 (description 마커 + `[tool.fastapi-fastkit]` 테이블) — 이후 `is_fastkit_project()` 가 생성된 프로젝트를 식별 가능 -`FastAPI-fastkit`은 FastAPI 프로젝트 확장을 쉽게 만들어 줍니다. +### FastAPI 프로젝트에 새 라우트 추가 -다음으로 FastAPI 프로젝트에 새 라우트 엔드포인트를 추가하세요: +`FastAPI-fastkit` 은 FastAPI 프로젝트 확장을 쉽게 만들어 줍니다. + +다음 명령으로 FastAPI 프로젝트에 새 라우트 엔드포인트를 추가하세요:
```console -$ fastkit addroute my-awesome-project user - 새 라우트 추가 중 +$ fastkit addroute user my-awesome-project + Adding New Route ┌──────────────────┬──────────────────────────────────────────┐ -│ 프로젝트 │ my-awesome-project │ -│ 라우트 이름 │ user │ -│ 대상 디렉터리 │ ~your-project-path~ │ +│ Project │ my-awesome-project │ +│ Route Name │ user │ +│ Target Directory │ ~your-project-path~ │ └──────────────────┴──────────────────────────────────────────┘ -프로젝트 'my-awesome-project'에 라우트 'user'를 추가하시겠습니까? [Y/n]: y +Do you want to add route 'user' to project 'my-awesome-project'? [Y/n]: y -╭──────────────────────── 정보 ───────────────────────╮ -│ ℹ API 라우터를 포함하도록 main.py를 업데이트했습니다 │ -╰─────────────────────────────────────────────────────╯ -╭─────────────────────── 성공 ───────────────────────╮ -│ ✨ 새 라우트 'user'를 프로젝트에 성공적으로 추가했습니다 │ -│ `my-awesome-project` │ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ ╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Successfully added new route 'user' to project │ +│ `my-awesome-project` │ +╰───────────────────────────────────────────────────────╯ ```
-### 구조화된 FastAPI 데모 프로젝트를 바로 배치하기 +### 구조화된 FastAPI 데모 프로젝트를 즉시 배치 구조화된 FastAPI 데모 프로젝트로 시작할 수도 있습니다. -데모 프로젝트는 다양한 기술 스택으로 구성되어 있으며 간단한 아이템 CRUD 엔드포인트가 구현되어 있습니다. +데모 프로젝트는 다양한 기술 스택과 함께 간단한 item CRUD 엔드포인트가 구현된 형태로 제공됩니다. 다음 명령으로 구조화된 FastAPI 데모 프로젝트를 즉시 배치하세요: @@ -398,59 +393,59 @@ $ fastkit addroute my-awesome-project user ```console $ fastkit startdemo -프로젝트 이름을 입력하세요: my-awesome-demo -작성자 이름을 입력하세요: John Doe -작성자 이메일을 입력하세요: john@example.com -프로젝트 설명을 입력하세요: My awesome FastAPI demo -'fastapi-default' 템플릿을 사용하여 FastAPI 프로젝트를 배포합니다 -템플릿 경로: +Enter the project name: my-awesome-demo +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI demo +Deploying FastAPI project using 'fastapi-default' template +Template path: /~fastapi_fastkit-package-path~/fastapi_project_template/fastapi-default - 프로젝트 정보 + Project Information ┌──────────────┬─────────────────────────┐ -│ 프로젝트 이름│ my-awesome-demo │ -│ 작성자 │ John Doe │ -│ 작성자 이메일│ john@example.com │ -│ 설명 │ My awesome FastAPI demo │ +│ Project Name │ my-awesome-demo │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI demo │ └──────────────┴─────────────────────────┘ - 템플릿 종속성 + Template Dependencies ┌──────────────┬───────────────────┐ -│ 종속성 1 │ fastapi │ -│ 종속성 2 │ uvicorn │ -│ 종속성 3 │ pydantic │ -│ 종속성 4 │ pydantic-settings │ -│ 종속성 5 │ python-dotenv │ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ └──────────────┴───────────────────┘ -사용 가능한 패키지 매니저: - 패키지 매니저 +Available Package Managers: + Package Managers ┌────────┬────────────────────────────────────────────┐ -│ PIP │ 표준 Python 패키지 관리자 │ -│ UV │ 빠른 Python 패키지 관리자 │ -│ PDM │ 현대적인 Python 종속성 관리 │ -│ POETRY │ Python 종속성 관리 및 패키징 │ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ └────────┴────────────────────────────────────────────┘ -패키지 매니저를 선택하세요 (pip, uv, pdm, poetry) [uv]: uv -프로젝트 생성을 진행하시겠습니까? [y/N]: y -FastAPI 템플릿 프로젝트가 '~your-project-path~'에 배포됩니다 +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI template project will deploy at '~your-project-path~' ---> 100% -╭─────────────────────── 성공 ───────────────────────╮ -│ ✨ 종속성이 성공적으로 설치되었습니다 │ -╰────────────────────────────────────────────────────╯ -╭─────────────────────── 성공 ───────────────────────╮ -│ ✨ FastAPI 프로젝트 'my-awesome-demo'가 │ -│ 'fastapi-default'에서 생성되어 다음 위치에 저장되었습니다 │ -│ ~your-project-path~! │ -╰────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-awesome-demo' from │ +│ 'fastapi-default' has been created and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ ```
-사용 가능한 FastAPI 데모 목록을 보려면 다음을 확인하세요: +사용 가능한 FastAPI 데모 목록을 보려면 다음 명령을 실행하세요:
@@ -475,53 +470,55 @@ $ fastkit list-templates ## 문서 -포괄적인 가이드와 상세한 사용법은 문서를 참고하세요: +종합적인 가이드와 자세한 사용법은 문서를 참고하세요: -- 📚 [User Guide](user-guide/quick-start.md) - 자세한 설치 및 사용 가이드 -- 🎯 [Tutorial](tutorial/getting-started.md) - 초보자를 위한 단계별 튜토리얼 -- 📖 [CLI Reference](user-guide/cli-reference.md) - 완전한 명령어 레퍼런스 -- 🔍 [Template Quality Assurance](reference/template-quality-assurance.md) - 자동화된 테스트와 품질 기준 +- 📚 **[사용자 가이드](user-guide/quick-start.md)** - 자세한 설치 및 사용 가이드 +- 🎯 **[튜토리얼](tutorial/getting-started.md)** - 초보자를 위한 단계별 튜토리얼 +- 📖 **[CLI 레퍼런스](user-guide/cli-reference.md)** - 전체 명령어 레퍼런스 +- 🔍 **[템플릿 품질 보증](reference/template-quality-assurance.md)** - 자동화된 테스트 및 품질 기준 ## 🚀 템플릿 기반 튜토리얼 -사전 구성된 템플릿으로 실전 사용 사례를 통해 FastAPI 개발을 학습하세요: +사전 구축된 템플릿으로 실전 사용 사례를 통해 FastAPI 개발을 학습하세요: -### 📖 코어 튜토리얼 +### 📖 핵심 튜토리얼 -- [기본 API 서버 구축](tutorial/basic-api-server.md) - `fastapi-default` 템플릿을 사용해 첫 FastAPI 서버 만들기 -- [비동기 CRUD API 구축](tutorial/async-crud-api.md) - `fastapi-async-crud` 템플릿으로 고성능 비동기 API 개발 +- **[기본 API 서버 만들기](tutorial/basic-api-server.md)** - `fastapi-default` 템플릿으로 첫 FastAPI 서버 만들기 +- **[비동기 CRUD API 만들기](tutorial/async-crud-api.md)** - `fastapi-async-crud` 템플릿으로 고성능 비동기 API 개발 +- **[도메인 지향 프로젝트](tutorial/domain-starter.md)** - 현재 권장 기본값인 `fastapi-domain-starter` 템플릿으로 중간 규모 API 구축 ### 🗄️ 데이터베이스 및 인프라 -- [데이터베이스 통합](tutorial/database-integration.md) - `fastapi-psql-orm` 템플릿으로 PostgreSQL + SQLAlchemy 활용 -- [Dockerizing 및 배포](tutorial/docker-deployment.md) - `fastapi-dockerized` 템플릿을 사용해 프로덕션 배포 환경 구성 +- **[데이터베이스 통합](tutorial/database-integration.md)** - `fastapi-psql-orm` 템플릿으로 PostgreSQL + SQLAlchemy 활용 +- **[Docker 기반 배포](tutorial/docker-deployment.md)** - `fastapi-dockerized` 템플릿으로 프로덕션 배포 환경 구성 ### ⚡ 고급 기능 -- **[커스텀 응답 처리 & 고급 API 설계](tutorial/custom-response-handling.md)** - `fastapi-custom-response` 템플릿으로 엔터프라이즈급 API 구축 -- **[MCP와의 통합](tutorial/mcp-integration.md)** - `fastapi-mcp` 템플릿을 사용해 AI 모델과 통합된 API 서버 생성 +- **[커스텀 응답 처리 및 고급 API 설계](tutorial/custom-response-handling.md)** - `fastapi-custom-response` 템플릿으로 엔터프라이즈급 API 구축 +- **[MCP 통합](tutorial/mcp-integration.md)** - `fastapi-mcp` 템플릿으로 AI 모델과 통합된 API 서버 만들기 각 튜토리얼은 다음을 제공합니다: + - ✅ **실용적인 예제** - 실제 프로젝트에서 바로 사용할 수 있는 코드 -- ✅ **단계별 가이드** - 초보자도 쉽게 따라 할 수 있는 상세 설명 +- ✅ **단계별 가이드** - 초보자도 따라가기 쉬운 자세한 설명 - ✅ **모범 사례** - 업계 표준 패턴과 보안 고려 사항 - ✅ **확장 방법** - 프로젝트를 한 단계 더 발전시키는 가이드 ## 기여 -커뮤니티의 기여를 환영합니다! FastAPI-fastkit은 Python과 FastAPI 입문자를 돕기 위해 설계되었으며, 여러분의 기여는 큰 영향을 미칠 수 있습니다. +커뮤니티의 기여를 환영합니다! FastAPI-fastkit은 Python과 FastAPI 입문자를 돕기 위해 설계되었으며, 여러분의 기여는 큰 변화를 만들 수 있습니다. ### 기여할 수 있는 항목 - 🚀 **새로운 FastAPI 템플릿** - 다양한 사용 사례를 위한 템플릿 추가 -- 🐛 **버그 수정** - 안정성과 신뢰성 개선에 도움 +- 🐛 **버그 수정** - 안정성과 신뢰성 개선 - 📚 **문서화** - 가이드, 예제, 번역 개선 - 🧪 **테스트** - 테스트 커버리지 확장 및 통합 테스트 추가 - 💡 **기능** - 새로운 CLI 기능 제안 및 구현 ### 기여 시작하기 -FastAPI-fastkit에 기여를 시작하려면 다음의 종합 가이드를 참고하세요: +FastAPI-fastkit에 기여를 시작하려면 다음 가이드를 참고하세요: - **[개발 환경 설정](contributing/development-setup.md)** - 개발 환경 설정에 대한 완전한 가이드 - **[코드 가이드라인](contributing/code-guidelines.md)** - 코딩 표준 및 모범 사례 @@ -529,29 +526,29 @@ FastAPI-fastkit에 기여를 시작하려면 다음의 종합 가이드를 참 - **[CODE_OF_CONDUCT.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CODE_OF_CONDUCT.md)** - 프로젝트 원칙과 커뮤니티 기준 - **[SECURITY.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/SECURITY.md)** - 보안 지침 및 신고 방법 -## FastAPI-fastkit의 의의 +## FastAPI-fastkit이 지향하는 것 -FastAPI-fastkit은 Python과 FastAPI 신규 사용자에게 빠르고 쉽게 사용할 수 있는 스타터 키트를 제공하는 것을 목표로 합니다. +FastAPI-fastkit은 Python과 FastAPI를 처음 접하는 사용자에게 빠르고 쓰기 쉬운 스타터 키트를 제공하는 것을 목표로 합니다. -이 아이디어는 FastAPI 입문자가 처음부터 학습하도록 돕기 위한 취지로 시작되었으며, [FastAPI 0.111.0 버전 업데이트](https://github.com/fastapi/fastapi/releases/tag/0.111.0)에서 추가된 FastAPI-cli 패키지의 프로덕션적 의의와도 맥락을 같이합니다. +이 아이디어는 FastAPI 입문자가 처음부터 차근차근 학습할 수 있도록 돕자는 취지에서 출발했으며, [FastAPI 0.111.0 버전 업데이트](https://github.com/fastapi/fastapi/releases/tag/0.111.0)에서 추가된 FastAPI-cli 패키지가 지닌 실전적 의미와도 맥락을 같이합니다. -오랫동안 FastAPI를 사용해 오며 사랑해온 사람으로서, FastAPI 개발자 [tiangolo](https://github.com/tiangolo)가 밝힌 [멋진 동기](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417)를 실현하는 데 도움이 될 프로젝트를 만들고 싶었습니다. +오랫동안 FastAPI를 사용해 온 사람으로서, FastAPI 개발자 [tiangolo](https://github.com/tiangolo)가 밝힌 [멋진 동기](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417)를 조금이나마 현실로 옮기는 데 도움이 되는 프로젝트를 만들고 싶었습니다. -FastAPI-fastkit은 다음을 제공함으로써 시작 단계와 프로덕션 준비 애플리케이션 구축 사이의 간극을 메웁니다: +FastAPI-fastkit은 다음과 같은 가치를 제공해 "첫 시작"과 "실전에 쓸 수 있는 애플리케이션" 사이의 간극을 메우고자 합니다: -- **즉각적인 생산성**: 초기 설정의 복잡성에 압도될 수 있는 신규 사용자에게 즉시 생산성을 제공 -- **모범 사례**: 모든 템플릿에 모범 사례가 내장되어 올바른 FastAPI 패턴 학습에 도움 -- **확장 가능한 기반**: 초보자에서 전문가로 성장함에 따라 함께 확장되는 기반 -- **커뮤니티 주도 템플릿**: 실제 FastAPI 사용 패턴을 반영한 커뮤니티 중심 템플릿 +- **즉각적인 생산성** — 초기 설정의 복잡성에 압도될 수 있는 신규 사용자에게 즉시 생산성을 제공 +- **모범 사례** — 모든 템플릿에 모범 사례가 내장되어 있어, 사용자가 올바른 FastAPI 패턴을 학습하는 데 도움 +- **확장 가능한 기반** — 초보자에서 전문가로 성장함에 따라 함께 확장되는 기반 +- **커뮤니티 주도 템플릿** — 실제 FastAPI 사용 패턴을 반영한 커뮤니티 중심 템플릿 ## 다음 단계 -FastAPI-fastkit을 시작할 준비가 되셨나요? 다음 단계를 따라 진행하세요: +FastAPI-fastkit을 시작할 준비가 되셨다면, 아래 순서대로 진행해 보세요: ### 🚀 빠른 시작 1. **[설치](user-guide/installation.md)**: FastAPI-fastkit 설치 -2. **[퀵 스타트](user-guide/quick-start.md)**: 5분 만에 첫 프로젝트 만들기 +2. **[퀵 스타트](user-guide/quick-start.md)**: 5분 안에 첫 프로젝트 만들기 3. **[입문 튜토리얼](tutorial/getting-started.md)**: 단계별 상세 튜토리얼 ### 📚 심화 학습 @@ -560,7 +557,7 @@ FastAPI-fastkit을 시작할 준비가 되셨나요? 다음 단계를 따라 진 - **[라우트 추가](user-guide/adding-routes.md)**: 프로젝트에 API 엔드포인트 추가 - **[템플릿 사용](user-guide/using-templates.md)**: 사전 구축된 프로젝트 템플릿 사용 -### 🛠️ 기여하기 +### 🛠️ 기여 FastAPI-fastkit에 기여하고 싶으신가요? @@ -568,7 +565,7 @@ FastAPI-fastkit에 기여하고 싶으신가요? - **[코드 가이드라인](contributing/code-guidelines.md)**: 코딩 표준 및 모범 사례 준수 - **[기여 가이드라인](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)**: 종합 기여 가이드 -### 🔍 참고 자료 +### 🔍 레퍼런스 - **[CLI 레퍼런스](user-guide/cli-reference.md)**: 전체 CLI 명령 레퍼런스 - **[템플릿 품질 보증](reference/template-quality-assurance.md)**: 자동화된 테스트 및 품질 기준 @@ -577,4 +574,4 @@ FastAPI-fastkit에 기여하고 싶으신가요? ## 라이선스 -이 프로젝트는 MIT 라이선스 하에 제공됩니다 - 자세한 내용은 [LICENSE](https://github.com/bnbong/FastAPI-fastkit/blob/main/LICENSE) 파일을 참조하세요. +이 프로젝트는 MIT 라이선스 하에 제공됩니다 — 자세한 내용은 [LICENSE](https://github.com/bnbong/FastAPI-fastkit/blob/main/LICENSE) 파일을 참고하세요. diff --git a/docs/ko/reference/faq.md b/docs/ko/reference/faq.md new file mode 100644 index 0000000..087e283 --- /dev/null +++ b/docs/ko/reference/faq.md @@ -0,0 +1,784 @@ +# 자주 묻는 질문 + +FastAPI-fastkit에 대해 자주 묻는 질문과 답변을 모아 둔 페이지입니다. + +## 설치와 환경 설정 + +### Q: 어떤 Python 버전을 지원하나요? + +**A:** FastAPI-fastkit은 **Python 3.12 이상**이 필요합니다. 가장 안정적으로 사용하려면 최신 안정 버전의 Python을 권장합니다. + +
+ +```console +$ python --version +Python 3.12.1 + +$ pip install fastapi-fastkit +``` + +
+ +### Q: FastAPI-fastkit 은 어떻게 설치하나요? + +**A:** `pip`으로 설치할 수 있습니다: + +
+ +```console +# 최신 안정 버전 +$ pip install fastapi-fastkit + +# GitHub 개발 버전 +$ pip install git+https://github.com/bnbong/FastAPI-fastkit.git + +# 특정 버전 +$ pip install fastapi-fastkit==1.0.0 +``` + +
+ +### Q: 권한 오류로 설치가 실패합니다 + +**A:** 가상 환경에서 설치하거나 사용자 권한으로 설치해 보세요: + +
+ +```console +# 가상 환경 생성 +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # Windows: fastapi-env\Scripts\activate + +# 가상 환경에 설치 +$ pip install fastapi-fastkit + +# 또는 현재 사용자에게만 설치 +$ pip install --user fastapi-fastkit +``` + +
+ +### Q: 설치 후 `fastkit` 명령을 찾을 수 없습니다 + +**A:** 보통 설치 디렉터리가 PATH 에 들어 있지 않아 발생합니다: + +
+ +```console +# 설치됐는지 확인 +$ pip show fastapi-fastkit + +# 설치 위치 찾기 +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__file__)" + +# 직접 실행 시도 +$ python -m fastapi_fastkit --version + +# 또는 PATH 에 추가 (Linux/macOS) +$ export PATH="$HOME/.local/bin:$PATH" +``` + +
+ +## 프로젝트 생성 + +### Q: 어떤 의존성 스택이 있나요? + +**A:** FastAPI-fastkit은 세 가지 의존성 스택을 제공합니다: + +- **MINIMAL**: FastAPI, Uvicorn, Pydantic, Pydantic-Settings (기본 웹 API) +- **STANDARD**: SQLAlchemy, Alembic, pytest 추가 (데이터베이스 지원) +- **FULL**: Redis, Celery 추가 (백그라운드 작업) + +!!! tip "기본 패키지 매니저" + 더 빠른 의존성 설치를 위해 기본 패키지 매니저는 `uv` 입니다. `pip`, `pdm`, `poetry` 도 선택할 수 있습니다. + +
+ +```console +$ fastkit init +# 프로젝트 생성 중에 선호하는 스택을 선택하세요 +``` + +
+ +### Q: 프로젝트 템플릿을 커스터마이즈할 수 있나요? + +**A:** 가능합니다! 다음 중 선택할 수 있습니다: + +1. **기존 템플릿 사용**: `fastkit startdemo` +2. **커스텀 템플릿 작성**: 기존 템플릿을 복사 후 수정 +3. **점진적으로 라우트 추가**: `fastkit addroute` + +
+ +```console +# 사전 구축 템플릿 사용 +$ fastkit list-templates +$ fastkit startdemo + +# 기존 프로젝트에 라우트 추가 +$ fastkit addroute users . # 현재 디렉터리에 'users' 라우트 추가 +$ fastkit addroute users my-project # 'my-project' 에 'users' 라우트 추가 +``` + +
+ +### Q: 특정 이름 형식으로 프로젝트를 어떻게 만드나요? + +**A:** 프로젝트 이름은 유효한 Python 식별자여야 합니다: + +- ✅ `my-api`, `blog_system`, `UserService` +- ❌ `my api`, `123project`, `project-name!` + +
+ +```console +$ fastkit init +Enter the project name: my_awesome_api # 유효 +Enter the project name: my-awesome-api # 유효 (하이픈은 언더스코어로 변환됨) +``` + +
+ +### Q: 프로젝트 생성이 "directory already exists" 로 실패합니다 + +**A:** 프로젝트 디렉터리가 이미 존재합니다. 다음 중 하나를 선택하세요: + +1. **다른 이름 사용** +2. **기존 디렉터리 제거** (안전한 경우에만) +3. **다른 출력 위치 사용** + +
+ +```console +# 디렉터리 존재 여부 확인 +$ ls my-project + +# 안전하다면 제거 (주의!) +$ rm -rf my-project + +# 또는 다른 위치에 생성 +$ mkdir projects +$ cd projects +$ fastkit init +``` + +
+ +### Q: 프로젝트 설정에 대화형 모드는 어떻게 사용하나요? + +**A:** `fastkit init --interactive`를 사용하면 단계별로 질문에 답하면서 프로젝트를 구성할 수 있습니다: + +
+ +```console +$ fastkit init --interactive +``` + +
+ +대화형 모드는 다음 단계를 순서대로 진행합니다: + +1. **프로젝트 정보** — 이름, 작성자, 이메일, 설명. +2. **아키텍처 프리셋** — 프로젝트 레이아웃을 선택합니다. 권장 기본값은 `domain-starter`이며, Enter만 누르면 그대로 선택됩니다. 각 프리셋이 만드는 레이아웃과 어떤 기능 조합에서 수동 연결이 필요한지는 [프리셋 / 기능 매트릭스](preset-feature-matrix.md)를 참고하세요. +3. **기능 선택** — 데이터베이스, 인증, 백그라운드 작업, 캐싱, 모니터링, 테스트, 유틸리티, 배포. +4. **패키지 매니저와 추가 패키지** — pip / uv / pdm / poetry 가운데 하나를 고르고, 필요하면 고정 버전으로 추가 패키지를 넣을 수 있습니다. +5. **확인** — 프로젝트가 만들어지기 전에 아키텍처 프리셋을 포함한 모든 선택 사항이 요약 표로 표시됩니다. + +대화형 모드에서는 아래 기능 목록에서 원하는 구성을 선택할 수 있습니다: + +| 카테고리 | 사용 가능한 옵션 | +|----------|-------------------| +| **아키텍처** | minimal, single-module, classic-layered, **domain-starter** (권장 기본값) | +| **데이터베이스** | PostgreSQL, MySQL, MongoDB, Redis, SQLite | +| **인증** | JWT, OAuth2, FastAPI-Users, Session-based | +| **백그라운드 작업** | Celery, Dramatiq | +| **테스트** | Basic (pytest), Coverage, Advanced (faker, factory-boy 포함) | +| **캐싱** | fastapi-cache2 와 함께 Redis | +| **모니터링** | Loguru, OpenTelemetry, Prometheus | +| **유틸리티** | CORS, Rate-Limiting, Pagination, WebSocket | +| **배포** | Docker, docker-compose 와 자동 생성 설정 | + +대화형 모드는 다음을 자동으로 생성합니다: + +- 선택한 기능이 반영된 `main.py` +- 코드 생성을 지원하는 옵션을 골랐을 때의 데이터베이스 / 인증 설정 파일 (예: 데이터베이스의 PostgreSQL/MySQL/SQLite/MongoDB, 인증의 JWT/FastAPI-Users). 그 밖의 옵션은 필요한 패키지만 설치합니다 +- 선택한 배포 옵션에 맞는 배포 파일 (`Docker` 선택 시 `Dockerfile`, `docker-compose` 선택 시 `docker-compose.yml`) +- 선택한 테스트 옵션에 맞는 테스트 설정 (커버리지 설정은 `Coverage` 또는 `Advanced` 를 선택했을 때만 포함) + +### Q: 대화형 모드에서 사용 가능한 기능을 어떻게 볼 수 있나요? + +**A:** `list-features` 명령으로 사용 가능한 모든 기능과 패키지를 표시할 수 있습니다: + +
+ +```console +$ fastkit list-features +# 모든 사용 가능한 기능을 카테고리별로 표시 +# 각 기능에 연결된 패키지와 함께 +``` + +
+ +각 기능 선택에 따라 어떤 패키지가 설치되는지 파악하는 데 도움이 됩니다. + +## 라우트 개발 + +### Q: 라우트에 인증을 어떻게 추가하나요? + +**A:** 인증용 의존성을 만드세요: + +```python +# src/api/deps.py +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer + +security = HTTPBearer() + +def get_current_user(token: str = Depends(security)): + # Verify token and return user + if not verify_token(token): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials" + ) + return get_user_from_token(token) + +# src/api/routes/users.py +@router.get("/me") +def get_current_user_profile(user = Depends(get_current_user)): + return user +``` + +### Q: 데이터베이스 모델은 어떻게 추가하나요? + +**A:** STANDARD 또는 FULL 스택에서는 SQLAlchemy 모델을 만들 수 있습니다: + +```python +# src/models/users.py +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) +``` + +### Q: 요청 데이터 검증은 어떻게 추가하나요? + +**A:** 스키마에서 Pydantic 모델을 사용하세요: + +```python +# src/schemas/users.py +from pydantic import BaseModel, EmailStr, Field + +class UserCreate(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + password: str = Field(..., min_length=8) + + @validator('username') + def validate_username(cls, v): + if not v.isalnum(): + raise ValueError('Username must be alphanumeric') + return v +``` + +### Q: 파일 업로드는 어떻게 처리하나요? + +**A:** FastAPI 의 `UploadFile` 을 사용하세요: + +```python +from fastapi import UploadFile, File + +@router.post("/upload") +async def upload_file(file: UploadFile = File(...)): + contents = await file.read() + + # Save file + with open(f"uploads/{file.filename}", "wb") as f: + f.write(contents) + + return {"filename": file.filename, "size": len(contents)} +``` + +## 템플릿 + +### Q: 어떤 템플릿이 있나요? + +**A:** FastAPI-fastkit 은 여러 사전 구축 템플릿을 포함합니다: + +
+ +```console +$ fastkit list-templates + Available Templates +┌─────────────────────────┬───────────────────────────────────┐ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-custom-response │ Custom Response System │ +│ fastapi-dockerized │ Dockerized FastAPI API │ +│ fastapi-empty │ Minimal FastAPI Project │ +│ fastapi-mcp │ MCP (Model Context Protocol) API │ +│ fastapi-psql-orm │ PostgreSQL FastAPI API │ +│ fastapi-single-module │ Single-file FastAPI Project │ +└─────────────────────────┴───────────────────────────────────┘ +``` + +
+ +### Q: 특정 템플릿은 어떻게 사용하나요? + +**A:** `startdemo` 명령을 사용하세요: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-blog +Select template: fastapi-psql-orm +``` + +
+ +### Q: 직접 템플릿을 만들 수 있나요? + +**A:** 가능합니다! 디렉터리 구조를 만들고 템플릿 변수를 사용하세요: + +``` +my-template/ +├── src/ +│ └── main.py-tpl +├── requirements.txt-tpl +└── template.yaml +``` + +```python +# main.py-tpl +from fastapi import FastAPI + +app = FastAPI(title="{{PROJECT_NAME}}") + +@app.get("/") +def read_root(): + return {"message": "Hello from {{PROJECT_NAME}}!"} +``` + +### Q: 기존 템플릿은 어떻게 수정하나요? + +**A:** 템플릿은 `fastapi_project_template` 디렉터리에 있습니다. 다음과 같이 할 수 있습니다: + +1. **저장소를 fork** 해서 템플릿 수정 +2. 기존 템플릿을 기반으로 **커스텀 템플릿 작성** +3. 프로젝트 생성 후 **특정 파일만 덮어쓰기** + +## 개발 서버 + +### Q: 개발 서버는 어떻게 시작하나요? + +**A:** 프로젝트 디렉터리에서 `runserver` 명령을 사용하세요: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate # 가상 환경 활성화 +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### Q: 서버가 시작되지 않습니다 — "Address already in use" + +**A:** 8000 번 포트가 사용 중입니다. 다른 포트를 쓰거나 기존 프로세스를 종료하세요: + +
+ +```console +# 다른 포트 사용 +$ fastkit runserver --port 8080 + +# 또는 기존 프로세스 찾아서 종료 +$ lsof -ti:8000 | xargs kill -9 + +# Windows +$ netstat -ano | findstr :8000 +$ taskkill /PID /F +``` + +
+ +### Q: 자동 리로드가 동작하지 않습니다 + +**A:** 프로젝트 디렉터리에 있고 가상 환경이 활성화돼 있는지 확인하세요: + +
+ +```console +# 현재 디렉터리 확인 +$ pwd +/path/to/my-project + +# 가상 환경 확인 +$ which python +/path/to/my-project/.venv/bin/python + +# 명시적 reload 옵션으로 시작 +$ fastkit runserver --reload +``` + +
+ +### Q: 프로덕션 환경에서는 서버를 어떻게 구성하나요? + +**A:** 프로덕션에서는 개발 서버를 사용하지 마세요. 대신: + +```python +# gunicorn 이나 비슷한 WSGI 서버 사용 +$ pip install gunicorn +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker + +# 또는 fastapi-dockerized 템플릿으로 Docker 사용 +$ fastkit startdemo # fastapi-dockerized 선택 +$ docker build -t my-app . +$ docker run -p 8000:8000 my-app +``` + +## 성능과 최적화 + +### Q: API 성능은 어떻게 개선하나요? + +**A:** 다양한 최적화 전략이 있습니다: + +1. I/O 작업에 **async/await 사용** +2. 비싼 작업에 **캐싱 추가** +3. **데이터베이스 쿼리 최적화** +4. 무거운 처리에 **백그라운드 작업 사용** + +```python +# 비동기 엔드포인트 +@router.get("/users/{user_id}") +async def get_user(user_id: int): + user = await users_service.get_user_async(user_id) + return user + +# 백그라운드 작업 +from fastapi import BackgroundTasks + +@router.post("/send-email") +def send_email(background_tasks: BackgroundTasks, email: str): + background_tasks.add_task(send_notification_email, email) + return {"message": "Email will be sent in background"} +``` + +### Q: 캐싱은 어떻게 추가하나요? + +**A:** 캐싱에 Redis 를 사용하세요: + +```python +import redis +from functools import wraps + +redis_client = redis.Redis(host='localhost', port=6379, db=0) + +def cache_result(expiration: int = 300): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}" + + # 캐시에서 가져오기 시도 + cached = redis_client.get(cache_key) + if cached: + return json.loads(cached) + + # 함수 실행 및 결과 캐싱 + result = await func(*args, **kwargs) + redis_client.setex(cache_key, expiration, json.dumps(result)) + return result + return wrapper + return decorator + +@cache_result(expiration=600) +async def get_expensive_data(): + # 비싼 작업 + return complex_calculation() +``` + +### Q: 동시 요청이 많을 때는 어떻게 처리하나요? + +**A:** 적절한 서버 설정을 사용하세요: + +
+ +```console +# 개발 +$ fastkit runserver --workers 1 # 개발용 단일 워커 + +# 프로덕션 +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker +$ uvicorn src.main:app --workers 4 --host 0.0.0.0 --port 8000 +``` + +
+ +## 테스트 + +### Q: 테스트는 어떻게 실행하나요? + +**A:** 프로젝트 디렉터리에서 pytest 를 사용하세요: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate +$ python -m pytest + +# 커버리지 포함 +$ python -m pytest --cov=src + +# 특정 테스트 파일만 +$ python -m pytest tests/test_users.py + +# verbose 출력 +$ python -m pytest -v +``` + +
+ +### Q: API 테스트는 어떻게 작성하나요? + +**A:** FastAPI 의 테스트 클라이언트를 사용하세요: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + response = client.post( + "/api/v1/users/", + json={"email": "test@example.com", "username": "testuser"} + ) + assert response.status_code == 201 + assert response.json()["email"] == "test@example.com" + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +### Q: 외부 의존성은 어떻게 모킹하나요? + +**A:** pytest 픽스처와 mocking 을 사용하세요: + +```python +import pytest +from unittest.mock import Mock, patch + +@pytest.fixture +def mock_database(): + with patch('src.database.get_db') as mock_db: + mock_db.return_value = Mock() + yield mock_db + +def test_user_creation_with_mock_db(mock_database): + # 모킹된 데이터베이스로 테스트 + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 +``` + +## 기여 + +### Q: FastAPI-fastkit에는 어떻게 기여하나요? + +**A:** 다음 단계를 따르세요: + +1. GitHub에서 **저장소를 포크** +2. **개발 환경 설정** +3. **기능 브랜치 생성** +4. 테스트와 함께 **변경 사항 작성** +5. **Pull Request 제출** + +
+ +```console +$ git clone https://github.com/yourusername/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make dev-setup # 개발 환경 설정 +$ git checkout -b feature/my-feature +# 변경 사항 작성... +$ make dev-check # 포맷, 린트, 테스트 +$ git commit -m "feat: add new feature" +$ git push origin feature/my-feature +``` + +
+ +### Q: Pull Request에는 무엇을 포함해야 하나요? + +**A:** 모든 Pull Request에는 다음 내용이 포함되어야 합니다: + +- [ ] 변경 사항에 대한 **명확한 설명** +- [ ] 새 기능에 대한 **테스트** +- [ ] 필요하다면 **문서 업데이트** +- [ ] **코드 가이드라인 준수** +- [ ] **모든 검사 통과** + +### Q: 버그는 어떻게 신고하나요? + +**A:** GitHub에 아래 정보를 포함한 이슈를 작성하세요: + +1. **버그 설명**과 기대 동작 +2. **재현 단계** +3. **환경 정보** (OS, Python 버전 등) +4. **에러 메시지**나 로그 +5. 가능하면 **최소 재현 예제** + +### Q: 새 기능은 어떻게 요청하나요? + +**A:** 다음 정보를 포함한 기능 요청 이슈를 여세요: + +1. 기능에 대한 **명확한 설명** +2. **사용 사례**와 동기 +3. **제안하는 구현 방식** (선택) +4. 비슷한 기능의 **예시** + +## 문제 해결 + +### Q: import 오류가 발생합니다 + +**A:** Python 경로와 가상 환경을 확인하세요: + +
+ +```console +# 가상 환경이 활성화됐는지 확인 +$ which python +/path/to/project/.venv/bin/python + +# Python 경로 확인 +$ python -c "import sys; print(sys.path)" + +# editable 모드로 재설치 (개발용) +$ pip install -e . +``` + +
+ +### Q: 데이터베이스 연결 문제 + +**A:** 데이터베이스 템플릿에서는 데이터베이스가 실행 중인지 확인하세요: + +
+ +```console +# PostgreSQL 템플릿 +$ docker-compose up -d postgres # 데이터베이스 시작 +$ alembic upgrade head # 마이그레이션 실행 + +# 연결 확인 +$ docker-compose logs postgres +``` + +
+ +### Q: 템플릿 파일을 찾을 수 없습니다 + +**A:** 보통 템플릿 경로 문제입니다: + +
+ +```console +# 사용 가능한 템플릿 확인 +$ fastkit list-templates + +# 템플릿 디렉터리 확인 +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__path__)" + +# 템플릿이 없으면 재설치 +$ pip uninstall fastapi-fastkit +$ pip install fastapi-fastkit +``` + +
+ +### Q: pre-commit 훅이 실패합니다 + +**A:** 훅을 설치하고 실행하세요: + +
+ +```console +$ pip install pre-commit +$ pre-commit install +$ pre-commit run --all-files + +# 포맷팅 문제 수정 +$ black src/ tests/ +$ isort src/ tests/ +``` + +
+ +### Q: CI 에서는 테스트가 실패하지만 로컬에서는 통과합니다 + +**A:** 흔한 원인과 해결책: + +1. **환경 차이**: Python 버전이 일치하는지 확인 +2. **누락된 의존성**: 테스트 요구 사항이 설치됐는지 확인 +3. **경로 문제**: 절대 경로 import 사용 +4. **타이밍 문제**: 비동기 테스트에 적절한 대기 추가 + +
+ +```console +# CI와 같은 Python 버전으로 테스트 +$ python3.12 -m pytest + +# 누락된 의존성 확인 +$ pip install -r requirements-dev.txt + +# 격리된 환경에서 테스트 실행 +$ tox +``` + +
+ +## 도움 받기 + +### Q: 도움은 어디서 받을 수 있나요? + +**A:** 도움을 받을 수 있는 여러 경로가 있습니다: + +- **GitHub Issues**: 버그와 기능 요청 +- **GitHub Discussions**: 질문과 커뮤니티 지원 +- **문서**: 사용 가이드와 튜토리얼 +- **코드 예제**: 기존 템플릿과 테스트 참고 + +### Q: 업데이트 소식은 어떻게 받나요? + +**A:** 프로젝트 업데이트를 따라가세요: + +- GitHub에서 **저장소 watch** +- 새 기능 확인을 위해 **릴리스 확인** +- 호환성 깨짐 변경에 대해서는 **영문 changelog 확인** +- 문서의 **모범 사례 따르기** + +!!! tip "Pro Tips" + - Python 프로젝트에는 항상 가상 환경을 사용하세요 + - FastAPI-fastkit 설치를 최신으로 유지하세요 + - 사용 가능한 명령은 `fastkit --help` 로 확인하세요 + - 막히면 문서를 확인하세요 + - GitHub Discussions에서 자유롭게 질문하세요 diff --git a/docs/ko/reference/preset-feature-matrix.md b/docs/ko/reference/preset-feature-matrix.md new file mode 100644 index 0000000..d6b13a8 --- /dev/null +++ b/docs/ko/reference/preset-feature-matrix.md @@ -0,0 +1,60 @@ +# 아키텍처 프리셋 / 기능 매트릭스 + +대화형 `fastkit init --interactive`는 기능을 고르기 전에 먼저 **아키텍처 프리셋**([issue #44](https://github.com/bnbong/FastAPI-fastkit/issues/44))을 묻습니다. 이 프리셋이 생성될 프로젝트의 레이아웃을 결정합니다. 프리셋마다 베이스 템플릿이 다르고, 생성된 설정 파일도 일괄적으로 `src/config/` 아래에 두지 않고 각 구조에 맞는 위치에 배치됩니다. + +이 페이지는 각 프리셋이 어떤 역할을 하는지, 파일이 어디에 생성되는지, 그리고 어떤 기능 조합에서 수동 연결이 필요한지를 한눈에 보여 주는 기준 문서입니다. + +## 프리셋 → 베이스 템플릿 + +| 프리셋 | 베이스 템플릿 | 설명 | +|---|---|---| +| `minimal` | `fastapi-empty` | 가장 작은 동작 가능한 FastAPI 앱입니다. 플레이스홀더 `main.py`가 기능 선택에 따라 다시 생성됩니다. | +| `single-module` | `fastapi-single-module` | 단일 파일 FastAPI 앱입니다. `main.py`가 다시 생성됩니다. | +| `classic-layered` | `fastapi-default` | 계층형 분할(`api/routes`, `crud`, `schemas`, `core`) 구조입니다. 템플릿이 제공하는 `main.py`는 그대로 유지됩니다. | +| `domain-starter` | `fastapi-domain-starter` | 도메인 지향 구조(`src/app/domains//`)입니다. 템플릿이 제공하는 `main.py`는 그대로 유지됩니다. **권장 기본값입니다.** | + +## 생성 파일 위치 + +| 프리셋 | `main.py` 오버레이 | 데이터베이스 설정 위치 | 인증 설정 위치 | +|---|---|---|---| +| `minimal` | `src/main.py`에서 다시 생성 | `src/config/database.py` | `src/config/auth.py` | +| `single-module` | `src/main.py`에서 다시 생성 | `src/config/database.py` | `src/config/auth.py` | +| `classic-layered` | 보존 (템플릿 제공) | `src/core/database.py` | `src/core/auth.py` | +| `domain-starter` | 보존 (템플릿 제공) | `src/app/core/database.py` | `src/app/core/auth.py` | + +## 프리셋별 데이터베이스 / 인증 기능 지원 + +아래 기능은 **모든** 프리셋에서 지원됩니다. 즉, 패키지 설치 자체는 항상 성공합니다. 차이는 동적으로 다시 쓰는 `main.py`가 해당 기능을 자동으로 연결해 주는지 여부에 있습니다. + +| 기능 | `minimal` / `single-module` | `classic-layered` / `domain-starter` | +|---|---|---| +| **데이터베이스** (PostgreSQL, MySQL, SQLite, MongoDB) | 설정 모듈을 생성하고, 추가로 다시 생성된 `main.py`에 `await init_db()` 호출용 스텁을 넣어 줍니다. | 설정 모듈을 프리셋에 맞는 경로에 생성합니다. 템플릿이 제공하는 `main.py`는 **그대로 유지**되므로, `get_db()`를 라우터에 직접 연결해야 합니다. | +| **인증** (JWT, FastAPI-Users, OAuth2, Session-based) | 인증 설정 모듈을 생성합니다. JWT의 경우 다시 생성된 `main.py`에 `HTTPBearer` import까지 추가합니다. | 인증 설정 모듈을 프리셋에 맞는 경로에 생성합니다. `main.py`에는 import가 추가되지 않으므로 의존성을 직접 연결해야 합니다. | +| **백그라운드 작업** (Celery, Dramatiq) | 패키지가 설치됩니다. 현재 main.py 오버레이는 없습니다. | 동일. | +| **캐싱** (Redis) | 패키지가 설치됩니다. 현재 main.py 오버레이는 없습니다. | 동일. | +| **CORS** (유틸리티) | 다시 생성된 `main.py`에 `allow_origins=['*']` 형태로 `CORSMiddleware`가 추가됩니다. | 템플릿이 제공하는 `main.py`에 **이미 연결**돼 있습니다 (`settings.all_cors_origins` 조건부). `.env`의 `BACKEND_CORS_ORIGINS` 값만 채우면 활성화되므로 코드를 수정할 필요가 없습니다. | +| **테스트** (Basic / Coverage / Advanced) | 프로젝트 루트에 `pytest.ini` 가 생성됩니다. | 동일. | +| **배포** (Docker, docker-compose) | 프로젝트 루트에 `Dockerfile` 또는 `docker-compose.yml` 이 작성됩니다. | 동일. | + +## "프리셋 호환성" 경고가 표시되는 시점 + +템플릿이 제공하는 `main.py`를 **그대로 유지하는** 프리셋(`classic-layered`, `domain-starter`)에서는 일부 기능이 앱에 자동으로 연결되지 않습니다. 그래서 CLI는 생성이 끝난 직후, 어떤 선택이 수동 연결을 필요로 하는지 정리한 경고를 한 번 보여 줍니다: + +| 선택한 기능 | `classic-layered` / `domain-starter` 에서 경고가 발생하나? | +|---|---| +| `CORS` (유틸리티) | ❌ — 템플릿이 제공하는 `main.py`에 이미 연결돼 있습니다. `.env`에 `BACKEND_CORS_ORIGINS`만 채우면 됩니다. | +| `Rate-Limiting` (유틸리티) | ✅ — `slowapi` 리미터 설정이 추가되지 않음 | +| `Prometheus` (모니터링) | ✅ — `Instrumentator().instrument(app)` 호출이 추가되지 않음 | +| 모든 데이터베이스 / 인증 선택 | ⚠️ — 설정 파일은 생성되지만, 라우터에 `Depends()` 로 연결하는 작업은 직접 해야 함 | + +`minimal` 과 `single-module` 프리셋에서는 동적 `main.py` 오버레이가 CORS, 레이트 제한, Prometheus 계측을 자동으로 처리하므로 경고가 발생하지 않습니다. + +## 지원되지 않는 조합 (안전 우선) + +생성기는 의도적으로 템플릿이 제공하는 `main.py` 안에 생성 코드를 무리하게 끼워 넣지 **않습니다**. 그렇게 하면 import가 깨지거나 라우터가 중복될 위험이 있기 때문입니다. 현재 동작 계약은 다음과 같습니다: + +- 선택한 패키지는 항상 설치됩니다 (`pip freeze` 결과가 사용자의 의도와 맞도록). +- 생성된 설정 모듈은 항상 프리셋에 맞는 경로에 놓입니다. +- `main.py` 보존 프리셋에서는 어떤 선택이 여전히 수동 연결을 필요로 하는지 사용자에게 알려 줍니다. 즉, 조용히 깨진 코드를 넘기지 않습니다. + +모든 기능을 자동으로 연결하고 싶다면 `minimal`이나 `single-module`을 고르세요. 두 프리셋은 기능 플래그를 바탕으로 `main.py`를 다시 생성합니다. diff --git a/docs/ko/reference/template-quality-assurance.md b/docs/ko/reference/template-quality-assurance.md new file mode 100644 index 0000000..9a8d3f5 --- /dev/null +++ b/docs/ko/reference/template-quality-assurance.md @@ -0,0 +1,230 @@ +# 템플릿 품질 보증 + +FastAPI-fastkit은 모든 템플릿이 다양한 환경과 패키지 매니저에서 일관된 품질과 동작을 유지하도록 종합적인 자동 검증 시스템을 제공합니다. + +## 다층 품질 보증 + +FastAPI-fastkit은 **서로 보완하는 두 가지 품질 보증 시스템**을 운영합니다: + +### 1. 정적 템플릿 검사 (Static Template Inspection) +**템플릿 구조와 문법에 대한 매주 자동 검증** + +### 2. 동적 템플릿 테스트 (Dynamic Template Testing) +**실제 프로젝트 생성을 동반한 종합 엔드투엔드 테스트** + +## 매주 자동 검사 + +매주 수요일 자정 (UTC) 에 GitHub Actions 워크플로가 모든 FastAPI 템플릿이 품질 기준을 만족하는지 자동으로 검사합니다: + +- ✅ **파일 구조 검증** — 필수 파일과 디렉터리가 모두 있는지 확인 +- ✅ **파일 확장자 검증** — 템플릿 파일이 올바른 `.py-tpl` 확장자를 쓰는지 확인 +- ✅ **의존성 확인** — FastAPI 와 필수 의존성이 제대로 선언돼 있는지 확인 +- ✅ **FastAPI 구현 확인** — 템플릿이 적절한 FastAPI 앱 초기화를 포함하는지 확인 +- ✅ **테스트 실행** — 템플릿 테스트를 실행해 동작을 확인 + +## 자동 템플릿 테스트 시스템 + +FastAPI-fastkit은 모든 템플릿을 폭넓게 검증할 수 있는 **자동 테스트 시스템**을 갖추고 있습니다: + +### 동적 템플릿 자동 발견 + +테스트 시스템은 **수동 설정 없이 모든 템플릿을 자동으로 발견**합니다: + +```console +# 모든 템플릿을 자동으로 테스트 +$ pytest tests/test_templates/test_all_templates.py -v + +# 발견된 모든 템플릿이 결과에 표시됨 +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-async-crud] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-dockerized] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-psql-orm] +``` + +### 종합 테스트 커버리지 + +각 템플릿은 **종합적인 엔드투엔드 테스트**를 거칩니다: + +#### ✅ 프로젝트 생성 과정 + +- 템플릿 복사와 파일 변환 +- 프로젝트 메타데이터 주입 (이름, 작성자, 설명) +- 파일 구조 검증 + +#### ✅ 패키지 매니저 호환성 + +- **UV** (기본값): Rust 기반의 빠른 패키지 매니저 +- **PDM**: 현대적인 Python 의존성 관리 +- **Poetry**: 검증된 의존성 관리 +- **PIP**: 전통적인 Python 패키지 매니저 + +#### ✅ 가상 환경 관리 + +- 패키지 매니저별 환경 생성 +- 의존성 설치 검증 +- 패키지 매니저 고유 워크플로 + +#### ✅ 의존성 해석 + +- `pyproject.toml` 생성 (UV, PDM, Poetry) +- `requirements.txt` 생성 (PIP) +- 메타데이터 규격 준수 (PEP 621) +- 빌드 시스템 설정 + +#### ✅ 프로젝트 구조 검증 + +- FastAPI 프로젝트 식별 +- 필수 파일 존재 여부 +- 디렉터리 구조 검증 + +### 테스트 실행 예시 + +**모든 템플릿 테스트 실행:** + +```console +$ pytest tests/test_templates/test_all_templates.py -v +``` + +**특정 템플릿만 테스트:** + +```console +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] -v +``` + +**PDM 환경에서 테스트:** + +```console +$ pdm run pytest tests/test_templates/test_all_templates.py -v +``` + +### 지속적 통합 (CI) + +자동 테스트 시스템은 **CI/CD 파이프라인**에서도 동작합니다: + +- ✅ **PR 검증**: 모든 PR은 영향 받는 템플릿을 테스트 +- ✅ **야간 테스트**: 전체 템플릿 모음에 대한 검증 +- ✅ **패키지 매니저 테스트**: 모든 매니저로 교차 검증 +- ✅ **환경 테스트**: 여러 Python 버전과 플랫폼 + +### 기여자가 얻는 이점 + +**추가 설정 없는 테스트:** + +- 🚀 새 템플릿 추가 → 자동 테스트 +- ⚡ 수동 테스트 파일 생성 불필요 +- 🛡️ 일관된 품질 기준 + +**종합 커버리지:** + +- 🔍 엔드투엔드 프로젝트 생성 테스트 +- 📦 다중 패키지 매니저 검증 +- 🏗️ 완전한 의존성 해석 테스트 +- ✅ 실사용 시나리오 시뮬레이션 + +**개발자 경험:** + +- 🎯 **템플릿 콘텐츠에 집중**: 테스트는 자동 +- 🔄 **즉시 피드백**: 빠른 테스트 실행 +- 📊 **명확한 결과**: 상세한 테스트 리포트 +- 🚫 **별도 보일러플레이트 불필요**: 테스트 설정을 따로 만들 필요 없음 + +## 수동 템플릿 검사 + +개발과 디버깅 목적으로 로컬 검사 스크립트나 Makefile 명령으로 템플릿을 직접 검사할 수 있습니다: + +### 검사 스크립트 직접 실행 + +```console +# 모든 템플릿 검사 +$ python scripts/inspect-templates.py + +# 특정 템플릿만 검사 +$ python scripts/inspect-templates.py --templates fastapi-default,fastapi-async-crud + +# 자세한 정보를 포함한 verbose 출력 +$ python scripts/inspect-templates.py --verbose + +# 결과를 사용자 지정 파일로 저장 +$ python scripts/inspect-templates.py --output my_results.json +``` + +### Makefile 명령 사용 + +```console +# 모든 템플릿 검사 +$ make inspect-templates + +# verbose 출력으로 검사 +$ make inspect-templates-verbose + +# 특정 템플릿만 검사 +$ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" +``` + +## 검사 결과 + +- **성공한 검사**는 워크플로 출력과 아티팩트에 기록됩니다 +- **실패한 검사**는 자세한 오류 리포트와 함께 GitHub 이슈를 자동으로 생성합니다 +- **검사 이력**은 GitHub Actions 아티팩트에 30일 동안 보존됩니다 + +## 검사 출력 이해하기 + +템플릿 검사를 실행하면 다음과 같은 출력을 볼 수 있습니다: + +```console +📋 Found 6 templates to inspect: fastapi-async-crud, fastapi-custom-response, fastapi-default, fastapi-dockerized, fastapi-empty, fastapi-psql-orm +============================================================ +🔍 Inspecting template: fastapi-async-crud + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud +✅ fastapi-async-crud: PASSED +---------------------------------------- +🔍 Inspecting template: fastapi-custom-response + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response +✅ fastapi-custom-response: PASSED +---------------------------------------- +... +============================================================ +📊 INSPECTION SUMMARY + Total templates: 6 + ✅ Passed: 6 + ❌ Failed: 0 +🎉 All templates passed inspection! +📄 Results saved to: template_inspection_results.json +``` + +## 템플릿 요구 사항 + +템플릿이 검사를 통과하려면 다음 요구 사항을 충족해야 합니다: + +### 파일 구조 + +- Python 소스 파일이 들어 있는 `src/` 디렉터리가 있어야 합니다 +- Python 파일은 `.py-tpl` 확장자를 사용해야 합니다 +- `tests/` 디렉터리와 `README.md-tpl` 파일을 포함해야 합니다 +- **최소 하나 이상**의 메타데이터 파일을 포함해야 합니다: + - `pyproject.toml-tpl` (권장, PEP 621), 또는 +- `setup.py-tpl` (레거시, 여전히 허용) +- `pyproject.toml-tpl` 이 `[project].dependencies` 를 선언한다면 `requirements.txt-tpl` 은 선택 사항입니다 + +### FastAPI 요구 사항 + +- FastAPI 앱 초기화를 포함해야 합니다 +- 다음 중 최소 한 곳에 `fastapi` 를 의존성으로 선언해야 합니다: `pyproject.toml-tpl` 의 `[project].dependencies`, `requirements.txt-tpl`, 또는 `setup.py-tpl` 의 `install_requires` +- 모든 템플릿 파일이 유효한 Python 문법이어야 합니다 + +### 식별 마커 + +생성된 프로젝트가 사용자 워크스페이스에 있는 다른 FastAPI 프로젝트와 구분되도록, 템플릿은 FastAPI-fastkit 식별 마커를 갖고 있어야 합니다: + +- `pyproject.toml-tpl` — `description` 의 `[FastAPI-fastkit templated]` 접두사와 `managed = true` 를 가진 `[tool.fastapi-fastkit]` 테이블 모두. +- `setup.py-tpl` — `setup()` 의 `description` 인자에 `[FastAPI-fastkit templated]` 접두사. + +`is_fastkit_project()`는 둘 중 하나만 있어도 인식합니다 (`pyproject`가 우선이고, `setup.py`는 레거시 fallback입니다. 매칭은 대소문자를 구분하지 않습니다). 메타데이터 주입 단계가 템플릿에서 마커를 빠뜨렸더라도, 생성된 프로젝트에는 마커가 들어가도록 보장합니다. + +### 품질 기준 + +- 모든 템플릿 파일은 문법적으로 올바라야 합니다 +- 의존성이 적절하게 명시돼야 합니다 +- 템플릿 구조는 FastAPI-fastkit 컨벤션을 따라야 합니다 + +이 자동 품질 보증 시스템은 모든 템플릿이 신뢰할 수 있고, 실사용에도 무리가 없는 상태를 유지하도록 돕습니다. diff --git a/docs/ko/reference/translation-status.md b/docs/ko/reference/translation-status.md new file mode 100644 index 0000000..f73f7d4 --- /dev/null +++ b/docs/ko/reference/translation-status.md @@ -0,0 +1,82 @@ +# 번역 현황 + +FastAPI-fastkit 문서는 여러 언어로 빌드되지만, 모든 번역이 **똑같은 수준으로 완성된 것은 아닙니다**. 이 페이지는 어디까지 번역되었는지, 번역되지 않은 페이지가 어떻게 보이는지, 그리고 번역에 어떻게 기여할 수 있는지를 한곳에서 안내합니다. + +## 원본 (Source of truth) + +> **영어 (`en`)가 원본입니다.** 문서에 적힌 모든 제품·CLI·API 동작은 영어 파일을 기준으로 먼저 작성됩니다. 다른 언어는 그 영어 원본을 번역한 것이며, 릴리스 시점보다 뒤처질 수 있습니다. +> +> 번역된 페이지가 영어 페이지와 다르면, 번역이 갱신될 때까지 **영어 페이지를 신뢰하세요**. + +영어 문서는 [`docs/en/`](https://github.com/bnbong/FastAPI-fastkit/tree/main/docs/en) 아래에 있습니다. 그 밖의 모든 언어(`docs/ko/`, `docs/ja/`, ...)는 번역 대상입니다. + +리포지토리 루트의 `CHANGELOG.md` 역시 이 영어 원본 범위에 포함됩니다. 언어별 `changelog.md` 페이지가 있더라도, 별도의 번역본 릴리스 이력을 유지하는 대신 영문 기준 changelog를 래핑하거나 진입점으로 제공하는 방식이 현재 정책입니다. + +## 언어별 번역 진행 상황 + +아래 숫자는 각 언어 디렉터리에 실제로 존재하는 마크다운 페이지 수를 영어 원본 대비로 보여줍니다. 언어 선택기에 보이는 언어 목록이 아니라, 리포지토리에 실제 체크인된 파일 수가 기준입니다 (다음 섹션에서 그 차이를 설명합니다). + +| 언어 | 상태 | 마크다운 페이지 | 비고 | +|---|---|---:|---| +| 🇬🇧 English (`en`) | ✅ 원본 | 26 / 26 | 기준이 되는 원문입니다. | +| 🇰🇷 한국어 (`ko`) | ✅ 완료 | 26 / 26 | 언어별 페이지는 모두 존재합니다. Phase 1: 최상위 + 핵심 user-guide, Phase 2: 나머지 user-guide + 모든 tutorial, Phase 3: contributing + reference. `docs/ko/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇯🇵 일본어 (`ja`) | 🔴 기본 구조만 있음 | 0 / 26 | 빌드 대상만 설정되어 있으며, 모든 페이지는 영어 원문으로 표시됩니다. | +| 🇨🇳 중국어 (`zh`) | 🔴 기본 구조만 있음 | 0 / 26 | 빌드 대상만 설정되어 있으며, 모든 페이지는 영어 원문으로 표시됩니다. | +| 🇪🇸 스페인어 (`es`) | 🔴 기본 구조만 있음 | 0 / 26 | 빌드 대상만 설정되어 있으며, 모든 페이지는 영어 원문으로 표시됩니다. | +| 🇫🇷 프랑스어 (`fr`) | 🔴 기본 구조만 있음 | 0 / 26 | 빌드 대상만 설정되어 있으며, 모든 페이지는 영어 원문으로 표시됩니다. | +| 🇩🇪 독일어 (`de`) | 🔴 기본 구조만 있음 | 0 / 26 | 빌드 대상만 설정되어 있으며, 모든 페이지는 영어 원문으로 표시됩니다. | + +*스냅샷 검증 시점: 2026-05-07. Phase 3(contributing + reference) 작업이 반영된 현재 브랜치 기준으로 `ko` 행을 다시 집계했습니다. 한국어는 언어별 페이지가 모두 존재하며, `docs/ko/changelog.md` 는 영문 기준 changelog를 그대로 가리킵니다.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: + +```console +$ for loc in en ko ja zh es fr de; do + echo "$loc: $(find docs/$loc -name '*.md' 2>/dev/null | wc -l | tr -d ' ')" + done +``` + +다시 센 결과가 표와 다르다면 표 정보가 오래된 것이므로, 직접 갱신하거나 PR / 이슈로 알려 주세요. + +범례: + +- ✅ **원본 (Source of truth)** — 문서를 작성할 때 기준으로 삼는 언어입니다. +- 🟡 **부분 번역 (Partial)** — 일부 페이지만 번역된 상태입니다. 번역되지 않은 페이지는 영어 원문이 대신 표시됩니다. +- 🔴 **기본 구조만 있음 (Skeleton)** — 언어 선택기에는 보이지만, 번역된 페이지가 아직 체크인되지 않은 상태입니다. 모든 페이지가 번역된 탐색 라벨 아래에서 영어 원문으로 표시됩니다. + +## 영어 대체 표시(fallback) 동작 방식 + +이 문서 사이트는 [`mkdocs-static-i18n`](https://github.com/ultrabug/mkdocs-static-i18n) 을 `fallback_to_default: true` 옵션으로 사용합니다. 의미는 다음과 같습니다: + +- 각 번역 언어에 대해 MkDocs는 해당 언어 디렉터리에 실제로 존재하는 페이지만 빌드해 출력합니다. +- 어떤 페이지가 그 언어에 **존재하지 않으면**, 그 페이지는 영어 버전이 대신 표시됩니다. +- 사이트 전체 언어 선택기는 각 언어의 번역 분량과 관계없이 설정된 모든 언어를 항상 보여 줍니다. 빌드가 모든 페이지에 접근 가능한 URL을 만들어 주기 때문입니다. 필요하면 영어 페이지가 대신 노출됩니다. + +따라서 언어 선택기에 보이는 🔴 기본 구조만 있음 항목은 **번역이 완료되었다는 뜻이 아니라**, 그 언어가 빌드 대상으로 설정되어 있다는 뜻일 뿐입니다. 외부 기여자가 페이지를 조금씩 번역해도 링크 구조가 깨지지 않도록 의도된 동작이지만, 그만큼 언어 선택기가 실제 번역 진행 상황보다 더 완성돼 보일 수 있습니다. + +## 문서 사이트를 보는 방법 + +- **가장 정확하고 최신의 정보**가 필요하다면 항상 영어 문서를 우선으로 보세요. +- **번역된 언어**를 볼 때는 이 페이지에서 해당 언어의 상태를 먼저 확인하세요. 🟡 또는 🔴 상태인 언어에서 아직 번역되지 않은 주제로 이동하면, 번역된 탐색 라벨 아래에서 영어 원문 페이지를 보고 있는 것입니다. + +## 기여 방법 + +현재는 **언어별로 추적 이슈를 하나 두고**, 그 안에서 **여러 단계(phase)**로 나누어 진행하는 방식을 사용합니다. 예를 들어 `ko`는 Phase 1(최상위 페이지 + 핵심 user-guide), Phase 2(나머지 user-guide + 모든 tutorial), Phase 3(contributing + reference)로 나누어 작업합니다. 각 단계는 독립된 PR로 머지되므로, 리뷰어는 언어 전체가 끝날 때까지 기다리지 않고 한 묶음씩 검토하고 승인할 수 있습니다. + +기여하고 싶다면: + +1. 작업 흐름·도구·스타일 규약은 [번역 가이드](../contributing/translation-guide.md)를 참고하세요. +2. **언어별 추적 이슈를 먼저 확인하거나 새로 여세요.** 이미 추적 이슈가 있는 언어라면 그 안에서 한 단계(또는 단계 안의 특정 페이지)를 맡아 작업이 겹치지 않도록 해 주세요. 추적 이슈가 없는 언어를 시작한다면, 어떤 페이지가 어느 단계에 속하는지 정리한 추적 이슈를 새로 열고 Phase 1부터 시작하세요. +3. **단계별로 PR 하나**를 올리는 방식을 권장합니다. "이 한 페이지만 고쳐 주세요"처럼 작은 PR, 특히 영어 원문과 어긋난 번역을 바로잡는 PR도 여전히 환영합니다. 다만 새 언어를 처음 도입하는 작업이라면, 단계 단위로 묶는 편이 용어집 결정과 교차 링크 표기를 한 묶음 안에서 일관되게 유지하기 쉽습니다. +4. `docs//` 아래에 새 파일을 추가하는 PR을 여세요. MkDocs가 자동으로 인식할 수 있도록 영어 원본과 파일 이름을 동일하게 유지하세요. +5. 언어별 changelog 페이지가 필요하다면, 번역본 release history를 새로 쓰기보다는 영문 기준 `CHANGELOG.md` 로 연결되는 래퍼 페이지로 유지하세요. +6. 새 번역이 반영되면 이 페이지의 표도 함께 갱신하세요. 페이지 상단의 재집계 스니펫으로 개수를 다시 세고, 마지막 검증 시점을 알 수 있도록 "스냅샷 검증 시점" 날짜도 함께 업데이트하세요. 언어가 아직 부분 번역 상태라면 어느 단계까지 진행되었는지 비고(Notes) 칸에 적어 주세요. + +번역 페이지가 영어 원본과 어긋나 있다는 버그 신고도 환영합니다 — 분류가 쉽도록 영어 페이지와 번역 페이지를 함께 링크해 주세요. + +## 🔴 기본 구조만 있는 언어를 그대로 두는 이유 + +두 가지 이유가 있습니다: + +1. **예측 가능한 URL 공간.** 각 언어는 이미 자신의 `//` 하위 트리에 도달 가능하므로, 번역된 페이지가 들어오는 시점에 첫날부터 안정적인 링크가 됩니다 — 이 가이드에 게시된 링크들도 마찬가지입니다. +2. **기여자 진입 장벽 낮추기.** 한 페이지를 번역하는 기여자가 새 언어 빌드를 MkDocs 설정에 추가하는 작업을 같이 할 필요 없이, 그냥 파일을 떨어뜨리기만 하면 되도록. + +🔴 기본 구조만 있는 상태로 오랫동안 기여가 없는 언어가 있다면, 빌드 대상을 계속 유지할지 다시 검토할 수 있습니다. 다만 그 결정은 별도로 추적되며, 이 현황 페이지에서 임의로 바꾸지는 **않습니다**. diff --git a/docs/ko/tutorial/async-crud-api.md b/docs/ko/tutorial/async-crud-api.md new file mode 100644 index 0000000..4dd1469 --- /dev/null +++ b/docs/ko/tutorial/async-crud-api.md @@ -0,0 +1,665 @@ +# 비동기 CRUD API 구축 + +FastAPI의 비동기 처리 능력을 활용해 고성능 CRUD API를 구축하는 방법을 배웁니다. 이 튜토리얼에서는 `fastapi-async-crud` 템플릿으로 비동기 파일 I/O와 효율적인 데이터 처리를 구현합니다. + +## 이 튜토리얼에서 배우는 내용 + +- 비동기 FastAPI 애플리케이션 이해 +- `async/await` 문법으로 비동기 CRUD 작업 수행 +- aiofiles 로 비동기 파일 처리 +- 비동기 테스트 작성과 실행 +- 성능 최적화 기법 + +## 사전 요구 사항 + +- [기본 API 서버 튜토리얼](basic-api-server.md) 완료 +- Python `async/await`의 기본 개념 이해 +- FastAPI-fastkit 설치 + +## 비동기 처리가 필요한 이유 + +동기 처리와 비동기 처리의 차이를 이해해 봅시다: + +### 동기 처리 + +```python +def process_items(): + item1 = read_file("item1.json") # 2초 대기 + item2 = read_file("item2.json") # 2초 대기 + item3 = read_file("item3.json") # 2초 대기 + return [item1, item2, item3] # 합계: 6초 +``` + +### 비동기 처리 + +```python +async def process_items(): + item1_task = read_file_async("item1.json") # 동시에 시작 + item2_task = read_file_async("item2.json") # 동시에 시작 + item3_task = read_file_async("item3.json") # 동시에 시작 + + items = await asyncio.gather(item1_task, item2_task, item3_task) + return items # 합계: 2초 +``` + +## 1단계: 비동기 CRUD 프로젝트 생성 + +`fastapi-async-crud` 템플릿으로 프로젝트를 만듭니다: + +
+ +```console +$ fastkit startdemo fastapi-async-crud +Enter the project name: async-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Asynchronous todo management API +Deploying FastAPI project using 'fastapi-async-crud' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ async-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Asynchronous todo management API │ +└──────────────┴─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ pytest-asyncio │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'async-todo-api' from 'fastapi-async-crud' has been created successfully! +``` + +
+ +## 2단계: 프로젝트 구조 분석 + +생성된 프로젝트의 주요 차이점을 살펴봅시다: + +``` +async-todo-api/ +├── src/ +│ ├── main.py # 비동기 FastAPI 애플리케이션 +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # 비동기 CRUD 엔드포인트 +│ ├── crud/ +│ │ └── items.py # 비동기 데이터 처리 로직 +│ ├── schemas/ +│ │ └── items.py # 데이터 모델 (동일) +│ ├── mocks/ +│ │ └── mock_items.json # JSON 파일 데이터베이스 +│ └── core/ +│ └── config.py # 설정 파일 +└── tests/ + ├── conftest.py # 비동기 테스트 구성 + └── test_items.py # 비동기 테스트 케이스 +``` + +### 주요 차이점 + +1. **aiofiles**: 비동기 파일 I/O 처리 +2. **pytest-asyncio**: 비동기 테스트 지원 +3. **async/await 패턴**: 모든 CRUD 작업이 비동기로 구현됨 + +## 3단계: 비동기 CRUD 로직 이해 + +### 비동기 데이터 처리 (`src/crud/items.py`) + +```python +import json +import asyncio +from typing import List, Optional +from aiofiles import open as aio_open +from pathlib import Path + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class AsyncItemCRUD: + def __init__(self, data_file: str = "src/mocks/mock_items.json"): + self.data_file = Path(data_file) + + async def _read_data(self) -> List[dict]: + """Asynchronously read data from JSON file""" + try: + async with aio_open(self.data_file, 'r', encoding='utf-8') as f: + content = await f.read() + return json.loads(content) + except FileNotFoundError: + return [] + + async def _write_data(self, data: List[dict]) -> None: + """Asynchronously write data to JSON file""" + async with aio_open(self.data_file, 'w', encoding='utf-8') as f: + await f.write(json.dumps(data, indent=2, ensure_ascii=False)) + + async def get_items(self) -> List[Item]: + """Retrieve all items (asynchronous)""" + data = await self._read_data() + return [Item(**item) for item in data] + + async def get_item(self, item_id: int) -> Optional[Item]: + """Retrieve specific item (asynchronous)""" + data = await self._read_data() + item_data = next((item for item in data if item["id"] == item_id), None) + return Item(**item_data) if item_data else None + + async def create_item(self, item: ItemCreate) -> Item: + """Create new item (asynchronous)""" + data = await self._read_data() + new_id = max([item["id"] for item in data], default=0) + 1 + + new_item = Item(id=new_id, **item.dict()) + data.append(new_item.dict()) + + await self._write_data(data) + return new_item + + async def update_item(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """Update item (asynchronous)""" + data = await self._read_data() + + for i, item in enumerate(data): + if item["id"] == item_id: + update_data = item_update.dict(exclude_unset=True) + data[i].update(update_data) + await self._write_data(data) + return Item(**data[i]) + + return None + + async def delete_item(self, item_id: int) -> bool: + """Delete item (asynchronous)""" + data = await self._read_data() + original_length = len(data) + + data = [item for item in data if item["id"] != item_id] + + if len(data) < original_length: + await self._write_data(data) + return True + + return False +``` + +### 비동기 API 엔드포인트 (`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status + +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import AsyncItemCRUD + +router = APIRouter() +crud = AsyncItemCRUD() + +@router.get("/", response_model=List[Item]) +async def read_items(): + """Retrieve all items (asynchronous)""" + return await crud.get_items() + +@router.get("/{item_id}", response_model=Item) +async def read_item(item_id: int): + """Retrieve specific item (asynchronous)""" + item = await crud.get_item(item_id) + if item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return item + +@router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: ItemCreate): + """Create new item (asynchronous)""" + return await crud.create_item(item) + +@router.put("/{item_id}", response_model=Item) +async def update_item(item_id: int, item_update: ItemUpdate): + """Update item (asynchronous)""" + updated_item = await crud.update_item(item_id, item_update) + if updated_item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item(item_id: int): + """Delete item (asynchronous)""" + deleted = await crud.delete_item(item_id) + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) +``` + +## 4단계: 서버 실행과 테스트 + +프로젝트 디렉터리로 이동해 서버를 실행합니다: + +
+ +```console +$ cd async-todo-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +### 성능 테스트 + +비동기 처리의 성능을 검증해 봅시다. 여러 요청을 동시에 보내 보세요: + +**동시 요청 테스트 (Python 스크립트)** + +```python +import asyncio +import aiohttp +import time + +async def create_item(session, item_data): + async with session.post("http://127.0.0.1:8000/items/", json=item_data) as response: + return await response.json() + +async def test_concurrent_requests(): + start_time = time.time() + + items_to_create = [ + {"name": f"Item {i}", "description": f"Description {i}", "price": i * 10, "tax": i} + for i in range(1, 11) # 10 개 item 을 동시에 생성 + ] + + async with aiohttp.ClientSession() as session: + tasks = [create_item(session, item) for item in items_to_create] + results = await asyncio.gather(*tasks) + + end_time = time.time() + print(f"Created 10 items in: {end_time - start_time:.2f} seconds") + print(f"Number of items created: {len(results)}") + +# 실행 +# asyncio.run(test_concurrent_requests()) +``` + +## 5단계: 비동기 테스트 작성 + +### 테스트 구성 (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from src.main import app + +@pytest.fixture(scope="session") +def event_loop(): + """Event loop configuration""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture +async def async_client(): + """Asynchronous test client""" + async with AsyncClient(app=app, base_url="http://test") as client: + yield client +``` + +### 비동기 테스트 케이스 (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_item_async(async_client: AsyncClient): + """Asynchronous item creation test""" + item_data = { + "name": "Test Item", + "description": "Item for asynchronous testing", + "price": 100.0, + "tax": 10.0 + } + + response = await async_client.post("/items/", json=item_data) + + assert response.status_code == 201 + data = response.json() + assert data["name"] == item_data["name"] + assert data["price"] == item_data["price"] + assert "id" in data + +@pytest.mark.asyncio +async def test_read_items_async(async_client: AsyncClient): + """Asynchronous item list retrieval test""" + response = await async_client.get("/items/") + + assert response.status_code == 200 + items = response.json() + assert isinstance(items, list) + +@pytest.mark.asyncio +async def test_concurrent_operations(async_client: AsyncClient): + """Concurrent operations test""" + import asyncio + + # 여러 item 을 동시에 생성 + tasks = [] + for i in range(5): + item_data = { + "name": f"ConcurrentItem{i}", + "description": f"Description{i}", + "price": i * 10, + "tax": i + } + task = async_client.post("/items/", json=item_data) + tasks.append(task) + + responses = await asyncio.gather(*tasks) + + # 모든 요청이 성공했는지 확인 + for response in responses: + assert response.status_code == 201 + + # 생성된 item 확인 + response = await async_client.get("/items/") + items = response.json() + assert len(items) >= 5 +``` + +### 테스트 실행 + +
+ +```console +$ pytest tests/ -v --asyncio-mode=auto +======================== test session starts ======================== +collected 8 items + +tests/test_items.py::test_create_item_async PASSED [ 12%] +tests/test_items.py::test_read_items_async PASSED [ 25%] +tests/test_items.py::test_read_item_async PASSED [ 37%] +tests/test_items.py::test_update_item_async PASSED [ 50%] +tests/test_items.py::test_delete_item_async PASSED [ 62%] +tests/test_items.py::test_concurrent_operations PASSED [ 75%] +tests/test_items.py::test_item_not_found_async PASSED [ 87%] +tests/test_items.py::test_invalid_item_data_async PASSED [100%] + +======================== 8 passed in 0.24s ======================== +``` + +
+ +## 6단계: 성능 모니터링과 최적화 + +### 응답 시간 측정 미들웨어 추가 + +`src/main.py` 에 성능 모니터링을 추가해 봅시다: + +```python +import time +from fastapi import FastAPI, Request +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + """Add request processing time to headers""" + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + +app.include_router(api_router) + +@app.get("/") +async def read_root(): + return {"message": "Welcome to the Asynchronous Todo API!"} +``` + +### 비동기 배치 처리 구현 + +여러 item 을 한 번에 처리하는 배치 엔드포인트를 추가해 봅시다: + +```python +# src/api/routes/items.py 에 추가 + +@router.post("/batch", response_model=List[Item]) +async def create_items_batch(items: List[ItemCreate]): + """Create multiple items concurrently (batch processing)""" + import asyncio + + # 모든 item 생성 작업을 동시에 실행 + tasks = [crud.create_item(item) for item in items] + created_items = await asyncio.gather(*tasks) + + return created_items + +@router.get("/batch/{item_ids}") +async def read_items_batch(item_ids: str): + """Retrieve multiple items concurrently (batch processing)""" + import asyncio + + # 쉼표로 구분된 ID 파싱 + ids = [int(id.strip()) for id in item_ids.split(",")] + + # 모든 item 조회 작업을 동시에 실행 + tasks = [crud.get_item(item_id) for item_id in ids] + items = await asyncio.gather(*tasks) + + # None 이 아닌 item 만 반환 + return [item for item in items if item is not None] +``` + +### 배치 처리 테스트 + +
+ +```console +# 배치 생성 테스트 +$ curl -X POST "http://127.0.0.1:8000/items/batch" \ + -H "Content-Type: application/json" \ + -d '[ + {"name": "Item1", "description": "Description1", "price": 10.0, "tax": 1.0}, + {"name": "Item2", "description": "Description2", "price": 20.0, "tax": 2.0}, + {"name": "Item3", "description": "Description3", "price": 30.0, "tax": 3.0} + ]' + +# 배치 조회 테스트 +$ curl -X GET "http://127.0.0.1:8000/items/batch/1,2,3" +``` + +
+ +## 7단계: 고급 비동기 패턴 + +### 레이트 제한 구현 + +```python +import asyncio +from collections import defaultdict +from fastapi import HTTPException, Request +from datetime import datetime, timedelta + +class AsyncRateLimiter: + def __init__(self, max_requests: int = 100, window_seconds: int = 60): + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = defaultdict(list) + + async def is_allowed(self, client_ip: str) -> bool: + now = datetime.now() + cutoff = now - timedelta(seconds=self.window_seconds) + + # 오래된 요청 기록 제거 + self.requests[client_ip] = [ + req_time for req_time in self.requests[client_ip] + if req_time > cutoff + ] + + # 현재 요청 수 확인 + if len(self.requests[client_ip]) >= self.max_requests: + return False + + # 현재 요청 기록 추가 + self.requests[client_ip].append(now) + return True + +# 전역 레이트 리미터 인스턴스 +rate_limiter = AsyncRateLimiter() + +@app.middleware("http") +async def rate_limit_middleware(request: Request, call_next): + client_ip = request.client.host + + if not await rate_limiter.is_allowed(client_ip): + raise HTTPException( + status_code=429, + detail="Too many requests" + ) + + response = await call_next(request) + return response +``` + +### 비동기 캐싱 구현 + +```python +import asyncio +from typing import Optional, Any +from datetime import datetime, timedelta + +class AsyncCache: + def __init__(self): + self._cache = {} + self._expiry = {} + + async def get(self, key: str) -> Optional[Any]: + # 만료된 항목 제거 + if key in self._expiry and datetime.now() > self._expiry[key]: + del self._cache[key] + del self._expiry[key] + return None + + return self._cache.get(key) + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + self._cache[key] = value + self._expiry[key] = datetime.now() + timedelta(seconds=ttl_seconds) + + async def delete(self, key: str): + self._cache.pop(key, None) + self._expiry.pop(key, None) + +# 전역 캐시 인스턴스 +cache = AsyncCache() + +# CRUD 메서드를 캐시 사용하도록 수정 +async def get_items_cached(self) -> List[Item]: + """Retrieve items using cache""" + cache_key = "all_items" + cached_items = await cache.get(cache_key) + + if cached_items: + return cached_items + + # 캐시가 없으면 파일에서 읽기 + items = await self.get_items() + await cache.set(cache_key, items, ttl_seconds=60) # 1분 캐시 + + return items +``` + +## 8단계: 프로덕션 고려 사항 + +### 커넥션 풀 관리 + +```python +# src/core/config.py 에 추가 +class Settings(BaseSettings): + # ... 기존 설정 ... + + # 비동기 처리 관련 설정 + MAX_CONCURRENT_REQUESTS: int = 100 + REQUEST_TIMEOUT: int = 30 + CONNECTION_POOL_SIZE: int = 20 + +settings = Settings() +``` + +### 에러 처리 개선 + +```python +import logging +from fastapi import HTTPException +from typing import Union + +logger = logging.getLogger(__name__) + +async def safe_async_operation(operation, *args, **kwargs) -> Union[Any, None]: + """Execute safe asynchronous operation""" + try: + return await operation(*args, **kwargs) + except asyncio.TimeoutError: + logger.error(f"Timeout in {operation.__name__}") + raise HTTPException(status_code=504, detail="Request timeout") + except Exception as e: + logger.error(f"Error in {operation.__name__}: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + +# 사용 예 +@router.get("/safe/{item_id}") +async def read_item_safe(item_id: int): + return await safe_async_operation(crud.get_item, item_id) +``` + +## 다음 단계 + +비동기 CRUD API 구축을 마쳤습니다! 다음으로 시도해 볼 만한 것들: + +1. **[데이터베이스 통합](database-integration.md)** — 비동기 SQLAlchemy와 PostgreSQL 사용 +2. **[Docker 컨테이너화](docker-deployment.md)** — 비동기 애플리케이션을 컨테이너화 +3. **[커스텀 응답 처리](custom-response-handling.md)** — 고급 응답 형식과 에러 처리 + + + +## 요약 + +이 튜토리얼에서는 비동기 FastAPI로 다음 작업을 진행했습니다: + +- ✅ 비동기 CRUD 작업 구현 +- ✅ aiofiles 로 파일 I/O 최적화 +- ✅ 동시 요청 처리와 성능 테스트 +- ✅ 비동기 테스트 작성과 실행 +- ✅ 배치 처리와 고급 비동기 패턴 구현 +- ✅ 프로덕션 고려 사항 (캐싱, 에러 처리, 커넥션 관리) 반영 + +비동기 처리에 익숙해지면 고성능 API 서버를 훨씬 자신 있게 구축할 수 있습니다! diff --git a/docs/ko/tutorial/basic-api-server.md b/docs/ko/tutorial/basic-api-server.md new file mode 100644 index 0000000..c06bd42 --- /dev/null +++ b/docs/ko/tutorial/basic-api-server.md @@ -0,0 +1,398 @@ +# 기본 API 서버 구축 + +FastAPI-fastkit으로 간단한 REST API 서버를 빠르게 만드는 방법을 배웁니다. 이 튜토리얼은 FastAPI 입문자에게 적합하며, 기본 CRUD API를 직접 만들어 보는 과정을 다룹니다. + +## 이 튜토리얼에서 배우는 내용 + +- `fastkit startdemo` 명령으로 기본 API 서버 만들기 +- FastAPI 프로젝트 구조 이해 +- 기본 CRUD 엔드포인트 사용 +- API 테스트와 문서화 +- 프로젝트 확장 방법 + +## 사전 요구 사항 + +- Python 3.12 이상 설치 +- FastAPI-fastkit 설치 (`pip install fastapi-fastkit`) +- Python 기초 지식 + +## 1단계: 기본 API 프로젝트 생성 + +`fastapi-default` 템플릿으로 기본 API 서버를 만들어 봅시다. + +
+ +```console +$ fastkit startdemo fastapi-default +Enter the project name: my-first-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: My first FastAPI server +Deploying FastAPI project using 'fastapi-default' template + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ My first FastAPI server │ +└──────────────┴────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'my-first-api' from 'fastapi-default' has been created successfully! +``` + +
+ +## 2단계: 생성된 프로젝트 구조 이해 + +생성된 프로젝트 구조를 살펴봅시다: + +``` +my-first-api/ +├── README.md # 프로젝트 문서 +├── requirements.txt # 의존성 패키지 목록 +├── setup.py # 패키지 구성 +├── scripts/ +│ └── run-server.sh # 서버 실행 스크립트 +├── src/ # 메인 소스 코드 +│ ├── main.py # FastAPI 애플리케이션 진입점 +│ ├── core/ +│ │ └── config.py # 설정 관리 +│ ├── api/ +│ │ ├── api.py # API 라우터 모음 +│ │ └── routes/ +│ │ └── items.py # item 관련 엔드포인트 +│ ├── schemas/ +│ │ └── items.py # 데이터 모델 정의 +│ ├── crud/ +│ │ └── items.py # 데이터 처리 로직 +│ └── mocks/ +│ └── mock_items.json # 테스트 데이터 +└── tests/ # 테스트 코드 + ├── __init__.py + ├── conftest.py + └── test_items.py +``` + +### 주요 파일 설명 + +- **`src/main.py`**: FastAPI 애플리케이션 진입점 +- **`src/api/routes/items.py`**: item 관련 API 엔드포인트 정의 +- **`src/schemas/items.py`**: 요청 / 응답 데이터 구조 정의 +- **`src/crud/items.py`**: 데이터베이스 작업 로직 +- **`src/mocks/mock_items.json`**: 개발용 샘플 데이터 + +## 3단계: 서버 실행 + +생성된 프로젝트 디렉터리로 이동해 서버를 실행합니다. + +
+ +```console +$ cd my-first-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Will watch for changes in these directories: ['/path/to/my-first-api'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +서버가 정상적으로 실행되면 브라우저에서 다음 URL 들에 접속할 수 있습니다: + +- **API 서버**: http://127.0.0.1:8000 +- **Swagger UI 문서**: http://127.0.0.1:8000/docs +- **ReDoc 문서**: http://127.0.0.1:8000/redoc + +## 4단계: API 엔드포인트 살펴보기 + +생성된 API는 기본적으로 다음 엔드포인트를 제공합니다: + +| 메서드 | 엔드포인트 | 설명 | +|--------|----------|-------------| +| GET | `/items/` | 모든 item 조회 | +| GET | `/items/{item_id}` | 특정 item 조회 | +| POST | `/items/` | 새 item 생성 | +| PUT | `/items/{item_id}` | item 갱신 | +| DELETE | `/items/{item_id}` | item 삭제 | + +### API 테스트 + +**1. 모든 item 조회** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/" +[ + { + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 + }, + { + "id": 2, + "name": "Mouse", + "description": "Wireless mouse", + "price": 29.99, + "tax": 2.99 + } +] +``` + +
+ +**2. 새 item 생성** + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.00, + "tax": 15.00 + }' + +{ + "id": 3, + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.0, + "tax": 15.0 +} +``` + +
+ +**3. 특정 item 조회** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/1" +{ + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 +} +``` + +
+ +## 5단계: Swagger UI 로 API 테스트 + +브라우저에서 http://127.0.0.1:8000/docs 로 이동하면 자동 생성된 API 문서를 볼 수 있습니다. + +Swagger UI 로 할 수 있는 일: + +1. **API 엔드포인트 보기**: 사용 가능한 모든 엔드포인트를 시각적으로 확인 +2. **요청 / 응답 스키마 확인**: 각 엔드포인트의 입출력 형식 확인 +3. **API 직접 테스트**: "Try it out" 버튼으로 실제 API 호출 수행 +4. **예시 데이터 보기**: 각 엔드포인트의 예시 요청 / 응답 데이터 확인 + +### Swagger UI 사용 방법 + +1. `/items/` GET 엔드포인트 클릭 +2. "Try it out" 버튼 클릭 +3. "Execute" 버튼 클릭 +4. 서버 응답 확인 + +## 6단계: 코드 구조 이해 + +### 메인 애플리케이션 (`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, + description=settings.DESCRIPTION, +) + +app.include_router(api_router) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +### Item 스키마 (`src/schemas/items.py`) + +```python +from pydantic import BaseModel +from typing import Optional + +class ItemBase(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + +class ItemCreate(ItemBase): + pass + +class ItemUpdate(ItemBase): + name: Optional[str] = None + price: Optional[float] = None + +class Item(ItemBase): + id: int + + class Config: + from_attributes = True +``` + +### CRUD 로직 (`src/crud/items.py`) + +```python +from typing import List, Optional +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self): + self.items: List[Item] = [] + self.next_id = 1 + + def create_item(self, item: ItemCreate) -> Item: + new_item = Item(id=self.next_id, **item.dict()) + self.items.append(new_item) + self.next_id += 1 + return new_item + + def get_items(self) -> List[Item]: + return self.items + + def get_item(self, item_id: int) -> Optional[Item]: + return next((item for item in self.items if item.id == item_id), None) +``` + +## 7단계: 프로젝트 확장 + +### 새 라우트 추가 + +`fastkit addroute` 명령으로 새 엔드포인트를 추가할 수 있습니다: + +
+ +```console +$ fastkit addroute user + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ user │ +│ Target Directory │ /path/to/my-first-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'user' to the current project? [Y/n]: y + +✨ Successfully added new route 'user' to the current project! +``` + +
+ +이 명령은 다음 파일들을 만듭니다: + +- `src/api/routes/user.py` - 사용자 관련 엔드포인트 +- `src/schemas/user.py` - 사용자 데이터 모델 +- `src/crud/user.py` - 사용자 데이터 처리 로직 + +### 환경 설정 커스터마이즈 + +`src/core/config.py` 를 수정해 프로젝트 설정을 변경할 수 있습니다: + +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "My First API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "My first FastAPI server" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## 8단계: 테스트 실행 + +프로젝트에는 기본 테스트가 포함돼 있습니다: + +
+ +```console +$ pytest tests/ -v +======================== test session starts ======================== +collected 4 items + +tests/test_items.py::test_create_item PASSED [ 25%] +tests/test_items.py::test_read_items PASSED [ 50%] +tests/test_items.py::test_read_item PASSED [ 75%] +tests/test_items.py::test_update_item PASSED [100%] + +======================== 4 passed in 0.15s ======================== +``` + +
+ +## 다음 단계 + +기본 API 서버 구축을 마쳤습니다! 다음으로 시도해 볼 만한 것들: + +1. **[비동기 CRUD API 구축](async-crud-api.md)** — 더 복잡한 비동기 처리 학습 +2. **[데이터베이스 통합](database-integration.md)** — PostgreSQL과 SQLAlchemy 사용 +3. **[Docker 컨테이너화](docker-deployment.md)** — 프로덕션 배포 준비 +4. **[커스텀 응답 처리](custom-response-handling.md)** — 고급 응답 형식 구성 + +## 문제 해결 + +### 자주 마주치는 문제 + +**Q: 서버가 시작되지 않습니다** +A: 가상 환경이 활성화돼 있고 의존성이 제대로 설치됐는지 확인하세요. + +**Q: API 엔드포인트에 접속이 안 됩니다** +A: 서버가 정상 동작 중이고 포트 번호 (기본값: 8000) 가 맞는지 확인하세요. + +**Q: Swagger UI에 API가 보이지 않습니다** +A: 라우터가 `src/main.py` 에 제대로 포함됐는지 확인하세요. + +## 요약 + +이 튜토리얼에서는 FastAPI-fastkit으로 다음 작업을 진행했습니다: + +- ✅ 기본 FastAPI 프로젝트 생성 +- ✅ 프로젝트 구조 이해 +- ✅ CRUD API 엔드포인트 사용 +- ✅ API 문서화 및 테스트 +- ✅ 프로젝트 확장 방법 + +이제 FastAPI의 기본을 익혔으니, 더 복잡한 프로젝트에도 도전해 보세요! diff --git a/docs/ko/tutorial/custom-response-handling.md b/docs/ko/tutorial/custom-response-handling.md new file mode 100644 index 0000000..ee8ce5b --- /dev/null +++ b/docs/ko/tutorial/custom-response-handling.md @@ -0,0 +1,1393 @@ +# 커스텀 응답 처리와 고급 API 설계 + +FastAPI의 고급 기능을 활용해 일관된 응답 형식, 에러 처리, 페이지네이션, 그리고 OpenAPI 문서 맞춤 구성을 구현하는 방법을 배웁니다. `fastapi-custom-response` 템플릿으로 엔터프라이즈급 API 설계 패턴을 구현합니다. + +## 이 튜토리얼에서 배우는 내용 + +- 표준화된 API 응답 형식 설계 +- 전역 예외 처리와 커스텀 에러 응답 +- 페이지네이션 시스템 구현 +- 필터링과 정렬 기능 +- OpenAPI 문서 커스터마이즈 +- API 버전 관리 +- 응답 캐싱과 최적화 + +## 사전 요구 사항 + +- [Docker 컨테이너화 튜토리얼](docker-deployment.md) 완료 +- REST API 설계 원칙에 대한 이해 +- HTTP 상태 코드 지식 +- OpenAPI / Swagger의 기본 개념 + +## 표준화된 API 응답이 중요한 이유 + +### 일관성 없는 응답 vs 표준화된 응답 + +**문제가 있는 응답 형식:** +```json +// 성공 +{"id": 1, "name": "item"} + +// 에러 +{"detail": "Not found"} + +// 목록 조회 +[{"id": 1}, {"id": 2}] +``` + +**표준화된 응답 형식:** +```json +// 성공 +{ + "success": true, + "data": {"id": 1, "name": "item"}, + "message": "Item retrieved successfully", + "timestamp": "2024-01-01T12:00:00Z" +} + +// 에러 +{ + "success": false, + "error": { + "code": "ITEM_NOT_FOUND", + "message": "Item not found", + "details": {"item_id": 123} + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +## 1단계: 커스텀 응답 프로젝트 생성 + +`fastapi-custom-response` 템플릿으로 프로젝트를 만듭니다: + +
+ +```console +$ fastkit startdemo fastapi-custom-response +Enter the project name: advanced-api-server +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: API server with advanced response handling +Deploying FastAPI project using 'fastapi-custom-response' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ advanced-api-server │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ API server with advanced response handling │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ python-multipart │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'advanced-api-server' from 'fastapi-custom-response' has been created successfully! +``` + +
+ +## 2단계: 프로젝트 구조 분석 + +생성된 프로젝트의 고급 기능을 살펴봅시다: + +``` +advanced-api-server/ +├── src/ +│ ├── main.py # FastAPI 애플리케이션 +│ ├── schemas/ +│ │ ├── base.py # 기본 응답 스키마 +│ │ ├── items.py # Item 스키마 +│ │ └── responses.py # 응답 형식 정의 +│ ├── helper/ +│ │ ├── exceptions.py # 커스텀 예외 클래스 +│ │ └── pagination.py # 페이지네이션 헬퍼 +│ ├── utils/ +│ │ ├── responses.py # 응답 유틸리티 +│ │ └── documents.py # OpenAPI 문서 커스터마이즈 +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # 고급 API 엔드포인트 +│ ├── crud/ +│ │ └── items.py # CRUD 로직 +│ └── core/ +│ └── config.py # 설정 +└── tests/ + └── test_responses.py # 응답 형식 테스트 +``` + +## 3단계: 표준화된 응답 스키마 구현 + +### 기본 응답 스키마 (`src/schemas/base.py`) + +```python +from typing import Generic, TypeVar, Optional, Any, Dict, List +from pydantic import BaseModel, Field +from datetime import datetime +from enum import Enum + +T = TypeVar('T') + +class ResponseStatus(str, Enum): + """Response status""" + SUCCESS = "success" + ERROR = "error" + WARNING = "warning" + +class ErrorDetail(BaseModel): + """Error detail information""" + code: str = Field(..., description="Error code") + message: str = Field(..., description="Error message") + field: Optional[str] = Field(None, description="Field where error occurred") + details: Optional[Dict[str, Any]] = Field(None, description="Additional error information") + +class BaseResponse(BaseModel, Generic[T]): + """Base response format""" + success: bool = Field(..., description="Request success status") + status: ResponseStatus = Field(..., description="Response status") + data: Optional[T] = Field(None, description="Response data") + message: Optional[str] = Field(None, description="Response message") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class ErrorResponse(BaseModel): + """Error response format""" + success: bool = Field(False, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="Response status") + error: ErrorDetail = Field(..., description="Error information") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class PaginationMeta(BaseModel): + """Pagination metadata""" + page: int = Field(..., ge=1, description="Current page") + size: int = Field(..., ge=1, le=100, description="Page size") + total: int = Field(..., ge=0, description="Total number of items") + pages: int = Field(..., ge=0, description="Total number of pages") + has_next: bool = Field(..., description="Whether next page exists") + has_prev: bool = Field(..., description="Whether previous page exists") + +class PaginatedResponse(BaseModel, Generic[T]): + """Paginated response""" + success: bool = Field(True, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.SUCCESS, description="Response status") + data: List[T] = Field(..., description="Data list") + meta: PaginationMeta = Field(..., description="Pagination information") + message: Optional[str] = Field(None, description="Response message") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response time") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class ValidationErrorDetail(BaseModel): + """Validation error detail""" + field: str = Field(..., description="Validation failed field") + message: str = Field(..., description="Error message") + invalid_value: Any = Field(..., description="Invalid value") + +class ValidationErrorResponse(BaseModel): + """Validation error response""" + success: bool = Field(False, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="Response status") + error: ErrorDetail = Field(..., description="Error information") + validation_errors: List[ValidationErrorDetail] = Field(..., description="Validation error list") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response time") + request_id: Optional[str] = Field(None, description="Request tracking ID") +``` + +### 응답 유틸리티 함수 (`src/utils/responses.py`) + +```python +from typing import Any, Optional, List, TypeVar +from fastapi import Request +from fastapi.responses import JSONResponse +import uuid + +from src.schemas.base import ( + BaseResponse, ErrorResponse, PaginatedResponse, + ResponseStatus, ErrorDetail, PaginationMeta +) + +T = TypeVar('T') + +def generate_request_id() -> str: + """Generate request tracking ID""" + return str(uuid.uuid4()) + +def success_response( + data: Any = None, + message: Optional[str] = None, + request_id: Optional[str] = None, + status_code: int = 200 +) -> JSONResponse: + """Generate success response""" + response_data = BaseResponse[Any]( + success=True, + status=ResponseStatus.SUCCESS, + data=data, + message=message or "Request processed successfully", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def error_response( + error_code: str, + error_message: str, + details: Optional[dict] = None, + status_code: int = 400, + request_id: Optional[str] = None +) -> JSONResponse: + """Generate error response""" + error_detail = ErrorDetail( + code=error_code, + message=error_message, + details=details + ) + + response_data = ErrorResponse( + error=error_detail, + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def paginated_response( + data: List[T], + page: int, + size: int, + total: int, + message: Optional[str] = None, + request_id: Optional[str] = None +) -> JSONResponse: + """Generate paginated response""" + pages = (total + size - 1) // size # 올림 계산 + has_next = page < pages + has_prev = page > 1 + + meta = PaginationMeta( + page=page, + size=size, + total=total, + pages=pages, + has_next=has_next, + has_prev=has_prev + ) + + response_data = PaginatedResponse[T]( + data=data, + meta=meta, + message=message or f"Page {page}/{pages} data retrieved", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=200, + content=response_data.dict(exclude_none=True) + ) + +class ResponseHelper: + """Response helper class""" + + @staticmethod + def created(data: Any, message: str = "Resource created successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=201) + + @staticmethod + def updated(data: Any, message: str = "Resource updated successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=200) + + @staticmethod + def deleted(message: str = "Resource deleted successfully") -> JSONResponse: + return success_response(data=None, message=message, status_code=204) + + @staticmethod + def not_found(resource: str = "Resource") -> JSONResponse: + return error_response( + error_code="RESOURCE_NOT_FOUND", + error_message=f"{resource} not found", + status_code=404 + ) + + @staticmethod + def bad_request(message: str = "Bad request") -> JSONResponse: + return error_response( + error_code="BAD_REQUEST", + error_message=message, + status_code=400 + ) + + @staticmethod + def unauthorized(message: str = "Authentication required") -> JSONResponse: + return error_response( + error_code="UNAUTHORIZED", + error_message=message, + status_code=401 + ) + + @staticmethod + def forbidden(message: str = "Permission denied") -> JSONResponse: + return error_response( + error_code="FORBIDDEN", + error_message=message, + status_code=403 + ) + + @staticmethod + def server_error(message: str = "Server internal error occurred") -> JSONResponse: + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message=message, + status_code=500 + ) +``` + +## 4단계: 커스텀 예외 처리 시스템 + +### 커스텀 예외 클래스 (`src/helper/exceptions.py`) + +```python +from typing import Optional, Dict, Any +from fastapi import HTTPException + +class BaseAPIException(HTTPException): + """Base API exception class""" + + def __init__( + self, + error_code: str, + message: str, + status_code: int = 400, + details: Optional[Dict[str, Any]] = None + ): + self.error_code = error_code + self.message = message + self.details = details or {} + super().__init__(status_code=status_code, detail=message) + +class ValidationException(BaseAPIException): + """Validation exception""" + + def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict] = None): + super().__init__( + error_code="VALIDATION_ERROR", + message=message, + status_code=422, + details=details or {"field": field} if field else None + ) + +class ResourceNotFoundException(BaseAPIException): + """Resource not found exception""" + + def __init__(self, resource: str, resource_id: Any): + super().__init__( + error_code="RESOURCE_NOT_FOUND", + message=f"{resource}(ID: {resource_id}) not found", + status_code=404, + details={"resource": resource, "id": resource_id} + ) + +class DuplicateResourceException(BaseAPIException): + """Duplicate resource exception""" + + def __init__(self, resource: str, field: str, value: Any): + super().__init__( + error_code="DUPLICATE_RESOURCE", + message=f"{resource} {field} '{value}' already exists", + status_code=409, + details={"resource": resource, "field": field, "value": value} + ) + +class BusinessLogicException(BaseAPIException): + """Business logic exception""" + + def __init__(self, message: str, error_code: str = "BUSINESS_LOGIC_ERROR"): + super().__init__( + error_code=error_code, + message=message, + status_code=422 + ) + +class RateLimitException(BaseAPIException): + """Request limit exception""" + + def __init__(self, retry_after: int = 60): + super().__init__( + error_code="RATE_LIMIT_EXCEEDED", + message="Request limit exceeded. Please try again later", + status_code=429, + details={"retry_after": retry_after} + ) + +class AuthenticationException(BaseAPIException): + """Authentication exception""" + + def __init__(self, message: str = "Authentication required"): + super().__init__( + error_code="AUTHENTICATION_REQUIRED", + message=message, + status_code=401 + ) + +class AuthorizationException(BaseAPIException): + """Authorization exception""" + + def __init__(self, message: str = "Permission denied"): + super().__init__( + error_code="INSUFFICIENT_PERMISSIONS", + message=message, + status_code=403 + ) +``` + +### 전역 예외 핸들러 (`src/main.py`) + +```python +from fastapi import FastAPI, Request, status +from fastapi.exceptions import RequestValidationError, HTTPException +from fastapi.responses import JSONResponse +from pydantic import ValidationError +import logging +import traceback + +from src.helper.exceptions import BaseAPIException +from src.utils.responses import error_response, generate_request_id +from src.schemas.base import ValidationErrorDetail, ValidationErrorResponse + +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Advanced API Server", + description="API server with advanced response handling", + version="1.0.0" +) + +@app.exception_handler(BaseAPIException) +async def custom_api_exception_handler(request: Request, exc: BaseAPIException): + """Custom API exception handler""" + request_id = generate_request_id() + + logger.error( + f"API Exception: {exc.error_code} - {exc.message}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "details": exc.details + } + ) + + return error_response( + error_code=exc.error_code, + error_message=exc.message, + details=exc.details, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Pydantic validation exception handler""" + request_id = generate_request_id() + + validation_errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + validation_errors.append( + ValidationErrorDetail( + field=field, + message=error["msg"], + invalid_value=error.get("input", "") + ) + ) + + error_response_data = ValidationErrorResponse( + error={ + "code": "VALIDATION_ERROR", + "message": "Input data validation failed", + "details": {"error_count": len(validation_errors)} + }, + validation_errors=validation_errors, + request_id=request_id + ) + + logger.warning( + f"Validation Error: {len(validation_errors)} validation errors", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "errors": [err.dict() for err in validation_errors] + } + ) + + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=error_response_data.dict(exclude_none=True) + ) + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """HTTP exception handler""" + request_id = generate_request_id() + + error_code_map = { + 400: "BAD_REQUEST", + 401: "UNAUTHORIZED", + 403: "FORBIDDEN", + 404: "NOT_FOUND", + 405: "METHOD_NOT_ALLOWED", + 500: "INTERNAL_SERVER_ERROR" + } + + error_code = error_code_map.get(exc.status_code, "HTTP_ERROR") + + return error_response( + error_code=error_code, + error_message=exc.detail, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """General exception handler""" + request_id = generate_request_id() + + logger.error( + f"Unhandled Exception: {type(exc).__name__} - {str(exc)}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "traceback": traceback.format_exc() + } + ) + + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message="Unexpected error occurred", + status_code=500, + request_id=request_id + ) +``` + +## 5단계: 고급 페이지네이션 시스템 + +### 페이지네이션 헬퍼 (`src/helper/pagination.py`) + +```python +from typing import List, Optional, Any, Dict, Callable +from pydantic import BaseModel, Field +from fastapi import Query +from enum import Enum + +class SortOrder(str, Enum): + """Sort order""" + ASC = "asc" + DESC = "desc" + +class PaginationParams(BaseModel): + """Pagination parameters""" + page: int = Field(1, ge=1, description="Page number") + size: int = Field(20, ge=1, le=100, description="Page size") + sort_by: Optional[str] = Field(None, description="Sort field") + sort_order: SortOrder = Field(SortOrder.ASC, description="Sort order") + +class FilterParams(BaseModel): + """Filtering parameters""" + search: Optional[str] = Field(None, description="Search term") + category: Optional[str] = Field(None, description="Category") + status: Optional[str] = Field(None, description="Status") + date_from: Optional[str] = Field(None, description="Start date (YYYY-MM-DD)") + date_to: Optional[str] = Field(None, description="End date (YYYY-MM-DD)") + +def pagination_params( + page: int = Query(1, ge=1, description="Page number"), + size: int = Query(20, ge=1, le=100, description="Page size"), + sort_by: Optional[str] = Query(None, description="Sort field"), + sort_order: SortOrder = Query(SortOrder.ASC, description="Sort order") +) -> PaginationParams: + """Pagination parameters dependency""" + return PaginationParams( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order + ) + +def filter_params( + search: Optional[str] = Query(None, description="Search term"), + category: Optional[str] = Query(None, description="Category"), + status: Optional[str] = Query(None, description="Status"), + date_from: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"), + date_to: Optional[str] = Query(None, description="End date (YYYY-MM-DD)") +) -> FilterParams: + """Filtering parameters dependency""" + return FilterParams( + search=search, + category=category, + status=status, + date_from=date_from, + date_to=date_to + ) + +class AdvancedPaginator: + """Advanced pagination class""" + + def __init__(self, data: List[Any], pagination: PaginationParams, filters: FilterParams): + self.data = data + self.pagination = pagination + self.filters = filters + self.filtered_data = self._apply_filters() + self.sorted_data = self._apply_sorting() + + def _apply_filters(self) -> List[Any]: + """Apply filters""" + filtered = self.data + + if self.filters.search: + # 검색어 필터 (예: name 또는 description 필드 검색) + search_term = self.filters.search.lower() + filtered = [ + item for item in filtered + if (hasattr(item, 'name') and search_term in item.name.lower()) or + (hasattr(item, 'description') and item.description and search_term in item.description.lower()) + ] + + if self.filters.category: + filtered = [item for item in filtered if hasattr(item, 'category') and item.category == self.filters.category] + + if self.filters.status: + filtered = [item for item in filtered if hasattr(item, 'status') and item.status == self.filters.status] + + # 날짜 필터링 구현 (날짜 필드가 있을 때) + if self.filters.date_from or self.filters.date_to: + from datetime import datetime + filtered = self._apply_date_filter(filtered) + + return filtered + + def _apply_date_filter(self, data: List[Any]) -> List[Any]: + """Apply date filter""" + from datetime import datetime + + if not self.filters.date_from and not self.filters.date_to: + return data + + filtered = [] + for item in data: + if not hasattr(item, 'created_at'): + continue + + item_date = item.created_at.date() if hasattr(item.created_at, 'date') else item.created_at + + if self.filters.date_from: + start_date = datetime.strptime(self.filters.date_from, "%Y-%m-%d").date() + if item_date < start_date: + continue + + if self.filters.date_to: + end_date = datetime.strptime(self.filters.date_to, "%Y-%m-%d").date() + if item_date > end_date: + continue + + filtered.append(item) + + return filtered + + def _apply_sorting(self) -> List[Any]: + """Apply sorting""" + if not self.pagination.sort_by: + return self.filtered_data + + reverse = self.pagination.sort_order == SortOrder.DESC + + try: + return sorted( + self.filtered_data, + key=lambda x: getattr(x, self.pagination.sort_by, 0), + reverse=reverse + ) + except (AttributeError, TypeError): + # 정렬 필드를 찾지 못하거나 정렬할 수 없으면 원본 반환 + return self.filtered_data + + def get_page(self) -> tuple[List[Any], int]: + """Return current page data and total count""" + total = len(self.sorted_data) + start = (self.pagination.page - 1) * self.pagination.size + end = start + self.pagination.size + + page_data = self.sorted_data[start:end] + return page_data, total + + def get_metadata(self) -> Dict[str, Any]: + """Return pagination metadata""" + total = len(self.sorted_data) + pages = (total + self.pagination.size - 1) // self.pagination.size + + return { + "page": self.pagination.page, + "size": self.pagination.size, + "total": total, + "pages": pages, + "has_next": self.pagination.page < pages, + "has_prev": self.pagination.page > 1, + "filters_applied": { + "search": self.filters.search, + "category": self.filters.category, + "status": self.filters.status, + "date_range": f"{self.filters.date_from} ~ {self.filters.date_to}" if self.filters.date_from or self.filters.date_to else None + }, + "sorting": { + "field": self.pagination.sort_by, + "order": self.pagination.sort_order + } if self.pagination.sort_by else None + } +``` + +## 6단계: 고급 API 엔드포인트 구현 + +### Item API 라우터 (`src/api/routes/items.py`) + +```python +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Path, BackgroundTasks +from fastapi.responses import JSONResponse + +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse +from src.helper.pagination import pagination_params, filter_params, PaginationParams, FilterParams, AdvancedPaginator +from src.helper.exceptions import ResourceNotFoundException, DuplicateResourceException, ValidationException +from src.utils.responses import success_response, paginated_response, ResponseHelper +from src.crud.items import ItemCRUD + +router = APIRouter(prefix="/items", tags=["items"]) +crud = ItemCRUD() + +@router.post("/", response_model=dict, status_code=201) +async def create_item( + item_create: ItemCreate, + background_tasks: BackgroundTasks +) -> JSONResponse: + """ + 새 item 생성 + + - **name**: item 이름 (필수) + - **description**: item 설명 (선택) + - **price**: 가격 (필수, 0 이상) + - **category**: 카테고리 (선택) + """ + # 중복 확인 + existing_item = await crud.get_by_name(item_create.name) + if existing_item: + raise DuplicateResourceException("Item", "name", item_create.name) + + # 비즈니스 로직 검증 + if item_create.price < 0: + raise ValidationException("Price must be 0 or greater", "price") + + # item 생성 + created_item = await crud.create(item_create) + + # 백그라운드 작업 (예: 알림 발송, 로깅 등) + background_tasks.add_task(send_creation_notification, created_item.id) + + return ResponseHelper.created( + data=created_item.dict(), + message=f"Item '{created_item.name}' created successfully" + ) + +@router.get("/", response_model=dict) +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + """ + item 목록 조회 (페이지네이션, 필터링, 정렬 지원) + + **페이지네이션:** + - page: 페이지 번호 (기본값: 1) + - size: 페이지 크기 (기본값: 20, 최대: 100) + + **정렬:** + - sort_by: 정렬 필드 (name, price, created_at 등) + - sort_order: 정렬 순서 (asc, desc) + + **필터링:** + - search: 검색어 (name 또는 description 필드 검색) + - category: 카테고리 필터 + - status: 상태 필터 + - date_from: 시작 날짜 (YYYY-MM-DD) + - date_to: 종료 날짜 (YYYY-MM-DD) + """ + # 모든 item 조회 + all_items = await crud.get_all() + + # 고급 페이지네이션 적용 + paginator = AdvancedPaginator(all_items, pagination, filters) + page_data, total = paginator.get_page() + + # 응답에 추가 메타데이터 포함 + metadata = paginator.get_metadata() + + # 커스텀 메시지 작성 + message = f"Total {total} items, {len(page_data)} items retrieved" + if filters.search: + message += f" (Search term: '{filters.search}')" + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=message + ) + +@router.get("/search/advanced", response_model=dict) +async def advanced_search( + q: str = Query(..., min_length=1, description="Search term"), + fields: List[str] = Query(["name", "description"], description="Search fields"), + exact_match: bool = Query(False, description="Exact match"), + case_sensitive: bool = Query(False, description="Case sensitive"), + pagination: PaginationParams = Depends(pagination_params) +) -> JSONResponse: + """ + 고급 검색 기능 + + - **q**: 검색어 (필수) + - **fields**: 검색 대상 필드 목록 + - **exact_match**: 정확 일치 + - **case_sensitive**: 대소문자 구분 + """ + results = await crud.advanced_search( + query=q, + fields=fields, + exact_match=exact_match, + case_sensitive=case_sensitive + ) + + # 페이지네이션 적용 + total = len(results) + start = (pagination.page - 1) * pagination.size + end = start + pagination.size + page_data = results[start:end] + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=f"'{q}' search results: {total} items" + ) + +@router.get("/{item_id}", response_model=dict) +async def get_item( + item_id: int = Path(..., gt=0, description="Item ID") +) -> JSONResponse: + """Get specific item""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + return success_response( + data=item.dict(), + message=f"Item '{item.name}' retrieved successfully" + ) + +@router.put("/{item_id}", response_model=dict) +async def update_item( + item_id: int = Path(..., gt=0, description="Item ID"), + item_update: ItemUpdate +) -> JSONResponse: + """Update item""" + existing_item = await crud.get_by_id(item_id) + if not existing_item: + raise ResourceNotFoundException("Item", item_id) + + # 다른 item 들과 이름 중복 확인 + if item_update.name and item_update.name != existing_item.name: + duplicate = await crud.get_by_name(item_update.name) + if duplicate: + raise DuplicateResourceException("Item", "name", item_update.name) + + updated_item = await crud.update(item_id, item_update) + + return ResponseHelper.updated( + data=updated_item.dict(), + message=f"Item '{updated_item.name}' updated successfully" + ) + +@router.delete("/{item_id}", response_model=dict, status_code=204) +async def delete_item( + item_id: int = Path(..., gt=0, description="Item ID"), + force: bool = Query(False, description="Force delete") +) -> JSONResponse: + """Delete item""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + # 삭제 전 검증 (예: 관련 주문 존재 여부) + if not force and await crud.has_related_orders(item_id): + raise ValidationException( + "Related orders exist, cannot be deleted. Use force=true to force delete" + ) + + await crud.delete(item_id) + + return ResponseHelper.deleted( + message=f"Item '{item.name}' deleted successfully" + ) + +@router.post("/bulk", response_model=dict) +async def bulk_create_items( + items: List[ItemCreate], + skip_duplicates: bool = Query(False, description="Skip duplicates") +) -> JSONResponse: + """Bulk create items""" + if len(items) > 100: + raise ValidationException("Maximum 100 items can be created at once") + + created_items = [] + skipped_items = [] + errors = [] + + for i, item_create in enumerate(items): + try: + # 중복 확인 + existing = await crud.get_by_name(item_create.name) + if existing: + if skip_duplicates: + skipped_items.append({"index": i, "name": item_create.name, "reason": "Duplicate name"}) + continue + else: + errors.append({"index": i, "name": item_create.name, "error": "Duplicate name"}) + continue + + created_item = await crud.create(item_create) + created_items.append(created_item) + + except Exception as e: + errors.append({"index": i, "name": item_create.name, "error": str(e)}) + + result = { + "created_count": len(created_items), + "skipped_count": len(skipped_items), + "error_count": len(errors), + "created_items": [item.dict() for item in created_items], + "skipped_items": skipped_items, + "errors": errors + } + + message = f"{len(created_items)} items created" + if skipped_items: + message += f", {len(skipped_items)} skipped" + if errors: + message += f", {len(errors)} errors" + + return success_response(data=result, message=message) + +async def send_creation_notification(item_id: int): + """Item creation notification (background task)""" + # 실제 구현에서는 이메일, Slack 등으로 알림 발송 + import asyncio + await asyncio.sleep(1) # 시뮬레이션 + print(f"Item {item_id} creation notification sent") +``` + +## 7단계: OpenAPI 문서 커스터마이즈 + +### OpenAPI 문서 커스터마이즈 (`src/utils/documents.py`) + +```python +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from typing import Dict, Any + +def custom_openapi(app: FastAPI) -> Dict[str, Any]: + """Create custom OpenAPI schema""" + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + # 커스텀 정보 추가 + openapi_schema["info"].update({ + "contact": { + "name": "API Support", + "url": "https://example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "termsOfService": "https://example.com/terms" + }) + + # 서버 정보 추가 + openapi_schema["servers"] = [ + { + "url": "https://api.example.com", + "description": "Production server" + }, + { + "url": "https://staging-api.example.com", + "description": "Staging server" + }, + { + "url": "http://localhost:8000", + "description": "Development server" + } + ] + + # 공통 응답 스키마 추가 + openapi_schema["components"]["schemas"].update({ + "SuccessResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": True}, + "status": {"type": "string", "example": "success"}, + "data": {"type": "object"}, + "message": {"type": "string", "example": "Request processed successfully"}, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": False}, + "status": {"type": "string", "example": "error"}, + "error": { + "type": "object", + "properties": { + "code": {"type": "string", "example": "RESOURCE_NOT_FOUND"}, + "message": {"type": "string", "example": "Resource not found"}, + "details": {"type": "object"} + } + }, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + } + }) + + # 태그 그룹과 설명 추가 + openapi_schema["tags"] = [ + { + "name": "items", + "description": "Item management API", + "externalDocs": { + "description": "More information", + "url": "https://example.com/docs/items" + } + }, + { + "name": "health", + "description": "System status check API" + } + ] + + # 보안 스키마 추가 + openapi_schema["components"]["securitySchemes"] = { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + + app.openapi_schema = openapi_schema + return app.openapi_schema + +def setup_docs(app: FastAPI): + """Setup documentation""" + app.openapi = lambda: custom_openapi(app) + + # Swagger UI 설정 + app.docs_url = "/docs" + app.redoc_url = "/redoc" + + # 추가 문서 엔드포인트 + @app.get("/openapi.json", include_in_schema=False) + async def get_openapi_endpoint(): + return custom_openapi(app) +``` + +### 메인 애플리케이션에 적용 (`src/main.py` 추가) + +```python +from src.utils.documents import setup_docs +from src.api.routes import items + +# 라우터 포함 +app.include_router(items.router, prefix="/api/v1") + +# 문서화 설정 적용 +setup_docs(app) + +# 요청 ID 미들웨어 추가 +@app.middleware("http") +async def add_request_id(request: Request, call_next): + request_id = generate_request_id() + request.state.request_id = request_id + + response = await call_next(request) + response.headers["X-Request-ID"] = request_id + + return response +``` + +## 8단계: 캐싱 시스템 구현 + +### 응답 캐싱 (`src/utils/cache.py`) + +```python +from typing import Optional, Any, Dict +from functools import wraps +import asyncio +import json +import hashlib +from datetime import datetime, timedelta + +class MemoryCache: + """Memory-based cache""" + + def __init__(self): + self._cache: Dict[str, Dict[str, Any]] = {} + + async def get(self, key: str) -> Optional[Any]: + """Get value from cache""" + if key not in self._cache: + return None + + item = self._cache[key] + if datetime.utcnow() > item["expires_at"]: + del self._cache[key] + return None + + return item["value"] + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + """Save value to cache""" + self._cache[key] = { + "value": value, + "expires_at": datetime.utcnow() + timedelta(seconds=ttl_seconds), + "created_at": datetime.utcnow() + } + + async def delete(self, key: str): + """Delete value from cache""" + self._cache.pop(key, None) + + async def clear(self): + """Delete all cache""" + self._cache.clear() + + def get_stats(self) -> Dict[str, Any]: + """Cache statistics""" + now = datetime.utcnow() + valid_items = [ + item for item in self._cache.values() + if now <= item["expires_at"] + ] + + return { + "total_items": len(self._cache), + "valid_items": len(valid_items), + "expired_items": len(self._cache) - len(valid_items), + "memory_usage_mb": len(str(self._cache)) / 1024 / 1024 + } + +# 전역 캐시 인스턴스 +cache = MemoryCache() + +def cache_response(ttl_seconds: int = 300, key_prefix: str = ""): + """Response caching decorator""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # 캐시 키 생성 + cache_key = generate_cache_key(func.__name__, args, kwargs, key_prefix) + + # 캐시에서 가져오기 + cached_response = await cache.get(cache_key) + if cached_response: + return cached_response + + # 함수 실행 + response = await func(*args, **kwargs) + + # 응답 캐시 + await cache.set(cache_key, response, ttl_seconds) + + return response + return wrapper + return decorator + +def generate_cache_key(func_name: str, args: tuple, kwargs: dict, prefix: str = "") -> str: + """Generate cache key""" + # 함수 이름과 인자를 기반으로 고유 키 생성 + key_data = { + "function": func_name, + "args": str(args), + "kwargs": sorted(kwargs.items()) + } + + key_string = json.dumps(key_data, sort_keys=True) + key_hash = hashlib.md5(key_string.encode()).hexdigest() + + return f"{prefix}:{func_name}:{key_hash}" if prefix else f"{func_name}:{key_hash}" + +# 캐시 관리 엔드포인트 +@app.get("/admin/cache/stats") +async def get_cache_stats(): + """Get cache statistics""" + stats = cache.get_stats() + return success_response(data=stats, message="Cache statistics retrieved") + +@app.delete("/admin/cache/clear") +async def clear_cache(): + """Delete all cache""" + await cache.clear() + return success_response(message="Cache deleted successfully") +``` + +### 캐싱 예시 + +```python +# src/api/routes/items.py 에 캐싱 적용 + +from src.utils.cache import cache_response + +@router.get("/", response_model=dict) +@cache_response(ttl_seconds=60, key_prefix="items_list") # 1분 캐싱 +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + # ... 기존 코드 ... + +@router.get("/{item_id}", response_model=dict) +@cache_response(ttl_seconds=300, key_prefix="item_detail") # 5분 캐싱 +async def get_item(item_id: int = Path(..., gt=0)) -> JSONResponse: + # ... 기존 코드 ... +``` + +## 9단계: API 테스트 + +### 서버 실행과 기본 테스트 + +
+ +```console +$ cd advanced-api-server +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# 커스텀 응답 형식 테스트 +$ curl -X POST "http://localhost:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics" + }' + +{ + "success": true, + "status": "success", + "data": { + "id": 1, + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'Advanced notebook' created successfully", + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} + +# 페이지네이션과 필터링 테스트 +$ curl "http://localhost:8000/api/v1/items/?page=1&size=10&search=notebook&sort_by=price&sort_order=desc" + +# 고급 검색 테스트 +$ curl "http://localhost:8000/api/v1/items/search/advanced?q=notebook&fields=name&fields=description&exact_match=false" + +# 에러 응답 테스트 +$ curl "http://localhost:8000/api/v1/items/999" + +{ + "success": false, + "status": "error", + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "Item (ID: 999) not found", + "details": { + "resource": "Item", + "id": 999 + } + }, + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} +``` + +
+ +### OpenAPI 문서 확인 + +브라우저에서 http://localhost:8000/docs 로 이동해 맞춤 구성된 API 문서를 확인하세요. + +## 다음 단계 + +커스텀 응답 처리 시스템을 마쳤습니다! 다음으로 시도해 볼 만한 것들: + +1. **[MCP 통합](mcp-integration.md)** — Model Context Protocol 구현 + + + + +## 요약 + +이 튜토리얼에서는 고급 응답 처리 시스템을 다음과 같이 구현했습니다: + +- ✅ 표준화된 API 응답 형식 설계 +- ✅ 전역 예외 처리와 커스텀 에러 응답 +- ✅ 고급 페이지네이션과 필터링 시스템 +- ✅ OpenAPI 문서 커스터마이즈 +- ✅ 응답 캐싱과 성능 최적화 +- ✅ 요청 추적 시스템 +- ✅ 백그라운드 작업 처리 +- ✅ 배치 작업 API + +이제 엔터프라이즈급 API 서버의 핵심 기능을 스스로 구현할 수 있습니다! diff --git a/docs/ko/tutorial/database-integration.md b/docs/ko/tutorial/database-integration.md new file mode 100644 index 0000000..c6ff2e3 --- /dev/null +++ b/docs/ko/tutorial/database-integration.md @@ -0,0 +1,1027 @@ +# 데이터베이스 통합 (PostgreSQL + SQLAlchemy) + +PostgreSQL 데이터베이스와 SQLAlchemy ORM을 사용해 실제 운영 환경에서도 활용할 수 있는 FastAPI 애플리케이션을 구축합니다. 이 튜토리얼에서는 `fastapi-psql-orm` 템플릿으로 완전한 데이터베이스 통합 시스템을 구현합니다. + +## 이 튜토리얼에서 배우는 내용 + +- PostgreSQL 데이터베이스 설정과 통합 +- SQLAlchemy ORM으로 데이터 모델링 +- Alembic으로 데이터베이스 마이그레이션 +- Docker Compose로 개발 환경 구성 +- 데이터베이스 커넥션 풀 관리 +- 트랜잭션 처리와 데이터 무결성 + +## 사전 요구 사항 + +- [비동기 CRUD API 튜토리얼](async-crud-api.md) 완료 +- Docker와 Docker Compose 설치 +- PostgreSQL 기초 지식 +- SQLAlchemy ORM 기본 개념 이해 + +## PostgreSQL과 SQLAlchemy가 필요한 이유 + +### JSON 파일 vs PostgreSQL 비교 + +| 항목 | JSON 파일 | PostgreSQL | +|----------|------------|------------| +| **성능** | 제한적 | 고성능 인덱싱 | +| **동시성** | 파일 잠금 문제 | 트랜잭션 지원 | +| **확장성** | 메모리 한계 | 대규모 데이터 처리 | +| **무결성** | 보장되지 않음 | ACID 속성 보장 | +| **쿼리** | 전체 데이터 로딩 필요 | 복잡한 쿼리 지원 | +| **백업** | 파일 복사 | 완전한 백업 / 복구 | + +## 1단계: PostgreSQL + ORM 프로젝트 생성 + +`fastapi-psql-orm` 템플릿으로 프로젝트를 만듭니다: + +
+ +```console +$ fastkit startdemo fastapi-psql-orm +Enter the project name: todo-postgres-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Todo management API using PostgreSQL +Deploying FastAPI project using 'fastapi-psql-orm' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ todo-postgres-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Todo management API using PostgreSQL │ +└──────────────┴─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ psycopg2 │ +│ Dependency 6 │ asyncpg │ +│ Dependency 7 │ sqlmodel │ +└──────────────┴────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'todo-postgres-api' from 'fastapi-psql-orm' has been created successfully! +``` + +
+ +## 2단계: 프로젝트 구조 분석 + +생성된 프로젝트는 완전한 데이터베이스 통합 환경을 제공합니다: + +``` +todo-postgres-api/ +├── docker-compose.yml # PostgreSQL 컨테이너 구성 +├── Dockerfile # 애플리케이션 컨테이너 +├── alembic.ini # Alembic 설정 +├── template-config.yml # 템플릿 설정 +├── scripts/ +│ ├── pre-start.sh # 시작 전 초기화 +│ └── test.sh # 테스트 실행 스크립트 +├── src/ +│ ├── main.py # FastAPI 애플리케이션 +│ ├── core/ +│ │ ├── config.py # 환경 설정 +│ │ └── db.py # 데이터베이스 연결 설정 +│ ├── api/ +│ │ ├── deps.py # 의존성 주입 +│ │ └── routes/ +│ │ └── items.py # API 엔드포인트 +│ ├── crud/ +│ │ └── items.py # 데이터베이스 작업 +│ ├── schemas/ +│ │ └── items.py # Pydantic 모델 +│ ├── utils/ +│ │ ├── backend_pre_start.py # 백엔드 초기화 +│ │ ├── init_data.py # 초기 데이터 로딩 +│ │ └── tests_pre_start.py # 테스트 준비 +│ └── alembic/ +│ ├── env.py # Alembic 환경 설정 +│ └── versions/ # 마이그레이션 파일 +└── tests/ + ├── conftest.py # 테스트 구성 + └── test_items.py # API 테스트 +``` + +### 핵심 구성 요소 + +1. **SQLModel**: SQLAlchemy + Pydantic 통합 +2. **Alembic**: 데이터베이스 스키마 마이그레이션 +3. **asyncpg**: 비동기 PostgreSQL 드라이버 +4. **Docker Compose**: 개발 환경 컨테이너화 + +## 3단계: 데이터베이스 설정 이해 + +### 데이터베이스 연결 설정 (`src/core/db.py`) + +```python +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.core.config import settings + +# 비동기 PostgreSQL 엔진 생성 +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, # SQL 로그 출력 + pool_size=20, # 커넥션 풀 크기 + max_overflow=0, # 추가 허용 커넥션 수 + pool_pre_ping=True, # 연결 상태 확인 +) + +# 비동기 세션 팩토리 +AsyncSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + +async def create_tables(): + """Create database tables""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + +async def get_session() -> AsyncSession: + """Provide database session (for dependency injection)""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() +``` + +### 환경 설정 (`src/core/config.py`) + +```python +from pydantic_settings import BaseSettings +from typing import Optional + +class Settings(BaseSettings): + PROJECT_NAME: str = "Todo PostgreSQL API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "Todo management API using PostgreSQL" + + # 데이터베이스 설정 + POSTGRES_SERVER: str = "localhost" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "password" + POSTGRES_DB: str = "todoapp" + POSTGRES_PORT: int = 5432 + + # 테스트 데이터베이스 + TEST_DATABASE_URL: Optional[str] = None + + # 디버그 모드 + DEBUG: bool = False + + @property + def DATABASE_URL(self) -> str: + """Generate PostgreSQL connection URL""" + return ( + f"postgresql+asyncpg://{self.POSTGRES_USER}:" + f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:" + f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + ) + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## 4단계: 데이터 모델 정의 + +### SQLModel 을 사용한 데이터 모델 (`src/schemas/items.py`) + +```python +from sqlmodel import SQLModel, Field +from typing import Optional +from datetime import datetime + +# 공통 필드 정의 +class ItemBase(SQLModel): + name: str = Field(index=True, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: float = Field(gt=0, description="Price must be greater than 0") + tax: Optional[float] = Field(default=None, ge=0) + is_active: bool = Field(default=True) + +# 데이터베이스 테이블 모델 +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # 인덱스 설정 + class Config: + schema_extra = { + "example": { + "name": "notebook", + "description": "High-performance gaming notebook", + "price": 1500000.0, + "tax": 150000.0, + "is_active": True + } + } + +# API 요청 / 응답 모델 +class ItemCreate(ItemBase): + pass + +class ItemUpdate(SQLModel): + name: Optional[str] = Field(default=None, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: Optional[float] = Field(default=None, gt=0) + tax: Optional[float] = Field(default=None, ge=0) + is_active: Optional[bool] = Field(default=None) + +class ItemResponse(ItemBase): + id: int + created_at: datetime + updated_at: Optional[datetime] +``` + +## 5단계: CRUD 작업 구현 + +### 데이터베이스 CRUD 로직 (`src/crud/items.py`) + +```python +from typing import List, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, delete +from sqlalchemy.orm import selectinload +from datetime import datetime + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, item_create: ItemCreate) -> Item: + """Create new item""" + db_item = Item(**item_create.dict()) + + self.db.add(db_item) + await self.db.commit() + await self.db.refresh(db_item) + + return db_item + + async def get_by_id(self, item_id: int) -> Optional[Item]: + """Get item by ID""" + statement = select(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + return result.scalar_one_or_none() + + async def get_many( + self, + skip: int = 0, + limit: int = 100, + active_only: bool = True + ) -> List[Item]: + """Get multiple items (pagination supported)""" + statement = select(Item) + + if active_only: + statement = statement.where(Item.is_active == True) + + statement = statement.offset(skip).limit(limit) + result = await self.db.execute(statement) + return result.scalars().all() + + async def update(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """Update item""" + # 갱신 데이터 준비 + update_data = item_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow() + + # 갱신 실행 + statement = ( + update(Item) + .where(Item.id == item_id) + .values(**update_data) + .returning(Item) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.scalar_one_or_none() + + async def delete(self, item_id: int) -> bool: + """Delete item (soft delete)""" + statement = ( + update(Item) + .where(Item.id == item_id) + .values(is_active=False, updated_at=datetime.utcnow()) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def hard_delete(self, item_id: int) -> bool: + """Delete item completely""" + statement = delete(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def search(self, query: str) -> List[Item]: + """Search item (name, description)""" + statement = select(Item).where( + (Item.name.ilike(f"%{query}%")) | + (Item.description.ilike(f"%{query}%")) + ).where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalars().all() + + async def get_total_count(self, active_only: bool = True) -> int: + """Get total item count""" + from sqlalchemy import func + + statement = select(func.count(Item.id)) + if active_only: + statement = statement.where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalar() +``` + +## 6단계: API 엔드포인트 구현 + +### 의존성 주입 설정 (`src/api/deps.py`) + +```python +from typing import AsyncGenerator +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from src.core.db import get_session +from src.crud.items import ItemCRUD + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Database session dependency""" + async for session in get_session(): + yield session + +def get_item_crud(db: AsyncSession = Depends(get_db)) -> ItemCRUD: + """Item CRUD dependency""" + return ItemCRUD(db) +``` + +### API 라우터 구현 (`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, Depends, HTTPException, Query, status + +from src.api.deps import get_item_crud +from src.crud.items import ItemCRUD +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse + +router = APIRouter() + +@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED) +async def create_item( + item_create: ItemCreate, + crud: ItemCRUD = Depends(get_item_crud) +): + """Create new item""" + return await crud.create(item_create) + +@router.get("/", response_model=List[ItemResponse]) +async def read_items( + skip: int = Query(0, ge=0, description="Skip items"), + limit: int = Query(100, ge=1, le=1000, description="Maximum items to retrieve"), + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Get item list (pagination supported)""" + return await crud.get_many(skip=skip, limit=limit, active_only=active_only) + +@router.get("/search", response_model=List[ItemResponse]) +async def search_items( + q: str = Query(..., min_length=1, description="Search term"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Search item""" + return await crud.search(q) + +@router.get("/count") +async def get_items_count( + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Get total item count""" + count = await crud.get_total_count(active_only) + return {"total": count} + +@router.get("/{item_id}", response_model=ItemResponse) +async def read_item( + item_id: int, + crud: ItemCRUD = Depends(get_item_crud) +): + """Get specific item""" + item = await crud.get_by_id(item_id) + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return item + +@router.put("/{item_id}", response_model=ItemResponse) +async def update_item( + item_id: int, + item_update: ItemUpdate, + crud: ItemCRUD = Depends(get_item_crud) +): + """Update item""" + updated_item = await crud.update(item_id, item_update) + if not updated_item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item( + item_id: int, + hard_delete: bool = Query(False, description="Complete delete"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Delete item""" + if hard_delete: + deleted = await crud.hard_delete(item_id) + else: + deleted = await crud.delete(item_id) + + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) +``` + +## 7단계: Docker 컨테이너 실행 + +### Docker Compose 설정 확인 (`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + db: + image: postgres:15 + restart: always + environment: + POSTGRES_DB: todoapp + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: . + restart: always + ports: + - "8000:8000" + environment: + POSTGRES_SERVER: db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: todoapp + depends_on: + - db + volumes: + - ./src:/app/src + +volumes: + postgres_data: +``` + +### 컨테이너 실행 + +
+ +```console +$ cd todo-postgres-api + +# 백그라운드로 서비스 시작 +$ docker-compose up -d +Creating network "todo-postgres-api_default" with the default driver +Creating volume "todo-postgres-api_postgres_data" with default driver +Pulling db (postgres:15)... +Creating todo-postgres-api_db_1 ... done +Building app +Creating todo-postgres-api_app_1 ... done + +# 서비스 상태 확인 +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------- +todo-postgres-api_app_1 uvicorn src.main:app --host=0.0.0.0 --port=8000 Up 0.0.0.0:8000->8000/tcp +todo-postgres-api_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp + +# 로그 확인 +$ docker-compose logs app +``` + +
+ +## 8단계: 데이터베이스 마이그레이션 + +### Alembic으로 초기 마이그레이션 생성 + +
+ +```console +# 컨테이너 내부에서 마이그레이션 실행 +$ docker-compose exec app alembic revision --autogenerate -m "Create items table" +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'items' +Generating migration script /app/src/alembic/versions/001_create_items_table.py ... done + +# 마이그레이션 적용 +$ docker-compose exec app alembic upgrade head +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.runtime.migration] Running upgrade -> 001, Create items table +``` + +
+ +### 마이그레이션 파일 확인 + +생성된 마이그레이션 파일을 확인합니다: + +```python +# src/alembic/versions/001_create_items_table.py +"""Create items table + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers +revision = '001' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=500), nullable=True), + sa.Column('price', sa.Float(), nullable=False), + sa.Column('tax', sa.Float(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_items_name'), table_name='items') + op.drop_table('items') + # ### end Alembic commands ### +``` + +## 9단계: API 테스트 + +### 기본 CRUD 테스트 + +
+ +```console +# 새 item 생성 +$ curl -X POST "http://localhost:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000, + "tax": 250000 + }' + +{ + "id": 1, + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000.0, + "tax": 250000.0, + "is_active": true, + "created_at": "2024-01-01T12:00:00.123456", + "updated_at": null +} + +# item 목록 조회 +$ curl "http://localhost:8000/items/" + +# 페이지네이션을 적용한 item 목록 조회 +$ curl "http://localhost:8000/items/?skip=0&limit=10" + +# item 검색 +$ curl "http://localhost:8000/items/search?q=MacBook" + +# item 개수 조회 +$ curl "http://localhost:8000/items/count" +{"total": 1} +``` + +
+ +### 고급 쿼리 기능 테스트 + +
+ +```console +# 비활성 item 까지 포함해 목록 조회 +$ curl "http://localhost:8000/items/?active_only=false" + +# item 갱신 +$ curl -X PUT "http://localhost:8000/items/1" \ + -H "Content-Type: application/json" \ + -d '{ + "price": 2300000, + "tax": 230000 + }' + +# 소프트 삭제 +$ curl -X DELETE "http://localhost:8000/items/1" + +# 하드 삭제 +$ curl -X DELETE "http://localhost:8000/items/1?hard_delete=true" +``` + +
+ +## 10단계: 고급 데이터베이스 기능 + +### 트랜잭션 처리 + +```python +# src/crud/items.py 에 추가 + +from sqlalchemy.exc import SQLAlchemyError + +async def create_items_batch(self, items_create: List[ItemCreate]) -> List[Item]: + """Create multiple items in a transaction""" + created_items = [] + + try: + for item_create in items_create: + db_item = Item(**item_create.dict()) + self.db.add(db_item) + created_items.append(db_item) + + await self.db.commit() + + # 모든 item 새로고침 + for item in created_items: + await self.db.refresh(item) + + return created_items + + except SQLAlchemyError: + await self.db.rollback() + raise +``` + +### 관계형 데이터 모델링 + +```python +# src/schemas/items.py 에 추가 + +from sqlmodel import Relationship + +class Category(SQLModel, table=True): + __tablename__ = "categories" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(max_length=50, unique=True) + description: Optional[str] = None + + # 관계 설정 + items: List["Item"] = Relationship(back_populates="category") + +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # 외래 키 추가 + category_id: Optional[int] = Field(foreign_key="categories.id") + + # 관계 설정 + category: Optional[Category] = Relationship(back_populates="items") +``` + +### 인덱스 최적화 + +```python +# src/schemas/items.py 에 추가 + +from sqlalchemy import Index + +class Item(ItemBase, table=True): + __tablename__ = "items" + + # ... 기존 필드 ... + + # 복합 인덱스 설정 + __table_args__ = ( + Index('ix_items_price_active', 'price', 'is_active'), + Index('ix_items_created_at', 'created_at'), + Index('ix_items_name_description', 'name', 'description'), # 전문 검색용 + ) +``` + +## 11단계: 테스트 작성 + +### 데이터베이스 테스트 구성 (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.main import app +from src.core.db import get_session +from src.core.config import settings + +# 테스트 데이터베이스 엔진 +test_engine = create_async_engine( + settings.TEST_DATABASE_URL or "sqlite+aiosqlite:///./test.db", + echo=False, +) + +TestSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=test_engine, + class_=AsyncSession, + expire_on_commit=False, +) + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture(scope="function") +async def db_session(): + # 테스트 테이블 생성 + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + # 세션 제공 + async with TestSessionLocal() as session: + yield session + + # 테스트 후 테이블 삭제 + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + +@pytest.fixture +async def client(db_session: AsyncSession): + # 의존성 오버라이드 + async def override_get_session(): + yield db_session + + app.dependency_overrides[get_session] = override_get_session + + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + app.dependency_overrides.clear() +``` + +### 통합 테스트 (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_and_read_item(client: AsyncClient): + """Integration test for creating and reading item""" + # item 생성 + item_data = { + "name": "Test Item", + "description": "Database test", + "price": 50000, + "tax": 5000 + } + + response = await client.post("/items/", json=item_data) + assert response.status_code == 201 + + created_item = response.json() + assert created_item["name"] == item_data["name"] + assert "id" in created_item + assert "created_at" in created_item + + # 생성된 item 조회 + item_id = created_item["id"] + response = await client.get(f"/items/{item_id}") + assert response.status_code == 200 + + retrieved_item = response.json() + assert retrieved_item["id"] == item_id + assert retrieved_item["name"] == item_data["name"] + +@pytest.mark.asyncio +async def test_item_pagination(client: AsyncClient): + """Test pagination feature""" + # 여러 item 생성 + for i in range(15): + item_data = { + "name": f"Item {i}", + "description": f"Description {i}", + "price": i * 1000, + "tax": i * 100 + } + await client.post("/items/", json=item_data) + + # 첫 페이지 조회 + response = await client.get("/items/?skip=0&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 10 + + # 두 번째 페이지 조회 + response = await client.get("/items/?skip=10&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 5 + +@pytest.mark.asyncio +async def test_item_search(client: AsyncClient): + """Test search feature""" + # 테스트 item 생성 + items = [ + {"name": "iPhone 15", "description": "Latest smartphone", "price": 1200000, "tax": 120000}, + {"name": "Galaxy S24", "description": "Samsung flagship", "price": 1100000, "tax": 110000}, + {"name": "MacBook Air", "description": "Apple notebook", "price": 1500000, "tax": 150000}, + ] + + for item in items: + await client.post("/items/", json=item) + + # "iPhone" 검색 + response = await client.get("/items/search?q=iPhone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["name"] == "iPhone 15" + + # "smartphone" 검색 (description) + response = await client.get("/items/search?q=smartphone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["description"] == "Latest smartphone" +``` + +### 테스트 실행 + +
+ +```console +# 컨테이너 내부에서 테스트 실행 +$ docker-compose exec app python -m pytest tests/ -v +======================== test session starts ======================== +collected 12 items + +tests/test_items.py::test_create_and_read_item PASSED [ 8%] +tests/test_items.py::test_item_pagination PASSED [16%] +tests/test_items.py::test_item_search PASSED [25%] +tests/test_items.py::test_update_item PASSED [33%] +tests/test_items.py::test_delete_item PASSED [41%] +tests/test_items.py::test_soft_delete PASSED [50%] +tests/test_items.py::test_item_not_found PASSED [58%] +tests/test_items.py::test_invalid_item_data PASSED [66%] +tests/test_items.py::test_database_transaction PASSED [75%] +tests/test_items.py::test_concurrent_operations PASSED [83%] +tests/test_items.py::test_item_count PASSED [91%] +tests/test_items.py::test_batch_operations PASSED [100%] + +======================== 12 passed in 2.34s ======================== +``` + +
+ +## 12단계: 프로덕션 배포 고려 사항 + +### 커넥션 풀 최적화 + +```python +# src/core/config.py 에 추가 + +class Settings(BaseSettings): + # ... 기존 설정 ... + + # 데이터베이스 커넥션 풀 설정 + DB_POOL_SIZE: int = 20 + DB_MAX_OVERFLOW: int = 0 + DB_POOL_PRE_PING: bool = True + DB_POOL_RECYCLE: int = 300 # 5분 + + # 쿼리 타임아웃 + DB_QUERY_TIMEOUT: int = 30 + + # 커넥션 재시도 설정 + DB_RETRY_ATTEMPTS: int = 3 + DB_RETRY_DELAY: int = 1 +``` + +### 데이터베이스 모니터링 + +```python +# src/core/db.py 에 추가 + +import logging +from sqlalchemy import event +from sqlalchemy.engine import Engine + +logger = logging.getLogger(__name__) + +@event.listens_for(Engine, "before_cursor_execute") +def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log before query execution""" + context._query_start_time = time.time() + +@event.listens_for(Engine, "after_cursor_execute") +def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log after query execution""" + total = time.time() - context._query_start_time + if total > 1.0: # 느린 쿼리 (1초 이상) 기록 + logger.warning(f"Slow query: {total:.2f}s - {statement[:100]}...") +``` + +## 다음 단계 + +PostgreSQL 데이터베이스 통합을 마쳤습니다! 다음으로 시도해 볼 만한 것들: + +1. **[Docker 컨테이너화](docker-deployment.md)** — 프로덕션 배포 환경 구축 +2. **[커스텀 응답 처리](custom-response-handling.md)** — 고급 API 응답 형식 + + + +## 요약 + +이 튜토리얼에서는 PostgreSQL과 SQLAlchemy를 사용해 다음 작업을 진행했습니다: + +- ✅ PostgreSQL 데이터베이스 통합 +- ✅ SQLModel 로 ORM 구현 +- ✅ Alembic 마이그레이션 시스템 구축 +- ✅ 고급 CRUD 작업과 쿼리 최적화 +- ✅ 트랜잭션 처리와 데이터 무결성 +- ✅ 페이지네이션, 검색, 정렬 기능 +- ✅ 통합 테스트와 데이터베이스 테스트 +- ✅ 프로덕션 배포 고려 사항 + +이제 실제 운영 환경에서도 쓸 수 있는 견고한 데이터베이스 기반 API를 구축할 수 있습니다! diff --git a/docs/ko/tutorial/docker-deployment.md b/docs/ko/tutorial/docker-deployment.md new file mode 100644 index 0000000..b110d8b --- /dev/null +++ b/docs/ko/tutorial/docker-deployment.md @@ -0,0 +1,1177 @@ +# Docker 컨테이너화와 배포 + +FastAPI 애플리케이션을 Docker로 컨테이너화해 일관된 개발 환경을 만들고 배포까지 준비하는 방법을 배웁니다. `fastapi-dockerized` 템플릿으로 완전한 Docker 기반 배포 환경을 구성합니다. + +## 이 튜토리얼에서 배우는 내용 + +- Docker로 FastAPI 애플리케이션 컨테이너화 +- 멀티 스테이지 빌드로 최적화된 Docker 이미지 만들기 +- Docker Compose로 개발 환경 구성 +- 프로덕션 배포용 Docker 설정 +- 컨테이너 모니터링과 로그 관리 +- CI/CD 파이프라인 구축 + +## 사전 요구 사항 + +- [데이터베이스 통합 튜토리얼](database-integration.md) 완료 +- Docker와 Docker Compose 설치 +- 기본 Docker 명령에 대한 이해 +- 컨테이너 개념의 기초 지식 + +## Docker 컨테이너화의 장점 + +### 기존 방식 vs Docker 방식 + +| 항목 | 기존 방식 | Docker 방식 | +|----------|---------------------|-----------------| +| **환경 일관성** | 환경 간 차이 발생 | 어디서나 동일한 환경 | +| **의존성 관리** | 수동 설치 필요 | 모든 의존성이 이미지에 포함 | +| **배포 속도** | 느림 | 빠른 배포 가능 | +| **확장성** | 제한적 | 손쉬운 스케일링 | +| **롤백** | 복잡 | 이전 버전으로 즉시 롤백 | +| **자원 사용** | 무거움 | 가벼운 컨테이너 | + +## 1단계: Docker 기반 프로젝트 생성 + +`fastapi-dockerized` 템플릿으로 프로젝트를 만듭니다: + +
+ +```console +$ fastkit startdemo fastapi-dockerized +Enter the project name: dockerized-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Dockerized todo management API +Deploying FastAPI project using 'fastapi-dockerized' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ dockerized-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Dockerized todo management API │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'dockerized-todo-api' from 'fastapi-dockerized' has been created successfully! +``` + +
+ +## 2단계: Docker 설정 파일 분석 + +생성된 프로젝트의 Docker 관련 파일들을 살펴봅시다: + +``` +dockerized-todo-api/ +├── Dockerfile # Docker 이미지 빌드 설정 +├── docker-compose.yml # 개발 환경 컨테이너 구성 +├── docker-compose.prod.yml # 프로덕션 환경 구성 +├── .dockerignore # Docker 빌드 시 제외할 파일 +├── scripts/ +│ ├── start.sh # 컨테이너 시작 스크립트 +│ ├── prestart.sh # 시작 전 초기화 스크립트 +│ └── gunicorn.conf.py # Gunicorn 설정 +├── src/ +│ ├── main.py # FastAPI 애플리케이션 +│ └── ... # 기타 소스 코드 +└── requirements.txt # Python 의존성 +``` + +### Dockerfile 분석 + +```dockerfile +# 멀티 스테이지 빌드를 사용한 최적화된 Dockerfile + +# ============================================ +# 1단계: 빌드 스테이지 +# ============================================ +FROM python:3.12-slim as builder + +# 빌드 도구 설치 +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 의존성 파일 복사 및 설치 +COPY requirements.txt . +RUN pip install --user --no-cache-dir -r requirements.txt + +# ============================================ +# 2단계: 런타임 스테이지 +# ============================================ +FROM python:3.12-slim + +# 시스템 갱신과 필수 패키지 설치 +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# non-root 사용자 생성 (보안 강화) +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# 애플리케이션 디렉터리 생성 +WORKDIR /app + +# 빌드 스테이지에서 Python 패키지 복사 +COPY --from=builder /root/.local /home/appuser/.local + +# 애플리케이션 코드 복사 +COPY . . + +# 파일 권한 설정 +RUN chown -R appuser:appuser /app +RUN chmod +x scripts/start.sh scripts/prestart.sh + +# Python 패키지 경로를 PATH 에 추가 +ENV PATH=/home/appuser/.local/bin:$PATH + +# non-root 사용자로 전환 +USER appuser + +# 헬스 체크 구성 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# 포트 노출 +EXPOSE 8000 + +# 시작 스크립트 실행 +CMD ["./scripts/start.sh"] +``` + +### Docker Compose 개발 환경 (`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: dockerized-todo-api + restart: unless-stopped + ports: + - "8000:8000" + environment: + - ENVIRONMENT=development + - DEBUG=true + - RELOAD=true + volumes: + # 개발용 볼륨 마운트 (코드 변경 시 자동 리로드) + - ./src:/app/src:ro + - ./scripts:/app/scripts:ro + networks: + - app-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Redis (캐싱 및 세션 저장소) + redis: + image: redis:7-alpine + container_name: dockerized-todo-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - app-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # Nginx (리버스 프록시) + nginx: + image: nginx:alpine + container_name: dockerized-todo-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - app + networks: + - app-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + redis_data: + +networks: + app-network: + driver: bridge +``` + +### Docker Compose 프로덕션 환경 (`docker-compose.prod.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + restart: always + environment: + - ENVIRONMENT=production + - DEBUG=false + - WORKERS=4 + - MAX_WORKERS=8 + volumes: + - app_logs:/app/logs + networks: + - app-network + deploy: + replicas: 2 + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + redis: + image: redis:7-alpine + restart: always + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + + nginx: + image: nginx:alpine + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - app + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + +volumes: + redis_data: + app_logs: + nginx_logs: + +networks: + app-network: + driver: overlay + attachable: true +``` + +## 3단계: 시작 스크립트 구성 + +### 메인 시작 스크립트 (`scripts/start.sh`) + +```bash +#!/bin/bash + +set -e + +# 환경 변수 설정 +export PYTHONPATH=/app:$PYTHONPATH + +# 시작 전 스크립트 실행 +echo "Running pre-start script..." +./scripts/prestart.sh + +# 환경에 따라 실행 모드 결정 +if [[ "$ENVIRONMENT" == "production" ]]; then + echo "Starting production server with Gunicorn..." + exec gunicorn src.main:app \ + --config scripts/gunicorn.conf.py \ + --bind 0.0.0.0:8000 \ + --workers ${WORKERS:-4} \ + --worker-class uvicorn.workers.UvicornWorker \ + --max-requests 1000 \ + --max-requests-jitter 100 \ + --preload \ + --access-logfile - \ + --error-logfile - +else + echo "Starting development server with Uvicorn..." + if [[ "$RELOAD" == "true" ]]; then + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + --reload-dir src \ + --log-level debug + else + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-level info + fi +fi +``` + +### 시작 전 스크립트 (`scripts/prestart.sh`) + +```bash +#!/bin/bash + +set -e + +echo "Running pre-start checks..." + +# Python 모듈과 의존성 확인 +echo "Checking Python dependencies..." +python -c "import fastapi, uvicorn, pydantic; print('✓ Core dependencies OK')" + +# 환경 변수 확인 +if [[ -z "$ENVIRONMENT" ]]; then + export ENVIRONMENT="development" + echo "ℹ ENVIRONMENT not set, defaulting to development" +fi + +# 로그 디렉터리 생성 +mkdir -p /app/logs +touch /app/logs/app.log + +# health 엔드포인트 존재 확인 +echo "Checking health endpoint..." +python -c " +from src.main import app +routes = [route.path for route in app.routes] +if '/health' not in routes: + print('⚠ Warning: /health endpoint not found') +else: + print('✓ Health endpoint OK') +" + +echo "Pre-start checks completed successfully!" +``` + +### Gunicorn 설정 (`scripts/gunicorn.conf.py`) + +```python +import multiprocessing +import os + +# 서버 소켓 +bind = "0.0.0.0:8000" +backlog = 2048 + +# 워커 프로세스 +workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1)) +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 +max_requests = 1000 +max_requests_jitter = 100 + +# 워커 재시작 설정 +preload_app = True +timeout = 120 +keepalive = 2 + +# 로깅 +accesslog = "-" +errorlog = "-" +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' + +# 프로세스 이름 +proc_name = "dockerized-todo-api" + +# 보안 +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# 성능 튜닝 +def when_ready(server): + server.log.info("Server is ready. Spawning workers") + +def worker_int(worker): + worker.log.info("worker received INT or QUIT signal") + +def pre_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def worker_abort(worker): + worker.log.info("worker received SIGABRT signal") +``` + +## 4단계: 헬스 체크와 모니터링 구현 + +### 헬스 체크 엔드포인트 추가 (`src/main.py`) + +```python +from fastapi import FastAPI, status, Depends +from fastapi.responses import JSONResponse +import psutil +import time +from datetime import datetime + +app = FastAPI( + title="Dockerized Todo API", + description="Dockerized todo management API", + version="1.0.0" +) + +# 애플리케이션 시작 시간 +start_time = time.time() + +@app.get("/health", status_code=status.HTTP_200_OK) +async def health_check(): + """ + Container health check endpoint + """ + current_time = time.time() + uptime = current_time - start_time + + # 시스템 자원 정보 + memory_info = psutil.virtual_memory() + cpu_percent = psutil.cpu_percent(interval=1) + + health_data = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "uptime_seconds": round(uptime, 2), + "version": app.version, + "system": { + "memory_usage_percent": memory_info.percent, + "memory_available_mb": round(memory_info.available / 1024 / 1024, 2), + "cpu_usage_percent": cpu_percent, + }, + "checks": { + "database": await check_database_connection(), + "redis": await check_redis_connection(), + "disk_space": check_disk_space(), + } + } + + # 모든 체크가 통과했는지 확인 + all_checks_passed = all(health_data["checks"].values()) + + if not all_checks_passed: + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content=health_data + ) + + return health_data + +async def check_database_connection() -> bool: + """Check database connection status""" + try: + # 실제 구현에서는 데이터베이스 연결을 테스트 + return True + except Exception: + return False + +async def check_redis_connection() -> bool: + """Check Redis connection status""" + try: + # 실제 구현에서는 Redis 연결을 테스트 + return True + except Exception: + return False + +def check_disk_space() -> bool: + """Check disk space""" + disk_usage = psutil.disk_usage('/') + free_percentage = (disk_usage.free / disk_usage.total) * 100 + return free_percentage > 10 # 10% 이상 여유 공간 필요 + +@app.get("/health/ready", status_code=status.HTTP_200_OK) +async def readiness_check(): + """ + Kubernetes readiness probe endpoint + """ + # 애플리케이션이 트래픽을 받을 준비가 됐는지 확인 + return {"status": "ready", "timestamp": datetime.utcnow().isoformat()} + +@app.get("/health/live", status_code=status.HTTP_200_OK) +async def liveness_check(): + """ + Kubernetes liveness probe endpoint + """ + return {"status": "alive", "timestamp": datetime.utcnow().isoformat()} +``` + +## 5단계: Nginx 리버스 프록시 구성 + +### 개발 환경 Nginx 설정 (`nginx/nginx.conf`) + +```nginx +events { + worker_connections 1024; +} + +http { + upstream fastapi_backend { + # 컨테이너 이름으로 백엔드 지정 + server app:8000; + } + + # 로그 형식 정의 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # 기본 설정 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # Gzip 압축 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/atom+xml image/svg+xml; + + server { + listen 80; + server_name localhost; + + # 보안 헤더 + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + + # 헬스 체크 엔드포인트 + location /health { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 헬스 체크는 빠르게 응답해야 함 + proxy_connect_timeout 5s; + proxy_send_timeout 5s; + proxy_read_timeout 5s; + } + + # API 엔드포인트 + location / { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 타임아웃 설정 + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # 버퍼링 설정 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # 정적 파일 캐싱 (향후 사용) + location /static { + expires 1y; + add_header Cache-Control public; + add_header ETag ""; + } + } +} +``` + +### 프로덕션 Nginx 설정 (`nginx/nginx.prod.conf`) + +```nginx +events { + worker_connections 2048; +} + +http { + upstream fastapi_backend { + # 여러 app 인스턴스에 대한 로드 밸런싱 + server app:8000 max_fails=3 fail_timeout=30s; + # server app2:8000 max_fails=3 fail_timeout=30s; # 스케일링용 + + # Keep-alive + keepalive 32; + } + + # 보안 설정 + server_tokens off; + + # 레이트 제한 + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=health:10m rate=100r/s; + + # SSL 설정 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; + } + + server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # 보안 헤더 + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # 헬스 체크 (레이트 제한 적용) + location /health { + limit_req zone=health burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + + # API 엔드포인트 (레이트 제한 적용) + location / { + limit_req zone=api burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + } +} +``` + +## 6단계: 컨테이너 빌드와 실행 + +### 개발 환경에서 실행 + +
+ +```console +$ cd dockerized-todo-api + +# Docker 이미지 빌드 +$ docker-compose build +Building app +Step 1/15 : FROM python:3.12-slim as builder + ---> abc123def456 +Step 2/15 : RUN apt-get update && apt-get install -y build-essential curl + ---> Running in xyz789abc123 +... +Successfully built def456ghi789 +Successfully tagged dockerized-todo-api_app:latest + +# 컨테이너 실행 (백그라운드) +$ docker-compose up -d +Creating network "dockerized-todo-api_app-network" with driver "bridge" +Creating volume "dockerized-todo-api_redis_data" with default driver +Creating dockerized-todo-redis ... done +Creating dockerized-todo-api ... done +Creating dockerized-todo-nginx ... done + +# 컨테이너 상태 확인 +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------------------ +dockerized-todo-api ./scripts/start.sh Up (healthy) 8000/tcp +dockerized-todo-nginx /docker-entrypoint.sh ngin ... Up 0.0.0.0:80->80/tcp, :::80->80/tcp +dockerized-todo-redis docker-entrypoint.sh redis ... Up (healthy) 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp +``` + +
+ +### 로그 확인 + +
+ +```console +# 모든 서비스 로그 확인 +$ docker-compose logs + +# 특정 서비스 로그 확인 +$ docker-compose logs app +$ docker-compose logs nginx +$ docker-compose logs redis + +# 실시간 로그 확인 +$ docker-compose logs -f app +``` + +
+ +### 헬스 체크 테스트 + +
+ +```console +# 기본 헬스 체크 +$ curl http://localhost/health +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00.123456", + "uptime_seconds": 45.67, + "version": "1.0.0", + "system": { + "memory_usage_percent": 25.3, + "memory_available_mb": 3072.45, + "cpu_usage_percent": 5.2 + }, + "checks": { + "database": true, + "redis": true, + "disk_space": true + } +} + +# Kubernetes probe 테스트 +$ curl http://localhost/health/ready +$ curl http://localhost/health/live +``` + +
+ +## 7단계: 프로덕션 배포 + +### 환경 변수 설정 (`.env.prod`) + +```bash +# 애플리케이션 설정 +ENVIRONMENT=production +DEBUG=false +SECRET_KEY=your-super-secret-key-here +WORKERS=4 + +# 데이터베이스 설정 +DATABASE_URL=postgresql://user:password@db:5432/todoapp +REDIS_URL=redis://:password@redis:6379/0 +REDIS_PASSWORD=your-redis-password + +# 로깅 설정 +LOG_LEVEL=info +LOG_FILE=/app/logs/app.log + +# 보안 설정 +ALLOWED_HOSTS=["your-domain.com"] +CORS_ORIGINS=["https://your-frontend.com"] + +# 모니터링 +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +``` + +### 프로덕션 배포 명령 + +
+ +```console +# 프로덕션 환경에 배포 +$ docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d + +# 스케일링 (app 인스턴스 수 늘리기) +$ docker-compose -f docker-compose.prod.yml up -d --scale app=3 + +# 롤링 업데이트 +$ docker-compose -f docker-compose.prod.yml build app +$ docker-compose -f docker-compose.prod.yml up -d --no-deps app + +# 백업 전 안전한 종료 +$ docker-compose -f docker-compose.prod.yml down --timeout 30 +``` + +
+ +## 8단계: 모니터링과 로깅 + +### Docker 컨테이너 자원 모니터링 + +
+ +```console +# 실시간 자원 사용량 확인 +$ docker stats + +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +abc123def456 dockerized-todo-api 2.34% 128.5MiB / 1GiB 12.55% 1.23MB / 456kB 12.3MB / 4.56MB 15 +def456ghi789 dockerized-todo-nginx 0.12% 12.5MiB / 256MiB 4.88% 456kB / 1.23MB 1.23MB / 456kB 3 +ghi789jkl012 dockerized-todo-redis 1.45% 32.1MiB / 512MiB 6.27% 789kB / 2.34MB 4.56MB / 1.23MB 4 + +# 특정 컨테이너 상세 정보 확인 +$ docker inspect dockerized-todo-api + +# 컨테이너 내부 프로세스 확인 +$ docker-compose exec app ps aux +``` + +
+ +### 로그 집계와 분석 + +```yaml +# docker-compose.logging.yml +version: '3.8' + +services: + # 로그 집계용 ELK Stack + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.6.0 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + networks: + - logging + + logstash: + image: docker.elastic.co/logstash/logstash:8.6.0 + volumes: + - ./logstash/pipeline:/usr/share/logstash/pipeline:ro + - ./logstash/config:/usr/share/logstash/config:ro + networks: + - logging + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana:8.6.0 + ports: + - "5601:5601" + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + networks: + - logging + depends_on: + - elasticsearch + + # 로그 수집용 Fluentd + fluentd: + image: fluent/fluentd:v1.16-debian-1 + volumes: + - ./fluentd/conf:/fluentd/etc:ro + - /var/log:/var/log:ro + networks: + - logging + depends_on: + - elasticsearch + +volumes: + elasticsearch_data: + +networks: + logging: + driver: bridge +``` + +### Prometheus 메트릭 수집 + +```python +# src/monitoring.py +from prometheus_client import Counter, Histogram, Gauge, generate_latest +from fastapi import Request, Response +import time + +# 메트릭 정의 +REQUEST_COUNT = Counter( + 'http_requests_total', + 'Total HTTP requests', + ['method', 'endpoint', 'status_code'] +) + +REQUEST_DURATION = Histogram( + 'http_request_duration_seconds', + 'HTTP request duration in seconds', + ['method', 'endpoint'] +) + +ACTIVE_CONNECTIONS = Gauge( + 'active_connections', + 'Number of active connections' +) + +async def metrics_middleware(request: Request, call_next): + """Prometheus metric collection middleware""" + start_time = time.time() + method = request.method + endpoint = request.url.path + + ACTIVE_CONNECTIONS.inc() + + try: + response = await call_next(request) + status_code = response.status_code + except Exception as e: + status_code = 500 + raise + finally: + duration = time.time() - start_time + REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration) + REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=status_code).inc() + ACTIVE_CONNECTIONS.dec() + + return response + +@app.get("/metrics") +async def get_metrics(): + """Prometheus metric endpoint""" + return Response(generate_latest(), media_type="text/plain") +``` + +## 9단계: CI/CD 파이프라인 구축 + +### GitHub Actions 워크플로 (`.github/workflows/deploy.yml`) + +```yaml +name: Deploy to Production + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx + + - name: Run tests + run: | + pytest tests/ -v --cov=src --cov-report=xml + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + + build: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Deploy to production + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.PROD_HOST }} + username: ${{ secrets.PROD_USERNAME }} + key: ${{ secrets.PROD_SSH_KEY }} + script: | + cd /opt/dockerized-todo-api + + # 새 이미지 풀 + docker-compose -f docker-compose.prod.yml pull + + # 롤링 업데이트 + docker-compose -f docker-compose.prod.yml up -d --no-deps app + + # 헬스 체크 + sleep 30 + curl -f http://localhost/health || exit 1 + + # 이전 이미지 정리 + docker image prune -f +``` + +## 10단계: 보안 강화 + +### 컨테이너 보안 설정 + +```dockerfile +# Dockerfile 에 보안 강화 추가 + +# non-root 사용자로 실행 +USER appuser + +# 읽기 전용 루트 파일 시스템 +# docker run --read-only --tmpfs /tmp dockerized-todo-api + +# 권한 제한 +# docker run --cap-drop=ALL dockerized-todo-api + +# 네트워크 격리 +# docker run --network=none dockerized-todo-api +``` + +### Docker Compose 보안 설정 + +```yaml +# docker-compose.yml 에 보안 설정 추가 +services: + app: + # ... 기존 설정 ... + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + read_only: true + tmpfs: + - /tmp + - /app/logs + user: "1000:1000" +``` + +### 시크릿 관리 + +```yaml +# docker-compose.yml 에 시크릿 설정 추가 +version: '3.8' + +services: + app: + secrets: + - db_password + - api_key + environment: + - DB_PASSWORD_FILE=/run/secrets/db_password + - API_KEY_FILE=/run/secrets/api_key + +secrets: + db_password: + file: ./secrets/db_password.txt + api_key: + external: true +``` + +## 다음 단계 + +Docker 컨테이너화를 마쳤습니다! 다음으로 시도해 볼 만한 것들: + +1. **[커스텀 응답 처리](custom-response-handling.md)** — 고급 API 응답 형식 구현 + + + + +## 요약 + +이 튜토리얼에서는 Docker로 다음 작업을 진행했습니다: + +- ✅ 멀티 스테이지 빌드로 최적화된 컨테이너 이미지 생성 +- ✅ Docker Compose 로 개발 / 프로덕션 환경 구성 +- ✅ Nginx 리버스 프록시와 로드 밸런싱 구성 +- ✅ 헬스 체크와 모니터링 시스템 구축 +- ✅ CI/CD 파이프라인을 통한 자동화된 배포 구현 +- ✅ 프로덕션 수준의 보안 설정 적용 +- ✅ 로깅과 메트릭 수집 시스템 구현 + +이제 FastAPI 애플리케이션을 안전하고 효율적으로 프로덕션 환경에 배포할 수 있습니다! diff --git a/docs/ko/tutorial/domain-starter.md b/docs/ko/tutorial/domain-starter.md new file mode 100644 index 0000000..878b7ab --- /dev/null +++ b/docs/ko/tutorial/domain-starter.md @@ -0,0 +1,392 @@ +# `fastapi-domain-starter`로 도메인 지향 FastAPI + +권장 기본 레이아웃인 `src/app/domains/` 아래 **비즈니스 개념별 폴더 구조**를 사용해 중간 규모 FastAPI 서비스를 구축합니다. 이 튜토리얼에서는 `fastapi-domain-starter` 템플릿을 처음부터 끝까지 따라가며, 프로젝트 생성 방법과 각 최상위 패키지의 역할, 번들된 `items` 예제가 연결되는 방식, 다음 도메인을 추가하는 방법까지 살펴봅니다. + +## 배우는 내용 + +- `fastkit startdemo fastapi-domain-starter`로 프로젝트 생성 +- 레이아웃에서 `core`, `db`, `domains`, `tests`가 맡는 역할 +- 한 도메인이 router → service → repository → schemas → models 로 분할되는 방식 +- 새 도메인 추가의 계약 (items 폴더 복사, 라우터 등록) +- 번들된 `/health` 엔드포인트와 `/api/v1/items` CRUD 가 앱에 어떻게 연결되는지 + +## 사전 요구 사항 + +- Python 3.12+ +- FastAPI-fastkit 설치 (`pip install fastapi-fastkit`) +- 기본적인 FastAPI 개념에 익숙함 (path operation, pydantic 스키마, 의존성) + +처음 만드는 FastAPI 프로젝트라면 [기본 API 서버 구축](basic-api-server.md)부터 시작하세요. 그 튜토리얼은 더 단순한 `fastapi-default` 템플릿을 사용합니다. + +## 1단계: 프로젝트 생성 + +```console +$ fastkit startdemo fastapi-domain-starter +Enter the project name: orders-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Domain-oriented orders service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +`fastkit`이 템플릿을 복사하고, 플레이스홀더를 채우고, 가상 환경을 만들고, 의존성을 설치합니다. 작업이 끝나면 바로 프로젝트 안으로 들어가 보세요: + +```console +$ cd orders-api +$ bash scripts/run-server.sh # 또는: uvicorn src.app.main:app --reload +``` + +API 문서는 에서 확인할 수 있습니다. + +## 2단계: 생성된 트리 + +``` +orders-api/ +├── README.md +├── pyproject.toml # PEP 621 메타데이터 + [tool.fastapi-fastkit] +├── requirements.txt # 핀 고정 의존성 (템플릿이 두 파일을 모두 제공하며, 패키지를 추가하면서 직접 유지보수) +├── .env # SECRET_KEY, ENVIRONMENT +├── .gitignore +├── scripts/ +│ ├── format.sh # black + isort +│ ├── lint.sh # black --check + isort --check + mypy +│ ├── run-server.sh # uvicorn src.app.main:app --reload +│ └── test.sh # pytest +├── src/ +│ ├── __init__.py +│ └── app/ # 애플리케이션 패키지 +│ ├── __init__.py +│ ├── main.py # FastAPI() + 미들웨어 + api_router 포함 +│ ├── core/ # 횡단 관심 설정 +│ │ ├── __init__.py +│ │ └── config.py # pydantic-settings (PROJECT_NAME, CORS, ...) +│ ├── db/ # 영속성 추상화 +│ │ ├── __init__.py +│ │ └── memory.py # InMemoryStore[T] 제네릭 키-값 저장소 +│ ├── api/ # 전송 계층 라우팅 +│ │ ├── __init__.py +│ │ ├── health.py # GET /health +│ │ └── router.py # health + 모든 도메인 라우터 집계 +│ └── domains/ # 비즈니스 개념 (각각 폴더 하나) +│ ├── __init__.py +│ └── items/ # 예제 도메인 +│ ├── __init__.py +│ ├── models.py # @dataclass Item (엔티티) +│ ├── schemas.py # ItemCreate, ItemRead (pydantic) +│ ├── repository.py # InMemoryStore 위의 ItemRepository +│ ├── service.py # ItemService + ItemNotFoundError +│ └── router.py # APIRouter(prefix="/items") +└── tests/ + ├── __init__.py + ├── conftest.py # TestClient 픽스처, 스토어 리셋 + ├── test_health.py + └── test_items.py +``` + +먼저 기억해 둘 핵심은 두 가지입니다: + +1. **`src/app/`**은 **애플리케이션 패키지**입니다. 런타임이 import하는 모든 것이 여기 있고, 테스트도 여기서 import합니다 (`from src.app.main import app`). 바깥쪽 `src/`는 프로젝트를 `pip install` 가능한 패키지로 유지하기 위해 존재합니다. +2. **`src/app/domains//`**는 **개념별 슬라이스**입니다. 각 비즈니스 개념(items, orders, users, ...)이 자기만의 router / service / repository / schemas / models를 갖고, 해당 개념과 관련된 코드를 그 안에 모아 둡니다. + +## 3단계: 각 최상위 패키지의 역할 + +### `src/app/core/` — 설정 + +여기에는 여러 도메인에서 공통으로 쓰는 애플리케이션 설정이 들어갑니다. 번들된 `config.py`는 `.env` / 환경 변수에서 값을 읽는 pydantic-settings `Settings` 클래스를 제공합니다: + +```python +class Settings(BaseSettings): + PROJECT_NAME: str = "" + ENVIRONMENT: Literal["development", "staging", "production"] = "development" + SECRET_KEY: str = secrets.token_urlsafe(32) + API_V1_PREFIX: str = "/api/v1" + BACKEND_CORS_ORIGINS: ... = [] + ... + +settings = Settings() +``` + +`main.py`는 `settings.PROJECT_NAME`, `settings.API_V1_PREFIX`, `settings.all_cors_origins`를 읽어 FastAPI 앱을 구성합니다. + +**`core/`에 추가할 대상:** 특정 도메인에 속하지 않는 모든 공통 요소입니다. 전역 설정, 구조화 로깅, 사용자 정의 미들웨어, 보안 헬퍼 등이 여기에 들어갑니다. + +### `src/app/db/` — 영속성 경계 + +데이터 저장소에 대한 추상화를 담는 영역입니다. 스타터에는 `memory.py`가 함께 들어 있으며, 엔티티 타입별로 쓸 수 있는 프로세스 로컬 `InMemoryStore[T]`를 제공합니다. 각 도메인의 repository는 이 `InMemoryStore`를 감싸기 때문에, 나중에 SQLAlchemy나 비동기 드라이버로 바꾸더라도 repository만 다시 작성하면 됩니다. + +```python +class InMemoryStore(Generic[T]): + def list(self) -> Iterable[T]: ... + def get(self, id_: int) -> Optional[T]: ... + def add(self, item: T) -> int: ... + def replace(self, id_: int, item: T) -> bool: ... + def delete(self, id_: int) -> bool: ... + def clear(self) -> None: ... +``` + +**`db/`를 확장할 시점:** `InMemoryStore` 대신 실제 데이터베이스를 쓸 때입니다. 이때는 데이터베이스 세션 팩토리가 들어 있는 `session.py`를 추가하세요. 도메인 repository의 외부 인터페이스가 바뀌지 않도록 공개 메서드 형태(`list` / `get` / `add` / ...)는 최대한 그대로 유지하는 편이 좋습니다. + +### `src/app/api/` — 전송 라우팅 + +이 영역은 크게 두 부분으로 나뉩니다: + +- `health.py` — `{"status": "ok"}`를 반환하는 `GET /health`를 제공하는 작은 `APIRouter`입니다. 부수 효과가 없어 liveness probe로 쓰기 좋습니다. +- `router.py` — **최상위 집계기**입니다. health 라우터와 모든 도메인 라우터를 한곳에 모으고, 합쳐진 `api_router`를 `/api/v1` 아래 FastAPI 앱에 마운트합니다: + +```python +# src/app/api/router.py +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +``` + +```python +# src/app/main.py +app.include_router(api_router, prefix=settings.API_V1_PREFIX) +``` + +**굳이 여기서 모으는 이유:** 새 도메인을 추가할 때 라우터 등록을 위해 `src/app/api/router.py`만 수정하면 되기 때문입니다. `main.py`는 건드릴 일이 거의 없어집니다. + +### `src/app/domains//` — 비즈니스 슬라이스 + +프로젝트가 자라면 대부분의 코드가 여기 살게 됩니다. 각 도메인은 다섯 개의 파일을 소유합니다: + +| 파일 | 역할 | +|---|---| +| `models.py` | 도메인 엔티티 (스타터에서는 `@dataclass`; 추후 SQLAlchemy / SQLModel 가능). 와이어 포맷이 아닌 내부 모양. | +| `schemas.py` | API 입출력 스키마 (pydantic). 와이어 포맷이 도메인 로직을 건드리지 않고 진화할 수 있도록 엔티티와 분리. | +| `repository.py` | 데이터 접근. 스토어를 item 타입화된 메서드로 감쌈. 영속성을 갈아끼우는 봉합선. | +| `service.py` | 비즈니스 로직. 라우터는 `service` 를 호출하지, 절대 `repository` 를 직접 호출하지 않음. 도메인 고유 예외 (예: `ItemNotFoundError`) 도 여기 위치. | +| `router.py` | HTTP 전송. pydantic 스키마 ↔ service 호출을 변환; 도메인 예외를 `HTTPException` 으로 변환. | + +**의존성 방향**은 `router → service → repository → store` 입니다. 각 계층은 자기 아래 계층에만 의존합니다. 스키마는 router 와 service 에서 참조하고, 모델은 repository 와 service 에서 참조합니다. + +### `tests/` + +런타임 레이아웃을 거울처럼 따라갑니다 — 동작을 고정할 가치가 있는 표면마다 테스트 모듈 하나. 스타터는 다음을 제공합니다: + +- `conftest.py` — 테스트 사이에 items 스토어를 리셋하는 autouse 픽스처와 `TestClient(app)` 를 감싸는 `client` 픽스처. +- `test_health.py` — `GET /api/v1/health` 가 200 + `{"status": "ok"}` 를 반환하는지 검증. +- `test_items.py` — items 엔드포인트의 전체 CRUD 커버리지. 알 수 없는 id 의 404 와 잘못된 페이로드의 422 도 포함. + +다음으로 실행: + +```console +$ bash scripts/test.sh # 또는: pytest +``` + +## 4단계: 번들된 `items` 도메인 살펴보기 + +예제 도메인은 작은 엔티티 위의 CRUD 입니다: + +```python +# src/app/domains/items/models.py +@dataclass +class Item: + id: int + name: str + price: float + in_stock: bool = True +``` + +API 스키마는 입력 모양과 출력 모양을 분리해, 서버가 통제하는 필드 (`id`) 와 검증 (price ≥ 0) 을 추가할 수 있게 합니다: + +```python +# src/app/domains/items/schemas.py +class ItemCreate(BaseModel): + name: str = Field(min_length=1, max_length=120) + price: float = Field(ge=0) + in_stock: bool = True + +class ItemRead(BaseModel): + id: int + name: str + price: float + in_stock: bool + model_config = ConfigDict(from_attributes=True) +``` + +repository 는 인메모리 스토어를 감싸고 삽입 시 id 를 부여합니다: + +```python +# src/app/domains/items/repository.py +class ItemRepository: + def __init__(self, store: Optional[InMemoryStore[Item]] = None) -> None: + self._store = store if store is not None else _store + + def add(self, name: str, price: float, in_stock: bool = True) -> Item: + item = Item(id=0, name=name, price=price, in_stock=in_stock) + new_id = self._store.add(item) + item.id = new_id + return item + # list_all / get / replace / delete / reset 생략 +``` + +service 계층은 비즈니스 규칙이 쌓이는 자리입니다. 지금은 사용자 정의 예외 하나만 있는 얇은 패스스루에 가깝지만, 앞으로는 "열려 있는 주문 안의 item은 삭제할 수 없다" 같은 정책이 이 계층에 들어가게 됩니다: + +```python +# src/app/domains/items/service.py +class ItemNotFoundError(Exception): ... + +class ItemService: + def __init__(self, repository: Optional[ItemRepository] = None) -> None: + self._repository = repository if repository is not None else ItemRepository() + + def get_item(self, item_id: int) -> Item: + item = self._repository.get(item_id) + if item is None: + raise ItemNotFoundError(f"Item {item_id} does not exist") + return item + # list_items / create_item / replace_item / delete_item 생략 +``` + +라우터는 HTTP를 직접 아는 유일한 계층입니다. 테스트에서 쉽게 오버라이드할 수 있도록 service를 FastAPI `Depends(...)`로 받고, `ItemNotFoundError`를 `HTTPException(404)`로 매핑합니다: + +```python +# src/app/domains/items/router.py +router = APIRouter(prefix="/items", tags=["items"]) + +def get_item_service() -> ItemService: + return ItemService() + +@router.get("/{item_id}", response_model=ItemRead) +def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> ItemRead: + try: + return ItemRead.model_validate(service.get_item(item_id)) + except ItemNotFoundError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) +``` + +전체 라우터가 노출하는 것: + +| 메서드 | 경로 | 동작 | +|---|---|---| +| `GET` | `/api/v1/items` | items 목록 | +| `GET` | `/api/v1/items/{item_id}` | 하나 조회 | +| `POST` | `/api/v1/items` | 생성 (201 반환) | +| `PUT` | `/api/v1/items/{item_id}` | 교체 | +| `DELETE` | `/api/v1/items/{item_id}` | 삭제 (204 반환) | +| `GET` | `/api/v1/health` | Liveness probe | + +직접 시도: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name":"Mug","price":9.5,"in_stock":true}' +{"id":1,"name":"Mug","price":9.5,"in_stock":true} + +$ curl http://127.0.0.1:8000/api/v1/items +[{"id":1,"name":"Mug","price":9.5,"in_stock":true}] + +$ curl http://127.0.0.1:8000/api/v1/items/999 +{"detail":"Item 999 does not exist"} +``` + +## 5단계: 다음 도메인 추가 + +스타터는 **도메인 추가가 복사-이름변경 작업**이 되도록 설계됐습니다. `items` 옆에 `users` 도메인을 만들고 싶다고 가정해 봅시다: + +### 1. `items/` 폴더 복사 + +```console +$ cp -r src/app/domains/items src/app/domains/users +``` + +### 2. 엔티티, 스키마, 파일별 클래스 이름 다시 쓰기 + +```python +# src/app/domains/users/models.py +from dataclasses import dataclass + +@dataclass +class User: + id: int + email: str + is_active: bool = True +``` + +```python +# src/app/domains/users/schemas.py +from pydantic import BaseModel, ConfigDict, Field + +class UserCreate(BaseModel): + # 평문 ``str`` 을 쓰면 그대로 붙여 넣어도 안전합니다. 대신 pydantic + # 의 내장 이메일 검증을 사용하려면 선택 의존성 + # (``pip install 'pydantic[email]'`` — ``email-validator`` 를 끌어옴) + # 을 설치하고 ``str`` 을 ``EmailStr`` 로 바꾸세요. + email: str = Field(min_length=3, max_length=320) + is_active: bool = True + +class UserRead(BaseModel): + id: int + email: str + is_active: bool + model_config = ConfigDict(from_attributes=True) +``` + +`models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py` 전반에 걸쳐 `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` 로 이름을 바꾸세요. 라우터의 `prefix="/items"` → `prefix="/users"` 와 `tags=["items"]` → `tags=["users"]` 도 잊지 마세요. + +repository 는 같은 `InMemoryStore` 기반 패턴을 유지할 수 있습니다 — 엔티티 타입에 대해 제네릭이기 때문입니다: + +```python +# src/app/domains/users/repository.py +_store: InMemoryStore[User] = InMemoryStore() + +class UserRepository: + def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: + self._store = store if store is not None else _store + # ... ItemRepository 와 같은 모양 ... +``` + +### 3. 도메인 `__init__.py` 갱신 + +items 도메인은 호출자가 `from src.app.domains.items import service` 처럼 쓸 수 있도록 자기 모듈들을 다시 export 합니다. users 도 같은 방식으로: + +```python +# src/app/domains/users/__init__.py +from src.app.domains.users import ( # noqa: F401 + models, + repository, + router, + schemas, + service, +) +``` + +### 4. 집계기에 라우터 등록 + +여기가 **`domains/users/` 바깥에서 손대야 하는 유일한 파일**입니다: + +```python +# src/app/api/router.py +from src.app.api import health +from src.app.domains.items import router as items_router +from src.app.domains.users import router as users_router # ← 추가 + +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +api_router.include_router(users_router.router) # ← 추가 +``` + +서버 재시작 후 `/docs` 에서 `/api/v1/users` 가 마운트된 것을 확인할 수 있습니다. + +### 5. 테스트 추가 + +`tests/test_items.py` 를 거울처럼 옮긴 `tests/test_users.py` 를 만드세요 — 동일한 클라이언트 기반 모양으로 새 엔드포인트를 호출합니다. `conftest.py` 의 autouse 스토어 리셋 픽스처가 이미 각 테스트를 격리해 줍니다. + +`InMemoryStore` 를 쓰는 두 번째 도메인을 추가한다면, 그 스토어도 리셋하도록 픽스처를 확장하거나, 도메인별로 픽스처 하나씩 두세요. + +## 6단계: 다음 행선지 + +- [아키텍처 프리셋 매트릭스](../reference/preset-feature-matrix.md) 는 `fastkit init --interactive` 가 각 프리셋에 대해 무엇을 생성하는지 보여 줍니다. `domain-starter` 아래에서 어떤 기능 선택이 수동 연결을 필요로 하는지도 포함됩니다. +- [`fastapi-default` 튜토리얼](basic-api-server.md) 은 결정 전에 레이아웃을 비교해 보고 싶다면 계층형 대안을 다룹니다. +- 데이터베이스 통합은 [데이터베이스 통합 튜토리얼](database-integration.md) 이 PostgreSQL + SQLAlchemy + Alembic 패턴을 보여 줍니다. 같은 아이디어가 `src/app/db/` 와 도메인별 `repository.py` 파일들에 그대로 들어갑니다. + +## 정리 + +- **생성**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → `/docs` 에서 문서. +- **레이아웃**: 설정용 `core/`, 영속성 추상화용 `db/`, 비즈니스 슬라이스용 `domains//`, 단일 집계 지점인 `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..f2dcfc9 --- /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 users blog-api +✨ Successfully added new route 'users' to project 'blog-api' + +$ fastkit addroute posts blog-api +✨ Successfully added new route 'posts' to project 'blog-api' + +$ fastkit addroute comments blog-api +✨ 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..d92f4f4 --- /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 users my-first-api + 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..b2e398c --- /dev/null +++ b/docs/ko/user-guide/adding-routes.md @@ -0,0 +1,581 @@ +# 라우트 추가 + +기존 FastAPI 프로젝트에 새 API 라우트를 추가하는 방법을 안내합니다. + +## 기본 라우트 추가 + +### `addroute` 명령 사용 + +FastAPI-fastkit의 `addroute` 명령을 쓰면 새 라우트를 간편하게 추가할 수 있습니다: + +
+ +```console +$ fastkit addroute users my-awesome-api + 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 products my-awesome-api +$ fastkit addroute orders my-awesome-api +$ fastkit addroute categories my-awesome-api + +# 각각이 전체 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 new file mode 100644 index 0000000..f89fed6 --- /dev/null +++ b/docs/ko/user-guide/choosing-a-starter.md @@ -0,0 +1,145 @@ +# 어떤 스타터를 선택해야 할까? + +FastAPI-fastkit은 프로젝트를 시작하는 여러 가지 방법을 제공합니다. 이 페이지는 처음 쓰는 분을 위한 **선택 가이드**입니다. 여기서 방향을 정한 뒤, 실제 프로젝트 생성은 [퀵 스타트](quick-start.md)로 넘어가 진행하세요. + +확신이 없다면, 답은 다음과 같습니다: + +> **`fastkit init --interactive`로 시작해서 `domain-starter` 프리셋을 선택하세요.** 현재 권장 기본값입니다. + +이 페이지의 나머지 부분은 그 이유와, 다른 선택을 해야 할 경우를 설명합니다. + +## TL;DR — 사용자 유형별 선택 + +| 당신이... | 시작점 | +|---|---| +| 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`**) | + +## `startdemo`와 `init --interactive`는 무엇이 다른가? + +이 둘이 메인 진입점이며, 서로 다른 용도를 갖습니다. + +### `fastkit startdemo