Files
keinasystem/00_Gemini向け統合指示書.md
2026-02-15 10:33:34 +09:00

937 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Keina System - Gemini向け実装指示書
## 📋 このドキュメントについて
これは、農業生産者向けの作付け計画管理システム「Keina System」の実装に必要な全情報を統合したドキュメントです。
**実装エージェントGemini/OpenCodeは、以下の順序でドキュメントを読み、理解してから実装を開始してください。**
---
## 🎯 システムの全体像
### 目的
年間の作付け計画を管理し、役場への申請書類(水稲共済細目書・中山間地域等直接支払交付金)を自動生成するシステム。
### ユーザー
- 65歳の農家元プログラマー
- シングルユーザー(マルチテナント不要)
- PCで登録・編集、スマホで参照
### 主要機能Phase 1 / MVP
1. 作付け計画の一覧表示・編集
2. 水稲共済細目書のCSV出力
3. 中山間交付金申請書のCSV出力
4. 前年度作付けのコピー機能
5. 圃場情報のスマホ参照
### 技術スタック
- **バックエンド**: Django 5.0 + 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:1関係の詳細
- 申請書CSV出力のアルゴリズム
- **👉 読むべき理由**: データモデルの設計ミスは後から修正困難
### 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.0
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.IntegerField("耕地番号")
s_num = models.IntegerField("分筆番号")
address = models.CharField("地名地番", max_length=200)
kanji_name = models.CharField("漢字地名", max_length=200)
area = models.FloatField("本地面積(m2)")
class Meta:
unique_together = [['k_num', 's_num']]
ordering = ['k_num', 's_num']
class OfficialChusankanField(models.Model):
"""中山間マスタ(中山間.ods"""
c_id = models.IntegerField("ID", unique=True)
oaza = models.CharField("大字", max_length=100)
aza = models.CharField("", max_length=100)
chiban = models.IntegerField("地番")
area = models.IntegerField("農地面積(m2)")
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)
# 紐付けキーraw値
raw_kyosai_k_num = models.IntegerField("細目_耕地番号")
raw_kyosai_s_num = models.IntegerField("細目_分筆番号")
raw_chusankan_id = models.IntegerField("中山間_ID", null=True, blank=True)
# 外部キー(紐付け済み)
kyosai_field = models.ForeignKey(
OfficialKyosaiField,
on_delete=models.SET_NULL,
null=True,
related_name='fields'
)
chusankan_field = models.ForeignKey(
OfficialChusankanField,
on_delete=models.SET_NULL,
null=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 = models.BooleanField("作付けする", default=True)
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']
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):
"""吉田農地台帳のインポート(紐付け処理含む)"""
file = request.FILES['file']
df = pd.read_excel(file, engine='odf')
for _, row in df.iterrows():
# 共済マスタとの紐付け
try:
kyosai = OfficialKyosaiField.objects.get(
k_num=row['細目_耕地番号'],
s_num=row['細目_分筆番号']
)
except OfficialKyosaiField.DoesNotExist:
kyosai = None
# 中山間マスタとの紐付け
chusankan = None
if pd.notna(row['中山間_ID']):
try:
chusankan = OfficialChusankanField.objects.get(
c_id=int(row['中山間_ID'])
)
except OfficialChusankanField.DoesNotExist:
pass
# 実圃場を作成
Field.objects.update_or_create(
name=row['名称'],
defaults={
'address': row['住所'],
'area_tan': row['面積(反)'],
'area_m2': int(row['面積(反)'] * 1000),
'owner_name': row['地主'],
'raw_kyosai_k_num': row['細目_耕地番号'],
'raw_kyosai_s_num': row['細目_分筆番号'],
'raw_chusankan_id': int(row['中山間_ID']) if pd.notna(row['中山間_ID']) else None,
'kyosai_field': kyosai,
'chusankan_field': chusankan,
}
)
return Response({'status': 'success', 'imported': len(df)})
```
### Step 5: 作付け計画APIDay 5
#### apps/plans/views.py
```python
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Plan
from .serializers import PlanSerializer
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)})
```
### 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月52</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内部は全て `m2` で保存、表示時に `反` に変換
- **紐付けキー**: `raw_*` フィールドと外部キー `*_field` の両方を持つ
- **ユニーク制約**: `(field, year)` で作付け計画は1つまで
### 2. 申請書CSV生成
- **共済マスタをベースにループ**: 実圃場ベースではない
- **作物の名寄せ**: Pythonのセットで重複排除
- **未割当の扱い**: 「未設定」として出力
### 3. UI/UX
- **未割当の強調**: 赤または黄色の背景色
- **スマホ対応**: 文字サイズ16px以上、タップ領域44px以上
- **検索のリアルタイム性**: `useState` + `filter` で実装
### 4. 認証
- **JWT認証**: アクセストークン1日+ リフレッシュトークン7日
- **シンプルな実装**: パスワードリセットはPhase 2
### 5. パフォーマンス
- **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! 🚀**