Files
sam-kd/chatbot/md_rag/upload.php
hskwon aca1767eb9 초기 커밋: 5130 레거시 시스템
- URL 하드코딩 → .env APP_URL 기반 동적 URL로 변경
- DB 연결 하드코딩 → .env 기반으로 변경
- MySQL strict mode DATE 오류 수정
2025-12-10 20:14:31 +09:00

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"); ?>