diff --git a/docs/03-migration-plan.md b/docs/03-migration-plan.md new file mode 100644 index 0000000..555c536 --- /dev/null +++ b/docs/03-migration-plan.md @@ -0,0 +1,212 @@ +# Migration Plan — gnuboard5(PHP) → Next.js + Node.js + PostgreSQL + +## 0. 목표 +1. 기존 사이트(`slot-ss.com`) 의 **모든 기능을 보존**한 채 stack 을 PHP → Node/Next 로 전면 교체. +2. **테마 시스템을 빌트인** — 운영자가 관리자 화면에서 다음 4종 중 하나를 선택할 수 있어야 함: + - **기본** (그누보드 default 룩앤필 reproduction) + - **이윰빌더** (현재 운영 중인 `eb4_maga_005` 매거진 레이아웃 reproduction) + - **아미나빌더** (그누보드5 의 또 다른 인기 빌더 — Aminam Builder 룩앤필) + - **영카드(YoungCart)** (영카트 쇼핑몰 중심 레이아웃) +3. PostgreSQL 단일 DB. 회원/포인트/게시판/쇼핑/베팅/검수 모두 통합. +4. 운영 가능한 마이그레이션 체크리스트와 롤백 경로 제공. + +## 1. 신규 스택 + +| 레이어 | 기술 | 용도 | +|--------|------|------| +| Web | **Next.js 15** (App Router, RSC) + TypeScript | SSR/ISR 페이지, SEO, 4테마 라우팅 | +| API | **Hono** (Node.js, edge-friendly) — 또는 Next.js Route Handler | REST + WebSocket 핸들러 | +| Auth | **NextAuth.js v5 (Auth.js)** | 세션, 소셜 로그인, 2FA | +| ORM | **Drizzle ORM** | TypeScript 타입 안전, raw SQL 친화 | +| DB | **PostgreSQL 17** | 메인 데이터 | +| Cache | **Redis 7** | 세션, 캐시, 랭킹, rate-limit | +| Queue | **BullMQ** (Redis backed) | 27개 그누보드 cron 대체 | +| Object Storage | **S3 호환** (R2/MinIO/AWS) | 첨부파일·에디터 이미지 (현재 82.5GB) | +| Search | **Meilisearch** (옵션) 또는 PostgreSQL FTS | 게시판/회원 검색 (현재 sphinx 대체) | +| Realtime | **Socket.IO** 또는 Pusher | 바카라/룰렛 실시간, 알림 | +| 결제 | KCP / 이니시스 / LG U+ Node SDK 또는 직접 통신 | 기존 4종 게이트웨이 대응 | +| 본인인증 | OK-Name / iNICert / Naver/Kakao API | 본인인증 | +| Deploy | Docker + (Vercel | Coolify | Self-hosted) | — | + +## 2. 모노레포 레이아웃 (pnpm workspace + Turborepo) + +``` +slot/ +├── apps/ +│ ├── web/ # Next.js 사용자 사이트 +│ ├── admin/ # Next.js 관리자 (별도 호스트 권장) +│ └── api/ # Hono API + WebSocket +├── packages/ +│ ├── db/ # Drizzle 스키마, 마이그레이션, 시드 +│ ├── auth/ # 인증 헬퍼, PBKDF2 검증, NextAuth 설정 +│ ├── themes/ # 4종 테마 — basic/eyoom/amina/youngcart +│ ├── ui/ # 공통 컴포넌트 (Button, Modal, Editor, Pagination, ...) +│ ├── shop/ # 쇼핑몰 도메인 (item, cart, order, payment) +│ ├── games/ # 바카라/룰렛/복권/슬롯 코어 + Swiun API +│ ├── shared/ # 타입, 유틸, validators (zod) +│ └── workers/ # BullMQ 워커 (rank-up, point-dist, sms) +└── ops/ + ├── docker/ # docker-compose, Dockerfiles + └── pgloader/ # 기존 db/migrate_*.load 이전 +``` + +## 3. URL 호환성 + +기존 `.htaccess` 가 정의한 라우트는 그대로 유지 (SEO 보존): + +| 기존 패턴 | Next.js 라우트 | +|-----------|---------------| +| `/` | `/` | +| `/` | `/[boTable]` | +| `//` | `/[boTable]/[wrId]` | +| `/group/` | `/group/[grId]` | +| `/page/` | `/page/[pid]` | +| `/content/` | `/content/[coId]` | +| `/mypage/` | `/mypage/[tab]` | +| `/shop/list-` | `/shop/list/[caId]` | +| `/shop/brand-` | `/shop/brand/[brCd]` | +| `/shop/` | `/shop/[itId]` | +| `/adm/*` | `/admin/*` (별도 admin 앱) | +| `/bbs/login.php` | `/login` | +| `/bbs/board.php?bo_table=X` | `/[X]` (rewrite) | + +기존 PHP URL(`/bbs/board.php?bo_table=free`)에 대해서는 미들웨어에서 새 URL로 301 리다이렉트. + +## 4. 데이터 마이그레이션 + +1. **이미 완료**: MariaDB → PostgreSQL (pgloader, schema=`inspection2`) +2. **다음 단계**: + - `packages/db/schema/*.ts` — Drizzle 스키마를 신규 design 으로 정의 + - `g5_member` → `members` + - `g5_board` + `g5_write_` → `boards` + 단일 `posts` 테이블 + `comments` 테이블 (write 테이블 분리 구조 폐기) + - `g5_point` → `point_ledger` + - `g5_visit` → `visits` (또는 ClickHouse / 시계열 DB 로 분리) + - 게임/베팅/검수/SMS/챗봇 → 각각 도메인 패키지로 + - `packages/db/migrate-from-legacy/*.ts` — 운영 데이터를 신규 스키마로 ETL 변환 + - 36개 `g5_write_` 테이블을 단일 `posts` 로 union (board_id 컬럼 추가) + - PBKDF2 비밀번호 해시는 그대로 유지 (legacy verifier로 검증, 첫 로그인 시 bcrypt 로 자동 업그레이드) +3. **검증 쿼리**: 기존 vs 신규 row count, sum(mb_point), 활성 회원 수 등 매칭 확인. + +## 5. 인증·세션 호환 + +### 5.1 비밀번호 호환 +- 그누보드 PBKDF2 (sha256, 12000 iter, 24-byte salt+hash) 검증을 그대로 구현 (`packages/auth/legacy-pbkdf2.ts`) +- 신규 가입자는 `bcrypt` 또는 `argon2id` 사용 +- 기존 회원이 첫 로그인 성공 시: PBKDF2 검증 후 신규 알고리즘으로 rehash (자동 업그레이드) + +### 5.2 세션 +- NextAuth Database session (PostgreSQL) — Redis 캐시 +- `mb_id` 기반 식별자 → 신규 `user_id` (UUID v7) 매핑 테이블 유지 + +### 5.3 2FA (ask-otp 호환) +- 관리자 admin 계정 OTP 그대로 유지 +- TOTP (Google Authenticator) 표준 사용 + +## 6. 게시판 모델 통합 + +기존: 게시판마다 `g5_write_` 테이블 분리 (스키마는 동일) +신규: 단일 `posts` 테이블 + `board_id` 외래키 + +```sql +CREATE TABLE posts ( + id UUID PRIMARY KEY, + board_id TEXT NOT NULL REFERENCES boards(slug), + parent_id UUID, -- 댓글이면 부모 글 id + reply_path TEXT, -- gnuboard wr_reply 호환 (중첩 댓글 정렬) + num BIGINT, -- 글 그룹 (정렬용, gnuboard wr_num 호환) + author_id UUID REFERENCES members(id), + author_name TEXT, -- 비회원 글 + subject TEXT, + content TEXT, + attachments JSONB, -- file metadata + hit INTEGER DEFAULT 0, + good INTEGER DEFAULT 0, + bad INTEGER DEFAULT 0, + is_comment BOOLEAN DEFAULT FALSE, + is_secret BOOLEAN DEFAULT FALSE, + ip INET, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ +); +CREATE INDEX posts_board_idx ON posts (board_id, created_at DESC); +CREATE INDEX posts_board_thread_idx ON posts (board_id, num, reply_path); +CREATE INDEX posts_author_idx ON posts (author_id, created_at DESC); +``` + +PostgreSQL 의 partial index, partition, JSONB 활용으로 36개 테이블 union 후에도 성능 무리 없음. +초대형 게시판 (`free` 442K rows) 은 **list partition** 으로 분할 가능. + +## 7. 슬롯/카지노 도메인 + +### 7.1 게임 포인트 +- `point_ledger` (append-only) — 적립/사용/환전 내역 +- 트리거 또는 워커가 회원 잔고 (`members.point_balance`) 갱신 +- 운영 DB 의 `g5_point` (594만 행) → 그대로 임포트 + +### 7.2 슬롯 베팅 (Swiun API 브릿지) +- `apps/api/src/routes/swiun.ts` 가 외부 슬롯사 API 와 통신 +- WebSocket 으로 실시간 베팅 결과 푸시 +- 기존 `plugin/swiunApi/*.php` 의 로직 1:1 이전 + +### 7.3 바카라 +- 기존 `plugin/bacara/*.php` → `packages/games/bacara/*` +- 게임 룰: 베팅(Player/Banker/Tie) + 결과 + 정산 +- 실시간 페이지: Socket.IO 룸 단위 브로드캐스트 + +### 7.4 룰렛 +- 기존 `roulette/index.php` 의 휠 게임 → React + Framer Motion 으로 재구현 +- 회당 1회 무료 + 추가 회전은 포인트 차감 + +### 7.5 복권 +- `g5_write_lottery_ticket` (45,724건) → `lottery_tickets` 테이블 +- 응모 → 추첨 (cron) → 당첨자 통보 → 포인트 지급 + +### 7.6 블랙리스트 / 검수 +- `blacklist_table` (193K), `check_table` (359K) → `blacklist`, `inspection_logs` +- 관리자가 ID/IP/회원명 검색 가능 + +### 7.7 챗봇 +- `chatbot_conversations`, `chatbot_feedback` → `chatbot_*` +- OpenAI 또는 Anthropic API 연동 (`apps/api/src/routes/chatbot.ts`) + +## 8. 27개 cron → BullMQ 워커 + +기존 `plugin/cron/auto.*rankup.php` 27개 + 기타 cron 들을: + +```ts +// packages/workers/jobs/rank-update.ts +queue.add('rank.update', { game: 'bacara' }, { repeat: { cron: '*/5 * * * *' } }); +queue.add('rank.update', { game: 'slots' }, { repeat: { cron: '*/5 * * * *' } }); +// ... +``` + +## 9. 첨부파일 (총 82.5GB) + +- 운영 서버에서 R2/S3 로 일회성 업로드 (rclone) +- 신규 시스템은 처음부터 직접 S3 에 저장 +- DB 의 `g5_board_file.bf_file` 경로 → S3 키로 변환 +- 마이그레이션 동안 폴백: missing key → 운영 서버 origin 으로 프록시 + +## 10. 단계별 마일스톤 + +| 단계 | 산출물 | 검증 | +|------|-------|------| +| **M0** (완료) | 소스 + DB 로컬 사본, PostgreSQL 셋업, 로컬 PHP 정상 가동 | 로그인 OK | +| **M1** (1~2주) | 모노레포 스캐폴딩, Drizzle 스키마, ETL 첫 패스 (members, posts, point), Next.js 기본 셸, 4-테마 토글 | 회원 로그인 (PBKDF2), 자유게시판 list/view | +| **M2** (3~4주) | 게시판 글쓰기·댓글·첨부, 마이페이지, 포인트, 회원 가입/탈퇴, 알림 | 핵심 게시판 동작 | +| **M3** (5~6주) | 슬롯/바카라/룰렛/복권 기능 이전, BullMQ 워커, 챗봇 | 게임 정상 동작 | +| **M4** (7~8주) | 영카트 쇼핑몰, 결제 게이트웨이, SMS, 본인인증, 소셜 로그인 | 주문/결제/SMS 검증 | +| **M5** (9~10주) | 관리자 페이지 (회원/게시판/베팅/룰렛 관리, **테마 선택 UI**) | 관리자 OTP 로그인 + 일상 운영 가능 | +| **M6** (11~12주) | 첨부파일 S3 이전, SEO 검사, 성능 부하 테스트 (k6), 보안 감사 | Lighthouse 90+ / 부하 테스트 통과 | +| **M7** (13주) | 카나리 트래픽 5% → 50% → 100% 절체. DNS cutover. | 라이브 | + +## 11. 롤백 +- 신규 시스템 cutover 후 **6주간** 기존 PHP 인프라 hot-standby 유지 +- DNS TTL 60s → 4시간 단계 상승 +- DB는 신규(PostgreSQL) → 구(MariaDB) 역방향 CDC (Debezium 또는 logical replication adapter) — 비상 시 MariaDB 로 1시간 내 복귀 가능 + +## 12. 보안 사항 +- 운영 DB 패스워드 (`iiOii5*^^*`), 토큰 키 (`ac57f676fe741f0ab3471d81dbee3bf1`), 서버 root 패스워드 모두 **신규 시스템 첫 배포 시 회전** +- ENV 변수는 `.env.local` 또는 Doppler / 1Password / AWS Secrets Manager 로 관리 +- 결제·본인인증 키는 운영 환경에만 주입 +- WAF (Cloudflare) + rate-limit (Hono middleware + Redis) diff --git a/docs/04-theme-architecture.md b/docs/04-theme-architecture.md new file mode 100644 index 0000000..5601bf8 --- /dev/null +++ b/docs/04-theme-architecture.md @@ -0,0 +1,204 @@ +# 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; + board: ComponentType; + post: ComponentType; + shop: ComponentType; + page: ComponentType; + }; + slots: { + Header: ComponentType; + Footer: ComponentType; + Sidebar: ComponentType; + PostList: ComponentType; + PostCard: ComponentType; + PostView: ComponentType; + LoginForm: ComponentType; + NewWindow: ComponentType; + // ...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//list.skin.html.php` | `slots/PostList.tsx` | +| `skin/board//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`) → `` +- 회원 랭킹 (`eyoom_ranking`) → `` +- 출석 (`eyoom_attendance`) → `` +- 배너 (`eyoom_banner`) → `` + +## 8. 아미나빌더 디자인 노트 +- 메가메뉴 (1단 큰 카테고리 + 2단 서브) +- 카드형 그리드 (3열 → 모바일 1열) +- 섹션별 스킨 시스템 (홈에 여러 게시판 위젯 동시 렌더) +- 라이트/다크 테마 토글 +- 인기 회원 랭킹, 실시간 댓글, 인기글 사이드바 + +## 9. 영카드 디자인 노트 +- 상단 카테고리 가로 네비 +- 메인 = 베스트 상품 그리드 + 쿠폰존 + 이벤트 슬라이더 +- 마이페이지: 주문/배송/쿠폰/적립금 +- 게시판은 영카트 기본 룩 (테이블형 리스트) + +## 10. 신규 테마 추가 절차 (확장성) +1. `packages/themes//` 폴더 생성, manifest 작성 +2. `ThemeRegistry.register({ id: '', loader: () => import('./') })` 추가 +3. `app_settings.theme.global` 후보에 자동 노출 +4. 별도 빌드 없이 hot reload (Next.js dev 모드) + +## 11. 성능 +- 각 테마 번들은 lazy chunk → 활성 테마만 다운로드 +- 테마 토큰은 `` 인라인 → FOUC 방지 +- RSC 단에서 테마 결정 → 클라이언트 hydration 최소화 diff --git a/docs/05-local-dev-setup.md b/docs/05-local-dev-setup.md new file mode 100644 index 0000000..a65d03d --- /dev/null +++ b/docs/05-local-dev-setup.md @@ -0,0 +1,194 @@ +# Local Development Setup + +## 0. 사전조건 +- macOS / Linux +- **Docker Desktop** 실행 중 +- **Homebrew** (PostgreSQL, pgloader 용) +- **Node.js 20+**, **pnpm** (신규 시스템 작업 시) + +## 1. 원본 PHP 사이트 로컬 가동 + +```bash +# 0) 사전: Docker 데스크톱 실행 / 5432 PostgreSQL 사용 가능 / 33306 미사용 +brew install postgresql@17 pgloader +brew services start postgresql@17 + +# 1) (한 번만) MariaDB 임시 컨테이너 띄우기 +docker network create slot-net 2>/dev/null +docker run -d --name slot-mariadb \ + --network slot-net \ + -e MARIADB_ROOT_PASSWORD=rootpass \ + -p 33306:3306 \ + -v slot-mariadb-data:/var/lib/mysql \ + --restart unless-stopped \ + mariadb:10.5 \ + --character-set-server=utf8mb4 \ + --collation-server=utf8mb4_unicode_ci \ + --max-allowed-packet=512M \ + --innodb-buffer-pool-size=1G + +# 2) DB dump 복원 (db/inspection2.sql.gz, db/inspection.sql.gz 있어야 함) +docker cp db/inspection2.sql.gz slot-mariadb:/tmp/ +docker cp db/inspection.sql.gz slot-mariadb:/tmp/ +docker exec slot-mariadb sh -c "mariadb -uroot -prootpass -e 'CREATE DATABASE inspection2 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE inspection DEFAULT CHARACTER SET utf8mb4;'" +docker exec slot-mariadb sh -c "gunzip -c /tmp/inspection2.sql.gz | mariadb -uroot -prootpass --default-character-set=utf8mb4 inspection2" +docker exec slot-mariadb sh -c "gunzip -c /tmp/inspection.sql.gz | mariadb -uroot -prootpass --default-character-set=utf8mb4 inspection" + +# 3) PHP + Apache + Redis 컨테이너 가동 +cd docker && docker compose up -d + +# 첫 실행 시 entrypoint 가 ~5분 정도 걸린다 (apt-get + composer install). +# 진행 상태: docker logs -f slot-php + +# 4) 사이트 접속 +open http://localhost:8088/ +open http://localhost:8088/adm/ # 관리자 (admin 계정 OTP 필요) +``` + +### 1.1 테스트 계정 (로컬에서만 사용) +원본 dump 의 admin 비밀번호는 알 수 없으므로 로컬 검증용 계정을 만들어 사용: + +```bash +# PBKDF2 해시 생성 +docker exec slot-php php -r ' +define("_GNUBOARD_", true); +include_once("/var/www/html/lib/pbkdf2.compat.php"); +echo create_hash("test1234"); echo PHP_EOL;' + +# 위 출력 해시를 변수에 담아 회원 추가 (level 10 = 일반회원, 12 = 최고관리자) +HASH='<위에서 출력된 해시>' +docker exec slot-mariadb mariadb -uroot -prootpass inspection2 -e " +INSERT IGNORE INTO g5_member + (mb_id, mb_password, mb_name, mb_nick, mb_email, mb_level, mb_datetime, mb_today_login, mb_open) +VALUES + ('testlogin', '$HASH', '테스트', '테스트', 'test@local.test', 10, NOW(), NOW(), 1);" +``` + +이제 `testlogin` / `test1234` 로 로그인 가능. 관리자는 admin 계정 비밀번호를 PBKDF2 해시로 직접 update 후 로그인 (운영 OTP 가 활성화되어 있어 OTP 단계 통과 필요). + +## 2. PostgreSQL 마이그레이션 (재실행) + +```bash +# 빈 DB 생성 +psql -h localhost -d postgres -c " +DROP DATABASE IF EXISTS slot; +CREATE DATABASE slot WITH ENCODING='UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE=template0; +DROP DATABASE IF EXISTS slot_legacy; +CREATE DATABASE slot_legacy WITH ENCODING='UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE=template0;" + +# pgloader 실행 +cd db +pgloader migrate_inspection2.load # ~6분, 165 테이블 +pgloader migrate_inspection.load # ~10초 + +# 검증 +psql -h localhost -d slot -c " +SELECT count(*) AS pg_tables, COALESCE(SUM(reltuples)::bigint,0) AS approx_rows +FROM pg_class c JOIN pg_namespace n ON c.relnamespace=n.oid +WHERE n.nspname='inspection2' AND c.relkind='r';" +``` + +## 3. 운영 서버에서 새 DB dump 가져오기 (옵션) + +`scripts/refresh-db.sh` (예시): + +```bash +#!/bin/bash +# 1) 원격에서 dump 생성 +sshpass -p "$SSH_PASS" ssh -p 21693 root@103.31.14.207 'bash -s' < db/remote_dump.sh +# 2) 로컬로 복사 +sshpass -p "$SSH_PASS" rsync -az -e 'ssh -p 21693' root@103.31.14.207:/tmp/slot_dbdump/ db/ +# 3) 로컬 MariaDB 로 재복원 (위 1.2 단계 반복) +``` + +운영 서버 SSH 패스워드는 환경변수 `SSH_PASS` 로만 주입하고 저장소에는 두지 말 것. + +## 4. 자주 쓰는 명령 + +```bash +# PHP 컨테이너 셸 +docker exec -it slot-php bash + +# Apache 에러 로그 +docker logs slot-php 2>&1 | grep -iE "error|warn|fatal" | tail -30 +# (php_errors.log 는 컨테이너 안 /var/log/apache2/php_errors.log 에 저장됨) +docker exec slot-php tail -f /var/log/apache2/php_errors.log + +# MariaDB 셸 +docker exec -it slot-mariadb mariadb -uroot -prootpass inspection2 + +# PostgreSQL 셸 +psql -h localhost -d slot +# 데이터는 schema 'inspection2' 안: +# slot=> SET search_path TO inspection2; SELECT count(*) FROM g5_member; + +# Redis 셸 +docker exec -it slot-redis redis-cli + +# 전체 스택 종료 +cd docker && docker compose down + +# 전체 스택 재시작 + 재초기화 (entrypoint 다시 실행) +cd docker && docker compose down && docker compose up -d +docker logs -f slot-php +``` + +## 5. 알려진 경고 (정상 동작과 무관) + +| 메시지 | 원인 | 해결 | +|--------|------|------| +| `Notice: Undefined index HTTP_X_FORWARDED_FOR` | common.php:2 의 부주의한 헤더 접근 | `display_errors=Off` 로 숨김 (현재 적용됨) | +| `Class 'Redis' not found` | php-redis 익스텐션 누락 | entrypoint 가 `pecl install redis` 자동 실행 | +| `Headers already sent ...` | display_errors 가 On 이고 PHP notice 출력 | display_errors Off (현재 적용됨) | +| `apache2: Could not reliably determine ...` | ServerName 미설정 (cosmetic) | 무시 | +| `pdo (pdo.so) is already loaded!` | Debian 기본 PHP 에 pdo 가 빌트인이라 두 번 로드 | 무시 | + +## 6. 운영 서버 / 로컬 차이점 요약 + +| 항목 | 운영 | 로컬 | +|------|------|------| +| URL | https://slot-ss.com | http://localhost:8088 | +| `G5_DOMAIN` | https://slot-ss.com (config.php) | env `G5_DOMAIN_OVERRIDE` | +| DB 호스트 | localhost | docker network "slot-net" / `slot-mariadb` | +| DB 비밀번호 | `iiOii5*^^*` (운영) | `rootpass` (로컬) | +| Redis | (운영 서버 redis) | `slot-redis` 컨테이너 | +| 첨부파일 | `/var/www/slot-ss.com/data/file/...`, `data/editor/...` | 로컬 src/data 는 빈 디렉토리 (실파일 없음) | +| 사용자 업로드 | 운영 그대로 | **없음** — 다운받지 않음 | + +## 7. 트러블슈팅 + +### 7.1 컨테이너가 unhealthy 또는 재시작 반복 +```bash +docker logs slot-php 2>&1 | tail -50 +# pecl install redis 가 빌드 의존성 문제로 실패할 수 있음 → entrypoint 가 || true 로 무시 +# 그래도 실패 시: +docker exec slot-php bash -lc 'apt-get update && apt-get install -y libssl-dev && pecl install redis && docker-php-ext-enable redis && apache2ctl restart' +``` + +### 7.2 사이트 500 +```bash +# php_errors.log 확인 (display_errors 는 Off 이므로 화면엔 안 뜸) +docker exec slot-php tail -50 /var/log/apache2/php_errors.log +``` + +### 7.3 DB 연결 실패 +```bash +# slot-mariadb 가 같은 네트워크에 있는지 확인 +docker network inspect slot-net +# 없으면: +docker network connect slot-net slot-mariadb +``` + +### 7.4 포트 충돌 +- 8088 사용 중이면 `docker/docker-compose.yml` 의 `ports: "8088:80"` 을 다른 포트로 변경 +- 33306 사용 중이면 위와 마찬가지 + +## 8. 신규 시스템 (Next.js) 개발 셋업 +(추후 M1 단계에서 추가 예정) + +```bash +# 모노레포 루트 +pnpm install +pnpm db:migrate +pnpm dev # apps/web (3000) + apps/admin (3001) + apps/api (4000) 동시 실행 +``` diff --git a/docs/06-feature-inventory.md b/docs/06-feature-inventory.md new file mode 100644 index 0000000..fb6c988 --- /dev/null +++ b/docs/06-feature-inventory.md @@ -0,0 +1,191 @@ +# Feature Inventory — slot-ss.com (슬롯/카지노 커뮤니티 도메인) + +이 문서는 신규 시스템 (Next.js + Node.js + PostgreSQL) 으로 옮겨야 할 모든 기능을 도메인별로 정리한다. 항목당 (1) 기존 위치, (2) DB 의존, (3) 신규 패키지 매핑. + +## 1. 회원·인증 + +| 기능 | 기존 | DB | 신규 패키지 | +|------|-----|-----|-------------| +| 일반 회원가입 | `bbs/register*.php` | `g5_member` | `apps/web/auth/register` | +| 로그인 / 로그아웃 | `bbs/login.php`, `login_check.php`, `logout.php` | `g5_member`, `g5_login` | `packages/auth` (NextAuth.js) | +| 비밀번호 찾기 | `bbs/password_lost.php` | `g5_member.mb_lost_certify` | `apps/web/auth/recovery` | +| 메일 인증 | (다양한 register*.php) | `g5_member.mb_email_certify` | `packages/auth/email-verify` | +| **2FA OTP** | `plugin/ask-otp/` | (별도 외부 OTP 서버) | `packages/auth/totp` | +| **본인인증 — OK-Name** | `plugin/okname/` | `g5_member_cert_history`, `g5_cert_history` | `packages/auth/okname` | +| **본인인증 — KCP cert** | `plugin/kcpcert/` | 동상 | `packages/auth/kcp` | +| **본인인증 — iNICert** | `plugin/inicert/` | 동상 | `packages/auth/inicert` | +| **소셜 로그인** | `plugin/sns/` (Naver/Kakao/Facebook) | `g5_member_social_profiles` | NextAuth providers | +| 회원 등급 (lv 1-12) | `g5_member.mb_level` + `g5_auth` | `g5_auth` | `packages/auth/permissions` | +| 차단/탈퇴 | `g5_member.mb_intercept_date`, `mb_leave_date` | 동상 | 동상 | +| 회원 검색/관리 | `adm/member_*.php` | `g5_member` | `apps/admin/members` | +| **레벨/랭크 시스템** | `bbs/*rank.php` (12종) + `lib/member.rank.lib.php` | `g5_eyoom_member`, 게임별 포인트 테이블 | `packages/games/ranking` | +| 출석체크 | `bbs/attendance.php`, `data/attendance.config.php` | `g5_eyoom_attendance` | `apps/web/attendance` | +| 회원 활동 로그 | `g5_eyoom_activity`, `writing_activity` | 동상 | `packages/shared/activity` | +| 회원 메모 (관리자 메모) | `g5_eyoom_mbmemo` | 동상 | `apps/admin/members/memo` | +| 옐로카드 (경고) | `g5_eyoom_yellowcard` | 동상 | `apps/admin/members/yellowcard` | + +## 2. 게시판 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 게시판 목록 | `bbs/group.php`, `bbs/board.php?bo_table=X` | `g5_group`, `g5_board` | `apps/web/[boardSlug]` | +| 글 작성/수정/삭제 | `bbs/write.php`, `write_update.php`, `delete.php` | `g5_write_` | `apps/web/[boardSlug]/write` + API | +| 글 보기 | `bbs/board.php?bo_table=X&wr_id=N` | `g5_write_` + `g5_board_file` | `apps/web/[boardSlug]/[wrId]` | +| 댓글 | `bbs/write_comment_update.php` | `g5_write_` (is_comment) | 동상 | +| 추천/비추천 | `bbs/good.php`, `bbs/bad.php` | `g5_board_good` | API | +| 첨부 파일 업로드 | `lib/board.lib.php` | `g5_board_file` | `packages/shared/upload` (S3) | +| 글 검색 | `bbs/search.php`, `lib/parsing.lib.php`, `lib/SphinxSearch.class.php` | sphinx index | Meilisearch 또는 PG FTS | +| 인기 검색어 | `bbs/popular*.php`, `lib/popular.lib.php` | `g5_popular` | API + Redis | +| 신규글 위젯 | `lib/latest.lib.php` | `g5_board_new` | RSC + ISR | +| 인기글 / 추천글 | `lib/eblatest.*` | `g5_eyoom_*` | 동상 | +| 글 임시저장 | `bbs/autosave.php` | `g5_autosave` | localStorage + 서버 백업 | +| 스크랩 | `bbs/scrap_popin.php` | `g5_scrap` | 동상 | +| 1:1 문의 | `bbs/qa*.php` | `g5_qa_config`, `g5_qa_content` | `apps/web/help/qa` | +| 자주묻는질문 | `bbs/faq.php` | `g5_faq`, `g5_faq_master` | `apps/web/help/faq` | +| 메모(쪽지) | `bbs/memo*.php` | `g5_memo` | `apps/web/inbox` | +| 새창 알림 | `bbs/new.php` | `g5_new_win` | `apps/admin/popups` | +| 게시판 새글 표시 | (자동 — `g5_board_new`) | 동상 | 동상 | +| 멀티미디어 게시판 (`webtoon`, `ai`) | board skin 변형 | 게시판 데이터 | 테마 슬롯 + `` | + +### 운영 게시판 36종 +| 카테고리 | 게시판 슬러그 | +|---------|----------| +| 종합 / 자유 | `free`, `humor`, `notice`, `event`, `column`, `news` | +| 슬롯 후기/리뷰 | `review`, `slotreview`, `pick` | +| 슬롯 제조사 | `slotche`, `slotche2~12` (프라그마틱, 플레이엔고, 릴랙스게이밍, 하바네로, CQ9 등) | +| 게임/이벤트 | `lottery_ticket`(슬생복권), `dividend`(배당), `gift_coupons`/`gift_exchanges`(기프티콘) | +| 검수/검증 | `mukti`(먹튀사이트), `fakesite`(가품사이트), `complaint`(먹튀신고), `inspection`(검수), `guarantee`(보증) | +| AI / 19금 | `ai`(AI 사진), `webtoon`(번역망가), `rear`(후방) | +| 안내 | `guide`, `qa`, `reservation` | +| 가짜 (테스트?) | `fakes`, `fakeframe` | + +## 3. 슬롯/카지노 게임 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| **바카라 게임** | `plugin/bacara/`, `bbs/bacara.php` | `bacara_betting` | `packages/games/bacara` + WebSocket | +| 바카라 랭킹 | `bbs/bacararank.php` | 동상 | `apps/web/games/bacara/rank` | +| **룰렛 게임** | `roulette/index.php`, `plugin/roulette/` | `g5_eyoom_*` | `packages/games/roulette` | +| 룰렛 어드민 | `adm/roulette/{roulettelist,form,rewardlist,chancelist}.php` | 동상 | `apps/admin/games/roulette` | +| **복권 (슬생복권)** | `lottery/index.php`, `bok.php`, `lib/lottery.lib.php` | `g5_write_lottery_ticket`, `lottery_history` | `packages/games/lottery` | +| 복권 추첨 | `adm/roulette/lotterywinninglist.php` | 동상 | BullMQ cron + 어드민 | +| **Swiun 슬롯 API** | `plugin/swiunApi/` (`trans_point`, `game`, `get_betlist`, `_config`) | `swiun_betting`, `game_point` | `packages/games/swiun` | +| **게임 포인트** | (전반적) | `game_point` (4.4M rows), `g5_point` (5.9M) | `point_ledger` 통합 | +| **27개 게임별 랭크 자동 업데이트** | `plugin/cron/auto.{slot,bacara,marilyn,...}rankup.php` (27개) | `g5_eyoom_member` + 게임 포인트 | `packages/workers/jobs/rank.update.ts` | +| 가짜 게임 포인트 (개발용?) | `plugin/cron/fake_game_points.php` | 동상 | dev-only 시드 | + +## 4. 검수·블랙리스트 (커뮤니티 신뢰) + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 먹튀 사이트 등록/검색 | `bbs/inspection.php`, `inspection.list.update.php`, `inspection.update.php`, `bbs/cagumsa.php`(가검사) | `blacklist_table` (193K), `blacklist_search`, `check_table` | `apps/web/blacklist` | +| 블랙리스트 어드민 | `adm/eyoom_admin/core/board/blacklist_search.php` | 동상 | `apps/admin/blacklist` | +| 100up 블랙리스트 (구버전) | `db100up/` | 동상 | (legacy, 사용 안 함) | +| 검수 로그 | `writing_activity`, `writing_activity_bak`, `writing_today` | 동상 | `inspection_logs` | + +## 5. 포인트·환전 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 포인트 적립/사용 원장 | `lib/common.lib.php::insert_point()` | `g5_point` | `point_ledger` | +| 포인트 환전 신청 | (회원 페이지) | `point_exchange`, `event_point_exchange` | `apps/web/wallet/exchange` | +| 기프티콘 교환 | `bbs/giftcon*.php` | `g5_write_gift_coupons`, `g5_write_gift_exchanges`, `officecon_products` | `apps/web/wallet/gifticon` | +| 슬롯버프 (특별 보상) | `bbs/slotbuff*.php` | `slotbuff`, `slotbuff-category` | `apps/web/wallet/slotbuff` | +| 베팅 어드민 | `adm/betting_list.php`, `betting_list_update.php` | `bacara_betting`, `swiun_betting`, `game_point` | `apps/admin/betting` | + +## 6. 영카트 쇼핑몰 (현재 데이터 거의 없음, 기능은 보존) + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 상품 등록/수정 | `adm/shop_admin/item_*.php` | `g5_shop_item`, `g5_shop_item_option` | `apps/admin/shop/items` | +| 카테고리 | `shop/list.php`, `adm/shop_admin/categoryform.php` | `g5_shop_category` | `apps/admin/shop/categories` | +| 브랜드 | `shop/brand.php` | (`g5_shop_item.it_brand`) | 동상 | +| 장바구니 | `shop/cart.php` | `g5_shop_cart` | `apps/web/cart` | +| 주문/결제 | `shop/orderform*.php`, `shop/inicis/*` | `g5_shop_order`, `g5_shop_order_data`, `g5_shop_inicis_log` | `apps/web/checkout` | +| 결제 게이트웨이 — 이니시스 | `shop/inicis/`, `shop/kcp/` | 동상 | `packages/shop/payments/inicis` | +| 결제 게이트웨이 — KCP | `shop/kcp/`, `plugin/kcpcert/` | 동상 | `packages/shop/payments/kcp` | +| 결제 게이트웨이 — LG U+ | `plugin/lgxpay/` | 동상 | `packages/shop/payments/lgxpay` | +| 쿠폰 | `g5_shop_coupon`, `g5_shop_coupon_zone`, `g5_shop_coupon_log` | 동상 | `apps/web/coupons` | +| 위시리스트 | `shop/wishlist.php` | `g5_shop_wish` | 동상 | +| 상품 후기 | `shop/itemuse*.php` | `g5_shop_item_use` | 동상 | +| 상품 Q&A | `shop/itemqa*.php` | `g5_shop_item_qa` | 동상 | +| 재입고 SMS | `g5_shop_item_stocksms` | 동상 | `packages/shop/restock-notify` | + +## 7. SMS / 메일 / 알림 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| SMS 발송 (Aligo) | `plugin/sms5/`, `data/aligo-sms.data` | `sms5_*` (`book`, `book_group`, `config`, `form`, `form_group`, `history`, `write`) | `packages/notify/sms` | +| 이메일 발송 | `plugin/PHPMailer/` | `g5_mail` | `packages/notify/email` (Resend/SES) | +| 푸시 알림 | `plugin/notifier/`, `g5_plugin_notifier_hist` | 동상 | `packages/notify/push` (Web Push) | +| Slack 알림 | `plugin/slack/` | — | `packages/notify/slack` | + +## 8. 챗봇 (AI) + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 챗봇 대화 | `plugin/chatbot/api_proxy.php`, `view.php`, `script.js`, `style.css` | `chatbot_conversations` | `packages/chatbot` (OpenAI/Anthropic) | +| 챗봇 피드백 | `plugin/chatbot/feedback.php` | `chatbot_feedback` | 동상 | +| 챗봇 로그 | `plugin/chatbot/log.php` | 동상 | 동상 | + +## 9. SEO·마케팅 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| SEO 메타 (페이지/게시판별) | `data/eyoom.seocfg.php`, `theme/.../meta_config.php`, `theme/.../seo.map.php` | `ask_seo`, `ask_seo_url` | `packages/shared/seo` + Next.js metadata API | +| robots.txt / sitemap | `robots.txt` | — | Next.js sitemap.ts | +| Naver Syndication | `plugin/syndi/` | — | `packages/notify/naver-syn` | +| Google Indexing API | `google_indexing-api.php`, `silicon-cocoa-*.json` | — | API 라우트 | +| OG 이미지 / Twitter 카드 | `head.html.php` 인라인 | — | Next.js Open Graph | +| 도메인 이력 / 도메인체크 | `domainCheck.php`, `domainCheck2.php`, `ajax.domainCheck.php`, DB `DomainCheck` | (별도 DB, 거의 빈 상태) | `apps/web/tools/domain-check` (옵션) | + +## 10. 관리자 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 관리자 대시보드 | `adm/index.php` | (요약쿼리) | `apps/admin/dashboard` | +| 회원 관리 | `adm/member_*.php` | `g5_member` | `apps/admin/members` | +| 게시판 관리 | `adm/boardgroup_*.php`, `board_*.php` | `g5_board`, `g5_group` | `apps/admin/boards` | +| 환경설정 | `adm/config_*.php` | `g5_config` | `apps/admin/settings` | +| 메뉴 관리 | `adm/menu_*.php` | `g5_menu` | `apps/admin/menu` | +| 권한 관리 | `adm/auth_*.php` | `g5_auth` | `apps/admin/permissions` | +| 점프 (관리자 전환) | (이윰) | `g5_eyoom_manager` | `apps/admin/managers` | +| 베팅/룰렛/복권 어드민 | (위 §3) | (위 §3) | `apps/admin/games/*` | +| 통계 (방문/회원/포인트) | `adm/visit_*.php` | `g5_visit`, `g5_visit_sum`, `g5_login` | `apps/admin/stats` | +| **테마 관리 (4종 선택)** | (없음 — 이윰 빌더 내부) | (신규) `app_settings` | `apps/admin/themes` | + +## 11. 기타 / 인프라 + +| 기능 | 기존 | DB | 신규 | +|------|-----|-----|------| +| 캐시 (Sphinx Search) | `lib/SphinxSearch.class.php`, `sphinx.conf` | sphinx index | Meilisearch | +| 캐시 (Redis) | `lib/RedisCache.class.php` | redis | Redis (그대로) | +| 캐시 (파일) | `lib/Cache/` | `data/cache/` | Redis | +| 훅 시스템 | `lib/Hook/` (`run_event`, `run_replace`) | — | EventEmitter / Plugin pattern | +| reCAPTCHA | `plugin/recaptcha/`, `plugin/recaptcha_inv/` | — | next-recaptcha-v3 | +| 자체 캡차 | `plugin/kcaptcha/` | — | (필요 시) | +| 디버그바 | `plugin/debugbar/` | — | dev only — Next.js devtools | +| jQuery 차트 | `plugin/jqplot/` | — | Recharts / Chart.js | +| HTML 정화 | `plugin/htmlpurifier/` | — | DOMPurify (server side: sanitize-html) | + +## 12. 도메인 데이터 통계 (현재 운영) + +| 항목 | 값 | +|------|-----| +| 회원 수 | 3,064 | +| 관리자 수 | 4 (`admin`, `admin2`, `admin3`, `admin4`) | +| 운영 게시판 수 | 36 | +| 총 게시글 수 (모든 g5_write_*) | ~764K | +| 자유게시판 글 / 댓글 | 90,591 / 351,685 | +| 후기게시판 글 / 댓글 | 8,399 / 112,462 | +| 먹튀사이트 글 / 댓글 | 1,824 / 35,501 | +| 가품사이트 글 / 댓글 | 1,517 / 31,609 | +| 포인트 원장 행 수 | 5,950,013 | +| 게임 포인트 행 수 | 4,383,542 | +| 슬롯 게임 회차 (games_table) | 303,238 | +| 바카라 베팅 | 7,987 | +| 복권 응모 | 45,724 | +| 블랙리스트 | 193,190 | +| 누적 방문 | 4,566,650 | +| 첨부파일 메타 | 12,606 | +| 첨부파일 실용량 (서버) | data/file 8.5GB + data/editor 74GB | diff --git a/next-app/.env.example b/next-app/.env.example new file mode 100644 index 0000000..3514af4 --- /dev/null +++ b/next-app/.env.example @@ -0,0 +1,28 @@ +## Postgres (the migrated DB) +DATABASE_URL="postgresql://chpark@localhost:5432/slot" +DATABASE_LEGACY_URL="postgresql://chpark@localhost:5432/slot_legacy" + +## Redis (cache + BullMQ) +REDIS_URL="redis://localhost:6379" + +## Auth +AUTH_SECRET="change-me-32-chars-minimum-please-rotate" +AUTH_URL="http://localhost:3000" + +## App +NEXT_PUBLIC_APP_URL="http://localhost:3000" +NEXT_PUBLIC_API_URL="http://localhost:4000" + +## Theme default (overridable in DB app_settings) +THEME_DEFAULT="eyoom" + +## S3-compatible object storage (R2 / MinIO / AWS) +S3_ENDPOINT="" +S3_REGION="auto" +S3_BUCKET="slot-uploads" +S3_ACCESS_KEY="" +S3_SECRET_KEY="" + +## OpenAI / Anthropic (chatbot) +OPENAI_API_KEY="" +ANTHROPIC_API_KEY="" diff --git a/next-app/apps/web/next-env.d.ts b/next-app/apps/web/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/next-app/apps/web/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next-app/apps/web/next.config.mjs b/next-app/apps/web/next.config.mjs new file mode 100644 index 0000000..76fdd1b --- /dev/null +++ b/next-app/apps/web/next.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['@slot/themes', '@slot/db', '@slot/auth'], + serverExternalPackages: ['postgres', '@node-rs/argon2'], + // Backwards-compatible 301 redirects from gnuboard URLs handled by middleware +}; +export default nextConfig; diff --git a/next-app/apps/web/package.json b/next-app/apps/web/package.json new file mode 100644 index 0000000..20f78e9 --- /dev/null +++ b/next-app/apps/web/package.json @@ -0,0 +1,30 @@ +{ + "name": "@slot/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev -p 3000", + "build": "next build", + "start": "next start -p 3000", + "lint": "next lint" + }, + "dependencies": { + "@slot/db": "workspace:*", + "@slot/auth": "workspace:*", + "@slot/themes": "workspace:*", + "next": "^15.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "drizzle-orm": "^0.36.4", + "postgres": "^3.4.5" + }, + "devDependencies": { + "typescript": "^5.7.2", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "@types/node": "^22.10.0", + "eslint": "^9.17.0", + "eslint-config-next": "^15.1.0" + } +} diff --git a/next-app/apps/web/src/app/[boardSlug]/[wrId]/page.tsx b/next-app/apps/web/src/app/[boardSlug]/[wrId]/page.tsx new file mode 100644 index 0000000..89e0a1b --- /dev/null +++ b/next-app/apps/web/src/app/[boardSlug]/[wrId]/page.tsx @@ -0,0 +1,45 @@ +import { notFound } from 'next/navigation'; +import { getBoardMeta, getPost } from '@/lib/legacy-board'; +import { getThemeForPath } from '@/lib/theme'; +import { headers } from 'next/headers'; + +export const dynamic = 'force-dynamic'; + +export default async function PostPage({ params }: { params: Promise<{ boardSlug: string; wrId: string }> }) { + const { boardSlug, wrId } = await params; + const meta = await getBoardMeta(boardSlug); + if (!meta) return notFound(); + const post = await getPost(boardSlug, wrId); + if (!post) return notFound(); + const h = await headers(); + const theme = await getThemeForPath(h.get('x-pathname') ?? `/${boardSlug}/${wrId}`); + const PostView = theme.slots.PostView; + const commentsTree = ( +
+

댓글 ({post.comments.length})

+ {post.comments.length === 0 &&

아직 댓글이 없습니다.

} + {post.comments.map((c, i) => ( +
+
+ {c.authorName} · {new Date(c.createdAt).toLocaleString('ko-KR')} +
+
+
+ ))} +
+ ); + return ( + + ); +} diff --git a/next-app/apps/web/src/app/[boardSlug]/page.tsx b/next-app/apps/web/src/app/[boardSlug]/page.tsx new file mode 100644 index 0000000..2e15c42 --- /dev/null +++ b/next-app/apps/web/src/app/[boardSlug]/page.tsx @@ -0,0 +1,19 @@ +import { notFound } from 'next/navigation'; +import { getBoardMeta, listPosts } from '@/lib/legacy-board'; +import { getThemeForPath } from '@/lib/theme'; +import { headers } from 'next/headers'; + +export const dynamic = 'force-dynamic'; + +export default async function BoardPage({ params, searchParams }: { params: Promise<{ boardSlug: string }>; searchParams: Promise<{ page?: string }> }) { + const { boardSlug } = await params; + const sp = await searchParams; + const page = Math.max(1, parseInt(sp.page ?? '1', 10) || 1); + const meta = await getBoardMeta(boardSlug); + if (!meta) return notFound(); + const { items, totalPages } = await listPosts(boardSlug, page); + const h = await headers(); + const theme = await getThemeForPath(h.get('x-pathname') ?? `/${boardSlug}`); + const PostList = theme.slots.PostList; + return ; +} diff --git a/next-app/apps/web/src/app/admin/themes/page.tsx b/next-app/apps/web/src/app/admin/themes/page.tsx new file mode 100644 index 0000000..6480fe2 --- /dev/null +++ b/next-app/apps/web/src/app/admin/themes/page.tsx @@ -0,0 +1,69 @@ +import { listThemes, THEME_LABELS, type ThemeId } from '@slot/themes'; +import { getActiveGlobalTheme, setActiveGlobalTheme } from '@/lib/theme'; +import { revalidatePath } from 'next/cache'; +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +async function selectTheme(formData: FormData) { + 'use server'; + const id = String(formData.get('themeId') ?? '') as ThemeId; + if (!['basic', 'eyoom', 'amina', 'youngcart'].includes(id)) return; + await setActiveGlobalTheme(id); + revalidatePath('/', 'layout'); + redirect('/admin/themes?ok=1'); +} + +export default async function AdminThemesPage({ searchParams }: { searchParams: Promise<{ ok?: string }> }) { + const sp = await searchParams; + const active = await getActiveGlobalTheme().catch(() => 'eyoom' as ThemeId); + const themes = listThemes(); + return ( +
+

테마 관리

+

+ 사용자에게 보여줄 사이트 전체 테마를 선택하세요. 4종 중 하나를 선택할 수 있습니다. +

+ {sp.ok &&
저장되었습니다. 페이지를 새로고침하여 확인하세요.
} + +
+ {themes.map((t) => ( +
+ + +
+ ))} +
+ +

+ ※ 영역별 오버라이드 (게시판/페이지마다 다른 테마)는 향후 추가 예정입니다. +

+
+ ); +} diff --git a/next-app/apps/web/src/app/api/auth/login/route.ts b/next-app/apps/web/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..6a9d9d3 --- /dev/null +++ b/next-app/apps/web/src/app/api/auth/login/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { login, SESSION_COOKIE } from '@slot/auth'; +import { getRequestIp } from '@/lib/session'; + +export async function POST(req: NextRequest) { + const form = await req.formData(); + const loginId = String(form.get('loginId') ?? '').trim(); + const password = String(form.get('password') ?? ''); + if (!loginId || !password) { + return NextResponse.redirect(new URL('/login?error=1', req.url), { status: 303 }); + } + const ip = await getRequestIp(); + const result = await login(loginId, password, { ip, userAgent: req.headers.get('user-agent') ?? undefined }); + if (!result.ok) { + return NextResponse.redirect(new URL('/login?error=1', req.url), { status: 303 }); + } + const res = NextResponse.redirect(new URL('/', req.url), { status: 303 }); + res.cookies.set(SESSION_COOKIE, result.session.id, { + httpOnly: true, + sameSite: 'lax', + path: '/', + expires: result.session.expiresAt, + }); + return res; +} diff --git a/next-app/apps/web/src/app/api/auth/logout/route.ts b/next-app/apps/web/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..7b93a40 --- /dev/null +++ b/next-app/apps/web/src/app/api/auth/logout/route.ts @@ -0,0 +1,10 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { destroySession, SESSION_COOKIE } from '@slot/auth'; + +export async function POST(req: NextRequest) { + const sid = req.cookies.get(SESSION_COOKIE)?.value; + if (sid) await destroySession(sid).catch(() => {}); + const res = NextResponse.redirect(new URL('/', req.url), { status: 303 }); + res.cookies.delete(SESSION_COOKIE); + return res; +} diff --git a/next-app/apps/web/src/app/layout.tsx b/next-app/apps/web/src/app/layout.tsx new file mode 100644 index 0000000..9d5ff9c --- /dev/null +++ b/next-app/apps/web/src/app/layout.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from 'next'; +import { headers } from 'next/headers'; +import { getThemeForPath } from '@/lib/theme'; +import { tokensToCssVars } from '@slot/themes'; +import { getCurrentMember } from '@/lib/session'; + +export const metadata: Metadata = { + title: 'Slot - 슬롯 커뮤니티', + description: '안전 슬롯사이트 추천 및 슬롯커뮤니티', +}; + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const h = await headers(); + const pathname = h.get('x-pathname') ?? '/'; + const member = await getCurrentMember(); + const theme = await getThemeForPath(pathname, member?.themePref as any); + const Root = theme.layouts.root; + return ( + + +