Skip to content

Commit 2fda98a

Browse files
committed
Merge branch 'develop' into release/2
2 parents 9c2fcf7 + d1f0597 commit 2fda98a

File tree

15 files changed

+502
-128
lines changed

15 files changed

+502
-128
lines changed

README.md

Lines changed: 104 additions & 57 deletions
Large diffs are not rendered by default.

backend/dto/game.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ type StartGameResult struct {
1515
}
1616

1717
type CheckAnswerRequest struct {
18-
Answer string `json:"answer" binding:"required"`
18+
Answer string `json:"answer" binding:"required,max=200"`
1919
}

backend/repositories/hint_repository.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type IHintRepository interface {
1515
BookmarkRound(id uint) error
1616
GetRandomBookmarkedRound() (*models.Round, error)
1717
GetAllBookmarkedRounds() ([]models.Round, error)
18+
GetRecentAnswers(limit int) ([]string, error)
1819
}
1920

2021
type HintRepository struct {
@@ -79,3 +80,17 @@ func (r *HintRepository) GetAllBookmarkedRounds() ([]models.Round, error) {
7980
}
8081
return rounds, nil
8182
}
83+
84+
func (r *HintRepository) GetRecentAnswers(limit int) ([]string, error) {
85+
var rounds []models.Round
86+
if err := r.db.Order("created_at desc").Limit(limit).Find(&rounds).Error; err != nil {
87+
return nil, err
88+
}
89+
answers := make([]string, 0, len(rounds))
90+
for _, round := range rounds {
91+
if len(round.Answers) > 0 {
92+
answers = append(answers, round.Answers[0])
93+
}
94+
}
95+
return answers, nil
96+
}

backend/services/hint_service.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ import (
1515
"google.golang.org/genai"
1616
)
1717

18-
const MinRoundAnswerRevealDuration = 90 * time.Second
18+
const (
19+
MinRoundAnswerRevealDuration = 90 * time.Second
20+
RecentAnswersLimit = 30
21+
)
1922

2023
var (
21-
ErrRoundNotFound = errors.New("round not found")
22-
ErrRoundNotFinished = errors.New("round is not finished yet")
23-
ErrRoundTooEarly = errors.New("game has not been played long enough")
24-
ErrBookmarkNotFound = errors.New("bookmark not found")
24+
ErrRoundNotFound = errors.New("round not found")
25+
ErrRoundNotFinished = errors.New("round is not finished yet")
26+
ErrRoundTooEarly = errors.New("game has not been played long enough")
27+
ErrBookmarkNotFound = errors.New("bookmark not found")
28+
ErrNoAnswersAvailable = errors.New("no answers available")
2529
)
2630

2731
type IHintService interface {
@@ -53,7 +57,23 @@ func (s *HintService) StartGame() (*dto.StartGameResult, error) {
5357
return nil, fmt.Errorf("failed to create Gemini client: %w", err)
5458
}
5559

56-
const prompt = `
60+
// 過去の出題済みお題を取得(直近30件)
61+
pastAnswers, err := s.repository.GetRecentAnswers(RecentAnswersLimit)
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to get recent answers: %w", err)
64+
}
65+
66+
excludeSection := ""
67+
if len(pastAnswers) > 0 {
68+
excludeSection = fmt.Sprintf(`
69+
70+
# 禁止お題
71+
以下のお題は過去に出題済みなので、絶対に使わないでください:
72+
%s
73+
`, strings.Join(pastAnswers, "、"))
74+
}
75+
76+
const basePrompt = `
5777
# Role
5878
あなたは京都(上品・皮肉)と大阪(効率・本音)の個性を完璧に描き分ける脚本家であり、厳密なJSONデータを出力するシステムです。
5979
@@ -169,10 +189,12 @@ func (s *HintService) StartGame() (*dto.StartGameResult, error) {
169189
},
170190
}
171191

192+
fullPrompt := basePrompt + excludeSection
193+
172194
res, err := client.Models.GenerateContent(
173195
ctx,
174196
"gemini-2.5-flash",
175-
genai.Text(prompt),
197+
genai.Text(fullPrompt),
176198
config,
177199
)
178200
if err != nil {
@@ -186,8 +208,6 @@ func (s *HintService) StartGame() (*dto.StartGameResult, error) {
186208

187209
text := res.Text()
188210

189-
fmt.Println(text)
190-
191211
if err := json.Unmarshal([]byte(text), &geminiData); err != nil {
192212
return nil, fmt.Errorf("JSONパースに失敗しました: %w (raw: %s)", err, text)
193213
}
@@ -221,6 +241,9 @@ func (s *HintService) GetAnswer(id uint) (string, error) {
221241
if err != nil {
222242
return "", err
223243
}
244+
if len(round.Answers) == 0 {
245+
return "", fmt.Errorf("%w: id=%d", ErrNoAnswersAvailable, id)
246+
}
224247
if !round.IsFinished {
225248
if time.Since(round.CreatedAt) < MinRoundAnswerRevealDuration {
226249
return "", fmt.Errorf("%w", ErrRoundTooEarly)
@@ -257,6 +280,9 @@ func (s *HintService) GetFinishedRoundByID(id uint) (*dto.RoundResponse, error)
257280
if !round.IsFinished {
258281
return nil, fmt.Errorf("%w: id=%d", ErrRoundNotFinished, id)
259282
}
283+
if len(round.Answers) == 0 {
284+
return nil, fmt.Errorf("%w: id=%d", ErrNoAnswersAvailable, id)
285+
}
260286
result := &dto.RoundResponse{
261287
ID: round.ID,
262288
Answer: round.Answers[0],
@@ -274,6 +300,9 @@ func (s *HintService) BookmarkRound(id uint) (*dto.RoundResponse, error) {
274300
if !round.IsFinished {
275301
return nil, fmt.Errorf("%w: id=%d", ErrRoundNotFinished, id)
276302
}
303+
if len(round.Answers) == 0 {
304+
return nil, fmt.Errorf("%w: id=%d", ErrNoAnswersAvailable, id)
305+
}
277306
if err := s.repository.BookmarkRound(id); err != nil {
278307
return nil, fmt.Errorf("failed to bookmark round: %w", err)
279308
}
@@ -295,6 +324,9 @@ func (s *HintService) GetRandomBookmark() (*dto.RoundResponse, error) {
295324
return nil, ErrBookmarkNotFound
296325
}
297326

327+
if len(round.Answers) == 0 {
328+
return nil, ErrNoAnswersAvailable
329+
}
298330
return &dto.RoundResponse{
299331
ID: round.ID,
300332
Answer: round.Answers[0],
@@ -312,6 +344,9 @@ func (s *HintService) GetBookmarkedList() ([]dto.RoundResponse, error) {
312344
response := []dto.RoundResponse{}
313345

314346
for _, r := range rounds {
347+
if len(r.Answers) == 0 {
348+
continue
349+
}
315350
limit := 4
316351
if len(r.Hints) < limit {
317352
limit = len(r.Hints)

frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ function App() {
3636
onClick={() => navigate("/board")}
3737
className="title-base-button btn-story"
3838
>
39-
共有されたストーリー見る
39+
みんなのストーリーを見る
4040
</Button>
4141
</div>
4242
</Paper>

frontend/src/Board/Board.css

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.board-page-container {
22
width: 100%;
3-
height: 100vh;
3+
height: 100dvh;
44
margin: 0;
55
padding: 20px 40px;
66
display: flex;
@@ -51,13 +51,50 @@
5151

5252
.loading-container {
5353
width: 100%;
54-
height: 100vh;
54+
height: 100dvh;
5555
display: flex;
56+
flex-direction: column;
5657
justify-content: center;
5758
align-items: center;
5859
font-size: 24px;
5960
color: #555;
6061
background-color: #f5f5f5;
62+
gap: 12px;
63+
}
64+
65+
.error-icon-container {
66+
width: 80px;
67+
height: 80px;
68+
border-radius: 50%;
69+
overflow: hidden;
70+
display: flex;
71+
align-items: center;
72+
justify-content: center;
73+
background: #ffffff;
74+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
75+
border: 3px solid #fff;
76+
margin-bottom: 8px;
77+
}
78+
79+
.error-icon-container img {
80+
width: 100%;
81+
height: 100%;
82+
object-fit: cover;
83+
transform: scale(1.7);
84+
}
85+
86+
.error-message {
87+
font-size: 16px;
88+
font-weight: 600;
89+
color: #666;
90+
margin: 0;
91+
}
92+
93+
.error-actions {
94+
display: flex;
95+
flex-direction: column;
96+
gap: 8px;
97+
margin-top: 8px;
6198
}
6299

63100
/* Material Design Color Tokens */

frontend/src/Board/BoardList.css

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@
111111
align-items: center;
112112
}
113113

114+
.board-card-story-id {
115+
display: inline-block;
116+
background-color: #fff4e6;
117+
color: #ed6c02;
118+
font-size: 0.7rem;
119+
font-weight: 700;
120+
letter-spacing: 0.03em;
121+
padding: 2px 10px;
122+
border-radius: 12px;
123+
margin-bottom: 10px;
124+
}
125+
114126
.board-card-date {
115127
color: #999;
116128
font-size: 0.75rem;
@@ -133,21 +145,66 @@
133145
background-color: #faf7f2;
134146
}
135147

136-
/* --- 空状態 --- */
148+
/* --- 空状態・エラー状態 --- */
137149
.board-list-empty {
138150
min-height: 100vh;
139151
display: flex;
140152
flex-direction: column;
141153
justify-content: center;
142154
align-items: center;
143-
gap: 20px;
155+
gap: 12px;
144156
background-color: #faf7f2;
145157
color: #888;
146158
font-size: 1.1rem;
147159
}
148160

161+
.board-list-empty .error-icon-container {
162+
width: 80px;
163+
height: 80px;
164+
border-radius: 50%;
165+
overflow: hidden;
166+
display: flex;
167+
align-items: center;
168+
justify-content: center;
169+
background: #ffffff;
170+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
171+
border: 3px solid #fff;
172+
margin-bottom: 4px;
173+
}
174+
175+
.board-list-empty .error-icon-container img {
176+
width: 100%;
177+
height: 100%;
178+
object-fit: cover;
179+
transform: scale(1.7);
180+
}
181+
182+
.board-list-empty .error-message {
183+
font-size: 16px;
184+
font-weight: 600;
185+
color: #666;
186+
margin: 0;
187+
}
188+
189+
.board-list-empty .error-actions {
190+
display: flex;
191+
flex-direction: column;
192+
gap: 8px;
193+
margin-top: 4px;
194+
}
195+
149196
.board-list-back-button {
150197
background-color: #ed6c02 !important;
198+
color: #ffffff !important;
199+
text-transform: none !important;
200+
font-weight: bold !important;
201+
border-radius: 8px !important;
202+
}
203+
204+
.board-list-back-button-outlined {
205+
background-color: #ffffff !important;
206+
color: #ed6c02 !important;
207+
border: 1px solid #ed6c02 !important;
151208
text-transform: none !important;
152209
font-weight: bold !important;
153210
border-radius: 8px !important;

frontend/src/Board/BoardList.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,25 @@ 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

54-
// results配列をそのままセット
55-
setRounds(data.results);
59+
const sortedRounds = data.results.sort((a, b) => b.id - a.id);
60+
setRounds(sortedRounds);
61+
// results配列を降順にセット
5662
} catch (error) {
5763
console.error("ラウンド一覧の取得に失敗しました:", error);
64+
setFetchError(true);
5865
} finally {
5966
setIsLoading(false);
6067
}
@@ -80,6 +87,34 @@ function BoardList() {
8087
return <div className="board-list-loading">読み込み中...</div>;
8188
}
8289

90+
// エラー時の表示
91+
if (fetchError) {
92+
return (
93+
<div className="board-list-empty">
94+
<div className="error-icon-container">
95+
<img src="/Image/Kyoto.jpg" alt="Error" />
96+
</div>
97+
<p className="error-message">データの読み込みに失敗しました</p>
98+
<div className="error-actions">
99+
<Button
100+
variant="contained"
101+
onClick={() => window.location.reload()}
102+
className="board-list-back-button"
103+
>
104+
もう一度試す
105+
</Button>
106+
<Button
107+
variant="outlined"
108+
onClick={() => navigate("/")}
109+
className="board-list-back-button-outlined"
110+
>
111+
タイトルに戻る
112+
</Button>
113+
</div>
114+
</div>
115+
);
116+
}
117+
83118
// データが空の場合の表示
84119
if (rounds.length === 0) {
85120
return (
@@ -120,6 +155,9 @@ function BoardList() {
120155
className="board-card"
121156
onClick={() => navigate(`/board/${round.id}`)}
122157
>
158+
{/* ストーリーID */}
159+
<div className="board-card-story-id">ストーリー #{round.id}</div>
160+
123161
{/* ヒントのプレビュー(チャット形式で最初の数個を表示) */}
124162
<div className="board-card-hints">
125163
{round.hints.slice(0, PREVIEW_HINT_COUNT).map((hint, index) => (

0 commit comments

Comments
 (0)