Skip to content

Commit e9757ca

Browse files
authored
Merge pull request #39 from CleanEngine/develop
feat: 프로필 기능, 테스트 인프라, UI/UX 개선 등 대규모 업데이트
2 parents f4a24f0 + 1f64eda commit e9757ca

File tree

88 files changed

+3898
-463
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+3898
-463
lines changed

README.md

Lines changed: 295 additions & 44 deletions
Large diffs are not rendered by default.

biome.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@
7171
}
7272
}
7373
}
74+
},
75+
{
76+
"include": [
77+
"**/*.test.ts",
78+
"**/*.test.tsx",
79+
"**/*.test.js",
80+
"**/*.test.jsx",
81+
"**/*.spec.ts",
82+
"**/*.spec.tsx",
83+
"**/*.spec.js",
84+
"**/*.spec.jsx"
85+
],
86+
"linter": {
87+
"rules": {
88+
"suspicious": {
89+
"noExplicitAny": "off"
90+
}
91+
}
92+
}
7493
}
7594
],
7695
"javascript": {

package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7-
"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev",
7+
"dev": "react-router dev",
88
"build": "react-router build",
9-
"start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js",
9+
"start": "react-router-serve ./build/server/index.js",
1010
"typecheck": "react-router typegen && tsc",
1111
"test": "vitest",
1212
"coverage": "vitest run --coverage",
@@ -32,11 +32,13 @@
3232
"isbot": "^5",
3333
"ky": "^1.8.1",
3434
"lightweight-charts": "^5.0.7",
35+
"lottie-react": "^2.4.1",
3536
"motion": "^12.12.1",
3637
"react": "^19.1.0",
3738
"react-dom": "^19.1.0",
3839
"react-router": "^7.5.3",
3940
"react-toastify": "^11.0.5",
41+
"recharts": "^3.0.2",
4042
"ws": "^8.18.2",
4143
"xstate": "^5.19.3"
4244
},
@@ -68,6 +70,8 @@
6870
},
6971
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
7072
"msw": {
71-
"workerDirectory": ["public"]
73+
"workerDirectory": [
74+
"public"
75+
]
7276
}
7377
}

react-router.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { Config } from '@react-router/dev/config';
2-
import { sentryOnBuildEnd } from '@sentry/react-router';
32

43
export default {
54
appDirectory: './src/app',
65
ssr: true,
7-
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
8-
await sentryOnBuildEnd({viteConfig, reactRouterConfig, buildManifest});
9-
},
6+
/* Sentry 설정 제외 */
7+
// buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
8+
// await sentryOnBuildEnd({viteConfig, reactRouterConfig, buildManifest});
9+
// },
1010
} satisfies Config;

src/app/entry.client.tsx

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
1-
import * as Sentry from '@sentry/react-router';
21
/* v8 ignore start */
32
import { startTransition } from 'react';
43
import { hydrateRoot } from 'react-dom/client';
54
import { HydratedRouter } from 'react-router/dom';
65

7-
Sentry.init({
8-
dsn: 'https://8343c6ee467e6f35f22c570a68cd2e6e@o4509544992407552.ingest.us.sentry.io/4509548888391680',
6+
/* Sentry 설정 제외 */
7+
// Sentry.init({
8+
// dsn: 'https://8343c6ee467e6f35f22c570a68cd2e6e@o4509544992407552.ingest.us.sentry.io/4509548888391680',
99

10-
sendDefaultPii: true,
10+
// sendDefaultPii: true,
1111

12-
integrations: [
13-
Sentry.reactRouterTracingIntegration(),
14-
Sentry.replayIntegration(),
15-
Sentry.feedbackIntegration({
16-
colorScheme: 'system',
17-
}),
18-
],
12+
// integrations: [
13+
// Sentry.reactRouterTracingIntegration(),
14+
// Sentry.replayIntegration(),
15+
// Sentry.feedbackIntegration({
16+
// colorScheme: 'system',
17+
// }),
18+
// ],
1919

20-
_experiments: { enableLogs: true },
20+
// _experiments: { enableLogs: true },
2121

22-
tracesSampleRate: 1.0,
22+
// tracesSampleRate: 1.0,
2323

24-
// Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled
25-
tracePropagationTargets: [/^\//, /^https:\/\/investfuture\.my\/api/],
26-
// Capture Replay for 10% of all sessions,
27-
// plus 100% of sessions with an error
28-
replaysSessionSampleRate: 0.1,
29-
replaysOnErrorSampleRate: 1.0,
30-
});
24+
// // Set `tracePropagationTargets` to declare which URL(s) should have trace propagation enabled
25+
// tracePropagationTargets: [/^\//, /^https:\/\/investfuture\.my\/api/],
26+
// // Capture Replay for 10% of all sessions,
27+
// // plus 100% of sessions with an error
28+
// replaysSessionSampleRate: 0.1,
29+
// replaysOnErrorSampleRate: 1.0,
30+
// });
3131

3232
async function prepareApp() {
33-
return Promise.resolve();
33+
if (process.env.NODE_ENV !== 'development') {
34+
return;
35+
}
36+
37+
const { worker } = await import('../mocks/browser');
38+
39+
return worker.start();
3440
}
3541

3642
prepareApp().then(() => {

src/app/entry.server.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
/* v8 ignore start */
22
import { PassThrough } from 'node:stream';
3+
import { server } from '~/mocks/server';
34

45
import { createReadableStreamFromReadable } from '@react-router/node';
5-
import {
6-
getMetaTagTransformer,
7-
wrapSentryHandleRequest,
8-
} from '@sentry/react-router';
96
import { isbot } from 'isbot';
107
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
118
import { renderToPipeableStream } from 'react-dom/server';
@@ -14,6 +11,15 @@ import { ServerRouter } from 'react-router';
1411

1512
export const streamTimeout = 5_000;
1613

14+
if (process.env.NODE_ENV === 'development') {
15+
server.listen();
16+
17+
server.events.on('request:start', ({ request }) => {
18+
// biome-ignore lint/suspicious/noConsole: <explanation>
19+
console.log('MSW intercepted:', request.method, request.url);
20+
});
21+
}
22+
1723
function handleRequest(
1824
request: Request,
1925
responseStatusCode: number,
@@ -51,7 +57,9 @@ function handleRequest(
5157
}),
5258
);
5359

54-
pipe(getMetaTagTransformer(body));
60+
/* Sentry 설정 제외 */
61+
// pipe(getMetaTagTransformer(body));
62+
pipe(body);
5563
},
5664
onShellError(error: unknown) {
5765
reject(error);
@@ -75,5 +83,7 @@ function handleRequest(
7583
});
7684
}
7785

78-
export default wrapSentryHandleRequest(handleRequest);
86+
/* Sentry 설정 제외 */
87+
// export default wrapSentryHandleRequest(handleRequest);
88+
export default handleRequest;
7989
/* v8 ignore end */
Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,106 @@
1-
import { renderHook } from '@testing-library/react';
2-
import { describe, expect, it, vi } from 'vitest';
3-
1+
import { act, renderHook } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
43
import StompProvider, { useStompClient } from './StompProvider';
54

6-
vi.mock('@stomp/stompjs', () => {
5+
const brokerURL = 'ws://localhost:8080';
6+
7+
const { MockClient } = vi.hoisted(() => {
78
return {
8-
Client: vi.fn().mockImplementation(() => {
9-
return {
10-
activate: vi.fn(),
11-
deactivate: vi.fn(),
12-
onConnect: null,
13-
onDisconnect: null,
14-
onWebSocketError: null,
15-
onStompError: null,
16-
};
9+
MockClient: vi.fn(function (this: any, config: any) {
10+
this.brokerURL = config.brokerURL;
11+
this.activate = vi.fn();
12+
this.deactivate = vi.fn();
13+
this.onConnect = null;
14+
this.onDisconnect = null;
15+
this.onWebSocketError = null;
16+
this.onStompError = null;
1717
}),
1818
};
1919
});
2020

21+
vi.mock('@stomp/stompjs', () => {
22+
return {
23+
Client: MockClient,
24+
};
25+
});
26+
2127
describe('useStompClient 테스트', () => {
28+
beforeEach(() => {
29+
vi.clearAllMocks();
30+
});
31+
2232
it('useStompClient hook은 StompProvider 외부에서 사용하면 에러를 던진다.', () => {
2333
expect(() => renderHook(() => useStompClient())).toThrowError();
2434
});
2535

26-
it('useStompClient hook은 StompProvider 내부에서 사용하면 정상 작동한다.', () => {
36+
it('초기 상태에서는 connected가 false이다.', () => {
2737
const { result } = renderHook(() => useStompClient(), {
2838
wrapper: ({ children }) => (
29-
<StompProvider brokerURL="">{children}</StompProvider>
39+
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
3040
),
3141
});
3242

3343
expect(result.current).toHaveProperty('client');
44+
expect(result.current.client).toBeTruthy();
3445
expect(result.current).toHaveProperty('connected');
46+
expect(result.current.connected).toBe(false);
47+
});
48+
49+
it('onConnect 콜백이 호출되면 connected가 true가 된다.', () => {
50+
const { result } = renderHook(() => useStompClient(), {
51+
wrapper: ({ children }) => (
52+
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
53+
),
54+
});
55+
56+
expect(result.current.connected).toBe(false);
57+
58+
act(() => {
59+
const clientInstance = MockClient.mock.instances[0] as any;
60+
clientInstance?.onConnect?.();
61+
});
62+
63+
expect(result.current.connected).toBe(true);
64+
});
65+
66+
it('onDisconnect 콜백이 호출되면 connected가 false가 된다.', () => {
67+
const { result } = renderHook(() => useStompClient(), {
68+
wrapper: ({ children }) => (
69+
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
70+
),
71+
});
72+
73+
const clientInstance = MockClient.mock.instances[0] as any;
74+
act(() => {
75+
clientInstance?.onConnect?.();
76+
});
77+
expect(result.current.connected).toBe(true);
78+
79+
act(() => {
80+
clientInstance?.onDisconnect?.();
81+
});
82+
83+
expect(result.current.connected).toBe(false);
84+
});
85+
86+
it('onWebSocketError 콜백이 호출되면 connected가 false가 된다.', () => {
87+
const { result } = renderHook(() => useStompClient(), {
88+
wrapper: ({ children }) => (
89+
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
90+
),
91+
});
92+
93+
const clientInstance = MockClient.mock.instances[0] as any;
94+
act(() => {
95+
clientInstance?.onConnect?.();
96+
});
97+
expect(result.current.connected).toBe(true);
98+
99+
act(() => {
100+
const mockError = new Error('WebSocket connection failed');
101+
clientInstance?.onWebSocketError?.(mockError);
102+
});
103+
104+
expect(result.current.connected).toBe(false);
35105
});
36106
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import UserIdProvider, { useUserId } from './UserInfoProvider';
4+
5+
vi.mock('window', () => {
6+
const mockStore = new Map();
7+
8+
return {
9+
localStorage: {
10+
getItem: vi.fn((key) => mockStore.get(key)),
11+
setItem: vi.fn((key, value) => mockStore.set(key, value)),
12+
removeItem: vi.fn((key) => mockStore.delete(key)),
13+
clear: vi.fn(() => mockStore.clear()),
14+
},
15+
};
16+
});
17+
18+
const MOCK_USERID = 4;
19+
20+
describe('UserInfoProvider 테스트', () => {
21+
beforeEach(() => {
22+
vi.clearAllMocks();
23+
window.localStorage.clear();
24+
});
25+
26+
it('useUserId hook은 UserInfoProvider 외부에서 사용하면 에러를 던진다.', () => {
27+
expect(() => renderHook(() => useUserId())).toThrowError();
28+
});
29+
30+
it('초기 상태에서는 userId가 null이다.', () => {
31+
const { result } = renderHook(useUserId, {
32+
wrapper: ({ children }) => <UserIdProvider>{children}</UserIdProvider>,
33+
});
34+
35+
expect(result.current.userId).toBe(null);
36+
});
37+
38+
it('초기 마운트 시 로컬스토리지에서 userId를 불러온다.', () => {
39+
window.localStorage.setItem('userId', MOCK_USERID.toString());
40+
41+
const { result } = renderHook(useUserId, {
42+
wrapper: ({ children }) => <UserIdProvider>{children}</UserIdProvider>,
43+
});
44+
45+
expect(result.current.userId).toBe(MOCK_USERID);
46+
});
47+
48+
it('setUserId를 호출하면 userId 상태가 업데이트된다.', () => {
49+
const { result } = renderHook(useUserId, {
50+
wrapper: ({ children }) => <UserIdProvider>{children}</UserIdProvider>,
51+
});
52+
53+
expect(result.current.userId).toBe(null);
54+
55+
act(() => {
56+
result.current.setUserId(MOCK_USERID);
57+
});
58+
59+
expect(result.current.userId).toBe(MOCK_USERID);
60+
});
61+
});

0 commit comments

Comments
 (0)