# 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

水稲共済細目書({{ year }}年度)

{% for row in rows %} {% endfor %}
耕地番号 分筆番号 地名地番 漢字地名 本地面積
(m2)
作付品目 品種 備考
{{ row.k_num }} {{ row.s_num }} {{ row.address }} {{ row.kanji_name }} {{ row.area }} {{ row.crops }} {{ row.varieties }} {{ row.note }}
``` #### apps/reports/templates/reports/chusankan_template.html ```html

中山間地域等直接支払交付金({{ year }}年度)

{% for row in rows %} {% endfor %}
ID 大字 地番 農地面積
(m2)
作付品目 品種 備考
{{ row.c_id }} {{ row.oaza }} {{ row.aza }} {{ row.chiban }} {{ row.area }} {{ row.crops }} {{ row.varieties }} {{ row.note }}
``` #### 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 (

申請書ダウンロード

{/* 水稲共済細目書 */}

📄 水稲共済細目書

提出時期: 2月・5月(年2回)

区画数: 31区画

{/* 中山間交付金 */}

📄 中山間地域等直接支払交付金

提出時期: 5月(年1回)

区画数: 71区画

) } ``` --- ## ⚠️ 重要な実装上の注意点 ### 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! 🚀**