神社 地域 にデジタルを
❝地図とグラフで見る豊中市データ❞のコーディング
Django4 python3.10 javascript wsl bootstrap folium geopandas shell_plus pandas plotly
大阪府のデータを用いたMap・Graphのコーディングと内容は相似しています。
ただし、豊中市内の町丁レベルで区分けし、その単位で7年間のデータを取り込むため、大阪府のデータよりもはるかに多量を要します。
前段階がかなり多く、豊中市が公表しているデータ、大阪府の公表データ・大阪府警の公表データから、豊中市部位の町丁データを取り出すなどが必要。
豊中市の場合は、蛍池の蛍が螢であったりと表記ゆれの対応が必要で、住所が漢数字だったりアラビア数字だったりしたため、統一させる加工が必要でした。
町丁レベルで合わせても、データが欠落している場合もあります。
町丁レベルの画面の切り替え表示には、Getパラメータを用いJavascriptで実装しました。
セレクトボックスへの町丁データの投入は、views.py側から受け取る形を取っています。
動きは遅いですが、ほぼ想定通りに動きます。概要を記します。
<更新:2023-9-6>
以下の記述でも動きますが、動作が遅いため実際には修正しています。
まず地図の読み込みが遅いため、foliumで処理完了状態のhtmlを出力し、このhtmlをダイレクトに描画する形に変更しました。出力後のhtmlに微修正を施しています。
map.save(xxx.html)でhtmlファイルが出力されます。グラフの描画遅いため、plotlyのjsの読み方を工夫しました。
views.pyで、fig.to_html(include_plotlyjs=False)としてjsの読み込みをFalseにし
template側でcdnでplotlyのjsを読み込みます。体感ですが、早くなりました。
事前準備(手順の概略)
<Poetryで仮想環境を準備(例)>事前にPoetryをインストールしておく
仮想環境をフォルダ直下にする
poetry config virtualenvs.in-project true
~$ mkdir o_poetry_dj
~$ cd o_poetry_dj/
~/o_poetry_dj$ poetry init
以下の設問にEnter、yes、noを入力
This command will guide you through creating your pyproject.toml config.
Package name [o_poetry_dj]:※Enterキー
Version [0.1.0]:※Enterキー
Description []:※Enterキー
Author [user , n to skip]:※Enterキー
License []:※Enterキー
Compatible Python versions [^3.10]:※Enterキー
Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file
[tool.poetry]
name = "o_poetry_dj"
version = "0.1.0"
description = ""
authors = ["user "]
[tool.poetry.dependencies]
python = "^3.10"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Do you confirm generation? (yes/no) [yes] yes
poetry shell
<venvの場合(例)>※venvがインストールされていることを確認
python3 -m venv app_toyonaka_data
source app_o_ytdl/bin/activate
以後、仮想環境で作業
必要なパッケージをインストール
<poetryの場合>
(仮想名) poetry add folium geopandas pandas plotly...
<pipの場合>
(仮想名) pip install folium geopandas pandas plotly...
加えて
shell_plusがつかえるようにする ※Djangoのshellでモデルの操作をするときなど、sehllが格段に使いやすくなります。
(仮想名) poetry add django-extensions ...or...
(仮想名) pip install django-extensions
使い方
(仮想名) python manage.py shell_plus
以上
Djangoプロジェクトにアプリを作成
python manage.py startapp toyonaka_data
スーパーユーザーを作成
python manage.py createsuperuser
settings.py(config/settings.py)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"toyonaka_data",
...省略...
# 3th party
'django_extensions',
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
...省略...
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
プロジェクトフォルダのsettings.pyに記述
urls.py(config/urls.py)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("toyonaka_data/", include("toyonaka_data.urls")),
...省略...
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
プロジェクトフォルダのurls.pyに記述
フォルダ構成
toyonaka_data/
├── admin.py
├── apps.py
├── __init__.py
├── management/
├── migrations/
├── models.py
├── __pycache__/
├── tests.py
├── urls.py
└── views.py
managementフォルダにcommandsフォルダを作り、load_toyonaka_data.pyとload_toyonaka_generation.pyを配置します。
├── management/
│ └── commands/
│ ├── load_toyonaka_data.py
│ ├── load_toyonaka_generation.py
models.py
from django.db import models
class ToyonakaBase(models.Model):
area = models.CharField(max_length=100)
population = models.IntegerField()
setai = models.IntegerField()
setai_per = models.FloatField()
crime = models.FloatField(null=True, blank=True)
elevation = models.FloatField()
def __str__(self):
return "{} - {}".format(self.aria, self.population)
class ToyonakaGeneration(models.Model):
area = models.CharField(max_length=100)
age = models.CharField(max_length=30)
male = models.IntegerField()
female = models.IntegerField()
total = models.IntegerField()
year = models.CharField(max_length=30)
age_range = models.CharField(max_length=30)
market_range = models.CharField(max_length=30)
def __str__(self):
return str(self.pk)
models.pyには、前処理したデータのカラム(列名)に合わせたモデルを設計します。
toyonaka_data/management/commands/load_toyonaka_data.py
modelにデータを投入するための独自コマンドを2つ作る
import csv
from django.conf import settings
from django.core.management.base import BaseCommand
from toyonaka_data.models import ToyonakaBase
class Command(BaseCommand):
# データを更新する場合は削除して挿入し直す
# if ToyonakaBase.objects.count() > 0:
# ToyonakaBase.objects.all().delete()
help = "豊中市の基礎データをモデルに投入"
def handle(self, *args, **kwargs):
data_file = settings.BASE_DIR / "data" / "last_data2.csv"
keys = (
"area", "population", "setai", "setai/per", "crime", "elevation"
)
records = []
with open(data_file, "r") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
records.append({k: row[k] for k in keys})
for record in records:
ToyonakaBase.objects.get_or_create(
area=record["area"],
population=record["population"],
setai=record["setai"],
setai_per=record["setai/per"],
crime=record["crime"],
elevation=record["elevation"]
)
toyonaka_data/management/commands/load_toyonaka_generation.py
import csv
from django.conf import settings
from django.core.management.base import BaseCommand
from toyonaka_data.models import ToyonakaGeneration
class Command(BaseCommand):
#データを更新刷る場合は削除して挿入し直す
if ToyonakaGeneration.objects.count() > 0:
ToyonakaGeneration.objects.all().delete()
help = "豊中市の年齢別人口の推移データをモデルに投入"
def handle(self, *args, **kwargs):
data_file = settings.BASE_DIR / "data" / "h27tor3_toyonaka_bese.csv"
keys = (
"area", "age", "male", "female", "total", "year",
"age_range", "market_range"
)
records = []
with open(data_file, "r") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
records.append({k: row[k] for k in keys})
for record in records:
ToyonakaGeneration.objects.get_or_create(
area=record["area"],
age=record["age"],
male=record["male"],
female=record["female"],
total=record["total"],
year=record["year"],
age_range=record["age_range"],
market_range=record["market_range"]
)
(仮想名) python manage.py load_toyonaka_generation または load_toyonaka_data
- コマンドを使ってDBモデルに投入します。
- shell_plusで投入できたか確認
urls.py
from django.urls import path
from .views import(
toyonaka_map_index, toyonaka_map_setai,
toyonaka_map_hinan, toyonaka_map_crime,
toyona_age_range, toyona_market_range,
)
app_name = "toyonaka_data"
urlpatterns = [
path("", toyonaka_map_index, name="main"),
path("setai/", toyonaka_map_setai, name="setai"),
path("ele_hinan/", toyonaka_map_hinan, name="ele_hinan"),
path("crime/", toyonaka_map_crime, name="crime"),
path("g_main/", toyona_age_range, name="g_main"),
path("g_market/", toyona_market_range, name="g_market"),
]
地図とグラフを表示させるためのurlを設定します。
views.py
from django.shortcuts import render
import folium
import geopandas as gpd
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from .models import ToyonakaBase, ToyonakaGeneration
from django.conf import settings
from folium.features import CustomIcon
icon_path = settings.BASE_DIR / "data" / 'jija_marker.png'
icon = CustomIcon(
icon_image = str(icon_path)
,icon_size = (25, 25)
,icon_anchor = (30, 30)
,popup_anchor = (3, 3)
)
def toyonaka_map_index(request):
data = ToyonakaBase.objects.all().values()
df = pd.DataFrame(data)
geo_data = settings.BASE_DIR / "data" / "toyonaka_geo2.json"
geo_df = gpd.read_file(geo_data)
geo_df = geo_df[["S_NAME", "geometry", "Y_CODE", "X_CODE"]]
last_df = pd.merge(geo_df, df, how="left", left_on="S_NAME", right_on="area")
last_df["pref"] = [x for x in range(1, 338)]
# print(last_df)
# print(last_df.columns)
last_df = last_df[["pref", "area", "geometry", "population", "setai", "setai_per", "crime",
"elevation", "Y_CODE", "X_CODE"]]
last_df_plot = last_df.copy()
last_df_plot["pref"] = last_df["pref"].astype(str)
### Map 処理 ###
last_df_plot.crs = "epsg:4326"
toyonaka_pop = folium.Map(
location=["34.7812", "135.4697"],
tiles="cartodbpositron",
zoom_start=13,
)
folium.Choropleth(
geo_data=last_df_plot,
data=last_df_plot,
columns=["pref", "population"],
key_on="feature.properties.pref",
name="令和3年 豊中市の人口分布",
fill_color="RdPu",
fill_opacity=0.5,
line_opacity=0.2,
legend_name="令和3年 豊中市の人口分布",
# bins=bins,
reset=True,
).add_to(toyonaka_pop)
style_function = lambda x: {
"fillColor": "#ffffff",
"color": "#000000",
"fillOpacity": 0.1,
"weight": 0.1
}
highlight_function = lambda x: {
"fillColor": "#000000",
"color": "#000000",
"fillOpacity": 0.50,
"weight": 0.1
}
NIL = folium.features.GeoJson(
last_df_plot,
style_function=style_function,
control=False,
highlight_function=highlight_function,
tooltip=folium.features.GeoJsonTooltip(
fields=["area", "population"],
aliases=["町名 : ", "人口(人) : "],
style=("background-color: white; color: #333333; font-family: arial; font-size: 12px; padding: 10px")
)
)
toyonaka_pop.add_child(NIL)
folium.Marker(location=[34.76967354061533, 135.48738845413897],
tooltip="当 若宮 住吉神社", icon=icon).add_to(toyonaka_pop)
# folium.Marker(location=[34.76967354061533, 135.48738845413897],
# tooltip="若宮 住吉神社").add_to(toyonaka_pop)
context = {
"map": toyonaka_pop._repr_html_(),
}
return render(request, "toyonaka_data/main.html", context)
def toyonaka_map_setai(request):
data = ToyonakaBase.objects.all().values()
df = ...
上のコードとほぼ同じ "setai_per"をキーにする
...
return render(request, "toyonaka_data/setai.html", context)
def toyonaka_map_hinan(request):
### 避難場所のデータ ###
hinan_path = settings.BASE_DIR / "data" / "2021_toyonaka_hinan.csv"
df_hinan = pd.read_csv(hinan_path)
df_hinan = df_hinan[["名称", "住所", "緯度", "経度", "災害種別_洪水", "災害種別_津波", "想定収容人数"]]
df_hinan_flood = df_hinan[df_hinan["災害種別_洪水"]==1]
df_hinan_tsunami = df_hinan[df_hinan["災害種別_津波"]==1]
### end ###
data = ToyonakaBase.objects.all().values()
df = ...
上のコードとほぼ同じ "elevation"をキーに
...
folium.Marker(location=[34.76967354061533, 135.48738845413897],
tooltip="若宮 住吉神社", icon=icon).add_to(toyonaka_ele)
... 省略 ...
return render(request, "toyonaka_data/ele_hinan.html", context)
def toyonaka_map_crime(request):
data = ToyonakaBase.objects.all().values()
df = ...
上のコードとほぼ同じ "crime"をキーに
...
return render(request, "toyonaka_data/crime.html", context)
### Graph setting ###
color_list = ["#073472", "#DAB021", "#B9D4E8", "#25232F", "#BF7356"]
colname_list = ["幼年人口-0~14歳", "高年齢人口-65歳以上", "生産年齢人口-15~64歳"]
colname_male = ["M1層-男性20~34歳", "M2層-男性35~49歳", "M3層-男性50~64歳"]
colname_female = ["F1層-女性20~34歳", "F2層-女性35~49歳", "F3層-女性50~64歳"]
### end ###
def toyona_age_range(request):
data = ToyonakaGeneration.objects.all().values()
s_area = ToyonakaGeneration.objects.all().values_list("area", flat=True).order_by("area").distinct()
chose_area = request.GET.get("area")
if chose_area:
chose_area=chose_area
else:
chose_area="若竹町1丁目"
print(chose_area)
df = pd.DataFrame(data)
df_s = df[df["area"]==chose_area].groupby(["area", "year", "age_range"])["total"].sum().unstack()
df_s = df_s.reset_index()
fig = go.Figure()
t_text = df_s["area"][0]
for i, col in enumerate(df_s.columns[2:]):
fig.add_trace(go.Bar(
y=df_s["year"], x=df_s[col],
marker_color=color_list[i],
name=colname_list[i]
))
fig.update_traces(width=0.5,
hovertemplate="%{x}人",
texttemplate="%{x}人",
textposition="inside",
textfont={"color": "white",
"size": 12},
orientation="h")
fig.update_layout(title=f"<b>{t_text}人口構成の推移",
legend=dict(orientation="h",
xanchor="right",
x=1,
yanchor="bottom",
y=1.05,
traceorder="normal"),
yaxis=dict(title='平成27年~令和3年',
tickformat='年',
),
xaxis=dict(title='単位:人',
tickformat='人',
),
barmode="stack",
plot_bgcolor="white",
height=700)
html = fig.to_html()
context = {"chart": html, "area": s_area}
return render(request, "toyonaka_data/g_main.html", context)
def toyona_market_range(request):
data = ToyonakaGeneration.objects.all().values()
s_area = ToyonakaGeneration.objects.all().values_list("area", flat=True).order_by("area").distinct()
chose_area = request.GET.get("area")
if chose_area:
chose_area=chose_area
else:
chose_area="若竹町1丁目"
print(chose_area)
df = pd.DataFrame(data)
df_cm = df[df["area"]==chose_area].groupby(["area", "year", "market_range"])["male"].sum().unstack()
df_cm = df_cm.reset_index()
df_cf = df[df["area"]==chose_area].groupby(["area", "year", "market_range"])["female"].sum().unstack()
df_cf = df_cf.reset_index()
fig = go.Figure()
t_text = df_cf["area"][0]
for i, col in enumerate(df_cm.columns[2:5]):
fig.add_trace(go.Scatter(
x=df_cm["year"], y=df_cm[col],
mode='markers+lines',
marker=dict(
size=13,
symbol='star',
),
name=colname_male[i],
line=dict(color=color_list[i]),
opacity=0.8
))
for i, col in enumerate(df_cf.columns[2:5]):
fig.add_trace(go.Scatter(
x=df_cf["year"], y=df_cf[col],
mode='markers+lines',
marker=dict(
size=13,
symbol='square',
),
name=colname_female[i],
line=dict(color=color_list[i]),
opacity=0.8
))
fig.update_layout(
title=f"<b>{t_text}の世代人口の推移",
# legend=dict(
# x=0.01,
# y=0.99,
# xanchor='left',
# yanchor='top',
# orientation='h'
# ),
xaxis=dict(title='平成27年~令和3年',
tickformat='年',
),
yaxis=dict(title='単位:人',
tickformat='人',
),
height=700
)
html = fig.to_html()
context = {"chart": html, "area": s_area}
return render(request, "toyonaka_data/g_market.html", context)
views.pyは地図、グラフともにコードを記述するため、長いコードになりました。
- valuesでpandasのdataframeとして読み込みます。27行目
- geopandasで境界データを読み込みます。30行目
- 豊中市の中心をlocationに指定してマップを作成。50行目以降
- 161行目以降はグラフ表示の記述。Getパラメータで受け取り、項目に応じた表示に切り分けます。
templates/base.html
※雛形のイメージhtmlです 用途に合わせて手直してください
{% load static %}
<!doctype html>
<html lanh=ja>
<head>
<!-- Require meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0", shrink-to-fit=none>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<!-- custom css & js -->
<link rel="stylesheet" href="{% static 'style.css' %}">
<script src="{% static 'main.js' %}" defer></script>
<title>Spiner | {% block title %}{% endblock title %}</title>
</head>
<body>
<div class="container mt-3">
{% block content %}
{% endblock content %}
</div>
<!-- Optional Javascript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS-->
<script
src="https://code.jquery.com/jquery-3.6.3.min.js"
integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
</body>
</html>
templates/toyonaka_data/main.html
base.htmlを継承した表示用のhtmlを作成します。
{% extends 'base.html' %}
{% block title %}地図で見る豊中市のデータ - 人口 -{% endblock title %}
{% block contents %}
<div class="row">
<div class="col">
<h3 class="mb-3">地図で見る豊中市のデータ - 人口 -</h3>
<div class="dropdown dropdown-on-hover d-inline-block">
<button class="btn btn-dark dropdown-toggle" id="dropdownMenuHoverButton" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">豊中市の地図データを選択</button>
<div class="dropdown-menu dropdown-menu-right py-0" aria-labelledby="dropdownMenuHoverButton">
<a class="dropdown-item" href="{% url 'toyonaka_data:main' %}">令和3年 豊中市の人口の分布:人</a>
<a class="dropdown-item" href="{% url 'toyonaka_data:setai' %}">令和2年 豊中市の一世帯の人数:人</a>
<a class="dropdown-item" href="{% url 'toyonaka_data:ele_hinan' %}">豊中市の町丁の標準地の標高と避難所(令和4年更新場所)</a>
<a class="dropdown-item" href="{% url 'toyonaka_data:crime' %}">令和3年 豊中市の盗難犯罪の発生の分布</a>
</div>
</div>
<h4 class="text-center">令和3年 豊中市の人口の分布:人</h4>
{{ map|safe }}
<p><small>豊中市の境界のデータ<br>
「e-Stat」<br>
https://www.e-stat.go.jp/gis/statmap-search?page=2&type=2&aggregateUnitForBoundary=A&toukeiCode=00200521&toukeiYear=2015&serveyId=A002005212015&prefCode=27&coordsys=1&format=shape</small></p>
<p><small>年齢別人口【町丁目別】<br>
「豊中市」<br>
https://www.city.toyonaka.osaka.jp/joho/toukei_joho/jinkou_toukei/nenreibetsujinko/index.html</small></p>
</div>
</div>
{% endblock contents %}
ほぼ同じ内容で、以下のテンプレートを用意します。
- templates/toyonaka_data/crime.html
- templates/toyonaka_data/ele_hinan.html
- templates/toyonaka_data/setai.html
templates/area_data/bar_65over.html
棒グラフ表示用のhtmlを作成します。
{% extends 'base.html' %}
{% load static %}
{% block title %}グラフで見る豊中市のデータ - 年代構成 -{% endblock title %}
{% block contents %}
<div class="row">
<div class="col">
<h3 class="mb-3">グラフで見る豊中市のデータ - 年代構成 -</h3>
<form nethod="GET" action="{% url 'toyonaka_data:g_main' %}" name="area-form">
<div class="select-box">
<div class="select-option">
<input type="text" placeholder="市内の町丁を選択" id="soValue" name="area">
</div>
<div class="content">
<div class="search">
<input type="text" id="optionSearch" placeholder="Search" name="">
</div>
<ul class="options">
{% for item in area %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</div>
</div>
<button type="submit" class="btn btn-primary mt-2 mb-3">グラフ表示</button>
</form>
{{ chart|safe }}
<p><small>年齢別人口【町丁目別】<br>
「豊中市」<br>
https://www.city.toyonaka.osaka.jp/joho/toukei_joho/jinkou_toukei/nenreibetsujinko/index.html</small></p>
</div>
</div>
{% endblock contents %}
{% block end_scripts %}
<script src={% static 'js/selected.js' %}></script>
{% endblock end_scripts %}
ほぼ同じ内容で、以下のテンプレートを用意します。
- templates/toyonaka_data/g_market.html
static/js/selected.js
セレクトボックスと検索のためのjavascriptを記述します
const selectBox = document.querySelector(".select-box");
const selectOption = document.querySelector(".select-option");
const soValue = document.querySelector("#soValue");
const optionSearch = document.querySelector("#optionSearch");
const options = document.querySelector(".options")
const optionsList = document.querySelectorAll(".options li");
selectOption.addEventListener("click", function(){
selectBox.classList.toggle("active");
})
optionsList.forEach(function(optionListSingle){
optionListSingle.addEventListener("click", function(){
text = this.textContent;
soValue.value = text;
selectBox.classList.remove("active");
})
});
optionSearch.addEventListener("keyup", function(){
var filter, li, i, textValue;
filter = optionSearch.value;
li = options.getElementsByTagName("li");
for(i=0; i<li.length; i++){
liCount = li[i];
textValue = liCount.textContent || liCount.innerText;
if(textValue.indexOf(filter) > -1){
li[i].style.display = "";
}else{
li[i].style.display = "none";
}
}
})
投稿内容 ランダム表示
HTMLメールを活用してみた
最終更新:2023年07月14日
❝地図とグラフで見る大阪府データ❞のコーディング
最終更新:2023年09月06日
神社のオリジナルTシャツを作ってみた
最終更新:2023年08月07日
❝世界のニュースを少し知る❞のコーディング
最終更新:2023年07月14日
python3.10 Django4 wsl bootstrap javascript
カテゴリ
私の願い
私は神社の宮司です。神社や地域を担う次世代の人々に対し、何かを残してお役に立ててもらいたいとの願いが、強く芽生えました。個業としての神社や、小規模な地域社会に、恩恵が届くのが遅くなりそうな「デジタル」の分野。門外漢として奮闘した実体験から得た経験則を、わずかずつでも残し未来につなぎたいと願うばかりです。
最近の投稿
- 簡易で安価なカメラで防犯・外出対策を
最終更新:2023年09月08日
- 神社のオリジナルTシャツを作ってみた
最終更新:2023年08月07日
- HTMLメールを活用してみた
最終更新:2023年07月14日
- 豊中市Graphの基データの説明
最終更新:2023年07月14日