Skip to content

Commit 6696d9d

Browse files
committed
[FE] 型定義追加、エラーUI
1 parent 34254fc commit 6696d9d

File tree

3 files changed

+144
-24
lines changed

3 files changed

+144
-24
lines changed

frontend/src/Board/BoardList.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,24 @@ function BoardList() {
4343
const [rounds, setRounds] = useState<Round[]>([]);
4444
// ローディング状態
4545
const [isLoading, setIsLoading] = useState(true);
46+
// エラー状態
47+
const [fetchError, setFetchError] = useState(false);
4648

4749
// ページ表示時にAPIからデータを取得する
4850
useEffect(() => {
4951
const fetchRounds = async () => {
5052
try {
5153
const response = await fetch("/api/solo/bookmark/all");
54+
if (!response.ok) {
55+
throw new Error(`サーバーエラー: ${response.status}`);
56+
}
5257
const data: ApiResponse = await response.json();
5358

5459
// results配列をそのままセット
5560
setRounds(data.results);
5661
} catch (error) {
5762
console.error("ラウンド一覧の取得に失敗しました:", error);
63+
setFetchError(true);
5864
} finally {
5965
setIsLoading(false);
6066
}
@@ -80,6 +86,30 @@ function BoardList() {
8086
return <div className="board-list-loading">読み込み中...</div>;
8187
}
8288

89+
// エラー時の表示
90+
if (fetchError) {
91+
return (
92+
<div className="board-list-empty">
93+
<p>データの読み込みに失敗しました</p>
94+
<Button
95+
variant="contained"
96+
onClick={() => window.location.reload()}
97+
className="board-list-back-button"
98+
>
99+
もう一度試す
100+
</Button>
101+
<Button
102+
variant="outlined"
103+
onClick={() => navigate("/")}
104+
className="board-list-back-button"
105+
style={{ marginTop: "8px" }}
106+
>
107+
タイトルに戻る
108+
</Button>
109+
</div>
110+
);
111+
}
112+
83113
// データが空の場合の表示
84114
if (rounds.length === 0) {
85115
return (

frontend/src/Board/index.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ function Board() {
5252
const [messages, setMessages] = useState<Message[]>([]);
5353
// ローディング状態
5454
const [isLoading, setIsLoading] = useState(true);
55+
// エラー状態
56+
const [fetchError, setFetchError] = useState(false);
5557
// 答えの表示/非表示
5658
const [isAnswerVisible, setIsAnswerVisible] = useState(false);
5759

@@ -70,6 +72,9 @@ function Board() {
7072
const fetchRoundData = async () => {
7173
try {
7274
const response = await fetch(`/api/solo/board/${id}`);
75+
if (!response.ok) {
76+
throw new Error(`サーバーエラー: ${response.status}`);
77+
}
7378
const data: RoundResponse = await response.json();
7479

7580
// 正解をセット
@@ -87,6 +92,7 @@ function Board() {
8792
setMessages(formattedMessages);
8893
} catch (error) {
8994
console.error("ラウンドデータの取得に失敗しました:", error);
95+
setFetchError(true);
9096
} finally {
9197
setIsLoading(false);
9298
}
@@ -117,6 +123,28 @@ function Board() {
117123
return <div className="loading-container">読み込み中...</div>;
118124
}
119125

126+
if (fetchError) {
127+
return (
128+
<div className="loading-container">
129+
<p>データの読み込みに失敗しました</p>
130+
<Button
131+
variant="contained"
132+
onClick={() => window.location.reload()}
133+
style={{ marginTop: "16px" }}
134+
>
135+
もう一度試す
136+
</Button>
137+
<Button
138+
variant="outlined"
139+
onClick={() => navigate("/board")}
140+
style={{ marginTop: "8px" }}
141+
>
142+
一覧に戻る
143+
</Button>
144+
</div>
145+
);
146+
}
147+
120148
return (
121149
<div className="board-page-container">
122150
{/* チャット形式のメッセージ表示エリア */}

frontend/src/Solo/index.tsx

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from "react";
1+
import React, { useEffect, useMemo, useRef, useState } from "react";
22
import { useNavigate } from "react-router-dom";
33
import ResultButtons from "../Result/Components/ResultButtons";
44
import ResultOverlay from "../Result/Components/ResultOverlay";
@@ -10,16 +10,16 @@ import "./Solo.css";
1010

1111
const HINT_ICONS = ["/Image/Kyoto.jpg", "/Image/Osaka.jpg"];
1212

13+
type MessageItem = {
14+
messageId: number;
15+
hint: string;
16+
isUser: boolean;
17+
icon?: string;
18+
isDivider?: boolean;
19+
};
20+
1321
function Solo() {
14-
const [messages, setMessages] = useState<
15-
{
16-
messageId: number;
17-
hint: string;
18-
isUser: boolean;
19-
icon?: string;
20-
isDivider?: boolean;
21-
}[]
22-
>([]);
22+
const [messages, setMessages] = useState<MessageItem[]>([]);
2323
const [hints, setHints] = useState<string[]>([]);
2424
const [gameId, setGameId] = useState<number | null>(null);
2525
const [inputValue, setInputValue] = useState("");
@@ -32,7 +32,8 @@ function Solo() {
3232
const [isBookmarked, setIsBookmarked] = useState(false);
3333
const [isAnimating, setIsAnimating] = useState(false);
3434
const [isGivenUp, setIsGivenUp] = useState(false);
35-
const [pendingHints, setPendingHints] = useState<any[]>([]);
35+
const [pendingHints, setPendingHints] = useState<MessageItem[]>([]);
36+
const [fetchError, setFetchError] = useState(false);
3637
const [loadingMsgIndex, setLoadingMsgIndex] = useState(0);
3738
const loadingMessages = [
3839
"エスカレーターは右側に立つ",
@@ -51,6 +52,7 @@ function Solo() {
5152
const messagesAreaRef = useRef<HTMLDivElement>(null);
5253
const isAtBottomRef = useRef(true);
5354
const hasFetchedData = useRef(false);
55+
const giveUpTimerRef = useRef<number | null>(null);
5456

5557
useEffect(() => {
5658
if (hasFetchedData.current) return;
@@ -63,6 +65,9 @@ function Solo() {
6365
headers: { "Content-Type": "application/json" },
6466
body: JSON.stringify({}),
6567
});
68+
if (!res.ok) {
69+
throw new Error(`サーバーエラー: ${res.status}`);
70+
}
6671
const data = await res.json();
6772
if (data.result && data.result.hints) {
6873
setHints(data.result.hints);
@@ -80,6 +85,7 @@ function Solo() {
8085
}
8186
} catch (e) {
8287
console.error(e);
88+
setFetchError(true);
8389
} finally {
8490
setIsLoading(false);
8591
}
@@ -172,6 +178,9 @@ function Solo() {
172178
headers: { "Content-Type": "application/json" },
173179
body: JSON.stringify({ answer: newMessage.hint }),
174180
});
181+
if (!res.ok) {
182+
throw new Error(`サーバーエラー: ${res.status}`);
183+
}
175184
const data = await res.json();
176185
const isCorrect = data.isCorrect;
177186

@@ -190,26 +199,40 @@ function Solo() {
190199
}
191200
} catch (error) {
192201
console.error("Error submitting answer:", error);
202+
setMessages((prev) => [
203+
...prev,
204+
{
205+
messageId: Date.now() + 1,
206+
hint: "通信エラーが発生しました。もう一度お試しください。",
207+
isUser: false,
208+
icon: "/Image/Kyoto.jpg",
209+
},
210+
]);
193211
}
194212
};
195213

196214
// 全ヒントが表示されたかどうかを判定する
197-
const shownHintCount = messages.filter(
198-
(m) =>
199-
!m.isUser &&
200-
!m.isDivider &&
201-
m.hint !== "正解やで!" &&
202-
m.hint !== "不正解どす..." &&
203-
!m.hint.startsWith("正解は「"),
204-
).length;
205-
const allHintsShown = hints.length > 0 && shownHintCount >= hints.length;
215+
const allHintsShown = useMemo(() => {
216+
const shownHintCount = messages.filter(
217+
(m) =>
218+
!m.isUser &&
219+
!m.isDivider &&
220+
m.hint !== "正解やで!" &&
221+
m.hint !== "不正解どす..." &&
222+
!m.hint.startsWith("正解は「"),
223+
).length;
224+
return hints.length > 0 && shownHintCount >= hints.length;
225+
}, [messages, hints]);
206226

207227
// ギブアップ処理:正解を取得してチャットに表示する
208228
const handleGiveUp = async () => {
209229
if (gameId === null) return;
210230

211231
try {
212232
const res = await fetch(`/api/solo/${gameId}/answer`);
233+
if (!res.ok) {
234+
throw new Error(`サーバーエラー: ${res.status}`);
235+
}
213236
const data = await res.json();
214237
const correctAnswer = data.answer;
215238

@@ -225,7 +248,7 @@ function Solo() {
225248
]);
226249

227250
// 少し間を空けてから正解を表示する
228-
setTimeout(() => {
251+
giveUpTimerRef.current = setTimeout(() => {
229252
setMessages((prev) => [
230253
...prev,
231254
{
@@ -241,9 +264,27 @@ function Solo() {
241264
}, 500);
242265
} catch (error) {
243266
console.error("正解の取得に失敗しました:", error);
267+
setMessages((prev) => [
268+
...prev,
269+
{
270+
messageId: Date.now(),
271+
hint: "通信エラーが発生しました。もう一度お試しください。",
272+
isUser: false,
273+
icon: "/Image/Kyoto.jpg",
274+
},
275+
]);
244276
}
245277
};
246278

279+
// giveUpTimerのクリーンアップ
280+
useEffect(() => {
281+
return () => {
282+
if (giveUpTimerRef.current !== null) {
283+
clearTimeout(giveUpTimerRef.current);
284+
}
285+
};
286+
}, []);
287+
247288
const handleTitle = () => navigate("/");
248289
const handleRetry = () => window.location.reload();
249290
const handleShare = () => setIsShareOpen(true);
@@ -342,9 +383,7 @@ function Solo() {
342383
<div className="loading-text">
343384
関西あるある
344385
<br />
345-
<div className="aruaru-text">
346-
{loadingMessages[loadingMsgIndex]}
347-
</div>
386+
<div className="aruaru-text">{loadingMessages[loadingMsgIndex]}</div>
348387
</div>
349388
</div>
350389
<div className="spinner" />
@@ -353,6 +392,29 @@ function Solo() {
353392
);
354393
}
355394

395+
if (fetchError) {
396+
return (
397+
<div className="loading-container">
398+
<div className="loading-icon-container">
399+
<img
400+
src="/Image/Kyoto.jpg"
401+
alt="Error Icon"
402+
className="loading-icon-img"
403+
/>
404+
</div>
405+
<div className="loading-progress-text">読み込みに失敗しました</div>
406+
<button
407+
type="button"
408+
className="giveup-chat-bubble"
409+
onClick={() => window.location.reload()}
410+
style={{ marginTop: "16px" }}
411+
>
412+
<span className="giveup-chat-text">もう一度試す</span>
413+
</button>
414+
</div>
415+
);
416+
}
417+
356418
return (
357419
<div className="solo-container">
358420
{showResultOverlay && gameId !== null && (

0 commit comments

Comments
 (0)