# Keina System - Gemini向け実装指示書

## 📋 このドキュメントについて

これは、農業生産者向けの作付け計画管理システム「Keina System」の実装に必要な全情報を統合したドキュメントです。

**実装エージェント（Gemini/OpenCode）は、以下の順序でドキュメントを読み、理解してから実装を開始してください。**

---

## 🎯 システムの全体像

### 目的
年間の作付け計画を管理し、役場への申請書類（水稲共済細目書・中山間地域等直接支払交付金）を自動生成するシステム。

### ユーザー
- 65歳の農家（元プログラマー）
- シングルユーザー（マルチテナント不要）
- PCで登録・編集、スマホで参照

### 主要機能（Phase 1 / MVP）
1. 作付け計画の一覧表示・インライン編集
2. 水稲共済細目書のPDF出力
3. 中山間交付金申請書のPDF出力
4. 前年度作付けのコピー機能
5. 圃場情報のスマホ参照

### 技術スタック
- **バックエンド**: Django 5.2 + Django REST Framework + GeoDjango
- **フロントエンド**: Next.js 14 (App Router) + Tailwind CSS
- **データベース**: PostgreSQL 16 + PostGIS 3.4
- **インフラ**: Docker Compose

---

## 📚 必読ドキュメント（この順に読むこと）

実装前に、以下のドキュメントを**必ず全て読んでください**。各ドキュメントには実装に必要な重要な情報が含まれています。

### 1. プロダクトビジョン.md
- システムの目的、ユーザー像、成功の定義
- なぜこのシステムを作るのか、何を目指すのか
- **👉 読むべき理由**: ゴールを理解しないと、適切な実装判断ができない

### 2. ユーザーストーリー.md
- 具体的な利用シーン（優先度付き）
- 受け入れ基準、実装すべき機能の詳細
- **👉 読むべき理由**: 何を作るべきか、どこまで作るべきかが明確になる

### 3. データ仕様書.md
- 3種類のデータ（実圃場、共済マスタ、中山間マスタ）の関係
- 紐付けロジック（M:N関係）の詳細
- 申請書PDF出力のアルゴリズム
- **👉 読むべき理由**: データモデルの設計ミスは後から修正困難

### 4. 画面設計書.md
- 全画面のワイヤーフレーム
- UI共通仕様（カラー、フォント、レスポンシブ）
- **👉 読むべき理由**: UIの実装イメージが具体的につかめる

### 5. 実装優先順位.md
- 10日間の実装スケジュール
- 技術スタックの詳細、潜在的な課題と対策
- **👉 読むべき理由**: 何から作るか、どう作るかの指針

---

## 🚀 実装の進め方（ステップバイステップ）

### Step 0: 事前準備（必須）
1. 上記5つのドキュメントを**全て読む**
2. 不明点があれば、実装開始前に質問する
3. 実データ（`吉田農地台帳.ods`, `水稲共済細目用.ods`, `中山間.ods`）を確認

### Step 1: 環境構築（Day 1）
```bash
# プロジェクト構造
keinasystem/
├── docker-compose.yml
├── backend/
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── manage.py
│   └── keinasystem/
│       ├── settings.py
│       ├── urls.py
│       └── apps/
│           ├── fields/       # 圃場管理
│           ├── plans/        # 作付け計画
│           └── reports/      # 申請書出力
└── frontend/
    ├── Dockerfile
    ├── package.json
    ├── app/
    │   ├── login/
    │   ├── dashboard/
    │   ├── allocation/       # 作付け計画編集
    │   └── reports/          # 申請書ダウンロード
    └── lib/
        ├── api.ts            # API呼び出し
        └── types.ts          # 型定義
```

#### docker-compose.yml
```yaml
version: '3.8'

services:
  db:
    image: postgis/postgis:16-3.4
    environment:
      POSTGRES_DB: keina_db
      POSTGRES_USER: keina_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  backend:
    build: ./backend
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./backend:/app
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgis://keina_user:${DB_PASSWORD}@db:5432/keina_db
      SECRET_KEY: ${SECRET_KEY}
    depends_on:
      - db

  frontend:
    build: ./frontend
    command: npm run dev
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    environment:
      NEXT_PUBLIC_API_URL: http://localhost:8000

volumes:
  postgres_data:
```

### Step 2: Django セットアップ（Day 1-2）

#### backend/requirements.txt
```txt
Django==5.2
djangorestframework==3.14
django-cors-headers==4.3
psycopg2-binary==2.9
djoser==2.2
djangorestframework-simplejwt==5.3
pandas==2.1
odfpy==1.4
WeasyPrint==60.1
```

#### backend/keinasystem/settings.py（重要な設定のみ）
```python
INSTALLED_APPS = [
    # Django標準
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.gis',  # GeoDjango
    
    # サードパーティ
    'rest_framework',
    'rest_framework_simplejwt',
    'djoser',
    'corsheaders',
    
    # 自作アプリ
    'apps.fields',
    'apps.plans',
    'apps.reports',
]

# PostGIS設定
DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'keina_db',
        'USER': 'keina_user',
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': 'db',
        'PORT': '5432',
    }
}

# REST Framework設定
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

# JWT設定
from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
}

# CORS設定
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
]
```

### Step 3: データモデル実装（Day 3）

#### apps/fields/models.py
```python
from django.contrib.gis.db import models

class OfficialKyosaiField(models.Model):
    """共済マスタ（水稲共済細目用.ods）"""
    k_num = models.CharField("耕地番号", max_length=20)
    s_num = models.CharField("分筆番号", max_length=20, blank=True)
    address = models.CharField("地名地番", max_length=200)
    kanji_name = models.CharField("漢字地名", max_length=200)
    area = models.IntegerField("本地面積(m2)")

    class Meta:
        unique_together = [['k_num', 's_num']]
        ordering = ['k_num', 's_num']

class OfficialChusankanField(models.Model):
    """中山間マスタ（中山間.ods）- 17列全て保存"""
    c_id = models.CharField("ID", max_length=20, unique=True)
    chusankan_flag = models.CharField("中山間", max_length=10, blank=True)
    oaza = models.CharField("大字", max_length=100)
    aza = models.CharField("字", max_length=100)
    chiban = models.CharField("地番", max_length=50)
    branch_num = models.CharField("枝番", max_length=20, blank=True)
    land_type = models.CharField("地目", max_length=20, blank=True)
    area = models.IntegerField("農地面積(m2)")
    planting_area = models.IntegerField("植栽面積(m2)", null=True, blank=True)
    original_crop = models.CharField("作付け品目", max_length=100, blank=True)
    manager = models.CharField("協定管理者", max_length=100, blank=True)
    owner = models.CharField("所有者", max_length=100, blank=True)
    slope = models.CharField("傾斜度", max_length=20, blank=True)
    base_amount = models.IntegerField("基本金額", null=True, blank=True)
    steep_slope_addition = models.IntegerField("超急傾斜加算額", null=True, blank=True)
    smart_agri_addition = models.IntegerField("スマート農業加算額", null=True, blank=True)
    payment_amount = models.IntegerField("交付金額", null=True, blank=True)

    class Meta:
        ordering = ['c_id']

class Field(models.Model):
    """実圃場（吉田農地台帳.ods）"""
    name = models.CharField("名称", max_length=100)
    address = models.CharField("住所", max_length=200)
    area_tan = models.FloatField("面積(反)")
    area_m2 = models.IntegerField("面積(m2)")  # area_tan * 1000
    owner_name = models.CharField("地主", max_length=100)
    
    # グループ・表示順
    group_name = models.CharField("グループ名", max_length=100, blank=True)
    display_order = models.IntegerField("表示順", default=0)

    # M:N紐付け（1つの圃場が複数の申請区画に紐づく場合がある）
    kyosai_fields = models.ManyToManyField(
        OfficialKyosaiField,
        blank=True,
        related_name='fields'
    )
    chusankan_fields = models.ManyToManyField(
        OfficialChusankanField,
        blank=True,
        related_name='fields'
    )
    
    # 地理情報（Phase 1では使用しないが、構造だけ準備）
    location = models.PointField(null=True, blank=True)
    
    class Meta:
        ordering = ['name']
```

#### apps/plans/models.py
```python
from django.db import models
from apps.fields.models import Field

class Crop(models.Model):
    """作物マスタ"""
    name = models.CharField("作物名", max_length=50, unique=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):
    """作付け計画"""
    field = models.ForeignKey(Field, on_delete=models.CASCADE, related_name='plans')
    year = models.IntegerField("年度")
    crop = models.ForeignKey(Crop, on_delete=models.PROTECT, null=True, blank=True)
    variety = models.ForeignKey(Variety, on_delete=models.SET_NULL, null=True, blank=True)
    notes = models.TextField("備考", blank=True)
    
    class Meta:
        unique_together = [['field', 'year']]
        ordering = ['year', 'field']
```

### Step 4: インポート機能（Day 4）

#### apps/fields/views.py（インポートAPI）
```python
from rest_framework.decorators import api_view
from rest_framework.response import Response
import pandas as pd

@api_view(['POST'])
def import_kyosai_master(request):
    """共済マスタのインポート"""
    file = request.FILES['file']
    df = pd.read_excel(file, engine='odf')
    
    for _, row in df.iterrows():
        OfficialKyosaiField.objects.update_or_create(
            k_num=row['耕地番号'],
            s_num=row['分筆番号'],
            defaults={
                'address': row['地名 地番'],
                'kanji_name': row['漢字地名'],
                'area': row['本地面積 (m2)'],
            }
        )
    
    return Response({'status': 'success', 'imported': len(df)})

@api_view(['POST'])
def import_yoshida_fields(request):
    """吉田農地台帳のインポート（M:N紐付け処理含む）"""
    file = request.FILES['file']
    df = pd.read_excel(file, engine='odf')

    for _, row in df.iterrows():
        # 実圃場を作成
        field, created = Field.objects.update_or_create(
            name=row['名称'],
            defaults={
                'address': row['住所'],
                'area_tan': row['面積(反)'],
                'area_m2': int(row['面積(反)'] * 1000),
                'owner_name': row['地主'],
            }
        )

        # 共済マスタとのM:N紐付け
        try:
            kyosai = OfficialKyosaiField.objects.get(
                k_num=str(row['細目_耕地番号']),
                s_num=str(row['細目_分筆番号'])
            )
            field.kyosai_fields.add(kyosai)
        except OfficialKyosaiField.DoesNotExist:
            pass

        # 中山間マスタとのM:N紐付け
        if pd.notna(row.get('中山間_ID')):
            try:
                chusankan = OfficialChusankanField.objects.get(
                    c_id=str(int(row['中山間_ID']))
                )
                field.chusankan_fields.add(chusankan)
            except OfficialChusankanField.DoesNotExist:
                pass

    return Response({'status': 'success', 'imported': len(df)})
```

### Step 5: 作付け計画API（Day 5）

#### 作物・品種マスタについて

初期データの自動投入は行わない。作物・品種はDjango管理画面またはUIから手動で登録する運用とする。

主な作物: 米、トウモロコシ、エンドウ、野菜、その他

#### apps/plans/views.py
```python
from rest_framework import viewsets
from rest_framework.decorators import action, api_view
from rest_framework.response import Response
from .models import Plan, Crop, Variety
from .serializers import PlanSerializer, CropSerializer, VarietySerializer

class PlanViewSet(viewsets.ModelViewSet):
    queryset = Plan.objects.all()
    serializer_class = PlanSerializer
    
    def get_queryset(self):
        queryset = super().get_queryset()
        year = self.request.query_params.get('year')
        if year:
            queryset = queryset.filter(year=year)
        return queryset
    
    @action(detail=False, methods=['post'])
    def bulk_update(self, request):
        """一括更新"""
        field_ids = request.data.get('field_ids', [])
        crop_id = request.data.get('crop_id')
        variety_id = request.data.get('variety_id')
        year = request.data.get('year')
        
        for field_id in field_ids:
            Plan.objects.update_or_create(
                field_id=field_id,
                year=year,
                defaults={
                    'crop_id': crop_id,
                    'variety_id': variety_id,
                }
            )
        
        return Response({'status': 'success', 'updated': len(field_ids)})
    
    @action(detail=False, methods=['post'])
    def copy_from_previous_year(self, request):
        """前年度コピー"""
        source_year = request.data.get('source_year')
        target_year = request.data.get('target_year')
        
        plans = Plan.objects.filter(year=source_year)
        
        for plan in plans:
            Plan.objects.update_or_create(
                field=plan.field,
                year=target_year,
                defaults={
                    'crop': plan.crop,
                    'variety': plan.variety,
                    'notes': plan.notes,
                }
            )
        
        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）

#### apps/reports/views.py
```python
from rest_framework.decorators import api_view
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML
from apps.fields.models import OfficialKyosaiField, OfficialChusankanField
from apps.plans.models import Plan

@api_view(['GET'])
def generate_kyosai_pdf(request):
    """水稲共済細目書PDF"""
    year = int(request.query_params.get('year'))
    
    # 1. データ集約
    output_rows = []
    for kyosai in OfficialKyosaiField.objects.all().order_by('k_num', 's_num'):
        # この共済区画に紐づく実圃場を取得
        fields = kyosai.fields.all()
        
        # 各実圃場の作付け計画を取得
        plans = Plan.objects.filter(
            field__in=fields,
            year=year
        ).select_related('crop', 'variety')
        
        # 作物名と品種を集約
        crops = sorted(list(set([p.crop.name for p in plans if p.crop])))
        varieties = sorted(list(set([p.variety.name for p in plans if p.variety])))
        
        output_rows.append({
            'k_num': kyosai.k_num,
            's_num': kyosai.s_num,
            'address': kyosai.address,
            'kanji_name': kyosai.kanji_name,
            'area': kyosai.area,
            'crops': ','.join(crops) if crops else '未設定',
            'varieties': ','.join(varieties) if varieties else '',
            'note': f'{len(fields)}筆合算' if len(fields) > 1 else ''
        })
    
    # 2. HTMLテンプレートで表を生成
    html = render_to_string('reports/kyosai_template.html', {
        'year': year,
        'rows': output_rows
    })
    
    # 3. HTML → PDF変換
    pdf = HTML(string=html).write_pdf()
    
    response = HttpResponse(pdf, content_type='application/pdf')
    response['Content-Disposition'] = f'attachment; filename="kyosai_{year}.pdf"'
    return response

@api_view(['GET'])
def generate_chusankan_pdf(request):
    """中山間交付金PDF"""
    year = int(request.query_params.get('year'))
    
    # データ集約（共済と同様）
    output_rows = []
    for chusankan in OfficialChusankanField.objects.all().order_by('c_id'):
        fields = chusankan.fields.all()
        plans = Plan.objects.filter(field__in=fields, year=year).select_related('crop', 'variety')
        
        crops = sorted(list(set([p.crop.name for p in plans if p.crop])))
        varieties = sorted(list(set([p.variety.name for p in plans if p.variety])))
        
        output_rows.append({
            'c_id': chusankan.c_id,
            'oaza': chusankan.oaza,
            'aza': chusankan.aza,
            'chiban': chusankan.chiban,
            'area': chusankan.area,
            'crops': ','.join(crops) if crops else '未設定',
            'varieties': ','.join(varieties) if varieties else '',
            'note': f'{len(fields)}筆合算' if len(fields) > 1 else ''
        })
    
    html = render_to_string('reports/chusankan_template.html', {
        'year': year,
        'rows': output_rows
    })
    
    pdf = HTML(string=html).write_pdf()
    
    response = HttpResponse(pdf, content_type='application/pdf')
    response['Content-Disposition'] = f'attachment; filename="chusankan_{year}.pdf"'
    return response
```

#### apps/reports/templates/reports/kyosai_template.html
```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        @page {
            size: A4;
            margin: 2cm;
        }
        body {
            font-family: "MS Gothic", "Yu Gothic", monospace;
            font-size: 10pt;
        }
        h1 {
            text-align: center;
            font-size: 14pt;
            margin-bottom: 20px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: 9pt;
        }
        th, td {
            border: 1px solid black;
            padding: 4px;
            text-align: center;
        }
        th {
            background-color: #f0f0f0;
            font-weight: bold;
        }
        .left {
            text-align: left;
        }
    </style>
</head>
<body>
    <h1>水稲共済細目書（{{ year }}年度）</h1>
    <table>
        <thead>
            <tr>
                <th style="width: 8%;">耕地番号</th>
                <th style="width: 8%;">分筆番号</th>
                <th style="width: 20%;">地名地番</th>
                <th style="width: 18%;">漢字地名</th>
                <th style="width: 10%;">本地面積<br>(m2)</th>
                <th style="width: 12%;">作付品目</th>
                <th style="width: 14%;">品種</th>
                <th style="width: 10%;">備考</th>
            </tr>
        </thead>
        <tbody>
            {% for row in rows %}
            <tr>
                <td>{{ row.k_num }}</td>
                <td>{{ row.s_num }}</td>
                <td class="left">{{ row.address }}</td>
                <td class="left">{{ row.kanji_name }}</td>
                <td>{{ row.area }}</td>
                <td class="left">{{ row.crops }}</td>
                <td class="left">{{ row.varieties }}</td>
                <td>{{ row.note }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>
```

#### apps/reports/templates/reports/chusankan_template.html
```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        @page { size: A4; margin: 2cm; }
        body { font-family: "MS Gothic", "Yu Gothic", monospace; font-size: 10pt; }
        h1 { text-align: center; font-size: 14pt; margin-bottom: 20px; }
        table { width: 100%; border-collapse: collapse; font-size: 9pt; }
        th, td { border: 1px solid black; padding: 4px; text-align: center; }
        th { background-color: #f0f0f0; font-weight: bold; }
        .left { text-align: left; }
    </style>
</head>
<body>
    <h1>中山間地域等直接支払交付金（{{ year }}年度）</h1>
    <table>
        <thead>
            <tr>
                <th style="width: 8%;">ID</th>
                <th style="width: 15%;">大字</th>
                <th style="width: 15%;">字</th>
                <th style="width: 10%;">地番</th>
                <th style="width: 12%;">農地面積<br>(m2)</th>
                <th style="width: 15%;">作付品目</th>
                <th style="width: 15%;">品種</th>
                <th style="width: 10%;">備考</th>
            </tr>
        </thead>
        <tbody>
            {% for row in rows %}
            <tr>
                <td>{{ row.c_id }}</td>
                <td class="left">{{ row.oaza }}</td>
                <td class="left">{{ row.aza }}</td>
                <td>{{ row.chiban }}</td>
                <td>{{ row.area }}</td>
                <td class="left">{{ row.crops }}</td>
                <td class="left">{{ row.varieties }}</td>
                <td>{{ row.note }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>
```

#### apps/reports/urls.py
```python
from django.urls import path
from . import views

urlpatterns = [
    path('kyosai/', views.generate_kyosai_pdf, name='kyosai_pdf'),
    path('chusankan/', views.generate_chusankan_pdf, name='chusankan_pdf'),
]
```

### Step 7: フロントエンド実装（Day 6, 8-9）

#### frontend/app/reports/page.tsx（申請書ダウンロード画面）
```tsx
'use client'

import { useState } from 'react'

export default function ReportsPage() {
  const [year, setYear] = useState(2025)
  
  const downloadPDF = async (type: 'kyosai' | 'chusankan') => {
    const endpoint = type === 'kyosai' 
      ? `/api/reports/kyosai/?year=${year}`
      : `/api/reports/chusankan/?year=${year}`
    
    try {
      const response = await fetch(`http://localhost:8000${endpoint}`, {
        headers: {
          'Authorization': `JWT ${localStorage.getItem('accessToken')}`
        }
      })
      
      const blob = await response.blob()
      const url = window.URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = type === 'kyosai' 
        ? `水稲共済細目書_${year}年度.pdf`
        : `中山間交付金_${year}年度.pdf`
      document.body.appendChild(a)
      a.click()
      window.URL.revokeObjectURL(url)
      document.body.removeChild(a)
    } catch (error) {
      console.error('PDF download failed:', error)
      alert('PDFのダウンロードに失敗しました')
    }
  }
  
  const previewPDF = (type: 'kyosai' | 'chusankan') => {
    const endpoint = type === 'kyosai' 
      ? `/api/reports/kyosai/?year=${year}`
      : `/api/reports/chusankan/?year=${year}`
    
    const url = `http://localhost:8000${endpoint}`
    window.open(url, '_blank')
  }
  
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">申請書ダウンロード</h1>
      
      <div className="mb-4">
        <label className="mr-2">年度:</label>
        <select 
          value={year} 
          onChange={e => setYear(parseInt(e.target.value))}
          className="border rounded px-4 py-2"
        >
          <option value={2024}>2024年度</option>
          <option value={2025}>2025年度</option>
          <option value={2026}>2026年度</option>
        </select>
      </div>
      
      <div className="grid gap-4">
        {/* 水稲共済細目書 */}
        <div className="border rounded p-4">
          <h2 className="text-xl font-bold mb-2">📄 水稲共済細目書</h2>
          <p className="text-sm text-gray-600 mb-2">提出時期: 2月・5月（年2回）</p>
          <p className="text-sm text-gray-600 mb-4">区画数: 31区画</p>
          
          <div className="flex gap-2">
            <button 
              onClick={() => previewPDF('kyosai')}
              className="bg-blue-500 text-white px-4 py-2 rounded"
            >
              プレビュー
            </button>
            <button 
              onClick={() => downloadPDF('kyosai')}
              className="bg-green-500 text-white px-4 py-2 rounded"
            >
              PDFダウンロード
            </button>
          </div>
        </div>
        
        {/* 中山間交付金 */}
        <div className="border rounded p-4">
          <h2 className="text-xl font-bold mb-2">📄 中山間地域等直接支払交付金</h2>
          <p className="text-sm text-gray-600 mb-2">提出時期: 5月（年1回）</p>
          <p className="text-sm text-gray-600 mb-4">区画数: 71区画</p>
          
          <div className="flex gap-2">
            <button 
              onClick={() => previewPDF('chusankan')}
              className="bg-blue-500 text-white px-4 py-2 rounded"
            >
              プレビュー
            </button>
            <button 
              onClick={() => downloadPDF('chusankan')}
              className="bg-green-500 text-white px-4 py-2 rounded"
            >
              PDFダウンロード
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}
```

---

## ⚠️ 重要な実装上の注意点

### 1. データベース設計
- **面積単位**: DB内部は `area_m2`（IntegerField）で保存、表示用に `area_tan`（DecimalField）も保持。1反=1000m2
- **紐付け**: Field ↔ OfficialKyosaiField、Field ↔ OfficialChusankanField は **M:N**（ManyToManyField）
- **ユニーク制約**: `(field, year)` で作付け計画は1つまで、`(k_num, s_num)` で共済区画は一意
- **品種マスタ**: `(crop, name)` で一意制約

### 2. 作物・品種の統一
- **すべての作物で品種選択UIは統一**: 作物による操作の違いなし
- **「作付けしない」系も特別扱いしない**: 「その他」という作物に統一
- **品種の追加**: その場で追加可能、データベースに永続化
- **初期データ**: なし（管理画面またはUIから登録する運用）

### 3. 集計サイドバー
- **リアルタイム更新**: 作付け計画を保存するたびに自動更新
- **PC**: 開閉可能なサイドバー（幅200px固定）
- **スマホ**: モーダル表示
- **集計API**: `/api/plans/summary/?year=2025` で作物別・品種別の面積を返す

### 4. 申請書PDF生成
- **共済マスタをベースにループ**: 実圃場ベースではない
- **作物の名寄せ**: Pythonのセットで重複排除
- **未割当の扱い**: 「未設定」として出力

### 5. UI/UX
- **未割当の強調**: 赤または黄色の背景色
- **スマホ対応**: 文字サイズ16px以上、タップ領域44px以上
- **検索のリアルタイム性**: `useState` + `filter` で実装
- **品種追加のUI**: インライン入力、別画面に遷移しない

### 6. 認証
- **JWT認証**: アクセストークン（1日）+ リフレッシュトークン（7日）
- **シンプルな実装**: パスワードリセットはPhase 2

### 7. パフォーマンス
- **Phase 1では最適化不要**: 圃場数39筆、共済31区画、中山間71区画 → 十分軽い
- **Phase 2で検討**: 栽培履歴が増えたらページネーション

---

## 📝 実装チェックリスト

実装完了時に、以下を全て確認してください：

### 環境構築
- [ ] `docker-compose up` でコンテナが全て起動する
- [ ] PostgreSQL + PostGIS が正常に動作する
- [ ] Django の `makemigrations` / `migrate` が成功する
- [ ] Next.js の `npm run dev` が成功する

### バックエンド
- [ ] `/admin` でDjango管理画面にアクセスできる
- [ ] `/api/auth/jwt/create/` でJWTトークンを取得できる
- [ ] `/api/fields/` で圃場一覧を取得できる
- [ ] `/api/plans/?year=2025` で作付け計画を取得できる
- [ ] `/api/reports/kyosai/?year=2025` でPDFをダウンロードできる
- [ ] `/api/reports/chusankan/?year=2025` でPDFをダウンロードできる

### フロントエンド
- [ ] `/login` でログインできる
- [ ] `/allocation` で作付け計画一覧を表示できる
- [ ] 作付け計画を編集できる
- [ ] `/reports` で申請書をダウンロードできる
- [ ] PDFをプレビュー表示できる（新しいタブ）
- [ ] スマホで見やすい（Chrome DevToolsで確認）

### データ整合性
- [ ] 共済PDFの耕地番号が正しい
- [ ] 中山間PDFのIDが正しい
- [ ] 作物の名寄せが正しい
- [ ] 未割当の圃場が適切に扱われる
- [ ] PDFの表レイアウトが整っている
- [ ] PDFをA4用紙に印刷して読める（フォントサイズ、余白）

---

## 🆘 トラブルシューティング

### PostGISが動かない
```python
# settings.py に以下を追加
GDAL_LIBRARY_PATH = '/usr/lib/libgdal.so'
GEOS_LIBRARY_PATH = '/usr/lib/libgeos_c.so'
```

### ODSファイルが読めない
```bash
pip install odfpy --break-system-packages
```

### WeasyPrintのインストールエラー
```bash
# Ubuntu/Debianの場合、システムライブラリが必要
apt-get install python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0
pip install WeasyPrint --break-system-packages
```

### PDFの日本語が表示されない
```python
# HTMLテンプレートのCSSに日本語フォントを明示
body {
    font-family: "MS Gothic", "Yu Gothic", "Hiragino Sans", sans-serif;
}
```

### CORSエラー
```python
# settings.py
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
]
```

### マイグレーションエラー
```bash
docker-compose exec backend python manage.py makemigrations
docker-compose exec backend python manage.py migrate
```

---

## 🎉 実装完了の定義

以下が全て完了したら、Phase 1は実装完了とする：

1. ✅ ログインできる
2. ✅ 作付け計画を編集できる
3. ✅ 水稲共済PDFをダウンロードできる
4. ✅ 中山間PDFをダウンロードできる
5. ✅ PDFをプレビュー表示できる
6. ✅ PDFをA4用紙に印刷してそのまま提出できる品質
7. ✅ 前年度コピーができる
8. ✅ スマホで圃場情報を見られる
9. ✅ 実データ（3つのODSファイル）をインポートできる

---

## 📞 質問・不明点がある場合

実装中に疑問が生じた場合、以下を確認してください：

1. **データ仕様書.md**: データの紐付けロジックが不明な場合
2. **画面設計書.md**: UIの詳細が不明な場合
3. **ユーザーストーリー.md**: 機能の目的・受け入れ基準が不明な場合

それでも解決しない場合は、**実装を止めて質問してください**。

---

**Good luck with the implementation! 🚀**
