packages/
โโโ api/ # API ๊ด๋ จ ๋ก์ง ๋ฐ ํ
โโโ ui/ # UI ์ปดํฌ๋ํธ ๋ฐ ์คํ์ผ
โโโ config/ # ๊ณตํต ์ค์
โโโ tsconfig/ # TypeScript ์ค์
Branch ์ด๋ฆ์ ์์ ๋ชฉ์ ๊ณผ ์ฐ๊ด๋ ์ด์ ๋ฒํธ๋ฅผ ํฌํจํ๋ ๋ฐฉ์
<ํ์
>/<์ด์ ๋ฒํธ>-<๊ฐ๋จํ ์ค๋ช
>
- feature/1234-add-user-login
- bugfix/5678-fix-login-error
- release/1.2.0- feature/ - ์๋ก์ด ๊ธฐ๋ฅ ๊ฐ๋ฐ ์
- bugfix/ - ๋ฒ๊ทธ ์์ ์
- hotfix/ - ๊ธด๊ธํ ๋ฒ๊ทธ ์์ ์ (๋ณดํต ํ๋ก๋์ ํ๊ฒฝ์์ ๋ฐ์)
- release/ - ๋ฆด๋ฆฌ์ฆ ์ค๋น ์
- chore/ - ๋น๋ ๋ฐ ๊ธฐํ ์์ ์๋ํ, ๋ฌธ์ ์์ ๋ฑ ์ฝ๋์ ๊ด๋ จ ์๋ ์์
// packages/api/src/types/index.ts
export interface NewFeatureResponse {
id: string;
name: string;
status: 'active' | 'inactive';
createdAt: string;
}// packages/api/src/client/api-client.ts
export class ElectronAPIClient {
// ๊ธฐ์กด ๋ฉ์๋๋ค...
async getNewFeature(id: string): Promise<NewFeatureResponse> {
if (typeof window !== 'undefined' && (window as any).electronAPI) {
return await (window as any).electronAPI.getNewFeature(id);
}
throw new Error('Electron API not available');
}
}
export class WebAPIClient {
// ๊ธฐ์กด ๋ฉ์๋๋ค...
async getNewFeature(id: string): Promise<NewFeatureResponse> {
const response = await fetch(`${this.baseURL}/new-feature/${id}`);
if (!response.ok) {
throw new Error('Failed to fetch new feature');
}
return await response.json();
}
}// packages/api/src/hooks/use-new-feature.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { createAPIClient } from '../../index';
import { NewFeatureResponse } from '../types';
let apiClient: ReturnType<typeof createAPIClient> | null = null;
function getApiClient() {
if (!apiClient) {
apiClient = createAPIClient();
}
return apiClient;
}
// ์กฐํ ํ
export function useNewFeature(id: string) {
return useQuery({
queryKey: ['newFeature', id],
queryFn: () => getApiClient().getNewFeature(id),
enabled: !!id,
staleTime: 5 * 60 * 1000, // 5๋ถ
retry: 3,
});
}
// ์์ฑ/์์ /์ญ์ ๋ฎคํ
์ด์
ํ
export function useNewFeatureMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Partial<NewFeatureResponse>) =>
getApiClient().createNewFeature(data),
onSuccess: () => {
// ๊ด๋ จ ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries({ queryKey: ['newFeature'] });
},
});
}// packages/api/src/hooks/index.ts
export * from './use-new-feature';
// ๊ธฐ์กด ์ต์คํฌํธ๋ค...// packages/api/index.ts
// ๊ธฐ์กด ์ต์คํฌํธ๋ค...
export * from './src/hooks/use-new-feature';// packages/api/src/utils/new-feature-utils.ts
export function formatNewFeatureName(name: string): string {
return name.trim().toLowerCase().replace(/\s+/g, '-');
}
export function validateNewFeatureData(data: any): boolean {
return data && typeof data.name === 'string' && data.name.length > 0;
}// packages/api/src/utils/index.ts
export * from './new-feature-utils';
// ๊ธฐ์กด ์ต์คํฌํธ๋ค...packages/ui/src/components/NewComponent/
โโโ NewComponent.tsx
โโโ NewComponent.css (์ ํ์ฌํญ)
โโโ index.ts (์ ํ์ฌํญ)
// packages/ui/src/components/NewComponent/NewComponent.tsx
import React from 'react';
import { cn } from '../../utils/cn';
interface NewComponentProps {
title: string;
description?: string;
variant?: 'default' | 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
className?: string;
children?: React.ReactNode;
}
export function NewComponent({
title,
description,
variant = 'default',
size = 'md',
className,
children,
}: NewComponentProps) {
return (
<div
className={cn(
'rounded-lg border p-4',
{
'bg-white border-gray-200': variant === 'default',
'bg-blue-50 border-blue-200': variant === 'primary',
'bg-gray-50 border-gray-300': variant === 'secondary',
'p-2 text-sm': size === 'sm',
'p-4 text-base': size === 'md',
'p-6 text-lg': size === 'lg',
},
className
)}
>
<h3 className="font-semibold text-gray-900">{title}</h3>
{description && (
<p className="mt-1 text-sm text-gray-600">{description}</p>
)}
{children}
</div>
);
}// packages/ui/src/components/NewComponent/index.ts
export { NewComponent } from './NewComponent';// packages/ui/src/stories/NewComponent.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { NewComponent } from '../components/NewComponent/NewComponent';
const meta: Meta<typeof NewComponent> = {
title: 'Components/NewComponent',
component: NewComponent,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: { type: 'select' },
options: ['default', 'primary', 'secondary'],
},
size: {
control: { type: 'select' },
options: ['sm', 'md', 'lg'],
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
title: '๊ธฐ๋ณธ ์ปดํฌ๋ํธ',
description: '์ด๊ฒ์ ๊ธฐ๋ณธ ์ปดํฌ๋ํธ์
๋๋ค.',
},
};
export const Primary: Story = {
args: {
title: 'Primary ์ปดํฌ๋ํธ',
description: '์ด๊ฒ์ Primary ์คํ์ผ์ ์ปดํฌ๋ํธ์
๋๋ค.',
variant: 'primary',
},
};
export const Large: Story = {
args: {
title: 'Large ์ปดํฌ๋ํธ',
description: '์ด๊ฒ์ Large ํฌ๊ธฐ์ ์ปดํฌ๋ํธ์
๋๋ค.',
size: 'lg',
},
};// packages/ui/index.tsx
import './src/styles/globals.css';
export * from './src/components/Button/Button';
export * from './src/components/Header/Header';
export * from './src/components/Page/Page';
export * from './src/components/Alert/Alert';
export * from './src/components/AnimatedBox/AnimatedBox';
export * from './src/components/NewComponent/NewComponent'; // ์๋ก ์ถ๊ฐ// packages/ui/package.json
{
"exports": {
".": "./index.tsx",
"./tokens.css": "./src/styles/tokens.css",
"./globals.css": "./src/styles/globals.css",
"./theme.css": "./src/styles/theme.css",
"./Button": "./src/components/Button/Button.tsx",
"./AnimatedBox": "./src/components/AnimatedBox/AnimatedBox.tsx",
"./Header": "./src/components/Header/Header.tsx",
"./Page": "./src/components/Page/Page.tsx",
"./Alert": "./src/components/Alert/Alert.tsx",
"./NewComponent": "./src/components/NewComponent/NewComponent.tsx"
}
}// packages/api/src/hooks/__tests__/use-new-feature.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useNewFeature } from '../use-new-feature';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe('useNewFeature', () => {
it('should fetch new feature data', async () => {
const { result } = renderHook(() => useNewFeature('test-id'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
});
});// packages/ui/src/components/NewComponent/__tests__/NewComponent.test.tsx
import { render, screen } from '@testing-library/react';
import { NewComponent } from '../NewComponent';
describe('NewComponent', () => {
it('renders with title', () => {
render(<NewComponent title="Test Title" />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
it('renders with description', () => {
render(
<NewComponent
title="Test Title"
description="Test Description"
/>
);
expect(screen.getByText('Test Description')).toBeInTheDocument();
});
});# ์ ์ฒด ํ๋ก์ ํธ ์์กด์ฑ ์ค์น
npm install
# ๊ฐ๋ฐ ์๋ฒ ์์ (์น)
npm run dev
# Storybook ์์ (UI ํจํค์ง)
cd packages/ui
npm run storybook- API ํจํค์ง: ํ์ ์ ์ โ API ํด๋ผ์ด์ธํธ โ React Query ํ โ ์ต์คํฌํธ
- UI ํจํค์ง: ์ปดํฌ๋ํธ ๊ตฌํ โ Storybook ์คํ ๋ฆฌ โ ์ต์คํฌํธ โ ํ ์คํธ
# ํ์
์ฒดํฌ
npm run typecheck
# ๋ฆฐํ
npm run lint
# ํฌ๋งทํ
npm run format
# ํ
์คํธ
npm run test// apps/web/src/App.tsx ๋๋ apps/electron/layers/renderer/src/App.tsx
import { useHealth, useVersion, createAPIClient } from 'api';
function App() {
const { data: health } = useHealth();
const { data: version } = useVersion();
return (
<div>
<p>Health: {health?.status}</p>
<p>Version: {version?.version}</p>
</div>
);
}// apps/web/src/App.tsx ๋๋ apps/electron/layers/renderer/src/App.tsx
import { Button, Header, Page, NewComponent } from 'ui';
import 'ui/globals.css';
function App() {
return (
<Page>
<Header title="My App" />
<NewComponent
title="Welcome"
description="This is a new component"
variant="primary"
/>
<Button>Click me</Button>
</Page>
);
}# ์ ์ฒด ํ๋ก์ ํธ ๋น๋
npm run build
# ํน์ ํจํค์ง๋ง ๋น๋
cd packages/api && npm run typecheck
cd packages/ui && npm run build-storybookcd apps/electron
npm run build