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
9 changes: 9 additions & 0 deletions src/main/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BrowserWindow, ipcMain, shell, dialog } from 'electron'
import fs from 'fs'
import { createSubWindow } from '../windows/subWindow'
import axios from 'axios'
import { getMainWindow } from '../windows/mainWindow'

/**
* NOTE: IPC Handler 한 번에 등록, 관리
Expand Down Expand Up @@ -35,6 +36,14 @@ export function registerIpcHandlers(mainWindow?: BrowserWindow): void {
win?.close()
})

// 연결 목록 업데이트 신호 중계
ipcMain.on('connections-updated', () => {
const mainWindow = getMainWindow()
if (mainWindow) {
mainWindow.webContents.send('connections-updated')
}
})

// 파일 저장 핸들러: SQL
ipcMain.on('save-sql', async (_event, sqlContent: string) => {
const window = BrowserWindow.getFocusedWindow()
Expand Down
46 changes: 21 additions & 25 deletions src/renderer/src/components/connection-wizard/wizard-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,35 +148,31 @@ export function ConnectionWizard(): JSX.Element {

const filteredPayload = Object.fromEntries(Object.entries(payload).filter(([, v]) => v != null))

api
.post('/api/user/db/create/profile', filteredPayload)
.then((response) => {
const id = response.data.id as string
setConnectionDetail((prev) => ({
...prev,
id: id
}))
createAnnotation(id)
})
.catch(() => {
toast.error('데이터베이스 연결 생성 중 오류가 발생했습니다.')
})
.finally(() => {
// NOTE: 페이지 새로고침 -> 저장된거 불러오도록
window.location.reload()
onClose()
})
try {
const response = await api.post('/api/user/db/create/profile', filteredPayload)
const id = response.data.id as string
await createAnnotation(id)

// 메인 프로세스를 통해 메인 윈도우에 변경 사항을 알립니다.
window.electron.ipcRenderer.send('connections-updated')
toast.success('데이터베이스 연결이 성공적으로 저장되었습니다.')
onClose()
} catch (error) {
toast.error('데이터베이스 연결 저장 중 오류가 발생했습니다.')
console.error('Failed to save connection:', error)
}
}

const createAnnotation = (db_profile_id: string): void => {
api
.post('/api/annotations/create', {
const createAnnotation = async (db_profile_id: string): Promise<void> => {
try {
await api.post('/api/annotations/create', {
db_profile_id: db_profile_id
})
.then(() => {})
.catch(() => {
toast.error('어노테이션 생성 중 오류가 발생했습니다.')
})
} catch (error) {
// 어노테이션 생성 실패는 일단 에러 토스트만 띄우고 플로우는 계속 진행시킵니다.
toast.error('어노테이션 생성 중 오류가 발생했습니다.')
console.error('Failed to create annotation:', error)
}
}

return (
Expand Down
12 changes: 8 additions & 4 deletions src/renderer/src/components/layout/side-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { cn } from '@/lib/utils'
import type { NavItem } from '../workspace/types'
import { useLocation, useNavigate } from 'react-router-dom'

interface SidebarProps {
hasConnections: boolean
}

const bottomNavItems: NavItem[] = [
{
id: 'settings',
Expand Down Expand Up @@ -40,7 +44,7 @@ function NavButton({ item }: { item: NavItem }): React.JSX.Element {
'size-10 rounded-lg',
item.active && 'bg-neutral-700 outline-1 outline-offset-[-1px] outline-white/20',
!item.active && 'hover:bg-neutral-700/50',
item.disabled && 'cursor-not-allowed'
item.disabled && 'cursor-not-allowed opacity-50'
)}
onClick={item.onClick}
disabled={item.disabled}
Expand All @@ -57,7 +61,7 @@ function NavButton({ item }: { item: NavItem }): React.JSX.Element {
)
}

export function Sidebar(): React.JSX.Element {
export function Sidebar({ hasConnections }: SidebarProps): React.JSX.Element {
const navigate = useNavigate()
const location = useLocation()

Expand All @@ -72,9 +76,9 @@ export function Sidebar(): React.JSX.Element {
id: 'tags',
icon: Tag,
active: location.pathname === '/erd',
disabled: false,
disabled: !hasConnections,
onClick: (): void | Promise<void> => navigate('/erd')
} // TODO: DB 연결 후에 disabled: false
}
]

return (
Expand Down
43 changes: 39 additions & 4 deletions src/renderer/src/components/workspace/main-page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
import { useEffect, useState, useCallback } from 'react'
import { Sidebar } from '../layout/side-bar'
import WorkSpace from './workspace'
import { WorkspaceEmptyState } from './workspace-empty-state'
import { ConnectionProfile } from '@renderer/types/database'
import { ApiResponse } from '@renderer/types'
import { api } from '@renderer/utils/api'
import { toast } from 'sonner'

export function MainPage(): React.JSX.Element {
// TODO: DB 연결 상태에 따라 WorkspaceEmptyState 또는 WorkSpace를 렌더링해야 함
const isConnected = true // 이 값은 실제 DB 연결 상태에 따라 동적으로 변경되어야 함
const [connections, setConnections] = useState<ConnectionProfile[]>([])

const checkConnections = useCallback(async (): Promise<void> => {
try {
const res = await api.get<ApiResponse<ConnectionProfile[]>>('/api/user/db/find/all')
if (res && Array.isArray(res.data)) {
setConnections(res.data)
} else {
throw new Error('Invalid API response format')
}
} catch (error) {
toast.error('DB 연결 목록을 불러오는 데 실패했습니다.')
console.error('[MainPage] DB 연결 목록 조회 실패:', error)
}
}, [])

useEffect(() => {
checkConnections()

// connections-updated 신호를 수신하여 연결 목록을 다시 불러옵니다.
const unsubscribe = window.electron.ipcRenderer.on('connections-updated', () => {
console.log('[MainPage] Received connections-updated signal. Refetching connections...')
checkConnections()
})

// 컴포넌트가 언마운트될 때 리스너를 정리합니다.
return () => {
if (typeof unsubscribe === 'function') {
unsubscribe()
}
}
}, [checkConnections])

return (
<div className="w-screen h-screen bg-zinc-900 flex overflow-hidden">
<Sidebar />
{isConnected ? <WorkSpace /> : <WorkspaceEmptyState />}
<Sidebar hasConnections={connections.length > 0} />
{connections.length > 0 ? <WorkSpace connections={connections} /> : <WorkspaceEmptyState />}
</div>
)
}
54 changes: 18 additions & 36 deletions src/renderer/src/components/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { QueryPanel, type QueryResultData, type ActiveTab } from './query-panel'
import { api } from '@renderer/utils/api'
import { toast } from 'sonner'
import { ConnectionProfile } from '@renderer/types/database'
import { ApiResponse } from '@renderer/types'

// API 응답 타입을 Workspace 레벨에서 정의합니다.
interface QueryApiResponse {
Expand All @@ -15,13 +14,13 @@ interface QueryApiResponse {
}
}

const WorkSpace = (): React.JSX.Element => {
// --- 기존 상태 ---
const [connections, setConnections] = useState<ConnectionProfile[]>([])
const [activeConnection, setActiveConnection] = useState<ConnectionProfile | null>(null)
const [isConnectionLoading, setIsConnectionLoading] = useState(true)
interface WorkSpaceProps {
connections: ConnectionProfile[]
}

// --- QueryPanel로부터 이동된 상태 ---
const WorkSpace = ({ connections }: WorkSpaceProps): React.JSX.Element => {
// --- 상태 관리 ---
const [activeConnection, setActiveConnection] = useState<ConnectionProfile | null>(null)
const [query, setQuery] = useState(
'SELECT p.ProductName, SUM(sod.sales_quantity) as total_quantity_sold, SUM(sod.sales_quantity * sod.UnitPrice) as total_revenue FROM Products p JOIN SalesOrderDetails sod ON p.ProductID = sod.ProductID GROUP BY p.ProductID, p.ProductName ORDER BY total_revenue DESC LIMIT 5;'
)
Expand All @@ -30,30 +29,18 @@ const WorkSpace = (): React.JSX.Element => {
const [queryError, setQueryError] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<ActiveTab>('editor')

// MainPage로부터 받은 connections prop이 변경될 때 activeConnection을 설정합니다.
useEffect(() => {
const fetchConnections = async (): Promise<void> => {
try {
const res = await api.get<ApiResponse<ConnectionProfile[]>>('/api/user/db/find/all')
if (res && Array.isArray(res.data)) {
const fetchedConnections = res.data
setConnections(fetchedConnections)
if (fetchedConnections.length > 0) {
setActiveConnection(fetchedConnections[0])
}
} else {
throw new Error('Invalid API response format')
}
} catch (error) {
toast.error('DB 연결 목록을 불러오는 데 실패했습니다.')
console.error('[Workspace] DB 연결 목록 조회 실패:', error)
} finally {
setIsConnectionLoading(false)
}
const isActiveConnectionValid = connections.some((c) => c.id === activeConnection?.id)

if (!isActiveConnectionValid && connections.length > 0) {
setActiveConnection(connections[0])
} else if (connections.length === 0) {
setActiveConnection(null)
}
fetchConnections()
}, [])
}, [connections, activeConnection])

// --- QueryPanel로부터 이동된 쿼리 실행 함수 ---
// --- 핸들러 ---
const handleExecuteQuery = async (
queryToExecute: string,
chatMessageId?: string
Expand Down Expand Up @@ -92,36 +79,31 @@ const WorkSpace = (): React.JSX.Element => {
}
}

// --- AI 채팅 패널에서 호출할 함수 ---
const handleAiQueryExecute = (sql: string, chatMessageId: string): void => {
setQuery(sql) // 쿼리 편집기 내용 업데이트
handleExecuteQuery(sql, chatMessageId) // 쿼리 실행
setQuery(sql)
handleExecuteQuery(sql, chatMessageId)
}

return (
<main className="flex flex-1 h-full bg-zinc-900 pt-2 pr-2 pb-2">
<DbSchemaPanel profiles={connections} isLoading={isConnectionLoading} />
<DbSchemaPanel profiles={connections} isLoading={false} />
<div className="grid grid-cols-2 flex-1 h-full ml-2 gap-2">
<div className="min-w-0 flex">
<AiChatPanel onExecuteQuery={handleAiQueryExecute} />
</div>
<div className="min-w-0 flex">
<QueryPanel
// Connection state
connections={connections}
activeConnection={activeConnection}
setActiveConnection={setActiveConnection}
// Query state
query={query}
setQuery={setQuery}
result={result}
isLoading={isQueryLoading}
error={queryError}
setError={setQueryError}
// Tab state
activeTab={activeTab}
setActiveTab={setActiveTab}
// Execution handler
onExecuteQuery={() => handleExecuteQuery(query)}
/>
</div>
Expand Down