diff --git a/package.json b/package.json
index f63cee1..ae84929 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "smallclaw",
- "version": "4.1.0",
+ "version": "4.1.1",
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
"main": "dist/index.js",
"bin": {
diff --git a/src/gateway/routes-language.ts b/src/gateway/routes-language.ts
index 70b628d..50b40a2 100644
--- a/src/gateway/routes-language.ts
+++ b/src/gateway/routes-language.ts
@@ -35,7 +35,7 @@ export function registerLanguageRoutes(app: express.Application): void {
const result = await ollama.chatWithThinking(
[{ role: 'user' as const, content: prompt }],
'executor',
- { num_predict: 1000, temperature: 0.3 }
+ { num_predict: 2000, temperature: 0.3, think: false }
);
const raw = String(result.message?.content || '').trim();
let lesson: any;
@@ -60,7 +60,7 @@ export function registerLanguageRoutes(app: express.Application): void {
const result = await ollama.chatWithThinking(
[{ role: 'user' as const, content: prompt }],
'executor',
- { num_predict: 700, temperature: 0.2 }
+ { num_predict: 2000, temperature: 0.2, think: false }
);
const raw = String(result.message?.content || '').trim();
let cards: any[] = [];
@@ -95,7 +95,7 @@ export function registerLanguageRoutes(app: express.Application): void {
const result = await ollama.chatWithThinking(
[{ role: 'user' as const, content: prompt }],
'executor',
- { num_predict: 10, temperature: 0 }
+ { num_predict: 10, temperature: 0, think: false }
);
const verdict = String(result.message?.content || '').toLowerCase().includes('correct') ? 'correct' : 'incorrect';
res.json({ success: true, result: verdict, score: verdict === 'correct' ? 1 : 0 });
diff --git a/src/gateway/server-v2.ts b/src/gateway/server-v2.ts
index 999c567..21decda 100644
--- a/src/gateway/server-v2.ts
+++ b/src/gateway/server-v2.ts
@@ -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',
function: {
@@ -5328,6 +5342,10 @@ async function handleChat(
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 (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 (pptxToolNames.has(name) && presenterEnabled && !hasPptxKeyword) return false;
if (academicToolNames.has(name) && !hasPptxKeyword) return false;
diff --git a/web-ui/language-app.html b/web-ui/language-app.html
index b689c55..9c2e63b 100644
--- a/web-ui/language-app.html
+++ b/web-ui/language-app.html
@@ -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 .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-wrap { flex: 1; overflow: auto; padding: 0; }
.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
플래시카드 — 전체
0 / 0
- 발음 표시
+ 발음 표시
+ 🀄 타일로 보기
-
+
@@ -352,6 +366,7 @@ body { background: var(--bg); color: var(--text); font-family: 'Nanum Gothic', s
학습 필요: 0
+
@@ -654,6 +669,7 @@ let STATE = {
category: 'all',
vocabCat: 'all',
quizCat: 'all',
+ viewMode: 'single', // 'single' | 'grid'
deck: [],
deckIdx: 0,
known: new Set(),
@@ -727,7 +743,7 @@ function buildDeck(cat) {
STATE.deckIdx = 0;
STATE.flipped = false;
document.getElementById('card-inner').classList.remove('flipped');
- renderCard();
+ renderFlashView();
}
function setCategory(cat) {
STATE.category = cat;
@@ -735,6 +751,44 @@ function setCategory(cat) {
buildDeck(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 = '
단어 없음
';
+ return;
+ }
+ grid.innerHTML = d.map(v => `
+
+
+
+
${CAT_LABELS[v.cat]||''}
+
${v.uz}
+ ${showRom?`
[${v.rom}]
`:''}
+
+
+
+
+ `).join('');
+}
function renderCard() {
const d = STATE.deck;
if (!d.length) {
@@ -799,7 +853,7 @@ function shuffleDeck() {
STATE.deckIdx = 0;
STATE.flipped = false;
document.getElementById('card-inner').classList.remove('flipped');
- renderCard();
+ renderFlashView();
}
function resetDeck() {
buildDeck(STATE.category);
diff --git a/web-ui/mind-app.html b/web-ui/mind-app.html
index 94b2034..13f170f 100644
--- a/web-ui/mind-app.html
+++ b/web-ui/mind-app.html
@@ -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 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;}
+.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 ── */
.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
📋
AUDIT알코올 선별
+
+ 📋
+ PCL-5PTSD(외상후스트레스) 선별
+
+
+ 📋
+ MDQ양극성장애 선별
+
+
+ 📋
+ OCI-R강박장애 선별
+
+
+ 📋
+ ASRS-6성인 ADHD 선별
+
+
+ 📋
+ SPIN사회불안 척도
+
✍️
생각 기록지CBT 인지 재구성
@@ -519,6 +543,11 @@ const toolTitles={
who5:'📋 WHO-5 웰빙 지수',
rosenberg:'📋 Rosenberg 자존감 척도',
audit:'📋 AUDIT 알코올 사용장애 선별검사',
+ pcl5:'📋 PCL-5 PTSD(외상후스트레스장애) 선별 검사',
+ mdq:'📋 MDQ 기분장애 선별 질문지',
+ ocir:'📋 OCI-R 강박장애 선별 검사',
+ asrs:'📋 ASRS-6 성인 ADHD 자가보고 척도',
+ spin:'📋 SPIN 사회공포증 척도',
cbt:'✍️ 생각 기록지 (CBT)',
breath:'🌬️ 호흡 명상',
};
@@ -539,7 +568,7 @@ function showTool(name){
const widget=document.getElementById('center-widget');
widget.style.display='';
// 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='';})();
}
@@ -602,6 +631,7 @@ const TESTS={
'남들이 눈치 챌 정도로 너무 느리게 움직이거나 말을 함, 또는 반대로 너무 안절부절 못하거나 들떠 있음',
'스스로를 해치거나 자살하는 것이 낫겠다는 생각',
],
+ crisisItem:8,
levels:[
{max:4,cls:'minimal',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(여).',
},
+ 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={};
@@ -858,6 +1038,19 @@ function submitQuestionnaire(testId){
`).join('')}
`:'';
const itemTable=`# 문항 응답 점수 ${items.map(it=>`${it.n} ${escHtml(it.q)} ${escHtml(it.label)} ${it.score} `).join('')}
`;
const disclaimer=`안내 본 검사는 자기보고식 선별도구로 임상 진단을 대체하지 않습니다. 결과는 참고용이며, 정확한 평가는 임상심리사 또는 정신건강의학과 전문의를 통해 받으세요.${test.note?'
'+escHtml(test.note):''}${test.refs?`
참고: ${escHtml(test.refs)}
`:''}
`;
+ // 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)?`
+
⚠️ 확인이 필요합니다
+
"${escHtml(test.questions[test.crisisItem])}" 문항에 응답하신 내용이 있습니다. 지금 힘든 마음이시라면 아래 연락처로 바로 도움을 받으실 수 있습니다.
+
+ ☎ 자살예방상담전화 1393 (24시간, 무료)
+ ☎ 정신건강위기상담전화 1577-0199 (24시간)
+ ☎ 생명의전화 1588-9191
+ ☎ 청소년전화 1388
+
+
`:'';
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=`