Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,17 @@ async function startBackendServices(): Promise<void> {
} else if (platform === 'darwin') {
apiPath = path.join(basePath, 'mac', 'qgenie-api')
aiPath = path.join(basePath, 'mac', 'qgenie-ai')
} else if (platform === 'linux') {
apiPath = path.join(basePath, 'linux', 'qgenie-api')
aiPath = path.join(basePath, 'linux', 'qgenie-ai')
} else {
throw new Error(`Unsupported platform: ${platform}`)
}

if (!fs.existsSync(apiPath)) throw new Error(`API executable not found: ${apiPath}`)
if (!fs.existsSync(aiPath)) throw new Error(`AI executable not found: ${aiPath}`)

if (platform === 'darwin') {
if (platform === 'darwin' || platform === 'linux') {
fs.chmodSync(apiPath, 0o755)
fs.chmodSync(aiPath, 0o755)
}
Expand Down
49 changes: 48 additions & 1 deletion src/main/ipc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrowserWindow, ipcMain, shell } from 'electron'
import { BrowserWindow, ipcMain, shell, dialog } from 'electron'
import fs from 'fs'
import { createSubWindow } from '../windows/subWindow'
import axios from 'axios'

Expand Down Expand Up @@ -34,6 +35,50 @@ export function registerIpcHandlers(mainWindow?: BrowserWindow): void {
win?.close()
})

// 파일 저장 핸들러: SQL
ipcMain.on('save-sql', async (_event, sqlContent: string) => {
const window = BrowserWindow.getFocusedWindow()
if (!window) return

const { filePath } = await dialog.showSaveDialog(window, {
title: 'Save SQL File',
defaultPath: `query-${Date.now()}.sql`,
filters: [{ name: 'SQL Files', extensions: ['sql'] }]
})

if (filePath) {
try {
fs.writeFileSync(filePath, sqlContent, 'utf-8')
console.log('SQL file saved to:', filePath)
} catch (err) {
console.error('Failed to save SQL file:', err)
}
}
})

// 파일 저장 핸들러: CSV
ipcMain.on('save-csv', async (_event, csvContent: string) => {
const window = BrowserWindow.getFocusedWindow()
if (!window) return

const { filePath } = await dialog.showSaveDialog(window, {
title: 'Save CSV File',
defaultPath: `results-${Date.now()}.csv`,
filters: [{ name: 'CSV Files', extensions: ['csv'] }]
})

if (filePath) {
try {
// CSV 파일에 BOM을 추가하여 Excel에서 한글이 깨지지 않도록 함
const BOM = '\uFEFF'
fs.writeFileSync(filePath, BOM + csvContent, 'utf-8')
console.log('CSV file saved to:', filePath)
} catch (err) {
console.error('Failed to save CSV file:', err)
}
}
})

// Vercel AI SDK 'useChat' 훅을 위한 핸들러 (스트리밍 시뮬레이션)
ipcMain.handle('chat:completion', async (event, { messages, chatTabId }) => {
if (!chatTabId) {
Expand All @@ -58,6 +103,8 @@ export function registerIpcHandlers(mainWindow?: BrowserWindow): void {
requestBody
)

console.log('Backend response data:', JSON.stringify(response.data, null, 2))

if (response.data && response.data.data) {
const aiMessage = response.data.data.message as string

Expand Down
2 changes: 1 addition & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { CustomAPI } from './index.d'
const api: CustomAPI = {
versions: process.versions,
send: <T = unknown>(channel: string, data?: T) => {
const validChannels = ['open-sub-window', 'ping', 'open-external']
const validChannels = ['open-sub-window', 'ping', 'open-external', 'save-sql', 'save-csv']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ export default function ChatMessage({
isUser ? 'items-end' : 'items-start'
)}
>
{(isAi || isUser) && (
{(isAi || isUser) && mainContent && (
<div
className={cn(
"w-fit max-w-md px-3 py-1.5 rounded-lg text-xs font-medium font-['Pretendard'] leading-none text-neutral-200",
isUser
? 'bg-gradient-to-b from-neutral-700 to-zinc-800 outline-1 outline-offset-[-1px] outline-white/20'
: 'bg-zinc-900', // AI 메시지 배경색 추가
: '',
// 내용과 SQL이 모두 없을 때만 최소 높이를 적용
!mainContent && !sql ? 'min-h-[20px]' : ''
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toast } from 'sonner'
import { api } from '@renderer/utils/api'
import { SchemaNode, SchemaNodeType } from './db-schema.types'
import SchemaTreeItem from './schema-tree-item'
import { ConnectionProfile } from '../../../types/database'

// API 응답 타입을 위한 인터페이스 정의
interface ApiResponse<T> {
Expand All @@ -11,12 +12,6 @@ interface ApiResponse<T> {
data: T
}

interface DbProfile {
id: string
view_name?: string
name?: string
}

interface ColumnInfo {
name: string
}
Expand Down Expand Up @@ -47,28 +42,34 @@ const initializeExpandedState = (nodes: SchemaNode[]): Record<string, boolean> =
return state
}

interface DbSchemaPanelProps {
profiles: ConnectionProfile[]
isLoading: boolean
}

/**
* @author nahyeongjin1
* @summary DB 스키마 정보를 보여주는 패널
* @returns JSX.Element
*/
export default function DbSchemaPanel(): React.JSX.Element {
export default function DbSchemaPanel({
profiles,
isLoading: isLoadingProfiles
}: DbSchemaPanelProps): React.JSX.Element {
const [schemaData, setSchemaData] = useState<SchemaNode[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isSchemaLoading, setIsSchemaLoading] = useState(true)
const [expandedNodes, setExpandedNodes] = useState<Record<string, boolean>>({})

useEffect(() => {
// profiles가 비어있거나 로딩 중이면 아무것도 하지 않음
if (isLoadingProfiles || profiles.length === 0) {
if (!isLoadingProfiles) setIsSchemaLoading(false)
return
}

const fetchSchemaData = async (): Promise<void> => {
try {
const profilesRes = (await api.get('/api/user/db/find/all')) as unknown as ApiResponse<
DbProfile[]
>
const profiles = profilesRes.data

if (!profiles || !Array.isArray(profiles)) {
throw new Error('Invalid profile data received')
}

console.log('[DbSchemaPanel] 받은 profiles로 스키마 정보 조회를 시작합니다:', profiles)
const allSchemasPromises = profiles.map((profile) =>
api.get(`/api/user/db/find/hierarchical-schema/${profile.id}`)
)
Expand All @@ -87,7 +88,7 @@ export default function DbSchemaPanel(): React.JSX.Element {
response.data.length === 0
) {
console.warn(
`Could not fetch schema for ${profile.view_name}: ${response.message || 'Empty data'}`
`[DbSchemaPanel] 스키마 조회 실패 ${profile.view_name}: ${response.message || 'Empty data'}`
)
return null
}
Expand Down Expand Up @@ -115,19 +116,19 @@ export default function DbSchemaPanel(): React.JSX.Element {
}
})
.filter(Boolean) as SchemaNode[]

console.log('[DbSchemaPanel] 스키마 정보 변환 완료:', transformedData)
setSchemaData(transformedData)
setExpandedNodes(initializeExpandedState(transformedData))
} catch (error) {
toast.error('데이터베이스 스키마 정보를 불러오는 데 실패했습니다.')
console.error(error)
console.error('[DbSchemaPanel] 스키마 정보 조회 실패:', error)
} finally {
setIsLoading(false)
setIsSchemaLoading(false)
}
}

fetchSchemaData()
}, [])
}, [profiles, isLoadingProfiles])

const handleToggle = (nodeId: string): void => {
setExpandedNodes((prev) => ({
Expand All @@ -136,7 +137,7 @@ export default function DbSchemaPanel(): React.JSX.Element {
}))
}

if (isLoading) {
if (isLoadingProfiles || isSchemaLoading) {
return (
<div className="w-56 h-full p-3 bg-neutral-800 flex items-center justify-center">
<p className="text-neutral-400 text-sm">로딩 중...</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'
import { ConnectionProfile } from '../../../types/database'
import { ChevronDown } from 'lucide-react'

interface ConnectionSelectorProps {
connections: ConnectionProfile[]
activeConnection: ConnectionProfile | null
setActiveConnection: (connection: ConnectionProfile | null) => void
}

const ConnectionSelector: React.FC<ConnectionSelectorProps> = ({
connections,
activeConnection,
setActiveConnection
}) => {
const handleSelectionChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
const selectedId = event.target.value
const selectedConnection = connections.find((c) => c.id === selectedId) || null
console.log('[ConnectionSelector] DB 연결 변경:', selectedConnection)
setActiveConnection(selectedConnection)
}

return (
<div className="relative inline-flex items-center">
<select
value={activeConnection?.id || ''}
onChange={handleSelectionChange}
className="appearance-none bg-neutral-700 border border-neutral-600 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2 pr-8"
disabled={connections.length === 0}
>
{connections.length === 0 ? (
<option>연결 없음</option>
) : (
connections.map((connection) => (
<option key={connection.id} value={connection.id}>
{connection.view_name || connection.name || connection.id}
</option>
))
)}
</select>
<ChevronDown className="absolute right-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />
</div>
)
}

export default ConnectionSelector
Loading