feat(posimai-sc): 用語インデックスと試験モード、SW v4

Made-with: Cursor
This commit is contained in:
posimai 2026-04-20 13:49:03 +09:00
parent e6bb59df86
commit 492da3f2d9
4 changed files with 397 additions and 3 deletions

View File

@ -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 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 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-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} .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-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)} .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{margin-bottom:10px}
.concept-text p:last-child{margin-bottom:0} .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-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} .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
<main id="main"> <main id="main">
<!-- HOME --> <!-- HOME -->
<div x-show="!currentUnit && !weakDrillActive"> <div x-show="!currentUnit && !weakDrillActive && !glossaryViewActive && !examActive">
<div class="home-hero"> <div class="home-hero">
<h1>情報処理<span>安全確保支援士</span></h1> <h1>情報処理<span>安全確保支援士</span></h1>
<p>セキュリティの基礎から攻撃手法・法規まで、AM2試験に対応した31単元を体系的に学習します。開発者の視点から「なぜそうなのか」を理解しましょう。</p> <p>セキュリティの基礎から攻撃手法・法規まで、AM2試験に対応した31単元を体系的に学習します。開発者の視点から「なぜそうなのか」を理解しましょう。</p>
@ -450,6 +467,17 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</template> </template>
</div> </div>
<div class="home-tools">
<button class="btn-sm" type="button" @click="openGlossary()">
<i data-lucide="library" style="width:12px;height:12px"></i>
用語インデックス
</button>
<button class="btn-sm" type="button" @click="openExam()">
<i data-lucide="clipboard-check" style="width:12px;height:12px"></i>
試験モード
</button>
</div>
<!-- Weak drill button --> <!-- Weak drill button -->
<div style="margin-bottom:20px;display:flex;justify-content:center"> <div style="margin-bottom:20px;display:flex;justify-content:center">
<button class="btn-sm" :class="weakDrillCandidates.length ? 'btn-accent' : ''" <button class="btn-sm" :class="weakDrillCandidates.length ? 'btn-accent' : ''"
@ -476,6 +504,128 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div> </div>
</div> </div>
<!-- GLOSSARY INDEX -->
<div x-show="glossaryViewActive && !currentUnit && !weakDrillActive">
<div class="card glossary-panel">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="library" style="width:13px;height:13px"></i>
用語インデックス
</div>
<button class="btn-sm" type="button" @click="closeGlossary()" aria-label="用語インデックスを閉じる">
<i data-lucide="x" style="width:11px;height:11px"></i>
閉じる
</button>
</div>
<p class="glossary-intro">各単元の「初学者向け」枠から用語と短い説明を集約しています。行を押すとその単元を開きます。開いているとき <kbd class="kbd-tiny">/</kbd> で検索欄にフォーカスします。</p>
<div class="glossary-search-wrap">
<i data-lucide="search" class="glossary-search-icon" style="width:12px;height:12px"></i>
<input class="search-input glossary-search-input" type="search" placeholder="用語・説明・単元で検索..." x-model="glossarySearch" aria-label="用語インデックスの検索">
</div>
<div class="glossary-meta-line" x-text="filteredGlossary.length + ' 件 / 全 ' + glossaryRows.length + ' 件'"></div>
<div class="glossary-list">
<template x-for="(row, gi) in filteredGlossary" :key="'g'+gi+'-'+row.unitId+'-'+row.term">
<div class="glossary-row" role="button" tabindex="0"
@click="openUnitFromGlossary(row.unitId)"
@keydown.enter.prevent="openUnitFromGlossary(row.unitId)"
@keydown.space.prevent="openUnitFromGlossary(row.unitId)">
<div class="glossary-term" x-text="row.term"></div>
<div class="glossary-hint" x-text="row.hint"></div>
<div class="glossary-unit">
<span class="glossary-num" x-text="row.num"></span>
<span x-text="row.title"></span>
<i data-lucide="chevron-right" style="width:14px;height:14px;flex-shrink:0;opacity:0.55;margin-left:auto"></i>
</div>
</div>
</template>
</div>
</div>
</div>
<!-- EXAM MODE -->
<div x-show="examActive && !currentUnit && !weakDrillActive">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;flex-wrap:wrap;gap:8px">
<div class="card-title" style="margin-bottom:0">
<i data-lucide="clipboard-check" style="width:13px;height:13px"></i>
試験モード
</div>
<button class="btn-sm" type="button" @click="exitExam()" aria-label="試験モードを終了する">
<i data-lucide="x" style="width:11px;height:11px"></i>
終了
</button>
</div>
<div x-show="examPhase==='setup'">
<p class="exam-setup-text">頻出単元を優先し、最大10問をランダムに出題します問題が足りない場合は重要・その他単元も混ざります。ここでの結果は単元ごとのベストスコアには保存されません。</p>
<div style="display:flex;justify-content:center;margin-top:12px">
<button class="btn-sm btn-accent" type="button" @click="startExamRun()">10問に挑戦</button>
</div>
</div>
<div x-show="examPhase==='run' && examCurrentEntry">
<div class="drill-prog-wrap">
<div class="drill-prog-label">
<span x-text="'試験 ' + (examIdx+1) + ' / ' + examTotal"></span>
<span x-text="examCurrentEntry.unit.num"></span>
</div>
<div class="drill-prog-bar">
<div class="drill-prog-fill" :style="'width:' + (examTotal ? (examIdx/examTotal*100) : 0) + '%'"></div>
</div>
</div>
<div class="drill-unit-lbl">
<span x-text="examCurrentEntry.unit.catLabel + ' — ' + examCurrentEntry.unit.title"></span>
</div>
<div class="q-card" :class="exQCardCls()">
<div class="q-text" x-html="safeHtml(examCurrentEntry.q.q)"></div>
<div class="q-choices">
<template x-for="(ch, ci) in examCurrentEntry.q.choices" :key="ci">
<button class="q-choice" type="button"
:class="exChoiceCls(ci, examCurrentEntry.q.answer)"
:disabled="exAnswered()"
@click="doExamAnswer(ci, examCurrentEntry.q.answer, examCurrentEntry.q.exp)">
<span class="choice-key" x-text="keys[ci]"></span>
<span x-html="safeHtml(ch)"></span>
</button>
</template>
</div>
<div class="q-exp" x-show="exAnswered()">
<strong>解説:</strong> <span x-html="safeHtml(examQuizState[examIdx]?.exp)"></span>
</div>
</div>
<div class="quiz-result" x-show="exAnswered()" style="margin-top:14px">
<div class="result-actions">
<button class="btn-sm btn-accent" type="button" @click="nextExamQ()"
x-text="examIdx < examTotal - 1 ? '次の問題へ' : '結果を見る'"></button>
</div>
</div>
</div>
<div x-show="examPhase==='result'">
<div class="drill-result">
<div class="drill-result-pct" x-text="examCorrectCount + ' / ' + examTotal"></div>
<div class="drill-result-sub">正解(試験モード)</div>
</div>
<div x-show="examWrongItems.length" style="margin-top:14px">
<div class="card-title" style="font-size:12px;margin-bottom:8px">要復習(不正解)</div>
<template x-for="(w, wi) in examWrongItems" :key="'w'+wi">
<div class="drill-unit-row drill-row-link" role="button" tabindex="0"
@click="openUnitFromExam(w.unitId)"
@keydown.enter.prevent="openUnitFromExam(w.unitId)"
@keydown.space.prevent="openUnitFromExam(w.unitId)">
<span class="drill-unit-row-title" x-text="w.num + ' ' + w.title"></span>
<span class="drill-unit-row-ng">単元へ</span>
</div>
</template>
</div>
<div class="result-actions" style="margin-top:18px">
<button class="btn-sm btn-accent" type="button" @click="startExamRun()">もう一度</button>
<button class="btn-sm" type="button" @click="exitExam()">ホームへ</button>
</div>
</div>
</div>
</div>
<!-- WEAK DRILL --> <!-- WEAK DRILL -->
<div x-show="weakDrillActive"> <div x-show="weakDrillActive">
<div class="card"> <div class="card">

View File

@ -1,6 +1,74 @@
import { DRILLS } from './data/drills.js'; import { DRILLS } from './data/drills.js';
import { CATEGORIES } from './data/categories.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(/(?=<strong>)/i).map(s => s.trim()).filter(s => /^<strong>/i.test(s));
const terms = [];
for (const p of parts) {
const doc = new DOMParser().parseFromString('<div class="seg">'+p+'</div>', '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('<div class="groot">'+conceptHtml+'</div>', '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) { function sanitizeTrustedHtml(dirty) {
if (dirty == null || dirty === '') return ''; if (dirty == null || dirty === '') return '';
const s = String(dirty); const s = String(dirty);
@ -50,11 +118,22 @@ document.addEventListener('alpine:init', () => {
weakDrillResults:[], weakDrillResults:[],
weakDrillDone:false, weakDrillDone:false,
glossaryRows:[],
glossaryViewActive:false,
glossarySearch:'',
examActive:false,
examPhase:'setup',
examQuestions:[],
examIdx:0,
examQuizState:{},
init(){ init(){
this.categories = CATEGORIES.map(cat=>({ this.categories = CATEGORIES.map(cat=>({
...cat, ...cat,
units: cat.units.map(u=>({...u, catLabel:cat.label})) units: cat.units.map(u=>({...u, catLabel:cat.label}))
})); }));
this.glossaryRows = buildGlossaryRows(this.categories);
try{ this.progress = JSON.parse(localStorage.getItem('posimai-sc-progress')||'{}'); } try{ this.progress = JSON.parse(localStorage.getItem('posimai-sc-progress')||'{}'); }
catch{ this.progress = {}; } catch{ this.progress = {}; }
try{ this.scores = JSON.parse(localStorage.getItem('posimai-sc-scores')||'{}'); } 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(t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.isContentEditable)) return;
if(e.key==='/'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ if(e.key==='/'&&!e.ctrlKey&&!e.metaKey&&!e.altKey){
e.preventDefault(); e.preventDefault();
const gs=document.querySelector('.glossary-search-input');
if(this.glossaryViewActive&&gs){ gs.focus(); return; }
const si=document.querySelector('.search-input'); const si=document.querySelector('.search-input');
if(si) si.focus(); if(si) si.focus();
} }
@ -106,6 +187,13 @@ document.addEventListener('alpine:init', () => {
this.conceptExpanded=false; this.conceptExpanded=false;
this.stepMode=false; this.stepMode=false;
this.weakDrillActive=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.syncUnitToUrl();
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); }); this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
}, },
@ -119,6 +207,16 @@ document.addEventListener('alpine:init', () => {
})).filter(cat=>cat.units.length>0); })).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 totalCount(){ return this.categories.reduce((s,c)=>s+c.units.length,0); },
get doneCount(){ return Object.keys(this.progress).length; }, get doneCount(){ return Object.keys(this.progress).length; },
get progressPct(){ return this.totalCount ? Math.round(this.doneCount/this.totalCount*100) : 0; }, get progressPct(){ return this.totalCount ? Math.round(this.doneCount/this.totalCount*100) : 0; },
@ -191,6 +289,8 @@ document.addEventListener('alpine:init', () => {
}, },
openUnit(unit){ openUnit(unit){
this.glossaryViewActive=false;
this.examActive=false;
this.currentUnit=unit; this.currentUnit=unit;
this.quizState={}; this.quizState={};
this.conceptExpanded=true; this.conceptExpanded=true;
@ -337,7 +437,151 @@ document.addEventListener('alpine:init', () => {
}) })
.slice(0,5); .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<this.examQuestions.length-1){
this.examIdx++;
this.$nextTick(()=>{
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;i<this.examQuestions.length;i++){
if(this.examQuizState[i]?.correct) n++;
}
return n;
},
get examWrongItems(){
const out=[];
for(let i=0;i<this.examQuestions.length;i++){
const s=this.examQuizState[i];
if(s&&!s.correct){
const e=this.examQuestions[i];
out.push({ idx:i, unitId:e.unitId, num:e.unit.num, title:e.unit.title, q:e.q.q });
}
}
return out;
},
openUnitFromExam(uid){
const u=this.allUnits().find(x=>x.id===uid);
if(u){
this.exitExam();
this.openUnit(u);
}
},
startWeakDrill(){ startWeakDrill(){
this.glossaryViewActive=false;
this.examActive=false;
this.weakDrillUnits=[...this.weakDrillCandidates]; this.weakDrillUnits=[...this.weakDrillCandidates];
this.weakDrillUnitIdx=0; this.weakDrillUnitIdx=0;
this.weakDrillQuizState={}; this.weakDrillQuizState={};

View File

@ -640,7 +640,7 @@ export const CATEGORIES = [
] ]
}, },
{ id:'s31', num:'S31', title:'コンプライアンス・証跡(ログ)入門', freq:'mid', diff:1, { id:'s31', num:'S31', title:'コンプライアンス・証跡(ログ)入門', freq:'mid', diff:1,
concept:`<p><strong>コンプライアンス</strong>は、法令・業界ガイドライン・社内規程に沿って業務を運営することです。情報セキュリティ分野では、個人情報保護やISMSの文脈で「記録」「監査」「証跡」がセットで問われます。</p><div class="formula-box"><div class="formula-label">初学者向け:証跡とログ</div><strong>コンプライアンス</strong>:ルールに適合して運用すること。<strong>証跡</strong>:いつ・誰が・何をしたかを後から説明できる記録。<strong>システムログ</strong>:ログイン成否・設定変更などを機械的に残すもの。ログは攻撃者に消されると意味がないため、<strong>権限分離・改ざん検知・外部SIEM</strong>で保護する。</div><div class="formula-box"><div class="formula-label">内部統制との関係</div>重要な処理は<strong>職務分離</strong>や承認フローと組み合わせ、ログはその実行事実を残す。保存期間・閲覧権限・マスキングは個人情報保護と両立して設計する。</div><p>クラウドではログの有効化や保管先の設計も利用者責任に含まれる場合がある(責任共有モデル)。</p>`, concept:`<p><strong>コンプライアンス</strong>は、法令・業界ガイドライン・社内規程に沿って業務を運営することです。情報セキュリティ分野では、個人情報保護やISMSの文脈で「記録」「監査」「証跡」がセットで問われます。</p><div class="formula-box"><div class="formula-label">初学者向け:証跡とログ</div><strong>コンプライアンス</strong>:ルールに適合して運用すること。<strong>証跡</strong>:いつ・誰が・何をしたかを後から説明できる記録。<strong>システムログ</strong>:ログイン成否・設定変更などを機械的に残すもの。ログは攻撃者に消されると意味がないため、<span class="inline-em">権限分離・改ざん検知・外部SIEM</span>で保護する。</div><div class="formula-box"><div class="formula-label">内部統制との関係</div>重要な処理は<strong>職務分離</strong>や承認フローと組み合わせ、ログはその実行事実を残す。保存期間・閲覧権限・マスキングは個人情報保護と両立して設計する。</div><p>クラウドではログの有効化や保管先の設計も利用者責任に含まれる場合がある(責任共有モデル)。</p>`,
examtips:[ examtips:[
'「コンプライアンス=法令遵守だけ」と捉えない。社内規程・契約・業界基準も含む。', '「コンプライアンス=法令遵守だけ」と捉えない。社内規程・契約・業界基準も含む。',
'監査証跡は「後から説明できること」が目的。ログを取らないより、取り方と保護が問われる。', '監査証跡は「後から説明できること」が目的。ログを取らないより、取り方と保護が問われる。',

View File

@ -1,5 +1,5 @@
// posimai-sc SW — same-origin の静的資産のみキャッシュCDN は対象外) // posimai-sc SW — same-origin の静的資産のみキャッシュCDN は対象外)
const CACHE = 'posimai-sc-v3'; const CACHE = 'posimai-sc-v4';
const STATIC = [ const STATIC = [
'/', '/',
'/index.html', '/index.html',