최초 커밋

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
+55
View File
@@ -0,0 +1,55 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json');
// ⭐️ 여기에 추가!
$product_code = $_POST['product_code'] ?? '';
$family_version = $_POST['family_version'] ?? '';
$external_version = $_POST['external_version'] ?? '';
$conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['message' => 'DB 연결 실패: ' . $conn->connect_error]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// FK 에러 방지: products 테이블에 존재하는지 확인
$chk = $conn->prepare("SELECT COUNT(*) FROM products WHERE code=?");
$chk->bind_param("s", $product_code);
$chk->execute();
$chk->bind_result($cnt);
$chk->fetch();
$chk->close();
if ($cnt < 1) {
http_response_code(400);
echo json_encode(['message' => '존재하지 않는 제품코드입니다']);
exit;
}
// 실제 insert/update 쿼리 실행
$sql = "INSERT INTO deploy_versions (product_code, family_version, external_version)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE
family_version=VALUES(family_version),
external_version=VALUES(external_version)";
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['message' => '쿼리 준비 오류: ' . $conn->error]);
exit;
}
$stmt->bind_param("sss", $product_code, $family_version, $external_version);
if ($stmt->execute()) {
echo json_encode(['message' => '저장 완료']);
} else {
http_response_code(500);
echo json_encode(['message' => 'DB 오류: ' . $stmt->error]);
}
$stmt->close();
$conn->close();
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
// upload_common.php
// 파일 업로드 전용 공통 초기화
// (1) Descope 세션 불러오기
require_once __DIR__ . '/../skin/member/basic/descope_session.php';
// (2) 현재 로그인 사용자 정보 안전하게 꺼내기
$currentUserId = $_SESSION['user']['loginIds'][0] ?? '';
$currentUserName = $_SESSION['user']['name'] ?? '알 수 없음';
$currentUserEmail= $_SESSION['user']['email'] ?? '';
// (3) 관리자 여부 플래그
$ADMIN_EMAILS = [
'kjy0426@hanmaceng.co.kr',
'b24014@hanmaceng.co.kr',
'b23065@hanmaceng.co.kr',
'b23008@baroncs.co.kr',
'cjy627@hanmaceng.co.kr',
'b23072@hanmaceng.co.kr',
];
$isAdmin = in_array($currentUserId, $ADMIN_EMAILS, true);
// (4) JSON 응답 시 헤더 자동 세팅 (필요 시)
if (!headers_sent()) {
header("Content-Type: application/json; charset=utf-8");
}
+17
View File
@@ -0,0 +1,17 @@
<?php
header("Content-Type: application/json; charset=utf-8");
$pdo = new PDO("mysql:host=localhost;dbname=egbim;charset=utf8mb4","egbim","baron3840!!",[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$productCode = $_POST['product_code'] ?? '';
$swVersion = $_POST['sw_version'] ?? '';
$st = $pdo->prepare("
SELECT COUNT(*) FROM deploy_files
WHERE product_code=? AND sw_version=? AND deleted=0
");
$st->execute([$productCode, $swVersion]);
echo json_encode(['exists' => $st->fetchColumn() > 0]);
+236
View File
@@ -0,0 +1,236 @@
<?php
// // /egbim/sw_upload/upload_delete.php
// header("Content-Type: application/json; charset=utf-8");
// ini_set('display_errors', 1);
// error_reporting(E_ALL);
// require __DIR__ . '/../vendor/autoload.php'; // AWS SDK
// use Aws\S3\S3Client;
// use Aws\Exception\AwsException;
// // === DB 연결 ===
// $conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
// if ($conn->connect_error) {
// http_response_code(500);
// echo json_encode(['status' => 'fail', 'message' => 'DB 연결 실패']);
// exit;
// }
// mysqli_set_charset($conn, 'utf8mb4');
// // === 파라미터 ===
// $ids = $_POST['ids'] ?? [];
// if (!is_array($ids) || empty($ids)) {
// http_response_code(400);
// echo json_encode(['status' => 'fail', 'message' => '삭제할 ID가 없습니다.']);
// exit;
// }
// // === S3 클라이언트 ===
// $bucket = 'baron-software-test'; // 삭제는 test 버킷에서만
// $s3 = new S3Client([
// 'version' => 'latest',
// 'region' => 'auto',
// 'endpoint' => 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
// 'credentials' => [
// 'key' => '11d7027f505658acb3aeb40c3955a206',
// 'secret' => '65a78f6c582e0bdfccc2f431fc7b7be2ba11f999f0a8a87142011eac8b84761f',
// ]
// ]);
// $success = [];
// $failed = [];
// foreach ($ids as $id) {
// $id = (int)$id;
// $res = $conn->query("SELECT * FROM deploy_files WHERE id={$id} AND deleted=0");
// $file = $res->fetch_assoc();
// if (!$file) {
// $failed[] = ['id' => $id, 'error' => '파일 없음'];
// continue;
// }
// // ❌ 릴리즈된 경우 삭제 금지
// if (!empty($file['release_date'])) {
// $failed[] = ['error' => '이미 릴리즈된 파일은 삭제할 수 없습니다.'];
// continue;
// }
// $objectKey = $file['filepath'];
// try {
// // 1) S3 객체 삭제
// $s3->deleteObject([
// 'Bucket' => $bucket,
// 'Key' => $objectKey,
// ]);
// // 2) DB 플래그 업데이트
// $conn->query("UPDATE deploy_files SET deleted=1 WHERE id={$id}");
// // 3) ✅ 최신 내부 버전 갱신
// $productCode = $conn->real_escape_string($file['product_code']);
// // 삭제 후 남아있는 최신 버전 조회
// $verRes = $conn->prepare("
// SELECT sw_version
// FROM deploy_files
// WHERE product_code=? AND deleted=0
// ORDER BY
// CAST(SUBSTRING_INDEX(sw_version, '.', 1) AS UNSIGNED) DESC,
// CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(sw_version, '.', 2), '.', -1) AS UNSIGNED) DESC,
// CAST(SUBSTRING_INDEX(sw_version, '.', -1) AS UNSIGNED) DESC
// LIMIT 1
// ");
// $verRes->bind_param("s", $productCode);
// $verRes->execute();
// $newLatest = $verRes->get_result()->fetch_assoc()['sw_version'] ?? null;
// if ($newLatest) {
// // 남아있는 최신 버전으로 업데이트
// $conn->query("
// UPDATE deploy_versions_test
// SET family_version='{$newLatest}', updated_at=NOW()
// WHERE product_code='{$productCode}'
// ");
// } else {
// // 남은 버전이 없으면 초기화
// $conn->query("
// UPDATE deploy_versions_test
// SET family_version='0.0.0', updated_at=NOW()
// WHERE product_code='{$productCode}'
// ");
// }
// $success[] = $id; // ✅ id 로 반환
// } catch (AwsException $e) {
// $failed[] = ['id' => $id, 'error' => $e->getAwsErrorMessage()];
// }
// }
// echo json_encode([
// 'status' => empty($failed) ? 'ok' : 'partial',
// 'deleted' => $success,
// 'failed' => $failed,
// ]);
?>
<?php
// /egbim/sw_upload/upload_delete.php
header("Content-Type: application/json; charset=utf-8");
ini_set('display_errors', 1);
error_reporting(E_ALL);
require __DIR__ . '/../vendor/autoload.php'; // AWS SDK
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
// === DB 연결 ===
$conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['status' => 'fail', 'message' => 'DB 연결 실패']);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// === 파라미터 ===
$productCode = $_POST['product_code'] ?? '';
$swVersion = $_POST['sw_version'] ?? '';
if (!$productCode || !$swVersion) {
http_response_code(400);
echo json_encode(['status' => 'fail', 'message' => '삭제할 대상이 없습니다.']);
exit;
}
// === S3 클라이언트 ===
$bucket = 'baron-software-test'; // 삭제는 test 버킷에서만
$s3 = new S3Client([
'version' => 'latest',
'region' => 'auto',
'endpoint' => 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
'credentials' => [
'key' => '11d7027f505658acb3aeb40c3955a206',
'secret' => '65a78f6c582e0bdfccc2f431fc7b7be2ba11f999f0a8a87142011eac8b84761f',
]
]);
$success = [];
$failed = [];
// === 버전 단위 전체 파일 조회 ===
$res = $conn->query("
SELECT * FROM deploy_files
WHERE product_code='{$conn->real_escape_string($productCode)}'
AND sw_version='{$conn->real_escape_string($swVersion)}'
AND deleted=0
");
$files = $res->fetch_all(MYSQLI_ASSOC);
if (!$files) {
echo json_encode(['status'=>'fail','message'=>'해당 버전의 파일이 없습니다.']);
exit;
}
// === 삭제 처리 루프 ===
foreach ($files as $file) {
// ❌ 릴리즈된 경우 → 전체 삭제 불가 처리
if (!empty($file['release_date'])) {
echo json_encode([
'status' => 'fail',
'message' => '이미 릴리즈된 파일은 삭제할 수 없습니다.'
]);
exit;
}
}
// ✅ 릴리즈 안 된 경우만 여기 도달 → 전체 삭제 진행
foreach ($files as $file) {
try {
// 1) S3 객체 삭제
$s3->deleteObject([
'Bucket' => $bucket,
'Key' => $file['filepath'],
]);
// 2) DB 플래그 업데이트
$conn->query("UPDATE deploy_files SET deleted=1 WHERE id={$file['id']}");
$success[] = $file['filename'];
} catch (AwsException $e) {
echo json_encode([
'status' => 'fail',
'message' => '파일 삭제 중 오류: '.$e->getAwsErrorMessage()
]);
exit;
}
}
// === 최신 내부 버전 갱신 ===
$verRes = $conn->query("
SELECT sw_version
FROM deploy_files
WHERE product_code='{$conn->real_escape_string($productCode)}'
AND deleted=0
ORDER BY
CAST(SUBSTRING_INDEX(sw_version, '.', 1) AS UNSIGNED) DESC,
CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(sw_version, '.', 2), '.', -1) AS UNSIGNED) DESC,
CAST(SUBSTRING_INDEX(sw_version, '.', -1) AS UNSIGNED) DESC
LIMIT 1
");
$newLatest = $verRes->fetch_assoc()['sw_version'] ?? '0.0.0';
$conn->query("
UPDATE deploy_versions_test
SET family_version='{$newLatest}', updated_at=NOW()
WHERE product_code='{$conn->real_escape_string($productCode)}'
");
echo json_encode([
'status' => empty($failed) ? 'ok' : 'fail',
'deleted' => $success,
'failed' => $failed,
'new_latest' => $newLatest ?? '0.0.0',
'message' => empty($failed)
? '삭제 완료'
: '삭제 실패: 이미 릴리즈된 파일이 있습니다.'
]);
+155
View File
@@ -0,0 +1,155 @@
<?php
// require __DIR__ . '/../vendor/autoload.php';
// use Aws\S3\S3Client;
// use Aws\Exception\AwsException;
// header("Content-Type: application/zip");
// header("Content-Disposition: attachment; filename=download.zip");
// $product = $_GET['product_code'] ?? '';
// $version = $_GET['sw_version'] ?? '';
// if (!$product || !$version) {
// http_response_code(400);
// echo "필수값 없음";
// exit;
// }
// // === DB 연결 ===
// $conn = new mysqli("localhost", "egbim", "baron3840!!", "egbim");
// mysqli_set_charset($conn, "utf8mb4");
// $res = $conn->query("SELECT filename, original_name, filepath FROM deploy_files
// WHERE product_code='".$conn->real_escape_string($product)."'
// AND sw_version='".$conn->real_escape_string($version)."'
// AND deleted=0");
// $files = $res->fetch_all(MYSQLI_ASSOC);
// if (empty($files)) {
// echo "파일 없음";
// exit;
// }
// // === S3 Client ===
// $s3 = new S3Client([
// 'version' => 'latest',
// 'region' => 'auto',
// 'endpoint'=> 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
// 'credentials' => [
// 'key' => '11d7027f505658acb3aeb40c3955a206',
// 'secret' => '65a78f6c582e0bdfccc2f431fc7b7be2ba11f999f0a8a87142011eac8b84761f',
// ]
// ]);
// // === 임시 ZIP 파일 ===
// $tmpZip = tempnam(sys_get_temp_dir(), "zip");
// $zip = new ZipArchive();
// $zip->open($tmpZip, ZipArchive::OVERWRITE);
// foreach ($files as $f) {
// $key = $f['filepath']; // 예: 999/1.4.6/releasenote_999_1.4.6.md
// $saveName = $f['original_name'] ?: basename($f['filename']);
// try {
// $obj = $s3->getObject([
// 'Bucket' => 'baron-software-release',
// 'Key' => $key
// ]);
// $content = (string)$obj['Body'];
// if ($content !== '') {
// $zip->addFromString($saveName, $content);
// }
// } catch (AwsException $e) {
// error_log("다운로드 실패: $key - " . $e->getMessage());
// }
// }
// $zip->close();
// // === ZIP 출력 ===
// readfile($tmpZip);
// unlink($tmpZip);
// exit;
?>
<?php
require __DIR__ . '/../vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
ini_set('display_errors', 0);
error_reporting(0);
$product = $_GET['product_code'] ?? '';
$version = $_GET['sw_version'] ?? '';
if (!$product || !$version) {
http_response_code(400);
exit("필수값 없음");
}
// === DB 연결 ===
$conn = new mysqli("localhost", "egbim", "baron3840!!", "egbim");
mysqli_set_charset($conn, "utf8mb4");
$sql = "SELECT filename, original_name, filepath, release_date
FROM deploy_files
WHERE product_code=? AND sw_version=? AND deleted=0";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $product, $version);
$stmt->execute();
$res = $stmt->get_result();
$files = $res->fetch_all(MYSQLI_ASSOC);
$stmt->close();
$conn->close();
if (empty($files)) {
http_response_code(404);
exit("파일 없음");
}
// === S3 Client ===
$s3 = new S3Client([
'version' => 'latest',
'region' => 'auto',
'endpoint'=> 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
'credentials' => [
'key' => '11d7027f505658acb3aeb40c3955a206',
'secret' => '65a78f6c582e0bdfccc2f431fc7b7be2ba11f999f0a8a87142011eac8b84761f',
]
]);
// === ZIP 생성 ===
$tmpZip = tempnam(sys_get_temp_dir(), "zip");
$zip = new ZipArchive();
$zip->open($tmpZip, ZipArchive::OVERWRITE);
foreach ($files as $f) {
$key = $f['filepath'];
$saveName = $f['original_name'] ?: basename($f['filename']);
$bucket = $f['release_date'] ? 'baron-software-release' : 'baron-software-test';
try {
$obj = $s3->getObject([
'Bucket' => $bucket,
'Key' => $key
]);
$content = (string)$obj['Body'];
$zip->addFromString($saveName, $content);
} catch (AwsException $e) {
// 실패한 파일은 스킵 (ZIP은 정상 유지)
}
}
$zip->close();
// === ZIP 전송 ===
if (ob_get_level()) ob_end_clean();
header("Content-Type: application/zip");
header("Content-Disposition: attachment; filename=\"{$product}_{$version}.zip\"");
header("Content-Length: " . filesize($tmpZip));
readfile($tmpZip);
unlink($tmpZip);
exit;
+128
View File
@@ -0,0 +1,128 @@
<?php
// header("Content-Type: application/json; charset=utf-8");
// // require_once __DIR__ . '/upload_common.php';
// // require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/bbs/admin_guard.php';
// $conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
// if ($conn->connect_error) {
// http_response_code(500);
// echo json_encode(['message' => 'DB 연결 실패']);
// exit;
// }
// mysqli_set_charset($conn, 'utf8mb4');
// // === 선택된 제품코드 파라미터 ===
// $product_code = $_GET['product_code'] ?? '';
// // TODO: 권한 부여는 추후 적용
// // $user_role = $_SESSION['user']['role'] ?? 'user'; // 예: admin, manager, user
// // if ($user_role !== 'admin') {
// // // 권한 체크 로직 들어갈 자리
// // }
// $sql = "SELECT id, product_code, sw_version, version_type,
// filename, filepath, filetype,
// filesize, uploader,
// DATE_FORMAT(upload_date, '%Y-%m-%d %H:%i:%s') as upload_date,
// DATE_FORMAT(test_done_date, '%Y-%m-%d %H:%i:%s') as test_done_date,
// tester,
// DATE_FORMAT(release_date, '%Y-%m-%d %H:%i:%s') as release_date,
// releaser,
// deleted
// FROM deploy_files
// WHERE 1=1";
// // 제품코드가 지정된 경우만 필터
// if ($product_code !== '') {
// $sql .= " AND product_code = '". $conn->real_escape_string($product_code) ."'";
// }
// $sql .= " ORDER BY id DESC";
// $res = $conn->query($sql);
// $files = [];
// while ($row = $res->fetch_assoc()) {
// $files[] = $row;
// }
// echo json_encode(['files' => $files], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// $conn->close();
?>
<?php
header("Content-Type: application/json; charset=utf-8");
// ✅ 1) Descope 세션 복원 (JWT → $_SESSION['user'])
require_once $_SERVER['DOCUMENT_ROOT'].'/eng/skin/member/basic/descope_session.php';
// ✅ 2) 관리자 권한 체크
require_once $_SERVER['DOCUMENT_ROOT'].'/eng/bbs/upload_admin_guard.php';
// ✅ 관리자만 접근 가능. 일반 사용자는 403 Forbidden
$host = 'localhost';
$user = 'egbim';
$pass = 'baron3840!!';
$db = 'egbim';
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['status'=>'fail','message'=>'DB 연결 실패']);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// === 파라미터 ===
$product_code = $_GET['product_code'] ?? '';
$show_deleted = isset($_GET['show_deleted']) ? (int)$_GET['show_deleted'] : 0;
if (!$product_code) {
echo json_encode(['status'=>'fail','message'=>'제품코드 누락']);
exit;
}
// === 삭제 여부 조건 ===
$whereDeleted = $show_deleted ? "1=1" : "deleted=0";
// === 쿼리 ===
$sql = "
SELECT
id, product_code, sw_version, filename, filetype, filesize, filepath,
DATE_FORMAT(upload_date, '%Y-%m-%d %H:%i:%s') AS upload_date,
uploader,
tester,
DATE_FORMAT(test_done_date, '%Y-%m-%d %H:%i:%s') AS test_done_date,
releaser,
DATE_FORMAT(release_date, '%Y-%m-%d %H:%i:%s') AS release_date,
deleted
FROM deploy_files
WHERE product_code = ?
AND $whereDeleted
ORDER BY sw_version DESC, upload_date ASC
";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $product_code);
$stmt->execute();
$res = $stmt->get_result();
$files = [];
while ($row = $res->fetch_assoc()) {
$files[] = $row;
}
// ✅ 현재 로그인 관리자 정보 포함
echo json_encode([
'status' => 'ok',
'files' => $files,
'current_user' => [
'loginId' => $_SESSION['user']['loginIds'][0] ?? '',
'name' => get_upload_admin_display_name(
$_SESSION['user']['loginIds'][0] ?? '',
$_SESSION['user']['name'] ?? '알 수 없음'
)
]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$stmt->close();
$conn->close();
+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>
+793
View File
@@ -0,0 +1,793 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'].'/eng/skin/member/basic/descope_session.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/eng/bbs/upload_admin_guard.php';
// 로그인 안 되었거나 관리자 아니면 바로 로그인 화면으로
if (empty($_SESSION['user']['userId']) || !is_upload_admin()) {
header("Location: /eng/index.php?popup=login");
exit;
}
?>
<!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">한맥 가족사 S/W 업로드</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>
<option value="999" selected>TEST</option>
</select>
</div>
<div class="flex items-center space-x-4">
<div>
<span class="text-sm">내부 배포 버전(최신):</span>
<span id="latest-family" class="text-blue-600 font-semibold">0.0.0</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 class="flex items-center space-x-1">
<span class="text-sm">외부 배포 버전:</span>
<select id="external-version" class="p-1 border rounded">
<option value="">사용안함</option>
<!-- ✅ 서버에서 confirmed 버전 목록을 가져와 채움 -->
</select>
<button id="save-version" class="bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">저장</button>
</div>
</div>
</div>
<!-- 버튼 영역 -->
<div class="flex justify-between items-center mb-4">
<div class="flex items-center space-x-2">
<input type="checkbox" id="show-deleted" class="mr-1">
<label for="show-deleted" class="text-sm">삭제된 버전 표시</label>
</div>
<div class="flex space-x-2">
<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" id="btn-download">다운로드</button>
<button class="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600" id="btn-delete">파일삭제</button>
</div>
</div>
<!-- 테이블 -->
<div class="bg-white shadow rounded-lg overflow-x-auto max-h-[70vh] overflow-y-auto">
<table class="min-w-full text-sm text-left">
<thead class="bg-gray-200 sticky top-0 z-10">
<tr>
<th class="px-2 py-2 text-center w-[4%]"></th>
<th class="px-2 py-2 text-center w-[7%] whitespace-nowrap">버전</th>
<th class="px-2 py-2 text-center w-[20%] whitespace-nowrap">파일명</th>
<th class="px-2 py-2 text-center w-[8%] whitespace-nowrap">파일타입</th>
<th class="px-2 py-2 text-center w-[8%] whitespace-nowrap">크기</th>
<th class="px-2 py-2 text-center w-[12%] whitespace-nowrap">업로드일</th>
<th class="px-2 py-2 text-center w-[10%] whitespace-nowrap">업로드수행자</th>
<th class="px-2 py-2 text-center w-[12%] whitespace-nowrap">테스트완료</th>
<th class="px-2 py-2 text-center w-[10%] whitespace-nowrap">테스트수행자</th>
<th class="px-2 py-2 text-center w-[12%] whitespace-nowrap">릴리즈일</th>
<th class="px-2 py-2 text-center w-[10%] whitespace-nowrap">릴리즈수행자</th>
</tr>
</thead>
<!-- ✅ 제품코드별 tbody -->
<tbody id="file-list-1" class="file-list hidden"></tbody>
<tbody id="file-list-2" class="file-list hidden"></tbody>
<tbody id="file-list-3" class="file-list hidden"></tbody>
<tbody id="file-list-999" class="file-list"></tbody> <!-- TEST 기본 표시 -->
</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">신규 버전 업로드</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" max="99" required>
<span>.</span>
<input type="number" class="border w-14 p-1 mx-1 text-center" id="sw2" min="0" max="99" required>
<span>.</span>
<input type="number" class="border w-14 p-1 mx-1 text-center" id="sw3" min="0" max="999" required>
<span id="version-type-text" class="ml-3 font-bold text-gray-600">Major</span>
</div>
<!-- 파일 업로드 항목 -->
<div class="flex items-center mb-2">
<label class="w-36">설치파일(EXE):</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-36">패치파일(EXE):</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-36">릴리즈노트(MD):</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 class="flex items-center mb-2">
<label class="w-36">정보파일(ZIP):</label>
<input type="file" class="hidden" id="info-file">
<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>
<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', ()=>$('#upload-modal').removeClass('hidden'));
$('#upload-cancel').on('click', ()=>$('#upload-modal').addClass('hidden'));
$('#upload-modal').on('click', function(e){ if(e.target===this) $(this).addClass('hidden'); });
// 파일 버튼
$('#setup-add').on('click', ()=>$('#setup-file').click());
$('#patch-add').on('click', ()=>$('#patch-file').click());
$('#info-add').on('click', ()=>$('#info-file').click());
$('#release-add').on('click', ()=>$('#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 || ''); });
// ✅ 공통: 현재 로그인 사용자 이름 가져오기
function getCurrentUserName() {
return window.currentUserName || "알 수 없음";
}
// 업로드 실행
$('#upload-submit').on('click', async function() {
const productCode = $('#product-select').val(); // eg-bim
const swVersion = [$('#sw1').val(), $('#sw2').val(), $('#sw3').val()].join('.');
const versionType = $('#version-type-text').text().toLowerCase();
const sw1 = $('#sw1').val();
const sw2 = $('#sw2').val();
const sw3 = $('#sw3').val();
// ✅ S/W 버전 필수 체크
if (!sw1 || !sw2 || !sw3) {
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];
// ✅ 설치파일 필수 체크
if (!setupFile) {
alert("설치파일(EXE)은 반드시 업로드해야 합니다.");
return;
}
// ✅ 서버에 기존 버전 확인
try {
let checkRes = await $.post("/eng/sw_upload/upload_check_version.php", {
product_code: productCode,
sw_version: swVersion
});
if (checkRes.exists) {
const proceed = confirm(`기존에 업로드된 [${swVersion}] 버전이 존재합니다.\n파일을 덮어쓰겠습니까?`);
if (!proceed) return; // ❌ 취소하면 업로드 중단
}
} catch (err) {
console.error("버전 확인 오류:", err);
alert("버전 확인 중 오류가 발생했습니다.");
return;
}
// 파일 규칙 이름 매핑
const rename = (type, ext) => {
const prod = productCode; // 이미 소문자 값 들어옴
const ver = swVersion.toLowerCase();
switch (type) {
case "setup": return `setup_${prod}_${ver}${ext}`;
case "patch": return `patch_${prod}_${ver}${ext}`;
case "info": return `updateinfo_${prod}_${ver}${ext}`;
case "md": return `releasenote_${prod}_${ver}${ext}`;
}
};
const files = [];
if (setupFile) files.push({ orig: setupFile, type:"setup", ext:".exe" });
if (patchFile) files.push({ orig: patchFile, type:"patch", ext:".exe" });
if (infoFile) files.push({ orig: infoFile, type:"info", ext:"." + infoFile.name.split(".").pop() });
if (releaseFile) files.push({ orig: releaseFile, type:"md", ext:".md" });
try {
// 1) Presigned 업로드
for (let f of files) {
const renamed = rename(f.type, f.ext);
let formData = new FormData();
formData.append("product_code", productCode);
formData.append("sw_version", swVersion);
formData.append("filename", renamed);
formData.append("content_type", f.orig.type || "application/octet-stream");
// presign 요청
let presignRes = await fetch("/eng/sw_upload/upload_presign.php", { method:"POST", body:formData });
let presignData = await presignRes.json();
// 파일 업로드
let putRes = await fetch(presignData.url, {
method:"PUT",
headers:{ "Content-Type": f.orig.type || "application/octet-stream" },
body:f.orig
});
if (!putRes.ok) throw new Error(renamed+" 업로드 실패");
// DB용 정보 세팅
f.path = presignData.key;
f.name = renamed;
f.size = f.orig.size;
f.filetype = f.type;
}
const userName = sessionStorage.getItem("userName") || "알 수 없음";
const loginId = sessionStorage.getItem("loginId") || "";
// 2) DB Insert
$.post("/eng/sw_upload/upload_popup.php", {
product_code: productCode,
sw_version: swVersion,
version_type: versionType,
uploader: userName,
files: JSON.stringify(files.map(f=>({
name: f.name, // 규칙 파일명
original: f.orig.name, // 원본 파일명
path: f.path,
type: f.filetype,
size: f.size
})))
}, function(res){
if(res.group){
loadFileList(productCode);
alert("업로드 및 DB 저장 성공!");
}
}, "json");
$('#upload-modal').addClass('hidden');
// $('.text-blue-600').text(swVersion);
} catch(err){
alert("업로드 중 오류: "+err.message);
}
});
$(document).ready(function(){
const productCode = $('#product-select').val();
loadFileList(productCode);
loadVersion(productCode);
// 제품 선택이 바뀌면 다시 조회
$('#product-select').on('change', function(){
const newCode = $(this).val();
loadFileList(newCode);
loadVersion(newCode);
});
});
function loadFileList(productCode){
const showDeleted = $("#show-deleted").is(":checked") ? 1 : 0;
$.ajax({
url: "/eng/sw_upload/upload_list.php",
type: "GET",
data: { product_code: productCode, show_deleted: showDeleted },
dataType: "json",
success: function(res){
const $tbody = $("#file-list-" + productCode);
$tbody.empty();
if(res.status !== "ok"){
$tbody.append(`<tr><td colspan="11" class="text-center py-4 text-red-500">조회 실패: ${res.message}</td></tr>`);
return;
}
if(res.files && res.files.length > 0){
renderFilesFromDB(res.files, $tbody);
} else {
$tbody.append(`<tr><td colspan="11" class="text-center py-4 text-gray-500">데이터가 없습니다.</td></tr>`);
}
},
error: function(xhr){
alert("리스트 조회 중 오류가 발생했습니다. (code: "+xhr.status+")");
}
});
}
// 체크박스 토글 이벤트
$(document).on("change", "#show-deleted", function(){
const productCode = $("#product-select").val();
loadFileList(productCode);
});
// 리스트 렌더링
function renderFilesFromDB(files, $tbody){
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)=>{
$tbody.append(`
<tr class="border-b">
${idx===0 ? `
<td class="px-4 py-2 text-center ${f.deleted==1?'bg-red-100':''}" rowspan="${rowspan}">
${f.deleted==1 ? '' : `<input type="checkbox" value="${f.product_code}|${f.sw_version}"/>`}
</td>
<td class="px-4 py-2 font-bold text-blue-600 text-center" rowspan="${rowspan}">
${f.sw_version}
</td>
` : ''}
<td class="px-4 py-2 ${f.deleted==1?'bg-red-100':''}">${f.filename}</td>
<td class="px-4 py-2 text-center ${f.deleted==1?'bg-red-100':''}">${f.filetype}</td>
<td class="px-4 py-2 text-center ${f.deleted==1?'bg-red-100':''}">
${f.filetype==="md"
? (Math.round(f.filesize/1024*100)/100)+"KB"
: (Math.round(f.filesize/1024/1024*100)/100)+"MB"}
</td>
${idx===0?`
<td class="px-4 py-2 text-center" rowspan="${rowspan}">${f.upload_date}</td>
<td class="px-4 py-2 text-center" rowspan="${rowspan}">${f.uploader}</td>
<td class="px-4 py-2 text-center" rowspan="${rowspan}">
${f.deleted==1
? '<span class="text-red-500">삭제됨</span>'
: (f.test_done_date
? f.test_done_date
: `<button class="bg-green-500 text-white px-2 py-1 rounded btn-testdone"
data-product="${f.product_code}"
data-version="${f.sw_version}">완료확정</button>`)}
</td>
<td class="px-4 py-2 text-center" rowspan="${rowspan}">${f.tester || ''}</td>
<td class="px-4 py-2 text-center" rowspan="${rowspan}">
${f.deleted==1
? '<span class="text-red-500">삭제됨</span>'
: (f.release_date
? f.release_date
: `<button class="bg-blue-500 text-white px-2 py-1 rounded btn-release"
data-product="${f.product_code}"
data-version="${f.sw_version}">배포확정</button>`)}
</td>
<td class="px-4 py-2 text-center" rowspan="${rowspan}">${f.releaser || ''}</td>
`:''}
</tr>
`);
});
});
}
// ✅ 제품 선택 이벤트
$('#product-select').on('change', function(){
const newCode = $(this).val();
// 모든 리스트 숨기고 선택된 것만 표시
$(".file-list").addClass("hidden");
$("#file-list-" + newCode).removeClass("hidden");
// 데이터 로드
loadFileList(newCode);
loadVersion(newCode);
});
// ✅ 초기 로드 시 TEST 불러오기
$(document).ready(function(){
const productCode = $('#product-select').val();
loadFileList(productCode);
loadVersion(productCode);
});
// 버전 입력 시 타입 자동 결정
function updateVersionType() {
const major = parseInt($('#sw1').val() || 0, 10);
const minor = parseInt($('#sw2').val() || 0, 10);
const patch = parseInt($('#sw3').val() || 0, 10);
let typeText = 'MAJOR'; // 기본값
if (patch > 0) {
typeText = 'PATCH'; // 최우선
} else if (minor > 0) {
typeText = 'MINOR';
} else if (major > 0) {
typeText = 'MAJOR';
}
$('#version-type-text').text(typeText);
// ✅ PATCH 영역 입력값에 따라 패치파일 업로드창 활성화/비활성화
if (patch > 0) {
$('#patch-file').prop('disabled', false);
$('#patch-filename').prop('disabled', false);
$('#patch-add').prop('disabled', false).removeClass('bg-gray-400').addClass('bg-blue-500');
} else {
$('#patch-file').prop('disabled', true).val('');
$('#patch-filename').prop('disabled', true).val('');
$('#patch-add').prop('disabled', true).removeClass('bg-blue-500').addClass('bg-gray-400');
}
}
// 이벤트 바인딩
$('#sw1, #sw2, #sw3').on('input', updateVersionType);
// 초기 표시
updateVersionType();
// ✅ 테스트 완료 버튼 클릭
$(document).on('click', '.btn-testdone', function(){
const productCode = $(this).data('product');
const swVersion = $(this).data('version');
const userName = sessionStorage.getItem("userName") || "알 수 없음";
const loginId = sessionStorage.getItem("loginId") || "";
$.post("/eng/sw_upload/upload_testdone.php", {
product_code: productCode,
sw_version: swVersion,
tester: userName
}, function(res){
if(res.test_done_date){
// 같은 버전 전체 갱신
$(`#file-list tr`).each(function(){
const version = $(this).find("td:nth-child(2)").text().trim(); // 5번째 열 = 버전
if(version === swVersion){
$(this).find("td").eq(7).text(res.test_done_date); // 테스트완료 컬럼
$(this).find("td").eq(8).text(res.tester); // 테스트자 컬럼
}
});
alert("테스트 완료되었습니다."); // ✅ 추가
loadFileList(productCode); // ✅ 리스트 전체 새로고침
}
}, "json");
});
// ✅ 배포 확정 버튼 클릭
$(document).on("click", ".btn-release", function(){
const productCode = $(this).data("product");
const swVersion = $(this).data("version");
const userName = sessionStorage.getItem("userName") || "알 수 없음";
const loginId = sessionStorage.getItem("loginId") || "";
if(!confirm(`[버전 ${swVersion}] 릴리즈 서버로 이동하시겠습니까?`)) return;
$.post("/eng/sw_upload/upload_release.php", {
product_code: productCode,
sw_version: swVersion,
releaser: userName
}, function(res){
if(res.release_date){
// 같은 버전 전체 갱신
$(`#file-list tr`).each(function(){
const version = $(this).find("td:nth-child(2)").text().trim(); // ✅ 2번째 열 = 버전
if(version === swVersion){
$(this).find("td").eq(9).text(res.release_date); // ✅ 10번째 열 = 릴리즈일
$(this).find("td").eq(10).text(res.releaser); // ✅ 11번째 열 = 릴리즈자
}
});
alert("배포가 확정되었습니다."); // ✅ 알림
loadFileList(productCode); // ✅ 리스트 전체 새로고침
} else {
alert("실패: " + (res.message || "배포 확정 처리 중 오류 발생"));
}
}, "json");
});
// ✅ 다운로드 버튼 클릭
$(document).on("click", "#btn-download", function () {
const checked = $("input[type=checkbox]:checked")
.map(function () {
return $(this).val();
})
.get();
if (checked.length === 0) {
alert("다운로드할 버전을 선택하세요.");
return;
}
checked.forEach(val => {
const [product, version] = val.split("|");
// ✅ 요청 URL 생성
const url = "/eng/sw_upload/upload_download.php?product_code=" + encodeURIComponent(product) +
"&sw_version=" + encodeURIComponent(version);
console.log("다운로드 요청:", url); // 디버깅 로그
// ✅ a 태그를 만들어 강제 클릭 (download 속성 제거)
const link = document.createElement("a");
link.href = url;
link.target = "_blank"; // 새 탭 열기 → 다운로드 트리거 보장
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
});
// ✅ 삭제 버튼 클릭
$(document).on("click", "#btn-delete", function () {
const checked = $("input[type=checkbox]:checked")
.map(function () { return $(this).val(); })
.get();
if (checked.length === 0) {
alert("삭제할 버전을 선택하세요.");
return;
}
if (!confirm("선택한 버전 전체 파일을 삭제하시겠습니까?")) return;
checked.forEach(item => {
const [product, version] = item.split("|");
$.post("/eng/sw_upload/upload_delete.php", {
product_code: product,
sw_version: version
}, function(res) {
if (res.status === "ok") {
alert(`[${version}] 삭제 완료!`);
loadFileList(product);
} else {
if (res.failed && res.failed.length > 0) {
let errors = res.failed.map(f => `${f.file}: ${f.error}`).join("\n");
alert("삭제 실패:\n" + errors);
} else {
alert("삭제 실패: " + (res.message || "알 수 없는 오류"));
}
}
}, "json");
});
});
// 외부 배포 버전 저장/변경 버튼
// $(document).on("click", "#save-version", function(){
// const productCode = $("#product-select").val();
// const version = [
// $("#ver1").val() || 0,
// $("#ver2").val() || 0,
// $("#ver3").val() || 0
// ].join(".");
// $.post("/eng/sw_upload/upload_version_update.php", {
// product_code: productCode,
// version: version
// }, function(res){
// if(res.status === "ok"){
// alert("외부 배포 버전 저장/변경 완료!");
// loadVersion(productCode); // 저장 후 새 값 반영
// } else {
// alert("실패: " + res.message);
// }
// }, "json");
// });
$(document).on("click", "#save-version", function(){
const productCode = $("#product-select").val();
const version = $("#external-version").val(); // ✅ 콤보박스 값
// ✅ confirm 추가
const msg = version
? `외부 배포를 ${version} 버전으로 변경하시겠습니까?`
: `외부 배포를 '사용안함'으로 변경하시겠습니까?`;
if(!confirm(msg)) return; // ❌ 취소 시 중단
$.post("/eng/sw_upload/upload_version_update.php", {
product_code: productCode,
version: version
}, function(res){
if(res.status === "ok"){
// alert("외부 배포 버전 저장/변경 완료!");
loadVersion(productCode); // 저장 후 새 값 반영
} else {
alert("실패: " + res.message);
}
}, "json");
});
// 페이지 로드 시 현재 버전 불러오기
// function loadVersion(productCode){
// $.getJSON("/eng/sw_upload/upload_version_get.php", { product_code: productCode }, function(res){
// if(res.status === "ok"){
// // 내부 최신 버전 표시
// $("#latest-family").text(res.family_version);
// // 외부 버전 입력칸 채우기
// const parts = res.external_version.split(".");
// $("#ver1").val(parts[0] || 0);
// $("#ver2").val(parts[1] || 0);
// $("#ver3").val(parts[2] || 0);
// // 버튼 상태 (저장 vs 변경)
// if(res.external_exists){
// $("#save-version")
// .text("변경")
// .data("mode","update")
// .removeClass("bg-blue-500 hover:bg-blue-600")
// .addClass("bg-yellow-500 hover:bg-yellow-600");
// } else {
// $("#save-version")
// .text("저장")
// .data("mode","save")
// .removeClass("bg-yellow-500 hover:bg-yellow-600")
// .addClass("bg-blue-500 hover:bg-blue-600");
// }
// }
// });
// }
function loadVersion(productCode){
$.getJSON("/eng/sw_upload/upload_version_get.php", { product_code: productCode }, function(res){
if(res.status === "ok"){
// 내부 최신 버전 표시
$("#latest-family").text(res.family_version);
// 외부 버전 콤보박스 채우기
const $sel = $("#external-version");
$sel.empty();
$sel.append(`<option value="">사용안함</option>`);
if(res.confirmed_versions && res.confirmed_versions.length > 0){
res.confirmed_versions.forEach(v=>{
$sel.append(`<option value="${v}">${v}</option>`);
});
}
// 현재 저장된 외부 버전 선택
if(res.external_version){
$sel.val(res.external_version);
} else {
$sel.val("");
}
// 버튼 상태 (저장 vs 변경)
if(res.external_exists){
$("#save-version")
.text("변경")
.data("mode","update")
.removeClass("bg-blue-500 hover:bg-blue-600")
.addClass("bg-yellow-500 hover:bg-yellow-600");
} else {
$("#save-version")
.text("저장")
.data("mode","save")
.removeClass("bg-yellow-500 hover:bg-yellow-600")
.addClass("bg-blue-500 hover:bg-blue-600");
}
}
});
}
// 숫자 입력 자동 보정 (0~max 범위)
function bindRangeCorrection(selector) {
$(selector).on("input", function() {
let val = parseInt($(this).val(), 10);
let min = parseInt($(this).attr("min"), 10) || 0;
let max = parseInt($(this).attr("max"), 10);
if (isNaN(val)) {
$(this).val(""); // 입력 없으면 비워둠
} else if (val < min) {
$(this).val(min);
} else if (val > max) {
$(this).val(max);
}
});
}
// 적용 (팝업 + 상단 외부 버전)
bindRangeCorrection("#sw1, #sw2, #sw3");
$('#btn-new').on('click', ()=>{
// 🔄 파일 input 초기화
$('#setup-file, #patch-file, #release-file, #info-file').val('');
$('#setup-filename, #patch-filename, #release-filename, #info-filename').val('');
// 내부 최신 버전 가져오기
const latestVer = $('#latest-family').text().trim(); // "1.2.3"
if (latestVer) {
const parts = latestVer.split('.');
$('#sw1').val(parts[0] || 0);
$('#sw2').val(parts[1] || 0);
$('#sw3').val(parts[2] || 0);
}
// patch 여부 확인 (최신 버전의 세 번째 자리)
const patch = parseInt($('#sw3').val() || 0, 10);
if (patch > 0) {
$('#patch-file').prop('disabled', false);
$('#patch-filename').prop('disabled', false);
$('#patch-add').prop('disabled', false)
.removeClass('bg-gray-400').addClass('bg-blue-500');
} else {
$('#patch-file').prop('disabled', true).val('');
$('#patch-filename').prop('disabled', true).val('');
$('#patch-add').prop('disabled', true)
.removeClass('bg-blue-500').addClass('bg-gray-400');
}
updateVersionType(); // 버전 타입 갱신 (Major/Minor/Patch)
$('#upload-modal').removeClass('hidden');
});
// 팝업 열기
$('#btn-new').on('click', ()=>{
// 🔄 파일 input만 초기화
$('#setup-file, #patch-file, #release-file, #info-file').val('');
// 파일명 표시란 초기화
$('#setup-filename, #patch-filename, #release-filename, #info-filename').val('');
// 패치파일 버튼 상태 리셋 (단, PATCH 값이 있으면 활성화 유지)
const patch = parseInt($('#sw3').val() || 0, 10);
if (patch > 0) {
$('#patch-file').prop('disabled', false);
$('#patch-filename').prop('disabled', false);
$('#patch-add').prop('disabled', false)
.removeClass('bg-gray-400').addClass('bg-blue-500');
} else {
$('#patch-file').prop('disabled', true).val('');
$('#patch-filename').prop('disabled', true).val('');
$('#patch-add').prop('disabled', true)
.removeClass('bg-blue-500').addClass('bg-gray-400');
}
$('#upload-modal').removeClass('hidden');
});
</script>
</body>
</html>
+274
View File
@@ -0,0 +1,274 @@
<?php
// header("Content-Type: application/json; charset=utf-8");
// // DB 연결
// $conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
// if ($conn->connect_error) {
// http_response_code(500);
// echo json_encode(['message' => 'DB 연결 실패']);
// exit;
// }
// mysqli_set_charset($conn, 'utf8mb4');
// // === 파라미터 받기 ===
// $product_code = $_POST['product_code'] ?? '';
// $sw_version = $_POST['sw_version'] ?? '';
// $version_type = $_POST['version_type'] ?? 'patch'; // major/minor/patch
// $uploader = $_POST['uploader'] ?? '관리자';
// $files = isset($_POST['files']) ? json_decode($_POST['files'], true) : [];
// if (!$product_code || !$sw_version || empty($files)) {
// http_response_code(400);
// echo json_encode(['message' => '필수값 누락']);
// exit;
// }
// $inserted = [];
// foreach ($files as $f) {
// $filename = $f['name'] ?? ''; // 규칙 적용된 이름
// $original_name = $f['original'] ?? $filename; // 원본 파일명 (없으면 동일)
// $filepath = $f['path'] ?? '';
// $filetype = $f['type'] ?? '';
// $filesize = $f['size'] ?? 0;
// $sql = "INSERT INTO deploy_files
// (product_code, sw_version, version_type,
// filename, original_name, filepath, filetype,
// filesize, uploader, upload_date, deleted)
// VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)";
// $stmt = $conn->prepare($sql);
// if (!$stmt) {
// http_response_code(500);
// echo json_encode(['message' => '쿼리 준비 실패: '.$conn->error]);
// exit;
// }
// // 바인딩
// $stmt->bind_param(
// "sssssssis",
// $product_code,
// $sw_version,
// $version_type,
// $filename,
// $original_name,
// $filepath,
// $filetype,
// $filesize,
// $uploader
// );
// if ($stmt->execute()) {
// $inserted[] = [
// 'id' => $stmt->insert_id,
// 'filename' => $filename,
// 'original' => $original_name,
// 'filetype' => $filetype,
// 'version' => $sw_version,
// 'size' => round($filesize/1024/1024, 2)."MB"
// ];
// } else {
// http_response_code(500);
// echo json_encode(['message' => 'DB insert 실패: '.$stmt->error]);
// exit;
// }
// $stmt->close();
// }
// /* ✅ 버전 기록: deploy_versions_test 업데이트 */
// $check = $conn->prepare("SELECT COUNT(*) FROM deploy_versions_test WHERE product_code=?");
// if (!$check) {
// error_log("쿼리 준비 실패: ".$conn->error);
// } else {
// $check->bind_param("s", $product_code);
// $check->execute();
// $check->bind_result($cnt);
// $check->fetch();
// $check->close();
// if ($cnt > 0) {
// $upd = $conn->prepare("
// UPDATE deploy_versions_test
// SET family_version=?, external_version=?, updated_at=NOW()
// WHERE product_code=?
// ");
// if (!$upd) {
// error_log("UPDATE 준비 실패: ".$conn->error);
// } else {
// $upd->bind_param("sss", $sw_version, $sw_version, $product_code);
// if (!$upd->execute()) {
// error_log("UPDATE 실행 실패: ".$upd->error);
// }
// $upd->close();
// }
// } else {
// $ins = $conn->prepare("
// INSERT INTO deploy_versions_test (product_code, family_version, external_version, updated_at)
// VALUES (?, ?, ?, NOW())
// ");
// if (!$ins) {
// error_log("INSERT 준비 실패: ".$conn->error);
// } else {
// $ins->bind_param("sss", $product_code, $sw_version, $sw_version);
// if (!$ins->execute()) {
// error_log("INSERT 실행 실패: ".$ins->error);
// }
// $ins->close();
// }
// }
// }
// echo json_encode([
// 'message' => '업로드 DB 기록 완료',
// 'group' => [
// 'files' => $inserted,
// 'upload' => date('Y-m-d H:i:s'),
// 'uploader' => $uploader,
// 'testdone' => '<button class="bg-green-500 text-white px-2 py-1 rounded">확인</button>',
// 'tester' => '',
// 'release' => '<button class="bg-blue-500 text-white px-2 py-1 rounded">확인</button>',
// 'releaser' => ''
// ]
// ]);
// $conn->close();
?>
<?php
header("Content-Type: application/json; charset=utf-8");
// DB 연결
$conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['message' => 'DB 연결 실패']);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// === 파라미터 받기 ===
$product_code = $_POST['product_code'] ?? '';
$sw_version = $_POST['sw_version'] ?? '';
$version_type = $_POST['version_type'] ?? 'patch'; // major/minor/patch
$uploader = $_POST['uploader'] ?? '알 수 없음'; // JS에서 세션스토리지 값
$files = isset($_POST['files']) ? json_decode($_POST['files'], true) : [];
if (!$product_code || !$sw_version || empty($files)) {
http_response_code(400);
echo json_encode(['message' => '필수값 누락']);
exit;
}
$inserted = [];
foreach ($files as $f) {
$filename = $f['name'] ?? '';
$original_name = $f['original'] ?? $filename;
$filepath = $f['path'] ?? '';
$filetype = $f['type'] ?? '';
$filesize = $f['size'] ?? 0;
$sql = "
INSERT INTO deploy_files
(product_code, sw_version, version_type,
filename, original_name, filepath, filetype,
filesize, uploader, upload_date, deleted)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), 0)
ON DUPLICATE KEY UPDATE
filename = VALUES(filename),
original_name = VALUES(original_name),
filepath = VALUES(filepath),
filesize = VALUES(filesize),
uploader = VALUES(uploader),
upload_date = NOW(),
deleted = 0
";
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['message' => '쿼리 준비 실패: '.$conn->error]);
exit;
}
$stmt->bind_param(
"sssssssis",
$product_code,
$sw_version,
$version_type,
$filename,
$original_name,
$filepath,
$filetype,
$filesize,
$uploader
);
if ($stmt->execute()) {
$inserted[] = [
'filename' => $filename,
'original' => $original_name,
'filetype' => $filetype,
'version' => $sw_version,
'size' => round($filesize/1024/1024, 2)."MB"
];
} else {
http_response_code(500);
echo json_encode(['message' => 'DB insert/update 실패: '.$stmt->error]);
exit;
}
$stmt->close();
}
/* ✅ 버전 기록: deploy_versions_test 업데이트 */
$check = $conn->prepare("SELECT COUNT(*) FROM deploy_versions_test WHERE product_code=?");
if ($check) {
$check->bind_param("s", $product_code);
$check->execute();
$check->bind_result($cnt);
$check->fetch();
$check->close();
if ($cnt > 0) {
// ✅ 기존 레코드 있으면 family_version만 갱신
$upd = $conn->prepare("
UPDATE deploy_versions_test
SET family_version=?, updated_at=NOW()
WHERE product_code=?
");
if ($upd) {
$upd->bind_param("ss", $sw_version, $product_code);
$upd->execute();
$upd->close();
}
} else {
// ✅ 신규 insert 시 external_version은 NULL로 둠
$ins = $conn->prepare("
INSERT INTO deploy_versions_test (product_code, family_version, updated_at)
VALUES (?, ?, NOW())
");
if ($ins) {
$ins->bind_param("ss", $product_code, $sw_version);
$ins->execute();
$ins->close();
}
}
}
echo json_encode([
'message' => '업로드 DB 기록 완료 (insert or update)',
'group' => [
'files' => $inserted,
'upload' => date('Y-m-d H:i:s'),
'uploader' => $uploader,
'testdone' => '<button class="bg-green-500 text-white px-2 py-1 rounded">확인</button>',
'tester' => '',
'release' => '<button class="bg-blue-500 text-white px-2 py-1 rounded">확인</button>',
'releaser' => ''
]
]);
$conn->close();
?>
+75
View File
@@ -0,0 +1,75 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
header('Content-Type: application/json; charset=utf-8');
require __DIR__ . '/../vendor/autoload.php';
use Aws\S3\S3Client;
use Aws\Credentials\Credentials;
// === 버킷/키 설정 ===
$bucket_name = "baron-software-test";
$account_id = "81fa2d48964d31dd0da9558f9ce601d1";
$access_key = "9ade5915f7abd402db60a87f3b3b6d76";
$secret_key = "a1a1cfd59f739977f98fb73f214894e39dbac9a39b1f6208020aa2e1303ea54c";
// === 프론트에서 넘어온 값 ===
$product_code = $_POST['product_code'] ?? '';
$sw_version = $_POST['sw_version'] ?? '';
$filename = $_POST['filename'] ?? '';
$content_type = $_POST['content_type'] ?? 'application/octet-stream';
if (!$product_code || !$sw_version || !$filename) {
http_response_code(400);
echo json_encode(['error' => '필수값 누락 (product_code, sw_version, filename)']);
exit;
}
// === 제품 코드 → 실제 폴더명 매핑 ===
$map = [
"1" => "eg-bim",
"2" => "tova",
"3" => "gaia",
"999" => "test"
];
$folder = $map[$product_code] ?? $product_code; // 혹시 매핑 없으면 그대로 사용
// === 업로드 경로 ===
// ex) eg-bim/1.2.3/setup_eg-bim_1.2.3.exe
$key = "{$folder}/{$sw_version}/{$filename}";
try {
$credentials = new Credentials($access_key, $secret_key);
$s3 = new S3Client([
'region' => 'auto',
'version' => 'latest',
'endpoint' => "https://{$account_id}.r2.cloudflarestorage.com",
'credentials' => $credentials,
]);
// Presign URL 생성
$cmd = $s3->getCommand('PutObject', [
'Bucket' => $bucket_name,
'Key' => $key,
'ContentType' => $content_type
]);
$request = $s3->createPresignedRequest($cmd, '+1 hour');
echo json_encode([
'url' => (string)$request->getUri(), // 브라우저에서 PUT 할 주소
'key' => $key // DB에 저장할 전체 경로
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'error' => 'Presign 실패',
'detail' => $e->getMessage()
]);
}
?>
+250
View File
@@ -0,0 +1,250 @@
<?php
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// error_reporting(E_ALL);
// header("Content-Type: application/json; charset=utf-8");
// require __DIR__ . '/../vendor/autoload.php'; // AWS SDK 로드
// use Aws\S3\S3Client;
// use Aws\Exception\AwsException;
// // === DB 연결 ===
// $conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
// if ($conn->connect_error) {
// http_response_code(500);
// echo json_encode(['message' => 'DB 연결 실패']);
// exit;
// }
// mysqli_set_charset($conn, 'utf8mb4');
// // === 파라미터 ===
// $file_id = $_POST['id'] ?? 0;
// $releaser = $_POST['releaser'] ?? '관리자';
// if (!$file_id) {
// http_response_code(400);
// echo json_encode(['message' => '필수값 누락']);
// exit;
// }
// // === 파일 정보 가져오기 ===
// $res = $conn->query("SELECT * FROM deploy_files WHERE id=".(int)$file_id);
// $file = $res->fetch_assoc();
// if (!$file) {
// http_response_code(404);
// echo json_encode(['message' => '파일 정보 없음']);
// exit;
// }
// $sourceBucket = 'baron-software-test';
// $targetBucket = 'baron-software-release';
// $objectKey = $file['filepath'];
// // === S3 클라이언트 생성 ===
// // Test 버킷 (Read/Delete)
// $testS3 = new S3Client([
// 'version' => 'latest',
// 'region' => 'auto',
// 'endpoint' => 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
// 'credentials' => [
// 'key' => '9ade5915f7abd402db60a87f3b3b6d76', // ✅ write key
// 'secret' => 'a1a1cfd59f739977f98fb73f214894e39dbac9a39b1f6208020aa2e1303ea54c',
// ]
// ]);
// // Release 버킷 (Write)
// $releaseS3 = new S3Client([
// 'version' => 'latest',
// 'region' => 'auto',
// 'endpoint' => 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
// 'credentials' => [
// 'key' => '4d0b8feaf0e12873a3d95bd3aa0b787c',
// 'secret' => 'dc72a027e5263590f0bc89fc0b5a1e6c846cc4f036642430681364c4a9056ca4',
// ]
// ]);
// try {
// // 1. 원본 객체 다운로드
// $object = $testS3->getObject([
// 'Bucket' => $sourceBucket,
// 'Key' => $objectKey,
// ]);
// // 2. Release 버킷에 업로드
// $releaseS3->putObject([
// 'Bucket' => $targetBucket,
// 'Key' => $objectKey,
// 'Body' => $object['Body'],
// ]);
// // 3. Test 버킷에서 원본 삭제
// $testS3->deleteObject([
// 'Bucket' => $sourceBucket,
// 'Key' => $objectKey,
// ]);
// // 4. DB 업데이트
// $stmt = $conn->prepare("UPDATE deploy_files SET release_date=NOW(), releaser=? WHERE id=?");
// $stmt->bind_param("si", $releaser, $file_id);
// $stmt->execute();
// echo json_encode([
// 'status' => 'ok',
// 'message' => '릴리즈 확정 완료',
// 'targetKey' => $objectKey,
// ]);
// } catch (AwsException $e) {
// http_response_code(500);
// echo json_encode([
// 'status' => 'fail',
// 'message' => '릴리즈 실패: '.$e->getAwsErrorMessage(),
// 'error' => $e->getMessage()
// ]);
// }
?>
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
header("Content-Type: application/json; charset=utf-8");
require __DIR__ . '/../vendor/autoload.php'; // AWS SDK 로드
use Aws\S3\S3Client;
use Aws\Exception\AwsException;
// === DB 연결 ===
$conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['message' => 'DB 연결 실패']);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// === 파라미터 ===
$product_code = $_POST['product_code'] ?? '';
$sw_version = $_POST['sw_version'] ?? '';
// 릴리즈 처리 → releaser를 세션에서 가져옴
$releaser = $_POST['releaser'] ?? '알 수 없음'; // JS에서 넘겨준 userName
$loginId = $_POST['loginId'] ?? ''; // JS에서 넘겨준 loginId
if (!$product_code || !$sw_version) {
http_response_code(400);
echo json_encode(['message' => '필수값 누락']);
exit;
}
// === 대상 파일 목록 조회 ===
$sql = "SELECT * FROM deploy_files WHERE product_code=? AND sw_version=? AND deleted=0";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $product_code, $sw_version);
$stmt->execute();
$res = $stmt->get_result();
$files = [];
while ($row = $res->fetch_assoc()) {
$files[] = $row;
}
$stmt->close();
if (empty($files)) {
http_response_code(404);
echo json_encode(['message' => '해당 버전에 대한 파일이 없습니다.']);
exit;
}
// ✅ 테스트 완료 여부 체크
if (empty($files[0]['test_done_date'])) {
echo json_encode(['status'=>'fail','message'=>'테스트 완료 후 릴리즈 가능합니다.']);
exit;
}
// === 버킷 설정 ===
$sourceBucket = 'baron-software-test';
$targetBucket = 'baron-software-release';
$s3 = new S3Client([
'version' => 'latest',
'region' => 'auto',
'endpoint' => 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
'credentials' => [
// ❗ source(read/write) + target(write) 권한이 모두 있는 키
'key' => '11d7027f505658acb3aeb40c3955a206',
'secret' => '65a78f6c582e0bdfccc2f431fc7b7be2ba11f999f0a8a87142011eac8b84761f',
]
]);
$success = [];
$failed = [];
foreach ($files as $file) {
$objectKey = $file['filepath'];
try {
// 1. CopyObject (테스트 → 릴리즈)
$s3->copyObject([
'Bucket' => $targetBucket,
'Key' => $objectKey,
'CopySource' => $sourceBucket . '/' . $objectKey,
]);
// 2. 원본 삭제 (테스트 버킷)
$s3->deleteObject([
'Bucket' => $sourceBucket,
'Key' => $objectKey,
]);
$success[] = $objectKey;
} catch (AwsException $e) {
$failed[] = $objectKey;
}
}
if (!empty($failed)) {
// ❌ 하나라도 실패 → 전체 실패 처리
echo json_encode([
'status' => 'fail',
'message' => '릴리즈 실패: 일부 파일 이동 오류',
'failed' => $failed
]);
exit;
}
// ✅ 여기까지 오면 전체 이동 성공
// 1) deploy_files 업데이트
$stmt = $conn->prepare("
UPDATE deploy_files
SET release_date = NOW(), releaser = ?
WHERE product_code=? AND sw_version=? AND deleted=0
");
$stmt->bind_param("sss", $releaser, $product_code, $sw_version);
$stmt->execute();
$stmt->close();
// 2) deploy_versions.family_version 갱신
$stmt2 = $conn->prepare("
INSERT INTO deploy_versions (product_code, family_version, updated_at)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE
family_version = VALUES(family_version),
updated_at = NOW()
");
$stmt2->bind_param("ss", $product_code, $sw_version);
$stmt2->execute();
$stmt2->close();
// 최종 응답
echo json_encode([
'status' => 'ok',
'message' => '릴리즈 확정 완료',
'moved' => $success,
'release_date' => date('Y-m-d H:i:s'),
'releaser' => $releaser
]);
+45
View File
@@ -0,0 +1,45 @@
<?php
header("Content-Type: application/json; charset=utf-8");
// DB 연결
$conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['message' => 'DB 연결 실패']);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// === 파라미터 받기 ===
$product_code = $_POST['product_code'] ?? '';
$sw_version = $_POST['sw_version'] ?? '';
$tester = $_POST['tester'] ?? '알 수 없음';
if (!$product_code || !$sw_version) {
http_response_code(400);
echo json_encode(['message' => '필수값 누락']);
exit;
}
// === 버전 단위 전체 업데이트 ===
$sql = "UPDATE deploy_files
SET test_done_date = NOW(), tester = ?
WHERE sw_version = ?
AND product_code = ?"; // 순서 변경
$stmt = $conn->prepare($sql);
$stmt->bind_param("sss", $tester, $sw_version, $product_code);
if ($stmt->execute()) {
echo json_encode([
'message' => '테스트 완료 업데이트 성공',
'test_done_date' => date('Y-m-d H:i:s'),
'tester' => $tester
]);
} else {
http_response_code(500);
echo json_encode(['message' => 'DB 업데이트 실패: '.$stmt->error]);
}
$stmt->close();
$conn->close();
?>
+79
View File
@@ -0,0 +1,79 @@
<?php
header("Content-Type: application/json; charset=utf-8");
try {
$pdo = new PDO(
"mysql:host=localhost;dbname=egbim;charset=utf8mb4",
"egbim", "baron3840!!",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$productCode = $_GET['product_code'] ?? '';
if (!$productCode) {
echo json_encode([
'status' => 'fail',
'message' => '제품코드 없음'
]);
exit;
}
// ✅ 내부 최신 버전 & 외부 버전 가져오기
$st = $pdo->prepare("
SELECT family_version, external_version
FROM deploy_versions
WHERE product_code=?
ORDER BY updated_at DESC
LIMIT 1
");
$st->execute([$productCode]);
$verRow = $st->fetch();
$familyVer = $verRow['family_version'] ?? '0.0.0';
$externalVer = $verRow['external_version'] ?? '';
// 👉 내부와 외부가 같으면 "사용안함" 으로 표시되도록 빈 값 처리
if ($externalVer === $familyVer) {
$externalVer = '';
}
// ✅ 배포 확정된 버전 목록 (콤보박스 옵션용)
$st = $pdo->prepare("
SELECT DISTINCT sw_version
FROM deploy_files
WHERE product_code=?
AND release_date IS NOT NULL
AND deleted=0
ORDER BY
CAST(SUBSTRING_INDEX(sw_version, '.', 1) AS UNSIGNED) DESC,
CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(sw_version, '.', 2), '.', -1) AS UNSIGNED) DESC,
CAST(SUBSTRING_INDEX(sw_version, '.', -1) AS UNSIGNED) DESC
");
$st->execute([$productCode]);
$allVersions = $st->fetchAll(PDO::FETCH_COLUMN);
// ✅ 여기서 필터링
$filtered = [];
foreach ($allVersions as $v) {
// 내부 배포버전보다 높은 버전 제외
if (version_compare($v, $familyVer, '>')) continue;
// 현재 외부배포버전이 지정되어 있으면 그보다 낮은 버전 제외
if ($externalVer !== '' && version_compare($v, $externalVer, '<')) continue;
$filtered[] = $v;
}
echo json_encode([
'status' => 'ok',
'family_version' => $familyVer,
'external_version' => $externalVer, // "" 이면 "사용안함"
'external_exists' => ($externalVer !== ''),
'confirmed_versions' => $filtered // ✅ 필터링된 버전만 내려줌
]);
} catch (Exception $e) {
echo json_encode([
'status' => 'fail',
'message' => $e->getMessage()
]);
}
+112
View File
@@ -0,0 +1,112 @@
<?php
// header("Content-Type: application/json; charset=utf-8");
// require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/skin/member/basic/descope_session.php';
// try {
// $pdo = new PDO(
// "mysql:host=localhost;dbname=egbim;charset=utf8mb4",
// "egbim", "baron3840!!",
// [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
// );
// $productCode = $_POST['product_code'] ?? '';
// $version = $_POST['version'] ?? '';
// $userName = $_SESSION['user']['name'] ?? '알수없음';
// if(!$productCode){
// echo json_encode(['status'=>'fail','message'=>'제품 코드 누락']);
// exit;
// }
// // ✅ 버전이 비어있으면 기본값 "0.0.0"
// $version = trim($version) !== '' ? $version : '0.0.0';
// // ✅ family_version = external_version 동일하게 저장
// $st = $pdo->prepare("
// INSERT INTO deploy_versions (product_code, family_version, external_version, external_updated_by, updated_at)
// VALUES (:pc, :ver, :ver, :user, NOW())
// ON DUPLICATE KEY UPDATE
// family_version = VALUES(family_version),
// external_version = VALUES(external_version),
// external_updated_by = VALUES(external_updated_by),
// updated_at = NOW()
// ");
// $st->execute([
// ':pc' => $productCode,
// ':ver' => $version,
// ':user' => $userName
// ]);
// echo json_encode([
// 'status' => 'ok',
// 'family_version' => $version,
// 'external_version' => $version,
// 'updated_by' => $userName
// ]);
// } catch(Exception $e){
// echo json_encode(['status'=>'fail','message'=>$e->getMessage()]);
// }
?>
<?php
header("Content-Type: application/json; charset=utf-8");
require_once $_SERVER['DOCUMENT_ROOT'].'/eng/skin/member/basic/descope_session.php';
try {
$pdo = new PDO(
"mysql:host=localhost;dbname=egbim;charset=utf8mb4",
"egbim", "baron3840!!",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$productCode = $_POST['product_code'] ?? '';
$version = trim($_POST['version'] ?? '');
$userName = $_SESSION['user']['name'] ?? '알수없음';
if (!$productCode) {
echo json_encode(['status'=>'fail','message'=>'제품 코드 누락']);
exit;
}
// 현재 family_version 가져오기 (내부 최신 버전)
$st = $pdo->prepare("SELECT family_version FROM deploy_versions WHERE product_code=? LIMIT 1");
$st->execute([$productCode]);
$currentFamily = $st->fetchColumn() ?: '0.0.0';
if ($version === '') {
// ✅ 사용안함 → external_version = 내부 배포 버전(family_version)과 동일
$externalVer = $currentFamily;
} else {
// ✅ 외부 버전 직접 지정 → external_version만 변경
$externalVer = $version;
}
// DB 업데이트 (family_version은 유지, external_version만 갱신)
$st = $pdo->prepare("
INSERT INTO deploy_versions (product_code, family_version, external_version, external_updated_by, updated_at)
VALUES (:pc, :fam, :ext, :user, NOW())
ON DUPLICATE KEY UPDATE
family_version = VALUES(family_version), -- 기존 값 유지 위해 다시 넣음
external_version = VALUES(external_version),
external_updated_by = VALUES(external_updated_by),
updated_at = NOW()
");
$st->execute([
':pc' => $productCode,
':fam' => $currentFamily, // 유지
':ext' => $externalVer,
':user' => $userName
]);
echo json_encode([
'status' => 'ok',
'family_version' => $currentFamily,
'external_version' => $externalVer,
'updated_by' => $userName
]);
} catch(Exception $e){
echo json_encode(['status'=>'fail','message'=>$e->getMessage()]);
}