【個人開発】名鉄名古屋駅の乗車位置検索アプリに駅名の検索機能を実装してみた（React/TypeScript)

# はじめに
本記事では個人開発アプリの[乗車位置チェッカーof名鉄名古屋駅]("https://sta.orion-raptured.com")の技術的な話題について触れることができればと思います。本アプリケーションの概要は下記リンクにて紹介させていただいております。

https://sta.orion-raptured.com

https://qiita.com/takeo-t/items/ed18f0d42946c610ba60

また、Reactは下記のバージョンを使用して開発しています。
```
React 18.2.0
TypeScript 4.9.5
```

# 1. 状態の管理
ReactのhooksであるuseStateを使用して、アプリケーションの複数の状態を管理しています。これには、入力値、全駅のリスト、検索結果、選択された駅などが含まれます。
```src/components/pages/Home.tsx
export const Home: FC = memo(() => {
　　　　　　//ユーザー入力値の状態
    const [inputValue, setInputValue] = useState(""); 
　　　　　//名鉄線全駅の取得
    const [allStations, setAllStations] = useState<Station[]>([]);
　　　　//検索結果の表示
    const [searchResults, setSearchResults] = useState<Station[]>([]);
　　　　//検索結果から選択された駅
    const [selectedStation, setSelectedStation] = useState<Station | null>(null);
　　　//検索結果が無い場合の状態を管理
    const [noResults, setNoResults] = useState(false)
　　　//名鉄名古屋駅からの直通列車がない駅の乗り換え駅を管理
    const [changeStationData, setChangeStationData] = useState<{ data: ChangeStationData[] } | null>(null);
　　 //乗り換え情報をAPIから取得し管理
    const [matchingStation, setMatchingStation] = useState<ChangeStationData | null>(null);
```

# 2. データの取得
useEffectを使用して、Rails APIから全ての駅データを非同期的に取得しています。
データは axios を使用して取得され、成功時にステートが更新されます。
```src/components/pages/Home.tsx
useEffect(() => {
        axios.get("Rails APIのURL")
        .then(response => {
            const dataCamelCased = keysToCamelCase(response.data.data);
            setAllStations(dataCamelCased);
            console.log(dataCamelCased);
        })
        .catch(error => {
            console.log(error);
            alert('読み込みに失敗しました。')
        });
      }, []);
```
各駅のデータ構造は下記jsonのようになっています。主なデータはid,駅名,駅が所属する路線名,乗車位置情報などです。
将来的に停車種別の表示に対応したいためその情報を含んでいます。また乗り換えの有無をBooleanで格納しています。
```
[
    {
        "id": 150101,
        "line_name": "名古屋本線",
        "station_num": "NH37",
        "station_name": "栄生",
        "station_name_kana": "さこう",
        "track_num": 1,
        "position": "一宮・岐阜方面",
        "rpd_ltd_exp": false,//快速特急
        "limited_exp": false,//特急
        "rpd_exp": false,//快速急行
        "exp": true,//急行
        "semi_exp": true,//準急
        "position_color": "青",
        "color": "blue",
        "change_station": false
    },
    {
        "id": 150102,
        "line_name": "名古屋本線",
        "station_num": "NH40",
        "station_name": "二ツ杁",
        "station_name_kana": "ふたついり",
        "track_num": 1,
        "position": "一宮・岐阜方面",
        "rpd_ltd_exp": false,
        "limited_exp": false,
        "rpd_exp": false,
        "exp": false,
        "semi_exp": true,
        "position_color": "青",
        "color": "blue",
        "change_station": true
    }, ...//その他名鉄線内250以上の駅
]
```
idの役割を補足します。
```
"id": 150101
1...発着番線を示す(名鉄名古屋駅1番ホーム)
5...並び位置を示す(一宮・岐阜方面)
01...路線名を示す(名鉄名古屋本線)
01...駅名を示す(栄生駅)
```


# 3. キャメルケース変換
Rails APIから取得したデータはスネークケースなので、Reactの慣習に従いキャメルケースに変換する関数 toCamelCase と keysToCamelCase を実装しています。
```src/components/pages/Home.tsx
const toCamelCase = (str: string) => {
        return str.replace(/([-_][a-z])/g, (group) =>
          group.toUpperCase()
            .replace('-', '')
            .replace('_', '')
        );
      };
    
      const keysToCamelCase = (obj: any): any => {
        if (obj instanceof Array) {
          return obj.map((v) => keysToCamelCase(v));
        } else if (obj !== null && obj.constructor === Object) {
          return Object.fromEntries(
            Object.entries(obj).map(
              ([key, value]) => [toCamelCase(key), keysToCamelCase(value)]
            )
          );
        }
        return obj;
      };

    //line_name
    // ↓
    //LineName
```
# 4. 検索機能
handleInputChange関数でユーザー入力が変更されるたびに検索関数がトリガーされます。
search関数で、入力値と一致する駅名をフィルタリングします。今のところはロジックのとおり漢字とひらがな入力に対応しています。
```src/components/pages/Home.tsx
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        setInputValue(event.target.value);
        setNoResults(false);
        search(event.target.value);
    }; //ユーザーの入力値を取得(例. 栄生（さこう）駅）

  
    const search = (input: string) => {
        if (input === "") {
            setSearchResults([]);
            setSelectedStation(null);
            setNoResults(false);
            return;
        } else {
            const searchedStations = allStations.filter(
                (station) =>
                (station.stationName !== undefined &&
                station.stationName !== null &&
                station.stationName.toUpperCase().includes(input.toUpperCase())) ||//漢字入力
                (station.stationNameKana !== undefined &&
                station.stationNameKana !== null &&
                station.stationNameKana.toUpperCase().includes(input.toUpperCase()))//ひらがな入力
            );
            //漢字とかな入力に対応したロジック
            setSearchResults(searchedStations);
            if (searchedStations.length === 0) {
                setNoResults(true);
            } else {
        }
    }
```

# 5. 乗換駅の取得
名鉄名古屋駅から一本の列車で辿り着けないような駅も存在します。その課題を解決するため乗換駅の案内表示機能も実装しました。駅をクリックすると、その駅に関連する乗換情報がAPIから取得されます。
取得した乗換情報はステートに保存され、紐づいた駅に基づいて表示されます。
```src/components/pages/Home.tsx
    useEffect(() => {
        if (selectedStation && changeStationData && changeStationData.data) {
            const match = changeStationData.data.find(stationData => stationData.id === selectedStation.id);
            setMatchingStation(match || null);
        } else {
            setMatchingStation(null);
        }
    }, [selectedStation, changeStationData]);
```
![IMG_7843.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726580/42ad782a-4797-6d5f-f4b1-1612567e5cc2.jpeg)
(乗換駅の表示)

# 6. レンダリング
駅の検索結果、選択された駅、乗り換えの案内など、状態に基づいて乗車位置情報を表示します。
各駅のデータに付与されているidを元に乗車位置表示のサインを生成しています。

```src/components/pages/Home.tsx
const forStations: string[] = ['岡崎・豊橋','鳴海・豊明','河和.内海.中部国際空港','大江・太田川','一宮・岐阜','須ヶ口・国府宮','津島・弥富','犬山・可児','西春・岩倉'];
    const forStationsEn: string[] = ['Okazaki Toyohashi','Narumi Toyoake','Kowa Utsumi Cen Japan Airport','Oe Otagawa','Ichinomiya Gifu','Sukaguchi Konomiya','Tsushima Yatomi','Inuyama Kani','Nishiharu Iwakura'];
    const trainClass: string[] = ['快特・特急・快急・急行・準急','普通'];
        const stationMapping: Record<string, StationMappingItem> = {
            '41': {forStations: forStations[0], forStationsEn: forStationsEn[0], trainClass: trainClass[0]},
            '42': {forStations: forStations[1], forStationsEn: forStationsEn[1], trainClass: trainClass[1]},
            '43': {forStations: forStations[2], forStationsEn: forStationsEn[2], trainClass: trainClass[0]},
            '44': {forStations: forStations[3], forStationsEn: forStationsEn[3], trainClass: trainClass[0]},
            '15': {forStations: forStations[4], forStationsEn: forStationsEn[4], trainClass: trainClass[0]},
            '16': {forStations: forStations[5], forStationsEn: forStationsEn[5], trainClass: trainClass[1]},
            '17': {forStations: forStations[6], forStationsEn: forStationsEn[6], trainClass: trainClass[0]},
            '18': {forStations: forStations[7], forStationsEn: forStationsEn[7], trainClass: trainClass[0]},
            '19': {forStations: forStations[8], forStationsEn: forStationsEn[8], trainClass: trainClass[1]}
        };
//idの先頭2桁の数字に応じて乗車位置の表示を変数stationMappingに格納する

```


```src/components/organisms/SearchResult.tsx
return (
        selectedStation &&
        <Box mb={5}>
            <Box mb={5} fontSize={['sm', 'md', 'lg']}>検索結果</Box>
            <Box fontSize={['sm', 'md', 'lg']}>
            <p>駅名: {selectedStation.stationName}({selectedStation.stationNameKana})駅</p>
            <p>路線名: {selectedStation.lineName}</p>
            <p>到着ホーム: {selectedStation.trackNum}番ホーム</p>
            </Box>
            <Box mt={5} fontSize={['sm', 'md', 'lg']}>下記表示の{selectedStation.positionColor}色乗車位置に並んでください。</Box>
            {(() => {
                const item = stationMapping[selectedStation.id.toString().substring(0, 2)];
                return (
                    <>
                        {renderStationInfo(item)}
                        <Box display="flex" justifyContent="center" alignItems="center">
                        </Box>
                        <Box m={5} display="flex" justifyContent="center" alignItems="center">
                            <Button size="sm" onClick={onClick}>検索結果をクリア</Button>
                        </Box>
                    </>
                );
            })()}
        </Box>
    );
}

```
本アプリケーションはデザインの部分にchakra-uiを用いていますが、特に乗車位置を示すサインはユーザーが分かりやすいよう現実のものを忠実に再現したかったためscssを書きました。
```src/components/pages/scssShowStations.module.scss
.stationInfoBase {
    text-align: center;
    width: 80vw;
    max-width: 300px;
    height: 80px;
    margin: 2rem;
    padding-right: 2rem;
    padding-bottom: 2rem;
    border: 1px solid #bbb;
    color: white;
    border-radius: 10px;
    box-shadow: 0 10px 25px 0 rgba(0, 0, 0, .5);

    @media (max-width: 768px) {
        margin: 1rem;
        padding-right: 1.5rem;
        padding-bottom: 1.5rem;
    }

    .title {
        font-size: 40px;

        @media (max-width: 768px) {
            font-size: 24px;
        }
    }

    .description {
        font-size: 10px;

        @media (max-width: 768px) {
            font-size: 8px;
        }
    }
}

.stationInfoBlue {
    @extend .stationInfoBase;
    background: radial-gradient(rgb(157, 157, 246),(rgb(0, 0, 255)));
}
.stationInfoLightBlue {
    @extend .stationInfoBase;
    background: radial-gradient(rgb(75, 255, 249),(rgb(0, 251, 255)));
}
.stationInfoPurple {
    @extend .stationInfoBase;
    background: radial-gradient(rgb(254, 48, 223),(rgb(255, 0, 170)));
}

.stationInfoGreen {
    @extend .stationInfoBase;
    background: radial-gradient(rgb(169, 246, 157),(rgb(2, 254, 65)));
}

.stationInfoYellow {
    @extend .stationInfoBase;
    background: radial-gradient(rgb(255, 251, 0),(rgba(237, 222, 57, 0.897)));
}
```
現実の乗車位置表示とアプリ上の乗車位置表示の比較です。現実のものは乗車位置にドア位置に応じた番号が書いてあります。本アプリケーションはそこまで再現ができていないのが現状ですが、現実に沿った形で表示ができるように今後も改良を進めて参ります。
![IMG_3137のコピー.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726580/65630681-aa96-1d33-0b91-bc3b0a005a31.jpeg)
(現実の乗車位置表示)
![IMG_6935のコピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726580/db1ede9c-e451-99ce-72ad-281d72b6e709.jpeg)
(アプリ上の乗車位置表示)

# 7. まとめ
　おおよその機能の流れは、"ユーザーが駅名を入力→一致した駅名の検索結果を表示→駅名を選択するとidに従い乗車位置表示を行う"というものです。
実装にあたってはChatGPT4.0の力も活用しています。（生成AIの賢さに驚いております。）
今後も名鉄名古屋駅で迷う方を助けるための機能を追加して、より便利なアプリケーションに育てていきたいです。
最後までお読みになってくださり、ありがとうございました。