S1 파일럿 3종(sirsoft-board · sirsoft-gdpr · sirsoft-admin_basic)에 이어
결제·본인인증 동형군 6종의 AGENTS.md · README.md · docs/ 5문서를 집필했다.
확장을 고치려는 쪽이 매번 src/ 를 훑어 구조를 재발견하지 않도록, 설계 의도와
확장점(발행·구독 훅)·수정 시 동반 의무·금지 패턴을 코드 근거로 서술했다.
생성기(ExtensionDocScaffolder)에서 표를 무의미하게 만들던 세 결함을 함께 고쳤다.
getLayoutExtensions 기본 구현이 돌려주는 절대경로가 파일 목록과 중복돼 로컬
머신 경로가 커밋 문서에 실리던 문제, getNotificationDefinitions 를 'key'/'event'
로 읽어 모든 행이 '-' 로 찍히던 문제, getSettingsLayout 절대경로가 정규화 없이
노출되던 문제다. 셋 다 예외를 남기지 않고 표만 조용히 망가뜨리므로
ExtensionDocContractTest 에 각각의 되돌림 red 를 확인한 단언을 두었다.
템플릿의 extensions/{id}/ 는 모듈·플러그인의 발행과 반대 방향(오버라이드)이라
별도 블록(template-overrides)으로 분리했다.
sirsoft-admin_basic 의 컴포넌트·핸들러·레이아웃 문서를 코어 docs/ 에서 그 템플릿
소유로 이관하고, 남은 참조 6축을 재는 가드를 추가했다. 이관 후 남은 옛 경로는
오류가 아니라 헛걸음으로만 나타나 드러나지 않는다. 의 상대 링크가
한 단계 얕아 공유 docs/ 대신 를 가리키던 문제도 함께 고쳤다.
README 상단의 확장명 이미지 배지를 평문 H1 로 바꿨다( 지시 2026-08-31).
루트 README.md · README.ko.md 도 같은 기준을 적용했다. 정보 배지는 유지한다.
sirsoft-gdpr: 회원탈퇴로 자동 철회된 동의가 관리자 동의 이력 화면의 출처 필터로
걸러지지 않던 문제를 고쳤다. Repository 가 'withdraw' 리터럴을 직접 UPDATE 에
싣는데 그 값이 ConsentSource enum 에 없어, 화면 필터 옵션·라벨 어느 쪽에도
도달하지 못했다. 어휘를 enum 단일 출처로 모으고 ko·en·ja 라벨과 필터 옵션을
함께 채웠으며, 어휘 대조 테스트의 모집단에 Repository 를 편입했다.
13 KiB
반응형 레이아웃 개발 (engine-v1.1.0+)
그누보드7 템플릿 시스템의 반응형 UI 구현 가이드
TL;DR (5초 요약)
1. responsive 속성: 컴포넌트 레벨 breakpoint 오버라이드 (권장)
2. Tailwind breakpoint: sm/md/lg/xl/2xl 클래스
3. 전역 상태: _global.isMobile 등으로 조건부 렌더링
4. z-index 계층: modal(50) > dropdown(40) > sidebar(30)
5. 모바일 우선: 기본 스타일 후 md:, lg: 추가
템플릿별 지원 현황
| 템플릿 식별자 | features.responsive | 설명 |
|---|---|---|
sirsoft-admin_basic |
미선언 | 데스크톱 중심, Tailwind responsive 클래스는 동작 |
sirsoft-basic |
true |
MobileNav 포함, portable preset 지원 |
상세 컴포넌트 목록: sirsoft-admin_basic, sirsoft-basic
목차
- 개요
- responsive 속성 (권장)
- Tailwind Breakpoint 활용
- 전역 상태 기반 동적 스타일
- 조건부 렌더링
- 모범 사례
- 완전한 반응형 레이아웃 예시
- 임의의 breakpoint 사용
- z-index 계층 관리
개요
그누보드7 템플릿 시스템은 responsive 속성, Tailwind CSS breakpoint, 전역 상태를 활용하여 순수 레이아웃 JSON으로 반응형 UI를 구현합니다.
반응형 구현 방법 비교
| 방법 | 사용 시점 | 특징 |
|---|---|---|
responsive 속성 |
props, children, text 변경 시 | 컴포넌트 레벨 오버라이드, 권장 |
| Tailwind breakpoint | className만 변경 시 | CSS 레벨, 간단한 스타일 변경 |
| 전역 상태 | 사용자 인터랙션 기반 | 토글 버튼 등 상태 제어 |
1. responsive 속성 (권장)
responsive 속성을 사용하면 화면 크기에 따라 컴포넌트의 props, children, text, if, iteration을 오버라이드할 수 있습니다.
Breakpoint 프리셋
| 프리셋 | 화면 너비 | 설명 |
|---|---|---|
mobile |
0 ~ 767px | 모바일 전용 |
tablet |
768 ~ 1023px | 태블릿 전용 |
desktop |
1024px 이상 | 데스크톱 전용 |
portable |
0 ~ 1023px | 모바일 + 태블릿 (비데스크톱) |
확장 코드에서 같은 경계를 판정할 때
모듈·플러그인이 화면 폭에 따라 다른 동작을 고르는 경우(팝업 대신 페이지 전환 등), 그 판정은
레이아웃이 responsive 로 띄우는 안내와 같은 값을 같은 방법으로 읽어야 한다.
// ✅ 엔진과 동일 — ResponsiveManager 가 window.innerWidth 로 breakpoint 를 정한다
const PORTABLE_MAX_WIDTH = 1023;
const isPortable = window.innerWidth <= PORTABLE_MAX_WIDTH;
// ❌ 경계에서 어긋난다
const isPortable = window.matchMedia('(max-width: 1023px)').matches;
matchMedia 는 CSS 픽셀 기준이라 devicePixelRatio 가 정수가 아닌 환경에서 window.innerWidth
와 1px 어긋난다. 실측(Chrome, dPR 1.0000000447)에서 innerWidth === 1023 인데
matchMedia('(max-width: 1023px)') 가 false 였다. 이 어긋남은 딱 경계 폭에서만 나타나므로
개발 중에는 드러나지 않고, 화면은 "페이지로 이동합니다" 라고 안내해 놓고 실제로는 팝업이 열리는
형태로 사용자에게만 보인다.
경계값 자체도 상수로 두고 SSoT(ResponsiveManager 의 portable 정의)를 주석에 밝힌다 —
프리셋 범위가 바뀌면 확장도 함께 고쳐야 한다는 신호가 코드에 남는다.
portable 프리셋 사용 규칙
portable 프리셋은 mobile과 tablet을 합친 범위입니다. 다음 규칙을 준수하세요:
✅ 사용: portable만 단독 사용 (mobile+tablet 동일 처리)
❌ 금지: portable과 mobile/tablet 혼용
올바른 사용 예시:
{
"responsive": {
"portable": {
"props": { "className": "flex-col" }
}
}
}
잘못된 사용 예시:
{
"responsive": {
"portable": { "props": { "className": "hidden" } },
"tablet": { "props": { "className": "flex" } }
}
}
mobile과 tablet을 구분해야 하는 경우:
{
"responsive": {
"mobile": { "props": { "className": "grid-cols-1" } },
"tablet": { "props": { "className": "grid-cols-2" } }
}
}
커스텀 범위
| 형식 | 설명 | 예시 |
|---|---|---|
min-max |
범위 지정 | 0-599, 600-899 |
min- |
최소값 이상 | 1200- (1200px 이상) |
-max |
최대값 이하 | -599 (599px 이하) |
기본 사용법
{
"id": "responsive_button",
"type": "basic",
"name": "Button",
"props": {
"className": "px-4 py-2",
"variant": "primary",
"size": "large"
},
"text": "데스크톱 버튼",
"responsive": {
"mobile": {
"props": {
"className": "px-2 py-1",
"size": "small"
},
"text": "모바일 버튼"
},
"tablet": {
"props": {
"size": "medium"
},
"text": "태블릿 버튼"
}
}
}
Props 머지 규칙
얕은 머지 (Shallow Merge): 지정한 props만 대체, 미지정 props는 유지
{
"props": {
"className": "base-class",
"variant": "primary",
"size": "large"
},
"responsive": {
"mobile": {
"props": {
"className": "mobile-class"
}
}
}
}
모바일에서 적용되는 props:
className:"mobile-class"(대체됨)variant:"primary"(유지)size:"large"(유지)
children 완전 교체
{
"id": "card",
"type": "composite",
"name": "Card",
"children": [
{ "id": "desktop-content", "type": "basic", "name": "Div", "text": "데스크톱 전용" },
{ "id": "desktop-details", "type": "basic", "name": "Div", "text": "상세 정보" }
],
"responsive": {
"mobile": {
"children": [
{ "id": "mobile-content", "type": "basic", "name": "Div", "text": "모바일 요약" }
]
}
}
}
if 조건 오버라이드
{
"id": "sidebar",
"type": "basic",
"name": "Div",
"if": "{{true}}",
"responsive": {
"mobile": {
"if": "{{_global.sidebarOpen}}"
}
}
}
커스텀 범위 사용
{
"id": "grid",
"type": "basic",
"name": "Div",
"props": {
"className": "grid grid-cols-4"
},
"responsive": {
"0-599": {
"props": { "className": "grid grid-cols-1" }
},
"600-899": {
"props": { "className": "grid grid-cols-2" }
},
"900-1199": {
"props": { "className": "grid grid-cols-3" }
},
"1200-": {
"props": { "className": "grid grid-cols-4" }
}
}
}
우선순위
- 커스텀 범위 > 프리셋: 커스텀 범위가 프리셋보다 우선 적용
- 좁은 범위 > 넓은 범위: 여러 범위에 매칭되면 좁은 범위가 우선
{
"responsive": {
"mobile": { "text": "모바일 프리셋" },
"0-480": { "text": "작은 모바일" }
}
}
400px에서: "작은 모바일" (커스텀 범위 우선)
600px에서: "모바일 프리셋" (프리셋만 매칭)
2. Tailwind Breakpoint 활용
기본 Breakpoint
| Breakpoint | 최소 너비 |
|---|---|
sm |
640px |
md |
768px |
lg |
1024px |
xl |
1280px |
2xl |
1536px |
사용 예시
{
"id": "mobile_header",
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center h-16 border-b px-4 md:hidden"
}
}
해석: 기본(모바일)에서는 flex items-center h-16 border-b px-4, md(768px) 이상에서는 hidden
다중 breakpoint
{
"props": {
"className": "w-full sm:w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/6"
}
}
3. 전역 상태 기반 동적 스타일
모바일 사이드바 예시
{
"id": "sidebar",
"type": "basic",
"name": "Div",
"props": {
"className": "{{_global.sidebarOpen ? 'translate-x-0' : '-translate-x-full'}} fixed inset-y-0 left-0 z-50 w-64 transition-transform duration-300 lg:relative lg:translate-x-0 bg-white"
}
}
해석
- 모바일:
_global.sidebarOpen상태에 따라 슬라이드 인/아웃true:translate-x-0(제자리)false:-translate-x-full(왼쪽으로 완전히 숨김)
- 데스크톱 (lg 이상): 항상 표시 (
lg:relative lg:translate-x-0) - 애니메이션:
transition-transform duration-300(300ms)
4. 조건부 렌더링
오버레이 예시
{
"id": "overlay",
"type": "basic",
"name": "Div",
"if": "{{_global.sidebarOpen}}",
"props": {
"className": "fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": false
}
}
]
}
특징
if속성으로 조건부 렌더링 (사이드바 열릴 때만 DOM에 추가)- 모바일에서만 표시 (
lg:hidden) - 클릭 시 사이드바 닫기
5. 모범 사례
DO
- Tailwind breakpoint 적극 활용
- 전역 상태로 UI 상태 관리
- 조건부 렌더링으로 불필요한 요소 제거
- CSS transition으로 부드러운 애니메이션
- 레이아웃 JSON만으로 반응형 구현
DON'T
- 플랫폼별 집합 컴포넌트 생성 금지 (MobileHeader, MobileSidebar 등)
- JavaScript로 직접 DOM 조작 금지
- 인라인 스타일 과도한 사용 지양
- 미디어 쿼리 직접 작성 지양 (Tailwind 사용)
6. 완전한 반응형 레이아웃 예시
모바일 사이드바 토글이 있는 관리자 레이아웃:
{
"components": [
{
"id": "admin_layout_root",
"type": "basic",
"name": "Div",
"props": {
"className": "flex h-screen overflow-hidden"
},
"children": [
{
"id": "mobile_header",
"type": "basic",
"name": "Div",
"props": {
"className": "flex items-center justify-between h-16 border-b px-4 md:hidden"
},
"children": [
{
"id": "mobile_menu_button",
"type": "basic",
"name": "Button",
"props": {
"className": "p-2 hover:bg-gray-100 rounded-lg"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": "{{!_global.sidebarOpen}}"
}
}
],
"children": [
{
"type": "basic",
"name": "Icon",
"props": {
"name": "bars"
}
}
]
}
]
},
{
"id": "mobile_overlay",
"type": "basic",
"name": "Div",
"if": "{{_global.sidebarOpen}}",
"props": {
"className": "fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
},
"actions": [
{
"event": "onClick",
"type": "setState",
"target": "global",
"payload": {
"sidebarOpen": false
}
}
]
},
{
"id": "sidebar",
"type": "basic",
"name": "Div",
"props": {
"className": "{{_global.sidebarOpen ? 'translate-x-0' : '-translate-x-full'}} fixed inset-y-0 left-0 z-50 w-64 transition-transform duration-300 md:relative md:translate-x-0 flex flex-col border-r bg-white"
},
"children": []
},
{
"id": "content",
"type": "basic",
"name": "Div",
"props": {
"className": "flex-1 overflow-auto"
},
"children": []
}
]
}
]
}
7. 임의의 breakpoint 사용
Tailwind v3부터는 임의의 값을 직접 지정할 수 있습니다:
{
"props": {
"className": "hidden min-[900px]:flex max-[1200px]:grid"
}
}
8. z-index 계층 관리
반응형 레이아웃에서 z-index 계층 구조:
| z-index | 용도 | 설명 |
|---|---|---|
z-50 |
사이드바 | 최상위 |
z-40 |
오버레이 | 사이드바 아래, 컨텐츠 위 |
z-auto |
컨텐츠 | 기본값 |