diff --git a/posimai-sc/index.html b/posimai-sc/index.html index 2b082ec0..9404785f 100644 --- a/posimai-sc/index.html +++ b/posimai-sc/index.html @@ -134,6 +134,22 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1 .home-hint code{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text2);background:var(--surface);padding:2px 6px;border-radius:4px;border:1px solid var(--border)} .home-hint strong{color:var(--text2);font-weight:600} .home-hint kbd{font-family:'JetBrains Mono',monospace;font-size:10px;padding:2px 5px;border:1px solid var(--border);border-radius:4px;background:var(--surface)} +.home-tools{display:flex;justify-content:center;flex-wrap:wrap;gap:10px;margin-bottom:16px} +.glossary-intro{font-size:12px;color:var(--text2);line-height:1.65;margin-bottom:12px} +.glossary-search-wrap{position:relative;margin-bottom:8px} +.glossary-search-icon{position:absolute;left:8px;top:50%;transform:translateY(-50%);color:var(--text3);pointer-events:none} +.glossary-meta-line{font-size:10px;color:var(--text3);margin-bottom:8px} +.glossary-list{max-height:min(58vh,520px);overflow-y:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface2);padding:4px 12px;margin-top:4px} +.glossary-row{padding:10px 0;border-bottom:1px solid var(--border);cursor:pointer;text-align:left;transition:background .12s} +.glossary-row:last-child{border-bottom:none} +.glossary-row:hover{background:var(--accent-dim)} +.glossary-term{font-weight:600;font-size:13px;color:var(--text);margin-bottom:4px} +.glossary-hint{font-size:12px;color:var(--text2);line-height:1.55;margin-bottom:6px;word-break:break-word} +.glossary-unit{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--text3)} +.glossary-num{font-family:'JetBrains Mono',monospace;font-size:10px;padding:1px 5px;border-radius:4px;border:1px solid var(--border2);background:var(--surface)} +.kbd-tiny{font-family:'JetBrains Mono',monospace;font-size:10px;padding:1px 5px;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text2)} +.exam-setup-text{font-size:12px;color:var(--text2);line-height:1.65;margin-bottom:4px} +.drill-row-link{cursor:pointer} .stats-row{display:flex;gap:10px;justify-content:center;margin-top:20px;flex-wrap:wrap} .stat-card{background:var(--surface);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid var(--border);border-radius:var(--radius);padding:14px 22px;text-align:center} .stat-val{font-family:'JetBrains Mono',monospace;font-size:22px;font-weight:500;color:var(--accent)} @@ -167,6 +183,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1 .concept-text p{margin-bottom:10px} .concept-text p:last-child{margin-bottom:0} +.formula-box .inline-em{font-weight:600;color:var(--text);font-family:'JetBrains Mono',monospace} .formula-box{background:var(--surface2);border:1px solid var(--accent-border);border-radius:var(--radius-sm);padding:12px 16px;margin:12px 0;font-family:'JetBrains Mono',monospace;font-size:11.5px;color:var(--accent);line-height:1.8} .formula-label{font-size:10px;color:var(--text3);font-family:'Geist',sans-serif;font-weight:600;letter-spacing:.07em;text-transform:uppercase;margin-bottom:6px} @@ -413,7 +430,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
-
+

情報処理安全確保支援士

セキュリティの基礎から攻撃手法・法規まで、AM2試験に対応した31単元を体系的に学習します。開発者の視点から「なぜそうなのか」を理解しましょう。

@@ -450,6 +467,17 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
+
+ + +
+
+ +
+
+
+
+ + 用語インデックス +
+ +
+

各単元の「初学者向け」枠から用語と短い説明を集約しています。行を押すとその単元を開きます。開いているとき / で検索欄にフォーカスします。

+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + 試験モード +
+ +
+ +
+

頻出単元を優先し、最大10問をランダムに出題します(問題が足りない場合は重要・その他単元も混ざります)。ここでの結果は単元ごとのベストスコアには保存されません。

+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
+ +
+
+ 解説: +
+
+
+
+ +
+
+
+ +
+
+
+
正解(試験モード)
+
+
+
要復習(不正解)
+ +
+
+ + +
+
+
+
+
diff --git a/posimai-sc/js/app.js b/posimai-sc/js/app.js index c6d4aada..9ce6954b 100644 --- a/posimai-sc/js/app.js +++ b/posimai-sc/js/app.js @@ -1,6 +1,74 @@ import { DRILLS } from './data/drills.js'; import { CATEGORIES } from './data/categories.js'; +function shuffleInPlace(arr) { + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} + +function extractTermsFromBeginnerBox(box) { + const clone = box.cloneNode(true); + clone.querySelector('.formula-label')?.remove(); + const inner = clone.innerHTML.trim(); + const parts = inner.split(/(?=)/i).map(s => s.trim()).filter(s => /^/i.test(s)); + const terms = []; + for (const p of parts) { + const doc = new DOMParser().parseFromString('
'+p+'
', 'text/html'); + const root = doc.querySelector('.seg'); + const st = root && root.querySelector('strong'); + if (!st) continue; + const term = st.textContent.trim(); + let hint = ''; + let n = st.nextSibling; + while (n) { + if (n.nodeType === 3) hint += n.textContent; + else if (n.nodeType === 1) hint += n.textContent; + n = n.nextSibling; + } + hint = hint.replace(/^[::\s.]+/, '').trim(); + if (term) terms.push({ term, hint }); + } + return terms; +} + +function parseConceptForGlossary(conceptHtml) { + if (!conceptHtml) return []; + const doc = new DOMParser().parseFromString('
'+conceptHtml+'
', 'text/html'); + const root = doc.querySelector('.groot'); + if (!root) return []; + const boxes = root.querySelectorAll('.formula-box'); + for (const box of boxes) { + const lab = box.querySelector(':scope > .formula-label'); + if (!lab || lab.textContent.indexOf('初学者向け') === -1) continue; + return extractTermsFromBeginnerBox(box); + } + return []; +} + +function buildGlossaryRows(categories) { + const rows = []; + for (const cat of categories) { + for (const u of cat.units) { + const terms = parseConceptForGlossary(u.concept); + for (const t of terms) { + rows.push({ + term: t.term, + hint: t.hint, + unitId: u.id, + num: u.num, + title: u.title, + catLabel: cat.label + }); + } + } + } + rows.sort((a, b) => a.term.localeCompare(b.term, 'ja')); + return rows; +} + function sanitizeTrustedHtml(dirty) { if (dirty == null || dirty === '') return ''; const s = String(dirty); @@ -50,11 +118,22 @@ document.addEventListener('alpine:init', () => { weakDrillResults:[], weakDrillDone:false, + glossaryRows:[], + glossaryViewActive:false, + glossarySearch:'', + + examActive:false, + examPhase:'setup', + examQuestions:[], + examIdx:0, + examQuizState:{}, + init(){ this.categories = CATEGORIES.map(cat=>({ ...cat, units: cat.units.map(u=>({...u, catLabel:cat.label})) })); + this.glossaryRows = buildGlossaryRows(this.categories); try{ this.progress = JSON.parse(localStorage.getItem('posimai-sc-progress')||'{}'); } catch{ this.progress = {}; } try{ this.scores = JSON.parse(localStorage.getItem('posimai-sc-scores')||'{}'); } @@ -82,6 +161,8 @@ document.addEventListener('alpine:init', () => { if(t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable)) return; if(e.key==='/'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ e.preventDefault(); + const gs=document.querySelector('.glossary-search-input'); + if(this.glossaryViewActive&&gs){ gs.focus(); return; } const si=document.querySelector('.search-input'); if(si) si.focus(); } @@ -106,6 +187,13 @@ document.addEventListener('alpine:init', () => { this.conceptExpanded=false; this.stepMode=false; this.weakDrillActive=false; + this.glossaryViewActive=false; + this.glossarySearch=''; + this.examActive=false; + this.examPhase='setup'; + this.examQuestions=[]; + this.examIdx=0; + this.examQuizState={}; this.syncUnitToUrl(); this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); }, @@ -119,6 +207,16 @@ document.addEventListener('alpine:init', () => { })).filter(cat=>cat.units.length>0); }, + get filteredGlossary(){ + const q=(this.glossarySearch||'').trim().toLowerCase(); + if(!q) return this.glossaryRows; + return this.glossaryRows.filter(r=>{ + return r.term.toLowerCase().includes(q)||r.hint.toLowerCase().includes(q) + ||r.num.toLowerCase().includes(q)||r.title.toLowerCase().includes(q) + ||r.catLabel.toLowerCase().includes(q); + }); + }, + get totalCount(){ return this.categories.reduce((s,c)=>s+c.units.length,0); }, get doneCount(){ return Object.keys(this.progress).length; }, get progressPct(){ return this.totalCount ? Math.round(this.doneCount/this.totalCount*100) : 0; }, @@ -191,6 +289,8 @@ document.addEventListener('alpine:init', () => { }, openUnit(unit){ + this.glossaryViewActive=false; + this.examActive=false; this.currentUnit=unit; this.quizState={}; this.conceptExpanded=true; @@ -337,7 +437,151 @@ document.addEventListener('alpine:init', () => { }) .slice(0,5); }, + openGlossary(){ + this.examActive=false; + this.glossaryViewActive=true; + this.glossarySearch=''; + this.$nextTick(()=>{ + if(window.lucide) lucide.createIcons(); + const g=document.querySelector('.glossary-search-input'); + if(g) g.focus(); + }); + }, + closeGlossary(){ + this.glossaryViewActive=false; + this.glossarySearch=''; + this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); + }, + openUnitFromGlossary(uid){ + const u=this.allUnits().find(x=>x.id===uid); + if(u){ + this.glossaryViewActive=false; + this.openUnit(u); + } + }, + + openExam(){ + this.glossaryViewActive=false; + this.examActive=true; + this.examPhase='setup'; + this.examQuestions=[]; + this.examIdx=0; + this.examQuizState={}; + this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); + }, + exitExam(){ + this.examActive=false; + this.examPhase='setup'; + this.examQuestions=[]; + this.examIdx=0; + this.examQuizState={}; + this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); + }, + fillExamQuestions(){ + const pool=[]; + const pushFreq=f=>{ + for(const u of this.allUnits()){ + if(u.freq!==f) continue; + (u.quiz||[]).forEach((q,qi)=>{ pool.push({ unitId:u.id, qi, unit:u, q }); }); + } + }; + pushFreq('high'); + if(pool.length<10) pushFreq('mid'); + if(pool.length<10){ + for(const u of this.allUnits()){ + if(u.freq==='high'||u.freq==='mid') continue; + (u.quiz||[]).forEach((q,qi)=>{ pool.push({ unitId:u.id, qi, unit:u, q }); }); + } + } + shuffleInPlace(pool); + this.examQuestions=pool.slice(0, Math.min(10, pool.length)); + }, + startExamRun(){ + this.fillExamQuestions(); + if(!this.examQuestions.length) return; + this.examPhase='run'; + this.examIdx=0; + this.examQuizState={}; + this.$nextTick(()=>{ + if(window.lucide) lucide.createIcons(); + const m=document.getElementById('main'); + if(m) m.scrollTo({ top:0, behavior:'auto' }); + }); + }, + get examCurrentEntry(){ return this.examQuestions[this.examIdx]||null; }, + get examTotal(){ return this.examQuestions.length; }, + exAnswered(){ + return !!this.examQuizState[this.examIdx]; + }, + exQCardCls(){ + const s=this.examQuizState[this.examIdx]; + if(!s) return ''; + return s.correct?'correct':'wrong'; + }, + exChoiceCls(ci,correct){ + const s=this.examQuizState[this.examIdx]; + 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 ''; + }, + doExamAnswer(ci,correct,exp){ + if(this.examQuizState[this.examIdx]) return; + this.examQuizState[this.examIdx]={ + selected:ci, + correct:ci===correct, + exp:sanitizeStoredHtml(exp) + }; + this.examQuizState={...this.examQuizState}; + this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); + }, + nextExamQ(){ + if(this.examIdx{ + if(window.lucide) lucide.createIcons(); + const m=document.getElementById('main'); + if(m) m.scrollTo({ top:0, behavior:'smooth' }); + }); + } else { + this.examPhase='result'; + this.$nextTick(()=>{ + if(window.lucide) lucide.createIcons(); + const m=document.getElementById('main'); + if(m) m.scrollTo({ top:0, behavior:'smooth' }); + }); + } + }, + get examCorrectCount(){ + let n=0; + for(let i=0;ix.id===uid); + if(u){ + this.exitExam(); + this.openUnit(u); + } + }, + startWeakDrill(){ + this.glossaryViewActive=false; + this.examActive=false; this.weakDrillUnits=[...this.weakDrillCandidates]; this.weakDrillUnitIdx=0; this.weakDrillQuizState={}; diff --git a/posimai-sc/js/data/categories.js b/posimai-sc/js/data/categories.js index c9d1d7f7..926c5404 100644 --- a/posimai-sc/js/data/categories.js +++ b/posimai-sc/js/data/categories.js @@ -640,7 +640,7 @@ export const CATEGORIES = [ ] }, { id:'s31', num:'S31', title:'コンプライアンス・証跡(ログ)入門', freq:'mid', diff:1, - concept:`

コンプライアンスは、法令・業界ガイドライン・社内規程に沿って業務を運営することです。情報セキュリティ分野では、個人情報保護やISMSの文脈で「記録」「監査」「証跡」がセットで問われます。

初学者向け:証跡とログ
コンプライアンス:ルールに適合して運用すること。証跡:いつ・誰が・何をしたかを後から説明できる記録。システムログ:ログイン成否・設定変更などを機械的に残すもの。ログは攻撃者に消されると意味がないため、権限分離・改ざん検知・外部SIEMで保護する。
内部統制との関係
重要な処理は職務分離や承認フローと組み合わせ、ログはその実行事実を残す。保存期間・閲覧権限・マスキングは個人情報保護と両立して設計する。

クラウドではログの有効化や保管先の設計も利用者責任に含まれる場合がある(責任共有モデル)。

`, + concept:`

コンプライアンスは、法令・業界ガイドライン・社内規程に沿って業務を運営することです。情報セキュリティ分野では、個人情報保護やISMSの文脈で「記録」「監査」「証跡」がセットで問われます。

初学者向け:証跡とログ
コンプライアンス:ルールに適合して運用すること。証跡:いつ・誰が・何をしたかを後から説明できる記録。システムログ:ログイン成否・設定変更などを機械的に残すもの。ログは攻撃者に消されると意味がないため、権限分離・改ざん検知・外部SIEMで保護する。
内部統制との関係
重要な処理は職務分離や承認フローと組み合わせ、ログはその実行事実を残す。保存期間・閲覧権限・マスキングは個人情報保護と両立して設計する。

クラウドではログの有効化や保管先の設計も利用者責任に含まれる場合がある(責任共有モデル)。

`, examtips:[ '「コンプライアンス=法令遵守だけ」と捉えない。社内規程・契約・業界基準も含む。', '監査証跡は「後から説明できること」が目的。ログを取らないより、取り方と保護が問われる。', diff --git a/posimai-sc/sw.js b/posimai-sc/sw.js index 1712f208..10186ab7 100644 --- a/posimai-sc/sw.js +++ b/posimai-sc/sw.js @@ -1,5 +1,5 @@ // posimai-sc SW — same-origin の静的資産のみキャッシュ(CDN は対象外) -const CACHE = 'posimai-sc-v3'; +const CACHE = 'posimai-sc-v4'; const STATIC = [ '/', '/index.html',