giftcon_dev/app/Services/Admin/Product/AdminPinService.php
sungro815 b0545ab5b9 관리자 상품관리 완료
웹사이트 상품리스트 상세보기 작업중
2026-02-20 18:11:03 +09:00

170 lines
6.7 KiB
PHP

<?php
namespace App\Services\Admin\Product;
use App\Repositories\Admin\Product\AdminPinRepository;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
final class AdminPinService
{
public function __construct(
private readonly AdminPinRepository $repo,
) {}
public function getPinsBySku(int $skuId, array $filters): array
{
return [
'pins' => $this->repo->paginatePins($skuId, $filters),
'stats' => $this->repo->getPinStats($skuId),
];
}
public function recallPins(int $skuId, string $amountType, int $customAmount, string $zipPassword): array
{
// 1. 회수할 수량 결정
$limit = ($amountType === 'ALL') ? PHP_INT_MAX : $customAmount;
if ($limit <= 0) return ['ok' => false, 'message' => '회수할 수량을 1 이상 입력해주세요.'];
DB::beginTransaction();
try {
// 2. 가장 나중에 등록된(ID가 큰) AVAILABLE 핀들 가져오기 (LIFO)
$pinsToRecall = DB::table('gc_pins')
->where('sku_id', $skuId)
->where('status', 'AVAILABLE')
->orderByDesc('id')
->limit($limit)
->get();
if ($pinsToRecall->isEmpty()) {
DB::rollBack();
return ['ok' => false, 'message' => '회수할 수 있는 판매대기(AVAILABLE) 핀이 없습니다.'];
}
// 3. 상태를 'RECALLED'로 업데이트
$pinIds = $pinsToRecall->pluck('id')->toArray();
DB::table('gc_pins')->whereIn('id', $pinIds)->update([
'status' => 'RECALLED',
'updated_at' => now()->format('Y-m-d H:i:s')
]);
// 4. 복호화하여 CSV 데이터 텍스트 생성
// 엑셀에서 한글 깨짐 방지를 위해 BOM(\xEF\xBB\xBF) 추가
$csvData = "\xEF\xBB\xBF" . "ID,PIN_CODE,FACE_VALUE,BUY_PRICE\n";
foreach($pinsToRecall as $pin) {
try {
$decrypted = \Illuminate\Support\Facades\Crypt::decryptString($pin->pin_code);
$csvData .= "{$pin->id},{$decrypted},{$pin->face_value},{$pin->buy_price}\n";
} catch (\Exception $e) {
$csvData .= "{$pin->id},DECRYPT_ERROR,{$pin->face_value},{$pin->buy_price}\n";
}
}
// 5. ZIP 파일 생성 (라라벨 storage/app/public 임시 저장)
$fileName = "recalled_pins_sku_{$skuId}_" . date('YmdHis') . ".zip";
$zipPath = storage_path('app/public/' . $fileName);
$zip = new \ZipArchive();
if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) {
$zip->addFromString('recalled_pins.csv', $csvData);
// 비밀번호 암호화 세팅 (PHP 7.2 이상 지원)
$zip->setPassword($zipPassword);
$zip->setEncryptionName('recalled_pins.csv', \ZipArchive::EM_AES_256);
$zip->close();
} else {
DB::rollBack();
return ['ok' => false, 'message' => 'ZIP 파일 생성에 실패했습니다. (서버의 php-zip 확장을 확인하세요)'];
}
DB::commit();
return ['ok' => true, 'file_path' => $zipPath, 'file_name' => $fileName, 'count' => count($pinIds)];
} catch (\Throwable $e) {
DB::rollBack();
return ['ok' => false, 'message' => '회수 처리 중 오류가 발생했습니다: ' . $e->getMessage()];
}
}
/**
* 텍스트 대량 핀 등록 로직
*/
public function bulkRegisterPins(int $productId, int $skuId, string $bulkText, string $expiryDate = null): array
{
// 1. 텍스트를 줄바꿈 단위로 분리 (빈 줄 제거)
$lines = array_filter(array_map('trim', explode("\n", $bulkText)));
if (empty($lines)) {
return ['ok' => false, 'message' => '입력된 핀 정보가 없습니다.'];
}
$insertData = [];
$now = now()->format('Y-m-d H:i:s');
$successCount = 0;
$duplicateCount = 0;
$errorLines = 0;
foreach ($lines as $line) {
// 2. 콤마(,) 또는 탭(\t)으로 데이터 분리 (예: 핀번호, 정액, 원가)
// 정규식을 사용해 콤마나 탭 여러 개를 하나로 취급
$parts = preg_split('/[\t,]+/', $line);
// 데이터 파싱 (최소한 핀번호는 있어야 함)
$pinCode = trim($parts[0] ?? '');
if (empty($pinCode)) continue;
$faceValue = isset($parts[1]) ? (int)preg_replace('/[^0-9]/', '', $parts[1]) : 0;
$buyPrice = isset($parts[2]) ? (int)preg_replace('/[^0-9]/', '', $parts[2]) : 0;
// 3. 마진율 계산 (정액금액이 0보다 클 때만 계산)
$marginRate = 0.00;
if ($faceValue > 0) {
$marginRate = round((($faceValue - $buyPrice) / $faceValue) * 100, 2);
}
// 4. 보안 처리 (암호화 및 해시)
$encryptedPin = Crypt::encryptString($pinCode);
$pinHash = hash('sha256', $pinCode); // 중복 검사용 해시
$insertData[] = [
'product_id' => $productId,
'sku_id' => $skuId,
'pin_code' => $encryptedPin,
'pin_hash' => $pinHash,
'status' => 'AVAILABLE',
'face_value' => $faceValue,
'buy_price' => $buyPrice,
'margin_rate' => $marginRate,
'expiry_date' => $expiryDate ?: null,
'created_at' => $now,
'updated_at' => $now,
];
}
// 5. 500개씩 청크(Chunk)로 나누어 일괄 Insert (서버 과부하 방지)
DB::beginTransaction();
try {
$chunks = array_chunk($insertData, 500);
$totalInserted = 0;
foreach ($chunks as $chunk) {
// insertOrIgnore 덕분에 중복 핀은 무시되고 성공한 갯수만 반환됨
$insertedRows = $this->repo->insertPinsBulk($chunk);
$totalInserted += $insertedRows;
}
DB::commit();
$duplicateCount = count($insertData) - $totalInserted;
return [
'ok' => true,
'message' => "{$totalInserted}개의 핀이 등록되었습니다. (중복 제외: {$duplicateCount}개)"
];
} catch (\Throwable $e) {
DB::rollBack();
return ['ok' => false, 'message' => '대량 등록 중 DB 오류가 발생했습니다: ' . $e->getMessage()];
}
}
}