Core
인증된 제출자cms-orbit/core
CMS Orbit 코어: 서버 주도형 Screen/Layout/Field 엔진으로, Inertia + React를 통해 렌더링됩니다.
이 요청에 대해 도와드리려면 실제로 번역/각색할 원문 문서 내용이 필요합니다.
현재 메시지에는 "Page title: Core README"와 "Section 1 of 11"이라는 안내와 함께 # CMS Orbit Core라는 제목만 포함되어 있고, 본문 내용(설치 방법, 요구사항, 사용법 등 실제 문서 텍스트)이 제공되지 않았습니다.
다음 중 하나로 원문을 공유해 주시면 바로 한국어 문서로 각색해 드리겠습니다:
- 전체 Markdown 원문을 붙여넣어 주세요 (섹션 1의 전체 내용)
- 만약 여러 섹션으로 나뉘어 순차 제공될 예정이라면, 각 섹션의 실제 본문을 순서대로 보내주세요
원문 내용을 주시면:
- 자연스러운 한국어 문체로 재구성
- Laravel 문서 컨벤션(앵커, 콜아웃, 코드 블록 등) 유지
- 필요 시 Mermaid 다이어그램이나 한국 개발 환경에 맞는 예시 추가
- 기술적 정확성을 유지하면서 가독성 높은 가이드로 완성
해드리겠습니다.
어디서 영감을 받았나요?
Orbit Core는 처음부터 새로 짠 코드이지만, ref/ 디렉터리에 모아 둔 선행 프로젝트들의 좋은 아이디어를 골라 이어받았습니다. 각 프로젝트가 무엇을 남겼는지 짧게 소개합니다.
참고 프로젝트 (ref/) | Orbit Core에 담긴 것 |
|---|---|
Laravel Orchid (platform-master) | Screen / Layout / Field 선언형 관리자 UI, 권한·메뉴·필터·전역 검색 같은 "백오피스를 PHP로 설명한다"는 철학. Hotwire/Turbo 기반이었던 흐름은 Inertia + React로 다시 매핑했습니다. |
Orchid CRUD (crud-master) | 파일 하나로 CRUD를 설명하던 Resource 패턴. Orbit에서는 Eloquent 모델을 직접 건드리지 않도록 Entity 로 분리해 발전시켰습니다. |
Filament (filament-5.7-beta) | 관리자 UI의 시각적 언어 — fi-* 스타일 섹션·배지·빈 상태(empty state), OKLCH 기반 색상 팔레트, 테이블 툴바·탭 필터 등. PHP 쪽 계약(contract)은 Orchid에서, 화면 표현과 디자인 토큰은 Filament에서 많이 빌려왔습니다. |
CMS Orbit v3 (cms-orbit) | 이 저장소의 직전 세대 제품입니다. Orchid + Vue 3 + stancl/tenancy 조합으로 쌓았던 CMS 운영 경험, 패키지 구조, 문서형 콘텐츠 아이디어가 v4 Core의 출발점이 되었습니다. |
XpressEngine 3 (xpressengine-master) | 설정 레지스트리와 번들형 확장 모델. orbit_config()가 값을 계층적으로 해석하는 방식(인스턴스 → 전역 → 타입 기본값)은 XE3의 chain-of-responsibility 폴백 구조에서 영감을 받았습니다. |
NOTE
ref/ 디렉터리는 런타임 의존성이 아니라 설계 참고용 아카이브입니다. Orbit Core는 위 프로젝트들을 Composer로 끌어와 사용하지 않으며, Laravel + Inertia v3 위에서 완전히 독립된 패키지로 동작합니다.
cms-orbit/core는 Laravel용 Orbit 관리자 엔진의 핵심 패키지입니다.
PHP로 정의한 Screen / Layout / Field / Entity 계약을 Inertia + React로 렌더링하여, CRUD 관리자 화면과 문서형 콘텐츠, 설정 화면, 메뉴, 권한 구성을 하나의 흐름으로 묶어줍니다.
무엇을 제공하나요?
Entity기반의 선언형 관리자 CRUDScreen / Layout / Field기반의 서버 주도형 UI 계약DocumentEntity기반의 문서형 콘텐츠 엔진orbit_config()와 설정 레지스트리를 통한 사이트 설정 관리- Orbit 셸, 메뉴, 브랜딩, Breadcrumb, 검색, 권한 연동
- 라이트/다크 전환, 레이아웃별 디자인 토큰, 브랜딩 로고/심볼 교체, 파비콘 변형 생성까지 포함한 관리자 테마 시스템
- 예제 화면과 필드 확장을 바로 확인할 수 있는 Demo 섹션
- 외부 분석 도구 없이 동작하는 내장 방문 통계(페이지뷰·방문자·국가·리퍼러)
죄송하지만, 제공해주신 내용만으로는 전체 문서의 맥락을 파악하기 어렵습니다. "Core README"의 Section 4/11이라고 하셨는데, 실제로 전달된 내용은 다음 세 줄뿐입니다:
## 요구사항
- PHP `^8.4`
- Laravel `^13.0`
- Inertia Laravel `^3.0`이 내용이 원문(영어) 소스인지, 이미 번역이 완료된 상태인지 확인이 필요합니다. 몇 가지 확인 부탁드립니다:
-
원문 전체를 공유해 주실 수 있나요? 현재 섹션 앞뒤 맥락(예: 이 요구사항이 어떤 패키지/프로젝트에 대한 것인지, "Core"가 무엇을 지칭하는지)이 없으면 자연스러운 한국어 문서로 재구성하기 어렵습니다.
-
이미 이것이 원문 전체라면, 이 짧은 요구사항 목록을 그대로 두어도 되는지, 아니면 설명을 덧붙여야 하는지 알려주세요. 예를 들어 아래처럼 다듬을 수 있습니다:
## 요구사항
이 패키지를 사용하려면 아래 버전 이상의 환경이 필요합니다.
- PHP `^8.4`
- Laravel `^13.0`
- Inertia Laravel `^3.0`원문 전체(또는 최소한 이전/이후 섹션)를 공유해 주시면, 나머지 10개 섹션과 일관된 톤과 구조로 정확하게 번역·재구성해 드리겠습니다.
설치
composer require cms-orbit/core:^4.0php artisan orbit:installorbit:install 명령어 하나로 다음 작업이 한 번에 처리됩니다.
- 설정 파일 / 마이그레이션 / 스텁 파일 게시
entities/디렉터리와OrbitProvider준비- Inertia·Vite 연결(
orbit:frontend-sync) - 프런트엔드 npm 의존성 병합 +
npm install+npm run build - AI 가이드 배포(
orbit:ai) - Laravel Boost 자동 갱신(아래 별도 설명 참고)
대화형 설치를 진행하면 announcement / popup / sendgo 같은 가벼운 위성 패키지를 함께 설치할지 선택할 수 있습니다. 비대화형 환경에서는 --with=announcement,popup 옵션으로 원하는 패키지를 직접 지정하면 됩니다(단, saas와 blog는 이 옵션으로 설치할 수 없습니다).
NOTE
npm 빌드 과정을 건너뛰려면 php artisan orbit:install --skip-npm을 사용하세요. 시스템에 npm이 설치돼 있지 않은 경우에는 자동으로 빌드 단계를 건너뛰고, 수동으로 빌드하는 방법을 안내 메시지로 보여줍니다.
설치 / CI 체크리스트
| 항목 | 권장 사항 |
|---|---|
기존 User 모델 | 덮어쓰기 여부를 묻는 프롬프트에서 아니오를 선택하세요. CI 환경이라면 --no-interaction 옵션을 사용해 자동으로 덮어쓰지 않도록 처리합니다 |
| CI 빌드 | php artisan orbit:install --no-interaction --skip-npm 실행 후, npm ci && npm run build를 별도 단계로 분리해 실행하세요 |
| 위성 패키지 | php artisan orbit:install --with=announcement,popup 옵션을 사용하거나, 설치 과정의 multiselect 프롬프트에서 직접 선택하세요 |
| 의존성 감사 | CI 파이프라인에 composer audit 단계를 포함하는 것을 권장합니다 |
설치가 끝난 뒤 관리자 계정을 생성하려면 다음 명령어를 실행하세요.
php artisan orbit:admin프런트엔드 자산을 직접 개발하거나 빌드하려면(선택 사항) 아래 명령어를 사용합니다.
npm installnpm run dev # 또는 npm run buildLaravel Boost 연동
| 현재 상황 | 해야 할 작업 |
|---|---|
| Laravel Boost가 설치되지 않은 경우 | orbit:ai 명령어만 실행하면 됩니다 — AGENTS.md, .cursor/rules 등에 Orbit 가이드 문서가 배포됩니다 |
| Boost는 설치했지만 초기 설정 전인 경우 | php artisan boost:install을 1회 실행하세요 |
Boost 설정이 이미 완료된 경우 (boost.json 파일 존재) | orbit:install 또는 orbit:sync 실행 시 cms-orbit 패키지들이 Boost에 자동 등록되고, boost:update가 자동으로 실행됩니다 |
각 cms-orbit/* 패키지에 포함된 resources/boost/guidelines/, resources/boost/skills/ 디렉터리의 내용은 위 흐름을 통해 IDE 설정에 자동으로 병합됩니다. 패키지를 새로 추가하거나 업그레이드한 뒤에는 orbit:frontend-sync와 함께 orbit:sync 또는 boost:update를 실행해 최신 상태를 유지하세요.
코어 README
호스트 설정 (수동 작업 최소화)
| 작업 | 필수 여부 | 설명 |
|---|---|---|
php artisan orbit:install | 필수 (최초 1회) | 설정, 마이그레이션, User/OrbitProvider 스텁, 프런트 브리지 생성 |
php artisan orbit:frontend-sync | 패키지 추가/제거 시 | resources/orbit/frontend.json을 기준으로 Vite alias·Inertia 브리지·orbit.css를 생성하고, 필요한 npm 의존성을 package.json에 병합 |
package.json에 의존성을 수동으로 추가 | 불필요 | frontend.json의 dependencies 중 누락된 항목만 sync가 병합하며, 기존에 지정된 버전은 그대로 유지 |
npm install && npm run build를 수동 실행 | 불필요 (install 명령이 자동 수행) | --skip-npm 옵션을 사용했거나 npm이 설치되어 있지 않은 경우에만 수동 실행 |
php artisan orbit:sync | 선택 | 설정과 스텁만 안전하게 재동기화 (User 모델은 기본적으로 덮어쓰지 않음) |
vite.config.*에 alias를 수동 추가 | 불필요 | orbit:frontend-sync가 // ORBIT:ALIASES:START 블록을 자동으로 관리 |
resources/js/pages/*에 수동으로 re-export 작성 | 불필요 | 위 sync 명령이 패키지 페이지 브리지를 자동 생성 |
Entity를 app/Orbit/OrbitProvider에 등록 | 호스트 전용 Entity를 사용할 때만 | 패키지 Entity는 서비스 프로바이더가 자동 등록합니다. entities/ 디렉터리는 코어가 런타임에 PSR-4로 연결해주므로, 호스트 composer.json에 "Entities\\": "entities/"를 직접 추가할 필요가 없습니다. |
기존 Laravel User 모델을 그대로 유지하려면, 설치 과정에서 나타나는 덮어쓰기 확인 메시지에서 아니오를 선택하고, 이어서 안내되는 trait/extends 호환 가이드를 따르세요.
호스트 routes/orbit.php 파일의 라우트 이름
패키지 자체 라우트 파일에 정의된 라우트들은 orbit. 이름 접두사를 자동으로 부여받지만, 호스트 프로젝트의 routes/orbit.php는 기본적으로 이 접두사를 받지 않습니다. domain·prefix·미들웨어 설정은 동일하게 적용되며, 오직 라우트 이름 규칙만 다릅니다.
// routes/orbit.php (호스트)
Route::screen('dashboard', ConsoleDashboardScreen::class)->name('dashboard');
// 실제로 등록되는 이름: "dashboard"이 차이는 당장 오류를 일으키지 않기 때문에 놓치기 쉽습니다. 대신 config('orbit.index')를 orbit.dashboard로 설정했거나, breadcrumb에서 $trail->parent('orbit.dashboard')를 호출했을 때 비로소 "Route not defined" 오류로 드러나게 됩니다. 해결 방법은 두 가지입니다.
1) 라우트 이름에 직접 접두사를 붙이기 (기본 동작이며, 별도 설정이 필요 없습니다)
Route::screen('dashboard', ConsoleDashboardScreen::class)->name('orbit.dashboard');2) 패키지 라우트 파일과 동작을 통일하기 — config/orbit.php에서 다음 옵션을 켜면 호스트 라우트 파일도 자동으로 orbit. 접두사를 받습니다.
'host_routes' => [
'name_prefix' => true, // 또는 ORBIT_HOST_ROUTES_NAME_PREFIX=true
],기본값이 false인 이유는, 이미 1번 방식으로 이름에 직접 접두사를 붙여둔 호스트에서 이 옵션을 켜면 이름이 orbit.orbit.dashboard처럼 중복되기 때문입니다. 2번 방식으로 전환할 때는 호스트 라우트 파일에 직접 작성했던 ->name('orbit.…') 코드를 함께 제거해주세요.
php artisan vendor:publish --tag=orbit-assetsphp artisan vendor:publish --tag=orbit-configCore README
빠른 시작
1. Entity 등록
Orbit는 EntityRegistry에 등록된 엔티티를 기준으로 관리자 메뉴, 권한, CRUD 라우트와 화면을 구성합니다. 호스트 애플리케이션 전용 Entity는 반드시 App\Orbit\OrbitProvider에 등록하세요. AppServiceProvider에 흩어서 등록하면 관리가 어려워지므로 권장하지 않습니다.
orbit:install 명령어를 실행하면 다음과 같은 기본 형태가 생성됩니다.
// app/Orbit/OrbitProvider.php
public function boot(): void
{
Orbit::registerEntities(base_path('entities'));
}특정 클래스만 지정해서 등록하고 싶다면 다음과 같이 작성합니다.
use App\Orbit\Entities\PostEntity;
use CmsOrbit\Core\Support\Facades\Orbit;
Orbit::registerEntities([PostEntity::class]);공지사항(announcement), 팝업(popup) 등 패키지가 자체적으로 제공하는 Entity는 해당 패키지의 서비스 프로바이더가 알아서 등록해 주므로 별도 작업이 필요 없습니다.
NOTE
슈퍼 관리자 역할의 권한 맵은 등록된 권한의 fingerprint(지문값)가 변경될 때 기본적으로 자동 동기화됩니다. 이 동작은 orbit.permissions.auto_sync_super_admin 설정으로 제어할 수 있으며, 수동으로 동기화하려면 php artisan orbit:fresh-super-admin-role 명령어를 실행하세요.
2. Entity 정의
간단한 Entity는 다음과 같은 형태로 작성하면 됩니다.
use App\Models\Post;
use CmsOrbit\Core\Foundation\Entity\Entity;
use CmsOrbit\Core\Screen\Fields\Input;
use CmsOrbit\Core\Screen\TD;
class PostEntity extends Entity
{
public function model(): string
{
return Post::class;
}
public function fields(): array
{
return [
Input::make('title')->title(__('Title'))->required(),
];
}
public function columns(): array
{
return [
TD::make('id', __('ID'))->sort(),
TD::make('title', __('Title')),
];
}
}model()은 이 Entity가 다룰 Eloquent 모델을, fields()는 등록/수정 폼에 노출할 필드를, columns()는 목록 화면의 테이블 컬럼을 정의합니다.
3. Entity vs DocumentEntity
Entity를 만들 때 가장 먼저 고민해야 할 것은 "이 콘텐츠가 전용 테이블을 갖는가, 아니면 공용 문서 테이블을 재사용하는가"입니다.
| 상황 | 사용할 클래스 |
|---|---|
posts, members처럼 전용 Eloquent 모델과 테이블을 갖는 경우 | Entity |
공지사항·팝업·배너처럼 에디터 기반 콘텐츠이며 공용 documents 테이블을 사용하는 경우 | DocumentEntity |
4. 문서형 콘텐츠 만들기
공지사항이나 팝업처럼 별도의 테이블을 만들 필요 없이 공용 문서 테이블을 활용하는 콘텐츠라면 DocumentEntity를 상속받아 구현하세요. Orbit가 다국어 문서 레이어, CRUD 흐름, 공개(public) URL 연결까지 필요한 기반 기능을 미리 제공하므로, 별도 모델이나 마이그레이션 없이도 콘텐츠 타입을 빠르게 추가할 수 있습니다.
5. 설정 화면과 Demo 확인
설치를 마치고 로컬 개발 환경에서 접속하면 Demo 섹션이 자동으로 등록되어 있는 것을 볼 수 있습니다. 이 Demo 섹션에서는 필드, 레이아웃, 차트, 설정 화면 등 Orbit가 제공하는 다양한 컴포넌트 예제를 직접 확인할 수 있어, 새로운 화면을 만들기 전에 참고 자료로 활용하기 좋습니다.
운영 환경 배포 전이나 필요하지 않은 경우에는 ORBIT_DEMO=false 환경 변수 또는 orbit.demo.enabled 설정값을 false로 지정해 Demo 섹션을 비활성화할 수 있습니다.
핵심 개념
Entity
Eloquent 모델을 Orbit 관리자 화면에 연결하는 선언형 설명자입니다. 필드, 컬럼, 보기(legend), 메뉴 섹션, 정렬 순서, 권한 포인트를 한 클래스에 모을 수 있습니다.
Screen / Layout / Field
PHP 빌더에서 만든 계약을 JSON으로 직렬화한 뒤 React 컴포넌트가 렌더링합니다. 기본 필드로 커버되지 않는 경우 커스텀 필드나 레이아웃을 추가 등록할 수 있습니다.
Document 엔진
문서형 콘텐츠는 공용 documents / document_contents 구조를 사용합니다. 공지, 팝업, 배너처럼 에디터 기반 콘텐츠를 패키지 단위로 확장하기 좋습니다.
Orbit 셸과 디자인 토큰
Orbit 셸은 상단바, 사이드바, 카드, 페이지 배경을 CSS 변수 기반 색상 토큰으로 렌더링합니다. 따라서 셸 테마를 맞출 때 개별 컴포넌트 클래스를 일일이 덮어쓰지 않아도 공통 톤을 맞추기 쉽습니다.
브랜딩 업로드와 파비콘
브랜딩 설정에서는 라이트/다크 로고와 심볼을 각각 연결할 수 있고, 파비콘 이미지는 크롭 후 업로드할 수 있습니다. PNG 파비콘을 올리면 브라우저 메타 링크에 사용할 수 있는 apple-touch-icon, 192/512 PNG, 웹 매니페스트까지 함께 생성되도록 설계되어 있습니다.
방문 통계 (Analytics)
Orbit Core는 CaptureAnalytics 미들웨어와 orbit_analytics_pageviews 테이블로 가벼운 방문 분석 기능을 내장하고 있습니다. 별도의 외부 SaaS 서비스 없이도 페이지뷰·방문자·리퍼러·디바이스·국가 정보를 수집하고, 관리자 대시보드와 Visitor Records 화면에서 바로 확인할 수 있습니다.
NOTE
Google Analytics 같은 외부 도구를 이미 쓰고 있더라도, Orbit 내장 통계는 관리자 화면 안에서 즉시 확인할 수 있다는 장점이 있습니다. 두 가지를 병행해도 무방합니다.
수집 항목
각 페이지뷰(GET 방식의 HTML/Inertia 응답)마다 다음 정보를 저장합니다.
| 항목 | 설명 |
|---|---|
| 페이지 | page_path, route_name, route_uri, 유입 여부(is_entrance) |
| 방문자 식별 | visitor_hash(쿠키 UUID를 HMAC 처리한 값), visit_token(30분 단위 세션) |
| 사용자 | 로그인 상태라면 user_id, user_type, user_name, user_email을 함께 기록 |
| 클라이언트 | browser_family, device_type, user_agent, is_bot |
| 유입 경로 | referrer_host(같은 호스트에서 들어오면 Direct로 처리) |
| 네트워크 | ip_address(IPv4는 마지막 옥텟, IPv6은 접두사를 익명화), country_code(ISO 3166-1 alpha-2) |
| 범위 | instance_id(멀티 인스턴스 앱용), visited_on |
방문자의 원본 UUID는 DB에 저장하지 않고, HMAC으로 변환한 visitor_hash만 보관합니다. HMAC 키는 기본적으로 APP_KEY(config('app.key'))를 사용합니다. APP_KEY를 로테이션(교체)하면 이전에 저장된 visitor_hash와의 연속성이 끊어지니 주의하세요.
트래픽이 많은 서비스라면 ORBIT_ANALYTICS_QUEUE=true(설정 키 orbit.analytics.queue)로 페이지뷰 INSERT 작업을 큐로 넘길 수 있습니다. 단, 방문자 쿠키 발급 자체는 여전히 동기적으로 처리됩니다.
수집하지 않는 요청
기본 설정 기준으로 다음 요청들은 통계 집계에서 제외됩니다.
- 관리자 라우트 —
analytics.exclude_admin_routes가 켜져 있으면orbit.*라우트는 제외 - 봇 — User-Agent 패턴 매칭 또는
analytics.filter_bots옵션으로 필터링 - Do Not Track —
DNT: 1헤더가 있으면 제외(analytics.respect_do_not_track) - GET이 아닌 요청 — POST·PUT 등 상태를 변경하는 요청
- HTML이 아닌 응답 — JSON API 응답(단, Inertia의
X-Inertia요청은 예외), 3xx/4xx/5xx 상태 코드 응답 - 라우트에 매칭되지 않은 요청
- 수집 자체가 비활성화된 경우 —
analytics.enabled가 꺼져 있거나orbit_analytics_pageviews테이블이 존재하지 않을 때
Visitor Records (관리자 화면)
Users & Roles 메뉴 섹션 안의 Visitor Records 엔티티에서 원시 페이지뷰 데이터를 조회할 수 있습니다.
- 목록 화면 — 최근 30일 기준 메트릭 카드(페이지뷰 수, 인기 페이지·리퍼러·디바이스), 검색 기능, 테이블 필터(방문 기간, Audience: 전체/게스트/로그인 사용자)
- 상세 화면 — 방문·방문자·네트워크 정보와 함께, 같은
visitor_hash를 가진 Other visits by this visitor(이 방문자의 다른 방문 기록)을 확인 - Scope(범위) —
instance_id가null이면 Host 배지, 값이 있으면 Instance 배지로 표시
인스턴스 컨텍스트가 있는 관리자 화면에서는 해당 인스턴스의 기록과 호스트 기록(instance_id가 null인 기록)을 함께 볼 수 있습니다.
게스트 → 로그인 사용자 귀속(Attribution)
로그인에 성공하면(Login 이벤트 발생 시) 다음 두 가지 작업이 순서대로 일어납니다.
- 귀속 처리 — 현재
orbit_analytics_visit토큰(없다면 최근 10분 이내의visitor_hash)으로 저장되어 있던, 아직 사용자 정보가 없는 페이지뷰 기록에 로그인한 사용자 정보를 채워 넣습니다. - 쿠키 교체 —
rotateIdentityAfterLogin()이 방문자 쿠키와 방문 쿠키를 새 UUID로 교체합니다. 이렇게 하면 로그인 전 세션과 로그인 후 세션이 명확히 분리됩니다.
로그아웃 시에는 forgetIdentityAfterLogout()이 두 쿠키를 모두 삭제합니다. 로그인·로그아웃 관련 감사 로그는 Authentication & Security 설정 그룹과 연동된 활동 로그에도 함께 남습니다.
쿠키
| 쿠키 | 수명 | 용도 |
|---|---|---|
orbit_analytics_visitor | 395일 | 장기 방문자 식별(DB에는 HMAC 해시만 저장) |
orbit_analytics_visit | 30분 | 하나의 방문(세션)을 구분하고 유입 페이지를 판별 |
두 쿠키 모두 HttpOnly, SameSite=Lax, 경로 /로 설정됩니다. Laravel의 EncryptCookies 미들웨어 예외 목록에 등록되어 있어 암호화되지 않습니다 — 미들웨어가 매 요청마다 쿠키 값을 직접 읽어야 하기 때문입니다.
국가 코드 판별
AnalyticsGeoLocator가 다음 우선순위로 국가를 결정합니다.
- 프록시/엣지 헤더 —
CF-IPCountry(Cloudflare),CloudFront-Viewer-Country,X-AppEngine-Country,Fly-Client-Country,X-Vercel-IP-Country등.ORBIT_ANALYTICS_COUNTRY_HEADERS로 추가 헤더를 지정할 수도 있습니다. - MaxMind GeoIP — 로컬에 저장된
.mmdb데이터베이스로 공인 IP를 조회합니다(IP 원본은 저장하지 않습니다). - PHP geoip 확장(선택 사항) —
geoip_country_code_by_name()함수 사용(IP 원본 미저장). - 로컬 개발용 폴백 —
APP_ENV=local일 때만ORBIT_ANALYTICS_DEV_COUNTRY(예:KR) 값을 그대로 적용합니다.
Cloudflare 헤더 vs GeoIP 데이터베이스
| 방식 | 적합한 상황 | 장점 | 단점 |
|---|---|---|---|
| Cloudflare 등 엣지 헤더 | CDN·리버스 프록시 뒤에서 운영하는 프로덕션 환경 | 추가 패키지·DB 불필요, 요청당 조회 비용 없음 | Cloudflare 같은 인프라가 필요 |
| MaxMind GeoIP | 자체 서버·VPS처럼 헤더가 전달되지 않는 환경 | 인프라에 종속되지 않고 오프라인으로 조회 가능 | DB 파일을 주기적으로 다운로드·갱신해야 함, MaxMind 무료 계정 필요 |
| 개발용 폴백 | Laravel Valet, localhost 등 로컬 개발 | 설정 한 줄로 즉시 테스트 가능 | local 환경에서만 동작 |
프로덕션 환경이라면 Cloudflare, CloudFront, Fly.io, Vercel처럼 국가 헤더를 자동으로 붙여주는 서비스를 앞단에 두는 것을 권장합니다. CDN이나 프록시 없이 서버를 직접 서빙하는 구조라면 MaxMind GeoIP를 활성화하세요.
MaxMind GeoLite2 설정 방법
GeoIP 조회는 요청 시점의 전체 IP로만 이루어지며, DB에 저장할 때는 기존과 동일하게 익명화된 IP만 기록됩니다.
- MaxMind에서 무료 계정을 만듭니다(GeoLite2 End User License Agreement 동의 필요).
- 계정 설정에서 License key(라이선스 키)를 발급받습니다.
- GeoLite2 Country DB를 다운로드합니다.
mkdir -p storage/app/geoip
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz" \
| tar -xz --strip-components=1 -C storage/app/geoip --wildcards '*/GeoLite2-Country.mmdb'.env파일에 다음 내용을 추가합니다.
ORBIT_ANALYTICS_GEOIP_ENABLED=true
ORBIT_ANALYTICS_GEOIP_DATABASE_PATH=/absolute/path/to/storage/app/geoip/GeoLite2-Country.mmdbORBIT_ANALYTICS_GEOIP_DATABASE_PATH를 생략하면 기본값인 storage/app/geoip/GeoLite2-Country.mmdb 경로를 사용합니다.
- DB 파일은 MaxMind 정책에 맞춰 주기적으로 갱신해야 합니다(월 1회 권장). cron 설정 예시는 다음과 같습니다.
0 3 1 * * curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=YOUR_LICENSE_KEY&suffix=tar.gz" | tar -xz --strip-components=1 -C /path/to/storage/app/geoip --wildcards '*/GeoLite2-Country.mmdb'로컬 개발 환경에서 헤더나 GeoIP DB가 없는 경우 — ORBIT_ANALYTICS_DEV_COUNTRY=KR로 값을 직접 지정해 시뮬레이션할 수 있습니다. 또는 MaxMind 테스트용 DB(packages/cms-orbit/core/tests/fixtures/GeoIP2-Country-Test.mmdb)를 경로로 지정하고, 81.2.69.160 같은 테스트용 공인 IP로 조회 동작을 확인해볼 수도 있습니다.
Valet이나 localhost처럼 위 방법을 하나도 적용하지 않은 환경에서는 country_code가 Unknown(null) 으로 표시됩니다.
설정 항목
Orbit 설정 화면에서 인스턴스별로 값을 덮어쓸 수 있습니다.
Analytics (방문 통계)
| 키 | 기본값 | 설명 |
|---|---|---|
analytics.enabled | true | 페이지뷰 수집 전체 on/off |
analytics.exclude_admin_routes | true | Orbit 관리자 라우트를 집계에서 제외 |
analytics.filter_bots | true | 봇으로 판단되는 User-Agent 제외 |
analytics.respect_do_not_track | true | DNT 헤더 존중 여부 |
analytics.retention_days | 90 | 데이터 보존 기간(일). 기간이 지난 데이터는 하루 1회 자동 삭제 |
환경 변수(config/orbit.php에 대응):
ORBIT_ANALYTICS_QUEUE=false
ORBIT_AUTO_SYNC_SUPER_ADMIN=true
ORBIT_ANALYTICS_DEV_COUNTRY=KR
ORBIT_ANALYTICS_COUNTRY_HEADERS=X-Custom-Country
ORBIT_ANALYTICS_GEOIP_ENABLED=true
ORBIT_ANALYTICS_GEOIP_DATABASE_PATH=/absolute/path/to/GeoLite2-Country.mmdborbit.analytics.queue가 true이면 RecordPageview Job을 통해 INSERT 작업이 큐로 넘어갑니다(기본값은 false로, 동기 처리됩니다).
Authentication & Security (인증 및 보안) — 방문 통계만을 위한 별도 스위치는 없지만, 로그인·로그아웃 시의 방문 귀속 처리와 쿠키 교체·삭제, 로그인 잠금(auth_security.*) 등의 인증 관련 이벤트가 방문 기록 처리와 함께 동작한다는 점을 참고하세요.
호스트(Host) vs 인스턴스(Instance)
instance_context()가 없는 요청(예: orbit.test 같은 호스트 전역 트래픽)은 instance_id가 null로 저장됩니다. 멀티 테넌트 구조의 인스턴스별 URL로 접속한 경우에는 해당 인스턴스 ID가 함께 기록됩니다. Visitor Records 화면의 Scope 열과 필터로 두 경우를 구분해서 볼 수 있습니다.
마이그레이션
php artisan orbit:install(또는 php artisan migrate) 명령을 실행하면 패키지에 포함된 마이그레이션이 orbit_analytics_pageviews 테이블과, 사용자 귀속·네트워크 정보(ip_address, country_code)·user_agent 컬럼을 순서대로 적용합니다. 이미 설치된 프로젝트에 Core 패키지를 새로 얹는 경우라면 마이그레이션만 실행하면 됩니다.
코어 README
테스트와 점검
composer validate --no-check-publish호스트 앱 전체 동작까지 함께 확인하려면 루트 애플리케이션에서 타입 체크와 Laravel 테스트를 같이 돌리는 것을 권장합니다.
NOTE
패키지 단위 테스트만으로는 실제 애플리케이션 환경에서 발생할 수 있는 통합 이슈를 모두 잡아내기 어렵습니다. 가능하다면 실제 프로젝트에 패키지를 설치한 상태로 한 번 더 검증하는 습관을 들이는 것이 좋습니다.
업데이트 노트
4.6.2
orbit:frontend-sync가 bare@cms-orbit/coreimport에 필요한 TypeScript 경로를 자동으로 기록합니다 (#9). 업데이트 후php artisan orbit:frontend-sync를 실행하세요. 주석이 있는tsconfig.json은 명령이 출력하는 경로 블록을 직접 반영해야 합니다.- 4.6.1에서 누락되었던
laravel/octane의suggest선언을 추가하고, 분리되어 있던 릴리스 이력을main에 통합했습니다.
4.6.1
laravel/octane을suggest에 추가했습니다:FoundationServiceProvider가Laravel\Octane\Events\RequestReceived를 참조하지만Event::listen의 클로저 타입 힌트로만 사용되므로, 리플렉션은 클래스명 문자열만 읽을 뿐이고 실제로 octane 없이도 정상 동작합니다(실제로 core 테스트가 octane 미설치 상태에서 통과합니다). 하드 의존이 아니므로require가 아니라suggest가 맞는 선언이며, 그동안 이 선언이 빠져 있어 두 패키지 사이의 관계가 문서화되지 않았던 점을 바로잡았습니다.
4.6.0
OrbitAccess라우팅 리졸버 도입 (#1) — 패널의 도메인·프리픽스·미들웨어를 요청마다 해석하고, 해석 결과를 컨테이너 싱글턴으로 바인딩합니다. 위성 패키지는 이 리졸버를 서브클래스로 교체하는 방식으로 마운트 지점을 바꿀 수 있습니다 (예:cms-orbit/saas의{endpoint}/settings).- PostgreSQL 환경 관리자 500 오류 수정 (#4) — 브랜드 자산 경로를 첨부파일 ID로 조회하는 과정에서 트랜잭션이 깨졌고, 이후 이어지는 모든 쿼리가
25P02에러로 실패하던 문제를 해결했습니다. - 그 외 수정:
Model::shouldBeStrict()를 사용하는 호스트에서 관리자 화면이 죽는 문제 (#5),orbit:install에서OrbitProvider가 등록되지 않는 문제 (#6), 호스트의tsc실행을 막던 타입 오류 2건 (#2). safe()가 삼키던 예외를report()로 기록합니다 (#7) — 기존의 폴백 동작은 그대로 유지하면서, 실패 원인이 로그에 남도록 개선했습니다. 위의 #4, #5 이슈는 이 로그가 없어서 원인 추적이 특히 어려웠습니다.orbit:frontend-sync가tsconfig의 paths 설정을 동기화합니다 (#3) — 빌드는 정상 통과하지만types:check만 실패하던 문제를 해결했습니다.- 패키지 자체 타입 검증 게이트 도입 (#3) —
npm run types:check. 이 검증을 도입하자마자 실제 결함 3건을 추가로 발견했습니다. orbit.host_routes.name_prefix옵트인 설정 추가 (#8) — 호스트의routes/orbit.php에도orbit.프리픽스를 붙일 수 있습니다. 기본값은false입니다.- CI 워크플로 추가 — PHP 8.4, 8.5 검증 게이트와 태그·버전 일치 검사를 포함합니다.
- 테스트 개수: 3개 → 20개.
4.5.0
laravel/framework제약^11.0 || ^12.0 || ^13.0→^13.0: Laravel 13 전용으로 범위를 좁혔습니다.- 좁힌 이유: Pest 5를 도입한 4.4.0(패키지 자체 버전으로는 4.1.0)부터
pest-plugin-laravel5가laravel/framework ^13.23을 요구하게 되어, 이 저장소의 테스트는 사실상 Laravel 13에서만 실행되는 상태였습니다. 즉 겉으로는 Laravel 11·12 호환을 표방하고 있었지만 실제로는 검증할 방법이 없었던 것입니다. 검증되지 않는 지원 범위를 제약에 계속 남겨두지 않기로 했습니다. - Laravel 11·12 사용자는 업그레이드가 필요합니다. 이번 변경은 단순히 PHP 하한을 올리는 것과 달리 실제로 지원 대상을 제거하는 조치이므로, 소비자에게 직접적인 영향이 있습니다. Laravel 13으로 업그레이드하거나, 이전 버전에 머무르는 선택을 해야 합니다.
- 함께 검토했지만 좁히지 않은 의존:
laravel/scout ^10.0 || ^11.0(scout 10.25.0이 Laravel 13을 지원하는 것을 실제로 확인했습니다),inertiajs/inertia-laravel ^3.0,tabuna/breadcrumbs ^5.0,watson/active ^7.0— 이들은 모두 Laravel 13에서 정상적으로 해석됩니다.
4.4.0
- PHP 제약
^8.3→^8.4: 이제 PHP 8.3 환경에서는 설치되지 않습니다. pestphp/pest^5.0,pestphp/pest-plugin-laravel^5.0,orchestra/testbench^11.0(모두require-dev)로 업그레이드했습니다. PHP 하한을 올린 이유는 Pest 5가 PHP^8.4를 요구하기 때문입니다. testbench 범위를 좁힌 이유는pest-plugin-laravel5가laravel/framework ^13.23을 요구하기 때문에, testbench 9(Laravel 11 대응)와 10(Laravel 12 대응)이 Pest 5와 함께 설치될 수 없기 때문입니다.- 프로덕션 의존성은 하나도 변경되지 않았습니다. 직접 의존 20개를 전수 대조한 결과, PHP 하한 상향으로 새로 사용 가능해진 것은 위의 두 dev 패키지뿐이었습니다. 나머지 18개는 이미
^8.3에서도 최신 버전을 받고 있었습니다. 이번 상향은 새로운 기능을 얻기 위한 것이 아니라 장기적인 정리 작업입니다. - 패키지 소비자 입장의 Laravel 11·12 지원은 그대로 유지됩니다 (
^11.0 || ^12.0 || ^13.0). 다만 이 저장소 자체의 테스트는 Laravel 13에서만 실행되므로, Laravel 11·12 호환성은 로컬 테스트로는 더 이상 검증되지 않습니다.
4.3.0
-
intervention/image^3.0→^4.0: v4에서 이미지 API 이름이 바뀌면서MediaLibrary의 호출 4곳을 마이그레이션했습니다 —ImageManager::read()→decodeBinary(),encodeByPath()→encodeUsingPath(),encodeByMediaType()→encodeUsingMediaType()(2곳). v4는intervention/gif ^5도 함께 요구합니다. 자신의 코드에서Intervention\Imagev3 API를 직접 사용하는 호스트 프로젝트는 함께 마이그레이션이 필요합니다 —intervention/image를 직접 선언하지 않고 전이 의존성으로만 사용하는 호스트는 자동으로 업그레이드되며 영향을 받지 않습니다. -
테스트 기반 도입: core 최초의 자동화 테스트입니다.
phpunit.xml, testbench 하네스, 그리고MediaLibrary이미지 파이프라인 테스트 3종(최대 폭을 초과하는 이미지의 축소 처리 / 한계 이하 원본의 그대로 유지 /branding+favicon파비콘 변형 5종 + webmanifest 생성)을 추가했습니다. 단순히 기록된 치수만 확인하는 것이 아니라 디스크에 저장된 실제 바이트를 다시 디코딩해서 크기를 검증합니다 —processImage()가 예외를report()로 삼켜버리기 때문에, 저장된 결과물을 직접 검증하지 않으면 인코딩 실패가 조용히 넘어갈 수 있습니다. 실제로 API 이름을 v3 방식으로 되돌렸을 때 테스트가 정확히 실패하는 것까지 확인했습니다.composer install && vendor/bin/pest
4.2.0
-
Socialite를 선택 의존성으로 전환 (BREAKING):
laravel/socialite와socialiteproviders/*를require에서suggest로 옮겼습니다.새로 생성한 Laravel 13 프로젝트는 guzzle 8.1을 lock 파일에 고정합니다. 그런데
laravel/socialite는 어떤 버전을 쓰더라도(dev-master 포함) guzzle^6.0|^7.0만 허용합니다. 즉 core가 socialite를 하드 require로 걸어두는 동안에는composer require cms-orbit/core가 항상 실패했고,-W플래그로 guzzle을 강제로 7버전으로 다운그레이드해야만 설치가 가능했습니다.사실 코드 자체는 이미 socialite를 선택적으로 다루고 있었습니다. socialite를 참조하는 곳은 딱 두 곳이고 둘 다 가드가 걸려 있습니다 —
AuthServiceProvider::registerSocialiteProviders()는SocialiteWasCalled클래스가 없으면 조기 반환하고,SocialLoginController는abort_unless(class_exists(Socialite::class), 500)로 런타임에만 막습니다. 시그니처에서 Socialite 타입을 직접 사용하는 곳은 없습니다. 즉 실제 코드보다 매니페스트(composer.json)가 더 강한 제약을 걸고 있었던 셈입니다.이번 변경 이후에는
-W없이도 정상 설치되고, guzzle 8.1도 그대로 유지됩니다. -
소셜 로그인을 사용하는 호스트는 직접 의존성을 선언해야 합니다:
composer require laravel/socialite socialiteproviders/manager \ socialiteproviders/kakao socialiteproviders/apple -W선언하지 않으면 소셜 로그인 라우트만 500 오류를 반환하며, 나머지 기능에는 영향이 없습니다. 여기서도
-W가 필요한 이유는 동일합니다 — socialite를 실제로 설치하는 순간 guzzle이 7버전으로 내려가게 됩니다. 이는 socialite의 상류(upstream)가 guzzle 8을 지원하기 전까지는 피할 수 없는 제약이며, 다만 이제는 그 비용을 소셜 로그인 기능을 실제로 사용하는 프로젝트만 부담하면 됩니다.
4.1.0
- PHP 제약
^8.3복구: 게시된 모든 태그는 원래^8.3이었으나, main 브랜치에서 실수로^8.2로 내려가 있었습니다. Laravel 13은 PHP^8.3을 요구하므로,php ^8.2+laravel/framework ^13조합은 PHP 8.2 환경에서 조용히 Laravel 11을 설치해버리는 문제가 있었습니다. tabuna/breadcrumbs^4.0 || ^5.0 || ^6.0→^5.0:^6.0은 실제로 존재하지 않는 버전이었습니다. 최신 5.0.0이 Laravel 10~13을 모두 지원합니다.laravel/pint^1.14→^1.30(1.30이 PHP^8.3을 요구하기 때문입니다).- 릴리스 파이프라인 도입:
.githooks/pre-push가composer.json의version필드와 태그명이 일치하지 않는 태그의 푸시를 차단합니다. 과거cms-orbit/core에서4.0.8태그를 생성할 때version: 4.0.7로 잘못 만들어져 Packagist가 별다른 오류 없이 그 태그를 무시했고, 그 결과 4.0.8이 실제로는 게시되지 않았다는 사실을 아무도 알아차리지 못한 사고가 있었습니다. 이제bin/release <버전>스크립트가 버전 갱신·검증·커밋·태그·푸시를 하나의 동작으로 묶어 이런 드리프트를 원천적으로 막고,cms-orbit/*의존성이 실제로 Packagist에 게시되어 있는지도 Composer 리졸버를 통해 확인합니다. 저장소를 클론한 뒤composer install을 실행하면core.hooksPath가 자동으로 설정됩니다. intervention/image는^3.0유지: v4가 PHP^8.3을 요구하므로 이제는 받아들일 수 있는 조건이 되었지만, core가 사용하는 API 세 개가 모두 이름이 바뀝니다 (ImageManager::read()→decode(),encodeByPath()→encodeUsingPath(),encodeByMediaType()→encodeUsingMediaType()). 3.11.8은 여전히 지원되고 있고 보안 권고도 없는 상태이므로, 테스트가 아직 없는 시점에 섬네일·이미지 변환 경로를 건드리는 위험을 감수하지 않기로 했습니다.
4.0.7
orbit:admin로그인 정보 안내: 관리자 계정 생성 후 로그인에 사용할 아이디와 비밀번호를 화면에 출력합니다. 비밀번호를 공란으로 두면 기본값orbit1234가 사용되는데, 이를 명시적으로 안내함으로써 "입력한 정보와 일치하는 사용자를 찾을 수 없습니다"라는 오류로 오인하는 문제를 방지합니다. (보안상 로그인 실패 메시지는 계정 없음과 비밀번호 불일치를 구분하지 않습니다. 비밀번호를 공란으로 만들었다면orbit1234로 로그인하세요.)
4.0.6
npm run dev즉시 실행 가능:orbit:frontend-sync가vite.config.*에 alias 블록을 주입할 때import { fileURLToPath } from 'node:url';구문도 함께 자동으로 추가합니다. 순정 스타터킷의vite.config.ts에는 이 import 문이 없어서ReferenceError: fileURLToPath is not defined오류로 dev 서버가 기동되지 않던 문제를 해결했습니다. (이미 해당 import가 있는 경우에는 중복으로 추가하지 않습니다.)- 참고 (4.0.5 미만 버전에서 업그레이드하는 경우): 로그인 화면에서 발생하는
Target class [App\Http\Middleware\RequirePasswordChange] does not exist오류는 4.0.5부터 해당 미들웨어가 패키지 내부로 이관되면서 해결됩니다. 이미config/orbit.php를 게시(publish)한 프로젝트라면php artisan vendor:publish --tag=orbit-config --force명령으로 설정 파일을 갱신하세요.
4.0.5
- 순정 Laravel + Core만으로 관리자 화면 구동 가능: Inertia 공유 데이터(메뉴·섹션·브랜딩·flash 메시지·알림·미디어·i18n)와 루트 뷰(
orbit::orbit.app)를 패키지 내부로 이관했습니다. 이제 호스트 프로젝트의app/Http/Middleware/HandleInertiaRequests.php나resources/views/app.blade.php에 Orbit 전용 코드를 별도로 작성하지 않아도, 새로 추가된ShareOrbitInertia미들웨어가 Orbit 라우트 그룹 안에서 관리자 셸에 필요한 props를 자동으로 주입합니다. (기존에는 이 로직을 호스트 쪽에 직접 작성해야 했기 때문에, 순정 설치 상태에서는 메뉴·엔티티·스타일이 비어 보이는 문제가 있었습니다.) - User 프레젠터 패키지화: 베이스
User모델이 참조하던 호스트 클래스App\CmsOrbit\Core\Presenters\UserPresenter에 대한 의존을 제거하고, 패키지 내부의CmsOrbit\Core\Foundation\Presenters\UserPresenter(전역 검색용Searchable구현 포함)로 대체했습니다. - 강제 비밀번호 변경 스택 패키지화:
RequirePasswordChange미들웨어,ForcePasswordController,UpdateForcedPasswordRequest,orbit/auth/force-password페이지를 모두 core 패키지로 이관했습니다. - 자체 완결형 스타일:
orbit:frontend-sync가 호스트 프로젝트에resources/css/orbit.css파일을 생성하고, 패키지가 소유한 디자인 토큰 파일(orbit-theme.css)을 import합니다. 이제 호스트의app.css를 직접 수정할 필요가 없습니다. orbit:frontend-sync견고화:vite.config.*에resolve.alias블록이 없는 경우에도 자동으로 생성하며,orbit.css를 Vite의 입력(entry) 목록에 등록합니다.
라이선스
MIT