Skip to content
Merged
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
111 changes: 90 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,96 @@
<img src="https://capsule-render.vercel.app/api?type=waving&color=F1EFFD&height=200&section=header&text=Taskify&fontSize=80&fontColor=5534DA&animation=fadeIn" />
<div align="center">
<img src="https://capsule-render.vercel.app/api?type=waving&color=F1EFFD&height=200&section=header&text=Taskify&fontSize=80&fontColor=5534DA&animation=fadeIn"/>
</div>

## 🖐️ 프로젝트 소개
<div align="center">
<img src="https://github.com/user-attachments/assets/73032ea5-93db-4a41-8f7a-9cdc7d6a0022" width="400px" height="300px"/>
</div>

**Taskify**는 일정 관리 기능을 지원하는 웹 애플리케이션입니다.
<div align="center">
<p>
<strong>Taskify</strong>는 일정 관리 기능을 지원하는 웹 애플리케이션입니다.</br>초대 기능을 활용하여 다른 사용자와 일정 관리를 공유할 수 있습니다.</br>다른 사람들과 일정을 공유하여 성공적으로 <strong>Task</strong>를 해결할 수 있습니다.
</p>
<nav>
<a href="#서비스소개">📅 서비스 소개</a></br>
<a href="#프로젝트구조">🔧 프로젝트 구조</a></br>
<a href="#진행과정">🔥진행과정에서 겪은 일</a></br>
<a href="#팀원소개">🧑 팀원소개</a>
</nav>
</div>

초대 기능을 활용하여 다른 사용자와 일정 관리를 공유할 수 있습니다.
<h2 id="서비스소개">📅 서비스 소개</h2>

다른 사람들과 일정을 공유하여 성공적으로 **Task**를 해결할 수 있습니다.
<h2 id="프로젝트구조">🔧 프로젝트 구조</h2>

## 🧑 팀원 소개
### ⚙️ API 흐름 도식화

<div align="center">
<img src="https://github.com/user-attachments/assets/a242c693-a908-41c9-9575-4070e991725f" alt="일반적인 API 흐름도" witdh="500px" height="300px"/></br>
<img src="https://github.com/user-attachments/assets/bdd08c59-29be-49f1-8443-b450699b8207" alt="로그인 흐름도" witdh="500px" height="300px"/></br>
<img src="https://github.com/user-attachments/assets/fac06ef8-22fb-4812-8eae-09246269b700" alt="리다이렉트 흐름도" witdh="500px" height="300px"/>

</div>


### 🗂️ 디렉토리 구조

```bash
📂src
┣ 📜middleware.ts
┣ 📂apis
┃ ┣ 📂auth
┃ ┃ ┣ 📜index.ts
┃ ┃ ┣ 📜queries.ts
┃ ┃ ┗ 📜types.ts
┃ ┣ 📂cards
┃ ┃ ┣ 📜index.ts
┃ ┃ ┣ 📜queries.ts
┃ ┃ ┗ 📜types.ts
┃ ┗ ...
┣ 📂app
┃ ┣ 📜globals.css
┃ ┣ 📜layout.tsx
┃ ┣ 📂(after-login)
┃ ┃ ┣ 📂mydashboard
┃ ┃ ┗ ...
┃ ┗ 📂(before-login)
┃ ┣ 📂(auth)
┃ ┃ ┗ 📂login
┃ ┗ ...
┣ 📂assets
┣ 📂components
┃ ┣ 📂auth
┃ ┣ 📂ui
┃ ┗ ...
┣ 📂constants
┣ 📂fonts
┣ 📂hooks
┣ 📂stores
┣ 📂types
┃ ┗ 📜common.ts
┗ 📂utils

```

### 💎 주요 기술 스택

|기술 이름|선정 이유|
|---|---|
|![Static Badge](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white) |컴포넌트 기반 설계 방식인 리액트 라이브러리를 활용하여 SSR과 CSR를 혼합해서 사용하기 위해 선정한 프레임워크입니다.|
|![Static Badge](https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square)|Props 타입 지정으로 인한 런타임 오류 감소, vscode 자동 완성 기능 등 타입 안정성을 통한 코드 품질 개선을 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/React--Query-FF4154?style=for-the-badge&logo=react-query&logoColor=white)|UI 상태와 서버 상태를 분리하고, api 데이터에 대한 Promise를 집약적으로 관리하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/State%20Management-Zustand-FF9900?logo=zustand)|전역 UI 상태를 관리하기 위해 선정했습니다. 현재 프로젝트에서는 모달의 상태를 zustand로 관리합니다.|
|![Static Badge](https://img.shields.io/static/v1?style=for-the-badge&message=Axios&color=5A29E4&logo=Axios&logoColor=FFFFFF&label=)|axios instance의 interceptor 기능을 통한 중복 코드 최소화 등을 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/react--hook--form-EC5990?style=for-the-badge&logo=reacthookform&logoColor=white)|폼의 상태를 집약적으로 관리하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/-Zod-3E67B1?style=flat&logo=zod&logoColor=white)|폼의 유효성 정의 및 타입 추출이 용이하고, API request 타입정의 및 safeParse()메소드를 통한 API 응답 데이터 타입 검증을 하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/es_toolkit-0080FF?style=flat-square&logo=es_toolkit&logoColor=blue&style=for-the-badge)|유틸리티 기능을 선언형으로 작성함으로써 코드 가독성을 향상시키기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/Tailwind_CSS-grey?style=for-the-badge&logo=tailwind-css&logoColor=38B2AC)|유틸리티 클래스 사용으로 클래스 네이밍 고민 감소, 디자인 시스템이 미흡 시 유연한 대응이 가능하기에 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white)|Next.js와의 완벽한 통합 및 소규모 웹 애플리케이션에서 무료 플랜을 제공해주기 때문에 선정했습니다.|

<h2 id="진행과정">🔥진행과정에서 겪은 일</h2>

<h2 id="팀원소개">🧑 팀원 소개</h2>

<markdown-accessiblity-table>
<table align="center">
Expand Down Expand Up @@ -49,21 +131,8 @@
</table>
</markdown-accessiblity-table>

## 🗒️ 기능 소개


## 💎 기술 스택

|기술 이름|선정 이유|
|---|---|
|![Static Badge](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white) |리액트 서버 컴포넌트 활용, 서버 사이드 렌더링을 간편하게 사용하기 위해 선정한 프레임워크입니다.(App router)|
|![Static Badge](https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square)|Props 타입 지정으로 인한 잘못된 타입 사용 방지, vscode 자동 완성 기능 활용 등을 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/Tailwind_CSS-grey?style=for-the-badge&logo=tailwind-css&logoColor=38B2AC)|유틸리티 클래스 사용으로 클래스 네이밍 고민 감소, 일관된 스타일을 적용하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/react--hook--form-EC5990?style=for-the-badge&logo=reacthookform&logoColor=white)|client에서 validate 검사를 수월하게 관리하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/-Zod-3E67B1?style=flat&logo=zod&logoColor=white)|런타임 환경에서도 타입 검사를 수행하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/es_toolkit-0080FF?style=flat-square&logo=es_toolkit&logoColor=blue&style=for-the-badge)|유틸리티 기능을 선언형으로 작성함으로써 코드 가독성을 향상시키기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/react_datepicker-000000?style=flat-square&logo=react_datepicker&logoColor=white&style=for-the-badge)|날짜 선택 UI를 쉽게 적용하기 위해 선정했습니다.|
|![Static Badge](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white)|복잡한 setup 없이 자동으로 Next.js 프로젝트를 배포하기 위해 선정했습니다.|


<img src="https://capsule-render.vercel.app/api?type=waving&color=F1EFFD&height=200&section=footer&fontSize=80" />
<div align="center">
<img src="https://capsule-render.vercel.app/api?type=waving&color=F1EFFD&height=200&section=footer&fontSize=80" />
</div>
5 changes: 4 additions & 1 deletion src/app/(after-login)/dashboard/[id]/edit/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Page, PageInner } from '@/components/layout/Page';
import { CardSkeleton } from '@/components/ui/Card/Card';
import { GoBackSkeleton } from '@/components/ui/Link/GoBackLink';

export default function loading() {
return (
<Page>
<PageInner>
<span className='text-md text-gray-40'>대시보드 정보를 불러오는 중입니다.</span>
<GoBackSkeleton />
<CardSkeleton count={3} />
</PageInner>
</Page>
);
Expand Down
5 changes: 4 additions & 1 deletion src/app/(after-login)/mypage/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Page, PageInner } from '@/components/layout/Page';
import { CardSkeleton } from '@/components/ui/Card/Card';
import { GoBackSkeleton } from '@/components/ui/Link/GoBackLink';

export default function loading() {
return (
<Page>
<PageInner>
<span className='text-md text-gray-40'>내정보를 불러오는 중입니다.</span>
<GoBackSkeleton />
<CardSkeleton count={2} />
</PageInner>
</Page>
);
Expand Down
4 changes: 1 addition & 3 deletions src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import SubmitButton from '@/components/auth/SubmitButton';
import { LOGIN_FORM_PLACEHOLDER } from '@/constants/auth';
import { loginSchema, LoginFormData } from '@/apis/auth/types';
import { login } from '@/apis/auth';
import { useRouter } from 'next/navigation';
import useAlert from '@/hooks/useAlert';

export default function LoginForm() {
Expand All @@ -24,7 +23,6 @@ export default function LoginForm() {
},
});

const router = useRouter();
const alert = useAlert();

const onSubmit = async (loginFormData: LoginFormData) => {
Expand All @@ -33,7 +31,7 @@ export default function LoginForm() {
alert(response.message);
} else {
await alert('로그인이 완료되었습니다!');
router.replace('/mydashboard');
window.location.reload();
}
};

Expand Down
4 changes: 1 addition & 3 deletions src/components/dashboard-header/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';
import { motion } from 'motion/react';
import { useGetUser } from '@/apis/users/queries';
Expand All @@ -14,15 +13,14 @@ export default function Profile() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { data, isFetching } = useGetUser();
const alert = useAlert();
const router = useRouter();
const menuRef = useRef<HTMLDivElement | null>(null);
const queryClient = useQueryClient();

const handleLogout = async () => {
await axiosClientHelper.post('/auth/logout');
await alert('로그아웃 했습니다.');
queryClient.invalidateQueries();
router.replace('/');
window.location.reload();
};

useEffect(() => {
Expand Down
12 changes: 9 additions & 3 deletions src/components/dashboard/DetailTodo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,27 @@ import kebob from '@/assets/icons/kebab.svg';
import RoundChip from '../ui/Chip/RoundChip';
import { getErrorMessage } from '@/utils/errorMessage';
import { formatDate } from '@/utils/formatDate';
import { useColumnsQuery } from '@/apis/columns/queries';

interface DetailTodoProps {
card: Card;
onEdit: () => void;
}

const NO_IMAGE_BASE_URL = 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/taskify/task_image/';

const DetailTodo = forwardRef<ModalHandle, DetailTodoProps>(({ card }, ref) => {
const DetailTodo = forwardRef<ModalHandle, DetailTodoProps>(({ card, onEdit }, ref) => {
const alert = useAlert();
const confirm = useConfirm();
const router = useRouter();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

const formattedDueDate = formatDate(card.dueDate);

const { data: columnsData } = useColumnsQuery(card.dashboardId);
const columns = columnsData?.data ?? [];
const foundColumn = columns.find((col) => col.id === card.columnId);

const handleDeleteCard = async () => {
const userConfirmed = await confirm('이 카드를 삭제하시겠습니까?', {
buttons: {
Expand All @@ -55,7 +61,7 @@ const DetailTodo = forwardRef<ModalHandle, DetailTodoProps>(({ card }, ref) => {
};

const handleEditCard = () => {
alert('수정 모달 열기');
onEdit();
};

const handleXClick = () => {
Expand Down Expand Up @@ -121,7 +127,7 @@ const DetailTodo = forwardRef<ModalHandle, DetailTodoProps>(({ card }, ref) => {
</div>

<div className='flex items-center gap-3'>
<RoundChip label='To Do' />
<RoundChip label={foundColumn?.title || ''} />
<div className='h-5 w-[1px] bg-gray-30'></div>
{card.tags.map((tag) => (
<TagChip key={tag} label={tag} />
Expand Down
10 changes: 9 additions & 1 deletion src/components/dashboard/TodoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ import calendar from '@/assets/icons/calendar.svg';
import { useRef } from 'react';
import { ModalHandle } from '../ui/Modal/Modal';
import DetailTodo from '@/components/dashboard/DetailTodo';
import TodoEditModal from './TodoEditModal';

interface TodoCardProps {
card: Card;
}

export default function TodoCard({ card }: TodoCardProps) {
const detailTodoModalRef = useRef<ModalHandle>(null);
const editModalRef = useRef<ModalHandle>(null);
const formattedDate = formatDate(card.dueDate);

const handleOpenEditModal = () => {
detailTodoModalRef.current?.close();
editModalRef.current?.open();
};

return (
<>
<div
Expand All @@ -44,7 +51,8 @@ export default function TodoCard({ card }: TodoCardProps) {
</div>
</div>

<DetailTodo ref={detailTodoModalRef} card={card} />
<DetailTodo ref={detailTodoModalRef} card={card} onEdit={handleOpenEditModal} />
<TodoEditModal ref={editModalRef} card={card} />
</>
);
}
Loading