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}의 + + +
+
+
+ {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} + /> + +
+ +
+
+
+ ); +}; 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 = () => { /> ); })} +
); }; 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'; +