macbook 에서 리눅스로 이동

This commit is contained in:
k3341095
2026-03-05 10:35:28 +09:00
commit ffd13c0fbb
83 changed files with 12262 additions and 0 deletions

31
docs/api-spec.md Normal file
View File

@@ -0,0 +1,31 @@
# API 계약(1차)
## 공개 API
- `GET /api/public/route-by-host`
- Query: `host`, `path`
- Response:
- `status`: `ok | not_found`
- `page`: block json
- `campaignId`, `pageId`, `routeId`
- `POST /api/public/lead`
- Body: `campaignId`, `pageId`, `routeId`, `payload`, `sourceMeta`
## 관리자 API
- 인증: `POST /api/admin/auth/login` (현재는 역할 기반 데모 토큰 반환)
- 캠페인: `GET /api/admin/campaigns`, `POST /api/admin/campaigns`, `PUT /api/admin/campaigns/:id`
- 페이지: `GET /api/admin/pages`, `GET /api/admin/pages/:id`, `POST /api/admin/pages`, `PUT /api/admin/pages/:id`
- 라우트: `GET /api/admin/pages/:id/routes`, `POST /api/admin/pages/:id/routes`
- 조건: `GET /api/admin/pages/:id/conditions`, `POST /api/admin/pages/:id/conditions`, `PATCH /api/admin/pages/:id/conditions/:conditionId`, `DELETE /api/admin/pages/:id/conditions/:conditionId`
- 리드: `GET /api/admin/leads`
- 사용자: `GET /api/admin/users`, `POST /api/admin/users`, `PUT /api/admin/users/:id/role`
## 역할/권한(1차)
- 헤더: `x-user-role`
- 값: `ADMIN`, `LEAD_MANAGER`
- `ADMIN`: 관리자 API 전체 사용
- `LEAD_MANAGER`: 리드 조회(`/api/admin/leads`)만 사용
## 공통 에러
- 401 `{ error: "unauthorized" }`
- 403 `{ error: "forbidden" }`
- 422 `{ error: "validation", details: [...] }`

137
docs/database.md Normal file
View File

@@ -0,0 +1,137 @@
# DB 설계(1차)
## 핵심 엔티티
- Campaign(종목), LandingPage, LandingRoute, RouteCondition, Lead, User
- 역할: ADMIN / LEAD_MANAGER
## 엔티티 관계
- Campaign 1:N LandingPage
- Campaign 1:N LandingRoute
- LandingRoute 1:N RouteCondition
- LandingRoute N:1 LandingPage(기본 페이지)
- RouteCondition N:1 LandingPage(조건 매칭 페이지)
- LandingPage 1:N Lead
- Campaign 1:N Lead
## 권한 정책
- ADMIN: 모든 관리자 기능
- LEAD_MANAGER: 리드 조회만 가능
## Prisma 스키마 (SQLite 기준)
```prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
enum UserRole {
ADMIN
LEAD_MANAGER
}
model Campaign {
id String @id @default(cuid())
name String
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
landingPages LandingPage[]
routes LandingRoute[]
leads Lead[]
}
model LandingPage {
id String @id @default(cuid())
campaignId String
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
name String
slug String?
blocks Json
isDefault Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
defaultRoutes LandingRoute[] @relation("RouteDefaultPage")
conditions RouteCondition[]
leads Lead[]
@@index([campaignId, isActive])
}
model LandingRoute {
id String @id @default(cuid())
campaignId String
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
host String
path String
defaultPageId String
defaultPage LandingPage @relation("RouteDefaultPage", fields: [defaultPageId], references: [id], onDelete: Restrict)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
conditions RouteCondition[]
leads Lead[]
@@unique([host, path])
}
model RouteCondition {
id String @id @default(cuid())
routeId String
route LandingRoute @relation(fields: [routeId], references: [id], onDelete: Cascade)
pageId String
page LandingPage @relation(fields: [pageId], references: [id], onDelete: Restrict)
label String
priority Int @default(0)
isActive Boolean @default(true)
startDate DateTime?
endDate DateTime?
timezone String @default("Asia/Seoul")
weekMask String @default("0000000")
startMinute Int?
endMinute Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Lead {
id String @id @default(cuid())
campaignId String
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
pageId String
page LandingPage @relation(fields: [pageId], references: [id], onDelete: Restrict)
routeId String?
route LandingRoute? @relation(fields: [routeId], references: [id], onDelete: SetNull)
payload Json
sourceMeta Json?
submittedAt DateTime @default(now())
@@index([campaignId, submittedAt])
}
model User {
id String @id @default(cuid())
email String @unique
name String?
password String
role UserRole @default(ADMIN)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
### 참고
- `defaultRoutes``@relation("RouteDefaultPage")`로 라우트 기본 페이지 역참조입니다.
- 동일 엔티티에서 기본 라우트를 찾을 때는 `LandingRoute.defaultPageId`가 FK입니다.
- PostgreSQL 전환 시 datasource `provider`만 교체하지 않고 nullable/시간대 처리 포함 마이그레이션 검증이 필요하다.

View File

@@ -0,0 +1,29 @@
# 라우팅/조건 매칭(1차: exact)
## 라우팅 기본값
- `host + path` exact 매칭(정확 일치) 사용
- 기본 도메인: `aaa.com`
- 기본 라우트 path: `/`
- 예시: `aaa.com` + `/` , `aaa.com/google`
## 렌더 우선순위
1. `landingRoute` 조회 (`host`, `path`, `isActive`)
2. 해당 route의 `RouteCondition` 중 active 조건만 필터
3. 요일/시간/날짜 조건 일치 검사
4. `priority desc`, `updatedAt desc`로 정렬 후 첫 번째 사용
5. 조건 미매칭 시 기본 페이지 사용
## 호스트 정규화
- 요청 host는 소문자 변환, `www.` 제거, 포트(`:4000`, `:3000`) 분리 후 비교
- path는 정확 일치(`exact`) 방식
## 조건 스펙
- weekday: `weekMask` 7자리 문자열(일~토: `sun`~`sat`)에서 1이면 매칭
- time: `startMinute`~`endMinute` (0~1439)
- `start <= end` : 당일 범위
- `start > end` : 자정을 넘는 범위(예: 20:00~06:00)
- date: `startDate`, `endDate`
## 블록 정책(빌더)
- 블록형 구성 + 드래그 정렬
- 카카오 버튼 블록은 `kakaoSyncCode`를 페이지 블록 설정에 저장

37
docs/strategy.md Normal file
View File

@@ -0,0 +1,37 @@
# 랜딩 페이지 SaaS 전략 개요
작성일: 2026-03-01
## 제품 방향
- 광고 운영자가 랜딩 페이지를 빠르게 생성·수정·배포할 수 있는 SaaS
- 핵심 가치: 빠른 제작 + 안정적인 발행 + 팀 운영 + 리드 관리
## 기술 스택
- Nuxt 4 + Nuxt UI + Pinia
- Elysia (API)
- Prisma + SQLite(개발) + PostgreSQL(운영 전환 예정)
- Bun
## MVP 우선순위
1. 사용자/역할 관리
2. 캠페인/페이지 CRUD
3. 블록형 빌더
4. 도메인+경로 라우팅
5. 조건 탭(요일/시간) 기반 페이지 분기
6. 리드 조회
## 추후 확장(현재 1차 제외)
- 버전관리/롤백
- 예약 발행
- 내부 분석 대시보드
- 결제/구독
- 다국어/SEO 고도화
## 개발 규칙
- TypeScript strict
- 공식 생성 방식 준수
- Nuxt: 공식 nuxi 초기화/디렉토리
- Elysia: 공식 앱 초기화/미들웨어 등록
- Prisma: `prisma init` + schema + migrate
- Bun: 공식 실행 패턴

31
docs/ui-flow.md Normal file
View File

@@ -0,0 +1,31 @@
# 화면/라우트 설계(1차)
## 공개 라우트
- `/{path?}`: 공개 랜딩 진입점(호스트+경로로 렌더 대상 결정)
- `/_lead/success`, `/_lead/error`: 제출 결과 화면
- `/admin`: 관리자 홈
## 관리자 라우트(현재 구현)
- `/admin/login`: 역할 선택 로그인(데모 토큰)
- `/admin`: 대시보드
- `/admin/campaigns`: 캠페인 목록/생성
- `/admin/pages`: 페이지 목록/생성
- `/admin/pages/[id]/builder`: 페이지 블록 빌더
- `/admin/pages/[id]/routes`: 도메인 라우트 관리
- `/admin/pages/[id]/conditions`: 요일/시간 조건 관리
- `/admin/leads`: 리드 조회
- `/admin/builder/[id]`: 과거 호환용 리다이렉트 경로
## 1차 최소 화면
- 로그인
- 대시보드
- 캠페인 목록/생성
- 페이지 목록/생성/빌더 연결
- 리드 조회
## 다음 구현 우선순위
- 공개 노출 라우트(이미지/폼/버튼/푸터 블록 렌더)
- 블록 기반 랜딩 폼 제출 처리 개선(검증, 중복 제출)
- 조건 탭(요일/시간/기간 기반 룰 편집) 품질 개선
- 라우트/도메인 매핑 UI 고도화
- 사용자/역할 관리(리드관리자 역할 제한)