331 lines
15 KiB
PHP
331 lines
15 KiB
PHP
|
|
<?php
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/session.php");
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/load_header.php");
|
||
|
|
|
||
|
|
// chatbot/md_rag/upload.php
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . "/myheader.php");
|
||
|
|
require_once($_SERVER['DOCUMENT_ROOT'] . '/vendor/autoload.php'); // Load Google Client
|
||
|
|
|
||
|
|
// --- GCS Configuration ---
|
||
|
|
$projectRoot = $_SERVER['DOCUMENT_ROOT']; // Adjust if needed
|
||
|
|
$credentialsPath = $projectRoot . "/apikey/google_service_account.json";
|
||
|
|
// Read Bucket Name from config or hardcode as per plan
|
||
|
|
$bucketName = 'codebridge-speech-audio-files';
|
||
|
|
$folderPrefix = 'tenant_knowledge_base/';
|
||
|
|
|
||
|
|
// Initialize Google Client
|
||
|
|
$client = new Google_Client();
|
||
|
|
$client->setAuthConfig($credentialsPath);
|
||
|
|
$client->addScope(Google_Service_Storage::CLOUD_PLATFORM);
|
||
|
|
$storage = new Google_Service_Storage($client);
|
||
|
|
|
||
|
|
$msg = "";
|
||
|
|
|
||
|
|
// Handle File Upload
|
||
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['md_file'])) {
|
||
|
|
$file = $_FILES['md_file'];
|
||
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||
|
|
|
||
|
|
if ($ext !== 'md') {
|
||
|
|
$msg = "오직 .md 파일만 업로드 가능합니다.";
|
||
|
|
} else {
|
||
|
|
$gcsObjectName = $folderPrefix . basename($file['name']);
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Upload to GCS
|
||
|
|
$fileContent = file_get_contents($file['tmp_name']);
|
||
|
|
$postBody = new Google_Service_Storage_StorageObject();
|
||
|
|
$postBody->setName($gcsObjectName);
|
||
|
|
|
||
|
|
$storage->objects->insert($bucketName, $postBody, [
|
||
|
|
'data' => $fileContent,
|
||
|
|
'mimeType' => 'text/markdown',
|
||
|
|
'uploadType' => 'media'
|
||
|
|
]);
|
||
|
|
|
||
|
|
$msg = "GCS 업로드 성공: " . htmlspecialchars($file['name']);
|
||
|
|
} catch (Exception $e) {
|
||
|
|
$msg = "GCS 업로드 실패: " . $e->getMessage();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle File Delete
|
||
|
|
if (isset($_GET['del'])) {
|
||
|
|
$delFileName = $_GET['del']; // This should be the simple filename
|
||
|
|
$gcsObjectName = $folderPrefix . basename($delFileName);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$storage->objects->delete($bucketName, $gcsObjectName);
|
||
|
|
$msg = "GCS 파일 삭제 완료: " . htmlspecialchars($delFileName);
|
||
|
|
} catch (Exception $e) {
|
||
|
|
$msg = "GCS 삭제 실패: " . $e->getMessage();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// List Files from GCS
|
||
|
|
$files = [];
|
||
|
|
try {
|
||
|
|
$objects = $storage->objects->listObjects($bucketName, ['prefix' => $folderPrefix]);
|
||
|
|
// The list includes the folder itself sometimes, and full paths
|
||
|
|
if ($objects->getItems()) {
|
||
|
|
foreach ($objects->getItems() as $object) {
|
||
|
|
$name = $object->getName();
|
||
|
|
// Filter out the folder itself "tenant_knowledge_base/"
|
||
|
|
if ($name === $folderPrefix) continue;
|
||
|
|
|
||
|
|
// Extract pure filename
|
||
|
|
$pureName = str_replace($folderPrefix, '', $name);
|
||
|
|
if(empty($pureName)) continue;
|
||
|
|
|
||
|
|
$size = $object->getSize(); // bytes
|
||
|
|
|
||
|
|
$files[] = [
|
||
|
|
'name' => $pureName,
|
||
|
|
'size' => $size
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (Exception $e) {
|
||
|
|
$msg = "파일 목록 로드 실패: " . $e->getMessage();
|
||
|
|
}
|
||
|
|
?>
|
||
|
|
|
||
|
|
|
||
|
|
<!-- Fonts: Pretendard -->
|
||
|
|
<link rel="stylesheet" as="style" crossorigin href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.8/dist/web/static/pretendard.css" />
|
||
|
|
|
||
|
|
<!-- Tailwind CSS (Prefixed to avoid Bootstrap conflict) -->
|
||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
|
<script>
|
||
|
|
tailwind.config = {
|
||
|
|
prefix: 'tw-',
|
||
|
|
corePlugins: {
|
||
|
|
preflight: false, // Disable base reset to protect Bootstrap navbar
|
||
|
|
},
|
||
|
|
theme: {
|
||
|
|
extend: {
|
||
|
|
fontFamily: {
|
||
|
|
sans: ['Pretendard', 'sans-serif'],
|
||
|
|
},
|
||
|
|
colors: {
|
||
|
|
primary: '#2563eb', // blue-600
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
/* Local Scoped Styles for smoother integration */
|
||
|
|
.tw-wrapper {
|
||
|
|
font-family: 'Pretendard', sans-serif;
|
||
|
|
background-color: #f8fafc; /* slate-50 */
|
||
|
|
}
|
||
|
|
.tw-card {
|
||
|
|
background-color: white;
|
||
|
|
border-radius: 1rem; /* rounded-2xl */
|
||
|
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); /* shadow-md */
|
||
|
|
border: 1px solid #f1f5f9; /* border-slate-100 */
|
||
|
|
}
|
||
|
|
.tw-btn-primary {
|
||
|
|
background-color: #2563eb;
|
||
|
|
color: white;
|
||
|
|
transition: all 0.2s;
|
||
|
|
}
|
||
|
|
.tw-btn-primary:hover {
|
||
|
|
background-color: #1d4ed8;
|
||
|
|
}
|
||
|
|
/* Input file styling override */
|
||
|
|
input[type=file]::file-selector-button {
|
||
|
|
background-color: #f1f5f9;
|
||
|
|
border: 0;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
margin-right: 1rem;
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
color: #475569;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background .2s;
|
||
|
|
}
|
||
|
|
input[type=file]::file-selector-button:hover {
|
||
|
|
background-color: #e2e8f0;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<div class="page-wrapper tw-wrapper tw-p-6">
|
||
|
|
<div class="page-content">
|
||
|
|
<!-- Breadcrumb (Styled to fit quietly) -->
|
||
|
|
<div class="tw-mb-6 tw-flex tw-items-center tw-justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 class="tw-text-2xl tw-font-bold tw-text-slate-800">챗봇 지식 관리 (GCS)</h1>
|
||
|
|
<p class="tw-text-slate-500 tw-text-sm tw-mt-1">Tenant Knowledge Base (MD) - Google Storage</p>
|
||
|
|
</div>
|
||
|
|
<nav class="tw-text-sm tw-text-slate-400">
|
||
|
|
<ol class="tw-flex tw-gap-2">
|
||
|
|
<li><a href="/" class="hover:tw-text-blue-600"><i class="bx bx-home-alt"></i></a></li>
|
||
|
|
<li>/</li>
|
||
|
|
<li>챗봇 관리</li>
|
||
|
|
<li>/</li>
|
||
|
|
<li class="tw-text-slate-600 tw-font-medium">지식관리</li>
|
||
|
|
</ol>
|
||
|
|
</nav>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<?php if($msg): ?>
|
||
|
|
<div class="tw-mb-6 tw-bg-blue-50 tw-border tw-border-blue-100 tw-text-blue-700 tw-px-4 tw-py-3 tw-rounded-xl tw-flex tw-items-center tw-justify-between">
|
||
|
|
<div class="tw-flex tw-items-center tw-gap-2">
|
||
|
|
<i class="bi bi-info-circle-fill"></i>
|
||
|
|
<span><?= $msg ?></span>
|
||
|
|
</div>
|
||
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
|
|
</div>
|
||
|
|
<?php endif; ?>
|
||
|
|
|
||
|
|
<div class="row tw-gap-y-6">
|
||
|
|
<!-- 1. File Upload -->
|
||
|
|
<div class="col-12 col-lg-6">
|
||
|
|
<div class="tw-card tw-p-6 tw-h-full">
|
||
|
|
<div class="tw-flex tw-items-center tw-gap-3 tw-mb-4">
|
||
|
|
<div class="tw-w-10 tw-h-10 tw-rounded-full tw-bg-blue-100 tw-flex tw-items-center tw-justify-center tw-text-blue-600">
|
||
|
|
<i class="bi bi-cloud-upload tw-text-xl"></i>
|
||
|
|
</div>
|
||
|
|
<h2 class="tw-text-lg tw-font-bold tw-text-slate-800 tw-m-0">파일 업로드</h2>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<form method="post" enctype="multipart/form-data" class="tw-space-y-4">
|
||
|
|
<div>
|
||
|
|
<label for="md_file" class="tw-block tw-text-sm tw-font-medium tw-text-slate-700 tw-mb-2">Markdown 파일 (.md)</label>
|
||
|
|
<input class="form-control tw-block tw-w-full tw-text-sm tw-text-slate-500
|
||
|
|
file:tw-mr-4 file:tw-py-2 file:tw-px-4
|
||
|
|
file:tw-rounded-full file:tw-border-0
|
||
|
|
file:tw-text-sm file:tw-font-semibold
|
||
|
|
file:tw-bg-blue-50 file:tw-text-blue-700
|
||
|
|
hover:file:tw-bg-blue-100
|
||
|
|
tw-border-gray-200 tw-rounded-xl tw-p-2"
|
||
|
|
type="file" id="md_file" name="md_file" accept=".md" required>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="tw-bg-slate-50 tw-p-4 tw-rounded-xl tw-text-xs tw-text-slate-500 tw-space-y-1">
|
||
|
|
<p class="tw-flex tw-gap-2"><i class="bi bi-check-circle tw-text-emerald-500"></i> Notion 등에서 Export한 Markdown(.md) 파일을 업로드하세요.</p>
|
||
|
|
<p class="tw-flex tw-gap-2"><i class="bi bi-check-circle tw-text-emerald-500"></i> 파일명은 문서의 제목으로 사용됩니다.</p>
|
||
|
|
<p class="tw-flex tw-gap-2"><i class="bi bi-google tw-text-blue-500"></i> Google Cloud Storage에 저장됩니다.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button type="submit" class="tw-w-full tw-btn-primary tw-py-3 tw-rounded-xl tw-font-semibold tw-flex tw-items-center tw-justify-center tw-gap-2 tw-shadow-blue-200 tw-shadow-lg transform active:tw-scale-[0.98] tw-transition-all">
|
||
|
|
<i class="bi bi-upload"></i> GCS로 업로드 하기
|
||
|
|
</button>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 2. File List -->
|
||
|
|
<div class="col-12 col-lg-6">
|
||
|
|
<!-- Ingest Status Card (Sticky Top of Column) -->
|
||
|
|
<div class="tw-card tw-p-6 tw-mb-6 tw-bg-gradient-to-br tw-from-slate-800 tw-to-slate-900 tw-text-white tw-border-0">
|
||
|
|
<div class="tw-flex tw-justify-between tw-items-start tw-mb-4">
|
||
|
|
<div>
|
||
|
|
<h2 class="tw-text-lg tw-font-bold tw-text-white tw-m-0">AI 학습 데이터 생성</h2>
|
||
|
|
<p class="tw-text-slate-400 tw-text-sm tw-mt-1">GCS 파일을 분석하여 벡터 DB를 갱신합니다.</p>
|
||
|
|
</div>
|
||
|
|
<div class="tw-w-10 tw-h-10 tw-rounded-full tw-bg-white/10 tw-flex tw-items-center tw-justify-center tw-backdrop-blur-sm">
|
||
|
|
<i class="bi bi-cpu tw-text-xl"></i>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button onclick="startIngestion()" class="tw-w-full tw-bg-white tw-text-slate-900 hover:tw-bg-blue-50 tw-py-3 tw-rounded-xl tw-font-bold tw-flex tw-items-center tw-justify-center tw-gap-2 tw-shadow-lg transform active:tw-scale-[0.98] tw-transition-all">
|
||
|
|
<i class="bi bi-lightning-charge-fill tw-text-yellow-500"></i> 학습 시작 (Vectorize)
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<div id="ingestStatus" class="tw-mt-4" style="display:none;">
|
||
|
|
<div class="progress tw-h-2 tw-bg-white/20 tw-rounded-full tw-overflow-hidden">
|
||
|
|
<div id="progressBar" class="progress-bar tw-bg-blue-500"
|
||
|
|
role="progressbar" style="width: 0%"></div>
|
||
|
|
</div>
|
||
|
|
<small id="statusText" class="tw-block tw-text-center tw-text-blue-300 tw-mt-2 tw-text-xs">대기중...</small>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- File List Table -->
|
||
|
|
<div class="tw-card tw-p-0 tw-overflow-hidden">
|
||
|
|
<div class="tw-p-5 tw-border-b tw-border-slate-100 tw-flex tw-justify-between tw-items-center">
|
||
|
|
<h5 class="tw-font-bold tw-text-slate-800 tw-m-0">GCS 파일 목록 <span class="tw-text-blue-600 tw-text-sm tw-font-normal tw-ml-1">(Total: <?= count($files) ?>)</span></h5>
|
||
|
|
</div>
|
||
|
|
<div class="tw-max-h-[400px] tw-overflow-y-auto custom-scrollbar">
|
||
|
|
<table class="tw-w-full tw-text-sm tw-text-left">
|
||
|
|
<thead class="tw-bg-slate-50 tw-text-slate-500 tw-uppercase tw-text-xs tw-font-semibold tw-sticky tw-top-0">
|
||
|
|
<tr>
|
||
|
|
<th class="tw-px-6 tw-py-3">파일명</th>
|
||
|
|
<th class="tw-px-6 tw-py-3">크기</th>
|
||
|
|
<th class="tw-px-6 tw-py-3 tw-text-right">관리</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody class="tw-divide-y tw-divide-slate-100">
|
||
|
|
<?php if(empty($files)): ?>
|
||
|
|
<tr><td colspan="3" class="tw-px-6 tw-py-8 tw-text-center tw-text-slate-400">GCS 버킷에 파일이 없습니다.</td></tr>
|
||
|
|
<?php else: ?>
|
||
|
|
<?php foreach($files as $f):
|
||
|
|
$fName = $f['name'];
|
||
|
|
$size = round($f['size']/1024, 1) . ' KB';
|
||
|
|
?>
|
||
|
|
<tr class="hover:tw-bg-slate-50 tw-transition-colors">
|
||
|
|
<td class="tw-px-6 tw-py-3 tw-font-medium tw-text-slate-700">
|
||
|
|
<div class="tw-flex tw-items-center tw-gap-2">
|
||
|
|
<i class="bi bi-file-text-fill tw-text-blue-400"></i>
|
||
|
|
<?= htmlspecialchars($fName) ?>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td class="tw-px-6 tw-py-3 tw-text-slate-500"><?= $size ?></td>
|
||
|
|
<td class="tw-px-6 tw-py-3 tw-text-right">
|
||
|
|
<a href="?del=<?= urlencode($fName) ?>" onclick="return confirm('GCS에서 영구 삭제하시겠습니까?');"
|
||
|
|
class="tw-text-red-400 hover:tw-text-red-600 tw-transition-colors tw-p-2 hover:tw-bg-red-50 tw-rounded-full" title="삭제"><i class="bi bi-trash"></i></a>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<?php endforeach; ?>
|
||
|
|
<?php endif; ?>
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
function startIngestion() {
|
||
|
|
if(!confirm('GCS의 모든 파일을 분석하여 벡터 데이터를 갱신하시겠습니까?')) return;
|
||
|
|
|
||
|
|
document.getElementById('ingestStatus').style.display = 'block';
|
||
|
|
const pBar = document.getElementById('progressBar');
|
||
|
|
const pText = document.getElementById('statusText');
|
||
|
|
|
||
|
|
pBar.style.width = '10%';
|
||
|
|
pText.innerText = '학습 스크립트 실행 중...';
|
||
|
|
|
||
|
|
// Call ingest.php via AJAX
|
||
|
|
fetch('ingest.php')
|
||
|
|
.then(response => response.text())
|
||
|
|
.then(data => {
|
||
|
|
pBar.style.width = '100%';
|
||
|
|
// Tailwind class update
|
||
|
|
pBar.classList.remove('bg-blue-500');
|
||
|
|
pBar.classList.add('bg-green-500');
|
||
|
|
pBar.style.backgroundColor = '#10b981';
|
||
|
|
|
||
|
|
pText.innerText = '완료! 결과: ' + data;
|
||
|
|
pText.classList.remove('tw-text-blue-300');
|
||
|
|
pText.classList.add('tw-text-green-300');
|
||
|
|
|
||
|
|
alert('학습이 완료되었습니다.');
|
||
|
|
})
|
||
|
|
.catch(err => {
|
||
|
|
pBar.style.backgroundColor = '#ef4444'; // red-500
|
||
|
|
pText.innerText = '오류 발생: ' + err;
|
||
|
|
pText.classList.remove('tw-text-blue-300');
|
||
|
|
pText.classList.add('tw-text-red-300');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<?php require_once($_SERVER['DOCUMENT_ROOT'] . "/footer.php"); ?>
|