Files
Gnuboard7/templates/_bundled/sirsoft-basic/editor-spec/states.json
T
2026-07-01 10:30:32 +09:00

627 lines
21 KiB
JSON

{
"comment": "라우트별 변종 화면. 로그인 실패 / 검색 결과 유무 / 알림 유무 등. 사용자 board/shop/page 라우트는 코어 그룹 concat 으로 중복되지 않도록 각 모듈 editor-spec.json 에 일원화한다(A-4/A-5).",
"groups": [
{
"scope": {
"kind": "route",
"match": "/login"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.login_default",
"default": true,
"initialState": {
"local": {}
},
"formErrors": {}
},
{
"id": "login_failed",
"label": "$t:editor.state.login_failed",
"initialState": {
"global": {
"loginError": "이메일 또는 비밀번호가 올바르지 않습니다"
}
},
"comment": "검증 오류 경로는 로그인 폼이 실제로 읽는 상태 경로를 키로 직접 지정한다. 로그인 폼은 _global.loginErrors.{field} 를 읽으므로 그 경로로 주입.",
"formErrors": {
"_global.loginErrors.email": "이메일 또는 비밀번호가 올바르지 않습니다",
"_global.loginErrors.password": "이메일 또는 비밀번호가 올바르지 않습니다"
}
}
]
},
{
"scope": {
"kind": "route",
"match": "/search"
},
"comment": "검색 결과 영역은 searchResults 데이터소스가 condition `{{query?.q}}` 로 게이트된다(본문 line 62). 검색어가 없으면 결과 영역 자체가 렌더되지 않아 두 상태가 동일해 보이므로, 두 변종 모두 query.q 를 주입해 결과 영역을 띄운 뒤 데이터 유무로 변종을 구분한다. — searchActiveTab(기본 'all') 게이트로 게시글/상품/페이지 서브탭 본체가 분기된다(_search_results.json). 각 서브탭 상태를 추가해 탭별 프레젠테이션을 캔버스에 노출(클릭 불가, 상태 패치로 시뮬레이션). all 탭 posts/pages 섹션 본체는 searchResults.data.{posts,pages}.items 바인딩이라 base 샘플에 해당 배열을 채워 렌더 보장.",
"items": [
{
"id": "with_results",
"label": "$t:editor.state.search_with_results",
"default": true,
"initialState": {
"query": {
"q": "티셔츠"
}
}
},
{
"id": "empty_results",
"label": "$t:editor.state.search_empty",
"initialState": {
"query": {
"q": "없는검색어"
}
},
"sampleDataOverrides": {
"byDataSourceId": {
"searchResults": {
"data": {
"data": [],
"meta": {
"total": 0,
"per_page": 20,
"current_page": 1,
"query": "없는검색어"
}
}
}
}
}
},
{
"id": "tab_posts",
"label": "$t:editor.state.search_tab_posts",
"initialState": {
"query": {
"q": "티셔츠"
},
"global": {
"searchActiveTab": "posts"
}
}
},
{
"id": "tab_products",
"label": "$t:editor.state.search_tab_products",
"initialState": {
"query": {
"q": "티셔츠"
},
"global": {
"searchActiveTab": "products"
}
}
},
{
"id": "tab_pages",
"label": "$t:editor.state.search_tab_pages",
"initialState": {
"query": {
"q": "티셔츠"
},
"global": {
"searchActiveTab": "pages"
}
}
}
]
},
{
"scope": {
"kind": "route",
"match": "/mypage/notifications"
},
"items": [
{
"id": "with_notifications",
"label": "$t:editor.state.notifications_with",
"default": true
},
{
"id": "empty_notifications",
"label": "$t:editor.state.notifications_empty",
"comment": "override 는 base 를 통째 교체하므로 base 와 동일 shape(data.data 배열만 비움). unread_count/페이지네이션 메타도 빈 상태 정합값으로 채워 헤더가 깨지지 않게 한다.",
"sampleDataOverrides": {
"byDataSourceId": {
"userNotifications": {
"data": {
"unread_count": 0,
"current_page": 1,
"last_page": 1,
"per_page": 10,
"total": 0,
"data": []
}
}
}
}
}
]
},
{
"comment": "내 정보 수정 화면: 로그인 사용자가 비밀번호를 재확인하는 단계와 실제 정보 수정 단계로 구분된다. profile-edit 레이아웃은 _local.isPasswordVerified 로 두 partial(_password_verify_section / _edit)을 if 분기하므로, 그 상태 키를 initialState.local 로 주입해 각 단계를 미리 본다. 검증 오류는 폼이 읽는 실제 경로(_local.verifyPasswordError / _local.profileErrors.{field})로 주입한다.",
"scope": {
"kind": "route",
"match": "/mypage/profile/edit"
},
"items": [
{
"id": "password_entry",
"label": "$t:editor.state.profile_password_entry",
"default": true,
"initialState": {
"local": {
"isPasswordVerified": false
}
}
},
{
"id": "actual_edit",
"label": "$t:editor.state.profile_actual_edit",
"initialState": {
"local": {
"isPasswordVerified": true
}
}
},
{
"id": "validation_failed",
"label": "$t:editor.state.profile_validation_failed",
"initialState": {
"local": {
"isPasswordVerified": true
}
},
"formErrors": {
"_local.profileErrors.name": [
"이름을 입력해 주세요"
],
"_local.profileErrors.nickname": [
"이미 사용 중인 닉네임입니다"
],
"_local.profileErrors.mobile": [
"올바른 휴대폰 번호 형식이 아닙니다"
]
}
}
]
},
{
"comment": "비밀번호 찾기. forgot_password 레이아웃은 {{_local.success}} 로 입력 폼/발송 완료 안내를 if 분기한다(본문 line 77/96).",
"scope": {
"kind": "route",
"match": "/forgot-password"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.forgot_default",
"default": true,
"initialState": {
"local": {
"success": false
}
}
},
{
"id": "success",
"label": "$t:editor.state.forgot_success",
"initialState": {
"local": {
"success": true
}
}
}
]
},
{
"comment": "비밀번호 재설정. reset_password 레이아웃은 _local.tokenError / _local.success 로 토큰오류·재설정폼·완료 화면을 if 분기한다(본문 line 307/390/446). 검증 오류는 폼이 읽는 _local.resetErrors.{field}?.[0] 경로로 주입.",
"scope": {
"kind": "route",
"match": "/reset-password"
},
"items": [
{
"id": "token_valid",
"label": "$t:editor.state.reset_token_valid",
"default": true,
"initialState": {
"local": {
"tokenError": false,
"success": false
}
}
},
{
"id": "token_error",
"label": "$t:editor.state.reset_token_error",
"initialState": {
"local": {
"tokenError": true
}
}
},
{
"id": "success",
"label": "$t:editor.state.reset_success",
"initialState": {
"local": {
"tokenError": false,
"success": true
}
}
}
]
},
{
"comment": "본인인증 챌린지. identity_challenge 레이아웃은 _local.render_hint 값(text_code/link/external_redirect)으로 세 화면을 if 분기한다(본문 line 134/391/496).",
"scope": {
"kind": "route",
"match": "/identity/challenge"
},
"items": [
{
"id": "text_code",
"label": "$t:editor.state.challenge_text_code",
"default": true,
"initialState": {
"local": {
"render_hint": "text_code"
}
}
},
{
"id": "link",
"label": "$t:editor.state.challenge_link",
"initialState": {
"local": {
"render_hint": "link"
}
}
},
{
"id": "external_redirect",
"label": "$t:editor.state.challenge_external",
"initialState": {
"local": {
"render_hint": "external_redirect"
}
}
},
{
"id": "code_error",
"label": "$t:editor.state.challenge_error",
"comment": "코드 입력 화면에서 인증 실패 시 _local.error 배너가 표시된다(본문 line 164/175).",
"initialState": {
"local": {
"render_hint": "text_code",
"error": "인증번호가 올바르지 않습니다. 다시 시도해 주세요."
}
}
}
]
},
{
"comment": "비밀번호 변경. change-password 레이아웃은 {{_local.success}} 로 입력 폼/완료 안내를 if 분기한다(본문 line 98/154).",
"scope": {
"kind": "route",
"match": "/mypage/change-password"
},
"items": [
{
"id": "form_default",
"label": "$t:editor.state.change_password_form",
"default": true,
"initialState": {
"local": {
"success": false
}
}
},
{
"id": "success",
"label": "$t:editor.state.change_password_success",
"initialState": {
"local": {
"success": true
}
}
}
]
},
{
"comment": "찜 목록. _list partial 이 wishlist.data.data 길이로 그리드/빈 상태를 if 분기한다(line 60/110).",
"scope": {
"kind": "route",
"match": "/mypage/wishlist"
},
"items": [
{
"id": "with_items",
"label": "$t:editor.state.wishlist_with_items",
"default": true
},
{
"id": "empty_wishlist",
"label": "$t:editor.state.wishlist_empty",
"sampleDataOverrides": {
"byDataSourceId": {
"wishlist": {
"data": {
"data": [],
"pagination": {
"total": 0,
"per_page": 20,
"current_page": 1
}
}
}
}
}
}
]
},
{
"comment": "배송지 관리. _list partial 이 addresses.data.addresses.data 길이로 목록/빈 상태를 if 분기한다(line 55/179).",
"scope": {
"kind": "route",
"match": "/mypage/addresses"
},
"items": [
{
"id": "with_addresses",
"label": "$t:editor.state.addresses_with",
"default": true
},
{
"id": "empty_addresses",
"label": "$t:editor.state.addresses_empty",
"sampleDataOverrides": {
"byDataSourceId": {
"addresses": {
"data": {
"addresses": {
"data": []
}
}
}
}
}
}
]
},
{
"comment": "회원 공개 프로필. users/show 레이아웃은 profile.data.status 가 'withdrawn' 인지로 일반/탈퇴 회원 정보를 if 분기한다(본문 line 187/269).",
"scope": {
"kind": "route",
"match": "/users/:userId"
},
"items": [
{
"id": "active_user",
"label": "$t:editor.state.user_active",
"default": true,
"comment": "override 는 base profile 샘플을 통째 교체하므로 base 와 동일 충실 shape 를 유지해야 한다(name/bio/created_at 누락 시 캔버스 헤더가 '-' 로 붕괴). status 만 active 로 고정.",
"sampleDataOverrides": {
"byDataSourceId": {
"profile": {
"data": {
"uuid": "00000000-0000-4000-8000-000000000018",
"name": "김민준",
"nickname": "민준이",
"avatar": "/images/placeholder.png",
"bio": "프론트엔드 개발과 사진을 좋아하는 평범한 회원입니다.",
"status": "active",
"status_label": "활성",
"created_at": "2025-09-14",
"is_owner": false
}
}
}
}
},
{
"id": "withdrawn_user",
"label": "$t:editor.state.user_withdrawn",
"comment": "탈퇴 회원. base shape 통째 복제 후 status=withdrawn + name 을 탈퇴 표기로 변형(users/show line 269 탈퇴 분기 본체 미리보기). bio/created_at 도 유지해 헤더 붕괴 방지.",
"sampleDataOverrides": {
"byDataSourceId": {
"profile": {
"data": {
"uuid": "00000000-0000-0000-0000-000000000099",
"name": "탈퇴한 회원",
"nickname": "탈퇴한 회원",
"avatar": null,
"bio": null,
"status": "withdrawn",
"status_label": "탈퇴",
"created_at": "2024-03-02",
"is_owner": false
}
}
}
}
}
]
},
{
"comment": "회원 작성글. users/posts 레이아웃은 userPosts.data.data 길이로 목록/빈 상태를 if 분기한다(본문 line 198/226).",
"scope": {
"kind": "route",
"match": "/users/:userId/posts"
},
"items": [
{
"id": "with_posts",
"label": "$t:editor.state.user_posts_with",
"default": true
},
{
"id": "no_posts",
"label": "$t:editor.state.user_posts_empty",
"sampleDataOverrides": {
"byDataSourceId": {
"userPosts": {
"data": {
"data": [],
"total": 0,
"current_page": 1,
"per_page": 20
}
}
}
}
}
]
},
{
"comment": "회원가입. register 레이아웃은 _local.registerError 배너 + _local.errors.{field}?.[0] 인라인 필드 에러(이메일·비밀번호·이름·닉네임)로 검증 실패를 표시한다(본문 line 54/134/183/274). 메시지 값은 캔버스 translationEngine 이 해석하는 실제 콘텐츠 키($t:auth.register.error.*)를 사용한다(editor.* 파셜은 미리보기 사전에 없어 raw 키 노출).",
"scope": {
"kind": "route",
"match": "/register"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.register_default",
"default": true
},
{
"id": "valid_input",
"label": "$t:editor.state.register_valid_input",
"comment": "PasswordInput 의 비밀번호 일치 성공 표시(녹색 '비밀번호가 일치합니다')와 길이 통과(showValidation minLength:8)는 폼 상태 값 기반이다. 확인 필드는 confirmTarget `{{_local.registerForm?.password}}` 와 자기 값(registerForm.password_confirmation)이 같을 때 일치로 표시되므로, 두 값을 8자 이상 동일하게 시드해 성공 표시를 미리본다.",
"initialState": {
"local": {
"registerForm": {
"email": "user@example.com",
"password": "password123",
"password_confirmation": "password123",
"name": "홍길동",
"nickname": "gildong"
}
}
}
},
{
"id": "validation_failed",
"label": "$t:editor.state.register_validation_failed",
"formErrors": {
"_local.registerError": "$t:auth.register.error.email_exists",
"_local.errors.email": [
"$t:auth.register.error.email_exists"
],
"_local.errors.password": [
"$t:auth.register.error.password_min|count=8"
],
"_local.errors.nickname": [
"$t:auth.register.error.nickname_required"
]
}
}
]
},
{
"comment": "마이페이지 게시판 활동: _my_posts / _my_comments 두 서브탭이 _global.boardActivitySubTab(기본 'my-posts') 게이트로 분기된다(_list.json line 291/306). 내댓글 본체(myComments, auto_fetch:false 지연소스)는 기본 상태로는 캔버스에 미렌더되므로 boardActivitySubTab 패치로 노출(탭 게이트). 편집기는 정적 시뮬레이션이라 샘플은 auto_fetch 무관하게 주입됨.",
"scope": {
"kind": "route",
"match": "/mypage/board"
},
"items": [
{
"id": "my_posts",
"label": "$t:editor.state.board_activity_my_posts",
"default": true,
"initialState": {
"global": {
"boardActivitySubTab": "my-posts"
}
}
},
{
"id": "my_comments",
"label": "$t:editor.state.board_activity_my_comments",
"initialState": {
"global": {
"boardActivitySubTab": "my-comments"
}
}
}
]
},
{
"comment": " states 전수 — 주문/결제 배송지 관리 모달(_modal_address_manage, id=addressManageModal). 주소 검색 확장점(checkout_modal_address_search_slot)은 _local.addressModalMode === 'form' 게이트 뒤라 모달 편집 모드에서 이 상태로 연다. route scope 가 아니라 modal scope 인 이유: 확장 편집 모드의 shop/checkout 은 본문 확장점이 기본 노출되어 모달이 렌더되지 않고, 모달 내부 게이트는 모달 편집 모드(scope.kind=modal)에서 열어야 한다. /shop/checkout route 그룹(normal 등)은 ecommerce 모듈 스펙 소유 — 중복 정의 금지.",
"scope": {
"kind": "modal",
"match": "addressManageModal"
},
"items": [
{
"id": "address_list",
"label": "$t:editor.state.address_modal_list",
"default": true
},
{
"id": "address_manual_form",
"label": "$t:editor.state.address_modal_form",
"initialState": {
"local": {
"addressModalMode": "form"
}
}
}
]
},
{
"comment": " states 전수 — 주문 상세. 배송지 변경 모달(_modal_change_address)의 주소 검색 확장점(modal_change_address_search_slot)은 (_local.changeAddressMode ?? 'saved') === 'manual' 게이트 뒤. 확장 편집 모드의 모달 표시용 append 와 조합해 노출한다.",
"scope": {
"kind": "route",
"match": "/mypage/orders/:order_number"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.order_show_default",
"default": true
},
{
"id": "change_address_manual",
"label": "$t:editor.state.order_change_address_manual",
"initialState": {
"local": {
"changeAddressMode": "manual"
}
}
}
]
},
{
"comment": " states 전수 — 공통 레이아웃. 모바일 메뉴 드로어(mobile_nav_drawer)는 _global.mobileMenuOpen 게이트(닫힘=translate-x-full 화면 밖)라 편집기 정적 시뮬레이션에서 편집 불가. 이 상태로 펼침을 시뮬레이션해 모바일 디바이스 탭과 조합 편집한다.",
"scope": {
"kind": "base",
"match": "_user_base"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.user_base_default",
"default": true
},
{
"id": "mobile_menu_open",
"label": "$t:editor.state.mobile_menu_open",
"initialState": {
"global": {
"mobileMenuOpen": true
}
}
}
]
}
]
}