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

[fix/#152]:랜딩페이지 #154

Merged
merged 22 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
105 changes: 61 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,69 +1,84 @@
## 🧑‍💻 프로젝트 소개
### 우리들의 행복한 시간 ⏰

### 우리들의 행복한 시간 ⏰

> 우리FIS 아카데미 교육생들을 대상으로 공부한 시간을 측정하여 학습 기록을 확인할 수 있는 서비스

[우행시에 접속하고 싶다면? 클릭해주세요! 👀](https://woohangshi.vercel.app/)
- 테스트 유저 ID: [email protected]
[우행시에 접속하고 싶다면? 클릭해주세요! 👀](https://woohangshi.vercel.app/)

- 테스트 유저 ID: [email protected]
- 테스트 유저 PW: test1234!

<br/>

## 👻 팀원구성

| <img src="https://github.com/riverkite0708.png" width="200" /> | <img src="https://github.com/Kong-E.png" width="200" /> | <img src="https://github.com/doyi0107.png" width="200" /> |
|:---:|:---:|:---:|
| [강재연](https://github.com/riverkite0708) | [공소연](https://github.com/Kong-E) | [이도이](https://github.com/doyi0107) |
| 프론트엔드 | 프론트엔드 | 프론트엔드 |
| UI 가이드라인, <br /> 기록확인, 마이페이지 구현 | 인증/인가, 공부하기 구현, <br /> API 설정 | 랜딩 페이지, 과목선택, <br /> 순위조회 구현 |
| :------------------------------------------------------------: | :-----------------------------------------------------: | :-------------------------------------------------------: |
| [강재연](https://github.com/riverkite0708) | [공소연](https://github.com/Kong-E) | [이도이](https://github.com/doyi0107) |
| 프론트엔드 | 프론트엔드 | 프론트엔드 |
| UI 가이드라인, <br /> 기록확인, 마이페이지 구현 | 인증/인가, 공부하기 구현, <br /> API 설정 | 랜딩 페이지, 과목선택, <br /> 순위조회 구현 |

| <img src="https://github.com/khwoowoo.png" width="200" /> | <img src="https://github.com/rlfrkdms1.png" width="200" /> | <img src="https://github.com/qbobl5.png" width="200" /> | <img src="https://github.com/yaejinkong.png" width="200" /> |
|:---:|:---:|:---:|:---:|
| [강현우(팀장)](https://github.com/khwoowoo) | [길가은](https://github.com/rlfrkdms1) | [김혜빈](https://github.com/qbobl5) | [공예진](https://github.com/yaejinkong) |
| 백엔드 | 백엔드 | 백엔드 | 백엔드 |
| 클라우드 인프라 구축, <br /> 랭킹 API 개발 | 인증/인가, 이메일 인증 기능, <br /> 공부시간 기록 API 개발 | CI 환경 구축, <br /> 타이머 조회 및 캘린더 API 개발 | 과목 및 회원 정보 API 개발 |
| :-------------------------------------------------------: | :--------------------------------------------------------: | :-----------------------------------------------------: | :---------------------------------------------------------: |
| [강현우(팀장)](https://github.com/khwoowoo) | [길가은](https://github.com/rlfrkdms1) | [김혜빈](https://github.com/qbobl5) | [공예진](https://github.com/yaejinkong) |
| 백엔드 | 백엔드 | 백엔드 | 백엔드 |
| 클라우드 인프라 구축, <br /> 랭킹 API 개발 | 인증/인가, 이메일 인증 기능, <br /> 공부시간 기록 API 개발 | CI 환경 구축, <br /> 타이머 조회 및 캘린더 API 개발 | 과목 및 회원 정보 API 개발 |

<br/>

## ⭐ 프로젝트 주요 기능

준비중 입니다.

<br/>

## ⚙️ 시스템 아키텍처
### 프로젝트 구조도
![프로젝트 구조도](https://github.com/user-attachments/assets/97c151ee-4875-4c52-b446-0f5b88dabaaa)
---
### 인프라 구조도

### 프로젝트 구조도

## ![프로젝트 구조도](https://github.com/user-attachments/assets/97c151ee-4875-4c52-b446-0f5b88dabaaa)

### 인프라 구조도

![인프라 구조도](https://github.com/user-attachments/assets/81017061-93c3-4720-9abc-eafc10874017)

<br/>

## 📚 기술 스택
### Common

### Common

![Notion](https://img.shields.io/badge/Notion-eeeeee.svg?style=flat-square&logo=notion&logoColor=000000)
![GitHub](https://img.shields.io/badge/Github-%23121011.svg?style=flat-square&logo=github&logoColor=white)
![Postman](https://img.shields.io/badge/Postman-FF6C37?style=flat-square&logo=postman&logoColor=white)

### UI/UX
### UI/UX

![Radix UI](https://img.shields.io/badge/Radix%20ui-161618.svg?style=flat-square&logo=radix-ui&logoColor=white)
![figma](https://img.shields.io/badge/Figma-f24e1e?style=flat-square&logo=figma&logoColor=ffffff)

### Frontend
### Frontend

![Next JS](https://img.shields.io/badge/Next.js-black?style=flat-square&logo=next.js&logoColor=white)
![Zustand](https://img.shields.io/badge/Zustand-orange?style=flat-square&logo=zustand&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-%23007ACC.svg?style=flat-square&logo=typescript&logoColor=white)
![authjs](https://img.shields.io/badge/Auth.js-1eabf4?style=flat-square&logo=nextauth&logoColor=black)
![SWR](https://img.shields.io/badge/SWR-black.svg?style=flat-square&logo=swr&logoColor=white)
![SWR](https://img.shields.io/badge/SWR-black.svg?style=flat-square&logo=swr&logoColor=white)
![ESLint](https://img.shields.io/badge/ESLint-4B3263?style=flat-square&logo=ESLint&logoColor=white)
![Prettier](https://img.shields.io/badge/Prettier-%23F7B93E.svg?style=flat-square&logo=prettier&logoColor=black)
![Vercel](https://img.shields.io/badge/Vercel-000000?style=flat-square&logo=Vercel&logoColor=white)

### Backend
### Backend

<img src="https://img.shields.io/badge/SpringBoot-6DB33F?style=flat-square&logo=SpringBoot&logoColor=white"> <img src="https://img.shields.io/badge/SpringDataJpa-6DB33F?style=flat-square&logo=SpringDataJpat&logoColor=white"> <img src="https://img.shields.io/badge/QueryDsl-137CBD?style=flat-square&logo=QueryDsl&logoColor=white"> <img src="https://img.shields.io/badge/Gradle-02303A?style=flat-square&logo=Gradle&logoColor=white"> <img src="https://img.shields.io/badge/Swagger-85EA2D?style=flat-square&logo=Swagger&logoColor=white"> <img src="https://img.shields.io/badge/JWT-black?style=flat-square&logo=JSON%20web%20tokens">

### Infra & DB
### Infra & DB

<img src="https://img.shields.io/badge/MySQL-4479A1?style=flat-square&logo=MySQL&logoColor=white"> <img src="https://img.shields.io/badge/AmazonEC2-FF9900?style=flat-square&logo=AmazonEC2&logoColor=white"> <img src="https://img.shields.io/badge/AmazonRDS-527FFF?style=flat-square&logo=AmazonRDS&logoColor=white"> <img src="https://img.shields.io/badge/AmazonS3-569A31?style=flat-square&logo=AmazonS3&logoColor=white"> <img src="https://img.shields.io/badge/Redis-DC382D?style=flat-square&logo=Redis&logoColor=white">

### CI/CD
### CI/CD

<img src="https://img.shields.io/badge/GithubActions-2088FF?style=flat-square&logo=GithubActions&logoColor=white"> <img src="https://img.shields.io/badge/AmazonElasticBeanstalk-yellow?style=flat-square&logo=AmazonAWS&logoColor=white" alt="Amazon Elastic Beanstalk Status" />

<br/>
Expand All @@ -77,18 +92,18 @@
│   ├── easteregg/ ▶️ 이스터에그 폴더
│   ├── icons/ ▶️ 아이콘 폴더
│   ├── imgs/ ▶️ 이미지 폴더
│   └── fonts/ 폰트 폴더
│   └── fonts/ 폰트 폴더
├── app/ ▶️ 앱 라우팅 폴더
│   ├── (auth)/ ▶️ 인증 인가 폴더
│   ├── actions/ ▶️ auth.js 함수 호출 폴더
│   ├── (auth)/ ▶️ 인증 인가 폴더
│   ├── actions/ ▶️ auth.js 함수 호출 폴더
│   ├── api/auth/[...nextauth]/ ▶️ auth.js 설정 폴더
│   ├── mypage/ ▶️ 마이페이지 폴더
│   ├── ranking/ ▶️ 순위조회 폴더
│   ├── record/ ▶️ 기록확인 폴더
│   ├── study/ ▶️ 공부시작 폴더
│   ├── page.tsx ▶️ root 경로 페이지
│   └── layout.tsx ▶️ root 경로 레이아웃 구조
├── components/ ▶️ 컴포넌트 폴더
│   ├── page.tsx ▶️ root 경로 페이지
│   └── layout.tsx ▶️ root 경로 레이아웃 구조
├── components/ ▶️ 컴포넌트 폴더
│   ├── common/ ▶️ 공통 컴포넌트 폴더
│   │   ├── Header/
│   │   │   ├── Header컴포넌트.tsx
Expand All @@ -99,15 +114,15 @@
│   │   ├── 컴포넌트.tsx
│   │   └── 컴포넌트.module.css
│   ├── 라우팅폴더명/컴포넌트.tsx
│   └── 라우팅폴더명/컴포넌트.module.css
│   └── 라우팅폴더명/컴포넌트.module.css
├── constants/
│   └── 상수명.ts
├── hooks/
│   └── 커스텀훅.ts
├── apis/
│   ├── instancs.ts ▶️ api 요청 기본 설정 파일
│   └── 도메인Api.ts
├── stores/ ▶️ Zustand Store 폴더
├── stores/ ▶️ Zustand Store 폴더
│   └── 도메인Store.ts
├── types/ ▶️ TypeScript Interface 설정 폴더
│   └── 도메인Type.ts
Expand All @@ -121,27 +136,29 @@
└── package.json ▶️ NPM 프로젝트 설정 파일
```

## 🎈 Commit 방법
## 🎈 Commit 방법

꼭 다음의 방법을 따라서 커밋할 필요는 없지만, 알아보기 쉽게하기 위함.
\
커밋의 제목은 타입을 기재 후 간단한 요약(명령조)을 기재 함.
\
본문 작성시 자세한 내용을 누구든 알아볼 수 있기 기재 함(어떻게 보다 **왜**에 초점을 맞춰 작성).
\
**타입은 다음과 같음.**
* feat : 새로운 기능 추가
* fix : 버그 수정
* docs : 문서 수정
* style : 코드 formatting, 세미콜론(;) 누락, 코드 변경이 없는 경우
* refactor : 코드 리팩터링
* test : 테스트 코드, 리팩터링 테스트 코드 추가(프로덕션 코드 변경 X)
* chore : 빌드 업무 수정, 패키지 매니저 수정(프로덕션 코드 변경 X)
* design : CSS 등 사용자 UI 디자인 변경
* comment : 필요한 주석 추가 및 변경
* rename : 파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우
* remove : 파일을 삭제하는 작업만 수행한 경우
* !BREAKING CHANGE : 커다란 API 변경의 경우
* !HOTFIX : 급하게 치명적인 버그를 고쳐야 하는 경우

- feat : 새로운 기능 추가
- fix : 버그 수정
- docs : 문서 수정
- style : 코드 formatting, 세미콜론(;) 누락, 코드 변경이 없는 경우
- refactor : 코드 리팩터링
- test : 테스트 코드, 리팩터링 테스트 코드 추가(프로덕션 코드 변경 X)
- chore : 빌드 업무 수정, 패키지 매니저 수정(프로덕션 코드 변경 X)
- design : CSS 등 사용자 UI 디자인 변경
- comment : 필요한 주석 추가 및 변경
- rename : 파일 혹은 폴더명을 수정하거나 옮기는 작업만인 경우
- remove : 파일을 삭제하는 작업만 수행한 경우
- !BREAKING CHANGE : 커다란 API 변경의 경우
- !HOTFIX : 급하게 치명적인 버그를 고쳐야 하는 경우

예시
`[feat/#이슈번호]: 타워 추가`
14 changes: 11 additions & 3 deletions apis/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@ import { redirect } from 'next/dist/server/api-utils';

interface RequestOptions {
headers?: Record<string, string>;
[key: string]: string | Record<string, string> | undefined;
isMultipart?: boolean; // 멀티파트 여부를 나타내는 속성 추가
body?: any; // 요청 본문에 대한 타입 정의
[key: string]: any; // 다른 속성 허용
}

const fetchInstance = async (url: string, options: RequestOptions = {}) => {
const session = await auth();
const accessToken = session?.user?.accessToken;

const headers: RequestOptions['headers'] = {
'Content-Type': 'application/json',
...options.headers,
};

// FormData인 경우 Content-Type 설정 제거
if (options.body instanceof FormData) {
delete headers['Content-Type'];
} else {
headers['Content-Type'] = 'application/json';
}

if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}
Expand All @@ -35,7 +43,7 @@ const fetchInstance = async (url: string, options: RequestOptions = {}) => {
console.error('Token Expired');
}
console.error('Fetch Error:', errorResponse);
return { error: errorResponse };
return response;
}

if (response.headers.get('Content-Type')?.includes('application/json')) {
Expand Down
11 changes: 11 additions & 0 deletions apis/memberApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { revalidatePath } from 'next/cache';
import { instance } from './instance';

// 유저정보 조회
Expand All @@ -9,6 +10,16 @@ export const getUserInfo = async () => {
return response;
};

// 유저정보 수정
export const patchUserInfo = async ({ name, course }: { name: string; course: string }) => {
const response = await instance(`members`, {
method: 'PATCH',
body: JSON.stringify({ name, course }),
});

return response;
};

// 비밀번호 업데이트
export const postPwUpdate = async ({ oldPassword, newPassword }: { oldPassword: string; newPassword: string }) => {
const response = await instance('members', {
Expand Down
2 changes: 1 addition & 1 deletion apis/rankingApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { instance } from './instance';
import { ApiResponse } from '../types/rankingType';
import rankingImg from '../assets/icons/ranking_profile_img.png';
import rankingImg from '@/public/imgs/ranking/ranking_profile_img.png';

//랭킹 조회 api
export const getMemberRanking = async ({
Expand Down
18 changes: 9 additions & 9 deletions app/font.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,40 @@
font-weight: 900;
font-display: swap;
src:
url('/font/Pretendard-Black.subset.woff2') format('woff2'),
url('/font/Pretendard-Black.subset.woff') format('woff');
url('/fonts/Pretendard-Black.subset.woff2') format('woff2'),
url('/fonts/Pretendard-Black.subset.woff') format('woff');
}

@font-face {
font-family: 'Pretendard';
font-weight: 700;
font-display: swap;
src: url('/font/Pretendard-Bold.subset.woff') format('woff');
src: url('/fonts/Pretendard-Bold.subset.woff') format('woff');
}

@font-face {
font-family: 'Pretendard';
font-weight: 500;
font-display: swap;
src:
url('/font/Pretendard-Medium.subset.woff2') format('woff2'),
url('/font/Pretendard-Medium.subset.woff') format('woff');
url('/fonts/Pretendard-Medium.subset.woff2') format('woff2'),
url('/fonts/Pretendard-Medium.subset.woff') format('woff');
}

@font-face {
font-family: 'Pretendard';
font-weight: 400;
font-display: swap;
src:
url('/font/Pretendard-Regular.subset.woff2') format('woff2'),
url('/font/Pretendard-Regular.subset.woff') format('woff');
url('/fonts/Pretendard-Regular.subset.woff2') format('woff2'),
url('/fonts/Pretendard-Regular.subset.woff') format('woff');
}

@font-face {
font-family: 'Pretendard';
font-weight: 100;
font-display: swap;
src:
url('/font/Pretendard-Thin.subset.woff2') format('woff2'),
url('/font/Pretendard-Thin.subset.woff') format('woff');
url('/fonts/Pretendard-Thin.subset.woff2') format('woff2'),
url('/fonts/Pretendard-Thin.subset.woff') format('woff');
}
67 changes: 67 additions & 0 deletions app/mypage/infoupdate/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';

import ClassRadioGroup from '@/components/auth/ClassRadioGroup';
import InputField from '@/components/auth/InputField';
import CommonButton from '@/components/common/CommonButton';
import { Box } from '@radix-ui/themes';
import { patchUserInfo } from '@/apis/memberApi';
import { useState, useTransition } from 'react';

export default function InfoUpdate() {
const [isPending, startTransition] = useTransition();
const [userInfo, setUserInfo] = useState({
name: '',
course: '',
});

const handleNameChange = (name: string) => {
setUserInfo({ ...userInfo, name });
};

const handleCourseChange = (value: string) => {
setUserInfo({ ...userInfo, course: value });
};

const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (userInfo.name.trim() === '') {
alert('수정할 이름을 입력해주세요.');
return;
}
if (userInfo.course === '') {
alert('수정할 과정을 선택해주세요.');
return;
}

// Transition 안에서 비동기 작업 시작
startTransition(() => {
patchUserInfo(userInfo); // 유저 정보 패치
localStorage.removeItem('userInfo'); // 로컬 스토리지에서 유저 정보 제거
});
};

return (
<Box className="form_box" p="1">
<form onSubmit={submitHandler}>
<Box className="row">
<InputField
label="이름"
id="user_name"
placeholder="이름을 입력해주세요."
value={userInfo.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleNameChange(e.target.value)}
/>
<Box mt="3" className="row">
<ClassRadioGroup course={userInfo.course} onChange={handleCourseChange} />
</Box>
<Box mt="5">
<CommonButton type="submit" style="dark_purple" disabled={isPending}>
{isPending ? '수정 중...' : '수정하기'}
</CommonButton>
</Box>
</Box>
</form>
</Box>
);
}
Loading
Loading