공개 는 헤더 「로고 이미지」에 이미지를 지정하면 화면이 엑박이 되는 제보였다.
image 위젯은 배경용으로 설계되어 {url,size,repeat,position} 객체를 내보내는데, 값 슬롯이
하나뿐인 apply 경로(propValue/cssVar/단일 styleProp)가 그 객체를 그대로 props 에 기록해
소비 컴포넌트가 [object Object] 를 URL 로 받았다. 예외도 콘솔 오류도 서버 로그도 남지
않는다 — 깨진 이미지 요청은 SPA catch-all 때문에 404 조차 아니라 200(HTML)이고, 편집기
미리보기는 정상이라 조작 중에는 이상이 보이지 않는다.
방어선을 넷으로 세웠다. 쓰기 축약(공용 헬퍼 scalarizeImageValue 단일 지점, 게이트는 위젯
이름이며 값 형태 sniffing 이 아니다) · 읽기 역조립(표현식 문자열도 되감아 업로드 1클릭에
소실되지 않게 한다) · 런타임 방어(업그레이드 전 화면을 위해 템플릿 Img 가 url 을 해석하고
손상값이면 src 를 아예 붙이지 않는다) · 저장 데이터 백필(업그레이드 스텝). 런타임과 백필은
완전히 같은 엄격 판정식(키 집합 ⊆ 4키 AND url 보유)을 쓴다 — 엔진의 느슨한 판정식을 백필에
이식하면 레이아웃 전수에서 정상 props 2,219건을 파괴한다(실측).
전수조사에서 파생한 인접 결함 넷을 함께 고쳤다.
- number 위젯이 코어 레지스트리에 미등록이라 「탭 표시 게시판 수」 같은 컨트롤이
「지원하지 않는 컨트롤」로 폴백해 편집 자체가 불가했다. nodeKey apply 는 coreProps 가
선언만 하고 엔진 switch 에 case 가 없어 무음 no-op 이었다.
- 상속(base)·주입(extension) 노드 중 바인딩을 가진 것이 data_bound 로 분류돼 편집이
열려 있었는데, 저장 마스킹이 그 노드를 통째로 폐기하므로 편집분이 오류도 경고도 없이
사라졌다(저장은 200 이고 history 는 clear 돼 undo 도 불가). 출처 잠금이 항상 우선하도록
판정 순서를 통일하고, 단일 판정 헬퍼로 인라인 편집·복제·Delete·잘라내기·드래그 commit
까지 전 표면을 같은 기준으로 막았다.
- prop 자리의 표현식 값을 위젯이 해석하지 못해 빈 컨트롤로 보이고, 조작하는 순간 환경설정
과의 연결이 소리 없이 끊겼다. 판정·배지·잠금·해제·복구를 ControlRenderer 공용 게이트
한 곳으로 올려 신규 위젯에도 자동 적용되게 했다. 「직접 지정으로 바꾸기」에는 「되돌리기」를
동반해 편도가 되지 않게 한다.
- 편집기 모드에서 updateTemplateData 가 빈 레이아웃으로 같은 reactRoot 에 두 번째 커밋을
걸어 편집기 트리를 통째로 제거했다. renderTemplate 의 편집기 분기가 비동기라 부팅 중
setGlobalState 가 그 커밋 뒤에 도착할 때만 발현하는 경합이었다.
15 KiB
그누보드7 Basic 템플릿 — 에이전트 가이드
이 문서는 이 템플릿을 수정하는 에이전트·확장개발자를 위한 것입니다. 도입 검토·운영 관점은 README.md 를 보세요.
TL;DR (5초 요약)
1. 유형: 템플릿 (sirsoft-basic, type=user) — 방문자가 보는 사이트 전체 화면 166개. 게시판·이커머스 모듈은 방문자 화면을 갖지 않으므로 상점·게시판 UI 는 여기가 소유한다. 서버 코드 0줄
2. 확장 방식: 훅 0/0 — 확장점은 화면 구조다. 다른 확장이 `layout_extensions` 로 조각을 끼우고, `extensions/{확장}/` 오버라이드로 원본 조각을 대체한다
3. 건드리면 안 되는 것: `extends` 없는 독립 레이아웃에서 toast/modal 사용, 상점 경로 하드코딩(표현식 유지), 목록 이동의 `mergeQuery` 누락, 비회원 주문 토큰 진입 정리 제거
4. 작업 위치: `templates/_bundled/sirsoft-basic` — 활성 디렉토리 직접 수정 금지
5. 반영: `php artisan template:update sirsoft-basic --force`
1. 이 확장은 무엇인가
방문자가 보는 사이트 전체 화면을 소유하는 사용자(User) 템플릿입니다. 홈·게시판·상점· 마이페이지·인증·오류 화면 166개가 여기 있습니다.
이것이 왜 템플릿에 있는가가 이 확장을 이해하는 열쇠입니다. 게시판 모듈도 이커머스 모듈도
레이아웃이 전부 admin 그룹이고 방문자 화면을 갖지 않습니다 — 두 모듈은 관리자 CRUD 와 공개
API 까지만 소유하고, 그 API 를 소비해 실제로 그리는 것은 이 템플릿입니다. 상점 디자인을 바꾸는
작업은 이커머스 모듈이 아니라 여기입니다.
설계 원칙 넷:
- 모든 화면이
_user_base를 상속한다. 헤더·푸터·모바일 네비와 토스트·모달 호스트가 거기 있습니다.extends없이 독립 레이아웃을 만들면toast·openModal이 성공으로 기록되지만 화면에는 아무것도 나타나지 않습니다. - 화면은 조각으로 나눈다. 166개 중 124개가 partial 입니다. 조각은 여러 화면이 공유하며, 화면 하나를 고칠 때는 그 화면 이름의 partials 디렉토리를 함께 엽니다.
- 모듈 설정이 라우트에 스며든다. 상점 경로 9개가 표현식이라 운영자가 이커머스 설정에서 상점 경로를 바꾸거나 루트에 두면 라우트가 그에 맞춰 바뀝니다.
- 다른 확장의 조각을 갈아 끼울 수 있다.
extensions/{확장}/오버라이드가 그 장치이며, 지금은 주소 검색 조각 하나가 있습니다.
의도적으로 하지 않는 것: 서버 코드 일체(PHP 0줄 · 모델 0 · 라우트 파일 없음 · 훅 발행 0). 데이터는 전부 모듈·코어의 공개 API 에서 옵니다. 그래서 이 템플릿을 다른 것으로 바꿔도 데이터는 그대로이고, 반대로 이 템플릿만으로는 아무 기능도 동작하지 않습니다 — manifest 가 게시판· 이커머스·페이지 모듈과 주소 검색 플러그인을 의존으로 선언하는 이유입니다.
2. 디렉토리 지도
| 경로 | 역할 | 수정 시 필요한 절차 |
|---|---|---|
template.json |
manifest (버전 SSoT) | version 변경 시 package.json·package-lock.json 동기화 |
routes.json |
라우트 → 레이아웃 매핑 | php artisan template:update sirsoft-basic --force |
layouts/ |
레이아웃 JSON | php artisan template:update sirsoft-basic --force (빌드 불필요) |
extensions/ |
다른 확장 화면에 주입하는 레이아웃 조각 | php artisan template:update sirsoft-basic --force (빌드 불필요) |
seo-config.json |
SEO 렌더 설정 | php artisan template:update sirsoft-basic --force |
src/components/ |
React 컴포넌트 | php artisan template:build → php artisan template:update sirsoft-basic --force |
src/handlers/ |
템플릿 전용 액션 핸들러 | php artisan template:build → php artisan template:update sirsoft-basic --force |
dist/ |
커밋되는 빌드 산출물 | --production 으로 재빌드 (sourceMappingURL 잔존 금지) |
editor-spec.json |
레이아웃 편집기 스펙 | php artisan template:update sirsoft-basic --force |
editor-spec/ |
분할 편집기 스펙 | php artisan template:update sirsoft-basic --force |
tests/ |
테스트 | 변경 범위만 필터 실행 |
CHANGELOG.md |
변경 이력 | 버전 상향 시 항목 추가 (미기재 시 버전 상향 불가) |
components.json |
편집기 컴포넌트 선언 (레이아웃 저작자가 읽는 props 계약) | php artisan template:update sirsoft-basic --force |
docs/ |
개발자 문서 | 표면 변경 시 php artisan ext:docgen 재실행 |
lang/ |
다국어 | 키 추가 시 ko·en 동시 반영 + 번들 ja 팩 동기화 |
3. 핵심 흐름
서버 코드가 없으므로 흐름은 전부 라우트 → 레이아웃 → 데이터소스 → 컴포넌트입니다.
화면 렌더: 브라우저가 경로 진입 → routes.json 이 그 경로에 대응하는 레이아웃을 지목 →
레이아웃이 _user_base 를 상속해 헤더·푸터를 얻고 콘텐츠 슬롯을 채움 → data_sources 가
모듈·코어의 공개 API 를 호출 → 응답을 컴포넌트에 바인딩. 상점 경로는 이 첫 단계에서
표현식이 평가되어 운영자가 지정한 경로가 됩니다.
장바구니(비회원 포함): 진입 시 initCartKey 가 localStorage 의 장바구니 키를 확인하고
없으면 API 로 발급받습니다 → 담기·수량 변경·옵션 변경은 setCartOption ·
recalculateCart 등이 이커머스 API 를 호출하고 결과로 상태를 갱신 → 로그인하면 코어
auth.after_login 을 구독하는 이커머스 리스너가 비회원 장바구니를 회원 장바구니로 병합합니다
(그 병합은 서버 쪽 일이며 이 템플릿은 키만 넘깁니다).
비회원 주문 조회: 주문 완료 시 서버가 발급한 토큰을 saveGuestOrderToken 이 보관 →
조회 화면이 그 토큰으로 API 를 호출하면 서버의 VerifyGuestOrderToken 미들웨어가 신원을
확인합니다. 토큰이 곧 신원이므로 clearGuestTokenOnEntry 가 진입 시점에 남은 토큰을
정리합니다 — 공용 PC 에서 다음 사용자가 남의 주문을 열지 못하게 하는 방어입니다.
본인인증(IDV): 어떤 API 가 428 을 돌려주면 코어 인터셉터가 그것을 잡아, 부트스트랩에서 등록한 launcher 로 인증 화면을 엽니다. 인증을 마치면 원래 액션이 재개됩니다 — 각 화면이 428 을 개별 처리하지 않습니다.
4. 확장점
| 확장점 | 수 | 상세 |
|---|---|---|
| 제공 컴포넌트 | 79개 | 제공 컴포넌트 |
| 레이아웃 | 166개 | 레이아웃 목록 |
| 전용 핸들러 | 32개 | 템플릿 전용 핸들러 |
| 확장 오버라이드 | 1개 | 확장 오버라이드 |
이 템플릿은 훅을 발행하지도 구독하지도 않습니다(0/0). 템플릿의 확장점은 훅이 아니라 화면 구조 자체입니다.
| 확장점 | 어떻게 쓰는가 |
|---|---|
| 레이아웃 166개 | 다른 확장이 layout_extensions 로 자기 조각을 끼워 넣습니다 — 이커머스의 헤더 통화 선택기·마이페이지 마일리지 카드, 마케팅의 회원가입 동의 항목이 그 예입니다 |
| 제공 컴포넌트 79개 | 조각을 만드는 확장이 이 컴포넌트로 화면을 구성합니다. 여기 없는 컴포넌트를 쓰면 그 조각은 이 템플릿에서 렌더되지 않습니다 |
| 전용 핸들러 32개 | 조각의 액션에서 부를 수 있습니다. 네임스페이스가 붙은 10개(sirsoft-basic.*)는 전체 이름으로, 나머지는 이름만으로 부릅니다 |
| 확장 오버라이드 | extensions/{확장}/ 에 같은 이름의 조각을 두면 그 확장이 제공한 원본을 대체합니다 |
다른 확장이 이 템플릿에 화면을 얹는 방향이 정상입니다. 반대로 이 템플릿이 모듈의 관리자 화면에 개입하지는 않습니다.
seo-config.json 도 확장점입니다 — 봇 요청에 서버가 화면을 렌더할 때 어떤 속성을 HTML 로
내보낼지 이 파일이 정합니다. 새 컴포넌트가 텍스트를 담는 새 prop 을 쓰면 text_props 에
추가해야 봇 화면에 그 글자가 나타납니다.
5. 수정 시 동반 의무
_bundled에서만 수정하고php artisan template:update sirsoft-basic --force로 반영- manifest version 상향 시
package.json·package-lock.json동기화 + CHANGELOG 기재 - 레이아웃 JSON 변경 시 빌드 없이 update 만 — 신규 Tailwind 클래스는 빌드된 CSS 에 존재하는지 확인
- TSX/TS 변경 시
--production재빌드 후dist/커밋 (sourceMappingURL 잔존 금지) - 다국어 키 추가 시 ko·en 동시 반영 + 번들 ja 언어팩 증분 동기화
- 새 화면을 추가했다면
_user_base를 상속하는지,routes.json에 경로가 등록됐는지 확인 - 상점 관련 경로·링크는 표현식(
route_path·no_route)을 유지 — 문자열로 굳히지 않는다 - 목록 클러스터(목록 → 상세 → 형제 상세 → 폼 → 복귀) 이동 전 leg 에
mergeQuery: true - 텍스트를 담는 새 컴포넌트 prop 을 도입했다면
seo-config.json의text_props에 추가 (봇 화면에서만 글자가 사라진다) - 의존 모듈(게시판·이커머스·페이지)의 공개 API 응답 형태가 바뀌면 이 템플릿의 화면이 조용히 빈다 — 그 모듈을 올릴 때 함께 확인
extensions/sirsoft-daum_postcode/오버라이드는 원본이 바뀌어도 따라가지 않는다 — 그 플러그인 업그레이드 후 확인- TSX/TS 를 고쳤다면
template:build --production후dist/동반 커밋 (sourceMappingURL잔존 금지) - 프론트엔드 변경은 Playwright spec 동반 — 단위 테스트만으로는 화면 회귀가 드러나지 않는다
- 레이아웃·컴포넌트·
data_source를 건드렸다면docs/editor-spec.md의 동반 의무 표를 따라editor-spec/블록을 함께 갱신 — 컴포넌트는 팔레트·역량·중첩 넷 다 손대야 편집기에서 온전히 동작하고, 하나만 빠지면 절반만 동작한다. 반영은php artisan template:update sirsoft-basic --force(편집기는 활성 디렉토리만 읽는다) - 로그인 화면의 2단계 인증 단계를 고쳤다면 1단계·2단계
if의 상보성과login/loginTwoFactor의 상호배타if를 함께 확인 — 한쪽이 빠지면 인증번호 단계에서 Enter 가 새 challenge 를 발급한다
6. 금지 패턴
| 금지 | 올바른 사용 | 이유 |
|---|---|---|
extends 없는 독립 레이아웃에서 toast·openModal 사용 |
_user_base 를 상속하거나, 독립 레이아웃이라면 Toast·모달 호스트 컴포넌트를 직접 마운트 |
호스트가 없으면 핸들러는 성공으로 기록되는데 화면에는 아무것도 나타나지 않는다 |
상점 경로를 /shop/... 로 하드코딩 |
routes.json 의 표현식 유지 (route_path · no_route 반영) |
운영자가 경로를 바꾸거나 루트에 두면 하드코딩한 링크만 조용히 깨진다 |
목록 → 상세 → 목록 이동에서 mergeQuery 누락 |
목록 클러스터 내 모든 이동에 "mergeQuery": true |
검색어·페이지·필터가 사라져 사용자가 처음부터 다시 찾아야 한다 |
| 비회원 주문 토큰을 진입 시점에 정리하지 않음 | clearGuestTokenOnEntry 유지 |
토큰이 곧 신원이다 — 공용 PC 에서 다음 사용자가 남의 주문을 연다 |
401 오류 레이아웃에서 로그인으로 직접 리다이렉트 |
코어 TemplateApp.showRouteError 가드에 위임 |
이중 리다이렉트가 되고, 돌아올 위치를 코어가 이미 관리한다 |
모듈·플러그인에서 AuthManager.updateConfig() 호출 |
템플릿 부트스트랩에서만 | 여러 확장이 로그인 경로를 다투면 어느 값이 이기는지 설치 순서에 좌우된다 |
새 컴포넌트가 텍스트를 담는 prop 을 추가하면서 seo-config.json 을 그대로 두기 |
text_props 에 그 prop 추가 |
봇 화면에서만 그 글자가 사라진다 — 사람 눈에는 정상이라 검색 노출이 줄어든 뒤에야 드러난다 |
| 레이아웃 JSON 에 빌드된 CSS 에 없는 Tailwind 클래스 사용 | 기존 레이아웃에 쓰인 클래스이거나 빌드 산출물에 존재하는지 확인 | 그 스타일만 조용히 빠져 화면이 어긋난다 |
dist/ 재빌드 없이 src/ 만 고치고 커밋 |
template:build --production 후 dist/ 동반 커밋 |
브라우저가 받는 것은 커밋된 dist/ 다 — 소스 수정이 사문화된다 |
onSuccess·시퀀스 안에서 방금 저장한 상태(_global.*/_local.*)를 형제 액션의 if·값으로 재독 |
그 자리에서는 {{response.*}} 만 읽는다 |
그 시점 컨텍스트는 아직 갱신 전이라 stale 값으로 조용히 분기한다 |
7. 테스트 실행
| 종류 | 개수 | 위치 |
|---|---|---|
| PHPUnit | 0개 | — |
| Vitest | 148개 | vitest.config.ts |
| Playwright | 8개 | tests/Playwright |
| 시나리오 매니페스트 | 4개 | tests/scenarios |
# Vitest (확장 디렉토리에서) (PowerShell)
cd templates/_bundled/sirsoft-basic && powershell -Command "npm run test:run -- <대상>"
# Playwright E2E (확장 디렉토리에서) (Bash)
cd templates/_bundled/sirsoft-basic && npm run test:e2e -- specs/<대상>.spec.ts
무필터 전체 실행은 금지되어 있습니다 — 변경 범위에 걸리는 대상만 지정해 실행합니다.
8. 문서 목차
| 문서 | 내용 | 상태 |
|---|---|---|
| docs/README.md | 문서 통합 목차와 실측 집계 | ✅ |
| docs/architecture.md | 설계 의도·계층 지도·디렉토리 맵 | ✅ |
| docs/components.md | 템플릿이 제공하는 컴포넌트 | ✅ |
| docs/layouts.md | 레이아웃 목록과 라우트 매핑 | ✅ |
| docs/handlers.md | 템플릿 전용 핸들러와 부트스트랩 | ✅ |
| docs/editor-spec.md | 레이아웃 편집기에 선언한 팔레트·컨트롤·샘플 데이터 | ✅ |
| CHANGELOG.md | 변경 이력 | ✅ |