From bbdc8ab2a09e207e40490d8f010b387573385b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EC=84=B1=EC=A7=84?= Date: Wed, 8 Jul 2026 12:07:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=EC=9E=90=EB=A3=8C=EC=8B=A4=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=B0=8F=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=AA=A9=EC=97=85=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspaces/[workspaceId]/files/page.tsx | 14 ++ src/entities/resource/index.ts | 8 + src/entities/resource/model/mock-resources.ts | 140 ++++++++++++++ src/entities/resource/model/resource.types.ts | 26 +++ src/entities/workspace/index.ts | 2 +- .../workspace/model/mock-workspace.ts | 18 +- src/features/manage-resources/index.ts | 4 + .../model/use-resource-library-state.ts | 82 ++++++++ .../manage-resources/ui/ResourceAddDialog.tsx | 179 ++++++++++++++++++ .../manage-resources/ui/ResourceList.tsx | 77 ++++++++ src/views/store-operation/resources/index.ts | 2 + .../resources/ui/ResourcesView.tsx | 62 ++++++ .../workspace-shell/ui/WorkspaceHeader.tsx | 2 +- 13 files changed, 613 insertions(+), 3 deletions(-) create mode 100644 src/app/workspaces/[workspaceId]/files/page.tsx create mode 100644 src/entities/resource/index.ts create mode 100644 src/entities/resource/model/mock-resources.ts create mode 100644 src/entities/resource/model/resource.types.ts create mode 100644 src/features/manage-resources/index.ts create mode 100644 src/features/manage-resources/model/use-resource-library-state.ts create mode 100644 src/features/manage-resources/ui/ResourceAddDialog.tsx create mode 100644 src/features/manage-resources/ui/ResourceList.tsx create mode 100644 src/views/store-operation/resources/index.ts create mode 100644 src/views/store-operation/resources/ui/ResourcesView.tsx diff --git a/src/app/workspaces/[workspaceId]/files/page.tsx b/src/app/workspaces/[workspaceId]/files/page.tsx new file mode 100644 index 0000000..8758d1e --- /dev/null +++ b/src/app/workspaces/[workspaceId]/files/page.tsx @@ -0,0 +1,14 @@ +// 워크스페이스 자료실 페이지의 라우트 진입점입니다. +import { ResourcesView } from '@/views/store-operation/resources'; + +interface FilesPageProps { + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function FilesPage({ params }: FilesPageProps) { + const { workspaceId } = await params; + + return ; +} diff --git a/src/entities/resource/index.ts b/src/entities/resource/index.ts new file mode 100644 index 0000000..2f9a981 --- /dev/null +++ b/src/entities/resource/index.ts @@ -0,0 +1,8 @@ +// 자료실 도메인의 타입과 목업 데이터 공개 API입니다. +export type { + ResourceFormValues, + ResourceItem, + ResourceLinkProvider, + ResourceType, +} from './model/resource.types'; +export { mockResources } from './model/mock-resources'; diff --git a/src/entities/resource/model/mock-resources.ts b/src/entities/resource/model/mock-resources.ts new file mode 100644 index 0000000..c859ca8 --- /dev/null +++ b/src/entities/resource/model/mock-resources.ts @@ -0,0 +1,140 @@ +// Supabase Storage와 resources 테이블 연결 전 자료실 화면에서 사용하는 목업 데이터입니다. +import type { ResourceItem } from './resource.types'; + +export const mockResources: ResourceItem[] = [ + { + id: 'resource-1', + workspaceId: 'store-workspace', + title: '7월 신메뉴 레시피.pdf', + description: '망고라떼, 피치에이드 레시피', + resourceType: 'file', + fileName: '7월 신메뉴 레시피.pdf', + uploadedBy: '김민서', + createdAt: '2025-06-28', + }, + { + id: 'resource-2', + workspaceId: 'store-workspace', + title: '6월 매출 정산.xls', + description: '일별 매출 집계표', + resourceType: 'file', + fileName: '6월 매출 정산.xlsx', + uploadedBy: '이준혁', + createdAt: '2025-06-30', + }, + { + id: 'resource-3', + workspaceId: 'store-workspace', + title: '재고 관리 시트', + description: 'Google 스프레드시트', + resourceType: 'link', + linkProvider: 'link', + url: 'https://docs.google.com/spreadsheets', + uploadedBy: '김민서', + createdAt: '2025-06-01', + }, + { + id: 'resource-4', + workspaceId: 'store-workspace', + title: '카드단말기 사용설명서.pdf', + description: '신형 단말기 매뉴얼', + resourceType: 'file', + fileName: '카드단말기 사용설명서.pdf', + uploadedBy: '이준혁', + createdAt: '2025-06-22', + }, + { + id: 'resource-5', + workspaceId: 'team-workspace', + title: '캡스톤 기획서 v2.pdf', + description: '최종 기획 문서', + resourceType: 'file', + fileName: '캡스톤 기획서 v2.pdf', + uploadedBy: '김지은', + createdAt: '2025-06-28', + }, + { + id: 'resource-6', + workspaceId: 'team-workspace', + title: 'Figma 디자인 파일', + description: 'UI 컴포넌트 모음', + resourceType: 'link', + linkProvider: 'figma', + url: 'https://figma.com', + uploadedBy: '박서준', + createdAt: '2025-06-26', + }, + { + id: 'resource-7', + workspaceId: 'team-workspace', + title: '사용자 인터뷰 결과.xlsx', + description: '인터뷰 응답 원본 데이터', + resourceType: 'file', + fileName: '사용자 인터뷰 결과.xlsx', + uploadedBy: '이하은', + createdAt: '2025-06-24', + }, + { + id: 'resource-8', + workspaceId: 'team-workspace', + title: 'API 명세서 (Notion)', + description: 'REST API 문서', + resourceType: 'link', + linkProvider: 'notion', + url: 'https://notion.so', + uploadedBy: '최민준', + createdAt: '2025-06-22', + }, + { + id: 'resource-9', + workspaceId: 'team-workspace', + title: '로고 최종본.zip', + description: 'PNG, SVG, AI 파일 포함', + resourceType: 'file', + fileName: '로고 최종본.zip', + uploadedBy: '박서준', + createdAt: '2025-06-20', + }, + { + id: 'resource-10', + workspaceId: 'side-workspace', + title: 'Fitto 피그마 파일', + description: '전체 UI/X 디자인', + resourceType: 'link', + linkProvider: 'figma', + url: 'http://figma.com', + uploadedBy: '박서준', + createdAt: '2025-06-30', + }, + { + id: 'resource-11', + workspaceId: 'side-workspace', + title: 'Github Repository', + description: '매인 코드 저장소', + resourceType: 'link', + linkProvider: 'github', + url: 'http://github.com', + uploadedBy: '이하은', + createdAt: '2025-06-31', + }, + { + id: 'resource-12', + workspaceId: 'side-workspace', + title: 'API 명세서 v3.pdf', + description: 'REST API 전체 문제', + resourceType: 'file', + fileName: 'API 명세서 v3.pdf', + uploadedBy: '이하은', + createdAt: '2025-06-29', + }, + { + id: 'resource-13', + workspaceId: 'side-workspace', + title: '기획서_Fitto_v2.pdf', + description: '서비스 기획 문서', + resourceType: 'file', + fileName: '기획서_Fitto_v2.pdf', + uploadedBy: '김지은', + createdAt: '2025-06-15', + }, +]; diff --git a/src/entities/resource/model/resource.types.ts b/src/entities/resource/model/resource.types.ts new file mode 100644 index 0000000..63a8436 --- /dev/null +++ b/src/entities/resource/model/resource.types.ts @@ -0,0 +1,26 @@ +// 자료실에서 파일과 외부 링크를 같은 목록으로 다루기 위한 타입입니다. +export type ResourceType = 'file' | 'link'; + +export type ResourceLinkProvider = 'link' | 'notion' | 'figma' | 'github'; + +export interface ResourceItem { + id: string; + workspaceId: string; + title: string; + description: string; + resourceType: ResourceType; + linkProvider?: ResourceLinkProvider; + url?: string; + fileName?: string; + uploadedBy: string; + createdAt: string; +} + +export interface ResourceFormValues { + resourceType: ResourceType; + title: string; + description: string; + url: string; + fileName: string; + linkProvider: ResourceLinkProvider; +} diff --git a/src/entities/workspace/index.ts b/src/entities/workspace/index.ts index d2cd1b1..56afa9c 100644 --- a/src/entities/workspace/index.ts +++ b/src/entities/workspace/index.ts @@ -1,6 +1,6 @@ // workspace 엔티티 Public API export type { Workspace, WorkspacePurpose, WorkspaceSummary } from './model/workspace.types'; -export { getMockWorkspaceById, mockWorkspace } from './model/mock-workspace'; +export { getMockWorkspaceById, mockWorkspace, mockWorkspaces } from './model/mock-workspace'; export { WORKSPACE_PURPOSE_META, FALLBACK_PURPOSE_META } from './config/purpose'; export { WORKSPACE_TEMPLATE_DETAIL, diff --git a/src/entities/workspace/model/mock-workspace.ts b/src/entities/workspace/model/mock-workspace.ts index b352c01..edb1f4b 100644 --- a/src/entities/workspace/model/mock-workspace.ts +++ b/src/entities/workspace/model/mock-workspace.ts @@ -1,4 +1,4 @@ -// 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다. +// 워크스페이스 API 연결 전 사이드바와 purpose별 navigation 확인에 사용하는 목업입니다. import { cache } from 'react'; import type { Workspace } from './workspace.types'; @@ -8,14 +8,30 @@ const mockWorkspacesById: Record = { name: '캡스톤 디자인 팀', purpose: 'team-project', }, + 'team-workspace': { + id: 'team-workspace', + name: '캡스톤 디자인 팀', + purpose: 'team-project', + }, + 'side-workspace': { + id: 'side-workspace', + name: 'Fitto 앱 개발팀', + purpose: 'side-project', + }, 'store-test': { id: 'store-test', name: '카페 그레이 운영', purpose: 'store-operation', }, + 'store-workspace': { + id: 'store-workspace', + name: '카페 그레이 운영', + purpose: 'store-operation', + }, }; export const mockWorkspace: Workspace = mockWorkspacesById['store-test']; +export const mockWorkspaces: Workspace[] = Object.values(mockWorkspacesById); export const getMockWorkspaceById = cache( (workspaceId: string): Workspace | null => mockWorkspacesById[workspaceId] ?? null, diff --git a/src/features/manage-resources/index.ts b/src/features/manage-resources/index.ts new file mode 100644 index 0000000..cc20096 --- /dev/null +++ b/src/features/manage-resources/index.ts @@ -0,0 +1,4 @@ +// 자료실 관리 기능의 상태 훅과 UI 컴포넌트 공개 API입니다. +export { useResourceLibraryState } from './model/use-resource-library-state'; +export { ResourceAddDialog } from './ui/ResourceAddDialog'; +export { ResourceList } from './ui/ResourceList'; diff --git a/src/features/manage-resources/model/use-resource-library-state.ts b/src/features/manage-resources/model/use-resource-library-state.ts new file mode 100644 index 0000000..a81c5d4 --- /dev/null +++ b/src/features/manage-resources/model/use-resource-library-state.ts @@ -0,0 +1,82 @@ +'use client'; + +// 자료실 목록과 파일/링크 추가 모달의 클라이언트 목업 상태를 관리합니다. +import { useMemo, useState } from 'react'; +import type { ResourceFormValues, ResourceItem, ResourceType } from '@/entities/resource'; + +interface UseResourceLibraryStateParams { + initialResources: ResourceItem[]; + workspaceId: string; + uploaderName: string; +} + +function sortResources(resources: ResourceItem[]) { + return [...resources].sort((first, second) => second.createdAt.localeCompare(first.createdAt)); +} + +function createResourceId() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return `resource-${crypto.randomUUID()}`; + } + + return `resource-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +} + +function createTodayLabel() { + return new Date().toISOString().slice(0, 10); +} + +export function useResourceLibraryState({ + initialResources, + workspaceId, + uploaderName, +}: UseResourceLibraryStateParams) { + const workspaceResources = useMemo( + () => sortResources(initialResources.filter((resource) => resource.workspaceId === workspaceId)), + [initialResources, workspaceId], + ); + const [resources, setResources] = useState(workspaceResources); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [dialogResourceType, setDialogResourceType] = useState('file'); + + const addResource = (values: ResourceFormValues) => { + const title = values.title.trim() || values.fileName.trim() || values.url.trim(); + const description = values.description.trim(); + + if (!title) { + return; + } + + if (values.resourceType === 'link' && !values.url.trim()) { + return; + } + + const nextResource: ResourceItem = { + id: createResourceId(), + workspaceId, + title, + description, + resourceType: values.resourceType, + linkProvider: values.resourceType === 'link' ? values.linkProvider : undefined, + url: values.resourceType === 'link' ? values.url.trim() : undefined, + fileName: values.resourceType === 'file' ? values.fileName.trim() : undefined, + uploadedBy: uploaderName, + createdAt: createTodayLabel(), + }; + + setResources((currentResources) => sortResources([nextResource, ...currentResources])); + setIsDialogOpen(false); + }; + + return { + resources, + isDialogOpen, + dialogResourceType, + openDialog: (nextResourceType: ResourceType) => { + setDialogResourceType(nextResourceType); + setIsDialogOpen(true); + }, + closeDialog: () => setIsDialogOpen(false), + addResource, + }; +} diff --git a/src/features/manage-resources/ui/ResourceAddDialog.tsx b/src/features/manage-resources/ui/ResourceAddDialog.tsx new file mode 100644 index 0000000..4b135ee --- /dev/null +++ b/src/features/manage-resources/ui/ResourceAddDialog.tsx @@ -0,0 +1,179 @@ +'use client'; + +// 자료실에 파일 목업 또는 외부 링크 목업을 추가하는 모달입니다. +import { useState } from 'react'; +import { CloudUpload, GitBranch, Link, X } from 'lucide-react'; +import type { ResourceFormValues, ResourceLinkProvider, ResourceType } from '@/entities/resource'; +import { cn } from '@/shared/lib/utils'; + +interface ResourceAddDialogProps { + isOpen: boolean; + initialResourceType: ResourceType; + onClose: () => void; + onSubmit: (values: ResourceFormValues) => void; +} + +const linkProviders: Array<{ + label: string; + value: ResourceLinkProvider; + icon: typeof Link; +}> = [ + { label: '링크', value: 'link', icon: Link }, + { label: '노션', value: 'notion', icon: Link }, + { label: '피그마', value: 'figma', icon: Link }, + { label: '깃허브', value: 'github', icon: GitBranch }, +]; + +export function ResourceAddDialog({ + isOpen, + initialResourceType, + onClose, + onSubmit, +}: ResourceAddDialogProps) { + const [resourceType, setResourceType] = useState(initialResourceType); + const [linkProvider, setLinkProvider] = useState('link'); + const [url, setUrl] = useState(''); + const [fileName, setFileName] = useState(''); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + + if (!isOpen) { + return null; + } + + const canSubmit = resourceType === 'file' ? fileName.trim().length > 0 : url.trim().length > 0; + + const submitResource = () => { + onSubmit({ + resourceType, + linkProvider, + url, + fileName, + title, + description, + }); + setUrl(''); + setFileName(''); + setTitle(''); + setDescription(''); + }; + + return ( +
+
+
+

자료 추가

+ +
+ +
+ {(['link', 'file'] as ResourceType[]).map((type) => ( + + ))} +
+ +
+ {resourceType === 'file' ? ( + + ) : ( + <> +
+ {linkProviders.map((provider) => { + const Icon = provider.icon; + + return ( + + ); + })} +
+ + setUrl(event.target.value)} + placeholder="https://github.com/TeampleRun/syncly" + className="h-11 w-full rounded-xl bg-slate-100 px-4 text-sm font-medium text-slate-800 outline-none placeholder:text-slate-400 focus:ring-2 focus:ring-indigo-300" + /> + + )} + + setTitle(event.target.value)} + placeholder={resourceType === 'file' ? '자료 제목 (비우면 파일명 사용)' : '자료 제목'} + className="h-11 w-full rounded-xl bg-slate-100 px-4 text-sm font-medium text-slate-800 outline-none placeholder:text-slate-400 focus:ring-2 focus:ring-indigo-300" + /> +