コミットを比較

...

2 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 0d7757b2df #370 2026-06-15 22:09:10 +09:00
みてるぞ ece95838f0 グカネータ公開 / 洗澡鹿のパス変更 (#361) (#369)
Reviewed-on: #369
Co-authored-by: miteruzo <miteruzo@naver.com>
Co-committed-by: miteruzo <miteruzo@naver.com>
2026-06-14 05:40:31 +09:00
9個のファイルの変更101行の追加281行の削除
バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 559 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 146 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 1.2 MiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 188 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 201 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 196 KiB

バイナリファイルは表示されません.

変更後

幅:  |  高さ:  |  サイズ: 179 KiB

+1 -1
ファイルの表示
@@ -30,7 +30,7 @@ export type GekanatorQuestionSource =
| 'ai_generated'
| 'admin_curated'
export type GekanatorPerformanceMode = 'lite' | 'normal'
export type GekanatorPerformanceMode = 'normal'
export type GekanatorQuestionCondition =
| { type: 'tag'; key: string }
+100 -280
ファイルの表示
@@ -29,7 +29,6 @@ import type { FC } from 'react'
import type { GekanatorAnswerLog,
GekanatorAnswerValue,
GekanatorExtraQuestion,
GekanatorPerformanceMode,
GekanatorQuestionCondition,
GekanatorQuestionKind,
GekanatorQuestion,
@@ -162,17 +161,16 @@ const confidenceTemperature = 6
const gameStorageKey = 'gekanator:game:v1'
const recentGamesStorageKey = 'gekanator:recent-games:v1'
const backgroundMotionStorageKey = 'gekanator:background-motion:v1'
const performanceModeStorageKey = 'gekanator:performance-mode:v1'
const maxQuestionSuggestionsPerGame = 3
const maxStoredRecentGames = 12
const mascotAssetByState: Record<MascotState, string> = {
idle: '/gekanator/mascot-idle.png',
thinking_far: '/gekanator/mascot-thinking-far.png',
thinking_mid: '/gekanator/mascot-thinking-mid.png',
thinking_near: '/gekanator/mascot-thinking-near.png',
confident: '/gekanator/mascot-confident.png',
celebrate: '/gekanator/mascot-celebrate.png',
failed: '/gekanator/mascot-failed.png' }
idle: '/assets/gekanator/mascot-idle.png',
thinking_far: '/assets/gekanator/mascot-thinking-far.png',
thinking_mid: '/assets/gekanator/mascot-thinking-mid.png',
thinking_near: '/assets/gekanator/mascot-thinking-near.png',
confident: '/assets/gekanator/mascot-confident.png',
celebrate: '/assets/gekanator/mascot-celebrate.png',
failed: '/assets/gekanator/mascot-failed.png' }
const mascotAltByState: Record<MascotState, string> = {
idle: '待機する洗澡鹿',
thinking_far: '遠くを見つめる洗澡鹿',
@@ -391,14 +389,8 @@ const storeRecentGameSummary = (
}
const loadBackgroundMotionMode = (
performanceMode?: GekanatorPerformanceMode,
): BackgroundMotionMode => {
const fallbackMode =
performanceMode === 'lite' ? 'off'
: performanceMode === 'normal' ? 'on'
: detectDefaultPerformanceMode () === 'lite' ? 'off'
: 'on'
const loadBackgroundMotionMode = (): BackgroundMotionMode => {
const fallbackMode = 'on'
try
{
const raw = localStorage.getItem (backgroundMotionStorageKey)
@@ -414,38 +406,6 @@ const loadBackgroundMotionMode = (
}
const detectDefaultPerformanceMode = (): GekanatorPerformanceMode => {
if (typeof window === 'undefined')
return 'normal'
const isMobileWidth =
typeof window.matchMedia === 'function'
? window.matchMedia ('(max-width: 767px)').matches
: window.innerWidth <= 767
const memory = (navigator as Navigator & { deviceMemory?: number }).deviceMemory
if ((typeof memory === 'number' && memory <= 4) || isMobileWidth)
return 'lite'
return 'normal'
}
const loadPerformanceMode = (): GekanatorPerformanceMode => {
try
{
const raw = localStorage.getItem (performanceModeStorageKey)
if (raw === 'lite' || raw === 'normal')
return raw
}
catch
{
return detectDefaultPerformanceMode ()
}
return detectDefaultPerformanceMode ()
}
const resettableExtraQuestionState = (): {
extraQuestions: GekanatorExtraQuestion[]
extraQuestionAnswers: Record<string, GekanatorAnswerValue>
@@ -1055,11 +1015,9 @@ const rankedEntriesForCounts = <T extends string | number> (
const buildQuestionsForCandidateIds = (
{ candidateIds,
materialIndex,
performanceMode,
acceptedQuestions }: { candidateIds: number[]
materialIndex: GekanatorQuestionMaterialIndex
performanceMode: GekanatorPerformanceMode
acceptedQuestions: GekanatorQuestion[] },
materialIndex: GekanatorQuestionMaterialIndex
acceptedQuestions: GekanatorQuestion[] },
): GekanatorQuestion[] => {
const total = candidateIds.length
if (total === 0)
@@ -1089,23 +1047,16 @@ const buildQuestionsForCandidateIds = (
const monthDay = materialIndex.originalMonthDayByPostId.get (postId)
if (monthDay)
monthDayCounts.set (monthDay, (monthDayCounts.get (monthDay) ?? 0) + 1)
if (performanceMode === 'normal')
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
materialIndex.titleTermsByPostId.get (postId)?.forEach (term =>
titleTermCounts.set (term, (titleTermCounts.get (term) ?? 0) + 1))
const titleLength = materialIndex.titleLengthByPostId.get (postId) ?? 0
titleLengths.push (titleLength)
if (materialIndex.titleAsciiPostIds.has (postId))
++asciiCount
})
const tagCap =
performanceMode === 'lite'
? total >= 80 ? 96 : 64
: total >= 120 ? 128 : 96
const titleTermCap =
performanceMode === 'lite'
? 0
: total >= 80 ? 10 : total >= 24 ? 14 : 20
const tagCap = total >= 120 ? 128 : 96
const titleTermCap = total >= 80 ? 10 : total >= 24 ? 14 : 20
const factCap = total >= 80 ? 8 : 12
const sortedLengths = [...titleLengths].sort ((a, b) => a - b)
const titleLengthMedian = sortedLengths[Math.floor (sortedLengths.length / 2)] ?? 0
@@ -1181,17 +1132,16 @@ const buildQuestionsForCandidateIds = (
materialIndex }))
})
if (performanceMode === 'normal')
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
.filter (([term]) => String (term).length <= 24)
.forEach (([term]) => {
questions.push (buildIndexedQuestion ({
condition: { type: 'title-contains', text: String (term) },
text: `題名に「${ term }」が含まれる?`,
kind: 'title',
priorityWeight: .96,
materialIndex }))
})
rankedEntriesForCounts ({ counts: titleTermCounts, total, cap: titleTermCap })
.filter (([term]) => String (term).length <= 24)
.forEach (([term]) => {
questions.push (buildIndexedQuestion ({
condition: { type: 'title-contains', text: String (term) },
text: `題名に「${ term }」が含まれる?`,
kind: 'title',
priorityWeight: .96,
materialIndex }))
})
return mergeQuestions ([...questions, ...acceptedQuestions])
}
@@ -1615,41 +1565,33 @@ const contradictionPenaltyFor = ({
}
const chooseQuestion = ({
posts,
questions,
scores,
answers,
askedIds,
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex,
}: {
posts: Post[]
questions: GekanatorQuestion[]
scores: Map<number, number>
answers: GekanatorAnswerLog[]
askedIds: Set<string>
gameSeed: string
recentFirstQuestionPenaltyById: Map<string, number>
userPriorWeights: Map<number, number>
performanceMode: GekanatorPerformanceMode
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
}): GekanatorQuestion | null => {
const candidateIds = posts.map (post => post.id)
const candidateIdSet = new Set (candidateIds)
const chooseQuestion = (
{ posts,
questions,
scores,
answers,
askedIds,
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
materialIndex,
matchIndex }: { posts: Post[]
questions: GekanatorQuestion[]
scores: Map<number, number>
answers: GekanatorAnswerLog[]
askedIds: Set<string>
gameSeed: string
recentFirstQuestionPenaltyById: Map<string, number>
userPriorWeights: Map<number, number>
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex },
): GekanatorQuestion | null => {
const dynamicMatchIndex = new Map<string, Set<number>> ()
const invertedSignature = (signature: string): string =>
signature.replace (/[01]/g, value => value === '1' ? '0' : '1')
const redundantSignatures = (
candidates: Post[],
): Set<string> => {
const redundantSignatures = (candidates: Post[]): Set<string> => {
const signatures = new Set<string> ()
questions
.filter (question => askedIds.has (question.id))
@@ -1668,89 +1610,6 @@ const chooseQuestion = ({
return signatures
}
if (performanceMode === 'lite')
{
const nonTagCount =
questions.filter (question => askedIds.has (question.id) && question.kind !== 'tag').length
const ranked = questions
.filter (question => !(askedIds.has (question.id)))
.map (question => {
if (isQuestionHardFilteredAfterAnswers (question, answers))
return null
const yes = matchingPostCountInIds ({
candidateIds,
candidateIdSet,
posts,
materialIndex,
matchIndex,
question,
dynamicMatchIndex })
const no = posts.length - yes
if (yes === 0 || no === 0)
return null
const splitScore = Math.abs (posts.length / 2 - yes) / posts.length
const minSide = posts.length < 10 ? 1 : Math.max (2, Math.floor (posts.length * .08))
const narrowPenalty = yes < minSide || no < minSide ? .18 : 0
const tagPenalty = question.kind === 'tag' && nonTagCount < 3 ? .1 : 0
const contradictionPenalty = contradictionPenaltyFor ({ question, answers })
const sourceBonus = sourcePriorityOffset (question)
const priorityBonus = priorityWeightOffset (question)
const categoryPenalty = questionCategoryPenalty (question, answers.length, 0)
return {
question,
score: splitScore * 100
+ narrowPenalty
+ tagPenalty
+ contradictionPenalty
+ sourceBonus
+ priorityBonus
+ categoryPenalty,
narrow: narrowPenalty > 0 }
})
.filter ((item): item is {
question: GekanatorQuestion
score: number
narrow: boolean } => item !== null && Number.isFinite (item.score))
.sort ((a, b) => {
if (a.score !== b.score)
return a.score - b.score
return a.question.id.localeCompare (b.question.id)
})
const pool = (
ranked.some (item => !(item.narrow))
? ranked.filter (item => !(item.narrow))
: ranked)
.slice (0, 8)
if (pool.length === 0)
return null
const bestScore = pool[0]?.score ?? 0
const weightedPool = pool.map (item => ({
...item,
weight: Math.exp ((bestScore - item.score) / 1.6) }))
const totalPoolWeight =
weightedPool.reduce ((sum, item) => sum + item.weight, 0) || 1
const seed = `${ gameSeed }:lite:${ [...askedIds].sort ().join ('|') }:${
weightedPool.map (item => `${ item.question.id }:${ item.score.toFixed (4) }`).join ('|')
}`
const target = deterministicUnitFloat (seed) * totalPoolWeight
let cumulative = 0
for (const item of weightedPool)
{
cumulative += item.weight
if (target <= cumulative)
return item.question
}
return weightedPool[weightedPool.length - 1]?.question ?? null
}
const scoredPosts = posts
.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
.sort ((a, b) => b.score - a.score)
@@ -2361,7 +2220,6 @@ const nextQuestionPlanFor = (
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex,
lastGuessQuestionCount,
@@ -2376,7 +2234,6 @@ const nextQuestionPlanFor = (
gameSeed: string
recentFirstQuestionPenaltyById: Map<string, number>
userPriorWeights: Map<number, number>
performanceMode: GekanatorPerformanceMode
materialIndex: GekanatorQuestionMaterialIndex
matchIndex: GekanatorMatchIndex
lastGuessQuestionCount: number
@@ -2439,7 +2296,6 @@ const nextQuestionPlanFor = (
buildQuestionsForCandidateIds ({
candidateIds: scopePosts.map (post => post.id),
materialIndex,
performanceMode,
acceptedQuestions })
if (eligiblePosts.length === 1)
@@ -2505,7 +2361,6 @@ const nextQuestionPlanFor = (
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex })
@@ -3108,10 +2963,8 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const canPersistGame = user !== null
const [recentGames, setRecentGames] = useState<RecentGameSummary[]> (
() => loadRecentGames ())
const [performanceMode] =
useState<GekanatorPerformanceMode> (() => loadPerformanceMode ())
const [backgroundMotionMode, setBackgroundMotionMode] = useState<BackgroundMotionMode> (
() => loadBackgroundMotionMode (loadPerformanceMode ()))
() => loadBackgroundMotionMode ())
const [prefersReducedMotion, setPrefersReducedMotion] = useState (false)
const [gameSeed, setGameSeed] = useState (
storedGame?.gameSeed ?? createGameSeed ())
@@ -3328,17 +3181,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
}
}, [backgroundMotionMode])
useEffect (() => {
try
{
localStorage.setItem (performanceModeStorageKey, performanceMode)
}
catch
{
return
}
}, [performanceMode])
const askedQuestionById = useMemo (
() => new Map (askedQuestionBank.map (question => [question.id, question])),
[askedQuestionBank])
@@ -3361,9 +3203,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
() => new Map (scoringQuestions.map (question => [question.id, question])),
[scoringQuestions])
const recentFirstQuestionPenaltyById = useMemo (() => {
if (performanceMode === 'lite')
return new Map<string, number> ()
const penalties = new Map<string, number> ()
recentGames.forEach ((game, index) => {
@@ -3376,12 +3215,10 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
})
return penalties
}, [performanceMode, recentGames])
}, [recentGames])
const userPriorWeights = useMemo (
() => performanceMode === 'lite'
? new Map<number, number> ()
: userPriorWeightsFor (posts, recentGames),
[performanceMode, posts, recentGames])
() => userPriorWeightsFor (posts, recentGames),
[posts, recentGames])
const availablePosts = useMemo (
() => posts.filter (post => !(rejectedPostIds.has (post.id))),
[posts, rejectedPostIds])
@@ -3397,7 +3234,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex: acceptedQuestionMatchIndex,
lastGuessQuestionCount,
@@ -3405,7 +3241,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
winningRunStartAnswerCount }),
[posts, eligiblePosts, availablePosts, acceptedQuestions, scores,
answers, askedIds, gameSeed, recentFirstQuestionPenaltyById,
userPriorWeights, performanceMode, materialIndex, acceptedQuestionMatchIndex,
userPriorWeights, materialIndex, acceptedQuestionMatchIndex,
lastGuessQuestionCount, winningRunTargetId, winningRunStartAnswerCount])
const winningRunTargetPost = useMemo (
() => questionPlan.winningRunTargetId === null
@@ -3458,35 +3294,28 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
const reviewCorrectPost =
posts.find (post => post.id === reviewCorrectPostId) ?? null
const effectiveResultWon =
resultWon ?? (
reviewGuessedPostId !== null
&& reviewCorrectPostId !== null
? reviewGuessedPostId === reviewCorrectPostId
: null)
resultWon
?? ((reviewGuessedPostId !== null && reviewCorrectPostId !== null)
? reviewGuessedPostId === reviewCorrectPostId
: null)
const effectiveBackgroundMotionMode =
performanceMode === 'lite'
? 'off'
: backgroundMotionMode === 'off'
? 'off'
: prefersReducedMotion
? 'calm'
: backgroundMotionMode
backgroundMotionMode === 'off'
? 'off'
: (prefersReducedMotion
? 'calm'
: backgroundMotionMode)
const backgroundPosts = useMemo (
() => performanceMode === 'lite'
? []
: backgroundPostsFor ({
() => backgroundPostsFor ({
phase,
eligiblePosts,
availablePosts,
displayedGuess,
reviewCorrectPost,
reviewGuessedPost }),
[performanceMode, phase, eligiblePosts, availablePosts, displayedGuess,
reviewCorrectPost, reviewGuessedPost])
[phase, eligiblePosts, availablePosts, displayedGuess, reviewCorrectPost,
reviewGuessedPost])
const backgroundVisualSeed =
performanceMode === 'lite'
? ''
: `${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
`${ gameSeed }:${ phase }:${ answers.length }:${ activeGuessId ?? '' }:${
questionPlan.question?.id ?? ''
}:${ questionPlan.questionMode ?? '' }:${ winningRunQuestionsAsked }:${
rejectedPostIds.size
@@ -3617,7 +3446,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
let recoveredQuestions = buildQuestionsForCandidateIds ({
candidateIds: recoveredEligiblePosts.map (post => post.id),
materialIndex,
performanceMode,
acceptedQuestions })
let recoveredScoringQuestions = mergeQuestions ([
...recoveredQuestions,
@@ -3643,7 +3471,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
recoveredQuestions = buildQuestionsForCandidateIds ({
candidateIds: recoveredEligiblePosts.map (post => post.id),
materialIndex,
performanceMode,
acceptedQuestions })
recoveredScoringQuestions = mergeQuestions ([
...recoveredQuestions,
@@ -3667,7 +3494,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex: acceptedQuestionMatchIndex })
const fallbackQuestion = nextQuestion ?? chooseFallbackQuestion ({
@@ -3735,7 +3561,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
}, [
posts,
gameSeed,
performanceMode,
materialIndex,
acceptedQuestions,
acceptedQuestionMatchIndex,
@@ -3806,7 +3631,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex: acceptedQuestionMatchIndex,
lastGuessQuestionCount,
@@ -3840,7 +3664,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex: acceptedQuestionMatchIndex,
lastGuessQuestionCount,
@@ -4071,7 +3894,6 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
gameSeed,
recentFirstQuestionPenaltyById,
userPriorWeights,
performanceMode,
materialIndex,
matchIndex: acceptedQuestionMatchIndex,
lastGuessQuestionCount,
@@ -4286,16 +4108,15 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
<title>{`グカネータ | ${ SITE_TITLE }`}</title>
</Helmet>
{performanceMode !== 'lite' && (
<GekanatorBackdrop
posts={backgroundPosts}
mascotAsset={mascotAsset}
phase={phase}
displayedGuess={displayedGuess}
visualSeed={backgroundVisualSeed}
motionMode={effectiveBackgroundMotionMode}
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
winningRunQuestionCount={winningRunQuestionsAsked}/>)}
<GekanatorBackdrop
posts={backgroundPosts}
mascotAsset={mascotAsset}
phase={phase}
displayedGuess={displayedGuess}
visualSeed={backgroundVisualSeed}
motionMode={effectiveBackgroundMotionMode}
winningRunTargetPost={winningRunActive ? winningRunTargetPost : null}
winningRunQuestionCount={winningRunQuestionsAsked}/>
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
<header className="flex flex-wrap items-end justify-between gap-3">
@@ -4305,32 +4126,31 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
</h1>
</div>
<div className="flex flex-wrap justify-end gap-2">
{performanceMode === 'normal' && (
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
text-xs shadow-sm backdrop-blur dark:border-red-800
dark:bg-red-950/75">
<span className="mr-2 font-bold text-neutral-600 dark:text-neutral-300">
</span>
{[{ mode: 'off' as const, label: 'オ' },
{ mode: 'on' as const, label: 'オン' }]
.map (({ mode, label }) => (
<button
key={mode}
type="button"
className={cn (
'rounded-full px-2.5 py-1 transition-colors',
backgroundMotionMode === mode
? 'bg-pink-600 text-white'
: 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900')}
onClick={() => setBackgroundMotionMode (mode)}>
{label}
</button>))}
{prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && (
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
</span>)}
</div>)}
<div className="rounded-full border border-yellow-300 bg-white/80 px-2 py-1
text-xs shadow-sm backdrop-blur dark:border-red-800
dark:bg-red-950/75">
<span className="mr-2 font-bold text-neutral-600 dark:text-neutral-300">
</span>
{[{ mode: 'off' as const, label: 'オフ' },
{ mode: 'on' as const, label: 'オ' }]
.map (({ mode, label }) => (
<button
key={mode}
type="button"
className={cn (
'rounded-full px-2.5 py-1 transition-colors',
backgroundMotionMode === mode
? 'bg-pink-600 text-white'
: 'text-neutral-600 hover:bg-yellow-100 dark:text-neutral-300 dark:hover:bg-red-900')}
onClick={() => setBackgroundMotionMode (mode)}>
{label}
</button>))}
{prefersReducedMotion && effectiveBackgroundMotionMode !== 'off' && (
<span className="ml-2 text-[11px] text-neutral-500 dark:text-neutral-400">
</span>)}
</div>
</div>
</header>
@@ -4608,7 +4428,7 @@ const GekanatorPage: FC<{ user: User | null }> = ({ user }) => {
{reviewGuessedPostId !== null && reviewCorrectPostId !== null && (
<p className="text-sm text-neutral-600 dark:text-neutral-300">
: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : '違ひ'}
: {reviewGuessedPostId === reviewCorrectPostId ? '当たり' : 'はずれ'}
</p>)}
{saveMutation.isError && (