diff --git a/src/assets/icons/ic_heart_filled.svg b/src/assets/icons/ic_heart_filled.svg new file mode 100644 index 00000000..85eb2468 --- /dev/null +++ b/src/assets/icons/ic_heart_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ic_heart_unfilled.svg b/src/assets/icons/ic_heart_unfilled.svg new file mode 100644 index 00000000..1457abb5 --- /dev/null +++ b/src/assets/icons/ic_heart_unfilled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ic_profile_default.svg b/src/assets/icons/ic_profile_default.svg new file mode 100644 index 00000000..3cb5b146 --- /dev/null +++ b/src/assets/icons/ic_profile_default.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/lib/styles/colors.ts b/src/lib/styles/colors.ts deleted file mode 100644 index 64ce3eba..00000000 --- a/src/lib/styles/colors.ts +++ /dev/null @@ -1,79 +0,0 @@ -export const colors = { - white: '#FFFFFF', - black: '#000000', - - gray10: '#FCFCFC', - gray30: '#F0F0F0', - gray50: '#E4E4E5', - gray100: '#C3C3C6', - gray200: '#9D9DA4', - gray300: '#808087', - gray400: '#66666D', - gray500: '#515159', - gray600: '#3F3F47', - gray700: '#2E2E35', - gray800: '#202025', - gray950: '#0F0F12', - - blue50: '#C8E1FF', - blue100: '#8FC0FF', - blue200: '#619EFF', - blue300: '#4485FF', - blue400: '#346FFA', - blue500: '#2C53DF', - blue600: '#2649B3', - blue700: '#253B8C', - blue800: '#23306A', - blue900: '#20274D', - - red50: '#FFD1D3', - red100: '#FFA8AD', - red200: '#FE818B', - red300: '#FA616D', - red400: '#F04251', - red500: '#CA2F3D', - red600: '#9E2733', - red700: '#7A242D', - red800: '#562025', - red900: '#3C2020', - - green50: '#CCFFEC', - green100: '#82F6CB', - green200: '#4EE4AD', - green300: '#26CF91', - green400: '#16BF81', - green500: '#13A06C', - green600: '#138A5E', - green700: '#136D4C', - green800: '#13533C', - green900: '#15372B', - - yellow50: '#FFF4D4', - yellow100: '#FFE9B2', - yellow200: '#FFDE8A', - yellow300: '#FFCD59', - yellow400: '#FFC234', - yellow500: '#FFB326', - yellow600: '#EBA01E', - yellow700: '#B57B1D', - yellow800: '#72531E', - yellow900: '#3D301A', - - orange50: '#FFECE5', - orange100: '#FFCEBD', - orange200: '#FFA480', - orange300: '#FF834A', - orange400: '#F77234', - orange500: '#D4591C', - orange600: '#AD4E17', - orange700: '#853D11', - orange800: '#5C2B0C', - orange900: '#422109', - - attention: '#FFC234', - error: '#F04251', - background: '#0F0F12', - secondary: '#F77234', - success: '#346FFA', - information: '#16BF81', -}; diff --git a/src/lib/types/blog.ts b/src/lib/types/blog.ts new file mode 100644 index 00000000..9eb520a5 --- /dev/null +++ b/src/lib/types/blog.ts @@ -0,0 +1,29 @@ +export type BlogPostListType = { + limit: number; + totalCount: number; + totalPage: number; + currentPage: number; + data: BlogPostType[]; + hasNextPage: boolean; + hasPrevPage: boolean; +}; + +export type BlogPostType = { + id: number; + part: string; + generation: number; + thumbnailUrl: string; + title: string; + description: string; + author: string; + authorProfileImageUrl: string | null; + url: string; + uploadedAt: string; + + /* article */ + likeCount?: number; + liked?: boolean; + + /* review */ + subject?: string; +}; diff --git a/src/views/BlogPage/components/BlogPost/DefaultProfileImage/index.tsx b/src/views/BlogPage/components/BlogPost/DefaultProfileImage/index.tsx new file mode 100644 index 00000000..a1719617 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/DefaultProfileImage/index.tsx @@ -0,0 +1,11 @@ +import Image from 'next/image'; +import icProfileDefault from '@src/assets/icons/ic_profile_default.svg'; +import * as S from './style'; + +export default function DefaultProfileImage() { + return ( + + 작성자 프로필 이미지 + + ); +} diff --git a/src/views/BlogPage/components/BlogPost/DefaultProfileImage/style.ts b/src/views/BlogPage/components/BlogPost/DefaultProfileImage/style.ts new file mode 100644 index 00000000..fff60363 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/DefaultProfileImage/style.ts @@ -0,0 +1,13 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; + +export const DefaultProfileImage = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 18px; + height: 18px; + border-radius: 18px; + background-color: ${colors.gray700}; +`; diff --git a/src/views/BlogPage/components/BlogPost/Header/index.tsx b/src/views/BlogPage/components/BlogPost/Header/index.tsx new file mode 100644 index 00000000..84ca29e1 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/Header/index.tsx @@ -0,0 +1,37 @@ +import { BlogPostType } from '@src/lib/types/blog'; +import { formatDate } from '@src/lib/utils/dateFormat'; +import DefaultProfileImage from '@src/views/BlogPage/components/BlogPost/DefaultProfileImage'; +import * as S from './style'; + +interface HeaderProps { + selectedTap: string; + blogPost: BlogPostType; +} + +export default function Header({ selectedTap, blogPost }: HeaderProps) { + return ( + + {selectedTap === 'ARTICLE' ? ( + <> + + {blogPost.authorProfileImageUrl ? ( + + ) : ( + + )} +
{blogPost.author}
+
+ +
{formatDate(new Date(blogPost.uploadedAt), 'yyyymmdd', '.')}
+ + ) : ( +
{blogPost.subject}
+ )} +
+ ); +} diff --git a/src/views/BlogPage/components/BlogPost/Header/style.ts b/src/views/BlogPage/components/BlogPost/Header/style.ts new file mode 100644 index 00000000..886d4323 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/Header/style.ts @@ -0,0 +1,46 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; +import Image from 'next/image'; + +export const Header = styled.div` + display: flex; + height: 23px; + margin-bottom: 4px; + + color: ${colors.gray200}; + font-size: 14px; + font-weight: 400; + line-height: 160%; + letter-spacing: -0.21px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + height: 16px; + margin-bottom: 0; + + font-size: 12px; + font-weight: 500; + line-height: 135%; /* 16.2px */ + letter-spacing: -0.18px; + } +`; + +export const Profile = styled.div` + display: flex; + align-items: center; + gap: 6px; +`; + +export const ProfileImage = styled(Image)` + border-radius: 18px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + width: 15px; + height: 15px; + } +`; + +export const Divider = styled.div` + padding: 0 2px 0 2px; +`; diff --git a/src/views/BlogPage/components/BlogPost/Like/index.tsx b/src/views/BlogPage/components/BlogPost/Like/index.tsx new file mode 100644 index 00000000..92b766a4 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/Like/index.tsx @@ -0,0 +1,23 @@ +import Image from 'next/image'; +import icHeartFilled from '@src/assets/icons/ic_heart_filled.svg'; +import icHeartUnfilled from '@src/assets/icons/ic_heart_unfilled.svg'; +import { BlogPostType } from '@src/lib/types/blog'; +import * as S from './style'; + +interface LikeProps { + blogPost: BlogPostType; +} + +export default function Like({ blogPost }: LikeProps) { + return ( + + 좋아요 + {blogPost.likeCount} + + ); +} diff --git a/src/views/BlogPage/components/BlogPost/Like/style.ts b/src/views/BlogPage/components/BlogPost/Like/style.ts new file mode 100644 index 00000000..235699f0 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/Like/style.ts @@ -0,0 +1,34 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; + +export const Like = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: 3px; + + position: absolute; + top: 10px; + right: 10px; + + height: 28px; + padding: 2px 8px; + border-radius: 6px; + background: ${colors.grayAlpha100}; + + color: ${colors.gray100}; + font-size: 14px; + font-weight: 400; + line-height: 160%; /* 22.4px */ + letter-spacing: -0.21px; + + transition: opacity 0.1s ease-out; + &:hover { + opacity: 0.8; + } + + /* 모바일 뷰 */ + @media (max-width: 767px) { + display: none; + } +`; diff --git a/src/views/BlogPage/components/BlogPost/index.tsx b/src/views/BlogPage/components/BlogPost/index.tsx new file mode 100644 index 00000000..02921f2a --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/index.tsx @@ -0,0 +1,45 @@ +import { useEffect, useRef, useState } from 'react'; +import type { BlogPostType } from '@src/lib/types/blog'; +import { parsePartToKorean } from '@src/lib/utils/parsePartToKorean'; +import Header from '@src/views/BlogPage/components/BlogPost/Header'; +import Like from '@src/views/BlogPage/components/BlogPost/Like'; +import * as S from './style'; + +const TWO_LINE_TITLE_HEIGHT = 72; + +interface BlogPostProps { + selectedTap: string; + blogPost: BlogPostType; +} + +export default function BlogPost({ selectedTap, blogPost }: BlogPostProps) { + const titleRef = useRef(null); + const [descriptionLine, setDescriptionLine] = useState(1); + + useEffect(() => { + if (titleRef.current) { + const titleHeight = titleRef.current.clientHeight; + setDescriptionLine(titleHeight >= TWO_LINE_TITLE_HEIGHT ? 1 : 2); + } + }, []); + + return ( + +
+
+ + {blogPost.title} + {blogPost.description} + + + {blogPost.generation}기 + {parsePartToKorean(blogPost.part)} + +
+ + + {selectedTap === 'ARTICLE' && } + +
+ ); +} diff --git a/src/views/BlogPage/components/BlogPost/style.ts b/src/views/BlogPage/components/BlogPost/style.ts new file mode 100644 index 00000000..27f32466 --- /dev/null +++ b/src/views/BlogPage/components/BlogPost/style.ts @@ -0,0 +1,138 @@ +import styled from '@emotion/styled'; +import { colors } from '@sopt-makers/colors'; +import Image from 'next/image'; +import Link from 'next/link'; + +export const BlogPost = styled(Link)` + display: flex; + justify-content: space-between; + gap: 36px; + + cursor: pointer; + transition: opacity 0.2s linear; + &:hover { + opacity: 0.8; + } + + /* 모바일 뷰 */ + @media (max-width: 767px) { + gap: 16px; + } +`; + +export const Body = styled.div` + height: 94px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + display: flex; + align-items: center; + height: 53px; + } +`; + +export const Title = styled.div` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + + max-height: 72px; + + color: ${colors.white}; + font-size: 24px; + font-weight: 600; + line-height: 150%; + letter-spacing: -0.48px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + font-size: 16px; + letter-spacing: -0.24px; + } +`; + +export const Description = styled.div<{ descriptionLine: number }>` + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: ${({ descriptionLine }) => descriptionLine}; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + + color: ${colors.gray200}; + font-size: 16px; + font-weight: 400; + line-height: 160%; + letter-spacing: -0.24px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + display: none; + } +`; + +export const TagList = styled.div` + display: flex; + gap: 8px; + margin-top: 10px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + margin-top: 18px; + } +`; + +export const Tag = styled.div` + display: flex; + justify-content: center; + align-items: center; + + height: 28px; + padding: 0px 6px; + border-radius: 6px; + background: ${colors.gray700}; + + color: ${colors.gray50}; + font-size: 12px; + font-weight: 500; + line-height: 135%; /* 16.2px */ + letter-spacing: -0.18px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + height: 20px; + + font-size: 11px; + font-weight: 600; + letter-spacing: -0.165px; + } +`; + +export const ThumbnailWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + + position: relative; + width: 239px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + width: 105px; + } +`; + +export const Thumbnail = styled(Image)` + border-radius: 8px; + object-fit: cover; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + width: 105px; + height: 70px; + border-radius: 5px; + } +`; diff --git a/src/views/BlogPage/components/BlogPostList/index.tsx b/src/views/BlogPage/components/BlogPostList/index.tsx new file mode 100644 index 00000000..c73e336e --- /dev/null +++ b/src/views/BlogPage/components/BlogPostList/index.tsx @@ -0,0 +1,18 @@ +import type { BlogPostListType } from '@src/lib/types/blog'; +import BlogPost from '@src/views/BlogPage/components/BlogPost'; +import * as S from './style'; + +interface BlogPostListProps { + selectedTap: string; // REVIEW || ARTICLE + blogPostList: BlogPostListType; +} + +export default function BlogPostList({ selectedTap, blogPostList }: BlogPostListProps) { + return ( + + {blogPostList.data.map((blogPost) => ( + + ))} + + ); +} diff --git a/src/views/BlogPage/components/BlogPostList/style.ts b/src/views/BlogPage/components/BlogPostList/style.ts new file mode 100644 index 00000000..8be095fc --- /dev/null +++ b/src/views/BlogPage/components/BlogPostList/style.ts @@ -0,0 +1,17 @@ +import styled from '@emotion/styled'; + +export const BlogPostList = styled.div` + display: flex; + flex-direction: column; + gap: 80px; + + width: 100%; + max-width: 900px; + min-width: 335px; + margin-top: 2px; + + /* 모바일 뷰 */ + @media (max-width: 767px) { + gap: 36px; + } +`;