Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
f8c3eec
refactor: charts 컴포넌트 shared 디렉토리로 이동
BHyeonKim Jun 29, 2025
d2aa4ab
config: recharts 라이브러리 추가
BHyeonKim Jul 1, 2025
7fec092
refactor: 호가창 컴포넌트 amcharts => recharts로 교체;
BHyeonKim Jul 1, 2025
41e20c7
fix: 렌더링마다 스크롤이 움직이는 버그 수정
BHyeonKim Jul 1, 2025
5733e28
style: 호가창 색상 변경
BHyeonKim Jul 1, 2025
d20697d
refactor: fs routes => code routes로 변경
BHyeonKim Jul 1, 2025
4fe7442
feat: 프로필 네브링크 추가
BHyeonKim Jul 2, 2025
29276c5
feat: 파이 차트 컴포넌트 추가
BHyeonKim Jul 2, 2025
83bfcbc
feat: 코인 자산 목록을 보여주는 테이블 컴포넌트 추가
BHyeonKim Jul 3, 2025
0aa75c7
feat: AssetInfoGraphic 컴포넌트 추가
BHyeonKim Jul 5, 2025
0976cc2
config: Sentry 설정 주석 처리
BHyeonKim Jul 7, 2025
98884cd
feat: 숫자가 0 부터 증가하는 IncrementingNumber 컴포넌트 추가
BHyeonKim Jul 8, 2025
baf47cc
feat: Tab 컴포넌트 추가
BHyeonKim Jul 8, 2025
b859422
fix: 부모가 flex container일때 줄어들지 않게 수정
BHyeonKim Jul 10, 2025
ecaddf4
feat: Pagination 컴포넌트 추가
BHyeonKim Jul 10, 2025
98da578
feat: 테이블 로우에 도넛차트와 똑같은 색으로 표시
BHyeonKim Jul 11, 2025
2646648
feat: 파이차트 모양 변경 및 CustomActiveShape 컴포넌트 추가
BHyeonKim Jul 11, 2025
f24e772
feat: 파이차트를 누르면 테이블이 해당 코인으로 스크롤되게 설정
BHyeonKim Jul 12, 2025
8ec86ec
feat: arrow 아이콘 추가
BHyeonKim Jul 13, 2025
04d57a0
feat: 사용자 체결 조회 리스트 컴포넌트 추가
BHyeonKim Jul 13, 2025
4dccdce
fix: Tab 컴포넌트에 key 추가
BHyeonKim Jul 13, 2025
d0630c5
config: msw 설정 재추가
BHyeonKim Jul 13, 2025
aa0f69b
feat: profile route 추가
BHyeonKim Jul 14, 2025
2cfe6f6
test: 사용자 정보 msw api 추가
BHyeonKim Jul 15, 2025
8e4e560
feat: streaming을 사용한 profile 사용자 거래 데이터 패칭
BHyeonKim Jul 16, 2025
2432d08
fix: spec 변경에 따른 /history api 수정
BHyeonKim Jul 16, 2025
2178618
fix: 스트리밍을 사용한 데이터 패칭 제거
BHyeonKim Jul 16, 2025
2aea91a
feat: 모달 외부를 클릭하면 이전 페이지로 돌아갈 수 있게 수정
BHyeonKim Jul 16, 2025
2b7ff1f
test: history mock api 추가
BHyeonKim Jul 16, 2025
9d14a50
feat: 로그인시 이전 페이지로 이동하도록 설정
BHyeonKim Jul 17, 2025
de21c0c
fix: react router가 ssr => csr로 전환되는 문제 수정
BHyeonKim Jul 17, 2025
60b296b
refactor: profile route에서 history route 분리
BHyeonKim Jul 20, 2025
9ed26f2
feat: Error 컴포넌트 추가
BHyeonKim Jul 21, 2025
8ab7457
feat: 각각 Route에 대한 error boundary 추가
BHyeonKim Jul 21, 2025
3d17745
fix: 백엔드 api 수정에 따른 컴포넌트 및 인터페이스 수정
BHyeonKim Jul 23, 2025
9f1083f
feat: 체결내역 목록에 체결날짜 추가
BHyeonKim Jul 23, 2025
8c0e5fb
feat: 첫번째 페이지 및 마지막 페이지일 때 arrow 버튼 disable
BHyeonKim Jul 23, 2025
5d22be4
fix: Error 컴포넌트에 ClientOnly 적용
BHyeonKim Jul 23, 2025
2f52c2f
fix: 체결내역 목록 에러핸들링 로직 수정
BHyeonKim Jul 23, 2025
9b6d181
fix: profile route 에러 핸들링 로직 수정
BHyeonKim Jul 23, 2025
181b6d6
feat: history 모킹 api 예외처리 추가
BHyeonKim Jul 23, 2025
4b53b21
style: Button 컴포넌트 warn 스타일 추가
BHyeonKim Jul 23, 2025
04de9a0
feat: Error Component 뒤로가기 버튼 추가
BHyeonKim Jul 23, 2025
239695e
refactor: useScrollBottom => useScrollIntoView로 리팩토링
BHyeonKim Jul 23, 2025
b9c68f2
feat: 다음 history 페이지를 조회할 때 최상단으로 이동하도록 설정
BHyeonKim Jul 25, 2025
3b50d65
feat: callback route에 error handling 로직 추가
BHyeonKim Jul 25, 2025
ca248eb
feat: 주문 삭제 api 추가
BHyeonKim Jul 25, 2025
84ca1b8
feat: 로딩스피너 추가
BHyeonKim Jul 25, 2025
2d77739
feat: fetcher를 사용하여 주문 취소기능 추가
BHyeonKim Jul 25, 2025
0629eff
style: 프로필 route 패딩 및 테두리 색상 수정
BHyeonKim Jul 25, 2025
9064d41
style: 체결내역 텍스트 수정
BHyeonKim Jul 25, 2025
ea4a479
fix: 페이지네이션 컴포넌트 버그 수정
BHyeonKim Jul 26, 2025
3a9fd17
feat: 컨텐츠가 없을시 보여주는 fallback 컴포넌트 추가
BHyeonKim Jul 26, 2025
c6ca1b8
fix: /userinfo/trades 모킹 api 수정
BHyeonKim Jul 26, 2025
b09bc6a
style: Fallback component 패딩 추가
BHyeonKim Jul 26, 2025
4240263
fix: 파이차트 소수 단위표시 제거
BHyeonKim Jul 26, 2025
9a42a3a
feat: 파이차트에 예수금(원화)도 표시
BHyeonKim Jul 26, 2025
b4db40d
feat: 매도 호가 가격 내림차순 정렬로 변경
caniro Jul 26, 2025
55e1615
feat: 로그인 검증용 서버 유틸 함수 추가
BHyeonKim Jul 26, 2025
ba2e367
fix: 서버함수 실행시 쿠키를 담아서 요청하도록 수정
BHyeonKim Jul 26, 2025
c5b1c6c
refactor: customReferer를 가져오는 유틸 함수 및 훅 추가
BHyeonKim Jul 26, 2025
b059f65
feat: 로그인이 풀렸을 경우 login route로 리다이렉트
BHyeonKim Jul 26, 2025
64288f5
fix: 이미 로그인한 사용자의 login route 접근 방지
BHyeonKim Jul 26, 2025
9f60579
Merge pull request #36 from CleanEngine/feat/orderbook-sort
BHyeonKim Jul 26, 2025
549b6c8
fix: 다른 코인을 선택해도 똑같은 가격이 표시되는 문제 수정
BHyeonKim Jul 27, 2025
b532a98
style: 오타 수정
BHyeonKim Jul 27, 2025
f1bc085
refactor: 한국시간 계산 로직 수정
BHyeonKim Jul 27, 2025
18bab19
style: 클래스 오타 수정
BHyeonKim Jul 27, 2025
428bdb4
refactor: 공통타입 분리
BHyeonKim Jul 27, 2025
1131827
style: 오타수정
BHyeonKim Jul 27, 2025
976af51
fix: 로직 수정
BHyeonKim Jul 27, 2025
533642a
style: 폴더 이름 오타 수정
BHyeonKim Jul 27, 2025
f79e41e
fix: 타입 누락 프로퍼티 추가
BHyeonKim Jul 27, 2025
ed4712d
refactor: 예외처리 로직을 react hook 실행전으로 이동
BHyeonKim Jul 27, 2025
ac2f441
test: 테스트 불필요 파일 제외
BHyeonKim Jul 27, 2025
7a4cb58
feat: fallback 가격 표시
BHyeonKim Jul 27, 2025
3a3b50e
feat: base64 data to svg 유틸함수 추가
BHyeonKim Jul 27, 2025
7f23520
config: test파일에 대한 rule 수정
BHyeonKim Jul 30, 2025
c603910
test: useCurrentPrice 훅 테스트 추가
BHyeonKim Jul 30, 2025
6a39d3b
test: CoinPriceWithName 컴포넌트 테스트 수정
BHyeonKim Jul 30, 2025
a3bc8a7
test: CoinWithIconAndName 컴포넌트 테스트 수정
BHyeonKim Jul 30, 2025
21f16a3
test: history api 단위 테스트 추가
BHyeonKim Jul 30, 2025
6c12bf6
test: StompProvider 테스트 추가
BHyeonKim Jul 30, 2025
4ad2568
test: StompProvider test hoisted 적용
BHyeonKim Jul 30, 2025
ab65c31
test: UserInfoProvider 단위테스트 추가
BHyeonKim Jul 30, 2025
624cf0c
test: chatbot 컴포넌트 테스트 수정
BHyeonKim Jul 31, 2025
fd659d9
test: chatbot 컴포넌트 테스트 추가
BHyeonKim Jul 31, 2025
1f64eda
docs: readme 업데이트
BHyeonKim Aug 2, 2025
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
339 changes: 295 additions & 44 deletions README.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@
}
}
}
},
{
"include": [
"**/*.test.ts",
"**/*.test.tsx",
"**/*.test.js",
"**/*.test.jsx",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.spec.js",
"**/*.spec.jsx"
],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
],
"javascript": {
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS='--import ./instrument.server.mjs' react-router dev",
"dev": "react-router dev",
"build": "react-router build",
"start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc",
"test": "vitest",
"coverage": "vitest run --coverage",
Expand All @@ -32,11 +32,13 @@
"isbot": "^5",
"ky": "^1.8.1",
"lightweight-charts": "^5.0.7",
"lottie-react": "^2.4.1",
"motion": "^12.12.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.5.3",
"react-toastify": "^11.0.5",
"recharts": "^3.0.2",
"ws": "^8.18.2",
"xstate": "^5.19.3"
},
Expand Down Expand Up @@ -68,6 +70,8 @@
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
"msw": {
"workerDirectory": ["public"]
"workerDirectory": [
"public"
]
}
}
8 changes: 4 additions & 4 deletions react-router.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Config } from '@react-router/dev/config';
import { sentryOnBuildEnd } from '@sentry/react-router';

export default {
appDirectory: './src/app',
ssr: true,
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
await sentryOnBuildEnd({viteConfig, reactRouterConfig, buildManifest});
},
/* Sentry 설정 제외 */
// buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
// await sentryOnBuildEnd({viteConfig, reactRouterConfig, buildManifest});
// },
} satisfies Config;
48 changes: 27 additions & 21 deletions src/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import * as Sentry from '@sentry/react-router';
/* v8 ignore start */
import { startTransition } from 'react';
import { hydrateRoot } from 'react-dom/client';
import { HydratedRouter } from 'react-router/dom';

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

sendDefaultPii: true,
// sendDefaultPii: true,

integrations: [
Sentry.reactRouterTracingIntegration(),
Sentry.replayIntegration(),
Sentry.feedbackIntegration({
colorScheme: 'system',
}),
],
// integrations: [
// Sentry.reactRouterTracingIntegration(),
// Sentry.replayIntegration(),
// Sentry.feedbackIntegration({
// colorScheme: 'system',
// }),
// ],

_experiments: { enableLogs: true },
// _experiments: { enableLogs: true },

tracesSampleRate: 1.0,
// tracesSampleRate: 1.0,

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

async function prepareApp() {
return Promise.resolve();
if (process.env.NODE_ENV !== 'development') {
return;
}

const { worker } = await import('../mocks/browser');

return worker.start();
}

prepareApp().then(() => {
Expand Down
22 changes: 16 additions & 6 deletions src/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
/* v8 ignore start */
import { PassThrough } from 'node:stream';
import { server } from '~/mocks/server';

import { createReadableStreamFromReadable } from '@react-router/node';
import {
getMetaTagTransformer,
wrapSentryHandleRequest,
} from '@sentry/react-router';
import { isbot } from 'isbot';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
import { renderToPipeableStream } from 'react-dom/server';
Expand All @@ -14,6 +11,15 @@ import { ServerRouter } from 'react-router';

export const streamTimeout = 5_000;

if (process.env.NODE_ENV === 'development') {
server.listen();

server.events.on('request:start', ({ request }) => {
// biome-ignore lint/suspicious/noConsole: <explanation>
console.log('MSW intercepted:', request.method, request.url);
});
}

function handleRequest(
request: Request,
responseStatusCode: number,
Expand Down Expand Up @@ -51,7 +57,9 @@ function handleRequest(
}),
);

pipe(getMetaTagTransformer(body));
/* Sentry 설정 제외 */
// pipe(getMetaTagTransformer(body));
pipe(body);
},
onShellError(error: unknown) {
reject(error);
Expand All @@ -75,5 +83,7 @@ function handleRequest(
});
}

export default wrapSentryHandleRequest(handleRequest);
/* Sentry 설정 제외 */
// export default wrapSentryHandleRequest(handleRequest);
export default handleRequest;
/* v8 ignore end */
100 changes: 85 additions & 15 deletions src/app/provider/StompProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,106 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import StompProvider, { useStompClient } from './StompProvider';

vi.mock('@stomp/stompjs', () => {
const brokerURL = 'ws://localhost:8080';

const { MockClient } = vi.hoisted(() => {
return {
Client: vi.fn().mockImplementation(() => {
return {
activate: vi.fn(),
deactivate: vi.fn(),
onConnect: null,
onDisconnect: null,
onWebSocketError: null,
onStompError: null,
};
MockClient: vi.fn(function (this: any, config: any) {
this.brokerURL = config.brokerURL;
this.activate = vi.fn();
this.deactivate = vi.fn();
this.onConnect = null;
this.onDisconnect = null;
this.onWebSocketError = null;
this.onStompError = null;
}),
};
});

vi.mock('@stomp/stompjs', () => {
return {
Client: MockClient,
};
});

describe('useStompClient 테스트', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('useStompClient hook은 StompProvider 외부에서 사용하면 에러를 던진다.', () => {
expect(() => renderHook(() => useStompClient())).toThrowError();
});

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

expect(result.current).toHaveProperty('client');
expect(result.current.client).toBeTruthy();
expect(result.current).toHaveProperty('connected');
expect(result.current.connected).toBe(false);
});

it('onConnect 콜백이 호출되면 connected가 true가 된다.', () => {
const { result } = renderHook(() => useStompClient(), {
wrapper: ({ children }) => (
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
),
});

expect(result.current.connected).toBe(false);

act(() => {
const clientInstance = MockClient.mock.instances[0] as any;
clientInstance?.onConnect?.();
});

expect(result.current.connected).toBe(true);
});

it('onDisconnect 콜백이 호출되면 connected가 false가 된다.', () => {
const { result } = renderHook(() => useStompClient(), {
wrapper: ({ children }) => (
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
),
});

const clientInstance = MockClient.mock.instances[0] as any;
act(() => {
clientInstance?.onConnect?.();
});
expect(result.current.connected).toBe(true);

act(() => {
clientInstance?.onDisconnect?.();
});

expect(result.current.connected).toBe(false);
});

it('onWebSocketError 콜백이 호출되면 connected가 false가 된다.', () => {
const { result } = renderHook(() => useStompClient(), {
wrapper: ({ children }) => (
<StompProvider brokerURL={brokerURL}>{children}</StompProvider>
),
});

const clientInstance = MockClient.mock.instances[0] as any;
act(() => {
clientInstance?.onConnect?.();
});
expect(result.current.connected).toBe(true);

act(() => {
const mockError = new Error('WebSocket connection failed');
clientInstance?.onWebSocketError?.(mockError);
});

expect(result.current.connected).toBe(false);
});
});
61 changes: 61 additions & 0 deletions src/app/provider/UserInfoProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import UserIdProvider, { useUserId } from './UserInfoProvider';

vi.mock('window', () => {
const mockStore = new Map();

return {
localStorage: {
getItem: vi.fn((key) => mockStore.get(key)),
setItem: vi.fn((key, value) => mockStore.set(key, value)),
removeItem: vi.fn((key) => mockStore.delete(key)),
clear: vi.fn(() => mockStore.clear()),
},
};
});

const MOCK_USERID = 4;

describe('UserInfoProvider 테스트', () => {
beforeEach(() => {
vi.clearAllMocks();
window.localStorage.clear();
});

it('useUserId hook은 UserInfoProvider 외부에서 사용하면 에러를 던진다.', () => {
expect(() => renderHook(() => useUserId())).toThrowError();
});

it('초기 상태에서는 userId가 null이다.', () => {
const { result } = renderHook(useUserId, {
wrapper: ({ children }) => <UserIdProvider>{children}</UserIdProvider>,
});

expect(result.current.userId).toBe(null);
});

it('초기 마운트 시 로컬스토리지에서 userId를 불러온다.', () => {
window.localStorage.setItem('userId', MOCK_USERID.toString());

const { result } = renderHook(useUserId, {
wrapper: ({ children }) => <UserIdProvider>{children}</UserIdProvider>,
});

expect(result.current.userId).toBe(MOCK_USERID);
});

it('setUserId를 호출하면 userId 상태가 업데이트된다.', () => {
const { result } = renderHook(useUserId, {
wrapper: ({ children }) => <UserIdProvider>{children}</UserIdProvider>,
});

expect(result.current.userId).toBe(null);

act(() => {
result.current.setUserId(MOCK_USERID);
});

expect(result.current.userId).toBe(MOCK_USERID);
});
});
Loading
Loading