From 7be48864b5695ceda33de5b5e487b1bdce841fe5 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Mon, 5 Jan 2026 22:11:22 +0900 Subject: [PATCH 1/5] =?UTF-8?q?SQL=E3=81=AE=E8=87=B4=E5=91=BD=E7=9A=84?= =?UTF-8?q?=E3=81=AA=E3=83=90=E3=82=B0=E3=82=92=E3=81=84=E3=81=8F=E3=81=A4?= =?UTF-8?q?=E3=81=8B=E4=BF=AE=E6=AD=A3=20(#1371)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ひらがな混じりで駅検索ができないバグを修正 * get_train_types_by_station_idが動かないバグを修正 * GetStationsByLineGroupIdの修正 * ユニットテスト拡充 * cargo fmt --- ...35b00edeb2b60996e3e7333772ed9202f327.json} | 98 +++++------ ...05ca06ee06027d6a0e4bd07cf73e8554226b.json} | 4 +- stationapi/src/domain/normalize.rs | 69 ++++++++ .../src/infrastructure/line_repository.rs | 149 +++++++++++++++-- .../src/infrastructure/station_repository.rs | 155 ++++++++++++++++-- stationapi/src/use_case/interactor/query.rs | 2 +- 6 files changed, 403 insertions(+), 74 deletions(-) rename .sqlx/{query-d3f9188af86adfa1b16ea4ce538299c4e0942aeadc2497c7752b6b98fb78087f.json => query-25923b43426ac7b1829aaf1c726635b00edeb2b60996e3e7333772ed9202f327.json} (61%) rename .sqlx/{query-6a624360045e2278d6c1e08559918dcd026348e9304bba9584e72061034ac3db.json => query-269872b7f724f7361c016b696ddc05ca06ee06027d6a0e4bd07cf73e8554226b.json} (94%) diff --git a/.sqlx/query-d3f9188af86adfa1b16ea4ce538299c4e0942aeadc2497c7752b6b98fb78087f.json b/.sqlx/query-25923b43426ac7b1829aaf1c726635b00edeb2b60996e3e7333772ed9202f327.json similarity index 61% rename from .sqlx/query-d3f9188af86adfa1b16ea4ce538299c4e0942aeadc2497c7752b6b98fb78087f.json rename to .sqlx/query-25923b43426ac7b1829aaf1c726635b00edeb2b60996e3e7333772ed9202f327.json index 54d2fc59..f4914ee2 100644 --- a/.sqlx/query-d3f9188af86adfa1b16ea4ce538299c4e0942aeadc2497c7752b6b98fb78087f.json +++ b/.sqlx/query-25923b43426ac7b1829aaf1c726635b00edeb2b60996e3e7333772ed9202f327.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT l.line_cd,\n l.company_cd,\n l.line_type,\n l.line_symbol1,\n l.line_symbol2,\n l.line_symbol3,\n l.line_symbol4,\n l.line_symbol1_color,\n l.line_symbol2_color,\n l.line_symbol3_color,\n l.line_symbol4_color,\n l.line_symbol1_shape,\n l.line_symbol2_shape,\n l.line_symbol3_shape,\n l.line_symbol4_shape,\n l.e_status,\n l.e_sort,\n COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance,\n s.station_cd,\n s.station_g_cd,\n sst.line_group_cd,\n sst.type_cd,\n COALESCE(alias_data.line_name, l.line_name) AS line_name,\n COALESCE(alias_data.line_name_k, l.line_name_k) AS line_name_k,\n COALESCE(alias_data.line_name_h, l.line_name_h) AS line_name_h,\n COALESCE(alias_data.line_name_r, l.line_name_r) AS line_name_r,\n COALESCE(alias_data.line_name_zh, l.line_name_zh) AS line_name_zh,\n COALESCE(alias_data.line_name_ko, l.line_name_ko) AS line_name_ko,\n COALESCE(alias_data.line_color_c, l.line_color_c) AS line_color_c,\n l.transport_type\n FROM lines AS l\n JOIN stations AS s ON s.station_cd = $1\n LEFT JOIN station_station_types AS sst ON sst.station_cd = s.station_cd AND sst.pass <> 1\n LEFT JOIN (\n SELECT DISTINCT ON (la.station_cd)\n la.station_cd,\n a.line_name,\n a.line_name_k,\n a.line_name_h,\n a.line_name_r,\n a.line_name_zh,\n a.line_name_ko,\n a.line_color_c\n FROM line_aliases AS la\n JOIN aliases AS a ON la.alias_cd = a.id\n WHERE la.station_cd = $1\n LIMIT 1\n ) AS alias_data ON alias_data.station_cd = s.station_cd\n WHERE l.line_cd = s.line_cd", + "query": "SELECT l.line_cd,\n l.company_cd,\n l.line_type,\n COALESCE(alias_data.line_name, l.line_name) AS line_name,\n COALESCE(alias_data.line_name_k, l.line_name_k) AS line_name_k,\n COALESCE(alias_data.line_name_h, l.line_name_h) AS line_name_h,\n COALESCE(alias_data.line_name_r, l.line_name_r) AS line_name_r,\n COALESCE(alias_data.line_name_zh, l.line_name_zh) AS line_name_zh,\n COALESCE(alias_data.line_name_ko, l.line_name_ko) AS line_name_ko,\n COALESCE(alias_data.line_color_c, l.line_color_c) AS line_color_c,\n l.line_symbol1,\n l.line_symbol2,\n l.line_symbol3,\n l.line_symbol4,\n l.line_symbol1_color,\n l.line_symbol2_color,\n l.line_symbol3_color,\n l.line_symbol4_color,\n l.line_symbol1_shape,\n l.line_symbol2_shape,\n l.line_symbol3_shape,\n l.line_symbol4_shape,\n l.e_status,\n l.e_sort,\n COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance,\n sst.line_group_cd AS \"line_group_cd?\",\n s.station_cd,\n s.station_g_cd,\n sst.type_cd AS \"type_cd?\",\n l.transport_type\n FROM lines AS l\n JOIN stations AS s ON s.station_cd = $1\n LEFT JOIN station_station_types AS sst ON sst.station_cd = s.station_cd AND sst.pass <> 1\n LEFT JOIN (\n SELECT DISTINCT ON (la.station_cd)\n la.station_cd,\n a.line_name,\n a.line_name_k,\n a.line_name_h,\n a.line_name_r,\n a.line_name_zh,\n a.line_name_ko,\n a.line_color_c\n FROM line_aliases AS la\n JOIN aliases AS a ON la.alias_cd = a.id\n WHERE la.station_cd = $1\n LIMIT 1\n ) AS alias_data ON alias_data.station_cd = s.station_cd\n WHERE l.line_cd = s.line_cd", "describe": { "columns": [ { @@ -20,133 +20,133 @@ }, { "ordinal": 3, - "name": "line_symbol1", + "name": "line_name", "type_info": "Text" }, { "ordinal": 4, - "name": "line_symbol2", + "name": "line_name_k", "type_info": "Text" }, { "ordinal": 5, - "name": "line_symbol3", + "name": "line_name_h", "type_info": "Text" }, { "ordinal": 6, - "name": "line_symbol4", + "name": "line_name_r", "type_info": "Text" }, { "ordinal": 7, - "name": "line_symbol1_color", + "name": "line_name_zh", "type_info": "Text" }, { "ordinal": 8, - "name": "line_symbol2_color", + "name": "line_name_ko", "type_info": "Text" }, { "ordinal": 9, - "name": "line_symbol3_color", + "name": "line_color_c", "type_info": "Text" }, { "ordinal": 10, - "name": "line_symbol4_color", + "name": "line_symbol1", "type_info": "Text" }, { "ordinal": 11, - "name": "line_symbol1_shape", + "name": "line_symbol2", "type_info": "Text" }, { "ordinal": 12, - "name": "line_symbol2_shape", + "name": "line_symbol3", "type_info": "Text" }, { "ordinal": 13, - "name": "line_symbol3_shape", + "name": "line_symbol4", "type_info": "Text" }, { "ordinal": 14, - "name": "line_symbol4_shape", + "name": "line_symbol1_color", "type_info": "Text" }, { "ordinal": 15, - "name": "e_status", - "type_info": "Int4" + "name": "line_symbol2_color", + "type_info": "Text" }, { "ordinal": 16, - "name": "e_sort", - "type_info": "Int4" + "name": "line_symbol3_color", + "type_info": "Text" }, { "ordinal": 17, - "name": "average_distance", - "type_info": "Float8" + "name": "line_symbol4_color", + "type_info": "Text" }, { "ordinal": 18, - "name": "station_cd", - "type_info": "Int4" + "name": "line_symbol1_shape", + "type_info": "Text" }, { "ordinal": 19, - "name": "station_g_cd", - "type_info": "Int4" + "name": "line_symbol2_shape", + "type_info": "Text" }, { "ordinal": 20, - "name": "line_group_cd", - "type_info": "Int4" + "name": "line_symbol3_shape", + "type_info": "Text" }, { "ordinal": 21, - "name": "type_cd", - "type_info": "Int4" + "name": "line_symbol4_shape", + "type_info": "Text" }, { "ordinal": 22, - "name": "line_name", - "type_info": "Text" + "name": "e_status", + "type_info": "Int4" }, { "ordinal": 23, - "name": "line_name_k", - "type_info": "Text" + "name": "e_sort", + "type_info": "Int4" }, { "ordinal": 24, - "name": "line_name_h", - "type_info": "Text" + "name": "average_distance", + "type_info": "Float8" }, { "ordinal": 25, - "name": "line_name_r", - "type_info": "Text" + "name": "line_group_cd?", + "type_info": "Int4" }, { "ordinal": 26, - "name": "line_name_zh", - "type_info": "Text" + "name": "station_cd", + "type_info": "Int4" }, { "ordinal": 27, - "name": "line_name_ko", - "type_info": "Text" + "name": "station_g_cd", + "type_info": "Int4" }, { "ordinal": 28, - "name": "line_color_c", - "type_info": "Text" + "name": "type_cd?", + "type_info": "Int4" }, { "ordinal": 29, @@ -163,6 +163,13 @@ false, false, false, + null, + null, + null, + null, + null, + null, + null, true, true, true, @@ -182,15 +189,8 @@ false, false, false, - null, - null, - null, - null, - null, - null, - null, false ] }, - "hash": "d3f9188af86adfa1b16ea4ce538299c4e0942aeadc2497c7752b6b98fb78087f" + "hash": "25923b43426ac7b1829aaf1c726635b00edeb2b60996e3e7333772ed9202f327" } diff --git a/.sqlx/query-6a624360045e2278d6c1e08559918dcd026348e9304bba9584e72061034ac3db.json b/.sqlx/query-269872b7f724f7361c016b696ddc05ca06ee06027d6a0e4bd07cf73e8554226b.json similarity index 94% rename from .sqlx/query-6a624360045e2278d6c1e08559918dcd026348e9304bba9584e72061034ac3db.json rename to .sqlx/query-269872b7f724f7361c016b696ddc05ca06ee06027d6a0e4bd07cf73e8554226b.json index 7f837cee..b44f8430 100644 --- a/.sqlx/query-6a624360045e2278d6c1e08559918dcd026348e9304bba9584e72061034ac3db.json +++ b/.sqlx/query-269872b7f724f7361c016b696ddc05ca06ee06027d6a0e4bd07cf73e8554226b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n s.station_cd,\n s.station_g_cd,\n s.station_name,\n s.station_name_k,\n s.station_name_r,\n s.station_name_rn,\n s.station_name_zh,\n s.station_name_ko,\n s.station_number1,\n s.station_number2,\n s.station_number3,\n s.station_number4,\n s.three_letter_code,\n s.line_cd,\n s.pref_cd,\n s.post,\n s.address,\n s.lon,\n s.lat,\n s.open_ymd,\n s.close_ymd,\n s.e_status,\n s.e_sort,\n COALESCE(NULLIF(COALESCE(a.line_name, l.line_name), ''), NULL) AS line_name,\n COALESCE(NULLIF(COALESCE(a.line_name_k, l.line_name_k), ''), NULL) AS line_name_k,\n COALESCE(NULLIF(COALESCE(a.line_name_h, l.line_name_h), ''), NULL) AS line_name_h,\n COALESCE(NULLIF(COALESCE(a.line_name_r, l.line_name_r), ''), NULL) AS line_name_r,\n COALESCE(NULLIF(COALESCE(a.line_name_zh, l.line_name_zh), ''), NULL) AS line_name_zh,\n COALESCE(NULLIF(COALESCE(a.line_name_ko, l.line_name_ko), ''), NULL) AS line_name_ko,\n COALESCE(NULLIF(COALESCE(a.line_color_c, l.line_color_c), ''), NULL) AS line_color_c,\n l.company_cd,\n l.line_type,\n l.line_symbol1,\n l.line_symbol2,\n l.line_symbol3,\n l.line_symbol4,\n l.line_symbol1_color,\n l.line_symbol2_color,\n l.line_symbol3_color,\n l.line_symbol4_color,\n l.line_symbol1_shape,\n l.line_symbol2_shape,\n l.line_symbol3_shape,\n l.line_symbol4_shape,\n COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance,\n sst.id AS sst_id,\n sst.type_cd,\n sst.line_group_cd,\n sst.pass,\n t.id AS type_id,\n t.type_name,\n t.type_name_k,\n t.type_name_r,\n t.type_name_zh,\n t.type_name_ko,\n t.color,\n t.direction,\n t.kind,\n s.transport_type\n FROM stations AS s\n JOIN lines AS l ON l.line_cd = s.line_cd AND l.e_status = 0\n LEFT JOIN station_station_types AS sst ON sst.line_group_cd = $1\n LEFT JOIN types AS t ON t.type_cd = sst.type_cd\n LEFT JOIN line_aliases AS la ON la.station_cd = s.station_cd\n LEFT JOIN aliases AS a ON a.id = la.alias_cd\n WHERE\n s.line_cd = l.line_cd\n AND s.station_cd = sst.station_cd\n AND s.e_status = 0\n ORDER BY sst.id", + "query": "SELECT\n s.station_cd,\n s.station_g_cd,\n s.station_name,\n s.station_name_k,\n s.station_name_r,\n s.station_name_rn,\n s.station_name_zh,\n s.station_name_ko,\n s.station_number1,\n s.station_number2,\n s.station_number3,\n s.station_number4,\n s.three_letter_code,\n s.line_cd,\n s.pref_cd,\n s.post,\n s.address,\n s.lon,\n s.lat,\n s.open_ymd,\n s.close_ymd,\n s.e_status,\n s.e_sort,\n COALESCE(NULLIF(COALESCE(a.line_name, l.line_name), ''), NULL) AS line_name,\n COALESCE(NULLIF(COALESCE(a.line_name_k, l.line_name_k), ''), NULL) AS line_name_k,\n COALESCE(NULLIF(COALESCE(a.line_name_h, l.line_name_h), ''), NULL) AS line_name_h,\n COALESCE(NULLIF(COALESCE(a.line_name_r, l.line_name_r), ''), NULL) AS line_name_r,\n COALESCE(NULLIF(COALESCE(a.line_name_zh, l.line_name_zh), ''), NULL) AS line_name_zh,\n COALESCE(NULLIF(COALESCE(a.line_name_ko, l.line_name_ko), ''), NULL) AS line_name_ko,\n COALESCE(NULLIF(COALESCE(a.line_color_c, l.line_color_c), ''), NULL) AS line_color_c,\n l.company_cd,\n l.line_type,\n l.line_symbol1,\n l.line_symbol2,\n l.line_symbol3,\n l.line_symbol4,\n l.line_symbol1_color,\n l.line_symbol2_color,\n l.line_symbol3_color,\n l.line_symbol4_color,\n l.line_symbol1_shape,\n l.line_symbol2_shape,\n l.line_symbol3_shape,\n l.line_symbol4_shape,\n COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance,\n sst.id AS sst_id,\n sst.type_cd,\n sst.line_group_cd,\n sst.pass,\n t.id AS type_id,\n t.type_name,\n t.type_name_k,\n t.type_name_r,\n t.type_name_zh,\n t.type_name_ko,\n t.color,\n t.direction,\n t.kind,\n s.transport_type\n FROM stations AS s\n JOIN lines AS l ON l.line_cd = s.line_cd AND l.e_status = 0\n JOIN station_station_types AS sst ON sst.line_group_cd = $1 AND sst.station_cd = s.station_cd\n JOIN types AS t ON t.type_cd = sst.type_cd\n LEFT JOIN line_aliases AS la ON la.station_cd = s.station_cd\n LEFT JOIN aliases AS a ON a.id = la.alias_cd\n WHERE\n s.e_status = 0\n ORDER BY sst.id", "describe": { "columns": [ { @@ -366,5 +366,5 @@ false ] }, - "hash": "6a624360045e2278d6c1e08559918dcd026348e9304bba9584e72061034ac3db" + "hash": "269872b7f724f7361c016b696ddc05ca06ee06027d6a0e4bd07cf73e8554226b" } diff --git a/stationapi/src/domain/normalize.rs b/stationapi/src/domain/normalize.rs index 9eb6c831..c9c91c98 100644 --- a/stationapi/src/domain/normalize.rs +++ b/stationapi/src/domain/normalize.rs @@ -33,4 +33,73 @@ mod tests { // ひらがな・カタカナ混合のテスト assert_eq!(normalize_for_search("とうキョウ"), "トウキョウ"); } + + #[test] + fn test_normalize_hiragana_to_katakana() { + // 全ひらがな→カタカナ変換 + assert_eq!(normalize_for_search("しんじゅく"), "シンジュク"); + assert_eq!(normalize_for_search("おおさか"), "オオサカ"); + assert_eq!(normalize_for_search("きょうと"), "キョウト"); + // 小さいひらがな + assert_eq!(normalize_for_search("ぁぃぅぇぉ"), "ァィゥェォ"); + assert_eq!(normalize_for_search("っゃゅょ"), "ッャュョ"); + // ひらがなの範囲の端(ぁ〜ん) + assert_eq!(normalize_for_search("ぁ"), "ァ"); + assert_eq!(normalize_for_search("ん"), "ン"); + } + + #[test] + fn test_normalize_fullwidth_numbers() { + // 全角数字→半角数字変換 + assert_eq!(normalize_for_search("0123456789"), "0123456789"); + assert_eq!(normalize_for_search("東京123"), "東京123"); + } + + #[test] + fn test_normalize_fullwidth_alphabet() { + // 全角英字→半角英字変換 + assert_eq!(normalize_for_search("ABCDE"), "ABCDE"); + assert_eq!(normalize_for_search("abcde"), "abcde"); + assert_eq!(normalize_for_search("Z"), "Z"); + assert_eq!(normalize_for_search("z"), "z"); + } + + #[test] + fn test_normalize_mixed_input() { + // 漢字+ひらがな+全角数字の混合 + assert_eq!(normalize_for_search("東京駅1番線"), "東京駅1番線"); + // ひらがな+漢字+全角英字 + assert_eq!( + normalize_for_search("しんじゅく駅WEST"), + "シンジュク駅WEST" + ); + // 複合パターン + assert_eq!( + normalize_for_search("とうきょう123ABC"), + "トウキョウ123ABC" + ); + } + + #[test] + fn test_normalize_preserves_other_characters() { + // カタカナはそのまま + assert_eq!(normalize_for_search("トウキョウ"), "トウキョウ"); + // 漢字はそのまま + assert_eq!(normalize_for_search("東京"), "東京"); + // 半角英数字はそのまま + assert_eq!(normalize_for_search("Tokyo123"), "Tokyo123"); + // 記号はそのまま + assert_eq!(normalize_for_search("東京-品川"), "東京-品川"); + assert_eq!(normalize_for_search("(テスト)"), "(テスト)"); + } + + #[test] + fn test_normalize_station_name_search_patterns() { + // 実際の駅名検索で使われるパターン + assert_eq!(normalize_for_search("しながわ"), "シナガワ"); + assert_eq!(normalize_for_search("うえの"), "ウエノ"); + assert_eq!(normalize_for_search("あきはばら"), "アキハバラ"); + assert_eq!(normalize_for_search("いけぶくろ"), "イケブクロ"); + assert_eq!(normalize_for_search("しぶや"), "シブヤ"); + } } diff --git a/stationapi/src/infrastructure/line_repository.rs b/stationapi/src/infrastructure/line_repository.rs index b90dcc97..725eb42d 100644 --- a/stationapi/src/infrastructure/line_repository.rs +++ b/stationapi/src/infrastructure/line_repository.rs @@ -218,9 +218,16 @@ impl InternalLineRepository { let station_id = station_id as i32; let rows: Option = sqlx::query_as!( LineRow, - "SELECT l.line_cd, + r#"SELECT l.line_cd, l.company_cd, l.line_type, + COALESCE(alias_data.line_name, l.line_name) AS line_name, + COALESCE(alias_data.line_name_k, l.line_name_k) AS line_name_k, + COALESCE(alias_data.line_name_h, l.line_name_h) AS line_name_h, + COALESCE(alias_data.line_name_r, l.line_name_r) AS line_name_r, + COALESCE(alias_data.line_name_zh, l.line_name_zh) AS line_name_zh, + COALESCE(alias_data.line_name_ko, l.line_name_ko) AS line_name_ko, + COALESCE(alias_data.line_color_c, l.line_color_c) AS line_color_c, l.line_symbol1, l.line_symbol2, l.line_symbol3, @@ -236,17 +243,10 @@ impl InternalLineRepository { l.e_status, l.e_sort, COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance, + sst.line_group_cd AS "line_group_cd?", s.station_cd, s.station_g_cd, - sst.line_group_cd, - sst.type_cd, - COALESCE(alias_data.line_name, l.line_name) AS line_name, - COALESCE(alias_data.line_name_k, l.line_name_k) AS line_name_k, - COALESCE(alias_data.line_name_h, l.line_name_h) AS line_name_h, - COALESCE(alias_data.line_name_r, l.line_name_r) AS line_name_r, - COALESCE(alias_data.line_name_zh, l.line_name_zh) AS line_name_zh, - COALESCE(alias_data.line_name_ko, l.line_name_ko) AS line_name_ko, - COALESCE(alias_data.line_color_c, l.line_color_c) AS line_color_c, + sst.type_cd AS "type_cd?", l.transport_type FROM lines AS l JOIN stations AS s ON s.station_cd = $1 @@ -266,7 +266,7 @@ impl InternalLineRepository { WHERE la.station_cd = $1 LIMIT 1 ) AS alias_data ON alias_data.station_cd = s.station_cd - WHERE l.line_cd = s.line_cd", + WHERE l.line_cd = s.line_cd"#, station_id, ) .fetch_optional(conn) @@ -1522,4 +1522,131 @@ mod tests { cleanup_test_data(&repository.pool).await; } + + // ============================================ + // Tests for find_by_station_id optional fields + // (line_group_cd and type_cd are now optional) + // ============================================ + + #[tokio::test] + async fn test_line_row_with_optional_line_group_cd_none() { + // line_group_cdがNoneの場合(station_station_typesにマッチしない駅) + let line_row = LineRow { + line_cd: 1, + company_cd: 1, + line_type: Some(1), + line_name: Some("Test Line".to_string()), + line_name_k: Some("テストライン".to_string()), + line_name_h: Some("テストライン".to_string()), + line_name_r: Some("Test Line".to_string()), + line_name_zh: None, + line_name_ko: None, + line_color_c: Some("#FF0000".to_string()), + line_symbol1: None, + line_symbol2: None, + line_symbol3: None, + line_symbol4: None, + line_symbol1_color: None, + line_symbol2_color: None, + line_symbol3_color: None, + line_symbol4_color: None, + line_symbol1_shape: None, + line_symbol2_shape: None, + line_symbol3_shape: None, + line_symbol4_shape: None, + e_status: 0, + e_sort: 1, + average_distance: Some(1.5), + line_group_cd: None, // 重要: オプショナルでNone + station_cd: Some(101), + station_g_cd: Some(201), + type_cd: None, // 重要: オプショナルでNone + transport_type: Some(0), + }; + + let line: Line = line_row.into(); + + assert_eq!(line.line_cd, 1); + assert_eq!(line.station_cd, Some(101)); + assert_eq!(line.station_g_cd, Some(201)); + assert_eq!(line.line_group_cd, None); + assert_eq!(line.type_cd, None); + } + + #[tokio::test] + async fn test_line_row_with_optional_line_group_cd_some() { + // line_group_cdがSomeの場合(station_station_typesにマッチする駅) + let line_row = LineRow { + line_cd: 1, + company_cd: 1, + line_type: Some(1), + line_name: Some("Test Line".to_string()), + line_name_k: Some("テストライン".to_string()), + line_name_h: Some("テストライン".to_string()), + line_name_r: Some("Test Line".to_string()), + line_name_zh: None, + line_name_ko: None, + line_color_c: Some("#FF0000".to_string()), + line_symbol1: None, + line_symbol2: None, + line_symbol3: None, + line_symbol4: None, + line_symbol1_color: None, + line_symbol2_color: None, + line_symbol3_color: None, + line_symbol4_color: None, + line_symbol1_shape: None, + line_symbol2_shape: None, + line_symbol3_shape: None, + line_symbol4_shape: None, + e_status: 0, + e_sort: 1, + average_distance: Some(1.5), + line_group_cd: Some(301), // 重要: オプショナルでSome + station_cd: Some(101), + station_g_cd: Some(201), + type_cd: Some(1), // 重要: オプショナルでSome + transport_type: Some(0), + }; + + let line: Line = line_row.into(); + + assert_eq!(line.line_cd, 1); + assert_eq!(line.station_cd, Some(101)); + assert_eq!(line.station_g_cd, Some(201)); + assert_eq!(line.line_group_cd, Some(301)); + assert_eq!(line.type_cd, Some(1)); + } + + #[tokio::test] + #[cfg_attr(not(feature = "integration-tests"), ignore)] + async fn test_find_by_station_id_without_station_station_types() { + // station_station_typesにエントリがない駅でもfind_by_station_idが動作することを確認 + let pool = setup_test_db().await; + + // テストデータをセットアップ(station_station_typesにエントリがない駅を追加) + setup_test_data(&pool).await; + + // station_station_typesにエントリがない駅を追加 + sqlx::query( + "INSERT INTO stations (station_cd, station_g_cd, line_cd, e_status) VALUES (200, 300, 1, 0)" + ) + .execute(&pool) + .await + .unwrap(); + + let mut conn = pool.acquire().await.unwrap(); + let result = InternalLineRepository::find_by_station_id(200, &mut conn).await; + + assert!(result.is_ok()); + let line = result.unwrap(); + // LEFT JOINなので駅は見つかるが、line_group_cdとtype_cdはNone + if let Some(line) = line { + assert_eq!(line.station_cd, Some(200)); + assert_eq!(line.line_group_cd, None); + assert_eq!(line.type_cd, None); + } + + cleanup_test_data(&pool).await; + } } diff --git a/stationapi/src/infrastructure/station_repository.rs b/stationapi/src/infrastructure/station_repository.rs index 621fff40..14c4c385 100644 --- a/stationapi/src/infrastructure/station_repository.rs +++ b/stationapi/src/infrastructure/station_repository.rs @@ -6,6 +6,7 @@ use crate::{ domain::{ entity::{gtfs::TransportType, station::Station}, error::DomainError, + normalize::normalize_for_search, repository::station_repository::StationRepository, }, proto::StopCondition, @@ -1005,7 +1006,10 @@ impl InternalStationRepository { transport_type: Option, conn: &mut PgConnection, ) -> Result, DomainError> { - let station_name = &(format!("%{station_name}%")); + // 元の入力用パターン(漢字・その他) + let station_name_pattern = &(format!("%{station_name}%")); + // カタカナ検索用に正規化されたパターン(ひらがな→カタカナ変換) + let station_name_k_pattern = &(format!("%{}%", normalize_for_search(&station_name))); let limit = limit.map(|v| v as i64); let from_station_group_id = from_station_group_id.map(|id| id as i32); let transport_type_value: Option = transport_type.map(|t| t as i32); @@ -1127,11 +1131,11 @@ impl InternalStationRepository { ORDER BY station_g_cd, station_name LIMIT $7"#, from_station_group_id, - station_name, - station_name, - station_name, - station_name, - station_name, + station_name_pattern, + station_name_pattern, + station_name_k_pattern, // station_name_k用には正規化されたパターンを使用 + station_name_pattern, + station_name_pattern, limit, transport_type_value ) @@ -1211,14 +1215,12 @@ impl InternalStationRepository { s.transport_type FROM stations AS s JOIN lines AS l ON l.line_cd = s.line_cd AND l.e_status = 0 - LEFT JOIN station_station_types AS sst ON sst.line_group_cd = $1 - LEFT JOIN types AS t ON t.type_cd = sst.type_cd + JOIN station_station_types AS sst ON sst.line_group_cd = $1 AND sst.station_cd = s.station_cd + JOIN types AS t ON t.type_cd = sst.type_cd LEFT JOIN line_aliases AS la ON la.station_cd = s.station_cd LEFT JOIN aliases AS a ON a.id = la.alias_cd WHERE - s.line_cd = l.line_cd - AND s.station_cd = sst.station_cd - AND s.e_status = 0 + s.e_status = 0 ORDER BY sst.id"#, line_group_id as i32 ) @@ -1716,4 +1718,135 @@ mod tests { // This test would require proper database setup and test data // Skipped in regular test runs } + + // ============================================ + // Tests for get_by_name search pattern generation + // ============================================ + + mod get_by_name_tests { + use crate::domain::normalize::normalize_for_search; + + #[test] + fn test_search_pattern_generation() { + // station_name_pattern: 元の入力をそのまま使用 + let station_name = "しんじゅく"; + let station_name_pattern = format!("%{station_name}%"); + assert_eq!(station_name_pattern, "%しんじゅく%"); + + // station_name_k_pattern: ひらがな→カタカナ変換して使用 + let station_name_k_pattern = format!("%{}%", normalize_for_search(station_name)); + assert_eq!(station_name_k_pattern, "%シンジュク%"); + } + + #[test] + fn test_hiragana_search_converts_to_katakana_for_name_k() { + // ひらがな入力の場合、station_name_kはカタカナに変換される + let input = "とうきょう"; + let pattern_for_name_k = format!("%{}%", normalize_for_search(input)); + assert_eq!(pattern_for_name_k, "%トウキョウ%"); + } + + #[test] + fn test_katakana_search_remains_katakana() { + // カタカナ入力の場合、station_name_kもカタカナのまま + let input = "トウキョウ"; + let pattern_for_name_k = format!("%{}%", normalize_for_search(input)); + assert_eq!(pattern_for_name_k, "%トウキョウ%"); + } + + #[test] + fn test_kanji_search_remains_kanji_for_name() { + // 漢字入力の場合、station_nameは漢字のまま + let input = "東京"; + let pattern_for_name = format!("%{input}%"); + assert_eq!(pattern_for_name, "%東京%"); + + // station_name_kも漢字のまま(normalize_for_searchは漢字を変換しない) + let pattern_for_name_k = format!("%{}%", normalize_for_search(input)); + assert_eq!(pattern_for_name_k, "%東京%"); + } + + #[test] + fn test_mixed_hiragana_kanji_search() { + // ひらがな+漢字の混合入力 + let input = "しん宿"; + let pattern_for_name = format!("%{input}%"); + assert_eq!(pattern_for_name, "%しん宿%"); + + // station_name_k用: ひらがな部分だけカタカナに変換 + let pattern_for_name_k = format!("%{}%", normalize_for_search(input)); + assert_eq!(pattern_for_name_k, "%シン宿%"); + } + + #[test] + fn test_search_pattern_with_special_stations() { + // 実際の駅名での動作確認 + let test_cases = vec![ + ("しながわ", "%シナガワ%"), + ("うえの", "%ウエノ%"), + ("あきはばら", "%アキハバラ%"), + ("いけぶくろ", "%イケブクロ%"), + ("おおさか", "%オオサカ%"), + ]; + + for (input, expected_k_pattern) in test_cases { + let pattern_for_name_k = format!("%{}%", normalize_for_search(input)); + assert_eq!( + pattern_for_name_k, expected_k_pattern, + "Failed for input: {input}" + ); + } + } + } + + // ============================================ + // Tests for get_by_line_group_id SQL structure + // ============================================ + + mod get_by_line_group_id_tests { + #[test] + fn test_line_group_id_join_condition_structure() { + // JOINの条件が正しく構成されることを確認 + // 修正後: JOIN station_station_types AS sst ON sst.line_group_cd = $1 AND sst.station_cd = s.station_cd + let line_group_id = 1; + let join_condition = format!( + "sst.line_group_cd = {} AND sst.station_cd = s.station_cd", + line_group_id + ); + assert!(join_condition.contains("sst.line_group_cd = 1")); + assert!(join_condition.contains("sst.station_cd = s.station_cd")); + } + + #[test] + fn test_line_group_id_parameter_binding() { + // パラメータが正しくバインドされることを確認 + let line_group_id: u32 = 123; + let bound_value = line_group_id as i32; + assert_eq!(bound_value, 123); + } + } + + // ============================================ + // Database integration tests (require actual DB) + // ============================================ + + #[tokio::test] + #[ignore] // Requires actual database setup + async fn test_get_by_name_with_hiragana_search() { + // ひらがなで駅を検索できることを確認 + // 例: "しんじゅく" で "新宿" (station_name_k: "シンジュク") がヒットする + } + + #[tokio::test] + #[ignore] // Requires actual database setup + async fn test_get_by_line_group_id_returns_correct_stations() { + // line_group_idで指定した路線グループの駅のみが返されることを確認 + // 以前のバグ: JOINの条件が不完全で意図しない駅が返されていた + } + + #[tokio::test] + #[ignore] // Requires actual database setup + async fn test_get_by_line_group_id_station_order() { + // 返される駅がsst.idの順序でソートされていることを確認 + } } diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs index 57bdcdbe..67043a9e 100644 --- a/stationapi/src/use_case/interactor/query.rs +++ b/stationapi/src/use_case/interactor/query.rs @@ -202,7 +202,7 @@ where let stations = self .station_repository .get_by_name( - normalize_for_search(&station_name), + station_name, limit, from_station_group_id, filter_to_db_type(transport_type), From c231ef30e0aefeab3a8d7b39b00c3bbd36bd98a8 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Tue, 6 Jan 2026 18:35:39 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E3=83=90=E3=82=B9=E8=B7=AF=E7=B7=9A?= =?UTF-8?q?=E3=81=AFline=5Ftype=E3=82=92OtherLineType=E3=81=AB=E5=BC=B7?= =?UTF-8?q?=E5=88=B6=20(#1372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- stationapi/src/use_case/dto/line.rs | 332 +++++++++++++++++++++++++++- 1 file changed, 331 insertions(+), 1 deletion(-) diff --git a/stationapi/src/use_case/dto/line.rs b/stationapi/src/use_case/dto/line.rs index 16cbea73..9f18aeee 100644 --- a/stationapi/src/use_case/dto/line.rs +++ b/stationapi/src/use_case/dto/line.rs @@ -5,6 +5,14 @@ use crate::{ impl From for GrpcLine { fn from(line: Line) -> Self { + // バス路線の場合は line_type を OtherLineType (0) に強制 + // (鉄道用の line_type が誤って設定されている可能性があるため) + let line_type = if line.transport_type == TransportType::Bus { + 0 // OtherLineType + } else { + line.line_type.unwrap_or_default() + }; + Self { id: line.line_cd as u32, name_short: line.line_name, @@ -14,7 +22,7 @@ impl From for GrpcLine { name_chinese: line.line_name_zh, name_korean: line.line_name_ko, color: line.line_color_c.unwrap_or_default(), - line_type: line.line_type.unwrap_or_default(), + line_type, line_symbols: line.line_symbols.into_iter().map(|s| s.into()).collect(), status: line.e_status, station: line.station.map(|s| Box::new(s.into())), @@ -34,3 +42,325 @@ fn convert_transport_type(t: TransportType) -> i32 { TransportType::Bus => GrpcTransportType::Bus as i32, } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::proto::LineType as GrpcLineType; + + fn create_test_line(transport_type: TransportType, line_type: Option) -> Line { + Line { + line_cd: 1, + company_cd: 1, + company: None, + line_name: "テスト路線".to_string(), + line_name_k: "テストロセン".to_string(), + line_name_h: "てすとろせん".to_string(), + line_name_r: Some("Test Line".to_string()), + line_name_zh: Some("测试线路".to_string()), + line_name_ko: Some("테스트노선".to_string()), + line_color_c: Some("#FF0000".to_string()), + line_type, + line_symbols: vec![], + line_symbol1: None, + line_symbol2: None, + line_symbol3: None, + line_symbol4: None, + line_symbol1_color: None, + line_symbol2_color: None, + line_symbol3_color: None, + line_symbol4_color: None, + line_symbol1_shape: None, + line_symbol2_shape: None, + line_symbol3_shape: None, + line_symbol4_shape: None, + e_status: 0, + e_sort: 1, + station: None, + train_type: None, + line_group_cd: None, + station_cd: None, + station_g_cd: None, + average_distance: Some(1.5), + type_cd: None, + transport_type, + } + } + + // ============================================ + // バス路線の line_type 変換テスト + // ============================================ + + #[test] + fn test_bus_line_with_subway_line_type_returns_other() { + // バス路線にSubway(3)が設定されていても、OtherLineType(0)が返される + let bus_line = create_test_line(TransportType::Bus, Some(GrpcLineType::Subway as i32)); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + assert_eq!(grpc_line.transport_type, GrpcTransportType::Bus as i32); + } + + #[test] + fn test_bus_line_with_bullet_train_line_type_returns_other() { + // バス路線にBulletTrain(1)が設定されていても、OtherLineType(0)が返される + let bus_line = create_test_line(TransportType::Bus, Some(GrpcLineType::BulletTrain as i32)); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + #[test] + fn test_bus_line_with_normal_line_type_returns_other() { + // バス路線にNormal(2)が設定されていても、OtherLineType(0)が返される + let bus_line = create_test_line(TransportType::Bus, Some(GrpcLineType::Normal as i32)); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + #[test] + fn test_bus_line_with_tram_line_type_returns_other() { + // バス路線にTram(4)が設定されていても、OtherLineType(0)が返される + let bus_line = create_test_line(TransportType::Bus, Some(GrpcLineType::Tram as i32)); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + #[test] + fn test_bus_line_with_monorail_line_type_returns_other() { + // バス路線にMonorailOrAGT(5)が設定されていても、OtherLineType(0)が返される + let bus_line = + create_test_line(TransportType::Bus, Some(GrpcLineType::MonorailOrAgt as i32)); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + #[test] + fn test_bus_line_with_none_line_type_returns_other() { + // バス路線でline_typeがNoneでも、OtherLineType(0)が返される + let bus_line = create_test_line(TransportType::Bus, None); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + #[test] + fn test_bus_line_with_other_line_type_returns_other() { + // バス路線でOtherLineType(0)が設定されていれば、そのままOtherLineType(0)が返される + let bus_line = + create_test_line(TransportType::Bus, Some(GrpcLineType::OtherLineType as i32)); + let grpc_line: GrpcLine = bus_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + // ============================================ + // 鉄道路線の line_type 変換テスト + // ============================================ + + #[test] + fn test_rail_line_with_subway_line_type_preserved() { + // 鉄道路線ではSubway(3)がそのまま返される + let rail_line = create_test_line(TransportType::Rail, Some(GrpcLineType::Subway as i32)); + let grpc_line: GrpcLine = rail_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::Subway as i32); + assert_eq!(grpc_line.transport_type, GrpcTransportType::Rail as i32); + } + + #[test] + fn test_rail_line_with_bullet_train_line_type_preserved() { + // 鉄道路線ではBulletTrain(1)がそのまま返される + let rail_line = + create_test_line(TransportType::Rail, Some(GrpcLineType::BulletTrain as i32)); + let grpc_line: GrpcLine = rail_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::BulletTrain as i32); + } + + #[test] + fn test_rail_line_with_normal_line_type_preserved() { + // 鉄道路線ではNormal(2)がそのまま返される + let rail_line = create_test_line(TransportType::Rail, Some(GrpcLineType::Normal as i32)); + let grpc_line: GrpcLine = rail_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::Normal as i32); + } + + #[test] + fn test_rail_line_with_tram_line_type_preserved() { + // 鉄道路線ではTram(4)がそのまま返される + let rail_line = create_test_line(TransportType::Rail, Some(GrpcLineType::Tram as i32)); + let grpc_line: GrpcLine = rail_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::Tram as i32); + } + + #[test] + fn test_rail_line_with_monorail_line_type_preserved() { + // 鉄道路線ではMonorailOrAGT(5)がそのまま返される + let rail_line = create_test_line( + TransportType::Rail, + Some(GrpcLineType::MonorailOrAgt as i32), + ); + let grpc_line: GrpcLine = rail_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::MonorailOrAgt as i32); + } + + #[test] + fn test_rail_line_with_none_line_type_defaults_to_other() { + // 鉄道路線でline_typeがNoneの場合、OtherLineType(0)がデフォルトで返される + let rail_line = create_test_line(TransportType::Rail, None); + let grpc_line: GrpcLine = rail_line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + // ============================================ + // convert_transport_type 関数のテスト + // ============================================ + + #[test] + fn test_convert_transport_type_rail() { + let result = convert_transport_type(TransportType::Rail); + assert_eq!(result, GrpcTransportType::Rail as i32); + } + + #[test] + fn test_convert_transport_type_bus() { + let result = convert_transport_type(TransportType::Bus); + assert_eq!(result, GrpcTransportType::Bus as i32); + } + + // ============================================ + // Line から GrpcLine への変換テスト (その他フィールド) + // ============================================ + + #[test] + fn test_line_to_grpc_line_basic_fields() { + let line = create_test_line(TransportType::Rail, Some(GrpcLineType::Normal as i32)); + let grpc_line: GrpcLine = line.into(); + + assert_eq!(grpc_line.id, 1); + assert_eq!(grpc_line.name_short, "テスト路線"); + assert_eq!(grpc_line.name_katakana, "テストロセン"); + assert_eq!(grpc_line.name_full, "てすとろせん"); + assert_eq!(grpc_line.name_roman, Some("Test Line".to_string())); + assert_eq!(grpc_line.name_chinese, Some("测试线路".to_string())); + assert_eq!(grpc_line.name_korean, Some("테스트노선".to_string())); + assert_eq!(grpc_line.color, "#FF0000"); + assert_eq!(grpc_line.status, 0); + assert!((grpc_line.average_distance - 1.5).abs() < f64::EPSILON); + } + + #[test] + fn test_line_to_grpc_line_with_none_optional_fields() { + let mut line = create_test_line(TransportType::Rail, None); + line.line_name_r = None; + line.line_name_zh = None; + line.line_name_ko = None; + line.line_color_c = None; + line.average_distance = None; + + let grpc_line: GrpcLine = line.into(); + + // name_romanはNoneでもSome("")になる + assert_eq!(grpc_line.name_roman, Some("".to_string())); + assert_eq!(grpc_line.name_chinese, None); + assert_eq!(grpc_line.name_korean, None); + // colorはNoneでも""になる + assert_eq!(grpc_line.color, ""); + // average_distanceはNoneでも0.0になる + assert!((grpc_line.average_distance - 0.0).abs() < f64::EPSILON); + } + + #[test] + fn test_line_to_grpc_line_empty_line_symbols() { + let line = create_test_line(TransportType::Rail, None); + let grpc_line: GrpcLine = line.into(); + + assert!(grpc_line.line_symbols.is_empty()); + } + + #[test] + fn test_line_to_grpc_line_without_station() { + let line = create_test_line(TransportType::Rail, None); + let grpc_line: GrpcLine = line.into(); + + assert!(grpc_line.station.is_none()); + } + + #[test] + fn test_line_to_grpc_line_without_company() { + let line = create_test_line(TransportType::Rail, None); + let grpc_line: GrpcLine = line.into(); + + assert!(grpc_line.company.is_none()); + } + + #[test] + fn test_line_to_grpc_line_without_train_type() { + let line = create_test_line(TransportType::Rail, None); + let grpc_line: GrpcLine = line.into(); + + assert!(grpc_line.train_type.is_none()); + } + + // ============================================ + // 境界値・エッジケースのテスト + // ============================================ + + #[test] + fn test_line_type_boundary_values() { + // line_typeの境界値テスト + let test_cases = vec![ + (0, GrpcLineType::OtherLineType as i32), + (1, GrpcLineType::BulletTrain as i32), + (2, GrpcLineType::Normal as i32), + (3, GrpcLineType::Subway as i32), + (4, GrpcLineType::Tram as i32), + (5, GrpcLineType::MonorailOrAgt as i32), + ]; + + for (input, expected) in test_cases { + let line = create_test_line(TransportType::Rail, Some(input)); + let grpc_line: GrpcLine = line.into(); + assert_eq!( + grpc_line.line_type, expected, + "Failed for line_type: {input}" + ); + } + } + + #[test] + fn test_line_type_unknown_value_preserved_for_rail() { + // 鉄道路線で未知のline_type値は保持される + let line = create_test_line(TransportType::Rail, Some(99)); + let grpc_line: GrpcLine = line.into(); + + assert_eq!(grpc_line.line_type, 99); + } + + #[test] + fn test_bus_line_unknown_line_type_returns_other() { + // バス路線では未知のline_type値もOtherLineTypeになる + let line = create_test_line(TransportType::Bus, Some(99)); + let grpc_line: GrpcLine = line.into(); + + assert_eq!(grpc_line.line_type, GrpcLineType::OtherLineType as i32); + } + + #[test] + fn test_line_cd_to_id_conversion() { + // line_cdがu32に正しく変換されることを確認 + let mut line = create_test_line(TransportType::Rail, None); + line.line_cd = 12345; + let grpc_line: GrpcLine = line.into(); + + assert_eq!(grpc_line.id, 12345); + } +} From 1094d30f1a59d95542d1c334728bd7413557d2f4 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Wed, 7 Jan 2026 21:58:42 +0900 Subject: [PATCH 3/5] =?UTF-8?q?direction=5Fid=E3=81=8CNULL=E3=81=AE?= =?UTF-8?q?=E3=83=88=E3=83=AA=E3=83=83=E3=83=97=E3=81=AB=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=90=E3=82=B9=E5=81=9C=E3=82=82=E6=AD=A3?= =?UTF-8?q?=E3=81=97=E3=81=8F=E5=8F=96=E3=82=8A=E8=BE=BC=E3=82=80=E3=82=88?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3=20(#1373)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * direction_idがNULLのトリップに存在するバス停も正しく取り込むよう修正 GTFSの仕様ではdirection_idはオプショナルであり、NULLの場合でも バス停は有効な停留所として扱うべきである。 これまではvariant_only_with_neighbors CTEで「direction_id IS NOT NULL」 のトリップにのみ存在するバス停をフィルタリングしていたため、 早81の原宿駅前や渋谷駅東口などのバス停がレスポンスから除外されていた。 * バリアントバス停の位置推定でメイントリップに存在する隣接バス停を優先 variant_only_with_neighborsでバス停の隣接情報を選択する際に、 隣接バス停(prev_stop_id/next_stop_id)がメイントリップに存在する レコードを優先するよう修正。 これにより、原宿駅前などのバス停がより正確な位置に挿入される。 --------- Co-authored-by: Claude --- stationapi/src/import.rs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/stationapi/src/import.rs b/stationapi/src/import.rs index 6f4f2e77..1b991878 100644 --- a/stationapi/src/import.rs +++ b/stationapi/src/import.rs @@ -1636,7 +1636,7 @@ async fn build_stop_route_mapping( ) ), -- Find variant-only stops (not on main trip) with their neighbor info - -- Exclude stops that ONLY appear on NULL direction_id trips (loop routes) + -- Prioritize records where neighbors exist on main trip for better position estimation variant_only_with_neighbors AS ( SELECT DISTINCT ON (vts.parent_stop_id, vts.route_id) vts.parent_stop_id, @@ -1645,21 +1645,25 @@ async fn build_stop_route_mapping( vts.prev_stop_id, vts.next_stop_id FROM variant_trip_stops_with_neighbors vts + LEFT JOIN main_trip_stops mts_prev + ON vts.prev_stop_id = mts_prev.parent_stop_id + AND vts.route_id = mts_prev.route_id + LEFT JOIN main_trip_stops mts_next + ON vts.next_stop_id = mts_next.parent_stop_id + AND vts.route_id = mts_next.route_id WHERE NOT EXISTS ( SELECT 1 FROM main_trip_stops mts WHERE mts.parent_stop_id = vts.parent_stop_id AND mts.route_id = vts.route_id ) - -- Only include stops that appear on at least one non-NULL direction_id trip - AND EXISTS ( - SELECT 1 FROM gtfs_trips gt2 - JOIN gtfs_stop_times gst2 ON gt2.trip_id = gst2.trip_id - JOIN gtfs_stops gs2 ON gst2.stop_id = gs2.stop_id - WHERE gt2.route_id = vts.route_id - AND COALESCE(gs2.parent_station, gs2.stop_id) = vts.parent_stop_id - AND gt2.direction_id IS NOT NULL - ) - ORDER BY vts.parent_stop_id, vts.route_id, vts.stop_sequence + ORDER BY vts.parent_stop_id, vts.route_id, + -- Prioritize records where neighbors exist on main trip + CASE + WHEN mts_prev.parent_stop_id IS NOT NULL AND mts_next.parent_stop_id IS NOT NULL THEN 0 + WHEN mts_prev.parent_stop_id IS NOT NULL OR mts_next.parent_stop_id IS NOT NULL THEN 1 + ELSE 2 + END, + vts.stop_sequence ), -- Recursive CTE to find the nearest main-trip stop by following prev chain prev_chain AS ( From 5b51e728eac37fdd592d3c43a72c67818386194f Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Sun, 11 Jan 2026 07:30:26 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E8=A5=BF=E9=89=84=E3=83=87=E3=83=BC?= =?UTF-8?q?=E3=82=BF=E6=9B=B4=E6=96=B0=20(#1375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 春日原駅を特急停車駅に指定 * 聖マリア病院前の駅名修正 --- data/3!stations.csv | 2 +- data/5!station_station_types.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/3!stations.csv b/data/3!stations.csv index 42a0b25d..d0e58a9b 100644 --- a/data/3!stations.csv +++ b/data/3!stations.csv @@ -7292,7 +7292,7 @@ station_cd,station_g_cd,station_name,station_name_k,station_name_r,station_name_ 3600124,3600124,櫛原,クシワラ,Kushiwara,Kushiwara,栉原,구시와라,26,,,,,36001,40,830-0013,福岡県久留米市櫛原町原口1516-4,130.524319,33.319592,0000-00-00,0000-00-00,0,3600124 3600125,3600125,西鉄久留米,ニシテツクルメ,Nishitetsu-Kurume,Nishitetsu-Kurume,西铁久留米,니시테쓰쿠루메,27,,,,,36001,40,830-0032,福岡県久留米市東町309-2,130.521053,33.312276,0000-00-00,0000-00-00,0,3600125 3600126,3600126,花畑,ハナバタケ,Hanabatake,Hanabatake,花畑,하나바타케,28,,,,,36001,40,830-0038,福岡県久留米市西町968,130.515972,33.306643,0000-00-00,0000-00-00,0,3600126 -3600127,3600127,聖マリア病院前,セイマリアビョウインマエ,Shikenjo-Mae,Shikenjo-Mae,试验场前,시켄조마에,29,,,,,36001,40,830-0047,福岡県久留米市津福本町235,130.509923,33.301985,0000-00-00,0000-00-00,0,3600127 +3600127,3600127,聖マリア病院前,セイマリアビョウインマエ,St. Mary's Hospital,St. Mary's Hospital,圣玛丽病院前,성마리아병원마에,29,,,,,36001,40,830-0047,福岡県久留米市津福本町235,130.509923,33.301985,0000-00-00,0000-00-00,0,3600127 3600128,3600128,津福,ツブク,Tsubuku,Tsubuku,津福,쓰부쿠,30,,,,,36001,40,830-0047,福岡県久留米市津福本町1587,130.497788,33.296505,0000-00-00,0000-00-00,0,3600128 3600129,3600129,安武,ヤスタケ,Yasutake,Yasutake,安武,야스타케,31,,,,,36001,40,830-0072,福岡県久留米市安武町安武本3327,130.489061,33.286311,0000-00-00,0000-00-00,0,3600129 3600130,3600130,大善寺,ダイゼンジ,Daisenji,Daisenji,大善寺,다이젠지,32,,,,,36001,40,830-0073,福岡県久留米市大善寺町宮本1173,130.473897,33.270432,0000-00-00,0000-00-00,0,3600130 diff --git a/data/5!station_station_types.csv b/data/5!station_station_types.csv index 4a6f16b4..187b871b 100644 --- a/data/5!station_station_types.csv +++ b/data/5!station_station_types.csv @@ -10058,7 +10058,7 @@ DEFAULT,3600105,306,267,0,大橋 DEFAULT,3600106,306,267,1,井尻 DEFAULT,3600107,306,267,1,雑餉隈 DEFAULT,3600150,306,267,1,桜並木 -DEFAULT,3600108,306,267,1,春日原 +DEFAULT,3600108,306,267,0,春日原 DEFAULT,3600109,306,267,1,白木原 DEFAULT,3600110,306,267,1,下大利 DEFAULT,3600111,306,267,1,都府楼前 From 7b89c15c0755956cefbcf89c17e269f3e0b7fc9f Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Fri, 16 Jan 2026 00:54:47 +0900 Subject: [PATCH 5/5] Merge pull request #1379 from TrainLCD/fix/nearby-stations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RailAndBusが指定された場合はRailを先頭にソート --- stationapi/src/presentation/controller/grpc.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/stationapi/src/presentation/controller/grpc.rs b/stationapi/src/presentation/controller/grpc.rs index 77ba0a27..c6582105 100644 --- a/stationapi/src/presentation/controller/grpc.rs +++ b/stationapi/src/presentation/controller/grpc.rs @@ -1,5 +1,5 @@ use crate::{ - domain::entity::gtfs::TransportTypeFilter, + domain::entity::gtfs::{TransportType, TransportTypeFilter}, infrastructure::{ company_repository::MyCompanyRepository, line_repository::MyLineRepository, station_repository::MyStationRepository, train_type_repository::MyTrainTypeRepository, @@ -123,7 +123,7 @@ impl StationApi for MyApi { let longitude = request_ref.longitude; let limit = request_ref.limit; let transport_type = convert_transport_type(request_ref.transport_type); - let stations = match self + let mut stations = match self .query_use_case .get_stations_by_coordinates(latitude, longitude, limit, transport_type) .await @@ -132,6 +132,15 @@ impl StationApi for MyApi { Err(err) => return Err(PresentationalError::from(err).into()), }; + // RailAndBusが指定された場合は、Railを先頭にソート(stable sortで距離順を維持) + if transport_type == TransportTypeFilter::RailAndBus { + stations.sort_by(|a, b| match (a.transport_type, b.transport_type) { + (TransportType::Rail, TransportType::Bus) => std::cmp::Ordering::Less, + (TransportType::Bus, TransportType::Rail) => std::cmp::Ordering::Greater, + _ => std::cmp::Ordering::Equal, + }); + } + Ok(tonic::Response::new(MultipleStationResponse { stations: stations.into_iter().map(|station| station.into()).collect(), }))