Add remaining samples, tooling, and local project assets

This commit is contained in:
2026-04-15 18:02:17 +09:00
parent 05d43a7999
commit 1ff6c6cbb2
862 changed files with 18979 additions and 21 deletions

View File

@@ -0,0 +1,175 @@
"use strict";
var aptabase = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
init: () => init,
trackEvent: () => trackEvent
});
// ../shared.ts
var defaultLocale;
var defaultIsDebug;
var isInBrowser = typeof window !== "undefined" && typeof window.fetch !== "undefined";
var isInBrowserExtension = typeof chrome !== "undefined" && !!chrome.runtime?.id;
var _sessionId = newSessionId();
var _lastTouched = /* @__PURE__ */ new Date();
var _hosts = {
US: "https://us.aptabase.com",
EU: "https://eu.aptabase.com",
DEV: "https://localhost:3000",
SH: ""
};
function inMemorySessionId(timeout) {
let now = /* @__PURE__ */ new Date();
const diffInMs = now.getTime() - _lastTouched.getTime();
const diffInSec = Math.floor(diffInMs / 1e3);
if (diffInSec > timeout) {
_sessionId = newSessionId();
}
_lastTouched = now;
return _sessionId;
}
function newSessionId() {
const epochInSeconds = Math.floor(Date.now() / 1e3).toString();
const random = Math.floor(Math.random() * 1e8).toString().padStart(8, "0");
return epochInSeconds + random;
}
function validateAppKey(appKey) {
const parts = appKey.split("-");
if (parts.length !== 3 || _hosts[parts[1]] === void 0) {
console.warn(`The Aptabase App Key "${appKey}" is invalid. Tracking will be disabled.`);
return false;
}
return true;
}
function getApiUrl(appKey, options) {
const region = appKey.split("-")[1];
if (region === "SH") {
if (!options?.host) {
console.warn(`Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`);
return;
}
return `${options.host}/api/v0/event`;
}
const host = options?.host ?? _hosts[region];
return `${host}/api/v0/event`;
}
async function sendEvent(opts) {
if (!isInBrowser && !isInBrowserExtension) {
console.warn(`Aptabase: trackEvent requires a browser environment. Event "${opts.eventName}" will be discarded.`);
return;
}
if (!opts.appKey) {
console.warn(`Aptabase: init must be called before trackEvent. Event "${opts.eventName}" will be discarded.`);
return;
}
try {
const response = await fetch(opts.apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"App-Key": opts.appKey
},
credentials: "omit",
body: JSON.stringify({
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
sessionId: opts.sessionId,
eventName: opts.eventName,
systemProps: {
locale: opts.locale ?? getBrowserLocale(),
isDebug: opts.isDebug ?? getIsDebug(),
appVersion: opts.appVersion ?? "",
sdkVersion: opts.sdkVersion
},
props: opts.props
})
});
if (response.status >= 300) {
const responseBody = await response.text();
console.warn(`Failed to send event "${opts.eventName}": ${response.status} ${responseBody}`);
}
} catch (e) {
console.warn(`Failed to send event "${opts.eventName}"`);
console.warn(e);
}
}
function getBrowserLocale() {
if (defaultLocale) {
return defaultLocale;
}
if (typeof navigator === "undefined") {
return void 0;
}
if (navigator.languages.length > 0) {
defaultLocale = navigator.languages[0];
} else {
defaultLocale = navigator.language;
}
return defaultLocale;
}
function getIsDebug() {
if (defaultIsDebug !== void 0) {
return defaultIsDebug;
}
if (true) {
defaultIsDebug = true;
return defaultIsDebug;
}
if (typeof location === "undefined") {
defaultIsDebug = false;
return defaultIsDebug;
}
defaultIsDebug = location.hostname === "localhost";
return defaultIsDebug;
}
// src/index.ts
var SESSION_TIMEOUT = 1 * 60 * 60;
var sdkVersion = `aptabase-web@${"0.4.3"}`;
var _appKey = "";
var _apiUrl;
var _options;
function init(appKey, options) {
if (!validateAppKey(appKey))
return;
_apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);
_appKey = appKey;
_options = options;
}
async function trackEvent(eventName, props) {
if (!_apiUrl)
return;
const sessionId = inMemorySessionId(SESSION_TIMEOUT);
await sendEvent({
apiUrl: _apiUrl,
sessionId,
appKey: _appKey,
isDebug: _options?.isDebug,
appVersion: _options?.appVersion,
sdkVersion,
eventName,
props
});
}
return __toCommonJS(src_exports);
})();
//# sourceMappingURL=aptabase.debug.js.map

2
samples/src/lib/aptabase.min.js vendored Normal file
View File

@@ -0,0 +1,2 @@
"use strict";var aptabase=(()=>{var a=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var I=(e,n)=>{for(var t in n)a(e,t,{get:n[t],enumerable:!0})},A=(e,n,t,o)=>{if(n&&typeof n=="object"||typeof n=="function")for(let s of y(n))!S.call(e,s)&&s!==t&&a(e,s,{get:()=>n[s],enumerable:!(o=w(n,s))||o.enumerable});return e};var E=e=>A(a({},"__esModule",{value:!0}),e);var k={};I(k,{init:()=>T,trackEvent:()=>$});var r,i,D=typeof window<"u"&&typeof window.fetch<"u",O=typeof chrome<"u"&&!!chrome.runtime?.id,p=g(),c=new Date,u={US:"https://us.aptabase.com",EU:"https://eu.aptabase.com",DEV:"https://localhost:3000",SH:""};function f(e){let n=new Date,t=n.getTime()-c.getTime();return Math.floor(t/1e3)>e&&(p=g()),c=n,p}function g(){let e=Math.floor(Date.now()/1e3).toString(),n=Math.floor(Math.random()*1e8).toString().padStart(8,"0");return e+n}function b(e){let n=e.split("-");return n.length!==3||u[n[1]]===void 0?(console.warn(`The Aptabase App Key "${e}" is invalid. Tracking will be disabled.`),!1):!0}function m(e,n){let t=e.split("-")[1];if(t==="SH"){if(!n?.host){console.warn("Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.");return}return`${n.host}/api/v0/event`}return`${n?.host??u[t]}/api/v0/event`}async function v(e){if(!D&&!O){console.warn(`Aptabase: trackEvent requires a browser environment. Event "${e.eventName}" will be discarded.`);return}if(!e.appKey){console.warn(`Aptabase: init must be called before trackEvent. Event "${e.eventName}" will be discarded.`);return}try{let n=await fetch(e.apiUrl,{method:"POST",headers:{"Content-Type":"application/json","App-Key":e.appKey},credentials:"omit",body:JSON.stringify({timestamp:new Date().toISOString(),sessionId:e.sessionId,eventName:e.eventName,systemProps:{locale:e.locale??V(),isDebug:e.isDebug??N(),appVersion:e.appVersion??"",sdkVersion:e.sdkVersion},props:e.props})});if(n.status>=300){let t=await n.text();console.warn(`Failed to send event "${e.eventName}": ${n.status} ${t}`)}}catch(n){console.warn(`Failed to send event "${e.eventName}"`),console.warn(n)}}function V(){if(r)return r;if(!(typeof navigator>"u"))return navigator.languages.length>0?r=navigator.languages[0]:r=navigator.language,r}function N(){return i!==void 0||(i=!0),i}var x=1*60*60,U="aptabase-web@0.4.3",h="",d,l;function T(e,n){b(e)&&(d=n?.apiUrl??m(e,n),h=e,l=n)}async function $(e,n){if(!d)return;let t=f(x);await v({apiUrl:d,sessionId:t,appKey:h,isDebug:l?.isDebug,appVersion:l?.appVersion,sdkVersion:U,eventName:e,props:n})}return E(k);})();
//# sourceMappingURL=aptabase.min.js.map

View File

@@ -0,0 +1,17 @@
// 상대 경로를 정확하게 맞춰야 합니다
// utils 폴더에서 content/sub로 가는 경로
const allComponents = import.meta.glob("../content/sub/**/*.astro");
export async function loadComponent(path: string) {
// path 앞에 ../ 추가
const componentPath = `../content/sub/${path}.astro`;
const loader = allComponents[componentPath];
if (!loader) {
console.error("Available paths:", Object.keys(allComponents));
console.error("Requested path:", componentPath);
throw new Error(`Component not found: ${componentPath}`);
}
return (await loader()).default;
}

View File

@@ -0,0 +1,39 @@
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeEnhancer from "./rehypeEnhancer.js";
import { resolveImagePath } from "./markdownUtils.js";
export async function markdownToHtml(raw) {
const cleaned = raw
.replace(/^import\s+.*from\s+['"].*['"];?$/gm, "")
// 🔹 참조 스타일 이미지 정의를 저장
.replace(
/\[([^\]]+)\]:\s*\.\.\/.*assets\/images\/(.+)/g,
(_, ref, path) => `[${ref}]: /civil-engineering-lab/images/${path}`
)
// 🔹 참조 스타일 이미지 사용 시 클래스 추가
.replace(
/!\[\]\[([^\]]+)\]\{\.([a-zA-Z0-9-_]+)\}/g,
(match, ref, className) => {
// ![][image1]{.my-class} → HTML로 변환
return `<img data-ref="${ref}" class="${className}" />`;
}
)
.replace(
/!\[([^\]]*)\]\((@img\/[^)]+)\)/g,
(_, alt, path) => `![${alt}](${resolveImagePath(path)})`
);
return String(
await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeEnhancer)
.use(rehypeStringify)
.process(cleaned)
);
}

View File

@@ -0,0 +1,22 @@
// /src/utils/markdownUtils.js
const imageCache = new Map();
export function resolveImagePath(src) {
if (!src) return src;
if (src.startsWith("@img/")) {
return src.replace("@img/", "/civil-engineering-lab/images/");
}
if (src.startsWith("../assets/images/")) {
return src.replace("../assets/images/", "/civil-engineering-lab/images/");
}
return src;
}
/**
* href가 외부 링크인지 확인
* - http://, https://, // 로 시작하면 외부 링크로 판단
*/
export function isExternalLink(href) {
if (!href) return false;
return /^https?:\/\//.test(href) || href.startsWith("//");
}

View File

@@ -0,0 +1,72 @@
// modalController.ts
let isInitialized = false;
let escapeHandler: ((e: KeyboardEvent) => void) | null = null;
export function initModalController() {
if (isInitialized) {
return; // 이미 초기화되었으면 재초기화하지 않음
}
isInitialized = true;
// 버튼 클릭 이벤트 위임
document.addEventListener("click", (e) => {
const target = (e.target as HTMLElement).closest("[data-modal]");
if (target) {
const modalId = target.getAttribute("data-modal");
if (modalId) openModal(modalId);
document.querySelector(".nav-wrap")?.classList.add("open");
}
// 모달 닫기 버튼 클릭
const closeBtn = (e.target as HTMLElement).closest(".modal .close");
if (closeBtn) {
const modal = closeBtn.closest(".modal") as HTMLElement;
if (modal) closeModal(modal);
document.querySelector(".nav-wrap")?.classList.remove("open");
}
// 모달 외부 클릭
const modalEl = (e.target as HTMLElement).closest(".modal");
if (modalEl && e.target === modalEl) {
closeModal(modalEl);
document.querySelector(".nav-wrap")?.classList.remove("open");
}
});
// ESC 키 이벤트
escapeHandler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
const activeModals = document.querySelectorAll<HTMLElement>(".modal");
activeModals.forEach((modal) => {
if (modal.style.display === "block") closeModal(modal);
});
}
};
document.addEventListener("keydown", escapeHandler);
}
function openModal(modalId: string) {
const modal = document.getElementById(modalId) as HTMLElement;
if (!modal) return;
const video = modal.querySelector("video") as HTMLVideoElement;
modal.style.display = "block";
if (video) {
video.currentTime = 0;
video.play();
}
}
function closeModal(modal: HTMLElement) {
const video = modal.querySelector("video") as HTMLVideoElement;
modal.style.display = "none";
if (video) {
video.pause();
video.currentTime = 0;
}
}

View File

@@ -0,0 +1,118 @@
// ✅ /help 경로용 컴포넌트 네비게이션만 정의
export const navigation = [
{
label: "인터페이스",
directory: "interface",
type: "component" as const,
items: [
{ slug: "information", title: "로그인/로그아웃" },
{ slug: "interface01", title: "메인화면구성" },
{ slug: "interface02", title: "기본기능" },
{ slug: "interface03", title: "사용자 설정 백업 & 복원" },
],
},
{
label: "사용자화",
directory: "customize",
type: "component" as const,
items: [
{ slug: "customize01", title: "명령어아이콘바" },
{ slug: "customize02", title: "시스템설정" },
{ slug: "customize03", title: "작업환경설정" },
{ slug: "customize04", title: "단축키설정" },
],
},
{
label: "멀티작업공간",
directory: "multi",
type: "component" as const,
collapsed: true,
items: [
{ slug: "multi01", title: "파일탭 분리" },
{ slug: "multi02", title: "Layout 탭 분리" },
{ slug: "multi03", title: "3D 작업 전용" },
],
},
{
label: "명령어 전체보기",
directory: "command",
type: "component" as const,
collapsed: true,
items: [
{ slug: "command01", title: "명령어 전체보기 구성" },
{ slug: "command02", title: "토목/도로 특화명령어" },
{ slug: "command03", title: "구조/배근 특화명령어" },
],
},
{
label: "스타일 관리",
directory: "style",
type: "component" as const,
items: [
{ slug: "style01", title: "선스타일 상세보기" },
{ slug: "style02", title: "면스타일 상세보기" },
{ slug: "style03", title: "문자스타일 상세보기" },
],
},
{
label: "객체특성관리",
directory: "feature",
type: "component" as const,
items: [
{ slug: "feature01", title: "속성바,속성창" },
{ slug: "feature02", title: "색상상세보기" },
],
},
{
label: "레이어관리",
directory: "layer",
type: "component" as const,
items: [{ slug: "layer01", title: "레이어 상세보기" }],
},
{
label: "통합블록관리",
directory: "block",
type: "component" as const,
items: [
{ slug: "block01", title: "블록의 종류" },
{ slug: "block02", title: "블록" },
{ slug: "block03", title: "속성블록" },
{ slug: "block04", title: "외부참조" },
{ slug: "block05", title: "블록 라이브러리" },
],
},
{
label: "인쇄",
directory: "print",
type: "component" as const,
items: [
{ slug: "print01", title: "인쇄창 화면구성" },
{ slug: "multiprint01", title: "도면 출력 옵션 설정" },
{ slug: "multiprint02", title: "다중 인쇄창 화면 구성" },
],
},
{
label: "도면관리",
directory: "floorplan",
type: "component" as const,
items: [
{ slug: "floorplan01", title: "도면탐색 및 정보열람" },
{ slug: "floorplan02", title: "도면정보 수정" },
{ slug: "floorplan03", title: "도면정보항목선택" },
],
},
];
// ✅ component 라우트만 생성
export const pageRoutes = navigation.flatMap((section) =>
section.items.map((item) => ({
slug: item.slug,
title: `가이드-${item.title}`,
type: section.type,
path: `${section.directory}/${item.slug}`,
group: section.directory,
sectionLabel: section.label,
}))
);
export const defaultPage = pageRoutes[0];

View File

@@ -0,0 +1,21 @@
// src/utils/rehypeEnhancer.js
import { visit } from "unist-util-visit";
import { resolveImagePath, isExternalLink } from "./markdownUtils.js";
export default function rehypeEnhancer() {
return (tree) => {
visit(tree, "element", (node) => {
// 이미지 경로 처리
if (node.tagName === "img" && node.properties?.src) {
node.properties.src = resolveImagePath(node.properties.src);
node.properties.loading = "lazy";
}
// 외부 링크 처리
if (node.tagName === "a" && isExternalLink(node.properties?.href)) {
node.properties.target = "_blank";
node.properties.rel = "noopener noreferrer";
}
});
};
}

View File

@@ -0,0 +1,57 @@
import { visit } from "unist-util-visit";
const parseFlag = (value, fallback = false) => {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
}
return fallback;
};
/**
* Attach hierarchical numbers (e.g., 1, 1.1, 1.1.1) to heading nodes.
* Numbers are stored in data-heading-number and can be rendered via CSS/TOC.
*/
export default function remarkNumberedHeadings(options = {}) {
const {
minLevel = 2,
maxLevel = 4,
frontmatterField = "numberedHeadings",
defaultEnabled = false,
} = options;
return (tree, file) => {
const frontmatter =
file?.data?.astro?.frontmatter ??
file?.data?.frontmatter ??
file?.data?.matter ??
{};
const enabled = parseFlag(frontmatter?.[frontmatterField], defaultEnabled);
if (!enabled) return;
const counters = Array.from({ length: maxLevel + 2 }, () => 0);
visit(tree, "heading", (node) => {
const level = node.depth ?? 0;
if (level < minLevel || level > maxLevel) return;
counters[level] += 1;
for (let i = level + 1; i < counters.length; i += 1) {
counters[i] = 0;
}
const parts = [];
for (let i = minLevel; i <= level; i += 1) {
if (counters[i] === 0) break;
parts.push(String(counters[i]));
}
const number = `${parts.join(".")}.`;
node.data = node.data ?? {};
node.data.hProperties = node.data.hProperties ?? {};
node.data.hProperties["data-heading-number"] = number;
});
};
}

View File

@@ -0,0 +1,143 @@
import fs from 'fs';
import path from 'path';
import { getCollection } from 'astro:content';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';
import { pageRoutes } from './navigation.js';
/* -------------------------------
* Markdown → 순수 텍스트
* ------------------------------- */
export async function extractPlainText(markdown) {
if (!markdown) return '';
const cleaned = markdown
.replace(/^import\s+.*from\s+['"].*['"];?$/gm, '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<[^>]+>/g, '')
.trim();
const result = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeStringify)
.process(cleaned);
return String(result)
.replace(/<[^>]+>/g, ' ')
.replace(/&[a-zA-Z#0-9]+;/g, ' ')
.replace(/\s{2,}/g, ' ')
.trim();
}
/* -------------------------------
* Astro 파일 → 순수 텍스트
* ------------------------------- */
export function extractAstroText(fileContent) {
return fileContent
.replace(/---[\s\S]*?---/g, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/\{[\s\S]*?\}/g, '')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/\s{2,}/g, ' ')
.trim();
}
/* -------------------------------
* 폴더 재귀 탐색
* ------------------------------- */
function getAstroFiles(dir) {
if (!fs.existsSync(dir)) return [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
let results = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) results = results.concat(getAstroFiles(fullPath));
else if (entry.isFile() && entry.name.endsWith('.astro')) results.push(fullPath);
}
return results;
}
/* -------------------------------
* 전체 검색 데이터 생성
* ------------------------------- */
export async function generateSearchData() {
// ① Docs 전부
const allDocs = await getCollection('docs');
const mdxSearchData = await Promise.all(
allDocs.map(async (doc) => {
const plain = await extractPlainText(doc.body);
const text = plain.toLowerCase();
const slugPath = (doc.slug || doc.id).replace(/^ko\//, '');
const url = `/${slugPath}${slugPath.endsWith('/') ? '' : '/'}`;
return {
id: slugPath,
title: doc.data.title || slugPath,
titleLower: (doc.data.title || slugPath).toLowerCase(),
content: plain.slice(0, 500),
fullContent: plain,
fullLower: text,
url,
category: 'mdx',
type: 'mdx'
};
})
);
// ② 메뉴
const menuSearchData = pageRoutes
.filter(r => r.path?.startsWith('/'))
.map(route => {
const rawTitle = route.title || path.basename(route.path);
const titleText = rawTitle.startsWith('가이드-') ? rawTitle.substring(4) : rawTitle;
return {
id: route.path,
title: titleText,
titleLower: titleText.toLowerCase(),
content: '',
fullContent: '',
fullLower: '',
url: `/help${route.path}`,
category: 'menu',
type: 'menu'
};
});
// ③ 실제 Astro 파일
const subRoot = path.resolve('./src/content/sub');
const subFiles = getAstroFiles(subRoot);
const subSearchData = subFiles.map(filePath => {
const content = fs.readFileSync(filePath, 'utf-8');
const text = extractAstroText(content);
const fileName = path.basename(filePath, '.astro');
// ✅ navigation/pageRoutes에서 title 가져오기
const route = pageRoutes.find(r => r.path.endsWith(fileName));
let title = route
? route.title.startsWith('가이드-') ? route.title.substring(4) : route.title
: content.match(/<title>(.*?)<\/title>/i)?.[1] ||
content.match(/<h1[^>]*>(.*?)<\/h1>/i)?.[1] ||
fileName;
return {
id: fileName,
title,
titleLower: title.toLowerCase(),
content: text.slice(0, 500),
fullContent: text,
fullLower: text.toLowerCase(),
url: `/help/${fileName}`,
category: 'astro-file',
type: 'astro-file'
};
});
return [...mdxSearchData, ...menuSearchData, ...subSearchData];
}