Files
HeuJung 0dfb45951b fix(core,basic): 검색 봇 화면 렌더러 패리티 결손 일괄 수정
일반 화면(React)과 봇 화면(PHP)은 같은 레이아웃을 각각 렌더하는데 봇
화면 쪽 지원 범위가 뒤처져, 확장이 교체한 본문이 통째로 빈 채 나갔다
(공개 이슈 ). 같은 결손 계열을 전수 조사해 함께 닫는다.

- 확장 주입 props 를 렌더 진입 시점에 해석해 자손 전체에 상속
 → 게시글·답글 원글·댓글·페이지·상품 설명 5개 지점 복구
- 사용자 작성 HTML 정화기 신설 — isHtml 판정에 따라 이스케이프/정화.
 미정화 상태에서 봇 경로로 저장형 스크립트가 실제 실행됐다
- 노드 키 패리티: conditions / type:iterator / responsive 매칭 규칙 /
 children 문자열 / text 우선순위 / $t:defer / 레이아웃 초기 상태
- 표현식: 단일 바인딩 판정 교체, ||·&& 를 값 반환(JS 시맨틱)으로,
 비교식이 raw 경로에서 null 이 되던 문제
- 데이터소스: 엔드포인트 표현식 전면 해석 + 레이아웃 8곳 선언 보강
- 봇 페이지 캐시 1회 전량 무효화 업그레이드 스텝

지원 범위 SSoT 표를 문서에 고정하고, 한쪽만 고치는 것을 막는 검사 룰과
레이아웃 전수 순회 테스트로 잠갔다.
2026-07-27 10:03:06 +09:00

641 lines
27 KiB
JSON

{
"version": "1.0.0",
"layout_name": "users/show",
"extends": "_user_base",
"permissions": [],
"meta": {
"seo": {
"enabled": true,
"data_sources": ["profile", "postStats", "recentPosts"],
"priority": 0.4,
"changefreq": "monthly",
"og": {
"type": "profile"
},
"structured_data": {
"@type": "ProfilePage"
}
}
},
"data_sources": [
{
"id": "profile",
"label_key": "$t:editor.data_source.profile",
"type": "api",
"endpoint": "/api/users/{{route.userId}}/profile",
"method": "GET",
"auto_fetch": true,
"auth_required": false,
"refetchOnMount": true,
"loading_strategy": "progressive",
"errorHandling": {
"404": {
"handler": "showErrorPage",
"params": {
"target": "content"
}
}
}
},
{
"id": "postStats",
"label_key": "$t:editor.data_source.postStats",
"type": "api",
"endpoint": "/api/modules/sirsoft-board/users/{{route.userId}}/posts/stats",
"method": "GET",
"auto_fetch": true,
"auth_required": false,
"loading_strategy": "progressive"
},
{
"id": "recentPosts",
"label_key": "$t:editor.data_source.recentPosts",
"type": "api",
"endpoint": "/api/modules/sirsoft-board/users/{{route.userId}}/posts",
"method": "GET",
"auto_fetch": true,
"auth_required": false,
"loading_strategy": "progressive",
"params": {
"per_page": 5
}
}
],
"slots": {
"content": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "min-h-screen bg-gray-50 dark:bg-gray-900"
},
"children": [
{
"type": "layout",
"name": "Container",
"props": {
"className": "py-8"
},
"responsive": {
"portable": {
"props": {
"className": "py-4"
}
}
},
"children": [
{
"comment": "로딩/에러 상태 처리",
"type": "basic",
"name": "Div",
"if": "{{profile.loading}}",
"props": {
"className": "flex justify-center items-center py-20"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"
}
}
]
},
{
"comment": "프로필 내용",
"type": "basic",
"name": "Div",
"if": "{{profile.data && !profile.loading && !profile.error}}",
"props": {
"className": "space-y-6"
},
"children": [
{
"comment": "페이지 헤더",
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center gap-2 mb-6"
},
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "user",
"size": "xl",
"className": "text-gray-900 dark:text-white"
}
},
{
"type": "basic",
"name": "H1",
"props": {
"className": "text-2xl font-bold text-gray-900 dark:text-white"
},
"text": "$t:user.profile.page_title"
}
]
},
{
"comment": "프로필 카드",
"type": "basic",
"name": "Div",
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6"
},
"responsive": {
"portable": {
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4"
}
}
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex gap-6 items-start"
},
"responsive": {
"portable": {
"props": {
"className": "flex flex-col items-center gap-4"
}
}
},
"children": [
{
"comment": "아바타 (author 객체로 탈퇴 사용자 자동 처리)",
"type": "composite",
"name": "Avatar",
"props": {
"author": "{{profile.data}}",
"size": "xl"
},
"responsive": {
"portable": {
"props": {
"size": "lg"
}
}
}
},
{
"comment": "기본 정보 (일반 사용자)",
"type": "basic",
"name": "Div",
"if": "{{profile.data?.status !== 'withdrawn'}}",
"props": {
"className": "flex-1 space-y-3"
},
"responsive": {
"portable": {
"props": {
"className": "w-full space-y-3 text-center"
}
}
},
"children": [
{
"comment": "이름 (배지 제거)",
"type": "basic",
"name": "H2",
"props": {
"className": "text-2xl font-bold text-gray-900 dark:text-white"
},
"responsive": {
"portable": {
"props": {
"className": "text-xl font-bold text-gray-900 dark:text-white"
}
}
},
"text": "{{profile.data?.name ?? '-'}}"
},
{
"comment": "가입일",
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400"
},
"responsive": {
"portable": {
"props": {
"className": "flex items-center justify-center gap-2 text-sm text-gray-500 dark:text-gray-400"
}
}
},
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "calendar",
"size": "sm"
}
},
{
"type": "basic",
"name": "Span",
"text": "$t:user.profile.joined_at"
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "font-medium text-gray-700 dark:text-gray-300"
},
"text": "{{profile.data?.created_at ?? '-'}}"
}
]
},
{
"comment": "자기소개",
"if": "{{profile.data?.bio}}",
"type": "basic",
"name": "P",
"props": {
"className": "text-gray-600 dark:text-gray-300 whitespace-pre-wrap mt-4"
},
"text": "{{profile.data?.bio}}"
}
]
},
{
"comment": "기본 정보 (탈퇴한 사용자)",
"type": "basic",
"name": "Div",
"if": "{{profile.data?.status === 'withdrawn'}}",
"props": {
"className": "flex-1 space-y-3"
},
"responsive": {
"portable": {
"props": {
"className": "w-full space-y-3 text-center"
}
}
},
"children": [
{
"comment": "탈퇴한 사용자 텍스트",
"type": "basic",
"name": "H2",
"props": {
"className": "text-2xl font-bold text-gray-400 dark:text-gray-500 line-through"
},
"responsive": {
"portable": {
"props": {
"className": "text-xl font-bold text-gray-400 dark:text-gray-500 line-through"
}
}
},
"text": "$t:userinfo.withdrawn_user"
}
]
}
]
}
]
},
{
"comment": "활동 통계 카드",
"type": "basic",
"name": "Div",
"props": {
"className": "grid grid-cols-2 gap-4"
},
"responsive": {
"portable": {
"props": {
"className": "grid grid-cols-2 gap-3"
}
}
},
"children": [
{
"comment": "게시글 수",
"type": "basic",
"name": "Div",
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 text-center"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "text-3xl font-bold text-blue-600 dark:text-blue-400"
},
"text": "{{postStats.data.posts_count ?? 0 | number}}"
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "text-sm text-gray-500 dark:text-gray-400 mt-1"
},
"text": "$t:user.profile.posts_count"
}
]
},
{
"comment": "댓글 수",
"type": "basic",
"name": "Div",
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4 text-center"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "text-3xl font-bold text-green-600 dark:text-green-400"
},
"text": "{{postStats.data.comments_count ?? 0 | number}}"
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "text-sm text-gray-500 dark:text-gray-400 mt-1"
},
"text": "$t:user.profile.comments_count"
}
]
}
]
},
{
"comment": "최근 게시글 섹션",
"if": "{{recentPosts.data?.data?.length > 0}}",
"type": "basic",
"name": "Div",
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between"
},
"children": [
{
"type": "basic",
"name": "H3",
"props": {
"className": "text-lg font-semibold text-gray-900 dark:text-white"
},
"text": "$t:user.profile.recent_posts"
},
{
"type": "basic",
"name": "Button",
"props": {
"className": "text-sm text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
},
"actions": [
{
"type": "click",
"handler": "navigate",
"params": {
"path": "/users/{{route.userId}}/posts"
}
}
],
"children": [
{
"type": "basic",
"name": "Span",
"text": "$t:user.profile.view_all"
}
]
}
]
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "divide-y divide-gray-200 dark:divide-gray-700"
},
"children": [
{
"type": "basic",
"name": "Button",
"iteration": {
"source": "{{recentPosts.data?.data ?? []}}",
"item_var": "post"
},
"props": {
"className": "block w-full text-left px-6 py-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer"
},
"actions": [
{
"type": "click",
"handler": "navigate",
"params": {
"path": "/board/{{post.board_slug}}/{{post.id}}"
}
}
],
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center justify-between gap-4"
},
"children": [
{
"comment": "제목 영역",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-1 min-w-0 flex items-center gap-2 overflow-hidden"
},
"children": [
{
"comment": "게시판명 배지",
"type": "basic",
"name": "Span",
"props": {
"className": "flex-shrink-0 text-xs px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded whitespace-nowrap"
},
"text": "{{post.board_name}}"
},
{
"comment": "카테고리 배지",
"type": "basic",
"name": "Span",
"if": "{{post.category}}",
"props": {
"className": "flex-shrink-0 text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded whitespace-nowrap"
},
"text": "{{post.category}}"
},
{
"comment": "비밀글 아이콘",
"type": "basic",
"name": "Icon",
"if": "{{post.is_secret}}",
"props": {
"name": "lock",
"size": "xs",
"className": "flex-shrink-0 text-yellow-500 dark:text-yellow-400"
}
},
{
"comment": "블라인드 아이콘",
"type": "basic",
"name": "Icon",
"if": "{{post.status === 'blinded'}}",
"props": {
"name": "eye-slash",
"size": "xs",
"className": "flex-shrink-0 text-orange-500 dark:text-orange-400"
}
},
{
"comment": "제목 + 댓글수 컨테이너",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-1 min-w-0 flex items-center gap-1"
},
"children": [
{
"comment": "제목",
"type": "basic",
"name": "Span",
"props": {
"className": "truncate text-sm font-medium text-gray-900 dark:text-white"
},
"text": "{{post.title}}"
},
{
"comment": "댓글 수",
"type": "basic",
"name": "Span",
"if": "{{post.comment_count > 0}}",
"props": {
"className": "flex-shrink-0 text-xs text-blue-600 dark:text-blue-400 font-medium"
},
"text": "[{{post.comment_count}}]"
}
]
}
]
},
{
"type": "basic",
"name": "Span",
"props": {
"className": "text-xs text-gray-500 dark:text-gray-400 flex-shrink-0 hidden md:inline",
"title": "{{post.created_at ?? ''}}"
},
"text": "{{post.created_at_formatted ?? ''}}"
}
]
}
]
}
]
}
]
},
{
"comment": "최근 게시글 없음 상태",
"if": "{{!recentPosts.data?.data?.length}}",
"type": "basic",
"name": "Div",
"props": {
"className": "bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
},
"children": [
{
"type": "basic",
"name": "Div",
"props": {
"className": "px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between"
},
"children": [
{
"type": "basic",
"name": "H3",
"props": {
"className": "text-lg font-semibold text-gray-900 dark:text-white"
},
"text": "$t:user.profile.recent_posts"
},
{
"type": "basic",
"name": "Button",
"props": {
"className": "text-sm text-blue-600 dark:text-blue-400 hover:underline cursor-pointer"
},
"actions": [
{
"type": "click",
"handler": "navigate",
"params": {
"path": "/users/{{route.userId}}/posts"
}
}
],
"children": [
{
"type": "basic",
"name": "Span",
"text": "$t:user.profile.view_all"
}
]
}
]
},
{
"type": "basic",
"name": "Div",
"props": {
"className": "text-center py-12"
},
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "file-circle-xmark",
"size": "2xl",
"className": "text-gray-300 dark:text-gray-600 mb-4"
}
},
{
"type": "basic",
"name": "P",
"props": {
"className": "text-gray-500 dark:text-gray-400"
},
"text": "$t:user.profile.no_posts"
}
]
}
]
}
]
}
]
}
]
}
]
}
}