diff --git a/components/Layout/index.tsx b/components/Layout/index.tsx new file mode 100644 index 0000000..b2a8d45 --- /dev/null +++ b/components/Layout/index.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import * as S from './styles'; +import Header from 'components/header/Header'; +const Layout: React.FC = ({ children }) => { + return ( + <> +
+ {children} + + ); +}; + +export default Layout; diff --git a/components/Layout/styles.ts b/components/Layout/styles.ts new file mode 100644 index 0000000..6d4da2b --- /dev/null +++ b/components/Layout/styles.ts @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export const Wrapper = styled.div` + max-width: ${(props) => props.theme.desktop}; + padding: 0 30px; + margin: 0 auto; +`; diff --git a/components/Profile/Banner/AddImage/index.tsx b/components/Profile/Banner/AddImage/index.tsx new file mode 100644 index 0000000..0c06e1b --- /dev/null +++ b/components/Profile/Banner/AddImage/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import ImageUploadWrapper from 'components/common/ImageUploadWrapper'; +import { AddImageWrapper, AddImageSvg, AddImageText } from 'components/common/Atomic/AddItem'; + +interface Props { + text: string; +} + +const AddImage: React.FC = ({ text }) => { + return ( + + + + {text} + + + ); +}; + +export default AddImage; diff --git a/components/Profile/Banner/index.tsx b/components/Profile/Banner/index.tsx new file mode 100644 index 0000000..8374edc --- /dev/null +++ b/components/Profile/Banner/index.tsx @@ -0,0 +1,16 @@ +import React, { useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +import * as S from './styles'; + +import AddImage from './AddImage'; + +import { userRegisterInfoState, UserRegisterInfoType } from 'recoil/auth'; + +const Banner: React.FC = () => { + const [banner, setBanner] = useState(); + + return {!banner && }; +}; + +export default Banner; diff --git a/components/Profile/Banner/styles.ts b/components/Profile/Banner/styles.ts new file mode 100644 index 0000000..763c726 --- /dev/null +++ b/components/Profile/Banner/styles.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; +import { styledProps } from './types'; + +export const BannerWrapper = styled.div` + width: 100%; + height: 280px; + background-color: ${(props) => props.theme.color.backgroundGray}; + background-image: ${(props) => props.url && `url(${props.url})`}; + background-repeat: no-repeat; + background-position: center; + background-size: cover; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/components/Profile/Banner/types.ts b/components/Profile/Banner/types.ts new file mode 100644 index 0000000..c2abbf1 --- /dev/null +++ b/components/Profile/Banner/types.ts @@ -0,0 +1,6 @@ +import { DefaultTheme } from 'styled-components'; + +export interface styledProps { + url?: string | ArrayBuffer | null; + theme: DefaultTheme; +} diff --git a/components/Profile/ItemList/index.tsx b/components/Profile/ItemList/index.tsx new file mode 100644 index 0000000..303b11e --- /dev/null +++ b/components/Profile/ItemList/index.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; +import * as S from './styles'; +import AddItemCard from 'components/common/AddItemCard'; +import Thumbnail from 'components/common/Thumbnail'; +interface Props { + itemList: string[]; //데이터 형식에따라 타입 변환할 것 +} +const ItemList: React.FC = ({ itemList }) => { + useEffect(() => { + console.log(itemList); + }, [itemList]); + return ( + + {itemList.length ? ( + itemList.map((item, i) => ) + ) : ( + + )} + + ); +}; + +export default ItemList; diff --git a/components/Profile/ItemList/styles.tsx b/components/Profile/ItemList/styles.tsx new file mode 100644 index 0000000..2773983 --- /dev/null +++ b/components/Profile/ItemList/styles.tsx @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const ItemListWrapper = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(363px, 1fr)); + + row-gap: 25px; + column-gap: 24px; +`; diff --git a/components/Profile/UserInfo/index.tsx b/components/Profile/UserInfo/index.tsx new file mode 100644 index 0000000..9b5c349 --- /dev/null +++ b/components/Profile/UserInfo/index.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import * as S from './styles'; +import Image from 'next/image'; +import { + ProfileEditButton, + UploadProductButton, + FollowButton, + MessageButton, +} from 'components/common/Atomic/Tabs/Button'; +import { Keyword } from 'components/common/Atomic/Tabs/Keyword'; +import { numberWithCommas } from 'utils/numberWithCommas'; + +const UserInfo = () => { + const [userName, setUserName] = useState('Andre'); + const [abilties, setAbiliies] = useState([ + '일러스트레이션', + '그래픽 디자인', + '일러스트레이션', + '그래픽 디자인', + '일러스트레이션', + '그래픽 디자인', + '일러스트레이션', + '그래픽 디자인', + '일러스트레이션', + '그래픽', + '마케팅', + ]); + const [followers, setFollowers] = useState(10214); + const [followings, setFollowings] = useState(35150); + const [currentUser, setCurrentUser] = useState(false); + const [intro, setIntro] = useState( + '사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.사용자 소개입니다.' + ); + const handler = () => { + setCurrentUser(!currentUser); + }; + return ( + + + + + +

{userName}

+ +
+ {abilties.map((ability, i) => ( + {ability} + ))} +
+ + 팔로워 + {numberWithCommas(followers)} + 팔로잉 + {numberWithCommas(followings)} + +

{intro}

+
+
+ + {currentUser ? ( + <> + {' '} + + + 프로필 수정 + + + + 작품 업로드 + + + ) : ( + <> + + + 팔로우 + + + + 메시지 + + + )} + +
+ ); +}; + +export default UserInfo; diff --git a/components/Profile/UserInfo/styles.ts b/components/Profile/UserInfo/styles.ts new file mode 100644 index 0000000..8696209 --- /dev/null +++ b/components/Profile/UserInfo/styles.ts @@ -0,0 +1,57 @@ +import styled from 'styled-components'; +import Image from 'next/image'; +export const InfoWrapper = styled.div` + padding: 24px; + position: relative; + margin-bottom: 80px; + display: flex; +`; + +export const ProfileImg = styled.div``; + +export const InfoSection = styled.div` + margin-left: 24px; + width: 610px; + + & > h1{ + font-size: 20px; + line-height: 1.3; + font-weight : ${({ theme }) => theme.fontWeight.bold} + color: ${({ theme }) => theme.color.profileNameBlack}; + margin-bottom: 16px; + } +`; + +export const InfoDescription = styled.div` + display: flex; + flex-direction: column; + flex-wrap: wrap; + + p { + color: ${({ theme }) => theme.color.gray_700}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + font-size: 12px; + line-height: 1.416666; + } +`; + +export const InfoAside = styled.div` + position: absolute; + right: 24px; +`; + +export const FollowInfo = styled.div` + margin-bottom: 8px; + span { + font-weight: 400; + font-size: 12px; + line-height: 1.833333; + letter-spacing: -0.01em; + color: ${({ theme }) => theme.color.gray_700}; + margin-right: 16px; + } + + span:nth-child(2n-1) { + margin-right: 4px; + } +`; diff --git a/components/common/AddItemCard/index.tsx b/components/common/AddItemCard/index.tsx new file mode 100644 index 0000000..323fbc5 --- /dev/null +++ b/components/common/AddItemCard/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import styled from 'styled-components'; +import { AddImageWrapper, AddImageSvg } from '../Atomic/AddItem'; +const AddItem = () => { + return ( + + + 게시물을 추가 해주세요. + + + + ); +}; + +export default AddItem; + +const AddItemCard = styled.div` + width: 363px; + height: 280px; + background-color: ${({ theme }) => theme.color.gray_100}; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; +`; + +const AddImageText = styled.span` + color: ${(props) => props.theme.color.addTextGray}; + font-size: 14px; + margin-bottom: 16px; +`; diff --git a/components/common/Atomic/AddItem/index.ts b/components/common/Atomic/AddItem/index.ts new file mode 100644 index 0000000..9c8397e --- /dev/null +++ b/components/common/Atomic/AddItem/index.ts @@ -0,0 +1,24 @@ +import styled, { DefaultTheme } from 'styled-components'; +import Image from 'next/image'; + +export const AddImageWrapper = styled.div` + cursor: pointer; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const AddImageSvg = styled(Image).attrs((props) => ({ + src: '/images/add_project_default.svg', +}))` + display: block; + border-radius: 50%; +`; + +export const AddImageText = styled.span<{ theme: DefaultTheme }>` + color: ${(props) => props.theme.color.addTextGray}; + font-size: 14px; + margin-top: 16px; +`; diff --git a/components/common/Atomic/Tabs/Button/index.ts b/components/common/Atomic/Tabs/Button/index.ts new file mode 100644 index 0000000..fe107c5 --- /dev/null +++ b/components/common/Atomic/Tabs/Button/index.ts @@ -0,0 +1,50 @@ +import styled, { css } from 'styled-components'; + +export const DefaultButton = styled.button<{ bgColor?: boolean }>` + height: 40px; + border-radius: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + + ${({ bgColor, theme }) => { + const color = bgColor ? theme.color.DefaultPrimaryGreen : theme.color.backgroundGray; + const pressed = bgColor ? theme.color.PressedPrimaryGreen : theme.color.backgroundHoverGray; + return css` + background-color: ${color}; + &: hover { + background-color: ${pressed}; + } + &: active { + background-color: ${pressed}; + } + `; + }} + + & + & { + margin-left: 16px; + } + + & > span { + font-size: 16px; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + line-height: 1.448125; + margin-left: 4px; + } +`; + +export const ProfileEditButton = styled(DefaultButton)` + padding: 10px 24px 10px 20px; +`; + +export const UploadProductButton = styled(DefaultButton)` + padding: 10px 24px 10px 20px; +`; + +export const FollowButton = styled(DefaultButton)` + padding: 10px 16px; +`; + +export const MessageButton = styled(DefaultButton)` + padding: 10px 16px; +`; diff --git a/components/common/Atomic/Tabs/Keyword/index.ts b/components/common/Atomic/Tabs/Keyword/index.ts new file mode 100644 index 0000000..a7a73a8 --- /dev/null +++ b/components/common/Atomic/Tabs/Keyword/index.ts @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const Keyword = styled.span` + display: inline-block; + height: 24px; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + line-height: 1.4166666; + + background-color: ${({ theme }) => theme.color.backgroundGray}; + + margin-right: 8px; + margin-bottom: 8px; +`; diff --git a/components/common/Atomic/Tabs/TabButton/index.ts b/components/common/Atomic/Tabs/TabButton/index.ts new file mode 100644 index 0000000..bfe74af --- /dev/null +++ b/components/common/Atomic/Tabs/TabButton/index.ts @@ -0,0 +1,31 @@ +import styled, { css, DefaultTheme } from 'styled-components'; + +export const TabButton = styled.button<{ active: boolean; theme: DefaultTheme }>` + display: inline-block; + height: 31px; + font-size: 16px; + line-height: 1.448125; + padding: 4px 12px; + border-radius: 24px; + margin-left: 24px; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + background-color: ${({ theme }) => theme.color.white}; + border: 1px solid ${({ theme }) => theme.color.gray_300}; + color: ${({ theme }) => theme.color.black}; + span { + margin-left: 4px; + color: ${({ theme }) => theme.color.gray_400}; + } + ${({ active }) => { + return ( + active && + css` + border: none; + background-color: ${({ theme }) => theme.color.black}; + color: ${({ theme }) => theme.color.white}; + span { + } + ` + ); + }}; +`; diff --git a/components/common/Thumbnail/index.tsx b/components/common/Thumbnail/index.tsx new file mode 100644 index 0000000..0f9e659 --- /dev/null +++ b/components/common/Thumbnail/index.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import Image from 'next/image'; + +interface Props { + item: string; //작품 정보에 대한 타입을 정의해줘야함 +} +const Thumbnail: React.FC = ({ item }) => { + return ( + + + + + {item.length > 0 && ( + +

{item}

+
+ )} +
+ ); +}; + +export default Thumbnail; + +const ItemCard = styled.div` + position: relative; + width: 363px; + height: 280px; + border-radius: 10px; + background-color: ${({ theme }) => theme.color.gray_500}; + overflow: hidden; +`; + +const ItemInfo = styled.div` + position: absolute; + bottom: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.3) 70.31%); + width: 100%; + height: 100px; + padding: 0 16px; + + p { + position: absolute; + color: ${({ theme }) => theme.color.white}; + font-weght: ${({ theme }) => theme.fontWeight.medium}; + font-size: 16px; + line-height: 1.4375; + letter-spacing: 0.02em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 332px; + height: 23px; + bottom: 16px; + } +`; + +const ImageWrapper = styled.button` + position: absolute; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + width: 32px; + height: 32px; + top: 16px; + right: 16px; + background: rgba(0, 0, 0, 0.5); + border-radius: 5px; +`; diff --git a/db.json b/db.json new file mode 100644 index 0000000..b505452 --- /dev/null +++ b/db.json @@ -0,0 +1,13 @@ +{ + "user": [ + { + "id": 1, + "userProfile": null, + "email": "abg3000@naver.com", + "nickname": "장종오", + "description": "안녕하세요", + "mainCategory": [], + "banner": null + } + ] +} diff --git a/pages/_document.tsx b/pages/_document.tsx index baf8fed..8de0d86 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -38,6 +38,10 @@ class MyDocument extends Document { +
diff --git a/pages/profile.tsx b/pages/profile.tsx new file mode 100644 index 0000000..c392f0f --- /dev/null +++ b/pages/profile.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useState } from 'react'; +import Layout from 'components/Layout'; +import Banner from 'components/Profile/Banner'; +import UserInfo from 'components/Profile/UserInfo'; +import ItemList from 'components/Profile/ItemList'; +import { TabButton } from 'components/common/Atomic/Tabs/TabButton'; +const Profile: React.FC = () => { + const [user, getUserQuery] = useState('serre'); // useQuery로 유저정보 받아옴. + + //Suspense를 사용하게 된다면, useQuery를 여러개 선언하는것은 사용할 수 없으므로, useQueries를 사용해야함 + const Items = { + items: [], //['아이템1', '아이템2'], + scrabs: [ + '', + '스크랩1 제목입니다.스크랩1 제목입니다.스크랩1 제목입니다.스크랩1 제목입니다.스크랩1 제목입니다.', + '스크랩2', + '스크랩3', + '스크랩4', + '스크랩5', + '스크랩6', + '스크랩7', + '스크랩8', + ], + }; + const [currentTab, setCurrentTab] = useState(0); + const tabMenuArr = [ + ['작업물', 'items'], + ['스크랩', 'scrabs'], + ]; + const selectTab = useCallback( + (index: number) => () => { + setCurrentTab(index); + }, + [currentTab, setCurrentTab] + ); + return ( + + + +
+ {tabMenuArr.map((tab, i) => ( + + {tab[0]} + {Items[tabMenuArr[i][1]].length} + + ))} +
+ +
+ ); +}; + +export default Profile; diff --git a/public/images/Message.svg b/public/images/Message.svg new file mode 100644 index 0000000..f9a863a --- /dev/null +++ b/public/images/Message.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/add_project_default.svg b/public/images/add_project_default.svg new file mode 100644 index 0000000..5590139 --- /dev/null +++ b/public/images/add_project_default.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/add_project_pressed.svg b/public/images/add_project_pressed.svg new file mode 100644 index 0000000..e66aceb --- /dev/null +++ b/public/images/add_project_pressed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/images/icon-add-round.svg b/public/images/icon-add-round.svg new file mode 100644 index 0000000..1d8f149 --- /dev/null +++ b/public/images/icon-add-round.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/icon-folder-add.svg b/public/images/icon-folder-add.svg new file mode 100644 index 0000000..7d9da0a --- /dev/null +++ b/public/images/icon-folder-add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/icon-message.svg b/public/images/icon-message.svg new file mode 100644 index 0000000..f9a863a --- /dev/null +++ b/public/images/icon-message.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/profile-edit-write2.png b/public/images/profile-edit-write2.png new file mode 100644 index 0000000..69f0f11 Binary files /dev/null and b/public/images/profile-edit-write2.png differ diff --git a/public/images/profile-edit-write2.svg b/public/images/profile-edit-write2.svg new file mode 100644 index 0000000..1fee3c4 --- /dev/null +++ b/public/images/profile-edit-write2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/profile-edit.svg b/public/images/profile-edit.svg new file mode 100644 index 0000000..15ab526 --- /dev/null +++ b/public/images/profile-edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/profile_off.svg b/public/images/profile_off.svg new file mode 100644 index 0000000..4aea2f9 --- /dev/null +++ b/public/images/profile_off.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/styles/globalStyle.ts b/styles/globalStyle.ts index 45abc6f..af6012a 100644 --- a/styles/globalStyle.ts +++ b/styles/globalStyle.ts @@ -16,11 +16,14 @@ export const GlobalStyle = createGlobalStyle` html{ font-size: 16px; -webkit-text-size-adjust: none; - font-family: -apple-system,BlinkMacSystemFont,helvetica,Apple SD Gothic Neo,sans-serif; + font-family: 'Noto Sans KR',sans-serif; font-display: fallback; -ms-overflow-style: none; scrollbar-width: none; } + button,span,input{ + font-family: 'Noto Sans KR',sans-serif; + } button { background: none; padding: 0; diff --git a/styles/style.d.ts b/styles/style.d.ts index 2237fb4..ac26ac6 100644 --- a/styles/style.d.ts +++ b/styles/style.d.ts @@ -3,12 +3,30 @@ import 'styled-components'; declare module 'styled-components' { export interface DefaultTheme { color: { + white: '#ffffff'; + black: '#000000'; purple: '#8661de'; blue: '#00bac7'; gray: '#f6f6f6'; green: '#07b495'; + DefaultPrimaryGreen: '#bdF486'; + PressedPrimaryGreen: '#abf066'; lightGreen: '#99ecdd'; darkGray: '#54595d'; + backgroundGray: '#eeeeee'; + backgroundHoverGray: '#e0e0e0'; + addTextGray: '#9e9e9e'; + gray_100: '#f5f5f5'; + gray_300: '#e5e5e5'; + gray_400: '#bdbdbd'; + gray_500: '#757575'; + gray_700: '#616161'; + profileNameBlack: '#212121'; }; + fontWeight: { + bold: 700; + medium: 500; + }; + desktop: '1200px'; } } diff --git a/styles/theme.ts b/styles/theme.ts index f5beaf1..c67607e 100644 --- a/styles/theme.ts +++ b/styles/theme.ts @@ -2,11 +2,29 @@ import { DefaultTheme } from 'styled-components'; export const theme: DefaultTheme = { color: { + white: '#ffffff', + black: '#000000', purple: '#8661de', blue: '#00bac7', gray: '#f6f6f6', green: '#07b495', + DefaultPrimaryGreen: '#bdF486', + PressedPrimaryGreen: '#abf066', lightGreen: '#99ecdd', darkGray: '#54595d', + backgroundGray: '#eeeeee', //색깔 변수 이름 소통 필요 + backgroundHoverGray: '#e0e0e0', + addTextGray: '#9e9e9e', + gray_100: '#f5f5f5', + gray_300: '#e5e5e5', + gray_400: '#bdbdbd', + gray_500: '#757575', + gray_700: '#616161', + profileNameBlack: '#212121', }, + fontWeight: { + bold: 700, + medium: 500, + }, + desktop: '1200px', }; diff --git a/tsconfig.json b/tsconfig.json index d292f69..722e2d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "isolatedModules": true, "jsx": "preserve", "rootDir": ".", - "incremental": true + "incremental": true, + "baseUrl": "." }, "include": [ "next-env.d.ts", diff --git a/utils/numberWithCommas.ts b/utils/numberWithCommas.ts new file mode 100644 index 0000000..1d76b24 --- /dev/null +++ b/utils/numberWithCommas.ts @@ -0,0 +1,7 @@ +export const numberWithCommas = (num: number): string => { + let parts: number | string[] = num; + parts = parts.toString().split('.'); + return parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') + (parts[1] ? '.' + parts[1] : ''); +}; + +//숫자를 넣으면 3자리마다 , 찍히고 소수점 2자리 까지 표현