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};
+`;