diff --git a/src/app/workspaces/[workspaceId]/meeting-notes/new/page.tsx b/src/app/workspaces/[workspaceId]/meeting-notes/new/page.tsx index dab95ba..7b2cce0 100644 --- a/src/app/workspaces/[workspaceId]/meeting-notes/new/page.tsx +++ b/src/app/workspaces/[workspaceId]/meeting-notes/new/page.tsx @@ -1,4 +1,5 @@ import { NewMeetingNotePage } from '@/views/meeting-notes'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; interface WorkspaceNewMeetingNotePageProps { params: Promise<{ @@ -10,6 +11,7 @@ export default async function WorkspaceNewMeetingNotePage({ params, }: WorkspaceNewMeetingNotePageProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'meeting-notes'); return ; } diff --git a/src/app/workspaces/[workspaceId]/meeting-notes/page.tsx b/src/app/workspaces/[workspaceId]/meeting-notes/page.tsx index 84e4faa..47c125d 100644 --- a/src/app/workspaces/[workspaceId]/meeting-notes/page.tsx +++ b/src/app/workspaces/[workspaceId]/meeting-notes/page.tsx @@ -1,4 +1,5 @@ import { MeetingNotesPage } from '@/views/meeting-notes'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; interface WorkspaceMeetingNotesPageProps { params: Promise<{ @@ -10,6 +11,7 @@ export default async function WorkspaceMeetingNotesPage({ params, }: WorkspaceMeetingNotesPageProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'meeting-notes'); return ; } diff --git a/src/app/workspaces/[workspaceId]/notices/page.tsx b/src/app/workspaces/[workspaceId]/notices/page.tsx index 8e069b9..40ef63b 100644 --- a/src/app/workspaces/[workspaceId]/notices/page.tsx +++ b/src/app/workspaces/[workspaceId]/notices/page.tsx @@ -1,5 +1,6 @@ // 워크스페이스 공지 페이지의 라우트 진입점입니다. import { getNoticeBoard } from '@/entities/notice/api/get-notice-board'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; import { NoticesView } from '@/views/store-operation/notices'; interface NoticesPageProps { @@ -10,6 +11,7 @@ interface NoticesPageProps { export default async function NoticesPage({ params }: NoticesPageProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'notices'); const initialData = await getNoticeBoard(workspaceId); return ; diff --git a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx index a6b5722..2aa5514 100644 --- a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx +++ b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx @@ -1,5 +1,6 @@ // 진행률 차트 라우트 — 워크스페이스 존재/용도만 서버에서 판정하고, 데이터 조회·조립은 client 컨테이너(useQuery)에 위임한다. import { ProgressChartPage } from '@/views/progress-chart'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; interface WorkspaceProgressChartPageProps { params: Promise<{ @@ -11,6 +12,7 @@ export default async function WorkspaceProgressChartPage({ params, }: WorkspaceProgressChartPageProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'progress-chart'); return ; } diff --git a/src/app/workspaces/[workspaceId]/project-management/page.tsx b/src/app/workspaces/[workspaceId]/project-management/page.tsx index 3b86497..66d5c39 100644 --- a/src/app/workspaces/[workspaceId]/project-management/page.tsx +++ b/src/app/workspaces/[workspaceId]/project-management/page.tsx @@ -1,4 +1,5 @@ import { ProjectManagementPage } from '@/views/project-management'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; interface WorkspaceProjectManagementPageProps { params: Promise<{ @@ -10,6 +11,7 @@ export default async function WorkspaceProjectManagementPage({ params, }: WorkspaceProjectManagementPageProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'project-management'); return ; } diff --git a/src/app/workspaces/[workspaceId]/sprint-board/page.tsx b/src/app/workspaces/[workspaceId]/sprint-board/page.tsx index 7115077..e6a0049 100644 --- a/src/app/workspaces/[workspaceId]/sprint-board/page.tsx +++ b/src/app/workspaces/[workspaceId]/sprint-board/page.tsx @@ -1,6 +1,7 @@ // 스프린트 보드 라우트 — 선택 스프린트를 searchParam(?sprint=id)으로 읽어 client 컨테이너에 넘긴다. // 스프린트/태스크는 컨테이너가 useQuery로 조회하고, 담당자 표시명 해석용 members만 서버에서 조회해 주입한다. import { getWorkspaceMembersByWorkspaceId } from '@/entities/workspace-member/api/get-workspace-members-by-id'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; import { SprintBoardView } from '@/views/side-project/sprint-board'; interface SprintBoardRouteProps { @@ -10,6 +11,7 @@ interface SprintBoardRouteProps { export default async function SprintBoardPage({ params, searchParams }: SprintBoardRouteProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'sprint-board'); const { sprint: sprintParam } = await searchParams; const selectedSprintId = typeof sprintParam === 'string' ? sprintParam : undefined; diff --git a/src/app/workspaces/[workspaceId]/unavailable/page.tsx b/src/app/workspaces/[workspaceId]/unavailable/page.tsx new file mode 100644 index 0000000..5807b33 --- /dev/null +++ b/src/app/workspaces/[workspaceId]/unavailable/page.tsx @@ -0,0 +1,14 @@ +// 목적에 맞지 않는 워크스페이스 전용 모듈에 직접 접근했을 때 표시하는 라우트입니다. +import { WorkspaceUnavailableView } from '@/views/workspace-unavailable'; + +interface WorkspaceUnavailablePageProps { + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function WorkspaceUnavailablePage({ params }: WorkspaceUnavailablePageProps) { + const { workspaceId } = await params; + + return ; +} diff --git a/src/app/workspaces/[workspaceId]/work-schedule/page.tsx b/src/app/workspaces/[workspaceId]/work-schedule/page.tsx index 22d4b40..90a291f 100644 --- a/src/app/workspaces/[workspaceId]/work-schedule/page.tsx +++ b/src/app/workspaces/[workspaceId]/work-schedule/page.tsx @@ -1,5 +1,6 @@ // 현재 주의 멤버, 근무유형, 스케줄 데이터를 병렬 조회해 근무 스케줄 화면에 전달하는 서버 페이지입니다. import { getWorkspaceMembersByWorkspaceId } from '@/entities/workspace-member/api/get-workspace-members-by-id'; +import { assertWorkspaceRouteAccess } from '@/entities/workspace/lib/assert-workspace-route-access'; import { getWorkScheduleEntriesByWeek } from '@/entities/work-schedule/api/get-work-schedule-entries-by-week'; import { getWorkShiftTypesByWorkspaceId } from '@/entities/work-schedule/api/get-work-shift-types-by-workspace-id'; import { ensureWeeklyWorkScheduleEntries } from '@/entities/work-schedule/api/ensure-weekly-work-schedule-entries'; @@ -14,6 +15,7 @@ interface WorkSchedulePageProps { export default async function WorkSchedulePage({ params }: WorkSchedulePageProps) { const { workspaceId } = await params; + await assertWorkspaceRouteAccess(workspaceId, 'work-schedule'); const { startDate, endDate } = getCurrentWeekRange(); await ensureWeeklyWorkScheduleEntries(workspaceId, startDate); const [members, shifts, schedule] = await Promise.all([ diff --git a/src/entities/workspace/lib/assert-workspace-route-access.ts b/src/entities/workspace/lib/assert-workspace-route-access.ts new file mode 100644 index 0000000..2c70da9 --- /dev/null +++ b/src/entities/workspace/lib/assert-workspace-route-access.ts @@ -0,0 +1,33 @@ +// 워크스페이스 목적에 맞지 않는 전용 모듈 URL의 직접 접근을 서버에서 차단합니다. +import { redirect } from 'next/navigation'; +import { getWorkspaceById } from '../api/get-workspace-by-id'; +import type { WorkspacePurpose } from '../model/workspace.types'; + +type WorkspaceProtectedRoute = + | 'meeting-notes' + | 'notices' + | 'progress-chart' + | 'project-management' + | 'sprint-board' + | 'work-schedule'; + +// 각 목적에서 제공하는 전용 모듈 라우트를 정의합니다. 공통 모듈은 별도 보호가 필요하지 않습니다. +const workspaceRoutePurposes: Record = { + 'meeting-notes': ['team-project', 'side-project'], + notices: ['team-project', 'store-operation'], + 'progress-chart': ['team-project', 'side-project'], + 'project-management': ['team-project'], + 'sprint-board': ['side-project'], + 'work-schedule': ['store-operation'], +}; + +export async function assertWorkspaceRouteAccess( + workspaceId: string, + route: WorkspaceProtectedRoute, +): Promise { + const workspace = await getWorkspaceById(workspaceId); + + if (!workspace || !workspaceRoutePurposes[route].includes(workspace.purpose)) { + redirect(`/workspaces/${workspaceId}/unavailable`); + } +} diff --git a/src/views/workspace-unavailable/index.ts b/src/views/workspace-unavailable/index.ts new file mode 100644 index 0000000..983a3d2 --- /dev/null +++ b/src/views/workspace-unavailable/index.ts @@ -0,0 +1,2 @@ +// 접근할 수 없는 워크스페이스 전용 모듈 화면의 Public API입니다. +export { WorkspaceUnavailableView } from './ui/WorkspaceUnavailableView'; diff --git a/src/views/workspace-unavailable/ui/WorkspaceUnavailableView.tsx b/src/views/workspace-unavailable/ui/WorkspaceUnavailableView.tsx new file mode 100644 index 0000000..6932a6a --- /dev/null +++ b/src/views/workspace-unavailable/ui/WorkspaceUnavailableView.tsx @@ -0,0 +1,30 @@ +// 현재 워크스페이스 목적에서 제공하지 않는 모듈에 접근했을 때 안내하는 화면입니다. +import { ArrowLeft, ShieldAlert } from 'lucide-react'; +import Link from 'next/link'; + +interface WorkspaceUnavailableViewProps { + workspaceId: string; +} + +export function WorkspaceUnavailableView({ workspaceId }: WorkspaceUnavailableViewProps) { + return ( +
+
+ + +

접근할 수 없는 페이지입니다.

+

+ 현재 워크스페이스 템플릿에서는 이 기능을 사용할 수 없습니다. +

+ +
+
+ ); +} diff --git a/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx b/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx index 0158ea9..36259e8 100644 --- a/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx +++ b/src/widgets/workspace-shell/ui/WorkspaceHeader.tsx @@ -21,6 +21,7 @@ interface WorkspaceHeaderProps { function getCurrentPageTitle(pathname: string, navigationItems: WorkspaceNavigationItem[]): string { if (pathname.endsWith('/notifications')) return '알림'; + if (pathname.endsWith('/unavailable')) return '접근 제한'; const currentNavigationItem = navigationItems.find((item) => pathname.endsWith(`/${item.href}`)); return currentNavigationItem?.label ?? '대시보드';