├── .nvmrc ├── src ├── react-app-env.d.ts ├── pages │ ├── Direct │ │ └── index.js │ ├── Edit │ │ ├── index.js │ │ └── Edit.tsx │ ├── Home │ │ ├── index.js │ │ └── Home.tsx │ ├── Profile │ │ ├── index.tsx │ │ └── Profile.tsx │ ├── Paragraph │ │ └── index.js │ ├── Landing │ │ └── index.tsx │ └── Auth │ │ └── index.tsx ├── styles │ ├── UI │ │ ├── Card │ │ │ ├── index.js │ │ │ └── Card.ts │ │ ├── Button │ │ │ ├── index.js │ │ │ └── Button.ts │ │ ├── Notification │ │ │ ├── index.js │ │ │ └── Notification.tsx │ │ └── ModalCard │ │ │ └── index.js │ ├── common.style.ts │ ├── theme.ts │ ├── styled.d.ts │ └── globalStyles.ts ├── components │ ├── Common │ │ ├── Article │ │ │ ├── index.js │ │ │ ├── ArticleImgSlider │ │ │ │ ├── index.js │ │ │ │ ├── ImgHashTagAvatar.tsx │ │ │ │ └── ImgHashTagUsername.tsx │ │ │ ├── ArticleCommentFormLayout.tsx │ │ │ ├── ArticleGap.tsx │ │ │ └── ArticleAlone │ │ │ │ ├── ArticleAlone.tsx │ │ │ │ └── ArticleAloneModal.tsx │ │ ├── Header │ │ │ ├── index.js │ │ │ ├── Upload │ │ │ │ ├── Cut │ │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ ├── Edit │ │ │ │ │ └── index.js │ │ │ │ ├── Content │ │ │ │ │ └── index.js │ │ │ │ ├── Uploading │ │ │ │ │ ├── index.js │ │ │ │ │ └── Uploading.tsx │ │ │ │ ├── DragAndDrop │ │ │ │ │ └── index.js │ │ │ │ ├── UploadHeader │ │ │ │ │ └── index.js │ │ │ │ ├── UploadComplete │ │ │ │ │ ├── index.js │ │ │ │ │ └── UploadComplete.tsx │ │ │ │ ├── WarningMaxUploadNumberModal.tsx │ │ │ │ └── UploadWarningModal.tsx │ │ │ └── Header.tsx │ │ ├── Footer │ │ │ ├── FooterText.type.ts │ │ │ ├── FooterRow.tsx │ │ │ ├── FooterTextPiece.tsx │ │ │ ├── Footer.tsx │ │ │ └── InstagramLinks.tsx │ │ ├── Username.ts │ │ ├── ContentBox.tsx │ │ ├── ImageSprite.tsx │ │ ├── Modal │ │ │ ├── Portal.ts │ │ │ ├── ModalContent │ │ │ │ └── ModalTitleContent.tsx │ │ │ └── BlockModal.tsx │ │ ├── StringFragmentWithMentionOrHashtagLink.tsx │ │ ├── Loading.tsx │ │ ├── PopHeart.tsx │ │ └── StoryCircle.tsx │ ├── Direct │ │ ├── Aside │ │ │ ├── ChatList │ │ │ │ ├── index.js │ │ │ │ ├── ChatListItem │ │ │ │ │ └── index.js │ │ │ │ └── ChatList.type.ts │ │ │ └── AsideHeader.tsx │ │ └── Section │ │ │ ├── requestsSection.tsx │ │ │ ├── Modals │ │ │ ├── NewChatModal │ │ │ │ ├── NewChatInviteButton.tsx │ │ │ │ ├── NewChatModal.tsx │ │ │ │ ├── NewChatFriendList.tsx │ │ │ │ ├── NewChatModalTitle.tsx │ │ │ │ ├── NewChatSearchBar.tsx │ │ │ │ └── NewChatRecommendUser.tsx │ │ │ ├── DeleteChatModal.tsx │ │ │ ├── CommonDirectModal.tsx │ │ │ └── LikedMemberModal.tsx │ │ │ └── InboxSection.tsx │ ├── Home │ │ ├── Modals │ │ │ ├── ReportModal │ │ │ │ ├── index.js │ │ │ │ └── ReportModal.tsx │ │ │ ├── ModalHeader.tsx │ │ │ └── FollowingModal.tsx │ │ ├── ExtraLoadingCircle.tsx │ │ ├── Story.tsx │ │ ├── HomeSection.tsx │ │ └── SearchListItem.tsx │ ├── Auth │ │ ├── SubmitButton.ts │ │ ├── Line.tsx │ │ ├── ResetPassword │ │ │ └── index.tsx │ │ ├── Suggest.tsx │ │ ├── SignUpForm │ │ │ ├── validator.ts │ │ │ └── SignUpFormContent.tsx │ │ ├── FacebookLogin.tsx │ │ ├── AppDownload.tsx │ │ ├── LoginForm │ │ │ ├── FormAndButton.tsx │ │ │ └── LoginFormContent.tsx │ │ └── Form.tsx │ ├── Edit │ │ ├── Section │ │ │ ├── Section.tsx │ │ │ └── EditItemInput.tsx │ │ ├── Menus │ │ │ ├── naver_map.ts │ │ │ └── Activity.tsx │ │ └── Aside │ │ │ └── Aside.tsx │ └── Profile │ │ ├── Article │ │ ├── NoArticle.tsx │ │ ├── SingleRow.tsx │ │ └── Article.tsx │ │ └── Modals │ │ ├── SettingModal.tsx │ │ └── UserActionModal.tsx ├── utils │ ├── maps.ts │ └── constant.ts ├── assets │ ├── Images │ │ ├── direct.png │ │ ├── sprite.png │ │ ├── sprite2.png │ │ ├── sprite3.png │ │ ├── sprite4.png │ │ ├── sprite5.png │ │ ├── appStore.png │ │ ├── emailLogo.png │ │ ├── googlePlay.png │ │ ├── home-phone.png │ │ ├── slider │ │ │ ├── home.jpg │ │ │ ├── talk.jpg │ │ │ ├── ImageEdit.jpg │ │ │ ├── instagram.jpg │ │ │ └── takePhoto.jpg │ │ ├── loginPageSprite.png │ │ ├── logo-hello-world.png │ │ └── location-error-maps.png │ └── Svgs │ │ ├── circle.svg │ │ ├── header-nav-bar │ │ ├── store.svg │ │ ├── change.svg │ │ ├── profile.svg │ │ └── setting.svg │ │ ├── arrow-up.svg │ │ ├── checkedCircle.svg │ │ ├── cut.svg │ │ ├── filled-checked-icon.svg │ │ ├── filledBookmark.svg │ │ ├── direct.svg │ │ ├── threeDots.svg │ │ ├── home-active.svg │ │ ├── direct-active.svg │ │ ├── downV.svg │ │ ├── v.svg │ │ ├── plus.svg │ │ ├── commentBubble.svg │ │ ├── slide.svg │ │ ├── direct-detail-info-active.svg │ │ ├── avatar.svg │ │ ├── emptyBookmark.svg │ │ ├── heart-active.svg │ │ ├── close.svg │ │ ├── rightArrow.svg │ │ ├── squareCut.svg │ │ ├── fatRectangle.svg │ │ ├── thinRectangle.svg │ │ ├── leftArrow.svg │ │ ├── map-active.svg │ │ ├── position.svg │ │ ├── home.svg │ │ ├── resize.svg │ │ ├── map.svg │ │ ├── direct-emoji-icon.svg │ │ ├── location.svg │ │ ├── redHeart.svg │ │ ├── new-article-active.svg │ │ ├── gallery.svg │ │ ├── heart.svg │ │ ├── paperAirplane.svg │ │ ├── cancel.svg │ │ ├── delete.svg │ │ ├── back.svg │ │ ├── smileFace.svg │ │ ├── direct-detail-info.svg │ │ ├── direct-image-upload.svg │ │ ├── dm-write.svg │ │ ├── tag.svg │ │ ├── new-article.svg │ │ ├── grid.svg │ │ ├── emptyHeart.svg │ │ ├── photoOutline.svg │ │ ├── setting.svg │ │ ├── moreComments.svg │ │ ├── CloseSVG.tsx │ │ ├── instagram-logo-loading.svg │ │ └── imgOrVideo.svg ├── app │ └── store │ │ ├── Hooks.ts │ │ ├── ducks │ │ ├── home │ │ │ └── homeThunk.type.ts │ │ ├── auth │ │ │ └── authThunk.type.ts │ │ ├── Article │ │ │ └── articleThunk.ts │ │ ├── common │ │ │ ├── commonThunk.ts │ │ │ └── commonSlice.ts │ │ ├── modal │ │ │ └── modalThunk.ts │ │ └── edit │ │ │ ├── editSlice.ts │ │ │ └── editThunk.ts │ │ └── store.ts ├── InstagramLoading.tsx ├── hooks │ ├── useGapText.ts │ ├── useNumberSummary.ts │ ├── useOnView.ts │ ├── useCopy.ts │ ├── useOutsideClick.ts │ └── useInput.ts ├── index.tsx ├── App.tsx └── customAxios.ts ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── favicon-192.png ├── manifest.json └── index.html ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ └── feature_request.md ├── base-tsconfig.json ├── .gitignore ├── .prettierrc ├── tsconfig.json └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.17.0 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/pages/Direct/index.js: -------------------------------------------------------------------------------- 1 | import Direct from "./Direct"; 2 | 3 | export default Direct; 4 | -------------------------------------------------------------------------------- /src/pages/Edit/index.js: -------------------------------------------------------------------------------- 1 | import Edit from "pages/Edit/Edit"; 2 | 3 | export default Edit; 4 | -------------------------------------------------------------------------------- /src/pages/Home/index.js: -------------------------------------------------------------------------------- 1 | import Home from "pages/Home/Home"; 2 | 3 | export default Home; 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/styles/UI/Card/index.js: -------------------------------------------------------------------------------- 1 | import Card from "styles/UI/Card/Card"; 2 | 3 | export default Card; 4 | -------------------------------------------------------------------------------- /src/components/Common/Article/index.js: -------------------------------------------------------------------------------- 1 | import Article from "./Article"; 2 | 3 | export default Article; 4 | -------------------------------------------------------------------------------- /src/components/Common/Header/index.js: -------------------------------------------------------------------------------- 1 | import Header from "./Header"; 2 | 3 | export default Header; 4 | -------------------------------------------------------------------------------- /src/styles/UI/Button/index.js: -------------------------------------------------------------------------------- 1 | import Button from "styles/UI/Button/Button"; 2 | export default Button; 3 | -------------------------------------------------------------------------------- /src/pages/Profile/index.tsx: -------------------------------------------------------------------------------- 1 | import Profile from "pages/Profile/Profile"; 2 | 3 | export default Profile; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/components/Direct/Aside/ChatList/index.js: -------------------------------------------------------------------------------- 1 | import ChatList from "./ChatList"; 2 | 3 | export default ChatList; 4 | -------------------------------------------------------------------------------- /src/pages/Paragraph/index.js: -------------------------------------------------------------------------------- 1 | import Paragraph from "pages/Paragraph/Paragraph"; 2 | 3 | export default Paragraph; 4 | -------------------------------------------------------------------------------- /src/styles/UI/Notification/index.js: -------------------------------------------------------------------------------- 1 | import Notification from "./Notification"; 2 | 3 | export default Notification; 4 | -------------------------------------------------------------------------------- /public/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/public/favicon-192.png -------------------------------------------------------------------------------- /src/styles/UI/ModalCard/index.js: -------------------------------------------------------------------------------- 1 | import ModalCard from "styles/UI/ModalCard/ModalCard"; 2 | 3 | export default ModalCard; 4 | -------------------------------------------------------------------------------- /src/utils/maps.ts: -------------------------------------------------------------------------------- 1 | export const isInvalidLocation = (location: string) => { 2 | return location === "Unknown"; 3 | }; 4 | -------------------------------------------------------------------------------- /src/components/Home/Modals/ReportModal/index.js: -------------------------------------------------------------------------------- 1 | import ReportModal from "./ReportModal"; 2 | 3 | export default ReportModal; 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 개요 2 | 3 | ## 작업사항 4 | 5 | - 내용을 적어주세요. 6 | 7 | ## 주요 변경 로직 8 | 9 | - 내용을 적어주세요. 10 | -------------------------------------------------------------------------------- /src/assets/Images/direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/direct.png -------------------------------------------------------------------------------- /src/assets/Images/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/sprite.png -------------------------------------------------------------------------------- /src/assets/Images/sprite2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/sprite2.png -------------------------------------------------------------------------------- /src/assets/Images/sprite3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/sprite3.png -------------------------------------------------------------------------------- /src/assets/Images/sprite4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/sprite4.png -------------------------------------------------------------------------------- /src/assets/Images/sprite5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/sprite5.png -------------------------------------------------------------------------------- /src/assets/Images/appStore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/appStore.png -------------------------------------------------------------------------------- /src/assets/Images/emailLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/emailLogo.png -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/Cut/index.js: -------------------------------------------------------------------------------- 1 | import Cut from "components/Common/Header/Upload/Cut/Cut"; 2 | 3 | export default Cut; 4 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/index.js: -------------------------------------------------------------------------------- 1 | import Upload from "components/Common/Header/Upload/Upload"; 2 | 3 | export default Upload; 4 | -------------------------------------------------------------------------------- /src/components/Direct/Aside/ChatList/ChatListItem/index.js: -------------------------------------------------------------------------------- 1 | import ChatListItem from "./ChatListItem"; 2 | 3 | export default ChatListItem; 4 | -------------------------------------------------------------------------------- /src/assets/Images/googlePlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/googlePlay.png -------------------------------------------------------------------------------- /src/assets/Images/home-phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/home-phone.png -------------------------------------------------------------------------------- /src/assets/Images/slider/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/slider/home.jpg -------------------------------------------------------------------------------- /src/assets/Images/slider/talk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/slider/talk.jpg -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/Edit/index.js: -------------------------------------------------------------------------------- 1 | import Edit from "components/Common/Header/Upload/Edit/Edit"; 2 | 3 | export default Edit; 4 | -------------------------------------------------------------------------------- /src/components/Direct/Aside/ChatList/ChatList.type.ts: -------------------------------------------------------------------------------- 1 | export default interface ChatListProps { 2 | chatList?: Array; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/Images/loginPageSprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/loginPageSprite.png -------------------------------------------------------------------------------- /src/assets/Images/logo-hello-world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/logo-hello-world.png -------------------------------------------------------------------------------- /src/assets/Images/slider/ImageEdit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/slider/ImageEdit.jpg -------------------------------------------------------------------------------- /src/assets/Images/slider/instagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/slider/instagram.jpg -------------------------------------------------------------------------------- /src/assets/Images/slider/takePhoto.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/slider/takePhoto.jpg -------------------------------------------------------------------------------- /src/components/Common/Footer/FooterText.type.ts: -------------------------------------------------------------------------------- 1 | export default interface FooterContentProps { 2 | content: Array; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/Images/location-error-maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Instagram-Clone-Coding/React_instagram_clone/HEAD/src/assets/Images/location-error-maps.png -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/Content/index.js: -------------------------------------------------------------------------------- 1 | import Content from "components/Common/Header/Upload/Content/Content"; 2 | 3 | export default Content; 4 | -------------------------------------------------------------------------------- /base-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@": ["./src/"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/Uploading/index.js: -------------------------------------------------------------------------------- 1 | import Uploading from "components/Common/Header/Upload/Uploading/Uploading"; 2 | 3 | export default Uploading; 4 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/DragAndDrop/index.js: -------------------------------------------------------------------------------- 1 | import DragAndDrop from "components/Common/Header/Upload/DragAndDrop/DragAndDrop"; 2 | 3 | export default DragAndDrop; 4 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/UploadHeader/index.js: -------------------------------------------------------------------------------- 1 | import UploadHeader from "components/Common/Header/Upload/UploadHeader/UploadHeader"; 2 | 3 | export default UploadHeader; 4 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleImgSlider/index.js: -------------------------------------------------------------------------------- 1 | import ArticleImgSlider from "components/Common/Article/ArticleImgSlider/ArticleImgSlider"; 2 | 3 | export default ArticleImgSlider; 4 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/UploadComplete/index.js: -------------------------------------------------------------------------------- 1 | import UploadComplete from "components/Common/Header/Upload/UploadComplete/UploadComplete"; 2 | 3 | export default UploadComplete; 4 | -------------------------------------------------------------------------------- /src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const INVALID_TOKEN_MESSAGE = "유효하지 않은 토큰입니다."; 2 | export const EXPIRED_TOKEN_MESSAGE = 3 | "만료된 REFRESH 토큰입니다. 재로그인 해주십시오."; 4 | export const FAIL_TO_REISSUE_MESSAGE = "fail to reIssue"; 5 | -------------------------------------------------------------------------------- /src/assets/Svgs/circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /src/styles/common.style.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const SizedBox = styled.div<{ 4 | height?: number; 5 | width?: number; 6 | }>` 7 | ${({ height }) => height && `height: ${height}px;`} 8 | ${({ width }) => width && `width: ${width}px;`} 9 | `; 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Description 11 | 12 | > description 13 | 14 | ## Todo 15 | - [ ] todo1 16 | - [ ] todo2 17 | - [ ] todo3 18 | -------------------------------------------------------------------------------- /src/components/Common/Username.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Username = styled.span` 4 | font-weight: ${(props) => props.theme.font.bold}; 5 | cursor: pointer; 6 | &:hover { 7 | text-decoration: underline; 8 | } 9 | `; 10 | 11 | export default Username; 12 | -------------------------------------------------------------------------------- /src/assets/Svgs/header-nav-bar/store.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Svgs/arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/Svgs/checkedCircle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Svgs/cut.svg: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/Svgs/filled-checked-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Svgs/filledBookmark.svg: -------------------------------------------------------------------------------- 1 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/Svgs/direct.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/Common/ContentBox.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Card from "styles/UI/Card/Card"; 3 | 4 | const ContentBox = styled(Card)` 5 | margin: ${(props) => props.margin}; 6 | padding: ${(props) => props.padding}; 7 | border-radius: 1px; 8 | `; 9 | 10 | export default ContentBox; 11 | -------------------------------------------------------------------------------- /src/assets/Svgs/threeDots.svg: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/store/Hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; 2 | import type { RootState, AppDispatch } from "./store"; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/app/store/ducks/home/homeThunk.type.ts: -------------------------------------------------------------------------------- 1 | // fetched data type 2 | export interface RecentArticlesProps { 3 | data: { 4 | data: PostType.ArticleProps[]; 5 | }; 6 | } 7 | 8 | export interface ExtraArticleProps { 9 | data: { 10 | data: { 11 | content: [PostType.ArticleProps]; 12 | empty: boolean; 13 | }; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/Svgs/home-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/Svgs/direct-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/Svgs/downV.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/Svgs/v.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Auth/SubmitButton.ts: -------------------------------------------------------------------------------- 1 | import Button from "styles/UI/Button/Button"; 2 | import styled from "styled-components"; 3 | 4 | const SubmitButton = styled(Button)` 5 | margin: 8px 40px; 6 | opacity: 1; 7 | border: 1px solid transparent; 8 | &:disabled { 9 | opacity: 0.3; 10 | pointer-events: none; 11 | } 12 | `; 13 | 14 | export default SubmitButton; 15 | -------------------------------------------------------------------------------- /src/assets/Svgs/plus.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/Svgs/commentBubble.svg: -------------------------------------------------------------------------------- 1 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/Svgs/slide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Svgs/direct-detail-info-active.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/Svgs/avatar.svg: -------------------------------------------------------------------------------- 1 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/Svgs/emptyBookmark.svg: -------------------------------------------------------------------------------- 1 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/Svgs/heart-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/Svgs/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/Svgs/rightArrow.svg: -------------------------------------------------------------------------------- 1 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/Svgs/squareCut.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/styles/UI/Button/Button.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Button = styled.button` 4 | border-radius: ${(props) => props.radius + "px"}; 5 | padding: 5px 9px; 6 | background-color: ${(props) => props.bgColor || props.theme.color.blue}; 7 | color: ${(props) => props.color}; 8 | `; 9 | 10 | Button.defaultProps = { 11 | radius: 4, 12 | color: "white", 13 | }; 14 | 15 | export default Button; 16 | -------------------------------------------------------------------------------- /src/assets/Svgs/fatRectangle.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/Svgs/header-nav-bar/change.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/Svgs/thinRectangle.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/Svgs/leftArrow.svg: -------------------------------------------------------------------------------- 1 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/Common/ImageSprite.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const ImageSprite = styled.div` 4 | background: ${(props) => `url(${props.url}) no-repeat`}; 5 | width: ${(props) => props.width}px; 6 | height: ${(props) => props.height}px; 7 | background-position: ${(props) => props.position}; 8 | background-size: ${(props) => props.size && `${props.size}`}; 9 | `; 10 | 11 | export default ImageSprite; 12 | -------------------------------------------------------------------------------- /src/assets/Svgs/map-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/Svgs/position.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | #image 26 | .png 27 | .idea/ 28 | 29 | 30 | # 31 | .env -------------------------------------------------------------------------------- /src/app/store/ducks/auth/authThunk.type.ts: -------------------------------------------------------------------------------- 1 | export interface SignInRequestType { 2 | username: string; 3 | password: string; 4 | } 5 | 6 | export type FormState = "signUp" | "confirmEmail" | "signIn"; 7 | 8 | export type LoginDevice = { 9 | tokenId: string; 10 | location: { 11 | city: string; 12 | longitude: string; 13 | latitude: string; 14 | }; 15 | device: string; 16 | lastLoginDate: string; 17 | current: boolean; 18 | }; 19 | -------------------------------------------------------------------------------- /src/styles/theme.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTheme } from "styled-components"; 2 | 3 | const theme: DefaultTheme = { 4 | color: { 5 | bg_gray: "#fafafa", 6 | bg_white: "#ffffff", 7 | bd_gray: "#dbdbdb", 8 | blue: "#0095F6", 9 | }, 10 | font: { 11 | default_black: "#262626", 12 | bold: 600, 13 | gray: "#8e8e8e", 14 | link_blue: "#00376b", 15 | red: "#ed4956", 16 | }, 17 | }; 18 | 19 | export default theme; 20 | -------------------------------------------------------------------------------- /src/assets/Svgs/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "preserve", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": false, 14 | "tabWidth": 4, 15 | "trailingComma": "all", 16 | "useTabs": false 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/Svgs/resize.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/Svgs/map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/styles/styled.d.ts: -------------------------------------------------------------------------------- 1 | import "styled-components"; 2 | 3 | declare module "styled-components" { 4 | export interface DefaultTheme { 5 | color: { 6 | bg_gray: string; 7 | bg_white: string; 8 | bd_gray: string; 9 | blue: string; 10 | }; 11 | font: { 12 | default_black: string; 13 | bold: number; 14 | gray: string; 15 | link_blue: string; 16 | red: string; 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/Svgs/direct-emoji-icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Common/Modal/Portal.ts: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { createPortal } from "react-dom"; 3 | 4 | 5 | interface PortalProps { 6 | children: React.ReactNode; 7 | elementId: string; 8 | } 9 | 10 | 11 | const Portal = ({ children, elementId } : PortalProps) => { 12 | const rootElement = useMemo(() => document.getElementById(elementId), [ 13 | elementId, 14 | ]); 15 | 16 | return createPortal(children, rootElement as HTMLElement); 17 | }; 18 | 19 | export default Portal; -------------------------------------------------------------------------------- /src/assets/Svgs/location.svg: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/Svgs/redHeart.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/InstagramLoading.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { ReactComponent as InstagramLogoLoading } from "assets/Svgs/instagram-logo-loading.svg"; 3 | 4 | const StyledInstagramLoading = styled.div` 5 | position: absolute; 6 | top: 50%; 7 | left: 50%; 8 | margin: -25px 0 0 -25px; 9 | `; 10 | 11 | const InstagramLoading = () => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default InstagramLoading; 20 | -------------------------------------------------------------------------------- /src/assets/Svgs/new-article-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/hooks/useGapText.ts: -------------------------------------------------------------------------------- 1 | const useGapText = (createdAt: string): string => { 2 | const gap = Date.now() - Date.parse(createdAt); 3 | if (gap >= 604800000) { 4 | return `${Math.floor(gap / 604800000)}주`; 5 | } else if (gap >= 86400000) { 6 | return `${Math.floor(gap / 86400000)}일`; 7 | } else if (gap >= 3600000) { 8 | return `${Math.floor(gap / 3600000)}시간`; 9 | } else if (gap >= 60000) { 10 | return `${Math.floor(gap / 60000)}분`; 11 | } else { 12 | return "방금"; 13 | } 14 | }; 15 | 16 | export default useGapText; 17 | -------------------------------------------------------------------------------- /src/components/Home/ExtraLoadingCircle.tsx: -------------------------------------------------------------------------------- 1 | import Loading from "components/Common/Loading"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | const StyledExtraLoadingCircle = styled.div` 6 | height: 48px; 7 | margin-top: 40px; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | `; 12 | 13 | const ExtraLoadingCircle = () => { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default ExtraLoadingCircle; 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/Svgs/gallery.svg: -------------------------------------------------------------------------------- 1 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/Svgs/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/Svgs/header-nav-bar/profile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import App from "App"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { ThemeProvider } from "styled-components"; 5 | import GlobalStlyes from "styles/globalStyles"; 6 | import theme from "styles/theme"; 7 | 8 | import { Provider } from "react-redux"; 9 | import { store } from "app/store/store"; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById("root"), 21 | ); 22 | -------------------------------------------------------------------------------- /src/hooks/useNumberSummary.ts: -------------------------------------------------------------------------------- 1 | const MILLION = 1000000; 2 | const THOUSAND: number = 1000; 3 | 4 | const useNumberSummary = (num: number): string => { 5 | if (num > MILLION * 100) { 6 | return `${Math.floor(num / MILLION)}백만`; // 123백만 7 | } else if (num > MILLION) { 8 | return `${Math.floor((num / MILLION) * 10) / 10}백만`; // 1.2백만, 12.4백만 9 | } else if (num > THOUSAND * 100) { 10 | return `${Math.floor(num / THOUSAND)}천`; // 123천 11 | } else if (num > THOUSAND * 10) { 12 | return `${Math.floor((num / THOUSAND) * 10) / 10}천`; // 12.4천 13 | } else { 14 | return `${num}`; // 그냥 수 15 | } 16 | }; 17 | export default useNumberSummary; 18 | -------------------------------------------------------------------------------- /src/assets/Svgs/paperAirplane.svg: -------------------------------------------------------------------------------- 1 | 11 | 21 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Common/Footer/FooterRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import FooterTextPiece from "components/Common/Footer/FooterTextPiece"; 4 | import FooterContentProps from "components/Common/Footer/FooterText.type"; 5 | 6 | const FlexRow = styled.div` 7 | display: flex; 8 | justify-content: center; 9 | flex-wrap: wrap; 10 | `; 11 | 12 | function FooterRow(props: FooterContentProps) { 13 | return ( 14 | 15 | {props.content.map((data, index) => ( 16 | 17 | ))} 18 | 19 | ); 20 | } 21 | 22 | export default FooterRow; 23 | -------------------------------------------------------------------------------- /src/assets/Svgs/cancel.svg: -------------------------------------------------------------------------------- 1 | 10 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/Svgs/delete.svg: -------------------------------------------------------------------------------- 1 | 10 | 21 | 32 | 33 | -------------------------------------------------------------------------------- /src/assets/Svgs/back.svg: -------------------------------------------------------------------------------- 1 | 10 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/Svgs/smileFace.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": "src", 19 | "typeRoots": ["./node_modules/@types", "./src/@types"] 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /src/assets/Svgs/direct-detail-info.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 8 | 10 | -------------------------------------------------------------------------------- /src/assets/Svgs/direct-image-upload.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 8 | -------------------------------------------------------------------------------- /src/assets/Svgs/dm-write.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/Svgs/tag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleImgSlider/ImgHashTagAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { ReactComponent as Avatar } from "assets/Svgs/avatar.svg"; 2 | import styled from "styled-components"; 3 | 4 | const StyledImgHashTagAvater = styled.button` 5 | background-color: rgba(0, 0, 0, 0.8); 6 | margin: 12px; 7 | width: 28px; 8 | height: 28px; 9 | border-radius: 50%; 10 | @keyframes avatarOn { 11 | from { 12 | opacity: 0; 13 | } 14 | to { 15 | opacity: 1; 16 | } 17 | } 18 | animation: 0.5s avatarOn; 19 | `; 20 | 21 | const ImgHashTagAvatar = () => { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default ImgHashTagAvatar; 30 | -------------------------------------------------------------------------------- /src/assets/Svgs/new-article.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/styles/UI/Card/Card.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export interface CardProps { 4 | isNav?: boolean; 5 | radius?: number; 6 | isArticle?: boolean; 7 | } 8 | 9 | const Card = styled.div` 10 | border: 1px solid 11 | ${(props) => 12 | props.isNav || props.isArticle 13 | ? "none" 14 | : props.theme.color.bd_gray}; 15 | border-bottom: 1px solid 16 | ${(props) => (props.isNav ? props.theme.color.bd_gray : "none")}; 17 | border-radius: ${(props) => (props.isArticle ? 4 : props.radius) + "px"}; 18 | background-color: ${(props) => props.theme.color.bg_white}; 19 | `; 20 | 21 | Card.defaultProps = { 22 | isNav: false, 23 | radius: 3, 24 | isArticle: false, 25 | }; 26 | 27 | export default Card; 28 | -------------------------------------------------------------------------------- /src/hooks/useOnView.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useMemo, useState } from "react"; 2 | 3 | const useOnView = (ref: RefObject | null) => { 4 | const [isIntersecting, setIntersecting] = useState(false); 5 | 6 | const observer = useMemo( 7 | () => 8 | new IntersectionObserver(([entry]) => 9 | setIntersecting(entry.isIntersecting), 10 | ), 11 | [], 12 | ); 13 | 14 | useEffect(() => { 15 | if (ref?.current) { 16 | observer.observe(ref.current); 17 | } 18 | // Remove the observer as soon as the component is unmounted 19 | return () => { 20 | observer.disconnect(); 21 | }; 22 | }, [observer, ref]); 23 | 24 | return isIntersecting; 25 | }; 26 | 27 | export default useOnView; 28 | -------------------------------------------------------------------------------- /src/assets/Svgs/grid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Common/Footer/FooterTextPiece.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const ContentStyle = styled.div` 4 | margin: 0 8px 12px 8px; 5 | a { 6 | text-decoration: none; 7 | } 8 | 9 | .text { 10 | color: #8e8e8e; 11 | font-weight: 400; 12 | font-size: 12px; 13 | line-height: 16px; 14 | margin: -2px 0 -3px; 15 | } 16 | `; 17 | 18 | function FooterTextPiece(props: CommonType.FooterTextProps) { 19 | return ( 20 | 21 | {props.url ? ( 22 | 23 |
{props.text}
24 |
25 | ) : ( 26 |
{props.text}
27 | )} 28 |
29 | ); 30 | } 31 | 32 | export default FooterTextPiece; 33 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/Uploading/Uploading.tsx: -------------------------------------------------------------------------------- 1 | import UploadHeader from "components/Common/Header/Upload/UploadHeader"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | const StyledUploading = styled.div` 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | `; 12 | 13 | const Uploading = () => { 14 | return ( 15 | <> 16 | 17 | 18 | 공유 중입니다. 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Uploading; 30 | -------------------------------------------------------------------------------- /src/assets/Svgs/emptyHeart.svg: -------------------------------------------------------------------------------- 1 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Edit/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from "app/store/Hooks"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import Activity from "../Menus/Activity"; 5 | import PasswordEdit from "../Menus/PasswordEdit"; 6 | import ProfileEdit from "../Menus/ProfileEdit"; 7 | 8 | const Container = styled.div``; 9 | 10 | const Section = () => { 11 | const currentMenu = useAppSelector((state) => state.edit.currentMenu); 12 | const renderComponent = () => { 13 | switch (currentMenu) { 14 | case "프로필 편집": 15 | return ; 16 | case "비밀번호 변경": 17 | return ; 18 | case "로그인 활동": 19 | return ; 20 | } 21 | }; 22 | return {renderComponent()}; 23 | }; 24 | 25 | export default Section; 26 | -------------------------------------------------------------------------------- /src/components/Common/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import FooterRow from "components/Common/Footer/FooterRow"; 3 | import InstagramLinks from "components/Common/Footer/InstagramLinks"; 4 | 5 | const FooterContainer = styled.footer` 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | padding: 0 16px; 10 | margin-bottom: 40px; 11 | 12 | .copyrightContainer { 13 | margin: 12px 0; 14 | width: 100%; 15 | } 16 | `; 17 | 18 | const copyright = [{ text: "한국어" }, { text: "© 2021 Instagram from Meta" }]; 19 | 20 | export function Footer() { 21 | return ( 22 | 23 | 24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Auth/Line.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const LineStyle = styled.div<{ margin?: string }>` 4 | margin: ${(props) => props.margin || "10px 40px 18px"}; 5 | display: flex; 6 | 7 | & > span { 8 | color: #8e8e8e; 9 | font-size: 13px; 10 | font-weight: ${(props) => props.theme.font.bold}; 11 | margin: 0 18px; 12 | line-height: 15px; 13 | } 14 | 15 | & > div { 16 | background-color: ${(props) => props.theme.color.bd_gray}; 17 | height: 1px; 18 | flex-grow: 1; 19 | flex-shrink: 1; 20 | top: 0.45em; 21 | position: relative; 22 | } 23 | `; 24 | 25 | export default function Line(props: { margin?: string }) { 26 | return ( 27 | 28 |
29 | 또는 30 |
31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Common/Modal/ModalContent/ModalTitleContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | 5 | interface ModalTitleContentProps { 6 | title: string; 7 | description?: string; 8 | } 9 | 10 | const ModalTitleContentContainer = styled.div` 11 | text-align: center; 12 | display: flex; 13 | flex-direction: column; 14 | margin: 0 40px; 15 | margin-bottom: 30px; 16 | 17 | h3{ 18 | font-weight: 600; 19 | font-size: 18px; 20 | line-height: 24px; 21 | } 22 | 23 | div{ 24 | padding-top: 1rem; 25 | color: #8e8e8e; 26 | } 27 | `; 28 | const ModalTitleContent = ({ title, description }: ModalTitleContentProps) => { 29 | return ( 30 | 31 |

{title}

32 |
{description}
33 |
34 | ); 35 | }; 36 | 37 | export default ModalTitleContent; -------------------------------------------------------------------------------- /src/assets/Svgs/photoOutline.svg: -------------------------------------------------------------------------------- 1 | 10 | 14 | 21 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleCommentFormLayout.tsx: -------------------------------------------------------------------------------- 1 | import CommentForm from "components/Common/Article/CommentForm"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | const StyledAricleCommentFormLayout = styled.div` 6 | padding: 6px 16px; 7 | display: flex; 8 | align-items: center; 9 | border-top: 1px solid ${(props) => props.theme.color.bd_gray}; 10 | `; 11 | 12 | interface ArticleCommentFormLayoutProps { 13 | postId: number; 14 | isInLargerArticle: boolean; 15 | } 16 | 17 | const ArticleCommentFormLayout = ({ 18 | postId, 19 | isInLargerArticle, 20 | }: ArticleCommentFormLayoutProps) => { 21 | return ( 22 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default React.memo(ArticleCommentFormLayout); 32 | -------------------------------------------------------------------------------- /src/components/Auth/ResetPassword/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from "app/store/Hooks"; 2 | import styled from "styled-components"; 3 | import HeaderBeforeLogin from "./HeaderBeforeLogin"; 4 | import InformSentEmail from "./InformSentEmail"; 5 | import ResetPasswordEmailRequestForm from "./ResetPasswordEmailRequestForm"; 6 | 7 | const Container = styled.div` 8 | height: 100vh; 9 | `; 10 | 11 | export default function ResetPassword() { 12 | const sentEmail = useAppSelector((state) => state.auth.resetPassword.email); 13 | 14 | return ( 15 | 16 | {sentEmail ? ( 17 | <> 18 | 19 | 20 | 21 | ) : ( 22 | <> 23 | 24 | 25 | 26 | )} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/Svgs/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Home/Modals/ModalHeader.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import CloseSVG from "assets/Svgs/CloseSVG"; 3 | 4 | const StyledModalHeader = styled.div` 5 | height: 43px; 6 | display: flex; 7 | align-items: center; 8 | border-bottom: ${(props) => props.theme.color.bd_gray} 1px solid; 9 | & > h1 { 10 | flex: 1; 11 | text-align: center; 12 | font-size: 16px; 13 | font-weight: ${(props) => props.theme.font.bold}; 14 | } 15 | & > div, 16 | & > button { 17 | min-width: 48px; 18 | } 19 | `; 20 | 21 | interface ModalHeaderProps { 22 | title: string; 23 | onModalOff: () => void; 24 | } 25 | 26 | const ModalHeader = ({ title, onModalOff }: ModalHeaderProps) => { 27 | return ( 28 | 29 |
30 |

{title}

31 | 34 |
35 | ); 36 | }; 37 | 38 | export default ModalHeader; 39 | -------------------------------------------------------------------------------- /src/assets/Svgs/header-nav-bar/setting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/UploadComplete/UploadComplete.tsx: -------------------------------------------------------------------------------- 1 | import UploadHeader from "components/Common/Header/Upload/UploadHeader"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | const StyledUploadComplete = styled.div` 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | & > h2 { 13 | margin: 16px 0; 14 | font-size: 22px; 15 | font-weight: 300; 16 | } 17 | `; 18 | const UploadComplete = () => { 19 | return ( 20 | <> 21 | 22 | 23 | 게시물이 공유되었습니다 29 |

게시물이 공유되었습니다

30 |
31 | 32 | ); 33 | }; 34 | 35 | export default UploadComplete; 36 | -------------------------------------------------------------------------------- /src/assets/Svgs/moreComments.svg: -------------------------------------------------------------------------------- 1 | 10 | 댓글 더 읽어들이기 11 | 21 | 32 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Direct/Section/requestsSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const RequestsSectionContainer = styled.section` 5 | padding: 24px; 6 | height: 100%; 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | h2 { 13 | font-weight: 300; 14 | font-size: 22px; 15 | line-height: 26px; 16 | margin-bottom: 10px; 17 | } 18 | 19 | div { 20 | text-align: center; 21 | color: #8e8e8e; 22 | font-weight: 400; 23 | font-size: 14px; 24 | line-height: 18px; 25 | } 26 | `; 27 | 28 | const RequestsSection = () => { 29 | return ( 30 | 31 |

메시지 요청

32 |
33 | 회원님이 제한하거나 팔로우하지 않는 사람에게 받은 메시지입니다. 34 | 회원님에게 메시지를 보낼 수 있도록 허용하지 않는 이상 상대방은 35 | 회원님이 요청을 확인했다는 사실을 알 수 없습니다. 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default RequestsSection; 42 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleGap.tsx: -------------------------------------------------------------------------------- 1 | import useGapText from "hooks/useGapText"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | interface StyledArticleGapProps { 6 | isAboutComment: boolean; 7 | } 8 | 9 | const StyledArticleGap = styled.time` 10 | padding-left: ${({ isAboutComment }) => (isAboutComment ? "0px" : "16px")}; 11 | margin-right: 12px; 12 | margin-bottom: 16px; 13 | color: ${(props) => props.theme.font.gray}; 14 | font-size: ${({ isAboutComment }) => (isAboutComment ? "12px" : "10px")}; 15 | `; 16 | 17 | interface ArticleGapProps { 18 | postUploadDate: string; 19 | isAboutComment?: boolean; 20 | } 21 | 22 | const ArticleGap = ({ 23 | postUploadDate, 24 | isAboutComment = false, 25 | }: ArticleGapProps) => { 26 | const gapText = `${useGapText(postUploadDate)} ${ 27 | isAboutComment ? "" : "전" 28 | }`; 29 | 30 | return ( 31 | 32 | {gapText} 33 | 34 | ); 35 | }; 36 | 37 | export default React.memo(ArticleGap); 38 | -------------------------------------------------------------------------------- /src/components/Edit/Menus/naver_map.ts: -------------------------------------------------------------------------------- 1 | export default class NaverMap { 2 | private apiKey?: string; 3 | private baseURL: string; 4 | 5 | constructor() { 6 | this.apiKey = process.env.REACT_APP_NAVER_MAP_ID; 7 | this.baseURL = `https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${this.apiKey}`; 8 | } 9 | 10 | loadScript(callbackAfterLoad: Function) { 11 | const existingScript = document.getElementById(`naverMaps`); 12 | 13 | if (!existingScript) { 14 | const mapScript = this.getScript(this.baseURL, `naverMaps`); 15 | mapScript.onload = () => { 16 | if (callbackAfterLoad) callbackAfterLoad(); 17 | }; 18 | } 19 | if (existingScript && callbackAfterLoad) callbackAfterLoad(); 20 | } 21 | 22 | getScript(src: string, id: string) { 23 | const script = document.createElement("script"); 24 | script.src = src; 25 | script.id = id; 26 | document.body.appendChild(script); 27 | return script; 28 | } 29 | } 30 | 31 | // 참고: https://betterprogramming.pub/loading-third-party-scripts-dynamically-in-reactjs-458c41a7013d 32 | -------------------------------------------------------------------------------- /src/components/Profile/Article/NoArticle.tsx: -------------------------------------------------------------------------------- 1 | import ImageSprite from "components/Common/ImageSprite"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | const NoArticleContainer = styled.div` 6 | width: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | flex-direction: column; 11 | margin: 100px auto; 12 | h2 { 13 | font-weight: 300; 14 | font-size: 28px; 15 | line-height: 32px; 16 | color: #262626; 17 | margin: 30px; 18 | } 19 | p { 20 | width: 350px; 21 | text-align: center; 22 | line-height: 1.2rem; 23 | } 24 | `; 25 | 26 | interface NoArticleProps { 27 | imageSprite: CommonType.ImageProps; 28 | content: string; 29 | description?: string; 30 | } 31 | const NoArticle = ({ imageSprite, content, description }: NoArticleProps) => { 32 | return ( 33 | 34 | 35 |

{content}

36 | {description &&

{description}

} 37 |
38 | ); 39 | }; 40 | 41 | export default NoArticle; 42 | -------------------------------------------------------------------------------- /src/components/Home/Story.tsx: -------------------------------------------------------------------------------- 1 | import StoryCircle from "components/Common/StoryCircle"; 2 | import styled from "styled-components"; 3 | 4 | const ListLayout = styled.li` 5 | min-width: 80px; 6 | height: 100%; 7 | padding: 0 4px; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: space-between; 12 | cursor: pointer; 13 | span { 14 | margin-top: 2px; 15 | font-size: 12px; 16 | line-height: 14px; 17 | width: 100%; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | white-space: nowrap; 21 | text-align: center; 22 | } 23 | `; 24 | 25 | interface StoryProps { 26 | src: string; 27 | username: string; 28 | } 29 | 30 | const Story = ({ src, username }: StoryProps) => { 31 | return ( 32 | 33 | 39 | {username} 40 | 41 | ); 42 | }; 43 | 44 | export default Story; 45 | -------------------------------------------------------------------------------- /src/components/Auth/Suggest.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from "app/store/Hooks"; 2 | import { Link } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | 5 | const SentenceContainer = styled.div` 6 | font-size: 14px; 7 | line-height: 18px; 8 | margin: -3px 0 -4px; 9 | text-align: center; 10 | 11 | p { 12 | margin: 15px; 13 | a { 14 | text-decoration: none; 15 | font-weight: 600; 16 | color: ${(props) => props.theme.color.blue}; 17 | } 18 | } 19 | `; 20 | 21 | const routerMessageState = { 22 | signin: ["계정이 없으신가요? ", "가입하기", "emailsignup"], 23 | emailsignup: ["계정이 있으신가요? ", "로그인", "login"], 24 | }; 25 | 26 | export default function Suggest() { 27 | const formState = useAppSelector((state) => state.auth.currentFormState); 28 | const [question, suggest, linkRouter] = 29 | routerMessageState[formState === "signIn" ? "signin" : "emailsignup"]; 30 | 31 | return ( 32 | 33 |

34 | {question} 35 | {suggest} 36 |

37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/hooks/useCopy.ts: -------------------------------------------------------------------------------- 1 | import { homeActions } from "app/store/ducks/home/homeSlice"; 2 | import { modalActions } from "app/store/ducks/modal/modalSlice"; 3 | import { useAppDispatch } from "app/store/Hooks"; 4 | 5 | const useCopy = (copyValue: string) => { 6 | const dispatch = useAppDispatch(); 7 | return () => { 8 | if (!document.queryCommandSupported("copy")) { 9 | return alert("복사하기가 지원되지 않는 브라우저입니다."); 10 | } 11 | const textarea = document.createElement("textarea"); 12 | textarea.value = copyValue; 13 | textarea.style.position = "fixed"; 14 | textarea.style.top = "0"; 15 | textarea.style.left = "0"; 16 | // 포지션을 주지 않으면 복사 시 스크롤이 하단으로 이동 17 | 18 | document.body.appendChild(textarea); 19 | textarea.focus(); 20 | textarea.select(); 21 | document.execCommand("copy"); 22 | document.body.removeChild(textarea); 23 | dispatch(modalActions.resetModal()); 24 | dispatch(homeActions.notificateIsCopied()); 25 | setTimeout( 26 | () => dispatch(homeActions.closeIsCopiedNotification()), 27 | 8500, 28 | ); 29 | }; 30 | }; 31 | 32 | export default useCopy; 33 | -------------------------------------------------------------------------------- /src/hooks/useOutsideClick.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | const useOutsideClick = ( 4 | ref: React.RefObject, 5 | setter: Function, 6 | trigger?: React.RefObject, 7 | ) => { 8 | useEffect(() => { 9 | const handleClickOutside = (event: MouseEvent) => { 10 | if (trigger?.current.contains(event.target as Node)) { 11 | // event 발생시킨 요소가 trigger에 속한다면, 외부에 클릭한 거로 여기지 않음 12 | // 즉, event는 외부클릭 | trigger 클릭으로 나눌 수 있음 13 | // 여기서는 event 외부클릭을 처리하고, 14 | // trigger클릭은 각 컴포넌트에서 onClick으로 처리하기위해, return;으로 종료함 15 | return; 16 | } 17 | if (ref.current && !ref.current.contains(event.target as Node)) { 18 | // 상단바에서 Link 의 클릭을 우선시 하기 위해 처리 19 | setTimeout(() => { 20 | setter(false); 21 | }, 0); 22 | } 23 | }; 24 | document.addEventListener("mousedown", handleClickOutside); 25 | 26 | return () => { 27 | document.removeEventListener("mousedown", handleClickOutside); 28 | }; 29 | }, [ref, setter]); 30 | }; 31 | 32 | export default useOutsideClick; 33 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/NewChatModal/NewChatInviteButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { useAppSelector } from "app/store/Hooks"; 4 | import CloseSVG from "assets/Svgs/CloseSVG"; 5 | 6 | const NewChatInviteButtonContainer = styled.div` 7 | display: flex; 8 | align-items: center; 9 | padding: 8px 12px; 10 | background-color: #e0f1ff; 11 | border-radius: 4px; 12 | 13 | button { 14 | color: #0095f6; 15 | font-weight: 400; 16 | font-size: 14px; 17 | line-height: 18px; 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | & > button { 23 | margin-right: 5px; 24 | } 25 | `; 26 | 27 | interface NewChatInviteButtonProps { 28 | username: string; 29 | } 30 | 31 | const NewChatInviteButton = ({ username }: NewChatInviteButtonProps) => { 32 | return ( 33 | 34 | 35 | 38 | 39 | ); 40 | }; 41 | 42 | export default NewChatInviteButton; 43 | -------------------------------------------------------------------------------- /src/styles/globalStyles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | import reset from "styled-reset"; 3 | 4 | const GlobalStlyes = createGlobalStyle` 5 | ${reset} 6 | 7 | * { 8 | box-sizing: border-box; 9 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif !important; 10 | box-sizing:border-box; 11 | } 12 | 13 | body, input,textarea ,button { 14 | font-size: 14px; 15 | line-height: 18px; 16 | padding:0; 17 | margin:0; 18 | } 19 | 20 | body,input,textarea { 21 | color:${(props) => props.theme.font.default_black}; 22 | background-color: ${(props) => props.theme.color.bg_gray}; 23 | } 24 | a { 25 | color:${(props) => props.theme.font.default_black}; 26 | } 27 | 28 | input { 29 | border: 1px solid ${(props) => props.theme.color.bd_gray} 30 | } 31 | 32 | input:focus { 33 | outline:none; 34 | } 35 | button { 36 | border:none; 37 | background-color:inherit; 38 | cursor:pointer; 39 | font-weight: ${(props) => props.theme.font.bold} 40 | } 41 | `; 42 | 43 | export default GlobalStlyes; 44 | -------------------------------------------------------------------------------- /src/assets/Svgs/CloseSVG.tsx: -------------------------------------------------------------------------------- 1 | interface CloseSVGProps { 2 | color: string; 3 | size: string; 4 | onClick?: () => void; 5 | } 6 | 7 | const CloseSVG = ({ color, size, onClick }: CloseSVGProps) => { 8 | return ( 9 | 19 | 27 | 38 | 39 | ); 40 | }; 41 | 42 | export default CloseSVG; 43 | -------------------------------------------------------------------------------- /src/components/Home/HomeSection.tsx: -------------------------------------------------------------------------------- 1 | import { getHomeArticles } from "app/store/ducks/home/homThunk"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import Article from "components/Common/Article"; 4 | import ExtraLoadingCircle from "components/Home/ExtraLoadingCircle"; 5 | import { useEffect } from "react"; 6 | 7 | const HomeSection = () => { 8 | const { articles, isLoading, isExtraArticleLoading } = useAppSelector( 9 | (state) => state.home, 10 | ); 11 | const dispatch = useAppDispatch(); 12 | 13 | useEffect(() => { 14 | const fetchData = async () => { 15 | await dispatch(getHomeArticles()); 16 | }; 17 | fetchData(); 18 | }, [dispatch]); 19 | 20 | return ( 21 |
22 | {isLoading || 23 | articles.map((article, index) => ( 24 |
29 | ))} 30 | {isExtraArticleLoading && } 31 |
32 | ); 33 | }; 34 | 35 | export default HomeSection; 36 | -------------------------------------------------------------------------------- /src/pages/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import HomeAside from "components/Home/HomeAside"; 3 | import HomeStories from "components/Home/HomeStories"; 4 | import HomeSection from "components/Home/HomeSection"; 5 | import { useEffect } from "react"; 6 | import { useAppDispatch } from "app/store/Hooks"; 7 | import { modalActions } from "app/store/ducks/modal/modalSlice"; 8 | 9 | const Layout = styled.div` 10 | padding-top: 30px; 11 | width: 100%; 12 | max-width: 935px; 13 | margin: 0 auto; 14 | display: flex; 15 | main { 16 | width: 100%; 17 | max-width: 614px; 18 | margin-right: 28px; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | @media (max-width: 1000px) { 23 | max-width: 600px; 24 | main { 25 | margin-right: 0; 26 | } 27 | } 28 | `; 29 | 30 | const Home = () => { 31 | const dispatch = useAppDispatch(); 32 | useEffect(() => { 33 | return () => { 34 | dispatch(modalActions.resetModal()); 35 | }; 36 | }, [dispatch]); 37 | 38 | return ( 39 | 40 |
41 | {/* */} 42 | 43 |
44 | 45 |
46 | ); 47 | }; 48 | 49 | export default Home; 50 | -------------------------------------------------------------------------------- /src/pages/Landing/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import ShowingInstagram from "components/Auth/InstagramImageSlider"; 3 | import { useAppSelector } from "app/store/Hooks"; 4 | import Form from "components/Auth/Form"; 5 | 6 | const Container = styled.section` 7 | display: flex; 8 | flex-direction: column; 9 | min-height: 100vh; 10 | 11 | .container { 12 | display: flex; 13 | justify-content: center; 14 | padding-bottom: 32px; 15 | margin: 32px auto 0; 16 | width: 100%; 17 | max-width: 935px; 18 | flex-grow: 1; 19 | } 20 | 21 | .form { 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: center; 25 | margin-top: 12px; 26 | max-width: 350px; 27 | flex-grow: 1; 28 | } 29 | `; 30 | 31 | export default function Landing() { 32 | const formState = useAppSelector((state) => state.auth.currentFormState); 33 | 34 | return ( 35 | 36 |
37 | 38 |
39 |
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/NewChatModal/NewChatModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import NewChatModalTitle from "./NewChatModalTitle"; 4 | import NewChatSearchBar from "./NewChatSearchBar"; 5 | import NewChatFriendList from "./NewChatFriendList"; 6 | import ModalCard from "styles/UI/ModalCard"; 7 | import { useAppDispatch } from "app/store/Hooks"; 8 | import { 9 | closeModal, 10 | openModal, 11 | resetSelectNewChatUser, 12 | } from "app/store/ducks/direct/DirectSlice"; 13 | 14 | const NewChatModalContainer = styled.div` 15 | padding-top: 30px; 16 | `; 17 | 18 | const NewChatModal = () => { 19 | const dispatch = useAppDispatch(); 20 | 21 | useEffect(() => { 22 | dispatch(resetSelectNewChatUser()); 23 | }, [dispatch]); 24 | 25 | return ( 26 | { 29 | dispatch(openModal("newChat")); 30 | }} 31 | onModalOff={() => { 32 | dispatch(closeModal()); 33 | }} 34 | > 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default NewChatModal; 45 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { authAction } from "app/store/ducks/auth/authSlice"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import { customAxios, setAccessTokenInAxiosHeaders } from "customAxios"; 4 | import InstagramLoading from "InstagramLoading"; 5 | import { useEffect } from "react"; 6 | import Routes from "Routes"; 7 | 8 | function App() { 9 | const isRefreshTokenChecking = useAppSelector( 10 | (state) => state.auth.isRefreshTokenChecking, 11 | ); 12 | const dispatch = useAppDispatch(); 13 | useEffect(() => { 14 | const reIssueToken = async () => { 15 | try { 16 | const { 17 | data: { data }, 18 | }: { 19 | data: AuthType.TokenResponse; 20 | } = await customAxios.post(`/reissue`); 21 | if (data) { 22 | setAccessTokenInAxiosHeaders(data); 23 | dispatch(authAction.login()); 24 | } 25 | // - refresh token: 없음 | 만료됨 -> 401에러 -> catch로 넘어감 26 | } finally { 27 | dispatch(authAction.finishRefreshTokenChecking()); 28 | } 29 | }; 30 | reIssueToken(); 31 | }, [dispatch]); 32 | 33 | return ( 34 |
35 | {isRefreshTokenChecking ? : } 36 |
37 | ); 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/DeleteChatModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import ModalTitleContent from "components/Common/Modal/ModalContent/ModalTitleContent"; 4 | import ModalButtonContent from "components/Common/Modal/ModalContent/ModalButtonContent"; 5 | import ModalCard from "styles/UI/ModalCard"; 6 | import { closeModal, openModal } from "app/store/ducks/direct/DirectSlice"; 7 | import { useAppDispatch } from "app/store/Hooks"; 8 | 9 | const DeleteChatModalContainer = styled.div` 10 | padding-top: 20px; 11 | `; 12 | 13 | const DeleteChatModal = () => { 14 | const dispatch = useAppDispatch(); 15 | return ( 16 | { 19 | dispatch(openModal("deleteChat")); 20 | }} 21 | onModalOff={() => { 22 | dispatch(closeModal()); 23 | }} 24 | > 25 | 26 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default DeleteChatModal; 39 | -------------------------------------------------------------------------------- /src/assets/Svgs/instagram-logo-loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import { authReducer } from "app/store/ducks/auth/authSlice"; 3 | import { homeReducer } from "app/store/ducks/home/homeSlice"; 4 | import { modalReducer } from "app/store/ducks/modal/modalSlice"; 5 | import { directReducer } from "app/store/ducks/direct/DirectSlice"; 6 | import { uploadReducer } from "app/store/ducks/upload/uploadSlice"; 7 | import { profileReducer } from "app/store/ducks/profile/profileSlice"; 8 | import { editReducer } from "./ducks/edit/editSlice"; 9 | import { commonReducer } from "./ducks/common/commonSlice"; 10 | import { paragraphReducer } from "app/store/ducks/paragraph/paragraphSlice"; 11 | 12 | export const store = configureStore({ 13 | reducer: { 14 | direct: directReducer, 15 | auth: authReducer, 16 | home: homeReducer, 17 | paragraph: paragraphReducer, 18 | modal: modalReducer, 19 | upload: uploadReducer, 20 | profile: profileReducer, 21 | edit: editReducer, 22 | common: commonReducer, 23 | }, 24 | middleware: (getDefaultMiddleware) => 25 | getDefaultMiddleware({ 26 | serializableCheck: false, 27 | }), 28 | }); 29 | 30 | // Infer the `RootState` and `AppDispatch` types from the store itself 31 | export type RootState = ReturnType; 32 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 33 | export type AppDispatch = typeof store.dispatch; 34 | -------------------------------------------------------------------------------- /src/components/Auth/SignUpForm/validator.ts: -------------------------------------------------------------------------------- 1 | const lengthScopeValidator = ( 2 | value: string, 3 | minLength: number, 4 | maxLength: number, 5 | ): boolean => { 6 | const Length = value.length; 7 | return Length >= minLength && Length <= maxLength; 8 | }; 9 | 10 | const hasSpecialChar = (value: string) => { 11 | const specialChar = /[^0-9a-zA-Z]/g; 12 | return specialChar.test(value); 13 | }; 14 | 15 | export const emailFormValidator = (email: string): boolean => { 16 | const emailFormat = 17 | /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i; 18 | return emailFormat.test(email); 19 | }; 20 | 21 | export const nameValidator = (name: string): boolean => { 22 | return lengthScopeValidator(name, 2, 12); 23 | }; 24 | 25 | export const usernameValidator = (username: string): boolean => { 26 | if (!lengthScopeValidator(username, 4, 12)) { 27 | return false; 28 | } 29 | 30 | return !hasSpecialChar(username); 31 | }; 32 | 33 | export const passwordValidator = (password: string): boolean => { 34 | if (!lengthScopeValidator(password, 8, 20)) { 35 | return false; 36 | } 37 | 38 | if (hasSpecialChar(password)) { 39 | return false; 40 | } 41 | 42 | // 숫자,영어 최소 1개 있는지 체크하는 정규식 43 | // 참고) https://stackoverflow.com/questions/7684815/regex-pattern-to-match-at-least-1-number-and-1-character-in-a-string 44 | return /(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)/.test(password); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Auth/FacebookLogin.tsx: -------------------------------------------------------------------------------- 1 | import ImageSprite from "components/Common/ImageSprite"; 2 | import styled from "styled-components"; 3 | import Button from "styles/UI/Button/Button"; 4 | import sprite from "assets/Images/loginPageSprite.png"; 5 | 6 | const FacebookButtonContainer = styled.div` 7 | margin: 8px 40px; 8 | 9 | & > button { 10 | width: 100%; 11 | border: 1px solid transparent; 12 | 13 | & > div { 14 | display: inline-block; 15 | margin-right: 8px; 16 | position: relative; 17 | top: 3px; 18 | } 19 | } 20 | `; 21 | 22 | const backgroundWhiteFacebook: CommonType.ImageProps = { 23 | width: 16, 24 | height: 16, 25 | position: `-414px -259px`, 26 | url: sprite, 27 | }; 28 | 29 | const backgroundBlueFacebook: CommonType.ImageProps = { 30 | width: 16, 31 | height: 16, 32 | position: `-414px -300px`, 33 | url: sprite, 34 | }; 35 | 36 | export default function FacebookLogin({ bgColor, color }: UIType.ButtonProps) { 37 | return ( 38 | 39 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Edit/Menus/Activity.tsx: -------------------------------------------------------------------------------- 1 | import { getLoginDevice } from "app/store/ducks/auth/authThunk"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import DeviceItem from "components/Edit/Menus/deviceItem"; 4 | import { useEffect } from "react"; 5 | import styled from "styled-components"; 6 | 7 | const Container = styled.article` 8 | margin: 1rem 4rem; 9 | 10 | .title { 11 | font-size: 1.6em; 12 | padding: 1.5rem 0 1rem 0; 13 | font-weight: 300; 14 | } 15 | 16 | .sub-title { 17 | font-size: 1rem; 18 | padding: 1rem 0; 19 | font-weight: 600; 20 | } 21 | `; 22 | 23 | const Activity = () => { 24 | const dispatch = useAppDispatch(); 25 | const { loginDeviceList } = useAppSelector((state) => state.auth); 26 | 27 | useEffect(() => { 28 | dispatch(getLoginDevice()); 29 | }, [dispatch]); 30 | 31 | return ( 32 | 33 |
로그인 활동
34 |
로그인한 위치
35 | {loginDeviceList ? ( 36 | loginDeviceList.map((user, index) => ( 37 | 42 | )) 43 | ) : ( 44 |
로그인 기록을 확인할 수 없습니다.
45 | )} 46 |
47 | ); 48 | }; 49 | 50 | export default Activity; 51 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleAlone/ArticleAlone.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from "app/store/Hooks"; 2 | import Article from "components/Common/Article/Article"; 3 | import LargerArticle from "components/Common/Article/ArticleAlone/LargerArticle"; 4 | import { useEffect, useState } from "react"; 5 | import styled from "styled-components"; 6 | 7 | const StyledArticleAlone = styled.div` 8 | width: 100%; 9 | `; 10 | 11 | interface ArticleAloneProps { 12 | isModal?: boolean; 13 | } 14 | 15 | const ArticleAlone = ({ isModal = false }: ArticleAloneProps) => { 16 | const article = useAppSelector(({ paragraph }) => paragraph.articleObj); 17 | const [isMobileWidth, setIsMobileWidth] = useState( 18 | window.innerWidth <= 735, 19 | ); 20 | 21 | useEffect(() => { 22 | const resizeEventHandler = () => 23 | setIsMobileWidth(window.innerWidth <= 735); 24 | window.addEventListener("resize", resizeEventHandler); 25 | 26 | return () => { 27 | window.removeEventListener("resize", resizeEventHandler); 28 | }; 29 | }, []); 30 | 31 | return ( 32 | 33 | {isMobileWidth ? ( 34 |
39 | ) : ( 40 | 41 | )} 42 | 43 | ); 44 | }; 45 | 46 | export default ArticleAlone; 47 | -------------------------------------------------------------------------------- /src/pages/Edit/Edit.tsx: -------------------------------------------------------------------------------- 1 | import Aside from "components/Edit/Aside/Aside"; 2 | import Section from "components/Edit/Section/Section"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | import theme from "styles/theme"; 6 | 7 | const Layout = styled.div` 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | 12 | padding: 20px; 13 | 14 | @media (max-width: 935px) { 15 | padding: 0; 16 | } 17 | .container { 18 | display: flex; 19 | 20 | width: 100%; 21 | max-width: 935px; 22 | background-color: #fff; 23 | 24 | aside { 25 | width: 250px; 26 | position: relative; 27 | } 28 | 29 | section { 30 | flex: 1 1 auto; 31 | } 32 | 33 | @media (max-width: 935px) { 34 | aside { 35 | width: 200px; 36 | } 37 | 38 | section { 39 | } 40 | } 41 | } 42 | `; 43 | 44 | const Edit = () => { 45 | const borderStyle = `1px solid ${theme.color.bd_gray}`; 46 | 47 | return ( 48 | 49 |
50 |
57 |
58 | ); 59 | }; 60 | 61 | export default Edit; 62 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/NewChatModal/NewChatFriendList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import styled from "styled-components"; 3 | // import { dummyChatList } from "components/Direct/Aside/AsideBody"; 4 | import NewChatRecommendUser from "./NewChatRecommendUser"; 5 | import { useAppSelector } from "../../../../../app/store/Hooks"; 6 | 7 | const NewChatFriendListContainer = styled.div` 8 | height: 350px; 9 | overflow-y: auto; 10 | 11 | & > span { 12 | display: block; 13 | font-weight: 600; 14 | font-size: 14px; 15 | line-height: 18px; 16 | margin: 12px 16px; 17 | } 18 | .new-chat-recommend-container { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | @media (max-width: 736px) { 24 | height: 80vh; 25 | } 26 | `; 27 | 28 | const NewChatFriendList = () => { 29 | const searchUsers = useAppSelector((state) => state.common.searchUsers); 30 | 31 | return ( 32 | 33 | 추천 34 | 35 |
36 | {searchUsers.map( 37 | (user) => 38 | user.member && ( 39 | 43 | ), 44 | )} 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default NewChatFriendList; 51 | -------------------------------------------------------------------------------- /src/assets/Svgs/imgOrVideo.svg: -------------------------------------------------------------------------------- 1 | 10 | 14 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /src/app/store/ducks/Article/articleThunk.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit"; 2 | import { authAction } from "app/store/ducks/auth/authSlice"; 3 | import { authorizedCustomAxios } from "customAxios"; 4 | import { FAIL_TO_REISSUE_MESSAGE } from "utils/constant"; 5 | 6 | // 각 게시물에서 필요한 곳에서 가져와 dispatch하면 되고, 7 | // unwrap() 처리하여 state 및 error 핸들링하면 됩니다. 8 | export const postSaveArticle = createAsyncThunk< 9 | { status: boolean }, 10 | { 11 | postId: number; 12 | } 13 | >("article/postSaveArticle", async (payload, ThunkOptions) => { 14 | const config = { 15 | params: { postId: payload.postId }, 16 | }; 17 | try { 18 | const { 19 | data: { data }, // status 20 | } = await authorizedCustomAxios.post(`/posts/save`, null, config); 21 | return data; 22 | } catch (error) { 23 | error === FAIL_TO_REISSUE_MESSAGE && 24 | ThunkOptions.dispatch(authAction.logout()); 25 | throw ThunkOptions.rejectWithValue(error); 26 | } 27 | }); 28 | 29 | export const deleteSaveArticle = createAsyncThunk< 30 | { status: boolean }, 31 | { 32 | postId: number; 33 | } 34 | >("article/deleteSaveArticle", async (payload, ThunkOptions) => { 35 | const config = { 36 | params: { postId: payload.postId }, 37 | }; 38 | try { 39 | const { 40 | data: { data }, // status 41 | } = await authorizedCustomAxios.delete(`/posts/save`, config); 42 | return data; 43 | } catch (error) { 44 | error === FAIL_TO_REISSUE_MESSAGE && 45 | ThunkOptions.dispatch(authAction.logout()); 46 | throw ThunkOptions.rejectWithValue(error); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/Auth/SignUpForm/SignUpFormContent.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import FacebookLogin from "components/Auth/FacebookLogin"; 3 | import Line from "components/Auth/Line"; 4 | import InputAndButton from "./InputAndButton"; 5 | import ImageSprite from "components/Common/ImageSprite"; 6 | import sprite from "assets/Images/loginPageSprite.png"; 7 | 8 | const SignUpFormContainer = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | .logo { 13 | margin: 22px auto 12px; 14 | } 15 | 16 | .signUpForm { 17 | display: flex; 18 | flex-direction: column; 19 | margin-bottom: 20px; 20 | 21 | .signUpMessage { 22 | font-size: 17px; 23 | margin: 0 40px 10px; 24 | color: ${(props) => props.theme.font.gray}; 25 | font-weight: ${(props) => props.theme.font.bold}; 26 | line-height: 20px; 27 | text-align: center; 28 | } 29 | } 30 | `; 31 | 32 | const instagramImage: CommonType.ImageProps = { 33 | width: 175, 34 | height: 51, 35 | position: `0 -130px`, 36 | url: sprite, 37 | }; 38 | 39 | export default function SignUpForm() { 40 | return ( 41 | 42 | 43 | 44 |

45 | 친구들의 사진과 동영상을 보려면 가입하세요. 46 |

47 | 48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/store/ducks/common/commonThunk.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit"; 2 | import { RootState } from "app/store/store"; 3 | import { authorizedCustomAxios } from "customAxios"; 4 | import { FAIL_TO_REISSUE_MESSAGE } from "utils/constant"; 5 | import { authAction } from "../auth/authSlice"; 6 | 7 | export const searchUser = createAsyncThunk< 8 | CommonType.searchResultType[], 9 | { 10 | keyword: string; 11 | }, 12 | { state: RootState } 13 | >( 14 | "common/searchUser", 15 | async (payload, { getState, dispatch, rejectWithValue }) => { 16 | const config = { 17 | params: { text: payload.keyword }, 18 | }; 19 | try { 20 | const { data } = await authorizedCustomAxios.get( 21 | `/topsearch`, 22 | config, 23 | ); 24 | 25 | return data.data; 26 | } catch (error) { 27 | error === FAIL_TO_REISSUE_MESSAGE && dispatch(authAction.logout()); 28 | throw rejectWithValue(error); 29 | } 30 | }, 31 | ); 32 | 33 | export const getSearchRecord = createAsyncThunk< 34 | CommonType.searchResultType[], 35 | void, 36 | { state: RootState } 37 | >( 38 | "common/getSearchRecord", 39 | async (payload, { getState, dispatch, rejectWithValue }) => { 40 | try { 41 | const { data } = await authorizedCustomAxios.get( 42 | "/topsearch/recent/top", 43 | ); 44 | 45 | return data.data.content; 46 | } catch (error) { 47 | error === FAIL_TO_REISSUE_MESSAGE && dispatch(authAction.logout()); 48 | throw rejectWithValue(error); 49 | } 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /src/components/Common/Modal/BlockModal.tsx: -------------------------------------------------------------------------------- 1 | import ModalButtonContent from "components/Common/Modal/ModalContent/ModalButtonContent"; 2 | import ModalTitleContent from "components/Common/Modal/ModalContent/ModalTitleContent"; 3 | import { authorizedCustomAxios } from "customAxios"; 4 | import React from "react"; 5 | import styled from "styled-components"; 6 | import ModalCard from "styles/UI/ModalCard"; 7 | 8 | const BlockModalInner = styled.div` 9 | padding-top: 20px; 10 | `; 11 | 12 | interface BlockModalProps { 13 | onModalOn: () => void; 14 | onModalOff: () => void; 15 | blockMemberUsername: string; 16 | } 17 | 18 | const BlockModal = ({ 19 | onModalOn, 20 | onModalOff, 21 | blockMemberUsername, 22 | }: BlockModalProps) => { 23 | const blockHandler = async () => { 24 | await authorizedCustomAxios(`/${blockMemberUsername}/block`); 25 | }; 26 | return ( 27 | 32 | 33 | 39 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default BlockModal; 50 | -------------------------------------------------------------------------------- /src/styles/UI/Notification/Notification.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import styled from "styled-components"; 3 | 4 | const StyledNotification = styled.div` 5 | position: fixed; 6 | bottom: -100%; 7 | left: 0; 8 | width: 100%; 9 | @-webkit-keyframes wait5 { 10 | 0% { 11 | bottom: -100%; 12 | } 13 | 15% { 14 | bottom: 0; 15 | } 16 | 70% { 17 | bottom: 0; 18 | } 19 | 100% { 20 | bottom: -100%; 21 | } 22 | } 23 | @keyframes wait5 { 24 | 0% { 25 | bottom: -100%; 26 | } 27 | 15% { 28 | bottom: 0; 29 | } 30 | 70% { 31 | bottom: 0; 32 | } 33 | 100% { 34 | bottom: -100%; 35 | } 36 | } 37 | animation: wait5 8.5s; 38 | 39 | background-color: ${(props) => props.theme.font.default_black}; 40 | min-height: 44px; 41 | padding: 0 16px; 42 | & > p { 43 | color: white; 44 | max-height: 72px; 45 | overflow: hidden; 46 | padding: 12px 0; 47 | } 48 | `; 49 | 50 | interface NotificationProps { 51 | text: string; 52 | reset?: Function; 53 | } 54 | 55 | const Notification = ({ text, reset }: NotificationProps) => { 56 | // after 8.5s unfold notification 57 | setTimeout(() => { 58 | reset && reset(); 59 | }, 8500); 60 | 61 | const notificationRoot = ReactDOM.createPortal( 62 | 63 |

{text}

64 |
, 65 | document.getElementById("notification-root")!, 66 | ); 67 | return notificationRoot; 68 | }; 69 | 70 | export default Notification; 71 | -------------------------------------------------------------------------------- /src/components/Profile/Modals/SettingModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ModalCard from "styles/UI/ModalCard"; 3 | import styled from "styled-components"; 4 | import { useAppDispatch } from "app/store/Hooks"; 5 | import { logout } from "app/store/ducks/auth/authThunk"; 6 | 7 | const SettingModalInner = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | 12 | & > div { 13 | width: 100%; 14 | flex: 1; 15 | height: 48px; 16 | line-height: 48px; 17 | text-align: center; 18 | cursor: pointer; 19 | } 20 | 21 | & > div:not(:first-child) { 22 | border-top: ${(props) => props.theme.color.bd_gray} 1px solid; 23 | } 24 | `; 25 | 26 | interface SettingModalProps { 27 | onModalOn: () => void; 28 | onModalOff: () => void; 29 | } 30 | 31 | const SettingModal = ({ onModalOn, onModalOff }: SettingModalProps) => { 32 | const dispatch = useAppDispatch(); 33 | return ( 34 | 39 | 40 |
비밀번호 변경
41 |
네임 태그
42 |
앱 및 웹사이트
43 |
알림
44 |
개인정보 및 보안
45 |
로그인 활동
46 |
Instagram에서 보낸 이메일
47 |
문제 신고
48 |
dispatch(logout())}>로그아웃
49 |
취소
50 |
51 |
52 | ); 53 | }; 54 | 55 | export default SettingModal; 56 | -------------------------------------------------------------------------------- /src/components/Edit/Aside/Aside.tsx: -------------------------------------------------------------------------------- 1 | import { selectMenu } from "app/store/ducks/edit/editSlice"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | 6 | const menus: EditType.menuType[] = [ 7 | "프로필 편집", 8 | "비밀번호 변경", 9 | "앱 및 웹사이트", 10 | "이메일 및 SMS", 11 | "푸시 알림", 12 | "연락처 관리", 13 | "개인정보 및 보안", 14 | "로그인 활동", 15 | "Instagram에서 보낸 이메일", 16 | "도움말", 17 | ]; 18 | const Container = styled.div` 19 | display: flex; 20 | flex-direction: column; 21 | 22 | ul { 23 | } 24 | `; 25 | 26 | // selected 라는 props 를 넘겨주기 위해 별도로 선언합니다. 27 | const Menu = styled.li<{ selected: boolean }>` 28 | padding: 16px 16px 16px 30px; 29 | cursor: pointer; 30 | color: #262626; 31 | font-size: 16px; 32 | line-height: 20px; 33 | font-weight: ${({ selected }) => selected && "600"}; 34 | border-left: ${({ selected }) => selected && "2px solid #262626"}; 35 | `; 36 | 37 | const Aside = () => { 38 | const dispatch = useAppDispatch(); 39 | const currentMenu = useAppSelector((state) => state.edit.currentMenu); 40 | return ( 41 | 42 |
    43 | {menus.map((menu) => ( 44 | { 48 | dispatch(selectMenu(menu)); 49 | }} 50 | > 51 | {menu} 52 | 53 | ))} 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default Aside; 60 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/CommonDirectModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import ModalTitleContent from "components/Common/Modal/ModalContent/ModalTitleContent"; 4 | import ModalButtonContent from "components/Common/Modal/ModalContent/ModalButtonContent"; 5 | import ModalCard from "styles/UI/ModalCard"; 6 | import { 7 | closeModal, 8 | openModal, 9 | selectView, 10 | } from "app/store/ducks/direct/DirectSlice"; 11 | import { useAppDispatch } from "app/store/Hooks"; 12 | 13 | const CommonDirectModalContainer = styled.div` 14 | padding-top: 20px; 15 | `; 16 | 17 | interface CommonDirectModalProps { 18 | modalType: Direct.modalType; 19 | title: string; 20 | description: string; 21 | actionName: string; 22 | actionHandler?: () => void; // message 삭제에서 사용 23 | } 24 | 25 | const CommonDirectModal = ({ 26 | modalType, 27 | title, 28 | description, 29 | actionName, 30 | actionHandler, 31 | }: CommonDirectModalProps) => { 32 | const dispatch = useAppDispatch(); 33 | 34 | return ( 35 | { 38 | dispatch(openModal(modalType)); 39 | }} 40 | onModalOff={() => { 41 | dispatch(closeModal()); 42 | }} 43 | > 44 | 45 | 46 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default CommonDirectModal; 56 | -------------------------------------------------------------------------------- /src/app/store/ducks/modal/modalThunk.ts: -------------------------------------------------------------------------------- 1 | import { modalActions } from "app/store/ducks/modal/modalSlice"; 2 | import { RootState } from "app/store/store"; 3 | import { createAsyncThunk } from "@reduxjs/toolkit"; 4 | import { authAction } from "app/store/ducks/auth/authSlice"; 5 | import { authorizedCustomAxios } from "customAxios"; 6 | import { FAIL_TO_REISSUE_MESSAGE } from "utils/constant"; 7 | 8 | interface GetMiniProfileResponseType extends AxiosType.ResponseType { 9 | data: ModalType.MiniProfileProps; 10 | } 11 | 12 | export const getMiniProfile = createAsyncThunk< 13 | ModalType.MiniProfileStateProps, 14 | { 15 | memberUsername: string; 16 | modalPosition: ModalType.ModalPositionProps; 17 | }, 18 | { state: RootState } 19 | >( 20 | "modal/getMiniProfile", 21 | async (payload, { getState, dispatch, rejectWithValue }) => { 22 | const currentMinProfileState = getState().modal.miniProfile; 23 | if (currentMinProfileState?.memberUsername === payload.memberUsername) 24 | return { 25 | ...currentMinProfileState, 26 | isLoading: false, 27 | modalPosition: payload.modalPosition, 28 | }; 29 | try { 30 | const { 31 | data: { data }, 32 | } = await authorizedCustomAxios.get( 33 | `/accounts/${payload.memberUsername}/mini`, 34 | ); 35 | 36 | return { 37 | ...data, 38 | isLoading: false, 39 | modalPosition: payload.modalPosition, 40 | }; 41 | } catch (error) { 42 | error === FAIL_TO_REISSUE_MESSAGE && dispatch(authAction.logout()); 43 | throw rejectWithValue(error); 44 | } 45 | }, 46 | ); 47 | -------------------------------------------------------------------------------- /src/components/Auth/AppDownload.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import apple from "assets/Images/appStore.png"; 3 | import android from "assets/Images/googlePlay.png"; 4 | 5 | const DownloadStyle = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | text-align: center; 9 | & > p { 10 | margin: 10px 20px; 11 | } 12 | .appImage { 13 | display: flex; 14 | justify-content: center; 15 | margin: 10px 0; 16 | & > a { 17 | margin-right: 8px; 18 | } 19 | & > a:last-child { 20 | margin-right: 0; 21 | } 22 | & > a > img { 23 | height: 40px; 24 | } 25 | } 26 | `; 27 | 28 | const props = [ 29 | { 30 | url: "https://apps.apple.com/app/instagram/id389801252?vt=lo", 31 | src: apple, 32 | alt: "애플스토어에서 인스타그램 검색한 결과", 33 | }, 34 | { 35 | url: "https://play.google.com/store/apps/details?id=com.instagram.android&referrer=utm_source%3Dinstagramweb&utm_campaign=loginPage&ig_mid=F86341FF-8B95-459C-BC37-EC07B51F263E&utm_content=lo&utm_medium=badge", 36 | src: android, 37 | alt: "구글플레이스토어에서 인스타그램 검색한 결과", 38 | }, 39 | ]; 40 | 41 | export default function Appdownload() { 42 | return ( 43 | 44 |

앱을 다운로드하세요.

45 |
46 | {props.map((downloadData, idx) => { 47 | return ( 48 | 49 | {downloadData.alt} 53 | 54 | ); 55 | })} 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Common/StringFragmentWithMentionOrHashtagLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | 5 | const StyledMentionOrHashtagLink = styled(Link)` 6 | color: ${(props) => props.theme.font.link_blue}; 7 | text-decoration: none; 8 | `; 9 | 10 | interface StringFragmentWithMentionOrHashtagLinkProps { 11 | str: string; 12 | mentions: string[]; 13 | hashtags: string[]; 14 | } 15 | 16 | const StringFragmentWithMentionOrHashtagLink = ({ 17 | str, 18 | mentions, 19 | hashtags, 20 | }: StringFragmentWithMentionOrHashtagLinkProps) => { 21 | return ( 22 | <> 23 | {str.split(/([@#][^\s#@]+)/g).map((ele) => { 24 | for (const mention of mentions) { 25 | if ("@" + mention === ele) 26 | return ( 27 | 31 | {ele} 32 | 33 | ); 34 | } 35 | for (const hashtag of hashtags) { 36 | if ("#" + hashtag === ele) 37 | return ( 38 | 42 | {ele} 43 | 44 | ); 45 | } 46 | return ele; 47 | })} 48 | 49 | ); 50 | }; 51 | 52 | export default StringFragmentWithMentionOrHashtagLink; 53 | -------------------------------------------------------------------------------- /src/customAxios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig } from "axios"; 2 | 3 | var jwt = require(`jsonwebtoken`); 4 | 5 | export const customAxios: AxiosInstance = axios.create({ 6 | baseURL: `http://ec2-52-79-71-191.ap-northeast-2.compute.amazonaws.com:8080`, 7 | withCredentials: true, 8 | }); 9 | 10 | export const authorizedCustomAxios: AxiosInstance = axios.create({ 11 | baseURL: `http://ec2-52-79-71-191.ap-northeast-2.compute.amazonaws.com:8080`, 12 | withCredentials: true, 13 | }); 14 | 15 | export const checkToken = async (config: AxiosRequestConfig) => { 16 | const accessToken = 17 | authorizedCustomAxios.defaults.headers.common.Authorization.split( 18 | " ", 19 | )[1]; 20 | const decode = jwt.decode(accessToken); 21 | const nowDate = new Date().getTime() / 1000; 22 | if (decode.exp < nowDate) { 23 | try { 24 | const { 25 | data: { data }, 26 | }: { 27 | data: AuthType.TokenResponse; 28 | } = await customAxios.post(`/reissue`); 29 | if (data) { 30 | setAccessTokenInAxiosHeaders(data); 31 | if (config.headers) { 32 | config.headers[ 33 | `Authorization` 34 | ] = `${data.type} ${data.accessToken}`; 35 | } 36 | } 37 | } catch (error) { 38 | // 토큰재발급 실패한 경우(refreshToken 쿠키 제거 - httpOnly쿠키라, 백엔드에서만 제거가능) 39 | customAxios.post(`/logout/only/cookie`); 40 | window.location.replace("/"); // FIXME: 새로고침 => 토큰 재발급 불필요하게 호출 ** 41 | } 42 | } 43 | return config; 44 | }; 45 | 46 | export const setAccessTokenInAxiosHeaders = (token: AuthType.Token) => { 47 | authorizedCustomAxios.defaults.headers.common[ 48 | `Authorization` 49 | ] = `${token.type} ${token.accessToken}`; 50 | }; 51 | 52 | authorizedCustomAxios.interceptors.request.use(checkToken); 53 | -------------------------------------------------------------------------------- /src/components/Common/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { ReactComponent as LoadingCircle } from "assets/Svgs/loadingCircle.svg"; 2 | import styled from "styled-components"; 3 | 4 | const StyledLoading = styled.div` 5 | @-webkit-keyframes rotating { 6 | from { 7 | -webkit-transform: rotate(0deg); 8 | -o-transform: rotate(0deg); 9 | transform: rotate(0deg); 10 | } 11 | to { 12 | -webkit-transform: rotate(360deg); 13 | -o-transform: rotate(360deg); 14 | transform: rotate(360deg); 15 | } 16 | } 17 | @keyframes rotating { 18 | from { 19 | -ms-transform: rotate(0deg); 20 | -moz-transform: rotate(0deg); 21 | -webkit-transform: rotate(0deg); 22 | -o-transform: rotate(0deg); 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | -ms-transform: rotate(360deg); 27 | -moz-transform: rotate(360deg); 28 | -webkit-transform: rotate(360deg); 29 | -o-transform: rotate(360deg); 30 | transform: rotate(360deg); 31 | } 32 | } 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | & > svg { 37 | -webkit-animation: rotating 1.2s steps(12) infinite; 38 | -moz-animation: rotating 1.2s steps(12) infinite; 39 | -ms-animation: rotating 1.2s steps(12) infinite; 40 | -o-animation: rotating 1.2s steps(12) infinite; 41 | animation: rotating 1.2s steps(12) infinite; 42 | } 43 | `; 44 | 45 | interface LoadingProps { 46 | size: number; 47 | isInButton?: boolean; 48 | } 49 | 50 | const Loading = ({ size, isInButton = false }: LoadingProps) => { 51 | return ( 52 | 53 | 58 | 59 | ); 60 | }; 61 | 62 | export default Loading; 63 | -------------------------------------------------------------------------------- /src/components/Common/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import SearchBar from "./SearchBar"; 3 | import NavItems from "./NavItems"; 4 | import { NavLink } from "react-router-dom"; 5 | import { useEffect } from "react"; 6 | import { useAppDispatch } from "app/store/Hooks"; 7 | import { getUserInfo } from "app/store/ducks/auth/authThunk"; 8 | import Logo from "assets/Images/logo-hello-world.png"; 9 | 10 | const HeaderContainer = styled.nav` 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | 15 | background-color: #fff; 16 | background-color: rgba(var(--d87, 255, 255, 255), 1); 17 | border-bottom: 1px solid rgba(var(--b6a, 219, 219, 219), 1); 18 | height: 54px; 19 | position: fixed; 20 | top: 0; 21 | 22 | width: 100%; 23 | z-index: 101; 24 | `; 25 | 26 | const HeaderContentsWrapper = styled.div` 27 | display: flex; 28 | align-items: center; 29 | 30 | padding: 0 20px; 31 | width: 100%; 32 | max-width: 975px; 33 | `; 34 | 35 | const LogoWrapper = styled(NavLink)` 36 | display: inline-flex; 37 | align-items: center; 38 | flex: 1 9999 0%; 39 | min-width: 40px; 40 | 41 | img { 42 | width: 110px; 43 | } 44 | `; 45 | 46 | const FakeHeader = styled.div` 47 | height: 54px; 48 | `; 49 | 50 | const Header = () => { 51 | const dispatch = useAppDispatch(); 52 | useEffect(() => { 53 | dispatch(getUserInfo()); 54 | }, []); 55 | return ( 56 | <> 57 | 58 | 59 | 60 | hello world 로고 61 | 62 | {/* */} 63 | 64 | {/* */} 65 | 66 | 67 | 68 | 69 | 70 | ); 71 | }; 72 | 73 | export default Header; 74 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/WarningMaxUploadNumberModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import Button from "styles/UI/Button"; 4 | import ModalCard from "styles/UI/ModalCard"; 5 | 6 | const StyledWarningMaxUploadNumberModalInner = styled.div` 7 | padding: 24px; 8 | height: 200px; 9 | display: flex; 10 | position: relative; 11 | justify-content: center; 12 | align-items: center; 13 | & > h1 { 14 | width: 100%; 15 | height: 42px; 16 | line-height: 42px; 17 | font-size: 16px; 18 | font-weight: ${(props) => props.theme.font.bold}; 19 | text-align: center; 20 | position: absolute; 21 | top: 0; 22 | border-bottom: 1px solid ${(props) => props.theme.color.bd_gray}; 23 | } 24 | & > h2 { 25 | font-size: 18px; 26 | } 27 | & > button { 28 | position: absolute; 29 | bottom: 14px; 30 | right: 14px; 31 | width: 20%; 32 | min-width: 80px; 33 | } 34 | `; 35 | 36 | interface WarningMaxUploadNumberModalProps { 37 | onModalOn: () => void; 38 | onModalOff: () => void; 39 | warnigContent: "images" | "tags"; 40 | } 41 | 42 | const WarningMaxUploadNumberModal = ({ 43 | onModalOn, 44 | onModalOff, 45 | warnigContent, 46 | }: WarningMaxUploadNumberModalProps) => { 47 | return ( 48 | 53 | 54 |

주의

55 |

56 | {warnigContent === "images" 57 | ? "사진 업로드는 최대 10개까지만 가능합니다" 58 | : "각 이미지에 태그는 최대 20개까지만 가능합니다"} 59 |

60 | 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default WarningMaxUploadNumberModal; 67 | -------------------------------------------------------------------------------- /src/pages/Auth/index.tsx: -------------------------------------------------------------------------------- 1 | import { authAction } from "app/store/ducks/auth/authSlice"; 2 | import { useAppDispatch } from "app/store/Hooks"; 3 | import Form from "components/Auth/Form"; 4 | import { useEffect } from "react"; 5 | import { useHistory, useLocation } from "react-router-dom"; 6 | import styled from "styled-components"; 7 | import queryString from "query-string"; 8 | import { signInUseCode } from "app/store/ducks/auth/authThunk"; 9 | import InstagramLoading from "InstagramLoading"; 10 | 11 | const Section = styled.section` 12 | flex-shrink: 0; 13 | min-height: 100vh; 14 | overflow: hidden; 15 | 16 | .form-container { 17 | flex-shrink: 0; 18 | display: flex; 19 | justify-content: center; 20 | } 21 | `; 22 | 23 | export default function AuthPage(props: { router: "signIn" | "signUp" }) { 24 | const dispatch = useAppDispatch(); 25 | const history = useHistory(); 26 | const { search } = useLocation(); 27 | const { username, code } = queryString.parse( 28 | search, 29 | ) as AuthType.resetPasswordQuery; 30 | 31 | useEffect(() => { 32 | const checkLoginUseCode = async () => { 33 | try { 34 | await dispatch(signInUseCode({ username, code })).unwrap(); 35 | history.push("/"); 36 | } catch (error) { 37 | // 에러인 경우(만료된 code) => 404페이지(만료된 페이지입니다) 38 | history.push("/error"); 39 | } 40 | }; 41 | 42 | if (username && code) { 43 | checkLoginUseCode(); 44 | } else { 45 | dispatch(authAction.changeFormState(props.router)); 46 | } 47 | }, [props.router]); 48 | 49 | return ( 50 | <> 51 | {username && code ? ( 52 | 53 | ) : ( 54 |
55 |
56 |
57 |
58 |
59 | )} 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instagram_clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.6.2", 7 | "@stomp/stompjs": "^6.1.2", 8 | "axios": "^0.24.0", 9 | "emoji-picker-react": "^3.5.0", 10 | "jsonwebtoken": "^8.5.1", 11 | "moment": "^2.29.4", 12 | "query-string": "^7.1.1", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-redux": "^7.2.6", 16 | "react-router-dom": "^5.3.0", 17 | "react-scripts": "4.0.3", 18 | "sockjs-client": "^1.5.2", 19 | "stompjs": "^2.3.3", 20 | "styled-components": "^5.3.1", 21 | "styled-reset": "^4.3.4", 22 | "typescript": "^4.1.2", 23 | "uuid": "^9.0.0" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject", 29 | "predeploy": "react-scripts build", 30 | "deploy": "gh-pages -d build", 31 | "build": "react-scripts build", 32 | "postbuild": "cp build/index.html build/404.html" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@types/jsonwebtoken": "^8.5.8", 54 | "@types/navermaps": "^3.0.16", 55 | "@types/node": "^12.0.0", 56 | "@types/query-string": "^6.3.0", 57 | "@types/react": "^17.0.0", 58 | "@types/react-dom": "^17.0.0", 59 | "@types/react-router-dom": "^5.3.0", 60 | "@types/sockjs-client": "^1.5.1", 61 | "@types/stompjs": "^2.3.5", 62 | "@types/styled-components": "^5.1.14", 63 | "@types/uuid": "^9.0.1", 64 | "react-error-overlay": "6.0.9" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Common/PopHeart.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { ReactComponent as RedHeart } from "../../assets/Svgs/redHeart.svg"; 3 | import { ReactComponent as EmptyHeart } from "../../assets/Svgs/emptyHeart.svg"; 4 | import { useState } from "react"; 5 | 6 | const HeartBox = styled.div` 7 | display: flex; 8 | align-items: center; 9 | cursor: pointer; 10 | svg.pop { 11 | animation: pop 0.3s forwards; 12 | @-webkit-keyframes pop { 13 | 0% { 14 | transform: scale(1); 15 | } 16 | 50% { 17 | transform: scale(1.2); 18 | } 19 | 100% { 20 | transform: none; 21 | } 22 | } 23 | @keyframes pop { 24 | 0% { 25 | transform: scale(1); 26 | } 27 | 50% { 28 | transform: scale(1.2); 29 | } 30 | 100% { 31 | transform: none; 32 | } 33 | } 34 | } 35 | .not:hover { 36 | opacity: 0.6; 37 | } 38 | `; 39 | 40 | interface PopHeartProps { 41 | className?: string; 42 | size: number; 43 | isLiked: boolean; 44 | onToggleLike: () => void; 45 | } 46 | 47 | const PopHeart = ({ 48 | className, 49 | size, 50 | isLiked, 51 | onToggleLike, 52 | }: PopHeartProps) => { 53 | const [isFirst, setIsFirst] = useState(true); 54 | 55 | const checkFirstRenderingHandler = () => { 56 | isFirst && setIsFirst(false); 57 | onToggleLike(); 58 | }; 59 | 60 | return ( 61 | 62 | {isLiked ? ( 63 | 68 | ) : ( 69 | 74 | )} 75 | 76 | ); 77 | }; 78 | 79 | export default PopHeart; 80 | -------------------------------------------------------------------------------- /src/pages/Profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import ProfileHeader from "components/Profile/Header/ProfileHeader"; 5 | import Category from "components/Profile/Category"; 6 | import Article from "components/Profile/Article/Article"; 7 | import { useParams } from "react-router-dom"; 8 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 9 | import { 10 | getPosts, 11 | lookUpUserProfile, 12 | } from "app/store/ducks/profile/profileThunk"; 13 | import { resetExtraPostPage } from "app/store/ducks/profile/profileSlice"; 14 | 15 | const Layout = styled.main` 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | flex-shrink: 0; 21 | margin: 0; 22 | padding: 0; 23 | background-color: #fafafa; 24 | 25 | .container { 26 | flex-grow: 1; 27 | margin: 0 auto 30px; 28 | max-width: 935px; 29 | } 30 | 31 | @media (min-width: 736px) { 32 | .container { 33 | padding: 30px 20px 0; 34 | width: calc(100% - 40px); 35 | box-sizing: content-box; 36 | } 37 | } 38 | `; 39 | 40 | const Profile = () => { 41 | const { username } = useParams<{ username: string }>(); 42 | const dispatch = useAppDispatch(); 43 | const currentCategory = useAppSelector( 44 | (state) => state.profile.currentCategory, 45 | ); 46 | 47 | // mount 가 되면 받은 username 으로 이 유저의 모든 프로필 정보를 호출합니다. 48 | useEffect(() => { 49 | dispatch(lookUpUserProfile({ username })); 50 | }, [dispatch, username]); 51 | 52 | // 게시글들을 가져와 줍니다. 53 | useEffect(() => { 54 | const getPost = async () => { 55 | await dispatch(getPosts({ username: username })); 56 | }; 57 | dispatch(resetExtraPostPage()); 58 | getPost(); 59 | }, [currentCategory, dispatch, username]); 60 | 61 | return ( 62 | 63 |
64 | 65 | 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default Profile; 73 | -------------------------------------------------------------------------------- /src/app/store/ducks/edit/editSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { changePassword, getEditItem } from "./editThunk"; 3 | export interface InitialStateType { 4 | currentMenu: EditType.menuType; 5 | editItem: EditType.editItemType; 6 | modal: EditType.modalType; 7 | isLoading: boolean; 8 | } 9 | 10 | const initialState: InitialStateType = { 11 | currentMenu: "프로필 편집", 12 | editItem: { 13 | memberUsername: "", 14 | memberName: "", 15 | memberWebsite: null, 16 | memberIntroduce: null, 17 | memberEmail: null, 18 | memberPhone: null, 19 | memberGender: "비공개", 20 | }, 21 | modal: null, 22 | isLoading: false, 23 | }; 24 | 25 | const editSlice = createSlice({ 26 | name: "edit", 27 | initialState, 28 | reducers: { 29 | selectMenu: (state, action: PayloadAction) => { 30 | state.currentMenu = action.payload; 31 | }, 32 | changeEditItem: ( 33 | state, 34 | action: PayloadAction<{ 35 | name: EditType.editItemKeyType; 36 | value: string; 37 | }>, 38 | ) => { 39 | const { name, value } = action.payload; 40 | 41 | state.editItem = { ...state.editItem, [name]: value }; 42 | }, 43 | selectModal: (state, action: PayloadAction) => { 44 | state.modal = action.payload; 45 | }, 46 | }, 47 | extraReducers: (build) => { 48 | build 49 | .addCase(getEditItem.fulfilled, (state, action) => { 50 | state.editItem = action.payload; 51 | }) 52 | .addCase(changePassword.pending, (state) => { 53 | state.isLoading = true; 54 | }) 55 | .addCase(changePassword.fulfilled, (state) => { 56 | state.isLoading = false; 57 | }) 58 | .addCase(changePassword.rejected, (state) => { 59 | state.isLoading = false; 60 | }); 61 | }, 62 | }); 63 | 64 | export const { selectMenu, changeEditItem, selectModal } = editSlice.actions; 65 | export const editReducer = editSlice.reducer; 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 19 | 23 | 24 | 33 | React App 34 | 35 | 36 | 37 |
38 | 39 |
40 | 50 | 51 | -------------------------------------------------------------------------------- /src/app/store/ducks/common/commonSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { getSearchRecord, searchUser } from "./commonThunk"; 3 | 4 | export interface InitialStateType { 5 | isLoading: boolean; 6 | searchUserKeyword: string; 7 | searchUsers: CommonType.searchResultType[]; 8 | recordedUsers: CommonType.searchResultType[]; 9 | } 10 | 11 | const initialState: InitialStateType = { 12 | isLoading: false, 13 | searchUserKeyword: "", 14 | searchUsers: [], 15 | recordedUsers: [], 16 | }; 17 | 18 | const commontSlice = createSlice({ 19 | name: "common", 20 | initialState, 21 | reducers: { 22 | changeSearchUser: (state, action: PayloadAction) => { 23 | state.searchUserKeyword = action.payload; 24 | if (state.searchUserKeyword === "") { 25 | state.searchUsers = []; 26 | } 27 | }, 28 | resetSearch: (state) => { 29 | state.searchUserKeyword = ""; 30 | state.searchUsers = []; 31 | }, 32 | resetRecordedUsers: (state) => { 33 | state.recordedUsers = []; 34 | }, 35 | }, 36 | extraReducers: (build) => { 37 | build 38 | .addCase(searchUser.pending, (state) => { 39 | state.isLoading = true; 40 | }) 41 | .addCase(searchUser.fulfilled, (state, action) => { 42 | state.isLoading = false; 43 | state.searchUsers = action.payload; 44 | }) 45 | .addCase(searchUser.rejected, (state) => { 46 | state.isLoading = false; 47 | }) 48 | .addCase(getSearchRecord.pending, (state) => { 49 | state.isLoading = true; 50 | }) 51 | .addCase(getSearchRecord.fulfilled, (state, action) => { 52 | state.isLoading = false; 53 | state.recordedUsers = action.payload; 54 | }) 55 | .addCase(getSearchRecord.rejected, (state) => { 56 | state.isLoading = false; 57 | }); 58 | }, 59 | }); 60 | 61 | export const { changeSearchUser, resetRecordedUsers, resetSearch } = 62 | commontSlice.actions; 63 | 64 | export const commonReducer = commontSlice.reducer; 65 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/NewChatModal/NewChatModalTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { 4 | closeModal, 5 | selectNewChatUser, 6 | selectView, 7 | } from "app/store/ducks/direct/DirectSlice"; 8 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 9 | import Loading from "components/Common/Loading"; 10 | import { makeRoom } from "app/store/ducks/direct/DirectThunk"; 11 | import CloseSVG from "assets/Svgs/CloseSVG"; 12 | 13 | interface NewChatModalTitleContainerType { 14 | isSelected: boolean; 15 | } 16 | 17 | const NewChatModalTitleContainer = styled.div` 18 | margin-top: -30px; 19 | padding: 10px 15px; 20 | display: flex; 21 | justify-content: space-between; 22 | border-bottom: 1px solid #dbdbdb; 23 | 24 | svg { 25 | cursor: pointer; 26 | } 27 | 28 | h1 { 29 | font-size: 1rem; 30 | font-weight: 600; 31 | } 32 | 33 | button { 34 | color: #0095f6; 35 | opacity: ${(props) => (props.isSelected ? 1.0 : 0.2)}; 36 | } 37 | `; 38 | 39 | const NewChatModalTitle = () => { 40 | const dispatch = useAppDispatch(); 41 | const { selectedNewChatUsers, isLoading } = useAppSelector( 42 | (state) => state.direct, 43 | ); 44 | 45 | const makeRoomHandler = async () => { 46 | if (selectedNewChatUsers.length > 0) { 47 | await dispatch(makeRoom({ usernames: selectedNewChatUsers })); 48 | dispatch(closeModal()); 49 | dispatch(selectView("chat")); 50 | } 51 | }; 52 | 53 | return ( 54 | 0} 56 | > 57 | { 61 | dispatch(closeModal()); 62 | }} 63 | /> 64 |

새로운 메시지

65 | {isLoading ? ( 66 | 67 | ) : ( 68 | 69 | )} 70 |
71 | ); 72 | }; 73 | 74 | export default NewChatModalTitle; 75 | -------------------------------------------------------------------------------- /src/components/Auth/LoginForm/FormAndButton.tsx: -------------------------------------------------------------------------------- 1 | import Input from "components/Common/Input"; 2 | import { MouseEvent } from "react"; 3 | import SubmitButton from "components/Auth/SubmitButton"; 4 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 5 | import { signIn } from "app/store/ducks/auth/authThunk"; 6 | import useInput from "hooks/useInput"; 7 | import Loading from "components/Common/Loading"; 8 | 9 | const placeholder = { 10 | username: "사용자 이름", 11 | password: "비밀번호", 12 | }; 13 | 14 | export default function LoginFormAndButton() { 15 | const [usernameInputProps, usernameIsValid, usernameIsFocus] = useInput( 16 | "", 17 | undefined, 18 | (value) => value.length > 0, 19 | ); 20 | const [passwordInputProps, passwordIsValid, passwordIsFocus] = useInput( 21 | "", 22 | undefined, 23 | (value) => value.length > 5, 24 | ); 25 | 26 | const dispatch = useAppDispatch(); 27 | const isLoading = useAppSelector((state) => state.auth.isLoading); 28 | 29 | const submitButtonClickHandler = (event: MouseEvent) => { 30 | event.preventDefault(); 31 | const requestSignIn = async () => { 32 | await dispatch( 33 | signIn({ 34 | username: usernameInputProps.value, 35 | password: passwordInputProps.value, 36 | }), 37 | ); 38 | }; 39 | requestSignIn(); 40 | }; 41 | 42 | return ( 43 | <> 44 | 51 | 58 | 63 | {isLoading ? : "로그인"} 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Profile/Modals/UserActionModal.tsx: -------------------------------------------------------------------------------- 1 | import { selectModal } from "app/store/ducks/profile/profileSlice"; 2 | import { useAppDispatch } from "app/store/Hooks"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | import ModalCard from "styles/UI/ModalCard"; 6 | 7 | const UserActionModalInner = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | 12 | & > div { 13 | width: 100%; 14 | flex: 1; 15 | height: 48px; 16 | line-height: 48px; 17 | text-align: center; 18 | cursor: pointer; 19 | } 20 | & > div:not(:first-child) { 21 | border-top: ${(props) => props.theme.color.bd_gray} 1px solid; 22 | } 23 | 24 | & > div:not(:last-child) { 25 | color: #ed4956; 26 | font-weight: bold; 27 | } 28 | `; 29 | 30 | interface UserActionModalProps { 31 | onModalOn: () => void; 32 | onModalOff: () => void; 33 | } 34 | 35 | const UserActionModal = ({ onModalOn, onModalOff }: UserActionModalProps) => { 36 | const dispatch = useAppDispatch(); 37 | return ( 38 | 43 | 44 |
{ 47 | dispatch(selectModal("block")); 48 | }} 49 | > 50 | 차단 51 |
52 |
{ 55 | console.log("제한"); 56 | }} 57 | > 58 | 제한 59 |
60 |
{ 63 | console.log("신고"); 64 | }} 65 | > 66 | 신고 67 |
68 |
69 | 취소 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default UserActionModal; 77 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/NewChatModal/NewChatSearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { changeSearchUser } from "app/store/ducks/common/commonSlice"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | import NewChatInviteButton from "./NewChatInviteButton"; 6 | import { searchUser } from "app/store/ducks/common/commonThunk"; 7 | const NewChatSearchBarContainer = styled.div` 8 | padding: 8px 16px; 9 | 10 | border-bottom: 1px solid #dbdbdb; 11 | h4 { 12 | font-size: 1rem; 13 | font-weight: 600; 14 | margin: 6px 0; 15 | } 16 | 17 | .input-container { 18 | display: flex; 19 | flex-direction: column; 20 | 21 | .button-container { 22 | display: flex; 23 | width: 100%; 24 | flex-wrap: wrap; 25 | gap: 5px; 26 | } 27 | input { 28 | background: 0 0; 29 | border: none; 30 | flex-grow: 1; 31 | font-size: 14px; 32 | line-height: 30px; 33 | overflow: visible; 34 | padding: 4px 12px; 35 | 36 | &::placeholder { 37 | color: #dbdbdb; 38 | } 39 | } 40 | } 41 | `; 42 | 43 | const NewChatSearchBar = () => { 44 | const dispatch = useAppDispatch(); 45 | const { selectedNewChatUsers } = useAppSelector((state) => state.direct); 46 | const { searchUserKeyword } = useAppSelector((state) => state.common); 47 | 48 | return ( 49 | 50 |

받는 사람:

51 |
52 |
53 | {selectedNewChatUsers.map((username) => ( 54 | 55 | ))} 56 |
57 | 58 | { 63 | dispatch(changeSearchUser(e.target.value)); 64 | await dispatch(searchUser({ keyword: e.target.value })); 65 | }} 66 | /> 67 |
68 |
69 | ); 70 | }; 71 | 72 | export default NewChatSearchBar; 73 | -------------------------------------------------------------------------------- /src/components/Profile/Article/SingleRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import styled from "styled-components"; 3 | import SingleContent from "./SingleContent"; 4 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 5 | import { getExtraPosts } from "app/store/ducks/profile/profileThunk"; 6 | import { useParams } from "react-router-dom"; 7 | import useOnView from "hooks/useOnView"; 8 | import { v4 as uuidv4 } from "uuid"; 9 | 10 | const SingleRowContainer = styled.div` 11 | display: flex; 12 | gap: 3px; 13 | @media (min-width: 736px) { 14 | gap: 4px; 15 | } 16 | & > .emptyLayout { 17 | flex: 1; 18 | } 19 | `; 20 | 21 | interface SingleRowProps { 22 | posts: Profile.PostType[]; 23 | isObserving: boolean; 24 | isLinkToParagraph?: boolean; 25 | } 26 | 27 | const SingleRow = ({ 28 | posts, 29 | isObserving, 30 | isLinkToParagraph = false, 31 | }: SingleRowProps) => { 32 | const dispatch = useAppDispatch(); 33 | const extraPostPage = useAppSelector( 34 | (state) => state.profile.extraPostPage, 35 | ); 36 | const { username } = useParams<{ username: string }>(); 37 | const postRef = useRef(null); 38 | 39 | const isVisible = useOnView(postRef); 40 | 41 | useEffect(() => { 42 | const dispatchExtraPost = async () => { 43 | try { 44 | await dispatch( 45 | getExtraPosts({ 46 | page: extraPostPage + 1, 47 | username, 48 | }), 49 | ); 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | }; 54 | 55 | isObserving && isVisible && dispatchExtraPost(); // 이 때 비동기 작업 및 무한 스크롤 56 | // isLast && isVisible && dispatchExtraArticle(); 57 | }, [isObserving, isVisible, dispatch]); 58 | return ( 59 | 60 | {posts.map((post) => 61 | post ? ( 62 | 67 | ) : ( 68 |
69 | ), 70 | )} 71 |
72 | ); 73 | }; 74 | 75 | export default SingleRow; 76 | -------------------------------------------------------------------------------- /src/hooks/useInput.ts: -------------------------------------------------------------------------------- 1 | import { usernameValidator } from "components/Auth/SignUpForm/validator"; 2 | import { customAxios } from "customAxios"; 3 | import { ChangeEvent, useState } from "react"; 4 | 5 | type ReturnType = [AuthType.useInputProps, boolean | null, boolean, () => void]; 6 | 7 | const useInput = ( 8 | initialValue: string, 9 | onBlurValidator?: (value: string) => boolean, 10 | onChangeValidator?: (value: string) => boolean, 11 | ): ReturnType => { 12 | const [value, setValue] = useState(initialValue); 13 | const [isValid, setIsValid] = useState(null); 14 | const [isFocus, setIsFocus] = useState(false); 15 | 16 | const onChange = (event: ChangeEvent) => { 17 | const processdValue = event.target.value.trim(); 18 | setValue(processdValue); 19 | onChangeValidator && setIsValid(onChangeValidator(processdValue)); 20 | }; 21 | 22 | const onFocus = () => { 23 | setIsFocus(true); 24 | onBlurValidator && !isValid && setIsValid(null); 25 | }; 26 | 27 | const resetValue = () => setValue(""); 28 | 29 | const onBlur = () => { 30 | setIsFocus(false); 31 | if (onBlurValidator) { 32 | const validResult = onBlurValidator(value); 33 | setIsValid(validResult); 34 | 35 | if (onBlurValidator === usernameValidator) { 36 | const usernameValidatorWithDispatch = async ( 37 | username: string, 38 | ) => { 39 | try { 40 | const config = { 41 | params: { 42 | username, 43 | }, 44 | }; 45 | const { 46 | data: { data }, 47 | } = await customAxios.get(`/accounts/check`, config); 48 | setIsValid(data); 49 | } catch (error) { 50 | setIsValid(null); 51 | } 52 | }; 53 | validResult && usernameValidatorWithDispatch(value); 54 | } 55 | } 56 | }; 57 | 58 | return [ 59 | onBlurValidator || onChangeValidator 60 | ? { value, onChange, onBlur, onFocus } 61 | : { value, onChange }, 62 | isValid, 63 | isFocus, 64 | resetValue, 65 | ]; 66 | }; 67 | 68 | export default useInput; 69 | -------------------------------------------------------------------------------- /src/components/Auth/Form.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import ContentBox from "components/Common/ContentBox"; 4 | import Suggest from "components/Auth/Suggest"; 5 | import EmailConfirmForm from "components/Auth/SignUpForm/EmailConfirmForm"; 6 | import SignUpForm from "components/Auth/SignUpForm/SignUpFormContent"; 7 | import LoginForm from "components/Auth/LoginForm/LoginFormContent"; 8 | import { useLocation } from "react-router-dom"; 9 | import { useAppSelector } from "app/store/Hooks"; 10 | 11 | const Container = styled.div<{ pathname: string }>` 12 | display: flex; 13 | flex-direction: column; 14 | min-height: ${(props) => (props.pathname === "/" ? 0 : 100)}vh; 15 | margin-top: 12px; 16 | justify-content: center; 17 | max-width: 350px; 18 | flex-grow: 1; 19 | 20 | .warning-message { 21 | padding: 5px; 22 | text-align: center; 23 | color: red; 24 | 25 | .warning { 26 | font-weight: 700; 27 | margin-right: 5px; 28 | } 29 | 30 | .not-instagram { 31 | text-decoration: underline; 32 | } 33 | } 34 | `; 35 | 36 | const contentBox: UIType.ContentBoxProps = { 37 | padding: `10px 0`, 38 | margin: `0 0 10px`, 39 | }; 40 | 41 | export default function Form(props: { router: "signIn" | "signUp" }) { 42 | const { pathname } = useLocation(); 43 | const currentForm = useAppSelector((state) => state.auth.currentFormState); 44 | 45 | return ( 46 | 47 | 48 | {props.router === "signIn" ? ( 49 | 50 | ) : currentForm === "confirmEmail" ? ( 51 | 52 | ) : ( 53 | 54 | )} 55 | 56 | 57 | 58 | 59 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/Common/Footer/InstagramLinks.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { useLocation } from "react-router-dom"; 3 | import FooterRow from "components/Common/Footer/FooterRow"; 4 | 5 | const Links = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | margin-top: 24px; 9 | width: 100%; 10 | `; 11 | 12 | const InstagramRelateLink = [ 13 | { text: "Meta", url: "https://about.facebook.com/meta" }, 14 | { text: "소개", url: "https://about.instagram.com/" }, 15 | { text: "블로그", url: "https://about.instagram.com/blog" }, 16 | { 17 | text: "채용 정보", 18 | url: "https://about.instagram.com/about-us/careers", 19 | }, 20 | { text: "도움말", url: "https://help.instagram.com/" }, 21 | { text: "API", url: "https://developers.facebook.com/docs/instagram" }, 22 | { 23 | text: "개인정보처리방침", 24 | url: "https://help.instagram.com/519522125107875", 25 | }, 26 | { text: "약관", url: "https://help.instagram.com/581066165581870" }, 27 | { 28 | text: "인기 계정", 29 | url: "https://www.instagram.com/directory/profiles/", 30 | }, 31 | { 32 | text: "해시태그", 33 | url: "https://www.instagram.com/directory/hashtags/", 34 | }, 35 | { 36 | text: "위치", 37 | url: "https://www.instagram.com/explore/locations/", 38 | }, 39 | { 40 | text: "Instagram Lite", 41 | url: "https://www.instagram.com/web/lite/", 42 | }, 43 | ]; 44 | 45 | const SearchResult = [ 46 | { text: "뷰티", url: "https://www.instagram.com/topics/beauty/" }, 47 | { 48 | text: "댄스", 49 | url: "https://www.instagram.com/topics/dance-and-performance/", 50 | }, 51 | { text: "피트니스", url: "https://www.instagram.com/topics/fitness/" }, 52 | { text: "식음료", url: "https://www.instagram.com/topics/food-and-drink/" }, 53 | { 54 | text: "집 및 정원", 55 | url: "https://www.instagram.com/topics/home-and-garden/", 56 | }, 57 | { text: "음악", url: "https://www.instagram.com/topics/music/" }, 58 | { 59 | text: "시각 예술", 60 | url: "https://www.instagram.com/topics/visual-arts/", 61 | }, 62 | ]; 63 | 64 | function InstagramLinks() { 65 | const { pathname } = useLocation(); 66 | 67 | return ( 68 | 69 | 70 | {pathname === "/" && } 71 | 72 | ); 73 | } 74 | 75 | export default InstagramLinks; 76 | -------------------------------------------------------------------------------- /src/components/Direct/Section/InboxSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components"; 3 | import ImageSprite from "components/Common/ImageSprite"; 4 | import Button from "styles/UI/Button"; 5 | import { useAppDispatch } from "app/store/Hooks"; 6 | import { 7 | openModal, 8 | resetSelectedRoom, 9 | } from "app/store/ducks/direct/DirectSlice"; 10 | import sprite from "assets/Images/sprite.png"; 11 | 12 | const InboxSectionContainer = styled.div` 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | height: 100%; 18 | 19 | .paper-image { 20 | margin-bottom: 10px; 21 | } 22 | 23 | .content-section { 24 | display: flex; 25 | flex-direction: column; 26 | 27 | .content-title { 28 | font-weight: 300; 29 | font-size: 22px; 30 | line-height: 26px; 31 | text-align: center; 32 | margin-bottom: 10px; 33 | } 34 | 35 | .content-desc { 36 | color: #8e8e8e; 37 | font-weight: 400; 38 | font-size: 14px; 39 | line-height: 18px; 40 | margin-bottom: 20px; 41 | } 42 | } 43 | `; 44 | 45 | const paperImage: CommonType.ImageProps = { 46 | width: 96, 47 | height: 96, 48 | position: `-196px -205px`, 49 | url: sprite, 50 | }; 51 | 52 | const InboxSection = () => { 53 | const dispatch = useAppDispatch(); 54 | 55 | useEffect(() => { 56 | dispatch(resetSelectedRoom()); 57 | }, [dispatch]); 58 | 59 | return ( 60 | 61 |
62 | 63 |
64 |
65 | 내 메시지 66 | 67 | 친구나 그룹에 비공개 사진과 메시지를 보내보세요. 68 | 69 |
70 |
71 | 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default InboxSection; 86 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/LikedMemberModal.tsx: -------------------------------------------------------------------------------- 1 | import { useAppSelector } from "app/store/Hooks"; 2 | import ModalHeader from "components/Home/Modals/ModalHeader"; 3 | import React, { useEffect, useState } from "react"; 4 | import styled from "styled-components"; 5 | import ModalCard from "styles/UI/ModalCard"; 6 | 7 | const LikedMemberModalInner = styled.div` 8 | .member { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | margin: 10px; 13 | .member-info { 14 | display: flex; 15 | align-items: center; 16 | img { 17 | width: 40px; 18 | height: 40px; 19 | border-radius: 100%; 20 | margin-right: 10px; 21 | } 22 | } 23 | 24 | .heart { 25 | } 26 | } 27 | `; 28 | 29 | interface LikedMemberModalProps { 30 | onModalOn: () => void; 31 | onModalOff: () => void; 32 | } 33 | 34 | const LikedMemberModal = ({ onModalOn, onModalOff }: LikedMemberModalProps) => { 35 | const [likedMembers, setLikedMembers] = useState( 36 | [], 37 | ); 38 | const chatMessageList = useAppSelector( 39 | (state) => state.direct.chatMessageList, 40 | ); 41 | const selectedMessageId = useAppSelector( 42 | (state) => state.direct.selectedMessageId, 43 | ); 44 | useEffect(() => { 45 | chatMessageList.forEach((chatMessageItem) => { 46 | if (chatMessageItem.messageId === selectedMessageId) { 47 | setLikedMembers(chatMessageItem.likeMembers); 48 | } 49 | }); 50 | }, []); 51 | 52 | return ( 53 | 58 | 59 | 60 | {likedMembers.map((member) => ( 61 |
62 |
63 | member 64 | {member.username} 65 |
66 |
❤️
67 |
68 | ))} 69 |
70 |
71 | ); 72 | }; 73 | 74 | export default LikedMemberModal; 75 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleAlone/ArticleAloneModal.tsx: -------------------------------------------------------------------------------- 1 | import { modalActions } from "app/store/ducks/modal/modalSlice"; 2 | import { paragraphActions } from "app/store/ducks/paragraph/paragraphSlice"; 3 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 4 | import ArticleAlone from "components/Common/Article/ArticleAlone/ArticleAlone"; 5 | import Loading from "components/Common/Loading"; 6 | import { authorizedCustomAxios } from "customAxios"; 7 | import React, { useEffect, useState } from "react"; 8 | import ModalCard from "styles/UI/ModalCard"; 9 | 10 | interface ArticleAloneModalProps { 11 | onModalOn: () => void; 12 | onModalOff: () => void; 13 | } 14 | 15 | interface OnlyArticleDataType { 16 | data: PostType.ArticleProps; 17 | } 18 | 19 | const ArticleAloneModal = ({ 20 | onModalOn, 21 | onModalOff, 22 | }: ArticleAloneModalProps) => { 23 | const [isDataFetching, setIsDataFetching] = useState(true); 24 | const [windowWidth, setWindowWidth] = useState(window.innerWidth); 25 | const postId = useAppSelector( 26 | (state) => state.modal.articleAloneModalPostId, 27 | ); 28 | const dispatch = useAppDispatch(); 29 | 30 | useEffect(() => { 31 | const getArticleData = async () => { 32 | try { 33 | const { 34 | data: { data }, 35 | } = await authorizedCustomAxios.get( 36 | `/posts/${postId}`, 37 | ); 38 | dispatch(paragraphActions.setArticle(data)); 39 | setIsDataFetching(false); 40 | } catch (error) { 41 | dispatch(modalActions.stopArticleAloneModal()); 42 | } 43 | }; 44 | getArticleData(); 45 | const resizeEventHandler = () => setWindowWidth(window.innerWidth); 46 | window.addEventListener("resize", resizeEventHandler); 47 | 48 | return () => { 49 | window.removeEventListener("resize", resizeEventHandler); 50 | }; 51 | }, [dispatch, postId]); 52 | 53 | return ( 54 | 62 | {isDataFetching ? ( 63 | 64 | ) : ( 65 | 66 | )} 67 | 68 | ); 69 | }; 70 | 71 | export default ArticleAloneModal; 72 | -------------------------------------------------------------------------------- /src/components/Direct/Aside/AsideHeader.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { ReactComponent as ArrowUp } from "assets/Svgs/arrow-up.svg"; 3 | import { ReactComponent as DmWrite } from "assets/Svgs/dm-write.svg"; 4 | import theme from "styles/theme"; 5 | import { openModal } from "app/store/ducks/direct/DirectSlice"; 6 | import NewChatModal from "components/Direct/Section/Modals/NewChatModal/NewChatModal"; 7 | import ConvertAccountModal from "components/Direct/Section/Modals/ConvertAccountModal"; 8 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 9 | 10 | const Container = styled.header` 11 | svg { 12 | cursor: pointer; 13 | } 14 | `; 15 | 16 | const HeaderTop = styled.div` 17 | display: flex; 18 | align-items: center; 19 | padding: 0 20px; 20 | height: 60px; 21 | border-bottom: 1px solid ${theme.color.bd_gray}; 22 | `; 23 | 24 | const NickWrapper = styled.div` 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex: 1; 29 | cursor: pointer; 30 | 31 | & > p { 32 | display: block; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | white-space: nowrap; 36 | color: rgba(var(--i1d, 38, 38, 38), 1); 37 | font-weight: 600; 38 | font-size: 16px; 39 | line-height: 24px; 40 | } 41 | `; 42 | 43 | const Rotate = styled.span` 44 | display: inline-block; 45 | transform: rotate(180deg); 46 | padding: 8px; 47 | `; 48 | 49 | const AsideHeader = () => { 50 | const dispatch = useAppDispatch(); 51 | const { modal } = useAppSelector((state) => state.direct); 52 | const userInfo = useAppSelector((state) => state.auth.userInfo); 53 | // styled-components => sass 54 | return ( 55 | 56 | 57 | { 59 | dispatch(openModal("convertAccount")); 60 | }} 61 | > 62 |

{userInfo?.memberUsername}

63 | 64 | 65 | 66 |
67 | { 69 | dispatch(openModal("newChat")); 70 | }} 71 | /> 72 |
73 | {modal === "newChat" && } 74 | {modal === "convertAccount" && } 75 |
76 | ); 77 | }; 78 | 79 | export default AsideHeader; 80 | -------------------------------------------------------------------------------- /src/app/store/ducks/edit/editThunk.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from "@reduxjs/toolkit"; 2 | import { authorizedCustomAxios } from "customAxios"; 3 | import { FAIL_TO_REISSUE_MESSAGE } from "utils/constant"; 4 | import { authAction } from "../auth/authSlice"; 5 | 6 | export const getEditItem = createAsyncThunk( 7 | "edit/getEditItem", 8 | async (payload, ThunkOptions) => { 9 | try { 10 | const { data } = await authorizedCustomAxios.get("/accounts/edit"); 11 | delete data.data["memberImageUrl"]; // memberImageUrl 은 따로 관리하므로 제거하고 받습니다. 12 | if (data.data["memberGender"] === "PRIVATE") { 13 | data.data["memberGender"] = "비공개"; 14 | } 15 | if (data.data["memberGender"] === "FEMALE") { 16 | data.data["memberGender"] = "여성"; 17 | } 18 | if (data.data["memberGender"] === "MALE") { 19 | data.data["memberGender"] = "남성"; 20 | } 21 | 22 | return data.data; 23 | } catch (error) { 24 | error === FAIL_TO_REISSUE_MESSAGE && 25 | ThunkOptions.dispatch(authAction.logout()); 26 | throw ThunkOptions.rejectWithValue(error); 27 | } 28 | }, 29 | ); 30 | 31 | export const edit = createAsyncThunk( 32 | "edit", 33 | async (payload, ThunkOptions) => { 34 | try { 35 | const gender = payload.memberGender; 36 | let genderEN = "PRIVATE"; 37 | 38 | switch (gender) { 39 | case "비공개": 40 | genderEN = "PRIVATE"; 41 | break; 42 | case "남성": 43 | genderEN = "MALE"; 44 | break; 45 | case "여성": 46 | genderEN = "FEMALE"; 47 | break; 48 | } 49 | const { data } = await authorizedCustomAxios.put("/accounts/edit", { 50 | ...payload, 51 | memberGender: genderEN, 52 | }); 53 | return data; 54 | } catch (error: any) { 55 | return error.response.data; 56 | } 57 | }, 58 | ); 59 | 60 | export const changePassword = createAsyncThunk< 61 | any, 62 | { newPassword: string; oldPassword: string } 63 | >("edit/password", async (payload, ThunkOptions) => { 64 | try { 65 | const { data } = await authorizedCustomAxios.put( 66 | "/accounts/password", 67 | payload, 68 | ); 69 | return data; 70 | } catch (error: any) { 71 | return error.response.data; 72 | } 73 | }); 74 | -------------------------------------------------------------------------------- /src/components/Auth/LoginForm/LoginFormContent.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Link } from "react-router-dom"; 3 | import Line from "components/Auth/Line"; 4 | import FacebookLogin from "components/Auth/FacebookLogin"; 5 | import LoginFormAndButton from "components/Auth/LoginForm/FormAndButton"; 6 | import { useAppSelector } from "app/store/Hooks"; 7 | import Logo from "assets/Images/logo-hello-world.png"; 8 | 9 | const FormContainer = styled.div` 10 | .logo-container { 11 | text-align: center; 12 | } 13 | 14 | .logo { 15 | margin-top: 1rem; 16 | width: 200px; 17 | } 18 | 19 | .inputContainer { 20 | display: flex; 21 | flex-direction: column; 22 | margin-bottom: 10px; 23 | max-width: 350px; 24 | width: 100%; 25 | 26 | a { 27 | margin-top: 12px; 28 | font-size: 12px; 29 | line-height: 16px; 30 | color: #385185; 31 | width: 100%; 32 | text-align: center; 33 | text-decoration: none; 34 | } 35 | 36 | .inputForm { 37 | display: flex; 38 | flex-direction: column; 39 | 40 | .loginForm { 41 | margin-top: 24px; 42 | display: flex; 43 | flex-direction: column; 44 | } 45 | .errorMessage { 46 | color: #ed4956; 47 | font-size: 14px; 48 | line-height: 18px; 49 | text-align: center; 50 | margin: 10px 40px; 51 | } 52 | } 53 | } 54 | `; 55 | 56 | export default function LoginForm() { 57 | const errorMessage = useAppSelector((state) => state.auth.errorMessage); 58 | 59 | return ( 60 | 61 |
62 | hello world 로고 63 |
64 |
65 | 66 |
67 | 68 | 69 | 70 |
71 | {errorMessage && ( 72 |
73 |

{errorMessage}

74 |
75 | )} 76 | 77 | 78 | 비밀번호를 잊으셨나요? 79 | 80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/components/Edit/Section/EditItemInput.tsx: -------------------------------------------------------------------------------- 1 | import { changeEditItem, selectModal } from "app/store/ducks/edit/editSlice"; 2 | import { useAppDispatch } from "app/store/Hooks"; 3 | import styled from "styled-components"; 4 | 5 | const en2kr = { 6 | memberUsername: "사용자 이름", 7 | memberName: "이름", 8 | memberWebsite: "웹사이트", 9 | memberIntroduce: "소개", 10 | memberEmail: "이메일", 11 | memberPhone: "전화번호", 12 | memberGender: "성별", 13 | }; 14 | 15 | const Container = styled.div` 16 | display: flex; 17 | margin: 20px 100px 20px 0px; 18 | aside { 19 | text-align: right; 20 | padding-right: 32px; 21 | margin-top: 6px; 22 | font-size: 16px; 23 | font-weight: 600; 24 | line-height: 18px; 25 | } 26 | .input-wrapper { 27 | width: 355px; 28 | input { 29 | border: 1px solid #dbdbdb; 30 | background: 0 0; 31 | border-radius: 3px; 32 | font-size: 16px; 33 | height: 32px; 34 | padding: 0 10px; 35 | width: 100%; 36 | } 37 | 38 | .guide { 39 | margin-top: 10px; 40 | color: ${(props) => props.theme.font.gray}; 41 | font-weight: 400; 42 | font-size: 12px; 43 | line-height: 16px; 44 | } 45 | } 46 | `; 47 | 48 | interface EditItemProps { 49 | item: [EditType.editItemKeyType, string | null]; 50 | guide?: string; 51 | } 52 | 53 | const EditItemInput = ({ item, guide }: EditItemProps) => { 54 | const dispatch = useAppDispatch(); 55 | return ( 56 | 57 | 60 |
61 | { 67 | dispatch( 68 | changeEditItem({ 69 | name: item[0], 70 | value: e.target.value, 71 | }), 72 | ); 73 | }} 74 | onClick={() => { 75 | if (item[0] === "memberGender") { 76 | dispatch(selectModal("gender")); 77 | } 78 | }} 79 | /> 80 |
{guide}
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default EditItemInput; 87 | -------------------------------------------------------------------------------- /src/components/Profile/Article/Article.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import SingleRow from "./SingleRow"; 4 | import { useAppSelector } from "app/store/Hooks"; 5 | import Loading from "components/Common/Loading"; 6 | import sprite3 from "assets/Images/sprite3.png"; 7 | import sprite2 from "assets/Images/sprite2.png"; 8 | import NoArticle from "./NoArticle"; 9 | 10 | const ArticleContainer = styled.main` 11 | display: flex; 12 | flex-direction: column; 13 | 14 | article { 15 | } 16 | 17 | @media (min-width: 736px) { 18 | } 19 | `; 20 | 21 | const noArticleImage: CommonType.ImageProps = { 22 | width: 24, 23 | height: 24, 24 | position: `-252px -426px`, 25 | url: sprite3, 26 | size: `569px 521px`, 27 | }; 28 | 29 | const noTagImage: CommonType.ImageProps = { 30 | width: 62, 31 | height: 62, 32 | position: `-189px -288px`, 33 | url: sprite2, 34 | size: `440px 411px`, 35 | }; 36 | const noSaveImage: CommonType.ImageProps = { 37 | width: 62, 38 | height: 62, 39 | position: `-126px -288px`, 40 | url: sprite2, 41 | size: `440px 411px`, 42 | }; 43 | 44 | function Article() { 45 | const posts = useAppSelector((state) => state.profile.posts); 46 | const isExtraPostLoading = useAppSelector( 47 | (state) => state.profile.isExtraPostLoading, 48 | ); 49 | const currentCategory = useAppSelector( 50 | (state) => state.profile.currentCategory, 51 | ); 52 | 53 | return ( 54 | 55 | {[...Array(Math.ceil(posts.length / 3))].map((a, index) => ( 56 | 65 | ))} 66 | {isExtraPostLoading && } 67 | {posts.length === 0 && currentCategory === "uploaded" && ( 68 | 69 | )} 70 | {posts.length === 0 && currentCategory === "tagged" && ( 71 | 72 | )} 73 | {posts.length === 0 && currentCategory === "saved" && ( 74 | 79 | )} 80 | 81 | ); 82 | } 83 | 84 | export default Article; 85 | -------------------------------------------------------------------------------- /src/components/Home/Modals/ReportModal/ReportModal.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import ModalCard from "styles/UI/ModalCard"; 3 | import ModalHeader from "../ModalHeader"; 4 | import { ReactComponent as V } from "../../../../assets/Svgs/v.svg"; 5 | import Loading from "components/Common/Loading"; 6 | 7 | const REPORT_REASONS = [ 8 | "스팸", 9 | "나체 이미지 또는 성적 행위", 10 | "혐오 발언 또는 상징", 11 | "폭력 또는 위험한 단체", 12 | "불법 또는 규제 상품 판매", 13 | "따돌림 또는 괴롭힘", 14 | "지적 재산권 침해", 15 | "자살 또는 자해", 16 | "섭식 장애", 17 | "사기 또는 거짓", 18 | "거짓 정보", 19 | "마음에 들지 않습니다", 20 | ]; 21 | 22 | const ReportModalInner = styled.div` 23 | & > * { 24 | border-bottom: ${(props) => props.theme.color.bd_gray} 1px solid; 25 | } 26 | & > h4 { 27 | margin-top: 24px; 28 | padding: 0 16px; 29 | padding-bottom: 16px; 30 | font-size: 16px; 31 | line-height: 24px; 32 | font-weight: ${(props) => props.theme.font.bold}; 33 | } 34 | & > button { 35 | width: 100%; 36 | height: 50px; 37 | display: flex; 38 | align-items: center; 39 | padding: 16px; 40 | font-weight: normal; 41 | & > span { 42 | flex: 1 1 auto; 43 | text-align: start; 44 | } 45 | & > div { 46 | transform: rotate(90deg); 47 | } 48 | } 49 | & > .reportModal__loading { 50 | min-height: 122px; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | `; 56 | 57 | interface ReportModalProps { 58 | onModalOn: () => void; 59 | onModalOff: () => void; 60 | } 61 | 62 | const ReportModal = ({ onModalOn, onModalOff }: ReportModalProps) => { 63 | return ( 64 | 69 | 70 | 71 | {true ? ( 72 | <> 73 |

이 게시물을 신고하는 이유

74 | {REPORT_REASONS.map((reason, index) => ( 75 | 81 | ))} 82 | 83 | ) : ( 84 |
85 | 86 |
87 | )} 88 |
89 |
90 | ); 91 | }; 92 | 93 | export default ReportModal; 94 | -------------------------------------------------------------------------------- /src/components/Common/StoryCircle.tsx: -------------------------------------------------------------------------------- 1 | import sprite2 from "assets/Images/sprite2.png"; 2 | import styled, { DefaultTheme } from "styled-components"; 3 | 4 | interface StyledStoryCircleProps { 5 | scale: number; 6 | type: "unread" | "read" | "default"; 7 | } 8 | 9 | const handleBackground = ( 10 | type: "unread" | "read" | "default", 11 | theme: DefaultTheme, 12 | ) => { 13 | switch (type) { 14 | case "unread": 15 | return `url(${sprite2}) no-repeat 17.287% 64.265%`; 16 | case "read": 17 | return theme.color.bd_gray; 18 | case "default": 19 | return ""; 20 | } 21 | }; 22 | 23 | const handleBackgroundWidth = ( 24 | type: "unread" | "read" | "default", 25 | scale: number, 26 | ) => { 27 | switch (type) { 28 | case "unread": 29 | return `${64 * scale}px`; 30 | case "read": 31 | return `${64 * scale - 2}px`; 32 | case "default": 33 | return `${64 * scale}px`; 34 | } 35 | }; 36 | 37 | const StyledStoryCircle = styled.div` 38 | background: ${(props) => handleBackground(props.type, props.theme)}; 39 | background-size: ${(props) => 40 | `${440 * props.scale}px ${411 * props.scale}px`}; 41 | width: ${(props) => handleBackgroundWidth(props.type, props.scale)}; 42 | height: ${(props) => handleBackgroundWidth(props.type, props.scale)}; 43 | border-radius: ${(props) => props.type === "read" && `50%`}; 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | cursor: pointer; 48 | img { 49 | width: ${(props) => `${64 * props.scale - 10}px`}; 50 | 51 | height: ${(props) => `${64 * props.scale - 10}px`}; 52 | border: ${(props) => props.type === "read" && `2px solid white`}; 53 | box-sizing: ${(props) => props.type === "read" && "content-box"}; 54 | border-radius: 50%; 55 | /* z-index: 0; */ 56 | } 57 | `; 58 | 59 | interface StoryCircleProps { 60 | type: "unread" | "read" | "default"; 61 | avatarUrl: string; 62 | username: string; 63 | scale: number; 64 | onMouseEnter?: ( 65 | event: React.MouseEvent, 66 | ) => void; 67 | onMouseLeave?: ( 68 | event: React.MouseEvent, 69 | ) => void; 70 | } 71 | 72 | const StoryCircle = ({ 73 | type = "unread", 74 | avatarUrl, 75 | username, 76 | scale, 77 | onMouseEnter, 78 | onMouseLeave, 79 | }: StoryCircleProps) => { 80 | return ( 81 | 87 | {username} 88 | 89 | ); 90 | }; 91 | 92 | export default StoryCircle; 93 | -------------------------------------------------------------------------------- /src/components/Common/Header/Upload/UploadWarningModal.tsx: -------------------------------------------------------------------------------- 1 | import { uploadActions } from "app/store/ducks/upload/uploadSlice"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import React from "react"; 4 | import styled from "styled-components"; 5 | import ModalCard from "styles/UI/ModalCard"; 6 | 7 | const StyledUploadWarningModalInner = styled.div` 8 | width: 100%; 9 | & * { 10 | text-align: center; 11 | } 12 | & > .warning__header { 13 | margin: 32px 32px 16px 32px; 14 | & > h3 { 15 | font-weight: ${(props) => props.theme.font.bold}; 16 | font-size: 18px; 17 | } 18 | & > div { 19 | padding-top: 16px; 20 | color: ${(props) => props.theme.font.gray}; 21 | } 22 | } 23 | & > .warning__delete, 24 | & > .warning__cancel { 25 | display: block; 26 | width: 100%; 27 | border-top: 1px solid ${(props) => props.theme.color.bd_gray}; 28 | padding: 4px 8px; 29 | line-height: 40px; 30 | min-height: 48px; 31 | } 32 | & > .warning__delete { 33 | margin-top: 16px; 34 | color: ${(props) => props.theme.font.red}; 35 | } 36 | & > .warning__cancel { 37 | font-weight: normal; 38 | } 39 | `; 40 | 41 | const UploadWarningModal = () => { 42 | const purposeOfWarningModal = useAppSelector( 43 | ({ upload }) => upload.purposeOfWarningModal, 44 | ); 45 | const dispatch = useAppDispatch(); 46 | 47 | const deleteBtnClickHandler = () => { 48 | if (purposeOfWarningModal === "toDragAndDrop") { 49 | dispatch(uploadActions.prevStep()); 50 | } else { 51 | dispatch(uploadActions.cancelUpload()); 52 | } 53 | }; 54 | 55 | return ( 56 | 59 | dispatch(uploadActions.startWarningModal(purposeOfWarningModal)) 60 | } 61 | onModalOff={() => dispatch(uploadActions.cancelWarningModal())} 62 | > 63 | 64 |
65 |

게시물을 삭제하시겠어요?

66 |
지금 나가면 수정 내용이 저장되지 않습니다.
67 |
68 | 74 | 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default UploadWarningModal; 86 | -------------------------------------------------------------------------------- /src/components/Home/Modals/FollowingModal.tsx: -------------------------------------------------------------------------------- 1 | import { postUnfollow } from "app/store/ducks/home/homThunk"; 2 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 3 | import StoryCircle from "components/Common/StoryCircle"; 4 | import styled from "styled-components"; 5 | import ModalCard from "styles/UI/ModalCard"; 6 | 7 | const FollowingModalInner = styled.div` 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | & > div { 14 | width: 100%; 15 | text-align: center; 16 | } 17 | & > div:first-child { 18 | margin: 16px; 19 | margin-top: 32px; 20 | } 21 | & > .followingModal-warning { 22 | margin: 16px 0; 23 | } 24 | & > .followingModal-delete { 25 | margin-top: 16px; 26 | color: ${(props) => props.theme.font.red}; 27 | font-weight: 700; 28 | } 29 | & > .followingModal-cancel { 30 | } 31 | & > .followingModal-delete, 32 | & > .followingModal-cancel { 33 | border-top: ${(props) => props.theme.color.bd_gray} 1px solid; 34 | height: 48px; 35 | line-height: 48px; 36 | cursor: pointer; 37 | } 38 | `; 39 | 40 | interface FollowingModalProps { 41 | onModalOn: () => void; 42 | onModalOff: () => void; 43 | memberUsername: string; 44 | memberImageUrl: string; 45 | } 46 | 47 | const MODAL_CIRCLE_SIZE = 90 / 64; 48 | 49 | const FollowingModal = ({ 50 | onModalOn, 51 | onModalOff, 52 | memberUsername, 53 | memberImageUrl, 54 | }: FollowingModalProps) => { 55 | const dispatch = useAppDispatch(); 56 | const unFollowHandler = () => { 57 | // 언팔로우 58 | const unFollowUser = async () => { 59 | await dispatch(postUnfollow({ username: memberUsername })); 60 | }; 61 | unFollowUser(); 62 | onModalOff(); 63 | }; 64 | 65 | return ( 66 | 71 | 72 | 78 |
79 | @{memberUsername}님의 팔로우를 취소하시겠어요? 80 |
81 |
85 | 팔로우 취소 86 |
87 |
88 | 취소 89 |
90 |
91 |
92 | ); 93 | }; 94 | 95 | export default FollowingModal; 96 | -------------------------------------------------------------------------------- /src/components/Home/SearchListItem.tsx: -------------------------------------------------------------------------------- 1 | import { getSearchRecord } from "app/store/ducks/common/commonThunk"; 2 | import { useAppDispatch } from "app/store/Hooks"; 3 | import CloseSVG from "assets/Svgs/CloseSVG"; 4 | import SearchListItemLayout from "components/Home/SearchListItemLayout"; 5 | import { authorizedCustomAxios } from "customAxios"; 6 | import React, { Dispatch, SetStateAction } from "react"; 7 | import { Link } from "react-router-dom"; 8 | import styled from "styled-components"; 9 | import theme from "styles/theme"; 10 | 11 | const Container = styled.div` 12 | display: flex; 13 | width: 100%; 14 | a { 15 | display: flex; 16 | width: 328px; 17 | align-items: center; 18 | gap: 20px; 19 | height: 60px; 20 | cursor: pointer; 21 | text-decoration: none; 22 | } 23 | .close-button { 24 | flex: 1; 25 | } 26 | `; 27 | 28 | interface SearchListItemProps extends CommonType.searchResultType { 29 | setIsFocused: Dispatch>; 30 | button?: boolean; 31 | } 32 | 33 | const SearchListItem = ({ 34 | dtype, 35 | member, 36 | followingMemberFollow, 37 | setIsFocused, 38 | button, 39 | name, 40 | postCount, 41 | }: SearchListItemProps) => { 42 | const dispatch = useAppDispatch(); 43 | 44 | // 공통으로 사용하는 config 45 | const config = { 46 | params: { 47 | entityName: member?.username || `#${name}`, 48 | entityType: dtype, 49 | }, 50 | }; 51 | 52 | // 사람을 클릭하여 그 사람 프로필로 이동 혹은 해시태그 클릭 53 | const itemClickHandler = async () => { 54 | // 조회수 증가 55 | await authorizedCustomAxios.post("/topsearch/mark", null, config); 56 | // 모달끄기 57 | setIsFocused(false); 58 | }; 59 | 60 | // 사람 한명을 지우는겁니다. 61 | const removeUserHandler = async () => { 62 | await authorizedCustomAxios.delete("/topsearch/recent", config); 63 | await dispatch(getSearchRecord()); 64 | }; 65 | 66 | return ( 67 | 68 | {dtype === "MEMBER" && member ? ( 69 | 73 | 77 | 78 | ) : ( 79 | 80 | 81 | 82 | )} 83 | {button && ( 84 | 87 | )} 88 | 89 | ); 90 | }; 91 | 92 | export default SearchListItem; 93 | -------------------------------------------------------------------------------- /src/components/Direct/Section/Modals/NewChatModal/NewChatRecommendUser.tsx: -------------------------------------------------------------------------------- 1 | import { changeSearchUser } from "app/store/ducks/common/commonSlice"; 2 | import { 3 | selectNewChatUser, 4 | unSelectNewChatUser, 5 | } from "app/store/ducks/direct/DirectSlice"; 6 | import { useAppDispatch, useAppSelector } from "app/store/Hooks"; 7 | import { ReactComponent as CheckedCircle } from "assets/Svgs/checkedCircle.svg"; 8 | import { ReactComponent as Circle } from "assets/Svgs/circle.svg"; 9 | import React from "react"; 10 | import styled from "styled-components"; 11 | 12 | const NewChatRecommendUserContainer = styled.div` 13 | padding: 8px 16px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | cursor: pointer; 18 | .user-info { 19 | display: flex; 20 | 21 | .user-image { 22 | width: 44px; 23 | height: 44px; 24 | border-radius: 50%; 25 | margin-right: 12px; 26 | } 27 | 28 | .user-name-container { 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: space-evenly; 32 | 33 | .user-memberUsername { 34 | font-weight: 600; 35 | } 36 | 37 | .user-memberName { 38 | color: #8e8e8e; 39 | } 40 | } 41 | } 42 | `; 43 | 44 | const NewChatRecommendUser = ({ member }: CommonType.searchResultType) => { 45 | const { selectedNewChatUsers } = useAppSelector((state) => state.direct); 46 | const dispatch = useAppDispatch(); 47 | if (!member) return null; 48 | 49 | const selectNewChatUserHandler = () => { 50 | // 이미 선택했다면 제거해줍니다. 51 | if (selectedNewChatUsers.includes(member?.username)) { 52 | dispatch(unSelectNewChatUser(member?.username)); 53 | } else { 54 | dispatch(selectNewChatUser(member?.username)); 55 | dispatch(changeSearchUser("")); 56 | } 57 | }; 58 | return ( 59 | 60 |
61 | avatarImg 66 |
67 | 68 | {member.username} 69 | 70 | {member.name} 71 |
72 |
73 |
74 | {selectedNewChatUsers.includes(member.username) ? ( 75 | 76 | ) : ( 77 | 78 | )} 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default NewChatRecommendUser; 85 | -------------------------------------------------------------------------------- /src/components/Common/Article/ArticleImgSlider/ImgHashTagUsername.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | 5 | interface StyledImgHashTagUsernameProps { 6 | styleprops: { 7 | x: number; 8 | y: number; 9 | isimghashtagson: boolean | null; // 이 key 에러 발생 방지 위해 생성함 10 | }; 11 | } 12 | 13 | const StyledImgHashTagUsername = styled(Link)` 14 | position: absolute; 15 | left: ${(props) => props.styleprops.x + "%"}; 16 | bottom: ${(props) => props.styleprops.y + "%"}; 17 | text-decoration: none; 18 | background-color: black; 19 | border-radius: 4px; 20 | margin-top: 6px; 21 | height: 36px; 22 | & > div { 23 | position: relative; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | height: 100%; 29 | & > .username__topArrow { 30 | position: absolute; 31 | top: -5px; 32 | border: 6px solid transparent; 33 | border-top: 0; 34 | border-bottom: 6px solid rgba(0, 0, 0, 0.85); 35 | } 36 | & > span { 37 | color: white; 38 | margin: 0 12px; 39 | line-height: 18px; 40 | font-weight: ${(props) => props.theme.font.bold}; 41 | } 42 | } 43 | opacity: 0; 44 | @keyframes usernameOn { 45 | 0% { 46 | opacity: 0; 47 | } 48 | 50% { 49 | opacity: 0; 50 | } 51 | 100% { 52 | opacity: 1; 53 | } 54 | } 55 | @keyframes usernameOff { 56 | 0% { 57 | opacity: 1; 58 | } 59 | 70% { 60 | opacity: 1; 61 | } 62 | 100% { 63 | opacity: 0; 64 | } 65 | } 66 | animation: 0.5s 67 | ${(props) => 68 | props.styleprops.isimghashtagson 69 | ? "usernameOn" 70 | : props.styleprops.isimghashtagson === false 71 | ? "usernameOff" 72 | : ""} 73 | forwards; 74 | `; 75 | 76 | export const ImgHashTagUsername = ({ 77 | postTagDTO, 78 | isImgHashTagsOn, 79 | }: { 80 | postTagDTO: PostType.PostImgTagDTOProps; 81 | isImgHashTagsOn: boolean | null; 82 | }) => { 83 | return ( 84 | 92 |
93 |
94 | {postTagDTO.tag.username} 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default ImgHashTagUsername; 101 | --------------------------------------------------------------------------------