- .gitignore에 이미 등록되어 있으나 과거 커밋으로 트래킹 중이던 파일 제거 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
23 KiB
Research: Next.js / React ERP & Admin Panel Architecture Patterns (2025-2026)
Date: 2026-02-11 Purpose: Compare SAM ERP's current architecture against proven open-source patterns Confidence: High (0.85) - Based on 6 major open-source projects and established methodologies
Executive Summary
After investigating 6 major open-source admin/ERP frameworks and 3 architectural methodologies, the dominant pattern emerging in 2025-2026 is a hybrid approach: domain/feature-based folder organization combined with headless CRUD hooks and a provider-based API abstraction layer. Pure Atomic Design is losing ground to Feature-Sliced Design (FSD) for application-level organization, though Atomic Design remains useful for the shared UI component layer.
Key Findings
- Resource-based CRUD abstraction (react-admin, Refine) is the most proven pattern for 50+ page admin apps
- Feature/domain-based folder structure is winning over layer-based (atoms/molecules/organisms) for application code
- Provider pattern (dataProvider, authProvider) decouples UI from API more effectively than scattered Server Actions
- Config-driven UI generation (Payload CMS) reduces code duplication for similar pages
- Headless hooks (useListController, useTable, useForm) separate business logic from UI completely
1. Project-by-Project Architecture Analysis
1.1 React-Admin (marmelab) -- 25K+ GitHub Stars
Architecture: Resource-based SPA with Provider pattern
Key Concepts:
- Resources: The core abstraction. Each entity (posts, users, orders) is a "resource" with CRUD views
- Providers: Adapter layer between UI and backend
dataProvider- abstracts all API calls (getList, getOne, create, update, delete)authProvider- handles authentication flowi18nProvider- internationalization
- Headless Core:
ra-corepackage contains all hooks, zero UI dependency - Controller Hooks:
useListController,useEditController,useCreateController,useShowController
Folder Pattern:
src/
resources/
posts/
PostList.tsx # <List> view
PostEdit.tsx # <Edit> view
PostCreate.tsx # <Create> view
PostShow.tsx # <Show> view
users/
UserList.tsx
UserEdit.tsx
providers/
dataProvider.ts # API abstraction
authProvider.ts # Auth abstraction
App.tsx # Resource registration
CRUD Registration Pattern:
<Admin dataProvider={dataProvider} authProvider={authProvider}>
<Resource name="posts" list={PostList} edit={PostEdit} create={PostCreate} />
<Resource name="users" list={UserList} edit={UserEdit} />
</Admin>
SAM Comparison:
| Aspect | react-admin | SAM ERP |
|---|---|---|
| API Layer | Centralized dataProvider | 89 scattered actions.ts files |
| CRUD Views | Resource-based registration | Manual page creation per domain |
| State | React Query (built-in) | Zustand + manual fetching |
| Form | react-hook-form (built-in) | Mixed (migrating to RHF+Zod) |
Sources:
1.2 Refine -- 30K+ GitHub Stars
Architecture: Headless meta-framework with resource-based CRUD
Key Concepts:
- Headless by design: Zero UI opinion, works with Ant Design, Material UI, Shadcn, or custom
- Data Provider Interface: Standardized CRUD methods (getList, getOne, create, update, deleteOne)
- Resource Hooks:
useTable,useForm,useShow,useSelect-- all headless - Inferencer: Auto-generates CRUD pages from API schema
Data Provider Interface:
const dataProvider = {
getList: ({ resource, pagination, sorters, filters }) => Promise,
getOne: ({ resource, id }) => Promise,
create: ({ resource, variables }) => Promise,
update: ({ resource, id, variables }) => Promise,
deleteOne: ({ resource, id }) => Promise,
getMany: ({ resource, ids }) => Promise,
custom: ({ url, method, payload }) => Promise,
};
Headless Hook Pattern:
// useTable returns data + controls, you handle UI
const { tableProps, sorters, filters } = useTable({ resource: "products" });
// useForm returns form state + submit, you handle UI
const { formProps, saveButtonProps } = useForm({ resource: "products", action: "create" });
SAM Comparison:
| Aspect | Refine | SAM ERP |
|---|---|---|
| API Abstraction | Single dataProvider | Per-domain actions.ts |
| List Page | useTable hook | UniversalListPage template |
| Form | useForm hook (headless) | Manual per-page forms |
| Code Generation | Inferencer auto-gen | Manual creation |
Sources:
1.3 Payload CMS 3.0 -- 30K+ GitHub Stars
Architecture: Config-driven, Next.js-native with auto-generated admin UI
Key Concepts:
- Collection Config: Define schema once, get admin UI + API + types automatically
- Field System: Rich field types auto-generate corresponding UI components
- Hooks: beforeChange, afterRead, beforeValidate at collection and field level
- Access Control: Document-level and field-level permissions in config
- Next.js Native: Installs directly into /app folder, uses Server Components
Config-Driven Pattern:
// collections/Products.ts
export const Products: CollectionConfig = {
slug: 'products',
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'price', 'status'],
},
access: {
read: () => true,
create: isAdmin,
update: isAdminOrSelf,
},
hooks: {
beforeChange: [calculateTotal],
afterRead: [formatCurrency],
},
fields: [
{ name: 'name', type: 'text', required: true },
{ name: 'price', type: 'number', min: 0 },
{ name: 'status', type: 'select', options: ['draft', 'published'] },
{ name: 'category', type: 'relationship', relationTo: 'categories' },
],
};
SAM Comparison:
| Aspect | Payload CMS | SAM ERP |
|---|---|---|
| Page Generation | Auto from config | Manual per page |
| Field Definitions | Centralized schema | Inline JSX per form |
| Access Control | Config-based per field | Manual per component |
| Type Safety | Auto-generated from schema | Manual interface definitions |
Sources:
1.4 Medusa Admin v2 -- 26K+ GitHub Stars
Architecture: Domain-based routes with widget injection system
Key Concepts:
- Domain Routes: Routes organized by business domain (products, orders, customers)
- Widget System: Inject custom React components into predetermined zones
- UI Routes: File-based routing under src/admin/routes/
- Hook-based data fetching: Domain-specific hooks for API integration
- Monorepo: UI library (@medusajs/ui) separate from admin logic
Folder Structure:
packages/admin/dashboard/src/
routes/
products/
product-list/
components/
hooks/
page.tsx
product-detail/
components/
hooks/
page.tsx
orders/
order-list/
order-detail/
customers/
hooks/ # Shared hooks
components/ # Shared components
lib/ # Utilities
SAM Comparison:
| Aspect | Medusa Admin | SAM ERP |
|---|---|---|
| Route Organization | Domain > Action > Components | Domain > page.tsx + actions.ts |
| Shared Components | Separate UI package | organisms/molecules/atoms |
| Hooks | Per-route + shared | Global + inline |
| Extensibility | Widget injection zones | N/A |
Sources:
1.5 AdminJS
Architecture: Auto-generated admin from resource configuration
Key Concepts:
- Resource Registration: Register database models, get admin UI automatically
- Component Customization: Override via ComponentLoader
- Dashboard Customization: Custom React components for dashboard
SAM Relevance: Lower -- AdminJS is more backend-driven (Node.js ORM-based) and less applicable to a frontend-heavy ERP.
Sources:
1.6 Hoppscotch
Architecture: Monorepo with shared-library pattern
Key Concepts:
- @hoppscotch/common: 90% of UI and business logic in shared package
- @hoppscotch/data: Type safety across all layers
- Platform-specific code: Thin wrapper handling native capabilities
SAM Relevance: The shared-library-as-core pattern is interesting for large codebases where most logic is platform-agnostic.
Sources:
2. Architectural Methodologies Comparison
2.1 Feature-Sliced Design (FSD) -- Rising Standard
7-Layer Architecture:
app/ # App initialization, providers, routing
processes/ # Complex cross-page business flows (deprecated in latest)
pages/ # Full page compositions
widgets/ # Self-contained UI blocks with business logic
features/ # User-facing actions (login, add-to-cart)
entities/ # Business entities (user, product, order)
shared/ # Reusable utilities, UI kit, configs
Key Rules:
- Layers can ONLY import from layers below them
- Each layer divided into slices (domain groupings)
- Each slice divided into segments (ui/, model/, api/, lib/, config/)
FSD Applied to ERP:
src/
app/ # App shell, providers
pages/
quality-qms/ # QMS page composition
sales-quote/ # Quote page composition
widgets/
inspection-report/ # Self-contained inspection UI
ui/
model/
api/
quote-calculator/
features/
add-inspection-item/
approve-quote/
entities/
inspection/
ui/ (InspectionCard, InspectionRow)
model/ (types, store)
api/ (getInspection, updateInspection)
quote/
ui/
model/
api/
shared/
ui/ (Button, Table, Modal -- your atoms)
lib/ (formatDate, exportUtils)
api/ (httpClient, apiProxy)
config/ (constants)
Sources:
2.2 Atomic Design -- Aging for App-Level Organization
SAM's Current Approach:
components/
atoms/ # Basic UI elements
molecules/ # (unused)
organisms/ # Complex composed components
templates/ # Page layout templates
Industry Assessment (2025-2026):
- Atomic Design excels for UI component libraries (shared/ layer)
- Struggles with domain complexity -- "UserCard" and "ProductCard" are both organisms but semantically distinct
- Grouping by visual complexity (atom/molecule/organism) dilutes domain boundaries
- Most large-scale projects have moved to feature/domain organization for application code
- Atomic Design remains valuable for the shared UI kit layer only
Sources:
2.3 Modular Monolith (Frontend)
Key Principles for ERP:
- Single deployment, but internally organized as independent modules
- Each module = bounded context with clear API boundaries
- Modules communicate through well-defined interfaces, not direct imports
- Common concerns (auth, logging) handled at application level
Applied to Next.js ERP:
src/
modules/
quality/
components/
hooks/
actions/
types/
index.ts # Public API -- only exports from here
sales/
components/
hooks/
actions/
types/
index.ts
accounting/
...
shared/ # Cross-module utilities
app/ # Next.js routing (thin layer)
Sources:
3. Server Actions Organization Patterns
Pattern A: Colocated (SAM's Current -- 89 files)
app/[locale]/(protected)/quality/qms/
page.tsx
actions.ts # Server actions for this route
Pros: Easy to find, clear ownership Cons: Duplication across similar pages, no reuse
Pattern B: Domain-Centralized (react-admin / Refine style)
src/
actions/
quality/
inspection.ts # All inspection-related server actions
qms.ts
sales/
quote.ts
order.ts
lib/
api-client.ts # Shared fetch logic with auth
Pros: Reusable across pages, easier to maintain Cons: Indirection, harder to find for route-specific logic
Pattern C: Hybrid (Recommended for large apps)
app/[locale]/(protected)/quality/qms/
page.tsx
_actions.ts # Route-specific actions only
src/
domains/
quality/
actions/ # Shared domain actions
inspection.ts
qms.ts
hooks/
types/
Pros: Route-specific stays colocated, shared logic centralized Cons: Need clear rules on what goes where
Industry Consensus
For 100+ page apps, the hybrid approach (Pattern C) dominates. Route-specific logic stays colocated; shared domain logic is centralized. The key is having a clear data provider / API client layer that all server actions delegate to.
Sources:
4. CRUD Abstraction Patterns for 50+ Similar Pages
Pattern 1: Resource Hooks (react-admin / Refine approach)
// hooks/useResourceList.ts
function useResourceList<T>(resource: string, options?: ListOptions) {
const [data, setData] = useState<T[]>([]);
const [pagination, setPagination] = useState({ page: 1, pageSize: 20 });
const [filters, setFilters] = useState({});
const [sorters, setSorters] = useState({});
useEffect(() => {
fetchList(resource, { pagination, filters, sorters })
.then(result => setData(result.data));
}, [resource, pagination, filters, sorters]);
return { data, pagination, setPagination, filters, setFilters, sorters, setSorters };
}
// Usage in any list page
function QualityInspectionList() {
const { data, pagination, filters } = useResourceList<Inspection>('quality/inspections');
return <UniversalListPage data={data} columns={inspectionColumns} />;
}
Pattern 2: Config-Driven Pages (Payload CMS approach)
// configs/quality-inspection.config.ts
export const inspectionConfig: ResourceConfig = {
resource: 'quality/inspections',
list: {
columns: [
{ key: 'id', label: '번호' },
{ key: 'name', label: '검사명' },
{ key: 'status', label: '상태', render: StatusBadge },
],
filters: [
{ key: 'status', type: 'select', options: statusOptions },
{ key: 'dateRange', type: 'daterange' },
],
defaultSort: { key: 'createdAt', direction: 'desc' },
},
form: {
fields: [
{ name: 'name', type: 'text', required: true, label: '검사명' },
{ name: 'type', type: 'select', options: typeOptions, label: '검사유형' },
],
},
};
// Generic page component
function ResourceListPage({ config }: { config: ResourceConfig }) {
const list = useResourceList(config.resource);
return <UniversalListPage {...list} columns={config.list.columns} />;
}
Pattern 3: Template Composition (SAM's current direction, improved)
// templates/UniversalCRUDPage.tsx -- enhanced version
function UniversalCRUDPage<T>({
resource,
listConfig,
detailConfig,
formConfig,
}: CRUDPageProps<T>) {
// Handles list/detail/form modes based on URL
// Integrates data fetching, pagination, filtering
// Renders appropriate template based on mode
}
Industry Assessment
- Pattern 1 (Resource Hooks) is the most widely adopted -- used by react-admin (25K stars) and Refine (30K stars)
- Pattern 2 (Config-Driven) reduces code the most but requires upfront investment in the config system
- Pattern 3 (Template Composition) is the middle ground -- SAM's
UniversalListPageis already this direction
Recommendation: Evolve toward a Provider + Resource Hooks layer. Keep UniversalListPage and IntegratedDetailTemplate but back them with a standardized data provider.
5. Comparison Matrix: SAM ERP vs Industry Patterns
| Dimension | SAM ERP (Current) | react-admin | Refine | Payload CMS | FSD | Recommendation |
|---|---|---|---|---|---|---|
| Folder Structure | Domain-based (app router) | Resource-based | Resource-based | Collection-based | Layer > Slice > Segment | Hybrid Domain + FSD shared layer |
| Component Org | Atomic Design (partial) | Flat per resource | Flat per resource | Config-driven | Layer-based (entities/features) | FSD for app code, Atomic for shared UI |
| API Layer | 89 colocated actions.ts | Centralized dataProvider | Centralized dataProvider | Built-in Local API | api/ segment per slice | Centralized API client + domain actions |
| CRUD Abstraction | UniversalListPage template | Resource + Controller hooks | useTable/useForm hooks | Auto-generated from config | Manual per feature | Add resource hooks on top of templates |
| Form Handling | Mixed (migrating to RHF+Zod) | react-hook-form (built-in) | react-hook-form (headless) | Auto from field config | Manual per feature | Complete RHF+Zod migration |
| State Management | Zustand stores | React Query (built-in) | React Query (built-in) | Server-side | Per-slice model/ | Keep Zustand for UI state, add React Query for server state |
| Type Safety | Manual interfaces | Built-in types | TypeScript throughout | Auto-generated from schema | Manual per segment | Consider schema-driven type generation |
| 50+ Page Scale | Manual duplication | Resource registration | Inferencer + hooks | Collection config | Slice per entity | Resource hooks + config-driven columns |
6. Actionable Recommendations for SAM ERP
Priority 1: Introduce a Data Provider / API Client Layer
Why: The biggest gap vs. industry standard. 89 scattered actions.ts files means duplicated fetch logic, inconsistent error handling, and no centralized caching.
Action: Create a dataProvider abstraction inspired by react-admin/Refine:
// src/lib/data-provider.ts
export const dataProvider = {
getList: (resource, params) => proxyFetch(`/api/proxy/${resource}`, params),
getOne: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`),
create: (resource, data) => proxyFetch(`/api/proxy/${resource}`, { method: 'POST', body: data }),
update: (resource, id, data) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'PUT', body: data }),
delete: (resource, id) => proxyFetch(`/api/proxy/${resource}/${id}`, { method: 'DELETE' }),
};
Priority 2: Create Resource Hooks
Why: Reduce per-page boilerplate for list/detail/form patterns.
Action: Build useResourceList, useResourceDetail, useResourceForm hooks that wrap the data provider.
Priority 3: Evolve Folder Structure Toward Hybrid FSD
Why: Atomic Design for app-level code leads to unclear domain boundaries.
Action:
- Keep
shared/ui/(atoms/organisms) for reusable UI components - Add
domains/orentities/for business-logic grouping - Keep
app/routes thin -- delegate to domain components
Priority 4: Complete Form Standardization
Why: Mixed form patterns make maintenance harder and prevent reusable form configs.
Action: Complete the react-hook-form + Zod migration. Consider field-config-driven forms (Payload pattern) for highly repetitive forms.
Priority 5: Consider Server State Management (React Query / TanStack Query)
Why: react-admin and Refine both use React Query internally for caching, optimistic updates, and background refetching. Zustand is better suited for client UI state.
Action: Evaluate adding TanStack Query for server state alongside Zustand for UI state.
7. What SAM ERP Is Already Doing Well
- Domain-based routing (
app/[locale]/(protected)/quality/...) aligns with industry best practice - UniversalListPage + IntegratedDetailTemplate is the right abstraction direction (similar to react-admin's List/Edit components)
- SearchableSelectionModal as a reusable pattern is good (similar to react-admin's ReferenceInput)
- Server Actions in colocated files follows Next.js official recommendation for route-specific logic
- Zustand for global state is a solid choice for UI state (sidebar state, theme, etc.)
Sources
Open-Source Projects
- react-admin - Architecture
- react-admin - GitHub
- Refine - Data Provider
- Refine - GitHub
- Payload CMS - Collections
- Payload CMS - GitHub
- Medusa - Admin Development
- Medusa - GitHub
Architectural Methodologies
- Feature-Sliced Design
- FSD - Layers Reference
- Atomic Design + FSD Hybrid
- Clean Architecture vs FSD in Next.js