Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[노은수] week15 & 16 #1035

Open
wants to merge 7 commits into
base: part3-노은수
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 29 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
# 13주차
# 15 주차
## 기본 구현 사항
- [x] TypeScript를 활용해 프로젝트의 필요한 곳에 타입을 명시해 주세요.
- [x] 검색어를 입력하면 현재 폴더에 있는 링크들 중 “url”, “title”, “description”에 검색어가 포함된 링크들만 필터해서 보이게 해주세요.
- [x] x 버튼을 클릭하면 입력값이 없던 ui 상태로 돌아갑니다.
- [x] 로그인/회원가입시 성공 응답으로 받은 accessToken을 로컬 스토리지에 저장합니다.
- [x] 로그인/회원가입 페이지에 접근시 로컬 스토리지에 accessToken이 있는 경우 “/folder” 페이지로 이동합니다.
- [x] “회원 가입하기”를 클릭하면 ‘/signup’ 페이지로 이동합니다.
- [x] 이메일 input에 placeholder는 “이메일을 입력해 주세요.”비밀번호 input에 placeholder는 “비밀번호를 입력해 주세요.”로 설정해 주세요.
- [x] 이메일 input에서 focus out 할 때, 값이 없을 경우 아래에 “이메일을 입력해 주세요.” 에러 메세지를 보입니다.
- [x] 이메일 input에서 focus out 할 때, 이메일 형식에 맞지 않는 값이 있는 경우 아래에 “올바른 이메일 주소가 아닙니다.” 에러 메세지를 보입니다.
- [x] 비밀번호 input에서 focus out 할 때, 값이 없을 경우 아래에 “비밀번호를 입력해 주세요.” 에러 메세지를 보입니다.
- [x] 로그인 실패하는 경우, 이메일 input 아래에 “이메일을 확인해 주세요.”, 비밀번호 input 아래에 “비밀번호를 확인해 주세요.” 에러 메세지를 보입니다.
- [x] 로그인 버튼 클릭 또는 Enter키 입력으로 로그인 실행돼야 합니다.
- [x] https://bootcamp-api.codeit.kr/docs 에 명세된 “/api/sign-in”으로 { “email”: “[email protected]”, “password”: “sprint101” } POST 요청해서 성공 응답을 받으면 “/folder”로 이동합니다.
- [ ] 소셜 로그인에 구글 아이콘 클릭시 ‘https://www.google.com’카카오 아이콘 클릭시 ‘https://www.kakaocorp.com/page’로 이동하게 해주세요.
- [x] 눈 모양 아이콘 클릭시 비밀번호의 문자열이 보이기도 하고, 가려지기도 합니다.
- [x] 비밀번호의 문자열이 가려질 때는 눈 모양 아이콘에는 사선이 그어져있고, 비밀번호의 문자열이 보일 때는 사선이 없는 눈 모양 아이콘이 보이도록 합니다.

## 심화 구현 사항
- [ ] 상단에 있던 링크 추가하기 영역이 가려져 보이지 않을 때 최하단에 링크 추가하기 영역을 고정하도록 만들어 주세요.
- [ ] 푸터가 시작되는 지점에서는 최하단에 고정된 링크 추가하기 영역이 보이지 않게 해주세요.(IntersectionObserver를 활용해 보세요.)
- [x] 로그인, 회원가입 기능에 react-hook-form을 활용해 주세요.

# 14주차
# 16 주차
## 기본 구현 사항
- [x] 기존 React 프로젝트에서 진행했던 작업물을 Next.js 프로젝트에 맞게 변경 및 이전해 주세요.
- [x] next/link의 Link를 활용해 Linkbrary 아이콘을 클릭하면 ‘/’ 페이지로 이동하게 해주세요.
- [ ] 링크 공유 페이지의 url path를 ‘/shared’에서 ‘/shared/{folderId}’로 변경해 주세요.
- [ ] 폴더의 정보는 ‘/api/folders/{folderId}’, 폴더 소유자의 정보는 ‘/api/users/{userId}’를 활용해 주세요.
- [ ] 링크 공유 페이지에서 폴더의 링크 데이터는 ‘/api/users/{userId}/links?folderId={folderId}’를 사용해 주세요.
- [ ] 폴더 페이지에서 유저가 access token이 없는 경우 ‘/signin’페이지로 이동하게 해주세요.
- [ ] 테스트 유저는 id: “[email protected]”, pw: “sprint101” 를 활용해 보세요.
- [ ] 폴더 페이지의 url path가 ‘/folder’일 경우 폴더 목록에서 “전체” 가 선택되어 있고, ‘/folder/{folderId}’일 경우 폴더 목록에서 {folderId} 에 해당하는 폴더가 선택되어 있고 폴더에 있는 링크들을 볼 수 있게 해주세요.
- [ ] 폴더 페이지에서 현재 유저의 폴더 목록 데이터를 받아올 때 ‘/api/folders’를 활용해 주세요.
- [ ] 폴더 페이지에서 전체 링크 데이터를 받아올 때 ‘/api/links’, 특정 폴더의 링크를 받아올 때 ‘/api/links?folderId={folderId}’를 활용해 주세요.
- [ ] 유효한 access token이 있는 경우 ‘/api/users’로 현재 로그인한 유저 정보를 받아 상단 네비게이션 유저 프로필을 보이게 해주세요.

# css 적용
- [x] css 적용 방식을 선택한다. (css in js vs tailwind)
-[x] 테일윈드의 경우, 컴포넌트 단위부터 조금씩 바꾼다.
- [x] css in js를 사용한 경우, 그 이유를 분명히 한다.
- next는 css in js를 선호하지 않는다. 이를 고찰한다.

# app Router 적용
- [x] 앱 라우터를 적용한다.
- [x] pages를 제거한다.
- [x] pages의 _app.tsx, _documents.tsx 내용은 app/layout.tsx로 이전한다.
- [x] pages의 index.tsx는 app/page.tsx에 이전한다.
- [x] styles를 제거한다.
- [x] 만약, pr 머지 충돌이 일어날 경우, 원격 저장소의 내용을 위와 같은 절차로 제거한다.

# 서버 컴포넌트 사용
- [x] 데이터를 호출해야 하는 경우, 서버 컴포넌트로 분류한다.
- [x] 서버 컴포넌트는 클라이언트 컴포넌트에서 호출(import)이 불가능하다. -> {children}으로 호출한다. 이는, 비동기 렌더링을 적용하기 위함이다.
- [x] useState, useEffect와 같은 생명 주기, 상태를 가질 수 없다. 즉, 1번 렌더링 된다.
- [x] 클라이언트 컴포넌트와 서버 컴포넌트를 엄격히 분리한다.
## 심화 구현 사항
- [ ] 리퀘스트 헤더에 인증 토큰을 첨부할 때 axios interceptors 또는 이와 유사한 기능을 활용해 주세요.

# 클라이언트 컴포넌트 사용
- [x] 클라이언트 컴포넌트 내부에서는 데이터를 호출하지 않는다.
- [x] 컴포넌트 포함 관계를 통해, 서버에서 렌더링이 되고 있는지, 브라우저에서 렌더링이 되고 있는지를 분명히 구별한다. 이를 통해, 웹 사이트를 최적화한다.
## 추가 구현 사항
- [ ] search 부드럽게 적용하기. (spa로 이용)
49 changes: 45 additions & 4 deletions apis/api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
const API_URL = {
USER: "https://bootcamp-api.codeit.kr/api/users/1",
LINK: "https://bootcamp-api.codeit.kr/api/users/1/links",
FOLDER_LIST: "https://bootcamp-api.codeit.kr/api/users/1/folders",
SHARED: "https://bootcamp-api.codeit.kr/api/sample/folder",
USER_CHECK: "https://bootcamp-api.codeit.kr/api/check-email",
SIGN_IN: "https://bootcamp-api.codeit.kr/api/sign-in",
};

export async function getUser() {
const response = await fetch("https://bootcamp-api.codeit.kr/api/users/1");
const response = await fetch(API_URL.USER);
const body = await response.json();
return body;
}

export async function getLink(folderId: string | null) {
if (!folderId) {
const response = await fetch("https://bootcamp-api.codeit.kr/api/users/1/links");
const response = await fetch(API_URL.LINK);
const body = await response.json();
return body;
}
Expand All @@ -16,13 +25,45 @@ export async function getLink(folderId: string | null) {
}

export async function getFolderList() {
const response = await fetch("https://bootcamp-api.codeit.kr/api/users/1/folders");
const response = await fetch(API_URL.FOLDER_LIST);
const body = await response.json();
return body;
}

export async function getShared() {
const response = await fetch("https://bootcamp-api.codeit.kr/api/sample/folder");
const response = await fetch(API_URL.SHARED);
const body = await response.json();
return body;
}

export async function duplicationCheck(email: string) {
const response = await fetch(API_URL.USER_CHECK, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ email: email }),
});

if (response.ok === false) {
throw new Error("duplication");
}

return response;
}

export async function signIn(email: string, password: string) {
const response = await fetch(API_URL.SIGN_IN, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ email: email, password: password}),
});

if (response.ok === false) {
throw new Error("login failed");
}

return response;
}
26 changes: 26 additions & 0 deletions app/(sign)/layout.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "@/styles/global.scss";

.container {
display: flex;
justify-content: center;
min-height: 100vh;
padding: calc(120 / 844 * 100vh) 3.2rem 5rem;
background-color: $color-light-blue;

@include tablet {
padding-top: calc(200 / 982 * 100vh);
}

@include desktop {
padding-top: calc(238 / 982 * 100vh);
}
}

.items {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 3rem;
width: 100%;
max-width: 40rem;
}
16 changes: 16 additions & 0 deletions app/(sign)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import classNames from "classnames/bind";
import styles from "./layout.module.scss";

const cx = classNames.bind(styles);

const signLayout = ({ children }: { children: React.ReactNode }) => {
return (
<div className={cx("container")}>
<div className={cx("items")}>
{children}
</div>
</div>
);
};

export default signLayout;
18 changes: 18 additions & 0 deletions app/(sign)/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ROUTE } from "@/lib/constant";

import SignHeader from "@/components/sign/SignHeader";
import SignInForm from "@/components/sign/SignInForm";

const page = () => {
return (
<>
<SignHeader
message="회원이 아니신가요?"
link={{ text: "회원 가입하기", href: ROUTE.회원가입 }}
/>
<SignInForm />
</>
);
};

export default page;
18 changes: 18 additions & 0 deletions app/(sign)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ROUTE } from "@/lib/constant";

import SignHeader from "@/components/sign/SignHeader";
import SignUpForm from "@/components/sign/SignUpForm";

const page = () => {
return (
<>
<SignHeader
message="이미 회원이신가요?"
link={{ text: "로그인 하기", href: ROUTE.로그인 }}
/>
<SignUpForm />
</>
);
};

export default page;
1 change: 1 addition & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";

import './globals.css'
import "@/styles/reset.css";

const inter = Inter({ subsets: ["latin"] });

Expand Down
9 changes: 9 additions & 0 deletions app/shared/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

const page = () => {
return (
<div>page</div>
)
}

export default page
12 changes: 12 additions & 0 deletions components/sign/Cta.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import "@/styles/global.scss";

.container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 0.8rem;
background: linear-gradient(91deg, $color-primary 0.12%, #6ae3fe 101.84%);
color: $color-gray-light;
}
16 changes: 16 additions & 0 deletions components/sign/Cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReactNode } from "react";

import styles from "./Cta.module.scss";
import classNames from "classnames/bind";

const cx = classNames.bind(styles);

type CtaProps = {
children: ReactNode;
};

const Cta = ({ children }: CtaProps) => {
return <div className={cx("container")}>{children}</div>;
};

export default Cta
30 changes: 30 additions & 0 deletions components/sign/SignHeader.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import "@/styles/global.scss";

.container {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 1.6rem;
}

.logo {
width: fit-content;
height: 3.8rem;
}

.message-box {
display: flex;
column-gap: 0.8rem;
font-size: 1.6rem;
}

.message {
line-height: 150%;
}

.link {
height: fit-content;
font-weight: 600;
color: $color-primary;
border-bottom: solid 0.1rem $color-primary;
}
39 changes: 39 additions & 0 deletions components/sign/SignHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Image from "next/image";
import Link from "next/link";
import { Url } from "next/dist/shared/lib/router/router";

import LinkbraryIcon from "@/public/logo.svg";
import { ROUTE } from "@/lib/constant";

import classNames from "classnames/bind";
import styles from "./SignHeader.module.scss";

const cx = classNames.bind(styles);

type SignHeaderProps = {
message: string;
link: {
href: Url;
text: string;
};
};

const SignHeader = ({ message, link }: SignHeaderProps) => {
const { href, text } = link;

return (
<div className={cx("container")}>
<Link href={ROUTE.랜딩}>
<Image src={LinkbraryIcon} className={cx("logo")} alt="로고" />
</Link>
<p className={cx("message-box")}>
<span className={cx("message")}>{message}</span>
<Link className={cx("link")} href={href}>
{text}
</Link>
</p>
</div>
);
};

export default SignHeader;
23 changes: 23 additions & 0 deletions components/sign/SignInForm.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.form {
display: flex;
flex-direction: column;
row-gap: 2.4rem;
width: 100%;
}

.label {
font-size: 1.4rem;
}

.input-box {
display: flex;
flex-direction: column;
row-gap: 1.2rem;
}

.button {
margin-top: 0.6rem;
height: 5.4rem;
font-size: 1.8rem;
font-weight: 600;
}
Loading