Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6f17240
Feat: Link 페이지 기본 ui 구현, 기능 추가해야 함
junjeeong Nov 8, 2024
bcebebc
Merge branch 'develop' into feature/Link-페이지
junjeeong Nov 8, 2024
49ab5df
Fix: CardItem -> LinkCard로 변경
junjeeong Nov 8, 2024
536f59f
Updating: /link 페이지 구축중
junjeeong Nov 8, 2024
91de8a2
Merge branch 'develop' into feature/Link-페이지
junjeeong Nov 8, 2024
9af6e59
link 페이지 작업중
junjeeong Nov 10, 2024
7c23e60
Merge branch 'develop' into feature/Link-페이지
junjeeong Nov 10, 2024
0924ff1
Feat : /link 페이지 SSR로 데이터 받아오는 로직 추가
junjeeong Nov 10, 2024
f129f67
스타일 수정
junjeeong Nov 10, 2024
4027ea8
Feat: FolderTag handleSubmit 함수 추가
junjeeong Nov 10, 2024
dcef227
Feat: FolderTag handleSubmit 함수 추가
junjeeong Nov 10, 2024
98d1f38
Fix : 받는 prop 수정, handleSubmit 로직 수정
junjeeong Nov 10, 2024
75fe98b
Fix : 받는 prop 수정, handleSubmit 로직 수정
junjeeong Nov 10, 2024
787d8e8
Feat: tag hover 기능, 선택된 tag 배경색 추가, tag 눌렀을 때 router.push 하는 기능 추가
junjeeong Nov 10, 2024
b13ed23
Fix: ci 에러 고침
junjeeong Nov 10, 2024
45ae2bd
Merge branch 'feature/AddLinkInput' into feature/Link-페이지
junjeeong Nov 10, 2024
38b81fd
Feat: AddLinkInput, FolderTag 하위 작업 merge함, 검색, 폴더선택, 링크추가 기능 작업중
junjeeong Nov 10, 2024
f6f58b7
Fix: #65 Pr에서 받은 피드백 적용
junjeeong Nov 10, 2024
b1a0e41
Fix: #65 Pr에서 받은 피드백 적용
junjeeong Nov 10, 2024
bac1868
Fix: CI 오류 수정
junjeeong Nov 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions components/FolderTag.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
interface FolderData {
id: number;
createdAt: string;
name: string;
linkCount: number;
}
import { useRouter } from "next/router";
import { FolderListData } from "@/types/folderTypes";

const FolderTag = (list: FolderData[]) => {
const folderStyle = "py-[8px] px-[12px] border border-purple-100 rounded-md";
const FolderTag = ({ folderList }: FolderListData) => {
const router = useRouter();
const { folder: currentFolderId } = router.query;

const folderStyle =
"py-[8px] px-[12px] border border-purple100 rounded-md hover:bg-purple100 hover:text-white";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클릭 가능한 요소임을 안내하기 위해 cursor-pointer 도 추가하면 좋을것같아요 !


const handleSubmit = (id: number | string) => {
router.push({
pathname: router.pathname,
query: { ...router.query, folder: id },
});
};

return (
<div className="flex flex-wrap gap-[8px]">
<div className={folderStyle}>전체</div>
{list.map((folder) => (
<div key={folder.id} className={folderStyle}>
{folder.name}
</div>
<ul className="flex flex-wrap gap-[8px]">
<li>
<button className={folderStyle} onClick={() => handleSubmit("")}>
전체
</button>
</li>
{folderList.map((folder) => (
<li key={folder.id}>
<button
className={`${folderStyle} ${folder.id === Number(currentFolderId) && "bg-purple100 text-white"}`}
type="submit"
onClick={() => handleSubmit(folder.id)}
>
{folder.name}
</button>
</li>
))}
</div>
</ul>
);
};

Expand Down
18 changes: 18 additions & 0 deletions components/Link/ActionButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Image from "next/image";

const ActionButtons = () => (
<div className="w-[192px] h-[18px] flex justify-between gap-[12px] text-gray400">
{[
{ src: "/icons/share.svg", alt: "공유", text: "공유" },
{ src: "/icons/pen.svg", alt: "이름 변경", text: "이름 변경" },
{ src: "/icons/delete.svg", alt: "삭제", text: "삭제" },
].map(({ src, alt, text }) => (
<button key={text} className=" flex items-center gap-[4px] text-sm ">
<Image width={18} height={18} src={src} alt={alt} />
<span>{text}</span>
</button>
))}
</div>
);

export default ActionButtons;
39 changes: 21 additions & 18 deletions components/Link/AddLinkInput.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
import { ChangeEvent, FormEvent, useState } from "react";
import { postLink } from "@/lib/api/link";
import { ChangeEvent, KeyboardEvent, useState } from "react";
import { FolderListData } from "@/types/folderTypes";
import Image from "next/image";
import SubmitButton from "../SubMitButton";

const AddLinkInput = (folderId: number) => {
const [value, setValue] = useState("");
const AddLinkInput = ({ folderList }: FolderListData) => {
const [link, setLink] = useState("");

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
setLink(e.target.value);
};

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
await postLink({ url: value, folderId });
// postLink 하고 추가된 link가 보이도록 하는 로직 구현해야 함.
const handleClick = () => {
// Addmodal 띄우면서 link 전달
};

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
handleClick();
}
};

return (
<form
onSubmit={handleSubmit}
className="flex gap-[12px] items-center w-[800px] h-[69px] py-[16px] px-[20px] border border-blue-500 rounded-[10px] md:w-[704px] sm:w-[325px] sm:h-[53px] transition-all"
>
<div className="flex bg-white gap-[12px] items-center w-[800px] h-[69px] py-[16px] px-[20px] border border-blue-500 rounded-[10px] md:w-[704px] sm:w-[325px] sm:h-[53px] transition-all">
<Image src="/icons/link.svg" width={20} height={20} alt="link icon" />
<input
onChange={handleChange}
value={value}
onKeyDown={handleKeyDown}
value={link}
placeholder="링크를 추가해 보세요."
className="flex-grow"
/>
<SubmitButton color="positive" className="w-[80px] h-[37px]">
추가하기
</SubmitButton>
</form>
<div onClick={handleClick}>
<SubmitButton className="w-[80px] h-[37px]">추가하기</SubmitButton>
</div>
</div>
);
};

Expand Down
25 changes: 12 additions & 13 deletions components/LinkCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@ import { useState } from "react";
import timeAgo from "@/util/timAgo";
import Image from "next/image";

interface linkDataType {
id: number;
title: string;
description: string;
favorite?: boolean;
imageSource: string;
url: string;
createdAt: string;
interface LinkCardProps {
info: {
id: number;
title: string;
description: string;
favorite?: boolean;
imageSource: string;
url: string;
createdAt: string;
};
isFavoritePage?: boolean;
}

interface CardItemProps extends linkDataType {
isFavoritePage?: boolean; // 즐겨찾기 페이지 여부를 판별하는 flag
}

const LinkCard = ({ isFavoritePage, ...info }: CardItemProps) => {
const LinkCard = ({ isFavoritePage, info }: LinkCardProps) => {
const [isSubscribed, setIsSubscribed] = useState(false);
const [isOpen, setIsOpen] = useState(false);

Expand Down
55 changes: 32 additions & 23 deletions pages/api/folders/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import axiosInstance from "@/lib/api/axiosInstanceApi";
import axios from "axios";
import { NextApiRequest, NextApiResponse } from "next";
import { parse } from "cookie";
import axiosInstance from "@/lib/api/axiosInstanceApi";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const token = req.cookies.accessToken;
console.log("Token:", token); // 쿠키 확인
const cookies = parse(req.headers.cookie || "");
const accessToken = cookies.accessToken;

if (!token) {
return res.status(401).json({ error: "사용자 정보를 찾을 수 없습니다." });
}
switch (req.method) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch문을 사용하는게 코드가 더 정리된 느낌이네요 ! 👍

case "GET":
// 유저의 모든 폴더 조회
try {
const response = await axiosInstance.get("/folders", {
headers: { Authorization: `Bearer ${accessToken}` },
});
return res.status(201).json(response.data);
} catch (err) {
console.error(err);
return res
.status(500)
.json({ message: "모든 폴저 조회에 실패했습니다." });
}

if (req.method === "POST") {
try {
await axiosInstance.post("/folders", req.body, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return res.status(200).json({ message: "폴더 생성 성공" });
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
const status = error.response.status;
const message = error.response.data?.message || "알 수 없는 오류 발생";
return res.status(status).json({ message });
case "POST":
// 유저의 폴더 생성
try {
const response = await axiosInstance.post("/folders", req.body, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return res.status(201).json(response.data);
} catch (err) {
console.error(err);
return res.status(500).json({ message: "폴더 생성에 실패했습니다." });
}
}
} else {
res.status(405).json({ message: "허용되지 않은 접근 방법" });

default:
res.setHeader("Allow", ["GET", "POST"]);
return res.status(405).end(`메서드 ${req.method}는 허용되지 않습니다.`);
}
};

Expand Down
55 changes: 32 additions & 23 deletions pages/api/links/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import axiosInstance from "@/lib/api/axiosInstanceApi";
import axios, { isAxiosError } from "axios";
import { NextApiRequest, NextApiResponse } from "next";
import { parse } from "cookie";
import axiosInstance from "@/lib/api/axiosInstanceApi";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const token = req.cookies.accessToken;
console.log("Token:", token);
const cookies = parse(req.headers.cookie || "");
const accessToken = cookies.accessToken;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제거된 코드와 작성하신 코드에서 쿠키를 가져오는 방법의 차이가 뭔지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

차이 없습니다! 병합하다가 삭제된 것 같아요

if (!token) {
return res.status(401).json({ error: "사용자 정보를 찾을 수 없습니다." });
}
switch (req.method) {
case "GET":
// 유저의 전체 링크 조회
try {
const response = await axiosInstance.get("/links", {
headers: { Authorization: `Bearer ${accessToken}` },
});
return res.status(201).json(response.data);
} catch (err) {
console.error(err);
return res
.status(500)
.json({ message: "전체 링크 조회에 실패했습니다." });
}

if (req.method === "POST") {
try {
await axiosInstance.post("/links", req.body, {
headers: {
Authorization: `Bearer ${token}`,
},
});
return res.status(201).json({ message: "링크 추가 성공" });
} catch (error) {
if (isAxiosError(error) && error.response) {
const status = error.response.status;
const message = error.response.data?.message || "알 수 없는 오류 발생";
return res.status(status).json({ message });
case "POST":
// 링크 생성 로직
try {
const response = await axiosInstance.post("/links", req.body, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return res.status(201).json(response.data);
} catch (err) {
console.error(err);
return res.status(500).json({ message: "링크 생성에 실패했습니다." });
}
}
} else {
res.status(405).json({ message: "허용되지 않은 접근 방법" });

default:
res.setHeader("Allow", ["GET", "POST"]);
return res.status(405).end(`메서드 ${req.method}는 허용되지 않습니다.`);
}
};

Expand Down
11 changes: 1 addition & 10 deletions pages/favorite/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,7 @@ const FavoritePage = ({ favoriteList, totalCount }: FavoriteProps) => {
<CardsLayout>
{favoriteList.length > 0
? favoriteList.map((favorite) => (
<LinkCard
key={favorite.id}
id={favorite.id}
url={favorite.url}
title={favorite.title}
imageSource={favorite.imageSource}
description={favorite.description}
createdAt={favorite.createdAt}
isFavoritePage={true}
/>
<LinkCard key={favorite.id} info={favorite} />
))
: null}
</CardsLayout>
Expand Down
75 changes: 75 additions & 0 deletions pages/link/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { GetServerSidePropsContext } from "next";
import { proxy } from "@/lib/api/axiosInstanceApi";
import { LinkData } from "@/types/linkTypes";
import { FolderData } from "@/types/folderTypes";
import { SearchInput } from "../../components/Search/SearchInput";
import Container from "@/components/Layout/Container";
import CardsLayout from "@/components/Layout/CardsLayout";
import ActionButtons from "@/components/Link/ActionButtons";
import AddLinkInput from "@/components/Link/AddLinkInput";
import FolderTag from "../../components/FolderTag";
import LinkCard from "../../components/LinkCard";

interface LinkPageProps {
linkList: LinkData[];
folderList: FolderData[];
}

export const getServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { req } = context;

const fetchData = async (endpoint: string) => {
const response = await proxy.get(endpoint, {
headers: {
Cookie: req.headers.cookie,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 여기서 헤더가 다르기때문에 쿠키를 불러오는 방식이 달라진거군요!
그렇다면 Authorization 헤더를 쓰는것과 어떻게 다른지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클라이언트 -> 서버로 요청을 보낼 때는 헤더에 쿠키가 자동으로 실어져 갑니다!

여기는 getServerSideProps 함수이니 서버 -> 서버로 요청을 보내는 것이라 헤더에 쿠키가 없습니다.

대책으로 context.req 객체에 접근해 cookie를 받아와 헤더에 직접적으로 실어주는 것이고요 (�get 요청 함수는 인자가 순서대로 url,body,header 입니다)

이렇게 하면 서버-> 서버라 할지라도 요청받는 서버에서 헤더에 쿠키가 실어져있으니 쿠키를 읽어 그 안에 token을 꺼내 요청을 보낼 수 있는 원리입니다.

},
});
return response.data;
};

const [links, folders] = await Promise.all([
fetchData("/api/links"),
fetchData("/api/folders"),
]);

return {
props: {
linkList: links.list || [],
folderList: folders || [],
},
};
};

const LinkPage = ({ linkList, folderList }: LinkPageProps) => {
return (
<>
<div className="bg-gray100 w-full h-[219px] flex justify-center items-center">
<AddLinkInput folderList={folderList} />
</div>
<main className="mt-[40px]">
<Container>
<SearchInput />
<div className="flex justify-between mt-[40px]">
{folderList && <FolderTag folderList={folderList} />}
<button className="w-[79px] h-[19px] text-purple100">
폴더 추가 +
</button>
</div>
<div className="flex justify-between items-center mt-[24px]">
<h1 className="text-2xl ">유용한 글</h1>
<ActionButtons />
</div>
<CardsLayout>
{linkList.map((link) => (
<LinkCard key={link.id} info={link} />
))}
</CardsLayout>
</Container>
</main>
</>
);
};

export default LinkPage;
8 changes: 8 additions & 0 deletions public/icons/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading