v4.1.1: weather_map_screenshot 연결 + 언어앱 카드생성 버그 수정 + 상담앱 심리검사 5종 추가
- weather_map_screenshot 툴이 코드(weather.ts)·레지스트리엔 있었지만 buildTools() 스키마가 없어 모델이 호출 자체를 못 하던 걸 연결. 날씨앱(wt_ 세션) 전용으로 게이팅 - 언어앱 "AI 카드 생성"이 실제로는 0장을 반환하던 버그 수정 — config.json 모델이 kimi-k2.6:cloud(추론모델)로 바뀌면서 num_predict가 너무 작아 생각(thinking)만 하다 토큰을 다 쓰고 실제 답변을 못 내놓고 있었음. think:false + num_predict 상향으로 해결 (generate-flashcards/generate-lesson/check-answer 3개 API 모두 동일 수정) - 언어앱 플래시카드에 타일 그리드 뷰 추가 — 카테고리 카드 전체를 한번에 펼쳐보고 개별 탭으로 뒤집어볼 수 있음 - 상담앱 심리검사를 7종 → 12종으로 확장: PCL-5(PTSD), MDQ(양극성), OCI-R(강박), ASRS-6(성인ADHD), SPIN(사회불안) 추가. PHQ-9 9번 문항(자살/자해 생각) 안전장치를 안내문 한 줄에서 결과화면 상단 위기상담 배너로 강화 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "smallclaw",
|
"name": "smallclaw",
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function registerLanguageRoutes(app: express.Application): void {
|
|||||||
const result = await ollama.chatWithThinking(
|
const result = await ollama.chatWithThinking(
|
||||||
[{ role: 'user' as const, content: prompt }],
|
[{ role: 'user' as const, content: prompt }],
|
||||||
'executor',
|
'executor',
|
||||||
{ num_predict: 1000, temperature: 0.3 }
|
{ num_predict: 2000, temperature: 0.3, think: false }
|
||||||
);
|
);
|
||||||
const raw = String(result.message?.content || '').trim();
|
const raw = String(result.message?.content || '').trim();
|
||||||
let lesson: any;
|
let lesson: any;
|
||||||
@@ -60,7 +60,7 @@ export function registerLanguageRoutes(app: express.Application): void {
|
|||||||
const result = await ollama.chatWithThinking(
|
const result = await ollama.chatWithThinking(
|
||||||
[{ role: 'user' as const, content: prompt }],
|
[{ role: 'user' as const, content: prompt }],
|
||||||
'executor',
|
'executor',
|
||||||
{ num_predict: 700, temperature: 0.2 }
|
{ num_predict: 2000, temperature: 0.2, think: false }
|
||||||
);
|
);
|
||||||
const raw = String(result.message?.content || '').trim();
|
const raw = String(result.message?.content || '').trim();
|
||||||
let cards: any[] = [];
|
let cards: any[] = [];
|
||||||
@@ -95,7 +95,7 @@ export function registerLanguageRoutes(app: express.Application): void {
|
|||||||
const result = await ollama.chatWithThinking(
|
const result = await ollama.chatWithThinking(
|
||||||
[{ role: 'user' as const, content: prompt }],
|
[{ role: 'user' as const, content: prompt }],
|
||||||
'executor',
|
'executor',
|
||||||
{ num_predict: 10, temperature: 0 }
|
{ num_predict: 10, temperature: 0, think: false }
|
||||||
);
|
);
|
||||||
const verdict = String(result.message?.content || '').toLowerCase().includes('correct') ? 'correct' : 'incorrect';
|
const verdict = String(result.message?.content || '').toLowerCase().includes('correct') ? 'correct' : 'incorrect';
|
||||||
res.json({ success: true, result: verdict, score: verdict === 'correct' ? 1 : 0 });
|
res.json({ success: true, result: verdict, score: verdict === 'correct' ? 1 : 0 });
|
||||||
|
|||||||
@@ -2578,6 +2578,20 @@ function buildTools() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'weather_map_screenshot',
|
||||||
|
description: 'Capture a real screenshot of a rendered Windy weather map (radar, satellite, wind, precipitation, etc.) so you can actually SEE it before describing it. Call this with the exact embed.windy.com URL you just generated — do not guess what the map shows from the URL parameters alone.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object', required: ['url'],
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string', description: 'The exact https://embed.windy.com/embed2.html?... URL to capture (must match the one shown to the user)' },
|
||||||
|
wait_ms: { type: 'number', description: 'Milliseconds to wait for map tiles to finish loading before capturing (default 4000, max 8000)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
@@ -5328,6 +5342,10 @@ async function handleChat(
|
|||||||
if (name.startsWith('mcp__dental-dict-sqlite__') && !isDentalAppSession) return false;
|
if (name.startsWith('mcp__dental-dict-sqlite__') && !isDentalAppSession) return false;
|
||||||
if ((name.startsWith('mcp__psychotherapy-cases-sqlite__') || name.startsWith('mcp__psychiatry-cases-sqlite__')) && !isMindAppSession) return false;
|
if ((name.startsWith('mcp__psychotherapy-cases-sqlite__') || name.startsWith('mcp__psychiatry-cases-sqlite__')) && !isMindAppSession) return false;
|
||||||
if (weatherToolNames.has(name) && !isWeatherAppSession && !(meteorologistEnabled && hasWeatherKeyword)) return false;
|
if (weatherToolNames.has(name) && !isWeatherAppSession && !(meteorologistEnabled && hasWeatherKeyword)) return false;
|
||||||
|
// Windy map screenshots only make sense inside the dedicated weather app tab (the
|
||||||
|
// model there has already generated the embed.windy.com URL to capture) — unlike the
|
||||||
|
// other weather tools, not offered via the meteorologist skill in general chat.
|
||||||
|
if (name === 'weather_map_screenshot' && !isWeatherAppSession) return false;
|
||||||
if (legalToolNames.has(name) && !isLawyerAppSession && !(lawyerEnabled && hasLegalKeyword)) return false;
|
if (legalToolNames.has(name) && !isLawyerAppSession && !(lawyerEnabled && hasLegalKeyword)) return false;
|
||||||
if (pptxToolNames.has(name) && presenterEnabled && !hasPptxKeyword) return false;
|
if (pptxToolNames.has(name) && presenterEnabled && !hasPptxKeyword) return false;
|
||||||
if (academicToolNames.has(name) && !hasPptxKeyword) return false;
|
if (academicToolNames.has(name) && !hasPptxKeyword) return false;
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
|
|||||||
.known-bar .known-val { color: #22c55e; font-weight: 700; }
|
.known-bar .known-val { color: #22c55e; font-weight: 700; }
|
||||||
.known-bar .unknown-val { color: #f87171; font-weight: 700; }
|
.known-bar .unknown-val { color: #f87171; font-weight: 700; }
|
||||||
|
|
||||||
|
/* ── Flashcard tile grid ── */
|
||||||
|
.card-grid { flex: 1; display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); grid-auto-rows: 1fr; gap: 20px; padding: 24px; width: 100%; height: 100%; overflow-y: auto; align-content: stretch; box-sizing: border-box; }
|
||||||
|
.tile-stage { perspective: 1000px; min-height: 220px; cursor: pointer; }
|
||||||
|
.tile-inner { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.4s cubic-bezier(.4,0,.2,1); }
|
||||||
|
.tile-inner.flipped { transform: rotateY(180deg); }
|
||||||
|
.tile-face { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; border-radius: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border: 1px solid var(--line); box-shadow: 0 4px 18px rgba(0,0,0,.16); text-align: center; }
|
||||||
|
.tile-front { background: var(--panel); }
|
||||||
|
.tile-back { background: #0d1f1a; border-color: #1a4a35; transform: rotateY(180deg); }
|
||||||
|
.tile-uz { font-size: 26px; font-weight: 800; color: var(--text); word-break: keep-all; }
|
||||||
|
.tile-rom { font-size: 15px; color: var(--muted); font-style: italic; margin-top: 8px; }
|
||||||
|
.tile-ko { font-size: 26px; font-weight: 700; color: #22c55e; word-break: keep-all; }
|
||||||
|
.tile-cat { position: absolute; top: 8px; right: 10px; font-size: 11px; color: var(--muted); }
|
||||||
|
|
||||||
/* ── Vocab table ── */
|
/* ── Vocab table ── */
|
||||||
.vocab-wrap { flex: 1; overflow: auto; padding: 0; }
|
.vocab-wrap { flex: 1; overflow: auto; padding: 0; }
|
||||||
.vocab-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
.vocab-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
@@ -324,10 +337,11 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
|
|||||||
<span class="lg-toolbar-title" id="flash-title">플래시카드 — 전체</span>
|
<span class="lg-toolbar-title" id="flash-title">플래시카드 — 전체</span>
|
||||||
<span style="font-size:12px;color:var(--muted)" id="flash-counter">0 / 0</span>
|
<span style="font-size:12px;color:var(--muted)" id="flash-counter">0 / 0</span>
|
||||||
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--muted);cursor:pointer">
|
<label style="display:flex;align-items:center;gap:5px;font-size:12px;color:var(--muted);cursor:pointer">
|
||||||
<input type="checkbox" id="show-rom" checked onchange="renderCard()" style="accent-color:#22c55e">발음 표시
|
<input type="checkbox" id="show-rom" checked onchange="renderFlashView()" style="accent-color:#22c55e">발음 표시
|
||||||
</label>
|
</label>
|
||||||
|
<button class="lg-btn" id="view-toggle-btn" onclick="toggleViewMode()">🀄 타일로 보기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-wrap">
|
<div class="card-wrap" id="card-wrap-single">
|
||||||
<div class="card-stage" onclick="flipCard()" id="card-stage">
|
<div class="card-stage" onclick="flipCard()" id="card-stage">
|
||||||
<div class="card-inner" id="card-inner">
|
<div class="card-inner" id="card-inner">
|
||||||
<div class="card-face card-front">
|
<div class="card-face card-front">
|
||||||
@@ -352,6 +366,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
|
|||||||
<span>학습 필요: <span class="unknown-val" id="unknown-count">0</span></span>
|
<span>학습 필요: <span class="unknown-val" id="unknown-count">0</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-grid" id="card-grid" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ═══ VOCAB PANEL ═══ -->
|
<!-- ═══ VOCAB PANEL ═══ -->
|
||||||
@@ -654,6 +669,7 @@ let STATE = {
|
|||||||
category: 'all',
|
category: 'all',
|
||||||
vocabCat: 'all',
|
vocabCat: 'all',
|
||||||
quizCat: 'all',
|
quizCat: 'all',
|
||||||
|
viewMode: 'single', // 'single' | 'grid'
|
||||||
deck: [],
|
deck: [],
|
||||||
deckIdx: 0,
|
deckIdx: 0,
|
||||||
known: new Set(),
|
known: new Set(),
|
||||||
@@ -727,7 +743,7 @@ function buildDeck(cat) {
|
|||||||
STATE.deckIdx = 0;
|
STATE.deckIdx = 0;
|
||||||
STATE.flipped = false;
|
STATE.flipped = false;
|
||||||
document.getElementById('card-inner').classList.remove('flipped');
|
document.getElementById('card-inner').classList.remove('flipped');
|
||||||
renderCard();
|
renderFlashView();
|
||||||
}
|
}
|
||||||
function setCategory(cat) {
|
function setCategory(cat) {
|
||||||
STATE.category = cat;
|
STATE.category = cat;
|
||||||
@@ -735,6 +751,44 @@ function setCategory(cat) {
|
|||||||
buildDeck(cat);
|
buildDeck(cat);
|
||||||
document.getElementById('flash-title').textContent = '플래시카드 — ' + (CAT_LABELS[cat]||'전체');
|
document.getElementById('flash-title').textContent = '플래시카드 — ' + (CAT_LABELS[cat]||'전체');
|
||||||
}
|
}
|
||||||
|
function renderFlashView() {
|
||||||
|
if (STATE.viewMode === 'grid') {
|
||||||
|
document.getElementById('flash-counter').textContent = STATE.deck.length ? `${STATE.deck.length}개 전체` : '0개';
|
||||||
|
renderGrid();
|
||||||
|
} else {
|
||||||
|
renderCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggleViewMode() {
|
||||||
|
STATE.viewMode = STATE.viewMode === 'single' ? 'grid' : 'single';
|
||||||
|
document.getElementById('card-wrap-single').style.display = STATE.viewMode === 'single' ? '' : 'none';
|
||||||
|
document.getElementById('card-grid').style.display = STATE.viewMode === 'grid' ? '' : 'none';
|
||||||
|
document.getElementById('view-toggle-btn').textContent = STATE.viewMode === 'single' ? '🀄 타일로 보기' : '🔄 한장씩 보기';
|
||||||
|
renderFlashView();
|
||||||
|
}
|
||||||
|
function renderGrid() {
|
||||||
|
const showRom = document.getElementById('show-rom').checked;
|
||||||
|
const d = STATE.deck;
|
||||||
|
const grid = document.getElementById('card-grid');
|
||||||
|
if (!d.length) {
|
||||||
|
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;color:var(--muted)">단어 없음</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
grid.innerHTML = d.map(v => `
|
||||||
|
<div class="tile-stage" onclick="this.querySelector('.tile-inner').classList.toggle('flipped')">
|
||||||
|
<div class="tile-inner">
|
||||||
|
<div class="tile-face tile-front">
|
||||||
|
<span class="tile-cat">${CAT_LABELS[v.cat]||''}</span>
|
||||||
|
<div class="tile-uz">${v.uz}</div>
|
||||||
|
${showRom?`<div class="tile-rom">[${v.rom}]</div>`:''}
|
||||||
|
</div>
|
||||||
|
<div class="tile-face tile-back">
|
||||||
|
<div class="tile-ko">${v.ko}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
function renderCard() {
|
function renderCard() {
|
||||||
const d = STATE.deck;
|
const d = STATE.deck;
|
||||||
if (!d.length) {
|
if (!d.length) {
|
||||||
@@ -799,7 +853,7 @@ function shuffleDeck() {
|
|||||||
STATE.deckIdx = 0;
|
STATE.deckIdx = 0;
|
||||||
STATE.flipped = false;
|
STATE.flipped = false;
|
||||||
document.getElementById('card-inner').classList.remove('flipped');
|
document.getElementById('card-inner').classList.remove('flipped');
|
||||||
renderCard();
|
renderFlashView();
|
||||||
}
|
}
|
||||||
function resetDeck() {
|
function resetDeck() {
|
||||||
buildDeck(STATE.category);
|
buildDeck(STATE.category);
|
||||||
|
|||||||
+195
-1
@@ -106,6 +106,10 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
|||||||
.q-disclaimer{font-size:10px;color:var(--muted);line-height:1.6;margin-top:12px;padding:10px 12px;border:1px dashed var(--line);border-radius:7px;}
|
.q-disclaimer{font-size:10px;color:var(--muted);line-height:1.6;margin-top:12px;padding:10px 12px;border:1px dashed var(--line);border-radius:7px;}
|
||||||
.q-disclaimer strong{display:block;color:var(--text);margin-bottom:3px;font-size:10px;}
|
.q-disclaimer strong{display:block;color:var(--text);margin-bottom:3px;font-size:10px;}
|
||||||
.q-refs{font-size:10px;color:var(--muted);margin-top:6px;font-style:italic;opacity:.85;}
|
.q-refs{font-size:10px;color:var(--muted);margin-top:6px;font-style:italic;opacity:.85;}
|
||||||
|
.crisis-banner{background:rgba(239,68,68,.1);border:2px solid rgba(239,68,68,.5);border-radius:12px;padding:16px;margin-bottom:16px;}
|
||||||
|
.crisis-banner strong{display:block;color:#f87171;font-size:14px;margin-bottom:8px;}
|
||||||
|
.crisis-banner p{font-size:12px;color:var(--text);line-height:1.6;margin-bottom:8px;}
|
||||||
|
.crisis-banner .crisis-lines{display:flex;flex-direction:column;gap:4px;font-size:13px;font-weight:700;color:var(--text);}
|
||||||
|
|
||||||
/* ── Breathing widget ── */
|
/* ── Breathing widget ── */
|
||||||
.breath-container{display:flex;flex-direction:column;align-items:center;gap:24px;padding:20px 0;}
|
.breath-container{display:flex;flex-direction:column;align-items:center;gap:24px;padding:20px 0;}
|
||||||
@@ -314,6 +318,26 @@ body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;dis
|
|||||||
<span>📋</span>
|
<span>📋</span>
|
||||||
<span class="t-lbl">AUDIT<span class="t-sub">알코올 선별</span></span>
|
<span class="t-lbl">AUDIT<span class="t-sub">알코올 선별</span></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="mn-tool" id="tool-pcl5" onclick="showTool('pcl5')">
|
||||||
|
<span>📋</span>
|
||||||
|
<span class="t-lbl">PCL-5<span class="t-sub">PTSD(외상후스트레스) 선별</span></span>
|
||||||
|
</button>
|
||||||
|
<button class="mn-tool" id="tool-mdq" onclick="showTool('mdq')">
|
||||||
|
<span>📋</span>
|
||||||
|
<span class="t-lbl">MDQ<span class="t-sub">양극성장애 선별</span></span>
|
||||||
|
</button>
|
||||||
|
<button class="mn-tool" id="tool-ocir" onclick="showTool('ocir')">
|
||||||
|
<span>📋</span>
|
||||||
|
<span class="t-lbl">OCI-R<span class="t-sub">강박장애 선별</span></span>
|
||||||
|
</button>
|
||||||
|
<button class="mn-tool" id="tool-asrs" onclick="showTool('asrs')">
|
||||||
|
<span>📋</span>
|
||||||
|
<span class="t-lbl">ASRS-6<span class="t-sub">성인 ADHD 선별</span></span>
|
||||||
|
</button>
|
||||||
|
<button class="mn-tool" id="tool-spin" onclick="showTool('spin')">
|
||||||
|
<span>📋</span>
|
||||||
|
<span class="t-lbl">SPIN<span class="t-sub">사회불안 척도</span></span>
|
||||||
|
</button>
|
||||||
<button class="mn-tool" id="tool-cbt" onclick="showTool('cbt')">
|
<button class="mn-tool" id="tool-cbt" onclick="showTool('cbt')">
|
||||||
<span>✍️</span>
|
<span>✍️</span>
|
||||||
<span class="t-lbl">생각 기록지<span class="t-sub">CBT 인지 재구성</span></span>
|
<span class="t-lbl">생각 기록지<span class="t-sub">CBT 인지 재구성</span></span>
|
||||||
@@ -519,6 +543,11 @@ const toolTitles={
|
|||||||
who5:'📋 WHO-5 웰빙 지수',
|
who5:'📋 WHO-5 웰빙 지수',
|
||||||
rosenberg:'📋 Rosenberg 자존감 척도',
|
rosenberg:'📋 Rosenberg 자존감 척도',
|
||||||
audit:'📋 AUDIT 알코올 사용장애 선별검사',
|
audit:'📋 AUDIT 알코올 사용장애 선별검사',
|
||||||
|
pcl5:'📋 PCL-5 PTSD(외상후스트레스장애) 선별 검사',
|
||||||
|
mdq:'📋 MDQ 기분장애 선별 질문지',
|
||||||
|
ocir:'📋 OCI-R 강박장애 선별 검사',
|
||||||
|
asrs:'📋 ASRS-6 성인 ADHD 자가보고 척도',
|
||||||
|
spin:'📋 SPIN 사회공포증 척도',
|
||||||
cbt:'✍️ 생각 기록지 (CBT)',
|
cbt:'✍️ 생각 기록지 (CBT)',
|
||||||
breath:'🌬️ 호흡 명상',
|
breath:'🌬️ 호흡 명상',
|
||||||
};
|
};
|
||||||
@@ -539,7 +568,7 @@ function showTool(name){
|
|||||||
const widget=document.getElementById('center-widget');
|
const widget=document.getElementById('center-widget');
|
||||||
widget.style.display='';
|
widget.style.display='';
|
||||||
// render
|
// render
|
||||||
const renders={emotion:renderEmotion,phq9:()=>renderQuestionnaire('phq9'),gad7:()=>renderQuestionnaire('gad7'),pss10:()=>renderQuestionnaire('pss10'),isi:()=>renderQuestionnaire('isi'),who5:()=>renderQuestionnaire('who5'),rosenberg:()=>renderQuestionnaire('rosenberg'),audit:()=>renderQuestionnaire('audit'),cbt:renderCBT,breath:renderBreath};
|
const renders={emotion:renderEmotion,phq9:()=>renderQuestionnaire('phq9'),gad7:()=>renderQuestionnaire('gad7'),pss10:()=>renderQuestionnaire('pss10'),isi:()=>renderQuestionnaire('isi'),who5:()=>renderQuestionnaire('who5'),rosenberg:()=>renderQuestionnaire('rosenberg'),audit:()=>renderQuestionnaire('audit'),pcl5:()=>renderQuestionnaire('pcl5'),mdq:()=>renderQuestionnaire('mdq'),ocir:()=>renderQuestionnaire('ocir'),asrs:()=>renderQuestionnaire('asrs'),spin:()=>renderQuestionnaire('spin'),cbt:renderCBT,breath:renderBreath};
|
||||||
(renders[name]||function(){widget.innerHTML='<div class="widget"><p>준비 중입니다.</p></div>';})();
|
(renders[name]||function(){widget.innerHTML='<div class="widget"><p>준비 중입니다.</p></div>';})();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,6 +631,7 @@ const TESTS={
|
|||||||
'남들이 눈치 챌 정도로 너무 느리게 움직이거나 말을 함, 또는 반대로 너무 안절부절 못하거나 들떠 있음',
|
'남들이 눈치 챌 정도로 너무 느리게 움직이거나 말을 함, 또는 반대로 너무 안절부절 못하거나 들떠 있음',
|
||||||
'스스로를 해치거나 자살하는 것이 낫겠다는 생각',
|
'스스로를 해치거나 자살하는 것이 낫겠다는 생각',
|
||||||
],
|
],
|
||||||
|
crisisItem:8,
|
||||||
levels:[
|
levels:[
|
||||||
{max:4,cls:'minimal',label:'정상 범위',desc:'우울 증상이 거의 없습니다.'},
|
{max:4,cls:'minimal',label:'정상 범위',desc:'우울 증상이 거의 없습니다.'},
|
||||||
{max:9,cls:'mild',label:'경미한 우울',desc:'가벼운 우울 증상이 있습니다. 생활 습관 개선이 도움될 수 있습니다.'},
|
{max:9,cls:'mild',label:'경미한 우울',desc:'가벼운 우울 증상이 있습니다. 생활 습관 개선이 도움될 수 있습니다.'},
|
||||||
@@ -762,6 +792,156 @@ const TESTS={
|
|||||||
],
|
],
|
||||||
refs:'Saunders et al.(1993); 한국어판 Lee et al.(2000). AUDIT-C 절단점 ≥4(남)/≥3(여).',
|
refs:'Saunders et al.(1993); 한국어판 Lee et al.(2000). AUDIT-C 절단점 ≥4(남)/≥3(여).',
|
||||||
},
|
},
|
||||||
|
pcl5:{
|
||||||
|
title:'PCL-5 외상후스트레스장애(PTSD) 선별 검사',
|
||||||
|
intro:'지난 한 달 동안, 스트레스가 매우 심했던 사건 경험 때문에 다음 문제들로 얼마나 자주 힘들었습니까? 특정 사건을 구체적으로 지목하지 않아도 괜찮습니다.',
|
||||||
|
scale:['전혀 아님(0)','조금(1)','보통(2)','많이(3)','매우 많이(4)'],
|
||||||
|
questions:[
|
||||||
|
'그 사건에 대한 반복적이고 원치 않는 괴로운 기억',
|
||||||
|
'그 사건에 대한 반복적이고 괴로운 꿈',
|
||||||
|
'갑자기 그 사건이 다시 일어나고 있는 것처럼 느끼거나 행동함 (플래시백)',
|
||||||
|
'그 사건을 떠올리게 하는 것에 노출됐을 때 강한 심리적 고통을 느낌',
|
||||||
|
'그 사건을 떠올리게 하는 것에 노출됐을 때 강한 신체 반응(심장 두근거림, 호흡곤란, 식은땀 등)이 나타남',
|
||||||
|
'그 사건에 대한 기억·생각·감정을 애써 피하려 함',
|
||||||
|
'그 사건을 떠올리게 하는 사람·장소·대화·활동·사물·상황을 피하려 함',
|
||||||
|
'그 사건의 중요한 부분을 기억하기 어려움',
|
||||||
|
'자신·타인·세상에 대한 강한 부정적 신념 (예: "나는 나쁘다", "아무도 믿을 수 없다", "세상은 완전히 위험하다")',
|
||||||
|
'그 사건이나 그로 인해 벌어진 일에 대해 자신 또는 타인을 탓함',
|
||||||
|
'두려움·공포·분노·죄책감·수치심 같은 강한 부정적 감정',
|
||||||
|
'예전에 즐기던 활동에 대한 흥미 상실',
|
||||||
|
'다른 사람들로부터 거리감이나 단절감을 느낌',
|
||||||
|
'긍정적인 감정(행복, 사랑 등)을 경험하기 어려움',
|
||||||
|
'과민한 행동, 분노 폭발, 공격적인 행동',
|
||||||
|
'지나치게 위험하거나 스스로를 해칠 수 있는 행동을 함',
|
||||||
|
'지나치게 경계하거나 주위를 살핌',
|
||||||
|
'쉽게 놀라거나 소스라침',
|
||||||
|
'집중하는 데 어려움을 겪음',
|
||||||
|
'잠들기 어렵거나 잠을 유지하기 어려움',
|
||||||
|
],
|
||||||
|
levels:[
|
||||||
|
{max:9,cls:'minimal',label:'정상 범위',desc:'외상 관련 증상이 거의 없습니다.'},
|
||||||
|
{max:32,cls:'mild',label:'경미~중등도 증상',desc:'일부 외상 관련 증상이 있습니다. 경과를 지켜보며 필요시 상담을 고려하세요.'},
|
||||||
|
{max:80,cls:'severe',label:'PTSD 가능성 높음',desc:'외상후스트레스장애(PTSD) 가능성을 시사하는 점수입니다. 전문가 평가를 권장합니다.'},
|
||||||
|
],
|
||||||
|
refs:'Weathers et al.(2013), National Center for PTSD 개발. 절단점 ≥31~33 (연구별로 상이).',
|
||||||
|
note:'특정 외상 사건을 염두에 두고 응답하는 검사입니다. 사건 자체를 이야기하고 싶지 않다면 하지 않아도 괜찮습니다.',
|
||||||
|
},
|
||||||
|
mdq:{
|
||||||
|
title:'MDQ 기분장애 선별 질문지 (양극성 스펙트럼)',
|
||||||
|
intro:'지금까지 살아오면서, 평소의 나와는 다르게 아래와 같았던 시기가 있었는지 "예/아니오"로 답해주세요.',
|
||||||
|
scale:['아니오(0)','예(1)'],
|
||||||
|
questions:[
|
||||||
|
'평소와 달리 기분이 너무 좋거나 들떠서, 남들이 평소의 당신 같지 않다고 하거나 그것 때문에 문제가 생긴 적이 있나요?',
|
||||||
|
'너무 짜증이 나서 사람들에게 소리치거나 싸움·논쟁을 시작한 적이 있나요?',
|
||||||
|
'평소보다 훨씬 자신감이 넘쳤던 적이 있나요?',
|
||||||
|
'평소보다 잠을 훨씬 적게 잤는데도 피곤하지 않았던 적이 있나요?',
|
||||||
|
'평소보다 말이 훨씬 많아지거나 말이 빨라진 적이 있나요?',
|
||||||
|
'생각이 머릿속에서 너무 빠르게 흘러가서 멈출 수 없었던 적이 있나요?',
|
||||||
|
'주변 것들에 너무 쉽게 정신이 팔려서 집중하기 어려웠던 적이 있나요?',
|
||||||
|
'평소보다 훨씬 에너지가 넘쳤던 적이 있나요?',
|
||||||
|
'평소보다 훨씬 활동적이거나 많은 일을 벌였던 적이 있나요?',
|
||||||
|
'평소보다 훨씬 사교적이거나 외향적이 되었던 적이 있나요? (예: 한밤중에 친구에게 전화)',
|
||||||
|
'평소보다 성(性)에 대한 관심이 훨씬 커진 적이 있나요?',
|
||||||
|
'평소와 다르게 무리하거나 위험할 수 있는 행동(과소비, 난폭 운전 등)을 한 적이 있나요?',
|
||||||
|
'돈을 너무 많이 써서 본인이나 가족이 곤란해진 적이 있나요?',
|
||||||
|
],
|
||||||
|
levels:[
|
||||||
|
{max:6,cls:'minimal',label:'낮음',desc:'조증·경조증 관련 증상이 뚜렷하지 않습니다.'},
|
||||||
|
{max:13,cls:'moderate',label:'양극성 스펙트럼 증상 가능성',desc:'조증·경조증과 관련된 증상이 여러 개 확인됩니다. 전문가 평가를 통해 정확히 확인해보는 것이 좋습니다.'},
|
||||||
|
],
|
||||||
|
refs:'Hirschfeld et al.(2000) 개발. 원본 절단점은 "예" 7개 이상.',
|
||||||
|
note:'원본 MDQ는 "예" 개수 외에도 "여러 증상이 같은 시기에 함께 나타났는지"와 "일상에 얼마나 지장을 주었는지"를 함께 확인해야 양성으로 판정합니다. 이 검사는 문항 수를 단순화한 선별용이며, 이 결과만으로 양극성장애 여부를 판단할 수 없습니다.',
|
||||||
|
},
|
||||||
|
ocir:{
|
||||||
|
title:'OCI-R 강박장애 선별 검사',
|
||||||
|
intro:'지난 한 달 동안, 다음 경험들이 얼마나 괴로움이나 불편함을 주었습니까?',
|
||||||
|
scale:['전혀 아님(0)','약간(1)','보통(2)','많이(3)','매우 많이(4)'],
|
||||||
|
questions:[
|
||||||
|
'물건을 너무 많이 모아둬서 방해가 될 정도이다 (저장)',
|
||||||
|
'필요 이상으로 자주 물건을 확인한다 (확인)',
|
||||||
|
'물건이 제대로 정리되어 있지 않으면 속상하다 (정리정돈)',
|
||||||
|
'어떤 일을 할 때 숫자를 세야만 할 것 같은 충동을 느낀다 (중화행동)',
|
||||||
|
'낯선 사람이나 특정 사람이 만진 물건을 만지기가 꺼려진다 (씻기)',
|
||||||
|
'내 생각을 스스로 통제하기 어렵다고 느낀다 (침투사고)',
|
||||||
|
'필요 없는 물건들을 모은다 (저장)',
|
||||||
|
'문·창문·서랍 등을 반복해서 확인한다 (확인)',
|
||||||
|
'누군가 내가 정리해 둔 것을 바꿔놓으면 속상하다 (정리정돈)',
|
||||||
|
'특정 숫자를 반복해야 할 것 같은 느낌이 든다 (중화행동)',
|
||||||
|
'오염된 느낌 때문에 씻거나 청소를 해야 할 때가 있다 (씻기)',
|
||||||
|
'원치 않는 불쾌한 생각이 떠올라 괴롭다 (침투사고)',
|
||||||
|
'나중에 필요할까봐 물건을 버리지 못한다 (저장)',
|
||||||
|
'가스·수도꼭지·전등 스위치를 끈 후에도 반복해서 확인한다 (확인)',
|
||||||
|
'물건이 특정한 순서로 정리되어 있어야 한다 (정리정돈)',
|
||||||
|
'좋은 숫자와 나쁜 숫자가 있다고 느낀다 (중화행동)',
|
||||||
|
'필요 이상으로 손을 자주, 오래 씻는다 (씻기)',
|
||||||
|
'불쾌한 생각이 자꾸 떠올라 떨쳐내기 어렵다 (침투사고)',
|
||||||
|
],
|
||||||
|
levels:[
|
||||||
|
{max:13,cls:'minimal',label:'정상 범위',desc:'강박 관련 증상이 거의 없습니다.'},
|
||||||
|
{max:20,cls:'mild',label:'경미한 증상',desc:'가벼운 강박 관련 증상이 있습니다.'},
|
||||||
|
{max:72,cls:'moderate',label:'강박장애 가능성',desc:'강박장애(OCD) 가능성을 시사하는 점수입니다. 전문가 평가를 권장합니다.'},
|
||||||
|
],
|
||||||
|
subscores:[
|
||||||
|
{name:'저장(Hoarding)',items:[0,6,12]},
|
||||||
|
{name:'확인(Checking)',items:[1,7,13]},
|
||||||
|
{name:'정리정돈(Ordering)',items:[2,8,14]},
|
||||||
|
{name:'중화행동(Neutralizing)',items:[3,9,15]},
|
||||||
|
{name:'씻기(Washing)',items:[4,10,16]},
|
||||||
|
{name:'침투사고(Obsessing)',items:[5,11,17]},
|
||||||
|
],
|
||||||
|
refs:'Foa et al.(2002) 개발. 절단점 ≥21 (연구별로 18~21 범위).',
|
||||||
|
note:'각 문항 뒤 괄호는 이해를 돕기 위한 하위척도 표시로, 실제 검사에는 표시되지 않습니다.',
|
||||||
|
},
|
||||||
|
asrs:{
|
||||||
|
title:'ASRS-6 성인 ADHD 자가보고 척도',
|
||||||
|
intro:'지난 6개월 동안 다음 문제들이 얼마나 자주 있었습니까? (세계보건기구 WHO 개발 성인 ADHD 선별 도구)',
|
||||||
|
scale:['전혀 없음(0)','드물게(1)','가끔(2)','자주(3)','매우 자주(4)'],
|
||||||
|
questions:[
|
||||||
|
'어려운 부분을 끝낸 후, 프로젝트의 마무리 세부 작업을 처리하는 데 어려움을 겪은 적이 얼마나 자주 있었나요?',
|
||||||
|
'체계적으로 처리해야 하는 일을 할 때, 순서를 정리하는 데 어려움을 겪은 적이 얼마나 자주 있었나요?',
|
||||||
|
'약속이나 해야 할 일을 잊어버린 적이 얼마나 자주 있었나요?',
|
||||||
|
'많은 생각이 필요한 일을 시작하는 것을 미루거나 피한 적이 얼마나 자주 있었나요?',
|
||||||
|
'오래 앉아 있어야 할 때 손이나 발을 꼼지락거리거나 몸을 움직인 적이 얼마나 자주 있었나요?',
|
||||||
|
'마치 모터가 달린 것처럼 지나치게 활동적이거나 무언가를 해야만 할 것 같은 느낌이 든 적이 얼마나 자주 있었나요?',
|
||||||
|
],
|
||||||
|
levels:[
|
||||||
|
{max:13,cls:'minimal',label:'낮음',desc:'ADHD 관련 증상이 뚜렷하지 않습니다.'},
|
||||||
|
{max:24,cls:'moderate',label:'성인 ADHD 증상과 일치',desc:'성인 ADHD와 관련된 증상 패턴이 확인됩니다. 전문가 평가를 권장합니다.'},
|
||||||
|
],
|
||||||
|
refs:'Kessler et al.(2005), WHO/Harvard 개발 ASRS v1.1 Part A(6문항 스크리너).',
|
||||||
|
note:'원본 ASRS-6는 문항별로 다른 절단 기준(문항에 따라 "가끔" 이상 또는 "자주" 이상)을 적용해 4개 이상 해당되면 양성으로 판정하는 방식입니다. 이 검사는 단순 합산 점수로 근사한 것이며, 정확한 채점은 원본 방식과 다를 수 있습니다.',
|
||||||
|
},
|
||||||
|
spin:{
|
||||||
|
title:'SPIN 사회공포증 척도',
|
||||||
|
intro:'지난 한 주 동안, 다음 상황들이 얼마나 불편하거나 두려웠습니까?',
|
||||||
|
scale:['전혀 아님(0)','약간(1)','보통(2)','많이(3)','매우 많이(4)'],
|
||||||
|
questions:[
|
||||||
|
'권위 있는 사람들이 두렵다',
|
||||||
|
'사람들 앞에서 얼굴이 빨개지는 것이 신경 쓰인다',
|
||||||
|
'파티나 사교 모임이 두렵다',
|
||||||
|
'모르는 사람과 대화하는 것을 피한다',
|
||||||
|
'비판받는 것이 매우 두렵다',
|
||||||
|
'창피할까봐 어떤 일을 하거나 사람들에게 말하는 것을 피한다',
|
||||||
|
'사람들 앞에서 땀을 흘리는 것이 괴롭다',
|
||||||
|
'파티에 가는 것을 피한다',
|
||||||
|
'내가 주목받는 활동을 피한다',
|
||||||
|
'낯선 사람에게 말을 거는 것이 두렵다',
|
||||||
|
'연설이나 발표를 해야 하는 상황을 피한다',
|
||||||
|
'비판받지 않기 위해서라면 무엇이든 하겠다',
|
||||||
|
'사람들 곁에 있을 때 심장이 두근거려 괴롭다',
|
||||||
|
'사람들이 지켜볼 수도 있는 상황에서 무언가를 하는 것이 두렵다',
|
||||||
|
'창피당하거나 바보처럼 보이는 것이 가장 두려운 일 중 하나이다',
|
||||||
|
'권위 있는 사람에게 말을 거는 것을 피한다',
|
||||||
|
'사람들 앞에서 떨리거나 흔들리는 것이 괴롭다',
|
||||||
|
],
|
||||||
|
levels:[
|
||||||
|
{max:20,cls:'minimal',label:'정상 범위',desc:'사회불안 관련 증상이 거의 없습니다.'},
|
||||||
|
{max:30,cls:'mild',label:'경미한 사회불안',desc:'가벼운 사회불안 증상이 있습니다.'},
|
||||||
|
{max:40,cls:'moderate',label:'중등도 사회불안',desc:'중간 정도의 사회불안 증상입니다. 전문가 상담을 고려해보세요.'},
|
||||||
|
{max:68,cls:'severe',label:'심각한 사회불안',desc:'심각한 사회불안 증상입니다. 전문가 상담을 권장합니다.'},
|
||||||
|
],
|
||||||
|
refs:'Connor et al.(2000) 개발. 절단점 ≥19(경도 이상 시사).',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let testAnswers={};
|
let testAnswers={};
|
||||||
@@ -858,6 +1038,19 @@ function submitQuestionnaire(testId){
|
|||||||
</div>`).join('')}</div>`:'';
|
</div>`).join('')}</div>`:'';
|
||||||
const itemTable=`<div class="q-result-detail"><table><thead><tr><th>#</th><th>문항</th><th>응답</th><th>점수</th></tr></thead><tbody>${items.map(it=>`<tr><td class="q-num">${it.n}</td><td>${escHtml(it.q)}</td><td>${escHtml(it.label)}</td><td class="q-sc">${it.score}</td></tr>`).join('')}</tbody></table></div>`;
|
const itemTable=`<div class="q-result-detail"><table><thead><tr><th>#</th><th>문항</th><th>응답</th><th>점수</th></tr></thead><tbody>${items.map(it=>`<tr><td class="q-num">${it.n}</td><td>${escHtml(it.q)}</td><td>${escHtml(it.label)}</td><td class="q-sc">${it.score}</td></tr>`).join('')}</tbody></table></div>`;
|
||||||
const disclaimer=`<div class="q-disclaimer"><strong>안내</strong>본 검사는 자기보고식 선별도구로 임상 진단을 대체하지 않습니다. 결과는 참고용이며, 정확한 평가는 임상심리사 또는 정신건강의학과 전문의를 통해 받으세요.${test.note?'<br>'+escHtml(test.note):''}${test.refs?`<div class="q-refs">참고: ${escHtml(test.refs)}</div>`:''}</div>`;
|
const disclaimer=`<div class="q-disclaimer"><strong>안내</strong>본 검사는 자기보고식 선별도구로 임상 진단을 대체하지 않습니다. 결과는 참고용이며, 정확한 평가는 임상심리사 또는 정신건강의학과 전문의를 통해 받으세요.${test.note?'<br>'+escHtml(test.note):''}${test.refs?`<div class="q-refs">참고: ${escHtml(test.refs)}</div>`:''}</div>`;
|
||||||
|
// Standard clinical practice for PHQ-9: item 9 (suicidal/self-harm ideation) warrants
|
||||||
|
// a direct safety check regardless of total score — previously buried as one line in
|
||||||
|
// the disclaimer, now a prominent banner if the respondent scored above 0 on it.
|
||||||
|
const crisisBanner=(test.crisisItem!==undefined&&items[test.crisisItem].score>0)?`<div class="crisis-banner">
|
||||||
|
<strong>⚠️ 확인이 필요합니다</strong>
|
||||||
|
<p>"${escHtml(test.questions[test.crisisItem])}" 문항에 응답하신 내용이 있습니다. 지금 힘든 마음이시라면 아래 연락처로 바로 도움을 받으실 수 있습니다.</p>
|
||||||
|
<div class="crisis-lines">
|
||||||
|
<span>☎ 자살예방상담전화 1393 (24시간, 무료)</span>
|
||||||
|
<span>☎ 정신건강위기상담전화 1577-0199 (24시간)</span>
|
||||||
|
<span>☎ 생명의전화 1588-9191</span>
|
||||||
|
<span>☎ 청소년전화 1388</span>
|
||||||
|
</div>
|
||||||
|
</div>`:'';
|
||||||
|
|
||||||
lastResult={title:test.title,score,level,subscores,items,refs:test.refs,minScore:range.min,maxScore:range.max};
|
lastResult={title:test.title,score,level,subscores,items,refs:test.refs,minScore:range.min,maxScore:range.max};
|
||||||
|
|
||||||
@@ -865,6 +1058,7 @@ function submitQuestionnaire(testId){
|
|||||||
w.innerHTML=`
|
w.innerHTML=`
|
||||||
<div class="widget">
|
<div class="widget">
|
||||||
<h2>${test.title} 결과</h2>
|
<h2>${test.title} 결과</h2>
|
||||||
|
${crisisBanner}
|
||||||
<div class="result-card ${level.cls}">
|
<div class="result-card ${level.cls}">
|
||||||
<div class="r-score">${scoreDisplay}</div>
|
<div class="r-score">${scoreDisplay}</div>
|
||||||
<div class="r-label">${level.label}</div>
|
<div class="r-label">${level.label}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user