980d7d905d
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.
205 lines
8.6 KiB
Markdown
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 최소화
|