최초 커밋

This commit is contained in:
root
2026-07-23 16:15:57 +09:00
commit c8f5efb6b7
14652 changed files with 4812531 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
<!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>