feat(boki): 弱点特訓モード・3ステップ学習・概念折りたたみを追加

- 機能1: 弱点集中特訓モード(最大5単元、bestScore昇順、クリア率表示)
- 機能2: 単元内3ステップ学習(keypoints FC→借貸2択→クイズ、全23単元ドリル)
- 機能3: 概念の1段落折りたたみ(Alpine.js x-show、もっと詳しくトグル)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mai 2026-04-19 14:11:21 +09:00
parent 061d654d37
commit 152e248f46
1 changed files with 533 additions and 6 deletions

View File

@ -333,6 +333,43 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
.vo-hseg{height:2px;width:60px;background:var(--border)} .vo-hseg{height:2px;width:60px;background:var(--border)}
.vo-pct{font-size:10px;color:var(--text3);background:var(--surface2);border:1px solid var(--border);border-radius:20px;padding:2px 7px;white-space:nowrap;margin-bottom:2px} .vo-pct{font-size:10px;color:var(--text3);background:var(--surface2);border:1px solid var(--border);border-radius:20px;padding:2px 7px;white-space:nowrap;margin-bottom:2px}
.vo-connector{display:flex;flex-direction:column;align-items:center} .vo-connector{display:flex;flex-direction:column;align-items:center}
/* Weak drill mode */
.drill-unit-lbl{font-size:10px;font-weight:600;color:var(--text3);letter-spacing:.07em;text-transform:uppercase;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
.drill-prog-wrap{margin-bottom:14px}
.drill-prog-label{font-size:10px;color:var(--text3);display:flex;justify-content:space-between;margin-bottom:4px}
.drill-prog-bar{height:3px;background:var(--surface2);border-radius:2px;overflow:hidden}
.drill-prog-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .3s}
.drill-result{text-align:center;padding:20px 0}
.drill-result-pct{font-family:'JetBrains Mono',monospace;font-size:42px;font-weight:500;color:var(--accent)}
.drill-result-sub{font-size:11px;color:var(--text3);letter-spacing:.05em;text-transform:uppercase;margin-top:4px}
.drill-result-detail{font-size:13px;color:var(--text2);margin-top:14px;line-height:1.7}
.drill-unit-row{display:flex;align-items:center;justify-content:space-between;padding:7px 12px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);margin-bottom:5px;font-size:12px}
.drill-unit-row-title{color:var(--text2);flex:1}
.drill-unit-row-ok{color:var(--ok);font-weight:600;font-family:'JetBrains Mono',monospace;font-size:11px}
.drill-unit-row-ng{color:var(--err);font-weight:600;font-family:'JetBrains Mono',monospace;font-size:11px}
/* Step mode */
.step-pips{display:flex;gap:6px;margin-bottom:18px}
.step-pip{height:4px;flex:1;border-radius:2px;background:var(--border);transition:background .3s}
.step-pip.s-active{background:var(--accent)}
.step-pip.s-done{background:rgba(74,222,128,.5)}
.flash-card{background:var(--surface2);border:2px solid var(--border);border-radius:var(--radius);padding:22px 18px;text-align:center;min-height:120px;display:flex;flex-direction:column;align-items:center;justify-content:center;cursor:pointer;transition:border-color .2s,background .15s;margin-bottom:14px;user-select:none}
.flash-card:hover{border-color:var(--accent-border);background:var(--accent-dim)}
.flash-card-front{font-size:12px;color:var(--text3);letter-spacing:.04em;margin-bottom:4px}
.flash-card-back{font-size:13px;color:var(--text);line-height:1.75;padding-top:12px;border-top:1px solid var(--border);width:100%;margin-top:10px}
.step-count{font-size:10px;color:var(--text3);text-align:center;margin-bottom:10px;letter-spacing:.04em}
.s2choice{display:flex;gap:10px;margin-bottom:10px}
.s2btn{flex:1;padding:16px 10px;border-radius:var(--radius);border:2px solid var(--border);background:var(--surface);font-size:14px;font-weight:600;color:var(--text2);cursor:pointer;transition:border-color .2s,background .15s,color .15s;font-family:'Geist',sans-serif;line-height:1.35}
.s2btn:hover:not(:disabled){border-color:var(--accent-border);color:var(--accent);background:var(--accent-dim)}
.s2btn:disabled{cursor:default}
.s2btn.s2-ok{border-color:rgba(74,222,128,.5) !important;background:rgba(74,222,128,.1) !important;color:var(--ok) !important}
.s2btn.s2-ng{border-color:rgba(248,113,113,.4) !important;background:rgba(248,113,113,.08) !important;color:var(--err) !important}
.s2-exp{font-size:12px;color:var(--text2);padding:8px 11px;background:rgba(34,211,238,.05);border:1px solid var(--accent-border);border-radius:var(--radius-sm);margin-bottom:10px;line-height:1.65}
.s2-exp strong{color:var(--accent)}
.step-action-row{display:flex;gap:8px;justify-content:flex-end;margin-top:4px}
/* Concept expand */
.concept-expand-btn{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--accent);background:none;border:none;cursor:pointer;padding:4px 0 0;font-family:'Geist',sans-serif;font-weight:500}
.concept-expand-btn:hover{opacity:.75}
</style> </style>
</head> </head>
<body> <body>
@ -404,7 +441,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
<main id="main"> <main id="main">
<!-- HOME --> <!-- HOME -->
<div x-show="!currentUnit"> <div x-show="!currentUnit && !weakDrillActive">
<div class="home-hero"> <div class="home-hero">
<h1>簿記<span>2級</span>学習アプリ</h1> <h1>簿記<span>2級</span>学習アプリ</h1>
<p>商業簿記・工業簿記を単元ごとに学習し、理解度チェックで定着させましょう。3級の基礎から丁寧に復習できます。</p> <p>商業簿記・工業簿記を単元ごとに学習し、理解度チェックで定着させましょう。3級の基礎から丁寧に復習できます。</p>
@ -438,6 +475,15 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div> </div>
</template> </template>
</div> </div>
<!-- Weak drill button -->
<div style="margin-bottom:20px;display:flex;justify-content:center">
<button class="btn-sm" :class="weakDrillCandidates.length ? 'btn-accent' : ''"
:disabled="!weakDrillCandidates.length"
@click="weakDrillCandidates.length && startWeakDrill()">
<i data-lucide="zap" style="width:12px;height:12px"></i>
<span x-text="weakDrillCandidates.length ? '弱点特訓(' + weakDrillCandidates.length + '単元)' : '弱点なし'"></span>
</button>
</div>
<div class="home-cats"> <div class="home-cats">
<template x-for="cat in categories" :key="cat.id"> <template x-for="cat in categories" :key="cat.id">
<div class="home-cat-card" @click="openUnit(cat.units[0])"> <div class="home-cat-card" @click="openUnit(cat.units[0])">
@ -454,8 +500,95 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div> </div>
</div> </div>
<!-- WEAK DRILL -->
<div x-show="weakDrillActive">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="zap" style="width:13px;height:13px"></i>
弱点集中特訓
</div>
<button class="btn-sm" @click="exitWeakDrill()">
<i data-lucide="x" style="width:11px;height:11px"></i>
中断
</button>
</div>
<!-- Summary after all done -->
<div x-show="weakDrillDone" class="drill-result">
<div class="drill-result-pct" x-text="weakDrillClearCount + ' / ' + weakDrillResults.length"></div>
<div class="drill-result-sub">単元クリア</div>
<div class="drill-result-detail">
<template x-for="r in weakDrillResults" :key="r.unitId">
<div class="drill-unit-row">
<span class="drill-unit-row-title" x-text="r.num + ' ' + r.unitTitle"></span>
<span :class="r.correct >= r.total ? 'drill-unit-row-ok' : 'drill-unit-row-ng'"
x-text="r.correct + '/' + r.total"></span>
</div>
</template>
</div>
<div class="result-actions" style="margin-top:18px">
<button class="btn-sm btn-accent" @click="exitWeakDrill()">
<i data-lucide="home" style="width:11px;height:11px"></i>
ホームへ
</button>
</div>
</div>
<!-- Active drill -->
<div x-show="!weakDrillDone && weakDrillCurrentUnit">
<div class="drill-prog-wrap">
<div class="drill-prog-label">
<span x-text="'単元 ' + (weakDrillUnitIdx+1) + ' / ' + weakDrillUnits.length"></span>
<span x-text="weakDrillCurrentUnit?.num"></span>
</div>
<div class="drill-prog-bar">
<div class="drill-prog-fill" :style="'width:' + ((weakDrillUnitIdx)/weakDrillUnits.length*100) + '%'"></div>
</div>
</div>
<div class="drill-unit-lbl">
<span x-text="weakDrillCurrentUnit?.catLabel + ' — ' + weakDrillCurrentUnit?.title"></span>
</div>
<template x-for="(q, qi) in (weakDrillCurrentUnit?.quiz || [])" :key="qi">
<div class="q-card" :class="wdQCardCls(qi)">
<div class="q-num">
<i data-lucide="help-circle" style="width:11px;height:11px"></i>
<span x-text="qi+1"></span>
</div>
<div class="q-text" x-html="q.q"></div>
<div class="q-choices">
<template x-for="(ch, ci) in q.choices" :key="ci">
<button class="q-choice"
:class="wdChoiceCls(qi, ci, q.answer)"
:disabled="wdAnswered(qi)"
@click="doWeakDrillAnswer(qi, ci, q.answer, q.exp)">
<span class="choice-key" x-text="keys[ci]"></span>
<span x-html="ch"></span>
</button>
</template>
</div>
<div class="q-exp" x-show="wdAnswered(qi)">
<strong>解説:</strong> <span x-html="weakDrillQuizState[qi]?.exp"></span>
</div>
</div>
</template>
<div class="quiz-result" x-show="weakDrillAllAnswered" style="margin-top:14px">
<div class="result-score" x-text="weakDrillUnitScore"></div>
<div class="result-lbl">正解 / 全問</div>
<div class="result-actions">
<button class="btn-sm btn-accent" @click="nextWeakDrillUnit()"
x-text="weakDrillUnitIdx < weakDrillUnits.length - 1 ? '次の単元へ' : '結果を見る'">
</button>
</div>
</div>
</div>
</div>
</div>
<!-- UNIT VIEW --> <!-- UNIT VIEW -->
<div x-show="currentUnit"> <div x-show="currentUnit && !weakDrillActive">
<div class="unit-header"> <div class="unit-header">
<button class="unit-back sidebar-toggle" @click="goHome()"> <button class="unit-back sidebar-toggle" @click="goHome()">
<i data-lucide="arrow-left" style="width:12px;height:12px"></i> <i data-lucide="arrow-left" style="width:12px;height:12px"></i>
@ -471,13 +604,116 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div> </div>
</div> </div>
<!-- Study mode buttons -->
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap" x-show="currentUnit && !stepMode">
<button class="btn-sm btn-accent" @click="startStepMode()"
x-show="currentUnit?.keypoints?.length">
<i data-lucide="layers" style="width:11px;height:11px"></i>
3ステップで学ぶ
</button>
</div>
<!-- Step mode card -->
<div class="card" x-show="stepMode" style="margin-bottom:14px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="layers" style="width:13px;height:13px"></i>
3ステップで学ぶ
</div>
<button class="btn-sm" @click="exitStepMode()">
<i data-lucide="x" style="width:11px;height:11px"></i>
中断
</button>
</div>
<div class="step-pips">
<div class="step-pip" :class="stepPipCls(1)"></div>
<div class="step-pip" :class="stepPipCls(2)"></div>
<div class="step-pip" :class="stepPipCls(3)"></div>
</div>
<!-- Step 1: flashcards -->
<div x-show="stepStep === 1">
<div class="card-title" style="margin-bottom:10px">
<i data-lucide="book-open" style="width:12px;height:12px"></i>
Step 1 — 用語確認
</div>
<div class="step-count" x-text="(stepKpIdx+1) + ' / ' + (currentUnit?.keypoints?.length||0) + ' ポイント'"></div>
<div class="flash-card" @click="revealFlash()">
<div class="flash-card-front">タップして確認</div>
<div class="flash-card-back" x-show="stepFlashRevealed"
x-html="currentUnit?.keypoints?.[stepKpIdx]"></div>
<div x-show="!stepFlashRevealed" style="font-size:11px;color:var(--text3);margin-top:8px">
ポイント <span x-text="stepKpIdx+1"></span>
</div>
</div>
<div class="step-action-row">
<button class="btn-sm btn-accent" @click="nextStepFlash()" x-show="stepFlashRevealed">
次へ
<i data-lucide="arrow-right" style="width:11px;height:11px"></i>
</button>
</div>
</div>
<!-- Step 2: 2-choice drills -->
<div x-show="stepStep === 2">
<div class="card-title" style="margin-bottom:10px">
<i data-lucide="git-branch" style="width:12px;height:12px"></i>
Step 2 — 仕訳イメージ
</div>
<template x-if="unitDrills.length > 0">
<div>
<div class="step-count" x-text="(stepDrillIdx+1) + ' / ' + unitDrills.length + ' 問'"></div>
<div x-html="unitVizHtml" x-show="unitVizHtml"></div>
<div class="q-text" style="font-size:13px;margin-bottom:12px"
x-html="unitDrills[stepDrillIdx]?.q"></div>
<div class="s2choice">
<template x-for="(ch, ci) in (unitDrills[stepDrillIdx]?.choices||[])" :key="ci">
<button class="s2btn"
:class="stepDrillAnswered ? (ci === unitDrills[stepDrillIdx].answer ? 's2-ok' : (stepDrillSelected === ci ? 's2-ng' : '')) : ''"
:disabled="stepDrillAnswered"
@click="doStepDrill(ci)">
<span x-html="ch"></span>
</button>
</template>
</div>
<div class="s2-exp" x-show="stepDrillAnswered">
<strong>解説:</strong> <span x-html="unitDrills[stepDrillIdx]?.exp"></span>
</div>
<div class="step-action-row">
<button class="btn-sm btn-accent" @click="nextStepDrill()" x-show="stepDrillAnswered">
<span x-text="stepDrillIdx < unitDrills.length - 1 ? '次の問題' : 'Step 3 へ'"></span>
<i data-lucide="arrow-right" style="width:11px;height:11px"></i>
</button>
</div>
</div>
</template>
</div>
<!-- Step 3: header only (quiz is below) -->
<div x-show="stepStep === 3">
<div class="card-title" style="margin-bottom:8px">
<i data-lucide="check-circle-2" style="width:12px;height:12px"></i>
Step 3 — 本番問題
</div>
<p style="font-size:12px;color:var(--text2);line-height:1.65">
下の理解度チェックを解いて完了です。全問正解でこの単元をマスターしましょう。
</p>
</div>
</div>
<!-- Concept --> <!-- Concept -->
<div class="card" x-show="currentUnit"> <div class="card" x-show="currentUnit && !stepMode">
<div class="card-title"> <div class="card-title">
<i data-lucide="book-open" style="width:13px;height:13px"></i> <i data-lucide="book-open" style="width:13px;height:13px"></i>
概念解説 概念解説
</div> </div>
<div class="concept-text" x-html="currentUnit?.concept"></div> <div class="concept-text" x-html="conceptPreview"></div>
<div class="concept-text" x-show="conceptExpanded" x-html="conceptRest"></div>
<button class="concept-expand-btn" x-show="conceptRest"
@click="conceptExpanded = !conceptExpanded">
<i :data-lucide="conceptExpanded ? 'chevron-up' : 'chevron-down'" style="width:11px;height:11px"></i>
<span x-text="conceptExpanded ? '閉じる' : 'もっと詳しく'"></span>
</button>
</div> </div>
<!-- Exam tips --> <!-- Exam tips -->
@ -494,7 +730,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</template> </template>
</div> </div>
<!-- Keypoints --> <!-- Keypoints -->
<div class="card" x-show="currentUnit?.keypoints?.length"> <div class="card" x-show="!stepMode && currentUnit?.keypoints?.length">
<div class="card-title"> <div class="card-title">
<i data-lucide="zap" style="width:13px;height:13px"></i> <i data-lucide="zap" style="width:13px;height:13px"></i>
重要ポイント 重要ポイント
@ -510,7 +746,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div> </div>
<!-- Quiz --> <!-- Quiz -->
<div class="card" x-show="currentUnit?.quiz?.length"> <div class="card" x-show="currentUnit?.quiz?.length && (!stepMode || stepStep === 3)">
<div class="quiz-header"> <div class="quiz-header">
<div class="quiz-title-label"> <div class="quiz-title-label">
<i data-lucide="check-circle-2" style="width:13px;height:13px"></i> <i data-lucide="check-circle-2" style="width:13px;height:13px"></i>
@ -582,6 +818,101 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div> </div>
<script> <script>
const DRILLS = {
'c01':[
{q:'現金¥50,000が増加した場合、仕訳でどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:0,exp:'資産の増加は借方(左側)に記入します。'},
{q:'売上¥100,000が発生した場合、仕訳でどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:1,exp:'収益の発生は貸方(右側)に記入します。'}
],
'c02':[
{q:'三分法で商品を仕入れたとき「仕入」勘定はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:0,exp:'費用の発生(仕入)は借方に記入します。'},
{q:'決算整理で期末商品を計上する際「繰越商品」はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:0,exp:'繰越商品(資産)の増加は借方に記入します。'}
],
'c03':[
{q:'間接法の減価償却で「減価償却累計額」はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:1,exp:'減価償却累計額(資産のマイナス評価勘定)は貸方に記入します。'},
{q:'帳簿価額より高く売却した場合に生じる「固定資産売却益」はどちらか。',choices:['借方(左側)','貸方(右側)'],answer:1,exp:'収益の発生は貸方に記入します。'}
],
'c04':[
{q:'「貸倒引当金繰入」(費用)の発生はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:0,exp:'費用の発生は借方に記入します。'},
{q:'「貸倒引当金」(評価性引当金)の設定はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:1,exp:'貸倒引当金(資産のマイナス)は貸方に計上します。'}
],
'c05':[
{q:'売買目的有価証券の期末時価が帳簿価額を上回った場合の評価差額はどこに計上するか。',choices:['損益計算書(収益)','純資産の部B/S'],answer:0,exp:'売買目的有価証券の評価損益は当期の損益計算書に計上します。'},
{q:'その他有価証券の評価差額金はどこに計上するか。',choices:['損益計算書','貸借対照表の純資産の部'],answer:1,exp:'その他有価証券評価差額金はB/S純資産の部に計上します税効果考慮後。'}
],
'c06':[
{q:'受取手形を受け取ったとき「受取手形」勘定はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:0,exp:'受取手形(資産)の増加は借方に記入します。'},
{q:'受取手形を割引に出したとき「受取手形」はどちらに記入するか。',choices:['借方(左側)','貸方(右側)'],answer:1,exp:'受取手形(資産)が減少するため貸方に記入します。'}
],
'c07':[
{q:'円高になった場合、外貨建売掛金(輸出債権)の換算替えで生じるのはどちらか。',choices:['為替差損(不利)','為替差益(有利)'],answer:0,exp:'円高では売掛金の円換算額が減少するため為替差損が生じます。'},
{q:'円高になった場合、外貨建買掛金(輸入債務)の換算替えで生じるのはどちらか。',choices:['為替差損','為替差益'],answer:1,exp:'買掛金(債務)は円高で返済額が減少するため為替差益が生じます。'}
],
'c08':[
{q:'ファイナンス・リース開始時に計上する「リース資産」はどちらに記入するか。',choices:['借方(資産増加)','貸方(負債増加)'],answer:0,exp:'リース資産(資産)の増加は借方に記入します。'},
{q:'ファイナンス・リース開始時に計上する「リース債務」はどちらに記入するか。',choices:['借方','貸方'],answer:1,exp:'リース債務(負債)の増加は貸方に記入します。'}
],
'c09':[
{q:'将来減算一時差異が生じた場合「繰延税金資産」はB/Sのどの区分か。',choices:['資産の部(借方)','負債の部(貸方)'],answer:0,exp:'繰延税金資産は将来の税金軽減効果を資産として計上します。'},
{q:'交際費超過額(永久差異)は税効果会計の対象になるか。',choices:['対象になる','対象にならない'],answer:1,exp:'永久差異は将来も解消しないため税効果会計の対象外です。'}
],
'c10':[
{q:'株式を発行して現金を受け取ったとき「資本金」はどちらに記入するか。',choices:['借方','貸方'],answer:1,exp:'資本金(純資産)の増加は貸方に記入します。'},
{q:'自己株式はB/Sのどの区分に表示するか。',choices:['資産の部','純資産の部(マイナス項目)'],answer:1,exp:'自己株式は純資産の部から控除する形式で表示します(資産ではありません)。'}
],
'c11':[
{q:'社債を割引発行(額面>発行価額)した場合、毎期の帳簿価額はどうなるか。',choices:['増加する(額面に近づく)','減少する'],answer:0,exp:'割引発行では償却原価法で社債勘定を毎期増額し、満期に額面と一致させます。'},
{q:'社債利息(費用)の仕訳でどちらに記入するか。',choices:['借方(費用発生)','貸方'],answer:0,exp:'費用の発生は借方に記入します。'}
],
'c12':[
{q:'本店が支店に商品を送付したとき、本店の帳簿で「支店」勘定はどちらか。',choices:['借方(資産増加)','貸方(負債増加)'],answer:0,exp:'本店の「支店」勘定は資産勘定のため増加は借方に記入します。'},
{q:'合併財務諸表で内部取引(振替売上・振替仕入)はどう処理するか。',choices:['そのまま合算する','相殺消去する'],answer:1,exp:'内部取引は合併財務諸表から相殺消去します。'}
],
'c13':[
{q:'のれんは最長何年以内で規則的に償却するか。',choices:['10年以内','20年以内'],answer:1,exp:'のれんは20年以内の期間で規則的に償却します日本基準。'},
{q:'非支配株主持分はB/Sのどの区分に表示するか。',choices:['負債の部','純資産の部(株主資本と区分)'],answer:1,exp:'非支配株主持分はB/S純資産の部に株主資本とは別枠で表示します。'}
],
'c14':[
{q:'間接法のCF計算書で「減価償却費」を営業CFにどう調整するか。',choices:['加算する','減算する'],answer:0,exp:'減価償却費は現金支出なし→費用として差し引いた分を加算して戻します。'},
{q:'固定資産の取得はCF計算書のどの区分に記載するか。',choices:['営業活動CF','投資活動CF'],answer:1,exp:'固定資産の取得・売却は投資活動によるキャッシュフローに記載します。'}
],
'c15':[
{q:'流動比率の計算式「流動資産÷○○×100」の○○はどれか。',choices:['流動資産','流動負債'],answer:1,exp:'流動比率 流動資産 ÷ 流動負債 × 100。分母は流動負債です。'},
{q:'自己資本比率が高いほど財務安全性はどうなるか。',choices:['高くなる(良い)','低くなる(悪い)'],answer:0,exp:'自己資本比率が高いほど借入依存が少なく財務安全性が高いと評価されます。'}
],
'i01':[
{q:'工場の電力料は製造原価のどの区分に含まれるか。',choices:['直接費','製造間接費'],answer:1,exp:'複数製品に共通して発生する電力料は製造間接費(間接費)に分類されます。'},
{q:'仕掛品とはどういう状態の製品か。',choices:['完成した製品','製造途中の未完成品'],answer:1,exp:'仕掛品は製造途中でまだ完成していない製品のことです。'}
],
'i02':[
{q:'先入先出法で物価が上昇しているとき、材料費は移動平均法より高いか低いか。',choices:['高くなる','低くなる'],answer:1,exp:'先入先出法は古い(安い)材料から消費するため物価上昇時は材料費が低くなります。'},
{q:'材料費の計算で「月末在庫」はどのように作用するか。',choices:['材料費を増やす','材料費を減らす'],answer:1,exp:'月末在庫は未消費分のため「月初在庫+当月仕入−月末在庫」から差し引かれます。'}
],
'i03':[
{q:'直接工(製品に直接携わる作業員)の賃金はどの区分か。',choices:['直接労務費','製造間接費'],answer:0,exp:'直接工の賃金は直接労務費に分類されます。'},
{q:'予定賃率が実際賃率より低い場合、賃率差異はどちらか。',choices:['有利差異','不利差異'],answer:1,exp:'実際賃率が予定より高い→コスト超過→不利差異です。'}
],
'i04':[
{q:'配賦差異「実際発生額−予定配賦額」がプラスの場合はどちらか。',choices:['有利差異(配賦過剰)','不利差異(配賦不足)'],answer:1,exp:'実際発生額が予定配賦額より多い=コスト超過=不利差異(配賦不足)です。'},
{q:'製造間接費の予定配賦率は何を基準操業度で割って求めるか。',choices:['実際発生額','製造間接費予算額'],answer:1,exp:'予定配賦率 製造間接費予算額 ÷ 基準操業度で計算します。'}
],
'i05':[
{q:'個別原価計算で原価を集計する単位はどれか。',choices:['製造工程ごと','製造指図書ごと'],answer:1,exp:'個別原価計算は受注(製造指図書)ごとに原価を集計します。'},
{q:'月末に完成していない指図書の原価はどこに計上されるか。',choices:['製品勘定','仕掛品勘定'],answer:1,exp:'完成していない指図書の原価は期末仕掛品として仕掛品勘定に残ります。'}
],
'i06':[
{q:'月末仕掛品200個・加工進捗度50%のとき加工費の完成品換算量は何個か。',choices:['200個そのまま','100個×50%'],answer:1,exp:'換算量 200 × 50% 100個。材料費始点投入は200個のまま。'},
{q:'始点で全量投入する材料費は月末仕掛品に含まれるか。',choices:['含まれる100%','含まれない'],answer:0,exp:'始点投入材料は製造開始時点で全量消費扱いのため月末仕掛品にも100%含まれます。'}
],
'i07':[
{q:'価格差異「(標準価格−実際価格)×実際消費量」がプラスのときはどちらか。',choices:['有利差異','不利差異'],answer:0,exp:'標準>実際(実際が安かった)→有利差異です。マイナスが不利差異。'},
{q:'数量差異の計算式で乗数(かける価格)はどちらか。',choices:['実際価格','標準価格'],answer:1,exp:'数量差異 =(標準消費量−実際消費量)×<strong>標準価格</strong>で計算します。'}
],
'i08':[
{q:'直接原価計算で固定製造間接費はどのように処理するか。',choices:['製品原価に含める','期間費用として全額計上'],answer:1,exp:'直接原価計算では固定費を製品原価に含めず発生した期の期間費用として処理します。'},
{q:'損益分岐点売上高の公式「固定費÷○○」の○○はどれか。',choices:['変動費率','貢献利益率'],answer:1,exp:'損益分岐点売上高 固定費 ÷ 貢献利益率1変動費率'}
]
};
const CATEGORIES = [ const CATEGORIES = [
{ id:'commercial', label:'商業簿記', icon:'landmark', units:[ { id:'commercial', label:'商業簿記', icon:'landmark', units:[
{ id:'c01', num:'C01', title:'仕訳の基礎復習', freq:'high', diff:1, { id:'c01', num:'C01', title:'仕訳の基礎復習', freq:'high', diff:1,
@ -856,6 +1187,23 @@ function bokiApp(){
scores:{}, scores:{},
wrongUnits:{}, wrongUnits:{},
keys:['A','B','C','D','E'], keys:['A','B','C','D','E'],
// concept fold
conceptExpanded:false,
// step mode
stepMode:false,
stepStep:1,
stepKpIdx:0,
stepFlashRevealed:false,
stepDrillIdx:0,
stepDrillAnswered:false,
stepDrillSelected:-1,
// weak drill
weakDrillActive:false,
weakDrillUnits:[],
weakDrillUnitIdx:0,
weakDrillQuizState:{},
weakDrillResults:[],
weakDrillDone:false,
init(){ init(){
this.categories = CATEGORIES.map(cat=>({ this.categories = CATEGORIES.map(cat=>({
@ -898,6 +1246,9 @@ function bokiApp(){
goHome(){ goHome(){
this.currentUnit=null; this.currentUnit=null;
this.quizState={}; this.quizState={};
this.conceptExpanded=false;
this.stepMode=false;
this.weakDrillActive=false;
this.syncUnitToUrl(); this.syncUnitToUrl();
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
}, },
@ -978,6 +1329,8 @@ function bokiApp(){
openUnit(unit){ openUnit(unit){
this.currentUnit=unit; this.currentUnit=unit;
this.quizState={}; this.quizState={};
this.conceptExpanded=false;
this.stepMode=false;
this.syncUnitToUrl(); this.syncUnitToUrl();
this.$nextTick(()=>{ this.$nextTick(()=>{
if(window.lucide) lucide.createIcons(); if(window.lucide) lucide.createIcons();
@ -1095,6 +1448,180 @@ function bokiApp(){
const val=this.isDark?'dark':'light'; const val=this.isDark?'dark':'light';
document.documentElement.setAttribute('data-theme',val); document.documentElement.setAttribute('data-theme',val);
localStorage.setItem('posimai-boki-theme',val); localStorage.setItem('posimai-boki-theme',val);
},
// ---- Concept expand ----
get conceptPreview(){
const c=this.currentUnit?.concept||'';
const i=c.indexOf('</p>');
return i===-1 ? c : c.substring(0,i+4);
},
get conceptRest(){
const c=this.currentUnit?.concept||'';
const i=c.indexOf('</p>');
return i===-1 ? '' : c.substring(i+4);
},
// ---- Weak drill ----
get weakDrillCandidates(){
return this.allUnits()
.filter(u=>this.hasWrong(u.id))
.sort((a,b)=>{
const sa=this.scores[a.id]?.best||0;
const sb=this.scores[b.id]?.best||0;
return sa-sb;
})
.slice(0,5);
},
startWeakDrill(){
this.weakDrillUnits=[...this.weakDrillCandidates];
this.weakDrillUnitIdx=0;
this.weakDrillQuizState={};
this.weakDrillResults=[];
this.weakDrillDone=false;
this.weakDrillActive=true;
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
exitWeakDrill(){
this.weakDrillActive=false;
this.weakDrillDone=false;
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
get weakDrillCurrentUnit(){
return this.weakDrillUnits[this.weakDrillUnitIdx]||null;
},
wdAnswered(qi){ return !!this.weakDrillQuizState[qi]; },
wdQCardCls(qi){
const s=this.weakDrillQuizState[qi];
if(!s) return '';
return s.correct?'correct':'wrong';
},
wdChoiceCls(qi,ci,correct){
const s=this.weakDrillQuizState[qi];
if(!s) return '';
if(ci===s.selected&&s.correct) return 'sel-ok';
if(ci===s.selected&&!s.correct) return 'sel-ng';
if(ci===correct&&!s.correct) return 'rev-ok';
return '';
},
doWeakDrillAnswer(qi,ci,correct,exp){
if(this.weakDrillQuizState[qi]) return;
this.weakDrillQuizState[qi]={selected:ci,correct:ci===correct,exp};
this.weakDrillQuizState={...this.weakDrillQuizState};
},
get weakDrillAllAnswered(){
const total=this.weakDrillCurrentUnit?.quiz?.length||0;
return total>0 && Object.keys(this.weakDrillQuizState).length>=total;
},
get weakDrillUnitScore(){
const correct=Object.values(this.weakDrillQuizState).filter(s=>s?.correct).length;
const total=this.weakDrillCurrentUnit?.quiz?.length||0;
return correct+' / '+total;
},
nextWeakDrillUnit(){
const unit=this.weakDrillCurrentUnit;
if(!unit) return;
const correct=Object.values(this.weakDrillQuizState).filter(s=>s?.correct).length;
const total=unit.quiz?.length||0;
this.weakDrillResults.push({unitId:unit.id,num:unit.num,unitTitle:unit.title,correct,total});
// Update wrong tracking
if(correct>=total){
delete this.wrongUnits[unit.id];
localStorage.setItem('posimai-boki-wrong',JSON.stringify(this.wrongUnits));
this.wrongUnits={...this.wrongUnits};
}
if(this.weakDrillUnitIdx<this.weakDrillUnits.length-1){
this.weakDrillUnitIdx++;
this.weakDrillQuizState={};
} else {
this.weakDrillDone=true;
}
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
get weakDrillClearCount(){
return this.weakDrillResults.filter(r=>r.correct>=r.total).length;
},
// ---- Step mode ----
startStepMode(){
this.stepMode=true;
this.stepStep=1;
this.stepKpIdx=0;
this.stepFlashRevealed=false;
this.stepDrillIdx=0;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
this.quizState={};
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({top:0,behavior:'smooth'});
});
},
exitStepMode(){
this.stepMode=false;
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
},
stepPipCls(n){
if(this.stepStep===n) return 's-active';
if(this.stepStep>n) return 's-done';
return '';
},
revealFlash(){
this.stepFlashRevealed=true;
},
nextStepFlash(){
const kps=this.currentUnit?.keypoints||[];
if(this.stepKpIdx<kps.length-1){
this.stepKpIdx++;
this.stepFlashRevealed=false;
} else {
// All keypoints done — go to step 2 (or 3 if no drills)
const drills=DRILLS[this.currentUnit?.id]||[];
this.stepStep=drills.length>0 ? 2 : 3;
this.stepDrillIdx=0;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
if(this.stepStep===3){
this.$nextTick(()=>{
const m=document.getElementById('main');
if(m) m.scrollTo({top:m.scrollHeight,behavior:'smooth'});
});
}
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
}
},
get unitDrills(){
return DRILLS[this.currentUnit?.id]||[];
},
get unitVizHtml(){
const c=this.currentUnit?.concept||'';
if(!c) return '';
const d=document.createElement('div');
d.innerHTML=c;
const v=d.querySelector('.viz-taccount');
return v?v.outerHTML:'';
},
doStepDrill(ci){
if(this.stepDrillAnswered) return;
this.stepDrillSelected=ci;
this.stepDrillAnswered=true;
},
nextStepDrill(){
const drills=this.unitDrills;
if(this.stepDrillIdx<drills.length-1){
this.stepDrillIdx++;
this.stepDrillAnswered=false;
this.stepDrillSelected=-1;
} else {
// All drills done — go to step 3
this.stepStep=3;
this.$nextTick(()=>{
if(window.lucide) lucide.createIcons();
const m=document.getElementById('main');
if(m) m.scrollTo({top:m.scrollHeight,behavior:'smooth'});
});
}
} }
}; };
} }