diff --git a/README.md b/README.md index 259539177..533e4c079 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,65 @@ Use `/tasks` to create an actionable task list, then ask your agent to implement For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md). +## πŸš€ FastAPI Server Usage + +This project has been converted to a FastAPI web service. Here's how to run the server and use the API. + +### 1. Installation + +Ensure you have all the necessary dependencies installed: + +```bash +pip install -r requirements.txt +``` + +### 2. Running the Server + +You can run the server using `uvicorn`: + +```bash +uvicorn src.main:app --reload +``` + +The server will be available at `http://localhost:8000`. + +### 3. API Endpoints + +All endpoints are available under the `/api/v1` prefix. You can access the interactive OpenAPI documentation at `http://localhost:8000/docs`. + +#### Generate a Specification + +- **Endpoint:** `POST /api/v1/spec/generate` +- **Description:** Creates a feature specification document from a user's description. +- **Example Request:** + ```bash + curl -X POST "http://localhost:8000/api/v1/spec/generate" \ + -H "Content-Type: application/json" \ + -d '{"feature_description": "A new login system using email and password."}' + ``` + +#### Generate an Implementation Plan + +- **Endpoint:** `POST /api/v1/plan/generate` +- **Description:** Creates a technical implementation plan for a given feature. +- **Example Request:** + ```bash + curl -X POST "http://localhost:8000/api/v1/plan/generate" \ + -H "Content-Type: application/json" \ + -d '{"feature_name": "New Login System"}' + ``` + +#### Generate a Task List + +- **Endpoint:** `POST /api/v1/tasks/generate` +- **Description:** Creates a list of development tasks for a given feature. +- **Example Request:** + ```bash + curl -X POST "http://localhost:8000/api/v1/tasks/generate" \ + -H "Content-Type: application/json" \ + -d '{"feature_name": "New Login System"}' + ``` + ## πŸ“š Core philosophy Spec-Driven Development is a structured process that emphasizes: diff --git a/extracted_prompts.md b/extracted_prompts.md new file mode 100644 index 000000000..2bdcb73fd --- /dev/null +++ b/extracted_prompts.md @@ -0,0 +1,91 @@ +# Extracted LLM Prompts + +## From spec-template.md: For AI Generation + +When creating this spec from a user prompt: +1. **Mark all ambiguities**: Use [NEEDS CLARIFICATION: specific question] for any assumption you'd need to make +2. **Don't guess**: If the prompt doesn't specify something (e.g., "login system" without auth method), mark it +3. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item +4. **Common underspecified areas**: + - User types and permissions + - Data retention/deletion policies + - Performance targets and scale + - Error handling behaviors + - Integration requirements + - Security/compliance needs + +--- + +## From plan-template.md: Execution Flow (/plan command scope) + +``` +1. Load feature spec from Input path + β†’ If not found: ERROR "No feature spec at {path}" +2. Fill Technical Context (scan for NEEDS CLARIFICATION) + β†’ Detect Project Type from context (web=frontend+backend, mobile=app+api) + β†’ Set Structure Decision based on project type +3. Evaluate Constitution Check section below + β†’ If violations exist: Document in Complexity Tracking + β†’ If no justification possible: ERROR "Simplify approach first" + β†’ Update Progress Tracking: Initial Constitution Check +4. Execute Phase 0 β†’ research.md + β†’ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns" +5. Execute Phase 1 β†’ contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, or `GEMINI.md` for Gemini CLI). +6. Re-evaluate Constitution Check section + β†’ If new violations: Refactor design, return to Phase 1 + β†’ Update Progress Tracking: Post-Design Constitution Check +7. Plan Phase 2 β†’ Describe task generation approach (DO NOT create tasks.md) +8. STOP - Ready for /tasks command +``` + +--- + +## From tasks-template.md: Execution Flow (main) & Task Generation Rules + +### Execution Flow (main) +``` +1. Load plan.md from feature directory + β†’ If not found: ERROR "No implementation plan found" + β†’ Extract: tech stack, libraries, structure +2. Load optional design documents: + β†’ data-model.md: Extract entities β†’ model tasks + β†’ contracts/: Each file β†’ contract test task + β†’ research.md: Extract decisions β†’ setup tasks +3. Generate tasks by category: + β†’ Setup: project init, dependencies, linting + β†’ Tests: contract tests, integration tests + β†’ Core: models, services, CLI commands + β†’ Integration: DB, middleware, logging + β†’ Polish: unit tests, performance, docs +4. Apply task rules: + β†’ Different files = mark [P] for parallel + β†’ Same file = sequential (no [P]) + β†’ Tests before implementation (TDD) +5. Number tasks sequentially (T001, T002...) +6. Generate dependency graph +7. Create parallel execution examples +8. Validate task completeness: + β†’ All contracts have tests? + β†’ All entities have models? + β†’ All endpoints implemented? +9. Return: SUCCESS (tasks ready for execution) +``` + +### Task Generation Rules +*Applied during main() execution* + +1. **From Contracts**: + - Each contract file β†’ contract test task [P] + - Each endpoint β†’ implementation task + +2. **From Data Model**: + - Each entity β†’ model creation task [P] + - Relationships β†’ service layer tasks + +3. **From User Stories**: + - Each story β†’ integration test [P] + - Quickstart scenarios β†’ validation tasks + +4. **Ordering**: + - Setup β†’ Tests β†’ Models β†’ Services β†’ Endpoints β†’ Polish + - Dependencies block parallel execution \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..da8533f76 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +aiofiles \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/api/endpoints/__init__.py b/src/api/endpoints/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/api/endpoints/plans.py b/src/api/endpoints/plans.py new file mode 100644 index 000000000..f306ffefc --- /dev/null +++ b/src/api/endpoints/plans.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +""" +κ΅¬ν˜„ κ³„νš(Plan) 생성과 κ΄€λ ¨λœ API μ—”λ“œν¬μΈνŠΈλ₯Ό μ •μ˜ν•˜λŠ” 파일. +""" + +from fastapi import APIRouter, Depends +from src.models.documents import PlanGenerateRequest, ApiResponse +from src.services.document_generator import DocumentGeneratorService + +# 'plans' λΌμš°ν„°λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +router = APIRouter(prefix="/plan", tags=["Plans"]) + +# μ˜μ‘΄μ„± μ£Όμž… ν•¨μˆ˜λŠ” μž¬μ‚¬μš© κ°€λŠ₯ν•˜λ―€λ‘œ, μ—¬κΈ°μ„œλŠ” κ°„λ‹¨ν•˜κ²Œ μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +# 더 큰 μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œλŠ” 곡톡 μ˜μ‘΄μ„± 관리 νŒŒμΌμ„ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. +def get_doc_service() -> DocumentGeneratorService: + return DocumentGeneratorService() + +@router.post("/generate", response_model=ApiResponse[str]) +async def generate_plan( + request_body: PlanGenerateRequest, + doc_service: DocumentGeneratorService = Depends(get_doc_service) +): + """ + κΈ°λŠ₯ 이름을 λ°›μ•„ κ΅¬ν˜„ κ³„νš(plan) λ¬Έμ„œλ₯Ό μƒμ„±ν•˜λŠ” API μ—”λ“œν¬μΈνŠΈ. + + Args: + request_body (PlanGenerateRequest): κΈ°λŠ₯ 이름이 λ‹΄κΈ΄ μš”μ²­ λ³Έλ¬Έ. + doc_service (DocumentGeneratorService): λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λŠ” μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€. + + Returns: + ApiResponse[str]: μƒμ„±λœ κ³„νš λ¬Έμ„œμ˜ λ‚΄μš©μ„ 담은 곡톡 응닡 객체. + """ + try: + # μ„œλΉ„μŠ€μ˜ create_plan λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ κ³„νš λ¬Έμ„œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. + generated_plan = await doc_service.create_plan( + feature_name=request_body.feature_name + ) + return ApiResponse( + success=True, + message="κ³„νš λ¬Έμ„œκ°€ μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", + data=generated_plan + ) + except Exception as e: + return ApiResponse( + success=False, + message=f"였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}", + data=None + ) \ No newline at end of file diff --git a/src/api/endpoints/specifications.py b/src/api/endpoints/specifications.py new file mode 100644 index 000000000..27772bfce --- /dev/null +++ b/src/api/endpoints/specifications.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +""" +사양(Specification) 생성과 κ΄€λ ¨λœ API μ—”λ“œν¬μΈνŠΈλ₯Ό μ •μ˜ν•˜λŠ” 파일. +""" + +# APIRouterλŠ” μ—”λ“œν¬μΈνŠΈλ“€μ„ κ·Έλ£Ήν™”ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. +from fastapi import APIRouter, Depends +# Pydantic λͺ¨λΈλ“€μ„ κ°€μ Έμ˜΅λ‹ˆλ‹€. 데이터 μœ νš¨μ„± 검사 및 응닡 ꡬ쑰 μ •μ˜μ— μ‚¬μš©λ©λ‹ˆλ‹€. +from src.models.documents import SpecGenerateRequest, ApiResponse +# λ¬Έμ„œ 생성 λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ λ‹΄κ³  μžˆλŠ” μ„œλΉ„μŠ€ 클래슀λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. +from src.services.document_generator import DocumentGeneratorService + +# 'specifications' λΌμš°ν„°λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +# prefix="/spec"λŠ” 이 λΌμš°ν„°μ— μ†ν•œ λͺ¨λ“  κ²½λ‘œκ°€ /spec으둜 μ‹œμž‘ν•˜λ„λ‘ ν•©λ‹ˆλ‹€. +# tags=["Specifications"]λŠ” OpenAPI λ¬Έμ„œμ—μ„œ μ—”λ“œν¬μΈνŠΈλ“€μ„ 'Specifications' 그룹으둜 λ¬Άμ–΄μ€λ‹ˆλ‹€. +router = APIRouter(prefix="/spec", tags=["Specifications"]) + +# μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜λŠ” μ˜μ‘΄μ„± μ£Όμž…(Dependency Injection) ν•¨μˆ˜. +# 이 ν•¨μˆ˜λŠ” FastAPIκ°€ μ—”λ“œν¬μΈνŠΈ ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•  λ•Œλ§ˆλ‹€ +# DocumentGeneratorService의 μƒˆ μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜μ—¬ μ œκ³΅ν•©λ‹ˆλ‹€. +# 이λ₯Ό 톡해 μ½”λ“œμ˜ μž¬μ‚¬μš©μ„±κ³Ό ν…ŒμŠ€νŠΈ μš©μ΄μ„±μ΄ ν–₯μƒλ©λ‹ˆλ‹€. +def get_spec_service() -> DocumentGeneratorService: + """ + DocumentGeneratorService μΈμŠ€ν„΄μŠ€λ₯Ό λ°˜ν™˜ν•˜λŠ” μ˜μ‘΄μ„± ν•¨μˆ˜. + """ + # μ„œλΉ„μŠ€ 클래슀의 μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€. + return DocumentGeneratorService() + +@router.post("/generate", response_model=ApiResponse[str]) +async def generate_specification( + # request_bodyλŠ” SpecGenerateRequest λͺ¨λΈμ— 따라 μœ νš¨μ„± 검사λ₯Ό κ±°μΉ©λ‹ˆλ‹€. + # ν΄λΌμ΄μ–ΈνŠΈκ°€ 보낸 JSON 본문이 이 λͺ¨λΈμ˜ ꡬ쑰와 μΌμΉ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€. + request_body: SpecGenerateRequest, + # Depends(get_spec_service)λŠ” get_spec_service ν•¨μˆ˜λ₯Ό μ‹€ν–‰ν•˜κ³  + # κ·Έ λ°˜ν™˜κ°’(DocumentGeneratorService μΈμŠ€ν„΄μŠ€)을 spec_service λ§€κ°œλ³€μˆ˜μ— μ£Όμž…ν•©λ‹ˆλ‹€. + spec_service: DocumentGeneratorService = Depends(get_spec_service) +): + """ + μ‚¬μš©μžλ‘œλΆ€ν„° κΈ°λŠ₯ μ„€λͺ…을 λ°›μ•„ 사양(specification) λ¬Έμ„œλ₯Ό μƒμ„±ν•˜λŠ” API μ—”λ“œν¬μΈνŠΈ. + + Args: + request_body (SpecGenerateRequest): μ‚¬μš©μžκ°€ μ œκ³΅ν•œ κΈ°λŠ₯ μ„€λͺ…이 λ‹΄κΈ΄ μš”μ²­ λ³Έλ¬Έ. + spec_service (DocumentGeneratorService): λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λŠ” μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€. + FastAPI의 μ˜μ‘΄μ„± μ£Όμž…μ— μ˜ν•΄ μ œκ³΅λ©λ‹ˆλ‹€. + + Returns: + ApiResponse[str]: μƒμ„±λœ 사양 λ¬Έμ„œμ˜ λ‚΄μš©μ„ 담은 곡톡 응닡 객체. + μ‹€νŒ¨ μ‹œ μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό 포함할 수 μžˆμŠ΅λ‹ˆλ‹€. + """ + try: + # μ£Όμž…λœ μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€μ˜ create_specification λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€. + # μš”μ²­ λ³Έλ¬Έμ—μ„œ 받은 feature_description을 인자둜 μ „λ‹¬ν•©λ‹ˆλ‹€. + # 비동기 ν•¨μˆ˜μ΄λ―€λ‘œ await ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. + generated_spec = await spec_service.create_specification( + feature_description=request_body.feature_description + ) + + # μ„±κ³΅μ μœΌλ‘œ λ¬Έμ„œκ°€ μƒμ„±λ˜λ©΄, ApiResponse λͺ¨λΈμ— 맞좰 응닡을 κ΅¬μ„±ν•©λ‹ˆλ‹€. + # data ν•„λ“œμ— μƒμ„±λœ 사양 λ¬Έμ„œ λ‚΄μš©μ„ λ‹΄μ•„ λ°˜ν™˜ν•©λ‹ˆλ‹€. + return ApiResponse( + success=True, + message="사양 λ¬Έμ„œκ°€ μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", + data=generated_spec + ) + except Exception as e: + # μ„œλΉ„μŠ€ λ‘œμ§μ—μ„œ μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ©΄, μ‹€νŒ¨ 응닡을 κ΅¬μ„±ν•©λ‹ˆλ‹€. + # successλ₯Ό False둜 μ„€μ •ν•˜κ³ , μ˜ˆμ™Έ λ©”μ‹œμ§€λ₯Ό message ν•„λ“œμ— λ‹΄μ•„ λ°˜ν™˜ν•©λ‹ˆλ‹€. + # μ‹€μ œ ν”„λ‘œλ•μ…˜ ν™˜κ²½μ—μ„œλŠ” HTTP 500 μ—λŸ¬λ₯Ό λ°˜ν™˜ν•˜κ³  둜그λ₯Ό λ‚¨κΈ°λŠ” 것이 더 μ’‹μŠ΅λ‹ˆλ‹€. + return ApiResponse( + success=False, + message=f"였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}", + data=None + ) \ No newline at end of file diff --git a/src/api/endpoints/tasks.py b/src/api/endpoints/tasks.py new file mode 100644 index 000000000..0fce8c0ef --- /dev/null +++ b/src/api/endpoints/tasks.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +""" +μž‘μ—… λͺ©λ‘(Tasks) 생성과 κ΄€λ ¨λœ API μ—”λ“œν¬μΈνŠΈλ₯Ό μ •μ˜ν•˜λŠ” 파일. +""" + +from fastapi import APIRouter, Depends +from src.models.documents import TasksGenerateRequest, ApiResponse +from src.services.document_generator import DocumentGeneratorService + +# 'tasks' λΌμš°ν„°λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +router = APIRouter(prefix="/tasks", tags=["Tasks"]) + +# μ˜μ‘΄μ„± μ£Όμž… ν•¨μˆ˜ +def get_doc_service() -> DocumentGeneratorService: + return DocumentGeneratorService() + +@router.post("/generate", response_model=ApiResponse[str]) +async def generate_tasks( + request_body: TasksGenerateRequest, + doc_service: DocumentGeneratorService = Depends(get_doc_service) +): + """ + κΈ°λŠ₯ 이름을 λ°›μ•„ μž‘μ—… λͺ©λ‘(tasks) λ¬Έμ„œλ₯Ό μƒμ„±ν•˜λŠ” API μ—”λ“œν¬μΈνŠΈ. + + Args: + request_body (TasksGenerateRequest): κΈ°λŠ₯ 이름이 λ‹΄κΈ΄ μš”μ²­ λ³Έλ¬Έ. + doc_service (DocumentGeneratorService): λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λŠ” μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€. + + Returns: + ApiResponse[str]: μƒμ„±λœ μž‘μ—… λͺ©λ‘μ˜ λ‚΄μš©μ„ 담은 곡톡 응닡 객체. + """ + try: + # μ„œλΉ„μŠ€μ˜ create_tasks λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ μž‘μ—… λͺ©λ‘μ„ μƒμ„±ν•©λ‹ˆλ‹€. + generated_tasks = await doc_service.create_tasks( + feature_name=request_body.feature_name + ) + return ApiResponse( + success=True, + message="μž‘μ—… λͺ©λ‘μ΄ μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", + data=generated_tasks + ) + except Exception as e: + return ApiResponse( + success=False, + message=f"였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}", + data=None + ) \ No newline at end of file diff --git a/src/api/router.py b/src/api/router.py new file mode 100644 index 000000000..d5ecffa44 --- /dev/null +++ b/src/api/router.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" +API λΌμš°ν„°λ₯Ό ν†΅ν•©ν•˜λŠ” 파일. +μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λͺ¨λ“  μ—”λ“œν¬μΈνŠΈ λΌμš°ν„°λ“€μ„ λͺ¨μ•„ FastAPI 앱에 등둝할 수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. +""" + +# APIRouter 클래슀λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. +from fastapi import APIRouter +# 사양(spec) 생성 κ΄€λ ¨ μ—”λ“œν¬μΈνŠΈκ°€ μ •μ˜λœ λΌμš°ν„°λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. +from src.api.endpoints import specifications + +# μ΅œμƒμœ„ API λΌμš°ν„°λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +# 이 λΌμš°ν„°λŠ” λ‹€λ₯Έ λͺ¨λ“  κΈ°λŠ₯별 λΌμš°ν„°λ“€μ„ ν¬ν•¨ν•˜κ²Œ λ©λ‹ˆλ‹€. +api_router = APIRouter() + +# specifications λΌμš°ν„°λ₯Ό api_router에 ν¬ν•¨μ‹œν‚΅λ‹ˆλ‹€. +# μ΄λ ‡κ²Œ ν•˜λ©΄ specifications.py에 μ •μ˜λœ λͺ¨λ“  μ—”λ“œν¬μΈνŠΈλ“€μ΄ +# 이 api_router에 λ“±λ‘λ©λ‹ˆλ‹€. +api_router.include_router(specifications.router) + +# 여기에 λ‚˜μ€‘μ— plan, tasks λΌμš°ν„°λ„ 좔가될 μ˜ˆμ •μž…λ‹ˆλ‹€. +from src.api.endpoints import plans, tasks +api_router.include_router(plans.router) +api_router.include_router(tasks.router) \ No newline at end of file diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/config.py b/src/core/config.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/main.py b/src/main.py new file mode 100644 index 000000000..27160d927 --- /dev/null +++ b/src/main.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +""" +FastAPI μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 메인 파일. +μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μƒμ„±ν•˜κ³ , API λΌμš°ν„°λ₯Ό ν¬ν•¨ν•˜λ©°, μ„œλ²„ μ‹€ν–‰μ˜ μ‹œμž‘μ  역할을 ν•©λ‹ˆλ‹€. +""" + +# FastAPI 클래슀λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μƒμ„±ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. +from fastapi import FastAPI +# API λΌμš°ν„°λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. 이 λΌμš°ν„°λŠ” λͺ¨λ“  API μ—”λ“œν¬μΈνŠΈλ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€. +from src.api.router import api_router + +# FastAPI μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +# 이 μΈμŠ€ν„΄μŠ€κ°€ λͺ¨λ“  API의 쀑심이 λ©λ‹ˆλ‹€. +app = FastAPI( + title="Document Generator API", # API의 제λͺ© + description="CLI-based document generator converted to a FastAPI service.", # API에 λŒ€ν•œ μ„€λͺ… + version="0.1.0", # API 버전 +) + +# 루트 μ—”λ“œν¬μΈνŠΈ: μ„œλ²„μ˜ μƒνƒœλ₯Ό ν™•μΈν•˜κΈ° μœ„ν•œ κ°„λ‹¨ν•œ ν…ŒμŠ€νŠΈ κ²½λ‘œμž…λ‹ˆλ‹€. +@app.get("/", tags=["Status"]) +def health_check(): + """ + μ„œλ²„ ν—¬μŠ€ 체크(Health Check) μ—”λ“œν¬μΈνŠΈ. + + Returns: + dict: μ„œλ²„κ°€ μ •μƒμ μœΌλ‘œ λ™μž‘ν•˜κ³  μžˆμŒμ„ μ•Œλ¦¬λŠ” λ©”μ‹œμ§€. + """ + # μ„œλ²„ μƒνƒœλ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ”•μ…”λ„ˆλ¦¬ 객체. + # 이 λ©”μ‹œμ§€λŠ” ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μ„œλ²„κ°€ μ€€λΉ„λ˜μ—ˆμŒμ„ μ•Œλ¦½λ‹ˆλ‹€. + response_message = {"status": "ok", "message": "Welcome to the Document Generator API!"} + return response_message + +# API λΌμš°ν„°λ₯Ό μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— ν¬ν•¨μ‹œν‚΅λ‹ˆλ‹€. +# prefix="/api/v1"은 λͺ¨λ“  API κ²½λ‘œκ°€ /api/v1으둜 μ‹œμž‘ν•˜λ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€. +# 예λ₯Ό λ“€μ–΄, specifications.py에 μ •μ˜λœ /generate-spec κ²½λ‘œλŠ” +# http://localhost:8000/api/v1/generate-spec 으둜 μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€. +app.include_router(api_router, prefix="/api/v1") + +# Uvicorn μ„œλ²„λ₯Ό 직접 μ‹€ν–‰ν•˜κΈ° μœ„ν•œ μ½”λ“œ λΈ”λ‘μž…λ‹ˆλ‹€. +# 'python src/main.py' λͺ…λ ΉμœΌλ‘œ μ‹€ν–‰ν•  λ•Œ μ‚¬μš©λ©λ‹ˆλ‹€. +if __name__ == "__main__": + # uvicorn 라이브러리λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. ASGI μ„œλ²„ κ΅¬ν˜„μ²΄μž…λ‹ˆλ‹€. + import uvicorn + # uvicorn.run()을 ν˜ΈμΆœν•˜μ—¬ μ„œλ²„λ₯Ό μ‹œμž‘ν•©λ‹ˆλ‹€. + # "src.main:app"은 src ν΄λ”μ˜ main.py 파일 μ•ˆμ— μžˆλŠ” app 객체λ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€. + # host="0.0.0.0"은 λͺ¨λ“  λ„€νŠΈμ›Œν¬ μΈν„°νŽ˜μ΄μŠ€μ—μ„œ 접속을 ν—ˆμš©ν•©λ‹ˆλ‹€. + # port=8000은 8000번 포트λ₯Ό μ‚¬μš©ν•˜λ„λ‘ μ„€μ •ν•©λ‹ˆλ‹€. + # reload=TrueλŠ” μ½”λ“œ λ³€κ²½ μ‹œ μ„œλ²„λ₯Ό μžλ™μœΌλ‘œ μž¬μ‹œμž‘ν•˜λŠ” 개발용 μ˜΅μ…˜μž…λ‹ˆλ‹€. + uvicorn.run("src.main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/models/common.py b/src/models/common.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/models/documents.py b/src/models/documents.py new file mode 100644 index 000000000..5585c656f --- /dev/null +++ b/src/models/documents.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +""" +Pydantic λͺ¨λΈμ„ μ •μ˜ν•˜λŠ” νŒŒμΌμž…λ‹ˆλ‹€. +API μš”μ²­ 및 μ‘λ‹΅μ˜ 데이터 ꡬ쑰λ₯Ό μ •μ˜ν•˜κ³  μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. +""" + +# Pydantic λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ BaseModel을 κ°€μ Έμ˜΅λ‹ˆλ‹€. +# λͺ¨λ“  λͺ¨λΈμ€ 이 BaseModel을 상속받아 λ§Œλ“€μ–΄μ§‘λ‹ˆλ‹€. +from pydantic import BaseModel +# typing λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œ μ œλ„€λ¦­ νƒ€μž…μ„ μ§€μ›ν•˜κΈ° μœ„ν•΄ Genericκ³Ό TypeVarλ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. +from typing import Generic, TypeVar + +# --- μš”μ²­ λͺ¨λΈ --- + +class SpecGenerateRequest(BaseModel): + """ + '사양(spec) 생성' API에 λŒ€ν•œ μš”μ²­ λͺ¨λΈ. + + Attributes: + feature_description (str): μ‚¬μš©μžκ°€ μž…λ ₯ν•œ κΈ°λŠ₯에 λŒ€ν•œ 상세 μ„€λͺ…. + 이 μ„€λͺ…을 기반으둜 사양 λ¬Έμ„œκ°€ μƒμ„±λ©λ‹ˆλ‹€. + """ + # κΈ°λŠ₯ μ„€λͺ…을 μ €μž₯ν•˜λŠ” λ³€μˆ˜. λ¬Έμžμ—΄ νƒ€μž…μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€. + feature_description: str + + +class PlanGenerateRequest(BaseModel): + """ + 'κ³„νš(plan) 생성' API에 λŒ€ν•œ μš”μ²­ λͺ¨λΈ. + + Attributes: + feature_name (str): κ³„νšμ„ 생성할 κΈ°λŠ₯의 이름. + 이 이름은 보톡 μ‚¬μ–‘μ—μ„œ μ •μ˜λ©λ‹ˆλ‹€. + """ + # κΈ°λŠ₯ 이름을 μ €μž₯ν•˜λŠ” λ³€μˆ˜. λ¬Έμžμ—΄ νƒ€μž…μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€. + feature_name: str + + +class TasksGenerateRequest(BaseModel): + """ + 'μž‘μ—…(tasks) 생성' API에 λŒ€ν•œ μš”μ²­ λͺ¨λΈ. + + Attributes: + feature_name (str): μž‘μ—… λͺ©λ‘μ„ 생성할 κΈ°λŠ₯의 이름. + 이 이름은 κ³„νšκ³Ό 사양을 μ°Έμ‘°ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. + """ + # κΈ°λŠ₯ 이름을 μ €μž₯ν•˜λŠ” λ³€μˆ˜. λ¬Έμžμ—΄ νƒ€μž…μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€. + feature_name: str + + +# --- 응닡 λͺ¨λΈ --- + +# μ œλ„€λ¦­ 데이터 νƒ€μž…μ„ μœ„ν•œ TypeVarλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. +# μ–΄λ–€ νƒ€μž…μ΄λ“  담을 수 μžˆλŠ” μ œλ„€λ¦­ 응닡 λͺ¨λΈμ„ λ§Œλ“€κΈ° μœ„ν•΄ μ‚¬μš©λ©λ‹ˆλ‹€. +T = TypeVar('T') + +class ApiResponse(BaseModel, Generic[T]): + """ + λͺ¨λ“  API에 λŒ€ν•œ 곡톡적인 응닡 래퍼(wrapper) λͺ¨λΈ. + μΌκ΄€λœ 응닡 ꡬ쑰λ₯Ό μ œκ³΅ν•˜κΈ° μœ„ν•΄ μ‚¬μš©λ©λ‹ˆλ‹€. + + Attributes: + success (bool): API μš”μ²­μ˜ 성곡 μ—¬λΆ€λ₯Ό λ‚˜νƒ€λ‚΄λŠ” ν”Œλž˜κ·Έ. + message (str): API 처리 결과에 λŒ€ν•œ λ©”μ‹œμ§€. + data (T | None): API의 μ‹€μ œ κ²°κ³Ό 데이터. μ œλ„€λ¦­ νƒ€μž…μ„ μ‚¬μš©ν•˜μ—¬ + λ‹€μ–‘ν•œ μ’…λ₯˜μ˜ 데이터λ₯Ό 담을 수 μžˆμŠ΅λ‹ˆλ‹€. + """ + # 성곡 μ—¬λΆ€λ₯Ό λ‚˜νƒ€λ‚΄λŠ” λ³€μˆ˜. λΆˆλ¦¬μ–Έ νƒ€μž…μž…λ‹ˆλ‹€. + success: bool = True + # 응닡 λ©”μ‹œμ§€λ₯Ό μ €μž₯ν•˜λŠ” λ³€μˆ˜. λ¬Έμžμ—΄ νƒ€μž…μž…λ‹ˆλ‹€. + message: str = "μ„±κ³΅μ μœΌλ‘œ μ²˜λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€." + # μ‹€μ œ 데이터λ₯Ό λ‹΄λŠ” λ³€μˆ˜. νƒ€μž…μ€ μ œλ„€λ¦­ T이며, 데이터가 없을 경우 None이 될 수 μžˆμŠ΅λ‹ˆλ‹€. + data: T | None = None \ No newline at end of file diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/services/document_generator.py b/src/services/document_generator.py new file mode 100644 index 000000000..69d7f4388 --- /dev/null +++ b/src/services/document_generator.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- + +""" +λ¬Έμ„œ 생성과 κ΄€λ ¨λœ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ μ²˜λ¦¬ν•˜λŠ” μ„œλΉ„μŠ€ 파일. +ν…œν”Œλ¦Ώ νŒŒμΌμ„ 읽고, 동적 데이터λ₯Ό μ‚½μž…ν•˜μ—¬ μ΅œμ’… λ¬Έμ„œλ₯Ό μƒμ„±ν•˜λŠ” 역할을 ν•©λ‹ˆλ‹€. +""" + +# λ‚ μ§œμ™€ μ‹œκ°„μ„ 닀루기 μœ„ν•œ datetime 라이브러리λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. +from datetime import datetime +# 비동기 파일 I/O μž‘μ—…μ„ μœ„ν•œ aiofiles 라이브러리λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€. +import aiofiles + +class DocumentGeneratorService: + """ + λ¬Έμ„œ 생성 λ‘œμ§μ„ λ‹΄λ‹Ήν•˜λŠ” μ„œλΉ„μŠ€ 클래슀. + """ + + async def create_specification(self, feature_description: str) -> str: + """ + κΈ°λŠ₯ μ„€λͺ…을 기반으둜 사양(specification) λ¬Έμ„œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. + + 이 ν•¨μˆ˜λŠ” λΉ„λ™κΈ°μ μœΌλ‘œ λ™μž‘ν•˜λ©°, 'spec-template.md' νŒŒμΌμ„ 읽어 + ν•„μš”ν•œ 뢀뢄을 μ‚¬μš©μžμ˜ μž…λ ₯κ³Ό ν˜„μž¬ λ‚ μ§œλ‘œ κ΅μ²΄ν•œ ν›„, + μ™„μ„±λœ λ¬Έμ„œ λ‚΄μš©μ„ λ¬Έμžμ—΄λ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€. + + Args: + feature_description (str): API μš”μ²­μ„ 톡해 전달받은 μ‚¬μš©μžμ˜ κΈ°λŠ₯ μ„€λͺ…. + + Returns: + str: 동적 데이터가 μ±„μ›Œμ§„ μ™„μ„±λœ 사양 λ¬Έμ„œμ˜ λ‚΄μš©. + + Raises: + FileNotFoundError: 'templates/spec-template.md' 파일이 μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 λ°œμƒ. + Exception: 파일 읽기 λ˜λŠ” λ‹€λ₯Έ 예기치 μ•Šμ€ 였λ₯˜ λ°œμƒ μ‹œ. + """ + try: + # ν…œν”Œλ¦Ώ 파일의 경둜λ₯Ό λ³€μˆ˜μ— μ €μž₯ν•©λ‹ˆλ‹€. + template_path = "templates/spec-template.md" + + # aiofilesλ₯Ό μ‚¬μš©ν•˜μ—¬ ν…œν”Œλ¦Ώ νŒŒμΌμ„ λΉ„λ™κΈ°μ μœΌλ‘œ μ—½λ‹ˆλ‹€. + # 'async with' ꡬ문은 파일이 μžλ™μœΌλ‘œ λ‹«νžˆλ„λ‘ 보μž₯ν•©λ‹ˆλ‹€. + async with aiofiles.open(template_path, mode='r', encoding='utf-8') as f: + # 파일의 전체 λ‚΄μš©μ„ λΉ„λ™κΈ°μ μœΌλ‘œ 읽어 content λ³€μˆ˜μ— μ €μž₯ν•©λ‹ˆλ‹€. + content = await f.read() + + # ν˜„μž¬ λ‚ μ§œλ₯Ό 'YYYY-MM-DD' ν˜•μ‹μ˜ λ¬Έμžμ—΄λ‘œ ν¬λ§·νŒ…ν•©λ‹ˆλ‹€. + # 이 λ³€μˆ˜λŠ” ν…œν”Œλ¦Ώμ˜ '[DATE]' 뢀뢄을 λŒ€μ²΄ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€. + current_date = datetime.now().strftime("%Y-%m-%d") + + # ν…œν”Œλ¦Ώ λ‚΄μš©μ—μ„œ ν”Œλ ˆμ΄μŠ€ν™€λ”(placeholder)λ₯Ό μ‹€μ œ λ°μ΄ν„°λ‘œ κ΅μ²΄ν•©λ‹ˆλ‹€. + # 1. '[FEATURE NAME]' -> 'Generated Feature' (μž„μ‹œ 이름) + # 2. '[DATE]' -> ν˜„μž¬ λ‚ μ§œ + # 3. '$ARGUMENTS' -> μ‚¬μš©μžκ°€ μž…λ ₯ν•œ κΈ°λŠ₯ μ„€λͺ… + # feature_name λ³€μˆ˜λŠ” λ‚˜μ€‘μ— 더 λ™μ μœΌλ‘œ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. + feature_name = "Generated Feature" + content = content.replace("[FEATURE NAME]", feature_name) + content = content.replace("[DATE]", current_date) + content = content.replace("$ARGUMENTS", feature_description) + + # μ™„μ„±λœ λ¬Έμ„œ λ‚΄μš©μ„ λ°˜ν™˜ν•©λ‹ˆλ‹€. + return content + + except FileNotFoundError: + # ν…œν”Œλ¦Ώ 파일이 없을 경우, μ—λŸ¬ λ©”μ‹œμ§€λ₯Ό ν¬ν•¨ν•˜μ—¬ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. + # 이 μ˜ˆμ™ΈλŠ” API μ—”λ“œν¬μΈνŠΈμ—μ„œ μ²˜λ¦¬λ˜μ–΄ μ‚¬μš©μžμ—κ²Œ μ μ ˆν•œ 였λ₯˜ 응닡을 λ³΄λƒ…λ‹ˆλ‹€. + raise FileNotFoundError(f"ν…œν”Œλ¦Ώ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {template_path}") + except Exception as e: + # 파일 읽기 쀑 λ‹€λ₯Έ μ˜ˆμ™Έκ°€ λ°œμƒν•  경우, ν•΄λ‹Ή μ˜ˆμ™Έλ₯Ό λ‹€μ‹œ λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. + # λ‘œκΉ… 등을 μΆ”κ°€ν•˜μ—¬ 였λ₯˜λ₯Ό 좔적할 수 μžˆμŠ΅λ‹ˆλ‹€. + # 예λ₯Ό λ“€μ–΄: logging.error(f"사양 생성 쀑 였λ₯˜ λ°œμƒ: {e}") + raise e + + async def create_plan(self, feature_name: str) -> str: + """ + κΈ°λŠ₯ 이름을 기반으둜 κ΅¬ν˜„ κ³„νš(plan) λ¬Έμ„œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. + + Args: + feature_name (str): κ³„νšμ„ 생성할 κΈ°λŠ₯의 이름. + + Returns: + str: μ™„μ„±λœ κ³„νš λ¬Έμ„œμ˜ λ‚΄μš©. + """ + try: + # κ³„νš ν…œν”Œλ¦Ώ 파일의 경둜. + template_path = "templates/plan-template.md" + async with aiofiles.open(template_path, mode='r', encoding='utf-8') as f: + content = await f.read() + + # ν˜„μž¬ λ‚ μ§œλ₯Ό ν¬λ§·νŒ…ν•©λ‹ˆλ‹€. + current_date = datetime.now().strftime("%Y-%m-%d") + + # ν…œν”Œλ¦Ώμ˜ ν”Œλ ˆμ΄μŠ€ν™€λ”λ₯Ό κ΅μ²΄ν•©λ‹ˆλ‹€. + content = content.replace("[FEATURE]", feature_name) + content = content.replace("[DATE]", current_date) + # '[###-feature-name]'κ³Ό 같은 λ‹€λ₯Έ ν”Œλ ˆμ΄μŠ€ν™€λ”λŠ” + # μ‹€μ œ κ΅¬ν˜„ μ‹œ 더 μ •κ΅ν•œ 둜직으둜 처리될 수 μžˆμŠ΅λ‹ˆλ‹€. + + return content + except FileNotFoundError: + raise FileNotFoundError(f"ν…œν”Œλ¦Ώ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {template_path}") + except Exception as e: + raise e + + async def create_tasks(self, feature_name: str) -> str: + """ + κΈ°λŠ₯ 이름을 기반으둜 μž‘μ—… λͺ©λ‘(tasks) λ¬Έμ„œλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€. + + Args: + feature_name (str): μž‘μ—… λͺ©λ‘μ„ 생성할 κΈ°λŠ₯의 이름. + + Returns: + str: μ™„μ„±λœ μž‘μ—… λͺ©λ‘ λ¬Έμ„œμ˜ λ‚΄μš©. + """ + try: + # μž‘μ—… ν…œν”Œλ¦Ώ 파일의 경둜. + template_path = "templates/tasks-template.md" + async with aiofiles.open(template_path, mode='r', encoding='utf-8') as f: + content = await f.read() + + # ν…œν”Œλ¦Ώμ˜ ν”Œλ ˆμ΄μŠ€ν™€λ”λ₯Ό κ΅μ²΄ν•©λ‹ˆλ‹€. + content = content.replace("[FEATURE NAME]", feature_name) + + return content + except FileNotFoundError: + raise FileNotFoundError(f"ν…œν”Œλ¦Ώ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {template_path}") + except Exception as e: + raise e \ No newline at end of file