diff --git a/src/assets/icons/eye.svg b/src/assets/icons/eye.svg index 4ff026c..4eee05e 100644 --- a/src/assets/icons/eye.svg +++ b/src/assets/icons/eye.svg @@ -1,5 +1,4 @@ - + - diff --git a/src/constants/tag.ts b/src/constants/tag.ts index b017dc2..6b3a008 100644 --- a/src/constants/tag.ts +++ b/src/constants/tag.ts @@ -1,3 +1,26 @@ +export const TAG_CATEGORY_MAP = { + IOS: "iOS", + ANDROID: "Android", + FRONTEND: "Frontend", + BACKEND: "Backend", + DATA_ENGINEERING: "Data_Engineering", + DATA_SCIENCE: "Data_Science", + DATABASE: "Database", + AI_ML: "AI_ML", + DEVOPS: "DevOps", + CLOUD: "Cloud", + SYSTEMS_OS: "Systems_OS", + NETWORKING: "Networking", + SECURITY: "Security", + GAME_DEV: "Game_Dev", + AR_VR_XR: "AR_VR_XR", + EMBEDDED_IOT: "Embedded_IoT", + BLOCKCHAIN_WEB3: "Blockchain_Web3", + QA_TEST: "QA_Test", + PRODUCT_UX: "Product_UX", + ARCHITECTURE: "Architecture", +} as const; + export const TAG = [ "iOS", "Android", diff --git a/src/hooks/useGetInfiniteBookmarkList.ts b/src/hooks/useGetInfiniteBookmarkList.ts new file mode 100644 index 0000000..0fdd984 --- /dev/null +++ b/src/hooks/useGetInfiniteBookmarkList.ts @@ -0,0 +1,20 @@ +//북마크 리스트 무한스크롤 +import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; +import { getBookmarkList } from "../lib/activity"; + +export const useInfiniteBookmarkPosts = (size = 20) => { + return useSuspenseInfiniteQuery({ + queryKey: ["posts", "bookmarks"], + queryFn: ({ pageParam }) => + getBookmarkList({ + lastBookmarkId: pageParam, + size, + }), + initialPageParam: undefined, + getNextPageParam: lastPage => { + if (!lastPage.data.hasNext) return undefined; + return lastPage.data.lastBookmarkId; + }, + select: res => res.pages, + }); +}; diff --git a/src/hooks/useGetInfiniteCompaniesList.ts b/src/hooks/useGetInfiniteCompaniesList.ts new file mode 100644 index 0000000..50c2ae6 --- /dev/null +++ b/src/hooks/useGetInfiniteCompaniesList.ts @@ -0,0 +1,35 @@ +import { useSuspenseInfiniteQuery } from "@tanstack/react-query"; +import { getCompaniesPostList } from "../lib/post"; + +interface UseInfiniteCompaniesPostsParams { + companies: string[]; + size?: number; +} + +export const useInfiniteCompaniesPosts = ({ + companies, + size = 20, +}: UseInfiniteCompaniesPostsParams) => { + return useSuspenseInfiniteQuery({ + queryKey: ["posts", "companies", companies], + queryFn: ({ pageParam }) => + getCompaniesPostList({ + companies, + size, + ...pageParam, + }), + + initialPageParam: {}, + + getNextPageParam: lastPage => { + if (!lastPage.data?.hasNext) return undefined; + + return { + lastPublishedAt: lastPage.data.lastPublishedAt, + lastPostId: lastPage.data.lastPostId, + }; + }, + + select: res => res.pages, + }); +}; diff --git a/src/hooks/useGetInfinitePostList.ts b/src/hooks/useGetInfinitePostList.ts new file mode 100644 index 0000000..c23c53b --- /dev/null +++ b/src/hooks/useGetInfinitePostList.ts @@ -0,0 +1,48 @@ +// src/hooks/useInfinitePosts.ts +import { + useSuspenseInfiniteQuery, + type QueryFunctionContext, +} from "@tanstack/react-query"; +import type { PageParamType, PostResponseDto } from "../types/post"; +import { getPostList } from "../lib/post"; + +interface UseInfinitePostsParams { + sortBy: "LATEST" | "POPULAR"; + size?: number; +} + +export const useInfinitePosts = ({ + sortBy, + size = 20, +}: UseInfinitePostsParams) => { + return useSuspenseInfiniteQuery< + PostResponseDto, // queryFn return + Error, // error + PostResponseDto[], // select result + ["posts", typeof sortBy], // queryKey + PageParamType + >({ + queryKey: ["posts", sortBy], + queryFn: ({ + pageParam, + }: QueryFunctionContext<["posts", typeof sortBy], PageParamType>) => + getPostList({ + sortBy, + size, + ...pageParam, + }), + + initialPageParam: {}, + + getNextPageParam: lastPage => { + if (!lastPage.data?.hasNext) return undefined; + + return { + lastPublishedAt: lastPage.data.lastPublishedAt, + lastPostId: lastPage.data.lastPostId, + }; + }, + + select: res => res.pages, + }); +}; diff --git a/src/index.css b/src/index.css index 24d8a4c..d8a458c 100644 --- a/src/index.css +++ b/src/index.css @@ -185,15 +185,6 @@ } } -html { - -ms-overflow-style: none; /* IE, Edge */ - scrollbar-width: none; /* Firefox */ - overscroll-behavior: none; -} -html::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ -} - .scrollbar-hide::-webkit-scrollbar { display: none; } diff --git a/src/lib/activity.ts b/src/lib/activity.ts new file mode 100644 index 0000000..f4ce7cd --- /dev/null +++ b/src/lib/activity.ts @@ -0,0 +1,56 @@ +//사용자 활동 api + +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import api from "./api"; +import type { UseInfiniteBookmarkPostsParams } from "../types/post"; + +//1. 북마크 추가 +export const postBookmark = async (postId: number) => { + const { data } = await api.post("/api/v1/activities/bookmarks", { postId }); + return data; +}; + +export const usePostBookmark = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (postId: number) => postBookmark(postId), + onSuccess: async () => { + console.log("북마크성공"); + await queryClient.invalidateQueries({ + queryKey: ["posts"], + }); + }, + onError: err => console.log(err), + }); +}; + +//1. 북마크 제거 +export const deleteBookmark = async (postId: number) => { + const { data } = await api.delete("/api/v1/activities/bookmarks", { + data: { postId }, + }); + return data; +}; + +export const useDeleteBookmark = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (postId: number) => deleteBookmark(postId), + onSuccess: () => { + console.log("북마크 삭제"); + queryClient.invalidateQueries({ + queryKey: ["posts"], + }); + }, + onError: err => console.log(err), + }); +}; + +//북마크 목록 조회 +export const getBookmarkList = async ( + params: UseInfiniteBookmarkPostsParams, +) => { + const { data } = await api.get("/api/v1/activities/bookmarks", { params }); + return data; +}; diff --git a/src/lib/api.ts b/src/lib/api.ts index 3af3651..bfeed1f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,15 +1,32 @@ import axios from "axios"; -import useUserStore from "../store/useUserStore"; +// import useUserStore from "../store/useUserStore"; const api = axios.create({ baseURL: "https://techfork.shop", }); -// const TEMP_TOKEN = import.meta.env.VITE_APP_DEV_TOKEN; +const TEMP_TOKEN = import.meta.env.VITE_APP_DEV_TOKEN; + +// api.interceptors.request.use( +// config => { +// const accessToken = useUserStore.getState().user?.accessToken; +// if (accessToken) { +// config.headers.Authorization = `Bearer ${accessToken}`; +// } + +// return config; +// }, +// error => { +// return Promise.reject(error); +// }, +// ); + +// export default api; + api.interceptors.request.use( config => { - const accessToken = useUserStore.getState().user?.accessToken; - if (accessToken) { - config.headers.Authorization = `Bearer ${accessToken}`; + // const accessToken = useUserStore.getState().user?.accessToken; + if (TEMP_TOKEN) { + config.headers.Authorization = `Bearer ${TEMP_TOKEN}`; } return config; diff --git a/src/lib/company.ts b/src/lib/company.ts new file mode 100644 index 0000000..d7cb326 --- /dev/null +++ b/src/lib/company.ts @@ -0,0 +1,16 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import api from "./api"; + +//게시글이 있는 회사 목록 조회 +export const getCompanyList = async () => { + const { data } = await api.get("/api/v2/posts/companies"); + return data; +}; + +export const useGetCompany = () => { + return useSuspenseQuery({ + queryFn: getCompanyList, + queryKey: ["company"], + select: res => res.data, + }); +}; diff --git a/src/lib/my.ts b/src/lib/my.ts new file mode 100644 index 0000000..611cfc3 --- /dev/null +++ b/src/lib/my.ts @@ -0,0 +1,18 @@ +//사용자와 관련된 정보를 가져옵니다. + +import { useSuspenseQuery } from "@tanstack/react-query"; +import api from "./api"; + +//내 관심사 조회 +export const getMyInterest = async () => { + const { data } = await api.get("/api/v1/users/me/interests"); + return data; +}; + +export const useGetMyInterest = () => { + return useSuspenseQuery({ + queryKey: ["my", "interest"], + queryFn: getMyInterest, + select: res => res.data.interests, + }); +}; diff --git a/src/lib/post.ts b/src/lib/post.ts new file mode 100644 index 0000000..59b3126 --- /dev/null +++ b/src/lib/post.ts @@ -0,0 +1,38 @@ +// 최근 게시글 , 인기있는 게시글 + +import type { PostResponseDto } from "../types/post"; +import api from "./api"; + +export interface GetPostListParams { + sortBy: "LATEST" | "POPULAR"; + size?: number; + lastPublishedAt?: string; + lastPostId?: number; +} +export const getPostList = async ( + params: GetPostListParams, +): Promise => { + const res = await api.get("/api/v2/posts/recent", { + params, + }); + + return res.data; +}; + +//기업별 게시글 조회 + +export interface GetCompaniesPostListParams { + companies?: string[]; + size?: number; + lastPublishedAt?: string; + lastPostId?: number; +} +export const getCompaniesPostList = async ( + params: GetCompaniesPostListParams, +): Promise => { + const res = await api.get("/api/v2/posts/by-company", { + params, + }); + + return res.data; +}; diff --git a/src/lib/recommendation.ts b/src/lib/recommendation.ts new file mode 100644 index 0000000..3c567a1 --- /dev/null +++ b/src/lib/recommendation.ts @@ -0,0 +1,42 @@ +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from "@tanstack/react-query"; +import api from "./api"; + +//추천 게시글 조회 +export const getRecommendPostList = async () => { + const { data } = await api.get("/api/v1/recommendations"); + return data; +}; + +export const useGetRecommendPostList = () => { + return useSuspenseQuery({ + queryKey: ["my", "recommend"], + queryFn: getRecommendPostList, + select: res => res.data, + }); +}; + +//추천 새로고침 +export const postRecommendList = async () => { + const { data } = await api.post("/api/v1/recommendations/regenerate"); + return data; +}; + +export const usePostRecommendPostList = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: postRecommendList, + onSuccess: async () => { + console.log("성공"); + await queryClient.refetchQueries({ + queryKey: ["my", "recommend"], + }); + }, + onError: err => { + console.log(err); + }, + }); +}; diff --git a/src/pages/Login/KakaoLogin.tsx b/src/pages/Login/KakaoLogin.tsx index cd4c274..ae33492 100644 --- a/src/pages/Login/KakaoLogin.tsx +++ b/src/pages/Login/KakaoLogin.tsx @@ -27,5 +27,5 @@ export const KakaoLogin = () => { }, []); console.log(searchParams); - return
카카오로그인
; + return
; }; diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx index fdc6615..1dd7e96 100644 --- a/src/pages/home/HomePage.tsx +++ b/src/pages/home/HomePage.tsx @@ -2,28 +2,102 @@ import { TabSelectList } from "./components/TabSelectList"; import PopOn from "@/assets/icons/pop-on.svg"; import PopOff from "@/assets/icons/pop-off.svg"; import Restart from "@/assets/icons/restart.svg"; - import { CardItem } from "../../shared/CardItem"; import { CompanyItem } from "./components/CompanyItem"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { CompaniesModal } from "./components/CompaniesModal"; import { HomeCompanySelectBtn } from "./components/HomeCompanySelectBtn"; import { useCompanyStore } from "../../store/uesCompanyStore"; -import { MockData } from "../../Mock/company"; -import { useTagStore } from "../../store/useTagStore"; import { SelectionBtn } from "../../shared/select-button/SelectionBtn"; import { TAB_MAP } from "../../constants/tab"; +import type { CardItemProps, PostResponseDto } from "../../types/post"; +import { useInfinitePosts } from "../../hooks/useGetInfinitePostList"; +import { useGetCompany } from "../../lib/company"; +import type { CompanyType } from "../../types/company"; +import { useInfiniteCompaniesPosts } from "../../hooks/useGetInfiniteCompaniesList"; +import { useGetMyInterest } from "../../lib/my"; +import { TagCodeToLabel } from "../../utils/tagCodeToLabel"; +import { + useGetRecommendPostList, + usePostRecommendPostList, +} from "../../lib/recommendation"; +import type { InterestTypeDto } from "../../types/my"; export const HomePage = () => { const [selectedTab, setSelectedTab] = useState(0); // 0 = 기업별 게시글 const [modal, setModal] = useState(false); const { companies, toggleCompany } = useCompanyStore(); - // console.log(companies); + const companyQuery = useInfiniteCompaniesPosts({ + companies, + }); + + const recentQuery = useInfinitePosts({ + sortBy: "LATEST", + }); + + const popularQuery = useInfinitePosts({ + sortBy: "POPULAR", + }); + + const activeQuery = (() => { + switch (selectedTab) { + case 0: + return companyQuery; + case 1: + return null; // 추천은 무한스크롤 ㄴ + case 2: + return recentQuery; + case 3: + return popularQuery; + } + })(); + + const { data: recommendData } = useGetRecommendPostList(); + + //회사 불러오기 + const { data: companyData } = useGetCompany(); + console.log(companyData); + + const infiniteRef = useRef(null); + + const isInfiniteTab = selectedTab !== 1; + + const infiniteQuery = isInfiniteTab ? activeQuery : null; + + const data = infiniteQuery?.data; + const fetchNextPage = infiniteQuery?.fetchNextPage; + const hasNextPage = infiniteQuery?.hasNextPage; + const isFetchingNextPage = infiniteQuery?.isFetchingNextPage; - const { tag } = useTagStore(); - console.log(tag); + const { data: myInterest } = useGetMyInterest(); + console.log(myInterest); - const maxCompany = MockData.data.slice(0, 8); //최대 8개까지 + useEffect(() => { + if (!infiniteRef.current || !hasNextPage || !fetchNextPage) return; + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting && !isFetchingNextPage) { + fetchNextPage(); + } + }); + observer.observe(infiniteRef.current); + return () => observer.disconnect(); + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); + + //데이터 + const posts: CardItemProps[] = (() => { + if (selectedTab === 1) { + return recommendData?.recommendations ?? []; + } + + return data?.flatMap((page: PostResponseDto) => page.data.posts) ?? []; + })(); + + // console.log(posts); + + //게시글 + const maxCompany = companyData.companies.slice(0, 8); + + const { mutate: postRecommendList } = usePostRecommendPostList(); return (
setModal(false)}> @@ -38,26 +112,36 @@ export const HomePage = () => { {selectedTab === 0 && ( <>
- {companies.length !== 0 && ( + {companyData.companies.length !== 0 && ( <>
선택된 기업:
- {companies.map(company => ( - e.stopPropagation()} - /> - ))} + {companies.map(company => { + const matchedCompany = companyData.companies.find( + (item: CompanyType) => item.company === company, + ); + + return ( + e.stopPropagation()} + /> + ); + })} )}
{/* 게시글일때 회사 네모item */}
- {maxCompany.map(item => { + {maxCompany.map((item: CompanyType) => { return ( toggleCompany(item.companies)} + company={item.company} + logoUrl={item.logoUrl} + newDot={item.hasNewPost} + selected={companies.includes(item.company)} + onClick={() => toggleCompany(item.company)} /> ); })} @@ -76,7 +160,7 @@ export const HomePage = () => { onClick={e => e.stopPropagation()} className="absolute top-25 right-40" > - +
)} @@ -88,24 +172,19 @@ export const HomePage = () => { <>

나의 관심 분야:

- - 전체 - React - TypeScript - 전체 - 전체 - React - TypeScript - 전체 - 전체 - React - - {tag.map(item => { - return {item}; - })} + {myInterest.map((item: InterestTypeDto) => + TagCodeToLabel(item.category, item.keywords).map(label => ( + + {label} + + )), + )}
- @@ -113,18 +192,24 @@ export const HomePage = () => { )} +
); }; diff --git a/src/pages/home/components/CompaniesModal.tsx b/src/pages/home/components/CompaniesModal.tsx index 31a54a2..d5c7379 100644 --- a/src/pages/home/components/CompaniesModal.tsx +++ b/src/pages/home/components/CompaniesModal.tsx @@ -1,9 +1,13 @@ import { CompanyModalItem } from "./CompanyModalItem"; import { useCompanyStore } from "../../../store/uesCompanyStore"; -import { MockData } from "../../../Mock/company"; import { useRef } from "react"; +import type { CompanyResponseDto } from "../../../types/company"; -export const CompaniesModal = () => { +interface CompaniesModalProps { + companyData: CompanyResponseDto; +} + +export const CompaniesModal = ({ companyData }: CompaniesModalProps) => { const { toggleCompany, companies } = useCompanyStore(); const headerRef = useRef(null); const scrollToTop = () => { @@ -15,7 +19,7 @@ export const CompaniesModal = () => { console.log(companies); return (
@@ -24,20 +28,20 @@ export const CompaniesModal = () => {

전체 기업

-

{MockData.data.length}개

+

{companyData.totalNumber}개

{/* content */}
- {MockData.data.map(item => ( + {companyData.companies.map(item => ( toggleCompany(item.companies)} + key={item.company} + company={item.company} + logoUrl={item.logoUrl} + selected={companies.includes(item.company)} + onClick={() => toggleCompany(item.company)} /> ))}
diff --git a/src/pages/home/components/CompanyItem.tsx b/src/pages/home/components/CompanyItem.tsx index 8a3fb2c..030b6a0 100644 --- a/src/pages/home/components/CompanyItem.tsx +++ b/src/pages/home/components/CompanyItem.tsx @@ -1,9 +1,8 @@ -import User from "@/assets/images/user.png"; import Dot from "@/assets/icons/dot.svg"; import { cn } from "../../../utils/cn"; interface CompanyItemProps { company?: string; - img?: string; + logoUrl?: string; newDot?: boolean; selected: boolean; onClick?: () => void; @@ -12,6 +11,7 @@ export const CompanyItem = ({ company = "company", newDot = false, selected = true, + logoUrl, onClick, }: CompanyItemProps) => { return ( @@ -26,7 +26,7 @@ export const CompanyItem = ({ )} onClick={onClick} > - company + company

{company}

diff --git a/src/pages/home/components/CompanyModalItem.tsx b/src/pages/home/components/CompanyModalItem.tsx index e4124aa..79e2f40 100644 --- a/src/pages/home/components/CompanyModalItem.tsx +++ b/src/pages/home/components/CompanyModalItem.tsx @@ -1,14 +1,14 @@ -import User from "@/assets/images/user.png"; import { cn } from "../../../utils/cn"; interface CompanyModalItemProps { company?: string; - img?: string; + logoUrl?: string; selected: boolean; onClick: () => void; } export const CompanyModalItem = ({ company = "company", selected = false, + logoUrl, onClick, }: CompanyModalItemProps) => { return ( @@ -20,7 +20,7 @@ export const CompanyModalItem = ({ onClick={onClick} >
- company + company

void; + logoUrl: string; } export const HomeCompanySelectBtn = ({ company, onClick, + logoUrl, }: HomeCompanySelectBtnProps) => { const { toggleCompany } = useCompanyStore(); @@ -18,7 +19,7 @@ export const HomeCompanySelectBtn = ({ className="flex gap-[6px] items-center py-2 px-3 border border-bgNormal bg-white w-fit rounded-[20px]" onClick={onClick} > - company + company

{company}

{ const [selectedTab, setSelected] = useState(0); + const infiniteRef = useRef(null); + + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteBookmarkPosts(); + + useEffect(() => { + if (!infiniteRef.current || !hasNextPage) return; + const observer = new IntersectionObserver(entries => { + if (entries[0].isIntersecting && !isFetchingNextPage) { + fetchNextPage(); + } + }); + observer.observe(infiniteRef.current); + return () => observer.disconnect(); + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); + + const posts = data?.flatMap(page => page.data.bookmarks) ?? []; + + console.log(posts); + return (
{ tagList={MYPAGE_TAP} />
    - - - - - - - - - - - + {posts.map(item => { + return ( + + ); + })}
+
); }; diff --git a/src/shared/CardItem.tsx b/src/shared/CardItem.tsx index ae7b45e..ad0bb85 100644 --- a/src/shared/CardItem.tsx +++ b/src/shared/CardItem.tsx @@ -1,25 +1,90 @@ import BookOn from "@/assets/icons/book-on.svg"; +import BookOff from "@/assets/icons/book-off.svg"; import Eye from "@/assets/icons/eye.svg"; -import User from "@/assets/images/user.png"; +import { forwardRef } from "react"; +import { useDeleteBookmark, usePostBookmark } from "../lib/activity"; -// interface CardItemProps { -// image?: string; -// } +interface CardItemProps { + id?: number; + title: string; + company: string; + url?: string; + logoUrl: string; + thumbnailUrl: string; + publishedAt?: string; + isBookmarked: boolean; + viewCount: number; + keywords?: string[]; +} -export const CardItem = () => { - return ( -
  • -
    - - -
    -
    -

    Title

    -

    - 배포주절주절ㅇㅇㅇㅇㅇㅇㅇdddddddㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇㅇ -

    -
    - -
  • - ); -}; +export const CardItem = forwardRef( + ( + { viewCount, logoUrl, title, thumbnailUrl, company, url, id, isBookmarked }, + ref, + ) => { + //북마크 추가 + const handleSubmitPostBookmark = usePostBookmark(); + const handleSubmitDeleteBookmark = useDeleteBookmark(); + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (!id) return; + if (isBookmarked) { + handleSubmitDeleteBookmark.mutate(id); + } else { + handleSubmitPostBookmark.mutate(id); + } + }; + + return ( +
  • +
    + {company} + 북마크 handleClick(e)} + /> +
    +
    +

    + {title} +

    + {thumbnailUrl ? ( + 썸네일 + ) : ( +

    + 이 글은 유튜브에서 성공적으로 콘텐츠를 제작하고 비즈니스 + 플랫폼으로 활용하기 위한 전략을 다룹니다. 많은 크리에이터들이 + 자신의 영상이 성공하지 않는 이유에 대해 고민하지만, 대중이 원하는 + 콘텐츠는 명확히 존재합니다. +
    이를 파악하고 제작하는 것이 중요하며, 성공적인 유튜브 채널 + 운영을 위해서는 타겟 청중을 이해하고 그들의 관심사에 맞춘 콘텐츠를 + 제공해야 합니다.
    + 또한, 유튜브 알고리즘을 이해하고 활용하는 것이 필수적이며, SEO + 최적화와 꾸준한 업로드 일정이 성공의 열쇠가 됩니다. 이 글은 이러한 + 요소들을 종합적으로 분석하고, 크리에이터들이 실질적으로 적용할 수 + 있는 팁과 전략을 제시합니다. +

    + )} +
    +
    + +

    {viewCount}

    +
    + +
  • + ); + }, +); diff --git a/src/types/company.ts b/src/types/company.ts new file mode 100644 index 0000000..671820e --- /dev/null +++ b/src/types/company.ts @@ -0,0 +1,10 @@ +export type CompanyType = { + company: string; + hasNewPost: boolean; + logoUrl: string; +}; + +export type CompanyResponseDto = { + companies: CompanyType[]; + totalNumber: string; +}; diff --git a/src/types/my.ts b/src/types/my.ts new file mode 100644 index 0000000..c8bb3f7 --- /dev/null +++ b/src/types/my.ts @@ -0,0 +1,6 @@ +//나와맞는게시글 + +export type InterestTypeDto = { + category: string; + keywords: string[]; +}; diff --git a/src/types/post.ts b/src/types/post.ts new file mode 100644 index 0000000..49478e0 --- /dev/null +++ b/src/types/post.ts @@ -0,0 +1,69 @@ +// 페이지 게시글 응답 DTO +export type PostParamsDto = { + sortBy: string; + lastViewCount: number; + lastPublishedAt?: string; + lastPostId?: number; + size: number; +}; + +//페이지 단위 무한스클롤 응답 +export type PostResponseDto = { + data: PostListResponse; + code: string; + isSuccess: boolean; + message: string; +}; + +//페이지 내부 게시글 응답 리스트 타입 +type PostListResponse = { + hasNext: boolean; + lastPostId: number; + lastPublishedAt: string; + lastViewCount: number; + posts: CardItemProps[]; +}; + +//card Item +export type CardItemProps = { + id?: number; + title: string; + company: string; + url?: string; + logoUrl: string; + thumbnailUrl: string; + publishedAt?: string; + isBookmarked: boolean; + viewCount: number; + keywords?: string[]; + postId?: number; +}; + +export type PageParamType = { + lastPublishedAt?: string; + lastPostId?: number; +}; + +// 북마크 +export type UseInfiniteBookmarkPostsParams = { + lastBookmarkId?: number; + size: number; +}; + +export type PostListBookmarkResponse = { + bookmarkId: number; + postId: number; + title: string; + url: string; + companyName: string; + logoUrl: string; + publishedAt: string; +}; + +//북마크 단위 +export type PostBookmarkResponseDto = { + data: PostListBookmarkResponse; + code: string; + isSuccess: boolean; + message: string; +}; diff --git a/src/utils/tagCodeToLabel.ts b/src/utils/tagCodeToLabel.ts new file mode 100644 index 0000000..c686e4e --- /dev/null +++ b/src/utils/tagCodeToLabel.ts @@ -0,0 +1,19 @@ +import { TAG_CATEGORY_MAP, TAG_MAP } from "../constants/tag"; + +export function TagCodeToLabel( + serverCategory: string, + codes: string[], +): string[] { + const clientCategory = + TAG_CATEGORY_MAP[serverCategory as keyof typeof TAG_CATEGORY_MAP]; + + if (!clientCategory) return codes; + + const tags = TAG_MAP[clientCategory]; + if (!tags) return codes; + + return codes.map(code => { + const found = tags.find(tag => tag.code === code); + return found?.label ?? code; + }); +} diff --git a/src/utils/tagMaching.ts b/src/utils/tagMaching.ts deleted file mode 100644 index 1350044..0000000 --- a/src/utils/tagMaching.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TAG_MAP } from "../constants/tag"; - -export const getFormattedInterests = (selectedTags: string[]) => { - return Object.entries(TAG_MAP) - .map(([category, keywords]) => { - const matchingKeywords = keywords.filter(keyword => - selectedTags.includes(keyword.code), - ); - - return { - category, - keywords: matchingKeywords, - }; - }) - .filter(item => item.keywords.length > 0); -};