330 lines
14 KiB
HTML
330 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="color-scheme" content="light dark" />
|
|
<meta
|
|
name="description"
|
|
content="Vignette는 상담 수련생이 AI 내담자와 회기를 연습하고, 평가 피드백과 사례개념화 워크시트를 확인하는 교육용 AI 심리상담 시뮬레이션 훈련 플랫폼입니다."
|
|
/>
|
|
<meta name="robots" content="index,follow,max-snippet:160,max-image-preview:large" />
|
|
<link rel="canonical" href="https://vignette.chanpaca.net/" />
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
<link rel="shortcut icon" href="/favicon.svg" />
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:locale" content="ko_KR" />
|
|
<meta property="og:site_name" content="Vignette" />
|
|
<meta property="og:title" content="Vignette · AI 심리상담 시뮬레이션 훈련" />
|
|
<meta
|
|
property="og:description"
|
|
content="AI 내담자 회기 연습, 평가 피드백, 사례개념화 워크시트를 한 흐름으로 제공하는 상담 교육 플랫폼."
|
|
/>
|
|
<meta property="og:url" content="https://vignette.chanpaca.net/" />
|
|
<meta
|
|
property="og:image"
|
|
content="https://vignette.chanpaca.net/design-elements/clinical-paper-ambient.png"
|
|
/>
|
|
<meta property="og:image:width" content="1672" />
|
|
<meta property="og:image:height" content="941" />
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
<meta name="twitter:title" content="Vignette · AI 심리상담 시뮬레이션 훈련" />
|
|
<meta
|
|
name="twitter:description"
|
|
content="상담 수련생을 위한 AI 내담자 회기 연습과 평가 피드백 플랫폼."
|
|
/>
|
|
<meta
|
|
name="twitter:image"
|
|
content="https://vignette.chanpaca.net/design-elements/clinical-paper-ambient.png"
|
|
/>
|
|
<title>Vignette · 상담 시뮬레이션</title>
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "WebApplication",
|
|
"name": "Vignette",
|
|
"url": "https://vignette.chanpaca.net/",
|
|
"applicationCategory": "EducationalApplication",
|
|
"operatingSystem": "Web",
|
|
"inLanguage": "ko-KR",
|
|
"description": "상담 수련생이 AI 내담자와 회기를 연습하고 평가 피드백과 사례개념화 워크시트를 확인하는 교육용 AI 심리상담 시뮬레이션 훈련 플랫폼.",
|
|
"audience": {
|
|
"@type": "EducationalAudience",
|
|
"educationalRole": "counseling trainee"
|
|
},
|
|
"provider": {
|
|
"@type": "Organization",
|
|
"name": "Vignette"
|
|
}
|
|
}
|
|
</script>
|
|
<!-- 본문/UI 기본 폰트: Pretendard Variable (DESIGN_CONCEPT §3.6) -->
|
|
<!-- self-host 로 전환했다. jsDelivr 렌더 블로킹 스타일시트와 그에 딸린
|
|
preconnect/dns-prefetch 를 제거해 외부 요청을 0으로 만들었다.
|
|
@font-face 선언은 src/styles/tokens.css 최상단에 있다. -->
|
|
<link
|
|
rel="preload"
|
|
as="font"
|
|
type="font/woff2"
|
|
href="/fonts/PretendardVariable.woff2"
|
|
crossorigin
|
|
/>
|
|
<script>
|
|
(function () {
|
|
var errors = [];
|
|
var loadedAt = Date.now();
|
|
var sentDiagnostics = {};
|
|
var healthyCheckTimer = null;
|
|
var healthyObserver = null;
|
|
function short(value) {
|
|
return String(value || "").slice(0, 900);
|
|
}
|
|
function assetLabel() {
|
|
var scripts = Array.prototype.slice.call(document.scripts || []);
|
|
var match = scripts
|
|
.map(function (script) {
|
|
return script.src || "";
|
|
})
|
|
.filter(function (src) {
|
|
return src.indexOf("/assets/index-") !== -1 && /\.js($|\?)/.test(src);
|
|
})
|
|
.pop();
|
|
return match ? match.split("/").pop() : "module-not-discovered";
|
|
}
|
|
function remember(kind, detail) {
|
|
errors.push({
|
|
kind: kind,
|
|
detail: short(detail),
|
|
at: new Date().toISOString(),
|
|
});
|
|
if (errors.length > 5) errors.shift();
|
|
render("runtime-error");
|
|
}
|
|
function isVisiblyRendered(node) {
|
|
if (!node) return false;
|
|
var main = document.querySelector(".vg-main");
|
|
var mainRect = main ? main.getBoundingClientRect() : null;
|
|
var viewportTop = mainRect ? mainRect.top : 0;
|
|
var viewportBottom = mainRect ? mainRect.bottom : window.innerHeight;
|
|
var rect = node.getBoundingClientRect();
|
|
var style = window.getComputedStyle(node);
|
|
return (
|
|
rect.width > 1 &&
|
|
rect.height > 1 &&
|
|
rect.bottom > viewportTop &&
|
|
rect.top < viewportBottom &&
|
|
style.display !== "none" &&
|
|
style.visibility !== "hidden" &&
|
|
style.opacity !== "0"
|
|
);
|
|
}
|
|
function visibleCount(scope) {
|
|
var root = scope || document;
|
|
var nodes = Array.prototype.slice.call(
|
|
root.querySelectorAll(
|
|
"main,header,h1,h2,p,a,button,input,select,article,section,.vg-shell,.vg-route-error,.vgops-head,.vgops-diagnostic,.lg-root",
|
|
),
|
|
);
|
|
return nodes.filter(isVisiblyRendered).length;
|
|
}
|
|
function appHealth() {
|
|
var root = document.getElementById("root");
|
|
var path = window.location.pathname;
|
|
var isAdmin = path === "/admin" || path.indexOf("/admin/") === 0;
|
|
var text = (root && root.innerText ? root.innerText : "").trim();
|
|
var visible = root ? visibleCount(root) : 0;
|
|
var adminContent = root
|
|
? root.querySelector(
|
|
"[data-vignette-admin-root],[data-vignette-admin-diagnostic],[data-testid='admin-ai-page']",
|
|
)
|
|
: null;
|
|
var hasAdminContent = !isAdmin || isVisiblyRendered(adminContent);
|
|
return {
|
|
healthy: Boolean(
|
|
root &&
|
|
root.childElementCount > 0 &&
|
|
text.length > 24 &&
|
|
visible > 0 &&
|
|
hasAdminContent,
|
|
),
|
|
visible: visible,
|
|
};
|
|
}
|
|
function dismissDiagnosticIfHealthy() {
|
|
if (!appHealth().healthy) return false;
|
|
var panel = document.getElementById("vignette-boot-diagnostic");
|
|
if (panel) panel.remove();
|
|
return true;
|
|
}
|
|
function scheduleHealthyCheck() {
|
|
if (healthyCheckTimer !== null) window.clearTimeout(healthyCheckTimer);
|
|
healthyCheckTimer = window.setTimeout(function () {
|
|
healthyCheckTimer = null;
|
|
dismissDiagnosticIfHealthy();
|
|
}, 60);
|
|
}
|
|
function observeAppHealth() {
|
|
if (healthyObserver || !window.MutationObserver) return;
|
|
var root = document.getElementById("root");
|
|
if (!root) {
|
|
window.setTimeout(observeAppHealth, 100);
|
|
return;
|
|
}
|
|
healthyObserver = new MutationObserver(scheduleHealthyCheck);
|
|
healthyObserver.observe(root, {
|
|
attributes: true,
|
|
attributeFilter: ["aria-hidden", "class", "hidden", "style"],
|
|
characterData: true,
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
scheduleHealthyCheck();
|
|
}
|
|
function diagnosticApiBase() {
|
|
var host = window.location.hostname;
|
|
if (host === "vignette.chanpaca.net" || host.endsWith(".pages.dev")) {
|
|
return "https://api-vignette.chanpaca.net";
|
|
}
|
|
if (host === "vnet.18ka.net") {
|
|
return "https://api-vnet.18ka.net";
|
|
}
|
|
return "/api";
|
|
}
|
|
function sendDiagnostic(reason, textLength, visible) {
|
|
var key = reason + ":" + window.location.pathname;
|
|
if (sentDiagnostics[key]) return;
|
|
sentDiagnostics[key] = true;
|
|
var root = document.getElementById("root");
|
|
var payload = {
|
|
reason: reason,
|
|
path: window.location.pathname,
|
|
href: window.location.href,
|
|
asset: assetLabel(),
|
|
elapsed_ms: Date.now() - loadedAt,
|
|
body_text_length: textLength,
|
|
root_text_length: root && root.innerText ? root.innerText.trim().length : 0,
|
|
root_child_count: root ? root.childElementCount : 0,
|
|
visible_nodes: visible,
|
|
main_scroll_top:
|
|
document.querySelector(".vg-main") &&
|
|
document.querySelector(".vg-main").scrollTop
|
|
? Math.round(document.querySelector(".vg-main").scrollTop)
|
|
: 0,
|
|
document_ready_state: document.readyState,
|
|
viewport: window.innerWidth + "x" + window.innerHeight,
|
|
user_agent: short(window.navigator && window.navigator.userAgent),
|
|
errors: errors.slice(-5),
|
|
};
|
|
var url = diagnosticApiBase().replace(/\/$/, "") + "/client-diagnostics";
|
|
var body = JSON.stringify(payload);
|
|
try {
|
|
if (window.navigator && navigator.sendBeacon) {
|
|
var blob = new Blob([body], { type: "application/json" });
|
|
if (navigator.sendBeacon(url, blob)) return;
|
|
}
|
|
} catch (error) {
|
|
/* fetch fallback below */
|
|
}
|
|
try {
|
|
fetch(url, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
keepalive: true,
|
|
headers: { "Content-Type": "application/json" },
|
|
body: body,
|
|
}).catch(function () {});
|
|
} catch (error) {
|
|
/* diagnostics must never affect app boot */
|
|
}
|
|
}
|
|
function render(reason) {
|
|
var path = window.location.pathname;
|
|
var text = (document.body && document.body.innerText ? document.body.innerText : "").trim();
|
|
var health = appHealth();
|
|
if (health.healthy && reason !== "manual") {
|
|
if (reason !== "watchdog") {
|
|
sendDiagnostic(reason, text.length, health.visible);
|
|
}
|
|
dismissDiagnosticIfHealthy();
|
|
return;
|
|
}
|
|
sendDiagnostic(reason, text.length, health.visible);
|
|
|
|
var panel = document.getElementById("vignette-boot-diagnostic");
|
|
if (!panel) {
|
|
if (!document.body) {
|
|
window.setTimeout(function () {
|
|
render(reason);
|
|
}, 100);
|
|
return;
|
|
}
|
|
panel = document.createElement("section");
|
|
panel.id = "vignette-boot-diagnostic";
|
|
panel.setAttribute("role", "alert");
|
|
panel.style.cssText =
|
|
"position:fixed;z-index:2147483647;inset:24px auto auto 24px;max-width:min(760px,calc(100vw - 48px));padding:18px;border:1px solid #d59b47;border-radius:8px;background:#fffaf0;color:#21312e;box-shadow:0 18px 48px rgba(10,20,18,.18);font:13px/1.5 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;white-space:normal;";
|
|
document.body.appendChild(panel);
|
|
}
|
|
var lines = [
|
|
"<b style=\"display:block;font-size:18px;margin-bottom:6px\">Vignette 화면 진단</b>",
|
|
"<div>React 화면이 정상 콘텐츠를 표시하지 못해서 HTML 부트스트랩 진단을 띄웠습니다.</div>",
|
|
"<dl style=\"display:grid;grid-template-columns:132px minmax(0,1fr);gap:0;margin:12px 0 0;border:1px solid rgba(0,0,0,.12);border-radius:8px;overflow:hidden\">",
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">reason</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere\">" +
|
|
reason +
|
|
"</dd>",
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">path</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere\">" +
|
|
path +
|
|
"</dd>",
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">asset</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere\">" +
|
|
assetLabel() +
|
|
"</dd>",
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">bodyText</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere\">" +
|
|
text.length +
|
|
"</dd>",
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">visibleNodes</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere\">" +
|
|
health.visible +
|
|
"</dd>",
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">elapsed</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere\">" +
|
|
(Date.now() - loadedAt) +
|
|
"ms</dd>",
|
|
];
|
|
if (errors.length) {
|
|
lines.push(
|
|
"<dt style=\"padding:7px 9px;background:rgba(0,0,0,.04);font-weight:700\">errors</dt><dd style=\"margin:0;padding:7px 9px;overflow-wrap:anywhere;white-space:pre-wrap\">" +
|
|
errors
|
|
.map(function (item) {
|
|
return item.at + " " + item.kind + ": " + item.detail;
|
|
})
|
|
.join("\n") +
|
|
"</dd>",
|
|
);
|
|
}
|
|
lines.push("</dl>");
|
|
panel.innerHTML = lines.join("");
|
|
}
|
|
window.addEventListener("error", function (event) {
|
|
remember("error", event.message || event.error || "unknown error");
|
|
});
|
|
window.addEventListener("unhandledrejection", function (event) {
|
|
remember("unhandledrejection", event.reason && (event.reason.stack || event.reason.message || event.reason));
|
|
});
|
|
window.__VIGNETTE_BOOT_DIAG__ = {
|
|
check: function () {
|
|
render("manual");
|
|
},
|
|
};
|
|
observeAppHealth();
|
|
window.setTimeout(function () {
|
|
render("watchdog");
|
|
}, 3500);
|
|
window.setTimeout(function () {
|
|
render("watchdog");
|
|
}, 8000);
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
<script type="module" src="/src/main.tsx"></script>
|
|
</body>
|
|
</html>
|