예제
첫 App 만들기
App과 Resource를 생성해 한 테넌트에 설치하고 터미널과 브라우저 결과를 확인합니다.
예제 유형 레시피
개요
첫 App 예제
이 예제는 빈 로컬 환경에서 Workshop App과 Note Resource를 만들고 활성화한
뒤, 테넌트에 설치해 브라우저에서 여는 전체 흐름을 다룹니다. 각 명령이 무엇을
만들고 어디서 성공을 확인하는지에 집중하며, Resource가 추가하는 실제 업무
화면의 변화는 Resource 생성하기 예제에서 단계별로 확인합니다.
최종 결과
| 단계 | 생기는 것 | 확인 위치 |
|---|---|---|
| App 생성 | packages/workshop/ 아래 패키지 메타데이터, Manifest, Service Provider, Overview, Route, 로케일, 패키지 문서·테스트 기준선 | App generator의 CREATE 출력과 패키지 파일 |
| Resource 생성 | Note 모델·Policy·Controller, wsp_notes 마이그레이션, Resource Module, Filament Resource, list·form·show·inspector 프런트엔드 화면 | Resource generator의 CREATE·UPDATE 출력 |
| 호스트 활성화 | 로컬 Composer 패키지 등록, root pnpm-lock.yaml workspace 항목, Installed App map, descriptor·번역·권한 동기화 | Package app activation complete.와 package doctor |
| 테넌트 설치 | 선택한 테넌트의 wsp_notes 테이블과 활성 App 설치 상태 | 개발 설치 계획의 install |
| 화면 확인 | App 런처의 Workshop, App Menu의 노트, 생성·목록·상세 화면 | 실습 테넌트 브라우저 |
App 기준선만 생성한 시점에는 모델, 마이그레이션, 업무 화면이 없습니다. Note
Resource를 추가해도 아직 어떤 테넌트에도 보이지 않습니다. 호스트 활성화는 코드를
로드 대상으로 만들고, 테넌트 설치는 그 코드를 선택한 테넌트에서 켭니다.
App 클래스와 Core 등록 흐름
Generator의 중심은 파일 수가 아니라 WorkshopServiceProvider와
WorkshopAppManifest가 맺는 등록 계약입니다.
| Workshop 클래스 | 상속·구현 계약 | 담당하는 일 |
|---|---|---|
WorkshopServiceProvider | Laravel ServiceProvider 상속 | composer.json의 App metadata를 읽고 Manifest를 Core의 AppRegistrar에 전달 |
WorkshopAppManifest | SDK AbstractPackageAppManifest 상속, NavigationContribution 구현 | 안정적인 App identity, contribution discovery root, migration·Filament Resource, Overview surface와 App Launcher 항목 공개 |
Composer가 WorkshopServiceProvider 로드
→ provider가 AppPackageMetadataReader로 AppDefinition 생성
→ WorkshopAppManifest(AppDefinition) 생성
→ AppRegistrar::register(manifest)
→ AppRegistry가 App을 등록하고 contributionLocations()를
NexiaContributionRegistry에 전달
→ Core catalog가 App Launcher, Overview, migration과 이후 Resource 기여를 소비
→ tenant 설치 상태가 해당 tenant에서의 가시성과 실행 여부를 결정
AbstractPackageAppManifest는 id(), name(), definition()처럼 package
metadata에서 결정되는 부분을 고정합니다. Workshop은 그 위에서 discovery 위치와
화면 entry를 추가합니다. 따라서 App 폴더를 복사한 것만으로 Core에 등록되는 것이
아니라, Composer가 Service Provider를 로드하고 그 Provider가 Manifest를
AppRegistrar에 넘겨야 합니다.
실행 전 확인
- Core의
app,vite,postgres,redis가 실행 중이어야 합니다. - Core는
APP_ENV=local로 실행해야 합니다. 이 예제는 로컬 전용 개발 설치 명령을 사용합니다. - App이 아직 설치되지 않은 실습 테넌트가 있어야 합니다. 이 레시피는 테넌트를 새로 만들지 않습니다.
- Core 저장소 루트에서 실행해야 합니다.
packages/workshop이 없어야 생성기의 전체CREATE출력을 볼 수 있습니다.
테넌트 ID를 확인합니다.
task artisan -- tenants:list --no-ansi출력에서 실습 도메인 옆의 ID를 복사합니다. 예를 들어 다음 출력이라면 사용할 ID는
abc123def456입니다.
id: abc123def456 ................................................... acme
생성·활성화·설치 실행
TENANT에 방금 확인한 ID를 넣어 실행합니다.
TENANT=abc123def456 sh docs/developers/examples/first-app/commands.sh스크립트는 다음 순서로 움직입니다.
- App 생성을
--dry-run으로 미리 보고 실제 생성합니다. packages/workshop을 독립 Git 저장소로 초기화합니다.NoteResource를--dry-run으로 미리 보고 실제 생성합니다.- 새 로케일 파일을 읽도록 컴파일된 번역 캐시를 비웁니다.
- App을 호스트에 활성화합니다.
- 로컬 개발 설치 계획을 dry-run으로 확인한 뒤 선택한 테넌트에 마이그레이션·초기화·설치를 적용합니다.
- 새 PHP·프런트엔드 엔트리를 읽도록 App과 Vite를 재시작합니다.
터미널 출력 읽기
App dry-run에는 현재 기준선 19개 경로가 WOULD로 보입니다. 실제 생성은
같은 위치를 CREATE로 표시하고 Package app scaffolded.로 끝납니다.
Resource dry-run에는 새 Resource 파일 20개와 App 로컬 Route·프런트엔드 등록·로케일 변경이 보입니다. 실제 생성 후에는 아래 migration처럼 실행 시각이 붙은 파일이 생깁니다.
packages/workshop/database/migrations/tenant/
2026_.._.._......_create_wsp_notes_table.php
활성화가 끝나면 다음 문장이 보입니다.
Package app activation complete.
설치 dry-run 표는 새 테넌트에서 한 행이어야 합니다.
+----------+----------+---------+
| App key | App name | Action |
+----------+----------+---------+
| workshop | Workshop | install |
+----------+----------+---------+
already-active가 보이면 그 테넌트에는 이미 Workshop이 설치된 것입니다. 처음 설치되는
모습을 보려면 빈 테넌트를 선택하세요.
생성된 파일 확인
다음 경로가 역할을 나눠 가집니다.
packages/workshop/
composer.json # App identity와 Composer 패키지
src/WorkshopAppManifest.php # 호스트에 공개하는 App contribution
src/WorkshopServiceProvider.php # Route와 로케일 등록
src/Contribution/Resources/NoteModule.php
src/Models/Note.php
src/Http/Controllers/NoteController.php
src/Policies/NotePolicy.php
database/migrations/tenant/*_create_wsp_notes_table.php
resources/js/overview/WorkshopOverviewSurface.tsx
resources/js/resources/notes/ # list·form·show·inspector
resources/lang/{en,ko,zh}.json
tests/Feature/NoteAuthorizationTest.php
vendor/amuzcorp/nexia-workshop은 Composer 설치 뷰입니다. 생성물을 고칠 때는
항상 packages/workshop을 편집합니다.
App과 로컬 Composer overlay는 ignore되지만, 활성화가 새 pnpm workspace importer를
추가하면 Core의 root pnpm-lock.yaml은 변경됩니다. 실습 변경을 제품 lock 변경처럼
커밋하지 말고, 실제 App을 추가하는 작업에서는 Composer graph와 함께 의도적으로
검토합니다.
브라우저에서 확인
실습 테넌트를 새로고침하고 다음 순서로 엽니다.
- 왼쪽 App 런처의 앱을 엽니다.
- 기타 앱 → Workshop을 선택합니다.
- App Menu의 노트를 선택합니다.
- 빈 목록에서 새 노트를 누르고 이름을
설치 확인 노트로 저장합니다.
성공하면 상세 화면에 이름 설치 확인 노트와 상태 초안이 보이고, 목록은
총 1개가 됩니다. Workshop은 TENANT로 선택한 테넌트에만 나타납니다.
터미널에서 검증
같은 테넌트 ID로 검증 스크립트를 실행합니다.
TENANT=abc123def456 sh docs/developers/examples/first-app/verify.sh검증 스크립트는 패키지와 Resource의 대표 파일, 영구 identity, 독립 Git 경계, Core의 ignore 경계, Composer 설치 symlink를 확인합니다. 이어 package doctor를 실행하고, 선택한 테넌트의 설치 계획이 오류 없이 계산되는지 dry-run으로 확인합니다. 마지막에 아래 문장이 나오면 터미널 검증이 끝난 것입니다.
First App lifecycle verified. Confirm the Note screen in the browser.
자주 발생하는 오류
| 증상 | 원인 | 해결 | 다시 확인 |
|---|---|---|---|
Practice package already exists: packages/workshop | 레시피는 기존 실습 패키지를 덮어쓰지 않음 | 깨끗한 실습 checkout에서 시작하거나 기존 packages/workshop을 그대로 점검 | test ! -e packages/workshop이 성공한 뒤 commands.sh 재실행 |
requires translation key [workshop.note.list.title] | Workshop 등록 전에 만들어진 번역 catalog cache가 새 키를 포함하지 않음 | 번역 catalog cache를 지우고 App을 다시 활성화 | 활성화 출력에서 translation catalogs validated와 Package app activation complete. 확인 |
| 설치 명령은 성공했지만 런처에 Workshop이 없음 | 브라우저에서 연 tenant가 TENANT와 다르거나 장기 실행 중인 PHP·Vite process가 새 패키지 이전 상태를 유지함 | 같은 tenant를 열고 task app:reload 후 Vite를 재시작 | 기타 앱 → Workshop → 노트가 나타나는지 확인 |
새 번역 키를 찾지 못할 때
다음처럼 생성된 키가 없다는 오류가 보일 수 있습니다.
requires translation key [workshop.note.list.title]
파일이 실제로 resources/lang/{en,ko,zh}.json에 있다면, 새 패키지를 등록하기 전에
만들어진 컴파일 번역 캐시가 원인입니다. 레시피 스크립트는 활성화 전에 이 캐시를
미리 비웁니다. 활성화를 따로 실행했다면 다음처럼 복구합니다.
task artisan -- nexia-runtime:clear-translation-catalog-cache --no-ansi
task artisan -- nexia-apps:activate-package-app workshop재실행 후 translation catalogs validated와
Package app activation complete.가 함께 보여야 합니다.
아직 결정하지 않은 것
Generator 결과는 실행 가능한 기준선이지 완성된 업무 App이 아닙니다. Note에는
예제용 이름과 상태만 있고, 삭제 Policy는 기본적으로 거부합니다. 실제 App에서는
도메인 필드, 상태 전이, 삭제·보존 정책, 권한 범위, 검색·감사 요구사항을 먼저
결정한 뒤 생성된 파일을 수정해야 합니다.
포함된 파일
생성·활성화·설치
docs/developers/examples/first-app/commands.sh#!/usr/bin/env sh
set -eu
: "${TENANT:?Set TENANT to the practice tenant ID shown by tenants:list.}"
app_root='packages/workshop'
test ! -e "$app_root" || {
printf 'Practice package already exists: %s\n' "$app_root" >&2
printf '%s\n' 'Use a clean practice checkout or inspect the existing package instead.' >&2
exit 1
}
task artisan -- nexia-apps:make-package-app Workshop \
--family=other \
--key=workshop \
--table-prefix=wsp \
--icon=box \
--sort=990 \
--dry-run
task artisan -- nexia-apps:make-package-app Workshop \
--family=other \
--key=workshop \
--table-prefix=wsp \
--icon=box \
--sort=990
git -C "$app_root" init -b feat/workshop-app
task artisan -- nexia-apps:make-package-resource workshop Note \
--record-owner=legal_entity \
--label-ko=노트 \
--dry-run
task artisan -- nexia-apps:make-package-resource workshop Note \
--record-owner=legal_entity \
--label-ko=노트
# A compiled catalog created before this package cannot contain its new keys.
task artisan -- nexia-runtime:clear-translation-catalog-cache --no-ansi
task artisan -- nexia-apps:activate-package-app workshop
# A new local Workshop is unpublished; development installation migrates and
# initializes it for this explicit practice tenant.
task artisan -- nexia-apps:install-dev-app workshop --tenant="$TENANT" --dry-run
task artisan -- nexia-apps:install-dev-app workshop --tenant="$TENANT" --with-prerequisites
# Long-running PHP and Vite processes started before the new package existed.
task app:reload
docker compose restart vite
docker compose up --wait --no-deps vite
printf '%s\n' 'First App installed. Reload the practice tenant and open Apps > Workshop > Note.'App 라이프사이클 검증
docs/developers/examples/first-app/verify.sh#!/usr/bin/env sh
set -eu
: "${TENANT:?Set TENANT to the same practice tenant ID used during installation.}"
app_root='packages/workshop'
for required in \
"$app_root/composer.json" \
"$app_root/src/WorkshopAppManifest.php" \
"$app_root/src/WorkshopServiceProvider.php" \
"$app_root/src/Contribution/Resources/NoteModule.php" \
"$app_root/src/Models/Note.php" \
"$app_root/src/Http/Controllers/NoteController.php" \
"$app_root/src/Policies/NotePolicy.php" \
"$app_root/resources/js/index.ts" \
"$app_root/resources/js/resources/notes/surface/NoteListSurface.tsx" \
"$app_root/resources/js/resources/notes/surface/NoteFormSurface.tsx" \
"$app_root/resources/js/resources/notes/surface/NoteShowSurface.tsx" \
"$app_root/resources/lang/en.json" \
"$app_root/resources/lang/ko.json" \
"$app_root/resources/lang/zh.json" \
"$app_root/tests/Feature/NoteAuthorizationTest.php" \
"$app_root/tests/Pest.php" \
"$app_root/docs/README.md"
do
test -f "$required" || {
printf 'Missing generated App or Resource file: %s\n' "$required" >&2
exit 1
}
done
set -- "$app_root"/database/migrations/tenant/*_create_wsp_notes_table.php
test "$#" -eq 1 && test -f "$1" || {
printf '%s\n' 'Expected exactly one wsp_notes tenant migration.' >&2
exit 1
}
grep -Fq '"app_key": "workshop"' "$app_root/composer.json"
grep -Fq '"app_table_prefix": "wsp"' "$app_root/composer.json"
test "$(git -C "$app_root" rev-parse --show-toplevel)" = "$(cd "$app_root" && pwd)" || {
printf '%s\n' 'The generated App Package is not an independent Git repository.' >&2
exit 1
}
git check-ignore -q "$app_root" || {
printf '%s\n' 'Core does not ignore the generated App Package.' >&2
exit 1
}
test -L vendor/amuzcorp/nexia-workshop || {
printf '%s\n' 'The Composer installation view is not linked to the Workshop source.' >&2
exit 1
}
task artisan -- nexia-apps:doctor-package-app workshop --no-ansi
task artisan -- nexia-apps:install-dev-app workshop --tenant="$TENANT" --dry-run
printf '%s\n' 'First App lifecycle verified. Confirm the Note screen in the browser.'