diff --git a/src/app/routes/paths.ts b/src/app/routes/paths.ts index 075a6e19..6807b064 100644 --- a/src/app/routes/paths.ts +++ b/src/app/routes/paths.ts @@ -1,4 +1,5 @@ import MypageNoticeIcon from '@/shared/assets/mypage/loudspeaker.svg?react'; +import MypagePortfolio from '@/shared/assets/mypage/portfolio.svg?react'; import MypageTermOfServiceIcon from '@/shared/assets/mypage/term-of-service.svg?react'; import MypageProfileIcon from '@/shared/assets/mypage/update-profile.svg?react'; import FixedMemoList from '@/widgets/memo/FixedMemoList'; @@ -29,6 +30,7 @@ export const ROUTE_SEGMENTS = { MEMO_EDIT: 'edit/:memoId', MY_PAGE: 'mypage', MY_PAGE_PROFILE: 'mypage/profile', + MY_PAGE_PORTFOLIO: 'mypage/portfolio', NOTICE: 'notice', END: 'end', } as const; @@ -66,6 +68,8 @@ export const PATHS = { MY_PAGE: (teamId: number) => `${basePath(teamId)}/${ROUTE_SEGMENTS.MY_PAGE}`, PROFILE: (teamId: number) => `${basePath(teamId)}/${ROUTE_SEGMENTS.MY_PAGE_PROFILE}`, + PORTFOLIO: (teamId: number) => + `${basePath(teamId)}/${ROUTE_SEGMENTS.MY_PAGE_PORTFOLIO}`, /* 공지사항 페이지 */ NOTICE: (teamId: number) => `${basePath(teamId)}/${ROUTE_SEGMENTS.NOTICE}`, @@ -114,6 +118,7 @@ export const mainRoutes: RouteConfig[] = [ export const mypageRoutes: RouteConfig[] = [ { label: '프로필 수정', to: PATHS.PROFILE, icon: MypageProfileIcon }, + { label: '포트폴리오', to: PATHS.PORTFOLIO, icon: MypagePortfolio }, { label: '공지사항', to: PATHS.NOTICE, icon: MypageNoticeIcon }, { label: '이용약관 및 개인정보처리방침', diff --git a/src/app/routes/routes.tsx b/src/app/routes/routes.tsx index 77ec6668..f473aa65 100644 --- a/src/app/routes/routes.tsx +++ b/src/app/routes/routes.tsx @@ -11,6 +11,7 @@ import MakeTeamPage from '@/pages/MakeTeamPage/make-team'; import { ManagementPage } from '@/pages/ManagementPage'; import { ExtraMemoPage, RedirectToRootFolder } from '@/pages/MemoPage'; import { MyPage } from '@/pages/MyPage'; +import Portfolio from '@/pages/MyPage/portfolio.tsx'; import ProfilePage from '@/pages/MyPage/profile'; import NoticePage from '@/pages/NoticePage/notice'; import Redirect from '@/pages/RedirectPage/redirect'; @@ -87,6 +88,10 @@ export default function AppRoutes() { path={ROUTE_SEGMENTS.MY_PAGE_PROFILE} element={} /> + } + /> {/* 공지 페이지 */} } /> diff --git a/src/entities/management/ui/Schedule.tsx b/src/entities/management/ui/Schedule.tsx index ce28bded..1c79da50 100644 --- a/src/entities/management/ui/Schedule.tsx +++ b/src/entities/management/ui/Schedule.tsx @@ -44,10 +44,6 @@ export const Schedule = ({ const { isOpen, setIsOpen, toggle } = useToggle(); - useEffect(() => { - console.log('팀 스케줄? ', schedule); - }, [schedule]); - useEffect(() => { if (members?.length && selectedMembers.length === 0) { setSelectedMembers(members); diff --git a/src/entities/portfolio/model/portfolio.mock.ts b/src/entities/portfolio/model/portfolio.mock.ts new file mode 100644 index 00000000..3e217f82 --- /dev/null +++ b/src/entities/portfolio/model/portfolio.mock.ts @@ -0,0 +1,30 @@ +export const MOCK_PROJECTS = [ + { + id: 1, + name: '팀매니저 ver.1', + duration: '2024.07 ~ 2024.08', + }, + { + id: 2, + name: '팀매니저 ver.2', + duration: '2024.09 ~ ing', + }, +]; + +export const projectInfoMock = [ + { + id: 1, + title: '프로젝트 카테고리', + tags: ['팀매니저', '팀프로젝트'], + }, + { + id: 2, + title: '프로젝트 멤버', + tags: ['지유', '안지유'], + }, + { + id: 3, + title: '나의 역할', + tags: ['개발자', '프론트엔드'], + }, +]; diff --git a/src/entities/portfolio/ui/ProjectCard.tsx b/src/entities/portfolio/ui/ProjectCard.tsx new file mode 100644 index 00000000..7f1645ee --- /dev/null +++ b/src/entities/portfolio/ui/ProjectCard.tsx @@ -0,0 +1,78 @@ +import styled from 'styled-components'; +import ArrowIcon from '@/shared/assets/common/arrow.svg?react'; + +interface ProjectCardProps { + id: number; + name: string; + duration: string; + selected: boolean; + onSelect: (id: number) => void; +} + +export function ProjectCard({ + id, + name, + duration, + selected, + onSelect, +}: ProjectCardProps) { + return ( + onSelect(id)}> + + {name} + {duration} + + + + + + ); +} + +const CardContainer = styled.div<{ $selected: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + width: 487px; + height: 64px; + border-radius: 7px; + border: 1px solid + ${({ $selected, theme }) => + $selected ? theme.colors.mainBlue : theme.colors.lightGray}; + padding: 0 16px; + cursor: pointer; + + background: ${({ $selected, theme }) => + $selected ? theme.colors.background : 'white'}; +`; + +const ProjectName = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + +const Name = styled.p<{ $selected: boolean }>` + font-size: 16px; + font-weight: ${({ $selected }) => ($selected ? 700 : 500)}; + color: ${({ theme }) => theme.colors.black}; +`; + +const Duration = styled.p` + font-size: 12px; + font-weight: 400; + color: ${({ theme }) => theme.colors.gray}; +`; + +const ArrowWrapper = styled.div` + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/src/entities/portfolio/ui/ProjectInfoSection.tsx b/src/entities/portfolio/ui/ProjectInfoSection.tsx new file mode 100644 index 00000000..146b1e22 --- /dev/null +++ b/src/entities/portfolio/ui/ProjectInfoSection.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; +import { Tag } from '@/shared/ui/Tag/Tag.tsx'; + +interface ProjectInfoSectionProps { + id: number; + title: string; + tags: string[]; +} + +export function ProjectInfoSection({ title, tags }: ProjectInfoSectionProps) { + return ( + + {title} + + + {tags.map((tag) => ( + + ))} + + + ); +} + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 480px; + gap: 4px; +`; + +const Title = styled.div` + width: 100%; + height: 18px; + display: flex; + align-items: center; + font-size: 12px; + font-weight: 600; + color: ${({ theme }) => theme.colors.black}; +`; + +const TagContainer = styled.div` + width: 100%; + height: 44px; + display: flex; + align-items: center; + gap: 7px; + border-bottom: 1px solid ${({ theme }) => theme.colors.silver}; +`; diff --git a/src/entities/portfolio/ui/ProjectTitle.tsx b/src/entities/portfolio/ui/ProjectTitle.tsx new file mode 100644 index 00000000..77a2ef3f --- /dev/null +++ b/src/entities/portfolio/ui/ProjectTitle.tsx @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +interface ProjectTitleProps { + title: string; + duration: string; +} + +export function ProjectTitle({ title, duration }: ProjectTitleProps) { + return ( + + + {title} + {duration} + + + ); +} + +const Container = styled.div` + width: 480px; + height: 34px; + display: flex; + border-bottom: 1px solid ${({ theme }) => theme.colors.silver}; +`; + +const TitleContainer = styled.div` + width: 230px; + height: 27px; + gap: 19px; + display: flex; + align-items: center; +`; + +const Title = styled.p` + font-size: 18px; + font-weight: 700; + color: ${({ theme }) => theme.colors.black}; + margin: 4.5px 0; +`; + +const Duration = styled.p` + font-size: 12px; + font-weight: 400; + color: ${({ theme }) => theme.colors.darkGray}; +`; diff --git a/src/features/management/model/useSchedule.ts b/src/features/management/model/useSchedule.ts index 6b71f9cc..35940bf5 100644 --- a/src/features/management/model/useSchedule.ts +++ b/src/features/management/model/useSchedule.ts @@ -64,7 +64,6 @@ export const useSchedule = ( }; const submit = () => { - console.log('등록된 시간:', weeklyTimes); onSubmit?.(weeklyTimes); setShowRegister(false); }; diff --git a/src/pages/ManagementPage/management.tsx b/src/pages/ManagementPage/management.tsx index a5f134f5..f074fa75 100644 --- a/src/pages/ManagementPage/management.tsx +++ b/src/pages/ManagementPage/management.tsx @@ -61,7 +61,6 @@ export function ManagementPage() { } const transformedMySchedule = transformScheduleData(mySchedule); const transformedPartialSchedule = transformScheduleData(partialSchedule); - console.log(schedule, transformedPartialSchedule); return ( diff --git a/src/pages/MyPage/portfolio.tsx b/src/pages/MyPage/portfolio.tsx new file mode 100644 index 00000000..752b656e --- /dev/null +++ b/src/pages/MyPage/portfolio.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { + MOCK_PROJECTS, + projectInfoMock, +} from '@/entities/portfolio/model/portfolio.mock.ts'; +import Skeleton from '@/shared/components/skeleton/Skeleton.tsx'; +import MypageHeader from '@/widgets/mypage/MypageHeader.tsx'; +import { ProjectDetail } from '@/widgets/portfolio/ui/ProjectDetail.tsx'; +import { ProjectList } from '@/widgets/portfolio/ui/ProjectList.tsx'; + +export default function Portfolio() { + const projects = MOCK_PROJECTS; + const [selectedProjectId, setSelectedProjectId] = useState( + null, + ); + // TODO: API 연동 후 isPending으로 변경 + const [isLoading, setIsLoading] = useState(true); + + // 프로젝트 존재 여부 + const hasProjects = projects.length > 0; + + const selectedProject = + selectedProjectId === null + ? null + : (projects.find((project) => project.id === selectedProjectId) ?? null); + + const handleSelectProject = (projectId: number) => { + setSelectedProjectId((prev) => (prev === projectId ? null : projectId)); + }; + + useEffect(() => { + const timer = setTimeout(() => { + setIsLoading(false); + }, 800); + return () => clearTimeout(timer); + }, []); + + return ( + + 포트폴리오 + + + {isLoading ? ( + <> + + + + ) : ( + <> + + + + )} + + + ); +} + +const Container = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100vh; +`; + +const ContentWrapper = styled.div` + display: flex; + width: 95%; + margin: 46px 0 74px 5%; + gap: 20px; +`; diff --git a/src/shared/assets/common/arrow.svg b/src/shared/assets/common/arrow.svg index bb043561..5817e5cb 100644 --- a/src/shared/assets/common/arrow.svg +++ b/src/shared/assets/common/arrow.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/shared/assets/mypage/portfolio.svg b/src/shared/assets/mypage/portfolio.svg new file mode 100644 index 00000000..83eefaaf --- /dev/null +++ b/src/shared/assets/mypage/portfolio.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/ui/Tag/Tag.tsx b/src/shared/ui/Tag/Tag.tsx new file mode 100644 index 00000000..f63e5607 --- /dev/null +++ b/src/shared/ui/Tag/Tag.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components'; + +interface TagProps { + text: string; + variant?: 'default' | 'primary'; +} + +export function Tag({ text }: TagProps) { + return ( + + {text} + + ); +} + +const TagBox = styled.div` + max-width: 80px; + height: 28px; + display: flex; + justify-content: center; + align-items: center; + padding: 5px 8px; + border-radius: 3px; + background: ${({ theme }) => theme.colors.background}; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const TagText = styled.span` + font-size: 12px; + font-weight: 500; + color: ${({ theme }) => theme.colors.mainBlue}; +`; diff --git a/src/shared/ui/Tag/index.ts b/src/shared/ui/Tag/index.ts new file mode 100644 index 00000000..ba2338b7 --- /dev/null +++ b/src/shared/ui/Tag/index.ts @@ -0,0 +1 @@ +export { Tag } from './Tag'; diff --git a/src/widgets/portfolio/ui/ProjectDetail.tsx b/src/widgets/portfolio/ui/ProjectDetail.tsx new file mode 100644 index 00000000..e8a88d49 --- /dev/null +++ b/src/widgets/portfolio/ui/ProjectDetail.tsx @@ -0,0 +1,75 @@ +import styled from 'styled-components'; +import { ProjectInfoSection } from '@/entities/portfolio/ui/ProjectInfoSection.tsx'; +import { ProjectTitle } from '@/entities/portfolio/ui/ProjectTitle.tsx'; +import { ContainerText, Wrapper } from '@/widgets/portfolio/ui/ProjectList.tsx'; + +interface ProjectDetailProps { + hasProjects: boolean; + selectedProject: { + name: string; + duration: string; + } | null; + projectInfo: { + id: number; + title: string; + tags: string[]; + }[]; +} + +export function ProjectDetail({ + hasProjects, + selectedProject, + projectInfo, +}: ProjectDetailProps) { + // 프로젝트가 아예 없을 때 + if (!hasProjects) { + return ( + + + 아직 확인할 프로젝트가 없습니다. + + + ); + } + + // 프로젝트는 존재, 선택 전 + if (!selectedProject) { + return ( + + + 프로젝트를 클릭해 확인할 수 있습니다. + + + ); + } + + return ( + + + {projectInfo.map((item) => ( + + ))} +
공유했던 파일
+
+ ); +} + +const DetailContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 534px; + height: 562px; + padding: 24px 0; + gap: 20px; + background: white; + border-radius: 10px; +`; diff --git a/src/widgets/portfolio/ui/ProjectList.tsx b/src/widgets/portfolio/ui/ProjectList.tsx new file mode 100644 index 00000000..35dbaad8 --- /dev/null +++ b/src/widgets/portfolio/ui/ProjectList.tsx @@ -0,0 +1,67 @@ +import styled from 'styled-components'; +import { MOCK_PROJECTS } from '@/entities/portfolio/model/portfolio.mock.ts'; +import { ProjectCard } from '@/entities/portfolio/ui/ProjectCard.tsx'; + +interface ProjectListProps { + projects: typeof MOCK_PROJECTS; + hasProjects: boolean; + selectedProjectId: number | null; + onSelectProject: (id: number) => void; +} + +export function ProjectList({ + projects, + hasProjects, + selectedProjectId, + onSelectProject, +}: ProjectListProps) { + if (!hasProjects) { + return ( + + + 아직 끝난 프로젝트가 없습니다. + + + ); + } + return ( + + {projects.map((project) => ( + + ))} + + ); +} + +const ListContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 534px; + height: 562px; + padding: 24px 0; + gap: 20px; + background: white; + border-radius: 10px; +`; + +export const Wrapper = styled.div` + width: 100%; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +`; + +export const ContainerText = styled.div` + font-size: 16px; + font-weight: 500; + color: ${({ theme }) => theme.colors.black}; +`;