Skip to content

Commit bfb1400

Browse files
authored
Merge branch 'develop' into feature-fe-#316
2 parents 0eebdb1 + bfa57e2 commit bfb1400

20 files changed

+497
-44
lines changed

apps/backend/src/workspace/dtos/getUserWorkspacesResponse.dto.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ export class GetUserWorkspacesResponseDto {
1717
title: 'naver-boostcamp-9th',
1818
description: '네이버 부스트캠프 9기 워크스페이스입니다',
1919
thumbnailUrl: 'https://example.com/image1.png',
20-
role: 'owner',
20+
role: 'guest',
21+
visibility: 'private',
2122
},
2223
{
2324
workspaceId: 'snowflake-id-2',
2425
title: '2024-fall-컴퓨터구조',
2526
description: null,
2627
thumbnailUrl: null,
27-
role: 'guest',
28+
role: 'owner',
29+
visibility: 'public',
2830
},
2931
],
3032
description: '사용자가 속한 모든 워크스페이스 배열',

apps/backend/src/workspace/dtos/userWorkspace.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export class UserWorkspaceDto {
44
description: string | null;
55
thumbnailUrl: string | null;
66
role: 'owner' | 'guest';
7+
visibility: 'public' | 'private';
78
}

apps/backend/src/workspace/workspace.controller.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ describe('WorkspaceController', () => {
2727
generateInviteUrl: jest.fn(),
2828
processInviteUrl: jest.fn(),
2929
checkAccess: jest.fn(),
30+
updateVisibility: jest.fn(),
3031
},
3132
},
3233
{

apps/backend/src/workspace/workspace.controller.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
Post,
44
Delete,
55
Get,
6+
Patch,
67
UseGuards,
78
Request,
89
Body,
@@ -27,6 +28,8 @@ export enum WorkspaceResponseMessage {
2728
WORKSPACE_INVITED = '워크스페이스 게스트 초대 링크가 생성되었습니다.',
2829
WORKSPACE_JOINED = '워크스페이스에 게스트로 등록되었습니다.',
2930
WORKSPACE_ACCESS_CHECKED = '워크스페이스에 대한 사용자의 접근 권한이 확인되었습니다.',
31+
WORKSPACE_UPDATED_TO_PUBLIC = '워크스페이스가 공개로 설정되었습니다.',
32+
WORKSPACE_UPDATED_TO_PRIVATE = '워크스페이스가 비공개로 설정되었습니다.',
3033
}
3134

3235
@Controller('workspace')
@@ -152,4 +155,34 @@ export class WorkspaceController {
152155
message: WorkspaceResponseMessage.WORKSPACE_ACCESS_CHECKED,
153156
};
154157
}
158+
159+
@ApiResponse({
160+
type: MessageResponseDto,
161+
})
162+
@ApiOperation({
163+
summary: '워크스페이스를 비공개에서 공개로 설정합니다.',
164+
})
165+
@Patch('/:id/public')
166+
@UseGuards(JwtAuthGuard) // 로그인 인증
167+
@HttpCode(HttpStatus.OK)
168+
async makeWorkspacePublic(@Request() req, @Param('id') id: string) {
169+
const userId = req.user.sub; // 인증된 사용자 ID
170+
await this.workspaceService.updateVisibility(userId, id, 'public');
171+
return { message: WorkspaceResponseMessage.WORKSPACE_UPDATED_TO_PUBLIC };
172+
}
173+
174+
@ApiResponse({
175+
type: MessageResponseDto,
176+
})
177+
@ApiOperation({
178+
summary: '워크스페이스를 공개에서 비공개로 설정합니다.',
179+
})
180+
@Patch('/:id/private')
181+
@UseGuards(JwtAuthGuard) // 로그인 인증
182+
@HttpCode(HttpStatus.OK)
183+
async makeWorkspacePrivate(@Request() req, @Param('id') id: string) {
184+
const userId = req.user.sub; // 인증된 사용자 ID
185+
await this.workspaceService.updateVisibility(userId, id, 'private');
186+
return { message: WorkspaceResponseMessage.WORKSPACE_UPDATED_TO_PRIVATE };
187+
}
155188
}

apps/backend/src/workspace/workspace.service.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ describe('WorkspaceService', () => {
256256
description: 'Workspace Description',
257257
thumbnailUrl: 'http://example.com/thumbnail.png',
258258
role: 'owner',
259+
visibility: 'private',
259260
},
260261
]);
261262
expect(roleRepository.find).toHaveBeenCalledWith({

apps/backend/src/workspace/workspace.service.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export class WorkspaceService {
110110
description: role.workspace.description || null,
111111
thumbnailUrl: role.workspace.thumbnailUrl || null,
112112
role: role.role as 'owner' | 'guest',
113+
visibility: role.workspace.visibility as 'public' | 'private',
113114
}));
114115
}
115116

@@ -264,4 +265,34 @@ export class WorkspaceService {
264265
});
265266
}
266267
}
268+
269+
async updateVisibility(
270+
userId: number,
271+
workspaceId: string,
272+
visibility: 'public' | 'private',
273+
): Promise<void> {
274+
// 워크스페이스가 존재하는지 확인
275+
const workspace = await this.workspaceRepository.findOneBy({
276+
snowflakeId: workspaceId,
277+
});
278+
279+
if (!workspace) {
280+
throw new WorkspaceNotFoundException();
281+
}
282+
283+
// Role Repository에서 해당 workspace의 owner인지 확인
284+
const role = await this.roleRepository.findOneBy({
285+
workspaceId: workspace.id,
286+
userId: userId,
287+
role: 'owner',
288+
});
289+
// 아니면 exception 뱉기
290+
if (!role) {
291+
throw new NotWorkspaceOwnerException();
292+
}
293+
294+
// 가시성 변경
295+
workspace.visibility = visibility;
296+
await this.workspaceRepository.save(workspace);
297+
}
267298
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Post } from "@/shared/api";
2+
import { SetWorkspaceStatusResponse } from "../model/workspaceInviteTypes";
3+
4+
export const setWorkspaceStatusToPrivate = async (id: string) => {
5+
// TODO: URL 맞게 고치기.
6+
const url = `/api/workspace/${id}/private`;
7+
await Post<SetWorkspaceStatusResponse, null>(url);
8+
};
9+
10+
export const setWorkspaceStatusToPublic = async (id: string) => {
11+
// TODO: URL 맞게 고치기.
12+
const url = `/api/workspace/${id}/public`;
13+
await Post<SetWorkspaceStatusResponse, null>(url);
14+
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Get, Post } from "@/shared/api";
2+
import {
3+
WorkspaceInviteLinkRequest,
4+
WorkspaceInviteLinkResponse,
5+
ValidateWorkspaceLinkResponse,
6+
} from "@/features/workspace/model/workspaceInviteTypes";
7+
8+
export const createWorkspaceInviteLink = async (id: string) => {
9+
const url = `/api/workspace/${id}/invite`;
10+
11+
const res = await Post<
12+
WorkspaceInviteLinkResponse,
13+
WorkspaceInviteLinkRequest
14+
>(url, { id });
15+
16+
return res.data.inviteUrl;
17+
};
18+
19+
export const validateWorkspaceInviteLink = async (token: string) => {
20+
const url = `/api/workspace/join?token=${token}`;
21+
const res = await Get<ValidateWorkspaceLinkResponse>(url);
22+
return res.data;
23+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { create } from "zustand";
2+
3+
type InviteLinkStore = {
4+
inviteLink: string | null;
5+
setInviteLink: (link: string) => void;
6+
};
7+
8+
export const useInviteLinkStore = create<InviteLinkStore>((set) => ({
9+
inviteLink: null,
10+
setInviteLink: (link) => set({ inviteLink: link }),
11+
}));
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import {
3+
createWorkspaceInviteLink,
4+
validateWorkspaceInviteLink,
5+
} from "../api/worskspaceInviteApi";
6+
7+
export const useCreateWorkspaceInviteLink = () => {
8+
return useMutation({
9+
mutationFn: (id: string) => createWorkspaceInviteLink(id),
10+
});
11+
};
12+
13+
export const useValidateWorkspaceInviteLink = () => {
14+
return useMutation({
15+
mutationFn: (token: string) => validateWorkspaceInviteLink(token),
16+
});
17+
};

0 commit comments

Comments
 (0)