최초 커밋

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();
?>
+29
View File
@@ -0,0 +1,29 @@
<?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',
'rmsgud1202@hanmaceng.co.kr',
'b25023@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)
? '삭제 완료'
: '삭제 실패: 이미 릴리즈된 파일이 있습니다.'
]);
+84
View File
@@ -0,0 +1,84 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Aws\S3\S3Client;
// 실서비스 시에는 에러 표시를 끄는 것이 좋습니다.
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 1. 파라미터 받기
$filepath = $_GET['path'] ?? '';
$filename = $_GET['name'] ?? '';
if (!$filepath || !$filename) {
exit("필수 파라미터(path, name)가 누락되었습니다.");
}
// 2. DB 연결
$conn = new mysqli("localhost", "egbim", "baron3840!!", "egbim");
if ($conn->connect_error) {
die("연결 실패: " . $conn->connect_error);
}
/**
* 3. DB 조회 (중요 수정 사항)
* 동일한 filepath가 여러 개일 경우:
* 1순위: release_date가 있는 것 우선 (DESC)
* 2순위: 가장 최근에 업로드된 것 우선 (upload_date DESC)
*/
$sql = "SELECT release_date FROM deploy_files
WHERE filepath = ?
ORDER BY (release_date IS NULL), release_date DESC, upload_date DESC
LIMIT 1";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $filepath);
$stmt->execute();
$stmt->bind_result($release_date);
$fetch_success = $stmt->fetch();
$stmt->close();
$conn->close();
// DB에 해당 파일 경로 자체가 없는 경우
if (!$fetch_success) {
exit("DB에서 파일 정보를 찾을 수 없습니다. 경로: " . htmlspecialchars($filepath));
}
/**
* 4. 버킷 결정 로직
* 데이터가 존재하고, NULL/공백/0000-00-00이 아닐 때만 release 버킷 사용
*/
$is_released = (!empty($release_date) && $release_date !== '0000-00-00');
$bucket = $is_released ? 'baron-software-release' : 'baron-software-test';
// 5. S3 Client 설정
$s3 = new S3Client([
'version' => 'latest',
'region' => 'auto',
'endpoint'=> 'https://81fa2d48964d31dd0da9558f9ce601d1.r2.cloudflarestorage.com',
'credentials' => [
'key' => '11d7027f505658acb3aeb40c3955a206',
'secret' => '65a78f6c582e0bdfccc2f431fc7b7be2ba11f999f0a8a87142011eac8b84761f',
],
'use_path_style_endpoint' => true
]);
try {
// 6. Presigned URL 생성 (10분 유효)
$cmd = $s3->getCommand('GetObject', [
'Bucket' => $bucket,
'Key' => $filepath,
'ResponseContentDisposition' => 'attachment; filename="' . rawurlencode($filename) . '"'
]);
$request = $s3->createPresignedRequest($cmd, '+10 minutes');
// 7. R2 직접 다운로드 주소로 리다이렉트
header("Location: " . (string)$request->getUri());
exit;
} catch (Exception $e) {
exit("다운로드 링크 생성 실패: " . $e->getMessage());
}
?>
+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'].'/egbim/skin/member/basic/descope_session.php';
// ✅ 2) 관리자 권한 체크
require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/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();
+633
View File
@@ -0,0 +1,633 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/skin/member/basic/descope_session.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/bbs/upload_admin_guard.php';
// 로그인 안 되었거나 관리자 아니면 바로 로그인 화면으로
if (empty($_SESSION['user']['userId']) || !is_upload_admin()) {
header("Location: /egbim/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="4">EG-BIM-MODELER</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>
<select id="external-version" class="p-1 border rounded">
<option value="">사용안함</option>
</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-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 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-4" class="file-list hidden"></tbody>
<tbody id="file-list-999" class="file-list"></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">신규 버전 업로드</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();
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();
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("/egbim/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) => {
// 제품 코드와 제품명 매핑 (DB/서버 폴더 구조와 일치해야 함)
const productMap = {
"1": "eg-bim",
"2": "tova",
"3": "gaia",
"4": "eg-bim-modeler", // 수정된 부분: EG-BIM-MODELER 파일명 규칙 매핑 추가
"999": "test"
};
const prodName = productMap[productCode] || productCode; // 매핑 없으면 코드 그대로 사용
const ver = swVersion.toLowerCase();
switch (type) {
case "setup": return `setup_${prodName}_${ver}${ext}`;
case "patch": return `patch_${prodName}_${ver}${ext}`;
case "info": return `updateinfo_${prodName}_${ver}${ext}`;
case "md": return `releasenote_${prodName}_${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 {
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");
let presignRes = await fetch("/egbim/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+" 업로드 실패");
f.path = presignData.key;
f.name = renamed;
f.size = f.orig.size;
f.filetype = f.type;
}
const userName = sessionStorage.getItem("userName") || "알 수 없음";
$.post("/egbim/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');
} 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: "/egbim/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)=>{
// ✅ 직접 다운로드 API 주소 생성 (filepath와 filename을 인자로 전달)
const downloadUrl = `/egbim/sw_upload/upload_download.php?path=${encodeURIComponent(f.filepath)}&name=${encodeURIComponent(f.filename)}`;
$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':''}">
<a href="${downloadUrl}" class="text-blue-600 hover:text-blue-800 hover:underline flex items-center" target="_blank">
<i class="fa-solid fa-file-arrow-down mr-2 opacity-70"></i>
${f.filename}
</a>
</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);
});
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);
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") || "알 수 없음";
$.post("/egbim/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();
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") || "알 수 없음";
// ✅ 1. 사용자에게 제시할 확인 문구 설정
const confirmText = "배포 완료 요청";
// ✅ 2. 입력창(prompt) 띄우기
const userInput = prompt(`[버전 ${swVersion}] 릴리즈 서버로 이동하시겠습니까?\n\n진행하시려면 하단에 "${confirmText}" 문구를 정확히 입력해주세요.`);
// ✅ 3. 검증 로직
if (userInput === null) return; // '취소'를 누른 경우
if (userInput.trim() !== confirmText) {
alert(`입력한 문구가 일치하지 않습니다.\n(입력: ${userInput})`);
return;
}
// ✅ 4. 문구가 일치할 경우에만 기존 서버 통신(AJAX) 실행
$.post("/egbim/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();
if(version === swVersion){
$(this).find("td").eq(9).text(res.release_date);
$(this).find("td").eq(10).text(res.releaser);
}
});
alert(`[버전 ${swVersion}] 배포가 성공적으로 확정되었습니다.`);
loadFileList(productCode);
} else {
alert("실패: " + (res.message || "배포 확정 처리 중 오류 발생"));
}
}, "json");
});
$(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("/egbim/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 = $("#external-version").val(); // "사용안함" 선택 시 "" 값이 전달됨
const msg = (version && version !== "")
? `외부 배포를 [ ${version} ] 버전으로 변경하시겠습니까?`
: `외부 배포를 '사용안함'으로 설정하시겠습니까?`;
if (!confirm(msg)) return;
$.post("/egbim/sw_upload/upload_version_update.php", {
product_code: productCode,
version: version // 백엔드에서 ""를 받으면 내부 버전과 동일하게 맞춰 "사용안함" 처리함
}, function(res) {
if (res.status === "ok") {
alert("배포 버전 설정이 완료되었습니다.");
loadVersion(productCode); // UI 새로고침
} else {
alert("설정 실패: " + res.message);
}
}, "json");
});
// ✅ 외부 배포 버전 정보 및 선택 목록 로드 함수 수정
function loadVersion(productCode) {
$.getJSON("/egbim/sw_upload/upload_version_get.php", { product_code: productCode }, function(res) {
if (res.status === "ok") {
// ✅ 이 코드가 있어야 "0.0.0" 대신 "1.2.0" 등의 실제 버전이 화면에 보입니다.
$("#latest-family").text(res.family_version);
const $sel = $("#external-version");
$sel.empty();
// 목록 그리기
if (res.confirmed_versions) {
res.confirmed_versions.forEach(v => {
const label = (v === "") ? "사용안함" : v;
$sel.append(`<option value="${v}">${label}</option>`);
});
}
$sel.val(res.external_version);
if (res.external_version !== "") {
$("#save-version").text("변경").removeClass("bg-blue-500").addClass("bg-yellow-500");
} else {
$("#save-version").text("저장").removeClass("bg-yellow-500").addClass("bg-blue-500");
}
}
});
}
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', ()=>{
$('#setup-file, #patch-file, #release-file, #info-file').val('');
$('#setup-filename, #patch-filename, #release-filename, #info-filename').val('');
const latestVer = $('#latest-family').text().trim();
if (latestVer) {
const parts = latestVer.split('.');
$('#sw1').val(parts[0] || 0);
$('#sw2').val(parts[1] || 0);
$('#sw3').val(parts[2] || 0);
}
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();
$('#upload-modal').removeClass('hidden');
});
</script>
</body>
</html>
+636
View File
@@ -0,0 +1,636 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/skin/member/basic/descope_session.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/egbim/bbs/upload_admin_guard.php';
// 로그인 안 되었거나 관리자 아니면 바로 로그인 화면으로
if (empty($_SESSION['user']['userId']) || !is_upload_admin()) {
header("Location: /egbim/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="4">EG-BIM-MODELER</option>
<option value="5">EG-BIM_VIEWER</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>
<select id="external-version" class="p-1 border rounded">
<option value="">사용안함</option>
</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-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 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-4" class="file-list hidden"></tbody>
<tbody id="file-list-5" class="file-list hidden"></tbody>
<tbody id="file-list-999" class="file-list"></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">신규 버전 업로드</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();
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();
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("/egbim/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) => {
// 제품 코드와 제품명 매핑 (DB/서버 폴더 구조와 일치해야 함)
const productMap = {
"1": "eg-bim",
"2": "tova",
"3": "gaia",
"4": "eg-bim-modeler", // 수정된 부분: EG-BIM-MODELER 파일명 규칙 매핑 추가
"5": "eg-bim-viewer",
"999": "test"
};
const prodName = productMap[productCode] || productCode; // 매핑 없으면 코드 그대로 사용
const ver = swVersion.toLowerCase();
switch (type) {
case "setup": return `setup_${prodName}_${ver}${ext}`;
case "patch": return `patch_${prodName}_${ver}${ext}`;
case "info": return `updateinfo_${prodName}_${ver}${ext}`;
case "md": return `releasenote_${prodName}_${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 {
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");
let presignRes = await fetch("/egbim/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+" 업로드 실패");
f.path = presignData.key;
f.name = renamed;
f.size = f.orig.size;
f.filetype = f.type;
}
const userName = sessionStorage.getItem("userName") || "알 수 없음";
$.post("/egbim/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');
} 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: "/egbim/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)=>{
// ✅ 직접 다운로드 API 주소 생성 (filepath와 filename을 인자로 전달)
const downloadUrl = `/egbim/sw_upload/upload_download.php?path=${encodeURIComponent(f.filepath)}&name=${encodeURIComponent(f.filename)}`;
$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':''}">
<a href="${downloadUrl}" class="text-blue-600 hover:text-blue-800 hover:underline flex items-center" target="_blank">
<i class="fa-solid fa-file-arrow-down mr-2 opacity-70"></i>
${f.filename}
</a>
</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);
});
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);
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") || "알 수 없음";
$.post("/egbim/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();
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") || "알 수 없음";
// ✅ 1. 사용자에게 제시할 확인 문구 설정
const confirmText = "배포 완료 요청";
// ✅ 2. 입력창(prompt) 띄우기
const userInput = prompt(`[버전 ${swVersion}] 릴리즈 서버로 이동하시겠습니까?\n\n진행하시려면 하단에 "${confirmText}" 문구를 정확히 입력해주세요.`);
// ✅ 3. 검증 로직
if (userInput === null) return; // '취소'를 누른 경우
if (userInput.trim() !== confirmText) {
alert(`입력한 문구가 일치하지 않습니다.\n(입력: ${userInput})`);
return;
}
// ✅ 4. 문구가 일치할 경우에만 기존 서버 통신(AJAX) 실행
$.post("/egbim/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();
if(version === swVersion){
$(this).find("td").eq(9).text(res.release_date);
$(this).find("td").eq(10).text(res.releaser);
}
});
alert(`[버전 ${swVersion}] 배포가 성공적으로 확정되었습니다.`);
loadFileList(productCode);
} else {
alert("실패: " + (res.message || "배포 확정 처리 중 오류 발생"));
}
}, "json");
});
$(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("/egbim/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 = $("#external-version").val(); // "사용안함" 선택 시 "" 값이 전달됨
const msg = (version && version !== "")
? `외부 배포를 [ ${version} ] 버전으로 변경하시겠습니까?`
: `외부 배포를 '사용안함'으로 설정하시겠습니까?`;
if (!confirm(msg)) return;
$.post("/egbim/sw_upload/upload_version_update.php", {
product_code: productCode,
version: version // 백엔드에서 ""를 받으면 내부 버전과 동일하게 맞춰 "사용안함" 처리함
}, function(res) {
if (res.status === "ok") {
alert("배포 버전 설정이 완료되었습니다.");
loadVersion(productCode); // UI 새로고침
} else {
alert("설정 실패: " + res.message);
}
}, "json");
});
// ✅ 외부 배포 버전 정보 및 선택 목록 로드 함수 수정
function loadVersion(productCode) {
$.getJSON("/egbim/sw_upload/upload_version_get.php", { product_code: productCode }, function(res) {
if (res.status === "ok") {
// ✅ 이 코드가 있어야 "0.0.0" 대신 "1.2.0" 등의 실제 버전이 화면에 보입니다.
$("#latest-family").text(res.family_version);
const $sel = $("#external-version");
$sel.empty();
// 목록 그리기
if (res.confirmed_versions) {
res.confirmed_versions.forEach(v => {
const label = (v === "") ? "사용안함" : v;
$sel.append(`<option value="${v}">${label}</option>`);
});
}
$sel.val(res.external_version);
if (res.external_version !== "") {
$("#save-version").text("변경").removeClass("bg-blue-500").addClass("bg-yellow-500");
} else {
$("#save-version").text("저장").removeClass("bg-yellow-500").addClass("bg-blue-500");
}
}
});
}
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', ()=>{
$('#setup-file, #patch-file, #release-file, #info-file').val('');
$('#setup-filename, #patch-filename, #release-filename, #info-filename').val('');
const latestVer = $('#latest-family').text().trim();
if (latestVer) {
const parts = latestVer.split('.');
$('#sw1').val(parts[0] || 0);
$('#sw2').val(parts[1] || 0);
$('#sw3').val(parts[2] || 0);
}
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();
$('#upload-modal').removeClass('hidden');
});
</script>
</body>
</html>
+142
View File
@@ -0,0 +1,142 @@
<?php
header("Content-Type: application/json; charset=utf-8");
// ✅ 핵심: 숨어있는 모든 DB 에러를 밖으로 던지도록 강력한 에러 리포팅 설정
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
// DB 연결
$conn = new mysqli('localhost', 'egbim', 'baron3840!!', 'egbim');
$conn->set_charset('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)) {
throw new Exception("필수값 누락 (제품코드, 버전, 파일 정보 중 하나가 없습니다)");
}
// ✅ 트랜잭션 시작 (중간에 에러나면 데이터베이스를 롤백해서 데이터가 꼬이지 않게 보호함)
$conn->begin_transaction();
$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);
// ✅ 용량이 매우 큰 파일(2GB 이상)의 경우 정수형(i) 바인딩 시 에러가 날 수 있어, 모두 문자열(s)로 바인딩하여 안전하게 MySQL에 넘깁니다.
$stmt->bind_param(
"sssssssss",
$product_code,
$sw_version,
$version_type,
$filename,
$original_name,
$filepath,
$filetype,
$filesize,
$uploader
);
$stmt->execute();
$inserted[] = [
'filename' => $filename,
'original' => $original_name,
'filetype' => $filetype,
'version' => $sw_version,
'size' => round($filesize/1024/1024, 2)."MB"
];
$stmt->close();
}
/* ✅ 버전 기록: deploy_versions_test 업데이트 */
$check = $conn->prepare("SELECT COUNT(*) FROM deploy_versions_test WHERE product_code=?");
$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=?, updated_at=NOW()
WHERE product_code=?
");
$upd->bind_param("ss", $sw_version, $product_code);
$upd->execute();
$upd->close();
} else {
// 신규 추가
$ins = $conn->prepare("
INSERT INTO deploy_versions_test (product_code, family_version, updated_at)
VALUES (?, ?, NOW())
");
$ins->bind_param("ss", $product_code, $sw_version);
$ins->execute();
$ins->close();
}
// ✅ 위 모든 쿼리가 단 하나의 에러 없이 정상 실행되었을 때만 커밋(진짜 저장)
$conn->commit();
// 프론트엔드로 성공 메시지 전달
echo json_encode([
'success' => true,
'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' => ''
]
]);
} catch (Exception $e) {
// ✅ 쿼리 중 하나라도 에러가 나면 여기로 빠져나와서 롤백 처리
if (isset($conn) && $conn->ping()) {
$conn->rollback();
}
// 이 메시지가 프론트엔드의 alert 창에 그대로 뜨게 됩니다!
echo json_encode([
'success' => false,
'message' => 'DB 에러 상세 원인: ' . $e->getMessage()
]);
} finally {
if (isset($conn) && $conn->ping()) {
$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();
?>
+74
View File
@@ -0,0 +1,74 @@
<?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;
}
// 1️⃣ 현재 제품의 최신 내부/외부 버전 정보 가져오기
// updated_at 기준으로 가장 최신 행 1개만 가져옵니다.
$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(PDO::FETCH_ASSOC);
// 데이터가 아예 없을 경우를 대비한 기본값
$familyVer = $verRow['family_version'] ?? '0.0.0';
$externalVer = $verRow['external_version'] ?? '';
// 2️⃣ "사용안함" 로직: 내부와 외부 버전이 같으면 외부 버전은 빈 값으로 처리
if ($externalVer === $familyVer) {
$externalVer = '';
}
// 3️⃣ 콤보박스에 들어갈 '배포 확정'된 버전 목록 가져오기 (deploy_files 테이블 기준)
$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);
// 4️⃣ 필터링 및 "사용안함" 옵션 추가
$filtered = [];
foreach ($allVersions as $v) {
// 내부 배포 버전(최신 작업물)보다 높은 버전은 목록에서 제외 (논리적 오류 방지)
if (version_compare($v, $familyVer, '>')) continue;
$filtered[] = $v;
}
// 목록 최상단에 항상 "사용안함" 추가
array_unshift($filtered, "");
echo json_encode([
'status' => 'ok',
'family_version' => $familyVer, // 프론트의 #latest-family에 표시됨
'external_version' => $externalVer, // 현재 선택된 값
'external_exists' => ($externalVer !== ''),
'confirmed_versions' => $filtered
]);
} catch (Exception $e) {
echo json_encode(['status' => 'fail', 'message' => $e->getMessage()]);
}
+186
View File
@@ -0,0 +1,186 @@
<?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'].'/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 = trim($_POST['version'] ?? ''); // 프론트에서 "" 혹은 "1.2.3" 전달
// $userName = $_SESSION['user']['name'] ?? '알수없음';
// if(!$productCode){
// echo json_encode(['status'=>'fail','message'=>'제품 코드 누락']);
// exit;
// }
// // 1. 현재 테이블의 최신 정보를 먼저 가져옵니다 (내부 버전 유지 목적)
// $st = $pdo->prepare("SELECT family_version FROM deploy_versions WHERE product_code=? ORDER BY updated_at DESC LIMIT 1");
// $st->execute([$productCode]);
// $currentFamily = $st->fetchColumn() ?: '0.0.0';
// // 2. "사용안함" 처리 로직
// // upload_version_get.php 로직에 맞춰 "사용안함"일 때는 외부 버전을 내부 버전과 동일하게 세팅
// if ($version === '' || $version === '0.0.0') {
// $targetExternal = $currentFamily;
// } else {
// $targetExternal = $version;
// }
// // 3. DB 업데이트
// $st = $pdo->prepare("
// INSERT INTO deploy_versions (product_code, family_version, external_version, external_updated_by, updated_at)
// VALUES (:pc, :family, :external, :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,
// ':family' => $currentFamily, // 내부 버전은 그대로 유지
// ':external' => $targetExternal, // 외부 버전만 변경
// ':user' => $userName
// ]);
// echo json_encode([
// 'status' => 'ok',
// 'family_version' => $currentFamily,
// 'external_version' => ($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'].'/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 = isset($_POST['version']) ? trim($_POST['version']) : '';
$userName = $_SESSION['user']['name'] ?? '알수없음';
if(!$productCode){
echo json_encode(['status'=>'fail','message'=>'제품 코드 누락']);
exit;
}
// 1️⃣ 현재 DB에 저장된 내부 최신 버전(family_version)을 가져옵니다.
// 외부 버전만 바꿀 때 내부 버전 정보를 잃어버리지 않기 위함입니다.
$st = $pdo->prepare("SELECT family_version FROM deploy_versions WHERE product_code=? ORDER BY updated_at DESC LIMIT 1");
$st->execute([$productCode]);
$currentFamily = $st->fetchColumn();
// 만약 데이터가 아예 없다면 기본값 설정
if (!$currentFamily) $currentFamily = '0.0.0';
// 2️⃣ "사용안함" 처리 핵심 로직
// GET API 로직(if $externalVer === $familyVer)에 맞춰서,
// 사용자가 "사용안함"을 선택했다면 external_version을 family_version과 똑같이 맞춰 저장합니다.
if ($version === '') {
$targetExternal = $currentFamily; // 내부 버전과 동일하게 변경
} else {
$targetExternal = $version; // 선택한 특정 버전으로 변경
}
// 3️⃣ DB 업데이트 (INSERT ... ON DUPLICATE KEY UPDATE)
// product_code가 PK이거나 Unique 인덱스여야 기존 데이터가 수정됩니다.
$st = $pdo->prepare("
INSERT INTO deploy_versions
(product_code, family_version, external_version, external_updated_by, updated_at)
VALUES
(:pc, :family, :external, :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,
':family' => $currentFamily,
':external' => $targetExternal,
':user' => $userName
]);
echo json_encode([
'status' => 'ok',
'family_version' => $currentFamily,
'external_version' => $targetExternal, // 외부 버전은 targetExternal로 설정
'updated_by' => $userName
]);
} catch(Exception $e){
echo json_encode(['status'=>'fail', 'message'=> $e->getMessage()]);
}
?>