Files
HeuJung 3945b6f1b3 feat(core,extensions): 구동 에셋 자체 제공 · 자산 실패 폴백 · 운영자 추가 에셋(custom/)
공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다.

브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도
남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데
자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기
하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발
대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다.
런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다.

자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그
실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML
에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다.
편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다.

두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의
custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에
의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다.
확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을
고치면 그 변경을 감지해 재게시까지 예약된다.

FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접
넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로
나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린
스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과
분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠
화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를
함께 뒀다.

동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에
써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
2026-08-27 16:47:14 +09:00

394 lines
13 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "sirsoft-basic 템플릿 SEO 페이지 생성 설정",
"text_props": ["text", "label", "value", "title"],
"attr_map": {
"className": "class",
"htmlFor": "for"
},
"allowed_attrs": [
"class", "id", "href", "src", "alt", "title", "name", "type",
"placeholder", "for", "target", "rel", "width", "height",
"role", "aria-label", "aria-describedby", "data-testid", "style"
],
"stylesheets": [
"vendor/font-awesome/6.4.0/css/all.inlined.css"
],
"self_closing": ["img", "input", "hr", "br"],
"seo_overrides": {
"_local": {
"collapsedReplies": { "*": false },
"activeTab": ["info", "reviews", "qna"]
}
},
"render_modes": {
"image_gallery": {
"type": "iterate",
"source": "$props_source",
"item_tag": "img",
"item_attrs": { "src": "{url|download_url|src|image_url}", "alt": "{alt_text_current|alt_text|alt}" }
},
"tab_list": {
"type": "iterate",
"source": "$props_source",
"item_tag": "span",
"item_content": "{label}",
"badge_field": "badge"
},
"select_options": {
"type": "iterate",
"source": "$props_source",
"item_tag": "option",
"item_attrs": { "value": "{value}" },
"item_content": "{label|label_key|text|name}"
},
"text_format": {
"type": "format"
},
"html_content": {
"type": "raw",
"source": "$props_source"
},
"pagination_links": {
"type": "pagination",
"max_links": 10
},
"header_nav": {
"type": "fields",
"source": "$all_props",
"fields": [
{ "tag": "a", "attrs": { "href": "/" }, "content": "{siteName}" },
{
"tag": "nav",
"children": [
{ "tag": "a", "attrs": { "href": "/" }, "content": "$t:nav.home" },
{ "tag": "a", "attrs": { "href": "/boards/popular" }, "content": "$t:nav.popular" },
{ "tag": "a", "attrs": { "href": "/shop/products" }, "content": "$t:nav.shop" }
]
},
{
"tag": "nav",
"iterate": "boards",
"item_tag": "a",
"item_content": "{name}",
"item_attrs": { "href": "/board/{slug}" }
}
]
},
"footer_nav": {
"type": "fields",
"source": "$all_props",
"fields": [
{ "tag": "p", "content": "{siteName}" },
{ "tag": "p", "content": "{siteDescription}", "if": "{siteDescription}" },
{
"tag": "nav",
"children": [
{ "tag": "a", "attrs": { "href": "{socialLinks.github}" }, "content": "GitHub", "if": "{socialLinks.github}" },
{ "tag": "a", "attrs": { "href": "{socialLinks.twitter}" }, "content": "Twitter", "if": "{socialLinks.twitter}" },
{ "tag": "a", "attrs": { "href": "{socialLinks.discord}" }, "content": "Discord", "if": "{socialLinks.discord}" }
]
},
{
"tag": "div",
"children": [
{
"tag": "div",
"children": [
{ "tag": "h4", "content": "$t:footer.community" },
{ "tag": "a", "attrs": { "href": "/" }, "content": "$t:nav.home" },
{ "tag": "a", "attrs": { "href": "/boards/popular" }, "content": "$t:nav.popular" },
{ "tag": "a", "attrs": { "href": "/boards" }, "content": "$t:footer.all_boards" }
]
},
{
"tag": "div",
"children": [
{ "tag": "h4", "content": "$t:footer.info" },
{ "tag": "a", "attrs": { "href": "/page/about" }, "content": "$t:footer.about" },
{ "tag": "a", "attrs": { "href": "/page/faq" }, "content": "$t:footer.faq" },
{ "tag": "a", "attrs": { "href": "/page/contact" }, "content": "$t:footer.contact" }
]
},
{
"tag": "div",
"children": [
{ "tag": "h4", "content": "$t:footer.policy" },
{ "tag": "a", "attrs": { "href": "/page/terms" }, "content": "$t:footer.terms" },
{ "tag": "a", "attrs": { "href": "/page/privacy" }, "content": "$t:footer.privacy" },
{ "tag": "a", "attrs": { "href": "/page/refund" }, "content": "$t:footer.refund" }
]
}
]
},
{ "tag": "p", "content": "© {siteName}. All rights reserved." },
{ "tag": "p", "content": "Powered by 그누보드7" }
]
},
"product_card_view": {
"type": "fields",
"source": "$props_source",
"link": {
"href": "/products/{id}",
"base_url": "$var:shopBase"
},
"fields": [
{ "tag": "img", "attrs": { "src": "{thumbnail_url}", "alt": "{name_localized|name}" } },
{ "tag": "h3", "content": "{name_localized|name}" },
{
"tag": "p",
"children": [
{ "tag": "span", "content": "{primary_category}", "if": "{primary_category}" },
{ "tag": "span", "content": "{brand_name}", "if": "{brand_name}" }
]
},
{
"tag": "p",
"children": [
{ "tag": "span", "content": "{selling_price_formatted}" },
{ "tag": "del", "content": "{list_price_formatted}", "if": "{discount_rate}" },
{ "tag": "span", "content": "{discount_rate}%", "if": "{discount_rate}" }
]
},
{
"tag": "p",
"iterate": "labels",
"item_tag": "span",
"item_content": "{name}"
},
{ "tag": "span", "content": "{sales_status_label}", "if": "{sales_status}" }
]
}
},
"component_map": {
"Div": { "tag": "div" },
"Span": { "tag": "span" },
"P": { "tag": "p" },
"H1": { "tag": "h1" },
"H2": { "tag": "h2" },
"H3": { "tag": "h3" },
"H4": { "tag": "h4" },
"H5": { "tag": "h5" },
"H6": { "tag": "h6" },
"A": { "tag": "a" },
"Button": { "tag": "button" },
"Img": { "tag": "img" },
"Input": { "tag": "input" },
"Form": { "tag": "form" },
"Ul": { "tag": "ul" },
"Ol": { "tag": "ol" },
"Li": { "tag": "li" },
"Nav": { "tag": "nav" },
"Header": { "tag": "header", "render": "header_nav" },
"Footer": { "tag": "footer", "render": "footer_nav" },
"Main": { "tag": "main" },
"Section": { "tag": "section" },
"Article": { "tag": "article" },
"Icon": { "tag": "i", "name_to_class": "fas fa-{name}" },
"Table": { "tag": "table" },
"Thead": { "tag": "thead" },
"Tbody": { "tag": "tbody" },
"Tr": { "tag": "tr" },
"Th": { "tag": "th" },
"Td": { "tag": "td" },
"Label": { "tag": "label" },
"Select": {
"tag": "select",
"render": "select_options",
"props_source": "options"
},
"Option": { "tag": "option" },
"Textarea": { "tag": "textarea" },
"Strong": { "tag": "strong" },
"Em": { "tag": "em" },
"Small": { "tag": "small" },
"Hr": { "tag": "hr" },
"Br": { "tag": "br" },
"Fragment": { "tag": "" },
"ProductImageViewer": {
"tag": "div",
"render": "image_gallery",
"props_source": "images"
},
"TabNavigation": {
"tag": "nav",
"render": "tab_list",
"props_source": "tabs"
},
"StarRating": {
"tag": "span",
"render": "text_format",
"format": "{value} / {max}",
"defaults": { "max": "5" }
},
"Pagination": {
"tag": "nav",
"render": "pagination_links"
},
"FormField": {
"tag": "div"
},
"Modal": {
"tag": "div",
"skip": true
},
"ConfirmDialog": {
"tag": "div",
"skip": true
},
"Accordion": {
"tag": "div"
},
"AccordionItem": {
"tag": "div"
},
"Tabs": {
"tag": "div"
},
"Badge": {
"tag": "span"
},
"Alert": {
"tag": "div"
},
"Toast": {
"tag": "div",
"skip": true
},
"Tooltip": {
"tag": "span"
},
"Dropdown": {
"tag": "div"
},
"Breadcrumb": {
"tag": "nav"
},
"FileUploader": {
"tag": "div",
"skip": true
},
"HtmlEditor": {
"tag": "div",
"skip": true
},
"DatePicker": {
"tag": "div",
"skip": true
},
"Toggle": {
"tag": "div",
"skip": true
},
"Checkbox": {
"tag": "div"
},
"Radio": {
"tag": "div"
},
"TagInput": {
"tag": "div",
"skip": true
},
"Sortable": {
"tag": "div",
"skip": true
},
"DynamicFieldList": {
"tag": "div",
"skip": true
},
"ProductCard": {
"tag": "article",
"render": "product_card_view",
"props_source": "product"
},
"ImageGallery": {
"tag": "div",
"render": "image_gallery",
"props_source": "images"
},
"QuantitySelector": {
"tag": "div",
"skip": true
},
"PostReactions": {
"tag": "div",
"skip": true
},
"SocialLoginButtons": {
"tag": "div",
"skip": true
},
"SearchBar": {
"tag": "div"
},
"Avatar": {
"tag": "span",
"render": "text_format",
"format": "{author.name}"
},
"UserInfo": {
"tag": "span",
"render": "text_format",
"format": "{author.name} · {subText}"
},
"HtmlContent": {
"tag": "div",
"render": "html_content",
"props_source": "content"
},
"ExpandableContent": {
"tag": "div"
},
"ThemeToggle": {
"tag": "div",
"skip": true
},
"PageTransitionIndicator": {
"tag": "div",
"skip": true
},
"MobileNav": {
"tag": "nav",
"skip": true
},
"RichTextEditor": {
"tag": "div",
"skip": true
},
"AvatarUploader": {
"tag": "div",
"skip": true
},
"PasswordInput": {
"tag": "div",
"skip": true
},
"Container": {
"tag": "div"
},
"Flex": {
"tag": "div"
},
"Grid": {
"tag": "div"
},
"SectionLayout": {
"tag": "section"
},
"ThreeColumnLayout": {
"tag": "div"
},
"DataGrid": {
"tag": "table"
},
"PageHeader": {
"tag": "header"
},
"Card": {
"tag": "article"
}
}
}