diff --git a/src/app/appRouter.tsx b/src/app/appRouter.tsx
index 86168f5..0ad4c90 100644
--- a/src/app/appRouter.tsx
+++ b/src/app/appRouter.tsx
@@ -5,6 +5,7 @@ import {
DetailArchivePage,
ArchiveListPage,
WriteGatheringPage,
+ PortfolioListPage,
WriteArchivePage,
RegisterPage,
SearchPage,
@@ -22,7 +23,7 @@ const AppRouter = () => {
},
{
path: '/portfolio',
- element: <>{/** portfolioPage */}>,
+ element: ,
},
{
path: '/archive',
diff --git a/src/shared/ui/GatheringCard/GatheringCard.module.scss b/src/features/gathering/ui/GatheringCard/GatheringCard.module.scss
similarity index 100%
rename from src/shared/ui/GatheringCard/GatheringCard.module.scss
rename to src/features/gathering/ui/GatheringCard/GatheringCard.module.scss
diff --git a/src/shared/ui/GatheringCard/GatheringCard.tsx b/src/features/gathering/ui/GatheringCard/GatheringCard.tsx
similarity index 100%
rename from src/shared/ui/GatheringCard/GatheringCard.tsx
rename to src/features/gathering/ui/GatheringCard/GatheringCard.tsx
diff --git a/src/shared/ui/GatheringCard/JobTag.module.scss b/src/features/gathering/ui/GatheringCard/JobTag.module.scss
similarity index 100%
rename from src/shared/ui/GatheringCard/JobTag.module.scss
rename to src/features/gathering/ui/GatheringCard/JobTag.module.scss
diff --git a/src/shared/ui/GatheringCard/JobTag.tsx b/src/features/gathering/ui/GatheringCard/JobTag.tsx
similarity index 100%
rename from src/shared/ui/GatheringCard/JobTag.tsx
rename to src/features/gathering/ui/GatheringCard/JobTag.tsx
diff --git a/src/shared/ui/GatheringCard/index.ts b/src/features/gathering/ui/GatheringCard/index.ts
similarity index 100%
rename from src/shared/ui/GatheringCard/index.ts
rename to src/features/gathering/ui/GatheringCard/index.ts
diff --git a/src/features/index.ts b/src/features/index.ts
index c8ba2f4..5ba81b2 100644
--- a/src/features/index.ts
+++ b/src/features/index.ts
@@ -1,3 +1,5 @@
export * from './archive';
export * from './gathering';
+export * from './portfolio';
export * from './search';
+
diff --git a/src/features/portfolio/index.ts b/src/features/portfolio/index.ts
new file mode 100644
index 0000000..8fff3fe
--- /dev/null
+++ b/src/features/portfolio/index.ts
@@ -0,0 +1,2 @@
+export { PortfolioCard } from './ui/PortfolioCard';
+export type { Portfolio } from './model/types';
diff --git a/src/features/portfolio/model/types.ts b/src/features/portfolio/model/types.ts
new file mode 100644
index 0000000..a98c764
--- /dev/null
+++ b/src/features/portfolio/model/types.ts
@@ -0,0 +1,9 @@
+export type Portfolio = {
+ portFolioId: number;
+ portFolioUrl: string;
+ username: string;
+ introduction: string;
+ majorJobGroup: string;
+ minorJobGroup: string;
+ memberImageUrl: string;
+};
diff --git a/src/features/portfolio/ui/PortfolioCard.module.scss b/src/features/portfolio/ui/PortfolioCard.module.scss
new file mode 100644
index 0000000..2895149
--- /dev/null
+++ b/src/features/portfolio/ui/PortfolioCard.module.scss
@@ -0,0 +1,105 @@
+.container {
+ width: 18rem;
+ height: 30.5625rem;
+ padding-top: 2rem;
+
+ // border: 1px solid $primary-color;
+ transition:
+ transform 0.3s ease,
+ box-shadow 0.3s ease; // 트랜지션 효과 추가
+
+ &:hover {
+ transform: translateY(-8px) scale(1.02); // 위로 떠오르면서 약간 확대
+ }
+}
+
+.card {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.cardHeader {
+ position: relative;
+ width: 18rem;
+ height: 18.75rem;
+ overflow: hidden;
+}
+
+.cardImg {
+ display: block;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ border-radius: 12px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ background-color: #afafaf;
+ border-radius: 12px;
+ fill: $primary-color;
+ transition: transform 0.3s ease;
+ }
+}
+
+.contactBtn {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ background-color: white;
+ border: 1px solid $primary-color;
+ border-radius: 2rem;
+}
+
+.cardFooter {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ padding: 0 0.5rem;
+}
+
+.firstInfo {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ justify-content: space-between;
+ color: $primary-color;
+}
+
+.name {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: $primary-color;
+}
+
+.job {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: space-between;
+ font-size: 1rem;
+ font-weight: 500;
+ color: $primary-color;
+}
+
+.introduction {
+ display: -webkit-box;
+ overflow: hidden;
+ font-size: 0.875rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #afafaf;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+ word-break: break-word;
+ -webkit-box-orient: vertical;
+}
+
+// hover 시 이미지만 확대하고 싶은 경우 추가
+.container:hover .cardImg img {
+ transform: scale(1.05);
+}
diff --git a/src/features/portfolio/ui/PortfolioCard.tsx b/src/features/portfolio/ui/PortfolioCard.tsx
new file mode 100644
index 0000000..8141cb1
--- /dev/null
+++ b/src/features/portfolio/ui/PortfolioCard.tsx
@@ -0,0 +1,55 @@
+import { Link } from 'react-router-dom';
+
+import styles from './PortfolioCard.module.scss';
+
+import profileImg from '@/shared/assets/paletteLogo.svg';
+
+interface PortfolioCardProps {
+ portFolioId: number;
+ portFolioUrl: string;
+ username: string;
+ introduction: string;
+ majorJobGroup: string;
+ minorJobGroup: string;
+ memberImageUrl: string;
+}
+
+export const PortfolioCard = ({
+ // portFolioId,
+ portFolioUrl,
+ username,
+ introduction,
+ majorJobGroup,
+ minorJobGroup,
+ memberImageUrl,
+}: PortfolioCardProps) => {
+ return (
+
+
+
+
+
+
+
연락하기
+
+
+
+ {username}
+ ♥
+
+
+
+ {minorJobGroup}
+ @{majorJobGroup}
+
+
대표 색
+
+
{introduction}
+
+
+
+ );
+};
diff --git a/src/pages/PortfolioListPage/PortfolioListPage.module.scss b/src/pages/PortfolioListPage/PortfolioListPage.module.scss
new file mode 100644
index 0000000..c971a49
--- /dev/null
+++ b/src/pages/PortfolioListPage/PortfolioListPage.module.scss
@@ -0,0 +1,78 @@
+.pageWrapper {
+ position: relative;
+ width: 100%;
+ max-width: 1640px;
+ min-height: 100vh;
+ padding: 0 1rem;
+ margin: 0 auto;
+}
+
+.h1 {
+ margin-top: 4rem;
+ margin-bottom: 2.5rem;
+ font-size: 1.875rem;
+ font-weight: 700;
+ color: $primary-color;
+ text-align: center;
+}
+
+.contentContainer {
+ position: sticky;
+ display: flex; // grid 대신 flex 사용
+ gap: 2rem;
+ overflow: auto;
+
+ @media screen and (width <= 1440px) {
+ gap: 2rem;
+ }
+
+ @media screen and (width <= 1200px) {
+ gap: 1.5rem;
+ }
+
+ @media screen and (width <= 830px) {
+ flex-direction: column;
+ gap: 1.5rem;
+ }
+}
+
+.sidebarWrapper {
+ flex-shrink: 0;
+ width: 250px;
+
+ @media screen and (width <= 1200px) {
+ width: 220px;
+ }
+
+ @media screen and (width <= 830px) {
+ width: 100%;
+ }
+}
+
+.sidebarContainer {
+ position: fixed;
+ top: 16.5rem;
+ width: inherit;
+ max-height: calc(100vh - 14.5rem);
+ overflow-y: auto;
+
+ @media screen and (width <= 830px) {
+ position: relative;
+ top: 0;
+ width: 100%;
+ max-height: none;
+ }
+}
+
+.mainContent {
+ flex: 1;
+ min-width: 0; // flex item 오버플로우 방지
+}
+
+.buttonWrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ max-width: 1340px;
+ margin: 0 auto;
+}
diff --git a/src/pages/PortfolioListPage/PortfolioListPage.tsx b/src/pages/PortfolioListPage/PortfolioListPage.tsx
new file mode 100644
index 0000000..19667e4
--- /dev/null
+++ b/src/pages/PortfolioListPage/PortfolioListPage.tsx
@@ -0,0 +1,49 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import styles from './PortfolioListPage.module.scss';
+
+import { JOB_CATEGORIES } from '@/shared/model';
+import { SidebarFilter } from '@/shared/ui';
+import { Button, SelectBtn } from '@/shared/ui';
+import { PortFolioGrid } from '@/widgets';
+export const PortfolioListPage = () => {
+ const navigate = useNavigate();
+ const [sort, setSort] = useState({ label: '최신순', value: 'latest' });
+ return (
+
+
포트폴리오
+
+
+
+
+
+ {
+ setSort(newValue as { label: string; value: string });
+ }}
+ options={[
+ { label: '최신순', value: 'latest' },
+ { label: '인기순', value: 'popular' },
+ ]}
+ value={sort}
+ />
+ {
+ navigate('/');
+ }}
+ >
+ 포트폴리오 등록하기
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/index.ts b/src/pages/index.ts
index 17ed4ff..8ca00d6 100644
--- a/src/pages/index.ts
+++ b/src/pages/index.ts
@@ -1,7 +1,9 @@
-export * from './GatheringListPage';
-export { WriteArchivePage } from './WriteArchivePage/WriteArchivePage';
+export { ArchiveListPage } from './ArchiveListPage/ArchiveListPage';
export { DetailArchivePage } from './DetailArchivePage/DetailArchivePage';
+export * from './GatheringListPage';
+export { PortfolioListPage } from './PortfolioListPage/PortfolioListPage';
export { RegisterPage } from './RegisterPage/RegisterPage';
-export { WriteGatheringPage } from './WriteGatheringPage/WriteGatheringPage';
export { SearchPage } from './SearchPage/SearchPage';
-export { ArchiveListPage } from './ArchiveListPage/ArchiveListPage';
+export { WriteArchivePage } from './WriteArchivePage/WriteArchivePage';
+export { WriteGatheringPage } from './WriteGatheringPage/WriteGatheringPage';
+
diff --git a/src/shared/model/index.ts b/src/shared/model/index.ts
new file mode 100644
index 0000000..b784155
--- /dev/null
+++ b/src/shared/model/index.ts
@@ -0,0 +1 @@
+export {JOB_CATEGORIES} from './jobCategories'
\ No newline at end of file
diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts
index 0035e19..adb2840 100644
--- a/src/shared/ui/index.ts
+++ b/src/shared/ui/index.ts
@@ -1,7 +1,7 @@
export { Button } from './Button/Button';
export { Modal } from './Modal/Modal';
-export { GatheringCard } from './GatheringCard/GatheringCard';
-export { JobTag } from './GatheringCard/JobTag';
+export { GatheringCard } from '../../features/gathering/ui/GatheringCard/GatheringCard';
+export { JobTag } from '../../features/gathering/ui/GatheringCard/JobTag';
export { SelectBtn } from './SelectBtn/SelectBtn';
export { MarkdownEditor } from './MarkdownEditor/MarkdownEditor';
export { Input } from './Input/Input';
diff --git a/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.module.scss b/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.module.scss
index ba6a09a..f7a7c22 100644
--- a/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.module.scss
+++ b/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.module.scss
@@ -1,9 +1,10 @@
.container {
display: flex;
gap: 9.38rem;
- justify-content: center;
align-items: center;
+ justify-content: center;
width: 100%;
- margin-bottom: 2rem;
padding: 1rem;
+ margin-right: 1rem;
+ margin-bottom: 2rem;
}
diff --git a/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.tsx b/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.tsx
index 6a919f7..09bfb22 100644
--- a/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.tsx
+++ b/src/widgets/GatheringSelectCon/ui/GatheringSelectCon.tsx
@@ -1,12 +1,11 @@
-// SelectBtn.tsx는 그대로 유지하고, GatheringSelectCon을 수정합니다
+import { useNavigate } from 'react-router-dom';
import styles from './GatheringSelectCon.module.scss';
import { gatheringFilterOptions } from '@/features';
import type { Option } from '@/shared/model/SelectBtnTypes';
-import { SelectBtn } from '@/shared/ui';
+import { Button, SelectBtn } from '@/shared/ui';
-// subject를 제외한 키만 허용하도록 타입 수정
type GatheringFilterKey = Exclude;
interface SelectConfig {
@@ -16,6 +15,7 @@ interface SelectConfig {
}
export const GatheringSelectCon = () => {
+ const navigate = useNavigate();
const selectConfigs: SelectConfig[] = [
{
key: 'contact',
@@ -42,7 +42,6 @@ export const GatheringSelectCon = () => {
return (
{selectConfigs.map(config => {
- // options가 Option[] 타입임을 보장
const options = gatheringFilterOptions[config.key] as Option[];
return (
@@ -55,6 +54,13 @@ export const GatheringSelectCon = () => {
/>
);
})}
+ {
+ navigate('/');
+ }}
+ >
+ 게더링 등록하기
+
);
};
diff --git a/src/widgets/PortfolioGrid/PortFolioGrid.module.scss b/src/widgets/PortfolioGrid/PortFolioGrid.module.scss
new file mode 100644
index 0000000..8601a8a
--- /dev/null
+++ b/src/widgets/PortfolioGrid/PortFolioGrid.module.scss
@@ -0,0 +1,15 @@
+.container {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); // 18rem = 288px
+ gap: 36px 24px; // row-gap: 36px, column-gap: 24px
+ width: 100%;
+ max-width: 1920px;
+ padding: 24px;
+ margin: 0 auto;
+
+ @media screen and (width <= 768px) {
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 24px 16px;
+ padding: 16px;
+ }
+}
diff --git a/src/widgets/PortfolioGrid/PortFolioGrid.tsx b/src/widgets/PortfolioGrid/PortFolioGrid.tsx
new file mode 100644
index 0000000..0022cca
--- /dev/null
+++ b/src/widgets/PortfolioGrid/PortFolioGrid.tsx
@@ -0,0 +1,80 @@
+import styles from './PortFolioGrid.module.scss';
+
+import { PortfolioCard } from '@/features';
+import type { Portfolio } from '@/features';
+
+export const PortFolioGrid = () => {
+ const portfolios: Portfolio[] = [
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ {
+ portFolioId: 28,
+ portFolioUrl: 'https://example.com/portfolio/28',
+ username: '리성원',
+ introduction: '안녕하세요',
+ majorJobGroup: '개발',
+ minorJobGroup: '서버/백엔드 개발자',
+ memberImageUrl: 'fdsf',
+ },
+ ];
+
+ return (
+
+ {portfolios.map(portfolio => (
+
+ ))}
+
+ );
+};
diff --git a/src/widgets/SearchAll/SearchAll.tsx b/src/widgets/SearchAll/SearchAll.tsx
index dd5bc6d..4788ea2 100644
--- a/src/widgets/SearchAll/SearchAll.tsx
+++ b/src/widgets/SearchAll/SearchAll.tsx
@@ -2,10 +2,10 @@ import { useNavigate } from 'react-router-dom';
import styles from './SearchAll.module.scss';
-import type { ArchiveCardDTO } from '@/features';
import { ArchiveCard } from '@/features';
+import type { ArchiveCardDTO } from '@/features';
+import type { GatheringCardProps } from '@/features/gathering/ui/GatheringCard/GatheringCard';
import { Button, GatheringCard } from '@/shared/ui';
-import type { GatheringCardProps } from '@/shared/ui/GatheringCard';
export const SearchAll = ({
archives,
diff --git a/src/widgets/SearchTap/SearchTap.tsx b/src/widgets/SearchTap/SearchTap.tsx
index f4bcbcd..17612b9 100644
--- a/src/widgets/SearchTap/SearchTap.tsx
+++ b/src/widgets/SearchTap/SearchTap.tsx
@@ -4,7 +4,7 @@ import styles from './SearchTap.module.scss';
import { SearchAll } from '../SearchAll/SearchAll';
import type { ArchiveCardDTO, Color } from '@/features';
-import type { GatheringCardProps } from '@/shared/ui/GatheringCard';
+import type { GatheringCardProps } from '@/features/gathering/ui/GatheringCard/GatheringCard';
const dummyArchives: ArchiveCardDTO[] = Array.from({ length: 9 }, (_, i) => ({
archiveId: i,
@@ -37,7 +37,7 @@ const renderingSearchTap = (activeTab: string, setActiveTab: (t: string) => void
);
} else if (activeTab === '아카이브') {
return ;
- } else if (activeTab === '소모임') {
+ } else if (activeTab === '게더링') {
return ;
}
};
@@ -49,7 +49,7 @@ export const SearchTap = ({
activeTab: string;
setActiveTab: (t: string) => void;
}) => {
- const tabs = ['전체', '아카이브', '소모임'];
+ const tabs = ['전체', '아카이브', '게더링'];
return (
diff --git a/src/widgets/index.ts b/src/widgets/index.ts
index f301db4..fdae5d7 100644
--- a/src/widgets/index.ts
+++ b/src/widgets/index.ts
@@ -1,10 +1,12 @@
+export * from './ArchiveGrid';
export * from './DetailArchive';
export * from './GatheringGrid';
export * from './GatheringSelectCon/index';
export * from './Layout';
+export * from './PortfolioGrid/PortFolioGrid';
+export * from './SearchAll/SearchAll';
+export * from './SearchTap/SearchTap';
export * from './WriteArchive';
-export * from './WriteGathering/WriteGatheringOpts';
export * from './WriteGathering/WriteGatheringDetail';
-export * from './ArchiveGrid';
-export * from './SearchTap/SearchTap';
-export * from './SearchAll/SearchAll';
+export * from './WriteGathering/WriteGatheringOpts';
+