Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/dto/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ type StartGameResult struct {
}

type CheckAnswerRequest struct {
Answer string `json:"answer" binding:"required"`
Answer string `json:"answer" binding:"required,max=200"`
}
26 changes: 20 additions & 6 deletions backend/services/hint_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import (
const MinRoundAnswerRevealDuration = 90 * time.Second

var (
ErrRoundNotFound = errors.New("round not found")
ErrRoundNotFinished = errors.New("round is not finished yet")
ErrRoundTooEarly = errors.New("game has not been played long enough")
ErrBookmarkNotFound = errors.New("bookmark not found")
ErrRoundNotFound = errors.New("round not found")
ErrRoundNotFinished = errors.New("round is not finished yet")
ErrRoundTooEarly = errors.New("game has not been played long enough")
ErrBookmarkNotFound = errors.New("bookmark not found")
ErrNoAnswersAvailable = errors.New("no answers available")
)

type IHintService interface {
Expand Down Expand Up @@ -186,8 +187,6 @@ func (s *HintService) StartGame() (*dto.StartGameResult, error) {

text := res.Text()

fmt.Println(text)

if err := json.Unmarshal([]byte(text), &geminiData); err != nil {
return nil, fmt.Errorf("JSONパースに失敗しました: %w (raw: %s)", err, text)
}
Expand Down Expand Up @@ -221,6 +220,9 @@ func (s *HintService) GetAnswer(id uint) (string, error) {
if err != nil {
return "", err
}
if len(round.Answers) == 0 {
return "", fmt.Errorf("%w: id=%d", ErrNoAnswersAvailable, id)
}
if !round.IsFinished {
if time.Since(round.CreatedAt) < MinRoundAnswerRevealDuration {
return "", fmt.Errorf("%w", ErrRoundTooEarly)
Expand Down Expand Up @@ -257,6 +259,9 @@ func (s *HintService) GetFinishedRoundByID(id uint) (*dto.RoundResponse, error)
if !round.IsFinished {
return nil, fmt.Errorf("%w: id=%d", ErrRoundNotFinished, id)
}
if len(round.Answers) == 0 {
return nil, fmt.Errorf("%w: id=%d", ErrNoAnswersAvailable, id)
}
result := &dto.RoundResponse{
ID: round.ID,
Answer: round.Answers[0],
Expand All @@ -274,6 +279,9 @@ func (s *HintService) BookmarkRound(id uint) (*dto.RoundResponse, error) {
if !round.IsFinished {
return nil, fmt.Errorf("%w: id=%d", ErrRoundNotFinished, id)
}
if len(round.Answers) == 0 {
return nil, fmt.Errorf("%w: id=%d", ErrNoAnswersAvailable, id)
}
if err := s.repository.BookmarkRound(id); err != nil {
return nil, fmt.Errorf("failed to bookmark round: %w", err)
}
Expand All @@ -295,6 +303,9 @@ func (s *HintService) GetRandomBookmark() (*dto.RoundResponse, error) {
return nil, ErrBookmarkNotFound
}

if len(round.Answers) == 0 {
return nil, ErrNoAnswersAvailable
}
return &dto.RoundResponse{
ID: round.ID,
Answer: round.Answers[0],
Expand All @@ -312,6 +323,9 @@ func (s *HintService) GetBookmarkedList() ([]dto.RoundResponse, error) {
response := []dto.RoundResponse{}

for _, r := range rounds {
if len(r.Answers) == 0 {
continue
}
limit := 4
if len(r.Hints) < limit {
limit = len(r.Hints)
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/Board/Board.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,48 @@
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 24px;
color: #555;
background-color: #f5f5f5;
gap: 12px;
}

.error-icon-container {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
border: 3px solid #fff;
margin-bottom: 8px;
}

.error-icon-container img {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.7);
}

.error-message {
font-size: 16px;
font-weight: 600;
color: #666;
margin: 0;
}

.error-actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}

/* Material Design Color Tokens */
Expand Down
49 changes: 47 additions & 2 deletions frontend/src/Board/BoardList.css
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,66 @@
background-color: #faf7f2;
}

/* --- 空状態 --- */
/* --- 空状態・エラー状態 --- */
.board-list-empty {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
gap: 12px;
background-color: #faf7f2;
color: #888;
font-size: 1.1rem;
}

.board-list-empty .error-icon-container {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
border: 3px solid #fff;
margin-bottom: 4px;
}

.board-list-empty .error-icon-container img {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.7);
}

.board-list-empty .error-message {
font-size: 16px;
font-weight: 600;
color: #666;
margin: 0;
}

.board-list-empty .error-actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}

.board-list-back-button {
background-color: #ed6c02 !important;
color: #ffffff !important;
text-transform: none !important;
font-weight: bold !important;
border-radius: 8px !important;
}

.board-list-back-button-outlined {
background-color: #ffffff !important;
color: #ed6c02 !important;
border: 1px solid #ed6c02 !important;
text-transform: none !important;
font-weight: bold !important;
border-radius: 8px !important;
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/Board/BoardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,24 @@ function BoardList() {
const [rounds, setRounds] = useState<Round[]>([]);
// ローディング状態
const [isLoading, setIsLoading] = useState(true);
// エラー状態
const [fetchError, setFetchError] = useState(false);

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

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

// エラー時の表示
if (fetchError) {
return (
<div className="board-list-empty">
<div className="error-icon-container">
<img src="/Image/Kyoto.jpg" alt="Error" />
</div>
<p className="error-message">データの読み込みに失敗しました</p>
<div className="error-actions">
<Button
variant="contained"
onClick={() => window.location.reload()}
className="board-list-back-button"
>
もう一度試す
</Button>
<Button
variant="outlined"
onClick={() => navigate("/")}
className="board-list-back-button-outlined"
>
タイトルに戻る
</Button>
</div>
</div>
);
}

// データが空の場合の表示
if (rounds.length === 0) {
return (
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/Board/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ function Board() {
const [messages, setMessages] = useState<Message[]>([]);
// ローディング状態
const [isLoading, setIsLoading] = useState(true);
// エラー状態
const [fetchError, setFetchError] = useState(false);
// 答えの表示/非表示
const [isAnswerVisible, setIsAnswerVisible] = useState(false);

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

// 正解をセット
Expand All @@ -87,6 +92,7 @@ function Board() {
setMessages(formattedMessages);
} catch (error) {
console.error("ラウンドデータの取得に失敗しました:", error);
setFetchError(true);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -117,6 +123,50 @@ function Board() {
return <div className="loading-container">読み込み中...</div>;
}

if (fetchError) {
return (
<div className="loading-container">
<div className="error-icon-container">
<img src="/Image/Kyoto.jpg" alt="Error" />
</div>
<p className="error-message">データの読み込みに失敗しました</p>
<div className="error-actions">
<Button
variant="contained"
onClick={() => window.location.reload()}
style={{
width: "200px",
padding: "10px 20px",
borderRadius: "8px",
backgroundColor: "#ed6c02",
color: "#ffffff",
fontWeight: "bold",
textTransform: "none",
}}
>
もう一度試す
</Button>
<Button
variant="outlined"
onClick={() => navigate("/board")}
style={{
width: "200px",
padding: "10px 20px",
borderRadius: "8px",
borderColor: "#ed6c02",
color: "#ed6c02",
backgroundColor: "#ffffff",
fontWeight: "bold",
textTransform: "none",
}}
>
一覧に戻る
</Button>
</div>
</div>
);
}

return (
<div className="board-page-container">
{/* チャット形式のメッセージ表示エリア */}
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/Solo/Solo.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,50 @@
letter-spacing: 0.01em;
}

.error-actions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}

.error-button-primary {
all: unset;
box-sizing: border-box;
cursor: pointer;
padding: 10px 40px;
border-radius: 8px;
background-color: #ed6c02;
color: #ffffff;
font-size: 14px;
font-weight: bold;
text-align: center;
transition: background-color 0.2s;
}

.error-button-primary:hover {
background-color: #e65100;
}

.error-button-outlined {
all: unset;
box-sizing: border-box;
cursor: pointer;
padding: 10px 40px;
border-radius: 8px;
background-color: #ffffff;
color: #ed6c02;
border: 1px solid #ed6c02;
font-size: 14px;
font-weight: bold;
text-align: center;
transition: background-color 0.2s;
}

.error-button-outlined:hover {
background-color: #fff8f0;
}

/* 吹き出しのしっぽ(上向き) */
.loading-text::before {
content: "";
Expand Down
Loading