Files
Gnuboard7/templates/_bundled/sirsoft-basic/docs/layouts.md
T
HeuJung 50007d5cc6 fix(auth): 2단계 인증을 켠 사이트의 로그인 흐름 구현
2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.

원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.

이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.

## 구현

- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
 `ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
 「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
 수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
 코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
 뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
 사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
 못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
 문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.

## 이번 점검에서 함께 고친 것

- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
 그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
 한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
 성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
 같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.

## 버전

코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
2026-09-07 17:08:14 +09:00

984 lines
45 KiB
Markdown

# Basic — 레이아웃
> 레이아웃 목록과 라우트 매핑 · 진입점: [AGENTS.md](../AGENTS.md)
## 레이아웃 목록
<!-- @generated:layouts START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
레이아웃 166개 (루트: `layouts`).
| 그룹 | 개수 |
|---|---|
| `(root)` | 2개 |
| `auth` | 5개 |
| `board` | 5개 |
| `errors` | 6개 |
| `mypage` | 11개 |
| `page` | 1개 |
| `partials` | 124개 |
| `search` | 1개 |
| `shop` | 9개 |
| `users` | 2개 |
| 레이아웃 | 그룹 | 종류 | extends |
|---|---|---|---|
| `_user_base` | `(root)` | partial | - |
| `forgot_password` | `auth` | 화면 | `_user_base` |
| `identity_challenge` | `auth` | 화면 | `_user_base` |
| `login` | `auth` | 화면 | `_user_base` |
| `register` | `auth` | 화면 | `_user_base` |
| `reset_password` | `auth` | 화면 | `_user_base` |
| `boards` | `board` | 화면 | `_user_base` |
| `form` | `board` | 화면 | `_user_base` |
| `index` | `board` | 화면 | `_user_base` |
| `popular` | `board` | 화면 | `_user_base` |
| `show` | `board` | 화면 | `_user_base` |
| `401` | `errors` | 화면 | `_user_base` |
| `403` | `errors` | 화면 | `_user_base` |
| `404` | `errors` | 화면 | `_user_base` |
| `500` | `errors` | 화면 | `_user_base` |
| `503` | `errors` | 화면 | `_user_base` |
| `maintenance` | `errors` | 화면 | `_user_base` |
| `home` | `(root)` | 화면 | `_user_base` |
| `addresses` | `mypage` | 화면 | `_user_base` |
| `board` | `mypage` | 화면 | `_user_base` |
| `change-password` | `mypage` | 화면 | `_user_base` |
| `inquiries` | `mypage` | 화면 | `_user_base` |
| `mileage` | `mypage` | 화면 | `_user_base` |
| `notifications` | `mypage` | 화면 | `_user_base` |
| `orders` | `mypage` | 화면 | `_user_base` |
| `show` | `mypage` | 화면 | `_user_base` |
| `profile-edit` | `mypage` | 화면 | `_user_base` |
| `profile` | `mypage` | 화면 | `_user_base` |
| `wishlist` | `mypage` | 화면 | `_user_base` |
| `show` | `page` | 화면 | `_user_base` |
| `_identity_challenge_modal` | `partials` | partial | - |
| `_modal_notification_delete_all_confirm` | `partials` | partial | - |
| `_login_form` | `partials` | partial | - |
| `_modal_privacy` | `partials` | partial | - |
| `_modal_terms` | `partials` | partial | - |
| `_redirect_if_logged_in` | `partials` | partial | - |
| `_register_form` | `partials` | partial | - |
| `_board_card` | `partials` | partial | - |
| `_parent_post` | `partials` | partial | - |
| `_password_verify_modal` | `partials` | partial | - |
| `_post_form` | `partials` | partial | - |
| `_type_renderer` | `partials` | partial | - |
| `_admin_links` | `partials` | partial | - |
| `_empty_states` | `partials` | partial | - |
| `_loading_error` | `partials` | partial | - |
| `_type_renderer` | `partials` | partial | - |
| `_write_button` | `partials` | partial | - |
| `_popular_item` | `partials` | partial | - |
| `_popular_list` | `partials` | partial | - |
| `_comment_input` | `partials` | partial | - |
| `_comment_item` | `partials` | partial | - |
| `_comment_section` | `partials` | partial | - |
| `_navigation` | `partials` | partial | - |
| `_post_attachments` | `partials` | partial | - |
| `_reply_section` | `partials` | partial | - |
| `_type_renderer` | `partials` | partial | - |
| `_modal_delete` | `partials` | partial | - |
| `_modal_report` | `partials` | partial | - |
| `_password_verify_modal` | `partials` | partial | - |
| `form` | `partials` | 화면 | - |
| `index` | `partials` | 화면 | - |
| `show` | `partials` | 화면 | - |
| `index` | `partials` | 화면 | - |
| `index` | `partials` | 화면 | - |
| `_currency_selector` | `partials` | partial | - |
| `_board_summary` | `partials` | partial | - |
| `_community_guide` | `partials` | partial | - |
| `_popular_boards` | `partials` | partial | - |
| `_recent_posts` | `partials` | partial | - |
| `_shop_promo` | `partials` | partial | - |
| `_stat_card_boards` | `partials` | partial | - |
| `_stat_card_comments` | `partials` | partial | - |
| `_stat_card_posts` | `partials` | partial | - |
| `_stat_card_users` | `partials` | partial | - |
| `_welcome_card` | `partials` | partial | - |
| `_tab_navigation` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_modal_address` | `partials` | partial | - |
| `_modal_confirm_delete` | `partials` | partial | - |
| `_modal_confirm_overwrite` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_my_comments` | `partials` | partial | - |
| `_my_posts` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_history` | `partials` | partial | - |
| `_items` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_modal_cancel` | `partials` | partial | - |
| `_modal_change_address` | `partials` | partial | - |
| `_modal_confirm_purchase` | `partials` | partial | - |
| `_modal_write_review` | `partials` | partial | - |
| `_orderer` | `partials` | partial | - |
| `_payment` | `partials` | partial | - |
| `_shipping` | `partials` | partial | - |
| `_status_header` | `partials` | partial | - |
| `_edit` | `partials` | partial | - |
| `_modal_withdraw` | `partials` | partial | - |
| `_password_verify_section` | `partials` | partial | - |
| `_view` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_search_filters` | `partials` | partial | - |
| `_search_input` | `partials` | partial | - |
| `_search_results` | `partials` | partial | - |
| `_search_states` | `partials` | partial | - |
| `_search_tabs` | `partials` | partial | - |
| `_section` | `partials` | partial | - |
| `_list` | `partials` | partial | - |
| `_section` | `partials` | partial | - |
| `_item_card` | `partials` | partial | - |
| `_item_list` | `partials` | partial | - |
| `_section` | `partials` | partial | - |
| `_cart_item` | `partials` | partial | - |
| `_cart_list` | `partials` | partial | - |
| `_cart_summary` | `partials` | partial | - |
| `_checkout_discount` | `partials` | partial | - |
| `_checkout_items` | `partials` | partial | - |
| `_checkout_mileage` | `partials` | partial | - |
| `_checkout_orderer` | `partials` | partial | - |
| `_checkout_payment` | `partials` | partial | - |
| `_checkout_shipping` | `partials` | partial | - |
| `_checkout_summary` | `partials` | partial | - |
| `_modal_address_manage` | `partials` | partial | - |
| `_modal_cart_delete_confirm` | `partials` | partial | - |
| `_modal_cart_option_change` | `partials` | partial | - |
| `_modal_cart_unavailable` | `partials` | partial | - |
| `_modal_coupon_download` | `partials` | partial | - |
| `_modal_exclusive_coupon_confirm` | `partials` | partial | - |
| `_modal_temp_order_not_found` | `partials` | partial | - |
| `_product_purchase_card` | `partials` | partial | - |
| `_admin_edit_link` | `partials` | partial | - |
| `_header` | `partials` | partial | - |
| `_info_summary` | `partials` | partial | - |
| `_modal_cart_added` | `partials` | partial | - |
| `_modal_coupon_download_confirm` | `partials` | partial | - |
| `_modal_inquiry_delete` | `partials` | partial | - |
| `_modal_login_required` | `partials` | partial | - |
| `_modal_qna_reply` | `partials` | partial | - |
| `_modal_qna_write` | `partials` | partial | - |
| `_modal_review_image` | `partials` | partial | - |
| `_price_mobile` | `partials` | partial | - |
| `_purchase_card` | `partials` | partial | - |
| `_review_avatar` | `partials` | partial | - |
| `_tab_detail` | `partials` | partial | - |
| `_tab_qna` | `partials` | partial | - |
| `_tab_reviews` | `partials` | partial | - |
| `_category_breadcrumb` | `partials` | partial | - |
| `_category_filter` | `partials` | partial | - |
| `_new_products` | `partials` | partial | - |
| `_popular_products` | `partials` | partial | - |
| `_product_grid` | `partials` | partial | - |
| `_recent_products` | `partials` | partial | - |
| `_search_filter_bar` | `partials` | partial | - |
| `index` | `search` | 화면 | `_user_base` |
| `cart` | `shop` | 화면 | `_user_base` |
| `category` | `shop` | 화면 | `_user_base` |
| `checkout` | `shop` | 화면 | `_user_base` |
| `guest_order_form` | `shop` | 화면 | `_user_base` |
| `guest_order_show` | `shop` | 화면 | `_user_base` |
| `index` | `shop` | 화면 | `_user_base` |
| `order_complete` | `shop` | 화면 | `_user_base` |
| `reorder` | `shop` | 화면 | `_user_base` |
| `show` | `shop` | 화면 | `_user_base` |
| `posts` | `users` | 화면 | `_user_base` |
| `show` | `users` | 화면 | `_user_base` |
<!-- @generated:layouts END -->
<!-- @intent START -->
166개 중 **124개가 partial** 입니다(화면 42). 이 비율이 이 템플릿의 구조를 그대로 보여줍니다 —
화면 하나가 여러 조각으로 나뉘어 있고, 조각은 여러 화면이 공유합니다. 화면 하나를 고칠 때는
그 화면 이름의 partials 디렉토리를 함께 열어야 전체가 보입니다.
그룹은 방문자 여정과 1:1 입니다 — `auth`(로그인·가입·비밀번호·본인인증) · `board`(게시판) ·
`shop`(상점) · `mypage`(마이페이지) · `page`(단일 문서) · `search` · `users` · `errors`.
`(root)` 둘은 `_user_base`(모든 화면의 베이스)와 `home` 입니다.
**모든 화면이 `_user_base` 를 상속합니다.** 헤더·푸터·모바일 네비·토스트·모달 호스트가 거기
있으므로, `extends` 없이 독립 레이아웃을 새로 만들면 그 화면에서는 `toast` 나 `openModal` 이
성공으로 기록되지만 **화면에는 아무것도 나타나지 않습니다**(호스트 컴포넌트가 마운트되지 않아서).
새 화면은 특별한 이유가 없는 한 `_user_base` 를 상속합니다.
`errors/` 6종(401·403·404·500·503·maintenance)은 코어가 오류 상황에서 직접 부릅니다. 401 은
**로그인 리다이렉트를 여기서 구현하지 않습니다** — 코어 `TemplateApp.showRouteError` 가드가
처리하므로, 이 레이아웃에서 다시 이동시키면 이중 리다이렉트가 됩니다.
<!-- @intent END -->
## 라우트 매핑
<!-- @generated:layout-map START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 경로 | 레이아웃 | 이름 |
|---|---|---|
| `/` | `home` | - |
| `/login` | `auth/login` | - |
| `/register` | `auth/register` | - |
| `/forgot-password` | `auth/forgot_password` | - |
| `/reset-password` | `auth/reset_password` | - |
| `/identity/challenge` | `auth/identity_challenge` | - |
| `/boards` | `board/boards` | - |
| `/boards/popular` | `board/popular` | - |
| `/board/:slug` | `board/index` | - |
| `/board/:slug/write` | `board/form` | - |
| `/board/:slug/:id` | `board/show` | - |
| `/board/:slug/:id/edit` | `board/form` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/products` | `shop/index` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/category/:slug` | `shop/category` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/products/:product_code` | `shop/show` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/cart` | `shop/cart` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/checkout` | `shop/checkout` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/orders/:id/complete` | `shop/order_complete` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/guest/orders` | `shop/guest_order_form` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/guest/orders/:order_number` | `shop/guest_order_show` | - |
| `/{{_global.modules?.['sirsoft-ecommerce']?.basic_info?.no_route ? '' : (_global.modules?.['sirsoft-ecommerce']?.basic_info?.route_path ?? 'shop')}}/reorder/:id` | `shop/reorder` | - |
| `/mypage` | `-` | - |
| `/mypage/profile` | `mypage/profile` | - |
| `/mypage/profile/edit` | `mypage/profile-edit` | - |
| `/mypage/change-password` | `mypage/change-password` | - |
| `/mypage/orders` | `mypage/orders` | - |
| `/mypage/orders/:order_number` | `mypage/orders/show` | - |
| `/mypage/mileage` | `mypage/mileage` | - |
| `/mypage/wishlist` | `mypage/wishlist` | - |
| `/mypage/addresses` | `mypage/addresses` | - |
| `/mypage/notifications` | `mypage/notifications` | - |
| `/mypage/board` | `mypage/board` | - |
| `/mypage/inquiries` | `mypage/inquiries` | - |
| `/users/:userId` | `users/show` | - |
| `/users/:userId/posts` | `users/posts` | - |
| `/page/:slug` | `page/show` | - |
| `/search` | `search/index` | - |
| `/404` | `errors/404` | - |
| `/403` | `errors/403` | - |
| `/500` | `errors/500` | - |
<!-- @generated:layout-map END -->
<!-- @intent START -->
상점 경로 9개만 **표현식**입니다. 운영자가 이커머스 환경설정에서 상점 경로(`route_path`,
기본 `shop`)를 바꾸거나 아예 루트로 두면(`no_route`), 그 설정이 라우트 문자열에 그대로
반영됩니다 — `routes.json` 이 정적 파일이 아니라 **표현식을 담을 수 있다**는 것이 이 템플릿의
전제입니다.
그래서 상점 라우트를 손볼 때는 세 경우를 함께 생각해야 합니다: 기본(`/shop/...`) · 운영자
지정 경로(`/store/...`) · 루트 배치(`/products`). 문자열을 하드코딩하면 뒤의 둘이 조용히
깨집니다.
`/mypage` 자체는 레이아웃이 `-` 입니다 — 진입하면 하위 탭 중 하나로 넘기는 자리이며 자기
화면을 갖지 않습니다.
라우트를 바꾸면 `php artisan template:update sirsoft-basic --force` 로 반영합니다. 빌드는
필요 없습니다.
<!-- @intent END -->
## 확장 오버라이드
<!-- @generated:template-overrides START — ext:docgen 이 갱신. 이 블록 안은 직접 수정하지 않는다 -->
| 대상 | 설명 |
|---|---|
| `extensions/sirsoft-daum_postcode/user-address-search.json` | 모듈/플러그인 확장 조각을 대체하는 오버라이드 |
<!-- @generated:template-overrides END -->
<!-- @intent START -->
오버라이드는 **확장이 제공한 조각을 이 템플릿의 것으로 갈아 끼우는** 장치입니다. 지금은 하나
있습니다 — `sirsoft-daum_postcode` 의 주소 검색 조각.
플러그인이 제공하는 원본 조각은 이커머스 관리자 화면에 맞춰져 있어, 방문자 화면의 배송지 입력
디자인과 어긋납니다. 조각을 고치는 대신 **템플릿이 자기 버전을 얹는** 것이 이 방향입니다 —
플러그인을 업데이트해도 이 오버라이드는 남고, 다른 템플릿은 원본을 그대로 씁니다.
그 대가로 **원본이 바뀌면 이 사본은 따라가지 않습니다.** 플러그인이 조각의 동작(핸들러 이름·
필드 계약)을 바꾸면 오버라이드만 옛 계약을 붙들고 있게 되고, 증상은 "주소 검색 버튼이
무반응" 으로만 나타납니다. 해당 플러그인을 업그레이드한 뒤에는 이 오버라이드를 함께 확인합니다.
<!-- @intent END -->
## 이관 원문 상세
> 아래는 코어 `docs/frontend/templates/sirsoft-basic/layouts.md` 에 있던 원문을 이 문서로 옮긴 것입니다(#601). 이관 시점 그대로
> 보존하되, **코드가 SSoT 인 값과 어긋나는 부분에는 정정 주석**을 달았습니다.
### sirsoft-basic 레이아웃
> **템플릿 식별자**: `sirsoft-basic` (type: user)
> **관련 문서**: [컴포넌트](components.md) | [핸들러](handlers.md) | [레이아웃 JSON 스키마](../../../../docs/frontend/layout-json.md)
> **정정(#601)**: 아래 페이지 맵·개수는 이관 시점 값입니다. 실측 총계와 그룹별 분포는 위
> 「레이아웃 목록」 블록이 SSoT 이며, 이관 이후 추가·삭제된 레이아웃이 있을 수 있습니다.
---
### TL;DR (5초 요약)
```text
1. 베이스: _user_base.json (헤더 + 푸터 + 모바일 네비 + 콘텐츠 슬롯)
2. 코어 페이지 30개+: 인증, 게시판, 쇼핑몰, 마이페이지, 검색, 프로필
3. Partial 100개+: 폼, 모달, 탭, 카드, 목록, 필터, 타입별 렌더러
4. 에러 페이지 6개: 401, 403, 404, 500, 503, maintenance
5. 패턴: 게시판(타입별 렌더러), 쇼핑몰(장바구니/결제 플로우), 마이페이지(탭 네비게이션)
```
---
### 목차
1. [페이지 맵 (트리 구조)](#페이지-맵-트리-구조)
2. [카테고리별 가이드](#카테고리별-가이드)
- [인증 페이지 패턴](#인증-페이지-패턴)
- [게시판 패턴](#게시판-패턴)
- [쇼핑몰 패턴](#쇼핑몰-패턴)
- [마이페이지 패턴](#마이페이지-패턴)
- [검색 패턴](#검색-패턴)
- [기타 페이지 패턴](#기타-페이지-패턴)
- [에러 페이지 패턴](#에러-페이지-패턴)
---
### 페이지 맵 (트리 구조)
```text
_user_base.json (베이스 레이아웃)
│
├── home.json (홈/대시보드)
│ └── partials/home/
│ ├── _welcome_card.json (환영 카드)
│ ├── _stat_card_users.json (통계: 사용자)
│ ├── _stat_card_posts.json (통계: 게시글)
│ ├── _stat_card_comments.json (통계: 댓글)
│ ├── _stat_card_boards.json (통계: 게시판)
│ ├── _recent_posts.json (최근 게시글)
│ ├── _popular_boards.json (인기 게시판)
│ ├── _board_summary.json (게시판 요약)
│ ├── _community_guide.json (커뮤니티 가이드)
│ └── _shop_promo.json (쇼핑 프로모션)
│
├── 인증 (auth/)
│ ├── login.json (로그인)
│ ├── register.json (회원가입)
│ ├── forgot_password.json (비밀번호 찾기)
│ ├── reset_password.json (비밀번호 재설정)
│ └── partials/auth/
│ ├── _login_form.json (로그인 폼)
│ ├── _register_form.json (회원가입 폼)
│ ├── _redirect_if_logged_in.json (로그인 상태 리다이렉트)
│ ├── _modal_terms.json (이용약관 모달)
│ └── _modal_privacy.json (개인정보 처리방침 모달)
│
├── 게시판 (board/)
│ ├── boards.json (게시판 목록)
│ │ └── partials/board/boards/
│ │ └── _board_card.json (게시판 카드)
│ ├── index.json (게시글 목록)
│ │ └── partials/board/index/
│ │ ├── _type_renderer.json (타입별 렌더러 분기)
│ │ ├── _empty_states.json (빈 상태)
│ │ ├── _loading_error.json (로딩/에러)
│ │ └── _write_button.json (글쓰기 버튼)
│ ├── show.json (게시글 상세)
│ │ └── partials/board/show/
│ │ ├── _type_renderer.json (타입별 렌더러 분기)
│ │ ├── _comment_section.json (댓글 섹션)
│ │ ├── _comment_item.json (댓글 아이템)
│ │ ├── _comment_input.json (댓글 입력)
│ │ ├── _reply_section.json (답글 섹션)
│ │ ├── _navigation.json (이전글/다음글)
│ │ ├── _post_attachments.json (첨부파일)
│ │ └── modals/
│ │ ├── _modal_delete.json (삭제 확인)
│ │ ├── _modal_report.json (신고)
│ │ └── _password_verify_modal.json (비밀번호 확인)
│ ├── form.json (게시글 작성/수정)
│ │ └── partials/board/form/
│ │ ├── _post_form.json (게시글 폼)
│ │ ├── _parent_post.json (원글 표시)
│ │ ├── _type_renderer.json (타입별 렌더러 분기)
│ │ └── _password_verify_modal.json (비밀번호 확인)
│ ├── popular.json (인기글)
│ │ └── partials/board/popular/
│ │ ├── _popular_list.json (인기글 리스트)
│ │ └── _popular_item.json (인기글 아이템)
│ └── partials/board/types/ (게시판 타입별 부분 레이아웃)
│ ├── basic/
│ │ ├── index.json (기본 목록)
│ │ ├── show.json (기본 상세)
│ │ └── form.json (기본 폼)
│ ├── card/
│ │ └── index.json (카드형 목록)
│ └── gallery/
│ └── index.json (갤러리형 목록)
│
├── 쇼핑몰 (shop/)
│ ├── index.json (상품 목록/메인)
│ │ └── partials/shop/list/
│ │ ├── _product_grid.json (상품 그리드)
│ │ ├── _popular_products.json (인기 상품)
│ │ ├── _new_products.json (신상품)
│ │ ├── _recent_products.json (최근 본 상품)
│ │ ├── _category_filter.json (카테고리 필터)
│ │ ├── _category_breadcrumb.json (카테고리 경로)
│ │ └── _search_filter_bar.json (검색/필터 바)
│ ├── category.json (카테고리 상품)
│ ├── show.json (상품 상세)
│ │ └── partials/shop/detail/
│ │ ├── _header.json (상품 헤더)
│ │ ├── _info_summary.json (상품 정보 요약)
│ │ ├── _purchase_card.json (구매 카드)
│ │ ├── _price_mobile.json (모바일 가격)
│ │ ├── _tab_detail.json (상세 탭)
│ │ ├── _tab_reviews.json (리뷰 탭)
│ │ ├── _tab_qna.json (Q&A 탭)
│ │ ├── _review_avatar.json (리뷰 아바타)
│ │ ├── _modal_cart_added.json (장바구니 추가 확인)
│ │ ├── _modal_coupon_download_confirm.json (쿠폰 다운로드 확인)
│ │ ├── _modal_login_required.json (로그인 필요)
│ │ └── _modal_review_image.json (리뷰 이미지 확대)
│ ├── cart.json (장바구니)
│ │ └── partials/shop/
│ │ ├── _cart_list.json (장바구니 목록)
│ │ ├── _cart_item.json (장바구니 아이템)
│ │ ├── _cart_summary.json (장바구니 요약)
│ │ ├── _product_purchase_card.json (구매 카드)
│ │ ├── _modal_cart_delete_confirm.json (삭제 확인)
│ │ ├── _modal_cart_option_change.json (옵션 변경)
│ │ ├── _modal_cart_unavailable.json (품절 안내)
│ │ └── _modal_temp_order_not_found.json (임시주문 없음)
│ ├── checkout.json (결제)
│ │ └── partials/shop/
│ │ ├── _checkout_orderer.json (주문자 정보)
│ │ ├── _checkout_shipping.json (배송 정보)
│ │ ├── _checkout_items.json (주문 상품)
│ │ ├── _checkout_discount.json (할인/쿠폰)
│ │ ├── _checkout_mileage.json (마일리지)
│ │ ├── _checkout_payment.json (결제 수단)
│ │ ├── _checkout_summary.json (주문 요약)
│ │ ├── _modal_address_manage.json (배송지 관리)
│ │ ├── _modal_coupon_download.json (쿠폰 다운로드)
│ │ └── _modal_exclusive_coupon_confirm.json (전용 쿠폰 확인)
│ └── order_complete.json (주문 완료)
│
├── 마이페이지 (mypage/)
│ ├── profile.json (프로필 보기)
│ ├── profile-edit.json (프로필 수정)
│ │ └── partials/mypage/profile/
│ │ ├── _view.json (프로필 보기 섹션)
│ │ ├── _edit.json (프로필 수정 섹션)
│ │ ├── _password_verify_section.json (비밀번호 확인)
│ │ └── _modal_withdraw.json (회원 탈퇴)
│ ├── orders.json (주문 내역)
│ │ └── partials/mypage/orders/
│ │ └── _list.json (주문 목록)
│ ├── orders/show.json (주문 상세)
│ │ └── partials/mypage/orders/
│ │ ├── _status_header.json (주문 상태 헤더)
│ │ ├── _items.json (주문 상품)
│ │ ├── _shipping.json (배송 정보)
│ │ ├── _payment.json (결제 정보)
│ │ ├── _history.json (주문 이력)
│ │ ├── _modal_cancel.json (주문 취소)
│ │ └── _modal_change_address.json (배송지 변경)
│ ├── board.json (내 게시글/댓글)
│ │ └── partials/mypage/board/
│ │ ├── _list.json (탭 목록)
│ │ ├── _my_posts.json (내 게시글)
│ │ └── _my_comments.json (내 댓글)
│ ├── addresses.json (배송지 관리)
│ │ └── partials/mypage/addresses/
│ │ ├── _list.json (배송지 목록)
│ │ ├── _modal_address.json (배송지 추가/수정)
│ │ ├── _modal_confirm_delete.json (삭제 확인)
│ │ └── _modal_confirm_overwrite.json (덮어쓰기 확인)
│ ├── notifications.json (알림)
│ │ └── partials/mypage/notifications/
│ │ └── _list.json (알림 목록)
│ ├── change-password.json (비밀번호 변경)
│ ├── wishlist.json (위시리스트)
│ │ └── partials/mypage/wishlist/
│ │ └── _list.json (위시리스트 목록)
│ ├── inquiries.json (상품 1:1 문의내역)
│ │ └── partials/mypage/inquiries/
│ │ └── _list.json (문의 목록)
│ └── partials/mypage/
│ └── _tab_navigation.json (마이페이지 공통 탭)
│ hiddenTabIds: inquiries 탭은 sirsoft-ecommerce inquiry 설정 시에만 노출
│
├── 검색 (search/)
│ └── index.json (통합 검색)
│ └── partials/search/
│ ├── _search_input.json (검색 입력)
│ ├── _search_tabs.json (검색 탭)
│ ├── _search_filters.json (검색 필터)
│ ├── _search_results.json (검색 결과)
│ ├── _search_states.json (검색 상태)
│ ├── posts/
│ │ ├── _section.json (게시글 결과 섹션)
│ │ └── _list.json (게시글 결과 목록)
│ ├── products/
│ │ ├── _section.json (상품 결과 섹션)
│ │ ├── _item_card.json (상품 카드)
│ │ └── _item_list.json (상품 리스트)
│ └── pages/
│ └── _section.json (페이지 결과 섹션)
│
├── 사용자 (users/)
│ ├── show.json (사용자 프로필)
│ └── posts.json (사용자 게시글)
│
├── 페이지 (page/)
│ └── show.json (정적 페이지)
│
├── 공통 Partial
│ └── partials/common/
│ └── _currency_selector.json (통화 선택기)
│
└── 에러 페이지 (errors/)
├── 401.json (인증 필요)
├── 403.json (접근 거부)
├── 404.json (페이지 없음)
├── 500.json (서버 오류)
├── 503.json (서비스 불가)
└── maintenance.json (점검 중)
```
---
### 카테고리별 가이드
#### 인증 페이지 패턴
**대표**: `auth/login.json`, `auth/register.json`
**구성**:
```text
extends: _user_base
slots.content:
└── Flex (중앙 정렬)
├── _redirect_if_logged_in.json (로그인 시 리다이렉트)
└── Div (카드 스타일 래퍼)
├── H2 (제목)
├── partial: _login_form.json 또는 _register_form.json
└── 하단 링크 (회원가입/로그인 전환, 비밀번호 찾기)
```
**특수사항**:
- `_redirect_if_logged_in.json`: 이미 로그인한 사용자를 홈으로 리다이렉트하는 partial
- 로그인 폼은 `SocialLoginButtons` 포함 (소셜 로그인 지원)
- 회원가입 폼은 `PasswordInput` 사용 (비밀번호 규칙 검증, 확인 일치)
- 약관 동의 모달: `_modal_terms.json`, `_modal_privacy.json`
- `blur_until_loaded`: 현재 사용자 정보 로드 전까지 폼 블러 처리
**핸들러 패턴**:
- `apiCall` — 로그인 (POST `/api/auth/login`), 회원가입 (POST `/api/auth/register`)
- `navigate` — 성공 후 홈/대시보드로 이동
- `setState` — 폼 에러, 로딩 상태
**2단계 인증(인증번호) 단계**
관리자가 보안 설정에서 2단계 인증을 켠 사이트에서는 로그인 응답이 두 형태로 갈립니다 —
정상 로그인과 인증번호 요구(challenge)입니다. `_login_form.json` 은 그 둘을 **같은 카드 안에서**
단계 전환으로 처리합니다.
- 1단계(이메일·비밀번호) 블록은 `if: "{{!_global.twoFactor?.required}}"`, 2단계(인증번호) 블록은
그 부정형을 답니다. 두 `if` 는 언제나 상보여야 합니다 — 한쪽만 고치면 두 단계가 겹쳐 보이거나
둘 다 사라집니다.
- 제출 시퀀스의 `login` 과 `loginTwoFactor` 도 같은 쌍으로 상호배타입니다. `loginTwoFactor` 쪽
`if` 가 빠지면 인증번호 단계에서 Enter 를 눌렀을 때 새 challenge 가 발급되어 흐름이 끊깁니다.
- 인증번호 입력은 controlled 입니다(`value` + `onChange` 의 `setState` 쌍). `events: {}` 래퍼는
쓰지 않습니다.
- 화면 진입 시 `auth/login.json` 의 `init_actions` 첫 액션이 `twoFactor` 를 `null` 로 되돌립니다.
전역 상태라 화면을 떠나도 남기 때문에, 이 리셋이 없으면 다시 들어왔을 때 1단계가 보이지 않습니다.
- 만료 시각은 정적 표기입니다(`| datetime`). 카운트다운을 쓰지 않습니다 — `startInterval` 은 등록
시점 컨텍스트를 고정하고 정지 호출처가 없어 화면을 떠난 뒤에도 남습니다.
**금지 — 같은 시퀀스·onSuccess 안에서 방금 저장한 상태를 다시 읽지 않습니다**
`setState target:global` 직후 형제 액션의 `if` 나 값으로 `_global.*` 를 재독하면 갱신 이전 값을
읽습니다(시퀀스 처리기는 `_local` 만 갱신합니다). 그 자리에서는 `{{response.*}}` 만 씁니다.
오류도 경고도 남지 않고 분기만 조용히 어긋나므로 화면을 보지 않으면 드러나지 않습니다.
---
#### 게시판 패턴
**대표**: `board/index.json`, `board/show.json`, `board/form.json`
**구성**:
```text
extends: _user_base
data_sources: [posts (게시글 목록/상세)]
slots.content:
└── Container
├── 타입 렌더러 (_type_renderer.json)
│ → 게시판 타입에 따라 분기:
│ ├── basic/index.json (기본 테이블형)
│ ├── card/index.json (카드형)
│ └── gallery/index.json (갤러리형)
├── Pagination
└── 글쓰기 버튼 (_write_button.json)
```
**data_sources**:
```json
{
"id": "posts",
"endpoint": "/api/modules/sirsoft-board/boards/{{route.slug}}/posts",
"method": "GET",
"params": {
"page": "{{query.page ?? 1}}",
"search": "{{query.search ?? ''}}",
"category": "{{query.category ?? ''}}"
},
"auto_fetch": true,
"refetchOnMount": true,
"errorHandling": {
"403": { "handler": "showErrorPage", "params": { "target": "content" } }
}
}
```
**타입별 렌더러 시스템**:
게시판은 `board_type` 필드에 따라 다른 레이아웃을 렌더링합니다:
- `basic` — 테이블형 게시글 목록 (기본)
- `card` — 카드 그리드형 목록
- `gallery` — 이미지 갤러리형 목록
```text
_type_renderer.json (조건 분기):
├── if: board_type === 'basic' → partial: types/basic/index.json
├── if: board_type === 'card' → partial: types/card/index.json
└── if: board_type === 'gallery' → partial: types/gallery/index.json
```
**게시글 상세 구성**:
```text
show.json:
└── _type_renderer.json → types/basic/show.json
├── 게시글 본문 (HtmlContent)
├── _post_attachments.json (첨부파일)
├── PostReactions (리액션)
├── _comment_section.json
│ ├── _comment_input.json (댓글 입력)
│ └── _comment_item.json (댓글 아이템, iteration)
│ └── _reply_section.json (답글)
├── _navigation.json (이전/다음 글)
└── modals: 삭제, 신고, 비밀번호 확인
```
**게시글 작성/수정 구성**:
```text
form.json:
├── _parent_post.json (답글 시 원글 표시)
└── _post_form.json
├── Input (제목)
├── Select (카테고리)
├── HtmlEditor (본문)
├── FileUploader (첨부파일)
├── Checkbox (공지, 비밀글 등)
└── Button (저장/취소)
```
**핸들러 패턴**:
- `apiCall` — CRUD, 댓글 작성/삭제, 리액션, 신고
- `navigate` — 게시판 이동, 글 상세/목록 이동
- `setState` — 검색, 필터, 페이지네이션, 댓글 접기/펼치기
- `replaceUrl` — 페이지네이션 시 URL만 변경
---
#### 쇼핑몰 패턴
**대표**: `shop/index.json`, `shop/show.json`, `shop/cart.json`, `shop/checkout.json`
##### 상품 목록 (shop/index.json)
```text
extends: _user_base
data_sources: [products (상품 API), categories (카테고리)]
slots.content:
└── Container
├── _category_breadcrumb.json (경로)
├── _search_filter_bar.json (검색/정렬)
├── _category_filter.json (카테고리 사이드)
├── _product_grid.json (상품 그리드, ProductCard iteration)
├── _popular_products.json (인기 상품)
├── _new_products.json (신상품)
├── _recent_products.json (최근 본 상품)
└── Pagination
```
##### 상품 상세 (shop/show.json)
```text
extends: _user_base
init_actions: [최근 본 상품 localStorage 저장]
data_sources: [product, reviews, popularProducts, relatedProducts]
computed: [displayPrice, displayListPrice, ...]
slots.content:
└── ThreeColumnLayout
├── left: _header.json (이미지 뷰어 — ProductImageViewer)
├── center: _info_summary.json (상품 정보, 옵션 선택)
└── right: _purchase_card.json (가격, 수량, 구매 버튼)
└── _price_mobile.json (모바일 가격 — responsive portable)
└── TabNavigation
├── _tab_detail.json (상세 정보 — HtmlContent)
├── _tab_reviews.json (리뷰 — iteration)
└── _tab_qna.json (Q&A — iteration)
└── modals: 장바구니 추가, 쿠폰 다운로드, 로그인 필요, 리뷰 이미지
```
**SEO 특수사항**:
- `meta.seo.structured_data`: Product, AggregateRating 스키마
- `meta.seo.og`: 상품 이미지, 가격 정보 포함
**핸들러 패턴**:
- `sirsoft-basic.addSelectedItemIfComplete` — 옵션 선택 완료 시 자동 추가
- `sirsoft-basic.updateSelectedItemQuantity` — 수량 변경
- `sirsoft-basic.getDisplayPrice` — 다중 통화 가격 표시
- `apiCall` — 장바구니 추가, 리뷰 작성, 위시리스트 토글
- `saveToLocalStorage` — 최근 본 상품 저장
##### 장바구니 (shop/cart.json)
```text
extends: _user_base
state: { selectedItems, allSelected, isCalculating, optionModal, deleteModal, ... }
data_sources: [cartItems (POST /cart/query)]
slots.content:
└── Flex
├── _cart_list.json (장바구니 목록)
│ └── _cart_item.json (아이템, iteration)
│ ├── Checkbox (선택)
│ ├── 상품 정보/가격
│ ├── QuantitySelector
│ └── 삭제/옵션변경 버튼
└── _cart_summary.json (주문 요약, 결제 버튼)
└── modals: 삭제 확인, 옵션 변경, 품절 안내
```
**특수사항**:
- `auth_mode: "optional"`: 비회원도 장바구니 사용 가능 (X-Cart-Key 헤더)
- `state` 섹션으로 모달/선택 상태 관리
- 장바구니 핸들러: `toggleCartItemSelection`, `selectAllCartItems`, `recalculateCart`
##### 결제 (shop/checkout.json)
```text
extends: _user_base
data_sources: [tempOrder (임시 주문)]
slots.content:
└── Container
├── _checkout_orderer.json (주문자 정보)
├── _checkout_shipping.json (배송 정보)
├── _checkout_items.json (주문 상품 목록)
├── _checkout_discount.json (할인/쿠폰)
├── _checkout_mileage.json (마일리지)
├── _checkout_payment.json (결제 수단 선택)
└── _checkout_summary.json (최종 요약 + 결제 버튼)
└── modals: 배송지 관리, 쿠폰 다운로드, 전용 쿠폰 확인
```
**핸들러 패턴**:
- `apiCall` — 주문 생성, 배송지 조회/저장, 쿠폰 적용
- `setState` — 배송지 선택, 결제 수단, 쿠폰/마일리지 계산
---
#### 마이페이지 패턴
**대표**: `mypage/profile.json`, `mypage/orders.json`
**구성**:
```text
extends: _user_base
computed: { currentTab: "profile" }
data_sources: [user (인증 사용자 정보)]
transition_overlay: { target: "mypage_tab_content" }
slots.content:
└── Div
├── _tab_navigation.json (공통 마이페이지 탭)
│ → 프로필, 주문내역, 게시글, 배송지, 알림, 위시리스트
└── 탭별 콘텐츠 영역
```
**탭 구조**:
각 마이페이지 하위 페이지는 동일한 `_tab_navigation.json`을 공유하되, `computed.currentTab`으로 활성 탭을 구분합니다.
| 탭 | 레이아웃 | Partial |
|---|---------|---------|
| 프로필 | `profile.json` | `_view.json` |
| 프로필 수정 | `profile-edit.json` | `_edit.json`, `_password_verify_section.json`, `_modal_withdraw.json` |
| 주문 내역 | `orders.json` | `_list.json` |
| 주문 상세 | `orders/show.json` | `_status_header.json`, `_items.json`, `_shipping.json`, `_payment.json`, `_history.json` |
| 내 게시글 | `board.json` | `_my_posts.json`, `_my_comments.json` |
| 배송지 | `addresses.json` | `_list.json`, `_modal_address.json`, `_modal_confirm_delete.json` |
| 알림 | `notifications.json` | `_list.json` |
| 비밀번호 변경 | `change-password.json` | (인라인) |
| 위시리스트 | `wishlist.json` | `_list.json` |
| 상품 1:1 문의내역 | `inquiries.json` | `_list.json` (sirsoft-ecommerce 모듈 연동 시에만 탭 표시) |
**핸들러 패턴**:
- `apiCall` — 프로필 수정, 주문 취소, 배송지 CRUD, 알림 읽음 처리
- `navigate` — 탭 간 이동 (별도 페이지)
- `setState` — 폼 상태, 모달 제어
- `openModal`/`closeModal` — 탈퇴, 주소 편집, 삭제 확인
---
#### 검색 패턴
**대표**: `search/index.json`
**구성**:
```text
extends: _user_base
state: { q: "" }
init_actions: [setState — query 파라미터에서 검색어 추출]
global_state: { searchActiveTab, searchBoardFilter, searchSortBy, searchPage }
data_sources: [searchPosts, searchProducts, searchPages]
slots.content:
└── Container
├── _search_input.json (검색 입력 — SearchBar)
├── _search_tabs.json (탭: 전체/게시글/상품/페이지)
├── _search_filters.json (정렬, 게시판 필터)
├── _search_states.json (검색 전/로딩/결과없음 상태)
└── _search_results.json (결과 표시)
├── posts/_section.json → posts/_list.json
├── products/_section.json → products/_item_card.json / _item_list.json
└── pages/_section.json
```
**특수사항**:
- 통합 검색: 게시글 + 상품 + 페이지 동시 검색
- `searchActiveTab`으로 탭 전환 시 해당 카테고리만 표시
- 상품 결과는 카드/리스트 뷰 전환 지원
- `global_state`로 검색 상태 유지 (탭, 필터, 정렬, 페이지)
**핸들러 패턴**:
- `apiCall` — 검색 API (게시글, 상품, 페이지 별도)
- `setState` — 탭 전환, 필터, 정렬
- `replaceUrl` — 검색어/필터 변경 시 URL 갱신
---
#### 기타 페이지 패턴
##### 사용자 프로필 (users/show.json, users/posts.json)
```text
extends: _user_base
data_sources: [user (공개 프로필)]
slots.content:
└── Container
├── Avatar + 사용자 정보
└── 게시글 목록 (users/posts.json)
```
##### 정적 페이지 (page/show.json)
```text
extends: _user_base
data_sources: [page (페이지 콘텐츠)]
slots.content:
└── Container
└── HtmlContent (본문 렌더링)
```
##### 홈 (home.json)
```text
extends: _user_base
data_sources: [stats, recent_posts, popular_boards]
meta.seo: { structured_data: WebSite }
slots.content:
└── Container
├── _welcome_card.json (환영 배너)
├── Grid (통계 카드 4개)
│ ├── _stat_card_users.json
│ ├── _stat_card_posts.json
│ ├── _stat_card_comments.json
│ └── _stat_card_boards.json
├── _recent_posts.json (최근 게시글)
├── _popular_boards.json (인기 게시판)
├── _board_summary.json (게시판 요약)
├── _community_guide.json (가이드)
└── _shop_promo.json (쇼핑 프로모션)
```
---
#### 에러 페이지 패턴
**대표**: `errors/404.json`
**구성**:
```text
(extends 없음 — 독립 레이아웃)
components:
└── Div (전체 화면 중앙 정렬)
├── Icon (에러 아이콘)
├── H1 (에러 코드)
├── P (에러 메시지)
└── Button (홈으로 이동)
```
**핸들러 패턴**:
- `navigate` — 홈으로 이동
---
### 베이스 레이아웃 구조
#### _user_base.json
모든 사용자 페이지의 공통 구조를 정의합니다.
```text
_user_base.json
├── globalHeaders: [X-Cart-Key (이커머스·인증 API) / X-Guest-Order-Token (비회원 주문 후속 액션) / X-Board-Secret-View-Token (게시판 API)]
├── transition_overlay: { style: "skeleton", target: "main_content_area" }
├── init_actions: [initTheme, initCartKey, loadPreferredCurrency, setState(shopBase)]
├── data_sources:
│ ├── boards (게시판 메뉴 — progressive, fallback)
│ ├── current_user (인증 사용자 — auth_required, suppress 401)
│ └── cart (장바구니 카운트 — optional auth)
├── components:
│ ├── Toast (전역 알림)
│ ├── PageTransitionIndicator
│ ├── Div (min-h-screen flex flex-col)
│ │ ├── mobile_overlay (모바일 메뉴 오버레이 — responsive portable)
│ │ ├── MobileNav (모바일 네비게이션 드로어)
│ │ ├── Header (사이트 헤더 — 로고, 메뉴, 검색, 사용자)
│ │ ├── Div#main_content_area (콘텐츠 영역)
│ │ │ └── slot: "content" (← 하위 레이아웃이 채움)
│ │ └── Footer (사이트 푸터)
│ └── PageTransitionBlur (전환 블러)
```
**슬롯**:
- `content` — 각 페이지의 메인 콘텐츠가 삽입되는 위치
**특수사항**:
- `globalHeaders`: 패턴별 공통 헤더 자동 첨부
- `/api/modules/sirsoft-ecommerce/*` — `X-Cart-Key`, `X-Currency`, `X-Shipping-Country`
- `/api/modules/sirsoft-ecommerce/guest/orders/*` — `X-Guest-Order-Token` (비회원 주문 후속 액션)
- `/api/modules/sirsoft-board/*` — `X-Board-Secret-View-Token`
비밀번호를 입력해 비밀글 원문을 연 사실을 다음 요청으로 넘기는 값입니다. 서버는 이
사실을 검증 응답 하나에만 담고 있어서, 토큰을 싣지 않으면 원문을 연 사용자도 댓글·답글·
신고에서 전부 거부됩니다. 토큰은 게시글에 결속되므로 다른 글에는 통하지 않고, 비밀글이
아닌 요청에서는 서버가 아예 평가하지 않습니다. `board/types/basic/show.json` 의
비밀번호 검증 `onSuccess` 가 `_global.secretViewToken` 에 넣습니다.
- `transition_overlay.style: "skeleton"`: 페이지 전환 시 PageSkeleton 표시
- `responsive`: 모바일 오버레이/네비게이션은 `portable` breakpoint에서만 표시
- `auth_mode: "optional"`: 비회원도 장바구니 카운트 조회 가능
- `errorHandling.401.handler: "suppress"`: 비회원의 인증 API 401은 에러 전파 방지
---
### 관련 문서
- [sirsoft-basic 컴포넌트](components.md)
- [sirsoft-basic 핸들러](handlers.md)
- [sirsoft-admin_basic 레이아웃](../../sirsoft-admin_basic/docs/layouts.md)
- [레이아웃 JSON 스키마](../../../../docs/frontend/layout-json.md)
- [레이아웃 상속](../../../../docs/frontend/layout-json-inheritance.md)