From 3724eea08e62beb323a6abf256b9805746c04665 Mon Sep 17 00:00:00 2001
From: Tsubasa SEKIGUCHI <oss@tinykitten.me>
Date: Sun, 8 Feb 2026 05:14:25 +0000
Subject: [PATCH 1/3] =?UTF-8?q?GetStationsByLineGroupIdList=20RPC=E8=BF=BD?=
 =?UTF-8?q?=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 stationapi/proto                              |   2 +-
 .../domain/repository/station_repository.rs   |  19 +++
 .../src/infrastructure/station_repository.rs  | 114 ++++++++++++++++++
 .../src/presentation/controller/grpc.rs       |  31 ++++-
 stationapi/src/use_case/interactor/query.rs   |  33 +++++
 stationapi/src/use_case/traits/query.rs       |   5 +
 6 files changed, 202 insertions(+), 2 deletions(-)

diff --git a/stationapi/proto b/stationapi/proto
index b0508057..6f5474ca 160000
--- a/stationapi/proto
+++ b/stationapi/proto
@@ -1 +1 @@
-Subproject commit b050805770f4063d509b3acb5016f9464dc96417
+Subproject commit 6f5474cabcdb637bab5ce9417c578417fbb5f055
diff --git a/stationapi/src/domain/repository/station_repository.rs b/stationapi/src/domain/repository/station_repository.rs
index 9bb53d82..94a447a6 100644
--- a/stationapi/src/domain/repository/station_repository.rs
+++ b/stationapi/src/domain/repository/station_repository.rs
@@ -39,6 +39,10 @@ pub trait StationRepository: Send + Sync + 'static {
         transport_type: Option<TransportType>,
     ) -> Result<Vec<Station>, DomainError>;
     async fn get_by_line_group_id(&self, line_group_id: u32) -> Result<Vec<Station>, DomainError>;
+    async fn get_by_line_group_id_vec(
+        &self,
+        line_group_ids: &[u32],
+    ) -> Result<Vec<Station>, DomainError>;
     async fn get_route_stops(
         &self,
         from_station_id: u32,
@@ -219,6 +223,21 @@ mod tests {
             Ok(result)
         }
 
+        async fn get_by_line_group_id_vec(
+            &self,
+            line_group_ids: &[u32],
+        ) -> Result<Vec<Station>, DomainError> {
+            let result: Vec<Station> = self
+                .stations
+                .values()
+                .filter(|station| {
+                    line_group_ids.contains(&(station.line_group_cd.unwrap_or(0) as u32))
+                })
+                .cloned()
+                .collect();
+            Ok(result)
+        }
+
         async fn get_route_stops(
             &self,
             from_station_id: u32,
diff --git a/stationapi/src/infrastructure/station_repository.rs b/stationapi/src/infrastructure/station_repository.rs
index 0e111e88..d1615b68 100644
--- a/stationapi/src/infrastructure/station_repository.rs
+++ b/stationapi/src/infrastructure/station_repository.rs
@@ -272,6 +272,14 @@ impl StationRepository for MyStationRepository {
         InternalStationRepository::get_by_line_group_id(line_group_id, &mut conn).await
     }
 
+    async fn get_by_line_group_id_vec(
+        &self,
+        line_group_ids: &[u32],
+    ) -> Result<Vec<Station>, DomainError> {
+        let mut conn = self.pool.acquire().await?;
+        InternalStationRepository::get_by_line_group_id_vec(line_group_ids, &mut conn).await
+    }
+
     async fn get_route_stops(
         &self,
         from_station_id: u32,
@@ -1343,6 +1351,112 @@ impl InternalStationRepository {
         Ok(stations)
     }
 
+    async fn get_by_line_group_id_vec(
+        line_group_ids: &[u32],
+        conn: &mut PgConnection,
+    ) -> Result<Vec<Station>, DomainError> {
+        if line_group_ids.is_empty() {
+            return Ok(vec![]);
+        }
+
+        let params = (1..=line_group_ids.len())
+            .map(|i| format!("${i}"))
+            .collect::<Vec<_>>()
+            .join(", ");
+
+        let order_case = line_group_ids
+            .iter()
+            .enumerate()
+            .map(|(i, _)| format!("WHEN ${} THEN {}", i + 1 + line_group_ids.len(), i))
+            .collect::<Vec<_>>()
+            .join(" ");
+
+        let query_str = format!(
+            r#"SELECT
+            s.station_cd,
+            s.station_g_cd,
+            s.station_name,
+            s.station_name_k,
+            s.station_name_r,
+            s.station_name_rn,
+            s.station_name_zh,
+            s.station_name_ko,
+            s.station_number1,
+            s.station_number2,
+            s.station_number3,
+            s.station_number4,
+            s.three_letter_code,
+            s.line_cd,
+            s.pref_cd,
+            s.post,
+            s.address,
+            s.lon,
+            s.lat,
+            s.open_ymd,
+            s.close_ymd,
+            s.e_status,
+            s.e_sort,
+            COALESCE(NULLIF(COALESCE(a.line_name, l.line_name), ''), NULL) AS line_name,
+            COALESCE(NULLIF(COALESCE(a.line_name_k, l.line_name_k), ''), NULL) AS line_name_k,
+            COALESCE(NULLIF(COALESCE(a.line_name_h, l.line_name_h), ''), NULL) AS line_name_h,
+            COALESCE(NULLIF(COALESCE(a.line_name_r, l.line_name_r), ''), NULL) AS line_name_r,
+            COALESCE(NULLIF(COALESCE(a.line_name_zh, l.line_name_zh), ''), NULL) AS line_name_zh,
+            COALESCE(NULLIF(COALESCE(a.line_name_ko, l.line_name_ko), ''), NULL) AS line_name_ko,
+            COALESCE(NULLIF(COALESCE(a.line_color_c, l.line_color_c), ''), NULL) AS line_color_c,
+            l.company_cd,
+            l.line_type,
+            l.line_symbol1,
+            l.line_symbol2,
+            l.line_symbol3,
+            l.line_symbol4,
+            l.line_symbol1_color,
+            l.line_symbol2_color,
+            l.line_symbol3_color,
+            l.line_symbol4_color,
+            l.line_symbol1_shape,
+            l.line_symbol2_shape,
+            l.line_symbol3_shape,
+            l.line_symbol4_shape,
+            COALESCE(l.average_distance, 0.0)::DOUBLE PRECISION AS average_distance,
+            sst.id AS sst_id,
+            sst.type_cd,
+            sst.line_group_cd,
+            sst.pass,
+            t.id AS type_id,
+            t.type_name,
+            t.type_name_k,
+            t.type_name_r,
+            t.type_name_zh,
+            t.type_name_ko,
+            t.color,
+            t.direction,
+            t.kind,
+            s.transport_type
+          FROM stations AS s
+          JOIN lines AS l ON l.line_cd = s.line_cd AND l.e_status = 0
+          JOIN station_station_types AS sst ON sst.line_group_cd IN ( {params} ) 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.e_status = 0
+          ORDER BY CASE sst.line_group_cd {order_case} END, sst.id"#
+        );
+
+        let mut query = sqlx::query_as::<_, StationRow>(&query_str);
+        for id in line_group_ids {
+            query = query.bind(*id as i32);
+        }
+        for id in line_group_ids {
+            query = query.bind(*id as i32);
+        }
+
+        let rows = query.fetch_all(conn).await?;
+        let stations: Vec<Station> = rows.into_iter().map(|row| row.into()).collect();
+
+        Ok(stations)
+    }
+
     async fn get_route_stops(
         from_station_id: u32,
         to_station_id: u32,
diff --git a/stationapi/src/presentation/controller/grpc.rs b/stationapi/src/presentation/controller/grpc.rs
index 6716fc0c..846d5f86 100644
--- a/stationapi/src/presentation/controller/grpc.rs
+++ b/stationapi/src/presentation/controller/grpc.rs
@@ -9,7 +9,8 @@ use crate::{
         station_api_server::StationApi, GetConnectedStationsRequest, GetLineByIdListRequest,
         GetLineByIdRequest, GetLinesByNameRequest, GetRouteRequest, GetStationByCoordinatesRequest,
         GetStationByGroupIdRequest, GetStationByIdListRequest, GetStationByIdRequest,
-        GetStationByLineIdListRequest, GetStationByLineIdRequest, GetStationsByLineGroupIdRequest,
+        GetStationByLineIdListRequest, GetStationByLineIdRequest,
+        GetStationsByLineGroupIdListRequest, GetStationsByLineGroupIdRequest,
         GetStationsByNameRequest, GetTrainTypesByStationIdRequest, MultipleLineResponse,
         MultipleStationResponse, MultipleTrainTypeResponse, Route, RouteMinimalResponse,
         RouteResponse, RouteTypeResponse, SingleLineResponse, SingleStationResponse,
@@ -231,6 +232,26 @@ impl StationApi for MyApi {
         }
     }
 
+    async fn get_stations_by_line_group_id_list(
+        &self,
+        request: tonic::Request<GetStationsByLineGroupIdListRequest>,
+    ) -> Result<tonic::Response<MultipleStationResponse>, tonic::Status> {
+        let request_ref = request.get_ref();
+        let line_group_ids = &request_ref.line_group_ids;
+        let transport_type = convert_transport_type(request_ref.transport_type);
+
+        match self
+            .query_use_case
+            .get_stations_by_line_group_id_vec(line_group_ids, transport_type)
+            .await
+        {
+            Ok(stations) => Ok(Response::new(MultipleStationResponse {
+                stations: stations.into_iter().map(|station| station.into()).collect(),
+            })),
+            Err(err) => Err(PresentationalError::from(err).into()),
+        }
+    }
+
     async fn get_train_types_by_station_id(
         &self,
         request: tonic::Request<GetTrainTypesByStationIdRequest>,
@@ -765,6 +786,14 @@ mod tests {
             Ok(vec![])
         }
 
+        async fn get_stations_by_line_group_id_vec(
+            &self,
+            _line_group_ids: &[u32],
+            _transport_type: TransportTypeFilter,
+        ) -> Result<Vec<Station>, UseCaseError> {
+            Ok(vec![])
+        }
+
         async fn get_train_types_by_station_id(
             &self,
             _station_id: u32,
diff --git a/stationapi/src/use_case/interactor/query.rs b/stationapi/src/use_case/interactor/query.rs
index 3f57c6bf..f3d48acc 100644
--- a/stationapi/src/use_case/interactor/query.rs
+++ b/stationapi/src/use_case/interactor/query.rs
@@ -389,6 +389,27 @@ where
 
         Ok(stations)
     }
+    async fn get_stations_by_line_group_id_vec(
+        &self,
+        line_group_ids: &[u32],
+        transport_type: TransportTypeFilter,
+    ) -> Result<Vec<Station>, UseCaseError> {
+        let stations = self
+            .station_repository
+            .get_by_line_group_id_vec(line_group_ids)
+            .await?;
+
+        let stations: Vec<Station> = stations
+            .into_iter()
+            .filter(|s| matches_transport_filter(s.transport_type, transport_type))
+            .collect();
+
+        let stations = self
+            .update_station_vec_with_attributes(stations, None, transport_type)
+            .await?;
+
+        Ok(stations)
+    }
     fn get_station_numbers(&self, station: &Station) -> Vec<StationNumber> {
         let line_symbols_raw = [
             &station.line_symbol1,
@@ -1370,6 +1391,12 @@ mod tests {
             async fn get_by_line_group_id(&self, _: u32) -> Result<Vec<Station>, DomainError> {
                 Ok(vec![])
             }
+            async fn get_by_line_group_id_vec(
+                &self,
+                _: &[u32],
+            ) -> Result<Vec<Station>, DomainError> {
+                Ok(vec![])
+            }
             async fn get_route_stops(
                 &self,
                 _: u32,
@@ -1762,6 +1789,12 @@ mod tests {
             async fn get_by_line_group_id(&self, _: u32) -> Result<Vec<Station>, DomainError> {
                 Ok(vec![])
             }
+            async fn get_by_line_group_id_vec(
+                &self,
+                _: &[u32],
+            ) -> Result<Vec<Station>, DomainError> {
+                Ok(vec![])
+            }
             async fn get_route_stops(
                 &self,
                 _: u32,
diff --git a/stationapi/src/use_case/traits/query.rs b/stationapi/src/use_case/traits/query.rs
index 13c313c0..d134c7d4 100644
--- a/stationapi/src/use_case/traits/query.rs
+++ b/stationapi/src/use_case/traits/query.rs
@@ -82,6 +82,11 @@ pub trait QueryUseCase: Send + Sync + 'static {
         line_group_id: u32,
         transport_type: TransportTypeFilter,
     ) -> Result<Vec<Station>, UseCaseError>;
+    async fn get_stations_by_line_group_id_vec(
+        &self,
+        line_group_ids: &[u32],
+        transport_type: TransportTypeFilter,
+    ) -> Result<Vec<Station>, UseCaseError>;
     async fn get_train_types_by_station_id(
         &self,
         station_id: u32,

From 3ed762ee69b176b42ffd944e042fb934d8491b54 Mon Sep 17 00:00:00 2001
From: Tsubasa SEKIGUCHI <oss@tinykitten.me>
Date: Sun, 8 Feb 2026 05:20:23 +0000
Subject: [PATCH 2/3] submodule update

---
 stationapi/proto | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/stationapi/proto b/stationapi/proto
index 6f5474ca..7eede39e 160000
--- a/stationapi/proto
+++ b/stationapi/proto
@@ -1 +1 @@
-Subproject commit 6f5474cabcdb637bab5ce9417c578417fbb5f055
+Subproject commit 7eede39eb74fac9cde5e3ed0535cc1aff8b95580

From 05b0e7d8a8a57e93c3ab9ed22b96acac8c506adc Mon Sep 17 00:00:00 2001
From: Tsubasa SEKIGUCHI <oss@tinykitten.me>
Date: Sun, 8 Feb 2026 05:26:40 +0000
Subject: [PATCH 3/3] =?UTF-8?q?Mock=E3=81=AEget=5Fby=5Fline=5Fgroup=5Fid?=
 =?UTF-8?q?=5Fvec=E3=81=A7None=E3=81=8C=E3=83=9E=E3=83=83=E3=83=81?=
 =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 stationapi/src/domain/repository/station_repository.rs | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/stationapi/src/domain/repository/station_repository.rs b/stationapi/src/domain/repository/station_repository.rs
index 94a447a6..151c24a3 100644
--- a/stationapi/src/domain/repository/station_repository.rs
+++ b/stationapi/src/domain/repository/station_repository.rs
@@ -231,7 +231,10 @@ mod tests {
                 .stations
                 .values()
                 .filter(|station| {
-                    line_group_ids.contains(&(station.line_group_cd.unwrap_or(0) as u32))
+                    station
+                        .line_group_cd
+                        .map(|v| line_group_ids.contains(&(v as u32)))
+                        .unwrap_or(false)
                 })
                 .cloned()
                 .collect();
