- 품목 삭제 시 모든 참조 테이블 사용 여부 체크 (Force Delete) - Product: BOM 구성품/상위품목, BOM 템플릿, 주문, 견적 - Material: BOM 구성품, BOM 템플릿, 입고, LOT - 사용 중인 품목 삭제 불가, 미사용 품목만 영구 삭제 - 일괄 삭제도 동일 로직 적용 - DuplicateCodeException 예외 처리 추가 - ApiResponse.handle()에서 정상 처리되도록 수정 - Handler.php에도 fallback 처리 추가 - i18n 에러 메시지 추가 (in_use, batch_in_use)
171 lines
5.9 KiB
PHP
171 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace App\Exceptions;
|
|
|
|
use Illuminate\Auth\AuthenticationException;
|
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use Throwable;
|
|
|
|
class Handler extends ExceptionHandler
|
|
{
|
|
public function report(Throwable $e): void
|
|
{
|
|
try {
|
|
if (
|
|
app()->environment('local') || app()->environment('production')
|
|
|| app()->environment('LOCAL') || app()->environment('DEV')
|
|
) {
|
|
$this->sendSlackException($e);
|
|
}
|
|
} catch (\Throwable $ex) {
|
|
// app()이 아직 사용 불가하거나 env 문제가 있을 때는 무시
|
|
}
|
|
|
|
parent::report($e); // 로그는 그대로
|
|
}
|
|
|
|
protected function sendSlackException(Throwable $e): void
|
|
{
|
|
try {
|
|
$url = env('LOG_SLACK_WEBHOOK_URL');
|
|
|
|
if (! $url) {
|
|
return;
|
|
}
|
|
|
|
$ip = request()?->ip() ?? 'N/A'; // 요청이 없는 경우 대비
|
|
|
|
Http::post($url, [
|
|
'text' => "*[Laravel] 예외 발생!*\n".
|
|
"• 메시지: `{$e->getMessage()}`\n".
|
|
"• 위치: `{$e->getFile()}:{$e->getLine()}`\n".
|
|
'• 시간: '.now()->toDateTimeString()."\n".
|
|
"• IP: `{$ip}`",
|
|
]);
|
|
} catch (Throwable $ex) {
|
|
logger()->error('슬랙 전송 실패', ['message' => $ex->getMessage()]);
|
|
}
|
|
}
|
|
|
|
public function render($request, Throwable $exception)
|
|
{
|
|
// API 라우트는 무조건 JSON 응답 (Accept 헤더 없어도)
|
|
$isApiRoute = str_starts_with($request->path(), 'api/');
|
|
|
|
if ($request->expectsJson() || $isApiRoute) {
|
|
// 422 Unprocessable Entity - Validation 실패
|
|
if ($exception instanceof ValidationException) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '입력값 검증 실패',
|
|
'error' => [
|
|
'code' => 422,
|
|
'details' => $exception->errors(),
|
|
],
|
|
], 422);
|
|
}
|
|
|
|
// 400 Bad Request
|
|
if ($exception instanceof BadRequestHttpException) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '잘못된 요청',
|
|
'data' => null,
|
|
], 400);
|
|
}
|
|
|
|
// 401 Unauthorized
|
|
if ($exception instanceof AuthenticationException) {
|
|
// 토큰 만료 여부 확인
|
|
$errorCode = null;
|
|
$message = '인증 실패';
|
|
|
|
// Bearer 토큰이 있는 경우 만료 여부 확인
|
|
$bearerToken = $request->bearerToken();
|
|
if ($bearerToken) {
|
|
$token = \Laravel\Sanctum\PersonalAccessToken::findToken($bearerToken);
|
|
if ($token && $token->expires_at && $token->expires_at->isPast()) {
|
|
$errorCode = 'TOKEN_EXPIRED';
|
|
$message = __('error.token_expired');
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => $message,
|
|
'error_code' => $errorCode,
|
|
'data' => null,
|
|
], 401);
|
|
}
|
|
|
|
// 403 Forbidden
|
|
if ($exception instanceof AccessDeniedHttpException) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '권한 없음',
|
|
'data' => null,
|
|
], 403);
|
|
}
|
|
|
|
// 404 Not Found
|
|
if ($exception instanceof NotFoundHttpException) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '존재하지 않는 URI 또는 데이터',
|
|
'data' => null,
|
|
], 404);
|
|
}
|
|
|
|
// 405 Method Not Allowed
|
|
if ($exception instanceof MethodNotAllowedHttpException) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '허용되지 않는 메서드',
|
|
'data' => null,
|
|
], 405);
|
|
}
|
|
|
|
// 400 Bad Request - 품목 코드 중복
|
|
if ($exception instanceof DuplicateCodeException) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => $exception->getMessage(),
|
|
'error' => [
|
|
'code' => 400,
|
|
],
|
|
'duplicate_id' => $exception->getDuplicateId(),
|
|
'duplicate_code' => $exception->getDuplicateCode(),
|
|
], 400);
|
|
}
|
|
|
|
// 500 Internal Server Error (기타 모든 에러)
|
|
if (
|
|
$exception instanceof HttpException &&
|
|
$exception->getStatusCode() === 500
|
|
) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '서버 에러',
|
|
'data' => null,
|
|
], 500);
|
|
}
|
|
|
|
// 그 외 모든 예외
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => '서버 에러',
|
|
'data' => null,
|
|
], 500);
|
|
}
|
|
|
|
return parent::render($request, $exception);
|
|
}
|
|
}
|