feat: MNG→API 토큰 교환 엔드포인트 추가
- POST /api/v1/internal/exchange-token 추가 - HMAC-SHA256 서명 기반 서버간 인증 - InternalTokenService: 서명 검증 및 Sanctum 토큰 발급 - ExchangeTokenRequest: 요청 검증 (user_id, tenant_id, exp, signature) - ApiKeyMiddleware: 내부 통신 경로 화이트리스트 추가 - i18n 메시지 추가 (error.internal.*, message.internal.*) 환경변수 필요: INTERNAL_EXCHANGE_SECRET (MNG와 동일)
This commit is contained in:
46
app/Http/Controllers/Api/V1/InternalController.php
Normal file
46
app/Http/Controllers/Api/V1/InternalController.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Internal\ExchangeTokenRequest;
|
||||||
|
use App\Services\InternalTokenService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내부 서버간 통신 컨트롤러
|
||||||
|
*
|
||||||
|
* MNG → API 서버간 인증 토큰 교환 등을 처리합니다.
|
||||||
|
*/
|
||||||
|
class InternalController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private InternalTokenService $tokenService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 교환
|
||||||
|
*
|
||||||
|
* MNG 서버에서 HMAC 서명된 페이로드로 API 토큰을 발급받습니다.
|
||||||
|
*/
|
||||||
|
public function exchangeToken(ExchangeTokenRequest $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$result = $this->tokenService->exchange(
|
||||||
|
userId: $validated['user_id'],
|
||||||
|
tenantId: $validated['tenant_id'],
|
||||||
|
exp: $validated['exp'],
|
||||||
|
signature: $validated['signature']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $result['success']) {
|
||||||
|
return ApiResponse::error($result['error'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::success(
|
||||||
|
data: $result['data'],
|
||||||
|
message: __('message.internal.token_exchanged')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ public function handle(Request $request, Closure $next)
|
|||||||
'api/v1/register',
|
'api/v1/register',
|
||||||
'api/v1/refresh',
|
'api/v1/refresh',
|
||||||
'api/v1/debug-apikey',
|
'api/v1/debug-apikey',
|
||||||
|
'api/v1/internal/*', // 내부 서버간 통신 (HMAC 인증 사용)
|
||||||
'api-docs', // Swagger UI
|
'api-docs', // Swagger UI
|
||||||
'api-docs/*', // Swagger 하위 경로
|
'api-docs/*', // Swagger 하위 경로
|
||||||
'docs', // L5-Swagger UI
|
'docs', // L5-Swagger UI
|
||||||
@@ -120,6 +121,7 @@ public function handle(Request $request, Closure $next)
|
|||||||
'api/v1/register',
|
'api/v1/register',
|
||||||
'api/v1/refresh',
|
'api/v1/refresh',
|
||||||
'api/v1/debug-apikey',
|
'api/v1/debug-apikey',
|
||||||
|
'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용)
|
||||||
// 추가적으로 허용하고 싶은 라우트
|
// 추가적으로 허용하고 싶은 라우트
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
53
app/Http/Requests/Internal/ExchangeTokenRequest.php
Normal file
53
app/Http/Requests/Internal/ExchangeTokenRequest.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Internal;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 교환 요청 검증
|
||||||
|
*/
|
||||||
|
class ExchangeTokenRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id' => ['required', 'integer', 'min:1'],
|
||||||
|
'tenant_id' => ['required', 'integer', 'min:1'],
|
||||||
|
'exp' => ['required', 'integer', 'min:1'],
|
||||||
|
'signature' => ['required', 'string', 'size:64'], // SHA256 hex = 64 chars
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom messages for validator errors.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_id.required' => __('validation.required', ['attribute' => 'user_id']),
|
||||||
|
'user_id.integer' => __('validation.integer', ['attribute' => 'user_id']),
|
||||||
|
'tenant_id.required' => __('validation.required', ['attribute' => 'tenant_id']),
|
||||||
|
'tenant_id.integer' => __('validation.integer', ['attribute' => 'tenant_id']),
|
||||||
|
'exp.required' => __('validation.required', ['attribute' => 'exp']),
|
||||||
|
'exp.integer' => __('validation.integer', ['attribute' => 'exp']),
|
||||||
|
'signature.required' => __('validation.required', ['attribute' => 'signature']),
|
||||||
|
'signature.size' => __('validation.size.string', ['attribute' => 'signature', 'size' => 64]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
164
app/Services/InternalTokenService.php
Normal file
164
app/Services/InternalTokenService.php
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Members\User;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내부 서버간 토큰 교환 서비스
|
||||||
|
*
|
||||||
|
* MNG 서버에서 HMAC 서명된 페이로드를 검증하고
|
||||||
|
* API 사용을 위한 Sanctum 토큰을 발급합니다.
|
||||||
|
*/
|
||||||
|
class InternalTokenService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 토큰 유효 시간 (초) - 기본 1시간
|
||||||
|
*/
|
||||||
|
private const TOKEN_EXPIRES_IN = 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서명 유효 시간 (초) - 기본 5분
|
||||||
|
*/
|
||||||
|
private const SIGNATURE_VALID_DURATION = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 서명된 페이로드 검증
|
||||||
|
*
|
||||||
|
* @param int $userId 사용자 ID
|
||||||
|
* @param int $tenantId 테넌트 ID
|
||||||
|
* @param int $exp 만료 타임스탬프
|
||||||
|
* @param string $signature HMAC 서명
|
||||||
|
* @return array{valid: bool, error?: string}
|
||||||
|
*/
|
||||||
|
public function verifySignature(int $userId, int $tenantId, int $exp, string $signature): array
|
||||||
|
{
|
||||||
|
$sharedSecret = config('services.internal.exchange_secret');
|
||||||
|
|
||||||
|
if (empty($sharedSecret)) {
|
||||||
|
Log::error('[InternalTokenService] exchange_secret not configured');
|
||||||
|
|
||||||
|
return ['valid' => false, 'error' => __('error.internal.secret_not_configured')];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 만료 시간 검증 (현재 시간 기준 SIGNATURE_VALID_DURATION 초 이내)
|
||||||
|
$now = time();
|
||||||
|
if ($exp < $now) {
|
||||||
|
Log::warning('[InternalTokenService] Signature expired', [
|
||||||
|
'exp' => $exp,
|
||||||
|
'now' => $now,
|
||||||
|
'diff' => $now - $exp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['valid' => false, 'error' => __('error.internal.signature_expired')];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exp > $now + self::SIGNATURE_VALID_DURATION) {
|
||||||
|
Log::warning('[InternalTokenService] Signature exp too far in future', [
|
||||||
|
'exp' => $exp,
|
||||||
|
'now' => $now,
|
||||||
|
'diff' => $exp - $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['valid' => false, 'error' => __('error.internal.invalid_exp')];
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMAC 서명 검증
|
||||||
|
$payload = "{$userId}:{$tenantId}:{$exp}";
|
||||||
|
$expectedSignature = hash_hmac('sha256', $payload, $sharedSecret);
|
||||||
|
|
||||||
|
if (! hash_equals($expectedSignature, $signature)) {
|
||||||
|
Log::warning('[InternalTokenService] Invalid signature', [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ['valid' => false, 'error' => __('error.internal.invalid_signature')];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['valid' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자 토큰 발급
|
||||||
|
*
|
||||||
|
* @param int $userId 사용자 ID
|
||||||
|
* @param int $tenantId 테넌트 ID
|
||||||
|
* @return array{access_token: string, token_type: string, expires_in: int}|null
|
||||||
|
*/
|
||||||
|
public function issueToken(int $userId, int $tenantId): ?array
|
||||||
|
{
|
||||||
|
$user = User::find($userId);
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
Log::warning('[InternalTokenService] User not found', ['user_id' => $userId]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 해당 테넌트에 소속되어 있는지 확인
|
||||||
|
$userTenant = $user->userTenants()->where('tenant_id', $tenantId)->first();
|
||||||
|
if (! $userTenant) {
|
||||||
|
Log::warning('[InternalTokenService] User not in tenant', [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기존 mng_session 토큰 삭제 (중복 방지)
|
||||||
|
$user->tokens()->where('name', 'mng_session')->delete();
|
||||||
|
|
||||||
|
// 새 토큰 발급
|
||||||
|
$token = $user->createToken('mng_session', ['*'], now()->addSeconds(self::TOKEN_EXPIRES_IN));
|
||||||
|
|
||||||
|
Log::info('[InternalTokenService] Token issued', [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'token_id' => $token->accessToken->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'access_token' => $token->plainTextToken,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'expires_in' => self::TOKEN_EXPIRES_IN,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 토큰 교환 실행 (검증 + 발급)
|
||||||
|
*
|
||||||
|
* @param int $userId 사용자 ID
|
||||||
|
* @param int $tenantId 테넌트 ID
|
||||||
|
* @param int $exp 만료 타임스탬프
|
||||||
|
* @param string $signature HMAC 서명
|
||||||
|
* @return array{success: bool, data?: array, error?: string}
|
||||||
|
*/
|
||||||
|
public function exchange(int $userId, int $tenantId, int $exp, string $signature): array
|
||||||
|
{
|
||||||
|
// 1. 서명 검증
|
||||||
|
$verification = $this->verifySignature($userId, $tenantId, $exp, $signature);
|
||||||
|
if (! $verification['valid']) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $verification['error'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 토큰 발급
|
||||||
|
$tokenData = $this->issueToken($userId, $tenantId);
|
||||||
|
if (! $tokenData) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => __('error.internal.token_issue_failed'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => $tokenData,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -241,6 +241,17 @@
|
|||||||
'connection_failed' => 'AI 서버 연결에 실패했습니다.',
|
'connection_failed' => 'AI 서버 연결에 실패했습니다.',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 가지급금 관리 관련
|
||||||
|
'loan' => [
|
||||||
|
'not_found' => '가지급금 정보를 찾을 수 없습니다.',
|
||||||
|
'not_editable' => '정산 완료된 가지급금은 수정할 수 없습니다.',
|
||||||
|
'not_deletable' => '정산 완료된 가지급금은 삭제할 수 없습니다.',
|
||||||
|
'not_settleable' => '이미 정산 완료된 가지급금입니다.',
|
||||||
|
'settlement_exceeds' => '정산금액이 가지급금액을 초과할 수 없습니다.',
|
||||||
|
'invalid_withdrawal' => '유효하지 않은 출금 내역입니다.',
|
||||||
|
'user_not_found' => '직원 정보를 찾을 수 없습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
// 내부 서버간 통신 관련
|
// 내부 서버간 통신 관련
|
||||||
'internal' => [
|
'internal' => [
|
||||||
'secret_not_configured' => '내부 교환 비밀키가 설정되지 않았습니다.',
|
'secret_not_configured' => '내부 교환 비밀키가 설정되지 않았습니다.',
|
||||||
|
|||||||
@@ -300,4 +300,20 @@
|
|||||||
'generated' => 'AI 리포트가 생성되었습니다.',
|
'generated' => 'AI 리포트가 생성되었습니다.',
|
||||||
'deleted' => 'AI 리포트가 삭제되었습니다.',
|
'deleted' => 'AI 리포트가 삭제되었습니다.',
|
||||||
],
|
],
|
||||||
|
// 가지급금 관리
|
||||||
|
'loan' => [
|
||||||
|
'fetched' => '가지급금을 조회했습니다.',
|
||||||
|
'created' => '가지급금이 등록되었습니다.',
|
||||||
|
'updated' => '가지급금이 수정되었습니다.',
|
||||||
|
'deleted' => '가지급금이 삭제되었습니다.',
|
||||||
|
'settled' => '가지급금이 정산되었습니다.',
|
||||||
|
'summary_fetched' => '가지급금 요약을 조회했습니다.',
|
||||||
|
'interest_calculated' => '인정이자가 계산되었습니다.',
|
||||||
|
'interest_report_fetched' => '인정이자 리포트를 조회했습니다.',
|
||||||
|
],
|
||||||
|
|
||||||
|
// 내부 서버간 통신
|
||||||
|
'internal' => [
|
||||||
|
'token_exchanged' => '토큰이 발급되었습니다.',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Api\Admin\GlobalMenuController;
|
use App\Http\Controllers\Api\Admin\GlobalMenuController;
|
||||||
use App\Http\Controllers\Api\V1\AdminController;
|
use App\Http\Controllers\Api\V1\AdminController;
|
||||||
|
use App\Http\Controllers\Api\V1\AiReportController;
|
||||||
use App\Http\Controllers\Api\V1\ApiController;
|
use App\Http\Controllers\Api\V1\ApiController;
|
||||||
use App\Http\Controllers\Api\V1\ApprovalController;
|
use App\Http\Controllers\Api\V1\ApprovalController;
|
||||||
use App\Http\Controllers\Api\V1\ApprovalFormController;
|
use App\Http\Controllers\Api\V1\ApprovalFormController;
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
use App\Http\Controllers\Api\V1\EstimateController;
|
use App\Http\Controllers\Api\V1\EstimateController;
|
||||||
use App\Http\Controllers\Api\V1\FileStorageController;
|
use App\Http\Controllers\Api\V1\FileStorageController;
|
||||||
use App\Http\Controllers\Api\V1\FolderController;
|
use App\Http\Controllers\Api\V1\FolderController;
|
||||||
|
use App\Http\Controllers\Api\V1\InternalController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\CustomTabController;
|
use App\Http\Controllers\Api\V1\ItemMaster\CustomTabController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\EntityRelationshipController;
|
use App\Http\Controllers\Api\V1\ItemMaster\EntityRelationshipController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemBomItemController;
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemBomItemController;
|
||||||
@@ -37,19 +39,20 @@
|
|||||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemMasterController;
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemMasterController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemPageController;
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemPageController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
|
use App\Http\Controllers\Api\V1\ItemMaster\ItemSectionController;
|
||||||
|
// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\SectionTemplateController;
|
use App\Http\Controllers\Api\V1\ItemMaster\SectionTemplateController;
|
||||||
use App\Http\Controllers\Api\V1\ItemMaster\UnitOptionController;
|
use App\Http\Controllers\Api\V1\ItemMaster\UnitOptionController;
|
||||||
// use App\Http\Controllers\Api\V1\MaterialController; // REMOVED: materials 테이블 삭제됨
|
|
||||||
use App\Http\Controllers\Api\V1\ItemsBomController;
|
use App\Http\Controllers\Api\V1\ItemsBomController;
|
||||||
use App\Http\Controllers\Api\V1\ItemsController;
|
use App\Http\Controllers\Api\V1\ItemsController;
|
||||||
use App\Http\Controllers\Api\V1\ItemsFileController;
|
use App\Http\Controllers\Api\V1\ItemsFileController;
|
||||||
use App\Http\Controllers\Api\V1\LeaveController;
|
use App\Http\Controllers\Api\V1\LeaveController;
|
||||||
|
use App\Http\Controllers\Api\V1\LoanController;
|
||||||
use App\Http\Controllers\Api\V1\MenuController;
|
use App\Http\Controllers\Api\V1\MenuController;
|
||||||
use App\Http\Controllers\Api\V1\ModelSetController;
|
use App\Http\Controllers\Api\V1\ModelSetController;
|
||||||
use App\Http\Controllers\Api\V1\PayrollController;
|
|
||||||
use App\Http\Controllers\Api\V1\PermissionController;
|
|
||||||
// use App\Http\Controllers\Api\V1\ProductBomItemController; // REMOVED: products 테이블 삭제됨
|
// use App\Http\Controllers\Api\V1\ProductBomItemController; // REMOVED: products 테이블 삭제됨
|
||||||
// use App\Http\Controllers\Api\V1\ProductController; // REMOVED: products 테이블 삭제됨
|
// use App\Http\Controllers\Api\V1\ProductController; // REMOVED: products 테이블 삭제됨
|
||||||
|
use App\Http\Controllers\Api\V1\PayrollController;
|
||||||
|
use App\Http\Controllers\Api\V1\PermissionController;
|
||||||
use App\Http\Controllers\Api\V1\PostController;
|
use App\Http\Controllers\Api\V1\PostController;
|
||||||
use App\Http\Controllers\Api\V1\PricingController;
|
use App\Http\Controllers\Api\V1\PricingController;
|
||||||
use App\Http\Controllers\Api\V1\PurchaseController;
|
use App\Http\Controllers\Api\V1\PurchaseController;
|
||||||
@@ -58,23 +61,21 @@
|
|||||||
use App\Http\Controllers\Api\V1\RefreshController;
|
use App\Http\Controllers\Api\V1\RefreshController;
|
||||||
use App\Http\Controllers\Api\V1\RegisterController;
|
use App\Http\Controllers\Api\V1\RegisterController;
|
||||||
use App\Http\Controllers\Api\V1\ReportController;
|
use App\Http\Controllers\Api\V1\ReportController;
|
||||||
use App\Http\Controllers\Api\V1\AiReportController;
|
|
||||||
use App\Http\Controllers\Api\V1\RoleController;
|
use App\Http\Controllers\Api\V1\RoleController;
|
||||||
use App\Http\Controllers\Api\V1\RolePermissionController;
|
use App\Http\Controllers\Api\V1\RolePermissionController;
|
||||||
use App\Http\Controllers\Api\V1\SaleController;
|
use App\Http\Controllers\Api\V1\SaleController;
|
||||||
use App\Http\Controllers\Api\V1\SiteController;
|
use App\Http\Controllers\Api\V1\SiteController;
|
||||||
use App\Http\Controllers\Api\V1\TenantController;
|
|
||||||
// 설계 전용 (디자인 네임스페이스)
|
// 설계 전용 (디자인 네임스페이스)
|
||||||
|
use App\Http\Controllers\Api\V1\TenantController;
|
||||||
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
||||||
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
use App\Http\Controllers\Api\V1\TenantOptionGroupController;
|
||||||
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
use App\Http\Controllers\Api\V1\TenantOptionValueController;
|
||||||
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
use App\Http\Controllers\Api\V1\TenantStatFieldController;
|
||||||
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
|
||||||
// 모델셋 관리 (견적 시스템)
|
// 모델셋 관리 (견적 시스템)
|
||||||
|
use App\Http\Controllers\Api\V1\TenantUserProfileController;
|
||||||
use App\Http\Controllers\Api\V1\UserController;
|
use App\Http\Controllers\Api\V1\UserController;
|
||||||
use App\Http\Controllers\Api\V1\UserRoleController;
|
use App\Http\Controllers\Api\V1\UserRoleController;
|
||||||
use App\Http\Controllers\Api\V1\WithdrawalController;
|
use App\Http\Controllers\Api\V1\WithdrawalController;
|
||||||
use App\Http\Controllers\Api\V1\InternalController;
|
|
||||||
use App\Http\Controllers\Api\V1\WorkSettingController;
|
use App\Http\Controllers\Api\V1\WorkSettingController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@@ -391,6 +392,18 @@
|
|||||||
Route::get('/{id}/payslip', [PayrollController::class, 'payslip'])->whereNumber('id')->name('v1.payrolls.payslip');
|
Route::get('/{id}/payslip', [PayrollController::class, 'payslip'])->whereNumber('id')->name('v1.payrolls.payslip');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Loan API (가지급금 관리)
|
||||||
|
Route::prefix('loans')->group(function () {
|
||||||
|
Route::get('', [LoanController::class, 'index'])->name('v1.loans.index');
|
||||||
|
Route::post('', [LoanController::class, 'store'])->name('v1.loans.store');
|
||||||
|
Route::get('/summary', [LoanController::class, 'summary'])->name('v1.loans.summary');
|
||||||
|
Route::post('/calculate-interest', [LoanController::class, 'calculateInterest'])->name('v1.loans.calculate-interest');
|
||||||
|
Route::get('/{id}', [LoanController::class, 'show'])->whereNumber('id')->name('v1.loans.show');
|
||||||
|
Route::put('/{id}', [LoanController::class, 'update'])->whereNumber('id')->name('v1.loans.update');
|
||||||
|
Route::delete('/{id}', [LoanController::class, 'destroy'])->whereNumber('id')->name('v1.loans.destroy');
|
||||||
|
Route::post('/{id}/settle', [LoanController::class, 'settle'])->whereNumber('id')->name('v1.loans.settle');
|
||||||
|
});
|
||||||
|
|
||||||
// Sale API (매출 관리)
|
// Sale API (매출 관리)
|
||||||
Route::prefix('sales')->group(function () {
|
Route::prefix('sales')->group(function () {
|
||||||
Route::get('', [SaleController::class, 'index'])->name('v1.sales.index');
|
Route::get('', [SaleController::class, 'index'])->name('v1.sales.index');
|
||||||
|
|||||||
Reference in New Issue
Block a user