$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()]; } } }