1- import React , { useEffect , useRef , useState } from "react" ;
1+ import React , { useEffect , useMemo , useRef , useState } from "react" ;
22import { useNavigate } from "react-router-dom" ;
33import ResultButtons from "../Result/Components/ResultButtons" ;
44import ResultOverlay from "../Result/Components/ResultOverlay" ;
@@ -10,16 +10,16 @@ import "./Solo.css";
1010
1111const 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+
1321function 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