29 KiB
29 KiB
Keina System - Gemini向け実装指示書
📋 このドキュメントについて
これは、農業生産者向けの作付け計画管理システム「Keina System」の実装に必要な全情報を統合したドキュメントです。
実装エージェント(Gemini/OpenCode)は、以下の順序でドキュメントを読み、理解してから実装を開始してください。
🎯 システムの全体像
目的
年間の作付け計画を管理し、役場への申請書類(水稲共済細目書・中山間地域等直接支払交付金)を自動生成するシステム。
ユーザー
- 65歳の農家(元プログラマー)
- シングルユーザー(マルチテナント不要)
- PCで登録・編集、スマホで参照
主要機能(Phase 1 / MVP)
- 作付け計画の一覧表示・編集
- 水稲共済細目書のCSV出力
- 中山間交付金申請書のCSV出力
- 前年度作付けのコピー機能
- 圃場情報のスマホ参照
技術スタック
- バックエンド: 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: 事前準備(必須)
- 上記5つのドキュメントを全て読む
- 不明点があれば、実装開始前に質問する
- 実データ(
吉田農地台帳.ods,水稲共済細目用.ods,中山間.ods)を確認
Step 1: 環境構築(Day 1)
# プロジェクト構造
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
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
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(重要な設定のみ)
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
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
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)
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: 作付け計画API(Day 5)
apps/plans/views.py
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
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
<!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
<!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
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(申請書ダウンロード画面)
'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内部は全て
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が動かない
# settings.py に以下を追加
GDAL_LIBRARY_PATH = '/usr/lib/libgdal.so'
GEOS_LIBRARY_PATH = '/usr/lib/libgeos_c.so'
ODSファイルが読めない
pip install odfpy --break-system-packages
WeasyPrintのインストールエラー
# Ubuntu/Debianの場合、システムライブラリが必要
apt-get install python3-cffi python3-brotli libpango-1.0-0 libpangoft2-1.0-0
pip install WeasyPrint --break-system-packages
PDFの日本語が表示されない
# HTMLテンプレートのCSSに日本語フォントを明示
body {
font-family: "MS Gothic", "Yu Gothic", "Hiragino Sans", sans-serif;
}
CORSエラー
# settings.py
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
]
マイグレーションエラー
docker-compose exec backend python manage.py makemigrations
docker-compose exec backend python manage.py migrate
🎉 実装完了の定義
以下が全て完了したら、Phase 1は実装完了とする:
- ✅ ログインできる
- ✅ 作付け計画を編集できる
- ✅ 水稲共済PDFをダウンロードできる
- ✅ 中山間PDFをダウンロードできる
- ✅ PDFをプレビュー表示できる
- ✅ PDFをA4用紙に印刷してそのまま提出できる品質
- ✅ 前年度コピーができる
- ✅ スマホで圃場情報を見られる
- ✅ 実データ(3つのODSファイル)をインポートできる
📞 質問・不明点がある場合
実装中に疑問が生じた場合、以下を確認してください:
- データ仕様書.md: データの紐付けロジックが不明な場合
- 画面設計書.md: UIの詳細が不明な場合
- ユーザーストーリー.md: 機能の目的・受け入れ基準が不明な場合
それでも解決しない場合は、実装を止めて質問してください。
Good luck with the implementation! 🚀