fix: [api-explorer] 즐겨찾기 및 태그명 표시 개선
- 즐겨찾기 클릭 시 404 오류 수정 (selectEndpointByPath 함수 추가) - 태그명 형식을 "한글명 (English)"로 변경 - 사용자 목록 조회 오류 수정 (user_tenants 피벗 테이블 사용) - 즐겨찾기 토글 시 페이지 새로고침 없이 로컬 상태 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -450,9 +450,22 @@ public function setDefaultEnvironment(int $id): JsonResponse
|
||||
*/
|
||||
public function users(): JsonResponse
|
||||
{
|
||||
$tenantId = auth()->user()->tenant_id;
|
||||
// user_tenants 피벗 테이블에서 기본 테넌트 조회
|
||||
$defaultTenant = \DB::table('user_tenants')
|
||||
->where('user_id', auth()->id())
|
||||
->where('is_default', true)
|
||||
->first();
|
||||
|
||||
$users = \App\Models\User::where('tenant_id', $tenantId)
|
||||
if (!$defaultTenant) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$tenantId = $defaultTenant->tenant_id;
|
||||
|
||||
// 해당 테넌트에 속한 사용자 목록 조회
|
||||
$users = \App\Models\User::whereHas('tenants', function ($query) use ($tenantId) {
|
||||
$query->where('tenant_id', $tenantId);
|
||||
})
|
||||
->select(['id', 'name', 'email'])
|
||||
->orderBy('name')
|
||||
->limit(100)
|
||||
|
||||
@@ -471,12 +471,194 @@ function filterEndpoints() {
|
||||
renderSidebar(grouped);
|
||||
}
|
||||
|
||||
// 사이드바 렌더링
|
||||
// 태그명 한글 매핑
|
||||
const tagNameMap = {
|
||||
// 인증/사용자
|
||||
'Auth': '인증',
|
||||
'User': '사용자',
|
||||
'Admin-Users': '관리자-사용자',
|
||||
'UserInvitation': '사용자 초대',
|
||||
'UserRole': '사용자 역할',
|
||||
'Member': '회원',
|
||||
|
||||
// 결재
|
||||
'Approvals': '결재',
|
||||
'Approval Forms': '결재 양식',
|
||||
'Approval Lines': '결재선',
|
||||
|
||||
// 조직
|
||||
'Tenant': '테넌트',
|
||||
'Tenant.Fields': '테넌트 필드',
|
||||
'Tenant.Option Groups': '테넌트 옵션그룹',
|
||||
'Tenant.Option Values': '테넌트 옵션값',
|
||||
'Tenant.Profiles': '테넌트 프로필',
|
||||
'TenantStatField': '테넌트 통계필드',
|
||||
'Menu': '메뉴',
|
||||
'Role': '역할',
|
||||
'RolePermission': '역할 권한',
|
||||
'Permission': '권한',
|
||||
'Department': '부서',
|
||||
|
||||
// 품목/제품
|
||||
'Product': '제품',
|
||||
'Items': '품목',
|
||||
'Items BOM': '품목 BOM',
|
||||
'Items Files': '품목 파일',
|
||||
'ItemMaster': '품목마스터',
|
||||
'ItemMaster-Relationships': '품목마스터 관계',
|
||||
'Category': '카테고리',
|
||||
'Category-Fields': '카테고리 필드',
|
||||
'Category-Logs': '카테고리 로그',
|
||||
'Category-Templates': '카테고리 템플릿',
|
||||
'Classification': '분류',
|
||||
|
||||
// 설계/BOM
|
||||
'Model': '모델',
|
||||
'ModelVersion': '모델 버전',
|
||||
'BomTemplate': 'BOM 템플릿',
|
||||
'Design BOM': '설계 BOM',
|
||||
'Design Audit': '설계 감사',
|
||||
'BOM Calculation': 'BOM 계산',
|
||||
|
||||
// 견적/주문
|
||||
'Estimate': '견적',
|
||||
'Quote': '견적서',
|
||||
'Plans': '플랜',
|
||||
|
||||
// 거래처
|
||||
'Client': '거래처',
|
||||
'ClientGroup': '거래처 그룹',
|
||||
'Sites': '현장',
|
||||
|
||||
// 재무/회계
|
||||
'Account': '계정',
|
||||
'BankAccounts': '계좌',
|
||||
'Cards': '카드',
|
||||
'Deposits': '입금',
|
||||
'Withdrawals': '출금',
|
||||
'Sales': '매출',
|
||||
'Purchases': '매입',
|
||||
'Payments': '결제',
|
||||
'TaxInvoices': '세금계산서',
|
||||
'BadDebt': '악성채권',
|
||||
'Pricing': '단가',
|
||||
'Loans': '대출',
|
||||
|
||||
// HR
|
||||
'Employees': '직원',
|
||||
'Attendances': '근태',
|
||||
'Leaves': '휴가',
|
||||
'Payrolls': '급여',
|
||||
'WorkSettings': '근무설정',
|
||||
|
||||
// 파일/게시판
|
||||
'Files': '파일',
|
||||
'Folder': '폴더',
|
||||
'Board': '게시판',
|
||||
'Post': '게시글',
|
||||
'Popup': '팝업',
|
||||
|
||||
// 설정
|
||||
'Settings - Common Codes': '설정 - 공통코드',
|
||||
'Settings - Fields': '설정 - 필드',
|
||||
'NotificationSetting': '알림설정',
|
||||
'BarobillSettings': '바로빌설정',
|
||||
'Subscriptions': '구독',
|
||||
|
||||
// 시스템
|
||||
'Dashboard': '대시보드',
|
||||
'Reports': '리포트',
|
||||
'AI Reports': 'AI 리포트',
|
||||
'Push': '푸시',
|
||||
'Internal': '내부',
|
||||
'API Key 인증': 'API Key 인증',
|
||||
|
||||
'default': '기타'
|
||||
};
|
||||
|
||||
// 태그명 한글로 변환 (한글명 (English) 형식)
|
||||
function getKoreanTagName(tag) {
|
||||
const koreanName = tagNameMap[tag];
|
||||
if (koreanName && koreanName !== tag) {
|
||||
return `${koreanName} (${tag})`;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
// 사이드바 렌더링 (즐겨찾기 포함)
|
||||
function renderSidebar(groupedEndpoints) {
|
||||
const container = document.getElementById('endpoint-list');
|
||||
const tags = Object.keys(groupedEndpoints).sort();
|
||||
|
||||
if (tags.length === 0) {
|
||||
let html = '';
|
||||
|
||||
// 즐겨찾기 섹션 (메서드 필터 적용)
|
||||
const bookmarkedItems = allEndpoints.filter(ep => {
|
||||
const key = ep.path + '|' + ep.method;
|
||||
if (!bookmarkedEndpoints.includes(key)) return false;
|
||||
|
||||
// 메서드 필터 적용
|
||||
if (activeMethodFilters.length > 0 && !activeMethodFilters.includes(ep.method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 검색어 필터 적용
|
||||
const searchQuery = document.getElementById('search-input').value.toLowerCase().trim();
|
||||
if (searchQuery) {
|
||||
const searchTargets = [
|
||||
ep.path || '',
|
||||
ep.summary || '',
|
||||
ep.description || '',
|
||||
ep.operationId || '',
|
||||
...(ep.tags || [])
|
||||
].map(s => s.toLowerCase());
|
||||
return searchTargets.some(target => target.includes(searchQuery));
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (bookmarkedItems.length > 0) {
|
||||
html += `
|
||||
<div class="border-b border-gray-200 pb-2 mb-2">
|
||||
<div class="tag-header">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
즐겨찾기 (${bookmarkedItems.length})
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
`;
|
||||
|
||||
bookmarkedItems.forEach(endpoint => {
|
||||
html += `
|
||||
<div class="endpoint-item" data-operation-id="${endpoint.operationId}" onclick="selectEndpoint('${endpoint.operationId}', this)">
|
||||
<span class="method-badge method-${endpoint.method.toLowerCase()}">
|
||||
${endpoint.method}
|
||||
</span>
|
||||
<span class="endpoint-path" title="${escapeHtml(endpoint.summary || endpoint.path)}">
|
||||
${escapeHtml(endpoint.path)}
|
||||
</span>
|
||||
<button onclick="event.stopPropagation(); toggleBookmark('${escapeHtml(endpoint.path)}', '${endpoint.method}', this)"
|
||||
class="text-yellow-500 hover:text-yellow-600">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 검색/필터 결과가 없는 경우
|
||||
if (tags.length === 0 && bookmarkedItems.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<svg class="w-8 h-8 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -488,10 +670,11 @@ function renderSidebar(groupedEndpoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
// 태그별 그룹
|
||||
tags.forEach(tag => {
|
||||
const endpoints = groupedEndpoints[tag];
|
||||
const tagSlug = tag.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const koreanTag = getKoreanTagName(tag);
|
||||
|
||||
html += `
|
||||
<div class="tag-group">
|
||||
@@ -500,7 +683,7 @@ function renderSidebar(groupedEndpoints) {
|
||||
<svg class="w-4 h-4 text-gray-400 transition-transform rotate-90" id="chevron-${tagSlug}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
${escapeHtml(tag)}
|
||||
${escapeHtml(koreanTag)}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">${endpoints.length}</span>
|
||||
</div>
|
||||
@@ -564,6 +747,16 @@ function selectEndpoint(operationId, element) {
|
||||
});
|
||||
}
|
||||
|
||||
// 엔드포인트 선택 (path와 method로 검색) - 즐겨찾기용
|
||||
function selectEndpointByPath(path, method, element) {
|
||||
const endpoint = allEndpoints.find(ep => ep.path === path && ep.method === method);
|
||||
if (endpoint) {
|
||||
selectEndpoint(endpoint.operationId, element);
|
||||
} else {
|
||||
showToast('해당 API를 찾을 수 없습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// API 실행
|
||||
async function executeApi(event) {
|
||||
event.preventDefault();
|
||||
@@ -931,7 +1124,7 @@ function fillFormFromHistory(data) {
|
||||
// (필요시 endpoint URL에서 파싱하여 채울 수 있음)
|
||||
}
|
||||
|
||||
// 즐겨찾기 토글
|
||||
// 즐겨찾기 토글 (새로고침 없이 로컬 상태 업데이트)
|
||||
async function toggleBookmark(endpoint, method, button) {
|
||||
const response = await fetch('{{ route("dev-tools.api-explorer.bookmarks.toggle") }}', {
|
||||
method: 'POST',
|
||||
@@ -943,20 +1136,23 @@ function fillFormFromHistory(data) {
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const key = endpoint + '|' + method;
|
||||
|
||||
if (result.action === 'added') {
|
||||
button.classList.add('text-yellow-500');
|
||||
button.classList.remove('text-gray-400');
|
||||
// 즐겨찾기 추가
|
||||
if (!bookmarkedEndpoints.includes(key)) {
|
||||
bookmarkedEndpoints.push(key);
|
||||
}
|
||||
} else {
|
||||
button.classList.remove('text-yellow-500');
|
||||
button.classList.add('text-gray-400');
|
||||
// 즐겨찾기 제거
|
||||
const index = bookmarkedEndpoints.indexOf(key);
|
||||
if (index > -1) {
|
||||
bookmarkedEndpoints.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 사이드바 새로고침
|
||||
htmx.ajax('GET', '{{ route("dev-tools.api-explorer.endpoints") }}', {
|
||||
target: '#endpoint-list',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
// 현재 필터 상태 유지하면서 사이드바 다시 렌더링
|
||||
filterEndpoints();
|
||||
}
|
||||
|
||||
// 유틸리티
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
@foreach($bookmarks as $bookmark)
|
||||
<div class="endpoint-item" onclick="selectEndpoint('{{ $bookmark->method }}_{{ str_replace('/', '_', $bookmark->endpoint) }}', this)">
|
||||
<div class="endpoint-item" onclick="selectEndpointByPath('{{ $bookmark->endpoint }}', '{{ $bookmark->method }}', this)">
|
||||
<span class="method-badge method-{{ strtolower($bookmark->method) }}">
|
||||
{{ $bookmark->method }}
|
||||
</span>
|
||||
@@ -30,15 +30,124 @@ class="text-yellow-500 hover:text-yellow-600">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
// 태그명 한글 매핑
|
||||
$tagNameMap = [
|
||||
// 인증/사용자
|
||||
'Auth' => '인증',
|
||||
'User' => '사용자',
|
||||
'Admin-Users' => '관리자-사용자',
|
||||
'UserInvitation' => '사용자 초대',
|
||||
'UserRole' => '사용자 역할',
|
||||
'Member' => '회원',
|
||||
|
||||
// 결재
|
||||
'Approvals' => '결재',
|
||||
'Approval Forms' => '결재 양식',
|
||||
'Approval Lines' => '결재선',
|
||||
|
||||
// 조직
|
||||
'Tenant' => '테넌트',
|
||||
'Tenant.Fields' => '테넌트 필드',
|
||||
'Tenant.Option Groups' => '테넌트 옵션그룹',
|
||||
'Tenant.Option Values' => '테넌트 옵션값',
|
||||
'Tenant.Profiles' => '테넌트 프로필',
|
||||
'TenantStatField' => '테넌트 통계필드',
|
||||
'Menu' => '메뉴',
|
||||
'Role' => '역할',
|
||||
'RolePermission' => '역할 권한',
|
||||
'Permission' => '권한',
|
||||
'Department' => '부서',
|
||||
|
||||
// 품목/제품
|
||||
'Product' => '제품',
|
||||
'Items' => '품목',
|
||||
'Items BOM' => '품목 BOM',
|
||||
'Items Files' => '품목 파일',
|
||||
'ItemMaster' => '품목마스터',
|
||||
'ItemMaster-Relationships' => '품목마스터 관계',
|
||||
'Category' => '카테고리',
|
||||
'Category-Fields' => '카테고리 필드',
|
||||
'Category-Logs' => '카테고리 로그',
|
||||
'Category-Templates' => '카테고리 템플릿',
|
||||
'Classification' => '분류',
|
||||
|
||||
// 설계/BOM
|
||||
'Model' => '모델',
|
||||
'ModelVersion' => '모델 버전',
|
||||
'BomTemplate' => 'BOM 템플릿',
|
||||
'Design BOM' => '설계 BOM',
|
||||
'Design Audit' => '설계 감사',
|
||||
'BOM Calculation' => 'BOM 계산',
|
||||
|
||||
// 견적/주문
|
||||
'Estimate' => '견적',
|
||||
'Quote' => '견적서',
|
||||
'Plans' => '플랜',
|
||||
|
||||
// 거래처
|
||||
'Client' => '거래처',
|
||||
'ClientGroup' => '거래처 그룹',
|
||||
'Sites' => '현장',
|
||||
|
||||
// 재무/회계
|
||||
'Account' => '계정',
|
||||
'BankAccounts' => '계좌',
|
||||
'Cards' => '카드',
|
||||
'Deposits' => '입금',
|
||||
'Withdrawals' => '출금',
|
||||
'Sales' => '매출',
|
||||
'Purchases' => '매입',
|
||||
'Payments' => '결제',
|
||||
'TaxInvoices' => '세금계산서',
|
||||
'BadDebt' => '악성채권',
|
||||
'Pricing' => '단가',
|
||||
'Loans' => '대출',
|
||||
|
||||
// HR
|
||||
'Employees' => '직원',
|
||||
'Attendances' => '근태',
|
||||
'Leaves' => '휴가',
|
||||
'Payrolls' => '급여',
|
||||
'WorkSettings' => '근무설정',
|
||||
|
||||
// 파일/게시판
|
||||
'Files' => '파일',
|
||||
'Folder' => '폴더',
|
||||
'Board' => '게시판',
|
||||
'Post' => '게시글',
|
||||
'Popup' => '팝업',
|
||||
|
||||
// 설정
|
||||
'Settings - Common Codes' => '설정 - 공통코드',
|
||||
'Settings - Fields' => '설정 - 필드',
|
||||
'NotificationSetting' => '알림설정',
|
||||
'BarobillSettings' => '바로빌설정',
|
||||
'Subscriptions' => '구독',
|
||||
|
||||
// 시스템
|
||||
'Dashboard' => '대시보드',
|
||||
'Reports' => '리포트',
|
||||
'AI Reports' => 'AI 리포트',
|
||||
'Push' => '푸시',
|
||||
'Internal' => '내부',
|
||||
'API Key 인증' => 'API Key 인증',
|
||||
];
|
||||
@endphp
|
||||
|
||||
{{-- 태그별 엔드포인트 그룹 --}}
|
||||
@forelse($endpointsByTag as $tag => $endpoints)
|
||||
@php
|
||||
$koreanTag = $tagNameMap[$tag] ?? null;
|
||||
$displayTag = $koreanTag && $koreanTag !== $tag ? "{$koreanTag} ({$tag})" : $tag;
|
||||
@endphp
|
||||
<div class="tag-group">
|
||||
<div class="tag-header" onclick="toggleTagGroup('{{ Str::slug($tag) }}')">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-gray-400 transition-transform" id="chevron-{{ Str::slug($tag) }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{{ $tag }}
|
||||
{{ $displayTag }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ $endpoints->count() }}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user