493 lines
10 KiB
Vue
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>
|