diff --git a/.cursorrules b/.cursorrules
index 7edf7fb8..d5af3e40 100644
--- a/.cursorrules
+++ b/.cursorrules
@@ -1,5 +1,5 @@
# Cursor AI Rules for Cheolsu Proxy Project
-당신은 TypeScript, React19, Shadcn UI, Radix UI, Tailwind, Rust, Tauri, MITM(Man In the Middle attack) Proxy, FSD(Feature-Sliced Design), zustand 전문가입니다.
+당신은 TypeScript, React19, Shadcn UI, Radix UI, Tailwind, Rust, Tauri, MITM(Man In The Middle attack) Proxy, FSD(Feature-Sliced Design), zustand 전문가입니다.
## 프로젝트 개요
- Cheolsu Proxy Rust 기반의 프록시 도구로, Tauri를 사용한 데스크톱 애플리케이션입니다.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c41d5fe1..01923777 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -119,7 +119,7 @@ importers:
specifier: ^1.3.8
version: 1.4.0
zod:
- specifier: ^4.1.8
+ specifier: ^4.1.12
version: 4.1.12
zustand:
specifier: ^5.0.8
diff --git a/tauri-ui/package.json b/tauri-ui/package.json
index 11bff741..fea9fce4 100644
--- a/tauri-ui/package.json
+++ b/tauri-ui/package.json
@@ -45,7 +45,7 @@
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.12",
"tw-animate-css": "^1.3.8",
- "zod": "^4.1.8",
+ "zod": "^4.1.12",
"zustand": "^5.0.8"
},
"devDependencies": {
diff --git a/tauri-ui/pnpm-lock.yaml b/tauri-ui/pnpm-lock.yaml
index 01409bdd..378ff745 100644
--- a/tauri-ui/pnpm-lock.yaml
+++ b/tauri-ui/pnpm-lock.yaml
@@ -111,8 +111,8 @@ importers:
specifier: ^1.3.8
version: 1.3.8
zod:
- specifier: ^4.1.8
- version: 4.1.9
+ specifier: ^4.1.12
+ version: 4.1.12
zustand:
specifier: ^5.0.8
version: 5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1))
@@ -2384,8 +2384,8 @@ packages:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
- zod@4.1.9:
- resolution: {integrity: sha512-HI32jTq0AUAC125z30E8bQNz0RQ+9Uc+4J7V97gLYjZVKRjeydPgGt6dvQzFrav7MYOUGFqqOGiHpA/fdbd0cQ==}
+ zod@4.1.12:
+ resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
zustand@5.0.8:
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
@@ -4559,7 +4559,7 @@ snapshots:
yn@3.1.1: {}
- zod@4.1.9: {}
+ zod@4.1.12: {}
zustand@5.0.8(@types/react@19.1.12)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)):
optionalDependencies:
diff --git a/tauri-ui/src-tauri/Cargo.toml b/tauri-ui/src-tauri/Cargo.toml
index 9e369e98..d1e2739f 100644
--- a/tauri-ui/src-tauri/Cargo.toml
+++ b/tauri-ui/src-tauri/Cargo.toml
@@ -16,9 +16,6 @@ edition = "2021"
name = "cheolsu_proxy"
crate-type = ["staticlib", "cdylib", "rlib"]
-# 빌드 스크립트 추가
-build = "build.rs"
-
[build-dependencies]
tauri-build = { version = "2", features = [] }
diff --git a/tauri-ui/src/app/App.tsx b/tauri-ui/src/app/App.tsx
index 4a2e6758..3500fe90 100644
--- a/tauri-ui/src/app/App.tsx
+++ b/tauri-ui/src/app/App.tsx
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
-import { useThemeProvider, RouterProvider } from './providers';
+import { useThemeProvider, RouterProvider, ProxyEventProvider } from './providers';
import { Toaster } from '@/shared/ui';
import { useProxyStore } from '@/shared/stores';
@@ -14,8 +14,10 @@ const App: React.FC = () => {
return (
);
};
diff --git a/tauri-ui/src/app/providers/index.ts b/tauri-ui/src/app/providers/index.ts
index f1e94c9e..67270dbc 100644
--- a/tauri-ui/src/app/providers/index.ts
+++ b/tauri-ui/src/app/providers/index.ts
@@ -1,2 +1,3 @@
export * from './use-theme-provider';
export * from './router-provider';
+export * from './proxy-event-provider';
diff --git a/tauri-ui/src/app/providers/proxy-event-provider.tsx b/tauri-ui/src/app/providers/proxy-event-provider.tsx
new file mode 100644
index 00000000..832bd2b2
--- /dev/null
+++ b/tauri-ui/src/app/providers/proxy-event-provider.tsx
@@ -0,0 +1,34 @@
+import { useEffect } from 'react';
+
+import { listen } from '@tauri-apps/api/event';
+
+import type { ProxyEventTuple, HttpTransaction } from '@/entities/proxy';
+import { useTransactionStore } from '@/shared/stores';
+
+/**
+ * 전역 Proxy 이벤트 리스너 Provider
+ * 앱 전체에서 proxy_event를 수신하여 transaction store에 저장합니다.
+ * 탭 이동과 무관하게 이벤트를 계속 수신할 수 있습니다.
+ */
+export const ProxyEventProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const { addTransaction, isPaused } = useTransactionStore();
+
+ useEffect(() => {
+ const unlisten = listen('proxy_event', (event) => {
+ // isPaused 상태와 관계없이 이벤트는 수신하되, store에 추가할지만 결정
+ if (isPaused) return;
+
+ const [request, response] = event.payload;
+ const transaction: HttpTransaction = { request, response };
+
+ // zustand store에 transaction 추가
+ addTransaction(transaction);
+ });
+
+ return () => {
+ unlisten.then((f) => f());
+ };
+ }, [addTransaction, isPaused]);
+
+ return <>{children}>;
+};
diff --git a/tauri-ui/src/pages/network-dashboard/hooks/index.ts b/tauri-ui/src/pages/network-dashboard/hooks/index.ts
index 98a7a200..d8ac55e2 100644
--- a/tauri-ui/src/pages/network-dashboard/hooks/index.ts
+++ b/tauri-ui/src/pages/network-dashboard/hooks/index.ts
@@ -1,3 +1,2 @@
export * from './use-transaction-filters';
-export * from './use-proxy-event-control';
export * from './use-transactions';
diff --git a/tauri-ui/src/pages/network-dashboard/hooks/use-proxy-event-control.ts b/tauri-ui/src/pages/network-dashboard/hooks/use-proxy-event-control.ts
deleted file mode 100644
index 12ed0b8e..00000000
--- a/tauri-ui/src/pages/network-dashboard/hooks/use-proxy-event-control.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useState, useEffect, useCallback } from 'react';
-
-import { listen } from '@tauri-apps/api/event';
-
-import type { ProxyEventTuple, HttpTransaction } from '@/entities/proxy';
-
-interface UseProxyEventControlProps {
- onTransactionReceived: (transaction: HttpTransaction) => void;
- initialPaused?: boolean;
-}
-
-export const useProxyEventControl = ({ onTransactionReceived, initialPaused = false }: UseProxyEventControlProps) => {
- const [paused, setPaused] = useState(initialPaused);
-
- const togglePause = useCallback(() => setPaused((prev) => !prev), []);
- const pause = useCallback(() => setPaused(true), []);
- const resume = useCallback(() => setPaused(false), []);
-
- useEffect(() => {
- if (paused) return;
-
- const unlisten = listen('proxy_event', (event) => {
- const [request, response] = event.payload;
- onTransactionReceived({ request, response });
- });
-
- return () => {
- unlisten.then((f) => f());
- };
- }, [paused]);
-
- return { paused, togglePause, pause, resume };
-};
diff --git a/tauri-ui/src/pages/network-dashboard/hooks/use-transactions.ts b/tauri-ui/src/pages/network-dashboard/hooks/use-transactions.ts
index 97c2c5f1..17acc2b1 100644
--- a/tauri-ui/src/pages/network-dashboard/hooks/use-transactions.ts
+++ b/tauri-ui/src/pages/network-dashboard/hooks/use-transactions.ts
@@ -1,48 +1,70 @@
-import { useState, useCallback } from 'react';
+import { useCallback, useEffect } from 'react';
import type { HttpTransaction } from '@/entities/proxy';
+import { useTransactionStore } from '@/shared/stores';
-export const useTransactions = () => {
- const [transactions, setTransactions] = useState([]);
- const [selectedTransaction, setSelectedTransaction] = useState(null);
+interface UseTransactionsProps {
+ initialPaused?: boolean;
+}
- const addTransaction = useCallback((transaction: HttpTransaction) => {
- setTransactions(prev => {
- const existingTransaction = prev.find(t => t.request?.time === transaction.request?.time);
-
- if (existingTransaction) {
- return prev;
- }
-
- return [...prev, transaction];
- });
- }, []);
-
- const clearTransactions = useCallback(() => {
- setTransactions([]);
- setSelectedTransaction(null)
- }, []);
-
- const deleteTransaction = useCallback((id: number) => {
- setTransactions(prev => prev.filter((transaction) => transaction?.request?.time !== id));
- }, []);
+/**
+ * Transaction 관련 모든 기능을 관리하는 통합 Hook
+ * - Transaction 데이터 관리
+ * - Transaction 선택 관리
+ * - Proxy 이벤트 제어 (pause/resume)
+ */
+export const useTransactions = ({ initialPaused = false }: UseTransactionsProps = {}) => {
+ const {
+ transactions,
+ selectedTransaction,
+ isPaused,
+ addTransaction,
+ clearTransactions,
+ deleteTransaction,
+ setSelectedTransaction,
+ setPaused,
+ togglePause,
+ } = useTransactionStore();
+ // 초기 paused 상태 설정
+ useEffect(() => {
+ if (initialPaused !== undefined) {
+ setPaused(initialPaused);
+ }
+ }, [initialPaused, setPaused]);
- const createTransactionSelectHandler = useCallback((transaction: HttpTransaction) => () => {
- setSelectedTransaction(transaction);
- }, []);
+ const createTransactionSelectHandler = useCallback(
+ (transaction: HttpTransaction) => () => {
+ setSelectedTransaction(transaction);
+ },
+ [setSelectedTransaction],
+ );
const clearSelectedTransaction = useCallback(() => {
- setSelectedTransaction(null)
- }, [])
+ setSelectedTransaction(null);
+ }, [setSelectedTransaction]);
+
+ const pause = useCallback(() => setPaused(true), [setPaused]);
+ const resume = useCallback(() => setPaused(false), [setPaused]);
return {
+ // Transaction 데이터
transactions,
+ selectedTransaction,
addTransaction,
clearTransactions,
deleteTransaction,
- selectedTransaction,
+
+ // Transaction 선택 관리
createTransactionSelectHandler,
- clearSelectedTransaction
+ clearSelectedTransaction,
+
+ // Proxy 이벤트 제어
+ isPaused,
+ paused: isPaused, // 하위 호환성을 위해 별칭 제공
+ setPaused,
+ togglePause,
+ pause,
+ resume,
};
};
diff --git a/tauri-ui/src/pages/network-dashboard/ui/network-dashboard.tsx b/tauri-ui/src/pages/network-dashboard/ui/network-dashboard.tsx
index b8898f10..564992cf 100644
--- a/tauri-ui/src/pages/network-dashboard/ui/network-dashboard.tsx
+++ b/tauri-ui/src/pages/network-dashboard/ui/network-dashboard.tsx
@@ -8,7 +8,7 @@ import { NetworkTable } from '@/widgets/network-table';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/shared/ui';
-import { useProxyEventControl, useTransactionFilters, useTransactions } from '../hooks';
+import { useTransactionFilters, useTransactions } from '../hooks';
import { useProxyStore } from '@/shared/stores';
import { HostPathTree } from '@/widgets/host-path-tree/ui/host-path-tree';
@@ -17,16 +17,15 @@ export const NetworkDashboard = () => {
const {
transactions,
- addTransaction,
clearTransactions,
deleteTransaction,
selectedTransaction,
createTransactionSelectHandler,
clearSelectedTransaction,
+ paused,
+ togglePause,
} = useTransactions();
- const { paused, togglePause } = useProxyEventControl({ onTransactionReceived: addTransaction });
-
const {
searchQuery,
setMethodFilter,
@@ -38,14 +37,14 @@ export const NetworkDashboard = () => {
} = useTransactionFilters({ transactions });
const createTransactionDeleteHandler = useCallback(
- (id: number) => () => {
+ (id: string) => () => {
deleteTransaction(id);
- if (selectedTransaction?.request?.time === id) {
+ if (selectedTransaction?.request?.id === id) {
clearSelectedTransaction();
}
},
- [],
+ [deleteTransaction, selectedTransaction, clearSelectedTransaction],
);
return (
diff --git a/tauri-ui/src/pages/sessions/context/session-form-context.tsx b/tauri-ui/src/pages/sessions/context/session-form-context.tsx
new file mode 100644
index 00000000..6617ed55
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/context/session-form-context.tsx
@@ -0,0 +1,25 @@
+import { createContext, useContext } from 'react';
+import { useForm } from '@tanstack/react-form';
+import { zodValidator } from '@tanstack/zod-form-adapter';
+
+import type { SessionEditFormData } from '../lib/session-edit-schema';
+
+export interface SessionFormInstance {
+ getFieldValue: (name: string) => any;
+ setFieldValue: (name: string, value: any) => void;
+ validate: () => Promise;
+ reset: () => void;
+ getValues: () => SessionEditFormData;
+}
+
+const SessionFormContext = createContext(null);
+
+export const useSessionForm = () => {
+ const context = useContext(SessionFormContext);
+ if (!context) {
+ throw new Error('useSessionForm must be used within a SessionFormProvider');
+ }
+ return context;
+};
+
+export { SessionFormContext };
diff --git a/tauri-ui/src/pages/sessions/hooks/index.ts b/tauri-ui/src/pages/sessions/hooks/index.ts
new file mode 100644
index 00000000..12e6f5d3
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/hooks/index.ts
@@ -0,0 +1 @@
+export * from './use-session-edit';
diff --git a/tauri-ui/src/pages/sessions/hooks/use-session-edit.ts b/tauri-ui/src/pages/sessions/hooks/use-session-edit.ts
new file mode 100644
index 00000000..0f79fef1
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/hooks/use-session-edit.ts
@@ -0,0 +1,122 @@
+import { useState, useCallback, useRef } from 'react';
+import { useForm } from '@tanstack/react-form';
+
+import type { SessionStore } from '@/entities/session';
+import { useSessionStore } from '@/shared/stores';
+import type { SessionEditFormData } from '../lib/session-edit-schema';
+
+interface UseSessionEditProps {
+ session: SessionStore;
+}
+
+// 객체 비교를 위한 헬퍼 함수
+const isEqual = (a: any, b: any): boolean => {
+ // undefined와 null을 같은 것으로 처리
+ if (a === undefined || a === null) a = undefined;
+ if (b === undefined || b === null) b = undefined;
+
+ if (a === undefined && b === undefined) return true;
+ if (a === undefined || b === undefined) return false;
+
+ if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
+ return JSON.stringify(a) === JSON.stringify(b);
+ }
+ return a === b;
+};
+
+/**
+ * 세션 편집을 위한 Hook
+ * 폼 기반으로 세션 데이터를 편집하고 저장할 수 있습니다.
+ */
+export const useSessionEdit = ({ session }: UseSessionEditProps) => {
+ const { updateSession } = useSessionStore();
+ const [isEditing, setIsEditing] = useState(false);
+ const originalDataRef = useRef(null);
+
+ // 폼 초기값 설정
+ const getInitialValues = (): SessionEditFormData => {
+ return {
+ id: session.id,
+ url: session.url,
+ method: session.method,
+ isActive: session.isActive,
+ request: session.request,
+ response: session.response,
+ };
+ };
+
+ const form = useForm({
+ defaultValues: getInitialValues(),
+ onSubmit: async ({ value }) => {
+ console.log('Form submitted with value:', value);
+ // 현재 폼 데이터와 원본 데이터를 비교해서 변경된 필드만 추출
+ const originalData = originalDataRef.current;
+ console.log('Original data:', originalData);
+ if (!originalData) return;
+
+ // 변경된 필드만 추출
+ const changedFields: Partial = {};
+
+ if (!isEqual(value.request, originalData.request)) {
+ changedFields.request = value.request;
+ }
+
+ if (!isEqual(value.response, originalData.response)) {
+ changedFields.response = value.response;
+ }
+
+ if (!isEqual(value.url, originalData.url)) {
+ changedFields.url = value.url;
+ }
+
+ if (!isEqual(value.method, originalData.method)) {
+ changedFields.method = value.method;
+ }
+
+ if (!isEqual(value.isActive, originalData.isActive)) {
+ changedFields.isActive = value.isActive;
+ }
+
+ // 변경된 필드가 있는 경우에만 저장
+ if (Object.keys(changedFields).length > 0) {
+ const updatedSession = { ...session, ...changedFields };
+ console.log('Updating session:', updatedSession);
+ console.log('Changed fields:', changedFields);
+ updateSession(updatedSession as any);
+ setIsEditing(false);
+ } else {
+ console.log('No changes detected');
+ setIsEditing(false);
+ }
+ },
+ });
+
+ const startEditing = useCallback(() => {
+ const initialValues = getInitialValues();
+ originalDataRef.current = initialValues;
+ form.setFieldValue('id', initialValues.id);
+ form.setFieldValue('url', initialValues.url);
+ form.setFieldValue('method', initialValues.method);
+ form.setFieldValue('isActive', initialValues.isActive);
+ form.setFieldValue('request', initialValues.request);
+ form.setFieldValue('response', initialValues.response);
+ setIsEditing(true);
+ }, [form]);
+
+ const cancelEditing = useCallback(() => {
+ setIsEditing(false);
+ originalDataRef.current = null;
+ }, []);
+
+ const saveChanges = useCallback(() => {
+ form.handleSubmit();
+ }, [form]);
+
+ return {
+ isEditing,
+ form,
+ startEditing,
+ cancelEditing,
+ saveChanges,
+ };
+};
diff --git a/tauri-ui/src/pages/sessions/lib/session-edit-schema.ts b/tauri-ui/src/pages/sessions/lib/session-edit-schema.ts
new file mode 100644
index 00000000..45f8f46d
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/lib/session-edit-schema.ts
@@ -0,0 +1,25 @@
+import { z } from 'zod';
+
+// 세션 편집을 위한 스키마
+export const sessionEditSchema = z.object({
+ id: z.string().min(1, 'ID is required'),
+ url: z.string().url('Valid URL is required'),
+ method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'CONNECT', 'TRACE', 'OTHERS']),
+ isActive: z.boolean(),
+ request: z
+ .object({
+ headers: z.record(z.string(), z.string()).optional(),
+ data: z.union([z.record(z.string(), z.any()), z.string()]).optional(),
+ params: z.union([z.record(z.string(), z.any()), z.string()]).optional(),
+ })
+ .optional(),
+ response: z
+ .object({
+ status: z.number().min(100).max(599),
+ headers: z.record(z.string(), z.string()).optional(),
+ data: z.union([z.record(z.string(), z.any()), z.string()]).optional(),
+ })
+ .optional(),
+});
+
+export type SessionEditFormData = z.infer;
diff --git a/tauri-ui/src/pages/sessions/ui/session-editor.tsx b/tauri-ui/src/pages/sessions/ui/session-editor.tsx
new file mode 100644
index 00000000..7dd9adee
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/ui/session-editor.tsx
@@ -0,0 +1,277 @@
+import { useState, useEffect } from 'react';
+import Editor from '@monaco-editor/react';
+
+import type { SessionStore } from '@/entities/session';
+import { Button, Input, Badge, Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui';
+import { useSessionEdit } from '../hooks/use-session-edit';
+import { formatValueToJsonString, handleEditorChange } from '../utils';
+import { toast } from 'sonner';
+import { Save, X } from 'lucide-react';
+
+interface SessionEditorProps {
+ session: SessionStore;
+ isEditing: boolean;
+ onSave: () => void;
+ onCancel: () => void;
+}
+
+/**
+ * 세션 편집을 위한 폼 기반 편집 컴포넌트
+ */
+export const SessionEditor = ({ session, isEditing, onSave, onCancel }: SessionEditorProps) => {
+ const { form, saveChanges, startEditing } = useSessionEdit({ session });
+ const [isSaving, setIsSaving] = useState(false);
+
+ // 편집 모드가 활성화될 때 startEditing 호출
+ useEffect(() => {
+ if (isEditing) {
+ startEditing();
+ }
+ }, [isEditing, startEditing]);
+
+ const handleSave = async () => {
+ try {
+ setIsSaving(true);
+ await saveChanges();
+ toast.success('Session updated successfully');
+ onSave();
+ } catch (error) {
+ toast.error('Failed to save session');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
{isEditing ? 'Edit Session' : 'Session Details'}
+
+ {isEditing && (
+
+
+
+
+ )}
+
+
+
+
+ {/* Basic Info */}
+
+
+
+
+ {(field) => (
+
+ {field.state.value}
+
+ )}
+
+
+
+
+
+
+ {(field) => (
+
+
+ {field.state.value}
+
+
+ )}
+
+
+
+
+
+
+ {(field) => (
+
+ isEditing && field.handleChange(!field.state.value)}
+ >
+ {field.state.value ? 'Active' : 'Inactive'}
+
+
+ )}
+
+
+
+
+ {/* Response Status */}
+
+
+
+
+ {(field) => (
+ field.handleChange(Number.parseInt(e.target.value) || 200)}
+ placeholder="200"
+ disabled={!isEditing}
+ className={`mt-1 ${isEditing ? '' : 'bg-muted/50 cursor-not-allowed'}`}
+ />
+ )}
+
+
+
+
+
+ {/* Request/Response Tabs */}
+
+
+ Request Headers
+ Request Data
+ Response Headers
+ Response Data
+
+
+
+
+
+
+ {(field) => (
+
+ handleEditorChange(value, isEditing, field.handleChange)}
+ options={{
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 12,
+ lineNumbers: 'on',
+ roundedSelection: false,
+ scrollbar: {
+ vertical: 'auto',
+ horizontal: 'auto',
+ },
+ automaticLayout: true,
+ formatOnPaste: isEditing,
+ formatOnType: isEditing,
+ readOnly: !isEditing,
+ }}
+ theme="vs"
+ />
+
+ )}
+
+
+
+
+
+
+
+
+ {(field) => (
+
+ handleEditorChange(value, isEditing, field.handleChange)}
+ options={{
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 12,
+ lineNumbers: 'on',
+ roundedSelection: false,
+ scrollbar: {
+ vertical: 'auto',
+ horizontal: 'auto',
+ },
+ automaticLayout: true,
+ formatOnPaste: isEditing,
+ formatOnType: isEditing,
+ readOnly: !isEditing,
+ }}
+ theme="vs"
+ />
+
+ )}
+
+
+
+
+
+
+
+
+ {(field) => (
+
+ handleEditorChange(value, isEditing, field.handleChange)}
+ options={{
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 12,
+ lineNumbers: 'on',
+ roundedSelection: false,
+ scrollbar: {
+ vertical: 'auto',
+ horizontal: 'auto',
+ },
+ automaticLayout: true,
+ formatOnPaste: isEditing,
+ formatOnType: isEditing,
+ readOnly: !isEditing,
+ }}
+ theme="vs"
+ />
+
+ )}
+
+
+
+
+
+
+
+
+ {(field) => (
+
+ handleEditorChange(value, isEditing, field.handleChange)}
+ options={{
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 12,
+ lineNumbers: 'on',
+ roundedSelection: false,
+ scrollbar: {
+ vertical: 'auto',
+ horizontal: 'auto',
+ },
+ automaticLayout: true,
+ formatOnPaste: isEditing,
+ formatOnType: isEditing,
+ readOnly: !isEditing,
+ }}
+ theme="vs"
+ />
+
+ )}
+
+
+
+
+
+
+ );
+};
diff --git a/tauri-ui/src/pages/sessions/ui/sessions-page.tsx b/tauri-ui/src/pages/sessions/ui/sessions-page.tsx
index b6f44efb..7b79a84a 100644
--- a/tauri-ui/src/pages/sessions/ui/sessions-page.tsx
+++ b/tauri-ui/src/pages/sessions/ui/sessions-page.tsx
@@ -1,10 +1,13 @@
+import { useState } from 'react';
+
import { useSessionStore, useProxyStore } from '@/shared/stores';
-import { Card, CardContent, CardHeader, CardTitle } from '@/shared/ui';
+import { Card, CardContent, CardHeader } from '@/shared/ui';
import { Badge } from '@/shared/ui';
-import { Trash2, Copy, ExternalLink } from 'lucide-react';
+import { Trash2, Edit } from 'lucide-react';
import { Button } from '@/shared/ui';
import { toast } from 'sonner';
import { AppSidebar } from '@/shared/app-sidebar';
+import { SessionEditor } from './session-editor';
/**
* 세션 데이터를 표시하는 페이지
@@ -14,20 +17,23 @@ import { AppSidebar } from '@/shared/app-sidebar';
export const SessionsPage = () => {
const { isConnected } = useProxyStore();
const { sessions, deleteSession } = useSessionStore();
+ const [editingSessionId, setEditingSessionId] = useState(null);
const handleDeleteSession = (id: string) => {
deleteSession(id);
toast.success('Session deleted successfully');
};
- const handleCopySession = (session: any) => {
- const sessionText = JSON.stringify(session, null, 2);
- navigator.clipboard.writeText(sessionText);
- toast.success('Session data copied to clipboard');
+ const handleEditSession = (sessionId: string) => {
+ setEditingSessionId(sessionId);
+ };
+
+ const handleSaveSession = () => {
+ setEditingSessionId(null);
};
- const handleOpenUrl = (url: string) => {
- window.open(url, '_blank');
+ const handleCancelEdit = () => {
+ setEditingSessionId(null);
};
return (
@@ -35,17 +41,17 @@ export const SessionsPage = () => {
-
-
-
-
Saved Sessions
-
Manage and view your saved HTTP sessions
-
-
+ {/* Header similar to NetworkHeader */}
+
+
+
Saved Sessions
+
{sessions.length} sessions
+
+
{sessions.length === 0 ? (
@@ -62,30 +68,27 @@ export const SessionsPage = () => {
-
{session.url}
+
+
+ {session.method}
+
+
+ {session.url}
+
+
{session.isActive ? 'Active' : 'Inactive'}
-
- {session.method}
-
-
-
- {/* Request Section */}
-
-
Request
-
- {session.request?.headers && (
-
-
Headers:
-
- {JSON.stringify(session.request.headers, null, 2)}
-
-
- )}
- {session.request?.data && (
-
-
Data:
-
- {JSON.stringify(session.request.data, null, 2)}
-
-
- )}
- {session.request?.params && (
-
-
Params:
-
- {JSON.stringify(session.request.params, null, 2)}
-
-
- )}
-
-
-
- {/* Response Section */}
-
-
Response
-
- {session.response?.status && (
-
- Status:
-
- {session.response.status}
-
-
- )}
- {session.response?.headers && (
-
-
Headers:
-
- {JSON.stringify(session.response.headers, null, 2)}
-
-
- )}
- {session.response?.data && (
-
-
Data:
-
- {JSON.stringify(session.response.data, null, 2)}
-
-
- )}
-
-
-
+
))}
diff --git a/tauri-ui/src/pages/sessions/utils/index.ts b/tauri-ui/src/pages/sessions/utils/index.ts
new file mode 100644
index 00000000..ddb6a86b
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/utils/index.ts
@@ -0,0 +1 @@
+export { formatValueToJsonString, handleEditorChange } from './json-editor-utils';
diff --git a/tauri-ui/src/pages/sessions/utils/json-editor-utils.ts b/tauri-ui/src/pages/sessions/utils/json-editor-utils.ts
new file mode 100644
index 00000000..7a637060
--- /dev/null
+++ b/tauri-ui/src/pages/sessions/utils/json-editor-utils.ts
@@ -0,0 +1,58 @@
+/**
+ * JSON 에디터를 위한 유틸리티 함수들
+ */
+
+/**
+ * 값을 JSON 문자열로 포맷팅합니다
+ * @param value - 포맷팅할 값
+ * @returns 포맷팅된 JSON 문자열
+ */
+export const formatValueToJsonString = (value: unknown): string => {
+ if (!value) return '';
+
+ // 이미 문자열인 경우 파싱 시도
+ if (typeof value === 'string') {
+ try {
+ const parsed = JSON.parse(value);
+ return JSON.stringify(parsed, null, 2);
+ } catch {
+ return value;
+ }
+ }
+
+ // 객체인 경우 그대로 문자열화
+ return JSON.stringify(value, null, 2);
+};
+
+/**
+ * 에디터 값 변경을 처리합니다
+ * @param value - 에디터에서 입력된 값
+ * @param isEditing - 편집 모드 여부
+ * @param handleChange - 값 변경 핸들러
+ */
+export const handleEditorChange =
(
+ value: string | undefined,
+ isEditing: boolean,
+ handleChange: (value: T) => void,
+): void => {
+ if (!isEditing || value === undefined) return;
+
+ // 빈 텍스트인 경우 undefined로 저장
+ if (value.trim() === '') {
+ handleChange(undefined as T);
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(value);
+ // 빈 객체인 경우 undefined로 저장
+ if (parsed && typeof parsed === 'object' && Object.keys(parsed).length === 0) {
+ handleChange(undefined as T);
+ } else {
+ handleChange(parsed as T);
+ }
+ } catch {
+ // Invalid JSON, save as string
+ handleChange(value as T);
+ }
+};
diff --git a/tauri-ui/src/shared/stores/index.ts b/tauri-ui/src/shared/stores/index.ts
index 42c965b9..33b41e2d 100644
--- a/tauri-ui/src/shared/stores/index.ts
+++ b/tauri-ui/src/shared/stores/index.ts
@@ -1,2 +1,3 @@
export * from './session-store';
export * from './proxy-store';
+export * from './transaction-store';
diff --git a/tauri-ui/src/shared/stores/transaction-store.ts b/tauri-ui/src/shared/stores/transaction-store.ts
new file mode 100644
index 00000000..83367f87
--- /dev/null
+++ b/tauri-ui/src/shared/stores/transaction-store.ts
@@ -0,0 +1,78 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+import type { HttpTransaction } from '@/entities/proxy';
+
+interface TransactionState {
+ transactions: HttpTransaction[];
+ selectedTransaction: HttpTransaction | null;
+ isPaused: boolean;
+ addTransaction: (transaction: HttpTransaction) => void;
+ setSelectedTransaction: (transaction: HttpTransaction | null) => void;
+ clearTransactions: () => void;
+ deleteTransaction: (id: string) => void;
+ setPaused: (paused: boolean) => void;
+ togglePause: () => void;
+}
+
+export const useTransactionStore = create()(
+ persist(
+ (set, get) => ({
+ transactions: [],
+ selectedTransaction: null,
+ isPaused: false,
+
+ addTransaction: (transaction: HttpTransaction) => {
+ const { isPaused, transactions } = get();
+ if (isPaused) return;
+
+ // 중복 transaction 체크 (id 기준)
+ const existingTransaction = transactions.find((t) => t.request?.id === transaction.request?.id);
+ if (existingTransaction) return;
+
+ set((state) => ({
+ transactions: [transaction, ...state.transactions],
+ }));
+ },
+
+ setSelectedTransaction: (transaction: HttpTransaction | null) => {
+ set({ selectedTransaction: transaction });
+ },
+
+ clearTransactions: () => {
+ set({ transactions: [], selectedTransaction: null });
+ },
+
+ deleteTransaction: (id: string) => {
+ set((state) => {
+ const filteredTransactions = state.transactions.filter((transaction) => transaction?.request?.id !== id);
+
+ // 삭제된 transaction이 현재 선택된 transaction이면 선택 해제
+ const newSelectedTransaction =
+ state.selectedTransaction?.request?.id === id ? null : state.selectedTransaction;
+
+ return {
+ transactions: filteredTransactions,
+ selectedTransaction: newSelectedTransaction,
+ };
+ });
+ },
+
+ setPaused: (paused: boolean) => {
+ set({ isPaused: paused });
+ },
+
+ togglePause: () => {
+ set((state) => ({ isPaused: !state.isPaused }));
+ },
+ }),
+ {
+ name: 'cheolsu-transaction-store',
+ // transactions는 persist하지 않음 (앱 재시작 시 초기화되어야 함)
+ // currentTransaction만 persist
+ partialize: (state) => ({
+ isPaused: state.isPaused,
+ }),
+ },
+ ),
+);
diff --git a/tauri-ui/src/shared/ui/index.ts b/tauri-ui/src/shared/ui/index.ts
index 2aba708b..78c29240 100644
--- a/tauri-ui/src/shared/ui/index.ts
+++ b/tauri-ui/src/shared/ui/index.ts
@@ -13,3 +13,4 @@ export * from './resizable';
export * from './scroll-area';
export * from './sonner';
export * from './virtualized-scroll-area';
+export * from './dialog';
diff --git a/tauri-ui/src/widgets/network-table/ui/network-table.tsx b/tauri-ui/src/widgets/network-table/ui/network-table.tsx
index f3dceffe..51768674 100644
--- a/tauri-ui/src/widgets/network-table/ui/network-table.tsx
+++ b/tauri-ui/src/widgets/network-table/ui/network-table.tsx
@@ -8,7 +8,7 @@ interface NetworkTableProps {
transactions: HttpTransaction[];
selectedTransaction: HttpTransaction | null;
createTransactionSelectHandler: (transaction: HttpTransaction) => () => void;
- createTransactionDeleteHandler: (id: number) => () => void;
+ createTransactionDeleteHandler: (id: string) => () => void;
}
export const NetworkTable = ({
diff --git a/tauri-ui/src/widgets/network-table/ui/table-body.tsx b/tauri-ui/src/widgets/network-table/ui/table-body.tsx
index f5e49666..9f1d1c84 100644
--- a/tauri-ui/src/widgets/network-table/ui/table-body.tsx
+++ b/tauri-ui/src/widgets/network-table/ui/table-body.tsx
@@ -10,13 +10,13 @@ import { TableRow } from './table-row';
interface TableBodyProps {
data: TableRowData[];
createTransactionSelectHandler: (request: HttpTransaction) => () => void;
- createTransactionDeleteHandler: (id: number) => () => void;
+ createTransactionDeleteHandler: (id: string) => () => void;
}
export const TableBody = ({ data, createTransactionSelectHandler, createTransactionDeleteHandler }: TableBodyProps) => {
const rowHandlers = useMemo(() => {
return data.map((rowData, index) => {
- const id = rowData.transaction.request?.time ?? index;
+ const id = rowData.transaction.request?.id ?? index.toString();
return {
onSelect: createTransactionSelectHandler(rowData.transaction),
onDelete: createTransactionDeleteHandler(id),