diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml deleted file mode 100644 index 3e769663..00000000 --- a/.github/workflows/node.js.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Node.js CI - -on: - push: - branches: ['merge/front'] - pull_request: - branches: ['merge/front'] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.x] - steps: - # 1. 리포지토리 코드 체크아웃 - - name: Checkout repository - uses: actions/checkout@v4 - - # 2. pnpm 설치 (버전 9.15.4) - - name: Set up pnpm - uses: pnpm/action-setup@v2 - with: - version: 9.15.4 - - # 3. Node.js 환경 설정 (노드 버전 '18', pnpm 캐싱 사용) - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'pnpm' - cache-dependency-path: ./frontend/package-lock.json - - # 4. 프론트엔드 폴더에서 의존성 설치 - - name: Install dependencies in frontend - working-directory: frontend - run: pnpm install - - # 5. 프론트엔드 폴더에서 테스트 실행 - - name: Run build in frontend - working-directory: frontend - run: pnpm build - - # 6. 빌드 결과물이 있는 폴더를 AWS S3로 배포 - # (리포지토리 루트 기준에서 SOURCE_DIR을 지정) - - name: Deploy to S3 - uses: awact/s3-action@master - with: - args: --acl public-read --follow-symlinks --delete - env: - AWS_REGION: 'ap-northeast-2' - AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - SOURCE_DIR: './frontend/dist' - - - name: Invalidate CloudFront Cache - uses: chetan/invalidate-cloudfront-action@master - env: - DISTRIBUTION: ${{ secrets.AWS_DISTRIBUTION_ID }} - AWS_REGION: 'ap-northeast-2' - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - PATHS: '/index.html' - continue-on-error: true diff --git a/frontend/src/apis/channel/dto.ts b/frontend/src/apis/channel/dto.ts index dbc25ea2..d73b27dc 100644 --- a/frontend/src/apis/channel/dto.ts +++ b/frontend/src/apis/channel/dto.ts @@ -18,6 +18,7 @@ export interface GetChannelResponse { } export interface postWorkspaceChannelsRequestDto { + workspaceId: string; name: string; isPrivate: boolean; emails: string[]; diff --git a/frontend/src/apis/channel/index.ts b/frontend/src/apis/channel/index.ts index f859e338..67701a6c 100644 --- a/frontend/src/apis/channel/index.ts +++ b/frontend/src/apis/channel/index.ts @@ -9,14 +9,14 @@ import { GetChannelResponse, GetMessagesResponse } from './dto'; export const getChannel = async ( channelId: string ): Promise => { - const response = await https.get(`/api/channel?channelId=${channelId}`); + const response = await https.get(`api/channel?channelId=${channelId}`); return response.data; }; export const getMessages = async ( channelId: string ): Promise => { - const response = await https.get(`/api/chatMessage?channelId=${channelId}`); + const response = await https.get(`/chatMessage?channelId=${channelId}`); return response.data; }; @@ -31,15 +31,17 @@ export const getUserJoinedWorkspaceChannels = async ( workspaceId: string ): Promise => { const { data } = await https.get(`/api/channels/workspaces/${workspaceId}`); - return data; + return data.channels; }; export const postNewWorkspaceChannels = async ({ + workspaceId, name, isPrivate, emails, }: postWorkspaceChannelsRequestDto): Promise => { const { data } = await https.post(`/api/channels`, { + workspaceId, name, isPrivate, emails, diff --git a/frontend/src/apis/dm/index.ts b/frontend/src/apis/dm/index.ts index b85d0ea0..c5a8c08e 100644 --- a/frontend/src/apis/dm/index.ts +++ b/frontend/src/apis/dm/index.ts @@ -7,6 +7,6 @@ export const getDMList = async (): Promise => { }; export const getDMMessage = async (dmId: string): Promise => { - const { data } = await https.get(`api/dms/${dmId}`); + const { data } = await https.get(`/api/dms/${dmId}`); return data; }; diff --git a/frontend/src/apis/workspace/dto.ts b/frontend/src/apis/workspace/dto.ts index e2a5e6d3..854a6e76 100644 --- a/frontend/src/apis/workspace/dto.ts +++ b/frontend/src/apis/workspace/dto.ts @@ -19,9 +19,9 @@ export interface GetUserWorkspaceListeResponse { export interface PostNewWorkspaceRequestDto { workspaceName: string; ownerId: string; - username: string; + userName: string; profileImage: string; - inviteResults: string[]; + inviteUserList: string[]; } export interface PostNewWorkspaceResponseDto { diff --git a/frontend/src/apis/workspace/index.ts b/frontend/src/apis/workspace/index.ts index 60def46c..7fccff46 100644 --- a/frontend/src/apis/workspace/index.ts +++ b/frontend/src/apis/workspace/index.ts @@ -22,7 +22,7 @@ export const getUserWorkspace = async ( export const getUserWorkspaces = async (): Promise => { const { data } = await https.get('/api/workspaces'); - return data; + return data.userWorkspaces; }; export const postRemoveWorkspace = async (workspaceId: string) => { @@ -42,6 +42,6 @@ export const postInviteWorkspace = async ( }; export const postLeaveWorkspace = async (workspaceId: string) => { - const { data } = await https.post(`/api/workspaces/${workspaceId}/leave`); + const { data } = await https.delete(`/api/workspaces/${workspaceId}/leave`); return data; }; diff --git a/frontend/src/components/modals/channel/ChannelCreateModal/index.tsx b/frontend/src/components/modals/channel/ChannelCreateModal/index.tsx index 1cf3cb92..2e6d56c7 100644 --- a/frontend/src/components/modals/channel/ChannelCreateModal/index.tsx +++ b/frontend/src/components/modals/channel/ChannelCreateModal/index.tsx @@ -3,31 +3,30 @@ import ChannelCreateSecondStepModal from '@/components/modals/channel/ChannelCre import useCreateChannelMutation from '@/hooks/channel/useCreateChannelMutation'; import { useModalStore } from '@/stores/modalStore'; import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router'; const ChannelCreateModal = () => { const [step, setStep] = useState(1); const [channelName, setChannelName] = useState(''); const [channelVisibility, setChannelVisibility] = useState(true); const [emails, setEmails] = useState([]); - const { createChannel } = useCreateChannelMutation(); + const { workspaceId } = useParams(); + const { createChannel } = useCreateChannelMutation(workspaceId!); const closeModla = useModalStore(state => state.closeModal); + const naviagate = useNavigate(); const handleNewChannelSubmit = () => { - createChannel( - { - name: channelName, - isPrivate: channelVisibility, - emails, - }, - { - onSuccess: () => { - alert(`${channelName} 채널 생성에 하셨습니다.`); - }, - onError: () => { - alert(`${channelName} 채널 생성에 실패하였습니다. `); - }, - } - ); + if (!workspaceId) { + alert('워크스페이스 접근 오류'); + naviagate('/workspaces'); + return; + } + createChannel({ + workspaceId, + name: channelName, + isPrivate: channelVisibility, + emails, + }); closeModla(); }; diff --git a/frontend/src/components/workspace/WorkspaceAvatarList.tsx b/frontend/src/components/workspace/WorkspaceAvatarList.tsx index 99688558..0fb8bcb6 100644 --- a/frontend/src/components/workspace/WorkspaceAvatarList.tsx +++ b/frontend/src/components/workspace/WorkspaceAvatarList.tsx @@ -8,9 +8,12 @@ interface WorkspaceAvatarListProps { const WorkspaceAvatarList = ({ members }: WorkspaceAvatarListProps) => { return (
- {members.slice(0, 5).map(item => ( - - ))} + {members && + members + .slice(0, 5) + .map(item => ( + + ))}
); }; diff --git a/frontend/src/components/workspace/WorkspaceChannelPanel/WorkspaceChannels.tsx b/frontend/src/components/workspace/WorkspaceChannelPanel/WorkspaceChannels.tsx index b5853086..2a1e916f 100644 --- a/frontend/src/components/workspace/WorkspaceChannelPanel/WorkspaceChannels.tsx +++ b/frontend/src/components/workspace/WorkspaceChannelPanel/WorkspaceChannels.tsx @@ -16,9 +16,10 @@ const WorkspaceChannels = ({ }: WorkspaceAccordionSectionProps) => { return ( - {channelList?.map((channel, index) => ( - - ))} + {channelList && + channelList?.map((channel, index) => ( + + ))} { const user = useUserStore(state => state.user); const currentUserRole = workspaceInfo?.ownerId === user.userId ? 'admin' : 'member'; + const naviagte = useNavigate(); if (!workspaceId) return

워크스페이스 정보를 불러오는 중...

; if (isChannelLoading || isWorkspaceLoading || isDMLoading) @@ -46,7 +47,10 @@ const WorkspaceChannelPanel = () => { onInvite={() => { setModal('USER_INVITE'); }} - onLogout={() => alert('로그아웃이 완료되었습니다.')} + onLogout={() => { + alert('로그아웃이 완료되었습니다'); + naviagte('/'); + }} onDelete={() => { setModal('WORKSPACE_DELETE'); }} @@ -57,7 +61,9 @@ const WorkspaceChannelPanel = () => { setModal('CHANNEL_CREATE')} - onExploreChannels={() => console.log('채널 탐색')} + onExploreChannels={() => + alert('채널 탐색은 아직 준비중인 서비스입니다') + } channelList={channelList} /> { const { @@ -16,7 +17,7 @@ const StepSetInviteUsers = () => { initWorkspaceStore, } = useWorkspaceCreationStore(); const [validEmail, setValidEmail] = useState(false); - const { createWorkspace } = useCreateWorkspace(); + const { createWorkspace } = useCreateWorkspaceMutation(); const navigate = useNavigate(); const submitWorkspaceInfo = () => { @@ -24,10 +25,10 @@ const StepSetInviteUsers = () => { createWorkspace( { workspaceName: workspaceName, - ownerId: '1', - username: userName, + ownerId: getDummyOwnerId(), + userName: userName, profileImage: workspaceProfileImage, - inviteResults: invitedUsers, + inviteUserList: invitedUsers, }, { onSuccess: data => { diff --git a/frontend/src/components/workspace/WorkspaceListItem.tsx b/frontend/src/components/workspace/WorkspaceListItem.tsx index a24344d0..900f2490 100644 --- a/frontend/src/components/workspace/WorkspaceListItem.tsx +++ b/frontend/src/components/workspace/WorkspaceListItem.tsx @@ -1,8 +1,8 @@ import { useNavigate } from 'react-router'; import Avatar from '@/components/common/Avatar'; import ArrorIcon from '@/components/common/ArrorIcon'; -import WorkspaceListItemDetail from '@/components/workspace/WorkspaceListItemDetail'; import { WorkspaceMember } from '@/types/user'; +import WorkspaceListItemDetail from '@/components/workspace/WorkspaceListItemDetail'; interface WorkspaceListItemProps { name: string; @@ -24,7 +24,6 @@ const WorkspaceListItem = ({ const navigateToWorkspace = (workspaceId: string) => { navigate(`/workspace/${workspaceId}`); }; - return (
diff --git a/frontend/src/hooks/channel/useCreateChannelMutation.ts b/frontend/src/hooks/channel/useCreateChannelMutation.ts index 2eae8ad5..400cbc3f 100644 --- a/frontend/src/hooks/channel/useCreateChannelMutation.ts +++ b/frontend/src/hooks/channel/useCreateChannelMutation.ts @@ -1,9 +1,20 @@ import { postNewWorkspaceChannels } from '@/apis/channel'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; -const useCreateChannelMutation = () => { +const useCreateChannelMutation = (workspaceId: string) => { + const queryClient = useQueryClient(); const { mutate: createChannel, ...rest } = useMutation({ mutationFn: postNewWorkspaceChannels, + onSuccess: () => { + alert(`채널 생성에 하셨습니다.`); + + queryClient.invalidateQueries({ + queryKey: ['channels', workspaceId], + }); + }, + onError: () => { + alert(`채널 생성에 실패하였습니다. `); + }, }); return { createChannel, ...rest }; }; diff --git a/frontend/src/hooks/workspace/useCreateWorkspace.ts b/frontend/src/hooks/workspace/useCreateWorkspaceMutation.ts similarity index 54% rename from frontend/src/hooks/workspace/useCreateWorkspace.ts rename to frontend/src/hooks/workspace/useCreateWorkspaceMutation.ts index 75004900..55cb31fa 100644 --- a/frontend/src/hooks/workspace/useCreateWorkspace.ts +++ b/frontend/src/hooks/workspace/useCreateWorkspaceMutation.ts @@ -1,12 +1,16 @@ import { postNewWorkspace } from '@/apis/workspace'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; -export const useCreateWorkspace = () => { +export const useCreateWorkspaceMutation = () => { + const queryClient = useQueryClient(); const { mutate: createWorkspace, ...rest } = useMutation({ mutationKey: ['makeworkspace'], mutationFn: postNewWorkspace, onSuccess: () => { alert('성공'); + queryClient.invalidateQueries({ + queryKey: ['workspaces'], + }); }, }); diff --git a/frontend/src/hooks/workspace/useLeaveWorkspaceMutation.ts b/frontend/src/hooks/workspace/useLeaveWorkspaceMutation.ts index b04f28c2..cf6859f2 100644 --- a/frontend/src/hooks/workspace/useLeaveWorkspaceMutation.ts +++ b/frontend/src/hooks/workspace/useLeaveWorkspaceMutation.ts @@ -1,11 +1,26 @@ -import { postLeaveWorkspace } from '@/apis/workspace'; -import { useMutation } from '@tanstack/react-query'; +import { postLeaveWorkspace, getUserWorkspaces } from '@/apis/workspace'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; const useLeaveWorkspaceMutation = () => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); const { mutate: leaveWorkspace, ...rest } = useMutation({ mutationFn: (workpspaceId: string) => postLeaveWorkspace(workpspaceId), - onSuccess: () => { + onSuccess: async () => { alert('성공적으로 나가졌습니다.'); + queryClient.invalidateQueries({ + queryKey: ['workspaces'], + }); + const updateWorkspaces = await queryClient.fetchQuery({ + queryKey: ['workspaces'], + queryFn: getUserWorkspaces, + }); + if (updateWorkspaces.workspaces.length > 0) { + navigate(`/workspace/${updateWorkspaces.workspaces[0].workspaceId}`); + } else { + navigate('/workspaces'); + } }, onError: () => { alert('워크스페이스 나가기 에러'); diff --git a/frontend/src/lib/https.ts b/frontend/src/lib/https.ts index a6b673fe..f47b9e0f 100644 --- a/frontend/src/lib/https.ts +++ b/frontend/src/lib/https.ts @@ -1,13 +1,24 @@ import axios from 'axios'; +import { getTocken } from '@/lib/utils'; + +const mockApiList = ['/api/dms']; const https = axios.create({ - baseURL: '/', // api 나오면 연결 || api withCredentials: true, timeout: 10000, }); https.interceptors.request.use( config => { + const isMockApi = mockApiList.some(item => config.url!.startsWith(item)); + + if (isMockApi) { + config.baseURL = ''; + } else { + config.baseURL = import.meta.env.VITE_BASE_API_URL; + } + + config.headers.Authorization = `Bearer ${getTocken()}`; return config; }, error => { diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index f88eb13f..d5ea4bce 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -15,3 +15,11 @@ export const isValidKoreanEnglish = (text: string): boolean => { const koreanEnglishRegex = /^[가-힣a-zA-Z]+$/; // 한글 + 영어만 허용 return koreanEnglishRegex.test(text); }; + +export const getTocken = () => { + return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcklkIjoiMDNjNmIwODMtZThkNi00ODhjLWFhODMtMmEwMWIzZjM5ZDAwIiwiaWF0IjoxNTE2MjM5MDIyfQ.iVTdh4kkGh6f6gEZLf9MJPwkjusaXf58z_Tc4ncummw'; +}; + +export const getDummyOwnerId = () => { + return '03c6b083-e8d6-488c-aa83-2a01b3f39d00'; +}; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 0769816c..8bbf56b4 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -29,7 +29,7 @@ export const handlers = [ { status: 400 } ); } - if (!newPost.username) { + if (!newPost.userName) { return HttpResponse.json( { error: 'username is required' }, { status: 400 } @@ -38,7 +38,7 @@ export const handlers = [ const workspaceId = nanoid(8); const userList = []; - for (let i = 0; i < newPost.inviteResults.length; i++) { + for (let i = 0; i < newPost.inviteUserList.length; i++) { const dummyUser = { userId: 'user_12345', profileImage: 'https://example.com/user_12345.png', @@ -56,7 +56,7 @@ export const handlers = [ workspaceId: workspaceId, name: newPost.workspaceName, profileImage: newPost.profileImage, - memberCount: newPost.inviteResults.length, + memberCount: newPost.inviteUserList.length, users: userList, }; const responseData: PostNewWorkspaceResponseDto = { @@ -67,7 +67,7 @@ export const handlers = [ profileImage: newPost.profileImage, inviteResults: { success: [], - failed: newPost.inviteResults, + failed: newPost.inviteUserList, }, createdAt: new Date().toISOString(), }; diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx new file mode 100644 index 00000000..06a2a89f --- /dev/null +++ b/frontend/src/pages/NotFoundPage.tsx @@ -0,0 +1,21 @@ +import { Button } from '@/components/ui/button'; +import { useNavigate } from 'react-router'; + +const NotFoundPage = () => { + const navigate = useNavigate(); + return ( +
+ 404 Not Found + +
+ ); +}; + +export default NotFoundPage; diff --git a/frontend/src/pages/workspace/WorkspaceListPage.tsx b/frontend/src/pages/workspace/WorkspaceListPage.tsx index bf0f9e48..bcf6ed19 100644 --- a/frontend/src/pages/workspace/WorkspaceListPage.tsx +++ b/frontend/src/pages/workspace/WorkspaceListPage.tsx @@ -18,36 +18,47 @@ const WorkSpaceListPage = () => { return (
- -
- 아래에서 워크스페이스를 선택하여 팀과 계속 협업하세요. -
- -
-

- {workspacesInfo && workspacesInfo.email} -

-
님의 워크스페이스 관리
-
- {workspacesInfo && - workspacesInfo.workspaces.map(item => { - return ( - - ); - })} -
+
+ {workspacesInfo && workspacesInfo?.workspaces.length === 0 && ( +
+ 가입되어 있는 워크스페이가 없습니다. +
+ )} + +
+ {workspacesInfo && workspacesInfo?.workspaces.length > 0 && ( + <> + + 아래에서 워크스페이스를 선택하여 팀과 계속 협업하세요. + + +
+

+ {workspacesInfo && workspacesInfo.email} +

+
님의 워크스페이스 관리
+
+ {workspacesInfo && + workspacesInfo.workspaces.map(item => { + return ( + + ); + })} +
+ + )}
); }; diff --git a/frontend/src/routers/index.tsx b/frontend/src/routers/index.tsx index 192f4445..5573b30e 100644 --- a/frontend/src/routers/index.tsx +++ b/frontend/src/routers/index.tsx @@ -9,6 +9,7 @@ import ChannelPage from '@/pages/channel/ChannelPage'; import ActivityPage from '@/pages/activity/ActivityPage'; import WorkspaceChannelPanel from '@/components/workspace/WorkspaceChannelPanel'; import SplitPaneLayout from '@/components/common/SplitPaneLayout'; +import NotFoundPage from '@/pages/NotFoundPage'; export const router = createBrowserRouter([ { @@ -63,4 +64,8 @@ export const router = createBrowserRouter([ }, ], }, + { + path: '/*', + element: , + }, ]);