데브포일 홈
DEV MODE - 실서버 영향 없음 📦 배포 관리
테넌트별 설정 & 커스터마이징
+250 XP
LEVEL 55 QUEST

테넌트별 설정 & 커스터마이징

JSON 기반 테넌트 설정, AI 에이전트 커스터마이징, 브랜딩 설정을 구현합니다.

tenant_settings 테이블 (JSON 기반)

왜 JSON 기반 설정인가?

테넌트마다 필요한 설정 항목이 다릅니다. 컬럼을 계속 추가하면 스키마가 비대해지고 마이그레이션이 복잡해집니다. JSON 컬럼을 사용하면 스키마 변경 없이 유연하게 설정을 추가할 수 있습니다. MariaDB 10.2 이상에서 JSON 타입과 JSON 함수를 완벽 지원합니다.
-- 테넌트 설정 테이블
CREATE TABLE tenant_settings (
    id           INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tenant_id    INT UNSIGNED NOT NULL UNIQUE,
    settings     JSON NOT NULL,
    updated_at   DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    updated_by   INT UNSIGNED DEFAULT NULL,

    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- 기본 설정 JSON 구조 예시
INSERT INTO tenant_settings (tenant_id, settings) VALUES (1, '{
    "branding": {
        "logo_url": null,
        "primary_color": "#8b5cf6",
        "secondary_color": "#1e1b4b",
        "company_name_display": "My SaaS Company",
        "favicon_url": null
    },
    "ai_agent": {
        "personality": "professional",
        "tone": "formal",
        "language": "ko",
        "custom_greeting": "안녕하세요! 무엇을 도와드릴까요?",
        "max_response_length": 500,
        "allowed_topics": ["customer_support", "product_info"],
        "blocked_topics": ["competitor_info", "pricing_negotiation"]
    },
    "notifications": {
        "email_alerts": true,
        "slack_webhook": null,
        "daily_report": false,
        "usage_warning_threshold": 80
    },
    "custom_domain": {
        "domain": null,
        "ssl_status": "pending",
        "verified": false
    }
}');
        
// TenantSettings.php - 테넌트 설정 관리 클래스

class TenantSettings {
    private PDO $pdo;
    private array $settings = [];
    private bool $loaded = false;

    // 기본 설정값 (테넌트 설정이 없을 때 사용)
    private const DEFAULTS = [
        'branding.primary_color'   => '#8b5cf6',
        'branding.secondary_color' => '#1e1b4b',
        'ai_agent.personality'     => 'professional',
        'ai_agent.tone'            => 'formal',
        'ai_agent.language'        => 'ko',
        'ai_agent.max_response_length' => 500,
        'notifications.email_alerts'   => true,
        'notifications.usage_warning_threshold' => 80,
    ];

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    /**
     * 설정 로드 (Lazy Loading)
     */
    private function load(): void
    {
        if ($this->loaded) return;

        $tenantId = TenantManager::id();
        $stmt = $this->pdo->prepare("
            SELECT settings FROM tenant_settings WHERE tenant_id = :tid
        ");
        $stmt->execute([':tid' => $tenantId]);
        $json = $stmt->fetchColumn();

        $this->settings = $json ? json_decode($json, true) : [];
        $this->loaded = true;
    }

    /**
     * 설정값 조회 (dot notation 지원)
     * 예: $settings->get('branding.primary_color')
     */
    public function get(string $key, mixed $default = null): mixed
    {
        $this->load();

        $keys = explode('.', $key);
        $value = $this->settings;

        foreach ($keys as $k) {
            if (!is_array($value) || !isset($value[$k])) {
                return $default ?? (self::DEFAULTS[$key] ?? null);
            }
            $value = $value[$k];
        }

        return $value;
    }

    /**
     * 설정값 변경 (dot notation)
     * 예: $settings->set('branding.primary_color', '#ff0000')
     */
    public function set(string $key, mixed $value): void
    {
        $this->load();

        $keys = explode('.', $key);
        $ref = &$this->settings;

        foreach ($keys as $i => $k) {
            if ($i === count($keys) - 1) {
                $ref[$k] = $value;
            } else {
                if (!isset($ref[$k]) || !is_array($ref[$k])) {
                    $ref[$k] = [];
                }
                $ref = &$ref[$k];
            }
        }
    }

    /**
     * 설정 저장 (DB 반영)
     */
    public function save(): void
    {
        $tenantId = TenantManager::id();
        $json = json_encode($this->settings, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

        $stmt = $this->pdo->prepare("
            INSERT INTO tenant_settings (tenant_id, settings, updated_by)
            VALUES (:tid, :settings, :uid)
            ON DUPLICATE KEY UPDATE
                settings = :settings2,
                updated_by = :uid2
        ");
        $stmt->execute([
            ':tid'       => $tenantId,
            ':settings'  => $json,
            ':uid'       => $_SESSION['user_id'] ?? null,
            ':settings2' => $json,
            ':uid2'      => $_SESSION['user_id'] ?? null,
        ]);
    }
}
        

AI Agent 성격/톤 커스터마이징

테넌트마다 다른 AI 성격

SaaS의 핵심 가치 중 하나는 각 고객사에 맞는 AI 에이전트를 제공하는 것입니다. 법률 회사는 격식 있는 톤을, 스타트업은 친근한 톤을 원합니다. 이를 테넌트 설정으로 제어합니다.
// AIAgentConfig.php - 테넌트별 AI 에이전트 설정

class AIAgentConfig {
    private TenantSettings $settings;

    // 미리 정의된 성격 프리셋
    private const PERSONALITY_PRESETS = [
        'professional' => [
            'system_prompt' => '당신은 전문적이고 신뢰감 있는 비즈니스 어시스턴트입니다. 정확하고 간결한 답변을 제공합니다.',
            'temperature'   => 0.3,
        ],
        'friendly' => [
            'system_prompt' => '당신은 친근하고 따뜻한 고객 서비스 담당자입니다. 공감하며 대화합니다.',
            'temperature'   => 0.7,
        ],
        'technical' => [
            'system_prompt' => '당신은 기술 전문가입니다. 코드와 기술 용어를 적극적으로 활용하여 설명합니다.',
            'temperature'   => 0.2,
        ],
    ];

    public function __construct(TenantSettings $settings)
    {
        $this->settings = $settings;
    }

    /**
     * AI API 호출에 사용할 시스템 프롬프트 생성
     */
    public function buildSystemPrompt(): string
    {
        $personality = $this->settings->get('ai_agent.personality', 'professional');
        $preset      = self::PERSONALITY_PRESETS[$personality] ?? self::PERSONALITY_PRESETS['professional'];

        $tone     = $this->settings->get('ai_agent.tone', 'formal');
        $lang     = $this->settings->get('ai_agent.language', 'ko');
        $greeting = $this->settings->get('ai_agent.custom_greeting');
        $maxLen   = $this->settings->get('ai_agent.max_response_length', 500);
        $blocked  = $this->settings->get('ai_agent.blocked_topics', []);

        // 커스텀 프롬프트가 있으면 우선 적용
        $customPrompt = $this->settings->get('ai_agent.custom_system_prompt');
        $basePrompt = $customPrompt ?: $preset['system_prompt'];

        $prompt = $basePrompt . "\n\n";
        $prompt .= "톤: {$tone}\n";
        $prompt .= "응답 언어: {$lang}\n";
        $prompt .= "최대 응답 길이: {$maxLen}자\n";

        if ($greeting) {
            $prompt .= "첫 인사: {$greeting}\n";
        }

        if (!empty($blocked)) {
            $blockedStr = implode(', ', $blocked);
            $prompt .= "금지 주제: {$blockedStr} (이 주제에 대해서는 정중히 거절)\n";
        }

        return $prompt;
    }

    /**
     * AI API 호출 파라미터 반환
     */
    public function getApiParams(): array
    {
        $personality = $this->settings->get('ai_agent.personality', 'professional');
        $preset      = self::PERSONALITY_PRESETS[$personality] ?? self::PERSONALITY_PRESETS['professional'];

        return [
            'temperature' => $preset['temperature'],
            'max_tokens'  => $this->settings->get('ai_agent.max_response_length', 500),
        ];
    }
}
        
system_prompt AI 모델에게 역할과 행동 규칙을 지정하는 초기 지시문. 테넌트별로 다른 프롬프트를 사용하여 맞춤형 AI를 제공합니다.
temperature AI 응답의 창의성/무작위성 조절 값 (0~1). 낮을수록 일관되고, 높을수록 다양한 답변을 생성합니다.
🔒
여기까지는 미리보기입니다
테넌트별 설정 & 커스터마이징
무료 가입하면 이어서 볼 수 있고, 강의를 완료할 때마다 XP와 레벨이 쌓입니다.
Google로 3초 만에 시작 →🧵 Threads로 시작무료 공개 강의 둘러보기 (Lv.1~3)