323 lines
14 KiB
PHP
323 lines
14 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>배포 파일 관리 시스템</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
</head>
|
|
<body class="bg-gray-100 p-6">
|
|
<div class="max-w-screen-xl mx-auto">
|
|
<div class="flex items-center mb-4">
|
|
<svg class="w-7 h-7 text-blue-600 mr-2" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5-5m0 0l5 5m-5-5v12" />
|
|
</svg>
|
|
<span class="text-xl font-bold mr-4">파일 업로드 관리자</span>
|
|
</div>
|
|
|
|
<!-- 헤드 구성 -->
|
|
<div class="bg-white p-4 rounded shadow mb-6 flex items-center justify-between">
|
|
<div class="flex items-center space-x-4">
|
|
<label for="product-select" class="font-bold">제품명(제품코드):</label>
|
|
<select id="product-select" class="p-2 border border-gray-300 rounded">
|
|
<option value="1">EG-BIM</option>
|
|
<option value="2">TOVA</option>
|
|
<option value="3">GAIA</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center space-x-4">
|
|
<div>
|
|
<span class="text-sm">내부 배포 버전(최신):</span>
|
|
<span class="text-blue-600 font-semibold">0.28.1</span>
|
|
</div>
|
|
<div class="flex items-center space-x-1">
|
|
<span class="text-sm">외부 배포 버전:</span>
|
|
<input type="number" id="ver1" class="w-14 p-1 border rounded" min="0" max="99">
|
|
<span class="text-sm">.</span>
|
|
<input type="number" id="ver2" class="w-14 p-1 border rounded" min="0" max="99">
|
|
<span class="text-sm">.</span>
|
|
<input type="number" id="ver3" class="w-14 p-1 border rounded" min="0" max="99">
|
|
</div>
|
|
<button id="save-version" class="bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">저장</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 버튼 영역 -->
|
|
<div class="flex justify-end space-x-2 mb-4">
|
|
<button id="btn-new" class="bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600">+ 신규 버전</button>
|
|
<button class="bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600">다운로드</button>
|
|
<button class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">파일삭제</button>
|
|
</div>
|
|
|
|
<!-- 테이블 -->
|
|
<div class="bg-white shadow rounded-lg overflow-x-auto">
|
|
<table class="min-w-full text-sm text-left">
|
|
<thead class="bg-gray-200">
|
|
<tr>
|
|
<th class="px-4 py-2"><input type="checkbox" id="select-all" /></th>
|
|
<th class="px-4 py-2">번호</th>
|
|
<th class="px-4 py-2">파일명</th>
|
|
<th class="px-4 py-2">파일타입</th>
|
|
<th class="px-4 py-2">버전</th>
|
|
<th class="px-4 py-2">크기</th>
|
|
<th class="px-4 py-2">업로드일</th>
|
|
<th class="px-4 py-2">업로드자</th>
|
|
<th class="px-4 py-2">테스트완료</th>
|
|
<th class="px-4 py-2">테스트자</th>
|
|
<th class="px-4 py-2">릴리즈일</th>
|
|
<th class="px-4 py-2">릴리즈자</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="file-list">
|
|
<!-- JavaScript로 동적 삽입 예정 -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 신규 버전 업로드 팝업(모달) -->
|
|
<div id="upload-modal" class="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white p-8 rounded shadow-lg w-full max-w-lg relative">
|
|
<h2 class="text-xl font-bold mb-6 text-center">
|
|
신규 버전 업로드
|
|
<span class="text-base text-gray-500">(테스트 서버)</span>
|
|
</h2>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex items-center">
|
|
<label class="w-28">S/W 버전 :</label>
|
|
<input type="number" class="border w-14 p-1 mx-1 text-center" id="sw1" min="0">
|
|
<span>.</span>
|
|
<input type="number" class="border w-14 p-1 mx-1 text-center" id="sw2" min="0">
|
|
<span>.</span>
|
|
<input type="number" class="border w-14 p-1 mx-1 text-center" id="sw3" min="0">
|
|
<span class="ml-3 text-gray-500">MAJOR</span>
|
|
</div>
|
|
|
|
<!-- 설치파일 -->
|
|
<div class="flex items-center mb-2">
|
|
<label class="w-39 flex justify-between items-center">
|
|
<span>설치파일(EXE)</span><span>:</span>
|
|
</label>
|
|
<input type="file" class="hidden" id="setup-file" accept=".exe">
|
|
<input type="text" class="border flex-1 p-1 bg-gray-100 ml-2" id="setup-filename" readonly>
|
|
<button type="button" class="ml-2 px-3 py-1 bg-blue-500 text-white rounded" id="setup-add">추가</button>
|
|
</div>
|
|
|
|
<!-- 패치파일 -->
|
|
<div class="flex items-center mb-2">
|
|
<label class="w-39 flex justify-between items-center">
|
|
<span>패치파일(EXE)</span><span>:</span>
|
|
</label>
|
|
<input type="file" class="hidden" id="patch-file" accept=".exe">
|
|
<input type="text" class="border flex-1 p-1 bg-gray-100 ml-2" id="patch-filename" readonly>
|
|
<button type="button" class="ml-2 px-3 py-1 bg-blue-500 text-white rounded" id="patch-add">추가</button>
|
|
</div>
|
|
|
|
<!-- 정보파일 -->
|
|
<div class="flex items-center mb-2">
|
|
<label class="w-39 flex justify-between items-center">
|
|
<span>정보파일</span><span>:</span>
|
|
</label>
|
|
<input type="file" class="hidden" id="info-file" accept=".zip">
|
|
<input type="text" class="border flex-1 p-1 bg-gray-100 ml-2" id="info-filename" readonly>
|
|
<button type="button" class="ml-2 px-3 py-1 bg-blue-500 text-white rounded" id="info-add">추가</button>
|
|
</div>
|
|
|
|
<!-- ✅ 릴리즈노트 -->
|
|
<div class="flex items-center mb-2">
|
|
<label class="w-39 flex justify-between items-center">
|
|
<span>릴리즈노트(MD)</span><span>:</span>
|
|
</label>
|
|
<input type="file" class="hidden" id="release-file" accept=".md">
|
|
<input type="text" class="border flex-1 p-1 bg-gray-100 ml-2" id="release-filename" readonly>
|
|
<button type="button" class="ml-2 px-3 py-1 bg-blue-500 text-white rounded" id="release-add">추가</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-2 mt-6">
|
|
<button id="upload-submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">업로드</button>
|
|
<button id="upload-cancel" class="bg-gray-400 text-white px-4 py-2 rounded hover:bg-gray-500">취소</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 신규버전 버튼 → 팝업
|
|
$('#btn-new').on('click', function() {
|
|
$('#upload-modal').removeClass('hidden');
|
|
});
|
|
// 취소 버튼, 바깥 클릭 → 닫기
|
|
$('#upload-cancel').on('click', function() {
|
|
$('#upload-modal').addClass('hidden');
|
|
});
|
|
$('#upload-modal').on('click', function(e){
|
|
if (e.target === this) $(this).addClass('hidden');
|
|
});
|
|
|
|
// 파일 추가 버튼
|
|
$('#setup-add').on('click', function() { $('#setup-file').click(); });
|
|
$('#patch-add').on('click', function() { $('#patch-file').click(); });
|
|
$('#info-add').on('click', function() { $('#info-file').click(); });
|
|
$('#release-add').on('click', function() { $('#release-file').click(); }); // ✅ 릴리즈노트
|
|
|
|
// 파일명 표시
|
|
$('#setup-file').on('change', function(){ $('#setup-filename').val(this.files[0]?.name || ''); });
|
|
$('#patch-file').on('change', function(){ $('#patch-filename').val(this.files[0]?.name || ''); });
|
|
$('#info-file').on('change', function(){ $('#info-filename').val(this.files[0]?.name || ''); });
|
|
$('#release-file').on('change', function(){ $('#release-filename').val(this.files[0]?.name || ''); });
|
|
|
|
// Presigned 업로드 + DB 저장
|
|
$('#upload-submit').on('click', async function() {
|
|
const productCode = $('#product-select').val();
|
|
const swVersion = [$('#sw1').val(), $('#sw2').val(), $('#sw3').val()].join('.');
|
|
|
|
if (!$('#sw1').val() || !$('#sw2').val() || !$('#sw3').val()) {
|
|
alert("S/W 버전을 입력하세요");
|
|
return;
|
|
}
|
|
|
|
const setupFile = $('#setup-file')[0].files[0];
|
|
const patchFile = $('#patch-file')[0].files[0];
|
|
const infoFile = $('#info-file')[0].files[0];
|
|
const releaseFile = $('#release-file')[0].files[0];
|
|
|
|
const files = [];
|
|
if (setupFile) files.push({name: setupFile.name, type: "setup", size: setupFile.size, file: setupFile});
|
|
if (patchFile) files.push({name: patchFile.name, type: "patch", size: patchFile.size, file: patchFile});
|
|
if (infoFile) files.push({name: infoFile.name, type: "info", size: infoFile.size, file: infoFile});
|
|
if (releaseFile) files.push({name: releaseFile.name, type: "md", size: releaseFile.size, file: releaseFile});
|
|
|
|
try {
|
|
// === 1) Presigned URL 업로드 ===
|
|
for (let f of files) {
|
|
let formData = new FormData();
|
|
formData.append("content_type", f.file.type || "application/octet-stream");
|
|
formData.append("filename", f.name);
|
|
|
|
let presignRes = await fetch("/egbim/sw_upload/upload_presign.php", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
|
|
// 안전하게 JSON 파싱 (디버깅용 로그 포함)
|
|
let text = await presignRes.text();
|
|
let presignData;
|
|
try {
|
|
presignData = JSON.parse(text);
|
|
} catch (e) {
|
|
console.error("❌ JSON 파싱 실패. 원본 응답:", text);
|
|
throw new Error("presign 응답이 JSON이 아님");
|
|
}
|
|
|
|
let putRes = await fetch(presignData.url, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/octet-stream" },
|
|
body: f.file,
|
|
});
|
|
if (!putRes.ok) throw new Error(f.name + " 업로드 실패");
|
|
|
|
f.path = presignData.key || f.name;
|
|
}
|
|
|
|
// === 2) DB Insert ===
|
|
$.post("/egbim/sw_upload/upload_popup.php", {
|
|
product_code: productCode,
|
|
sw_version: swVersion,
|
|
is_major: 0,
|
|
uploader: "송대일",
|
|
files: JSON.stringify(files)
|
|
}, function(res){
|
|
if(res.group){
|
|
renderFilesGrouped(res.group);
|
|
alert("업로드 및 DB 저장 성공!");
|
|
}
|
|
}, "json");
|
|
|
|
// 모달 닫기 + 최신 버전 표시 갱신
|
|
$('#upload-modal').addClass('hidden');
|
|
$('.text-blue-600').text(swVersion);
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
alert("업로드 중 오류 발생: " + err.message);
|
|
}
|
|
});
|
|
|
|
// 테이블 append (버전 묶음)
|
|
function renderFilesGrouped(groupData) {
|
|
let rowspan = groupData.files.length;
|
|
groupData.files.forEach((f, idx) => {
|
|
$('#file-list').append(`
|
|
<tr class="border-b">
|
|
<td class="px-4 py-2"><input type="checkbox" class="file-checkbox" data-id="${f.id}" /></td>
|
|
<td class="px-4 py-2">${f.id}</td>
|
|
<td class="px-4 py-2">${f.filename}</td>
|
|
<td class="px-4 py-2">${f.filetype}</td>
|
|
<td class="px-4 py-2">${f.version}</td>
|
|
<td class="px-4 py-2">${f.size}</td>
|
|
${idx === 0 ? `
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${groupData.upload}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${groupData.uploader}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${groupData.testdone}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${groupData.tester}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${groupData.release}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${groupData.releaser}</td>
|
|
` : ``}
|
|
</tr>
|
|
`);
|
|
});
|
|
}
|
|
|
|
$(document).ready(function(){
|
|
loadFileList();
|
|
});
|
|
|
|
function loadFileList(){
|
|
$.getJSON("/egbim/sw_upload/upload_list.php", function(res){
|
|
$('#file-list').empty();
|
|
if(res.files){
|
|
// 그룹화해서 렌더링
|
|
renderFilesFromDB(res.files);
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderFilesFromDB(files){
|
|
let grouped = {};
|
|
files.forEach(f=>{
|
|
let key = f.sw_version + "_" + f.upload_date;
|
|
if(!grouped[key]) grouped[key] = {meta:f, files:[]};
|
|
grouped[key].files.push(f);
|
|
});
|
|
|
|
Object.values(grouped).forEach(g=>{
|
|
let rowspan = g.files.length;
|
|
g.files.forEach((f, idx)=>{
|
|
$('#file-list').append(`
|
|
<tr class="border-b">
|
|
<td class="px-4 py-2"><input type="checkbox" /></td>
|
|
<td class="px-4 py-2">${f.id}</td>
|
|
<td class="px-4 py-2">${f.setup_filename || f.patch_filename || f.info_filename || f.md_filename}</td>
|
|
<td class="px-4 py-2">${f.filetype}</td>
|
|
<td class="px-4 py-2">${f.sw_version}</td>
|
|
<td class="px-4 py-2">${Math.round(f.filesize/1024/1024*100)/100}MB</td>
|
|
${idx===0?`
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${f.upload_date}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}">${f.uploader}</td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}"><button class="bg-green-500 text-white px-2 py-1 rounded">완료 확정</button></td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}"></td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}"><button class="bg-blue-500 text-white px-2 py-1 rounded">배포 확정</button></td>
|
|
<td class="px-4 py-2" rowspan="${rowspan}"></td>
|
|
`:''}
|
|
</tr>
|
|
`);
|
|
});
|
|
});
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|