Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#8] 워크스페이스 페이지 이동 기능, 워크스페이스 이름 변경 기능 구현 #100

Merged
merged 44 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
11da411
✨ feat: 워크스페이스 요청 후 에러 발생 시 에러 발생을 알려주는 컴포넌트 추가
lee0jae330 Nov 13, 2024
e3f8d9e
✨ feat: 워크스페이스 불러오기 시 에러가 발생한 경우 에러 컴포넌트를 렌더링하도록 변경 및 api 호출 영역 렌더링 조…
lee0jae330 Nov 13, 2024
98eba36
🔨 refactor: 편집 시간 format 메소드 shared/utils로 분리
lee0jae330 Nov 13, 2024
6243f1e
✨ feat: WorkspaceItem에 onClick이라는 props 추가 및 카드 div 추가
lee0jae330 Nov 13, 2024
13187ee
✨ feat: 워크스페이스 item 클릭 시 workspace 페이지로 navigate 되도록 기능 추가
lee0jae330 Nov 13, 2024
de6006b
✨ feat: useGetWorkspaceList 반환값으로 isError를 리턴하도록 변경
lee0jae330 Nov 13, 2024
e953069
🙀 chore: 워크스페이스 조회 api 엔드포인트 변경 /workspace -> /workspace/list
lee0jae330 Nov 13, 2024
0568f46
🙀 chore: swagger 스키마 추가 WorkspaceIdResposne, WorkspaceListDto, NextCu…
lee0jae330 Nov 13, 2024
a7e8eb7
💬 comment: 워크스페이스 목록 조회 api swagger 주석 추가
lee0jae330 Nov 13, 2024
09a3475
🙀 chore: swagger-output.json gitignore에 추가
lee0jae330 Nov 13, 2024
df03708
Remove swagger-output.json from tracking
lee0jae330 Nov 13, 2024
bd503ff
✨ feat: 워크스페이스 하나만 조회하는 findWorkspaceByWorkspaceId 메소드 추가
lee0jae330 Nov 13, 2024
90405fe
✨ feat: GET /workspace?workspace=워크스페이스ID api 로직 추가
lee0jae330 Nov 13, 2024
1c836e4
✨ feat: 라우팅 추가 및 swagger 주석 추가
lee0jae330 Nov 13, 2024
4be5887
✨ feat: 404 에러 메세지 추가
lee0jae330 Nov 13, 2024
dc9c8e2
🙀 chore: 타입명 변경 TworkspaceDto => Tworkspace
lee0jae330 Nov 13, 2024
2dd32ba
✨ feat: 스웨거 WorkspaceDto 타입 추가
lee0jae330 Nov 13, 2024
d89e661
🙀 chore: find -> findOne 메소드 사용 및 workspace_id, name만 반환하도록 변경
lee0jae330 Nov 13, 2024
0a374a8
🙀 chore: 404 에러 조건 변경
lee0jae330 Nov 13, 2024
6537ed8
🙀 chore: findOne.projection 옵션 변경
lee0jae330 Nov 13, 2024
7c81b7b
🙀 chore: 스웨거 주석 수정
lee0jae330 Nov 13, 2024
6c5fca7
🙀 chore: UUID 패키지 설치
lee0jae330 Nov 13, 2024
245a669
✨ feat: crypto 모듈은 브라우저에서 지원이 안되는 문제가 있어 uuid 모듈로 변경
lee0jae330 Nov 13, 2024
5077a6f
✨ feat: workspace 정보 조회 시 응답 타입 정의
lee0jae330 Nov 13, 2024
8af70ca
✨ feat: 단일 워크스페이스의 정보 api 요청하는 useGetWorkspace 커스텀 추가
lee0jae330 Nov 13, 2024
b0accfd
✨ feat: 단일 워크스페이스 정보 조회 api 연동
lee0jae330 Nov 13, 2024
abc56be
✨ feat: WorkspacePageHeader, WorkspaceNameInput의 props 추가, 해당 props를 …
lee0jae330 Nov 13, 2024
94edaee
🙀 chore: EmptyWorkspace가 WorkspaceGrid 자식 컴포넌트로 동작할 시 디자인이 변경되는 문제가 있…
lee0jae330 Nov 13, 2024
b52224d
✨ feat: user_id, workspace_id 를 통해 document를 찾고 newName으로 해당 document…
lee0jae330 Nov 13, 2024
863831b
✨ feat: 이름 변경 요청 api 로직을 처리하는 메소드
lee0jae330 Nov 13, 2024
3fd9a4e
✨ feat: /workspace/name PATCH 라우팅 추가 및 스웨거 명세 작성
lee0jae330 Nov 13, 2024
ee4a152
✨ feat: PATCH /workspace/name 메서드 연동
lee0jae330 Nov 13, 2024
e506457
✨ feat: Workspace 정보를 관리하는 useWorkspaceStore 전역상태 커스텀 훅 추가
lee0jae330 Nov 13, 2024
64dcae1
✨ feat: useGetWorkspace 커스텀 훅으로 workspace의 정보를 요청할 시 성공하면 workspace 의…
lee0jae330 Nov 13, 2024
93a97a0
✨ feat: 워크스페이스 정보를 불러오는 것을 실패하면 NotFound 페이지를 렌더링함
lee0jae330 Nov 13, 2024
abbd5ea
🔨 refactor: workspace 정보를 전역 상태로 관리함에 따라 props 삭제
lee0jae330 Nov 13, 2024
3f352ac
✨ feat: 워크스페이스의 이름 변경을 요청하는 커스텀 훅 추가
lee0jae330 Nov 13, 2024
fd96c39
🙀 chore: index.ts re-export 추가
lee0jae330 Nov 13, 2024
133b876
✨ feat: 이름 변경 로직 추가, 이름 변경 요청 대기 시 input 창 비활성화 및 로딩 위젯 렌더링 기능 추가
lee0jae330 Nov 13, 2024
0b8e7a3
🎨 style: 토스트 메세지 위치 변경
lee0jae330 Nov 14, 2024
b4f7c67
Merge branch 'dev' into feat/8
lee0jae330 Nov 14, 2024
3c53efa
Update boolock-dev-cicd.yml
lee0jae330 Nov 14, 2024
d9aff79
🙀 chore: effect 임포트 변경
lee0jae330 Nov 14, 2024
b0040a4
Merge branch 'feat/8' of github.com:boostcampwm-2024/web31-BooLock in…
lee0jae330 Nov 14, 2024
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
1 change: 1 addition & 0 deletions .github/workflows/boolock-dev-cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
- name: Build Backend
run: |
cd apps/server
pnpm run swagger-auto
pnpm run build

- name: Build base image
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ Thumbs.db

*storybook.log


swagger-output.json

# Ignore Storybook build folder
storybook-static/
storybook-static/
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"react-spinners": "^0.14.1",
"react-youtube": "^10.1.0",
"storybook": "^8.4.1",
"uuid": "^11.0.3",
"zustand": "^5.0.1"
},
"devDependencies": {
Expand Down
45 changes: 14 additions & 31 deletions apps/client/src/entities/home/WorkspaceItem.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,33 @@
import TrashSVG from '@/shared/assets/trash.svg?react';
import { formatRelativeOrAbsoluteDate } from '@/shared/utils';
import { useModalStore } from '@/shared/store';

type WorkspaceItemProps = {
title: string;
thumbnail: string;
lastEdited: string;
onClick: () => void;
};

const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const rtf = new Intl.RelativeTimeFormat('ko', { numeric: 'auto' });
const diffMinutes = Math.floor((date.getTime() - now.getTime()) / (1000 * 60));
const diffHours = Math.floor(diffMinutes / 60);
if (diffMinutes > -60) {
return rtf.format(diffMinutes, 'minute');
}

if (diffHours > -24) {
return rtf.format(diffHours, 'hour');
}
return new Intl.DateTimeFormat('ko', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
};

export const WorkspaceItem = ({ title, thumbnail, lastEdited }: WorkspaceItemProps) => {
export const WorkspaceItem = ({ title, thumbnail, lastEdited, onClick }: WorkspaceItemProps) => {
const { openModal: onOpen } = useModalStore();
return (
<li className="shadow-drop relative rounded-lg">
<button className="absolute right-2 top-2 text-gray-300 hover:text-red-500" onClick={onOpen}>
<TrashSVG width={16} />
</button>
<div className="flex h-[180px] overflow-hidden bg-gray-50">
{/* TODO: 썸네일 형태에 따라 이미지 태그 OR 백그라운드로 지정 */}
{thumbnail && (
<img src={thumbnail} alt="workspace thumbnail" className="h-32 w-full object-cover" />
)}
<div className="cursor-pointer" onClick={onClick}>
<div className="flex h-[180px] overflow-hidden bg-gray-50">
{/* TODO: 썸네일 형태에 따라 이미지 태그 OR 백그라운드로 지정 */}
{thumbnail && (
<img src={thumbnail} alt="workspace thumbnail" className="h-32 w-full object-cover" />
)}
</div>
<aside className="p-4 pb-6">
<h4 className="text-bold-md mb-1.5 text-gray-500">{title}</h4>
<p className="text-medium-sm text-gray-200">{formatRelativeOrAbsoluteDate(lastEdited)}</p>
</aside>
</div>

<aside className="p-4 pb-6">
<h4 className="text-bold-md mb-1.5 text-gray-500">{title}</h4>
<p className="text-medium-sm text-gray-200">{formatDate(lastEdited)}</p>
</aside>
</li>
);
};
8 changes: 8 additions & 0 deletions apps/client/src/entities/home/WorkspaceLoadError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const WorkspaceLoadError = () => {
return (
<div className="flex h-[23rem] w-full flex-col items-center justify-center gap-4 rounded-lg bg-gray-50 text-gray-200">
<img src="./images/not_found.png" className="h-40 w-40" />
<p className="text-center">워크스페이스를 불러오지 못했습니다.</p>
</div>
);
};
1 change: 1 addition & 0 deletions apps/client/src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { GuideVideo } from './home/GuideVideo';
export { WorkspaceItem } from './home/WorkspaceItem';
export { WorkspaceAddBtn } from './home/WorkspaceAddBtn';
export { WorkspaceLoadError } from './home/WorkspaceLoadError';

export { RedoButton } from './workspace/RedoButton';
export { UndoButton } from './workspace/UndoButton';
Expand Down
44 changes: 25 additions & 19 deletions apps/client/src/entities/workspace/WorkspaceNameInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { FocusEventHandler, KeyboardEventHandler, useState } from 'react';
import { FocusEventHandler, KeyboardEventHandler } from 'react';

import toast from 'react-hot-toast';
import Spinner from '@/shared/assets/spinner.svg?react';
import { useParams } from 'react-router-dom';
import { useUpdateWorkspaceName } from '@/shared/hooks';
import { useWorkspaceStore } from '@/shared/store';

export const WorkspaceNameInput = () => {
// TODO: 워크스페이스 이름 변경 로직 필요

const [name, setName] = useState<string>('');
const { workspaceId } = useParams() as { workspaceId: string };
const { name } = useWorkspaceStore();
const { mutate, isPending } = useUpdateWorkspaceName();

const handleBlur: FocusEventHandler<HTMLInputElement> = (
event: React.FocusEvent<HTMLInputElement>
) => {
// TODO: 이름 변경 로직 추가
if (event.target.value === name) {
if (event.target.value === name || event.target.value === '') {
return;
}
toast.error('이름 변경 실패');
setName(event.target.value);
mutate({ workspaceId, newName: event.target.value });
};

const handleEnter: KeyboardEventHandler<HTMLInputElement> = (e) => {
Expand All @@ -25,24 +26,29 @@ export const WorkspaceNameInput = () => {
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// TODO: 이름 변경 로직 추가
e.currentTarget.blur();
if (name === e.currentTarget.value) {
if (name === e.currentTarget.value || e.currentTarget.value === '') {
return;
}
setName(e.currentTarget.value);
mutate({ workspaceId, newName: e.currentTarget.value });
e.preventDefault();
};

// TODO: 워크스페이스 이름 존재 시 placeholder가 그에 맞추어 변경되어야함
return (
<>
<input
placeholder={name === '' ? '워크스페이스 이름' : name}
className="placeholder:text-semibold-rg w-[272px] rounded-md border border-green-500 px-3 py-1 placeholder:text-gray-100 focus:outline-none"
onBlur={handleBlur}
onKeyDown={handleEnter}
/>
<div className="relative flex items-center">
<input
placeholder={name === '' ? '워크스페이스 이름' : name}
className="placeholder:text-semibold-rg w-[272px] rounded-md border border-green-500 px-3 py-1 placeholder:text-gray-100 focus:outline-none"
onBlur={handleBlur}
onKeyDown={handleEnter}
maxLength={20}
disabled={isPending}
/>
{isPending && (
<Spinner className="absolute right-5 inline h-4 w-4 animate-spin fill-green-500 text-gray-200" />
)}
</div>
</>
);
};
17 changes: 15 additions & 2 deletions apps/client/src/pages/WorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import { WorkspacePageHeader, WorkspaceContent } from '@/widgets';
import { WorkspaceContent, WorkspacePageHeader } from '@/widgets';

import { Loading } from '@/shared/ui';
import { NotFound } from '@/pages/NotFound';
import { useGetWorkspace } from '@/shared/hooks';
import { useParams } from 'react-router-dom';

// TODO: useParams 훅을 통해 workspaceId 가져오기
export const WorkspacePage = () => {
const { workspaceId } = useParams();

const { isPending, isError } = useGetWorkspace(workspaceId as string);

if (isError) {
return <NotFound />;
}

return (
<>
{isPending && <Loading />}
<WorkspacePageHeader />
<WorkspaceContent />
</>
Expand Down
37 changes: 34 additions & 3 deletions apps/client/src/shared/api/workspaceApi.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Instance } from './axiosInstance';
import { TcreatedWorkspaceDto, TpagedWorkspaceListResultDto } from '@/shared/types';
import {
TcreatedWorkspaceDto,
TgetWorkspaceResponse,
TpagedWorkspaceListResultDto,
Tworkspace,
} from '@/shared/types';

import { Instance } from '@/shared/api';

export const WorkspaceApi = () => {
const createWorkspace = async (userId: string) => {
Expand All @@ -20,7 +26,7 @@ export const WorkspaceApi = () => {

const getWorkspaceList = async (userId: string, cursor: string) => {
const response = await Instance.get(
`/workspace${cursor !== 'null' ? `?cursor=${encodeURIComponent(cursor)}` : ''}`,
`/workspace/list${cursor !== 'null' ? `?cursor=${encodeURIComponent(cursor)}` : ''}`,
{
headers: { 'user-id': userId },
}
Expand All @@ -31,8 +37,33 @@ export const WorkspaceApi = () => {
throw new Error('Invalid response from server');
};

const getWorkspace = async (userId: string, workspaceId: string) => {
const response = await Instance.get(`/workspace?workspaceId=${workspaceId}`, {
headers: { 'user-id': userId },
});
if (response) {
return response.data as TgetWorkspaceResponse;
}
throw new Error('Invalid response from server');
};

const updateWorkspaceName = async (userId: string, workspaceId: string, newName: string) => {
const response = await Instance.patch(
'/workspace/name',
{ workspaceId, newName },
{ headers: { 'user-id': userId } }
);

if (response) {
return response.data as Tworkspace;
}
throw new Error('Invalid response from server');
};

return {
createWorkspace,
getWorkspaceList,
getWorkspace,
updateWorkspaceName,
};
};
14 changes: 14 additions & 0 deletions apps/client/src/shared/assets/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/client/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { useCreateWorkspace } from './queries/useCreateWorkspace';
export { useGetWorkspaceList } from './queries/useGetWorkspaceList';
export { useGetWorkspace } from './queries/useGetWorkspace';
export { useUpdateWorkspaceName } from './queries/useUpdateWorkspaceName';
30 changes: 30 additions & 0 deletions apps/client/src/shared/hooks/queries/useGetWorkspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { WorkspaceApi } from '@/shared/api';
import { getUserId } from '@/shared/utils';
import toast from 'react-hot-toast';
import { useQuery } from '@tanstack/react-query';
import { useWorkspaceStore } from '@/shared/store';

export const useGetWorkspace = (workspaceId: string) => {
const workspaceApi = WorkspaceApi();
const userId = getUserId();
const { setName } = useWorkspaceStore();
const { data, isPending, isError } = useQuery({
queryKey: ['getWorkspace', workspaceId],
queryFn: () => {
return workspaceApi.getWorkspace(userId, workspaceId);
},
});

React.useEffect(() => {
if (isError) {
toast.error('워크스페이스 정보 불러오기 실패');
Copy link
Collaborator

@chichoc chichoc Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성공/실패시 문구를 통일하면 어떨까 싶습니다

  • 성공: 워크스페이스 이름이 변경되었습니다.
  • 실패: 워크스페이스 불러오는데 실패했습니다.

return;
}
if (data) {
setName(data.workspaceDto.name);
}
}, [data, setName, isError]);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import React from 'react';로 임포트해와서 React.useEffect() 로 가져오기보다
import {useEffect} from 'react';로 임포트해와서 useEffect() 로 쓰는 걸 추천드립니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 감사합니다

return { data, isPending, isError };
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const useGetWorkspaceList = () => {
fetchNextPage,
isPending,
isFetchingNextPage,
isError,
data: workspaceList,
} = useInfiniteQuery({
queryKey: ['getWorkspaceList'],
Expand All @@ -25,5 +26,5 @@ export const useGetWorkspaceList = () => {
select: (data) =>
(data.pages ?? []).flatMap((page) => page.pagedWorkspaceListResult.workspaceList),
});
return { hasNextPage, fetchNextPage, isFetchingNextPage, isPending, workspaceList };
return { hasNextPage, fetchNextPage, isFetchingNextPage, isPending, isError, workspaceList };
};
26 changes: 26 additions & 0 deletions apps/client/src/shared/hooks/queries/useUpdateWorkspaceName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { WorkspaceApi } from '@/shared/api';
import { getUserId } from '@/shared/utils';
import toast from 'react-hot-toast';
import { useMutation } from '@tanstack/react-query';
import { useWorkspaceStore } from '@/shared/store';

export const useUpdateWorkspaceName = () => {
const workspaceApi = WorkspaceApi();
const userId = getUserId();
const { setName } = useWorkspaceStore();

const { mutate, isPending } = useMutation({
mutationFn: ({ workspaceId, newName }: { workspaceId: string; newName: string }) => {
return workspaceApi.updateWorkspaceName(userId, workspaceId, newName);
},
onSuccess: (data) => {
toast.success('워크스페이스 이름이 변경되었습니다.');
setName(data?.name!);
},
onError: () => {
toast.error('워크스페이스 이름 변경을 실패했습니다.');
},
});

return { mutate, isPending };
};
1 change: 1 addition & 0 deletions apps/client/src/shared/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { useLoadingStore } from './useLoadingStore';
export { useModalStore } from './useModalStore';
export { useWorkspaceStore } from './useWorkspaceStore';
11 changes: 11 additions & 0 deletions apps/client/src/shared/store/useWorkspaceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { create } from 'zustand';

type TWorkspace = {
name: string;
setName: (newName: string) => void;
};

export const useWorkspaceStore = create<TWorkspace>((set) => ({
name: '워크스페이스 이름',
setName: (newName) => set({ name: newName }),
}));
2 changes: 2 additions & 0 deletions apps/client/src/shared/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export type {
TcreatedWorkspaceDto,
TgetWorkspaceResponse,
TworkspaceDto,
Tworkspace,
TpagedWorkspaceListResultDto,
TpagedWorkspaceListResult,
Tcursor,
Expand Down
Loading
Loading