📚 修正したドキュメント

 02_ユーザーストーリー.md - 作物マスタの定義を統一
 03_データ仕様書.md - 作物・品種マスタを更新
 04_画面設計書.md - 集計サイドバーと編集モーダルのUI
 05_実装優先順位.md - Day 5-6に集計API・品種追加APIを追加
 06_Gemini向け統合指示書.md - コード例を全面更新
This commit is contained in:
Akira
2026-02-15 10:41:55 +09:00
parent ed899fb97d
commit 60dca6aab1
5 changed files with 381 additions and 77 deletions

View File

@@ -291,18 +291,19 @@ from apps.fields.models import Field
class Crop(models.Model):
"""作物マスタ"""
name = models.CharField("作物名", max_length=50, unique=True)
is_planting = models.BooleanField("作付けする", default=True)
# is_planting フィールドは削除(「その他」で統一するため)
class Meta:
ordering = ['name']
class Variety(models.Model):
"""品種マスタ"""
"""品種マスタ(すべての作物で統一)"""
crop = models.ForeignKey(Crop, on_delete=models.CASCADE, related_name='varieties')
name = models.CharField("品種名", max_length=100)
class Meta:
ordering = ['crop', 'name']
unique_together = [['crop', 'name']] # 同じ作物内で品種名は一意
class Plan(models.Model):
"""作付け計画"""
@@ -391,13 +392,60 @@ def import_yoshida_fields(request):
### Step 5: 作付け計画APIDay 5
#### 作物・品種の初期データ投入
**apps/plans/management/commands/init_crops.py**
```python
from django.core.management.base import BaseCommand
from apps.plans.models import Crop, Variety
class Command(BaseCommand):
help = '作物・品種マスタの初期データを投入'
def handle(self, *args, **options):
# 作物マスタと品種
crops_data = {
'': ['にこまる', 'たちはるか', 'たちはるか(特栽)'],
'トウモロコシ': [],
'エンドウ': ['久留米豊'],
'野菜': [],
'その他': [
'完全休耕',
'緑肥(ヘアリーベッチ)',
'緑肥(レンゲ)',
'景観作物(コスモス)',
'景観作物(ヒマワリ)'
]
}
for crop_name, varieties in crops_data.items():
crop, created = Crop.objects.get_or_create(name=crop_name)
if created:
self.stdout.write(f'作物「{crop_name}」を作成')
for variety_name in varieties:
variety, created = Variety.objects.get_or_create(
crop=crop,
name=variety_name
)
if created:
self.stdout.write(f' 品種「{variety_name}」を追加')
self.stdout.write(self.style.SUCCESS('初期データ投入完了'))
```
**実行:**
```bash
docker-compose exec backend python manage.py init_crops
```
#### apps/plans/views.py
```python
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.decorators import action, api_view
from rest_framework.response import Response
from .models import Plan
from .serializers import PlanSerializer
from .models import Plan, Crop, Variety
from .serializers import PlanSerializer, CropSerializer, VarietySerializer
class PlanViewSet(viewsets.ModelViewSet):
queryset = Plan.objects.all()
@@ -450,6 +498,79 @@ class PlanViewSet(viewsets.ModelViewSet):
)
return Response({'status': 'success', 'copied': len(plans)})
@action(detail=False, methods=['get'])
def summary(self, request):
"""集計(サイドバー用)"""
year = int(request.query_params.get('year'))
plans = Plan.objects.filter(year=year).select_related('crop', 'variety', 'field')
# 作物別集計
crop_summary = {}
total_area = 0
for plan in plans:
crop_name = plan.crop.name if plan.crop else '未設定'
variety_name = plan.variety.name if plan.variety else '(品種なし)'
area = plan.field.area_tan
total_area += area
if crop_name not in crop_summary:
crop_summary[crop_name] = {'total': 0, 'varieties': {}}
crop_summary[crop_name]['total'] += area
if variety_name not in crop_summary[crop_name]['varieties']:
crop_summary[crop_name]['varieties'][variety_name] = 0
crop_summary[crop_name]['varieties'][variety_name] += area
# 反を小数点1桁に丸める
for crop in crop_summary.values():
crop['total'] = round(crop['total'], 1)
for variety in crop['varieties']:
crop['varieties'][variety] = round(crop['varieties'][variety], 1)
return Response({
'total_area': round(total_area, 1),
'crops': crop_summary
})
@api_view(['POST'])
def add_variety(request):
"""新しい品種を追加"""
crop_id = request.data.get('crop_id')
name = request.data.get('name')
# 重複チェック
if Variety.objects.filter(crop_id=crop_id, name=name).exists():
return Response({'error': 'この品種は既に登録されています'}, status=400)
variety = Variety.objects.create(crop_id=crop_id, name=name)
return Response({
'id': variety.id,
'name': variety.name,
'crop_id': variety.crop_id
})
@api_view(['GET'])
def get_crops_with_varieties(request):
"""作物と品種の一覧を取得"""
crops = Crop.objects.prefetch_related('varieties').all()
data = []
for crop in crops:
data.append({
'id': crop.id,
'name': crop.name,
'varieties': [
{'id': v.id, 'name': v.name}
for v in crop.varieties.all()
]
})
return Response(data)
```
### Step 6: 申請書PDF生成Day 7
@@ -803,22 +924,43 @@ export default function ReportsPage() {
- **面積単位**: DB内部は全て `m2` で保存、表示時に `反` に変換
- **紐付けキー**: `raw_*` フィールドと外部キー `*_field` の両方を持つ
- **ユニーク制約**: `(field, year)` で作付け計画は1つまで
- **品種マスタ**: `(crop, name)` で一意制約
### 2. 申請書CSV生成
### 2. 作物・品種の統一
- **すべての作物で品種選択UIは統一**: 作物による操作の違いなし
- **「作付けしない」系も特別扱いしない**: 「その他」という作物に統一
- **品種の追加**: その場で追加可能、データベースに永続化
- **初期データ**:
```
米: にこまる、たちはるか、たちはるか(特栽)
トウモロコシ: (ユーザーが追加)
エンドウ: 久留米豊
野菜: (ユーザーが追加)
その他: 完全休耕、緑肥(ヘアリーベッチ)、緑肥(レンゲ)、景観作物(コスモス)、景観作物(ヒマワリ)
```
### 3. 集計サイドバー
- **リアルタイム更新**: 作付け計画を保存するたびに自動更新
- **PC**: 開閉可能なサイドバー幅200px固定
- **スマホ**: モーダル表示
- **集計API**: `/api/plans/summary/?year=2025` で作物別・品種別の面積を返す
### 4. 申請書PDF生成
- **共済マスタをベースにループ**: 実圃場ベースではない
- **作物の名寄せ**: Pythonのセットで重複排除
- **未割当の扱い**: 「未設定」として出力
### 3. UI/UX
### 5. UI/UX
- **未割当の強調**: 赤または黄色の背景色
- **スマホ対応**: 文字サイズ16px以上、タップ領域44px以上
- **検索のリアルタイム性**: `useState` + `filter` で実装
- **品種追加のUI**: インライン入力、別画面に遷移しない
### 4. 認証
### 6. 認証
- **JWT認証**: アクセストークン1日+ リフレッシュトークン7日
- **シンプルな実装**: パスワードリセットはPhase 2
### 5. パフォーマンス
### 7. パフォーマンス
- **Phase 1では最適化不要**: 圃場数39筆、共済31区画、中山間71区画 → 十分軽い
- **Phase 2で検討**: 栽培履歴が増えたらページネーション