APIを繋ぎ込む - #24
Conversation
|
@coderabbitai レビューしてください |
|
@coderabbitai plz review |
|
🧠 Learnings used✅ Actions performedReview triggered.
|
|
No actionable comments were generated in the recent review. 🎉 WalkthroughReact + TypeScript + Vite を用いた新しいプレスリリース編集フロントエンドを追加。TanStack Query を使ったサーバ同期、TipTap エディタ、ESLint/TypeScript/Vite 設定、パッケージ定義、静的アセット(HTML/CSS)を導入。 Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Browser as ブラウザ
participant App as React App
participant Query as TanStack Query
participant Editor as TipTap Editor
participant API as Backend API
Browser->>App: ページ読み込み
App->>Query: usePressReleaseQuery 実行
Query->>API: GET /press-releases/1
API-->>Query: { title, content }
Query-->>App: データ返却
App->>Editor: 初期コンテンツ設定
Editor-->>Browser: エディタ表示
User->>Editor: コンテンツ編集
Editor-->>App: ローカル状態更新
User->>App: 保存ボタンクリック
App->>Query: useSavePressReleaseMutation 実行
Query->>API: POST /press-releases/1 (title + JSON content)
API-->>Query: 成功応答
Query->>Query: キャッシュ無効化
Query->>API: GET /press-releases/1 (再取得)
API-->>Query: 更新データ
Query-->>App: UI 更新
App-->>Browser: 保存完了表示
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (5)
webapp/frontend/react/package.json (1)
2-2: パッケージ名をプロジェクトに適したものに変更してください。
"name": "react"は汎用的すぎます。"press-release-editor"や"hackathon2026-press-release-editor"など、プロジェクトを識別できる名前に変更することを推奨します。- "name": "react", + "name": "press-release-editor",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/react/package.json` at line 2, Update the package.json "name" field from the generic value "react" to a project-specific identifier (for example "press-release-editor" or "hackathon2026-press-release-editor") so the package can be uniquely identified; edit the "name" key in package.json accordingly and ensure the new name follows npm naming rules (lowercase, hyphens, no spaces).webapp/frontend/react/index.html (2)
7-7: タイトルをプロジェクトに適したものに更新してください。「react」はプレースホルダーです。例えば「プレスリリースエディタ」など、実際のアプリケーション名に変更することを推奨します。
- <title>react</title> + <title>プレスリリースエディタ</title>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/react/index.html` at line 7, HTMLの<title>がプレースホルダーの "react" になっているため、プロジェクト名または機能に合わせた適切なタイトルに変更してください(該当箇所: index.html の <title> 要素)。例: "プレスリリースエディタ" のようなアプリケーション固有の名前に置き換え、ユーザーに表示されるタブやSEOに反映されるように更新してください。
2-2:lang属性の確認が必要です。日本語向けのプレスリリースエディタであれば、
lang="ja"に変更することを検討してください。-<html lang="en"> +<html lang="ja">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/react/index.html` at line 2, 現在のHTMLルートタグは <html lang="en"> となっており、日本語向けのプレスリリースエディタであれば言語属性を適切に設定する必要があります。該当する <html lang="en"> を見つけて、対象ロケールが日本語の場合は lang="ja" に変更してください(必要に応じてドキュメント言語に合わせて動的に設定するロジックを導入している箇所があればそちらも更新してください)。webapp/frontend/react/vite.config.ts (1)
1-7: 開発環境での API プロキシ設定を検討してください。プレスリリース API を統合する場合、開発環境で CORS 問題を回避するために
server.proxyの設定が必要になる可能性があります。♻️ プロキシ設定の例
export default defineConfig({ plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', // バックエンドのURL + changeOrigin: true, + }, + }, + }, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/react/vite.config.ts` around lines 1 - 7, 開発環境で CORS を回避するために Vite の設定に API プロキシを追加してください: vite.config.ts の defineConfig 呼び出し内に server.proxy オプションを追加し、プレスリリース API のパス(例: '/api/releases' や '/press')をターゲットの開発バックエンド URL にリダイレクトする設定を行ってください。必要に応じて changeOrigin と secure フラグやパス書き換えルールを設定し、既存の plugins: [react()] 設定はそのまま残してください。webapp/frontend/react/src/main.tsx (1)
7-7: QueryClient にデフォルトオプションの設定を検討してください。本番環境では、リトライロジックや
staleTimeなどのデフォルトオプションを設定することで、より良いユーザー体験を提供できます。♻️ 推奨される設定例
-const queryClient = new QueryClient() +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60, // 1分 + retry: 1, + }, + }, +})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@webapp/frontend/react/src/main.tsx` at line 7, The QueryClient instantiation lacks application-wide default options; update the new QueryClient(...) call in main.tsx to pass defaultOptions for queries (e.g., set queries.retry to a sensible small number or function, queries.staleTime to a duration in ms, and queries.refetchOnWindowFocus to false) so you get consistent retry/backoff and caching behavior across hooks; locate the const queryClient = new QueryClient() expression and replace it with a QueryClient configured with those defaultOptions for queries (and optionally mutations) to control global retry, staleTime, and refetch behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@webapp/frontend/react/.gitignore`:
- Around line 10-13: The .gitignore currently only includes a broad '*.local'
but does not explicitly ignore base and other env files, risking accidental
commits of secrets; update the .gitignore to add explicit environment variable
patterns such as .env, .env.* (or at minimum .env and
.env.production/.env.development/.env.test) so all common dotenv files are
ignored; modify the same file where node_modules/dist entries exist and ensure
entries like ".env" and ".env.*" (or specific ".env.production", ".env.local",
".env.test") are added to cover all environment files.
In `@webapp/frontend/react/src/App.tsx`:
- Around line 61-88: The save handler and button must guard against the editor
being null during asynchronous initialization: update handleSave to first check
that the editor instance from useEditor is non-null before calling
editor.getJSON() (return early or show an error if null), and also add the
editor presence check to the button's disabled prop (e.g., disabled={isPending
|| !editor}) so the button is disabled until useEditor returns a valid instance;
reference useEditor, the editor variable, handleSave, and editor.getJSON when
making the changes.
- Around line 13-51: Update usePressReleaseQuery to check response.ok and throw
an Error when the fetch returns a non-2xx status (include status and/or
statusText in the error message) so TanStack Query surfaces HTTP errors;
specifically modify the async queryFn inside usePressReleaseQuery to inspect
response.ok and throw accordingly instead of directly calling res.json(). In
App, read isError and error from usePressReleaseQuery (e.g., const { data,
isPending, isError, error } = usePressReleaseQuery()), render an error UI when
isError using error.message, and guard JSON.parse(data.content) with a try/catch
so JSON parse failures are caught and produce a user-facing error state (or
fallback content) instead of crashing the component.
In `@webapp/frontend/react/src/index.css`:
- Around line 5-7: Remove the extraneous blank line before the CSS declarations
and fix the text-rendering value to use the lowercase token expected by
Stylelint: delete the empty line above the font-synthesis and text-rendering
rules and change text-rendering: optimizeLegibility to text-rendering:
optimizelegibility in index.css (look for the font-synthesis and text-rendering
declarations to locate the code).
---
Nitpick comments:
In `@webapp/frontend/react/index.html`:
- Line 7: HTMLの<title>がプレースホルダーの "react"
になっているため、プロジェクト名または機能に合わせた適切なタイトルに変更してください(該当箇所: index.html の <title> 要素)。例:
"プレスリリースエディタ" のようなアプリケーション固有の名前に置き換え、ユーザーに表示されるタブやSEOに反映されるように更新してください。
- Line 2: 現在のHTMLルートタグは <html lang="en">
となっており、日本語向けのプレスリリースエディタであれば言語属性を適切に設定する必要があります。該当する <html lang="en">
を見つけて、対象ロケールが日本語の場合は lang="ja"
に変更してください(必要に応じてドキュメント言語に合わせて動的に設定するロジックを導入している箇所があればそちらも更新してください)。
In `@webapp/frontend/react/package.json`:
- Line 2: Update the package.json "name" field from the generic value "react" to
a project-specific identifier (for example "press-release-editor" or
"hackathon2026-press-release-editor") so the package can be uniquely identified;
edit the "name" key in package.json accordingly and ensure the new name follows
npm naming rules (lowercase, hyphens, no spaces).
In `@webapp/frontend/react/src/main.tsx`:
- Line 7: The QueryClient instantiation lacks application-wide default options;
update the new QueryClient(...) call in main.tsx to pass defaultOptions for
queries (e.g., set queries.retry to a sensible small number or function,
queries.staleTime to a duration in ms, and queries.refetchOnWindowFocus to
false) so you get consistent retry/backoff and caching behavior across hooks;
locate the const queryClient = new QueryClient() expression and replace it with
a QueryClient configured with those defaultOptions for queries (and optionally
mutations) to control global retry, staleTime, and refetch behavior.
In `@webapp/frontend/react/vite.config.ts`:
- Around line 1-7: 開発環境で CORS を回避するために Vite の設定に API プロキシを追加してください:
vite.config.ts の defineConfig 呼び出し内に server.proxy オプションを追加し、プレスリリース API のパス(例:
'/api/releases' や '/press')をターゲットの開発バックエンド URL にリダイレクトする設定を行ってください。必要に応じて
changeOrigin と secure フラグやパス書き換えルールを設定し、既存の plugins: [react()] 設定はそのまま残してください。
| node_modules | ||
| dist | ||
| dist-ssr | ||
| *.local |
There was a problem hiding this comment.
環境変数ファイルの保護を強化してください。
*.localパターンは.env.localなどをカバーしていますが、ベースとなる.envファイルが明示的に無視されていません。シークレット情報の誤コミットを防ぐため、環境変数ファイルのパターンを追加することをおすすめします。Based on learnings: プロジェクトでは「シークレットをコミットしない」ことが重要です。
🔒 環境変数ファイルのパターンを追加する提案
node_modules
dist
dist-ssr
*.local
+
+# env files
+.env
+.env*.local📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| node_modules | |
| dist | |
| dist-ssr | |
| *.local | |
| node_modules | |
| dist | |
| dist-ssr | |
| *.local | |
| # env files | |
| .env | |
| .env*.local |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/react/.gitignore` around lines 10 - 13, The .gitignore
currently only includes a broad '*.local' but does not explicitly ignore base
and other env files, risking accidental commits of secrets; update the
.gitignore to add explicit environment variable patterns such as .env, .env.*
(or at minimum .env and .env.production/.env.development/.env.test) so all
common dotenv files are ignored; modify the same file where node_modules/dist
entries exist and ensure entries like ".env" and ".env.*" (or specific
".env.production", ".env.local", ".env.test") are added to cover all environment
files.
| function usePressReleaseQuery() { | ||
| return useQuery({queryKey, queryFn: async () => { | ||
| return await fetch(`${BASE_URL}/press-releases/1`).then(res => res.json()) | ||
| }}); | ||
| } | ||
|
|
||
| function useSavePressReleaseMutation() { | ||
| const queryClient = useQueryClient() | ||
|
|
||
| return useMutation({ | ||
| mutationFn: async (data: { title: string; content: string }) => { | ||
| const response = await fetch(`${BASE_URL}/press-releases/1`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(data), | ||
| }) | ||
| if (!response.ok) { | ||
| throw new Error('保存に失敗しました') | ||
| } | ||
| return response.json() | ||
| }, | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey }) | ||
| }, | ||
| onError: (error) => { | ||
| alert(`エラー: ${error.message}`) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
|
|
||
| export function App() { | ||
| const { data, isPending } = usePressReleaseQuery() | ||
|
|
||
| if (isPending) return null; | ||
|
|
||
| return <Page title={data.title} content={JSON.parse(data.content)} /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# ファイルの存在と内容を確認
find . -name "App.tsx" -path "*/frontend/react/src/*" 2>/dev/null | head -20Repository: PRTIMES/hackathon2026-spring-press-release-editor
Length of output: 128
🏁 Script executed:
# リポジトリ構造を確認
ls -la webapp/frontend/react/src/ 2>/dev/null || ls -la . | head -30Repository: PRTIMES/hackathon2026-spring-press-release-editor
Length of output: 491
🏁 Script executed:
# useQueryとエラーハンドリングのパターンを検索
rg "useQuery.*response\.ok|isError" --type tsx --type ts -A 3 -B 3 2>/dev/null | head -40Repository: PRTIMES/hackathon2026-spring-press-release-editor
Length of output: 75
🏁 Script executed:
cat -n ./webapp/frontend/react/src/App.tsxRepository: PRTIMES/hackathon2026-spring-press-release-editor
Length of output: 3649
🌐 Web query:
TanStack Query useQuery error handling response.ok fetch pattern best practice
💡 Result:
Best practice with useQuery + fetch: make your queryFn throw on non-2xx (because fetch won’t), and throw something that preserves HTTP details (status/body) so the UI can react. TanStack Query marks a query as errored only when the queryFn throws / returns a rejected promise. (tanstack.com)
Recommended pattern (typed, keeps status + server payload, supports cancellation)
type ApiErrorBody = { message?: string; code?: string };
export class HttpError extends Error {
status: number;
body?: unknown;
constructor(status: number, message: string, body?: unknown) {
super(message);
this.name = "HttpError";
this.status = status;
this.body = body;
}
}
export async function fetchJson<T>(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<T> {
const res = await fetch(input, init);
if (!res.ok) {
// try to capture server error details (often JSON, sometimes not)
let body: unknown;
try { body = await res.clone().json(); } catch { body = await res.clone().text().catch(() => undefined); }
const msg =
(typeof body === "object" && body && "message" in body && typeof (body as any).message === "string")
? (body as ApiErrorBody).message!
: `Request failed (${res.status})`;
throw new HttpError(res.status, msg, body);
}
return res.json() as Promise<T>;
}const query = useQuery({
queryKey: ["todos", todoId],
queryFn: ({ signal }) => fetchJson<Todo>(`/api/todos/${todoId}`, { signal }),
});TanStack Query explicitly recommends this if (!response.ok) throw ... approach for fetch, and it passes an AbortSignal into your query function so in-flight requests can be cancelled. (tanstack.com)
Handling in UI (status-specific)
if (query.isError) {
const err = query.error;
if (err instanceof HttpError && err.status === 404) return "Not found";
return "Something went wrong";
}Common extras
- Retry only “retryable” statuses (don’t retry 400/401/403/404; maybe retry 408/429/5xx).
- If you want errors to go to an Error Boundary, use TanStack Query’s
throwOnErroroption (and note Suspense variants always throw). (github.com) - If you need the HTTP status in
useQuery, you must put it on the thrown error (TanStack Query won’t magically infer it fromfetch). (github.com)
Citations:
- 1: https://tanstack.com/query/v4/docs/framework/react/guides/query-functions?utm_source=openai
- 2: https://tanstack.com/query/v4/docs/framework/react/guides/query-functions?utm_source=openai
- 3: [react-query] useSuspenseQuery missing docs on
throwOnErrorTanStack/query#6577 - 4: Advanced Error Handling in React Queries: Retrieving Server Error Codes Alongside isError Conditions TanStack/query#6490
usePressReleaseQuery で HTTP エラーをハンドリングし、App コンポーネントでクエリエラーと JSON パースエラーに対応する必要があります。
usePressReleaseQuery の fetch は response.ok をチェックしていないため、404 などのエラー時に不正なレスポンスを JSON パースしてしまいます。TanStack Query のベストプラクティスに従い、response.ok が false の場合は例外をスロー してください。また、App コンポーネント側で isError をチェックし、JSON パースエラーに対応してください。
修正案
function usePressReleaseQuery() {
return useQuery({queryKey, queryFn: async () => {
- return await fetch(`${BASE_URL}/press-releases/1`).then(res => res.json())
+ const res = await fetch(`${BASE_URL}/press-releases/1`)
+ if (!res.ok) {
+ throw new Error('取得に失敗しました')
+ }
+ return res.json()
}});
}
export function App() {
- const { data, isPending } = usePressReleaseQuery()
+ const { data, isPending, isError } = usePressReleaseQuery()
if (isPending) return null;
+ if (isError || !data) return <div>取得に失敗しました</div>
+
+ let parsedContent: unknown
+ try {
+ parsedContent = JSON.parse(data.content)
+ } catch {
+ return <div>コンテンツ形式が不正です</div>
+ }
- return <Page title={data.title} content={JSON.parse(data.content)} />
+ return <Page title={data.title} content={parsedContent} />
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/react/src/App.tsx` around lines 13 - 51, Update
usePressReleaseQuery to check response.ok and throw an Error when the fetch
returns a non-2xx status (include status and/or statusText in the error message)
so TanStack Query surfaces HTTP errors; specifically modify the async queryFn
inside usePressReleaseQuery to inspect response.ok and throw accordingly instead
of directly calling res.json(). In App, read isError and error from
usePressReleaseQuery (e.g., const { data, isPending, isError, error } =
usePressReleaseQuery()), render an error UI when isError using error.message,
and guard JSON.parse(data.content) with a try/catch so JSON parse failures are
caught and produce a user-facing error state (or fallback content) instead of
crashing the component.
| const editor = useEditor({ | ||
| extensions: [ | ||
| Document, | ||
| Heading, | ||
| Paragraph, | ||
| Text | ||
| ], | ||
| content, | ||
| }) | ||
|
|
||
| const {isPending, mutate} = useSavePressReleaseMutation() | ||
|
|
||
| const handleSave = () => { | ||
| mutate({ | ||
| title, | ||
| content: JSON.stringify(editor.getJSON()) | ||
| }) | ||
| } | ||
|
|
||
| return ( | ||
| <div className='container'> | ||
| {/* ヘッダー */} | ||
| <header className='header'> | ||
| <h1 className='title'>プレスリリースエディター</h1> | ||
| <button | ||
| onClick={handleSave} | ||
| className='saveButton' | ||
| disabled={isPending} |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
TipTap useEditor hook initialization null behavior returns during initial render
💡 Result:
useEditor() can legitimately return null during the initial render because the editor instance isn’t guaranteed to be created synchronously on that first render (historically/for SSR it may be deferred to an effect/second render). Tiptap’s docs even show guarding against this by rendering nothing until editor exists. [1]
Key points / what to do:
- Treat the editor as “async-ready”: handle
Editor | nulland render a loading state (ornull) until it’s initialized. [1][2] - SSR / hydration case: if you’re using SSR, set
immediatelyRender: falseto prevent server rendering and expecteditorto benulluntil the client initializes it—then guard render until it’s ready. [1][3] - If using the Composable API (
<Tiptap>): preferuseTiptap()and checkisReady(pluseditor) before using it in child components. [2]
Example guard (common pattern):
const editor = useEditor({ extensions: [StarterKit], content, immediatelyRender: false })
if (!editor) return null
return <EditorContent editor={editor} />This “null first, editor later” behavior is expected and commonly answered this way. [1][4]
Sources:
[1] Tiptap React docs (SSR + guard pattern) (tiptap.dev)
[2] Tiptap React Composable API docs (isReady) (tiptap.dev)
[3] immediatelyRender behavior explained in release notes (newreleases.io)
[4] Stack Overflow discussion confirming initial null + guard (stackoverflow.com)
Citations:
- 1: https://tiptap.dev/docs/editor/getting-started/install/react?utm_source=openai
- 2: https://tiptap.dev/docs/guides/react-composable-api?utm_source=openai
- 3: https://newreleases.io/project/github/ueberdosis/tiptap/release/%40tiptap%2Freact%402.5.0-pre.14?utm_source=openai
- 4: https://stackoverflow.com/questions/70057834/tiptap-useeditor-returning-null?utm_source=openai
Editor初期化完了までボタンと保存処理の両方を無効化してください。
TipTap の useEditor は初期レンダー時に null を返します(ドキュメント公式確認)。これはエディタインスタンスが非同期で初期化されるためです。現在のコードでは、初期化中に保存ボタンが押下されると editor.getJSON() でエラーが発生します。
以下の2つのガードを追加してください:
handleSave内で null チェック- ボタンの
disabled属性に!editorを追加
修正案
const editor = useEditor({
extensions: [
Document,
Heading,
Paragraph,
Text
],
content,
})
const {isPending, mutate} = useSavePressReleaseMutation()
const handleSave = () => {
+ if (!editor) return
mutate({
title,
content: JSON.stringify(editor.getJSON())
})
}
@@
<button
onClick={handleSave}
className='saveButton'
- disabled={isPending}
+ disabled={isPending || !editor}
>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/react/src/App.tsx` around lines 61 - 88, The save handler and
button must guard against the editor being null during asynchronous
initialization: update handleSave to first check that the editor instance from
useEditor is non-null before calling editor.getJSON() (return early or show an
error if null), and also add the editor presence check to the button's disabled
prop (e.g., disabled={isPending || !editor}) so the button is disabled until
useEditor returns a valid instance; reference useEditor, the editor variable,
handleSave, and editor.getJSON when making the changes.
|
|
||
| font-synthesis: none; | ||
| text-rendering: optimizeLegibility; |
There was a problem hiding this comment.
Stylelint エラーを修正してください。
静的解析ツールが以下の問題を検出しています:
- Line 6: 宣言前の空行が不要
- Line 7:
optimizeLegibilityは小文字のoptimizelegibilityにする必要がある(Stylelint のルールに従う場合)
🛠️ 修正案
font-weight: 400;
-
font-synthesis: none;
- text-rendering: optimizeLegibility;
+ text-rendering: optimizelegibility;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| font-synthesis: none; | |
| text-rendering: optimizeLegibility; | |
| font-synthesis: none; | |
| text-rendering: optimizelegibility; |
🧰 Tools
🪛 Stylelint (17.3.0)
[error] 6-6: Unexpected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
[error] 7-7: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case)
(value-keyword-case)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/frontend/react/src/index.css` around lines 5 - 7, Remove the
extraneous blank line before the CSS declarations and fix the text-rendering
value to use the lowercase token expected by Stylelint: delete the empty line
above the font-synthesis and text-rendering rules and change text-rendering:
optimizeLegibility to text-rendering: optimizelegibility in index.css (look for
the font-synthesis and text-rendering declarations to locate the code).
概要
プレスリリース取得と保存のAPIを繋ぎ込みました
Summary by CodeRabbit
リリースノート