Files
sam-api/app/Helpers/ApiResponse.php
hskwon 7278c4742f fix: ApiResponse 4xx 에러에서 스택 트레이스 제외 및 Controller 메서드 수정
- 4xx 클라이언트 에러에는 스택 트레이스 제외
- 5xx 서버 에러에만 debug 모드에서 스택 트레이스 포함
- 10개 Controller의 ApiResponse::handle() → success() 수정
  - BankAccountController, SiteController, CardController
  - DepositController, WithdrawalController, SaleController
  - PurchaseController, PayrollController, ReportController
  - WorkSettingController
- import 경로 수정 (App\Http\Responses → App\Helpers)
2025-12-18 15:42:46 +09:00

203 lines
6.8 KiB
PHP

<?php
namespace App\Helpers;
use App\Exceptions\DuplicateCodeException;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ApiResponse
{
public function normalizeFiles(array $laravelFiles): array
{
$files = ['name' => [], 'type' => [], 'tmp_name' => [], 'size' => [], 'fileType' => []];
foreach ($laravelFiles as $file) {
$files['name'][] = $file->getClientOriginalName();
$files['type'][] = $file->getClientMimeType();
$files['tmp_name'][] = $file->getPathname();
$files['size'][] = $file->getSize();
$files['fileType'][] = '';
}
return $files;
}
// DebugQuery Helper
public static function debugQueryLog(): array
{
$logs = DB::getQueryLog();
return collect($logs)
->skip(3)
->map(function ($log) {
$query = $log['query'];
foreach ($log['bindings'] as $binding) {
$binding = is_numeric($binding) ? $binding : "'".addslashes($binding)."'";
$query = preg_replace('/\\?/', $binding, $query, 1);
}
// \n 제거
$query = str_replace(["\n", "\r"], ' ', $query)." (time: {$log['time']})";
return trim($query);
})->toArray();
}
// ApiResponse Helper
public static function success(
$data = null,
string $message = '요청 성공',
array $debug = [],
int $statusCode = 200
): JsonResponse {
$response = [
'success' => true,
'message' => $message,
'data' => $data,
];
if (! empty($debug)) {
$response['query'] = $debug;
}
return response()->json($response, $statusCode);
}
public static function error(
string $message = '요청 실패',
int $code = 400,
array $error = []
): JsonResponse {
return response()->json([
'success' => false,
'message' => $message,
'error' => [
'code' => $code,
'details' => $error['details'] ?? null,
],
], $code);
}
public static function validate(
bool $condition,
string $message = 'Validation failed',
int $code = 422,
array $extra = []
): ?JsonResponse {
return $condition ? null : self::error($message, $code, $extra);
}
public static function response($type = '', $query = '', $key = ''): array
{
$debug = app()->environment('local') && request()->is('api/*');
$result = match ($type) {
'get' => $key ? $query->get()->keyBy($key) : $query->get(),
'getSub' => $query->get(),
'count' => $query->count(),
'first' => $query->first(),
'success' => 'Success',
'result' => $query,
default => null,
};
if ($type == 'getSub') {
$array = $result->map(function ($item) {
return (array) $item;
})->toArray();
foreach ($array as $row) {
$data[$row[$key]][] = $row;
}
$result = $data ?? [];
}
$response['data'] = $result;
$response['query'] = $debug ? self::debugQueryLog() : [];
// 다음 요청에 로그가 섞이지 않도록 비워준다 (로컬에서만 의미있음)
if ($debug) {
DB::flushQueryLog();
}
return $response;
}
public static function handle(
callable $callback,
string $responseTitle = '요청'
): JsonResponse {
try {
$result = $callback();
// 이미 JsonResponse면 그대로 반환
if ($result instanceof JsonResponse) {
return $result;
}
// [신규] 서비스가 에러 ‘신호 배열’을 반환한 경우 감지
// 허용 포맷 예:
// ['error' => 'NO_TENANT', 'code' => 400]
// ['code' => 404, 'message' => '데이터 없음']
if (is_array($result) && (
array_key_exists('error', $result) ||
(array_key_exists('code', $result) && is_numeric($result['code']) && (int) $result['code'] >= 400)
)
) {
$code = (int) ($result['code'] ?? 400);
$message = (string) ($result['message'] ?? ($result['error'] ?? '서버 에러'));
$details = $result['details'] ?? null;
// 에러에도 쿼리 로그 포함되도록 error()가 처리하게 맡김
return self::error($message, $code, ['details' => $details]);
}
// 표준 박스( ['data'=>..., 'query'=>..., 'statusCode'=>...] ) 하위호환
if (is_array($result) && array_key_exists('data', $result)) {
$data = $result['data'];
$debug = $result['query'] ?? [];
$statusCode = $result['statusCode'] ?? 200;
} else {
// 그냥 도메인 결과만 반환한 경우
$data = $result;
$debug = (app()->environment('local') && request()->is('api/*'))
? self::debugQueryLog()
: [];
$statusCode = 200;
}
return self::success($data, $responseTitle, $debug, $statusCode);
} catch (\Throwable $e) {
// 품목 코드 중복 예외 - duplicate_id, duplicate_code 포함
if ($e instanceof DuplicateCodeException) {
return response()->json([
'success' => false,
'message' => $e->getMessage(),
'error' => ['code' => 400],
'duplicate_id' => $e->getDuplicateId(),
'duplicate_code' => $e->getDuplicateCode(),
], 400);
}
// HttpException 계열은 상태코드/메시지를 그대로 반영
if ($e instanceof HttpException) {
$statusCode = $e->getStatusCode();
// 4xx 클라이언트 에러에는 스택 트레이스 제외, 5xx 서버 에러에만 debug 모드에서 포함
$includeTrace = $statusCode >= 500 && config('app.debug');
return self::error(
$e->getMessage() ?: '서버 에러',
$statusCode,
['details' => $includeTrace ? $e->getTraceAsString() : null]
);
}
// 일반 예외는 500으로 처리, debug 모드에서만 스택 트레이스 포함
return self::error('서버 에러', 500, [
'details' => config('app.debug') ? $e->getTraceAsString() : null,
]);
}
}
}