Skip to content

Commit 8481d67

Browse files
authored
Merge pull request #367 from boostcampwm-2024/bug-fe-#361
워크 스페이스 라우트 보호 및 입장 권한, 공개 범위 변경, 링크 생성, main 라우트 처리
2 parents 98ce563 + 2270d6b commit 8481d67

File tree

14 files changed

+310
-68
lines changed

14 files changed

+310
-68
lines changed

apps/frontend/src/app/App.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { useSyncedUsers } from "@/entities/user";
2-
import { useGetUser } from "@/features/auth";
3-
import { CanvasToolsView } from "@/widgets/CanvasToolsView";
2+
import { useProtectedWorkspace } from "@/features/workspace";
43
import { CanvasView } from "@/widgets/CanvasView";
54
import { EditorView } from "@/widgets/EditorView";
65
import { PageSideBarView } from "@/widgets/PageSideBarView";
6+
import { CanvasToolsView } from "@/widgets/CanvasToolsView";
77
import { SideWrapper } from "@/shared/ui";
88

99
function App() {
1010
useSyncedUsers();
11-
useGetUser();
11+
const { isLoading } = useProtectedWorkspace();
12+
13+
if (isLoading) {
14+
return (
15+
<div className="fixed inset-0 flex items-center justify-center bg-white">
16+
<div className="animate-pulse text-gray-400">Loading...</div>
17+
</div>
18+
);
19+
}
1220

1321
return (
1422
<div className="fixed inset-0 bg-white">

apps/frontend/src/app/routeTree.gen.ts

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,85 +10,104 @@
1010

1111
// Import Routes
1212

13-
import { Route as rootRoute } from "./routes/__root";
14-
import { Route as IndexImport } from "./routes/index";
15-
import { Route as WorkspaceWorkspaceIdImport } from "./routes/workspace/$workspaceId";
13+
import { Route as rootRoute } from './routes/__root'
14+
import { Route as IndexImport } from './routes/index'
15+
import { Route as JoinIndexImport } from './routes/join/index'
16+
import { Route as WorkspaceWorkspaceIdImport } from './routes/workspace/$workspaceId'
1617

1718
// Create/Update Routes
1819

1920
const IndexRoute = IndexImport.update({
20-
id: "/",
21-
path: "/",
21+
id: '/',
22+
path: '/',
2223
getParentRoute: () => rootRoute,
23-
} as any);
24+
} as any)
25+
26+
const JoinIndexRoute = JoinIndexImport.update({
27+
id: '/join/',
28+
path: '/join/',
29+
getParentRoute: () => rootRoute,
30+
} as any)
2431

2532
const WorkspaceWorkspaceIdRoute = WorkspaceWorkspaceIdImport.update({
26-
id: "/workspace/$workspaceId",
27-
path: "/workspace/$workspaceId",
33+
id: '/workspace/$workspaceId',
34+
path: '/workspace/$workspaceId',
2835
getParentRoute: () => rootRoute,
29-
} as any);
36+
} as any)
3037

3138
// Populate the FileRoutesByPath interface
3239

33-
declare module "@tanstack/react-router" {
40+
declare module '@tanstack/react-router' {
3441
interface FileRoutesByPath {
35-
"/": {
36-
id: "/";
37-
path: "/";
38-
fullPath: "/";
39-
preLoaderRoute: typeof IndexImport;
40-
parentRoute: typeof rootRoute;
41-
};
42-
"/workspace/$workspaceId": {
43-
id: "/workspace/$workspaceId";
44-
path: "/workspace/$workspaceId";
45-
fullPath: "/workspace/$workspaceId";
46-
preLoaderRoute: typeof WorkspaceWorkspaceIdImport;
47-
parentRoute: typeof rootRoute;
48-
};
42+
'/': {
43+
id: '/'
44+
path: '/'
45+
fullPath: '/'
46+
preLoaderRoute: typeof IndexImport
47+
parentRoute: typeof rootRoute
48+
}
49+
'/workspace/$workspaceId': {
50+
id: '/workspace/$workspaceId'
51+
path: '/workspace/$workspaceId'
52+
fullPath: '/workspace/$workspaceId'
53+
preLoaderRoute: typeof WorkspaceWorkspaceIdImport
54+
parentRoute: typeof rootRoute
55+
}
56+
'/join/': {
57+
id: '/join/'
58+
path: '/join'
59+
fullPath: '/join'
60+
preLoaderRoute: typeof JoinIndexImport
61+
parentRoute: typeof rootRoute
62+
}
4963
}
5064
}
5165

5266
// Create and export the route tree
5367

5468
export interface FileRoutesByFullPath {
55-
"/": typeof IndexRoute;
56-
"/workspace/$workspaceId": typeof WorkspaceWorkspaceIdRoute;
69+
'/': typeof IndexRoute
70+
'/workspace/$workspaceId': typeof WorkspaceWorkspaceIdRoute
71+
'/join': typeof JoinIndexRoute
5772
}
5873

5974
export interface FileRoutesByTo {
60-
"/": typeof IndexRoute;
61-
"/workspace/$workspaceId": typeof WorkspaceWorkspaceIdRoute;
75+
'/': typeof IndexRoute
76+
'/workspace/$workspaceId': typeof WorkspaceWorkspaceIdRoute
77+
'/join': typeof JoinIndexRoute
6278
}
6379

6480
export interface FileRoutesById {
65-
__root__: typeof rootRoute;
66-
"/": typeof IndexRoute;
67-
"/workspace/$workspaceId": typeof WorkspaceWorkspaceIdRoute;
81+
__root__: typeof rootRoute
82+
'/': typeof IndexRoute
83+
'/workspace/$workspaceId': typeof WorkspaceWorkspaceIdRoute
84+
'/join/': typeof JoinIndexRoute
6885
}
6986

7087
export interface FileRouteTypes {
71-
fileRoutesByFullPath: FileRoutesByFullPath;
72-
fullPaths: "/" | "/workspace/$workspaceId";
73-
fileRoutesByTo: FileRoutesByTo;
74-
to: "/" | "/workspace/$workspaceId";
75-
id: "__root__" | "/" | "/workspace/$workspaceId";
76-
fileRoutesById: FileRoutesById;
88+
fileRoutesByFullPath: FileRoutesByFullPath
89+
fullPaths: '/' | '/workspace/$workspaceId' | '/join'
90+
fileRoutesByTo: FileRoutesByTo
91+
to: '/' | '/workspace/$workspaceId' | '/join'
92+
id: '__root__' | '/' | '/workspace/$workspaceId' | '/join/'
93+
fileRoutesById: FileRoutesById
7794
}
7895

7996
export interface RootRouteChildren {
80-
IndexRoute: typeof IndexRoute;
81-
WorkspaceWorkspaceIdRoute: typeof WorkspaceWorkspaceIdRoute;
97+
IndexRoute: typeof IndexRoute
98+
WorkspaceWorkspaceIdRoute: typeof WorkspaceWorkspaceIdRoute
99+
JoinIndexRoute: typeof JoinIndexRoute
82100
}
83101

84102
const rootRouteChildren: RootRouteChildren = {
85103
IndexRoute: IndexRoute,
86104
WorkspaceWorkspaceIdRoute: WorkspaceWorkspaceIdRoute,
87-
};
105+
JoinIndexRoute: JoinIndexRoute,
106+
}
88107

89108
export const routeTree = rootRoute
90109
._addFileChildren(rootRouteChildren)
91-
._addFileTypes<FileRouteTypes>();
110+
._addFileTypes<FileRouteTypes>()
92111

93112
/* ROUTE_MANIFEST_START
94113
{
@@ -97,14 +116,18 @@ export const routeTree = rootRoute
97116
"filePath": "__root.tsx",
98117
"children": [
99118
"/",
100-
"/workspace/$workspaceId"
119+
"/workspace/$workspaceId",
120+
"/join/"
101121
]
102122
},
103123
"/": {
104124
"filePath": "index.tsx"
105125
},
106126
"/workspace/$workspaceId": {
107127
"filePath": "workspace/$workspaceId.tsx"
128+
},
129+
"/join/": {
130+
"filePath": "join/index.tsx"
108131
}
109132
}
110133
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useEffect } from "react";
2+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
3+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4+
5+
import { useValidateWorkspaceInviteLink } from "@/features/workspace";
6+
7+
const joinQueryClient = new QueryClient();
8+
9+
function JoinWrapper() {
10+
return (
11+
<QueryClientProvider client={joinQueryClient}>
12+
<JoinComponent />
13+
</QueryClientProvider>
14+
);
15+
}
16+
17+
export const Route = createFileRoute("/join/")({
18+
validateSearch: (search: Record<string, unknown>) => {
19+
const workspaceId = search.workspaceId as string;
20+
const token = search.token as string;
21+
22+
if (!workspaceId || !token) {
23+
throw new Error("유효한 링크가 아닙니다.");
24+
}
25+
26+
return { workspaceId, token };
27+
},
28+
component: JoinWrapper,
29+
});
30+
31+
function JoinComponent() {
32+
const { workspaceId, token } = Route.useSearch();
33+
const navigate = useNavigate();
34+
const { mutate: validateInvite, isPending } =
35+
useValidateWorkspaceInviteLink();
36+
37+
useEffect(() => {
38+
if (!token || !workspaceId) {
39+
navigate({ to: "/" });
40+
return;
41+
}
42+
43+
validateInvite(token, {
44+
onSuccess: () => {
45+
navigate({
46+
to: "/workspace/$workspaceId",
47+
params: { workspaceId },
48+
replace: true,
49+
});
50+
},
51+
onError: () => {
52+
navigate({ to: "/" });
53+
},
54+
});
55+
}, [token, workspaceId, validateInvite, navigate]);
56+
57+
return (
58+
<div className="flex h-screen items-center justify-center">
59+
<div className="text-center">
60+
<div className="mb-2 text-lg font-medium">
61+
워크스페이스 초대 확인 중
62+
</div>
63+
{isPending && (
64+
<div className="text-sm text-gray-500">
65+
워크스페이스 {workspaceId} 입장 중...
66+
</div>
67+
)}
68+
</div>
69+
</div>
70+
);
71+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2+
3+
import { getUser, logout } from "../api/authApi";
4+
5+
export const useGetUser = () => {
6+
return useQuery({
7+
queryKey: ["user"],
8+
queryFn: getUser,
9+
retry: false,
10+
refetchOnWindowFocus: false,
11+
});
12+
};
13+
14+
export const useLogout = () => {
15+
const queryClient = useQueryClient();
16+
17+
const logoutMutation = useMutation({
18+
mutationFn: logout,
19+
onSuccess: async () => {
20+
queryClient.setQueryData(["user"], null);
21+
await queryClient.invalidateQueries({ queryKey: ["user"] });
22+
},
23+
});
24+
25+
return logoutMutation;
26+
};

apps/frontend/src/features/workspace/api/workspaceApi.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
CreateWorkSpaceResponse,
33
CreateWorkSpaceResquest,
4+
GetCurrentUserWorkspaceResponse,
45
GetUserWorkspaceResponse,
56
RemoveWorkSpaceResponse,
67
} from "../model/workspaceTypes";
@@ -30,3 +31,14 @@ export const getUserWorkspaces = async () => {
3031
const res = await Get<GetUserWorkspaceResponse>(url);
3132
return res.data;
3233
};
34+
35+
export const getCurrentWorkspace = async (
36+
workspaceId: string,
37+
userId: string,
38+
) => {
39+
const url = `${BASE_URL}/${workspaceId}/${userId}`;
40+
41+
// Response type 바꾸기
42+
const res = await Get<GetCurrentUserWorkspaceResponse>(url);
43+
return res.data;
44+
};
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { SetWorkspaceStatusResponse } from "../model/workspaceInviteTypes";
2-
import { Post } from "@/shared/api";
2+
import { Patch } from "@/shared/api";
33

44
export const setWorkspaceStatusToPrivate = async (id: string) => {
5-
// TODO: URL 맞게 고치기.
65
const url = `/api/workspace/${id}/private`;
7-
await Post<SetWorkspaceStatusResponse, null>(url);
6+
await Patch<SetWorkspaceStatusResponse, null>(url);
87
};
98

109
export const setWorkspaceStatusToPublic = async (id: string) => {
11-
// TODO: URL 맞게 고치기.
1210
const url = `/api/workspace/${id}/public`;
13-
await Post<SetWorkspaceStatusResponse, null>(url);
11+
await Patch<SetWorkspaceStatusResponse, null>(url);
1412
};

apps/frontend/src/features/workspace/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export { useUserWorkspace } from "./model/workspaceQuries";
2+
export { useProtectedWorkspace } from "./model/useProtectedWorkspace";
3+
export { useValidateWorkspaceInviteLink } from "./model/workspaceMutations";
24

35
export { ShareTool } from "./ui/ShareTool";
46
export { WorkspaceAddButton } from "./ui/WorkspaceAddButton";
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useEffect } from "react";
2+
import { useNavigate } from "@tanstack/react-router";
3+
import { useCurrentWorkspace } from "@/features/workspace/model/workspaceQuries";
4+
5+
export const useProtectedWorkspace = () => {
6+
const navigate = useNavigate();
7+
const { data: workspaceData, isLoading, error } = useCurrentWorkspace();
8+
9+
useEffect(() => {
10+
if (!isLoading && (error || !workspaceData)) {
11+
navigate({ to: "/" });
12+
}
13+
}, [isLoading, workspaceData, error, navigate]);
14+
15+
return {
16+
isLoading,
17+
workspaceData,
18+
};
19+
};

apps/frontend/src/features/workspace/model/workspaceMutations.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createWorkspaceInviteLink,
1010
validateWorkspaceInviteLink,
1111
} from "../api/worskspaceInviteApi";
12+
import { useWorkspace } from "@/shared/lib";
1213

1314
// response로 workspaceId가 오는데 userWorkspace를 어떻게 invalidate 할까?
1415
// login state에 있는 userId로?
@@ -48,9 +49,9 @@ export const useValidateWorkspaceInviteLink = () => {
4849

4950
export const useToggleWorkspaceStatus = (
5051
currentStatus: "public" | "private" | undefined,
51-
currentWorkspaceId: string,
5252
) => {
5353
const queryClient = useQueryClient();
54+
const currentWorkspaceId = useWorkspace();
5455

5556
return useMutation({
5657
mutationFn: () => {
@@ -67,6 +68,7 @@ export const useToggleWorkspaceStatus = (
6768
},
6869
onSuccess: () => {
6970
queryClient.invalidateQueries({ queryKey: ["userWorkspace"] });
71+
queryClient.invalidateQueries({ queryKey: ["currentWorkspace"] });
7072
},
7173
});
7274
};

0 commit comments

Comments
 (0)