중첩 라우트의 상위 리소스 ID 를 조회 스코프에 반영하지 않아 A 의 경로로 B 의 하위 리소스를 읽거나 변경할 수 있었다. 방어 위치를 Repository where 절(SSoT)로 통일하고 컨트롤러 사후 비교에 의존하지 않게 했다. 같은 리소스의 형제 엔드포인트가 서로 다른 강도로 검증하던 부분도 맞췄다. 계층 리소스는 자기 자신·자손을 부모로 지정하는 순환을 검증 계층에서 차단하고, 검증을 우회하는 경로(시더/훅/오염 데이터)를 위해 path 재귀에 방문 ID 가드를 뒀다. 설정값이 정하는 깊이 상한을 Service 가 리터럴로 재클램프하던 부분을 제거해 게시판 설정이 실제로 적용되게 했다. 쓰기 경로는 validated 기준으로 좁혀 FormRequest 미정의 필드가 fillable 로 새지 않게 했다. 기존 오염 데이터는 업그레이드 스텝이 정리한다(삭제 없이 최상위로 이동). 재발 방지로 audit 룰 3종을 추가하고, 규정 문서와 트러블슈팅 사례를 함께 갱신했다. 공개 이슈: gnuboard/g7
62 KiB
그누보드7 Development Guide
이 문서는 그누보드7 오픈소스 CMS 프로젝트의 개발 가이드입니다. AI 에이전트 및 외부 기여자를 위한 참고 자료입니다.
빠른 참조 - 상세 가이드 문서
백엔드 backend/ (32개)
| 문서 | 설명 | TL;DR 핵심 |
|---|---|---|
| activity-log-hooks.md | 활동 로그 훅 레퍼런스 (Activity Log Hooks Reference) | 코어 66훅 + 이커머스 92훅 + 게시판 32훅 + 페이지 8훅 = 총 198훅 |
| activity-log.md | 활동 로그 시스템 (Activity Log System) | Monolog 기반: Service 훅 → Listener → Log::channel('activity... |
| admin-settings-access.md | Admin 환경설정 값 접근 (g7_core_settings vs config()) |
동기화 SSoT: storage/app/settings/*.json → SettingsServicePr... |
| api-documentation.md | API 레퍼런스 문서 규정 (API Documentation) | 모든 API 엔드포인트는 레퍼런스 문서 필수 — 메서드/URI/파라미터/응답 필드 + 요청·응답 예시 ... |
| api-resources.md | API 리소스 | Resource: BaseApiResource 상속 필수 / Collection: BaseApiColl... |
| authentication.md | 인증 및 세션 처리 | Laravel Sanctum 토큰 전용 인증 (Bearer 토큰만 사용) |
| broadcasting.md | Broadcasting (실시간 이벤트) | Laravel Reverb 사용 (WebSocket) |
| console-confirm.md | 콘솔 yes/no 프롬프트 (ConsoleConfirm) | 콘솔 커맨드의 yes/no 프롬프트는 $this->unifiedConfirm() 사용 — Laravel... |
| controllers.md | 컨트롤러 계층 구조 | AdminBaseController / AuthBaseController / PublicBaseCont... |
| core-config.md | 코어 설정 (config/core.php) | config/core.php = 코어 권한/역할/메뉴/메일템플릿의 SSoT (Single Source ... |
| core-update-system.md | 코어 업데이트 시스템 (Core Update System) | 코어 업그레이드 스텝: upgrades/ 디렉토리 (프로젝트 루트), 네임스페이스 App\Upgrades |
| data-sync-helpers.md | 데이터 동기화 Helper (Data Sync Helpers) | 모든 데이터 동기화는 Service/Seeder 가 Helper 를 호출해 수행 (직접 Model 조작... |
| dto.md | DTO (Data Transfer Object) 사용 규칙 | DTO 두 패턴 — Value Object(불변 1회 전달) vs Data Carrier(다단계 변형/... |
| enum.md | Enum 사용 규칙 | 상태/타입/분류 = Enum 필수 (PHP 8.1+ Backed Enum) |
| exceptions.md | Custom Exception 다국어 처리 | 예외 메시지 하드코딩 금지 → __() 함수 필수 |
| geoip.md | GeoIP 시스템 (MaxMind GeoLite2) | MaxMind GeoLite2-City DB 기반 IP → 타임존 감지 (SetTimezone 미들웨어... |
| identity-messages.md | 본인인증 메시지 템플릿 시스템 (Identity Messages) | 알림 시스템(notification_*)과 완전 분리된 IDV 전용 템플릿 인프라 |
| identity-policies.md | 본인인증 정책 시스템 (Identity Policies) | - |
| identity-providers.md | IDV Provider 작성 가이드 (Identity Verification Providers) | VerificationProviderInterface 구현 + IdentityProviderManage... |
| language-pack-service.md | LanguagePackService (백엔드 Service 레이어) | LanguagePackService 가 install/activate/deactivate/uninsta... |
| middleware.md | 미들웨어 등록 규칙 | 인증 필요 미들웨어 → 전역 등록 금지! |
| notification-system.md | 알림 시스템 (Notification System) | GenericNotification 범용 클래스 1개로 모든 알림 처리 (개별 클래스 불필요) |
| response-helper.md | API 응답 규칙 (ResponseHelper) | 모든 API 응답은 ResponseHelper 사용 |
| routing.md | 라우트 네이밍 및 경로 | 모든 라우트는 name() 필수: ->name('api.users.index') |
| search-system.md | Scout 검색 엔진 시스템 (Search System) | Laravel Scout + DatabaseFulltextEngine: MySQL FULLTEXT + ... |
| seo-system.md | SEO 페이지 생성기 시스템 (SEO Page Generator) | SeoMiddleware: 봇 요청 감지 → ?locale= 파라미터 해석 → SeoRenderer가 ... |
| service-provider.md | 서비스 프로바이더 안전성 | DB 접근 전 .env 파일 존재 확인 필수 |
| service-repository.md | Service-Repository 패턴 | RepositoryInterface 주입 필수 (구체 클래스 직접 주입 금지) |
| settings-multilingual-enrichment.md | Settings 카탈로그 다국어 자동 보강 | settings JSON 의 다국어 카탈로그 라벨(_cached_name 등)은 카탈로그 빌드 시점에 보강 |
| translatable-seeders.md | 다국어 시더 인터페이스 (Translatable Seeders) | 다국어 JSON 컬럼(name 등)을 시드하는 확장 entity 시더는 TranslatableSeede... |
| user-overrides.md | 사용자 수정 보존 (HasUserOverrides Trait) | 모델에 use HasUserOverrides; + `protected array $trackable... |
| validation.md | 검증 (Validation) | 필수: FormRequest에서 검증 (Service에 검증 로직 배치 금지) |
프론트엔드 frontend/ (50개)
| 문서 | 설명 | TL;DR 핵심 |
|---|---|---|
| actions-g7core-api.md | 액션 시스템 - G7Core API (React 컴포넌트용) | - |
| actions-handlers-navigation.md | 액션 핸들러 - 네비게이션 | - |
| actions-handlers-state.md | 액션 핸들러 - 상태 관리 | - |
| actions-handlers-ui.md | 액션 핸들러 - UI 인터랙션 | - |
| actions-handlers.md | 액션 핸들러 - 핸들러별 상세 사용법 | navigate: 페이지 이동 (path, query, mergeQuery 옵션) |
| actions.md | 액션 핸들러 가이드 | 구조: type 또는 event(이벤트), handler(핸들러명), params(옵션) |
| auth-system.md | 인증 시스템 (AuthManager) | AuthManager: 싱글톤 인증 상태 관리 클래스 |
| component-props-composite.md | 컴포넌트 Props 레퍼런스 - Composite | FileUploader: autoUpload, uploadTriggerEvent, imageCompre... |
| component-props.md | 컴포넌트 Props 레퍼런스 | - |
| components-advanced.md | 컴포넌트 고급 기능 | - |
| components-patterns.md | 컴포넌트 패턴 및 다국어 | - |
| components-types.md | 컴포넌트 타입별 개발 규칙 | - |
| components.md | 컴포넌트 개발 규칙 | HTML 태그 직접 사용 금지 ( → Div, → Button) |
| dark-mode.md | 다크 모드 지원 (engine-v1.1.0+) | Tailwind dark: variant 사용 (예: bg-white dark:bg-gray-800) |
| data-binding-i18n.md | 데이터 바인딩 - 다국어 처리 | - |
| data-binding.md | 데이터 바인딩 및 표현식 | API 데이터: {{user.name}}, URL 파라미터: {{route.id}} |
| data-sources-advanced.md | 데이터 소스 - 고급 기능 | - |
| data-sources.md | 데이터 소스 (Data Sources) | data_sources 배열에 API 정의: id, endpoint, method |
| editors.md | 에디터 컴포넌트 가이드 | HtmlEditor: HTML/텍스트 편집, 게시판/상품 설명 등 사용 |
| g7core-api-advanced.md | G7Core 전역 API 레퍼런스 - 고급 | - |
| g7core-api.md | G7Core 전역 API 레퍼런스 | G7Core.state: get/set/subscribe 전역 상태 관리 |
| g7core-helpers.md | G7Core 헬퍼 API | - |
| identity-guard-interceptor.md | IdentityGuardInterceptor — 코어 본인인증 인터셉터 레퍼런스 | ActionDispatcher.handleApiCall 응답 후처리에서 isIdentityRequire... |
| identity-verification-ui.md | 본인인증(IDV) 공통 UI 가이드 | 모든 IDV 강제 지점은 동일한 428 응답 형식을 공유 (코어 9 + 게시판 4 + 이커머스 4 + N) |
| layout-json-components-loading.md | 레이아웃 JSON - 데이터 로딩 및 생명주기 | - |
| layout-json-components-rendering.md | 레이아웃 JSON - 조건부/반복 렌더링 | - |
| layout-json-components-slots.md | 레이아웃 JSON - 슬롯 시스템 | - |
| layout-json-components.md | 레이아웃 JSON - 컴포넌트 (반복 렌더링, Blur, 생명주기, 슬롯) | if: 조건부 렌더링 (type: "conditional" 사용 금지!) |
| layout-json-features-actions.md | 레이아웃 JSON - 초기화, 모달, 액션, 스크립트 | - |
| layout-json-features-error.md | 레이아웃 JSON - 에러 핸들링 | - |
| layout-json-features-styling.md | 레이아웃 JSON - 스타일 및 계산된 값 | - |
| layout-json-features.md | 레이아웃 JSON - 기능 (에러 핸들링, 초기화, 모달, 액션) | classMap: 조건부 CSS 클래스 (key → variants 매핑) |
| layout-json-inheritance.md | 레이아웃 JSON - 상속 (Extends, Partial, 병합) | extends: 베이스 레이아웃 상속 (type: "slot" 위치에 삽입) |
| layout-json.md | 레이아웃 JSON 스키마 | HTML 태그 직접 사용 금지 → 기본 컴포넌트 사용 (Div, Button, Span) |
| layout-testing.md | 그누보드7 레이아웃 파일 렌더링 테스트 가이드 | createLayoutTest()로 테스트 헬퍼 생성, mockApi()로 API 응답 모킹 |
| modal-usage.md | Modal 컴포넌트 사용 가이드 | modals 섹션 모달은 openModal 핸들러로 열고, closeModal 핸들러로 닫음 |
| responsive-layout.md | 반응형 레이아웃 개발 (engine-v1.1.0+) | responsive 속성: 컴포넌트 레벨 breakpoint 오버라이드 (권장) |
| security.md | 보안 및 검증 | 레이아웃 JSON: FormRequest + Custom Rule 10종 검증 (서버 사전 차단) |
| state-management-advanced.md | 상태 관리 - 고급 기능 | - |
| state-management-forms.md | 상태 관리 - 폼 자동 바인딩 및 setState | - |
| state-management.md | 전역 상태 관리 | 전역 상태: _global.속성명 (앱 전체 공유, 페이지 이동 시 유지) |
| tailwind-safelist.md | Tailwind Safelist 가이드 | Tailwind는 빌드 시 사용된 클래스만 CSS에 포함 |
| template-development.md | 템플릿 개발 가이드라인 | 디렉토리: templates/[vendor-template]/ (예: sirsoft-admin_basic) |
| template-handlers.md | 템플릿 전용 핸들러 | setLocale: 앱 언어 변경 — 엔진 빌트인 (ActionDispatcher) |
| components.md | sirsoft-admin_basic 컴포넌트 | Basic 37개: HTML 래핑 (Div, Button, Input, Select, Form, A, ... |
| handlers.md | sirsoft-admin_basic 핸들러 | setLocale: 앱 언어 변경 (locale 파라미터) |
| layouts.md | sirsoft-admin_basic 레이아웃 | 베이스: _admin_base.json (사이드바 + 헤더 + 콘텐츠 슬롯) |
| components.md | sirsoft-basic 컴포넌트 | Basic 26개: HTML 래핑 (Div, Button, Input, Select, Form, A, ... |
| handlers.md | sirsoft-basic 핸들러 | setTheme/initTheme: 다크/라이트 모드 전환 (admin과 동일 키 공유) |
| layouts.md | sirsoft-basic 레이아웃 | 베이스: _user_base.json (헤더 + 푸터 + 모바일 네비 + 콘텐츠 슬롯) |
확장 시스템 extension/ (30개)
| 문서 | 설명 | TL;DR 핵심 |
|---|---|---|
| cache-driver.md | 캐시 드라이버 시스템 (CacheInterface) | 모든 캐시 저장은 CacheInterface 사용 (Cache:: 직접 호출 금지) |
| changelog-rules.md | Changelog 규칙 (Changelog Rules) | 확장/코어 버전 업 시 CHANGELOG.md에 변경사항 기록 필수 (미기록 시 버전 업 불가) |
| editor-spec.md | 편집기 스펙 (editor-spec.json) | editor-spec.json = 편집기 팔레트/스타일 컨트롤/중첩 규칙/샘플 데이터/레시피의 선언 (... |
| extension-manager.md | ExtensionManager (확장 관리자) | composer.json 수정 없음 - 런타임 오토로드 방식 사용 |
| extension-update-system.md | 확장 업데이트 시스템 (Extension Update System) | 업데이트 감지 우선순위: GitHub > _bundled (2단계, _pending 미참여) |
| hooks.md | 훅 시스템 (Hook System) | Action 훅: doAction() - 부가 작업 (로그, 알림, 캐시) |
| language-packs.md | 언어팩 시스템 (Language Packs) | 코어/번들 확장의 lang/{ko,en}/ 는 가상 보호 행으로 자동 노출 (DB 없이 항상 activ... |
| layout-extensions.md | 레이아웃 확장 시스템 (Layout Extensions) | - |
| menus.md | 메뉴 시스템 | 구조: User → Role → role_menus 피벗 → Menu |
| module-assets.md | 모듈 프론트엔드 에셋 시스템 | module.json에 에셋 매니페스트 정의 (js, css, loading strategy) |
| module-basics.md | 모듈 개발 기초 | 디렉토리: vendor-module (예: sirsoft-ecommerce) |
| module-commands.md | 모듈 Artisan 커맨드 | 목록: php artisan module:list |
| module-i18n.md | 모듈 다국어 시스템 | 백엔드: /lang/{locale}/*.php → __('vendor-module::key') |
| module-identity-settings.md | 모듈/플러그인 본인인증(IDV) 설정 통합 가이드 | 정책/목적/메시지: module.php::getIdentity{Policies,Purposes,Mess... |
| module-layouts.md | 모듈 레이아웃 시스템 | 위치: modules/_bundled/vendor-module/resources/layouts/admi... |
| module-routing.md | 모듈 라우트 규칙 | URL prefix 자동: /api/modules/[vendor-module]/... |
| module-settings.md | 모듈 환경설정 시스템 개발 가이드 | - |
| permissions.md | 권한 시스템 | 구조: User → Role → Permission (기능 레벨) |
| plugin-development.md | 플러그인 개발 가이드 | 디렉토리: plugins/vendor-plugin (예: sirsoft-payment) |
| sample-extensions.md | 학습용 샘플 확장 (Sample Extensions) | 샘플 확장 4종: gnuboard7-hello_module / _plugin / _admin_templ... |
| storage-driver.md | 스토리지 드라이버 시스템 (StorageInterface) | 모든 파일 저장은 StorageInterface 사용 (Storage::disk() 직접 호출 금지) |
| template-basics.md | 템플릿 시스템 기초 | 타입: Admin (관리자용), User (일반사용자용) |
| template-caching.md | 템플릿 캐싱 전략 | - |
| template-commands.md | 템플릿 Artisan 커맨드 | 목록: php artisan template:list |
| template-idv-bootstrap.md | 템플릿 IDV launcher 등록 가이드 | 템플릿 부트스트랩(initTemplate)에서 window.G7Core.identity.setLaunc... |
| template-routing.md | 템플릿 라우트/언어 파일 규칙 | - |
| template-security.md | 템플릿 보안 정책 | - |
| template-workflow.md | 템플릿 개발 워크플로우 | 필수 파일: template.json, routes.json, _base.json, errors/{40... |
| upgrade-step-guide.md | 업그레이드 스텝 작성 가이드 (Upgrade Step Guide) | upgrade step 이 실행되는 환경은 경로에 따라 다르다 — 섹션 9 "업그레이드 경로" 먼저 읽기 |
| vendor-bundle.md | Vendor 번들 시스템 (Vendor Bundle System) | - |
공통 (5개)
| 문서 | 설명 | TL;DR 핵심 |
|---|---|---|
| cheatsheet.md | 그누보드7 자주 쓰는 명령어 치트시트 | _bundled에서 레이아웃 JSON만 수정 → 확장 업데이트(--force)만 실행 (빌드 불필요) |
| database-guide.md | 그누보드7 데이터베이스 개발 가이드 | 마이그레이션: 한국어 comment 필수, down() 구현 필수 |
| requirements.md | 그누보드7 시스템 요구사항 (System Requirements) | PHP 8.2+ 필수 |
| testing-guide.md | 그누보드7 테스트 가이드 | 테스트 통과 = 작업 완료 (작성만으로 불충분!) |
| e2e-testing.md | 그누보드7 Playwright E2E 테스트 가이드 | - |
API 레퍼런스 진입점
엔드포인트별 요청 파라미터·응답 필드·요청/응답 예시. 공통 규약(Bearer 토큰 인증, 응답 봉투, 페이지네이션, 401/403/422/428)은 진입점 문서 상단에 정리되어 있다.
| 대상 | 진입점 | 문서/엔드포인트 |
|---|---|---|
| 코어 | docs/backend/api/README.md | 35 / 291 |
확장 API 레퍼런스 (11개 확장, 자동 스캔)
각 확장이 소유하는 API 문서 목차.
php artisan api:docgen이 생성하며, 이 표는{modules,plugins}/_bundled/*/docs/api/README.md를 패턴 스캔해 자동 편입된다(확장명 하드코딩 없음).
| 확장 | 유형 | API 문서 목차 | 문서/엔드포인트 |
|---|---|---|---|
gnuboard7-hello_module |
모듈 | docs/api/ | 1 / 2 |
sirsoft-board |
모듈 | docs/api/ | 10 / 80 |
sirsoft-ecommerce |
모듈 | docs/api/ | 33 / 231 |
sirsoft-page |
모듈 | docs/api/ | 2 / 17 |
sirsoft-ckeditor5 |
플러그인 | docs/api/ | 2 / 2 |
sirsoft-gdpr |
플러그인 | docs/api/ | 4 / 15 |
sirsoft-marketing |
플러그인 | docs/api/ | 2 / 2 |
sirsoft-pay_kginicis |
플러그인 | docs/api/ | 5 / 22 |
sirsoft-pay_nhnkcp |
플러그인 | docs/api/ | 0 / 0 |
sirsoft-pay_nicepayments |
플러그인 | docs/api/ | 0 / 0 |
sirsoft-verification_kginicis |
플러그인 | docs/api/ | 1 / 1 |
프로젝트 개요
프로젝트명: 그누보드7 목적: 오픈소스 CMS 플랫폼 설계 원칙: 코어 수정 최소화, 모듈화, 플러그인 시스템, 템플릿 시스템, 동적 로딩
버전 동기화 의무
코어 또는 번들 확장의 공개 표면을 수정할 때, 그 변경의 영향 범위에 있는 다른 확장의 버전 제약(g7_version, dependencies.{modules|plugins})을 함께 갱신한다.
① 코어 → 확장 동기화 (requires.g7_version)
- 트리거: 코어 공개 확장 표면(
app/Extension/Abstract*,HookManager,ExtensionManager,ModuleManager,PluginManager,TemplateManager,app/Contracts/Extension/**,app/Extension/Helpers/**,app/Seo/Contracts/**,app/ActivityLog/**공개 API, 루트CHANGELOG.mdAdded/Changed/Removed) 수정 - 조치: 영향 받는 번들 확장의
g7_version상향 + 각 확장 CHANGELOG 에 변경 기재
② 확장 → 확장 동기화 (dependencies.{modules|plugins})
- 트리거: 번들 모듈/플러그인의 공개 Service/Contract/Repository/Model/Route, 발행 훅·이벤트, CHANGELOG 수정
- 조치: 그 확장에 의존하는 다른 번들 확장 전수 스캔 → 최소 버전 제약 상향 여부 판정
판정 순서
- 기존 소비자 API 시그니처/동작을 건드렸는가 → 소비 확장 최소 버전 상향
- 새 공개 API 가 도입되었는가 → 후보 확장 전수 스캔 후 검토
- 의존 관계 B 의 공개 API 가 변경되었는가 → A 의
dependencies.B상향 - 동기화 대상이 없다면 그 근거("순수 내부 리팩토링" 등)를 변경 이력에 기록
상세: changelog-rules.md "코어 버전 제약 정책"
CRITICAL RULES - 절대 금지 패턴 (DO NOT)
API/핸들러 호출
| 금지 | 올바른 사용 |
|---|---|
G7Core.actions.execute |
G7Core.dispatch |
G7Core.api.call |
G7Core.dispatch({ handler: 'apiCall', ... }) |
handler: "api" |
handler: "apiCall" |
handler: "nav" |
handler: "navigate" |
handler: "setLocalState" |
handler: "setState" + target: "local" |
navigate + replace: true (URL만 변경 시) |
handler: "replaceUrl" |
apiCall params.target (params 내부) |
target 은 액션 top-level. params 내부 위치 시 URL 미해석 |
apiCall params.onSuccess / params.onError (params 내부) |
액션 top-level. params 내부면 무시됨 |
refetchDataSource params.id |
params.dataSourceId 사용 |
handler: "showToast" |
handler: "toast" |
모달 안에서 부모 _local.* 참조 |
데이터소스 응답 필드 또는 _global 사용 (모달은 별도 컨텍스트) |
데이터 바인딩
| 금지 | 올바른 사용 |
|---|---|
{{products.data}} |
{{products?.data?.data}} (배열 경로 확인) |
{{value}} |
{{value ?? ''}} (fallback 필수) |
{{error.data}} |
{{error.errors}} (API 응답 구조) |
{{error.data?.errors ?? {}}} |
{{error.errors}} ({}}} 파서 모호성 회피) |
$value (이벤트 값) |
$event.target.value |
{{props.xxx}} (Partial) |
data_sources ID 직접 참조 |
{{$response.xxx}} (onSuccess) |
{{response.xxx}} ($ 접두사 없음) |
iteration/반복 렌더링
| 금지 | 올바른 사용 |
|---|---|
"item", "index" |
"item_var", "index_var" |
| iteration 내 if 순서 무시 | if가 iteration보다 먼저 평가됨 |
컴포넌트 Props
| 금지 | 올바른 사용 |
|---|---|
Icon className="w-4 h-4" |
className="text-base" (아래 등가표) 또는 size prop |
Select valueKey/labelKey |
computed로 { value, label } 변환 |
Form 내 Button type 없음 |
type="button" 명시 (submit 방지) |
options={{options}} |
options={{options ?? []}} (fallback) |
Icon 은 <i> 글리프라 박스 크기가 곧 font-size 다. w-N h-N 은 박스만 정하고 글리프는 부모 font-size 를 상속하므로 어긋난다. 기존 w-N h-N 을 옮길 때는 아래 등가표를 쓴다 (Chrome 실측).
w-N h-N |
px | 등가 className |
|---|---|---|
w-3 h-3 |
12 | text-xs |
w-3.5 h-3.5 |
14 | text-sm |
w-4 h-4 |
16 | text-base |
w-5 h-5 |
20 | text-xl |
w-6 h-6 |
24 | text-2xl |
w-12 h-12 |
48 | text-5xl |
size prop 은 Font Awesome fa-* 클래스로 매핑되며 등가가 아니다 — size="sm" → fa-sm → font-size: 0.875em(상대값) + line-height 붕괴로 16px 이 12.25×0.88px 이 된다. 새 아이콘에는 써도 되지만, 기존 w-N h-N 의 치환용으로는 쓰지 않는다.
상태 관리
| 금지 | 올바른 사용 |
|---|---|
| 스냅샷 기반 setState | 함수형 업데이트 또는 stateRef.current |
| closeModal 후 setState | setState 후 closeModal (순서 중요) |
| sortable 내 폼 자동바인딩 | parentFormContextProp={undefined} |
| await 후 캡처된 상태 사용 | await 후 G7Core.state.getLocal() 재조회 |
setState params 키에 {{}} 사용 |
키는 정적 경로만, 배열 조작은 .map()/.filter() |
핸들러 정의
| 금지 | 올바른 사용 |
|---|---|
{{handler()}} (표현식에서 호출) |
actions: [{ handler: "xxx" }] |
globalHeaders 사용 (engine-v1.16.0+)
| 금지 | 올바른 사용 |
|---|---|
"globalHeaders": { "X-Key": "value" } |
"globalHeaders": [{ "pattern": "*", "headers": {...} }] |
| 모든 API에 개별 headers 설정 | globalHeaders로 공통 헤더 정의 |
| pattern 없이 헤더 정의 | pattern 필수 (*, /api/shop/* 등) |
인증/리다이렉트 규칙 (engine-v1.47.0+)
| 금지 | 올바른 사용 |
|---|---|
모듈/플러그인에서 AuthManager.updateConfig() 호출 |
템플릿 부트스트랩(initTemplate)에서만 호출 |
AuthManager.updateConfig({ loginPath: 'https://...' }) (외부 origin) |
loginPath 는 / 로 시작하는 동일 origin path-only |
AuthManager.updateConfig({ loginPath: '//evil.com/...' }) (protocol-relative) |
// 시작 금지 (open redirect 방지) |
401 에러 페이지(errors/401.json)에서 직접 로그인 리다이렉트 구현 |
코어 TemplateApp.showRouteError 가드에 위임 (자동 처리) |
정적 확장자 라우트 / 자산 URL 생성
| 금지 | 올바른 사용 |
|---|---|
Route::get('{id}/routes.json', ...) (.js/.css/.json/.map 단일 등록) |
Route::dualSuffix('{id}/routes', 'json', ...) — 확장자 형태 + 확장자 없는 형태 동시 등록 |
Route::get('bundle.js', ...) (접미사가 종류를 구분해 제거 불가) |
Route::dualSuffixSegment('bundle', 'js', ...) (bundle.js + bundle/js) |
Route::get('assets/{id}/{path}', ...) (와일드카드 자산) |
Route::dualAsset('assets/{id}', ...) (.../{path} + ?file= 쿼리) |
서버에서 '/api/templates/assets/'.$id.'/'.$path 문자열 조립 |
App\Support\AssetUrl::templateAsset($id, $path) |
프론트에서 `/api/templates/${id}/routes.json` 템플릿 리터럴 조립 |
resources/js/core/support/assetUrl.ts 의 suffixed() / templateAsset() 등 |
정규식 location 은 프리픽스 location 보다 먼저 매칭되므로, 정적 최적화 블록(location ~* \.(js|css|json)$)이 있는 서버에서는 확장자 붙은 동적 응답이 try_files ... /index.php 폴백 기회 없이 404 가 된다. 서버측 AssetUrl 과 프론트측 assetUrl.ts 는 동일 규칙을 공유하므로 한쪽만 바꾸면 그 자산만 404 가 된다. 상세: routing.md "정적 확장자로 끝나는 동적 엔드포인트", api/README.md "자산 URL 이중 모드".
중첩 리소스 스코프 / 계층 무결성
| 금지 | 올바른 사용 |
|---|---|
| 중첩 라우트의 상위 리소스 ID 를 받아만 두고 조회에 미반영 | Repository where 절에 상위 스코프 반영(SSoT) + Service 가 상위 ID 전달 → 교차 접근 시 404 |
$request->except(...) / ->all() 결과를 Service 쓰기 메서드로 전달 |
$request->validated() 기준 (FormRequest 미정의 필드가 $fillable 로 새는 것 차단) |
요청 배열 항목의 Rule::exists 에 상위 스코프 미부착 |
Rule::exists(Model::class,'id')->where('order_id', $order->id) → 422 |
수정/순서변경 FormRequest 의 parent_id 에 Rule::exists 만 부착 |
자손 전체를 검사하는 순환 방지 Rule 부착 (자기참조만 막는 Rule 은 A→B→A 통과) |
| 같은 리소스의 두 엔드포인트가 서로 다른 검증 강도 | 부모 변경 경로 전부 동일 강도 — 약한 쪽이 우회로가 된다 |
| 설정값이 정하는 한계를 Service 에서 리터럴로 재클램프 | Service 는 계산만, 상한 검증은 Rule 단일 책임 (이중 클램프 시 깊이 제한이 통째로 무력화) |
| 계층 재귀(path/depth 재계산)에 방문 ID 가드 없음 | 방문 집합으로 유한 종료 — 검증 우회 경로/오염 데이터에서도 무한 루프 금지 |
상세: validation.md "계층 리소스 순환 참조" / "배열 항목의 상위 스코프", service-repository.md "중첩 리소스 스코프" / "설정 기반 한계값"
Listener 데이터 접근
| 금지 | 올바른 사용 |
|---|---|
Listener 에서 Model::query/find/where/create 직접 호출 |
Repository 인터페이스 주입 후 위임 |
Listener 에서 DB::table()->update(...) |
Repository 의 도메인 의도 메서드 (recalculate*/anonymize* 등) |
Listener 에서 $row->save() / saveQuietly() / delete() |
Repository 의 update/save/delete 호출 |
| Listener 생성자에 구체 Repository 직접 주입 | Repository Interface 주입 |
Listener 에서 request() / $_POST 직접 접근 |
Service 가 검증 후 도메인 객체로 전달 받기 |
Filter 훅에 'type' => 'filter' 누락 |
type 명시 필수 (반환값 무시 회귀 차단) |
Listener 가 HookListenerInterface 미구현 (auto-discovery 대상) |
implements + getSubscribedHooks() 정적 메서드 |
템플릿 엔진 내부 버전 (engine-v1.x.x)
코드와 문서에서 engine-v1.x.x 형태의 버전은 템플릿 엔진의 내부 개발 이력입니다.
그누보드7 공식 버전(config/app.php)과는 무관합니다.
- CHANGELOG:
resources/js/core/template-engine/CHANGELOG.md - 표기법:
engine-v1.X.Y(engine- 접두사 필수,v1.X.Y단독 사용 금지) - 사용처: @since JSDoc, 인라인 주석, 규정 문서
엔진 CHANGELOG 반영 규칙
| 트리거 | 필수 작업 |
|---|---|
resources/js/core/template-engine/** 기능 추가 시 |
마이너 버전 업 (engine-v1.X+1.0) + CHANGELOG 기록 |
resources/js/core/template-engine/** 버그 수정 시 |
패치 버전 업 (engine-v1.X.Y+1) + CHANGELOG 기록 |
resources/js/core/*.ts (TemplateApp, G7CoreGlobals 등) 버그 수정 시 |
CHANGELOG [Unreleased] 또는 해당 버전에 기록 |
코드에 @since 추가 시 |
CHANGELOG 해당 버전에 항목 추가 |
| 규정 문서에 엔진 버전 표기 시 | engine-v1.X.Y 형식 사용 |
엔진 CHANGELOG 대상 범위
엔진 CHANGELOG에 기록하는 대상은 엔진 코어 코드의 변경사항입니다:
| 포함 (엔진 코드) | 제외 (비엔진 코드) |
|---|---|
resources/js/core/template-engine/** |
templates/**/src/components/** (템플릿 컴포넌트) |
resources/js/core/TemplateApp.ts |
modules/**/resources/layouts/** (모듈 레이아웃) |
resources/js/core/G7CoreGlobals.ts |
resources/layouts/** (코어 레이아웃 JSON) |
resources/js/core/template-engine.ts |
docs/** (규정 문서) |
resources/js/core/types/ (엔진 타입) |
백엔드 PHP 코드 |
엔진 CHANGELOG 작성 형식
Keep a Changelog 표준:
### Added— 새 기능### Fixed— 버그 수정### Changed— 기존 기능 변경### Deprecated— 곧 제거될 기능### Removed— 제거된 기능
CHANGELOG 항목 작성 규칙
엔진 코드 수정 후 CHANGELOG 미기록 시 작업 미완료로 간주합니다.
항목 형식: - 수정 내용 요약 (수정 파일명)
# 좋은 예
- setState dot notation 멀티 키 병합 시 이전 키 변경 유실 방지 (ActionDispatcher)
- blocking 데이터소스 + errorHandling 데드락 — fallback 동기 적용, 에러핸들러 비동기 실행
# 나쁜 예
- 버그 수정 ← 무엇을 수정했는지 불명확
- ActionDispatcher.ts 수정 ← 파일명만으로는 변경 내용 파악 불가
패치 버전 항목 형식: - (engine-v1.X.Y) 수정 내용 (파일명)
- (engine-v1.17.5) dataKey 자동 바인딩 컴포넌트에서 setState 호출 시 stale 값 방지
버전 결정 기준:
| 상황 | 버전 처리 |
|---|---|
| 새 기능 추가 (핸들러, 속성, API) | 마이너 버전 업: engine-v1.X+1.0 |
| 기존 기능 버그 수정 | 패치 버전 업: engine-v1.X.Y+1 |
| 특정 버전에 귀속 불가한 수정 | [Unreleased] 섹션에 기록 |
| 릴리스 시 | [Unreleased] → [engine-v1.X.0]으로 이동 |
대규모 Fixed 섹션 카테고리 분류 (항목 10개 초과 시):
### Fixed
#### 상태 동기화
- 항목 1
- 항목 2
#### 캐시
- 항목 3
공개 CHANGELOG 작성 규칙
코어(CHANGELOG.md) 및 확장(modules/*/CHANGELOG.md, plugins/*/CHANGELOG.md, templates/*/CHANGELOG.md)의 릴리즈 CHANGELOG 작성 규칙입니다.
톤과 표현
- 사용자/개발자가 읽는 문서이므로 사용자 관점으로 작성
- "~할 수 있도록 개선", "~하도록 변경", "~문제 수정" 톤 사용
- 각 불릿은 1~2줄로 간결하게
- 내부 구현 상세(클래스명, 파일 경로, 테스트 건수, 훅 체인, DI 패턴)는 포함하지 않음
- 이슈 번호(
#123)는 포함하지 않음
포함/제외 대상
| 포함 | 제외 |
|---|---|
| 사용자에게 보이는 기능 추가/변경 | 내부 파일 경로, 클래스/메서드명 |
| API 변경 (엔드포인트, 파라미터) | 테스트 건수/파일명 |
| 기존 기능의 버그 수정 | 리팩토링 세부사항 |
| 성능 개선 (체감 가능한 것) | 내부 규정/문서 변경 |
| Breaking Change | 내부 작업 이슈 번호 단독 (예: refs #347) |
| 엔진 버전 참조 (engine-v1.X.Y) | 코드 패턴 설명 |
공개 제보자 attribution ((#N @login 님께서 제보해주셨습니다.)) |
— |
KISA 등 공식 보안 채널 ((KISA 측에서 제보해주셨습니다 — KVE-XXXX-XXXXX)) |
— |
공개 제보자 attribution
공개 저장소(GitHub) 이슈로 제보·건의된 항목이 출시 CHANGELOG 에 반영되면, 항목 끝에 공개 이슈 번호와 제보자 GitHub 핸들 멘션을 부착합니다.
- 형식:
- (본문) (#N @login 님께서 제보해주셨습니다.)또는... 건의해주셨습니다. - 톤: 버그 리포트는 "제보", 제안형 개선 요청은 "건의"
- 다중 매칭:
(#A @x, #B @y 님께서 제보해주셨습니다.)/ 혼재:(#A @x 님께서 제보해주시고, #B @y 님께서 건의해주셨습니다.) - KISA 등 공식 보안 채널: GitHub 멘션 없이 텍스트 "KISA 측에서" + 공개 가능한 식별자만
신규 기능의 버그 수정 제외 규칙
해당 릴리즈에서 새로 도입한 기능의 개발 중 버그 수정은 Fixed에 기록하지 않습니다. 사용자 관점에서 그 기능은 해당 릴리즈에서 처음 제공되므로, "추가했다가 고쳤다"는 내부 개발 이력일 뿐입니다.
판단 기준: 해당 버그가 이전 릴리즈에도 존재했던 기능에서 발생한 것인지 확인
기능 그룹핑
Added/Changed/Fixed 내 항목이 10개를 초과하면 #### 서브 헤딩으로 기능 단위 분류합니다.
Keep a Changelog 형식
## [버전] - YYYY-MM-DD헤더 필수### Added/### Changed/### Fixed/### Removed카테고리 사용- 최신 버전이 파일 상단
레이아웃 JSON 구현 규칙
1. 새로운 기능 사용 전 → 반드시 해당 규정 문서에서 지원 여부 확인
2. 지원되지 않는 문법 사용 금지 → 추측/가정으로 구현하지 않음
3. 불확실한 경우 → 기존 레이아웃 패턴 참조
4. 규정 문서에 없는 기능 → 절대 사용 금지
레이아웃 작성 체크리스트
□ 레이아웃 구조가 layout-json.md 스키마와 일치하는가?
□ 사용할 컴포넌트가 components.md에 정의되어 있는가?
□ 컴포넌트 props가 component-props.md에 정의된 것만 사용하는가?
□ 사용할 핸들러가 actions.md에 정의되어 있는가?
□ 핸들러의 params 구조가 actions-handlers.md와 일치하는가?
□ 데이터 바인딩 문법이 data-binding.md에 정의된 형식인가?
□ 다크 모드 클래스가 dark-mode.md 규칙을 따르는가?
□ 기존 유사 레이아웃에서 동일 패턴이 사용되고 있는가?
주의 사항
필수: 규정 문서에 정의된 핸들러/props/바인딩 문법만 사용 (API 응답 구조도 확인 후 바인딩)
필수: Partial은 컴포넌트 치환만 수행 (computed, data_sources, modals, state 미지원)
필수: data_sources ID 고유성 유지, 조건부 렌더링은 if 속성만 사용 (type: "conditional" 미지원)
테스트 프로토콜
기능 구현 = 테스트 코드 작성 필수
신규 기능 / 도메인 표면 변경 = 시나리오 매니페스트(tests/scenarios/<feature>.yaml) 작성 의무 — 입력 axis cross product + 후속 효과 체인 전수 커버
테스트 통과 = 작업 완료 (작성만으로 불충분!)
기존 테스트 있음 → 변경사항 반영하여 수정 후 실행
기능 구현 시 관련된 모든 계층(백엔드+프론트엔드+레이아웃 렌더링) 테스트 필수
주의: 모듈/플러그인 프론트엔드 테스트는 독립 vitest.config.ts 사용 (루트 config 포함 금지)
필수: 도메인 매트릭스 = 테스트 위치/형식 가이드. 입력 조합 망라 의무는 시나리오 매니페스트가 SSoT
필수: 버그 수정은 먼저 실패하는 회귀 테스트 → fail 확인 → 수정 → green 4단계
필수: 테스트 중 발견한 무관 에러도 같은 세션에서 처리 (stale test 또는 로직 수정)
필수: 릴리스 전 composer test-smoke 통과
상세: docs/testing-guide.md — 기능 단위 시나리오 매트릭스, 도메인 매트릭스, Pre-release Smoke Suite, 회귀 테스트 4단계, 무관 에러 처리
그누보드7 레이아웃 렌더링 테스트
그누보드7 레이아웃 테스트는 브라우저 기반 E2E가 아님!
Vitest + createLayoutTest() 유틸리티 사용 → 추가 인프라 불필요
"인프라 부족" 이유로 레이아웃 테스트 건너뛰기 절대 금지
레이아웃 테스트는 해당 레이아웃이 속한 확장 디렉토리에 작성
모듈 테스트: modules/_bundled/{id}/resources/js/__tests__/layouts/
템플릿 테스트: templates/_bundled/{id}/__tests__/layouts/
코어 테스트: resources/js/core/template-engine/__tests__/layouts/
| 특성 | 설명 |
|---|---|
| 테스트 환경 | Vitest (jsdom) - 브라우저 불필요 |
| 렌더링 | DynamicRenderer를 통한 실제 React 렌더링 |
| 유틸리티 | createLayoutTest() - 이미 구축됨 |
| API 모킹 | mockApi() - fetch 자동 모킹 |
| 상태 관리 | getState(), setState() - 즉시 사용 가능 |
| 액션 트리거 | triggerAction() - 핸들러 실행 |
import { createLayoutTest, screen } from '../utils/layoutTestUtils';
const testUtils = createLayoutTest(layoutJson);
testUtils.mockApi('products', { response: { data: [] } });
await testUtils.render();
expect(screen.getByTestId('element')).toBeInTheDocument();
testUtils.cleanup();
테스트 작성 트리거
| 수정 대상 | 테스트 파일 위치 | 테스트 유형 |
|---|---|---|
app/Models/*.php |
tests/Unit/Models/*Test.php |
모델 메서드, 관계, 스코프 |
app/Services/*.php |
tests/Unit/Services/*Test.php |
비즈니스 로직 |
app/Enums/*.php |
tests/Unit/Enums/*Test.php |
Enum 메서드 |
app/Http/Controllers/**/*.php |
tests/Feature/**/*Test.php |
API 엔드포인트 |
database/migrations/*.php |
해당 모델/서비스 테스트에서 검증 | 스키마 변경 |
templates/**/src/components/**/*.tsx |
templates/**/__tests__/*.test.tsx |
컴포넌트 |
resources/js/core/**/*.ts |
resources/js/core/__tests__/*.test.ts |
템플릿 엔진 |
resources/layouts/**/*.json |
resources/js/core/template-engine/__tests__/layouts/*.test.tsx |
코어 레이아웃 렌더링 |
modules/**/resources/layouts/**/*.json |
modules/_bundled/{id}/resources/js/__tests__/layouts/*.test.tsx |
모듈 레이아웃 렌더링 |
templates/**/layouts/**/*.json |
templates/_bundled/{id}/__tests__/layouts/*.test.tsx |
템플릿 레이아웃 렌더링 |
기능 구현 시 전 계층 테스트
| 작업 유형 | 백엔드 (PHPUnit) | 프론트엔드 (Vitest) | 레이아웃 렌더링 (Vitest) |
|---|---|---|---|
| 새 화면 구현 | API 엔드포인트 테스트 | 컴포넌트 테스트 | 레이아웃 JSON 렌더링 테스트 |
| 기존 화면 수정 | 변경된 API 테스트 | 변경된 컴포넌트 테스트 | 레이아웃 렌더링 회귀 테스트 |
| 데이터 흐름 변경 | Service/Repository 테스트 | 상태 관리 테스트 | 데이터 바인딩 렌더링 테스트 |
Windows 환경 테스트 규칙
프론트엔드 (npm/Vitest) → PowerShell 래퍼 필수
백엔드 (PHPUnit/Laravel) → Bash 직접 실행
프론트엔드 (템플릿 디렉토리에서 실행 권장):
# 템플릿 디렉토리에서 실행 (해당 템플릿만 테스트)
cd templates/sirsoft-admin_basic
powershell -Command "npm run test:run" # 전체
powershell -Command "npm run test:run -- DataGrid" # 특정 테스트
# 루트에서 실행 (모든 테스트)
powershell -Command "npm run test:run"
powershell -Command "npm run test:run -- template-engine" # 코어 테스트
백엔드:
php artisan test
php artisan test --filter=TestName
_bundled 확장 테스트 (활성 디렉토리 복사 불필요):
# _bundled 모듈 테스트 직접 실행
php vendor/bin/phpunit modules/_bundled/sirsoft-ecommerce/tests
php vendor/bin/phpunit --filter=ShippingPolicyControllerTest modules/_bundled/sirsoft-ecommerce/tests
# _bundled 모듈 프론트엔드 테스트
cd modules/_bundled/sirsoft-ecommerce
powershell -Command "npm run test:run"
필수 준수 사항
필수: 기능 구현 시 모든 계층(백엔드+프론트엔드+레이아웃) 테스트 포함
필수: 테스트 통과 확인 후 완료 선언 (기존 테스트 유지 — 삭제/skip 금지)
필수: createLayoutTest() 유틸리티 활용 (추가 인프라 불필요)
npm install 규칙
기본 npm install은 package-lock.json을 자동 수정할 수 있으므로, lock 파일 변경 의도가 없는 의존성 복구나 작업 환경 재구성에는 npm install --package-lock=false를 사용합니다.
| 상황 | 권장 명령어 | 비고 |
|---|---|---|
| 누락 의존성 복구 / 작업 환경 재구성 | npm install --package-lock=false |
lock 파일 변경 없이 설치 |
| clean install | npm ci |
package.json과 package-lock.json이 동기화된 경우 |
| 의존성 신규 추가/업데이트 | npm install <pkg> |
lock 변경이 작업 범위에 포함된 경우만 |
lock 파일 변경 의도가 없는 상황에서 npm install 단독 실행을 피합니다. module.json, plugin.json, template.json의 version을 바꾸면 해당 확장의 package.json, package-lock.json, composer.json 버전도 함께 동기화합니다. 의존성 재설치 없이 lock 파일의 version 필드만 갱신할 때는 npm install --package-lock-only를 사용합니다.
핵심 원칙
1. 동적 로딩
절대 금지: composer.json에 모듈/플러그인 하드코딩
필수: /modules와 /plugins 디렉토리 스캔으로 자동 발견
2. 코어 수정 최소화
- 모든 확장은 모듈/플러그인으로 구현
- 훅 시스템을 통한 기능 추가
- 서비스 계층에서 훅 실행
3. 계층 분리
Controller → Request → Service → RepositoryInterface → Repository → Model
4. Repository 인터페이스
절대 금지: Repository 구체 클래스 직접 타입힌트
필수: Repository 인터페이스를 통한 DI
필수: CoreServiceProvider에서 인터페이스-구현체 바인딩
기술 스택
백엔드
- PHP: 8.2+
- Laravel: 12.x
- 데이터베이스: MySQL 8.0
- 인증: Laravel Sanctum 4.x
- 테스트: PHPUnit 11.x
- 코드 스타일: Laravel Pint (PSR-12)
아키텍처 패턴
디렉토리 구조 개요
/
├── /app # 코어 애플리케이션
├── /modules # 모듈 디렉토리
│ ├── _bundled/ # 선탑재 확장 소스 (Git 추적)
│ ├── _pending/ # 외부 다운로드 대기소 (Git 제외)
│ └── vendor-module/ # 활성 설치 디렉토리 (Git 제외)
├── /plugins # 플러그인 디렉토리 (동일 구조)
├── /templates # 템플릿 디렉토리 (동일 구조)
├── /resources/js/core/ # 코어 렌더링 엔진
└── /public/build/ # Vite 빌드 결과
네이밍 규칙
| 항목 | 디렉토리명 | 네임스페이스 |
|---|---|---|
| 모듈 | sirsoft-ecommerce |
Modules\Sirsoft\Ecommerce\ |
| 플러그인 | sirsoft-payment |
Plugins\Sirsoft\Payment\ |
| 템플릿 | sirsoft-admin_basic |
- |
백엔드 개발 - 핵심 요약
절대 금지: Service 클래스에 검증 로직 구현 → FormRequest + Custom Rule 사용
절대 금지: FormRequest authorize()에서 인증/권한 로직 → permission 미들웨어 사용
필수: __() 함수를 사용한 다국어 처리
필수: 상태/타입/분류는 Enum으로 정의
절대 금지: 인증 필요 미들웨어를 append()로 전역 등록 → appendToGroup('api') 사용
절대 금지: DB CASCADE에 의존한 삭제 → Service에서 명시적 삭제 (훅/파일/로깅 보장)
절대 금지: 로케일 하드코딩 → config('app.supported_locales') 사용
필수: 마이그레이션 한국어 comment 필수, down() 구현 필수
주의: ResponseHelper::success($messageKey, $data) — 메시지가 첫 번째 인수
상세 규칙 (API 리소스, ServiceProvider, validation, 인증, 활동 로그 등): docs/backend/ 각 문서 참조
컨트롤러 계층
BaseApiController (최상위)
├── AdminBaseController (관리자 전용)
├── AuthBaseController (인증된 사용자)
└── PublicBaseController (공개 API)
파사드 사용
✅ use Illuminate\Support\Facades\Log; → Log::info()
❌ \Log::info(), auth()->user() 금지
프론트엔드/템플릿 시스템
상세: docs/frontend/
필수: 기본 컴포넌트만 사용 (Div, Button, H2 등 — HTML 태그 직접 사용 금지)
필수: 집합 컴포넌트 재사용 우선
필수: 다크 모드 light/dark variant 함께 지정
필수: HtmlEditor 사용 (RichTextEditor 미구현)
확장 시스템 빠른 참조
상세: docs/extension/
필수: 모든 확장 작업은 _bundled 디렉토리에서만 수행 (활성 디렉토리 직접 수정 금지)
필수: 프로덕션 반영은 update 커맨드로만 수행 (_bundled → 활성 디렉토리)
필수: 확장 코드 변경 시 manifest 버전 업 (미변경 시 업데이트 감지 불가)
필수: 버전 업 시 CHANGELOG.md 기록 — Keep a Changelog 표준 (미기록 시 버전 업 불가)
필수: StorageInterface 사용 (Storage::disk() 직접 호출 금지)
필수: 확장 미들웨어는 getMiddleware() 로 부착 대상(targets) 명시 선언 (self-gate) — SP Kernel 미들웨어 그룹 직접 조작·라우트 파일 자기 미들웨어 FQCN 부착 금지, 무규율 전역 개입 금지
필수: 코어 레이아웃에 모듈 UI 주입은 layout_extensions만 사용
필수: 모든 확장 작업은 Artisan 커맨드로 수행
상세 규칙 (플러그인 의존성, 훅 시스템, 버전 동기화, 업그레이드 스텝 등): docs/extension/ 각 문서 참조
확장 타입 요약
| 타입 | 네이밍 | 네임스페이스 | 예시 |
|---|---|---|---|
| 모듈 | vendor-module | Modules\Vendor\Module\ | sirsoft-ecommerce |
| 플러그인 | vendor-plugin | Plugins\Vendor\Plugin\ | sirsoft-payment |
| 템플릿 | vendor-template | - | sirsoft-admin_basic |
한국어 사용 규칙
한국어: 사용자 대상 텍스트, 주석, 문서, 커밋 메시지, DB comment
영어: 변수명, 함수명, 클래스명
Laravel 기본 메서드 주석은 영어 유지 (up(), down() 등)
코드 품질
Laravel Pint
vendor/bin/pint --dirty
PHPDoc
/**
* 상품을 생성합니다.
*
* @param array $data 상품 생성 데이터
* @return Product 생성된 상품 모델
* @throws \Exception 생성 실패 시
*/
public function createProduct(array $data): Product
빌드 vs 확장 업데이트
| 수정 파일 유형 | 필요한 작업 |
|---|---|
*.json (레이아웃만) |
{type}:update {id} --force 실행 |
*.tsx, *.ts + *.json |
{type}:build + {type}:update {id} --force |
*.tsx, *.ts만 |
{type}:build + {type}:update {id} --force |
lang-packs/_bundled/** (번들 언어팩 콘텐츠/버전) |
language-pack:update {id} --force (빌드 불필요) |
# 확장 업데이트 (_bundled → 활성 반영)
php artisan template:update sirsoft-admin_basic --force
php artisan module:update sirsoft-ecommerce --force
php artisan plugin:update sirsoft-payment --force
php artisan language-pack:update g7-core-ja --force
번들 언어팩도 _bundled 는 배포 원본일 뿐이다. 설치본(lang-packs/{id}/)을 갱신하지 않으면 새로 추가한 번역 키가 런타임에 존재하지 않아 해당 로케일이 조용히 기준 로케일로 폴백한다.
코어 3-번들 구조 + 공유 런타임 (engine-v1.51.0+)
core:build 는 코어 프론트엔드를 3개 IIFE 번들로 빌드한다:
| 번들 | 로드 시점 | vite config |
|---|---|---|
template-engine.min.js |
모든 페이지 (동기 <script>) |
vite.config.core.js |
layout-editor.min.js |
/admin/layout-editor/* 진입 시 런타임 주입 |
vite.config.editor.js |
devtools.min.js |
디버그 모드에서만 런타임 주입 | vite.config.devtools.js |
lazy 번들(편집기/devtools)이 코어 런타임(DynamicRenderer·엔진 싱글톤·React Context·DevTools 코어)을 재사용할 때는 재번들하지 않고 window.G7Core.__runtime 을 빌려 쓴다 — React/컨텍스트/싱글톤 인스턴스 동일성이 강제되기 때문(사본이 둘이면 "Invalid hook call"·컨텍스트 미매칭). 메인 번들이 G7CoreGlobals 에서 공유 대상을 G7Core.__runtime 에 노출하고, lazy 번들 vite config 는 React 4종(react/react-dom/react-dom/client/react/jsx-runtime)을 external→window 로, 코어 런타임 모듈을 resolveId 플러그인으로 __runtime-shims/ 로 치환한다.
확장 번들 병합 (서버측 concat)
활성 모듈/플러그인의 프론트엔드 IIFE JS·CSS 는 타입별로 서버에서 하나의 번들로 병합해 서빙한다(/api/{modules,plugins}/bundle.{js,css}?v={version}). ExtensionBundleService 가 정렬·필터·concat·캐시를 전담하고, 프론트는 window.G7Config.bundleUrls 를 읽어 모듈 번들 → 플러그인 번들 순으로 로드한다. 병합 규율:
- priority 순서는 선언형 — 실행 순서는 오직 manifest
loading.priority오름차순(uasort). 특정 확장 이름을 지목하는 분기를 두지 않는다. - IIFE 사이는
\n;\n(JS)/\n(CSS) 로 잇는다. 미사용 시 ASI 경계가 깨져 번들 전체 파싱 에러가 난다. - 소스맵은 prod strip, dev 는 개별 에셋 서빙 절대 URL 로 rewrite. 개별 에셋 서빙 라우트(
*.map포함)는 존치한다. - 번들 URL 은 반드시 same-origin(
/api/...). 외부 origin/CDN·protocol-relative 는 gdpr preblocker 에 자기 차단된다. - 확장 에셋 절대경로는
getBuiltAssetAbsolutePaths()(=getModulePath()/getPluginPath()) 만 쓴다.base_path("modules"|"plugins")직접 조립은_bundled경로 오해석 → 빈 번들. - concat 루프는 확장별 try/catch — 실패 확장만 skip 하고 나머지 병합을 지속한다.
- 번들 파일명에 확장 캐시 버전을 포함(
{type}.{version}.{js,css}). 조합 변경 시 version bump → 새 파일명 → 자동 재생성. 구파일 GC 는ext-bundles:cleanup+{module,plugin,template}:cache-clear가 담당한다. prod 은 version-in-path 디스크 캐시, 비프로덕션은 매 요청 concat.
빌드 명령어 (Artisan)
# 코어 템플릿 엔진 (resources/js/core/template-engine/**)
php artisan core:build # 기본: 템플릿 엔진만 빌드
php artisan core:build --full # 전체 빌드 (npm run build)
php artisan core:build --watch # 파일 감시 모드
# 모듈 빌드 (기본: _bundled 디렉토리)
php artisan module:build sirsoft-ecommerce # _bundled에서 빌드
php artisan module:build --all # 모든 _bundled 모듈 빌드
php artisan module:build sirsoft-ecommerce --watch # 활성 디렉토리에서 watch
php artisan module:build sirsoft-ecommerce --active # 활성 디렉토리에서 빌드
# 템플릿 빌드 (기본: _bundled 디렉토리)
php artisan template:build sirsoft-admin_basic # _bundled에서 빌드
php artisan template:build --all # 모든 _bundled 템플릿 빌드
php artisan template:build sirsoft-admin_basic --watch # 활성 디렉토리에서 watch
php artisan template:build sirsoft-admin_basic --active # 활성 디렉토리에서 빌드
# 플러그인 빌드 (기본: _bundled 디렉토리)
php artisan plugin:build sirsoft-payment # _bundled에서 빌드
php artisan plugin:build --all # 모든 _bundled 플러그인 빌드
php artisan plugin:build sirsoft-payment --watch # 활성 디렉토리에서 watch
php artisan plugin:build sirsoft-payment --active # 활성 디렉토리에서 빌드
빌드 원칙: 기본값은
_bundled디렉토리. 빌드 결과물은 빌드 경로 내에만 남음. 활성 디렉토리 반영은update커맨드로만 수행.--watch모드는 실시간 개발용으로 활성 디렉토리를 자동 사용.
확장 시스템 Artisan 명령어
# 코어 업데이트
php artisan core:check-updates # 코어 업데이트 확인
php artisan core:update [--force] [--no-backup] [--no-maintenance] # 코어 업데이트 실행
php artisan core:execute-upgrade-steps --from=X.Y.Z --to=A.B.C [--force] # 업그레이드 스텝 단독 실행 (HANDOFF 안내/수동 복구용 — 사전·사후 단계 자동 수행)
# 모듈
php artisan module:list
php artisan module:install [identifier]
php artisan module:activate [identifier]
php artisan module:deactivate [identifier]
php artisan module:uninstall [identifier]
php artisan module:composer-install [identifier?] [--all]
php artisan module:cache-clear [identifier?]
php artisan module:seed [identifier] [--sample] [--count=key=value]
php artisan module:check-updates [identifier?]
php artisan module:update [identifier] [--force] [--source=auto|bundled|github]
# 플러그인
php artisan plugin:list
php artisan plugin:install [identifier]
php artisan plugin:activate [identifier]
php artisan plugin:deactivate [identifier]
php artisan plugin:uninstall [identifier]
php artisan plugin:composer-install [identifier?] [--all]
php artisan plugin:cache-clear [identifier?]
php artisan plugin:seed [identifier] [--sample] [--count=key=value]
php artisan plugin:check-updates [identifier?]
php artisan plugin:update [identifier] [--force] [--source=auto|bundled|github]
# 템플릿
php artisan template:list
php artisan template:install [identifier]
php artisan template:activate [identifier]
php artisan template:deactivate [identifier]
php artisan template:uninstall [identifier]
php artisan template:cache-clear
php artisan template:check-updates [identifier?]
php artisan template:update [identifier] [--layout-strategy=overwrite] [--force] [--source=auto|bundled|github]
# Composer 의존성 (모듈/플러그인별 독립 vendor/)
php artisan extension:composer-install
# 오토로드
php artisan extension:update-autoload
SEO Artisan 커맨드
php artisan seo:warmup [--layout=]
php artisan seo:clear [--layout=]
php artisan seo:stats
php artisan seo:generate-sitemap [--sync]
코드 스타일/마이그레이션 명령어
# 코드 스타일 (Laravel Pint)
vendor/bin/pint --dirty
# 마이그레이션
php artisan make:migration create_[table]_table
php artisan migrate
php artisan migrate:rollback
파일 유형별 규정 확인
파일 수정 전 해당 규정 파일을 먼저 확인합니다:
| 수정 대상 파일 패턴 | 작업 전 필수 참조 |
|---|---|
app/Http/Controllers/** |
controllers.md, api-documentation.md |
app/Services/** |
service-repository.md |
app/Http/Requests/** |
validation.md |
app/Repositories/** |
service-repository.md |
app/Http/Resources/** |
api-resources.md |
app/**/DTO/**, modules/**/src/DTO/**, plugins/**/src/DTO/** |
dto.md |
database/migrations/** |
database-guide.md |
database/seeders/** |
database-guide.md |
resources/layouts/**/*.json |
layout-json.md |
templates/**/layouts/**/*.json |
layout-json.md |
templates/**/src/components/**/*.tsx |
components.md |
modules/**/Listeners/** |
hooks.md |
plugins/**/Listeners/** |
hooks.md |
lang/{ko,en}/**/*.php |
database-guide.md (다국어 섹션) — 코어 백엔드 다국어 |
lang/{ko,en}.json, lang/partial/{ko,en}/** |
data-binding-i18n.md — 코어 프론트엔드 다국어 ($t:core.*) |
lang/** |
database-guide.md (다국어 섹션) |
routes/** |
routing.md |
app/Seo/** |
seo-system.md |
참고 파일 위치
- AbstractModule:
app/Extension/AbstractModule.php - HookManager:
app/Extension/HookManager.php - ModuleManager:
app/Extension/ModuleManager.php - PluginManager:
app/Extension/PluginManager.php - TemplateManager:
app/Extension/TemplateManager.php - CoreStorageDriver:
app/Extension/Storage/CoreStorageDriver.php - ResponseHelper:
app/Helpers/ResponseHelper.php - ExtensionStatusGuard:
app/Extension/Helpers/ExtensionStatusGuard.php - ExtensionBackupHelper:
app/Extension/Helpers/ExtensionBackupHelper.php - ExtensionPendingHelper:
app/Extension/Helpers/ExtensionPendingHelper.php - ExtensionRoleSyncHelper:
app/Extension/Helpers/ExtensionRoleSyncHelper.php - ExtensionMenuSyncHelper:
app/Extension/Helpers/ExtensionMenuSyncHelper.php - SettingsMigrator:
app/Extension/Helpers/SettingsMigrator.php - UpgradeStepInterface:
app/Contracts/Extension/UpgradeStepInterface.php - UpgradeContext:
app/Extension/UpgradeContext.php - SeoRenderer:
app/Seo/SeoRenderer.php - SeoMiddleware:
app/Seo/SeoMiddleware.php - SeoCacheManager:
app/Seo/SeoCacheManager.php - SeoServiceProvider:
app/Seo/SeoServiceProvider.php - SitemapContributorInterface:
app/Seo/Contracts/SitemapContributorInterface.php - SitemapGenerator:
app/Seo/SitemapGenerator.php - ActivityLogChannel:
app/ActivityLog/ActivityLogChannel.php - ActivityLogHandler:
app/ActivityLog/ActivityLogHandler.php - ActivityLogProcessor:
app/ActivityLog/ActivityLogProcessor.php - ResolvesActivityLogType:
app/ActivityLog/Traits/ResolvesActivityLogType.php - ChangeDetector:
app/ActivityLog/ChangeDetector.php - CoreActivityLogListener:
app/Listeners/CoreActivityLogListener.php