This commit is contained in:
2025-12-17 13:25:40 +09:00
parent 486b224585
commit 476641bc62
14 changed files with 995 additions and 0 deletions

27
db_test.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
$host = 'mysql';
$db = 'chandj';
$user = 'root';
$pass = 'root';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
echo "Connected successfully to database '$db' on host '$host'.";
// Optional: Try to list tables to be sure
$stmt = $pdo->query("SHOW TABLES");
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
echo "\nTables found: " . implode(", ", array_slice($tables, 0, 5)) . "...";
} catch (\PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
?>

24
ref/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

186
ref/App.tsx Normal file
View File

@@ -0,0 +1,186 @@
import React, { useState, useMemo } from 'react';
import { SALES_ASSETS } from './constants';
import { SalesAsset } from './types';
import SalesCard from './components/SalesCard';
import Assistant from './components/Assistant';
import AssetDetailModal from './components/AssetDetailModal';
import { LayoutGrid, Menu, Bell, Search, ShieldCheck, CheckCircle2 } from 'lucide-react';
const App: React.FC = () => {
const [activeFilter, setActiveFilter] = useState('All');
const [selectedAsset, setSelectedAsset] = useState<SalesAsset | null>(null);
const [showToast, setShowToast] = useState(false);
// Filter Logic
const filteredAssets = useMemo(() => {
if (activeFilter === 'All') return SALES_ASSETS;
return SALES_ASSETS.filter(asset => {
if (activeFilter === 'CEO Logic') return asset.tags.some(tag => ['Concept', 'Pitch', 'Pain Points', 'Solution', 'Closing'].includes(tag));
if (activeFilter === 'Legal/Tax') return asset.tags.some(tag => ['Legal', 'Benefit', 'Risk', 'Finance'].includes(tag));
if (activeFilter === 'Demo') return asset.tags.some(tag => ['Demo', 'Dashboard', 'Video', 'Mobile', 'UX', 'Infra'].includes(tag));
return true;
});
}, [activeFilter]);
const handleDownload = () => {
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
};
return (
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans selection:bg-brand-200 selection:text-brand-900">
{/* Toast Notification */}
<div
className={`fixed top-24 right-4 z-[60] bg-slate-800 text-white px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 transition-all duration-500 transform ${showToast ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'}`}
>
<CheckCircle2 className="text-green-400" size={24} />
<div>
<h4 className="font-bold text-sm">Download Started</h4>
<p className="text-slate-400 text-xs">CodeBridgeX_Proposal_v2.4.pdf</p>
</div>
</div>
{/* Navbar */}
<nav className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center gap-3 cursor-pointer" onClick={() => setActiveFilter('All')}>
<div className="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-lg shadow-lg shadow-brand-200">
S
</div>
<span className="text-xl font-bold tracking-tight text-slate-900">CodeBridgeX <span className="text-brand-600">SAM</span></span>
</div>
<div className="flex items-center gap-4">
<button className="p-2 text-slate-500 hover:text-brand-600 transition-colors hidden sm:block">
<Search size={20} />
</button>
<button className="p-2 text-slate-500 hover:text-brand-600 transition-colors relative">
<Bell size={20} />
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
</button>
<button className="p-2 text-slate-500 hover:text-slate-900 sm:hidden">
<Menu size={24} />
</button>
<div className="hidden sm:flex items-center gap-2 ml-2">
<div className="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center text-slate-500 font-bold border border-slate-300">
S
</div>
<span className="text-sm font-medium text-slate-700"> </span>
</div>
</div>
</div>
</div>
</nav>
{/* Hero Section */}
<header className="relative bg-white pt-16 pb-20 lg:pt-24 lg:pb-28 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="text-center max-w-4xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 text-brand-700 text-xs font-semibold uppercase tracking-wider mb-6 animate-fade-in-up">
<ShieldCheck size={14} />
CEO Management Solution
</div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-slate-900 tracking-tight mb-6 animate-fade-in-up delay-100">
.<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-600 to-indigo-600">
.
</span>
</h1>
<p className="text-lg sm:text-xl text-slate-500 mb-10 leading-relaxed max-w-2xl mx-auto animate-fade-in-up delay-200">
"SAM" ERP가 . <br className="hidden sm:block"/>
, .<br />
<strong> CEO를 릿 </strong> .
</p>
<div className="flex justify-center gap-4 animate-fade-in-up delay-300">
<button
onClick={handleDownload}
className="px-8 py-3 rounded-xl bg-slate-900 text-white font-semibold hover:bg-slate-800 hover:shadow-lg transition-all transform hover:-translate-y-1 active:scale-95"
>
</button>
<button
onClick={() => setActiveFilter('Demo')}
className="px-8 py-3 rounded-xl bg-white text-slate-700 border border-slate-200 font-semibold hover:bg-slate-50 hover:border-slate-300 transition-all flex items-center gap-2 active:scale-95"
>
<LayoutGrid size={18} />
</button>
</div>
</div>
</div>
{/* Background Decorations */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full z-0 pointer-events-none">
<div className="absolute top-20 left-10 w-72 h-72 bg-brand-200 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob"></div>
<div className="absolute top-20 right-10 w-72 h-72 bg-indigo-200 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-1/2 w-72 h-72 bg-pink-200 rounded-full mix-blend-multiply filter blur-3xl opacity-30 animate-blob animation-delay-4000"></div>
</div>
</header>
{/* Main Grid Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-bold text-slate-900">Sales Materials</h2>
<div className="flex gap-2">
{['All', 'CEO Logic', 'Legal/Tax', 'Demo'].map((filter) => (
<button
key={filter}
onClick={() => setActiveFilter(filter)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200 ${
activeFilter === filter
? 'bg-slate-900 text-white shadow-md'
: 'bg-white text-slate-600 hover:bg-slate-100 border border-transparent hover:border-slate-200'
}`}
>
{filter}
</button>
))}
</div>
</div>
{/* Masonry-like Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 auto-rows-[minmax(200px,auto)]">
{filteredAssets.map((asset) => (
<SalesCard
key={asset.id}
asset={asset}
onClick={(a) => setSelectedAsset(a)}
/>
))}
{filteredAssets.length === 0 && (
<div className="col-span-3 text-center py-20 text-slate-400">
.
</div>
)}
</div>
</main>
{/* Footer */}
<footer className="bg-white border-t border-slate-200 py-12 mt-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-6">
<p className="text-slate-500 text-sm">© 2024 CodeBridgeX Corp. Sales Enablement Portal.</p>
<div className="flex gap-6">
<a href="#" className="text-slate-400 hover:text-slate-600">Internal Use Only</a>
<a href="#" className="text-slate-400 hover:text-slate-600">Sales Script</a>
<a href="#" className="text-slate-400 hover:text-slate-600">Support</a>
</div>
</div>
</footer>
{/* Detail Modal */}
{selectedAsset && (
<AssetDetailModal
asset={selectedAsset}
onClose={() => setSelectedAsset(null)}
/>
)}
{/* AI Assistant */}
<Assistant />
</div>
);
};
export default App;

20
ref/README.md Normal file
View File

@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1yue3NOVr2MDV0U4mHpOBv1pf3ByPhT2-
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

View File

@@ -0,0 +1,173 @@
import React, { useEffect, useState } from 'react';
import { SalesAsset, AssetType } from '../types';
import { X, Copy, Check, MessageSquare, Play, Maximize2 } from 'lucide-react';
interface AssetDetailModalProps {
asset: SalesAsset | null;
onClose: () => void;
}
const AssetDetailModal: React.FC<AssetDetailModalProps> = ({ asset, onClose }) => {
const [copied, setCopied] = useState(false);
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [onClose]);
if (!asset) return null;
const handleCopyScript = () => {
if (asset.script) {
navigator.clipboard.writeText(asset.script);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
<div
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
onClick={onClose}
/>
<div className="relative w-full max-w-4xl max-h-[90vh] bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header (Absolute for Video/Image types to float over, Relative for others) */}
<button
onClick={onClose}
className="absolute top-4 right-4 z-20 p-2 bg-black/20 hover:bg-black/40 text-white rounded-full transition-colors backdrop-blur-md"
>
<X size={20} />
</button>
<div className="flex-1 overflow-y-auto custom-scrollbar">
{/* Media Section */}
<div className="relative bg-slate-100 w-full min-h-[300px] md:min-h-[400px] flex items-center justify-center group">
{asset.type === AssetType.VIDEO && (
<iframe
src={`https://player.vimeo.com/video/${asset.src}?autoplay=1&title=0&byline=0&portrait=0`}
className="w-full h-full absolute inset-0"
frameBorder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowFullScreen
title={asset.videoTitle}
></iframe>
)}
{asset.type === AssetType.IMAGE && (
<img
src={asset.src}
alt={asset.title}
className="w-full h-full object-contain bg-slate-900"
/>
)}
{asset.type === AssetType.STAT && (
<div className="w-full h-full flex flex-col items-center justify-center bg-brand-600 text-white p-12">
<div className="text-8xl font-extrabold tracking-tighter mb-4">{asset.statValue}</div>
<div className="text-2xl text-brand-100 font-medium">{asset.statLabel}</div>
</div>
)}
{asset.type === AssetType.TEXT && (
<div className="w-full h-full flex flex-col items-center justify-center bg-gradient-to-br from-slate-800 to-slate-900 text-white p-12 text-center">
<MessageSquare size={64} className="mb-6 opacity-50" />
<h2 className="text-3xl md:text-4xl font-bold max-w-2xl leading-tight">{asset.title}</h2>
<div className="mt-4 flex gap-2">
{asset.tags.map(tag => (
<span key={tag} className="text-xs font-semibold bg-white/10 px-3 py-1 rounded-full">{tag}</span>
))}
</div>
</div>
)}
</div>
{/* Content Section */}
<div className="p-6 md:p-8">
<div className="flex flex-col md:flex-row gap-8">
{/* Left: Info */}
<div className="flex-1">
<h2 className="text-2xl font-bold text-slate-900 mb-2 flex items-center gap-2">
{asset.title}
{asset.type !== AssetType.TEXT && (
<div className="flex gap-2">
{asset.tags.map(tag => (
<span key={tag} className="text-[10px] uppercase font-bold text-brand-600 bg-brand-50 px-2 py-1 rounded-md border border-brand-100">
{tag}
</span>
))}
</div>
)}
</h2>
<p className="text-slate-600 text-lg leading-relaxed mb-6">
{asset.content || asset.description}
</p>
{/* Additional Metadata if needed */}
<div className="grid grid-cols-2 gap-4 text-sm text-slate-500 bg-slate-50 p-4 rounded-xl">
<div>
<span className="block font-semibold text-slate-900 mb-1">Asset Type</span>
{asset.type}
</div>
<div>
<span className="block font-semibold text-slate-900 mb-1">Last Updated</span>
2024.05.20
</div>
</div>
</div>
{/* Right: Sales Script (The "Key" feature) */}
<div className="w-full md:w-96 bg-brand-50 rounded-xl p-6 border border-brand-100 flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="font-bold text-brand-900 flex items-center gap-2">
<MessageSquare size={18} />
Sales Pitch Script
</h3>
<button
onClick={handleCopyScript}
className="text-xs flex items-center gap-1 text-brand-600 hover:text-brand-800 font-medium transition-colors"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
<div className="flex-1 bg-white p-4 rounded-lg text-slate-700 text-sm leading-relaxed shadow-sm border border-brand-100 italic relative">
<span className="absolute top-2 left-2 text-4xl text-brand-100 font-serif leading-none"></span>
<div className="relative z-10 pt-2 pb-2 px-1">
{asset.script ? asset.script : "No specific script available for this asset. Use the description as a guide."}
</div>
<span className="absolute bottom-[-10px] right-4 text-4xl text-brand-100 font-serif leading-none rotate-180"></span>
</div>
<div className="mt-4 pt-4 border-t border-brand-200">
<p className="text-xs text-brand-600 text-center font-medium">
Tip: 고객의 .
</p>
</div>
</div>
</div>
</div>
</div>
{/* Footer Actions */}
<div className="p-4 border-t border-slate-100 bg-slate-50 flex justify-end gap-3">
<button
onClick={onClose}
className="px-5 py-2.5 rounded-xl border border-slate-300 text-slate-700 font-medium hover:bg-slate-100 transition-colors"
>
Close
</button>
<button className="px-5 py-2.5 rounded-xl bg-slate-900 text-white font-medium hover:bg-slate-800 transition-colors shadow-lg flex items-center gap-2">
Share Asset Link <Maximize2 size={16} />
</button>
</div>
</div>
</div>
);
};
export default AssetDetailModal;

View File

@@ -0,0 +1,160 @@
import React, { useState, useRef, useEffect } from 'react';
import { GoogleGenAI } from "@google/genai";
import { Message } from '../types';
import { SYSTEM_INSTRUCTION } from '../constants';
import { MessageSquare, X, Send, Loader2, Sparkles } from 'lucide-react';
const Assistant: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{ role: 'model', text: '안녕하십니까! CodeBridgeX 영업 지원 AI입니다. CEO 설득을 위한 핵심 스크립트나 자료가 필요하신가요?' }
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isOpen]);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage = input.trim();
setInput('');
setMessages(prev => [...prev, { role: 'user', text: userMessage }]);
setIsLoading(true);
try {
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error("API Key is missing");
}
const ai = new GoogleGenAI({ apiKey });
// Build history for context
const history = messages.map(m => ({
role: m.role,
parts: [{ text: m.text }]
}));
const chat = ai.chats.create({
model: 'gemini-2.5-flash',
config: {
systemInstruction: SYSTEM_INSTRUCTION,
},
history: history
});
const result = await chat.sendMessageStream({ message: userMessage });
let fullResponse = '';
setMessages(prev => [...prev, { role: 'model', text: '' }]); // Placeholder
for await (const chunk of result) {
const text = chunk.text;
if (text) {
fullResponse += text;
setMessages(prev => {
const newMsgs = [...prev];
newMsgs[newMsgs.length - 1].text = fullResponse;
return newMsgs;
});
}
}
} catch (error) {
console.error("AI Error:", error);
setMessages(prev => [...prev, { role: 'model', text: '죄송합니다. 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' }]);
} finally {
setIsLoading(false);
}
};
return (
<>
{/* Floating Action Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-105 ${
isOpen ? 'bg-slate-800 text-white rotate-90' : 'bg-brand-600 text-white'
}`}
>
{isOpen ? <X size={24} /> : <MessageSquare size={24} />}
</button>
{/* Chat Window */}
<div
className={`fixed bottom-24 right-6 z-40 w-[90vw] md:w-[380px] bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden flex flex-col transition-all duration-300 origin-bottom-right transform ${
isOpen ? 'scale-100 opacity-100 translate-y-0' : 'scale-90 opacity-0 translate-y-10 pointer-events-none'
}`}
style={{ height: '600px', maxHeight: '75vh' }}
>
{/* Header */}
<div className="bg-slate-900 p-4 flex items-center gap-3">
<div className="bg-brand-500 p-2 rounded-lg">
<Sparkles size={18} className="text-white" />
</div>
<div>
<h3 className="text-white font-bold text-sm">SAM Sales Assistant</h3>
<p className="text-slate-400 text-xs">Powered by Gemini 2.5 Flash</p>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 bg-slate-50 space-y-4">
{messages.map((msg, idx) => (
<div
key={idx}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[85%] p-3 rounded-2xl text-sm leading-relaxed ${
msg.role === 'user'
? 'bg-brand-600 text-white rounded-tr-none'
: 'bg-white text-slate-800 border border-slate-200 rounded-tl-none shadow-sm'
}`}
>
{msg.text}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-white p-3 rounded-2xl rounded-tl-none border border-slate-200 shadow-sm">
<Loader2 size={16} className="animate-spin text-brand-500" />
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="p-3 bg-white border-t border-slate-100">
<div className="flex items-center gap-2 bg-slate-100 rounded-full px-4 py-2 border border-transparent focus-within:border-brand-300 focus-within:bg-white transition-colors">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder="제안 스크립트를 요청하세요..."
className="flex-1 bg-transparent outline-none text-sm text-slate-800 placeholder:text-slate-400"
disabled={isLoading}
/>
<button
onClick={handleSend}
disabled={isLoading || !input.trim()}
className="text-brand-600 hover:text-brand-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Send size={18} />
</button>
</div>
</div>
</div>
</>
);
};
export default Assistant;

View File

@@ -0,0 +1,124 @@
import React from 'react';
import { SalesAsset, AssetType } from '../types';
import { Play, ArrowUpRight, BarChart3, Image as ImageIcon } from 'lucide-react';
interface SalesCardProps {
asset: SalesAsset;
onClick: (asset: SalesAsset) => void;
}
const SalesCard: React.FC<SalesCardProps> = ({ asset, onClick }) => {
const baseClasses = `
group relative overflow-hidden rounded-2xl bg-white border border-slate-100 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 ease-out cursor-pointer
${asset.gridSpan === 'col-span-2' ? 'md:col-span-2' : 'md:col-span-1'}
${asset.rowSpan === 'row-span-2' ? 'md:row-span-2' : 'md:row-span-1'}
flex flex-col
`;
// Handle click properly
const handleClick = (e: React.MouseEvent) => {
// Prevent triggering if clicking specific action buttons if needed,
// but generally the whole card should open the modal.
onClick(asset);
};
// Render Video Card
if (asset.type === AssetType.VIDEO) {
return (
<div className={`${baseClasses} bg-slate-900 text-white min-h-[300px]`} onClick={handleClick}>
<div className="absolute inset-0 w-full h-full pointer-events-none">
{/* Pointer events none on iframe to allow card click, or use a cover div */}
<iframe
src={`https://player.vimeo.com/video/${asset.src}?background=1&autoplay=0&loop=0&byline=0&title=0`}
className="w-full h-full object-cover opacity-60 group-hover:opacity-40 transition-opacity duration-500"
frameBorder="0"
allow="autoplay; fullscreen; picture-in-picture"
title={asset.videoTitle}
></iframe>
</div>
<div className="relative z-10 p-6 flex flex-col h-full justify-between pointer-events-none">
<div className="flex justify-between items-start">
<span className="bg-red-600 text-white text-xs font-bold px-2 py-1 rounded-full uppercase tracking-wider flex items-center gap-1">
<Play size={10} fill="currentColor" /> Video
</span>
</div>
<div>
<h3 className="text-2xl font-bold mb-2 leading-tight">{asset.title}</h3>
<p className="text-slate-300 text-sm line-clamp-2">{asset.description}</p>
</div>
<div className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-white/80 group-hover:text-red-400 transition-colors">
<ArrowUpRight size={16} />
</div>
</div>
</div>
);
}
// Render Image Card
if (asset.type === AssetType.IMAGE) {
return (
<div className={`${baseClasses} min-h-[250px]`} onClick={handleClick}>
<div className="absolute inset-0">
<img
src={asset.src}
alt={asset.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-80" />
</div>
<div className="relative z-10 p-6 mt-auto text-white h-full flex flex-col justify-end">
<div className="mb-auto">
<span className="bg-white/20 backdrop-blur-md text-white text-xs font-bold px-2 py-1 rounded-full uppercase tracking-wider inline-flex items-center gap-1">
<ImageIcon size={10} /> Image
</span>
</div>
<h3 className="text-xl font-bold mb-1">{asset.title}</h3>
<p className="text-slate-200 text-sm">{asset.description}</p>
</div>
</div>
);
}
// Render Stat Card
if (asset.type === AssetType.STAT) {
return (
<div className={`${baseClasses} bg-brand-600 text-white p-6 flex flex-col justify-between min-h-[200px]`} onClick={handleClick}>
<div className="flex justify-between items-start opacity-80">
<BarChart3 size={24} />
<span className="text-xs font-medium bg-white/10 px-2 py-1 rounded-lg">{asset.tags[0]}</span>
</div>
<div className="mt-4">
<div className="text-5xl font-extrabold tracking-tight mb-1">{asset.statValue}</div>
<div className="text-brand-100 font-medium">{asset.statLabel}</div>
</div>
<div className="mt-4 text-xs text-brand-100 opacity-60 flex items-center justify-between">
<span>* </span>
<ArrowUpRight size={16} />
</div>
</div>
);
}
// Render Text Card (Default)
return (
<div className={`${baseClasses} p-6 flex flex-col justify-between min-h-[200px] hover:border-brand-200`} onClick={handleClick}>
<div>
<div className="flex flex-wrap gap-2 mb-4">
{asset.tags.map(tag => (
<span key={tag} className="text-[10px] uppercase font-bold text-slate-500 bg-slate-100 px-2 py-1 rounded-md">
{tag}
</span>
))}
</div>
<h3 className="text-xl font-bold text-slate-900 mb-3">{asset.title}</h3>
<p className="text-slate-600 text-sm leading-relaxed line-clamp-4">{asset.content}</p>
</div>
<div className="mt-6 pt-4 border-t border-slate-100 flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium group-hover:text-brand-600 transition-colors"> </span>
<ArrowUpRight size={16} className="text-slate-400 group-hover:text-brand-600 transition-colors" />
</div>
</div>
);
};
export default SalesCard;

119
ref/constants.ts Normal file
View File

@@ -0,0 +1,119 @@
import { SalesAsset, AssetType } from './types';
export const SYSTEM_INSTRUCTION = `
You are "SAM AI", an expert sales assistant for CodeBridgeX's SAM (Smart Automation Management) solution.
Your target audience is Sales Managers pitching to SMB CEOs.
Key Value Proposition:
"SAM is not just an ERP for staff; it is a Weapon for the CEO."
"ERP is a tool for employees to report; SAM is a dashboard for the CEO to control."
Core Features to Highlight:
1. **CEO-Centric View:** Real-time dashboard replacing manual reports.
2. **Financial Intelligence:** Real-time calculation of suspense payment interest (4.6%), estimated VAT, and entertainment expense limits.
3. **Legal Support:** Built-in debt collection management with a dedicated lawyer (0 won retainer, 20% success fee).
4. **Emotional UX:** "SAM~" notification sound for new orders/payments to boost morale.
5. **Infrastructure:** Unlimited IP access (mobile included) and free GPS-based attendance tracking.
When answering, guide the sales rep on how to overcome objections using these points. Emphasize that this is a "Management Instrument," not just software.
`;
export const SALES_ASSETS: SalesAsset[] = [
{
id: '1',
type: AssetType.TEXT,
title: 'Concept: 대표를 위한 무기',
content: '기존 ERP는 직원의 관리 도구였지만, SAM은 대표님의 의사결정 무기입니다. 직원의 보고를 기다리지 마십시오. SAM이 대표님께 직접, 실시간으로 회사의 현황을 보고합니다.',
script: "대표님, ERP나 MES 들어보셨죠? 보통 직원들이 입력하고 관리하는 도구입니다. 정작 대표님은 직원한테 보고를 받아야만 회사를 알 수 있죠. SAM은 반대입니다. 직원이 아니라 '대표님을 위한 무기'입니다. 외근 중이든 집이든, 대표님 폰에서 회사의 자금, 인력, 리스크가 한눈에 보입니다.",
tags: ['Concept', 'Pitch', 'Opener'],
gridSpan: 'col-span-2',
rowSpan: 'row-span-1'
},
{
id: '2',
type: AssetType.STAT,
title: '법무 지원 비용',
statValue: '0원',
statLabel: '변호사 착수금',
script: "미수금 문제로 변호사 찾으시면 기본 착수금만 300~500만원 달라고 하죠? 배보다 배꼽이 더 큽니다. SAM을 쓰시면 '착수금 0원'에 전담 법무팀이 생깁니다. 성공보수만 20% 주시면 됩니다. 못 받으면 비용도 없습니다.",
tags: ['Legal', 'Benefit'],
gridSpan: 'col-span-1',
rowSpan: 'row-span-1'
},
{
id: '3',
type: AssetType.VIDEO,
title: 'CEO 시크릿 대시보드',
src: '76979871', // Placeholder for Dashboard Demo
videoTitle: 'CEO Dashboard Demo',
description: '가지급금 이자 4.6%, 예상 부가세, 미수금 현황이 오직 대표님 화면에만 실시간으로 계산되어 표시됩니다.',
script: "이 화면은 직원들은 못 봅니다. 오직 대표님 아이디로 로그인했을 때만 뜹니다. 여기 붉은 글씨 보이시죠? 현재 가지급금에 대한 인정이자 4.6%가 실시간으로 계산돼서 '대표님, 세금 폭탄 조심하세요'라고 경고해주는 겁니다. 세무사가 알려주기 전에 SAM이 먼저 알려드립니다.",
tags: ['Demo', 'Dashboard', 'Finance'],
gridSpan: 'col-span-2',
rowSpan: 'row-span-2'
},
{
id: '4',
type: AssetType.TEXT,
title: '20가지 고충 해결',
content: '사람(근태, 인수인계), 돈(세금, 미수금), 운영(현장관리), 대표의 삶(리스크). CEO가 겪는 20가지 핵심 고충을 시스템 하나로 방어합니다.',
script: "중소기업 대표님의 머릿속을 20가지로 정리해봤습니다. 직원 근태, 자금 압박, 세무 조사... 이 모든 걸 혼자 감당하고 계시지 않습니까? SAM은 단순 프로그램이 아니라, 이 20가지 리스크를 막아주는 방패입니다.",
tags: ['Pain Points', 'Solution'],
gridSpan: 'col-span-1',
rowSpan: 'row-span-1'
},
{
id: '5',
type: AssetType.IMAGE,
title: '모바일 & 감성 알림',
src: 'https://images.unsplash.com/photo-1556742049-0cfed4f7a07d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
description: '수주/입금 시 울리는 "SAM~" 알림음. 외근 중에도 회사가 돌아가는 소리를 들으세요.',
script: "가장 인기 있는 기능입니다. 외근 나가 계실 때 불안하시죠? 직원이 큰 수주를 따오거나, 거래처에서 돈을 입금하면 대표님 폰에서 'SAM~' 하고 알림이 옵니다. 그 소리만 들으면 '아, 우리 회사 잘 돌아가고 있구나' 안심이 되실 겁니다.",
tags: ['Mobile', 'UX', 'Emotion'],
gridSpan: 'col-span-1',
rowSpan: 'row-span-1'
},
{
id: '6',
type: AssetType.STAT,
title: '인프라 혁신',
statValue: '무제한',
statLabel: '접속 IP 및 모바일',
script: "타사 ERP는 아이디 하나 추가할 때마다 돈 받죠? 접속 IP도 3개로 제한하고요. 저희는 통 큽니다. 직원 100명이 써도, 집에서 쓰든 카페에서 쓰든 '무제한'입니다. 추가 비용 걱정 없이 맘껏 쓰십시오.",
tags: ['Infra', 'GPS', 'Attendance'],
gridSpan: 'col-span-1',
rowSpan: 'row-span-1'
},
{
id: '7',
type: AssetType.TEXT,
title: '자동화 & 인수인계',
content: '직원이 갑자기 퇴사해도 걱정 없습니다. 견적부터 발주, 출고까지 모든 이력이 클릭 한 번으로 자동 인수인계됩니다. USB를 찾을 필요가 없습니다.',
script: "직원이 갑자기 그만둔다고 하면 눈앞이 캄캄하시죠? 파일 어디 있냐, 거래처 연락처 뭐냐... SAM을 쓰시면 그럴 일 없습니다. 모든 업무 기록이 서버에 남기 때문에, 후임자는 '클릭' 한 번이면 전임자의 모든 업무를 그대로 이어받습니다.",
tags: ['Automation', 'Management'],
gridSpan: 'col-span-1',
rowSpan: 'row-span-1'
},
{
id: '8',
type: AssetType.VIDEO,
title: '채권 추심 & 법무',
src: '824804225', // Placeholder for Legal Feature
videoTitle: 'Legal Support System',
description: '미수금 소송 포기하지 마세요. SAM 도입 시 전담 법무팀/심사팀이 배정됩니다. 성공보수 20%, 법원 출석 대행까지.',
script: "거래처가 돈 안 주고 버티면 대표님이 직접 전화해서 싫은 소리 하셔야 하죠? 이제 SAM 버튼만 누르세요. 내용증명 발송부터 가압류, 소송까지 저희 변호사가 알아서 처리하고 진행 상황을 앱으로 보고합니다. 감정 소모는 저희가 하겠습니다.",
tags: ['Legal', 'Risk'],
gridSpan: 'col-span-2',
rowSpan: 'row-span-1'
},
{
id: '9',
type: AssetType.TEXT,
title: '도입 제안 (Closing)',
content: '월 구독료로 수천만 원대 맞춤형 ERP 기능과 개인 비서, 전담 변호사를 고용하는 효과를 누리십시오. 내일부터는 "걱정" 대신 "설렘"으로 출근하십시오.',
script: "직원 한 명 월급의 1/10도 안 되는 비용입니다. 이 돈으로 24시간 비서, 전담 변호사, 그리고 완벽한 경영 시스템을 고용하시는 겁니다. 오늘 결정하시고, 내일부터는 가벼운 마음으로 출근하십시오.",
tags: ['Closing', 'Pricing'],
gridSpan: 'col-span-1',
rowSpan: 'row-span-1'
},
];

63
ref/index.html Normal file
View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SAM - Sales Asset Manager</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: {
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
500: '#0ea5e9',
600: '#0284c7',
900: '#0c4a6e',
}
}
}
}
}
</script>
<style>
/* Custom Scrollbar for a sleek look */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
<script type="importmap">
{
"imports": {
"react/": "https://esm.sh/react@^19.2.3/",
"react": "https://esm.sh/react@^19.2.3",
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"@google/genai": "https://esm.sh/@google/genai@^1.33.0",
"lucide-react": "https://esm.sh/lucide-react@^0.561.0"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

15
ref/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
ref/metadata.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "SAM - CodeBridgeX Sales",
"description": "Sales enablement platform for CodeBridgeX SAM - The ERP for CEOs.",
"requestFramePermissions": []
}

23
ref/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "sam---codebridgex-sales",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.3",
"react-dom": "^19.2.3",
"@google/genai": "^1.33.0",
"lucide-react": "^0.561.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

29
ref/tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

27
ref/types.ts Normal file
View File

@@ -0,0 +1,27 @@
export enum AssetType {
IMAGE = 'IMAGE',
VIDEO = 'VIDEO',
TEXT = 'TEXT',
STAT = 'STAT'
}
export interface SalesAsset {
id: string;
type: AssetType;
title: string;
description?: string;
src?: string; // Image URL or Vimeo ID
videoTitle?: string;
content?: string; // For text based cards
script?: string; // The verbal script for sales reps
statValue?: string; // For stat cards
statLabel?: string;
tags: string[];
gridSpan?: 'col-span-1' | 'col-span-2'; // For layout control
rowSpan?: 'row-span-1' | 'row-span-2';
}
export interface Message {
role: 'user' | 'model';
text: string;
}