Content is user-generated and unverified.

BrandOS 퍼스널브랜딩 솔루션

완전 개발 가이드 v1.0

PHP 8.2+ / MariaDB 10.11+ / 멀티테넌트 SaaS 패키지
최종 업데이트: 2025


목차

  1. 프로젝트 개요 및 철학
  2. 경쟁 차별화 전략
  3. 시스템 아키텍처
  4. 데이터베이스 설계
  5. 디렉토리 구조
  6. 핵심 기능 상세 설계
  7. 템플릿 및 AI 스타일 시스템
  8. 관리자 패널 구성
  9. 보안 설계
  10. 성능 최적화
  11. 개발 로드맵
  12. 패키지 출시 체크리스트

1. 프로젝트 개요 및 철학

1-1. 기본 정보

항목내용
솔루션명BrandOS
접속 방식https://myoc.kr/{tenant_slug} (파라미터형 멀티테넌트)
주요 타겟정치지망생, 퇴직 공무원/공기업인, 1인 기업/브랜드
개발 스택PHP 8.2+, MariaDB 10.11+, Redis(캐시/세션), Nginx

1-2. 핵심 철학

"홈페이지 만드는 도구가 아니라, 찍으면 알아서 퍼지는 나만의 발행 시스템"

  • 윅스/아임웹 = 집 짓기 도구 → 만들고 나면 방치
  • BrandOS = 내가 움직이는 곳마다 자동으로 기록되고 알려지는 시스템

사용자가 하는 일: 사진 선택 → 한 줄 메모 → 올리기 버튼 하나
솔루션이 하는 일:

  • 내 브랜드 홈에 게시 (SEO 누적)
  • 인스타그램 자동 발행
  • 카카오스토리 자동 발행
  • 회원들에게 알림
  • 구글 검색에 내 이름이 쌓임

2. 경쟁 차별화 전략

2-1. 경쟁사 한계 분석

경쟁사한계
윅스/아임웹만들고 나면 끝. 운영·발행·SNS 연동은 사용자 몫 → 방치
AI 뚝딱 홈페이지생성은 쉬우나 콘텐츠가 살아 움직이지 않음 → 예쁜 명함
SNS 직접 운영플랫폼 알고리즘 종속, 계정 정지 시 전부 소멸, 플랫폼별 반복 노동

2-2. 5대 차별화 전략

① Zero Friction 입력 설계

  • 에디터를 없앰. 사진 → EXIF 날짜·장소 자동 추출 → 한 줄 메모 → 완료
  • 모바일 첫 화면 = 카메라롤 접근
  • 긴 글은 옵션, 기본은 사진+한줄+태그

② SEO 자동 누적

  • 올릴수록 검색 노출이 쌓이는 구조
  • Person 스키마, OG 태그 자동 생성
  • 올리는 행위 자체가 SEO 작업

③ SNS는 "복사"가 아닌 "연장" (플랫폼 독립 전략)

  • 원본은 내 홈에 존재
  • SNS는 트래픽을 내 홈으로 끌어오는 유입 채널
  • 계정이 막혀도 내 콘텐츠는 내 홈에 영구 보존

④ 타겟 특화 기능

  • 활동 연표: 연도별 활동 자동 정리 (이력 아카이빙)
  • 지지자 관계 관리: 생일 알림, 포인트(감사 표현)
  • 구역/지역 태깅: "○○구 활동" 자동 필터
  • 공약/약속 게시판: 진행 상태 추적 가능한 전용 스킨

⑤ 운영 부담 제로

  • 게시물 예약 발행
  • 자동 콘텐츠 리마인더 ("2주째 새 소식이 없어요")
  • 통계는 문장으로 표시 ("이번 주 127명이 홍보사진을 봤어요")

2-3. 공통 콘텐츠 허브 전략

중앙에서 고퀄리티 영상/정보성 게시물을 올리면 전체 테넌트 사이트에 노출.
테넌트별 URL로 공유 시 테넌트 브랜딩이 유지된 채로 전달됨.

공통 게시물 원본: https://myoc.kr/_common/health/123
홍길동 테넌트 공유 URL: https://myoc.kr/hong/health/123
→ 카카오톡 수신자가 링크 열면 홍길동 브랜딩으로 공통 콘텐츠 표시
→ 자연스럽게 홍길동 팬/지지자 유입으로 연결

효과: 공통 콘텐츠 1건 → 수만 테넌트가 각자 팔로워에게 자연 확산


3. 시스템 아키텍처

3-1. 멀티테넌트 구조 전략

Single Database / Shared Schema 방식
→ 모든 테이블에 tenant_id FK를 통한 데이터 분리
→ 스키마 복잡도 대비 유지보수/백업 효율 최우선
→ 테넌트별 테이블 분리 절대 금지 (수만 테넌트 × 수십 게시판 = DB 붕괴)

3-2. URL 라우팅 흐름

Request: https://myoc.kr/hong/board/notice
  └─ Nginx → index.php
       └─ Router::boot()
            └─ TenantResolver::resolve('hong')     ← slug → tenant_id 확정
                 └─ TenantContext::set($tenant)     ← 전역 컨텍스트 싱글턴 주입
                      └─ 이후 모든 DB 쿼리에 tenant_id 자동 바인딩

3-3. TenantMiddleware

php
class TenantMiddleware
{
    public function handle(Request $req, Closure $next): Response
    {
        $slug   = $req->segment(1);
        $tenant = TenantRepository::findBySlug($slug);

        if (!$tenant || !$tenant->isActive()) {
            return Response::abort(404);
        }

        TenantContext::set($tenant);        // 싱글턴 전역 주입
        View::share('tenant', $tenant);
        SEOMeta::applyTenant($tenant);      // <head> SEO 자동 주입

        return $next($req);
    }
}

3-4. BaseModel — tenant_id 자동 바인딩

php
abstract class BaseModel
{
    protected function applyTenantScope(string $sql, array $params): array
    {
        $tenant = TenantContext::get();
        if ($tenant && $this->hasTenantScope) {
            $sql    .= ' AND tenant_id = :__tid';
            $params[':__tid'] = $tenant->id;
        }
        return [$sql, $params];
    }
}
// 모든 Model이 BaseModel 상속 → 테넌트 월담 구조적 차단

4. 데이터베이스 설계

4-1. 테넌트 마스터

sql
CREATE TABLE tenants (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    slug            VARCHAR(50)  NOT NULL UNIQUE,
    display_name    VARCHAR(100) NOT NULL,
    owner_user_id   INT UNSIGNED NOT NULL,
    plan            ENUM('free','basic','pro') DEFAULT 'free',
    status          ENUM('active','suspended','deleted') DEFAULT 'active',
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_slug (slug)
) ENGINE=InnoDB;

4-2. 테넌트 프로필 (SEO 핵심)

sql
CREATE TABLE tenant_profiles (
    tenant_id       INT UNSIGNED PRIMARY KEY,
    real_name       VARCHAR(100),
    tagline         VARCHAR(200),
    bio             TEXT,
    avatar_path     VARCHAR(300),
    cover_path      VARCHAR(300),
    meta_title      VARCHAR(100),       -- <title> 커스텀
    meta_desc       VARCHAR(300),       -- <meta name="description">
    og_image_path   VARCHAR(300),       -- Open Graph 이미지
    canonical_url   VARCHAR(300),
    keywords        VARCHAR(500),
    schema_type     ENUM('Person','Organization','LocalBusiness') DEFAULT 'Person',
    FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB;

4-3. SNS 연동 설정

sql
CREATE TABLE tenant_sns_settings (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id       INT UNSIGNED NOT NULL,
    provider        ENUM('kakao_story','instagram','facebook','twitter','threads','naver_blog'),
    access_token    TEXT,               -- AES-256 암호화 저장
    refresh_token   TEXT,
    token_expires   DATETIME,
    account_id      VARCHAR(200),
    is_active       TINYINT(1) DEFAULT 1,
    UNIQUE KEY uq_tenant_provider (tenant_id, provider),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
) ENGINE=InnoDB;

4-4. 게시판 정의

sql
-- tenant_id = NULL 이면 공통 게시판
CREATE TABLE boards (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id       INT UNSIGNED DEFAULT NULL,
    board_key       VARCHAR(50)  NOT NULL,
    board_name      VARCHAR(100) NOT NULL,
    skin            ENUM('list','webzine','gallery') DEFAULT 'list',
    use_sns_share   TINYINT(1) DEFAULT 0,
    use_comment     TINYINT(1) DEFAULT 1,
    use_like        TINYINT(1) DEFAULT 1,
    use_file        TINYINT(1) DEFAULT 1,
    sort_order      SMALLINT DEFAULT 0,
    is_active       TINYINT(1) DEFAULT 1,
    UNIQUE KEY uq_tenant_key (tenant_id, board_key),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
) ENGINE=InnoDB;

-- 공통 게시판 초기 데이터
INSERT INTO boards (tenant_id, board_key, board_name, skin)
VALUES (NULL, 'health', '건강정보', 'webzine'),
       (NULL, 'life',   '생활정보', 'webzine'),
       (NULL, 'issue',  '이슈',     'webzine');

4-5. 게시물 (통합 — 공통/테넌트 혼합)

sql
CREATE TABLE posts (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id       INT UNSIGNED DEFAULT NULL,  -- NULL = 공통 게시물
    board_id        INT UNSIGNED NOT NULL,
    user_id         INT UNSIGNED NOT NULL,
    post_type       ENUM('tenant','common') DEFAULT 'tenant',
    title           VARCHAR(300) NOT NULL,
    content         MEDIUMTEXT,
    thumb_path      VARCHAR(300),
    media_type      ENUM('none','image','video') DEFAULT 'none',
    video_url       VARCHAR(500),               -- YouTube/Vimeo 또는 직접 URL
    location        VARCHAR(200),               -- 활동 장소
    event_at        DATETIME,                   -- 활동 일시
    view_count      INT UNSIGNED DEFAULT 0,
    share_count     INT UNSIGNED DEFAULT 0,     -- 카카오톡 등 공유 누적
    status          ENUM('draft','published','hidden','deleted') DEFAULT 'published',
    published_at    DATETIME,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_tenant_board  (tenant_id, board_id, status, published_at),
    INDEX idx_common        (post_type, board_id, status, published_at),
    FOREIGN KEY (board_id) REFERENCES boards(id)
) ENGINE=InnoDB;

/*
  구조 요약:
  tenant_id = 123, post_type = 'tenant'  → 홍길동 전용 게시물
  tenant_id = NULL, post_type = 'common' → 공통 게시물 (한 건 저장, 수만 테넌트가 읽기만)
*/

4-6. 테넌트별 공통게시판 노출 설정

sql
CREATE TABLE tenant_common_boards (
    tenant_id       INT UNSIGNED NOT NULL,
    board_id        INT UNSIGNED NOT NULL,  -- 공통 board id
    is_visible      TINYINT(1) DEFAULT 1,
    sort_order      SMALLINT DEFAULT 0,
    PRIMARY KEY (tenant_id, board_id),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (board_id)  REFERENCES boards(id)
) ENGINE=InnoDB;

4-7. SNS 발행 로그

sql
CREATE TABLE sns_publish_logs (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    post_id         INT UNSIGNED NOT NULL,
    tenant_id       INT UNSIGNED NOT NULL,
    provider        VARCHAR(30) NOT NULL,
    status          ENUM('pending','success','failed') DEFAULT 'pending',
    sns_post_id     VARCHAR(200),
    error_msg       TEXT,
    published_at    DATETIME,
    INDEX idx_post   (post_id),
    FOREIGN KEY (post_id) REFERENCES posts(id)
) ENGINE=InnoDB;

4-8. 공유 로그

sql
CREATE TABLE post_share_logs (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    post_id         INT UNSIGNED NOT NULL,
    tenant_id       INT UNSIGNED DEFAULT NULL,  -- 어느 테넌트 페이지에서 공유됐는지
    share_channel   ENUM('kakao','kakao_story','url_copy','other') DEFAULT 'kakao',
    shared_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_post   (post_id),
    INDEX idx_tenant (tenant_id, shared_at)
) ENGINE=InnoDB;

4-9. 회원

sql
CREATE TABLE members (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id       INT UNSIGNED NOT NULL,
    email           VARCHAR(200) NOT NULL,
    password_hash   VARCHAR(255) NOT NULL,
    nickname        VARCHAR(100),
    point           INT DEFAULT 0,
    role            ENUM('owner','manager','member') DEFAULT 'member',
    status          ENUM('active','banned','withdrawn') DEFAULT 'active',
    joined_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uq_tenant_email (tenant_id, email),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
) ENGINE=InnoDB;

4-10. 메뉴 관리

sql
CREATE TABLE menus (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id       INT UNSIGNED NOT NULL,
    parent_id       INT UNSIGNED DEFAULT NULL,
    label           VARCHAR(100) NOT NULL,
    link_type       ENUM('board','url','page') DEFAULT 'url',
    link_value      VARCHAR(300),
    sort_order      SMALLINT DEFAULT 0,
    is_visible      TINYINT(1) DEFAULT 1,
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
) ENGINE=InnoDB;

4-11. 템플릿 시스템

sql
-- 템플릿 마스터 (시스템 제공 + AI 생성 통합)
CREATE TABLE templates (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name            VARCHAR(100) NOT NULL,
    preview_path    VARCHAR(300),
    category        ENUM('minimal','bold','elegant','warm','official','trendy'),
    target_persona  VARCHAR(100),           -- "정치인", "강사", "작가" 등
    css_tokens      JSON NOT NULL,          -- 디자인 토큰 전체
    layout_preset   VARCHAR(50) DEFAULT 'standard',
    source          ENUM('human','ai') DEFAULT 'human',
    quality_score   TINYINT UNSIGNED DEFAULT 0,
    is_public       TINYINT(1) DEFAULT 0,   -- 검수 통과 후 공개
    used_count      INT UNSIGNED DEFAULT 0,
    created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

-- 테넌트 적용 템플릿
CREATE TABLE tenant_templates (
    tenant_id       INT UNSIGNED PRIMARY KEY,
    template_id     INT UNSIGNED NOT NULL,
    custom_tokens   JSON DEFAULT NULL,      -- 테넌트 미세조정 오버라이드
    applied_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (tenant_id)  REFERENCES tenants(id),
    FOREIGN KEY (template_id) REFERENCES templates(id)
) ENGINE=InnoDB;

5. 디렉토리 구조

/brandos
├── public/                         ← DocumentRoot (Nginx)
│   ├── index.php                   ← 단일 진입점
│   ├── assets/                     ← 전역 CSS/JS
│   └── uploads/
│       └── {tenant_id}/            ← 테넌트별 업로드 완전 분리
│
├── app/
│   ├── Core/
│   │   ├── Router.php
│   │   ├── Request.php
│   │   ├── Response.php
│   │   └── TenantContext.php       ← 전역 테넌트 싱글턴
│   │
│   ├── Middleware/
│   │   ├── TenantMiddleware.php
│   │   ├── AuthMiddleware.php
│   │   └── CsrfMiddleware.php
│   │
│   ├── Models/
│   │   ├── BaseModel.php           ← tenant_id 자동 바인딩
│   │   ├── Tenant.php
│   │   ├── Post.php
│   │   ├── Board.php
│   │   └── Member.php
│   │
│   ├── Services/
│   │   ├── SnsPublisher/
│   │   │   ├── SnsPublisherInterface.php
│   │   │   ├── KakaoStoryPublisher.php
│   │   │   ├── InstagramPublisher.php
│   │   │   └── SnsPublishJob.php   ← 큐 처리
│   │   ├── SeoService.php
│   │   ├── MediaService.php        ← 이미지 리사이즈/EXIF 처리
│   │   ├── TemplateService.php     ← CSS 토큰 주입
│   │   ├── AiTemplateGenerator.php ← Claude API 연동
│   │   └── PointService.php
│   │
│   ├── Controllers/
│   │   ├── Admin/                  ← 솔루션 최고관리자
│   │   ├── Tenant/                 ← 테넌트 관리자
│   │   └── Front/                  ← 공개 프론트
│   │
│   └── Views/
│       ├── _layout/
│       │   ├── base.html.php
│       │   ├── presets/
│       │   │   ├── standard.html.php
│       │   │   ├── sidebar-left.html.php
│       │   │   ├── fullhero.html.php
│       │   │   ├── minimal-top.html.php
│       │   │   └── magazine.html.php
│       │   └── tenant_sub.html.php
│       ├── skins/
│       │   ├── list/
│       │   ├── webzine/
│       │   └── gallery/
│       └── admin/
│
├── config/
│   ├── database.php
│   ├── app.php
│   └── sns.php                     ← SNS API 앱 키 (서버 전역)
│
├── storage/
│   ├── cache/
│   ├── logs/
│   └── queue/                      ← SNS 발행 큐 파일
│
└── cron/
    └── sns_queue_worker.php        ← 큐 처리 크론잡 (1분 주기)

6. 핵심 기능 상세 설계

6-1. SEO 자동화

php
class SeoService
{
    public function renderHead(Tenant $t, ?Post $post = null): string
    {
        $profile   = $t->profile;
        $title     = ($post?->title ? $post->title . ' | ' : '') . ($profile->meta_title ?? $t->display_name);
        $desc      = $post?->excerpt(120) ?? $profile->meta_desc;
        $ogImage   = $post?->thumb_url ?? $profile->og_image_url;
        $canonical = $post?->canonical_url ?? "https://myoc.kr/{$t->slug}";

        // JSON-LD 구조화 데이터 자동 생성
        $schema = [
            '@context'    => 'https://schema.org',
            '@type'       => $profile->schema_type,  // Person / Organization
            'name'        => $profile->real_name,
            'url'         => $canonical,
            'description' => $desc,
            'image'       => $ogImage,
        ];

        return $this->renderMetaTags($title, $desc, $ogImage, $canonical, $schema);
    }
}
  • 테넌트별 Sitemap 자동 생성: /{slug}/sitemap.xml
  • lastmod = updated_at, changefreq = 게시판 업데이트 빈도 기반
  • 올리는 행위 자체가 SEO 작업 → 사용자가 SEO를 몰라도 됨

6-2. 게시물 조회 — 공통/테넌트 통합 피드

php
class PostRepository
{
    // 메인 피드: 테넌트 게시물 + 공통 게시물 혼합
    public function getFeedForTenant(int $tenantId, int $limit = 20): array
    {
        $sql = "
            (
                SELECT p.*, b.board_name, 'tenant' AS source
                FROM posts p
                JOIN boards b ON b.id = p.board_id
                WHERE p.tenant_id = :tid
                  AND p.status = 'published'
            )
            UNION ALL
            (
                SELECT p.*, b.board_name, 'common' AS source
                FROM posts p
                JOIN boards b ON b.id = p.board_id
                JOIN tenant_common_boards tcb
                  ON tcb.board_id = p.board_id AND tcb.tenant_id = :tid2
                WHERE p.post_type = 'common'
                  AND p.status = 'published'
                  AND tcb.is_visible = 1
            )
            ORDER BY published_at DESC
            LIMIT :limit
        ";
        return $this->db->query($sql, ['tid' => $tenantId, 'tid2' => $tenantId, 'limit' => $limit]);
    }
}

6-3. 공유 URL 설계

php
class PostController
{
    public function view(string $slug, string $boardKey, int $postId): Response
    {
        $tenant   = TenantContext::get();
        $post     = PostRepository::findForTenant($tenant->id, $postId);
        // 공통 게시물(post->tenant_id === null)이든 테넌트 게시물이든 동일 렌더링
        // 공통 게시물도 현재 테넌트 브랜딩을 씌워서 표시

        // 공유 URL = 현재 테넌트 기준 (SNS/카카오톡 공유 시 이 URL 전달)
        $shareUrl = "https://myoc.kr/{$slug}/{$boardKey}/{$postId}";

        return View::render('front/post_view', compact('post', 'shareUrl', 'tenant'));
    }
}

6-4. SNS 동시 발행 시스템 (비동기 큐)

발행 흐름

글쓰기 폼 제출
  → PostController::store()
      → Post 저장 (DB)
      → MediaService: 이미지 처리
      → 선택된 SNS → SnsPublishJob::dispatch()
           → sns_publish_logs에 'pending' INSERT
           → queue 파일에 job 적재
  → 사용자에게 즉시 응답 (비동기)

[크론잡 1분 주기] sns_queue_worker.php
  → pending 잡 조회
  → SnsPublisherInterface::publish($job)
  → 결과를 sns_publish_logs 업데이트

주의: SNS 발행을 동기 처리하면 API 지연/토큰 만료 재시도 시 UX가 망가짐.
반드시 비동기 큐 방식으로 처리할 것.

Instagram Graph API 발행

php
class InstagramPublisher implements SnsPublisherInterface
{
    public function publish(SnsPublishJob $job): PublishResult
    {
        $setting = $job->tenant->snsSetting('instagram');
        $post    = $job->post;

        // Step 1: 미디어 컨테이너 생성
        $container = $this->api->createMediaContainer(
            token:    $setting->access_token,
            imageUrl: $post->thumb_absolute_url,
            caption:  $this->buildCaption($post),
        );

        // Step 2: 게시
        $result = $this->api->publishMedia(
            token:       $setting->access_token,
            containerId: $container->id,
        );

        return new PublishResult(success: true, snsPostId: $result->id);
    }

    private function buildCaption(Post $post): string
    {
        $parts = [$post->title];
        if ($post->event_at) $parts[] = '📅 ' . $post->event_at->format('Y.m.d');
        if ($post->location) $parts[] = '📍 ' . $post->location;
        if ($post->content)  $parts[] = "\n" . strip_tags(mb_substr($post->content, 0, 200));
        return implode("\n", $parts);
    }
}

카카오스토리 발행

  • Kakao REST API → POST /v1/api/story/post/photo
  • 이미지 업로드 후 storyMediaIds 배열로 게시

Facebook/Instagram은 동일 Graph API 기반 → 묶어서 개발
네이버 블로그 API는 안정성 낮음 → 우선순위 후순위 권장

6-5. 미디어 처리 (스마트폰 사진 대응)

php
class MediaService
{
    public function processUpload(UploadedFile $file, int $tenantId): MediaResult
    {
        $image = imagecreatefromstring(file_get_contents($file->tmpPath));
        $exif  = exif_read_data($file->tmpPath);

        // EXIF 회전 보정 (스마트폰 사진 필수)
        $image = $this->autoRotate($image, $exif);

        // 리사이즈 3종 생성
        $this->saveResized($image, $tenantId, $file->hash, [
            'original' => null,  // 원본
            'medium'   => 800,   // 중간
            'thumb'    => 400,   // 썸네일
        ]);

        // EXIF에서 촬영 일시·GPS 추출 → 활동소식 자동 채움
        return new MediaResult(
            path:    "uploads/{$tenantId}/{$file->hash}",
            takenAt: $exif['DateTimeOriginal'] ?? null,
            gpsLat:  $this->parseGps($exif['GPSLatitude'] ?? null),
            gpsLng:  $this->parseGps($exif['GPSLongitude'] ?? null),
        );
    }
}

6-6. 게시판 스킨

스킨용도레이아웃
list공지/텍스트 중심제목+날짜+요약 테이블형
webzine활동소식, 공통 콘텐츠카드형, 본문 미리보기+썸네일
gallery사진 갤러리Masonry/Grid, 이미지 우선
  • /app/Views/skins/{skin}/ 디렉토리로 분리
  • 테넌트 관리자가 게시판별로 스킨 선택
  • skin.config.php로 스킨별 옵션 정의 (컬럼 수, 페이지당 수 등)

7. 템플릿 및 AI 스타일 시스템

7-1. 3레이어 분리 원칙

Layer 1. 데이터 레이어 (DB)       → posts, tenant_profiles
Layer 2. 구조 레이어 (HTML)       → 레이아웃 프리셋 (5~7종 고정)
Layer 3. 스타일 레이어 (CSS 토큰) → AI가 생성하는 영역

핵심: AI는 HTML을 건드리지 않고 CSS 토큰만 생성.
깨지는 레이아웃이 구조적으로 불가능하고, 검수도 색상 대비 체크 수준으로 단순화.

7-2. CSS 토큰 시스템

css
:root {
    /* 색상 */
    --color-primary:       #1a56db;
    --color-secondary:     #7e3af2;
    --color-accent:        #ff5a1f;
    --color-bg:            #ffffff;
    --color-bg-subtle:     #f9fafb;
    --color-text:          #111928;
    --color-text-muted:    #6b7280;
    --color-border:        #e5e7eb;

    /* 타이포그래피 */
    --font-display:        'Pretendard', sans-serif;
    --font-body:           'Pretendard', sans-serif;
    --font-size-base:      16px;
    --font-weight-heading: 700;
    --line-height-base:    1.6;

    /* 형태 */
    --radius-sm:           4px;
    --radius-md:           8px;
    --radius-lg:           16px;

    /* 헤더 */
    --header-height:       64px;
    --header-bg:           var(--color-primary);
    --header-text:         #ffffff;
    --header-style:        'solid';  /* solid | transparent | blur */

    /* 카드 */
    --card-shadow:         0 1px 3px rgba(0,0,0,.1);

    /* 히어로 */
    --hero-style:          'gradient';  /* gradient | image | split | minimal */
    --hero-height:         480px;
}

CSS 토큰 JSON (DB 저장 예시)

json
{
    "color_primary":       "#1a56db",
    "color_bg":            "#ffffff",
    "font_display":        "Noto Serif KR",
    "font_body":           "Pretendard",
    "radius_md":           "2px",
    "header_style":        "transparent",
    "hero_style":          "split",
    "card_shadow":         "none",
    "font_weight_heading": "800"
}

테넌트 페이지 head에 동적 주입

php
$tokens = $tenant->template->mergedTokens(); // template + custom_tokens 오버라이드 병합
echo "<style>:root {\n";
foreach ($tokens as $key => $value) {
    echo "  --" . str_replace('_', '-', $key) . ": {$value};\n";
}
echo "}</style>";

7-3. 레이아웃 프리셋 (HTML 구조 변형)

preset구조적합 대상
standard상단 헤더 + 히어로 + 본문범용
sidebar-left좌측 프로필 고정 + 우측 피드개인 브랜드
fullhero전면 커버 이미지 + 스크롤 본문정치인, 강사
minimal-top로고+메뉴만, 히어로 없음기업/전문직
magazine2단 그리드 메인콘텐츠 중심

프리셋은 5~7종 고정. AI는 프리셋 중 하나를 선택하고 토큰만 생성.
HTML은 사람이 한 번 만들면 영구 재사용.

7-4. AI 템플릿 생성 파이프라인

[1단계] 관리자 요청
  예: "트렌디한 20대 여성 강사 스타일 10종"
    ↓
[2단계] AI 프롬프트 생성 → Claude API 호출
  → CSS 토큰 JSON 배열만 반환하도록 구조화 프롬프트
    ↓
[3단계] 자동 렌더 검증
  → 생성 토큰을 실제 템플릿에 주입
  → 헤드리스 브라우저로 스크린샷 자동 촬영
  → 색상 대비비 자동 체크 (WCAG AA 기준, 최소 4.5:1)
    ↓
[4단계] 관리자 검수 큐
  → 스크린샷 + 토큰 나란히 표시
  → 승인 / 수정 / 반려
    ↓
[5단계] is_public = 1 → 테넌트 선택 화면에 노출

AI 호출 프롬프트 구조

php
$prompt = "
당신은 웹 디자인 전문가입니다.
아래 JSON 스키마를 반드시 지키는 CSS 토큰 세트를 {$count}종 생성하세요.
페르소나: {$persona}
분위기: {$mood}
레이아웃 프리셋: {$preset} (변경 불가)

제약:
- 배경색과 텍스트 대비비 WCAG AA 기준(4.5:1) 이상 필수
- 흰 글씨에 흰 배경 금지
- 한국 웹 환경에 적합한 폰트 사용

반드시 아래 JSON 배열만 반환, 다른 텍스트 일절 없음:
[{ \"name\": \"...\", \"layout_preset\": \"...\", \"tokens\": { ...토큰... } }, ...]

토큰 스키마:
" . json_encode($tokenSchema, JSON_UNESCAPED_UNICODE);

7-5. 테넌트 템플릿 선택 UX

템플릿 선택 화면
  → 카테고리 필터: 미니멀 / 강렬 / 우아한 / 공식적 / 따뜻한
  → 페르소나 필터: 정치인 / 강사 / 작가 / 의료인 / 기업인
  → 썸네일 그리드 (실제 내 콘텐츠가 채워진 라이브 미리보기)
       ↑ 핵심 차별점: 샘플이 아닌 내 프로필·게시물로 미리보기
  → 클릭 → 즉시 적용 (새로고침 없이 CSS 토큰 교체)
  → 미세조정: 메인 컬러 1개만 조정 가능 (컬러피커)

7-6. 경쟁사 비교

항목윅스/아임웹BrandOS
스타일 변경블럭 재배치 필요클릭 한 번
새 템플릿 제작디자이너 필요AI 토큰 생성 파이프라인
미리보기샘플 콘텐츠내 실제 콘텐츠
무한 확장불가AI 파이프라인으로 지속 추가
커스텀자유롭지만 복잡컬러 하나만 (단순·명확)

8. 관리자 패널 구성

8-1. 솔루션 최고관리자 (/admin)

  • 테넌트 목록/생성/정지/삭제
  • 플랜 관리, 사용량 모니터링
  • 공통 게시판 및 게시물 관리
  • SNS API 앱 설정 (서버 전역 클라이언트 ID/Secret)
  • AI 템플릿 생성 및 검수 큐
  • 시스템 공지, 업데이트 배포

8-2. 테넌트 관리자 (/{slug}/manage)

메뉴주요 기능
대시보드방문자 수, 최근 게시물, SNS 발행 현황, 공유 통계
프로필 관리인물 정보, 사진, 소개, SEO 메타 설정
메뉴 관리드래그앤드롭 메뉴 편집기
게시판 관리게시판 생성/수정, 스킨 선택, SNS 연동 여부, 공통게시판 표시 설정
게시물 관리전체 게시물 목록/수정/삭제, 예약 발행
회원 관리회원 목록, 포인트 지급/차감, 등급 조정
SNS 설정플랫폼별 OAuth 연동, 발행 로그 확인
템플릿레이아웃 프리셋 선택, 스타일 선택, 컬러 미세조정, 배너 관리
SEO 설정타이틀, 설명, OG 이미지, 사이트맵, schema.org 타입

9. 보안 설계

항목적용 방안
SNS 토큰AES-256-CBC 암호화 저장, 복호화 키는 .env 관리 (절대 DB에 평문 저장 금지)
CSRF모든 폼 제출에 CSRF 토큰 검증
XSS출력 시 htmlspecialchars() 전처리, 에디터는 허용 태그 화이트리스트
SQL InjectionPDO prepared statements 100% 적용
파일 업로드MIME 실제 검사, 업로드 디렉토리 PHP 실행 차단 (Nginx deny)
테넌트 월담모든 쿼리에 WHERE tenant_id = ? 강제 바인딩 (BaseModel 레벨)
레이트 리밋로그인/SNS 발행 API에 Redis 기반 Rate Limit
비밀번호password_hash() bcrypt, 평문 저장 절대 금지

10. 성능 최적화

항목방안
테넌트 메타 캐싱Redis tenant:{slug} 키, TTL 10분. 설정 변경 시 즉시 무효화
게시물 목록 캐싱게시물 작성/수정 시 해당 테넌트 게시판 캐시 자동 무효화
이미지 최적화Lazy Load, WebP 자동 변환, 3종 리사이즈 (원본/800/400)
DB 인덱스 필수(tenant_id, status, published_at), (post_type, board_id, status) 복합 인덱스
SNS 발행동기 처리 절대 금지 → 크론 큐 비동기 처리
CSS 토큰인라인 <style> 주입, 별도 HTTP 요청 없음

11. 개발 로드맵

Phase기간주요 작업
P1. 기반2주디렉토리 구조, Router, TenantMiddleware, BaseModel, DB 스키마 전체 생성
P2. 코어3주테넌트 생성/관리, 회원가입/로그인, 게시판/게시물 CRUD, 공통 게시물 피드
P3. SEO1주SeoService, JSON-LD, Sitemap 자동 생성, OG 태그 자동화
P4. SNS2주카카오스토리·Instagram Publisher, 큐 시스템, 발행 로그, 공유 URL
P5. 스킨/템플릿2주3종 게시판 스킨, 레이아웃 프리셋 5종, CSS 토큰 주입, 배너 관리
P6. AI 템플릿1주AiTemplateGenerator, 검수 큐, 라이브 미리보기, 컬러 미세조정
P7. 마감1주보안 점검, 성능 튜닝(인덱스/캐시), 관리자 대시보드 완성
P8. 패키지1주설치 마법사(install.php), 라이선스 시스템, 문서화

총 예상 기간: 약 13주 (약 3개월)


12. 패키지 출시 체크리스트

필수 파일

  • install.php : DB 자동 생성, 최고관리자 계정 설정 마법사
  • .env.example : DB 접속, AES 암호화 키, SNS 앱 ID 샘플
  • migrations/ : 버전별 DB 마이그레이션 스크립트
  • demo_data.sql : 샘플 테넌트/게시물/템플릿 포함

라이선스

  • 도메인 기반 라이선스 검증 모듈
  • 플랜별 기능 제한 로직 (free/basic/pro)

문서

  • 설치 가이드 (서버 요구사항, Nginx 설정, cron 등록 방법)
  • 테넌트 관리자 사용 매뉴얼
  • SNS 연동 설정 가이드 (플랫폼별 API 키 발급 절차 포함)
  • 개발자 커스터마이징 가이드 (스킨/프리셋 추가 방법)

품질

  • 모바일 반응형 전 화면 검수
  • CSRF/XSS/SQL Injection 보안 테스트
  • 멀티테넌트 월담 방지 테스트 (테넌트 A가 테넌트 B 데이터 접근 불가 확인)
  • SNS 토큰 암호화/복호화 검증
  • 색상 대비비 WCAG AA 기준 전 템플릿 통과 확인

BrandOS Development Guide v1.0 — 본 문서를 기반으로 전체 개발이 가능하도록 설계되었습니다.

Content is user-generated and unverified.
    BrandOS Personal Branding SaaS — Complete Development Guide v1.0 | Claude