不動産紹介サイトの情報から住所を特定するWebアプリを作ってた

不動産、好きですか?私は中古物件を見るのが好きでたまに SUUMO や HOME's のような不動産紹介サイトを徘徊しています。 そこでよさげな物件を見つけると、不動産紹介サイトの限られた情報から住所を特定するというのが趣味です。

不動産紹介サイトに書いてある物件の場所に関わる情報は、以下です。

  1. 大まかな住所(東京都渋谷区道玄坂1丁目 など、丁目までの情報)
  2. 周辺施設からの距離(セブンイレブン 渋谷道玄坂1丁目店:徒歩5分など)

これらの情報から、Google Map を開いてだいたいここだろう!という場所にストリートビューのピンを刺して、外観が合ってたら特定!みたいなキモいことをやっています。

これを Web アプリで簡単にできたらいいのでは?と思い作ってみることにしました。

作ってみた

デモ / リポジトリ

実際に情報を入れてみたあとのスクショが以下です。候補地が赤く塗られてるので、あとはこの絞られた範囲の中で人間が探せばいい、という寸法です。

ほぼほぼ AI エージェントに書いてもらったのでそんなに面白ポイントはないですが、しいて言えば…

  • Leaflet.js + OpenStreetMap でマップを表示している

    Google Map のほうがシームレスにストリートビューでピンさせて便利だけど、API キーの用意とか面倒だし今回の用途ならこれで十分と判断。

  • 本当は例えば徒歩5分なら0~4分圏内は候補地から除外したかった

    ドーナッツ上の円が交差した部分を候補地としたほうがより正確なはず。しかしこれを実現するには、円の上に白抜きでさらに円を描画する、みたいなことをやる必要があり、マップが非常に見にくくなったのでやめた。

  • 徒歩1分=80m

    不動産の表示に関する公正競争規約で決まってるらしい。へぇ。しかしあくまで円で描画した範囲は直線距離で、実際は道はもうちょっと曲がりくねっていると思うのでバッファを設ける必要がありそう。

感想

作ってみたのはいいけど、数回使って飽きてしまった…

恐らく、限られた情報から住所を特定することにゲーム性を感じていたので、そこを効率化するのは違かったのかもしれない…

無駄も大事という教訓ですね。みなさん、無駄も楽しみましょう。本年もよろしくお願いします。


この記事は はてなエンジニア Advent Calendar 2025 - Hatena Developer Blog の37日目の記事です。

storycap + reg-suit による VRT 導入メモ

Next.js 環境の VRT をするために、storycap + reg-suit を導入したので、導入時に困ったりしたことをメモ。

CI 環境は GitHub Actions、保存先は S3。

storycap がタイムアウトする

最初 storybook dev で storybook を立ち上げて snapshot を取ってみたが、タイムアウトしてしまっていた。

$ npx storycap http://localhost:6006 --serverCmd 'storybook dev -p 6006'
info Wait for connecting storybook server http://localhost:6006.
error Timed out waiting for: http-get://localhost:6006

--serverTimeout オプションを伸ばしても無駄で、storybook dev -p 6006 & sleep 15' のよう sleep すると動くが、たまに動かなくてイマイチ安定しなかった。

storybook dev を諦めて、ビルドしたあとに http-server でホストするようにしたらうまくいった。

$ npx storybook build
$ npx storycap http://localhost:6006 --serverCmd 'npx http-server -p 6006 storybook-static'

前回の snapshot と比較できない

最初は reg-suit init に従って reg-keygen-git-hash-pluginで比較元、比較先の snapshot key を取得しようとしたが、Failed to detect the previous snapshot key と言われ、うまくいかなかった。

actions/checkout@v4fetch-depth: 0 にするとうまくいくが、checkout に時間がかかるようになってしまったので、reg-keygen-git-hash-plugin をやめてreg-simple-keygen-plugin で snapshot key を自前で設定するようにした。

日本語が文字化けする

日本語が豆腐(□) になってしまっていた。CI の中で fonts-ipafont を明示的に入れてあげて解決。

最終的に、actions の .yml ファイルと regconfig.json は以下のようになった。

# actions.yml
name: vrt-nextjs

on:
  push:
    branches:
      - main
  pull_request:
    paths:
      - '<file_path>'

jobs:
  vrt-nextjs:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: checkout
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@4
        with:
          aws-region: <region>
          role-to-assume: <role>
          role-session-name: <role_name>

      - name: Install Japanese Fonts
        run: |
          sudo apt-get install fonts-ipafont

      - name: Setup node
        uses: actions/setup-node@v4
        with:
          node-version-file: '.node-version'

      - name: npm ci
        run: npm ci

      - name: build storybook
        run: npx storybook build

      - name: capture snapshot
        run: npx storycap http://localhost:6006 --serverCmd 'npx http-server -p 6006 storybook-static' --serverTimeout 60000

      - name: exec reg-suit
        run: npx reg-suit run
        env:
          # on: push で動いた場合は比較したいわけじゃなくて比較対象のスクリーンショットを取りたいだけなので、ダミーのキーを指定しておく
          REG_EXPECTED_KEY: ${{ github.event.pull_request.base.sha || 'pseudo_expected_key' }}
          REG_ACTUAL_KEY: ${{ github.event.pull_request.head.sha || github.sha }}
# regconfig.json
{
  "core": {
    "workingDir": ".reg",
    "actualDir": "__screenshots__",
    "thresholdRate": 0,
    "addIgnore": true,
    "ximgdiff": {
      "invocationType": "client"
    }
  },
  "plugins": {
    "reg-simple-keygen-plugin": {
      "expectedKey": "$REG_EXPECTED_KEY",
      "actualKey": "$REG_ACTUAL_KEY"
    },
    "reg-notify-github-plugin": {
      "prComment": true,
      "prCommentBehavior": "default",
      "clientId": "<client_id>"
    },
    "reg-publish-s3-plugin": {
      "bucketName": "<bucket_name>",
      "acl": "private"
    }
  }
}

次は setup-node キャッシュとか ubuntu-latest じゃなくて別な高速なランナーにするとかしたい。

リモートワークで退勤後にテンションを上げるための技術

この記事は、はてなエンジニアAdvent Calendar 2023 の2024年1月9日の記事です。

昨日はid:arthur-1さんの Advent Calendarを25日分続ける技術、あるいは物を発明し素早く作る技術 - Diary of a Perpetual Student でした。アウトプットが続かない私としては25日も続けるのは尊敬です。見習いたい…



弊社はフルリモートで働くことが可能*1で、通勤が苦手な自分にとっては天国です。しかし、リモートで働いていると日々にメリハリが無く、仕事を終えてもテンションが上がらないな…と感じていました。

そこで、退勤ボタンを押した時にテンションを上げるための技術を導入しました。

用意したのはこちらのアイテムです。


早速ですが、実際に退勤ボタンを押した際の様子をご覧ください。

𝑷𝒂𝒓𝒕𝒚 𝑻𝒊𝒎𝒆 ...


どうですか?テンションが上がりそうでしょう?BGM は権利の問題上フリーの音源に変えていますが、実際には Get Wild を流しています。

作り方

準備

まずはスマートプラグとミラーボールをコンセントに繋ぎ、IFTTT 経由でオンオフできるようにします。その辺の詳細は以前 在宅ワーク中に家族に会議中だとアピールするライフハック - magamingのブログで書いたので、そちらを見てもらうとよさそうです。

スマートプラグでなくても、赤外線リモコンがついているミラーボールを、Nature Remo や SwitchBot などのスマートリモコンで操作する形でもよいと思います。

スクリプトの作成

弊社では勤怠システムに Akashi を利用しているので、ブラウザ上で勤怠管理ができます。これが何を意味するか、つまり、JavaScript を埋め込み放題という事です。今回は以下のような JS を用意しました。

const iftttUrl = '<IFTTT でスマートプラグをONにするためのURL>';
// GetWild
const videoId = 'NHKq8IOXPxA';

const playerElement = document.createElement('div');
playerElement.id = 'player';
document.body.appendChild(playerElement);

const tag = document.createElement('script');
tag.src ='https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

var player;
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    // 音だけ再生できればよいので、サイズは 0
    height: '0',
    width: '0',
    videoId: videoId,
  });
}

const partyTime = () => {
  fetch(iftttUrl);
  // ifttt によるスイッチオンにタイムラグがあるので1秒待つ
  setTimeout(() => { 
    // サビまでシーク
    player.seekTo(60);
    player.playVideo();
  }, 1000);
}

const taikinButton = document.querySelector(<退勤ボタンの class>);
taikinButton.addEventListener('click', partyTime);

要は iframe の Player を無理やり埋め込んで、退勤ボタンを押したら「IFTTT URL を叩く」「Youtube Player API で再生」をやっているだけです。

スクリプトの埋め込み

最初は Chrome 拡張作ろうかと思ってましたが、めんどくなってきたので今回は ScriptAutoRunner で雑に JS を実行する形にしました。今回のように、雑にJSで遊びたいときにはオススメです。ブラウザ上で動く勤怠システムであれば、Akashi でなくても導入可能なはずです。


いかがでしたか?皆様も退勤に彩りを与えてみてはいかがでしょうか。それではよい退勤ライフを…

GitHub actions で PR が無ければ作る、あれば更新する

こんな感じでできた。gh pr list で標準で jq 使えることが今回の発見。かっこいい。

env:
  TARGET_BRANCH: 'target-branch'
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  create-or-update-pr:
    runs-on: ubuntu-latest
    steps:
      - name: search pr number and set to env
        # この場合は head が 'target-branch' かつマージされてないブランチを探している
        run: echo "PR_NUMBER=$(gh pr list --search "head:${{ env.TARGET_BRANCH }} is:unmerged" --json number --jq ".[].number")" >> $GITHUB_ENV
      - name: create pull request
        # 空なら 0 が入る ref. https://docs.github.com/ja/actions/learn-github-actions/expressions#operators
        if: ${{ env.PR_NUMBER == 0 }}
        run: gh pr create --base main --head ${{ env.TARGET_BRANCH }} --title "タイトル" --body "本文"
      - name: update pull request
        if: ${{ env.PR_NUMBER != 0 }}
        run: gh pr edit ${{ env.PR_NUMBER }} --title "更新後のタイトル" --body "更新後の本文"

MySQL でテーブル定義を JSON形式で吐き出す

諸事情でテーブル名と、テーブルのカラムを以下のような JSON 形式で出す必要があった。

{
  "<テーブル名1>": [
    "<カラム名1>",
    "<カラム名2>",
    ...
  ],
  "<テーブル名2>": [
    "<カラム名1>",
    "<カラム名2>",
    ...
  ]
}

最初は MySQL でテーブル定義を csv に吐き出して、スプレッドシートに貼り付けて、GAS で JSON 形式に変換する… みたいな回りくどいことをやっていたけど、JSON 周りの関数を使えばどうやら MySQL 一発でいけそうだった。

SELECT
    JSON_OBJECTAGG(
        c1.table_name, 
        (
            SELECT JSON_ARRAYAGG(column_name)
                FROM information_schema.columns as c2
                WHERE c2.table_name = c1.table_name
        )
    )
FROM information_schema.columns as c1
WHERE table_schema = '<DB名>'
;

これらの関数は MySQL 5.7.22 以降なら使える模様だけど、sort には未対応っぽいのでいい感じに並べ替えたいなら別な方法を取る必要があるかも。

Aggregates a result set as a single JSON array whose elements consist of the rows. The order of elements in this array is undefined.

MySQL :: MySQL 8.0 Reference Manual :: 12.19.1 Aggregate Function Descriptions

【GraphQL】何らかの条件で何らかを集めてくるフィールドの設計、どうする?

何を言っているかわからないと思うけど、例えば「1週間以内に新規登録したユーザー一覧を返す」みたいなフィールドをどう設計するかという話。いくつかパターンがありそう。

1. 専用のフィールドを用意する

query {
  recentlyRegisteredUsers: [User!]!
}

クライアントからはこのフィールド呼べばいいだけなので楽。ただし、新たに「最近更新があったユーザー一覧を返したい」のような要望がでてきたら、似たようなフィールドが無尽蔵に増えていく可能性がある。

2. 全部返すフィールドを特定の条件でフィルタできるようにする

query {
  users(
    filter: UserFilter
  ): [User!]!
}

enum UserFilter {
    "1週間以内に新規登録したユーザー"
    RECENTLY_REGISTERED
}

1 よりはフィールドの治安は保たれそうだけど、代わりにフィルタ条件が増えていくことになる。

3. 検索用のフィールドを用意する

query {
  searchUser(
    registeredSince: DateTime,
    registeredUntil: DateTime
  ): [User!]!
}

2 と似てるけど、クライアント側から検索条件を指定するタイプ。シンプルな検索条件ならこれでいいけど、複雑になってくるとクライアント側からのクエリが大変なことになりそうだし、検索条件のオプションも増えまくって大変になりそう。

4. queryString で検索できるようにする

query {
  searchUser(
    query: String
  ): [User!]!
}

クエリ文字列でなんでも検索する方式。こちらもシンプルな検索条件ならいいけど、複雑になってくるとクエリを組み立てるのが大変になりそう。queryString をパースする必要があるのでバックエンドの実装は他の案と比べて複雑になるかも。

結局どれがいい?

以下のような方針が良い気がしている。

  • 検索条件がシンプルなら、検索用フィールドから検索できるようにする

    今回の例なら、ユーザーの登録時期で検索とかは汎用的な要件なのでこちらがいいのかも?

  • 検索条件が複雑なら、専用のフィールドを用意する

    例えば「1ヶ月以内に登録して、なおかつ1週間以内に5回以上更新があったプレミアム登録済みのユーザー一覧」みたいな条件だと特殊な要件すぎるのでこちらがいいのかも?

他に何かオススメあったら教えてください!

キーボードとマウスを新しくした

今まで普通のテンキーレスのキーボードを使っていたんだけど、FPS をプレイするときにマウスが当たってしまってエイムの邪魔になるので60%キーボードを買った。

Ducky One 3 Mini 60% keyboard Classic Pure White

エンターキーの色が違うのがおしゃれ。めっちゃ光ってるけど、ライトパターン10個くらいあって控えめなのも選べるのでよい。

せっかくいいキーボード買ったので、仕事用の Mac Book Pro でも使っている。今までは周りが HHKB とか使っているのを見ても「キーボード買ったところでタイピング速度なんてそんな変わらなくね?」と冷めた目で見て頑なに Mac Book の付属のキーボードを使っていたけど、しばらく使ったところ、確かにタイピング速度は別に変わらないけど気に入ったガジェット使ってると気分は上がっていいな、となった(手のひら高速回転)。

マウスもついでに新しくしてG PRO X SUPERLIGHT にしたんだけど、Mac で使うときにマウス加速が邪魔すぎるので LinearMouse を入れて切っている。Mac 使ってると、こういうのわざわざアプリケーション入れないと設定できないのイマイチだよな〜って思う。アプリケーションごとに音量をいじれないとか、Windows ならそういうのは OS 標準でできるのに…

なお FPS は全然上手くなってない様子。