Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
2026-02-03 11:23:58 +09:00
52 changed files with 2982 additions and 534 deletions

View File

@@ -24,6 +24,8 @@ public function __construct(
/**
* 회원사 조회 및 비밀번호 검증 헬퍼
*
* 회원사의 서버 모드에 따라 BarobillService의 서버 모드도 자동 전환합니다.
*
* @return BarobillMember|JsonResponse 회원사 객체 또는 에러 응답
*/
private function validateMemberForUrlApi(int $id): BarobillMember|JsonResponse
@@ -45,6 +47,9 @@ private function validateMemberForUrlApi(int $id): BarobillMember|JsonResponse
], 422);
}
// 회원사의 서버 모드로 BarobillService 전환
$this->barobillService->setServerMode($member->server_mode ?? 'test');
return $member;
}
@@ -856,4 +861,91 @@ public function getServiceCodes(): JsonResponse
],
]);
}
/**
* 회원사별 서버 모드 변경
*
* 특정 회원사의 바로빌 서버 모드(테스트/운영)를 변경합니다.
* 주의: 운영 서버로 전환 시 요금이 부과됩니다.
*/
public function updateServerMode(Request $request, int $id): JsonResponse
{
$member = BarobillMember::find($id);
if (!$member) {
return response()->json([
'success' => false,
'message' => '회원사를 찾을 수 없습니다.',
], 404);
}
$validated = $request->validate([
'server_mode' => 'required|in:test,production',
'confirmed' => 'required|boolean',
], [
'server_mode.required' => '서버 모드를 선택해주세요.',
'server_mode.in' => '서버 모드는 test 또는 production 이어야 합니다.',
'confirmed.required' => '경고 확인이 필요합니다.',
]);
// 확인 체크
if (!$validated['confirmed']) {
return response()->json([
'success' => false,
'message' => '서버 변경 경고를 확인해주세요.',
], 422);
}
$oldMode = $member->server_mode ?? 'test';
$newMode = $validated['server_mode'];
// 변경 없으면 바로 반환
if ($oldMode === $newMode) {
return response()->json([
'success' => true,
'message' => '서버 모드가 이미 ' . ($newMode === 'test' ? '테스트' : '운영') . ' 서버입니다.',
'data' => $member,
]);
}
$member->update(['server_mode' => $newMode]);
Log::info('바로빌 회원사 서버 모드 변경', [
'member_id' => $id,
'biz_no' => $member->biz_no,
'corp_name' => $member->corp_name,
'old_mode' => $oldMode,
'new_mode' => $newMode,
'user_id' => auth()->id(),
]);
return response()->json([
'success' => true,
'message' => ($newMode === 'test' ? '테스트' : '운영') . ' 서버로 변경되었습니다.',
'data' => $member->fresh(),
]);
}
/**
* 회원사별 서버 모드 조회
*/
public function getServerMode(int $id): JsonResponse
{
$member = BarobillMember::find($id);
if (!$member) {
return response()->json([
'success' => false,
'message' => '회원사를 찾을 수 없습니다.',
], 404);
}
return response()->json([
'success' => true,
'data' => [
'server_mode' => $member->server_mode ?? 'test',
'server_mode_label' => $member->server_mode_label,
],
]);
}
}

View File

@@ -102,6 +102,53 @@ public function store(Request $request): JsonResponse
}
}
/**
* 서비스 설정 개별 저장 (체크박스 변경 시 즉시 저장)
*/
public function updateService(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id', 1);
$validated = $request->validate([
'field' => 'required|in:use_tax_invoice,use_bank_account,use_card_usage,use_hometax',
'value' => 'required|boolean',
]);
try {
$setting = BarobillSetting::updateOrCreate(
['tenant_id' => $tenantId],
[
$validated['field'] => $validated['value'],
'corp_name' => '미설정',
'ceo_name' => '미설정',
'corp_num' => '0000000000',
]
);
$serviceNames = [
'use_tax_invoice' => '전자세금계산서',
'use_bank_account' => '계좌조회',
'use_card_usage' => '카드사용내역',
'use_hometax' => '홈텍스매입/매출',
];
$serviceName = $serviceNames[$validated['field']] ?? $validated['field'];
$status = $validated['value'] ? '활성화' : '비활성화';
return response()->json([
'success' => true,
'message' => "{$serviceName} 서비스가 {$status}되었습니다.",
]);
} catch (\Exception $e) {
Log::error('바로빌 서비스 설정 저장 실패', ['error' => $e->getMessage()]);
return response()->json([
'success' => false,
'message' => '설정 저장에 실패했습니다.',
], 500);
}
}
/**
* 서비스 이용 여부 확인 (다른 메뉴에서 참조용)
*/

View File

@@ -107,16 +107,58 @@ public function index(Request $request): View|Response
// 해당 테넌트의 바로빌 회원사 정보
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
// 서버 모드에 따라 SOAP 설정 재초기화
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
return view('barobill.eaccount.index', [
'certKey' => $this->certKey,
'corpNum' => $this->corpNum,
'isTestMode' => $this->isTestMode,
'isTestMode' => $isTestMode,
'hasSoapClient' => $this->soapClient !== null,
'currentTenant' => $currentTenant,
'barobillMember' => $barobillMember,
]);
}
/**
* 회원사 서버 모드에 따라 SOAP 설정 적용
*/
private function applyMemberServerMode(BarobillMember $member): void
{
$memberTestMode = $member->isTestMode();
$targetEnv = $memberTestMode ? 'test' : 'production';
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
$config = BarobillConfig::where('environment', $targetEnv)->first();
if ($config) {
$this->isTestMode = $memberTestMode;
$this->certKey = $config->cert_key;
$this->corpNum = $config->corp_num;
$baseUrl = $config->base_url ?: ($memberTestMode
? 'https://testws.baroservice.com'
: 'https://ws.baroservice.com');
$this->soapUrl = $baseUrl . '/BANKACCOUNT.asmx?WSDL';
// SOAP 클라이언트 재초기화
$this->initSoapClient();
Log::info('[Eaccount] 서버 모드 적용', [
'targetEnv' => $targetEnv,
'certKey' => substr($this->certKey ?? '', 0, 10) . '...',
'corpNum' => $this->corpNum,
'soapUrl' => $this->soapUrl,
]);
} else {
Log::warning('[Eaccount] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
}
}
/**
* 등록된 계좌 목록 조회 (GetBankAccountEx)
*/
@@ -129,6 +171,11 @@ public function accounts(Request $request): JsonResponse
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
// 테넌트별 서버 모드 적용
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
// 바로빌 사용자 ID 결정
$userId = $barobillMember?->barobill_id ?? '';
@@ -210,6 +257,11 @@ public function transactions(Request $request): JsonResponse
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
$userId = $barobillMember?->barobill_id ?? '';
// 테넌트별 서버 모드 적용
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
// DB에서 저장된 계정과목 데이터 조회
$savedData = BankTransaction::getByDateRange($tenantId, $startDate, $endDate, $bankAccountNum ?: null);
@@ -532,12 +584,11 @@ private function getBankName(string $code): string
}
/**
* 계정과목 목록 조회
* 계정과목 목록 조회 (글로벌 데이터)
*/
public function accountCodes(): JsonResponse
{
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$codes = AccountCode::getActiveByTenant($tenantId);
$codes = AccountCode::getActive();
return response()->json([
'success' => true,
@@ -551,15 +602,11 @@ public function accountCodes(): JsonResponse
}
/**
* 전체 계정과목 목록 조회 (설정용, 비활성 포함)
* 전체 계정과목 목록 조회 (설정용, 비활성 포함, 글로벌 데이터)
*/
public function accountCodesAll(): JsonResponse
{
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$codes = AccountCode::where('tenant_id', $tenantId)
->orderBy('sort_order')
->orderBy('code')
->get();
$codes = AccountCode::getAll();
return response()->json([
'success' => true,
@@ -568,23 +615,19 @@ public function accountCodesAll(): JsonResponse
}
/**
* 계정과목 추가
* 계정과목 추가 (글로벌 데이터)
*/
public function accountCodeStore(Request $request): JsonResponse
{
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$validated = $request->validate([
'code' => 'required|string|max:10',
'name' => 'required|string|max:100',
'category' => 'nullable|string|max:50',
]);
// 중복 체크
$exists = AccountCode::where('tenant_id', $tenantId)
->where('code', $validated['code'])
->exists();
// 중복 체크 (글로벌)
$exists = AccountCode::where('code', $validated['code'])->exists();
if ($exists) {
return response()->json([
@@ -593,10 +636,10 @@ public function accountCodeStore(Request $request): JsonResponse
], 422);
}
$maxSort = AccountCode::where('tenant_id', $tenantId)->max('sort_order') ?? 0;
$maxSort = AccountCode::max('sort_order') ?? 0;
$accountCode = AccountCode::create([
'tenant_id' => $tenantId,
'tenant_id' => self::HEADQUARTERS_TENANT_ID, // 글로벌 데이터는 기본 테넌트에 저장
'code' => $validated['code'],
'name' => $validated['name'],
'category' => $validated['category'] ?? null,
@@ -618,16 +661,12 @@ public function accountCodeStore(Request $request): JsonResponse
}
/**
* 계정과목 수정
* 계정과목 수정 (글로벌 데이터)
*/
public function accountCodeUpdate(Request $request, int $id): JsonResponse
{
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$accountCode = AccountCode::where('tenant_id', $tenantId)
->where('id', $id)
->first();
$accountCode = AccountCode::find($id);
if (!$accountCode) {
return response()->json([
@@ -643,10 +682,9 @@ public function accountCodeUpdate(Request $request, int $id): JsonResponse
'is_active' => 'sometimes|boolean',
]);
// 코드 변경 시 중복 체크
// 코드 변경 시 중복 체크 (글로벌)
if (isset($validated['code']) && $validated['code'] !== $accountCode->code) {
$exists = AccountCode::where('tenant_id', $tenantId)
->where('code', $validated['code'])
$exists = AccountCode::where('code', $validated['code'])
->where('id', '!=', $id)
->exists();
@@ -674,16 +712,12 @@ public function accountCodeUpdate(Request $request, int $id): JsonResponse
}
/**
* 계정과목 삭제
* 계정과목 삭제 (글로벌 데이터)
*/
public function accountCodeDestroy(int $id): JsonResponse
{
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$accountCode = AccountCode::where('tenant_id', $tenantId)
->where('id', $id)
->first();
$accountCode = AccountCode::find($id);
if (!$accountCode) {
return response()->json([

View File

@@ -107,16 +107,58 @@ public function index(Request $request): View|Response
// 해당 테넌트의 바로빌 회원사 정보
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
// 서버 모드에 따라 SOAP 설정 재초기화
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
return view('barobill.ecard.index', [
'certKey' => $this->certKey,
'corpNum' => $this->corpNum,
'isTestMode' => $this->isTestMode,
'isTestMode' => $isTestMode,
'hasSoapClient' => $this->soapClient !== null,
'currentTenant' => $currentTenant,
'barobillMember' => $barobillMember,
]);
}
/**
* 회원사 서버 모드에 따라 SOAP 설정 적용
*/
private function applyMemberServerMode(BarobillMember $member): void
{
$memberTestMode = $member->isTestMode();
$targetEnv = $memberTestMode ? 'test' : 'production';
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
$config = BarobillConfig::where('environment', $targetEnv)->first();
if ($config) {
$this->isTestMode = $memberTestMode;
$this->certKey = $config->cert_key;
$this->corpNum = $config->corp_num;
$baseUrl = $config->base_url ?: ($memberTestMode
? 'https://testws.baroservice.com'
: 'https://ws.baroservice.com');
$this->soapUrl = $baseUrl . '/CARD.asmx?WSDL';
// SOAP 클라이언트 재초기화
$this->initSoapClient();
Log::info('[Ecard] 서버 모드 적용', [
'targetEnv' => $targetEnv,
'certKey' => substr($this->certKey ?? '', 0, 10) . '...',
'corpNum' => $this->corpNum,
'soapUrl' => $this->soapUrl,
]);
} else {
Log::warning('[Ecard] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
}
}
/**
* 등록된 카드 목록 조회 (GetCardEx2)
* 레퍼런스: https://dev.barobill.co.kr/docs/references/카드조회-API#GetCardEx2
@@ -124,6 +166,13 @@ public function index(Request $request): View|Response
public function cards(Request $request): JsonResponse
{
try {
// 테넌트별 서버 모드 적용
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
$availOnly = $request->input('availOnly', 0);
$result = $this->callSoap('GetCardEx2', [
@@ -220,6 +269,11 @@ public function transactions(Request $request): JsonResponse
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
$userId = $barobillMember?->barobill_id ?? '';
// 테넌트별 서버 모드 적용
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
// 디버그 로그
Log::info('[ECard] 조회 요청', [
'tenantId' => $tenantId,

View File

@@ -99,16 +99,58 @@ public function index(Request $request): View|Response
? BarobillMember::where('tenant_id', $tenantId)->first()
: null;
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
// 서버 모드에 따라 SOAP 설정 재초기화
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
return view('barobill.etax.index', [
'certKey' => $this->certKey,
'corpNum' => $this->corpNum,
'isTestMode' => $this->isTestMode,
'isTestMode' => $isTestMode,
'hasSoapClient' => $this->soapClient !== null,
'currentTenant' => $currentTenant,
'barobillMember' => $barobillMember,
]);
}
/**
* 회원사 서버 모드에 따라 SOAP 설정 적용
*/
private function applyMemberServerMode(BarobillMember $member): void
{
$memberTestMode = $member->isTestMode();
$targetEnv = $memberTestMode ? 'test' : 'production';
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
$config = BarobillConfig::where('environment', $targetEnv)->first();
if ($config) {
$this->isTestMode = $memberTestMode;
$this->certKey = $config->cert_key;
$this->corpNum = $config->corp_num;
$baseUrl = $config->base_url ?: ($memberTestMode
? 'https://testws.baroservice.com'
: 'https://ws.baroservice.com');
$this->soapUrl = $baseUrl . '/TI.asmx?WSDL';
// SOAP 클라이언트 재초기화
$this->initSoapClient();
Log::info('[Etax] 서버 모드 적용', [
'targetEnv' => $targetEnv,
'certKey' => substr($this->certKey ?? '', 0, 10) . '...',
'corpNum' => $this->corpNum,
'soapUrl' => $this->soapUrl,
]);
} else {
Log::warning('[Etax] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
}
}
// 바로빌 파트너사 (본사) 테넌트 ID
private const HEADQUARTERS_TENANT_ID = 1;
@@ -153,6 +195,13 @@ public function getInvoices(): JsonResponse
*/
public function issue(Request $request): JsonResponse
{
// 테넌트별 서버 모드 적용
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
$input = $request->all();
$useRealAPI = $this->soapClient !== null && ($this->isTestMode || !empty($this->certKey));
@@ -221,6 +270,13 @@ public function issue(Request $request): JsonResponse
*/
public function sendToNts(Request $request): JsonResponse
{
// 테넌트별 서버 모드 적용
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
$invoiceId = $request->input('invoiceId');
// 인보이스 조회

View File

@@ -107,10 +107,18 @@ public function index(Request $request): View|Response
// 해당 테넌트의 바로빌 회원사 정보
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
// 서버 모드에 따라 SOAP 설정 재초기화
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
return view('barobill.hometax.index', [
'certKey' => $this->certKey,
'corpNum' => $this->corpNum,
'isTestMode' => $this->isTestMode,
'isTestMode' => $isTestMode,
'hasSoapClient' => $this->soapClient !== null,
'tenantId' => $tenantId,
'currentTenant' => $currentTenant,
@@ -118,6 +126,39 @@ public function index(Request $request): View|Response
]);
}
/**
* 회원사 서버 모드에 따라 SOAP 설정 적용
*/
private function applyMemberServerMode(BarobillMember $member): void
{
$memberTestMode = $member->isTestMode();
$targetEnv = $memberTestMode ? 'test' : 'production';
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
$config = BarobillConfig::where('environment', $targetEnv)->first();
if ($config) {
$this->isTestMode = $memberTestMode;
$this->certKey = $config->cert_key;
$this->corpNum = $config->corp_num;
$this->baseUrl = $config->base_url ?: ($memberTestMode
? 'https://testws.baroservice.com'
: 'https://ws.baroservice.com');
// SOAP 클라이언트 재초기화
$this->initSoapClient();
Log::info('[Hometax] 서버 모드 적용', [
'targetEnv' => $targetEnv,
'certKey' => substr($this->certKey ?? '', 0, 10) . '...',
'corpNum' => $this->corpNum,
'baseUrl' => $this->baseUrl,
]);
} else {
Log::warning('[Hometax] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
}
}
/**
* 매출 세금계산서 목록 조회 (GetPeriodTaxInvoiceSalesList)
*
@@ -146,6 +187,11 @@ public function sales(Request $request): JsonResponse
]);
}
// 테넌트별 서버 모드 적용
if ($barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
$userId = $barobillMember->barobill_id ?? '';
if (empty($userId)) {
@@ -221,12 +267,16 @@ public function sales(Request $request): JsonResponse
// 작성일 기준으로 정렬 (최신순)
usort($allInvoices, fn($a, $b) => strcmp($b['writeDate'] ?? '', $a['writeDate'] ?? ''));
// 마지막 매출 수집 시간 업데이트
$barobillMember->update(['last_sales_fetch_at' => now()]);
return response()->json([
'success' => true,
'data' => [
'invoices' => $allInvoices,
'pagination' => $lastPagination,
'summary' => $totalSummary
'summary' => $totalSummary,
'lastFetchAt' => now()->format('Y-m-d H:i:s')
]
]);
} catch (\Throwable $e) {
@@ -266,6 +316,11 @@ public function purchases(Request $request): JsonResponse
]);
}
// 테넌트별 서버 모드 적용
if ($barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
$userId = $barobillMember->barobill_id ?? '';
if (empty($userId)) {
@@ -341,12 +396,16 @@ public function purchases(Request $request): JsonResponse
// 작성일 기준으로 정렬 (최신순)
usort($allInvoices, fn($a, $b) => strcmp($b['writeDate'] ?? '', $a['writeDate'] ?? ''));
// 마지막 매입 수집 시간 업데이트
$barobillMember->update(['last_purchases_fetch_at' => now()]);
return response()->json([
'success' => true,
'data' => [
'invoices' => $allInvoices,
'pagination' => $lastPagination,
'summary' => $totalSummary
'summary' => $totalSummary,
'lastFetchAt' => now()->format('Y-m-d H:i:s')
]
]);
} catch (\Throwable $e) {
@@ -491,6 +550,12 @@ public function diagnose(Request $request): JsonResponse
try {
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
// 테넌트별 서버 모드 적용
if ($barobillMember && $barobillMember->server_mode) {
$this->applyMemberServerMode($barobillMember);
}
$userId = $barobillMember?->barobill_id ?? '';
$memberCorpNum = $barobillMember?->biz_no ?? '';
@@ -577,18 +642,24 @@ public function requestCollect(Request $request): JsonResponse
}
/**
* 수집 상태 확인 (미지원 안내)
* 수집 상태 확인 (마지막 수집 시간 조회)
*/
public function collectStatus(Request $request): JsonResponse
{
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
$salesLastFetch = $barobillMember?->last_sales_fetch_at;
$purchasesLastFetch = $barobillMember?->last_purchases_fetch_at;
return response()->json([
'success' => true,
'data' => [
'salesLastCollectDate' => '',
'purchaseLastCollectDate' => '',
'salesLastCollectDate' => $salesLastFetch ? $salesLastFetch->format('Y-m-d H:i') : '',
'purchaseLastCollectDate' => $purchasesLastFetch ? $purchasesLastFetch->format('Y-m-d H:i') : '',
'isCollecting' => false,
'collectStateText' => '확인 필요',
'message' => '서비스 상태 진단 기능을 사용하여 홈택스 연동 상태를 확인해주세요.'
'collectStateText' => ($salesLastFetch || $purchasesLastFetch) ? '조회 완료' : '조회 전',
'message' => '매출/매입 탭을 클릭하면 데이터가 조회되고 수집 시간이 기록됩니다.'
]
]);
}

View File

@@ -0,0 +1,166 @@
<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Controller;
use App\Models\CorporateVehicle;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
class CorporateVehicleController extends Controller
{
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('finance.corporate-vehicles'));
}
return view('finance.corporate-vehicles');
}
public function list(Request $request): JsonResponse
{
$tenantId = session('tenant_id', 1);
$query = CorporateVehicle::where('tenant_id', $tenantId);
// 필터링
if ($request->filled('ownership_type') && $request->ownership_type !== 'all') {
$query->where('ownership_type', $request->ownership_type);
}
if ($request->filled('vehicle_type') && $request->vehicle_type !== 'all') {
$query->where('vehicle_type', $request->vehicle_type);
}
if ($request->filled('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('plate_number', 'like', "%{$search}%")
->orWhere('model', 'like', "%{$search}%")
->orWhere('driver', 'like', "%{$search}%");
});
}
$vehicles = $query->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'data' => $vehicles,
]);
}
public function store(Request $request): JsonResponse
{
$request->validate([
'plate_number' => 'required|string|max:20',
'model' => 'required|string|max:100',
'vehicle_type' => 'required|string|max:20',
'ownership_type' => 'required|in:corporate,rent,lease',
]);
$tenantId = session('tenant_id', 1);
$vehicle = CorporateVehicle::create([
'tenant_id' => $tenantId,
'plate_number' => $request->plate_number,
'model' => $request->model,
'vehicle_type' => $request->vehicle_type,
'ownership_type' => $request->ownership_type,
'year' => $request->year,
'driver' => $request->driver,
'status' => $request->status ?? 'active',
'mileage' => $request->mileage ?? 0,
'memo' => $request->memo,
// 법인차량 전용
'purchase_date' => $request->purchase_date,
'purchase_price' => $request->purchase_price ?? 0,
// 렌트/리스 전용
'contract_date' => $request->contract_date,
'rent_company' => $request->rent_company,
'rent_company_tel' => $request->rent_company_tel,
'rent_period' => $request->rent_period,
'agreed_mileage' => $request->agreed_mileage,
'vehicle_price' => $request->vehicle_price ?? 0,
'residual_value' => $request->residual_value ?? 0,
'deposit' => $request->deposit ?? 0,
'monthly_rent' => $request->monthly_rent ?? 0,
'monthly_rent_tax' => $request->monthly_rent_tax ?? 0,
'insurance_company' => $request->insurance_company,
'insurance_company_tel' => $request->insurance_company_tel,
]);
return response()->json([
'success' => true,
'message' => '차량이 등록되었습니다.',
'data' => $vehicle,
]);
}
public function update(Request $request, int $id): JsonResponse
{
$tenantId = session('tenant_id', 1);
$vehicle = CorporateVehicle::where('tenant_id', $tenantId)->findOrFail($id);
$request->validate([
'plate_number' => 'required|string|max:20',
'model' => 'required|string|max:100',
'vehicle_type' => 'required|string|max:20',
'ownership_type' => 'required|in:corporate,rent,lease',
]);
$vehicle->update([
'plate_number' => $request->plate_number,
'model' => $request->model,
'vehicle_type' => $request->vehicle_type,
'ownership_type' => $request->ownership_type,
'year' => $request->year,
'driver' => $request->driver,
'status' => $request->status ?? 'active',
'mileage' => $request->mileage ?? 0,
'memo' => $request->memo,
// 법인차량 전용
'purchase_date' => $request->purchase_date,
'purchase_price' => $request->purchase_price ?? 0,
// 렌트/리스 전용
'contract_date' => $request->contract_date,
'rent_company' => $request->rent_company,
'rent_company_tel' => $request->rent_company_tel,
'rent_period' => $request->rent_period,
'agreed_mileage' => $request->agreed_mileage,
'vehicle_price' => $request->vehicle_price ?? 0,
'residual_value' => $request->residual_value ?? 0,
'deposit' => $request->deposit ?? 0,
'monthly_rent' => $request->monthly_rent ?? 0,
'monthly_rent_tax' => $request->monthly_rent_tax ?? 0,
'insurance_company' => $request->insurance_company,
'insurance_company_tel' => $request->insurance_company_tel,
]);
return response()->json([
'success' => true,
'message' => '차량 정보가 수정되었습니다.',
'data' => $vehicle,
]);
}
public function destroy(int $id): JsonResponse
{
$tenantId = session('tenant_id', 1);
$vehicle = CorporateVehicle::where('tenant_id', $tenantId)->findOrFail($id);
$vehicle->delete();
return response()->json([
'success' => true,
'message' => '차량이 삭제되었습니다.',
]);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesCommission;
use App\Models\Sales\SalesScenarioChecklist;
use App\Models\Sales\SalesTenantManagement;
use App\Models\Sales\TenantProspect;
@@ -82,7 +83,7 @@ private function getIndexData(Request $request): array
// 영업 역할을 가진 사용자 목록 (영업파트너)
$salesPartners = User::whereHas('userRoles', function ($q) {
$q->whereHas('role', function ($rq) {
$rq->whereIn('name', ['sales', 'manager', 'recruiter']);
$rq->whereIn('name', ['sales', 'manager']);
});
})->orderBy('name')->get();
@@ -136,6 +137,14 @@ private function getIndexData(Request $request): array
$prospect->hq_status = $management?->hq_status ?? 'pending';
$prospect->hq_status_label = $management?->hq_status_label ?? '대기';
$prospect->manager_user = $management?->manager;
// 수당 정보 (management가 있는 경우)
if ($management) {
$commission = SalesCommission::where('management_id', $management->id)->first();
$prospect->commission = $commission;
} else {
$prospect->commission = null;
}
}
// 전체 통계
@@ -185,4 +194,87 @@ public function updateHqStatus(int $id, Request $request)
'hq_status_label' => $management->hq_status_label,
]);
}
/**
* 수당 날짜 기록/수정
*/
public function updateCommissionDate(int $id, Request $request)
{
$this->checkAdminAccess();
$request->validate([
'field' => 'required|in:first_payment_at,first_partner_paid_at,second_payment_at,second_partner_paid_at,first_subscription_at,manager_paid_at',
'date' => 'nullable|date',
]);
$prospect = TenantProspect::findOrFail($id);
$management = SalesTenantManagement::findOrCreateByProspect($prospect->id);
// Commission 레코드 조회 또는 생성
$commission = SalesCommission::firstOrCreate(
['management_id' => $management->id],
[
'tenant_id' => $prospect->tenant_id ?? 1,
'payment_type' => 'deposit',
'payment_amount' => 0,
'payment_date' => now(),
'base_amount' => 0,
'partner_rate' => 0,
'manager_rate' => 0,
'partner_commission' => 0,
'manager_commission' => 0,
'scheduled_payment_date' => now()->addMonth()->day(10),
'status' => SalesCommission::STATUS_PENDING,
'partner_id' => $management->sales_partner_id ?? 0,
'manager_user_id' => $management->manager_user_id,
]
);
$field = $request->input('field');
$date = $request->input('date') ?: now()->format('Y-m-d');
$commission->update([
$field => $date,
]);
return response()->json([
'success' => true,
'field' => $field,
'date' => $commission->$field?->format('Y-m-d'),
'date_display' => $commission->$field?->format('m/d'),
]);
}
/**
* 수당 날짜 삭제 (초기화)
*/
public function clearCommissionDate(int $id, Request $request)
{
$this->checkAdminAccess();
$request->validate([
'field' => 'required|in:first_payment_at,first_partner_paid_at,second_payment_at,second_partner_paid_at,first_subscription_at,manager_paid_at',
]);
$prospect = TenantProspect::findOrFail($id);
$management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first();
if (!$management) {
return response()->json(['success' => false, 'message' => '관리 정보가 없습니다.']);
}
$commission = SalesCommission::where('management_id', $management->id)->first();
if (!$commission) {
return response()->json(['success' => false, 'message' => '수당 정보가 없습니다.']);
}
$field = $request->input('field');
$commission->update([$field => null]);
return response()->json([
'success' => true,
'field' => $field,
]);
}
}

View File

@@ -251,10 +251,8 @@ private function getDashboardData(Request $request): array
->get()
->keyBy('tenant_id');
// 내가 유치한 영업파트너 목록 (드롭다운용)
$allManagers = auth()->user()->children()
->where('is_active', true)
->get(['id', 'name', 'email']);
// 상담매니저 역할을 가진 모든 사용자 (드롭다운용)
$allManagers = $this->getAllManagerUsers();
// 내가 매니저로만 참여하는 건 (다른 사람이 등록, 내가 매니저)
$managerOnlyProspects = $this->getManagerOnlyProspects($currentUserId);
@@ -412,10 +410,8 @@ public function refreshTenantList(Request $request): View
->get()
->keyBy('tenant_id');
// 내가 유치한 영업파트너 목록 (드롭다운용)
$allManagers = auth()->user()->children()
->where('is_active', true)
->get(['id', 'name', 'email']);
// 상담매니저 역할을 가진 모든 사용자 (드롭다운용)
$allManagers = $this->getAllManagerUsers();
return view('sales.dashboard.partials.tenant-list', compact(
'tenants',
@@ -768,6 +764,58 @@ private function getCommissionData(): array
return compact('commissionSummary', 'recentCommissions', 'partner');
}
/**
* 상담매니저 역할을 가진 모든 사용자 조회
* (tenant_id 조건 없이 - 파트너 관리 페이지와 동일한 방식)
*/
private function getAllManagerUsers()
{
return User::whereHas('userRoles.role', fn($q) => $q->where('name', 'manager'))
->where('is_active', true)
->where('id', '!=', auth()->id()) // 본인 제외
->get(['id', 'name', 'email']);
}
/**
* 매니저 검색 API (AJAX)
* (tenant_id 조건 없이 - 모든 상담매니저 검색 가능)
*/
public function searchManagers(Request $request): JsonResponse
{
$query = $request->input('q', '');
$authId = auth()->id();
// 디버깅: SQL 쿼리 로깅
\DB::enableQueryLog();
$managers = User::whereHas('userRoles.role', fn($q) => $q->where('name', 'manager'))
->where('is_active', true)
->where('id', '!=', $authId)
->when($query, function ($q) use ($query) {
$q->where(function ($subQ) use ($query) {
$subQ->where('name', 'like', "%{$query}%")
->orWhere('email', 'like', "%{$query}%");
});
})
->limit(10)
->get(['id', 'name', 'email']);
$sqlLog = \DB::getQueryLog();
\Log::info('searchManagers 디버그', [
'query' => $query,
'auth_id' => $authId,
'result_count' => $managers->count(),
'results' => $managers->pluck('name')->toArray(),
'sql' => $sqlLog,
]);
return response()->json([
'success' => true,
'managers' => $managers,
]);
}
/**
* 영업파트너 가이드북 도움말 모달
*/

View File

@@ -261,7 +261,7 @@ public function delegateRole(Request $request, int $id)
{
$validated = $request->validate([
'to_user_id' => 'required|exists:users,id',
'role_name' => 'required|string|in:manager,recruiter',
'role_name' => 'required|string|in:manager',
]);
$fromUser = User::findOrFail($id);
@@ -270,7 +270,7 @@ public function delegateRole(Request $request, int $id)
try {
$this->service->delegateRole($fromUser, $toUser, $validated['role_name']);
$roleLabel = $validated['role_name'] === 'manager' ? '매니저' : '유치담당';
$roleLabel = '상담매니저';
return redirect()->back()
->with('success', "{$roleLabel} 역할이 {$toUser->name}님에게 위임되었습니다.");
} catch (\InvalidArgumentException $e) {
@@ -285,13 +285,13 @@ public function delegateRole(Request $request, int $id)
public function assignRole(Request $request, int $id)
{
$validated = $request->validate([
'role_name' => 'required|string|in:sales,manager,recruiter',
'role_name' => 'required|string|in:sales,manager',
]);
$partner = User::findOrFail($id);
$this->service->assignRole($partner, $validated['role_name']);
$roleLabels = ['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당'];
$roleLabels = ['sales' => '영업파트너', 'manager' => '상담매니저'];
return redirect()->back()
->with('success', "{$roleLabels[$validated['role_name']]} 역할이 부여되었습니다.");
}
@@ -302,13 +302,13 @@ public function assignRole(Request $request, int $id)
public function removeRole(Request $request, int $id)
{
$validated = $request->validate([
'role_name' => 'required|string|in:sales,manager,recruiter',
'role_name' => 'required|string|in:sales,manager',
]);
$partner = User::findOrFail($id);
$this->service->removeRole($partner, $validated['role_name']);
$roleLabels = ['sales' => '영업', 'manager' => '매니저', 'recruiter' => '유치담당'];
$roleLabels = ['sales' => '영업파트너', 'manager' => '상담매니저'];
return redirect()->back()
->with('success', "{$roleLabels[$validated['role_name']]} 역할이 제거되었습니다.");
}

View File

@@ -228,6 +228,7 @@ public function prospectManagerScenario(int $prospectId, Request $request): View
$steps = config('sales_scenario.manager_steps');
$currentStep = (int) $request->input('step', 1);
$icons = config('sales_scenario.icons');
$readonly = $request->boolean('readonly', false);
// 가망고객 영업 관리 정보 조회 또는 생성
$management = SalesTenantManagement::findOrCreateByProspect($prospectId);
@@ -250,6 +251,7 @@ public function prospectManagerScenario(int $prospectId, Request $request): View
'icons' => $icons,
'management' => $management,
'isProspect' => true,
'readonly' => $readonly,
]);
}
@@ -262,6 +264,7 @@ public function prospectManagerScenario(int $prospectId, Request $request): View
'icons' => $icons,
'management' => $management,
'isProspect' => true,
'readonly' => $readonly,
]);
}

View File

@@ -171,9 +171,10 @@ public function destroy(int $id)
->with('error', '이미 테넌트로 전환된 영업권은 삭제할 수 없습니다.');
}
// 본인 또는 관리자만 삭제 가능
if ($prospect->registered_by !== auth()->id()) {
// TODO: 관리자 권한 체크 추가
// 관리자만 삭제 가능
if (!auth()->user()->isAdmin()) {
return redirect()->route('sales.prospects.index')
->with('error', '삭제 권한이 없습니다. 본사 운영팀에 문의하세요.');
}
$prospect->delete();