Files
HeuJung 0ff4b674ac fix(sirsoft-basic, sirsoft-admin_basic): 모바일 드로어 선호설정 아코디언화 및 뷰포트 개선
모바일 헤더 우측 그룹이 420px 를 요구해 폰 뷰포트에서 통화 선택기가 잘렸다.
언어·통화·배송국가를 드로어로 옮겨 가로 칩으로 통일했으나, 이번엔 드로어가
세로로 길어져 정작 메뉴가 스크롤 밖으로 밀렸다.

언어와 통화·배송국가를 각각 독립 아코디언(기본 접힘)으로 바꾸고, 접힘 상태에서도
트리거에 현재 선택값을 요약 표기해 펼치지 않고 확인할 수 있게 했다. 언어는 템플릿이,
통화·배송국가는 이커머스 주입 조각이 각자 상태를 소유해 서로 간섭하지 않는다.

통화 패널은 새 플래그를 만들지 않고 데스크톱 드롭다운의 기존 토글을 재사용한다 —
portable 오버라이드에서 if 를 생략하면 엔진이 base if 를 상속하므로 자동으로 기본
접힘이 된다. 이 과정에서 모바일로 펼칠 때 전체 화면 배경 오버레이가 드로어 클릭을
가로채는 결함을 함께 차단했다.

그 밖에 모달 좌우 여백, 주문 취소·일괄변경 팝업의 세로 스택 전환, 은행 관리 입력칸
축소, 쇼핑몰 환경설정·쿠폰·엑셀 다운로드 화면의 가로 넘침을 함께 해소했다.
2026-07-10 19:27:25 +09:00

441 lines
15 KiB
JSON

{
"comment": "admin 목록/폼 상태 세트. with_data/empty_data/loading + edit_existing/validation_failed/saving.",
"groups": [
{
"comment": "베이스 레이아웃 _admin_base. 모바일 드로어(left_sidebar_area)는 _global.sidebarOpen 으로 열리고, 그 안의 언어/통화/배송국가 칩(admin_drawer_prefs)은 responsive.portable 게이트라 디바이스=모바일 + 드로어 열림 상태에서만 캔버스에 보인다. 편집기 캔버스는 정적 시뮬레이션이라 햄버거 클릭이 없으므로 sidebarOpen 을 상태로 주입해야 드로어 내부를 편집할 수 있다.",
"scope": {
"kind": "base",
"match": "_admin_base"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.admin_base_default",
"default": true
},
{
"id": "sidebar_open",
"label": "$t:editor.state.sidebar_open",
"initialState": {
"global": {
"sidebarOpen": true
}
}
}
]
},
{
"scope": {
"kind": "route",
"match": "*/admin/users"
},
"items": [
{
"id": "with_data",
"label": "$t:editor.state.with_data",
"default": true
},
{
"id": "empty_data",
"label": "$t:editor.state.empty_data",
"sampleDataOverrides": {
"byDataSourceId": {
"users": {
"data": {
"data": [],
"meta": {
"total": 0,
"per_page": 20,
"current_page": 1
}
}
}
}
}
}
]
},
{
"scope": {
"kind": "route",
"match": "*/admin/users/:id/edit"
},
"items": [
{
"id": "edit_existing",
"label": "$t:editor.state.edit_existing",
"default": true,
"comment": "/1.1-bis — 편집기는 데이터소스 initLocal(\"form\")을 실행하지 않으므로(runtime API→form 흐름 부재) 수정 폼 입력칸이 비어 보인다. user.data 와 동일 값으로 _local.form 을 직접 시드해 캔버스에 prefill 한다(읽기전용 표시는 user.data, 입력칸은 _local.form). 필드명 = admin_user_form.json 의 name= 입력.",
"initialState": {
"local": {
"form": {
"name": "이서연",
"email": "seoyeon.lee@example.com",
"nickname": "서연",
"role_ids": [2, 5],
"status": "active",
"mobile": "010-3456-7890",
"phone": "02-777-8800",
"homepage": "https://seoyeon.example.com",
"zipcode": "06292",
"address": "서울특별시 강남구 강남대로 382",
"address_detail": "메리츠타워 22층",
"country": "KR",
"language": "ko",
"timezone": "Asia/Seoul",
"signature": "운영팀 이서연입니다.",
"bio": "커뮤니티 운영과 CS를 담당하고 있습니다.",
"admin_memo": "2025년 우수 운영자. CS 응대 만족도 1위. 콘텐츠 신고 처리 권한 보유."
}
}
}
},
{
"id": "create_mode",
"label": "$t:editor.state.create_mode",
"comment": " route 패치 — admin_user_form 은 {{route?.id}} 유무로 수정/신규 분기(제목·엔드포인트·메서드 PUT/POST). route.id 를 null 로 제거해 신규 작성 모드 화면을 미리본다. 신규 모드는 빈 폼이 의도이므로 _local.form 시드 없음.",
"initialState": {
"route": {
"id": null
}
}
},
{
"id": "validation_failed",
"label": "$t:editor.state.validation_failed",
"comment": "관리자 회원 폼(admin_user_form)은 _local.errors.{field}?.[0] (배열)을 읽으므로 그 경로로 배열 메시지를 주입한다(경로 직접 지정). 수정 컨텍스트이므로 edit_existing 과 동일 폼 시드 위에 검증 오류를 얹는다.",
"initialState": {
"local": {
"form": {
"name": "",
"email": "invalid-email",
"nickname": "서연",
"role_ids": [2, 5],
"status": "active",
"mobile": "010-3456-7890",
"phone": "02-777-8800",
"homepage": "https://seoyeon.example.com",
"zipcode": "06292",
"address": "서울특별시 강남구 강남대로 382",
"address_detail": "메리츠타워 22층",
"country": "KR",
"language": "ko",
"timezone": "Asia/Seoul",
"signature": "운영팀 이서연입니다.",
"bio": "커뮤니티 운영과 CS를 담당하고 있습니다.",
"admin_memo": "2025년 우수 운영자. CS 응대 만족도 1위. 콘텐츠 신고 처리 권한 보유."
}
}
},
"formErrors": {
"_local.errors.name": ["필수 입력 항목입니다"],
"_local.errors.email": ["올바른 이메일 형식이 아닙니다"]
}
}
]
},
{
"comment": " query 패치 실증 — admin_settings 는 {{_global.activeSettingsTab || query.tab || 'general'}} 으로 탭 화면을 분기한다. 편집기는 평소 query 가 비어 있어 SEO 등 비-기본 탭을 미리볼 수 없으므로, query.tab + 우선 키 _global.activeSettingsTab 을 함께 주입해 탭 변종을 미리본다.",
"scope": {
"kind": "route",
"match": "*/admin/settings"
},
"items": [
{
"id": "settings_general",
"label": "$t:editor.state.settings_general",
"default": true,
"initialState": {
"query": { "tab": "general" },
"global": { "activeSettingsTab": "general" }
}
},
{
"id": "settings_mail",
"label": "$t:editor.state.settings_mail",
"initialState": {
"query": { "tab": "mail" },
"global": { "activeSettingsTab": "mail" }
}
},
{
"id": "settings_upload",
"label": "$t:editor.state.settings_upload",
"initialState": {
"query": { "tab": "upload" },
"global": { "activeSettingsTab": "upload" }
}
},
{
"id": "settings_seo",
"label": "$t:editor.state.settings_seo",
"initialState": {
"query": { "tab": "seo" },
"global": { "activeSettingsTab": "seo" }
}
},
{
"id": "settings_security",
"label": "$t:editor.state.settings_security",
"initialState": {
"query": { "tab": "security" },
"global": { "activeSettingsTab": "security" }
}
},
{
"id": "settings_drivers",
"label": "$t:editor.state.settings_drivers",
"initialState": {
"query": { "tab": "drivers" },
"global": { "activeSettingsTab": "drivers" }
}
},
{
"id": "settings_advanced",
"label": "$t:editor.state.settings_advanced",
"initialState": {
"query": { "tab": "advanced" },
"global": { "activeSettingsTab": "advanced" }
}
},
{
"id": "settings_notification_definitions",
"label": "$t:editor.state.settings_notification_definitions",
"initialState": {
"query": { "tab": "notification_definitions" },
"global": { "activeSettingsTab": "notification_definitions" }
}
},
{
"id": "settings_language_packs",
"label": "$t:editor.state.settings_language_packs",
"initialState": {
"query": { "tab": "language_packs" },
"global": { "activeSettingsTab": "language_packs" }
}
},
{
"id": "settings_identity",
"label": "$t:editor.state.settings_identity",
"initialState": {
"query": { "tab": "identity" },
"global": { "activeSettingsTab": "identity" }
}
},
{
"id": "settings_info",
"label": "$t:editor.state.settings_info",
"initialState": {
"query": { "tab": "info" },
"global": { "activeSettingsTab": "info" }
}
}
]
},
{
"comment": "관리자 비밀번호 재설정. admin_reset_password 는 _local.tokenError / _local.success 로 토큰오류·재설정폼·완료 화면을 if 분기하고(본문 line 97/212/262), 검증 실패는 _local.resetErrors.{password,password_confirmation} 인라인 필드 에러로 표시한다(본문 line 361/370/411).",
"scope": {
"kind": "route",
"match": "*/admin/reset-password"
},
"items": [
{
"id": "token_valid",
"label": "$t:editor.state.token_valid",
"default": true,
"initialState": {
"local": {
"tokenError": false,
"success": false
}
}
},
{
"id": "token_invalid",
"label": "$t:editor.state.token_invalid",
"initialState": {
"local": {
"tokenError": true
}
}
},
{
"id": "validation_failed",
"label": "$t:editor.state.validation_failed",
"initialState": {
"local": {
"tokenError": false,
"success": false
}
},
"formErrors": {
"_local.resetErrors.password": ["필수 입력 항목입니다"],
"_local.resetErrors.password_confirmation": ["필수 입력 항목입니다"]
}
},
{
"id": "success",
"label": "$t:editor.state.reset_success",
"initialState": {
"local": {
"tokenError": false,
"success": true
}
}
}
]
},
{
"comment": "권한(역할) 폼. admin_role_form 은 {{route?.id}} 유무로 수정/신규를 분기한다(본문 line 33/71). 검증 오류는 회원 폼과 달리 _global.formErrors.{field}?.[0] 경로를 읽는다(본문 line 210/484 — 경로 직접 지정).",
"scope": {
"kind": "route",
"match": "*/admin/roles/:id/edit"
},
"items": [
{
"id": "edit_existing",
"label": "$t:editor.state.edit_existing",
"default": true
},
{
"id": "create_mode",
"label": "$t:editor.state.create_mode",
"initialState": {
"route": {
"id": null
}
}
},
{
"id": "validation_failed",
"label": "$t:editor.state.validation_failed",
"formErrors": {
"_global.formErrors.name": ["필수 입력 항목입니다"]
}
}
]
},
{
"comment": "관리자 본인인증 챌린지. auth/identity_challenge 는 (_local.render_hint ?? 'text_code') 값(text_code/link/external_redirect)으로 세 화면을 if 분기한다(본문 line 94/324/369).",
"scope": {
"kind": "route",
"match": "*/admin/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 배너 표시.",
"initialState": {
"local": {
"render_hint": "text_code",
"error": "인증번호가 올바르지 않습니다. 다시 시도해 주세요."
}
}
}
]
},
{
"comment": "템플릿 목록. admin_template_list 는 라우트 path param route.type(user/admin)으로 데이터소스 type 파라미터를 분기한다(본문 line 45/600). 탭 하이라이트는 _global.activeTemplateTab 이지만 목록 내용은 route.type 이 SSoT 이므로 두 값을 함께 주입한다.",
"scope": {
"kind": "route",
"match": "*/admin/templates/:type"
},
"items": [
{
"id": "user_templates",
"label": "$t:editor.state.tab.template_user",
"default": true,
"initialState": {
"route": { "type": "user" },
"global": { "activeTemplateTab": "user" }
}
},
{
"id": "admin_templates",
"label": "$t:editor.state.tab.template_admin",
"initialState": {
"route": { "type": "admin" },
"global": { "activeTemplateTab": "admin" }
}
}
]
},
{
"comment": "관리자 로그인. admin_login 은 _local.loginError 배너 + _local.loginErrors.{email,password} 인라인 필드 에러로 로그인 실패를 표시한다(본문 line 139/178/221).",
"scope": {
"kind": "route",
"match": "*/admin/login"
},
"items": [
{
"id": "default",
"label": "$t:editor.state.login_default",
"default": true
},
{
"id": "login_failed",
"label": "$t:editor.state.login_failed",
"formErrors": {
"_local.loginError": "올바른 이메일 형식이 아닙니다",
"_local.loginErrors.email": ["올바른 이메일 형식이 아닙니다"],
"_local.loginErrors.password": ["필수 입력 항목입니다"]
}
}
]
},
{
"comment": "관리자 비밀번호 찾기. admin_forgot_password 는 {{_local.success}} 로 입력 폼/메일 발송 완료 화면을 if 분기한다(본문 line 93/106).",
"scope": {
"kind": "route",
"match": "*/admin/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 }
}
}
]
}
]
}