diff --git a/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx b/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx
index b40150b..6cf8039 100644
--- a/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx
+++ b/client/src/app/features/top/components/heritage-detail/HeritageDetailLayout.tsx
@@ -191,7 +191,11 @@ export function HeritageDetailLayout({ item }: { item: WorldHeritageDetailVm })
{/* Left: Overview → Gallery */}
-
+ navigate(`/heritages/${item.id}/gallery`)}
+ />
{/* Right: Sidebar (PC only) */}
diff --git a/client/src/app/features/top/components/heritage-detail/gallery/HeritageGalleryPage.tsx b/client/src/app/features/top/components/heritage-detail/gallery/HeritageGalleryPage.tsx
new file mode 100644
index 0000000..1c5a201
--- /dev/null
+++ b/client/src/app/features/top/components/heritage-detail/gallery/HeritageGalleryPage.tsx
@@ -0,0 +1,65 @@
+import type { WorldHeritageImageVm } from "../../../../../../domain/types.ts";
+import { Lightbox } from "../Lightbox.tsx";
+
+export function HeritageGalleryPage({
+ title,
+ images,
+ lightboxIndex,
+ onSelectImage,
+ onCloseLightbox,
+ onNavigateLightbox,
+ onBack,
+}: {
+ title: string;
+ images: WorldHeritageImageVm[];
+ lightboxIndex: number | null;
+ onSelectImage: (index: number) => void;
+ onCloseLightbox: () => void;
+ onNavigateLightbox: (index: number) => void;
+ onBack: () => void;
+}) {
+ return (
+
+
+
+ Gallery
+ {images.length} photos
+
+
+ {images.map((img, index) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/client/src/app/features/top/containers/__tests__/heritage-gallery-container.test.tsx b/client/src/app/features/top/containers/__tests__/heritage-gallery-container.test.tsx
new file mode 100644
index 0000000..946c248
--- /dev/null
+++ b/client/src/app/features/top/containers/__tests__/heritage-gallery-container.test.tsx
@@ -0,0 +1,155 @@
+/** @jest-environment jsdom */
+
+import { render, screen, fireEvent } from "@testing-library/react";
+import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { HeritageGalleryContainer } from "../heritage-gallery-container";
+import { useWorldHeritageDetail } from "../../hooks/use-world-heritage-detail";
+import type { WorldHeritageVm } from "../../../../../domain/types";
+
+jest.mock("../../hooks/use-world-heritage-detail", () => ({
+ useWorldHeritageDetail: jest.fn(),
+}));
+
+const useWorldHeritageDetailMock = useWorldHeritageDetail as jest.MockedFunction<
+ (id: string | null | undefined) => {
+ item: WorldHeritageVm | null;
+ isLoading: boolean;
+ isError: boolean;
+ error: unknown;
+ reload: () => void;
+ }
+>;
+
+const buildVm = (overrides: Partial = {}): WorldHeritageVm => ({
+ id: 1,
+ officialName: "Official",
+ name: "Kyoto",
+ heritageNameJp: "京都",
+ country: "Japan",
+ countryNameJp: "日本",
+ region: "Asia",
+ stateParty: "Japan",
+ category: "Cultural",
+ criteria: [],
+ yearInscribed: 2000,
+ areaHectares: null,
+ bufferZoneHectares: null,
+ isEndangered: false,
+ latitude: null,
+ longitude: null,
+ shortDescription: "dummy",
+ shortDescriptionJp: null,
+ unescoSiteUrl: null,
+ statePartyCodes: [],
+ statePartiesMeta: {},
+ primaryStatePartyCode: null,
+ thumbnailUrl: null,
+ title: "Kyoto",
+ subtitle: "Japan · Asia",
+ displaySubName: null,
+ displayDescription: "dummy",
+ areaText: "—",
+ bufferText: "—",
+ criteriaText: "",
+ images: [
+ {
+ id: 1,
+ url: "https://example.com/1.jpg",
+ alt: "Photo 1",
+ credit: null,
+ width: 800,
+ height: 600,
+ isPrimary: true,
+ },
+ {
+ id: 2,
+ url: "https://example.com/2.jpg",
+ alt: "Photo 2",
+ credit: null,
+ width: 800,
+ height: 600,
+ isPrimary: false,
+ },
+ ],
+ ...overrides,
+});
+
+const renderWithRoute = (initialEntry: string) =>
+ render(
+
+
+ Index Page} />
+ Detail Page} />
+ } />
+
+ ,
+ );
+
+describe("HeritageGalleryContainer", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ useWorldHeritageDetailMock.mockReturnValue({
+ item: null,
+ isLoading: false,
+ isError: false,
+ error: null,
+ reload: jest.fn(),
+ });
+ });
+
+ test("読み込み中は Spinner を表示する", () => {
+ useWorldHeritageDetailMock.mockReturnValue({
+ item: null,
+ isLoading: true,
+ isError: false,
+ error: null,
+ reload: jest.fn(),
+ });
+
+ renderWithRoute("/heritages/1/gallery");
+ expect(screen.getByRole("status", { name: "Loading" })).toBeInTheDocument();
+ });
+
+ test("エラー時は ErrorPanel を表示する", () => {
+ useWorldHeritageDetailMock.mockReturnValue({
+ item: null,
+ isLoading: false,
+ isError: true,
+ error: new Error("boom"),
+ reload: jest.fn(),
+ });
+
+ renderWithRoute("/heritages/1/gallery");
+ expect(screen.getByText("Failed to load this site.")).toBeInTheDocument();
+ });
+
+ test("成功時には全件の画像を表示する", () => {
+ useWorldHeritageDetailMock.mockReturnValue({
+ item: buildVm(),
+ isLoading: false,
+ isError: false,
+ error: null,
+ reload: jest.fn(),
+ });
+
+ renderWithRoute("/heritages/1/gallery");
+
+ expect(screen.getByText("2 photos")).toBeInTheDocument();
+ expect(screen.getAllByRole("button", { name: /Open photo/ })).toHaveLength(2);
+ });
+
+ test("戻るボタンを押すと遺産詳細ページへ遷移する", () => {
+ useWorldHeritageDetailMock.mockReturnValue({
+ item: buildVm(),
+ isLoading: false,
+ isError: false,
+ error: null,
+ reload: jest.fn(),
+ });
+
+ renderWithRoute("/heritages/1/gallery");
+
+ fireEvent.click(screen.getByRole("button", { name: /Kyoto/ }));
+ expect(screen.getByText("Detail Page")).toBeInTheDocument();
+ });
+});
diff --git a/client/src/app/features/top/containers/heritage-gallery-container.tsx b/client/src/app/features/top/containers/heritage-gallery-container.tsx
new file mode 100644
index 0000000..c2e703f
--- /dev/null
+++ b/client/src/app/features/top/containers/heritage-gallery-container.tsx
@@ -0,0 +1,42 @@
+import { useEffect, useState } from "react";
+import { useParams, useNavigate } from "react-router-dom";
+import { useWorldHeritageDetail } from "../hooks/use-world-heritage-detail";
+import { HeritageGalleryPage } from "../components/heritage-detail/gallery/HeritageGalleryPage";
+import { Spinner } from "@shared/uis/Spinner.tsx";
+import { ErrorPanel } from "@shared/uis/ErrorPanel.tsx";
+
+export function HeritageGalleryContainer() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const [lightboxIndex, setLightboxIndex] = useState(null);
+
+ useEffect(() => {
+ if (!id) navigate("/heritages", { replace: true });
+ }, [id, navigate]);
+
+ const { item, reload, isLoading, isError } = useWorldHeritageDetail(id);
+
+ if (isLoading) return ;
+
+ if (isError) {
+ return (
+
+
+
+ );
+ }
+
+ if (!item) return null;
+
+ return (
+ setLightboxIndex(null)}
+ onNavigateLightbox={setLightboxIndex}
+ onBack={() => navigate(`/heritages/${item.id}`)}
+ />
+ );
+}
diff --git a/client/src/app/routes/AppRoutes.tsx b/client/src/app/routes/AppRoutes.tsx
index 16522c1..3ee64c7 100644
--- a/client/src/app/routes/AppRoutes.tsx
+++ b/client/src/app/routes/AppRoutes.tsx
@@ -2,6 +2,7 @@ import { Routes, Route, Navigate } from "react-router-dom";
import TopPageContainer from "@features/top/containers/top-page-container.tsx";
import { WorldHeritageDetailContainer } from "@features/top/containers/world-heritage-detail-container.tsx";
import { CriteriaDetailContainer } from "@features/top/containers/criteria-detail-container.tsx";
+import { HeritageGalleryContainer } from "@features/top/containers/heritage-gallery-container.tsx";
import { SearchHeritageResultsContainer } from "@features/search/containers/search-heritage-result-container.tsx";
import { BreadcrumbProvider } from "@features/breadcrumbs/BreadCrumbProvider.tsx";
import { LocaleProvider } from "@shared/locale/LocaleProvider.tsx";
@@ -16,6 +17,7 @@ export function AppRoutes() {
} />
} />
} />
+ } />
} />
} />