Files
slot/docs/04-theme-architecture.md
chpark 980d7d905d Add Next.js + PostgreSQL rewrite scaffold with 4-theme system
Stack
- Next.js 15 (App Router) + TypeScript + Drizzle ORM + postgres-js
- Node scrypt for password hashing; PBKDF2 verifier for legacy gnuboard5 hashes
- pnpm workspace monorepo: apps/web + packages/{db,auth,themes,...}

Themes (admin-selectable at /admin/themes)
- basic     : 그누보드 default reproduction (light, blue accent)
- eyoom     : eb4_maga_005 매거진 reproduction (dark, orange accent, ranking sidebar)
- amina     : Aminam Builder reproduction (light, violet gradient, card grid)
- youngcart : 영카트 shop reproduction (red accent, search bar, category nav)

DB
- New schema (12 tables) pushed to PG via drizzle-kit: members, sessions, boards,
  posts, point_ledger, app_settings, bacara_*, lottery_tickets, roulette_spins,
  game_points, board_groups
- Legacy data still readable from inspection2 schema via @slot/db/legacy

Verified end-to-end against the migrated DB on localhost:3000:
- Home renders with active theme tokens injected as CSS variables
- /free lists 442K real posts from inspection2.g5_write_free
- Login (testlogin/test1234) issues session cookie, header switches to
  "테스트님 환영합니다 / 로그아웃"
- Switching app_settings.theme.global from eyoom → amina swaps colors,
  layout, and Korean nav labels site-wide on next request

Migration docs added: 03-migration-plan, 04-theme-architecture,
05-local-dev-setup, 06-feature-inventory.
2026-04-27 18:51:32 +09:00

205 lines
8.6 KiB
Markdown

# Theme Architecture — 4-Theme Built-in System
## 0. 요구사항 (사용자 명시)
> "테마 관리에서 리액트 기반 한 가지 — **기본 / 이윰빌더 / 아미나빌더 / 영카드** — 이렇게 선택할 수 있게 구현"
→ 운영자가 어드민 화면에서 사이트 전체 테마를 한 번에 전환 가능. 페이지 단위 / 게시판 단위 오버라이드도 지원.
## 1. 4종 테마 정의
| 코드 | 한글명 | 모티프 | 활용 시나리오 |
|------|--------|--------|--------------|
| `basic` | **기본** | 그누보드5 default skin (responsive bootstrap-ish) | 미니멀, 신규 도메인 시작 |
| `eyoom` | **이윰빌더** | 현재 운영 중인 `eb4_maga_005` (매거진 + 사이드바 + 위젯) | 컨텐츠 중심 커뮤니티 |
| `amina` | **아미나빌더** | 그누보드용 Amina Builder (모던 카드형 + 메가메뉴) | 트래픽 큰 종합 커뮤니티 |
| `youngcart` | **영카드(영카트)** | 영카트 쇼핑몰 + 게시판 + 마이페이지 | 쇼핑 중심 사이트 |
## 2. 패키지 구조 — `packages/themes/`
```
packages/themes/
├── core/ # 테마 공통 인터페이스
│ ├── ThemeProvider.tsx # context, 토큰, css var 주입
│ ├── ThemeRegistry.ts # 모든 테마 메타데이터 + lazy loader
│ ├── types.ts # ThemeManifest, Slot, Layout 타입
│ └── slots.ts # Header/Footer/Side/Banner/PostList/PostView 슬롯
├── basic/
│ ├── manifest.ts
│ ├── tokens.ts # CSS variables (--color-primary, --space-md, etc.)
│ ├── layouts/{root,board,post,shop}.tsx
│ ├── components/{Header,Footer,Sidebar,PostCard,...}.tsx
│ ├── styles/ # tailwind.config 또는 sass
│ └── index.ts # default export = ThemeManifest
├── eyoom/
│ ├── manifest.ts
│ ├── tokens.ts # 이윰 매거진 컬러/타이포 (참고: src/theme/eb4_maga_005)
│ ├── layouts/...
│ ├── components/{MegaMenu,RankingSidebar,LatestWidget,...}.tsx
│ └── index.ts
├── amina/ # Aminam Builder reproduction
│ └── ...
└── youngcart/ # YoungCart shop-centric
├── components/{ProductGrid,CartDrawer,CategoryNav,...}.tsx
└── ...
```
## 3. 테마 인터페이스
```ts
// packages/themes/core/types.ts
export interface ThemeManifest {
id: 'basic' | 'eyoom' | 'amina' | 'youngcart';
name: string; // 한글 표시명
preview: string; // 썸네일 URL
tokens: ThemeTokens; // CSS 변수 (런타임 주입)
layouts: {
root: ComponentType<LayoutProps>;
board: ComponentType<BoardLayoutProps>;
post: ComponentType<PostLayoutProps>;
shop: ComponentType<ShopLayoutProps>;
page: ComponentType<PageLayoutProps>;
};
slots: {
Header: ComponentType<HeaderProps>;
Footer: ComponentType<FooterProps>;
Sidebar: ComponentType<SidebarProps>;
PostList: ComponentType<PostListProps>;
PostCard: ComponentType<PostCardProps>;
PostView: ComponentType<PostViewProps>;
LoginForm: ComponentType<LoginFormProps>;
NewWindow: ComponentType<NewWindowProps>;
// ...20개 정도 슬롯 표준화
};
features?: {
shop?: boolean; // youngcart 만 true
magazineWidgets?: boolean; // eyoom 만 true
megaMenu?: boolean; // amina, eyoom
};
}
```
## 4. Slot/Override 메커니즘
### 4.1 글로벌 테마
- `app_settings.theme = 'eyoom'` (DB) 가 활성 테마
- `apps/web/src/app/layout.tsx``ThemeRegistry.load(activeTheme)` 으로 동적 import
- React Server Component 단계에서 결정 → 첫 페인트부터 올바른 레이아웃
### 4.2 페이지 단위 override
- 게시판/카테고리/페이지에 `theme_override` 컬럼
- 예: 자유게시판은 `eyoom`, 쇼핑은 `youngcart` 동시 운영 가능
### 4.3 사용자 토글 (옵션)
- 회원이 `mb_theme_pref` 로 본인 선호 테마 선택 가능
- cookie + DB 저장
- 관리자가 기능 on/off 제어
## 5. 관리자 — 테마 선택 UI
`apps/admin/src/app/themes/page.tsx`:
```
┌─ 테마 관리 ──────────────────────────────┐
│ [기본] [이윰빌더] [아미나빌더] [영카드] │ ← 4개 카드, 활성 테마 강조
│ │
│ ◉ 기본 사이트 테마: [이윰빌더 ▼] │
│ │
│ ─ 영역별 오버라이드 ─ │
│ · 자유게시판: (글로벌) │
│ · 쇼핑몰: 영카드 │
│ · /event 페이지: 아미나빌더 │
│ │
│ ─ 미리보기 ─ │
│ https://localhost:3000/?_preview=amina │
│ │
│ [저장] [기본값 복원] │
└──────────────────────────────────────────┘
```
DB:
```sql
CREATE TABLE app_settings (
key TEXT PRIMARY KEY,
value JSONB
);
INSERT INTO app_settings VALUES
('theme.global', '"eyoom"'::jsonb),
('theme.overrides', '[{"path":"/shop/*","theme":"youngcart"}]');
```
## 6. 디자인 토큰 표준
각 테마는 동일한 토큰 스키마를 만족 → CSS 변수로 주입 → 컴포넌트는 토큰만 참조 (매직 컬러 금지):
```ts
// tokens.ts (예: eyoom)
export const tokens: ThemeTokens = {
color: {
primary: '#ff5722',
primaryDark: '#e64a19',
bg: '#0e0f12',
bgSurface: '#16181d',
text: '#e6e6e6',
textMuted: '#9aa0a6',
border: '#2a2d34',
success: '#21d07a',
danger: '#e74c3c',
warning: '#f5b041',
},
font: {
sans: '"Noto Sans KR", system-ui, sans-serif',
mono: '"JetBrains Mono", monospace',
},
radius: { sm: '4px', md: '8px', lg: '14px', pill: '999px' },
space: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '40px' },
shadow: { sm: '...', md: '...', lg: '...' },
z: { sticky: 100, modal: 1000, toast: 2000 },
};
```
각 테마는 동일 키를 채우되 값만 다르게 (basic 은 라이트, eyoom 은 다크 매거진, amina 는 그라디언트, youngcart 는 화이트 + 액센트)
## 7. 마이그레이션 매핑 (이윰 → React)
| 기존 (`theme/eb4_maga_005/`) | 신규 (`packages/themes/eyoom/`) |
|-----------------------------|-------------------------------|
| `head.html.php` | `layouts/root.tsx` (Header) |
| `head.sub.html.php` | `slots/Header.tsx` |
| `tail.html.php` | `slots/Footer.tsx` |
| `side.html.php` | `slots/Sidebar.tsx` |
| `index.html.php` | `layouts/root.tsx` (홈 위젯) |
| `skin/board/<X>/list.skin.html.php` | `slots/PostList.tsx` |
| `skin/board/<X>/view.skin.html.php` | `slots/PostView.tsx` |
| `css/eyoom.css`, `css/eyoom-style.css` | `styles/*.css` (CSS module 또는 tailwind) |
| `js/eyoom.js` | React hooks / event handlers |
### 매거진 위젯 (이윰 코어)
- 최신글 슬라이더 (`eyoom_slider`) → `<LatestSlider />`
- 회원 랭킹 (`eyoom_ranking`) → `<MemberRanking type="bacara|slots|..." />`
- 출석 (`eyoom_attendance`) → `<AttendanceWidget />`
- 배너 (`eyoom_banner`) → `<RotatingBanner />`
## 8. 아미나빌더 디자인 노트
- 메가메뉴 (1단 큰 카테고리 + 2단 서브)
- 카드형 그리드 (3열 → 모바일 1열)
- 섹션별 스킨 시스템 (홈에 여러 게시판 위젯 동시 렌더)
- 라이트/다크 테마 토글
- 인기 회원 랭킹, 실시간 댓글, 인기글 사이드바
## 9. 영카드 디자인 노트
- 상단 카테고리 가로 네비
- 메인 = 베스트 상품 그리드 + 쿠폰존 + 이벤트 슬라이더
- 마이페이지: 주문/배송/쿠폰/적립금
- 게시판은 영카트 기본 룩 (테이블형 리스트)
## 10. 신규 테마 추가 절차 (확장성)
1. `packages/themes/<id>/` 폴더 생성, manifest 작성
2. `ThemeRegistry.register({ id: '<id>', loader: () => import('./<id>') })` 추가
3. `app_settings.theme.global` 후보에 자동 노출
4. 별도 빌드 없이 hot reload (Next.js dev 모드)
## 11. 성능
- 각 테마 번들은 lazy chunk → 활성 테마만 다운로드
- 테마 토큰은 `<style>:root{--color-primary: ...}</style>` 인라인 → FOUC 방지
- RSC 단에서 테마 결정 → 클라이언트 hydration 최소화