Files
report/frontend/src/components/Sidebar.vue
T
2026-07-19 03:44:35 +09:00

493 lines
10 KiB
Vue

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { BarChart3, Wallet, LayoutDashboard, BookOpen, FileText, Users, LogOut, Plus, Building2, MessageSquare, Info, Settings, X, Shield } from 'lucide-vue-next'
import apiClient from '../api/client'
const router = useRouter()
const props = defineProps({
companies: Array
})
const emit = defineEmits(['select', 'edit', 'refresh', 'changeView', 'logout'])
const showInput = ref(false)
const newCompanyName = ref('')
const showAISettings = ref(false)
const aiConfig = ref({
geminiKey: localStorage.getItem('ai_gemini_key') || '',
gptKey: localStorage.getItem('ai_gpt_key') || '',
preferredProvider: localStorage.getItem('ai_preferred_provider') || 'gemini'
})
const saveAISettings = () => {
localStorage.setItem('ai_gemini_key', aiConfig.value.geminiKey)
localStorage.setItem('ai_gpt_key', aiConfig.value.gptKey)
localStorage.setItem('ai_preferred_provider', aiConfig.value.preferredProvider)
showAISettings.value = false
alert('AI 설정이 저장되었습니다.')
}
const goToAIChat = () => {
if (router.currentRoute.value.path !== '/ai-chat') {
router.push('/ai-chat')
}
}
const goToAdmin = () => {
if (router.currentRoute.value.path !== '/admin') {
router.push('/admin')
}
}
const handleAddCompany = async () => {
if (!newCompanyName.value.trim()) return
try {
const res = await apiClient.post('/api/companies', { name: newCompanyName.value })
if (res.ok) {
newCompanyName.value = ''
showInput.value = false
emit('refresh') // 목록 갱신 요청
alert('업체가 등록되었습니다.')
}
} catch (err) {
alert('등록 실패')
}
}
</script>
<template>
<aside class="sidebar">
<div class="sidebar-header">
<h2>관리 Tool</h2>
<!-- <div class="user-info">
<div class="avatar">W</div>
<span>강촌상사 리포트</span>
</div> -->
</div>
<nav class="sidebar-nav">
<!-- 1. 보고서 (단일 메뉴) -->
<div class="nav-group clickable-group" @click="emit('changeView', 'dashboard')">
<h3>
<LayoutDashboard :size="14" /> 보고서
</h3>
</div>
<div class="sidebar-divider"></div>
<!-- 2. 입금액 상세정리 -->
<div class="nav-group">
<h3>
<Wallet :size="14" /> 입금액 상세정리
</h3>
<ul>
<li @click="emit('changeView', 'deposit-all')">
<span class="dot"></span> 년도별 전체 (월별)
</li>
<li @click="emit('changeView', 'deposit-kmong')">
<span class="dot"></span> 크몽 입금액
</li>
</ul>
</div>
<!-- 4. 업체정보 -->
<div class="nav-group">
<h3 @click="emit('changeView', 'company-total')">
<Info :size="14" /> 업체정보
</h3>
</div>
</nav>
<div class="sidebar-footer">
<button class="btn-config" @click="showAISettings = true">
<Settings :size="18" /> AI 설정
</button>
<button class="btn-ai" @click="goToAIChat">
<MessageSquare :size="18" /> AI 모드
</button>
<button class="btn-admin" @click="goToAdmin">
<Shield :size="18" /> 관리자
</button>
<button class="btn-logout" @click="emit('logout')">
<LogOut :size="18" /> 로그아웃
</button>
</div>
<!-- AI 설정 모달 -->
<div v-if="showAISettings" class="settings-modal-overlay" @click.self="showAISettings = false">
<div class="settings-modal">
<div class="modal-header">
<h3>AI 연동 설정</h3>
<button class="btn-close" @click="showAISettings = false">
<X :size="20" />
</button>
</div>
<div class="modal-body">
<div class="input-group">
<label>주요 사용 AI</label>
<select v-model="aiConfig.preferredProvider" class="config-select">
<option value="gemini">Google Gemini</option>
<option value="gpt">OpenAI GPT-4</option>
</select>
</div>
<div class="input-group">
<label>Gemini API Key</label>
<input v-model="aiConfig.geminiKey" type="password" placeholder="AIzaSy...">
</div>
<div class="input-group">
<label>GPT API Key</label>
<input v-model="aiConfig.gptKey" type="password" placeholder="sk-...">
</div>
<p class="config-hint">* 키를 입력하지 않으면 서버의 기본 키를 사용합니다.</p>
</div>
<div class="modal-footer">
<button class="btn-save-config" @click="saveAISettings">설정 저장</button>
</div>
</div>
</div>
</aside>
</template>
<style scoped>
.sidebar-footer {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.btn-config {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
background: rgba(255, 255, 255, 0.05);
color: #94a3b8;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-config:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
/* Settings Modal Styles */
.settings-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.settings-modal {
background: #1e293b;
width: 100%;
max-width: 400px;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
}
.modal-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
font-size: 1.1rem;
color: #fff;
margin: 0;
}
.btn-close {
background: none;
border: none;
color: #94a3b8;
cursor: pointer;
padding: 4px;
}
.modal-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.config-select {
width: 100%;
padding: 10px;
background: #0f172a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #fff;
font-size: 0.9rem;
}
.modal-body input {
width: 100%;
padding: 10px 12px;
background: #0f172a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #fff;
font-size: 0.9rem;
}
.config-hint {
font-size: 0.75rem;
color: #64748b;
margin-top: -0.5rem;
}
.modal-footer {
padding: 1.25rem 1.5rem;
background: rgba(0, 0, 0, 0.2);
text-align: right;
}
.btn-save-config {
background: #2563eb;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-save-config:hover {
background: #1d4ed8;
}
.btn-ai {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.2);
}
.btn-ai:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px -2px rgba(99, 102, 241, 0.3);
opacity: 0.9;
}
.btn-admin {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
background: linear-gradient(135deg, #0ea5e9 0%, #2563eb 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 6px -1px rgba(14, 165, 233, 0.2);
}
.btn-admin:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px -2px rgba(14, 165, 233, 0.3);
opacity: 0.9;
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-right: 0.5rem;
}
.btn-add-inline {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 2px;
}
.btn-add-inline:hover {
background: var(--primary);
border-color: var(--primary);
}
.sidebar-input-box {
padding: 0 0.5rem 0.75rem;
}
.sidebar-input-box input {
width: 100%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
padding: 8px;
border-radius: 6px;
font-size: 0.85rem;
}
.nav-group {
margin-bottom: 1.5rem;
}
.nav-group h3 {
font-size: 0.85rem !important;
color: #94a3b8;
display: flex !important;
align-items: center;
gap: 8px;
cursor: pointer;
}
.nav-group h3:hover,
.nav-group.clickable-group:hover h3 {
color: #fff;
}
.clickable-group {
cursor: pointer;
padding: 4px 0;
border-radius: 6px;
transition: background 0.2s;
}
.clickable-group:hover {
background: rgba(255, 255, 255, 0.05);
}
.nav-group ul {
margin-top: 0.5rem;
padding-left: 0.5rem;
}
.nav-group li {
font-size: 0.8rem !important;
color: #64748b !important;
padding: 6px 12px !important;
display: flex !important;
align-items: center;
gap: 8px;
cursor: pointer;
}
.nav-group li:hover {
color: #fff !important;
background: rgba(255, 255, 255, 0.05) !important;
}
.dot {
width: 4px;
height: 4px;
background: #475569;
border-radius: 50%;
}
.sidebar-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 1.5rem 0;
}
.scrollable-list {
max-height: 240px;
overflow-y: auto;
margin-top: 0.5rem;
list-style: none;
padding: 0;
}
.company-list-item {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
padding: 8px 12px !important;
border-radius: 6px;
cursor: pointer;
margin-bottom: 4px;
gap: 10px;
transition: background 0.2s;
}
.company-list-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.item-content {
flex: 1;
min-width: 0;
}
.truncate {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.85rem;
color: #cbd5e1;
}
.btn-edit-active {
background: var(--primary);
color: white;
border: none;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
white-space: nowrap;
}
.btn-edit-active:hover {
background: #1d4ed8;
}
.empty-msg {
font-size: 0.8rem;
color: #64748b;
padding: 1rem;
text-align: center;
}
</style>