- 업무일지 공용코드 동적 로드 (상태, 요청부서, 패키지, 프로세스, 업무형태) - 업무형태 선택 모달 구현 (프로세스 > 분류 > 항목 3단계 선택) - ESC 키로 모달 닫기 기능 추가 - 필터 섹션 디자인 개선 및 접기/펼치기 기능 추가 - 프로젝트명 50자 초과 시 말줄임(...) 표시 - 상태 "진행중" 오렌지 색상으로 변경 - 테이블 row height 축소 및 날짜 구분선 제거 - 네비게이션 로고 클릭 시 대시보드 이동 기능 추가 - 대시보드 개인정보 표시 제거 (휴가자 명단) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
825 lines
40 KiB
HTML
825 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
<title>공용코드관리</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: '#3B82F6',
|
|
secondary: '#6B7280',
|
|
success: '#10B981',
|
|
danger: '#EF4444',
|
|
warning: '#F59E0B'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
.glass-effect {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
}
|
|
.gradient-bg {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
.card-hover {
|
|
transition: all 0.3s ease;
|
|
}
|
|
.card-hover:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
/* 스크롤바 스타일링 */
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 16px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
border-radius: 8px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
/* 애니메이션 */
|
|
.animate-fade-in {
|
|
animation: fadeIn 0.5s ease-in-out;
|
|
}
|
|
|
|
.animate-slide-up {
|
|
animation: slideUp 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from { transform: translateY(10px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
/* 셀렉트 박스 옵션 스타일링 */
|
|
select option {
|
|
background-color: #374151 !important;
|
|
color: white !important;
|
|
}
|
|
|
|
select option:hover {
|
|
background-color: #4B5563 !important;
|
|
}
|
|
|
|
select option:checked {
|
|
background-color: #6366F1 !important;
|
|
}
|
|
|
|
/* 테이블 셀 텍스트 오버플로우 처리 */
|
|
.truncate {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* 값(문자열) 열 최대 너비 제한 */
|
|
.svalue-cell {
|
|
max-width: 128px; /* w-32 = 128px */
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
|
<div class="container mx-auto px-4 py-8">
|
|
|
|
<!-- 2열 구조 메인 컨테이너 -->
|
|
<div class="flex gap-6 h-[calc(100vh-200px)]">
|
|
<!-- 좌측: 코드그룹 리스트 -->
|
|
<div class="w-80">
|
|
<div class="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
|
|
<div class="p-4 border-b border-white/10">
|
|
<h3 class="text-lg font-semibold text-white flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l-7 7-7-7M19 21l-7-7-7 7"></path>
|
|
</svg>
|
|
코드그룹 목록
|
|
</h3>
|
|
</div>
|
|
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
|
|
<div id="groupList" class="space-y-1">
|
|
<!-- 그룹 목록이 여기에 표시됩니다 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 우측: 상세 데이터 -->
|
|
<div class="flex-1">
|
|
<div class="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
|
|
<!-- 상단 헤더 -->
|
|
<div class="p-4 border-b border-white/10 flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-white flex items-center">
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<span id="selectedGroupTitle">코드그룹을 선택하세요</span>
|
|
</h3>
|
|
<p class="text-white/70 text-sm mt-1">총 <span id="recordCount" class="text-white font-medium">0</span>건</p>
|
|
</div>
|
|
<button onclick="showAddModal()" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg transition-all border border-white/30 flex items-center text-sm">
|
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
|
</svg>
|
|
추가
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 데이터 테이블 -->
|
|
<div class="flex-1 overflow-x-auto overflow-y-auto custom-scrollbar">
|
|
<table class="w-full">
|
|
<thead class="bg-white/10 sticky top-0">
|
|
<tr>
|
|
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">코드</th>
|
|
<th class="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">비고</th>
|
|
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(문자열)</th>
|
|
<th class="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(숫자)</th>
|
|
<th class="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(실수)</th>
|
|
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값2</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dataTable" class="divide-y divide-white/10">
|
|
<tr>
|
|
<td colspan="6" class="px-4 py-8 text-center text-white/70">
|
|
<svg class="w-12 h-12 mx-auto mb-2 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
좌측에서 코드그룹을 선택하세요
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 로딩 인디케이터 -->
|
|
<div id="loadingIndicator" class="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm hidden">
|
|
<div class="flex items-center">
|
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
데이터 로딩 중...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 추가/편집 모달 -->
|
|
<div id="editModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up">
|
|
<!-- 모달 헤더 -->
|
|
<div class="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
|
<h2 id="modalTitle" class="text-xl font-semibold text-white">공용코드 추가</h2>
|
|
<button onclick="hideEditModal()" class="text-white/70 hover:text-white transition-colors">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 모달 내용 -->
|
|
<form id="editForm" class="p-6">
|
|
<input type="hidden" id="editIdx" value="">
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/70 mb-2">코드그룹 *</label>
|
|
<select id="editGrp" required class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
|
<option value="" class="bg-gray-800 text-white">선택하세요</option>
|
|
<!-- 동적으로 로드됩니다 -->
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/70 mb-2">코드 *</label>
|
|
<input type="text" id="editCode" required class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="코드를 입력하세요">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/70 mb-2">값(문자열)</label>
|
|
<input type="text" id="editSvalue" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="문자열 값">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/70 mb-2">값(숫자)</label>
|
|
<input type="number" id="editIvalue" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="숫자 값">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/70 mb-2">값(실수)</label>
|
|
<input type="number" step="0.01" id="editFvalue" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="실수 값">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-white/70 mb-2">값2</label>
|
|
<input type="text" id="editSvalue2" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="추가 문자열 값">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<label class="block text-sm font-medium text-white/70 mb-2">비고</label>
|
|
<textarea id="editMemo" rows="3" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" placeholder="비고사항을 입력하세요"></textarea>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- 모달 푸터 -->
|
|
<div class="px-6 py-4 border-t border-white/10 flex justify-between">
|
|
<button onclick="deleteCurrentItem()" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center">
|
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
삭제
|
|
</button>
|
|
<div class="flex gap-2">
|
|
<button onclick="hideEditModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button onclick="saveData()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 삭제 확인 모달 -->
|
|
<div id="deleteModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="glass-effect rounded-2xl w-full max-w-md animate-slide-up">
|
|
<div class="p-6">
|
|
<div class="flex items-center mb-4">
|
|
<div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
|
|
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900">삭제 확인</h3>
|
|
<p class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</p>
|
|
</div>
|
|
</div>
|
|
<p class="text-gray-700 mb-6">선택한 공용코드를 삭제하시겠습니까?<br><span class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</span></p>
|
|
<div class="flex justify-end gap-2">
|
|
<button onclick="hideDeleteModal()" class="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-lg transition-colors">
|
|
취소
|
|
</button>
|
|
<button onclick="confirmDelete()" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 공통 네비게이션 컴포넌트
|
|
class CommonNavigation {
|
|
constructor(currentPage = '') {
|
|
this.currentPage = currentPage;
|
|
this.menuItems = [];
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
try {
|
|
await this.loadMenuItems();
|
|
this.createNavigation();
|
|
this.addEventListeners();
|
|
} catch (error) {
|
|
console.error('Navigation initialization failed:', error);
|
|
this.createFallbackNavigation();
|
|
}
|
|
}
|
|
|
|
async loadMenuItems() {
|
|
try {
|
|
const response = await fetch('/Common/GetNavigationMenu');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
|
|
if (data.Success && data.Data) {
|
|
this.menuItems = data.Data;
|
|
} else {
|
|
throw new Error(data.Message || 'Failed to load menu items');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load navigation menu:', error);
|
|
this.menuItems = this.getDefaultMenuItems();
|
|
}
|
|
}
|
|
|
|
getDefaultMenuItems() {
|
|
return [
|
|
{ key: 'dashboard', title: '대시보드', url: '/Dashboard/', icon: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z', isVisible: true, sortOrder: 1 },
|
|
{ key: 'common', title: '공용코드', url: '/Common', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', isVisible: true, sortOrder: 2 },
|
|
{ key: 'jobreport', title: '업무일지', url: '/Jobreport/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2', isVisible: true, sortOrder: 3 },
|
|
{ key: 'kuntae', title: '근태관리', url: '/Kuntae/', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', isVisible: true, sortOrder: 4 },
|
|
{ key: 'todo', title: '할일관리', url: '/Todo/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4', isVisible: true, sortOrder: 5 }
|
|
];
|
|
}
|
|
|
|
createFallbackNavigation() {
|
|
this.createNavigation();
|
|
}
|
|
|
|
createNavigation() {
|
|
const nav = document.createElement('nav');
|
|
nav.className = 'glass-effect border-b border-white/10';
|
|
nav.innerHTML = this.getNavigationHTML();
|
|
document.body.insertBefore(nav, document.body.firstChild);
|
|
}
|
|
|
|
getNavigationHTML() {
|
|
const visibleItems = this.menuItems.filter(item => item.isVisible).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
return `
|
|
<div class="container mx-auto px-4">
|
|
<div class="flex items-center justify-between h-16">
|
|
<div class="flex items-center space-x-8">
|
|
<a href="/Dashboard/" class="flex items-center space-x-2 hover:opacity-80 transition-opacity cursor-pointer">
|
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<span class="text-xl font-bold text-white">GroupWare</span>
|
|
</a>
|
|
<nav class="hidden md:flex space-x-1">
|
|
${visibleItems.map(item => `
|
|
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
this.currentPage === item.key
|
|
? 'bg-white/20 text-white'
|
|
: 'text-white/60 hover:text-white hover:bg-white/10'
|
|
}">
|
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
|
|
</svg>
|
|
${item.title}
|
|
</a>
|
|
`).join('')}
|
|
</nav>
|
|
</div>
|
|
<div class="flex items-center space-x-4">
|
|
<div class="text-sm text-white/60">
|
|
<span id="currentUser">사용자</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
getMenuItemHTML(item) {
|
|
const isActive = this.currentPage === item.key;
|
|
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
|
|
|
return `
|
|
<a href="${item.url}" class="${activeClass} transition-colors px-3 py-2 rounded-lg">
|
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
|
|
</svg>
|
|
${item.title}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
getMobileMenuItemHTML(item) {
|
|
const isActive = this.currentPage === item.key;
|
|
const activeClass = isActive ? 'text-white bg-white/20' : 'text-white/80 hover:text-white hover:bg-white/10';
|
|
|
|
return `
|
|
<a href="${item.url}" class="block ${activeClass} transition-colors px-3 py-2 rounded-lg mb-2">
|
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
|
|
</svg>
|
|
${item.title}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
addEventListeners() {
|
|
// 모바일 메뉴 토글
|
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
|
|
if (mobileMenuButton && mobileMenu) {
|
|
mobileMenuButton.addEventListener('click', function() {
|
|
mobileMenu.classList.toggle('hidden');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let currentData = [];
|
|
let deleteTargetIdx = null;
|
|
let groupData = [];
|
|
let selectedGroupCode = null;
|
|
|
|
// 페이지 로드시 초기 데이터 로드
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadGroups();
|
|
});
|
|
|
|
// 코드그룹 목록 로드
|
|
function loadGroups() {
|
|
showLoading();
|
|
|
|
fetch('/Common/GetGroups')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
groupData = data || [];
|
|
renderGroupList();
|
|
renderEditGroupSelect();
|
|
hideLoading();
|
|
})
|
|
.catch(error => {
|
|
console.error('그룹 데이터 로드 중 오류 발생:', error);
|
|
hideLoading();
|
|
showNotification('그룹 데이터 로드 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// 좌측 그룹 리스트 렌더링
|
|
function renderGroupList() {
|
|
const groupList = document.getElementById('groupList');
|
|
|
|
if (groupData.length === 0) {
|
|
groupList.innerHTML = '<div class="text-white/70 text-center py-4">그룹 데이터가 없습니다.</div>';
|
|
return;
|
|
}
|
|
|
|
groupList.innerHTML = groupData.map(group => `
|
|
<div class="group-item cursor-pointer p-3 rounded-lg border border-white/20 hover:bg-white/10 transition-all ${selectedGroupCode === group.code ? 'bg-white/20' : ''}"
|
|
onclick="selectGroup('${group.code}', '${group.memo}')">
|
|
<div class="flex items-center">
|
|
<div class="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center mr-3">
|
|
<span class="text-white text-sm font-medium">${group.code}</span>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-white font-medium">${group.memo}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 그룹 선택 처리
|
|
function selectGroup(code, name) {
|
|
selectedGroupCode = code;
|
|
|
|
// 선택된 그룹 하이라이트 업데이트
|
|
document.querySelectorAll('.group-item').forEach(item => {
|
|
item.classList.remove('bg-white/20');
|
|
});
|
|
event.target.closest('.group-item').classList.add('bg-white/20');
|
|
|
|
// 우측 제목 업데이트
|
|
document.getElementById('selectedGroupTitle').textContent = `${code} - ${name}`;
|
|
|
|
// 해당 그룹의 데이터 로드
|
|
loadDataByGroup(code);
|
|
}
|
|
|
|
// 편집 모달용 그룹 셀렉트 렌더링
|
|
function renderEditGroupSelect() {
|
|
const editGrp = document.getElementById('editGrp');
|
|
editGrp.innerHTML = '<option value="" class="bg-gray-800 text-white">선택하세요</option>' +
|
|
groupData.map(group =>
|
|
`<option value="${group.code}" class="bg-gray-800 text-white">${group.code}-${group.memo}</option>`
|
|
).join('');
|
|
}
|
|
|
|
// 특정 그룹의 데이터 로드
|
|
function loadDataByGroup(grp) {
|
|
showLoading();
|
|
|
|
let url = '/Common/GetList';
|
|
if (grp) {
|
|
url += '?grp=' + encodeURIComponent(grp);
|
|
}
|
|
|
|
fetch(url)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
currentData = data || [];
|
|
renderTable();
|
|
hideLoading();
|
|
})
|
|
.catch(error => {
|
|
console.error('데이터 로드 중 오류 발생:', error);
|
|
hideLoading();
|
|
showNotification('데이터 로드 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// 테이블 렌더링
|
|
function renderTable() {
|
|
const tbody = document.getElementById('dataTable');
|
|
const recordCount = document.getElementById('recordCount');
|
|
|
|
if (currentData.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="px-4 py-8 text-center text-white/70">
|
|
<svg class="w-12 h-12 mx-auto mb-2 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
${selectedGroupCode ? '데이터가 없습니다.' : '좌측에서 코드그룹을 선택하세요'}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
recordCount.textContent = '0';
|
|
return;
|
|
}
|
|
|
|
recordCount.textContent = currentData.length;
|
|
|
|
tbody.innerHTML = currentData.map(item => {
|
|
return `
|
|
<tr class="hover:bg-white/5 transition-colors cursor-pointer" onclick="editItem(${item.idx})">
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.code || '-'}</td>
|
|
<td class="px-4 py-4 text-sm text-white">${item.memo || '-'}</td>
|
|
<td class="px-4 py-4 text-sm text-white svalue-cell" title="${item.svalue || '-'}">${item.svalue || '-'}</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.ivalue || '0'}</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.fvalue || '0.0'}</td>
|
|
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.svalue2 || '-'}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 추가 모달 표시
|
|
function showAddModal() {
|
|
if (!selectedGroupCode) {
|
|
showNotification('먼저 코드그룹을 선택하세요.', 'warning');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('modalTitle').textContent = '공용코드 추가';
|
|
document.getElementById('editForm').reset();
|
|
document.getElementById('editIdx').value = '';
|
|
|
|
// 현재 선택된 코드그룹을 자동 입력
|
|
document.getElementById('editGrp').value = selectedGroupCode;
|
|
|
|
// 삭제 버튼 비활성화 (새로 추가하는 항목이므로)
|
|
const deleteBtn = document.querySelector('#editModal button[onclick="deleteCurrentItem()"]');
|
|
if (deleteBtn) {
|
|
deleteBtn.disabled = true;
|
|
deleteBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
|
}
|
|
|
|
document.getElementById('editModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 편집 모달 표시
|
|
function editItem(idx) {
|
|
const item = currentData.find(x => x.idx === idx);
|
|
if (!item) return;
|
|
|
|
document.getElementById('modalTitle').textContent = '공용코드 편집';
|
|
document.getElementById('editIdx').value = item.idx;
|
|
document.getElementById('editGrp').value = item.grp || '';
|
|
document.getElementById('editCode').value = item.code || '';
|
|
document.getElementById('editSvalue').value = item.svalue || '';
|
|
document.getElementById('editIvalue').value = item.ivalue || '';
|
|
document.getElementById('editFvalue').value = item.fvalue || '';
|
|
document.getElementById('editSvalue2').value = item.svalue2 || '';
|
|
document.getElementById('editMemo').value = item.memo || '';
|
|
|
|
// 삭제 버튼 활성화 (기존 항목 편집이므로)
|
|
const deleteBtn = document.querySelector('#editModal button[onclick="deleteCurrentItem()"]');
|
|
if (deleteBtn) {
|
|
deleteBtn.disabled = false;
|
|
deleteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
}
|
|
|
|
document.getElementById('editModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 편집 모달 숨기기
|
|
function hideEditModal() {
|
|
document.getElementById('editModal').classList.add('hidden');
|
|
}
|
|
|
|
// 삭제 확인
|
|
function deleteItem(idx) {
|
|
deleteTargetIdx = idx;
|
|
document.getElementById('deleteModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 삭제 모달 숨기기
|
|
function hideDeleteModal() {
|
|
document.getElementById('deleteModal').classList.add('hidden');
|
|
deleteTargetIdx = null;
|
|
}
|
|
|
|
// 삭제 실행
|
|
function confirmDelete() {
|
|
if (!deleteTargetIdx) return;
|
|
|
|
showLoading();
|
|
|
|
fetch('/Common/Delete', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ idx: deleteTargetIdx })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
hideLoading();
|
|
if (data.Success) {
|
|
showNotification(data.Message, 'success');
|
|
hideDeleteModal();
|
|
// 현재 선택된 그룹의 데이터 새로고침
|
|
if (selectedGroupCode) {
|
|
loadDataByGroup(selectedGroupCode);
|
|
}
|
|
} else {
|
|
showNotification(data.Message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
hideLoading();
|
|
console.error('삭제 중 오류 발생:', error);
|
|
showNotification('삭제 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// 편집 다이얼로그에서 현재 항목 삭제
|
|
function deleteCurrentItem() {
|
|
const editIdx = document.getElementById('editIdx').value;
|
|
if (!editIdx) {
|
|
showNotification('삭제할 항목이 없습니다.', 'warning');
|
|
return;
|
|
}
|
|
|
|
deleteTargetIdx = parseInt(editIdx);
|
|
hideEditModal();
|
|
document.getElementById('deleteModal').classList.remove('hidden');
|
|
}
|
|
|
|
// 데이터 저장
|
|
function saveData() {
|
|
const form = document.getElementById('editForm');
|
|
if (!form.checkValidity()) {
|
|
form.reportValidity();
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
idx: parseInt(document.getElementById('editIdx').value) || 0,
|
|
grp: document.getElementById('editGrp').value,
|
|
code: document.getElementById('editCode').value,
|
|
svalue: document.getElementById('editSvalue').value,
|
|
ivalue: parseInt(document.getElementById('editIvalue').value) || 0,
|
|
fvalue: parseFloat(document.getElementById('editFvalue').value) || 0.0,
|
|
svalue2: document.getElementById('editSvalue2').value,
|
|
memo: document.getElementById('editMemo').value
|
|
};
|
|
|
|
showLoading();
|
|
|
|
fetch('/Common/Save', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => response.json())
|
|
.then(result => {
|
|
hideLoading();
|
|
if (result.Success) {
|
|
showNotification(result.Message, 'success');
|
|
hideEditModal();
|
|
// 현재 선택된 그룹의 데이터 새로고침
|
|
if (selectedGroupCode) {
|
|
loadDataByGroup(selectedGroupCode);
|
|
}
|
|
} else {
|
|
showNotification(result.Message, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
hideLoading();
|
|
console.error('저장 중 오류 발생:', error);
|
|
showNotification('저장 중 오류가 발생했습니다.', 'error');
|
|
});
|
|
}
|
|
|
|
// 로딩 표시
|
|
function showLoading() {
|
|
document.getElementById('loadingIndicator').classList.remove('hidden');
|
|
}
|
|
|
|
// 로딩 숨기기
|
|
function hideLoading() {
|
|
document.getElementById('loadingIndicator').classList.add('hidden');
|
|
}
|
|
|
|
// 알림 표시
|
|
function showNotification(message, type = 'info') {
|
|
const colors = {
|
|
info: 'bg-blue-500/90 backdrop-blur-sm',
|
|
success: 'bg-green-500/90 backdrop-blur-sm',
|
|
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
|
error: 'bg-red-500/90 backdrop-blur-sm'
|
|
};
|
|
|
|
const icons = {
|
|
info: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>`,
|
|
success: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>`,
|
|
warning: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>`,
|
|
error: `<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>`
|
|
};
|
|
|
|
const notification = document.createElement('div');
|
|
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
|
notification.innerHTML = `
|
|
<div class="flex items-center">
|
|
${icons[type]}
|
|
<span class="ml-2">${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
// 초기 상태 설정 (오른쪽에서 슬라이드 인)
|
|
notification.style.transform = 'translateX(100%)';
|
|
notification.style.opacity = '0';
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
// 애니메이션으로 표시
|
|
setTimeout(() => {
|
|
notification.style.transform = 'translateX(0)';
|
|
notification.style.opacity = '1';
|
|
}, 10);
|
|
|
|
// 자동 숨김
|
|
setTimeout(() => {
|
|
notification.style.transform = 'translateX(100%)';
|
|
notification.style.opacity = '0';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// 키보드 이벤트
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
hideEditModal();
|
|
hideDeleteModal();
|
|
}
|
|
});
|
|
|
|
// 전역 함수로 내비게이션 초기화
|
|
function initNavigation(currentPage = '') {
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new CommonNavigation(currentPage);
|
|
});
|
|
} else {
|
|
new CommonNavigation(currentPage);
|
|
}
|
|
}
|
|
|
|
// 공통 네비게이션 초기화
|
|
initNavigation('common');
|
|
</script>
|
|
</body>
|
|
</html> |