Skip to content

Commit

Permalink
[박상준] Week19 (#495)
Browse files Browse the repository at this point in the history
* fix: 로그인 유무에 의한 페이지 리다이렉션 설정

* fix: userContext 분리 provider 컴포넌트 생성, 커스텀훅 구현

* fix: react hook form 타입 지정

* fix: react-hook-form정상적용

* feat: 리액트 쿼리 설치 및 세팅

* refactor: user context 데이터 react-query로 변경

* refactor: 폴더 리스트 페이지 전체 로직 리팩토링

* feat: 폴더 리스트 리액트 쿼리로 변경

* feat: 링크 리스트 리액트 쿼리 적용

* feat: 검색 기능, debounce기능 추가

* refactor: 로그인전  user정보 리퀘스트 방지

* feat: 로그인 페이지 리액트 쿼리 적용

* refactor: 링크 공유 페이지 리팩토링

* feat: 회원가입 페이지 리액트 쿼리 적용 및 react-hook-form 리팩토링

* refactor: 로그인 페이지 버튼 disabled 옵션 추가 및 스타일 변경

* refactor: 로딩 컴포넌트 포탈에서 렌더링, 페이지 최상단에 위치설정

* refactor: 회원가입 버튼 disabled 옵션 추가 및 로그인 로직 수정

* feat: 폴더 추가 리액트 쿼리 적용, 생성시 refetch설정

* feat: 폴더이름 변경 리액트 쿼리 적용

* fix: api주소 변경으로 인한 코드 수정

* fix: 검색기능 수정

* feat: shared페이지 리액트 쿼리 적용

* fix: 카드 케밥 메뉴, 즐겨찾기 아이콘 설정

* feat: 링크 추가 기능 리액트 쿼리 적용 및 사용자 편의성 개선

* refactor: 카드 리스트 렌더링 오류, 디펜던시값 추가

* feat: 링크 삭제 기능 리액트 쿼리 적용

* feat: 링크 삭제 기능 리액트 쿼리 적용

* refactor: api 리스폰스 도착전 버튼 비활성화 설정

* feat: 링크 즐겨찾기 기능 구현 및 옵티미스틱 업데이트 구현

* refactor: 링크 데이터 로직 리팩토링

* refactor: 즐겨찾기 폴더 수정, 삭제 버튼 제거

* fix: 빌드 오류 캐시 데이터 타입 지정 및 공유 페이지 수정

* fix: 모달 외부 클릭, esc클릭시 모달 close, 케밥 close 옵션 수정

* fix: 링크 추가시 인풋 value 초기화

* feat: nextAuth 적용, 미들웨어 적용, 로그인 로직 리액트 쿼리 삭제

* refactor: 로그인 페이지 앱라우터로 리펙토링

* refactor: 회원가입 페이지 앱라우터로 마이그레이션

* refactor: 공유 페이지 앱라우터로 리팩토링

* fix: 폴더 공유 링크 수정

* fix: 링크 경로 수정
  • Loading branch information
sj0724 authored Jul 3, 2024
1 parent f791c80 commit 452ee5e
Show file tree
Hide file tree
Showing 68 changed files with 2,855 additions and 1,149 deletions.
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["prettier-plugin-tailwindcss"]
}
5 changes: 5 additions & 0 deletions app/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import '../styles/color.css';
@import '../styles/reset.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
18 changes: 18 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import QueryProvider from "@/ui/providers/queryProvider";
import "./global.css";
import Footer from "@/components/Footer/Footer";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<QueryProvider>{children}</QueryProvider>
<Footer />
</body>
</html>
);
}
129 changes: 129 additions & 0 deletions app/login/_components/loginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/Button/Button";
import { Controller, useForm } from "react-hook-form";
import { emailPattern } from "@/util/util";
import AuthInput from "@/components/Input/AuthInput";
import { signIn } from "next-auth/react";
import Image from "next/image";
import eyeIcon from "@/public/eye-on.svg";
import eyeOffIcon from "@/public/eye_off.svg";
import { useRouter } from "next/navigation";

export interface FormValueType {
id: string;
password: string;
confirmPassword?: string;
}

export default function LoginForm() {
const [textHidden, setTextHidden] = useState(true);
const {
handleSubmit,
control,
setError,
formState: { isValid },
} = useForm<FormValueType>({ mode: "onChange" });
const router = useRouter();

const formAction = async (data: FormValueType) => {
const result = await signIn("credentials", {
id: data.id,
password: data.password,
redirect: false,
});
if (result?.ok) {
router.push("/folder");
} else if (result?.error) {
setError("id", {
type: "manual",
message: "아이디를 다시 확인해주세요!",
});
setError("password", {
type: "manual",
message: "비밀번호를 다시 확인해주세요!",
});
}
};

const hiddenText = () => {
setTextHidden(!textHidden);
};

return (
<form
className="flex flex-col justify-start gap-[1rem]"
onSubmit={handleSubmit(formAction)}
>
<div className="gap--[1.2rem] relative flex flex-col items-start">
<Controller
name="id"
control={control}
rules={{
required: "이메일을 입력해주세요!",
pattern: {
value: emailPattern,
message: "이메일 형식이 아닙니다!",
},
}}
render={({
field: { value, onChange, onBlur },
fieldState: { error },
}) => (
<AuthInput
id="email"
label="이메일"
value={value}
onChange={onChange}
onBlur={onBlur}
type="text"
placeholder="이메일"
size="md"
error={error}
/>
)}
/>
</div>
<div className="gap--[1.2rem] relative flex flex-col items-start">
<Controller
name="password"
control={control}
rules={{
required: "비밀번호를 입력해주세요!",
}}
render={({
field: { value, onChange, onBlur },
fieldState: { error },
}) => (
<AuthInput
id="password"
label="비밀번호"
value={value}
onChange={onChange}
onBlur={onBlur}
type={textHidden ? "password" : "text"}
placeholder="비밀번호"
size="md"
error={error}
/>
)}
/>
<div
className="absolute right-[1.5rem] top-[5.5rem] m-0 cursor-pointer border-none p-0"
onClick={hiddenText}
>
<Image
src={textHidden ? eyeOffIcon : eyeIcon}
width={16}
height={16}
alt="눈 아이콘"
/>
</div>
</div>
<Button size={"lg"} type="submit" buttonActive={!isValid}>
로그인
</Button>
</form>
);
}
57 changes: 57 additions & 0 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Image from "next/image";
import Link from "next/link";
import LoginForm from "./_components/loginForm";
import LogoIcon from "@/public/logo.svg";
import kakaoIcon from "@/public/Kakao.svg";
import googleIcon from "@/public/googleIcon.png";

export default function Login() {
return (
<div className="flex h-[100vh] flex-col items-center justify-center bg-[--Background] p-[3.2rem]">
<div className="flex flex-col content-center items-center gap-[3.2rem]">
<div className="flex flex-col items-center justify-center gap-[3rem]">
<div className="flex flex-col justify-center gap-[1.6rem]">
<Link href="/">
<Image src={LogoIcon} alt="메인로고" width={210} height={38} />
</Link>
<div className="flex items-center justify-center gap-[0.8rem]">
<span className="text-[1.6rem]">회원이 아니신가요?</span>
<Link href="/signup" style={{ textDecoration: "none" }}>
<p className="text-[1.6rem] font-[600] text-[--Primary]">
회원 가입하기
</p>
</Link>
</div>
</div>
<LoginForm />
</div>
<div className="flex w-[40rem] items-center justify-between rounded-[0.8rem] border border-solid border-[--Linkbrary-gray20] bg-[--Linkbrary-gray10] px-[2.4rem] py-[1.2rem]">
<p className="text-[1.4rem] font-[--Linkbrary-gray100]">
소셜 로그인
</p>
<div className="flex items-center justify-normal gap-[1.6rem]">
<Link
href="https://www.google.com/"
target="_blank"
className="flex h-[4.2rem] w-[4.2rem] items-center justify-center rounded-full bg-[--Section-white]"
>
<Image src={googleIcon} alt="구글아이콘" width={22} height={22} />
</Link>
<Link
href="https://www.kakaocorp.com/page/"
target="_blank"
className="flex h-[4.2rem] w-[4.2rem] items-center justify-center rounded-full bg-[--Kakao-logo-color]"
>
<Image
src={kakaoIcon}
alt="카카오아이콘"
width={26}
height={24}
/>
</Link>
</div>
</div>
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions app/sharedFolder/[[...folderId]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getFolderData, getFolderList, getUserData } from "@/service/api";
import SharedFolderInfo from "../_components/SharedFolderInfo";
import SharedFolderContents from "../_components/SharedFolderContents";

export default async function SharedFolder({
params,
}: {
params: { folderId: string };
}) {
const folderInfo = await getFolderData(params.folderId);
const folderLink = await getFolderList(params.folderId);
const linkList = folderLink?.data ?? [];
const owner = await getUserData(folderInfo?.data[0].user_id);

return (
<>
<SharedFolderInfo owner={owner.data[0]} folderInfo={folderInfo.data[0]} />
<div className="mx-auto my-0 flex max-w-[106rem] flex-col items-center justify-center py-[5rem]">
<SharedFolderContents list={linkList} />
</div>
</>
);
}
16 changes: 16 additions & 0 deletions app/sharedFolder/_components/SharedFolderContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Links } from "@/hooks/useGetFolder";
import SharedLinkItem from "./SharedLinkItem";

export default function SharedFolderContents({ list }: { list: Links }) {
return (
<div className="relative mx-auto my-0 grid gap-[2rem] sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{list.length > 0 ? (
list.map((item) => <SharedLinkItem item={item} key={item.id} />)
) : (
<div className="flex h-[50vh] flex-col items-center justify-center py-[8rem]">
저장된 링크가 없습니다.
</div>
)}
</div>
);
}
35 changes: 35 additions & 0 deletions app/sharedFolder/_components/SharedFolderInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { User } from "@/contexts/UserContext";
import Image from "next/image";

interface FolderInfoType {
id: number;
created_at: Date;
favorite: boolean;
name: string;
user_id: number;
}

export default function SharedFolderInfo({
owner,
folderInfo,
}: {
owner: User;
folderInfo: FolderInfoType;
}) {
return (
<div className="flex w-full flex-col items-center justify-center bg-[--Background] pb-[6rem] pt-[2rem]">
<Image
src={owner?.image_source}
alt="owner 이미지"
width={60}
height={60}
/>
<p className="mt-[1.2rem] h-[2.4rem] text-[1.6rem] font-[400] leading-[2.4rem]">
{owner?.name}
</p>
<p className="mt-[2rem] h-[5.5rem] text-[4rem] font-[600] leading-normal">
{folderInfo?.name}
</p>
</div>
);
}
46 changes: 46 additions & 0 deletions app/sharedFolder/_components/SharedLinkItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { changeDate, calculateDate } from "@/util/util";
import Image from "next/image";
import Link from "next/link";
import logo from "@/public/logo.svg";
import { LinkData } from "@/hooks/useGetFolder";

export default async function SharedLinkItem({ item }: { item: LinkData }) {
const { url, description, image_source, title } = item;
const nowDate = new Date();
const createDate = changeDate(new Date(item.created_at));
const date = calculateDate(
(Number(nowDate) - Number(new Date(item.created_at))) / 1000,
);

return (
<Link href={url} target="_blank" rel="noreferrer">
<div className="relative flex w-[34rem] flex-col items-center rounded-[1.5rem] text-[1.6rem] no-underline shadow-[0_5px_25px_0px_rgba(0,0,0,0.3)]">
<div className="flex h-[23.5rem] w-full flex-col items-center overflow-hidden rounded-[1.5rem_1.5rem_0_0]">
{image_source ? (
<div className="relative h-full w-full transition ease-in hover:scale-[170%]">
<Image
src={image_source}
alt="카드 이미지"
fill
objectFit="cover"
/>
</div>
) : (
<div>
<Image src={logo} alt="빈 이미지" width={133} height={24} />
</div>
)}
</div>
<div className="relative flex w-full flex-col gap-[1rem] rounded-b-[1.5rem] bg-[--Section-white] px-[2rem] py-[1.5rem]">
<p className="text-[1.3rem] text-[--Description]">
{date.time} {date.result} ago
</p>
<p className="overflow-hidden text-ellipsis whitespace-nowrap text-[1.4rem]">
{title ? title : description}
</p>
<p className="h-[2.4rem] text-[1.4rem]">{createDate}</p>
</div>
</div>
</Link>
);
}
23 changes: 23 additions & 0 deletions app/sharedFolder/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Image from "next/image";
import Link from "next/link";
import React from "react";
import Logo from "@/public/logo.svg";

export default function SharedFolderLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<div className="sticky top-0 z-10 w-full bg-[--Background] px-[3.2rem] py-[2rem] lg:px-[20rem]">
<div className="flex w-fit flex-col items-start justify-center">
<Link href="/folder">
<Image src={Logo} alt="메인 로고" width={133} height={24} />
</Link>
</div>
</div>
{children}
</div>
);
}
Loading

0 comments on commit 452ee5e

Please sign in to comment.