Repository: ult-sohtome/shopping-site
Pull request #1: 商品一覧ページの作成
Author: login: ult-sohtome
Created: 2025-05-02T04:30:10Z | Merged: 2025-05-09T05:57:57Z

## 変更点
- Fake Store API から商品一覧を取得し、カード形式で表示する
- 商品には「詳細を見る」ボタンを設置し、詳細ページへ遷移できるようにする
- 通信中はローディング表示、失敗時はエラーメッセージを表示する
- データ取得処理は必ずインターフェース経由で行うこと

## 残務
- 「詳細を見る」ボタンから遷移した詳細ページの作成


Diff:
diff --git a/src/App.vue b/src/App.vue
index 7905b05..86f2ed5 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,18 +1,12 @@
 <script setup lang="ts">
 import { RouterLink, RouterView } from 'vue-router'
-import HelloWorld from './components/HelloWorld.vue'
 </script>
 
 <template>
   <header>
-    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
-
     <div class="wrapper">
-      <HelloWorld msg="You did it!" />
-
       <nav>
-        <RouterLink to="/">Home</RouterLink>
-        <RouterLink to="/about">About</RouterLink>
+        <RouterLink to="/">商品一覧</RouterLink>
       </nav>
     </div>
   </header>
@@ -22,17 +16,12 @@ import HelloWorld from './components/HelloWorld.vue'
 
 <style scoped>
 header {
-  line-height: 1.5;
-  max-height: 100vh;
-}
-
-.logo {
-  display: block;
-  margin: 0 auto 2rem;
+  justify-content: center;
+  place-items: center;
 }
 
 nav {
-  width: 100%;
+  display: flex;
   font-size: 12px;
   text-align: center;
   margin-top: 2rem;
@@ -47,7 +36,6 @@ nav a.router-link-exact-active:hover {
 }
 
 nav a {
-  display: inline-block;
   padding: 0 1rem;
   border-left: 1px solid var(--color-border);
 }
@@ -59,23 +47,11 @@ nav a:first-of-type {
 @media (min-width: 1024px) {
   header {
     display: flex;
-    place-items: center;
     padding-right: calc(var(--section-gap) / 2);
   }
 
-  .logo {
-    margin: 0 2rem 0 0;
-  }
-
-  header .wrapper {
-    display: flex;
-    place-items: flex-start;
-    flex-wrap: wrap;
-  }
-
   nav {
     text-align: left;
-    margin-left: -1rem;
     font-size: 1rem;
 
     padding: 1rem 0;
diff --git a/src/assets/main.css b/src/assets/main.css
index 36fb845..f754498 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -23,13 +23,11 @@ a,
 
 @media (min-width: 1024px) {
   body {
-    display: flex;
-    place-items: center;
+    display: block;
   }
 
   #app {
-    display: grid;
-    grid-template-columns: 1fr 1fr;
+    display: block;
     padding: 0 2rem;
   }
 }
diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue
deleted file mode 100644
index d174cf8..0000000
--- a/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script setup lang="ts">
-defineProps<{
-  msg: string
-}>()
-</script>
-
-<template>
-  <div class="greetings">
-    <h1 class="green">{{ msg }}</h1>
-    <h3>
-      You’ve successfully created a project with
-      <a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
-      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
-    </h3>
-  </div>
-</template>
-
-<style scoped>
-h1 {
-  font-weight: 500;
-  font-size: 2.6rem;
-  position: relative;
-  top: -10px;
-}
-
-h3 {
-  font-size: 1.2rem;
-}
-
-.greetings h1,
-.greetings h3 {
-  text-align: center;
-}
-
-@media (min-width: 1024px) {
-  .greetings h1,
-  .greetings h3 {
-    text-align: left;
-  }
-}
-</style>
diff --git a/src/components/TheWelcome.vue b/src/components/TheWelcome.vue
deleted file mode 100644
index 8b731d9..0000000
--- a/src/components/TheWelcome.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script setup lang="ts">
-import WelcomeItem from './WelcomeItem.vue'
-import DocumentationIcon from './icons/IconDocumentation.vue'
-import ToolingIcon from './icons/IconTooling.vue'
-import EcosystemIcon from './icons/IconEcosystem.vue'
-import CommunityIcon from './icons/IconCommunity.vue'
-import SupportIcon from './icons/IconSupport.vue'
-
-const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
-</script>
-
-<template>
-  <WelcomeItem>
-    <template #icon>
-      <DocumentationIcon />
-    </template>
-    <template #heading>Documentation</template>
-
-    Vue’s
-    <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
-    provides you with all information you need to get started.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <ToolingIcon />
-    </template>
-    <template #heading>Tooling</template>
-
-    This project is served and bundled with
-    <a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
-    recommended IDE setup is
-    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
-    +
-    <a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
-      >Vue - Official</a
-    >. If you need to test your components and web pages, check out
-    <a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
-    and
-    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
-    /
-    <a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
-
-    <br />
-
-    More instructions are available in
-    <a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
-    >.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <EcosystemIcon />
-    </template>
-    <template #heading>Ecosystem</template>
-
-    Get official tools and libraries for your project:
-    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
-    <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
-    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
-    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
-    you need more resources, we suggest paying
-    <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
-    a visit.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <CommunityIcon />
-    </template>
-    <template #heading>Community</template>
-
-    Got stuck? Ask your question on
-    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
-    (our official Discord server), or
-    <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
-      >StackOverflow</a
-    >. You should also follow the official
-    <a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
-    Bluesky account or the
-    <a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
-    X account for latest news in the Vue world.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <SupportIcon />
-    </template>
-    <template #heading>Support Vue</template>
-
-    As an independent project, Vue relies on community backing for its sustainability. You can help
-    us by
-    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
-  </WelcomeItem>
-</template>
diff --git a/src/components/WelcomeItem.vue b/src/components/WelcomeItem.vue
deleted file mode 100644
index 6d7086a..0000000
--- a/src/components/WelcomeItem.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<template>
-  <div class="item">
-    <i>
-      <slot name="icon"></slot>
-    </i>
-    <div class="details">
-      <h3>
-        <slot name="heading"></slot>
-      </h3>
-      <slot></slot>
-    </div>
-  </div>
-</template>
-
-<style scoped>
-.item {
-  margin-top: 2rem;
-  display: flex;
-  position: relative;
-}
-
-.details {
-  flex: 1;
-  margin-left: 1rem;
-}
-
-i {
-  display: flex;
-  place-items: center;
-  place-content: center;
-  width: 32px;
-  height: 32px;
-
-  color: var(--color-text);
-}
-
-h3 {
-  font-size: 1.2rem;
-  font-weight: 500;
-  margin-bottom: 0.4rem;
-  color: var(--color-heading);
-}
-
-@media (min-width: 1024px) {
-  .item {
-    margin-top: 0;
-    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
-  }
-
-  i {
-    top: calc(50% - 25px);
-    left: -26px;
-    position: absolute;
-    border: 1px solid var(--color-border);
-    background: var(--color-background);
-    border-radius: 8px;
-    width: 50px;
-    height: 50px;
-  }
-
-  .item:before {
-    content: ' ';
-    border-left: 1px solid var(--color-border);
-    position: absolute;
-    left: 0;
-    bottom: calc(50% + 25px);
-    height: calc(50% - 25px);
-  }
-
-  .item:after {
-    content: ' ';
-    border-left: 1px solid var(--color-border);
-    position: absolute;
-    left: 0;
-    top: calc(50% + 25px);
-    height: calc(50% - 25px);
-  }
-
-  .item:first-of-type:before {
-    display: none;
-  }
-
-  .item:last-of-type:after {
-    display: none;
-  }
-}
-</style>
diff --git a/src/components/__tests__/HelloWorld.spec.ts b/src/components/__tests__/HelloWorld.spec.ts
deleted file mode 100644
index 2533202..0000000
--- a/src/components/__tests__/HelloWorld.spec.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { describe, it, expect } from 'vitest'
-
-import { mount } from '@vue/test-utils'
-import HelloWorld from '../HelloWorld.vue'
-
-describe('HelloWorld', () => {
-  it('renders properly', () => {
-    const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
-    expect(wrapper.text()).toContain('Hello Vitest')
-  })
-})
diff --git a/src/components/icons/IconCommunity.vue b/src/components/icons/IconCommunity.vue
deleted file mode 100644
index 2dc8b05..0000000
--- a/src/components/icons/IconCommunity.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-<template>
-  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
-    <path
-      d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
-    />
-  </svg>
-</template>
diff --git a/src/components/icons/IconDocumentation.vue b/src/components/icons/IconDocumentation.vue
deleted file mode 100644
index 6d4791c..0000000
--- a/src/components/icons/IconDocumentation.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-<template>
-  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
-    <path
-      d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
-    />
-  </svg>
-</template>
diff --git a/src/components/icons/IconEcosystem.vue b/src/components/icons/IconEcosystem.vue
deleted file mode 100644
index c3a4f07..0000000
--- a/src/components/icons/IconEcosystem.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-<template>
-  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
-    <path
-      d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
-    />
-  </svg>
-</template>
diff --git a/src/components/icons/IconSupport.vue b/src/components/icons/IconSupport.vue
deleted file mode 100644
index 7452834..0000000
--- a/src/components/icons/IconSupport.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-<template>
-  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
-    <path
-      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
-    />
-  </svg>
-</template>
diff --git a/src/components/icons/IconTooling.vue b/src/components/icons/IconTooling.vue
deleted file mode 100644
index 660598d..0000000
--- a/src/components/icons/IconTooling.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
-<template>
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    xmlns:xlink="http://www.w3.org/1999/xlink"
-    aria-hidden="true"
-    role="img"
-    class="iconify iconify--mdi"
-    width="24"
-    height="24"
-    preserveAspectRatio="xMidYMid meet"
-    viewBox="0 0 24 24"
-  >
-    <path
-      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
-      fill="currentColor"
-    ></path>
-  </svg>
-</template>
diff --git a/src/interfaces/ProductRepositoryInterface.ts b/src/interfaces/ProductRepositoryInterface.ts
new file mode 100644
index 0000000..1a4b7c2
--- /dev/null
+++ b/src/interfaces/ProductRepositoryInterface.ts
@@ -0,0 +1,12 @@
+export interface Product {
+  id: number;
+  title: string;
+  price: number;
+  description: string;
+  category: string;
+  image: string;
+}
+
+export interface ProductRepositoryInterface {
+  getAllProducts(): Promise<Product[]>;
+}
\ No newline at end of file
diff --git a/src/interfaces/RateRepositoryInterface.ts b/src/interfaces/RateRepositoryInterface.ts
new file mode 100644
index 0000000..98d693c
--- /dev/null
+++ b/src/interfaces/RateRepositoryInterface.ts
@@ -0,0 +1,3 @@
+export interface RateRepositoryInterface {
+  fetchRateForYen(): Promise<number>;
+}
\ No newline at end of file
diff --git a/src/repositories/ApiProductRepository.ts b/src/repositories/ApiProductRepository.ts
new file mode 100644
index 0000000..da2d542
--- /dev/null
+++ b/src/repositories/ApiProductRepository.ts
@@ -0,0 +1,11 @@
+import type { Product, ProductRepositoryInterface } from '../interfaces/ProductRepositoryInterface';
+
+export class ApiProductRepository implements ProductRepositoryInterface {
+  async getAllProducts(): Promise<Product[]> {
+      const response = await fetch('https://fakestoreapi.com/products');
+      if (!response.ok) {
+        throw new Error('商品データの取得に失敗しました。');
+      }
+      return await response.json();
+  }
+}
\ No newline at end of file
diff --git a/src/repositories/ApiRateRepository.ts b/src/repositories/ApiRateRepository.ts
new file mode 100644
index 0000000..4421cd8
--- /dev/null
+++ b/src/repositories/ApiRateRepository.ts
@@ -0,0 +1,10 @@
+import type { RateRepositoryInterface } from "../interfaces/RateRepositoryInterface";
+
+export class ApiRateRepository implements RateRepositoryInterface {
+  async fetchRateForYen(): Promise<number> {
+    const response = await fetch("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json");
+    const data = await response.json();
+    return data.usd.jpy;
+  }
+}
+
diff --git a/src/router/index.ts b/src/router/index.ts
index 3e49915..2b97ab6 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,22 +1,19 @@
 import { createRouter, createWebHistory } from 'vue-router'
-import HomeView from '../views/HomeView.vue'
+import ProductList from '../views/ProductListView.vue'
 
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
     {
       path: '/',
-      name: 'home',
-      component: HomeView,
+      name: 'productList',
+      component: ProductList,
     },
     {
-      path: '/about',
-      name: 'about',
-      // route level code-splitting
-      // this generates a separate chunk (About.[hash].js) for this route
-      // which is lazy-loaded when the route is visited.
-      component: () => import('../views/AboutView.vue'),
-    },
+      path: '/product/:id',
+      name: 'productDetail',
+      component: () => import('../views/ProductDetailView.vue'),
+    }
   ],
 })
 
diff --git a/src/stores/UseRateStore.ts b/src/stores/UseRateStore.ts
new file mode 100644
index 0000000..19d05e3
--- /dev/null
+++ b/src/stores/UseRateStore.ts
@@ -0,0 +1,22 @@
+import { defineStore } from 'pinia';
+import type { RateRepositoryInterface } from '@/interfaces/RateRepositoryInterface';
+
+type State = {
+  jpyRate: number | null;
+};
+
+export const useRateStore = defineStore('rate', {
+  state: (): State => ({
+    jpyRate: null,
+  }),
+  actions: {
+    async loadRateForYen(repository: RateRepositoryInterface) {
+      this.jpyRate = await repository.fetchRateForYen();
+    },
+    async initializeRate(repository: RateRepositoryInterface) {
+      if (this.jpyRate === null) {
+        await this.loadRateForYen(repository);
+      }
+    }
+  }
+});
\ No newline at end of file
diff --git a/src/utils/priceFormatter.ts b/src/utils/priceFormatter.ts
new file mode 100644
index 0000000..8a34680
--- /dev/null
+++ b/src/utils/priceFormatter.ts
@@ -0,0 +1,3 @@
+export function convertToYen(priceInDollar: number, rate: number): number {
+  return Math.round(priceInDollar * rate);
+}
\ No newline at end of file
diff --git a/src/views/AboutView.vue b/src/views/AboutView.vue
deleted file mode 100644
index 756ad2a..0000000
--- a/src/views/AboutView.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>
-
-<style>
-@media (min-width: 1024px) {
-  .about {
-    min-height: 100vh;
-    display: flex;
-    align-items: center;
-  }
-}
-</style>
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
deleted file mode 100644
index d5c0217..0000000
--- a/src/views/HomeView.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-<script setup lang="ts">
-import TheWelcome from '../components/TheWelcome.vue'
-</script>
-
-<template>
-  <main>
-    <TheWelcome />
-  </main>
-</template>
diff --git a/src/views/ProductDetailView.vue b/src/views/ProductDetailView.vue
new file mode 100644
index 0000000..0fc0560
--- /dev/null
+++ b/src/views/ProductDetailView.vue
@@ -0,0 +1,14 @@
+<script lang="ts">
+  import { defineComponent } from 'vue';
+
+  export default defineComponent({
+    name: 'ProductDetailView',
+  });
+</script>
+
+<template>
+  <main>
+    <h1>商品詳細</h1>
+    <div class="message">商品データ読み込み中...</div>
+  </main>
+</template>
\ No newline at end of file
diff --git a/src/views/ProductListView.vue b/src/views/ProductListView.vue
new file mode 100644
index 0000000..7a757cb
--- /dev/null
+++ b/src/views/ProductListView.vue
@@ -0,0 +1,82 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import type { Product, ProductRepositoryInterface } from '@/interfaces/ProductRepositoryInterface';
+import type { RateRepositoryInterface } from '@/interfaces/RateRepositoryInterface';
+import { ApiProductRepository } from '@/repositories/ApiProductRepository';
+import { convertToYen } from '@/utils/priceFormatter';
+import { useRouter } from 'vue-router';
+import { ApiRateRepository } from '@/repositories/ApiRateRepository';
+import { useRateStore } from '@/stores/UseRateStore';
+
+const props = withDefaults(defineProps<{
+  productRepository?: ProductRepositoryInterface;
+  rateRepository?: RateRepositoryInterface;
+}>(), {
+  productRepository: () => new ApiProductRepository(),
+  rateRepository: () => new ApiRateRepository()
+});
+
+const products = ref<Product[]>([]);
+const loading = ref(true);
+const error = ref<string | null>(null);
+const router = useRouter();
+const rateStore = useRateStore();
+const rate = ref<number>(0);
+
+const handleDetailClick = (productId: number) => {
+  router.push(`/product/${productId}`);
+}
+
+onMounted(async () => {
+  try {
+    await rateStore.initializeRate(props.rateRepository);
+    if (rateStore.jpyRate === null) {
+      throw new Error('JPYレートが取得できませんでした。');
+    }
+    rate.value = rateStore.jpyRate;
+    const response = await props.productRepository.getAllProducts();
+    products.value = response;
+  } catch (e: any) {
+    error.value = e.message;
+  } finally {
+    loading.value = false;
+  }
+})
+</script>
+
+<template>
+  <main>
+    <h1>商品一覧</h1>
+    <div v-if="loading" class="message">商品データ読み込み中...</div>
+    <div v-else-if="error" class="message">{{ error }}</div>
+    <div v-else class="product-list">
+      <div v-for="product in products" :key="product.id" class="product-card">
+        <img :src="product.image" alt="商品画像" width="50" />
+        <h2>{{ product.title }}</h2>
+        <p>価格: {{ convertToYen(product.price, rate).toLocaleString() }}円</p>
+        <button @click="handleDetailClick(product.id)">詳細を見る</button>
+      </div>
+    </div>
+  </main>
+</template>
+
+<style scoped>
+.product-card {
+  border: 1px solid #ccc;
+  margin: 1em;
+  padding: 1em;
+}
+
+.product-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+  gap: 1rem;
+  padding: 1rem;
+}
+
+.message {
+  text-align: center;
+  font-size: 1.2rem;
+}
+
+</style>
\ No newline at end of file


Reviews:
user: login: ult-shimizu
body: @ult-sohtome 
PRありがとうございます！
外部APIの使い方、非同期処理、typescript、vue.jsの書きっぷり、いいかんじです！
数点コメントしたのでご確認をお願いします！
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T02:50:41Z
user: login: ult-sohtome
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T06:19:04Z
user: login: ult-sohtome
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T06:19:58Z
user: login: ult-sohtome
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T07:59:44Z
user: login: ult-shimizu
body: @ult-sohtome 
修正ありがとうございます！
２点コメントしたのでご確認をお願いします！
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T09:18:30Z
user: login: ult-shimizu
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-07T09:22:48Z
user: login: ult-sohtome
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-08T04:37:57Z
user: login: ult-shimizu
body: @ult-sohtome 
修正ありがとうございます！storeの使い方など、イメージバッチリです！
細かいですが２点コメントしたのでお時間のある時にご確認をお願いします。
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T01:06:25Z
user: login: ult-sohtome
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:33:55Z
user: login: ult-sohtome
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:49:22Z
user: login: ult-shimizu
body: @ult-sohtome 
修正ありがとうございます！LGTMです！
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T01:55:18Z
user: login: ult-shimizu
body: @ult-sohtome 
修正ありがとうございます！propsの書き方など勉強になります！LGTMです！
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T05:47:19Z

Review comments:
diff_hunk: @@ -1,17 +1,21 @@
-<script setup lang="ts">
+<script lang="ts">
+import { defineComponent } from 'vue'
 import { RouterLink, RouterView } from 'vue-router'
-import HelloWorld from './components/HelloWorld.vue'
+
+export default defineComponent({
+  name: 'App',
+  components: {
+    RouterLink,
+    RouterView,
+  }
+});
 </script>
 
 <template>
   <header>
-    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
-
     <div class="wrapper">
-      <HelloWorld msg="You did it!" />
-
       <nav>
-        <RouterLink to="/">Home</RouterLink>
+        <RouterLink to="/">商品一覧</RouterLink>
         <RouterLink to="/about">About</RouterLink>
path: src/App.vue
user: login: ult-shimizu
body: Aboutいらなそうなので削除でお願いします！
created_at: 2025-05-02T07:28:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,12 @@
+export interface Product {
+  id: number;
+  title: string;
+  price: number;
+  description: string;
+  category: string;
+  image: string;
+}
+
+export interface ProductRepository {
path: src/interfaces/ProductRepository.ts
user: login: ult-shimizu
body: interface であることがわかるよう、‘ProductRepositoryInterface`というクラス名でお願いします！
また、ファイル名も同じ形でお願いします！
created_at: 2025-05-07T02:17:24Z
author_association: COLLABORATOR
diff_hunk: @@ -1,22 +1,24 @@
 import { createRouter, createWebHistory } from 'vue-router'
-import HomeView from '../views/HomeView.vue'
+import ProductList from '../views/ProductListView.vue'
 
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
     {
       path: '/',
-      name: 'home',
-      component: HomeView,
+      name: 'productList',
+      component: ProductList,
     },
     {
       path: '/about',
       name: 'about',
-      // route level code-splitting
-      // this generates a separate chunk (About.[hash].js) for this route
-      // which is lazy-loaded when the route is visited.
       component: () => import('../views/AboutView.vue'),
     },
path: src/router/index.ts
user: login: ult-shimizu
body: aboutは削除でお願いします！
created_at: 2025-05-07T02:20:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,3 @@
+export function convertToYen(priceInDollar: number, rate = 140): number {
path: src/utils/priceFormatter.ts
user: login: ult-shimizu
body: rate換算すると日本人にわかりやすいですね！
せっかくなので固定値ではなく、APIを使って換算できるとよりGoodです！
https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json
created_at: 2025-05-07T02:33:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,78 @@
+<script lang="ts">
+import { ref, onMounted, defineComponent } from 'vue';
+import type { Product } from '@/interfaces/ProductRepository';
+import { ApiProductRepository } from '@/repositories/ApiProductRepository';
+import { convertToYen } from '@/utils/priceFormatter';
+import { useRouter } from 'vue-router';
+
+export default defineComponent({
+  name: 'ProductListView',
+  setup() {
+    const products = ref<Product[]>([]);
+    const loading = ref(true);
+    const error = ref<string | null>(null);
+    const repository = new ApiProductRepository();
path: src/views/ProductListView.vue
user: login: ult-shimizu
body: インターフェース用意していただいたので、propsで受け取る形にできると、後々のテストがしやすくなると思います！
デフォルト値に定義するイメージです
https://github.com/ultinet-inc/hypat-ix/blob/f0d8a22c3e31a424cb563cd0c45ebe6e0c20ea86/resources/js/du/pages/chat/ChatPage.vue#L44
created_at: 2025-05-07T02:35:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,78 @@
+<script lang="ts">
+import { ref, onMounted, defineComponent } from 'vue';
+import type { Product } from '@/interfaces/ProductRepository';
+import { ApiProductRepository } from '@/repositories/ApiProductRepository';
+import { convertToYen } from '@/utils/priceFormatter';
+import { useRouter } from 'vue-router';
+
+export default defineComponent({
+  name: 'ProductListView',
+  setup() {
+    const products = ref<Product[]>([]);
+    const loading = ref(true);
+    const error = ref<string | null>(null);
+    const repository = new ApiProductRepository();
path: src/views/ProductListView.vue
user: login: ult-sohtome
body: propsの理解が未だ浅いですが、頂いた参照URLを元に修正しました！
[a84e705](https://github.com/ult-sohtome/shopping-site/pull/1/commits/a84e705fdca84639d719373f56e1fb43a9a0a04a)
ご確認のほどよろしくお願いします。
created_at: 2025-05-07T06:19:04Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,12 @@
+export interface Product {
+  id: number;
+  title: string;
+  price: number;
+  description: string;
+  category: string;
+  image: string;
+}
+
+export interface ProductRepository {
path: src/interfaces/ProductRepository.ts
user: login: ult-sohtome
body: [4194509](https://github.com/ult-sohtome/shopping-site/pull/1/commits/419450907ed94c2e01aa8e4ce9de1a5122c2a0a5)
こちらのコミットで、名前の変更および不要なAboutページ関連を削除しました。

ご確認のほどよろしくお願いします！
created_at: 2025-05-07T06:19:58Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,3 @@
+export function convertToYen(priceInDollar: number, rate = 140): number {
path: src/utils/priceFormatter.ts
user: login: ult-sohtome
body: [fa9f481](https://github.com/ult-sohtome/shopping-site/pull/1/commits/fa9f481b60dbfdb326f7a8156c113b144e608c96)
このコミットにて、APIを使ったレート換算に修正しました！
ご確認のほどよろしくお願いします！
created_at: 2025-05-07T07:59:44Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,10 @@
+async function getRateForYen(): Promise<number> {
+  const apiResponse = await fetch("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json");
+  const data = await apiResponse.json();
+  return data.usd.jpy;
+}
+
+export async function convertToYen(priceInDollar: number): Promise<number> {
+  const rate = await getRateForYen();
path: src/utils/priceFormatter.ts
user: login: ult-shimizu
body: util関数は気軽に使える関数にしたいため、非同期処理は含まない形にしたいです！
以下のようなイメージでお願いしたいです！
・repositoryを用意（APIリクエストを扱うクラス）
　・fetchRateForYen()を定義（APIリクエストする関数）
・pinaのstoreを用意（rate換算に関する処理、状態を管理するstore）
　・stateにjpy（名前はお任せで...）を用意
　・actionに
　　・setRepository()のような関数を用意
　　　※ここでDI注入するイメージ。ただし、もっとよい方法がありそうなので、PRレビュー会とかで他の方法も聞いてみたいです…。
　　・fetchJpy()（名前はお任せで...）を用意
　　　・repositoryのfetchRateForYen()を呼び出す
　　　・stateにjpyを格納
・各viewコンポーネントはpinaのstoreを使う形
　・propsでRepositoryを受け取る
　・mountedのタイミングでstoreのactionを呼び出す（setRepository()→fetchJpy()）
　・表示したいタイミングでstoreのstateを使う
created_at: 2025-05-07T09:05:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,90 @@
+<script lang="ts">
+import { ref, onMounted, defineComponent, type PropType } from 'vue';
+import type { Product, ProductRepositoryInterface, ProductWithYen } from '@/interfaces/ProductRepositoryInterface';
+import { ApiProductRepository } from '@/repositories/ApiProductRepository';
+import { convertToYen } from '@/utils/priceFormatter';
+import { useRouter } from 'vue-router';
+
+export default defineComponent({
+  name: 'ProductListView',
+  props: {
+    productRepository: {
+      type: Object as PropType<ProductRepositoryInterface>,
+      required: false,
+      default: () => new ApiProductRepository(),
+    }
+  },
+  setup(props) {
+    const products = ref<ProductWithYen[]>([]);
+    const loading = ref(true);
+    const error = ref<string | null>(null);
+    const router = useRouter();
+
+    const handleDetailClick = (productId: number) => {
+      router.push(`/product/${productId}`);
+    }
+
+    onMounted(async () => {
+      try {
+        const response = await props.productRepository.getAllProducts();
+        const converted = await Promise.all(
+          response.map(async (product): Promise<ProductWithYen> => ({
+            ...product,
+            priceInYen: await convertToYen(product.price),
path: src/views/ProductListView.vue
user: login: ult-shimizu
body: 商品の数だけAPIリクエストされてしまっています。
全く同じリクエストのようなので、１回だけ呼び出されるようにしたいです！
created_at: 2025-05-07T09:17:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+async function getRateForYen(): Promise<number> {
+  const apiResponse = await fetch("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json");
+  const data = await apiResponse.json();
+  return data.usd.jpy;
+}
+
+export async function convertToYen(priceInDollar: number): Promise<number> {
+  const rate = await getRateForYen();
path: src/utils/priceFormatter.ts
user: login: ult-shimizu
body: 補足：DI注入のイメージ(chcatGPT作)
```
import { defineStore } from 'pinia';
import type { UserService } from '@/services/UserService';

export const useUserStore = defineStore('user', {
  state: () => ({
    userService: null as UserService | null,
    userData: null as { id: number; name: string } | null,
  }),

  actions: {
    setUserService(service: UserService) {
      this.userService = service;
    },

    async loadUser() {
      if (!this.userService) {
        throw new Error('UserService is not set');
      }
      this.userData = await this.userService.fetchUser();
    }
  }
});
```
created_at: 2025-05-07T09:22:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+async function getRateForYen(): Promise<number> {
+  const apiResponse = await fetch("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json");
+  const data = await apiResponse.json();
+  return data.usd.jpy;
+}
+
+export async function convertToYen(priceInDollar: number): Promise<number> {
+  const rate = await getRateForYen();
path: src/utils/priceFormatter.ts
user: login: ult-sohtome
body: アドバイスありがとうございます！
[6a0a6b9](https://github.com/ult-sohtome/shopping-site/pull/1/commits/6a0a6b96087a4537c6c1963e56f060e4349fcfb9)
このコミットにて修正しました！

> ・actionに
　　・setRepository()のような関数を用意

修正当初、stateにリポジトリを用意し、`setRepository()`関数でDI注入するようにしていました。
```
export const useRateStore = defineStore('rate', {
  state: () => ({
    jpyRate: null as number | null,
    repository: null as RateRepositoryInterface | null,
  }),
  actions: {
    setRepository(repository: RateRepositoryInterface) {
      this.repository = repository;
    },...
```
これをchatGPTにチェックしてもらったところ、以下のようなコメントをもらったので`setRepository()`関数を用意しないかたちを採用してみました！
```
1. repository を Pinia の state に持たせない
　Pinia の state に格納されていると、永続化や devtools に不要な値が表示される可能性があります。
　repository は「一時的な依存」であり、state ではなく setup() 内スコープの変数でも十分です。
2. ProductListView.vue では、props.rateRepository を毎回使えばよく、内部に setRepository() は不要
```
清水さんがイメージしていた`setRepository()のような関数を用意`と異なっていましたら教えていただけますと幸いです。
上記内容もふまえ、お時間のある時にご確認のほどよろしくお願いします！
created_at: 2025-05-08T04:37:57Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,18 @@
+import { defineStore } from 'pinia';
+import type { RateRepositoryInterface } from '@/interfaces/RateRepositoryInterface';
+
+export const useRateStore = defineStore('rate', {
+  state: () => ({
+    jpyRate: null as number | null
+  }),
+  actions: {
+    async loadRateForYen(repository: RateRepositoryInterface) {
+      this.jpyRate = await repository.fetchRateForYen();
+    },
+    async initializeRate(repository: RateRepositoryInterface) {
path: src/stores/rate.ts
user: login: ult-shimizu
body: 修正ありがとうございます！
stateに持つより、引数で毎回渡す形の方がよさそうです！！！
created_at: 2025-05-09T00:45:21Z
author_association: COLLABORATOR
path: src/stores/rate.ts
user: login: ult-shimizu
body: ‘useRateStore‘でexportしているので、ファイル名‘UseRateStore‘とした方が混乱しなさそう...と思いました！
created_at: 2025-05-09T00:47:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+<script lang="ts">
+import { ref, onMounted, defineComponent, type PropType } from 'vue';
+import type { Product, ProductRepositoryInterface } from '@/interfaces/ProductRepositoryInterface';
+import { ApiProductRepository } from '@/repositories/ApiProductRepository';
+import { convertToYen } from '@/utils/priceFormatter';
+import { useRouter } from 'vue-router';
+import { ApiRateRepository } from '@/repositories/ApiRateRepository';
+import { useRateStore } from '@/stores/rate';
+
+export default defineComponent({
+  name: 'ProductListView',
+  props: {
+    productRepository: {
+      type: Object as PropType<ProductRepositoryInterface>,
+      required: false,
+      default: () => new ApiProductRepository(),
+    },
+    rateRepository: {
+      type: Object as PropType<ApiRateRepository>,
+      required: false,
+      default: () => new ApiRateRepository(),
+    }
+  },
+  setup(props) {
+    const products = ref<Product[]>([]);
+    const loading = ref(true);
+    const error = ref<string | null>(null);
+    const router = useRouter();
+    const rateStore = useRateStore();
+    const rate = ref<number>(0);
+    const handleDetailClick = (productId: number) => {
+      router.push(`/product/${productId}`);
+    }
+
+    onMounted(async () => {
+      try {
+        await rateStore.initializeRate(props.rateRepository);
+        if (rateStore.jpyRate === null) {
+          throw new Error('JPYレートが取得できませんでした。');
+        }
+        rate.value = rateStore.jpyRate as number;
path: src/views/ProductListView.vue
user: login: ult-shimizu
body: asはできれば避けたいです。
asを使ってしまうと型を無理やり上書きするようなイメージなので、将来のメンテがしにくくなります。
（型を修正した時にtypescript上でエラーが発生しないため、修正箇所に気づきにくい）

今回`rateStore.jpyRate === null`のチェックをしているので、rateStore.jpyRateはnumber型になっていそうです。
なのでasが必要なさそうなのですが、何か別の意図などありますか...？
created_at: 2025-05-09T00:48:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+async function getRateForYen(): Promise<number> {
+  const apiResponse = await fetch("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json");
+  const data = await apiResponse.json();
+  return data.usd.jpy;
+}
+
+export async function convertToYen(priceInDollar: number): Promise<number> {
+  const rate = await getRateForYen();
path: src/utils/priceFormatter.ts
user: login: ult-shimizu
body: ありがとうございます！
イメージバッチリです！！！
setRepository()はなんか違和感あるな。。。と思っていたので、早乙女さんに記述いただいた形でお願いしたいです！！！
created_at: 2025-05-09T01:04:27Z
author_association: COLLABORATOR
path: src/stores/rate.ts
user: login: ult-sohtome
body: ご指摘ありがとうございます！
修正しました！
created_at: 2025-05-09T01:33:54Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,98 @@
+<script lang="ts">
+import { ref, onMounted, defineComponent, type PropType } from 'vue';
+import type { Product, ProductRepositoryInterface } from '@/interfaces/ProductRepositoryInterface';
+import { ApiProductRepository } from '@/repositories/ApiProductRepository';
+import { convertToYen } from '@/utils/priceFormatter';
+import { useRouter } from 'vue-router';
+import { ApiRateRepository } from '@/repositories/ApiRateRepository';
+import { useRateStore } from '@/stores/rate';
+
+export default defineComponent({
+  name: 'ProductListView',
+  props: {
+    productRepository: {
+      type: Object as PropType<ProductRepositoryInterface>,
+      required: false,
+      default: () => new ApiProductRepository(),
+    },
+    rateRepository: {
+      type: Object as PropType<ApiRateRepository>,
+      required: false,
+      default: () => new ApiRateRepository(),
+    }
+  },
+  setup(props) {
+    const products = ref<Product[]>([]);
+    const loading = ref(true);
+    const error = ref<string | null>(null);
+    const router = useRouter();
+    const rateStore = useRateStore();
+    const rate = ref<number>(0);
+    const handleDetailClick = (productId: number) => {
+      router.push(`/product/${productId}`);
+    }
+
+    onMounted(async () => {
+      try {
+        await rateStore.initializeRate(props.rateRepository);
+        if (rateStore.jpyRate === null) {
+          throw new Error('JPYレートが取得できませんでした。');
+        }
+        rate.value = rateStore.jpyRate as number;
path: src/views/ProductListView.vue
user: login: ult-sohtome
body: 以前、asについて伺っていた注意点について失念していました…！
特に意図はありません！ご指摘ありがとうございます。
修正しました！
created_at: 2025-05-09T01:49:22Z
author_association: OWNER

PR comments:
user: login: ult-sohtome
created_at: 2025-05-07T08:02:46Z
body: @ult-shimizu 
お疲れ様です。PRレビュー会にてレビューして頂きありがとうございました！
コメント箇所について修正しましたので、お手すきの際にご確認のほどよろしくお願いします！

- https://github.com/ult-sohtome/shopping-site/pull/1#discussion_r2076873985
- https://github.com/ult-sohtome/shopping-site/pull/1#discussion_r2076872672
- https://github.com/ult-sohtome/shopping-site/pull/1#discussion_r2077035258
author_association: OWNER
user: login: ult-sohtome
created_at: 2025-05-09T05:57:51Z
body: ありがとうございました！マージさせていただきます。
author_association: OWNER