diff --git a/.github/workflows/fe-code-check.yml b/.github/workflows/fe-code-check.yml new file mode 100644 index 00000000..ac64c135 --- /dev/null +++ b/.github/workflows/fe-code-check.yml @@ -0,0 +1,41 @@ +name: Frontend Lint Check + +on: + pull_request: + branches: [dev/fe] + paths: + - "src/frontend/**" + push: + branches: [dev/fe] + paths: + - "src/frontend/**" + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "yarn" + cache-dependency-path: "./src/frontend/yarn.lock" + + - name: Install Dependencies + working-directory: ./src/frontend + run: yarn install --frozen-lockfile + + - name: Run ESLint + working-directory: ./src/frontend + run: yarn lint --max-warnings=0 + + - name: Run Type Check + working-directory: ./src/frontend + run: yarn typecheck + + - name: Run Prettier Check + working-directory: ./src/frontend + run: yarn format diff --git a/src/frontend/eslint.config.js b/src/frontend/eslint.config.js index 65c8ad42..764a9214 100644 --- a/src/frontend/eslint.config.js +++ b/src/frontend/eslint.config.js @@ -7,6 +7,7 @@ import reactHooks from 'eslint-plugin-react-hooks' import simpleImportSort from 'eslint-plugin-simple-import-sort' import tanstackPlugin from '@tanstack/eslint-plugin-query' import unusedImports from 'eslint-plugin-unused-imports' +import reactPlugin from 'eslint-plugin-react' export default tseslint.config( { ignores: ['dist', 'node_modules', '**/*.config.js', '!**/eslint.config.js'] }, @@ -37,10 +38,14 @@ export default tseslint.config( 'simple-import-sort': simpleImportSort, '@typescript-eslint': tseslint.plugin, onlyWarn, - tanstack: tanstackPlugin + tanstack: tanstackPlugin, + react: reactPlugin }, rules: { ...reactHooks.configs.recommended.rules, + 'no-console': ['error', { allow: ['warn', 'error'] }], + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', 'import/no-anonymous-default-export': 'off', 'prettier/prettier': 'error', 'unused-imports/no-unused-imports': 'error', diff --git a/src/frontend/package.json b/src/frontend/package.json index 6af0a2bd..bddbf83e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -8,9 +8,12 @@ "dev": "vite dev", "start": "vite", "typecheck": "tsc", + "test": "vitest", "check": "yarn formatter && yarn lint:fix", + "lint": "eslint \"src/**/*.{ts,tsx}\"", "lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\"", - "formatter": "prettier --write \"src/**/*.{ts,tsx}\"", + "format": "prettier --check \"src/**/*.{ts,tsx}\"", + "format:fix": "prettier --write \"src/**/*.{ts,tsx}\"", "preview": "vite preview", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", @@ -42,6 +45,7 @@ "devDependencies": { "@chromatic-com/storybook": "^3.2.4", "@eslint/js": "^9.17.0", + "@mswjs/socket.io-binding": "^0.1.1", "@storybook/addon-essentials": "^8.5.0", "@storybook/addon-interactions": "^8.5.0", "@storybook/addon-onboarding": "^8.5.0", @@ -49,6 +53,7 @@ "@storybook/react-vite": "^8.5.0", "@storybook/test": "^8.5.0", "@tanstack/eslint-plugin-query": "^5.66.1", + "@testing-library/react": "^16.2.0", "@types/node": "^20", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.1", @@ -56,11 +61,13 @@ "eslint": "^9.17.0", "eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-prettier": "^5.2.3", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-storybook": "^0.11.2", "eslint-plugin-unused-imports": "^4.1.4", "globals": "^15.14.0", + "jsdom": "^26.0.0", "msw": "^2.7.1", "prettier": "^3.4.2", "sharp": "^0.33.5", diff --git a/src/frontend/src/__mock__/data/auth.mock.ts b/src/frontend/src/__mock__/data/auth.mock.ts index 7611e966..09412996 100644 --- a/src/frontend/src/__mock__/data/auth.mock.ts +++ b/src/frontend/src/__mock__/data/auth.mock.ts @@ -1,19 +1,48 @@ -import { CommonResponseType } from '@/apis/schema/types/common' +import { LoginResponseSchema } from '@/apis/schema/types/auth' -export const registerMock: CommonResponseType<{ id: string; name: string }> = { - code: 'AUTH100', - message: 'Register OK', +export const statusCheckMock = { + code: 'AUTH109', + message: 'login status check success!', result: { - id: '1', - name: 'John Doe' + status: true } } -export const loginMock: CommonResponseType<{ accessToken: string }> = { +export const registerMock = { + code: 'AUTH104', + message: 'register success!' +} + +export const loginMock: LoginResponseSchema = { code: 'AUTH100', message: 'Login OK', result: { accessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzQwMzcwODQ5LCJleHAiOjE3NDAzNzA4NTl9.7bQ_NfH38iBCJIJVzlnrH5WepVBh1vrpaC3sOgfWWRM', + refreshToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzQwMzcwODQ5LCJleHAiOjE3NDAzNzA4NTl9.7bQ_NfH38iBCJIJVzlnrH5WepVBh1vrpaC3sOgfWWRM' + } +} + +export const logoutMock = { code: 'AUTH101', message: 'Logout success!' } + +export const refreshTokenMock = { + code: 'AUTH102', + message: 'refresh success', + result: { + accessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzQwMzcwODQ5LCJleHAiOjE3NDAzNzA4NTl9.7bQ_NfH38iBCJIJVzlnrH5WepVBh1vrpaC3sOgfWWRM', + refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzQwMzcwODQ5LCJleHAiOjE3NDAzNzA4NTl9.7bQ_NfH38iBCJIJVzlnrH5WepVBh1vrpaC3sOgfWWRM' } } + +export const verifyTokenMock = { + code: 'AUTH105', + message: 'token verify success!' +} + +export const healthCheckMock = { + code: 'AUTH108', + message: 'health check success!' +} diff --git a/src/frontend/src/__mock__/data/search.handler.ts b/src/frontend/src/__mock__/data/search.handler.ts new file mode 100644 index 00000000..9379abc0 --- /dev/null +++ b/src/frontend/src/__mock__/data/search.handler.ts @@ -0,0 +1,660 @@ +export const mockMessages = [ + { + id: 0, + serverId: 0, + sequence: 0, + channelId: 0, + sendMemberId: 0, + content: '테스트 메시지 1', + createdAt: '2025-03-12T14:14:54.827Z' + }, + { + id: 1, + serverId: 0, + sequence: 1, + channelId: 0, + sendMemberId: 1, + content: '테스트 메시지 2', + createdAt: '2025-03-12T14:14:54.827Z' + }, + { + id: 2, + serverId: 0, + sequence: 2, + channelId: 0, + sendMemberId: 2, + content: '테스트 메시지 3', + createdAt: '2025-03-12T14:14:54.827Z' + }, + { + id: 3, + serverId: 0, + sequence: 3, + channelId: 0, + sendMemberId: 3, + content: '테스트 메시지 4', + createdAt: '2025-03-12T14:14:54.827Z' + } +] + +export const mockUnreadChannels = [ + { + channelId: 0, + unreadCount: 1 + }, + { + channelId: 1, + unreadCount: 2 + }, + { + channelId: 2, + unreadCount: 3 + } +] + +export const mockUnreadMessages = [ + { + serverId: 1, + channels: [ + { + channelId: 0, + unreadCount: 1 + }, + { + channelId: 1, + unreadCount: 2 + }, + { + channelId: 2, + unreadCount: 3 + } + ], + totalUnread: 6 + }, + { + serverId: 2, + channels: [ + { + channelId: 0, + unreadCount: 1 + }, + { + channelId: 1, + unreadCount: 2 + }, + { + channelId: 2, + unreadCount: 3 + } + ], + totalUnread: 6 + } +] + +export const mockMessagesByChannel = { + serverId: 1, + channelId: 1, + lastMessageId: null, + totalCount: 43, + messages: [ + { + id: 25530793929740290, + serverId: 1, + channelId: 1, + sequence: 43, + sendMemberId: 1, + content: 'ㅊㅊ', + attachedFiles: null, + createdAt: '2025-03-12T10:50:15', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 25530790221975550, + serverId: 1, + channelId: 1, + sequence: 42, + sendMemberId: 1, + content: 'ㅁㅁ', + attachedFiles: null, + createdAt: '2025-03-12T10:50:14', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 25530787659255810, + serverId: 1, + channelId: 1, + sequence: 41, + sendMemberId: 1, + content: 'ㄴㄴ', + attachedFiles: null, + createdAt: '2025-03-12T10:50:13', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 25530784417058816, + serverId: 1, + channelId: 1, + sequence: 40, + sendMemberId: 1, + content: 'ㅇㅇ', + attachedFiles: null, + createdAt: '2025-03-12T10:50:12', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21146333658353664, + serverId: 1, + channelId: 1, + sequence: 39, + sendMemberId: 1, + content: 'ㅎㅇㅎㅇ', + attachedFiles: null, + createdAt: '2025-02-28T08:27:58', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21144030708764670, + serverId: 1, + channelId: 1, + sequence: 38, + sendMemberId: 1, + content: 'ㅇㅇ', + attachedFiles: null, + createdAt: '2025-02-28T08:18:49', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143788768595970, + serverId: 1, + channelId: 1, + sequence: 37, + sendMemberId: 2, + content: '최고최고', + attachedFiles: null, + createdAt: '2025-02-28T08:17:51', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143777653690370, + serverId: 1, + channelId: 1, + sequence: 36, + sendMemberId: 2, + content: '너무 고생하셨어요', + attachedFiles: null, + createdAt: '2025-02-28T08:17:48', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143765872021504, + serverId: 1, + channelId: 1, + sequence: 35, + sendMemberId: 1, + content: '고생하셨어요/!!!!', + attachedFiles: null, + createdAt: '2025-02-28T08:17:46', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143754098479104, + serverId: 1, + channelId: 1, + sequence: 34, + sendMemberId: 2, + content: '와', + attachedFiles: null, + createdAt: '2025-02-28T08:17:43', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143743877091330, + serverId: 1, + channelId: 1, + sequence: 33, + sendMemberId: 1, + content: '~!!!!!!!!', + attachedFiles: null, + createdAt: '2025-02-28T08:17:40', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143743147151360, + serverId: 1, + channelId: 1, + sequence: 32, + sendMemberId: 2, + content: '!!', + attachedFiles: null, + createdAt: '2025-02-28T08:17:40', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143661521801216, + serverId: 1, + channelId: 1, + sequence: 31, + sendMemberId: 2, + content: '안녕', + attachedFiles: null, + createdAt: '2025-02-28T08:17:21', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143641234083840, + serverId: 1, + channelId: 1, + sequence: 30, + sendMemberId: 1, + content: '안녕하세여~~~', + attachedFiles: null, + createdAt: '2025-02-28T08:17:16', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21143045399646210, + serverId: 1, + channelId: 1, + sequence: 29, + sendMemberId: 2, + content: 'ㅎㄹㅎㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:14:54', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21142627097382910, + serverId: 1, + channelId: 1, + sequence: 28, + sendMemberId: 1, + content: 'ㅗ어로', + attachedFiles: null, + createdAt: '2025-02-28T08:13:15.112499563', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141833694580736, + serverId: 1, + channelId: 1, + sequence: 27, + sendMemberId: 2, + content: 'ㄹㄹㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:10:05', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141828049047550, + serverId: 1, + channelId: 1, + sequence: 26, + sendMemberId: 2, + content: 'ㅇㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:10:04', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141826664927230, + serverId: 1, + channelId: 1, + sequence: 25, + sendMemberId: 2, + content: 'ㅇㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:10:03', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141824920096770, + serverId: 1, + channelId: 1, + sequence: 24, + sendMemberId: 2, + content: 'ㅇㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:10:03', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141822931996670, + serverId: 1, + channelId: 1, + sequence: 23, + sendMemberId: 2, + content: 'ㅇㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:10:02', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141816485351424, + serverId: 1, + channelId: 1, + sequence: 22, + sendMemberId: 2, + content: 'ㄹㅇㄹㅇㄹㅇㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:10:01', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141777302163456, + serverId: 1, + channelId: 1, + sequence: 21, + sendMemberId: 2, + content: 'ㄹㄹㄹㄹㄹㄹㄹㄹㄹㄹ', + attachedFiles: null, + createdAt: '2025-02-28T08:09:52', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21141706665758720, + serverId: 1, + channelId: 1, + sequence: 20, + sendMemberId: 1, + content: 'ㅇㅇㅇ', + attachedFiles: null, + createdAt: '2025-02-28T08:09:35', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21140260620734464, + serverId: 1, + channelId: 1, + sequence: 19, + sendMemberId: 1, + content: '하위', + attachedFiles: null, + createdAt: '2025-02-28T08:03:50.900628893', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21140143947780096, + serverId: 1, + channelId: 1, + sequence: 18, + sendMemberId: 1, + content: 'dbdb', + attachedFiles: null, + createdAt: '2025-02-28T08:03:23.083932832', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21140130404372480, + serverId: 1, + channelId: 1, + sequence: 17, + sendMemberId: 1, + content: 'dndbd', + attachedFiles: null, + createdAt: '2025-02-28T08:03:19.853917625', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21140124486340610, + serverId: 1, + channelId: 1, + sequence: 16, + sendMemberId: 2, + content: '안녕하세요', + attachedFiles: null, + createdAt: '2025-02-28T08:03:17', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21140108015177730, + serverId: 1, + channelId: 1, + sequence: 15, + sendMemberId: 1, + content: 'dndbd', + attachedFiles: null, + createdAt: '2025-02-28T08:03:14.517509908', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21139739113558016, + serverId: 1, + channelId: 1, + sequence: 14, + sendMemberId: 1, + content: 'sndndbd', + attachedFiles: null, + createdAt: '2025-02-28T08:01:46.563220205', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21139644364230656, + serverId: 1, + channelId: 1, + sequence: 13, + sendMemberId: 1, + content: '나야', + attachedFiles: null, + createdAt: '2025-02-28T08:01:23', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21139582162702336, + serverId: 1, + channelId: 1, + sequence: 12, + sendMemberId: 1, + content: 'dndn', + attachedFiles: null, + createdAt: '2025-02-28T08:01:09.14379056', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21139570905190400, + serverId: 1, + channelId: 1, + sequence: 11, + sendMemberId: 1, + content: 'djsjd', + attachedFiles: null, + createdAt: '2025-02-28T08:01:06.458949602', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21010532496838656, + serverId: 1, + channelId: 1, + sequence: 10, + sendMemberId: 4, + content: '네.. 저. ㅕ기있어요 안녕하세요', + attachedFiles: null, + createdAt: '2025-02-27T23:28:20', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 21010504084492290, + serverId: 1, + channelId: 1, + sequence: 9, + sendMemberId: 1, + content: '안녕하세요 테3님 거기계신가요', + attachedFiles: null, + createdAt: '2025-02-27T23:28:14', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20892764027883520, + serverId: 1, + channelId: 1, + sequence: 8, + sendMemberId: 3, + content: 'fh', + attachedFiles: null, + createdAt: '2025-02-27T15:40:23.113944717', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20879352082337790, + serverId: 1, + channelId: 1, + sequence: 7, + sendMemberId: 1, + content: '안녕하세여', + attachedFiles: null, + createdAt: '2025-02-27T14:47:04', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20879320998481920, + serverId: 1, + channelId: 1, + sequence: 6, + sendMemberId: 3, + content: 'dn', + attachedFiles: null, + createdAt: '2025-02-27T14:46:58.046718825', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20870099116494850, + serverId: 1, + channelId: 1, + sequence: 5, + sendMemberId: 1, + content: '하이루', + attachedFiles: null, + createdAt: '2025-02-27T14:10:18', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20870082309787650, + serverId: 1, + channelId: 1, + sequence: 4, + sendMemberId: 2, + content: '안녕ㄴ테스트2', + attachedFiles: null, + createdAt: '2025-02-27T14:10:14', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20870062655410176, + serverId: 1, + channelId: 1, + sequence: 3, + sendMemberId: 1, + content: '안녕테스트야', + attachedFiles: null, + createdAt: '2025-02-27T14:10:10', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20870042975604736, + serverId: 1, + channelId: 1, + sequence: 2, + sendMemberId: 2, + content: '안녕 하이루', + attachedFiles: null, + createdAt: '2025-02-27T14:10:05', + updatedAt: null, + messageType: 'TEXT', + deleted: false + }, + { + id: 20868769161416704, + serverId: 1, + channelId: 1, + sequence: 1, + sendMemberId: 1, + content: '안녕하세요 여기 하이루 서버인가연', + attachedFiles: null, + createdAt: '2025-02-27T14:05:01', + updatedAt: null, + messageType: 'TEXT', + deleted: false + } + ] +} diff --git a/src/frontend/src/__mock__/data/service.mock.ts b/src/frontend/src/__mock__/data/service.mock.ts new file mode 100644 index 00000000..82e5fd46 --- /dev/null +++ b/src/frontend/src/__mock__/data/service.mock.ts @@ -0,0 +1,150 @@ +import { CommonResponseType } from '@/apis/schema/types/common' +import { + GetServerMemebersResponseSchema, + GetServersResponseSchema +} from '@/apis/schema/types/service' + +export const mockServersData = [ + { + serverId: 1, + serverName: 'Server 1', + serverImageUrl: null + } +] as { + serverId: number + serverName: string + serverImageUrl: string | null +}[] + +export const mockCategoriesData = [ + { + categoryId: 1, + categoryName: '채팅 채널', + position: 1 + }, + { + categoryId: 2, + categoryName: '음성 채널', + position: 2 + } +] + +export const mockChannelsData = [ + { + channelId: 1, + categoryId: 1, + channelName: '일반', + position: 1, + channelType: 'CHAT', + privateStatus: false, + channelMemberIdList: [1, 2, 3, 4], + lastSequence: 0 + }, + { + channelId: 2, + categoryId: 2, + channelName: '일반', + position: 1, + channelType: 'VOICE', + privateStatus: false, + channelMemberIdList: [1, 2, 3, 4], + lastSequence: 0 + }, + { + channelId: 13, + categoryId: 1, + channelName: '채널2', + position: 2, + channelType: 'CHAT', + privateStatus: false, + channelMemberIdList: [1, 2, 3, 4], + lastSequence: 2 + }, + { + channelId: 16, + categoryId: 1, + channelName: 'ㅁㄴㅇㅁㄴㅇ', + position: 3, + channelType: 'CHAT', + privateStatus: false, + channelMemberIdList: [1, 2, 3, 4], + lastSequence: 0 + } +] as { + channelId: number + channelName: string + channelType: string + privateStatus: boolean + categoryId: number | null + position: number + channelMemberIdList: number[] + lastSequence: number +}[] + +export const mockServers = { + code: 'SERVER_LIST_SUCCESS', + message: '서버 목록 조회 성공', + result: { + serverId: 1, + serverName: 'Server 1', + ownerId: 1, + serverImageUrl: null, + categoryInfoList: mockCategoriesData, + channelInfoList: mockChannelsData + } +} + +export const mockServersList: CommonResponseType = { + code: 'SERVER_LIST_SUCCESS', + message: '서버 목록 조회 성공', + result: { servers: mockServersData } +} + +export const mockServerMembers: CommonResponseType = { + code: 'SERVER_MEMBER_LIST_SUCCESS', + message: '서버 멤버 목록 조회 성공', + result: { + serverId: 1, + serverMemberInfoList: [ + { + memberId: 1, + nickName: '테스트1', + avatarUrl: null, + bannerUrl: null, + joinAt: '2025-02-27T14:04:50.258047', + globalStatus: 'ONLINE' + }, + { + memberId: 2, + nickName: '테슷트2', + avatarUrl: null, + bannerUrl: null, + joinAt: '2025-02-27T14:09:57.037225', + globalStatus: 'OFFLINE' + }, + { + memberId: 3, + nickName: '서테스트', + avatarUrl: null, + bannerUrl: null, + joinAt: '2025-02-27T14:46:42.392247', + globalStatus: 'OFFLINE' + }, + { + memberId: 4, + nickName: '테3', + avatarUrl: null, + bannerUrl: null, + joinAt: '2025-02-27T23:27:00.269422', + globalStatus: 'OFFLINE' + } + ] + } +} + +export const mockCategory = { + categoryId: 1, + serverId: 1, + categoryName: '카테고리 1', + categoryPosition: 1 +} diff --git a/src/frontend/src/__mock__/data/user.mock.ts b/src/frontend/src/__mock__/data/user.mock.ts new file mode 100644 index 00000000..1c9648fe --- /dev/null +++ b/src/frontend/src/__mock__/data/user.mock.ts @@ -0,0 +1,41 @@ +import { User } from '@sentry/react' + +export const userMock = { + id: 1, + name: 'test', + nickname: 'test', + email: 'test@test.com', + birthdate: '2021-01-01', + avatarUrl: null, + bannerUrl: null, + introduce: null, + customPresenceStatus: 'ONLINE', + lastAccessAt: '2021-01-01T00:00:00.000Z' +} as User + +export const friendsMock = [ + { + friendId: 1, + memberId: 2, + memberName: '김테스트', + memberNickname: '테슷트2', + memberAvatarUrl: null, + memberBannerUrl: null, + memberIntroduce: null, + memberEmail: 'test@test2.com', + globalStatus: 'OFFLINE', + createdAt: null + }, + { + friendId: 6, + memberId: 4, + memberName: '테3', + memberNickname: '테3', + memberAvatarUrl: null, + memberBannerUrl: null, + memberIntroduce: null, + memberEmail: 'test@test3.com', + globalStatus: 'OFFLINE', + createdAt: null + } +] diff --git a/src/frontend/src/__mock__/handlers/auth.handler.ts b/src/frontend/src/__mock__/handlers/auth.handler.ts index 7e5f362a..57d5311a 100644 --- a/src/frontend/src/__mock__/handlers/auth.handler.ts +++ b/src/frontend/src/__mock__/handlers/auth.handler.ts @@ -2,21 +2,36 @@ import { http, HttpResponse } from 'msw' import { SERVER_URL } from '@/constants/env' -import { loginMock, registerMock } from '../data/auth.mock' +import { + healthCheckMock, + loginMock, + logoutMock, + refreshTokenMock, + registerMock, + statusCheckMock, + verifyTokenMock +} from '../data/auth.mock' export const authHandler = [ + http.get(`${SERVER_URL}/auth-server/auth/status-check`, () => { + return new HttpResponse(JSON.stringify(statusCheckMock)) + }), http.post(`${SERVER_URL}/auth-server/auth/register`, () => { - return new HttpResponse(JSON.stringify(registerMock), { - headers: { - 'Set-Cookie': 'refreshToken=1234567890; HttpOnly; Secure; SameSite=Strict' - } - }) + return new HttpResponse(JSON.stringify(registerMock)) }), http.post(`${SERVER_URL}/auth-server/auth/login`, () => { - return new HttpResponse(JSON.stringify(loginMock), { - headers: { - 'Set-Cookie': 'refreshToken=1234567890; HttpOnly; Secure; SameSite=Strict' - } - }) + return new HttpResponse(JSON.stringify(loginMock)) + }), + http.post(`${SERVER_URL}/auth-server/auth/logout`, () => { + return new HttpResponse(JSON.stringify(logoutMock)) + }), + http.post(`${SERVER_URL}/auth-server/auth/refresh`, () => { + return new HttpResponse(JSON.stringify(refreshTokenMock)) + }), + http.post(`${SERVER_URL}/auth-server/auth/verify-token`, () => { + return new HttpResponse(JSON.stringify(verifyTokenMock)) + }), + http.get(`${SERVER_URL}/auth-server/auth/health-check`, () => { + return new HttpResponse(JSON.stringify(healthCheckMock)) }) ] diff --git a/src/frontend/src/__mock__/handlers/chatting.handler.ts b/src/frontend/src/__mock__/handlers/chatting.handler.ts new file mode 100644 index 00000000..a75b8ab4 --- /dev/null +++ b/src/frontend/src/__mock__/handlers/chatting.handler.ts @@ -0,0 +1,18 @@ +import { WebSocketLink, ws } from 'msw' + +import { CHAT_SERVER_URL } from '@/constants/env' +import { log } from '@/utils/log' + +const chattingServer: WebSocketLink = ws.link(CHAT_SERVER_URL) + +export const chattingHandlers = [ + chattingServer.addEventListener('connection', ({ client }) => { + log('✅ WebSocket connection initiated') + + // 클라이언트 메시지를 서버로 전달 + client.addEventListener('message', (event) => { + // 연결 시도만 하고 실제 전송은 하지 않음 + log('Message from client:', event.data) + }) + }) +] diff --git a/src/frontend/src/__mock__/handlers/search.handler.ts b/src/frontend/src/__mock__/handlers/search.handler.ts new file mode 100644 index 00000000..6ee32d19 --- /dev/null +++ b/src/frontend/src/__mock__/handlers/search.handler.ts @@ -0,0 +1,75 @@ +import { http, HttpResponse } from 'msw' + +import { + GetAllServerUnreadCountResponseSchema, + GetHistoryChattingMessageResponseSchema, + GetSingleServerUnreadCountResponseSchema +} from '@/apis/schema/types/search' +import { SERVER_URL } from '@/constants/env' + +import { + mockMessages, + mockMessagesByChannel, + mockUnreadChannels, + mockUnreadMessages +} from '../data/search.handler' +const copyMockMessages = [...mockMessages] +const copyUnreadChannels = structuredClone(mockUnreadChannels) +const copyUnreadMessages = structuredClone(mockUnreadMessages) +const copyMessagesByChannel = structuredClone(mockMessagesByChannel) + +const SEARCH_SERVER_PATH = `/search-server/server` +const SEARCH_HISTORY_PATH = `/search-server/history` + +export const searchHandler = [ + http.post(`${SERVER_URL}${SEARCH_SERVER_PATH}/:serverId`, ({ params }) => { + const { serverId } = params as { serverId: string } + return HttpResponse.json({ + code: 'MESSAGE_FOUND', + message: 'Message found', + result: { + serverId: Number(serverId), + totalCount: copyMockMessages.length, + messages: copyMockMessages + } as GetHistoryChattingMessageResponseSchema + }) + }), + http.get(`${SERVER_URL}${SEARCH_HISTORY_PATH}/unread/server/:serverId`, ({ params }) => { + const { serverId } = params as { serverId: string } + return HttpResponse.json({ + code: 'UNREAD_MESSAGE_FOUND', + message: 'Unread message found', + result: { + serverId: Number(serverId), + channels: copyUnreadChannels, + totalUnread: copyUnreadChannels.reduce((acc, curr) => acc + curr.unreadCount, 0) + } as GetSingleServerUnreadCountResponseSchema + }) + }), + http.get(`${SERVER_URL}${SEARCH_HISTORY_PATH}/unread/server/all`, () => { + return HttpResponse.json({ + code: 'UNREAD_MESSAGE_FOUND', + message: 'Unread message found', + result: { + memberId: 1, + totalServerCount: copyUnreadMessages.length, + serverInfos: copyUnreadMessages + } as GetAllServerUnreadCountResponseSchema + }) + }), + http.get( + `${SERVER_URL}${SEARCH_HISTORY_PATH}/server/:serverId/channel/:channelId/messages`, + ({ params }) => { + const { serverId, channelId } = params as { serverId: string; channelId: string } + return HttpResponse.json({ + code: 'UNREAD_MESSAGE_FOUND', + message: 'Unread message found', + result: { + ...copyMessagesByChannel, + serverId: Number(serverId), + channelId: Number(channelId) + } + }) + } + ) +] diff --git a/src/frontend/src/__mock__/handlers/service.handler.ts b/src/frontend/src/__mock__/handlers/service.handler.ts new file mode 100644 index 00000000..f1a9087a --- /dev/null +++ b/src/frontend/src/__mock__/handlers/service.handler.ts @@ -0,0 +1,313 @@ +import { http, HttpResponse } from 'msw' + +import { + CreateCategoryRequestSchema, + CreateChannelRequestSchema, + UpdateCategoryRequestSchema, + UpdateChannelRequestSchema, + UpdateServerRequestSchema +} from '@/apis/schema/types/service' +import { SERVER_URL } from '@/constants/env' + +import { + mockCategoriesData, + mockCategory, + mockChannelsData, + mockServerMembers, + mockServers, + mockServersData +} from '../data/service.mock' + +let copyMockServers = [...mockServersData] +let copyMockCategories = [...mockCategoriesData] +let copyMockChannels = [...mockChannelsData] + +const SERVER_PATH = `/service-server/servers` +const CHANNEL_PATH = `/service-server/channels` +const CATEGORY_PATH = `/service-server/categories` + +export const serviceHandler = [ + http.delete(`${SERVER_URL}${SERVER_PATH}/:serverId`, ({ params }) => { + const serverId = Number(params.serverId) + const server = copyMockServers.find((server) => server.serverId === serverId) + if (!server) { + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_NOT_FOUND', + message: 'Server not found' + }), + { status: 404 } + ) + } + + copyMockServers = copyMockServers.filter((server) => server.serverId !== serverId) + + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_DELETED', + message: 'Server deleted', + result: { + serverId + } + }) + ) + }), + http.delete(`${SERVER_URL}${SERVER_PATH}/:serverId/withdraw`, ({ params }) => { + const serverId = Number(params.serverId) + const server = copyMockServers.find((server) => server.serverId === serverId) + if (!server) { + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_NOT_FOUND', + message: 'Server not found' + }), + { status: 404 } + ) + } + copyMockServers = copyMockServers.filter((server) => server.serverId !== serverId) + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_WITHDRAWN', + message: 'Server withdrawn', + result: { + serverId + } + }) + ) + }), + http.get(`${SERVER_URL}${SERVER_PATH}/:serverId`, () => { + return new HttpResponse(JSON.stringify(mockServers)) + }), + http.get(`${SERVER_URL}${SERVER_PATH}`, () => { + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_LIST_SUCCESS', + message: 'Server list success', + result: { + servers: copyMockServers + } + }) + ) + }), + http.get(`${SERVER_URL}${SERVER_PATH}/:serverId/members`, () => { + return new HttpResponse(JSON.stringify(mockServerMembers)) + }), + http.get(`${SERVER_URL}${SERVER_PATH}/:serverId/channels/info`, () => { + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_INFO_FOUND', + message: 'Channel info found', + result: { + channels: copyMockChannels + } + }) + ) + }), + http.post(`${SERVER_URL}${SERVER_PATH}`, () => { + const mockServerId = Math.floor(Math.random() * 1000000) + copyMockServers.push({ + serverId: mockServerId, + serverName: 'test', + serverImageUrl: null + }) + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_CREATED', + message: 'Server created', + result: { + serverId: mockServerId + } + }) + ) + }), + http.post(`${SERVER_URL}${SERVER_PATH}/:serverId/participate`, ({ params }) => { + const { serverId } = params as { serverId: string } + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_PARTICIPATED', + message: 'Server participated', + result: { + serverId + } + }) + ) + }), + http.post(`${SERVER_URL}${SERVER_PATH}/:serverId/invite`, ({ params }) => { + const { serverId } = params as { serverId: string } + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_INVITED', + message: 'Server invited', + result: { serverId } + }) + ) + }), + http.put(`${SERVER_URL}${SERVER_PATH}/:serverId`, async ({ params, request }) => { + const { serverId } = params as { serverId: string } + const { serverName, serverImageUrl } = (await request.json()) as UpdateServerRequestSchema + const server = copyMockServers.find((server) => server.serverId === Number(serverId)) + if (!server) { + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_NOT_FOUND', + message: 'Server not found' + }), + { status: 404 } + ) + } + server.serverName = serverName + server.serverImageUrl = serverImageUrl ?? null + return new HttpResponse( + JSON.stringify({ + code: 'SERVER_UPDATED', + message: 'Server updated', + result: { serverId } + }) + ) + }), + http.delete(`${SERVER_URL}${CHANNEL_PATH}/:channelId`, ({ params }) => { + const { channelId } = params as { channelId: string } + copyMockChannels = copyMockChannels.filter((channel) => channel.channelId !== Number(channelId)) + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_DELETED', + message: 'Channel deleted', + result: { channelId: Number(channelId) } + }) + ) + }), + http.get(`${SERVER_URL}${CHANNEL_PATH}/:channelId`, ({ params }) => { + const { channelId } = params as { channelId: string } + const channel = copyMockChannels.find((channel) => channel.channelId === Number(channelId)) + if (!channel) { + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_NOT_FOUND', + message: 'Channel not found' + }), + { status: 404 } + ) + } + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_FOUND', + message: 'Channel found', + result: { channel } + }) + ) + }), + http.post(`${SERVER_URL}${CHANNEL_PATH}`, async ({ request }) => { + const { channelName, channelType, privateStatus, categoryId } = + (await request.json()) as CreateChannelRequestSchema + const mockChannelId = Math.floor(Math.random() * 1000000) + copyMockChannels.push({ + channelId: mockChannelId, + categoryId: categoryId ?? 1, + channelName, + channelType, + position: 1, + privateStatus: privateStatus ?? false, + channelMemberIdList: [1, 2, 3], + lastSequence: 0 + }) + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_CREATED', + message: 'Channel created', + result: { channelId: mockChannelId } + }) + ) + }), + http.put(`${SERVER_URL}${CHANNEL_PATH}/:channelId`, async ({ params, request }) => { + const { channelId } = params as { channelId: string } + const { channelName, privateStatus } = (await request.json()) as UpdateChannelRequestSchema + const channel = copyMockChannels.find((channel) => channel.channelId === Number(channelId)) + if (!channel) { + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_NOT_FOUND', + message: 'Channel not found' + }), + { status: 404 } + ) + } + channel.channelName = channelName + channel.privateStatus = privateStatus + return new HttpResponse( + JSON.stringify({ + code: 'CHANNEL_UPDATED', + message: 'Channel updated', + result: { channelId: Number(channelId) } + }) + ) + }), + http.delete(`${SERVER_URL}${CATEGORY_PATH}/:categoryId`, ({ params }) => { + const { categoryId } = params as { categoryId: string } + copyMockCategories = copyMockCategories.filter( + (category) => category.categoryId !== Number(categoryId) + ) + return new HttpResponse( + JSON.stringify({ + code: 'CATEGORY_DELETED', + message: 'Category deleted', + result: { categoryId: Number(categoryId) } + }) + ) + }), + http.get(`${SERVER_URL}${CATEGORY_PATH}/:categoryId`, ({ params }) => { + const { categoryId } = params as { categoryId: string } + const category = copyMockCategories.find( + (category) => category.categoryId === Number(categoryId) + ) + if (!category) { + return new HttpResponse( + JSON.stringify({ + code: 'CATEGORY_NOT_FOUND', + message: 'Category not found' + }), + { status: 404 } + ) + } + return new HttpResponse(JSON.stringify({ ...mockCategory, categoryId: Number(categoryId) })) + }), + http.post(`${SERVER_URL}${CATEGORY_PATH}`, async ({ request }) => { + const { categoryName } = (await request.json()) as CreateCategoryRequestSchema + const mockCategoryId = Math.floor(Math.random() * 1000000) + copyMockCategories.push({ + categoryId: mockCategoryId, + categoryName, + position: copyMockCategories.length + 1 + }) + return new HttpResponse( + JSON.stringify({ + code: 'CATEGORY_CREATED', + message: 'Category created', + result: { categoryId: mockCategoryId } + }) + ) + }), + http.put(`${SERVER_URL}${CATEGORY_PATH}/:categoryId`, async ({ params, request }) => { + const { categoryId } = params as { categoryId: string } + const { categoryName } = (await request.json()) as UpdateCategoryRequestSchema + const category = copyMockCategories.find( + (category) => category.categoryId === Number(categoryId) + ) + if (!category) { + return new HttpResponse( + JSON.stringify({ + code: 'CATEGORY_NOT_FOUND', + message: 'Category not found' + }), + { status: 404 } + ) + } + category.categoryName = categoryName + return new HttpResponse( + JSON.stringify({ + code: 'CATEGORY_UPDATED', + message: 'Category updated', + result: { categoryId: Number(categoryId) } + }) + ) + }) +] diff --git a/src/frontend/src/__mock__/handlers/signaling.handler.ts b/src/frontend/src/__mock__/handlers/signaling.handler.ts new file mode 100644 index 00000000..64858bf3 --- /dev/null +++ b/src/frontend/src/__mock__/handlers/signaling.handler.ts @@ -0,0 +1,64 @@ +import { toSocketIo } from '@mswjs/socket.io-binding' +import { WebSocketLink, ws } from 'msw' + +import { SIGNALING_NODE_SERVER_URL } from '@/constants/env' +import { log } from '@/utils/log' + +const signalingServer: WebSocketLink = ws.link( + `${SIGNALING_NODE_SERVER_URL}/socket.io/?EIO=4&transport=websocket` +) + +export const signalingHandlers = [ + signalingServer.addEventListener('connection', (connection) => { + log('✅ WebSocket connection initiated') + + const io = toSocketIo(connection) + const { client } = io + const mockSocketId = 'mock-' + Math.random().toString(36).substring(2, 9) + let roomName: string + let userId: string + + // 클라이언트 메시지를 서버로 전달 + connection.client.addEventListener('message', (event) => { + // 연결 시도만 하고 실제 전송은 하지 않음 + log('Message from client:', event.data) + }) + + // 클라이언트 이벤트 처리 + client.on('join_room', (_, data: unknown) => { + try { + const { roomName: room, userId: user } = data as { roomName: string; userId: string } + roomName = room + userId = user + log(`✅ User ${userId} joined room ${roomName}`) + client.emit('welcome', mockSocketId, userId) + } catch (e) { + log('Error in join_room:', e) + } + }) + + // 나머지 이벤트 핸들러들... + client.on('offer', (_, offer, remoteId) => { + log('Received offer for:', remoteId) + client.emit('offer', offer, mockSocketId, userId) + }) + + client.on('answer', (_, answer, remoteId) => { + log('Received answer for:', remoteId) + client.emit('answer', answer, mockSocketId, userId) + }) + + client.on('ice', (_, ice, remoteId) => { + log('Received ICE candidate for:', remoteId) + client.emit('ice', ice, mockSocketId, userId) + }) + + client.on('disconnect', () => { + log('❌ Client disconnected:', mockSocketId) + client.emit('user_left', mockSocketId) + }) + + // 초기 handshake 응답 + client.emit('connect') + }) +] diff --git a/src/frontend/src/__mock__/handlers/user.handler.ts b/src/frontend/src/__mock__/handlers/user.handler.ts new file mode 100644 index 00000000..d7383c55 --- /dev/null +++ b/src/frontend/src/__mock__/handlers/user.handler.ts @@ -0,0 +1,197 @@ +import { http, HttpResponse } from 'msw' + +import { + GetFriendListResponseSchema, + GetFriendPendingListResponseSchema, + GetUserSelfResponseSchema, + UpdateUserPresenceStatusRequestSchema, + UpdateUserRequestSchema +} from '@/apis/schema/types/user' +import { SERVER_URL } from '@/constants/env' + +import { friendsMock, userMock } from '../data/user.mock' + +const copyUserMock = { ...userMock } +let copyFriendsMock = [...friendsMock] +const copyFriendRequestsMock = [...friendsMock] + +const MEMBER_PATH = '/user-server/members' +const FRIEND_PATH = '/user-server/friends' + +export const userHandler = [ + http.delete(`${SERVER_URL}${MEMBER_PATH}`, () => { + return new HttpResponse( + JSON.stringify({ + code: 'MEMBER_DELETED', + message: 'Member deleted', + result: { memberId: 1 } + }) + ) + }), + http.get(`${SERVER_URL}${MEMBER_PATH}/:memberId`, ({ params }) => { + const { memberId } = params as { memberId: string } + return new HttpResponse( + JSON.stringify({ + code: 'MEMBER_FOUND', + message: 'Member found', + result: { + ...copyUserMock, + memberId: Number(memberId) + } + }) + ) + }), + http.get(`${SERVER_URL}${MEMBER_PATH}/self`, () => { + return new HttpResponse( + JSON.stringify({ + code: 'MEMBER_FOUND', + message: 'Member found', + result: userMock as GetUserSelfResponseSchema + }) + ) + }), + http.get(`${SERVER_URL}${MEMBER_PATH}/search`, ({ params }) => { + const { nickname } = params as { nickname: string } + return new HttpResponse( + JSON.stringify({ + code: 'MEMBER_FOUND', + message: 'Member found', + result: { + ...copyUserMock, + nickname: nickname + } + }) + ) + }), + http.patch(`${SERVER_URL}${MEMBER_PATH}/:memberId`, async ({ params, request }) => { + const { memberId } = params as { memberId: string } + const { name, nickname, birthdate, avatarUrl, bannerUrl, introduce } = + (await request.json()) as UpdateUserRequestSchema + copyUserMock.name = name + copyUserMock.nickname = nickname + copyUserMock.birthdate = birthdate + copyUserMock.avatarUrl = avatarUrl ?? null + copyUserMock.bannerUrl = bannerUrl ?? null + copyUserMock.introduce = introduce ?? null + return new HttpResponse( + JSON.stringify({ + code: 'MEMBER_UPDATED', + message: 'Member updated', + result: { + ...copyUserMock, + memberId: Number(memberId) + } + }) + ) + }), + http.patch(`${SERVER_URL}${MEMBER_PATH}/presence`, async ({ request }) => { + const { customPresenceStatus } = (await request.json()) as UpdateUserPresenceStatusRequestSchema + copyUserMock.presenceStatus = customPresenceStatus + return new HttpResponse( + JSON.stringify({ + code: 'MEMBER_UPDATED', + message: 'Member updated', + result: copyUserMock + }) + ) + }), + http.delete(`${SERVER_URL}${FRIEND_PATH}/:friendId`, ({ params }) => { + const { friendId } = params as { friendId: string } + copyFriendsMock = copyFriendsMock.filter((friend) => friend.friendId !== Number(friendId)) + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_DELETED', + message: 'Friend deleted', + result: { + id: Number(friendId) + } + }) + ) + }), + http.delete(`${SERVER_URL}${FRIEND_PATH}/:friendId/cancel`, ({ params }) => { + const { friendId } = params as { friendId: string } + copyFriendsMock = copyFriendsMock.filter((friend) => friend.friendId !== Number(friendId)) + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_CANCELLED', + message: 'Friend cancelled', + result: { + id: Number(friendId) + } + }) + ) + }), + http.get(`${SERVER_URL}${FRIEND_PATH}`, ({ params }) => { + const { memberId } = params as { memberId: string } + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_FOUND', + message: 'Friend found', + result: { + memberId: Number(memberId), + friendsCount: copyFriendRequestsMock.length, + friends: copyFriendRequestsMock + } as GetFriendListResponseSchema + }) + ) + }), + http.get(`${SERVER_URL}${FRIEND_PATH}/pending`, ({ params }) => { + const { memberId } = params as { memberId: string } + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_FOUND', + message: 'Friend found', + result: { + memberId: Number(memberId), + pendingFriendsCount: copyFriendRequestsMock.length, + receivePendingFriendsCount: copyFriendRequestsMock.length, + sendPendingFriends: copyFriendRequestsMock, + receivePendingFriends: copyFriendRequestsMock + } as GetFriendPendingListResponseSchema + }) + ) + }), + http.patch(`${SERVER_URL}${FRIEND_PATH}/:friendId/declined`, ({ params }) => { + const { friendId } = params as { friendId: string } + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_PENDING', + message: 'Friend pending', + result: { + id: Number(friendId), + friendStatus: 'PENDING' + } + }) + ) + }), + http.patch(`${SERVER_URL}${FRIEND_PATH}/:friendId/accepted`, ({ params }) => { + const { friendId } = params as { friendId: string } + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_ACCEPTED', + message: 'Friend accepted', + result: { + id: Number(friendId), + friendStatus: 'ACCEPTED' + } + }) + ) + }), + http.post(`${SERVER_URL}${FRIEND_PATH}`, () => { + const randomId = Math.floor(Math.random() * 1000000) + const randomFromMemberId = Math.floor(Math.random() * 1000000) + const randomToMemberId = Math.floor(Math.random() * 1000000) + return new HttpResponse( + JSON.stringify({ + code: 'FRIEND_REQUESTED', + message: 'Friend requested', + result: { + friendId: randomId, + fromMemberId: randomFromMemberId, + toMemeberId: randomToMemberId, + createdAt: new Date().toISOString() + } + }) + ) + }) +] diff --git a/src/frontend/src/__mock__/worker.ts b/src/frontend/src/__mock__/worker.ts index fd9e5f9a..c2f731f1 100644 --- a/src/frontend/src/__mock__/worker.ts +++ b/src/frontend/src/__mock__/worker.ts @@ -1,5 +1,17 @@ import { setupWorker } from 'msw/browser' import { authHandler } from './handlers/auth.handler' +import { chattingHandlers } from './handlers/chatting.handler' +import { searchHandler } from './handlers/search.handler' +import { serviceHandler } from './handlers/service.handler' +import { signalingHandlers } from './handlers/signaling.handler' +import { userHandler } from './handlers/user.handler' -export const worker = setupWorker(...authHandler) +export const worker = setupWorker( + ...authHandler, + ...serviceHandler, + ...userHandler, + ...searchHandler, + ...signalingHandlers, + ...chattingHandlers +) diff --git a/src/frontend/src/apis/schema/common.ts b/src/frontend/src/apis/schema/common.ts index 4108a9c2..31394228 100644 --- a/src/frontend/src/apis/schema/common.ts +++ b/src/frontend/src/apis/schema/common.ts @@ -1,7 +1,7 @@ import { z } from 'zod' const commonResponseSchema = z.object({ - code: z.number(), + code: z.string(), message: z.string(), result: z.optional(z.any()) }) diff --git a/src/frontend/src/apis/schema/types/search.ts b/src/frontend/src/apis/schema/types/search.ts index 244464db..584f8c99 100644 --- a/src/frontend/src/apis/schema/types/search.ts +++ b/src/frontend/src/apis/schema/types/search.ts @@ -30,7 +30,6 @@ export interface GetHistoryChattingMessagesRequestSchema { } export interface GetHistoryChattingMessageResponseSchema { - id: number serverId: number channelId: number lastMessageId: number diff --git a/src/frontend/src/apis/schema/types/service.ts b/src/frontend/src/apis/schema/types/service.ts index 4181dde3..801758e6 100644 --- a/src/frontend/src/apis/schema/types/service.ts +++ b/src/frontend/src/apis/schema/types/service.ts @@ -71,6 +71,7 @@ export interface GetServerMemebersResponseSchema { nickName: string avatarUrl: string | null bannerUrl: string | null + joinAt: string globalStatus: CustomPresenceStatus }[] } diff --git a/src/frontend/src/components/auth-input/auth-input.spec.tsx b/src/frontend/src/components/auth-input/auth-input.spec.tsx new file mode 100644 index 00000000..580df71c --- /dev/null +++ b/src/frontend/src/components/auth-input/auth-input.spec.tsx @@ -0,0 +1,44 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import AuthInput from './index' + +describe('AuthInput', () => { + it('should render', () => { + render() + + const label = screen.getByText('Email') + expect(label).not.toBeNull() + }) + + it('should render error message', () => { + render( + + ) + + const error = screen.getByText('Invalid email') + expect(error).not.toBeNull() + }) + + it('should render required message', () => { + render( + + ) + + const required = screen.getByText('*') + expect(required).not.toBeNull() + }) + + it('should render input', () => { + render() + + const input = screen.getByRole('textbox') + expect(input).not.toBeNull() + }) +}) diff --git a/src/frontend/src/components/avatar/avatar.spec.tsx b/src/frontend/src/components/avatar/avatar.spec.tsx new file mode 100644 index 00000000..3def9ab7 --- /dev/null +++ b/src/frontend/src/components/avatar/avatar.spec.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import Avatar from './index' + +describe('Avatar', () => { + it('should render without avatar url', () => { + render( + + ) + const img = screen.getByRole('img') + expect(img.getAttribute('src')).toBe('/image/common/default-avatar.png') + }) + + it('should render with avatar url', () => { + render( + + ) + const img = screen.getByRole('img') + expect(img.getAttribute('src')).toBe('/homepage/hero-character.png') + }) +}) diff --git a/src/frontend/src/components/chat-area/index.tsx b/src/frontend/src/components/chat-area/index.tsx index cb201bc9..bb6a005b 100644 --- a/src/frontend/src/components/chat-area/index.tsx +++ b/src/frontend/src/components/chat-area/index.tsx @@ -51,7 +51,7 @@ function ChatArea({ if (historyMessages && chatKey) { setMessages(Number(chatKey), historyMessages) } - }, [historyMessages, chatKey]) + }, [historyMessages, chatKey, setMessages]) useEffect(() => { if (messagesRef.current) { diff --git a/src/frontend/src/components/check-box/check-box.spec.tsx b/src/frontend/src/components/check-box/check-box.spec.tsx new file mode 100644 index 00000000..fde68871 --- /dev/null +++ b/src/frontend/src/components/check-box/check-box.spec.tsx @@ -0,0 +1,62 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import CheckBox from './index' + +describe('CheckBox', () => { + const onChange = vi.fn() + it('should render', () => { + render( + + ) + + const checkBox = screen.getByRole('checkbox') + expect(checkBox).not.toBeNull() + }) + + it('should render checked', () => { + render( + + ) + + const checkBox = screen.getByRole('checkbox') + expect(checkBox).not.toBeNull() + }) + + it('should render label', () => { + render( + + ) + + const label = screen.getByText('Checkbox') + expect(label).not.toBeNull() + }) + + it('should call onChange', () => { + const onChange = vi.fn() + render( + + ) + + const checkBox = screen.getByRole('checkbox') + fireEvent.click(checkBox) + + expect(onChange).toHaveBeenCalled() + }) +}) diff --git a/src/frontend/src/components/date-input/dete-input.spec.tsx b/src/frontend/src/components/date-input/dete-input.spec.tsx new file mode 100644 index 00000000..08722121 --- /dev/null +++ b/src/frontend/src/components/date-input/dete-input.spec.tsx @@ -0,0 +1,74 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' + +import DateInput from './index' + +describe('DateInput', () => { + const setDate = vi.fn() + it('should render', () => { + render( + + ) + + const label = screen.getByText('Date') + expect(label).not.toBeNull() + }) + + it('should call setDate', () => { + render( + + ) + + const selectBoxYear = screen.getAllByRole('combobox')[0] + fireEvent.click(selectBoxYear) + + const option1 = screen.getByText('2025') + fireEvent.click(option1) + + const selectBoxMonth = screen.getAllByRole('combobox')[1] + fireEvent.click(selectBoxMonth) + + const option2 = screen.getByText('1월') + fireEvent.click(option2) + + const selectBoxDay = screen.getAllByRole('combobox')[2] + fireEvent.click(selectBoxDay) + + const option3 = screen.getByText('30') + fireEvent.click(option3) + + expect(setDate).toHaveBeenCalledWith('2025-01-30') + }) + + it('should render error', () => { + render( + + ) + + const error = screen.getByText('Error') + expect(error).not.toBeNull() + }) + + it('should render required', () => { + render( + + ) + + const required = screen.getByText('*') + expect(required).not.toBeNull() + }) +}) diff --git a/src/frontend/src/components/header-tool-bar/header-tool-bar.stories.tsx b/src/frontend/src/components/header-tool-bar/header-tool-bar.stories.tsx index 9050ee55..f04ff45d 100644 --- a/src/frontend/src/components/header-tool-bar/header-tool-bar.stories.tsx +++ b/src/frontend/src/components/header-tool-bar/header-tool-bar.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react' +import { fn } from '@storybook/test' import HeaderToolBar from '.' @@ -27,11 +28,11 @@ export const PrimaryHeaderToolBar: Story = { args: { type: 'DEFAULT', isStatusBarOpen: false, - onToggleStatusBar: () => console.log('Status bar toggled'), + onToggleStatusBar: () => fn(), searchProps: { value: '검색', - onChange: () => console.log('검색'), - handleClear: () => console.log('검색 초기화'), + onChange: () => fn(), + handleClear: () => fn(), placeholder: '검색' } } @@ -40,7 +41,7 @@ export const PrimaryHeaderToolBar: Story = { export const VoiceChannelHeaderToolBar: Story = { args: { type: 'VOICE', - onClose: () => console.log('Voice channel closed') + onClose: () => fn() } } @@ -49,8 +50,8 @@ export const DirectMessageHeaderToolBar: Story = { type: 'DM', searchProps: { value: '검색', - onChange: () => console.log('검색'), - handleClear: () => console.log('검색 초기화'), + onChange: () => fn(), + handleClear: () => fn(), placeholder: '검색' } } @@ -60,11 +61,11 @@ export const StatusBarOpenHeaderToolBar: Story = { args: { type: 'DEFAULT', isStatusBarOpen: true, - onToggleStatusBar: () => console.log('Status bar toggled'), + onToggleStatusBar: () => fn(), searchProps: { value: '검색', - onChange: () => console.log('검색'), - handleClear: () => console.log('검색 초기화'), + onChange: () => fn(), + handleClear: () => fn(), placeholder: '검색' } } diff --git a/src/frontend/src/components/select-box/index.tsx b/src/frontend/src/components/select-box/index.tsx index 6225f117..e1b2e7b2 100644 --- a/src/frontend/src/components/select-box/index.tsx +++ b/src/frontend/src/components/select-box/index.tsx @@ -91,10 +91,13 @@ const SelectBox = ({ ) : null}
-
+
+ ({ {...props} />