Files
HeuJung dd8f9a49c4 fix(core,template): 편집기 image 위젯 값 형태·데이터 연결 값 보호·상속 노드 편집 표면 수정
공개 는 헤더 「로고 이미지」에 이미지를 지정하면 화면이 엑박이 되는 제보였다.
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 가 그 커밋 뒤에 도착할 때만 발현하는 경합이었다.
2026-09-08 17:20:01 +09:00

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 를 소비해 실제로 그리는 것은 이 템플릿입니다. 상점 디자인을 바꾸는 작업은 이커머스 모듈이 아니라 여기입니다.

설계 원칙 넷:

  1. 모든 화면이 _user_base 를 상속한다. 헤더·푸터·모바일 네비와 토스트·모달 호스트가 거기 있습니다. extends 없이 독립 레이아웃을 만들면 toast·openModal 이 성공으로 기록되지만 화면에는 아무것도 나타나지 않습니다.
  2. 화면은 조각으로 나눈다. 166개 중 124개가 partial 입니다. 조각은 여러 화면이 공유하며, 화면 하나를 고칠 때는 그 화면 이름의 partials 디렉토리를 함께 엽니다.
  3. 모듈 설정이 라우트에 스며든다. 상점 경로 9개가 표현식이라 운영자가 이커머스 설정에서 상점 경로를 바꾸거나 루트에 두면 라우트가 그에 맞춰 바뀝니다.
  4. 다른 확장의 조각을 갈아 끼울 수 있다. 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 변경 이력 ✅