diff --git a/README.md b/README.md index 0887d3bb..5e4662e3 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,163 @@ + + # Coworkers -> 업무 배정·현황 공유 + 익명 롤링페이퍼 기능을 갖춘 협업 서비스 +### 팀원 + +| [@SanginJeong](https://github.com/SanginJeong) | [@KimWonSeon](https://github.com/KimWonSeon) | [@wlrnjs](https://github.com/wlrnjs) | +| :----------------------------------------------------------: | :---------------------------------------------------------: | :-----------------------------------------------------: | +| | | | +| **정상인 / PL** | **김원선 / FE** | **서지권 / FE** | + +## 기술 스택 + +### Core + +- **Framework**: Next.js 16.0.1 (App Router) +- **Language**: TypeScript 5.x +- **Styling**: Tailwind CSS 3.4.18 +- **State Management**: + - React Query 5.x (TanStack Query) + - Zustand 5.x +- **API**: Axios 1.13.1 +- **Animation**: + - Framer Motion 12.x + - GSAP 3.13.0 +- **UI Components**: + - React Calendar 6.x + - React Hot Toast 2.x + +### Development + +- **Documentation**: Storybook 10.x +- **Linting**: + - ESLint 9.x + - Prettier 3.6.2 +- **Husky**: + - Husky 9.x + - lint-staged 16.x +- **UI Testing**: + - Chromatic + - Storybook Test Runner + +## 주요 기능 + +### 인증 + +- JWT 기반 인증 +- 카카오 소셜 로그인 연동 +- 이메일/비밀번호 로그인 +- 자동 로그인 유지 + +### 상태 관리 + +- **서버 상태 관리**: React Query (TanStack Query) +- **클라이언트 상태 관리**: Zustand + +### 성능 최적화 + +- **이미지 최적화**: + - Next.js Image 컴포넌트를 활용한 자동 최적화 + - WebP 포맷 지원을 통한 용량 감소 + - Lazy Loading 적용 + +- **번들 최적화**: + - 코드 스플리팅 (Code Splitting) + - 동적 임포트(dynamic import)를 활용한 지연 로딩 + - Tree Shaking을 통한 미사용 코드 제거 + - Webpack 설정 최적화 (--webpack 플래그 사용) + +- **애니메이션**: + - GSAP: 스크롤 기반 애니메이션 구현 + - Framer Motion: 인터랙티브한 UI 컴포넌트 구현 + +### 테스트 + +- **시각적 테스팅**: Chromatic을 활용한 UI 컴포넌트 시각적 테스트 +- **컴포넌트 문서화**: Storybook을 활용한 컴포넌트 문서화 및 개발 +- **접근성 검사**: Storybook a11y 애드온을 활용한 접근성 점검 -## 🔗 참고 자료 +## 개발 환경 설정 -- 디자인(Figma): https://www.figma.com/design/d5ogtLVSv1m7e8kx1Lfamy/%5BCCC%5DCowokers?node-id=52-1213 -- API(Swagger): https://fe-project-cowokers.vercel.app/docs/#/ -- 노션(프로젝트 계획): 추가 예정 +```bash +# 저장소 클론 +git clone https://github.com/sprint18-4-4/Coworkers.git +cd Coworkers -## 👥 팀 +# 의존성 설치 +npm install -- 김원선 · 박신천 · 서지권 · 정상인 +# 환경 변수 설정 +cp .env.local.example .env.local +# .env.local 파일 수정 -## 🧰 기술 스택 +# 개발 서버 실행 +npm run dev +``` -- Framework: **Next.js (App Router)**, React, TypeScript -- 스타일: **Tailwind CSS** -- 상태/데이터: **React Query** -- 배포/스토리지: Vercel, AWS S3 -- 협업: Git & GitHub -- (선택) Storybook, 테스트 코드 +## 스크립트 -## 🎯 프로젝트 목표 +```json +{ + "dev": "next dev --webpack", + "build": "next build --webpack", + "start": "next start", + "lint": "eslint", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "chromatic": "npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN", + "build-all": "npm run build && npm run build-storybook" +} +``` -1. 익명 롤링페이퍼 작성/조회/댓글/좋아요 -2. React Query 기반 상태 처리/데이터 관리 -3. 사용자 편의 중심의 직관적 UI/UX -4. Next.js를 활용한 SPA 구현 및 UI 통일성 유지 +## 컨벤션 -## ✨ 주요 기능 +### Git 커밋 메시지 -- 상단 네비게이션/팀 참여 메뉴 -- 랜딩 분기(로그인 여부에 따라 팀 페이지 또는 로그인) -- 회원가입/로그인/비밀번호 재설정/간편가입(카카오) -- 팀 생성/수정/삭제/초대 링크/멤버 리스트 -- 할 일(Task) 목록/추가(한 번·매일·주·월)/상세/수정/삭제/완료 -- 마이 히스토리(일자별 완료 목록) -- 계정 설정(프로필 이미지/이름/비밀번호/탈퇴) -- 자유게시판(목록/베스트/검색/CRUD) +```plaintext +- feat: 새로운 기능 추가 +- fix: 버그 수정 +- design: UI/UX 및 스타일 변경 +- docs: 문서 작성 또는 수정 +- refactor: 코드 리팩토링 (기능 변화 없음) +- chore: 설정, 빌드, 패키지 등 유지보수 +- test: 테스트 코드 및 주석 추가/수정 +- hotfix: 긴급 버그 수정 +- review: 코드 리뷰 요청 +- performance: 성능 최적화 +- main: 메인 브랜치 관련 변경 (배포 등) +``` -## ✅ 요구사항(요약 체크리스트) +### 코드 스타일 -- 공통: 폰트/컬러 시스템 설정, 공용 컴포넌트, 반응형, React Query, Next.js, TS, (선택) Storybook/테스트 -- 네비게이션/프로필 메뉴: 팀 참여하기·마이 히스토리·계정 설정·로그아웃 -- 랜딩: ‘지금 시작하기’ → 로그인 상태 분기 -- 회원가입/로그인: 입력 검증 메시지/에러 처리/간편가입 플로우 -- 팀: 중복 검사·생성 후 이동·수정/삭제·초대 모달(링크 복사) -- 할 일: 목록/추가 모달·반복 옵션·유효성·상세 CRUD/완료 -- 마이 히스토리: 일자별 완료 목록 -- 계정 설정: 프로필 이미지/이름/비번 변경·회원 탈퇴 모달 -- 자유게시판: 전체/베스트(좋아요순)/검색(제목 부분일치)/CRUD +- 컴포넌트: PascalCase (예: `UserProfile.tsx`) +- util 함수: camelCase (예: `formatDate.ts`) +- 상수: UPPER_SNAKE_CASE (예: `API_ENDPOINT.ts`) -## 🗂️ 폴더 구조 +### 폴더 구조 -> 폴더 구조는 **추후 결정(TBD)**. App Router 기준으로 세팅 예정. +```plaintext +├── src +│ ├── api +│ │ ├── axios.ts +│ │ └── hooks.ts +│ ├── app +│ │ └── login +│ │ ├── _components +│ │ ├── _hooks +│ │ ├── _types +│ │ ├── _constants +│ │ └── page.tsx +│ ├── constants +│ │ └── 공통 상수 +│ ├── common +│ │ └── 공통 컴포넌트 +│ ├── hooks +│ │ └── 공통 훅 +│ ├── types +│ │ └── 공통 타입 +│ └── utils +│ └── 공통 유틸 +├── public +└── package.json +``` diff --git a/package-lock.json b/package-lock.json index 597fe670..9778992b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "coworkers", "version": "0.1.0", "dependencies": { + "@gsap/react": "^2.1.2", "@react-pdf/renderer": "^4.3.1", "@tanstack/react-query": "^5.90.5", "@tanstack/react-query-devtools": "^5.90.2", @@ -15,6 +16,8 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.19", + "framer-motion": "^12.23.24", + "gsap": "^3.13.0", "next": "16.0.1", "react": "19.2.0", "react-calendar": "^6.0.0", @@ -2542,6 +2545,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -9155,6 +9168,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -9459,6 +9499,12 @@ "dev": true, "license": "MIT" }, + "node_modules/gsap": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz", + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -11072,6 +11118,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index f5276c2d..6b2d2417 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build-all": "npm run build && npm run build-storybook" }, "dependencies": { + "@gsap/react": "^2.1.2", "@react-pdf/renderer": "^4.3.1", "@tanstack/react-query": "^5.90.5", "@tanstack/react-query-devtools": "^5.90.2", @@ -21,6 +22,8 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.19", + "framer-motion": "^12.23.24", + "gsap": "^3.13.0", "next": "16.0.1", "react": "19.2.0", "react-calendar": "^6.0.0", diff --git a/src/MOCK_DATA.ts b/src/MOCK_DATA.ts deleted file mode 100644 index 572350f4..00000000 --- a/src/MOCK_DATA.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { DateNumber, Day, TaskListItemType } from "./types"; -import { GetTaskDetailResponse } from "./api/axios/task/_types"; -import { TaskGroupItem } from "./api/axios/task/_types"; - -export const USER_MOCK_DATA = { - teamId: "1", - image: "/TEST_IMG/image-1.jpg", - nickname: "안해나", - updatedAt: "2025-11-07T03:00:00Z", - createdAt: "2025-05-01T10:00:00Z", - email: "asdf@example.com", - id: 1, - memberships: [ - { - group: { - teamId: "1", - updatedAt: "2025-11-01T12:00:00Z", - createdAt: "2025-05-01T09:30:00Z", - image: "/TEST_IMG/image-1.jpg", - name: "CodeIt", - id: 101, - }, - role: "admin", - userImage: "/TEST_IMG/image-1.jpg", - userEmail: "asdf@example.com", - userName: "안해나", - groupId: 101, - userId: 1, - }, - { - group: { - teamId: "1", - updatedAt: "2025-10-28T08:00:00Z", - createdAt: "2025-04-15T09:00:00Z", - image: "/TEST_IMG/image-1.jpg", - name: "CodeIt", - id: 102, - }, - role: "member", - userImage: "/TEST_IMG/image-1.jpg", - userEmail: "asdf@example.com", - userName: "안해나", - groupId: 102, - userId: 1, - }, - ], -}; - -export const SIDEBAR_MOCK_DATA = [ - { - title: "경영관리팀", - }, - { - title: "프로덕트팀", - }, - { - title: "마케팅팀", - }, - { - title: "콘텐츠팀", - }, -]; - -export const MOBILE_SIDEBAR_MENU_MOCK_DATA = [ - { - menu: "경영관리팀", - href: "/", - }, - { - menu: "프로덕트팀", - href: "/test", - }, - { - menu: "마케팅팀", - href: "/test2", - }, - { - menu: "콘텐츠팀", - href: "/test3", - }, -]; - -export const MY_HISTORY_ITEM_MOCK_DATA: TaskListItemType[] = [ - { - id: 1, - name: "아침 운동하기", - description: "30분 러닝 또는 스트레칭", - date: "2025-11-14T09:00:00.000Z", - doneAt: "2025-11-14T09:30:00.000Z", - updatedAt: "2025-11-14T09:30:00.000Z", - deletedAt: null, - displayIndex: 0, - commentCount: 2, - recurringId: 101, - frequency: "DAILY", - writer: { - image: "/images/user1.png", - nickname: "지권", - id: 11, - }, - doneBy: { - user: { - image: "/images/user1.png", - nickname: "지권", - id: 11, - }, - }, - }, - { - id: 2, - name: "주간 회고 작성", - description: "이번 주 목표 대비 진행 상황 점검", - date: "2025-11-15T20:00:00.000Z", - doneAt: "2025-11-15T20:15:00.000Z", - updatedAt: "2025-11-15T20:15:00.000Z", - deletedAt: null, - displayIndex: 1, - commentCount: 0, - recurringId: 202, - frequency: "WEEKLY", - writer: { - image: "/images/user2.png", - nickname: "홍길동", - id: 12, - }, - doneBy: { - user: { - image: "/images/user2.png", - nickname: "홍길동", - id: 12, - }, - }, - }, -]; - -export const LIST_DATE_MOCK_DATA: { day: Day; date: DateNumber }[] = [ - { day: "월", date: 1 }, - { day: "화", date: 2 }, - { day: "수", date: 3 }, - { day: "목", date: 4 }, - { day: "금", date: 5 }, - { day: "토", date: 6 }, - { day: "일", date: 7 }, -]; - -export const COMMENT_MOCK_DATA = { - id: 1, - content: "lorem ipsum dolor sit amet consectetur adipisicing elit", - createdAt: "2024-07-29", - updatedAt: "2024-07-29", - user: { id: 1, nickname: "안해나", image: "" }, -}; - -export const TASK_DETAIL_MOCK_DATA: GetTaskDetailResponse = { - id: 0, - name: "회의록 정리하기", - description: "팀 회의에서 나온 내용을 정리하는 작업입니다.", - displayIndex: 0, - commentCount: 2, - - frequency: "DAILY", - recurringId: 0, - - date: "2025-11-21T20:27:10.529Z", - doneAt: "2025-11-21T20:27:10.529Z", - updatedAt: "2025-11-21T20:27:10.529Z", - deletedAt: null, - - doneBy: { - user: { - id: 1, - nickname: "짱구", - image: "/TEST_IMG/image-1.jpg", - }, - }, - - writer: { - id: 2, - nickname: "철수", - image: "/TEST_IMG/image-1.jpg", - }, - - recurring: { - id: 0, - name: "회의록 정리하기", - description: "팀 회의에서 나온 내용을 정리하는 작업입니다.", - createdAt: "2025-11-21T20:27:10.529Z", - updatedAt: "2025-11-21T20:27:10.529Z", - startDate: "2025-11-21T20:27:10.529Z", - frequencyType: "DAILY", - weekDays: [], - monthDay: null, - taskListId: 0, - groupId: 0, - writerId: 0, - }, -}; - -export const TASK_DETAIL_COMMENT_MOCK_DATA = [ - { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "짱구", - id: 1, - }, - userId: 1, - taskId: 101, - updatedAt: "2025-11-21T21:26:31.011Z", - createdAt: "2025-11-21T21:26:31.011Z", - content: "오늘 해야 할 작업 메모입니다.", - id: 1, - }, - { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "철수", - id: 2, - }, - userId: 2, - taskId: 102, - updatedAt: "2025-11-21T21:28:10.011Z", - createdAt: "2025-11-21T21:28:10.011Z", - content: "API 연결 테스트 완료했습니다.", - id: 2, - }, - { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "유리", - id: 3, - }, - userId: 3, - taskId: 103, - updatedAt: "2025-11-21T21:30:02.011Z", - createdAt: "2025-11-21T21:30:02.011Z", - content: "UI 최종 수정했습니다.", - id: 3, - }, - { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "맹구", - id: 4, - }, - userId: 4, - taskId: 104, - updatedAt: "2025-11-21T21:32:44.011Z", - createdAt: "2025-11-21T21:32:44.011Z", - content: "버그 리포트 정리했습니다.", - id: 4, - }, -]; - -export const TASK_GROUP_MOCK_DATA: TaskGroupItem[] = [ - { - doneBy: { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "string", - id: 1, - }, - }, - writer: { - image: "/TEST_IMG/image-1.jpg", - nickname: "짱구", - id: 1, - }, - displayIndex: 0, - commentCount: 0, - deletedAt: "2025-11-21T22:50:39.165Z", - recurringId: 0, - frequency: "DAILY", - updatedAt: "2025-11-21T22:50:39.165Z", - doneAt: "2025-11-21T22:50:39.165Z", - date: "2025-11-21T22:50:39.165Z", - description: "법인 설립 비용 안내 드리기", - name: "법인 설립 비용 안내 드리기", - id: 1, - }, - { - doneBy: { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "string", - id: 2, - }, - }, - writer: { - image: "/TEST_IMG/image-1.jpg", - nickname: "맹구", - id: 2, - }, - displayIndex: 1, - commentCount: 1, - deletedAt: "2025-11-21T22:50:39.165Z", - recurringId: 0, - frequency: "WEEKLY", - updatedAt: "2025-11-21T22:50:39.165Z", - doneAt: "2025-11-21T22:50:39.165Z", - date: "2025-11-22T22:50:39.165Z", - description: "법인 설립 비용 혹은 등기 비용 안내 드리기", - name: "법인 설립 비용 혹은 등기 비용 안내 드리기", - id: 2, - }, - { - doneBy: { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "string", - id: 3, - }, - }, - writer: { - image: "/TEST_IMG/image-1.jpg", - nickname: "철수", - id: 3, - }, - displayIndex: 2, - commentCount: 2, - deletedAt: "2025-11-21T22:50:39.165Z", - recurringId: 0, - frequency: "MONTHLY", - updatedAt: "2025-11-21T22:50:39.165Z", - doneAt: "2025-11-21T22:50:39.165Z", - date: "2025-11-23T22:50:39.165Z", - description: "법인 설립 비용 혹은 등기 비용 혹은 기타 비용 안내 드리기", - name: "법인 설립 비용 혹은 등기 비용 혹은 기타 비용 안내 드리기법인 설립 비용 혹은 등기 비용 혹은 기타 비용 안내 드리기법인 설립 비용 혹은 등기 비용 혹은 기타 비용 안내 드리기법인 설립 비용 혹은 등기 비용 혹은 기타 비용 안내 드리기", - id: 3, - }, - { - doneBy: { - user: { - image: "/TEST_IMG/image-1.jpg", - nickname: "string", - id: 4, - }, - }, - writer: { - image: "/TEST_IMG/image-1.jpg", - nickname: "유리", - id: 4, - }, - displayIndex: 3, - commentCount: 2, - deletedAt: "2025-11-21T22:50:39.165Z", - recurringId: 0, - frequency: "ONCE", - updatedAt: "2025-11-21T22:50:39.165Z", - doneAt: "2025-11-21T22:50:39.165Z", - date: "2025-11-24T22:50:39.165Z", - description: "법인 설립 비용 혹은 등기 비용 혹은 기타 비용 혹은 기타 비용 안내 드리기", - name: "법인 설립 비용 혹은 등기 비용 혹은 기타 비용 혹은 기타 비용 안내 드리기", - id: 4, - }, -]; diff --git a/src/api/axios/article/_type.ts b/src/api/axios/article/_type.ts index 6de0f63d..f37e2141 100644 --- a/src/api/axios/article/_type.ts +++ b/src/api/axios/article/_type.ts @@ -51,3 +51,22 @@ export interface DeleteArticleResponse { id?: number; message?: string; } + +export interface PostArticleRequest { + image?: string; + content: string; + title: string; +} + +export type PostArticleResponse = ArticleListItem; + +export interface PatchArticleRequest { + articleId: number; + body: { + image?: string | null; + content?: string; + title?: string; + }; +} + +export type PatchArticleResponse = ArticleDetail; diff --git a/src/api/axios/article/patchArticle.ts b/src/api/axios/article/patchArticle.ts new file mode 100644 index 00000000..63c7fed8 --- /dev/null +++ b/src/api/axios/article/patchArticle.ts @@ -0,0 +1,9 @@ +import { instance } from "@/lib"; +import { PatchArticleRequest, PatchArticleResponse } from "./_type"; + +const patchArticle = async ({ articleId, body }: PatchArticleRequest) => { + const { data } = await instance.patch(`/articles/${articleId}`, body); + return data; +}; + +export default patchArticle; diff --git a/src/api/axios/article/postArticle.ts b/src/api/axios/article/postArticle.ts new file mode 100644 index 00000000..5c8b8b6c --- /dev/null +++ b/src/api/axios/article/postArticle.ts @@ -0,0 +1,9 @@ +import { instance } from "@/lib"; +import { PostArticleRequest, PostArticleResponse } from "./_type"; + +const postArticle = async (body: PostArticleRequest) => { + const { data } = await instance.post("/articles", body); + return data; +}; + +export default postArticle; diff --git a/src/api/axios/group/_types/type.ts b/src/api/axios/group/_type.ts similarity index 77% rename from src/api/axios/group/_types/type.ts rename to src/api/axios/group/_type.ts index 25f2dd1c..b7aec370 100644 --- a/src/api/axios/group/_types/type.ts +++ b/src/api/axios/group/_type.ts @@ -35,3 +35,14 @@ export interface PatchGroupRequest { } export type PatchGroupResponse = Group; + +export interface GetInvitationRequest { + id: number; +} + +export type GetInvitationResponse = string; + +export interface DeleteMemberRequest { + id: number; + memberUserId: number; +} diff --git a/src/api/axios/group/deleteGroup/deleteGroup.ts b/src/api/axios/group/deleteGroup.ts similarity index 77% rename from src/api/axios/group/deleteGroup/deleteGroup.ts rename to src/api/axios/group/deleteGroup.ts index 72745ab3..dce6717e 100644 --- a/src/api/axios/group/deleteGroup/deleteGroup.ts +++ b/src/api/axios/group/deleteGroup.ts @@ -1,5 +1,5 @@ import { instance } from "@/lib"; -import { DeleteGroupRequest, DeleteGroupResponse } from "../_types/type"; +import { DeleteGroupRequest, DeleteGroupResponse } from "./_type"; const deleteGroup = async ({ id }: DeleteGroupRequest): Promise => { const { data } = await instance.delete(`groups/${id}`); diff --git a/src/api/axios/group/deleteMember.ts b/src/api/axios/group/deleteMember.ts new file mode 100644 index 00000000..de4d8ff5 --- /dev/null +++ b/src/api/axios/group/deleteMember.ts @@ -0,0 +1,9 @@ +import { instance } from "@/lib"; +import { DeleteMemberRequest } from "./_type"; + +const deleteMember = async ({ id, memberUserId }: DeleteMemberRequest) => { + const { data } = await instance.delete(`/groups/${id}/member/${memberUserId}`); + return data; +}; + +export default deleteMember; diff --git a/src/api/axios/group/getGroups/getGroups.ts b/src/api/axios/group/getGroups.ts similarity index 77% rename from src/api/axios/group/getGroups/getGroups.ts rename to src/api/axios/group/getGroups.ts index 594fae05..43bb23e5 100644 --- a/src/api/axios/group/getGroups/getGroups.ts +++ b/src/api/axios/group/getGroups.ts @@ -1,5 +1,5 @@ import { instance } from "@/lib"; -import { GetGroupsRequest, GetGroupsResponse } from "../_types/type"; +import { GetGroupsRequest, GetGroupsResponse } from "./_type"; const getGroups = async ({ id }: GetGroupsRequest): Promise => { const { data } = await instance.get(`/groups/${id}`); diff --git a/src/api/axios/group/getInvitation.ts b/src/api/axios/group/getInvitation.ts new file mode 100644 index 00000000..abd315bb --- /dev/null +++ b/src/api/axios/group/getInvitation.ts @@ -0,0 +1,9 @@ +import { instance } from "@/lib"; +import { GetInvitationRequest, GetInvitationResponse } from "./_type"; + +const getInvitation = async ({ id }: GetInvitationRequest) => { + const { data } = await instance.get(`/groups/${id}/invitation`); + return data; +}; + +export default getInvitation; diff --git a/src/api/axios/group/patchGroup/patchGroup.ts b/src/api/axios/group/patchGroup.ts similarity index 77% rename from src/api/axios/group/patchGroup/patchGroup.ts rename to src/api/axios/group/patchGroup.ts index 957f2f90..add75f8c 100644 --- a/src/api/axios/group/patchGroup/patchGroup.ts +++ b/src/api/axios/group/patchGroup.ts @@ -1,5 +1,5 @@ import { instance } from "@/lib"; -import { PatchGroupRequest, PatchGroupResponse } from "../_types/type"; +import { PatchGroupRequest, PatchGroupResponse } from "./_type"; const patchGroup = async ({ param, body }: PatchGroupRequest): Promise => { const { data } = await instance.patch(`/groups/${param.id}`, body); diff --git a/src/api/axios/index.ts b/src/api/axios/index.ts index 710ac80f..1b303843 100644 --- a/src/api/axios/index.ts +++ b/src/api/axios/index.ts @@ -1,9 +1,9 @@ -export { default as getGroups } from "./group/getGroups/getGroups"; +export { default as getGroups } from "./group/getGroups"; export { default as postLogin } from "./auth/login"; -export { default as getUser } from "./user/getUser/getUser"; +export { default as getUser } from "./user/getUser"; export { default as postResetPassword } from "./auth/resetPassword"; export { default as getHistory } from "./user/getHistory"; -export { default as deleteGroup } from "./group/deleteGroup/deleteGroup"; +export { default as deleteGroup } from "./group/deleteGroup"; export { default as getArticles } from "./article/getArticles"; export { default as getArticle } from "./article/getArticle"; export { default as postRecurring } from "./recurring/postRecurring"; @@ -14,7 +14,7 @@ export { default as getTaskDetail } from "./task/getTaskDetail"; export { default as patchTaskDetail } from "./task/patchTaskDetail"; export { default as postTaskList } from "./task-list/postTaskList"; export { default as deleteTaskList } from "./task-list/deleteTaskList"; -export { default as patchGroup } from "./group/patchGroup/patchGroup"; +export { default as patchGroup } from "./group/patchGroup"; export { default as postSignup } from "./auth/signup"; export { default as postCreateTeam } from "./team-creation/postCreateTeam"; export { default as postImageUpload } from "./image/postImageUpload"; @@ -24,12 +24,16 @@ export { default as deleteComment } from "./comment/deleteComment"; export { default as patchComment } from "./comment/patchComment"; export { default as postTeamJoin } from "./team-join/postTeamJoin"; export { default as patchResetPassword } from "./auth/patchResetPassword"; -export { default as patchUserProfile } from "./user/patchUser/patchUserProfile"; -export { default as patchUserPassword } from "./user/patchUserPassword/patchUserPassword"; -export { default as deleteUser } from "./user/deleteUser/deleteUser"; +export { default as patchUserProfile } from "./user/patchUserProfile"; +export { default as patchUserPassword } from "./user/patchUserPassword"; +export { default as deleteUser } from "./user/deleteUser"; +export { default as postArticle } from "./article/postArticle"; export { default as getArticleComments } from "./articleComment/getArticleComments"; export { default as postArticleComment } from "./articleComment/postArticleComment"; export { default as postArticleLike } from "./article/postArticleLike"; export { default as deleteArticle } from "./article/deleteArticle"; export { default as deleteArticleComment } from "./articleComment/deleteArticleComment"; export { default as patchArticleComment } from "./articleComment/patchArticleComment"; +export { default as getInvitation } from "./group/getInvitation"; +export { default as deleteMember } from "./group/deleteMember"; +export { default as patchArticle } from "./article/patchArticle"; diff --git a/src/api/axios/user/deleteUser/deleteUser.ts b/src/api/axios/user/deleteUser.ts similarity index 100% rename from src/api/axios/user/deleteUser/deleteUser.ts rename to src/api/axios/user/deleteUser.ts diff --git a/src/api/axios/user/getHistory.ts b/src/api/axios/user/getHistory.ts index 7cb45902..1890adf4 100644 --- a/src/api/axios/user/getHistory.ts +++ b/src/api/axios/user/getHistory.ts @@ -1,5 +1,5 @@ import { instance } from "@/lib"; -import { GetHistoryResponse } from "./_types/type"; +import { GetHistoryResponse } from "./type"; const getHistory = async (): Promise => { const response = await instance.get("/user/history"); diff --git a/src/api/axios/user/getUser/getUser.ts b/src/api/axios/user/getUser.ts similarity index 100% rename from src/api/axios/user/getUser/getUser.ts rename to src/api/axios/user/getUser.ts diff --git a/src/api/axios/user/patchUserPassword/patchUserPassword.ts b/src/api/axios/user/patchUserPassword.ts similarity index 94% rename from src/api/axios/user/patchUserPassword/patchUserPassword.ts rename to src/api/axios/user/patchUserPassword.ts index 8374f7e7..568996a0 100644 --- a/src/api/axios/user/patchUserPassword/patchUserPassword.ts +++ b/src/api/axios/user/patchUserPassword.ts @@ -1,5 +1,5 @@ import { instance } from "@/lib"; -import { PatchUserPasswordRequest, PatchUserPasswordResponse } from "../_types/type"; +import { PatchUserPasswordRequest, PatchUserPasswordResponse } from "./type"; const patchUserPassword = async (request: PatchUserPasswordRequest): Promise => { const { data } = await instance.patch("/user/password", request); diff --git a/src/api/axios/user/patchUser/patchUserProfile.ts b/src/api/axios/user/patchUserProfile.ts similarity index 94% rename from src/api/axios/user/patchUser/patchUserProfile.ts rename to src/api/axios/user/patchUserProfile.ts index 88a8d108..c0fd386a 100644 --- a/src/api/axios/user/patchUser/patchUserProfile.ts +++ b/src/api/axios/user/patchUserProfile.ts @@ -1,5 +1,5 @@ import { instance } from "@/lib"; -import { PatchUserProfileRequest, PatchUserProfileResponse } from "../_types/type"; +import { PatchUserProfileRequest, PatchUserProfileResponse } from "./type"; const patchUserProfile = async (request: PatchUserProfileRequest): Promise => { const { data } = await instance.patch("/user", request); diff --git a/src/api/axios/user/_types/type.ts b/src/api/axios/user/type.ts similarity index 100% rename from src/api/axios/user/_types/type.ts rename to src/api/axios/user/type.ts diff --git a/src/api/hooks/article/useGetArticlesInfinite.ts b/src/api/hooks/article/useGetArticlesInfinite.ts new file mode 100644 index 00000000..8e27b0d1 --- /dev/null +++ b/src/api/hooks/article/useGetArticlesInfinite.ts @@ -0,0 +1,35 @@ +"use client"; + +import { getArticles } from "@/api/axios"; +import { GetArticlesRequest } from "@/api/axios/article/_type"; +import { useInfiniteQuery } from "@tanstack/react-query"; + +type UseGetArticlesInfiniteParams = Omit & { + pageSize?: number; +}; + +const useGetArticlesInfinite = (params: UseGetArticlesInfiniteParams) => { + const { pageSize = 6, ...rest } = params; + + return useInfiniteQuery({ + queryKey: ["articles", rest], + queryFn: ({ pageParam = 1 }) => + getArticles({ + ...rest, + page: pageParam, + pageSize, + }), + initialPageParam: 1, + + getNextPageParam: (lastPage, allPages) => { + const loadedCount = allPages.reduce((acc, page) => acc + page.list.length, 0); + + return loadedCount < lastPage.totalCount ? allPages.length + 1 : undefined; + }, + + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 60 * 24, + }); +}; + +export default useGetArticlesInfinite; diff --git a/src/api/hooks/article/usePatchArticle.ts b/src/api/hooks/article/usePatchArticle.ts new file mode 100644 index 00000000..63983e4b --- /dev/null +++ b/src/api/hooks/article/usePatchArticle.ts @@ -0,0 +1,23 @@ +import { patchArticle } from "@/api/axios"; +import { toastKit } from "@/utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +const usePatchArticle = () => { + const { success, error } = toastKit(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ["patchArticle"], + mutationFn: patchArticle, + onSuccess: (data) => { + const articleId = data.id; + success("게시물을 성공적으로 수정하였습니다."); + queryClient.invalidateQueries({ queryKey: ["article", articleId] }); + }, + onError: () => { + error("게시물을 수정하지 못하였습니다."); + }, + }); +}; + +export default usePatchArticle; diff --git a/src/api/hooks/article/usePostArticle.ts b/src/api/hooks/article/usePostArticle.ts new file mode 100644 index 00000000..0452c011 --- /dev/null +++ b/src/api/hooks/article/usePostArticle.ts @@ -0,0 +1,23 @@ +import { postArticle } from "@/api/axios"; +import { toastKit } from "@/utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; + +const usePostArticle = () => { + const router = useRouter(); + const queryClient = useQueryClient(); + const { success, error } = toastKit(); + return useMutation({ + mutationFn: postArticle, + onSuccess: (data) => { + success("게시물 등록을 성공했습니다."); + queryClient.invalidateQueries({ queryKey: ["articles"] }); + router.replace(`/dashboard/${data.id}`); + }, + onError: () => { + error("게시물을 등록하지 못하였습니다."); + }, + }); +}; + +export default usePostArticle; diff --git a/src/api/hooks/auth/usePatchResetPassword.ts b/src/api/hooks/auth/usePatchResetPassword.ts index 36f9cbe2..91867b86 100644 --- a/src/api/hooks/auth/usePatchResetPassword.ts +++ b/src/api/hooks/auth/usePatchResetPassword.ts @@ -19,7 +19,7 @@ const usePatchResetPassword = (options?: UsePatchResetPasswordOptions) => { onSuccess: () => { success("비밀번호가 성공적으로 변경되었습니다."); options?.onSuccess?.(); - router.push("/login"); + router.replace("/login"); }, onError: (err: AxiosError) => { const message = err.response?.data?.message || err.message || "비밀번호 재설정에 실패했습니다."; diff --git a/src/api/hooks/auth/usePostLogin.ts b/src/api/hooks/auth/usePostLogin.ts index 2fb43b4c..96d14ff3 100644 --- a/src/api/hooks/auth/usePostLogin.ts +++ b/src/api/hooks/auth/usePostLogin.ts @@ -16,7 +16,7 @@ const usePostLogin = () => { tokenStorage.setAccessToken(data.accessToken); - router.push("/team"); + router.replace("/team"); }, onError: (error) => { console.error("로그인 실패", error); diff --git a/src/api/hooks/auth/usePostSignup.ts b/src/api/hooks/auth/usePostSignup.ts index ddb82f90..563d45f7 100644 --- a/src/api/hooks/auth/usePostSignup.ts +++ b/src/api/hooks/auth/usePostSignup.ts @@ -10,6 +10,7 @@ type ErrorResponse = { email?: { message: string; }; + nickname?: { message: string }; }; }; @@ -29,12 +30,15 @@ const usePostSignup = () => { tokenStorage.setAccessToken(data.accessToken); success("회원가입이 완료되었습니다!"); - router.push("/"); + router.replace("/"); }, onError: (err: AxiosError) => { const responseData = err.response?.data; - const errorMessage = responseData?.details?.email?.message || "회원가입에 실패했습니다. 다시 시도해주세요."; + const emailError = responseData?.details?.email?.message; + const nicknameError = responseData?.details?.nickname?.message; + + const errorMessage = nicknameError || emailError || "회원가입에 실패했습니다. 다시 시도해주세요."; error(errorMessage); }, diff --git a/src/api/hooks/group/useDeleteGroup.ts b/src/api/hooks/group/useDeleteGroup.ts index d0d3129c..6d0793c9 100644 --- a/src/api/hooks/group/useDeleteGroup.ts +++ b/src/api/hooks/group/useDeleteGroup.ts @@ -1,15 +1,22 @@ import { deleteGroup } from "@/api/axios"; import { toastKit } from "@/utils"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; const useDeleteGroup = () => { const { success, error } = toastKit(); const router = useRouter(); + const queryClient = useQueryClient(); return useMutation({ mutationFn: deleteGroup, onSuccess: () => { success("팀을 성공적으로 삭제 하였습니다."); + queryClient.invalidateQueries({ + queryKey: ["groups"], + }); + queryClient.invalidateQueries({ + queryKey: ["user"], + }); router.replace("/team"); }, onError: () => error("팀을 삭제하지 못하였습니다."), diff --git a/src/api/hooks/group/useDeleteMember.ts b/src/api/hooks/group/useDeleteMember.ts new file mode 100644 index 00000000..ad332e09 --- /dev/null +++ b/src/api/hooks/group/useDeleteMember.ts @@ -0,0 +1,21 @@ +import { deleteMember } from "@/api/axios"; +import { toastKit } from "@/utils"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +const useDeleteMember = () => { + const { success, error } = toastKit(); + const queryClient = useQueryClient(); + return useMutation({ + mutationKey: ["deleteMember"], + mutationFn: deleteMember, + onSuccess: () => { + success("팀원을 성공적으로 내보냈습니다."); + queryClient.invalidateQueries({ queryKey: ["groups"] }); + }, + onError: () => { + error("팀원을 내보내지 못했습니다."); + }, + }); +}; + +export default useDeleteMember; diff --git a/src/api/hooks/group/useGetGroups.ts b/src/api/hooks/group/useGetGroups.ts index 5fa902d6..73a7664f 100644 --- a/src/api/hooks/group/useGetGroups.ts +++ b/src/api/hooks/group/useGetGroups.ts @@ -1,5 +1,5 @@ import { getGroups } from "@/api/axios"; -import { GetGroupsRequest } from "../../axios/group/_types/type"; +import { GetGroupsRequest } from "../../axios/group/_type"; import { useQuery } from "@tanstack/react-query"; const useGetGroups = ({ id }: GetGroupsRequest) => { diff --git a/src/api/hooks/group/useGetInvitation.ts b/src/api/hooks/group/useGetInvitation.ts new file mode 100644 index 00000000..4487b299 --- /dev/null +++ b/src/api/hooks/group/useGetInvitation.ts @@ -0,0 +1,20 @@ +import { getInvitation } from "@/api/axios"; +import { toastKit } from "@/utils"; +import { useMutation } from "@tanstack/react-query"; + +const useGetInvitation = () => { + const { success, error } = toastKit(); + return useMutation({ + mutationKey: ["getInvitation"], + mutationFn: getInvitation, + onSuccess: (data) => { + navigator.clipboard.writeText(data); + success("클립보드에 성공적으로 복사하였습니다."); + }, + onError: () => { + error("초대 링크를 복사하지 못하였습니다."); + }, + }); +}; + +export default useGetInvitation; diff --git a/src/api/hooks/group/usePatchGroup.ts b/src/api/hooks/group/usePatchGroup.ts index 32be4f9b..f06e18c9 100644 --- a/src/api/hooks/group/usePatchGroup.ts +++ b/src/api/hooks/group/usePatchGroup.ts @@ -1,18 +1,16 @@ import { patchGroup } from "@/api/axios"; import { toastKit } from "@/utils"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useRouter } from "next/navigation"; const usePatchGroup = () => { const { success, error } = toastKit(); - const router = useRouter(); const queryClient = useQueryClient(); return useMutation({ mutationFn: patchGroup, onSuccess: () => { success("팀 이름을 성공적으로 변경하였습니다."); queryClient.invalidateQueries({ queryKey: ["groups"] }); - router.back(); + queryClient.invalidateQueries({ queryKey: ["user"] }); }, onError: () => { error("팀 이름을 변경하지 못했습니다."); diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts index 3c836267..25b919cd 100644 --- a/src/api/hooks/index.ts +++ b/src/api/hooks/index.ts @@ -1,7 +1,7 @@ export { default as usePostLogin } from "./auth/usePostLogin"; export { default as useGetUser } from "./user/useGetUser"; export { default as useGetGroups } from "./group/useGetGroups"; -export { default as useGetHistory } from "./my-history/useGetHistory"; +export { default as useGetHistory } from "./user/useGetHistory"; export { default as useGetTask } from "./task/useGetTask"; export { default as usePatchTask } from "./task/usePatchTask"; export { default as usePostRecurring } from "./recurring/usePostRecurring"; @@ -25,6 +25,7 @@ export { default as usePatchResetPassword } from "./auth/usePatchResetPassword"; export { default as usePatchUserProfile } from "./user/usePatchUserProfile"; export { default as usePatchUserPassword } from "./user/usePatchUserPassword"; export { default as useDeleteUser } from "./user/useDeleteUser"; +export { default as usePostArticle } from "./article/usePostArticle"; export { default as useGetArticleComments } from "./articleComment/useGetArticleComments"; export { default as usePostArticleComment } from "./articleComment/usePostArticleComment"; export { default as usePostArticleLike } from "./article/usePostArticleLike"; @@ -32,3 +33,6 @@ export { default as useDeleteArticleLike } from "./article/useDeleteArticleLike" export { default as useDeleteArticle } from "./article/useDeleteArticle"; export { default as useDeleteArticleComment } from "./articleComment/useDeleteArticleComment"; export { default as usePatchArticleComment } from "./articleComment/usePatchArticleComment"; +export { default as useGetInvitation } from "./group/useGetInvitation"; +export { default as useDeleteMember } from "./group/useDeleteMember"; +export { default as usePatchArticle } from "./article/usePatchArticle"; diff --git a/src/api/hooks/recurring/usePostRecurring.ts b/src/api/hooks/recurring/usePostRecurring.ts index a8a5db91..759b317f 100644 --- a/src/api/hooks/recurring/usePostRecurring.ts +++ b/src/api/hooks/recurring/usePostRecurring.ts @@ -16,7 +16,10 @@ const usePostRecurring = () => { const { groupId, taskListId } = variables; queryClient.invalidateQueries({ - queryKey: ["task-list", groupId, taskListId], + queryKey: ["task-list", String(groupId), String(taskListId)], + }); + queryClient.invalidateQueries({ + queryKey: ["groups", groupId], }); }, diff --git a/src/api/hooks/task-list/useDeleteTaskList.ts b/src/api/hooks/task-list/useDeleteTaskList.ts index 887ac29e..a8532355 100644 --- a/src/api/hooks/task-list/useDeleteTaskList.ts +++ b/src/api/hooks/task-list/useDeleteTaskList.ts @@ -15,7 +15,6 @@ const useDeleteTaskList = () => { success("할 일 삭제 성공"); queryClient.invalidateQueries({ - // TODO(지권): groupId 네이밍 변경 queryKey: ["groups", groupId], }); }, diff --git a/src/api/hooks/task-list/usePostTaskList.ts b/src/api/hooks/task-list/usePostTaskList.ts index 71560a4e..40d391d4 100644 --- a/src/api/hooks/task-list/usePostTaskList.ts +++ b/src/api/hooks/task-list/usePostTaskList.ts @@ -2,22 +2,26 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toastKit } from "@/utils"; import { postTaskList } from "@/api/axios"; import { PostTaskListRequest } from "@/api/axios/task-list/_types"; +import { useRouter } from "next/navigation"; const usePostTaskList = () => { + const router = useRouter(); const { success, error } = toastKit(); const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ groupId, name }: PostTaskListRequest) => postTaskList({ groupId, name }), - onSuccess: (_data, variables) => { + onSuccess: (data, variables) => { + const { id } = data; const { groupId } = variables; success("할 일 추가 성공"); - // TODO(지권): groupId 네이밍 변경 queryClient.invalidateQueries({ - queryKey: ["groups", groupId], + queryKey: ["groups", Number(groupId)], }); + + router.replace(`/team/${groupId}/task-list/${id}`); }, onError: () => { diff --git a/src/api/hooks/task/useDeleteTask.ts b/src/api/hooks/task/useDeleteTask.ts index 176abd0c..2ebb9ee8 100644 --- a/src/api/hooks/task/useDeleteTask.ts +++ b/src/api/hooks/task/useDeleteTask.ts @@ -27,7 +27,7 @@ const useDeleteTask = () => { queryKey: ["task-list-detail", groupId, taskListId, taskId], }); queryClient.invalidateQueries({ - queryKey: ["groups", groupId], + queryKey: ["groups", Number(groupId)], }); router.replace(`/team/${groupId}/task-list/${taskListId}`); diff --git a/src/api/hooks/task/usePatchTask.ts b/src/api/hooks/task/usePatchTask.ts index 30d568ab..c44bbae6 100644 --- a/src/api/hooks/task/usePatchTask.ts +++ b/src/api/hooks/task/usePatchTask.ts @@ -14,7 +14,6 @@ const usePatchTask = () => { const { groupId } = variables; success("할 일 수정 성공"); - // TODO(지권): groupId 네이밍 변경 queryClient.invalidateQueries({ queryKey: ["groups", groupId], }); diff --git a/src/api/hooks/task/usePatchTaskDetail.ts b/src/api/hooks/task/usePatchTaskDetail.ts index c3418fa8..b2439756 100644 --- a/src/api/hooks/task/usePatchTaskDetail.ts +++ b/src/api/hooks/task/usePatchTaskDetail.ts @@ -22,6 +22,15 @@ const usePatchTaskDetail = () => { queryClient.invalidateQueries({ queryKey: ["task-list-detail", groupId, taskListId, taskId], }); + queryClient.invalidateQueries({ + queryKey: ["groups", Number(groupId)], + }); + queryClient.invalidateQueries({ + queryKey: ["my-history"], + }); + queryClient.invalidateQueries({ + queryKey: ["groups", groupId], + }); }, onError: () => { diff --git a/src/api/hooks/team-creation/usePostCreateTeam.ts b/src/api/hooks/team-creation/usePostCreateTeam.ts index 15875ad5..70a67ccc 100644 --- a/src/api/hooks/team-creation/usePostCreateTeam.ts +++ b/src/api/hooks/team-creation/usePostCreateTeam.ts @@ -1,6 +1,6 @@ import { postCreateTeam } from "@/api/axios"; import { toastKit } from "@/utils"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { AxiosError } from "axios"; import { useRouter } from "next/navigation"; @@ -10,6 +10,7 @@ interface ErrorResponse { const usePostCreateTeam = () => { const router = useRouter(); + const queryClient = useQueryClient(); const { success, error } = toastKit(); return useMutation({ @@ -17,6 +18,8 @@ const usePostCreateTeam = () => { onSuccess: (data) => { success("팀 생성 완료"); router.push(`/team/${data.id}`); + + queryClient.invalidateQueries({ queryKey: ["user"] }); }, onError: (err: AxiosError) => { const message = err.response?.data?.message || err.message || "팀 생성에 실패했습니다."; diff --git a/src/api/hooks/team-join/usePostTeamJoin.ts b/src/api/hooks/team-join/usePostTeamJoin.ts index 79dbd6bb..a4a94b15 100644 --- a/src/api/hooks/team-join/usePostTeamJoin.ts +++ b/src/api/hooks/team-join/usePostTeamJoin.ts @@ -1,6 +1,6 @@ import { AxiosError } from "axios"; import { useRouter } from "next/navigation"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import postTeamJoin from "@/api/axios/team-join/postTeamJoin"; import { PostTeamJoinRequest, PostTeamJoinResponse } from "@/api/axios/team-join/_type/type"; import { toastKit } from "@/utils"; @@ -12,6 +12,7 @@ type UsePostTeamJoinOptions = { const usePostTeamJoin = (options?: UsePostTeamJoinOptions) => { const router = useRouter(); + const queryClient = useQueryClient(); const { success, error } = toastKit(); return useMutation({ @@ -20,6 +21,8 @@ const usePostTeamJoin = (options?: UsePostTeamJoinOptions) => { success("팀에 성공적으로 참여했습니다!"); options?.onSuccess?.(data); router.push(`/team/${data.groupId}`); + + queryClient.invalidateQueries({ queryKey: ["user"] }); }, onError: (errors) => { const axiosError = errors as AxiosError<{ message?: string }>; diff --git a/src/api/hooks/my-history/useGetHistory.ts b/src/api/hooks/user/useGetHistory.ts similarity index 100% rename from src/api/hooks/my-history/useGetHistory.ts rename to src/api/hooks/user/useGetHistory.ts diff --git a/src/app/(route)/_components/OverlayLoading/OverlayLoading.tsx b/src/app/(route)/_components/OverlayLoading/OverlayLoading.tsx new file mode 100644 index 00000000..dae4d1dd --- /dev/null +++ b/src/app/(route)/_components/OverlayLoading/OverlayLoading.tsx @@ -0,0 +1,11 @@ +import { LoadingSpinner } from "@/features"; + +const OverlayLoading = () => { + return ( +
+ +
+ ); +}; + +export default OverlayLoading; diff --git a/src/app/(route)/_components/index.ts b/src/app/(route)/_components/index.ts index a4bc81d4..47d7e846 100644 --- a/src/app/(route)/_components/index.ts +++ b/src/app/(route)/_components/index.ts @@ -1,2 +1,3 @@ export { default as CenteredCardLayout } from "./layouts/CenteredCardLayout"; export { default as SocialAuthSection } from "./SocialAuthSection/SocialAuthSection"; +export { default as OverlayLoading } from "./OverlayLoading/OverlayLoading"; diff --git a/src/app/(route)/_components/layouts/CenteredCardLayout.tsx b/src/app/(route)/_components/layouts/CenteredCardLayout.tsx index 11331965..d4105375 100644 --- a/src/app/(route)/_components/layouts/CenteredCardLayout.tsx +++ b/src/app/(route)/_components/layouts/CenteredCardLayout.tsx @@ -35,11 +35,11 @@ const CenteredCardLayout = ({ bottomContent, }: CenteredCardLayoutProps) => { return ( -
+
{ + const container = useRef(null); + + useGSAP( + () => { + gsap.from(".feature-block", { + scrollTrigger: { + trigger: ".feature-block", + start: "top bottom", + invalidateOnRefresh: true, + toggleActions: "play none none reverse", + }, + y: 0, + opacity: 0, + scale: 0.5, + duration: 0.8, + ease: "back.out(2.5)", + }); + }, + { scope: container }, + ); + return ( -