diff --git a/next.config.mjs b/next.config.mjs index 90b170b..8db54e5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -4,15 +4,20 @@ const nextConfig = { reactStrictMode: true, images: { - domains: ['www.kobis.or.kr', 'd2qf2amuam62ps.cloudfront.net'] + domains: [ + 'www.kobis.or.kr', + 'd2qf2amuam62ps.cloudfront.net', + 'scs-phinf.pstatic.net', + 'api.moaguide.com', + ], }, + swcMinify: true, + compiler: { - styledComponents: true - }, - images: { - domains: ['api.moaguide.com'], + styledComponents: true, }, + webpack: (config, { isServer }) => { if (isServer) { if (Array.isArray(config.resolve.alias)) @@ -24,7 +29,7 @@ const nextConfig = { else config.resolve.alias['msw/node'] = false; } return config; - } + }, }; export default nextConfig; \ No newline at end of file diff --git a/public/images/mypage/bookmark-white.svg b/public/images/mypage/bookmark-white.svg new file mode 100644 index 0000000..c242024 --- /dev/null +++ b/public/images/mypage/bookmark-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/mypage/googleSocial.svg b/public/images/mypage/googleSocial.svg new file mode 100644 index 0000000..02b2ad2 --- /dev/null +++ b/public/images/mypage/googleSocial.svg @@ -0,0 +1,14 @@ + + + + google + + + + + + + + + + \ No newline at end of file diff --git a/public/images/mypage/kakaoSocial.svg b/public/images/mypage/kakaoSocial.svg new file mode 100644 index 0000000..41c1132 --- /dev/null +++ b/public/images/mypage/kakaoSocial.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/public/images/mypage/naverSocial.svg b/public/images/mypage/naverSocial.svg new file mode 100644 index 0000000..6a36296 --- /dev/null +++ b/public/images/mypage/naverSocial.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/public/images/report/left-button.svg b/public/images/report/left-button.svg new file mode 100644 index 0000000..636ab85 --- /dev/null +++ b/public/images/report/left-button.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/mypage/edit/page.tsx b/src/app/mypage/edit/page.tsx index 9931e11..0e67193 100644 --- a/src/app/mypage/edit/page.tsx +++ b/src/app/mypage/edit/page.tsx @@ -1,12 +1,12 @@ 'use client'; import { useModalStore } from '@/store/modal.store'; import { useMemberStore } from '@/store/user.store'; -import { checkEmail } from '@/utils/checkEmail'; import { useRouter } from 'next/navigation'; import React, { ChangeEvent, useEffect, useState } from 'react'; import { disablePageScroll, enablePageScroll } from 'scroll-lock'; import { checkNicknameAvailability } from '@/service/auth'; import { updateNickname } from '@/service/change'; +import { getSocialInfo } from '@/utils/checkEmail'; const Editpage = () => { const router = useRouter(); @@ -17,6 +17,8 @@ const Editpage = () => { const [isNameComplete, setIsNameComplete] = useState(''); const { setModalType, setOpen, open } = useModalStore(); + const socialInfo = getSocialInfo(member.loginType, member.memberEmail); + const handleNameChange = (e: ChangeEvent) => { const regex = e.target.value.replace(/[^a-zA-Z0-9ㄱ-ㅎㅏ-ㅣ가-힣]/g, ''); setNameValue(regex); @@ -136,9 +138,9 @@ const Editpage = () => {
이메일
-
- -
{checkEmail(member?.memberEmail)} 가입
+
+ {socialInfo.imgSrc && socialLogo} +
{socialInfo.platform} 가입
diff --git a/src/components/mypage/MypageHeader.tsx b/src/components/mypage/MypageHeader.tsx index d94ce10..c1370a4 100644 --- a/src/components/mypage/MypageHeader.tsx +++ b/src/components/mypage/MypageHeader.tsx @@ -21,22 +21,14 @@ const MypageHeader = () => {
-
{member.subscribe} 이용 중
-
- +
+ +
관심종목
-
-
-
-
- -
-
관심종목
-
-
-
24개
-
- +
+
+
24개
+
diff --git a/src/components/mypage/MypageMenu.tsx b/src/components/mypage/MypageMenu.tsx index 01255ec..f72281a 100644 --- a/src/components/mypage/MypageMenu.tsx +++ b/src/components/mypage/MypageMenu.tsx @@ -3,22 +3,6 @@ import React from 'react'; const MypageMenu = () => { return (
- {/* 구독 관리 */} -
-
-
- 구독 -
-
구독 관리
-
-
- 구독 -
-
{/* 공지사항 */}
@@ -35,22 +19,6 @@ const MypageMenu = () => { />
- {/* 쿠폰 관리 */} -
-
-
- 쿠폰 -
-
쿠폰 관리
-
-
- 쿠폰 -
-
{/* 알림 설정 */}
@@ -67,22 +35,6 @@ const MypageMenu = () => { />
- {/* 결제 관리 */} -
-
-
- 결제 -
-
결제 관리
-
-
- 결제 -
-
{/* 카카로톡 1:1 문의 */}
diff --git a/src/components/practice/CategoryPractice.tsx b/src/components/practice/CategoryPractice.tsx index 1b906ee..8b80acc 100644 --- a/src/components/practice/CategoryPractice.tsx +++ b/src/components/practice/CategoryPractice.tsx @@ -1,14 +1,17 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { useReportStore } from '@/store/report.store'; import { Virtuoso } from 'react-virtuoso'; import { getStudyGuides, getArticles } from '@/factory/ReportLists'; import CategoryPracticeItem from './CategoryPracticeItem'; import CategoryPracticeItemSkeleton from '../skeleton/CategoryPracticeItemSkeleton'; import SubLoadmapBottomArticleSkeleton from '../skeleton/SubLoadmapBottomArticleSkeleton'; -import CategorySubloadmapBottomArticle from './CategorySubloadmapBottomArticle'; +import CategorySubloadmapBottomArticle from './CategorySubloadmapBottomArticle'; const CategoryPractice = () => { const { currentCategory, subCategory, sort, setSubCategory } = useReportStore(); + const [showSkeleton, setShowSkeleton] = useState(true); + const [guideLoaded, setGuideLoaded] = useState(false); + const [articleLoaded, setArticleLoaded] = useState(false); const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, isLoading @@ -16,6 +19,28 @@ const CategoryPractice = () => { ? getStudyGuides(currentCategory, subCategory, sort) : getArticles(); + useEffect(() => { + if (subCategory === 'guide' && !guideLoaded) { + setShowSkeleton(true); + const timer = setTimeout(() => { + setShowSkeleton(false); + setGuideLoaded(true); + }, 1000); + return () => clearTimeout(timer); + } + + if (subCategory === 'article' && !articleLoaded) { + setShowSkeleton(true); + const timer = setTimeout(() => { + setShowSkeleton(false); + setArticleLoaded(true); + }, 1000); + return () => clearTimeout(timer); + } + + setShowSkeleton(false); + }, [subCategory, guideLoaded, articleLoaded]); + const loadMore = useCallback(() => { if (hasNextPage && !isFetching && !isFetchingNextPage && !isLoading) { setTimeout(() => { @@ -33,20 +58,20 @@ const CategoryPractice = () => { onClick={() => setSubCategory('guide')} className={`pb-5 cursor-pointer ${subCategory === 'guide' ? ' text-gray700 border-b-2 border-normal ' : 'text-gray300'}`} > - 투자 가이드 + 조각투자 가이드
setSubCategory('article')} className={`pb-5 cursor-pointer ${subCategory === 'article' ? ' text-gray700 border-b-2 border-normal ' : 'text-gray300'}`} > - 아티클 + 재테크 가이드
- {isLoading ? ( + {showSkeleton || isLoading ? ( subCategory === 'guide' ? ( - Array.from({ length: 3 }).map((_, i) => ) + Array.from({ length: 10 }).map((_, i) => ) ) : ( Array.from({ length: 10 }).map((_, i) => ) ) @@ -54,16 +79,44 @@ const CategoryPractice = () => { ( - subCategory === 'guide' ? ( - - ) : ( - - ) - )} + itemContent={(index, item) => { + if (subCategory === 'guide') { + return ; + } else { + const firstItem = allPosts[index * 2]; + const secondItem = allPosts[index * 2 + 1]; + + return ( +
+ {firstItem && ( + + )} + {secondItem && ( + + )} +
+ ); + } + }} /> )}
diff --git a/src/components/practice/CategoryPracticeItem.tsx b/src/components/practice/CategoryPracticeItem.tsx index 2949513..5a489d2 100644 --- a/src/components/practice/CategoryPracticeItem.tsx +++ b/src/components/practice/CategoryPracticeItem.tsx @@ -1,75 +1,29 @@ -import type { ReportListsItem, StudyGuidesItem, SubLoadmap } from '@/types/homeComponentsType'; -import { formatCategory } from '@/utils/formatCategory'; -import axios from 'axios'; -import { format, parseISO } from 'date-fns'; -import { useRouter } from 'next/navigation'; -import React, { useState } from 'react'; -import SubLoadmapSkeleton from '../skeleton/SubLoadmapSkeleton'; -import CategorySubloadmapItem from './CategorySubloadmapItem'; +import type { StudyGuidesItem, SubLoadmap } from '@/types/homeComponentsType'; +import React from 'react'; +import { useRouter } from 'next/navigation'; -const CategoryPracticeItem = ({ id, difficulty, title, description }: StudyGuidesItem) => { - const [details, setDetails] = useState(null); - const [showDetails, setShowDetails] = useState(false); - const [loadingDetails, setLoadingDetails] = useState(false); - const [toggleImage, setToggleImage] = useState('/images/report/toggle_button.svg'); +const CategoryPracticeItem = ({ id, title, link }: StudyGuidesItem) => { + const router = useRouter(); - const fetchDetails = async (itemId: number) => { - if (!details && !loadingDetails) { - setLoadingDetails(true); - try { - const response = await axios.get(`https://api.moaguide.com/study/guide/${itemId}`); - setDetails(response.data); - setLoadingDetails(false); - } catch (error) { - console.error('Fetching details failed:', error); - setLoadingDetails(false); - } - } - }; - - const handleToggle = () => { - setShowDetails(!showDetails); - if (!showDetails && !details) { - fetchDetails(id); - } - - setToggleImage(!showDetails ? '/images/report/toggle_button_close.svg' : '/images/report/toggle_button.svg'); + const handleClick = () => { + console.log(link); + window.open(link, '_blank'); }; return ( -
-
-
-
- {difficulty} -
+
+
+
{title}
-
{description}
+
더보기
Toggle Details
- {showDetails && ( - loadingDetails ? ( - Array.from({ length: 3 }).map((_, i) => ( - - )) - ) : ( - details?.map((detail, index) => ( - - )) - ) - )}
); diff --git a/src/components/practice/CategorySubloadmapBottomArticle.tsx b/src/components/practice/CategorySubloadmapBottomArticle.tsx index def1d66..fa4f348 100644 --- a/src/components/practice/CategorySubloadmapBottomArticle.tsx +++ b/src/components/practice/CategorySubloadmapBottomArticle.tsx @@ -1,29 +1,27 @@ import React from 'react'; -import { SubLoadmapBottomArticleItemsProps } from '@/types/homeComponentsType'; +import { InvestmentGuideProps } from '@/types/homeComponentsType'; import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -const CategorySubloadmapBottomArticle: React.FC = ({ data }) => { - const router = useRouter(); +const CategorySubloadmapBottomArticle: React.FC = ({ id, title, description, date, imageLink, link }) => { const handleClick = () => { - console.log(data.id); - router.push(`/practice/${data.id}`); + console.log(link); + window.open(link, '_blank'); }; return ( -
+
{data.title}
-
{data.title}
-
{data.description}
+
{title}
+
{description}ㆍ{date}
diff --git a/src/components/practice/CategorySubloadmapItem.tsx b/src/components/practice/CategorySubloadmapItem.tsx deleted file mode 100644 index be27d72..0000000 --- a/src/components/practice/CategorySubloadmapItem.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { CategorySubloadmapItemProps, SubLoadmapBottomArticle } from '@/types/homeComponentsType'; -import axios from 'axios'; -import React, { useState } from 'react'; -import CategorySubloadmapBottomArticle from './CategorySubloadmapBottomArticle'; -import SubLoadmapBottomArticleSkeleton from '../skeleton/SubLoadmapBottomArticleSkeleton'; - -const CategorySubloadmapItem: React.FC = ({ data, isTop, isBottom }) => { - - const [details, setDetails] = useState(null); - const [showDetails, setShowDetails] = useState(false); - const [loadingDetails, setLoadingDetails] = useState(false); - const [toggleImage, setToggleImage] = useState('/images/report/toggle_button.svg'); - - const fetchDetails = async () => { - if (!details) { // 데이터가 로드되지 않았을 때만 API 호출 - setLoadingDetails(true); - try { - const response = await axios.get(`https://api.moaguide.com/study/guide/article?subcategory=${data.id}`); - setTimeout(() => { - setDetails(response.data); - setLoadingDetails(false); - }, 500); - } catch (error) { - console.error('Fetching details failed:', error); - setLoadingDetails(false); - } - } - setShowDetails(!showDetails); - setToggleImage(showDetails ? '/images/report/toggle_button.svg' : '/images/report/toggle_button_close.svg'); - }; - - return ( -
-
-
-
{`${data.number}. ${data.title}`}
-
{data.description}
-
- Toggle Details -
- - {showDetails && ( -
- {loadingDetails ? ( - Array.from({ length: 2 }).map((_, i) => ( - - )) - ) : ( - details?.map((detail, index) => ( - - )) - )} -
- )} -
- ); -}; - -export default CategorySubloadmapItem; \ No newline at end of file diff --git a/src/components/sign/SignLayout.tsx b/src/components/sign/SignLayout.tsx index 1598af1..1d1f825 100644 --- a/src/components/sign/SignLayout.tsx +++ b/src/components/sign/SignLayout.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { useRouter } from 'next/navigation'; import { login } from '@/service/auth'; import { useAuthStore } from '@/store/userAuth.store'; -import throttle from 'lodash/throttle'; // lodash에서 throttle 함수 import +import throttle from 'lodash/throttle'; const SignLayout = () => { const [email, setEmail] = useState(''); diff --git a/src/components/skeleton/CategoryPracticeItemSkeleton.tsx b/src/components/skeleton/CategoryPracticeItemSkeleton.tsx index 230dc12..86ed860 100644 --- a/src/components/skeleton/CategoryPracticeItemSkeleton.tsx +++ b/src/components/skeleton/CategoryPracticeItemSkeleton.tsx @@ -2,21 +2,11 @@ import React from 'react'; const CategoryPracticeItemSkeleton = () => { return ( -
-
- {/* Category Skeleton */} -
- {/* Title Skeleton */} -
- {/* Date Skeleton */} -
-
+
{/* Image Skeleton */} -
-
-
+
); }; -export default CategoryPracticeItemSkeleton; +export default CategoryPracticeItemSkeleton; \ No newline at end of file diff --git a/src/components/skeleton/SubLoadmapBottomArticleSkeleton.tsx b/src/components/skeleton/SubLoadmapBottomArticleSkeleton.tsx index 3bc6288..97de13e 100644 --- a/src/components/skeleton/SubLoadmapBottomArticleSkeleton.tsx +++ b/src/components/skeleton/SubLoadmapBottomArticleSkeleton.tsx @@ -2,19 +2,36 @@ import React from 'react'; const SubLoadmapBottomArticleSkeleton = () => { return ( -
-
-
-
+
+ {/* 첫 번째 스켈레톤 */} +
+
+
+
+
+
+ {/* Title Skeleton */} +
+ {/* Description Skeleton */} +
+
+
-
- {/* Number and Title Skeleton */} -
- {/* Description Skeleton */} -
+ + {/* 두 번째 스켈레톤 */} +
+
+
+
+
+
+ {/* Title Skeleton */} +
+ {/* Description Skeleton */} +
+
+
- {/* Image Skeleton */} -
); }; diff --git a/src/factory/ReportLists.ts b/src/factory/ReportLists.ts index 162d668..5b27aa5 100644 --- a/src/factory/ReportLists.ts +++ b/src/factory/ReportLists.ts @@ -2,14 +2,14 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import axios from 'axios'; const fetchStudyGuides = async ({ pageParam = 1 }) => { - const { data } = await axios.get(`https://api.moaguide.com/study/guide?page=${pageParam}&size=3`); + const { data } = await axios.get(`https://api.moaguide.com/study/guide?page=${pageParam}&size=10`); return { - content: data.roadmaps, + content: data.roadmap, nextPage: pageParam + 1, totalPages: data.total, - totalElements: data.roadmaps.length, + totalElements: data.roadmap.length, currentPage: data.page, - isLast: data.page + 1 >= data.total + isLast: data.page + 1 >= data.total, }; }; @@ -19,7 +19,7 @@ export const getStudyGuides = (category: string, subCategory: string, sort: stri const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, isLoading } = useInfiniteQuery({ queryKey, - queryFn: fetchStudyGuides, + queryFn: fetchStudyGuides, getNextPageParam: (lastPage) => { return lastPage.isLast ? undefined : lastPage.nextPage; }, @@ -28,12 +28,12 @@ export const getStudyGuides = (category: string, subCategory: string, sort: stri }); return { - data: data?.pages.flatMap((page) => page.content) || [], + data: data?.pages.flatMap((page) => page.content) || [], fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, - isLoading + isLoading, }; }; diff --git a/src/service/auth.ts b/src/service/auth.ts index 10c9f57..b13acf8 100644 --- a/src/service/auth.ts +++ b/src/service/auth.ts @@ -92,9 +92,11 @@ export const login = async (email: string, password: string) => { memberEmail: userInfo.email, memberNickName: userInfo.nickname, memberPhone: userInfo.phonenumber, - subscribe: '1개월 플랜', + loginType: userInfo.loginType, }); + console.log(userInfo); + return response.data; } catch (error) { console.error('로그인 오류:', error); diff --git a/src/store/user.store.ts b/src/store/user.store.ts index 8b0c461..e441801 100644 --- a/src/store/user.store.ts +++ b/src/store/user.store.ts @@ -7,7 +7,7 @@ interface IUserInfo { memberEmail: string; memberNickName: string; memberPhone: string; - subscribe: string; + loginType: string; } interface IMember { @@ -23,7 +23,7 @@ export const useMemberStore = create( memberEmail: '', memberNickName: '', memberPhone: '', - subscribe: '' + loginType: '', }, setMember: (userInfo: IUserInfo) => set({ member: userInfo }), clearMember: () => set({ @@ -31,7 +31,7 @@ export const useMemberStore = create( memberEmail: '', memberNickName: '', memberPhone: '', - subscribe: '' + loginType: '', } }), }), @@ -39,4 +39,4 @@ export const useMemberStore = create( name: 'userInfo' } ) -); +); \ No newline at end of file diff --git a/src/types/homeComponentsType.ts b/src/types/homeComponentsType.ts index b97251b..51e17b9 100644 --- a/src/types/homeComponentsType.ts +++ b/src/types/homeComponentsType.ts @@ -66,9 +66,8 @@ export interface SearchedItem { export interface StudyGuidesItem { id: number; - difficulty: string; title: string; - description: string; + link: string; } export interface SubLoadmap { @@ -89,23 +88,16 @@ export interface CategorySubloadmapSkeletonItemProps { isBottom: boolean; } -export interface SubLoadmapBottomArticle { +export interface InvestmentGuideProps { id: number; title: string; description: string; imageLink: string; date: string; - content: string; - pdfLink: string; + link: string; } -export interface SubLoadmapBottomArticleItemsProps { - data: SubLoadmapBottomArticle; - isTop: boolean; - isBottom: boolean; -} - export interface ArticleItem { id: number; title: string; diff --git a/src/utils/checkEmail.ts b/src/utils/checkEmail.ts index 7e07b51..23b3a85 100644 --- a/src/utils/checkEmail.ts +++ b/src/utils/checkEmail.ts @@ -1,33 +1,30 @@ -/* -유저의 이메일을 받아와서 도메인에 맞는 주소를 출력하는 함수입니다 -e.g. input) test@gmail.com -> output) 구글 -*/ -export const checkEmail = (email: string): string | null => { - const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - if (!emailPattern.test(email)) { - return null; - } - - const domainPattern = /@([^@.]+)\./; - const match = email.match(domainPattern); - - if (match && match[1] === 'naver') { - return '네이버'; - } else if (match && match[1] === 'kakao') { - return '카카오'; - } else if (match && match[1] === 'gmail') { - return '구글'; - } else if (match && match[1] === 'nate') { - return '네이트'; - } else if (match && match[1] === 'daum') { - return '다음'; - } else if (match && match[1] === 'hanmail') { - return '다음'; - } else if (match && match[1]) { - return match[1]; - } else { - return null; +export const getSocialInfo = (loginType: string, email: string) => { + switch (loginType) { + case 'kakao': + return { + platform: '카카오', + imgSrc: '/images/mypage/kakaoSocial.svg', + }; + case 'google': + return { + platform: '구글', + imgSrc: '/images/mypage/googleSocial.svg', + }; + case 'naver': + return { + platform: '네이버', + imgSrc: '/images/mypage/naverSocial.svg', + }; + case 'local': + return { + platform: '일반', + imgSrc: null, + }; + default: + return { + platform: '알 수 없음', + imgSrc: null, + }; } -}; +}; \ No newline at end of file